Adding supplementary required files
This commit is contained in:
parent
06faadb3a4
commit
4fa33d1de6
7582
package-lock.json
generated
Normal file
7582
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
84
package.json
Normal file
84
package.json
Normal file
@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "slack2hangoutschat-webhook",
|
||||
"version": "0.2.0",
|
||||
"description": "Slack 2 Google HangoutChat webhook converter",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tchiotludo/slack2hangoutschat-webhook.git"
|
||||
},
|
||||
"keywords": [
|
||||
"hangouts-chat",
|
||||
"google-chat",
|
||||
"slack",
|
||||
"webhook"
|
||||
],
|
||||
"author": "tchiotludo",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "node dist/server.js",
|
||||
"build": "npm run build:ts && npm run lint:ts",
|
||||
"build:ts": "tsc",
|
||||
"watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"cyan.bold,green.bold\" \"npm run watch:ts\" \"npm run watch:node\"",
|
||||
"watch:test": "npm run test -- --watchAll",
|
||||
"watch:node": "nodemon dist/server.js",
|
||||
"watch:ts": "tsc -w",
|
||||
"test": "jest --forceExit --coverage --verbose",
|
||||
"lint": "npm run lint:ts && ",
|
||||
"lint:ts": "tslint -c tslint.json -p tsconfig.json"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@slack/client": "^5.0.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"compression": "^1.7.4",
|
||||
"errorhandler": "^1.5.1",
|
||||
"express": "^4.17.1",
|
||||
"express-winston": "^4.0.1",
|
||||
"hangouts-chat-webhook": "^0.1.1",
|
||||
"superagent": "^5.1.2",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.0.1",
|
||||
"@types/errorhandler": "^0.0.32",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/express-winston": "^4.0.0",
|
||||
"@types/jest": "^24.0.23",
|
||||
"@types/node": "^12.12.16",
|
||||
"@types/superagent": "^4.1.4",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"concurrently": "^5.0.1",
|
||||
"coveralls": "^3.0.9",
|
||||
"jest": "^24.9.0",
|
||||
"jest-extended": "^0.11.2",
|
||||
"jest-junit": "^10.0.0",
|
||||
"nodemon": "^2.0.1",
|
||||
"supertest": "^4.0.2",
|
||||
"ts-jest": "^24.2.0",
|
||||
"ts-node": "^8.5.4",
|
||||
"tslint": "^5.20.1",
|
||||
"typescript": "^3.7.3"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"tsConfigFile": "tsconfig.json"
|
||||
}
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": "./node_modules/ts-jest/preprocessor.js"
|
||||
},
|
||||
"testMatch": [
|
||||
"**/test/**/*.test.(ts|js)"
|
||||
],
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
BIN
public/chat.png
Normal file
BIN
public/chat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
BIN
public/favicon.jpg
Normal file
BIN
public/favicon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
175
public/index.html
Normal file
175
public/index.html
Normal file
@ -0,0 +1,175 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Slack 2 Hangouts Chat Webhook</title>
|
||||
<meta name="description" content="">
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="icon" href="favicon.jpg">
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:700" rel="stylesheet">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="text-center">
|
||||
<a href="https://github.com/you"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_red_aa0000.png" alt="Fork me on GitHub"></a>
|
||||
|
||||
<div class="cover-container d-flex h-100 p-3 mx-auto flex-column">
|
||||
<main role="main" class="inner cover">
|
||||
<article class="mb-5">
|
||||
<img src="slack.png" alt="Slack"/>
|
||||
<span class="text-warning">⇢</span>
|
||||
<img src="chat.png" alt="Hangouts chat"/>
|
||||
</article>
|
||||
|
||||
<h1 class="cover-heading text-warning">Slack 2 Hangouts Chat Webhook</h1>
|
||||
<p class="lead mb-5">
|
||||
Don't wait that applications <code>add notification</code> to Hangouts Chat. <br />
|
||||
<code>Simply paste</code> your Hangouts Chat webhook url below and get a webhook url
|
||||
<code>compatible</code> with Slack webhook.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="url">Enter your Hangouts Chat webhook url</label>
|
||||
<input type="url" class="form-control" id="url"
|
||||
placeholder="https://chat.googleapis.com/v1/spaces/{{space}}/messages?key={{key}&token={{token}}}"
|
||||
required>
|
||||
<div class="invalid-feedback">
|
||||
Please provide a Hangouts Chat webhook url.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="converted">Webhook url to use in your application</label>
|
||||
<input type="url" class="form-control" id="converted" readonly>
|
||||
</div>
|
||||
<input type="button" class="mt-2 btn btn-lg btn-info convert" value="Give me my url !" />
|
||||
|
||||
<form class="d-none">
|
||||
<div class="alert alert-dismissible mt-5 d-none text-left" role="alert">
|
||||
<pre>
|
||||
</pre>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-5">
|
||||
<label for="slack">Slack webhook</label>
|
||||
<textarea class="form-control" id="slack" rows="10">{
|
||||
"attachments": [
|
||||
{
|
||||
"fallback": "Required plain-text summary of the attachment.",
|
||||
"color": "#36a64f",
|
||||
"pretext": "Optional text that appears above the attachment block",
|
||||
"author_name": "Bobby Tables",
|
||||
"author_link": "https://gsuite.google.com/products/chat/",
|
||||
"author_icon": "https://www.gstatic.com/images/branding/product/2x/chat_64dp.png",
|
||||
"title": "Slack API Documentation",
|
||||
"title_link": "https://api.slack.com/",
|
||||
"text": "Optional text that appears within the attachment",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Priority",
|
||||
"value": "High",
|
||||
"short": false
|
||||
}
|
||||
],
|
||||
"image_url": "https://assets.brandfolder.com/oox8px-b08c7c-5m1qjd/original/full-color-mark%202x.png",
|
||||
"thumb_url": "https://assets.brandfolder.com/oox90q-9q2cew-bw1vdr/view.png",
|
||||
"footer": "Slack API",
|
||||
"footer_icon": "https://platform.slack-edge.com/img/default_application_icon.png",
|
||||
"ts": 123456789
|
||||
}
|
||||
]
|
||||
}</textarea>
|
||||
</div>
|
||||
|
||||
<input type="submit" class="mt-2 btn btn-lg btn-info" value="Test webhook !" />
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<footer class="mastfoot mt-5">
|
||||
<div class="inner">
|
||||
<p>
|
||||
<a href="https://www.npmjs.com/package/slack2hangoutschat-webhook">
|
||||
<img src="https://img.shields.io/npm/dt/slack2hangoutschat-webhook.svg?style=social" alt="Npm downloads">
|
||||
</a>
|
||||
<a href="https://github.com/tchiotludo/slack2hangoutschat-webhook">
|
||||
<img src="https://img.shields.io/github/stars/tchiotludo/slack2hangoutschat-webhook.svg?style=social&label=Stars"
|
||||
alt="Github Stars"/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.3.1.min.js"
|
||||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="application/javascript">
|
||||
let form = $('form');
|
||||
let converted = $('#converted');
|
||||
let alert = $('.alert');
|
||||
|
||||
alert.on('ready', () => {
|
||||
alert.alert();
|
||||
});
|
||||
|
||||
$('input.convert').on('click', function() {
|
||||
let url = $('#url');
|
||||
|
||||
let invalid = $('.invalid-feedback');
|
||||
|
||||
invalid.css('display', 'none');
|
||||
form.addClass('d-none');
|
||||
|
||||
let match = url
|
||||
.val()
|
||||
.match(/https:\/\/chat\.googleapis\.com\/v1\/spaces\/([^/]+)\/messages\?(.*)/);
|
||||
|
||||
if (match && match[1] && match[2]) {
|
||||
converted.val(window.location.origin + '/' + match[1] + '?' + match[2]);
|
||||
form.removeClass('d-none')
|
||||
} else {
|
||||
converted.val('');
|
||||
invalid.css('display', 'block');
|
||||
}
|
||||
});
|
||||
|
||||
form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
$.ajax({
|
||||
url: converted.val(),
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: $('textarea').val(),
|
||||
contentType: "application/json",
|
||||
beforeSend: () => {
|
||||
alert
|
||||
.addClass('d-none')
|
||||
.removeClass('alert-success')
|
||||
.removeClass('alert-danger')
|
||||
|
||||
}
|
||||
})
|
||||
.done((data) => {
|
||||
alert
|
||||
.removeClass('d-none')
|
||||
.addClass('alert-success')
|
||||
.find('pre')
|
||||
.text(JSON.stringify(data, null, 2))
|
||||
})
|
||||
.fail((xhr) => {
|
||||
alert
|
||||
.removeClass('d-none')
|
||||
.addClass('alert-danger')
|
||||
.find('pre')
|
||||
.text(xhr.responseText)
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
BIN
public/slack.png
Normal file
BIN
public/slack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
87
public/style.css
Normal file
87
public/style.css
Normal file
@ -0,0 +1,87 @@
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.btn-secondary:hover,
|
||||
.btn-secondary:focus {
|
||||
color: #333;
|
||||
text-shadow: none;
|
||||
background-color: #fff;
|
||||
border: .05rem solid #fff;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
background-color: #333;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
body {
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-box;
|
||||
display: flex;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-box-pack: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
|
||||
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
max-width: 42em;
|
||||
}
|
||||
|
||||
.cover {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.cover article {
|
||||
font-family: Montserrat, serif;
|
||||
font-size: 20vw;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
|
||||
border: .05rem solid #000;
|
||||
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
|
||||
line-height: 5rem;
|
||||
padding: 3vw;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.cover article img {
|
||||
width: 28%;
|
||||
}
|
||||
|
||||
.cover article span {
|
||||
font-size: 20vw;
|
||||
}
|
||||
|
||||
@media (min-width: 42rem) {
|
||||
.cover article, .cover article span {
|
||||
font-size: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cover .btn-lg {
|
||||
padding: .75rem 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mastfoot {
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
223
src/converter.ts
Normal file
223
src/converter.ts
Normal file
@ -0,0 +1,223 @@
|
||||
"use strict";
|
||||
|
||||
import { IncomingWebhookSendArguments, MessageAttachment } from "@slack/client";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Image,
|
||||
Section,
|
||||
TextButton,
|
||||
WidgetMarkup,
|
||||
Message,
|
||||
KeyValue,
|
||||
CardHeader,
|
||||
User,
|
||||
TextParagraph,
|
||||
OnClick,
|
||||
OpenLink,
|
||||
Icon,
|
||||
ImageButton
|
||||
} from "hangouts-chat-webhook";
|
||||
|
||||
export class Converter {
|
||||
public static convert(slack: IncomingWebhookSendArguments): Message {
|
||||
const message: Message = new Message();
|
||||
|
||||
if (!slack.attachments && slack.text) {
|
||||
message.setText(slack.text);
|
||||
} else if (slack.attachments) {
|
||||
message.setPreviewText(slack.text);
|
||||
|
||||
if (slack.username) {
|
||||
message.setSender(new User().setDisplayName(slack["username"]));
|
||||
}
|
||||
|
||||
slack.attachments.forEach(attachment => {
|
||||
const header: CardHeader = new CardHeader()
|
||||
.setTitle(attachment["title"]);
|
||||
|
||||
if (attachment.fallback) {
|
||||
message.setFallbackText(message.getFallbackText() ? attachment.fallback + "\n" : attachment.fallback);
|
||||
message.setPreviewText(message.getFallbackText().trim());
|
||||
}
|
||||
|
||||
if (attachment.pretext) {
|
||||
header.setSubtitle(attachment.pretext);
|
||||
}
|
||||
|
||||
const card: Card = new Card()
|
||||
.setHeader(header);
|
||||
|
||||
const section: Section = new Section();
|
||||
|
||||
if (attachment.author_name) {
|
||||
section.addWidget(Converter.author(attachment));
|
||||
}
|
||||
|
||||
if (attachment.image_url) {
|
||||
section.addWidget(Converter.image(attachment));
|
||||
}
|
||||
|
||||
if (attachment.text) {
|
||||
section.addWidget(Converter.text(attachment));
|
||||
}
|
||||
|
||||
if (attachment.fields) {
|
||||
attachment.fields.forEach(field => {
|
||||
section.addWidget(Converter.field(field));
|
||||
});
|
||||
}
|
||||
|
||||
if (attachment.footer) {
|
||||
section.addWidget(Converter.footer(attachment));
|
||||
}
|
||||
|
||||
if (attachment.title_link) {
|
||||
section.addWidget(Converter.bottomLink(attachment));
|
||||
}
|
||||
|
||||
message.addCard(card.addSection(section));
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private static image(attachment: MessageAttachment): WidgetMarkup {
|
||||
return new WidgetMarkup()
|
||||
.setImage(new Image()
|
||||
.setImageUrl(attachment["image_url"])
|
||||
);
|
||||
}
|
||||
|
||||
private static formatText(text: string): string {
|
||||
const match = /^>>>([\s\S]*)/gm.exec(text);
|
||||
|
||||
if (match && match.length > 0) {
|
||||
text = text.replace(match[0], match[1]
|
||||
.trim()
|
||||
.replace(/\n/g, "\n" + '<font color="#e3e4e6">┃</font> ')
|
||||
);
|
||||
}
|
||||
|
||||
return text
|
||||
.replace(/\*(.+?)\*/g, "<b>$1</b>")
|
||||
.replace(/_(.+?)_/g, "<i>$1</i>")
|
||||
.replace(/~(.+?)~/g, "<strike>$1</strike>")
|
||||
.replace(/```(.+?)```/g, "<font color=\"#424242\">$1</font>")
|
||||
.replace(/^>(.+?)/gm, "<font color=\"#e3e4e6\">┃</font> $1")
|
||||
.replace(/`(.+?)`/g, "<font color=\"#d72b3f\">$1</font>")
|
||||
.trim();
|
||||
}
|
||||
|
||||
private static text(attachment: MessageAttachment): WidgetMarkup {
|
||||
let color: String;
|
||||
|
||||
if (attachment.color) {
|
||||
let hex: String = attachment.color;
|
||||
if (attachment.color == "danger") {
|
||||
hex = "#a30200";
|
||||
} else if (attachment.color == "warning") {
|
||||
hex = "#daa038";
|
||||
} else if (attachment.color == "good") {
|
||||
hex = "#2eb886";
|
||||
} else if (attachment.color.substr(0, 1) != "#") {
|
||||
hex = "#e8e8e8";
|
||||
}
|
||||
|
||||
color = '<font color="' + hex + '"><b>▮</b></font> ';
|
||||
}
|
||||
|
||||
return new WidgetMarkup()
|
||||
.setTextParagraph(new TextParagraph()
|
||||
.setText(color + Converter.formatText(attachment.text))
|
||||
);
|
||||
}
|
||||
|
||||
private static author(attachment: MessageAttachment): WidgetMarkup {
|
||||
const widget: WidgetMarkup = new WidgetMarkup();
|
||||
|
||||
let onclick: OnClick;
|
||||
if (attachment.author_link) {
|
||||
onclick = new OnClick()
|
||||
.setOpenLink(new OpenLink()
|
||||
.setUrl(attachment.author_link)
|
||||
);
|
||||
}
|
||||
|
||||
if (attachment.author_icon) {
|
||||
widget.addButton(new Button()
|
||||
.setImageButton(new ImageButton()
|
||||
.setIconUrl(attachment.author_icon)
|
||||
.setName(attachment.author_name)
|
||||
.setOnClick(onclick)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
widget.addButton(new Button()
|
||||
.setTextButton(new TextButton()
|
||||
.setText(attachment.author_name)
|
||||
.setOnClick(onclick)
|
||||
)
|
||||
);
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
private static field(field: any): WidgetMarkup {
|
||||
return new WidgetMarkup()
|
||||
.setKeyValue(new KeyValue()
|
||||
.setTopLabel(field["title"])
|
||||
.setContent(field["value"])
|
||||
.setContentMultiline(!field["short"])
|
||||
.setIcon(Icon.DESCRIPTION)
|
||||
);
|
||||
}
|
||||
|
||||
private static footer(attachment: MessageAttachment): WidgetMarkup {
|
||||
const footerLink: OnClick = new OnClick();
|
||||
|
||||
if (attachment.title_link) {
|
||||
footerLink.setOpenLink(new OpenLink()
|
||||
.setUrl(attachment.title_link)
|
||||
);
|
||||
}
|
||||
|
||||
const footer: WidgetMarkup = new WidgetMarkup();
|
||||
|
||||
if (attachment.footer_icon) {
|
||||
footer.addButton(new Button()
|
||||
.setImageButton(new ImageButton()
|
||||
.setIconUrl(attachment.footer_icon)
|
||||
.setName(attachment.footer)
|
||||
.setOnClick(footerLink)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
footer.addButton(new Button()
|
||||
.setTextButton(new TextButton()
|
||||
.setText(attachment.footer)
|
||||
.setOnClick(footerLink)
|
||||
)
|
||||
);
|
||||
|
||||
return footer;
|
||||
}
|
||||
|
||||
private static bottomLink(attachment: MessageAttachment): WidgetMarkup {
|
||||
return new WidgetMarkup()
|
||||
.addButton(new Button()
|
||||
.setTextButton(new TextButton()
|
||||
.setText("Open")
|
||||
.setOnClick(new OnClick()
|
||||
.setOpenLink(new OpenLink().
|
||||
setUrl(attachment.title_link)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
3
src/index.ts
Normal file
3
src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { Webhook } from "./webhook";
|
||||
export { Converter } from "./converter";
|
||||
export { app } from "./server";
|
||||
79
src/server.ts
Normal file
79
src/server.ts
Normal file
@ -0,0 +1,79 @@
|
||||
"use strict";
|
||||
|
||||
import url from "url";
|
||||
import express, { Request, Response } from "express";
|
||||
import compression from "compression";
|
||||
import bodyParser from "body-parser";
|
||||
import winston from "winston";
|
||||
import expressWinston from "express-winston";
|
||||
import errorHandler from "errorhandler";
|
||||
import { Webhook } from "./webhook";
|
||||
|
||||
export const app = express();
|
||||
|
||||
app.set("port", process.env.PORT || 3000);
|
||||
|
||||
// request
|
||||
app.use(compression());
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({extended: true}));
|
||||
|
||||
// logger
|
||||
app.use((req, res, next) => {
|
||||
Object.assign(res.locals, {
|
||||
logUrl: url.parse(req.url).pathname
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
const logger = winston.createLogger({
|
||||
transports: [
|
||||
new winston.transports.Console({})
|
||||
],
|
||||
format: winston.format.combine(
|
||||
winston.format.splat(),
|
||||
winston.format.colorize({}),
|
||||
winston.format.timestamp(),
|
||||
winston.format.printf(info => {
|
||||
let meta: string = "";
|
||||
if (info.meta && app.get("env") == "development") {
|
||||
meta = JSON.stringify(info.meta || {}, undefined, 4);
|
||||
}
|
||||
|
||||
return `${info.timestamp} ${info.level}: ${info.message} ${meta}`;
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
app.use(expressWinston.logger({
|
||||
winstonInstance: logger,
|
||||
msg: "{{req.method}} {{res.locals.logUrl}} {{res.statusCode}} {{res.responseTime}}ms",
|
||||
colorize: true,
|
||||
}));
|
||||
|
||||
// router
|
||||
app.use(express.static("public"));
|
||||
app.post("/:space", Webhook.express);
|
||||
|
||||
// errors
|
||||
app.use(expressWinston.errorLogger({
|
||||
winstonInstance: logger,
|
||||
msg: "{{req.method}} {{res.locals.logUrl}} {{res.statusCode}} {{err.message}}"
|
||||
}));
|
||||
|
||||
app.use(errorHandler({log: false}));
|
||||
|
||||
// docker exit
|
||||
process.on("SIGINT", function() {
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// listen
|
||||
app.listen(app.get("port"), () => {
|
||||
logger.info(
|
||||
"App is running at http://localhost:%d in %s mode",
|
||||
app.get("port"),
|
||||
app.get("env")
|
||||
);
|
||||
});
|
||||
|
||||
115
src/webhook.ts
Normal file
115
src/webhook.ts
Normal file
@ -0,0 +1,115 @@
|
||||
"use strict";
|
||||
|
||||
import * as url from "url";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import * as superagent from "superagent";
|
||||
import { Converter } from "./converter";
|
||||
import { IncomingWebhookSendArguments } from "@slack/client";
|
||||
|
||||
export class WebhookResponse {
|
||||
public status: number;
|
||||
public headers: any;
|
||||
public body: string;
|
||||
|
||||
constructor(status: number, headers: any, body: string) {
|
||||
this.status = status;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
export class WebhookError {
|
||||
public status: number;
|
||||
public headers: any;
|
||||
public body: string;
|
||||
public error: string;
|
||||
|
||||
constructor(status: number, headers: any, body: string, error: string) {
|
||||
this.status = status;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export class Webhook {
|
||||
public static send(space: string, key: string, token: string, body: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const webhookUrl: string = "https://chat.googleapis.com/v1/spaces/" + space + "/messages?key=" + key + "&token=" + token;
|
||||
|
||||
superagent.post(webhookUrl)
|
||||
.set("Content-Type", "application/json; charset=UTF-8")
|
||||
.send(Converter.convert(body as IncomingWebhookSendArguments))
|
||||
.then((response: superagent.Response) => {
|
||||
resolve(new WebhookResponse(
|
||||
response.status,
|
||||
response.header,
|
||||
response.body
|
||||
));
|
||||
})
|
||||
.catch((err: any) => {
|
||||
reject(new WebhookError(
|
||||
err.status,
|
||||
err.header,
|
||||
err.body,
|
||||
err.response && err.response.text ? JSON.parse(err.response.text) : "Request failed",
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static express(req: Request, res: Response, next: NextFunction): void {
|
||||
const parsed: url.UrlWithParsedQuery = url.parse(req.url, true);
|
||||
|
||||
Webhook.send(req.params["space"], <string>parsed.query.key, <string>parsed.query.token, req.body)
|
||||
.then((value: WebhookResponse) => res
|
||||
.status(value.status)
|
||||
.json(value)
|
||||
)
|
||||
.catch((reason: WebhookError) => {
|
||||
if (!(reason instanceof WebhookError)) {
|
||||
return next(reason);
|
||||
}
|
||||
|
||||
return res
|
||||
.status(reason.status)
|
||||
.json(reason);
|
||||
});
|
||||
}
|
||||
|
||||
public static async azure(context: any, req: any) {
|
||||
const parsed = url.parse(req.url, true);
|
||||
|
||||
try {
|
||||
const value: WebhookResponse = await Webhook.send(
|
||||
<string> parsed.query.space,
|
||||
<string> parsed.query.key,
|
||||
<string> parsed.query.token,
|
||||
req.body
|
||||
);
|
||||
|
||||
context.res = {
|
||||
status: value.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=UTF-8"
|
||||
},
|
||||
body: value
|
||||
};
|
||||
} catch (reason) {
|
||||
if (!(reason instanceof WebhookError)) {
|
||||
context.res = {
|
||||
status: 500,
|
||||
body: reason.message
|
||||
};
|
||||
} else {
|
||||
context.res = {
|
||||
status: reason.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=UTF-8"
|
||||
},
|
||||
body: reason
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
0
test/converter.test.js
Normal file
0
test/converter.test.js
Normal file
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"es2015",
|
||||
"es2016.array.include",
|
||||
"esnext.asynciterable"
|
||||
],
|
||||
"paths": {
|
||||
"*": [
|
||||
"node_modules/*",
|
||||
"src/types/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
60
tslint.json
Normal file
60
tslint.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"rules": {
|
||||
"class-name": true,
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
],
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace"
|
||||
],
|
||||
"no-var-keyword": true,
|
||||
"quotemark": [
|
||||
true,
|
||||
"double",
|
||||
"avoid-escape"
|
||||
],
|
||||
"semicolon": [
|
||||
true,
|
||||
"always",
|
||||
"ignore-bound-class-methods"
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-module",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
],
|
||||
"no-internal-module": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-null-keyword": true,
|
||||
"prefer-const": true,
|
||||
"jsdoc-format": true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user