70 Commits

Author SHA1 Message Date
4545c3ba19 Merge branch 'feature/mastodon_api' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 18:12:57 +04:00
bfa1514181 Update doc
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 18:11:17 +04:00
f0846308da Only update other avatars in one place
All checks were successful
continuous-integration/drone/push Build is passing
Prevent future mistakes
2025-05-17 18:02:13 +04:00
b3b7fe6359 Don't queue job when service isn't enabled 2025-05-17 18:02:13 +04:00
f0b541ee50 Add avatar to admin user page 2025-05-17 18:02:13 +04:00
df9077e3c1 Sync Mastodon IDs/profiles to local accounts
Add a new service to import some data from Mastodon accounts:

* Find users by username, store Mastodon account ID in local db when
  found
* Import display name (don't overwrite existing)
* Import avatar (don't overwrite existing)
2025-05-17 18:02:07 +04:00
74666130c0 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 17:30:30 +04:00
7c43a4d919 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 16:01:31 +04:00
520552ec70 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 15:38:20 +04:00
307095bfd0 WTF
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 22:21:33 +04:00
ae7291d4f1 Merge branch 'feature/ejabberd_pep' into live
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-15 22:07:26 +04:00
322ca98da9 Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 20:05:30 +04:00
66f3950b83 Add job for setting avatar via XMPP 2025-05-15 20:04:55 +04:00
4e115eb514 Merge branch 'feature/ejabberd_pep' into live
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-15 19:50:30 +04:00
fc9b471a10 Add job for setting avatar via XMPP 2025-05-15 19:50:05 +04:00
b33e47e60f Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:54:25 +04:00
9878e4a3e8 Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:38:58 +04:00
9c35323bcd Return response for ejabberd API calls 2025-05-15 12:38:40 +04:00
e2716d94c0 Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:22:29 +04:00
9394f649a6 Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:02:05 +04:00
41b9cb722b Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 11:48:10 +04:00
f1c13d7bd9 Add private_get to ejabberd service
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 11:47:23 +04:00
c6cb9caa6d Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-14 18:58:38 +04:00
208977177a Merge branch 'feature/user_avatars' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-14 15:40:47 +04:00
9a406b8381 Merge branch 'feature/user_avatars' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-14 14:44:14 +04:00
4c972bfe7a Merge branch 'feature/user_avatars' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-12 16:40:44 +04:00
0191d248f8 Merge branch 'feature/user_avatars' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-12 15:11:39 +04:00
710afb6c78 Merge branch 'chore/215-configs' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-06 20:14:42 +04:00
69e9662133 Merge branch 'chore/215-configs' into live
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-06 20:01:15 +04:00
a73d0f29bd Merge branch 'chore/215-configs' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-06 19:53:52 +04:00
7836805570 Merge branch 'chore/215-configs' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-06 19:04:03 +04:00
1fc434cb3a Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-05 17:39:06 +04:00
dad7d5195e Merge branch 'chore/db_configs' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-05 15:28:16 +04:00
596cad34da Merge branch 'chore/upgrade_rails' into live 2025-04-29 17:25:25 +04:00
aaee5cb4ed Merge branch 'chore/upgrade_rails' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-28 17:56:57 +04:00
ebaca5ba65 Merge branch 'chore/upgrade_rails' into live 2025-04-28 15:58:17 +04:00
b6bcfa2ee3 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-18 14:54:01 +04:00
9082ee45d8 Merge branch 'master' into live 2025-01-02 08:32:15 -05:00
29264aad98 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-16 13:33:35 +02:00
259b51a95e Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-24 21:40:25 +02:00
fb369530e3 Merge branch 'master' into live
Some checks failed
continuous-integration/drone/push Build is failing
2024-09-14 17:18:09 +02:00
5dc10a4d33 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 16:46:27 +02:00
2297c68046 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 16:41:01 +02:00
b82ab45c99 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 14:57:35 +02:00
d12c63db26 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-10 16:07:24 +02:00
e6a9ef84ce Merge branch 'master' into live
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-19 15:13:46 +02:00
b7e91344a0 Merge branch 'master' into live
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-19 14:48:35 +02:00
0f07e32781 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:49:29 +02:00
1311b5ed6a Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:46:00 +02:00
12f82061e8 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:42:32 +02:00
a07b4369ab Hide njump link for users without pubkey
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-23 17:33:34 +02:00
2605c06807 Merge branch 'feature/admin_pages' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-23 17:30:22 +02:00
1db768fb15 Merge branch 'feature/admin_pages' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-23 17:27:03 +02:00
8a7403df32 Merge branch 'feature/own_relay' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 15:52:03 +02:00
f0295fef7a Merge branch 'feature/own_relay' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 15:28:55 +02:00
090affd304 Merge branch 'feature/own_relay' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 14:51:20 +02:00
bafddd436b Merge branch 'feature/own_relay' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 13:59:59 +02:00
560f193c4b Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 13:56:11 +02:00
8aabbad5bb Merge branch 'feature/strfry_zap_receipts' into live 2024-06-20 13:56:00 +02:00
ba8d21eb7a Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-09 13:29:57 +02:00
53df455d53 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-09 13:17:51 +02:00
9f1af3a9aa Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-07 14:13:53 +02:00
1d09008ce2 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-04 17:45:13 +02:00
57c5317c38 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-21 18:29:13 +02:00
41bd920060 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-21 18:09:09 +02:00
0815fa6040 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 17:07:58 +02:00
af0e99aa50 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 16:55:10 +02:00
f05eec5255 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 16:48:28 +02:00
66ca2dc6b0 Merge branch 'feature/173-nostr_ldap' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 13:44:24 +02:00
800183e9da Increase sidekiq concurrency in prod
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 13:44:01 +02:00
13 changed files with 253 additions and 68 deletions

View File

@@ -35,8 +35,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) UserManager::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
@@ -193,7 +192,11 @@ class SettingsController < ApplicationController
def store_user_avatar def store_user_avatar
io = @user.avatar_new.tempfile io = @user.avatar_new.tempfile
img_data = process_avatar(io) img_data = UserManager::ProcessAvatar.call(io: io)
if img_data.blank?
@user.errors.add(:avatar, "failed to process file")
false
end
tempfile = Tempfile.create tempfile = Tempfile.create
tempfile.binmode tempfile.binmode
tempfile.write(img_data) tempfile.write(img_data)
@@ -212,18 +215,4 @@ class SettingsController < ApplicationController
@user.save @user.save
end end
end end
def process_avatar(io)
processed = ImageProcessing::Vips
.source(io)
.resize_to_fill(400, 400)
.saver(strip: true)
.call
io.rewind
processed.read
rescue Vips::Error => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error { "Image processing failed for avatar: #{e.message}" }
nil
end
end end

View File

@@ -4,19 +4,19 @@
class BtcpayManagerService < RestApiService class BtcpayManagerService < RestApiService
private private
def base_url def base_url
@base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}" @base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
end end
def auth_token def auth_token
@auth_token ||= Setting.btcpay_auth_token @auth_token ||= Setting.btcpay_auth_token
end end
def headers def headers
{ {
"Content-Type" => "application/json", "Content-Type" => "application/json",
"Accept" => "application/json", "Accept" => "application/json",
"Authorization" => "token #{auth_token}" "Authorization" => "token #{auth_token}"
} }
end end
end end

View File

@@ -0,0 +1,12 @@
module MastodonManager
class FetchUser < MastodonManagerService
def initialize(mastodon_id:)
@mastodon_id = mastodon_id
end
def call
user = get "v1/admin/accounts/#{@mastodon_id}"
user.with_indifferent_access
end
end
end

View File

@@ -0,0 +1,14 @@
module MastodonManager
class FindUser < MastodonManagerService
def initialize(username:)
@username = username
end
def call
users = get "v2/admin/accounts?username=#{@username}&origin=local"
users = users.map { |u| u.with_indifferent_access }
# Results may contain partial matches
users.find { |u| u.dig(:username).downcase == @username.downcase }
end
end
end

View File

@@ -0,0 +1,57 @@
module MastodonManager
class SyncAccountProfiles < MastodonManagerService
def initialize(direction: "down", overwrite: false)
@direction = direction
@overwrite = overwrite
if @direction != "down"
raise NotImplementedError
end
end
def call
Rails.logger.debug { "Syncing account profiles (direction: #{@direction}, overwrite: #{@overwrite})"}
User.find_each do |user|
if user.mastodon_id.blank?
mastodon_user = MastodonManager::FindUser.call username: user.cn
if mastodon_user
Rails.logger.debug { "Setting mastodon_id for user #{user.cn}" }
user.update! mastodon_id: @user[:id]
else
Rails.logger.debug { "No Mastodon user found for username #{user.cn}" }
next
end
end
next if user.avatar.attached? && user.display_name.present?
unless mastodon_user
Rails.logger.debug { "Fetching Mastodon account with ID #{user.mastodon_id} for #{user.cn}" }
mastodon_user = MastodonManager::FetchUser.call mastodon_id: user.mastodon_id
end
if user.display_name.blank?
if mastodon_display_name = mastodon_user.dig(:account, :display_name)
Rails.logger.debug { "Setting display name for user #{user.cn} from Mastodon" }
LdapManager::UpdateDisplayName.call(
dn: user.dn, display_name: mastodon_display_name
)
end
end
if !user.avatar.attached?
if avatar_url = mastodon_user.dig(:account, :avatar_static)
Rails.logger.debug { "Importing Mastodon avatar for user #{user.cn}" }
UserManager::ImportRemoteAvatar.call(
user: user, avatar_url: avatar_url
)
end
end
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error e
end
end
end
end

View File

@@ -4,19 +4,19 @@
class MastodonManagerService < RestApiService class MastodonManagerService < RestApiService
private private
def base_url def base_url
@base_url ||= "#{Setting.mastodon_public_url}/api" @base_url ||= "#{Setting.mastodon_public_url}/api"
end end
def auth_token def auth_token
@auth_token ||= Setting.mastodon_auth_token @auth_token ||= Setting.mastodon_auth_token
end end
def headers def headers
{ {
"Content-Type" => "application/json", "Content-Type" => "application/json",
"Accept" => "application/json", "Accept" => "application/json",
"Authorization" => "Bearer #{auth_token}" "Authorization" => "Bearer #{auth_token}"
} }
end end
end end

View File

@@ -1,27 +1,27 @@
class RestApiService < ApplicationService class RestApiService < ApplicationService
private private
def base_url def base_url
raise NotImplementedError raise NotImplementedError
end end
def headers def headers
raise NotImplementedError raise NotImplementedError
end end
def endpoint_url(path) def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}" "#{base_url}/#{path.gsub(/^\//, '')}"
end end
def get(path, params = {}) def get(path, params = {})
res = Faraday.get endpoint_url(path), params, headers res = Faraday.get endpoint_url(path), params, headers
# TODO handle unsuccessful responses with no valid JSON body # TODO handle unsuccessful responses with no valid JSON body
JSON.parse(res.body) JSON.parse(res.body)
end end
def post(path, payload) def post(path, payload)
res = Faraday.post endpoint_url(path), payload.to_json, headers res = Faraday.post endpoint_url(path), payload.to_json, headers
# TODO handle unsuccessful responses with no valid JSON body # TODO handle unsuccessful responses with no valid JSON body
JSON.parse(res.body) JSON.parse(res.body)
end end
end end

View File

@@ -0,0 +1,42 @@
module UserManager
class ImportRemoteAvatar < UserManagerService
def initialize(user:, avatar_url:)
@user = user
@avatar_url = avatar_url
end
def call
if import_remote_avatar
UserManager::UpdateAvatar.call(user: @user)
end
end
private
def import_remote_avatar
tempfile = Down.download(@avatar_url)
content_type = tempfile.content_type
unless %w[image/jpeg image/png].include?(content_type)
Rails.logger.warn { "Wrong content type of remote avatar for user #{user.cn}: '#{content_type}'" }
return false
end
img_data = UserManager::ProcessAvatar.call(io: tempfile)
tempfile = Tempfile.create
tempfile.binmode
tempfile.write(img_data)
tempfile.rewind
hash = Digest::SHA256.hexdigest(img_data)
ext = content_type == "image/png" ? "png" : "jpg"
filename = "#{hash}.#{ext}"
key = "users/#{@user.cn}/avatars/#{filename}"
@user.avatar.attach io: tempfile, key: key, filename: filename
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.warn "Importing remote avatar failed: \"#{e.message}\""
false
end
end
end

View File

@@ -0,0 +1,21 @@
module UserManager
class ProcessAvatar < UserManagerService
def initialize(io:)
@io = io
end
def call
processed = ImageProcessing::Vips
.source(@io)
.resize_to_fill(400, 400)
.saver(strip: true)
.call
@io.rewind
processed.read
rescue Vips::Error => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.warn { "Image processing failed for avatar: #{e.message}" }
nil
end
end
end

View File

@@ -0,0 +1,15 @@
module UserManager
class UpdateAvatar < UserManagerService
def initialize(user:)
@user = user
end
def call
LdapManager::UpdateAvatar.call(user: @user)
if Setting.ejabberd_enabled?
XmppSetAvatarJob.perform_later(user: @user)
end
end
end
end

View File

@@ -89,14 +89,42 @@
</section> </section>
<section class="sm:flex-1 sm:pt-0"> <section class="sm:flex-1 sm:pt-0">
<h3>LDAP</h3> <h3>Avatar</h3>
<% if @user.avatar.attached? %>
<table class="divided">
<tbody>
<tr>
<th class="align-top">Image</th>
<td class="align-top">
<%= image_tag image_url_for(@user.avatar), class: "h-20 w-20 rounded-lg" %>
</td>
</tr>
<tr>
<th>Content type</th>
<td>
<%= @user.avatar.content_type %>
</td>
</tr>
<tr>
<th>Size</th>
<td>
<%= number_to_human_size(@user.avatar.blob.byte_size) %>
</td>
</tr>
</tbody>
</table>
<% else %>
<p class="text-gray-500">No avatar uploaded</p>
<% end %>
<h3 class="mt-12">LDAP</h3>
<table class="divided"> <table class="divided">
<tbody> <tbody>
<tr> <tr>
<th>Avatar</th> <th>Avatar</th>
<td> <td>
<% if @ldap_avatar.present? %> <% if @ldap_avatar.present? %>
JPEG size: <%= @ldap_avatar.size %> JPEG size: <%= number_to_human_size(@ldap_avatar.size) %>
<% else %> <% else %>
&mdash; &mdash;
<% end %> <% end %>
@@ -239,7 +267,9 @@
) %> ) %>
</td> </td>
<td class="text-right"> <td class="text-right">
<% if @user.nostr_pubkey.present? %>
<%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %> <%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %>
<% end %>
</td> </td>
</tr> </tr>
<% end %> <% end %>

View File

@@ -1,4 +1,6 @@
:concurrency: 2 :concurrency: 2
production:
:concurrency: 10
:queues: :queues:
- default - default
- mailers - mailers

View File

@@ -4,7 +4,10 @@
(Optional) (Optional)
Log in to your Mastodon instance with an admin account and create a new * Log in to your Mastodon instance with an admin account
OAuth application: * Create a new OAuth application (Settings -> Development)
* Select `admin:read` access in the permissions
https://kosmos.social/settings/applications/new * After confirming, click on the application name in the list, and copy the
access token
* Configure it as `MASTODON_AUTH_TOKEN` via environment variable, or on
Admin -> Settings -> Services -> Mastodon