From cf48f1ba2a8feb5575f92e2521ef091ca9e2bea4 Mon Sep 17 00:00:00 2001 From: Ryoga Saito Date: Thu, 16 Nov 2023 23:43:56 +0900 Subject: [PATCH 1/5] =?UTF-8?q?RedeployService=E3=82=92bot.yaml=E3=81=8B?= =?UTF-8?q?=E3=82=89=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E3=82=89=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.rs | 25 ++++++++++++++----- src/main.rs | 67 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/config.rs b/src/config.rs index 1b86560..03acb4a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,7 @@ use crate::models::Team; pub struct Configuration { pub staff: StaffConfiguration, pub discord: DiscordConfiguration, - pub redeploy: RedeployServiceConfiguration, + pub redeploy: RedeployConfiguration, #[serde(default)] pub teams: Vec, @@ -41,17 +41,30 @@ pub struct DiscordConfiguration { } #[derive(Debug, Deserialize)] -pub struct RedeployServiceConfiguration { +pub struct RedeployConfiguration { + #[serde(flatten)] + pub service: RedeployServiceConfiguration, + pub notifiers: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RedeployServiceConfiguration { + Rstate(RstateRedeployServiceConfiguration), + Fake, +} + +#[derive(Debug, Deserialize)] +pub struct RstateRedeployServiceConfiguration { pub baseurl: String, pub username: String, pub password: String, - - pub notifiers: RedeployNotifiersConfiguration, } #[derive(Debug, Deserialize)] -pub struct RedeployNotifiersConfiguration { - pub discord: Option, +#[serde(rename_all = "snake_case")] +pub enum RedeployNotifiersConfiguration { + Discord(DiscordRedeployNotifierConfiguration), } #[derive(Debug, Deserialize)] diff --git a/src/main.rs b/src/main.rs index bf1c214..de6e1a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,13 @@ +use anyhow::Result; use bot::config::Configuration; +use bot::config::RedeployNotifiersConfiguration; +use bot::config::RedeployServiceConfiguration; use bot::services::redeploy::DiscordRedeployNotifier; use bot::services::redeploy::FakeRedeployService; +use bot::services::redeploy::RState; +use bot::services::redeploy::RStateConfig; use bot::services::redeploy::RedeployNotifier; +use bot::services::redeploy::RedeployService; use bot::Bot; use clap::Parser; use clap::Subcommand; @@ -26,6 +32,36 @@ enum Commands { DeleteCommands, } +fn build_redeploy_service( + config: &Configuration, +) -> Result> { + Ok(match &config.redeploy.service { + RedeployServiceConfiguration::Rstate(rstate) => Box::new(RState::new(RStateConfig { + baseurl: rstate.baseurl.clone(), + username: rstate.username.clone(), + password: rstate.password.clone(), + })?), + RedeployServiceConfiguration::Fake => Box::new(FakeRedeployService), + }) +} + +async fn build_redeploy_notifiers( + config: &Configuration, +) -> Result>> { + let mut notifiers: Vec> = Vec::new(); + for notifier_config in &config.redeploy.notifiers { + match notifier_config { + RedeployNotifiersConfiguration::Discord(discord) => { + notifiers.push(Box::new( + DiscordRedeployNotifier::new(&config.discord.token, &discord.webhook_url) + .await?, + )); + }, + } + } + Ok(notifiers) +} + #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); @@ -34,28 +70,25 @@ async fn main() { let config = match Configuration::load(args.config) { Ok(config) => config, Err(err) => { - tracing::error!("couldn't read config file: {:?}", err); + tracing::error!(?err, "couldn't read config file"); return; }, }; - let redeploy_service = FakeRedeployService; - - let mut redeploy_notifiers: Vec> = Vec::new(); + let redeploy_service = match build_redeploy_service(&config) { + Ok(service) => service, + Err(err) => { + tracing::error!(?err, "couldn't instantiate redeploy service"); + return; + }, + }; - match config.redeploy.notifiers.discord { - Some(notifier_config) => { - match DiscordRedeployNotifier::new(&config.discord.token, ¬ifier_config.webhook_url) - .await - { - Ok(notifier) => redeploy_notifiers.push(Box::new(notifier)), - Err(err) => { - tracing::error!("couldn't instantiate DiscordRedeployNotifier: {:?}", err); - return; - }, - } + let redeploy_notifiers = match build_redeploy_notifiers(&config).await { + Ok(notifiers) => notifiers, + Err(err) => { + tracing::error!("couldn't instantiate DiscordRedeployNotifier: {:?}", err); + return; }, - None => (), }; let bot = Bot::new( @@ -65,7 +98,7 @@ async fn main() { config.staff.password, config.teams, config.problems, - Box::new(redeploy_service), + redeploy_service, redeploy_notifiers, ); From c8d0d7bd59eb6261895c2495f8e84a9097c100e6 Mon Sep 17 00:00:00 2001 From: Ryoga Saito Date: Fri, 17 Nov 2023 00:04:54 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E7=8F=BE=E5=9C=A8=E3=81=AErstate=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=E3=82=92=E3=82=B5=E3=83=9D=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 5 ++-- Cargo.toml | 1 + Makefile | 17 ++++++++--- src/services/redeploy.rs | 61 +++++++++++++++++++++++++++++++--------- 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a7cd20..6aad3be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,7 @@ dependencies = [ "reqwest", "serde", "serde_derive", + "serde_json", "serde_yaml", "serenity", "thiserror", @@ -1144,9 +1145,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa 1.0.9", "ryu", diff --git a/Cargo.toml b/Cargo.toml index 2aacad4..1b6d295 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ derive_builder = "0.12.0" reqwest = { version = "0.11.9", features = ["json"] } serde = "1.0.131" serde_derive = "1.0.131" +serde_json = "1.0.108" serde_yaml = "0.8.21" serenity = { version = "0.11.6", default-features = false, features = ["client", "collector", "gateway", "rustls_backend", "model", "unstable_discord_api"] } thiserror = "1.0.30" diff --git a/Makefile b/Makefile index f742b7d..b97fb24 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ NAME ?= ictsc-discord-bot IMAGE_TAG ?= latest IMAGE ?= ghcr.io/ictsc/ictsc-discord-bot:$(IMAGE_TAG) -DOCKER_ARGS ?= --name $(NAME) -v "$(shell pwd)/bot.yaml:/bot.yaml" +DOCKER_ARGS ?= -v "$(shell pwd)/bot.yaml:/bot.yaml" --net host --env RUST_LOG=info,bot=debug all: @@ -13,8 +13,17 @@ build: .PHONY: start start: - DOCKER_BUILDKIT=1 docker run -d $(DOCKER_ARGS) $(IMAGE) -f /bot.yaml start + docker run -d --name $(NAME) $(DOCKER_ARGS) $(IMAGE) -f /bot.yaml start -.PHONY: start +.PHONY: sync-channels sync-roles +sync-channels sync-roles: + docker run -it $(DOCKER_ARGS) $(IMAGE) -f /bot.yaml $@ + +.PHONY: stop stop: - DOCKER_BUILDKIT=1 docker rm -f $(NAME) + docker rm -f $(NAME) + +.PHONY: logs +logs: + docker logs -f $(NAME) + diff --git a/src/services/redeploy.rs b/src/services/redeploy.rs index 7165fb5..5f84eea 100644 --- a/src/services/redeploy.rs +++ b/src/services/redeploy.rs @@ -3,12 +3,21 @@ use async_trait::async_trait; use reqwest::Client; use reqwest::ClientBuilder; use reqwest::StatusCode; +use serde::Deserialize; +use serde::Serialize; use serenity::http::Http; use serenity::model::prelude::Embed; use serenity::model::webhook::Webhook; use serenity::utils::Colour; -type RedeployResult = Result; +#[derive(Debug)] +pub struct RedeployJob { + pub id: String, + pub team_id: String, + pub problem_code: String, +} + +type RedeployResult = Result; #[async_trait] pub trait RedeployService { @@ -32,8 +41,16 @@ pub enum RedeployError { InvalidParameters, #[error("another job is in queue")] AnotherJobInQueue(String), + + // serde_jsonでserialize/deserializeに失敗した時に出るエラー + #[error("serde_json error: {0}")] + Json(#[from] serde_json::Error), + + // reqwestでHTTP接続に失敗した時に出るエラー #[error("reqwest error: {0}")] Reqwest(#[from] reqwest::Error), + + // なんだかよくわからないエラー #[error("unexpected error occured: {0}")] Unexpected(#[from] Box), } @@ -59,12 +76,19 @@ impl RState { } } -#[derive(Debug, serde::Serialize)] -struct DefaultRedeployServiceRedeployRequest<'a> { +#[derive(Debug, Serialize)] +struct RStatePostJobRequest<'a> { team_id: &'a str, prob_id: &'a str, } +#[derive(Debug, Deserialize)] +struct RStatePostJobResponse { + id: String, + team_id: String, + prob_id: String, +} + #[async_trait] impl RedeployService for RState { #[tracing::instrument(skip_all, fields(target = ?target))] @@ -74,7 +98,7 @@ impl RedeployService for RState { let response = self .client .post(format!("{}/admin/postJob", self.config.baseurl)) - .form(&DefaultRedeployServiceRedeployRequest { + .form(&RStatePostJobRequest { team_id: &target.team_id, prob_id: &target.problem_id, }) @@ -84,9 +108,14 @@ impl RedeployService for RState { match response.status() { StatusCode::OK => { - let url = String::from_utf8(response.bytes().await?.to_vec()) - .map_err(|err| RedeployError::Unexpected(Box::new(err)))?; - Ok(url) + let response: RStatePostJobResponse = + serde_json::from_slice(response.bytes().await?.as_ref())?; + + Ok(RedeployJob { + id: response.id, + team_id: response.team_id, + problem_code: response.prob_id, + }) }, StatusCode::BAD_REQUEST => { let data = String::from_utf8(response.bytes().await?.to_vec()) @@ -109,10 +138,14 @@ pub struct FakeRedeployService; #[async_trait] impl RedeployService for FakeRedeployService { - #[tracing::instrument(skip_all, fields(target = ?_target))] - async fn redeploy(&self, _target: &RedeployTarget) -> RedeployResult { + #[tracing::instrument(skip_all, fields(target = ?target))] + async fn redeploy(&self, target: &RedeployTarget) -> RedeployResult { tracing::info!("redeploy request received"); - Ok(String::from("https://example.com")) + Ok(RedeployJob { + id: String::from("00000000-0000-0000-0000-000000000000"), + team_id: target.team_id.clone(), + problem_code: target.problem_id.clone(), + }) } } @@ -146,15 +179,15 @@ impl RedeployNotifier for DiscordRedeployNotifier { impl DiscordRedeployNotifier { async fn _notify(&self, target: &RedeployTarget, result: &RedeployResult) -> Result<()> { let embed = match result { - Ok(url) => Embed::fake(|e| { - e.title("AAA") + Ok(job) => Embed::fake(|e| { + e.title("再展開開始通知") .colour(Colour::from_rgb(40, 167, 65)) .field("チームID", &target.team_id, true) .field("問題コード", &target.problem_id, true) - .field("再展開進捗URL", url, true) + .field("再展開Job ID", &job.id, true) }), Err(err) => Embed::fake(|e| { - e.title("AAA") + e.title("再展開失敗通知") .colour(Colour::from_rgb(236, 76, 82)) .field("チームID", &target.team_id, true) .field("問題コード", &target.problem_id, true) From 036a979c2c27fd918753c6258bb144a3f10264d1 Mon Sep 17 00:00:00 2001 From: Ryoga Saito Date: Fri, 17 Nov 2023 01:23:20 +0900 Subject: [PATCH 3/5] =?UTF-8?q?rstate=E3=81=8B=E3=82=89=E3=82=B9=E3=83=86?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=82=B9=E3=82=92=E5=8F=96=E5=BE=97=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 70 +++++++++++++++++++++++- Cargo.toml | 1 + src/main.rs | 1 + src/models.rs | 4 +- src/services/redeploy.rs | 112 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 176 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6aad3be..f38ebf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -132,6 +147,7 @@ dependencies = [ "anyhow", "async-trait", "base64", + "chrono", "clap", "derive_builder", "reqwest", @@ -166,9 +182,12 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.72" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -176,6 +195,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clap" version = "4.4.2" @@ -628,6 +662,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1762,6 +1819,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 1b6d295..b9c2f06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" anyhow = "1.0.53" async-trait = "0.1.52" base64 = "0.13.0" +chrono = { version = "0.4.31", features = ["serde"] } clap = { version = "4.4.2", features = ["derive"] } derive_builder = "0.12.0" reqwest = { version = "0.11.9", features = ["json"] } diff --git a/src/main.rs b/src/main.rs index de6e1a7..9f59f7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,7 @@ fn build_redeploy_service( baseurl: rstate.baseurl.clone(), username: rstate.username.clone(), password: rstate.password.clone(), + problems: config.problems.clone(), })?), RedeployServiceConfiguration::Fake => Box::new(FakeRedeployService), }) diff --git a/src/models.rs b/src/models.rs index cbaae54..c68ed72 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,13 +1,13 @@ use serde::Deserialize; -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Team { pub id: String, pub role_name: String, pub invitation_code: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Problem { pub code: String, pub name: String, diff --git a/src/services/redeploy.rs b/src/services/redeploy.rs index 5f84eea..63293b8 100644 --- a/src/services/redeploy.rs +++ b/src/services/redeploy.rs @@ -1,5 +1,7 @@ use anyhow::Result; use async_trait::async_trait; +use chrono::DateTime; +use chrono::Utc; use reqwest::Client; use reqwest::ClientBuilder; use reqwest::StatusCode; @@ -10,18 +12,38 @@ use serenity::model::prelude::Embed; use serenity::model::webhook::Webhook; use serenity::utils::Colour; -#[derive(Debug)] +use crate::models::Problem; + +#[derive(Debug, Clone)] pub struct RedeployJob { pub id: String, pub team_id: String, pub problem_code: String, } -type RedeployResult = Result; +type RedeployStatusList = Vec; + +#[derive(Debug, Clone)] +pub struct RedeployStatus { + pub team_id: String, + pub problem_code: String, + + // 再展開中かを表すフラグ + pub is_redeploying: bool, + + // 最後の再展開が開始された時刻 + pub last_redeploy_started_at: Option>, + + // 最後の再展開が完了した時刻 + pub last_redeploy_completed_at: Option>, +} + +type RedeployResult = Result; #[async_trait] pub trait RedeployService { - async fn redeploy(&self, target: &RedeployTarget) -> RedeployResult; + async fn redeploy(&self, target: &RedeployTarget) -> RedeployResult; + async fn get_status(&self, team_id: &str) -> RedeployResult; } #[derive(Debug)] @@ -32,7 +54,7 @@ pub struct RedeployTarget { #[async_trait] pub trait RedeployNotifier { - async fn notify(&self, target: &RedeployTarget, result: &RedeployResult); + async fn notify(&self, target: &RedeployTarget, result: &RedeployResult); } #[derive(Debug, thiserror::Error)] @@ -64,6 +86,7 @@ pub struct RStateConfig { pub baseurl: String, pub username: String, pub password: String, + pub problems: Vec, } impl RState { @@ -89,10 +112,17 @@ struct RStatePostJobResponse { prob_id: String, } +#[derive(Debug, Deserialize)] +struct RStateGetRedeployStatusResponse { + available: bool, + created_time: Option>, + completed_time: Option>, +} + #[async_trait] impl RedeployService for RState { #[tracing::instrument(skip_all, fields(target = ?target))] - async fn redeploy(&self, target: &RedeployTarget) -> RedeployResult { + async fn redeploy(&self, target: &RedeployTarget) -> RedeployResult { tracing::info!("redeploy request received"); let response = self @@ -132,6 +162,37 @@ impl RedeployService for RState { )), } } + + #[tracing::instrument(skip_all, fields(team_id = ?team_id))] + async fn get_status(&self, team_id: &str) -> RedeployResult { + tracing::trace!("get_status request received"); + + let mut statuses = Vec::new(); + for problem in &self.config.problems { + let response = self + .client + .get(format!( + "{}/backend/{}/{}", + self.config.baseurl, team_id, problem.code + )) + .send() + .await?; + + // /backend/statusは常に200を返すので、エラーハンドリングしない + let response: RStateGetRedeployStatusResponse = + serde_json::from_slice(response.bytes().await?.as_ref())?; + + statuses.push(RedeployStatus { + team_id: team_id.to_string(), + problem_code: problem.code.clone(), + is_redeploying: !response.available, + last_redeploy_started_at: response.created_time, + last_redeploy_completed_at: response.completed_time, + }); + } + + Ok(statuses) + } } pub struct FakeRedeployService; @@ -139,7 +200,7 @@ pub struct FakeRedeployService; #[async_trait] impl RedeployService for FakeRedeployService { #[tracing::instrument(skip_all, fields(target = ?target))] - async fn redeploy(&self, target: &RedeployTarget) -> RedeployResult { + async fn redeploy(&self, target: &RedeployTarget) -> RedeployResult { tracing::info!("redeploy request received"); Ok(RedeployJob { id: String::from("00000000-0000-0000-0000-000000000000"), @@ -147,6 +208,37 @@ impl RedeployService for FakeRedeployService { problem_code: target.problem_id.clone(), }) } + + #[tracing::instrument(skip_all, fields(team_id = ?team_id))] + async fn get_status(&self, team_id: &str) -> RedeployResult { + tracing::trace!("get_status request received"); + + let now = Utc::now(); + + Ok(vec![ + RedeployStatus { + team_id: team_id.to_string(), + problem_code: String::from("ABC"), + is_redeploying: false, + last_redeploy_started_at: None, + last_redeploy_completed_at: None, + }, + RedeployStatus { + team_id: team_id.to_string(), + problem_code: String::from("DEF"), + is_redeploying: true, + last_redeploy_started_at: Some(now), + last_redeploy_completed_at: None, + }, + RedeployStatus { + team_id: team_id.to_string(), + problem_code: String::from("GHI"), + is_redeploying: false, + last_redeploy_started_at: Some(now), + last_redeploy_completed_at: Some(now), + }, + ]) + } } #[derive(Debug)] @@ -169,7 +261,7 @@ impl DiscordRedeployNotifier { #[async_trait] impl RedeployNotifier for DiscordRedeployNotifier { #[tracing::instrument(skip_all, fields(target = ?target, result = ?result))] - async fn notify(&self, target: &RedeployTarget, result: &RedeployResult) { + async fn notify(&self, target: &RedeployTarget, result: &RedeployResult) { if let Err(err) = self._notify(target, result).await { tracing::error!("failed to notify: {:?}", err) } @@ -177,7 +269,11 @@ impl RedeployNotifier for DiscordRedeployNotifier { } impl DiscordRedeployNotifier { - async fn _notify(&self, target: &RedeployTarget, result: &RedeployResult) -> Result<()> { + async fn _notify( + &self, + target: &RedeployTarget, + result: &RedeployResult, + ) -> Result<()> { let embed = match result { Ok(job) => Embed::fake(|e| { e.title("再展開開始通知") From d60ac2654368cfee76a9a0a17832e5e59fb198ee Mon Sep 17 00:00:00 2001 From: Ryoga Saito Date: Fri, 17 Nov 2023 01:23:44 +0900 Subject: [PATCH 4/5] =?UTF-8?q?redeploy=E3=82=B3=E3=83=9E=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=82=92=E3=82=B5=E3=83=96=E3=82=B3=E3=83=9E=E3=83=B3?= =?UTF-8?q?=E3=83=89=E5=8C=96=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bot/commands/ask.rs | 4 +- src/bot/commands/join.rs | 2 +- src/bot/commands/redeploy.rs | 85 ++++++++++++++++++++++++++++----- src/bot/helpers/interactions.rs | 5 +- 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/bot/commands/ask.rs b/src/bot/commands/ask.rs index dfcf0b2..248b642 100644 --- a/src/bot/commands/ask.rs +++ b/src/bot/commands/ask.rs @@ -85,7 +85,9 @@ impl Bot { _ => return Err(AskCommandError::InvalidChannelTypeError), }; - let title = self.get_option_as_str(interaction, "title").unwrap(); + let title = self + .get_option_as_str(&interaction.data.options, "title") + .unwrap(); // 可読性や識別性から、質問タイトルは32文字以内に制限している。 if title.chars().count() > 32 { diff --git a/src/bot/commands/join.rs b/src/bot/commands/join.rs index 01319ee..4087934 100644 --- a/src/bot/commands/join.rs +++ b/src/bot/commands/join.rs @@ -78,7 +78,7 @@ impl Bot { } let invitation_code = self - .get_option_as_str(interaction, "invitation_code") + .get_option_as_str(&interaction.data.options, "invitation_code") .unwrap(); self.find_role_name_by_invitation_code(invitation_code) diff --git a/src/bot/commands/redeploy.rs b/src/bot/commands/redeploy.rs index c9c711f..9a44b0d 100644 --- a/src/bot/commands/redeploy.rs +++ b/src/bot/commands/redeploy.rs @@ -3,6 +3,7 @@ use std::time::Duration; use anyhow::Result; use serenity::builder::CreateApplicationCommand; use serenity::builder::CreateComponents; +use serenity::model::application::interaction::application_command::CommandDataOption; use serenity::model::prelude::application_command::ApplicationCommandInteraction; use serenity::model::prelude::command::CommandOptionType; use serenity::model::prelude::component::ButtonStyle; @@ -26,6 +27,9 @@ enum RedeployCommandError<'a> { #[error("予期しないエラーが発生しました。運営にお問い合わせください。")] UnexpectedSenderTeamsError, + #[error("予期しないエラーが発生しました。運営にお問い合わせください。")] + InconsistentCommandDefinitionError, + #[error("予期しないエラーが発生しました。")] HelperError(#[from] HelperError), } @@ -55,13 +59,25 @@ impl Bot { ) -> &mut CreateApplicationCommand { command .name("redeploy") - .description("問題環境を再展開します。") + .description("問題環境の再展開に関するコマンド") + .create_option(|option| { + option + .name("start") + .description("問題環境を再展開します。") + .kind(CommandOptionType::SubCommand) + .create_sub_option(|option| { + option + .name("problem_code") + .description("問題コード") + .kind(CommandOptionType::String) + .required(true) + }) + }) .create_option(|option| { option - .name("problem_code") - .description("問題コード") - .kind(CommandOptionType::String) - .required(true) + .name("status") + .description("現在の再展開状況を表示します。") + .kind(CommandOptionType::SubCommand) }) } @@ -70,7 +86,49 @@ impl Bot { ctx: &Context, interaction: &ApplicationCommandInteraction, ) -> Result<()> { - let problem = match self.validate_redeploy_command(interaction) { + if let Err(err) = self._handle_redeploy_command(ctx, interaction).await { + tracing::error!(?err, "failed to handle redeploy command"); + self.edit_response(interaction, |data| { + data.content(err.to_string()).components(|c| c) + }) + .await?; + } + + Ok(()) + } + + async fn _handle_redeploy_command( + &self, + ctx: &Context, + interaction: &ApplicationCommandInteraction, + ) -> RedeployCommandResult<()> { + let subcommand = interaction + .data + .options + .first() + .ok_or(RedeployCommandError::InconsistentCommandDefinitionError)?; + + if subcommand.kind != CommandOptionType::SubCommand { + return Err(RedeployCommandError::InconsistentCommandDefinitionError); + } + + Ok(match subcommand.name.as_str() { + "start" => { + self.handle_redeploy_start_subcommand(ctx, interaction, subcommand) + .await? + }, + "status" => {}, + _ => return Err(RedeployCommandError::InconsistentCommandDefinitionError), + }) + } + + async fn handle_redeploy_start_subcommand( + &self, + ctx: &Context, + interaction: &ApplicationCommandInteraction, + option: &CommandDataOption, + ) -> RedeployCommandResult<()> { + let problem = match self.validate_redeploy_start_subcommand(option) { Ok(problem) => problem, Err(err) => { self.respond(interaction, |data| { @@ -83,7 +141,10 @@ impl Bot { self.defer_response(interaction).await?; - if let Err(err) = self.do_redeploy_command(ctx, interaction, problem).await { + if let Err(err) = self + .do_redeploy_start_subcommand(ctx, interaction, problem) + .await + { tracing::error!(?err, "failed to do redeploy command"); self.edit_response(interaction, |data| { data.content(err.to_string()).components(|c| c) @@ -94,11 +155,13 @@ impl Bot { Ok(()) } - fn validate_redeploy_command<'t>( + fn validate_redeploy_start_subcommand<'t>( &self, - interaction: &'t ApplicationCommandInteraction, + option: &'t CommandDataOption, ) -> RedeployCommandResult<'t, &Problem> { - let problem_code = self.get_option_as_str(interaction, "problem_code").unwrap(); + let problem_code = self + .get_option_as_str(&option.options, "problem_code") + .unwrap(); let problem = self .problems @@ -109,7 +172,7 @@ impl Bot { } // TODO: いろいろガバガバなので修正する - async fn do_redeploy_command( + async fn do_redeploy_start_subcommand( &self, ctx: &Context, interaction: &ApplicationCommandInteraction, diff --git a/src/bot/helpers/interactions.rs b/src/bot/helpers/interactions.rs index ec69f6d..bf9eac0 100644 --- a/src/bot/helpers/interactions.rs +++ b/src/bot/helpers/interactions.rs @@ -1,5 +1,6 @@ use serenity::builder::CreateInteractionResponseData; use serenity::builder::EditInteractionResponse; +use serenity::model::application::interaction::application_command::CommandDataOption; use serenity::model::prelude::application_command::ApplicationCommandInteraction; use serenity::model::prelude::message_component::MessageComponentInteraction; use serenity::model::prelude::*; @@ -126,10 +127,10 @@ impl Bot { pub fn get_option_as_str<'t>( &self, - interaction: &'t ApplicationCommandInteraction, + options: &'t [CommandDataOption], name: &str, ) -> Option<&'t str> { - for option in &interaction.data.options { + for option in options { if option.name == name { return option.value.as_ref().and_then(|v| v.as_str()); } From cf0eba39fac9b21540d797a513ea4bbf313e2274 Mon Sep 17 00:00:00 2001 From: Ryoga Saito Date: Fri, 17 Nov 2023 01:57:59 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=E5=86=8D=E5=B1=95=E9=96=8B=E7=8A=B6?= =?UTF-8?q?=E6=B3=81=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bot/commands/redeploy.rs | 122 ++++++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 31 deletions(-) diff --git a/src/bot/commands/redeploy.rs b/src/bot/commands/redeploy.rs index 9a44b0d..30e73cd 100644 --- a/src/bot/commands/redeploy.rs +++ b/src/bot/commands/redeploy.rs @@ -8,11 +8,13 @@ use serenity::model::prelude::application_command::ApplicationCommandInteraction use serenity::model::prelude::command::CommandOptionType; use serenity::model::prelude::component::ButtonStyle; use serenity::model::prelude::component::ComponentType; +use serenity::model::user::User; use serenity::prelude::*; use crate::bot::helpers::HelperError; use crate::bot::Bot; use crate::models::Problem; +use crate::models::Team; use crate::services::redeploy::RedeployTarget; const CUSTOM_ID_REDEPLOY_CONFIRM: &str = "redeploy_confirm"; @@ -117,11 +119,33 @@ impl Bot { self.handle_redeploy_start_subcommand(ctx, interaction, subcommand) .await? }, - "status" => {}, + "status" => self.handle_redeploy_status_subcommand(interaction).await?, _ => return Err(RedeployCommandError::InconsistentCommandDefinitionError), }) } + async fn get_team_for(&self, user: &User) -> RedeployCommandResult { + let member = self.get_member(&user).await?; + + for role_id in member.roles { + let role = self.find_roles_by_id_cached(role_id).await.unwrap(); + match role { + Some(role) => { + for team in &self.teams { + if role.name == team.role_name { + return Ok(team.clone()); + } + } + }, + None => (), + } + } + + Err(RedeployCommandError::UnexpectedSenderTeamsError) + } +} + +impl Bot { async fn handle_redeploy_start_subcommand( &self, ctx: &Context, @@ -145,11 +169,8 @@ impl Bot { .do_redeploy_start_subcommand(ctx, interaction, problem) .await { - tracing::error!(?err, "failed to do redeploy command"); - self.edit_response(interaction, |data| { - data.content(err.to_string()).components(|c| c) - }) - .await?; + tracing::error!(?err, "failed to do redeploy start subcommand"); + return Err(err); } Ok(()) @@ -171,7 +192,6 @@ impl Bot { problem.ok_or(RedeployCommandError::InvalidProblemCodeError(problem_code)) } - // TODO: いろいろガバガバなので修正する async fn do_redeploy_start_subcommand( &self, ctx: &Context, @@ -179,30 +199,7 @@ impl Bot { problem: &Problem, ) -> RedeployCommandResult<()> { let sender = &interaction.user; - let sender_member = self.get_member(&sender).await?; - - let mut sender_teams = Vec::new(); - for role_id in sender_member.roles { - let role = self.find_roles_by_id_cached(role_id).await.unwrap(); - match role { - Some(role) => { - for team in &self.teams { - if role.name == team.role_name { - sender_teams.push(team); - } - } - }, - None => (), - } - } - - // /joinコマンドの制約上、ユーザは高々1つのチームにしか所属しないはずである。 - // また、/redeployはGuildでのみ使用可能なため、チームに所属していないユーザは使用できない。 - if sender_teams.len() != 1 { - return Err(RedeployCommandError::UnexpectedSenderTeamsError); - } - - let sender_team = sender_teams.first().unwrap(); + let sender_team = self.get_team_for(sender).await?; self.edit_response(interaction, |data| { // TODO: チーム名にする @@ -282,3 +279,66 @@ impl Bot { Ok(()) } } + +impl Bot { + async fn handle_redeploy_status_subcommand( + &self, + interaction: &ApplicationCommandInteraction, + ) -> RedeployCommandResult<()> { + self.defer_response(interaction).await?; + + if let Err(err) = self.do_redeploy_status_subcommand(interaction).await { + tracing::error!(?err, "failed to do redeploy status subcommand"); + return Err(err); + } + + Ok(()) + } + + async fn do_redeploy_status_subcommand( + &self, + interaction: &ApplicationCommandInteraction, + ) -> RedeployCommandResult<()> { + let sender = &interaction.user; + let sender_team = self.get_team_for(sender).await?; + + let statuses = self + .redeploy_service + .get_status(&sender_team.id) + .await + .unwrap(); + + let no_deploys = statuses + .iter() + .all(|status| status.last_redeploy_started_at.is_none()); + + if no_deploys { + self.edit_response(interaction, |data| { + data.content("まだ再展開は実行されていません。") + }) + .await?; + } + + self.edit_response(interaction, |data| { + data.embed(|e| { + e.title("再展開状況"); + // TODO: 再展開状況はいい感じに表示する。今日はもう疲れた。 + for status in &statuses { + if status.last_redeploy_started_at.is_none() { + continue; + } + + e.field( + &status.problem_code, + format!("{}", status.is_redeploying), + false, + ); + } + e + }) + }) + .await?; + + Ok(()) + } +}