5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-16 00:31:42 +00:00

Merge branch 'issue-14169' into 'rails'
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:
fauno 2023-09-27 19:53:22 +00:00
commit beec951cdf
22 changed files with 268 additions and 53 deletions

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -62,34 +62,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
@ -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

View 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

View file

@ -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

View 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

View file

@ -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}"

View file

@ -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
}

View 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

View file

@ -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

View file

@ -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]

View file

@ -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]

View 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/

View 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

View file

@ -0,0 +1 @@
-# nada

View file

@ -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'

View file

@ -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'

View file

@ -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