diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ea47e9..066829b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog This file contains highlights of what changes on each version of the [Akismet for JS](https://github.com/cedx/akismet.js) library. +## Version 9.1.0 +- Removed the dependency on [Babel](https://babeljs.io) compiler. +- Updated the package dependencies. + ## Version 9.0.0 - Breaking change: reverted the API of the `Client` class to an [Observable](http://reactivex.io/intro.html)-based one. - Added new unit tests. diff --git a/gulpfile.js b/gulpfile.js index 09bb4de0..3a62e68d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -4,22 +4,13 @@ const {david} = require('@cedx/gulp-david'); const {spawn} = require('child_process'); const del = require('del'); const gulp = require('gulp'); -const babel = require('gulp-babel'); const eslint = require('gulp-eslint'); const {normalize} = require('path'); /** * Runs the default tasks. */ -gulp.task('default', ['build']); - -/** - * Builds the sources. - */ -gulp.task('build', () => gulp.src('src/**/*.js') - .pipe(babel()) - .pipe(gulp.dest('lib')) -); +gulp.task('default', ['lint']); /** * Deletes all generated files and reset any saved state. @@ -49,7 +40,7 @@ gulp.task('doc', async () => { /** * Fixes the coding standards issues. */ -gulp.task('fix', () => gulp.src(['*.js', 'src/**/*.js', 'test/**/*.js'], {base: '.'}) +gulp.task('fix', () => gulp.src(['*.js', 'lib/**/*.js', 'test/**/*.js'], {base: '.'}) .pipe(eslint({fix: true})) .pipe(gulp.dest('.')) ); @@ -57,7 +48,7 @@ gulp.task('fix', () => gulp.src(['*.js', 'src/**/*.js', 'test/**/*.js'], {base: /** * Performs static analysis of source code. */ -gulp.task('lint', () => gulp.src(['*.js', 'src/**/*.js', 'test/**/*.js']) +gulp.task('lint', () => gulp.src(['*.js', 'lib/**/*.js', 'test/**/*.js']) .pipe(eslint()) .pipe(eslint.format()) ); @@ -69,7 +60,6 @@ gulp.task('test', () => _exec('node_modules/.bin/nyc', [ '--report-dir=var', '--reporter=lcovonly', normalize('node_modules/.bin/mocha'), - '--compilers=js:babel-register', '--recursive' ])); diff --git a/lib/author.js b/lib/author.js new file mode 100644 index 00000000..bec3dc56 --- /dev/null +++ b/lib/author.js @@ -0,0 +1,94 @@ +'use strict'; + +const {URL} = require('url'); + +/** + * Represents the author of a comment. + */ +exports.Author = class Author { + + /** + * Initializes a new instance of the class. + * @param {string} [ipAddress] The author's IP address. + * @param {string} [userAgent] The author's user agent. + */ + constructor(ipAddress = '', userAgent = '') { + + /** + * The author's mail address. + * @type {string} + */ + this.email = ''; + + /** + * The author's IP address. + * @type {string} + */ + this.ipAddress = ipAddress; + + /** + * The author's name. + * @type {string} + */ + this.name = ''; + + /** + * The role of the author. + * If you set it to `"administrator"`, Akismet will always return `false`. + * @type {string} + */ + this.role = ''; + + /** + * The URL of the author's website. + * @type {URL} + */ + this.url = null; + + /** + * The author's user agent, that is the string identifying the Web browser used to submit comments. + * @type {string} + */ + this.userAgent = userAgent; + } + + /** + * Creates a new author from the specified JSON map. + * @param {object} map A JSON map representing an author. + * @return {Author} The instance corresponding to the specified JSON map, or `null` if a parsing error occurred. + */ + static fromJSON(map) { + if (!map || typeof map != 'object') return null; + + let author = new Author(typeof map.user_ip == 'string' ? map.user_ip : ''); + author.email = typeof map.comment_author_email == 'string' ? map.comment_author_email : ''; + author.name = typeof map.comment_author == 'string' ? map.comment_author : ''; + author.role = typeof map.user_role == 'string' ? map.user_role : ''; + author.url = typeof map.comment_author_url == 'string' ? new URL(map.comment_author_url) : null; + author.userAgent = typeof map.user_agent == 'string' ? map.user_agent : ''; + return author; + } + + /** + * Converts this object to a map in JSON format. + * @return {object} The map in JSON format corresponding to this object. + */ + toJSON() { + let map = {}; + if (this.name.length) map.comment_author = this.name; + if (this.email.length) map.comment_author_email = this.email; + if (this.url) map.comment_author_url = this.url.href; + if (this.userAgent.length) map.user_agent = this.userAgent; + if (this.ipAddress.length) map.user_ip = this.ipAddress; + if (this.role.length) map.user_role = this.role; + return map; + } + + /** + * Returns a string representation of this object. + * @return {string} The string representation of this object. + */ + toString() { + return `${this.constructor.name} ${JSON.stringify(this)}`; + } +}; diff --git a/lib/blog.js b/lib/blog.js new file mode 100644 index 00000000..1d6a28ff --- /dev/null +++ b/lib/blog.js @@ -0,0 +1,70 @@ +'use strict'; + +const {URL} = require('url'); + +/** + * Represents the front page or home URL transmitted when making requests. + */ +exports.Blog = class Blog { + + /** + * Initializes a new instance of the class. + * @param {string|URL} [url] The blog or site URL. + * @param {string} [charset] he character encoding for the values included in comments. + * @param {string[]} [languages] The languages in use on the blog or site, in ISO 639-1 format. + */ + constructor(url = null, charset = '', languages = []) { + + /** + * The character encoding for the values included in comments. + * @type {string} + */ + this.charset = charset; + + /** + * The languages in use on the blog or site, in ISO 639-1 format. + * @type {string[]} + */ + this.languages = languages; + + /** + * The blog or site URL. + * @type {URL} + */ + this.url = typeof url == 'string' ? new URL(url) : url; + } + + /** + * Creates a new blog from the specified JSON map. + * @param {object} map A JSON map representing a blog. + * @return {Blog} The instance corresponding to the specified JSON map, or `null` if a parsing error occurred. + */ + static fromJSON(map) { + if (!map || typeof map != 'object') return null; + + let blog = new Blog(typeof map.blog == 'string' ? map.blog : null); + blog.charset = typeof map.blog_charset == 'string' ? map.blog_charset : ''; + blog.languages = typeof map.blog_lang == 'string' ? map.blog_lang.split(',').map(lang => lang.trim()).filter(lang => lang.length) : []; + return blog; + } + + /** + * Converts this object to a map in JSON format. + * @return {object} The map in JSON format corresponding to this object. + */ + toJSON() { + let map = {}; + if (this.url) map.blog = this.url.href; + if (this.charset.length) map.blog_charset = this.charset; + if (this.languages.length) map.blog_lang = this.languages.join(','); + return map; + } + + /** + * Returns a string representation of this object. + * @return {string} The string representation of this object. + */ + toString() { + return `${this.constructor.name} ${JSON.stringify(this)}`; + } +}; diff --git a/lib/client.js b/lib/client.js new file mode 100644 index 00000000..24cf26a3 --- /dev/null +++ b/lib/client.js @@ -0,0 +1,190 @@ +'use strict'; + +const {Observable, Subject} = require('rxjs'); +const superagent = require('superagent'); +const {URL} = require('url'); + +const {Blog} = require('./blog'); +const {version: pkgVersion} = require('../package.json'); + +/** + * Submits comments to the [Akismet](https://akismet.com) service. + */ +exports.Client = class Client { + + /** + * The HTTP header containing the Akismet error messages. + * @type {string} + */ + static get DEBUG_HEADER() { + return 'x-akismet-debug-help'; + } + + /** + * The URL of the default API end point. + * @type {URL} + */ + static get DEFAULT_ENDPOINT() { + return new URL('https://rest.akismet.com'); + } + + /** + * Initializes a new instance of the class. + * @param {string} [apiKey] The Akismet API key. + * @param {Blog|string} [blog] The front page or home URL of the instance making requests. + */ + constructor(apiKey = '', blog = null) { + + /** + * The Akismet API key. + * @type {string} + */ + this.apiKey = apiKey; + + /** + * The front page or home URL of the instance making requests. + * @type {Blog} + */ + this.blog = typeof blog == 'string' ? new Blog(blog) : blog; + + /** + * The URL of the API end point. + * @type {URL} + */ + this.endPoint = Client.DEFAULT_ENDPOINT; + + /** + * Value indicating whether the client operates in test mode. + * You can use it when submitting test queries to Akismet. + * @type {boolean} + */ + this.isTest = false; + + /** + * The user agent string to use when making requests. + * If possible, the user agent string should always have the following format: `Application Name/Version | Plugin Name/Version`. + * @type {string} + */ + this.userAgent = `Node.js/${process.version.substr(1)} | Akismet/${pkgVersion}`; + + /** + * The handler of "request" events. + * @type {Subject} + */ + this._onRequest = new Subject(); + + /** + * The handler of "response" events. + * @type {Subject} + */ + this._onResponse = new Subject(); + } + + /** + * The stream of "request" events. + * @type {Observable} + */ + get onRequest() { + return this._onRequest.asObservable(); + } + + /** + * The stream of "response" events. + * @type {Observable} + */ + get onResponse() { + return this._onResponse.asObservable(); + } + + /** + * Checks the specified comment against the service database, and returns a value indicating whether it is spam. + * @param {Comment} comment The comment to be checked. + * @return {Observable} A boolean value indicating whether it is spam. + */ + checkComment(comment) { + let baseURL = `${this.endPoint.protocol}//${this.apiKey}.${this.endPoint.host}`; + let endPoint = new URL('1.1/comment-check', baseURL); + return this._fetch(endPoint, comment.toJSON()).map(res => res == 'true'); + } + + /** + * Submits the specified comment that was incorrectly marked as spam but should not have been. + * @param {Comment} comment The comment to be submitted. + * @return {Observable} Completes once the comment has been submitted. + */ + submitHam(comment) { + let baseURL = `${this.endPoint.protocol}//${this.apiKey}.${this.endPoint.host}`; + let endPoint = new URL('1.1/submit-ham', baseURL); + return this._fetch(endPoint, comment.toJSON()); + } + + /** + * Submits the specified comment that was not marked as spam but should have been. + * @param {Comment} comment The comment to be submitted. + * @return {Observable} Completes once the comment has been submitted. + */ + submitSpam(comment) { + let baseURL = `${this.endPoint.protocol}//${this.apiKey}.${this.endPoint.host}`; + let endPoint = new URL('1.1/submit-spam', baseURL); + return this._fetch(endPoint, comment.toJSON()); + } + + /** + * Converts this object to a map in JSON format. + * @return {object} The map in JSON format corresponding to this object. + */ + toJSON() { + return { + apiKey: this.apiKey, + blog: this.blog ? this.blog.constructor.name : null, + endPoint: this.endPoint, + isTest: this.isTest, + userAgent: this.userAgent + }; + } + + /** + * Returns a string representation of this object. + * @return {string} The string representation of this object. + */ + toString() { + return `${this.constructor.name} ${JSON.stringify(this)}`; + } + + /** + * Checks the API key against the service database, and returns a value indicating whether it is valid. + * @return {Observable} A boolean value indicating whether it is a valid API key. + */ + verifyKey() { + let endPoint = new URL('1.1/verify-key', this.endPoint); + return this._fetch(endPoint, {key: this.apiKey}).map(res => res == 'valid'); + } + + /** + * Queries the service by posting the specified fields to a given end point, and returns the response as a string. + * @param {URL} endPoint The URL of the end point to query. + * @param {object} fields The fields describing the query body. + * @return {Observable} The response as string. + * @emits {superagent.Request} The "request" event. + * @emits {superagent.Response} The "response" event. + */ + _fetch(endPoint, fields) { + if (!this.apiKey.length || !this.blog) return Observable.throw(new Error('The API key or the blog URL is empty.')); + + let bodyFields = Object.assign(this.blog.toJSON(), fields); + if (this.isTest) bodyFields.is_test = '1'; + + let req = superagent.post(endPoint.href) + .type('form') + .set('User-Agent', this.userAgent) + .send(bodyFields); + + this._onRequest.next(req); + return Observable.from(req).map(res => { + this._onResponse.next(res); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + if (Client.DEBUG_HEADER in res.header) throw new Error(res.header[Client.DEBUG_HEADER]); + return res.text; + }); + } +}; diff --git a/lib/comment.js b/lib/comment.js new file mode 100644 index 00000000..dc9222cb --- /dev/null +++ b/lib/comment.js @@ -0,0 +1,107 @@ +'use strict'; + +const {URL} = require('url'); +const {Author} = require('./author'); + +/** + * Represents a comment submitted by an author. + */ +exports.Comment = class Comment { + + /** + * Initializes a new instance of the class. + * @param {Author} [author] The comment's author. + * @param {string} [content] The comment's content. + * @param {string} [type] The comment's type. + */ + constructor(author = null, content = '', type = '') { + + /** + * The comment's author. + * @type {Author} + */ + this.author = author; + + /** + * The comment's content. + * @type {string} + */ + this.content = content; + + /** + * The UTC timestamp of the creation of the comment. + * @type {Date} + */ + this.date = null; + + /** + * The permanent location of the entry the comment is submitted to. + * @type {URL} + */ + this.permalink = null; + + /** + * The UTC timestamp of the publication time for the post, page or thread on which the comment was posted. + * @type {Date} + */ + this.postModified = null; + + /** + * The URL of the webpage that linked to the entry being requested. + * @type {URL} + */ + this.referrer = null; + + /** + * The comment's type. + * This string value specifies a `CommentType` constant or a made up value like `"registration"`. + * @type {string} + */ + this.type = type; + } + + /** + * Creates a new comment from the specified JSON map. + * @param {object} map A JSON map representing a comment. + * @return {Comment} The instance corresponding to the specified JSON map, or `null` if a parsing error occurred. + */ + static fromJSON(map) { + if (!map || typeof map != 'object') return null; + + let hasAuthor = Object.keys(map) + .filter(key => /^comment_author/.test(key) || /^user/.test(key)) + .length > 0; + + let comment = new Comment(hasAuthor ? Author.fromJSON(map) : null); + comment.content = typeof map.comment_content == 'string' ? map.comment_content : ''; + comment.date = typeof map.comment_date_gmt == 'string' ? new Date(map.comment_date_gmt) : null; + comment.permalink = typeof map.permalink == 'string' ? new URL(map.permalink) : null; + comment.postModified = typeof map.comment_post_modified_gmt == 'string' ? new Date(map.comment_post_modified_gmt) : null; + comment.referrer = typeof map.referrer == 'string' ? new URL(map.referrer) : null; + comment.type = typeof map.comment_type == 'string' ? map.comment_type : ''; + return comment; + } + + /** + * Converts this object to a map in JSON format. + * @return {object} The map in JSON format corresponding to this object. + */ + toJSON() { + let map = this.author ? this.author.toJSON() : {}; + if (this.content.length) map.comment_content = this.content; + if (this.date) map.comment_date_gmt = this.date.toISOString(); + if (this.postModified) map.comment_post_modified_gmt = this.postModified.toISOString(); + if (this.type.length) map.comment_type = this.type; + if (this.permalink) map.permalink = this.permalink.href; + if (this.referrer) map.referrer = this.referrer.href; + return map; + } + + /** + * Returns a string representation of this object. + * @return {string} The string representation of this object. + */ + toString() { + return `${this.constructor.name} ${JSON.stringify(this)}`; + } +}; diff --git a/lib/comment_type.js b/lib/comment_type.js new file mode 100644 index 00000000..275e4c07 --- /dev/null +++ b/lib/comment_type.js @@ -0,0 +1,15 @@ +'use strict'; + +/** + * Specifies the type of a comment. + * @type {object} + * + * @property {string} COMMENT A standard comment. + * @property {string} PINGBACK A [pingback](https://en.wikipedia.org/wiki/Pingback) comment. + * @property {string} TRACKBACK A [trackback](https://en.wikipedia.org/wiki/Trackback) comment. + */ +exports.CommentType = Object.freeze({ + COMMENT: 'comment', + PINGBACK: 'pingback', + TRACKBACK: 'trackback' +}); diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 00000000..e67817f3 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,15 @@ +'use strict'; + +const {Author} = require('./author'); +const {Blog} = require('./blog'); +const {Client} = require('./client'); +const {Comment} = require('./comment'); +const {CommentType} = require('./comment_type'); + +module.exports = { + Author, + Blog, + Client, + Comment, + CommentType +}; diff --git a/package.json b/package.json index 9c12cd40..fcbbc589 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,9 @@ "homepage": "https://github.com/cedx/akismet.js", "license": "Apache-2.0", "main": "./lib/index", - "module": "./src/index", "name": "@cedx/akismet", "repository": "cedx/akismet.js", - "version": "9.0.0", + "version": "9.1.0", "dependencies": { "rxjs": "^5.4.2", "superagent": "^3.5.2" @@ -16,14 +15,12 @@ "devDependencies": { "@cedx/coveralls": "^2.0.0", "@cedx/gulp-david": "^8.0.0", - "babel-preset-env": "^1.6.0", - "babel-register": "^6.24.1", "chai": "^4.1.0", "del": "^3.0.0", "esdoc": "^0.5.2", + "esdoc-node": "^1.0.2", "estraverse": "^4.2.0", "gulp": "^3.9.1", - "gulp-babel": "^6.1.2", "gulp-eslint": "^4.0.0", "mocha": "^3.4.2", "nsp": "^2.6.3", @@ -42,7 +39,6 @@ ], "scripts": { "coverage": "coveralls --file=var/lcov.info", - "prepare": "gulp build", - "test": "nyc --report-dir=var --reporter=lcovonly mocha --compilers=js:babel-register --recursive" + "test": "nyc --report-dir=var --reporter=lcovonly mocha --recursive" } } diff --git a/test/author_test.js b/test/author_test.js index b80d0c23..07f19aaf 100644 --- a/test/author_test.js +++ b/test/author_test.js @@ -1,9 +1,8 @@ 'use strict'; -import {expect} from 'chai'; -import {describe, it} from 'mocha'; -import {URL} from 'url'; -import {Author} from '../src/index'; +const {expect} = require('chai'); +const {URL} = require('url'); +const {Author} = require('../lib'); /** * @test {Author} diff --git a/test/blog_test.js b/test/blog_test.js index 5e578a62..54a13e74 100644 --- a/test/blog_test.js +++ b/test/blog_test.js @@ -1,9 +1,8 @@ 'use strict'; -import {expect} from 'chai'; -import {describe, it} from 'mocha'; -import {URL} from 'url'; -import {Blog} from '../src/index'; +const {expect} = require('chai'); +const {URL} = require('url'); +const {Blog} = require('../lib'); /** * @test {Blog} diff --git a/test/client_test.js b/test/client_test.js index 4c50ef99..7ad78d7b 100644 --- a/test/client_test.js +++ b/test/client_test.js @@ -1,10 +1,9 @@ 'use strict'; -import {expect} from 'chai'; -import {describe, it} from 'mocha'; -import {Observable, Subject} from 'rxjs'; -import {URL} from 'url'; -import {Author, Client, Comment, CommentType} from '../src/index'; +const {expect} = require('chai'); +const {Observable, Subject} = require('rxjs'); +const {URL} = require('url'); +const {Author, Client, Comment, CommentType} = require('../lib'); /** * @test {Client} diff --git a/test/comment_test.js b/test/comment_test.js index f59ef2e0..2b773afd 100644 --- a/test/comment_test.js +++ b/test/comment_test.js @@ -1,9 +1,8 @@ 'use strict'; -import {expect} from 'chai'; -import {describe, it} from 'mocha'; -import {URL} from 'url'; -import {Author, Comment, CommentType} from '../src/index'; +const {expect} = require('chai'); +const {URL} = require('url'); +const {Author, Comment, CommentType} = require('../lib'); /** * @test {Comment}