UNPKG

nodebb-plugin-import

Version:
643 lines (455 loc) 26.5 kB
## Examples to fork: * https://github.com/akhoury/nodebb-plugin-import-vbulletin * https://github.com/akhoury/nodebb-plugin-import-discourse * https://github.com/akhoury/nodebb-plugin-import-ubb * https://github.com/a5mith/nodebb-plugin-import-smf * https://github.com/psychobunny/nodebb-plugin-import-phpbb * https://github.com/akhoury/nodebb-plugin-import-ipboard * https://github.com/akhoury/nodebb-plugin-import-punbb ## Terminology This section is up here because it's very important for you to read, so let's make a few things clear before we go on. * 'NodeBB' == 'NBB' == 'nbb' == 'Nbb' * when you see the term __OLD__ it refers to your source forum or bulletin-board * when you see the term __NEW__ it refers to NodeBB * __ALL__ of the __OLD__ __variables__, must start with an __underscore__ character: `_` * `_cid` --> old category id, some forum software uses different terms for categories, such as __forums__ or __boards__ * `_uid` --> old user id * `_tid` --> old topic id, some forum software uses different terms for topics, such as __threads__ * `_pid` --> old post id * `_roomId` --> old chatroom id * `_mid` --> old message id * `_gid` --> old group id * `_vid` --> old vote id * `_bid` --> old bookmark id * `cid` --> new category id * `uid` --> new user id * `tid` --> new topic id * `pid` --> new post id * `roomId` --> new chatroom id * `mid` --> new message id * `gid` --> new group id * `vid` --> new vote id * `bid` --> new bookmark id ## Required You need a node module that has the following interface. ## During development Don't forget to check the "Skip the module install" checkbox in the "Select an Exporter" section, so the -import plugin doesn't delete your changes. ### YourModule.setup(config, callback) [REQUIRED FUNCTION] * `config`: a JS object that will be passed to `setup()` and it contains the following: ```javascript { dbhost: '127.0.0.1', // a string, db host entered by the user on the UI dbuser: 'admin', // a string, db username entered by the user on the UI dbpass: '123456', // a string, db password entered by the user on the UI dbport: 3306, // a number, db port entered by the user on the UI dbname: 'my_schema', // db schema, or name, entered by the user on the UI tablePrefix: 'bb_', // db table prefix, entered by the user on the UI, ignore it if not applicable custom: {} // a custom hash for your custom stuff, // these values are not defaults, these are just examples } ``` * `callback(err, config)`: a function that send 2 arguments ``` - err: if truthy the export process will throw the error and stop - config: just return the configs that were setup on the exporter, in case they were modified ``` ### YourModule.getUsers(callback) [deprecated] ### YourModule.getPaginatedUsers(start, limit, callback) [REQUIRED FUNCTION] * `start` of the query row * `limit` of the query results * `callback` Query the records, filter them at will, then call the `callback(err, map)` with the following arguments: ``` - err: if truthy the export process will throw the error and stop - map: a hashmap of all the users ready to import ``` In the `map`, the `keys` are the users `_uid` (or the old user id). Each record should look like this: ```javascript { // notice how all the old variables start with an _ // if any of the required variables fails, the record will be skipped "_uid": 45, // REQUIRED "_email": "u45@example.com", // REQUIRED "_username": "user45", // REQUIRED "_joindate": 1386475817370, // OPTIONAL, [UNIT: MILLISECONDS], defaults to current, but what's the point of migrating if you don't preserve dates "_alternativeUsername": "u45alt", // OPTIONAL, defaults to '', some forums provide UserDisplayName, we could leverage that if the _username validation fails // if you would like to generate random passwords, you will need to set the config.passwordGen.enabled = true, note that this will impact performance pretty hard // the new passwords with the usernames, emails and some more stuff will be spit out in the logs // look for the [user-csv] OR [user-json] tags to grep for a list of them // save dem logs "_password": '', // OPTIONAL, if you have them, or you want to generate them on your own, great, if not, all passwords will be blank "_signature": "u45 signature", // OPTIONAL, defaults to '', over 150 chars will be truncated with an '...' at the end "_picture": "http://images.com/derp.png", // OPTIONAL, defaults to ''. Note that, if there is an '_piçture' on the 'normalized' object, the 'imported' objected will be augmented with a key imported.keptPicture = true, so you can iterate later and check if the images 200 or 404s "_pictureBlob": "...BINARY BLOB...", // OPTIONAL, defaults to null "_pictureFilename": "123.png", // OPTIONAL, only applicable if using _pictureBlob, defaults to '' "_path": "/myoldforum/user/123", // OPTIONAL, the old path to reach this user's page, defaults to '' "_slug": "old-user-slug", // OPTIONAL // obviously this one depends on implementing the optional getPaginatedGroups function "_groups": [123, 456, 789], // OPTIONAL, an array of old group ids that this user belongs to, "_website": "u45.com", // OPTIONAL, defaults to '' "_fullname": "this is dawg", // OPTIONAL, defaults to '' "_banned": 0, // OPTIONAL, defaults to 0 // read cids and tids by that user, it's more efficient to use _readCids if you know that a user has read all the topics in a category. "_readCids": [1, 2, 4, 5, 6, 7], // OPTIONAL, defaults to [] // untested with very large sets. So. "_readTids": [1, 2, 4, 5, 6, 7], // OPTIONAL, defaults to [] // following other _Uids, untested with very large sets. So. "_followingUids": [1, 2, 4, 5, 6, 7], // OPTIONAL, defaults to [] // friend other _Uids, untested with very large sets. So. // if you have https://github.com/sanbornmedia/nodebb-plugin-friends installed or want to use it "_friendsUids": [1, 2, 4, 5, 6, 7], // OPTIONAL, defaults to [] "_location": "u45 city", // OPTIONAL, defaults to '' // (there is a config for multiplying these with a number for moAr karma) // Also, if you're implementing getPaginatedVotes, every vote will also impact the user's reputation "_reputation": 123, // OPTIONAL, defaults to 0, "_profileviews": 1, // OPTIONAL, defaults to 0 "_birthday": "01/01/1977", // OPTIONAL, [FORMAT: mm/dd/yyyy], defaults to '' "_showemail": 0, // OPTIONAL, defaults to 0 "_lastposttime": 1386475817370, // OPTIONAL, [UNIT: MILLISECONDS], defaults to current "_level": "administrator" // OPTIONAL, [OPTIONS: 'administrator' or 'moderator'], defaults to '', also note that a moderator will become a NodeBB Moderator on ALL categories at the moment. "_lastonline": 1386475827370 // OPTIONAL, [UNIT: MILLISECONDS], defaults to undefined "_fields": { // OPTIONAL, extra field to be merge with on the main nodebb object hash defaults to undefined googleId: "123465" } } ``` ### YourModule.getCategories(callback) [deprecated] ### YourModule.getPaginatedCategories(start, limit, callback) [REQUIRED FUNCTION] * `start` of the query row * `limit` of the query results * `callback` Query the records, filter them at will, then call the `callback(err, map)` with the following arguments: Note: Categories are sometimes known as __forums__ in some forum software ``` - err: if truthy the export process will throw the error and stop - map: a hashmap of all the categories ready to import ``` In the `map`, the `keys` are the categories `_cid` (or the old category id). Each record should look like this: ```javascript { // notice how all the old variables start with an _ // if any of the required variables fails, the category and all of its topics/posts will be skipped "_cid": 2, // REQUIRED "_name": "Category 1", // REQUIRED "_description": "it's about category 1", // OPTIONAL "_order": 1 // OPTIONAL, defauls to its index + 1 "_path": "/myoldforum/category/123", // OPTIONAL, the old path to reach this category's page, defaults to '' "_slug": "old-category-slug", // OPTIONAL defaults to '' "_parentCid": 1, // OPTIONAL, parent category _cid defaults to null "_skip": 0, // OPTIONAL, if you want to intentionally skip that record "_color": "#FFFFFF", // OPTIONAL, text color, defaults to random "_bgColor": "#123ABC", // OPTIONAL, background color, defaults to random "_icon": "comment", // OPTIONAL, Font Awesome icon, defaults to random "_fields": { // OPTIONAL, extra field to be merge with on the main nodebb object hash defaults to undefined googleId: "123465" } } ``` ### YourModule.getTopics(callback) [deprecated] ### YourModule.getPaginatedTopics(start, limit, callback) [REQUIRED FUNCTION] * `start` of the query row * `limit` of the query results * `callback` Query the records, filter them at will, then call the `callback(err, map)` with the following arguments: Note: Topics are sometimes known as __threads__ in some forum software ``` - err: if truthy the export process will throw the error and stop - map: a hashmap of all the topics ready to import ``` In the `map`, the `keys` are the topics `_tid` (or the old topic id). Each record should look like this: ```javascript { // notice how all the old variables start with an _ // if any of the required variables fails, the topic and all of its posts will be skipped "_tid": 1, // REQUIRED, THE OLD TOPIC ID "_uid": 1, // OPTIONAL, THE OLD USER ID, Nodebb will create the topics for user 'Guest' if not provided "_uemail": "u45@example.com", // OPTIONAL, The OLD USER EMAIL. If the user is not imported, the plugin will get the user by his _uemail "_guest": "Some dude" // OPTIONAL, if you dont have _uid, you can pass a guest name to be used in future features, defaults to null "_cid": 1, // REQUIRED, THE OLD CATEGORY ID "_ip": "123.456.789.012", // OPTIONAL, not currently used in NodeBB core, but it might be in the future, defaults to null "_title": "this is topic 1 Title", // OPTIONAL, defaults to "Untitled :id" "_content": "This is the first content in this topic 1", // REQUIRED "_thumb": "http://foo.bar/picture.png", // OPTIONAL, a thumbnail for the topic if you have one, note that the importer will NOT validate the URL "_timestamp": 1386475817370, // OPTIONAL, [UNIT: Milliseconds], defaults to current, but what's the point of migrating if you dont preserve dates "_viewcount": 10, // OPTIONAL, defaults to 0 "_locked": 0, // OPTIONAL, defaults to 0, during migration, ALL topics will be unlocked then locked back up at the end "_tags": ["tag1", "tag2", "tag3"], // OPTIONAL, an array of tags, or a comma separated string would work too, defaults to null "_attachments": ["http://example.com/myfile.zip"], // OPTIONAL, an array of urls, to append to the content for download. // OR you can pass a filename with it "_attachments": [{url: "http://example.com/myfile.zip", filename: "www.zip"}], // OPTIONAL, an array of objects with urls and filenames, to append to the content for download. // OPTIONAL, an array of objects, each object mush have the binary BLOB, // either a filename or extension, then each file will be written to disk, // if no filename is provided, the extension will be used and a filename will be generated as attachment_t_{_tid}_{index}{extension} // and its url would be appended to the _content for download "_attachmentsBlobs": [ {blob: <BINARY>, filename: "myfile.zip"}, {blob: <BINARY>, extension: ".zip"} ], "_deleted": 0, // OPTIONAL, defaults to 0 "_pinned": 1 // OPTIONAL, defaults to 0 "_edited": 1386475817370 // OPTIONAL, [UNIT: Milliseconds] see post._edited defaults to null "_reputation": 1234, // OPTIONAL, defaults to 0, must be >= 0, not to be confused with _votes (see getPaginatedVotes for votes) "_path": "/myoldforum/topic/123", // OPTIONAL, the old path to reach this topic's page, defaults to '' "_slug": "old-topic-slug", // OPTIONAL, defaults to '' "_fields": { // OPTIONAL, extra field to be merge with on the main nodebb object hash defaults to undefined googleId: "123465" } } ``` ### YourModule.getPosts(callback) [deprecated] ### YourModule.getPaginatedPosts(start, limit, callback) [REQUIRED FUNCTION] * `start` of the query row * `limit` of the query results * `callback` Query the records, filter them at will, then call the `callback(err, map)` with the following arguments: ``` - err: if truthy the export process will throw the error and stop - map: a hashmap of all the posts ready to import ``` In the `map`, the `keys` are the posts `_pid` (or the old post id). Each record should look like this: ```javascript { // notice how all the old variables start with an _ // if any of the required variables fails, the post will be skipped "_pid": 65487, // REQUIRED, OLD POST ID "_tid": 1234, // REQUIRED, OLD TOPIC ID "_content": "Post content ba dum tss", // REQUIRED "_uid": 202, // OPTIONAL, OLD USER ID, if not provided NodeBB will create under the "Guest" username, unless _guest is passed. "_uemail": "u45@example.com", // OPTIONAL, The OLD USER EMAIL. If the user is not imported, the plugin will get the user by his _uemail "_toPid": 65485, // OPTIONAL, OLD REPLIED-TO POST ID, "_timestamp": 1386475829970 // OPTIONAL, [UNIT: Milliseconds], defaults to current, but what's the point of migrating if you dont preserve dates. "_guest": "Some dude" // OPTIONAL, if you don't have _uid, you can pass a guest name to be used in future features, defaults to null "_ip": "123.456.789.012", // OPTIONAL, not currently used in NodeBB core, but it might be in the future, defaults to null "_edited": 1386475829970, // OPTIONAL, [UNIT: Milliseconds], if and when the post was edited, defaults to null "_reputation": 0, // OPTIONAL, defaults to 0, must be >= 0, not to be confused with _votes (see getPaginatedVotes for votes) "_attachments": ["http://example.com/myfile.zip"], // OPTIONAL, an array of urls, to append to the content for download. // OPTIONAL, an array of objects, each object mush have the binary BLOB, // either a filename or extension, then each file will be written to disk, // if no filename is provided, the extension will be used and a filename will be generated as attachment_p_{_pid}_{index}{extension} // and its url would be appended to the _content for download "_attachmentsBlobs": [ {blob: <BINARY>, filename: "myfile.zip"}, {blob: <BINARY>, extension: ".zip"} ], "_path": "/myoldforum/topic/123#post56789", // OPTIONAL, the old path to reach this post's page and maybe deep link, defaults to '' "_slug": "old-post-slug", // OPTIONAL, defaults to '' "_fields": { // OPTIONAL, extra field to be merge with on the main nodebb object hash defaults to undefined googleId: "123465" } } ``` ### YourModule.getRooms(callback) [deprecated] ### YourModule.getPaginatedRooms(start, limit, callback) [OPTIONAL FUNCTION] * `start` of the query row * `limit` of the query results * `callback` Query the records, filter them at will, then call the `callback(err, map)` with the following arguments: ``` - err: if truthy the export process will throw the error and stop - map: a hashmap of all the chatrooms ready to import ``` In the `map`, the `keys` are the rooms `_rid` (or the old chatroom id). Each record should look like this: ```javascript { // notice how all the old variables start with an _ // if any of the required variables fails, the record will be skipped "_roomId": 45, // REQUIRED "_roomName": "import planning", // OPTIONAL, name of room "_uid": 10, // REQUIRED, owner of the room "_uids": [20, 25, 32], // REQUIRED, other users in the room "_timestamp": 1386475817370 // OPTIONAL, [UNIT: MILLISECONDS], defaults to current } ``` ### YourModule.getMessages(callback) [deprecated] ### YourModule.getPaginatedMessages(start, limit, callback) [OPTIONAL FUNCTION] * `start` of the query row * `limit` of the query results * `callback` Query the records, filter them at will, then call the `callback(err, map)` with the following arguments: ``` - err: if truthy the export process will throw the error and stop - map: a hashmap of all the messages ready to import ``` In the `map`, the `keys` are the messages `_mid` (or the old message id). Each record should look like this: ```javascript { // notice how all the old variables start with an _ // if any of the required variables fails, the record will be skipped "_mid": 45, // REQUIRED "_fromuid": 10, // REQUIRED "_roomId": 20, // PREFERRED, the _roomId if you are using get(Pagianted)Rooms "_touid": 20, // DEPRECATED, if you're not using getPaginatedRooms, you can just pass the _touid value here. // note: I know the camelcasing is weird here, but can't break backward compatible exporters yet. "_content": "Hello there!", // REQUIRED "_timestamp": 1386475817370 // OPTIONAL, [UNIT: MILLISECONDS], defaults to current } ``` ### YourModule.getGroups(callback) [deprecated] ### YourModule.getPaginatedGroups(start, limit, callback) [OPTIONAL FUNCTION] * `start` of the query row * `limit` of the query results * `callback` Query the records, filter them at will, then call the `callback(err, map)` with the following arguments: ``` - err: if truthy the export process will throw the error and stop - map: a hashmap of all the groups ready to import ``` In the `map`, the `keys` are the groups `_gid` (or the old group id). Each record should look like this: ```javascript { // notice how all the old variables start with an _ // if any of the required variables fails, the record will be skipped "_gid": 45, // REQUIRED, old group id "_name": "My group name", // REQUIRED "_ownerUid": 123, // REQUIRED, owner old user id, aka user._uid, "_description": "My group description", // OPTIONAL "_userTitle": "My group badge", // OPTIONAL, will show instead of the _name "_userTitleEnabled": 1, // OPTIONAL, to show the userTitle at all "_disableJoinRequests": 0, // OPTIONAL "_system": 0, // OPTIONAL, if system group "_private": 0, // OPTIONAL, if private group "_hidden": 0, // OPTIONAL, if hidden group "_timestamp": 1386475817370, // OPTIONAL, [UNIT: MILLISECONDS], defaults to current "_fields": { // OPTIONAL, extra field to be merge with on the main nodebb object hash defaults to undefined googleId: "123465" } } ``` ### YourModule.getVotes(callback) [deprecated] ### YourModule.getPaginatedVotes(start, limit, callback) [OPTIONAL FUNCTION] #### NOTE: Every vote WILL impact the post-user-owner reputation * `start` of the query row * `limit` of the query results * `callback` Query the records, filter them at will, then call the `callback(err, map)` with the following arguments: ``` - err: if truthy the export process will throw the error and stop - map: a hashmap of all the groups ready to import ``` In the `map`, the `keys` are the votes `_vid` (or the old vote id). Each record should look like this: ```javascript { // notice how all the old variables start with an _ // if any of the required variables fails, the record will be skipped "_vid": 987, // REQUIRED, old vote id "_uid": 789, // REQUIRED, old user id which did the vote "_uemail": "u45@example.com", // OPTIONAL, The OLD USER EMAIL. If the user is not imported, the plugin will get the user by his _uemail // 1 of these 2 ids is REQUIRED /* you shouldn't need to include `vote._tid` AND `vote._pid`, either or, use `_tid` when the Like occured on the "main-post" of that topic's tid (the importer will find the new `topic.mainPid` using the old `_tid`), and use `_pid` when it's on any other post within a topic. */ "_tid": 123, // MAYBE-OPTIONAL, old topic id which is the vote occured on, "_pid": 456, // MAYBE-OPTIONAL, old post id which is the vote occured on "_action": 1 // REQUIRED 1 or -1, 1 means UP, -1 means down } ``` ### YourModule.getBookmarks(callback) [deprecated] ### YourModule.getPaginatedBookmarks(start, limit, callback) [OPTIONAL FUNCTION] * `start` of the query row * `limit` of the query results * `callback` Query the records, filter them at will, then call the `callback(err, map)` with the following arguments: ``` - err: if truthy the export process will throw the error and stop - map: a hashmap of all the groups ready to import ``` In the `map`, the `keys` are the groups `_bid` (or the old bookmark Id). Each record should look like this: ```javascript { // notice how all the old variables start with an _ // if any of the required variables fails, the record will be skipped "_bid": 987, // REQUIRED, old bookmark id "_tid": 123, // REQUIRED, old topic id "_uid": 789, // REQUIRED, old user id "_index": 2 // REQUIRED, the index of the bookmarked-post, i.e. 5 if the 6'sh post of that topic was the bookmarked post } ``` ### YourModule.teardown(callback) [REQUIRED FUNCTION] If you need to do something before the export is done, like closing a connection or something, then call the `callback` ## Optionals ### `YourModule.each${Model}ImmediateProcess` These functions will run right after the whole import process is done, just in case you need to do something custom * `YourModule.eachUserImmediateProcess(user, options, callback)` * `YourModule.eachMessageImmediateProcess(message, options, callback)` * `YourModule.eachGroupImmediateProcess(group, options, callback)` * `YourModule.eachCategoryImmediateProcess(category, options, callback)` * `YourModule.eachTopicImmediateProcess(topic, options, callback)` * `YourModule.eachPostImmediateProcess(post, options, callback)` * `YourModule.eachBookmarkImmediateProcess(bookmark, options, callback)` * `YourModule.eachVoteImmediateProcess(vote, options, callback)` Example ```javascript YourModule.eachUserImmediateProcess = function (user, options, callback) { options.$refs.db.setObjectField('foo:uid', user.uid, user.uid, callback); }; ``` `options` has a $refs property which has a references to helpful modules which you can use in your exporter ```javascript options = { $refs: { utils, nconf, Meta, Categories, Groups, User, Messaging, Topics, Posts, File, db, privileges, Rooms, Votes, Bookmarks } }; ``` #### Logger functions * `YourModule.log([args])` * `YourModule.warn([args])` * `YourModule.error([args])` In these 3 functions, you can basically do whatever you want, such as printing something on the console based on its level, or logging to a file. However, the arguments that each call passes in will be picked up, and emitted in corresponding events, and shown to the user. The event emitted are: * `exporter.log` * `exporter.warn` * `exporter.error` You do not have to do anything extra to emit the events, just implement these functions at will and use them appropriately (see [this](https://github.com/akhoury/nodebb-plugin-import-ubb/blob/master/index.js#L366) for example). #### a testrun function * `YouModule.testrun(config, callback)` Just a function for you to be able to test your module independently from __nodebb-plugin-import__ ```javascript // for example YourModule.testrun = function(config, callback) { async.series([ function(next) { YourModule.setup(config, next); }, function(next) { YourModule.getUsers(next); }, function(next) { YourModule.getCategories(next); }, function(next) { YourModule.getTopics(next); }, function(next) { YourModule.getPosts(next); }, function(next) { YourModule.teardown(next); } ], function(err, results) { if(err) throw err; // or whatever else fs.writeFile('./tmp.json', JSON.stringify(results, undefined, 2), callback); }); }; ``` ## Important Note On Topics and Posts: * In most forums, when creating a topic, a post will be created immediately along with it. This last post will be the __main-post__ or __parent-post__ or __topic_content_post__ or whatever other term it's known by, and it's usually saved in the same __table__ with the other posts, known as the "__reply-posts__". Usually this __parent-post__ has some sort of flag to differentiate it, such as `is_parent = 1` or `parent = 0` or something similar. * Most likely, you may have to do some table `join`ing to get each topic's record along with its __parent-post__'s content, then save it as the `_content` on each `topicsMap.[_tid]` object. * You should discard all of the other data on that __parent-post__ as, in NodeBB, it will be the topic's content. * Remember to filter these __parent-posts__ from your __reply-posts__ query so they don't get imported twice. ## Conventions In order for your exporter to be automatically recognised by the [nodebb-plugin-import](https://github.com/akhoury/nodebb-plugin-import) plugin as a compatible exporter, its name needs to start with `nodebb-plugin-import-`, e.g. `nodebb-plugin-import-ubb` You don't have to do that for it to work, you can type it in manually and it works fine. #### Why does it contain the word __import__ when it's an exporter? Because it only works with the __nodebb-plugin-import__ plugin, and I wanted to namespace it somehow. I don't care anymore, call it whatever you want.