nu_plugin_twitch 0.1.1

A Nu Shell plugin to interact with Twitch IRC and API, making scripting easier.
Documentation
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()))
                })
        })
    }
}