Skip to content
This repository has been archived by the owner on Dec 30, 2021. It is now read-only.

Commit

Permalink
refactor: initial impl of ImgurClient class
Browse files Browse the repository at this point in the history
begin rewrite of client as an event emitter class

BREAKING CHANGE: ImgurClient class replaces imgur object
  • Loading branch information
kaimallea committed Mar 19, 2021
1 parent d598f44 commit 1609940
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/__tests__/mocks/handlers/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/mocks/handlers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
31 changes: 31 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
try {
return await got(options);
} catch (err) {
throw new Error(err);
}
}

async authorizedRequest(options: Options): Promise<any> {
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);
}
}
}
26 changes: 26 additions & 0 deletions src/helpers/credentials.ts
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 7 additions & 0 deletions src/helpers/endpoints.ts
Original file line number Diff line number Diff line change
@@ -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`;
28 changes: 28 additions & 0 deletions src/helpers/getAuthorizationHeader.test.ts
Original file line number Diff line number Diff line change
@@ -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"`
);
});
57 changes: 57 additions & 0 deletions src/helpers/getAuthorizationHeader.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> = {
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}`;
}
3 changes: 3 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './getAuthorizationHeader';
export * from './credentials';
export * from './endpoints';
19 changes: 19 additions & 0 deletions src/image/getImage.test.ts
Original file line number Diff line number Diff line change
@@ -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,
}
`);
});
50 changes: 50 additions & 0 deletions src/image/getImage.ts
Original file line number Diff line number Diff line change
@@ -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<ImageResponse> {
const url = `${IMAGE_ENDPOINT}/${imageHash}`;
return await client.authorizedRequest({ url });
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ImgurClient } from './client';

0 comments on commit 1609940

Please sign in to comment.