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