mirror of
https://github.com/discourse/discourse.git
synced 2024-12-25 18:23:57 +08:00
1107 lines
33 KiB
Ruby
1107 lines
33 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Notes:
|
|
#
|
|
# Written by Sam
|
|
#
|
|
# Lithium are quite protective of data, there is no simple way of exporting
|
|
# If you have leverage you may get a data dump, in my case it was provided in XML
|
|
# format
|
|
#
|
|
# First step is to convert it to db format so you can import it into a DB
|
|
# that was done using import_scripts/support/convert_mysql_xml_to_mysql.rb
|
|
#
|
|
|
|
require "mysql2"
|
|
require "csv"
|
|
require "reverse_markdown"
|
|
require File.expand_path(File.dirname(__FILE__) + "/base.rb")
|
|
require "htmlentities"
|
|
|
|
# remove table conversion
|
|
%i[table td tr th thead tbody].each { |tag| ReverseMarkdown::Converters.unregister(tag) }
|
|
|
|
class ImportScripts::Lithium < ImportScripts::Base
|
|
BATCH_SIZE = 1000
|
|
|
|
# CHANGE THESE BEFORE RUNNING THE IMPORTER
|
|
DATABASE = "wd"
|
|
PASSWORD = "password"
|
|
AVATAR_DIR = "/tmp/avatars"
|
|
ATTACHMENT_DIR = "/tmp/attachments"
|
|
UPLOAD_DIR = "/tmp/uploads"
|
|
|
|
OLD_DOMAIN = "community.wd.com"
|
|
|
|
TEMP = ""
|
|
|
|
USER_CUSTOM_FIELDS = [
|
|
{ name: "sso_id", user: "sso_id" },
|
|
{ name: "user_field_1", profile: "jobtitle" },
|
|
{ name: "user_field_2", profile: "company" },
|
|
{ name: "user_field_3", profile: "industry" },
|
|
]
|
|
|
|
LITHIUM_PROFILE_FIELDS =
|
|
"'profile.jobtitle', 'profile.company', 'profile.industry', 'profile.location'"
|
|
|
|
USERNAME_MAPPINGS = { admins: "admin_user" }.with_indifferent_access
|
|
|
|
def initialize
|
|
super
|
|
|
|
@old_username_to_new_usernames = {}
|
|
|
|
@htmlentities = HTMLEntities.new
|
|
|
|
@client =
|
|
Mysql2::Client.new(
|
|
host: "localhost",
|
|
username: "root",
|
|
password: PASSWORD,
|
|
database: DATABASE,
|
|
)
|
|
end
|
|
|
|
def execute
|
|
@max_start_id = Post.maximum(:id)
|
|
|
|
import_groups
|
|
import_categories
|
|
import_users
|
|
import_user_visits
|
|
import_topics
|
|
import_posts
|
|
import_likes
|
|
import_accepted_answers
|
|
import_pms
|
|
close_topics
|
|
create_permalinks
|
|
|
|
post_process_posts
|
|
end
|
|
|
|
def import_groups
|
|
puts "", "importing groups..."
|
|
|
|
groups = mysql_query <<-SQL
|
|
SELECT DISTINCT name
|
|
FROM roles
|
|
ORDER BY name
|
|
SQL
|
|
|
|
create_groups(groups) do |group|
|
|
{ id: group["name"], name: @htmlentities.decode(group["name"]).strip }
|
|
end
|
|
end
|
|
|
|
def import_users
|
|
puts "", "importing users"
|
|
|
|
user_count = mysql_query("SELECT COUNT(*) count FROM users").first["count"]
|
|
avatar_files = Dir.entries(AVATAR_DIR)
|
|
duplicate_emails =
|
|
mysql_query(
|
|
"SELECT email_lower FROM users GROUP BY email_lower HAVING COUNT(email_lower) > 1",
|
|
).map { |e| [e["email_lower"], 0] }.to_h
|
|
|
|
batches(BATCH_SIZE) do |offset|
|
|
users = mysql_query <<-SQL
|
|
SELECT id, nlogin, login_canon, email, registration_time, sso_id
|
|
FROM users
|
|
ORDER BY id
|
|
LIMIT #{BATCH_SIZE}
|
|
OFFSET #{offset}
|
|
SQL
|
|
|
|
break if users.size < 1
|
|
|
|
next if all_records_exist? :users, users.map { |u| u["id"].to_i }
|
|
|
|
users = users.to_a
|
|
first_id = users.first["id"]
|
|
last_id = users.last["id"]
|
|
|
|
profiles = mysql_query <<-SQL
|
|
SELECT user_id, param, nvalue
|
|
FROM user_profile
|
|
WHERE nvalue IS NOT NULL AND param IN (#{LITHIUM_PROFILE_FIELDS}) AND user_id >= #{first_id} AND user_id <= #{last_id}
|
|
ORDER BY user_id
|
|
SQL
|
|
|
|
create_users(users, total: user_count, offset: offset) do |user|
|
|
user_id = user["id"]
|
|
profile = profiles.select { |p| p["user_id"] == user_id }
|
|
result = profile.select { |p| p["param"] == "profile.location" }
|
|
location = result.count > 0 ? result.first["nvalue"] : nil
|
|
username = user["login_canon"]
|
|
username = USERNAME_MAPPINGS[username] if USERNAME_MAPPINGS[username].present?
|
|
|
|
email = user["email"].presence || fake_email
|
|
email_lower = email.downcase
|
|
if duplicate_emails.key?(email_lower)
|
|
duplicate_emails[email_lower] += 1
|
|
email.sub!("@", "+#{duplicate_emails[email_lower]}@") if duplicate_emails[email_lower] > 1
|
|
end
|
|
|
|
{
|
|
id: user_id,
|
|
name: user["nlogin"],
|
|
username: username,
|
|
email: email,
|
|
location: location,
|
|
custom_fields: user_custom_fields(user, profile),
|
|
# website: user["homepage"].strip,
|
|
# title: @htmlentities.decode(user["usertitle"]).strip,
|
|
# primary_group_id: group_id_from_imported_group_id(user["usergroupid"]),
|
|
created_at: unix_time(user["registration_time"]),
|
|
post_create_action:
|
|
proc do |u|
|
|
@old_username_to_new_usernames[user["login_canon"]] = u.username
|
|
|
|
# import user avatar
|
|
sso_id = u.custom_fields["sso_id"]
|
|
if sso_id.present?
|
|
prefix = "#{AVATAR_DIR}/#{sso_id}_"
|
|
file = get_file(prefix + "actual.jpeg")
|
|
file ||= get_file(prefix + "profile.jpeg")
|
|
|
|
if file.present?
|
|
upload = UploadCreator.new(file, file.path, type: "avatar").create_for(u.id)
|
|
u.create_user_avatar unless u.user_avatar
|
|
|
|
if !u.user_avatar.contains_upload?(upload.id)
|
|
u.user_avatar.update_columns(custom_upload_id: upload.id)
|
|
|
|
if u.uploaded_avatar_id.nil? ||
|
|
!u.user_avatar.contains_upload?(u.uploaded_avatar_id)
|
|
u.update_columns(uploaded_avatar_id: upload.id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
def import_user_visits
|
|
puts "", "importing user visits"
|
|
|
|
batches(BATCH_SIZE) do |offset|
|
|
visits = mysql_query <<-SQL
|
|
SELECT user_id, login_time
|
|
FROM user_log
|
|
ORDER BY user_id
|
|
LIMIT #{BATCH_SIZE}
|
|
OFFSET #{offset}
|
|
SQL
|
|
|
|
break if visits.size < 1
|
|
|
|
user_ids = visits.uniq { |v| v["user_id"] }
|
|
|
|
user_ids.each do |user_id|
|
|
user = UserCustomField.find_by(name: "import_id", value: user_id).try(:user)
|
|
raise "User not found for id #{user_id}" if user.blank?
|
|
|
|
user_visits = visits.select { |v| v["user_id"] == user_id }
|
|
user_visits.each do |v|
|
|
date = unix_time(v["login_time"])
|
|
user.update_visit_record!(date)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def user_custom_fields(user, profile)
|
|
fields = Hash.new
|
|
|
|
USER_CUSTOM_FIELDS.each do |attr|
|
|
name = attr[:name]
|
|
|
|
if attr[:user].present?
|
|
fields[name] = user[attr[:user]]
|
|
elsif attr[:profile].present? && profile.count > 0
|
|
result = profile.select { |p| p["param"] == "profile.#{attr[:profile]}" }
|
|
fields[name] = result.first["nvalue"] if result.count > 0
|
|
end
|
|
end
|
|
|
|
fields
|
|
end
|
|
|
|
def get_file(path)
|
|
return File.open(path) if File.exist?(path)
|
|
nil
|
|
end
|
|
|
|
def unix_time(t)
|
|
Time.at(t / 1000.0)
|
|
end
|
|
|
|
def import_profile_picture(old_user, imported_user)
|
|
query = mysql_query <<-SQL
|
|
SELECT filedata, filename
|
|
FROM customavatar
|
|
WHERE userid = #{old_user["userid"]}
|
|
ORDER BY dateline DESC
|
|
LIMIT 1
|
|
SQL
|
|
|
|
picture = query.first
|
|
|
|
return if picture.nil?
|
|
|
|
file = Tempfile.new("profile-picture")
|
|
file.write(picture["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
|
|
file.rewind
|
|
|
|
upload = UploadCreator.new(file, picture["filename"]).create_for(imported_user.id)
|
|
|
|
return if !upload.persisted?
|
|
|
|
imported_user.create_user_avatar
|
|
imported_user.user_avatar.update(custom_upload_id: upload.id)
|
|
imported_user.update(uploaded_avatar_id: upload.id)
|
|
ensure
|
|
begin
|
|
file.close
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
begin
|
|
file.unlind
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
end
|
|
|
|
def import_profile_background(old_user, imported_user)
|
|
query = mysql_query <<-SQL
|
|
SELECT filedata, filename
|
|
FROM customprofilepic
|
|
WHERE userid = #{old_user["userid"]}
|
|
ORDER BY dateline DESC
|
|
LIMIT 1
|
|
SQL
|
|
|
|
background = query.first
|
|
|
|
return if background.nil?
|
|
|
|
file = Tempfile.new("profile-background")
|
|
file.write(background["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
|
|
file.rewind
|
|
|
|
upload = UploadCreator.new(file, background["filename"]).create_for(imported_user.id)
|
|
|
|
return if !upload.persisted?
|
|
|
|
imported_user.user_profile.upload_profile_background(upload)
|
|
ensure
|
|
begin
|
|
file.close
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
begin
|
|
file.unlink
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
end
|
|
|
|
def import_categories
|
|
puts "", "importing top level categories..."
|
|
|
|
categories = mysql_query <<-SQL
|
|
SELECT n.node_id, n.display_id, c.nvalue c_title, b.nvalue b_title, n.position, n.parent_node_id, n.type_id
|
|
FROM nodes n
|
|
LEFT JOIN settings c ON n.node_id = c.node_id AND c.param = 'category.title'
|
|
LEFT JOIN settings b ON n.node_id = b.node_id AND b.param = 'board.title'
|
|
ORDER BY n.type_id DESC, n.node_id ASC
|
|
SQL
|
|
|
|
categories =
|
|
categories.map { |c| (c["name"] = c["c_title"] || c["b_title"] || c["display_id"]) && c }
|
|
|
|
# To prevent duplicate category names
|
|
categories =
|
|
categories.map do |category|
|
|
count = categories.to_a.count { |c| c["name"].present? && c["name"] == category["name"] }
|
|
category["name"] << " (#{category["node_id"]})" if count > 1
|
|
category
|
|
end
|
|
|
|
parent_categories = categories.select { |c| c["parent_node_id"] <= 2 }
|
|
|
|
create_categories(parent_categories) do |category|
|
|
{
|
|
id: category["node_id"],
|
|
name: category["name"],
|
|
position: category["position"],
|
|
post_create_action: lambda { |record| after_category_create(record, category) },
|
|
}
|
|
end
|
|
|
|
puts "", "importing children categories..."
|
|
|
|
children_categories = categories.select { |c| c["parent_node_id"] > 2 }
|
|
|
|
create_categories(children_categories) do |category|
|
|
{
|
|
id: category["node_id"],
|
|
name: category["name"],
|
|
position: category["position"],
|
|
parent_category_id: category_id_from_imported_category_id(category["parent_node_id"]),
|
|
post_create_action: lambda { |record| after_category_create(record, category) },
|
|
}
|
|
end
|
|
end
|
|
|
|
def after_category_create(category, params)
|
|
node_id = category.custom_fields["import_id"]
|
|
roles = mysql_query <<-SQL
|
|
SELECT name
|
|
FROM roles
|
|
WHERE node_id = #{node_id}
|
|
SQL
|
|
|
|
if roles.count > 0
|
|
category.update(read_restricted: true)
|
|
|
|
roles.each do |role|
|
|
group_id = group_id_from_imported_group_id(role["name"])
|
|
if group_id.present?
|
|
CategoryGroup.find_or_create_by(category: category, group_id: group_id) do |cg|
|
|
cg.permission_type = CategoryGroup.permission_types[:full]
|
|
end
|
|
else
|
|
puts "", "Group not found for id '#{role["name"]}'"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def staff_guardian
|
|
@_staff_guardian ||= Guardian.new(Discourse.system_user)
|
|
end
|
|
|
|
def import_topics
|
|
puts "", "importing topics..."
|
|
SiteSetting.tagging_enabled = true
|
|
default_max_tags_per_topic = SiteSetting.max_tags_per_topic
|
|
default_max_tag_length = SiteSetting.max_tag_length
|
|
SiteSetting.max_tags_per_topic = 10
|
|
SiteSetting.max_tag_length = 100
|
|
|
|
topic_count =
|
|
mysql_query("SELECT COUNT(*) count FROM message2 where id = root_id").first["count"]
|
|
topic_tags =
|
|
mysql_query(
|
|
"SELECT e.target_id, GROUP_CONCAT(l.tag_text SEPARATOR ',') tags FROM tag_events_label_message e LEFT JOIN tags_label l ON e.tag_id = l.tag_id GROUP BY e.target_id",
|
|
)
|
|
|
|
batches(BATCH_SIZE) do |offset|
|
|
topics = mysql_query <<-SQL
|
|
SELECT id, subject, body, deleted, user_id,
|
|
post_date, views, node_id, unique_id, row_version
|
|
FROM message2
|
|
WHERE id = root_id #{TEMP}
|
|
ORDER BY node_id, id
|
|
LIMIT #{BATCH_SIZE}
|
|
OFFSET #{offset}
|
|
SQL
|
|
|
|
break if topics.size < 1
|
|
|
|
next if all_records_exist? :posts, topics.map { |topic| "#{topic["node_id"]} #{topic["id"]}" }
|
|
|
|
create_posts(topics, total: topic_count, offset: offset) do |topic|
|
|
category_id = category_id_from_imported_category_id(topic["node_id"])
|
|
deleted_at = topic["deleted"] == 1 ? topic["row_version"] : nil
|
|
raw = topic["body"]
|
|
|
|
if category_id.present? && raw.present?
|
|
{
|
|
id: "#{topic["node_id"]} #{topic["id"]}",
|
|
user_id: user_id_from_imported_user_id(topic["user_id"]) || Discourse::SYSTEM_USER_ID,
|
|
title: @htmlentities.decode(topic["subject"]).strip[0...255],
|
|
category: category_id,
|
|
raw: raw,
|
|
created_at: unix_time(topic["post_date"]),
|
|
deleted_at: deleted_at,
|
|
views: topic["views"],
|
|
custom_fields: {
|
|
import_unique_id: topic["unique_id"],
|
|
},
|
|
import_mode: true,
|
|
post_create_action:
|
|
proc do |post|
|
|
result = topic_tags.select { |t| t["target_id"] == topic["unique_id"] }
|
|
if result.count > 0
|
|
tag_names = result.first["tags"].split(",")
|
|
DiscourseTagging.tag_topic_by_names(post.topic, staff_guardian, tag_names)
|
|
end
|
|
end,
|
|
}
|
|
else
|
|
message = "Unknown"
|
|
message = "Category '#{category_id}' not exist" if category_id.blank?
|
|
message = "Topic 'body' is empty" if raw.blank?
|
|
PluginStoreRow.find_or_create_by(
|
|
plugin_name: "topic_import_log",
|
|
key: topic["unique_id"].to_s,
|
|
value: message,
|
|
type_name: "String",
|
|
)
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
SiteSetting.max_tags_per_topic = default_max_tags_per_topic
|
|
SiteSetting.max_tag_length = default_max_tag_length
|
|
end
|
|
|
|
def import_posts
|
|
post_count =
|
|
mysql_query(
|
|
"SELECT COUNT(*) count FROM message2
|
|
WHERE id <> root_id",
|
|
).first[
|
|
"count"
|
|
]
|
|
|
|
puts "", "importing posts... (#{post_count})"
|
|
|
|
batches(BATCH_SIZE) do |offset|
|
|
posts = mysql_query <<-SQL
|
|
SELECT id, body, deleted, user_id,
|
|
post_date, parent_id, root_id, node_id, unique_id, row_version
|
|
FROM message2
|
|
WHERE id <> root_id #{TEMP}
|
|
ORDER BY node_id, root_id, id
|
|
LIMIT #{BATCH_SIZE}
|
|
OFFSET #{offset}
|
|
SQL
|
|
|
|
break if posts.size < 1
|
|
|
|
if all_records_exist? :posts,
|
|
posts.map { |post|
|
|
"#{post["node_id"]} #{post["root_id"]} #{post["id"]}"
|
|
}
|
|
next
|
|
end
|
|
|
|
create_posts(posts, total: post_count, offset: offset) do |post|
|
|
raw = post["raw"]
|
|
unless topic = topic_lookup_from_imported_post_id("#{post["node_id"]} #{post["root_id"]}")
|
|
next
|
|
end
|
|
|
|
deleted_at = topic["deleted"] == 1 ? topic["row_version"] : nil
|
|
raw = post["body"]
|
|
|
|
if raw.present?
|
|
new_post = {
|
|
id: "#{post["node_id"]} #{post["root_id"]} #{post["id"]}",
|
|
user_id: user_id_from_imported_user_id(post["user_id"]) || Discourse::SYSTEM_USER_ID,
|
|
topic_id: topic[:topic_id],
|
|
raw: raw,
|
|
created_at: unix_time(post["post_date"]),
|
|
deleted_at: deleted_at,
|
|
custom_fields: {
|
|
import_unique_id: post["unique_id"],
|
|
},
|
|
import_mode: true,
|
|
}
|
|
|
|
if parent =
|
|
topic_lookup_from_imported_post_id(
|
|
"#{post["node_id"]} #{post["root_id"]} #{post["parent_id"]}",
|
|
)
|
|
new_post[:reply_to_post_number] = parent[:post_number]
|
|
end
|
|
|
|
new_post
|
|
else
|
|
PluginStoreRow.find_or_create_by(
|
|
plugin_name: "post_import_log",
|
|
key: post["unique_id"].to_s,
|
|
value: "Post 'body' is empty",
|
|
type_name: "String",
|
|
)
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
SMILEY_SUBS = {
|
|
"smileyhappy" => "smiley",
|
|
"smileyindifferent" => "neutral_face",
|
|
"smileymad" => "angry",
|
|
"smileysad" => "cry",
|
|
"smileysurprised" => "dizzy_face",
|
|
"smileytongue" => "stuck_out_tongue",
|
|
"smileyvery-happy" => "grin",
|
|
"smileywink" => "wink",
|
|
"smileyfrustrated" => "confounded",
|
|
"smileyembarrassed" => "flushed",
|
|
"smileylol" => "laughing",
|
|
"cathappy" => "smiley_cat",
|
|
"catindifferent" => "cat",
|
|
"catmad" => "smirk_cat",
|
|
"catsad" => "crying_cat_face",
|
|
"catsurprised" => "scream_cat",
|
|
"cattongue" => "stuck_out_tongue",
|
|
"catvery-happy" => "smile_cat",
|
|
"catwink" => "wink",
|
|
"catfrustrated" => "grumpycat",
|
|
"catembarrassed" => "kissing_cat",
|
|
"catlol" => "joy_cat",
|
|
}
|
|
|
|
def import_likes
|
|
puts "\nimporting likes..."
|
|
|
|
sql =
|
|
"select source_id user_id, target_id post_id, row_version created_at from tag_events_score_message"
|
|
results = mysql_query(sql)
|
|
|
|
puts "loading unique id map"
|
|
existing_map = {}
|
|
PostCustomField
|
|
.where(name: "import_unique_id")
|
|
.pluck(:post_id, :value)
|
|
.each { |post_id, import_id| existing_map[import_id] = post_id }
|
|
|
|
puts "loading data into temp table"
|
|
DB.exec(
|
|
"create temp table like_data(user_id int, post_id int, created_at timestamp without time zone)",
|
|
)
|
|
PostAction.transaction do
|
|
results.each do |result|
|
|
result["user_id"] = user_id_from_imported_user_id(result["user_id"].to_s)
|
|
result["post_id"] = existing_map[result["post_id"].to_s]
|
|
|
|
next unless result["user_id"] && result["post_id"]
|
|
|
|
DB.exec(
|
|
"INSERT INTO like_data VALUES (:user_id,:post_id,:created_at)",
|
|
user_id: result["user_id"],
|
|
post_id: result["post_id"],
|
|
created_at: result["created_at"],
|
|
)
|
|
end
|
|
end
|
|
|
|
puts "creating missing post actions"
|
|
DB.exec <<~SQL
|
|
|
|
INSERT INTO post_actions (post_id, user_id, post_action_type_id, created_at, updated_at)
|
|
SELECT l.post_id, l.user_id, 2, l.created_at, l.created_at FROM like_data l
|
|
LEFT JOIN post_actions a ON a.post_id = l.post_id AND l.user_id = a.user_id AND a.post_action_type_id = 2
|
|
WHERE a.id IS NULL
|
|
SQL
|
|
|
|
puts "creating missing user actions"
|
|
DB.exec <<~SQL
|
|
INSERT INTO user_actions (user_id, action_type, target_topic_id, target_post_id, acting_user_id, created_at, updated_at)
|
|
SELECT pa.user_id, 1, p.topic_id, p.id, pa.user_id, pa.created_at, pa.created_at
|
|
FROM post_actions pa
|
|
JOIN posts p ON p.id = pa.post_id
|
|
LEFT JOIN user_actions ua ON action_type = 1 AND ua.target_post_id = pa.post_id AND ua.user_id = pa.user_id
|
|
|
|
WHERE ua.id IS NULL AND pa.post_action_type_id = 2
|
|
SQL
|
|
|
|
# reverse action
|
|
DB.exec <<~SQL
|
|
INSERT INTO user_actions (user_id, action_type, target_topic_id, target_post_id, acting_user_id, created_at, updated_at)
|
|
SELECT p.user_id, 2, p.topic_id, p.id, pa.user_id, pa.created_at, pa.created_at
|
|
FROM post_actions pa
|
|
JOIN posts p ON p.id = pa.post_id
|
|
LEFT JOIN user_actions ua ON action_type = 2 AND ua.target_post_id = pa.post_id AND
|
|
ua.acting_user_id = pa.user_id AND ua.user_id = p.user_id
|
|
|
|
WHERE ua.id IS NULL AND pa.post_action_type_id = 2
|
|
SQL
|
|
puts "updating like counts on posts"
|
|
|
|
DB.exec <<~SQL
|
|
UPDATE posts SET like_count = coalesce(cnt,0)
|
|
FROM (
|
|
SELECT post_id, count(*) cnt
|
|
FROM post_actions
|
|
WHERE post_action_type_id = 2 AND deleted_at IS NULL
|
|
GROUP BY post_id
|
|
) x
|
|
WHERE posts.like_count <> x.cnt AND posts.id = x.post_id
|
|
|
|
SQL
|
|
|
|
puts "updating like counts on topics"
|
|
|
|
DB.exec <<-SQL
|
|
UPDATE topics SET like_count = coalesce(cnt,0)
|
|
FROM (
|
|
SELECT topic_id, sum(like_count) cnt
|
|
FROM posts
|
|
WHERE deleted_at IS NULL
|
|
GROUP BY topic_id
|
|
) x
|
|
WHERE topics.like_count <> x.cnt AND topics.id = x.topic_id
|
|
|
|
SQL
|
|
end
|
|
|
|
def import_accepted_answers
|
|
puts "\nimporting accepted answers..."
|
|
|
|
sql =
|
|
"select unique_id post_id from message2 where (attributes & 0x4000 ) != 0 and deleted = 0;"
|
|
results = mysql_query(sql)
|
|
|
|
puts "loading unique id map"
|
|
existing_map = {}
|
|
PostCustomField
|
|
.where(name: "import_unique_id")
|
|
.pluck(:post_id, :value)
|
|
.each { |post_id, import_id| existing_map[import_id] = post_id }
|
|
|
|
puts "loading data into temp table"
|
|
DB.exec("create temp table accepted_data(post_id int primary key)")
|
|
PostAction.transaction do
|
|
results.each do |result|
|
|
result["post_id"] = existing_map[result["post_id"].to_s]
|
|
|
|
next unless result["post_id"]
|
|
|
|
DB.exec("INSERT INTO accepted_data VALUES (:post_id)", post_id: result["post_id"])
|
|
end
|
|
end
|
|
|
|
puts "deleting dupe answers"
|
|
DB.exec <<~SQL
|
|
DELETE FROM accepted_data WHERE post_id NOT IN (
|
|
SELECT post_id FROM
|
|
(
|
|
SELECT topic_id, MIN(post_id) post_id
|
|
FROM accepted_data a
|
|
JOIN posts p ON p.id = a.post_id
|
|
GROUP BY topic_id
|
|
) X
|
|
)
|
|
SQL
|
|
|
|
puts "importing accepted answers"
|
|
DB.exec <<~SQL
|
|
INSERT into post_custom_fields (name, value, post_id, created_at, updated_at)
|
|
SELECT 'is_accepted_answer', 'true', a.post_id, current_timestamp, current_timestamp
|
|
FROM accepted_data a
|
|
LEFT JOIN post_custom_fields f ON name = 'is_accepted_answer' AND f.post_id = a.post_id
|
|
WHERE f.id IS NULL
|
|
SQL
|
|
|
|
puts "marking accepted topics"
|
|
DB.exec <<~SQL
|
|
INSERT into topic_custom_fields (name, value, topic_id, created_at, updated_at)
|
|
SELECT 'accepted_answer_post_id', a.post_id::varchar, p.topic_id, current_timestamp, current_timestamp
|
|
FROM accepted_data a
|
|
JOIN posts p ON p.id = a.post_id
|
|
LEFT JOIN topic_custom_fields f ON name = 'accepted_answer_post_id' AND f.topic_id = p.topic_id
|
|
WHERE f.id IS NULL
|
|
SQL
|
|
puts "done importing accepted answers"
|
|
end
|
|
|
|
def import_pms
|
|
puts "", "importing pms..."
|
|
|
|
puts "determining participation records"
|
|
|
|
inbox = mysql_query("SELECT note_id, recipient_user_id user_id FROM tblia_notes_inbox")
|
|
outbox = mysql_query("SELECT note_id, recipient_id user_id FROM tblia_notes_outbox")
|
|
|
|
users = {}
|
|
|
|
[inbox, outbox].each do |r|
|
|
r.each do |row|
|
|
ary = (users[row["note_id"]] ||= Set.new)
|
|
user_id = user_id_from_imported_user_id(row["user_id"])
|
|
ary << user_id if user_id
|
|
end
|
|
end
|
|
|
|
puts "untangling PM soup"
|
|
|
|
note_to_subject = {}
|
|
subject_to_first_note = {}
|
|
|
|
mysql_query(
|
|
"SELECT note_id, subject, sender_user_id FROM tblia_notes_content order by note_id",
|
|
).each do |row|
|
|
user_id = user_id_from_imported_user_id(row["sender_user_id"])
|
|
ary = (users[row["note_id"]] ||= Set.new)
|
|
ary << user_id if user_id
|
|
note_to_subject[row["note_id"]] = row["subject"]
|
|
|
|
subject_to_first_note[[row["subject"], ary]] ||= row["note_id"] if row["subject"] !~ /^Re: /
|
|
end
|
|
|
|
puts "Loading user_id to username map"
|
|
user_map = {}
|
|
User.pluck(:id, :username).each { |id, username| user_map[id] = username }
|
|
|
|
topic_count = mysql_query("SELECT COUNT(*) count FROM tblia_notes_content").first["count"]
|
|
|
|
batches(BATCH_SIZE) do |offset|
|
|
topics = mysql_query <<-SQL
|
|
SELECT note_id, subject, body, sender_user_id, sent_time
|
|
FROM tblia_notes_content
|
|
ORDER BY note_id
|
|
LIMIT #{BATCH_SIZE}
|
|
OFFSET #{offset}
|
|
SQL
|
|
|
|
break if topics.size < 1
|
|
|
|
next if all_records_exist? :posts, topics.map { |topic| "pm_#{topic["note_id"]}" }
|
|
|
|
create_posts(topics, total: topic_count, offset: offset) do |topic|
|
|
user_id =
|
|
user_id_from_imported_user_id(topic["sender_user_id"]) || Discourse::SYSTEM_USER_ID
|
|
participants = users[topic["note_id"]]
|
|
|
|
usernames = (participants - [user_id]).map { |id| user_map[id] }
|
|
|
|
subject = topic["subject"]
|
|
topic_id = nil
|
|
|
|
if subject =~ /^Re: /
|
|
parent_id = subject_to_first_note[[subject[4..-1], participants]]
|
|
if parent_id
|
|
if t = topic_lookup_from_imported_post_id("pm_#{parent_id}")
|
|
topic_id = t[:topic_id]
|
|
end
|
|
end
|
|
end
|
|
|
|
raw = topic["body"]
|
|
|
|
if raw.present?
|
|
msg = {
|
|
id: "pm_#{topic["note_id"]}",
|
|
user_id: user_id,
|
|
raw: raw,
|
|
created_at: unix_time(topic["sent_time"]),
|
|
import_mode: true,
|
|
}
|
|
|
|
if topic_id
|
|
msg[:topic_id] = topic_id
|
|
else
|
|
msg[:title] = @htmlentities.decode(topic["subject"]).strip[0...255]
|
|
msg[:archetype] = Archetype.private_message
|
|
msg[:target_usernames] = usernames.join(",")
|
|
end
|
|
|
|
msg
|
|
else
|
|
PluginStoreRow.find_or_create_by(
|
|
plugin_name: "pm_import_log",
|
|
key: topic["note_id"].to_s,
|
|
value: "PM 'body' is empty",
|
|
type_name: "String",
|
|
)
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def close_topics
|
|
puts "\nclosing closed topics..."
|
|
|
|
sql =
|
|
"select unique_id post_id from message2 where root_id = id AND (attributes & 0x0002 ) != 0;"
|
|
results = mysql_query(sql)
|
|
|
|
# loading post map
|
|
existing_map = {}
|
|
PostCustomField
|
|
.where(name: "import_unique_id")
|
|
.pluck(:post_id, :value)
|
|
.each { |post_id, import_id| existing_map[import_id.to_i] = post_id.to_i }
|
|
|
|
results
|
|
.map { |r| r["post_id"] }
|
|
.each_slice(500) do |ids|
|
|
mapped = ids.map { |id| existing_map[id] }.compact
|
|
DB.exec(<<~SQL, ids: mapped) if mapped.present?
|
|
UPDATE topics SET closed = true
|
|
WHERE id IN (SELECT topic_id FROM posts where id in (:ids))
|
|
SQL
|
|
end
|
|
end
|
|
|
|
def create_permalinks
|
|
puts "Creating permalinks"
|
|
|
|
SiteSetting.permalink_normalizations = '/t5\\/.*p\\/(\\d+).*//p/\\1'
|
|
|
|
sql = <<-SQL
|
|
INSERT INTO permalinks (url, topic_id, created_at, updated_at)
|
|
SELECT '/p/' || value, p.topic_id, current_timestamp, current_timestamp
|
|
FROM post_custom_fields f
|
|
JOIN posts p on f.post_id = p.id AND post_number = 1
|
|
LEFT JOIN permalinks pm ON url = '/p/' || value
|
|
WHERE pm.id IS NULL AND f.name = 'import_unique_id'
|
|
SQL
|
|
|
|
r = DB.exec sql
|
|
puts "#{r} permalinks to topics added!"
|
|
|
|
sql = <<-SQL
|
|
INSERT INTO permalinks (url, post_id, created_at, updated_at)
|
|
SELECT '/p/' || value, p.id, current_timestamp, current_timestamp
|
|
FROM post_custom_fields f
|
|
JOIN posts p on f.post_id = p.id AND post_number <> 1
|
|
LEFT JOIN permalinks pm ON url = '/p/' || value
|
|
WHERE pm.id IS NULL AND f.name = 'import_unique_id'
|
|
SQL
|
|
|
|
r = DB.exec sql
|
|
puts "#{r} permalinks to posts added!"
|
|
end
|
|
|
|
def find_upload(user_id, attachment_id, real_filename)
|
|
filename = attachment_id.to_s.rjust(4, "0")
|
|
filename = File.join(ATTACHMENT_DIR, "000#{filename[0]}/#{filename}.dat")
|
|
|
|
unless File.exist?(filename)
|
|
puts "Attachment file doesn't exist: #{filename}"
|
|
return nil
|
|
end
|
|
real_filename.prepend SecureRandom.hex if real_filename[0] == "."
|
|
upload = create_upload(user_id, filename, real_filename)
|
|
|
|
if upload.nil? || !upload.valid?
|
|
puts "Upload not valid :("
|
|
puts upload.errors.inspect if upload
|
|
return nil
|
|
end
|
|
|
|
[upload, real_filename]
|
|
end
|
|
|
|
def post_process_posts
|
|
puts "", "Postprocessing posts..."
|
|
|
|
default_extensions = SiteSetting.authorized_extensions
|
|
default_max_att_size = SiteSetting.max_attachment_size_kb
|
|
SiteSetting.authorized_extensions = "*"
|
|
SiteSetting.max_attachment_size_kb = 307_200
|
|
|
|
current = 0
|
|
max = Post.count
|
|
|
|
begin
|
|
mysql_query("create index idxUniqueId on message2(unique_id)")
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
attachments =
|
|
mysql_query(
|
|
"SELECT a.attachment_id, a.file_name, m.message_uid FROM tblia_attachment a INNER JOIN tblia_message_attachments m ON a.attachment_id = m.attachment_id",
|
|
)
|
|
|
|
Post
|
|
.where("id > ?", @max_start_id)
|
|
.find_each do |post|
|
|
begin
|
|
id = post.custom_fields["import_unique_id"]
|
|
next unless id
|
|
raw = mysql_query("select body from message2 where unique_id = '#{id}'").first["body"]
|
|
unless raw
|
|
puts "Missing raw for post: #{post.id}"
|
|
next
|
|
end
|
|
new_raw = postprocess_post_raw(raw, post.user_id)
|
|
files = attachments.select { |a| a["message_uid"].to_s == id }
|
|
new_raw << html_for_attachments(post.user_id, files)
|
|
unless post.raw == new_raw
|
|
post.raw = new_raw
|
|
post.cooked = post.cook(new_raw)
|
|
cpp = CookedPostProcessor.new(post)
|
|
cpp.link_post_uploads
|
|
post.custom_fields["import_post_process"] = true
|
|
post.save
|
|
end
|
|
rescue PrettyText::JavaScriptError
|
|
puts "GOT A JS error on post: #{post.id}"
|
|
nil
|
|
ensure
|
|
print_status(current += 1, max)
|
|
end
|
|
end
|
|
|
|
SiteSetting.authorized_extensions = default_extensions
|
|
SiteSetting.max_attachment_size_kb = default_max_att_size
|
|
end
|
|
|
|
def postprocess_post_raw(raw, user_id)
|
|
matches = raw.match(%r{<messagetemplate.*</messagetemplate>}m) || []
|
|
matches.each do |match|
|
|
hash = Hash.from_xml(match)
|
|
template = hash["messagetemplate"]["zone"]["item"]
|
|
content = (template[0] || template)["content"] || ""
|
|
raw.sub!(match, content)
|
|
end
|
|
|
|
doc = Nokogiri::HTML5.fragment(raw)
|
|
|
|
doc
|
|
.css("a,img,li-image")
|
|
.each do |l|
|
|
upload_name, image, linked_upload = [nil] * 3
|
|
|
|
if l.name == "li-image" && l["id"]
|
|
upload_name = l["id"]
|
|
else
|
|
uri =
|
|
begin
|
|
URI.parse(l["href"] || l["src"])
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
uri.hostname = nil if uri && uri.hostname == OLD_DOMAIN
|
|
|
|
if uri && !uri.hostname
|
|
if l["href"]
|
|
l["href"] = uri.path
|
|
# we have an internal link, lets see if we can remap it?
|
|
permalink =
|
|
begin
|
|
Permalink.find_by_url(uri.path)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
|
|
if l["href"]
|
|
l["href"] = permalink.target_url if permalink && permalink.target_url
|
|
end
|
|
elsif l["src"]
|
|
# we need an upload here
|
|
upload_name = $1 if uri.path =~ %r{image-id/([^/]+)}
|
|
end
|
|
end
|
|
end
|
|
|
|
if upload_name
|
|
png = UPLOAD_DIR + "/" + upload_name + ".png"
|
|
jpg = UPLOAD_DIR + "/" + upload_name + ".jpg"
|
|
gif = UPLOAD_DIR + "/" + upload_name + ".gif"
|
|
|
|
# check to see if we have it
|
|
if File.exist?(png)
|
|
image = png
|
|
elsif File.exist?(jpg)
|
|
image = jpg
|
|
elsif File.exist?(gif)
|
|
image = gif
|
|
end
|
|
|
|
if image
|
|
File.open(image) do |file|
|
|
upload =
|
|
UploadCreator.new(
|
|
file,
|
|
"image." + (image.ends_with?(".png") ? "png" : "jpg"),
|
|
).create_for(user_id)
|
|
l.name = "img" if l.name == "li-image"
|
|
l["src"] = upload.url
|
|
end
|
|
else
|
|
puts "image was missing #{l["src"]}"
|
|
end
|
|
elsif linked_upload
|
|
segments = linked_upload.match(%r{/(\d*)/(\d)/([^.]*).(\w*)$})
|
|
|
|
if segments.present?
|
|
lithium_post_id = segments[1]
|
|
attachment_number = segments[2]
|
|
|
|
result =
|
|
mysql_query(
|
|
"select a.attachment_id, f.file_name from tblia_message_attachments a
|
|
INNER JOIN message2 m ON a.message_uid = m.unique_id
|
|
INNER JOIN tblia_attachment f ON a.attachment_id = f.attachment_id
|
|
where m.id = #{lithium_post_id} AND a.attach_num = #{attachment_number} limit 0, 1",
|
|
)
|
|
|
|
result.each do |row|
|
|
upload, filename = find_upload(user_id, row["attachment_id"], row["file_name"])
|
|
if upload.present?
|
|
l["href"] = upload.url
|
|
else
|
|
puts "attachment was missing #{l["href"]}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# for user mentions
|
|
doc
|
|
.css("li-user")
|
|
.each do |l|
|
|
uid = l["uid"]
|
|
|
|
if uid.present?
|
|
user = UserCustomField.find_by(name: "import_id", value: uid).try(:user)
|
|
if user.present?
|
|
username = user.username
|
|
span = l.document.create_element "span"
|
|
span.inner_html = "@#{username}"
|
|
l.replace span
|
|
end
|
|
end
|
|
end
|
|
|
|
raw = ReverseMarkdown.convert(doc.to_s)
|
|
raw.gsub!(/^\s* \s*$/, "")
|
|
# ugly quotes
|
|
raw.gsub!(/^>[\s\*]*$/, "")
|
|
raw.gsub!(/:([a-z]+):/) { |match| ":#{SMILEY_SUBS[$1] || $1}:" }
|
|
# nbsp central
|
|
raw.gsub!(/([a-zA-Z0-9]) ([a-zA-Z0-9])/, "\\1 \\2")
|
|
raw
|
|
end
|
|
|
|
def html_for_attachments(user_id, files)
|
|
html = +""
|
|
|
|
files.each do |file|
|
|
upload, filename = find_upload(user_id, file["attachment_id"], file["file_name"])
|
|
if upload.present?
|
|
html << "\n" if html.present?
|
|
html << html_for_upload(upload, filename)
|
|
end
|
|
end
|
|
|
|
html
|
|
end
|
|
|
|
def mysql_query(sql)
|
|
@client.query(sql, cache_rows: true)
|
|
end
|
|
end
|
|
|
|
ImportScripts::Lithium.new.perform
|