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