rpcs3/rpcs3/Emu/NP/clans_client.cpp
2025-12-23 13:26:34 +01:00

1151 lines
40 KiB
C++

#include "stdafx.h"
#include <string_view>
#include <util/types.hpp>
// wolfssl uses old-style casts which break clang builds
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wold-style-cast"
#pragma clang diagnostic ignored "-Wextern-c-compat"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#endif
#include <wolfssl/wolfcrypt/coding.h>
#ifdef __clang__
#pragma clang diagnostic pop
#endif
#include <Crypto/utils.h>
#include <Utilities/StrUtil.h>
#include <Utilities/StrFmt.h>
#include "Emu/Cell/Modules/sceNp.h"
#include "Emu/Cell/Modules/sceNpClans.h"
#include "Emu/NP/clans_client.h"
#include "Emu/NP/clans_config.h"
#include "Emu/NP/np_helpers.h"
#include "Emu/system_config.h"
LOG_CHANNEL(clan_log, "clans");
const char* REQ_TYPE_FUNC = "func";
const char* REQ_TYPE_SEC = "sec";
constexpr const char JID_FORMAT[] = "%s@un.br.np.playstation.net";
template <>
void fmt_class_string<clan::ClanRequestType>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](auto value)
{
switch (value)
{
case clan::ClanRequestType::FUNC: return "func";
case clan::ClanRequestType::SEC: return "sec";
}
return unknown;
});
}
template <>
void fmt_class_string<clan::ClanManagerOperationType>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](auto value)
{
switch (value)
{
case clan::ClanManagerOperationType::VIEW: return "view";
case clan::ClanManagerOperationType::UPDATE: return "update";
}
return unknown;
});
}
template <>
void fmt_class_string<clan::ClanSearchFilterOperator>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](auto value)
{
switch (value)
{
case clan::ClanSearchFilterOperator::Equal: return "eq";
case clan::ClanSearchFilterOperator::NotEqual: return "ne";
case clan::ClanSearchFilterOperator::GreaterThan: return "gt";
case clan::ClanSearchFilterOperator::GreaterThanOrEqual: return "ge";
case clan::ClanSearchFilterOperator::LessThan: return "lt";
case clan::ClanSearchFilterOperator::LessThanOrEqual: return "le";
case clan::ClanSearchFilterOperator::Like: return "lk";
}
return unknown;
});
}
template <>
void fmt_class_string<clan::ClanRequestAction>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](auto value)
{
switch (value)
{
case clan::ClanRequestAction::GetClanList: return "get_clan_list";
case clan::ClanRequestAction::GetClanInfo: return "get_clan_info";
case clan::ClanRequestAction::GetMemberInfo: return "get_member_info";
case clan::ClanRequestAction::GetMemberList: return "get_member_list";
case clan::ClanRequestAction::GetBlacklist: return "get_blacklist";
case clan::ClanRequestAction::RecordBlacklistEntry: return "record_blacklist_entry";
case clan::ClanRequestAction::DeleteBlacklistEntry: return "delete_blacklist_entry";
case clan::ClanRequestAction::ClanSearch: return "clan_search";
case clan::ClanRequestAction::CreateClan: return "create_clan";
case clan::ClanRequestAction::DisbandClan: return "disband_clan";
case clan::ClanRequestAction::RequestMembership: return "request_membership";
case clan::ClanRequestAction::CancelRequestMembership: return "cancel_request_membership";
case clan::ClanRequestAction::AcceptMembershipRequest: return "accept_membership_request";
case clan::ClanRequestAction::DeclineMembershipRequest: return "decline_membership_request";
case clan::ClanRequestAction::SendInvitation: return "send_invitation";
case clan::ClanRequestAction::CancelInvitation: return "cancel_invitation";
case clan::ClanRequestAction::AcceptInvitation: return "accept_invitation";
case clan::ClanRequestAction::DeclineInvitation: return "decline_invitation";
case clan::ClanRequestAction::UpdateMemberInfo: return "update_member_info";
case clan::ClanRequestAction::UpdateClanInfo: return "update_clan_info";
case clan::ClanRequestAction::JoinClan: return "join_clan";
case clan::ClanRequestAction::LeaveClan: return "leave_clan";
case clan::ClanRequestAction::KickMember: return "kick_member";
case clan::ClanRequestAction::ChangeMemberRole: return "change_member_role";
case clan::ClanRequestAction::RetrieveAnnouncements: return "retrieve_announcements";
case clan::ClanRequestAction::PostAnnouncement: return "post_announcement";
case clan::ClanRequestAction::DeleteAnnouncement: return "delete_announcement";
}
return unknown;
});
}
namespace clan
{
struct curl_memory
{
char* response;
size_t size;
};
size_t clans_client::curl_write_callback(void* data, size_t size, size_t nmemb, void* clientp)
{
const size_t realsize = size * nmemb;
std::vector<char>* mem = static_cast<std::vector<char>*>(clientp);
const size_t old_size = mem->size();
mem->resize(old_size + realsize);
memcpy(mem->data() + old_size, data, realsize);
return realsize;
}
struct clan_request_ctx
{
clan_request_ctx()
{
curl = curl_easy_init();
if (curl)
{
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
}
}
~clan_request_ctx()
{
if (curl)
{
curl_easy_cleanup(curl);
curl = nullptr;
}
}
CURL* curl = nullptr;
// TODO: this was arbitrarily chosen -- see if there's a real amount
static constexpr u32 SCE_NP_CLANS_MAX_CTX_NUM = 16;
static constexpr u32 id_base = 0xA001;
static constexpr u32 id_step = 1;
static constexpr u32 id_count = SCE_NP_CLANS_MAX_CTX_NUM;
SAVESTATE_INIT_POS(55);
};
clans_client::clans_client()
{
g_cfg_clans.load();
}
clans_client::~clans_client()
{
idm::clear<clan_request_ctx>();
}
SceNpClansError clans_client::create_request(s32& req_id)
{
if (!g_cfg.net.clans_enabled)
{
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
}
const s32 id = idm::make<clan_request_ctx>();
if (id == id_manager::id_traits<clan_request_ctx>::invalid)
{
return SceNpClansError::SCE_NP_CLANS_ERROR_EXCEEDS_MAX;
}
const auto ctx = idm::get_unlocked<clan_request_ctx>(id);
if (!ctx || !ctx->curl)
{
idm::remove<clan_request_ctx>(id);
return SceNpClansError::SCE_NP_CLANS_ERROR_NOT_INITIALIZED;
}
req_id = id;
return SceNpClansError::SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::destroy_request(u32 req_id)
{
if (idm::remove<clan_request_ctx>(req_id))
return SceNpClansError::SCE_NP_CLANS_SUCCESS;
return SceNpClansError::SCE_NP_CLANS_ERROR_INVALID_ARGUMENT;
}
SceNpClansError clans_client::send_request(u32 req_id, ClanRequestAction action, ClanManagerOperationType op_type, const pugi::xml_document& xml_body, pugi::xml_document& out_response)
{
auto ctx = idm::get_unlocked<clan_request_ctx>(req_id);
if (!ctx || !ctx->curl)
return SCE_NP_CLANS_ERROR_NOT_INITIALIZED;
CURL* curl = ctx->curl;
ClanRequestType req_type = ClanRequestType::FUNC;
const pugi::xml_node clan = xml_body.child("clan");
if (clan && clan.child("ticket"))
{
req_type = ClanRequestType::SEC;
}
const std::string host = g_cfg_clans.get_host();
const std::string protocol = g_cfg_clans.get_use_https() ? "https" : "http";
const std::string url = fmt::format("%s://%s/clan_manager_%s/%s/%s", protocol, host, op_type, req_type, action);
std::ostringstream oss;
xml_body.save(oss, "\t", 8U);
const std::string xml = oss.str();
char err_buf[CURL_ERROR_SIZE];
err_buf[0] = '\0';
std::vector<char> response_buffer;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_buffer);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, err_buf);
// WARN: This disables certificate verification!
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, xml.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, xml.size());
const CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK)
{
clan_log.error("curl_easy_perform() failed: %s", curl_easy_strerror(res));
clan_log.error("Error buffer: %s", err_buf);
return SCE_NP_CLANS_ERROR_BAD_REQUEST;
}
response_buffer.push_back('\0');
const pugi::xml_parse_result xml_res = out_response.load_string(response_buffer.data());
if (!xml_res)
{
clan_log.error("XML parsing failed: %s", xml_res.description());
return SCE_NP_CLANS_ERROR_BAD_RESPONSE;
}
const pugi::xml_node clan_result = out_response.child("clan");
if (!clan_result)
return SCE_NP_CLANS_ERROR_BAD_RESPONSE;
const pugi::xml_attribute result = clan_result.attribute("result");
if (!result)
return SCE_NP_CLANS_ERROR_BAD_RESPONSE;
const std::string result_str = result.as_string();
if (result_str != "00")
return static_cast<SceNpClansError>(0x80022800 | std::stoul(result_str, nullptr, 16));
return SCE_NP_CLANS_SUCCESS;
}
std::string clans_client::get_clan_ticket(np::np_handler& nph)
{
// Pretend we failed to get a ticket if the emulator isn't
// connected to RPCN.
if (nph.get_psn_status() != SCE_NP_MANAGER_STATUS_ONLINE)
return "";
const auto& npid = nph.get_npid();
const char* service_id = CLANS_SERVICE_ID;
const unsigned char* cookie = nullptr;
const u32 cookie_size = 0;
const char* entitlement_id = CLANS_ENTITLEMENT_ID;
const u32 consumed_count = 0;
// Use the cached ticket if available
np::ticket clan_ticket;
if (!nph.get_clan_ticket_ready())
{
// If not cached, request a new ticket
nph.req_ticket(0x00020001, &npid, service_id, cookie, cookie_size, entitlement_id, consumed_count);
}
clan_ticket = nph.get_clan_ticket();
if (clan_ticket.empty())
{
clan_log.error("Failed to get clan ticket");
return "";
}
std::vector<byte> ticket_bytes(1024);
uint32_t ticket_size = UINT32_MAX;
Base64_Encode_NoNl(clan_ticket.data(), static_cast<u32>(clan_ticket.size()), ticket_bytes.data(), &ticket_size);
const std::string ticket_str = std::string(reinterpret_cast<char*>(ticket_bytes.data()), ticket_size);
return ticket_str;
}
#pragma region Outgoing API Requests
SceNpClansError clans_client::get_clan_list(np::np_handler& nph, u32 req_id, const SceNpClansPagingRequest& paging, std::vector<SceNpClansEntry>& clan_list, SceNpClansPagingResult& page_result)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("start").text().set(paging.startPos);
clan.append_child("max").text().set(paging.max);
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetClanList, ClanManagerOperationType::VIEW, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
const pugi::xml_node clan_result = response.child("clan");
const pugi::xml_node list = clan_result.child("list");
const pugi::xml_attribute results = list.attribute("results");
const uint32_t results_count = results.as_uint();
const pugi::xml_attribute total = list.attribute("total");
const uint32_t total_count = total.as_uint();
int i = 0;
for (pugi::xml_node info = list.child("info"); info; info = info.next_sibling("info"), i++)
{
const pugi::xml_attribute id = info.attribute("id");
const uint32_t clan_id = id.as_uint();
const pugi::xml_node name = info.child("name");
const std::string name_str = name.text().as_string();
const pugi::xml_node tag = info.child("tag");
const std::string tag_str = tag.text().as_string();
const pugi::xml_node role = info.child("role");
const int32_t role_int = role.text().as_uint();
const pugi::xml_node status = info.child("status");
const uint32_t status_int = status.text().as_uint();
const pugi::xml_node onlinename = info.child("onlinename");
const std::string onlinename_str = onlinename.text().as_string();
const pugi::xml_node members = info.child("members");
const uint32_t members_int = members.text().as_uint();
SceNpClansEntry entry = SceNpClansEntry{
.info = SceNpClansClanBasicInfo{
.clanId = clan_id,
.numMembers = members_int,
.name = "",
.tag = "",
.reserved = {0, 0},
},
.role = static_cast<SceNpClansMemberRole>(role_int),
.status = static_cast<SceNpClansMemberStatus>(status_int)};
strcpy_trunc(entry.info.name, name_str);
strcpy_trunc(entry.info.tag, tag_str);
::at32(clan_list, i) = std::move(entry);
i++;
}
page_result = SceNpClansPagingResult{
.count = results_count,
.total = total_count};
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::get_clan_info(u32 req_id, SceNpClanId clan_id, SceNpClansClanInfo& clan_info)
{
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("id").text().set(clan_id);
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetClanInfo, ClanManagerOperationType::VIEW, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
const pugi::xml_node clan_result = response.child("clan");
const pugi::xml_node info = clan_result.child("info");
const std::string name_str = info.child("name").text().as_string();
const std::string tag_str = info.child("tag").text().as_string();
const uint32_t members_int = info.child("members").text().as_uint();
const std::string date_created_str = info.child("date-created").text().as_string();
const std::string description_str = info.child("description").text().as_string();
clan_info = SceNpClansClanInfo{
.info = SceNpClansClanBasicInfo{
.clanId = clan_id,
.numMembers = members_int,
.name = "",
.tag = "",
},
.updatable = SceNpClansUpdatableClanInfo{
.description = "",
}};
strcpy_trunc(clan_info.info.name, name_str);
strcpy_trunc(clan_info.info.tag, tag_str);
strcpy_trunc(clan_info.updatable.description, description_str);
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::get_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMemberEntry& mem_info)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data);
clan.append_child("jid").text().set(jid_str.c_str());
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetMemberInfo, ClanManagerOperationType::VIEW, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
const pugi::xml_node clan_result = response.child("clan");
const pugi::xml_node member_info = clan_result.child("info");
const pugi::xml_attribute member_jid = member_info.attribute("jid");
const std::string member_jid_str = member_jid.as_string();
const std::string member_username = fmt::split(member_jid_str, {"@"})[0];
SceNpId member_npid;
if (strncmp(member_username.c_str(), nph.get_npid().handle.data, 16) == 0)
{
member_npid = nph.get_npid();
}
else
{
np::string_to_npid(member_username, member_npid);
}
const pugi::xml_node role = member_info.child("role");
const uint32_t role_int = role.text().as_uint();
const pugi::xml_node status = member_info.child("status");
const uint32_t status_int = status.text().as_uint();
const pugi::xml_node description = member_info.child("description");
const std::string description_str = description.text().as_string();
mem_info = SceNpClansMemberEntry
{
.npid = std::move(member_npid),
.role = static_cast<SceNpClansMemberRole>(role_int),
.status = static_cast<SceNpClansMemberStatus>(status_int),
.updatable = SceNpClansUpdatableMemberInfo{
.description = "",
}
};
strcpy_trunc(mem_info.updatable.description, description_str);
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::get_member_list(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansPagingRequest& paging, SceNpClansMemberStatus /*status*/, std::vector<SceNpClansMemberEntry>& mem_list, SceNpClansPagingResult& page_result)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
clan.append_child("start").text().set(paging.startPos);
clan.append_child("max").text().set(paging.max);
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetMemberList, ClanManagerOperationType::VIEW, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
const pugi::xml_node clan_result = response.child("clan");
const pugi::xml_node list = clan_result.child("list");
const pugi::xml_attribute results = list.attribute("results");
const uint32_t results_count = results.as_uint();
const pugi::xml_attribute total = list.attribute("total");
const uint32_t total_count = total.as_uint();
int i = 0;
for (pugi::xml_node member_info = list.child("info"); member_info; member_info = member_info.next_sibling("info"))
{
const std::string member_jid = member_info.attribute("jid").as_string();
const std::string member_username = fmt::split(member_jid, {"@"})[0];
SceNpId member_npid;
if (strncmp(member_username.c_str(), nph.get_npid().handle.data, SCE_NET_NP_ONLINEID_MAX_LENGTH) == 0)
{
member_npid = nph.get_npid();
}
else
{
np::string_to_npid(member_username, member_npid);
}
const uint32_t role_int = member_info.child("role").text().as_uint();
const uint32_t status_int = member_info.child("status").text().as_uint();
const std::string description_str = member_info.child("description").text().as_string();
SceNpClansMemberEntry entry = SceNpClansMemberEntry
{
.npid = std::move(member_npid),
.role = static_cast<SceNpClansMemberRole>(role_int),
.status = static_cast<SceNpClansMemberStatus>(status_int),
};
strcpy_trunc(entry.updatable.description, description_str);
::at32(mem_list, i) = std::move(entry);
i++;
}
page_result = SceNpClansPagingResult
{
.count = results_count,
.total = total_count
};
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::get_blacklist(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansPagingRequest& paging, std::vector<SceNpClansBlacklistEntry>& bl, SceNpClansPagingResult& page_result)
{
const std::string ticket = get_clan_ticket(nph);
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
clan.append_child("start").text().set(paging.startPos);
clan.append_child("max").text().set(paging.max);
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetBlacklist, ClanManagerOperationType::VIEW, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
const pugi::xml_node clan_result = response.child("clan");
const pugi::xml_node list = clan_result.child("list");
const pugi::xml_attribute results = list.attribute("results");
const uint32_t results_count = results.as_uint();
const pugi::xml_attribute total = list.attribute("total");
const uint32_t total_count = total.as_uint();
int i = 0;
for (pugi::xml_node member = list.child("entry"); member; member = member.next_sibling("entry"))
{
const pugi::xml_node member_jid = member.child("jid");
const std::string member_jid_str = member_jid.text().as_string();
const std::string member_username = fmt::split(member_jid_str, {"@"})[0];
SceNpId member_npid = {};
if (strncmp(member_username.c_str(), nph.get_npid().handle.data, SCE_NET_NP_ONLINEID_MAX_LENGTH) == 0)
{
member_npid = nph.get_npid();
}
else
{
np::string_to_npid(member_username.c_str(), member_npid);
}
SceNpClansBlacklistEntry entry = SceNpClansBlacklistEntry
{
.entry = std::move(member_npid),
};
::at32(bl, i) = std::move(entry);
i++;
}
page_result = SceNpClansPagingResult
{
.count = results_count,
.total = total_count
};
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::add_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data);
clan.append_child("jid").text().set(jid_str.c_str());
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::RecordBlacklistEntry, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::remove_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id)
{
std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data);
clan.append_child("jid").text().set(jid_str.c_str());
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::DeleteBlacklistEntry, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::clan_search(u32 req_id, const SceNpClansPagingRequest& paging, const SceNpClansSearchableName& search, std::vector<SceNpClansClanBasicInfo>& clan_list, SceNpClansPagingResult& page_result)
{
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("start").text().set(paging.startPos);
clan.append_child("max").text().set(paging.max);
pugi::xml_node filter = clan.append_child("filter");
pugi::xml_node name = filter.append_child("name");
const std::string op_name = fmt::format("%s", static_cast<ClanSearchFilterOperator>(static_cast<s32>(search.nameSearchOp)));
name.append_attribute("op").set_value(op_name.c_str());
name.append_attribute("value").set_value(search.name);
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::ClanSearch, ClanManagerOperationType::VIEW, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
const pugi::xml_node clan_result = response.child("clan");
const pugi::xml_node list = clan_result.child("list");
const pugi::xml_attribute results = list.attribute("results");
const uint32_t results_count = results.as_uint();
const pugi::xml_attribute total = list.attribute("total");
const uint32_t total_count = total.as_uint();
int i = 0;
for (pugi::xml_node node = list.child("info"); node; node = node.next_sibling("info"))
{
const uint32_t clan_id = node.attribute("id").as_uint();
const std::string name_str = node.child("name").text().as_string();
const std::string tag_str = node.child("tag").text().as_string();
const uint32_t members_int = node.child("members").text().as_uint();
SceNpClansClanBasicInfo entry = SceNpClansClanBasicInfo
{
.clanId = clan_id,
.numMembers = members_int,
.name = "",
.tag = "",
.reserved = {0, 0},
};
strcpy_trunc(entry.name, name_str);
strcpy_trunc(entry.tag, tag_str);
::at32(clan_list, i) = std::move(entry);
i++;
}
page_result = SceNpClansPagingResult
{
.count = results_count,
.total = total_count
};
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::create_clan(np::np_handler& nph, u32 req_id, std::string_view name, std::string_view tag, vm::ptr<SceNpClanId> clan_id)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("name").text().set(name.data());
clan.append_child("tag").text().set(tag.data());
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::CreateClan, ClanManagerOperationType::UPDATE, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
const pugi::xml_node clan_result = response.child("clan");
const pugi::xml_attribute id = clan_result.attribute("id");
const uint32_t clan_id_int = id.as_uint();
*clan_id = clan_id_int;
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::disband_dlan(np::np_handler& nph, u32 req_id, SceNpClanId clan_id)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::DisbandClan, ClanManagerOperationType::UPDATE, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::request_membership(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage& /*message*/)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::RequestMembership, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::cancel_request_membership(np::np_handler& nph, u32 req_id, SceNpClanId clan_id)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::CancelRequestMembership, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::send_membership_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMessage& /*message*/, b8 allow)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data);
clan.append_child("jid").text().set(jid_str.c_str());
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, allow ? ClanRequestAction::AcceptMembershipRequest : ClanRequestAction::DeclineMembershipRequest, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::send_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMessage& /*message*/)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data);
clan.append_child("jid").text().set(jid_str.c_str());
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::SendInvitation, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::cancel_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data);
clan.append_child("jid").text().set(jid_str.c_str());
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::CancelInvitation, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::send_invitation_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage& /*message*/, b8 accept)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, accept ? ClanRequestAction::AcceptInvitation : ClanRequestAction::DeclineInvitation, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::update_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansUpdatableMemberInfo& info)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
pugi::xml_node role = clan.append_child("onlinename");
role.text().set(nph.get_npid().handle.data);
pugi::xml_node description = clan.append_child("description");
description.text().set(info.description);
pugi::xml_node status = clan.append_child("bin-attr1");
byte bin_attr_1[SCE_NP_CLANS_MEMBER_BINARY_ATTRIBUTE1_MAX_SIZE * 2 + 1] = {0};
uint32_t bin_attr_1_size = UINT32_MAX;
Base64_Encode_NoNl(info.binAttr1, info.binData1Size, bin_attr_1, &bin_attr_1_size);
if (bin_attr_1_size == UINT32_MAX)
return SCE_NP_CLANS_ERROR_INVALID_ARGUMENT;
// `reinterpret_cast` used to let the compiler select the correct overload of `set`
status.text().set(reinterpret_cast<const char*>(bin_attr_1));
pugi::xml_node allow_msg = clan.append_child("allow-msg");
allow_msg.text().set(static_cast<uint32_t>(info.allowMsg));
pugi::xml_node size = clan.append_child("size");
size.text().set(info.binData1Size);
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::UpdateMemberInfo, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::update_clan_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansUpdatableClanInfo& info)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
// TODO: implement binary and integer attributes (not implemented in server yet)
pugi::xml_node description = clan.append_child("description");
description.text().set(info.description);
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::UpdateClanInfo, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::join_clan(np::np_handler& nph, u32 req_id, SceNpClanId clan_id)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::JoinClan, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::leave_clan(np::np_handler& nph, u32 req_id, SceNpClanId clan_id)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::LeaveClan, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::kick_member(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMessage& /*message*/)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data);
clan.append_child("jid").text().set(jid_str.c_str());
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::KickMember, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::change_member_role(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMemberRole role)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data);
clan.append_child("jid").text().set(jid_str.c_str());
pugi::xml_node role_node = clan.append_child("role");
role_node.text().set(static_cast<uint32_t>(role));
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::ChangeMemberRole, ClanManagerOperationType::UPDATE, doc, response);
}
SceNpClansError clans_client::retrieve_announcements(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansPagingRequest& paging, std::vector<SceNpClansMessageEntry>& announcements, SceNpClansPagingResult& page_result)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
clan.append_child("start").text().set(paging.startPos);
clan.append_child("max").text().set(paging.max);
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::RetrieveAnnouncements, ClanManagerOperationType::VIEW, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
const pugi::xml_node clan_result = response.child("clan");
const pugi::xml_node list = clan_result.child("list");
const pugi::xml_attribute results = list.attribute("results");
const uint32_t results_count = results.as_uint();
const pugi::xml_attribute total = list.attribute("total");
const uint32_t total_count = total.as_uint();
int i = 0;
for (pugi::xml_node node = list.child("msg-info"); node; node = node.next_sibling("msg-info"))
{
const pugi::xml_attribute id = node.attribute("id");
const uint32_t msg_id = id.as_uint();
const std::string subject_str = node.child("subject").text().as_string();
const std::string msg_str = node.child("msg").text().as_string();
const std::string author_jid = node.child("jid").text().as_string();
const std::string msg_date = node.child("msg-date").text().as_string();
SceNpId author_npid;
const std::string author_username = fmt::split(author_jid, {"@"})[0];
if (strncmp(author_username.c_str(), nph.get_npid().handle.data, SCE_NET_NP_ONLINEID_MAX_LENGTH) == 0)
{
author_npid = nph.get_npid();
}
else
{
np::string_to_npid(author_username, author_npid);
}
// TODO: implement `binData` and `fromId`
SceNpClansMessageEntry entry = SceNpClansMessageEntry
{
.mId = msg_id,
.message = SceNpClansMessage {
.subject = "",
.body = "",
},
.npid = std::move(author_npid),
.postedBy = clan_id,
};
strcpy_trunc(entry.message.subject, subject_str);
strcpy_trunc(entry.message.body, msg_str);
::at32(announcements, i) = std::move(entry);
i++;
}
page_result = SceNpClansPagingResult
{
.count = results_count,
.total = total_count
};
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::post_announcement(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansMessage& announcement, const SceNpClansMessageData& /*data*/, u32 duration, SceNpClansMessageId& msg_id)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
pugi::xml_node subject = clan.append_child("subject");
subject.text().set(announcement.subject);
pugi::xml_node msg = clan.append_child("msg");
msg.text().set(announcement.body);
pugi::xml_node expire_date = clan.append_child("expire-date");
expire_date.text().set(duration);
pugi::xml_document response = pugi::xml_document();
const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::PostAnnouncement, ClanManagerOperationType::UPDATE, doc, response);
if (clan_res != SCE_NP_CLANS_SUCCESS)
return clan_res;
const pugi::xml_node clan_result = response.child("clan");
const pugi::xml_node msg_id_node = clan_result.child("id");
msg_id = msg_id_node.text().as_uint();
return SCE_NP_CLANS_SUCCESS;
}
SceNpClansError clans_client::delete_announcement(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessageId announcement_id)
{
const std::string ticket = get_clan_ticket(nph);
if (ticket.empty())
return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE;
pugi::xml_document doc = pugi::xml_document();
pugi::xml_node clan = doc.append_child("clan");
clan.append_child("ticket").text().set(ticket.c_str());
clan.append_child("id").text().set(clan_id);
clan.append_child("msg-id").text().set(announcement_id);
pugi::xml_document response = pugi::xml_document();
return send_request(req_id, ClanRequestAction::DeleteAnnouncement, ClanManagerOperationType::UPDATE, doc, response);
}
}
#pragma endregion