5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-25 23:06:22 +00:00

Merge branch 'production.panel.sutty.nl' of https://0xacab.org/sutty/sutty into panel.testing.sutty.nl

This commit is contained in:
Sutty 2024-03-06 15:26:26 +00:00
commit 18917e28e2
173 changed files with 3634 additions and 1905 deletions

View file

@ -1,8 +1,8 @@
NODE_OPTIONS=--openssl-legacy-provider
# pwgen -1 32
RAILS_MASTER_KEY=11111111111111111111111111111111
RAILS_GROUPS=assets
DELEGATE=athshe.sutty.nl
HAINISH=../haini.sh/haini.sh
DELEGATE=panel.sutty.nl
DATABASE_URL=postgres://suttier@postgresql.sutty.local/sutty
RAILS_ENV=development
IMAP_SERVER=
@ -37,3 +37,5 @@ AIRBRAKE_API_KEY=
GITLAB_URI=https://0xacab.org
GITLAB_PROJECT=
GITLAB_TOKEN=
PGVER=15
PGPID=/run/postgresql.pid

1
.env.development Normal file
View file

@ -0,0 +1 @@
HAINISH=../haini.sh/haini.sh

6
.gitignore vendored
View file

@ -28,7 +28,7 @@
/data/*
/_storage/*
.env*
.env.*
# Ignore master key for decrypting credentials and more.
/config/master.key
@ -48,3 +48,7 @@ yarn-debug.log*
/yarn-error.log
yarn-debug.log*
.yarn-integrity
/.task
/.yardoc
/public/doc/

View file

@ -1,33 +1,99 @@
image: "gitea.nulo.in/sutty/panel:3.14.10-2.7.8-panel.sutty.nl"
.apk-add: &apk-add
- "apk add go-task diffutils"
.disable-hainish: &disable-hainish
- "rm -f .env.development"
.cache-ruby: &cache-ruby
- paths:
- "vendor/ruby"
- ".bundle"
.cache-node: &cache-node
- paths:
- "node_modules"
.cache-task: &cache-task
- paths:
- ".task"
image: "registry.0xacab.org/sutty/sutty:3.17.3-3.1.4-rails"
variables:
RAILS_ENV: "production"
LC_ALL: "C.UTF-8"
HAINISH: ""
cache:
paths:
- "vendor/ruby"
assets:
stage: "build"
rules:
- if: "$CI_COMMIT_BRANCH == \"panel.sutty.nl\""
- if: "$CI_COMMIT_BRANCH"
changes:
compare_to: "refs/heads/rails"
paths:
- "package.json"
- "app/javascript/**/*"
- "app/assets/**/*"
stage: "deploy"
only:
- "rails"
- "17.3.alpine.panel.sutty.nl"
except:
- "schedules"
cache:
- *cache-ruby
- *cache-node
- *cache-task
before_script:
- "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_USERNAME}:${GITLAB_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"
- "apk add brotli"
- *apk-add
- *disable-hainish
script:
- "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"
- "go-task assets"
after_script:
- "git add public && git commit -m \"ci: assets [skip ci]\""
- "git push -o ci.skip"
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:
- "./bin/modified_files | ./bin/with_extension rb | xargs -r go-task bundle -- exec rubocop"
haml:
stage: "test"
cache:
- *cache-ruby
rules:
- if: "$CI_PIPELINE_SOURCE == 'merge_request_event'"
before_script:
- *apk-add
- *disable-hainish
script:
- "./bin/modified_files | ./bin/with_extension haml | xargs -r go-task bundle -- exec haml-lint"

View file

@ -19,7 +19,6 @@ pipeline:
when:
branch:
- "rails"
- "panel.sutty.nl"
- "17.3.alpine.panel.sutty.nl"
event: "push"
path:
@ -27,55 +26,8 @@ pipeline:
- "Dockerfile"
- ".dockerignore"
- ".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"
- "git add public && git commit -m \"ci: assets [skip ci]\""
- "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:
include:
- ALPINE_VERSION: "3.17.3"
RUBY_VERSION: "3.1"
RUBY_PATCH: "4"
- ALPINE_VERSION: "3.14.10"
RUBY_VERSION: "2.7"
RUBY_PATCH: "8"

View file

@ -1,6 +1,6 @@
ARG RUBY_VERSION=2.7
ARG RUBY_PATCH=8
ARG ALPINE_VERSION=3.14.10
ARG RUBY_VERSION=3.1
ARG RUBY_PATCH=4
ARG ALPINE_VERSION=3.17.3
ARG BASE_IMAGE=registry.nulo.in/sutty/rails
FROM ${BASE_IMAGE}:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}
ARG PANDOC_VERSION=2.18

19
Gemfile
View file

@ -1,15 +1,9 @@
# frozen_string_literal: true
if ENV['RAILS_ENV'] != 'production' && ENV['HAIN_ENV'].nil?
puts 'Usa haini.sh para generar un entorno de trabajo reproducible'
end
source ENV.fetch('GEMS_SOURCE', 'https://17.3.alpine.gems.sutty.nl')
ruby "~> #{ENV.fetch('RUBY_VERSION', '3.1')}"
gem 'dotenv-rails', require: 'dotenv/rails-now'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.1.0'
# Use Puma as the app server
@ -43,8 +37,9 @@ gem 'commonmarker'
gem 'devise'
gem 'devise-i18n'
gem 'devise_invitable'
gem 'distributed-press-api-client', '~> 0.3.0rc0'
gem 'njalla-api-client', '~> 0.2.0'
gem 'redis-client'
gem 'hiredis-client'
gem 'distributed-press-api-client', '~> 0.4.0rc2'
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
gem 'exception_notification'
gem 'fast_blank'
@ -72,6 +67,7 @@ gem 'redis', '~> 4.0', require: %w[redis redis/connection/hiredis]
gem 'redis-rails'
gem 'rollups', git: 'https://github.com/fauno/rollup.git', branch: 'update'
gem 'rubyzip'
gem 'ruby-brs'
gem 'rugged', '1.5.0.1'
gem 'git_clone_url'
gem 'concurrent-ruby-ext'
@ -84,6 +80,9 @@ gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
gem 'kaminari'
gem 'device_detector'
gem 'after_commit_everywhere', '~> 1.0'
gem 'aasm'
# database
gem 'hairtrigger'
gem 'pg'
@ -113,6 +112,7 @@ end
group :development, :test do
gem 'derailed_benchmarks'
gem 'dotenv-rails'
gem 'pry'
gem 'capybara'
gem 'selenium-webdriver'
@ -120,8 +120,9 @@ group :development, :test do
end
group :development do
gem 'reek'
gem 'yard'
gem 'brakeman'
gem 'bundler-audit'
gem 'haml-lint', require: false
gem 'letter_opener'
gem 'listen'

View file

@ -25,81 +25,98 @@ GIT
groupdate (>= 5.2)
GEM
remote: https://gems.sutty.nl/
remote: https://17.3.alpine.gems.sutty.nl/
specs:
actioncable (6.1.7.3)
actionpack (= 6.1.7.3)
activesupport (= 6.1.7.3)
aasm (5.5.0)
concurrent-ruby (~> 1.0)
actioncable (6.1.7.4)
actionpack (= 6.1.7.4)
activesupport (= 6.1.7.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.3)
actionpack (= 6.1.7.3)
activejob (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
actionmailbox (6.1.7.4)
actionpack (= 6.1.7.4)
activejob (= 6.1.7.4)
activerecord (= 6.1.7.4)
activestorage (= 6.1.7.4)
activesupport (= 6.1.7.4)
mail (>= 2.7.1)
actionmailer (6.1.7.3)
actionpack (= 6.1.7.3)
actionview (= 6.1.7.3)
activejob (= 6.1.7.3)
activesupport (= 6.1.7.3)
actionmailer (6.1.7.4)
actionpack (= 6.1.7.4)
actionview (= 6.1.7.4)
activejob (= 6.1.7.4)
activesupport (= 6.1.7.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.7.3)
actionview (= 6.1.7.3)
activesupport (= 6.1.7.3)
actionpack (6.1.7.4)
actionview (= 6.1.7.4)
activesupport (= 6.1.7.4)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.7.3)
actionpack (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
actiontext (6.1.7.4)
actionpack (= 6.1.7.4)
activerecord (= 6.1.7.4)
activestorage (= 6.1.7.4)
activesupport (= 6.1.7.4)
nokogiri (>= 1.8.5)
actionview (6.1.7.3)
activesupport (= 6.1.7.3)
actionview (6.1.7.4)
activesupport (= 6.1.7.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.1.7.3)
activesupport (= 6.1.7.3)
activejob (6.1.7.4)
activesupport (= 6.1.7.4)
globalid (>= 0.3.6)
activemodel (6.1.7.3)
activesupport (= 6.1.7.3)
activerecord (6.1.7.3)
activemodel (= 6.1.7.3)
activesupport (= 6.1.7.3)
activestorage (6.1.7.3)
actionpack (= 6.1.7.3)
activejob (= 6.1.7.3)
activerecord (= 6.1.7.3)
activesupport (= 6.1.7.3)
activemodel (6.1.7.4)
activesupport (= 6.1.7.4)
activerecord (6.1.7.4)
activemodel (= 6.1.7.4)
activesupport (= 6.1.7.4)
activestorage (6.1.7.4)
actionpack (= 6.1.7.4)
activejob (= 6.1.7.4)
activerecord (= 6.1.7.4)
activesupport (= 6.1.7.4)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.7.3)
activesupport (6.1.7.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.4)
addressable (2.8.6)
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)
bcrypt (3.1.19-x86_64-linux-musl)
autoprefixer-rails (10.4.13.0)
execjs (~> 2)
bcrypt (3.1.20-x86_64-linux-musl)
bcrypt_pbkdf (1.1.0-x86_64-linux-musl)
benchmark-ips (2.12.0)
bigdecimal (3.1.1)
bindex (0.8.1-x86_64-linux-musl)
blazer (2.6.5)
activerecord (>= 5)
chartkick (>= 3.2)
railties (>= 5)
safely_block (>= 0.1.1)
brakeman (5.4.1)
bootstrap (4.6.2)
autoprefixer-rails (>= 9.1.0)
popper_js (>= 1.16.1, < 2)
sassc-rails (>= 2.0.0)
brakeman (6.1.1)
racc
builder (3.2.4)
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
capybara (2.18.0)
addressable
mini_mime (>= 0.1.3)
@ -116,6 +133,7 @@ GEM
concurrent-ruby (1.2.2)
concurrent-ruby-ext (1.2.2-x86_64-linux-musl)
concurrent-ruby (= 1.2.2)
connection_pool (2.4.1)
crass (1.0.6)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
@ -123,7 +141,7 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
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)
derailed_benchmarks (2.1.2)
benchmark-ips (~> 2)
@ -137,8 +155,8 @@ GEM
rake (> 10, < 14)
ruby-statistics (>= 2.1)
thor (>= 0.19, < 2)
device_detector (1.1.1)
devise (4.9.2)
device_detector (1.1.2)
devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@ -146,14 +164,15 @@ GEM
warden (~> 1.2.3)
devise-i18n (1.11.0)
devise (>= 4.9.0)
devise_invitable (2.0.8)
devise_invitable (2.0.9)
actionmailer (>= 5.0)
devise (>= 4.6)
distributed-press-api-client (0.3.0rc0)
distributed-press-api-client (0.4.0rc2)
addressable (~> 2.3, >= 2.3.0)
climate_control
dry-schema
httparty (~> 0.18)
httparty-cache (~> 0.0.4)
json (~> 2.1, >= 2.1.0)
jwt (~> 2.6.0)
dotenv (2.8.1)
@ -162,10 +181,10 @@ GEM
railties (>= 3.2)
down (5.4.1)
addressable (~> 2.8)
dry-configurable (1.0.1)
dry-configurable (1.1.0)
dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6)
dry-core (1.0.0)
dry-core (1.0.1)
concurrent-ruby (~> 1.0)
zeitwerk (~> 2.6)
dry-inflector (1.0.0)
@ -174,7 +193,7 @@ GEM
concurrent-ruby (~> 1.0)
dry-core (~> 1.0, < 2)
zeitwerk (~> 2.6)
dry-schema (1.13.1)
dry-schema (1.13.3)
concurrent-ruby (~> 1.0)
dry-configurable (~> 1.0, >= 1.0.1)
dry-core (~> 1.0, < 2)
@ -182,7 +201,8 @@ GEM
dry-logic (>= 1.4, < 2)
dry-types (>= 1.7, < 2)
zeitwerk (~> 2.6)
dry-types (1.7.1)
dry-types (1.7.2)
bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
@ -198,6 +218,7 @@ GEM
exception_notification (4.5.0)
actionmailer (>= 5.2, < 8)
activesupport (>= 5.2, < 8)
execjs (2.8.1)
factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
@ -214,25 +235,25 @@ GEM
ffi (~> 1.0)
git_clone_url (2.0.0)
uri-ssh_git (>= 2.0)
globalid (1.1.0)
activesupport (>= 5.0)
globalid (1.2.1)
activesupport (>= 6.1)
groupdate (6.2.1)
activesupport (>= 5.2)
hairtrigger (1.0.0)
activerecord (>= 6.0, < 8)
ruby2ruby (~> 2.4)
ruby_parser (~> 3.10)
haml (6.1.2-x86_64-linux-musl)
haml (6.3.0)
temple (>= 0.8.2)
thor
tilt
haml-lint (0.999.999)
haml_lint
haml_lint (0.45.0)
haml (>= 4.0, < 6.2)
haml_lint (0.53.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
rubocop (>= 0.50.0)
rubocop (>= 1.0)
sysexits (~> 1.1)
hamlit (3.0.3-x86_64-linux-musl)
temple (>= 0.8.2)
@ -246,10 +267,14 @@ GEM
heapy (0.2.0)
thor
hiredis (0.6.3-x86_64-linux-musl)
hiredis-client (0.14.1-x86_64-linux-musl)
redis-client (= 0.14.1)
http_parser.rb (0.8.0-x86_64-linux-musl)
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httparty-cache (0.0.5)
httparty (~> 0.18)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
icalendar (2.8.0)
@ -281,7 +306,7 @@ GEM
terminal-table (~> 2.0)
jekyll-commonmark (1.4.0)
commonmarker (~> 0.22)
jekyll-images (0.4.1)
jekyll-images (0.4.4)
jekyll (~> 4)
ruby-filemagic (~> 0.7)
ruby-vips (~> 2)
@ -291,7 +316,7 @@ GEM
sassc (> 2.0.1, < 3.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
json (2.6.3-x86_64-linux-musl)
json (2.7.1-x86_64-linux-musl)
jwt (2.6.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
@ -309,7 +334,6 @@ GEM
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
kwalify (0.7.2)
launchy (2.5.2)
addressable (~> 2.8)
letter_opener (1.8.1)
@ -321,12 +345,12 @@ GEM
loaf (0.10.0)
railties (>= 3.2)
lockbox (1.2.0)
lograge (0.12.0)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.21.3)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@ -340,96 +364,97 @@ GEM
method_source (1.0.0)
mini_histogram (0.3.1)
mini_magick (4.12.0)
mini_mime (1.1.2)
mini_portile2 (2.8.2)
minitest (5.18.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.21.1)
mobility (1.2.9)
i18n (>= 0.6.10, < 2)
request_store (~> 1.0)
multi_xml (0.6.0)
net-imap (0.3.4)
net-imap (0.4.9)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.1)
net-protocol (0.2.2)
timeout
net-smtp (0.3.3)
net-smtp (0.4.0)
net-protocol
net-ssh (7.1.0)
net-ssh (7.2.1)
netaddr (2.0.6)
nio4r (2.5.9-x86_64-linux-musl)
njalla-api-client (0.2.0)
dry-schema
httparty (~> 0.18)
nokogiri (1.15.4-x86_64-linux-musl)
nio4r (2.7.0-x86_64-linux-musl)
nokogiri (1.16.0-x86_64-linux-musl)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
orm_adapter (0.5.0)
pairing_heap (3.0.1)
parallel (1.23.0)
parser (3.2.2.1)
parallel (1.24.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
pathutil (0.16.2)
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)
activerecord (>= 5.2)
activesupport (>= 5.2)
popper_js (1.16.1)
prometheus_exporter (2.0.8)
webrick
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (5.0.3)
puma (6.3.1-x86_64-linux-musl)
public_suffix (5.0.4)
puma (6.4.2-x86_64-linux-musl)
nio4r (~> 2.0)
pundit (2.3.0)
pundit (2.3.1)
activesupport (>= 3.0.0)
que (2.2.1)
racc (1.7.1-x86_64-linux-musl)
rack (2.2.7)
racc (1.7.3-x86_64-linux-musl)
rack (2.2.8)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-mini-profiler (3.1.0)
rack (>= 1.2.0)
rack-proxy (0.7.6)
rack-proxy (0.7.7)
rack
rack-test (2.1.0)
rack (>= 1.3)
rails (6.1.7.3)
actioncable (= 6.1.7.3)
actionmailbox (= 6.1.7.3)
actionmailer (= 6.1.7.3)
actionpack (= 6.1.7.3)
actiontext (= 6.1.7.3)
actionview (= 6.1.7.3)
activejob (= 6.1.7.3)
activemodel (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
rails (6.1.7.4)
actioncable (= 6.1.7.4)
actionmailbox (= 6.1.7.4)
actionmailer (= 6.1.7.4)
actionpack (= 6.1.7.4)
actiontext (= 6.1.7.4)
actionview (= 6.1.7.4)
activejob (= 6.1.7.4)
activemodel (= 6.1.7.4)
activerecord (= 6.1.7.4)
activestorage (= 6.1.7.4)
activesupport (= 6.1.7.4)
bundler (>= 1.15.0)
railties (= 6.1.7.3)
railties (= 6.1.7.4)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
rails-i18n (7.0.7)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails-i18n (7.0.8)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
rails_warden (0.6.0)
warden (>= 1.2.0)
railties (6.1.7.3)
actionpack (= 6.1.7.3)
activesupport (= 6.1.7.3)
railties (6.1.7.4)
actionpack (= 6.1.7.4)
activesupport (= 6.1.7.4)
method_source
rake (>= 12.2)
thor (~> 1.0)
rainbow (3.1.1)
rake (13.0.6)
rake (13.1.0)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
@ -441,6 +466,8 @@ GEM
redis-activesupport (5.3.0)
activesupport (>= 3, < 8)
redis-store (>= 1.3, < 2)
redis-client (0.14.1)
connection_pool
redis-rack (2.1.4)
rack (>= 2.0.8, < 3)
redis-store (>= 1.2, < 2)
@ -450,17 +477,13 @@ GEM
redis-store (>= 1.2, < 2)
redis-store (1.9.2)
redis (>= 4, < 6)
reek (6.1.4)
kwalify (~> 0.7.0)
parser (~> 3.2.0)
rainbow (>= 2.0, < 4.0)
regexp_parser (2.8.0)
regexp_parser (2.9.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.0)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.2.5)
rexml (3.2.6)
rgl (0.6.3)
pairing_heap (>= 0.3.0)
rexml (~> 3.2, >= 3.2.4)
@ -476,16 +499,19 @@ GEM
rubocop-ast (>= 1.24.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.28.1)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
rubocop-rails (2.19.1)
rubocop-rails (2.23.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
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-progressbar (1.13.0)
ruby-statistics (3.0.2)
ruby-vips (2.1.4)
ruby-vips (2.2.0)
ffi (~> 1.12)
ruby2ruby (2.5.0)
ruby_parser (~> 3.1)
@ -499,6 +525,12 @@ GEM
errbase (>= 0.1.1)
sassc (2.4.0-x86_64-linux-musl)
ffi (~> 1.9)
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
sprockets-rails
tilt
selenium-webdriver (4.9.1)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
@ -512,14 +544,14 @@ GEM
spring-watcher-listen (2.1.0)
listen (>= 2.7, < 4.0)
spring (>= 4)
sprockets (4.2.0)
sprockets (4.2.1)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
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)
stackprof (0.2.25-x86_64-linux-musl)
stream (0.5.5)
@ -528,21 +560,23 @@ GEM
jekyll (~> 4)
symbol-fstring (1.0.2-x86_64-linux-musl)
sysexits (1.2.0)
temple (0.10.1)
temple (0.10.3)
terminal-table (2.0.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thor (1.2.2)
tilt (2.1.0)
thor (1.3.0)
tilt (2.3.0)
timecop (0.9.6)
timeout (0.3.2)
timeout (0.4.1)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
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)
uri-ssh_git (2.0.0)
validates_hostname (1.0.13)
@ -567,16 +601,21 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.8)
yard (0.9.34)
zeitwerk (2.6.12)
PLATFORMS
x86_64-linux-musl
DEPENDENCIES
aasm
after_commit_everywhere (~> 1.0)
bcrypt (~> 3.1.7)
bcrypt_pbkdf
blazer
bootstrap (~> 4)
brakeman
bundler-audit
capybara
chartkick
commonmarker
@ -587,7 +626,7 @@ DEPENDENCIES
devise
devise-i18n
devise_invitable
distributed-press-api-client (~> 0.3.0rc0)
distributed-press-api-client (~> 0.4.0rc2)
dotenv-rails
down
ed25519
@ -603,6 +642,7 @@ DEPENDENCIES
haml-lint
hamlit-rails
hiredis
hiredis-client
httparty
icalendar
image_processing
@ -610,7 +650,6 @@ DEPENDENCIES
jbuilder (~> 2.5)
jekyll (~> 4.2.0)
jekyll-commonmark (~> 1.4.0)
jekyll-data
jekyll-images
jekyll-include-cache
kaminari
@ -623,7 +662,6 @@ DEPENDENCIES
mini_magick
mobility
net-ssh
njalla-api-client (~> 0.2.0)
nokogiri
pg
pg_search
@ -638,15 +676,17 @@ DEPENDENCIES
rails-i18n
rails_warden
redis (~> 4.0)
redis-client
redis-rails
reek
rgl
rollups!
rubocop-rails
ruby-brs
rubyzip
rugged (= 1.5.0.1)
safe_yaml
safely_block (~> 0.3.0)
sassc-rails
selenium-webdriver
sourcemap
spring
@ -658,10 +698,12 @@ DEPENDENCIES
terminal-table
timecop
turbolinks (~> 5)
uglifier (>= 1.3.0)
validates_hostname
web-console
webpacker
yaml_db!
yard
RUBY VERSION
ruby 3.1.4p223

143
Makefile
View file

@ -1,143 +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/"
assets: public/packs/manifest.json.br ## Compilar los assets
git add public/assets/ public/packs/ && NADA=true git commit -m "assets [skip ci]" ; true
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 := haini.sh 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

View file

@ -1,5 +1,5 @@
cleanup: bundle exec rake cleanup:everything
stats: bundle exec rake stats:process_all
distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que
emergency_cleanup: bundle exec rake cleanup:everything BEFORE=7
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que
stats: bundle exec rake stats:process_all

View file

@ -17,14 +17,32 @@ Para más información visita el [sitio de Sutty](https://sutty.nl/).
### Desarrollar
Todas las tareas se gestionan con `make`, por favor instala GNU Make
antes de comenzar.
Para facilitar la gestión de dependencias y entorno de desarrollo,
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
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
@ -39,10 +57,29 @@ For more information, visit [Sutty's website](https://sutty.nl/en/).
### 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
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/>

185
Taskfile.yaml Normal file
View file

@ -0,0 +1,185 @@
---
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"

View file

@ -54,6 +54,25 @@ $sizes: (
--background: #{$black};
--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
@ -196,7 +215,7 @@ fieldset {
&[type=button] {
@extend .btn;
@extend .btn-info;
@extend .btn-secondary;
@extend .m-0;
}
}
@ -210,10 +229,6 @@ svg {
}
.btn {
background-color: var(--foreground);
color: var(--background);
border: none;
border-radius: 0;
margin-right: 0.3rem;
margin-bottom: 0.3rem;
@ -253,7 +268,7 @@ svg {
color: $magenta;
}
.btn {
.btn-secondary {
background-color: $white;
color: $black;
border: none;

View file

@ -7,3 +7,22 @@ $cyan: #13fefe;
--background: #{$black};
--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;
}
}

View file

@ -19,7 +19,7 @@ module ActiveStorage
# stack.
def blob_args
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys.tap do |ba|
ba[:filename] = ba[:filename].unicode_normalize
ba[:filename] = ba[:filename].unicode_normalize.sub(/\A_+/, '')
end
end
end

View file

@ -16,12 +16,14 @@ module ActiveStorage
end
end
rescue_from ActiveRecord::RecordNotFound, with: :page_not_found
# Asociar el archivo subido al sitio correspondiente. Cada sitio
# tiene su propio servicio de subida de archivos.
def update
if (token = decode_verified_token)
if acceptable_content?(token)
blob = ActiveStorage::Blob.find_by_key token[:key]
blob = ActiveStorage::Blob.find_by_key! token[:key]
site = Site.find_by_name token[:service_name]
if remote_file?(token)
@ -59,6 +61,11 @@ module ActiveStorage
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

View file

@ -23,7 +23,39 @@ module Api
# XXX: Por alguna razón Airbrake envía los datos con Content-Type:
# text/plain.
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
def site

View 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

View 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

View file

@ -0,0 +1,165 @@
# 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
# Cuando una actividad ingresa en la cola de moderación, la
# recibimos por acá
#
# Vamos a recibir Create, Update, Delete, Follow, Undo 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
# Devuelve un error si el token no es válido
usuarie.present?
ActivityPub.transaction do
# Crea todos los registros necesarios y actualiza el estado
actor.present?
instance.present?
object.present?
activity_pub.present?
activity.update_activity_pub_state!
end
rescue ActiveRecord::RecordInvalid => e
ExceptionNotifier.notify_exception(e,
data: { site: site.name, usuarie: usuarie.email,
activity: original_activity })
ensure
head :accepted
end
# Cuando aprobamos una actividad, recibimos la confirmación y
# cambiamos el estado.
def onapproved
ActivityPub.transaction do
activity_pub.approve! if activity_pub.waiting?
end
head :accepted
end
# Cuando rechazamos una actividad, recibimos la confirmación y
# cambiamos el estado
def onrejected
ActivityPub.transaction do
activity_pub.reject! if activity_pub.waiting?
end
head :accepted
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 ||=
case original_activity[:object]
when String then original_activity[:object]
when Hash then original_activity.dig(:object, :id)
end
ensure
raise ActiveRecord::RecordNotFound, 'object id missing' unless @object_uri
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.find_or_initialize_by(uri: object_uri).tap do |o|
# XXX: Si el objeto es una actividad, esto siempre va a ser
# Generic
o.type ||= 'ActivityPub::Object::Generic'
o.content = original_object if object_embedded?
o.save!
# XXX: el objeto necesita ser guardado antes de poder
# procesarlo
ActivityPub::FetchJob.perform_later(site: site, object: o) 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.find_or_create_by!(site: site, object: object)
end
# Crea la actividad y la vincula con el estado
#
# @return [ActivityPub::Activity]
def activity
@activity ||= ActivityPub::Activity.type_from(original_activity).new(uri: original_activity[:id],
activity_pub: activity_pub).tap do |a|
a.content = original_activity.dup
a.content[:object] = object.uri
a.save!
end
end
# Actor, si no hay instancia, la crea en el momento
#
# @return [Actor]
def actor
@actor ||= ActivityPub::Actor.find_or_initialize_by(uri: original_activity[:actor]).tap do |a|
next if a.instance
a.instance = ActivityPub::Instance.find_or_create_by(hostname: URI.parse(a.uri).hostname)
a.save!
end
end
# Descubre la actividad recibida, generando un error si la
# actividad no está dirigida a nosotres.
#
# @todo Validar formato
# @return [Hash]
def original_activity
@original_activity ||= FastJsonparser.parse(request.raw_post).tap do |activity|
raise '@context missing' unless activity[:@context].presence
raise 'id missing' unless activity[:id].presence
raise 'object missing' unless activity[:object].presence
rescue RuntimeError => e
raise ActiveRecord::RecordNotFound, e.message
end
end
# @return [Hash,String]
def original_object
@original_object ||= original_activity[:object].dup
end
end
end
end
end

View file

@ -1,77 +0,0 @@
# frozen_string_literal: true
module Api
module V1
# Recibe webhooks y lanza un PullJob
class WebhooksController < BaseController
# responde con forbidden si falla la validación del token
rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer
# 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
private
# encuentra el sitio a partir de la url
def site
@site ||= Site.find_by_name!(params[:site_id])
end
# valida el token que envía la plataforma del webhook
#
# @return [String]
def token
@token ||=
begin
# Gitlab
if request.headers['X-Gitlab-Token'].present?
request.headers['X-Gitlab-Token']
# Github
elsif request.headers['X-Hub-Signature-256'].present?
token_from_signature(request.headers['X-Hub-Signature-256'], 'sha256=')
# Gitea
elsif request.headers['X-Gitea-Signature'].present?
token_from_signature(request.headers['X-Gitea-Signature'])
else
raise ActiveRecord::RecordNotFound, 'proveedor no soportado'
end
end
end
# valida token a partir de firma de webhook
#
# @return [String, Boolean]
def token_from_signature(signature, prepend = '')
payload = request.body.read
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.tap do |t|
raise ActiveRecord::RecordNotFound, 'token no encontrado' if t.nil?
end
end
# encuentra le usuarie
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

View file

@ -110,27 +110,6 @@ class SitesController < ApplicationController
redirect_to sites_path
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
def site

View file

@ -0,0 +1,28 @@
# 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
def perform(site:, object:)
ActivityPub::Object.transaction do
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?
return unless response.ok?
return if response.miss? && object.content.present?
content = FastJsonparser.parse(response.body)
object.update(content: content, type: ActivityPub::Object.type_from(content).name)
end
end
end
end

View file

@ -56,6 +56,10 @@ class DeployJob < ApplicationJob
rescue URI::Error
nil
end.compact
if d == @site.deployment_list.last && !status
raise DeployException, 'Falló la compilación'
end
rescue StandardError => e
status = false
seconds ||= 0

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true
# Permite traer los cambios desde webhooks
# Permite traer los cambios desde el repositorio remoto
class GitPullJob < ApplicationJob
# @param :site [Site]
# @param :usuarie [Usuarie]
@ -9,9 +8,20 @@ class GitPullJob < ApplicationJob
# @return [nil]
def perform(site, usuarie, message)
return unless site.repository.origin
return unless site.repository.fetch.positive?
site.repository.fetch
return if site.repository.up_to_date?
if site.repository.fast_forward?
site.repository.fast_forward!
else
site.repository.merge(usuarie, message)
end
site.repository.git_lfs_checkout
site.reindex_changes!
nil
end
end

View file

@ -11,7 +11,7 @@ module ActionDispatch
# Devolver el nombre de archivo con caracteres unicode
# normalizados
def original_filename
@original_filename.unicode_normalize
@original_filename.unicode_normalize.sub(/\A_+/, '')
end
end
end

View file

@ -96,7 +96,7 @@ module ActiveStorage
end
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

View file

@ -0,0 +1,62 @@
# 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.
#
# @see {https://www.w3.org/TR/activitypub/#client-to-server-interactions}
class ActivityPub < ApplicationRecord
include AASM
belongs_to :site
belongs_to :object, polymorphic: true
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] }
aasm do
# Todavía no hay una decisión sobre el objeto
state :paused, initial: true
# Estamos esperando respuesta desde la Social Inbox
state :waiting
# 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
# 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
before do
object.update(content: {}) unless object.content.empty?
end
end
# Si un objeto previamente aprobado fue actualizado, volvemos a
# pausarlo.
event :pause do
transitions from: %i[waiting approved rejected], to: :paused
end
# La actividad se aprueba
event :approve do
transitions from: :waiting, to: :approved
end
# La actividad fue rechazada
event :reject do
transitions from: :waiting, to: :rejected
end
end
end

View file

@ -0,0 +1,31 @@
# 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
has_one :object, through: :activity_pub
validates :activity_pub_id, presence: true
# 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

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class ActivityPub
class Activity
class Create < ActivityPub::Activity; end
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class ActivityPub
class Activity
class Delete < ActivityPub::Activity
# Si estamos eliminando el objeto, tenemos que vaciar su contenido y
# cambiar el estado a borrado.
def update_activity_pub_state!
activity_pub.remove!
end
end
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class ActivityPub
class Activity
class Flag < ActivityPub::Activity; end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
# = Follow =
#
# Una actividad de seguimiento se refiere siempre a une actore (el
# sitio) y proviene de otre actore.
class ActivityPub
class Activity
class Follow < ActivityPub::Activity; end
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class ActivityPub
class Activity
class Generic < ActivityPub::Activity; end
end
end

View file

@ -0,0 +1,27 @@
# 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.
#
# @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

View 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

View file

@ -0,0 +1,15 @@
# 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 :activity_pubs, as: :object
end
end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
class ActivityPub
module Concerns
module JsonLdConcern
extend ActiveSupport::Concern
included do
validates :uri, presence: true, uniqueness: true
# 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

View file

@ -0,0 +1,23 @@
# 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: true
has_many :activity_pubs
has_many :actors
aasm do
state :paused, initial: true
state :allowed
state :blocked
end
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Almacena objetos de ActivityPub, como Note, Article, etc.
class ActivityPub
class Object < ApplicationRecord
include ActivityPub::Concerns::JsonLdConcern
has_many :activity_pubs, as: :object
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# = Application =
#
# Una aplicación o instancia
class ActivityPub
class Object
class Application < ActivityPub::Object; end
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# = Article =
#
# Representa artículos
class ActivityPub
class Object
class Article < ActivityPub::Object; end
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# = Generic =
class ActivityPub
class Object
class Generic < ActivityPub::Object; end
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# = Note =
#
# Representa notas, el tipo más común de objeto del Fediverso.
class ActivityPub
class Object
class Note < ActivityPub::Object; end
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# = Organization =
#
# Una organización
class ActivityPub
class Object
class Organization < ActivityPub::Object; end
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
# = Person =
#
# Una persona, el perfil de une actore
class ActivityPub
class Object
class Person < ActivityPub::Object; end
end
end

View file

@ -10,10 +10,12 @@ require 'open3'
# :attributes`.
class Deploy < ApplicationRecord
belongs_to :site
belongs_to :rol
has_many :build_stats, dependent: :destroy
DEPENDENCIES = []
SOFT_DEPENDENCIES = []
DEPENDENCIES = [].freeze
SOFT_DEPENDENCIES = [].freeze
def deploy(**)
raise NotImplementedError
@ -55,20 +57,9 @@ class Deploy < ApplicationRecord
site.path
end
# Un entorno que solo tiene lo que necesitamos
#
# @return [Hash]
def env
# XXX: This doesn't support Windows paths :B
paths = [File.dirname(`which bundle`), '/usr/local/bin', '/usr/bin', '/bin']
# Las variables de entorno extra no pueden superponerse al local.
extra_env.merge({
'HOME' => home_dir,
'PATH' => paths.join(':'),
'JEKYLL_ENV' => Rails.env,
'LANG' => ENV['LANG'],
})
# XXX: Ver DeployLocal#bundle
def gems_dir
@gems_dir ||= Rails.root.join('_storage', 'gems', site.name)
end
# Un entorno que solo tiene lo que necesitamos
@ -83,7 +74,7 @@ class Deploy < ApplicationRecord
'HOME' => home_dir,
'PATH' => paths.join(':'),
'JEKYLL_ENV' => Rails.env,
'LANG' => ENV['LANG'],
'LANG' => ENV.fetch('LANG', nil)
})
end
@ -148,7 +139,7 @@ class Deploy < ApplicationRecord
# provisto con el archivo como parámetro
#
# @param :content [String]
def with_tempfile(content, &block)
def with_tempfile(content)
Tempfile.create(SecureRandom.hex) do |file|
file.write content.to_s
file.rewind

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'distributed_press/v1/client/site'
require 'njalla/v1'
# Soportar Distributed Press APIv1
#
@ -13,10 +12,10 @@ require 'njalla/v1'
# Al ser publicado, envía los archivos en un tarball y actualiza la
# información.
class DeployDistributedPress < Deploy
store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON
store :values, accessors: %i[hostname remote_site_id remote_info distributed_press_publisher_id], coder: JSON
before_create :create_remote_site!, :create_njalla_records!
before_destroy :delete_remote_site!, :delete_njalla_records!
before_create :create_remote_site!
before_destroy :delete_remote_site!
DEPENDENCIES = %i[deploy_local]
@ -31,17 +30,12 @@ class DeployDistributedPress < Deploy
time_start
create_remote_site! if remote_site_id.blank?
create_njalla_records!
save
if remote_site_id.blank?
raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press'
end
if create_njalla_records? && remote_info[:njalla].blank?
raise DeployJob::DeployException, 'No se pudieron crear los registros necesarios en Njalla'
end
site_client.tap do |c|
stdout = Thread.new(publisher.logger_out) do |io|
until io.eof?
@ -101,12 +95,14 @@ class DeployDistributedPress < Deploy
# El cliente de la API
#
# TODO: cuando soportemos más, tiene que haber una relación entre
# DeployDistributedPress y DistributedPressPublisher.
#
# @return [DistributedPressPublisher]
def publisher
@publisher ||= DistributedPressPublisher.last
@publisher ||=
if distributed_press_publisher_id
DistributedPressPublisher.find(distributed_press_publisher_id)
else
DistributedPressPublisher.find_by_default(true)
end
end
# El cliente para actualizar el sitio
@ -147,29 +143,6 @@ class DeployDistributedPress < Deploy
nil
end
# Crea los registros en Njalla
#
# XXX: Esto depende de nuestro DNS actual, cuando lo migremos hay
# que eliminarlo.
#
# @return [nil]
def create_njalla_records!
return unless create_njalla_records?
self.remote_info ||= {}
self.remote_info[:njalla] ||= {}
self.remote_info[:njalla][:a] ||= njalla.add_record(name: site.name, type: 'CNAME', content: "#{Site.domain}.").to_h
self.remote_info[:njalla][:cname] ||= njalla.add_record(name: "www.#{site.name}", type: 'CNAME', content: "#{Site.domain}.").to_h
self.remote_info[:njalla][:ns] ||= njalla.add_record(name: "_dnslink.#{site.name}", type: 'NS', content: "#{publisher.hostname}.").to_h
nil
rescue HTTParty::Error => e
ExceptionNotifier.notify_exception(e, data: { site: site.name })
self.remote_info.delete :njalla
ensure
nil
end
# Registra lo que sucedió
#
# @param status [Bool]
@ -187,31 +160,4 @@ class DeployDistributedPress < Deploy
ExceptionNotifier.notify_exception(e, data: { site: site.name })
nil
end
def delete_njalla_records!
return unless create_njalla_records?
%w[a ns cname].each do |type|
next if (id = remote_info.dig('njalla', type, 'id')).blank?
njalla.remove_record(id: id.to_i)
end
end
# Actualizar registros en Njalla
#
# @return [Njalla::V1::Domain]
def njalla
@njalla ||=
begin
client = Njalla::V1::Client.new(token: Rails.application.credentials.njalla)
Njalla::V1::Domain.new(domain: Site.domain, client: client)
end
end
# Detecta si tenemos que crear registros en Njalla
def create_njalla_records?
!site.name.end_with?('.')
end
end

View file

@ -7,11 +7,6 @@ class DeployLocal < Deploy
before_destroy :remove_destination!
def git_lfs(output: false)
run %(git lfs fetch), output: output
run %(git lfs checkout), output: output
end
def bundle(output: false)
run %(bundle config set --local clean 'true'), output: output
run %(bundle config set --local deployment 'true'), output: output if site.gemfile_lock_path?
@ -21,6 +16,11 @@ class DeployLocal < Deploy
run %(bundle install), output: output
end
def git_lfs(output: false)
run %(git lfs fetch), output: output
run %(git lfs checkout), output: output
end
# Realizamos la construcción del sitio usando Jekyll y un entorno
# limpio para no pasarle secretos
#

View file

@ -5,7 +5,9 @@ require 'distributed_press/v1/social/client'
# Publicar novedades al Fediverso
class DeploySocialDistributedPress < Deploy
# Solo luego de publicar remotamente
DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync]
DEPENDENCIES = %i[deploy_distributed_press deploy_rsync deploy_full_rsync].freeze
after_save :create_hooks!
# Envía las notificaciones
def deploy(output: false)
@ -52,4 +54,45 @@ class DeploySocialDistributedPress < Deploy
def flags_for_build(**args)
"--key #{Shellwords.escape args[:private_key].path}"
end
private
# Obtiene el hostname de la API de Sutty
#
# @return [String]
def api_hostname
Rails.application.routes.default_url_options[:host].sub('panel', 'api')
end
# Crea los hooks en la Social Inbox para que nos avise de actividades
# nuevas
#
# @return [nil]
def create_hooks!
hook_client = site.social_inbox.hook
webhook_class = DistributedPress::V1::Social::Schemas::Webhook
hook_client.class::EVENTS.each do |event|
event_url = :"v1_site_webhooks_#{event}_url"
webhook =
webhook_class.new.call({
method: 'POST',
url: Rails.application.routes.url_helpers.public_send(
event_url, site_id: site.name, host: api_hostname
),
headers: {
'X-Social-Inbox': rol.token
}
})
raise ArgumentError, webhook.errors.messages if webhook.failure?
response = hook_client.put(event: event, hook: webhook)
raise ArgumentError, response.body unless response.ok?
rescue ArgumentError => e
ExceptionNotifier.notify_exception(e, data: { site_id: site.name, usuarie_id: rol.usuarie_id })
end
end
end

View file

@ -13,8 +13,8 @@ class DistributedPressPublisher < ApplicationRecord
# @return [IO]
attr_reader :logger_out
# La instancia es única
validates_uniqueness_of :instance
# La instancia es necesaria pero no única
validates_presence_of :instance
# El token es necesario
validates_presence_of :token

View file

@ -6,7 +6,7 @@ class MetadataPath < MetadataTemplate
#
# @return [String]
def default_value
File.join(site.path, "_#{lang}", "#{date}-#{slug}#{ext}")
File.join(site.path, "_#{lang}", "#{limited_name}#{ext}")
end
# La ruta del archivo según Jekyll
@ -46,4 +46,12 @@ class MetadataPath < MetadataTemplate
def date
post.date.value.strftime('%F')
end
# Limita el nombre de archivo a 255 bytes, de forma que siempre
# podemos guardarlo
#
# @return [String]
def limited_name
"#{date}-#{slug}".mb_chars.limit(255 - ext.length)
end
end

View file

@ -132,8 +132,10 @@ class Post
src = element.attributes['src']
next unless src&.value&.start_with? 'public/'
file = MetadataFile.new(site: site, post: self, document: document, layout: layout)
file.value['path'] = src.value
src.value = Rails.application.routes.url_helpers.site_static_file_url(site, file: src.value)
src.value = Rails.application.routes.url_helpers.url_for(file.static_file)
end
# Notificar a les usuaries que están viendo una previsualización

View file

@ -11,6 +11,7 @@ class Rol < ApplicationRecord
belongs_to :usuarie
belongs_to :site
has_many :deploys
validates_inclusion_of :rol, in: ROLES

View file

@ -54,8 +54,7 @@ class Site < ApplicationRecord
before_create :clone_skel!
# Elimina el directorio al destruir un sitio
before_destroy :remove_directories!
# Cambiar el nombre del directorio
before_update :update_name!
before_save :add_private_key_if_missing!
# Guardar la configuración si hubo cambios
after_save :sync_attributes_with_config!
@ -159,19 +158,19 @@ class Site < ApplicationRecord
# Traer la ruta del sitio
def path
File.join(Site.site_path, name)
::File.join(Site.site_path, name)
end
# La ruta anterior
def path_was
File.join(Site.site_path, name_was)
::File.join(Site.site_path, name_was)
end
# Limpiar la ruta y unirla con el separador de directorios del
# sistema operativo. Como si algún día fuera a cambiar o
# soportáramos Windows :P
def relative_path(suspicious_path)
File.join(path, *suspicious_path.gsub('..', '/').gsub('./', '').squeeze('/').split('/'))
::File.join(path, *suspicious_path.gsub('..', '/').gsub('./', '').squeeze('/').split('/'))
end
# Obtiene la lista de traducciones actuales
@ -286,7 +285,7 @@ class Site < ApplicationRecord
end
def jekyll?
File.directory? path
::File.directory? path
end
def jekyll
@ -304,7 +303,7 @@ class Site < ApplicationRecord
# documentos de Jekyll hacia Sutty para que podamos leer los datos que
# necesitamos.
def load_jekyll
return unless name.present? && File.directory?(path)
return unless name.present? && ::File.directory?(path)
reload_jekyll!
end
@ -332,7 +331,7 @@ class Site < ApplicationRecord
# metadatos de Document
@configuration =
::Jekyll.configuration('source' => path,
'destination' => File.join(path, '_site'),
'destination' => ::File.join(path, '_site'),
'safe' => true, 'watch' => false,
'quiet' => true, 'excerpt_separator' => '')
@ -357,7 +356,7 @@ class Site < ApplicationRecord
# El directorio donde se almacenan los sitios
def self.site_path
@site_path ||= File.realpath(ENV.fetch('SITE_PATH', Rails.root.join('_sites')))
@site_path ||= ::File.realpath(ENV.fetch('SITE_PATH', Rails.root.join('_sites')))
end
def self.default
@ -387,7 +386,7 @@ class Site < ApplicationRecord
end
def gemfile_lock_path?
File.exist? gemfile_lock_path
::File.exist? gemfile_lock_path
end
private
@ -413,12 +412,6 @@ class Site < ApplicationRecord
FileUtils.rm_rf path
end
def update_name!
return unless name_changed?
FileUtils.mv path_was, path
reload_jekyll!
end
# Sincroniza algunos atributos del sitio con su configuración y
# guarda los cambios
@ -429,8 +422,8 @@ class Site < ApplicationRecord
config.theme = design.gem unless design.no_theme?
config.description = description
config.title = title
config.url = url(slash: false)
config.hostname = hostname
config.url ||= url(slash: false)
config.hostname ||= hostname
config.locales = locales.map(&:to_s)
end
@ -484,6 +477,12 @@ class Site < ApplicationRecord
I18n.t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.error'))
end
def run_in_path(&block)
Site.one_at_a_time.synchronize do
Dir.chdir path, &block
end
end
# Instala las gemas cuando es necesario:
#
# * El sitio existe
@ -499,6 +498,7 @@ class Site < ApplicationRecord
if !gems_installed? || gemfile_updated? || gemfile_lock_updated?
deploy_local.bundle
touch
FileUtils.touch(gemfile_path)
end
end
@ -519,12 +519,16 @@ class Site < ApplicationRecord
# Detecta si el Gemfile fue modificado
def gemfile_updated?
updated_at < File.mtime(File.join(path, 'Gemfile'))
updated_at < ::File.mtime(gemfile_path)
end
def gemfile_path
@gemfile_path ||= ::File.join(path, 'Gemfile')
end
# @return [String]
def gemfile_lock_path
@gemfile_lock_path ||= File.join(path, 'Gemfile.lock')
@gemfile_lock_path ||= ::File.join(path, 'Gemfile.lock')
end
# Detecta si el Gemfile.lock fue modificado con respecto al sitio o al
@ -532,8 +536,8 @@ class Site < ApplicationRecord
def gemfile_lock_updated?
return false unless gemfile_lock_path?
[updated_at, File.mtime(File.join(path, 'Gemfile'))].any? do |compare|
compare < File.mtime(gemfile_lock_path)
[updated_at, ::File.mtime(::File.join(path, 'Gemfile'))].any? do |compare|
compare < ::File.mtime(gemfile_lock_path)
end
end
end

View file

@ -21,7 +21,7 @@ class Site
# Leer el archivo de configuración y setear los atributos en el
# objeto actual, creando los metodos de ostruct
def read
data = YAML.safe_load(File.read(path))
data = YAML.safe_load(File.read(path), permitted_classes: [Time])
@hash = data.hash
data.each do |key, value|

View file

@ -76,12 +76,17 @@ class Site
# escribir los cambios
rugged.checkout 'HEAD', strategy: :force
git_sh("git", "lfs", "fetch", "origin", default_branch)
# reemplaza los pointers por los archivos correspondientes
git_sh("git", "lfs", "checkout")
commit
end
# Trae todos los archivos desde LFS
#
# @return [Boolean]
def git_lfs_checkout
git_sh('git', 'lfs', 'fetch', 'origin', default_branch)
git_sh('git', 'lfs', 'checkout')
end
# El último commit
#
# @return [Rugged::Commit]
@ -111,10 +116,30 @@ class Site
walker.each.to_a
end
# Hay commits sin aplicar?
def needs_pull?
fetch
!commits.empty?
# Detecta si hay que hacer un pull o no
#
# @return [Boolean]
def up_to_date?
rugged.merge_analysis(remote_head_commit).include?(:up_to_date)
end
# Detecta si es posible adelantar la historia local a la remota o
# necesitamos un merge
#
# @return [Boolean]
def fast_forward?
rugged.merge_analysis(remote_head_commit).include?(:fastforward)
end
# Mueve la historia local a la remota
#
# @see {https://stackoverflow.com/a/27077322}
# @return [nil]
def fast_forward!
rugged.checkout_tree(remote_head_commit)
rugged.references.update(rugged.head.resolve, remote_head_commit.oid)
nil
end
# Guarda los cambios en git

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'distributed_press/v1/social/client'
class Site
# Agrega soporte para Social Distributed Press en los sitios
module SocialDistributedPress
@ -8,15 +10,25 @@ class Site
included do
encrypts :private_key_pem
has_many :activity_pubs
before_save :generate_private_key_pem!, unless: :private_key_pem?
# @return [SocialInbox]
def social_inbox
@social_inbox ||= SocialInbox.new(site: self)
end
private
# Genera la llave privada y la almacena
#
# @return [nil]
def generate_private_key_pem!
self.private_key_pem ||= DistributedPress::V1::Social::Client.new(public_key_url: nil, key_size: 2048).private_key.export
self.private_key_pem ||= DistributedPress::V1::Social::Client.new(
public_key_url: nil,
key_size: 2048
).private_key.export
end
end
end

View file

@ -0,0 +1,81 @@
# frozen_string_literal: true
require 'distributed_press/v1/social/client'
require 'distributed_press/v1/social/hook'
require 'distributed_press/v1/social/inbox'
require 'distributed_press/v1/social/dereferencer'
require 'httparty/cache/store/redis'
# Gestiona la Social Inbox de un sitio
class SocialInbox
# @return [Site]
attr_reader :site
# @param :site [Site]
def initialize(site:)
@site = site
end
# @return [String]
def actor
@actor ||=
begin
user = site.config.dig('activity_pub', 'username')
user ||= hostname.split('.', 2).first
"@#{user}@#{hostname}"
end
end
def actor_id
@actor_id ||= generate_uri do |uri|
uri.path = '/about.jsonld'
end
end
# @return [DistributedPress::V1::Social::Client]
def client
@client ||= DistributedPress::V1::Social::Client.new(
url: site.config.dig('activity_pub', 'url'),
public_key_url: public_key_url,
private_key_pem: site.private_key_pem,
logger: Rails.logger,
cache_store: HTTParty::Cache::Store::Redis.new(redis_url: ENV['REDIS_SERVER'])
)
end
# @return [DistributedPress::V1::Social::Inbox]
def inbox
@inbox ||= DistributedPress::V1::Social::Inbox.new(client: client, actor: actor)
end
# @return [DistributedPress::V1::Social::Dereferencer]
def dereferencer
@dereferencer ||= DistributedPress::V1::Social::Dereferencer.new(client: client)
end
# @return [DistributedPress::V1::Social::Hook]
def hook
@hook ||= DistributedPress::V1::Social::Hook.new(client: client, actor: actor)
end
# @return [String]
def public_key_url
@public_key_url ||= generate_uri do |uri|
uri.path = '/about.jsonld'
uri.fragment = 'main-key'
end
end
def hostname
@hostname ||=
site.config.dig('activity_pub', 'hostname') || site.hostname
end
# Genera una URI dentro de este sitio
#
# @return [String]
def generate_uri(&block)
@public_key_url ||= URI("https://#{hostname}").tap(&block).to_s
end
end

View file

@ -31,6 +31,7 @@ class CleanupService
site.deploys.find_each(&:cleanup!)
site.repository.gc
lfs_cleanup
site.touch
end
end
@ -45,7 +46,14 @@ class CleanupService
Rails.logger.info "Limpiando repositorio git de #{site.name}"
site.repository.gc
lfs_cleanup
site.touch
end
end
private
def lfs_cleanup
site.repository.git_sh("git", "lfs", "prune")
site.repository.git_sh("git", "lfs", "dedup")
end
end

View file

@ -83,6 +83,23 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
site.repository.merge(usuarie).present?
end
def rename(name)
return if name == site.name
moved = false
site.name = name
Site.transaction do
raise ActiveRecord::Rollback if File.exists?(site.path)
FileUtils.mv (site.path_was, site.path)
moved = true
ActiveStorage::Blob.where(service_name: site.name_was).update_all(service_name: site.name)
site.save
rescue StandardError
FileUtils.mv (site.path, site.path_was) if moved
raise
end
end
private
# Guarda los cambios de la configuración en el repositorio git

View file

@ -25,4 +25,4 @@
class: 'form-control'
.form-group
= f.submit t('.submit'), class: 'btn btn-lg btn-block'
= f.submit t('.submit'), class: 'btn btn-secondary btn-lg btn-block'

View file

@ -30,5 +30,5 @@
placeholder: t('activerecord.attributes.usuarie.email')
.actions
= f.submit t('.resend_confirmation_instructions'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -32,4 +32,4 @@
placeholder: t('activerecord.attributes.usuarie.password')
.actions
= f.submit t('devise.invitations.edit.submit_button'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'

View file

@ -16,4 +16,4 @@
= f.text_field field, class: 'form-control'
.actions
= f.submit t('devise.invitations.new.submit_button'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'

View file

@ -39,6 +39,6 @@
.actions
= f.submit t('.change_my_password'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -20,5 +20,5 @@
placeholder: t('activerecord.attributes.usuarie.email')
.actions
= f.submit t('.send_me_reset_password_instructions'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -55,7 +55,7 @@
= t('.we_need_your_current_password_to_confirm_your_changes')
.actions
= f.submit t('.update'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
%hr/
.sr-only
@ -63,4 +63,4 @@
= button_to t('.cancel_my_account'),
registration_path(resource_name),
data: { confirm: t('.are_you_sure') },
method: :delete, class: 'btn btn-block'
method: :delete, class: 'btn btn-secondary btn-block'

View file

@ -56,6 +56,6 @@
.actions
= f.submit t('.sign_up'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -32,5 +32,5 @@
= t('login.remember_me', remember_for: distance_of_time_in_words(Usuarie.remember_for))
.actions
= f.submit t('.sign_in'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -4,12 +4,12 @@
- if controller_name != 'sessions'
= link_to t('.sign_in'), new_session_path(resource_name, params: locale),
class: 'btn btn-lg btn-block btn-success'
class: 'btn btn-lg btn-block btn-secondary'
%br/
- if devise_mapping.registerable? && controller_name != 'registrations'
= link_to t('.sign_up'), new_registration_path(resource_name, params: locale),
class: 'btn btn-lg btn-block btn-success'
class: 'btn btn-lg btn-block btn-secondary'
%br/
- if devise_mapping.recoverable?

View file

@ -20,5 +20,5 @@
placeholder: t('activerecord.attributes.usuarie.email')
.actions
= f.submit t('.resend_unlock_instructions'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -11,7 +11,7 @@
= select_tag 'to',
options_for_select(@options, @lang_to),
class: 'form-control'
= submit_tag t('i18n.translate'), class: 'btn', name: nil
= submit_tag t('i18n.translate'), class: 'btn btn-secondary', name: nil
- else
= t('i18n.translating.from')
= select_tag 'from',
@ -21,7 +21,7 @@
= select_tag 'to',
options_for_select(@options, @lang_to),
class: 'form-control'
= submit_tag t('i18n.change'), class: 'btn', name: nil
= submit_tag t('i18n.change'), class: 'btn btn-secondary', name: nil
= render 'layouts/help', help: t('help.i18n.index')
@ -33,16 +33,16 @@
= hidden_field 'i18n', 'lang_to', value: @lang_to
.form-group
.dropdown.inline
%button.btn.dropdown-toggle{type: 'button',
%button.btn.btn-secondary.dropdown-toggle{type: 'button',
data: { toggle: 'dropdown' },
aria: { haspopup: 'true', expanded: 'false' }}
= t('i18n.jump')
.dropdown-menu{aria: { labelledby: t('i18n.jump') }}
- @site.data.dig(@lang_from).each_pair do |section, content|
%a.dropdown-item{href: "##{section}"}= t("help.i18n.#{section}")
= submit_tag t('i18n.save'), class: 'btn'
= submit_tag t('i18n.save'), class: 'btn btn-secondary'
= render 'i18n/recursive', data: @site.data.dig(@lang_from), superkeys: []
.form-group
= submit_tag t('i18n.save'), class: 'btn'
= submit_tag t('i18n.save'), class: 'btn btn-secondary'

View file

@ -18,15 +18,15 @@
- if @site&.tienda?
%li.nav-item
= link_to t('.tienda'), @site.tienda_url,
role: 'button', class: 'btn'
role: 'button', class: 'btn btn-secondary'
%li.nav-item
= link_to t('.contact_us'), t('.contact_us_href'),
class: 'btn', rel: 'me', target: '_blank'
class: 'btn btn-secondary', rel: 'me', target: '_blank'
%li.nav-item
= link_to t('.logout'), main_app.destroy_usuarie_session_path,
method: :delete, role: 'button', class: 'btn'
method: :delete, role: 'button', class: 'btn btn-secondary'
- else
- params.permit!
- I18n.available_locales.each do |locale|

View file

@ -1,2 +1,2 @@
= link_to text, link, class: 'btn',
= link_to text, link, class: 'btn btn-secondary',
data: { toggle: 'tooltip' }, 'aria-role': 'button', title: tooltip

View file

@ -1,7 +1,7 @@
- invalid_help = site.config.fetch('invalid_help', t('.invalid_help'))
- sending_help = site.config.fetch('sending_help', t('.sending_help'))
.form-group
= submit_tag t('.save'), class: 'btn submit-post'
= submit_tag t('.save'), class: 'btn btn-secondary submit-post'
= render 'bootstrap/alert', class: 'invalid-help d-none' do
= invalid_help
= render 'bootstrap/alert', class: 'sending-help d-none' do

View file

@ -20,82 +20,82 @@
TODO: Eliminar todo el espacio en blanco para minificar HTML
.editor-toolbar{ style: 'z-index: 1' }
.editor-primary-toolbar.scrollbar-black
%button.btn{ type: 'button', title: t('editor.multimedia'), data: { editor_button: 'multimedia' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.multimedia'), data: { editor_button: 'multimedia' } }>
%i.fa.fa-fw.fa-upload>
%span.sr-only>= t('editor.multimedia')
%button.btn{ type: 'button', title: t('editor.bold'), data: { editor_button: 'mark-bold' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.bold'), data: { editor_button: 'mark-bold' } }>
%i.fa.fa-fw.fa-bold>
%span.sr-only>= t('editor.bold')
%button.btn{ type: 'button', title: t('editor.italic'), data: { editor_button: 'mark-italic' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.italic'), data: { editor_button: 'mark-italic' } }>
%i.fa.fa-fw.fa-italic>
%span.sr-only>= t('editor.italic')
%button.btn{ type: 'button', title: t('editor.mark'), data: { editor_button: 'mark-mark' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.mark'), data: { editor_button: 'mark-mark' } }>
%i.fa.fa-fw.fa-tint>
%span.sr-only>= t('editor.mark')
%button.btn{ type: 'button', title: t('editor.link'), data: { editor_button: 'mark-link' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.link'), data: { editor_button: 'mark-link' } }>
%i.fa.fa-fw.fa-link>
%span.sr-only>= t('editor.link')
%button.btn{ type: 'button', title: t('editor.deleted'), data: { editor_button: 'mark-deleted' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.deleted'), data: { editor_button: 'mark-deleted' } }>
%i.fa.fa-fw.fa-strikethrough>
%span.sr-only>= t('editor.deleted')
%button.btn{ type: 'button', title: t('editor.underline'), data: { editor_button: 'mark-underline' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.underline'), data: { editor_button: 'mark-underline' } }>
%i.fa.fa-fw.fa-underline>
%span.sr-only>= t('editor.underline')
%button.btn{ type: 'button', title: t('editor.super'), data: { editor_button: 'mark-super' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.super'), data: { editor_button: 'mark-super' } }>
%i.fa.fa-fw.fa-superscript>
%span.sr-only>= t('editor.super')
%button.btn{ type: 'button', title: t('editor.sub'), data: { editor_button: 'mark-sub' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.sub'), data: { editor_button: 'mark-sub' } }>
%i.fa.fa-fw.fa-subscript>
%span.sr-only>= t('editor.sub')
%button.btn{ type: 'button', title: t('editor.small'), data: { editor_button: 'mark-small' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.small'), data: { editor_button: 'mark-small' } }>
%i.fa.fa-fw.fa-subscript>
%span.sr-only>= t('editor.small')
%button.btn.mr-0{ type: 'button', title: t('editor.h1'), data: { editor_button: 'block-h1' } }>
%button.btn.btn-secondary.mr-0{ type: 'button', title: t('editor.h1'), data: { editor_button: 'block-h1' } }>
%i.fa.fa-fw.fa-heading>
1
%span.sr-only>= t('editor.h1')
%details.d-inline>
%summary.d-inline>
%span.btn.ml-0{ role: 'button', title: t('editor.more') }>
%span.btn.btn-secondary.ml-0{ role: 'button', title: t('editor.more') }>
%i.fa.fa-caret-right>
%span.sr-only= t('editor.more')
.d-inline>
%button.btn{ type: 'button', title: t('editor.h2'), data: { editor_button: 'block-h2' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h2'), data: { editor_button: 'block-h2' } }>
%i.fa.fa-fw.fa-heading>
2
%span.sr-only>= t('editor.h2')
%button.btn{ type: 'button', title: t('editor.h3'), data: { editor_button: 'block-h3' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h3'), data: { editor_button: 'block-h3' } }>
%i.fa.fa-fw.fa-heading>
3
%span.sr-only>= t('editor.h3')
%button.btn{ type: 'button', title: t('editor.h4'), data: { editor_button: 'block-h4' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h4'), data: { editor_button: 'block-h4' } }>
%i.fa.fa-fw.fa-heading>
4
%span.sr-only>= t('editor.h4')
%button.btn{ type: 'button', title: t('editor.h5'), data: { editor_button: 'block-h5' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h5'), data: { editor_button: 'block-h5' } }>
%i.fa.fa-fw.fa-heading>
5
%span.sr-only>= t('editor.h5')
%button.btn{ type: 'button', title: t('editor.h6'), data: { editor_button: 'block-h6' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h6'), data: { editor_button: 'block-h6' } }>
%i.fa.fa-fw.fa-heading>
6
%span.sr-only>= t('editor.h6')
%button.btn{ type: 'button', title: t('editor.ul'), data: { editor_button: 'block-unordered_list' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.ul'), data: { editor_button: 'block-unordered_list' } }>
%i.fa.fa-fw.fa-list-ul>
%span.sr-only>= t('editor.ul')
%button.btn{ type: 'button', title: t('editor.ol'), data: { editor_button: 'block-ordered_list' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.ol'), data: { editor_button: 'block-ordered_list' } }>
%i.fa.fa-fw.fa-list-ol>
%span.sr-only>= t('editor.ol')
%button.btn{ type: 'button', title: t('editor.left'), data: { editor_button: 'parentBlock-left' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.left'), data: { editor_button: 'parentBlock-left' } }>
%i.fa.fa-fw.fa-align-left>
%span.sr-only>= t('editor.left')
%button.btn{ type: 'button', title: t('editor.center'), data: { editor_button: 'parentBlock-center' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.center'), data: { editor_button: 'parentBlock-center' } }>
%i.fa.fa-fw.fa-align-center>
%span.sr-only>= t('editor.center')
%button.btn{ type: 'button', title: t('editor.right'), data: { editor_button: 'parentBlock-right' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.right'), data: { editor_button: 'parentBlock-right' } }>
%i.fa.fa-fw.fa-align-right>
%span.sr-only>= t('editor.right')
%button.btn{ type: 'button', title: t('editor.blockquote'), data: { editor_button: 'block-blockquote' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.blockquote'), data: { editor_button: 'block-blockquote' } }>
%i.fa.fa-fw.fa-quote-left>
%span.sr-only>= t('editor.blockquote')
@ -116,8 +116,8 @@
%label{ for: 'multimedia-alt' }= t('editor.description')
%input.form-control{ type: 'text', id: 'multimedia-alt', name: 'multimedia-alt' }/
.form-group
%button.btn{ type: 'button', id: 'multimedia-file-upload', name: 'multimedia-file-upload' }= t('editor.multimedia-upload')
%button.btn{ type: 'button', id: 'multimedia-remove', name: 'multimedia-remove' }= t('editor.multimedia-remove')
%button.btn.btn-secondary{ type: 'button', id: 'multimedia-file-upload', name: 'multimedia-file-upload' }= t('editor.multimedia-upload')
%button.btn.btn-secondary{ type: 'button', id: 'multimedia-remove', name: 'multimedia-remove' }= t('editor.multimedia-remove')
.form-group{ data: { editor_auxiliary: 'link' } }
%label{ for: 'link-url' }= t('editor.url')

View file

@ -22,13 +22,13 @@
= render 'schemas/row', site: @site, schema: schema, filter: @filter_params
- if policy(@site_stat).index?
= link_to t('stats.index.title'), site_stats_path(@site), class: 'btn'
= link_to t('stats.index.title'), site_stats_path(@site), class: 'btn btn-secondary'
- if policy(@site).edit?
= link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn'
= link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn btn-secondary'
- if policy(@site).private?
= link_to t('sites.private'), '../private/' + @site.name, class: 'btn', target: '_blank', rel: 'noopener'
= link_to t('sites.private'), '../private/' + @site.name, class: 'btn btn-secondary', target: '_blank', rel: 'noopener'
- if policy(SiteUsuarie.new(@site, current_usuarie)).index?
= render 'layouts/btn_with_tooltip',
@ -40,9 +40,9 @@
- if @site.design.credits
= render 'bootstrap/alert' do
= sanitize_markdown @site.design.credits
= link_to t('sites.donations.text'), t('sites.donations.url'), class: 'btn'
= link_to t('sites.donations.text'), t('sites.donations.url'), class: 'btn btn-secondary'
- if @site.design.designer_url
= link_to t('sites.designer_url'), @site.design.designer_url, class: 'btn'
= link_to t('sites.designer_url'), @site.design.designer_url, class: 'btn btn-secondary'
%section.col
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
@ -82,20 +82,20 @@
.d-flex.flex-row.justify-content-between
%div
- if reorder_allowed
= submit_tag t('posts.reorder.submit'), class: 'btn'
%button.btn{ data: { action: 'reorder#unselect' } }
= submit_tag t('posts.reorder.submit'), class: 'btn btn-secondary'
%button.btn.btn-secondary{ data: { action: 'reorder#unselect' } }
= t('posts.reorder.unselect')
%span.badge{ data: { target: 'reorder.counter' } } 0
%button.btn{ data: { action: 'reorder#up' } }= t('posts.reorder.up')
%button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down')
%button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
%button.btn.btn-secondary{ data: { action: 'reorder#up' } }= t('posts.reorder.up')
%button.btn.btn-secondary{ data: { action: 'reorder#down' } }= t('posts.reorder.down')
%button.btn.btn-secondary{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
%button.btn.btn-secondary{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
%input{ type: 'hidden', name: 'post[lang]', value: @locale }
- if @site.pagination
%div
= link_to_prev_page @posts, t('posts.prev'), class: 'btn'
= link_to_next_page @posts, t('posts.next'), class: 'btn'
= link_to_prev_page @posts, t('posts.prev'), class: 'btn btn-secondary'
= link_to_next_page @posts, t('posts.next'), class: 'btn btn-secondary'
%tbody
- dir = @site.data.dig(params[:locale], 'dir')
- size = @posts.size
@ -136,9 +136,9 @@
= post.order
%td.text-nowrap
- if @usuarie || policy(post).edit?
= link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block'
= link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-secondary btn-block'
- if @usuarie || policy(post).destroy?
= link_to t('posts.destroy'), site_post_path(@site, post.path), class: 'btn btn-block', method: :delete, data: { confirm: t('posts.confirm_destroy') }
= link_to t('posts.destroy'), site_post_path(@site, post.path), class: 'btn btn-secondary btn-block', method: :delete, data: { confirm: t('posts.confirm_destroy') }
-#
Rescatar cualquier error en un post, notificarlo e
ignorar su renderización.

View file

@ -4,7 +4,7 @@
%article.content.table-responsive-md
= link_to t('posts.edit'),
edit_site_post_path(@site, @post.id),
class: 'btn btn-block'
class: 'btn btn-secondary btn-block'
%table.table.table-condensed
%thead

View file

@ -3,7 +3,7 @@
method: :post,
class: 'form-inline inline' do
= submit_tag site.enqueued? ? t('sites.enqueued') : t('sites.enqueue'),
class: "btn no-border-radius #{local_assigns[:class]}",
class: "btn btn-secondary #{local_assigns[:class]}",
title: site.enqueued? ? t('help.sites.enqueued') : t('help.sites.enqueue'),
data: { disable_with: t('sites.enqueued') },
disabled: site.enqueued?

View file

@ -72,10 +72,10 @@
.btn-group{ role: 'group', 'aria-label': t('.design.actions') }
- if design.url
= link_to t('.design.url'), design.url,
target: '_blank', class: 'btn'
target: '_blank', class: 'btn btn-secondary'
- if design.license
= link_to t('.design.license'), design.license,
target: '_blank', class: 'btn'
target: '_blank', class: 'btn btn-secondary'
%hr/
.form-group.licenses#license_id
@ -99,7 +99,7 @@
tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6]
- unless licencia.custom?
= link_to t('.licencia.url'), licencia.url, target: '_blank', class: 'btn', rel: 'noopener'
= link_to t('.licencia.url'), licencia.url, target: '_blank', class: 'btn btn-secondary', rel: 'noopener'
%hr/
@ -164,4 +164,4 @@
deploy: deploy_fields, site: site
.form-group
= f.submit submit, class: 'btn btn-lg btn-block'
= f.submit submit, class: 'btn btn-secondary btn-lg btn-block'

View file

@ -27,4 +27,4 @@
.row.justify-content-center
.col-12.col-lg-8
= link_to t('.merge.request'), site_pull_path(@site),
method: 'post', class: 'btn btn-lg'
method: 'post', class: 'btn btn-secondary btn-lg'

View file

@ -4,7 +4,7 @@
%p.lead= t('.help')
- if policy(Site).new?
= link_to t('sites.new.title'), new_site_path,
class: 'btn'
class: 'btn btn-secondary'
%section.col
- if @sites.empty?
@ -29,18 +29,18 @@
= site.title
%p.lead= site.description
%br
= link_to t('.visit'), site.url, class: 'btn'
= link_to t('.visit'), site.url, class: 'btn btn-secondary'
- if rol.temporal
= button_to t('sites.invitations.accept'),
site_usuaries_accept_invitation_path(site),
method: :patch,
title: t('help.sites.invitations.accept'),
class: 'btn'
class: 'btn btn-secondary'
= button_to t('sites.invitations.reject'),
site_usuaries_reject_invitation_path(site),
method: :patch,
title: t('help.sites.invitations.reject'),
class: 'btn'
class: 'btn btn-secondary'
- else
- if policy(site).show?
= render 'layouts/btn_with_tooltip',

View file

@ -11,11 +11,11 @@
%form.mb-5.form-inline{ method: 'get' }
- Stat::INTERVALS.each do |interval|
= link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls], period_start: params[:period_start].to_date.try(:"beginning_of_#{interval}").to_date, period_end: params[:period_end]), class: "mb-0 btn #{'btn-primary active' if @interval == interval}"
= link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls], period_start: params[:period_start].to_date.try(:"beginning_of_#{interval}").to_date, period_end: params[:period_end]), class: "mb-0 btn #{@interval == interval ? 'btn-primary active' : 'btn-secondary' }"
%input.form-control{ type: 'date', name: :period_start, value: params[:period_start] }
%input.form-control{ type: 'date', name: :period_end, value: params[:period_end] }
%button.btn.mb-0{ type: 'submit' }= t('.filter')
%button.btn.btn-secondary.mb-0{ type: 'submit' }= t('.filter')
.mb-5
%h2= t('.host.title', count: @hostnames.size)
@ -34,7 +34,7 @@
%textarea#urls.form-control{ name: 'urls', autocomplete: 'on', required: true, rows: @normalized_urls.size + 1, aria_describedby: 'help-urls' }= @normalized_urls.join("\n")
%small#help-urls.feedback.form-text.text-muted= t('.urls.help')
.form-group
%button.btn{ type: 'submit' }= t('.urls.submit')
%button.btn.btn-secondary{ type: 'submit' }= t('.urls.submit')
- if @normalized_urls.present?
= line_chart site_stats_uris_path(urls: @normalized_urls, **@chart_params), **@chart_options

View file

@ -9,13 +9,13 @@
- if @policy.invite?
= link_to t('.invite'),
site_usuaries_invite_path(@site, invite_as: u.to_s),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip' },
title: t('.help.invite', invite_as: u.to_s)
- if policy(Collaboration.new(@site)).collaborate?
= link_to t('.public_invite'),
site_collaborate_path(@site),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip' },
title: t('.help.public_invite')
%p.lead= t(".help.#{u}")
@ -38,7 +38,7 @@
- if @policy.demote? && @site.usuarie?(cuenta)
= link_to t('.demote.text'),
site_usuarie_demote_path(@site, cuenta),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip',
confirm: t('.demote.confirm') },
title: t('.help.demote'),
@ -46,7 +46,7 @@
- if @policy.promote? && @site.invitade?(cuenta)
= link_to t('.promote.text'),
site_usuarie_promote_path(@site, cuenta),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip',
confirm: t('.promote.confirm') },
title: t('.help.promote'),
@ -54,7 +54,7 @@
- if @policy.destroy?
= link_to t('.destroy.text'),
site_usuarie_path(@site, cuenta),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip',
confirm: t('.destroy.confirm') },
title: t('.help.destroy'),

View file

@ -13,4 +13,4 @@
invite_as: invite_as)
= f.text_area :invitaciones, class: 'form-control'
.form-group
= f.submit t('.submit'), class: 'btn'
= f.submit t('.submit'), class: 'btn btn-secondary'

8
bin/modified_files Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
set -e
test -n "${CI_MERGE_REQUEST_DIFF_BASE_SHA}"
git diff --name-status ${CI_MERGE_REQUEST_DIFF_BASE_SHA} \
| grep -v "^D" \
| cut -f 2

4
bin/with_extension Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
grep "\.${1}$"
exit 0

View file

@ -2,6 +2,10 @@
require_relative 'boot'
require 'aasm'
require 'redis-client'
require 'hiredis-client'
require 'brs'
require 'rails'
# Pick the frameworks you want:
require 'active_model/railtie'
@ -21,6 +25,17 @@ require 'rails/test_unit/railtie'
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
if %w[development test].include? ENV['RAILS_ENV']
# https://github.com/bkeepers/dotenv/pull/453
Dotenv::Railtie.class_eval do
def overload
Dotenv.overload(*dotenv_files.reverse)
end
end
Dotenv::Railtie.overload
end
module Sutty
# Sutty!
class Application < Rails::Application

252
config/brakeman.ignore Normal file
View file

@ -0,0 +1,252 @@
{
"ignored_warnings": [
{
"warning_type": "Redirect",
"warning_code": 18,
"fingerprint": "0ae5c3990d49dfbfd4fd61874451f7a576d5056aca913068adf58c314625f810",
"check_name": "Redirect",
"message": "Possible unprotected redirect",
"file": "app/controllers/api/v1/posts_controller.rb",
"line": 20,
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
"code": "redirect_to((params[:redirect_to] or origin.to_s))",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V1::PostsController",
"method": "create"
},
"user_input": "params[:redirect_to]",
"confidence": "High",
"cwe_id": [
601
],
"note": "https://0xacab.org/sutty/sutty/-/issues/14957"
},
{
"warning_type": "Denial of Service",
"warning_code": 76,
"fingerprint": "1947d1a2ae6e4bf718d0cc563e660efca96897165e9a8dd18186c1d7abe6ddf6",
"check_name": "RegexDoS",
"message": "Model attribute used in regular expression",
"file": "app/controllers/api/v1/base_controller.rb",
"line": 20,
"link": "https://brakemanscanner.org/docs/warning_types/denial_of_service/",
"code": "/\\.#{Site.domain}\\z/",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V1::BaseController",
"method": "site_id"
},
"user_input": "Site.domain",
"confidence": "Medium",
"cwe_id": [
20,
185
],
"note": "No es un atributo, es una variable de entorno"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 4,
"fingerprint": "28d98d08a15c4b3ad94a2cfa20a12573de12d99f1a30b3ca51074ee1f1886592",
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in `link_to` href",
"file": "app/views/layouts/_breadcrumb.haml",
"line": 19,
"link": "https://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(t(\".tienda\"), Site.find(params[:site_id]).tienda_url, :role => \"button\", :class => \"btn\")",
"render_path": [
{
"type": "controller",
"class": "Api::V1::NoticesController",
"method": "site",
"line": 31,
"file": "app/controllers/api/v1/notices_controller.rb",
"rendered": {
"name": "layouts/application",
"file": "app/views/layouts/application.html.haml"
}
},
{
"type": "template",
"name": "layouts/application",
"line": 25,
"file": "app/views/layouts/application.html.haml",
"rendered": {
"name": "layouts/_breadcrumb",
"file": "app/views/layouts/_breadcrumb.haml"
}
}
],
"location": {
"type": "template",
"template": "layouts/_breadcrumb"
},
"user_input": "Site.find(params[:site_id]).tienda_url",
"confidence": "Weak",
"cwe_id": [
79
],
"note": ""
},
{
"warning_type": "Redirect",
"warning_code": 18,
"fingerprint": "5034e51aaa1bac06d15fdde5956edffbfd65f94f5620a409526bbea896dc7b5f",
"check_name": "Redirect",
"message": "Possible unprotected redirect",
"file": "app/controllers/api/v1/contact_controller.rb",
"line": 26,
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
"code": "redirect_to((params[:redirect] or origin.to_s))",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V1::ContactController",
"method": "receive"
},
"user_input": "params[:redirect]",
"confidence": "High",
"cwe_id": [
601
],
"note": "https://0xacab.org/sutty/sutty/-/issues/14957"
},
{
"warning_type": "Mass Assignment",
"warning_code": 70,
"fingerprint": "50582f39f8dfa900d3f2b5b9908b1592f8b8bd9e2d0b9d1cc05d77e5ede2d94e",
"check_name": "MassAssignment",
"message": "Specify exact keys allowed for mass assignment instead of using `permit!` which allows any keys",
"file": "app/views/layouts/_link_rel_alternate.haml",
"line": 2,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit!",
"render_path": [
{
"type": "controller",
"class": "Api::V1::BaseController",
"method": "site_id",
"line": 20,
"file": "app/controllers/api/v1/base_controller.rb",
"rendered": {
"name": "layouts/application",
"file": "app/views/layouts/application.html.haml"
}
},
{
"type": "template",
"name": "layouts/application",
"line": 21,
"file": "app/views/layouts/application.html.haml",
"rendered": {
"name": "layouts/_link_rel_alternate",
"file": "app/views/layouts/_link_rel_alternate.haml"
}
}
],
"location": {
"type": "template",
"template": "layouts/_link_rel_alternate"
},
"user_input": null,
"confidence": "Medium",
"cwe_id": [
915
],
"note": "https://0xacab.org/sutty/sutty/-/issues/14958"
},
{
"warning_type": "Mass Assignment",
"warning_code": 70,
"fingerprint": "b8e0aa898288bebb614ccc1340d169caa196d315c6ac2e4744081cc892c2ae97",
"check_name": "MassAssignment",
"message": "Specify exact keys allowed for mass assignment instead of using `permit!` which allows any keys",
"file": "app/views/layouts/_breadcrumb.haml",
"line": 30,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit!",
"render_path": [
{
"type": "controller",
"class": "Api::V1::BaseController",
"method": "site_id",
"line": 20,
"file": "app/controllers/api/v1/base_controller.rb",
"rendered": {
"name": "layouts/application",
"file": "app/views/layouts/application.html.haml"
}
},
{
"type": "template",
"name": "layouts/application",
"line": 25,
"file": "app/views/layouts/application.html.haml",
"rendered": {
"name": "layouts/_breadcrumb",
"file": "app/views/layouts/_breadcrumb.haml"
}
}
],
"location": {
"type": "template",
"template": "layouts/_breadcrumb"
},
"user_input": null,
"confidence": "Medium",
"cwe_id": [
915
],
"note": "https://0xacab.org/sutty/sutty/-/issues/14958"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 4,
"fingerprint": "c051421c7cf4c2706b8e27bfd2f3b0661ec6a6df873da322a6b634b59e80351b",
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in `link_to` href",
"file": "app/views/sites/_form.haml",
"line": 74,
"link": "https://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(t(\".design.url\"), (Unresolved Model).new.url, :target => \"_blank\", :class => \"btn\")",
"render_path": [
{
"type": "controller",
"class": "SitesController",
"method": "new",
"line": 31,
"file": "app/controllers/sites_controller.rb",
"rendered": {
"name": "sites/new",
"file": "app/views/sites/new.haml"
}
},
{
"type": "template",
"name": "sites/new",
"line": 6,
"file": "app/views/sites/new.haml",
"rendered": {
"name": "sites/_form",
"file": "app/views/sites/_form.haml"
}
}
],
"location": {
"type": "template",
"template": "sites/_form"
},
"user_input": "(Unresolved Model).new.url",
"confidence": "Weak",
"cwe_id": [
79
],
"note": ""
}
],
"updated": "2024-01-11 18:12:14 -0300",
"brakeman_version": "5.4.1"
}

View file

@ -322,6 +322,10 @@ en:
[Pixelfed](https://pixelfed.social/site/about), and
[others](https://fediverse.party/)) can follow your site,
receive news and interact with them.
There is also the possibility that Sutty advertises your content and/or Fediverse
user automatically to attract followers. You can do it with this
[form](https://cryptpad.fr/form/#/2/form/view/yp1KZwQjgU2RG-zhdQCyw4M8QhftNCVu8e+IJG2iN7Y/)
stats:
index:
title: Statistics

View file

@ -327,6 +327,10 @@ es:
[Pixelfed](https://pixelfed.social/site/about) y
[otros](https://fediverse.party/)) pueden seguir a tu sitio,
recibir novedades e interactuar con ellas.
También existe la posibilidad de que Sutty anuncie tu contenido y/o usuarie del Fediverse
en forma automática para atraer seguidorxs. Podés hacerlo con este
[Formulario](https://cryptpad.fr/form/#/2/form/view/XorL4I-nC17rcEwtol3ghsRDsivfg6g5685MK+TFZ-8/)
stats:
index:
title: Estadísticas

View file

@ -18,7 +18,15 @@ Rails.application.routes.draw do
get :'contact/cookie', to: 'invitades#contact_cookie'
post :'contact/:form', to: 'contact#receive', as: :contact
post :'webhooks/pull', to: 'webhooks#pull'
namespace :webhooks do
post :pull, to: 'pull#pull'
scope :social_inbox do
post :moderationqueued, to: 'social_inbox#moderationqueued'
post :onapproved, to: 'social_inbox#onapproved'
post :onrejected, to: 'social_inbox#onrejected'
end
end
end
end
end
@ -28,9 +36,6 @@ Rails.application.routes.draw do
# alias en nginx sin tener que usar expresiones regulares para
# detectar el nombre del sitio.
get '/sites/private/:site_id(*file)', to: 'private#show', constraints: { site_id: %r{[^/]+} }
# Obtener archivos estáticos desde el directorio público
get '/sites/:site_id/static_file/(*file)', to: 'sites#static_file', as: 'site_static_file',
constraints: { site_id: %r{[^/]+} }
get '/env.js', to: 'env#index'
match '/api/v3/projects/:site_id/notices' => 'api/v1/notices#create', via: %i[post]

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
# Una instancia es la instancia por defecto
class AddDefaultToDistributedPressPublisher < ActiveRecord::Migration[6.1]
def up
add_column :distributed_press_publishers, :default, :boolean, default: false
DistributedPressPublisher.last.update(default: true)
end
def down
remove_column :distributed_press_publishers, :default
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
# Establece una relación entre roles y deploys
class AddRolToDeploys < ActiveRecord::Migration[6.1]
def up
add_column :deploys, :rol_id, :integer, index: true
Deploy.find_each do |deploy|
rol_id = deploy.site.roles.find_by(rol: 'usuarie', temporal: false)&.id
deploy.update_column(:rol_id, rol_id) if rol_id
end
end
def down
remove_column :deploys, :rol_id
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
# Actividades. Se asocian a un objeto y a una cola de moderación
class CreateActivityPubActivities < ActiveRecord::Migration[6.1]
def change
create_table :activity_pub_activities, id: :uuid do |t|
t.timestamps
t.uuid :activity_pub_id, index: true, null: false
t.string :type, null: false
t.string :uri, null: false
t.jsonb :content, default: {}
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Almacena actores de ActivityPub y los relaciona con actividades
class CreateActivityPubActors < ActiveRecord::Migration[6.1]
def change
create_table :activity_pub_actors, id: :uuid do |t|
t.timestamps
t.uuid :instance_id, index: true, null: false
t.string :uri, index: true, unique: true, null: false
end
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
# Registro de actividades.
class CreateActivityPubs < ActiveRecord::Migration[6.1]
def change
create_table :activity_pubs, id: :uuid do |t|
t.timestamps
t.bigint :site_id, null: false
t.uuid :object_id, null: false
t.string :object_type, null: false
t.string :aasm_state, null: false
t.index %i[site_id object_id object_type], unique: true
end
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
# Almacena objetos de ActivityPub. Los objetos pueden estar compartidos
# por toda la instancia.
class CreateActivityPubObjects < ActiveRecord::Migration[6.1]
def change
create_table :activity_pub_objects, id: :uuid do |t|
t.timestamps
t.uuid :actor_id, index: true, null: false
t.string :type, null: false
t.string :uri, null: false, unique: true
t.jsonb :content, default: {}
end
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
# Almacena las instancias
class CreateActivityPubInstances < ActiveRecord::Migration[6.1]
def change
create_table :activity_pub_instances, id: :uuid do |t|
t.timestamps
t.string :hostname, index: true, unique: true, null: false
t.string :aasm_state, null: false
t.jsonb :content, default: {}
end
end
end

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