diff --git a/app/jobs/pretalx/import_job.rb b/app/jobs/pretalx/import_job.rb index 234170bd515db70e0439dfb86bee23b8ae91976a..268289bfaddf98d906a965f39ea5c235f5bd7e31 100644 --- a/app/jobs/pretalx/import_job.rb +++ b/app/jobs/pretalx/import_job.rb @@ -8,7 +8,7 @@ module Pretalx queue_as :default include ActionView::Helpers - def import_schedule(conference, url) + def import_schedule(conference, url, filedrop_config) response = HTTParty.get(url) response.success? or return Rails.logger.error "Failed to fetch schedule from #{url}" @@ -17,6 +17,8 @@ module Pretalx schedule.dig('schedule', 'conference', 'rooms') && schedule.dig('schedule', 'conference', 'days') + filedrop_index = fetch_filedrop_index(filedrop_config) + # We keep a local hash of the stages, because the sessions reference stages by name instead of id stages = {} schedule['schedule']['conference']['rooms'].each do |stage_data| @@ -60,6 +62,7 @@ module Pretalx end end session.recorded = !session_data.fetch('do_not_record', false) + update_filedrop_data(session, filedrop_config) if filedrop_index[session.ref_id] session.save! end end @@ -74,8 +77,98 @@ module Pretalx def perform(conference_slug, *args) conference = Conference.find_by(slug: conference_slug) - import_schedule(conference, conference.data['schedule_url']) + import_schedule(conference, conference.data['schedule_url'], conference.data['filedrop']) RevisionSet.create!(conference:) end + + private + + def fetch_filedrop_index(filedrop_config) + if !filedrop_config || !filedrop_config['url'] + return {} + end + + begin + response = HTTParty.get( + filedrop_config['url'] + "/", + basic_auth: { + username: fetch_credential("filedrop_user"), + password: fetch_credential("filedrop_password") }, + headers: { 'Accept' => 'application/json' }, + timeout: 5 + ) + data = JSON.parse(response.body) + rescue => e + Rails.logger.warn("Filedrop response for #{session.ref_id} failed: #{e.message}") + return {} + end + if !data["talks"].is_a?(Array) + Rails.logger.warn("Filedrop index was incomplete") + return {} + end + + return data["talks"].each_with_object({}) do |item, hash| + hash[item["id"]] = item + end + end + + def update_filedrop_data(session, filedrop_config) + if !filedrop_config || !filedrop_config['url'] + return {} + end + + response = HTTParty.get( + filedrop_config['url'] + "/talks/" + session.ref_id, + basic_auth: { + username: fetch_credential("filedrop_user"), + password: fetch_credential("filedrop_password") }, + headers: { 'Accept' => 'application/json' } + ) + + begin + data = JSON.parse(response.body) + rescue => e + Rails.logger.warn("Filedrop response could not be parsed: #{e.message}") + return {} + end + if !data["files"].is_a?(Array) || !data["comments"].is_a?(Array) + Rails.logger.warn("Filedrop info for #{session.ref_id} was incomplete") + return {} + end + + existing_comments = session.filedrop_comments.pluck(:body) + new_comments = data["comments"]&.pluck("body") || [] + + # Remove comments not in the JSON file + (existing_comments - new_comments).each do |body| + session.filedrop_comments.where(body: body).destroy_all + end + + # Add or update comments + data["comments"]&.each do |comment_data| + session.filedrop_comments.find_or_initialize_by(body: comment_data['body']).tap do |comment| + comment.orig_created = comment_data['created'] + comment.save! + end + end + + existing_files = session.filedrop_files.pluck(:name, :checksum) + new_files = data['filedrop_files']&.pluck('name', 'hash') || [] + + # Remove files not in the JSON file + (existing_files - new_files).each do |name, checksum| + session.filedrop_files.where(name: name, checksum: checksum).destroy_all + end + + # Add or update files + data['files']&.each do |file_data| + session.filedrop_files.find_or_initialize_by(name: file_data['name'], checksum: file_data['meta']['hash']).tap do |file| + file.size = file_data['meta']['size'] + file.orig_created = file_data['meta']['created'] + file.download(filedrop_config['url'] + file_data['url']) unless filedrop_config.fetch('skip_downloads', false) + file.save + end + end + end end end diff --git a/app/models/filedrop_comment.rb b/app/models/filedrop_comment.rb new file mode 100644 index 0000000000000000000000000000000000000000..dbb244b6b74052b8b40439b2968b41a7975a8d30 --- /dev/null +++ b/app/models/filedrop_comment.rb @@ -0,0 +1,3 @@ +class FiledropComment < ApplicationRecord + belongs_to :session +end diff --git a/app/models/filedrop_file.rb b/app/models/filedrop_file.rb new file mode 100644 index 0000000000000000000000000000000000000000..39f72ba6246ae5507bdfd1de72e11154488d1803 --- /dev/null +++ b/app/models/filedrop_file.rb @@ -0,0 +1,41 @@ +class FiledropFile < ApplicationRecord + belongs_to :session + + def sanitize_filename(filename) + filename.gsub(/[^\w\s.-]/, '_') + end + + def safe_download_path(download_dir, filename) + sanitized_filename = sanitize_filename(filename) + output_path = File.join(download_dir, sanitized_filename) + if File.expand_path(output_path).start_with?(File.expand_path(download_dir)) + output_path + else + raise "Invalid filename, potential directory traversal detected!" + end + end + + def download(url) + # TBD: only download if orig_created is newer than actual file change time + response = HTTParty.get(url) + if response.success? + File.open(local_path, 'wb') do |file| + file.write(response.body) + end + Rails.logger.debug("File downloaded successfully and saved as #{local_path}.") + else + Rails.logger.warn("Failed to download file #{name} for #{session.ref_id}: #{response.code} #{response.message}") + end + end + + def local_path + dir = File.join( + ActiveStorage::Blob.service.root, + "filedrop", + session.conference.slug, + session.ref_id + ) + FileUtils.mkdir_p(dir) + return File.join(dir, name) + end +end diff --git a/app/models/session.rb b/app/models/session.rb index 28e5bbf5ef4465881b47f0cac2deb180ef976ebd..86ca06a1537296757777eb0ec0271fb27623b694 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -6,6 +6,8 @@ class Session < ApplicationRecord has_many :candidates has_many :session_speakers, dependent: :destroy has_many :speakers, through: :session_speakers + has_many :filedrop_comments, dependent: :destroy + has_many :filedrop_files, dependent: :destroy scope :scheduled, -> { where(status: 'scheduled') } scope :future, -> { where(starts_at: Time.now..) } diff --git a/app/models/user.rb b/app/models/user.rb index b96957d3c7f6b789bffaf58d63b804ce43d27bbe..aa06e39cb5cd512b3601619c98cc73fb57c99336 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -25,7 +25,7 @@ class User < ApplicationRecord if (login = conditions.delete(:name)) where(conditions.to_h).where(["lower(name) = :value", { value: login.downcase }]).first else - logger.warn("Authentication did not query :name as expected, login will only work with exact case!") + Rails.logger.warn("Authentication did not query :name as expected, login will only work with exact case!") where(conditions.to_h).first end end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 7783f40d7b9aa875142fb9d4286784381d000513..229de6f89acd276d8c3693065c99d60583efe8bb 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -mk0Oxq2UirmjsN7tckn4aXG2nU0JQQpa9AhwJk4Brq+9F7r4991T5RF8r5y54PEx8EgfOhN6eLHW6j6rQv6pNn86IUT/Awoc1771fWX1k/RRC3V1bOKNdfs+z5XNMSVH3ElpNs77cHz2i0ASSzerooNodAlnTWh5IJY2wqmg/3o84ndPXk61JuC96YNdJKOyZTsYNGlhLWCdwM0R+ehyiI2V92jULwuv805ml7kv58DYjAuJ0Zl5hO0gDc8+xe49VHXZahZv8lXriouc2E57IK4+YkByu8alrfrSlnqTvcZQ5/PwdeSG2hcyRH7PBCJM/nRIkWMiMqTqANlYPhn2PYSa5Iuei5nHVD1eo8fwm90KPAN2eJ20o4mrLb3GgkokCyDvWTi4Z0FL5RvJx/AmBEeiSVvR/9/XD/FaCjjJTwF36I4Q11JZbB81rkmVqPZTdW8thatGDQq+f7pOJHtXTFFbDWvH6rj6MpEgHw7EBpdKO2ypzlCnMw==--Do4Eq2X5bdeAXmaK--y66xcHPgudqA58YaQNaZhQ== \ No newline at end of file +yyticap6zoGwsCHAopvKcSo3S00K8NrYsUdsKi4DT70yAL0/4AfxxhgTGX1y6sxKryjo93305AXDPr0r5SaSgaAIPMhLblHLamiYlVVhvLB7r+LrGm3gkSDRKeJd6xKXMC9FxjzrodSxxAmG1yz+c3geOpCfSTRH1znkb75Dc11NRJ7w48wNRjBbYKmvmwnaWsyW5QJFcKiGU7QzFHiyyenGujRTO/gc3nm/NQShd1dY9Y3idpg1PA68iaaVQrx5RrNq+KYtrEBYTMscJBEGxs8mJDMRYi6TsCuodl+m5agg0vAJiUsc/jPHS4Y5DiaaJKLF9J6DxZIZR+DobBybDvALNnYHUFWH1QcuyqfK8rdVqbtS40QbmVXwKBqMOEGs6mAdoAIRp1C2T1ZkcHPL2oHwUFuxKWI7gDeQ0lF+18Aixa+v4BB/uKfCKtPjyzREA4LqUWlNstlOfuRP9+AzVdHKWfDeotFaU8aEb/It0ktKcrEuDGcVAtpO9eWtvcoEZ8BlWaPGbVoTaxCJWDRbKe0JPz9JFPs0YEaT6nt5LaqWYI8yByK7M2thoK7DIoZ93zvqyTHM2IJjeUDWE6f+G0/aib48K0myT3g3NPvHhCXi3b879Q231oDQVDlbnX8piSoKBu/mIQ==--fOeHlW2YD8T8xWB0--pbQRclG873vKQ79QBDa07g== \ No newline at end of file diff --git a/db/migrate/20241225223325_create_filedrop_comments.rb b/db/migrate/20241225223325_create_filedrop_comments.rb new file mode 100644 index 0000000000000000000000000000000000000000..e75a7976b8d5bb47e184dd9dd9a98d39032783e3 --- /dev/null +++ b/db/migrate/20241225223325_create_filedrop_comments.rb @@ -0,0 +1,15 @@ +class CreateFiledropComments < ActiveRecord::Migration[7.1] + def change + create_table :filedrop_comments do |t| + t.text :body + t.datetime :orig_created + t.references :session, null: false, foreign_key: true + + t.timestamps + end + end + + def down + drop_table :filedrop_comments + end +end diff --git a/db/migrate/20241225223515_create_filedrop_files.rb b/db/migrate/20241225223515_create_filedrop_files.rb new file mode 100644 index 0000000000000000000000000000000000000000..becfb98d060cf24af27adee55d56c4a394657936 --- /dev/null +++ b/db/migrate/20241225223515_create_filedrop_files.rb @@ -0,0 +1,17 @@ +class CreateFiledropFiles < ActiveRecord::Migration[7.1] + def change + create_table :filedrop_files do |t| + t.string :name + t.integer :size + t.string :checksum + t.datetime :orig_created + t.references :session, null: false, foreign_key: true + + t.timestamps + end + end + + def down + drop_table :filedrop_files + end +end diff --git a/db/schema.rb b/db/schema.rb index fc0392c661bc77d2876fa992b58e410e7ea0da21..f2d669d7e780937eb61fbd77acab2e2e0d7a28f0 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_12_24_002828) do +ActiveRecord::Schema[7.1].define(version: 2024_12_25_223515) do create_table "assignments", force: :cascade do |t| t.integer "user_id", null: false t.integer "session_id", null: false @@ -55,6 +55,26 @@ ActiveRecord::Schema[7.1].define(version: 2024_12_24_002828) do t.index ["job_id"], name: "index_crono_jobs_on_job_id", unique: true end + create_table "filedrop_comments", force: :cascade do |t| + t.text "body" + t.datetime "orig_created" + t.integer "session_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["session_id"], name: "index_filedrop_comments_on_session_id" + end + + create_table "filedrop_files", force: :cascade do |t| + t.string "name" + t.integer "size" + t.string "checksum" + t.datetime "orig_created" + t.integer "session_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["session_id"], name: "index_filedrop_files_on_session_id" + end + create_table "model_versions", force: :cascade do |t| t.string "model" t.text "changed_data" @@ -290,6 +310,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_12_24_002828) do add_foreign_key "assignments", "users" add_foreign_key "candidates", "sessions" add_foreign_key "candidates", "users" + add_foreign_key "filedrop_comments", "sessions" + add_foreign_key "filedrop_files", "sessions" add_foreign_key "relevant_stages", "conferences" add_foreign_key "relevant_stages", "stages" add_foreign_key "revision_sets", "conferences" diff --git a/db/seeds.rb b/db/seeds.rb index 51926174fc983fa3b41132f29d5560192d78bae0..4b58a58f071063cb0a7325e41687950fb5247808 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -49,26 +49,34 @@ # c.save! # end -Conference.find_or_create_by!(slug: "38c3") do |c| +Conference.find_or_create_by!(slug: "38c3").tap do |c| c.name = "38th Chaos Communication Congress (de-en)" c.time_zone = "Berlin" c.starts_at = DateTime.parse("27 December 2024 10:30 CET") c.ends_at = DateTime.parse("30 December 2024 19:00 CET") c.data = { - "schedule_url" => "https://api.events.ccc.de/congress/2024/assembly/6840c453-af5c-413c-8127-adcbdcd98e9e/schedule.json" + "schedule_url" => "https://api.events.ccc.de/congress/2024/assembly/6840c453-af5c-413c-8127-adcbdcd98e9e/schedule.json", + "filedrop" => { + "url" => "https://speakers.c3lingo.org" + } + } c.import_job_class = "pretalx" c.location = "Congress Center Hamburg" c.save! end -Conference.find_or_create_by!(slug: "38c3-more") do |c| +Conference.find_or_create_by!(slug: "38c3-more").tap do |c| c.name = "38th Chaos Communication Congress (more languages)" c.time_zone = "Berlin" c.starts_at = DateTime.parse("27 December 2024 10:30 CET") c.ends_at = DateTime.parse("30 December 2024 19:00 CET") c.data = { - "schedule_url" => "https://api.events.ccc.de/congress/2024/assembly/6840c453-af5c-413c-8127-adcbdcd98e9e/schedule.json" + "schedule_url" => "https://api.events.ccc.de/congress/2024/assembly/6840c453-af5c-413c-8127-adcbdcd98e9e/schedule.json", + "filedrop" => { + "url" => "https://speakers.c3lingo.org", + "skip_downloads" => true + } } c.import_job_class = "pretalx" c.location = "Congress Center Hamburg" diff --git a/test/fixtures/filedrop_comments.yml b/test/fixtures/filedrop_comments.yml new file mode 100644 index 0000000000000000000000000000000000000000..14211cbbda587f625d9457b53957adac00bbc873 --- /dev/null +++ b/test/fixtures/filedrop_comments.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + body: MyText + session: one + +two: + body: MyText + session: two diff --git a/test/fixtures/filedrop_files.yml b/test/fixtures/filedrop_files.yml new file mode 100644 index 0000000000000000000000000000000000000000..2366dbd19b1015172fe6fe86470f17ac4602b1f8 --- /dev/null +++ b/test/fixtures/filedrop_files.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + size: 1 + checksum: MyString + session: one + +two: + name: MyString + size: 1 + checksum: MyString + session: two diff --git a/test/models/filedrop_comment_test.rb b/test/models/filedrop_comment_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..d3d6e986ba86fc5e90938d5c9d082168af5c629e --- /dev/null +++ b/test/models/filedrop_comment_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class FiledropCommentTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/filedrop_file_test.rb b/test/models/filedrop_file_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..3179bf265de23f529ab8e9a28fb134373a0a7b2c --- /dev/null +++ b/test/models/filedrop_file_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class FiledropFileTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end