Switch from ActiveRecord to Sequel; /add-puzzle
This commit is contained in:
parent
76dfc2fa0f
commit
1aa7447ae6
6
Gemfile
6
Gemfile
|
@ -2,7 +2,6 @@ source "https://rubygems.org"
|
|||
|
||||
gem "dotenv"
|
||||
|
||||
gem "activerecord"
|
||||
gem "sqlite3"
|
||||
|
||||
gem "activesupport"
|
||||
|
@ -12,3 +11,8 @@ gem "activejob"
|
|||
gem "discordrb"
|
||||
|
||||
gem "pry"
|
||||
|
||||
gem "toml-rb"
|
||||
|
||||
# ORM
|
||||
gem "sequel"
|
||||
|
|
17
Gemfile.lock
17
Gemfile.lock
|
@ -4,12 +4,6 @@ GEM
|
|||
activejob (7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
activerecord (7.1.3.2)
|
||||
activemodel (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
timeout (>= 0.4.0)
|
||||
activesupport (7.1.3.2)
|
||||
base64
|
||||
bigdecimal
|
||||
|
@ -22,6 +16,7 @@ GEM
|
|||
tzinfo (~> 2.0)
|
||||
base64 (0.2.0)
|
||||
bigdecimal (3.1.6)
|
||||
citrus (3.0.2)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.2.3)
|
||||
connection_pool (2.4.1)
|
||||
|
@ -58,14 +53,19 @@ GEM
|
|||
pry (0.14.2)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
racc (1.7.3)
|
||||
rest-client (2.1.0)
|
||||
http-accept (>= 1.7.0, < 2.0)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
ruby2_keywords (0.0.5)
|
||||
sequel (5.78.0)
|
||||
bigdecimal
|
||||
sqlite3 (1.7.2-x86_64-darwin)
|
||||
timeout (0.4.1)
|
||||
toml-rb (3.0.1)
|
||||
citrus (~> 3.0, > 3.0)
|
||||
racc (~> 1.7)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
websocket (1.2.10)
|
||||
|
@ -79,12 +79,13 @@ PLATFORMS
|
|||
|
||||
DEPENDENCIES
|
||||
activejob
|
||||
activerecord
|
||||
activesupport
|
||||
discordrb
|
||||
dotenv
|
||||
pry
|
||||
sequel
|
||||
sqlite3
|
||||
toml-rb
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.13
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Puzzle Format
|
||||
|
||||
This is out of date, see db.rb or 0001_create_puzzles
|
||||
|
||||
text, a string with the text of the puzzle
|
||||
solution, a string to compare
|
||||
check_solution, a string; a ruby expression that will evaluate to true or false. `discord_username` and `submission` are in scope.
|
||||
|
|
79
db.rb
79
db.rb
|
@ -1,32 +1,67 @@
|
|||
require "active_record"
|
||||
require "sequel"
|
||||
|
||||
ActiveRecord::Base.establish_connection(
|
||||
adapter: 'sqlite3',
|
||||
database: 'db.sqlite3'
|
||||
)
|
||||
# :created_at and :updated_at on all tables
|
||||
Sequel::Model.plugin :timestamps, update_on_create: true
|
||||
|
||||
DB = Sequel.connect("sqlite://db.sqlite3")
|
||||
|
||||
# Migrations Boilerplate
|
||||
Dir["migrations/*.rb"].each {|f| require_relative f}
|
||||
unless ActiveRecord::Base.connection.table_exists? :puzzles
|
||||
CreatePuzzleTable.migrate(:up)
|
||||
end
|
||||
|
||||
# Puzzle model
|
||||
class Puzzle < ActiveRecord::Base
|
||||
# .string :text, The markdown text of the message
|
||||
# Sequel creates getters and setters corresponding to DB columns. DB is the source of truth.
|
||||
# Sequel tracks which migrations have been run, and you can use it to run migrations
|
||||
# We need to write migrations manually
|
||||
class Puzzle < Sequel::Model
|
||||
# Fields:
|
||||
# primary_key :id
|
||||
|
||||
# One of these must be present
|
||||
# table.string :solution, null: true # A string to compare against
|
||||
# table.string :check_solution, null: true # a ruby expression that will evaluate to true or false. `discord_username` and `submission` are in scope.
|
||||
# # The markdown text of the message
|
||||
# String :text, null: false
|
||||
|
||||
# # Public_id or title, used to identify the puzzle when upserting toml
|
||||
# table.string :public_id
|
||||
# # One of these must be present
|
||||
# String :solution, null: true # A string to compare against
|
||||
# String :check_solution, null: true # a ruby expression that will evaluate to true or false. `discord_username` and `submission` are in scope.
|
||||
|
||||
# # used / current / ready / draft
|
||||
# table.integer :state, default: 0
|
||||
enum :state, %i[draft ready current used]
|
||||
# # We need a public_id or title that we can use to identify the puzzle, for example when upserting toml
|
||||
# # This is publicly displayed in hex, but stored as an integer so that we can sort
|
||||
# # This is the order that the puzzles are released
|
||||
# # Unreleased puzzles may have a null public_id
|
||||
# Integer :public_id, null: true
|
||||
|
||||
def validate
|
||||
return solution.present? || check_solution.present
|
||||
# # To facilitate editing puzzle messages, we want to store the Discord message id
|
||||
# # Obviously null if it hasn't been sent yet
|
||||
# Integer :message_id, null: true
|
||||
|
||||
# # draft / ready / current / used
|
||||
# String :state, default: "draft", null: false
|
||||
|
||||
# # Timestamps
|
||||
# DateTime :created_at, null: false
|
||||
# DateTime :updated_at, null: false
|
||||
|
||||
|
||||
# Scopes
|
||||
class << self
|
||||
def draft
|
||||
where state: "draft"
|
||||
end
|
||||
def ready
|
||||
where state: "ready"
|
||||
end
|
||||
def current
|
||||
where state: "current"
|
||||
end
|
||||
def used
|
||||
where state: "used"
|
||||
end
|
||||
end
|
||||
|
||||
# Validation
|
||||
plugin :validation_helpers
|
||||
def validate
|
||||
errors.add(:solution, "must be present") if solution.blank? and check_solution.blank?
|
||||
validates_presence :text
|
||||
end
|
||||
|
||||
# TODO: getter for getting the Discord message ID
|
||||
# $puzzle_channel.load_message message_id
|
||||
end
|
||||
|
|
107
main.rb
107
main.rb
|
@ -3,16 +3,16 @@ require "pp"
|
|||
require "dotenv"
|
||||
Dotenv.load("config/.env")
|
||||
|
||||
require "discordrb"
|
||||
|
||||
require "active_support/all"
|
||||
require "discordrb"
|
||||
require "toml-rb"
|
||||
|
||||
require_relative "send_puzzle_job"
|
||||
require_relative "db"
|
||||
|
||||
$bot = Discordrb::Bot.new(token: ENV["DISCORD_TOKEN"], client_id: ENV["DISCORD_CLIENT_ID"])
|
||||
|
||||
## Setup
|
||||
## Setup Discord Globals
|
||||
$bot.run :async
|
||||
at_exit { $bot.stop }
|
||||
|
||||
|
@ -22,13 +22,8 @@ $puzzle_channel = $server.channels.find { |c| c.name == "puzzle" }
|
|||
$study_channel = $server.channels.find { |c| c.name == "sylvains-study" }
|
||||
$me = $server.users.find { |u| u.username == "matthiasportzel" }
|
||||
|
||||
def remove_solver_role
|
||||
p "remove solver role"
|
||||
$puzzle_solver_role.members.each do |member|
|
||||
member.remove_role $puzzle_solver_role
|
||||
end
|
||||
end
|
||||
|
||||
## Discord Command Helper
|
||||
# Maps command name to snowflake
|
||||
$command_name_cache = {}
|
||||
def get_command_by_name command_name
|
||||
|
@ -61,12 +56,18 @@ def define_command text, options
|
|||
end
|
||||
end
|
||||
|
||||
## Commands
|
||||
|
||||
define_command "Remove role", [] do |event|
|
||||
remove_solver_role
|
||||
event.respond content: "Removed the solved role from all solvers", ephemeral: false
|
||||
end
|
||||
# /remove-role (was only for testing and isn't checking auth!)
|
||||
# define_command "Remove role", [] do |event|
|
||||
# p "remove solver role"
|
||||
# z$puzzle_solver_role.members.each do |member|
|
||||
# member.remove_role $puzzle_solver_role
|
||||
# end
|
||||
# event.respond content: "Removed the solved role from all solvers", ephemeral: false
|
||||
# end
|
||||
|
||||
# /remove-commands
|
||||
# There's probably a better way to do this re-loading
|
||||
define_command "Remove commands", [] do |event|
|
||||
if event.user != $me
|
||||
|
@ -84,6 +85,7 @@ define_command "Remove commands", [] do |event|
|
|||
event.edit_response content: "Removed all commands!"
|
||||
end
|
||||
|
||||
# /solve
|
||||
define_command("Solve", [
|
||||
{ type: :string, name: "solution", description: "Your solution to the puzzle", required: true }
|
||||
]) do |event|
|
||||
|
@ -118,6 +120,36 @@ define_command("Solve", [
|
|||
end
|
||||
end
|
||||
|
||||
# /add-puzzle
|
||||
EXAMPLE_TOML = <<EOF
|
||||
[[puzzles]]
|
||||
text = """
|
||||
Can you navigate in 3D space without seeing it?
|
||||
|
||||
The below list consists of alternating letters and numbers. These form pairs: first a direction, then a distance. There are six directions:
|
||||
* "N": North (negative y)
|
||||
* "E": East (positive x)
|
||||
* "S": South (positive y)
|
||||
* "W": West (negative x)
|
||||
* "I": In (positive z)
|
||||
* "O": Out (negative z)
|
||||
|
||||
If you start at (0, 0, 0), and follow all of these movement instructions in order, what are your coordinates at the end? Format as (X, Y, Z). Good luck!
|
||||
`W 3 N 5 I 2 W 3 E 2 O 1 I 5 S 2 S 8 O 2 I 8 W 5 W 6 W 2 W 1 E 7 O 9`
|
||||
"""
|
||||
solution = "(-11, 5, 3)"
|
||||
EOF
|
||||
|
||||
define_command("Add Puzzle", []) do |event|
|
||||
event.defer ephemeral: false
|
||||
event.user.dm "We'd love to have you submit a puzzle to The Linoleum Club! A good puzzle is easy for a skilled programmer and possible for a novice programmer, with sufficient effort."
|
||||
event.user.dm "After you submit your puzzle, it will be reviewed, solved, and edited by The Club's admin team (just Matthias), before being used as a weekly puzzle in the next couple of weeks."
|
||||
event.user.dm "To submit your puzzle, DM me (Dr. Sylvain) a TOML block with the fields `text` and `solution` in the `[[puzzles]]` array. Here's an example:"
|
||||
event.user.dm "```toml\n#{EXAMPLE_TOML}```"
|
||||
event.edit_response content: "DM'd you instructions!"
|
||||
end
|
||||
|
||||
# /irb-discord
|
||||
$irb_sessions = []
|
||||
|
||||
class DiscordIRBSession
|
||||
|
@ -153,17 +185,58 @@ $bot.message(from: $me) do |event|
|
|||
rescue => err
|
||||
res = err
|
||||
ensure
|
||||
# 5 x ` so we can use triple inside
|
||||
event.channel.send "`````rb\n#{PP.pp(res, "")}\n`````"
|
||||
event.channel.send "```rb\n#{PP.pp(res, "")}\n```"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
$bot.message do |event|
|
||||
# If this is a DM
|
||||
next unless event.channel == event.user.dm
|
||||
|
||||
toml_text = event.content
|
||||
if toml_text.starts_with? "```" and toml_text.ends_with "```"
|
||||
toml_text = toml_text.sub(/\A```(?:toml)?/, "").sub(/```\z/, "").strip
|
||||
end
|
||||
|
||||
# parse toml
|
||||
begin
|
||||
puzzle_data = TomlRB.parse(toml_text)
|
||||
rescue => TomlRB::ParseError
|
||||
# if toml parse fails, react to the message with ?
|
||||
event.message.react("\u2753")
|
||||
# Continue waiting for messages
|
||||
next
|
||||
end
|
||||
|
||||
# validate toml fields, sending error messages
|
||||
unless puzzle_data.has_key? "puzzles" and puzzle_data["puzzles"].is_a? Array
|
||||
event.channel.send "Please include your puzzle submission(s) in the `puzzles` Array.\n=> https://toml.io/en/v1.0.0#array-of-tables"
|
||||
next
|
||||
end
|
||||
puzzle_data["puzzles"].each do |puzzle|
|
||||
unless event.user == me
|
||||
# If it's not me, validate the puzzle
|
||||
unless puzzle.has_key? "text" and (puzzle.has_key? "check_solution" or puzzle.has_key? "solution") and puzzle.keys.length == 2
|
||||
event.channel.send "Invalid puzzle. Please include only the `text` and `solution` keys."
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# create and save the puzzle object
|
||||
p = Puzzle.new puzzle
|
||||
unless p.valid?
|
||||
message.channel.send "Invalid Puzzle. IDK why"
|
||||
next
|
||||
end
|
||||
p.save
|
||||
end
|
||||
end
|
||||
|
||||
# Get ready to send the next puzzle at the next UTC sunday
|
||||
SendPuzzleJob.new.enqueue(wait_until: Date.today.next_occurring(:sunday).beginning_of_day)
|
||||
|
||||
# binding.irb
|
||||
# TODO: We need to run this every Sunday, not just the next one
|
||||
|
||||
# Wait forever for the bot to exit
|
||||
$bot.join
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
(
|
||||
cd $(dirname "$0")
|
||||
|
||||
sequel -m migrations/ "sqlite://db.sqlite3"
|
||||
)
|
|
@ -1,22 +0,0 @@
|
|||
# TODO: finishing writing and migrated
|
||||
|
||||
class CreatePuzzleTable < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :puzzles do |table|
|
||||
# The markdown text of the message
|
||||
table.string :text
|
||||
|
||||
# One of these must be present
|
||||
table.string :solution, null: true
|
||||
table.string :check_solution, null: true
|
||||
|
||||
# We need a public_id or title that I can use to identify the puzzle when upserting toml
|
||||
table.string :public_id
|
||||
|
||||
# used / current / ready / draft
|
||||
table.integer :state, default: 0
|
||||
|
||||
table.timestamps
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
Sequel.migration do
|
||||
change do
|
||||
create_table :puzzles do
|
||||
primary_key :id
|
||||
|
||||
# The markdown text of the message
|
||||
String :text, null: false
|
||||
|
||||
# One of these must be present
|
||||
String :solution, null: true # A string to compare against
|
||||
String :check_solution, null: true # a ruby expression that will evaluate to true or false. `discord_username` and `submission` are in scope.
|
||||
|
||||
# We need a public_id or title that we can use to identify the puzzle, for example when upserting toml
|
||||
# This is publicly displayed in hex, but stored as an integer so that we can sort
|
||||
# This is the order that the puzzles are released
|
||||
# Unreleased puzzles may have a null public_id
|
||||
Integer :public_id, null: true
|
||||
|
||||
# To facilitate editing puzzle messages, we want to store the Discord message id
|
||||
# Obviously null if it hasn't been sent yet
|
||||
Integer :message_id, null: true
|
||||
|
||||
# draft / ready / current / used
|
||||
String :state, default: "draft", null: false
|
||||
|
||||
# Timestamps
|
||||
DateTime :created_at, null: false
|
||||
DateTime :updated_at, null: false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,19 +7,31 @@ class SendPuzzleJob < ActiveJob::Base
|
|||
# Do something later
|
||||
def perform
|
||||
# Remove the puzzle command from everyone
|
||||
remove_solver_role
|
||||
$puzzle_solver_role.members.each do |member|
|
||||
member.remove_role $puzzle_solver_role
|
||||
end
|
||||
|
||||
# Set the previous puzzle as used (saves)
|
||||
Puzzle.current.first.used!
|
||||
# Set the previous puzzle as used
|
||||
current_puzzle = Puzzle.current.first
|
||||
current_puzzle.state = "used"
|
||||
current_puzzle.save
|
||||
|
||||
# Get a random puzzle
|
||||
puzzle = Puzzle.ready.order("RANDOM()").first
|
||||
|
||||
if puzzle.present?
|
||||
# Send the new puzzle
|
||||
$puzzle_channel.send puzzle.text
|
||||
# TODO: puzzle public id to 0x + hex
|
||||
msg = $puzzle_channel.send puzzle.text
|
||||
# TODO: Submit your answer with /solve
|
||||
# TODO: If you have an idea for a future puzzle, use /add-puzzle
|
||||
|
||||
# Save message id
|
||||
puzzle.message_id = msg.id
|
||||
# Mark it as current
|
||||
puzzle.current!
|
||||
puzzle.state = "current"
|
||||
# Save
|
||||
puzzle.save
|
||||
else
|
||||
# Panic
|
||||
$study_channel.send "<@#{$me.id}> Help! No more puzzles! No more Club???"
|
||||
|
|
Loading…
Reference in New Issue