5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-26 01:06:21 +00:00

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

This commit is contained in:
maki 2024-05-15 17:00:43 -03:00
commit ffff78c0cb
90 changed files with 297 additions and 287 deletions

View file

@ -1,3 +1,6 @@
stages:
- "test"
- "deploy"
.apk-add: &apk-add
- "apk add go-task diffutils gitlab_ci_log_section"
.disable-hainish: &disable-hainish
@ -6,9 +9,15 @@
- paths:
- "vendor/ruby"
- ".bundle"
key:
files:
- "Gemfile.lock"
.cache-node: &cache-node
- paths:
- "node_modules"
key:
files:
- "yarn.lock"
.cache-task: &cache-task
- paths:
- ".task"
@ -18,11 +27,26 @@ variables:
LC_ALL: "C.UTF-8"
HAINISH: ""
cache:
push:
stage: "test"
only:
- "rails"
except:
- "schedules"
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_CI_PUSH_TOKEN:${GITLAB_CI_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git\""
script:
- "git commit --allow-empty -m \"ci: test [skip ci]\""
- "git push -o ci.skip origin HEAD:${CI_COMMIT_BRANCH}"
assets:
stage: "deploy"
only:
- "rails"
- "17.3.alpine.panel.sutty.nl"
- "production.panel.sutty.nl"
- "panel.sutty.nl"
- "panel.testing.sutty.nl"
except:
- "schedules"
cache:
@ -30,14 +54,14 @@ assets:
- *cache-node
- *cache-task
before_script:
- *apk-add
- "gitlab_ci_log_section --name git --header=\"Configuring git\""
- "git config --global user.email \"${GIT_USER_EMAIL:-$GITLAB_USER_EMAIL}\""
- "git config --global user.name \"${GIT_USER_NAME:-$GITLAB_USER_NAME}\""
- "git remote set-url --push origin \"https://${GITLAB_USERNAME}:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git\""
- "git remote set-url --push origin \"https://GITLAB_CI_PUSH_TOKEN:${GITLAB_CI_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git\""
- "gitlab_ci_log_section --name git --end"
- "gitlab_ci_log_section --name apk --header=\"Installing dependencies\""
- "apk add brotli"
- *apk-add
- *disable-hainish
- "gitlab_ci_log_section --name apk --end"
script:
@ -45,7 +69,7 @@ assets:
- "go-task assets"
after_script:
- "git add public && git commit -m \"ci: assets [skip ci]\""
- "git push -o ci.skip"
- "git push -o ci.skip origin HEAD:${CI_COMMIT_BRANCH}"
gem-audit:
stage: "test"
only:

View file

@ -80,6 +80,7 @@ gem 'yaml_db', git: 'https://0xacab.org/sutty/yaml_db.git'
gem 'kaminari'
gem 'device_detector'
gem 'htmlbeautifier'
gem 'dry-schema'
gem 'rubanok'
gem 'after_commit_everywhere', '~> 1.0'

View file

@ -118,10 +118,11 @@ GEM
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
capybara (2.18.0)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
@ -361,13 +362,14 @@ GEM
net-pop
net-smtp
marcel (1.0.2)
matrix (0.4.2)
memory_profiler (1.0.1)
mercenary (0.4.0)
method_source (1.0.0)
mini_histogram (0.3.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
mini_portile2 (2.8.6)
minitest (5.21.1)
mobility (1.2.9)
i18n (>= 0.6.10, < 2)
@ -387,7 +389,7 @@ GEM
net-ssh (7.2.1)
netaddr (2.0.6)
nio4r (2.7.0-x86_64-linux-musl)
nokogiri (1.16.0-x86_64-linux-musl)
nokogiri (1.16.5-x86_64-linux-musl)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
orm_adapter (0.5.0)
@ -408,7 +410,7 @@ GEM
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (5.0.4)
public_suffix (5.0.5)
puma (6.4.2-x86_64-linux-musl)
nio4r (~> 2.0)
pundit (2.3.1)
@ -487,7 +489,7 @@ GEM
redis-store (>= 1.2, < 2)
redis-store (1.9.2)
redis (>= 4, < 6)
regexp_parser (2.9.0)
regexp_parser (2.9.2)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.1)
@ -646,6 +648,7 @@ DEPENDENCIES
distributed-press-api-client (~> 0.4.1)
dotenv-rails
down
dry-schema
ed25519
email_address!
exception_notification

View file

@ -185,9 +185,13 @@ tasks:
- "test -f ../hain/usr/bin/bundler-audit"
rubocop:
desc: "Ruby linting"
deps:
- "gems"
cmds:
- "./bin/modified_files | ./bin/with_extension rb | xargs -r {{.HAINISH}} bundle exec rubocop {{.CLI_ARGS}}"
haml-lint:
desc: "HAML linting"
deps:
- "gems"
cmds:
- "./bin/modified_files | ./bin/with_extension haml | xargs -r {{.HAINISH}} bundle exec haml-lint {{.CLI_ARGS}}"

View file

@ -18,7 +18,7 @@ module Api
# Si todo salió bien, enviar los correos y redirigir al sitio.
# El sitio nos dice a dónde tenemos que ir.
ContactJob.perform_later site.id,
ContactJob.perform_later site,
params[:form],
contact_params.to_h.symbolize_keys,
params[:redirect]

View file

@ -11,7 +11,7 @@ module Api
# respondemos con lo mismo.
def create
if (site&.airbrake_valid? airbrake_token) && !detected_device.bot?
BacktraceJob.perform_later site_id: params[:site_id],
BacktraceJob.perform_later site: site,
params: airbrake_params.to_h
end

View file

@ -14,7 +14,7 @@ class ApplicationController < ActionController::Base
after_action :store_location!
before_action do
Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?('@' + ENV.fetch('SUTTY', 'sutty.nl'))
Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?("@#{ENV.fetch('SUTTY', 'sutty.nl')}")
end
# No tenemos índice de sutty, vamos directamente a ver el listado de
@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
end
private
def notify_unconfirmed_email
return unless current_usuarie
return if current_usuarie.confirmed?
@ -58,9 +58,7 @@ class ApplicationController < ActionController::Base
def current_locale
locale = params[:change_locale_to]
if locale.present? && I18n.locale_available?(locale)
session[:locale] = params[:change_locale_to]
end
session[:locale] = params[:change_locale_to] if locale.present? && I18n.locale_available?(locale)
session[:locale] || current_usuarie&.lang || I18n.locale
end
@ -121,5 +119,4 @@ class ApplicationController < ActionController::Base
session[:usuarie_return_to] = request.fullpath
end
end

View file

@ -4,7 +4,7 @@ class EnvController < ActionController::Base
skip_before_action :verify_authenticity_token
def index
@site = Site.find_by_name('panel')
@site = Site.find_by_name('panel') || Site.first
stale? @site if @site
end

View file

@ -13,7 +13,7 @@ module ApplicationHelper
root = names.shift
names.each do |n|
root += '[' + n.to_s + ']'
root += "[#{n}]"
end
[root, name]
@ -41,7 +41,7 @@ module ApplicationHelper
def plain_field_name_for(*names)
root, name = field_name_for(*names)
root + '[' + name.to_s + ']'
"#{root}[#{name}]"
end
def distance_of_time_in_words_if_more_than_a_minute(seconds)

View file

@ -11,14 +11,33 @@ class ActivityPub
class FetchJob < ApplicationJob
self.priority = 50
attr_reader :object, :response
# Notificar errores de JSON con el contenido, tomar los errores de
# validación y conexión como errores temporales y notificar todo lo
# demás sin reintentar.
#
# @param error [Exception]
# @return [Bool]
discard_on(FastJsonparser::ParseError) do |error|
ExceptionNotifier.notify_exception(error, data: { site: site.name, object: object.uri, body: response.body })
end
retry_on ActiveRecord::RecordInvalid
retry_on SocketError, wait: ApplicationJob.random_wait
retry_on SystemCallError, wait: ApplicationJob.random_wait
retry_on Net::OpenTimeout, wait: ApplicationJob.random_wait
retry_on OpenSSL::OpenSSLError, wait: ApplicationJob.random_wait
def perform(site:, object_id:)
ActivityPub::Object.transaction do
object = ::ActivityPub::Object.find(object_id)
@site = site
@object = ::ActivityPub::Object.find(object_id)
return if object.blank?
return if object.activity_pubs.where(aasm_state: 'removed').count.positive?
response = site.social_inbox.dereferencer.get(uri: object.uri)
@response = site.social_inbox.dereferencer.get(uri: object.uri)
# @todo Fallar cuando la respuesta no funcione?
# @todo Eliminar en 410 Gone
@ -31,7 +50,8 @@ class ActivityPub
content = FastJsonparser.parse(response.body)
# Modificar atómicamente
::ActivityPub::Object.lock.find(object_id).update!(content: content, type: ActivityPub::Object.type_from(content).name)
::ActivityPub::Object.lock.find(object_id).update!(content: content,
type: ActivityPub::Object.type_from(content).name)
object = ::ActivityPub::Object.find(object_id)
# Actualiza la mención
@ -39,8 +59,6 @@ class ActivityPub
# Arreglar las relaciones con actividades también
ActivityPub.where(object_id: object.id).update_all(object_type: object.type, updated_at: Time.now)
rescue FastJsonparser::ParseError => e
ExceptionNotifier.notify_exception(e, data: { site: site.name, object: object.uri, body: response.body })
end
end
end

View file

@ -3,7 +3,9 @@
class ActivityPub
# Procesar las actividades a medida que llegan
class ProcessJob < ApplicationJob
attr_reader :body, :initial_state
attr_reader :body
retry_on ActiveRecord::RecordInvalid
# Procesa la actividad en segundo plano
#
@ -12,7 +14,6 @@ class ActivityPub
def perform(site:, body:, initial_state: :paused)
@site = site
@body = body
@initial_state = initial_state
ActiveRecord::Base.connection_pool.with_connection do
::ActivityPub.transaction do
@ -28,24 +29,6 @@ class ActivityPub
end
end
# Al generar una excepción, en lugar de seguir intentando, enviamos
# el reporte.
def handle_error(error)
case error
when ActiveRecord::RecordInvalid then retry_in(ApplicationJob.random_wait)
else
ExceptionNotifier.notify_exception(
error,
data: {
site: site.name,
body: body,
initial_state: initial_state,
activity: original_activity,
message: 'Esta acción se canceló automáticamente, para regenerarla, volver a correr el proceso con los mismos parámetros.'
})
end
end
private
# Si el objeto ya viene incorporado en la actividad o lo tenemos

View file

@ -43,7 +43,9 @@ class ActivityPub
# Si alguna falló, reintentar
raise if logs.present?
rescue Exception => e
ExceptionNotifier.notify_exception(e, data: { site: site.name, logs: logs, blocklist: blocklist, allowlist: allowlist, pauselist: pauselist })
ExceptionNotifier.notify_exception(e,
data: { site: site.name, logs: logs, blocklist: blocklist,
allowlist: allowlist, pauselist: pauselist })
raise
end

View file

@ -8,16 +8,17 @@ class ApplicationJob < ActiveJob::Base
# superpongan tareas
#
# @return [Array<Integer>]
RANDOM_WAIT = [3, 5, 7, 11, 13]
RANDOM_WAIT = [3, 5, 7, 11, 13].freeze
# @return [ActiveSupport::Duration]
def self.random_wait
RANDOM_WAIT.sample.seconds
end
private
attr_reader :site
def site
@site ||= Site.find @params[:site_id]
# Si falla por cualquier cosa informar y descartar
discard_on(Exception) do |job, error|
ExceptionNotifier.notify_exception(error, data: { job: job })
end
end

View file

@ -6,10 +6,10 @@ class BacktraceJob < ApplicationJob
EMPTY_SOURCEMAP = { 'mappings' => '' }.freeze
attr_reader :params, :site_id
attr_reader :params
def perform(site_id:, params:)
@site_id = site_id
def perform(site:, params:)
@site = site
@params = params
unless sources.empty?
@ -44,10 +44,6 @@ class BacktraceJob < ApplicationJob
private
def site
@site ||= Site.find_by_id(site_id)
end
# Obtiene todos los archivos del backtrace solo si los puede descargar
# desde fuentes seguras.
#
@ -59,9 +55,7 @@ class BacktraceJob < ApplicationJob
x['backtrace']
end.flatten.map do |x|
x['file'].split('@').last
end.uniq.select do |x|
%r{\Ahttps://} =~ x
end
end.uniq.grep(%r{\Ahttps://})
end
# Descarga y devuelve los datos de un archivo

View file

@ -5,10 +5,8 @@ class ContactJob < ApplicationJob
# @param [Integer]
# @param [String]
# @param [Hash]
def perform(site_id, form_name, form, origin = nil)
# Retrocompabilidad al actualizar a 2.7.1
# @see ApplicationJob#site
@params = { site_id: site_id }
def perform(site, form_name, form, origin = nil)
@site = site
# Sanitizar los valores
form.each_key do |key|
@ -23,7 +21,7 @@ class ContactJob < ApplicationJob
usuaries.each_slice(10) do |u|
ContactMailer.with(form_name: form_name,
form: form,
site_id: site_id,
site: site,
usuaries_emails: u,
origin: origin)
.notify_usuaries.deliver_now

View file

@ -11,44 +11,36 @@ class DeployJob < ApplicationJob
# Lanzar lo antes posible
self.priority = 10
def handle_error(error)
case error
when DeployAlreadyRunningException then retry_in 1.minute
when DeployTimedOutException then expire
else super
end
end
retry_on DeployAlreadyRunningException, wait: 1.minute
discard_on DeployTimedOutException
# rubocop:disable Metrics/MethodLength
def perform(site, notify: true, time: Time.now, output: false)
@output = output
@site = site
ActiveRecord::Base.connection_pool.with_connection do
@site = Site.find(site)
# Si ya hay una tarea corriendo, aplazar esta. Si estuvo
# esperando más de 10 minutos, recuperar el estado anterior.
#
# Como el trabajo actual se aplaza al siguiente, arrastrar la
# hora original para poder ir haciendo timeouts.
if @site.building?
if site.building?
notify = false
if 10.minutes.ago >= time
raise DeployTimedOutException,
"#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
else
raise DeployAlreadyRunningException
end
raise DeployAlreadyRunningException unless 10.minutes.ago >= time
raise DeployTimedOutException,
"#{site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original"
end
@deployed = {}
@site.update status: 'building'
@site.deployment_list.each do |d|
site.update status: 'building'
site.deployment_list.each do |d|
begin
raise DeployException, 'Una dependencia falló' if failed_dependencies? d
status = d.deploy(output: @output)
status = d.deploy(output: output)
seconds = d.build_stats.last.try(:seconds) || 0
size = d.size
urls = d.urls.map do |url|
@ -57,9 +49,7 @@ class DeployJob < ApplicationJob
nil
end.compact
if d == @site.deployment_list.last && !status
raise DeployException, 'Falló la compilación'
end
raise DeployException, 'Falló la compilación' if d == site.deployment_list.last && !status
rescue StandardError => e
status = false
seconds ||= 0
@ -78,9 +68,9 @@ class DeployJob < ApplicationJob
}
end
return unless @output
return unless output
puts (Terminal::Table.new do |t|
puts(Terminal::Table.new do |t|
t << (%w[type] + @deployed.values.first.keys)
t.add_separator
@deployed.each do |type, row|
@ -90,12 +80,12 @@ class DeployJob < ApplicationJob
rescue DeployTimedOutException => e
notify_exception e
ensure
if @site.present?
@site.update status: 'waiting'
if site.present?
site.update status: 'waiting'
notify_usuaries if notify
puts "\a" if @output
puts "\a" if output
end
end
end
@ -125,7 +115,7 @@ class DeployJob < ApplicationJob
# @param :deploy [Deploy]
def notify_exception(exception, deploy = nil)
data = {
site: @site.id,
site: site.name,
deploy: deploy&.type,
log: deploy&.build_stats&.last&.log,
failed_dependencies: (failed_dependencies(deploy) if deploy)
@ -135,8 +125,10 @@ class DeployJob < ApplicationJob
end
def notify_usuaries
@site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie|
DeployMailer.with(usuarie: usuarie, site: @site.id)
usuarie_ids = site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id)
Usuarie.where(id: usuarie_ids).find_each do |usuarie|
DeployMailer.with(usuarie: usuarie, site: site)
.deployed(@deployed)
.deliver_now
end

View file

@ -7,6 +7,8 @@ class GitPullJob < ApplicationJob
# @param :message [String]
# @return [nil]
def perform(site, usuarie, message)
@site = site
return unless site.repository.origin
site.repository.fetch

View file

@ -6,6 +6,8 @@ class GitPushJob < ApplicationJob
# @param :site [Site]
# @return [nil]
def perform(site)
site.repository.push if site.repository.origin
@site = site
site.repository.push if site.repository.origin
end
end
end

View file

@ -15,8 +15,7 @@
# Lo mismo para salir de mantenimiento, agregando el atributo
# are_we_back: true al crear el Maintenance.
class MaintenanceJob < ApplicationJob
def perform(maintenance_id:)
maintenance = Maintenance.find(maintenance_id)
def perform(maintenance:)
# Decidir cuál vamos a enviar según el estado de Maintenance
mailer = maintenance.are_we_back ? :were_back : :notice

View file

@ -6,9 +6,6 @@ class PeriodicJob < ApplicationJob
STARTING_INTERVAL = Stat::INTERVALS.first
# Tener el sitio a mano
attr_reader :site
# Descartar y notificar si pasó algo más.
#
# XXX: En realidad deberíamos seguir reintentando?

View file

@ -7,8 +7,8 @@ class StatCollectionJob < PeriodicJob
STAT_NAME = 'stat_collection_job'
def perform(site_id:, once: true)
@site = Site.find site_id
def perform(site:, once: true)
@site = site
beginning = beginning_of_interval
stat = site.stats.create! name: STAT_NAME
@ -22,7 +22,7 @@ class StatCollectionJob < PeriodicJob
rollup.average(:seconds)
end
dimensions = { site_id: site_id }
dimensions = { site_id: site.id }
reduce_rollup(name: 'builds', operation: :sum, dimensions: dimensions)
reduce_rollup(name: 'space_used', operation: :average, dimensions: dimensions)

View file

@ -16,8 +16,8 @@ class UriCollectionJob < PeriodicJob
IMAGES = %w[.png .jpg .jpeg .gif .webp .jfif].freeze
STAT_NAME = 'uri_collection_job'
def perform(site_id:, once: true)
@site = Site.find site_id
def perform(site:, once: true)
@site = site
# Obtener el principio del intervalo anterior
beginning_of_interval

View file

@ -14,6 +14,8 @@ module Jekyll
extend ActiveSupport::Concern
included do
DATA_EXTENSIONS = %w[.yaml .yml .json .csv .tsv].freeze
def read_data_to(dir, data)
return unless File.directory?(dir) && !@entry_filter.symlink?(dir)
@ -24,7 +26,7 @@ module Jekyll
if File.directory?(path)
read_data_to(path, data[sanitize_filename(entry)] = {})
else
elsif DATA_EXTENSIONS.include?(File.extname(entry))
key = sanitize_filename(File.basename(entry, ".*"))
data[key] = read_data_file(path)
end

View file

@ -10,7 +10,7 @@ class ApplicationMailer < ActionMailer::Base
private
def site
@site ||= Site.find @params[:site_id]
@site ||= @params[:site]
end
def inline_logo!

View file

@ -13,8 +13,7 @@ class DeployMailer < ApplicationMailer
# rubocop:disable Metrics/AbcSize
def deployed(deploys = {})
usuarie = Usuarie.find(params[:usuarie])
site = usuarie.sites.find(params[:site])
usuarie = params[:usuarie]
hostname = site.hostname
deploys ||= {}

View file

@ -1,9 +0,0 @@
# frozen_string_literal: true
class InvitadxMailer < ApplicationMailer
def confirmation_required
@invitadx = params[:invitadx]
@site = params[:site]
mail from: "#{@site.config.dig('title')} <#{ENV.fetch('DEFAULT_FROM', 'sutty@kefir.red')}>", to: @invitadx.email, subject: t('.subject')
end
end

View file

@ -64,7 +64,7 @@ class ActivityPub < ApplicationRecord
# Array<String> o mezcla y obtener el que más nos convenga o
# adivinar uno.
when Array
links = object['url'].map.with_index do |link, i|
links = object['url'].map.with_index do |link, _i|
case link
when Hash then link
else { 'href' => link.to_s }
@ -93,7 +93,8 @@ class ActivityPub < ApplicationRecord
# Gestionar todos los errores
error_on_all_events do |e|
ExceptionNotifier.notify_exception(e, data: { site: site.name, activity_pub: self.id, activity: activities.first.uri })
ExceptionNotifier.notify_exception(e,
data: { site: site.name, activity_pub: id, activity: activities.first.uri })
end
# Se puede volver a pausa en caso de actualización remota, para

View file

@ -35,9 +35,9 @@ class ActivityPub
validates_inclusion_of :format, in: %w[mastodon fediblock none]
HOSTNAME_HEADERS = {
'mastodon' => '#domain',
'mastodon' => '#domain',
'fediblock' => 'domain'
}
}.freeze
def client
@client ||= Client.new

View file

@ -39,13 +39,14 @@ class ActivityPub
def referenced(site)
require 'distributed_press/v1/social/referenced_object'
@referenced ||= DistributedPress::V1::Social::ReferencedObject.new(object: content, dereferencer: site.social_inbox.dereferencer)
@referenced ||= DistributedPress::V1::Social::ReferencedObject.new(object: content,
dereferencer: site.social_inbox.dereferencer)
end
private
def uri_is_content_id?
return if self.uri == content['id']
return if uri == content['id']
errors.add(:activity_pub_objects, 'El ID del objeto no coincide con su URI')
end

View file

@ -40,7 +40,8 @@ class ActivityPub
def content
{
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => Rails.application.routes.url_helpers.v1_activity_pub_remote_flag_url(self, host: site.social_inbox_hostname),
'id' => Rails.application.routes.url_helpers.v1_activity_pub_remote_flag_url(self,
host: site.social_inbox_hostname),
'type' => 'Flag',
'actor' => main_site.social_inbox.actor_id,
'content' => message.to_s,
@ -53,7 +54,7 @@ class ActivityPub
#
# @return [Site]
def main_site
@main_site ||= Site.find(ENV.fetch('PANEL_ACTOR_SITE_ID') { 1 })
@main_site ||= Site.find(ENV.fetch('PANEL_ACTOR_SITE_ID', 1))
end
end
end

View file

@ -2,4 +2,28 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
# Obtener una lista filtrada de atributos al momento de serializar
#
# @return [String]
def to_yaml(options = {})
pruned_attributes.to_yaml(options)
end
# Devuelve todos los atributos menos los filtrados
#
# @return [Hash]
def pruned_attributes
self.class.inspection_filter.filter(serializable_hash)
end
# @param coder [Psych::Coder]
# @return nil
def encode_with(coder)
pruned_attributes.each_pair do |attr, value|
coder[attr] = value
end
nil
end
end

View file

@ -17,7 +17,7 @@ module Tienda
return t if new_record?
t.blank? ? 'https://' + name + '.' + ENV.fetch('TIENDA', 'tienda.sutty.nl') : t
t.blank? ? "https://#{name}.#{ENV.fetch('TIENDA', 'tienda.sutty.nl')}" : t
end
end
end

View file

@ -45,7 +45,8 @@ class FediblockState < ApplicationRecord
private
def block_instances!
ActivityPub::InstanceModerationJob.perform_later(site: site, hostnames: fediblock.hostnames, perform_remotely: false)
ActivityPub::InstanceModerationJob.perform_later(site: site, hostnames: fediblock.hostnames,
perform_remotely: false)
end
# Pausar todas las moderaciones de las instancias que no estén

View file

@ -16,7 +16,9 @@ class InstanceModeration < ApplicationRecord
state :blocked
error_on_all_events do |e|
ExceptionNotifier.notify_exception(e, data: { site: site.name, instance: instance.hostname, instance_moderation: id })
ExceptionNotifier.notify_exception(e,
data: { site: site.name, instance: instance.hostname,
instance_moderation: id })
end
after_all_events do

View file

@ -11,7 +11,7 @@ class LogEntry < ApplicationRecord
def resend
return if sent
ContactJob.perform_later site_id, params[:form], params
ContactJob.perform_later site, params[:form], params
end
def params

View file

@ -142,7 +142,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
# En caso de que algún campo necesite realizar acciones antes de ser
# guardado
def save
if !changed?
unless changed?
self[:value] = document_value if private?
return true

View file

@ -11,8 +11,11 @@ class Site < ApplicationRecord
include Site::BuildStats
include Site::LayoutOrdering
include Site::SocialDistributedPress
include Site::DefaultOptions
include Tienda
self.filter_attributes += [/_key/, /_ciphertext\z/]
# Cifrar la llave privada que cifra y decifra campos ocultos. Sutty
# tiene acceso pero los datos se guardan cifrados en el sitio. Esto
# protege información privada en repositorios públicos, pero no la
@ -235,11 +238,11 @@ class Site < ApplicationRecord
# layouts. Si pasamos un layout que no existe, obtenemos un
# NoMethodError
@layouts_struct ||= Struct.new(*layout_keys, keyword_init: true)
@layouts ||= @layouts_struct.new(**data['layouts'].map do |name, metadata|
@layouts ||= @layouts_struct.new(**data['layouts'].to_h do |name, metadata|
[name.to_sym,
Layout.new(site: self, name: name.to_sym, meta: metadata.delete('meta')&.with_indifferent_access,
metadata: metadata.with_indifferent_access)]
end.to_h)
end)
end
# TODO: Si la estructura de datos no existe, vamos a producir una
@ -314,7 +317,7 @@ class Site < ApplicationRecord
end
def reload
super.tap do |s|
super.tap do |_s|
reload_jekyll!
end
self
@ -403,7 +406,7 @@ class Site < ApplicationRecord
def clone_skel!
return if jekyll?
Rugged::Repository.clone_at(ENV['SKEL_SUTTY'], path, checkout_branch: design.gem)
Rugged::Repository.clone_at(ENV.fetch('SKEL_SUTTY', nil), path, checkout_branch: design.gem)
# Necesita un bloque
repository.rugged.remotes.rename('origin', 'upstream') {}
@ -497,11 +500,11 @@ class Site < ApplicationRecord
deploy_local = deploys.find_by_type('DeployLocal')
deploy_local.git_lfs
if !gems_installed? || gemfile_updated? || gemfile_lock_updated?
deploy_local.bundle
touch
FileUtils.touch(gemfile_path)
end
return unless !gems_installed? || gemfile_updated? || gemfile_lock_updated?
deploy_local.bundle
touch
FileUtils.touch(gemfile_path)
end
def gem_path

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'dry-schema'
class Site
# Las opciones por defecto se aplican durante la creación del sitio y
# luego se permite a les usuaries modificarlas según quieran. Por el
# momento las opciones nuevas que aparezcan no modifican un sitio que
# ya existe.
module DefaultOptions
extend ActiveSupport::Concern
Schema = Dry::Schema.Params do
optional(:colaboracion_anonima).value(:bool)
optional(:contact).value(:bool)
optional(:acepta_invitades).value(:bool)
optional(:slugify_mode).value(included_in?: Jekyll::Utils::SLUGIFY_MODES)
optional(:pagination).value(:bool)
end
included do
validate :validate_options_from_theme!, if: :persisted?
# @return [Dry::Schema::Result]
def options_from_theme
@options_from_theme ||= Schema.call(data['sutty'])
end
def update_options_from_theme
return true if options_from_theme.to_h.blank?
update(**options_from_theme.to_h)
end
private
def validate_options_from_theme!
options_from_theme.errors.each do |error|
errors.add(:default_options, "#{error.path.map(&:to_s).join('/')} #{error} (#{error.input})")
end
end
end
end
end

View file

@ -45,7 +45,7 @@ class SocialInbox
# @param url [String]
# @return [DistributedPress::V1::Social::Client]
def client_for(url)
raise "Falló generar un cliente" if url.blank?
raise 'Falló generar un cliente' if url.blank?
@client_for ||= {}
@client_for[url] ||=
@ -54,7 +54,7 @@ class SocialInbox
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'])
cache_store: HTTParty::Cache::Store::Redis.new(redis_url: ENV.fetch('REDIS_SERVER', nil))
)
end

View file

@ -21,6 +21,8 @@ class Usuarie < ApplicationRecord
has_many :blazer_audits, foreign_key: 'user_id', class_name: 'Blazer::Audit'
has_many :blazer_queries, foreign_key: 'creator_id', class_name: 'Blazer::Query'
self.filter_attributes += [/\Aemail\z/, /\Aencrypted_password\z/]
def name
email.split('@', 2).first
end
@ -74,10 +76,10 @@ class Usuarie < ApplicationRecord
# Si le usuarie (re)confirma su cuenta con una invitación pendiente,
# considerarla aceptada también.
def accept_invitation_after_confirmation!
if confirmed?
self.invitation_token = nil
self.invitation_accepted_at ||= Time.now.utc
end
return unless confirmed?
self.invitation_token = nil
self.invitation_accepted_at ||= Time.now.utc
end
# Muestra un error si el idioma no está disponible al cambiar el
@ -85,7 +87,7 @@ class Usuarie < ApplicationRecord
#
# @return [nil]
def locale_available!
return if I18n.locale_available? self.lang
return if I18n.locale_available? lang
errors.add(:lang, I18n.t('activerecord.errors.models.usuarie.attributes.lang.not_available'))
nil

View file

@ -5,7 +5,7 @@
SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
def deploy
site.enqueue!
DeployJob.perform_later site.id
DeployJob.perform_later site
end
# Crea un sitio, agrega un rol nuevo y guarda los cambios a la
@ -31,6 +31,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
add_role_to_deploys! role
site.save &&
site.update_options_from_theme &&
site.config.write &&
commit_config(action: :create) &&
site.reset.nil? &&
@ -236,8 +237,6 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
end
end
private
# Asignar un rol a cada deploy si no lo tenía ya
def add_role_to_deploys!(role = current_role)
site.deploys.each do |deploy|
@ -249,7 +248,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
@current_role ||= usuarie.rol_for_site(site)
end
def with_all_locales(&block)
def with_all_locales
site.locales.map do |locale|
next unless I18n.available_locales.include? locale

View file

@ -2,7 +2,7 @@
.d-flex.flex-row.w-100
- local = { report: { class: 'ml-auto', data: { confirm: t('.confirm_report') } } }
- ActorModeration.events.each do |actor_event|
- possible = !actor_moderation.public_send(:"may_#{actor_event}?")
- possible = actor_moderation.public_send(:"may_#{actor_event}?")
%div{ class: local.dig(actor_event, :class) }
= render 'components/btn_base',
text: t(".text_#{actor_event}"),

View file

@ -2,7 +2,7 @@
= cache @site do
:plain
window.env = {
AIRBRAKE_SITE_ID: #{@site.id},
AIRBRAKE_API_KEY: "#{@site.airbrake_api_key}",
AIRBRAKE_PROJECT_ID: #{@site.id},
AIRBRAKE_PROJECT_KEY: "#{@site.airbrake_api_key}",
PANEL_URL: "#{ENV['PANEL_URL']}"
}

View file

@ -13,8 +13,8 @@
- else
= link_to crumb.name, crumb.url, class: 'line-clamp-1'
%ul.navbar-nav.order-1.order-md-2
- if @current_usuarie || current_usuarie
- if @current_usuarie || current_usuarie
%ul.navbar-nav.order-1.order-md-2
- if @site&.tienda?
%li.nav-item
= link_to t('.tienda'), @site.tienda_url,

View file

@ -15,7 +15,7 @@
= 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'
= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload', defer: true
= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload'
= favicon_link_tag 'sutty_cuadrada.png', rel: 'apple-touch-icon', type: 'image/png'
= render 'layouts/link_rel_alternate'

View file

@ -43,7 +43,6 @@
= 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 btn-secondary'
@ -136,15 +135,16 @@
%span{ lang: post.locale, dir: dir }= category
= '/' unless post.front_matter['categories'].last == category
%td.text-nowrap
= post.created_at.strftime('%F')
%br/
= 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-secondary btn-block'
- if @usuarie || policy(post).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') }
%td.text-nowrap
= post.created_at.strftime('%F')
%br/
= post.order
%td.text-nowrap
.d-flex.flex-row.align-items-start
- if @usuarie || policy(post).edit?
= link_to t('posts.edit_post'), edit_site_post_path(@site, post.path), class: 'btn btn-secondary'
- if @usuarie || policy(post).destroy?
= link_to t('posts.destroy'), site_post_path(@site, post.path), class: 'btn btn-secondary', method: :delete, data: { confirm: t('posts.confirm_destroy') }
-#
Rescatar cualquier error en un post, notificarlo e
ignorar su renderización.

View file

@ -62,7 +62,7 @@ Rails.application.configure do
config.log_tags = %i[request_id]
# Use a different cache store in production.
config.cache_store = :redis_cache_store, { url: ENV['REDIS_SERVER'] }
config.cache_store = :redis_cache_store, { url: ENV.fetch('REDIS_SERVER', nil) }
config.action_mailer.perform_caching = false
@ -87,7 +87,7 @@ Rails.application.configure do
config.lograge.enabled = true
# Use default logging formatter so that PID and timestamp are not
# suppressed.
config.log_formatter = ::Logger::Formatter.new
config.log_formatter = Logger::Formatter.new
# Use a different logger for distributed setups.
require 'syslog/logger'
@ -140,9 +140,10 @@ Rails.application.configure do
domain: ENV.fetch('SUTTY', 'sutty.nl'),
enable_starttls_auto: false
}
config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', "noreply@sutty.nl") }
config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', 'noreply@sutty.nl') }
config.middleware.use ExceptionNotification::Rack, gitlab: {}, error_grouping: true, ignore_exceptions: ['DeployJob::DeployAlreadyRunningException']
config.middleware.use ExceptionNotification::Rack, gitlab: {}, error_grouping: true,
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'

View file

@ -5,4 +5,5 @@
# Configure sensitive parameters which will be filtered from the log file.
Rails.application.config.filter_parameters += %i[
password passw secret token _key crypt salt certificate otp ssn key
_pem _ciphertext email
]

View file

@ -1,5 +1,5 @@
# frozen_string_literal: true
Que::Web.use(Rack::Auth::Basic) do |user, password|
[user, password] == [ENV['HTTP_BASIC_USER'], ENV['HTTP_BASIC_PASSWORD']]
[user, password] == [ENV.fetch('HTTP_BASIC_USER', nil), ENV.fetch('HTTP_BASIC_PASSWORD', nil)]
end

View file

@ -5,7 +5,7 @@ class AddDefaultToDistributedPressPublisher < ActiveRecord::Migration[6.1]
def up
add_column :distributed_press_publishers, :default, :boolean, default: false
DistributedPressPublisher.last.update(default: true)
DistributedPressPublisher.last&.update(default: true)
end
def down

View file

@ -16,9 +16,7 @@ class CreateFediblockStates < ActiveRecord::Migration[6.1]
# Todas las listas están activas por defecto
DeploySocialDistributedPress.find_each do |deploy|
ActivityPub::Fediblock.find_each do |fediblock|
FediblockState.create(site: deploy.site, fediblock: fediblock, aasm_state: 'disabled').tap do |f|
f.enable!
end
FediblockState.create(site: deploy.site, fediblock: fediblock, aasm_state: 'disabled').tap(&:enable!)
end
end
end

View file

@ -4,9 +4,11 @@
# decompresión
class BrsDecompressorCorruptedSourceError < ActiveRecord::Migration[6.1]
def up
raise unless HTTParty.get("https://mas.to/api/v2/instance", headers: { "Accept-Encoding": "br;q=1.0,gzip;q=1.0,deflate;q=0.6,identity;q=0.3" }).ok?
raise unless HTTParty.get('https://mas.to/api/v2/instance',
headers: { 'Accept-Encoding': 'br;q=1.0,gzip;q=1.0,deflate;q=0.6,identity;q=0.3' }).ok?
QueJob.where("last_error_message like '%BRS::DecompressorCorruptedSourceError%'").update_all(error_count: 0, run_at: Time.now)
QueJob.where("last_error_message like '%BRS::DecompressorCorruptedSourceError%'").update_all(error_count: 0,
run_at: Time.now)
end
def down; end

View file

@ -4,7 +4,9 @@
class FixActivityType < ActiveRecord::Migration[6.1]
def up
%w[Like Announce].each do |type|
ActivityPub::Activity.where(Arel.sql("content->>'type' = '#{type}'")).update_all(type: "ActivityPub::Activity::#{type}", updated_at: Time.now)
ActivityPub::Activity.where(Arel.sql("content->>'type' = '#{type}'")).update_all(
type: "ActivityPub::Activity::#{type}", updated_at: Time.now
)
end
end

View file

@ -3,7 +3,7 @@
# De alguna forma se guardaron objetos duplicados!
class FixDuplicateObjects < ActiveRecord::Migration[6.1]
def up
ActivityPub::Object.group(:uri).count.select { |_, v| v > 1 }.keys.each do |uri|
ActivityPub::Object.group(:uri).count.select { |_, v| v > 1 }.each_key do |uri|
objects = ActivityPub::Object.where(uri: uri)
deleted_ids = objects[1..].map(&:delete).map(&:id)

View file

@ -14,9 +14,7 @@ class AddFedipactToFediblocks < ActiveRecord::Migration[6.1]
)
DeploySocialDistributedPress.find_each do |deploy|
FediblockState.create(site: deploy.site, fediblock: fedipact, aasm_state: 'disabled').tap do |f|
f.enable!
end
FediblockState.create(site: deploy.site, fediblock: fedipact, aasm_state: 'disabled').tap(&:enable!)
end
end

View file

@ -4,14 +4,14 @@
# no es válida y por eso teníamos objetos duplicados.
class AddMissingUniqueIndexes < ActiveRecord::Migration[6.1]
def up
ActivityPub::Object.group(:uri).count.select { |_, v| v > 1 }.keys.each do |uri|
ActivityPub::Object.group(:uri).count.select { |_, v| v > 1 }.each_key do |uri|
objects = ActivityPub::Object.where(uri: uri)
deleted_ids = objects[1..].map(&:delete).map(&:id)
ActivityPub.where(object_id: deleted_ids).update_all(object_id: objects.first.id, updated_at: Time.now)
end
ActivityPub::Actor.group(:uri).count.select { |_, v| v > 1 }.keys.each do |uri|
ActivityPub::Actor.group(:uri).count.select { |_, v| v > 1 }.each_key do |uri|
objects = ActivityPub::Actor.where(uri: uri)
deleted_ids = objects[1..].map(&:delete).map(&:id)
@ -21,7 +21,7 @@ class AddMissingUniqueIndexes < ActiveRecord::Migration[6.1]
ActivityPub::RemoteFlag.where(actor_id: deleted_ids).update_all(actor_id: objects.first.id, updated_at: Time.now)
end
ActivityPub::Instance.group(:hostname).count.select { |_, v| v > 1 }.keys.each do |hostname|
ActivityPub::Instance.group(:hostname).count.select { |_, v| v > 1 }.each_key do |hostname|
objects = ActivityPub::Instance.where(hostname: hostname)
deleted_ids = objects[1..].map(&:delete).map(&:id)

View file

@ -20,13 +20,13 @@ if CodeOfConduct.count.zero?
YAML.safe_load(File.read('db/seeds/codes_of_conduct.yml')).each do |coc|
CodeOfConduct.new(**coc).save!
end
end
end
if PrivacyPolicy.count.zero?
YAML.safe_load(File.read('db/seeds/privacy_policies.yml')).each do |pp|
PrivacyPolicy.new(**pp).save!
end
end
end
YAML.safe_load(File.read('db/seeds/activity_pub/fediblocks.yml')).each do |fediblock|
ActivityPub::Fediblock.find_or_create_by(id: fediblock['id']).tap do |f|

View file

@ -91,6 +91,14 @@
description_en: "We're working towards adding more themes for you to use. [Contact us!](https://sutty.nl/en/#contact)"
description_es: "Estamos trabajando para que puedas tener más diseños. [¡Escribinos!](https://sutty.nl/#contacto)"
priority: '3'
- name_en: 'Worker-recovered factory'
name_es: 'Empresa recuperada'
gem: 'empresa-recuperada-jekyll-theme'
url: 'https://empresa-recuperada.sutty.nl/'
disabled: true
description_en: "A template for [empresas recuperadas](https://en.wikipedia.org/wiki/Workers%27_self-management#Empresas_recuperadas_movement). We're working towards adding more themes for you to use. [Contact us!](https://sutty.nl/en/#contact)"
description_es: "Estamos trabajando para que puedas tener más diseños. [¡Escribinos!](https://sutty.nl/#contacto)"
priority: '3'
- name_en: 'More themes'
name_es: 'Más plantillas'
gem: 'sutty-theme-own'

View file

@ -3,9 +3,9 @@
namespace :stats do
desc 'Process stats'
task process_all: :environment do
Site.all.pluck(:id).each do |site_id|
UriCollectionJob.perform_now site_id: site_id, once: true
StatCollectionJob.perform_now site_id: site_id, once: true
Site.all.find_each do |site|
UriCollectionJob.perform_now site: site, once: true
StatCollectionJob.perform_now site: site, once: true
end
end
end

View file

@ -13,7 +13,7 @@
"@rails/activestorage": "^6.1.3-1",
"@rails/ujs": "^6.1.3-1",
"@rails/webpacker": "5.4.4",
"@suttyweb/editor": "^0.1.27",
"@suttyweb/editor": "^0.1.29",
"babel-loader": "^8.2.2",
"bs-custom-file-input": "^1.3.4",
"chart.js": "^3.5.1",

BIN
public/packs/css/application-5688882d.css (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/packs/manifest.json (Stored with Git LFS)

Binary file not shown.

BIN
public/packs/manifest.json.br (Stored with Git LFS)

Binary file not shown.

BIN
public/packs/manifest.json.gz (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1975,10 +1975,10 @@
resolved "https://registry.npmjs.org/@stimulus/webpack-helpers/-/webpack-helpers-1.1.1.tgz"
integrity sha512-XOkqSw53N9072FLHvpLM25PIwy+ndkSSbnTtjKuyzsv8K5yfkFB2rv68jU1pzqYa9FZLcvZWP4yazC0V38dx9A==
"@suttyweb/editor@^0.1.27":
version "0.1.27"
resolved "https://registry.yarnpkg.com/@suttyweb/editor/-/editor-0.1.27.tgz#9415a0b767e72dbe4fbf42ce87e62fb8f5125c31"
integrity sha512-Ts9TZtGiRIaHm+ffVBRl+/nuVcANWZNtFsrGacoajgEsagaIyA1cq8qjiNpPoM5ne9vTba3cAaLP04V/uEIhBw==
"@suttyweb/editor@^0.1.29":
version "0.1.29"
resolved "https://registry.yarnpkg.com/@suttyweb/editor/-/editor-0.1.29.tgz#8b5c6ae4e4d546002a96ecd65765d77d2a88d415"
integrity sha512-GshI8wE5UqXge2RhwAUxUXTRLPoOX7US9xVu1aLqT/deT/hDyN9S3PxVn9cJBf7uPHEqBzYXGDKWWF79PLqGHw==
dependencies:
"@floating-ui/dom" "^1.5.1"
linkifyjs "^4.1.1"