class Api::V2::WorksController < Api::V2::BaseController respond_to :json # POST - search for works based on imported url def search works = params[:works] original_urls = works.map { |w| w[:original_urls] }.flatten results = [] messages = [] if original_urls.nil? || original_urls.blank? || original_urls.empty? status = :empty_request messages << "Please provide a list of URLs to find." elsif original_urls.size >= ArchiveConfig.IMPORT_MAX_CHAPTERS status = :too_many_request messages << "Please provide no more than #{ArchiveConfig.IMPORT_MAX_CHAPTERS} URLs to find." else status = :ok results = find_existing_works(original_urls) messages << "Successfully searched all provided URLs." end render_api_response(status, messages, works: results) end # POST - create a work and invite authors to claim def create archivist = User.find_by(login: params[:archivist]) external_works = params[:items] || params[:works] works_responses = [] # check for top-level errors (not an archivist, no works...) status, messages = batch_errors(archivist, external_works) if status == :ok # Process the works, updating the flags works_responses = external_works.map { |external_work| import_work(archivist, external_work.merge(params.permit!)) } success_responses, error_responses = works_responses.partition { |r| [:ok, :created, :found].include?(r[:status]) } # Send claim notification emails for successful works if params[:send_claim_emails] && success_responses.present? notified_authors = notify_and_return_authors(success_responses.map { |r| r[:work] }, archivist) messages << "Claim emails sent to #{notified_authors.to_sentence}." end # set final status code and message depending on the flags status = :bad_request if error_responses.present? messages = response_message(messages, success_responses.present?, error_responses.present?) end render_api_response(status, messages, works: works_responses.map { |r| r.except!(:work) }) end private # Set messages based on success and error flags def response_message(messages, any_success, any_errors) messages << if any_success && any_errors "At least one work was not imported. Please check individual work responses for further information." elsif !any_success && any_errors "None of the works were imported. Please check individual work responses for further information." else "All works were successfully imported." end messages end # Work-level error handling for requests that are incomplete or too large def work_errors(work) status = :bad_request errors = [] urls = work[:chapter_urls] if urls.nil? || urls.empty? status = :empty_request errors << "This work doesn't contain any chapter URLs. " + "Works can only be imported from publicly-accessible chapter URLs." elsif urls.length >= ArchiveConfig.IMPORT_MAX_CHAPTERS status = :too_many_requests errors << "This work contains too many chapter URLs. A maximum of #{ArchiveConfig.IMPORT_MAX_CHAPTERS} " \ "chapters can be imported per work." end status = :ok if errors.empty? [status, errors] end # Search for works imported from the provided URLs def find_existing_works(original_urls) results = [] messages = [] original_urls.each do |original| original_id = "" if original.class == String original_url = original else original_id = original[:id] original_url = original[:url] end # Search for works - there may be duplicates search_results = find_work_by_import_url(original_url) if search_results[:works].empty? results << { status: :not_found, original_id: original_id, original_url: original_url, messages: [search_results[:message]] } else work_results = search_results[:works].map do |work| archive_url = work_url(work) message = "Work \"#{work.title}\", created on #{work.created_at.to_date.to_fs(:iso_date)} was found at \"#{archive_url}\"." messages << message { archive_url: archive_url, created: work.created_at, message: message } end results << { status: :found, original_id: original_id, original_url: original_url, search_results: work_results, messages: messages } end end results end def find_work_by_import_url(original_url) works = nil if original_url.blank? message = "Please provide the original URL for the work." else # We know the url will be identical no need for a call to find_by_url works = Work.where(imported_from_url: original_url) message = if works.empty? "No work has been imported from \"" + original_url + "\"." else "Found #{works.size} work(s) imported from \"" + original_url + "\"." end end { original_url: original_url, works: works, message: message } end # Use the story parser to scrape works from the chapter URLs def import_work(archivist, external_work) work_status, work_messages = work_errors(external_work) work_url = "" original_url = "" if work_status == :ok urls = external_work[:chapter_urls] original_url = urls.first storyparser = StoryParser.new options = story_parser_options(archivist, external_work) begin response = storyparser.import_chapters_into_story(urls, options) work = response[:work] work_status = response[:status] work.save if work_status == :created work_url = work_url(work) work_messages << response[:message] rescue => exception Rails.logger.error("------- API v2: error: #{exception.inspect}") work_status = :unprocessable_entity work_messages << "Unable to import this work." work_messages << exception.message end end { status: work_status, archive_url: work_url, original_id: external_work[:id], original_url: original_url, messages: work_messages, work: work } end # Send invitations to external authors for a given set of works def notify_and_return_authors(success_works, archivist) notified_authors = [] external_authors = success_works.map(&:external_authors).flatten.uniq external_authors&.each do |external_author| external_author.find_or_invite(archivist) # One of the external author pseuds is its email address so filter that one out author_names = external_author.names.reject { |a| a.name == external_author.email }.map(&:name).flatten.join(", ") notified_authors << author_names end notified_authors end # Request and response hashes # Create options map for StoryParser def story_parser_options(archivist, work_params) { archivist: archivist, import_multiple: "chapters", importing_for_others: true, do_not_set_current_author: true, post_without_preview: work_params[:post_without_preview].blank? ? true : work_params[:post_without_preview], restricted: work_params[:restricted], override_tags: work_params[:override_tags].nil? ? true : work_params[:override_tags], detect_tags: work_params[:detect_tags].nil? ? true : work_params[:detect_tags], collection_names: work_params[:collection_names], title: work_params[:title], fandom: work_params[:fandoms], archive_warning: work_params[:warnings], character: work_params[:characters], rating: work_params[:rating], relationship: work_params[:relationships], category: work_params[:categories], freeform: work_params[:additional_tags], language_id: Language.find_by(short: work_params[:language_code])&.id, summary: work_params[:summary], notes: work_params[:notes], encoding: work_params[:encoding], external_author_name: work_params[:external_author_name], external_author_email: work_params[:external_author_email], external_coauthor_name: work_params[:external_coauthor_name], external_coauthor_email: work_params[:external_coauthor_email] } end end