ãã®èšäºã¯ RevComm Advent Calendar 2022 ã® 22 æ¥ç®ã®èšäºã§ãã ã¯ããã« ããã«ã¡ã¯ãããã¯ãšã³ããšã³ãžãã¢ã®ãŸã€ã©ãããããã§ãã ç§ã¯æè¿ Rust ã§ Slackbot ãäœã£ãŠããŸãã ãã®Botã¯ã @Slackå ++ ãšæçš¿ãããšã Thank you @Slackå (counter: 1) ãšè¿çããŠãããŸãã ãŸããããäžåºŠ @Slackå ++ ãšæçš¿ãããšã Thank you @Slackå (counter: 2) ãšãªããæè¬ãããæ°ã ã counter ãå¢ããŠãããŸãã ãã® Bot ã¯åŒç€Ÿã®å¶åºŠã§ãã 15ïŒ
ã«ãŒã«ã䜿ã£ãŠéçºããŸããã 15ïŒ
ã«ãŒã«ãšã¯ãæ¥åå€ã®æŽ»åãžã®ç©æ¥µçãªé¢äžãæšå¥šããããã«ãæ¥åæéã®ãã¡ 15% ã¯ã³ã¢æ¥åå€ã®æŽ»åã«åãçµãã§è¯ããšããã«ãŒã«ã§ãã ãã®ã«ãŒã«ã®ãããã§ãæ¥ã
ãæè¡åã®å¹
ãåºããããšãã§ããŠããŸãã ä»åã¯ãSlackbot ã«ãªãŠã è¿ããããŠããããšãããŸã§ãèšäºã«ããŠãããŸãã ã¢ãããŒã·ã§ã³ äžã®ãã人ã®ããã«è¡åãããšããã«ã«ãã£ãŒãããã«æ ¹ä»ãããã ãã«ãªã¢ãŒãã®ç°å¢ã§ãæè¬ã®æ°æã¡ãæ°è»œã«è¡šçŸã§ããããã«ããã ãšããæãããéçºããŸããã ã€ã³ã¹ãã€ã¢ ãã® Bot ã¯ãPyConJP Staff çšã® Slack ã«å°å
¥ãããŠããŠãæåããã²ç€Ÿå
ã«ãåãå
¥ããããšæãéçºãã¯ãããŸããã PyConJP 2022 ã§ã玹ä»ãããŠããã®ã§ãã²ã芧ã«ãªã£ãŠã¿ãŠãã ããã https://2022.pycon.jp/timetable?id=ELUNPR æ§æ æ¥åã§ã¯äž»ã« Python ã䜿ã£ãŠããŸãããè§Šã£ãŠã¿ãããšããçç±ã§ Rust ãæ¡çšããŠããŸãã ä»åã¯ã詊ãã®ãããEC2 ã«ç°å¢ãæ§ç¯ããŸãã EC2 (Amazon Linux) Rust Axum ä»åãåºæ¥äžããã³ãŒãã¯ãã¡ãã§ãã Cargo.toml [package] name = "slackbot" version = "0.1.0" edition = "2021" publish = false [dependencies] axum = "0.6.1" tokio = { version = "1.0", features = ["full"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = [ "std", "env-filter" ] } dotenv = "0.15.0" reqwest = { version = "0.11", features = ["json"] } .env VERIFICATION_TOKEN= BOT_USER= BOT_USER_OAUTH_TOKEN= main.rs use axum :: { http :: StatusCode, routing :: {get, post}, Router, }; use std :: net :: SocketAddr; use dotenv :: dotenv; mod slackbot ; use crate :: slackbot :: slackbot; #[tokio::main] async fn main () { init ().await; let app = Router :: new () . route ( "/" , get (handler)) . route ( "/slackbot" , post (slackbot)); let addr = SocketAddr :: from (([ 127 , 0 , 0 , 1 ], 8080 )); tracing :: debug! ( "listening on {}" , addr); axum :: Server :: bind ( & addr) . serve (app. into_make_service ()) .await . unwrap (); } async fn init () { tracing_subscriber :: fmt () . with_max_level ( tracing :: Level :: DEBUG) . init (); dotenv (). ok (); } async fn handler () -> (StatusCode, String ) { ( StatusCode :: OK, String :: from ( "Hello, World!" )) } slackbot.rs use std :: env; use axum :: { http :: StatusCode, Json}; use serde :: {Deserialize, Serialize}; #[derive( Debug , Deserialize)] pub struct Request { token: String , challenge: Option < String > , event: Option < Event > , } impl Request { fn is_initialize ( & self ) -> bool { self .challenge. is_some () } } #[derive( Debug , Deserialize)] struct Event { channel: String , user: String , text: String , thread_ts: Option < String > , } impl Event { fn from_bot ( & self , bot_user: String ) -> bool { self .user == bot_user } } #[derive( Debug , Serialize)] struct PostBody { text: String , channel: String , thread_ts: Option < String > , } #[derive( Debug , Serialize)] pub struct Response { ok: bool , challenge: Option < String > , } pub async fn slackbot ( Json (req): Json < Request > ) -> (StatusCode, Json < Response > ) { tracing :: info! ( "slackbot" ); // Validate let verification_token = env :: var ( "VERIFICATION_TOKEN" ). expect ( "VERIFICATION_TOKEN must be set" ); if req.token != verification_token { tracing :: warn! ( "AuthenticationFailed, token: {}" , req.token); let res = Json (Response { ok: false , challenge: None , }); return ( StatusCode :: BAD_REQUEST, res); } // Verify from Slack if req. is_initialize () { let res = Json (Response { ok: true , challenge: req.challenge, }); return ( StatusCode :: OK, res); } // Validate match & req.event { Some (event) => { let bot_user = env :: var ( "BOT_USER" ). expect ( "BOT_USER must be set" ); if event. from_bot (bot_user) { tracing :: debug! ( "From happy bot" ); let res = Json (Response { ok: false , challenge: None , }); return ( StatusCode :: BAD_REQUEST, res); } } None => { let res = Json (Response { ok: false , challenge: None , }); return ( StatusCode :: BAD_REQUEST, res); } } // Execute let event = req.event. unwrap (); let post_body = PostBody { text: event.text, channel: event.channel, thread_ts: event.thread_ts. map ( | x | x), }; post_request (post_body).await; let res = Json (Response { ok: true , challenge: None , }); return ( StatusCode :: OK, res); } async fn post_request (post_body: PostBody) -> reqwest :: Result < () > { tracing :: info! ( "slackbot__post_request" ); let url = "https://slack.com/api/chat.postMessage" ; let bot_user_oauth_token = env :: var ( "BOT_USER_OAUTH_TOKEN" ). expect ( "BOT_USER_OAUTH_TOKEN must be set" ); let client = reqwest :: Client :: new (); let response = client . post (url) . header ( reqwest :: header :: AUTHORIZATION, format! ( "Bearer {}" , bot_user_oauth_token), ) . json ( & post_body) . send () .await ? ; Ok (()) } æºåâ Slack API ã® App äœæ 1/4 App äœæ Slack api ã® Your Apps ( https://api.slack.com/apps?new_app=1 ) ã® Create New App ãã App ãäœæããŸãã ä»å㯠From scratch ããäœæããŸããã 2/4 Token ååŸ ååŸãã Token 㯠.env ãã¡ã€ã«ã«èšèŒããŸãã Basic Information ( https://api.slack.com/apps? ) > App Credentials ã® Verification Token ãååŸããŸãã OAuth & Permissions ( https://api.slack.com/apps/XXXXXXXXXXXX/oauth? ) ã® Reinstall to Workspace ãããã£ã³ãã«ã«ã€ã³ã¹ããŒã«ããŸãã Bot User OAuth Token ãçŸããã®ã§ååŸããŸãã 3/4 æš©éã®èšå® OAuth & Permissions ( https://api.slack.com/apps/XXXXXXXXXXXX/oauth? ) ã® Scope Bot Token Scopes ãš EventSubscriptions ( https://api.slack.com/apps/XXXXXXXXXXXX/event-subscriptions? ) ã® Subscribe to bot events ã« å¿
èŠãªæš©éã远å ããŸãã 4/4 Slack ãã£ã³ãã«ãžã®è¿œå å°å
¥ããã Slack ãã£ã³ãã«ã«è¡ããSlack ãã£ã³ãã«åãã¯ãªãã¯ããã€ã³ãã°ã¬ãŒã·ã§ã³ããã¢ããªã远å ããŸãã å®è£
â Rust ã®ã€ã³ã¹ããŒã«ãš Rust ã§ POST ãåãåããããã«èšå® 1/5 Rust ãã€ã³ã¹ããŒã« rustup ã䜿ã£ãŠã€ã³ã¹ããŒã«ããŸãã詳现㯠å
¬åŒããã¥ã¡ã³ã ãåç
§ããŠãã ããã 2/5 Axum ã® Hello world Axum 㯠Rust ã® Web ãã¬ãŒã ã¯ãŒã¯ã®ã²ãšã€ã§ããAxum ãè±å¯ã« example ãæäŸããŠãããŠããã®ã§ããããå
ã«ã㊠Bot ã®éçºãããŠãããŸãã ãŸãã¯ãæå°æ§æã§ Hello World ããã£ãŠã¿ãŸãã https://github.com/tokio-rs/axum/tree/main/examples/hello-world ãclone Cargo.toml ã® axum ã axum = "0.6.1" ã«å€æŽ port ã 8080 ã«å€æŽ (Optional) REST API ã«ãããã®ã§ hundler ãä¿®æ£ (Optional) cargo run ã§ãµãŒããŒãç«ã¡äžããŸã curl localhost:8080 ãå©ããšã Hello, World! ãè¿ã£ãŠããŸã main.rs - use axum::{response::Html, routing::get, Router}; + use axum::{http::StatusCode, routing::get, Router}; use std::net::SocketAddr; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(handler)) - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); println!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } - async fn handler() -> Html<&'static str> { - Html("<h1>Hello, World!</h1>") + async fn handler() -> (StatusCode, String) { + (StatusCode::OK, String::from("Hello, World!")) } 3/5 challenge ãè¿ã Slack API ã« URL ãç»é²ããéã«ãæå¹ãª URL ã ãš Slack ã«ç¥ãããå¿
èŠããããŸãã POST ã§ verification_token ãš challenge ãšããã©ã³ãã ãªå€ãéãããŠããã®ã§ããã®ãŸãŸ challenge ãè¿ããŠãããããšã§æå¹ã ãšå€æãããŸãã serde ã¯ã¬ãŒã *1 ã远å ããŸããserde ã¯ã·ãªã¢ã©ã€ãº/ãã·ãªã¢ã©ã€ãºãããŠãããã¯ã¬ãŒãã§ãã å¥ãã¡ã€ã«ã§ã¡ãœããã远å ããŸãã slackbot.rs use axum :: { http :: StatusCode, Json}; use serde :: {Deserialize, Serialize}; #[derive( Debug , Deserialize)] pub struct Request { token: String , challenge: Option < String > , } #[derive( Debug , Serialize)] pub struct Response { ok: bool , challenge: Option < String > , } pub async fn slackbot ( Json (req): Json < Request > ) -> (StatusCode, Json < Response > ) { let res = Json (Response { ok: true , challenge: req.challenge, }); ( StatusCode :: OK, res) } 3. main.rs ã«æ°ãã POST ãåãåãã Route ã远å ããŸãã main.rs - use axum::{http::StatusCode, routing::get, Router}; + use axum::{ + http::StatusCode + routing::{get, post}, + Router, + }; use std::net::SocketAddr; + mod slackbot; + use crate::slackbot::slackbot; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(handler)) + .route("/slackbot", post(slackbot)); let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); 4/5 .env ãå°å
¥ãã ãæºåâ 2/4 Token ååŸãã§ååŸã§ããå€ .env ãã¡ã€ã«ã«èšå®ããŸããèªã¿åãã«ã¯ dotenv ã¯ã¬ãŒãã䜿ããŸãã main.rs use std::net::SocketAddr; + use dotenv::dotenv; mod slackbot; use crate::slackbot::slackbot; (äžç¥) async fn init() { tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .init(); + dotenv().ok(); } 5/5 tokenã§ããªããŒãããã ã4/5 .env ãå°å
¥ãããã§èšå®ããå€ã§ããªããŒãããŸãã slackbot.rs + use std::env; + use axum::{http::StatusCode, Json}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize)] pub struct Request { token: String, challenge: Option<String>, } #[derive(Debug, Serialize)] pub struct Response { ok: bool, challenge: Option<String>, } pub async fn slackbot(Json(req): Json<Request>) -> (StatusCode, Json<Response>) { + // Validate + let verification_token = + env::var("VERIFICATION_TOKEN").expect("VERIFICATION_TOKEN must be set"); + + if req.token != verification_token { + tracing::warn!("AuthenticationFailed, token: {}", req.token); + let res = Json(Response { + ok: false, + challenge: None, + }); + return (StatusCode::BAD_REQUEST, res); + } let res = Json(Response { ok: true, challenge: req.challenge, }); (StatusCode::OK, res) } æºåâ¡ URL ã®ç»é² EC2 ã§é©åœãªã€ã³ã¹ã¿ã³ã¹ãç«ã¡äžããŠãSSH ãš HTTP ã§éä¿¡ãã§ããããã«èšå®ããŸãã EventSubscriptions ( https://api.slack.com/apps/XXXXXXXXXXXX/event-subscriptions? ) ã§ãäžèšã§æºåãã EC2 ã® URL (äŸ: http://ec2-12-345-678-90.ap-northeast-1.compute.amazonaws.com/slackbot) ã RequestURL ã®æ¬ã«å
¥åããŸãã Slack API ããéãããŠãã challenge ãè¿ããŠããã° Verified ã«ãªããŸãã å®è£
â¡ Bot ãã Slack ã«æçš¿ãã 1/2 Slack ããã®å
¥åãåãåã ãã·ãªã¢ã©ã€ãº Event ã远èšããŸãã slackbot.rs #[derive(Debug, Deserialize)] pub struct Request { token: String, challenge: Option<String>, + event: Option<Event>, } + #[derive(Debug, Deserialize)] + struct Event { + channel: String, + user: String, + text: String, + thread_ts: Option<String>, + } 2/2 Bot ãããªãŠã è¿ããã reqwest ã¯ã¬ãŒãã§ Slack ã« æçš¿ããŸãã Botããã®æçš¿ã«è¿çããŠããŸããšç¡éã«ãŒãã«ãªã£ãŠããŸããããç¡èŠããŸãã slackbot.rs #[derive(Debug, Deserialize)] struct Event { channel: String, user: String, text: String, thread_ts: Option<String>, } + impl Event { + fn from_bot(&self, bot_user: String) -> bool { + self.user == bot_user + } + } (äžç¥) // Validate let verification_token = env::var("VERIFICATION_TOKEN").expect("VERIFICATION_TOKEN must be set"); if req.token != verification_token { tracing::warn!("AuthenticationFailed, token: {}", req.token); let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } + // Validate + match &req.event { + Some(event) => { + let bot_user = env::var("BOT_USER").expect("BOT_USER must be set"); + if event.from_bot(bot_user) { + tracing::debug!("From happy bot"); + let res = Json(Response { + ok: false, + challenge: None, + }); + return (StatusCode::BAD_REQUEST, res); + } + } + None => { + let res = Json(Response { + ok: false, + challenge: None, + }); + return (StatusCode::BAD_REQUEST, res); + } + } ããã§ãããã°çšéã« tracing ã¯ã¬ãŒãã远å ããŸããã 2. ãªã¯ãšã¹ãã challenge ã®ãããªãã§åå²ãããŸãã slackbot.rs #[derive(Debug, Deserialize)] pub struct Request { token: String, challenge: Option<String>, event: Option<Event>, } + impl Request { + fn is_initialize(&self) -> bool { + self.challenge.is_some() + } + } (äžç¥) // Validate let verification_token = env::var("VERIFICATION_TOKEN").expect("VERIFICATION_TOKEN must be set"); if req.token != verification_token { tracing::warn!("AuthenticationFailed, token: {}", req.token); let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } + // Verify from Slack + if req.is_initialize() { + let res = Json(Response { + ok: true, + challenge: req.challenge, + }); + return (StatusCode::OK, res); + } // Validate match &req.event { Some(event) => { let bot_user = env::var("BOT_USER").expect("BOT_USER must be set"); if event.from_bot(bot_user) { tracing::debug!("From happy bot"); let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } } None => { let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } } + let res = Json(Response { + ok: true, + challenge: None, + }); + return (StatusCode::OK, res); 3. Slack ãžã® POST ãªã¯ãšã¹ãã远å ããŸãã slackbot.rs impl Event { fn from_bot(&self, bot_user: String) -> bool { self.user == bot_user } } + #[derive(Debug, Serialize)] + struct PostBody { + text: String, + channel: String, + thread_ts: Option<String>, + } #[derive(Debug, Serialize)] pub struct Response { ok: bool, challenge: Option<String>, } (äžç¥) + // Execute + let event = req.event.unwrap(); + + let post_body = PostBody { + text: event.text, + channel: event.channel, + thread_ts: event.thread_ts.map(|x| x), + }; post_request(post_body).await; let res = Json(Response { ok: true, challenge: None, }); return (StatusCode::OK, res); } + async fn post_request(post_body: PostBody) -> reqwest::Result<()> { + tracing::info!("slackbot__post_request"); + + let url = "https://slack.com/api/chat.postMessage"; + let bot_user_oauth_token = + env::var("BOT_USER_OAUTH_TOKEN").expect("BOT_USER_OAUTH_TOKEN must be set"); + + let client = reqwest::Client::new(); + let response = client + .post(url) + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", bot_user_oauth_token), + ) + .json(&post_body) + .send() + .await?; + + Ok(()) + } ã§ããã³ãŒãã EC2 ã«ãããã€ããŠãå®éã« Slack ã§å
¥åããããšããªãŠã è¿ããã§ããããã«ãªããŸãããèšå®ãããã£ã³ãã«ã§äœãæçš¿ããŠã¿ãŠãã ããã ãŸãšã ãã®ãã¡ä»¥äžã®ãããªããŒããåŠç¿ããŠãããããšèããŠããŸãã ãã¡ã€ã«åå² ORM ã®å°å
¥ ãšã©ãŒãã³ããªã³ã° ãŸããä»åŸå®éçšãèŠæ®ããŠã®æ€èšŒãšããŠã test å°å
¥ linter formatter å°å
¥ ãªã©ãèããŠãããŸãã é¢çœãã£ããšãã æææš© OptionãSome æ§é äœãå ã¯ãŸã£ããšãã ãã¡ã€ã«ãã¯ãŒã¯ã¹ããŒã¹åå²ã®ã«ãŒã«ãäžçªã¯ãŸããŸããã é¢çœãã£ããšãããšã¯ãŸã£ããšããã¯çŽäžéã§ãPython ãšã¯æžãæ¹ãéããšããããªãšæããŸãã äžèšä»¥å€ã«ã现ããã€ãŸãããå€ãã£ãã§ããããšãŠã楜ããåŠã¶ããšãã§ããŸããã Python ã¯æžãããããéèŠããèšèªã ãšèšãããšãæ¹ããŠå®æããŸãããæ®æ®µããŸãèããã«æžããŠãããšãããèŠçŽãè¯ãæ©äŒãšãªããŸããã *1 : Rust ã§ã¯ Python ã®ããã±ãŒãžã«çžåœãããã®ãã¯ã¬ãŒããšåŒã³ãŸãã