This commit is contained in:
Kay Faraday 2022-10-21 05:50:19 +00:00
commit 5012cb42ae
836 changed files with 17778 additions and 7134 deletions

View File

@ -20,7 +20,7 @@
"forwardPorts": [3000, 4000],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "bundle install --path vendor/bundle && yarn install && ./bin/rails db:setup",
"postCreateCommand": "bundle install --path vendor/bundle && yarn install && git checkout -- Gemfile.lock && ./bin/rails db:setup",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"

View File

@ -27,6 +27,7 @@ services:
ES_ENABLED: 'true'
ES_HOST: es
ES_PORT: '9200'
LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
networks:
@ -72,6 +73,12 @@ services:
soft: -1
hard: -1
libretranslate:
image: libretranslate/libretranslate:v1.2.9
restart: unless-stopped
networks:
- internal_network
volumes:
postgres-data:
redis-data:

View File

@ -1,254 +0,0 @@
# Service dependencies
# You may set REDIS_URL instead for more advanced options
REDIS_HOST=$DATA_REDIS_HOST
REDIS_PORT=6379
# REDIS_DB=0
# You may set DATABASE_URL instead for more advanced options
DB_HOST=$DATA_DB_HOST
DB_USER=$DATA_DB_USER
DB_NAME=gonano
DB_PASS=$DATA_DB_PASS
DB_PORT=5432
# 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
BIND=0.0.0.0
# Federation
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
LOCAL_DOMAIN=${APP_NAME}.nanoapp.io
# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
# Use this only if you need to run mastodon on a different domain than the one used for federation.
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING.
# WEB_DOMAIN=mastodon.example.com
# Use this if you want to have several aliases handler@example1.com
# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
# be added. Comma separated values
# ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets
# Generate each with the `rake secret` task (`nanobox run bundle exec rake secret`)
SECRET_KEY_BASE=$SECRET_KEY_BASE
OTP_SECRET=$OTP_SECRET
# VAPID keys (used for push notifications)
# You can generate the keys using the following command (first is the private key, second is the public one)
# You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe.
#
# Generate with `rake mastodon:webpush:generate_vapid_key` task (`nanobox run bundle exec rake mastodon:webpush:generate_vapid_key`)
#
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY=$VAPID_PRIVATE_KEY
VAPID_PUBLIC_KEY=$VAPID_PUBLIC_KEY
# Registrations
# Single user mode will disable registrations and redirect frontpage to the first profile
# SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
# Optionally change default language
# DEFAULT_LOCALE=de
# E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
# If you want to use an SMTP server without authentication (e.g local Postfix relay)
# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and
# *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough).
SMTP_SERVER=$SMTP_SERVER
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
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
# 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=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=http
# 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=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=
# 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=
# SWIFT_CONTAINER=
# SWIFT_OBJECT_URL=
# SWIFT_REGION=
# Defaults to 'default'
# SWIFT_DOMAIN_NAME=
# Defaults to 60 seconds. Set to 0 to disable
# SWIFT_CACHE_TTL=
# Optional alias for S3 (e.g. to serve files on a custom domain, possibly using Cloudfront or Cloudflare)
# S3_ALIAS_HOST=
# Streaming API integration
# STREAMING_API_BASE_URL=
# Advanced settings
# If you need to use pgBouncer, you need to disable prepared statements:
# PREPARED_STATEMENTS=false
# Cluster number setting for streaming API server.
# If you comment out following line, cluster number will be `numOfCpuCores - 1`.
# STREAMING_CLUSTER_NUM=1
# Docker mastodon user
# If you use Docker, you may want to assign UID/GID manually.
# UID=1000
# GID=1000
# LDAP authentication (optional)
# LDAP_ENABLED=true
# LDAP_HOST=localhost
# LDAP_PORT=389
# LDAP_METHOD=simple_tls
# LDAP_BASE=
# LDAP_BIND_DN=
# LDAP_PASSWORD=
# LDAP_UID=cn
# LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_
# PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable
# and optional as fallback PAM_DEFAULT_SUFFIX
# The pam environment variable "email" is provided by:
# https://github.com/devkral/pam_email_extractor
# PAM_ENABLED=true
# 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)
# PAM_CONTROLLED_SERVICE=rpam
# Optional CAS authentication (cf. omniauth-cas) :
# CAS_ENABLED=true
# CAS_URL=https://sso.myserver.com/
# CAS_HOST=sso.myserver.com/
# CAS_PORT=443
# CAS_SSL=true
# CAS_VALIDATE_URL=
# CAS_CALLBACK_URL=
# CAS_LOGOUT_URL=
# CAS_LOGIN_URL=
# CAS_UID_FIELD='user'
# CAS_CA_PATH=
# CAS_DISABLE_SSL_VERIFICATION=false
# CAS_UID_KEY='user'
# CAS_NAME_KEY='name'
# CAS_EMAIL_KEY='email'
# CAS_NICKNAME_KEY='nickname'
# CAS_FIRST_NAME_KEY='firstname'
# CAS_LAST_NAME_KEY='lastname'
# CAS_LOCATION_KEY='location'
# CAS_IMAGE_KEY='image'
# CAS_PHONE_KEY='phone'
# CAS_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true
# SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
# SAML_ISSUER=https://example.com
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
# SAML_IDP_CERT=
# SAML_IDP_CERT_FINGERPRINT=
# SAML_NAME_IDENTIFIER_FORMAT=
# SAML_CERT=
# SAML_PRIVATE_KEY=
# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
# 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.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

View File

@ -10,6 +10,9 @@ on:
paths:
- .github/workflows/build-image.yml
- Dockerfile
permissions:
contents: read
jobs:
build-image:
runs-on: ubuntu-latest

View File

@ -9,6 +9,9 @@ on:
env:
RAILS_ENV: test
permissions:
contents: read
jobs:
check-i18n:
runs-on: ubuntu-latest

View File

@ -55,7 +55,7 @@ jobs:
with:
node-version: 16.x
cache: yarn
- name: Intall dependencies
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Set-up RuboCop Problem Mathcher
uses: r7kamura/rubocop-problem-matchers-action@v1

1
.ruby-gemset Normal file
View File

@ -0,0 +1 @@
mastodon

View File

@ -1 +1 @@
3.0.3
3.0.4

View File

@ -5,7 +5,7 @@ SHELL ["/bin/bash", "-c"]
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
# Install Node v16 (LTS)
ENV NODE_VER="16.15.1"
ENV NODE_VER="16.17.1"
RUN ARCH= && \
dpkgArch="$(dpkg --print-architecture)" && \
case "${dpkgArch##*-}" in \
@ -19,7 +19,7 @@ RUN ARCH= && \
esac && \
echo "Etc/UTC" > /etc/localtime && \
apt-get update && \
apt-get install -y --no-install-recommends ca-certificates wget python apt-utils && \
apt-get install -y --no-install-recommends ca-certificates wget python3 apt-utils && \
cd ~ && \
wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \
@ -27,7 +27,7 @@ RUN ARCH= && \
mv node-v$NODE_VER-linux-$ARCH /opt/node
# Install Ruby 3.0
ENV RUBY_VER="3.0.3"
ENV RUBY_VER="3.0.4"
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \

28
Gemfile
View File

@ -7,7 +7,7 @@ gem 'pkg-config', '~> 1.4'
gem 'rexml', '~> 3.2'
gem 'puma', '~> 5.6'
gem 'rails', '~> 6.1.6'
gem 'rails', '~> 6.1.7'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.4'
@ -16,7 +16,7 @@ gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.4'
gem 'makara', '~> 0.5'
gem 'pghero', '~> 2.8'
gem 'dotenv-rails', '~> 2.7'
gem 'dotenv-rails', '~> 2.8'
gem 'aws-sdk-s3', '~> 1.114', require: false
gem 'fog-core', '<= 2.1.0'
@ -26,7 +26,7 @@ gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.12.0', require: false
gem 'bootsnap', '~> 1.13.0', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.2'
@ -46,16 +46,16 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1'
gem 'color_diff', '~> 0.1'
gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.5'
gem 'doorkeeper', '~> 5.6'
gem 'ed25519', '~> 1.3'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.8'
gem 'redis-namespace', '~> 1.9'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 5.1'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.5.0'
gem 'httplog', '~> 1.6.0'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
@ -91,8 +91,8 @@ gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2022'
gem 'webpacker', '~> 5.4'
gem 'webpush', '~> 0.3'
gem 'webauthn', '~> 3.0.0.alpha1'
gem 'webpush', git: 'https://github.com/ClearlyClaire/webpush.git', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9'
gem 'webauthn', '~> 2.5'
gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2'
@ -101,10 +101,10 @@ gem 'rdf-normalize', '~> 0.5'
gem 'redcarpet', '~> 3.5'
group :development, :test do
gem 'fabrication', '~> 2.29'
gem 'fabrication', '~> 2.30'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 1.0', require: false
gem 'pry-byebug', '~> 3.9'
gem 'pry-byebug', '~> 3.10'
gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 5.1'
end
@ -116,13 +116,13 @@ end
group :test do
gem 'capybara', '~> 3.37'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.21'
gem 'faker', '~> 2.23'
gem 'microformats', '~> 4.4'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.21', require: false
gem 'webmock', '~> 3.14'
gem 'rspec_junit_formatter', '~> 0.5'
gem 'webmock', '~> 3.18'
gem 'rspec_junit_formatter', '~> 0.6'
end
group :development do
@ -136,7 +136,7 @@ group :development do
gem 'memory_profiler'
gem 'rubocop', '~> 1.30', require: false
gem 'rubocop-rails', '~> 2.15', require: false
gem 'brakeman', '~> 5.2', require: false
gem 'brakeman', '~> 5.3', require: false
gem 'bundler-audit', '~> 0.9', require: false
gem 'capistrano', '~> 3.17'

View File

@ -1,40 +1,49 @@
GIT
remote: https://github.com/ClearlyClaire/webpush.git
revision: f14a4d52e201128b1b00245d11b6de80d6cfdcd9
ref: f14a4d52e201128b1b00245d11b6de80d6cfdcd9
specs:
webpush (0.3.8)
hkdf (~> 0.2)
jwt (~> 2.0)
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.6)
actionpack (= 6.1.6)
activesupport (= 6.1.6)
actioncable (6.1.7)
actionpack (= 6.1.7)
activesupport (= 6.1.7)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.6)
actionpack (= 6.1.6)
activejob (= 6.1.6)
activerecord (= 6.1.6)
activestorage (= 6.1.6)
activesupport (= 6.1.6)
actionmailbox (6.1.7)
actionpack (= 6.1.7)
activejob (= 6.1.7)
activerecord (= 6.1.7)
activestorage (= 6.1.7)
activesupport (= 6.1.7)
mail (>= 2.7.1)
actionmailer (6.1.6)
actionpack (= 6.1.6)
actionview (= 6.1.6)
activejob (= 6.1.6)
activesupport (= 6.1.6)
actionmailer (6.1.7)
actionpack (= 6.1.7)
actionview (= 6.1.7)
activejob (= 6.1.7)
activesupport (= 6.1.7)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.6)
actionview (= 6.1.6)
activesupport (= 6.1.6)
actionpack (6.1.7)
actionview (= 6.1.7)
activesupport (= 6.1.7)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.6)
actionpack (= 6.1.6)
activerecord (= 6.1.6)
activestorage (= 6.1.6)
activesupport (= 6.1.6)
actiontext (6.1.7)
actionpack (= 6.1.7)
activerecord (= 6.1.7)
activestorage (= 6.1.7)
activesupport (= 6.1.7)
nokogiri (>= 1.8.5)
actionview (6.1.6)
activesupport (= 6.1.6)
actionview (6.1.7)
activesupport (= 6.1.7)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -45,31 +54,31 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8)
activejob (6.1.6)
activesupport (= 6.1.6)
activejob (6.1.7)
activesupport (= 6.1.7)
globalid (>= 0.3.6)
activemodel (6.1.6)
activesupport (= 6.1.6)
activerecord (6.1.6)
activemodel (= 6.1.6)
activesupport (= 6.1.6)
activestorage (6.1.6)
actionpack (= 6.1.6)
activejob (= 6.1.6)
activerecord (= 6.1.6)
activesupport (= 6.1.6)
activemodel (6.1.7)
activesupport (= 6.1.7)
activerecord (6.1.7)
activemodel (= 6.1.7)
activesupport (= 6.1.7)
activestorage (6.1.7)
actionpack (= 6.1.7)
activejob (= 6.1.7)
activerecord (= 6.1.7)
activesupport (= 6.1.7)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.6)
activesupport (6.1.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
airbrussh (1.4.0)
airbrussh (1.4.1)
sshkit (>= 1.6.1, != 1.7.0)
android_key_attestation (0.3.0)
annotate (3.2.0)
@ -79,7 +88,7 @@ GEM
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
awrence (1.1.1)
awrence (1.2.1)
aws-eventstream (1.2.0)
aws-partitions (1.587.0)
aws-sdk-core (3.130.2)
@ -101,12 +110,11 @@ GEM
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
better_html (1.0.16)
actionview (>= 4.0)
activesupport (>= 4.0)
better_html (2.0.1)
actionview (>= 6.0)
activesupport (>= 6.0)
ast (~> 2.0)
erubi (~> 1.4)
html_tokenizer (~> 0.0.6)
parser (>= 2.4)
smart_properties
bindata (2.4.10)
@ -114,22 +122,22 @@ GEM
debug_inspector (>= 0.0.1)
blurhash (0.1.6)
ffi (~> 1.14)
bootsnap (1.12.0)
bootsnap (1.13.0)
msgpack (~> 1.2)
brakeman (5.2.3)
brakeman (5.3.1)
browser (4.2.0)
brpoplpush-redis_script (0.1.2)
concurrent-ruby (~> 1.0, >= 1.0.5)
redis (>= 1.0, <= 5.0)
builder (3.2.4)
bullet (7.0.2)
bullet (7.0.3)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
byebug (11.1.3)
capistrano (3.17.0)
capistrano (3.17.1)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@ -167,10 +175,10 @@ GEM
coderay (1.1.3)
color_diff (0.1)
concurrent-ruby (1.1.10)
connection_pool (2.2.5)
cose (1.0.0)
connection_pool (2.3.0)
cose (1.2.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 0.4.0)
openssl-signature_algorithm (~> 1.0)
crack (0.4.5)
rexml
crass (1.0.6)
@ -198,11 +206,11 @@ GEM
docile (1.3.4)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.5.4)
doorkeeper (5.6.0)
railties (>= 5)
dotenv (2.7.6)
dotenv-rails (2.7.6)
dotenv (= 2.7.6)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
ed25519 (1.3.0)
elasticsearch (7.13.3)
@ -215,12 +223,12 @@ GEM
faraday (~> 1)
multi_json
encryptor (3.0.0)
erubi (1.10.0)
erubi (1.11.0)
et-orbi (1.2.7)
tzinfo
excon (0.76.0)
fabrication (2.29.0)
faker (2.21.0)
fabrication (2.30.0)
faker (2.23.0)
i18n (>= 1.8.11, < 2)
faraday (1.9.3)
faraday-em_http (~> 1.0)
@ -292,7 +300,6 @@ GEM
highline (2.0.3)
hiredis (0.6.3)
hkdf (0.3.0)
html_tokenizer (0.0.7)
htmlentities (4.3.4)
http (5.1.0)
addressable (~> 2.8)
@ -304,15 +311,15 @@ GEM
http-form_data (2.3.0)
http_accept_language (2.1.1)
httpclient (2.8.3)
httplog (1.5.0)
rack (>= 1.0)
httplog (1.6.0)
rack (>= 2.0)
rainbow (>= 2.0.0)
i18n (1.10.0)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.11)
i18n-tasks (1.0.12)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
better_html (~> 1.0)
better_html (>= 1.0, < 3.0)
erubi
highline (>= 2.0.0)
i18n
@ -329,18 +336,18 @@ GEM
activesupport (>= 4.2)
aes_key_wrap
bindata
json-ld (3.2.0)
json-ld (3.2.3)
htmlentities (~> 4.3)
json-canonicalization (~> 0.3)
link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.15)
rack (~> 2.2)
rdf (~> 3.2)
rdf (~> 3.2, >= 3.2.9)
json-ld-preloaded (3.2.0)
json-ld (~> 3.2)
rdf (~> 3.2)
jsonapi-renderer (0.2.2)
jwt (2.2.2)
jwt (2.4.1)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@ -377,7 +384,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.18.0)
loofah (2.19.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -398,16 +405,16 @@ GEM
mime-types-data (3.2022.0105)
mini_mime (1.1.2)
mini_portile2 (2.8.0)
minitest (5.16.0)
msgpack (1.5.2)
minitest (5.16.3)
msgpack (1.5.4)
multi_json (1.15.0)
multipart-post (2.1.1)
net-ldap (0.17.1)
net-scp (3.0.0)
net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (6.1.0)
net-scp (4.0.0.rc1)
net-ssh (>= 2.6.5, < 8.0.0)
net-ssh (7.0.1)
nio4r (2.5.8)
nokogiri (1.13.7)
nokogiri (1.13.8)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nsa (0.2.8)
@ -415,8 +422,8 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.13.17)
omniauth (1.9.1)
oj (3.13.21)
omniauth (1.9.2)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-cas (2.0.0)
@ -439,20 +446,21 @@ GEM
validate_email
validate_url
webfinger (>= 1.0.1)
openssl (2.2.0)
openssl-signature_algorithm (0.4.0)
openssl (3.0.0)
openssl-signature_algorithm (1.2.1)
openssl (> 2.0, < 3.1)
orm_adapter (0.5.0)
ox (2.14.11)
parallel (1.22.1)
parser (3.1.2.0)
parser (3.1.2.1)
ast (~> 2.4.1)
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.4.1)
pg (1.4.3)
pghero (2.8.3)
activerecord (>= 5)
pkg-config (1.4.7)
pkg-config (1.4.9)
posix-spawn (0.3.15)
premailer (1.14.2)
addressable
@ -462,16 +470,16 @@ GEM
actionmailer (>= 3)
premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0)
pry (0.13.1)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.9.0)
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (~> 0.13.0)
pry (>= 0.13, < 0.15)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.7)
puma (5.6.4)
public_suffix (5.0.0)
puma (5.6.5)
nio4r (~> 2.0)
pundit (2.2.0)
activesupport (>= 3.0.0)
@ -490,22 +498,22 @@ GEM
rack (>= 2.1.0)
rack-proxy (0.7.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.1.6)
actioncable (= 6.1.6)
actionmailbox (= 6.1.6)
actionmailer (= 6.1.6)
actionpack (= 6.1.6)
actiontext (= 6.1.6)
actionview (= 6.1.6)
activejob (= 6.1.6)
activemodel (= 6.1.6)
activerecord (= 6.1.6)
activestorage (= 6.1.6)
activesupport (= 6.1.6)
rack-test (2.0.2)
rack (>= 1.3)
rails (6.1.7)
actioncable (= 6.1.7)
actionmailbox (= 6.1.7)
actionmailer (= 6.1.7)
actionpack (= 6.1.7)
actiontext (= 6.1.7)
actionview (= 6.1.7)
activejob (= 6.1.7)
activemodel (= 6.1.7)
activerecord (= 6.1.7)
activestorage (= 6.1.7)
activesupport (= 6.1.7)
bundler (>= 1.15.0)
railties (= 6.1.6)
railties (= 6.1.7)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -521,22 +529,22 @@ GEM
railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
railties (6.1.6)
actionpack (= 6.1.6)
activesupport (= 6.1.6)
railties (6.1.7)
actionpack (= 6.1.7)
activesupport (= 6.1.7)
method_source
rake (>= 12.2)
thor (~> 1.0)
rainbow (3.1.1)
rake (13.0.6)
rdf (3.2.3)
rdf (3.2.9)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.5.0)
rdf (~> 3.2)
redcarpet (3.5.1)
redis (4.5.1)
redis-namespace (1.8.2)
redis (>= 3.0.4)
redis-namespace (1.9.0)
redis (>= 4)
regexp_parser (2.5.0)
request_store (1.5.1)
rack (>= 1.4)
@ -546,7 +554,7 @@ GEM
rexml (3.2.5)
rotp (6.2.0)
rpam2 (4.0.2)
rqrcode (2.1.1)
rqrcode (2.1.2)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
@ -569,8 +577,8 @@ GEM
rspec-sidekiq (3.1.0)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.11.0)
rspec_junit_formatter (0.5.1)
rspec-support (3.11.1)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.30.1)
parallel (~> 1.10)
@ -602,12 +610,11 @@ GEM
scenic (1.6.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securecompare (1.0.0)
semantic_range (3.0.0)
sidekiq (6.5.1)
connection_pool (>= 2.2.2)
sidekiq (6.5.7)
connection_pool (>= 2.2.5)
rack (~> 2.0)
redis (>= 4.2.0)
redis (>= 4.5.0, < 5)
sidekiq-bulk (0.2.0)
sidekiq
sidekiq-scheduler (4.0.2)
@ -615,7 +622,7 @@ GEM
rufus-scheduler (~> 3.2)
sidekiq (>= 4)
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.25)
sidekiq-unique-jobs (7.1.27)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 8.0)
@ -642,7 +649,7 @@ GEM
sshkit (1.21.2)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.19)
stackprof (0.2.21)
statsd-ruby (1.5.0)
stoplight (3.0.0)
strong_migrations (0.7.9)
@ -658,9 +665,10 @@ GEM
climate_control (>= 0.0.3, < 1.0)
thor (1.2.1)
tilt (2.0.10)
tpm-key_attestation (0.9.0)
tpm-key_attestation (0.11.0)
bindata (~> 2.4)
openssl-signature_algorithm (~> 0.4.0)
openssl (> 2.0, < 3.1)
openssl-signature_algorithm (~> 1.0)
tty-color (0.6.0)
tty-cursor (0.7.1)
tty-prompt (0.23.1)
@ -674,14 +682,14 @@ GEM
twitter-text (3.1.0)
idn-ruby
unf (~> 0.1.0)
tzinfo (2.0.4)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2022.1)
tzinfo-data (1.2022.4)
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (2.1.0)
unicode-display_width (2.3.0)
uniform_notifier (1.16.0)
validate_email (0.1.6)
activemodel (>= 3.0)
@ -691,20 +699,19 @@ GEM
public_suffix
warden (1.2.9)
rack (>= 2.0.9)
webauthn (3.0.0.alpha1)
webauthn (2.5.2)
android_key_attestation (~> 0.3.0)
awrence (~> 1.1)
bindata (~> 2.4)
cbor (~> 0.5.9)
cose (~> 1.0)
openssl (~> 2.0)
cose (~> 1.1)
openssl (>= 2.2, < 3.1)
safety_net_attestation (~> 0.4.0)
securecompare (~> 1.0)
tpm-key_attestation (~> 0.9.0)
tpm-key_attestation (~> 0.11.0)
webfinger (1.2.0)
activesupport
httpclient (>= 2.4)
webmock (3.14.0)
webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -713,14 +720,11 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webpush (0.3.8)
hkdf (~> 0.2)
jwt (~> 2.0)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wisper (2.0.1)
xorcist (1.1.2)
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.0)
@ -737,8 +741,8 @@ DEPENDENCIES
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.12.0)
brakeman (~> 5.2)
bootsnap (~> 1.13.0)
brakeman (~> 5.3)
browser
bullet (~> 7.0)
bundler-audit (~> 0.9)
@ -758,11 +762,11 @@ DEPENDENCIES
devise-two-factor (~> 4.0)
devise_pam_authenticatable2 (~> 9.2)
discard (~> 1.2)
doorkeeper (~> 5.5)
dotenv-rails (~> 2.7)
doorkeeper (~> 5.6)
dotenv-rails (~> 2.8)
ed25519 (~> 1.3)
fabrication (~> 2.29)
faker (~> 2.21)
fabrication (~> 2.30)
faker (~> 2.23)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
@ -775,7 +779,7 @@ DEPENDENCIES
htmlentities (~> 4.3)
http (~> 5.1)
http_accept_language (~> 2.1)
httplog (~> 1.5.0)
httplog (~> 1.6.0)
i18n-tasks (~> 1.0)
idn-ruby
json-ld
@ -807,26 +811,26 @@ DEPENDENCIES
posix-spawn
premailer-rails
private_address_check (~> 0.5)
pry-byebug (~> 3.9)
pry-byebug (~> 3.10)
pry-rails (~> 0.3)
puma (~> 5.6)
pundit (~> 2.2)
rack (~> 2.2.4)
rack-attack (~> 6.6)
rack-cors (~> 1.1)
rails (~> 6.1.6)
rails (~> 6.1.7)
rails-controller-testing (~> 1.0)
rails-i18n (~> 6.0)
rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.5)
redcarpet (~> 3.5)
redis (~> 4.5)
redis-namespace (~> 1.8)
redis-namespace (~> 1.9)
rexml (~> 3.2)
rqrcode (~> 2.1)
rspec-rails (~> 5.1)
rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.5)
rspec_junit_formatter (~> 0.6)
rubocop (~> 1.30)
rubocop-rails (~> 2.15)
ruby-progressbar (~> 1.11)
@ -848,8 +852,8 @@ DEPENDENCIES
tty-prompt (~> 0.23)
twitter-text (~> 3.1.0)
tzinfo-data (~> 1.2022)
webauthn (~> 3.0.0.alpha1)
webmock (~> 3.14)
webauthn (~> 2.5)
webmock (~> 3.18)
webpacker (~> 5.4)
webpush (~> 0.3)
webpush!
xorcist (~> 1.1)

View File

@ -10,17 +10,17 @@ class AboutController < ApplicationController
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: [:more, :terms]
before_action :set_expires_in, only: [:more]
before_action :set_registration_form_time, only: :show
skip_before_action :require_functional!, only: [:more, :terms]
skip_before_action :require_functional!, only: [:more]
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)
toc_generator = TOCGenerator.new(@instance_presenter.extended_description)
@rules = Rule.ordered
@contents = toc_generator.html
@ -28,8 +28,6 @@ class AboutController < ApplicationController
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
end
def terms; end
helper_method :display_blocks?
helper_method :display_blocks_rationale?
helper_method :public_fetch_mode?

View File

@ -7,7 +7,7 @@ class AccountsController < ApplicationController
include AccountControllerConcern
include SignatureAuthentication
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
before_action :set_body_classes

View File

@ -6,7 +6,7 @@ class ActivityPub::ClaimsController < ActivityPub::BaseController
skip_before_action :authenticate_user!
before_action :require_signature!
before_action :require_account_signature!
before_action :set_claim_result
def create

View File

@ -4,7 +4,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_items
before_action :set_size
before_action :set_type

View File

@ -4,7 +4,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!
before_action :require_account_signature!
before_action :set_items
before_action :set_cache_headers

View File

@ -6,7 +6,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
include AccountOwnedConcern
before_action :skip_unknown_actor_activity
before_action :require_signature!
before_action :require_actor_signature!
skip_before_action :authenticate_user!
def create
@ -49,17 +49,17 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
end
def upgrade_account
if signed_request_account.ostatus?
if signed_request_account&.ostatus?
signed_request_account.update(last_webfingered_at: nil)
ResolveAccountWorker.perform_async(signed_request_account.acct)
end
DeliveryFailureTracker.reset!(signed_request_account.inbox_url)
DeliveryFailureTracker.reset!(signed_request_actor.inbox_url)
end
def process_collection_synchronization
raw_params = request.headers['Collection-Synchronization']
return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true'
return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' || signed_request_account.nil?
# Re-using the syntax for signature parameters
tree = SignatureParamsParser.new.parse(raw_params)
@ -71,6 +71,6 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
end
def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
ActivityPub::ProcessingWorker.perform_async(signed_request_actor.id, body, @account&.id, signed_request_actor.class.name)
end
end

View File

@ -6,7 +6,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_statuses
before_action :set_cache_headers

View File

@ -7,7 +7,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
DESCENDANTS_LIMIT = 60
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status
before_action :set_cache_headers
before_action :set_replies

View File

@ -16,7 +16,11 @@ module Admin
def batch
authorize :account, :index?
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form = Form::AccountBatch.new(form_account_batch_params)
@form.current_account = current_account
@form.action = action_from_button
@form.select_all_matching = params[:select_all_matching]
@form.query = filtered_accounts
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')

View File

@ -23,6 +23,7 @@ module Admin
@role.current_account = current_account
if @role.save
log_action :create, @role
redirect_to admin_roles_path
else
render :new
@ -39,6 +40,7 @@ module Admin
@role.current_account = current_account
if @role.update(resource_params)
log_action :update, @role
redirect_to admin_roles_path
else
render :edit
@ -48,6 +50,7 @@ module Admin
def destroy
authorize @role, :destroy?
@role.destroy!
log_action :destroy, @role
redirect_to admin_roles_path
end

View File

@ -14,6 +14,7 @@ module Admin
@user.current_account = current_account
if @user.update(resource_params)
log_action :change_role, @user
redirect_to admin_account_path(@user.account_id), notice: I18n.t('admin.accounts.change_role.changed_msg')
else
render :show

View File

@ -131,4 +131,10 @@ class Api::BaseController < ApplicationController
def disallow_unauthenticated_api_access?
authorized_fetch_mode?
end
private
def respond_with_error(code)
render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action :set_account
after_action :insert_pagination_headers

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action :set_account
after_action :insert_pagination_headers

View File

@ -30,12 +30,12 @@ class Api::V1::AccountsController < Api::BaseController
self.response_body = Oj.dump(response.body)
self.status = response.status
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity
render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: :unprocessable_entity
end
def follow
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true)
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true)
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options)
end

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:canonical_email_blocks' }, only: [:index, :show, :test]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:canonical_email_blocks' }, except: [:index, :show, :test]
before_action :set_canonical_email_blocks, only: :index
before_action :set_canonical_email_blocks_from_test, only: [:test]
before_action :set_canonical_email_block, only: [:show, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(limit).freeze
def index
authorize :canonical_email_block, :index?
render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer
end
def show
authorize @canonical_email_block, :show?
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
end
def test
authorize :canonical_email_block, :test?
render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer
end
def create
authorize :canonical_email_block, :create?
@canonical_email_block = CanonicalEmailBlock.create!(resource_params)
log_action :create, @canonical_email_block
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
end
def destroy
authorize @canonical_email_block, :destroy?
@canonical_email_block.destroy!
log_action :destroy, @canonical_email_block
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
end
private
def resource_params
params.permit(:canonical_email_hash, :email)
end
def set_canonical_email_blocks
@canonical_email_blocks = CanonicalEmailBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_canonical_email_blocks_from_test
@canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email])
end
def set_canonical_email_block
@canonical_email_block = CanonicalEmailBlock.find(params[:id])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_canonical_email_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
end
def pagination_max_id
@canonical_email_blocks.last.id
end
def pagination_since_id
@canonical_email_blocks.first.id
end
def records_continue?
@canonical_email_blocks.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
end

View File

@ -0,0 +1,90 @@
# frozen_string_literal: true
class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:email_domain_blocks' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:email_domain_blocks' }, except: [:index, :show]
before_action :set_email_domain_blocks, only: :index
before_action :set_email_domain_block, only: [:show, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(
limit
).freeze
def create
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.create!(resource_params)
log_action :create, @email_domain_block
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
end
def index
authorize :email_domain_block, :index?
render json: @email_domain_blocks, each_serializer: REST::Admin::EmailDomainBlockSerializer
end
def show
authorize @email_domain_block, :show?
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
end
def destroy
authorize @email_domain_block, :destroy?
@email_domain_block.destroy!
log_action :destroy, @email_domain_block
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
end
private
def set_email_domain_blocks
@email_domain_blocks = EmailDomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_email_domain_block
@email_domain_block = EmailDomainBlock.find(params[:id])
end
def resource_params
params.permit(:domain)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_email_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
end
def pagination_max_id
@email_domain_blocks.last.id
end
def pagination_since_id
@email_domain_blocks.first.id
end
def records_continue?
@email_domain_blocks.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
end

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
class Api::V1::Admin::IpBlocksController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:ip_blocks' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:ip_blocks' }, except: [:index, :show]
before_action :set_ip_blocks, only: :index
before_action :set_ip_block, only: [:show, :update, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(
limit
).freeze
def create
authorize :ip_block, :create?
@ip_block = IpBlock.create!(resource_params)
log_action :create, @ip_block
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
def index
authorize :ip_block, :index?
render json: @ip_blocks, each_serializer: REST::Admin::IpBlockSerializer
end
def show
authorize @ip_block, :show?
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
def update
authorize @ip_block, :update?
@ip_block.update(resource_params)
log_action :update, @ip_block
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
def destroy
authorize @ip_block, :destroy?
@ip_block.destroy!
log_action :destroy, @ip_block
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
private
def set_ip_blocks
@ip_blocks = IpBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_ip_block
@ip_block = IpBlock.find(params[:id])
end
def resource_params
params.permit(:ip, :severity, :comment, :expires_in)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_ip_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
end
def pagination_max_id
@ip_blocks.last.id
end
def pagination_since_id
@ip_blocks.first.id
end
def records_continue?
@ip_blocks.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
class Api::V1::Filters::StatusesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
before_action :require_user!
before_action :set_status_filters, only: :index
before_action :set_status_filter, only: [:show, :destroy]
def index
render json: @status_filters, each_serializer: REST::FilterStatusSerializer
end
def create
@status_filter = current_account.custom_filters.find(params[:filter_id]).statuses.create!(resource_params)
render json: @status_filter, serializer: REST::FilterStatusSerializer
end
def show
render json: @status_filter, serializer: REST::FilterStatusSerializer
end
def destroy
@status_filter.destroy!
render_empty
end
private
def set_status_filters
filter = current_account.custom_filters.includes(:statuses).find(params[:filter_id])
@status_filters = filter.statuses
end
def set_status_filter
@status_filter = CustomFilterStatus.includes(:custom_filter).where(custom_filter: { account: current_account }).find(params[:id])
end
def resource_params
params.permit(:status_id)
end
end

View File

@ -6,6 +6,6 @@ class Api::V1::InstancesController < Api::BaseController
def show
expires_in 3.minutes, public: true
render_with_cache json: {}, serializer: REST::InstanceSerializer, root: 'instance'
render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance'
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class Api::V1::Statuses::TranslationsController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :set_status
before_action :set_translation
rescue_from TranslationService::NotConfiguredError, with: :not_found
rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable
def create
render json: @translation, serializer: REST::TranslationSerializer
end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def set_translation
@translation = TranslateStatusService.new.call(@status, content_locale)
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Api::V2::InstancesController < Api::V1::InstancesController
def show
expires_in 3.minutes, public: true
render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance'
end
end

View File

@ -83,7 +83,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def check_enabled_registrations
redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations?
redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations? || ip_blocked?
end
def allowed_registrations?
@ -94,6 +94,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
ENV['OMNIAUTH_ONLY'] == 'true'
end
def ip_blocked?
IpBlock.where(severity: :sign_up_block).where('ip >>= ?', request.remote_ip.to_s).exists?
end
def invite_code
if params[:user]
params[:user][:invite_code]

View File

@ -3,7 +3,11 @@
module AccountableConcern
extend ActiveSupport::Concern
def log_action(action, target, options = {})
Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
def log_action(action, target)
Admin::ActionLog.create(
account: current_account,
action: action,
target: target
)
end
end

View File

@ -45,10 +45,14 @@ module SignatureVerification
end
end
def require_signature!
def require_account_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
def require_actor_signature!
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor
end
def signed_request?
request.headers['Signature'].present?
end
@ -68,7 +72,11 @@ module SignatureVerification
end
def signed_request_account
return @signed_request_account if defined?(@signed_request_account)
signed_request_actor.is_a?(Account) ? signed_request_actor : nil
end
def signed_request_actor
return @signed_request_actor if defined?(@signed_request_actor)
raise SignatureVerificationError, 'Request not signed' unless signed_request?
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
@ -78,26 +86,30 @@ module SignatureVerification
verify_signature_strength!
verify_body_digest!
account = account_from_key_id(signature_params['keyId'])
actor = actor_from_key_id(signature_params['keyId'])
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string
return account unless verify_signature(account, signature, compare_signed_string).nil?
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
return account unless verify_signature(account, signature, compare_signed_string).nil?
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
@signed_request_account = nil
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
rescue SignatureVerificationError => e
@signature_verification_failure_reason = e.message
@signed_request_account = nil
fail_with! e.message
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
fail_with! "Failed to fetch remote data: #{e.message}"
rescue Mastodon::UnexpectedResponseError
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
rescue Stoplight::Error::RedLight
fail_with! 'Fetching attempt skipped because of recent connection failure'
end
def request_body
@ -106,6 +118,11 @@ module SignatureVerification
private
def fail_with!(message)
@signature_verification_failure_reason = message
@signed_request_actor = nil
end
def signature_params
@signature_params ||= begin
raw_signature = request.headers['Signature']
@ -138,13 +155,23 @@ module SignatureVerification
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
sha256 = digests.assoc('sha-256')
raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" if body_digest != sha256[1]
return if body_digest == sha256[1]
digest_size = begin
Base64.strict_decode64(sha256[1].strip).length
rescue ArgumentError
raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}"
end
raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32
raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
end
def verify_signature(account, signature, compare_signed_string)
if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
@signed_request_account = account
@signed_request_account
def verify_signature(actor, signature, compare_signed_string)
if actor.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
@signed_request_actor = actor
@signed_request_actor
end
rescue OpenSSL::PKey::RSAError
nil
@ -207,7 +234,7 @@ module SignatureVerification
signature_params['keyId'].blank? || signature_params['signature'].blank?
end
def account_from_key_id(key_id)
def actor_from_key_id(key_id)
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)
@ -216,27 +243,34 @@ module SignatureVerification
end
if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) }
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
account
end
rescue Mastodon::HostValidationError
nil
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
def stoplight_wrap_request(&block)
Stoplight("source:#{request.remote_ip}", &block)
.with_fallback { nil }
.with_threshold(1)
.with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
.run
end
def account_refresh_key(account)
return if account.local? || !account.activitypub?
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
def actor_refresh_key!(actor)
return if actor.local? || !actor.activitypub?
return actor.refresh! if actor.respond_to?(:refresh!) && actor.possibly_stale?
ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
end
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
class Filters::StatusesController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_filter
before_action :set_status_filters
before_action :set_pack
before_action :set_body_classes
PER_PAGE = 20
def index
@status_filter_batch_action = Form::StatusFilterBatchAction.new
end
def batch
@status_filter_batch_action = Form::StatusFilterBatchAction.new(status_filter_batch_action_params.merge(current_account: current_account, filter_id: params[:filter_id], type: action_from_button))
@status_filter_batch_action.save!
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
ensure
redirect_to edit_filter_path(@filter)
end
private
def set_pack
use_pack 'admin'
end
def set_filter
@filter = current_account.custom_filters.find(params[:filter_id])
end
def set_status_filters
@status_filters = @filter.statuses.preload(:status).page(params[:page]).per(PER_PAGE)
end
def status_filter_batch_action_params
params.require(:form_status_filter_batch_action).permit(status_filter_ids: [])
end
def action_from_button
if params[:remove]
'remove'
end
end
def set_body_classes
@body_classes = 'admin'
end
end

View File

@ -9,7 +9,7 @@ class FiltersController < ApplicationController
before_action :set_body_classes
def index
@filters = current_account.custom_filters.includes(:keywords).order(:phrase)
@filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase)
end
def new

View File

@ -4,7 +4,7 @@ class FollowerAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }

View File

@ -4,7 +4,7 @@ class FollowingAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }

View File

@ -2,10 +2,10 @@
class HomeController < ApplicationController
before_action :redirect_unauthenticated_to_permalinks!
before_action :authenticate_user!
before_action :set_pack
before_action :set_referrer_policy_header
before_action :set_instance_presenter
def index
@body_classes = 'app-body'
@ -16,7 +16,10 @@ class HomeController < ApplicationController
def redirect_unauthenticated_to_permalinks!
return if user_signed_in?
redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
redirect_path = PermalinkRedirector.new(request.path).redirect_path
redirect_path ||= default_redirect_path
redirect_to(redirect_path) if redirect_path.present?
end
def set_pack
@ -24,8 +27,10 @@ class HomeController < ApplicationController
end
def default_redirect_path
if request.path.start_with?('/web') || whitelist_mode?
if whitelist_mode?
new_user_session_path
elsif request.path.start_with?('/web')
nil
elsif single_user_mode?
short_account_path(Account.local.without_suspended.where('id > 0').first)
else
@ -36,4 +41,8 @@ class HomeController < ApplicationController
def set_referrer_policy_header
response.headers['Referrer-Policy'] = 'origin'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class PrivacyController < ApplicationController
layout 'public'
before_action :set_pack
before_action :set_instance_presenter
before_action :set_expires_in
skip_before_action :require_functional!
def show; end
private
def set_pack
use_pack 'public'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def set_expires_in
expires_in 0, public: true
end
end

View File

@ -58,7 +58,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_trends,
:setting_crop_images,
:setting_always_send_emails,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status appeal),
notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag trending_link trending_status appeal),
interactions: %i(must_be_follower must_be_following must_be_following_dm)
)
end

View File

@ -8,7 +8,7 @@ class StatusesController < ApplicationController
layout 'public'
before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers

View File

@ -8,7 +8,7 @@ class TagsController < ApplicationController
layout 'public'
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_local
before_action :set_tag

View File

@ -2,64 +2,29 @@
module Admin::ActionLogsHelper
def log_target(log)
if log.target
linkable_log_target(log.target)
else
log_target_from_history(log.target_type, log.recorded_changes)
end
end
private
def linkable_log_target(record)
case record.class.name
case log.target_type
when 'Account'
link_to record.acct, admin_account_path(record.id)
link_to log.human_identifier, admin_account_path(log.target_id)
when 'User'
link_to record.account.acct, admin_account_path(record.account_id)
when 'CustomEmoji'
record.shortcode
link_to log.human_identifier, admin_account_path(log.route_param)
when 'UserRole'
link_to log.human_identifier, admin_roles_path(log.target_id)
when 'Report'
link_to "##{record.id}", admin_report_path(record)
link_to "##{log.human_identifier}", admin_report_path(log.target_id)
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
link_to record.domain, "https://#{record.domain}"
link_to log.human_identifier, "https://#{log.human_identifier}"
when 'Status'
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
link_to log.human_identifier, log.permalink
when 'AccountWarning'
link_to record.target_account.acct, admin_account_path(record.target_account_id)
link_to log.human_identifier, admin_account_path(log.target_id)
when 'Announcement'
link_to truncate(record.text), edit_admin_announcement_path(record.id)
when 'IpBlock'
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
when 'Instance'
record.domain
link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id)
when 'IpBlock', 'Instance', 'CustomEmoji'
log.human_identifier
when 'CanonicalEmailBlock'
content_tag(:samp, log.human_identifier[0...7], title: log.human_identifier)
when 'Appeal'
link_to record.account.acct, disputes_strike_path(record.strike)
end
end
def log_target_from_history(type, attributes)
case type
when 'User'
attributes['username']
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
if tmp_status.account
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
else
I18n.t('admin.action_logs.deleted_status')
end
when 'Announcement'
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
when 'IpBlock'
"#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
when 'Instance'
attributes['domain']
link_to log.human_identifier, disputes_strike_path(log.route_param)
end
end
end

View File

@ -6,18 +6,71 @@ import ready from '../mastodon/ready';
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
const showSelectAll = () => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
selectAllMatchingElement.classList.add('active');
};
const hideSelectAll = () => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
const hiddenField = document.querySelector('#select_all_matching');
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
selectAllMatchingElement.classList.remove('active');
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
hiddenField.value = '0';
};
delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
content.checked = target.checked;
});
if (selectAllMatchingElement) {
if (target.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
});
delegate(document, '.batch-table__select-all button', 'click', () => {
const hiddenField = document.querySelector('#select_all_matching');
const active = hiddenField.value === '1';
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
if (active) {
hiddenField.value = '0';
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
} else {
hiddenField.value = '1';
notSelectedMsg.classList.remove('active');
selectedMsg.classList.add('active');
}
});
delegate(document, batchCheckboxClassName, 'change', () => {
const checkAllElement = document.querySelector('#batch_checkbox_all');
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
if (checkAllElement) {
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
if (selectAllMatchingElement) {
if (checkAllElement.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
}
});

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';

View File

@ -1,4 +1,4 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import api, { getLinks } from '../api';
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
@ -553,10 +553,12 @@ export function expandFollowingFail(id, error) {
export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
const loadedRelationships = getState().get('relationships');
const state = getState();
const loadedRelationships = state.get('relationships');
const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
const signedIn = !!state.getIn(['meta', 'me']);
if (newAccountIds.length === 0) {
if (!signedIn || newAccountIds.length === 0) {
return;
}

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { normalizeAnnouncement } from './importer/normalizer';
export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST';

View File

@ -0,0 +1,6 @@
export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE';
export const changeLayout = layout => ({
type: APP_LAYOUT_CHANGE,
layout,
});

View File

@ -1,4 +1,4 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
import { openModal } from './modal';

View File

@ -1,4 +1,4 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';

View File

@ -1,11 +1,11 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { CancelToken, isCancel } from 'axios';
import { throttle } from 'lodash';
import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light';
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
import { useEmoji } from './emojis';
import { tagHistory } from 'flavours/glitch/util/settings';
import { recoverHashtags } from 'flavours/glitch/util/hashtag';
import resizeImage from 'flavours/glitch/util/resize_image';
import { tagHistory } from '../settings';
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
import resizeImage from 'flavours/glitch/utils/resize_image';
import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines';
import { showAlertForError } from './alerts';

View File

@ -1,4 +1,4 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import api, { getLinks } from '../api';
import {
importFetchedAccounts,
importFetchedStatuses,

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
export const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST';
export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { importFetchedAccounts } from './importer';
import { fetchRelationships } from './accounts';

View File

@ -1,4 +1,4 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import api, { getLinks } from '../api';
export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';

View File

@ -1,4 +1,4 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';

View File

@ -1,9 +1,24 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { openModal } from './modal';
export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
export const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
export const FILTERS_STATUS_CREATE_REQUEST = 'FILTERS_STATUS_CREATE_REQUEST';
export const FILTERS_STATUS_CREATE_SUCCESS = 'FILTERS_STATUS_CREATE_SUCCESS';
export const FILTERS_STATUS_CREATE_FAIL = 'FILTERS_STATUS_CREATE_FAIL';
export const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
export const initAddFilter = (status, { contextType }) => dispatch =>
dispatch(openModal('FILTER', {
statusId: status?.get('id'),
contextType: contextType,
}));
export const fetchFilters = () => (dispatch, getState) => {
dispatch({
type: FILTERS_FETCH_REQUEST,
@ -11,7 +26,7 @@ export const fetchFilters = () => (dispatch, getState) => {
});
api(getState)
.get('/api/v1/filters')
.get('/api/v2/filters')
.then(({ data }) => dispatch({
type: FILTERS_FETCH_SUCCESS,
filters: data,
@ -24,3 +39,55 @@ export const fetchFilters = () => (dispatch, getState) => {
skipAlert: true,
}));
};
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
dispatch(createFilterStatusRequest());
api(getState).post(`/api/v1/filters/${params.filter_id}/statuses`, params).then(response => {
dispatch(createFilterStatusSuccess(response.data));
if (onSuccess) onSuccess();
}).catch(error => {
dispatch(createFilterStatusFail(error));
if (onFail) onFail();
});
};
export const createFilterStatusRequest = () => ({
type: FILTERS_STATUS_CREATE_REQUEST,
});
export const createFilterStatusSuccess = filter_status => ({
type: FILTERS_STATUS_CREATE_SUCCESS,
filter_status,
});
export const createFilterStatusFail = error => ({
type: FILTERS_STATUS_CREATE_FAIL,
error,
});
export const createFilter = (params, onSuccess, onFail) => (dispatch, getState) => {
dispatch(createFilterRequest());
api(getState).post('/api/v2/filters', params).then(response => {
dispatch(createFilterSuccess(response.data));
if (onSuccess) onSuccess(response.data);
}).catch(error => {
dispatch(createFilterFail(error));
if (onFail) onFail();
});
};
export const createFilterRequest = () => ({
type: FILTERS_CREATE_REQUEST,
});
export const createFilterSuccess = filter => ({
type: FILTERS_CREATE_SUCCESS,
filter,
});
export const createFilterFail = error => ({
type: FILTERS_CREATE_FAIL,
error,
});

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { importFetchedAccounts } from './importer';
export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST';
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS';

View File

@ -5,6 +5,7 @@ export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_IMPORT';
export const FILTERS_IMPORT = 'FILTERS_IMPORT';
function pushUnique(array, object) {
if (array.every(element => element.id !== object.id)) {
@ -28,6 +29,10 @@ export function importStatuses(statuses) {
return { type: STATUSES_IMPORT, statuses };
}
export function importFilters(filters) {
return { type: FILTERS_IMPORT, filters };
}
export function importPolls(polls) {
return { type: POLLS_IMPORT, polls };
}
@ -61,11 +66,16 @@ export function importFetchedStatuses(statuses) {
const accounts = [];
const normalStatuses = [];
const polls = [];
const filters = [];
function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), getState().get('local_settings')));
pushUnique(accounts, status.account);
if (status.filtered) {
status.filtered.forEach(result => pushUnique(filters, result.filter));
}
if (status.reblog && status.reblog.id) {
processStatus(status.reblog);
}
@ -80,6 +90,7 @@ export function importFetchedStatuses(statuses) {
dispatch(importPolls(polls));
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters));
};
}

View File

@ -1,7 +1,7 @@
import escapeTextContentForBrowser from 'escape-html';
import emojify from 'flavours/glitch/util/emoji';
import { unescapeHTML } from 'flavours/glitch/util/html';
import { autoHideCW } from 'flavours/glitch/util/content_warning';
import emojify from 'flavours/glitch/features/emoji/emoji';
import { unescapeHTML } from 'flavours/glitch/utils/html';
import { autoHideCW } from 'flavours/glitch/utils/content_warning';
const domParser = new DOMParser();
@ -42,6 +42,14 @@ export function normalizeAccount(account) {
return account;
}
export function normalizeFilterResult(result) {
const normalResult = { ...result };
normalResult.filter = normalResult.filter.id;
return normalResult;
}
export function normalizeStatus(status, normalOldStatus, settings) {
const normalStatus = { ...status };
normalStatus.account = status.account.id;
@ -54,6 +62,10 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.poll = status.poll.id;
}
if (status.filtered) {
normalStatus.filtered = status.filtered.map(normalizeFilterResult);
}
// Only calculate these values when status first encountered and
// when the underlying values change. Otherwise keep the ones
// already in the reducer

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { importFetchedAccounts, importFetchedStatus } from './importer';
export const REBLOG_REQUEST = 'REBLOG_REQUEST';

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { importFetchedAccounts } from './importer';
import { showAlertForError } from './alerts';

View File

@ -1,4 +1,4 @@
import { expandSpoilers, disableSwiping } from 'flavours/glitch/util/initial_state';
import { expandSpoilers, disableSwiping } from 'flavours/glitch/initial_state';
import { openModal } from './modal';
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';

View File

@ -1,6 +1,7 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { debounce } from 'lodash';
import compareId from 'flavours/glitch/util/compare_id';
import compareId from '../compare_id';
import { List as ImmutableList } from 'immutable';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
@ -11,7 +12,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0) {
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
@ -63,7 +64,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const _buildParams = (state) => {
const params = {};
const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
@ -82,9 +83,10 @@ const _buildParams = (state) => {
};
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
const params = _buildParams(getState());
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0) {
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}

View File

@ -1,4 +1,4 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
import { openModal } from 'flavours/glitch/actions/modal';

View File

@ -1,4 +1,4 @@
import api, { getLinks } from 'flavours/glitch/util/api';
import api, { getLinks } from '../api';
import IntlMessageFormat from 'intl-messageformat';
import { fetchFollowRequests, fetchRelationships } from './accounts';
import {
@ -11,12 +11,10 @@ import { submitMarkers } from './markers';
import { saveSettings } from './settings';
import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import { unescapeHTML } from 'flavours/glitch/util/html';
import { getFiltersRegex } from 'flavours/glitch/selectors';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
import compareId from 'flavours/glitch/util/compare_id';
import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer';
import { requestNotificationPermission } from 'flavours/glitch/util/notifications';
import { unescapeHTML } from 'flavours/glitch/utils/html';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import compareId from 'flavours/glitch/compare_id';
import { requestNotificationPermission } from 'flavours/glitch/utils/notifications';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
@ -74,20 +72,17 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
const filters = getFiltersRegex(getState(), { contextType: 'notifications' });
let filtered = false;
if (['mention', 'status'].includes(notification.type)) {
const dropRegex = filters[0];
const regex = filters[1];
const searchIndex = searchTextFromRawStatus(notification.status);
if (['mention', 'status'].includes(notification.type) && notification.status.filtered) {
const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
if (dropRegex && dropRegex.test(searchIndex)) {
if (filters.some(result => result.filter.filter_action === 'hide')) {
return;
}
filtered = regex && regex.test(searchIndex);
filtered = filters.length > 0;
}
if (['follow_request'].includes(notification.type)) {
@ -158,15 +153,22 @@ const excludeTypesFromFilter = filter => {
const noOp = () => {};
export function expandNotifications({ maxId } = {}, done = noOp) {
let expandNotificationsController = new AbortController();
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
return (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const notifications = getState().get('notifications');
const isLoadingMore = !!maxId;
if (notifications.get('isLoading')) {
done();
return;
if (forceLoad) {
expandNotificationsController.abort();
expandNotificationsController = new AbortController();
} else {
done();
return;
}
}
const params = {
@ -191,7 +193,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
dispatch(expandNotificationsRequest(isLoadingMore));
api(getState).get('/api/v1/notifications', { params }).then(response => {
api(getState).get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
@ -232,7 +234,7 @@ export function expandNotificationsFail(error, isLoadingMore) {
type: NOTIFICATIONS_EXPAND_FAIL,
error,
skipLoading: !isLoadingMore,
skipAlert: !isLoadingMore,
skipAlert: !isLoadingMore || error.name === 'AbortError',
};
};
@ -343,7 +345,7 @@ export function setFilter (filterType) {
path: ['notifications', 'quickFilter', 'active'],
value: filterType,
});
dispatch(expandNotifications());
dispatch(expandNotifications({ forceLoad: true }));
dispatch(saveSettings());
};
};

View File

@ -1,11 +1,11 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { importFetchedStatuses } from './importer';
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
import { me } from 'flavours/glitch/util/initial_state';
import { me } from 'flavours/glitch/initial_state';
export function fetchPinnedStatuses() {
return (dispatch, getState) => {

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { importFetchedPoll } from './importer';
export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';

View File

@ -1,19 +1,5 @@
import {
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
CLEAR_SUBSCRIPTION,
SET_ALERTS,
setAlerts,
} from './setter';
import { register, saveSettings } from './registerer';
export {
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
CLEAR_SUBSCRIPTION,
SET_ALERTS,
register,
};
import { setAlerts } from './setter';
import { saveSettings } from './registerer';
export function changeAlerts(path, value) {
return dispatch => {
@ -21,3 +7,11 @@ export function changeAlerts(path, value) {
dispatch(saveSettings());
};
}
export {
CLEAR_SUBSCRIPTION,
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
SET_ALERTS,
} from './setter';
export { register } from './registerer';

View File

@ -1,5 +1,5 @@
import api from 'flavours/glitch/util/api';
import { pushNotificationsSetting } from 'flavours/glitch/util/settings';
import api from '../../api';
import { pushNotificationsSetting } from '../../settings';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
// Taken from https://www.npmjs.com/package/web-push

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { openModal } from './modal';
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';

View File

@ -1,27 +0,0 @@
import api from 'flavours/glitch/util/api';
export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
export const RULES_FETCH_FAIL = 'RULES_FETCH_FAIL';
export const fetchRules = () => (dispatch, getState) => {
dispatch(fetchRulesRequest());
api(getState)
.get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules)))
.catch(err => dispatch(fetchRulesFail(err)));
};
const fetchRulesRequest = () => ({
type: RULES_FETCH_REQUEST,
});
const fetchRulesSuccess = rules => ({
type: RULES_FETCH_SUCCESS,
rules,
});
const fetchRulesFail = error => ({
type: RULES_FETCH_FAIL,
error,
});

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatuses } from './importer';

View File

@ -0,0 +1,30 @@
import api from '../api';
import { importFetchedAccount } from './importer';
export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
export const fetchServer = () => (dispatch, getState) => {
dispatch(fetchServerRequest());
api(getState)
.get('/api/v2/instance').then(({ data }) => {
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
dispatch(fetchServerSuccess(data));
}).catch(err => dispatch(fetchServerFail(err)));
};
const fetchServerRequest = () => ({
type: SERVER_FETCH_REQUEST,
});
const fetchServerSuccess = server => ({
type: SERVER_FETCH_SUCCESS,
server,
});
const fetchServerFail = error => ({
type: SERVER_FETCH_FAIL,
error,
});

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { debounce } from 'lodash';
import { showAlertForError } from './alerts';

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses } from './importer';
@ -42,9 +42,9 @@ export function fetchStatusRequest(id, skipLoading) {
};
};
export function fetchStatus(id) {
export function fetchStatus(id, forceFetch = false) {
return (dispatch, getState) => {
const skipLoading = getState().getIn(['statuses', id], null) !== null;
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
dispatch(fetchContext(id));

View File

@ -1,6 +1,6 @@
// @ts-check
import { connectStream } from 'flavours/glitch/util/stream';
import { connectStream } from '../stream';
import {
updateTimeline,
deleteFromTimelines,
@ -21,7 +21,6 @@ import {
updateReaction as updateAnnouncementsReaction,
deleteAnnouncement,
} from './announcements';
import { fetchFilters } from './filters';
import { getLocale } from 'mastodon/locales';
const { messages } = getLocale();
@ -97,9 +96,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
case 'conversation':
dispatch(updateConversations(JSON.parse(data.payload)));
break;
case 'filters_changed':
dispatch(fetchFilters());
break;
case 'announcement':
dispatch(updateAnnouncements(JSON.parse(data.payload)));
break;

View File

@ -1,4 +1,4 @@
import api from 'flavours/glitch/util/api';
import api from '../api';
import { importFetchedAccounts } from './importer';
import { fetchRelationships } from './accounts';

View File

@ -0,0 +1,92 @@
import api from '../api';
export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST';
export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS';
export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL';
export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST';
export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS';
export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL';
export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST';
export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS';
export const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL';
export const fetchHashtag = name => (dispatch, getState) => {
dispatch(fetchHashtagRequest());
api(getState).get(`/api/v1/tags/${name}`).then(({ data }) => {
dispatch(fetchHashtagSuccess(name, data));
}).catch(err => {
dispatch(fetchHashtagFail(err));
});
};
export const fetchHashtagRequest = () => ({
type: HASHTAG_FETCH_REQUEST,
});
export const fetchHashtagSuccess = (name, tag) => ({
type: HASHTAG_FETCH_SUCCESS,
name,
tag,
});
export const fetchHashtagFail = error => ({
type: HASHTAG_FETCH_FAIL,
error,
});
export const followHashtag = name => (dispatch, getState) => {
dispatch(followHashtagRequest(name));
api(getState).post(`/api/v1/tags/${name}/follow`).then(({ data }) => {
dispatch(followHashtagSuccess(name, data));
}).catch(err => {
dispatch(followHashtagFail(name, err));
});
};
export const followHashtagRequest = name => ({
type: HASHTAG_FOLLOW_REQUEST,
name,
});
export const followHashtagSuccess = (name, tag) => ({
type: HASHTAG_FOLLOW_SUCCESS,
name,
tag,
});
export const followHashtagFail = (name, error) => ({
type: HASHTAG_FOLLOW_FAIL,
name,
error,
});
export const unfollowHashtag = name => (dispatch, getState) => {
dispatch(unfollowHashtagRequest(name));
api(getState).post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => {
dispatch(unfollowHashtagSuccess(name, data));
}).catch(err => {
dispatch(unfollowHashtagFail(name, err));
});
};
export const unfollowHashtagRequest = name => ({
type: HASHTAG_UNFOLLOW_REQUEST,
name,
});
export const unfollowHashtagSuccess = (name, tag) => ({
type: HASHTAG_UNFOLLOW_SUCCESS,
name,
tag,
});
export const unfollowHashtagFail = (name, error) => ({
type: HASHTAG_UNFOLLOW_FAIL,
name,
error,
});

View File

@ -1,11 +1,10 @@
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers';
import api, { getLinks } from 'flavours/glitch/util/api';
import api, { getLinks } from 'flavours/glitch/api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'flavours/glitch/util/compare_id';
import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
import { getFiltersRegex } from 'flavours/glitch/selectors';
import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer';
import compareId from 'flavours/glitch/compare_id';
import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
import { toServerSideType } from 'flavours/glitch/utils/filters';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
@ -40,14 +39,13 @@ export function updateTimeline(timeline, status, accept) {
return;
}
const filters = getFiltersRegex(getState(), { contextType: timeline });
const dropRegex = filters[0];
const regex = filters[1];
const text = searchTextFromRawStatus(status);
let filtered = false;
let filtered = false;
if (status.account.id !== me) {
filtered = (dropRegex && dropRegex.test(text)) || (regex && regex.test(text));
if (status.filtered) {
const contextType = toServerSideType(timeline);
const filters = status.filtered.filter(result => result.filter.context.includes(contextType));
filtered = filters.length > 0;
}
dispatch(importFetchedStatus(status));

View File

@ -1,32 +1,139 @@
import api from 'flavours/glitch/util/api';
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST';
export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS';
export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL';
export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS';
export const TRENDS_TAGS_FETCH_FAIL = 'TRENDS_TAGS_FETCH_FAIL';
export const fetchTrends = () => (dispatch, getState) => {
dispatch(fetchTrendsRequest());
export const TRENDS_LINKS_FETCH_REQUEST = 'TRENDS_LINKS_FETCH_REQUEST';
export const TRENDS_LINKS_FETCH_SUCCESS = 'TRENDS_LINKS_FETCH_SUCCESS';
export const TRENDS_LINKS_FETCH_FAIL = 'TRENDS_LINKS_FETCH_FAIL';
export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST';
export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS';
export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL';
export const TRENDS_STATUSES_EXPAND_REQUEST = 'TRENDS_STATUSES_EXPAND_REQUEST';
export const TRENDS_STATUSES_EXPAND_SUCCESS = 'TRENDS_STATUSES_EXPAND_SUCCESS';
export const TRENDS_STATUSES_EXPAND_FAIL = 'TRENDS_STATUSES_EXPAND_FAIL';
export const fetchTrendingHashtags = () => (dispatch, getState) => {
dispatch(fetchTrendingHashtagsRequest());
api(getState)
.get('/api/v1/trends')
.then(({ data }) => dispatch(fetchTrendsSuccess(data)))
.catch(err => dispatch(fetchTrendsFail(err)));
.get('/api/v1/trends/tags')
.then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data)))
.catch(err => dispatch(fetchTrendingHashtagsFail(err)));
};
export const fetchTrendsRequest = () => ({
type: TRENDS_FETCH_REQUEST,
export const fetchTrendingHashtagsRequest = () => ({
type: TRENDS_TAGS_FETCH_REQUEST,
skipLoading: true,
});
export const fetchTrendsSuccess = trends => ({
type: TRENDS_FETCH_SUCCESS,
export const fetchTrendingHashtagsSuccess = trends => ({
type: TRENDS_TAGS_FETCH_SUCCESS,
trends,
skipLoading: true,
});
export const fetchTrendsFail = error => ({
type: TRENDS_FETCH_FAIL,
export const fetchTrendingHashtagsFail = error => ({
type: TRENDS_TAGS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
});
export const fetchTrendingLinks = () => (dispatch, getState) => {
dispatch(fetchTrendingLinksRequest());
api(getState)
.get('/api/v1/trends/links')
.then(({ data }) => dispatch(fetchTrendingLinksSuccess(data)))
.catch(err => dispatch(fetchTrendingLinksFail(err)));
};
export const fetchTrendingLinksRequest = () => ({
type: TRENDS_LINKS_FETCH_REQUEST,
skipLoading: true,
});
export const fetchTrendingLinksSuccess = trends => ({
type: TRENDS_LINKS_FETCH_SUCCESS,
trends,
skipLoading: true,
});
export const fetchTrendingLinksFail = error => ({
type: TRENDS_LINKS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
});
export const fetchTrendingStatuses = () => (dispatch, getState) => {
if (getState().getIn(['status_lists', 'trending', 'isLoading'])) {
return;
}
dispatch(fetchTrendingStatusesRequest());
api(getState).get('/api/v1/trends/statuses').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchTrendingStatusesSuccess(response.data, next ? next.uri : null));
}).catch(err => dispatch(fetchTrendingStatusesFail(err)));
};
export const fetchTrendingStatusesRequest = () => ({
type: TRENDS_STATUSES_FETCH_REQUEST,
skipLoading: true,
});
export const fetchTrendingStatusesSuccess = (statuses, next) => ({
type: TRENDS_STATUSES_FETCH_SUCCESS,
statuses,
next,
skipLoading: true,
});
export const fetchTrendingStatusesFail = error => ({
type: TRENDS_STATUSES_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
});
export const expandTrendingStatuses = () => (dispatch, getState) => {
const url = getState().getIn(['status_lists', 'trending', 'next'], null);
if (url === null || getState().getIn(['status_lists', 'trending', 'isLoading'])) {
return;
}
dispatch(expandTrendingStatusesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandTrendingStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandTrendingStatusesFail(error));
});
};
export const expandTrendingStatusesRequest = () => ({
type: TRENDS_STATUSES_EXPAND_REQUEST,
});
export const expandTrendingStatusesSuccess = (statuses, next) => ({
type: TRENDS_STATUSES_EXPAND_SUCCESS,
statuses,
next,
});
export const expandTrendingStatusesFail = error => ({
type: TRENDS_STATUSES_EXPAND_FAIL,
error,
});

View File

@ -1,6 +1,6 @@
import axios from 'axios';
import ready from './ready';
import LinkHeader from 'http-link-header';
import ready from './ready';
export const getLinks = response => {
const value = response.headers.link;

View File

@ -5,7 +5,7 @@ import includes from 'array-includes';
import assign from 'object-assign';
import values from 'object.values';
import isNaN from 'is-nan';
import { decode as decodeBase64 } from './base64';
import { decode as decodeBase64 } from './utils/base64';
import promiseFinally from 'promise.prototype.finally';
if (!Array.prototype.includes) {

View File

@ -7,8 +7,9 @@ import Permalink from './permalink';
import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'flavours/glitch/util/initial_state';
import { me } from 'flavours/glitch/initial_state';
import RelativeTimestamp from './relative_timestamp';
import Skeleton from 'flavours/glitch/components/skeleton';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -26,7 +27,7 @@ export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
account: ImmutablePropTypes.map,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
@ -77,7 +78,16 @@ class Account extends ImmutablePureComponent {
} = this.props;
if (!account) {
return <div />;
return (
<div className='account'>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div>
<DisplayName />
</div>
</div>
</div>
);
}
if (hidden) {

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'flavours/glitch/util/api';
import api from 'flavours/glitch/api';
import { FormattedNumber } from 'react-intl';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import classNames from 'classnames';

View File

@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'flavours/glitch/util/api';
import api from 'flavours/glitch/api';
import { FormattedNumber } from 'react-intl';
import { roundTo10 } from 'flavours/glitch/util/numbers';
import { roundTo10 } from 'flavours/glitch/utils/numbers';
import Skeleton from 'flavours/glitch/components/skeleton';
export default class Dimension extends React.PureComponent {

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'flavours/glitch/util/api';
import api from 'flavours/glitch/api';
import { injectIntl, defineMessages } from 'react-intl';
import classNames from 'classnames';

View File

@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'flavours/glitch/util/api';
import api from 'flavours/glitch/api';
import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
import classNames from 'classnames';
import { roundTo10 } from 'flavours/glitch/util/numbers';
import { roundTo10 } from 'flavours/glitch/utils/numbers';
const dateForCohort = cohort => {
switch(cohort.frequency) {

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'flavours/glitch/util/api';
import api from 'flavours/glitch/api';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Hashtag from 'flavours/glitch/components/hashtag';

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { FormattedNumber } from 'react-intl';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { reduceMotion } from 'flavours/glitch/util/initial_state';
import { reduceMotion } from 'flavours/glitch/initial_state';
const obfuscatedCount = count => {
if (count < 0) {

View File

@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light';
import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light';
import { assetHost } from 'flavours/glitch/util/config';
import { assetHost } from 'flavours/glitch/utils/config';
export default class AutosuggestEmoji extends React.PureComponent {

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
import { autoPlayGif } from 'flavours/glitch/initial_state';
import classNames from 'classnames';
export default class Avatar extends React.PureComponent {

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
import { autoPlayGif } from 'flavours/glitch/initial_state';
export default class AvatarComposite extends React.PureComponent {

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
import { autoPlayGif } from 'flavours/glitch/initial_state';
export default class AvatarOverlay extends React.PureComponent {

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollTop } from 'flavours/glitch/util/scroll';
import { scrollTop } from '../scroll';
export default class Column extends React.PureComponent {

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