mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 10:16:22 +00:00
Merge branch 'rails' of 0xacab.org:sutty/sutty into issue-13244
This commit is contained in:
commit
662570c4cc
415 changed files with 12217 additions and 3469 deletions
3
.bundler-audit.yml
Normal file
3
.bundler-audit.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
ignore:
|
||||||
|
- "CVE-2024-6531"
|
11
.editorconfig
Normal file
11
.editorconfig
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
|
@ -1,8 +1,8 @@
|
||||||
|
NODE_OPTIONS=--openssl-legacy-provider
|
||||||
# pwgen -1 32
|
# pwgen -1 32
|
||||||
RAILS_MASTER_KEY=11111111111111111111111111111111
|
RAILS_MASTER_KEY=11111111111111111111111111111111
|
||||||
RAILS_GROUPS=assets
|
RAILS_GROUPS=assets
|
||||||
DELEGATE=athshe.sutty.nl
|
DELEGATE=panel.sutty.nl
|
||||||
HAINISH=../haini.sh/haini.sh
|
|
||||||
DATABASE_URL=postgres://suttier@postgresql.sutty.local/sutty
|
DATABASE_URL=postgres://suttier@postgresql.sutty.local/sutty
|
||||||
RAILS_ENV=development
|
RAILS_ENV=development
|
||||||
IMAP_SERVER=
|
IMAP_SERVER=
|
||||||
|
@ -37,3 +37,7 @@ AIRBRAKE_API_KEY=
|
||||||
GITLAB_URI=https://0xacab.org
|
GITLAB_URI=https://0xacab.org
|
||||||
GITLAB_PROJECT=
|
GITLAB_PROJECT=
|
||||||
GITLAB_TOKEN=
|
GITLAB_TOKEN=
|
||||||
|
PGVER=15
|
||||||
|
PGPID=/run/postgresql.pid
|
||||||
|
PANEL_ACTOR_MENTION=@sutty@sutty.nl
|
||||||
|
PANEL_ACTOR_SITE_ID=1
|
1
.env.development
Normal file
1
.env.development
Normal file
|
@ -0,0 +1 @@
|
||||||
|
HAINISH=../haini.sh/haini.sh
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -28,7 +28,7 @@
|
||||||
/data/*
|
/data/*
|
||||||
/_storage/*
|
/_storage/*
|
||||||
|
|
||||||
.env*
|
.env.*
|
||||||
|
|
||||||
# Ignore master key for decrypting credentials and more.
|
# Ignore master key for decrypting credentials and more.
|
||||||
/config/master.key
|
/config/master.key
|
||||||
|
@ -48,3 +48,7 @@ yarn-debug.log*
|
||||||
/yarn-error.log
|
/yarn-error.log
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
|
/.task
|
||||||
|
/.yardoc
|
||||||
|
/public/doc/
|
||||||
|
|
140
.gitlab-ci.yml
140
.gitlab-ci.yml
|
@ -1,33 +1,127 @@
|
||||||
image: "gitea.nulo.in/sutty/panel:3.14.10-2.7.8-panel.sutty.nl"
|
stages:
|
||||||
|
- "test"
|
||||||
|
- "deploy"
|
||||||
|
.apk-add: &apk-add
|
||||||
|
- "apk add go-task diffutils gitlab_ci_log_section"
|
||||||
|
.disable-hainish: &disable-hainish
|
||||||
|
- "rm -f .env.development"
|
||||||
|
.cache-ruby: &cache-ruby
|
||||||
|
- paths:
|
||||||
|
- "vendor/ruby"
|
||||||
|
- ".bundle"
|
||||||
|
key:
|
||||||
|
files:
|
||||||
|
- "Gemfile.lock"
|
||||||
|
.cache-node: &cache-node
|
||||||
|
- paths:
|
||||||
|
- "node_modules"
|
||||||
|
key:
|
||||||
|
files:
|
||||||
|
- "yarn.lock"
|
||||||
|
.cache-task: &cache-task
|
||||||
|
- paths:
|
||||||
|
- ".task"
|
||||||
|
image: "registry.0xacab.org/sutty/sutty:3.17.3-3.1.4-rails"
|
||||||
variables:
|
variables:
|
||||||
RAILS_ENV: "production"
|
RAILS_ENV: "production"
|
||||||
LC_ALL: "C.UTF-8"
|
LC_ALL: "C.UTF-8"
|
||||||
|
HAINISH: ""
|
||||||
cache:
|
cache:
|
||||||
paths:
|
push:
|
||||||
- "vendor/ruby"
|
stage: "test"
|
||||||
assets:
|
only:
|
||||||
stage: "build"
|
- "rails"
|
||||||
rules:
|
except:
|
||||||
- if: "$CI_COMMIT_BRANCH == \"panel.sutty.nl\""
|
- "schedules"
|
||||||
- if: "$CI_COMMIT_BRANCH"
|
|
||||||
changes:
|
|
||||||
compare_to: "refs/heads/rails"
|
|
||||||
paths:
|
|
||||||
- "package.json"
|
|
||||||
- "app/javascript/**/*"
|
|
||||||
- "app/assets/**/*"
|
|
||||||
before_script:
|
before_script:
|
||||||
- "git config --global user.email \"${GIT_USER_EMAIL:-$GITLAB_USER_EMAIL}\""
|
- "git config --global user.email \"${GIT_USER_EMAIL:-$GITLAB_USER_EMAIL}\""
|
||||||
- "git config --global user.name \"${GIT_USER_NAME:-$GITLAB_USER_NAME}\""
|
- "git config --global user.name \"${GIT_USER_NAME:-$GITLAB_USER_NAME}\""
|
||||||
- "git remote set-url --push origin \"https://${GITLAB_USERNAME}:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git\""
|
- "git remote set-url --push origin \"https://GITLAB_CI_PUSH_TOKEN:${GITLAB_CI_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git\""
|
||||||
- "apk add python2 dotenv brotli"
|
|
||||||
- "mv config/credentials.yml.enc.ci config/credentials.yml.enc"
|
|
||||||
- "cp .env.example .env"
|
|
||||||
- "dotenv bundle install --path=vendor"
|
|
||||||
script:
|
script:
|
||||||
- "dotenv RAILS_ENV=production bundle exec rails webpacker:clobber"
|
- "git commit --allow-empty -m \"ci: test [skip ci]\""
|
||||||
- "dotenv RAILS_ENV=production bundle exec rails assets:precompile"
|
- "git push -o ci.skip origin HEAD:${CI_COMMIT_BRANCH}"
|
||||||
- "dotenv RAILS_ENV=production bundle exec rails assets:clean"
|
assets:
|
||||||
|
stage: "deploy"
|
||||||
|
only:
|
||||||
|
- "rails"
|
||||||
|
- "production.panel.sutty.nl"
|
||||||
|
- "panel.sutty.nl"
|
||||||
|
except:
|
||||||
|
- "schedules"
|
||||||
|
cache:
|
||||||
|
- *cache-ruby
|
||||||
|
- *cache-node
|
||||||
|
- *cache-task
|
||||||
|
before_script:
|
||||||
|
- *apk-add
|
||||||
|
- "gitlab_ci_log_section --name git --header=\"Configuring git\""
|
||||||
|
- "git config --global user.email \"${GIT_USER_EMAIL:-$GITLAB_USER_EMAIL}\""
|
||||||
|
- "git config --global user.name \"${GIT_USER_NAME:-$GITLAB_USER_NAME}\""
|
||||||
|
- "git remote set-url --push origin \"https://GITLAB_CI_PUSH_TOKEN:${GITLAB_CI_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git\""
|
||||||
|
- "gitlab_ci_log_section --name git --end"
|
||||||
|
- "gitlab_ci_log_section --name apk --header=\"Installing dependencies\""
|
||||||
|
- "apk add brotli"
|
||||||
|
- *disable-hainish
|
||||||
|
- "gitlab_ci_log_section --name apk --end"
|
||||||
|
script:
|
||||||
|
- "gitlab_ci_log_section --name assets --header=\"Building\""
|
||||||
|
- "go-task assets"
|
||||||
after_script:
|
after_script:
|
||||||
- "git add public && git commit -m \"ci: assets [skip ci]\""
|
- "git add public && git commit -m \"ci: assets [skip ci]\""
|
||||||
- "git push -o ci.skip"
|
- "git push -o ci.skip origin HEAD:${CI_COMMIT_BRANCH}"
|
||||||
|
gem-audit:
|
||||||
|
stage: "test"
|
||||||
|
only:
|
||||||
|
- "schedules"
|
||||||
|
cache:
|
||||||
|
- *cache-ruby
|
||||||
|
before_script:
|
||||||
|
- *apk-add
|
||||||
|
- *disable-hainish
|
||||||
|
script:
|
||||||
|
- "go-task gem-audit"
|
||||||
|
node-audit:
|
||||||
|
stage: "test"
|
||||||
|
only:
|
||||||
|
- "schedules"
|
||||||
|
cache:
|
||||||
|
- *cache-node
|
||||||
|
before_script:
|
||||||
|
- *apk-add
|
||||||
|
- *disable-hainish
|
||||||
|
script:
|
||||||
|
- "apk add go-task"
|
||||||
|
- "go-task node-audit"
|
||||||
|
brakeman:
|
||||||
|
stage: "test"
|
||||||
|
cache:
|
||||||
|
- *cache-ruby
|
||||||
|
rules:
|
||||||
|
- if: "$CI_PIPELINE_SOURCE == 'merge_request_event'"
|
||||||
|
before_script:
|
||||||
|
- *apk-add
|
||||||
|
- *disable-hainish
|
||||||
|
script:
|
||||||
|
- "go-task bundle -- exec brakeman"
|
||||||
|
rubocop:
|
||||||
|
stage: "test"
|
||||||
|
cache:
|
||||||
|
- *cache-ruby
|
||||||
|
rules:
|
||||||
|
- if: "$CI_PIPELINE_SOURCE == 'merge_request_event'"
|
||||||
|
before_script:
|
||||||
|
- *apk-add
|
||||||
|
- *disable-hainish
|
||||||
|
script:
|
||||||
|
- "go-task rubocop"
|
||||||
|
haml:
|
||||||
|
stage: "test"
|
||||||
|
cache:
|
||||||
|
- *cache-ruby
|
||||||
|
rules:
|
||||||
|
- if: "$CI_PIPELINE_SOURCE == 'merge_request_event'"
|
||||||
|
before_script:
|
||||||
|
- *apk-add
|
||||||
|
- *disable-hainish
|
||||||
|
script:
|
||||||
|
- "go-task haml-lint"
|
||||||
|
|
|
@ -19,7 +19,6 @@ pipeline:
|
||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
- "rails"
|
- "rails"
|
||||||
- "panel.sutty.nl"
|
|
||||||
- "17.3.alpine.panel.sutty.nl"
|
- "17.3.alpine.panel.sutty.nl"
|
||||||
event: "push"
|
event: "push"
|
||||||
path:
|
path:
|
||||||
|
@ -27,57 +26,8 @@ pipeline:
|
||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
- ".dockerignore"
|
- ".dockerignore"
|
||||||
- ".woodpecker.yml"
|
- ".woodpecker.yml"
|
||||||
assets:
|
|
||||||
image: "gitea.nulo.in/sutty/panel:3.14.10-2.7.8"
|
|
||||||
commands:
|
|
||||||
- "apk add python2 dotenv openssh-client brotli"
|
|
||||||
- "install -d -m 700 ~/.ssh/"
|
|
||||||
- "echo \"$${KNOWN_HOSTS}\" | base64 -d >> ~/.ssh/known_hosts"
|
|
||||||
- "chmod 600 ~/.ssh/known_hosts"
|
|
||||||
- "eval $(ssh-agent -s)"
|
|
||||||
- "echo \"$${SSH_KEY}\" | base64 -d | ssh-add -"
|
|
||||||
- "ssh $${ORIGIN%:*}"
|
|
||||||
- "git config user.name Woodpecker"
|
|
||||||
- "git config user.email ci@sutty.coop.ar"
|
|
||||||
- "git remote add upstream $${ORIGIN}"
|
|
||||||
- "git checkout -B ${CI_COMMIT_BRANCH}"
|
|
||||||
- "mv config/credentials.yml.enc.ci config/credentials.yml.enc"
|
|
||||||
- "yarn"
|
|
||||||
- "cp .env.example .env"
|
|
||||||
- "dotenv bundle install --path=vendor"
|
|
||||||
- "dotenv RAILS_ENV=production bundle exec rails webpacker:clobber"
|
|
||||||
- "dotenv RAILS_ENV=production bundle exec rails assets:precompile"
|
|
||||||
- "dotenv RAILS_ENV=production bundle exec rails assets:clean"
|
|
||||||
- "find public -type f -print0 | xargs -r0 brotli -k9f"
|
|
||||||
- "git add public && git commit -m \"ci: assets [skip ci]\""
|
|
||||||
- "git pull upstream ${CI_COMMIT_BRANCH}"
|
|
||||||
- "git push upstream ${CI_COMMIT_BRANCH}"
|
|
||||||
environment:
|
|
||||||
- "RUBY_VERSION=${RUBY_VERSION}"
|
|
||||||
- "GEMS_SOURCE=https://14.3.alpine.gems.sutty.nl"
|
|
||||||
secrets:
|
|
||||||
- "SSH_KEY"
|
|
||||||
- "KNOWN_HOSTS"
|
|
||||||
- "ORIGIN"
|
|
||||||
when:
|
|
||||||
branch:
|
|
||||||
- "rails"
|
|
||||||
- "panel.sutty.nl"
|
|
||||||
path:
|
|
||||||
include:
|
|
||||||
- "app/assets/**/*"
|
|
||||||
- "app/javascript/**/*"
|
|
||||||
- "package.json"
|
|
||||||
- "yarn.lock"
|
|
||||||
matrix:
|
|
||||||
ALPINE_VERSION: "3.14.10"
|
|
||||||
RUBY_VERSION: "2.7"
|
|
||||||
RUBY_PATCH: "8"
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- ALPINE_VERSION: "3.17.3"
|
- ALPINE_VERSION: "3.17.3"
|
||||||
RUBY_VERSION: "3.1"
|
RUBY_VERSION: "3.1"
|
||||||
RUBY_PATCH: "4"
|
RUBY_PATCH: "4"
|
||||||
- ALPINE_VERSION: "3.14.10"
|
|
||||||
RUBY_VERSION: "2.7"
|
|
||||||
RUBY_PATCH: "8"
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
ARG RUBY_VERSION=2.7
|
ARG RUBY_VERSION=3.1
|
||||||
ARG RUBY_PATCH=6
|
ARG RUBY_PATCH=4
|
||||||
ARG ALPINE_VERSION=3.13.10
|
ARG ALPINE_VERSION=3.17.3
|
||||||
ARG BASE_IMAGE=registry.nulo.in/sutty/rails
|
ARG BASE_IMAGE=registry.nulo.in/sutty/rails
|
||||||
FROM ${BASE_IMAGE}:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}
|
FROM ${BASE_IMAGE}:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}
|
||||||
ARG PANDOC_VERSION=2.18
|
ARG PANDOC_VERSION=2.18
|
||||||
|
|
25
Gemfile
25
Gemfile
|
@ -1,11 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source ENV.fetch('GEMS_SOURCE', 'https://17.3.alpine.gems.sutty.nl')
|
source ENV.fetch('GEMS_SOURCE', 'https://gems.sutty.nl')
|
||||||
|
|
||||||
ruby "~> #{ENV.fetch('RUBY_VERSION', '3.1')}"
|
ruby "~> #{ENV.fetch('RUBY_VERSION', '3.1')}"
|
||||||
|
|
||||||
gem 'dotenv-rails', require: 'dotenv/rails-now'
|
|
||||||
|
|
||||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||||
gem 'rails', '~> 6.1.0'
|
gem 'rails', '~> 6.1.0'
|
||||||
# Use Puma as the app server
|
# Use Puma as the app server
|
||||||
|
@ -39,8 +37,9 @@ gem 'commonmarker'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-i18n'
|
gem 'devise-i18n'
|
||||||
gem 'devise_invitable'
|
gem 'devise_invitable'
|
||||||
gem 'distributed-press-api-client', '~> 0.2.3'
|
gem 'redis-client'
|
||||||
gem 'njalla-api-client', '~> 0.2.0'
|
gem 'hiredis-client'
|
||||||
|
gem 'distributed-press-api-client', '~> 0.4.1'
|
||||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||||
gem 'exception_notification'
|
gem 'exception_notification'
|
||||||
gem 'fast_blank'
|
gem 'fast_blank'
|
||||||
|
@ -53,7 +52,6 @@ gem 'inline_svg'
|
||||||
gem 'httparty'
|
gem 'httparty'
|
||||||
gem 'safe_yaml', require: false
|
gem 'safe_yaml', require: false
|
||||||
gem 'jekyll', '~> 4.2.0'
|
gem 'jekyll', '~> 4.2.0'
|
||||||
gem 'jekyll-data'
|
|
||||||
gem 'jekyll-commonmark', '~> 1.4.0'
|
gem 'jekyll-commonmark', '~> 1.4.0'
|
||||||
gem 'jekyll-images'
|
gem 'jekyll-images'
|
||||||
gem 'jekyll-include-cache'
|
gem 'jekyll-include-cache'
|
||||||
|
@ -69,7 +67,9 @@ gem 'redis', '~> 4.0', require: %w[redis redis/connection/hiredis]
|
||||||
gem 'redis-rails'
|
gem 'redis-rails'
|
||||||
gem 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update'
|
gem 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update'
|
||||||
gem 'rubyzip'
|
gem 'rubyzip'
|
||||||
gem 'rugged'
|
gem 'ruby-brs'
|
||||||
|
gem 'rugged', '1.5.0.1'
|
||||||
|
gem 'git_clone_url'
|
||||||
gem 'concurrent-ruby-ext'
|
gem 'concurrent-ruby-ext'
|
||||||
gem 'que'
|
gem 'que'
|
||||||
gem 'symbol-fstring', require: 'fstring/all'
|
gem 'symbol-fstring', require: 'fstring/all'
|
||||||
|
@ -78,6 +78,14 @@ gem 'validates_hostname'
|
||||||
gem 'webpacker'
|
gem 'webpacker'
|
||||||
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
|
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
|
||||||
gem 'kaminari'
|
gem 'kaminari'
|
||||||
|
gem 'device_detector'
|
||||||
|
gem 'htmlbeautifier'
|
||||||
|
gem 'dry-schema'
|
||||||
|
gem 'rubanok'
|
||||||
|
|
||||||
|
gem 'after_commit_everywhere', '~> 1.0'
|
||||||
|
gem 'aasm'
|
||||||
|
gem 'que-web'
|
||||||
|
|
||||||
# database
|
# database
|
||||||
gem 'hairtrigger'
|
gem 'hairtrigger'
|
||||||
|
@ -108,6 +116,7 @@ end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'derailed_benchmarks'
|
gem 'derailed_benchmarks'
|
||||||
|
gem 'dotenv-rails'
|
||||||
gem 'pry'
|
gem 'pry'
|
||||||
# Adds support for Capybara system testing and selenium driver
|
# Adds support for Capybara system testing and selenium driver
|
||||||
gem 'capybara', '~> 2.13'
|
gem 'capybara', '~> 2.13'
|
||||||
|
@ -116,7 +125,9 @@ group :development, :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
gem 'yard'
|
||||||
gem 'brakeman'
|
gem 'brakeman'
|
||||||
|
gem 'bundler-audit'
|
||||||
gem 'haml-lint', require: false
|
gem 'haml-lint', require: false
|
||||||
gem 'letter_opener'
|
gem 'letter_opener'
|
||||||
gem 'listen'
|
gem 'listen'
|
||||||
|
|
322
Gemfile.lock
322
Gemfile.lock
|
@ -25,75 +25,83 @@ GIT
|
||||||
groupdate (>= 5.2)
|
groupdate (>= 5.2)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://17.3.alpine.gems.sutty.nl/
|
remote: https://gems.sutty.nl/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.7.3)
|
aasm (5.5.0)
|
||||||
actionpack (= 6.1.7.3)
|
concurrent-ruby (~> 1.0)
|
||||||
activesupport (= 6.1.7.3)
|
actioncable (6.1.7.8)
|
||||||
|
actionpack (= 6.1.7.8)
|
||||||
|
activesupport (= 6.1.7.8)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.7.3)
|
actionmailbox (6.1.7.8)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.8)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.8)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.8)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.7.3)
|
actionmailer (6.1.7.8)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.8)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.8)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.7.3)
|
actionpack (6.1.7.8)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.7.3)
|
actiontext (6.1.7.8)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.8)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.8)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.7.3)
|
actionview (6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
activejob (6.1.7.3)
|
activejob (6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.7.3)
|
activemodel (6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
activerecord (6.1.7.3)
|
activerecord (6.1.7.8)
|
||||||
activemodel (= 6.1.7.3)
|
activemodel (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
activestorage (6.1.7.3)
|
activestorage (6.1.7.8)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.8)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.8)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.7.3)
|
activesupport (6.1.7.8)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
zeitwerk (~> 2.3)
|
zeitwerk (~> 2.3)
|
||||||
addressable (2.8.4)
|
addressable (2.8.6)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
|
adsp (1.0.10)
|
||||||
|
after_commit_everywhere (1.4.0)
|
||||||
|
activerecord (>= 4.2)
|
||||||
|
activesupport
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
autoprefixer-rails (10.4.13.0)
|
autoprefixer-rails (10.4.13.0)
|
||||||
execjs (~> 2)
|
execjs (~> 2)
|
||||||
bcrypt (3.1.19-x86_64-linux-musl)
|
base64 (0.2.0)
|
||||||
|
bcrypt (3.1.20-x86_64-linux-musl)
|
||||||
bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
|
bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
|
||||||
benchmark-ips (2.12.0)
|
benchmark-ips (2.12.0)
|
||||||
|
bigdecimal (3.1.1)
|
||||||
bindex (0.8.1-x86_64-linux-musl)
|
bindex (0.8.1-x86_64-linux-musl)
|
||||||
blazer (2.6.5)
|
blazer (2.6.5)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
|
@ -104,8 +112,12 @@ GEM
|
||||||
autoprefixer-rails (>= 9.1.0)
|
autoprefixer-rails (>= 9.1.0)
|
||||||
popper_js (>= 1.16.1, < 2)
|
popper_js (>= 1.16.1, < 2)
|
||||||
sassc-rails (>= 2.0.0)
|
sassc-rails (>= 2.0.0)
|
||||||
brakeman (5.4.1)
|
brakeman (6.1.1)
|
||||||
builder (3.2.4)
|
racc
|
||||||
|
builder (3.3.0)
|
||||||
|
bundler-audit (0.9.1)
|
||||||
|
bundler (>= 1.2.0, < 3)
|
||||||
|
thor (~> 1.0)
|
||||||
capybara (2.18.0)
|
capybara (2.18.0)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
|
@ -121,6 +133,7 @@ GEM
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.2.2)
|
||||||
concurrent-ruby-ext (1.2.2-x86_64-linux-musl)
|
concurrent-ruby-ext (1.2.2-x86_64-linux-musl)
|
||||||
concurrent-ruby (= 1.2.2)
|
concurrent-ruby (= 1.2.2)
|
||||||
|
connection_pool (2.4.1)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
database_cleaner (2.0.2)
|
database_cleaner (2.0.2)
|
||||||
database_cleaner-active_record (>= 2, < 3)
|
database_cleaner-active_record (>= 2, < 3)
|
||||||
|
@ -128,7 +141,7 @@ GEM
|
||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.3.3-x86_64-linux-musl)
|
date (3.3.4-x86_64-linux-musl)
|
||||||
dead_end (4.0.0)
|
dead_end (4.0.0)
|
||||||
derailed_benchmarks (2.1.2)
|
derailed_benchmarks (2.1.2)
|
||||||
benchmark-ips (~> 2)
|
benchmark-ips (~> 2)
|
||||||
|
@ -142,7 +155,8 @@ GEM
|
||||||
rake (> 10, < 14)
|
rake (> 10, < 14)
|
||||||
ruby-statistics (>= 2.1)
|
ruby-statistics (>= 2.1)
|
||||||
thor (>= 0.19, < 2)
|
thor (>= 0.19, < 2)
|
||||||
devise (4.9.2)
|
device_detector (1.1.2)
|
||||||
|
devise (4.9.3)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
|
@ -150,14 +164,15 @@ GEM
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-i18n (1.11.0)
|
devise-i18n (1.11.0)
|
||||||
devise (>= 4.9.0)
|
devise (>= 4.9.0)
|
||||||
devise_invitable (2.0.8)
|
devise_invitable (2.0.9)
|
||||||
actionmailer (>= 5.0)
|
actionmailer (>= 5.0)
|
||||||
devise (>= 4.6)
|
devise (>= 4.6)
|
||||||
distributed-press-api-client (0.2.4)
|
distributed-press-api-client (0.4.1)
|
||||||
addressable (~> 2.3, >= 2.3.0)
|
addressable (~> 2.3, >= 2.3.0)
|
||||||
climate_control
|
climate_control
|
||||||
dry-schema
|
dry-schema
|
||||||
httparty (~> 0.18)
|
httparty (~> 0.18)
|
||||||
|
httparty-cache (~> 0.0.6)
|
||||||
json (~> 2.1, >= 2.1.0)
|
json (~> 2.1, >= 2.1.0)
|
||||||
jwt (~> 2.6.0)
|
jwt (~> 2.6.0)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
|
@ -166,10 +181,10 @@ GEM
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
down (5.4.1)
|
down (5.4.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
dry-configurable (1.0.1)
|
dry-configurable (1.1.0)
|
||||||
dry-core (~> 1.0, < 2)
|
dry-core (~> 1.0, < 2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
dry-core (1.0.0)
|
dry-core (1.0.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
dry-inflector (1.0.0)
|
dry-inflector (1.0.0)
|
||||||
|
@ -178,7 +193,7 @@ GEM
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-core (~> 1.0, < 2)
|
dry-core (~> 1.0, < 2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
dry-schema (1.13.1)
|
dry-schema (1.13.3)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-configurable (~> 1.0, >= 1.0.1)
|
dry-configurable (~> 1.0, >= 1.0.1)
|
||||||
dry-core (~> 1.0, < 2)
|
dry-core (~> 1.0, < 2)
|
||||||
|
@ -186,7 +201,8 @@ GEM
|
||||||
dry-logic (>= 1.4, < 2)
|
dry-logic (>= 1.4, < 2)
|
||||||
dry-types (>= 1.7, < 2)
|
dry-types (>= 1.7, < 2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
dry-types (1.7.1)
|
dry-types (1.7.2)
|
||||||
|
bigdecimal (~> 3.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
dry-core (~> 1.0)
|
dry-core (~> 1.0)
|
||||||
dry-inflector (~> 1.0)
|
dry-inflector (~> 1.0)
|
||||||
|
@ -197,7 +213,7 @@ GEM
|
||||||
eventmachine (>= 0.12.9)
|
eventmachine (>= 0.12.9)
|
||||||
http_parser.rb (~> 0)
|
http_parser.rb (~> 0)
|
||||||
errbase (0.2.2)
|
errbase (0.2.2)
|
||||||
erubi (1.12.0)
|
erubi (1.13.0)
|
||||||
eventmachine (1.2.7-x86_64-linux-musl)
|
eventmachine (1.2.7-x86_64-linux-musl)
|
||||||
exception_notification (4.5.0)
|
exception_notification (4.5.0)
|
||||||
actionmailer (>= 5.2, < 8)
|
actionmailer (>= 5.2, < 8)
|
||||||
|
@ -210,32 +226,34 @@ GEM
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
fast_blank (1.0.1-x86_64-linux-musl)
|
fast_blank (1.0.1-x86_64-linux-musl)
|
||||||
fast_jsonparser (0.5.0-x86_64-linux-musl)
|
fast_jsonparser (0.5.0-x86_64-linux-musl)
|
||||||
ffi (1.15.5-x86_64-linux-musl)
|
ffi (1.17.0-x86_64-linux-musl)
|
||||||
flamegraph (0.9.5)
|
flamegraph (0.9.5)
|
||||||
forwardable-extended (2.6.0)
|
forwardable-extended (2.6.0)
|
||||||
friendly_id (5.5.0)
|
friendly_id (5.5.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
get_process_mem (0.2.7)
|
get_process_mem (0.2.7)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
globalid (1.1.0)
|
git_clone_url (2.0.0)
|
||||||
activesupport (>= 5.0)
|
uri-ssh_git (>= 2.0)
|
||||||
|
globalid (1.2.1)
|
||||||
|
activesupport (>= 6.1)
|
||||||
groupdate (6.2.1)
|
groupdate (6.2.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
hairtrigger (1.0.0)
|
hairtrigger (1.0.0)
|
||||||
activerecord (>= 6.0, < 8)
|
activerecord (>= 6.0, < 8)
|
||||||
ruby2ruby (~> 2.4)
|
ruby2ruby (~> 2.4)
|
||||||
ruby_parser (~> 3.10)
|
ruby_parser (~> 3.10)
|
||||||
haml (6.1.2-x86_64-linux-musl)
|
haml (6.3.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
haml-lint (0.999.999)
|
haml-lint (0.999.999)
|
||||||
haml_lint
|
haml_lint
|
||||||
haml_lint (0.45.0)
|
haml_lint (0.53.0)
|
||||||
haml (>= 4.0, < 6.2)
|
haml (>= 5.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
rubocop (>= 0.50.0)
|
rubocop (>= 1.0)
|
||||||
sysexits (~> 1.1)
|
sysexits (~> 1.1)
|
||||||
hamlit (3.0.3-x86_64-linux-musl)
|
hamlit (3.0.3-x86_64-linux-musl)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
|
@ -249,11 +267,16 @@ GEM
|
||||||
heapy (0.2.0)
|
heapy (0.2.0)
|
||||||
thor
|
thor
|
||||||
hiredis (0.6.3-x86_64-linux-musl)
|
hiredis (0.6.3-x86_64-linux-musl)
|
||||||
|
hiredis-client (0.14.1-x86_64-linux-musl)
|
||||||
|
redis-client (= 0.14.1)
|
||||||
|
htmlbeautifier (1.4.2)
|
||||||
http_parser.rb (0.8.0-x86_64-linux-musl)
|
http_parser.rb (0.8.0-x86_64-linux-musl)
|
||||||
httparty (0.21.0)
|
httparty (0.21.0)
|
||||||
mini_mime (>= 1.0.0)
|
mini_mime (>= 1.0.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
i18n (1.14.1)
|
httparty-cache (0.0.6)
|
||||||
|
httparty (~> 0.18)
|
||||||
|
i18n (1.14.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
icalendar (2.8.0)
|
icalendar (2.8.0)
|
||||||
ice_cube (~> 0.16)
|
ice_cube (~> 0.16)
|
||||||
|
@ -284,9 +307,7 @@ GEM
|
||||||
terminal-table (~> 2.0)
|
terminal-table (~> 2.0)
|
||||||
jekyll-commonmark (1.4.0)
|
jekyll-commonmark (1.4.0)
|
||||||
commonmarker (~> 0.22)
|
commonmarker (~> 0.22)
|
||||||
jekyll-data (1.1.2)
|
jekyll-images (0.4.4)
|
||||||
jekyll (>= 3.3, < 5.0.0)
|
|
||||||
jekyll-images (0.4.1)
|
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
ruby-filemagic (~> 0.7)
|
ruby-filemagic (~> 0.7)
|
||||||
ruby-vips (~> 2)
|
ruby-vips (~> 2)
|
||||||
|
@ -296,7 +317,7 @@ GEM
|
||||||
sassc (> 2.0.1, < 3.0)
|
sassc (> 2.0.1, < 3.0)
|
||||||
jekyll-watch (2.2.1)
|
jekyll-watch (2.2.1)
|
||||||
listen (~> 3.0)
|
listen (~> 3.0)
|
||||||
json (2.6.3-x86_64-linux-musl)
|
json (2.7.1-x86_64-linux-musl)
|
||||||
jwt (2.6.0)
|
jwt (2.6.0)
|
||||||
kaminari (1.2.2)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
|
@ -325,12 +346,12 @@ GEM
|
||||||
loaf (0.10.0)
|
loaf (0.10.0)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
lockbox (1.2.0)
|
lockbox (1.2.0)
|
||||||
lograge (0.12.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.21.3)
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
|
@ -338,45 +359,45 @@ GEM
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.2)
|
marcel (1.0.4)
|
||||||
memory_profiler (1.0.1)
|
memory_profiler (1.0.1)
|
||||||
mercenary (0.4.0)
|
mercenary (0.4.0)
|
||||||
method_source (1.0.0)
|
method_source (1.1.0)
|
||||||
mini_histogram (0.3.1)
|
mini_histogram (0.3.1)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.2)
|
mini_portile2 (2.8.6)
|
||||||
minitest (5.18.0)
|
minitest (5.25.1)
|
||||||
mobility (1.2.9)
|
mobility (1.2.9)
|
||||||
i18n (>= 0.6.10, < 2)
|
i18n (>= 0.6.10, < 2)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
net-imap (0.3.4)
|
mustermann (3.0.0)
|
||||||
|
ruby2_keywords (~> 0.0.1)
|
||||||
|
net-imap (0.4.16)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.1.0)
|
net-ssh (7.2.1)
|
||||||
netaddr (2.0.6)
|
netaddr (2.0.6)
|
||||||
nio4r (2.5.9-x86_64-linux-musl)
|
nio4r (2.7.3-x86_64-linux-musl)
|
||||||
njalla-api-client (0.2.0)
|
nokogiri (1.16.7-x86_64-linux-musl)
|
||||||
dry-schema
|
|
||||||
httparty (~> 0.18)
|
|
||||||
nokogiri (1.15.4-x86_64-linux-musl)
|
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
pairing_heap (3.0.1)
|
pairing_heap (3.0.1)
|
||||||
parallel (1.23.0)
|
parallel (1.24.0)
|
||||||
parser (3.2.2.1)
|
parser (3.2.2.3)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
|
racc
|
||||||
pathutil (0.16.2)
|
pathutil (0.16.2)
|
||||||
forwardable-extended (~> 2.6)
|
forwardable-extended (~> 2.6)
|
||||||
pg (1.5.3-x86_64-linux-musl)
|
pg (1.5.4-x86_64-linux-musl)
|
||||||
pg_search (2.3.6)
|
pg_search (2.3.6)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
|
@ -386,55 +407,63 @@ GEM
|
||||||
pry (0.14.2)
|
pry (0.14.2)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
public_suffix (5.0.3)
|
public_suffix (5.0.5)
|
||||||
puma (6.3.1-x86_64-linux-musl)
|
puma (6.4.3-x86_64-linux-musl)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.0)
|
pundit (2.3.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
que (2.2.1)
|
que (2.2.1)
|
||||||
racc (1.7.1-x86_64-linux-musl)
|
que-web (0.10.0)
|
||||||
rack (2.2.7)
|
que (>= 1)
|
||||||
rack-cors (2.0.1)
|
sinatra
|
||||||
|
racc (1.8.1-x86_64-linux-musl)
|
||||||
|
rack (2.2.9)
|
||||||
|
rack-cors (2.0.2)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-mini-profiler (3.1.0)
|
rack-mini-profiler (3.1.0)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-proxy (0.7.6)
|
rack-protection (3.2.0)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
|
rack (~> 2.2, >= 2.2.4)
|
||||||
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (6.1.7.3)
|
rails (6.1.7.8)
|
||||||
actioncable (= 6.1.7.3)
|
actioncable (= 6.1.7.8)
|
||||||
actionmailbox (= 6.1.7.3)
|
actionmailbox (= 6.1.7.8)
|
||||||
actionmailer (= 6.1.7.3)
|
actionmailer (= 6.1.7.8)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.8)
|
||||||
actiontext (= 6.1.7.3)
|
actiontext (= 6.1.7.8)
|
||||||
actionview (= 6.1.7.3)
|
actionview (= 6.1.7.8)
|
||||||
activejob (= 6.1.7.3)
|
activejob (= 6.1.7.8)
|
||||||
activemodel (= 6.1.7.3)
|
activemodel (= 6.1.7.8)
|
||||||
activerecord (= 6.1.7.3)
|
activerecord (= 6.1.7.8)
|
||||||
activestorage (= 6.1.7.3)
|
activestorage (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.7.3)
|
railties (= 6.1.7.8)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 5.0.0)
|
||||||
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.5.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.19, >= 2.19.1)
|
loofah (~> 2.21)
|
||||||
rails-i18n (7.0.7)
|
nokogiri (~> 1.14)
|
||||||
|
rails-i18n (7.0.8)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
rails_warden (0.6.0)
|
rails_warden (0.6.0)
|
||||||
warden (>= 1.2.0)
|
warden (>= 1.2.0)
|
||||||
railties (6.1.7.3)
|
railties (6.1.7.8)
|
||||||
actionpack (= 6.1.7.3)
|
actionpack (= 6.1.7.8)
|
||||||
activesupport (= 6.1.7.3)
|
activesupport (= 6.1.7.8)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.2.1)
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
|
@ -446,6 +475,8 @@ GEM
|
||||||
redis-activesupport (5.3.0)
|
redis-activesupport (5.3.0)
|
||||||
activesupport (>= 3, < 8)
|
activesupport (>= 3, < 8)
|
||||||
redis-store (>= 1.3, < 2)
|
redis-store (>= 1.3, < 2)
|
||||||
|
redis-client (0.14.1)
|
||||||
|
connection_pool
|
||||||
redis-rack (2.1.4)
|
redis-rack (2.1.4)
|
||||||
rack (>= 2.0.8, < 3)
|
rack (>= 2.0.8, < 3)
|
||||||
redis-store (>= 1.2, < 2)
|
redis-store (>= 1.2, < 2)
|
||||||
|
@ -455,18 +486,19 @@ GEM
|
||||||
redis-store (>= 1.2, < 2)
|
redis-store (>= 1.2, < 2)
|
||||||
redis-store (1.9.2)
|
redis-store (1.9.2)
|
||||||
redis (>= 4, < 6)
|
redis (>= 4, < 6)
|
||||||
regexp_parser (2.8.0)
|
regexp_parser (2.9.0)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.0)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.2.5)
|
rexml (3.3.7)
|
||||||
rgl (0.6.3)
|
rgl (0.6.3)
|
||||||
pairing_heap (>= 0.3.0)
|
pairing_heap (>= 0.3.0)
|
||||||
rexml (~> 3.2, >= 3.2.4)
|
rexml (~> 3.2, >= 3.2.4)
|
||||||
stream (~> 0.5.3)
|
stream (~> 0.5.3)
|
||||||
rouge (3.30.0)
|
rouge (3.30.0)
|
||||||
|
rubanok (0.5.0)
|
||||||
rubocop (1.42.0)
|
rubocop (1.42.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
|
@ -477,24 +509,28 @@ GEM
|
||||||
rubocop-ast (>= 1.24.1, < 2.0)
|
rubocop-ast (>= 1.24.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
rubocop-ast (1.28.1)
|
rubocop-ast (1.30.0)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.2.1.0)
|
||||||
rubocop-rails (2.19.1)
|
rubocop-rails (2.23.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
|
rubocop-ast (>= 1.30.0, < 2.0)
|
||||||
|
ruby-brs (1.3.3-x86_64-linux-musl)
|
||||||
|
adsp (~> 1.0)
|
||||||
ruby-filemagic (0.7.3-x86_64-linux-musl)
|
ruby-filemagic (0.7.3-x86_64-linux-musl)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-statistics (3.0.2)
|
ruby-statistics (3.0.2)
|
||||||
ruby-vips (2.1.4)
|
ruby-vips (2.2.0)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
ruby2ruby (2.5.0)
|
ruby2ruby (2.5.0)
|
||||||
ruby_parser (~> 3.1)
|
ruby_parser (~> 3.1)
|
||||||
sexp_processor (~> 4.6)
|
sexp_processor (~> 4.6)
|
||||||
ruby_parser (3.20.1)
|
ruby_parser (3.20.1)
|
||||||
sexp_processor (~> 4.16)
|
sexp_processor (~> 4.16)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
rugged (1.6.3-x86_64-linux-musl)
|
rugged (1.5.0.1-x86_64-linux-musl)
|
||||||
safe_yaml (1.0.6)
|
safe_yaml (1.0.6)
|
||||||
safely_block (0.3.0)
|
safely_block (0.3.0)
|
||||||
errbase (>= 0.1.1)
|
errbase (>= 0.1.1)
|
||||||
|
@ -514,19 +550,24 @@ GEM
|
||||||
sexp_processor (4.17.0)
|
sexp_processor (4.17.0)
|
||||||
simpleidn (0.2.1)
|
simpleidn (0.2.1)
|
||||||
unf (~> 0.1.4)
|
unf (~> 0.1.4)
|
||||||
|
sinatra (3.2.0)
|
||||||
|
mustermann (~> 3.0)
|
||||||
|
rack (~> 2.2, >= 2.2.4)
|
||||||
|
rack-protection (= 3.2.0)
|
||||||
|
tilt (~> 2.0)
|
||||||
sourcemap (0.1.1)
|
sourcemap (0.1.1)
|
||||||
spring (4.1.1)
|
spring (4.1.1)
|
||||||
spring-watcher-listen (2.1.0)
|
spring-watcher-listen (2.1.0)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
spring (>= 4)
|
spring (>= 4)
|
||||||
sprockets (4.2.0)
|
sprockets (4.2.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (>= 2.2.4, < 4)
|
rack (>= 2.2.4, < 4)
|
||||||
sprockets-rails (3.4.2)
|
sprockets-rails (3.5.2)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 6.1)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sqlite3 (1.6.3-x86_64-linux-musl)
|
sqlite3 (1.7.0-x86_64-linux-musl)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
stackprof (0.2.25-x86_64-linux-musl)
|
stackprof (0.2.25-x86_64-linux-musl)
|
||||||
stream (0.5.5)
|
stream (0.5.5)
|
||||||
|
@ -535,13 +576,13 @@ GEM
|
||||||
jekyll (~> 4)
|
jekyll (~> 4)
|
||||||
symbol-fstring (1.0.2-x86_64-linux-musl)
|
symbol-fstring (1.0.2-x86_64-linux-musl)
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.10.1)
|
temple (0.10.3)
|
||||||
terminal-table (2.0.0)
|
terminal-table (2.0.0)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
thor (1.2.2)
|
thor (1.3.2)
|
||||||
tilt (2.1.0)
|
tilt (2.4.0)
|
||||||
timecop (0.9.6)
|
timecop (0.9.6)
|
||||||
timeout (0.3.2)
|
timeout (0.4.1)
|
||||||
turbolinks (5.2.1)
|
turbolinks (5.2.1)
|
||||||
turbolinks-source (~> 5.2)
|
turbolinks-source (~> 5.2)
|
||||||
turbolinks-source (5.2.0)
|
turbolinks-source (5.2.0)
|
||||||
|
@ -551,8 +592,9 @@ GEM
|
||||||
execjs (>= 0.3.0, < 3)
|
execjs (>= 0.3.0, < 3)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.8.2-x86_64-linux-musl)
|
unf_ext (0.0.9-x86_64-linux-musl)
|
||||||
unicode-display_width (1.8.0)
|
unicode-display_width (1.8.0)
|
||||||
|
uri-ssh_git (2.0.0)
|
||||||
validates_hostname (1.0.13)
|
validates_hostname (1.0.13)
|
||||||
activerecord (>= 3.0)
|
activerecord (>= 3.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
|
@ -568,36 +610,42 @@ GEM
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
webrick (1.8.1)
|
webrick (1.8.2)
|
||||||
websocket (1.2.9)
|
websocket (1.2.9)
|
||||||
websocket-driver (0.7.6-x86_64-linux-musl)
|
websocket-driver (0.7.6-x86_64-linux-musl)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.6.8)
|
yard (0.9.36)
|
||||||
|
zeitwerk (2.6.18)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x86_64-linux-musl
|
x86_64-linux-musl
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
aasm
|
||||||
|
after_commit_everywhere (~> 1.0)
|
||||||
bcrypt (~> 3.1.7)
|
bcrypt (~> 3.1.7)
|
||||||
bcrypt_pbkdf
|
bcrypt_pbkdf
|
||||||
blazer
|
blazer
|
||||||
bootstrap (~> 4)
|
bootstrap (~> 4)
|
||||||
brakeman
|
brakeman
|
||||||
|
bundler-audit
|
||||||
capybara (~> 2.13)
|
capybara (~> 2.13)
|
||||||
chartkick
|
chartkick
|
||||||
commonmarker
|
commonmarker
|
||||||
concurrent-ruby-ext
|
concurrent-ruby-ext
|
||||||
database_cleaner
|
database_cleaner
|
||||||
derailed_benchmarks
|
derailed_benchmarks
|
||||||
|
device_detector
|
||||||
devise
|
devise
|
||||||
devise-i18n
|
devise-i18n
|
||||||
devise_invitable
|
devise_invitable
|
||||||
distributed-press-api-client (~> 0.2.3)
|
distributed-press-api-client (~> 0.4.1)
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
down
|
down
|
||||||
|
dry-schema
|
||||||
ed25519
|
ed25519
|
||||||
email_address!
|
email_address!
|
||||||
exception_notification
|
exception_notification
|
||||||
|
@ -606,10 +654,13 @@ DEPENDENCIES
|
||||||
fast_jsonparser (~> 0.5.0)
|
fast_jsonparser (~> 0.5.0)
|
||||||
flamegraph
|
flamegraph
|
||||||
friendly_id
|
friendly_id
|
||||||
|
git_clone_url
|
||||||
hairtrigger
|
hairtrigger
|
||||||
haml-lint
|
haml-lint
|
||||||
hamlit-rails
|
hamlit-rails
|
||||||
hiredis
|
hiredis
|
||||||
|
hiredis-client
|
||||||
|
htmlbeautifier
|
||||||
httparty
|
httparty
|
||||||
icalendar
|
icalendar
|
||||||
image_processing
|
image_processing
|
||||||
|
@ -617,7 +668,6 @@ DEPENDENCIES
|
||||||
jbuilder (~> 2.5)
|
jbuilder (~> 2.5)
|
||||||
jekyll (~> 4.2.0)
|
jekyll (~> 4.2.0)
|
||||||
jekyll-commonmark (~> 1.4.0)
|
jekyll-commonmark (~> 1.4.0)
|
||||||
jekyll-data
|
|
||||||
jekyll-images
|
jekyll-images
|
||||||
jekyll-include-cache
|
jekyll-include-cache
|
||||||
kaminari
|
kaminari
|
||||||
|
@ -630,7 +680,6 @@ DEPENDENCIES
|
||||||
mini_magick
|
mini_magick
|
||||||
mobility
|
mobility
|
||||||
net-ssh
|
net-ssh
|
||||||
njalla-api-client (~> 0.2.0)
|
|
||||||
nokogiri
|
nokogiri
|
||||||
pg
|
pg
|
||||||
pg_search
|
pg_search
|
||||||
|
@ -639,18 +688,22 @@ DEPENDENCIES
|
||||||
puma
|
puma
|
||||||
pundit
|
pundit
|
||||||
que
|
que
|
||||||
|
que-web
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-mini-profiler
|
rack-mini-profiler
|
||||||
rails (~> 6.1.0)
|
rails (~> 6.1.0)
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rails_warden
|
rails_warden
|
||||||
redis (~> 4.0)
|
redis (~> 4.0)
|
||||||
|
redis-client
|
||||||
redis-rails
|
redis-rails
|
||||||
rgl
|
rgl
|
||||||
rollups!
|
rollups!
|
||||||
|
rubanok
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
|
ruby-brs
|
||||||
rubyzip
|
rubyzip
|
||||||
rugged
|
rugged (= 1.5.0.1)
|
||||||
safe_yaml
|
safe_yaml
|
||||||
safely_block (~> 0.3.0)
|
safely_block (~> 0.3.0)
|
||||||
sassc-rails
|
sassc-rails
|
||||||
|
@ -670,6 +723,7 @@ DEPENDENCIES
|
||||||
web-console
|
web-console
|
||||||
webpacker
|
webpacker
|
||||||
yaml_db!
|
yaml_db!
|
||||||
|
yard
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.1.4p223
|
ruby 3.1.4p223
|
||||||
|
|
140
Makefile
140
Makefile
|
@ -1,140 +0,0 @@
|
||||||
SHELL := /bin/bash
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
|
|
||||||
# Copiar el archivo de configuración y avisar cuando hay que
|
|
||||||
# actualizarlo.
|
|
||||||
.env: .env.example
|
|
||||||
@test -f $@ || cp -v $< $@
|
|
||||||
@test -f $@ && echo "Revisa $@ para actualizarlo con respecto a $<"
|
|
||||||
@test -f $@ && diff -auN --color $@ $<
|
|
||||||
|
|
||||||
include .env
|
|
||||||
|
|
||||||
export
|
|
||||||
|
|
||||||
# XXX: El espacio antes del comentario cuenta como espacio
|
|
||||||
args ?=## Argumentos para Hain
|
|
||||||
commit ?= origin/rails## Commit desde el que actualizar
|
|
||||||
env ?= staging## Entorno del nodo delegado
|
|
||||||
sutty ?= $(SUTTY)## Dirección local
|
|
||||||
delegate ?= $(DELEGATE)## Cambia el nodo delegado
|
|
||||||
hain ?= ENV_FILE=.env $(HAINISH)## Ubicación de Hainish
|
|
||||||
|
|
||||||
# El nodo delegado tiene dos entornos, production y staging.
|
|
||||||
# Dependiendo del entorno que elijamos, se van a generar los assets y el
|
|
||||||
# contenedor y subirse a un servidor u otro. No utilizamos CI/CD (aún).
|
|
||||||
#
|
|
||||||
# Production es el entorno de panel.sutty.nl
|
|
||||||
ifeq ($(env),production)
|
|
||||||
container ?= panel
|
|
||||||
## TODO: Cambiar a otra cosa
|
|
||||||
branch ?= rails
|
|
||||||
public ?= public
|
|
||||||
endif
|
|
||||||
|
|
||||||
# Staging es el entorno de panel.staging.sutty.nl
|
|
||||||
ifeq ($(env),staging)
|
|
||||||
container := staging
|
|
||||||
branch := staging
|
|
||||||
public := staging
|
|
||||||
endif
|
|
||||||
|
|
||||||
help: always ## Ayuda
|
|
||||||
@echo -e "Sutty\n" | sed -re "s/^.*/\x1B[38;5;197m&\x1B[0m/"
|
|
||||||
@echo -e "Servidor: https://panel.$(SUTTY_WITH_PORT)/\n"
|
|
||||||
@echo -e "Uso: make TAREA args=\"ARGUMENTOS\"\n"
|
|
||||||
@echo -e "Tareas:\n"
|
|
||||||
@grep -E "^[a-z\-]+:.*##" Makefile | sed -re "s/(.*):.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
|
|
||||||
@echo -e "\nArgumentos:\n"
|
|
||||||
@grep -E "^[a-z\-]+ \?=.*##" Makefile | sed -re "s/(.*) \?=.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
|
|
||||||
|
|
||||||
test: always ## Ejecutar los tests
|
|
||||||
$(MAKE) rake args="test RAILS_ENV=test $(args)"
|
|
||||||
|
|
||||||
postgresql: /etc/hosts ## Iniciar la base de datos
|
|
||||||
pgrep postgres >/dev/null || $(hain) postgresql
|
|
||||||
|
|
||||||
serve-js: /etc/hosts node_modules ## Iniciar el servidor de desarrollo de Javascript
|
|
||||||
$(hain) 'bundle exec ./bin/webpack-dev-server'
|
|
||||||
|
|
||||||
serve: /etc/hosts postgresql Gemfile.lock ## Iniciar el servidor de desarrollo de Rails
|
|
||||||
$(MAKE) rails args=server
|
|
||||||
|
|
||||||
rails: ## Corre rails dentro del entorno de desarrollo (pasar argumentos con args=).
|
|
||||||
$(MAKE) bundle args="exec rails $(args)"
|
|
||||||
|
|
||||||
rake: ## Corre rake dentro del entorno de desarrollo (pasar argumentos con args=).
|
|
||||||
$(MAKE) bundle args="exec rake $(args)"
|
|
||||||
|
|
||||||
bundle: ## Corre bundle dentro del entorno de desarrollo (pasar argumentos con args=).
|
|
||||||
$(hain) 'bundle $(args)'
|
|
||||||
|
|
||||||
psql := psql -h $(PG_HOST) -U $(PG_USER) -p $(PG_PORT) -d sutty
|
|
||||||
copy-table:
|
|
||||||
test -n "$(table)"
|
|
||||||
echo "truncate $(table) $(cascade);" | $(psql)
|
|
||||||
ssh $(delegate) docker exec postgresql pg_dump -U sutty -d sutty -t $(table) | $(psql)
|
|
||||||
|
|
||||||
psql:
|
|
||||||
$(psql)
|
|
||||||
|
|
||||||
rubocop: ## Yutea el código que está por ser commiteado
|
|
||||||
git status --porcelain \
|
|
||||||
| grep -E "^(A|M)" \
|
|
||||||
| sed "s/^...//" \
|
|
||||||
| grep ".rb$$" \
|
|
||||||
| ../haini.sh/haini.sh "xargs -r ./bin/rubocop --auto-correct"
|
|
||||||
|
|
||||||
audit: ## Encuentra dependencias con vulnerabilidades
|
|
||||||
$(hain) 'gem install bundler-audit'
|
|
||||||
$(hain) 'bundle audit --update'
|
|
||||||
|
|
||||||
brakeman: ## Busca posibles vulnerabilidades en Sutty
|
|
||||||
$(MAKE) bundle args='exec brakeman'
|
|
||||||
|
|
||||||
yarn: ## Tareas de yarn
|
|
||||||
$(hain) 'yarn $(args)'
|
|
||||||
|
|
||||||
clean: ## Limpieza
|
|
||||||
rm -rf _sites/test-* _deploy/test-* log/*.log tmp/cache tmp/letter_opener tmp/miniprofiler tmp/storage
|
|
||||||
|
|
||||||
build: Gemfile.lock ## Generar la imagen Docker
|
|
||||||
time docker build --build-arg="BRANCH=$(branch)" --build-arg="RAILS_MASTER_KEY=`cat config/master.key`" -t sutty/$(container) .
|
|
||||||
docker tag sutty/$(container):latest sutty:keep
|
|
||||||
@echo -e "\a"
|
|
||||||
|
|
||||||
save: ## Subir la imagen Docker al nodo delegado
|
|
||||||
time docker save sutty/$(container):latest | ssh root@$(delegate) docker load
|
|
||||||
date +%F | xargs -I {} git tag -f $(container)-{}
|
|
||||||
@echo -e "\a"
|
|
||||||
|
|
||||||
ota: ## Actualizar Rails en el nodo delegado
|
|
||||||
git push
|
|
||||||
ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl pull
|
|
||||||
ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl lfs prune
|
|
||||||
ssh $(delegate) chown -R 1000:82 /srv/sutty/srv/http/panel.sutty.nl
|
|
||||||
ssh $(delegate) docker exec $(container) rails reload
|
|
||||||
|
|
||||||
# Correr un test en particular por ejemplo
|
|
||||||
# `make test/models/usuarie_test.rb`
|
|
||||||
tests := $(shell find test/ -name "*_test.rb")
|
|
||||||
$(tests): always
|
|
||||||
$(MAKE) test args="TEST=$@"
|
|
||||||
|
|
||||||
# Agrega las direcciones locales al sistema
|
|
||||||
/etc/hosts: always
|
|
||||||
@echo "Chequeando si es necesario agregar el dominio local $(SUTTY)"
|
|
||||||
@grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@
|
|
||||||
@grep -q " api.$(SUTTY)$$" $@ || echo -e "127.0.0.1 api.$(SUTTY)\n::1 api.$(SUTTY)" | sudo tee -a $@
|
|
||||||
@grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 panel.$(SUTTY)" | sudo tee -a $@
|
|
||||||
@grep -q " postgresql.$(SUTTY)$$" $@ || echo -e "127.0.0.1 postgresql.$(SUTTY)\n::1 postgresql.$(SUTTY)" | sudo tee -a $@
|
|
||||||
|
|
||||||
# Instala las dependencias de Javascript
|
|
||||||
node_modules: package.json
|
|
||||||
$(MAKE) yarn
|
|
||||||
|
|
||||||
# Instala las dependencias de Rails
|
|
||||||
Gemfile.lock: Gemfile
|
|
||||||
$(MAKE) bundle args=install
|
|
||||||
|
|
||||||
.PHONY: always
|
|
2
Procfile
2
Procfile
|
@ -7,5 +7,7 @@ blazer: bundle exec rake blazer:send_failing_checks
|
||||||
prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
|
prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
|
||||||
distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
|
distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
|
||||||
cleanup: bundle exec rake cleanup:everything
|
cleanup: bundle exec rake cleanup:everything
|
||||||
|
emergency_cleanup: bundle exec rake cleanup:everything BEFORE=7
|
||||||
stats: bundle exec rake stats:process_all
|
stats: bundle exec rake stats:process_all
|
||||||
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que
|
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que
|
||||||
|
fediblock: bundle exec rails activity_pub:fediblocks
|
||||||
|
|
51
README.md
51
README.md
|
@ -17,14 +17,32 @@ Para más información visita el [sitio de Sutty](https://sutty.nl/).
|
||||||
|
|
||||||
### Desarrollar
|
### Desarrollar
|
||||||
|
|
||||||
Todas las tareas se gestionan con `make`, por favor instala GNU Make
|
Para facilitar la gestión de dependencias y entorno de desarrollo,
|
||||||
antes de comenzar.
|
instala [haini.sh](https://0xacab.org/sutty/haini.sh)
|
||||||
|
|
||||||
|
|
||||||
|
Todas las tareas se gestionan con `go-task`. [Instrucciones de
|
||||||
|
instalación (en inglés)](https://taskfile.dev/installation/)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make help
|
go-task
|
||||||
```
|
```
|
||||||
|
|
||||||
[Leer la documentación](https://docs.sutty.nl/)
|
### Variables de entorno
|
||||||
|
|
||||||
|
Las variables de entorno por defecto se encuentran en el archivo `.env`.
|
||||||
|
Para modificar las opciones, crear o modificar el archivo `.env.local`
|
||||||
|
con valores distintos.
|
||||||
|
|
||||||
|
### Documentación
|
||||||
|
|
||||||
|
Para navegar la documentación del código usando YARD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go-task doc serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Y luego navegar a <https://panel.sutty.local:3000/doc/>
|
||||||
|
|
||||||
## English
|
## English
|
||||||
|
|
||||||
|
@ -39,10 +57,29 @@ For more information, visit [Sutty's website](https://sutty.nl/en/).
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
Every task is run via `make`, please install GNU Make before developing.
|
|
||||||
|
To facilitate dependencies and dev environment, install
|
||||||
|
[haini.sh](https://0xacab.org/sutty/haini.sh)
|
||||||
|
|
||||||
|
Every task is run via `go-task`. [Installation
|
||||||
|
instructions](https://taskfile.dev/installation/).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make help
|
go-task
|
||||||
```
|
```
|
||||||
|
|
||||||
[Read the documentation](https://docs.sutty.nl/en/)
|
### Environment variables
|
||||||
|
|
||||||
|
Default env vars are store on `.env`. For local options, copy them to
|
||||||
|
`.env.local`.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
To browse documentation using YARD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go-task doc serve
|
||||||
|
```
|
||||||
|
|
||||||
|
And then open <https://panel.sutty.local:3000/doc/>
|
||||||
|
|
||||||
|
|
197
Taskfile.yaml
Normal file
197
Taskfile.yaml
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
---
|
||||||
|
version: "3"
|
||||||
|
vars:
|
||||||
|
CURRENT_BRANCH:
|
||||||
|
sh: "git rev-parse --abbrev-ref HEAD"
|
||||||
|
shopt:
|
||||||
|
- "globstar"
|
||||||
|
dotenv:
|
||||||
|
- ".env.development"
|
||||||
|
- ".env"
|
||||||
|
- ".env.local"
|
||||||
|
- ".env.development.local"
|
||||||
|
tasks:
|
||||||
|
credentials:
|
||||||
|
desc: "Generate credentials file"
|
||||||
|
cmds:
|
||||||
|
- "cp --no-clobber config/credentials.yml.enc.ci config/credentials.yml.enc"
|
||||||
|
sources:
|
||||||
|
- "config/credentials.yml.enc.ci"
|
||||||
|
generates:
|
||||||
|
- "config/credentials.yml.enc"
|
||||||
|
gems:
|
||||||
|
desc: "Install gems"
|
||||||
|
deps:
|
||||||
|
- "credentials"
|
||||||
|
cmds:
|
||||||
|
- "{{.HAINISH}} bundle config set --local path './vendor'"
|
||||||
|
- "{{.HAINISH}} bundle install"
|
||||||
|
sources:
|
||||||
|
- "Gemfile"
|
||||||
|
generates:
|
||||||
|
- "Gemfile.lock"
|
||||||
|
status:
|
||||||
|
- "test -d vendor/ruby"
|
||||||
|
clean:
|
||||||
|
desc: "Clean"
|
||||||
|
cmds:
|
||||||
|
- "rm -rf _sites/test-* _deploy/test-* log/*.log tmp/cache tmp/letter_opener tmp/miniprofiler tmp/storage"
|
||||||
|
node-modules:
|
||||||
|
desc: "Install Node modules"
|
||||||
|
cmds:
|
||||||
|
- "{{.HAINISH}} yarn"
|
||||||
|
sources:
|
||||||
|
- "package.json"
|
||||||
|
- "yarn.lock"
|
||||||
|
status:
|
||||||
|
- "test -d node_modules"
|
||||||
|
assets:
|
||||||
|
desc: "Generate assets"
|
||||||
|
deps:
|
||||||
|
- "node-modules"
|
||||||
|
- "gems"
|
||||||
|
cmds:
|
||||||
|
- "git lfs fetch"
|
||||||
|
- "git lfs checkout"
|
||||||
|
- task: "rails"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "webpacker:clobber RAILS_ENV=production"
|
||||||
|
- task: "rails"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "assets:precompile RAILS_ENV=production"
|
||||||
|
- task: "rails"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "assets:clean RAILS_ENV=production"
|
||||||
|
sources:
|
||||||
|
- "package.json"
|
||||||
|
- "yarn.lock"
|
||||||
|
- "app/assets/**/*"
|
||||||
|
- "app/javascript/**/*"
|
||||||
|
generates:
|
||||||
|
- "public/packs/manifest.json"
|
||||||
|
hosts:
|
||||||
|
desc: "Local DNS resolution for hostnames"
|
||||||
|
interactive: true
|
||||||
|
cmds:
|
||||||
|
- "echo -e \"127.0.0.1 panel.{{.SUTTY}} api.{{.SUTTY}} postgresql.{{.SUTTY}}\" | sudo tee -a /etc/hosts"
|
||||||
|
- "echo -e \"::1 panel.{{.SUTTY}} api.{{.SUTTY}} postgresql.{{.SUTTY}}\" | sudo tee -a /etc/hosts"
|
||||||
|
status:
|
||||||
|
- "grep -q \" panel.{{.SUTTY}} \" /etc/hosts"
|
||||||
|
database-init:
|
||||||
|
desc: "Database install"
|
||||||
|
cmds:
|
||||||
|
- "{{.HAINISH}} /usr/bin/initdb --locale en_US.utf8 -E UTF8 -D /var/lib/postgresql/{{.PGVER}}/data"
|
||||||
|
- "echo \"host all all samenet trust\" >> ../hain/var/lib/postgresql/{{.PGVER}}/data/pg_hba.conf"
|
||||||
|
- "echo \"listen_addresses = '*'\" >> ../hain/var/lib/postgresql/{{.PGVER}}/data/postgresql.conf"
|
||||||
|
- "echo \"external_pid_file = '{{.PGPID}}'\" >> ../hain/var/lib/postgresql/{{.PGVER}}/data/postgresql.conf"
|
||||||
|
- "install -dm755 ../hain/run/postgresql"
|
||||||
|
status:
|
||||||
|
- "test -d ../hain/var/lib/postgresql/{{.PGVER}}/data"
|
||||||
|
- "test -f ../hain/var/lib/postgresql/{{.PGVER}}/data/postgresql.conf"
|
||||||
|
database:
|
||||||
|
desc: "Database"
|
||||||
|
deps:
|
||||||
|
- "database-init"
|
||||||
|
cmds:
|
||||||
|
- "{{.HAINISH}} daemonize -c /var/lib/postgresql/{{.PGVER}}/data /usr/bin/postgres -D /var/lib/postgresql/{{.PGVER}}/data"
|
||||||
|
status:
|
||||||
|
- "test -f ../hain{{.PGPID}}"
|
||||||
|
- "pgrep -F ../hain{{.PGPID}}"
|
||||||
|
prepare:
|
||||||
|
desc: "Create database or run pending migrations"
|
||||||
|
deps:
|
||||||
|
- "database"
|
||||||
|
cmds:
|
||||||
|
- task: "rails"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "db:prepare"
|
||||||
|
serve:
|
||||||
|
desc: "Run Rails development server"
|
||||||
|
deps:
|
||||||
|
- "prepare"
|
||||||
|
- "gems"
|
||||||
|
cmds:
|
||||||
|
- ": == Development server running at https://panel.{{.SUTTY_WITH_PORT}} =="
|
||||||
|
- task: "rails"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "server"
|
||||||
|
status:
|
||||||
|
- "test -f tmp/pids/server.pid"
|
||||||
|
- "pgrep -F tmp/pids/server.pid"
|
||||||
|
yarn:
|
||||||
|
desc: "Yarn. Call with: go-task yarn -- arguments"
|
||||||
|
deps:
|
||||||
|
- "node-modules"
|
||||||
|
cmds:
|
||||||
|
- "{{.HAINISH}} yarn {{.CLI_ARGS}}"
|
||||||
|
- defer:
|
||||||
|
task: "notify"
|
||||||
|
bundle:
|
||||||
|
desc: "Bundle. Call with: go-task bundle -- arguments"
|
||||||
|
interactive: true
|
||||||
|
deps:
|
||||||
|
- "gems"
|
||||||
|
cmds:
|
||||||
|
- "{{.HAINISH}} bundle {{.CLI_ARGS}}"
|
||||||
|
- defer:
|
||||||
|
task: "notify"
|
||||||
|
rails:
|
||||||
|
desc: "Rails. Call with: go-task rails -- arguments"
|
||||||
|
cmds:
|
||||||
|
- task: "bundle"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "exec rails {{.CLI_ARGS}}"
|
||||||
|
console:
|
||||||
|
desc: "Rails console"
|
||||||
|
interactive: true
|
||||||
|
cmds:
|
||||||
|
- task: "rails"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "console"
|
||||||
|
doc:
|
||||||
|
desc: "Build documentation"
|
||||||
|
deps:
|
||||||
|
- "gems"
|
||||||
|
cmds:
|
||||||
|
- task: "bundle"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "exec yardoc -o public/doc app lib config db"
|
||||||
|
gem-audit:
|
||||||
|
desc: "Audit Gem dependencies"
|
||||||
|
deps:
|
||||||
|
- "gems"
|
||||||
|
- "bundler-audit"
|
||||||
|
cmds:
|
||||||
|
- task: "bundle"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "audit --update"
|
||||||
|
node-audit:
|
||||||
|
desc: "Audit Node dependencies"
|
||||||
|
deps:
|
||||||
|
- "node-modules"
|
||||||
|
cmds:
|
||||||
|
- task: "yarn"
|
||||||
|
vars:
|
||||||
|
CLI_ARGS: "audit"
|
||||||
|
notify:
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- "echo -e \"\a\""
|
||||||
|
bundler-audit:
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- "{{.HAINISH}} gem install bundler-audit"
|
||||||
|
status:
|
||||||
|
- "test -f ../hain/usr/bin/bundler-audit"
|
||||||
|
rubocop:
|
||||||
|
desc: "Ruby linting"
|
||||||
|
deps:
|
||||||
|
- "gems"
|
||||||
|
cmds:
|
||||||
|
- "./bin/modified_files | ./bin/with_extension rb | xargs -r {{.HAINISH}} bundle exec rubocop {{.CLI_ARGS}}"
|
||||||
|
haml-lint:
|
||||||
|
desc: "HAML linting"
|
||||||
|
deps:
|
||||||
|
- "gems"
|
||||||
|
cmds:
|
||||||
|
- "./bin/modified_files | ./bin/with_extension haml | xargs -r {{.HAINISH}} bundle exec haml-lint {{.CLI_ARGS}}"
|
|
@ -29,14 +29,25 @@ $sizes: (
|
||||||
"70ch": 70ch,
|
"70ch": 70ch,
|
||||||
);
|
);
|
||||||
|
|
||||||
.btn {
|
|
||||||
background-color: var(--foreground);
|
|
||||||
color: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
@import "bootstrap";
|
@import "bootstrap";
|
||||||
@import "editor";
|
@import "editor";
|
||||||
|
|
||||||
|
@each $color, $rgb in $theme-colors {
|
||||||
|
.#{$color} {
|
||||||
|
color: var(--#{$color});
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
color: var(--#{$color});
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection,
|
||||||
|
::selection {
|
||||||
|
background: var(--#{$color});
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
.editor-content {
|
.editor-content {
|
||||||
figure {
|
figure {
|
||||||
|
@ -57,6 +68,25 @@ $sizes: (
|
||||||
--background: #{$black};
|
--background: #{$black};
|
||||||
--color: #{$cyan};
|
--color: #{$cyan};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: $white;
|
||||||
|
color: $black;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $black;
|
||||||
|
background-color: $cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 0.2rem $cyan;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Encontrar la forma de generar esto desde los locales de Rails
|
// TODO: Encontrar la forma de generar esto desde los locales de Rails
|
||||||
|
@ -195,7 +225,7 @@ fieldset {
|
||||||
|
|
||||||
&[type=button] {
|
&[type=button] {
|
||||||
@extend .btn;
|
@extend .btn;
|
||||||
@extend .btn-info;
|
@extend .btn-secondary;
|
||||||
@extend .m-0;
|
@extend .m-0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,8 +239,6 @@ svg {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
margin-right: 0.3rem;
|
margin-right: 0.3rem;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
|
|
||||||
|
@ -246,7 +274,7 @@ svg {
|
||||||
color: $magenta;
|
color: $magenta;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn-secondary {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
color: $black;
|
color: $black;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -526,4 +554,70 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// force ci
|
@import "new_editor";
|
||||||
|
|
||||||
|
.new-editor {
|
||||||
|
.editor {
|
||||||
|
table {
|
||||||
|
@extend .table;
|
||||||
|
@extend .table-responsive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
p { min-height: $font-size-base * $line-height-base; }
|
||||||
|
h1 { min-height: $h1-font-size * $headings-line-height; }
|
||||||
|
h2 { min-height: $h2-font-size * $headings-line-height; }
|
||||||
|
h3 { min-height: $h3-font-size * $headings-line-height; }
|
||||||
|
h4 { min-height: $h4-font-size * $headings-line-height; }
|
||||||
|
h5 { min-height: $h5-font-size * $headings-line-height; }
|
||||||
|
h6 { min-height: $h6-font-size * $headings-line-height; }
|
||||||
|
|
||||||
|
iframe { border: 0; }
|
||||||
|
|
||||||
|
audio { width: 100%; }
|
||||||
|
|
||||||
|
img,
|
||||||
|
video,
|
||||||
|
iframe {
|
||||||
|
@extend .img-fluid;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// details styles
|
||||||
|
|
||||||
|
.details {
|
||||||
|
& > summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.hide-when-open {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-when-open {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
& > summary {
|
||||||
|
.hide-when-open {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-when-open {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
34
app/assets/stylesheets/dark.scss
Normal file
34
app/assets/stylesheets/dark.scss
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
$black: black;
|
||||||
|
$white: white;
|
||||||
|
$cyan: #13fefe;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--foreground: #{$white};
|
||||||
|
--background: #{$black};
|
||||||
|
--color: #{$cyan};
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: $white;
|
||||||
|
color: $black;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $black;
|
||||||
|
background-color: $cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 0.2rem $cyan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.editor {
|
.old.editor {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
*, *::before, *::after { box-sizing: inherit; }
|
*, *::before, *::after { box-sizing: inherit; }
|
||||||
|
|
||||||
|
|
22
app/assets/stylesheets/new_editor.scss
Normal file
22
app/assets/stylesheets/new_editor.scss
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
.new-editor {
|
||||||
|
.editor {
|
||||||
|
.menubar {
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
label.btn {
|
||||||
|
margin-bottom: 0.3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
.btn {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror,
|
||||||
|
& > ol li {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,8 +18,9 @@ module ActiveStorage
|
||||||
# para que puedan propagarse correctamente a través de todo el
|
# para que puedan propagarse correctamente a través de todo el
|
||||||
# stack.
|
# stack.
|
||||||
def blob_args
|
def blob_args
|
||||||
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys.tap do |ba|
|
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type,
|
||||||
ba[:filename] = ba[:filename].unicode_normalize
|
metadata: {}).to_h.symbolize_keys.tap do |ba|
|
||||||
|
ba[:filename] = ba[:filename].unicode_normalize.sub(/\A_+/, '')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,16 +6,45 @@ module ActiveStorage
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
|
alias_method :original_show, :show
|
||||||
|
|
||||||
|
# Permitir incrustar archivos subidos (especialmente PDFs) desde
|
||||||
|
# otros sitios.
|
||||||
|
def show
|
||||||
|
original_show.tap do |s|
|
||||||
|
response.headers.delete 'X-Frame-Options'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::RecordNotFound, with: :page_not_found
|
||||||
|
|
||||||
# Asociar el archivo subido al sitio correspondiente. Cada sitio
|
# Asociar el archivo subido al sitio correspondiente. Cada sitio
|
||||||
# tiene su propio servicio de subida de archivos.
|
# tiene su propio servicio de subida de archivos.
|
||||||
def update
|
def update
|
||||||
if (token = decode_verified_token)
|
if (token = decode_verified_token)
|
||||||
if acceptable_content?(token)
|
if acceptable_content?(token)
|
||||||
named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
|
blob = ActiveStorage::Blob.find_by_key! token[:key]
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.find_by_key token[:key]
|
|
||||||
site = Site.find_by_name token[:service_name]
|
site = Site.find_by_name token[:service_name]
|
||||||
|
|
||||||
|
if remote_file?(token)
|
||||||
|
begin
|
||||||
|
url = request.body.read
|
||||||
|
body = Down.download(url, max_size: 111.megabytes)
|
||||||
|
checksum = Digest::MD5.file(body.path).base64digest
|
||||||
|
blob.metadata[:url] = url
|
||||||
|
blob.update_columns checksum: checksum, byte_size: body.size, metadata: blob.metadata
|
||||||
|
rescue StandardError => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { key: token[:key], url: url, site: site.name })
|
||||||
|
|
||||||
|
head :content_too_large
|
||||||
|
end
|
||||||
|
else
|
||||||
|
body = request.body
|
||||||
|
checksum = token[:checksum]
|
||||||
|
end
|
||||||
|
|
||||||
|
named_disk_service(token[:service_name]).upload token[:key], body, checksum: checksum
|
||||||
|
|
||||||
site.static_files.attach(blob)
|
site.static_files.attach(blob)
|
||||||
else
|
else
|
||||||
head :unprocessable_entity
|
head :unprocessable_entity
|
||||||
|
@ -26,6 +55,17 @@ module ActiveStorage
|
||||||
rescue ActiveStorage::IntegrityError
|
rescue ActiveStorage::IntegrityError
|
||||||
head :unprocessable_entity
|
head :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remote_file?(token)
|
||||||
|
token[:content_type] == 'sutty/download-from-url'
|
||||||
|
end
|
||||||
|
|
||||||
|
def page_not_found(exception)
|
||||||
|
head :not_found
|
||||||
|
ExceptionNotifier.notify_exception(exception, data: {params: params.to_hash})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
74
app/controllers/activity_pubs_controller.rb
Normal file
74
app/controllers/activity_pubs_controller.rb
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Gestiona acciones de moderación
|
||||||
|
class ActivityPubsController < ApplicationController
|
||||||
|
include ModerationConcern
|
||||||
|
|
||||||
|
ActivityPub.events.each do |event|
|
||||||
|
define_method(event) do
|
||||||
|
authorize activity_pub
|
||||||
|
|
||||||
|
if event == :report
|
||||||
|
remote_flag_params(activity_pub).tap do |p|
|
||||||
|
activity_pub.remote_flag_id = p[:remote_flag_attributes][:id]
|
||||||
|
activity_pub.update(p)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
message =
|
||||||
|
if activity_pub.public_send(:"may_#{event}?") && activity_pub.public_send(:"#{event}!")
|
||||||
|
:success
|
||||||
|
else
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[message] = I18n.t("activity_pubs.#{event}.#{message}")
|
||||||
|
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_on_several
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
|
||||||
|
activity_pubs = site.activity_pubs.where(id: params[:activity_pub])
|
||||||
|
|
||||||
|
return if activity_pubs.count.zero?
|
||||||
|
|
||||||
|
authorize activity_pubs
|
||||||
|
|
||||||
|
action = params[:activity_pub_action].to_sym
|
||||||
|
method = :"#{action}_all!"
|
||||||
|
may = :"may_#{action}?"
|
||||||
|
|
||||||
|
return unless ActivityPub.events.include? action
|
||||||
|
|
||||||
|
# Crear una sola remote flag por autore
|
||||||
|
ActivityPub.transaction do
|
||||||
|
if action == :report
|
||||||
|
message = remote_flag_params(activity_pubs.first).dig(:remote_flag_attributes, :message)
|
||||||
|
|
||||||
|
activity_pubs.distinct.pluck(:actor_id).each do |actor_id|
|
||||||
|
remote_flag = ActivityPub::RemoteFlag.find_or_initialize_by(actor_id: actor_id, site_id: site.id)
|
||||||
|
remote_flag.message = message
|
||||||
|
# Lo estamos actualizando, con lo que lo vamos a volver a enviar
|
||||||
|
remote_flag.requeue if remote_flag.persisted?
|
||||||
|
remote_flag.save
|
||||||
|
# XXX: Idealmente todas las ActivityPub que enviamos pueden
|
||||||
|
# cambiar de estado, pero chequeamos de todas formas.
|
||||||
|
remote_flag.activity_pubs << (activity_pubs.where(actor_id: actor_id).to_a.select { |a| a.public_send(may) })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
message = activity_pubs.public_send(method) ? :success : :error
|
||||||
|
|
||||||
|
flash[message] = I18n.t("activity_pubs.action_on_several.#{message}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def activity_pub
|
||||||
|
@activity_pub ||= site.activity_pubs.find(params[:activity_pub_id])
|
||||||
|
end
|
||||||
|
end
|
85
app/controllers/actor_moderations_controller.rb
Normal file
85
app/controllers/actor_moderations_controller.rb
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Gestiona la cola de moderación de actores
|
||||||
|
class ActorModerationsController < ApplicationController
|
||||||
|
include ModerationConcern
|
||||||
|
include ModerationFiltersConcern
|
||||||
|
|
||||||
|
before_action :authenticate_usuarie!
|
||||||
|
|
||||||
|
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path
|
||||||
|
breadcrumb 'sites.index', :sites_path, match: :exact
|
||||||
|
|
||||||
|
ActorModeration.events.each do |actor_event|
|
||||||
|
define_method(actor_event) do
|
||||||
|
authorize actor_moderation
|
||||||
|
|
||||||
|
# Crea una RemoteFlag si se envían los parámetros adecuados
|
||||||
|
if actor_event == :report
|
||||||
|
remote_flag_params(actor_moderation).tap do |p|
|
||||||
|
actor_moderation.remote_flag_id = p[:remote_flag_attributes][:id]
|
||||||
|
actor_moderation.update(p)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
message =
|
||||||
|
if actor_moderation.public_send(:"may_#{actor_event}?") && actor_moderation.public_send(:"#{actor_event}!")
|
||||||
|
:success
|
||||||
|
else
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[message] = I18n.t("actor_moderations.#{actor_event}.#{message}")
|
||||||
|
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ver el perfil remoto
|
||||||
|
def show
|
||||||
|
breadcrumb site.title, site_posts_path(site)
|
||||||
|
breadcrumb I18n.t('moderation_queue.index.title'), site_moderation_queue_path(site)
|
||||||
|
|
||||||
|
@remote_profile = actor_moderation.actor.content
|
||||||
|
@moderation_queue = rubanok_process(site.activity_pubs.where(actor_id: actor_moderation.actor_id),
|
||||||
|
with: ActivityPubProcessor)
|
||||||
|
|
||||||
|
breadcrumb @remote_profile['name'] || actor_moderation.actor.mention || actor_moderation.actor.uri, ''
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_on_several
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
|
||||||
|
actor_moderations = site.actor_moderations.where(id: params[:actor_moderation])
|
||||||
|
|
||||||
|
return if actor_moderations.count.zero?
|
||||||
|
|
||||||
|
authorize actor_moderations
|
||||||
|
|
||||||
|
action = params[:actor_moderation_action].to_sym
|
||||||
|
method = :"#{action}_all!"
|
||||||
|
may = :"may_#{action}?"
|
||||||
|
|
||||||
|
return unless ActorModeration.events.include? action
|
||||||
|
|
||||||
|
ActorModeration.transaction do
|
||||||
|
if action == :report
|
||||||
|
actor_moderations.find_each do |actor_moderation|
|
||||||
|
next unless actor_moderation.public_send(may)
|
||||||
|
|
||||||
|
actor_moderation.update(actor_moderation_params(actor_moderation))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
message = actor_moderations.public_send(method) ? :success : :error
|
||||||
|
|
||||||
|
flash[message] = I18n.t("actor_moderations.action_on_several.#{message}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def actor_moderation
|
||||||
|
@actor_moderation ||= site.actor_moderations.find(params[:actor_moderation_id] || params[:id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
module ActivityPub
|
||||||
|
# Devuelve los reportes remotos hechos
|
||||||
|
#
|
||||||
|
# @todo Verificar la firma. Por ahora no es necesario porque no es
|
||||||
|
# posible obtener remotamente todos los reportes y se identifican por
|
||||||
|
# UUIDv4.
|
||||||
|
class RemoteFlagsController < BaseController
|
||||||
|
skip_forgery_protection
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: (remote_flag&.content || {}), content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @return [ActivityPub::RemoteFlag,nil]
|
||||||
|
def remote_flag
|
||||||
|
@remote_flag ||= ::ActivityPub::RemoteFlag.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,7 +18,7 @@ module Api
|
||||||
|
|
||||||
# Si todo salió bien, enviar los correos y redirigir al sitio.
|
# Si todo salió bien, enviar los correos y redirigir al sitio.
|
||||||
# El sitio nos dice a dónde tenemos que ir.
|
# El sitio nos dice a dónde tenemos que ir.
|
||||||
ContactJob.perform_later site.id,
|
ContactJob.perform_later site,
|
||||||
params[:form],
|
params[:form],
|
||||||
contact_params.to_h.symbolize_keys,
|
contact_params.to_h.symbolize_keys,
|
||||||
params[:redirect]
|
params[:redirect]
|
||||||
|
|
|
@ -10,8 +10,8 @@ module Api
|
||||||
# solo si la API key es verificable. Del otro lado siempre
|
# solo si la API key es verificable. Del otro lado siempre
|
||||||
# respondemos con lo mismo.
|
# respondemos con lo mismo.
|
||||||
def create
|
def create
|
||||||
if site&.airbrake_valid? airbrake_token
|
if (site&.airbrake_valid? airbrake_token) && !detected_device.bot?
|
||||||
BacktraceJob.perform_later site_id: params[:site_id],
|
BacktraceJob.perform_later site: site,
|
||||||
params: airbrake_params.to_h
|
params: airbrake_params.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,7 +23,39 @@ module Api
|
||||||
# XXX: Por alguna razón Airbrake envía los datos con Content-Type:
|
# XXX: Por alguna razón Airbrake envía los datos con Content-Type:
|
||||||
# text/plain.
|
# text/plain.
|
||||||
def airbrake_params
|
def airbrake_params
|
||||||
@airbrake_params ||= params.merge!(FastJsonparser.parse(request.raw_post) || {}).permit!
|
@airbrake_params ||=
|
||||||
|
params.merge!(FastJsonparser.parse(request.raw_post) || {})
|
||||||
|
.permit(
|
||||||
|
{
|
||||||
|
errors: [
|
||||||
|
:type,
|
||||||
|
:message,
|
||||||
|
{ backtrace: %i[file line column function] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: [
|
||||||
|
:url,
|
||||||
|
:language,
|
||||||
|
:severity,
|
||||||
|
:userAgent,
|
||||||
|
:windowError,
|
||||||
|
:rootDirectory,
|
||||||
|
{
|
||||||
|
history: [
|
||||||
|
:date,
|
||||||
|
:type,
|
||||||
|
:severity,
|
||||||
|
:target,
|
||||||
|
:method,
|
||||||
|
:duration,
|
||||||
|
:statusCode,
|
||||||
|
{ arguments: [] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def site
|
def site
|
||||||
|
@ -34,6 +66,11 @@ module Api
|
||||||
def airbrake_token
|
def airbrake_token
|
||||||
@airbrake_token ||= params[:key]
|
@airbrake_token ||= params[:key]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [DeviceDetector]
|
||||||
|
def detected_device
|
||||||
|
@detected_device ||= DeviceDetector.new(request.headers['User-Agent'], request.headers)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Api
|
||||||
|
|
||||||
# Lista de nombres de dominios a emitir certificados
|
# Lista de nombres de dominios a emitir certificados
|
||||||
def index
|
def index
|
||||||
render json: sites_names + alternative_names + api_names + www_names
|
render json: alternative_names + api_names + www_names
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -18,17 +18,16 @@ module Api
|
||||||
name.end_with?('.') ? name[0..-2] : "#{name}.#{Site.domain}"
|
name.end_with?('.') ? name[0..-2] : "#{name}.#{Site.domain}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Nombres de los sitios
|
def subdomain?(name)
|
||||||
def sites_names
|
name.end_with? ".#{Site.domain}"
|
||||||
Site.all.order(:name).pluck(:name).map do |name|
|
|
||||||
canonicalize name
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Dominios alternativos
|
# Dominios alternativos
|
||||||
def alternative_names
|
def alternative_names
|
||||||
(DeployAlternativeDomain.all.map(&:hostname) + DeployLocalizedDomain.all.map(&:hostname)).map do |name|
|
(DeployAlternativeDomain.all.map(&:hostname) + DeployLocalizedDomain.all.map(&:hostname)).map do |name|
|
||||||
canonicalize name
|
canonicalize name
|
||||||
|
end.reject do |name|
|
||||||
|
subdomain? name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -41,6 +40,8 @@ module Api
|
||||||
.or(Site.where(colaboracion_anonima: true))
|
.or(Site.where(colaboracion_anonima: true))
|
||||||
.select("'api.' || name as name").map(&:name).map do |name|
|
.select("'api.' || name as name").map(&:name).map do |name|
|
||||||
canonicalize name
|
canonicalize name
|
||||||
|
end.reject do |name|
|
||||||
|
subdomain? name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
79
app/controllers/api/v1/webhooks/concerns/webhook_concern.rb
Normal file
79
app/controllers/api/v1/webhooks/concerns/webhook_concern.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
module Webhooks
|
||||||
|
module Concerns
|
||||||
|
# Helpers para webhooks
|
||||||
|
module WebhookConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
skip_before_action :verify_authenticity_token
|
||||||
|
|
||||||
|
# Responde con forbidden si falla la validación del token
|
||||||
|
rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer
|
||||||
|
rescue_from ActiveRecord::RecordInvalid, with: :platforms_answer
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Valida el token que envía la plataforma en el webhook
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def token
|
||||||
|
@token ||=
|
||||||
|
begin
|
||||||
|
header = request.headers
|
||||||
|
token = header['X-Social-Inbox'].presence
|
||||||
|
token ||= header['X-Gitlab-Token'].presence
|
||||||
|
token ||= token_from_signature(header['X-Gitea-Signature'].presence)
|
||||||
|
token ||= token_from_signature(header['X-Hub-Signature-256'].presence, 'sha256=')
|
||||||
|
token
|
||||||
|
ensure
|
||||||
|
raise ActiveRecord::RecordNotFound, 'Proveedor no soportado' if token.blank?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Valida token a partir de firma
|
||||||
|
#
|
||||||
|
# @param signature [String,nil]
|
||||||
|
# @param prepend [String]
|
||||||
|
# @return [String, nil]
|
||||||
|
def token_from_signature(signature, prepend = '')
|
||||||
|
return if signature.nil?
|
||||||
|
|
||||||
|
payload = request.raw_post
|
||||||
|
|
||||||
|
site.roles.where(temporal: false, rol: 'usuarie').pluck(:token).find do |token|
|
||||||
|
new_signature = prepend + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), token, payload)
|
||||||
|
|
||||||
|
ActiveSupport::SecurityUtils.secure_compare(new_signature, signature.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra el sitio a partir de la URL
|
||||||
|
#
|
||||||
|
# @return [Site]
|
||||||
|
def site
|
||||||
|
@site ||= Site.find_by_name!(params[:site_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra le usuarie
|
||||||
|
#
|
||||||
|
# @return [Site]
|
||||||
|
def usuarie
|
||||||
|
@usuarie ||= site.roles.find_by!(temporal: false, rol: 'usuarie', token: token).usuarie
|
||||||
|
end
|
||||||
|
|
||||||
|
# Respuesta de error a plataformas
|
||||||
|
def platforms_answer(exception)
|
||||||
|
ExceptionNotifier.notify_exception(exception, data: { headers: request.headers.to_h })
|
||||||
|
|
||||||
|
head :forbidden
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
app/controllers/api/v1/webhooks/pull_controller.rb
Normal file
25
app/controllers/api/v1/webhooks/pull_controller.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
module Webhooks
|
||||||
|
# Recibe webhooks y lanza un PullJob
|
||||||
|
class PullController < BaseController
|
||||||
|
include Api::V1::Webhooks::Concerns::WebhookConcern
|
||||||
|
|
||||||
|
# Trae los cambios a partir de un post de Webhooks:
|
||||||
|
# (Gitlab, Github, Gitea, etc)
|
||||||
|
#
|
||||||
|
# @return [nil]
|
||||||
|
def pull
|
||||||
|
message = I18n.with_locale(site.default_locale) do
|
||||||
|
I18n.t('webhooks.pull.message')
|
||||||
|
end
|
||||||
|
|
||||||
|
GitPullJob.perform_later(site, usuarie, message)
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
59
app/controllers/api/v1/webhooks/social_inbox_controller.rb
Normal file
59
app/controllers/api/v1/webhooks/social_inbox_controller.rb
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
module Webhooks
|
||||||
|
# Recibe webhooks de la Social Inbox
|
||||||
|
#
|
||||||
|
# @see {https://www.w3.org/TR/activitypub/}
|
||||||
|
class SocialInboxController < BaseController
|
||||||
|
include Api::V1::Webhooks::Concerns::WebhookConcern
|
||||||
|
|
||||||
|
# Validar que el token sea correcto
|
||||||
|
before_action :usuarie
|
||||||
|
|
||||||
|
# Cuando una actividad ingresa en la cola de moderación, la
|
||||||
|
# recibimos por acá
|
||||||
|
#
|
||||||
|
# Vamos a recibir Create, Update, Delete, Follow, Undo,
|
||||||
|
# Announce, Like y obtener el objeto dentro de cada una para
|
||||||
|
# guardar un estado asociado al sitio.
|
||||||
|
#
|
||||||
|
# El objeto del estado puede ser un objeto o une actore,
|
||||||
|
# dependiendo de la actividad.
|
||||||
|
def moderationqueued
|
||||||
|
process! :paused
|
||||||
|
|
||||||
|
head :accepted
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cuando la Social Inbox acepta una actividad, la recibimos
|
||||||
|
# igual y la guardamos por si cambiamos de idea.
|
||||||
|
def onapproved
|
||||||
|
process! :approved
|
||||||
|
|
||||||
|
head :accepted
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cuando la Social Inbox rechaza una actividad, la recibimos
|
||||||
|
# igual y la guardamos por si cambiamos de idea.
|
||||||
|
def onrejected
|
||||||
|
process! :rejected
|
||||||
|
|
||||||
|
head :accepted
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Envía la actividad para procesamiento por separado.
|
||||||
|
#
|
||||||
|
# @param initial_state [Symbol]
|
||||||
|
def process!(initial_state)
|
||||||
|
::ActivityPub::ProcessJob
|
||||||
|
.set(wait: ApplicationJob.random_wait)
|
||||||
|
.perform_later(site: site, body: request.raw_post, initial_state: initial_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,7 +3,7 @@
|
||||||
# Forma de ingreso a Sutty
|
# Forma de ingreso a Sutty
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include ExceptionHandler
|
include ExceptionHandler
|
||||||
include Pundit
|
include Pundit::Authorization
|
||||||
|
|
||||||
protect_from_forgery with: :null_session, prepend: true
|
protect_from_forgery with: :null_session, prepend: true
|
||||||
|
|
||||||
|
@ -11,13 +11,10 @@ class ApplicationController < ActionController::Base
|
||||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||||
before_action :notify_unconfirmed_email, unless: :devise_controller?
|
before_action :notify_unconfirmed_email, unless: :devise_controller?
|
||||||
around_action :set_locale
|
around_action :set_locale
|
||||||
|
after_action :store_location!
|
||||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
|
||||||
rescue_from ActionController::RoutingError, with: :page_not_found
|
|
||||||
rescue_from ActionController::ParameterMissing, with: :page_not_found
|
|
||||||
|
|
||||||
before_action do
|
before_action do
|
||||||
Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?('@' + ENV.fetch('SUTTY', 'sutty.nl'))
|
Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?("@#{ENV.fetch('SUTTY', 'sutty.nl')}")
|
||||||
end
|
end
|
||||||
|
|
||||||
# No tenemos índice de sutty, vamos directamente a ver el listado de
|
# No tenemos índice de sutty, vamos directamente a ver el listado de
|
||||||
|
@ -59,7 +56,9 @@ class ApplicationController < ActionController::Base
|
||||||
#
|
#
|
||||||
# @return [String,Symbol]
|
# @return [String,Symbol]
|
||||||
def current_locale
|
def current_locale
|
||||||
session[:locale] = params[:change_locale_to] if params[:change_locale_to].present?
|
locale = params[:change_locale_to]
|
||||||
|
|
||||||
|
session[:locale] = params[:change_locale_to] if locale.present? && I18n.locale_available?(locale)
|
||||||
|
|
||||||
session[:locale] || current_usuarie&.lang || I18n.locale
|
session[:locale] || current_usuarie&.lang || I18n.locale
|
||||||
end
|
end
|
||||||
|
@ -71,11 +70,6 @@ class ApplicationController < ActionController::Base
|
||||||
I18n.with_locale(current_locale, &action)
|
I18n.with_locale(current_locale, &action)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Muestra una página 404
|
|
||||||
def page_not_found
|
|
||||||
render 'application/page_not_found', status: :not_found
|
|
||||||
end
|
|
||||||
|
|
||||||
# Necesario para poder acceder a Blazer. Solo les usuaries de este
|
# Necesario para poder acceder a Blazer. Solo les usuaries de este
|
||||||
# sitio pueden acceder al panel.
|
# sitio pueden acceder al panel.
|
||||||
def require_usuarie
|
def require_usuarie
|
||||||
|
@ -111,6 +105,16 @@ class ApplicationController < ActionController::Base
|
||||||
def after_sign_in_path_for(resource)
|
def after_sign_in_path_for(resource)
|
||||||
session[:locale] = nil
|
session[:locale] = nil
|
||||||
|
|
||||||
sites_path
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
# Guardar la ubicación para que devise redirija a donde íbamos, a
|
||||||
|
# menos que estemos recibiendo información o intentando ingresar.
|
||||||
|
def store_location!
|
||||||
|
return if request.xhr?
|
||||||
|
return unless request.request_method_symbol == :GET
|
||||||
|
return if devise_controller? && !is_a?(Devise::RegistrationsController) && params[:action] != 'edit'
|
||||||
|
|
||||||
|
session[:usuarie_return_to] = request.fullpath
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,7 +22,12 @@ class BuildStatsController < ApplicationController
|
||||||
|
|
||||||
@table = site.deployment_list.map do |deploy|
|
@table = site.deployment_list.map do |deploy|
|
||||||
type = deploy.class.name.underscore
|
type = deploy.class.name.underscore
|
||||||
urls = deploy.respond_to?(:urls) ? deploy.urls : [deploy.url].compact
|
urls = deploy.urls.map do |url|
|
||||||
|
URI.parse(url)
|
||||||
|
rescue URI::Error
|
||||||
|
nil
|
||||||
|
end.compact
|
||||||
|
|
||||||
urls = [nil] if urls.empty?
|
urls = [nil] if urls.empty?
|
||||||
build_stat = deploy.build_stats.where(status: true).last
|
build_stat = deploy.build_stats.where(status: true).last
|
||||||
seconds = build_stat&.seconds || 0
|
seconds = build_stat&.seconds || 0
|
||||||
|
|
|
@ -12,13 +12,31 @@ module ExceptionHandler
|
||||||
rescue_from PageNotFound, with: :page_not_found
|
rescue_from PageNotFound, with: :page_not_found
|
||||||
rescue_from ActionController::RoutingError, with: :page_not_found
|
rescue_from ActionController::RoutingError, with: :page_not_found
|
||||||
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||||
|
rescue_from Pundit::NilPolicyError, with: :page_not_found
|
||||||
|
rescue_from ActionController::RoutingError, with: :page_not_found
|
||||||
|
rescue_from ActionController::ParameterMissing, with: :page_not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
def site_not_found
|
def site_not_found
|
||||||
|
reset_response!
|
||||||
|
|
||||||
|
flash[:error] = I18n.t('errors.site_not_found')
|
||||||
|
|
||||||
redirect_to sites_path
|
redirect_to sites_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def page_not_found
|
def page_not_found
|
||||||
send_file Rails.root.join('public', '404.html')
|
reset_response!
|
||||||
|
|
||||||
|
render 'application/page_not_found', status: :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def reset_response!
|
||||||
|
self.response_body = nil
|
||||||
|
@_response_body = nil
|
||||||
|
|
||||||
|
headers.delete('Location')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
36
app/controllers/concerns/moderation_concern.rb
Normal file
36
app/controllers/concerns/moderation_concern.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModerationConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
private
|
||||||
|
|
||||||
|
def redirect_to_moderation_queue!
|
||||||
|
redirect_back fallback_location: site_moderation_queue_path(**(session[:moderation_queue_filters] || {}))
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def panel_actor_mention
|
||||||
|
@panel_actor_mention ||= ENV.fetch('PANEL_ACTOR_MENTION', '@sutty@sutty.nl')
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_flag_params(model)
|
||||||
|
remote_flag = ActivityPub::RemoteFlag.find_by(actor_id: model.actor_id)
|
||||||
|
|
||||||
|
{ remote_flag_attributes: { id: remote_flag&.id, message: ''.dup } }.tap do |p|
|
||||||
|
p[:remote_flag_attributes][:site_id] = model.site_id
|
||||||
|
p[:remote_flag_attributes][:actor_id] = model.actor_id
|
||||||
|
|
||||||
|
I18n.available_locales.each do |locale|
|
||||||
|
p[:remote_flag_attributes][:message].tap do |m|
|
||||||
|
m << I18n.t(locale)
|
||||||
|
m << ': '
|
||||||
|
m << I18n.t('remote_flags.report_message', locale: locale, panel_actor_mention: panel_actor_mention)
|
||||||
|
m << '\n\n'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
app/controllers/concerns/moderation_filters_concern.rb
Normal file
15
app/controllers/concerns/moderation_filters_concern.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModerationFiltersConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
before_action :store_filters_in_session!, only: %i[index show]
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def store_filters_in_session!
|
||||||
|
session[:moderation_queue_filters] = params.permit(:instance_state, :actor_state, :activity_pub_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,7 +4,7 @@ class EnvController < ActionController::Base
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@site = Site.find_by_name('panel')
|
@site = Site.find_by_name('panel') || Site.first
|
||||||
|
|
||||||
stale? @site if @site
|
stale? @site if @site
|
||||||
end
|
end
|
||||||
|
|
49
app/controllers/fediblock_states_controller.rb
Normal file
49
app/controllers/fediblock_states_controller.rb
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Estado de las listas de bloqueo en cada sitio
|
||||||
|
class FediblockStatesController < ApplicationController
|
||||||
|
# Realiza cambios en las listas de bloqueo
|
||||||
|
def action_on_several
|
||||||
|
# Encontrar todas y deshabilitar las que no se enviaron
|
||||||
|
site.fediblock_states.all.find_each do |fediblock_state|
|
||||||
|
if fediblock_states_ids.include? fediblock_state.id
|
||||||
|
fediblock_state.enable! if fediblock_state.may_enable?
|
||||||
|
elsif fediblock_state.may_disable?
|
||||||
|
fediblock_state.disable!
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:success] = I18n.t('fediblock_states.action_on_several.success')
|
||||||
|
rescue Exception => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||||
|
|
||||||
|
flash.delete(:success)
|
||||||
|
flash[:error] = I18n.t('fediblock_states.action_on_several.error')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bloquear otras instancias
|
||||||
|
if custom_blocklist.present?
|
||||||
|
if ActivityPub::InstanceModerationJob.perform_now(site: site, hostnames: custom_blocklist)
|
||||||
|
flash[:success] = I18n.t('fediblock_states.action_on_several.custom_blocklist_success')
|
||||||
|
else
|
||||||
|
flash[:error] = I18n.t('fediblock_states.action_on_several.custom_blocklist_error')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to site_moderation_queue_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fediblock_states_ids
|
||||||
|
params[:fediblock_states_ids] || []
|
||||||
|
end
|
||||||
|
|
||||||
|
# La lista de hostnames
|
||||||
|
def custom_blocklist
|
||||||
|
@custom_blocklist ||= fediblocks_states_params[:custom_blocklist].split("\n").map(&:strip).select(&:present?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fediblocks_states_params
|
||||||
|
@fediblocks_states_params ||= params.permit(:custom_blocklist, fediblock_states_ids: [])
|
||||||
|
end
|
||||||
|
end
|
51
app/controllers/instance_moderations_controller.rb
Normal file
51
app/controllers/instance_moderations_controller.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Actualiza la relación entre un sitio y una instancia
|
||||||
|
class InstanceModerationsController < ApplicationController
|
||||||
|
include ModerationConcern
|
||||||
|
|
||||||
|
InstanceModeration.events.each do |event|
|
||||||
|
define_method(event) do
|
||||||
|
authorize instance_moderation
|
||||||
|
|
||||||
|
message =
|
||||||
|
if instance_moderation.public_send(:"may_#{event}?") && instance_moderation.public_send(:"#{event}!")
|
||||||
|
:success
|
||||||
|
else
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[message] = I18n.t("instance_moderations.#{event}.#{message}")
|
||||||
|
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_on_several
|
||||||
|
redirect_to_moderation_queue!
|
||||||
|
|
||||||
|
instance_moderations = site.instance_moderations.where(id: params[:instance_moderation])
|
||||||
|
|
||||||
|
return if instance_moderations.count.zero?
|
||||||
|
|
||||||
|
authorize instance_moderations
|
||||||
|
|
||||||
|
action = params[:instance_moderation_action].to_sym
|
||||||
|
method = :"#{action}_all!"
|
||||||
|
|
||||||
|
return unless InstanceModeration.events.include? action
|
||||||
|
|
||||||
|
InstanceModeration.transaction do
|
||||||
|
message = instance_moderations.public_send(method) ? :success : :error
|
||||||
|
|
||||||
|
flash[:message] = I18n.t("instance_moderations.action_on_several.#{message}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @return [InstanceModeration]
|
||||||
|
def instance_moderation
|
||||||
|
@instance_moderation ||= site.instance_moderations.find(params[:instance_moderation_id])
|
||||||
|
end
|
||||||
|
end
|
26
app/controllers/moderation_queue_controller.rb
Normal file
26
app/controllers/moderation_queue_controller.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Cola de moderación de ActivityPub
|
||||||
|
class ModerationQueueController < ApplicationController
|
||||||
|
include ModerationFiltersConcern
|
||||||
|
|
||||||
|
before_action :authenticate_usuarie!
|
||||||
|
|
||||||
|
breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path
|
||||||
|
breadcrumb 'sites.index', :sites_path, match: :exact
|
||||||
|
|
||||||
|
# Cola de moderación viendo todo el sitio
|
||||||
|
def index
|
||||||
|
authorize ModerationQueue.new(site)
|
||||||
|
breadcrumb site.title, site_posts_path(site)
|
||||||
|
breadcrumb I18n.t('moderation_queue.index.title'), ''
|
||||||
|
|
||||||
|
site.moderation_checked!
|
||||||
|
|
||||||
|
# @todo cambiar el estado por query
|
||||||
|
@activity_pubs = site.activity_pubs
|
||||||
|
@instance_moderations = rubanok_process(site.instance_moderations, with: InstanceModerationProcessor)
|
||||||
|
@actor_moderations = rubanok_process(site.actor_moderations, with: ActorModerationProcessor)
|
||||||
|
@moderation_queue = rubanok_process(site.activity_pubs, with: ActivityPubProcessor)
|
||||||
|
end
|
||||||
|
end
|
|
@ -24,6 +24,7 @@ class PostsController < ApplicationController
|
||||||
|
|
||||||
# Todos los artículos de este sitio para el idioma actual
|
# Todos los artículos de este sitio para el idioma actual
|
||||||
@posts = site.indexed_posts.where(locale: locale)
|
@posts = site.indexed_posts.where(locale: locale)
|
||||||
|
@posts = @posts.page(filter_params.delete(:page)) if site.pagination
|
||||||
# De este tipo
|
# De este tipo
|
||||||
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
|
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
|
||||||
# Que estén dentro de la categoría
|
# Que estén dentro de la categoría
|
||||||
|
@ -154,7 +155,7 @@ class PostsController < ApplicationController
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def filter_params
|
def filter_params
|
||||||
@filter_params ||= params.permit(:q, :category, :layout).to_hash.select do |_, v|
|
@filter_params ||= params.permit(:q, :category, :layout, :page).to_hash.select do |_, v|
|
||||||
v.present?
|
v.present?
|
||||||
end.transform_keys(&:to_sym)
|
end.transform_keys(&:to_sym)
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,19 @@ class SitesController < ApplicationController
|
||||||
fresh_when @sites
|
fresh_when @sites
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Genera la caja del estado para HTMX
|
||||||
|
def status
|
||||||
|
authorize site
|
||||||
|
|
||||||
|
render('sites/status', layout: false) if stale? site
|
||||||
|
end
|
||||||
|
|
||||||
|
def button
|
||||||
|
authorize site
|
||||||
|
|
||||||
|
render('sites/build', layout: false)
|
||||||
|
end
|
||||||
|
|
||||||
# No tenemos propiedades de un sitio aún, así que vamos al listado de
|
# No tenemos propiedades de un sitio aún, así que vamos al listado de
|
||||||
# artículos
|
# artículos
|
||||||
def show
|
def show
|
||||||
|
@ -57,6 +70,7 @@ class SitesController < ApplicationController
|
||||||
usuarie: current_usuarie)
|
usuarie: current_usuarie)
|
||||||
|
|
||||||
if service.update.valid?
|
if service.update.valid?
|
||||||
|
flash[:notice] = I18n.t('sites.update.post')
|
||||||
redirect_to site_posts_path(site, locale: site.default_locale)
|
redirect_to site_posts_path(site, locale: site.default_locale)
|
||||||
else
|
else
|
||||||
render 'edit'
|
render 'edit'
|
||||||
|
@ -109,27 +123,6 @@ class SitesController < ApplicationController
|
||||||
redirect_to sites_path
|
redirect_to sites_path
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtiene y streamea archivos estáticos desde el repositorio mismo,
|
|
||||||
# pero sólo los públicos (es decir los archivos subidos desde Sutty).
|
|
||||||
def static_file
|
|
||||||
authorize site
|
|
||||||
|
|
||||||
file = params.require(:file) + '.' + params.require(:format)
|
|
||||||
|
|
||||||
raise ActionController::RoutingError.new(nil, nil) unless file.start_with? 'public/'
|
|
||||||
|
|
||||||
path = site.relative_path file
|
|
||||||
|
|
||||||
raise ActionController::RoutingError.new(nil, nil) unless File.exist? path
|
|
||||||
|
|
||||||
# TODO: Hacer esto usa recursos, pero menos que generar el sitio
|
|
||||||
# cada vez. Para poder usar X-Accel tendríamos que montar los
|
|
||||||
# repositorios en el servidor web, cosa que no queremos, o hacer
|
|
||||||
# links simbólicos desde todos los public, o usar un servidor web
|
|
||||||
# local que soporte sendfile mejor que Rails (nghttpd?)
|
|
||||||
send_file path
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def site
|
def site
|
||||||
|
|
|
@ -59,9 +59,6 @@ class StatsController < ApplicationController
|
||||||
.order('sum(value) desc')
|
.order('sum(value) desc')
|
||||||
.sum(:value)
|
.sum(:value)
|
||||||
.transform_values(&:to_i)
|
.transform_values(&:to_i)
|
||||||
.transform_values do |v|
|
|
||||||
v * nodes
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -73,9 +70,6 @@ class StatsController < ApplicationController
|
||||||
stats = rollup_scope.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series|
|
stats = rollup_scope.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series|
|
||||||
series.each do |serie|
|
series.each do |serie|
|
||||||
serie[:name] = serie.dig(:dimensions, 'host')
|
serie[:name] = serie.dig(:dimensions, 'host')
|
||||||
serie[:data].transform_values! do |value|
|
|
||||||
value * nodes
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -99,9 +93,6 @@ class StatsController < ApplicationController
|
||||||
stats = rollup_scope.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series|
|
stats = rollup_scope.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series|
|
||||||
series.each do |serie|
|
series.each do |serie|
|
||||||
serie[:name] = serie[:dimensions].slice('host', 'uri').values.join.sub('/index.html', '/')
|
serie[:name] = serie[:dimensions].slice('host', 'uri').values.join.sub('/index.html', '/')
|
||||||
serie[:data].transform_values! do |value|
|
|
||||||
value * nodes
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -197,21 +188,6 @@ class StatsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtiene la cantidad de nodos de Sutty, para poder calcular la
|
|
||||||
# cantidad de visitas.
|
|
||||||
#
|
|
||||||
# Como repartimos las visitas por nodo rotando las IPs en el
|
|
||||||
# nameserver y los resolvedores de DNS eligen un nameserver
|
|
||||||
# aleatoriamente, la cantidad de visitas se reparte
|
|
||||||
# equitativamente.
|
|
||||||
#
|
|
||||||
# XXX: Remover cuando podamos centralizar los AccessLog
|
|
||||||
#
|
|
||||||
# @return [Integer]
|
|
||||||
def nodes
|
|
||||||
@nodes ||= ENV.fetch('NODES', 1).to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
def period
|
def period
|
||||||
@period ||= begin
|
@period ||= begin
|
||||||
p = params.permit(:period_start, :period_end)
|
p = params.permit(:period_start, :period_end)
|
||||||
|
|
|
@ -13,7 +13,7 @@ module ApplicationHelper
|
||||||
root = names.shift
|
root = names.shift
|
||||||
|
|
||||||
names.each do |n|
|
names.each do |n|
|
||||||
root += '[' + n.to_s + ']'
|
root += "[#{n}]"
|
||||||
end
|
end
|
||||||
|
|
||||||
[root, name]
|
[root, name]
|
||||||
|
@ -22,7 +22,7 @@ module ApplicationHelper
|
||||||
def plain_field_name_for(*names)
|
def plain_field_name_for(*names)
|
||||||
root, name = field_name_for(*names)
|
root, name = field_name_for(*names)
|
||||||
|
|
||||||
root + '[' + name.to_s + ']'
|
"#{root}[#{name}]"
|
||||||
end
|
end
|
||||||
|
|
||||||
def distance_of_time_in_words_if_more_than_a_minute(seconds)
|
def distance_of_time_in_words_if_more_than_a_minute(seconds)
|
||||||
|
@ -33,10 +33,24 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Devuelve todas las etiquetas HTML que queremos mantener
|
# Sanitizador que elimina todo
|
||||||
def all_html_tags
|
#
|
||||||
%w[h1 h2 h3 h4 h5 h6 p a ul ol li table tr td th tbody thead
|
# @param html [String]
|
||||||
tfoot em strong sup blockquote cite pre section article]
|
# @return [String]
|
||||||
|
def text_plain(html)
|
||||||
|
sanitize(html, tags: [], attributes: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sanitizador con etiquetas y atributos por defecto
|
||||||
|
#
|
||||||
|
# @param html [String]
|
||||||
|
# @param options [Hash]
|
||||||
|
# @return [String]
|
||||||
|
def sanitize(html, options = {})
|
||||||
|
options[:tags] ||= Sutty::ALLOWED_TAGS
|
||||||
|
options[:attributes] ||= Sutty::ALLOWED_ATTRIBUTES
|
||||||
|
|
||||||
|
super(html, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Genera HTML y limpia etiquetas innecesarias
|
# Genera HTML y limpia etiquetas innecesarias
|
||||||
|
|
15
app/helpers/moderation_queue_helper.rb
Normal file
15
app/helpers/moderation_queue_helper.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ModerationQueueHelper
|
||||||
|
def filter_states(**args)
|
||||||
|
params.permit(:instance_state, :actor_state, :activity_pub_state).merge(**args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def active?(states, state_name, state)
|
||||||
|
if params[state_name].present?
|
||||||
|
params[state_name] == state.to_s
|
||||||
|
else
|
||||||
|
states.first == state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
17
app/javascript/controllers/details_controller.js
Normal file
17
app/javascript/controllers/details_controller.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const state = window.sessionStorage.getItem(this.element.id);
|
||||||
|
|
||||||
|
if (state === "open") {
|
||||||
|
this.element.setAttribute("open", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store(event = undefined) {
|
||||||
|
window.sessionStorage.setItem(this.element.id, event.newState);
|
||||||
|
}
|
||||||
|
}
|
106
app/javascript/controllers/dropdown_controller.js
Normal file
106
app/javascript/controllers/dropdown_controller.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
|
// https://getbootstrap.com/docs/4.6/components/dropdowns/#single-button
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["dropdown", "button", "item"];
|
||||||
|
|
||||||
|
// Al iniciar el controlador
|
||||||
|
connect() {
|
||||||
|
// Llevar la cuenta del item con foco
|
||||||
|
this.data.set("item", -1);
|
||||||
|
|
||||||
|
// Gestionar las teclas
|
||||||
|
this.keydownEvent = this.keydown.bind(this);
|
||||||
|
this.element.addEventListener("keydown", this.keydownEvent);
|
||||||
|
|
||||||
|
// Gestionar el foco
|
||||||
|
this.focusinEvent = this.focusin.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Al eliminar el controlador (al pasar a otra página)
|
||||||
|
disconnect() {
|
||||||
|
// Eliminar la gestión de teclas
|
||||||
|
this.element.removeEventListener("keydown", this.keydownEvent);
|
||||||
|
// Eliminar la gestión del foco
|
||||||
|
document.removeEventListener("focusin", this.focusinEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar u ocultar
|
||||||
|
toggle(event) {
|
||||||
|
(this.buttonTarget.ariaExpanded === "false") ? this.show() : this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar
|
||||||
|
show() {
|
||||||
|
this.buttonTarget.ariaExpanded = "true";
|
||||||
|
this.element.classList.add("show");
|
||||||
|
this.dropdownTarget.classList.add("show");
|
||||||
|
|
||||||
|
// Activar la gestión del foco
|
||||||
|
document.addEventListener("focusin", this.focusinEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ocultar
|
||||||
|
hide() {
|
||||||
|
this.buttonTarget.ariaExpanded = "false";
|
||||||
|
this.element.classList.remove("show");
|
||||||
|
this.dropdownTarget.classList.remove("show");
|
||||||
|
// Volver al inicio el foco de items
|
||||||
|
this.data.set("item", -1);
|
||||||
|
|
||||||
|
// Desactivar la gestión del foco
|
||||||
|
document.removeEventListener("focusin", this.focusinEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestionar el foco
|
||||||
|
focusin(event) {
|
||||||
|
const item = this.itemTargets.find(x => x === event.target);
|
||||||
|
|
||||||
|
// Si el foco se coloca sobre elementos del controlador, no hacer
|
||||||
|
// nada
|
||||||
|
if (event.target === this.buttonTarget || item) {
|
||||||
|
// Si es un item, el comportamiento de las flechas verticales y el
|
||||||
|
// Tab tiene que ser igual
|
||||||
|
if (item) this.data.set("item", this.itemTargets.indexOf(item));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// De lo contrario, ocultar
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestionar las teclas
|
||||||
|
keydown(event) {
|
||||||
|
const initial = parseInt(this.data.get("item"));
|
||||||
|
let item = initial;
|
||||||
|
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 27:
|
||||||
|
// Esc cierra el menú y devuelve el foco
|
||||||
|
this.hide();
|
||||||
|
this.buttonTarget.focus();
|
||||||
|
break;
|
||||||
|
case 38:
|
||||||
|
// Moverse hacia arriba con tope en el primer item
|
||||||
|
if (item > -1) item--;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 40:
|
||||||
|
// Moverse hacia abajo con tope en el último ítem, si el
|
||||||
|
// dropdown estaba cerrado, abrirlo.
|
||||||
|
if (item === -1) this.show();
|
||||||
|
if (item <= this.itemTargets.length) item++;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si cambió la posición del ítem, darle foco y actualizar el
|
||||||
|
// contador.
|
||||||
|
if (initial !== item) {
|
||||||
|
this.itemTargets[item]?.focus();
|
||||||
|
|
||||||
|
this.data.set("item", item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
app/javascript/controllers/file_preview_controller.js
Normal file
19
app/javascript/controllers/file_preview_controller.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Controller } from 'stimulus'
|
||||||
|
import bsCustomFileInput from "bs-custom-file-input";
|
||||||
|
|
||||||
|
document.addEventListener("turbolinks:load", () => {
|
||||||
|
bsCustomFileInput.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["preview", "input"];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
}
|
||||||
|
|
||||||
|
update(event = undefined) {
|
||||||
|
if (!this.hasPreviewTarget) return;
|
||||||
|
|
||||||
|
this.previewTarget.src = window.URL.createObjectURL(this.inputTarget.files[0])
|
||||||
|
}
|
||||||
|
}
|
11
app/javascript/controllers/select_all_controller.js
Normal file
11
app/javascript/controllers/select_all_controller.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Controller } from "stimulus";
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["toggle", "input"];
|
||||||
|
|
||||||
|
toggle(event = undefined) {
|
||||||
|
this.inputTargets.forEach(input => {
|
||||||
|
input.checked = this.toggleTarget.checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
7
app/javascript/etc/htmx_abort.js
Normal file
7
app/javascript/etc/htmx_abort.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// Cancela las peticiones pendientes de htmx para todos los elementos al
|
||||||
|
// cambiar de página.
|
||||||
|
document.addEventListener("turbolinks:click", () => {
|
||||||
|
for (const hx of document.querySelectorAll("[hx-get]")) {
|
||||||
|
window.htmx.trigger(hx, "htmx:abort");
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,11 +0,0 @@
|
||||||
document.addEventListener('turbolinks:load', () => {
|
|
||||||
document.querySelectorAll('input[type=file]').forEach(file => {
|
|
||||||
if (!file.dataset.preview) return
|
|
||||||
|
|
||||||
file.addEventListener('change', event => {
|
|
||||||
if (file.files.length === 0) return
|
|
||||||
|
|
||||||
document.querySelector('#' + file.dataset.preview).src = window.URL.createObjectURL(file.files[0])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,8 +1,9 @@
|
||||||
import './external_links'
|
import './external_links'
|
||||||
import './image_preview'
|
|
||||||
import './input-date'
|
import './input-date'
|
||||||
import './input-tag'
|
import './input-tag'
|
||||||
import './prosemirror'
|
import './prosemirror'
|
||||||
import './timezone'
|
import './timezone'
|
||||||
import './turbolinks-anchors'
|
import './turbolinks-anchors'
|
||||||
import './validation'
|
import './validation'
|
||||||
|
import './new_editor'
|
||||||
|
import './htmx_abort'
|
||||||
|
|
14
app/javascript/etc/new_editor.js
Normal file
14
app/javascript/etc/new_editor.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import SuttyEditor from "@suttyweb/editor";
|
||||||
|
|
||||||
|
import "@suttyweb/editor/dist/style.css";
|
||||||
|
|
||||||
|
document.addEventListener("turbolinks:load", () => {
|
||||||
|
document.querySelectorAll(".new-editor").forEach((editorContainer) => {
|
||||||
|
new SuttyEditor({
|
||||||
|
target: editorContainer,
|
||||||
|
props: {
|
||||||
|
textareaEl: editorContainer.querySelector("textarea"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,9 +9,16 @@ try {
|
||||||
host: window.env.PANEL_URL
|
host: window.env.PANEL_URL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ignoredErrors = ["htmx:afterRequest", "htmx:sendAbort"];
|
||||||
|
|
||||||
console.originalError = console.error;
|
console.originalError = console.error;
|
||||||
console.error = (...e) => {
|
console.error = (...e) => {
|
||||||
|
const msg = e.join(" ");
|
||||||
|
|
||||||
|
if (!ignoredErrors.some(x => msg.includes(x))) {
|
||||||
window.airbrake.notify(e.join(" "));
|
window.airbrake.notify(e.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
return console.originalError(...e);
|
return console.originalError(...e);
|
||||||
};
|
};
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
@ -33,3 +40,5 @@ import 'chartkick/chart.js'
|
||||||
Rails.start()
|
Rails.start()
|
||||||
Turbolinks.start()
|
Turbolinks.start()
|
||||||
ActiveStorage.start()
|
ActiveStorage.start()
|
||||||
|
|
||||||
|
window.htmx = require('htmx.org/dist/htmx.js')
|
||||||
|
|
25
app/jobs/activity_pub/fediblock_fetch_job.rb
Normal file
25
app/jobs/activity_pub/fediblock_fetch_job.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
# Se encarga de mantener las listas de bloqueo actualizadas. Luego de
|
||||||
|
# actualizar el listado de instancias, bloquea las instancias en cada
|
||||||
|
# sitio que tenga el fediblock habilitado.
|
||||||
|
class FediblockFetchJob < ApplicationJob
|
||||||
|
self.priority = 50
|
||||||
|
|
||||||
|
def perform
|
||||||
|
ActivityPub::Fediblock.find_each do |fediblock|
|
||||||
|
fediblock.process!
|
||||||
|
|
||||||
|
hostnames_added = fediblock.hostnames - fediblock.hostnames_was
|
||||||
|
|
||||||
|
# No hacer nada si no cambió con respecto a la versión anterior
|
||||||
|
next if hostnames_added.empty?
|
||||||
|
|
||||||
|
ActivityPub::FediblockUpdatedJob.perform_later(fediblock: fediblock, hostnames: hostnames_added)
|
||||||
|
rescue ActivityPub::Fediblock::FediblockDownloadError => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { fediblock: fediblock.title })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
app/jobs/activity_pub/fediblock_updated_job.rb
Normal file
29
app/jobs/activity_pub/fediblock_updated_job.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Se encarga de mantener sincronizadas las listas de instancias
|
||||||
|
# de los fediblocks con los sitios que las tengan activadas.
|
||||||
|
#
|
||||||
|
# También va a asociar las listas con todos los sitios que tengan la
|
||||||
|
# Social Inbox habilitada.
|
||||||
|
class ActivityPub
|
||||||
|
class FediblockUpdatedJob < ApplicationJob
|
||||||
|
self.priority = 50
|
||||||
|
|
||||||
|
# @param :fediblock [ActivityPub::Fediblock]
|
||||||
|
# @param :hostnames [Array<String>]
|
||||||
|
def perform(fediblock:, hostnames:)
|
||||||
|
instances = ActivityPub::Instance.where(hostname: hostnames)
|
||||||
|
|
||||||
|
# Todos los sitios con la Social Inbox habilitada
|
||||||
|
Site.where(id: DeploySocialDistributedPress.pluck(:site_id)).find_each do |site|
|
||||||
|
# Crea el estado si no existía
|
||||||
|
fediblock_state = site.fediblock_states.find_or_create_by(fediblock: fediblock)
|
||||||
|
|
||||||
|
# No hace nada con los deshabilitados
|
||||||
|
next unless fediblock_state.enabled?
|
||||||
|
|
||||||
|
ActivityPub::InstanceModerationJob.perform_later(site: site, hostnames: hostnames)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
65
app/jobs/activity_pub/fetch_job.rb
Normal file
65
app/jobs/activity_pub/fetch_job.rb
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Obtiene o actualiza el contenido de un objeto, usando las credenciales
|
||||||
|
# del sitio.
|
||||||
|
#
|
||||||
|
# XXX: Esto usa las credenciales del sitio para volver el objeto
|
||||||
|
# disponible para todo el CMS. Asumimos que el objeto devuelto es el
|
||||||
|
# mismo para todo el mundo y las credenciales solo son para
|
||||||
|
# autenticación.
|
||||||
|
class ActivityPub
|
||||||
|
class FetchJob < ApplicationJob
|
||||||
|
self.priority = 50
|
||||||
|
|
||||||
|
attr_reader :object, :response
|
||||||
|
|
||||||
|
# Notificar errores de JSON con el contenido, tomar los errores de
|
||||||
|
# validación y conexión como errores temporales y notificar todo lo
|
||||||
|
# demás sin reintentar.
|
||||||
|
#
|
||||||
|
# @param error [Exception]
|
||||||
|
# @return [Bool]
|
||||||
|
discard_on(FastJsonparser::ParseError) do |error|
|
||||||
|
ExceptionNotifier.notify_exception(error, data: { site: site.name, object: object.uri, body: response.body })
|
||||||
|
end
|
||||||
|
|
||||||
|
retry_on ActiveRecord::RecordInvalid
|
||||||
|
retry_on SocketError, wait: ApplicationJob.random_wait
|
||||||
|
retry_on SystemCallError, wait: ApplicationJob.random_wait
|
||||||
|
retry_on Net::OpenTimeout, wait: ApplicationJob.random_wait
|
||||||
|
retry_on OpenSSL::OpenSSLError, wait: ApplicationJob.random_wait
|
||||||
|
|
||||||
|
def perform(site:, object_id:)
|
||||||
|
ActivityPub::Object.transaction do
|
||||||
|
@site = site
|
||||||
|
@object = ::ActivityPub::Object.find(object_id)
|
||||||
|
|
||||||
|
return if object.blank?
|
||||||
|
return if object.activity_pubs.where(aasm_state: 'removed').count.positive?
|
||||||
|
|
||||||
|
@response = site.social_inbox.dereferencer.get(uri: object.uri)
|
||||||
|
|
||||||
|
# @todo Fallar cuando la respuesta no funcione?
|
||||||
|
# @todo Eliminar en 410 Gone
|
||||||
|
return unless response.success?
|
||||||
|
# Ignorar si ya la caché fue revalidada y ya teníamos el
|
||||||
|
# contenido
|
||||||
|
return if response.hit? && object.content.present?
|
||||||
|
|
||||||
|
current_type = object.type
|
||||||
|
content = FastJsonparser.parse(response.body)
|
||||||
|
|
||||||
|
# Modificar atómicamente
|
||||||
|
::ActivityPub::Object.lock.find(object_id).update!(content: content,
|
||||||
|
type: ActivityPub::Object.type_from(content).name)
|
||||||
|
|
||||||
|
object = ::ActivityPub::Object.find(object_id)
|
||||||
|
# Actualiza la mención
|
||||||
|
object.actor&.save! if object.actor_type?
|
||||||
|
|
||||||
|
# Arreglar las relaciones con actividades también
|
||||||
|
ActivityPub.where(object_id: object.id).update_all(object_type: object.type, updated_at: Time.now)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
16
app/jobs/activity_pub/inbox_job.rb
Normal file
16
app/jobs/activity_pub/inbox_job.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class InboxJob < ApplicationJob
|
||||||
|
self.priority = 10
|
||||||
|
|
||||||
|
# @param :site [Site]
|
||||||
|
# @param :activity [String]
|
||||||
|
# @param :action [Symbol]
|
||||||
|
def perform(site:, activity:, action:)
|
||||||
|
response = site.social_inbox.inbox.public_send(action, id: activity)
|
||||||
|
|
||||||
|
raise response.body unless response.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
38
app/jobs/activity_pub/instance_fetch_job.rb
Normal file
38
app/jobs/activity_pub/instance_fetch_job.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
# Obtiene o actualiza los datos de una instancia. Usamos un cliente
|
||||||
|
# de ActivityPub porque la instancia podría estar en federación
|
||||||
|
# limitada.
|
||||||
|
class InstanceFetchJob < ApplicationJob
|
||||||
|
self.priority = 100
|
||||||
|
|
||||||
|
def perform(site:, instance:)
|
||||||
|
%w[/api/v2/instance /api/v1/instance].each do |api|
|
||||||
|
uri = SocialInbox.generate_uri(instance.hostname) do |u|
|
||||||
|
u.path = api
|
||||||
|
end
|
||||||
|
|
||||||
|
response = site.social_inbox.dereferencer.get(uri: uri)
|
||||||
|
|
||||||
|
next unless response.success?
|
||||||
|
# @todo Validate schema
|
||||||
|
next unless response.parsed_response.is_a?(DistributedPress::V1::Social::ReferencedObject)
|
||||||
|
|
||||||
|
instance.update(content: response.parsed_response.object)
|
||||||
|
|
||||||
|
break
|
||||||
|
rescue BRS::BaseError,
|
||||||
|
Errno::ECONNREFUSED,
|
||||||
|
HTTParty::Error,
|
||||||
|
JSON::JSONError,
|
||||||
|
Net::OpenTimeout,
|
||||||
|
OpenSSL::OpenSSLError,
|
||||||
|
SocketError,
|
||||||
|
Errno::ENETUNREACH => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { instance: uri })
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
38
app/jobs/activity_pub/instance_moderation_job.rb
Normal file
38
app/jobs/activity_pub/instance_moderation_job.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
# Bloquea varias instancias de una sola vez
|
||||||
|
class InstanceModerationJob < ApplicationJob
|
||||||
|
# @param :site [Site]
|
||||||
|
# @param :hostnames [Array<String>]
|
||||||
|
# @param :perform_remotely [Bool]
|
||||||
|
def perform(site:, hostnames:, perform_remotely: true)
|
||||||
|
# Crear las instancias que no existan todavía
|
||||||
|
hostnames.each do |hostname|
|
||||||
|
ActivityPub::Instance.lock.find_or_create_by(hostname: hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
instances = ActivityPub::Instance.where(hostname: hostnames)
|
||||||
|
|
||||||
|
Site.transaction do
|
||||||
|
# Crea todas las moderaciones de instancia con un estado por
|
||||||
|
# defecto si no existen
|
||||||
|
instances.find_each do |instance|
|
||||||
|
# Esto bloquea cada una individualmente en la Social Inbox,
|
||||||
|
# idealmente son pocas instancias las que aparecen.
|
||||||
|
site.instance_moderations.lock.find_or_create_by(instance: instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
scope = site.instance_moderations.where(instance_id: instances.ids)
|
||||||
|
|
||||||
|
if perform_remotely
|
||||||
|
scope.block_all!
|
||||||
|
else
|
||||||
|
scope.block_all_without_callbacks!
|
||||||
|
end
|
||||||
|
|
||||||
|
ActivityPub::SyncListsJob.perform_later(site: site)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
145
app/jobs/activity_pub/process_job.rb
Normal file
145
app/jobs/activity_pub/process_job.rb
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
# Procesar las actividades a medida que llegan
|
||||||
|
class ProcessJob < ApplicationJob
|
||||||
|
attr_reader :body
|
||||||
|
|
||||||
|
retry_on ActiveRecord::RecordInvalid
|
||||||
|
|
||||||
|
# Procesa la actividad en segundo plano
|
||||||
|
#
|
||||||
|
# @param :body [String]
|
||||||
|
# @param :initial_state [Symbol,String]
|
||||||
|
def perform(site:, body:, initial_state: :paused)
|
||||||
|
@site = site
|
||||||
|
@body = body
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection_pool.with_connection do
|
||||||
|
::ActivityPub.transaction do
|
||||||
|
# Crea todos los registros necesarios y actualiza el estado
|
||||||
|
actor.present?
|
||||||
|
instance.present?
|
||||||
|
object.present?
|
||||||
|
activity_pub.present?
|
||||||
|
activity_pub.update(aasm_state: initial_state)
|
||||||
|
|
||||||
|
activity.update_activity_pub_state!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Si el objeto ya viene incorporado en la actividad o lo tenemos
|
||||||
|
# que traer remotamente.
|
||||||
|
#
|
||||||
|
# @return [Bool]
|
||||||
|
def object_embedded?
|
||||||
|
@object_embedded ||= original_activity[:object].is_a?(Hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra la URI del objeto o falla si no la encuentra.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def object_uri
|
||||||
|
@object_uri ||= ::ActivityPub.uri_from_object(original_activity[:object])
|
||||||
|
ensure
|
||||||
|
raise ActiveRecord::RecordNotFound, 'object id missing' if @object_uri.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Atajo a la instancia
|
||||||
|
#
|
||||||
|
# @return [ActivityPub::Instance]
|
||||||
|
def instance
|
||||||
|
actor.instance
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera un objeto a partir de la actividad. Si el objeto ya
|
||||||
|
# existe, actualiza su contenido. Si el objeto no viene
|
||||||
|
# incorporado, obtenemos el contenido más tarde.
|
||||||
|
#
|
||||||
|
# @return [ActivityPub::Object]
|
||||||
|
def object
|
||||||
|
@object ||= ::ActivityPub::Object.lock.find_or_initialize_by(uri: object_uri).tap do |o|
|
||||||
|
o.lock! if o.persisted?
|
||||||
|
o.content = original_object if object_embedded?
|
||||||
|
|
||||||
|
o.save!
|
||||||
|
|
||||||
|
# XXX: el objeto necesita ser guardado antes de poder
|
||||||
|
# procesarlo. No usamos GlobalID porque el tipo de objeto
|
||||||
|
# cambia y produce un error de deserialización.
|
||||||
|
::ActivityPub::FetchJob.perform_later(site: site, object_id: o.id) unless object_embedded?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera el seguimiento del estado del objeto con respecto al
|
||||||
|
# sitio.
|
||||||
|
#
|
||||||
|
# @return [ActivityPub]
|
||||||
|
def activity_pub
|
||||||
|
@activity_pub ||= site.activity_pubs.lock.find_or_create_by!(site: site, actor: actor, instance: instance,
|
||||||
|
object_id: object.id, object_type: object.type)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crea la actividad y la vincula con el estado
|
||||||
|
#
|
||||||
|
# @return [ActivityPub::Activity]
|
||||||
|
def activity
|
||||||
|
@activity ||=
|
||||||
|
::ActivityPub::Activity
|
||||||
|
.type_from(original_activity)
|
||||||
|
.lock
|
||||||
|
.find_or_initialize_by(uri: original_activity[:id], activity_pub: activity_pub, actor: actor).tap do |a|
|
||||||
|
a.lock! if a.persisted?
|
||||||
|
a.content = original_activity.dup
|
||||||
|
a.content[:object] = object.uri
|
||||||
|
a.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Actor, si no hay instancia, la crea en el momento, junto con
|
||||||
|
# su estado de moderación.
|
||||||
|
#
|
||||||
|
# @return [Actor]
|
||||||
|
def actor
|
||||||
|
@actor ||= ::ActivityPub::Actor.lock.find_or_initialize_by(uri: original_activity[:actor]).tap do |a|
|
||||||
|
a.lock! if a.persisted?
|
||||||
|
|
||||||
|
unless a.instance
|
||||||
|
a.instance = ::ActivityPub::Instance.lock.find_or_create_by(hostname: URI.parse(a.uri).hostname)
|
||||||
|
|
||||||
|
::ActivityPub::InstanceFetchJob.perform_later(site: site, instance: a.instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
site.instance_moderations.lock.find_or_create_by(instance: a.instance)
|
||||||
|
a.save!
|
||||||
|
|
||||||
|
site.actor_moderations.lock.find_or_create_by(actor: a)
|
||||||
|
|
||||||
|
::ActivityPub::FetchJob.perform_later(site: site, object_id: a.object.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Hash,String]
|
||||||
|
def original_object
|
||||||
|
@original_object ||= original_activity[:object].dup.tap do |o|
|
||||||
|
o[:@context] = original_activity[:@context].dup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Descubre la actividad recibida, generando un error si la
|
||||||
|
# actividad no está dirigida a nosotres.
|
||||||
|
#
|
||||||
|
# @todo Validar formato con Dry::Schema
|
||||||
|
# @return [Hash]
|
||||||
|
def original_activity
|
||||||
|
@original_activity ||= FastJsonparser.parse(body).tap do |activity|
|
||||||
|
raise '@context missing' unless activity[:@context].present?
|
||||||
|
raise 'id missing' unless activity[:id].present?
|
||||||
|
raise 'object missing' unless activity[:object].present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
37
app/jobs/activity_pub/remote_flag_job.rb
Normal file
37
app/jobs/activity_pub/remote_flag_job.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Envía un reporte directamente a la instancia remota
|
||||||
|
#
|
||||||
|
# @todo El panel debería ser su propia instancia y firmar sus propios
|
||||||
|
# mensajes.
|
||||||
|
# @todo Como la Social Inbox no soporta enviar actividades
|
||||||
|
# a destinataries que no sean seguidores, enviamos el reporte
|
||||||
|
# directamente a la instancia.
|
||||||
|
# @see {https://github.com/hyphacoop/social.distributed.press/issues/14}
|
||||||
|
class ActivityPub
|
||||||
|
class RemoteFlagJob < ApplicationJob
|
||||||
|
self.priority = 30
|
||||||
|
|
||||||
|
def perform(remote_flag:)
|
||||||
|
return unless remote_flag.may_queue?
|
||||||
|
|
||||||
|
inbox = remote_flag.actor&.content&.[]('inbox')
|
||||||
|
|
||||||
|
raise 'Inbox is missing for actor' if inbox.blank?
|
||||||
|
|
||||||
|
remote_flag.queue!
|
||||||
|
|
||||||
|
uri = URI.parse(inbox)
|
||||||
|
client = remote_flag.main_site.social_inbox.client_for(uri.origin)
|
||||||
|
response = client.post(endpoint: uri.path, body: remote_flag.content)
|
||||||
|
|
||||||
|
raise 'No se pudo enviar el reporte' unless response.success?
|
||||||
|
|
||||||
|
remote_flag.report!
|
||||||
|
rescue Exception => e
|
||||||
|
ExceptionNotifier.notify_exception(e, data: { remote_flag: remote_flag.id, response: response.parsed_response })
|
||||||
|
remote_flag.resend!
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
72
app/jobs/activity_pub/sync_lists_job.rb
Normal file
72
app/jobs/activity_pub/sync_lists_job.rb
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
# Sincroniza las listas de bloqueo y permitidas con el estado actual
|
||||||
|
# de la base de datos.
|
||||||
|
class SyncListsJob < ApplicationJob
|
||||||
|
# Siempre correr al final
|
||||||
|
self.priority = 100
|
||||||
|
|
||||||
|
attr_reader :logs
|
||||||
|
|
||||||
|
# Ejecuta todas las requests y consolida los posibles errores.
|
||||||
|
#
|
||||||
|
# @param site [Site]
|
||||||
|
def run(site:)
|
||||||
|
@logs = {}
|
||||||
|
|
||||||
|
instance_scope = site.instance_moderations.joins(:instance)
|
||||||
|
actor_scope = site.actor_moderations.joins(:actor)
|
||||||
|
|
||||||
|
blocklist = wildcardize(instance_scope.blocked.pluck(:hostname)) + actor_scope.blocked.distinct.pluck(:mention).compact + actor_scope.reported.distinct.pluck(:mention).compact
|
||||||
|
allowlist = wildcardize(instance_scope.allowed.pluck(:hostname)) + actor_scope.allowed.distinct.pluck(:mention).compact
|
||||||
|
pauselist = wildcardize(instance_scope.paused.pluck(:hostname)) + actor_scope.paused.distinct.pluck(:mention).compact
|
||||||
|
|
||||||
|
if blocklist.present?
|
||||||
|
Rails.logger.info "Bloqueando: #{blocklist.join(', ')}"
|
||||||
|
process(:blocked) { site.social_inbox.allowlist.delete(list: blocklist) }
|
||||||
|
process(:blocked) { site.social_inbox.blocklist.post(list: blocklist) }
|
||||||
|
end
|
||||||
|
|
||||||
|
if allowlist.present?
|
||||||
|
Rails.logger.info "Permitiendo: #{allowlist.join(', ')}"
|
||||||
|
process(:allowed) { site.social_inbox.blocklist.delete(list: allowlist) }
|
||||||
|
process(:allowed) { site.social_inbox.allowlist.post(list: allowlist) }
|
||||||
|
end
|
||||||
|
|
||||||
|
if pauselist.present?
|
||||||
|
Rails.logger.info "Pausando: #{pauselist.join(', ')}"
|
||||||
|
process(:paused) { site.social_inbox.blocklist.delete(list: pauselist) }
|
||||||
|
process(:paused) { site.social_inbox.allowlist.delete(list: pauselist) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Si alguna falló, reintentar
|
||||||
|
raise if logs.present?
|
||||||
|
rescue Exception => e
|
||||||
|
ExceptionNotifier.notify_exception(e,
|
||||||
|
data: { site: site.name, logs: logs, blocklist: blocklist,
|
||||||
|
allowlist: allowlist, pauselist: pauselist })
|
||||||
|
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process(stage)
|
||||||
|
response = yield
|
||||||
|
|
||||||
|
return if response.success?
|
||||||
|
|
||||||
|
logs[stage] ||= []
|
||||||
|
logs[stage] << { body: response.body, code: response.code }
|
||||||
|
end
|
||||||
|
|
||||||
|
# @params hostnames [Array<String>]
|
||||||
|
# @return [Array<String>]
|
||||||
|
def wildcardize(hostnames)
|
||||||
|
hostnames.map do |hostname|
|
||||||
|
"@*@#{hostname}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,9 +4,21 @@
|
||||||
class ApplicationJob < ActiveJob::Base
|
class ApplicationJob < ActiveJob::Base
|
||||||
include Que::ActiveJob::JobExtensions
|
include Que::ActiveJob::JobExtensions
|
||||||
|
|
||||||
private
|
# Esperar una cantidad random de segundos primos, para que no se
|
||||||
|
# superpongan tareas
|
||||||
|
#
|
||||||
|
# @return [Array<Integer>]
|
||||||
|
RANDOM_WAIT = [3, 5, 7, 11, 13].freeze
|
||||||
|
|
||||||
def site
|
# @return [ActiveSupport::Duration]
|
||||||
@site ||= Site.find @params[:site_id]
|
def self.random_wait
|
||||||
|
RANDOM_WAIT.sample.seconds
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :site
|
||||||
|
|
||||||
|
# Si falla por cualquier cosa informar y descartar
|
||||||
|
discard_on(Exception) do |job, error|
|
||||||
|
ExceptionNotifier.notify_exception(error, data: { job: job })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,10 +6,10 @@ class BacktraceJob < ApplicationJob
|
||||||
|
|
||||||
EMPTY_SOURCEMAP = { 'mappings' => '' }.freeze
|
EMPTY_SOURCEMAP = { 'mappings' => '' }.freeze
|
||||||
|
|
||||||
attr_reader :params, :site_id
|
attr_reader :params
|
||||||
|
|
||||||
def perform(site_id:, params:)
|
def perform(site:, params:)
|
||||||
@site_id = site_id
|
@site = site
|
||||||
@params = params
|
@params = params
|
||||||
|
|
||||||
unless sources.empty?
|
unless sources.empty?
|
||||||
|
@ -44,10 +44,6 @@ class BacktraceJob < ApplicationJob
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def site
|
|
||||||
@site ||= Site.find_by_id(site_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Obtiene todos los archivos del backtrace solo si los puede descargar
|
# Obtiene todos los archivos del backtrace solo si los puede descargar
|
||||||
# desde fuentes seguras.
|
# desde fuentes seguras.
|
||||||
#
|
#
|
||||||
|
@ -59,9 +55,7 @@ class BacktraceJob < ApplicationJob
|
||||||
x['backtrace']
|
x['backtrace']
|
||||||
end.flatten.map do |x|
|
end.flatten.map do |x|
|
||||||
x['file'].split('@').last
|
x['file'].split('@').last
|
||||||
end.uniq.select do |x|
|
end.uniq.grep(%r{\Ahttps://})
|
||||||
%r{\Ahttps://} =~ x
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Descarga y devuelve los datos de un archivo
|
# Descarga y devuelve los datos de un archivo
|
||||||
|
|
8
app/jobs/cleanup_job.rb
Normal file
8
app/jobs/cleanup_job.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Realiza tareas de limpieza en segundo plano
|
||||||
|
class CleanupJob < ApplicationJob
|
||||||
|
def perform(before = nil)
|
||||||
|
CleanupService.new(before: before).cleanup_everything!
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,10 +5,8 @@ class ContactJob < ApplicationJob
|
||||||
# @param [Integer]
|
# @param [Integer]
|
||||||
# @param [String]
|
# @param [String]
|
||||||
# @param [Hash]
|
# @param [Hash]
|
||||||
def perform(site_id, form_name, form, origin = nil)
|
def perform(site, form_name, form, origin = nil)
|
||||||
# Retrocompabilidad al actualizar a 2.7.1
|
@site = site
|
||||||
# @see ApplicationJob#site
|
|
||||||
@params = { site_id: site_id }
|
|
||||||
|
|
||||||
# Sanitizar los valores
|
# Sanitizar los valores
|
||||||
form.each_key do |key|
|
form.each_key do |key|
|
||||||
|
@ -23,7 +21,7 @@ class ContactJob < ApplicationJob
|
||||||
usuaries.each_slice(10) do |u|
|
usuaries.each_slice(10) do |u|
|
||||||
ContactMailer.with(form_name: form_name,
|
ContactMailer.with(form_name: form_name,
|
||||||
form: form,
|
form: form,
|
||||||
site_id: site_id,
|
site: site,
|
||||||
usuaries_emails: u,
|
usuaries_emails: u,
|
||||||
origin: origin)
|
origin: origin)
|
||||||
.notify_usuaries.deliver_now
|
.notify_usuaries.deliver_now
|
||||||
|
|
|
@ -11,47 +11,45 @@ class DeployJob < ApplicationJob
|
||||||
# Lanzar lo antes posible
|
# Lanzar lo antes posible
|
||||||
self.priority = 10
|
self.priority = 10
|
||||||
|
|
||||||
def handle_error(error)
|
retry_on DeployAlreadyRunningException, wait: 1.minute
|
||||||
case error
|
discard_on DeployTimedOutException
|
||||||
when DeployAlreadyRunningException then retry_in 1.minute
|
|
||||||
when DeployTimedOutException then expire
|
|
||||||
else super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# rubocop:disable Metrics/MethodLength
|
# rubocop:disable Metrics/MethodLength
|
||||||
def perform(site, notify: true, time: Time.now, output: false)
|
def perform(site, notify: true, time: Time.now, output: false)
|
||||||
@output = output
|
@site = site
|
||||||
|
|
||||||
ActiveRecord::Base.connection_pool.with_connection do
|
ActiveRecord::Base.connection_pool.with_connection do
|
||||||
@site = Site.find(site)
|
|
||||||
|
|
||||||
# Si ya hay una tarea corriendo, aplazar esta. Si estuvo
|
# Si ya hay una tarea corriendo, aplazar esta. Si estuvo
|
||||||
# esperando más de 10 minutos, recuperar el estado anterior.
|
# esperando más de 10 minutos, recuperar el estado anterior.
|
||||||
#
|
#
|
||||||
# Como el trabajo actual se aplaza al siguiente, arrastrar la
|
# Como el trabajo actual se aplaza al siguiente, arrastrar la
|
||||||
# hora original para poder ir haciendo timeouts.
|
# hora original para poder ir haciendo timeouts.
|
||||||
if @site.building?
|
if site.building?
|
||||||
notify = false
|
notify = false
|
||||||
|
|
||||||
if 10.minutes.ago >= time
|
raise DeployAlreadyRunningException unless 10.minutes.ago >= time
|
||||||
|
|
||||||
raise DeployTimedOutException,
|
raise DeployTimedOutException,
|
||||||
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
|
"#{site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
|
||||||
else
|
|
||||||
raise DeployAlreadyRunningException
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@deployed = {}
|
@deployed = {}
|
||||||
@site.update status: 'building'
|
site.update status: 'building'
|
||||||
@site.deployment_list.each do |d|
|
site.deployment_list.each do |d|
|
||||||
begin
|
begin
|
||||||
raise DeployException, 'Una dependencia falló' if failed_dependencies? d
|
raise DeployException, 'Una dependencia falló' if failed_dependencies? d
|
||||||
|
|
||||||
status = d.deploy(output: @output)
|
status = d.deploy(output: output)
|
||||||
seconds = d.build_stats.last.try(:seconds) || 0
|
seconds = d.build_stats.last.try(:seconds) || 0
|
||||||
size = d.size
|
size = d.size
|
||||||
urls = d.respond_to?(:urls) ? d.urls : [d.url].compact
|
urls = d.urls.map do |url|
|
||||||
|
URI.parse url
|
||||||
|
rescue URI::Error
|
||||||
|
nil
|
||||||
|
end.compact
|
||||||
|
|
||||||
|
raise DeployException, 'Falló la compilación' if d == site.deployment_list.last && !status
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
status = false
|
status = false
|
||||||
seconds ||= 0
|
seconds ||= 0
|
||||||
|
@ -70,9 +68,9 @@ class DeployJob < ApplicationJob
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless @output
|
return unless output
|
||||||
|
|
||||||
puts (Terminal::Table.new do |t|
|
puts(Terminal::Table.new do |t|
|
||||||
t << (%w[type] + @deployed.values.first.keys)
|
t << (%w[type] + @deployed.values.first.keys)
|
||||||
t.add_separator
|
t.add_separator
|
||||||
@deployed.each do |type, row|
|
@deployed.each do |type, row|
|
||||||
|
@ -80,12 +78,12 @@ class DeployJob < ApplicationJob
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
ensure
|
ensure
|
||||||
if @site.present?
|
if site.present?
|
||||||
@site.update status: 'waiting'
|
site.update status: 'waiting'
|
||||||
|
|
||||||
notify_usuaries if notify
|
notify_usuaries if notify
|
||||||
|
|
||||||
puts "\a" if @output
|
puts "\a" if output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -115,7 +113,7 @@ class DeployJob < ApplicationJob
|
||||||
# @param :deploy [Deploy]
|
# @param :deploy [Deploy]
|
||||||
def notify_exception(exception, deploy = nil)
|
def notify_exception(exception, deploy = nil)
|
||||||
data = {
|
data = {
|
||||||
site: @site.id,
|
site: site.name,
|
||||||
deploy: deploy&.type,
|
deploy: deploy&.type,
|
||||||
log: deploy&.build_stats&.last&.log,
|
log: deploy&.build_stats&.last&.log,
|
||||||
failed_dependencies: (failed_dependencies(deploy) if deploy)
|
failed_dependencies: (failed_dependencies(deploy) if deploy)
|
||||||
|
@ -125,8 +123,10 @@ class DeployJob < ApplicationJob
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_usuaries
|
def notify_usuaries
|
||||||
@site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
|
usuarie_ids = site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id)
|
||||||
DeployMailer.with(usuarie: usuarie, site: @site.id)
|
|
||||||
|
Usuarie.where(id: usuarie_ids).find_each do |usuarie|
|
||||||
|
DeployMailer.with(usuarie: usuarie, site: site)
|
||||||
.deployed(@deployed)
|
.deployed(@deployed)
|
||||||
.deliver_now
|
.deliver_now
|
||||||
end
|
end
|
||||||
|
|
18
app/jobs/git_pull_job.rb
Normal file
18
app/jobs/git_pull_job.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Permite traer los cambios desde webhooks
|
||||||
|
|
||||||
|
class GitPullJob < ApplicationJob
|
||||||
|
# @param :site [Site]
|
||||||
|
# @param :usuarie [Usuarie]
|
||||||
|
# @return [nil]
|
||||||
|
def perform(site, usuarie)
|
||||||
|
@site = site
|
||||||
|
|
||||||
|
return unless site.repository.origin
|
||||||
|
return unless site.repository.fetch.positive?
|
||||||
|
|
||||||
|
site.repository.merge(usuarie)
|
||||||
|
site.reindex_changes!
|
||||||
|
end
|
||||||
|
end
|
13
app/jobs/git_push_job.rb
Normal file
13
app/jobs/git_push_job.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Permite pushear los cambios cada vez que se
|
||||||
|
# hacen commits en un sitio
|
||||||
|
class GitPushJob < ApplicationJob
|
||||||
|
# @param :site [Site]
|
||||||
|
# @return [nil]
|
||||||
|
def perform(site)
|
||||||
|
@site = site
|
||||||
|
|
||||||
|
site.repository.push if site.repository.origin
|
||||||
|
end
|
||||||
|
end
|
|
@ -30,7 +30,7 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
count: 1,
|
count: 1,
|
||||||
issue: @issue['iid'],
|
issue: @issue['iid'],
|
||||||
user_agents: [user_agent].compact,
|
user_agents: [user_agent].compact,
|
||||||
params: [request&.filtered_parameters].compact,
|
params: request&.filtered_parameters&.as_json,
|
||||||
urls: [url].compact
|
urls: [url].compact
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -105,7 +105,7 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
def title
|
def title
|
||||||
@title ||= ''.dup.tap do |t|
|
@title ||= ''.dup.tap do |t|
|
||||||
t << "[#{exception.class}] " unless javascript?
|
t << "[#{exception.class}] " unless javascript?
|
||||||
t << exception.message
|
t << exception.message[0..200]
|
||||||
t << " [#{issue_data[:count]}]"
|
t << " [#{issue_data[:count]}]"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -192,7 +192,7 @@ class GitlabNotifierJob < ApplicationJob
|
||||||
```
|
```
|
||||||
#{request.request_method} #{url}
|
#{request.request_method} #{url}
|
||||||
|
|
||||||
#{pp request.filtered_parameters}
|
#{pp request.filtered_parameters.as_json}
|
||||||
```
|
```
|
||||||
|
|
||||||
REQUEST
|
REQUEST
|
||||||
|
|
|
@ -15,8 +15,7 @@
|
||||||
# Lo mismo para salir de mantenimiento, agregando el atributo
|
# Lo mismo para salir de mantenimiento, agregando el atributo
|
||||||
# are_we_back: true al crear el Maintenance.
|
# are_we_back: true al crear el Maintenance.
|
||||||
class MaintenanceJob < ApplicationJob
|
class MaintenanceJob < ApplicationJob
|
||||||
def perform(maintenance_id:)
|
def perform(maintenance:)
|
||||||
maintenance = Maintenance.find(maintenance_id)
|
|
||||||
# Decidir cuál vamos a enviar según el estado de Maintenance
|
# Decidir cuál vamos a enviar según el estado de Maintenance
|
||||||
mailer = maintenance.are_we_back ? :were_back : :notice
|
mailer = maintenance.are_we_back ? :were_back : :notice
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,6 @@ class PeriodicJob < ApplicationJob
|
||||||
|
|
||||||
STARTING_INTERVAL = Stat::INTERVALS.first
|
STARTING_INTERVAL = Stat::INTERVALS.first
|
||||||
|
|
||||||
# Tener el sitio a mano
|
|
||||||
attr_reader :site
|
|
||||||
|
|
||||||
# Descartar y notificar si pasó algo más.
|
# Descartar y notificar si pasó algo más.
|
||||||
#
|
#
|
||||||
# XXX: En realidad deberíamos seguir reintentando?
|
# XXX: En realidad deberíamos seguir reintentando?
|
||||||
|
|
|
@ -7,8 +7,8 @@ class StatCollectionJob < PeriodicJob
|
||||||
|
|
||||||
STAT_NAME = 'stat_collection_job'
|
STAT_NAME = 'stat_collection_job'
|
||||||
|
|
||||||
def perform(site_id:, once: true)
|
def perform(site:, once: true)
|
||||||
@site = Site.find site_id
|
@site = site
|
||||||
beginning = beginning_of_interval
|
beginning = beginning_of_interval
|
||||||
stat = site.stats.create! name: STAT_NAME
|
stat = site.stats.create! name: STAT_NAME
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class StatCollectionJob < PeriodicJob
|
||||||
rollup.average(:seconds)
|
rollup.average(:seconds)
|
||||||
end
|
end
|
||||||
|
|
||||||
dimensions = { site_id: site_id }
|
dimensions = { site_id: site.id }
|
||||||
|
|
||||||
reduce_rollup(name: 'builds', operation: :sum, dimensions: dimensions)
|
reduce_rollup(name: 'builds', operation: :sum, dimensions: dimensions)
|
||||||
reduce_rollup(name: 'space_used', operation: :average, dimensions: dimensions)
|
reduce_rollup(name: 'space_used', operation: :average, dimensions: dimensions)
|
||||||
|
|
|
@ -16,8 +16,8 @@ class UriCollectionJob < PeriodicJob
|
||||||
IMAGES = %w[.png .jpg .jpeg .gif .webp .jfif].freeze
|
IMAGES = %w[.png .jpg .jpeg .gif .webp .jfif].freeze
|
||||||
STAT_NAME = 'uri_collection_job'
|
STAT_NAME = 'uri_collection_job'
|
||||||
|
|
||||||
def perform(site_id:, once: true)
|
def perform(site:, once: true)
|
||||||
@site = Site.find site_id
|
@site = site
|
||||||
|
|
||||||
# Obtener el principio del intervalo anterior
|
# Obtener el principio del intervalo anterior
|
||||||
beginning_of_interval
|
beginning_of_interval
|
||||||
|
|
|
@ -11,7 +11,7 @@ module ActionDispatch
|
||||||
# Devolver el nombre de archivo con caracteres unicode
|
# Devolver el nombre de archivo con caracteres unicode
|
||||||
# normalizados
|
# normalizados
|
||||||
def original_filename
|
def original_filename
|
||||||
@original_filename.unicode_normalize
|
@original_filename.unicode_normalize.sub(/\A_+/, '')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -96,7 +96,7 @@ module ActiveStorage
|
||||||
end
|
end
|
||||||
|
|
||||||
def blob_for(key)
|
def blob_for(key)
|
||||||
ActiveStorage::Blob.find_by(key: key, service_name: name)
|
ActiveStorage::Blob.find_by!(key: key, service_name: name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
38
app/lib/jekyll/readers/data_reader_decorator.rb
Normal file
38
app/lib/jekyll/readers/data_reader_decorator.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Jekyll
|
||||||
|
module Readers
|
||||||
|
# Permite leer datos utilizando rutas absolutas.
|
||||||
|
#
|
||||||
|
# {Jekyll::DataReader} usa {Dir.chdir} con rutas relativas, lo que
|
||||||
|
# en nuestro uso provoca confusiones en el lector de datos.
|
||||||
|
#
|
||||||
|
# Con este módulo, podemos leer todos los archivos usando rutas
|
||||||
|
# absolutas, lo que nos permite reemplazar jekyll-data, que agregaba
|
||||||
|
# código duplicado.
|
||||||
|
module DataReaderDecorator
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
DATA_EXTENSIONS = %w[.yaml .yml .json .csv .tsv].freeze
|
||||||
|
|
||||||
|
def read_data_to(dir, data)
|
||||||
|
return unless File.directory?(dir) && !@entry_filter.symlink?(dir)
|
||||||
|
|
||||||
|
Dir.glob(File.join(dir, '*')).each do |path|
|
||||||
|
next if @entry_filter.symlink?(path)
|
||||||
|
|
||||||
|
entry = Pathname.new(path).relative_path_from(dir).to_s
|
||||||
|
|
||||||
|
if File.directory?(path)
|
||||||
|
read_data_to(path, data[sanitize_filename(entry)] = {})
|
||||||
|
elsif DATA_EXTENSIONS.include?(File.extname(entry))
|
||||||
|
key = sanitize_filename(File.basename(entry, ".*"))
|
||||||
|
data[key] = read_data_file(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@ class ApplicationMailer < ActionMailer::Base
|
||||||
private
|
private
|
||||||
|
|
||||||
def site
|
def site
|
||||||
@site ||= Site.find @params[:site_id]
|
@site ||= @params[:site]
|
||||||
end
|
end
|
||||||
|
|
||||||
def inline_logo!
|
def inline_logo!
|
||||||
|
|
|
@ -13,8 +13,7 @@ class DeployMailer < ApplicationMailer
|
||||||
|
|
||||||
# rubocop:disable Metrics/AbcSize
|
# rubocop:disable Metrics/AbcSize
|
||||||
def deployed(deploys = {})
|
def deployed(deploys = {})
|
||||||
usuarie = Usuarie.find(params[:usuarie])
|
usuarie = params[:usuarie]
|
||||||
site = usuarie.sites.find(params[:site])
|
|
||||||
hostname = site.hostname
|
hostname = site.hostname
|
||||||
deploys ||= {}
|
deploys ||= {}
|
||||||
|
|
||||||
|
@ -52,7 +51,7 @@ class DeployMailer < ApplicationMailer
|
||||||
t << (row.map do |k, v|
|
t << (row.map do |k, v|
|
||||||
case k
|
case k
|
||||||
when :seconds then v[:human]
|
when :seconds then v[:human]
|
||||||
when :urls then url
|
when :urls then url.to_s
|
||||||
else v
|
else v
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class InvitadxMailer < ApplicationMailer
|
|
||||||
def confirmation_required
|
|
||||||
@invitadx = params[:invitadx]
|
|
||||||
@site = params[:site]
|
|
||||||
mail from: "#{@site.config.dig('title')} <#{ENV.fetch('DEFAULT_FROM', 'sutty@kefir.red')}>", to: @invitadx.email, subject: t('.subject')
|
|
||||||
end
|
|
||||||
end
|
|
152
app/models/activity_pub.rb
Normal file
152
app/models/activity_pub.rb
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = ActivityPub =
|
||||||
|
#
|
||||||
|
# El registro de actividades recibidas y su estado. Cuando recibimos
|
||||||
|
# una actividad, puede estar destinada a varies actores dentro de Sutty,
|
||||||
|
# con lo que generamos una cola para cada une.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# @todo Ya que une actore puede hacer varias actividades sobre el mismo
|
||||||
|
# objeto, lo correcto sería que la actividad a moderar sea una sola en
|
||||||
|
# lugar de una lista acumulativa. Es decir cada ActivityPub representa
|
||||||
|
# el estado del conjunto (Actor, Object, Activity)
|
||||||
|
#
|
||||||
|
# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions}
|
||||||
|
class ActivityPub < ApplicationRecord
|
||||||
|
IGNORED_EVENTS = %i[pause remove].freeze
|
||||||
|
IGNORED_STATES = %i[removed].freeze
|
||||||
|
|
||||||
|
include AASM
|
||||||
|
|
||||||
|
belongs_to :instance
|
||||||
|
belongs_to :site
|
||||||
|
belongs_to :object, polymorphic: true
|
||||||
|
belongs_to :actor
|
||||||
|
belongs_to :remote_flag, optional: true, class_name: 'ActivityPub::RemoteFlag'
|
||||||
|
has_many :activities
|
||||||
|
|
||||||
|
validates :site_id, presence: true
|
||||||
|
validates :object_id, presence: true
|
||||||
|
validates :aasm_state, presence: true, inclusion: { in: %w[paused approved rejected reported removed] }
|
||||||
|
|
||||||
|
accepts_nested_attributes_for :remote_flag
|
||||||
|
|
||||||
|
# Encuentra la URI de un objeto
|
||||||
|
#
|
||||||
|
# @return [String, nil]
|
||||||
|
def self.uri_from_object(object)
|
||||||
|
case object
|
||||||
|
when Array then uri_from_object(object.first)
|
||||||
|
when String then object
|
||||||
|
when Hash then (object['id'] || object[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Obtiene el campo `url` de diversas formas. Si es una String, asumir
|
||||||
|
# que es una URL, si es un Hash, asumir que es un Link, si es un
|
||||||
|
# Array de Strings, obtener la primera, si es de Hash, obtener el
|
||||||
|
# primer link con rel=canonical y mediaType=text/html
|
||||||
|
#
|
||||||
|
# De lo contrario devolver el ID.
|
||||||
|
#
|
||||||
|
# @todo Refactorizar
|
||||||
|
# @param object [Hash]
|
||||||
|
# @return [String]
|
||||||
|
def self.url_from_object(object)
|
||||||
|
raise unless object.respond_to?(:[])
|
||||||
|
|
||||||
|
url =
|
||||||
|
case object['url']
|
||||||
|
when String then object['url']
|
||||||
|
when Hash then object['href']
|
||||||
|
# Esto es un lío porque queremos saber si es un Array<Hash> o
|
||||||
|
# Array<String> o mezcla y obtener el que más nos convenga o
|
||||||
|
# adivinar uno.
|
||||||
|
when Array
|
||||||
|
links = object['url'].map.with_index do |link, _i|
|
||||||
|
case link
|
||||||
|
when Hash then link
|
||||||
|
else { 'href' => link.to_s }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
links.find do |link|
|
||||||
|
link['rel'] == 'canonical' && link['mediaType'] == 'text/html'
|
||||||
|
end&.[]('href') || links.first&.[]('href')
|
||||||
|
end
|
||||||
|
|
||||||
|
url || object['id']
|
||||||
|
end
|
||||||
|
|
||||||
|
aasm do
|
||||||
|
# Todavía no hay una decisión sobre el objeto
|
||||||
|
state :paused, initial: true
|
||||||
|
# Le usuarie aprobó el objeto
|
||||||
|
state :approved
|
||||||
|
# Le usuarie rechazó el objeto
|
||||||
|
state :rejected
|
||||||
|
# Le usuarie reportó el objeto
|
||||||
|
state :reported
|
||||||
|
# Le actore eliminó el objeto
|
||||||
|
state :removed
|
||||||
|
|
||||||
|
# Gestionar todos los errores
|
||||||
|
error_on_all_events do |e|
|
||||||
|
ExceptionNotifier.notify_exception(e,
|
||||||
|
data: { site: site.name, activity_pub: id, activity: activities.first.uri })
|
||||||
|
end
|
||||||
|
|
||||||
|
# Se puede volver a pausa en caso de actualización remota, para
|
||||||
|
# revisar los cambios.
|
||||||
|
event :pause do
|
||||||
|
transitions to: :paused
|
||||||
|
end
|
||||||
|
|
||||||
|
# Recibir una acción de eliminación, eliminar el contenido de la
|
||||||
|
# base de datos. Esto elimina el contenido para todos los sitios
|
||||||
|
# porque estamos respetando lo que pidió le actore.
|
||||||
|
event :remove do
|
||||||
|
transitions to: :removed
|
||||||
|
|
||||||
|
after do
|
||||||
|
next if object.blank?
|
||||||
|
|
||||||
|
object.update(content: {}) unless object.content.empty?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# La actividad se aprueba, informándole a la Social Inbox que está
|
||||||
|
# aprobada. También recibimos la aprobación via
|
||||||
|
# webhook a modo de confirmación.
|
||||||
|
event :approve do
|
||||||
|
transitions from: %i[paused], to: :approved
|
||||||
|
|
||||||
|
after do
|
||||||
|
ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :accept)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# La actividad fue rechazada
|
||||||
|
event :reject do
|
||||||
|
transitions from: %i[paused approved], to: :rejected
|
||||||
|
|
||||||
|
after do
|
||||||
|
ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :reject)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reportarla implica rechazarla
|
||||||
|
event :report do
|
||||||
|
transitions from: %i[paused approved rejected], to: :reported
|
||||||
|
|
||||||
|
after do
|
||||||
|
ActivityPub::InboxJob.perform_later(site: site, activity: activities.first.uri, action: :reject)
|
||||||
|
ActivityPub::RemoteFlagJob.perform_later(remote_flag: remote_flag) if remote_flag.waiting?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Definir eventos en masa
|
||||||
|
include AasmEventsConcern
|
||||||
|
end
|
34
app/models/activity_pub/activity.rb
Normal file
34
app/models/activity_pub/activity.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = Activity =
|
||||||
|
#
|
||||||
|
# Lleva un registro de las actividades que nos piden hacer remotamente.
|
||||||
|
#
|
||||||
|
# Las actividades pueden tener distintos destinataries (sitios/actores).
|
||||||
|
#
|
||||||
|
# @todo Obtener el contenido del objeto dinámicamente si no existe
|
||||||
|
# localmente, por ejemplo cuando la actividad crea un objeto pero lo
|
||||||
|
# envía como referencia en lugar de anidarlo.
|
||||||
|
#
|
||||||
|
# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions}
|
||||||
|
class ActivityPub
|
||||||
|
class Activity < ApplicationRecord
|
||||||
|
include ActivityPub::Concerns::JsonLdConcern
|
||||||
|
|
||||||
|
belongs_to :activity_pub
|
||||||
|
belongs_to :actor, touch: true
|
||||||
|
has_one :object, through: :activity_pub
|
||||||
|
|
||||||
|
validates :activity_pub_id, presence: true
|
||||||
|
# Las actividades son únicas con respecto a su estado
|
||||||
|
validates :uri, presence: true, url: true, uniqueness: { scope: :activity_pub_id, message: 'estado duplicado' }
|
||||||
|
|
||||||
|
# Siempre en orden descendiente para saber el último estado
|
||||||
|
default_scope -> { order(created_at: :desc) }
|
||||||
|
|
||||||
|
# Cambia la máquina de estados según el tipo de actividad
|
||||||
|
def update_activity_pub_state!
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
8
app/models/activity_pub/activity/announce.rb
Normal file
8
app/models/activity_pub/activity/announce.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class Activity
|
||||||
|
# Boost
|
||||||
|
class Announce < ActivityPub::Activity; end
|
||||||
|
end
|
||||||
|
end
|
7
app/models/activity_pub/activity/create.rb
Normal file
7
app/models/activity_pub/activity/create.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class Activity
|
||||||
|
class Create < ActivityPub::Activity; end
|
||||||
|
end
|
||||||
|
end
|
38
app/models/activity_pub/activity/delete.rb
Normal file
38
app/models/activity_pub/activity/delete.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class Activity
|
||||||
|
class Delete < ActivityPub::Activity
|
||||||
|
# Los Delete se refieren a objetos. Al eliminar un objeto,
|
||||||
|
# cancelamos todas las actividades que tienen relacionadas.
|
||||||
|
#
|
||||||
|
# XXX: La actividad tiene una firma, pero la implementación no
|
||||||
|
# está recomendada
|
||||||
|
#
|
||||||
|
# @todo Validar que le Actor corresponda con los objetos. Esto ya
|
||||||
|
# lo haría la Social Inbox por nosotres.
|
||||||
|
# @see {https://docs.joinmastodon.org/spec/security/#ld}
|
||||||
|
def update_activity_pub_state!
|
||||||
|
ActiveRecord::Base.connection_pool.with_connection do
|
||||||
|
ActivityPub.transaction do
|
||||||
|
object = ActivityPub::Object.find_by(uri: ActivityPub.uri_from_object(content['object']))
|
||||||
|
|
||||||
|
if object.present?
|
||||||
|
object.activity_pubs.find_each do |activity_pub|
|
||||||
|
activity_pub.remove! if activity_pub.may_remove?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encontrar todas las acciones de moderación de le actore
|
||||||
|
# eliminade y moverlas a eliminar.
|
||||||
|
if (actor = ActivityPub::Actor.find_by(uri: object.uri)).present?
|
||||||
|
ActorModeration.where(actor_id: actor.id).remove_all!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
activity_pub.remove! if activity_pub.may_remove?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
7
app/models/activity_pub/activity/flag.rb
Normal file
7
app/models/activity_pub/activity/flag.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class Activity
|
||||||
|
class Flag < ActivityPub::Activity; end
|
||||||
|
end
|
||||||
|
end
|
18
app/models/activity_pub/activity/follow.rb
Normal file
18
app/models/activity_pub/activity/follow.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = Follow =
|
||||||
|
#
|
||||||
|
# Una actividad de seguimiento se refiere siempre a une actore (el
|
||||||
|
# sitio) y proviene de otre actore.
|
||||||
|
#
|
||||||
|
# Por ahora las solicitudes de seguimiento se auto-aprueban.
|
||||||
|
class ActivityPub
|
||||||
|
class Activity
|
||||||
|
class Follow < ActivityPub::Activity
|
||||||
|
# Auto-aprobar la solicitud de seguimiento
|
||||||
|
def update_activity_pub_state!
|
||||||
|
activity_pub.approve! if activity_pub.may_approve?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
7
app/models/activity_pub/activity/generic.rb
Normal file
7
app/models/activity_pub/activity/generic.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class Activity
|
||||||
|
class Generic < ActivityPub::Activity; end
|
||||||
|
end
|
||||||
|
end
|
8
app/models/activity_pub/activity/like.rb
Normal file
8
app/models/activity_pub/activity/like.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class Activity
|
||||||
|
# Like
|
||||||
|
class Like < ActivityPub::Activity; end
|
||||||
|
end
|
||||||
|
end
|
29
app/models/activity_pub/activity/undo.rb
Normal file
29
app/models/activity_pub/activity/undo.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = Undo =
|
||||||
|
#
|
||||||
|
# Deshace una actividad, dependiendo de la actividad a la que se
|
||||||
|
# refiere.
|
||||||
|
class ActivityPub
|
||||||
|
class Activity
|
||||||
|
class Undo < ActivityPub::Activity
|
||||||
|
# Una actividad de deshacer tiene anidada como objeto la actividad
|
||||||
|
# a deshacer. Para respetar la voluntad de le actore remote,
|
||||||
|
# tendríamos que eliminar cualquier actividad pendiente sobre el
|
||||||
|
# objeto.
|
||||||
|
#
|
||||||
|
# Sin embargo, estas acciones nunca deberían llegar a nuestra
|
||||||
|
# Inbox.
|
||||||
|
#
|
||||||
|
# @todo Validar que le Actor corresponda con los objetos. Esto ya
|
||||||
|
# lo haría la Social Inbox por nosotres.
|
||||||
|
# @see {https://github.com/hyphacoop/social.distributed.press/issues/43}
|
||||||
|
def update_activity_pub_state!
|
||||||
|
ActivityPub.transaction do
|
||||||
|
ActivityPub::Activity.find_by(uri: content['object'])&.activity_pub&.remove!
|
||||||
|
activity_pub.remove!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
app/models/activity_pub/activity/update.rb
Normal file
13
app/models/activity_pub/activity/update.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class Activity
|
||||||
|
class Update < ActivityPub::Activity
|
||||||
|
# Si estamos actualizando el objeto, tenemos que devolverlo a estado
|
||||||
|
# de moderación
|
||||||
|
def update_activity_pub_state!
|
||||||
|
activity_pub.pause! if activity_pub.approved?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
43
app/models/activity_pub/actor.rb
Normal file
43
app/models/activity_pub/actor.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = Actor =
|
||||||
|
#
|
||||||
|
# Actor es la entidad que realiza acciones en ActivityPub
|
||||||
|
#
|
||||||
|
# @todo Obtener el perfil dinámicamente
|
||||||
|
class ActivityPub
|
||||||
|
class Actor < ApplicationRecord
|
||||||
|
include ActivityPub::Concerns::JsonLdConcern
|
||||||
|
|
||||||
|
belongs_to :instance
|
||||||
|
has_many :actor_moderation
|
||||||
|
has_many :activity_pubs, as: :object
|
||||||
|
has_many :activities
|
||||||
|
has_many :remote_flags
|
||||||
|
|
||||||
|
# Les actores son únicxs a toda la base de datos
|
||||||
|
validates :uri, presence: true, url: true, uniqueness: true
|
||||||
|
|
||||||
|
before_save :mentionize!
|
||||||
|
|
||||||
|
# Obtiene el nombre de la Actor como mención, solo si obtuvimos el
|
||||||
|
# contenido de antemano.
|
||||||
|
#
|
||||||
|
# @return [String, nil]
|
||||||
|
def mentionize!
|
||||||
|
return if mention.present?
|
||||||
|
return if content['preferredUsername'].blank?
|
||||||
|
return if instance.blank?
|
||||||
|
|
||||||
|
self.mention ||= "@#{content['preferredUsername']}@#{instance.hostname}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def object
|
||||||
|
@object ||= ActivityPub::Object.lock.find_or_create_by(uri: uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
def content
|
||||||
|
object.content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
32
app/models/activity_pub/concerns/json_ld_concern.rb
Normal file
32
app/models/activity_pub/concerns/json_ld_concern.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
module Concerns
|
||||||
|
module JsonLdConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Cuando asignamos contenido, obtener la URI si no lo hicimos ya
|
||||||
|
before_save :uri_from_content!, unless: :uri?
|
||||||
|
|
||||||
|
# Obtiene un tipo de actividad a partir del tipo informado
|
||||||
|
#
|
||||||
|
# @param object [Hash]
|
||||||
|
# @return [Activity]
|
||||||
|
def self.type_from(object)
|
||||||
|
raise NameError unless object.is_a?(Hash)
|
||||||
|
|
||||||
|
"#{model_name.name}::#{object[:type].presence || 'Generic'}".constantize
|
||||||
|
rescue NameError
|
||||||
|
model_name.name.constantize::Generic
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def uri_from_content!
|
||||||
|
self.uri = content[:id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
82
app/models/activity_pub/fediblock.rb
Normal file
82
app/models/activity_pub/fediblock.rb
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'httparty'
|
||||||
|
|
||||||
|
# Listas de bloqueo y sus URLs de descarga
|
||||||
|
class ActivityPub
|
||||||
|
class Fediblock < ApplicationRecord
|
||||||
|
class Client
|
||||||
|
include ::HTTParty
|
||||||
|
|
||||||
|
# @param url [String]
|
||||||
|
# @return [HTTParty::Response]
|
||||||
|
def get(url)
|
||||||
|
self.class.get(url, parser: csv_parser)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Procesa el CSV
|
||||||
|
#
|
||||||
|
# @return [Proc]
|
||||||
|
def csv_parser
|
||||||
|
@csv_parser ||=
|
||||||
|
begin
|
||||||
|
require 'csv'
|
||||||
|
|
||||||
|
proc do |body, _|
|
||||||
|
CSV.parse(body, headers: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class FediblockDownloadError < ::StandardError; end
|
||||||
|
|
||||||
|
validates_presence_of :title, :url, :format
|
||||||
|
validates_inclusion_of :format, in: %w[mastodon fediblock none]
|
||||||
|
|
||||||
|
HOSTNAME_HEADERS = {
|
||||||
|
'mastodon' => '#domain',
|
||||||
|
'fediblock' => 'domain'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def client
|
||||||
|
@client ||= Client.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# Todas las instancias de este fediblock
|
||||||
|
def instances
|
||||||
|
ActivityPub::Instance.where(hostname: hostnames)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Descarga la lista y crea las instancias con el estado necesario
|
||||||
|
def process!
|
||||||
|
response = client.get(download_url)
|
||||||
|
|
||||||
|
raise FediblockDownloadError unless response.success?
|
||||||
|
|
||||||
|
Fediblock.transaction do
|
||||||
|
csv = response.parsed_response
|
||||||
|
process_csv! csv
|
||||||
|
|
||||||
|
update(hostnames: csv.map { |r| r[hostname_header] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def hostname_header
|
||||||
|
HOSTNAME_HEADERS[format]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Crea o encuentra instancias que ya existían y las bloquea
|
||||||
|
#
|
||||||
|
# @param csv [CSV::Table]
|
||||||
|
def process_csv!(csv)
|
||||||
|
csv.each do |row|
|
||||||
|
ActivityPub::Instance.find_or_create_by(hostname: row[hostname_header]).tap do |i|
|
||||||
|
i.block! if i.may_block?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
40
app/models/activity_pub/instance.rb
Normal file
40
app/models/activity_pub/instance.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = Instance =
|
||||||
|
#
|
||||||
|
# Representa cada instancia del fediverso que interactúa con la Social
|
||||||
|
# Inbox.
|
||||||
|
class ActivityPub
|
||||||
|
class Instance < ApplicationRecord
|
||||||
|
include AASM
|
||||||
|
|
||||||
|
validates :aasm_state, presence: true, inclusion: { in: %w[paused allowed blocked] }
|
||||||
|
validates :hostname, uniqueness: true, hostname: { allow_numeric_hostname: true }
|
||||||
|
|
||||||
|
has_many :activity_pubs
|
||||||
|
has_many :actors
|
||||||
|
has_many :instance_moderations
|
||||||
|
|
||||||
|
# XXX: Mantenemos esto por si queremos bloquear una instancia a
|
||||||
|
# nivel general
|
||||||
|
aasm do
|
||||||
|
state :paused, initial: true
|
||||||
|
state :allowed
|
||||||
|
state :blocked
|
||||||
|
|
||||||
|
# Al pasar una instancia a bloqueo, quiere decir que todos los
|
||||||
|
# sitios adoptan esta lista
|
||||||
|
event :block do
|
||||||
|
transitions from: %i[paused allowed], to: :blocked
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_name
|
||||||
|
"@*@#{hostname}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def uri
|
||||||
|
@uri ||= "https://#{hostname}/"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
67
app/models/activity_pub/object.rb
Normal file
67
app/models/activity_pub/object.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Almacena objetos de ActivityPub, como Note, Article, etc.
|
||||||
|
class ActivityPub
|
||||||
|
class Object < ApplicationRecord
|
||||||
|
include ActivityPub::Concerns::JsonLdConcern
|
||||||
|
|
||||||
|
before_validation :type_from_content!, unless: :type?
|
||||||
|
|
||||||
|
# Los objetos son únicos a toda la base de datos
|
||||||
|
validates :uri, presence: true, url: true, uniqueness: true
|
||||||
|
validate :uri_is_content_id?, if: :content?
|
||||||
|
|
||||||
|
has_many :activity_pubs, as: :object
|
||||||
|
|
||||||
|
# Encontrar le Actor por su relación con el objeto
|
||||||
|
#
|
||||||
|
# @return [ActivityPub::Actor,nil]
|
||||||
|
def actor
|
||||||
|
ActivityPub::Actor.find_by(uri: actor_uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
def actor_uri
|
||||||
|
content['attributedTo']
|
||||||
|
end
|
||||||
|
|
||||||
|
def actor_type?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def object_type?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Poder explorar propiedades remotas
|
||||||
|
#
|
||||||
|
# @return [DistributedPress::V1::Social::ReferencedObject]
|
||||||
|
def referenced(site)
|
||||||
|
require 'distributed_press/v1/social/referenced_object'
|
||||||
|
|
||||||
|
@referenced ||= DistributedPress::V1::Social::ReferencedObject.new(object: content,
|
||||||
|
dereferencer: site.social_inbox.dereferencer)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def uri_is_content_id?
|
||||||
|
return if uri == content['id']
|
||||||
|
|
||||||
|
errors.add(:activity_pub_objects, 'El ID del objeto no coincide con su URI')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra el tipo a partir del contenido, si existe.
|
||||||
|
#
|
||||||
|
# XXX: Si el objeto es una actividad, esto siempre va a ser
|
||||||
|
# Generic
|
||||||
|
def type_from_content!
|
||||||
|
self.type =
|
||||||
|
begin
|
||||||
|
"ActivityPub::Object::#{content['type'].presence || 'Generic'}".constantize
|
||||||
|
rescue NameError
|
||||||
|
ActivityPub::Object::Generic
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
12
app/models/activity_pub/object/application.rb
Normal file
12
app/models/activity_pub/object/application.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = Application =
|
||||||
|
#
|
||||||
|
# Una aplicación o instancia
|
||||||
|
class ActivityPub
|
||||||
|
class Object
|
||||||
|
class Application < ActivityPub::Object
|
||||||
|
include Concerns::ActorTypeConcern
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
app/models/activity_pub/object/article.rb
Normal file
10
app/models/activity_pub/object/article.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = Article =
|
||||||
|
#
|
||||||
|
# Representa artículos
|
||||||
|
class ActivityPub
|
||||||
|
class Object
|
||||||
|
class Article < ActivityPub::Object; end
|
||||||
|
end
|
||||||
|
end
|
10
app/models/activity_pub/object/audio.rb
Normal file
10
app/models/activity_pub/object/audio.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = Audio =
|
||||||
|
#
|
||||||
|
# Representa artículos
|
||||||
|
class ActivityPub
|
||||||
|
class Object
|
||||||
|
class Audio < ActivityPub::Object; end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub
|
||||||
|
class Object
|
||||||
|
module Concerns
|
||||||
|
module ActorTypeConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# La URI de le Actor en este caso es la misma id
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def actor_uri
|
||||||
|
uri
|
||||||
|
end
|
||||||
|
|
||||||
|
# El objeto referencia a une Actor
|
||||||
|
#
|
||||||
|
# @see {https://www.w3.org/TR/activitystreams-vocabulary/#actor-types}
|
||||||
|
def actor_type?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# El objeto es un objeto
|
||||||
|
#
|
||||||
|
# @see {https://www.w3.org/TR/activitystreams-vocabulary/#object-types}
|
||||||
|
def object_type?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
app/models/activity_pub/object/document.rb
Normal file
10
app/models/activity_pub/object/document.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# = Document =
|
||||||
|
#
|
||||||
|
# Representa artículos
|
||||||
|
class ActivityPub
|
||||||
|
class Object
|
||||||
|
class Document < ActivityPub::Object; end
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue