search
LoginSignup
0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

ぷりぷりあぷりけーしょんず Advent Calendar 2020 Day 10

posted at

朝活が辛いので推しに応援してもらいながら朝活頑張るbotを作ったよ

この記事はぷりぷりあぷりけーしょんず Advent Calendar 2020の10日目を穴埋めする記事です。

朝活をしたいけど寒くて辛い

最近、仕事前の切り替えスイッチにと朝活を始めようと決心したのですが見事に3日坊主で終わりました。
本当にきっかり3日で終わった。ことわざ?凄い!

どんな物事が継続できるか?

こんな私でも今月頭からしっかり継続できていることがあります。
それがFitBoxing2です

fitboxing.jpeg

毎日30分ほどのキツいコースを可愛い女の子(orイケメン)インストラクターが褒めたり応援してくれたりしながらこなしていくゲームです。

つまり

  • 寒い
  • 誰も褒めてくれない
  • 誰も応援してくれない
  • 寒い

大体こんな理由でやめてしまった朝活も可愛い女の子が褒めてくれたり応援してくれたら継続できるような予感がします。

朝活botを作ったよ

Slackに朝活チャンネルを作り、毎朝褒めたり応援したりしてくれるbotを作りました。

  • SlackのEventAPIでLambdaを発火
    • channelにjoinしたらユーザー登録
    • botにメンションしたら朝活記録を登録
    • channelから去ったらユーザー削除
  • CloudWatch Eventsで毎朝応援通知

データの格納先はDynamoDB、AWSの各サービスはCDKで管理していきます。

bot

日本のKAWAIIの代表といえば、そうこの方ですね

asakatsu-bot2.png

Slack Appからアプリを作成しBotイメージに上記の写真を設定し、この辺りの権限を付与しておいてあげます。

権限.png

CDK

AWS CDKで各サービスを定義していきます。

Lambda

今回は弊コミュニティの素晴らしいエンジニアが記事にしていたNodejsFunctionを使ってLambdaを定義します。

const handler = new NodejsFunction(this, 'asakatsu-handler', {
    functionName: `${appName}-handler`,
    timeout: Duration.seconds(10),
    runtime: lambda.Runtime.NODEJS_12_X,
    initialPolicy: [
        new iam.PolicyStatement({
            actions: ['dynamodb:*'],
            effect: iam.Effect.ALLOW,
            resources: [
                Stack.of(this).formatArn({
                    service: 'dynamodb',
                    resource: 'table',
                    resourceName: 'Asakatsu*'
                })
            ]
        }),
        new iam.PolicyStatement({
            actions: ["ssm:GetParameter"],
            effect: iam.Effect.ALLOW,
            resources: [
                Stack.of(this).formatArn({
                    service: "ssm",
                    resource: "parameter",
                    resourceName: "asakatsu-bot/*",
                }),
            ],
        }),
    ]
});

NodejsFunctionで定義するとランタイムがなぜかデフォルトでNODEJS_10_Xになってしまうので最新のNODEJS_12_Xを指定しておきます。

今までは定義した関数のファイルの場所を指定しなければなりませんでしたが

(experimental) Path to the entry file (JavaScript or TypeScript).
If the NodejsFunction is defined in stack.ts with my-handler as id (new NodejsFunction(this, 'my-handler')), the construct will look at stack.my-handler.ts and stack.my-handler.js.

とのことなのでasakatsu-bot-stack.asakatsu-handler.tsという少々長ったらしいファイルではありますがそこにLambda関数を用意してあげることにします。

また、最新のNodejsFunctionはesbuildを使ってビルドを行なっていますがローカルにesbuildがないととんでもないサイズのdockerイメージでビルドされてしまうので予めnpm install --save-dev esbuildしておきます。

$ docker image list
REPOSITORY                                                             TAG                 IMAGE ID            CREATED             SIZE
cdk-baf5f3dcfbe97271b31c2605b638d0610712bf0d176333d75863abaeb2ec6ddb   latest              e51cd2a7cb5e        4 days ago          1.9GB
amazon/aws-sam-cli-build-image-nodejs12.x                              latest              2eada5d9f2c4        2 weeks ago         1.87GB

DynamoDB

続いてDynamoDBの定義です。
今回NodejsFunctionの恩恵でLambdaのhandlerと一緒にビルドしてもらえることになったのでここに定義したテーブルの仕様をCDKとhandlerで二重管理しなくて良いようになりました。

import * as cdk from '@aws-cdk/core';
import {RemovalPolicy} from '@aws-cdk/core';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
import {AttributeType} from '@aws-cdk/aws-dynamodb';
import {TableOptions} from '@aws-cdk/aws-dynamodb/lib/table';

interface TableProps extends TableOptions {
    readonly tableName: string;
}

interface DynamoDBTable {
    readonly id: string;
    readonly tableProps: TableProps;
}

export const User: DynamoDBTable = {
    id: 'asakatsu-user',
    tableProps: {
        tableName: 'AsakatsuUser',
        partitionKey: {
            name: 'id',
            type: AttributeType.STRING
        },
        removalPolicy: RemovalPolicy.DESTROY
    }
}

export const Achievement: DynamoDBTable = {
    id: 'asakatsu-achievement',
    tableProps: {
        tableName: 'AsakatsuAchievement',
        partitionKey: {
            name: 'date',
            type: AttributeType.STRING
        },
        sortKey: {
            name: 'user_id',
            type: AttributeType.STRING
        },
        removalPolicy: RemovalPolicy.DESTROY
    },
}

export function createTable(stack: cdk.Stack): void {
    const tables: DynamoDBTable[] = [User, Achievement];
    tables.forEach(table => {
        new dynamodb.Table(stack, table.id, table.tableProps);
    });
}

例えばユーザー登録でDynamoDBを呼び出すときはここで定義しているtableNameをそのまま使うことができます。
(おそらくもっとちゃんとテーブル定義を作り込むことはできそうですが今回はざっくりこんな感じで)

import {DB} from './common';
import {User} from '../dynamodb';

export async function register(userId: string): Promise<void> {
    try {
        await DB.put({
            TableName: User.tableProps.tableName,
            Item: {
                id: userId
            }
        }).promise();
    } catch (e) {
        throw e;
    }
}

その他

その他APIGatewayやCloudWatchEventsなどの定義ですが特筆する点もないのでざっくりこんな感じなんだなーと見てもらえればと思います。


// cloud watch events
const lambdaTarget = new eventsTarget.LambdaFunction(handler);
new events.Rule(this, `${appName}-event`, {
    ruleName: `${appName}`,
    targets: [lambdaTarget],
    schedule: events.Schedule.cron({
        minute: '30',
        hour: '21',
    })
});

// api gateway
const api = new apigateway.RestApi(this, `${appName}-gw`, {
    restApiName: `${appName}-gw`
});

const integration = new apigateway.LambdaIntegration(handler, {
    proxy: true
});

api.root.addResource(`${appName}`)
    .addMethod('POST', integration);

EventHookされるスクリプト

上述通り、今回はAWSLambdaがEvent発火をトリガーに実行されます。
LambdaはNodejsFunctionの定義の簡易さの恩恵を最大に活かすため、コントローラとなるhandlerだけの定義を中心に以下のようなdir構成にしました。

.
├── asakatsu-bot-stack.asakatsu-handler.ts
├── asakatsu-bot-stack.ts
├── dynamodb.ts
└── lambda
    ├── common.ts
    ├── index.ts
    ├── notify-handler.ts
    ├── register-handler.ts
    ├── remove-handler.ts
    └── report-handler.ts
asakatsu-bot-stack.asakatsu-handler.ts
import {
    Response,
    SUCCESS_RESPONSE,
    report,
    register,
    remove, notify,
} from './lambda';


function challenge(challenge: string): Response {
    return {
        'isBase64Encoded': false,
        'statusCode': 200,
        'headers': {'Content-type': 'application/json'},
        'body': JSON.stringify({'challenge': challenge})
    };
}

export async function handler(event: any): Promise<Response> {
    if (event.source == 'aws.events') {
        await notify();
        return SUCCESS_RESPONSE;
    }

    const body = JSON.parse(event.body);

    if (body.challenge) {
        return challenge(body.challenge);
    }

    switch (body.event.type) {
        case 'member_joined_channel':
            await register(body.event.user);
            break;
        case 'member_left_channel':
            await remove(body.event.user);
            break;
        case 'app_mention':
            await report(body.event.user, body.event.text);
            break;
    }

    return SUCCESS_RESPONSE;
}

Lambda実行時に受け取るeventに合わせて呼び出すhandlerを指定するようにしました。
小さなAPIを作るのであれば管理が統一されるのとイベントが増えるたびに上記のhandlerに追加していくだけで良いので楽そうです。

各handlerの詳細についてはDynamoDBにストアする、Slackで応援メッセージを通知するなどのロジックになるので今回は割愛します。

完成したよ。

cdk deployをし、SlackAppの設定をworkspaseのchannelにしたら完成です。

asakatsu-pamyupamyu.png

できました。これで明日から毎朝ぱみゅぱみゅが応援してくれるのでどんなに寒くて辛くてもお布団から脱出し朝活ができそうです。好きな人に嘘なんてつけないですからね。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0
Help us understand the problem. What are the problem?