Post di RSVP del calendario

:information_source: Riepilogo Crea brevi risposte all’argomento per gli eventi RSVP
:hammer_and_wrench: Link Repository GitHub - mariodsantana/discourse-calendar-rsvp-posts
:open_book: Guida all’installazione Come installare i plugin in Discourse

Funzionalità

  • Notifiche in tempo reale - Poiché gli eventi RSVP creano ora un post di risposta nell’argomento, ogni RSVP attiva le notifiche per gli osservatori dell’argomento
  • Minimo ingombro - I post di notifica precedenti vengono eliminati, quindi ci sono al massimo 2 post in qualsiasi momento (1 cronologia + 1 notifica più recente)
  • Cronologia completa - Tutta l’attività RSVP viene conservata con i timestamp in ordine cronologico
  • Scoraggia il continuo cambiamento di idea - I timestamp rendono visibili le modifiche ripetute dell’RSVP

Configurazione

Le impostazioni del sito pertinenti (in Admin > Impostazioni > Plugin) configurano principalmente quali modifiche RSVP dovrebbero attivare un post: partecipazione, interessato, non partecipazione o rimozione della partecipazione esistente, e se attivare in caso di modifiche RSVP a eventi che iniziano nel passato.

L’impostazione rimanente attiva la modalità cronologia. Con la modalità cronologia disattivata, rimane solo il post di notifica più recente nell’argomento. Con la modalità cronologia attivata, il plugin gestisce un commento di “cronologia” aggiuntivo come segue:

  • Al primo RSVP, crea un semplice post di notifica che annuncia l’RSVP
  • Al secondo RSVP, trasforma il primo post in un post di cronologia con timestamp, quindi crea un nuovo post di notifica
  • Agli RSVP successivi, aggiunge l’RSVP al post di cronologia con timestamp, elimina il post di notifica precedente e ne crea uno nuovo

Impostazioni

Nome Descrizione
calendar_rsvp_posts_on_new_going pubblica per il nuovo “Partecipante”
calendar_rsvp_posts_on_new_interested pubblica per il nuovo “Interessato”
calendar_rsvp_posts_on_new_not_going pubblica per il nuovo “Non parteciperò”
calendar_rsvp_posts_on_removed_rsvp pubblica quando un RSVP viene rimosso
calendar_rsvp_posts_allow_past_events se pubblicare per eventi che iniziano nel passato
calendar_rsvp_posts_enable_history mantiene un post di cronologia con timestamp (predefinito: abilitato)
6 Mi Piace

Questo migliorerà la gestione degli eventi per molti!! Grazie mille per averlo fornito!!

1 Mi Piace

Grazie per il plugin :+1:

solo una piccola domanda: è possibile tradurre le risposte in un file?

Mi piace questa idea. Posso aggiungere un link al post della cronologia, che scaricherebbe un file CSV. È questo il tipo di cosa a cui stai pensando?

Mi sono espresso male :sweat_smile:, volevo sapere come tradurre le notifiche di risposta perché sono in inglese e vorrei metterle in italiano.

1 Mi Piace

LOL - Ora ho capito. Al momento il testo inglese è direttamente nel codice sorgente. Sono nuovo di Discourse, ma sono sicuro che ci sia un modo standard per rendere un plugin traducibile. Ci darò un’occhiata quando avrò un po’ di tempo.

Probabilmente implementerò anche il link CSV. :wink:

1 Mi Piace

Nessun problema :wink: . Prenditi il tuo tempo, non ho fretta

OK! Nuova versione disponibile nel repository. Non solo con l’internazionalizzazione (i18n), ma ho anche trovato e corretto un bug che impediva la pubblicazione degli aggiornamenti quando qualcuno annullava la propria RSVP.

Fatemi sapere se riscontrate problemi!

1 Mi Piace

Ciao, avevo un problema riguardante gli utenti quando partecipano o meno a un evento: nell’evento apparivano due avatar invece di uno, e il problema si risolveva aggiornando la pagina, ma creava confusione tra gli utenti. Ho modificato il codice dal mio lato, non so se possa interessarti.

# frozen_string_literal: true
# name: discourse-calendar-rsvp-posts
# about: Crea risposte brevi ai topic per gli eventi RSVP
# version: 0.4
# authors: Mario Santana

after_initialize do
 module ::CalendarRsvpPosts
   PLUGIN_NAME = "discourse-calendar-rsvp-posts"
   
   def self.history_marker
     I18n.t('calendar_rsvp_posts.markers.history')
   end
   
   def self.notification_marker
     I18n.t('calendar_rsvp_posts.markers.notification')
   end

   # Helper per decidere se ignorare questo evento
   def self.should_post_for_event?(event)
     return false if event.nil?
     return true if SiteSetting.calendar_rsvp_posts_allow_past_events
     return false if event.starts_at.nil?
     event.starts_at >= Time.current
   end

   # Trova eventuali post RSVP per un evento (storico o semplice notifica)
   def self.find_rsvp_posts(event)
     return [] if event.nil? || event.post.nil? || event.post.topic.nil?
     
     event.post.topic.posts
       .where(user_id: Discourse.system_user.id)
       .where("raw LIKE ? OR raw LIKE ?", 
              "%#{history_marker}%",
              "%#{notification_marker}%")
       .order(created_at: :asc)
   end

   # Trova specificamente il post dello storico
   def self.find_history_post(event)
     return nil if event.nil? || event.post.nil? || event.post.topic.nil?
     
     event.post.topic.posts
       .where(user_id: Discourse.system_user.id)
       .where("raw LIKE ?", "%#{history_marker}%")
       .order(created_at: :asc)
       .first
   end

   # Trova ed elimina tutti i post di notifica (ma non il post dello storico)
   def self.delete_notification_posts(event)
     return if event.nil? || event.post.nil? || event.post.topic.nil?
     
     notification_posts = event.post.topic.posts
       .where(user_id: Discourse.system_user.id)
       .where("raw LIKE ?", "%#{notification_marker}%")
     
     notification_posts.each do |post|
       begin
         PostDestroyer.new(Discourse.system_user, post, context: "pulizia calendar-rsvp-posts").destroy
       rescue StandardError => e
         Rails.logger.warn("calendar-rsvp-posts: impossibile eliminare il post di notifica #{post.id}: #{e}")
       end
     end
   end

   # Costruisce una riga di voce dello storico con timestamp
   def self.build_history_entry(username, action_label, extra_text = nil)
     timestamp = Time.current.strftime("%Y-%m-%d %H:%M UTC")
     entry = "- **#{timestamp}** - #{username} #{action_label}"
     entry += " (#{extra_text})" if extra_text.present?
     entry
   end

   # Costruisce o aggiorna il contenuto del post dello storico
   def self.build_history_raw(event, new_entry)
     event_title = (event.name.presence || event.post.topic.title).to_s
     parts = []
     parts << history_marker
     parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
     parts << ""
     parts << new_entry
     parts.join("\n")
   end

   # Aggiunge una nuova voce allo storico esistente
   def self.append_to_history(existing_raw, new_entry)
     lines = existing_raw.split("\n")
     header_end_idx = lines.index { |line| line.start_with?("### RSVP History") }
     
     if header_end_idx
       insert_idx = header_end_idx + 2
       lines.insert(insert_idx, new_entry)
     else
       lines << new_entry
     end
     
     lines.join("\n")
   end

   # Converte un semplice post di notifica nel formato storico
   def self.convert_to_history(simple_post, event)
     raw = simple_post.raw
     
     going_label = I18n.t('calendar_rsvp_posts.actions.going').gsub('(', '\\(').gsub(')', '\\)')
     interested_label = I18n.t('calendar_rsvp_posts.actions.interested').gsub('(', '\\(').gsub(')', '\\)')
     not_going_label = I18n.t('calendar_rsvp_posts.actions.not_going').gsub('(', '\\(').gsub(')', '\\)')
     removed_label = I18n.t('calendar_rsvp_posts.actions.removed').gsub('(', '\\(').gsub(')', '\\)')
     
     pattern = /\*\*([^\*]+)\s+(#{Regexp.escape(going_label)}|#{Regexp.escape(interested_label)}|#{Regexp.escape(not_going_label)}|#{Regexp.escape(removed_label)})\*\*/
     match = raw.match(pattern)
     
     if match
       username = match[1]
       action = match[2]
       
       extra_match = raw.match(/\.\s+([^.]+)\.$/)
       extra_text = extra_match ? extra_match[1] : nil
       
       timestamp = simple_post.created_at.strftime("%Y-%m-%d %H:%M UTC")
       first_entry = "- **#{timestamp}** - #{username} #{action}"
       first_entry += " (#{extra_text})" if extra_text.present?
       
       event_title = (event.name.presence || event.post.topic.title).to_s
       parts = []
       parts << history_marker
       parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
       parts << ""
       parts << first_entry
       parts.join("\n")
     else
       event_title = (event.name.presence || event.post.topic.title).to_s
       history_marker + "\n### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}\n\n" + raw
     end
   end

   # Costruisce un post di notifica
   def self.build_notification_raw(username, action_label, event, extra_text = nil)
     event_title = (event.name.presence || event.post.topic.title).to_s
     extra_text_formatted = extra_text.present? ? "#{extra_text} " : ""
     
     notification_raw = notification_marker + "\n"
     notification_raw += I18n.t('calendar_rsvp_posts.notification.template', 
                               event_title: event_title,
                               extra: extra_text_formatted,
                               username: username,
                               action: action_label)
     notification_raw
   end

   # Metodo logico centrale per gestire la creazione/aggiornamento dei post
   def self.publish_rsvp_update(event, username, action_label, extra_text = nil)
     if SiteSetting.calendar_rsvp_posts_enable_history
       history_post = find_history_post(event)
       all_rsvp_posts = find_rsvp_posts(event)
       new_entry = build_history_entry(username, action_label, extra_text)

       if all_rsvp_posts.empty?
         notification_raw = build_notification_raw(username, action_label, event, extra_text)
         PostCreator.create!(
           Discourse.system_user,
           topic_id: event.post.topic_id,
           raw: notification_raw,
           skip_validations: true
         )
       elsif history_post.nil?
         first_post = all_rsvp_posts.first
         history_raw = convert_to_history(first_post, event)
         history_raw = append_to_history(history_raw, new_entry)
         
         PostRevisor.new(first_post, event.post.topic).revise!(
           Discourse.system_user,
           raw: history_raw,
           skip_validations: true,
           skip_revision: false
         )

         notification_raw = build_notification_raw(username, action_label, event, extra_text)
         PostCreator.create!(
           Discourse.system_user,
           topic_id: event.post.topic_id,
           raw: notification_raw,
           skip_validations: true
         )
       else
         updated_raw = append_to_history(history_post.raw, new_entry)
         PostRevisor.new(history_post, event.post.topic).revise!(
           Discourse.system_user,
           raw: updated_raw,
           skip_validations: true,
           skip_revision: false
         )

         delete_notification_posts(event)

         notification_raw = build_notification_raw(username, action_label, event, extra_text)
         PostCreator.create!(
           Discourse.system_user,
           topic_id: event.post.topic_id,
           raw: notification_raw,
           skip_validations: true
         )
       end
     else
       all_rsvp_posts = find_rsvp_posts(event)
       
       all_rsvp_posts.each do |post|
         begin
           PostDestroyer.new(Discourse.system_user, post, context: "pulizia calendar-rsvp-posts").destroy
         rescue StandardError => e
           Rails.logger.warn("calendar-rsvp-posts: impossibile eliminare il post #{post.id}: #{e}")
         end
       end

       notification_raw = build_notification_raw(username, action_label, event, extra_text)
       PostCreator.create!(
         Discourse.system_user,
         topic_id: event.post.topic_id,
         raw: notification_raw,
         skip_validations: true
       )
     end
   end
 end

 # ==========================================
 # Definizione del Job in Background
 # ==========================================
 module ::Jobs
   class ProcessCalendarRsvpPost < ::Jobs::Base
     def execute(args)
       event_id = args[:event_id]
       username = args[:username]
       action_label = args[:action_label]
       extra_text = args[:extra_text]

       event = DiscoursePostEvent::Event.find_by(id: event_id)
       return unless event

       ::CalendarRsvpPosts.publish_rsvp_update(event, username, action_label, extra_text)
     end
   end
 end

 # ==========================================
 # Gestori degli Eventi
 # ==========================================

 # Gestore per creazione/aggiornamento della partecipazione
 proc_handler = proc do |invitee|
   begin
     event = invitee&.event
     next if event.nil?
     next unless CalendarRsvpPosts.should_post_for_event?(event)

     going_val = DiscoursePostEvent::Invitee.statuses[:going]
     interested_val = DiscoursePostEvent::Invitee.statuses[:interested]
     not_going_val = DiscoursePostEvent::Invitee.statuses[:not_going]

     new_status = invitee.status
     prev_status =
       if invitee.respond_to?(:previous_changes) && invitee.previous_changes["status"]
         invitee.previous_changes["status"][0]
       else
         nil
       end

     next if prev_status && prev_status == new_status

     username = invitee.user&.username || "qualcuno"
     action_label = nil

     if new_status == going_val && SiteSetting.calendar_rsvp_posts_on_new_going
       action_label = I18n.t('calendar_rsvp_posts.actions.going')
     elsif new_status == interested_val && SiteSetting.calendar_rsvp_posts_on_new_interested
       action_label = I18n.t('calendar_rsvp_posts.actions.interested')
     elsif new_status == not_going_val && SiteSetting.calendar_rsvp_posts_on_new_not_going
       action_label = I18n.t('calendar_rsvp_posts.actions.not_going')
     end

     next if action_label.nil?

     extra_text = nil
     if event.max_attendees.present?
       current_going = event.going_count
       prev_going =
         if prev_status == going_val && new_status != going_val
           current_going + 1
         elsif prev_status != going_val && new_status == going_val
           current_going - 1
         else
           current_going
         end

       was_full = prev_going >= event.max_attendees
       is_full = current_going >= event.max_attendees

       if was_full && !is_full
         extra_text = I18n.t('calendar_rsvp_posts.capacity.spots_available')
       elsif !was_full && is_full
         extra_text = I18n.t('calendar_rsvp_posts.capacity.now_full')
       end
     end

     # Metti in coda il job invece di bloccare la richiesta
     Jobs.enqueue(:process_calendar_rsvp_post, 
       event_id: event.id, 
       username: username, 
       action_label: action_label, 
       extra_text: extra_text
     )

   rescue StandardError => e
     Rails.logger.warn("calendar-rsvp-posts: errore del gestore: #{e}")
     Rails.logger.warn(e.backtrace.join("\n"))
   end
 end

 on(:discourse_calendar_post_event_invitee_status_changed, &proc_handler)

 # Gestore per eliminazioni esplicite degli invitati (rimozione RSVP)
 if defined?(DiscoursePostEvent::Invitee)
   DiscoursePostEvent::Invitee.class_eval do
     after_destroy do
       event = self.event
       
       should_process = event.present? &&
                        SiteSetting.calendar_rsvp_posts_on_removed_rsvp &&
                        (SiteSetting.calendar_rsvp_posts_allow_past_events || event.starts_at.nil? || event.starts_at >= Time.current)
       
       if should_process
         begin
           username = self.user&.username || "qualcuno"
           action_label = I18n.t('calendar_rsvp_posts.actions.removed')

           # Metti in coda il job invece di bloccare la richiesta
           Jobs.enqueue(:process_calendar_rsvp_post, 
             event_id: event.id, 
             username: username, 
             action_label: action_label,
             extra_text: nil
           )
           
         rescue StandardError => e
           Rails.logger.warn("calendar-rsvp-posts: errore del gestore after_destroy: #{e}")
           Rails.logger.warn(e.backtrace.join("\n"))
         end
       end
     end
   end
 end
end
1 Mi Piace

È un’ottima idea e capisco come possa creare confusione nell’interfaccia utente. Ho aggiunto un Mutex per evitare condizioni di gara e ho pubblicato una nuova versione del plugin.

Fammi sapere cosa ne pensi!

1 Mi Piace

dalla versione 2026.9.0-latest. Non ricevo più le iscrizioni agli eventi. Non so se sono l’unico a cui sta succedendo.

Penso di aver capito perché non funziona, dato che discourse calendar è stato sostituito da events

Ho modificato io il file plugin.rb per renderlo compatibile con la nuova versione di Discourse.

# frozen_string_literal: true
# name: discourse-calendar-rsvp-posts
# about: Create short topic replies for RSVP events
# version: 0.9
# authors: Mario Santana, gilles

after_initialize do
  module ::CalendarRsvpPosts
    PLUGIN_NAME = "discourse-calendar-rsvp-posts"

    def self.event_class
      "DiscourseEvents::Events::Event".safe_constantize ||
      "DiscourseEvents::Event".safe_constantize ||
      "DiscourseCalendar::Event".safe_constantize ||
      "DiscoursePostEvent::Event".safe_constantize
    end

    def self.invitee_class
      "DiscourseEvents::Events::Invitee".safe_constantize ||
      "DiscourseEvents::Invitee".safe_constantize ||
      "DiscourseCalendar::Invitee".safe_constantize ||
      "DiscoursePostEvent::Invitee".safe_constantize
    end
    
    def self.history_marker
      I18n.t('calendar_rsvp_posts.markers.history')
    end
    
    def self.notification_marker
      I18n.t('calendar_rsvp_posts.markers.notification')
    end

    # Helper to decide if we should ignore this event
    def self.should_post_for_event?(event)
      return false if event.nil?
      return true if SiteSetting.calendar_rsvp_posts_allow_past_events
      return false if event.starts_at.nil?
      event.starts_at >= Time.current
    end

    # Find any RSVP post for an event (history or simple notification)
    def self.find_rsvp_posts(event)
      return [] if event.nil? || event.post.nil? || event.post.topic.nil?
      
      event.post.topic.posts
        .where(user_id: Discourse.system_user.id)
        .where("raw LIKE ? OR raw LIKE ?", 
               "%#{history_marker}%",
               "%#{notification_marker}%")
        .order(created_at: :asc)
    end

    # Find the history post specifically
    def self.find_history_post(event)
      return nil if event.nil? || event.post.nil? || event.post.topic.nil?
      
      event.post.topic.posts
        .where(user_id: Discourse.system_user.id)
        .where("raw LIKE ?", "%#{history_marker}%")
        .order(created_at: :asc)
        .first
    end

    # Find and delete all notification posts (but not history post)
    def self.delete_notification_posts(event)
      return if event.nil? || event.post.nil? || event.post.topic.nil?
      
      notification_posts = event.post.topic.posts
        .where(user_id: Discourse.system_user.id)
        .where("raw LIKE ?", "%#{notification_marker}%")
      
      notification_posts.each do |post|
        begin
          PostDestroyer.new(Discourse.system_user, post, context: "calendar-rsvp-posts cleanup").destroy
        rescue StandardError => e
          Rails.logger.warn("calendar-rsvp-posts: failed to delete notification post #{post.id}: #{e}")
        end
      end
    end

    # Build a history entry line with timestamp
    def self.build_history_entry(username, action_label, extra_text = nil)
      timestamp = Time.current.strftime("%Y-%m-%d %H:%M UTC")
      entry = "- **#{timestamp}** - #{username} #{action_label}"
      entry += " (#{extra_text})" if extra_text.present?
      entry
    end

    # Build or update the history post content
    def self.build_history_raw(event, new_entry)
      event_title = (event.name.presence || event.post.topic.title).to_s
      parts = []
      parts << history_marker
      parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
      parts << ""
      parts << new_entry
      parts.join("\n")
    end

    # Append new entry to existing history
    def self.append_to_history(existing_raw, new_entry)
      lines = existing_raw.split("\n")
      header_end_idx = lines.index { |line| line.start_with?("### RSVP History") }
      
      if header_end_idx
        insert_idx = header_end_idx + 2
        lines.insert(insert_idx, new_entry)
      else
        lines << new_entry
      end
      
      lines.join("\n")
    end

    # Convert a simple notification post into history format
    def self.convert_to_history(simple_post, event)
      raw = simple_post.raw
      
      going_label = I18n.t('calendar_rsvp_posts.actions.going').gsub('(', '\\(').gsub(')', '\\)')
      interested_label = I18n.t('calendar_rsvp_posts.actions.interested').gsub('(', '\\(').gsub(')', '\\)')
      not_going_label = I18n.t('calendar_rsvp_posts.actions.not_going').gsub('(', '\\(').gsub(')', '\\)')
      removed_label = I18n.t('calendar_rsvp_posts.actions.removed').gsub('(', '\\(').gsub(')', '\\)')
      
      pattern = /\*\*([^\*]+)\s+(#{Regexp.escape(going_label)}|#{Regexp.escape(interested_label)}|#{Regexp.escape(not_going_label)}|#{Regexp.escape(removed_label)})\*\*/
      match = raw.match(pattern)
      
      if match
        username = match[1]
        action = match[2]
        
        extra_match = raw.match(/\.\s+([^.]+)\.$/)
        extra_text = extra_match ? extra_match[1] : nil
        
        timestamp = simple_post.created_at.strftime("%Y-%m-%d %H:%M UTC")
        first_entry = "- **#{timestamp}** - #{username} #{action}"
        first_entry += " (#{extra_text})" if extra_text.present?
        
        event_title = (event.name.presence || event.post.topic.title).to_s
        parts = []
        parts << history_marker
        parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
        parts << ""
        parts << first_entry
        parts.join("\n")
      else
        event_title = (event.name.presence || event.post.topic.title).to_s
        history_marker + "\n### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}\n\n" + raw
      end
    end

    # Build a notification post
    def self.build_notification_raw(username, action_label, event, extra_text = nil)
      event_title = (event.name.presence || event.post.topic.title).to_s
      extra_text_formatted = extra_text.present? ? "#{extra_text} " : ""
      
      notification_raw = notification_marker + "\n"
      notification_raw += I18n.t('calendar_rsvp_posts.notification.template', 
                                 event_title: event_title,
                                 extra: extra_text_formatted,
                                 username: username,
                                 action: action_label)
      notification_raw
    end

    # Centrale logic method to handle the creation/update of posts
    def self.publish_rsvp_update(event, username, action_label, extra_text = nil)
      if SiteSetting.calendar_rsvp_posts_enable_history
        history_post = find_history_post(event)
        all_rsvp_posts = find_rsvp_posts(event)
        new_entry = build_history_entry(username, action_label, extra_text)

        if all_rsvp_posts.empty?
          notification_raw = build_notification_raw(username, action_label, event, extra_text)
          PostCreator.create!(
            Discourse.system_user,
            topic_id: event.post.topic_id,
            raw: notification_raw,
            skip_validations: true
          )
        elsif history_post.nil?
          first_post = all_rsvp_posts.first
          history_raw = convert_to_history(first_post, event)
          history_raw = append_to_history(history_raw, new_entry)
          
          PostRevisor.new(first_post, event.post.topic).revise!(
            Discourse.system_user,
            raw: history_raw,
            skip_validations: true,
            skip_revision: false
          )

          notification_raw = build_notification_raw(username, action_label, event, extra_text)
          PostCreator.create!(
            Discourse.system_user,
            topic_id: event.post.topic_id,
            raw: notification_raw,
            skip_validations: true
          )
        else
          updated_raw = append_to_history(history_post.raw, new_entry)
          PostRevisor.new(history_post, event.post.topic).revise!(
            Discourse.system_user,
            raw: updated_raw,
            skip_validations: true,
            skip_revision: false
          )

          delete_notification_posts(event)

          notification_raw = build_notification_raw(username, action_label, event, extra_text)
          PostCreator.create!(
            Discourse.system_user,
            topic_id: event.post.topic_id,
            raw: notification_raw,
            skip_validations: true
          )
        end
      else
        all_rsvp_posts = find_rsvp_posts(event)
        
        all_rsvp_posts.each do |post|
          begin
            PostDestroyer.new(Discourse.system_user, post, context: "calendar-rsvp-posts cleanup").destroy
          rescue StandardError => e
            Rails.logger.warn("calendar-rsvp-posts: failed to delete post #{post.id}: #{e}")
          end
        end

        notification_raw = build_notification_raw(username, action_label, event, extra_text)
        PostCreator.create!(
          Discourse.system_user,
          topic_id: event.post.topic_id,
          raw: notification_raw,
          skip_validations: true
        )
      end
    end
  end

  # ==========================================
  # Background Job Definition
  # ==========================================
  module ::Jobs
    class ProcessCalendarRsvpPost < ::Jobs::Base
      def execute(args)
        event_id = args[:event_id]
        username = args[:username]
        action_label = args[:action_label]
        extra_text = args[:extra_text]

        event_cls = ::CalendarRsvpPosts.event_class
        return unless event_cls

        event = event_cls.find_by(id: event_id)
        return unless event

        ::CalendarRsvpPosts.publish_rsvp_update(event, username, action_label, extra_text)
      end
    end
  end

  # ==========================================
  # Event Handlers
  # ==========================================

  # Handler for create/update attendance
  proc_handler = proc do |invitee|
    begin
      event = invitee&.event
      next if event.nil?
      next unless CalendarRsvpPosts.should_post_for_event?(event)

      invitee_cls = CalendarRsvpPosts.invitee_class
      next unless invitee_cls

      going_val = invitee_cls.statuses[:going]
      interested_val = invitee_cls.statuses[:interested]
      not_going_val = invitee_cls.statuses[:not_going]

      new_status = invitee.status
      prev_status =
        if invitee.respond_to?(:previous_changes) && invitee.previous_changes["status"]
          invitee.previous_changes["status"][0]
        else
          nil
        end

      next if prev_status && prev_status == new_status

      username = invitee.user&.username || "someone"
      action_label = nil

      if new_status == going_val && SiteSetting.calendar_rsvp_posts_on_new_going
        action_label = I18n.t('calendar_rsvp_posts.actions.going')
      elsif new_status == interested_val && SiteSetting.calendar_rsvp_posts_on_new_interested
        action_label = I18n.t('calendar_rsvp_posts.actions.interested')
      elsif new_status == not_going_val && SiteSetting.calendar_rsvp_posts_on_new_not_going
        action_label = I18n.t('calendar_rsvp_posts.actions.not_going')
      end

      next if action_label.nil?

      extra_text = nil
      if event.max_attendees.present?
        current_going = event.going_count
        prev_going =
          if prev_status == going_val && new_status != going_val
            current_going + 1
          elsif prev_status != going_val && new_status == going_val
            current_going - 1
          else
            current_going
          end

        was_full = prev_going >= event.max_attendees
        is_full = current_going >= event.max_attendees

        if was_full && !is_full
          extra_text = I18n.t('calendar_rsvp_posts.capacity.spots_available')
        elsif !was_full && is_full
          extra_text = I18n.t('calendar_rsvp_posts.capacity.now_full')
        end
      end

      # Enqueue the job instead of blocking the request
      Jobs.enqueue(:process_calendar_rsvp_post, 
        event_id: event.id, 
        username: username, 
        action_label: action_label, 
        extra_text: extra_text
      )

    rescue StandardError => e
      Rails.logger.warn("calendar-rsvp-posts: handler error: #{e}")
      Rails.logger.warn(e.backtrace.join("\n"))
    end
  end

  on(:discourse_calendar_post_event_invitee_status_changed, &proc_handler)

  # Handler for explicit invitee deletions (removed RSVP)
  invitee_cls = CalendarRsvpPosts.invitee_class
  if invitee_cls
    invitee_cls.class_eval do
      after_destroy do
        event = self.event
        
        should_process = event.present? &&
                         SiteSetting.calendar_rsvp_posts_on_removed_rsvp &&
                         (SiteSetting.calendar_rsvp_posts_allow_past_events || event.starts_at.nil? || event.starts_at >= Time.current)
        
        if should_process
          begin
            username = self.user&.username || "someone"
            action_label = I18n.t('calendar_rsvp_posts.actions.removed')

            # Enqueue the job instead of blocking the request
            Jobs.enqueue(:process_calendar_rsvp_post, 
              event_id: event.id, 
              username: username, 
              action_label: action_label,
              extra_text: nil
            )
            
          rescue StandardError => e
            Rails.logger.warn("calendar-rsvp-posts: after_destroy handler error: #{e}")
            Rails.logger.warn(e.backtrace.join("\n"))
          end
        end
      end
    end
  end
end