800 lines
22 KiB
Ruby
800 lines
22 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "base"
|
|
require "mysql2"
|
|
require "htmlentities"
|
|
require "parallel"
|
|
|
|
class BulkImport::VBulletin < BulkImport::Base
|
|
TABLE_PREFIX ||= ENV["TABLE_PREFIX"] || "vb_"
|
|
SUSPENDED_TILL ||= Date.new(3000, 1, 1)
|
|
ATTACHMENT_DIR ||= ENV["ATTACHMENT_DIR"] || "/shared/import/data/attachments"
|
|
AVATAR_DIR ||= ENV["AVATAR_DIR"] || "/shared/import/data/customavatars"
|
|
|
|
def initialize
|
|
super
|
|
|
|
host = ENV["DB_HOST"] || "localhost"
|
|
username = ENV["DB_USERNAME"] || "root"
|
|
password = ENV["DB_PASSWORD"]
|
|
database = ENV["DB_NAME"] || "vbulletin"
|
|
charset = ENV["DB_CHARSET"] || "utf8"
|
|
|
|
@html_entities = HTMLEntities.new
|
|
@encoding = CHARSET_MAP[charset]
|
|
|
|
@client =
|
|
Mysql2::Client.new(
|
|
host: host,
|
|
username: username,
|
|
password: password,
|
|
database: database,
|
|
encoding: charset,
|
|
reconnect: true,
|
|
)
|
|
|
|
@client.query_options.merge!(as: :array, cache_rows: false)
|
|
|
|
@has_post_thanks = mysql_query(<<-SQL).to_a.count > 0
|
|
SELECT `COLUMN_NAME`
|
|
FROM `INFORMATION_SCHEMA`.`COLUMNS`
|
|
WHERE `TABLE_SCHEMA`='#{database}'
|
|
AND `TABLE_NAME`='user'
|
|
AND `COLUMN_NAME` LIKE 'post_thanks_%'
|
|
SQL
|
|
|
|
@user_ids_by_email = {}
|
|
end
|
|
|
|
def execute
|
|
# enable as per requirement:
|
|
# SiteSetting.automatic_backups_enabled = false
|
|
# SiteSetting.disable_emails = "non-staff"
|
|
# SiteSetting.authorized_extensions = '*'
|
|
# SiteSetting.max_image_size_kb = 102400
|
|
# SiteSetting.max_attachment_size_kb = 102400
|
|
# SiteSetting.clean_up_uploads = false
|
|
# SiteSetting.clean_orphan_uploads_grace_period_hours = 43200
|
|
|
|
import_groups
|
|
import_users
|
|
import_group_users
|
|
|
|
import_user_emails
|
|
import_user_stats
|
|
|
|
import_user_passwords
|
|
import_user_salts
|
|
import_user_profiles
|
|
|
|
import_categories
|
|
import_topics
|
|
import_posts
|
|
|
|
import_likes
|
|
|
|
import_private_topics
|
|
import_topic_allowed_users
|
|
import_private_posts
|
|
|
|
create_permalink_file
|
|
import_attachments
|
|
import_avatars
|
|
import_signatures
|
|
end
|
|
|
|
def execute_after
|
|
max_age = SiteSetting.delete_user_max_post_age
|
|
SiteSetting.delete_user_max_post_age = 50 * 365
|
|
|
|
merge_duplicated_users
|
|
|
|
SiteSetting.delete_user_max_post_age = max_age
|
|
end
|
|
|
|
def import_groups
|
|
puts "", "Importing groups..."
|
|
|
|
groups = mysql_stream <<-SQL
|
|
SELECT usergroupid, title, description, usertitle
|
|
FROM #{TABLE_PREFIX}usergroup
|
|
WHERE usergroupid > #{@last_imported_group_id}
|
|
ORDER BY usergroupid
|
|
SQL
|
|
|
|
create_groups(groups) do |row|
|
|
{
|
|
imported_id: row[0],
|
|
name: normalize_text(row[1]),
|
|
bio_raw: normalize_text(row[2]),
|
|
title: normalize_text(row[3]),
|
|
}
|
|
end
|
|
end
|
|
|
|
def import_users
|
|
puts "", "Importing users..."
|
|
|
|
users = mysql_stream <<-SQL
|
|
SELECT u.userid, username, email, joindate, birthday, ipaddress, u.usergroupid, bandate, liftdate
|
|
FROM #{TABLE_PREFIX}user u
|
|
LEFT JOIN #{TABLE_PREFIX}userban ub ON ub.userid = u.userid
|
|
WHERE u.userid > #{@last_imported_user_id}
|
|
ORDER BY u.userid
|
|
SQL
|
|
|
|
create_users(users) do |row|
|
|
u = {
|
|
imported_id: row[0],
|
|
username: normalize_text(row[1]),
|
|
name: normalize_text(row[1]),
|
|
email: row[2],
|
|
created_at: Time.zone.at(row[3]),
|
|
date_of_birth: parse_birthday(row[4]),
|
|
primary_group_id: group_id_from_imported_id(row[6]),
|
|
}
|
|
u[:ip_address] = row[5][/\b(?:\d{1,3}\.){3}\d{1,3}\b/] if row[5].present?
|
|
if row[7]
|
|
u[:suspended_at] = Time.zone.at(row[7])
|
|
u[:suspended_till] = row[8] > 0 ? Time.zone.at(row[8]) : SUSPENDED_TILL
|
|
end
|
|
u
|
|
end
|
|
end
|
|
|
|
def import_user_emails
|
|
puts "", "Importing user emails..."
|
|
|
|
users = mysql_stream <<-SQL
|
|
SELECT u.userid, email, joindate
|
|
FROM #{TABLE_PREFIX}user u
|
|
WHERE u.userid > #{@last_imported_user_id}
|
|
ORDER BY u.userid
|
|
SQL
|
|
|
|
create_user_emails(users) do |row|
|
|
user_id, email = row[0..1]
|
|
|
|
@user_ids_by_email[email.downcase] ||= []
|
|
user_ids = @user_ids_by_email[email.downcase] << user_id
|
|
|
|
if user_ids.count > 1
|
|
# fudge email to avoid conflicts; accounts from the 2nd and on will later be merged back into the first
|
|
# NOTE: gsub! is used to avoid creating a new (frozen) string
|
|
email.gsub!(/^/, SecureRandom.hex)
|
|
end
|
|
|
|
{
|
|
imported_id: user_id,
|
|
imported_user_id: user_id,
|
|
email: email,
|
|
created_at: Time.zone.at(row[2]),
|
|
}
|
|
end
|
|
|
|
# for debugging purposes; not used operationally
|
|
save_duplicated_users
|
|
end
|
|
|
|
def import_user_stats
|
|
puts "", "Importing user stats..."
|
|
|
|
users = mysql_stream <<-SQL
|
|
SELECT u.userid, joindate, posts, COUNT(t.threadid) AS threads, p.dateline
|
|
#{", post_thanks_user_amount, post_thanks_thanked_times" if @has_post_thanks}
|
|
FROM #{TABLE_PREFIX}user u
|
|
LEFT OUTER JOIN #{TABLE_PREFIX}post p ON p.postid = u.lastpostid
|
|
LEFT OUTER JOIN #{TABLE_PREFIX}thread t ON u.userid = t.postuserid
|
|
WHERE u.userid > #{@last_imported_user_id}
|
|
GROUP BY u.userid
|
|
ORDER BY u.userid
|
|
SQL
|
|
|
|
create_user_stats(users) do |row|
|
|
user = {
|
|
imported_id: row[0],
|
|
imported_user_id: row[0],
|
|
new_since: Time.zone.at(row[1]),
|
|
post_count: row[2],
|
|
topic_count: row[3],
|
|
first_post_created_at: row[4] && Time.zone.at(row[4]),
|
|
}
|
|
|
|
if @has_post_thanks
|
|
user[:likes_given] = row[5]
|
|
user[:likes_received] = row[6]
|
|
end
|
|
|
|
user
|
|
end
|
|
end
|
|
|
|
def import_group_users
|
|
puts "", "Importing group users..."
|
|
|
|
group_users = mysql_stream <<-SQL
|
|
SELECT usergroupid, userid
|
|
FROM #{TABLE_PREFIX}user
|
|
WHERE userid > #{@last_imported_user_id}
|
|
SQL
|
|
|
|
create_group_users(group_users) do |row|
|
|
{ group_id: group_id_from_imported_id(row[0]), user_id: user_id_from_imported_id(row[1]) }
|
|
end
|
|
end
|
|
|
|
def import_user_passwords
|
|
puts "", "Importing user passwords..."
|
|
|
|
user_passwords = mysql_stream <<-SQL
|
|
SELECT userid, password
|
|
FROM #{TABLE_PREFIX}user
|
|
WHERE userid > #{@last_imported_user_id}
|
|
ORDER BY userid
|
|
SQL
|
|
|
|
create_custom_fields("user", "password", user_passwords) do |row|
|
|
{ record_id: user_id_from_imported_id(row[0]), value: row[1] }
|
|
end
|
|
end
|
|
|
|
def import_user_salts
|
|
puts "", "Importing user salts..."
|
|
|
|
user_salts = mysql_stream <<-SQL
|
|
SELECT userid, salt
|
|
FROM #{TABLE_PREFIX}user
|
|
WHERE userid > #{@last_imported_user_id}
|
|
AND LENGTH(COALESCE(salt, '')) > 0
|
|
ORDER BY userid
|
|
SQL
|
|
|
|
create_custom_fields("user", "salt", user_salts) do |row|
|
|
{ record_id: user_id_from_imported_id(row[0]), value: row[1] }
|
|
end
|
|
end
|
|
|
|
def import_user_profiles
|
|
puts "", "Importing user profiles..."
|
|
|
|
user_profiles = mysql_stream <<-SQL
|
|
SELECT userid, homepage, profilevisits
|
|
FROM #{TABLE_PREFIX}user
|
|
WHERE userid > #{@last_imported_user_id}
|
|
ORDER BY userid
|
|
SQL
|
|
|
|
create_user_profiles(user_profiles) do |row|
|
|
{
|
|
user_id: user_id_from_imported_id(row[0]),
|
|
website:
|
|
(
|
|
begin
|
|
URI.parse(row[1]).to_s
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
),
|
|
views: row[2],
|
|
}
|
|
end
|
|
end
|
|
|
|
def import_categories
|
|
puts "", "Importing categories..."
|
|
|
|
categories = mysql_query(<<-SQL).to_a
|
|
select
|
|
forumid,
|
|
parentid,
|
|
case
|
|
when forumid in (
|
|
select distinct forumid from (
|
|
select forumid, title, count(title)
|
|
from forum
|
|
group by replace(replace(title, ':', ''), '&', '')
|
|
having count(title) > 1
|
|
) as duplicated_forum_ids
|
|
)
|
|
then
|
|
-- deduplicate by fudging the title; categories will needed to be manually merged later
|
|
concat(title, '_DUPLICATE_', forumid)
|
|
else
|
|
title
|
|
end as title,
|
|
description,
|
|
displayorder
|
|
from forum
|
|
order by forumid
|
|
SQL
|
|
|
|
return if categories.empty?
|
|
|
|
parent_categories = categories.select { |c| c[1] == -1 }
|
|
children_categories = categories.select { |c| c[1] != -1 }
|
|
|
|
parent_category_ids = Set.new parent_categories.map { |c| c[0] }
|
|
|
|
# cut down the tree to only 2 levels of categories
|
|
children_categories.each do |cc|
|
|
cc[1] = categories.find { |c| c[0] == cc[1] }[1] until parent_category_ids.include?(cc[1])
|
|
end
|
|
|
|
puts "", "Importing parent categories..."
|
|
create_categories(parent_categories) do |row|
|
|
{
|
|
imported_id: row[0],
|
|
name: normalize_text(row[2]),
|
|
description: normalize_text(row[3]),
|
|
position: row[4],
|
|
}
|
|
end
|
|
|
|
puts "", "Importing children categories..."
|
|
create_categories(children_categories) do |row|
|
|
{
|
|
imported_id: row[0],
|
|
name: normalize_text(row[2]),
|
|
description: normalize_text(row[3]),
|
|
position: row[4],
|
|
parent_category_id: category_id_from_imported_id(row[1]),
|
|
}
|
|
end
|
|
end
|
|
|
|
def import_topics
|
|
puts "", "Importing topics..."
|
|
|
|
topics = mysql_stream <<-SQL
|
|
SELECT threadid, title, forumid, postuserid, open, dateline, views, visible, sticky
|
|
FROM #{TABLE_PREFIX}thread t
|
|
WHERE threadid > #{@last_imported_topic_id}
|
|
AND EXISTS (SELECT 1 FROM #{TABLE_PREFIX}post p WHERE p.threadid = t.threadid)
|
|
ORDER BY threadid
|
|
SQL
|
|
|
|
create_topics(topics) do |row|
|
|
created_at = Time.zone.at(row[5])
|
|
|
|
t = {
|
|
imported_id: row[0],
|
|
title: normalize_text(row[1]),
|
|
category_id: category_id_from_imported_id(row[2]),
|
|
user_id: user_id_from_imported_id(row[3]),
|
|
closed: row[4] == 0,
|
|
created_at: created_at,
|
|
views: row[6],
|
|
visible: row[7] == 1,
|
|
}
|
|
|
|
t[:pinned_at] = created_at if row[8] == 1
|
|
|
|
t
|
|
end
|
|
end
|
|
|
|
def import_posts
|
|
puts "", "Importing posts..."
|
|
|
|
posts = mysql_stream <<-SQL
|
|
SELECT postid, p.threadid, parentid, userid, p.dateline, p.visible, pagetext
|
|
#{", post_thanks_amount" if @has_post_thanks}
|
|
|
|
FROM #{TABLE_PREFIX}post p
|
|
JOIN #{TABLE_PREFIX}thread t ON t.threadid = p.threadid
|
|
WHERE postid > #{@last_imported_post_id}
|
|
ORDER BY postid
|
|
SQL
|
|
|
|
create_posts(posts) do |row|
|
|
topic_id = topic_id_from_imported_id(row[1])
|
|
replied_post_topic_id = topic_id_from_imported_post_id(row[2])
|
|
reply_to_post_number =
|
|
topic_id == replied_post_topic_id ? post_number_from_imported_id(row[2]) : nil
|
|
|
|
post = {
|
|
imported_id: row[0],
|
|
topic_id: topic_id,
|
|
reply_to_post_number: reply_to_post_number,
|
|
user_id: user_id_from_imported_id(row[3]),
|
|
created_at: Time.zone.at(row[4]),
|
|
hidden: row[5] != 1,
|
|
raw: normalize_text(row[6]),
|
|
}
|
|
|
|
post[:like_count] = row[7] if @has_post_thanks
|
|
post
|
|
end
|
|
end
|
|
|
|
def import_likes
|
|
return unless @has_post_thanks
|
|
puts "", "Importing likes..."
|
|
|
|
@imported_likes = Set.new
|
|
@last_imported_post_id = 0
|
|
|
|
post_thanks = mysql_stream <<-SQL
|
|
SELECT postid, userid, date
|
|
FROM #{TABLE_PREFIX}post_thanks
|
|
WHERE postid > #{@last_imported_post_id}
|
|
ORDER BY postid
|
|
SQL
|
|
|
|
create_post_actions(post_thanks) do |row|
|
|
post_id = post_id_from_imported_id(row[0])
|
|
user_id = user_id_from_imported_id(row[1])
|
|
|
|
next if post_id.nil? || user_id.nil?
|
|
next if @imported_likes.add?([post_id, user_id]).nil?
|
|
|
|
{
|
|
post_id: post_id_from_imported_id(row[0]),
|
|
user_id: user_id_from_imported_id(row[1]),
|
|
post_action_type_id: 2,
|
|
created_at: Time.zone.at(row[2]),
|
|
}
|
|
end
|
|
end
|
|
|
|
def import_private_topics
|
|
puts "", "Importing private topics..."
|
|
|
|
@imported_topics = {}
|
|
|
|
topics = mysql_stream <<-SQL
|
|
SELECT pmtextid, title, fromuserid, touserarray, dateline
|
|
FROM #{TABLE_PREFIX}pmtext
|
|
WHERE pmtextid > (#{@last_imported_private_topic_id - PRIVATE_OFFSET})
|
|
ORDER BY pmtextid
|
|
SQL
|
|
|
|
create_topics(topics) do |row|
|
|
title = extract_pm_title(row[1])
|
|
user_ids = [row[2], row[3].scan(/i:(\d+)/)].flatten.map(&:to_i).sort
|
|
key = [title, user_ids]
|
|
|
|
next if @imported_topics.has_key?(key)
|
|
@imported_topics[key] = row[0] + PRIVATE_OFFSET
|
|
{
|
|
archetype: Archetype.private_message,
|
|
imported_id: row[0] + PRIVATE_OFFSET,
|
|
title: title,
|
|
user_id: user_id_from_imported_id(row[2]),
|
|
created_at: Time.zone.at(row[4]),
|
|
}
|
|
end
|
|
end
|
|
|
|
def import_topic_allowed_users
|
|
puts "", "Importing topic allowed users..."
|
|
|
|
allowed_users = Set.new
|
|
|
|
mysql_stream(<<-SQL).each do |row|
|
|
SELECT pmtextid, touserarray
|
|
FROM #{TABLE_PREFIX}pmtext
|
|
WHERE pmtextid > (#{@last_imported_private_topic_id - PRIVATE_OFFSET})
|
|
ORDER BY pmtextid
|
|
SQL
|
|
next unless topic_id = topic_id_from_imported_id(row[0] + PRIVATE_OFFSET)
|
|
row[1]
|
|
.scan(/i:(\d+)/)
|
|
.flatten
|
|
.each do |id|
|
|
next unless user_id = user_id_from_imported_id(id)
|
|
allowed_users << [topic_id, user_id]
|
|
end
|
|
end
|
|
|
|
create_topic_allowed_users(allowed_users) { |row| { topic_id: row[0], user_id: row[1] } }
|
|
end
|
|
|
|
def import_private_posts
|
|
puts "", "Importing private posts..."
|
|
|
|
posts = mysql_stream <<-SQL
|
|
SELECT pmtextid, title, fromuserid, touserarray, dateline, message
|
|
FROM #{TABLE_PREFIX}pmtext
|
|
WHERE pmtextid > #{@last_imported_private_post_id - PRIVATE_OFFSET}
|
|
ORDER BY pmtextid
|
|
SQL
|
|
|
|
create_posts(posts) do |row|
|
|
title = extract_pm_title(row[1])
|
|
user_ids = [row[2], row[3].scan(/i:(\d+)/)].flatten.map(&:to_i).sort
|
|
key = [title, user_ids]
|
|
|
|
next unless topic_id = topic_id_from_imported_id(@imported_topics[key])
|
|
|
|
{
|
|
imported_id: row[0] + PRIVATE_OFFSET,
|
|
topic_id: topic_id,
|
|
user_id: user_id_from_imported_id(row[2]),
|
|
created_at: Time.zone.at(row[4]),
|
|
raw: normalize_text(row[5]),
|
|
}
|
|
end
|
|
end
|
|
|
|
def create_permalink_file
|
|
puts "", "Creating Permalink File...", ""
|
|
|
|
total = Topic.listable_topics.count
|
|
start = Time.now
|
|
|
|
i = 0
|
|
File.open(File.expand_path("../vb_map.csv", __FILE__), "w") do |f|
|
|
Topic.listable_topics.find_each do |topic|
|
|
i += 1
|
|
pcf = topic.posts.includes(:_custom_fields).where(post_number: 1).first.custom_fields
|
|
if pcf && pcf["import_id"]
|
|
id = pcf["import_id"].split("-").last
|
|
|
|
f.print ["XXX#{id} YYY#{topic.id}"].to_csv
|
|
print "\r%7d/%7d - %6d/sec" % [i, total, i.to_f / (Time.now - start)] if i % 5000 == 0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# find the uploaded file information from the db
|
|
def find_upload(post, attachment_id)
|
|
sql =
|
|
"SELECT a.attachmentid attachment_id, a.userid user_id, a.filename filename
|
|
FROM #{TABLE_PREFIX}attachment a
|
|
WHERE a.attachmentid = #{attachment_id}"
|
|
results = mysql_query(sql)
|
|
|
|
unless row = results.first
|
|
puts "Couldn't find attachment record for attachment_id = #{attachment_id} post.id = #{post.id}"
|
|
return
|
|
end
|
|
|
|
attachment_id = row[0]
|
|
user_id = row[1]
|
|
db_filename = row[2]
|
|
|
|
filename =
|
|
File.join(ATTACHMENT_DIR, user_id.to_s.split("").join("/"), "#{attachment_id}.attach")
|
|
real_filename = db_filename
|
|
real_filename.prepend SecureRandom.hex if real_filename[0] == "."
|
|
|
|
unless File.exist?(filename)
|
|
puts "Attachment file #{row.inspect} doesn't exist"
|
|
return nil
|
|
end
|
|
|
|
upload = create_upload(post.user.id, filename, real_filename)
|
|
|
|
if upload.nil? || upload.errors.any?
|
|
puts "Upload not valid :("
|
|
puts upload.errors.inspect if upload
|
|
return
|
|
end
|
|
|
|
[upload, real_filename]
|
|
rescue Mysql2::Error => e
|
|
puts "SQL Error"
|
|
puts e.message
|
|
puts sql
|
|
end
|
|
|
|
def import_attachments
|
|
puts "", "importing attachments..."
|
|
|
|
RateLimiter.disable
|
|
current_count = 0
|
|
total_count = Post.count
|
|
success_count = 0
|
|
fail_count = 0
|
|
|
|
attachment_regex = %r{\[attach[^\]]*\](\d+)\[/attach\]}i
|
|
|
|
Post.find_each do |post|
|
|
current_count += 1
|
|
print_status current_count, total_count
|
|
|
|
new_raw = post.raw.dup
|
|
new_raw.gsub!(attachment_regex) do |s|
|
|
matches = attachment_regex.match(s)
|
|
attachment_id = matches[1]
|
|
|
|
upload, filename = find_upload(post, attachment_id)
|
|
unless upload
|
|
fail_count += 1
|
|
next
|
|
# should we strip invalid attach tags?
|
|
end
|
|
|
|
html_for_upload(upload, filename)
|
|
end
|
|
|
|
if new_raw != post.raw
|
|
PostRevisor.new(post).revise!(
|
|
post.user,
|
|
{ raw: new_raw },
|
|
bypass_bump: true,
|
|
edit_reason: "Import attachments from vBulletin",
|
|
)
|
|
end
|
|
|
|
success_count += 1
|
|
end
|
|
|
|
puts "", "imported #{success_count} attachments... failed: #{fail_count}."
|
|
RateLimiter.enable
|
|
end
|
|
|
|
def import_avatars
|
|
if AVATAR_DIR && File.exist?(AVATAR_DIR)
|
|
puts "", "importing user avatars"
|
|
|
|
RateLimiter.disable
|
|
start = Time.now
|
|
count = 0
|
|
|
|
Dir.foreach(AVATAR_DIR) do |item|
|
|
print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)]
|
|
|
|
next if item == (".") || item == ("..") || item == (".DS_Store")
|
|
next unless item =~ /avatar(\d+)_(\d).gif/
|
|
scan = item.scan(/avatar(\d+)_(\d).gif/)
|
|
next if scan[0][0].blank?
|
|
u = UserCustomField.find_by(name: "import_id", value: scan[0][0]).try(:user)
|
|
next if u.blank?
|
|
# raise "User not found for id #{user_id}" if user.blank?
|
|
|
|
photo_real_filename = File.join(AVATAR_DIR, item)
|
|
puts "#{photo_real_filename} not found" unless File.exist?(photo_real_filename)
|
|
|
|
upload = create_upload(u.id, photo_real_filename, File.basename(photo_real_filename))
|
|
count += 1
|
|
if upload.persisted?
|
|
u.import_mode = false
|
|
u.create_user_avatar
|
|
u.import_mode = true
|
|
u.user_avatar.update(custom_upload_id: upload.id)
|
|
u.update(uploaded_avatar_id: upload.id)
|
|
else
|
|
puts "Error: Upload did not persist for #{u.username} #{photo_real_filename}!"
|
|
end
|
|
end
|
|
|
|
puts "", "imported #{count} avatars..."
|
|
RateLimiter.enable
|
|
end
|
|
end
|
|
|
|
def import_signatures
|
|
puts "Importing user signatures..."
|
|
|
|
total_count = mysql_query(<<-SQL).first[0].to_i
|
|
SELECT COUNT(userid) count
|
|
FROM #{TABLE_PREFIX}sigparsed
|
|
SQL
|
|
current_count = 0
|
|
|
|
user_signatures = mysql_stream <<-SQL
|
|
SELECT userid, signatureparsed
|
|
FROM #{TABLE_PREFIX}sigparsed
|
|
ORDER BY userid
|
|
SQL
|
|
|
|
user_signatures.each do |sig|
|
|
current_count += 1
|
|
print_status current_count, total_count
|
|
user_id = sig[0]
|
|
user_sig = sig[1]
|
|
next if user_id.blank? || user_sig.blank?
|
|
|
|
u = UserCustomField.find_by(name: "import_id", value: user_id).try(:user)
|
|
next if u.blank?
|
|
|
|
# can not hold dupes
|
|
UserCustomField.where(
|
|
user_id: u.id,
|
|
name: %w[see_signatures signature_raw signature_cooked],
|
|
).destroy_all
|
|
|
|
user_sig.gsub!(%r{\[/?sigpic\]}i, "")
|
|
|
|
UserCustomField.create!(user_id: u.id, name: "see_signatures", value: true)
|
|
UserCustomField.create!(user_id: u.id, name: "signature_raw", value: user_sig)
|
|
UserCustomField.create!(
|
|
user_id: u.id,
|
|
name: "signature_cooked",
|
|
value: PrettyText.cook(user_sig, omit_nofollow: false),
|
|
)
|
|
end
|
|
end
|
|
|
|
def merge_duplicated_users
|
|
count = 0
|
|
total_count = 0
|
|
|
|
duplicated = {}
|
|
@user_ids_by_email
|
|
.select { |e, ids| ids.count > 1 }
|
|
.each_with_index do |(email, ids), i|
|
|
duplicated[email] = [ids, i]
|
|
count += 1
|
|
total_count += ids.count
|
|
end
|
|
|
|
puts "", "Merging #{total_count} duplicated users across #{count} distinct emails..."
|
|
|
|
start = Time.now
|
|
|
|
Parallel.each duplicated do |email, (user_ids, i)|
|
|
# nothing to do about these - they will remain a randomized hex string
|
|
next unless email.presence
|
|
|
|
# queried one by one to ensure ordering
|
|
first, *rest =
|
|
user_ids.map do |id|
|
|
UserCustomField.includes(:user).find_by!(name: "import_id", value: id).user
|
|
end
|
|
|
|
rest.each do |dup|
|
|
UserMerger.new(dup, first).merge!
|
|
first.reload
|
|
printf "."
|
|
end
|
|
|
|
print "\n%6d/%6d - %6d/sec" % [i, count, i.to_f / (Time.now - start)] if i % 10 == 0
|
|
end
|
|
|
|
puts
|
|
end
|
|
|
|
def save_duplicated_users
|
|
File.open("duplicated_users.json", "w+") { |f| f.puts @user_ids_by_email.to_json }
|
|
end
|
|
|
|
def read_duplicated_users
|
|
@user_ids_by_email = JSON.parse File.read("duplicated_users.json")
|
|
end
|
|
|
|
def extract_pm_title(title)
|
|
normalize_text(title).scrub.gsub(/^Re\s*:\s*/i, "")
|
|
end
|
|
|
|
def parse_birthday(birthday)
|
|
return if birthday.blank?
|
|
date_of_birth =
|
|
begin
|
|
Date.strptime(birthday.gsub(/[^\d-]+/, ""), "%m-%d-%Y")
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
return if date_of_birth.nil?
|
|
if date_of_birth.year < 1904
|
|
Date.new(1904, date_of_birth.month, date_of_birth.day)
|
|
else
|
|
date_of_birth
|
|
end
|
|
end
|
|
|
|
def print_status(current, max, start_time = nil)
|
|
if start_time.present?
|
|
elapsed_seconds = Time.now - start_time
|
|
elements_per_minute = "[%.0f items/min] " % [current / elapsed_seconds.to_f * 60]
|
|
else
|
|
elements_per_minute = ""
|
|
end
|
|
|
|
print "\r%9d / %d (%5.1f%%) %s" % [current, max, current / max.to_f * 100, elements_per_minute]
|
|
end
|
|
|
|
def mysql_stream(sql)
|
|
@client.query(sql, stream: true)
|
|
end
|
|
|
|
def mysql_query(sql)
|
|
@client.query(sql)
|
|
end
|
|
end
|
|
|
|
BulkImport::VBulletin.new.run
|