已完成从 vB3 到 Discourse 的迁移,包括旧版 vB3 的“感谢”功能

Just completed, for the most part (still in test bed, but the “hard parts” are basically done and the migration works fine), migrating a 15 year old vb3 forum, which was originally (believe-it-or-not) a vb2 forum from 2000.

This forum has been around for around 20 years (with unimaginable custom PHP code, tables, plugins, etc), and Discourse is the only forum software, in my view, “worthy” of a migration project, which I started around a week ago, going from feasibility to completion (basically).

First of all, I want to thank the entire Discourse team for such a remarkable “work-of-art” modern forum. Discourse is truly remarkable, in my view (and I am sure many others).

Second, I would like to thank the team who coded the original vbulletin.rb migration script. This script was about 95% OK. and I modified that script to get it to work properly for vb3. For example, vb3 does not have a filedata table like vb4 (related to attachments); so I modified vbulletin.rb to get it to work. Also, there were issues importing child vb3 forums as categories, but I modified the code to import all forums as top level categories and then wrote some postgres psql to create new parent-child categories relationships (or manually configured). There were other “gotchas” (a story for another day) but those goblins made it fun and challenging, but not insurmountable.

Third, I would like to thank Sam and his code lithium.rb where I used his import_likes routine as the basis for my vb3 thank you (from a legacy plugin, not part of vB OOTB) to discourse likes. I made minor changes to the import_likes routine and after a few hours of debugging, got it to work.

As you can see from this 2018 post, migrated today:

I thought it was important for users that their vb thanks migrated to discourse likes and so here is the code I used to migrate the likes for other lost vb3 forum souls:

First of all I created a vb3 mysql table called user_actions and wrote a PHP script to fill it from the legacy vb post_thanks table, like this (not polished, but working):

ubuntu:/var/www/includes/cron# cat thanks_to_discourse.php
 <?php

/**************************************************
+-----------+------------+-----------+-----------+
| thanker   | unixtime   | id        | thanked   |
+-----------+------------+-----------+-----------+
|         1 | 1584149592 | 303045211 | 302093876 |
| 302153369 | 1584136706 | 303045214 | 302116191 |
| 302108573 | 1584128526 | 303045211 | 302093876 |
| 302153369 | 1584126659 | 303042175 | 302116191 |
| 302153369 | 1584126400 | 303045174 | 302116191 |
| 302153369 | 1584117711 | 303045184 |         1 |
|     37898 | 1584108187 | 303045175 |         1 |
| 302181242 | 1584106664 | 303045201 | 302122047 |
| 302181242 | 1584104642 | 303045074 | 302052697 |
| 302025710 | 1584103722 | 303045184 |         1 |
+-----------+------------+-----------+-----------+
 **************************************************/

$query = 'SELECT p.threadid AS threadid,t.userid AS thanker,t.date AS unixtime,t.postid AS postid,p.userid AS thanked from post_thanks as t LEFT JOIN post p ON p.postid = t.postid';

$allthanks = $vbulletin->db->query_read($query);

/****************************************
mysql> describe user_actions;
+-----------------+------------------+------+-----+---------+----------------+
| Field           | Type             | Null | Key | Default | Extra          |
+-----------------+------------------+------+-----+---------+----------------+
| id              | int(11)          | NO   | PRI | NULL    | auto_increment |
| action_type     | int(11) unsigned | NO   |     | NULL    |                |
| user_id         | int(11) unsigned | NO   |     | NULL    |                |
| target_topic_id | int(11) unsigned | YES  |     | NULL    |                |
| target_post_id  | varchar(16)      | YES  |     | NULL    |                |
| target_user_id  | int(11) unsigned | YES  |     | NULL    |                |
| acting_user_id  | int(11) unsigned | YES  |     | NULL    |                |
| created_at      | timestamp        | YES  |     | NULL    |                |
| updated_at      | timestamp        | YES  |     | NULL    |                |
+-----------------+------------------+------+-----+---------+----------------+
9 rows in set (0.00 sec)
INSERT INTO table_name (column1, column2, column3, ...)
VALUES (value1, value2, value3, ...);
 *************************************/

$action_code = 2; // discourse like action == 2
$target_user_id = "";
while ($action = $vbulletin->db->fetch_array($allthanks)) {

    #code to deal with how discourse identifies the first post in a topic in the post_custom_fields table
    $query = 'SELECT firstpostid FROM thread WHERE threadid =' . $action['threadid'] . ' LIMIT 1';
    $threadinfo = $vbulletin->db->query_first($query);
    if ($threadinfo['firstpostid'] == $action['postid']) {
        $action['postid'] = 'thread-' . $action['threadid'];
    }

    $update = 'INSERT INTO user_actions (action_type, user_id, target_topic_id, target_post_id, target_user_id,acting_user_id, created_at, updated_at)' .
        ' VALUES (' . $action_code . ',' .
        '"' . $action['thanked'] . '",' .
        '"' . $action['threadid'] . '",' .
        '"' . $action['postid'] . '",' .
        '"' . $target_user_id . '",' .
        '"' . $action['thanker'] . '",' .
        'FROM_UNIXTIME(' . $action['unixtime'] . '),' .
        'FROM_UNIXTIME(' . $action['unixtime'] . '))';
    $doit = $vbulletin->db->query_write($update);
}

So, this mysql user_actions table is included with the migration dump.

Here is the modified import_likes routine I used to complete the migration on the discourse side:

def import_likes
    puts "\nimporting likes..."

    # created mysql user_actions table in vb3 using PHP script and included that table with migration dump
    sql = "select acting_user_id as user_id, target_post_id as post_id, created_at from user_actions"
    results = mysql_query(sql)
    puts "length() method form : #{results.count}\n\n"

    puts "skip loading unique id map"
    existing_map = {}
    PostCustomField.where(name: 'import_id').pluck(:post_id, :value).each do |post_id, import_id|
      existing_map[import_id] = post_id
      #puts "postcustomfield existing_map post_id: #{post_id} import_id #{import_id}\n"
    end

    puts "loading data into temp table"

    #manually created the temp like_data table so I could check the table after session ends
    #DB.exec("create temp table like_data(user_id integer, post_id integer, created_at timestamp without time zone)")
    
    puts "like_data temp table created"
    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"]

        puts "insert like table user_id: #{result["user_id"]} post_id #{result["post_id"]}\n"

        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
end

I hope this post helps others who might migrate from vB3 to discourse, since many people used the legacy vb3 “thank you” plugin for many years.

Also, I spent a lot of time in the discourse database and inside a lot of discourse ruby scripts, so this migration was tricky (not for the faint of heart to be sure), but certainly doable.

Actually, I enjoyed doing it. This was the first time in ruby for me; so it was fun to start to learn ruby and as a long time mysql person, the postgres part was mostly routine.

We will be going live soon… Thank You Discourse Team!!!

做得好,感谢分享!

感谢 @codinghorror

刚发现一个小问题,我会着手解决。import_likes 例程没有转移“话题发起者”首帖的点赞(只转移了回复的点赞)。

我得卷起袖子好好研究一下,然后发布更新后的 import_likes 例程。

看来我发帖有点 太早了,在宣布胜利之前,还有一个小妖精需要消灭。

更新:

我在 vB3 的 PHP 脚本中添加了以下代码,以补偿 vB3 和 Discourse 在处理话题/线程首帖时的差异:

    <?php
    #vB3 代码,用于处理 Discourse 如何在 post_custom_fields 表中识别话题中的首帖
    $query = 'SELECT firstpostid FROM thread WHERE threadid =' . $action['threadid'] . ' LIMIT 1';
    $threadinfo = $vbulletin->db->query_first($query);
    if ($threadinfo['firstpostid'] == $action['postid']) {
        $action['postid'] = 'thread-' . $action['threadid'];
    }

现在,它按预期运行,vB3 中的“感谢”已成功迁移到 Discourse,包括话题中的首帖;这是通过调整从 vB3 到 Discourse 表 post_custom_fields 的迁移表来实现的。

目前正在进行最终测试,但根据初步测试,看起来应该可以正常工作。

此外,我还在原帖中更新了代码,添加了上述 PHP 代码片段。

我了解到你们没有迁移密码,因为哈希值不同。你可以看看我的密码迁移插件(这里)。它很可能已经支持 VB3 哈希值,如果不支持,我也很乐意添加该功能。

谢谢 Michael,

我们不会迁移密码,而是希望用户自行更改密码(并改为更强、更长的密码)。

无论如何,非常感谢你的帮助提议!我们非常感激。

我们还有一个小问题,也许你能帮我们解决。

不知何故,用户个人资料中的“点赞”总数未能正确更新,尽管其他一切看起来都正常。我尝试过用 rake 任务来解决这个问题,但我的操作并未得到我需要的结果。

你知道有什么好方法可以手动更新这些个人资料的“点赞”计数器吗?

你用了哪个 rake 任务?出了什么问题?是所有数据都偏差很大,还是仅部分用户的数据有问题,还是……?

你好。

首先,我尝试了当时看来唯一合理的 rake 任务(因为问题与 user_actionspost_actions 相关);但并没有取得任何进展(实际上,现在回想起来,我应该先阅读那个 rake 任务,看看它具体做了什么,哈哈):

rake user_actions:rebuild

坦白说,到目前为止,我还没有确定 import_likes 迁移脚本中究竟哪里出了问题。如果我能确切知道问题出在哪里,我本可以修复它(我常花几个小时甚至几天去找出问题,而一旦找到,修复只需一分钟……这就是 编程的精髓,哈哈)。我已经从快照中恢复了好几次,并用几种不同的方式重新运行了 import_likes 流程(添加了额外的 puts 语句等),同时检查了相关表。结果仍不明确。再加上我对 rubydiscourse 整体还不够熟练;我尚未精通后端开发。(不过,我正在逐渐掌握 begin... rescue ... end 结构……哈哈,这已成为我作为 ruby 新手 的新调试工具之一)。

我所观察到的是,临时的 postgres like_data 表(在 discourse 端)没有问题,包含所有数据且格式正确,其条目与 mysql 迁移表以及 vB 站点的数据一致。

问题似乎出现在创建 user_actionspost_actions 的过程中,因为我的测试显示数据未能正确传输到这些关键表中。

请注意,核心的 vB3 迁移没有问题。此问题与一个旧的 vB3“感谢”插件有关,我正在将其转换为 discourse 的点赞功能。我接下来的计划是,如果今天晚些时候有时间,坐下来仔细查看数据从正常的 like_data 表传输到 user_actionspost_actions 时,具体在哪个环节出错了。

基于昨天的测试,我的初步结论是:ruby rake 任务可能无济于事,因为问题出在更新 user_actionspost_actions 表的过程中;因此,我需要确切找出问题出在哪里以及原因。问题可能很简单,比如数据库类型不匹配,或是代码中的其他小故障。

我们的 discourse 站点尚未上线,因此这并非 紧急情况。和许多其他地方的人一样,处理移民或银行等事务已变得 相当麻烦,今天我还得进城处理一些与 discourse 无关的任务。我其实更希望能专注于探索 discourse 并寻找 代码小故障

再次运行了迁移脚本(未对脚本进行任何修改),其中包括将自定义的 vb3 thanks 转换为 Discourse likesimport_likes)的脚本,现在看起来没问题了。

接下来将尝试同步最新的 vB 数据库转储(大 “D”),看看会发生什么!

对 Discourse 非常满意……再次感谢这个堪称“艺术品”的现代论坛。仍在测试中,但很快就会正式上线。

5.6k 个赞……这真是满满的爱 :heart:

在将所有 vb 感谢 迁移到 discourse 点赞 之后,以下是来自 2000 年(即二十年前)的一个迁移后主题帖示例,展示了主题中迁移后的点赞:

……而且我开始期待学习如何编写有趣的插件了…… :slight_smile:

我对 Discourse 及其团队非常满意……尤其是 Ruby 中的 begin .... rescue ...puts "look at me".... end……简直是救命稻草!

事实确实如此……只要给 老狗 扔点骨头,它们也能学会新把戏 :slight_smile:

感谢 Discourse 团队!

20 年前,我们在 vB2 上为 Unix 和 Linux 用户创建了一个论坛;大约 15 年前,我们“升级”到了 vB3。今天对全球 Unix 和 Linux 用户以及我们自身而言都是一个重要的转折点,因为我们迁移到 Discourse 的工作现已正式上线。

非常感谢你们开发了这款软件,并将其作为开源项目向所有人开放。你们的慷慨令人感激不尽。在我看来(我相信许多人也持相同观点),Discourse 无疑是 2020 年全球最好的论坛软件。

对于那些仍在使用旧版 vB3 并希望迁移到 Discourse 的用户,我要说明的是:这次迁移并没有我展示得那么“轻松”。除非你精通编程,并且能够熟练地在命令行中直接与数据库进行集成,否则我建议你考虑聘请 meta.discourse.org 上一些才华横溢且活跃的专业人士提供服务。

再次感谢 Discourse 团队!

附注:我将在新网站的这篇帖子中补充一些从实践中总结的经验教训: