mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-07-10 10:14:44 -06:00
Push events for webapi and some other functions fixes (#4636)
* push events for webapi and some other functions fixes * clang is not my friend
This commit is contained in:
parent
9c0310909e
commit
1dcce1cbc7
@ -4,12 +4,14 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include "common/elf_info.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/libraries/np/np_error.h"
|
||||
#include "core/libraries/np/np_manager.h"
|
||||
#include "core/libraries/np/np_score/np_score.h"
|
||||
#include "core/libraries/np/np_web_api/np_web_api.h"
|
||||
#include "core/user_settings.h"
|
||||
#include "imgui/shadnet_notifications_layer.h"
|
||||
#include "np_handler.h"
|
||||
@ -160,6 +162,9 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons
|
||||
client->onFriendStatus = [this, user_id](const ShadNet::NotifyFriendStatus& n) {
|
||||
OnFriendStatus(user_id, n);
|
||||
};
|
||||
client->onWebApiPushEvent = [this, user_id](const ShadNet::NotifyWebApiPushEvent& n) {
|
||||
OnWebApiPushEvent(user_id, n);
|
||||
};
|
||||
client->onAsyncReply = [this, user_id](ShadNet::CommandType cmd, u64 pkt_id,
|
||||
ShadNet::ErrorType err, const std::vector<u8>& body) {
|
||||
OnScoreReply(user_id, cmd, pkt_id, err, body);
|
||||
@ -446,6 +451,29 @@ void NpHandler::OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n
|
||||
}
|
||||
}
|
||||
|
||||
void NpHandler::OnWebApiPushEvent(s32 user_id, const ShadNet::NotifyWebApiPushEvent& n) {
|
||||
LOG_INFO(NpHandler, "user_id={} WebApiPushEvent svc='{}' type='{}' bytes={}", user_id,
|
||||
n.npServiceName, n.dataType, n.data.size());
|
||||
// Forward verbatim to the libSceNpWebApi push-event dispatch.it queues the event
|
||||
// and delivers it on the game's thread during sceNpCheckCallback to any registered
|
||||
// (and filter-matching) push-event callback.
|
||||
NpWebApi::PushEventInput ev;
|
||||
ev.targetUserId = user_id;
|
||||
ev.npServiceName = n.npServiceName;
|
||||
ev.npServiceLabel = n.npServiceLabel;
|
||||
ev.dataType = n.dataType;
|
||||
ev.data = n.data;
|
||||
if (!n.fromNpid.empty()) {
|
||||
ev.hasFrom = true;
|
||||
std::strncpy(ev.fromOnlineId.data, n.fromNpid.c_str(), sizeof(ev.fromOnlineId.data) - 1);
|
||||
}
|
||||
if (!n.toNpid.empty()) {
|
||||
ev.hasTo = true;
|
||||
std::strncpy(ev.toOnlineId.data, n.toNpid.c_str(), sizeof(ev.toOnlineId.data) - 1);
|
||||
}
|
||||
NpWebApi::EnqueuePushEvent(ev);
|
||||
}
|
||||
|
||||
void NpHandler::OnLoginResult(s32 user_id, const ShadNet::LoginResult& res) {
|
||||
if (res.error != ShadNet::ErrorType::NoError) {
|
||||
return;
|
||||
|
||||
@ -21,7 +21,6 @@
|
||||
#include "core/libraries/np/np_manager.h"
|
||||
#include "core/libraries/np/np_score/np_score.h"
|
||||
#include "core/libraries/np/np_score/np_score_ctx.h"
|
||||
#include "core/libraries/np/np_types.h"
|
||||
#include "core/libraries/rtc/rtc.h"
|
||||
#include "core/libraries/system/userservice.h"
|
||||
#include "shadnet/client.h"
|
||||
@ -213,6 +212,7 @@ private:
|
||||
void OnFriendNew(s32 user_id, const ShadNet::NotifyFriendNew& n);
|
||||
void OnFriendLost(s32 user_id, const ShadNet::NotifyFriendLost& n);
|
||||
void OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n);
|
||||
void OnWebApiPushEvent(s32 user_id, const ShadNet::NotifyWebApiPushEvent& n);
|
||||
void OnLoginResult(s32 user_id, const ShadNet::LoginResult& res);
|
||||
|
||||
// Async reply dispatch for score commands. Called from the per-user
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include "core/libraries/error_codes.h"
|
||||
#include "core/libraries/libs.h"
|
||||
#include "core/libraries/np/np_error.h"
|
||||
#include "core/libraries/np/np_manager.h"
|
||||
#include "core/libraries/np/np_web_api/np_web_api.h"
|
||||
#include "core/libraries/np/np_web_api/np_web_api_internal.h"
|
||||
|
||||
@ -16,6 +17,38 @@ namespace Libraries::Np::NpWebApi {
|
||||
static bool g_is_initialized = false;
|
||||
static s32 g_active_library_contexts = 0;
|
||||
|
||||
static std::string DecodeBase64(std::string_view in) {
|
||||
auto val = [](char c) -> int {
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return c - 'A';
|
||||
if (c >= 'a' && c <= 'z')
|
||||
return c - 'a' + 26;
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0' + 52;
|
||||
if (c == '+')
|
||||
return 62;
|
||||
if (c == '/')
|
||||
return 63;
|
||||
return -1;
|
||||
};
|
||||
std::string out;
|
||||
int buf = 0, bits = 0;
|
||||
for (char c : in) {
|
||||
if (c == '=' || c == '\0')
|
||||
break;
|
||||
const int v = val(c);
|
||||
if (v < 0)
|
||||
continue; // skip whitespace / invalid chars
|
||||
buf = (buf << 6) | v;
|
||||
bits += 6;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out.push_back(static_cast<char>((buf >> bits) & 0xFF));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceNpWebApiCreateContext(s32 libCtxId, OrbisNpOnlineId* onlineId) {
|
||||
if (libCtxId >= 0x8000) {
|
||||
return ORBIS_NP_WEBAPI_ERROR_INVALID_LIB_CONTEXT_ID;
|
||||
@ -137,11 +170,11 @@ s32 PS4_SYSV_ABI sceNpWebApiAbortRequest(s64 requestId) {
|
||||
|
||||
s32 PS4_SYSV_ABI sceNpWebApiAddHttpRequestHeader(s64 requestId, const char* pFieldName,
|
||||
const char* pValue) {
|
||||
LOG_ERROR(Lib_NpWebApi,
|
||||
"called (STUBBED) : requestId = {:#x}, "
|
||||
"pFieldName = '{}', pValue = '{}'",
|
||||
requestId, (pFieldName ? pFieldName : "null"), (pValue ? pValue : "null"));
|
||||
return ORBIS_OK;
|
||||
LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, pFieldName = '{}', pValue = '{}'",
|
||||
requestId, (pFieldName ? pFieldName : "null"), (pValue ? pValue : "null"));
|
||||
if (pFieldName == nullptr || pValue == nullptr)
|
||||
return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT;
|
||||
return addHttpRequestHeaderInternal(requestId, pFieldName, pValue);
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceNpWebApiAddMultipartPart(s64 requestId,
|
||||
@ -292,26 +325,27 @@ s32 PS4_SYSV_ABI sceNpWebApiGetConnectionStats(s32 userCtxId, const char* pApiGr
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceNpWebApiGetErrorCode() {
|
||||
LOG_ERROR(Lib_NpWebApi, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
const s32 code = getLastWebApiError();
|
||||
LOG_INFO(Lib_NpWebApi, "called : lastErrorCode = {:#x}", code);
|
||||
return code;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValue(s64 requestId, const char* pFieldName,
|
||||
char* pValue, u64 valueSize) {
|
||||
LOG_ERROR(Lib_NpWebApi,
|
||||
"called (STUBBED) : requestId = {:#x}, "
|
||||
"pFieldName = '{}', pValue = {}, valueSize = {}",
|
||||
requestId, (pFieldName ? pFieldName : "null"), fmt::ptr(pValue), valueSize);
|
||||
return ORBIS_OK;
|
||||
LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, pFieldName = '{}', valueSize = {}",
|
||||
requestId, (pFieldName ? pFieldName : "null"), valueSize);
|
||||
if (pFieldName == nullptr || pValue == nullptr || valueSize == 0)
|
||||
return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT;
|
||||
return getHttpResponseHeaderValueInternal(requestId, pFieldName, pValue, valueSize, nullptr);
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValueLength(s64 requestId, const char* pFieldName,
|
||||
u64* pValueLength) {
|
||||
LOG_ERROR(Lib_NpWebApi,
|
||||
"called (STUBBED) : requestId = {:#x}, "
|
||||
"pFieldName = '{}', pValueLength = {}",
|
||||
requestId, (pFieldName ? pFieldName : "null"), fmt::ptr(pValueLength));
|
||||
return ORBIS_OK;
|
||||
LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, pFieldName = '{}'", requestId,
|
||||
(pFieldName ? pFieldName : "null"));
|
||||
if (pFieldName == nullptr || pValueLength == nullptr)
|
||||
return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT;
|
||||
return getHttpResponseHeaderValueInternal(requestId, pFieldName, nullptr, 0, pValueLength);
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceNpWebApiGetHttpStatusCode(s64 requestId, s32* out_status_code) {
|
||||
@ -570,7 +604,35 @@ s32 PS4_SYSV_ABI sceNpWebApiUnregisterExtdPushEventCallback(s32 titleUserCtxId,
|
||||
|
||||
s32 PS4_SYSV_ABI sceNpWebApiUtilityParseNpId(const char* pJsonNpId,
|
||||
Libraries::Np::OrbisNpId* pNpId) {
|
||||
LOG_ERROR(Lib_NpWebApi, "(STUBBED) called");
|
||||
if (pJsonNpId == nullptr) {
|
||||
return ORBIS_NP_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
// Decode and parse the serialized npId. Format: "<handle>@<seg>[/<seg>][.<seg>]".
|
||||
// The part before '@' is the online-id handle (<=16 bytes); the segments after,
|
||||
// with their '/' and '.' separators removed, concatenate into the 8-byte opt
|
||||
const std::string decoded = DecodeBase64(pJsonNpId);
|
||||
std::string handle;
|
||||
std::string opt;
|
||||
const size_t at = decoded.find('@');
|
||||
if (at == std::string::npos) {
|
||||
handle = decoded;
|
||||
} else {
|
||||
handle = decoded.substr(0, at);
|
||||
for (size_t i = at + 1; i < decoded.size(); ++i) {
|
||||
const char c = decoded[i];
|
||||
if (c != '/' && c != '.') {
|
||||
opt.push_back(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pNpId != nullptr) {
|
||||
std::memset(pNpId, 0, sizeof(Libraries::Np::OrbisNpId));
|
||||
std::memcpy(pNpId->handle.data, handle.data(),
|
||||
std::min<size_t>(handle.size(), ORBIS_NP_ONLINEID_MAX_LENGTH));
|
||||
std::memcpy(pNpId->opt, opt.data(), std::min<size_t>(opt.size(), sizeof(pNpId->opt)));
|
||||
pNpId->reserved[0] = 0x01; // valid-handle flag, as the PRX sets
|
||||
}
|
||||
LOG_INFO(Lib_NpWebApi, "parsed npId -> handle='{}' opt='{}'", handle, opt);
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
@ -813,6 +875,10 @@ s32 PS4_SYSV_ABI Func_F9A32E8685627436() {
|
||||
}
|
||||
|
||||
void RegisterLib(Core::Loader::SymbolsResolver* sym) {
|
||||
// Drain queued push events on the game's thread during sceNpCheckCallback, so
|
||||
// registered push-event callbacks fire on a guest thread (see DrainPushEvents).
|
||||
Libraries::Np::NpManager::RegisterNpCallback("npwebapi_push", DrainPushEvents);
|
||||
|
||||
LIB_FUNCTION("x1Y7yiYSk7c", "libSceNpWebApiCompat", 1, "libSceNpWebApi",
|
||||
sceNpWebApiCreateContext);
|
||||
LIB_FUNCTION("y5Ta5JCzQHY", "libSceNpWebApiCompat", 1, "libSceNpWebApi",
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "common/types.h"
|
||||
#include "core/libraries/np/np_common.h"
|
||||
#include "core/libraries/np/np_types.h"
|
||||
@ -124,5 +127,24 @@ using OrbisNpWebApiInternalServicePushEventCallbackA = PS4_SYSV_ABI void (*)();
|
||||
|
||||
using OrbisNpWebApiNotificationCallback = PS4_SYSV_ABI void (*)(); // dummy
|
||||
|
||||
// --- Push-event delivery (emulator-internal) ---------------------------------
|
||||
// A producer (NpHandler / shadNet notification ingress) calls EnqueuePushEvent from
|
||||
// any thread. The event is queued and dispatched to registered push-event callbacks
|
||||
// on the game's thread during sceNpCheckCallback.This mirrors the real lib, where push events
|
||||
// arrive via the NP push/notification subsystem and are pumped on the callback-check thread.
|
||||
struct PushEventInput {
|
||||
Libraries::UserService::OrbisUserServiceUserId targetUserId = 0;
|
||||
std::string npServiceName; // e.g. "np:service:..."; empty for basic push
|
||||
OrbisNpServiceLabel npServiceLabel = 0;
|
||||
std::string dataType; // OrbisNpWebApiPushEventDataType value
|
||||
std::string data; // payload bytes
|
||||
OrbisNpOnlineId fromOnlineId{};
|
||||
bool hasFrom = false;
|
||||
OrbisNpOnlineId toOnlineId{};
|
||||
bool hasTo = false;
|
||||
std::vector<std::pair<std::string, std::string>> extdData; // (key,value) for extd push
|
||||
};
|
||||
void EnqueuePushEvent(const PushEventInput& ev);
|
||||
|
||||
void RegisterLib(Core::Loader::SymbolsResolver* sym);
|
||||
} // namespace Libraries::Np::NpWebApi
|
||||
@ -1,6 +1,13 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
#include "common/elf_info.h"
|
||||
#include "core/emulator_settings.h"
|
||||
@ -16,6 +23,11 @@ namespace Libraries::Np::NpWebApi {
|
||||
static std::recursive_mutex g_global_mutex;
|
||||
static std::map<s32, OrbisNpWebApiContext*> g_contexts;
|
||||
static s32 g_library_context_count = 0;
|
||||
|
||||
// Last WebApi error code parsed from an error response body
|
||||
static thread_local s32 g_last_webapi_error = ORBIS_OK;
|
||||
|
||||
static s32 captureWebApiError(const char* body, u64 len);
|
||||
static s32 g_user_context_count = 0;
|
||||
static s32 g_handle_count = 0;
|
||||
static s32 g_push_event_filter_count = 0;
|
||||
@ -568,7 +580,7 @@ void checkRequestTimeout(OrbisNpWebApiRequest* request) {
|
||||
}
|
||||
|
||||
s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s8 flag,
|
||||
const OrbisNpWebApiResponseInformationOption* pRespInfoOption) {
|
||||
OrbisNpWebApiResponseInformationOption* pRespInfoOption) {
|
||||
OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30);
|
||||
if (context == nullptr) {
|
||||
return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND;
|
||||
@ -710,6 +722,12 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s
|
||||
"be unauthenticated (expect 401 from server)",
|
||||
user_context->userId, request->userPath);
|
||||
}
|
||||
|
||||
// Replay app-supplied headers
|
||||
for (const auto& [hname, hvalue] : request->userHeaders) {
|
||||
Libraries::Http::sceHttpAddRequestHeader(req_id, hname.c_str(), hvalue.c_str(),
|
||||
/*mode=*/0);
|
||||
}
|
||||
}
|
||||
|
||||
setRequestState(request, 4);
|
||||
@ -729,10 +747,52 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s
|
||||
requestId, request->userApiGroup, request->userPath,
|
||||
magic_enum::enum_name(request->userMethod), request->http_request_id);
|
||||
|
||||
s32 sendResult = ORBIS_OK;
|
||||
if (flag != 0) {
|
||||
s32 status = 0;
|
||||
if (Libraries::Http::sceHttpGetStatusCode(request->http_request_id, &status) >= 0) {
|
||||
if (pRespInfoOption != nullptr) {
|
||||
pRespInfoOption->httpStatus = status;
|
||||
}
|
||||
if (status >= 400) {
|
||||
std::string errBody;
|
||||
char buf[256];
|
||||
for (;;) {
|
||||
const s32 n = Libraries::Http::sceHttpReadData(request->http_request_id, buf,
|
||||
sizeof(buf));
|
||||
if (n <= 0)
|
||||
break;
|
||||
errBody.append(buf, static_cast<size_t>(n));
|
||||
if (errBody.size() > 64u * 1024u)
|
||||
break; // sanity cap on a runaway error body
|
||||
}
|
||||
if (pRespInfoOption != nullptr) {
|
||||
pRespInfoOption->responseDataSize = errBody.size();
|
||||
char* const dst = pRespInfoOption->pErrorObject;
|
||||
const u64 cap = pRespInfoOption->errorObjectSize;
|
||||
if (dst != nullptr && cap > 0) {
|
||||
const u64 n = std::min<u64>(errBody.size(), cap - 1);
|
||||
if (n > 0)
|
||||
std::memcpy(dst, errBody.data(), n);
|
||||
dst[n] = '\0';
|
||||
}
|
||||
}
|
||||
const s32 npErr = captureWebApiError(errBody.data(), errBody.size());
|
||||
if (npErr > 0 && npErr < 0xc00000) {
|
||||
sendResult = static_cast<s32>(0x82000000u | static_cast<u32>(npErr));
|
||||
} else if (status >= 100 && status < 600) {
|
||||
sendResult = static_cast<s32>(0x82f00000u | static_cast<u32>(status));
|
||||
} else {
|
||||
sendResult = static_cast<s32>(0x82ffffffu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
releaseRequest(request);
|
||||
releaseUserContext(user_context);
|
||||
releaseContext(context);
|
||||
return ORBIS_OK;
|
||||
return sendResult;
|
||||
}
|
||||
|
||||
s32 abortRequestInternal(OrbisNpWebApiContext* context, OrbisNpWebApiUserContext* userContext,
|
||||
@ -1022,10 +1082,13 @@ s32 createPushEventFilterInternal(OrbisNpWebApiContext* context,
|
||||
filter->parentContext = context;
|
||||
filter->filterId = filterId;
|
||||
|
||||
LOG_INFO(Lib_NpWebApi, "createPushEventFilter: filterId={} dataTypeParams={}", filterId,
|
||||
filterParamNum);
|
||||
if (pFilterParam != nullptr && filterParamNum != 0) {
|
||||
for (u64 param_idx = 0; param_idx < filterParamNum; param_idx++) {
|
||||
OrbisNpWebApiPushEventFilterParameter copy = OrbisNpWebApiPushEventFilterParameter{};
|
||||
memcpy(©, &pFilterParam[param_idx], sizeof(OrbisNpWebApiPushEventFilterParameter));
|
||||
LOG_INFO(Lib_NpWebApi, " filterParam[{}] dataType='{}'", param_idx, copy.dataType.val);
|
||||
filter->filterParams.emplace_back(copy);
|
||||
}
|
||||
}
|
||||
@ -1353,12 +1416,23 @@ s32 createExtendedPushEventFilterInternal(
|
||||
|
||||
filter->npServiceLabel = npServiceLabel;
|
||||
|
||||
LOG_INFO(Lib_NpWebApi,
|
||||
"createExtdPushEventFilter: filterId={} service='{}' label={:#x} dataTypeParams={}",
|
||||
filterId, pNpServiceName ? pNpServiceName : "null", npServiceLabel, filterParamNum);
|
||||
if (pFilterParam != nullptr && filterParamNum != 0) {
|
||||
for (u64 param_idx = 0; param_idx < filterParamNum; param_idx++) {
|
||||
OrbisNpWebApiExtdPushEventFilterParameter copy =
|
||||
OrbisNpWebApiExtdPushEventFilterParameter{};
|
||||
memcpy(©, &pFilterParam[param_idx],
|
||||
sizeof(OrbisNpWebApiExtdPushEventFilterParameter));
|
||||
LOG_INFO(Lib_NpWebApi, " filterParam[{}] dataType='{}' extdKeys={}", param_idx,
|
||||
copy.dataType.val, copy.extdDataKeyNum); // debug
|
||||
if (copy.pExtdDataKey != nullptr) {
|
||||
for (u64 k = 0; k < copy.extdDataKeyNum; k++) {
|
||||
LOG_INFO(Lib_NpWebApi, " extdDataKey[{}]='{}'", k,
|
||||
copy.pExtdDataKey[k].val); // debug
|
||||
}
|
||||
}
|
||||
filter->filterParams.emplace_back(copy);
|
||||
// TODO: Every parameter is registered with an extended data filter through
|
||||
// sceNpPushRegisterExtendedDataFilter
|
||||
@ -1499,6 +1573,156 @@ s32 PS4_SYSV_ABI getHttpRequestIdFromRequest(OrbisNpWebApiRequest* request)
|
||||
return request->http_request_id;
|
||||
}
|
||||
|
||||
// Request/response header support
|
||||
|
||||
bool iEquals(const char* a, u64 aLen, const char* b) {
|
||||
u64 i = 0;
|
||||
for (; i < aLen && b[i]; ++i) {
|
||||
if (std::tolower((unsigned char)a[i]) != std::tolower((unsigned char)b[i]))
|
||||
return false;
|
||||
}
|
||||
return i == aLen && b[i] == '\0';
|
||||
}
|
||||
|
||||
// Locate `field`'s value in a raw HTTP response header block (CRLF-separated
|
||||
// "Name: Value" lines, possibly led by a status line). Returns false if absent.
|
||||
bool findHeaderValue(const char* block, u64 blockLen, const char* field, std::string& outValue) {
|
||||
u64 i = 0;
|
||||
while (i < blockLen) {
|
||||
u64 lineStart = i;
|
||||
while (i < blockLen && block[i] != '\n')
|
||||
++i;
|
||||
u64 lineEnd = i; // points at '\n' or end
|
||||
if (i < blockLen)
|
||||
++i; // step past '\n'
|
||||
// Trim a trailing '\r'.
|
||||
if (lineEnd > lineStart && block[lineEnd - 1] == '\r')
|
||||
--lineEnd;
|
||||
// Split on the first ':'.
|
||||
u64 colon = lineStart;
|
||||
while (colon < lineEnd && block[colon] != ':')
|
||||
++colon;
|
||||
if (colon >= lineEnd)
|
||||
continue; // no colon (status line / blank)
|
||||
u64 nameLen = colon - lineStart;
|
||||
if (!iEquals(block + lineStart, nameLen, field))
|
||||
continue;
|
||||
u64 vs = colon + 1;
|
||||
while (vs < lineEnd && (block[vs] == ' ' || block[vs] == '\t'))
|
||||
++vs;
|
||||
outValue.assign(block + vs, lineEnd - vs);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// extraction of the numeric WebApi error code from an error JSON body
|
||||
// (e.g. {"error":{"code":2240512,...}} or {"code":...}). Returns true on success.
|
||||
bool parseErrorCode(const char* body, u64 len, s32& out) {
|
||||
std::string_view sv(body, len);
|
||||
size_t k = sv.find("\"code\"");
|
||||
if (k == std::string_view::npos)
|
||||
return false;
|
||||
k += 6;
|
||||
while (k < sv.size() && (sv[k] == ' ' || sv[k] == ':' || sv[k] == '"'))
|
||||
++k;
|
||||
bool neg = false;
|
||||
if (k < sv.size() && (sv[k] == '-' || sv[k] == '+')) {
|
||||
neg = sv[k] == '-';
|
||||
++k;
|
||||
}
|
||||
if (k >= sv.size() || !std::isdigit((unsigned char)sv[k]))
|
||||
return false;
|
||||
long long v = 0;
|
||||
while (k < sv.size() && std::isdigit((unsigned char)sv[k])) {
|
||||
v = v * 10 + (sv[k] - '0');
|
||||
++k;
|
||||
}
|
||||
out = static_cast<s32>(neg ? -v : v);
|
||||
return true;
|
||||
}
|
||||
|
||||
static s32 captureWebApiError(const char* body, u64 len) {
|
||||
s32 code = 0;
|
||||
if (body != nullptr && len > 0 && parseErrorCode(body, len, code)) {
|
||||
g_last_webapi_error = code;
|
||||
return code;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
s32 getLastWebApiError() {
|
||||
return g_last_webapi_error;
|
||||
}
|
||||
|
||||
s32 addHttpRequestHeaderInternal(s64 requestId, const char* pFieldName, const char* pValue) {
|
||||
OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30);
|
||||
if (context == nullptr) {
|
||||
return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND;
|
||||
}
|
||||
OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20);
|
||||
if (user_context == nullptr) {
|
||||
releaseContext(context);
|
||||
return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND;
|
||||
}
|
||||
OrbisNpWebApiRequest* request = findRequest(user_context, requestId);
|
||||
if (request == nullptr) {
|
||||
releaseUserContext(user_context);
|
||||
releaseContext(context);
|
||||
return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND;
|
||||
}
|
||||
request->userHeaders.emplace_back(pFieldName, pValue);
|
||||
releaseRequest(request);
|
||||
releaseUserContext(user_context);
|
||||
releaseContext(context);
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
s32 getHttpResponseHeaderValueInternal(s64 requestId, const char* pFieldName, char* pValue,
|
||||
u64 valueSize, u64* pValueLength) {
|
||||
OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30);
|
||||
if (context == nullptr) {
|
||||
return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND;
|
||||
}
|
||||
OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20);
|
||||
if (user_context == nullptr) {
|
||||
releaseContext(context);
|
||||
return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND;
|
||||
}
|
||||
OrbisNpWebApiRequest* request = findRequestAndMarkBusy(user_context, requestId);
|
||||
if (request == nullptr) {
|
||||
releaseUserContext(user_context);
|
||||
releaseContext(context);
|
||||
return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND;
|
||||
}
|
||||
|
||||
char* block = nullptr;
|
||||
u64 blockSize = 0;
|
||||
const s32 httpReqId = getHttpRequestIdFromRequest(request);
|
||||
const s32 err = Libraries::Http::sceHttpGetAllResponseHeaders(httpReqId, &block, &blockSize);
|
||||
|
||||
s32 result = ORBIS_OK;
|
||||
if (err < 0) {
|
||||
result = err;
|
||||
} else {
|
||||
std::string value;
|
||||
const bool found = block != nullptr && findHeaderValue(block, blockSize, pFieldName, value);
|
||||
if (pValueLength != nullptr)
|
||||
*pValueLength = found ? value.size() : 0;
|
||||
if (pValue != nullptr && valueSize > 0) {
|
||||
const u64 n = found ? std::min<u64>(value.size(), valueSize - 1) : 0;
|
||||
if (n > 0)
|
||||
std::memcpy(pValue, value.data(), n);
|
||||
pValue[n] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
releaseRequest(request);
|
||||
releaseUserContext(user_context);
|
||||
releaseContext(context);
|
||||
return result;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI getHttpStatusCodeInternal(s64 requestId, s32* out_status_code) {
|
||||
s32 status_code;
|
||||
OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30);
|
||||
@ -1640,6 +1864,22 @@ s32 PS4_SYSV_ABI readDataInternal(s64 requestId, void* pData, u64 size) {
|
||||
result = ORBIS_NP_WEBAPI_ERROR_ABORTED;
|
||||
} else {
|
||||
result = offset;
|
||||
// Surface the server error code for sceNpWebApiGetErrorCode() when the body
|
||||
// just read belongs to an error response
|
||||
if (offset > 0) {
|
||||
s32 sc = 0;
|
||||
if (Libraries::Http::sceHttpGetStatusCode(getHttpRequestIdFromRequest(request), &sc) >=
|
||||
0 &&
|
||||
sc >= 400) {
|
||||
captureWebApiError(reinterpret_cast<const char*>(pData), offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result > 0) {
|
||||
LOG_INFO(Lib_NpWebApi, "readData reqId={:#x} -> {} bytes: {:.256s}", requestId, result,
|
||||
std::string(reinterpret_cast<const char*>(pData),
|
||||
std::min<u64>(result, 256))); // debug to be removed
|
||||
}
|
||||
|
||||
unlockContext(context);
|
||||
@ -1653,4 +1893,200 @@ s32 PS4_SYSV_ABI readDataInternal(s64 requestId, void* pData, u64 size) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- Push-event delivery -----------------------------------------------------
|
||||
//
|
||||
// Mirrors the native lib: notificationCallbackFunc -> FUN_0100e220 (extended) and the
|
||||
// service dispatch, with matching done by FUN_010049c0. Natively this is driven by the
|
||||
// NP manager's push listener thread (mnp:usr:npweblis)
|
||||
|
||||
using ServiceCb = PS4_SYSV_ABI void (*)(s32, s32, const char*, OrbisNpServiceLabel,
|
||||
const OrbisNpWebApiPushEventDataType*, const char*, u64,
|
||||
void*);
|
||||
struct PushPeerAddress {
|
||||
OrbisNpOnlineId onlineId;
|
||||
s32 platform;
|
||||
};
|
||||
using BasicCb = PS4_SYSV_ABI void (*)(s32, s32, const PushPeerAddress*, const PushPeerAddress*,
|
||||
const OrbisNpWebApiPushEventDataType*, const char*, u64,
|
||||
void*);
|
||||
using ExtdCbA = PS4_SYSV_ABI void (*)(s32, s32, const char*, OrbisNpServiceLabel,
|
||||
const OrbisNpPeerAddressA*, const OrbisNpOnlineId*,
|
||||
const OrbisNpPeerAddressA*, const OrbisNpOnlineId*,
|
||||
const OrbisNpWebApiPushEventDataType*, const char*, u64,
|
||||
const OrbisNpWebApiExtdPushEventExtdData*, u64, void*);
|
||||
|
||||
std::mutex g_push_mutex;
|
||||
std::deque<PushEventInput> g_push_queue;
|
||||
|
||||
template <typename Filter>
|
||||
bool filterMatches(const Filter* flt, const std::string& evServiceName, bool evHasServiceName,
|
||||
const std::string& evDataType) {
|
||||
const bool catch_all = flt->npServiceName.empty() && flt->npServiceLabel == 0xffffffffu;
|
||||
if (catch_all) {
|
||||
if (evHasServiceName) {
|
||||
return false;
|
||||
}
|
||||
} else if (!evHasServiceName || flt->npServiceName != evServiceName) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& p : flt->filterParams) {
|
||||
if (evDataType == p.dataType.val) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Network-thread hand-off only,the event is dispatched later from DrainPushEvents.
|
||||
void EnqueuePushEvent(const PushEventInput& ev) {
|
||||
std::scoped_lock lk{g_push_mutex};
|
||||
g_push_queue.push_back(ev);
|
||||
}
|
||||
|
||||
// Runs on the game thread from sceNpCheckCallback
|
||||
void DrainPushEvents() {
|
||||
std::deque<PushEventInput> local;
|
||||
{
|
||||
std::scoped_lock lk{g_push_mutex};
|
||||
if (g_push_queue.empty()) {
|
||||
return;
|
||||
}
|
||||
local.swap(g_push_queue);
|
||||
}
|
||||
|
||||
for (const PushEventInput& ev : local) {
|
||||
const bool ev_has_service = !ev.npServiceName.empty();
|
||||
OrbisNpWebApiPushEventDataType dt{};
|
||||
std::snprintf(dt.val, sizeof(dt.val), "%s", ev.dataType.c_str());
|
||||
const OrbisNpOnlineId* from_p = ev.hasFrom ? &ev.fromOnlineId : nullptr;
|
||||
const OrbisNpOnlineId* to_p = ev.hasTo ? &ev.toOnlineId : nullptr;
|
||||
|
||||
std::scoped_lock gl{g_global_mutex};
|
||||
for (auto& [libId, context] : g_contexts) {
|
||||
if (context == nullptr) {
|
||||
continue;
|
||||
}
|
||||
std::scoped_lock cl{context->contextLock};
|
||||
for (auto& [ucKey, uc] : context->userContexts) {
|
||||
if (uc == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (uc->userId != ev.targetUserId) {
|
||||
continue;
|
||||
}
|
||||
const s32 title_user_ctx_id = ucKey;
|
||||
|
||||
for (auto& [cbId, cb] : uc->extendedPushEventCallbacks) {
|
||||
if (cb == nullptr) {
|
||||
continue;
|
||||
}
|
||||
void (*raw)() = cb->cbFuncA ? reinterpret_cast<void (*)()>(cb->cbFuncA)
|
||||
: cb->cbFunc ? reinterpret_cast<void (*)()>(cb->cbFunc)
|
||||
: nullptr;
|
||||
if (raw == nullptr) {
|
||||
continue;
|
||||
}
|
||||
auto fit = context->extendedPushEventFilters.find(cb->filterId);
|
||||
if (fit == context->extendedPushEventFilters.end()) {
|
||||
continue;
|
||||
}
|
||||
const OrbisNpWebApiExtendedPushEventFilter* flt = fit->second;
|
||||
if (!filterMatches(flt, ev.npServiceName, ev_has_service, ev.dataType)) {
|
||||
continue;
|
||||
}
|
||||
std::vector<OrbisNpWebApiExtdPushEventExtdData> exarr;
|
||||
exarr.reserve(ev.extdData.size());
|
||||
for (auto& [k, v] : ev.extdData) {
|
||||
OrbisNpWebApiExtdPushEventExtdData e{};
|
||||
std::snprintf(e.extdDataKey.val, sizeof(e.extdDataKey.val), "%s",
|
||||
k.c_str());
|
||||
e.pData = const_cast<char*>(v.data());
|
||||
e.dataLen = v.size();
|
||||
exarr.push_back(e);
|
||||
}
|
||||
// Service name/label come from the FILTER
|
||||
const char* svc =
|
||||
flt->npServiceName.empty() ? nullptr : flt->npServiceName.c_str();
|
||||
// pData/pExtdData are NULL (and the counts 0) when the event
|
||||
// carries no payload / no matching extended data.
|
||||
const char* ext_data = ev.data.empty() ? nullptr : ev.data.data();
|
||||
const OrbisNpWebApiExtdPushEventExtdData* ext_arr =
|
||||
exarr.empty() ? nullptr : exarr.data();
|
||||
|
||||
LOG_INFO(
|
||||
Lib_NpWebApi,
|
||||
"DrainPushEvents: invoking extd cb ctx={:#x} cbId={} dataType='{}'",
|
||||
title_user_ctx_id, cbId,
|
||||
ev.dataType); // debug confirm the listener callback fires. to be removed
|
||||
reinterpret_cast<ExtdCbA>(raw)(
|
||||
title_user_ctx_id, cbId, svc, flt->npServiceLabel, nullptr, to_p, nullptr,
|
||||
from_p, &dt, ext_data, ev.data.size(), ext_arr, exarr.size(), cb->pUserArg);
|
||||
}
|
||||
|
||||
// Service push
|
||||
for (auto& [cbId, cb] : uc->servicePushEventCallbacks) {
|
||||
if (cb == nullptr || cb->cbFunc == nullptr) {
|
||||
continue;
|
||||
}
|
||||
auto fit = context->servicePushEventFilters.find(cb->filterId);
|
||||
if (fit == context->servicePushEventFilters.end()) {
|
||||
continue;
|
||||
}
|
||||
const OrbisNpWebApiServicePushEventFilter* flt = fit->second;
|
||||
if (!filterMatches(flt, ev.npServiceName, ev_has_service, ev.dataType)) {
|
||||
continue;
|
||||
}
|
||||
const char* svc =
|
||||
flt->npServiceName.empty() ? nullptr : flt->npServiceName.c_str();
|
||||
const char* svc_data = ev.data.empty() ? nullptr : ev.data.data();
|
||||
reinterpret_cast<ServiceCb>(reinterpret_cast<void (*)()>(cb->cbFunc))(
|
||||
title_user_ctx_id, cbId, svc, flt->npServiceLabel, &dt, svc_data,
|
||||
ev.data.size(), cb->pUserArg);
|
||||
}
|
||||
|
||||
// Basic push
|
||||
if (!ev_has_service) {
|
||||
for (auto& [cbId, cb] : uc->pushEventCallbacks) {
|
||||
if (cb == nullptr || cb->cbFunc == nullptr) {
|
||||
continue;
|
||||
}
|
||||
auto fit = context->pushEventFilters.find(cb->filterId);
|
||||
if (fit == context->pushEventFilters.end()) {
|
||||
continue;
|
||||
}
|
||||
const OrbisNpWebApiPushEventFilter* flt = fit->second;
|
||||
bool matched = false;
|
||||
for (const auto& p : flt->filterParams) {
|
||||
if (ev.dataType == p.dataType.val) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
continue;
|
||||
}
|
||||
LOG_INFO(Lib_NpWebApi,
|
||||
"DrainPushEvents: invoking basic cb ctx={:#x} cbId={} "
|
||||
"dataType='{}'",
|
||||
title_user_ctx_id, cbId,
|
||||
ev.dataType); // debug confirm the listener callback fires. to be
|
||||
// removed
|
||||
PushPeerAddress to_peer{}; // notified user (self)
|
||||
PushPeerAddress from_peer{}; // user that caused the event
|
||||
if (ev.hasTo) {
|
||||
to_peer.onlineId = ev.toOnlineId;
|
||||
}
|
||||
if (ev.hasFrom) {
|
||||
from_peer.onlineId = ev.fromOnlineId;
|
||||
}
|
||||
const char* p_data = ev.data.empty() ? nullptr : ev.data.data();
|
||||
reinterpret_cast<BasicCb>(reinterpret_cast<void (*)()>(cb->cbFunc))(
|
||||
title_user_ctx_id, cbId, &to_peer, &from_peer, &dt, p_data,
|
||||
ev.data.size(), cb->pUserArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}; // namespace Libraries::Np::NpWebApi
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "common/logging/log.h"
|
||||
#include "core/libraries/error_codes.h"
|
||||
#include "core/libraries/libs.h"
|
||||
@ -63,6 +66,9 @@ struct OrbisNpWebApiRequest {
|
||||
OrbisNpWebApiHttpMethod userMethod;
|
||||
u64 userContentLength;
|
||||
std::string userContentType;
|
||||
// App-supplied request headers (sceNpWebApiAddHttpRequestHeader), replayed onto
|
||||
// the libSceHttp request in the send path after the lib-managed CT/Authorization.
|
||||
std::vector<std::pair<std::string, std::string>> userHeaders;
|
||||
bool multipart;
|
||||
bool aborted;
|
||||
bool sent;
|
||||
@ -195,15 +201,27 @@ bool isRequestBusy(OrbisNpWebApiRequest* request); // FUN_0100c1b0
|
||||
s32 setRequestTimeout(s64 requestId, u32 timeout); // FUN_01003610
|
||||
void startRequestTimer(OrbisNpWebApiRequest* request); // FUN_0100c0d0
|
||||
void checkRequestTimeout(OrbisNpWebApiRequest* request); // FUN_0100c130
|
||||
s32 sendRequest(
|
||||
s64 requestId, s32 partIndex, const void* data, u64 dataSize, s8 flag,
|
||||
const OrbisNpWebApiResponseInformationOption* pResponseInformationOption); // FUN_01001c50
|
||||
s32 sendRequest(s64 requestId, s32 partIndex, const void* data, u64 dataSize, s8 flag,
|
||||
OrbisNpWebApiResponseInformationOption* pResponseInformationOption); // FUN_01001c50
|
||||
s32 abortRequestInternal(OrbisNpWebApiContext* context, OrbisNpWebApiUserContext* userContext,
|
||||
OrbisNpWebApiRequest* request); // FUN_01001b70
|
||||
s32 abortRequest(s64 requestId); // FUN_01002c70
|
||||
void releaseRequest(OrbisNpWebApiRequest* request); // FUN_01009fb0
|
||||
s32 deleteRequest(s64 requestId); // FUN_010019a0
|
||||
|
||||
// Request/response header helpers
|
||||
s32 addHttpRequestHeaderInternal(s64 requestId, const char* pFieldName, const char* pValue);
|
||||
s32 getHttpResponseHeaderValueInternal(s64 requestId, const char* pFieldName, char* pValue,
|
||||
u64 valueSize, u64* pValueLength);
|
||||
// Returns the WebApi error code captured from the most recent error response body
|
||||
// read on this thread (sceNpWebApiGetErrorCode). 0 if none.
|
||||
s32 getLastWebApiError();
|
||||
|
||||
// Game-thread pump: drains queued push events (EnqueuePushEvent) and dispatches them
|
||||
// to matching registered callbacks. Registered into the NpManager callback pump in
|
||||
// RegisterLib so it runs during sceNpCheckCallback.
|
||||
void DrainPushEvents();
|
||||
|
||||
// Handle functions
|
||||
s32 createHandleInternal(OrbisNpWebApiContext* context); // FUN_01007730
|
||||
s32 createHandle(s32 libCtxId); // FUN_01002ee0
|
||||
|
||||
@ -653,7 +653,10 @@ void ShadNetClient::HandleGetTokenReply(const std::vector<u8>& payload) {
|
||||
void ShadNetClient::HandleNotification(u16 cmd_raw, const std::vector<u8>& payload) {
|
||||
// Notification payload = u32 LE blob size + proto bytes
|
||||
const std::string blob = ExtractBlob(payload, 0);
|
||||
if (blob.empty()) {
|
||||
if (blob.empty() &&
|
||||
static_cast<NotificationType>(cmd_raw) != NotificationType::WebApiPushEvent) {
|
||||
// WebApiPushEvent is multi-field,its first field (service name) may be empty
|
||||
// legitimately, so it parses its own payload below rather than relying on blob.
|
||||
LOG_WARNING(ShadNet, "Empty notification payload type={}", cmd_raw);
|
||||
return;
|
||||
}
|
||||
@ -714,6 +717,31 @@ void ShadNetClient::HandleNotification(u16 cmd_raw, const std::vector<u8>& paylo
|
||||
onFriendStatus(n);
|
||||
break;
|
||||
}
|
||||
case NotificationType::WebApiPushEvent: {
|
||||
// Length-prefixed fields: npServiceName, npServiceLabel(u32 LE), dataType,
|
||||
// data, fromNpid, toNpid. ExtractBlob returns {} past the end, so a short
|
||||
// payload degrades to empty trailing fields rather than reading OOB.
|
||||
NotifyWebApiPushEvent n;
|
||||
int off = 0;
|
||||
n.npServiceName = ExtractBlob(payload, off);
|
||||
off += 4 + static_cast<int>(n.npServiceName.size());
|
||||
if (off + 4 <= static_cast<int>(payload.size())) {
|
||||
n.npServiceLabel = GetLE32(payload.data() + off);
|
||||
off += 4;
|
||||
}
|
||||
n.dataType = ExtractBlob(payload, off);
|
||||
off += 4 + static_cast<int>(n.dataType.size());
|
||||
n.data = ExtractBlob(payload, off);
|
||||
off += 4 + static_cast<int>(n.data.size());
|
||||
n.fromNpid = ExtractBlob(payload, off);
|
||||
off += 4 + static_cast<int>(n.fromNpid.size());
|
||||
n.toNpid = ExtractBlob(payload, off);
|
||||
LOG_INFO(ShadNet, "WebApiPushEvent svc='{}' type='{}' from='{}' bytes={}", n.npServiceName,
|
||||
n.dataType, n.fromNpid, n.data.size());
|
||||
if (onWebApiPushEvent)
|
||||
onWebApiPushEvent(n);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG_DEBUG(ShadNet, "Unknown notification type {}", cmd_raw);
|
||||
break;
|
||||
|
||||
@ -93,6 +93,7 @@ enum class NotificationType : u16 {
|
||||
FriendNew = 6,
|
||||
FriendLost = 7,
|
||||
FriendStatus = 8,
|
||||
WebApiPushEvent = 17, // Generic NP WebApi push event
|
||||
};
|
||||
|
||||
enum class ErrorType : uint8_t {
|
||||
@ -179,6 +180,14 @@ struct NotifyFriendStatus {
|
||||
bool online = false;
|
||||
u64 timestamp = 0;
|
||||
};
|
||||
struct NotifyWebApiPushEvent {
|
||||
std::string npServiceName; // may be empty (catch-all listeners match any)
|
||||
u32 npServiceLabel = 0;
|
||||
std::string dataType; // e.g. "np:service:..."
|
||||
std::string data; // raw event body (typically PSN-format JSON)
|
||||
std::string fromNpid; // may be empty
|
||||
std::string toNpid; // may be empty
|
||||
};
|
||||
|
||||
// ShadNetClient
|
||||
|
||||
@ -214,6 +223,7 @@ public:
|
||||
std::function<void(const NotifyFriendNew&)> onFriendNew;
|
||||
std::function<void(const NotifyFriendLost&)> onFriendLost;
|
||||
std::function<void(const NotifyFriendStatus&)> onFriendStatus;
|
||||
std::function<void(const NotifyWebApiPushEvent&)> onWebApiPushEvent;
|
||||
// Async reply callback.
|
||||
// cmd —command this reply is for (matches the request's cmd)
|
||||
// pkt_id —packet id echoed back from the original request header
|
||||
|
||||
Loading…
Reference in New Issue
Block a user