nu_plugin_twitch 0.1.1

A Nu Shell plugin to interact with Twitch IRC and API, making scripting easier.
Documentation
use std::any::Any;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use nu_protocol::{CustomValue, Record, ShellError, Span, Value, record};
use twitch_irc::message::{UserNoticeEvent, UserNoticeMessage};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TwitchNotice {
    pub channel: String,
    pub sender: String,
    pub message: Option<String>,
    pub event_id: String,
    pub event_type: String,
    pub event: Value,
    pub timestamp: DateTime<Utc>,
}

impl From<UserNoticeMessage> for TwitchNotice {
    fn from(value: UserNoticeMessage) -> Self {
        let span = Span::unknown();
        let (event_type, event) = TwitchNotice::event_record(
            value.sender.login.clone(),
            value.event_id.clone(),
            value.event,
        );
        Self {
            channel: value.channel_login,
            sender: value.sender.login.clone(),
            message: value.message_text,
            event_id: value.event_id.clone(),
            event_type,
            event: if !event.is_empty() {
                Value::record(event, span)
            } else {
                Value::nothing(span)
            },
            timestamp: value.server_timestamp,
        }
    }
}

impl TwitchNotice {
    fn event_record(sender: String, event_id: String, event: UserNoticeEvent) -> (String, Record) {
        let span = Span::unknown();
        match event {
            UserNoticeEvent::Raid {
                viewer_count,
                profile_image_url,
            } => (
                "RAID".into(),
                record!(
                    "from" => Value::string(sender, span),
                    "viewers" => Value::int(viewer_count as i64, span),
                    "profile_image_url" => Value::string(profile_image_url, span),
                ),
            ),
            UserNoticeEvent::SubOrResub {
                is_resub,
                cumulative_months,
                streak_months,
                sub_plan,
                ..
            } => (
                "SUB".into(),
                record!(
                    "is_resub" => Value::bool(is_resub, span),
                    "streak"   => streak_months.map(|s| Value::int(s as i64, span)).unwrap_or(Value::nothing(span)),
                    "total"    => Value::int(cumulative_months as i64, span),
                    "tier"     => Value::string(sub_plan, span),
                ),
            ),
            UserNoticeEvent::GiftPaidUpgrade { gifter_login, .. } => (
                "SUB_GIFT_UPGRADE".into(),
                record!(
                    "from"   => Value::string(gifter_login, span),
                    "to"     => Value::string(sender, span),
                    "tier"   => Value::nothing(span),
                    "months" => Value::int(1, span),
                ),
            ),
            UserNoticeEvent::AnonGiftPaidUpgrade { .. } => (
                "SUB_GIFT_UPGRADE".into(),
                record!(
                    "from"   => Value::nothing(span),
                    "to"     => Value::string(sender, span),
                    "tier"   => Value::nothing(span),
                    "months" => Value::int(1, span),
                ),
            ),
            UserNoticeEvent::SubGift {
                is_sender_anonymous,
                cumulative_months,
                recipient,
                sub_plan,
                num_gifted_months,
                ..
            } => (
                "GIFT_SUB".into(),
                record!(
                    "from"   => if !is_sender_anonymous { Value::string(sender, span) } else { Value::nothing(span) },
                    "to"     => Value::string(recipient.login, span),
                    "tier"   => Value::string(sub_plan, span),
                    "total"  => Value::int(cumulative_months as i64, span),
                    "months" => Value::int(num_gifted_months as i64, span),
                ),
            ),
            UserNoticeEvent::SubMysteryGift {
                mass_gift_count,
                sender_total_gifts,
                sub_plan,
            } => (
                "GIFT_SUBS".into(),
                record!(
                    "from"   => Value::string(sender, span),
                    "to"     => Value::nothing(span),
                    "gifted" => Value::int(mass_gift_count as i64, span),
                    "total"  => Value::int(sender_total_gifts as i64, span),
                    "tier"   => Value::string(sub_plan, span),
                ),
            ),
            _ => match event_id.as_str() {
                "announcement" => ("ANNOUNCEMENT".into(), record!()),
                _ => (
                    "UNKNOWN".into(),
                    record!(
                        "raw" => Value::string(event_id, span),
                    ),
                ),
            },
        }
    }
}

#[typetag::serde]
impl CustomValue for TwitchNotice {
    fn clone_value(&self, span: nu_protocol::Span) -> Value {
        Value::custom(Box::new(self.clone()), span)
    }

    fn type_name(&self) -> String {
        "TwitchNotice".into()
    }

    fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
        Ok(Value::record(
            record! {
                "channel" => Value::string(&self.channel, span),
                "sender"  => Value::string(&self.sender, span),
                "message" => self.message.clone()
                    .map(|msg| Value::string(msg, span))
                    .unwrap_or(Value::nothing(span)),
                "event_id" => Value::string(&self.event_id, span),
                "event_type" => Value::string(&self.event_type, span),
                "event" => self.event.clone().with_span(span),
                "timestamp" => Value::date(self.timestamp.into(), span),
            },
            span,
        ))
    }

    fn as_any(&self) -> &dyn Any {
        self
    }

    fn as_mut_any(&mut self) -> &mut dyn Any {
        self
    }

    /// Follow cell path by string key (e.g. columns)
    fn follow_path_string(
        &self,
        self_span: Span,
        column_name: String,
        path_span: Span,
    ) -> Result<Value, ShellError> {
        self.to_base_value(self_span)
            .unwrap()
            .with_span(path_span)
            .get_data_by_key(&column_name)
            .ok_or(ShellError::IncompatiblePathAccess {
                type_name: self.type_name(),
                span: path_span,
            })
    }
}