diff --git a/README.md b/README.md index ea16f78a..5bfbc678 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ If you don't have any credentials, you'll need to: 1. [Create an Imgur account](https://imgur.com/register) 1. [Register an application](https://api.imgur.com/#registerapp) +### **⚠️ For brevity, the rest of the examples will leave out the import and/or instantiation step.** + ### Upload one or more images and videos You can upload one or more files by simply passing a path to a file or array of paths to multiple files. @@ -118,8 +120,6 @@ Acceptable key/values match what [the Imgur API expects](https://apidocs.imgur.c Instances of `ImgurClient` emit `uploadProgress` events so that you can track progress with event listeners. ```ts -import { ImgurClient } from 'imgur'; - const client = new ImgurClient({ accessToken: process.env.ACCESS_TOKEN }); client.on('uploadProgress', (progress) => console.log(progress)); @@ -143,3 +143,46 @@ The progress object looks like the following: | `transferred` | total number of bytes transferred thus far | | `total` | total number of bytes to be transferred | | `id` | unique id for the media being transferred; useful when uploading multiple things concurrently | + +### Delete an image + +Requires an image hash or delete hash, which are obtained in an image upload response + +```ts +client.delete('someImageHash'); +``` + +### Update image information + +Update the title and/or description of an image + +```ts +client.updateImage({ + imageHash: 'someImageHash', + title: 'A new title', + description: 'A new description', +}); +``` + +Update multiple images at once: + +```ts +client.updateImage([ + { + imageHash: 'someImageHash', + title: 'A new title', + description: 'A new description', + }, + { + imageHash: 'anotherImageHash', + title: 'A better title', + description: 'A better description', + }, +]); +``` + +Favorite an image: + +```ts +client.favoriteImage('someImageHash'); +``` diff --git a/src/client.ts b/src/client.ts index ee5c0794..34fc6a64 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,14 +1,16 @@ import { EventEmitter } from 'events'; import got, { ExtendOptions, Got } from 'got'; -import { getAuthorizationHeader, Credentials } from './helpers'; -import { getImage, upload, Payload } from './image'; -import { IMGUR_API_PREFIX } from './helpers'; - -type ImgurApiResponse = { - data: Record; - status: number; - success: boolean; -}; +import { getAuthorizationHeader } from './getAuthorizationHeader'; +import { + deleteImage, + favoriteImage, + getImage, + upload, + updateImage, + UpdateImagePayload, +} from './image'; +import { IMGUR_API_PREFIX } from './common/endpoints'; +import { Credentials, Payload } from './common/types'; const USERAGENT = 'imgur/next (https://github.com/kaimallea/node-imgur)'; @@ -44,11 +46,23 @@ export class ImgurClient extends EventEmitter { return this.gotExtended.extend(options)(url); } - async getImage(imageHash: string) { + deleteImage(imageHash: string) { + return deleteImage(this, imageHash); + } + + favoriteImage(imageHash: string) { + return favoriteImage(this, imageHash); + } + + getImage(imageHash: string) { return getImage(this, imageHash); } - async upload(payload: string | string[] | Payload | Payload[]) { + updateImage(payload: UpdateImagePayload | UpdateImagePayload[]) { + return updateImage(this, payload); + } + + upload(payload: string | string[] | Payload | Payload[]) { return upload(this, payload); } } diff --git a/src/helpers/endpoints.ts b/src/common/endpoints.ts similarity index 100% rename from src/helpers/endpoints.ts rename to src/common/endpoints.ts diff --git a/src/common/types.ts b/src/common/types.ts new file mode 100644 index 00000000..d910a482 --- /dev/null +++ b/src/common/types.ts @@ -0,0 +1,47 @@ +export interface AccessToken { + accessToken: string; +} + +export interface ClientId { + clientId: string; +} + +export interface Login extends ClientId { + username: string; + password: string; +} + +export type Credentials = AccessToken | ClientId | Login; + +export function isAccessToken(arg: any): arg is AccessToken { + return arg.accessToken !== undefined; +} + +export function isClientId(arg: any): arg is ClientId { + return arg.clientId !== undefined; +} + +export function isLogin(arg: any): arg is Login { + return ( + arg.clientId !== undefined && + arg.username !== undefined && + arg.password !== undefined + ); +} + +export interface ImgurApiResponse { + data: Record | string | boolean; + status: number; + success: boolean; +} + +export interface Payload { + image?: string; + video?: string; + type?: 'file' | 'url' | 'base64'; + name?: string; + title?: string; + description?: string; + album?: string; + disable_audio?: '1' | '0'; +} diff --git a/src/common/utils.ts b/src/common/utils.ts new file mode 100644 index 00000000..3ff64a2f --- /dev/null +++ b/src/common/utils.ts @@ -0,0 +1,42 @@ +import { createReadStream } from 'fs'; +import FormData from 'form-data'; +import { Payload } from './types'; + +export function isVideo(payload: string | Payload) { + if (typeof payload === 'string') { + return false; + } + + return typeof payload.video !== 'undefined' && payload.video; +} + +export function getSource(payload: string | Payload) { + if (typeof payload === 'string') { + return payload; + } + + if (isVideo(payload)) { + return payload.video; + } else { + return payload.image; + } +} + +export function createForm(payload: string | Payload) { + const form = new FormData(); + + if (typeof payload === 'string') { + form.append('image', createReadStream(payload)); + return form; + } + + for (const [key, value] of Object.entries(payload)) { + if (key === 'image' || key === 'video') { + if (!payload.type || payload.type === 'file') + form.append(key, createReadStream(value)); + } else { + form.append(key, value); + } + } + return form; +} diff --git a/src/helpers/getAuthorizationHeader.test.ts b/src/getAuthorizationHeader.test.ts similarity index 96% rename from src/helpers/getAuthorizationHeader.test.ts rename to src/getAuthorizationHeader.test.ts index 77e916bb..fba4cd23 100644 --- a/src/helpers/getAuthorizationHeader.test.ts +++ b/src/getAuthorizationHeader.test.ts @@ -1,4 +1,4 @@ -import { ImgurClient } from '../client'; +import { ImgurClient } from './client'; import { getAuthorizationHeader } from './getAuthorizationHeader'; test('returns provided access code in bearer header', async () => { diff --git a/src/helpers/getAuthorizationHeader.ts b/src/getAuthorizationHeader.ts similarity index 90% rename from src/helpers/getAuthorizationHeader.ts rename to src/getAuthorizationHeader.ts index 3e8b9fc7..d77ffce6 100644 --- a/src/helpers/getAuthorizationHeader.ts +++ b/src/getAuthorizationHeader.ts @@ -1,6 +1,6 @@ -import { isAccessToken, isClientId, isLogin } from './credentials'; -import { ImgurClient } from '../client'; -import { IMGUR_API_PREFIX, AUTHORIZE_ENDPOINT } from '../helpers'; +import { 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) { if (isAccessToken(client.credentials)) { diff --git a/src/helpers/credentials.ts b/src/helpers/credentials.ts deleted file mode 100644 index 7acca632..00000000 --- a/src/helpers/credentials.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type AccessToken = { - accessToken: string; -}; - -export type ClientId = { - clientId: string; -}; - -export type Login = ClientId & { - username: string; - password: string; -}; - -export type Credentials = AccessToken | ClientId | Login; - -export function isAccessToken(arg: any): arg is AccessToken { - return arg.accessToken !== undefined; -} - -export function isLogin(arg: any): arg is Login { - return arg.username !== undefined && arg.password !== undefined; -} - -export function isClientId(arg: any): arg is ClientId { - return arg.clientId !== undefined; -} diff --git a/src/helpers/index.ts b/src/helpers/index.ts deleted file mode 100644 index dc08a9f3..00000000 --- a/src/helpers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './getAuthorizationHeader'; -export * from './credentials'; -export * from './endpoints'; diff --git a/src/image/deleteImage.test.ts b/src/image/deleteImage.test.ts new file mode 100644 index 00000000..6bf43183 --- /dev/null +++ b/src/image/deleteImage.test.ts @@ -0,0 +1,15 @@ +import { ImgurClient } from '../client'; +import { deleteImage } from './deleteImage'; + +test('delete works successfully', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const response = await deleteImage(client, 'CEddrgP'); + expect(response).toMatchInlineSnapshot(` + Object { + "data": true, + "status": 200, + "success": true, + } + `); +}); diff --git a/src/image/deleteImage.ts b/src/image/deleteImage.ts new file mode 100644 index 00000000..0db8fc4d --- /dev/null +++ b/src/image/deleteImage.ts @@ -0,0 +1,15 @@ +import { ImgurClient } from '../client'; +import { IMAGE_ENDPOINT } from '../common/endpoints'; + +export interface DeleteResponse { + data: true; + success: true; + status: 200; +} + +export async function deleteImage(client: ImgurClient, imageHash: string) { + const url = `${IMAGE_ENDPOINT}/${imageHash}`; + return (await client + .request(url, { method: 'DELETE' }) + .json()) as DeleteResponse; +} diff --git a/src/image/favoriteImage.test.ts b/src/image/favoriteImage.test.ts new file mode 100644 index 00000000..bdc3911f --- /dev/null +++ b/src/image/favoriteImage.test.ts @@ -0,0 +1,15 @@ +import { ImgurClient } from '../client'; +import { favoriteImage } from './favoriteImage'; + +test('favorite works successfully', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const response = await favoriteImage(client, 'CEddrgP'); + expect(response).toMatchInlineSnapshot(` + Object { + "data": "favorited", + "status": 200, + "success": true, + } + `); +}); diff --git a/src/image/favoriteImage.ts b/src/image/favoriteImage.ts new file mode 100644 index 00000000..4f3f2a43 --- /dev/null +++ b/src/image/favoriteImage.ts @@ -0,0 +1,15 @@ +import { ImgurClient } from '../client'; +import { IMAGE_ENDPOINT } from '../common/endpoints'; + +type FavoriteResponse = { + data: 'favorited'; + success: true; + status: 200; +}; + +export async function favoriteImage(client: ImgurClient, imageHash: string) { + const url = `${IMAGE_ENDPOINT}/${imageHash}/favorite`; + return (await client + .request(url, { method: 'POST' }) + .json()) as FavoriteResponse; +} diff --git a/src/image/getImage.ts b/src/image/getImage.ts index 4579a636..2df09dae 100644 --- a/src/image/getImage.ts +++ b/src/image/getImage.ts @@ -1,7 +1,7 @@ import { ImgurClient } from '../client'; -import { IMAGE_ENDPOINT } from '../helpers'; +import { IMAGE_ENDPOINT } from '../common/endpoints'; -type ImageResponse = { +export interface ImageResponse { data?: { id?: string; title?: string | null; @@ -39,12 +39,9 @@ type ImageResponse = { }; success?: boolean; status?: number; -}; +} -export async function getImage( - client: ImgurClient, - imageHash: string -): Promise { +export async function getImage(client: ImgurClient, imageHash: string) { const url = `${IMAGE_ENDPOINT}/${imageHash}`; return (await client.request(url).json()) as ImageResponse; } diff --git a/src/image/index.ts b/src/image/index.ts index 00140c15..d0a9a330 100644 --- a/src/image/index.ts +++ b/src/image/index.ts @@ -1,2 +1,5 @@ +export * from './deleteImage'; +export * from './favoriteImage'; export * from './getImage'; +export * from './updateImage'; export * from './upload'; diff --git a/src/image/updateImage.test.ts b/src/image/updateImage.test.ts new file mode 100644 index 00000000..150bc44a --- /dev/null +++ b/src/image/updateImage.test.ts @@ -0,0 +1,77 @@ +import { ImgurClient } from '../client'; +import { updateImage } from './updateImage'; + +test('update one image with all props', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const response = await updateImage(client, { + imageHash: 'abc123', + title: 'new title', + description: 'description', + }); + expect(response).toMatchInlineSnapshot(` + Object { + "data": true, + "status": 200, + "success": true, + } + `); +}); + +test('update one image with title only', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const response = await updateImage(client, { + imageHash: 'abc123', + title: 'new title', + }); + expect(response).toMatchInlineSnapshot(` + Object { + "data": true, + "status": 200, + "success": true, + } + `); +}); + +test('update one image without title or description', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const response = updateImage(client, { + imageHash: 'abc123', + }); + expect(response).rejects.toThrowErrorMatchingInlineSnapshot( + `"Update requires a title and/or description"` + ); +}); + +test('update multiple images, receive multiple response', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const response = await updateImage(client, [ + { + imageHash: 'meme123', + title: 'dank meme', + description: 'the dankiest of dank memes', + }, + { + imageHash: 'lol123', + title: 'this is funny', + description: '🤣', + }, + ]); + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "data": true, + "status": 200, + "success": true, + }, + Object { + "data": true, + "status": 200, + "success": true, + }, + ] + `); +}); diff --git a/src/image/updateImage.ts b/src/image/updateImage.ts new file mode 100644 index 00000000..8ddb8297 --- /dev/null +++ b/src/image/updateImage.ts @@ -0,0 +1,48 @@ +import { ImgurClient } from '../client'; +import { IMAGE_ENDPOINT } from '../common/endpoints'; +import { createForm } from '../common/utils'; +import { Payload } from '../common/types'; + +export interface UpdateImagePayload + extends Pick { + imageHash: string; +} + +function isValidUpdatePayload(p: UpdateImagePayload) { + return typeof p.title === 'string' || typeof p.description === 'string'; +} + +export async function updateImage( + client: ImgurClient, + payload: UpdateImagePayload | UpdateImagePayload[] +) { + if (Array.isArray(payload)) { + const promises = payload.map((p: UpdateImagePayload) => { + if (!isValidUpdatePayload(p)) { + throw new Error('Update requires a title and/or description'); + } + + const url = `${IMAGE_ENDPOINT}/${p.imageHash}`; + const form = createForm(p); + return client.request(url, { + method: 'POST', + body: form, + resolveBodyOnly: true, + }); + }); + + return await Promise.all(promises); + } + + if (!isValidUpdatePayload(payload)) { + throw new Error('Update requires a title and/or description'); + } + + const url = `${IMAGE_ENDPOINT}/${payload.imageHash}`; + const form = createForm(payload); + return await client.request(url, { + method: 'POST', + body: form, + resolveBodyOnly: true, + }); +} diff --git a/src/image/upload.test.ts b/src/image/upload.test.ts index 2166c54f..33a2ffa5 100644 --- a/src/image/upload.test.ts +++ b/src/image/upload.test.ts @@ -153,7 +153,7 @@ describe('test file uploads', () => { const eventHandler = jest.fn(); client.on('uploadProgress', eventHandler); - const response = await upload(client, { + await upload(client, { video, title: 'trailer for my new stream', description: 'yolo', diff --git a/src/image/upload.ts b/src/image/upload.ts index cd815141..ebcbfdbe 100644 --- a/src/image/upload.ts +++ b/src/image/upload.ts @@ -1,58 +1,9 @@ import { ImgurClient } from '../client'; -import { UPLOAD_ENDPOINT } from '../helpers'; -import { createReadStream } from 'fs'; -import FormData from 'form-data'; -import { Progress } from 'got/dist/source'; +import { createForm, getSource } from '../common/utils'; +import { Payload } from '../common/types'; +import { UPLOAD_ENDPOINT } from '../common/endpoints'; -export interface Payload { - image?: string; - video?: string; - type?: 'file' | 'url' | 'base64'; - name?: string; - title?: string; - description?: string; - album?: string; - disable_audio?: '1' | '0'; -} - -function isVideo(payload: string | Payload) { - if (typeof payload === 'string') { - return false; - } - - return typeof payload.video !== 'undefined' && payload.video; -} - -function getSource(payload: string | Payload) { - if (typeof payload === 'string') { - return payload; - } - - if (isVideo(payload)) { - return payload.video; - } else { - return payload.image; - } -} - -function createForm(payload: string | Payload) { - const form = new FormData(); - - if (typeof payload === 'string') { - form.append('image', createReadStream(payload)); - return form; - } - - for (const [key, value] of Object.entries(payload)) { - if (key === 'image' || key === 'video') { - if (!payload.type || payload.type === 'file') - form.append(key, createReadStream(value)); - } else { - form.append(key, value); - } - } - return form; -} +import { Progress } from 'got'; export async function upload( client: ImgurClient,