use super::{builder::ClusterBuilder, config::Config, event::Events, scheme::ShardScheme};
use crate::{
cluster::event::ShardEventsWithId,
shard::{
raw_message::Message, Command, Config as ShardConfig, Information, ResumeSession, Shard,
},
Intents,
};
use futures_util::{future, stream::SelectAll};
use std::{
collections::{hash_map::Values, HashMap},
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
iter::{FromIterator, FusedIterator},
};
use twilight_http::Client as HttpClient;
#[derive(Debug)]
pub struct ClusterCommandError {
kind: ClusterCommandErrorType,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl ClusterCommandError {
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &ClusterCommandErrorType {
&self.kind
}
#[must_use = "consuming the error and retrieving the source has no effect if left unused"]
pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
self.source
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(
self,
) -> (
ClusterCommandErrorType,
Option<Box<dyn Error + Send + Sync>>,
) {
(self.kind, self.source)
}
}
impl Display for ClusterCommandError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match &self.kind {
ClusterCommandErrorType::Sending => {
f.write_str("sending the message over the websocket failed")
}
ClusterCommandErrorType::ShardNonexistent { id } => {
f.write_str("shard ")?;
Display::fmt(id, f)?;
f.write_str(" does not exist")
}
}
}
}
impl Error for ClusterCommandError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|source| &**source as &(dyn Error + 'static))
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ClusterCommandErrorType {
Sending,
ShardNonexistent {
id: u64,
},
}
#[derive(Debug)]
pub struct ClusterSendError {
kind: ClusterSendErrorType,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl ClusterSendError {
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &ClusterSendErrorType {
&self.kind
}
#[allow(clippy::unused_self)]
#[must_use = "consuming the error and retrieving the source has no effect if left unused"]
pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
self.source
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(self) -> (ClusterSendErrorType, Option<Box<dyn Error + Send + Sync>>) {
(self.kind, self.source)
}
}
impl Display for ClusterSendError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match &self.kind {
ClusterSendErrorType::Sending => f.write_str("failed to send message over websocket"),
ClusterSendErrorType::ShardNonexistent { id } => {
f.write_str("shard ")?;
Display::fmt(id, f)?;
f.write_str(" does not exist")
}
}
}
}
impl Error for ClusterSendError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|source| &**source as &(dyn Error + 'static))
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ClusterSendErrorType {
Sending,
ShardNonexistent {
id: u64,
},
}
#[derive(Debug)]
pub struct ClusterStartError {
kind: ClusterStartErrorType,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl ClusterStartError {
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &ClusterStartErrorType {
&self.kind
}
#[must_use = "consuming the error and retrieving the source has no effect if left unused"]
pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
self.source
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(self) -> (ClusterStartErrorType, Option<Box<dyn Error + Send + Sync>>) {
(self.kind, self.source)
}
}
impl Display for ClusterStartError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match &self.kind {
ClusterStartErrorType::RetrievingGatewayInfo { .. } => {
f.write_str("getting the bot's gateway info failed")
}
}
}
}
impl Error for ClusterStartError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|source| &**source as &(dyn Error + 'static))
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ClusterStartErrorType {
RetrievingGatewayInfo,
}
#[derive(Debug)]
pub struct Cluster {
config: Config,
shards: HashMap<u64, Shard>,
}
impl Cluster {
pub async fn new(
token: impl Into<String>,
intents: Intents,
) -> Result<(Self, Events), ClusterStartError> {
Self::builder(token, intents).build().await
}
pub(super) async fn new_with_config(
mut config: Config,
shard_config: ShardConfig,
) -> Result<(Self, Events), ClusterStartError> {
#[derive(Default)]
struct ShardFold {
shards: HashMap<u64, Shard>,
streams: Vec<ShardEventsWithId>,
}
let scheme = match config.shard_scheme() {
ShardScheme::Auto => Self::retrieve_shard_count(&shard_config.http_client).await?,
other => other.clone(),
};
let iter = scheme.iter().expect("shard scheme is not auto");
let total = scheme.total().expect("shard scheme is not auto");
#[cfg(feature = "metrics")]
#[allow(clippy::cast_precision_loss)]
{
metrics::gauge!("Cluster-Shard-Count", total as f64);
}
let ShardFold { shards, streams } = iter.fold(ShardFold::default(), |mut fold, idx| {
let mut shard_config = shard_config.clone();
shard_config.shard = [idx, total];
if let Some(data) = config.resume_sessions.remove(&idx) {
shard_config.session_id = Some(data.session_id.into_boxed_str());
shard_config.sequence = Some(data.sequence);
}
let (shard, stream) = Shard::new_with_config(shard_config);
fold.shards.insert(idx, shard);
fold.streams.push(ShardEventsWithId::new(idx, stream));
fold
});
#[allow(clippy::from_iter_instead_of_collect)]
let select_all = SelectAll::from_iter(streams);
Ok((Self { config, shards }, Events::new(select_all)))
}
async fn retrieve_shard_count(http: &HttpClient) -> Result<ShardScheme, ClusterStartError> {
let gateway = http
.gateway()
.authed()
.exec()
.await
.map_err(|source| ClusterStartError {
kind: ClusterStartErrorType::RetrievingGatewayInfo,
source: Some(Box::new(source)),
})?
.model()
.await
.map_err(|source| ClusterStartError {
kind: ClusterStartErrorType::RetrievingGatewayInfo,
source: Some(Box::new(source)),
})?;
Ok(ShardScheme::Range {
from: 0,
to: gateway.shards - 1,
total: gateway.shards,
})
}
pub fn builder(token: impl Into<String>, intents: Intents) -> ClusterBuilder {
ClusterBuilder::new(token, intents)
}
pub const fn config(&self) -> &Config {
&self.config
}
pub async fn up(&self) {
future::join_all(self.shards.values().map(Shard::start)).await;
}
pub fn down(&self) {
for shard in self.shards.values() {
shard.shutdown();
}
}
pub fn down_resumable(&self) -> HashMap<u64, ResumeSession> {
self.shards
.values()
.map(Shard::shutdown_resumable)
.filter_map(|(id, session)| session.map(|s| (id, s)))
.collect()
}
pub fn shard(&self, id: u64) -> Option<&Shard> {
self.shards.get(&id)
}
pub fn shards(&self) -> Shards<'_> {
Shards {
iter: self.shards.values(),
}
}
pub fn info(&self) -> HashMap<u64, Information> {
self.shards
.iter()
.filter_map(|(id, shard)| shard.info().ok().map(|info| (*id, info)))
.collect()
}
pub async fn command(&self, id: u64, value: &impl Command) -> Result<(), ClusterCommandError> {
let shard = self.shard(id).ok_or(ClusterCommandError {
kind: ClusterCommandErrorType::ShardNonexistent { id },
source: None,
})?;
shard
.command(value)
.await
.map_err(|source| ClusterCommandError {
kind: ClusterCommandErrorType::Sending,
source: Some(Box::new(source)),
})
}
pub async fn send(&self, id: u64, message: Message) -> Result<(), ClusterSendError> {
let shard = self.shard(id).ok_or(ClusterSendError {
kind: ClusterSendErrorType::ShardNonexistent { id },
source: None,
})?;
shard
.send(message)
.await
.map_err(|source| ClusterSendError {
kind: ClusterSendErrorType::Sending,
source: Some(Box::new(source)),
})
}
}
pub struct Shards<'a> {
iter: Values<'a, u64, Shard>,
}
impl ExactSizeIterator for Shards<'_> {
fn len(&self) -> usize {
self.iter.len()
}
}
impl FusedIterator for Shards<'_> {}
impl<'a> Iterator for Shards<'a> {
type Item = &'a Shard;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
}
#[cfg(test)]
mod tests {
use super::{
Cluster, ClusterCommandError, ClusterCommandErrorType, ClusterSendError,
ClusterSendErrorType, ClusterStartError, ClusterStartErrorType,
};
use static_assertions::{assert_fields, assert_impl_all};
use std::{error::Error, fmt::Debug};
assert_impl_all!(ClusterCommandErrorType: Debug, Send, Sync);
assert_fields!(ClusterCommandErrorType::ShardNonexistent: id);
assert_impl_all!(ClusterCommandError: Error, Send, Sync);
assert_impl_all!(ClusterSendErrorType: Debug, Send, Sync);
assert_fields!(ClusterSendErrorType::ShardNonexistent: id);
assert_impl_all!(ClusterSendError: Error, Send, Sync);
assert_impl_all!(ClusterStartErrorType: Debug, Send, Sync);
assert_impl_all!(ClusterStartError: Error, Send, Sync);
assert_impl_all!(Cluster: Debug, Send, Sync);
}