foukoapi 0.1.2-alpha.2

Cross-platform bot framework in Rust: one codebase, many platforms. Shared accounts, embeds, keyboards, economy, i18n and pluggable storage; Telegram and Discord adapters included.
Documentation
//! Platform descriptors.
//!
//! A [`Platform`] is just a thin description of which adapter to spin up and
//! with what credentials. The actual driving logic lives in
//! [`crate::adapters`].

use std::fmt;

/// Which chat platform an update came from / should be dispatched to.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlatformKind {
    /// Telegram (MTProto Bot API).
    Telegram,
    /// Discord (gateway + REST).
    Discord,
}

impl fmt::Display for PlatformKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::Telegram => "telegram",
            Self::Discord => "discord",
        })
    }
}

/// A platform the [`Bot`](crate::Bot) should run on.
///
/// Built through the short constructors: [`Platform::telegram`],
/// [`Platform::discord`]. Each one returns a `Platform` even if that
/// platform's feature is turned off at compile time - the missing
/// adapter will simply log a warning and be skipped when the bot
/// starts, instead of failing the build.
#[derive(Debug, Clone)]
pub struct Platform {
    pub(crate) kind: PlatformKind,
    pub(crate) config: PlatformConfig,
}

#[derive(Debug, Clone)]
#[non_exhaustive]
#[allow(dead_code)] // Fields are read only when the corresponding adapter feature is on.
pub(crate) enum PlatformConfig {
    /// Telegram bot token from @BotFather.
    Telegram { token: String },
    /// Discord bot token.
    Discord { token: String },
}

impl Platform {
    /// Add a Telegram bot. `token` is whatever BotFather gives you.
    pub fn telegram(token: impl Into<String>) -> Self {
        Self {
            kind: PlatformKind::Telegram,
            config: PlatformConfig::Telegram {
                token: token.into(),
            },
        }
    }

    /// Add a Discord bot.
    pub fn discord(token: impl Into<String>) -> Self {
        Self {
            kind: PlatformKind::Discord,
            config: PlatformConfig::Discord {
                token: token.into(),
            },
        }
    }

    /// Which kind of platform this descriptor points at.
    pub fn kind(&self) -> PlatformKind {
        self.kind
    }
}

/// What the bot appears to be doing, shown in the member list and on its
/// profile. Discord renders it natively ("Playing X", a purple "LIVE"
/// badge for streaming, and so on); platforms without presence (Telegram)
/// ignore it.
///
/// ```no_run
/// use foukoapi::{Bot, Presence};
///
/// Bot::new()
///     .presence(Presence::streaming("bot.fouko.xyz", "https://bot.fouko.xyz"))
///     /* .add_platform(..) */;
/// ```
#[derive(Debug, Clone)]
pub struct Presence {
    pub(crate) kind: PresenceKind,
    pub(crate) name: String,
    /// Stream URL; only meaningful for [`PresenceKind::Streaming`].
    /// Discord shows the purple badge only for twitch.tv/youtube.com
    /// links, but any https URL is accepted and kept as the click-through.
    pub(crate) url: Option<String>,
    /// The smaller detail line under the activity name, when the client
    /// shows one (Discord does for streaming and rich activities).
    pub(crate) state: Option<String>,
    pub(crate) status: PresenceStatus,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PresenceKind {
    Playing,
    Streaming,
    Listening,
    Watching,
    Competing,
    Custom,
}

/// The bot's online dot.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum PresenceStatus {
    /// Green: online (the default).
    #[default]
    Online,
    /// Yellow: idle / away.
    Idle,
    /// Red: do not disturb.
    DoNotDisturb,
    /// Grey: appears offline while still working.
    Invisible,
}

impl Presence {
    fn new(kind: PresenceKind, name: impl Into<String>) -> Self {
        Self {
            kind,
            name: name.into(),
            url: None,
            state: None,
            status: PresenceStatus::Online,
        }
    }

    /// "Playing `name`".
    pub fn playing(name: impl Into<String>) -> Self {
        Self::new(PresenceKind::Playing, name)
    }

    /// "Streaming `name`" with a click-through `url`.
    pub fn streaming(name: impl Into<String>, url: impl Into<String>) -> Self {
        let mut p = Self::new(PresenceKind::Streaming, name);
        p.url = Some(url.into());
        p
    }

    /// "Listening to `name`".
    pub fn listening(name: impl Into<String>) -> Self {
        Self::new(PresenceKind::Listening, name)
    }

    /// "Watching `name`".
    pub fn watching(name: impl Into<String>) -> Self {
        Self::new(PresenceKind::Watching, name)
    }

    /// "Competing in `name`".
    pub fn competing(name: impl Into<String>) -> Self {
        Self::new(PresenceKind::Competing, name)
    }

    /// A custom status - the free-form line under the bot's name, no
    /// "Playing"/"Streaming" prefix. The place for a joke.
    pub fn custom(text: impl Into<String>) -> Self {
        Self::new(PresenceKind::Custom, text)
    }

    /// Change the online dot; green by default.
    pub fn status(mut self, status: PresenceStatus) -> Self {
        self.status = status;
        self
    }

    /// The smaller detail line under the activity name - "Streaming
    /// `<name>`" on top, this underneath. Discord shows it in the profile
    /// card; clients that have no place for it just skip it.
    pub fn state(mut self, state: impl Into<String>) -> Self {
        self.state = Some(state.into());
        self
    }
}