mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2024-11-24 06:27:24 +00:00
chore: remove all raw packet reading from chat packet handler (#1415)
* chore: default size to 33 on LU(W)Strings since that's the most common lenght Was doing this on other places, but not the main one * chore: remove all raw packet reading from chat packet handler and general chat packet cleanup * fix team invite/promote/kick * Address feedback * fix friends check * update comments * Address feedback Add GM level handeling * Address feedback
This commit is contained in:
parent
a62f6d63c6
commit
6592bbea46
@ -2,7 +2,6 @@
|
|||||||
#include "PlayerContainer.h"
|
#include "PlayerContainer.h"
|
||||||
#include "Database.h"
|
#include "Database.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "PacketUtils.h"
|
|
||||||
#include "BitStreamUtils.h"
|
#include "BitStreamUtils.h"
|
||||||
#include "Game.h"
|
#include "Game.h"
|
||||||
#include "dServer.h"
|
#include "dServer.h"
|
||||||
@ -18,6 +17,8 @@
|
|||||||
#include "eChatInternalMessageType.h"
|
#include "eChatInternalMessageType.h"
|
||||||
#include "eClientMessageType.h"
|
#include "eClientMessageType.h"
|
||||||
#include "eGameMessageType.h"
|
#include "eGameMessageType.h"
|
||||||
|
#include "StringifiedEnum.h"
|
||||||
|
#include "eGameMasterLevel.h"
|
||||||
|
|
||||||
void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
|
void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
|
||||||
//Get from the packet which player we want to do something with:
|
//Get from the packet which player we want to do something with:
|
||||||
@ -78,31 +79,27 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
|
|||||||
|
|
||||||
void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
|
void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
|
||||||
CINSTREAM_SKIP_HEADER;
|
CINSTREAM_SKIP_HEADER;
|
||||||
|
|
||||||
LWOOBJID requestorPlayerID;
|
LWOOBJID requestorPlayerID;
|
||||||
inStream.Read(requestorPlayerID);
|
LUWString LUplayerName;
|
||||||
uint32_t spacing{};
|
|
||||||
inStream.Read(spacing);
|
|
||||||
std::string playerName = "";
|
|
||||||
uint16_t character;
|
|
||||||
bool noMoreLettersInName = false;
|
|
||||||
|
|
||||||
for (uint32_t j = 0; j < 33; j++) {
|
|
||||||
inStream.Read(character);
|
|
||||||
if (character == '\0') noMoreLettersInName = true;
|
|
||||||
if (!noMoreLettersInName) playerName.push_back(static_cast<char>(character));
|
|
||||||
}
|
|
||||||
|
|
||||||
char isBestFriendRequest{};
|
char isBestFriendRequest{};
|
||||||
|
|
||||||
|
inStream.Read(requestorPlayerID);
|
||||||
|
inStream.IgnoreBytes(4);
|
||||||
|
inStream.Read(LUplayerName);
|
||||||
inStream.Read(isBestFriendRequest);
|
inStream.Read(isBestFriendRequest);
|
||||||
|
|
||||||
|
auto playerName = LUplayerName.GetAsString();
|
||||||
|
|
||||||
auto& requestor = Game::playerContainer.GetPlayerDataMutable(requestorPlayerID);
|
auto& requestor = Game::playerContainer.GetPlayerDataMutable(requestorPlayerID);
|
||||||
if (!requestor) {
|
if (!requestor) {
|
||||||
LOG("No requestor player %llu sent to %s found.", requestorPlayerID, playerName.c_str());
|
LOG("No requestor player %llu sent to %s found.", requestorPlayerID, playerName.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// you cannot friend yourself
|
||||||
if (requestor.playerName == playerName) {
|
if (requestor.playerName == playerName) {
|
||||||
SendFriendResponse(requestor, requestor, eAddFriendResponseType::MYTHRAN);
|
SendFriendResponse(requestor, requestor, eAddFriendResponseType::GENERALERROR);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -141,6 +138,13 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent GM friend spam
|
||||||
|
// If the player we are trying to be friends with is not a civilian and we are a civilian, abort the process
|
||||||
|
if (requestee.gmLevel > eGameMasterLevel::CIVILIAN && requestor.gmLevel == eGameMasterLevel::CIVILIAN ) {
|
||||||
|
SendFriendResponse(requestor, requestee, eAddFriendResponseType::MYTHRAN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isBestFriendRequest) {
|
if (isBestFriendRequest) {
|
||||||
|
|
||||||
uint8_t oldBestFriendStatus{};
|
uint8_t oldBestFriendStatus{};
|
||||||
@ -218,15 +222,19 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
|
|||||||
|
|
||||||
void ChatPacketHandler::HandleFriendResponse(Packet* packet) {
|
void ChatPacketHandler::HandleFriendResponse(Packet* packet) {
|
||||||
CINSTREAM_SKIP_HEADER;
|
CINSTREAM_SKIP_HEADER;
|
||||||
LWOOBJID playerID;
|
|
||||||
inStream.Read(playerID);
|
|
||||||
|
|
||||||
eAddFriendResponseCode clientResponseCode = static_cast<eAddFriendResponseCode>(packet->data[0x14]);
|
LWOOBJID playerID;
|
||||||
std::string friendName = PacketUtils::ReadString(0x15, packet, true);
|
eAddFriendResponseCode clientResponseCode;
|
||||||
|
LUWString friendName;
|
||||||
|
|
||||||
|
inStream.Read(playerID);
|
||||||
|
inStream.IgnoreBytes(4);
|
||||||
|
inStream.Read(clientResponseCode);
|
||||||
|
inStream.Read(friendName);
|
||||||
|
|
||||||
//Now to try and find both of these:
|
//Now to try and find both of these:
|
||||||
auto& requestor = Game::playerContainer.GetPlayerDataMutable(playerID);
|
auto& requestor = Game::playerContainer.GetPlayerDataMutable(playerID);
|
||||||
auto& requestee = Game::playerContainer.GetPlayerDataMutable(friendName);
|
auto& requestee = Game::playerContainer.GetPlayerDataMutable(friendName.GetAsString());
|
||||||
if (!requestor || !requestee) return;
|
if (!requestor || !requestee) return;
|
||||||
|
|
||||||
eAddFriendResponseType serverResponseCode{};
|
eAddFriendResponseType serverResponseCode{};
|
||||||
@ -288,8 +296,11 @@ void ChatPacketHandler::HandleFriendResponse(Packet* packet) {
|
|||||||
void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
|
void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
|
||||||
CINSTREAM_SKIP_HEADER;
|
CINSTREAM_SKIP_HEADER;
|
||||||
LWOOBJID playerID;
|
LWOOBJID playerID;
|
||||||
|
LUWString LUFriendName;
|
||||||
inStream.Read(playerID);
|
inStream.Read(playerID);
|
||||||
std::string friendName = PacketUtils::ReadString(0x14, packet, true);
|
inStream.IgnoreBytes(4);
|
||||||
|
inStream.Read(LUFriendName);
|
||||||
|
auto friendName = LUFriendName.GetAsString();
|
||||||
|
|
||||||
//we'll have to query the db here to find the user, since you can delete them while they're offline.
|
//we'll have to query the db here to find the user, since you can delete them while they're offline.
|
||||||
//First, we need to find their ID:
|
//First, we need to find their ID:
|
||||||
@ -335,123 +346,144 @@ void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
|
|||||||
SendRemoveFriend(goonB, goonAName, true);
|
SendRemoveFriend(goonB, goonAName, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatPacketHandler::HandleChatMessage(Packet* packet) {
|
void ChatPacketHandler::HandleGMLevelUpdate(Packet* packet) {
|
||||||
CINSTREAM_SKIP_HEADER;
|
|
||||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
|
||||||
inStream.Read(playerID);
|
|
||||||
|
|
||||||
const auto& sender = Game::playerContainer.GetPlayerData(playerID);
|
|
||||||
|
|
||||||
if (!sender) return;
|
|
||||||
|
|
||||||
if (sender.GetIsMuted()) return;
|
|
||||||
|
|
||||||
inStream.SetReadOffset(0x14 * 8);
|
|
||||||
|
|
||||||
uint8_t channel = 0;
|
|
||||||
inStream.Read(channel);
|
|
||||||
|
|
||||||
std::string message = PacketUtils::ReadString(0x66, packet, true, 512);
|
|
||||||
|
|
||||||
LOG("Got a message from (%s) [%d]: %s", sender.playerName.c_str(), channel, message.c_str());
|
|
||||||
|
|
||||||
if (channel != 8) return;
|
|
||||||
|
|
||||||
auto* team = Game::playerContainer.GetTeam(playerID);
|
|
||||||
|
|
||||||
if (team == nullptr) return;
|
|
||||||
|
|
||||||
for (const auto memberId : team->memberIDs) {
|
|
||||||
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
|
|
||||||
|
|
||||||
if (!otherMember) return;
|
|
||||||
|
|
||||||
CBITSTREAM;
|
|
||||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT_INTERNAL, eChatInternalMessageType::ROUTE_TO_PLAYER);
|
|
||||||
bitStream.Write(otherMember.playerID);
|
|
||||||
|
|
||||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, eChatMessageType::PRIVATE_CHAT_MESSAGE);
|
|
||||||
bitStream.Write(otherMember.playerID);
|
|
||||||
bitStream.Write<uint8_t>(8);
|
|
||||||
bitStream.Write<unsigned int>(69);
|
|
||||||
bitStream.Write(LUWString(sender.playerName));
|
|
||||||
bitStream.Write(sender.playerID);
|
|
||||||
bitStream.Write<uint16_t>(0);
|
|
||||||
bitStream.Write<uint8_t>(0); //not mythran nametag
|
|
||||||
bitStream.Write(LUWString(otherMember.playerName));
|
|
||||||
bitStream.Write<uint8_t>(0); //not mythran for receiver
|
|
||||||
bitStream.Write<uint8_t>(0); //teams?
|
|
||||||
bitStream.Write(LUWString(message, 512));
|
|
||||||
|
|
||||||
SystemAddress sysAddr = otherMember.sysAddr;
|
|
||||||
SEND_PACKET;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) {
|
|
||||||
LWOOBJID senderID = PacketUtils::ReadS64(0x08, packet);
|
|
||||||
std::string receiverName = PacketUtils::ReadString(0x66, packet, true);
|
|
||||||
std::string message = PacketUtils::ReadString(0xAA, packet, true, 512);
|
|
||||||
|
|
||||||
//Get the bois:
|
|
||||||
const auto& goonA = Game::playerContainer.GetPlayerData(senderID);
|
|
||||||
const auto& goonB = Game::playerContainer.GetPlayerData(receiverName);
|
|
||||||
if (!goonA || !goonB) return;
|
|
||||||
|
|
||||||
if (goonA.GetIsMuted()) return;
|
|
||||||
|
|
||||||
//To the sender:
|
|
||||||
{
|
|
||||||
CBITSTREAM;
|
|
||||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT_INTERNAL, eChatInternalMessageType::ROUTE_TO_PLAYER);
|
|
||||||
bitStream.Write(goonA.playerID);
|
|
||||||
|
|
||||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, eChatMessageType::PRIVATE_CHAT_MESSAGE);
|
|
||||||
bitStream.Write(goonA.playerID);
|
|
||||||
bitStream.Write<uint8_t>(7);
|
|
||||||
bitStream.Write<unsigned int>(69);
|
|
||||||
bitStream.Write(LUWString(goonA.playerName));
|
|
||||||
bitStream.Write(goonA.playerID);
|
|
||||||
bitStream.Write<uint16_t>(0);
|
|
||||||
bitStream.Write<uint8_t>(0); //not mythran nametag
|
|
||||||
bitStream.Write(LUWString(goonB.playerName));
|
|
||||||
bitStream.Write<uint8_t>(0); //not mythran for receiver
|
|
||||||
bitStream.Write<uint8_t>(0); //success
|
|
||||||
bitStream.Write(LUWString(message, 512));
|
|
||||||
|
|
||||||
SystemAddress sysAddr = goonA.sysAddr;
|
|
||||||
SEND_PACKET;
|
|
||||||
}
|
|
||||||
|
|
||||||
//To the receiver:
|
|
||||||
{
|
|
||||||
CBITSTREAM;
|
|
||||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT_INTERNAL, eChatInternalMessageType::ROUTE_TO_PLAYER);
|
|
||||||
bitStream.Write(goonB.playerID);
|
|
||||||
|
|
||||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, eChatMessageType::PRIVATE_CHAT_MESSAGE);
|
|
||||||
bitStream.Write(goonA.playerID);
|
|
||||||
bitStream.Write<uint8_t>(7);
|
|
||||||
bitStream.Write<unsigned int>(69);
|
|
||||||
bitStream.Write(LUWString(goonA.playerName));
|
|
||||||
bitStream.Write(goonA.playerID);
|
|
||||||
bitStream.Write<uint16_t>(0);
|
|
||||||
bitStream.Write<uint8_t>(0); //not mythran nametag
|
|
||||||
bitStream.Write(LUWString(goonB.playerName));
|
|
||||||
bitStream.Write<uint8_t>(0); //not mythran for receiver
|
|
||||||
bitStream.Write<uint8_t>(3); //new whisper
|
|
||||||
bitStream.Write(LUWString(message, 512));
|
|
||||||
|
|
||||||
SystemAddress sysAddr = goonB.sysAddr;
|
|
||||||
SEND_PACKET;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatPacketHandler::HandleTeamInvite(Packet* packet) {
|
|
||||||
CINSTREAM_SKIP_HEADER;
|
CINSTREAM_SKIP_HEADER;
|
||||||
LWOOBJID playerID;
|
LWOOBJID playerID;
|
||||||
inStream.Read(playerID);
|
inStream.Read(playerID);
|
||||||
std::string invitedPlayer = PacketUtils::ReadString(0x14, packet, true);
|
auto& player = Game::playerContainer.GetPlayerData(playerID);
|
||||||
|
if (!player) return;
|
||||||
|
inStream.Read(player.gmLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the structure the client uses to send this packet is shared in many chat messages
|
||||||
|
// that are sent to the server. Because of this, there are large gaps of unused data in chat messages
|
||||||
|
void ChatPacketHandler::HandleChatMessage(Packet* packet) {
|
||||||
|
CINSTREAM_SKIP_HEADER;
|
||||||
|
LWOOBJID playerID;
|
||||||
|
inStream.Read(playerID);
|
||||||
|
|
||||||
|
const auto& sender = Game::playerContainer.GetPlayerData(playerID);
|
||||||
|
if (!sender || sender.GetIsMuted()) return;
|
||||||
|
|
||||||
|
eChatChannel channel;
|
||||||
|
uint32_t size;
|
||||||
|
|
||||||
|
inStream.IgnoreBytes(4);
|
||||||
|
inStream.Read(channel);
|
||||||
|
inStream.Read(size);
|
||||||
|
inStream.IgnoreBytes(77);
|
||||||
|
|
||||||
|
LUWString message(size);
|
||||||
|
inStream.Read(message);
|
||||||
|
|
||||||
|
LOG("Got a message from (%s) via [%s]: %s", sender.playerName.c_str(), StringifiedEnum::ToString(channel).data(), message.GetAsString().c_str());
|
||||||
|
|
||||||
|
switch (channel) {
|
||||||
|
case eChatChannel::TEAM: {
|
||||||
|
auto* team = Game::playerContainer.GetTeam(playerID);
|
||||||
|
if (team == nullptr) return;
|
||||||
|
|
||||||
|
for (const auto memberId : team->memberIDs) {
|
||||||
|
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
|
||||||
|
if (!otherMember) return;
|
||||||
|
SendPrivateChatMessage(sender, otherMember, otherMember, message, eChatChannel::TEAM, eChatMessageResponseCode::SENT);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
LOG("Unhandled Chat channel [%s]", StringifiedEnum::ToString(channel).data());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the structure the client uses to send this packet is shared in many chat messages
|
||||||
|
// that are sent to the server. Because of this, there are large gaps of unused data in chat messages
|
||||||
|
void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) {
|
||||||
|
CINSTREAM_SKIP_HEADER;
|
||||||
|
LWOOBJID playerID;
|
||||||
|
inStream.Read(playerID);
|
||||||
|
|
||||||
|
const auto& sender = Game::playerContainer.GetPlayerData(playerID);
|
||||||
|
if (!sender || sender.GetIsMuted()) return;
|
||||||
|
|
||||||
|
eChatChannel channel;
|
||||||
|
uint32_t size;
|
||||||
|
LUWString LUReceiverName;
|
||||||
|
|
||||||
|
inStream.IgnoreBytes(4);
|
||||||
|
inStream.Read(channel);
|
||||||
|
if (channel != eChatChannel::PRIVATE_CHAT) LOG("WARNING: Received Private chat with the wrong channel!");
|
||||||
|
|
||||||
|
inStream.Read(size);
|
||||||
|
inStream.IgnoreBytes(77);
|
||||||
|
|
||||||
|
inStream.Read(LUReceiverName);
|
||||||
|
auto receiverName = LUReceiverName.GetAsString();
|
||||||
|
inStream.IgnoreBytes(2);
|
||||||
|
|
||||||
|
LUWString message(size);
|
||||||
|
inStream.Read(message);
|
||||||
|
|
||||||
|
LOG("Got a message from (%s) via [%s]: %s to %s", sender.playerName.c_str(), StringifiedEnum::ToString(channel).data(), message.GetAsString().c_str(), receiverName.c_str());
|
||||||
|
|
||||||
|
const auto& receiver = Game::playerContainer.GetPlayerData(receiverName);
|
||||||
|
if (!receiver) {
|
||||||
|
PlayerData otherPlayer;
|
||||||
|
otherPlayer.playerName = receiverName;
|
||||||
|
auto responseType = Database::Get()->GetCharacterInfo(receiverName)
|
||||||
|
? eChatMessageResponseCode::NOTONLINE
|
||||||
|
: eChatMessageResponseCode::GENERALERROR;
|
||||||
|
|
||||||
|
SendPrivateChatMessage(sender, otherPlayer, sender, message, eChatChannel::GENERAL, responseType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if they are friends
|
||||||
|
// only freinds can whispr each other
|
||||||
|
for (const auto& fr : receiver.friends) {
|
||||||
|
if (fr.friendID == sender.playerID) {
|
||||||
|
//To the sender:
|
||||||
|
SendPrivateChatMessage(sender, receiver, sender, message, eChatChannel::PRIVATE_CHAT, eChatMessageResponseCode::SENT);
|
||||||
|
//To the receiver:
|
||||||
|
SendPrivateChatMessage(sender, receiver, receiver, message, eChatChannel::PRIVATE_CHAT, eChatMessageResponseCode::RECEIVEDNEWWHISPER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SendPrivateChatMessage(sender, receiver, sender, message, eChatChannel::GENERAL, eChatMessageResponseCode::NOTFRIENDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatPacketHandler::SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode) {
|
||||||
|
CBITSTREAM;
|
||||||
|
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT_INTERNAL, eChatInternalMessageType::ROUTE_TO_PLAYER);
|
||||||
|
bitStream.Write(routeTo.playerID);
|
||||||
|
|
||||||
|
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, eChatMessageType::PRIVATE_CHAT_MESSAGE);
|
||||||
|
bitStream.Write(sender.playerID);
|
||||||
|
bitStream.Write(channel);
|
||||||
|
bitStream.Write<uint32_t>(0); // not used
|
||||||
|
bitStream.Write(LUWString(sender.playerName));
|
||||||
|
bitStream.Write(sender.playerID);
|
||||||
|
bitStream.Write<uint16_t>(0); // sourceID
|
||||||
|
bitStream.Write(sender.gmLevel);
|
||||||
|
bitStream.Write(LUWString(receiver.playerName));
|
||||||
|
bitStream.Write(receiver.gmLevel);
|
||||||
|
bitStream.Write(responseCode);
|
||||||
|
bitStream.Write(message);
|
||||||
|
|
||||||
|
SystemAddress sysAddr = routeTo.sysAddr;
|
||||||
|
SEND_PACKET;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ChatPacketHandler::HandleTeamInvite(Packet* packet) {
|
||||||
|
CINSTREAM_SKIP_HEADER;
|
||||||
|
|
||||||
|
LWOOBJID playerID;
|
||||||
|
LUWString invitedPlayer;
|
||||||
|
|
||||||
|
inStream.Read(playerID);
|
||||||
|
inStream.IgnoreBytes(4);
|
||||||
|
inStream.Read(invitedPlayer);
|
||||||
|
|
||||||
const auto& player = Game::playerContainer.GetPlayerData(playerID);
|
const auto& player = Game::playerContainer.GetPlayerData(playerID);
|
||||||
|
|
||||||
@ -463,7 +495,7 @@ void ChatPacketHandler::HandleTeamInvite(Packet* packet) {
|
|||||||
team = Game::playerContainer.CreateTeam(playerID);
|
team = Game::playerContainer.CreateTeam(playerID);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& other = Game::playerContainer.GetPlayerData(invitedPlayer);
|
const auto& other = Game::playerContainer.GetPlayerData(invitedPlayer.GetAsString());
|
||||||
|
|
||||||
if (!other) return;
|
if (!other) return;
|
||||||
|
|
||||||
@ -480,7 +512,7 @@ void ChatPacketHandler::HandleTeamInvite(Packet* packet) {
|
|||||||
|
|
||||||
SendTeamInvite(other, player);
|
SendTeamInvite(other, player);
|
||||||
|
|
||||||
LOG("Got team invite: %llu -> %s", playerID, invitedPlayer.c_str());
|
LOG("Got team invite: %llu -> %s", playerID, invitedPlayer.GetAsString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatPacketHandler::HandleTeamInviteResponse(Packet* packet) {
|
void ChatPacketHandler::HandleTeamInviteResponse(Packet* packet) {
|
||||||
@ -534,21 +566,25 @@ void ChatPacketHandler::HandleTeamLeave(Packet* packet) {
|
|||||||
|
|
||||||
void ChatPacketHandler::HandleTeamKick(Packet* packet) {
|
void ChatPacketHandler::HandleTeamKick(Packet* packet) {
|
||||||
CINSTREAM_SKIP_HEADER;
|
CINSTREAM_SKIP_HEADER;
|
||||||
|
|
||||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||||
|
LUWString kickedPlayer;
|
||||||
|
|
||||||
inStream.Read(playerID);
|
inStream.Read(playerID);
|
||||||
|
inStream.IgnoreBytes(4);
|
||||||
|
inStream.Read(kickedPlayer);
|
||||||
|
|
||||||
std::string kickedPlayer = PacketUtils::ReadString(0x14, packet, true);
|
|
||||||
|
|
||||||
LOG("(%llu) kicking (%s) from team", playerID, kickedPlayer.c_str());
|
LOG("(%llu) kicking (%s) from team", playerID, kickedPlayer.GetAsString().c_str());
|
||||||
|
|
||||||
const auto& kicked = Game::playerContainer.GetPlayerData(kickedPlayer);
|
const auto& kicked = Game::playerContainer.GetPlayerData(kickedPlayer.GetAsString());
|
||||||
|
|
||||||
LWOOBJID kickedId = LWOOBJID_EMPTY;
|
LWOOBJID kickedId = LWOOBJID_EMPTY;
|
||||||
|
|
||||||
if (kicked) {
|
if (kicked) {
|
||||||
kickedId = kicked.playerID;
|
kickedId = kicked.playerID;
|
||||||
} else {
|
} else {
|
||||||
kickedId = Game::playerContainer.GetId(GeneralUtils::UTF8ToUTF16(kickedPlayer));
|
kickedId = Game::playerContainer.GetId(kickedPlayer.string);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kickedId == LWOOBJID_EMPTY) return;
|
if (kickedId == LWOOBJID_EMPTY) return;
|
||||||
@ -564,14 +600,17 @@ void ChatPacketHandler::HandleTeamKick(Packet* packet) {
|
|||||||
|
|
||||||
void ChatPacketHandler::HandleTeamPromote(Packet* packet) {
|
void ChatPacketHandler::HandleTeamPromote(Packet* packet) {
|
||||||
CINSTREAM_SKIP_HEADER;
|
CINSTREAM_SKIP_HEADER;
|
||||||
|
|
||||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||||
|
LUWString promotedPlayer;
|
||||||
|
|
||||||
inStream.Read(playerID);
|
inStream.Read(playerID);
|
||||||
|
inStream.IgnoreBytes(4);
|
||||||
|
inStream.Read(promotedPlayer);
|
||||||
|
|
||||||
std::string promotedPlayer = PacketUtils::ReadString(0x14, packet, true);
|
LOG("(%llu) promoting (%s) to team leader", playerID, promotedPlayer.GetAsString().c_str());
|
||||||
|
|
||||||
LOG("(%llu) promoting (%s) to team leader", playerID, promotedPlayer.c_str());
|
const auto& promoted = Game::playerContainer.GetPlayerData(promotedPlayer.GetAsString());
|
||||||
|
|
||||||
const auto& promoted = Game::playerContainer.GetPlayerData(promotedPlayer);
|
|
||||||
|
|
||||||
if (!promoted) return;
|
if (!promoted) return;
|
||||||
|
|
||||||
|
@ -7,14 +7,53 @@ struct PlayerData;
|
|||||||
|
|
||||||
enum class eAddFriendResponseType : uint8_t;
|
enum class eAddFriendResponseType : uint8_t;
|
||||||
|
|
||||||
|
enum class eChatChannel : uint8_t {
|
||||||
|
SYSTEMNOTIFY = 0,
|
||||||
|
SYSTEMWARNING,
|
||||||
|
SYSTEMERROR,
|
||||||
|
BROADCAST,
|
||||||
|
LOCAL,
|
||||||
|
LOCALNOANIM,
|
||||||
|
EMOTE,
|
||||||
|
PRIVATE_CHAT,
|
||||||
|
TEAM,
|
||||||
|
TEAMLOCAL,
|
||||||
|
GUILD,
|
||||||
|
GUILDNOTIFY,
|
||||||
|
PROPERTY,
|
||||||
|
ADMIN,
|
||||||
|
COMBATDAMAGE,
|
||||||
|
COMBATHEALING,
|
||||||
|
COMBATLOOT,
|
||||||
|
COMBATEXP,
|
||||||
|
COMBATDEATH,
|
||||||
|
GENERAL,
|
||||||
|
TRADE,
|
||||||
|
LFG,
|
||||||
|
USER
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
enum class eChatMessageResponseCode : uint8_t {
|
||||||
|
SENT = 0,
|
||||||
|
NOTONLINE,
|
||||||
|
GENERALERROR,
|
||||||
|
RECEIVEDNEWWHISPER,
|
||||||
|
NOTFRIENDS,
|
||||||
|
SENDERFREETRIAL,
|
||||||
|
RECEIVERFREETRIAL,
|
||||||
|
};
|
||||||
|
|
||||||
namespace ChatPacketHandler {
|
namespace ChatPacketHandler {
|
||||||
void HandleFriendlistRequest(Packet* packet);
|
void HandleFriendlistRequest(Packet* packet);
|
||||||
void HandleFriendRequest(Packet* packet);
|
void HandleFriendRequest(Packet* packet);
|
||||||
void HandleFriendResponse(Packet* packet);
|
void HandleFriendResponse(Packet* packet);
|
||||||
void HandleRemoveFriend(Packet* packet);
|
void HandleRemoveFriend(Packet* packet);
|
||||||
|
void HandleGMLevelUpdate(Packet* packet);
|
||||||
|
|
||||||
void HandleChatMessage(Packet* packet);
|
void HandleChatMessage(Packet* packet);
|
||||||
void HandlePrivateChatMessage(Packet* packet);
|
void HandlePrivateChatMessage(Packet* packet);
|
||||||
|
void SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode);
|
||||||
|
|
||||||
void HandleTeamInvite(Packet* packet);
|
void HandleTeamInvite(Packet* packet);
|
||||||
void HandleTeamInviteResponse(Packet* packet);
|
void HandleTeamInviteResponse(Packet* packet);
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
#include "eChatInternalMessageType.h"
|
#include "eChatInternalMessageType.h"
|
||||||
#include "eWorldMessageType.h"
|
#include "eWorldMessageType.h"
|
||||||
#include "ChatIgnoreList.h"
|
#include "ChatIgnoreList.h"
|
||||||
|
#include "StringifiedEnum.h"
|
||||||
|
|
||||||
#include "Game.h"
|
#include "Game.h"
|
||||||
#include "Server.h"
|
#include "Server.h"
|
||||||
@ -223,7 +224,8 @@ void HandlePacket(Packet* packet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (static_cast<eConnectionType>(packet->data[1]) == eConnectionType::CHAT) {
|
if (static_cast<eConnectionType>(packet->data[1]) == eConnectionType::CHAT) {
|
||||||
switch (static_cast<eChatMessageType>(packet->data[3])) {
|
eChatMessageType chat_message_type = static_cast<eChatMessageType>(packet->data[3]);
|
||||||
|
switch (chat_message_type) {
|
||||||
case eChatMessageType::GET_FRIENDS_LIST:
|
case eChatMessageType::GET_FRIENDS_LIST:
|
||||||
ChatPacketHandler::HandleFriendlistRequest(packet);
|
ChatPacketHandler::HandleFriendlistRequest(packet);
|
||||||
break;
|
break;
|
||||||
@ -293,9 +295,61 @@ void HandlePacket(Packet* packet) {
|
|||||||
case eChatMessageType::TEAM_SET_LOOT:
|
case eChatMessageType::TEAM_SET_LOOT:
|
||||||
ChatPacketHandler::HandleTeamLootOption(packet);
|
ChatPacketHandler::HandleTeamLootOption(packet);
|
||||||
break;
|
break;
|
||||||
|
case eChatMessageType::GMLEVEL_UPDATE:
|
||||||
|
ChatPacketHandler::HandleGMLevelUpdate(packet);
|
||||||
|
break;
|
||||||
|
case eChatMessageType::LOGIN_SESSION_NOTIFY:
|
||||||
|
case eChatMessageType::USER_CHANNEL_CHAT_MESSAGE:
|
||||||
|
case eChatMessageType::WORLD_DISCONNECT_REQUEST:
|
||||||
|
case eChatMessageType::WORLD_PROXIMITY_RESPONSE:
|
||||||
|
case eChatMessageType::WORLD_PARCEL_RESPONSE:
|
||||||
|
case eChatMessageType::TEAM_MISSED_INVITE_CHECK:
|
||||||
|
case eChatMessageType::GUILD_CREATE:
|
||||||
|
case eChatMessageType::GUILD_INVITE:
|
||||||
|
case eChatMessageType::GUILD_INVITE_RESPONSE:
|
||||||
|
case eChatMessageType::GUILD_LEAVE:
|
||||||
|
case eChatMessageType::GUILD_KICK:
|
||||||
|
case eChatMessageType::GUILD_GET_STATUS:
|
||||||
|
case eChatMessageType::GUILD_GET_ALL:
|
||||||
|
case eChatMessageType::SHOW_ALL:
|
||||||
|
case eChatMessageType::BLUEPRINT_MODERATED:
|
||||||
|
case eChatMessageType::BLUEPRINT_MODEL_READY:
|
||||||
|
case eChatMessageType::PROPERTY_READY_FOR_APPROVAL:
|
||||||
|
case eChatMessageType::PROPERTY_MODERATION_CHANGED:
|
||||||
|
case eChatMessageType::PROPERTY_BUILDMODE_CHANGED:
|
||||||
|
case eChatMessageType::PROPERTY_BUILDMODE_CHANGED_REPORT:
|
||||||
|
case eChatMessageType::MAIL:
|
||||||
|
case eChatMessageType::WORLD_INSTANCE_LOCATION_REQUEST:
|
||||||
|
case eChatMessageType::REPUTATION_UPDATE:
|
||||||
|
case eChatMessageType::SEND_CANNED_TEXT:
|
||||||
|
case eChatMessageType::CHARACTER_NAME_CHANGE_REQUEST:
|
||||||
|
case eChatMessageType::CSR_REQUEST:
|
||||||
|
case eChatMessageType::CSR_REPLY:
|
||||||
|
case eChatMessageType::GM_KICK:
|
||||||
|
case eChatMessageType::GM_ANNOUNCE:
|
||||||
|
case eChatMessageType::WORLD_ROUTE_PACKET:
|
||||||
|
case eChatMessageType::GET_ZONE_POPULATIONS:
|
||||||
|
case eChatMessageType::REQUEST_MINIMUM_CHAT_MODE:
|
||||||
|
case eChatMessageType::MATCH_REQUEST:
|
||||||
|
case eChatMessageType::UGCMANIFEST_REPORT_MISSING_FILE:
|
||||||
|
case eChatMessageType::UGCMANIFEST_REPORT_DONE_FILE:
|
||||||
|
case eChatMessageType::UGCMANIFEST_REPORT_DONE_BLUEPRINT:
|
||||||
|
case eChatMessageType::UGCC_REQUEST:
|
||||||
|
case eChatMessageType::WHO:
|
||||||
|
case eChatMessageType::WORLD_PLAYERS_PET_MODERATED_ACKNOWLEDGE:
|
||||||
|
case eChatMessageType::ACHIEVEMENT_NOTIFY:
|
||||||
|
case eChatMessageType::GM_CLOSE_PRIVATE_CHAT_WINDOW:
|
||||||
|
case eChatMessageType::UNEXPECTED_DISCONNECT:
|
||||||
|
case eChatMessageType::PLAYER_READY:
|
||||||
|
case eChatMessageType::GET_DONATION_TOTAL:
|
||||||
|
case eChatMessageType::UPDATE_DONATION:
|
||||||
|
case eChatMessageType::PRG_CSR_COMMAND:
|
||||||
|
case eChatMessageType::HEARTBEAT_REQUEST_FROM_WORLD:
|
||||||
|
case eChatMessageType::UPDATE_FREE_TRIAL_STATUS:
|
||||||
|
LOG("Unhandled CHAT Message id: %s (%i)", StringifiedEnum::ToString(chat_message_type).data(), chat_message_type);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
LOG("Unknown CHAT id: %i", int(packet->data[3]));
|
LOG("Unknown CHAT Message id: %i", chat_message_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
#include "Database.h"
|
#include "Database.h"
|
||||||
#include "eConnectionType.h"
|
#include "eConnectionType.h"
|
||||||
#include "eChatInternalMessageType.h"
|
#include "eChatInternalMessageType.h"
|
||||||
|
#include "eGameMasterLevel.h"
|
||||||
#include "ChatPackets.h"
|
#include "ChatPackets.h"
|
||||||
#include "dConfig.h"
|
#include "dConfig.h"
|
||||||
|
|
||||||
@ -22,6 +23,10 @@ PlayerContainer::~PlayerContainer() {
|
|||||||
m_Players.clear();
|
m_Players.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlayerData::PlayerData() {
|
||||||
|
gmLevel == eGameMasterLevel::CIVILIAN;
|
||||||
|
}
|
||||||
|
|
||||||
TeamData::TeamData() {
|
TeamData::TeamData() {
|
||||||
lootFlag = Game::config->GetValue("default_team_loot") == "0" ? 0 : 1;
|
lootFlag = Game::config->GetValue("default_team_loot") == "0" ? 0 : 1;
|
||||||
}
|
}
|
||||||
@ -47,6 +52,7 @@ void PlayerContainer::InsertPlayer(Packet* packet) {
|
|||||||
|
|
||||||
inStream.Read(data.zoneID);
|
inStream.Read(data.zoneID);
|
||||||
inStream.Read(data.muteExpire);
|
inStream.Read(data.muteExpire);
|
||||||
|
inStream.Read(data.gmLevel);
|
||||||
data.sysAddr = packet->systemAddress;
|
data.sysAddr = packet->systemAddress;
|
||||||
|
|
||||||
m_Names[data.playerID] = GeneralUtils::UTF8ToUTF16(data.playerName);
|
m_Names[data.playerID] = GeneralUtils::UTF8ToUTF16(data.playerName);
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
#include "dServer.h"
|
#include "dServer.h"
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
enum class eGameMasterLevel : uint8_t;
|
||||||
|
|
||||||
struct IgnoreData {
|
struct IgnoreData {
|
||||||
IgnoreData(const std::string& name, const LWOOBJID& id) : playerName(name), playerId(id) {}
|
IgnoreData(const std::string& name, const LWOOBJID& id) : playerName(name), playerId(id) {}
|
||||||
inline bool operator==(const std::string& other) const noexcept {
|
inline bool operator==(const std::string& other) const noexcept {
|
||||||
@ -22,6 +24,7 @@ struct IgnoreData {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct PlayerData {
|
struct PlayerData {
|
||||||
|
PlayerData();
|
||||||
operator bool() const noexcept {
|
operator bool() const noexcept {
|
||||||
return playerID != LWOOBJID_EMPTY;
|
return playerID != LWOOBJID_EMPTY;
|
||||||
}
|
}
|
||||||
@ -42,6 +45,8 @@ struct PlayerData {
|
|||||||
std::string playerName;
|
std::string playerName;
|
||||||
std::vector<FriendData> friends;
|
std::vector<FriendData> friends;
|
||||||
std::vector<IgnoreData> ignoredPlayers;
|
std::vector<IgnoreData> ignoredPlayers;
|
||||||
|
eGameMasterLevel gmLevel;
|
||||||
|
bool isFTP = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TeamData {
|
struct TeamData {
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
#include "eTriggerEventType.h"
|
#include "eTriggerEventType.h"
|
||||||
#include "eObjectBits.h"
|
#include "eObjectBits.h"
|
||||||
#include "PositionUpdate.h"
|
#include "PositionUpdate.h"
|
||||||
|
#include "eChatMessageType.h"
|
||||||
|
|
||||||
//Component includes:
|
//Component includes:
|
||||||
#include "Component.h"
|
#include "Component.h"
|
||||||
@ -858,9 +859,20 @@ void Entity::SetGMLevel(eGameMasterLevel value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CharacterComponent* character = GetComponent<CharacterComponent>();
|
CharacterComponent* character = GetComponent<CharacterComponent>();
|
||||||
if (character) character->SetGMLevel(value);
|
if (!character) return;
|
||||||
|
character->SetGMLevel(value);
|
||||||
|
|
||||||
GameMessages::SendGMLevelBroadcast(m_ObjectID, value);
|
GameMessages::SendGMLevelBroadcast(m_ObjectID, value);
|
||||||
|
|
||||||
|
// Update the chat server of our GM Level
|
||||||
|
{
|
||||||
|
CBITSTREAM;
|
||||||
|
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, eChatMessageType::GMLEVEL_UPDATE);
|
||||||
|
bitStream.Write(m_ObjectID);
|
||||||
|
bitStream.Write(m_GMLevel);
|
||||||
|
|
||||||
|
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Entity::WriteBaseReplicaData(RakNet::BitStream* outBitStream, eReplicaPacketType packetType) {
|
void Entity::WriteBaseReplicaData(RakNet::BitStream* outBitStream, eReplicaPacketType packetType) {
|
||||||
|
@ -1,64 +1,8 @@
|
|||||||
#include "PacketUtils.h"
|
#include "PacketUtils.h"
|
||||||
#include <vector>
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include "Logger.h"
|
#include "Logger.h"
|
||||||
#include "Game.h"
|
#include "Game.h"
|
||||||
|
|
||||||
uint16_t PacketUtils::ReadU16(uint32_t startLoc, Packet* packet) {
|
|
||||||
if (startLoc + 2 > packet->length) return 0;
|
|
||||||
|
|
||||||
std::vector<unsigned char> t;
|
|
||||||
for (uint32_t i = startLoc; i < startLoc + 2; i++) t.push_back(packet->data[i]);
|
|
||||||
return *(uint16_t*)t.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t PacketUtils::ReadU32(uint32_t startLoc, Packet* packet) {
|
|
||||||
if (startLoc + 4 > packet->length) return 0;
|
|
||||||
|
|
||||||
std::vector<unsigned char> t;
|
|
||||||
for (uint32_t i = startLoc; i < startLoc + 4; i++) {
|
|
||||||
t.push_back(packet->data[i]);
|
|
||||||
}
|
|
||||||
return *(uint32_t*)t.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t PacketUtils::ReadU64(uint32_t startLoc, Packet* packet) {
|
|
||||||
if (startLoc + 8 > packet->length) return 0;
|
|
||||||
|
|
||||||
std::vector<unsigned char> t;
|
|
||||||
for (uint32_t i = startLoc; i < startLoc + 8; i++) t.push_back(packet->data[i]);
|
|
||||||
return *(uint64_t*)t.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
int64_t PacketUtils::ReadS64(uint32_t startLoc, Packet* packet) {
|
|
||||||
if (startLoc + 8 > packet->length) return 0;
|
|
||||||
|
|
||||||
std::vector<unsigned char> t;
|
|
||||||
for (size_t i = startLoc; i < startLoc + 8; i++) t.push_back(packet->data[i]);
|
|
||||||
return *(int64_t*)t.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string PacketUtils::ReadString(uint32_t startLoc, Packet* packet, bool wide, uint32_t maxLen) {
|
|
||||||
std::string readString = "";
|
|
||||||
|
|
||||||
if (wide) maxLen *= 2;
|
|
||||||
|
|
||||||
if (packet->length > startLoc) {
|
|
||||||
uint32_t i = 0;
|
|
||||||
while (packet->data[startLoc + i] != '\0' && packet->length > static_cast<uint32_t>(startLoc + i) && maxLen > i) {
|
|
||||||
readString.push_back(packet->data[startLoc + i]);
|
|
||||||
|
|
||||||
if (wide) {
|
|
||||||
i += 2; // Wide-char string
|
|
||||||
} else {
|
|
||||||
i++; // Regular string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return readString;
|
|
||||||
}
|
|
||||||
|
|
||||||
//! Saves a packet to the filesystem
|
//! Saves a packet to the filesystem
|
||||||
void PacketUtils::SavePacket(const std::string& filename, const char* data, size_t length) {
|
void PacketUtils::SavePacket(const std::string& filename, const char* data, size_t length) {
|
||||||
//If we don't log to the console, don't save the bin files either. This takes up a lot of time.
|
//If we don't log to the console, don't save the bin files either. This takes up a lot of time.
|
||||||
|
@ -8,11 +8,6 @@
|
|||||||
enum class eConnectionType : uint16_t;
|
enum class eConnectionType : uint16_t;
|
||||||
|
|
||||||
namespace PacketUtils {
|
namespace PacketUtils {
|
||||||
uint16_t ReadU16(uint32_t startLoc, Packet* packet);
|
|
||||||
uint32_t ReadU32(uint32_t startLoc, Packet* packet);
|
|
||||||
uint64_t ReadU64(uint32_t startLoc, Packet* packet);
|
|
||||||
int64_t ReadS64(uint32_t startLoc, Packet* packet);
|
|
||||||
std::string ReadString(uint32_t startLoc, Packet* packet, bool wide, uint32_t maxLen = 33);
|
|
||||||
void SavePacket(const std::string& filename, const char* data, size_t length);
|
void SavePacket(const std::string& filename, const char* data, size_t length);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1126,6 +1126,7 @@ void HandlePacket(Packet* packet) {
|
|||||||
bitStream.Write(zone.GetInstanceID());
|
bitStream.Write(zone.GetInstanceID());
|
||||||
bitStream.Write(zone.GetCloneID());
|
bitStream.Write(zone.GetCloneID());
|
||||||
bitStream.Write(player->GetParentUser()->GetMuteExpire());
|
bitStream.Write(player->GetParentUser()->GetMuteExpire());
|
||||||
|
bitStream.Write(player->GetGMLevel());
|
||||||
|
|
||||||
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
|
Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user