Calendar RSVP Posts

:information_source: Summary Creates short topic replies for RSVP events
:hammer_and_wrench: Repository Link GitHub - mariodsantana/discourse-calendar-rsvp-posts · GitHub
:open_book: Install Guide How to install plugins in Discourse

Features

  • Real-time notifications - Because RSVP events now create a reply post in the topic, every RSVP triggers notifications for topic watchers
  • Minimal clutter - Previous notification posts are deleted, so there’s a maximum of 2 posts at any time (1 history + 1 latest notification)
  • Complete history - All RSVP activity is preserved with timestamps in chronological order
  • Discourages flip-flopping - Timestamps make repeated RSVP changes visible

Configuration

The relevant site settings (in Admin > Settings > Plugins) mostly configure which RSVP changes should trigger a post - going, interested, not going, or removal of existing RSVP - and whether to trigger on RSVP changes to events that start in the past.

The remaining setting toggles history mode. With history mode disabled, only the latest notification post remains in the topic. With history mode enabled, the plugin manages an additional “history” comment as follows:

  • On the first RSVP, creates a simple notification post announcing the RSVP
  • On the second RSVP, transforms the first post into a timestamped history post, then creates a new notification post
  • On subsequent RSVPs, appends the RSVP to the history post with timestamp, deletes the previous notification post, and creates a new one

Settings

Name Description
calendar_rsvp_posts_on_new_going post on new “Going”
calendar_rsvp_posts_on_new_interested post on new “Interested”
calendar_rsvp_posts_on_new_not_going post on new “Not going”
calendar_rsvp_posts_on_removed_rsvp post when an RSVP is removed
calendar_rsvp_posts_allow_past_events whether to post for events that start in the past
calendar_rsvp_posts_enable_history maintain a timestamped history post (default: enabled)
6 Likes

This will improve event management for many !! Thanks a lot for providing this !!

1 Like

Thanks for the plugin :+1:

Just a small question: is it possible to translate the responses into a file?

I like this idea. I can add a link to the history post, which would download a CSV file. Is that the sort of thing you’re thinking of?

I misspoke :sweat_smile:, I wanted to know how to translate the reply notifications because they are in English and I would like to put them in French

1 Like

LOL - I understand now. Right now the English text is directly in the source code. I’m new to Discourse, but I’m sure there’s a standard way to make a plugin translatable. I’ll look into it when I get some time.

I’ll probably also implement the CSV link. :wink:

1 Like

No problem :wink: . Take your time, I’m not in a hurry

OK! New version up at the repository. Not only with i18n, but also found and fixed a bug that kept updates from being posted when someone removed their RSVP.

Let me know if you have any issues!

1 Like

Hi, I had an issue regarding users participating or not in an event. Two avatars were appearing in the event instead of one, and refreshing resolved it, but it caused confusion for users. I modified the code on my end; I don’t know if this is of interest to you.

# frozen_string_literal: true
# name: discourse-calendar-rsvp-posts
# about: Create short topic replies for RSVP events
# 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 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

   # Central 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 = DiscoursePostEvent::Event.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)

     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 || "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)
 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 || "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
1 Like

This is a great idea, and I can see how it would cause UI confusion. I’ve added a Mutex to avoid race conditions and pushed a new version of the plugin.

Let me know what you think!

1 Like

since version 2026.9.0-latest. I no longer receive event registrations. I don’t know if I’m the only one experiencing this.

I think I’ve figured out why it’s not working, because Discourse Calendar has been replaced by Events.

I modified the plugin.rb file on my end to make it compatible with the new version of 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

    # Central 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