å§ãã« ããã«ã¡ã¯ããããã£æ°å1幎ç®ã®é«ç°ãšæå±±ã§ãã çŸåšã¯ãžã§ãããŒããŒã·ã§ã³æéäžã§ããããããªéšçœ²ãåã£ãŠããŸãã çŸåšã¯ãæ
å ±ã·ã¹ãã ããŒã ã«é
å±ãããŠããã瀟å
åãã·ã¹ãã ã®éçºãéçšãè¡ã£ãŠããŸãããã®æ¥åã®äžç°ãšããŠäŸ¿å©ããŒã«ãïŒäººã§äœæããã®ã§ãããã«ã€ããŠè©±ããŠãããããšæããŸãïŒSlackBotãäœã£ãŠAWSã«ãããã€ããã人ã¯ãã²èªãã§ã¿ãŠãã ããïŒ èæ¯ ãããã£ã§ã¯åºå€æã«å€åå Žæãšä»äºå
容ãSlackã«æçš¿ããSlackã®ã¹ããŒã¿ã¹ãå€åå°ã«å¿ãããã®ã«å€ãããšããã«ãŒã«ããããŸãããŸããé倿ã«ãSlackãžã®å ±åãå¿
èŠã§ããããã¯ããªã¢ãŒãã¯ãŒã¯ã«ãããã€ããªãããªåãæ¹ã«ããã瀟å¡å
šå¡ããªãã£ã¹ã«ããããã§ã¯ãªãã®ã§ãã©ãã§åããŠãããã仿¥ã®ã¿ã¹ã¯ã¯ã©ããªã£ãŠããããªã©ãå
±æããå¿
èŠãããããã§ãã åŸæ¥ã¯ããã¹ãŠæåã§è¡ããªããã°ãªããªãã£ããããä»ã®åºå€æã®ã¿ã¹ã¯ãšåãããŠè¡ããªããã°ãªãããå¿ããã¡ã ã£ãããäœæ¥ã®è² æ
ã«ãªã£ãŠãããããŸããã ããã§ãæã
æ°äººã¯ãããªç¶æ³ãæç Žãã¹ããå
ã«æããSlackãžã®æçš¿ã»ã¹ããŒã¿ã¹å€æŽãèªååããã·ã¹ãã ã§ããããåºé倿¥œæ¥œãããã®å¶äœã«åãçµã¿ãŸããã åºé倿¥œæ¥œãããšã¯ïŒ åºé倿¥œæ¥œãããšã¯ã以äžã®ããã«ãSlackã®åºéå€å ±åçšãã£ã³ãã«ã«å ±åçšã¹ã¬ãããæ¯æ¥èªåçã«æçš¿ããã ã¡ãã»ãŒãžå
ã«ãããã¿ã³ãæŒãã ãã§Slackãžã®åºå€å ±åãSlackã®ã¹ããŒã¿ã¹å€æŽãè¡ããããšããã·ã¹ãã ã§ãã ããã«ãããå
ã«æããå ±åãã¹ããŒã¿ã¹å€æŽããã¿ã³äžã€ã§å®äºã§ãããããæ¥œã«åºé倿ã®ã¿ã¹ã¯ãããªãããšãã§ããŸãã ãŸãã瀟å
ã§äœ¿çšãããŠããã¹ã±ãžã¥ãŒã«ç®¡çã·ã¹ãã ã®IDãšPWãäºåã«ç»é²ããŠããããšã§ã å ±åå
容ã«åœæ¥ã®æ¥åå
容ãèªåçã«å«ããããšãã§ããŸãã ã·ã¹ãã æ§æ ã¢ãŒããã¯ã㣠æ¬ã·ã¹ãã ã¯ã察象ããããã£ã®ç€Ÿå¡ïŒæ°çŸäººçšåºŠïŒ&ïŒæ¥ã«äžäººåœããïŒåå©çšãããããšãæ³å®ããæ¯èŒçå°èŠæš¡ãªããŒã«ã§ããã§ãã®ã§æ§æã¯ãåŒã³åºããå°ãªãå Žåã«äœã³ã¹ãã§éçšã§ãããµãŒãã¬ã¹ãšããAWS Lambdaäžã«ãããã€ãè¡ããŸããã詳现ãªã¢ãŒããã¯ãã£ã¯ä»¥äžã®éãã§ãâ ãããã€ã«ã¯Serverless FrameworkãçšããŸãããèšå®ãã¡ã€ã«ã¯ä»¥äžã®éãã§ã(äžéšçç¥)â service: attendance-slack frameworkVersion: '3' provider: name: aws runtime: python3.10 region: ap-northeast-1 iam: role: statements: - Effect: Allow Action: - lambda:InvokeFunction - lambda:InvokeAsync Resource: "*" - Effect: Allow # ParameterStoreé¢é£ Action: - ssm:PutParameter - ssm:GetParameter Resource: "*" - Effect: Allow Action: - kms:Decrypt Resource: "*" - Effect: Allow # DynamoDBé¢é£ Action: - dynamodb:UpdateItem - dynamodb:GetItem - dynamodb:DeleteItem Resource: "*" environment: TZ: Asia/Tokyo SERVERLESS_STAGE: ${opt:stage, 'dev'} package: patterns: - "!**" - "function/**" functions: app: # ã¡ã€ã³é¢æ° handler: function.slack_bolt_function.lambda_handler name: attendance-slack-${sls:stage} url: true maximumEventAge: 21600 maximumRetryAttempts: 0 post_attendance_message: # åºå€å ±åçšã¡ãã»ãŒãžæçš¿é¢æ° handler: function.post_attendance_message.lambda_handler name: attendance-slack-post-attendance-message-${sls:stage} maximumEventAge: 21600 maximumRetryAttempts: 0 events: - schedule: cron(0 22 ? * 1-5 *) # å¹³æ¥æ7æ(JST)ã«å®è¡ post_leaving_message: # éå€å ±åçšã¡ãã»ãŒãžæçš¿é¢æ° handler: function.post_leaving_message.lambda_handler name: attendance-slack-post-leaving-message-${sls:stage} maximumEventAge: 21600 maximumRetryAttempts: 0 events: - schedule: cron(50 2 ? * 2-6 *) # å¹³æ¥æ11æ50å(JST)ã«å®è¡ manage_channels: # ãã£ã³ãã«ç®¡ççšé¢æ° handler: function.manage_channels.lambda_handler name: manage-channels-${sls:stage} url: true maximumEventAge: 21600 maximumRetryAttempts: 0 # å€éšããã±ãŒãžãLambdaäžã§åãããã©ã°ã€ã³ plugins: - serverless-python-requirements custom: pythonRequirements: zip: true slim: true useDownloadCache: false useStaticCache: false # DynamoDBã®ã€ã³ã¹ã¿ã³ã¹äœæ resources: Resources: ondemanddb: Type: 'AWS::DynamoDB::Table' Properties: TableName: user AttributeDefinitions: - AttributeName: slack_id AttributeType: S KeySchema: - AttributeName: slack_id KeyType: HASH # ãªã³ããã³ããã£ãã·ãã£ã¢ãŒã BillingMode: PAY_PER_REQUEST Bolt for Pythonã«ããã¢ããªå¶äœ åºé倿¥œæ¥œããã¯Slackã®å
¬åŒã©ã€ãã©ãªã§ãã Bolt for Python ã䜿ã£ãŠäœãããŠããŸããããã«ãã£ãŠãŠãŒã¶ããã®ã¢ã¯ã·ã§ã³(ã¹ã©ãã·ã¥ã³ãã³ãããã¿ã³ã¯ãªãã¯ãªã©)ãšåŠçã®çŽã¥ããéåžžã«ç°¡åã«å®è£
ã§ããŸãã ã¡ãã»ãŒãžçã¢ããªå
ã³ã³ããŒãã³ãã®æåœ¢ã«ã¯ Block Kit Builder ã䜿çšããŠããŸããããšãã°âã®ã¡ãã»ãŒãžãããã¯ã®ãã¡ãã¯ã³ã¯ãªãã¯çã¯ä»¥äžã®ãããªãœãŒã¹ã§ã§ããŠããŸãã blocks = [ { "type": "section", "text": { "type": "mrkdwn", "text": ":syukkin: *ãã¯ããããããŸãïŒ*:syukkin:\nãã¿ã³ãæŒããŠåºå€å ±åããŸãããïŒ", }, }, {"type": "divider"}, {"type": "section", "text": {"type": "mrkdwn", "text": "*ã¯ã³ã¯ãªãã¯ç*"}}, { "type": "actions", "block_id": "place_oneclick_block", "elements": [ { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "æ°å®¿17F"}, "value": "æ°å®¿17F", "action_id": "button_17f", }, { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "æ°å®¿18F"}, "value": "æ°å®¿18F", "action_id": "button_18f", }, { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "åšå®
"}, "value": "åšå®
", "action_id": "button_home", }, { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "暪æµ"}, "value": "暪æµ", "action_id": "button_yokohama", }, { "type": "button", "text": {"type": "plain_text", "emoji": true, "text": "ãã®ä»"}, "value": "ãã®ä»", "action_id": "button_others", }, ], }, {"type": "divider"}, # 詳现çãåæ§ã«... ] ãããSlackã® chat.postMessage APIã«blocksãšããŠæž¡ããŠAPIãå©ãããšã§æå®ã®ãã£ã³ãã«ã«éããŸãããã¿ã³ã®ã¢ã¯ã·ã§ã³ã¯ä»¥äžã®ããã«block_idãšaction_idãæå®ããŠååããå€ã¯action[“value”]ã§åãåããŸãããŸããbodyã«ãªã¯ãšã¹ãæ
å ±ãäžéãå
¥ã£ãŠããŸãã def attendance_oneclick_button_clicked( action: dict, body: dict, client: WebClient ) -> None: # ãŠãŒã¶idååŸ user_id = body["user"]["id"] schedule = "some schedule" # ããã§ã¹ã±ãžã¥ãŒã«ç®¡çããŒã«ããäºå®ããšã£ãŠãã schedule = "\n--ããããš--\n" + schedule msg = f"{body['user']['username']}ãå€åãéå§ããŸããã\nå Žæ: " + action["value"] + schedule # åºå€å ±åã¡ãã»ãŒãžæçš¿ client.chat_postMessage( channel=body["channel"]["id"], text=msg, thread_ts=body["message"]["ts"], username="åºé倿¥œæ¥œãã", ) # ã¹ããŒã¿ã¹å€æŽ place = action["value"] if place == "æ°å®¿17F": status_text = "ãªãã£ã¹ã¯ãŒã¯äž 17F" status_emoji = ":17f:" elif place == "æ°å®¿18F": status_text = "ãªãã£ã¹ã¯ãŒã¯äž 18F" status_emoji = ":18f:" elif place == "åšå®
": status_text = "ãªã¢ãŒãã¯ãŒã¯äž" status_emoji = ":working-from-home:" elif place == "暪æµ": status_text = "ãªãã£ã¹ã¯ãŒã¯äž 暪æµ" status_emoji = ":yokohama:" else: status_text = None status_emoji = None # å€åå Žæãããã®ä»ãã®ãšã以å€ã¯å€æŽãã if status_text and status_emoji: user_id = body["user"]["id"] client.users_profile_set( token=ps.get_parameter("SLACK_USER_TOKEN"), user=user_id, profile={ "status_text": status_text, "status_emoji": status_emoji, "status_expiration": 0, }, ) # Lazy Listenerèšå® app.action({"block_id": "place_oneclick_block", "action_id": re.compile("button_.+")})( ack=just_ack, lazy=[attendance_oneclick_button_clicked] ) Boltã§æ§ç¯ããSlackã¢ããªãLambdaäžã§åããå Žåã¯Lazy Listenerã®èšå®ãå¿
èŠã§ããããã«ã€ããŠã¯å
茩ã ãã§ã«èšäºãæžããŠããã®ã§èŠãŠã¿ãŠãã ãã ããªãã users.profile.set APIãçšããŠä»äººã®ã¹ããŒã¿ã¹å€æŽãè¡ãå Žåããã®äººã® ã¡ã³ããŒçš®å¥ ããäžäœã®ã¡ã³ããŒãçºè¡ãããŠãŒã¶ããŒã¯ã³ãå¿
èŠã§ããä»åã¯ãã©ã€ããªãŒãªãŒããŒæš©éã§æãåºããŠããã£ãŠããŸãã 䜿ã£ãŠã¿ãŠãããäººã®ææ³ ãããã£ã§ã¯Slackã«å人ã®åå ±ãã£ã³ãã«ãäœæããŠã€ã¶ãããçºä¿¡ããæåããããŸããããã«æçš¿ãããŠããææ³ã®äžéšãã玹ä»ããŸãã 奜è©ã§ããïŒ å·¥å€«ããç¹ AWSã«ãããã€ããã«ããããæ¯èŒçå®ãæ§æãæ¡çš èŠæš¡ãèæ
®ããå®å
šãµãŒãã¬ã¹æ§æ 瀟å
ããŒã«ã§ããããŠãŒã¶ãå€ããŠæ°çŸäººçšåºŠãªã®ã§ããµãŒãã¯ç«ãŠãã«ãªã¯ãšã¹ãããŒã¹ã®Lambda+DynamoDBæ§æã«ããŸããã ã·ãŒã¯ã¬ããã®ç®¡çãParameter Storeã§è¡ã ããŒããŒã·ã§ã³æ©èœãèŠããªãããšãªã©ãããSecrets Managerã§ãªãParameter Storeã䜿ãããšã§ã³ã¹ããæããŸããã API Gatewayã§ãªãLambda Function URLsãå©çš HTTPãšã³ããã€ã³ããšããŠLambda Function URLsãçŽæ¥å©çšããããã«ããèªèšŒçã¯Slack SDKã«ä»»ããŠããŸãã éçºå¹çãäžããããã«IaCãçµãã æ¬ãããžã§ã¯ãã§ã¯ãAWSãæ¡çšããŠãããããã³ã³ãœãŒã«ã䜿çšããæåã§ã®ãããã€ã«ã¯æéãå·¥æ°ãããããŸãã ãã®ãããäžèšã®ããã«Serverless Frameworkã䜿çšããŠããããã€ã§ããããã«ããŸããããŸããServerless Frameworkã¯Terraformçãšæ¯ã¹ãŠç°¡åã«èšå®ãæžããã®ã§ãæ°äººãšã³ãžãã¢ãIaCãå§ãããã£ãããšããŠã¯ããšãŠãè¯ãã®ã§ã¯ãªãã§ããããã èŠåŽããç¹ AWS, SlackAPIåå¿è
ïŒäººã§ã®èšèšã»éçº ç§ãã¡ã¯ã©ã¡ããAWSãSlackAPIãçšããéçºã®çµéšããªãã£ãããã仿§ã®æ€èšã«ã¯æéãããããŸããã AWSã§ããã°ãDBã®éžæãèšèšãã·ãŒã¯ã¬ããã®ç®¡çæ¹æ³ããã®ä»å¿
èŠãªãµãŒãã¹ã®åæšéžæãªã©ã«èŠåŽããŸãããããããªãç¹ã¯ç€Ÿå
åå ±ãã£ã³ãã«ã§ãŒãããŠãããšå
茩æ¹ãæããŠãããããããšã¯ChatGPTã«çžè«ãããããŠæ±ºããŠãããŸãããGPT4ã¯æåŒ·ã§ããã ïŒäœè«ã§ãããåŒç€Ÿã§ã¯äžèšã®ããã«Slackã§ãã€ã§ã質åå¯èœãªAIãåšäžããŠããŸãã詳现㯠ãã¡ã ïŒ ãŸãSlackAPIã§ã¯ãTokenã®æš©éãTier(1ååœããã®åŒã³åºãå¯èœåæ°)ãã€ã³ã¿ã©ã¯ã·ã§ã³ã®å®è£
æ¹æ³(ã·ã§ãŒãã«ãããã¹ã©ãã·ã¥ã³ãã³ããªã©)ã®æ€èšã«æ©ãŸãããŸããã ãžã§ãããŒãã«ããéçºæéã®å¶çŽ 9æäžæ¬ã«ã¯ä»ã®éšçœ²ãžç§»ããããéçºã¯ãã¡ãããäŒç»ã仿§ã®æ±ºå®ã諞ã
ã®èª¿æŽãªã©ãå«ããŠ3ãæã§éçºããªããŠã¯ãªããŸããã§ããããŸãå
è¿°ã®éãç§ãã¡ã«ã¯æè¡ã調ã¹ãæéãå¿
èŠã ã£ããããããŸãããšãé²ããŠããã®ã倧å€ã§ããããå
茩ã«é Œã£ãããIaCã«ããå¹çåãªã©ã«ããäœãšãä¹ãåããŸããã ãŸãšã ä»åã¯åºé倿ã®ã¿ã¹ã¯ãèªååããSlackã¢ããªãäœæããŸããã ä»åããµãããšãAWSãBoltã䜿çšããéçºã¯åããŠã§ãããã3ãæãšããçãæéã®äžã§ã²ãšã€åœ¢ã«ã§ããã®ã§ããã£ãã§ããå
ã»ã©ç޹ä»ããææ³ãå«ããããŒã ã®æ¹ã
ãã¿ããªæ¥œã«ãªã£ããšèšã£ãŠãããŠããããã£ãã§ãã ããã¿ãªããã«ãããããã£ãSlackã§ã®æ¯æ¥ã®ã¿ã¹ã¯ããããªããç§ãã¡ã®ããã«èªååãèããŠã¿ãŠã¯ãããã§ããããã We are hiring! ãããã£ã§ã¯ãããŸããŸãªãããã¯ããžææŠãããšã³ãžãã¢ãçµ¶è³åéäžã§ãïŒ ãèå³ã®ããæ¹ã¯ä»¥äžã®æ¡çšãµã€ããããæ°è»œã«ãé£çµ¡ãã ããïŒ ãããã£æ ªåŒäŒç€Ÿæ¡çšæ
å ± ã«ãžã¥ã¢ã«é¢è«ãåãä»ããŠããŸãïŒ ã«ãžã¥ã¢ã«é¢è« Tech TalkãMeetUpãéå¬ããŠãããŸãïŒ ãã¡ãããæ°è»œã«ãå¿åãã ããïŒ Event – NIFTY engineering ãããã£æ ªåŒäŒç€Ÿ – connpass