Skip to content

Commit

Permalink
Merge pull request #941 from tsg-ut/helloworld
Browse files Browse the repository at this point in the history
Implement helloworld bot
  • Loading branch information
hakatashi authored Oct 9, 2024
2 parents 2b0bafe + 7eb44e6 commit 0dba2c1
Show file tree
Hide file tree
Showing 48 changed files with 58,783 additions and 58,066 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ TWITTER_ACCESS_TOKEN_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# checkin
SWARM_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

# channel-notifier, tahoiya, wordle-battle
# channel-notifier, tahoiya, wordle-battle, topic
USER_TSGBOT=UXXXXXXXX

# deploy
Expand Down
2 changes: 1 addition & 1 deletion achievement-quiz/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ beforeEach(() => {
describe('response to /^実績当てクイズ$/', () => {
it('starts game by "実績当てクイズ"', async () => {
const response = await slack.getResponseTo('実績当てクイズ');
expect(response.username).toBe('実績当てクイズ');
expect('username' in response && response.username).toBe('実績当てクイズ');
expect(response.text).toContain('この実績なーんだ');
});
});
7 changes: 4 additions & 3 deletions achievements/index_production.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ beforeEach(() => {

describe('achievements', () => {
it('unlock chat achievement when chat is posted', async () => {
const {text, username} = await slack.getResponseTo('hoge');
expect(username).toBe('achievements');
expect(text).toContain('はじめまして!');
const response = await slack.getResponseTo('hoge');
// eslint-disable-next-line no-restricted-syntax
expect('username' in response && response.username).toBe('achievements');
expect(response.text).toContain('はじめまして!');
});
});
27 changes: 17 additions & 10 deletions atequiz/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { WebAPICallOptions, WebClient } from '@slack/web-api';
import { ChatPostMessageArguments, WebClient } from '@slack/web-api';
import type { EventEmitter } from 'events';
import { SlackInterface } from '../lib/slack';
import { ChatPostMessageArguments } from '@slack/web-api/dist/methods';
import assert from 'assert';
import { Mutex } from 'async-mutex';
import { Deferred } from '../lib/utils';
Expand Down Expand Up @@ -70,7 +69,7 @@ export class AteQuiz {
state: AteQuizState = 'waiting';
replaceKeys: { correctAnswerer: string } = { correctAnswerer: '[[!user]]' };
mutex: Mutex;
postOption: WebAPICallOptions;
postOption: Partial<ChatPostMessageArguments>;
threadTsDeferred: Deferred<string> = new Deferred();

judge(answer: string, _user: string): boolean {
Expand Down Expand Up @@ -119,7 +118,7 @@ export class AteQuiz {
constructor(
{ eventClient, webClient: slack }: SlackInterface,
problem: AteQuizProblem,
option?: WebAPICallOptions
option?: Partial<ChatPostMessageArguments>,
) {
this.eventClient = eventClient;
this.slack = slack;
Expand All @@ -138,8 +137,7 @@ export class AteQuiz {
async repostProblemMessage() {
const threadTs = await this.threadTsDeferred.promise;
return this.slack.chat.postMessage({
...this.problem.problemMessage,
...this.postOption,
...Object.assign({}, this.problem.problemMessage, this.postOption),
thread_ts: threadTs,
reply_broadcast: true,
});
Expand Down Expand Up @@ -187,13 +185,17 @@ export class AteQuiz {
} else {
this.state = 'unsolved';
await postMessage(
Object.assign({}, this.problem.unsolvedMessage, { thread_ts })
Object.assign(
{},
this.problem.unsolvedMessage,
{ thread_ts, reply_broadcast: true },
)
);

const answerMessage = await this.answerMessageGen();
if (this.problem.answerMessage) {
if (answerMessage) {
await postMessage(
Object.assign({}, this.problem.answerMessage, { thread_ts })
Object.assign({}, answerMessage, { thread_ts })
);
}
clearInterval(tickTimer);
Expand All @@ -204,6 +206,7 @@ export class AteQuiz {
};

this.eventClient.on('message', async (message) => {
const thread_ts = await this.threadTsDeferred.promise;
if (message.thread_ts === thread_ts) {
if (message.subtype === 'bot_message') return;
if (_option.mode === 'solo' && message.user !== _option.player) return;
Expand All @@ -216,7 +219,11 @@ export class AteQuiz {
clearInterval(tickTimer);

await postMessage(
Object.assign({}, await this.solvedMessageGen(message), { thread_ts })
Object.assign(
{},
await this.solvedMessageGen(message),
{ thread_ts, reply_broadcast: true },
)
);

const answerMessage = await this.answerMessageGen(message);
Expand Down
16 changes: 11 additions & 5 deletions auto-archiver/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ describe('auto-archiver', () => {
'引き続きこのチャンネルを使用しますか?',
].join('\n'));
expect(mockedPostMessage.mock.calls[0][0].channel).toBe(FAKE_CHANNEL);
expect(mockedPostMessage.mock.calls[0][0].blocks).toHaveLength(3);
// eslint-disable-next-line no-restricted-syntax
const blocks = 'blocks' in mockedPostMessage.mock.calls[0][0] ? mockedPostMessage.mock.calls[0][0].blocks : [];
expect(blocks).toHaveLength(3);

const MockedState = State as MockedStateInterface<StateObj>;
const state = MockedState.mocks.get('auto-archiver_state');
Expand Down Expand Up @@ -218,8 +220,10 @@ describe('auto-archiver', () => {
].join('\n'));
expect(mockedUpdateMessage.mock.calls[0][0].channel).toBe(FAKE_CHANNEL);
expect(mockedUpdateMessage.mock.calls[0][0].ts).toBe(FAKE_TIMESTAMP);
expect(mockedUpdateMessage.mock.calls[0][0].blocks).toHaveLength(3);
expect((mockedUpdateMessage.mock.calls[0][0].blocks[1] as SectionBlock).text.text).toBe('<@U12345678>の回答: *使用しない*');
// eslint-disable-next-line no-restricted-syntax
const blocks = 'blocks' in mockedUpdateMessage.mock.calls[0][0] ? mockedUpdateMessage.mock.calls[0][0].blocks : [];
expect(blocks).toHaveLength(3);
expect((blocks[1] as SectionBlock).text.text).toBe('<@U12345678>の回答: *使用しない*');
expect(slack.webClient.conversations.archive).toBeCalledWith({
channel: FAKE_CHANNEL,
});
Expand Down Expand Up @@ -284,8 +288,10 @@ describe('auto-archiver', () => {
].join('\n'));
expect(mockedUpdateMessage.mock.calls[0][0].channel).toBe(FAKE_CHANNEL);
expect(mockedUpdateMessage.mock.calls[0][0].ts).toBe(FAKE_TIMESTAMP);
expect(mockedUpdateMessage.mock.calls[0][0].blocks).toHaveLength(3);
expect((mockedUpdateMessage.mock.calls[0][0].blocks[1] as SectionBlock).text.text).toBe('<@U12345678>の回答: *使用する*');
// eslint-disable-next-line no-restricted-syntax
const blocks = 'blocks' in mockedUpdateMessage.mock.calls[0][0] ? mockedUpdateMessage.mock.calls[0][0].blocks : [];
expect(blocks).toHaveLength(3);
expect((blocks[1] as SectionBlock).text.text).toBe('<@U12345678>の回答: *使用する*');

const MockedState = State as MockedStateInterface<StateObj>;
const state = MockedState.mocks.get('auto-archiver_state');
Expand Down
4 changes: 0 additions & 4 deletions bungo-quiz/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,10 @@ export default ({ eventClient, webClient: slack }: SlackInterface) => {
solvedMessage: {
channel,
text: typicalMessageTextsGenerator.solved(` *${title}* (${author}) `),
reply_broadcast: true,
},
unsolvedMessage: {
channel,
text: typicalMessageTextsGenerator.unsolved(` *${title}* (${author}) `),
reply_broadcast: true,
},
answerMessage: { channel, text: cardURL },
correctAnswers: [title],
Expand Down Expand Up @@ -148,12 +146,10 @@ export default ({ eventClient, webClient: slack }: SlackInterface) => {
solvedMessage: {
channel,
text: typicalMessageTextsGenerator.solved(` *${author}* (${title}) `),
reply_broadcast: true,
},
unsolvedMessage: {
channel,
text: typicalMessageTextsGenerator.unsolved(` *${author}* (${title}) `),
reply_broadcast: true,
},
answerMessage: { channel, text: cardURL },
correctAnswers: author.split(" "),
Expand Down
2 changes: 0 additions & 2 deletions city-symbol/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,12 +391,10 @@ export default (slackClients: SlackInterface) => {
solvedMessage: {
channel: message.channel,
text: typicalMessageTextsGenerator.solved(` *${city.prefectureName}${city.cityName}* `),
reply_broadcast: true,
},
unsolvedMessage: {
channel: message.channel,
text: typicalMessageTextsGenerator.unsolved(` *${city.prefectureName}${city.cityName}* `),
reply_broadcast: true,
},
get answerMessage() {
const aiHints = aiHintsLoader.get() ?? [];
Expand Down
1 change: 0 additions & 1 deletion dicebot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ class DiceBot {
channel: channel,
username: "dicebot",
icon_emoji: ":game_die:",
icon_url: "",
text: text,
thread_ts: thread
});
Expand Down
4 changes: 1 addition & 3 deletions hayaoshi/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Mutex} from 'async-mutex';
// @ts-expect-error
// @ts-expect-error: not typed
import levenshtein from 'fast-levenshtein';
import {sample, shuffle, flatten, times, constant} from 'lodash';
import type {SlackInterface} from '../lib/slack';
Expand Down Expand Up @@ -80,8 +80,6 @@ export default ({eventClient, webClient: slack}: SlackInterface) => {
await slack.chat.update({
channel: process.env.CHANNEL_SANDBOX,
text: `問題です!\nQ. ${getQuestionText(state.question, state.hintCount)}\n\n⚠3回間違えると失格です!\n⚠「?」でメッセージを始めるとコメントできます`,
username: 'hayaoshi',
icon_emoji: ':question:',
ts: state.thread,
});
} else {
Expand Down
5 changes: 5 additions & 0 deletions helloworld/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "@hakatashi/eslint-config/typescript",
"rules": {
}
}
132 changes: 132 additions & 0 deletions helloworld/HelloWorld.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/* eslint-disable import/imports-first, import/first */
/* eslint-env jest */

jest.mock('../lib/slackUtils');
jest.mock('../lib/state');
jest.mock('os', () => ({
hostname: () => 'test-hostname',
release: () => 'test-release',
}));
jest.mock('crypto', () => ({
randomUUID: () => 'test-uuid',
}));


import type {MockedStateInterface} from '../lib/__mocks__/state';
import Slack from '../lib/slackMock';
import State from '../lib/state';
import {HelloWorld, type StateObj} from './HelloWorld';

let slack: Slack = null;
let helloWorld: HelloWorld = null;

const MockedState = State as MockedStateInterface<StateObj>;

beforeEach(async () => {
slack = new Slack();
process.env.CHANNEL_SANDBOX = slack.fakeChannel;

helloWorld = await HelloWorld.create(slack);
});

describe('helloworld', () => {
it('responds to "Hello"', async () => {
const response = await slack.getResponseTo('Hello');
expect(response).toEqual({
username: 'helloworld [test-hostname]',
channel: slack.fakeChannel,
text: 'World!',
});
});

it('can post Hello world message', async () => {
const postMessage = slack.webClient.chat.postMessage as jest.MockedFunction<typeof slack.webClient.chat.postMessage>;
postMessage.mockResolvedValueOnce({
ok: true,
ts: slack.fakeTimestamp,
channel: slack.fakeChannel,
});

await helloWorld.postHelloWorld();

const state = MockedState.mocks.get('helloworld');
expect(state.counter).toBe(0);
expect(state.uuid).toBe('test-uuid');
expect(state.latestStatusMessage).toEqual({
ts: slack.fakeTimestamp,
channel: slack.fakeChannel,
});

const mockedPostMessage = slack.webClient.chat.postMessage as jest.MockedFunction<typeof slack.webClient.chat.postMessage>;
expect(mockedPostMessage).toBeCalledWith({
username: 'helloworld [test-hostname]',
channel: slack.fakeChannel,
text: 'Hello, World!',
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: 'Hello, World!',
emoji: true,
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'おめでとうございます、TSGのSlackbotの開発環境のセットアップが完了しました! :tada::tada::tada:\n以下のボタンをクリックして、Event API が正常に動作しているか確認してください。',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '現在のカウンター: *0*',
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: '+1',
emoji: true,
},
action_id: 'helloworld_test-uuid_increment_1_button',
},
{
type: 'button',
text: {
type: 'plain_text',
text: '編集する',
emoji: true,
},
action_id: 'helloworld_test-uuid_edit_button',
},
],
},
{
type: 'context',
elements: [
{
type: 'plain_text',
text: '⚠この値は再起動後も保存されますが、再起動前に投稿されたメッセージの数字は更新されなくなります。ボタンを押すとエラーが出る場合は、「Slackbotを作ろう」ページの「WebSocketトンネルをセットアップする」などを参考に Event API のセットアップが正常にできているかもう一度確認してください。',
emoji: true,
},
],
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'このBOTは、Slackbot開発時のみ使用されるBOTで、本番環境では無効化されています。このBOTはSlackbotの動作確認に使えるほか、新しいBOTを開発する際の雛形として利用することもできます。',
},
},
],
});
});
});

Loading

0 comments on commit 0dba2c1

Please sign in to comment.