mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat: add fetchprofile command
This commit is contained in:
@@ -237,6 +237,11 @@ impl TextComponent {
|
||||
TextContent::Custom { key, with, .. } => {
|
||||
text.push_str(&get_translation_text(key.clone(), locale, with.clone()));
|
||||
}
|
||||
TextContent::PlayerSprite { profile, .. } => {
|
||||
if let Some(name) = profile.0.get_string("name") {
|
||||
text.push_str(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Recursively append extra components
|
||||
|
||||
@@ -139,6 +139,20 @@ impl TextComponentBase {
|
||||
compound.put_list("with", list);
|
||||
}
|
||||
}
|
||||
TextContent::PlayerSprite {
|
||||
type_name,
|
||||
profile,
|
||||
hat,
|
||||
} => {
|
||||
let full_type = if type_name.contains(':') {
|
||||
type_name.to_string()
|
||||
} else {
|
||||
format!("minecraft:{type_name}")
|
||||
};
|
||||
compound.put_string("type", full_type);
|
||||
compound.put_compound("player", profile.0.clone());
|
||||
compound.put_byte("hat", i8::from(*hat));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref color) = self.style.color {
|
||||
@@ -300,6 +314,10 @@ impl TextComponentBase {
|
||||
} => selector.into_owned(),
|
||||
TextContent::Keybind { keybind } => keybind.into_owned(),
|
||||
TextContent::Custom { key, with, .. } => translation_to_pretty(key, Locale::EnUs, with),
|
||||
TextContent::PlayerSprite { ref profile, .. } => profile
|
||||
.0
|
||||
.get_string("name")
|
||||
.map_or_else(|| "player_sprite".to_string(), ToString::to_string),
|
||||
};
|
||||
let style = self.style;
|
||||
let color = style.color;
|
||||
@@ -352,6 +370,11 @@ impl TextComponentBase {
|
||||
TextContent::Custom { key, .. } => {
|
||||
let _ = write!(text, "%{key}");
|
||||
}
|
||||
TextContent::PlayerSprite { profile, .. } => {
|
||||
if let Some(name) = profile.0.get_string("name") {
|
||||
text.push_str(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for child in &self.extra {
|
||||
@@ -412,6 +435,11 @@ impl TextComponentBase {
|
||||
TextContent::Custom { key, with, .. } => {
|
||||
text.push_str(&get_translation_text(key.clone(), locale, with.clone()));
|
||||
}
|
||||
TextContent::PlayerSprite { profile, .. } => {
|
||||
if let Some(name) = profile.0.get_string("name") {
|
||||
text.push_str(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Recursively append extra components
|
||||
@@ -450,6 +478,11 @@ impl TextComponentBase {
|
||||
} => selector.into_owned(),
|
||||
TextContent::Keybind { keybind } => keybind.into_owned(),
|
||||
TextContent::Custom { key, with, .. } => get_translation_text(key, locale, with),
|
||||
TextContent::PlayerSprite { profile, .. } => profile
|
||||
.0
|
||||
.get_string("name")
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
// Recursively append the text of all child components
|
||||
@@ -800,6 +833,20 @@ impl TextComponent {
|
||||
}
|
||||
|
||||
impl TextComponent {
|
||||
/// Creates a player sprite component.
|
||||
#[must_use]
|
||||
pub fn player_sprite(profile: pumpkin_nbt::NbtCompound, hat: bool) -> Self {
|
||||
Self(TextComponentBase {
|
||||
content: Box::new(TextContent::PlayerSprite {
|
||||
type_name: Cow::Borrowed("minecraft:player_sprite"),
|
||||
profile: ProfileNbt(profile),
|
||||
hat,
|
||||
}),
|
||||
style: Box::default(),
|
||||
extra: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
/// Encodes this component into a byte array using NBT serialization.
|
||||
///
|
||||
/// # Returns
|
||||
@@ -1158,6 +1205,17 @@ impl TextComponent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ProfileNbt(pub pumpkin_nbt::NbtCompound);
|
||||
|
||||
impl Eq for ProfileNbt {}
|
||||
|
||||
impl std::hash::Hash for ProfileNbt {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.to_string().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// The content type of the text component.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(untagged)]
|
||||
@@ -1203,6 +1261,13 @@ pub enum TextContent {
|
||||
/// Substitution parameters for the translation.
|
||||
with: Vec<TextComponentBase>,
|
||||
},
|
||||
/// A player sprite object component.
|
||||
#[serde(skip)]
|
||||
PlayerSprite {
|
||||
type_name: Cow<'static, str>,
|
||||
profile: ProfileNbt,
|
||||
hat: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Tests for the text component implementations.
|
||||
|
||||
387
crates/pumpkin/src/command/commands/fetchprofile.rs
Normal file
387
crates/pumpkin/src/command/commands/fetchprofile.rs
Normal file
@@ -0,0 +1,387 @@
|
||||
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
|
||||
use crate::command::argument_types::core::string::StringArgumentType;
|
||||
use crate::command::argument_types::entity::EntityArgumentType;
|
||||
use crate::command::argument_types::uuid::UuidArgumentType;
|
||||
use crate::command::context::command_context::CommandContext;
|
||||
use crate::command::context::command_source::CommandSource;
|
||||
use crate::command::errors::error_types::CommandErrorType;
|
||||
use crate::command::node::dispatcher::CommandDispatcher;
|
||||
use crate::command::node::{CommandExecutor, CommandExecutorResult};
|
||||
use crate::entity::EntityBase;
|
||||
use crate::net::authentication::{fetch_profile_by_uuid, lookup_profile_by_name};
|
||||
use crate::net::{GameProfile, offline_uuid};
|
||||
use crate::server::Server;
|
||||
use pumpkin_data::translation;
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_nbt::tag::NbtTag;
|
||||
use pumpkin_util::PermissionLvl;
|
||||
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
|
||||
use pumpkin_util::text::click::ClickEvent;
|
||||
use pumpkin_util::text::{TextComponent, color::NamedColor};
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DESCRIPTION: &str = "Fetches a player's profile.";
|
||||
const PERMISSION: &str = "minecraft:command.fetchprofile";
|
||||
|
||||
const ARG_NAME: &str = "name";
|
||||
const ARG_ID: &str = "id";
|
||||
const ARG_ENTITY: &str = "entity";
|
||||
|
||||
pub const NO_PROFILE_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_FETCHPROFILE_NO_PROFILE,
|
||||
translation::java::COMMANDS_FETCHPROFILE_NO_PROFILE,
|
||||
);
|
||||
|
||||
const fn uuid_to_int_array(uuid: &Uuid) -> [i32; 4] {
|
||||
let (most, least) = uuid.as_u64_pair();
|
||||
[
|
||||
(most >> 32) as i32,
|
||||
most as i32,
|
||||
(least >> 32) as i32,
|
||||
least as i32,
|
||||
]
|
||||
}
|
||||
|
||||
fn game_profile_to_nbt(profile: &GameProfile) -> NbtCompound {
|
||||
let mut compound = NbtCompound::new();
|
||||
let int_array = uuid_to_int_array(&profile.id);
|
||||
compound.put("id", NbtTag::IntArray(int_array.to_vec()));
|
||||
if !profile.name.is_empty() {
|
||||
compound.put_string("name", profile.name.clone());
|
||||
}
|
||||
let properties = profile.properties.load();
|
||||
if !properties.is_empty() {
|
||||
let mut prop_list = Vec::new();
|
||||
for prop in properties.iter() {
|
||||
let mut prop_compound = NbtCompound::new();
|
||||
prop_compound.put_string("name", prop.name.to_string());
|
||||
prop_compound.put_string("value", prop.value.to_string());
|
||||
if let Some(ref sig) = prop.signature {
|
||||
prop_compound.put_string("signature", sig.to_string());
|
||||
}
|
||||
prop_list.push(NbtTag::Compound(prop_compound));
|
||||
}
|
||||
compound.put_list("properties", prop_list);
|
||||
}
|
||||
compound
|
||||
}
|
||||
|
||||
fn format_clickable_list(items: Vec<TextComponent>) -> TextComponent {
|
||||
let mut root = TextComponent::empty();
|
||||
for (i, item) in items.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
root = root.add_child(TextComponent::text(" "));
|
||||
}
|
||||
let styled = item.color_named(NamedColor::Green);
|
||||
let wrapped = styled.wrap_in_square_brackets();
|
||||
root = root.add_child(wrapped);
|
||||
}
|
||||
root
|
||||
}
|
||||
|
||||
async fn report_resolved_profile(
|
||||
source: &CommandSource,
|
||||
profile: &GameProfile,
|
||||
message_id: &'static str,
|
||||
argument: TextComponent,
|
||||
) {
|
||||
let encoded_profile_compound = game_profile_to_nbt(profile);
|
||||
let encoded_profile_as_string = encoded_profile_compound.to_string();
|
||||
|
||||
let head_component = TextComponent::player_sprite(encoded_profile_compound, true);
|
||||
let encoded_component_as_string = head_component
|
||||
.0
|
||||
.clone()
|
||||
.to_translated()
|
||||
.to_nbt_compound()
|
||||
.to_string();
|
||||
|
||||
let clickable = format_clickable_list(vec![
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_FETCHPROFILE_COPY_COMPONENT,
|
||||
translation::java::COMMANDS_FETCHPROFILE_COPY_COMPONENT,
|
||||
[],
|
||||
)
|
||||
.click_event(ClickEvent::CopyToClipboard {
|
||||
value: Cow::from(encoded_profile_as_string.clone()),
|
||||
}),
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_FETCHPROFILE_GIVE_ITEM,
|
||||
translation::java::COMMANDS_FETCHPROFILE_GIVE_ITEM,
|
||||
[],
|
||||
)
|
||||
.click_event(ClickEvent::RunCommand {
|
||||
command: Cow::from(format!(
|
||||
"give @s minecraft:player_head[profile={encoded_profile_as_string}]"
|
||||
)),
|
||||
}),
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_FETCHPROFILE_SUMMON_MANNEQUIN,
|
||||
translation::java::COMMANDS_FETCHPROFILE_SUMMON_MANNEQUIN,
|
||||
[],
|
||||
)
|
||||
.click_event(ClickEvent::RunCommand {
|
||||
command: Cow::from(format!(
|
||||
"summon minecraft:mannequin ~ ~ ~ {{profile:{encoded_profile_as_string}}}"
|
||||
)),
|
||||
}),
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_FETCHPROFILE_COPY_TEXT,
|
||||
translation::java::COMMANDS_FETCHPROFILE_COPY_TEXT,
|
||||
[head_component.color_named(NamedColor::White)],
|
||||
)
|
||||
.click_event(ClickEvent::CopyToClipboard {
|
||||
value: Cow::from(encoded_component_as_string),
|
||||
}),
|
||||
]);
|
||||
|
||||
let msg = TextComponent::translate_cross(message_id, message_id, [argument, clickable]);
|
||||
|
||||
source.send_feedback(msg, false).await;
|
||||
}
|
||||
|
||||
async fn fetch_profile_by_name_helper(server: &Server, name: &str) -> Option<GameProfile> {
|
||||
if let Some(player) = server.get_player_by_name(name) {
|
||||
return Some(player.gameprofile.clone());
|
||||
}
|
||||
|
||||
let cached_entry = server.data.user_cache.write().await.get_by_name(name);
|
||||
|
||||
let auth_config = server
|
||||
.advanced_config
|
||||
.networking
|
||||
.java
|
||||
.authentication
|
||||
.clone();
|
||||
let name_string = name.to_string();
|
||||
|
||||
let mojang_res =
|
||||
tokio::task::spawn_blocking(move || lookup_profile_by_name(&name_string, &auth_config))
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok)
|
||||
.flatten();
|
||||
|
||||
if let Some((uuid, resolved_name)) = mojang_res {
|
||||
server
|
||||
.data
|
||||
.user_cache
|
||||
.write()
|
||||
.await
|
||||
.upsert(uuid, resolved_name.clone());
|
||||
let auth_config_clone = server
|
||||
.advanced_config
|
||||
.networking
|
||||
.java
|
||||
.authentication
|
||||
.clone();
|
||||
let full_profile =
|
||||
tokio::task::spawn_blocking(move || fetch_profile_by_uuid(uuid, &auth_config_clone))
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok)
|
||||
.flatten();
|
||||
|
||||
return Some(full_profile.unwrap_or_else(|| GameProfile {
|
||||
id: uuid,
|
||||
name: resolved_name,
|
||||
properties: arc_swap::ArcSwap::new(Arc::new(vec![])),
|
||||
profile_actions: None,
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(entry) = cached_entry {
|
||||
return Some(GameProfile {
|
||||
id: entry.uuid,
|
||||
name: entry.name,
|
||||
properties: arc_swap::ArcSwap::new(Arc::new(vec![])),
|
||||
profile_actions: None,
|
||||
});
|
||||
}
|
||||
|
||||
if !server.advanced_config.networking.java.online_mode
|
||||
&& let Ok(uuid) = offline_uuid(name)
|
||||
{
|
||||
let profile = GameProfile {
|
||||
id: uuid,
|
||||
name: name.to_string(),
|
||||
properties: arc_swap::ArcSwap::new(Arc::new(vec![])),
|
||||
profile_actions: None,
|
||||
};
|
||||
server
|
||||
.data
|
||||
.user_cache
|
||||
.write()
|
||||
.await
|
||||
.upsert(uuid, name.to_string());
|
||||
return Some(profile);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn fetch_profile_by_id_helper(server: &Server, id: Uuid) -> Option<GameProfile> {
|
||||
if let Some(player) = server.get_player_by_uuid(id) {
|
||||
return Some(player.gameprofile.clone());
|
||||
}
|
||||
|
||||
let auth_config = server
|
||||
.advanced_config
|
||||
.networking
|
||||
.java
|
||||
.authentication
|
||||
.clone();
|
||||
let mojang_res = tokio::task::spawn_blocking(move || fetch_profile_by_uuid(id, &auth_config))
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok)
|
||||
.flatten();
|
||||
|
||||
if let Some(profile) = mojang_res {
|
||||
server
|
||||
.data
|
||||
.user_cache
|
||||
.write()
|
||||
.await
|
||||
.upsert(profile.id, profile.name.clone());
|
||||
return Some(profile);
|
||||
}
|
||||
|
||||
let cached_entry = server.data.user_cache.write().await.get_by_uuid(id);
|
||||
if let Some(entry) = cached_entry {
|
||||
return Some(GameProfile {
|
||||
id: entry.uuid,
|
||||
name: entry.name,
|
||||
properties: arc_swap::ArcSwap::new(Arc::new(vec![])),
|
||||
profile_actions: None,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
struct ResolveNameExecutor;
|
||||
|
||||
impl CommandExecutor for ResolveNameExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let name = StringArgumentType::get(context, ARG_NAME)?;
|
||||
let server = context.server().clone();
|
||||
let source = context.source.clone();
|
||||
let name_owned = name.to_string();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let name_component = TextComponent::text(name_owned.clone());
|
||||
let result = fetch_profile_by_name_helper(&server, &name_owned).await;
|
||||
match result {
|
||||
Some(profile) => {
|
||||
report_resolved_profile(
|
||||
&source,
|
||||
&profile,
|
||||
translation::java::COMMANDS_FETCHPROFILE_NAME_SUCCESS,
|
||||
name_component,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
None => {
|
||||
source
|
||||
.send_error(TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_FETCHPROFILE_NAME_FAILURE,
|
||||
translation::java::COMMANDS_FETCHPROFILE_NAME_FAILURE,
|
||||
[name_component],
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ResolveIdExecutor;
|
||||
|
||||
impl CommandExecutor for ResolveIdExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let id = UuidArgumentType::get(context, ARG_ID)?;
|
||||
let server = context.server().clone();
|
||||
let source = context.source.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let id_component = TextComponent::text(id.to_string());
|
||||
let result = fetch_profile_by_id_helper(&server, id).await;
|
||||
match result {
|
||||
Some(profile) => {
|
||||
report_resolved_profile(
|
||||
&source,
|
||||
&profile,
|
||||
translation::java::COMMANDS_FETCHPROFILE_ID_SUCCESS,
|
||||
id_component,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
None => {
|
||||
source
|
||||
.send_error(TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_FETCHPROFILE_ID_FAILURE,
|
||||
translation::java::COMMANDS_FETCHPROFILE_ID_FAILURE,
|
||||
[id_component],
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct PrintForEntityExecutor;
|
||||
|
||||
impl CommandExecutor for PrintForEntityExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let entity = EntityArgumentType::get_entity(context, ARG_ENTITY).await?;
|
||||
|
||||
if let Some(player) = entity.get_player() {
|
||||
report_resolved_profile(
|
||||
&context.source,
|
||||
&player.gameprofile,
|
||||
translation::java::COMMANDS_FETCHPROFILE_ENTITY_SUCCESS,
|
||||
player.get_display_name().await,
|
||||
)
|
||||
.await;
|
||||
Ok(1)
|
||||
} else {
|
||||
Err(NO_PROFILE_ERROR_TYPE.create_without_context(entity.get_display_name().await))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) {
|
||||
registry.register_permission_or_panic(Permission::new(
|
||||
PERMISSION,
|
||||
DESCRIPTION,
|
||||
PermissionDefault::Op(PermissionLvl::Two),
|
||||
));
|
||||
|
||||
dispatcher.register(
|
||||
command("fetchprofile", DESCRIPTION)
|
||||
.requires(PERMISSION)
|
||||
.then(literal("name").then(
|
||||
argument(ARG_NAME, StringArgumentType::GreedyPhrase).executes(ResolveNameExecutor),
|
||||
))
|
||||
.then(
|
||||
literal("id").then(argument(ARG_ID, UuidArgumentType).executes(ResolveIdExecutor)),
|
||||
)
|
||||
.then(literal("entity").then(
|
||||
argument(ARG_ENTITY, EntityArgumentType::Entity).executes(PrintForEntityExecutor),
|
||||
)),
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ mod effect;
|
||||
mod enchant;
|
||||
mod execute;
|
||||
mod experience;
|
||||
mod fetchprofile;
|
||||
mod fill;
|
||||
mod fillbiome;
|
||||
mod forceload;
|
||||
@@ -204,6 +205,7 @@ pub async fn default_dispatcher(
|
||||
teammsg::register(&mut dispatcher, registry);
|
||||
clone::register(&mut dispatcher, registry);
|
||||
attribute::register(&mut dispatcher, registry);
|
||||
fetchprofile::register(&mut dispatcher, registry);
|
||||
dispatcher
|
||||
}
|
||||
|
||||
|
||||
@@ -5,25 +5,69 @@ use pumpkin_util::text::{TextComponent, color::NamedColor};
|
||||
use pumpkin_util::translation::get_translation_text;
|
||||
use serde::Deserialize;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::command::CommandResult;
|
||||
use crate::command::{CommandExecutor, CommandSender, args::ConsumedArgs, tree::CommandTree};
|
||||
|
||||
const NAMES: [&str; 2] = ["pumpkin", "version"];
|
||||
const NAMES: [&str; 3] = ["pumpkin", "version", "ver"];
|
||||
|
||||
const DESCRIPTION: &str = "Display information about Pumpkin.";
|
||||
|
||||
const CACHE_DURATION: Duration = Duration::from_hours(24);
|
||||
|
||||
struct Executor;
|
||||
|
||||
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const GIT_HASH: &str = env!("GIT_HASH");
|
||||
const GIT_HASH_FULL: &str = env!("GIT_HASH_FULL");
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct Contributor {
|
||||
login: String,
|
||||
}
|
||||
|
||||
struct ContributorCache {
|
||||
fetched_at: Instant,
|
||||
data: Vec<Contributor>,
|
||||
}
|
||||
|
||||
static CONTRIBUTORS_CACHE: LazyLock<Mutex<Option<ContributorCache>>> =
|
||||
LazyLock::new(|| Mutex::new(None));
|
||||
|
||||
struct DonatorCache {
|
||||
fetched_at: Instant,
|
||||
data: TextComponent,
|
||||
}
|
||||
|
||||
static DONATORS_CACHE: LazyLock<Mutex<Option<DonatorCache>>> = LazyLock::new(|| Mutex::new(None));
|
||||
|
||||
fn fetch_all_contributors_cached() -> Vec<Contributor> {
|
||||
if let Ok(guard) = CONTRIBUTORS_CACHE.lock()
|
||||
&& let Some(cache) = guard.as_ref()
|
||||
&& cache.fetched_at.elapsed() < CACHE_DURATION
|
||||
{
|
||||
return cache.data.clone();
|
||||
}
|
||||
|
||||
let contributors = fetch_all_contributors();
|
||||
if !contributors.is_empty() {
|
||||
if let Ok(mut guard) = CONTRIBUTORS_CACHE.lock() {
|
||||
*guard = Some(ContributorCache {
|
||||
fetched_at: Instant::now(),
|
||||
data: contributors.clone(),
|
||||
});
|
||||
}
|
||||
} else if let Ok(guard) = CONTRIBUTORS_CACHE.lock()
|
||||
&& let Some(cache) = guard.as_ref()
|
||||
{
|
||||
return cache.data.clone();
|
||||
}
|
||||
|
||||
contributors
|
||||
}
|
||||
|
||||
fn fetch_all_contributors() -> Vec<Contributor> {
|
||||
let mut all_contributors = Vec::new();
|
||||
let mut next_url = Some(
|
||||
@@ -173,6 +217,25 @@ fn fetch_donators_hover() -> TextComponent {
|
||||
donators_text.add_child(TextComponent::text("Unable to load donators"))
|
||||
}
|
||||
|
||||
fn fetch_donators_hover_cached() -> TextComponent {
|
||||
if let Ok(guard) = DONATORS_CACHE.lock()
|
||||
&& let Some(cache) = guard.as_ref()
|
||||
&& cache.fetched_at.elapsed() < CACHE_DURATION
|
||||
{
|
||||
return cache.data.clone();
|
||||
}
|
||||
|
||||
let donators = fetch_donators_hover();
|
||||
if let Ok(mut guard) = DONATORS_CACHE.lock() {
|
||||
*guard = Some(DonatorCache {
|
||||
fetched_at: Instant::now(),
|
||||
data: donators.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
donators
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
impl CommandExecutor for Executor {
|
||||
fn execute<'a>(
|
||||
@@ -182,7 +245,7 @@ impl CommandExecutor for Executor {
|
||||
_args: &'a ConsumedArgs<'a>,
|
||||
) -> CommandResult<'a> {
|
||||
Box::pin(async move {
|
||||
let contributors = tokio::task::spawn_blocking(fetch_all_contributors)
|
||||
let contributors = tokio::task::spawn_blocking(fetch_all_contributors_cached)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let contributor_names = contributors
|
||||
@@ -284,7 +347,7 @@ impl CommandExecutor for Executor {
|
||||
|
||||
msg = msg.add_child(TextComponent::text(" "));
|
||||
|
||||
let donators_hover = tokio::task::spawn_blocking(fetch_donators_hover)
|
||||
let donators_hover = tokio::task::spawn_blocking(fetch_donators_hover_cached)
|
||||
.await
|
||||
.unwrap_or_else(|_| TextComponent::text("Unable to load donators"));
|
||||
msg = msg.add_child(
|
||||
@@ -329,3 +392,43 @@ impl CommandExecutor for Executor {
|
||||
pub fn init_command_tree() -> CommandTree {
|
||||
CommandTree::new(NAMES, DESCRIPTION).execute(Executor)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cache_duration_is_24_hours() {
|
||||
assert_eq!(CACHE_DURATION, Duration::from_hours(24));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contributor_cache_updates_and_retrieves() {
|
||||
let mut guard = CONTRIBUTORS_CACHE.lock().unwrap();
|
||||
*guard = Some(ContributorCache {
|
||||
fetched_at: Instant::now(),
|
||||
data: vec![Contributor {
|
||||
login: "test_user".to_string(),
|
||||
}],
|
||||
});
|
||||
drop(guard);
|
||||
|
||||
let contributors = fetch_all_contributors_cached();
|
||||
assert_eq!(contributors.len(), 1);
|
||||
assert_eq!(contributors[0].login, "test_user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn donator_cache_updates_and_retrieves() {
|
||||
let expected = TextComponent::text("Cached Donator Test");
|
||||
let mut guard = DONATORS_CACHE.lock().unwrap();
|
||||
*guard = Some(DonatorCache {
|
||||
fetched_at: Instant::now(),
|
||||
data: expected.clone(),
|
||||
});
|
||||
drop(guard);
|
||||
|
||||
let cached = fetch_donators_hover_cached();
|
||||
assert_eq!(cached, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,6 +735,22 @@ mod test {
|
||||
dispatcher.register(tree, "minecraft:test");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpkin_command_aliases() {
|
||||
let config = BasicConfiguration::default();
|
||||
let registry = RwLock::new(PermissionRegistry::new());
|
||||
let dispatcher = default_dispatcher(®istry, &config)
|
||||
.await
|
||||
.fallback_dispatcher;
|
||||
|
||||
let pumpkin_tree = dispatcher.get_tree("pumpkin").unwrap();
|
||||
let version_tree = dispatcher.get_tree("version").unwrap();
|
||||
let ver_tree = dispatcher.get_tree("ver").unwrap();
|
||||
|
||||
assert_eq!(pumpkin_tree.description, version_tree.description);
|
||||
assert_eq!(pumpkin_tree.description, ver_tree.description);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_renderer_outputs_two_messages_with_context_styling() {
|
||||
let input = "0123456789abcdefghij";
|
||||
|
||||
@@ -214,6 +214,33 @@ pub fn lookup_profile_by_name(
|
||||
Ok(Some((parsed_uuid, profile.name)))
|
||||
}
|
||||
|
||||
pub fn fetch_profile_by_uuid(
|
||||
uuid: Uuid,
|
||||
_auth_config: &AuthenticationConfig,
|
||||
) -> Result<Option<GameProfile>, AuthError> {
|
||||
let url = format!(
|
||||
"https://sessionserver.mojang.com/session/minecraft/profile/{}?unsigned=false",
|
||||
uuid.simple()
|
||||
);
|
||||
|
||||
let mut response = ureq::get(&url)
|
||||
.call()
|
||||
.map_err(|_| AuthError::FailedResponse)?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {}
|
||||
StatusCode::NO_CONTENT | StatusCode::NOT_FOUND => return Ok(None),
|
||||
other => Err(AuthError::UnknownStatusCode(other))?,
|
||||
}
|
||||
|
||||
let profile: GameProfile = response
|
||||
.body_mut()
|
||||
.read_json()
|
||||
.map_err(|_| AuthError::FailedParse)?;
|
||||
|
||||
Ok(Some(profile))
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AuthError {
|
||||
#[error("Authentication servers are down")]
|
||||
|
||||
Reference in New Issue
Block a user