diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb
index ef03455..a44f78c 100644
--- a/app/controllers/settings_controller.rb
+++ b/app/controllers/settings_controller.rb
@@ -36,6 +36,7 @@ class SettingsController < ApplicationController
if @user.avatar_new.present?
if store_user_avatar
LdapManager::UpdateAvatar.call(user: @user)
+ XmppSetAvatarJob.perform_later(user: @user)
else
@validation_errors = @user.errors
render :show, status: :unprocessable_entity and return
diff --git a/app/jobs/xmpp_set_avatar_job.rb b/app/jobs/xmpp_set_avatar_job.rb
new file mode 100644
index 0000000..642225c
--- /dev/null
+++ b/app/jobs/xmpp_set_avatar_job.rb
@@ -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)
+
+ [
+ """
+
+
+
+ -
+ #{base64_data}
+
+
+
+
+ """.strip,
+ """
+
+
+
+ -
+
+
+
+
+
+
+
+ """.strip,
+ ]
+ end
+end
diff --git a/app/services/ejabberd_api_client.rb b/app/services/ejabberd_api_client.rb
index a0a8b87..df22d25 100644
--- a/app/services/ejabberd_api_client.rb
+++ b/app/services/ejabberd_api_client.rb
@@ -4,16 +4,14 @@ class EjabberdApiClient
end
def post(endpoint, payload)
- res = Faraday.post("#{@base_url}/#{endpoint}", payload.to_json,
- "Content-Type" => "application/json")
-
- if res.status != 200
- Rails.logger.error "[ejabberd] API request failed:"
- Rails.logger.error res.body
- #TODO Send custom event to Sentry
- end
+ Faraday.post "#{@base_url}/#{endpoint}", payload.to_json,
+ "Content-Type" => "application/json"
end
+ #
+ # API endpoints
+ #
+
def add_rosteritem(payload)
post "add_rosteritem", payload
end
@@ -22,8 +20,31 @@ class EjabberdApiClient
post "send_message", payload
end
+ def send_stanza(payload)
+ post "send_stanza", payload
+ end
+
+ def get_vcard2(user, name, subname)
+ payload = {
+ user: user.cn, host: user.ou,
+ name: name, subname: subname
+ }
+ post "get_vcard2", payload
+ end
+
+ def private_get(user, element_name, namespace)
+ payload = {
+ user: user.cn, host: user.ou,
+ element: element_name, ns: namespace
+ }
+ post "private_get", payload
+ end
+
def private_set(user, content)
- payload = { user: user.cn, host: user.ou, element: content }
+ payload = {
+ user: user.cn, host: user.ou,
+ element: content
+ }
post "private_set", payload
end
end
diff --git a/spec/features/settings/profile_spec.rb b/spec/features/settings/profile_spec.rb
index 95a4798..52932b4 100644
--- a/spec/features/settings/profile_spec.rb
+++ b/spec/features/settings/profile_spec.rb
@@ -46,6 +46,8 @@ RSpec.describe 'Profile settings', type: :feature do
feature "Update avatar" do
scenario "fails with validation error for wrong content type" do
+ expect(LdapManager::UpdateAvatar).not_to receive(:call)
+
visit setting_path(:profile)
attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/bitcoin.pdf"
click_button "Save"
@@ -57,8 +59,7 @@ RSpec.describe 'Profile settings', type: :feature do
end
scenario "fails with validation error for file size too large" do
- expect_any_instance_of(LdapManager::UpdateAvatar)
- .not_to receive(:replace_attribute).and_return(true)
+ expect(LdapManager::UpdateAvatar).not_to receive(:call)
visit setting_path(:profile)
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
file_path = "#{Rails.root}/spec/fixtures/files/taipei.jpg"
- expect_any_instance_of(LdapManager::UpdateAvatar)
- .to receive(:replace_attribute).and_return(true)
+ expect(LdapManager::UpdateAvatar)
+ .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)
attach_file "Avatar", file_path
@@ -89,8 +94,12 @@ RSpec.describe 'Profile settings', type: :feature do
scenario 'works with valid PNG file' do
file_path = "#{Rails.root}/spec/fixtures/files/bender.png"
- expect_any_instance_of(LdapManager::UpdateAvatar)
- .to receive(:replace_attribute).and_return(true)
+ expect(LdapManager::UpdateAvatar)
+ .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)
attach_file "Avatar", file_path