diff --git a/Gemfile b/Gemfile index fc463c28ebe7e5b99ca04dc9631da16a32c6b67c..1df1a541262f39a44e02f080f0a0703bcf24d224 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ gem "redis", ">= 4.0.1" # gem "kredis" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem "bcrypt", "~> 3.1.7" +gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ windows jruby ] @@ -69,3 +69,5 @@ gem "importmap-rails", "~> 2.0" gem "icalendar", "~> 2.10" gem "telegram-bot-ruby", "~> 2.0" + +gem "crono", "~> 2.0" diff --git a/Gemfile.lock b/Gemfile.lock index 8a92be5d73efea5fe78cd7921a3505614c877f4d..e9f8ce2b8da7c00f6b05bd686eca020d57b0af1b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,7 @@ GEM addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) base64 (0.2.0) + bcrypt (3.1.20) bigdecimal (3.1.7) bindex (0.8.1) bootsnap (1.18.3) @@ -95,6 +96,9 @@ GEM concurrent-ruby (1.2.3) connection_pool (2.4.1) crass (1.0.6) + crono (2.0.1) + rails (>= 5.2.8) + sprockets-rails date (3.3.4) debug (1.9.2) irb (~> 1.10) @@ -315,8 +319,10 @@ PLATFORMS x86_64-linux DEPENDENCIES + bcrypt (~> 3.1.7) bootsnap capybara + crono (~> 2.0) debug hotwire-rails (~> 0.1.3) httparty diff --git a/app/jobs/telegram_group_chat_notification_job.rb b/app/jobs/telegram_group_chat_notification_job.rb index ae907df2d2d369be11b59d36b615781a42d4162f..ea10213d89c0f66a15c5acc34c2f5c13275626ea 100644 --- a/app/jobs/telegram_group_chat_notification_job.rb +++ b/app/jobs/telegram_group_chat_notification_job.rb @@ -3,11 +3,11 @@ require 'telegram/bot' class TelegramGroupChatNotificationJob < NotificationJob queue_as :notifications - def perform(*args) + def perform(**args) channel = NotificationChannel.find_by(name: 'telegram_group_chat') token = channel.data['token'] Telegram::Bot::Client.run(token) do |bot| - bot.api.send_message(chat_id: args[:target], text: content) + bot.api.send_message(chat_id: args[:target], text: args[:text]) end end end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 3426f2244d12b0885bdf0726c9a4d485c0fc5660..20d75f3035a88522c005abb1cd99a0f2aed2beca 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -6,6 +6,9 @@ class Assignment < ApplicationRecord validate :no_overlapping_assignments + after_create_commit :notify_assignment_created + after_destroy_commit :notify_assignment_destroyed + private def no_overlapping_assignments @@ -25,4 +28,14 @@ class Assignment < ApplicationRecord errors.add(:base, "This assignment overlaps with another assignment for this user.") end end + + private + + def notify_assignment_created + ActiveSupport::Notifications.instrument("assignment.created", record: self) + end + + def notify_assignment_destroyed + ActiveSupport::Notifications.instrument("assignment.destroyed", record: self) + end end diff --git a/app/models/model_version.rb b/app/models/model_version.rb new file mode 100644 index 0000000000000000000000000000000000000000..10660b8bfc4076976d51a4755c3a4598d4a345f4 --- /dev/null +++ b/app/models/model_version.rb @@ -0,0 +1,2 @@ +class ModelVersion < ApplicationRecord +end diff --git a/app/models/notification_channel.rb b/app/models/notification_channel.rb index fd234e28c04aa0bf3fedbfbb7fd7ae296c70b74f..074e0c68bddb5beb97235562b8be62b98e118eb7 100644 --- a/app/models/notification_channel.rb +++ b/app/models/notification_channel.rb @@ -1,2 +1,3 @@ class NotificationChannel < ApplicationRecord + serialize :data, coder: JSON end diff --git a/app/models/user.rb b/app/models/user.rb index 8e3dc5ad13a7f4a5ac47d34ae4fc63f0c70c8b7b..f343dde36f9241cd59911fc229ba20186864402a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,9 +1,17 @@ class User < ApplicationRecord has_many :assignments + + has_secure_password + + validates :password, presence: true, length: { minimum: 6 }, allow_nil: true validates :email, uniqueness: { case_sensitive: false, message: "already in use" } after_initialize :set_avatar_color + def has_password? + !password_digest.nil? + end + def text_color r, g, b = avatar_color.delete_prefix('#').chars.each_slice(2).map { |hex| hex.join.to_i(16) } diff --git a/app/subscribers/assignment_audit_subscriber.rb b/app/subscribers/assignment_audit_subscriber.rb new file mode 100644 index 0000000000000000000000000000000000000000..4bdb724a3d6ea5af89695441445184eb8f169513 --- /dev/null +++ b/app/subscribers/assignment_audit_subscriber.rb @@ -0,0 +1,15 @@ +class AssignmentAuditSubscriber + def self.subscribe + ActiveSupport::Notifications.subscribe(/\Aassignment\..*/) do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + new.handle_event(event) # Call the instance method + end + end + + def handle_event(event) + action = event.name.split(".").last + record = event.payload[:record] + + ModelVersion.create!(model: 'assignment', action:, new_data: record.to_json) + end +end diff --git a/app/subscribers/telegram_bot_subscriber.rb b/app/subscribers/telegram_bot_subscriber.rb index 15fefb070e9024113d540706c32c57ec2d7ce094..27349ee7c3ec2f88cc477fa66f7f1bfcd79f7848 100644 --- a/app/subscribers/telegram_bot_subscriber.rb +++ b/app/subscribers/telegram_bot_subscriber.rb @@ -7,25 +7,23 @@ class TelegramBotSubscriber end def handle_event(event) - # 1. Extract Relevant Information - model_name = event.name.split(".").last # Get "session" or "speaker" + model_name, action = event.name.split(".") record = event.payload[:record] changes = event.payload[:changes] - # 2. Format Message for Telegram - message = format_telegram_message(model_name, record, changes) + message = format_telegram_message(model_name, action, record, changes) - # 3. Send Message via Telegram Bot API - # (Replace with your actual Telegram bot API integration) # TelegramBotAPI.sendMessage(chat_id: YOUR_CHAT_ID, text: message) Rails.logger.info("event: #{message}") + #TelegramGroupChatNotificationJob.perform_later(target: "-316096320", text: message) + TelegramGroupChatNotificationJob.perform_later(target: "2192297", text: message) end private - def format_telegram_message(model_name, record, changes) + def format_telegram_message(model_name, action, record, changes) # Customize this to your desired message format - "**#{model_name.capitalize} Updated:**\n\n" + + "**#{model_name.capitalize} #{action.capitalize}:**\n\n" + changes.map { |attr, (from, to)| "- #{attr}: #{from} -> #{to}" }.join("\n") end end diff --git a/config/cronotab.rb b/config/cronotab.rb new file mode 100644 index 0000000000000000000000000000000000000000..976cc9776733843ffa1f1641ca7451ef83c8654f --- /dev/null +++ b/config/cronotab.rb @@ -0,0 +1,17 @@ +# cronotab.rb — Crono configuration file +# +# Here you can specify periodic jobs and schedule. +# You can use ActiveJob's jobs from `app/jobs/` +# You can use any class. The only requirement is that +# class should have a method `perform` without arguments. +# +# class TestJob +# def perform +# puts 'Test!' +# end +# end +# +# Crono.perform(TestJob).every 2.days, at: '15:30' +# + +Crono.perform(FetchConferenceDataJob, 'rp2024').every 5.minutes diff --git a/config/initializers/subscribers.rb b/config/initializers/subscribers.rb index cc511bfbc2d1d31c4f65ecfa6931fe2ae62a691d..ad9d4de994f2f9d57ee4b9e3dccc38e606895c99 100644 --- a/config/initializers/subscribers.rb +++ b/config/initializers/subscribers.rb @@ -1,3 +1,4 @@ Rails.application.config.to_prepare do - TelegramBotSubscriber.subscribe # Subscribe to notifications -end \ No newline at end of file + TelegramBotSubscriber.subscribe + AssignmentAuditSubscriber.subscribe +end diff --git a/config/routes.rb b/config/routes.rb index 80999f2dfae0b1a57235af8ac99eac132cd1c586..ebfe92336b2b26f39e16bc567d53aa1d653e1b08 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,6 @@ Rails.application.routes.draw do + mount Crono::Engine, at: '/crono' + get 'login', to: 'users#login', as: :login post 'login', to: 'users#login' post 'logout', to: 'users#logout', as: :logout diff --git a/db/migrate/20240526145439_create_model_versions.rb b/db/migrate/20240526145439_create_model_versions.rb new file mode 100644 index 0000000000000000000000000000000000000000..8a55348899feb132e37bf60ce7dcfb9cc00e2940 --- /dev/null +++ b/db/migrate/20240526145439_create_model_versions.rb @@ -0,0 +1,11 @@ +class CreateModelVersions < ActiveRecord::Migration[7.1] + def change + create_table :model_versions do |t| + t.string :model + t.text :changed_data + t.text :new_data + + t.timestamps + end + end +end diff --git a/db/migrate/20240526145815_add_action_to_model_version.rb b/db/migrate/20240526145815_add_action_to_model_version.rb new file mode 100644 index 0000000000000000000000000000000000000000..f00bc31ebae0ff899f6e3c802d8148a963a19959 --- /dev/null +++ b/db/migrate/20240526145815_add_action_to_model_version.rb @@ -0,0 +1,5 @@ +class AddActionToModelVersion < ActiveRecord::Migration[7.1] + def change + add_column :model_versions, :action, :string + end +end diff --git a/db/migrate/20240526151339_create_crono_jobs.rb b/db/migrate/20240526151339_create_crono_jobs.rb new file mode 100644 index 0000000000000000000000000000000000000000..e0ed09e8b332465f5ceafa7e28a9d336d21ea900 --- /dev/null +++ b/db/migrate/20240526151339_create_crono_jobs.rb @@ -0,0 +1,12 @@ +class CreateCronoJobs < ActiveRecord::Migration[6.1] + def change + create_table :crono_jobs do |t| + t.string :job_id, null: false + t.text :log, limit: 1073741823 # LONGTEXT for MySQL + t.datetime :last_performed_at + t.boolean :healthy + t.timestamps null: false + end + add_index :crono_jobs, [:job_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index c3f6ce6618899243a05b2fad1730239e4494ac57..a5931539ade38fe0a84a890efa1c19610af28b29 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_05_26_121600) do +ActiveRecord::Schema[7.1].define(version: 2024_05_26_151339) do create_table "assignments", force: :cascade do |t| t.integer "user_id", null: false t.integer "session_id", null: false @@ -35,6 +35,25 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_26_121600) do t.string "time_zone" end + create_table "crono_jobs", force: :cascade do |t| + t.string "job_id", null: false + t.text "log", limit: 1073741823 + t.datetime "last_performed_at", precision: nil + t.boolean "healthy" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["job_id"], name: "index_crono_jobs_on_job_id", unique: true + end + + create_table "model_versions", force: :cascade do |t| + t.string "model" + t.text "changed_data" + t.text "new_data" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "action" + end + create_table "notification_channels", force: :cascade do |t| t.string "name" t.text "data" diff --git a/test/fixtures/model_versions.yml b/test/fixtures/model_versions.yml new file mode 100644 index 0000000000000000000000000000000000000000..8cd61a595d307c6e462070be257b15cf640d3c12 --- /dev/null +++ b/test/fixtures/model_versions.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + model: MyString + changed_data: MyText + new_data: MyText + +two: + model: MyString + changed_data: MyText + new_data: MyText diff --git a/test/models/model_version_test.rb b/test/models/model_version_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..6c238191fad3eea99208584d9abfdcaf023f4af9 --- /dev/null +++ b/test/models/model_version_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ModelVersionTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end