mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(db): guilds, guild_members, guild_invites and guild_events
Four tables (MySQL migration 97, SQLite 80) and the IGuilds interface with MySQL, SQLite and TestSQL implementations, for the guild system (docs/Guilds.md). A guild has a name (unique without regard to case), a name status like pet_names (1 waiting for moderation, 2 approved), its founder and when it was made. A character is in at most one guild (guild_members, with its rank: 1 leader, 2 officer, 3 veteran, 4 recruit, the client's names, and when it joined) and has at most one pending invite (guild_invites). guild_events is each guild's history and outlives the guild. Deleting a character removes its membership and the invites to and from it. Nothing uses the tables yet. Check: both migrations run on a fresh and an existing database; DatabaseParityTests Guilds passes against MariaDB (DLU_TEST_MYSQL_HOST) and SQLite gives the same results. Nothing to check in game. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,7 @@
|
||||
#include "IApiKeys.h"
|
||||
#include "IUgcLookup.h"
|
||||
#include "IPermissionGrants.h"
|
||||
#include "IGuilds.h"
|
||||
|
||||
#ifdef _DEBUG
|
||||
# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0)
|
||||
@@ -68,7 +69,7 @@ class GameDatabase :
|
||||
public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
|
||||
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
|
||||
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
|
||||
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys, public IUgcLookup, public IPermissionGrants {
|
||||
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys, public IUgcLookup, public IPermissionGrants, public IGuilds {
|
||||
public:
|
||||
virtual ~GameDatabase() = default;
|
||||
// TODO: These should be made private.
|
||||
|
||||
100
dDatabase/GameDatabase/ITables/IGuilds.h
Normal file
100
dDatabase/GameDatabase/ITables/IGuilds.h
Normal file
@@ -0,0 +1,100 @@
|
||||
#ifndef __IGUILDS__H__
|
||||
#define __IGUILDS__H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
|
||||
/**
|
||||
* Guilds (docs/Guilds.md). The chat server owns them: it is the only writer apart from the dashboard. A character is in
|
||||
* at most one guild and has at most one pending invite. The leader is the member with rank 1; there is no leader column.
|
||||
*/
|
||||
class IGuilds {
|
||||
public:
|
||||
// guilds.name_status, the way pet_names.approved counts
|
||||
enum NameStatus : int32_t {
|
||||
NAME_PENDING = 1, // waits for a moderator; other players see no guild name
|
||||
NAME_APPROVED = 2,
|
||||
};
|
||||
|
||||
struct Guild {
|
||||
int64_t id{};
|
||||
std::string name;
|
||||
int32_t nameStatus{};
|
||||
LWOOBJID founderId{};
|
||||
int64_t createdAt{};
|
||||
};
|
||||
|
||||
struct Member {
|
||||
LWOOBJID characterId{};
|
||||
int64_t guildId{};
|
||||
uint8_t rank{}; // 1 leader, 2 officer, 3 veteran, 4 recruit (the client's names)
|
||||
int64_t joinedAt{};
|
||||
std::string name; // charinfo.name; empty when the character is gone
|
||||
};
|
||||
|
||||
struct Invite {
|
||||
LWOOBJID characterId{}; // who is invited
|
||||
int64_t guildId{};
|
||||
LWOOBJID inviterId{};
|
||||
int64_t createdAt{};
|
||||
};
|
||||
|
||||
// One line of a guild's history
|
||||
struct Event {
|
||||
uint64_t id{};
|
||||
int64_t guildId{};
|
||||
int64_t time{};
|
||||
std::string kind; // created, joined, left, kicked, rank, leader, renamed, name_approved, name_rejected, disbanded
|
||||
LWOOBJID characterId{}; // who it was about (0: the guild itself)
|
||||
std::string characterName;
|
||||
std::string actor; // the character or dashboard user who did it
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
struct GuildSummary {
|
||||
Guild guild;
|
||||
uint32_t memberCount{};
|
||||
LWOOBJID leaderId{};
|
||||
std::string leaderName;
|
||||
};
|
||||
|
||||
struct GuildPage {
|
||||
uint32_t total{}; // every guild (or every pending one)
|
||||
uint32_t filtered{}; // matching the search
|
||||
std::vector<GuildSummary> guilds;
|
||||
};
|
||||
|
||||
// Adds a guild (guild.id is not used) and returns its id
|
||||
virtual int64_t InsertGuild(const Guild& guild) = 0;
|
||||
virtual std::optional<Guild> GetGuild(int64_t guildId) = 0;
|
||||
// Names are unique without regard to case
|
||||
virtual std::optional<Guild> GetGuildByName(const std::string& name) = 0;
|
||||
virtual void SetGuildName(int64_t guildId, const std::string& name, int32_t nameStatus) = 0;
|
||||
// Removes the guild, its members and its invites. Its events stay.
|
||||
virtual void DeleteGuild(int64_t guildId) = 0;
|
||||
|
||||
virtual void AddGuildMember(const Member& member) = 0;
|
||||
virtual void RemoveGuildMember(LWOOBJID characterId) = 0;
|
||||
virtual void SetGuildMemberRank(LWOOBJID characterId, uint8_t rank) = 0;
|
||||
virtual std::optional<Member> GetGuildMember(LWOOBJID characterId) = 0;
|
||||
// Ordered by rank, then who joined first
|
||||
virtual std::vector<Member> GetGuildMembers(int64_t guildId) = 0;
|
||||
|
||||
// Replaces the character's invite
|
||||
virtual void SetGuildInvite(const Invite& invite) = 0;
|
||||
virtual std::optional<Invite> GetGuildInvite(LWOOBJID characterId) = 0;
|
||||
virtual void DeleteGuildInvite(LWOOBJID characterId) = 0;
|
||||
|
||||
virtual uint64_t InsertGuildEvent(const Event& event) = 0;
|
||||
// Newest first; guildId 0: every guild's
|
||||
virtual std::vector<Event> GetGuildEvents(int64_t guildId, uint32_t limit) = 0;
|
||||
|
||||
// Guilds for the dashboard, newest first. search matches the name, or the id when it is a number.
|
||||
virtual GuildPage GetGuildPage(uint32_t start, uint32_t length, const std::string& search, bool pendingOnly) = 0;
|
||||
};
|
||||
|
||||
#endif //!__IGUILDS__H__
|
||||
@@ -113,5 +113,7 @@ void MySQLDatabase::DeleteCharacter(const LWOOBJID characterId) {
|
||||
ExecuteDelete("DELETE FROM ugc_modular_build WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM pet_names WHERE owner_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM player_positions WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM guild_members WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM guild_invites WHERE character_id=? OR inviter_id=?;", characterId, characterId);
|
||||
ExecuteDelete("DELETE FROM charinfo WHERE id=? LIMIT 1;", characterId);
|
||||
}
|
||||
|
||||
@@ -227,6 +227,23 @@ public:
|
||||
std::vector<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) override;
|
||||
std::vector<UgcMail> GetUgcMail(const std::vector<LWOOBJID>& subkeys, const LOT modelItemLot) override;
|
||||
|
||||
// IGuilds
|
||||
int64_t InsertGuild(const Guild& guild) override;
|
||||
std::optional<Guild> GetGuild(int64_t guildId) override;
|
||||
std::optional<Guild> GetGuildByName(const std::string& name) override;
|
||||
void SetGuildName(int64_t guildId, const std::string& name, int32_t nameStatus) override;
|
||||
void DeleteGuild(int64_t guildId) override;
|
||||
void AddGuildMember(const Member& member) override;
|
||||
void RemoveGuildMember(LWOOBJID characterId) override;
|
||||
void SetGuildMemberRank(LWOOBJID characterId, uint8_t rank) override;
|
||||
std::optional<Member> GetGuildMember(LWOOBJID characterId) override;
|
||||
std::vector<Member> GetGuildMembers(int64_t guildId) override;
|
||||
void SetGuildInvite(const Invite& invite) override;
|
||||
std::optional<Invite> GetGuildInvite(LWOOBJID characterId) override;
|
||||
void DeleteGuildInvite(LWOOBJID characterId) override;
|
||||
uint64_t InsertGuildEvent(const Event& event) override;
|
||||
std::vector<Event> GetGuildEvents(int64_t guildId, uint32_t limit) override;
|
||||
GuildPage GetGuildPage(uint32_t start, uint32_t length, const std::string& search, bool pendingOnly) override;
|
||||
// IPermissionGrants
|
||||
uint64_t InsertPermissionGrant(const Grant& grant) override;
|
||||
std::optional<Grant> GetPermissionGrant(uint64_t id) override;
|
||||
|
||||
@@ -13,6 +13,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
|
||||
"AccountStrikes.cpp"
|
||||
"ApiKeys.cpp"
|
||||
"PermissionGrants.cpp"
|
||||
"Guilds.cpp"
|
||||
"Moderation.cpp"
|
||||
"ServerHealth.cpp"
|
||||
"PlayerPositions.cpp"
|
||||
|
||||
174
dDatabase/GameDatabase/MySQL/Tables/Guilds.cpp
Normal file
174
dDatabase/GameDatabase/MySQL/Tables/Guilds.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
|
||||
namespace {
|
||||
template<typename Result> IGuilds::Guild ReadGuild(Result& result) {
|
||||
IGuilds::Guild guild;
|
||||
guild.id = result->getInt64("id");
|
||||
guild.name = result->getString("name").c_str();
|
||||
guild.nameStatus = result->getInt("name_status");
|
||||
guild.founderId = result->getInt64("founder_id");
|
||||
guild.createdAt = result->getInt64("created_at");
|
||||
return guild;
|
||||
}
|
||||
|
||||
template<typename Result> IGuilds::Member ReadMember(Result& result) {
|
||||
IGuilds::Member member;
|
||||
member.characterId = result->getInt64("character_id");
|
||||
member.guildId = result->getInt64("guild_id");
|
||||
member.rank = static_cast<uint8_t>(result->getUInt("guild_rank"));
|
||||
member.joinedAt = result->getInt64("joined_at");
|
||||
member.name = result->isNull("name") ? "" : result->getString("name").c_str();
|
||||
return member;
|
||||
}
|
||||
|
||||
template<typename Result> IGuilds::Event ReadEvent(Result& result) {
|
||||
IGuilds::Event event;
|
||||
event.id = result->getUInt64("id");
|
||||
event.guildId = result->getInt64("guild_id");
|
||||
event.time = result->getInt64("time");
|
||||
event.kind = result->getString("kind").c_str();
|
||||
event.characterId = result->getInt64("character_id");
|
||||
event.characterName = result->getString("character_name").c_str();
|
||||
event.actor = result->getString("actor").c_str();
|
||||
event.detail = result->getString("detail").c_str();
|
||||
return event;
|
||||
}
|
||||
|
||||
constexpr const char* MEMBER_SELECT = "SELECT m.character_id, m.guild_id, m.guild_rank, m.joined_at, c.name "
|
||||
"FROM guild_members m LEFT JOIN charinfo c ON c.id = m.character_id ";
|
||||
|
||||
// The member count and leader (lowest rank, first to join) of the guild in the outer query as g
|
||||
constexpr const char* SUMMARY_SELECT = "SELECT g.*, "
|
||||
"(SELECT COUNT(*) FROM guild_members m WHERE m.guild_id = g.id) AS member_count, "
|
||||
"(SELECT m.character_id FROM guild_members m WHERE m.guild_id = g.id ORDER BY m.guild_rank, m.joined_at, m.character_id LIMIT 1) AS leader_id "
|
||||
"FROM guilds g";
|
||||
}
|
||||
|
||||
int64_t MySQLDatabase::InsertGuild(const Guild& guild) {
|
||||
ExecuteInsert("INSERT INTO guilds (name, name_status, founder_id, created_at) VALUES (?, ?, ?, ?);",
|
||||
guild.name, guild.nameStatus, guild.founderId, guild.createdAt);
|
||||
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
|
||||
return result->next() ? result->getInt64("id") : 0;
|
||||
}
|
||||
|
||||
std::optional<IGuilds::Guild> MySQLDatabase::GetGuild(int64_t guildId) {
|
||||
auto result = ExecuteSelect("SELECT * FROM guilds WHERE id = ?;", guildId);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return ReadGuild(result);
|
||||
}
|
||||
|
||||
std::optional<IGuilds::Guild> MySQLDatabase::GetGuildByName(const std::string& name) {
|
||||
auto result = ExecuteSelect("SELECT * FROM guilds WHERE LOWER(name) = LOWER(?) LIMIT 1;", name);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return ReadGuild(result);
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetGuildName(int64_t guildId, const std::string& name, int32_t nameStatus) {
|
||||
ExecuteUpdate("UPDATE guilds SET name = ?, name_status = ? WHERE id = ?;", name, nameStatus, guildId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteGuild(int64_t guildId) {
|
||||
ExecuteDelete("DELETE FROM guild_members WHERE guild_id = ?;", guildId);
|
||||
ExecuteDelete("DELETE FROM guild_invites WHERE guild_id = ?;", guildId);
|
||||
ExecuteDelete("DELETE FROM guilds WHERE id = ?;", guildId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::AddGuildMember(const Member& member) {
|
||||
ExecuteInsert("INSERT INTO guild_members (character_id, guild_id, guild_rank, joined_at) VALUES (?, ?, ?, ?);",
|
||||
member.characterId, member.guildId, static_cast<uint32_t>(member.rank), member.joinedAt);
|
||||
}
|
||||
|
||||
void MySQLDatabase::RemoveGuildMember(LWOOBJID characterId) {
|
||||
ExecuteDelete("DELETE FROM guild_members WHERE character_id = ?;", characterId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetGuildMemberRank(LWOOBJID characterId, uint8_t rank) {
|
||||
ExecuteUpdate("UPDATE guild_members SET guild_rank = ? WHERE character_id = ?;", static_cast<uint32_t>(rank), characterId);
|
||||
}
|
||||
|
||||
std::optional<IGuilds::Member> MySQLDatabase::GetGuildMember(LWOOBJID characterId) {
|
||||
auto result = ExecuteSelect(std::string(MEMBER_SELECT) + "WHERE m.character_id = ?;", characterId);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return ReadMember(result);
|
||||
}
|
||||
|
||||
std::vector<IGuilds::Member> MySQLDatabase::GetGuildMembers(int64_t guildId) {
|
||||
std::vector<Member> members;
|
||||
auto result = ExecuteSelect(std::string(MEMBER_SELECT) + "WHERE m.guild_id = ? ORDER BY m.guild_rank, m.joined_at, m.character_id;", guildId);
|
||||
while (result->next()) members.push_back(ReadMember(result));
|
||||
return members;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetGuildInvite(const Invite& invite) {
|
||||
ExecuteInsert("REPLACE INTO guild_invites (character_id, guild_id, inviter_id, created_at) VALUES (?, ?, ?, ?);",
|
||||
invite.characterId, invite.guildId, invite.inviterId, invite.createdAt);
|
||||
}
|
||||
|
||||
std::optional<IGuilds::Invite> MySQLDatabase::GetGuildInvite(LWOOBJID characterId) {
|
||||
auto result = ExecuteSelect("SELECT * FROM guild_invites WHERE character_id = ?;", characterId);
|
||||
if (!result->next()) return std::nullopt;
|
||||
Invite invite;
|
||||
invite.characterId = result->getInt64("character_id");
|
||||
invite.guildId = result->getInt64("guild_id");
|
||||
invite.inviterId = result->getInt64("inviter_id");
|
||||
invite.createdAt = result->getInt64("created_at");
|
||||
return invite;
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteGuildInvite(LWOOBJID characterId) {
|
||||
ExecuteDelete("DELETE FROM guild_invites WHERE character_id = ?;", characterId);
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertGuildEvent(const Event& event) {
|
||||
ExecuteInsert("INSERT INTO guild_events (guild_id, time, kind, character_id, character_name, actor, detail) VALUES (?, ?, ?, ?, ?, ?, ?);",
|
||||
event.guildId, event.time, event.kind, event.characterId, event.characterName, event.actor, event.detail);
|
||||
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
|
||||
return result->next() ? result->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
std::vector<IGuilds::Event> MySQLDatabase::GetGuildEvents(int64_t guildId, uint32_t limit) {
|
||||
std::vector<Event> events;
|
||||
auto result = guildId == 0
|
||||
? ExecuteSelect("SELECT * FROM guild_events ORDER BY id DESC LIMIT ?;", limit)
|
||||
: ExecuteSelect("SELECT * FROM guild_events WHERE guild_id = ? ORDER BY id DESC LIMIT ?;", guildId, limit);
|
||||
while (result->next()) events.push_back(ReadEvent(result));
|
||||
return events;
|
||||
}
|
||||
|
||||
IGuilds::GuildPage MySQLDatabase::GetGuildPage(uint32_t start, uint32_t length, const std::string& search, bool pendingOnly) {
|
||||
GuildPage page;
|
||||
const std::string pending = pendingOnly ? " WHERE g.name_status = 1" : "";
|
||||
{
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM guilds g" + pending + ";");
|
||||
page.total = result->next() ? result->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// A number in the search box also matches the id (-1 never does)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(search).value_or(-1);
|
||||
const std::string searchFilter = "(g.name LIKE CONCAT('%', ?, '%') OR g.id = ?)";
|
||||
const std::string where = search.empty() ? pending : (pendingOnly ? pending + " AND " : std::string(" WHERE ")) + searchFilter;
|
||||
if (search.empty()) {
|
||||
page.filtered = page.total;
|
||||
} else {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM guilds g" + where + ";", search, searchId);
|
||||
page.filtered = result->next() ? result->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
const std::string query = std::string(SUMMARY_SELECT) + where + " ORDER BY g.id DESC LIMIT ?, ?;";
|
||||
auto result = search.empty() ? ExecuteSelect(query, start, length) : ExecuteSelect(query, search, searchId, start, length);
|
||||
while (result->next()) {
|
||||
GuildSummary summary;
|
||||
summary.guild = ReadGuild(result);
|
||||
summary.memberCount = result->getUInt("member_count");
|
||||
summary.leaderId = result->isNull("leader_id") ? 0 : result->getInt64("leader_id");
|
||||
page.guilds.push_back(std::move(summary));
|
||||
}
|
||||
for (auto& summary : page.guilds) {
|
||||
if (summary.leaderId == 0) continue;
|
||||
auto name = ExecuteSelect("SELECT name FROM charinfo WHERE id = ?;", summary.leaderId);
|
||||
if (name->next()) summary.leaderName = name->getString("name").c_str();
|
||||
}
|
||||
return page;
|
||||
}
|
||||
@@ -77,5 +77,7 @@ void SQLiteDatabase::DeleteCharacter(const LWOOBJID characterId) {
|
||||
ExecuteDelete("DELETE FROM ugc_modular_build WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM pet_names WHERE owner_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM player_positions WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM guild_members WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM guild_invites WHERE character_id=? OR inviter_id=?;", characterId, characterId);
|
||||
ExecuteDelete("DELETE FROM charinfo WHERE id=?;", characterId);
|
||||
}
|
||||
|
||||
@@ -211,6 +211,23 @@ public:
|
||||
std::vector<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) override;
|
||||
std::vector<UgcMail> GetUgcMail(const std::vector<LWOOBJID>& subkeys, const LOT modelItemLot) override;
|
||||
|
||||
// IGuilds
|
||||
int64_t InsertGuild(const Guild& guild) override;
|
||||
std::optional<Guild> GetGuild(int64_t guildId) override;
|
||||
std::optional<Guild> GetGuildByName(const std::string& name) override;
|
||||
void SetGuildName(int64_t guildId, const std::string& name, int32_t nameStatus) override;
|
||||
void DeleteGuild(int64_t guildId) override;
|
||||
void AddGuildMember(const Member& member) override;
|
||||
void RemoveGuildMember(LWOOBJID characterId) override;
|
||||
void SetGuildMemberRank(LWOOBJID characterId, uint8_t rank) override;
|
||||
std::optional<Member> GetGuildMember(LWOOBJID characterId) override;
|
||||
std::vector<Member> GetGuildMembers(int64_t guildId) override;
|
||||
void SetGuildInvite(const Invite& invite) override;
|
||||
std::optional<Invite> GetGuildInvite(LWOOBJID characterId) override;
|
||||
void DeleteGuildInvite(LWOOBJID characterId) override;
|
||||
uint64_t InsertGuildEvent(const Event& event) override;
|
||||
std::vector<Event> GetGuildEvents(int64_t guildId, uint32_t limit) override;
|
||||
GuildPage GetGuildPage(uint32_t start, uint32_t length, const std::string& search, bool pendingOnly) override;
|
||||
// IPermissionGrants
|
||||
uint64_t InsertPermissionGrant(const Grant& grant) override;
|
||||
std::optional<Grant> GetPermissionGrant(uint64_t id) override;
|
||||
|
||||
@@ -13,6 +13,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES
|
||||
"AccountStrikes.cpp"
|
||||
"ApiKeys.cpp"
|
||||
"PermissionGrants.cpp"
|
||||
"Guilds.cpp"
|
||||
"Moderation.cpp"
|
||||
"ServerHealth.cpp"
|
||||
"PlayerPositions.cpp"
|
||||
|
||||
174
dDatabase/GameDatabase/SQLite/Tables/Guilds.cpp
Normal file
174
dDatabase/GameDatabase/SQLite/Tables/Guilds.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#include "SQLiteDatabase.h"
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
|
||||
namespace {
|
||||
IGuilds::Guild ReadGuild(CppSQLite3Query& result) {
|
||||
IGuilds::Guild guild;
|
||||
guild.id = result.getInt64Field("id");
|
||||
guild.name = result.getStringField("name");
|
||||
guild.nameStatus = result.getIntField("name_status");
|
||||
guild.founderId = result.getInt64Field("founder_id");
|
||||
guild.createdAt = result.getInt64Field("created_at");
|
||||
return guild;
|
||||
}
|
||||
|
||||
IGuilds::Member ReadMember(CppSQLite3Query& result) {
|
||||
IGuilds::Member member;
|
||||
member.characterId = result.getInt64Field("character_id");
|
||||
member.guildId = result.getInt64Field("guild_id");
|
||||
member.rank = static_cast<uint8_t>(result.getIntField("guild_rank"));
|
||||
member.joinedAt = result.getInt64Field("joined_at");
|
||||
member.name = result.getStringField("name", "");
|
||||
return member;
|
||||
}
|
||||
|
||||
IGuilds::Event ReadEvent(CppSQLite3Query& result) {
|
||||
IGuilds::Event event;
|
||||
event.id = static_cast<uint64_t>(result.getInt64Field("id"));
|
||||
event.guildId = result.getInt64Field("guild_id");
|
||||
event.time = result.getInt64Field("time");
|
||||
event.kind = result.getStringField("kind");
|
||||
event.characterId = result.getInt64Field("character_id");
|
||||
event.characterName = result.getStringField("character_name");
|
||||
event.actor = result.getStringField("actor");
|
||||
event.detail = result.getStringField("detail");
|
||||
return event;
|
||||
}
|
||||
|
||||
constexpr const char* MEMBER_SELECT = "SELECT m.character_id, m.guild_id, m.guild_rank, m.joined_at, c.name "
|
||||
"FROM guild_members m LEFT JOIN charinfo c ON c.id = m.character_id ";
|
||||
|
||||
// The member count and leader (lowest rank, first to join) of the guild in the outer query as g
|
||||
constexpr const char* SUMMARY_SELECT = "SELECT g.*, "
|
||||
"(SELECT COUNT(*) FROM guild_members m WHERE m.guild_id = g.id) AS member_count, "
|
||||
"(SELECT m.character_id FROM guild_members m WHERE m.guild_id = g.id ORDER BY m.guild_rank, m.joined_at, m.character_id LIMIT 1) AS leader_id "
|
||||
"FROM guilds g";
|
||||
}
|
||||
|
||||
int64_t SQLiteDatabase::InsertGuild(const Guild& guild) {
|
||||
ExecuteInsert("INSERT INTO guilds (name, name_status, founder_id, created_at) VALUES (?, ?, ?, ?);",
|
||||
guild.name, guild.nameStatus, guild.founderId, guild.createdAt);
|
||||
auto [_, result] = ExecuteSelect("SELECT last_insert_rowid() AS id;");
|
||||
return result.eof() ? 0 : result.getInt64Field("id");
|
||||
}
|
||||
|
||||
std::optional<IGuilds::Guild> SQLiteDatabase::GetGuild(int64_t guildId) {
|
||||
auto [_, result] = ExecuteSelect("SELECT * FROM guilds WHERE id = ?;", guildId);
|
||||
if (result.eof()) return std::nullopt;
|
||||
return ReadGuild(result);
|
||||
}
|
||||
|
||||
std::optional<IGuilds::Guild> SQLiteDatabase::GetGuildByName(const std::string& name) {
|
||||
auto [_, result] = ExecuteSelect("SELECT * FROM guilds WHERE LOWER(name) = LOWER(?) LIMIT 1;", name);
|
||||
if (result.eof()) return std::nullopt;
|
||||
return ReadGuild(result);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::SetGuildName(int64_t guildId, const std::string& name, int32_t nameStatus) {
|
||||
ExecuteUpdate("UPDATE guilds SET name = ?, name_status = ? WHERE id = ?;", name, nameStatus, guildId);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::DeleteGuild(int64_t guildId) {
|
||||
ExecuteDelete("DELETE FROM guild_members WHERE guild_id = ?;", guildId);
|
||||
ExecuteDelete("DELETE FROM guild_invites WHERE guild_id = ?;", guildId);
|
||||
ExecuteDelete("DELETE FROM guilds WHERE id = ?;", guildId);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::AddGuildMember(const Member& member) {
|
||||
ExecuteInsert("INSERT INTO guild_members (character_id, guild_id, guild_rank, joined_at) VALUES (?, ?, ?, ?);",
|
||||
member.characterId, member.guildId, static_cast<uint32_t>(member.rank), member.joinedAt);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::RemoveGuildMember(LWOOBJID characterId) {
|
||||
ExecuteDelete("DELETE FROM guild_members WHERE character_id = ?;", characterId);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::SetGuildMemberRank(LWOOBJID characterId, uint8_t rank) {
|
||||
ExecuteUpdate("UPDATE guild_members SET guild_rank = ? WHERE character_id = ?;", static_cast<uint32_t>(rank), characterId);
|
||||
}
|
||||
|
||||
std::optional<IGuilds::Member> SQLiteDatabase::GetGuildMember(LWOOBJID characterId) {
|
||||
auto [_, result] = ExecuteSelect(std::string(MEMBER_SELECT) + "WHERE m.character_id = ?;", characterId);
|
||||
if (result.eof()) return std::nullopt;
|
||||
return ReadMember(result);
|
||||
}
|
||||
|
||||
std::vector<IGuilds::Member> SQLiteDatabase::GetGuildMembers(int64_t guildId) {
|
||||
std::vector<Member> members;
|
||||
auto [_, result] = ExecuteSelect(std::string(MEMBER_SELECT) + "WHERE m.guild_id = ? ORDER BY m.guild_rank, m.joined_at, m.character_id;", guildId);
|
||||
for (; !result.eof(); result.nextRow()) members.push_back(ReadMember(result));
|
||||
return members;
|
||||
}
|
||||
|
||||
void SQLiteDatabase::SetGuildInvite(const Invite& invite) {
|
||||
ExecuteInsert("REPLACE INTO guild_invites (character_id, guild_id, inviter_id, created_at) VALUES (?, ?, ?, ?);",
|
||||
invite.characterId, invite.guildId, invite.inviterId, invite.createdAt);
|
||||
}
|
||||
|
||||
std::optional<IGuilds::Invite> SQLiteDatabase::GetGuildInvite(LWOOBJID characterId) {
|
||||
auto [_, result] = ExecuteSelect("SELECT * FROM guild_invites WHERE character_id = ?;", characterId);
|
||||
if (result.eof()) return std::nullopt;
|
||||
Invite invite;
|
||||
invite.characterId = result.getInt64Field("character_id");
|
||||
invite.guildId = result.getInt64Field("guild_id");
|
||||
invite.inviterId = result.getInt64Field("inviter_id");
|
||||
invite.createdAt = result.getInt64Field("created_at");
|
||||
return invite;
|
||||
}
|
||||
|
||||
void SQLiteDatabase::DeleteGuildInvite(LWOOBJID characterId) {
|
||||
ExecuteDelete("DELETE FROM guild_invites WHERE character_id = ?;", characterId);
|
||||
}
|
||||
|
||||
uint64_t SQLiteDatabase::InsertGuildEvent(const Event& event) {
|
||||
ExecuteInsert("INSERT INTO guild_events (guild_id, time, kind, character_id, character_name, actor, detail) VALUES (?, ?, ?, ?, ?, ?, ?);",
|
||||
event.guildId, event.time, event.kind, event.characterId, event.characterName, event.actor, event.detail);
|
||||
auto [_, result] = ExecuteSelect("SELECT last_insert_rowid() AS id;");
|
||||
return result.eof() ? 0 : static_cast<uint64_t>(result.getInt64Field("id"));
|
||||
}
|
||||
|
||||
std::vector<IGuilds::Event> SQLiteDatabase::GetGuildEvents(int64_t guildId, uint32_t limit) {
|
||||
std::vector<Event> events;
|
||||
auto [_, result] = guildId == 0
|
||||
? ExecuteSelect("SELECT * FROM guild_events ORDER BY id DESC LIMIT ?;", limit)
|
||||
: ExecuteSelect("SELECT * FROM guild_events WHERE guild_id = ? ORDER BY id DESC LIMIT ?;", guildId, limit);
|
||||
for (; !result.eof(); result.nextRow()) events.push_back(ReadEvent(result));
|
||||
return events;
|
||||
}
|
||||
|
||||
IGuilds::GuildPage SQLiteDatabase::GetGuildPage(uint32_t start, uint32_t length, const std::string& search, bool pendingOnly) {
|
||||
GuildPage page;
|
||||
const std::string pending = pendingOnly ? " WHERE g.name_status = 1" : "";
|
||||
{
|
||||
auto [_, result] = ExecuteSelect("SELECT COUNT(*) AS count FROM guilds g" + pending + ";");
|
||||
page.total = result.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
|
||||
}
|
||||
|
||||
// A number in the search box also matches the id (-1 never does)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(search).value_or(-1);
|
||||
const std::string searchFilter = "(g.name LIKE '%' || ? || '%' OR g.id = ?)";
|
||||
const std::string where = search.empty() ? pending : (pendingOnly ? pending + " AND " : std::string(" WHERE ")) + searchFilter;
|
||||
if (search.empty()) {
|
||||
page.filtered = page.total;
|
||||
} else {
|
||||
auto [_, result] = ExecuteSelect("SELECT COUNT(*) AS count FROM guilds g" + where + ";", search, searchId);
|
||||
page.filtered = result.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
|
||||
}
|
||||
|
||||
const std::string query = std::string(SUMMARY_SELECT) + where + " ORDER BY g.id DESC LIMIT ? OFFSET ?;";
|
||||
auto [_, result] = search.empty() ? ExecuteSelect(query, length, start) : ExecuteSelect(query, search, searchId, length, start);
|
||||
for (; !result.eof(); result.nextRow()) {
|
||||
GuildSummary summary;
|
||||
summary.guild = ReadGuild(result);
|
||||
summary.memberCount = static_cast<uint32_t>(result.getIntField("member_count"));
|
||||
summary.leaderId = result.getInt64Field("leader_id", 0);
|
||||
page.guilds.push_back(std::move(summary));
|
||||
}
|
||||
for (auto& summary : page.guilds) {
|
||||
if (summary.leaderId == 0) continue;
|
||||
auto [__, name] = ExecuteSelect("SELECT name FROM charinfo WHERE id = ?;", summary.leaderId);
|
||||
if (!name.eof()) summary.leaderName = name.getStringField("name");
|
||||
}
|
||||
return page;
|
||||
}
|
||||
@@ -214,6 +214,22 @@ class TestSQLDatabase : public GameDatabase {
|
||||
std::vector<AccountNote> GetAccountNotes(uint32_t accountId) override { return {}; }
|
||||
std::optional<AccountNote> GetAccountNote(uint64_t id) override { return {}; }
|
||||
void DeleteAccountNote(uint64_t id) override {}
|
||||
int64_t InsertGuild(const Guild& guild) override { return 0; }
|
||||
std::optional<Guild> GetGuild(int64_t guildId) override { return {}; }
|
||||
std::optional<Guild> GetGuildByName(const std::string& name) override { return {}; }
|
||||
void SetGuildName(int64_t guildId, const std::string& name, int32_t nameStatus) override {}
|
||||
void DeleteGuild(int64_t guildId) override {}
|
||||
void AddGuildMember(const Member& member) override {}
|
||||
void RemoveGuildMember(LWOOBJID characterId) override {}
|
||||
void SetGuildMemberRank(LWOOBJID characterId, uint8_t rank) override {}
|
||||
std::optional<Member> GetGuildMember(LWOOBJID characterId) override { return {}; }
|
||||
std::vector<Member> GetGuildMembers(int64_t guildId) override { return {}; }
|
||||
void SetGuildInvite(const Invite& invite) override {}
|
||||
std::optional<Invite> GetGuildInvite(LWOOBJID characterId) override { return {}; }
|
||||
void DeleteGuildInvite(LWOOBJID characterId) override {}
|
||||
uint64_t InsertGuildEvent(const Event& event) override { return 0; }
|
||||
std::vector<Event> GetGuildEvents(int64_t guildId, uint32_t limit) override { return {}; }
|
||||
GuildPage GetGuildPage(uint32_t start, uint32_t length, const std::string& search, bool pendingOnly) override { return {}; }
|
||||
uint64_t InsertPermissionGrant(const Grant& grant) override { return 0; }
|
||||
std::optional<Grant> GetPermissionGrant(uint64_t id) override { return {}; }
|
||||
std::vector<Grant> GetPermissionGrants(const std::string& targetType, int64_t targetId) override { return {}; }
|
||||
|
||||
Reference in New Issue
Block a user