mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-15 12:51:42 +00:00
Merge branch 'rails' into issue-14352
This commit is contained in:
commit
ab1619e6f7
65 changed files with 3244 additions and 628 deletions
11
.editorconfig
Normal file
11
.editorconfig
Normal file
|
@ -0,0 +1,11 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
8
Gemfile
8
Gemfile
|
@ -39,8 +39,7 @@ gem 'commonmarker'
|
|||
gem 'devise'
|
||||
gem 'devise-i18n'
|
||||
gem 'devise_invitable'
|
||||
gem 'distributed-press-api-client', '~> 0.2.3'
|
||||
gem 'njalla-api-client', '~> 0.2.0'
|
||||
gem 'distributed-press-api-client', '~> 0.3.0rc0'
|
||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||
gem 'exception_notification'
|
||||
gem 'fast_blank'
|
||||
|
@ -53,7 +52,6 @@ gem 'inline_svg'
|
|||
gem 'httparty'
|
||||
gem 'safe_yaml', require: false
|
||||
gem 'jekyll', '~> 4.2.0'
|
||||
gem 'jekyll-data'
|
||||
gem 'jekyll-commonmark', '~> 1.4.0'
|
||||
gem 'jekyll-images'
|
||||
gem 'jekyll-include-cache'
|
||||
|
@ -69,7 +67,8 @@ 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 'rugged'
|
||||
gem 'rugged', '1.5.0.1'
|
||||
gem 'git_clone_url'
|
||||
gem 'concurrent-ruby-ext'
|
||||
gem 'que'
|
||||
gem 'symbol-fstring', require: 'fstring/all'
|
||||
|
@ -78,6 +77,7 @@ gem 'validates_hostname'
|
|||
gem 'webpacker'
|
||||
gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
|
||||
gem 'kaminari'
|
||||
gem 'device_detector'
|
||||
|
||||
# database
|
||||
gem 'hairtrigger'
|
||||
|
|
21
Gemfile.lock
21
Gemfile.lock
|
@ -142,6 +142,7 @@ GEM
|
|||
rake (> 10, < 14)
|
||||
ruby-statistics (>= 2.1)
|
||||
thor (>= 0.19, < 2)
|
||||
device_detector (1.1.1)
|
||||
devise (4.9.2)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
|
@ -153,7 +154,7 @@ GEM
|
|||
devise_invitable (2.0.8)
|
||||
actionmailer (>= 5.0)
|
||||
devise (>= 4.6)
|
||||
distributed-press-api-client (0.2.4)
|
||||
distributed-press-api-client (0.3.0rc0)
|
||||
addressable (~> 2.3, >= 2.3.0)
|
||||
climate_control
|
||||
dry-schema
|
||||
|
@ -217,6 +218,8 @@ GEM
|
|||
activerecord (>= 4.0.0)
|
||||
get_process_mem (0.2.7)
|
||||
ffi (~> 1.0)
|
||||
git_clone_url (2.0.0)
|
||||
uri-ssh_git (>= 2.0)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
groupdate (6.2.1)
|
||||
|
@ -284,8 +287,6 @@ GEM
|
|||
terminal-table (~> 2.0)
|
||||
jekyll-commonmark (1.4.0)
|
||||
commonmarker (~> 0.22)
|
||||
jekyll-data (1.1.2)
|
||||
jekyll (>= 3.3, < 5.0.0)
|
||||
jekyll-images (0.4.1)
|
||||
jekyll (~> 4)
|
||||
ruby-filemagic (~> 0.7)
|
||||
|
@ -363,9 +364,6 @@ GEM
|
|||
net-ssh (7.1.0)
|
||||
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)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
|
@ -494,7 +492,7 @@ GEM
|
|||
ruby_parser (3.20.1)
|
||||
sexp_processor (~> 4.16)
|
||||
rubyzip (2.3.2)
|
||||
rugged (1.6.3-x86_64-linux-musl)
|
||||
rugged (1.5.0.1-x86_64-linux-musl)
|
||||
safe_yaml (1.0.6)
|
||||
safely_block (0.3.0)
|
||||
errbase (>= 0.1.1)
|
||||
|
@ -553,6 +551,7 @@ GEM
|
|||
unf_ext
|
||||
unf_ext (0.0.8.2-x86_64-linux-musl)
|
||||
unicode-display_width (1.8.0)
|
||||
uri-ssh_git (2.0.0)
|
||||
validates_hostname (1.0.13)
|
||||
activerecord (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
|
@ -592,10 +591,11 @@ DEPENDENCIES
|
|||
concurrent-ruby-ext
|
||||
database_cleaner
|
||||
derailed_benchmarks
|
||||
device_detector
|
||||
devise
|
||||
devise-i18n
|
||||
devise_invitable
|
||||
distributed-press-api-client (~> 0.2.3)
|
||||
distributed-press-api-client (~> 0.3.0rc0)
|
||||
dotenv-rails
|
||||
down
|
||||
ed25519
|
||||
|
@ -606,6 +606,7 @@ DEPENDENCIES
|
|||
fast_jsonparser (~> 0.5.0)
|
||||
flamegraph
|
||||
friendly_id
|
||||
git_clone_url
|
||||
hairtrigger
|
||||
haml-lint
|
||||
hamlit-rails
|
||||
|
@ -617,7 +618,6 @@ DEPENDENCIES
|
|||
jbuilder (~> 2.5)
|
||||
jekyll (~> 4.2.0)
|
||||
jekyll-commonmark (~> 1.4.0)
|
||||
jekyll-data
|
||||
jekyll-images
|
||||
jekyll-include-cache
|
||||
kaminari
|
||||
|
@ -630,7 +630,6 @@ DEPENDENCIES
|
|||
mini_magick
|
||||
mobility
|
||||
net-ssh
|
||||
njalla-api-client (~> 0.2.0)
|
||||
nokogiri
|
||||
pg
|
||||
pg_search
|
||||
|
@ -650,7 +649,7 @@ DEPENDENCIES
|
|||
rollups!
|
||||
rubocop-rails
|
||||
rubyzip
|
||||
rugged
|
||||
rugged (= 1.5.0.1)
|
||||
safe_yaml
|
||||
safely_block (~> 0.3.0)
|
||||
sassc-rails
|
||||
|
|
4
Makefile
4
Makefile
|
@ -69,14 +69,14 @@ rake: ## Corre rake dentro del entorno de desarrollo (pasar argumentos con args=
|
|||
bundle: ## Corre bundle dentro del entorno de desarrollo (pasar argumentos con args=).
|
||||
$(hain) 'bundle $(args)'
|
||||
|
||||
psql := psql -h $(PG_HOST) -U $(PG_USER) -p $(PG_PORT) -d sutty
|
||||
psql := psql $(DATABASE_URL)
|
||||
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)
|
||||
$(hain) $(psql)
|
||||
|
||||
rubocop: ## Yutea el código que está por ser commiteado
|
||||
git status --porcelain \
|
||||
|
|
1
Procfile
1
Procfile
|
@ -7,5 +7,6 @@ blazer: bundle exec rake blazer:send_failing_checks
|
|||
prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
|
||||
distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
|
||||
cleanup: bundle exec rake cleanup:everything
|
||||
emergency_cleanup: bundle exec rake cleanup:everything BEFORE=7
|
||||
stats: bundle exec rake stats:process_all
|
||||
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que
|
||||
|
|
9
app/assets/stylesheets/dark.scss
Normal file
9
app/assets/stylesheets/dark.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
$black: black;
|
||||
$white: white;
|
||||
$cyan: #13fefe;
|
||||
|
||||
:root {
|
||||
--foreground: #{$white};
|
||||
--background: #{$black};
|
||||
--color: #{$cyan};
|
||||
}
|
|
@ -9,10 +9,10 @@ module Api
|
|||
# Generar un stacktrace en segundo plano y enviarlo por correo
|
||||
# solo si la API key es verificable. Del otro lado siempre
|
||||
# respondemos con lo mismo.
|
||||
def create
|
||||
if site&.airbrake_valid? airbrake_token
|
||||
def create
|
||||
if (site&.airbrake_valid? airbrake_token) && !detected_device.bot?
|
||||
BacktraceJob.perform_later site_id: params[:site_id],
|
||||
params: airbrake_params.to_h
|
||||
params: airbrake_params.to_h
|
||||
end
|
||||
|
||||
render status: 201, json: { id: 1, url: '' }
|
||||
|
@ -34,6 +34,11 @@ module Api
|
|||
def airbrake_token
|
||||
@airbrake_token ||= params[:key]
|
||||
end
|
||||
|
||||
# @return [DeviceDetector]
|
||||
def detected_device
|
||||
@detected_device ||= DeviceDetector.new(request.headers['User-Agent'], request.headers)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ module Api
|
|||
|
||||
# Lista de nombres de dominios a emitir certificados
|
||||
def index
|
||||
render json: sites_names + alternative_names + api_names + www_names
|
||||
render json: alternative_names + api_names + www_names
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -18,17 +18,16 @@ module Api
|
|||
name.end_with?('.') ? name[0..-2] : "#{name}.#{Site.domain}"
|
||||
end
|
||||
|
||||
# Nombres de los sitios
|
||||
def sites_names
|
||||
Site.all.order(:name).pluck(:name).map do |name|
|
||||
canonicalize name
|
||||
end
|
||||
def subdomain?(name)
|
||||
name.end_with? ".#{Site.domain}"
|
||||
end
|
||||
|
||||
# Dominios alternativos
|
||||
def alternative_names
|
||||
(DeployAlternativeDomain.all.map(&:hostname) + DeployLocalizedDomain.all.map(&:hostname)).map do |name|
|
||||
canonicalize name
|
||||
end.reject do |name|
|
||||
subdomain? name
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -41,6 +40,8 @@ module Api
|
|||
.or(Site.where(colaboracion_anonima: true))
|
||||
.select("'api.' || name as name").map(&:name).map do |name|
|
||||
canonicalize name
|
||||
end.reject do |name|
|
||||
subdomain? name
|
||||
end
|
||||
end
|
||||
|
||||
|
|
77
app/controllers/api/v1/webhooks_controller.rb
Normal file
77
app/controllers/api/v1/webhooks_controller.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
# 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
|
|
@ -59,7 +59,11 @@ class ApplicationController < ActionController::Base
|
|||
#
|
||||
# @return [String,Symbol]
|
||||
def current_locale
|
||||
session[:locale] = params[:change_locale_to] if params[:change_locale_to].present?
|
||||
locale = params[:change_locale_to]
|
||||
|
||||
if locale.present? && I18n.locale_available?(locale)
|
||||
session[:locale] = params[:change_locale_to]
|
||||
end
|
||||
|
||||
session[:locale] || current_usuarie&.lang || I18n.locale
|
||||
end
|
||||
|
|
|
@ -22,7 +22,12 @@ class BuildStatsController < ApplicationController
|
|||
|
||||
@table = site.deployment_list.map do |deploy|
|
||||
type = deploy.class.name.underscore
|
||||
urls = deploy.respond_to?(:urls) ? deploy.urls : [deploy.url].compact
|
||||
urls = deploy.urls.map do |url|
|
||||
URI.parse(url)
|
||||
rescue URI::Error
|
||||
nil
|
||||
end.compact
|
||||
|
||||
urls = [nil] if urls.empty?
|
||||
build_stat = deploy.build_stats.where(status: true).last
|
||||
seconds = build_stat&.seconds || 0
|
||||
|
|
|
@ -24,6 +24,7 @@ class PostsController < ApplicationController
|
|||
|
||||
# Todos los artículos de este sitio para el idioma actual
|
||||
@posts = site.indexed_posts.where(locale: locale)
|
||||
@posts = @posts.page(filter_params.delete(:page)) if site.pagination
|
||||
# De este tipo
|
||||
@posts = @posts.where(layout: filter_params[:layout]) if filter_params[:layout]
|
||||
# Que estén dentro de la categoría
|
||||
|
@ -154,7 +155,7 @@ class PostsController < ApplicationController
|
|||
#
|
||||
# @return [Hash]
|
||||
def filter_params
|
||||
@filter_params ||= params.permit(:q, :category, :layout).to_hash.select do |_, v|
|
||||
@filter_params ||= params.permit(:q, :category, :layout, :page).to_hash.select do |_, v|
|
||||
v.present?
|
||||
end.transform_keys(&:to_sym)
|
||||
end
|
||||
|
|
|
@ -57,6 +57,7 @@ class SitesController < ApplicationController
|
|||
usuarie: current_usuarie)
|
||||
|
||||
if service.update.valid?
|
||||
flash[:notice] = I18n.t('sites.update.post')
|
||||
redirect_to site_posts_path(site, locale: site.default_locale)
|
||||
else
|
||||
render 'edit'
|
||||
|
|
8
app/jobs/cleanup_job.rb
Normal file
8
app/jobs/cleanup_job.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Realiza tareas de limpieza en segundo plano
|
||||
class CleanupJob < ApplicationJob
|
||||
def perform(before = nil)
|
||||
CleanupService.new(before: before).cleanup_everything!
|
||||
end
|
||||
end
|
|
@ -51,7 +51,15 @@ class DeployJob < ApplicationJob
|
|||
status = d.deploy(output: @output)
|
||||
seconds = d.build_stats.last.try(:seconds) || 0
|
||||
size = d.size
|
||||
urls = d.respond_to?(:urls) ? d.urls : [d.url].compact
|
||||
urls = d.urls.map do |url|
|
||||
URI.parse url
|
||||
rescue URI::Error
|
||||
nil
|
||||
end.compact
|
||||
|
||||
if d == @site.deployment_list.last && !status
|
||||
raise DeployException, 'Falló la compilación'
|
||||
end
|
||||
rescue StandardError => e
|
||||
status = false
|
||||
seconds ||= 0
|
||||
|
|
16
app/jobs/git_pull_job.rb
Normal file
16
app/jobs/git_pull_job.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Permite traer los cambios desde webhooks
|
||||
|
||||
class GitPullJob < ApplicationJob
|
||||
# @param :site [Site]
|
||||
# @param :usuarie [Usuarie]
|
||||
# @return [nil]
|
||||
def perform(site, usuarie)
|
||||
return unless site.repository.origin
|
||||
return unless site.repository.fetch.positive?
|
||||
|
||||
site.repository.merge(usuarie)
|
||||
site.reindex_changes!
|
||||
end
|
||||
end
|
11
app/jobs/git_push_job.rb
Normal file
11
app/jobs/git_push_job.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Permite pushear los cambios cada vez que se
|
||||
# hacen commits en un sitio
|
||||
class GitPushJob < ApplicationJob
|
||||
# @param :site [Site]
|
||||
# @return [nil]
|
||||
def perform(site)
|
||||
site.repository.push if site.repository.origin
|
||||
end
|
||||
end
|
|
@ -52,7 +52,7 @@ class DeployMailer < ApplicationMailer
|
|||
t << (row.map do |k, v|
|
||||
case k
|
||||
when :seconds then v[:human]
|
||||
when :urls then url
|
||||
when :urls then url.to_s
|
||||
else v
|
||||
end
|
||||
end)
|
||||
|
|
|
@ -23,6 +23,11 @@ class Deploy < ApplicationRecord
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# @return [Array]
|
||||
def urls
|
||||
[url].compact
|
||||
end
|
||||
|
||||
def limit
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
@ -50,6 +55,27 @@ class Deploy < ApplicationRecord
|
|||
site.path
|
||||
end
|
||||
|
||||
# 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
|
||||
#
|
||||
# @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'],
|
||||
})
|
||||
end
|
||||
|
||||
# Corre un comando, lo registra en la base de datos y devuelve el
|
||||
# estado.
|
||||
#
|
||||
|
@ -60,22 +86,20 @@ class Deploy < ApplicationRecord
|
|||
lines = []
|
||||
|
||||
time_start
|
||||
Dir.chdir(site.path) do
|
||||
Open3.popen2e(env, cmd, unsetenv_others: true) do |_, o, t|
|
||||
# TODO: Enviar a un websocket para ver el proceso en vivo?
|
||||
Thread.new do
|
||||
o.each do |line|
|
||||
lines << line
|
||||
Open3.popen2e(env, cmd, unsetenv_others: true, chdir: site.path) do |_, o, t|
|
||||
# TODO: Enviar a un websocket para ver el proceso en vivo?
|
||||
Thread.new do
|
||||
o.each do |line|
|
||||
lines << line
|
||||
|
||||
puts line if output
|
||||
end
|
||||
rescue IOError => e
|
||||
lines << e.message
|
||||
puts e.message if output
|
||||
puts line if output
|
||||
end
|
||||
|
||||
r = t.value
|
||||
rescue IOError => e
|
||||
lines << e.message
|
||||
puts e.message if output
|
||||
end
|
||||
|
||||
r = t.value
|
||||
end
|
||||
time_stop
|
||||
|
||||
|
@ -95,6 +119,11 @@ class Deploy < ApplicationRecord
|
|||
@local_env ||= {}
|
||||
end
|
||||
|
||||
# Devuelve opciones para jekyll build
|
||||
#
|
||||
# @return [String,nil]
|
||||
def flags_for_build(**args); end
|
||||
|
||||
# Trae todas las dependencias
|
||||
#
|
||||
# @return [Array]
|
||||
|
@ -104,6 +133,21 @@ class Deploy < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
# Escribe el contenido en un archivo temporal y ejecuta el bloque
|
||||
# provisto con el archivo como parámetro
|
||||
#
|
||||
# @param :content [String]
|
||||
def with_tempfile(content, &block)
|
||||
Tempfile.create(SecureRandom.hex) do |file|
|
||||
file.write content.to_s
|
||||
file.rewind
|
||||
file.close
|
||||
|
||||
# @yieldparam :file [File]
|
||||
yield file
|
||||
end
|
||||
end
|
||||
|
||||
# @param [String]
|
||||
# @return [String]
|
||||
def readable_cmd(cmd)
|
||||
|
@ -114,7 +158,14 @@ class Deploy < ApplicationRecord
|
|||
@deploy_local ||= site.deploys.find_by(type: 'DeployLocal')
|
||||
end
|
||||
|
||||
def non_local_deploys
|
||||
@non_local_deploys ||= site.deploys.where.not(type: 'DeployLocal')
|
||||
# Consigue todas las variables de entorno configuradas por otros
|
||||
# deploys.
|
||||
#
|
||||
# @return [Hash]
|
||||
def extra_env
|
||||
@extra_env ||=
|
||||
site.deployment_list.reduce({}) do |extra, deploy|
|
||||
extra.merge deploy.local_env
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'distributed_press/v1/client/site'
|
||||
require 'njalla/v1'
|
||||
|
||||
# Soportar Distributed Press APIv1
|
||||
#
|
||||
|
@ -15,8 +14,8 @@ require 'njalla/v1'
|
|||
class DeployDistributedPress < Deploy
|
||||
store :values, accessors: %i[hostname remote_site_id remote_info], 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?
|
||||
|
@ -52,7 +46,12 @@ class DeployDistributedPress < Deploy
|
|||
end
|
||||
end
|
||||
|
||||
status = c.publish(publishing_site, deploy_local.destination)
|
||||
begin
|
||||
status = c.publish(publishing_site, deploy_local.destination)
|
||||
rescue DistributedPress::V1::Error => e
|
||||
ExceptionNotifier.notify_exception(e, data: { site: site.name })
|
||||
status = false
|
||||
end
|
||||
|
||||
if status
|
||||
self.remote_info[:distributed_press] = c.show(publishing_site).to_h
|
||||
|
@ -140,29 +139,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]
|
||||
|
@ -180,31 +156,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
|
||||
|
|
|
@ -12,13 +12,6 @@ class DeployLocal < Deploy
|
|||
run %(git lfs checkout), output: output
|
||||
end
|
||||
|
||||
def bundle(output: false)
|
||||
# XXX: Desde que ya no compartimos el directorio de gemas, tenemos
|
||||
# que hacer limpieza después de instalar.
|
||||
|
||||
run %(bundle install #{'--deployment' if site.gemfile_lock_path?} --no-cache --path="#{site.bundle_path}" --clean --without test development), output: output
|
||||
end
|
||||
|
||||
# Realizamos la construcción del sitio usando Jekyll y un entorno
|
||||
# limpio para no pasarle secretos
|
||||
#
|
||||
|
@ -74,34 +67,26 @@ class DeployLocal < Deploy
|
|||
FileUtils.rm_rf(File.join(site.path, '.jekyll-cache'))
|
||||
end
|
||||
|
||||
# Opciones necesarias para la compilación del sitio
|
||||
#
|
||||
# @return [Hash]
|
||||
def local_env
|
||||
@local_env ||= {
|
||||
'SPREE_API_KEY' => site.tienda_api_key,
|
||||
'SPREE_URL' => site.tienda_url,
|
||||
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||
'YARN_CACHE_FOLDER' => yarn_cache_dir,
|
||||
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mkdir
|
||||
FileUtils.mkdir_p destination
|
||||
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(':'),
|
||||
'SPREE_API_KEY' => site.tienda_api_key,
|
||||
'SPREE_URL' => site.tienda_url,
|
||||
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
|
||||
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
|
||||
'JEKYLL_ENV' => Rails.env,
|
||||
'LANG' => ENV['LANG'],
|
||||
'YARN_CACHE_FOLDER' => yarn_cache_dir,
|
||||
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
|
||||
})
|
||||
end
|
||||
|
||||
def yarn_cache_dir
|
||||
Rails.root.join('_yarn_cache').to_s
|
||||
end
|
||||
|
@ -144,8 +129,21 @@ class DeployLocal < Deploy
|
|||
run 'yarn install --production', 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?
|
||||
run %(bundle config set --local path '#{gems_dir}'), output: output
|
||||
run %(bundle config set --local without 'test development'), output: output
|
||||
run %(bundle config set --local cache_all 'false'), output: output
|
||||
run %(bundle install), output: output
|
||||
end
|
||||
|
||||
def jekyll_build(output: false)
|
||||
run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}"), output: output
|
||||
with_tempfile(site.private_key_pem) do |file|
|
||||
flags = extra_flags(private_key: file)
|
||||
|
||||
run %(bundle exec jekyll build --trace --profile #{flags} --destination "#{escaped_destination}"), output: output
|
||||
end
|
||||
end
|
||||
|
||||
# no debería haber espacios ni caracteres especiales, pero por si
|
||||
|
@ -159,17 +157,13 @@ class DeployLocal < Deploy
|
|||
FileUtils.rm_rf destination
|
||||
end
|
||||
|
||||
# Consigue todas las variables de entorno configuradas por otros
|
||||
# deploys.
|
||||
# Genera opciones extra desde los otros deploys
|
||||
#
|
||||
# @deprecated Solo tenía sentido para Distributed Press v0
|
||||
# @return [Hash]
|
||||
def extra_env
|
||||
@extra_env ||=
|
||||
non_local_deploys.reduce({}) do |extra_env, deploy|
|
||||
extra_env.tap do |e|
|
||||
e.merge! deploy.local_env
|
||||
end
|
||||
end
|
||||
# @param :args [Hash]
|
||||
# @return [String]
|
||||
def extra_flags(**args)
|
||||
site.deployment_list.map do |deploy|
|
||||
deploy.flags_for_build(**args)
|
||||
end.compact.join(' ')
|
||||
end
|
||||
end
|
||||
|
|
55
app/models/deploy_social_distributed_press.rb
Normal file
55
app/models/deploy_social_distributed_press.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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]
|
||||
|
||||
# Envía las notificaciones
|
||||
def deploy(output: false)
|
||||
with_tempfile(site.private_key_pem) do |file|
|
||||
key = Shellwords.escape file.path
|
||||
dest = Shellwords.escape destination
|
||||
|
||||
run %(bundle exec jekyll notify --trace --key #{key} --destination "#{dest}"), output: output
|
||||
end
|
||||
end
|
||||
|
||||
# Igual que DeployLocal
|
||||
#
|
||||
# @return [String]
|
||||
def destination
|
||||
File.join(Rails.root, '_deploy', site.hostname)
|
||||
end
|
||||
|
||||
# Solo uno
|
||||
#
|
||||
# @return [Integer]
|
||||
def limit
|
||||
1
|
||||
end
|
||||
|
||||
# Espacio ocupado, pero no podemos calcularlo
|
||||
#
|
||||
# @return [Integer]
|
||||
def size
|
||||
0
|
||||
end
|
||||
|
||||
# El perfil de actor
|
||||
#
|
||||
# @return [String,nil]
|
||||
def url
|
||||
site.data.dig('activity_pub', 'actor')
|
||||
end
|
||||
|
||||
# Genera la opción de llave privada para jekyll build
|
||||
#
|
||||
# @params :args [Hash]
|
||||
# @return [String]
|
||||
def flags_for_build(**args)
|
||||
"--key #{Shellwords.escape args[:private_key].path}"
|
||||
end
|
||||
end
|
|
@ -36,6 +36,15 @@ class IndexedPost < ApplicationRecord
|
|||
|
||||
belongs_to :site
|
||||
|
||||
# Encuentra el post original
|
||||
#
|
||||
# @return [nil,Post]
|
||||
def post
|
||||
return if post_id.blank?
|
||||
|
||||
@post ||= site.posts(lang: locale).find(post_id, uuid: true)
|
||||
end
|
||||
|
||||
# Convertir locale a direccionario de PG
|
||||
#
|
||||
# @param [String,Symbol]
|
||||
|
|
|
@ -4,8 +4,12 @@
|
|||
#
|
||||
# Esto es increíblemente difícil de lograr que salga bien!
|
||||
class MetadataBoolean < MetadataTemplate
|
||||
# El valor por defecto es una versión booleana de lo que diga (o no
|
||||
# diga) el esquema
|
||||
#
|
||||
# @return [Boolean]
|
||||
def default_value
|
||||
false
|
||||
!!super
|
||||
end
|
||||
|
||||
# Los checkboxes son especiales porque la especificación de HTML
|
||||
|
|
19
app/models/metadata_created_at.rb
Normal file
19
app/models/metadata_created_at.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Fecha y hora de creación
|
||||
class MetadataCreatedAt < MetadataTemplate
|
||||
# Por defecto la hora actual, pero por retrocompatibilidad, queremos
|
||||
# la fecha de publicación
|
||||
def default_value
|
||||
if post.date.value.to_date < Time.now.to_date
|
||||
post.date.value
|
||||
else
|
||||
Time.now
|
||||
end
|
||||
end
|
||||
|
||||
# Nunca cambia
|
||||
def value=(new_value)
|
||||
value
|
||||
end
|
||||
end
|
|
@ -34,7 +34,7 @@ class MetadataRelatedPosts < MetadataArray
|
|||
end
|
||||
|
||||
def title(post)
|
||||
"#{post&.title&.value || post&.slug&.value} (#{post.layout.humanized_name})"
|
||||
"#{post&.title&.value || post&.slug&.value} #{post&.date&.value.strftime('%F')} (#{post.layout.humanized_name})"
|
||||
end
|
||||
|
||||
# Encuentra el filtro
|
||||
|
|
|
@ -202,7 +202,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
|||
|
||||
def allowed_attributes
|
||||
@allowed_attributes ||= %w[style href src alt controls data-align data-multimedia data-multimedia-inner id
|
||||
name].freeze
|
||||
name start].freeze
|
||||
end
|
||||
|
||||
def allowed_tags
|
||||
|
|
|
@ -12,7 +12,7 @@ class Post
|
|||
DEFAULT_ATTRIBUTES = %i[site document layout].freeze
|
||||
# Otros atributos que no vienen en los metadatos
|
||||
PRIVATE_ATTRIBUTES = %i[path slug attributes errors].freeze
|
||||
PUBLIC_ATTRIBUTES = %i[lang date uuid].freeze
|
||||
PUBLIC_ATTRIBUTES = %i[lang date uuid created_at].freeze
|
||||
ATTR_SUFFIXES = %w[? =].freeze
|
||||
|
||||
attr_reader :attributes, :errors, :layout, :site, :document
|
||||
|
@ -217,6 +217,11 @@ class Post
|
|||
post: self, required: true)
|
||||
end
|
||||
|
||||
# La fecha de creación inmodificable del post
|
||||
def created_at
|
||||
@metadata[:created_at] ||= MetadataCreatedAt.new(document: document, site: site, layout: layout, name: :created_at, type: :created_at, post: self, required: true)
|
||||
end
|
||||
|
||||
# Detecta si es un atributo válido o no, a partir de la tabla de la
|
||||
# plantilla
|
||||
def attribute?(mid)
|
||||
|
@ -267,6 +272,7 @@ class Post
|
|||
# Y que no se procese liquid
|
||||
yaml['liquid'] = false
|
||||
yaml['usuaries'] = usuaries.map(&:id).uniq
|
||||
yaml['created_at'] = created_at.value
|
||||
yaml['last_modified_at'] = modified_at
|
||||
|
||||
"#{yaml.to_yaml}---\n\n#{body}"
|
||||
|
|
|
@ -14,6 +14,8 @@ class Rol < ApplicationRecord
|
|||
|
||||
validates_inclusion_of :rol, in: ROLES
|
||||
|
||||
before_save :add_token_if_missing!
|
||||
|
||||
def invitade?
|
||||
rol == INVITADE
|
||||
end
|
||||
|
@ -25,4 +27,11 @@ class Rol < ApplicationRecord
|
|||
def self.role?(rol)
|
||||
ROLES.include? rol
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Asegurarse que tenga un token
|
||||
def add_token_if_missing!
|
||||
self.token ||= SecureRandom.hex(64)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ class Site < ApplicationRecord
|
|||
include Site::DeployDependencies
|
||||
include Site::BuildStats
|
||||
include Site::LayoutOrdering
|
||||
include Site::SocialDistributedPress
|
||||
include Tienda
|
||||
|
||||
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
|
||||
|
@ -18,10 +19,6 @@ class Site < ApplicationRecord
|
|||
# protege de acceso al panel de Sutty!
|
||||
encrypts :private_key
|
||||
|
||||
# TODO: Hacer que los diferentes tipos de deploy se auto registren
|
||||
# @see app/services/site_service.rb
|
||||
DEPLOYS = %i[local private www zip hidden_service distributed_press].freeze
|
||||
|
||||
validates :name, uniqueness: true, hostname: {
|
||||
allow_root_label: true
|
||||
}
|
||||
|
@ -439,6 +436,10 @@ class Site < ApplicationRecord
|
|||
find_by(name: "#{Site.domain}.")
|
||||
end
|
||||
|
||||
def self.one_at_a_time
|
||||
@@one_at_a_time ||= Thread::Mutex.new
|
||||
end
|
||||
|
||||
def reset
|
||||
@read = false
|
||||
@layouts = nil
|
||||
|
@ -474,7 +475,10 @@ class Site < ApplicationRecord
|
|||
def clone_skel!
|
||||
return if jekyll?
|
||||
|
||||
Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path
|
||||
Rugged::Repository.clone_at(ENV['SKEL_SUTTY'], path, checkout_branch: design.gem)
|
||||
|
||||
# Necesita un bloque
|
||||
repository.rugged.remotes.rename('origin', 'upstream') {}
|
||||
end
|
||||
|
||||
# Elimina el directorio del sitio
|
||||
|
@ -498,8 +502,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
|
||||
|
||||
|
@ -553,6 +557,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
|
||||
|
|
|
@ -1,22 +1,125 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Indexa todos los artículos de un sitio
|
||||
#
|
||||
# TODO: Hacer opcional
|
||||
class Site
|
||||
# Indexa todos los artículos de un sitio
|
||||
#
|
||||
# TODO: Hacer opcional
|
||||
module Index
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# TODO: Debería ser un Job?
|
||||
after_create :index_posts!
|
||||
has_many :indexed_posts, dependent: :destroy
|
||||
|
||||
MODIFIED_STATUSES = %i[added modified].freeze
|
||||
DELETED_STATUSES = %i[deleted].freeze
|
||||
LOCALE_FROM_PATH = /\A_/.freeze
|
||||
|
||||
def index_posts!
|
||||
Site.transaction do
|
||||
docs.each(&:index!)
|
||||
|
||||
update(last_indexed_commit: repository.head_commit.oid)
|
||||
end
|
||||
end
|
||||
|
||||
# Encuentra los artículos modificados entre dos commits y los
|
||||
# reindexa.
|
||||
def reindex_changes!
|
||||
return unless reindexable?
|
||||
|
||||
Site.transaction do
|
||||
remove_deleted_posts!
|
||||
reindex_modified_posts!
|
||||
|
||||
update(last_indexed_commit: repository.head_commit.oid)
|
||||
end
|
||||
end
|
||||
|
||||
# No hacer nada si el repositorio no cambió o no hubo cambios
|
||||
# necesarios
|
||||
def reindexable?
|
||||
return false if last_indexed_commit.blank?
|
||||
return false if last_indexed_commit == repository.head_commit.oid
|
||||
|
||||
!indexable_posts.empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Trae el último commit indexado desde el repositorio
|
||||
#
|
||||
# @return [Rugged::Commit]
|
||||
def indexed_commit
|
||||
@indexed_commit ||= repository.rugged.lookup(last_indexed_commit)
|
||||
end
|
||||
|
||||
# Calcula la diferencia entre el último commit indexado y el
|
||||
# actual
|
||||
#
|
||||
# XXX: Esto no tiene en cuenta modificaciones en la historia como
|
||||
# cambio de ramas, reverts y etc, solo asume que se mueve hacia
|
||||
# adelante en la misma rama o las dos ramas están relacionadas.
|
||||
#
|
||||
# @return [Rugged::Diff]
|
||||
def diff_with_head
|
||||
@diff_with_head ||= indexed_commit.diff(repository.head_commit)
|
||||
end
|
||||
|
||||
# Obtiene todos los archivos a reindexar
|
||||
#
|
||||
# @return [Array<Rugged::Delta>]
|
||||
def indexable_posts
|
||||
@indexable_posts ||=
|
||||
diff_with_head.each_delta.select do |delta|
|
||||
locales.any? do |locale|
|
||||
delta.old_file[:path].start_with? "_#{locale}/"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Elimina los artículos eliminados o que cambiaron de ubicación
|
||||
# del índice
|
||||
def remove_deleted_posts!
|
||||
indexable_posts.select do |delta|
|
||||
DELETED_STATUSES.include? delta.status
|
||||
end.each do |delta|
|
||||
locale, path = locale_and_path_from(delta.old_file[:path])
|
||||
|
||||
indexed_posts.destroy_by(locale: locale, path: path).tap do |destroyed_posts|
|
||||
next unless destroyed_posts.empty?
|
||||
|
||||
Rails.logger.info I18n.t('indexed_posts.deleted', site: name, path: path, records: destroyed_posts.count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Reindexa artículos que cambiaron de ubicación, se agregaron
|
||||
# o fueron modificados
|
||||
def reindex_modified_posts!
|
||||
indexable_posts.select do |delta|
|
||||
MODIFIED_STATUSES.include? delta.status
|
||||
end.each do |delta|
|
||||
locale, path = locale_and_path_from(delta.new_file[:path])
|
||||
|
||||
posts(lang: locale).find(path).index!
|
||||
end
|
||||
end
|
||||
|
||||
# Obtiene el idioma y la ruta del post a partir de la ubicación en
|
||||
# el disco.
|
||||
#
|
||||
# Las rutas vienen en ASCII-9BIT desde Rugged, pero en realidad
|
||||
# son UTF-8
|
||||
#
|
||||
# @return [Array<String>]
|
||||
def locale_and_path_from(path)
|
||||
locale, path = path.force_encoding('utf-8').split(File::SEPARATOR, 2)
|
||||
|
||||
[
|
||||
locale.sub(LOCALE_FROM_PATH, ''),
|
||||
File.basename(path, '.*')
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,7 +29,7 @@ class Site
|
|||
|
||||
# Obtiene el origin
|
||||
#
|
||||
# @return [Rugged::Remote]
|
||||
# @return [Rugged::Remote, nil]
|
||||
def origin
|
||||
@origin ||= rugged.remotes.find do |remote|
|
||||
remote.name == 'origin'
|
||||
|
@ -54,7 +54,7 @@ class Site
|
|||
# Incorpora los cambios en el repositorio actual
|
||||
#
|
||||
# @return [Rugged::Commit]
|
||||
def merge(usuarie)
|
||||
def merge(usuarie, message = I18n.t('sites.fetch.merge.message'))
|
||||
merge = rugged.merge_commits(head_commit, remote_head_commit)
|
||||
|
||||
# No hacemos nada si hay conflictos, pero notificarnos
|
||||
|
@ -69,12 +69,16 @@ class Site
|
|||
.create(rugged, update_ref: 'HEAD',
|
||||
parents: [head_commit, remote_head_commit],
|
||||
tree: merge.write_tree(rugged),
|
||||
message: I18n.t('sites.fetch.merge.message'),
|
||||
message: message,
|
||||
author: author(usuarie), committer: committer)
|
||||
|
||||
# Forzamos el checkout para mover el HEAD al último commit y
|
||||
# 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
|
||||
|
||||
|
@ -153,27 +157,54 @@ class Site
|
|||
#
|
||||
# @return [Boolean]
|
||||
def gc
|
||||
env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path }
|
||||
cmd = 'git gc'
|
||||
git_sh("git", "gc")
|
||||
end
|
||||
|
||||
r = nil
|
||||
Open3.popen2e(env, cmd, unsetenv_others: true, chdir: path) do |_, _, t|
|
||||
r = t.value
|
||||
end
|
||||
|
||||
r&.success?
|
||||
# Pushea cambios al repositorio remoto
|
||||
#
|
||||
# @param :remote [Rugged::Remote]
|
||||
# @return [Boolean, nil]
|
||||
def push(remote = origin)
|
||||
remote.push(rugged.head.canonical_name, credentials: credentials_for(remote))
|
||||
git_sh('git', 'lfs', 'push', remote.name, default_branch)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @deprecated
|
||||
def credentials
|
||||
@credentials ||= credentials_for(origin)
|
||||
end
|
||||
|
||||
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las
|
||||
# credenciales necesarias para trabajar con repositorios remotos.
|
||||
#
|
||||
# @param :remote [Rugged::Remote]
|
||||
# @return [Nil, Rugged::Credentials::SshKey]
|
||||
def credentials
|
||||
def credentials_for(remote)
|
||||
return unless File.exist? private_key
|
||||
|
||||
@credentials ||= Rugged::Credentials::SshKey.new username: 'git', publickey: public_key, privatekey: private_key
|
||||
Rugged::Credentials::SshKey.new username: username_for(remote), publickey: public_key, privatekey: private_key
|
||||
end
|
||||
|
||||
# Obtiene el nombre de usuario para el repositorio remoto, por
|
||||
# defecto git
|
||||
#
|
||||
# @param :remote [Rugged::Remote]
|
||||
# @return [String]
|
||||
def username_for(remote)
|
||||
username = parse_url(remote.url)&.user if remote.respond_to? :url
|
||||
|
||||
username || 'git'
|
||||
end
|
||||
|
||||
# @param :url [String]
|
||||
# @return [URI, nil]
|
||||
def parse_url(url)
|
||||
GitCloneUrl.parse(url)
|
||||
rescue URI::Error => e
|
||||
ExceptionNotifier.notify_exception(e, data: { path: path, url: url })
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [String]
|
||||
|
@ -189,5 +220,20 @@ class Site
|
|||
def relativize(file)
|
||||
Pathname.new(file).relative_path_from(Pathname.new(path)).to_s
|
||||
end
|
||||
|
||||
# Ejecuta un comando de git
|
||||
#
|
||||
# @param :args [Array]
|
||||
# @return [Boolean]
|
||||
def git_sh(*args)
|
||||
env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path }
|
||||
|
||||
r = nil
|
||||
Open3.popen2e(env, *args, unsetenv_others: true, chdir: path) do |_, _, t|
|
||||
r = t.value
|
||||
end
|
||||
|
||||
r&.success?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
23
app/models/site/social_distributed_press.rb
Normal file
23
app/models/site/social_distributed_press.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Site
|
||||
# Agrega soporte para Social Distributed Press en los sitios
|
||||
module SocialDistributedPress
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
encrypts :private_key_pem
|
||||
|
||||
before_save :generate_private_key_pem!, unless: :private_key_pem?
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,6 +10,7 @@ class Usuarie < ApplicationRecord
|
|||
|
||||
validates_uniqueness_of :email
|
||||
validates_with EmailAddress::ActiveRecordValidator, field: :email
|
||||
validate :locale_available!
|
||||
|
||||
before_create :lang_from_locale!
|
||||
before_update :remove_confirmation_invitation_inconsistencies!
|
||||
|
@ -78,4 +79,15 @@ class Usuarie < ApplicationRecord
|
|||
self.invitation_accepted_at ||= Time.now.utc
|
||||
end
|
||||
end
|
||||
|
||||
# Muestra un error si el idioma no está disponible al cambiar el
|
||||
# idioma de la cuenta.
|
||||
#
|
||||
# @return [nil]
|
||||
def locale_available!
|
||||
return if I18n.locale_available? self.lang
|
||||
|
||||
errors.add(:lang, I18n.t('activerecord.errors.models.usuarie.attributes.lang.not_available'))
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
66
app/policies/indexed_post_policy.rb
Normal file
66
app/policies/indexed_post_policy.rb
Normal file
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Política de acceso a artículos
|
||||
class IndexedPostPolicy
|
||||
attr_reader :indexed_post, :usuarie, :site
|
||||
|
||||
def initialize(usuarie, indexed_post)
|
||||
@usuarie = usuarie
|
||||
@indexed_post = indexed_post
|
||||
@site = indexed_post.site
|
||||
end
|
||||
|
||||
def index?
|
||||
true
|
||||
end
|
||||
|
||||
# Les invitades solo pueden ver sus propios posts
|
||||
def show?
|
||||
site.usuarie?(usuarie) || site.indexed_posts.by_usuarie(usuarie.id).find_by_post_id(indexed_post.post_id).present?
|
||||
end
|
||||
|
||||
def preview?
|
||||
show?
|
||||
end
|
||||
|
||||
def new?
|
||||
create?
|
||||
end
|
||||
|
||||
def create?
|
||||
true
|
||||
end
|
||||
|
||||
def edit?
|
||||
update?
|
||||
end
|
||||
|
||||
# Les invitades solo pueden modificar sus propios artículos
|
||||
def update?
|
||||
show?
|
||||
end
|
||||
|
||||
# Solo las usuarias pueden eliminar artículos. Les invitades pueden
|
||||
# borrar sus propios artículos
|
||||
def destroy?
|
||||
update?
|
||||
end
|
||||
|
||||
# Las usuarias pueden ver todos los posts
|
||||
#
|
||||
# Les invitades solo pueden ver sus propios posts
|
||||
class Scope
|
||||
attr_reader :usuarie, :scope
|
||||
|
||||
def initialize(usuarie, scope)
|
||||
@usuarie = usuarie
|
||||
@scope = scope
|
||||
end
|
||||
|
||||
def resolve
|
||||
return scope if scope&.first&.site&.usuarie? usuarie
|
||||
|
||||
scope.by_usuarie(usuarie.id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,9 +23,11 @@ class CleanupService
|
|||
#
|
||||
# @return [nil]
|
||||
def cleanup_older_sites!
|
||||
Site.where('updated_at < ?', before).find_each do |site|
|
||||
Site.where('updated_at < ?', before).order(updated_at: :desc).find_each do |site|
|
||||
next unless File.directory? site.path
|
||||
|
||||
Rails.logger.info "Limpiando dependencias, archivos temporales y repositorio git de #{site.name}"
|
||||
|
||||
site.deploys.find_each(&:cleanup!)
|
||||
|
||||
site.repository.gc
|
||||
|
@ -37,9 +39,11 @@ class CleanupService
|
|||
#
|
||||
# @return [nil]
|
||||
def cleanup_newer_sites!
|
||||
Site.where('updated_at >= ?', before).find_each do |site|
|
||||
Site.where('updated_at >= ?', before).order(updated_at: :desc).find_each do |site|
|
||||
next unless File.directory? site.path
|
||||
|
||||
Rails.logger.info "Limpiando repositorio git de #{site.name}"
|
||||
|
||||
site.repository.gc
|
||||
site.touch
|
||||
end
|
||||
|
|
|
@ -102,6 +102,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
|||
usuarie: usuarie,
|
||||
message: I18n.t("post_service.#{action}",
|
||||
title: post&.title&.value))
|
||||
|
||||
GitPushJob.perform_later(site)
|
||||
end
|
||||
|
||||
# Solo permitir cambiar estos atributos de cada articulo
|
||||
|
|
|
@ -33,6 +33,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
|||
add_licencias &&
|
||||
add_code_of_conduct &&
|
||||
add_privacy_policy &&
|
||||
site.index_posts! &&
|
||||
deploy
|
||||
end
|
||||
|
||||
|
@ -54,9 +55,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
|||
|
||||
# Genera los Deploy necesarios para el sitio a menos que ya los tenga.
|
||||
def build_deploys
|
||||
Site::DEPLOYS.map { |deploy| "Deploy#{deploy.to_s.camelcase}" }
|
||||
.each do |deploy|
|
||||
next if site.deploys.find_by type: deploy
|
||||
Deploy.subclasses.each do |deploy|
|
||||
next if site.deploys.find_by type: deploy.name
|
||||
|
||||
site.deploys.build type: deploy
|
||||
end
|
||||
|
@ -97,6 +97,8 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
|
|||
add: [site.config.path],
|
||||
message: I18n.t("site_service.#{action}",
|
||||
name: site.name))
|
||||
|
||||
GitPushJob.perform_later(site)
|
||||
end
|
||||
|
||||
def add_role(temporal: true, rol: 'invitade')
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
- row[:urls].each do |url|
|
||||
%tr
|
||||
%th{ scope: 'row' }= row[:title]
|
||||
%td= link_to_if url.present?, url, url, class: 'word-break-all'
|
||||
%td= link_to_if (url.present? && url.scheme.present?), url.to_s, url.to_s, class: 'word-break-all'
|
||||
%td
|
||||
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
|
||||
%td= row[:size]
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
%tr
|
||||
%td= row[:title]
|
||||
%td= row[:status]
|
||||
%td= link_to_if url.present?, url, url
|
||||
%td= link_to_if (url.present? && url.scheme.present?), url.to_s, url.to_s
|
||||
%td
|
||||
%time{ datetime: row[:seconds][:machine] }= row[:seconds][:human]
|
||||
%td= row[:size]
|
||||
|
|
21
app/views/deploys/_deploy_social_distributed_press.haml
Normal file
21
app/views/deploys/_deploy_social_distributed_press.haml
Normal file
|
@ -0,0 +1,21 @@
|
|||
-# Publicar a la web distribuida
|
||||
|
||||
.row
|
||||
.col
|
||||
= deploy.hidden_field :id
|
||||
= deploy.hidden_field :type
|
||||
.custom-control.custom-switch
|
||||
-#
|
||||
El checkbox invierte la lógica de destrucción porque queremos
|
||||
crear el deploy si está activado y destruirlo si está
|
||||
desactivado.
|
||||
= deploy.check_box :_destroy,
|
||||
{ checked: deploy.object.persisted?, class: 'custom-control-input' },
|
||||
'0', '1'
|
||||
= deploy.label :_destroy, class: 'custom-control-label' do
|
||||
%h3= t('.title')
|
||||
= sanitize_markdown t('.help'),
|
||||
tags: %w[p strong em a]
|
||||
|
||||
|
||||
%hr/
|
|
@ -1,4 +1,4 @@
|
|||
- flash.each do |type, message|
|
||||
- unless type == 'js'
|
||||
= render 'bootstrap/alert' do
|
||||
= message
|
||||
= sanitize_markdown message, tags: %w[a strong em]
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
%script{ type: 'text/javascript', src: '/env.js' }
|
||||
= csrf_meta_tags
|
||||
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
|
||||
= stylesheet_link_tag 'dark', rel: 'alternate stylesheet', media: 'all', 'data-turbolinks-track': 'reload', title: t('dark')
|
||||
= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
|
||||
= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload'
|
||||
= favicon_link_tag 'sutty_cuadrada.png', rel: 'apple-touch-icon', type: 'image/png'
|
||||
|
|
4
app/views/posts/attribute_ro/_created_at.haml
Normal file
4
app/views/posts/attribute_ro/_created_at.haml
Normal file
|
@ -0,0 +1,4 @@
|
|||
%tr{ id: attribute }
|
||||
%th= post_label_t(attribute, post: post)
|
||||
%td{ dir: dir, lang: locale }
|
||||
%time{ datetime: metadata.value.xmlschema }= l metadata.value
|
1
app/views/posts/attributes/_created_at.haml
Normal file
1
app/views/posts/attributes/_created_at.haml
Normal file
|
@ -0,0 +1 @@
|
|||
-# nada
|
|
@ -84,7 +84,10 @@
|
|||
%button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
|
||||
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
|
||||
|
||||
%div
|
||||
- if @site.pagination
|
||||
%div
|
||||
= link_to_prev_page @posts, t('posts.prev'), class: 'btn'
|
||||
= link_to_next_page @posts, t('posts.next'), class: 'btn'
|
||||
%tbody
|
||||
- dir = @site.data.dig(params[:locale], 'dir')
|
||||
- size = @posts.size
|
||||
|
|
|
@ -46,36 +46,37 @@
|
|||
.invalid-feedback= site.errors.messages[:description].join(', ')
|
||||
%hr/
|
||||
|
||||
.form-group#design_id
|
||||
%h2= t('.design.title')
|
||||
%p.lead= t('.help.design')
|
||||
- if invalid? site, :design_id
|
||||
= render 'bootstrap/alert' do
|
||||
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
|
||||
layouts: site.incompatible_layouts.to_sentence)
|
||||
.row.row-cols-1.row-cols-md-2.designs
|
||||
-# Demasiado complejo para un f.collection_radio_buttons
|
||||
- Design.all.order(priority: :desc).each do |design|
|
||||
.design.col.d-flex.flex-column
|
||||
.custom-control.custom-radio
|
||||
= f.radio_button :design_id, design.id,
|
||||
checked: design.id == site.design_id,
|
||||
disabled: design.disabled,
|
||||
required: true, class: 'custom-control-input'
|
||||
= f.label "design_id_#{design.id}", design.name,
|
||||
class: 'custom-control-label'
|
||||
.flex-fill
|
||||
= sanitize_markdown design.description,
|
||||
tags: %w[p a strong em]
|
||||
- unless site.persisted?
|
||||
.form-group#design_id
|
||||
%h2= t('.design.title')
|
||||
%p.lead= t('.help.design')
|
||||
- if invalid? site, :design_id
|
||||
= render 'bootstrap/alert' do
|
||||
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
|
||||
layouts: site.incompatible_layouts.to_sentence)
|
||||
.row.row-cols-1.row-cols-md-2.designs
|
||||
-# Demasiado complejo para un f.collection_radio_buttons
|
||||
- Design.all.order(priority: :desc).each do |design|
|
||||
.design.col.d-flex.flex-column
|
||||
.custom-control.custom-radio
|
||||
= f.radio_button :design_id, design.id,
|
||||
checked: design.id == site.design_id,
|
||||
disabled: design.disabled,
|
||||
required: true, class: 'custom-control-input'
|
||||
= f.label "design_id_#{design.id}", design.name,
|
||||
class: 'custom-control-label'
|
||||
.flex-fill
|
||||
= sanitize_markdown design.description,
|
||||
tags: %w[p a strong em]
|
||||
|
||||
.btn-group{ role: 'group', 'aria-label': t('.design.actions') }
|
||||
- if design.url
|
||||
= link_to t('.design.url'), design.url,
|
||||
target: '_blank', class: 'btn'
|
||||
- if design.license
|
||||
= link_to t('.design.license'), design.license,
|
||||
target: '_blank', class: 'btn'
|
||||
%hr/
|
||||
.btn-group{ role: 'group', 'aria-label': t('.design.actions') }
|
||||
- if design.url
|
||||
= link_to t('.design.url'), design.url,
|
||||
target: '_blank', class: 'btn'
|
||||
- if design.license
|
||||
= link_to t('.design.license'), design.license,
|
||||
target: '_blank', class: 'btn'
|
||||
%hr/
|
||||
|
||||
.form-group.licenses#license_id
|
||||
%h2= t('.licencia.title')
|
||||
|
|
|
@ -54,10 +54,4 @@
|
|||
text: t('usuaries.index.title'),
|
||||
type: 'info',
|
||||
link: site_usuaries_path(site)
|
||||
- if policy(site).pull? && site.repository.needs_pull?
|
||||
= render 'layouts/btn_with_tooltip',
|
||||
tooltip: t('help.sites.pull'),
|
||||
text: t('.pull'),
|
||||
type: 'info',
|
||||
link: site_pull_path(site)
|
||||
= render 'sites/build', site: site
|
||||
|
|
|
@ -45,6 +45,8 @@ module Sutty
|
|||
config.active_storage.queues.purge = :default
|
||||
config.active_job.queue_adapter = :que
|
||||
|
||||
config.active_record.schema_format = :sql
|
||||
|
||||
config.to_prepare do
|
||||
# Load application's model / class decorators
|
||||
Dir.glob(File.join(File.dirname(__FILE__), '..', 'app', '**', '*_decorator.rb')).sort.each do |c|
|
||||
|
|
|
@ -142,7 +142,7 @@ Rails.application.configure do
|
|||
}
|
||||
config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', "noreply@sutty.nl") }
|
||||
|
||||
config.middleware.use ExceptionNotification::Rack, gitlab: {}, ignore_exceptions: (['DeployJob::DeployAlreadyRunningException'] + ExceptionNotifier.ignored_exceptions)
|
||||
config.middleware.use ExceptionNotification::Rack, gitlab: {}, ignore_exceptions: ['DeployJob::DeployAlreadyRunningException']
|
||||
|
||||
Rails.application.routes.default_url_options[:host] = "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}"
|
||||
Rails.application.routes.default_url_options[:protocol] = 'https'
|
||||
|
|
|
@ -123,6 +123,15 @@ module Jekyll
|
|||
spec.name == name
|
||||
end
|
||||
|
||||
unless spec
|
||||
I18n.with_locale(locale) do
|
||||
raise Jekyll::Errors::InvalidThemeName, I18n.t('activerecord.errors.models.site.attributes.design_id.missing_gem', theme: name)
|
||||
rescue Jekyll::Errors::InvalidThemeName => e
|
||||
ExceptionNotifier.notify_exception(e, data: { theme: name, site: File.basename(site.source) })
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
ruby_version = Gem::Version.new(RUBY_VERSION)
|
||||
ruby_version.canonical_segments[2] = 0
|
||||
base_path = Rails.root.join('_storage', 'gems', File.basename(site.source), 'ruby',
|
||||
|
@ -146,6 +155,11 @@ module Jekyll
|
|||
private
|
||||
|
||||
def gemspec; end
|
||||
|
||||
# @return [Symbol]
|
||||
def locale
|
||||
@locale ||= (site.config['locale'] || site.config['lang'] || I18n.locale).to_sym
|
||||
end
|
||||
end
|
||||
|
||||
# No necesitamos los archivos de la plantilla
|
||||
|
|
3
config/initializers/device_detector.rb
Normal file
3
config/initializers/device_detector.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
DeviceDetector.configure do |config|
|
||||
config.max_cache_keys = 5_000 # to check if not too much
|
||||
end
|
|
@ -1,4 +1,5 @@
|
|||
en:
|
||||
dark: Dark
|
||||
dir: ltr
|
||||
en: English
|
||||
es: Castellano
|
||||
|
@ -123,6 +124,10 @@ en:
|
|||
title: Distributed Web
|
||||
success: Success!
|
||||
error: Error
|
||||
deploy_social_distributed_press:
|
||||
title: Fediverse
|
||||
success: Success!
|
||||
error: Error
|
||||
deploy_reindex:
|
||||
title: Reindex
|
||||
success: Success!
|
||||
|
@ -167,6 +172,7 @@ en:
|
|||
usuarie: User
|
||||
licencia: License
|
||||
design: Design
|
||||
indexed_post: Indexed post
|
||||
attributes:
|
||||
usuarie:
|
||||
email: 'E-mail address'
|
||||
|
@ -191,9 +197,14 @@ en:
|
|||
deploys:
|
||||
deploy_local_presence: 'We need to be build the site!'
|
||||
design_id:
|
||||
missing_gem: "Site is configured to use %{theme} theme, but the corresponding gem is missing from Gemfile"
|
||||
layout_incompatible:
|
||||
error: "Design can't be changed because there are posts with incompatible layouts"
|
||||
help: "Your site has posts with layouts only compatible with the current design. If you change it, the site won't work as you expect. If you're trying out designs, you can delete posts in the following incompatible layouts:: %{layouts}."
|
||||
usuarie:
|
||||
attributes:
|
||||
lang:
|
||||
not_available: "This language is not yet available, would you help us by translating Sutty into it?"
|
||||
errors:
|
||||
argument_error: 'Argument `%{argument}` must be an instance of %{class}'
|
||||
unknown_locale: 'Unknown %{locale} locale'
|
||||
|
@ -307,6 +318,14 @@ en:
|
|||
indefinitely.
|
||||
|
||||
[Learn more](https://sutty.nl/learn-more-about-publish-to-dweb-functionality/)
|
||||
deploy_social_distributed_press:
|
||||
title: 'Publish on the Fediverse'
|
||||
help: |
|
||||
By using the ActivityPub protocol, people on the Fediverse
|
||||
([Mastodon](https://joinmastodon.org/servers),
|
||||
[Pixelfed](https://pixelfed.social/site/about), and
|
||||
[others](https://fediverse.party/)) can follow your site,
|
||||
receive news and interact with them.
|
||||
stats:
|
||||
index:
|
||||
title: Statistics
|
||||
|
@ -407,6 +426,8 @@ en:
|
|||
title: 'Edit %{site}'
|
||||
submit: 'Save changes'
|
||||
btn: 'Configuration'
|
||||
update:
|
||||
post: "Your changes have been saved. **If you enabled a publication method, don't forget to publish changes.**"
|
||||
form:
|
||||
errors:
|
||||
title: There were errors and we couldn't save your changes :(
|
||||
|
@ -415,7 +436,7 @@ en:
|
|||
name: "This will be the host name for your site, ie. **example**.sutty.nl. Choose an expression up to 63 characters. It can contain only lowercase letters, numbers and dashes, **and no spaces**. It can't start or end with a dash, or be entirely composed of numbers."
|
||||
title: 'The title can be anything you want'
|
||||
description: 'You site description that appears in search engines. Between 50 and 160 characters.'
|
||||
design: 'Select the design for your site. You can change it later. We add more designs from time to time!'
|
||||
design: 'Select the design for your site. We add more designs from time to time!'
|
||||
licencia: 'Everything we publish has automatic copyright. This
|
||||
means nobody can use our works without explicit permission. By
|
||||
using licenses, we stablish conditions by which we want to share
|
||||
|
@ -466,6 +487,9 @@ en:
|
|||
success: 'Site upgrade has been completed. Your next build will run this upgrade :)'
|
||||
error: "There was an error when trying to upgrade your site. This could be due to conflicts that couldn't be solved automatically. A report of the issue has already been sent to our admins. Sorry for the inconvenience! :("
|
||||
message: 'Skeleton upgrade'
|
||||
webhooks:
|
||||
pull:
|
||||
message: 'Webhooks pull'
|
||||
footer:
|
||||
powered_by: 'is developed by'
|
||||
i18n:
|
||||
|
@ -512,6 +536,8 @@ en:
|
|||
feedback: 'This field cannot be empty!'
|
||||
uuid:
|
||||
label: 'Unique identifier'
|
||||
created_at:
|
||||
label: 'Created at'
|
||||
geo:
|
||||
uri: 'Open in app'
|
||||
osm: 'Open in web map'
|
||||
|
@ -685,7 +711,7 @@ en:
|
|||
new: 'Create'
|
||||
edit: 'Configure'
|
||||
posts:
|
||||
new: 'New %{layout}'
|
||||
new: 'Add %{layout}'
|
||||
edit: 'Editing'
|
||||
usuaries:
|
||||
index: 'Users'
|
||||
|
@ -709,3 +735,5 @@ en:
|
|||
build_stats:
|
||||
index:
|
||||
title: "Publications"
|
||||
indexed_posts:
|
||||
deleted: "Deleted indexed post %{path} from %{site} (records: %{records})"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
es:
|
||||
dark: Oscuro
|
||||
es: Castellano
|
||||
en: English
|
||||
es-AR: Castellano Rioplatense
|
||||
|
@ -123,6 +124,10 @@ es:
|
|||
title: Web distribuida
|
||||
success: ¡Éxito!
|
||||
error: Hubo un error
|
||||
deploy_social_distributed_press:
|
||||
title: Fediverso
|
||||
success: ¡Éxito!
|
||||
error: Hubo un error
|
||||
deploy_reindex:
|
||||
title: Reindexación
|
||||
success: ¡Éxito!
|
||||
|
@ -167,6 +172,7 @@ es:
|
|||
usuarie: Usuarie
|
||||
licencia: Licencia
|
||||
design: Diseño
|
||||
indexed_post: Artículo indexado
|
||||
attributes:
|
||||
usuarie:
|
||||
email: 'Correo electrónico'
|
||||
|
@ -191,9 +197,14 @@ es:
|
|||
deploys:
|
||||
deploy_local_presence: '¡Necesitamos poder generar el sitio!'
|
||||
design_id:
|
||||
missing_gem: "El sitio usa la plantilla %{theme} pero la gema correspondiente no se encuentra en el Gemfile"
|
||||
layout_incompatible:
|
||||
error: 'No se puede cambiar la plantilla porque hay artículos con formatos incompatibles'
|
||||
help: 'En tu sitio hay artículos que solo son compatibles con el diseño actual, si cambias la plantilla el sitio no funcionará como esperas. Si estás probando plantillas, puedes eliminar los artículos en los formatos incompatibles: %{layouts}.'
|
||||
usuarie:
|
||||
attributes:
|
||||
lang:
|
||||
not_available: "Este idioma todavía no está disponible, ¿nos ayudas a agregarlo y mantenerlo?"
|
||||
errors:
|
||||
argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}'
|
||||
unknown_locale: 'El idioma %{locale} es desconocido'
|
||||
|
@ -312,6 +323,14 @@ es:
|
|||
copias de tu contenido indefinidamente.
|
||||
|
||||
[Saber más](https://sutty.nl/saber-mas-sobre-publicar-a-la-web-distribuida/)
|
||||
deploy_social_distributed_press:
|
||||
title: 'Publicar al Fediverso'
|
||||
help: |
|
||||
Utilizando el protocolo ActivityPub, otras personas en el
|
||||
Fediverso ([Mastodon](https://joinmastodon.org/servers),
|
||||
[Pixelfed](https://pixelfed.social/site/about) y
|
||||
[otros](https://fediverse.party/)) pueden seguir a tu sitio,
|
||||
recibir novedades e interactuar con ellas.
|
||||
stats:
|
||||
index:
|
||||
title: Estadísticas
|
||||
|
@ -413,6 +432,8 @@ es:
|
|||
title: 'Editar %{site}'
|
||||
submit: 'Guardar cambios'
|
||||
btn: 'Configuración'
|
||||
update:
|
||||
post: "Tus cambios han sido guardados. **Si agregaste un método de publicación, no te olvides de publicar cambios.**"
|
||||
form:
|
||||
errors:
|
||||
title: Hubo errores y no pudimos guardar tus cambios :(
|
||||
|
@ -421,7 +442,7 @@ es:
|
|||
name: 'El nombre de tu sitio que formará parte de la dirección (**ejemplo**.sutty.nl). Solo puede contener hasta 63 letras minúsculas, números y guiones, pero **sin espacios**. No puede empezar ni terminar con guión, ni estar compuesto enteramente por números.'
|
||||
title: 'El título de tu sitio puede ser lo que quieras.'
|
||||
description: 'La descripción del sitio, que saldrá en buscadores. Entre 50 y 160 caracteres.'
|
||||
design: 'Elegí el diseño que va a tener tu sitio aquí. Podés cambiarlo luego. De tanto en tanto vamos sumando diseños nuevos.'
|
||||
design: 'Elegí el diseño que va a tener tu sitio aquí. De tanto en tanto vamos sumando diseños nuevos.'
|
||||
licencia: 'Todo lo que publicamos posee automáticamente derechos
|
||||
de autore. Esto significa que nadie puede hacer uso de nuestras
|
||||
obras sin permiso explícito. Con las licencias establecemos
|
||||
|
@ -474,6 +495,9 @@ es:
|
|||
success: 'Ya se incorporaron los cambios en el sitio, se aplicarán en la próxima compilación que hagas :)'
|
||||
error: 'Hubo un error al incorporar los cambios en el sitio. Esto puede deberse a conflictos entre cambios que no se pueden resolver automáticamente. Hemos enviado un reporte del problema a les administradores de Sutty para que estén al tanto de la situación. ¡Lo sentimos! :('
|
||||
message: 'Actualización del esqueleto'
|
||||
webhooks:
|
||||
pull:
|
||||
message: 'Traer los cambios a partir de un evento remoto'
|
||||
footer:
|
||||
powered_by: 'es desarrollada por'
|
||||
i18n:
|
||||
|
@ -520,6 +544,8 @@ es:
|
|||
feedback: '¡Este campo no puede estar vacío!'
|
||||
uuid:
|
||||
label: 'Identificador único'
|
||||
created_at:
|
||||
label: 'Fecha de creación'
|
||||
geo:
|
||||
uri: 'Abrir en aplicación'
|
||||
osm: 'Abrir en mapa web'
|
||||
|
@ -693,7 +719,7 @@ es:
|
|||
new: 'Crear'
|
||||
edit: 'Configurar'
|
||||
posts:
|
||||
new: 'Nuevo %{layout}'
|
||||
new: 'Agregar %{layout}'
|
||||
edit: 'Editando'
|
||||
usuaries:
|
||||
index: 'Usuaries'
|
||||
|
@ -717,3 +743,5 @@ es:
|
|||
build_stats:
|
||||
index:
|
||||
title: "Publicaciones"
|
||||
indexed_posts:
|
||||
deleted: "Eliminado artículo %{path} de %{site} (filas: %{records})"
|
||||
|
|
|
@ -17,6 +17,8 @@ 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'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
8
db/migrate/20230519143500_add_pagination_to_site.rb
Normal file
8
db/migrate/20230519143500_add_pagination_to_site.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Agrega la opción de paginación a los sitios
|
||||
class AddPaginationToSite < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :sites, :pagination, :boolean, default: false
|
||||
end
|
||||
end
|
12
db/migrate/20230731195050_add_token_to_roles.rb
Normal file
12
db/migrate/20230731195050_add_token_to_roles.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class AddTokenToRoles < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
add_column :roles, :token, :string
|
||||
Rol.find_each do |m|
|
||||
m.update_column( :token, SecureRandom.hex(64) )
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :roles, :token
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Almacena las llaves privadas de cada sitio
|
||||
class AddPrivateKeyPemCiphertextToSites < ActiveRecord::Migration[6.1]
|
||||
# Agrega la columna cifrada
|
||||
def change
|
||||
add_column :sites, :private_key_pem_ciphertext, :text
|
||||
end
|
||||
end
|
20
db/migrate/20230921155401_site_rename_origin_to_upstream.rb
Normal file
20
db/migrate/20230921155401_site_rename_origin_to_upstream.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Renombrar todos los repositorios que apunten a skel como su origin
|
||||
class SiteRenameOriginToUpstream < ActiveRecord::Migration[6.1]
|
||||
# Renombrar
|
||||
def up
|
||||
Site.find_each do |site|
|
||||
next unless site.repository.origin&.url == ENV['SKEL_SUTTY']
|
||||
|
||||
site.repository.rugged.remotes.rename('origin', 'upstream') do |_|
|
||||
Rails.logger.info "#{site.name}: renamed origin to upstream"
|
||||
end
|
||||
rescue Rugged::Error, Rugged::OSError => e
|
||||
Rails.logger.warn "#{site.name}: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
# No se puede deshacer
|
||||
def down; end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Almacenar el último commit indexado
|
||||
class AddLastIndexedCommitToSites < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
add_column :sites, :last_indexed_commit, :string, null: true
|
||||
|
||||
Site.find_each do |site|
|
||||
site.update_columns(last_indexed_commit: site.repository.head_commit.oid)
|
||||
rescue Rugged::Error, Rugged::OSError => e
|
||||
puts "Falló #{site.name}, ignorando: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :sites, :last_indexed_commit
|
||||
end
|
||||
end
|
400
db/schema.rb
400
db/schema.rb
|
@ -1,400 +0,0 @@
|
|||
# This file is auto-generated from the current state of the database. Instead
|
||||
# of editing this file, please use the migrations feature of Active Record to
|
||||
# incrementally modify your database, and then regenerate this schema definition.
|
||||
#
|
||||
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||
# be faster and is potentially less error prone than running all of your
|
||||
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||
# migrations use external dependencies or application code.
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2023_04_15_153231) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_trgm"
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
|
||||
create_table "access_logs", id: :uuid, default: nil, force: :cascade do |t|
|
||||
t.string "host"
|
||||
t.float "msec"
|
||||
t.string "server_protocol"
|
||||
t.string "request_method"
|
||||
t.string "request_completion"
|
||||
t.string "uri"
|
||||
t.string "query_string"
|
||||
t.integer "status"
|
||||
t.string "sent_http_content_type"
|
||||
t.string "sent_http_content_encoding"
|
||||
t.string "sent_http_etag"
|
||||
t.string "sent_http_last_modified"
|
||||
t.string "http_accept"
|
||||
t.string "http_accept_encoding"
|
||||
t.string "http_accept_language"
|
||||
t.string "http_pragma"
|
||||
t.string "http_cache_control"
|
||||
t.string "http_if_none_match"
|
||||
t.string "http_dnt"
|
||||
t.string "http_user_agent"
|
||||
t.string "http_origin"
|
||||
t.float "request_time"
|
||||
t.integer "bytes_sent"
|
||||
t.integer "body_bytes_sent"
|
||||
t.integer "request_length"
|
||||
t.string "http_connection"
|
||||
t.string "pipe"
|
||||
t.integer "connection_requests"
|
||||
t.string "geoip2_data_country_name"
|
||||
t.string "geoip2_data_city_name"
|
||||
t.string "ssl_server_name"
|
||||
t.string "ssl_protocol"
|
||||
t.string "ssl_early_data"
|
||||
t.string "ssl_session_reused"
|
||||
t.string "ssl_curves"
|
||||
t.string "ssl_ciphers"
|
||||
t.string "ssl_cipher"
|
||||
t.string "sent_http_x_xss_protection"
|
||||
t.string "sent_http_x_frame_options"
|
||||
t.string "sent_http_x_content_type_options"
|
||||
t.string "sent_http_strict_transport_security"
|
||||
t.string "nginx_version"
|
||||
t.integer "pid"
|
||||
t.string "remote_user"
|
||||
t.boolean "crawler", default: false
|
||||
t.string "http_referer"
|
||||
t.datetime "created_at", precision: 6
|
||||
t.index ["geoip2_data_city_name"], name: "index_access_logs_on_geoip2_data_city_name"
|
||||
t.index ["geoip2_data_country_name"], name: "index_access_logs_on_geoip2_data_country_name"
|
||||
t.index ["host"], name: "index_access_logs_on_host"
|
||||
t.index ["http_origin"], name: "index_access_logs_on_http_origin"
|
||||
t.index ["http_user_agent"], name: "index_access_logs_on_http_user_agent"
|
||||
t.index ["status"], name: "index_access_logs_on_status"
|
||||
t.index ["uri"], name: "index_access_logs_on_uri"
|
||||
end
|
||||
|
||||
create_table "action_text_rich_texts", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.text "body"
|
||||
t.string "record_type", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
|
||||
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_blobs", force: :cascade do |t|
|
||||
t.string "key", null: false
|
||||
t.string "filename", null: false
|
||||
t.string "content_type"
|
||||
t.text "metadata"
|
||||
t.bigint "byte_size", null: false
|
||||
t.string "checksum", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.string "service_name", null: false
|
||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.string "variation_digest", null: false
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "blazer_audits", force: :cascade do |t|
|
||||
t.bigint "user_id"
|
||||
t.bigint "query_id"
|
||||
t.text "statement"
|
||||
t.string "data_source"
|
||||
t.datetime "created_at"
|
||||
t.index ["query_id"], name: "index_blazer_audits_on_query_id"
|
||||
t.index ["user_id"], name: "index_blazer_audits_on_user_id"
|
||||
end
|
||||
|
||||
create_table "blazer_checks", force: :cascade do |t|
|
||||
t.bigint "creator_id"
|
||||
t.bigint "query_id"
|
||||
t.string "state"
|
||||
t.string "schedule"
|
||||
t.text "emails"
|
||||
t.text "slack_channels"
|
||||
t.string "check_type"
|
||||
t.text "message"
|
||||
t.datetime "last_run_at"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["creator_id"], name: "index_blazer_checks_on_creator_id"
|
||||
t.index ["query_id"], name: "index_blazer_checks_on_query_id"
|
||||
end
|
||||
|
||||
create_table "blazer_dashboard_queries", force: :cascade do |t|
|
||||
t.bigint "dashboard_id"
|
||||
t.bigint "query_id"
|
||||
t.integer "position"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["dashboard_id"], name: "index_blazer_dashboard_queries_on_dashboard_id"
|
||||
t.index ["query_id"], name: "index_blazer_dashboard_queries_on_query_id"
|
||||
end
|
||||
|
||||
create_table "blazer_dashboards", force: :cascade do |t|
|
||||
t.bigint "creator_id"
|
||||
t.text "name"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["creator_id"], name: "index_blazer_dashboards_on_creator_id"
|
||||
end
|
||||
|
||||
create_table "blazer_queries", force: :cascade do |t|
|
||||
t.bigint "creator_id"
|
||||
t.string "name"
|
||||
t.text "description"
|
||||
t.text "statement"
|
||||
t.string "data_source"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["creator_id"], name: "index_blazer_queries_on_creator_id"
|
||||
end
|
||||
|
||||
create_table "build_stats", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "deploy_id"
|
||||
t.bigint "bytes"
|
||||
t.float "seconds"
|
||||
t.string "action", null: false
|
||||
t.text "log"
|
||||
t.boolean "status", default: false
|
||||
t.index ["deploy_id"], name: "index_build_stats_on_deploy_id"
|
||||
end
|
||||
|
||||
create_table "csp_reports", id: :uuid, default: nil, force: :cascade do |t|
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.string "disposition"
|
||||
t.string "referrer"
|
||||
t.string "blocked_uri"
|
||||
t.string "document_uri"
|
||||
t.string "effective_directive"
|
||||
t.string "original_policy"
|
||||
t.string "script_sample"
|
||||
t.string "status_code"
|
||||
t.string "violated_directive"
|
||||
t.integer "column_number"
|
||||
t.integer "line_number"
|
||||
t.string "source_file"
|
||||
end
|
||||
|
||||
create_table "deploys", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "site_id"
|
||||
t.string "type"
|
||||
t.text "values"
|
||||
t.index ["site_id"], name: "index_deploys_on_site_id"
|
||||
t.index ["type"], name: "index_deploys_on_type"
|
||||
end
|
||||
|
||||
create_table "designs", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "name"
|
||||
t.text "description"
|
||||
t.string "gem"
|
||||
t.string "url"
|
||||
t.string "license"
|
||||
t.boolean "disabled", default: false
|
||||
t.text "credits"
|
||||
t.string "designer_url"
|
||||
t.integer "priority"
|
||||
end
|
||||
|
||||
create_table "indexed_posts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.bigint "site_id"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.string "locale", default: "simple"
|
||||
t.string "layout", null: false
|
||||
t.string "path", null: false
|
||||
t.string "title", default: ""
|
||||
t.jsonb "front_matter", default: "{}"
|
||||
t.string "content", default: ""
|
||||
t.tsvector "indexed_content"
|
||||
t.integer "order", default: 0
|
||||
t.string "dictionary"
|
||||
t.index ["front_matter"], name: "index_indexed_posts_on_front_matter", using: :gin
|
||||
t.index ["indexed_content"], name: "index_indexed_posts_on_indexed_content", using: :gin
|
||||
t.index ["layout"], name: "index_indexed_posts_on_layout"
|
||||
t.index ["locale"], name: "index_indexed_posts_on_locale"
|
||||
t.index ["site_id"], name: "index_indexed_posts_on_site_id"
|
||||
end
|
||||
|
||||
create_table "licencias", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "name"
|
||||
t.text "description"
|
||||
t.text "deed"
|
||||
t.string "url"
|
||||
t.string "icons"
|
||||
end
|
||||
|
||||
create_table "log_entries", force: :cascade do |t|
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.bigint "site_id"
|
||||
t.text "text"
|
||||
t.boolean "sent", default: false
|
||||
t.index ["site_id"], name: "index_log_entries_on_site_id"
|
||||
end
|
||||
|
||||
create_table "maintenances", force: :cascade do |t|
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.text "message"
|
||||
t.datetime "estimated_from"
|
||||
t.datetime "estimated_to"
|
||||
t.boolean "are_we_back", default: false
|
||||
end
|
||||
|
||||
create_table "mobility_string_translations", force: :cascade do |t|
|
||||
t.string "locale", null: false
|
||||
t.string "key", null: false
|
||||
t.string "value"
|
||||
t.string "translatable_type"
|
||||
t.bigint "translatable_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_string_translations_on_translatable_attribute"
|
||||
t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_string_translations_on_keys", unique: true
|
||||
t.index ["translatable_type", "key", "value", "locale"], name: "index_mobility_string_translations_on_query_keys"
|
||||
end
|
||||
|
||||
create_table "mobility_text_translations", force: :cascade do |t|
|
||||
t.string "locale", null: false
|
||||
t.string "key", null: false
|
||||
t.text "value"
|
||||
t.string "translatable_type"
|
||||
t.bigint "translatable_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_text_translations_on_translatable_attribute"
|
||||
t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_text_translations_on_keys", unique: true
|
||||
end
|
||||
|
||||
create_table "roles", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "site_id"
|
||||
t.bigint "usuarie_id"
|
||||
t.string "rol"
|
||||
t.boolean "temporal"
|
||||
t.index ["site_id", "usuarie_id"], name: "index_roles_on_site_id_and_usuarie_id", unique: true
|
||||
t.index ["site_id"], name: "index_roles_on_site_id"
|
||||
t.index ["usuarie_id"], name: "index_roles_on_usuarie_id"
|
||||
end
|
||||
|
||||
create_table "rollups", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "interval", null: false
|
||||
t.datetime "time", null: false
|
||||
t.jsonb "dimensions", default: {}, null: false
|
||||
t.float "value"
|
||||
t.index ["name", "interval", "time", "dimensions"], name: "index_rollups_on_name_and_interval_and_time_and_dimensions", unique: true
|
||||
end
|
||||
|
||||
create_table "sites", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "name"
|
||||
t.bigint "design_id"
|
||||
t.bigint "licencia_id"
|
||||
t.string "status", default: "waiting"
|
||||
t.text "description"
|
||||
t.string "title"
|
||||
t.boolean "colaboracion_anonima", default: false
|
||||
t.boolean "contact", default: false
|
||||
t.string "private_key_ciphertext"
|
||||
t.boolean "acepta_invitades", default: false
|
||||
t.string "tienda_api_key_ciphertext", default: ""
|
||||
t.string "tienda_url", default: ""
|
||||
t.string "api_key_ciphertext"
|
||||
t.index ["design_id"], name: "index_sites_on_design_id"
|
||||
t.index ["licencia_id"], name: "index_sites_on_licencia_id"
|
||||
t.index ["name"], name: "index_sites_on_name", unique: true
|
||||
end
|
||||
|
||||
create_table "stats", force: :cascade do |t|
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.bigint "site_id"
|
||||
t.string "name", null: false
|
||||
t.index ["name"], name: "index_stats_on_name", using: :hash
|
||||
t.index ["site_id"], name: "index_stats_on_site_id"
|
||||
end
|
||||
|
||||
create_table "usuaries", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
t.string "reset_password_token"
|
||||
t.datetime "reset_password_sent_at"
|
||||
t.datetime "remember_created_at"
|
||||
t.string "confirmation_token"
|
||||
t.datetime "confirmed_at"
|
||||
t.datetime "confirmation_sent_at"
|
||||
t.string "unconfirmed_email"
|
||||
t.integer "failed_attempts", default: 0, null: false
|
||||
t.string "unlock_token"
|
||||
t.datetime "locked_at"
|
||||
t.boolean "acepta_politicas_de_privacidad", default: false
|
||||
t.string "invitation_token"
|
||||
t.datetime "invitation_created_at"
|
||||
t.datetime "invitation_sent_at"
|
||||
t.datetime "invitation_accepted_at"
|
||||
t.integer "invitation_limit"
|
||||
t.string "invited_by_type"
|
||||
t.bigint "invited_by_id"
|
||||
t.integer "invitations_count", default: 0
|
||||
t.string "lang", default: "es"
|
||||
t.index ["confirmation_token"], name: "index_usuaries_on_confirmation_token", unique: true
|
||||
t.index ["email"], name: "index_usuaries_on_email", unique: true
|
||||
t.index ["invitation_token"], name: "index_usuaries_on_invitation_token", unique: true
|
||||
t.index ["invitations_count"], name: "index_usuaries_on_invitations_count"
|
||||
t.index ["invited_by_id"], name: "index_usuaries_on_invited_by_id"
|
||||
t.index ["invited_by_type", "invited_by_id"], name: "index_usuaries_on_invited_by"
|
||||
t.index ["reset_password_token"], name: "index_usuaries_on_reset_password_token", unique: true
|
||||
t.index ["unlock_token"], name: "index_usuaries_on_unlock_token", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
|
||||
create_trigger("indexed_posts_before_insert_update_row_tr", :compatibility => 1).
|
||||
on("indexed_posts").
|
||||
before(:insert, :update) do
|
||||
<<-SQL_ACTIONS
|
||||
new.indexed_content := to_tsvector(('pg_catalog.' || new.dictionary)::regconfig, coalesce(new.title, '') || '
|
||||
' || coalesce(new.content,''));
|
||||
SQL_ACTIONS
|
||||
end
|
||||
|
||||
create_trigger("access_logs_before_insert_row_tr", :compatibility => 1).
|
||||
on("access_logs").
|
||||
before(:insert) do
|
||||
"new.created_at := to_timestamp(new.msec);"
|
||||
end
|
||||
|
||||
end
|
|
@ -18,7 +18,7 @@
|
|||
- name_en: 'Minima'
|
||||
name_es: 'Mínima'
|
||||
gem: 'sutty-minima'
|
||||
url: 'https://0xacab.org/sutty/jekyll/minima'
|
||||
url: 'https://minima.sutty.nl/'
|
||||
description_en: "Sutty Minima is based on [Minima](https://jekyll.github.io/minima/), a blog-focused theme for Jekyll."
|
||||
description_es: 'Sutty Mínima es una plantilla para blogs basada en [Mínima](https://jekyll.github.io/minima/).'
|
||||
license: 'https://0xacab.org/sutty/jekyll/minima/-/blob/master/LICENSE.txt'
|
||||
|
|
2323
db/structure.sql
Normal file
2323
db/structure.sql
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2,10 +2,9 @@
|
|||
|
||||
namespace :cleanup do
|
||||
desc 'Cleanup sites'
|
||||
task everything: :environment do
|
||||
task everything: :environment do |_, args|
|
||||
before = ENV.fetch('BEFORE', '30').to_i.days.ago
|
||||
service = CleanupService.new(before: before)
|
||||
|
||||
service.cleanup_everything!
|
||||
CleanupJob.perform_later(before)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,10 @@ check program stats
|
|||
every "0 1 * * *"
|
||||
if status != 0 then alert
|
||||
|
||||
check filesystem root with path /
|
||||
if space usage > 80% for 5 times within 15 cycles then alert
|
||||
if space usage > 90% for 5 cycles then exec "/usr/bin/foreman run -f /srv/Procfile -d /srv emergency_cleanup" as uid "rails" gid "www-data"
|
||||
|
||||
check process que with pidfile /srv/tmp/que.pid
|
||||
start program = "/usr/bin/foreman run -f /srv/Procfile -d /srv que"
|
||||
stop program = "/bin/sh -c 'cat /srv/tmp/que.pid | xargs -r kill'"
|
||||
|
|
Loading…
Reference in a new issue