Add Mastodon API client, service for syncing avatars and display names #225
@ -35,8 +35,7 @@ class SettingsController < ApplicationController
 | 
			
		||||
 | 
			
		||||
      if @user.avatar_new.present?
 | 
			
		||||
        if store_user_avatar
 | 
			
		||||
          LdapManager::UpdateAvatar.call(user: @user)
 | 
			
		||||
          XmppSetAvatarJob.perform_later(user: @user)
 | 
			
		||||
          UserManager::UpdateAvatar.call(user: @user)
 | 
			
		||||
        else
 | 
			
		||||
          @validation_errors = @user.errors
 | 
			
		||||
          render :show, status: :unprocessable_entity and return
 | 
			
		||||
@ -193,7 +192,11 @@ class SettingsController < ApplicationController
 | 
			
		||||
 | 
			
		||||
    def store_user_avatar
 | 
			
		||||
      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.binmode
 | 
			
		||||
      tempfile.write(img_data)
 | 
			
		||||
@ -212,18 +215,4 @@ class SettingsController < ApplicationController
 | 
			
		||||
        @user.save
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,9 @@ module Settings
 | 
			
		||||
 | 
			
		||||
      field :mastodon_address_domain, type: :string,
 | 
			
		||||
        default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
 | 
			
		||||
 | 
			
		||||
      field :mastodon_auth_token, type: :string,
 | 
			
		||||
        default: ENV["MASTODON_AUTH_TOKEN"].presence
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -1,36 +1,22 @@
 | 
			
		||||
#
 | 
			
		||||
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
 | 
			
		||||
#
 | 
			
		||||
class BtcpayManagerService < ApplicationService
 | 
			
		||||
class BtcpayManagerService < RestApiService
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
    def base_url
 | 
			
		||||
      @base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
 | 
			
		||||
    end
 | 
			
		||||
  def base_url
 | 
			
		||||
    @base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
    def auth_token
 | 
			
		||||
      @auth_token ||= Setting.btcpay_auth_token
 | 
			
		||||
    end
 | 
			
		||||
  def auth_token
 | 
			
		||||
    @auth_token ||= Setting.btcpay_auth_token
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
    def headers
 | 
			
		||||
      {
 | 
			
		||||
        "Content-Type" => "application/json",
 | 
			
		||||
        "Accept" => "application/json",
 | 
			
		||||
        "Authorization" => "token #{auth_token}"
 | 
			
		||||
      }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def endpoint_url(path)
 | 
			
		||||
      "#{base_url}/#{path.gsub(/^\//, '')}"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def get(path, params = {})
 | 
			
		||||
      res = Faraday.get endpoint_url(path), params, headers
 | 
			
		||||
      JSON.parse(res.body)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def post(path, payload)
 | 
			
		||||
      res = Faraday.post endpoint_url(path), payload.to_json, headers
 | 
			
		||||
      JSON.parse(res.body)
 | 
			
		||||
    end
 | 
			
		||||
  def headers
 | 
			
		||||
    {
 | 
			
		||||
      "Content-Type" => "application/json",
 | 
			
		||||
      "Accept" => "application/json",
 | 
			
		||||
      "Authorization" => "token #{auth_token}"
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,11 @@ module LdapManager
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def call
 | 
			
		||||
      replace_attribute @dn, :displayName, @display_name
 | 
			
		||||
      if @display_name.present?
 | 
			
		||||
        replace_attribute @dn, :displayName, @display_name
 | 
			
		||||
      else
 | 
			
		||||
        delete_attribute @dn, :displayName
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								app/services/mastodon_manager/fetch_user.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/services/mastodon_manager/fetch_user.rb
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										14
									
								
								app/services/mastodon_manager/find_user.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/services/mastodon_manager/find_user.rb
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										64
									
								
								app/services/mastodon_manager/sync_account_profiles.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								app/services/mastodon_manager/sync_account_profiles.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
module MastodonManager
 | 
			
		||||
  class SyncAccountProfiles < MastodonManagerService
 | 
			
		||||
    def initialize(direction: "down", overwrite: false, user: nil)
 | 
			
		||||
      @direction = direction
 | 
			
		||||
      @overwrite = overwrite
 | 
			
		||||
      @user = user
 | 
			
		||||
 | 
			
		||||
      if @direction != "down"
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def call
 | 
			
		||||
      if @user
 | 
			
		||||
        Rails.logger.debug { "Syncing account profile for user #{@user.cn} (direction: #{@direction}, overwrite: #{@overwrite})"}
 | 
			
		||||
        users = User.where(cn: @user.cn)
 | 
			
		||||
      else
 | 
			
		||||
        Rails.logger.debug { "Syncing account profiles (direction: #{@direction}, overwrite: #{@overwrite})"}
 | 
			
		||||
        users = User
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      users.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: mastodon_user.dig(:account, :id).to_i
 | 
			
		||||
          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
 | 
			
		||||
							
								
								
									
										22
									
								
								app/services/mastodon_manager_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/services/mastodon_manager_service.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
#
 | 
			
		||||
# API Docs: https://docs.joinmastodon.org/methods/
 | 
			
		||||
#
 | 
			
		||||
class MastodonManagerService < RestApiService
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def base_url
 | 
			
		||||
    @base_url ||= "#{Setting.mastodon_public_url}/api"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def auth_token
 | 
			
		||||
    @auth_token ||= Setting.mastodon_auth_token
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def headers
 | 
			
		||||
    {
 | 
			
		||||
      "Content-Type" => "application/json",
 | 
			
		||||
      "Accept" => "application/json",
 | 
			
		||||
      "Authorization" => "Bearer #{auth_token}"
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										27
									
								
								app/services/rest_api_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/services/rest_api_service.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
class RestApiService < ApplicationService
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def base_url
 | 
			
		||||
    raise NotImplementedError
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def headers
 | 
			
		||||
    raise NotImplementedError
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def endpoint_url(path)
 | 
			
		||||
    "#{base_url}/#{path.gsub(/^\//, '')}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get(path, params = {})
 | 
			
		||||
    res = Faraday.get endpoint_url(path), params, headers
 | 
			
		||||
    # TODO handle unsuccessful responses with no valid JSON body
 | 
			
		||||
    JSON.parse(res.body)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def post(path, payload)
 | 
			
		||||
    res = Faraday.post endpoint_url(path), payload.to_json, headers
 | 
			
		||||
    # TODO handle unsuccessful responses with no valid JSON body
 | 
			
		||||
    JSON.parse(res.body)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										42
									
								
								app/services/user_manager/import_remote_avatar.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/services/user_manager/import_remote_avatar.rb
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										21
									
								
								app/services/user_manager/process_avatar.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/services/user_manager/process_avatar.rb
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										15
									
								
								app/services/user_manager/update_avatar.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/services/user_manager/update_avatar.rb
									
									
									
									
									
										Normal 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
 | 
			
		||||
@ -16,5 +16,10 @@
 | 
			
		||||
          key: :mastodon_address_domain,
 | 
			
		||||
          title: "User address domain"
 | 
			
		||||
        ) %>
 | 
			
		||||
    <%= render FormElements::FieldsetResettableSettingComponent.new(
 | 
			
		||||
          key: :mastodon_auth_token,
 | 
			
		||||
          type: :password,
 | 
			
		||||
          title: "API auth token"
 | 
			
		||||
        ) %>
 | 
			
		||||
  <% end %>
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
@ -89,14 +89,42 @@
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
    <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">
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>Avatar</th>
 | 
			
		||||
            <td>
 | 
			
		||||
              <% if @ldap_avatar.present? %>
 | 
			
		||||
                JPEG size: <%= @ldap_avatar.size %>
 | 
			
		||||
                JPEG size: <%= number_to_human_size(@ldap_avatar.size) %>
 | 
			
		||||
              <% else %>
 | 
			
		||||
                —
 | 
			
		||||
              <% end %>
 | 
			
		||||
 | 
			
		||||
@ -52,7 +52,7 @@
 | 
			
		||||
  <section class="!pt-8 sm:!pt-12">
 | 
			
		||||
    <h3>OpenPGP</h3>
 | 
			
		||||
    <ul role="list">
 | 
			
		||||
      <% example_link = link_to "example", "https://accounts.kosmos.org/.well-known/openpgpkey/hu/yuca4ky39mhwkjo78qb8zjgbfj1hg3yf.txt?l=jimmy",
 | 
			
		||||
      <% example_link = link_to "example", "https://kosmos.org/.well-known/openpgpkey/hu/yuca4ky39mhwkjo78qb8zjgbfj1hg3yf.txt?l=jimmy",
 | 
			
		||||
          target: "_blank", class: "text-gray-500 underline" %>
 | 
			
		||||
      <%= render FormElements::FieldsetComponent.new(
 | 
			
		||||
            title: "Public key",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								db/migrate/20250517105755_add_mastodon_id_to_users.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20250517105755_add_mastodon_id_to_users.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
class AddMastodonIdToUsers < ActiveRecord::Migration[8.0]
 | 
			
		||||
  def change
 | 
			
		||||
    add_column :users, :mastodon_id, :bigint
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
#
 | 
			
		||||
# It's strongly recommended that you check this file into your version control system.
 | 
			
		||||
 | 
			
		||||
ActiveRecord::Schema[8.0].define(version: 2025_05_06_154628) do
 | 
			
		||||
ActiveRecord::Schema[8.0].define(version: 2025_05_17_105755) do
 | 
			
		||||
  create_table "active_storage_attachments", force: :cascade do |t|
 | 
			
		||||
    t.string "name", null: false
 | 
			
		||||
    t.string "record_type", null: false
 | 
			
		||||
@ -133,6 +133,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_06_154628) do
 | 
			
		||||
    t.string "pgp_fpr"
 | 
			
		||||
    t.string "lndhub_username"
 | 
			
		||||
    t.text "lndhub_password"
 | 
			
		||||
    t.bigint "mastodon_id"
 | 
			
		||||
    t.index ["email"], name: "index_users_on_email", unique: true
 | 
			
		||||
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								docs/dev/mastodon.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								docs/dev/mastodon.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
# Mastodon
 | 
			
		||||
 | 
			
		||||
## API access
 | 
			
		||||
 | 
			
		||||
(Optional)
 | 
			
		||||
 | 
			
		||||
* Log in to your Mastodon instance with an admin account
 | 
			
		||||
* Create a new OAuth application (Settings -> Development)
 | 
			
		||||
* Select `admin:read` access in the permissions
 | 
			
		||||
* 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
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user