diff --git a/.eslintrc.js b/.eslintrc.js index fa7e893f..ae095c9c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,10 +1,17 @@ module.exports = { + root: true, env: { es2021: true, node: true, jest: true, }, - extends: ['eslint:recommended', 'prettier'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ], parserOptions: { ecmaVersion: 12, sourceType: 'module', diff --git a/README.md b/README.md index 5bfbc678..9f310f4b 100644 --- a/README.md +++ b/README.md @@ -186,3 +186,43 @@ Favorite an image: ```ts client.favoriteImage('someImageHash'); ``` + +### Get gallery images + +```ts +client.getGallery({ + section: 'hot', + sort: 'viral', + mature: false, +}); +``` + +`getGallery()` accepts an object of type `GalleryOptions`. The follow options are available: + +| Key | Required | Description | +| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `section` | required | `hot` \| `top` \| `user` | +| `sort` | optional | `viral` \| `top` \| `time` \| `rising` (only available with user section). Defaults to viral | +| `page` | optional | `number` - the data paging number | +| `window` | optional | Change the date range of the request if the section is `top`. Accepted values are `day` \| `week` \| `month` \| `year` \| `all`. Defaults to `day` | +| `showViral` | optional | `true` \| `false` - Show or hide viral images from the `user` section. Defaults to `true` | +| `mature` | optional | `true` \| `false` - Show or hide mature (nsfw) images in the response section. Defaults to `false`. NOTE: This parameter is only required if un-authed. The response for authed users will respect their account setting | +| `album_previews` | optional | `true` \| `false` - Include image metadata for gallery posts which are albums | + +### Get subreddit gallery images + +```ts +client.getSubredditGallery({ + subreddit: 'wallstreetbets', + sort: 'time', +}); +``` + +`getSubredditGallery()` accepts an object of type `SubredditGalleryOptions`. The follow options are available: + +| Key | Required | Description | +| ----------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `subreddit` | required | A valid subreddit name | +| `sort` | optional | `time` \| `top` - defaults to time | +| `page` | optional | `number` - the data paging number | +| `window` | optional | Change the date range of the request if the section is `top`. Accepted values are `day` \| `week` \| `month` \| `year` \| `all`. Defaults to `week` | diff --git a/jest.config.js b/jest.config.js index 5c40be22..a121f963 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,8 +1,8 @@ module.exports = { - setupFilesAfterEnv: ['./jest.setup.js'], + setupFilesAfterEnv: ['/src/mocks/jest.setup.ts'], testPathIgnorePatterns: [ '/build/', '/node_modules/', - '/src/__tests__/mocks/', + '/src/mocks/', ], }; diff --git a/package-lock.json b/package-lock.json index f996b63b..c3cd3d60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ "@commitlint/config-conventional": "^12.0.1", "@types/jest": "^26.0.21", "@types/mock-fs": "^4.13.0", + "@typescript-eslint/eslint-plugin": "^4.20.0", + "@typescript-eslint/parser": "^4.20.0", "babel-jest": "^26.6.3", "commitizen": "^4.2.3", "cz-conventional-changelog": "^3.3.0", @@ -3124,6 +3126,12 @@ "integrity": "sha512-14t0v1ICYRtRVcHASzes0v/O+TIeASb8aD55cWF1PidtInhFWSXcmhzhHqGjUWf9SUq1w70cvd1cWKUULubAfQ==", "dev": true }, + "node_modules/@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "dev": true + }, "node_modules/@types/keyv": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", @@ -3224,6 +3232,163 @@ "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.20.0.tgz", + "integrity": "sha512-sw+3HO5aehYqn5w177z2D82ZQlqHCwcKSMboueo7oE4KU9QiC0SAgfS/D4z9xXvpTc8Bt41Raa9fBR8T2tIhoQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "4.20.0", + "@typescript-eslint/scope-manager": "4.20.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "lodash": "^4.17.15", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.20.0.tgz", + "integrity": "sha512-sQNlf6rjLq2yB5lELl3gOE7OuoA/6IVXJUJ+Vs7emrQMva14CkOwyQwD7CW+TkmOJ4Q/YGmoDLmbfFrpGmbKng==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.20.0", + "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/typescript-estree": "4.20.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.20.0.tgz", + "integrity": "sha512-m6vDtgL9EABdjMtKVw5rr6DdeMCH3OA1vFb0dAyuZSa3e5yw1YRzlwFnm9knma9Lz6b2GPvoNSa8vOXrqsaglA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.20.0", + "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/typescript-estree": "4.20.0", + "debug": "^4.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.20.0.tgz", + "integrity": "sha512-/zm6WR6iclD5HhGpcwl/GOYDTzrTHmvf8LLLkwKqqPKG6+KZt/CfSgPCiybshmck66M2L5fWSF/MKNuCwtKQSQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/visitor-keys": "4.20.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.20.0.tgz", + "integrity": "sha512-cYY+1PIjei1nk49JAPnH1VEnu7OYdWRdJhYI5wiKOUMhLTG1qsx5cQxCUTuwWCmQoyriadz3Ni8HZmGSofeC+w==", + "dev": true, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.20.0.tgz", + "integrity": "sha512-Knpp0reOd4ZsyoEJdW8i/sK3mtZ47Ls7ZHvD8WVABNx5Xnn7KhenMTRGegoyMTx6TiXlOVgMz9r0pDgXTEEIHA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/visitor-keys": "4.20.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.20.0.tgz", + "integrity": "sha512-NXKRM3oOVQL8yNFDNCZuieRIwZ5UtjNLYtmMx2PacEAGmbaEYtGgVHUHVyZvU/0rYZcizdrWjDo+WBtRPSgq+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.20.0", + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -17132,6 +17297,21 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -20286,6 +20466,12 @@ "integrity": "sha512-14t0v1ICYRtRVcHASzes0v/O+TIeASb8aD55cWF1PidtInhFWSXcmhzhHqGjUWf9SUq1w70cvd1cWKUULubAfQ==", "dev": true }, + "@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "dev": true + }, "@types/keyv": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", @@ -20385,6 +20571,89 @@ "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.20.0.tgz", + "integrity": "sha512-sw+3HO5aehYqn5w177z2D82ZQlqHCwcKSMboueo7oE4KU9QiC0SAgfS/D4z9xXvpTc8Bt41Raa9fBR8T2tIhoQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.20.0", + "@typescript-eslint/scope-manager": "4.20.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "lodash": "^4.17.15", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.20.0.tgz", + "integrity": "sha512-sQNlf6rjLq2yB5lELl3gOE7OuoA/6IVXJUJ+Vs7emrQMva14CkOwyQwD7CW+TkmOJ4Q/YGmoDLmbfFrpGmbKng==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.20.0", + "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/typescript-estree": "4.20.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.20.0.tgz", + "integrity": "sha512-m6vDtgL9EABdjMtKVw5rr6DdeMCH3OA1vFb0dAyuZSa3e5yw1YRzlwFnm9knma9Lz6b2GPvoNSa8vOXrqsaglA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "4.20.0", + "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/typescript-estree": "4.20.0", + "debug": "^4.1.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.20.0.tgz", + "integrity": "sha512-/zm6WR6iclD5HhGpcwl/GOYDTzrTHmvf8LLLkwKqqPKG6+KZt/CfSgPCiybshmck66M2L5fWSF/MKNuCwtKQSQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/visitor-keys": "4.20.0" + } + }, + "@typescript-eslint/types": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.20.0.tgz", + "integrity": "sha512-cYY+1PIjei1nk49JAPnH1VEnu7OYdWRdJhYI5wiKOUMhLTG1qsx5cQxCUTuwWCmQoyriadz3Ni8HZmGSofeC+w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.20.0.tgz", + "integrity": "sha512-Knpp0reOd4ZsyoEJdW8i/sK3mtZ47Ls7ZHvD8WVABNx5Xnn7KhenMTRGegoyMTx6TiXlOVgMz9r0pDgXTEEIHA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.20.0", + "@typescript-eslint/visitor-keys": "4.20.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.20.0.tgz", + "integrity": "sha512-NXKRM3oOVQL8yNFDNCZuieRIwZ5UtjNLYtmMx2PacEAGmbaEYtGgVHUHVyZvU/0rYZcizdrWjDo+WBtRPSgq+A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.20.0", + "eslint-visitor-keys": "^2.0.0" + } + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -31013,6 +31282,15 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 45ff5565..2147bf69 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,12 @@ "types": "lib/index.d.ts", "scripts": { "test": "jest", - "build": "babel --extensions '.js,.ts' src --out-dir lib --ignore \"src/__tests__/**/*\" && npm run types", + "build": "babel --extensions '.ts' src --out-dir lib && npm run types", "types": "tsc --build tsconfig.json", "typecheck": "tsc --noEmit --emitDeclarationOnly false", "clean": "rm -rf lib", "prepare": "husky install", - "lint": "eslint . && prettier --check . && npm run typecheck", + "lint": "eslint --ignore-path .gitignore . --ext .ts && prettier --check . && npm run typecheck", "commit": "cz" }, "devDependencies": { @@ -39,6 +39,8 @@ "@commitlint/config-conventional": "^12.0.1", "@types/jest": "^26.0.21", "@types/mock-fs": "^4.13.0", + "@typescript-eslint/eslint-plugin": "^4.20.0", + "@typescript-eslint/parser": "^4.20.0", "babel-jest": "^26.6.3", "commitizen": "^4.2.3", "cz-conventional-changelog": "^3.3.0", @@ -54,8 +56,8 @@ "typescript": "^4.2.3" }, "lint-staged": { - "*.js": [ - "eslint --fix", + "*.ts": [ + "eslint --ignore-path .gitignore . --ext .ts --fix", "jest --bail --findRelatedTests" ], "*.{js,css,md,yml,yaml,json}": "prettier --write" diff --git a/src/__tests__/createAlbum.js b/src/__tests__/createAlbum.js deleted file mode 100644 index ce224bcb..00000000 --- a/src/__tests__/createAlbum.js +++ /dev/null @@ -1,13 +0,0 @@ -import imgur from '../imgur.js'; - -beforeAll(() => imgur.setClientId('abc123')); - -test('should resolve new album details', () => { - expect.assertions(1); - return expect(imgur.createAlbum()).resolves.toMatchInlineSnapshot(` - Object { - "deletehash": "KCsF6XvjfqpImI8", - "id": "ybqNtEF", - } - `); -}); diff --git a/src/__tests__/deleteImage.js b/src/__tests__/deleteImage.js deleted file mode 100644 index a6d10cbe..00000000 --- a/src/__tests__/deleteImage.js +++ /dev/null @@ -1,15 +0,0 @@ -import imgur from '../imgur.js'; - -describe('deleteImage()', () => { - describe('delete image response', () => { - test('should fail when id is not passed', () => { - const errMsg = 'Missing delete hash'; - expect(imgur.deleteImage()).rejects.toThrowError(errMsg); - }); - - test('image is successfully deleted', async () => { - const resp = await imgur.deleteImage('JK9ybyj'); - expect(resp).toMatchInlineSnapshot(`true`); - }); - }); -}); diff --git a/src/__tests__/favoriteImage.js b/src/__tests__/favoriteImage.js deleted file mode 100644 index b40f80ab..00000000 --- a/src/__tests__/favoriteImage.js +++ /dev/null @@ -1,16 +0,0 @@ -import imgur from '../imgur.js'; - -describe('favoriteImage()', () => { - describe('favorite image response', () => { - test('should fail with no input', () => { - const errMsg = 'Missing image ID'; - - expect(imgur.favoriteImage()).rejects.toThrowError(errMsg); - }); - - test('should return successful favorite image response', async () => { - const resp = await imgur.favoriteImage('lDrXtHj'); - expect(resp).toMatchInlineSnapshot(`"favorited"`); - }); - }); -}); diff --git a/src/__tests__/getAPIUrl.js b/src/__tests__/getAPIUrl.js deleted file mode 100644 index a5e3b083..00000000 --- a/src/__tests__/getAPIUrl.js +++ /dev/null @@ -1,15 +0,0 @@ -import imgur from '../imgur.js'; - -describe('getAPIUrl()', () => { - test('should return the default API URL, if nothing is set', () => { - const defaultAPIUrl = 'https://api.imgur.com/3/'; - return expect(imgur.getAPIUrl()).toBe(defaultAPIUrl); - }); - - test('should return the same API URL that was set', () => { - const apiUrl = 'https://imgur-apiv3.p.mashape.com/'; - imgur.setAPIUrl(apiUrl); - - return expect(imgur.getAPIUrl()).toBe(apiUrl); - }); -}); diff --git a/src/__tests__/getAuthorizationHeader.js b/src/__tests__/getAuthorizationHeader.js deleted file mode 100644 index 0c4d7d91..00000000 --- a/src/__tests__/getAuthorizationHeader.js +++ /dev/null @@ -1,25 +0,0 @@ -import imgur from '../imgur.js'; - -afterEach(imgur.clearAllCredentials); - -test('returns provided access code in bearer header', async () => { - const accessToken = 'abc123'; - imgur.setAccessToken(accessToken); - const authorizationHeader = await imgur._getAuthorizationHeader(); - expect(authorizationHeader).toBe(`Bearer ${accessToken}`); -}); - -test('returns provided client id in client id header', async () => { - const clientId = 'abc123'; - imgur.setClientId(clientId); - const authorizationHeader = await imgur._getAuthorizationHeader(); - expect(authorizationHeader).toBe(`Client-ID ${clientId}`); -}); - -test('retrieves access token from imgur via provided username/password/clientid', async () => { - imgur.setCredentials('fakeusername', 'fakepassword', 'fakeclientd'); - const authorizationHeader = await imgur._getAuthorizationHeader(); - expect(authorizationHeader).toMatchInlineSnapshot( - `"Bearer 123accesstoken456"` - ); -}); diff --git a/src/__tests__/getClientId.js b/src/__tests__/getClientId.js deleted file mode 100644 index 727c21c1..00000000 --- a/src/__tests__/getClientId.js +++ /dev/null @@ -1,15 +0,0 @@ -import imgur from '../imgur.js'; - -describe('getClientId()', () => { - test('should return the default client id, if nothing is set', () => { - const defaultClientId = 'f0ea04148a54268'; - return expect(imgur.getClientId()).toBe(defaultClientId); - }); - - test('should return the same client that was set', () => { - const clientId = '123456789abcdef'; - imgur.setClientId(clientId); - - return expect(imgur.getClientId()).toBe(clientId); - }); -}); diff --git a/src/__tests__/getGalleryInfo.js b/src/__tests__/getGalleryInfo.js deleted file mode 100644 index c6c62351..00000000 --- a/src/__tests__/getGalleryInfo.js +++ /dev/null @@ -1,52 +0,0 @@ -import imgur from '../imgur.js'; - -beforeAll(() => imgur.setClientId('abc123')); - -describe('getGalleryInfo()', () => { - describe('get gallery info response', () => { - test('should fail when id is not passed', () => { - const errMsg = 'Invalid gallery ID'; - expect(imgur.getGalleryInfo()).rejects.toThrowError(errMsg); - }); - - test('gallery info is returned', async () => { - const resp = await imgur.getGalleryInfo('JK9ybyj'); - expect(resp).toMatchInlineSnapshot(` - Object { - "description": "gallery-description", - "id": "JK9ybyj", - "title": "gallery-title", - } - `); - }); - }); - - describe("delegates to _imgurRequest('gallery', ...)", () => { - const mockResult = { - data: [], - params: { - id: 'JK9ybyj', - }, - }; - const payload = 'JK9ybyj'; - const _imgurRequestBackup = imgur._imgurRequest; - - beforeEach(() => { - imgur._imgurRequest = jest - .fn() - .mockImplementation(() => Promise.resolve(mockResult)); - }); - - afterEach(() => { - imgur._imgurRequest.mockClear(); - imgur._imgurRequest = _imgurRequestBackup; - }); - - it('should delegate', () => { - const promise = imgur.getGalleryInfo('JK9ybyj'); - - expect(imgur._imgurRequest).toHaveBeenCalledWith('gallery', payload); - expect(promise).resolves.toMatchObject(mockResult); - }); - }); -}); diff --git a/src/__tests__/getMashapeKey.js b/src/__tests__/getMashapeKey.js deleted file mode 100644 index e4c28454..00000000 --- a/src/__tests__/getMashapeKey.js +++ /dev/null @@ -1,10 +0,0 @@ -import imgur from '../imgur.js'; - -describe('getMashapeKey()', () => { - test('should return the same client that was set', () => { - const mashapeKey = '123456789abcdef'; - imgur.setMashapeKey(mashapeKey); - - return expect(imgur.getMashapeKey()).toBe(mashapeKey); - }); -}); diff --git a/src/__tests__/imgurRequest.js b/src/__tests__/imgurRequest.js deleted file mode 100644 index 0359724a..00000000 --- a/src/__tests__/imgurRequest.js +++ /dev/null @@ -1,31 +0,0 @@ -import imgur from '../imgur.js'; - -beforeAll(() => imgur.setClientId('abc123')); - -test('should reject with invalid operation', () => { - expect.assertions(1); - return expect(imgur._imgurRequest()).rejects.toMatchInlineSnapshot( - `[Error: Invalid operation]` - ); -}); - -test('should reject with no payload', () => { - expect.assertions(1); - return expect( - imgur._imgurRequest('upload', null) - ).rejects.toMatchInlineSnapshot(`[Error: No payload specified]`); -}); - -test('should resolve with no payload when operation is allowlisted', () => { - expect.assertions(1); - return expect(imgur._imgurRequest('credits', null)).resolves - .toMatchInlineSnapshot(` - Object { - "ClientLimit": 12500, - "ClientRemaining": 12500, - "UserLimit": 500, - "UserRemaining": 500, - "UserReset": 1615614380, - } - `); -}); diff --git a/src/__tests__/mocks/handlers/gallery.js b/src/__tests__/mocks/handlers/gallery.js deleted file mode 100644 index f05cb0f8..00000000 --- a/src/__tests__/mocks/handlers/gallery.js +++ /dev/null @@ -1,13 +0,0 @@ -export function getHandler(req, res, ctx) { - const { id } = req.params; - const response = { - data: { - id, - title: 'gallery-title', - description: 'gallery-description', - }, - success: true, - status: 200, - }; - return res(ctx.json(response)); -} diff --git a/src/__tests__/search.js b/src/__tests__/search.js deleted file mode 100644 index b5972472..00000000 --- a/src/__tests__/search.js +++ /dev/null @@ -1,61 +0,0 @@ -import imgur from '../imgur.js'; - -describe('SEARCH', () => { - describe('search options validations', () => { - test('should fail when query is not passed', () => { - const errMsg = - 'Search requires a query. Try searching with a query (e.g cats).'; - expect(imgur.search()).rejects.toThrowError(errMsg); - }); - - test('should fail when query is passed a boolean', () => { - const errMsg = 'You did not pass a string as a query.'; - expect(imgur.search(true)).rejects.toThrowError(errMsg); - }); - - test('should fail when query is passed a number', () => { - const errMsg = 'You did not pass a string as a query.'; - expect(imgur.search(1)).rejects.toThrowError(errMsg); - }); - - test('should fail when query is passed a number', () => { - const errMsg = 'You did not pass a string as a query.'; - expect(imgur.search(1)).rejects.toThrowError(errMsg); - }); - }); - - describe("delegates to _imgurRequest('search', ...)", () => { - const mockResult = { - data: [], - params: { - page: '1', - dateRange: 'month', - sort: 'viral', - }, - }; - const payload = '/viral/month/1?q=meme'; - const _imgurRequestBackup = imgur._imgurRequest; - - beforeEach(() => { - imgur._imgurRequest = jest - .fn() - .mockImplementation(() => Promise.resolve(mockResult)); - }); - - afterEach(() => { - imgur._imgurRequest.mockClear(); - imgur._imgurRequest = _imgurRequestBackup; - }); - - it('should delegate', () => { - const promise = imgur.search('meme', { - sort: 'viral', - dateRange: 'month', - page: '1', - }); - - expect(imgur._imgurRequest).toHaveBeenCalledWith('search', payload); - expect(promise).resolves.toMatchObject(mockResult); - }); - }); -}); diff --git a/src/__tests__/setAPIUrl.js b/src/__tests__/setAPIUrl.js deleted file mode 100644 index 5dcd3f29..00000000 --- a/src/__tests__/setAPIUrl.js +++ /dev/null @@ -1,32 +0,0 @@ -import imgur from '../imgur.js'; - -describe('setAPIUrl()', () => { - beforeEach(() => { - const defaultImgurAPIUrl = 'https://api.imgur.com/3/'; - imgur.setAPIUrl(defaultImgurAPIUrl); - }); - - test('should return the API Url that was set', () => { - const imgurAPIUrl = 'https://imgur-apiv3.p.mashape.com/'; - imgur.setAPIUrl(imgurAPIUrl); - return expect(imgur.getAPIUrl()).toBe(imgurAPIUrl); - }); - - test('should not set an empty API Url', () => { - const imgurAPIUrl = ''; - imgur.setAPIUrl(imgurAPIUrl); - return expect(imgur.getAPIUrl()).not.toBe(imgurAPIUrl); - }); - - test('should not set a number', () => { - const imgurAPIUrl = 1024; - imgur.setAPIUrl(imgurAPIUrl); - return expect(imgur.getAPIUrl()).not.toBe(imgurAPIUrl); - }); - - test('should not set a boolean', () => { - const imgurAPIUrl = false; - imgur.setAPIUrl(imgurAPIUrl); - return expect(imgur.getAPIUrl()).not.toBe(imgurAPIUrl); - }); -}); diff --git a/src/__tests__/setClientId.js b/src/__tests__/setClientId.js deleted file mode 100644 index b05d2b3b..00000000 --- a/src/__tests__/setClientId.js +++ /dev/null @@ -1,32 +0,0 @@ -import imgur from '../imgur.js'; - -describe('setClientId()', () => { - beforeEach(() => { - const defaultClientId = '0123456789abcdef'; - imgur.setClientId(defaultClientId); - }); - - test('should return the client id that was set', () => { - const clientId = 'lolololol'; - imgur.setClientId(clientId); - return expect(imgur.getClientId()).toBe(clientId); - }); - - test('should not set an empty client id', () => { - const clientId = ''; - imgur.setClientId(clientId); - return expect(imgur.getClientId()).not.toBe(clientId); - }); - - test('should not set a number', () => { - const clientId = 1024; - imgur.setClientId(clientId); - return expect(imgur.getClientId()).not.toBe(clientId); - }); - - test('should not set a boolean', () => { - const clientId = false; - imgur.setClientId(clientId); - return expect(imgur.getClientId()).not.toBe(clientId); - }); -}); diff --git a/src/__tests__/setMashapeKey.js b/src/__tests__/setMashapeKey.js deleted file mode 100644 index e67a8775..00000000 --- a/src/__tests__/setMashapeKey.js +++ /dev/null @@ -1,32 +0,0 @@ -import imgur from '../imgur.js'; - -describe('setMashapeKey()', () => { - beforeEach(() => { - const defaultMashapeKey = '0123456789abcdef'; - imgur.setMashapeKey(defaultMashapeKey); - }); - - test('should return the Mashape Key that was set', () => { - const mashapeKey = '0123456789abcdef'; - imgur.setMashapeKey(mashapeKey); - return expect(imgur.getMashapeKey()).toBe(mashapeKey); - }); - - test('should not set an empty Mashape Key', () => { - const mashapeKey = ''; - imgur.setMashapeKey(mashapeKey); - return expect(imgur.getMashapeKey()).not.toBe(mashapeKey); - }); - - test('should not set a number', () => { - const mashapeKey = 1024; - imgur.setMashapeKey(mashapeKey); - return expect(imgur.getMashapeKey()).not.toBe(mashapeKey); - }); - - test('should not set a boolean', () => { - const mashapeKey = false; - imgur.setMashapeKey(mashapeKey); - return expect(imgur.getMashapeKey()).not.toBe(mashapeKey); - }); -}); diff --git a/src/__tests__/updateInfo.js b/src/__tests__/updateInfo.js deleted file mode 100644 index 6bdff491..00000000 --- a/src/__tests__/updateInfo.js +++ /dev/null @@ -1,72 +0,0 @@ -import imgur from '../imgur.js'; - -describe('updateInfo', () => { - describe('update image metadata', () => { - test('should fail when id is not passed', () => { - const errMsg = 'image id is required'; - expect(imgur.updateInfo()).rejects.toThrowError(errMsg); - }); - - test('should fail when query is passed a boolean', () => { - const errMsg = 'You did not pass a string as an id.'; - expect(imgur.updateInfo(true)).rejects.toThrowError(errMsg); - }); - - test('should fail when id passed is not a string', () => { - const errMsg = 'You did not pass a string as an id.'; - expect(imgur.updateInfo(1)).rejects.toThrowError(errMsg); - }); - - test('update one image and receive response', async () => { - const resp = await imgur.updateInfo( - 'JK9ybyj', - 'new-title', - 'new-description' - ); - expect(resp).toMatchInlineSnapshot(`true`); - }); - }); - - describe("delegates to _imgurRequest('update', ...)", () => { - const mockResult = { - data: [], - params: { - id: 'V8svmob', - title: 'image title', - message: 'image description', - }, - }; - const payload = 'V8svmob'; - const _imgurRequestBackup = imgur._imgurRequest; - - beforeEach(() => { - imgur._imgurRequest = jest - .fn() - .mockImplementation(() => Promise.resolve(mockResult)); - }); - - afterEach(() => { - imgur._imgurRequest.mockClear(); - imgur._imgurRequest = _imgurRequestBackup; - }); - - it('should delegate', () => { - const params = { - title: 'image title', - description: 'image description', - }; - const promise = imgur.updateInfo( - 'V8svmob', - params.title, - params.description - ); - - expect(imgur._imgurRequest).toHaveBeenCalledWith( - 'update', - payload, - params - ); - expect(promise).resolves.toMatchObject(mockResult); - }); - }); -}); diff --git a/src/__tests__/uploadFile.js b/src/__tests__/uploadFile.js deleted file mode 100644 index 294353f0..00000000 --- a/src/__tests__/uploadFile.js +++ /dev/null @@ -1,41 +0,0 @@ -import imgur from '../imgur.js'; - -beforeAll(() => imgur.setClientId('abc123')); - -test('upload one image image and receive response', async () => { - const resp = await imgur.uploadFile('/home/user/meme.jpg'); - expect(resp).toMatchInlineSnapshot(` - Object { - "deletehash": "jyby9KJ", - "description": null, - "id": "JK9ybyj", - "link": "https://i.imgur.com/JK9ybyj.jpg", - "title": null, - } - `); -}); - -test('upload multiple images and receive response', async () => { - const resp = await imgur.uploadFile([ - '/home/user/meme.jpg', - '/home/user/lol.jpg', - ]); - expect(resp).toMatchInlineSnapshot(` - Array [ - Object { - "deletehash": "jyby9KJ", - "description": null, - "id": "JK9ybyj", - "link": "https://i.imgur.com/JK9ybyj.jpg", - "title": null, - }, - Object { - "deletehash": "jyby9KJ", - "description": null, - "id": "JK9ybyj", - "link": "https://i.imgur.com/JK9ybyj.jpg", - "title": null, - }, - ] - `); -}); diff --git a/src/__tests__/uploadUrl.js b/src/__tests__/uploadUrl.js deleted file mode 100644 index 2938fc21..00000000 --- a/src/__tests__/uploadUrl.js +++ /dev/null @@ -1,55 +0,0 @@ -import imgur from '../imgur.js'; - -describe('uploadUrl()', () => { - describe('validation', () => { - test('should fail with no url', () => { - const errMsg = 'Invalid URL'; - - expect(imgur.uploadUrl()).rejects.toThrowError(errMsg); - }); - - test('should fail with on a malformed url', () => { - const errMsg = 'Invalid URL'; - - expect(imgur.uploadUrl('blarg')).rejects.toThrowError(errMsg); - }); - }); - - describe("delegates to _imgurRequest('upload', ...)", () => { - const mockResult = { foo: 'bar' }; - const testUrl = 'https://somewhere/test.png'; - - const _imgurRequestBackup = imgur._imgurRequest; - - beforeEach(() => { - imgur._imgurRequest = jest - .fn() - .mockImplementation(() => Promise.resolve(mockResult)); - }); - - afterEach(() => { - imgur._imgurRequest.mockClear(); - imgur._imgurRequest = _imgurRequestBackup; - }); - - test('should delegate', () => { - const promise = imgur.uploadUrl(testUrl); - - expect(imgur._imgurRequest).toHaveBeenCalledWith('upload', testUrl, { - type: 'url', - }); - expect(promise).resolves.toEqual(mockResult); - }); - - test('should propagate albumId', () => { - const albumId = '123'; - const promise = imgur.uploadUrl(testUrl, albumId); - - expect(imgur._imgurRequest).toHaveBeenCalledWith('upload', testUrl, { - album: albumId, - type: 'url', - }); - expect(promise).resolves.toEqual(mockResult); - }); - }); -}); diff --git a/src/client.ts b/src/client.ts index 34fc6a64..ddf492d7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import got, { ExtendOptions, Got } from 'got'; +import got, { CancelableRequest, ExtendOptions, Response, Got } from 'got'; import { getAuthorizationHeader } from './getAuthorizationHeader'; import { deleteImage, @@ -9,8 +9,20 @@ import { updateImage, UpdateImagePayload, } from './image'; +import { + GalleryOptions, + getGallery, + getSubredditGallery, + SubredditGalleryOptions, +} from './gallery'; import { IMGUR_API_PREFIX } from './common/endpoints'; -import { Credentials, Payload } from './common/types'; +import { + Credentials, + GalleryData, + ImageData, + ImgurApiResponse, + Payload, +} from './common/types'; const USERAGENT = 'imgur/next (https://github.com/kaimallea/node-imgur)'; @@ -38,31 +50,51 @@ export class ImgurClient extends EventEmitter { }); } - plainRequest(url: string, options: ExtendOptions = {}) { + plainRequest( + url: string, + options: ExtendOptions = {} + ): CancelableRequest> { return this.got.extend(options)(url); } - request(url: string, options: ExtendOptions = {}) { + request( + url: string, + options: ExtendOptions = {} + ): CancelableRequest> { return this.gotExtended.extend(options)(url); } - deleteImage(imageHash: string) { + deleteImage(imageHash: string): Promise> { return deleteImage(this, imageHash); } - favoriteImage(imageHash: string) { + favoriteImage(imageHash: string): Promise> { return favoriteImage(this, imageHash); } - getImage(imageHash: string) { + getGallery(options: GalleryOptions): Promise> { + return getGallery(this, options); + } + + getSubredditGallery( + options: SubredditGalleryOptions + ): Promise> { + return getSubredditGallery(this, options); + } + + getImage(imageHash: string): Promise> { return getImage(this, imageHash); } - updateImage(payload: UpdateImagePayload | UpdateImagePayload[]) { + updateImage( + payload: UpdateImagePayload | UpdateImagePayload[] + ): Promise | ImgurApiResponse[]> { return updateImage(this, payload); } - upload(payload: string | string[] | Payload | Payload[]) { + upload( + payload: string | string[] | Payload | Payload[] + ): Promise | ImgurApiResponse[]> { return upload(this, payload); } } diff --git a/src/common/endpoints.ts b/src/common/endpoints.ts index 1a4251fd..789004fc 100644 --- a/src/common/endpoints.ts +++ b/src/common/endpoints.ts @@ -7,3 +7,7 @@ export const AUTHORIZE_ENDPOINT = 'oauth2/authorize'; export const IMAGE_ENDPOINT = `${API_VERSION}/image`; export const UPLOAD_ENDPOINT = `${API_VERSION}/upload`; + +export const GALLERY_ENDPOINT = `${API_VERSION}/gallery`; + +export const SUBREDDIT_GALLERY_ENDPOINT = `${API_VERSION}/gallery/r`; diff --git a/src/common/types.ts b/src/common/types.ts index d910a482..298a1a12 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -13,28 +13,118 @@ export interface Login extends ClientId { export type Credentials = AccessToken | ClientId | Login; -export function isAccessToken(arg: any): arg is AccessToken { - return arg.accessToken !== undefined; +export function isAccessToken(arg: unknown): arg is AccessToken { + return (arg as AccessToken).accessToken !== undefined; } -export function isClientId(arg: any): arg is ClientId { - return arg.clientId !== undefined; +export function isClientId(arg: unknown): arg is ClientId { + return (arg as ClientId).clientId !== undefined; } -export function isLogin(arg: any): arg is Login { +export function isLogin(arg: unknown): arg is Login { return ( - arg.clientId !== undefined && - arg.username !== undefined && - arg.password !== undefined + (arg as Login).clientId !== undefined && + (arg as Login).username !== undefined && + (arg as Login).password !== undefined ); } -export interface ImgurApiResponse { - data: Record | string | boolean; +export interface ImgurApiResponse< + T = Record | Record[] | string | boolean +> { + data: T; status: number; success: boolean; } +interface CommonData { + id: string; + title: string | null; + description: string | null; + datetime: number; + link: string; + + ad_config?: { + safeFlags: string[]; + highRiskFlags: string[]; + unsafeFlags: string[]; + wallUnsafeFlags: string[]; + showsAds: boolean; + }; + ad_type: number; + ad_url: string; + + account_url: string | null; + account_id: string | null; + favorite: boolean; + is_ad: boolean; + is_album: boolean; + in_gallery: boolean; + in_most_viral: boolean; + nsfw: boolean | null; + section: string | null; + tags: Array<{ + name: string; + display_name: string; + followers: number; + total_items: number; + following: boolean; + is_whitelisted: boolean; + background_hash: string; + thumbnail_hash: string | null; + accent: string; + background_is_animated: boolean; + thumbnail_is_animated: boolean; + is_promoted: boolean; + description: string; + logo_hash: string | null; + logo_destination_url: string | null; + description_annotations: Record; + }>; + topic: string | null; + topic_id: string | null; + vote: null; + + comment_count: number | null; + favorite_count: number | null; + ups: number | null; + downs: number | null; + score: number | null; + points: number | null; + views: number; +} +export interface ImageData extends CommonData { + type: string; + width: number; + height: number; + size: number; + deletehash?: string; + bandwidth: number; + animated: boolean; + has_sound: boolean; + edited: string; + mp4_size?: number; + mp4?: string; + gifv?: string; + hls?: string; + looping?: boolean; + processing?: { + status: 'pending' | 'completed'; + }; +} + +export interface AlbumData extends CommonData { + cover: string | null; + cover_width: number | null; + cover_height: number | null; + layout: string; + privacy: string; + include_album_ads: boolean; + images: ImageData[]; + images_count: number; +} + +export type GalleryData = Array; export interface Payload { image?: string; video?: string; diff --git a/src/common/utils.ts b/src/common/utils.ts index 3ff64a2f..71e7f7f1 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -2,27 +2,27 @@ import { createReadStream } from 'fs'; import FormData from 'form-data'; import { Payload } from './types'; -export function isVideo(payload: string | Payload) { +export function isVideo(payload: string | Payload): boolean { if (typeof payload === 'string') { return false; } - return typeof payload.video !== 'undefined' && payload.video; + return typeof payload.video === 'string'; } -export function getSource(payload: string | Payload) { +export function getSource(payload: string | Payload): string { if (typeof payload === 'string') { return payload; } if (isVideo(payload)) { - return payload.video; + return payload.video as string; } else { - return payload.image; + return payload.image as string; } } -export function createForm(payload: string | Payload) { +export function createForm(payload: string | Payload): FormData { const form = new FormData(); if (typeof payload === 'string') { diff --git a/src/gallery/getGallery.test.ts b/src/gallery/getGallery.test.ts new file mode 100644 index 00000000..b3ac267f --- /dev/null +++ b/src/gallery/getGallery.test.ts @@ -0,0 +1,67 @@ +import { ImgurClient } from '../client'; +import { getGallery, GalleryOptions, constructGalleryUrl } from './getGallery'; + +test('constructGalleryUrl', () => { + expect( + constructGalleryUrl({} as GalleryOptions).pathname + ).toMatchInlineSnapshot(`"/3/gallery/hot/viral"`); + + expect( + constructGalleryUrl({ section: 'hot' }).pathname + ).toMatchInlineSnapshot(`"/3/gallery/hot/viral"`); + + expect( + constructGalleryUrl({ section: 'hot', sort: 'top' }).pathname + ).toMatchInlineSnapshot(`"/3/gallery/hot/top"`); + + expect( + constructGalleryUrl({ section: 'top', window: 'day' }).pathname + ).toMatchInlineSnapshot(`"/3/gallery/top/viral/day"`); + + expect( + constructGalleryUrl({ section: 'user', sort: 'rising' }).pathname + ).toMatchInlineSnapshot(`"/3/gallery/user/rising"`); + + const { href, pathname, search } = constructGalleryUrl({ + section: 'user', + sort: 'rising', + showViral: true, + mature: false, + album_previews: true, + }); + expect(pathname).toMatchInlineSnapshot(`"/3/gallery/user/rising"`); + expect(search).toMatchInlineSnapshot( + `"?showViral=true&mature=false&album_previews=true"` + ); + expect(href).toMatchInlineSnapshot( + `"https://api.imgur.com/3/gallery/user/rising?showViral=true&mature=false&album_previews=true"` + ); +}); + +test('returns an image response', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const response = await getGallery(client, { section: 'hot' }); + expect(response).toMatchInlineSnapshot(` + Object { + "data": Array [ + Object { + "description": "gallery-description", + "id": "ans7sd", + "images": Array [ + Object { + "description": null, + "id": "4yMKKLTz", + "link": "https://i.imgur.com/4yMKKLTz.jpg", + "title": null, + }, + ], + "link": "https://imgur.com/a/abc123", + "title": "gallery-title", + }, + ], + "status": 200, + "success": true, + } + `); +}); diff --git a/src/gallery/getGallery.ts b/src/gallery/getGallery.ts new file mode 100644 index 00000000..afecfbe7 --- /dev/null +++ b/src/gallery/getGallery.ts @@ -0,0 +1,87 @@ +import { ImgurClient } from '../client'; +import { GALLERY_ENDPOINT, IMGUR_API_PREFIX } from '../common/endpoints'; +import { ImgurApiResponse, GalleryData } from '../common/types'; + +export type CommonSectionProps = { + sort?: 'viral' | 'top' | 'time'; + page?: number; +}; + +export type HotSection = CommonSectionProps & { + section: 'hot'; +}; + +export type TopSection = CommonSectionProps & { + section: 'top'; + window?: 'day' | 'week' | 'month' | 'year' | 'all'; +}; + +export type UserSection = Omit & { + section: 'user'; + sort?: 'viral' | 'top' | 'time' | 'rising'; +}; + +export type SectionOptions = HotSection | TopSection | UserSection; + +export type PresentationOptions = { + showViral?: boolean; + mature?: boolean; + album_previews?: boolean; +}; + +export type GalleryOptions = SectionOptions & PresentationOptions; + +const defaultOptions: GalleryOptions = { + section: 'hot', + sort: 'viral', +}; + +export function constructGalleryUrl(options: GalleryOptions): URL { + const mergedOptions = Object.assign({}, defaultOptions, options); + + let uri = `${mergedOptions.section}`; + + if (mergedOptions.sort) { + uri += `/${mergedOptions.sort}`; + } + + if (mergedOptions.section === 'top' && mergedOptions.window) { + uri += `/${mergedOptions.window}`; + } + + if (mergedOptions.page) { + uri += `/${mergedOptions.page}`; + } + + const url = new URL(`${IMGUR_API_PREFIX}/${GALLERY_ENDPOINT}/${uri}`); + + if (mergedOptions.showViral !== undefined) { + url.searchParams.append('showViral', mergedOptions.showViral.toString()); + } + + if (mergedOptions.mature !== undefined) { + url.searchParams.append('mature', mergedOptions.mature.toString()); + } + + if (mergedOptions.album_previews !== undefined) { + url.searchParams.append( + 'album_previews', + mergedOptions.album_previews.toString() + ); + } + + return url; +} + +export async function getGallery( + client: ImgurClient, + options: GalleryOptions = defaultOptions +): Promise> { + const { pathname } = constructGalleryUrl(options); + // since we're using prefixUrl with got, we have to remove the starting slash or it'll throw + const finalPathname = pathname.slice(1); + + return (await client + .request(finalPathname) + .json()) as ImgurApiResponse; +} diff --git a/src/gallery/getSubredditGallery.test.ts b/src/gallery/getSubredditGallery.test.ts new file mode 100644 index 00000000..d34e67d3 --- /dev/null +++ b/src/gallery/getSubredditGallery.test.ts @@ -0,0 +1,69 @@ +import { ImgurClient } from '../client'; +import { + getSubredditGallery, + constructSubredditGalleryUrl, +} from './getSubredditGallery'; + +test('constructGalleryUrl', () => { + expect( + constructSubredditGalleryUrl({ + subreddit: 'wallstreetbets', + }).pathname + ).toMatchInlineSnapshot(`"/3/gallery/r/wallstreetbets"`); + + expect( + constructSubredditGalleryUrl({ subreddit: 'wallstreetbets', sort: 'time' }) + .pathname + ).toMatchInlineSnapshot(`"/3/gallery/r/wallstreetbets/time"`); + + expect( + constructSubredditGalleryUrl({ + subreddit: 'wallstreetbets', + sort: 'top', + window: 'day', + }).pathname + ).toMatchInlineSnapshot(`"/3/gallery/r/wallstreetbets/top/day"`); + + const { href, pathname } = constructSubredditGalleryUrl({ + subreddit: 'wallstreetbets', + sort: 'top', + window: 'day', + page: 2, + }); + expect(pathname).toMatchInlineSnapshot( + `"/3/gallery/r/wallstreetbets/top/day/2"` + ); + expect(href).toMatchInlineSnapshot( + `"https://api.imgur.com/3/gallery/r/wallstreetbets/top/day/2"` + ); +}); + +test('returns an image response', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const response = await getSubredditGallery(client, { + subreddit: 'wallstreetbets', + }); + expect(response).toMatchInlineSnapshot(` + Object { + "data": Array [ + Object { + "description": "gallery-description", + "id": "ans7sd", + "images": Array [ + Object { + "description": null, + "id": "4yMKKLTz", + "link": "https://i.imgur.com/4yMKKLTz.jpg", + "title": null, + }, + ], + "link": "https://imgur.com/a/abc123", + "title": "gallery-title", + }, + ], + "status": 200, + "success": true, + } + `); +}); diff --git a/src/gallery/getSubredditGallery.ts b/src/gallery/getSubredditGallery.ts new file mode 100644 index 00000000..ec1f8f56 --- /dev/null +++ b/src/gallery/getSubredditGallery.ts @@ -0,0 +1,56 @@ +import { ImgurClient } from '../client'; +import { + SUBREDDIT_GALLERY_ENDPOINT, + IMGUR_API_PREFIX, +} from '../common/endpoints'; +import { ImgurApiResponse, GalleryData } from '../common/types'; + +export type TimeOptions = { + subreddit: string; + sort?: 'time'; + page?: number; +}; + +export type TopOptions = Omit & { + sort?: 'top'; + window?: 'day' | 'week' | 'month' | 'year' | 'all'; +}; + +export type SubredditGalleryOptions = TimeOptions | TopOptions; + +export function constructSubredditGalleryUrl( + options: SubredditGalleryOptions +): URL { + let uri = `${options.subreddit}`; + + if (options.sort) { + uri += `/${options.sort}`; + } + + if (options.sort === 'top' && options.window) { + uri += `/${options.window}`; + } + + if (options.page) { + uri += `/${options.page}`; + } + + const url = new URL( + `${IMGUR_API_PREFIX}/${SUBREDDIT_GALLERY_ENDPOINT}/${uri}` + ); + + return url; +} + +export async function getSubredditGallery( + client: ImgurClient, + options: SubredditGalleryOptions +): Promise> { + const { pathname } = constructSubredditGalleryUrl(options); + // since we're using prefixUrl with got, we have to remove the starting slash or it'll throw + const finalPathname = pathname.slice(1); + + return (await client + .request(finalPathname) + .json()) as ImgurApiResponse; +} diff --git a/src/gallery/index.ts b/src/gallery/index.ts new file mode 100644 index 00000000..caa264f4 --- /dev/null +++ b/src/gallery/index.ts @@ -0,0 +1,2 @@ +export * from './getGallery'; +export * from './getSubredditGallery'; diff --git a/src/getAuthorizationHeader.ts b/src/getAuthorizationHeader.ts index d77ffce6..70bc9747 100644 --- a/src/getAuthorizationHeader.ts +++ b/src/getAuthorizationHeader.ts @@ -1,8 +1,15 @@ -import { isAccessToken, isClientId, isLogin } from './common/types'; +import { + AccessToken, + isAccessToken, + isClientId, + isLogin, +} from './common/types'; import { ImgurClient } from './client'; import { IMGUR_API_PREFIX, AUTHORIZE_ENDPOINT } from './common/endpoints'; -export async function getAuthorizationHeader(client: ImgurClient) { +export async function getAuthorizationHeader( + client: ImgurClient +): Promise { if (isAccessToken(client.credentials)) { return `Bearer ${client.credentials.accessToken}`; } @@ -13,7 +20,7 @@ export async function getAuthorizationHeader(client: ImgurClient) { const { clientId, username, password } = client.credentials; - const options: Record = { + const options: Record = { prefixUrl: IMGUR_API_PREFIX, searchParams: { client_id: clientId, @@ -67,6 +74,6 @@ export async function getAuthorizationHeader(client: ImgurClient) { ); const accessToken = token.access_token; - (client.credentials as any).accessToken = accessToken; + ((client.credentials as unknown) as AccessToken).accessToken = accessToken; return `Bearer ${accessToken}`; } diff --git a/src/image/deleteImage.ts b/src/image/deleteImage.ts index 0db8fc4d..e7f89cd9 100644 --- a/src/image/deleteImage.ts +++ b/src/image/deleteImage.ts @@ -1,15 +1,13 @@ import { ImgurClient } from '../client'; import { IMAGE_ENDPOINT } from '../common/endpoints'; +import { ImgurApiResponse } from '../common/types'; -export interface DeleteResponse { - data: true; - success: true; - status: 200; -} - -export async function deleteImage(client: ImgurClient, imageHash: string) { +export async function deleteImage( + client: ImgurClient, + imageHash: string +): Promise> { const url = `${IMAGE_ENDPOINT}/${imageHash}`; return (await client .request(url, { method: 'DELETE' }) - .json()) as DeleteResponse; + .json()) as ImgurApiResponse; } diff --git a/src/image/favoriteImage.ts b/src/image/favoriteImage.ts index 4f3f2a43..18b63f2d 100644 --- a/src/image/favoriteImage.ts +++ b/src/image/favoriteImage.ts @@ -1,15 +1,13 @@ import { ImgurClient } from '../client'; import { IMAGE_ENDPOINT } from '../common/endpoints'; +import { ImgurApiResponse } from '../common/types'; -type FavoriteResponse = { - data: 'favorited'; - success: true; - status: 200; -}; - -export async function favoriteImage(client: ImgurClient, imageHash: string) { +export async function favoriteImage( + client: ImgurClient, + imageHash: string +): Promise> { const url = `${IMAGE_ENDPOINT}/${imageHash}/favorite`; return (await client .request(url, { method: 'POST' }) - .json()) as FavoriteResponse; + .json()) as ImgurApiResponse<'favorited'>; } diff --git a/src/image/getImage.ts b/src/image/getImage.ts index 2df09dae..8f44b855 100644 --- a/src/image/getImage.ts +++ b/src/image/getImage.ts @@ -1,47 +1,11 @@ import { ImgurClient } from '../client'; import { IMAGE_ENDPOINT } from '../common/endpoints'; +import { ImgurApiResponse, ImageData } from '../common/types'; -export interface ImageResponse { - data?: { - id?: string; - title?: string | null; - description?: string | null; - datetime?: number; - type?: string; - animated?: boolean; - width?: number; - height?: number; - size?: number; - views?: number; - bandwidth?: number; - vote?: boolean | null; - favorite?: boolean; - nsfw?: boolean; - section?: string | null; - account_url?: string | null; - account_id?: string | null; - is_ad?: boolean; - in_most_viral?: boolean; - has_sound?: boolean; - tags?: string[]; - ad_type?: number; - ad_url?: string; - edited?: string; - in_gallery?: string; - link?: string; - ad_config?: { - safeFlags?: string[]; - highRiskFlags?: string[]; - unsafeFlags?: string[]; - wallUnsafeFlags?: string[]; - showsAds?: boolean; - }; - }; - success?: boolean; - status?: number; -} - -export async function getImage(client: ImgurClient, imageHash: string) { +export async function getImage( + client: ImgurClient, + imageHash: string +): Promise> { const url = `${IMAGE_ENDPOINT}/${imageHash}`; - return (await client.request(url).json()) as ImageResponse; + return (await client.request(url).json()) as ImgurApiResponse; } diff --git a/src/image/updateImage.ts b/src/image/updateImage.ts index 8ddb8297..d94694fa 100644 --- a/src/image/updateImage.ts +++ b/src/image/updateImage.ts @@ -1,7 +1,7 @@ import { ImgurClient } from '../client'; import { IMAGE_ENDPOINT } from '../common/endpoints'; import { createForm } from '../common/utils'; -import { Payload } from '../common/types'; +import { Payload, ImgurApiResponse } from '../common/types'; export interface UpdateImagePayload extends Pick { @@ -15,7 +15,7 @@ function isValidUpdatePayload(p: UpdateImagePayload) { export async function updateImage( client: ImgurClient, payload: UpdateImagePayload | UpdateImagePayload[] -) { +): Promise | ImgurApiResponse[]> { if (Array.isArray(payload)) { const promises = payload.map((p: UpdateImagePayload) => { if (!isValidUpdatePayload(p)) { @@ -24,11 +24,11 @@ export async function updateImage( const url = `${IMAGE_ENDPOINT}/${p.imageHash}`; const form = createForm(p); - return client.request(url, { + return (client.request(url, { method: 'POST', body: form, resolveBodyOnly: true, - }); + }) as unknown) as Promise>; }); return await Promise.all(promises); @@ -40,9 +40,9 @@ export async function updateImage( const url = `${IMAGE_ENDPOINT}/${payload.imageHash}`; const form = createForm(payload); - return await client.request(url, { + return ((await client.request(url, { method: 'POST', body: form, resolveBodyOnly: true, - }); + })) as unknown) as ImgurApiResponse; } diff --git a/src/image/upload.ts b/src/image/upload.ts index ebcbfdbe..fd0ea744 100644 --- a/src/image/upload.ts +++ b/src/image/upload.ts @@ -1,14 +1,13 @@ import { ImgurClient } from '../client'; import { createForm, getSource } from '../common/utils'; -import { Payload } from '../common/types'; +import { Payload, ImgurApiResponse, ImageData } from '../common/types'; import { UPLOAD_ENDPOINT } from '../common/endpoints'; - import { Progress } from 'got'; export async function upload( client: ImgurClient, payload: string | string[] | Payload | Payload[] -) { +): Promise | ImgurApiResponse[]> { if (Array.isArray(payload)) { const promises = payload.map((p: string | Payload) => { const form = createForm(p); @@ -23,7 +22,7 @@ export async function upload( client.emit('uploadProgress', { ...progress, id }); }); - return req; + return (req as unknown) as Promise>; }); return await Promise.all(promises); } @@ -40,5 +39,5 @@ export async function upload( client.emit('uploadProgress', { ...progress, id }); }); - return await req; + return ((await req) as unknown) as ImgurApiResponse; } diff --git a/src/imgur.js b/src/imgur.js deleted file mode 100644 index 3d0f0587..00000000 --- a/src/imgur.js +++ /dev/null @@ -1,667 +0,0 @@ -import got from 'got'; -import util from 'util'; -import fs from 'fs'; -import FormData from 'form-data'; -import { version as VERSION } from '../package.json'; -const readFile = util.promisify(fs.readFile); -const writeFile = util.promisify(fs.writeFile); - -const imgur = {}; - -// The following client ID is tied to the -// registered 'node-imgur' app and is available -// here for public, anonymous usage via this node -// module only. -const defaultClientId = 'f0ea04148a54268'; -let imgurClientId = process.env.IMGUR_CLIENT_ID || defaultClientId; -let imgurApiUrl = process.env.IMGUR_API_URL || 'https://api.imgur.com/3/'; -let imgurMashapeKey = process.env.IMGUR_MASHAPE_KEY; -let imgurUsername = null; -let imgurPassword = null; -let imgurAccessToken = null; - -// An IIFE that returns the OS-specific home directory -// as a location to optionally store the imgur client id -const DEFAULT_CLIENT_ID_PATH = (() => { - const envHome = process.platform === 'win32' ? 'USERPROFILE' : 'HOME'; - return process.env[envHome] + '/.imgur'; -})(); - -imgur.VERSION = VERSION; - -/** - * Send a request to imgur's public API - * - * @param {string} operation - operation to perform; 'info' or 'upload' - * @param {mixed} payload - image data - * @returns {promise} - */ -imgur._imgurRequest = async (operation, payload, extraFormParams) => { - const form = new FormData(); - const options = { - url: imgurApiUrl, - method: null, - encoding: 'utf8', - }; - const noPayloadRequired = ['credits', 'search', 'createAlbum']; - let response = null; - - if (!operation || typeof operation !== 'string') { - throw new Error('Invalid operation'); - } - - if (!payload) { - if (!noPayloadRequired.includes(operation)) { - throw new Error('No payload specified'); - } - } - - switch (operation) { - case 'upload': - options.method = 'POST'; - options.url += 'upload'; - break; - case 'credits': - options.method = 'GET'; - options.url += 'credits'; - break; - case 'info': - options.method = 'GET'; - options.url += 'image/' + payload; - break; - case 'update': - options.method = 'POST'; - options.url += 'image/' + payload; - break; - case 'album': - options.method = 'GET'; - options.url += 'album/' + payload; - break; - case 'createAlbum': - options.method = 'POST'; - options.url += 'album'; - break; - case 'delete': - options.method = 'DELETE'; - options.url += 'image/' + payload; - break; - case 'gallery': - options.method = 'GET'; - options.url += 'gallery/' + payload; - break; - case 'search': - options.method = 'GET'; - options.url += 'gallery/search/' + payload; - break; - case 'favorite': - options.method = 'POST'; - options.url += 'image/' + payload + '/favorite'; - break; - default: - throw new Error('Invalid operation'); - } - - const authorizationHeader = await imgur._getAuthorizationHeader(); - - if (imgurMashapeKey) { - options.headers = { - Authorization: authorizationHeader, - 'X-Mashape-Key': imgurMashapeKey, - }; - } else { - options.headers = { - Authorization: authorizationHeader, - }; - } - - if (typeof extraFormParams === 'object') { - if (operation === 'upload') { - form.append('image', payload); - } - - for (let param in extraFormParams) { - form.append(param, extraFormParams[param]); - } - - options.body = form; - } - - response = await imgur._request(options); - const { statusCode: status, statusMessage: message } = response; - if (status !== 200) { - throw new Error({ - status, - message, - }); - } else { - return JSON.parse(response.body).data; - } -}; - -/** - * Make a request, abstracting away the underlying logic - * - * @param {object} options - * @returns {promise} - */ -imgur._request = async (options) => await got(options); - -/** - * Get imgur access token using credentials - * - * @returns {promise} - */ -imgur._getAuthorizationHeader = async () => { - if (imgurAccessToken) { - return `Bearer ${imgurAccessToken}`; - } - - if (!(imgurUsername && imgurPassword)) { - return `Client-ID ${imgurClientId}`; - } - - const options = { - url: 'https://api.imgur.com/oauth2/authorize', - method: 'GET', - encoding: 'utf8', - searchParams: { - client_id: imgurClientId, - response_type: 'token', - }, - }; - - let response; - - response = await imgur._request(options); - - const cookies = Array.isArray(response.headers['set-cookie']) - ? response.headers['set-cookie'][0] - : response.headers['set-cookie']; - const authorize_token = cookies.match('(^|;)[s]*authorize_token=([^;]*)')[2]; - - options.method = 'POST'; - options.form = { - username: imgurUsername, - password: imgurPassword, - allow: authorize_token, - }; - options.followRedirect = false; - options.headers = { - cookie: 'authorize_token=' + authorize_token, - }; - - response = await imgur._request(options); - const location = response.headers.location; - const token = JSON.parse( - '{"' + - decodeURI(location.slice(location.indexOf('#') + 1)) - .replace(/"/g, '\\"') - .replace(/&/g, '","') - .replace(/=/g, '":"') + - '"}' - ); - imgurAccessToken = token.access_token; - return `Bearer ${imgurAccessToken}`; -}; - -/** - * Set your Authorization if authenticating separately - * @link https://api.imgur.com/#register - * @param {string} accessToken - */ -imgur.setAccessToken = function (accessToken) { - if (accessToken && typeof accessToken === 'string') { - imgurAccessToken = accessToken; - } -}; - -/** - * Set your credentials - * @link https://api.imgur.com/#register - * @param {string} username - * @param {string} password - * @param {string} clientId - */ -imgur.setCredentials = (username, password, clientId) => { - if (clientId && typeof clientId === 'string') { - imgurClientId = clientId; - } - if (username && typeof username === 'string') { - imgurUsername = username; - } - if (password && typeof password === 'string') { - imgurPassword = password; - } -}; - -/** - * Attempt to load the client ID from disk - * @deprecated -- since 1.0.0 -- will be removed in 2.0 along with cli - * @param {string} path - path to file with client id - * @returns {promise} - */ -imgur.loadClientId = async (path) => { - path = path || DEFAULT_CLIENT_ID_PATH; - - let data = null; - try { - data = await readFile(path, { encoding: 'utf-8' }); - } catch (e) { - throw new Error(e.message); - } - - if (!data) { - throw new Error('File is empty'); - } - - return data; -}; - -/** - * Attempt to save the client ID to disk - * @deprecated -- since 1.0.0 -- will be removed in 2.0 along with cli - * @param {string} path - path to save the client id to - * @returns {promise} - */ -imgur.saveClientId = async (clientId, path) => { - path = path || DEFAULT_CLIENT_ID_PATH; - - try { - await writeFile(path, clientId); - } catch (e) { - throw new Error(e.message); - } -}; - -/** - * Attempt to remove a saved client ID from disk - * NOTE: File remains but is emptied - * - * @deprecated -- since 1.0.0 -- will be removed in 2.0 along with cli - * @param {string} path - path to save the client id to - * @returns {promise} - */ -imgur.clearClientId = (path) => imgur.saveClientId('', path); - -/** - * Set your client ID - * @link https://api.imgur.com/#register - * @param {string} clientId - */ -imgur.setClientId = (clientId) => { - if (clientId && typeof clientId === 'string') { - imgurClientId = clientId; - } -}; - -/** - * Get currently set client ID - * @returns {string} client ID - */ -imgur.getClientId = () => imgurClientId; - -/** - * Set Imgur API URL - * @link https://api.imgur.com/#register or https://imgur-apiv3.p.mashape.com - * @param {string} url - URL to make the API calls to imgur - */ -imgur.setAPIUrl = (url) => { - if (url && typeof url === 'string') { - imgurApiUrl = url; - } -}; - -/** - * Get Imgur API Url - * @returns {string} API Url - */ -imgur.getAPIUrl = () => imgurApiUrl; - -/** - * Set Mashape Key - * @link https://market.mashape.com/imgur/imgur-9 - * @param {string} mashapeKey - */ -imgur.setMashapeKey = (mashapeKey) => { - if (mashapeKey && typeof mashapeKey === 'string') { - imgurMashapeKey = mashapeKey; - } -}; - -/** - * Get Mashape Key - * @returns {string} Mashape Key - */ -imgur.getMashapeKey = () => { - return imgurMashapeKey; -}; - -/** - * Delete image - * @param {string} deleteHash - deletehash of the image generated during upload - * @returns {promise} - */ -imgur.deleteImage = async (deleteHash) => { - if (!deleteHash) { - throw new Error('Missing delete hash'); - } - - return await imgur._imgurRequest('delete', deleteHash); -}; - -/** - * Favorite image - * @param {string} id - the id of the image to favorite - * @returns {promise} - */ -imgur.favoriteImage = async (id) => { - if (!id) { - throw new Error('Missing image ID'); - } - - return await imgur._imgurRequest('favorite', id); -}; - -/** - * Get gallery metadata - * @param {string} id - unique gallery id - * @returns {promise} - */ -imgur.getGalleryInfo = async (id) => { - if (!id) { - throw new Error('Invalid gallery ID'); - } - - return await imgur._imgurRequest('gallery', id); -}; - -/** - * Get image metadata - * @param {string} id - unique image id - * @returns {promise} - */ -imgur.getInfo = async (id) => { - if (!id) { - throw new Error('Invalid image ID'); - } - - return await imgur._imgurRequest('info', id); -}; - -/** - * Create an album - * @returns {promise} - */ -imgur.createAlbum = async () => { - return await imgur._imgurRequest('createAlbum'); -}; - -/** - * Get album metadata - * @param {string} id - unique album id - * @returns {promise} - */ -imgur.getAlbumInfo = async (id) => { - if (!id) { - throw new Error('Invalid album ID'); - } - - return await imgur._imgurRequest('album', id); -}; - -/** - * Update image metadata - * @param {string} id - unique image id - * @param {string} title - the title field - * @param {string} description - the description field - * @returns {promise} - */ -imgur.updateInfo = async (id, title, description) => { - const extraFormParams = {}; - - if (!id) { - throw new Error('image id is required'); - } else if (typeof id !== 'string') { - throw new Error('You did not pass a string as an id.'); - } - - if (typeof title === 'string' && title.length) { - extraFormParams.title = title; - } - - if (typeof description === 'string' && description.length) { - extraFormParams.description = description; - } - - return await imgur._imgurRequest('update', id, extraFormParams); -}; - -imgur.search = async (query, options) => { - const checkQuery = imgur.checkQuery(query); - let params; - options = options || {}; - if (checkQuery.constructor === Error) { - throw new Error(checkQuery); - } else { - params = imgur.initSearchParams(query, options); - const queryStr = params.queryStr; - delete params.queryStr; - - const json = await imgur._imgurRequest('search', queryStr); - return { ...json, params }; - } -}; - -imgur.checkQuery = (query) => { - let errMsg; - if (!query) { - errMsg = new Error( - 'Search requires a query. Try searching with a query (e.g cats).' - ); - } else if (typeof query != 'string') { - errMsg = new Error('You did not pass a string as a query.'); - } else { - errMsg = ''; - } - return errMsg; -}; - -imgur.initSearchParams = (query, options) => { - const params = { sort: 'time', dateRange: 'all', page: '1' }; - - for (const key in options) { - if (key == 'sort' || key == 'dateRange' || key == 'page') { - params[key] = params[key] != options[key] ? options[key] : params[key]; - } - } - - let queryStr = ''; - Object.keys(params).forEach((param) => { - queryStr += '/' + params[param]; - }); - queryStr += '?q=' + query; - params['queryStr'] = queryStr; - return params; -}; - -/** - * Upload an image file or multiple image files concurrently - * @param {string|[]string} path - path to a binary image file - * @param {string=} albumId - the album id to upload to - * @param {string=} title - the title of the image - * @param {string=} description - the description of the image - * @returns {promise} - */ -imgur.uploadFile = async (path, albumId, title, description) => { - const extraFormParams = {}; - - if (!path) { - throw new Error('No file(s) to upload'); - } - - if (typeof albumId === 'string' && albumId.length) { - extraFormParams.album = albumId; - } - - if (typeof title === 'string' && title.length) { - extraFormParams.title = title; - } - - if (typeof description === 'string' && description.length) { - extraFormParams.description = description; - } - - if (Array.isArray(path)) { - const promises = path.map((f) => - imgur._imgurRequest('upload', fs.createReadStream(f), extraFormParams) - ); - - return await Promise.all(promises); - } else { - return await imgur._imgurRequest( - 'upload', - fs.createReadStream(path), - extraFormParams - ); - } -}; - -/** - * Upload a url - * @param {string} url - address to an image on the web - * @param {string=} albumId - the album id to upload to - * @param {string=} title - the title of the image - * @param {string=} description - the description of the image - * @returns {promise} - */ -imgur.uploadUrl = async (url, albumId, title, description) => { - const extraFormParams = { type: 'url' }; - - if (typeof url === 'object') { - extraFormParams.title = url.title; - extraFormParams.description = url.description; - extraFormParams.album = url.albumId; - url = url.url; - } - - if (typeof albumId === 'string' && albumId.length) { - extraFormParams.album = albumId; - } - - if (typeof title === 'string' && title.length) { - extraFormParams.title = title; - } - - if (typeof description === 'string' && description.length) { - extraFormParams.description = description; - } - - if (!url) { - throw new Error('Invalid URL'); - } - - try { - new URL(url); - } catch (e) { - throw new Error('Invalid URL'); - } - - return await imgur._imgurRequest('upload', url, extraFormParams); -}; - -/** - * Upload a Base64-encoded string - * @link http://en.wikipedia.org/wiki/Base64 - * @param {string} base64 - a base-64 encoded string - * @param {string=} albumId - the album id to upload to - * @param {string=} title - the title of the image - * @param {string=} description - the description of the image - * @returns {promise} - on resolve, returns the resulting image object from imgur - */ -imgur.uploadBase64 = async (base64, albumId, title, description) => { - const extraFormParams = {}; - - if (typeof albumId === 'string' && albumId.length) { - extraFormParams.album = albumId; - } - - if (typeof title === 'string' && title.length) { - extraFormParams.title = title; - } - - if (typeof description === 'string' && description.length) { - extraFormParams.description = description; - } - - if (typeof base64 !== 'string' || !base64 || !base64.length) { - throw new Error('Invalid Base64 input'); - } - - return await imgur._imgurRequest('upload', base64, extraFormParams); -}; - -/** - * Upload an entire album of images - * @deprecated -- since 1.0.0 -- instead use imgur.createAlbum().then({ id } => imgur.uploadFile([...], id, ...)) - * @param {Array} images - array of image strings of desired type - * @param {string} uploadType - the type of the upload ('File', 'Url', 'Base64') - * @param {boolean=} failSafe - if true, it won't fail on invalid or empty image input and will return an object with empty album data and an empty image array - * @returns {promise} - on resolve, returns an object with the album data and and an array of image data objects {data: {...}, images: [{...}, ...]} - */ -imgur.uploadAlbum = async (images, uploadType, failSafe) => { - if ( - !images || - !images.length || - !(typeof images === 'string' || images instanceof Array) - ) { - if (failSafe) { - return { data: {}, images: [] }; - } else { - throw new Error('Invalid image input, only arrays supported'); - } - } - - const album = await imgur.createAlbum(); - const imageArr = await imgur.uploadImages(images, uploadType, album.id); - return { data: album, images: imageArr }; -}; - -/** - * Upload an entire album of images - * @deprecated -- since 1.0.0 -- use uploadFiles([...]) for files. array support to be added to uploadUrl and uploadBase64 - * @param {Array} images - array of image strings of desired type - * @param {string} uploadType - the type of the upload ('File', 'Url', 'Base64') - * @param {string=} albumId - the album id to upload to - * @returns {promise} - on resolve, returns an array of image data objects {album: {...}, images: [{...}, ...]} - */ -imgur.uploadImages = async (images, uploadType, albumId) => { - const upload = imgur['upload' + uploadType]; - - if ( - !images || - !images.length || - !(typeof images === 'string' || images instanceof Array) - ) { - throw new Error('Invalid image input, only arrays supported'); - } - - const promises = images.map((img) => upload(img, albumId)); - return await Promise.all(promises); -}; - -/** - * Get current credit limits - * @returns {promise} - */ -imgur.getCredits = async () => { - return await imgur._imgurRequest('credits'); -}; - -imgur.clearAllCredentials = () => { - imgurAccessToken = null; - imgurUsername = null; - imgurPassword = null; - imgurClientId = defaultClientId; -}; - -export default imgur; diff --git a/src/__tests__/mocks/handlers/album.js b/src/mocks/handlers/album.ts similarity index 83% rename from src/__tests__/mocks/handlers/album.js rename to src/mocks/handlers/album.ts index 6aa168e0..56641347 100644 --- a/src/__tests__/mocks/handlers/album.js +++ b/src/mocks/handlers/album.ts @@ -1,3 +1,5 @@ +import { Handler } from './'; + const AuthenticationRequiredResponse = { data: { error: 'Authentication required', @@ -17,10 +19,10 @@ const SuccessResponse = { status: 200, }; -export function postHandler(req, res, ctx) { +export const postHandler: Handler = (req, res, ctx) => { if (!req.headers.has('authorization')) { return res(ctx.status(401), ctx.json(AuthenticationRequiredResponse)); } return res(ctx.json(SuccessResponse)); -} +}; diff --git a/src/__tests__/mocks/handlers/authorize.js b/src/mocks/handlers/authorize.ts similarity index 86% rename from src/__tests__/mocks/handlers/authorize.js rename to src/mocks/handlers/authorize.ts index c3b5cb4b..15b50445 100644 --- a/src/__tests__/mocks/handlers/authorize.js +++ b/src/mocks/handlers/authorize.ts @@ -1,4 +1,6 @@ -const RequiredFieldErrorResponse = (method) => { +import { Handler } from './'; + +const RequiredFieldErrorResponse = (method: string) => { return { data: { error: 'client_id and response_type are required', @@ -20,11 +22,11 @@ const UnauthorizedErrorResponse = { status: 403, }; -function createRedirectUrl(username) { +function createRedirectUrl(username: string) { return `https://somedomain.com#access_token=123accesstoken456&expires_in=315360000&token_type=bearer&refresh_token=123refrestoken456&account_username=${username}&account_id=123456`; } -export function postHandler(req, res, ctx) { +export const postHandler: Handler = (req, res, ctx) => { const clientId = req.url.searchParams.get('client_id'); const responseType = req.url.searchParams.get('response_type'); @@ -33,7 +35,7 @@ export function postHandler(req, res, ctx) { } const { username, password, allow } = Object.fromEntries( - new URLSearchParams(req.body) + new URLSearchParams(req.body as string) ); if (!(username && password && allow)) { @@ -45,9 +47,9 @@ export function postHandler(req, res, ctx) { ctx.set('Location', createRedirectUrl(username)), ctx.cookie('authorize_token', allow) ); -} +}; -export function getHandler(req, res, ctx) { +export const getHandler: Handler = (req, res, ctx) => { const clientId = req.url.searchParams.get('client_id'); const responseType = req.url.searchParams.get('response_type'); @@ -75,4 +77,4 @@ export function getHandler(req, res, ctx) { ctx.status(200), ctx.body(html) ); -} +}; diff --git a/src/__tests__/mocks/handlers/credits.js b/src/mocks/handlers/credits.ts similarity index 85% rename from src/__tests__/mocks/handlers/credits.js rename to src/mocks/handlers/credits.ts index 4e83529c..f1f7350e 100644 --- a/src/__tests__/mocks/handlers/credits.js +++ b/src/mocks/handlers/credits.ts @@ -1,3 +1,5 @@ +import { Handler } from './'; + const AuthenticationRequiredResponse = { data: { error: 'Authentication required', @@ -20,10 +22,10 @@ const SuccessResponse = { status: 200, }; -export function getHandler(req, res, ctx) { +export const getHandler: Handler = (req, res, ctx) => { if (!req.headers.has('authorization')) { return res(ctx.status(401), ctx.json(AuthenticationRequiredResponse)); } return res(ctx.json(SuccessResponse)); -} +}; diff --git a/src/mocks/handlers/gallery.ts b/src/mocks/handlers/gallery.ts new file mode 100644 index 00000000..8e2adb02 --- /dev/null +++ b/src/mocks/handlers/gallery.ts @@ -0,0 +1,26 @@ +import { Handler } from './'; + +export const getHandler: Handler = (_req, res, ctx) => { + // const { section, sort, window, page } = req.params; + const response = { + data: [ + { + id: 'ans7sd', + title: 'gallery-title', + description: 'gallery-description', + link: 'https://imgur.com/a/abc123', + images: [ + { + id: '4yMKKLTz', + title: null, + description: null, + link: 'https://i.imgur.com/4yMKKLTz.jpg', + }, + ], + }, + ], + success: true, + status: 200, + }; + return res(ctx.json(response)); +}; diff --git a/src/__tests__/mocks/handlers/image.js b/src/mocks/handlers/image.ts similarity index 64% rename from src/__tests__/mocks/handlers/image.js rename to src/mocks/handlers/image.ts index f0c33fe2..f33c3717 100644 --- a/src/__tests__/mocks/handlers/image.js +++ b/src/mocks/handlers/image.ts @@ -1,3 +1,5 @@ +import { Handler } from './'; + const SuccessResponse = { data: true, success: true, @@ -10,7 +12,7 @@ const FavoriteSuccessResponse = { status: 200, }; -export function getHandler(req, res, ctx) { +export const getHandler: Handler = (req, res, ctx) => { const { id } = req.params; const response = { data: { @@ -22,16 +24,16 @@ export function getHandler(req, res, ctx) { status: 200, }; return res(ctx.json(response)); -} +}; -export function postHandler(_req, res, ctx) { +export const postHandler: Handler = (_req, res, ctx) => { return res(ctx.json(SuccessResponse)); -} +}; -export function deleteHandler(req, res, ctx) { +export const deleteHandler: Handler = (_req, res, ctx) => { return res(ctx.json(SuccessResponse)); -} +}; -export function postFavoriteHandler(req, res, ctx) { +export const postFavoriteHandler: Handler = (_req, res, ctx) => { return res(ctx.json(FavoriteSuccessResponse)); -} +}; diff --git a/src/__tests__/mocks/handlers/index.js b/src/mocks/handlers/index.ts similarity index 83% rename from src/__tests__/mocks/handlers/index.js rename to src/mocks/handlers/index.ts index 012296bd..68b9633d 100644 --- a/src/__tests__/mocks/handlers/index.js +++ b/src/mocks/handlers/index.ts @@ -1,4 +1,6 @@ import { rest } from 'msw'; +import { RestRequest, ResponseResolver, RestContext } from 'msw'; + import * as upload from './upload'; import * as authorize from './authorize'; import * as image from './image'; @@ -6,12 +8,14 @@ import * as gallery from './gallery'; import * as credits from './credits'; import * as album from './album'; +export type Handler = ResponseResolver; + export const handlers = [ //upload rest.post('https://api.imgur.com/3/upload', upload.postHandler), // gallery - rest.get('https://api.imgur.com/3/gallery/:id', gallery.getHandler), + rest.get('https://api.imgur.com/3/gallery/*', gallery.getHandler), // image rest.get('https://api.imgur.com/3/image/:id', image.getHandler), diff --git a/src/__tests__/mocks/handlers/upload.js b/src/mocks/handlers/upload.ts similarity index 74% rename from src/__tests__/mocks/handlers/upload.js rename to src/mocks/handlers/upload.ts index 15c31462..9e32db6a 100644 --- a/src/__tests__/mocks/handlers/upload.js +++ b/src/mocks/handlers/upload.ts @@ -1,3 +1,5 @@ +import { Handler } from './'; + const BadRequestErrorResponse = { status: 400, success: false, @@ -8,12 +10,19 @@ const BadRequestErrorResponse = { }, }; +type CreateResponseOptions = { + id?: string; + type?: string | null; + title?: string | null; + description?: string | null; +}; + function createResponse({ id = 'JK9ybyj', type = null, title = null, description = null, -}) { +}: CreateResponseOptions) { return { data: { id, @@ -27,14 +36,14 @@ function createResponse({ }; } -export function postHandler(req, res, ctx) { +export const postHandler: Handler = (req, res, ctx) => { const { image = null, video = null, type = null, title = null, description = null, - } = req.body; + } = req.body as Record; // image or video field is always required if (image !== null && video !== null) { @@ -45,7 +54,7 @@ export function postHandler(req, res, ctx) { // for any other type if (type !== null) { // only these types are allowed - if (!['file', 'url', 'base64'].includes(type)) { + if (!['file', 'url', 'base64'].includes(type as string)) { return res(ctx.status(400), ctx.json(BadRequestErrorResponse)); } // if type is not specified we assume we're uploading a file. @@ -54,5 +63,5 @@ export function postHandler(req, res, ctx) { return res(ctx.status(400), ctx.json(BadRequestErrorResponse)); } - return res(ctx.json(createResponse({ image, video, title, description }))); -} + return res(ctx.json(createResponse({ title, description }))); +}; diff --git a/jest.setup.js b/src/mocks/jest.setup.ts similarity index 91% rename from jest.setup.js rename to src/mocks/jest.setup.ts index f223b416..320607cf 100644 --- a/jest.setup.js +++ b/src/mocks/jest.setup.ts @@ -1,4 +1,4 @@ -import { server } from './src/__tests__/mocks/server.js'; +import { server } from './server'; import mockfs from 'mock-fs'; // Establish API mocking before all tests. diff --git a/src/__tests__/mocks/server.js b/src/mocks/server.ts similarity index 100% rename from src/__tests__/mocks/server.js rename to src/mocks/server.ts diff --git a/tsconfig.json b/tsconfig.json index 9a51c3d9..517ee30c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,8 @@ { "include": ["src/**/*"], - "exclude": ["src/__tests__/**/*"], "compilerOptions": { "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, - "allowJs": true /* Allow javascript files to be compiled. */, - // "checkJs": true /* Report errors in .js files. */, "declaration": true /* Generates corresponding '.d.ts' file. */, "emitDeclarationOnly": true, "outDir": "lib" /* Redirect output structure to the directory. */,