feat: add editing and other stuff
This commit is contained in:
parent
0da2212258
commit
36f52c6892
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -181,6 +181,16 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
|
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cached"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b99e696f7b2696ed5eae0d462a9eeafaea111d99e39b2c8ceb418afe1013bcfc"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.9.1",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
|
@ -214,6 +224,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"askama",
|
"askama",
|
||||||
"askama_warp",
|
"askama_warp",
|
||||||
|
"cached",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"irc",
|
"irc",
|
||||||
|
@ -566,6 +577,12 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
|
@ -707,7 +724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"hashbrown",
|
"hashbrown 0.11.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -9,6 +9,7 @@ edition = "2018"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
askama = { version = "0.10", features = ["with-warp"] }
|
askama = { version = "0.10", features = ["with-warp"] }
|
||||||
askama_warp = "0.11"
|
askama_warp = "0.11"
|
||||||
|
cached = { version = "0.25", default-features = false }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
irc = "0.15"
|
irc = "0.15"
|
||||||
|
|
87
src/app.rs
87
src/app.rs
|
@ -1,13 +1,34 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod duration_tools;
|
||||||
pub mod rhai_tools;
|
pub mod rhai_tools;
|
||||||
pub mod twitch;
|
pub mod twitch;
|
||||||
pub mod user_config;
|
pub mod user_config;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
|
|
||||||
use rhai::Engine;
|
|
||||||
use irc::client::prelude::{Client as IrcClient, Config as IrcConfig, Capability};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use twitch_api2::TwitchClient;
|
use cached::{TimedCache, Cached};
|
||||||
|
use futures::{StreamExt, SinkExt};
|
||||||
|
use irc::client::prelude::{Client as IrcClient, Config as IrcConfig, Capability};
|
||||||
|
use rhai::Engine;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::{
|
||||||
|
runtime::Handle,
|
||||||
|
sync::{
|
||||||
|
Mutex, RwLock,
|
||||||
|
mpsc::UnboundedSender,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tokio_tungstenite::tungstenite::Message as WsMessage;
|
||||||
|
use twitch_api2::{
|
||||||
|
TwitchClient,
|
||||||
|
helix::{
|
||||||
|
Scope,
|
||||||
|
users::User,
|
||||||
|
},
|
||||||
|
pubsub::Topic,
|
||||||
|
types::{UserId, UserName},
|
||||||
|
twitch_oauth2::{ClientSecret, UserToken, TwitchToken, ClientId, RefreshToken, AccessToken},
|
||||||
|
};
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
config::Config,
|
config::Config,
|
||||||
rhai_tools::{ExecutorState, ExecutorOutput},
|
rhai_tools::{ExecutorState, ExecutorOutput},
|
||||||
|
@ -15,20 +36,11 @@ use crate::app::{
|
||||||
twitch::Twitch,
|
twitch::Twitch,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Instant,
|
||||||
};
|
};
|
||||||
use futures::{StreamExt, SinkExt};
|
|
||||||
use twitch_api2::twitch_oauth2::{ClientSecret, UserToken, TwitchToken, ClientId, RefreshToken, AccessToken};
|
|
||||||
use twitch_api2::helix::Scope;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use twitch_api2::types::UserId;
|
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
|
||||||
use tokio::runtime::Handle;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use twitch_api2::pubsub::Topic;
|
|
||||||
use tokio_tungstenite::tungstenite::Message as WsMessage;
|
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub user_config: UserConfig,
|
pub user_config: UserConfig,
|
||||||
|
@ -38,9 +50,13 @@ pub struct State {
|
||||||
|
|
||||||
pub twitch: Arc<Twitch>,
|
pub twitch: Arc<Twitch>,
|
||||||
pub rewards_paused: RwLock<HashMap<String, bool>>,
|
pub rewards_paused: RwLock<HashMap<String, bool>>,
|
||||||
|
pub user_login_cache: Mutex<TimedCache<UserName, User>>,
|
||||||
|
pub user_id_cache: Mutex<TimedCache<UserId, User>>,
|
||||||
|
|
||||||
pub irc: IrcClient,
|
pub irc: IrcClient,
|
||||||
pub irc_queue: UnboundedSender<String>,
|
pub irc_queue: UnboundedSender<String>,
|
||||||
|
pub user_cooldowns: RwLock<HashMap<(UserId, String), Instant>>,
|
||||||
|
pub global_cooldowns: RwLock<HashMap<String, Instant>>,
|
||||||
|
|
||||||
pub rhai: Engine,
|
pub rhai: Engine,
|
||||||
pub script_cache: parking_lot::RwLock<HashMap<String, rhai::AST>>,
|
pub script_cache: parking_lot::RwLock<HashMap<String, rhai::AST>>,
|
||||||
|
@ -106,9 +122,13 @@ impl State {
|
||||||
let mut rhai = Engine::new();
|
let mut rhai = Engine::new();
|
||||||
rhai.set_max_expr_depths(0, 0);
|
rhai.set_max_expr_depths(0, 0);
|
||||||
rhai.register_type::<ExecutorState>()
|
rhai.register_type::<ExecutorState>()
|
||||||
|
.register_get("args", ExecutorState::args)
|
||||||
.register_get("initiator", ExecutorState::initiator)
|
.register_get("initiator", ExecutorState::initiator)
|
||||||
.register_get("initiator_id", ExecutorState::initiator_id)
|
.register_get("initiator_id", ExecutorState::initiator_id)
|
||||||
.register_get("args", ExecutorState::args)
|
.register_get("broadcaster", ExecutorState::broadcaster)
|
||||||
|
.register_get("moderator", ExecutorState::moderator)
|
||||||
|
.register_get("vip", ExecutorState::vip)
|
||||||
|
.register_get("subscriber", ExecutorState::subscriber)
|
||||||
.register_fn("get_username", ExecutorState::get_username::<&str>)
|
.register_fn("get_username", ExecutorState::get_username::<&str>)
|
||||||
.register_fn("get_user_id", ExecutorState::get_user_id::<&str>)
|
.register_fn("get_user_id", ExecutorState::get_user_id::<&str>)
|
||||||
.register_fn("get_channel_info", ExecutorState::get_channel_info::<&str>);
|
.register_fn("get_channel_info", ExecutorState::get_channel_info::<&str>);
|
||||||
|
@ -125,9 +145,13 @@ impl State {
|
||||||
|
|
||||||
twitch,
|
twitch,
|
||||||
rewards_paused: Default::default(),
|
rewards_paused: Default::default(),
|
||||||
|
user_login_cache: Mutex::new(TimedCache::with_lifespan(3600)),
|
||||||
|
user_id_cache: Mutex::new(TimedCache::with_lifespan(3600)),
|
||||||
|
|
||||||
irc,
|
irc,
|
||||||
irc_queue: queue_tx,
|
irc_queue: queue_tx,
|
||||||
|
user_cooldowns: Default::default(),
|
||||||
|
global_cooldowns: Default::default(),
|
||||||
|
|
||||||
rhai,
|
rhai,
|
||||||
script_cache: Default::default(),
|
script_cache: Default::default(),
|
||||||
|
@ -210,6 +234,37 @@ impl State {
|
||||||
|
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_from_id<S: Into<UserId>>(&self, id: S) -> anyhow::Result<Option<User>> {
|
||||||
|
let id = id.into();
|
||||||
|
if let Some(user) = self.user_id_cache.lock().await.cache_get(&id) {
|
||||||
|
return Ok(Some(user.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = self.twitch.client.helix.get_user_from_id(id, &self.twitch.bot_token).await?;
|
||||||
|
self.add_user_to_cache(&user).await;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_from_login<S: Into<UserName>>(&self, login: S) -> anyhow::Result<Option<User>> {
|
||||||
|
let login = login.into();
|
||||||
|
if let Some(user) = self.user_login_cache.lock().await.cache_get(&login) {
|
||||||
|
return Ok(Some(user.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = self.twitch.client.helix.get_user_from_login(login, &self.twitch.bot_token).await?;
|
||||||
|
self.add_user_to_cache(&user).await;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_user_to_cache(&self, user: &Option<User>) {
|
||||||
|
if let Some(user) = user {
|
||||||
|
self.user_id_cache.lock().await.cache_set(user.id.clone(), user.clone());
|
||||||
|
self.user_login_cache.lock().await.cache_set(user.login.clone(), user.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -267,7 +322,7 @@ pub fn run_rhai<S: Into<String>>(state: Arc<State>, script: S, fn_state: Executo
|
||||||
let ast = state.rhai.compile(&script)?;
|
let ast = state.rhai.compile(&script)?;
|
||||||
state.script_cache.write().insert(script.clone(), ast.clone());
|
state.script_cache.write().insert(script.clone(), ast.clone());
|
||||||
ast
|
ast
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut output = rhai::Dynamic::from(ExecutorOutput::default());
|
let mut output = rhai::Dynamic::from(ExecutorOutput::default());
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, DurationSeconds};
|
use serde_with::{serde_as, DurationSecondsWithFrac};
|
||||||
use crate::app::FullUserToken;
|
use crate::app::FullUserToken;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Default)]
|
#[derive(Deserialize, Serialize, Default)]
|
||||||
|
@ -30,9 +30,9 @@ pub enum CommandExecutor {
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Deserialize, Serialize, Default, Clone)]
|
#[derive(Deserialize, Serialize, Default, Clone)]
|
||||||
pub struct Cooldowns {
|
pub struct Cooldowns {
|
||||||
#[serde_as(as = "Option<DurationSeconds<f64>>")]
|
#[serde_as(as = "Option<DurationSecondsWithFrac<f64>>")]
|
||||||
pub global: Option<Duration>,
|
pub global: Option<Duration>,
|
||||||
#[serde_as(as = "Option<DurationSeconds<f64>>")]
|
#[serde_as(as = "Option<DurationSecondsWithFrac<f64>>")]
|
||||||
pub user: Option<Duration>,
|
pub user: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
src/app/duration_tools.rs
Normal file
12
src/app/duration_tools.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use chrono::Duration;
|
||||||
|
|
||||||
|
pub trait DurationTools {
|
||||||
|
fn seconds_f64(&self) -> f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DurationTools for Duration {
|
||||||
|
fn seconds_f64(&self) -> f64 {
|
||||||
|
let millis = self.num_milliseconds();
|
||||||
|
millis as f64 / 1_000.0
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::app::twitch::Twitch;
|
use twitch_api2::{
|
||||||
|
types::{UserId, UserName},
|
||||||
|
helix::channels::{GetChannelInformationRequest, ChannelInformation},
|
||||||
|
};
|
||||||
|
use crate::app::State;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use twitch_api2::types::{UserId, UserName};
|
|
||||||
use tokio::runtime::Handle;
|
|
||||||
use twitch_api2::helix::channels::{GetChannelInformationRequest, ChannelInformation};
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct ExecutorOutput {
|
pub struct ExecutorOutput {
|
||||||
|
@ -17,17 +18,20 @@ impl ExecutorOutput {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ExecutorState {
|
pub struct ExecutorState {
|
||||||
pub runtime: Handle,
|
pub state: Arc<State>,
|
||||||
pub twitch: Arc<Twitch>,
|
|
||||||
|
|
||||||
pub args: Vec<rhai::Dynamic>,
|
pub args: Vec<rhai::Dynamic>,
|
||||||
|
|
||||||
pub initiator: String,
|
pub initiator: String,
|
||||||
pub initiator_id: UserId,
|
pub initiator_id: UserId,
|
||||||
// TODO
|
/// Always false if this is a channel point reward event
|
||||||
// pub moderator: bool,
|
pub broadcaster: bool,
|
||||||
// pub broadcaster: bool,
|
/// Always false if this is a channel point reward event
|
||||||
// pub vip: bool,
|
pub moderator: bool,
|
||||||
|
/// Always false if this is a channel point reward event
|
||||||
|
pub vip: bool,
|
||||||
|
/// Always false if this is a channel point reward event
|
||||||
|
pub subscriber: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExecutorState {
|
impl ExecutorState {
|
||||||
|
@ -44,39 +48,45 @@ impl ExecutorState {
|
||||||
self.args.clone()
|
self.args.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn internal_get_username(&self, id: UserId) -> Option<String> {
|
pub fn broadcaster(&mut self) -> bool {
|
||||||
self.twitch.client.helix.get_user_from_id(id, &self.twitch.bot_token)
|
self.broadcaster
|
||||||
.await
|
}
|
||||||
.ok()?
|
|
||||||
|
pub fn moderator(&mut self) -> bool {
|
||||||
|
self.moderator
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vip(&mut self) -> bool {
|
||||||
|
self.vip
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscriber(&mut self) -> bool {
|
||||||
|
self.subscriber
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_username<S: Into<UserId>>(&mut self, id: S) -> rhai::Dynamic {
|
||||||
|
self.state.runtime.block_on(self.state.get_user_from_id(id))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
.map(|user| user.login.to_string())
|
.map(|user| user.login.to_string())
|
||||||
}
|
.map(rhai::Dynamic::from)
|
||||||
|
.unwrap_or_else(|| rhai::Dynamic::from(()))
|
||||||
pub fn get_username<S: Into<String>>(&mut self, id: S) -> rhai::Dynamic {
|
|
||||||
match self.runtime.block_on(self.internal_get_username(UserId::new(id.into()))) {
|
|
||||||
Some(x) => rhai::Dynamic::from(x),
|
|
||||||
None => rhai::Dynamic::from(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn internal_get_id<S: Into<UserName>>(&self, username: S) -> Option<UserId> {
|
|
||||||
self.twitch.client.helix.get_user_from_login(username, &self.twitch.bot_token)
|
|
||||||
.await
|
|
||||||
.ok()?
|
|
||||||
.map(|user| user.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_user_id<S: Into<UserName>>(&mut self, username: S) -> rhai::Dynamic {
|
pub fn get_user_id<S: Into<UserName>>(&mut self, username: S) -> rhai::Dynamic {
|
||||||
match self.runtime.block_on(self.internal_get_id(username)).map(|user| user.to_string()) {
|
self.state.runtime.block_on(self.state.get_user_from_login(username))
|
||||||
Some(x) => rhai::Dynamic::from(x),
|
.ok()
|
||||||
None => rhai::Dynamic::from(()),
|
.flatten()
|
||||||
}
|
.map(|user| user.id.to_string())
|
||||||
|
.map(rhai::Dynamic::from)
|
||||||
|
.unwrap_or_else(|| rhai::Dynamic::from(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn internal_get_channel_info<S: Into<UserId>>(&self, id: S) -> Option<(String, String, String)> {
|
async fn internal_get_channel_info<S: Into<UserId>>(&self, id: S) -> Option<(String, String, String)> {
|
||||||
let req = GetChannelInformationRequest::builder()
|
let req = GetChannelInformationRequest::builder()
|
||||||
.broadcaster_id(id.into())
|
.broadcaster_id(id.into())
|
||||||
.build();
|
.build();
|
||||||
self.twitch.client.helix.req_get(req, &self.twitch.bot_token)
|
self.state.twitch.client.helix.req_get(req, &self.state.twitch.bot_token)
|
||||||
.await
|
.await
|
||||||
.ok()?
|
.ok()?
|
||||||
.data
|
.data
|
||||||
|
@ -84,7 +94,7 @@ impl ExecutorState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_channel_info<S: Into<UserId>>(&mut self, id: S) -> rhai::Dynamic {
|
pub fn get_channel_info<S: Into<UserId>>(&mut self, id: S) -> rhai::Dynamic {
|
||||||
match self.runtime.block_on(self.internal_get_channel_info(id)) {
|
match self.state.runtime.block_on(self.internal_get_channel_info(id)) {
|
||||||
Some(x) => rhai::Dynamic::from(vec![
|
Some(x) => rhai::Dynamic::from(vec![
|
||||||
rhai::Dynamic::from(x.0),
|
rhai::Dynamic::from(x.0),
|
||||||
rhai::Dynamic::from(x.1),
|
rhai::Dynamic::from(x.1),
|
||||||
|
|
|
@ -23,14 +23,14 @@ pub async fn handle_irc_event(state: Arc<State>, event: Message) -> anyhow::Resu
|
||||||
match &event.command {
|
match &event.command {
|
||||||
Command::Response(resp, _) if *resp == Response::RPL_WELCOME => {
|
Command::Response(resp, _) if *resp == Response::RPL_WELCOME => {
|
||||||
state.irc.send_join(&channel_name)?;
|
state.irc.send_join(&channel_name)?;
|
||||||
},
|
}
|
||||||
// FIXME: do correct checking here
|
// FIXME: do correct checking here
|
||||||
Command::PRIVMSG(channel, message) if *channel == channel_name => {
|
Command::PRIVMSG(channel, message) if *channel == channel_name => {
|
||||||
on_privmsg(state, &event, &message).await?;
|
on_privmsg(state, &event, &message).await?;
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// eprintln!("{:#?}", c);
|
// eprintln!("{:#?}", c);
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -41,14 +41,26 @@ async fn on_privmsg(state: Arc<State>, event: &Message, message: &str) -> anyhow
|
||||||
.ok_or_else(|| anyhow::anyhow!("missing source"))?
|
.ok_or_else(|| anyhow::anyhow!("missing source"))?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let initiator_id = event.tags
|
let tags = event.tags.as_ref().ok_or_else(|| anyhow::anyhow!("missing tags"))?;
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("missing tags"))?
|
let initiator_id = tags
|
||||||
.iter()
|
.iter()
|
||||||
.find(|tag| tag.0 == "user-id")
|
.find(|tag| tag.0 == "user-id")
|
||||||
.and_then(|tag| tag.1.clone())
|
.and_then(|tag| tag.1.clone())
|
||||||
.ok_or_else(|| anyhow::anyhow!("missing user id"))?;
|
.ok_or_else(|| anyhow::anyhow!("missing user id"))?;
|
||||||
|
|
||||||
|
let badges: Vec<&str> = tags
|
||||||
|
.iter()
|
||||||
|
.find(|tag| tag.0 == "badges")
|
||||||
|
.and_then(|tag| tag.1.as_ref())
|
||||||
|
.map(|s| s.split(',').collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let broadcaster = badges.iter().any(|badge| badge.starts_with("broadcaster/"));
|
||||||
|
let moderator = badges.iter().any(|badge| badge.starts_with("moderator/"));
|
||||||
|
let vip = badges.iter().any(|badge| badge.starts_with("vip/"));
|
||||||
|
let subscriber = badges.iter().any(|badge| badge.starts_with("subscriber/"));
|
||||||
|
|
||||||
let words: Vec<&str> = message.split(' ').collect();
|
let words: Vec<&str> = message.split(' ').collect();
|
||||||
let command_name = words[0];
|
let command_name = words[0];
|
||||||
let args = words[1..].iter()
|
let args = words[1..].iter()
|
||||||
|
@ -61,23 +73,47 @@ async fn on_privmsg(state: Arc<State>, event: &Message, message: &str) -> anyhow
|
||||||
.commands
|
.commands
|
||||||
.iter().find(|command| command.name == command_name || command.aliases.iter().any(|alias| alias == command_name))
|
.iter().find(|command| command.name == command_name || command.aliases.iter().any(|alias| alias == command_name))
|
||||||
.map(Clone::clone);
|
.map(Clone::clone);
|
||||||
let command = match command {
|
let command: crate::app::config::Command = match command {
|
||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
None => return Ok(())
|
None => return Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(gcd) = &command.cooldowns.global {
|
||||||
|
if let Some(last_used) = state.global_cooldowns.read().await.get(&command.name).cloned() {
|
||||||
|
if last_used.elapsed() < gcd.to_std().unwrap() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.global_cooldowns.write().await.insert(command.name.clone(), Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cooldown) = &command.cooldowns.user {
|
||||||
|
let user_id = UserId::new(&initiator_id);
|
||||||
|
if let Some(last_used) = state.user_cooldowns.read().await.get(&(user_id.clone(), command.name.clone())).cloned() {
|
||||||
|
if last_used.elapsed() < cooldown.to_std().unwrap() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.user_cooldowns.write().await.insert((user_id, command.name.clone()), Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
match &command.executor {
|
match &command.executor {
|
||||||
CommandExecutor::Text(t) => { state.irc_queue.send(t.to_string()).ok(); },
|
CommandExecutor::Text(t) => { state.irc_queue.send(t.to_string()).ok(); }
|
||||||
CommandExecutor::Rhai(t) => {
|
CommandExecutor::Rhai(t) => {
|
||||||
let fn_state = ExecutorState {
|
let fn_state = ExecutorState {
|
||||||
twitch: Arc::clone(&state.twitch),
|
state: Arc::clone(&state),
|
||||||
initiator_id: UserId::new(initiator_id),
|
|
||||||
initiator,
|
|
||||||
args,
|
args,
|
||||||
runtime: state.runtime.clone(),
|
initiator,
|
||||||
|
initiator_id: UserId::new(initiator_id),
|
||||||
|
broadcaster,
|
||||||
|
moderator,
|
||||||
|
vip,
|
||||||
|
subscriber,
|
||||||
};
|
};
|
||||||
crate::app::run_rhai(Arc::clone(&state), t, fn_state);
|
crate::app::run_rhai(Arc::clone(&state), t, fn_state);
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -88,6 +124,7 @@ use twitch_api2::pubsub::{
|
||||||
TopicData,
|
TopicData,
|
||||||
channel_points::ChannelPointsChannelV1Reply,
|
channel_points::ChannelPointsChannelV1Reply,
|
||||||
};
|
};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
pub async fn handle_pubsub(state: Arc<State>, event: WsMessage) -> anyhow::Result<()> {
|
pub async fn handle_pubsub(state: Arc<State>, event: WsMessage) -> anyhow::Result<()> {
|
||||||
let json = match event {
|
let json = match event {
|
||||||
|
@ -116,11 +153,14 @@ pub async fn handle_pubsub(state: Arc<State>, event: WsMessage) -> anyhow::Resul
|
||||||
.map(|input| input.split(' ').map(ToOwned::to_owned).map(rhai::Dynamic::from).collect())
|
.map(|input| input.split(' ').map(ToOwned::to_owned).map(rhai::Dynamic::from).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let fn_state = ExecutorState {
|
let fn_state = ExecutorState {
|
||||||
twitch: Arc::clone(&state.twitch),
|
state: Arc::clone(&state),
|
||||||
|
args,
|
||||||
initiator: redemption.user.login.to_string(),
|
initiator: redemption.user.login.to_string(),
|
||||||
initiator_id: redemption.user.id,
|
initiator_id: redemption.user.id,
|
||||||
args,
|
broadcaster: false,
|
||||||
runtime: state.runtime.clone(),
|
moderator: false,
|
||||||
|
vip: false,
|
||||||
|
subscriber: false,
|
||||||
};
|
};
|
||||||
crate::app::run_rhai(Arc::clone(&state), &action.rhai, fn_state);
|
crate::app::run_rhai(Arc::clone(&state), &action.rhai, fn_state);
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ enum CustomRejection {
|
||||||
InvalidAccessToken,
|
InvalidAccessToken,
|
||||||
InvalidForm,
|
InvalidForm,
|
||||||
TwitchError,
|
TwitchError,
|
||||||
|
InvalidCommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl warp::reject::Reject for CustomRejection {}
|
impl warp::reject::Reject for CustomRejection {}
|
||||||
|
@ -57,6 +58,7 @@ async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
|
||||||
CustomRejection::InvalidAccessToken => (StatusCode::UNAUTHORIZED, Cow::from("invalid access token")),
|
CustomRejection::InvalidAccessToken => (StatusCode::UNAUTHORIZED, Cow::from("invalid access token")),
|
||||||
CustomRejection::InvalidForm => (StatusCode::BAD_REQUEST, Cow::from("invalid form submission")),
|
CustomRejection::InvalidForm => (StatusCode::BAD_REQUEST, Cow::from("invalid form submission")),
|
||||||
CustomRejection::TwitchError => (StatusCode::INTERNAL_SERVER_ERROR, Cow::from("twitch error")),
|
CustomRejection::TwitchError => (StatusCode::INTERNAL_SERVER_ERROR, Cow::from("twitch error")),
|
||||||
|
CustomRejection::InvalidCommand => (StatusCode::BAD_REQUEST, Cow::from("invalid command")),
|
||||||
// CustomRejection::Askama(e) => (StatusCode::INTERNAL_SERVER_ERROR, Cow::from(format!("templating error: {:#?}", e))),
|
// CustomRejection::Askama(e) => (StatusCode::INTERNAL_SERVER_ERROR, Cow::from(format!("templating error: {:#?}", e))),
|
||||||
}
|
}
|
||||||
} else if let Some(e) = err.find::<warp::reject::MissingCookie>() {
|
} else if let Some(e) = err.find::<warp::reject::MissingCookie>() {
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
use warp::{
|
use warp::{Filter, Reply, filters::BoxedFilter, http::Uri, Rejection};
|
||||||
Filter, Reply,
|
|
||||||
filters::BoxedFilter,
|
|
||||||
http::Uri,
|
|
||||||
};
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
State,
|
State,
|
||||||
config::{CommandExecutor, Command, Cooldowns},
|
config::{CommandExecutor, Command, Cooldowns},
|
||||||
web::{
|
web::{
|
||||||
CustomRejection,
|
CustomRejection,
|
||||||
template::commands::{CommandsTemplate, AddCommandTemplate},
|
template::commands::{CommandsTemplate, AddCommandTemplate, EditCommandTemplate},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -16,16 +12,20 @@ use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use chrono::Duration;
|
||||||
|
|
||||||
pub fn commands_routes(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
pub fn commands_routes(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||||
warp::get()
|
warp::get()
|
||||||
.and(
|
.and(
|
||||||
commands_get(Arc::clone(&state))
|
commands_get(Arc::clone(&state))
|
||||||
.or(commands_add_get())
|
.or(commands_add_get())
|
||||||
|
.or(commands_edit_get(Arc::clone(&state)))
|
||||||
)
|
)
|
||||||
.or(warp::post().and(
|
.or(warp::post().and(
|
||||||
commands_add_post(Arc::clone(&state))
|
commands_add_post(Arc::clone(&state))
|
||||||
.or(commands_delete_post(Arc::clone(&state)))
|
.or(commands_delete_post(Arc::clone(&state)))
|
||||||
|
.or(commands_edit_post(Arc::clone(&state)))
|
||||||
))
|
))
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
@ -52,41 +52,60 @@ fn commands_add_get() -> BoxedFilter<(impl Reply, )> {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commands_add_post(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
fn get_command_from_body(mut form: HashMap<String, String>) -> Result<Command, Rejection> {
|
||||||
warp::path("commands")
|
|
||||||
.and(warp::path("add"))
|
|
||||||
.and(warp::path::end())
|
|
||||||
.and(warp::body::content_length_limit(1024 * 5))
|
|
||||||
.and(warp::body::form())
|
|
||||||
.and_then(move |mut form: HashMap<String, String>| {
|
|
||||||
let state = Arc::clone(&state);
|
|
||||||
async move {
|
|
||||||
let form_get = try {
|
let form_get = try {
|
||||||
let name = form.remove("name")?;
|
let name = form.remove("name")?;
|
||||||
let aliases = form.remove("aliases")?;
|
let aliases = form.remove("aliases")?;
|
||||||
|
let cooldown = form.remove("cooldown").and_then(|t| if t.trim().is_empty() { None } else { Some(t) });
|
||||||
|
let gcd = form.remove("gcd").and_then(|t| if t.trim().is_empty() { None } else { Some(t) });
|
||||||
let kind = form.remove("type")?;
|
let kind = form.remove("type")?;
|
||||||
let script = form.remove("executor_data")?;
|
let script = form.remove("executor_data")?;
|
||||||
(name, aliases, kind, script)
|
(name, aliases, cooldown, gcd, kind, script)
|
||||||
};
|
};
|
||||||
|
|
||||||
let (name, aliases, kind, script) = match form_get {
|
let (name, aliases, cooldown, gcd, kind, script) = match form_get {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return Err(warp::reject::custom(CustomRejection::InvalidForm)),
|
None => return Err(warp::reject::custom(CustomRejection::InvalidForm)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let gcd = gcd
|
||||||
|
.and_then(|t| f64::from_str(&t).ok())
|
||||||
|
.map(|f| Duration::milliseconds((f * 1_000.0) as i64));
|
||||||
|
|
||||||
|
let cooldown = cooldown
|
||||||
|
.and_then(|t| f64::from_str(&t).ok())
|
||||||
|
.map(|f| Duration::milliseconds((f * 1_000.0) as i64));
|
||||||
|
|
||||||
let executor = match &*kind {
|
let executor = match &*kind {
|
||||||
"Text" => CommandExecutor::Text(script),
|
"Text" => CommandExecutor::Text(script),
|
||||||
"Rhai" => CommandExecutor::Rhai(script),
|
"Rhai" => CommandExecutor::Rhai(script),
|
||||||
_ => return Err(warp::reject::custom(CustomRejection::InvalidForm)),
|
_ => return Err(warp::reject::custom(CustomRejection::InvalidForm)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let command = Command {
|
Ok(Command {
|
||||||
name,
|
name,
|
||||||
executor,
|
executor,
|
||||||
aliases: aliases.split("\n").map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect(),
|
aliases: aliases.split("\n").map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect(),
|
||||||
cooldowns: Cooldowns::default(),
|
cooldowns: Cooldowns {
|
||||||
};
|
global: gcd,
|
||||||
|
user: cooldown,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commands_add_post(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||||
|
warp::path("commands")
|
||||||
|
.and(warp::path("add"))
|
||||||
|
.and(warp::path::end())
|
||||||
|
.and(warp::body::content_length_limit(1024 * 5))
|
||||||
|
.and(warp::body::form())
|
||||||
|
.and_then(move |form: HashMap<String, String>| {
|
||||||
|
let state = Arc::clone(&state);
|
||||||
|
async move {
|
||||||
|
let command = match get_command_from_body(form) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
state.config.write().await.commands.push(command);
|
state.config.write().await.commands.push(command);
|
||||||
|
|
||||||
Ok(warp::redirect(Uri::from_static("/commands")))
|
Ok(warp::redirect(Uri::from_static("/commands")))
|
||||||
|
@ -95,6 +114,57 @@ fn commands_add_post(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn commands_edit_get(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||||
|
warp::path("commands")
|
||||||
|
.and(warp::path("edit"))
|
||||||
|
.and(warp::path::param())
|
||||||
|
.and(warp::path::end())
|
||||||
|
.and_then(move |name: String| {
|
||||||
|
let state = Arc::clone(&state);
|
||||||
|
async move {
|
||||||
|
match state.config.read().await.commands
|
||||||
|
.iter()
|
||||||
|
.find(|command| command.name == name)
|
||||||
|
.cloned()
|
||||||
|
{
|
||||||
|
Some(command) => Ok(command),
|
||||||
|
None => Err(warp::reject::custom(CustomRejection::InvalidCommand)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|command: Command| EditCommandTemplate {
|
||||||
|
command,
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commands_edit_post(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||||
|
warp::path("commands")
|
||||||
|
.and(warp::path("edit"))
|
||||||
|
.and(warp::path::end())
|
||||||
|
.and(warp::body::content_length_limit(1024 * 5))
|
||||||
|
.and(warp::body::form())
|
||||||
|
.and_then(move |form: HashMap<String, String>| {
|
||||||
|
let state = Arc::clone(&state);
|
||||||
|
async move {
|
||||||
|
let command = match get_command_from_body(form) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
let mut config = state.config.write().await;
|
||||||
|
for existing in config.commands.iter_mut() {
|
||||||
|
if existing.name == &*command.name {
|
||||||
|
*existing = command;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(warp::redirect(Uri::from_static("/commands")))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
fn commands_delete_post(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
fn commands_delete_post(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||||
warp::path("commands")
|
warp::path("commands")
|
||||||
.and(warp::path("delete"))
|
.and(warp::path("delete"))
|
||||||
|
|
|
@ -17,12 +17,15 @@ use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use twitch_api2::helix::points::{GetCustomRewardRequest, CustomReward};
|
||||||
|
use crate::app::web::template::redemptions::ListRedemptionsTemplate;
|
||||||
|
|
||||||
pub fn redemptions_routes(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
pub fn redemptions_routes(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||||
warp::get()
|
warp::get()
|
||||||
.and(
|
.and(
|
||||||
redemptions_get(Arc::clone(&state))
|
redemptions_get(Arc::clone(&state))
|
||||||
.or(redemptions_add_get())
|
.or(redemptions_add_get())
|
||||||
|
.or(redemptions_list_get(Arc::clone(&state)))
|
||||||
)
|
)
|
||||||
.or(warp::post().and(
|
.or(warp::post().and(
|
||||||
redemptions_add_post(Arc::clone(&state))
|
redemptions_add_post(Arc::clone(&state))
|
||||||
|
@ -109,3 +112,26 @@ fn redemptions_delete_post(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn redemptions_list_get(state: Arc<State>) -> BoxedFilter<(impl Reply, )> {
|
||||||
|
warp::path("redemptions")
|
||||||
|
.and(warp::path("list"))
|
||||||
|
.and(warp::path::end())
|
||||||
|
.and_then( move || {
|
||||||
|
let state = Arc::clone(&state);
|
||||||
|
async move {
|
||||||
|
let req = GetCustomRewardRequest::builder()
|
||||||
|
.broadcaster_id(state.user_config.twitch.channel_id.to_string())
|
||||||
|
.build();
|
||||||
|
let rewards: Vec<CustomReward> = match state.twitch.client.helix.req_get(req, &state.twitch.user_token).await {
|
||||||
|
Ok(resp) => resp.data,
|
||||||
|
Err(_) => return Err(warp::reject::custom(CustomRejection::TwitchError)),
|
||||||
|
};
|
||||||
|
Ok(rewards)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|rewards: Vec<CustomReward>| ListRedemptionsTemplate {
|
||||||
|
rewards,
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use crate::app::config::{
|
use crate::app::{
|
||||||
|
duration_tools::DurationTools,
|
||||||
|
config::{
|
||||||
Command,
|
Command,
|
||||||
CommandExecutor,
|
CommandExecutor,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
@ -13,3 +16,9 @@ pub struct CommandsTemplate {
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "add_command.html")]
|
#[template(path = "add_command.html")]
|
||||||
pub struct AddCommandTemplate;
|
pub struct AddCommandTemplate;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "edit_command.html")]
|
||||||
|
pub struct EditCommandTemplate {
|
||||||
|
pub command: Command,
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,26 @@
|
||||||
form > * {
|
form > * {
|
||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs li:not(:first-child)::before {
|
||||||
|
content: '/';
|
||||||
|
margin-left: .5em;
|
||||||
|
margin-right: .5em;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs li.future {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs li.current {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% block head %}{% endblock head %}
|
{% block head %}{% endblock head %}
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -3,9 +3,19 @@
|
||||||
{% block title %}Add command{% endblock %}
|
{% block title %}Add command{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
<ul class="breadcrumbs">
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/commands">Commands</a></li>
|
||||||
|
<li class="current"><a href="/commands/add">Add</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<form action="/commands/add" method="post">
|
<form action="/commands/add" method="post">
|
||||||
<input type="text" name="name" placeholder="Name"/>
|
<input type="text" name="name" placeholder="Name"/>
|
||||||
<textarea name="aliases" placeholder="Aliases separated by newlines"></textarea>
|
<textarea name="aliases" placeholder="Aliases separated by newlines"></textarea>
|
||||||
|
<div>
|
||||||
|
<input type="number" step="0.01" name="cooldown" placeholder="Cooldown (s)" min="0"/>
|
||||||
|
<input type="number" step="0.01" name="gcd" placeholder="GCD (s)" min="0"/>
|
||||||
|
</div>
|
||||||
<select name="type">
|
<select name="type">
|
||||||
<option>Text</option>
|
<option>Text</option>
|
||||||
<option>Rhai</option>
|
<option>Rhai</option>
|
||||||
|
|
|
@ -3,6 +3,12 @@
|
||||||
{% block title %}Add redemption{% endblock %}
|
{% block title %}Add redemption{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
<ul class="breadcrumbs">
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/redemptions">Redemptions</a></li>
|
||||||
|
<li class="current"><a href="/redemptions/add">Add</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<form action="/redemptions/add" method="post">
|
<form action="/redemptions/add" method="post">
|
||||||
<input type="text" name="name" placeholder="Name"/>
|
<input type="text" name="name" placeholder="Name"/>
|
||||||
<input type="text" name="twitch_id" placeholder="Twitch redemption ID"/>
|
<input type="text" name="twitch_id" placeholder="Twitch redemption ID"/>
|
||||||
|
|
|
@ -20,9 +20,11 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div>
|
<ul class="breadcrumbs">
|
||||||
<a href="/commands/add">Add</a>
|
<li><a href="/">Home</a></li>
|
||||||
</div>
|
<li class="current"><a href="/commands">Commands</a></li>
|
||||||
|
<li class="future"><a href="/commands/add">Add</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
{% for command in commands %}
|
{% for command in commands %}
|
||||||
<div class="command">
|
<div class="command">
|
||||||
|
@ -30,6 +32,22 @@
|
||||||
{% if !command.aliases.is_empty() %}
|
{% if !command.aliases.is_empty() %}
|
||||||
<em>{{ command.aliases.join(", ") }}</em>
|
<em>{{ command.aliases.join(", ") }}</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{%- match command.cooldowns.user -%}
|
||||||
|
{%- when Some with (c) -%}
|
||||||
|
Cooldown: {{ c.seconds_f64() }}s
|
||||||
|
{%- else -%}
|
||||||
|
{%- endmatch -%}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{%- match command.cooldowns.global -%}
|
||||||
|
{%- when Some with (c) -%}
|
||||||
|
GCD: {{ c.seconds_f64() }}s
|
||||||
|
{%- else -%}
|
||||||
|
{%- endmatch -%}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<pre><code>
|
<pre><code>
|
||||||
{%- match command.executor -%}
|
{%- match command.executor -%}
|
||||||
{%- when CommandExecutor::Text with (t) -%}
|
{%- when CommandExecutor::Text with (t) -%}
|
||||||
|
@ -38,6 +56,7 @@
|
||||||
{{ t }}
|
{{ t }}
|
||||||
{%- endmatch -%}
|
{%- endmatch -%}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
<a href="/commands/edit/{{ command.name }}">Edit</a>
|
||||||
<form action="/commands/delete" method="post">
|
<form action="/commands/delete" method="post">
|
||||||
<input type="hidden" name="name" value="{{ command.name }}"/>
|
<input type="hidden" name="name" value="{{ command.name }}"/>
|
||||||
<button type="submit">Delete</button>
|
<button type="submit">Delete</button>
|
||||||
|
|
50
templates/edit_command.html
Normal file
50
templates/edit_command.html
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{% extends "_base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Edit command{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<ul class="breadcrumbs">
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/commands">Commands</a></li>
|
||||||
|
<li class="current"><a href="/commands/edit/{{ command.name }}">Edit</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<form action="/commands/edit" method="post">
|
||||||
|
<input type="text" name="name" placeholder="Name" value="{{ command.name }}"/>
|
||||||
|
<textarea name="aliases" placeholder="Aliases separated by newlines">{{ command.aliases.join("\n") }}</textarea>
|
||||||
|
<div>
|
||||||
|
{% let user %}
|
||||||
|
{% match command.cooldowns.user %}
|
||||||
|
{% when Some with (cd) %}
|
||||||
|
{% let user = cd.seconds_f64().to_string() %}
|
||||||
|
{% else %}
|
||||||
|
{% let user = "".to_string() %}
|
||||||
|
{% endmatch %}
|
||||||
|
<input type="number" step="0.01" name="cooldown" placeholder="Cooldown (s)" min="0" value="{{ user }}"/>
|
||||||
|
|
||||||
|
{% let global %}
|
||||||
|
{% match command.cooldowns.global %}
|
||||||
|
{% when Some with (cd) %}
|
||||||
|
{% let global = cd.seconds_f64().to_string() %}
|
||||||
|
{% else %}
|
||||||
|
{% let global = "".to_string() %}
|
||||||
|
{% endmatch %}
|
||||||
|
<input type="number" step="0.01" name="gcd" placeholder="GCD (s)" min="0" value = "{{ global }}"/>
|
||||||
|
</div>
|
||||||
|
{% match command.executor %}
|
||||||
|
{% when CommandExecutor::Text with (text) %}
|
||||||
|
<select name="type">
|
||||||
|
<option selected>Text</option>
|
||||||
|
<option>Rhai</option>
|
||||||
|
</select>
|
||||||
|
<textarea name="executor_data" placeholder="Text/script">{{ text }}</textarea>
|
||||||
|
{% when CommandExecutor::Rhai with (rhai) %}
|
||||||
|
<select name="type">
|
||||||
|
<option>Text</option>
|
||||||
|
<option selected>Rhai</option>
|
||||||
|
</select>
|
||||||
|
<textarea name="executor_data" placeholder="Text/script">{{ rhai }}</textarea>
|
||||||
|
{% endmatch %}
|
||||||
|
<button type="submit">Edit</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -11,10 +11,9 @@
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ul>
|
<ul class="breadcrumbs">
|
||||||
<li>
|
<li class="current"><a href="/">Home</a></li>
|
||||||
<a href="/commands">Commands</a>
|
<li class="future"><a href="/commands">Commands</a></li>
|
||||||
<a href="/redemptions">Redemptions</a>
|
<li class="future"><a href="/redemptions">Redemptions</a></li>
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -20,6 +20,12 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
<ul class="breadcrumbs">
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/redemptions">Redemptions</a></li>
|
||||||
|
<li class="current"><a href="/redemptions/list">List</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
{% for reward in rewards %}
|
{% for reward in rewards %}
|
||||||
<div class="reward">
|
<div class="reward">
|
||||||
<strong>{{ reward.title }}</strong>
|
<strong>{{ reward.title }}</strong>
|
||||||
|
|
|
@ -20,9 +20,12 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div>
|
<ul class="breadcrumbs">
|
||||||
<a href="/redemptions/add">Add</a>
|
<li><a href="/">Home</a></li>
|
||||||
</div>
|
<li class="current"><a href="/redemptions">Redemptions</a></li>
|
||||||
|
<li class="future"><a href="/redemptions/add">Add</a></li>
|
||||||
|
<li class="future"><a href="/redemptions/list">List</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
{% for redemption in redemptions %}
|
{% for redemption in redemptions %}
|
||||||
<div class="redemption">
|
<div class="redemption">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user