mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-16 02:31:41 +00:00
Merge branch 'issue-14169' into 'rails'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
activity pub #14169 See merge request sutty/sutty!202
This commit is contained in:
commit
beec951cdf
22 changed files with 268 additions and 53 deletions
2
Gemfile
2
Gemfile
|
@ -39,7 +39,7 @@ gem 'commonmarker'
|
|||
gem 'devise'
|
||||
gem 'devise-i18n'
|
||||
gem 'devise_invitable'
|
||||
gem 'distributed-press-api-client', '~> 0.2.3'
|
||||
gem 'distributed-press-api-client', '~> 0.3.0rc0'
|
||||
gem 'njalla-api-client', '~> 0.2.0'
|
||||
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
|
||||
gem 'exception_notification'
|
||||
|
|
|
@ -154,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
|
||||
|
@ -600,7 +600,7 @@ DEPENDENCIES
|
|||
devise
|
||||
devise-i18n
|
||||
devise_invitable
|
||||
distributed-press-api-client (~> 0.2.3)
|
||||
distributed-press-api-client (~> 0.3.0rc0)
|
||||
dotenv-rails
|
||||
down
|
||||
ed25519
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -51,7 +51,11 @@ 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
|
||||
rescue StandardError => e
|
||||
status = false
|
||||
seconds ||= 0
|
||||
|
|
|
@ -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
|
||||
|
@ -55,6 +60,22 @@ class Deploy < ApplicationRecord
|
|||
@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.
|
||||
#
|
||||
|
@ -98,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]
|
||||
|
@ -107,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)
|
||||
|
@ -117,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
|
||||
|
|
|
@ -62,32 +62,24 @@ class DeployLocal < Deploy
|
|||
FileUtils.rm_rf(File.join(site.path, '.jekyll-cache'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mkdir
|
||||
FileUtils.mkdir_p destination
|
||||
end
|
||||
|
||||
# Un entorno que solo tiene lo que necesitamos
|
||||
# Opciones necesarias para la compilación del sitio
|
||||
#
|
||||
# @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(':'),
|
||||
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,
|
||||
'JEKYLL_ENV' => Rails.env,
|
||||
'LANG' => ENV['LANG'],
|
||||
'YARN_CACHE_FOLDER' => yarn_cache_dir,
|
||||
'GEMS_SOURCE' => ENV['GEMS_SOURCE']
|
||||
})
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mkdir
|
||||
FileUtils.mkdir_p destination
|
||||
end
|
||||
|
||||
def yarn_cache_dir
|
||||
|
@ -142,7 +134,11 @@ class DeployLocal < Deploy
|
|||
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
|
||||
|
@ -156,17 +152,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
|
|
@ -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
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
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
|
|
@ -54,9 +54,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
|
||||
|
|
|
@ -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/
|
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
|
|
@ -123,6 +123,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!
|
||||
|
@ -307,6 +311,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
|
||||
|
@ -512,6 +524,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'
|
||||
|
|
|
@ -123,6 +123,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!
|
||||
|
@ -312,6 +316,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
|
||||
|
@ -520,6 +532,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'
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue