Skip to content

Commit c9fa570

Browse files
authored
Merge pull request #5 from ombulabs/IIRR-15-daily-puzzle
[IIRR-15, IIRR-16, IIRR-17] Add daily puzzle feature
2 parents 98e2f62 + 1403192 commit c9fa570

23 files changed

+348
-32
lines changed

Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ gem "thruster", require: false
4444
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
4545
# gem "image_processing", "~> 1.2"
4646

47+
group :development, :production do
48+
gem "sidekiq"
49+
gem "sidekiq-scheduler"
50+
end
51+
4752
group :development, :test do
4853
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
4954
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

Gemfile.lock

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ GEM
277277
rake (13.2.1)
278278
rdoc (6.12.0)
279279
psych (>= 4.0.0)
280+
redis-client (0.24.0)
281+
connection_pool
280282
regexp_parser (2.10.0)
281283
reline (0.6.0)
282284
io-console (~> 0.5)
@@ -315,13 +317,25 @@ GEM
315317
rubocop-rails (>= 2.30)
316318
ruby-progressbar (1.13.0)
317319
rubyzip (2.4.1)
320+
rufus-scheduler (3.9.2)
321+
fugit (~> 1.1, >= 1.11.1)
318322
securerandom (0.4.1)
319323
selenium-webdriver (4.29.1)
320324
base64 (~> 0.2)
321325
logger (~> 1.4)
322326
rexml (~> 3.2, >= 3.2.5)
323327
rubyzip (>= 1.2.2, < 3.0)
324328
websocket (~> 1.0)
329+
sidekiq (7.3.9)
330+
base64
331+
connection_pool (>= 2.3.0)
332+
logger
333+
rack (>= 2.2.4)
334+
redis-client (>= 0.22.2)
335+
sidekiq-scheduler (5.0.6)
336+
rufus-scheduler (~> 3.2)
337+
sidekiq (>= 6, < 8)
338+
tilt (>= 1.4.0, < 3)
325339
solid_cable (3.0.7)
326340
actioncable (>= 7.2)
327341
activejob (>= 7.2)
@@ -353,6 +367,7 @@ GEM
353367
thruster (0.1.12-aarch64-linux)
354368
thruster (0.1.12-arm64-darwin)
355369
thruster (0.1.12-x86_64-linux)
370+
tilt (2.6.0)
356371
timeout (0.4.3)
357372
turbo-rails (2.0.13)
358373
actionpack (>= 7.1.0)
@@ -411,6 +426,8 @@ DEPENDENCIES
411426
rails (~> 8.0.2)
412427
rubocop-rails-omakase
413428
selenium-webdriver
429+
sidekiq
430+
sidekiq-scheduler
414431
solid_cable
415432
solid_cache
416433
solid_queue

app/jobs/application_job.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
class ApplicationJob < ActiveJob::Base
22
# Automatically retry jobs that encountered a deadlock
33
# retry_on ActiveRecord::Deadlocked
4-
5-
# Most jobs are safe to ignore if the underlying records are no longer available
6-
# discard_on ActiveJob::DeserializationError
4+
def bot
5+
@bot ||= Discord::Bot.configure do |config|
6+
config.token = ENV.fetch("DISCORD_BOT_TOKEN", "")
7+
end
8+
end
79
end

app/jobs/daily_puzzle_job.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
class DailyPuzzleJob < ApplicationJob
2+
queue_as :default
3+
4+
def perform
5+
# TODO: Don't allow puzzle to be asked more then once
6+
puzzle = Puzzle.order("RANDOM()").first
7+
return unless puzzle
8+
9+
Server.where(active: true).each do |server|
10+
channel = server.channel
11+
next unless channel
12+
13+
bot.send_message(
14+
channel.channel_id,
15+
"",
16+
false,
17+
puzzle_embed(puzzle),
18+
nil,
19+
nil,
20+
nil,
21+
puzzle_buttons(puzzle.id)
22+
)
23+
end
24+
end
25+
26+
private
27+
28+
def puzzle_embed(puzzle)
29+
Discordrb::Webhooks::Embed.new(
30+
title: "Puzzle Time! 🧩",
31+
description: puzzle.question,
32+
color: 0x3498db # Blue color
33+
)
34+
end
35+
36+
def puzzle_buttons(puzzle_id)
37+
Discordrb::Webhooks::View.new do |view|
38+
view.row do |row|
39+
row.button(
40+
style: :primary,
41+
emoji: "👍",
42+
label: "Ruby",
43+
custom_id: "ruby__#{puzzle_id}"
44+
)
45+
row.button(
46+
style: :primary,
47+
emoji: "👍",
48+
label: "Rails",
49+
custom_id: "rails__#{puzzle_id}"
50+
)
51+
end
52+
end
53+
end
54+
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
module Discord
2+
module Events
3+
class PuzzleAnswer
4+
def initialize(bot)
5+
@bot = bot
6+
end
7+
8+
def listen
9+
# Listen for button clicks and filter based on custom_id pattern
10+
@bot.button { |event| handle(event) if valid_puzzle_answer?(event) }
11+
end
12+
13+
def handle(event)
14+
answer, puzzle_id = event.custom_id.split("__")
15+
16+
# Find the associated puzzle
17+
puzzle = find_puzzle(puzzle_id.to_i)
18+
return unless puzzle
19+
20+
# Find or create the user
21+
user = User.find_or_create_by(user_id: event.user.id) do |user|
22+
user.username = event.user.username
23+
# Users with permission to manage the server are admins, all other users fall under the member role
24+
user.role = event.user.permission?(:manage_server) ? 1 : 0
25+
end
26+
27+
if user.persisted?
28+
user.update(username: event.user.username, role: event.user.permission?(:manage_server) ? 1 : 0)
29+
end
30+
31+
# Check if the user has already answered this puzzle
32+
existing_answer = Answer.find_by(puzzle_id: puzzle.id, user_id: user.id)
33+
if existing_answer
34+
# If the user has already answered, prevent them from changing their answer
35+
event.respond(
36+
content: "You have already answered this puzzle. You cannot change your answer.",
37+
ephemeral: true # Only the user sees this message
38+
)
39+
return
40+
end
41+
42+
# Create the answer record
43+
answer = Answer.create!(
44+
puzzle_id: puzzle.id,
45+
user_id: user.id,
46+
choice: answer, # 'ruby' or 'rails'
47+
is_correct: puzzle.answer.to_s == answer # Correct answer check
48+
)
49+
50+
# Respond to the user with the result
51+
event.respond(
52+
content: nil,
53+
embeds: [ answer_embed(answer, puzzle) ],
54+
ephemeral: true # Only the user sees this message
55+
)
56+
end
57+
58+
private
59+
60+
def answer_embed(answer, puzzle)
61+
Discordrb::Webhooks::Embed.new(
62+
title: answer.is_correct ? "Correct! 🎉" : "Incorrect 😢",
63+
description: puzzle.explanation,
64+
color: 0xe60000,
65+
footer: Discordrb::Webhooks::EmbedFooter.new(text: "Your answer has been recorded, you cannot change it.")
66+
)
67+
end
68+
69+
def valid_puzzle_answer?(event)
70+
# Check if the custom_id matches the pattern (ruby__NUMBER or rails__NUMBER)
71+
event.custom_id.match?(/^(ruby|rails)__\d+$/)
72+
end
73+
74+
def find_puzzle(puzzle_id)
75+
Puzzle.find_by(id: puzzle_id)
76+
end
77+
end
78+
end
79+
end

app/lib/discord/events/server_create.rb

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def handle(event)
2222
puts "No suitable channel found to add the bot to."
2323
end
2424

25-
create_server(server)
25+
create_or_update_server(server)
2626
end
2727

2828
private
@@ -42,16 +42,29 @@ def welcome_message(server_name)
4242

4343
attr_reader :bot
4444

45-
def create_server(server)
46-
# Only create a new record if it doesn't already exist
47-
return if Server.exists?(server_id: server.id)
45+
def create_or_update_server(server)
46+
# Find or create the Server record
47+
discord_server = Server.find_or_create_by(server_id: server.id) do |s|
48+
s.name = server.name
49+
s.active = true
50+
end
4851

49-
# Create the new Server record
50-
discord_server = Server.create!(
51-
server_id: server.id,
52-
name: server.name
53-
)
52+
# Update the server name if it changed
53+
discord_server.update!(name: server.name, active: true)
54+
55+
# Update or create a system channel is one is set
5456
if server.system_channel
57+
create_or_update_system_channel(discord_server)
58+
end
59+
end
60+
61+
def create_or_update_system_channel(discord_server)
62+
if discord_server.channel
63+
discord_server.channel.update!(
64+
channel_id: server.system_channel.id,
65+
name: server.system_channel.name
66+
)
67+
else
5568
discord_server.create_channel!(
5669
channel_id: server.system_channel.id,
5770
name: server.system_channel.name
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module Discord
2+
module Events
3+
# This class listens and handles the event triggered when the bot is removed from a server.
4+
class ServerDelete
5+
def initialize(bot)
6+
@bot = bot
7+
end
8+
9+
def listen
10+
@bot.server_delete do |event|
11+
handle(event)
12+
end
13+
end
14+
15+
def handle(event)
16+
begin
17+
server_id = event.server
18+
rescue Discordrb::Errors::UnknownServer
19+
# This error is expected when the bot is removed from a server
20+
puts "Bot was removed from an unknown server"
21+
return
22+
end
23+
24+
server = Server.find_by(server_id: server_id)
25+
server&.update!(active: false)
26+
end
27+
28+
private
29+
30+
attr_reader :bot
31+
end
32+
end
33+
end

app/lib/discord/events/set_channel.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ def listen
1717

1818
def handle(event)
1919
user = event.user
20-
server = event.server
2120

2221
# Ensure the user has the necessary permissions to run the command
2322
unless user.permission?(:manage_server)
@@ -45,8 +44,6 @@ def display_settings(event)
4544
event.respond(content: content, ephemeral: true, components: channel_select)
4645
end
4746

48-
49-
5047
def listen_to_channel_select
5148
# Listen for the channel select event
5249
@bot.channel_select(custom_id: "set_channel_select") do |event|

app/models/answer.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class Answer < ApplicationRecord
2+
belongs_to :puzzle
3+
belongs_to :user
4+
end

app/models/discord_server.rb

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)