├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Vagrantfile.sample ├── index.js ├── lib ├── categories.js ├── discourse.js ├── groups.js ├── pms.js ├── search.js ├── topics.js └── users.js ├── package.json └── test ├── category.js ├── config.sample.json ├── pm.js ├── search.js ├── sync.js ├── topic.js └── user.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | .idea/ 11 | test/config.json 12 | 13 | .vagrant/ 14 | Vagrantfile 15 | 16 | pids 17 | logs 18 | results 19 | 20 | npm-debug.log 21 | node_modules 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing (Step-by-step, shamelessly cribbed from the Discourse project) 2 | 3 | 0. Research the function(s) you want to add in the [API documentation](https://github.com/discourse/discourse_api) or if they aren't documented, learn how to [reverse-engineer the Discourse API](https://meta.discourse.org/t/how-to-reverse-engineer-the-discourse-api/20576). 4 | 5 | 1. Clone the Repo: 6 | 7 | git clone git://github.com/dhyasama/discourse-api.git 8 | 9 | 2. Create a new Branch: 10 | 11 | cd discourse-api 12 | git checkout -b new_discourse-api_branch 13 | 14 | > Please keep your code clean: one feature or bug-fix per branch. If you find another bug, you want to fix while being in a new branch, please fix it in a separated branch instead. 15 | 16 | 3. Code 17 | * Adhere to common conventions you see in the existing code 18 | * Include tests, and ensure they pass 19 | 20 | 4. Commit 21 | 22 | For every commit please write a short (max 72 characters) summary in the first line followed with a blank line and then more detailed descriptions of the change. Use markdown syntax for simple styling. 23 | 24 | **NEVER leave the commit message blank!** Provide a detailed, clear, and complete description of your commit! 25 | 26 | 27 | 5. Update your branch 28 | 29 | ``` 30 | git fetch origin 31 | git rebase origin/master 32 | ``` 33 | 34 | 6. Fork 35 | 36 | ``` 37 | git remote add mine git@github.com:/discourse-api.git 38 | ``` 39 | 40 | 7. Push to your remote 41 | 42 | ``` 43 | git push mine new_discourse-api_branch 44 | ``` 45 | 46 | 8. Issue a Pull Request 47 | 48 | Before submitting a pull-request, clean up the history, go over your commits and squash together minor changes and fixes into the corresponding commits. You can squash commits with the interactive rebase command: 49 | 50 | ``` 51 | git fetch origin 52 | git checkout new_discourse-api_branch 53 | git rebase origin/master 54 | git rebase -i 55 | 56 | < the editor opens and allows you to change the commit history > 57 | < follow the instructions on the bottom of the editor > 58 | 59 | git push -f mine new_discourse-api_branch 60 | ``` 61 | 62 | 63 | In order to make a pull request, 64 | * Navigate to the Discourse repository you just pushed to (e.g. https://github.com/your-user-name/discourse-api) 65 | * Click "Pull Request". 66 | * Write your branch name in the branch field (this is filled with "master" by default) 67 | * Click "Update Commit Range". 68 | * Ensure the changesets you introduced are included in the "Commits" tab. 69 | * Ensure that the "Files Changed" incorporate all of your changes. 70 | * Fill in some details about your potential patch including a meaningful title. 71 | * Click "Send pull request". 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jason Reynolds 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | discourse-api 2 | ============= 3 | [![Known Vulnerabilities](https://snyk.io/test/github/dhyasama/discourse-api/0b0f8cc9a7fd9d19a580166469e003542f87c1de/badge.svg)](https://snyk.io/test/github/dhyasama/discourse-api/0b0f8cc9a7fd9d19a580166469e003542f87c1de) 4 | 5 | A simple Node wrapper for the Discourse API 6 | 7 | This is just a quick stab at using the Discourse api within a Node project I'm working on. It's rough and has limited 8 | coverage at the moment but will get better with time. Contributions are welcome. 9 | 10 | I added Vagrant to make testing easier. A local box is fastest. I've also set it up to use the Digital Ocean provider. 11 | Both using Sam Saffron's Discourse Docker project. After getting Discourse running with Docker, create a package or a 12 | snapshot and then use vagrant to fire it up for testing. 13 | 14 | https://github.com/SamSaffron/discourse_docker 15 | 16 | https://github.com/smdahlen/vagrant-digitalocean 17 | -------------------------------------------------------------------------------- /Vagrantfile.sample: -------------------------------------------------------------------------------- 1 | Vagrant.configure('2') do |config| 2 | 3 | config.vm.box = "" 4 | config.vm.box_url = "" 5 | config.vm.network :private_network, ip: "" 6 | 7 | config.vm.provider :digital_ocean do |provider, override| 8 | override.ssh.private_key_path = '~/.ssh/id_rsa' 9 | override.vm.box = 'digital_ocean' 10 | override.vm.box_url = "https://github.com/smdahlen/vagrant-digitalocean/raw/master/box/digital_ocean.box" 11 | 12 | provider.client_id = 'YOUR CLIENT ID' 13 | provider.api_key = 'YOUR API KEY' 14 | provider.image = 'YOUR SNAPSHOT NAME' 15 | provider.size = 'SIZE OF DROPLET' 16 | end 17 | 18 | config.vm.provider "virtualbox" do |v| 19 | v.memory = 2048 20 | end 21 | 22 | end -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = require('./lib/discourse'); -------------------------------------------------------------------------------- /lib/categories.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function(Discourse) { 2 | 3 | "use strict"; 4 | 5 | Discourse.prototype.createCategory = function(name, color, text_color, parent_category_id, callback) { 6 | 7 | this.post('categories', { 8 | name: name, 9 | color: color || (~~(Math.random()*(1<<24))).toString(16), 10 | text_color: text_color || 'FFFFFF', 11 | parent_category_id: parent_category_id || null 12 | }, function(error, body, httpCode) { 13 | callback(error, body, httpCode); 14 | }); 15 | 16 | }; 17 | 18 | Discourse.prototype.getCategories = function(parameters, callback) { 19 | 20 | this.get('categories.json', parameters || {}, function(error, body, httpCode) { 21 | callback(error, body, httpCode); 22 | }); 23 | 24 | }; 25 | 26 | Discourse.prototype.getCategoryLatestTopic = function(category_slug, params, callback) { 27 | var url = 'c/' + category_slug + '/l/latest.json'; 28 | this.get(url, params || {}, function(error, body,httpCode) { 29 | callback(error, body, httpCode); 30 | }); 31 | 32 | } 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /lib/discourse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | request = require('request'), 5 | requestSync = require('sync-request'), 6 | querystring = require('querystring'); 7 | 8 | var actionTypeEnum = { 9 | LIKE: 1, 10 | WAS_LIKED: 2, 11 | BOOKMARK: 3, 12 | NEW_TOPIC: 4, 13 | REPLY: 5, 14 | RESPONSE: 6, 15 | MENTION: 7, 16 | QUOTE: 9, 17 | STAR: 10, 18 | EDIT: 11, 19 | NEW_PRIVATE_MESSAGE: 12, 20 | GOT_PRIVATE_MESSAGE: 13 21 | }; 22 | 23 | var Discourse = function(url, api_key, api_username) { 24 | 25 | this.url = url; 26 | this.api_key = api_key; 27 | this.api_username = api_username; 28 | 29 | }; 30 | 31 | require('./categories')(Discourse, actionTypeEnum); 32 | require('./groups')(Discourse, actionTypeEnum); 33 | require('./pms')(Discourse, actionTypeEnum); 34 | require('./search')(Discourse, actionTypeEnum); 35 | require('./topics')(Discourse, actionTypeEnum); 36 | require('./users')(Discourse, actionTypeEnum); 37 | 38 | ////////////////////////////// 39 | 40 | Discourse.prototype.get = function(url, parameters, callback) { 41 | 42 | var getUrl = this.url + '/' + url + 43 | '?api_key=' + this.api_key + 44 | '&api_username=' + this.api_username + 45 | '&' + querystring.stringify(parameters); 46 | 47 | request.get({ 48 | url: getUrl 49 | }, 50 | function(error, response, body) { 51 | 52 | if (error) { 53 | callback(error, {}, 500); 54 | } 55 | else if (!error && !!body.status && body.status !== 'OK'){ 56 | error = new Error(body.description || body.error_message); 57 | } 58 | 59 | callback(error, body || {}, response != null ? response.statusCode : null); 60 | 61 | } 62 | ); 63 | }; 64 | 65 | Discourse.prototype.getSync = function(url, parameters, auth) { 66 | 67 | var qs = auth? { 68 | api_key: this.api_key, 69 | api_username: this.api_username 70 | } : {}; 71 | for (var p in parameters) qs[p] = parameters[p]; 72 | 73 | var getUrl = this.url + '/' + url; 74 | 75 | return requestSync('GET', getUrl, {qs: qs}); 76 | 77 | }; 78 | 79 | Discourse.prototype.post = function(url, parameters, callback) { 80 | 81 | var postUrl = this.url + '/' + url + '?api_key=' + this.api_key + '&api_username=' + this.api_username; 82 | 83 | request({ 84 | uri: postUrl, 85 | method: 'POST', 86 | form: parameters 87 | }, 88 | function (error, response, body) { 89 | 90 | if (!error && !!body.status && body.status !== 'OK') { 91 | error = new Error(body.description || body.error_message); 92 | } 93 | 94 | callback(error, body || {}, response != null ? response.statusCode : null); 95 | 96 | }); 97 | 98 | }; 99 | 100 | Discourse.prototype.postSync = function(url, parameters) { 101 | var postUrl = this.url + '/' + url + '?api_key=' + this.api_key + '&api_username=' + this.api_username; 102 | 103 | return requestSync('POST', postUrl, { 104 | headers: { 105 | 'content-type': 'application/json' 106 | }, 107 | body: JSON.stringify(parameters) 108 | }); 109 | 110 | }; 111 | 112 | Discourse.prototype.put = function(url, parameters, callback) { 113 | 114 | var putUrl = this.url + '/' + url + '?api_key=' + this.api_key + '&api_username=' + this.api_username; 115 | 116 | request({ 117 | uri: putUrl, 118 | method: 'PUT', 119 | form: parameters 120 | }, 121 | function (error, response, body) { 122 | 123 | if (!error && !!body.status && body.status !== 'OK') { 124 | error = new Error(body.description || body.error_message); 125 | } 126 | 127 | callback(error, body || {}, response != null ? response.statusCode : null); 128 | 129 | }); 130 | 131 | }; 132 | 133 | Discourse.prototype.putSync = function(url, parameters, callback) { 134 | 135 | var putUrl = this.url + '/' + url + '?api_key=' + this.api_key + '&api_username=' + this.api_username; 136 | 137 | return requestSync('PUT', putUrl, { 138 | headers: { 139 | 'content-type': 'application/json' 140 | }, 141 | body: JSON.stringify(parameters) 142 | }); 143 | 144 | }; 145 | 146 | Discourse.prototype.delete = function(url, parameters, callback) { 147 | 148 | var deleteUrl = this.url + '/' + url + '?api_key=' + this.api_key + '&api_username=' + this.api_username; 149 | 150 | request({ 151 | uri: deleteUrl, 152 | method: 'DELETE', 153 | form: parameters 154 | }, 155 | function (error, response, body) { 156 | 157 | if (!error && !!body.status && body.status !== 'OK') { 158 | error = new Error(body.description || body.error_message); 159 | } 160 | 161 | callback(error, body || {}, response != null ? response.statusCode : null); 162 | 163 | }); 164 | 165 | }; 166 | 167 | Discourse.prototype.deleteSync = function (url, parameters, callback) { 168 | 169 | var deleteUrl = this.url + '/' + url; 170 | var qs = { 171 | api_key: this.api_key, 172 | api_username: this.api_username 173 | }; 174 | for (var p in parameters) qs[p] = parameters[p]; 175 | 176 | return requestSync('DELETE', deleteUrl, { 177 | headers: { 178 | 'content-type': 'application/json' 179 | }, 180 | qs: qs 181 | }); 182 | }; 183 | 184 | ////////////////////////////// 185 | 186 | module.exports = Discourse; 187 | -------------------------------------------------------------------------------- /lib/groups.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function(Discourse) { 2 | 3 | "use strict"; 4 | 5 | Discourse.prototype.getGroupMembers = function(groupName, callback) { 6 | this.get('groups/' + groupName + '/members.json', {}, function (error, body, httpCode) { 7 | callback(error, body, httpCode); 8 | }); 9 | }; 10 | 11 | Discourse.prototype.createGroup = function(groupName, aliasLevel, automatic, email_domains, membership_retroactive, trust_level, primary_group, title, visible, callback) { 12 | this.post('admin/groups', 13 | { 14 | 'name': groupName, 15 | 'alias_level': aliasLevel || -2, 16 | 'automatic': automatic || false, 17 | 'automatic_membership_email_domains': email_domains || "", 18 | 'automatic_membership_retroactive': membership_retroactive || false, 19 | 'grant_trust_level': trust_level || 1, 20 | 'primary_group': primary_group || false, 21 | 'title': title, 22 | 'visible': visible || true 23 | }, 24 | function(error, body, httpCode) { 25 | callback(error, body, httpCode); 26 | } 27 | ); 28 | }; 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /lib/pms.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function(Discourse, actionTypeEnum) { 2 | 3 | "use strict"; 4 | 5 | var actionTypeEnum = actionTypeEnum; 6 | 7 | Discourse.prototype.createPrivateMessage = function(title, raw, target_usernames, callback) { 8 | this.post('posts', 9 | { 10 | 'title': title, 11 | 'raw': raw, 12 | 'target_usernames': target_usernames, 13 | 'archetype': 'private_message' 14 | }, 15 | function(error, body, httpCode) { 16 | callback(error, body, httpCode); 17 | } 18 | ); 19 | }; 20 | 21 | Discourse.prototype.getPrivateMessages = function(username, callback) { 22 | 23 | this.get( 24 | 'topics/private-messages/' + username + '.json', 25 | {}, 26 | function(error, body, httpCode) { 27 | callback(error, body, httpCode); 28 | }); 29 | 30 | }; 31 | 32 | Discourse.prototype.getUnreadPrivateMessages = function(username, callback) { 33 | 34 | this.get( 35 | 'topics/private-messages-unread/' + username + '.json', 36 | {}, 37 | function(error, body, httpCode) { 38 | callback(error, body, httpCode); 39 | }); 40 | 41 | }; 42 | 43 | Discourse.prototype.getPrivateMessageThread = function(topic_id, callback) { 44 | 45 | this.getTopicAndReplies(topic_id, callback); 46 | 47 | }; 48 | 49 | Discourse.prototype.getSentPrivateMessages = function(username, callback) { 50 | 51 | this.get('user_actions.json', 52 | { 53 | username: username, 54 | filter: actionTypeEnum.NEW_PRIVATE_MESSAGE 55 | }, 56 | function(error, body, httpCode) { 57 | callback(error, body, httpCode); 58 | } 59 | ); 60 | 61 | }; 62 | 63 | Discourse.prototype.getReceivedPrivateMessages = function(username, callback) { 64 | 65 | this.get('user_actions.json', 66 | { 67 | username: username, 68 | filter: actionTypeEnum.GOT_PRIVATE_MESSAGE 69 | }, 70 | function(error, body, httpCode) { 71 | callback(error, body, httpCode); 72 | } 73 | ); 74 | 75 | }; 76 | 77 | Discourse.prototype.replyToPrivateMessage = function(raw, topic_id, callback) { 78 | this.post('posts', { 'raw': raw, 'topic_id': topic_id }, function(error, body, httpCode) { 79 | callback(error, body, httpCode); 80 | }); 81 | }; 82 | 83 | }; 84 | -------------------------------------------------------------------------------- /lib/search.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function(Discourse) { 2 | 3 | "use strict"; 4 | 5 | Discourse.prototype.searchForUser = function(username, callback) { 6 | this.get('users/search/users.json', { term: username }, function(error, body, httpCode) { 7 | callback(error, body, httpCode); 8 | }); 9 | }; 10 | 11 | Discourse.prototype.search = function(term, callback) { 12 | this.get('search.json', { term: term }, function(error, body, httpCode) { 13 | callback(error, body, httpCode); 14 | }); 15 | }; 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /lib/topics.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function(Discourse, actionTypeEnum) { 2 | 3 | "use strict"; 4 | 5 | var actionTypeEnum = actionTypeEnum; 6 | 7 | Discourse.prototype.createTopic = function(title, raw, category, callback) { 8 | this.post('posts', { 'title': title, 'raw': raw, 'category': category, 'archetype': 'regular' }, function(error, body, httpCode) { 9 | callback(error, body, httpCode); 10 | }); 11 | }; 12 | 13 | Discourse.prototype.createTopicSync = function(title, raw, category) { 14 | 15 | return this.postSync('posts', { 'title': title, 'raw': raw, 'category': category, 'archetype': 'regular' }); 16 | 17 | }; 18 | 19 | Discourse.prototype.getCreatedTopics = function(username, callback) { 20 | 21 | var that = this; 22 | 23 | this.get('user_actions.json', 24 | { 25 | username: username, 26 | filter: that.actionTypeEnum.NEW_TOPIC 27 | }, 28 | function(error, body, httpCode) { 29 | callback(error, body, httpCode); 30 | } 31 | ); 32 | 33 | }; 34 | 35 | Discourse.prototype.getCreatedTopicsSync = function(username) { 36 | 37 | return this.getSync('topics/created-by/' + (username || this.api_username)+ '.json', {}, true); 38 | 39 | }; 40 | 41 | Discourse.prototype.getLastPostId = function(callback) { 42 | 43 | this.get('/posts.json', 44 | {}, 45 | function (error, body, httpCode) { 46 | callback(error, JSON.parse(body).latest_posts[0].id, httpCode); 47 | } 48 | ); 49 | 50 | }; 51 | 52 | Discourse.prototype.getLastPostIdSync = function() { 53 | 54 | var response = this.getSync('posts.json', {}, true); 55 | if (response.statusCode === 200) { 56 | var body = JSON.parse(response.body); 57 | return body.latest_posts[0].id; 58 | } else { 59 | throw new Error(response.headers.status); 60 | } 61 | 62 | }; 63 | 64 | Discourse.prototype.getPost = function(post_id, callback) { 65 | 66 | this.get('posts/' + post_id + '.json', 67 | {}, 68 | function (error, body, httpCode) { 69 | callback(error, JSON.parse(body), httpCode); 70 | } 71 | ); 72 | 73 | }; 74 | 75 | Discourse.prototype.getPostSync = function(post_id) { 76 | 77 | return JSON.parse(this.getSync('posts/' + post_id + '.json', {}, true).body); 78 | 79 | }; 80 | 81 | Discourse.prototype.replyToTopic = function(raw, topic_id, callback) { 82 | this.post('posts', { 'raw': raw, 'topic_id': topic_id }, function(error, body, httpCode) { 83 | callback(error, body, httpCode); 84 | }); 85 | }; 86 | 87 | Discourse.prototype.replyToPost = function(raw, topic_id, reply_to_post_number, callback) { 88 | this.post('posts', { 'raw': raw, 'topic_id': topic_id, 'reply_to_post_number': reply_to_post_number }, function(error, body, httpCode) { 89 | callback(error, body, httpCode); 90 | }); 91 | }; 92 | 93 | Discourse.prototype.getTopicAndReplies = function(topic_id, callback) { 94 | 95 | this.get('t/' + topic_id + '.json', 96 | {}, 97 | function(error, body, httpCode) { 98 | callback(error, body, httpCode); 99 | } 100 | ); 101 | 102 | }; 103 | 104 | Discourse.prototype.deleteTopic = function(topic_id, callback) { 105 | 106 | this.delete('t/' + topic_id, 107 | {}, 108 | function(error, body, httpCode) { 109 | callback(error, body, httpCode); 110 | } 111 | ); 112 | 113 | }; 114 | 115 | Discourse.prototype.updatePost = function(post_id, raw, edit_reason, callback) { 116 | 117 | this.put( 118 | 'posts/' + post_id, 119 | { 'post[raw]': raw, 'post[edit_reason]': edit_reason }, 120 | function(error, body, httpCode) { 121 | callback(error, body, httpCode); 122 | } 123 | ); 124 | 125 | }; 126 | 127 | Discourse.prototype.updatePostSync = function(post_id, raw, edit_reason) { 128 | 129 | return this.putSync('posts/' + post_id, { 130 | post: { 131 | raw: raw, 132 | edit_reason: edit_reason 133 | } 134 | }); 135 | 136 | }; 137 | 138 | Discourse.prototype.updateTopic = function(slug, topic_id, title, category, callback) { 139 | 140 | this.put( 141 | 't/' + slug + '/' + topic_id, 142 | { title: title, category: category }, 143 | function(error, body, httpCode) { 144 | callback(error, body, httpCode); 145 | } 146 | ); 147 | 148 | }; 149 | 150 | }; 151 | -------------------------------------------------------------------------------- /lib/users.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function(Discourse, actionTypeEnum) { 2 | 3 | "use strict"; 4 | 5 | var actionTypeEnum = actionTypeEnum; 6 | 7 | Discourse.prototype.activateUser = function (id, username, callback) { 8 | this.put('admin/users/' + id + '/activate', 9 | {context: 'admin/users/' + username}, 10 | function (error, body, httpCode) { 11 | callback(error, body, httpCode); 12 | } 13 | ); 14 | }; 15 | 16 | Discourse.prototype.approveUser = function (id, username, callback) { 17 | this.put('admin/users/' + id + '/approve', 18 | {context: 'admin/users/' + username}, 19 | function (error, body, httpCode) { 20 | callback(error, body, httpCode); 21 | } 22 | ); 23 | }; 24 | 25 | Discourse.prototype.createUser = function (name, email, username, password, active, callback) { 26 | 27 | var that = this; 28 | 29 | that.post('users', 30 | { 31 | 'name': name, 32 | 'email': email, 33 | 'username': username, 34 | 'password': password, 35 | 'active': active 36 | }, 37 | function (error, body, httpCode) { 38 | callback(error, body, httpCode); 39 | } 40 | ); 41 | 42 | }; 43 | 44 | Discourse.prototype.deleteUser = function (id, username, callback) { 45 | this.delete('admin/users/' + id + '.json', 46 | {context: '/admin/users/' + username}, 47 | function (error, body, httpCode) { 48 | callback(error, body, httpCode); 49 | } 50 | ); 51 | }; 52 | 53 | /** 54 | * Delete user and block their email and IP address 55 | * @param id 56 | * @param username 57 | * @param callback 58 | */ 59 | Discourse.prototype.deleteAndBlockUser = function (id, username, callback) { 60 | this.delete('/admin/users/' + id + '.json', 61 | { 62 | context: '/admin/users/' + username, 63 | block_email: true, 64 | block_urls: true, 65 | block_ip: true 66 | }, 67 | function (error, body, httpCode) { 68 | callback(error, body, httpCode); 69 | } 70 | ); 71 | }; 72 | 73 | Discourse.prototype.deleteAndBlockUserSync = function(id, username) { 74 | 75 | return this.deleteSync('admin/users/' + id + '.json', { 76 | context: '/admin/users/' + username, 77 | block_email: true, 78 | block_urls: true, 79 | block_ip: true 80 | }); 81 | }; 82 | 83 | /** 84 | * Filter users by username, email, or IP address via the Admin list of active users 85 | * @param {String} filter - username, email or IP address for partial matching against users 86 | * @param callback 87 | */ 88 | Discourse.prototype.filterUsers = function (filter, callback) { 89 | this.get('admin/users/list/active.json', { filter: filter, show_emails: true }, function(error, body, httpCode) { 90 | callback(error, body, httpCode); 91 | }); 92 | }; 93 | 94 | /** 95 | * Filter users by username, email, or IP address via the Admin list of active users 96 | * @param {String} filter - username, email or IP address for partial matching against users 97 | * @return Array of up to 100 user objects 98 | */ 99 | Discourse.prototype.filterUsersSync = function(filter) { 100 | return JSON.parse(this.getSync('admin/users/list/active.json', { filter: filter, show_emails: true }, true).body); 101 | } 102 | 103 | 104 | Discourse.prototype.getUser = function (username, callback) { 105 | this.get('users/' + username + '.json', 106 | {}, 107 | function (error, body, httpCode) { 108 | 109 | if (error) return callback(error, null); 110 | 111 | try { 112 | var json = JSON.parse(body); 113 | if (json.user.id) return callback(null, json); 114 | else return callback(null, null); 115 | } 116 | catch (err) { 117 | return callback(err, null); 118 | } 119 | 120 | } 121 | ); 122 | }; 123 | 124 | Discourse.prototype.getUserActivity = function (username, offset, callback) { 125 | this.get('user_actions.json', 126 | { 127 | username: username, 128 | filter: actionTypeEnum.REPLY, 129 | offset: offset || 0 130 | }, 131 | function (error, body, httpCode) { 132 | callback(error, body, httpCode); 133 | } 134 | ); 135 | }; 136 | 137 | Discourse.prototype.login = function (username, password, callback) { 138 | this.post('session', {'login': username, 'password': password}, function (error, body, httpCode) { 139 | callback(error, body, httpCode); 140 | }); 141 | }; 142 | 143 | Discourse.prototype.logout = function (username, callback) { 144 | this.delete('session/' + username, {}, function (error, body, httpCode) { 145 | callback(error, body, httpCode); 146 | }); 147 | }; 148 | 149 | Discourse.prototype.fetchConfirmationValue = function (callback) { 150 | 151 | // discourse api should bypass the honeypot since it is a trusted user (confirmed via api key) 152 | 153 | this.get('users/hp.json', 154 | {}, 155 | function (error, body, httpCode) { 156 | callback(error, body, httpCode); 157 | } 158 | ); 159 | 160 | }; 161 | 162 | Discourse.prototype.getUserEmail = function (username, callback) { 163 | this.put('users/' + username + '/emails.json', 164 | {context: '/users/' + username + '/activity'}, 165 | function (error, body, httpCode) { 166 | callback(error, body, httpCode); 167 | } 168 | ); 169 | }; 170 | 171 | }; 172 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Jason Reynolds", 3 | "name": "discourse-api", 4 | "description": "A simple Node wrapper for the Discourse API", 5 | "version": "1.8.0", 6 | "repository": "https://github.com/dhyasama/discourse-api", 7 | "private": false, 8 | "scripts": { 9 | "test": "mocha -R spec -t 10000" 10 | }, 11 | "dependencies": { 12 | "querystring": "~0.2.0", 13 | "request": "~2.74.0", 14 | "sync-request": "^3.0.0" 15 | }, 16 | "devDependencies": { 17 | "mocha": "~1.16.2", 18 | "should": "~2.1.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/category.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | should = require("should"), 5 | fs = require('fs'), 6 | path = require('path'), 7 | config = JSON.parse(fs.readFileSync(path.normalize(__dirname + '/config.json', 'utf8'))); 8 | 9 | describe('Discourse Category API', function() { 10 | 11 | var 12 | Discourse = require('../lib/discourse'), 13 | api = new Discourse(config.url, config.api.key, config.api.username); 14 | 15 | it('creates a category', function(done) { 16 | 17 | require('crypto').randomBytes(5, function(err, buf) { 18 | 19 | api.createCategory(config.category.name + ' ' + buf.toString('hex').toUpperCase(), config.category.color, config.category.text_color, config.category.parent_category_id, function(err, body, httpCode) { 20 | 21 | // make assertions 22 | should.not.exist(err); 23 | should.exist(body); 24 | 25 | httpCode.should.equal(200); 26 | 27 | var json = JSON.parse(body); 28 | 29 | // make more assertions 30 | json.should.have.properties('category'); 31 | json.category.id.should.be.above(0); 32 | 33 | done(); 34 | 35 | }); 36 | 37 | }); 38 | 39 | }); 40 | 41 | it('gets category list', function(done) { 42 | 43 | api.getCategories({}, function (err, body, httpCode) { 44 | // make assertions 45 | should.not.exist(err); 46 | should.exist(body); 47 | 48 | httpCode.should.equal(200); 49 | 50 | 51 | var json = JSON.parse(body); 52 | 53 | // make more assertions 54 | json.should.have.properties('category_list'); 55 | 56 | done(); 57 | }); 58 | 59 | }); 60 | 61 | it('gets latest topics from a category', function(done) { 62 | api.getCategoryLatestTopic('uncategorized', {}, function (err, body, httpCode){ 63 | // make assertions 64 | should.not.exist(err); 65 | should.exist(body); 66 | 67 | httpCode.should.equal(200); 68 | 69 | var json = JSON.parse(body); 70 | json.should.have.properties('topic_list'); 71 | done(); 72 | }); 73 | }); 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /test/config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "url": "", 4 | 5 | "api": { 6 | "key": "", 7 | "username": "" 8 | }, 9 | 10 | "user": { 11 | "new": { 12 | "email": "", 13 | "password": "" 14 | } 15 | }, 16 | 17 | "topic": { 18 | "category": "", 19 | "title": "", 20 | "body": "", 21 | "reply": { 22 | "body": "" 23 | }, 24 | "post": { 25 | "reply": { 26 | "body": "" 27 | } 28 | } 29 | }, 30 | 31 | "pm": { 32 | "target_usernames": "", 33 | "title": "", 34 | "body": "", 35 | "reply": { 36 | "body": "" 37 | } 38 | }, 39 | 40 | "search": { 41 | "term": "", 42 | "username": "" 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /test/pm.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | should = require("should"), 5 | fs = require('fs'), 6 | path = require('path'), 7 | config = JSON.parse(fs.readFileSync(path.normalize(__dirname + '/config.json', 'utf8'))); 8 | 9 | describe('Discourse Private Message API', function() { 10 | 11 | var 12 | Discourse = require('../lib/discourse'), 13 | api = new Discourse(config.url, config.api.key, config.api.username), 14 | topic_id = ''; 15 | 16 | it('creates a private message', function(done) { 17 | 18 | api.createPrivateMessage(config.pm.title, config.pm.body, config.pm.target_usernames, function(err, body, httpCode) { 19 | 20 | // make assertions 21 | should.not.exist(err); 22 | should.exist(body); 23 | httpCode.should.equal(200); 24 | 25 | var json = JSON.parse(body); 26 | 27 | // make more assertions 28 | json.should.have.properties('topic_id'); 29 | json.topic_id.should.be.above(0); 30 | 31 | // save for next test 32 | topic_id = json.topic_id; 33 | 34 | done(); 35 | 36 | }); 37 | }); 38 | 39 | it('replies to a private message', function(done) { 40 | 41 | // topic_id is set in previous test 42 | 43 | api.replyToPrivateMessage(config.pm.reply.body, topic_id, function(err, body, httpCode) { 44 | 45 | // make assertions 46 | should.not.exist(err); 47 | should.exist(body); 48 | httpCode.should.equal(200); 49 | 50 | var json = JSON.parse(body); 51 | 52 | // make more assertions 53 | json.should.have.properties('id'); 54 | json.id.should.be.above(0); 55 | 56 | done(); 57 | 58 | }); 59 | }); 60 | 61 | it('gets a private message thread', function(done) { 62 | 63 | // topic_id is set in previous test 64 | 65 | api.getPrivateMessageThread(topic_id, function(err, body, httpCode) { 66 | 67 | // make assertions 68 | should.not.exist(err); 69 | should.exist(body); 70 | httpCode.should.equal(200); 71 | 72 | var json = JSON.parse(body); 73 | 74 | // make more assertions 75 | json.should.have.properties('id'); 76 | json.id.should.equal(topic_id); 77 | 78 | done(); 79 | 80 | }); 81 | 82 | }); 83 | 84 | it('gets all private messages for a user', function(done) { 85 | 86 | api.getPrivateMessages(config.api.username, function(err, body, httpCode) { 87 | 88 | // make assertions 89 | should.not.exist(err); 90 | should.exist(body); 91 | httpCode.should.equal(200); 92 | 93 | // todo - check json 94 | 95 | done(); 96 | 97 | }); 98 | }); 99 | 100 | it('gets all unread private messages for a user', function(done) { 101 | 102 | api.getUnreadPrivateMessages(config.api.username, function(err, body, httpCode) { 103 | 104 | // make assertions 105 | should.not.exist(err); 106 | should.exist(body); 107 | httpCode.should.equal(200); 108 | 109 | // todo - check json 110 | 111 | done(); 112 | 113 | }); 114 | }); 115 | 116 | it('gets private messages sent by a user', function(done) { 117 | 118 | api.getSentPrivateMessages(config.api.username, function(err, body, httpCode) { 119 | 120 | // make assertions 121 | should.not.exist(err); 122 | should.exist(body); 123 | httpCode.should.equal(200); 124 | 125 | // todo - check json 126 | 127 | done(); 128 | 129 | }); 130 | }); 131 | 132 | it('gets private messages received by a user', function(done) { 133 | 134 | api.getReceivedPrivateMessages(config.api.username, function(err, body, httpCode) { 135 | 136 | // make assertions 137 | should.not.exist(err); 138 | should.exist(body); 139 | httpCode.should.equal(200); 140 | 141 | // todo - check json 142 | 143 | done(); 144 | 145 | }); 146 | }); 147 | 148 | }); -------------------------------------------------------------------------------- /test/search.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | should = require("should"), 5 | fs = require('fs'), 6 | path = require('path'), 7 | config = JSON.parse(fs.readFileSync(path.normalize(__dirname + '/config.json', 'utf8'))); 8 | 9 | describe('Discourse Search API', function() { 10 | 11 | var 12 | Discourse = require('../lib/discourse'), 13 | api = new Discourse(config.url, config.api.key, config.api.username); 14 | 15 | it('searches for a term', function(done) { 16 | 17 | api.search(config.search.term, function(err, body, httpCode) { 18 | 19 | // make assertions 20 | should.not.exist(err); 21 | should.exist(body); 22 | httpCode.should.equal(200); 23 | 24 | // todo - check json 25 | 26 | done(); 27 | 28 | }); 29 | }); 30 | 31 | it('searches for a user', function(done) { 32 | 33 | api.searchForUser(config.search.username, function(err, body, httpCode) { 34 | 35 | // make assertions 36 | should.not.exist(err); 37 | should.exist(body); 38 | httpCode.should.equal(200); 39 | 40 | // todo - check json 41 | 42 | done(); 43 | 44 | }); 45 | }); 46 | 47 | }); -------------------------------------------------------------------------------- /test/sync.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | should = require("should"), 5 | fs = require('fs'), 6 | path = require('path'), 7 | config = JSON.parse(fs.readFileSync(path.normalize(__dirname + '/config.json', 'utf8'))); 8 | 9 | 10 | describe('Discourse Sync API', function() { 11 | 12 | var 13 | Discourse = require('../lib/discourse'), 14 | api = new Discourse(config.url, config.api.key, config.api.username); 15 | 16 | it('gets site.json synchronously', function(done) { 17 | 18 | var res = api.getSync('/site.json'); 19 | res.statusCode.should.equal(200); 20 | 21 | done(); 22 | 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /test/topic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | should = require("should"), 5 | fs = require('fs'), 6 | path = require('path'), 7 | config = JSON.parse(fs.readFileSync(path.normalize(__dirname + '/config.json', 'utf8'))); 8 | 9 | describe('Discourse Topic API', function() { 10 | 11 | var 12 | Discourse = require('../lib/discourse'), 13 | api = new Discourse(config.url, config.api.key, config.api.username), 14 | topic_id = '', 15 | slug = '', 16 | post_id = ''; 17 | 18 | it('creates a topic', function(done) { 19 | 20 | // append random string to title because discourse has a setting to prevent duplicate titles 21 | // alternatively, turn off the setting :) 22 | 23 | require('crypto').randomBytes(5, function(err, buf) { 24 | 25 | api.createTopic(config.topic.title + ' ' + buf.toString('hex').toUpperCase(), config.topic.body, config.topic.category, function(err, body, httpCode) { 26 | 27 | // make assertions 28 | should.not.exist(err); 29 | should.exist(body); 30 | 31 | httpCode.should.equal(200); 32 | 33 | var json = JSON.parse(body); 34 | 35 | // make more assertions 36 | json.should.have.properties('topic_id'); 37 | json.topic_id.should.be.above(0); 38 | 39 | // save for subsequent tests 40 | topic_id = json.topic_id; 41 | slug = json.topic_slug; 42 | 43 | done(); 44 | 45 | }); 46 | 47 | }); 48 | 49 | }); 50 | 51 | it('updates a topic', function(done) { 52 | 53 | // topic_id set in previous test 54 | 55 | // append random string to title because discourse has a setting to prevent duplicate titles 56 | 57 | require('crypto').randomBytes(5, function(err, buf) { 58 | 59 | api.updateTopic(slug, topic_id, config.topic.title + ' UPDATE ' + buf.toString('hex').toUpperCase(), 'uncategorized', function(err, body, httpCode) { 60 | 61 | // make assertions 62 | should.not.exist(err); 63 | should.exist(body); 64 | httpCode.should.equal(200); 65 | 66 | var json = JSON.parse(body); 67 | 68 | // make more assertions 69 | json.should.have.properties('basic_topic'); 70 | json.basic_topic.id.should.be.above(0); 71 | 72 | done(); 73 | 74 | }); 75 | 76 | }); 77 | 78 | }); 79 | 80 | it('replies to a topic', function(done) { 81 | 82 | // topic_id set in previous test 83 | 84 | api.replyToTopic(config.topic.reply.body, topic_id, function(err, body, httpCode) { 85 | 86 | // make assertions 87 | should.not.exist(err); 88 | should.exist(body); 89 | httpCode.should.equal(200); 90 | 91 | var json = JSON.parse(body); 92 | 93 | // make more assertions 94 | json.should.have.properties('id'); 95 | json.id.should.be.above(0); 96 | 97 | post_id = json.id; 98 | 99 | done(); 100 | 101 | }); 102 | }); 103 | 104 | it('updates a post', function(done) { 105 | 106 | // topic_id set in previous test 107 | 108 | api.updatePost(post_id, 'i updated my post! horray!', 'testing', function(err, body, httpCode) { 109 | 110 | // make assertions 111 | should.not.exist(err); 112 | should.exist(body); 113 | httpCode.should.equal(200); 114 | 115 | var json = JSON.parse(body); 116 | 117 | // make more assertions 118 | json.should.have.properties('post'); 119 | //json.basic_topic.id.should.be.above(0); 120 | 121 | done(); 122 | 123 | }); 124 | 125 | }); 126 | 127 | it('replies to a post', function(done) { 128 | 129 | // topic_id set in previous test 130 | 131 | api.replyToPost(config.topic.post.reply.body, topic_id, 1, function(err, body, httpCode) { 132 | 133 | // make assertions 134 | should.not.exist(err); 135 | should.exist(body); 136 | httpCode.should.equal(200); 137 | 138 | var json = JSON.parse(body); 139 | 140 | // make more assertions 141 | json.should.have.properties('id'); 142 | json.id.should.be.above(0); 143 | 144 | done(); 145 | 146 | }); 147 | }); 148 | 149 | it('gets a topic and its replies', function(done) { 150 | 151 | // topic_id set in previous test 152 | 153 | api.getTopicAndReplies(topic_id, function(err, body, httpCode) { 154 | 155 | // make assertions 156 | should.not.exist(err); 157 | should.exist(body); 158 | httpCode.should.equal(200); 159 | 160 | var json = JSON.parse(body); 161 | 162 | // make more assertions 163 | json.should.have.properties('id'); 164 | json.id.should.be.above(0); 165 | 166 | done(); 167 | 168 | }); 169 | }); 170 | 171 | // it('deletes a topic', function(done) { 172 | // 173 | // // topic_id set in previous test 174 | // 175 | // api.deleteTopic(topic_id, function(err, body, httpCode) { 176 | // 177 | // // make assertions 178 | // should.not.exist(err); 179 | // should.exist(body); 180 | // httpCode.should.equal(200); 181 | // 182 | // // todo - check body 183 | // 184 | // done(); 185 | // 186 | // }); 187 | // 188 | // }); 189 | 190 | }); 191 | -------------------------------------------------------------------------------- /test/user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var 4 | should = require("should"), 5 | fs = require('fs'), 6 | path = require('path'), 7 | config = JSON.parse(fs.readFileSync(path.normalize(__dirname + '/config.json', 'utf8'))); 8 | 9 | describe('Discourse User API', function() { 10 | 11 | var 12 | Discourse = require('../lib/discourse'), 13 | api = new Discourse(config.url, config.api.key, config.api.username), 14 | user_id = '', 15 | username = '', 16 | password = ''; 17 | 18 | it('creates a user', function(done) { 19 | 20 | require('crypto').randomBytes(5, function(err, buf) { 21 | 22 | // pick a random username, doesn't have to be humane 23 | // use that to create a full name (TEST is first name, username is last name) 24 | // add username to email address so it will be unique. takes advantage of gmail trick of making dhyasama@gmail become dhyasama+[whatevs]@gmail.com 25 | // password is always the same so you can log in as user if you want to verify via web ui 26 | 27 | var 28 | random = buf.toString('hex').toUpperCase(), 29 | fullName = 'Test ' + random, 30 | email = config.user.new.email.replace('@', '+' + random + '@'); 31 | 32 | username = random; 33 | password = config.user.new.password; 34 | 35 | api.createUser(fullName, email, username, password, true, function(err, body, httpCode) { 36 | 37 | // make assertions 38 | should.not.exist(err); 39 | should.exist(body); 40 | httpCode.should.equal(200); 41 | 42 | var json = JSON.parse(body); 43 | 44 | //make more assertions 45 | json.should.have.properties('success'); 46 | json.success.should.equal(true); 47 | json.should.have.properties('active'); 48 | json.active.should.equal(true); 49 | 50 | user_id = json.user_id; 51 | 52 | done(); 53 | 54 | }); 55 | 56 | }); 57 | }); 58 | 59 | //it('activates a user', function(done) { 60 | // 61 | // api.activateUser(user_id, username, function(err, body, httpCode) { 62 | // 63 | // // make assertions 64 | // should.not.exist(err); 65 | // should.exist(body); 66 | // httpCode.should.equal(200); 67 | // 68 | // done(); 69 | // 70 | // }); 71 | // 72 | //}); 73 | 74 | it('gets a user', function(done) { 75 | 76 | // username is assigned in previous test 77 | 78 | api.getUser(username, function(err, user) { 79 | 80 | // make assertions 81 | should.not.exist(err); 82 | should.exist(user); 83 | 84 | // make more assertions 85 | user.should.have.properties('user'); 86 | user.user.should.have.properties('id'); 87 | 88 | done(); 89 | 90 | }); 91 | }); 92 | 93 | it('approves a user', function(done) { 94 | 95 | // user_id and username are assigned in previous test 96 | 97 | api.approveUser(user_id, username, function(err, body, httpCode) { 98 | 99 | // make assertions 100 | should.not.exist(err); 101 | should.exist(body); 102 | httpCode.should.equal(200); 103 | 104 | done(); 105 | 106 | }); 107 | }); 108 | 109 | //it('logs in a user', function(done) { 110 | // 111 | // // username and password are assigned in previous test 112 | // 113 | // api.login(username, password, function(err, body, httpCode) { 114 | // 115 | // // make assertions 116 | // should.not.exist(err); 117 | // should.exist(body); 118 | // httpCode.should.equal(200); 119 | // 120 | // var json = JSON.parse(body); 121 | // 122 | // // make more assertions 123 | // json.should.not.have.properties('error'); // todo - should this be in more places? 124 | // json.should.have.properties('user'); 125 | // json.user.should.have.properties('username'); 126 | // json.user.username.should.equal(username); 127 | // 128 | // done(); 129 | // 130 | // }); 131 | //}); 132 | 133 | it('gets a user email', function(done) { 134 | 135 | // username is assigned in previous test 136 | 137 | api.getUserEmail(username, function(err, body, httpCode) { 138 | 139 | // make assertions 140 | should.not.exist(err); 141 | should.exist(body); 142 | httpCode.should.equal(200); 143 | 144 | var json = JSON.parse(body); 145 | 146 | // make more assertions 147 | json.should.have.properties('email'); 148 | 149 | done(); 150 | }); 151 | }); 152 | 153 | }); 154 | --------------------------------------------------------------------------------