Skip to content

Commit

Permalink
#23 Combine realm statuses into single Discord message (#26)
Browse files Browse the repository at this point in the history
* #23 Combine realm statuses into single Discord message

* Add Code Climate config

* Update Code Climate config

* Clean-up
  • Loading branch information
samwisekind committed Jan 2, 2021
1 parent e386bd3 commit ded4fb8
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 95 deletions.
9 changes: 9 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2

exclude_patterns:
- '**/*.spec.ts'

checks:
similar-code:
config:
threshold: 50
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ The server can also be built and run locally without Docker — see [Development
Once the server is running, it will post messages to a single Discord channel (`DISCORD_CHANNEL_ID` defined in [Installation](#installation)) when:

* The status of a realm (for each in `REALM_SLUGS` defined in [Installation](#installation)) changes
* The server will poll the _World of Warcraft_ API every 5 minutes
* The server will only post a message when the realm status changes from its previous state (i.e. it won't post a message for the same status multiple times)
* The server will poll the _World of Warcraft_ API every 60 seconds
* The server will only post a message when the status of a realm changes from its previous state (i.e. it won't post messages for the same status multiple times)
* The realm status is provided in the API from the [connected realm](https://us.battle.net/support/en/article/000014296) data instead of the realm data itself – the server will automatically retrieve the connected realm data from the realm slug
* Daily quests and weekly raids reset
* The server will post a message every day when daily quests reset and a message on Tuesday when weekly raids reset, both at a fixed time of 1500 UTC (based on the US reset time)
Expand Down
5 changes: 1 addition & 4 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ describe('index', () => {
expect(cron.schedule).toHaveBeenNthCalledWith(2, '* * * * *', expect.any(Function));

expect(postResetMessage).toHaveBeenCalledTimes(1);

expect(postRealmStatus).toHaveBeenCalledTimes(2);
expect(postRealmStatus).toHaveBeenNthCalledWith(1, 'test_realm_slug_1');
expect(postRealmStatus).toHaveBeenNthCalledWith(2, 'test_realm_slug_2');
expect(postRealmStatus).toHaveBeenCalledTimes(1);
});
});
10 changes: 2 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import cron from 'node-cron';

import postResetMessage from './reset';
import postRealmStatus from './realm';
import postRealmStatuses from './realm';

global.fetch = require('node-fetch');

process.env.TZ = 'UTC';

const {
REALM_SLUGS,
REALM_SLUG, // Backward compatibility for pre-v1.0.2
} = process.env;

// Every day at 15:00 UTC
cron.schedule('0 15 * * *', () => {
try {
Expand All @@ -25,8 +20,7 @@ cron.schedule('0 15 * * *', () => {
// Every 60 seconds
cron.schedule('* * * * *', () => {
try {
String(REALM_SLUGS || /* istanbul ignore next */ REALM_SLUG)
.split(',').forEach((slug) => postRealmStatus(slug));
postRealmStatuses();
} catch (error) {
/* istanbul ignore next */
console.error('Unable to post realm status message', error);
Expand Down
132 changes: 71 additions & 61 deletions src/realm.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fetchMock from 'fetch-mock';

import postDiscordChannelMessage from './discord';
import postRealmStatus from './realm';
import postRealmStatuses from './realm';

jest.mock('./blizzard', () => ({
__esModule: true,
Expand All @@ -15,11 +15,23 @@ jest.mock('./discord', () => ({

beforeEach(() => {
fetchMock.get(
'begin:https://us.api.blizzard.com/data/wow/realm/',
'begin:https://us.api.blizzard.com/data/wow/realm/test_realm_slug_1',
{
body: {
slug: 'test_realm_slug_1',
name: { en_US: 'test_realm_name' },
connected_realm: { href: 'https://test-connected-realm-api.com/' },
connected_realm: { href: 'https://test-connected-realm-1-api.com/' },
},
},
);

fetchMock.get(
'begin:https://us.api.blizzard.com/data/wow/realm/test_realm_slug_2',
{
body: {
slug: 'test_realm_slug_2',
name: { en_US: 'test_realm_name' },
connected_realm: { href: 'https://test-connected-realm-2-api.com/' },
},
},
);
Expand All @@ -30,91 +42,89 @@ afterEach(() => {
fetchMock.restore();
});

describe('postRealmStatus', () => {
describe('postRealmStatuses', () => {
it('Makes first request without posting a message to Discord', async () => {
fetchMock.get(
'https://test-connected-realm-api.com/',
{
body: {
status: {
type: 'UP',
name: { en_US: 'Up' },
},
},
},
'https://test-connected-realm-1-api.com/',
{ body: { status: { type: 'UP', name: { en_US: 'Up' } } } },
);

fetchMock.get(
'https://test-connected-realm-2-api.com/',
{ body: { status: { type: 'UP', name: { en_US: 'Up' } } } },
);

await postRealmStatus('test_realm_slug');
await postRealmStatuses();

const [realm1, realm2, connectedRealm1, connectedRealm2] = fetchMock.calls();

expect(realm1[0]).toBe('https://us.api.blizzard.com/data/wow/realm/test_realm_slug_1?namespace=dynamic-us');
expect(realm1[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } });

expect(realm2[0]).toBe('https://us.api.blizzard.com/data/wow/realm/test_realm_slug_2?namespace=dynamic-us');
expect(realm2[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } });

expect(connectedRealm1[0]).toBe('https://test-connected-realm-1-api.com/');
expect(connectedRealm1[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } });

const [realm, connectedRealm] = fetchMock.calls();
expect(connectedRealm2[0]).toBe('https://test-connected-realm-2-api.com/');
expect(connectedRealm2[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } });

expect(realm[0]).toBe('https://us.api.blizzard.com/data/wow/realm/test_realm_slug?namespace=dynamic-us');
expect(realm[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } });
expect(connectedRealm[0]).toBe('https://test-connected-realm-api.com/');
expect(connectedRealm[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } });
expect(postDiscordChannelMessage).toHaveBeenCalledTimes(0);
});

it('Posts to Discord when realm status changes after the initial request', async () => {
it('Posts to Discord for multiple realm status changes', async () => {
fetchMock.get(
'https://test-connected-realm-api.com/',
{
body: {
status: {
type: 'DOWN',
name: { en_US: 'Down' },
},
},
},
'https://test-connected-realm-1-api.com/',
{ body: { status: { type: 'DOWN', name: { en_US: 'Down' } } } },
);

await postRealmStatus('test_realm_slug');

const [realm, connectedRealm] = fetchMock.calls();

expect(realm[0]).toBe('https://us.api.blizzard.com/data/wow/realm/test_realm_slug?namespace=dynamic-us');
expect(realm[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } });
fetchMock.get(
'https://test-connected-realm-2-api.com/',
{ body: { status: { type: 'DOWN', name: { en_US: 'Down' } } } },
);

expect(connectedRealm[0]).toBe('https://test-connected-realm-api.com/');
expect(connectedRealm[1]).toStrictEqual({ headers: { Authorization: 'Bearer test_blizzard_api_token' } });
await postRealmStatuses();

expect(postDiscordChannelMessage).toHaveBeenCalledTimes(1);
expect(postDiscordChannelMessage).toHaveBeenNthCalledWith(1, ':red_circle: test_realm_name is **down**');
expect(postDiscordChannelMessage).toHaveBeenNthCalledWith(
1,
':red_circle: test_realm_name is **down**\n:red_circle: test_realm_name is **down**',
);
});

it('Posts to Discord when realm status changes after posting for the first time', async () => {
it('Posts to Discord for a single realm status change', async () => {
fetchMock.get(
'https://test-connected-realm-api.com/',
{
body: {
status: {
type: 'UP',
name: { en_US: 'Up' },
},
},
},
'https://test-connected-realm-1-api.com/',
{ body: { status: { type: 'UP', name: { en_US: 'Up' } } } },
);

await postRealmStatus('test_realm_slug');
fetchMock.get(
'https://test-connected-realm-2-api.com/',
{ body: { status: { type: 'DOWN', name: { en_US: 'Down' } } } },
);

await postRealmStatuses();

expect(postDiscordChannelMessage).toHaveBeenCalledTimes(1);
expect(postDiscordChannelMessage).toHaveBeenNthCalledWith(1, ':green_circle: test_realm_name is **up**');
expect(postDiscordChannelMessage).toHaveBeenNthCalledWith(
1,
':green_circle: test_realm_name is **up**',
);
});

it('Does not post to Discord when realm status is the same', async () => {
it('Does not post to Discord when no realm status changes', async () => {
fetchMock.get(
'https://test-connected-realm-api.com/',
{
body: {
status: {
type: 'UP',
name: { en_US: 'Up' },
},
},
},
'https://test-connected-realm-1-api.com/',
{ body: { status: { type: 'UP', name: { en_US: 'Up' } } } },
);

fetchMock.get(
'https://test-connected-realm-2-api.com/',
{ body: { status: { type: 'DOWN', name: { en_US: 'Down' } } } },
);

await postRealmStatus('test_realm_slug');
await postRealmStatuses();

expect(postDiscordChannelMessage).toHaveBeenCalledTimes(0);
});
Expand Down
61 changes: 41 additions & 20 deletions src/realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,18 @@ import {

const {
NODE_ENV,
REALM_SLUGS,
REALM_SLUG, // Backwards compatibility for pre-v1.0.2
} = process.env;

const realmStatuses: {
[key: string]: string;
} = {};

/**
* Handler for posting a realm status message to Discord
* Helper for getting realm and connected realm data
*/
const postRealmStatus = async (slug: string) => {
/* istanbul ignore if */
if (NODE_ENV !== 'test') {
console.log('Posting realm status message to Discord for:', slug);
}

const getRealmData = async (slug: string) => {
const headers = { Authorization: `Bearer ${await getBlizzardAPIToken()}` };

// Get the realm data for the realm name and connected realm ID
Expand All @@ -38,22 +35,46 @@ const postRealmStatus = async (slug: string) => {
{ headers },
).then((response) => response.json());

if (
realmStatuses[slug] // Do nothing on the first request
&& realmStatuses[slug] !== connectedRealm.status.type
) {
let message = REALM_STATUS_MESSAGES.UP;
if (connectedRealm.status.type !== REALM_STATUS_UP) {
message = REALM_STATUS_MESSAGES.DOWN;
}
return { realm, connectedRealm };
};

message = message.replace('{REALM_NAME}', realm.name.en_US)
.replace('{REALM_STATUS}', connectedRealm.status.name.en_US.toLowerCase());
/**
* Handler for posting realm statuses message to Discord
*/
const postRealmStatuses = async () => {
const slugs = String(REALM_SLUGS || /* istanbul ignore next */ REALM_SLUG).split(',');

postDiscordChannelMessage(message);
/* istanbul ignore if */
if (NODE_ENV !== 'test') {
console.log('Posting realm status message to Discord for:', ...slugs);
}

realmStatuses[slug] = connectedRealm.status.type;
const messages: Array<string> = [];

const results = await Promise.all(slugs.map(getRealmData));

results.forEach(({ realm, connectedRealm }) => {
if (
realmStatuses[realm.slug] // Do nothing on the first request
&& realmStatuses[realm.slug] !== connectedRealm.status.type
) {
let message = REALM_STATUS_MESSAGES.UP;
if (connectedRealm.status.type !== REALM_STATUS_UP) {
message = REALM_STATUS_MESSAGES.DOWN;
}

message = message.replace('{REALM_NAME}', realm.name.en_US)
.replace('{REALM_STATUS}', connectedRealm.status.name.en_US.toLowerCase());

messages.push(message);
}

realmStatuses[realm.slug] = connectedRealm.status.type;
});

if (messages.length > 0) {
postDiscordChannelMessage(messages.join('\n'));
}
};

export default postRealmStatus;
export default postRealmStatuses;

0 comments on commit ded4fb8

Please sign in to comment.