Compare commits

...

2 Commits

Author SHA1 Message Date
5f276ff349
Queue XmppSetAvatarJob when new avatar is uploaded
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Failing after 10m9s
And let job do nothing in development for now
2025-05-15 22:04:25 +04:00
5916969447
Add job for setting avatar via XMPP 2025-05-15 20:05:53 +04:00
4 changed files with 123 additions and 20 deletions

View File

@ -36,6 +36,7 @@ class SettingsController < ApplicationController
if @user.avatar_new.present? if @user.avatar_new.present?
if store_user_avatar if store_user_avatar
LdapManager::UpdateAvatar.call(user: @user) LdapManager::UpdateAvatar.call(user: @user)
XmppSetAvatarJob.perform_later(user: @user)
else else
@validation_errors = @user.errors @validation_errors = @user.errors
render :show, status: :unprocessable_entity and return render :show, status: :unprocessable_entity and return

View File

@ -0,0 +1,97 @@
require 'digest'
require "image_processing/vips"
class XmppSetAvatarJob < ApplicationJob
queue_as :default
def perform(user:, overwrite: false)
return if Rails.env.development?
@user = user
unless overwrite
current_avatar = get_current_avatar
Rails.logger.info { "User #{user.cn} already has an avatar set" }
return if current_avatar.present?
end
Rails.logger.debug { "Setting XMPP avatar for user #{user.cn}" }
stanzas = build_xep0084_stanzas
stanzas.each do |stanza|
payload = { from: @user.address, to: @user.address, stanza: stanza }
res = ejabberd.send_stanza payload
raise res.inspect if res.status != 200
end
end
private
def ejabberd
@ejabberd ||= EjabberdApiClient.new
end
def get_current_avatar
res = ejabberd.get_vcard2 @user, "PHOTO", "BINVAL"
if res.status == 200
# VCARD PHOTO/BINVAL prop exists
res.body
elsif res.status == 400
# VCARD or PHOTO/BINVAL prop does not exist
nil
else
# Unexpected error, let job fail
raise res.inspect
end
end
def process_avatar
@user.avatar.blob.open do |file|
processed = ImageProcessing::Vips
.source(file)
.resize_to_fill(256, 256)
.convert("png")
.call
processed.read
end
end
# See https://xmpp.org/extensions/xep-0084.html
def build_xep0084_stanzas
img_data = process_avatar
sha1_hash = Digest::SHA1.hexdigest(img_data)
base64_data = Base64.strict_encode64(img_data)
[
"""
<iq type='set' from='#{@user.address}' id='avatar-data-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:data'>
<item id='#{sha1_hash}'>
<data xmlns='urn:xmpp:avatar:data'>#{base64_data}</data>
</item>
</publish>
</pubsub>
</iq>
""".strip,
"""
<iq type='set' from='#{@user.address}' id='avatar-metadata-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:metadata'>
<item id='#{sha1_hash}'>
<metadata xmlns='urn:xmpp:avatar:metadata'>
<info bytes='#{img_data.size}'
id='#{sha1_hash}'
height='256'
type='image/png'
width='256'/>
</metadata>
</item>
</publish>
</pubsub>
</iq>
""".strip,
]
end
end

View File

@ -4,16 +4,8 @@ class EjabberdApiClient
end end
def post(endpoint, payload) def post(endpoint, payload)
res = Faraday.post("#{@base_url}/#{endpoint}", payload.to_json, Faraday.post "#{@base_url}/#{endpoint}", payload.to_json,
"Content-Type" => "application/json") "Content-Type" => "application/json"
if res.status != 200
#TODO Send custom event to Sentry
Rails.logger.error "[ejabberd] API request failed:"
Rails.logger.error res.body
end
res
end end
# #
@ -24,6 +16,14 @@ class EjabberdApiClient
post "add_rosteritem", payload post "add_rosteritem", payload
end end
def send_message(payload)
post "send_message", payload
end
def send_stanza(payload)
post "send_stanza", payload
end
def get_vcard2(user, name, subname) def get_vcard2(user, name, subname)
payload = { payload = {
user: user.cn, host: user.ou, user: user.cn, host: user.ou,
@ -47,8 +47,4 @@ class EjabberdApiClient
} }
post "private_set", payload post "private_set", payload
end end
def send_message(payload)
post "send_message", payload
end
end end

View File

@ -46,6 +46,8 @@ RSpec.describe 'Profile settings', type: :feature do
feature "Update avatar" do feature "Update avatar" do
scenario "fails with validation error for wrong content type" do scenario "fails with validation error for wrong content type" do
expect(LdapManager::UpdateAvatar).not_to receive(:call)
visit setting_path(:profile) visit setting_path(:profile)
attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/bitcoin.pdf" attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/bitcoin.pdf"
click_button "Save" click_button "Save"
@ -57,8 +59,7 @@ RSpec.describe 'Profile settings', type: :feature do
end end
scenario "fails with validation error for file size too large" do scenario "fails with validation error for file size too large" do
expect_any_instance_of(LdapManager::UpdateAvatar) expect(LdapManager::UpdateAvatar).not_to receive(:call)
.not_to receive(:replace_attribute).and_return(true)
visit setting_path(:profile) visit setting_path(:profile)
attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/fsociety-irc.png" attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/fsociety-irc.png"
@ -73,8 +74,12 @@ RSpec.describe 'Profile settings', type: :feature do
scenario 'works with valid JPG file' do scenario 'works with valid JPG file' do
file_path = "#{Rails.root}/spec/fixtures/files/taipei.jpg" file_path = "#{Rails.root}/spec/fixtures/files/taipei.jpg"
expect_any_instance_of(LdapManager::UpdateAvatar) expect(LdapManager::UpdateAvatar)
.to receive(:replace_attribute).and_return(true) .to receive(:call).with(user: user)
.and_return(true)
expect(XmppSetAvatarJob)
.to receive(:perform_later).with(user: user)
.and_return(true)
visit setting_path(:profile) visit setting_path(:profile)
attach_file "Avatar", file_path attach_file "Avatar", file_path
@ -89,8 +94,12 @@ RSpec.describe 'Profile settings', type: :feature do
scenario 'works with valid PNG file' do scenario 'works with valid PNG file' do
file_path = "#{Rails.root}/spec/fixtures/files/bender.png" file_path = "#{Rails.root}/spec/fixtures/files/bender.png"
expect_any_instance_of(LdapManager::UpdateAvatar) expect(LdapManager::UpdateAvatar)
.to receive(:replace_attribute).and_return(true) .to receive(:call).with(user: user)
.and_return(true)
expect(XmppSetAvatarJob)
.to receive(:perform_later).with(user: user)
.and_return(true)
visit setting_path(:profile) visit setting_path(:profile)
attach_file "Avatar", file_path attach_file "Avatar", file_path