DEV: Improve phpBB3 import script (#15956)

* Optional import of custom user fields from phpBB 3.1+
* Optional import of likes from phpBB3
  Requires the phpBB "Thanks for posts" extension
* Fix import of bookmarks from phpBB3
* Update `created_at` of existing user
* Support mapping of phpBB forums to existing Discourse categories
  This is in addition to the ability of merging phpBB forums and importing into newly created Discourse categories.
This commit is contained in:
Gerhard Schlager 2022-02-16 13:04:31 +01:00 committed by GitHub
parent e945f301d1
commit 6394d7cddf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 171 additions and 22 deletions

View File

@ -366,6 +366,7 @@ class ImportScripts::Base
# try based on email # try based on email
if e.try(:record).try(:errors).try(:messages).try(:[], :primary_email).present? if e.try(:record).try(:errors).try(:messages).try(:[], :primary_email).present?
if existing = User.find_by_email(opts[:email].downcase) if existing = User.find_by_email(opts[:email].downcase)
existing.created_at = opts[:created_at] if opts[:created_at]
existing.custom_fields["import_id"] = import_id existing.custom_fields["import_id"] = import_id
existing.save! existing.save!
u = existing u = existing
@ -630,6 +631,35 @@ class ImportScripts::Base
[created, skipped] [created, skipped]
end end
def create_likes(results, opts = {})
created = 0
skipped = 0
total = opts[:total] || results.count
results.each do |result|
params = yield(result)
if params.nil?
skipped += 1
else
created_by = User.find_by(id: user_id_from_imported_user_id(params[:user_id]))
post = Post.find_by(id: post_id_from_imported_post_id(params[:post_id]))
if created_by && post
PostActionCreator.create(created_by, post, :like, created_at: params[:created_at])
created += 1
else
skipped += 1
puts "Skipping like for user id #{params[:user_id]} and post id #{params[:post_id]}"
end
end
print_status(created + skipped + (opts[:offset] || 0), total, get_start_time("likes"))
end
[created, skipped]
end
def close_inactive_topics(opts = {}) def close_inactive_topics(opts = {})
num_days = opts[:days] || 30 num_days = opts[:days] || 30
puts '', "Closing topics that have been inactive for more than #{num_days} days." puts '', "Closing topics that have been inactive for more than #{num_days} days."

View File

@ -13,7 +13,7 @@ module ImportScripts::PhpBB3
SQL SQL
end end
def fetch_users(last_user_id) def fetch_users(last_user_id, _profile_fields)
query(<<-SQL, :user_id) query(<<-SQL, :user_id)
SELECT u.user_id, u.user_email, u.username, u.user_password, u.user_regdate, u.user_lastvisit, u.user_ip, SELECT u.user_id, u.user_email, u.username, u.user_password, u.user_regdate, u.user_lastvisit, u.user_ip,
u.user_type, u.user_inactive_reason, g.group_name, b.ban_start, b.ban_end, b.ban_reason, u.user_type, u.user_inactive_reason, g.group_name, b.ban_start, b.ban_end, b.ban_reason,
@ -227,12 +227,31 @@ module ImportScripts::PhpBB3
SELECT b.user_id, t.topic_first_post_id SELECT b.user_id, t.topic_first_post_id
FROM #{@table_prefix}bookmarks b FROM #{@table_prefix}bookmarks b
JOIN #{@table_prefix}topics t ON (b.topic_id = t.topic_id) JOIN #{@table_prefix}topics t ON (b.topic_id = t.topic_id)
WHERE b.user_id > #{last_user_id} WHERE (b.user_id, b.topic_id) > (#{last_user_id}, #{last_topic_id})
ORDER BY b.user_id, b.topic_id ORDER BY b.user_id, b.topic_id
LIMIT #{@batch_size} LIMIT #{@batch_size}
SQL SQL
end end
def count_likes
count(<<-SQL)
SELECT COUNT(*) AS count
FROM #{@table_prefix}thanks
WHERE user_id <> poster_id
SQL
end
def fetch_likes(last_post_id, last_user_id)
query(<<-SQL, :post_id, :user_id)
SELECT post_id, user_id, thanks_time
FROM #{@table_prefix}thanks
WHERE user_id <> poster_id
AND (post_id, user_id) > (#{last_post_id}, #{last_user_id})
ORDER BY post_id, user_id
LIMIT #{@batch_size}
SQL
end
def get_smiley(smiley_code) def get_smiley(smiley_code)
query(<<-SQL).first query(<<-SQL).first
SELECT emotion, smiley_url SELECT emotion, smiley_url

View File

@ -5,7 +5,7 @@ require_relative '../support/constants'
module ImportScripts::PhpBB3 module ImportScripts::PhpBB3
class Database_3_1 < Database_3_0 class Database_3_1 < Database_3_0
def fetch_users(last_user_id) def fetch_users(last_user_id, profile_fields)
query(<<-SQL, :user_id) query(<<-SQL, :user_id)
SELECT u.user_id, u.user_email, u.username, SELECT u.user_id, u.user_email, u.username,
CASE WHEN u.user_password LIKE '$2y$%' CASE WHEN u.user_password LIKE '$2y$%'
@ -15,6 +15,7 @@ module ImportScripts::PhpBB3
u.user_type, u.user_inactive_reason, g.group_name, b.ban_start, b.ban_end, b.ban_reason, u.user_type, u.user_inactive_reason, g.group_name, b.ban_start, b.ban_end, b.ban_reason,
u.user_posts, f.pf_phpbb_website AS user_website, f.pf_phpbb_location AS user_from, u.user_posts, f.pf_phpbb_website AS user_website, f.pf_phpbb_location AS user_from,
u.user_birthday, u.user_avatar_type, u.user_avatar u.user_birthday, u.user_avatar_type, u.user_avatar
#{profile_fields_query(profile_fields)}
FROM #{@table_prefix}users u FROM #{@table_prefix}users u
LEFT OUTER JOIN #{@table_prefix}profile_fields_data f ON (u.user_id = f.user_id) LEFT OUTER JOIN #{@table_prefix}profile_fields_data f ON (u.user_id = f.user_id)
JOIN #{@table_prefix}groups g ON (g.group_id = u.group_id) JOIN #{@table_prefix}groups g ON (g.group_id = u.group_id)
@ -27,5 +28,18 @@ module ImportScripts::PhpBB3
LIMIT #{@batch_size} LIMIT #{@batch_size}
SQL SQL
end end
private
def profile_fields_query(profile_fields)
@profile_fields_query ||= begin
if profile_fields.present?
columns = profile_fields.map { |field| "pf_#{field[:phpbb_field_name]}" }
", #{columns.join(', ')}"
else
""
end
end
end
end end
end end

View File

@ -38,6 +38,7 @@ module ImportScripts::PhpBB3
import_posts import_posts
import_private_messages if @settings.import_private_messages import_private_messages if @settings.import_private_messages
import_bookmarks if @settings.import_bookmarks import_bookmarks if @settings.import_bookmarks
import_likes if @settings.import_likes
end end
def change_site_settings def change_site_settings
@ -71,7 +72,7 @@ module ImportScripts::PhpBB3
last_user_id = 0 last_user_id = 0
batches do |offset| batches do |offset|
rows, last_user_id = @database.fetch_users(last_user_id) rows, last_user_id = @database.fetch_users(last_user_id, @settings.custom_fields)
rows = rows.to_a.uniq { |row| row[:user_id] } rows = rows.to_a.uniq { |row| row[:user_id] }
break if rows.size < 1 break if rows.size < 1
@ -173,7 +174,7 @@ module ImportScripts::PhpBB3
importer = @importers.category_importer importer = @importers.category_importer
create_categories(rows) do |row| create_categories(rows) do |row|
next if @settings.category_mappings[row[:forum_id].to_s] == 'SKIP' next if @settings.category_mappings.dig(row[:forum_id].to_s, :skip)
importer.map_category(row) importer.map_category(row)
end end
@ -241,6 +242,25 @@ module ImportScripts::PhpBB3
end end
end end
def import_likes
puts '', 'importing likes'
total_count = @database.count_likes
last_post_id = last_user_id = 0
batches do |offset|
rows, last_post_id, last_user_id = @database.fetch_likes(last_post_id, last_user_id)
break if rows.size < 1
create_likes(rows, total: total_count, offset: offset) do |row|
{
post_id: @settings.prefix(row[:post_id]),
user_id: @settings.prefix(row[:user_id]),
created_at: Time.zone.at(row[:thanks_time])
}
end
end
end
def update_last_seen_at def update_last_seen_at
# no need for this since the importer sets last_seen_at for each user during the import # no need for this since the importer sets last_seen_at for each user during the import
end end

View File

@ -17,7 +17,7 @@ module ImportScripts::PhpBB3
return if @settings.category_mappings[row[:forum_id].to_s] return if @settings.category_mappings[row[:forum_id].to_s]
if row[:parent_id] && @settings.category_mappings[row[:parent_id].to_s] if row[:parent_id] && @settings.category_mappings[row[:parent_id].to_s]
puts "parent category (#{row[:parent_id]}) was mapped, but children was not (#{row[:forum_id]})" puts "parent category (#{row[:parent_id]}) was mapped, but child was not (#{row[:forum_id]})"
end end
{ {

View File

@ -22,7 +22,7 @@ module ImportScripts::PhpBB3
end end
def map_post(row) def map_post(row)
return if @settings.category_mappings[row[:forum_id].to_s] == 'SKIP' return if @settings.category_mappings.dig(row[:forum_id].to_s, :skip)
imported_user_id = @settings.prefix(row[:post_username].blank? ? row[:poster_id] : row[:post_username]) imported_user_id = @settings.prefix(row[:post_username].blank? ? row[:poster_id] : row[:post_username])
user_id = @lookup.user_id_from_imported_user_id(imported_user_id) || -1 user_id = @lookup.user_id_from_imported_user_id(imported_user_id) || -1
@ -56,8 +56,13 @@ module ImportScripts::PhpBB3
def map_first_post(row, mapped) def map_first_post(row, mapped)
poll_data = add_poll(row, mapped) if @settings.import_polls poll_data = add_poll(row, mapped) if @settings.import_polls
mapped[:category] = @lookup.category_id_from_imported_category_id(@settings.prefix(@settings.category_mappings[row[:forum_id].to_s])) || mapped[:category] = if category_mapping = @settings.category_mappings[row[:forum_id].to_s]
@lookup.category_id_from_imported_category_id(@settings.prefix(row[:forum_id])) category_mapping[:discourse_category_id] ||
@lookup.category_id_from_imported_category_id(@settings.prefix(category_mapping[:target_category_id]))
else
@lookup.category_id_from_imported_category_id(@settings.prefix(row[:forum_id]))
end
mapped[:title] = CGI.unescapeHTML(row[:topic_title]).strip[0...255] mapped[:title] = CGI.unescapeHTML(row[:topic_title]).strip[0...255]
mapped[:pinned_at] = mapped[:created_at] unless row[:topic_type] == Constants::POST_NORMAL mapped[:pinned_at] = mapped[:created_at] unless row[:topic_type] == Constants::POST_NORMAL
mapped[:pinned_globally] = row[:topic_type] == Constants::POST_GLOBAL mapped[:pinned_globally] = row[:topic_type] == Constants::POST_GLOBAL

View File

@ -42,6 +42,7 @@ module ImportScripts::PhpBB3
website: row[:user_website], website: row[:user_website],
location: row[:user_from], location: row[:user_from],
date_of_birth: parse_birthdate(row), date_of_birth: parse_birthdate(row),
custom_fields: custom_fields(row),
post_create_action: proc do |user| post_create_action: proc do |user|
suspend_user(user, row) suspend_user(user, row)
@avatar_importer.import_avatar(user, row) if row[:user_avatar_type].present? @avatar_importer.import_avatar(user, row) if row[:user_avatar_type].present?
@ -83,6 +84,45 @@ module ImportScripts::PhpBB3
birthdate && birthdate.year > 0 ? birthdate : nil birthdate && birthdate.year > 0 ? birthdate : nil
end end
def user_fields
@user_fields ||= begin
Hash[UserField.all.map { |field| [field.name, field] }]
end
end
def field_mappings
@field_mappings ||= begin
@settings.custom_fields.map do |field|
{
phpbb_field_name: "pf_#{field[:phpbb_field_name]}".to_sym,
discourse_user_field: user_fields[field[:discourse_field_name]]
}
end
end
end
def custom_fields(row)
return nil if @settings.custom_fields.blank?
custom_fields = {}
field_mappings.each do |field|
value = row[field[:phpbb_field_name]]
user_field = field[:discourse_user_field]
case user_field.field_type
when "confirm"
value = value == 1 ? true : nil
when "dropdown"
value = user_field.user_field_options.find { |option| option.value == value } ? value : nil
end
custom_fields["user_field_#{user_field.id}"] = value if value.present?
end
custom_fields
end
# Suspends the user if it is currently banned. # Suspends the user if it is currently banned.
def suspend_user(user, row, disable_email = false) def suspend_user(user, row, disable_email = false)
if row[:user_inactive_reason] == Constants::INACTIVE_MANUAL if row[:user_inactive_reason] == Constants::INACTIVE_MANUAL

View File

@ -36,20 +36,25 @@ import:
# Category mappings # Category mappings
# #
# For example, topics from phpBB category 1 and 2 will be imported # * "source_category_id" is the forum ID in phpBB3
# in the new "Foo Category" category, topics from phpBB category 3 # * "target_category_id" is either a forum ID from phpBB3 or a "forum_id"
# will be imported in subcategory "Bar category", topics from phpBB # from the "new_categories" setting (see above)
# category 4 will be merged into category 5 and category 6 will be # * "discourse_category_id" is a category ID from Discourse
# skipped. # * "skip" allows you to ignore a category during import
# #
# category_mappings: # Use "target_category_id" if you want to merge categories and use
# 1: foo # "discourse_category_id" if you want to import a forum into an existing
# 2: foo # category in Discourse.
# 3: bar
# 4: 5
# 6: SKIP
# #
category_mappings: {} # category_mappings:
# - source_category_id: 1
# target_category_id: foo
# - source_category_id: 2
# discourse_category_id: 42
# - source_category_id: 6
# skip: true
#
category_mappings: []
# Tag mappings # Tag mappings
# #
@ -122,6 +127,9 @@ import:
private_messages: true private_messages: true
polls: true polls: true
# Import likes from the phpBB's "Thanks for posts" extension
likes: false
# When true: each imported user will have the original username from phpBB as its name # When true: each imported user will have the original username from phpBB as its name
# When false: the name of each imported user will be blank unless the username was changed during import # When false: the name of each imported user will be blank unless the username was changed during import
username_as_name: false username_as_name: false
@ -134,3 +142,12 @@ import:
# here are two example mappings... # here are two example mappings...
smiley: [':D', ':-D', ':grin:'] smiley: [':D', ':-D', ':grin:']
heart: ':love:' heart: ':love:'
# Map custom profile fields from phpBB to custom user fields in Discourse (works for phpBB 3.1+)
#
# custom_fields:
# - phpbb_field_name: "company_name"
# discourse_field_name: "Company"
# - phpbb_field_name: "facebook"
# discourse_field_name: "Facebook"
custom_fields: []

View File

@ -24,6 +24,7 @@ module ImportScripts::PhpBB3
attr_reader :import_polls attr_reader :import_polls
attr_reader :import_bookmarks attr_reader :import_bookmarks
attr_reader :import_passwords attr_reader :import_passwords
attr_reader :import_likes
attr_reader :import_uploaded_avatars attr_reader :import_uploaded_avatars
attr_reader :import_remote_avatars attr_reader :import_remote_avatars
@ -38,6 +39,7 @@ module ImportScripts::PhpBB3
attr_reader :username_as_name attr_reader :username_as_name
attr_reader :emojis attr_reader :emojis
attr_reader :custom_fields
attr_reader :database attr_reader :database
@ -47,7 +49,7 @@ module ImportScripts::PhpBB3
@site_name = import_settings['site_name'] @site_name = import_settings['site_name']
@new_categories = import_settings['new_categories'] @new_categories = import_settings['new_categories']
@category_mappings = import_settings['category_mappings'] @category_mappings = import_settings.fetch('category_mappings', []).to_h { |m| [m[:source_category_id].to_s, m] }
@tag_mappings = import_settings['tag_mappings'] @tag_mappings = import_settings['tag_mappings']
@rank_mapping = import_settings['rank_mapping'] @rank_mapping = import_settings['rank_mapping']
@ -57,6 +59,7 @@ module ImportScripts::PhpBB3
@import_polls = import_settings['polls'] @import_polls = import_settings['polls']
@import_bookmarks = import_settings['bookmarks'] @import_bookmarks = import_settings['bookmarks']
@import_passwords = import_settings['passwords'] @import_passwords = import_settings['passwords']
@import_likes = import_settings['likes']
avatar_settings = import_settings['avatars'] avatar_settings = import_settings['avatars']
@import_uploaded_avatars = avatar_settings['uploaded'] @import_uploaded_avatars = avatar_settings['uploaded']
@ -72,6 +75,7 @@ module ImportScripts::PhpBB3
@username_as_name = import_settings['username_as_name'] @username_as_name = import_settings['username_as_name']
@emojis = import_settings.fetch('emojis', []) @emojis = import_settings.fetch('emojis', [])
@custom_fields = import_settings.fetch('custom_fields', [])
@database = DatabaseSettings.new(yaml['database']) @database = DatabaseSettings.new(yaml['database'])
end end