# frozen_string_literal: true # AnswerHub Importer # # Based on having access to a mysql dump. # Pass in the ENV variables listed below before running the script. require_relative "base" require "mysql2" require "open-uri" class ImportScripts::AnswerHub < ImportScripts::Base DB_NAME = ENV["DB_NAME"] || "answerhub" DB_PASS = ENV["DB_PASS"] || "answerhub" DB_USER = ENV["DB_USER"] || "answerhub" TABLE_PREFIX = ENV["TABLE_PREFIX"] || "network1" BATCH_SIZE = ENV["BATCH_SIZE"].to_i || 1000 ATTACHMENT_DIR = ENV["ATTACHMENT_DIR"] || "" PROCESS_UPLOADS = ENV["PROCESS_UPLOADS"].to_i || 0 ANSWERHUB_DOMAIN = ENV["ANSWERHUB_DOMAIN"] AVATAR_DIR = ENV["AVATAR_DIR"] || "" SITE_ID = ENV["SITE_ID"].to_i || 0 CATEGORY_MAP_FROM = ENV["CATEGORY_MAP_FROM"].to_i || 0 CATEGORY_MAP_TO = ENV["CATEGORY_MAP_TO"].to_i || 0 SCRAPE_AVATARS = ENV["SCRAPE_AVATARS"].to_i || 0 def initialize super @client = Mysql2::Client.new(host: "localhost", username: DB_USER, password: DB_PASS, database: DB_NAME) @skip_updates = true SiteSetting.tagging_enabled = true SiteSetting.max_tags_per_topic = 10 end def execute puts "Now starting the AnswerHub Import" puts "DB Name: #{DB_NAME}" puts "Table Prefix: #{TABLE_PREFIX}" puts import_users import_categories import_topics import_posts import_groups add_users_to_groups add_moderators add_admins import_avatars create_permalinks end def import_users puts "", "creating users" query = "SELECT count(*) count FROM #{TABLE_PREFIX}_authoritables WHERE c_type = 'user' AND c_active = 1 AND c_system <> 1;" total_count = @client.query(query).first["count"] puts "Total count: #{total_count}" @last_user_id = -1 batches(BATCH_SIZE) do |offset| query = "SELECT c_id, c_creation_date, c_name, c_primaryEmail, c_last_seen, c_description FROM #{TABLE_PREFIX}_authoritables WHERE c_type = 'user' AND c_active = 1 AND c_system <> 1 AND c_id > #{@last_user_id} LIMIT #{BATCH_SIZE};" results = @client.query(query) break if results.size < 1 @last_user_id = results.to_a.last["c_id"] create_users(results, total: total_count, offset: offset) do |user| # puts user['c_id'].to_s + ' ' + user['c_name'] next if @lookup.user_id_from_imported_user_id(user["c_id"]) { id: user["c_id"], email: "#{SecureRandom.hex}@invalid.invalid", username: user["c_name"], created_at: user["c_creation_date"], bio_raw: user["c_description"], last_seen_at: user["c_last_seen"], } end end end def import_categories puts "", "importing categories..." # Import parent categories first query = "SELECT c_id, c_name, c_plug, c_parent FROM containers WHERE c_type = 'space' AND c_active = 1 AND c_parent = 7 OR c_parent IS NULL" results = @client.query(query) create_categories(results) do |c| { id: c["c_id"], name: c["c_name"], parent_category_id: check_parent_id(c["c_parent"]) } end # Import sub-categories query = "SELECT c_id, c_name, c_plug, c_parent FROM containers WHERE c_type = 'space' AND c_active = 1 AND c_parent != 7 AND c_parent IS NOT NULL" results = @client.query(query) create_categories(results) do |c| # puts c.inspect { id: c["c_id"], name: c["c_name"], parent_category_id: category_id_from_imported_category_id(check_parent_id(c["c_parent"])), } end end def import_topics puts "", "importing topics..." count_query = "SELECT count(*) count FROM #{TABLE_PREFIX}_nodes WHERE c_visibility <> 'deleted' AND (c_type = 'question' OR c_type = 'kbentry');" total_count = @client.query(count_query).first["count"] @last_topic_id = -1 batches(BATCH_SIZE) do |offset| # Let's start with just question types query = "SELECT * FROM #{TABLE_PREFIX}_nodes WHERE c_id > #{@last_topic_id} AND c_visibility <> 'deleted' AND (c_type = 'question' OR c_type = 'kbentry') ORDER BY c_id ASC LIMIT #{BATCH_SIZE};" topics = @client.query(query) break if topics.size < 1 @last_topic_id = topics.to_a.last["c_id"] create_posts(topics, total: total_count, offset: offset) do |t| user_id = user_id_from_imported_user_id(t["c_author"]) || Discourse::SYSTEM_USER_ID body = process_mentions(t["c_body"]) body = process_uploads(body, user_id) if PROCESS_UPLOADS == 1 markdown_body = HtmlToMarkdown.new(body).to_markdown { id: t["c_id"], user_id: user_id, title: t["c_title"], category: category_id_from_imported_category_id(t["c_primaryContainer"]), raw: markdown_body, created_at: t["c_creation_date"], post_create_action: proc do |post| tag_names = t["c_topic_names"].split(",") DiscourseTagging.tag_topic_by_names(post.topic, staff_guardian, tag_names) end, } end end end def import_posts puts "", "importing posts..." count_query = "SELECT count(*) count FROM #{TABLE_PREFIX}_nodes WHERE c_visibility <> 'deleted' AND (c_type = 'answer' OR c_type = 'comment' OR c_type = 'kbentry');" total_count = @client.query(count_query).first["count"] @last_post_id = -1 batches(BATCH_SIZE) do |offset| query = "SELECT * FROM #{TABLE_PREFIX}_nodes WHERE c_id > #{@last_post_id} AND c_visibility <> 'deleted' AND (c_type = 'answer' OR c_type = 'comment' OR c_type = 'kbentry') ORDER BY c_id ASC LIMIT #{BATCH_SIZE};" posts = @client.query(query) next if all_records_exist? :posts, posts.map { |p| p["c_id"] } break if posts.size < 1 @last_post_id = posts.to_a.last["c_id"] create_posts(posts, total: total_count, offset: offset) do |p| t = topic_lookup_from_imported_post_id(p["c_originalParent"]) next unless t reply_to_post_id = post_id_from_imported_post_id(p["c_parent"]) reply_to_post = reply_to_post_id.present? ? Post.find(reply_to_post_id) : nil reply_to_post_number = reply_to_post.present? ? reply_to_post.post_number : nil user_id = user_id_from_imported_user_id(p["c_author"]) || Discourse::SYSTEM_USER_ID body = process_mentions(p["c_body"]) body = process_uploads(body, user_id) if PROCESS_UPLOADS == 1 markdown_body = HtmlToMarkdown.new(body).to_markdown { id: p["c_id"], user_id: user_id, topic_id: t[:topic_id], reply_to_post_number: reply_to_post_number, raw: markdown_body, created_at: p["c_creation_date"], post_create_action: proc do |post_info| begin if p["c_type"] == "answer" && p["c_marked"] == 1 post = Post.find(post_info[:id]) if post user_id = user_id_from_imported_user_id(p["c_author"]) || Discourse::SYSTEM_USER_ID current_user = User.find(user_id) solved = DiscourseSolved.accept_answer!(post, current_user) # puts "SOLVED: #{solved}" end end rescue ActiveRecord::RecordInvalid puts "SOLVED: Skipped post_id: #{post.id} because invalid" end end, } end end end def import_groups puts "", "importing groups..." query = "SELECT c_id, c_name FROM network6_authoritables WHERE c_type='group' AND c_id > 6;" # Ignore Anonymous, Users, Moderators, etc. groups = @client.query(query) create_groups(groups) do |group| { id: group["c_id"], name: group["c_name"], visibility_level: 1 } end end def add_users_to_groups puts "", "adding users to groups..." query = "SELECT c_id, c_name FROM network6_authoritables WHERE c_type='group' AND c_id > 6;" # Ignore Anonymous, Users, Moderators, etc. groups = @client.query(query) members_query = "SELECT * FROM network6_authoritable_groups;" group_members = @client.query(members_query) total_count = groups.count progress_count = 0 start_time = Time.now group_members.map groups.each do |group| dgroup = find_group_by_import_id(group["c_id"]) next if dgroup.custom_fields["import_users_added"] group_member_ids = group_members .map do |m| user_id_from_imported_user_id(m["c_members"]) if m["c_groups"] == group["c_id"] end .compact # add members dgroup.bulk_add(group_member_ids) # reload group dgroup.reload dgroup.custom_fields["import_users_added"] = true dgroup.save progress_count += 1 print_status(progress_count, total_count, start_time) end end def add_moderators puts "", "adding moderators..." query = "SELECT * FROM network6_authoritable_groups WHERE c_groups = 4;" moderators = @client.query(query) moderator_ids = moderators.map { |m| user_id_from_imported_user_id(m["c_members"]) }.compact moderator_ids.each do |id| user = User.find(id) user.grant_moderation! end end def add_admins puts "", "adding admins..." query = "SELECT * FROM network6_authoritable_groups WHERE c_groups = 5 OR c_groups = 6;" # Super Users, Network Administrators admins = @client.query(query) admin_ids = admins.map { |a| user_id_from_imported_user_id(a["c_members"]) }.compact admin_ids.each do |id| user = User.find(id) user.grant_admin! end end def import_avatars puts "", "importing user avatars" query = "SELECT * FROM network6_user_preferences WHERE c_key = 'avatarImage'" avatars = @client.query(query) avatars.each do |a| begin user_id = user_id_from_imported_user_id(a["c_user"]) user = User.find(user_id) if user filename = "avatar-#{user_id}.png" path = File.join(AVATAR_DIR, filename) next if !File.exist?(path) # Scrape Avatars - Avatars are saved in the db, but it might be easier to just scrape them if SCRAPE_AVATARS == 1 File.open(path, "wb") do |f| f << open( "https://#{ANSWERHUB_DOMAIN}/forums/users/#{a["c_user"]}/photo/view.html?s=240", ).read end end upload = @uploader.create_upload(user.id, path, filename) if upload.persisted? user.import_mode = false user.create_user_avatar user.import_mode = true user.user_avatar.update(custom_upload_id: upload.id) user.update(uploaded_avatar_id: upload.id) else Rails.logger.error("Could not persist avatar for user #{user.username}") end end rescue ActiveRecord::RecordNotFound puts "Could not find User for user_id: #{a["c_user"]}" end end end def process_uploads(body, user_id) if body.match(//) # There could be multiple images in a post images = body.scan(//) images.each do |image| filepath = File.basename(image).split('"')[0] filepath = File.join(ATTACHMENT_DIR, filepath) if File.exist?(filepath) filename = File.basename(filepath) upload = create_upload(user_id, filepath, filename) image_html = html_for_upload(upload, filename) original_image_html = '' body.sub!(original_image_html, image_html) end end end # Non-images if body.match(//) # There could be multiple files in a post files = body.scan(//) files.each do |file| filepath = File.basename(file).split('"')[0] filepath = File.join(ATTACHMENT_DIR, filepath) if File.exist?(filepath) filename = File.basename(filepath) upload = create_upload(user_id, filepath, filename) file_html = html_for_upload(upload, filename) original_file_html = '' body.sub!(original_file_html, file_html) end end end body end def process_mentions(body) raw = body.dup # https://example.forum.com/forums/users/1469/XYZ_Rob.html raw.gsub!(%r{(https://example.forum.com/forums/users/\d+/[\w_%-.]*.html)}) do legacy_url = $1 import_user_id = legacy_url.match(%r{https://example.forum.com/forums/users/(\d+)/[\w_%-.]*.html}).captures user = @lookup.find_user_by_import_id(import_user_id[0]) if user.present? # puts "/users/#{user.username}" "/users/#{user.username}" else # puts legacy_url legacy_url end end # /forums/users/395/petrocket.html raw.gsub!(%r{(/forums/users/\d+/[\w_%-.]*.html)}) do legacy_url = $1 import_user_id = legacy_url.match(%r{/forums/users/(\d+)/[\w_%-.]*.html}).captures # puts raw user = @lookup.find_user_by_import_id(import_user_id[0]) if user.present? # puts "/users/#{user.username}" "/users/#{user.username}" else # puts legacy_url legacy_url end end raw end def create_permalinks puts "", "Creating redirects...", "" # https://example.forum.com/forums/questions/2005/missing-file.html Topic.find_each do |topic| pcf = topic.first_post.custom_fields if pcf && pcf["import_id"] id = pcf["import_id"] slug = Slug.for(topic.title) begin Permalink.create(url: "questions/#{id}/#{slug}.html", topic_id: topic.id) rescue StandardError nil end print "." end end end def staff_guardian @_staff_guardian ||= Guardian.new(Discourse.system_user) end # Some category parent id's need to be adjusted def check_parent_id(id) return nil if SITE_ID > 0 && id == SITE_ID return CATEGORY_MAP_TO if CATEGORY_MAP_FROM > 0 && id == CATEGORY_MAP_FROM id end end ImportScripts::AnswerHub.new.perform