Skip to content

Commit

Permalink
Merge pull request #943 from tsg-ut/devcontainer2
Browse files Browse the repository at this point in the history
devcontainerおよびCodespacesの設定を追加
  • Loading branch information
hakatashi authored Oct 9, 2024
2 parents f66bb1b + 12dd4db commit 0d997f9
Show file tree
Hide file tree
Showing 17 changed files with 232 additions and 33 deletions.
1 change: 1 addition & 0 deletions .devcontainer/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
HTTP_PROXY_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
27 changes: 27 additions & 0 deletions .devcontainer/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
services:
slackbot:
build: slackbot
volumes:
- ..:/code
- /code/node_modules
- /code/functions/node_modules
stdin_open: true
tty: true
command: sleep infinity
environment:
- TEAM_ID=${TEAM_ID}
- SIGNING_SECRET=${SIGNING_SECRET}
- SLACK_TOKEN=${SLACK_TOKEN}
- HAKATASHI_TOKEN=${HAKATASHI_TOKEN}
- CHANNEL_SANDBOX=${CHANNEL_SANDBOX}
- SLACK_VERIFICATION_TOKEN=${SLACK_VERIFICATION_TOKEN}

tunnel:
build:
context: tunnel
dockerfile: Dockerfile
args:
- HTTP_PROXY_TOKEN=${HTTP_PROXY_TOKEN}
stdin_open: true
tty: true
command: node index.mjs --remote wss://slackbot-api.tsg.ne.jp/wsfwd --host slackbot
44 changes: 44 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
{
"name": "Node.js",
"dockerComposeFile": "compose.yaml",
"service": "slackbot",
"workspaceFolder": "/code",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": "true",
"username": "slackbot",
"upgradePackages": "true"
}
},
"runServices": [
"slackbot",
"tunnel"
],
"postAttachCommand": "docker logs slackbot_devcontainer-tunnel-1 && docker attach slackbot_devcontainer-tunnel-1",
"customizations": {
"vscode": {
"settings": {
"files.autoSave": "off",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "always",
"source.fixAll.eslint": "always"
},
"eslint.format.enable": true
},
"extensions": [
"ms-vscode.vscode-typescript-next",
"dbaeumer.vscode-eslint",
"GitHub.copilot",
"GitHub.copilot-chat",
"GitHub.vscode-pull-request-github",
"toba.vsfire"
]
}
}
}
16 changes: 16 additions & 0 deletions .devcontainer/slackbot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM node:20-bookworm

# Install build dependencies for node-canvas
# https://github.com/Automattic/node-canvas/wiki/Installation%3A-Ubuntu-and-other-Debian-based-systems
RUN apt-get update -y && \
apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev -y && \
apt-get install git bash -y && \
mkdir -p ~/.cache/slackbot/node_modules && \
mkdir -p ~/.cache/slackbot-functions/node_modules && \
git clone https://github.com/tsg-ut/slackbot.git --branch master --single-branch --recursive --depth 1 /code && \
cd /code && \
npm install && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

WORKDIR /code
10 changes: 10 additions & 0 deletions .devcontainer/tunnel/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM node:20-bookworm
ARG HTTP_PROXY_TOKEN

RUN apt-get update -y && \
apt-get install git bash -y && \
git clone https://${HTTP_PROXY_TOKEN}@github.com/tsg-ut/http-local-fwd.git && \
cd http-local-fwd/local && \
npm install

WORKDIR /http-local-fwd/local
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ tokens.sqlite3
.DS_Store

.vscode
!.vscode/launch.json
56 changes: 56 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Slackbot起動",
"type": "node",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run-script",
"dev",
"--",
"--only",
"${input:onlyArgument}"
],
"request": "launch",
"skipFiles": [
"<node_internals>/**"
],
"console": "internalConsole",
"outputCapture": "std"
},
{
"name": "ユニットテストの実行",
"type": "node",
"runtimeExecutable": "npm",
"args": [
"run-script",
"test",
"--",
"--colors",
"${input:testFilterArgument}"
],
"internalConsoleOptions": "openOnSessionStart",
"request": "launch",
"skipFiles": [
"<node_internals>/**"
],
"console": "internalConsole",
"outputCapture": "std"
}
],
"inputs": [
{
"id": "onlyArgument",
"type": "promptString",
"description": "起動するBOTの種類",
"default": "helloworld"
},
{
"id": "testFilterArgument",
"type": "promptString",
"description": "実行するユニットテストの正規表現フィルタ (全部実行する場合は空文字列を入力)",
"default": ""
}
]
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# slackbot

[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=false&ref=master&repo=105612722&skip_quickstart=false)

[![Test][action-image]][action-url]
[![Coverage Status][codecov-image]][codecov-url]

Expand All @@ -13,8 +15,6 @@

TSGのSlackで動くSlackbotたち

自分がOWNERのコードの変更は直接masterにpushして構いません。 ([CODEOWNERS](CODEOWNERS)参照)

## 環境構築

### Prerequisites
Expand Down
6 changes: 3 additions & 3 deletions helloworld/HelloWorld.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('helloworld', () => {
it('responds to "Hello"', async () => {
const response = await slack.getResponseTo('Hello');
expect(response).toEqual({
username: 'helloworld [test-hostname]',
username: 'helloworld [TEST_AUTHORITY]',
channel: slack.fakeChannel,
text: 'World!',
});
Expand All @@ -59,7 +59,7 @@ describe('helloworld', () => {

const mockedPostMessage = slack.webClient.chat.postMessage as jest.MockedFunction<typeof slack.webClient.chat.postMessage>;
expect(mockedPostMessage).toBeCalledWith({
username: 'helloworld [test-hostname]',
username: 'helloworld [TEST_AUTHORITY]',
channel: slack.fakeChannel,
text: 'Hello, World!',
blocks: [
Expand Down Expand Up @@ -113,7 +113,7 @@ describe('helloworld', () => {
elements: [
{
type: 'plain_text',
text: '⚠この値は再起動後も保存されますが、再起動前に投稿されたメッセージの数字は更新されなくなります。ボタンを押すとエラーが出る場合は、「Slackbotを作ろう」ページの「WebSocketトンネルをセットアップする」などを参考に Event API のセットアップが正常にできているかもう一度確認してください。',
text: '⚠この値は再起動後も保存されます。前回このメッセージを投稿してから60分以上経っている場合は、以前のメッセージが削除され再投稿されます。ボタンを押すとエラーが出る場合は、「Slackbotを作ろう」ページの「WebSocketトンネルをセットアップする」などを参考に Event API のセットアップが正常にできているかもう一度確認してください。',
emoji: true,
},
],
Expand Down
33 changes: 26 additions & 7 deletions helloworld/HelloWorld.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import {randomUUID} from 'crypto';
import type EventEmitter from 'events';
import os from 'os';
import type {BlockAction, ViewSubmitAction} from '@slack/bolt';
import type {SlackMessageAdapter} from '@slack/interactive-messages';
import type {MessageEvent, WebClient} from '@slack/web-api';
import {Mutex} from 'async-mutex';
import logger from '../lib/logger';
import type {SlackInterface} from '../lib/slack';
import {extractMessage} from '../lib/slackUtils';
import {extractMessage, getAuthorityLabel} from '../lib/slackUtils';
import State from '../lib/state';
import counterEditDialog from './views/counterEditDialog';
import helloWorldMessage from './views/helloWorldMessage';
Expand All @@ -31,7 +30,9 @@ export class HelloWorld {

#state: StateObj;

#SANDBOX_ID = process.env.CHANNEL_SANDBOX!;
#SANDBOX_ID = process.env.CHANNEL_SANDBOX ?? '';

#AUTHORITY = getAuthorityLabel();

// インスタンスを生成するためのファクトリメソッド
static async create(slack: SlackInterface) {
Expand Down Expand Up @@ -86,7 +87,7 @@ export class HelloWorld {
const stateValues = Object.assign({}, ...stateObjects);

mutex.runExclusive(() => (
this.setCounterValue(parseInt(stateValues.counter_input.value))
this.setCounterValue(parseInt(stateValues.counter_input.value) || 0)
));
});

Expand All @@ -105,11 +106,29 @@ export class HelloWorld {
}

private get username() {
return `helloworld [${os.hostname()}]`;
return `helloworld [${this.#AUTHORITY}]`;
}

// 「Hello, World!」メッセージを#sandboxに送信する
async postHelloWorld() {
if (this.#state.latestStatusMessage?.channel === this.#SANDBOX_ID) {
const timestamp = new Date(parseInt(this.#state.latestStatusMessage.ts) * 1000);
const elapsed = (Date.now() - timestamp.getTime()) / 1000;

// 直近のメッセージが60分以内に投稿されている場合は何もせず終了
if (elapsed < 60 * 60) {
log.info('Skipping postHelloWorld because the latest message was posted less than 60 minutes ago');
return;
}

// 直近のメッセージが60分以上前に投稿されている場合は削除して投稿し直す
log.info('Removing last status message because the latest message was posted more than 60 minutes ago');
await this.#slack.chat.delete({
channel: this.#state.latestStatusMessage.channel,
ts: this.#state.latestStatusMessage.ts,
});
}

const result = await this.#slack.chat.postMessage({
username: this.username,
channel: process.env.CHANNEL_SANDBOX,
Expand All @@ -129,7 +148,7 @@ export class HelloWorld {
this.#state.counter = value;

if (!this.#state.latestStatusMessage) {
log.warn('latestStatusMessage is not set');
log.error('latestStatusMessage is not set');
return;
}

Expand All @@ -143,7 +162,7 @@ export class HelloWorld {
}

// カウンター編集ダイアログを表示する
private async showCounterEditDialog({triggerId}: {triggerId: string}) {
private async showCounterEditDialog({triggerId}: { triggerId: string }) {
log.info('Showing counter edit dialog');

await this.#slack.views.open({
Expand Down
1 change: 1 addition & 0 deletions helloworld/views/counterEditDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default (state: StateObj): View => ({
action_id: 'counter_input',
is_decimal_allowed: false,
min_value: '0',
max_value: '10000000000',
initial_value: state.counter.toString(),
},
label: {
Expand Down
2 changes: 1 addition & 1 deletion helloworld/views/helloWorldMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default (state: StateObj): KnownBlock[] => [
elements: [
{
type: 'plain_text',
text: '⚠この値は再起動後も保存されますが、再起動前に投稿されたメッセージの数字は更新されなくなります。ボタンを押すとエラーが出る場合は、「Slackbotを作ろう」ページの「WebSocketトンネルをセットアップする」などを参考に Event API のセットアップが正常にできているかもう一度確認してください。',
text: '⚠この値は再起動後も保存されます。前回このメッセージを投稿してから60分以上経っている場合は、以前のメッセージが削除され再投稿されます。ボタンを押すとエラーが出る場合は、「Slackbotを作ろう」ページの「WebSocketトンネルをセットアップする」などを参考に Event API のセットアップが正常にできているかもう一度確認してください。',
emoji: true,
},
],
Expand Down
7 changes: 4 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import dotenv from 'dotenv';
dotenv.config();

import Fastify from 'fastify';
import os from 'os';
import qs from 'querystring';
import { eventClient, messageClient, tsgEventClient, webClient } from './lib/slack';

Expand All @@ -23,6 +22,7 @@ import { throttle, uniq } from 'lodash';
import { RequestHandler } from 'express-serve-static-core';
import { inspect } from 'util';
import concat from 'concat-stream';
import { getAuthorityLabel } from './lib/slackUtils';

const log = logger.child({ bot: 'index' });

Expand Down Expand Up @@ -187,9 +187,10 @@ eventClient.on('error', (error) => {
fastify.use('/slack-message', messageClient.requestListener());

const loadedPlugins = new Set<string>();
const authority = getAuthorityLabel();

const initializationMessage = await webClient.chat.postMessage({
username: `tsgbot [${os.hostname()}]`,
username: `tsgbot [${authority}]`,
channel: process.env.CHANNEL_SANDBOX,
text: `起動中⋯⋯ (${loadedPlugins.size}/${plugins.length})`,
attachments: plugins.map((name) => ({
Expand Down Expand Up @@ -246,7 +247,7 @@ eventClient.on('error', (error) => {

log.info('Launched');
webClient.chat.postMessage({
username: `tsgbot [${os.hostname()}]`,
username: `tsgbot [${authority}]`,
channel: process.env.CHANNEL_SANDBOX,
text: argv.startup,
});
Expand Down
8 changes: 6 additions & 2 deletions lib/__mocks__/slackUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-env node, jest */
import {MrkdwnElement, PlainTextElement} from '@slack/web-api';
import type {MessageEvent} from '@slack/bolt';
import { MrkdwnElement, PlainTextElement } from '@slack/web-api';
import type { MessageEvent } from '@slack/bolt';

export const getMemberName = jest.fn(async () => 'Dummy User');
export const getMemberIcon = jest.fn(async () => 'https://example.com/dummy.png');
Expand All @@ -18,4 +18,8 @@ export const mrkdwn = (text: string): MrkdwnElement => ({

export const extractMessage = (message: MessageEvent) => {
return message;
};

export const getAuthorityLabel = () => {
return 'TEST_AUTHORITY';
};
Loading

0 comments on commit 0d997f9

Please sign in to comment.