diff --git a/src/__tests__/mocks/handlers/image.js b/src/__tests__/mocks/handlers/image.js index cf617467..f0c33fe2 100644 --- a/src/__tests__/mocks/handlers/image.js +++ b/src/__tests__/mocks/handlers/image.js @@ -10,6 +10,20 @@ const FavoriteSuccessResponse = { status: 200, }; +export function getHandler(req, res, ctx) { + const { id } = req.params; + const response = { + data: { + id, + title: 'image-title', + description: 'image-description', + }, + success: true, + status: 200, + }; + return res(ctx.json(response)); +} + export function postHandler(_req, res, ctx) { return res(ctx.json(SuccessResponse)); } diff --git a/src/__tests__/mocks/handlers/index.js b/src/__tests__/mocks/handlers/index.js index d85bf053..012296bd 100644 --- a/src/__tests__/mocks/handlers/index.js +++ b/src/__tests__/mocks/handlers/index.js @@ -14,6 +14,7 @@ export const handlers = [ rest.get('https://api.imgur.com/3/gallery/:id', gallery.getHandler), // image + rest.get('https://api.imgur.com/3/image/:id', image.getHandler), rest.post('https://api.imgur.com/3/image/:id', image.postHandler), rest.post( 'https://api.imgur.com/3/image/:id/favorite', diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 00000000..ec5908b6 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,31 @@ +import { EventEmitter } from 'events'; +import got, { Options } from 'got'; +import { getAuthorizationHeader, Credentials } from './helpers'; + +export class ImgurClient extends EventEmitter { + constructor(readonly credentials: Credentials) { + super(); + } + + async request(options: Options): Promise { + try { + return await got(options); + } catch (err) { + throw new Error(err); + } + } + + async authorizedRequest(options: Options): Promise { + try { + const authorization = await getAuthorizationHeader(this); + const mergedOptions = got.mergeOptions(options, { + headers: { authorization }, + responseType: 'json', + resolveBodyOnly: true, + }); + return await this.request(mergedOptions); + } catch (err) { + throw new Error(err.message); + } + } +} diff --git a/src/helpers/credentials.ts b/src/helpers/credentials.ts new file mode 100644 index 00000000..7acca632 --- /dev/null +++ b/src/helpers/credentials.ts @@ -0,0 +1,26 @@ +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/endpoints.ts b/src/helpers/endpoints.ts new file mode 100644 index 00000000..7663dcfa --- /dev/null +++ b/src/helpers/endpoints.ts @@ -0,0 +1,7 @@ +const HOST = 'https://api.imgur.com'; +const API_VERSION = '3'; +const API_BASE = `${HOST}/${API_VERSION}`; + +export const AUTHORIZE_ENDPOINT = `${HOST}/oauth2/authorize`; + +export const IMAGE_ENDPOINT = `${API_BASE}/image`; diff --git a/src/helpers/getAuthorizationHeader.test.ts b/src/helpers/getAuthorizationHeader.test.ts new file mode 100644 index 00000000..77e916bb --- /dev/null +++ b/src/helpers/getAuthorizationHeader.test.ts @@ -0,0 +1,28 @@ +import { ImgurClient } from '../client'; +import { getAuthorizationHeader } from './getAuthorizationHeader'; + +test('returns provided access code in bearer header', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const authorizationHeader = await getAuthorizationHeader(client); + expect(authorizationHeader).toBe(`Bearer ${accessToken}`); +}); + +test('returns provided client id in client id header', async () => { + const clientId = 'abc123'; + const client = new ImgurClient({ clientId }); + const authorizationHeader = await getAuthorizationHeader(client); + expect(authorizationHeader).toBe(`Client-ID ${clientId}`); +}); + +test('retrieves access token from imgur via provided username/password/clientid', async () => { + const client = new ImgurClient({ + username: 'fakeusername', + password: 'fakepassword', + clientId: 'fakeclientd', + }); + const authorizationHeader = await getAuthorizationHeader(client); + expect(authorizationHeader).toMatchInlineSnapshot( + `"Bearer 123accesstoken456"` + ); +}); diff --git a/src/helpers/getAuthorizationHeader.ts b/src/helpers/getAuthorizationHeader.ts new file mode 100644 index 00000000..f7e98f8b --- /dev/null +++ b/src/helpers/getAuthorizationHeader.ts @@ -0,0 +1,57 @@ +import { isAccessToken, isClientId, isLogin } from './credentials'; +import { ImgurClient } from '../client'; +import { AUTHORIZE_ENDPOINT } from '../helpers'; + +export async function getAuthorizationHeader(client: ImgurClient) { + if (isAccessToken(client.credentials)) { + return `Bearer ${client.credentials.accessToken}`; + } + + if (isClientId(client.credentials) && !isLogin(client.credentials)) { + return `Client-ID ${client.credentials.clientId}`; + } + + const { clientId, username, password } = client.credentials; + + const options: Record = { + url: AUTHORIZE_ENDPOINT, + searchParams: { + client_id: clientId, + response_type: 'token', + }, + }; + + let response = await client.request(options); + + const cookies = Array.isArray(response.headers['set-cookie']) + ? response.headers['set-cookie'][0] + : response.headers['set-cookie']; + const authorizeToken = cookies.match('(^|;)[s]*authorize_token=([^;]*)')[2]; + + options.method = 'POST'; + options.form = { + username, + password, + allow: authorizeToken, + }; + + options.followRedirect = false; + options.headers = { + cookie: `authorize_token=${authorizeToken}`, + }; + + response = await client.request(options); + const location = response.headers.location; + const token = JSON.parse( + '{"' + + decodeURI(location.slice(location.indexOf('#') + 1)) + .replace(/"/g, '\\"') + .replace(/&/g, '","') + .replace(/=/g, '":"') + + '"}' + ); + + const accessToken = token.access_token; + (client.credentials as any).accessToken = accessToken; + return `Bearer ${accessToken}`; +} diff --git a/src/helpers/index.ts b/src/helpers/index.ts new file mode 100644 index 00000000..dc08a9f3 --- /dev/null +++ b/src/helpers/index.ts @@ -0,0 +1,3 @@ +export * from './getAuthorizationHeader'; +export * from './credentials'; +export * from './endpoints'; diff --git a/src/image/getImage.test.ts b/src/image/getImage.test.ts new file mode 100644 index 00000000..ad99f1ca --- /dev/null +++ b/src/image/getImage.test.ts @@ -0,0 +1,19 @@ +import { ImgurClient } from '../client'; +import { getImage } from './getImage'; + +test('returns an image response', async () => { + const accessToken = 'abc123'; + const client = new ImgurClient({ accessToken }); + const response = await getImage(client, 'CEddrgP'); + expect(response).toMatchInlineSnapshot(` + Object { + "data": Object { + "description": "image-description", + "id": "CEddrgP", + "title": "image-title", + }, + "status": 200, + "success": true, + } + `); +}); diff --git a/src/image/getImage.ts b/src/image/getImage.ts new file mode 100644 index 00000000..764a6438 --- /dev/null +++ b/src/image/getImage.ts @@ -0,0 +1,50 @@ +import { ImgurClient } from '../client'; +import { IMAGE_ENDPOINT } from '../helpers'; + +type 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 +): Promise { + const url = `${IMAGE_ENDPOINT}/${imageHash}`; + return await client.authorizedRequest({ url }); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..32d9122f --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export { ImgurClient } from './client';