use std::sync::Arc;
use nu_plugin::EvaluatedCall;
use nu_plugin::{EngineInterface, PluginCommand};
use nu_protocol::{LabeledError, PipelineData, Signature, SyntaxShape, Type, Value};
use tokio::runtime::Runtime;
use twitch_irc::login::RefreshingLoginCredentials;
use twitch_irc::{ClientConfig, SecureTCPTransport, TwitchIRCClient};
use crate::TwitchPlugin;
use crate::auth::{ApplicationCredentials, ProfileTokenProvider};
type TwitchProfileClient =
TwitchIRCClient<SecureTCPTransport, RefreshingLoginCredentials<ProfileTokenProvider>>;
pub struct TwitchSay;
impl PluginCommand for TwitchSay {
type Plugin = TwitchPlugin;
fn name(&self) -> &str {
"twitch say"
}
fn description(&self) -> &str {
"send input messages to the given channel"
}
fn signature(&self) -> Signature {
Signature::build(PluginCommand::name(self))
.required(
"CHANNEL",
SyntaxShape::String,
"channel to send messages to",
)
.named("as", SyntaxShape::String, "profile to send from", None)
.input_output_types(vec![
(Type::String, Type::Nothing),
(Type::List(Box::new(Type::String)), Type::Nothing),
])
}
fn run(
&self,
plugin: &TwitchPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: PipelineData,
) -> Result<PipelineData, LabeledError> {
plugin.setup_tracing();
let span = call.head;
let to = call.nth(0).unwrap().coerce_into_string()?;
let profile = call
.named
.first()
.map(|(_, v)| v.clone().unwrap().coerce_into_string().unwrap());
let app = ApplicationCredentials::load()?;
let token_storage = ProfileTokenProvider {
profile: profile.clone(),
};
tracing::debug!(command = "say", profile, "Loaded bot profile");
let credentials =
RefreshingLoginCredentials::init(app.client_id, app.client_secret, token_storage);
let config = ClientConfig::new_simple(credentials);
let (_, client) = plugin.reactor.block_on(async {
TwitchIRCClient::<
SecureTCPTransport,
RefreshingLoginCredentials<ProfileTokenProvider>,
>::new(config)
});
tracing::debug!(command = "say", profile, "Client initialized");
let messages: Box<dyn Iterator<Item = Result<String, LabeledError>>> =
match input {
PipelineData::Value(Value::String { val, .. }, _) => {
Box::new(vec![Ok(val)].into_iter())
}
PipelineData::Value(Value::List { vals, .. }, _) => {
Box::new(vals.into_iter().map(|msg| match msg {
Value::String { val: msg, .. } => Ok(msg.clone()),
_ => Err(LabeledError::new("invalid input").with_label(
"only string and list<string> input data is supported",
span,
)),
}))
}
PipelineData::ListStream(stream, _) => {
Box::new(stream.into_iter().map(|msg| match msg {
Value::String { val: msg, .. } => Ok(msg),
_ => Err(LabeledError::new("invalid input").with_label(
"only string and list<string> input data is supported",
span,
)),
}))
}
_ => Box::new(
vec![Err(LabeledError::new("invalid input").with_label(
"only string and list<string> input data is supported",
span,
))]
.into_iter(),
),
};
#[allow(clippy::manual_try_fold)]
messages.fold(Ok(PipelineData::Empty), |status, msg| {
status.and_then(|_| {
msg.and_then(|msg| Self::say(&plugin.reactor, client.clone(), to.clone(), msg))
})
})
}
}
impl TwitchSay {
fn say(
reactor: &Arc<Runtime>,
client: TwitchProfileClient,
to: String,
message: String,
) -> Result<PipelineData, LabeledError> {
reactor.block_on(async {
client
.say(to, message)
.await
.map(|_| PipelineData::Empty)
.map_err(|e| {
LabeledError::new("Failed to send message")
.with_inner(LabeledError::new(e.to_string()))
})
})
}
}