Skip to content

Commit

Permalink
feat: Add webhook type (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
simenandre authored Dec 31, 2021
1 parent 12686c3 commit 4ca3e5d
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 18 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ and reusable Slack code declaratively and ready for production.
- Used in many production workloads.
- Building blocks with [slack-block-builder].
- Supports sending messages directly to Slack Web API.
- Supports Slack webhooks.
- Supports Google Logging.

[slack-block-builder]: https://github.com/raycharius/slack-block-builder
Expand Down Expand Up @@ -66,6 +67,17 @@ import { SlackModule } from 'nestjs-slack';
export class AppModule {}
```

To use `webhook` type, you'll typically use these settings:

```typescript
SlackModule.forRoot({
type: 'webhook',
webhookOptions: {
url: '<the webhook url>',
},
}),
```

### Example

You can easily inject `SlackService` to be used in your services, controllers,
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"devDependencies": {
"@cobraz/prettier-config": "^1.0.1",
"@google-cloud/logging": "^9.5.5",
"@indivorg/eslint-config": "^2.0.0",
"@nestjs/common": "7.6.15",
"@nestjs/core": "7.6.15",
Expand All @@ -35,11 +36,11 @@
"husky": "7",
"jest": "26.6.3",
"lint-staged": "11.1.2",
"nock": "^13.2.1",
"prettier": "^2.3.2",
"reflect-metadata": "^0.1.13",
"slack-block-builder": "^2.1.0",
"ts-jest": "^26.4.4",
"@google-cloud/logging": "^9.5.5",
"typescript": "4.1.4"
},
"lint-staged": {
Expand All @@ -49,12 +50,13 @@
},
"dependencies": {
"@slack/web-api": "^6",
"axios": "^0.24.0",
"slack-block-builder": "^2"
},
"peerDependencies": {
"@google-cloud/logging": "^9",
"@nestjs/common": ">=7 <=8",
"@nestjs/core": ">=7 <=8",
"@google-cloud/logging": "^9"
"@nestjs/core": ">=7 <=8"
},
"peerDependenciesMeta": {
"@google-cloud/logging": {
Expand Down
5 changes: 5 additions & 0 deletions src/__tests__/google.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Test } from '@nestjs/testing';
import {
GOOGLE_LOGGING,
SLACK_MODULE_OPTIONS,
SLACK_WEBHOOK_URL,
SLACK_WEB_CLIENT,
} from '../constants';
import { SlackService } from '../slack.service';
Expand All @@ -24,6 +25,10 @@ describe('google logging', () => {
provide: SLACK_WEB_CLIENT,
useValue: null,
},
{
provide: SLACK_WEBHOOK_URL,
useValue: null,
},
{
provide: GOOGLE_LOGGING,
useValue: {
Expand Down
48 changes: 48 additions & 0 deletions src/__tests__/webhook.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SlackService } from '../slack.service';
import { createApp } from './fixtures';
import * as nock from 'nock';

describe('webhook', () => {
let service: SlackService;
let output: any;

const baseUrl = 'https://example.com';

beforeEach(async () => {
output = jest.fn();
const app = await createApp({
type: 'webhook',
webhookOptions: { url: `${baseUrl}/webhook` },
});
service = app.get<SlackService>(SlackService);
});

it('must send requests to API', async () => {
// nock.recorder.rec();
const scope = nock(baseUrl, { encodedQueryParams: true })
.post('/webhook', {
text: 'hello-world',
})
.reply(200, 'ok');

await service.postMessage({ text: 'hello-world' });

scope.done();
});

it('Should throw when request fails', () => {
nock(baseUrl, { encodedQueryParams: true })
.post('/webhook', {
text: 'hello-world',
})
.reply(500, 'fail');

return service
.postMessage({ text: 'hello-world' })
.catch(error =>
expect(error).toMatchInlineSnapshot(
`[Error: Could not send request to Slack Webhook: fail]`,
),
);
});
});
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const SLACK_MODULE_OPTIONS = 'SlackModuleOptions';
export const SLACK_WEB_CLIENT = 'SlackWebClient';
export const SLACK_WEBHOOK_URL = 'SlackWebhookUrl';
export const GOOGLE_LOGGING = 'SlackGoogleLogging';
43 changes: 42 additions & 1 deletion src/slack.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { WebClientOptions } from '@slack/web-api';
import {
GOOGLE_LOGGING,
SLACK_MODULE_OPTIONS,
SLACK_WEBHOOK_URL,
SLACK_WEB_CLIENT,
} from './constants';
import { SlackService } from './slack.service';
Expand All @@ -18,7 +19,18 @@ export interface SlackApiOptions {
clientOptions?: WebClientOptions;
}

export type SlackRequestType = 'api' | 'stdout' | 'google';
export interface SlackWebhookOptions {
/**
* Incoming Webhooks are a simple way to post messages from apps into Slack.
* Creating an Incoming Webhook gives you a unique URL to which you send a
* JSON payload with the message text and some options.
*
* Read more: https://api.slack.com/messaging/webhooks
*/
url: string;
}

export type SlackRequestType = 'api' | 'webhook' | 'stdout' | 'google';

export interface SlackConfig {
/**
Expand Down Expand Up @@ -51,6 +63,12 @@ export interface SlackConfig {
*/
apiOptions?: SlackApiOptions;

/**
* These configuration options are only required when type is set to
* `api`.
*/
webhookOptions?: SlackWebhookOptions;

/**
* Setting this changes which function is used to stdout.
*
Expand Down Expand Up @@ -82,6 +100,7 @@ export class SlackModule {
},
this.createAsyncGoogleLogger(options),
this.createAsyncWebClient(options),
this.createAsyncWebhook(options),
];
return {
global: options.isGlobal,
Expand Down Expand Up @@ -134,4 +153,26 @@ export class SlackModule {
},
};
}

private static createAsyncWebhook({ type }: SlackConfig): Provider {
if (type !== 'webhook') {
return {
provide: SLACK_WEBHOOK_URL,
useValue: null,
};
}

return {
provide: SLACK_WEBHOOK_URL,
inject: [SLACK_MODULE_OPTIONS],
useFactory: async (opts: SlackConfig) => {
invariant(
opts.webhookOptions,
'You must provide `webhookOptions` when using the webhook type.',
);

return opts.webhookOptions.url;
},
};
}
}
18 changes: 17 additions & 1 deletion src/slack.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { LogSync } from '@google-cloud/logging';
import axios, { AxiosError } from 'axios';
import { Inject, Injectable } from '@nestjs/common';
import type { ChatPostMessageArguments, WebClient } from '@slack/web-api';
import type { SlackBlockDto } from 'slack-block-builder';
import {
GOOGLE_LOGGING,
SLACK_MODULE_OPTIONS,
SLACK_WEBHOOK_URL,
SLACK_WEB_CLIENT,
} from './constants';
import type { SlackConfig, SlackRequestType } from './slack.module';
Expand All @@ -18,6 +20,7 @@ export class SlackService {
@Inject(SLACK_MODULE_OPTIONS) private readonly options: SlackConfig,
@Inject(SLACK_WEB_CLIENT) private readonly client: WebClient | null,
@Inject(GOOGLE_LOGGING) private readonly log: LogSync | null,
@Inject(SLACK_WEBHOOK_URL) private readonly webhookUrl: string | null,
) {}

/**
Expand Down Expand Up @@ -103,7 +106,7 @@ export class SlackService {
* @param opts
*/
async postMessage(req: SlackMessageOptions): Promise<void> {
if (!req.channel) {
if (!req.channel && this.options.type !== 'webhook') {
invariant(
this.options.defaultChannel,
'neither channel nor defaultChannel was applied',
Expand All @@ -113,6 +116,7 @@ export class SlackService {

const requestTypes: Record<SlackRequestType, () => Promise<void>> = {
api: async () => this.runApiRequest(req),
webhook: async () => this.runWebhookRequest(req),
stdout: async () => this.runStdoutRequest(req),
google: async () => this.runGoogleLoggingRequest(req),
};
Expand All @@ -129,6 +133,18 @@ export class SlackService {
await this.client.chat.postMessage(req as ChatPostMessageArguments);
}

private async runWebhookRequest(req: SlackMessageOptions) {
invariant(this.webhookUrl, 'expected webhook url to exist');

await axios.post(this.webhookUrl, req).catch((error: AxiosError) => {
invariant(error.response);

throw new Error(
`Could not send request to Slack Webhook: ${error.response.data}`,
);
});
}

private async runStdoutRequest(req: SlackMessageOptions) {
this.options.output(req);
}
Expand Down
Loading

0 comments on commit 4ca3e5d

Please sign in to comment.