From fff4882b4937121ed3640f0fbb94007a4d9d4f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AiDN=E2=84=A2?= <45371311+originalaidn@users.noreply.github.com> Date: Thu, 27 Jan 2022 13:22:24 +0100 Subject: [PATCH 1/9] added folders, added translations --- discord_api.smx => plugins/discord_api.smx | Bin .../discord_utilities.smx | Bin .../discord/DiscordRequest.sp | 0 .../discord/GetGuildChannels.sp | 0 {include => scripting}/discord/GetGuilds.sp | 0 .../discord/GuildMember.inc | 0 .../discord/GuildMembers.sp | 0 {include => scripting}/discord/GuildRole.sp | 0 .../discord/ListenToChannel.sp | 0 .../discord/MessageObject.sp | 0 {include => scripting}/discord/SendMessage.sp | 0 {include => scripting}/discord/SendWebHook.sp | 0 {include => scripting}/discord/UserObject.sp | 0 {include => scripting}/discord/bot.inc | 0 {include => scripting}/discord/channel.inc | 0 .../discord/deletemessage.sp | 0 {include => scripting}/discord/message.inc | 0 .../discord/message_embed.inc | 0 {include => scripting}/discord/reactions.sp | 0 {include => scripting}/discord/stocks.inc | 0 {include => scripting}/discord/user.inc | 0 {include => scripting}/discord/webhook.inc | 0 discord_api.sp => scripting/discord_api.sp | 0 .../discord_utilities.sp | 0 .../discord_utilities/discordrequest.sp | 372 ++--- .../discord_utilities/forwards.sp | 505 +++--- .../discord_utilities/globals.sp | 96 +- .../discord_utilities/helpers.sp | 1360 ++++++++--------- .../discord_utilities/modules.sp | 242 +-- .../discord_utilities/natives.sp | 158 +- .../discord_utilities/sql.sp | 624 ++++---- scripting/include/calladmin.inc | 292 ++++ {include => scripting/include}/discord.inc | 0 .../include}/discord_utilities.inc | 248 +-- .../Discord-Utilities.phrases.txt | 35 +- 35 files changed, 2117 insertions(+), 1815 deletions(-) rename discord_api.smx => plugins/discord_api.smx (100%) rename discord_utilities.smx => plugins/discord_utilities.smx (100%) rename {include => scripting}/discord/DiscordRequest.sp (100%) rename {include => scripting}/discord/GetGuildChannels.sp (100%) rename {include => scripting}/discord/GetGuilds.sp (100%) rename {include => scripting}/discord/GuildMember.inc (100%) rename {include => scripting}/discord/GuildMembers.sp (100%) rename {include => scripting}/discord/GuildRole.sp (100%) rename {include => scripting}/discord/ListenToChannel.sp (100%) rename {include => scripting}/discord/MessageObject.sp (100%) rename {include => scripting}/discord/SendMessage.sp (100%) rename {include => scripting}/discord/SendWebHook.sp (100%) rename {include => scripting}/discord/UserObject.sp (100%) rename {include => scripting}/discord/bot.inc (100%) rename {include => scripting}/discord/channel.inc (100%) rename {include => scripting}/discord/deletemessage.sp (100%) rename {include => scripting}/discord/message.inc (100%) rename {include => scripting}/discord/message_embed.inc (100%) rename {include => scripting}/discord/reactions.sp (100%) rename {include => scripting}/discord/stocks.inc (100%) rename {include => scripting}/discord/user.inc (100%) rename {include => scripting}/discord/webhook.inc (100%) rename discord_api.sp => scripting/discord_api.sp (100%) rename discord_utilities.sp => scripting/discord_utilities.sp (100%) rename {include => scripting}/discord_utilities/discordrequest.sp (95%) rename {include => scripting}/discord_utilities/forwards.sp (82%) rename {include => scripting}/discord_utilities/globals.sp (96%) rename {include => scripting}/discord_utilities/helpers.sp (96%) rename {include => scripting}/discord_utilities/modules.sp (96%) rename {include => scripting}/discord_utilities/natives.sp (96%) rename {include => scripting}/discord_utilities/sql.sp (96%) create mode 100644 scripting/include/calladmin.inc rename {include => scripting/include}/discord.inc (100%) rename {include => scripting/include}/discord_utilities.inc (96%) rename Discord-Utilities.phrases.txt => translations/Discord-Utilities.phrases.txt (93%) diff --git a/discord_api.smx b/plugins/discord_api.smx similarity index 100% rename from discord_api.smx rename to plugins/discord_api.smx diff --git a/discord_utilities.smx b/plugins/discord_utilities.smx similarity index 100% rename from discord_utilities.smx rename to plugins/discord_utilities.smx diff --git a/include/discord/DiscordRequest.sp b/scripting/discord/DiscordRequest.sp similarity index 100% rename from include/discord/DiscordRequest.sp rename to scripting/discord/DiscordRequest.sp diff --git a/include/discord/GetGuildChannels.sp b/scripting/discord/GetGuildChannels.sp similarity index 100% rename from include/discord/GetGuildChannels.sp rename to scripting/discord/GetGuildChannels.sp diff --git a/include/discord/GetGuilds.sp b/scripting/discord/GetGuilds.sp similarity index 100% rename from include/discord/GetGuilds.sp rename to scripting/discord/GetGuilds.sp diff --git a/include/discord/GuildMember.inc b/scripting/discord/GuildMember.inc similarity index 100% rename from include/discord/GuildMember.inc rename to scripting/discord/GuildMember.inc diff --git a/include/discord/GuildMembers.sp b/scripting/discord/GuildMembers.sp similarity index 100% rename from include/discord/GuildMembers.sp rename to scripting/discord/GuildMembers.sp diff --git a/include/discord/GuildRole.sp b/scripting/discord/GuildRole.sp similarity index 100% rename from include/discord/GuildRole.sp rename to scripting/discord/GuildRole.sp diff --git a/include/discord/ListenToChannel.sp b/scripting/discord/ListenToChannel.sp similarity index 100% rename from include/discord/ListenToChannel.sp rename to scripting/discord/ListenToChannel.sp diff --git a/include/discord/MessageObject.sp b/scripting/discord/MessageObject.sp similarity index 100% rename from include/discord/MessageObject.sp rename to scripting/discord/MessageObject.sp diff --git a/include/discord/SendMessage.sp b/scripting/discord/SendMessage.sp similarity index 100% rename from include/discord/SendMessage.sp rename to scripting/discord/SendMessage.sp diff --git a/include/discord/SendWebHook.sp b/scripting/discord/SendWebHook.sp similarity index 100% rename from include/discord/SendWebHook.sp rename to scripting/discord/SendWebHook.sp diff --git a/include/discord/UserObject.sp b/scripting/discord/UserObject.sp similarity index 100% rename from include/discord/UserObject.sp rename to scripting/discord/UserObject.sp diff --git a/include/discord/bot.inc b/scripting/discord/bot.inc similarity index 100% rename from include/discord/bot.inc rename to scripting/discord/bot.inc diff --git a/include/discord/channel.inc b/scripting/discord/channel.inc similarity index 100% rename from include/discord/channel.inc rename to scripting/discord/channel.inc diff --git a/include/discord/deletemessage.sp b/scripting/discord/deletemessage.sp similarity index 100% rename from include/discord/deletemessage.sp rename to scripting/discord/deletemessage.sp diff --git a/include/discord/message.inc b/scripting/discord/message.inc similarity index 100% rename from include/discord/message.inc rename to scripting/discord/message.inc diff --git a/include/discord/message_embed.inc b/scripting/discord/message_embed.inc similarity index 100% rename from include/discord/message_embed.inc rename to scripting/discord/message_embed.inc diff --git a/include/discord/reactions.sp b/scripting/discord/reactions.sp similarity index 100% rename from include/discord/reactions.sp rename to scripting/discord/reactions.sp diff --git a/include/discord/stocks.inc b/scripting/discord/stocks.inc similarity index 100% rename from include/discord/stocks.inc rename to scripting/discord/stocks.inc diff --git a/include/discord/user.inc b/scripting/discord/user.inc similarity index 100% rename from include/discord/user.inc rename to scripting/discord/user.inc diff --git a/include/discord/webhook.inc b/scripting/discord/webhook.inc similarity index 100% rename from include/discord/webhook.inc rename to scripting/discord/webhook.inc diff --git a/discord_api.sp b/scripting/discord_api.sp similarity index 100% rename from discord_api.sp rename to scripting/discord_api.sp diff --git a/discord_utilities.sp b/scripting/discord_utilities.sp similarity index 100% rename from discord_utilities.sp rename to scripting/discord_utilities.sp diff --git a/include/discord_utilities/discordrequest.sp b/scripting/discord_utilities/discordrequest.sp similarity index 95% rename from include/discord_utilities/discordrequest.sp rename to scripting/discord_utilities/discordrequest.sp index 92cf0c0..adad41d 100644 --- a/include/discord_utilities/discordrequest.sp +++ b/scripting/discord_utilities/discordrequest.sp @@ -1,187 +1,187 @@ -methodmap DiscordRequest < Handle -{ - public DiscordRequest(char[] url, EHTTPMethod method) - { - Handle request = SteamWorks_CreateHTTPRequest(method, url); - return view_as(request); - } - - public void SetJsonBody(Handle hJson) - { - static char stringJson[16384]; - stringJson[0] = '\0'; - if(hJson != null) - { - json_dump(hJson, stringJson, sizeof(stringJson), 0, true); - } - SteamWorks_SetHTTPRequestRawPostBody(this, "application/json; charset=UTF-8", stringJson, strlen(stringJson)); - if(hJson != null) delete hJson; - } - - public void SetJsonBodyEx(Handle hJson) - { - static char stringJson[16384]; - stringJson[0] = '\0'; - if(hJson != null) - { - json_dump(hJson, stringJson, sizeof(stringJson), 0, true); - } - SteamWorks_SetHTTPRequestRawPostBody(this, "application/json; charset=UTF-8", stringJson, strlen(stringJson)); - } - - property int Timeout - { - public set(int timeout) - { - SteamWorks_SetHTTPRequestNetworkActivityTimeout(this, timeout); - } - } - - public void SetCallbacks(SteamWorksHTTPRequestCompleted OnComplete, SteamWorksHTTPDataReceived DataReceived) - { - SteamWorks_SetHTTPCallbacks(this, OnComplete, HeadersReceived, DataReceived); - } - - public void SetContentSize() - { - SteamWorks_SetHTTPRequestHeaderValue(this, "Content-Length", "0"); - } - - public void SetContextValue(any data1, any data2) - { - SteamWorks_SetHTTPRequestContextValue(this, data1, data2); - } - - public void SetData(any data1, char[] route) - { - SteamWorks_SetHTTPRequestContextValue(this, data1, UrlToDP(route)); - } - - public void SetBot(DiscordBot bawt) - { - BuildAuthHeader(this, bawt); - } - - public void Send(char[] route) - { - DiscordSendRequest(this, route); - } -} - -public int HeadersReceived(Handle request, bool failure, any data, any datapack) -{ - DataPack dp = view_as(datapack); - if(failure) - { - delete dp; - return; - } - - char xRateLimit[16]; - char xRateLeft[16]; - char xRateReset[32]; - - bool exists = false; - - exists = SteamWorks_GetHTTPResponseHeaderValue(request, "X-RateLimit-Limit", xRateLimit, sizeof(xRateLimit)); - exists = SteamWorks_GetHTTPResponseHeaderValue(request, "X-RateLimit-Remaining", xRateLeft, sizeof(xRateLeft)); - exists = SteamWorks_GetHTTPResponseHeaderValue(request, "X-RateLimit-Reset", xRateReset, sizeof(xRateReset)); - - //Get url - char route[128]; - ResetPack(dp); - ReadPackString(dp, route, sizeof(route)); - delete dp; - - int reset = StringToInt(xRateReset); - if(reset > GetTime() + 3) - { - reset = GetTime() + 3; - } - - if(exists) - { - SetTrieValue(hRateReset, route, reset); - SetTrieValue(hRateLeft, route, StringToInt(xRateLeft)); - SetTrieValue(hRateLimit, route, StringToInt(xRateLimit)); - } - else - { - SetTrieValue(hRateReset, route, -1); - SetTrieValue(hRateLeft, route, -1); - SetTrieValue(hRateLimit, route, -1); - } -} - -public Handle UrlToDP(char[] url) -{ - DataPack dp = new DataPack(); - WritePackString(dp, url); - return dp; -} - -stock void BuildAuthHeader(Handle request, DiscordBot bawt) -{ - static char buffer[256]; - static char token[196]; - JsonObjectGetString(bawt, "token", token, sizeof(token)); - FormatEx(buffer, sizeof(buffer), "Bot %s", token); - SteamWorks_SetHTTPRequestHeaderValue(request, "Authorization", buffer); - //g_bIsBotLoaded = true; -} - -public void DiscordSendRequest(Handle request, const char[] route) -{ - //Check for reset - int time = GetTime(); - int resetTime; - - int defLimit = 0; - if(!GetTrieValue(hRateLimit, route, defLimit)) - { - defLimit = 1; - } - - bool exists = GetTrieValue(hRateReset, route, resetTime); - - if(!exists) - { - SetTrieValue(hRateReset, route, GetTime() + 5); - SetTrieValue(hRateLeft, route, defLimit - 1); - SteamWorks_SendHTTPRequest(request); - return; - } - - if(time == -1) - { - //No x-rate-limit send - SteamWorks_SendHTTPRequest(request); - return; - } - - if(time > resetTime) - { - SetTrieValue(hRateLeft, route, defLimit - 1); - SteamWorks_SendHTTPRequest(request); - return; - } - else - { - int left; - GetTrieValue(hRateLeft, route, left); - if(left == 0) - { - float remaining = float(resetTime) - float(time) + 1.0; - Handle dp = new DataPack(); - WritePackCell(dp, request); - WritePackString(dp, route); - CreateTimer(remaining, SendRequestAgain, dp); - } - else - { - left--; - SetTrieValue(hRateLeft, route, left); - SteamWorks_SendHTTPRequest(request); - } - } +methodmap DiscordRequest < Handle +{ + public DiscordRequest(char[] url, EHTTPMethod method) + { + Handle request = SteamWorks_CreateHTTPRequest(method, url); + return view_as(request); + } + + public void SetJsonBody(Handle hJson) + { + static char stringJson[16384]; + stringJson[0] = '\0'; + if(hJson != null) + { + json_dump(hJson, stringJson, sizeof(stringJson), 0, true); + } + SteamWorks_SetHTTPRequestRawPostBody(this, "application/json; charset=UTF-8", stringJson, strlen(stringJson)); + if(hJson != null) delete hJson; + } + + public void SetJsonBodyEx(Handle hJson) + { + static char stringJson[16384]; + stringJson[0] = '\0'; + if(hJson != null) + { + json_dump(hJson, stringJson, sizeof(stringJson), 0, true); + } + SteamWorks_SetHTTPRequestRawPostBody(this, "application/json; charset=UTF-8", stringJson, strlen(stringJson)); + } + + property int Timeout + { + public set(int timeout) + { + SteamWorks_SetHTTPRequestNetworkActivityTimeout(this, timeout); + } + } + + public void SetCallbacks(SteamWorksHTTPRequestCompleted OnComplete, SteamWorksHTTPDataReceived DataReceived) + { + SteamWorks_SetHTTPCallbacks(this, OnComplete, HeadersReceived, DataReceived); + } + + public void SetContentSize() + { + SteamWorks_SetHTTPRequestHeaderValue(this, "Content-Length", "0"); + } + + public void SetContextValue(any data1, any data2) + { + SteamWorks_SetHTTPRequestContextValue(this, data1, data2); + } + + public void SetData(any data1, char[] route) + { + SteamWorks_SetHTTPRequestContextValue(this, data1, UrlToDP(route)); + } + + public void SetBot(DiscordBot bawt) + { + BuildAuthHeader(this, bawt); + } + + public void Send(char[] route) + { + DiscordSendRequest(this, route); + } +} + +public int HeadersReceived(Handle request, bool failure, any data, any datapack) +{ + DataPack dp = view_as(datapack); + if(failure) + { + delete dp; + return; + } + + char xRateLimit[16]; + char xRateLeft[16]; + char xRateReset[32]; + + bool exists = false; + + exists = SteamWorks_GetHTTPResponseHeaderValue(request, "X-RateLimit-Limit", xRateLimit, sizeof(xRateLimit)); + exists = SteamWorks_GetHTTPResponseHeaderValue(request, "X-RateLimit-Remaining", xRateLeft, sizeof(xRateLeft)); + exists = SteamWorks_GetHTTPResponseHeaderValue(request, "X-RateLimit-Reset", xRateReset, sizeof(xRateReset)); + + //Get url + char route[128]; + ResetPack(dp); + ReadPackString(dp, route, sizeof(route)); + delete dp; + + int reset = StringToInt(xRateReset); + if(reset > GetTime() + 3) + { + reset = GetTime() + 3; + } + + if(exists) + { + SetTrieValue(hRateReset, route, reset); + SetTrieValue(hRateLeft, route, StringToInt(xRateLeft)); + SetTrieValue(hRateLimit, route, StringToInt(xRateLimit)); + } + else + { + SetTrieValue(hRateReset, route, -1); + SetTrieValue(hRateLeft, route, -1); + SetTrieValue(hRateLimit, route, -1); + } +} + +public Handle UrlToDP(char[] url) +{ + DataPack dp = new DataPack(); + WritePackString(dp, url); + return dp; +} + +stock void BuildAuthHeader(Handle request, DiscordBot bawt) +{ + static char buffer[256]; + static char token[196]; + JsonObjectGetString(bawt, "token", token, sizeof(token)); + FormatEx(buffer, sizeof(buffer), "Bot %s", token); + SteamWorks_SetHTTPRequestHeaderValue(request, "Authorization", buffer); + //g_bIsBotLoaded = true; +} + +public void DiscordSendRequest(Handle request, const char[] route) +{ + //Check for reset + int time = GetTime(); + int resetTime; + + int defLimit = 0; + if(!GetTrieValue(hRateLimit, route, defLimit)) + { + defLimit = 1; + } + + bool exists = GetTrieValue(hRateReset, route, resetTime); + + if(!exists) + { + SetTrieValue(hRateReset, route, GetTime() + 5); + SetTrieValue(hRateLeft, route, defLimit - 1); + SteamWorks_SendHTTPRequest(request); + return; + } + + if(time == -1) + { + //No x-rate-limit send + SteamWorks_SendHTTPRequest(request); + return; + } + + if(time > resetTime) + { + SetTrieValue(hRateLeft, route, defLimit - 1); + SteamWorks_SendHTTPRequest(request); + return; + } + else + { + int left; + GetTrieValue(hRateLeft, route, left); + if(left == 0) + { + float remaining = float(resetTime) - float(time) + 1.0; + Handle dp = new DataPack(); + WritePackCell(dp, request); + WritePackString(dp, route); + CreateTimer(remaining, SendRequestAgain, dp); + } + else + { + left--; + SetTrieValue(hRateLeft, route, left); + SteamWorks_SendHTTPRequest(request); + } + } } \ No newline at end of file diff --git a/include/discord_utilities/forwards.sp b/scripting/discord_utilities/forwards.sp similarity index 82% rename from include/discord_utilities/forwards.sp rename to scripting/discord_utilities/forwards.sp index b0d5113..27f7c13 100644 --- a/include/discord_utilities/forwards.sp +++ b/scripting/discord_utilities/forwards.sp @@ -1,247 +1,258 @@ -public void OnPluginEnd() -{ - KillBot(); -} - -public void OnLibraryAdded(const char[] szLibrary) -{ - if(StrEqual(szLibrary, "shavit")) g_bShavit = true; -} - -public void OnLibraryRemoved(const char[] szLibrary) -{ - if(StrEqual(szLibrary, "shavit")) g_bShavit = false; -} - -public void OnAllPluginsLoaded() -{ - if(!LibraryExists("discord-api")) - { - SetFailState("[Discord-Utilities] This plugin is fully dependant on \"Discord-API\" by Deathknife. (https://github.com/Deathknife/sourcemod-discord)"); - } - - g_bShavit = LibraryExists("shavit"); -} - -public void OnConfigsExecuted() -{ - LoadCvars(); - - if(Bot == view_as(INVALID_HANDLE)) - { - if(!CommandExists(g_sViewIDCommand)) - { - RegConsoleCmd(g_sViewIDCommand, Command_ViewId); - RegConsoleCmd("sm_verify", Command_ViewId); - } - CreateBot(); - } - - LoadCommands(); - - char sDTB[32]; - g_cDatabaseName.GetString(sDTB, sizeof(sDTB)); - g_cTableName.GetString(g_sTableName, sizeof(g_sTableName)); - SQL_TConnect(SQLQuery_Connect, sDTB); -} - -public void OnMapEnd() -{ - KillBot(); -} - -public void OnMapStart() -{ - if(g_cCheckInterval.FloatValue) - { - CreateTimer(g_cCheckInterval.FloatValue, VerifyAccounts, _, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE); - } -} - -public void OnClientDisconnect(int client) -{ - if(g_hDB == null) - { - return; - } - UpdatePlayer(client); -} - -public void OnClientPutInServer(int client) -{ - g_bChecked[client] = false; - g_bMember[client] = false; - g_sUniqueCode[client][0] = '\0'; - g_sUserID[client][0] = '\0'; - g_bRoleGiven[client] = false; -} - -public Action OnClientPreAdminCheck(int client) -{ - if(IsFakeClient(client) || g_hDB == null) - { - return; - } - - char szQuery[512], szSteamId[32]; - GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); - if(g_bIsMySQl) - { - g_hDB.Format(szQuery, sizeof(szQuery), "SELECT userid, member FROM %s WHERE steamid = '%s';", g_sTableName, szSteamId); - } - else - { - g_hDB.Format(szQuery, sizeof(szQuery), "SELECT userid, member FROM %s WHERE steamid = '%s'", g_sTableName, szSteamId); - } - SQL_TQuery(g_hDB, SQLQuery_GetUserData, szQuery, GetClientUserId(client)); -} - -public int OnSettingsChanged(ConVar convar, const char[] oldVal, const char[] newVal) -{ - if(StrEqual(oldVal, newVal, true)) - { - return; - } - if(convar == g_cVerificationChannelID) - { - strcopy(g_sVerificationChannelID, sizeof(g_sVerificationChannelID), newVal); - } - else if(convar == g_cGuildID) - { - strcopy(g_sGuildID, sizeof(g_sGuildID), newVal); - } - else if(convar == g_cRoleID) - { - strcopy(g_sRoleID, sizeof(g_sRoleID), newVal); - } - else if(convar == g_cBotToken) - { - strcopy(g_sBotToken, sizeof(g_sBotToken), newVal); - } - else if(convar == g_cLinkCommand) - { - strcopy(g_sLinkCommand, sizeof(g_sLinkCommand), newVal); - } - else if(convar == g_cViewIDCommand) - { - strcopy(g_sViewIDCommand, sizeof(g_sViewIDCommand), newVal); - } - else if(convar == g_cInviteLink) - { - strcopy(g_sInviteLink, sizeof(g_sInviteLink), newVal); - } - else if(convar == g_cServerPrefix) - { - strcopy(g_sServerPrefix, sizeof(g_sServerPrefix), newVal); - } - else if(convar == g_cTableName) - { - strcopy(g_sTableName, sizeof(g_sTableName), newVal); - char dtbname[32]; - g_cDatabaseName.GetString(dtbname, sizeof(dtbname)); - SQL_TConnect(SQLQuery_Connect, dtbname); - RefreshClients(); - } -} - -public void GuildList(DiscordBot bawt, char[] id, char[] name, char[] icon, bool owner, int permissions, const bool listen) -{ - Bot.GetGuildChannels(id, ChannelList, INVALID_FUNCTION, listen); -} - -public void ChannelList(DiscordBot bawt, const char[] guild, DiscordChannel Channel, const bool listen) -{ - if(StrEqual(g_sBotToken, "") || StrEqual(g_sVerificationChannelID, "")) - { - return; - } - if(Channel == null || Bot == null) - { - return; - } - if(Bot.IsListeningToChannel(Channel)) - { - //Bot.StopListeningToChannel(Channel); - return; - } - - - char id[20], name[32]; - Channel.GetID(id, sizeof(id)); - Channel.GetName(name, sizeof(name)); - - if(strlen(g_sVerificationChannelID) > 10) - { - if(StrEqual(id, g_sVerificationChannelID)) - { - g_sVerificationChannelName = name; - if(listen) - { - PrintToServer("******** STARTING TO LISTEN ***********"); - Bot.StartListeningToChannel(Channel, OnMessageReceived); - //Bot.PrintChannels(); - //Bot.PrintChannels(); - } - } - } -} - -/* -public Action smpc(int client, int args) -{ - Bot.PrintChannels(); -} -*/ - -public Action Command_ViewId(int client, int args) -{ - if(!client || StrEqual(g_sVerificationChannelID, "")) - { - return Plugin_Handled; - } - if(!g_bChecked[client]) - { - CReplyToCommand(client, "%s %T", g_sServerPrefix, "TryAgainLater", client); - return Plugin_Handled; - } - - //CPrintToChat(client, "%s %T", g_sServerPrefix, "LinkYourID", client, g_sUniqueCode[client]); - if(!g_bMember[client]) - { - CPrintToChat(client, "%s %T", g_sServerPrefix, "LinkConnect", client); - CPrintToChat(client, "%s {blue}%s", g_sServerPrefix, g_sInviteLink); - - CPrintToChat(client, "%s %T", g_sServerPrefix, "LinkUsage", client, g_sLinkCommand, g_sUniqueCode[client]); - CPrintToChat(client, "%s %T", g_sServerPrefix, "LinkUsage2", client, g_sVerificationChannelName); - CPrintToChat(client, "%s You can also {yellow}copy-paste{default} from {green}console{default} :)", g_sServerPrefix); - - PrintToConsole(client, "************************************"); - PrintToConsole(client, "[Discord] %T", "LinkConnect", client); - PrintToConsole(client, "[Discord] %s", g_sInviteLink); - PrintToConsole(client, "[Discord] Use %s %s", g_sLinkCommand, g_sUniqueCode[client]); - PrintToConsole(client, "************************************"); - } - else - { - CPrintToChat(client, "%s - You are already verified. Enjoy your benefits :)", g_sServerPrefix); - CPrintToChat(client, "%s - You can change your verified discord account using {LIME}!unverify{DEFAULT} and {LIME}!verify{DEFAULT} again", g_sServerPrefix); - } - - - - return Plugin_Handled; -} - -public Action Check(int client, const char[] command, int args) -{ - if(!client || client > MaxClients) - { - return Plugin_Continue; - } - if(!g_bMember[client]) - { - CPrintToChat(client, "%s %T", g_sServerPrefix, "MustVerify", client, ChangePartsInString(g_sViewIDCommand, "sm_", "!")); - return Plugin_Stop; - } - return Plugin_Continue; -} +public void OnPluginEnd() +{ + KillBot(); +} + +public void OnLibraryAdded(const char[] szLibrary) +{ + if(StrEqual(szLibrary, "shavit")) g_bShavit = true; +} + +public void OnLibraryRemoved(const char[] szLibrary) +{ + if(StrEqual(szLibrary, "shavit")) g_bShavit = false; +} + +public void OnAllPluginsLoaded() +{ + if(!LibraryExists("discord-api")) + { + SetFailState("[Discord-Utilities] This plugin is fully dependant on \"Discord-API\" by Deathknife. (https://github.com/Deathknife/sourcemod-discord)"); + } + + g_bShavit = LibraryExists("shavit"); +} + +public void OnConfigsExecuted() +{ + LoadCvars(); + + if(Bot == view_as(INVALID_HANDLE)) + { + if(!CommandExists(g_sViewIDCommand)) + { + RegConsoleCmd(g_sViewIDCommand, Command_ViewId); + RegConsoleCmd("sm_verify", Command_ViewId); + } + CreateBot(); + } + + LoadCommands(); + + char sDTB[32]; + g_cDatabaseName.GetString(sDTB, sizeof(sDTB)); + g_cTableName.GetString(g_sTableName, sizeof(g_sTableName)); + SQL_TConnect(SQLQuery_Connect, sDTB); +} + +public void OnMapEnd() +{ + KillBot(); +} + +public void OnMapStart() +{ + if(g_cCheckInterval.FloatValue) + { + CreateTimer(g_cCheckInterval.FloatValue, VerifyAccounts, _, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE); + } +} + +public void OnClientDisconnect(int client) +{ + if(g_hDB == null) + { + return; + } + UpdatePlayer(client); +} + +public void OnClientPutInServer(int client) +{ + g_bChecked[client] = false; + g_bMember[client] = false; + g_sUniqueCode[client][0] = '\0'; + g_sUserID[client][0] = '\0'; + g_bRoleGiven[client] = false; +} + +public Action OnClientPreAdminCheck(int client) +{ + if(IsFakeClient(client) || g_hDB == null) + { + return; + } + + char szQuery[512], szSteamId[32]; + GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); + if(g_bIsMySQl) + { + g_hDB.Format(szQuery, sizeof(szQuery), "SELECT userid, member FROM %s WHERE steamid = '%s';", g_sTableName, szSteamId); + } + else + { + g_hDB.Format(szQuery, sizeof(szQuery), "SELECT userid, member FROM %s WHERE steamid = '%s'", g_sTableName, szSteamId); + } + SQL_TQuery(g_hDB, SQLQuery_GetUserData, szQuery, GetClientUserId(client)); +} + +public int OnSettingsChanged(ConVar convar, const char[] oldVal, const char[] newVal) +{ + if(StrEqual(oldVal, newVal, true)) + { + return; + } + if(convar == g_cVerificationChannelID) + { + strcopy(g_sVerificationChannelID, sizeof(g_sVerificationChannelID), newVal); + } + else if(convar == g_cGuildID) + { + strcopy(g_sGuildID, sizeof(g_sGuildID), newVal); + } + else if(convar == g_cRoleID) + { + strcopy(g_sRoleID, sizeof(g_sRoleID), newVal); + } + else if(convar == g_cBotToken) + { + strcopy(g_sBotToken, sizeof(g_sBotToken), newVal); + } + else if(convar == g_cLinkCommand) + { + strcopy(g_sLinkCommand, sizeof(g_sLinkCommand), newVal); + } + else if(convar == g_cViewIDCommand) + { + strcopy(g_sViewIDCommand, sizeof(g_sViewIDCommand), newVal); + } + else if(convar == g_cInviteLink) + { + strcopy(g_sInviteLink, sizeof(g_sInviteLink), newVal); + } + else if(convar == g_cServerPrefix) + { + strcopy(g_sServerPrefix, sizeof(g_sServerPrefix), newVal); + } + else if(convar == g_cTableName) + { + strcopy(g_sTableName, sizeof(g_sTableName), newVal); + char dtbname[32]; + g_cDatabaseName.GetString(dtbname, sizeof(dtbname)); + SQL_TConnect(SQLQuery_Connect, dtbname); + RefreshClients(); + } +} + +public void GuildList(DiscordBot bawt, char[] id, char[] name, char[] icon, bool owner, int permissions, const bool listen) +{ + Bot.GetGuildChannels(id, ChannelList, INVALID_FUNCTION, listen); +} + +public void ChannelList(DiscordBot bawt, const char[] guild, DiscordChannel Channel, const bool listen) +{ + if(StrEqual(g_sBotToken, "") || StrEqual(g_sVerificationChannelID, "")) + { + return; + } + if(Channel == null || Bot == null) + { + return; + } + if(Bot.IsListeningToChannel(Channel)) + { + //Bot.StopListeningToChannel(Channel); + return; + } + + + char id[20], name[32]; + Channel.GetID(id, sizeof(id)); + Channel.GetName(name, sizeof(name)); + + if(strlen(g_sVerificationChannelID) > 10) + { + if(StrEqual(id, g_sVerificationChannelID)) + { + g_sVerificationChannelName = name; + if(listen) + { + PrintToServer("******** STARTING TO LISTEN ***********"); + Bot.StartListeningToChannel(Channel, OnMessageReceived); + //Bot.PrintChannels(); + //Bot.PrintChannels(); + } + } + } +} + +/* +public Action smpc(int client, int args) +{ + Bot.PrintChannels(); +} +*/ + +public Action Command_ViewId(int client, int args) +{ + if(!client || StrEqual(g_sVerificationChannelID, "")) + { + return Plugin_Handled; + } + if(!g_bChecked[client]) + { + CReplyToCommand(client, "%s %T", g_sServerPrefix, "TryAgainLater", client); + return Plugin_Handled; + } + + //CPrintToChat(client, "%s %T", g_sServerPrefix, "LinkYourID", client, g_sUniqueCode[client]); + if(!g_bMember[client]) + { + CPrintToChat(client, "%s %T", g_sServerPrefix, "LinkConnect", client); + CPrintToChat(client, "%s {blue}%s", g_sServerPrefix, g_sInviteLink); + + CPrintToChat(client, "%s %T", g_sServerPrefix, "LinkUsage", client, g_sLinkCommand, g_sUniqueCode[client], g_sVerificationChannelName); + CPrintToChat(client, "%s %T", g_sServerPrefix, "CopyPasteFromConsole", client); + + char buf[128], g_sServerPrefix2[128]; + Format(g_sServerPrefix2, sizeof(g_sServerPrefix2), g_sServerPrefix); + for(int i = 0; i < sizeof(C_Tag); i++) + { + ReplaceString(g_sServerPrefix2, sizeof(g_sServerPrefix2), C_Tag[i], ""); + } + + PrintToConsole(client, "*****************************************************"); + PrintToConsole(client, "%s %T", g_sServerPrefix2, "LinkConnect", client, g_sInviteLink); + PrintToConsole(client, "%s %s", g_sServerPrefix2, g_sInviteLink); + Format(buf, sizeof(buf), "%T", "LinkUsage", client, g_sLinkCommand, g_sUniqueCode[client], g_sVerificationChannelName); + for(int i = 0; i < sizeof(C_Tag); i++) + { + ReplaceString(buf, sizeof(buf), C_Tag[i], ""); + } + PrintToConsole(client, "%s %s", g_sServerPrefix2, buf); + PrintToConsole(client, "*****************************************************"); + } + else + { + CPrintToChat(client, "%s - %T", g_sServerPrefix, "AlreadyVerified"); + CPrintToChat(client, "%s - %T", g_sServerPrefix, "CanChange"); + } + + + + return Plugin_Handled; +} + +public Action Check(int client, const char[] command, int args) +{ + if(!client || client > MaxClients) + { + return Plugin_Continue; + } + if(!g_bMember[client]) + { + CPrintToChat(client, "%s %T", g_sServerPrefix, "MustVerify", client, ChangePartsInString(g_sViewIDCommand, "sm_", "!")); + return Plugin_Stop; + } + return Plugin_Continue; +} diff --git a/include/discord_utilities/globals.sp b/scripting/discord_utilities/globals.sp similarity index 96% rename from include/discord_utilities/globals.sp rename to scripting/discord_utilities/globals.sp index f9d5e6d..9aef28d 100644 --- a/include/discord_utilities/globals.sp +++ b/scripting/discord_utilities/globals.sp @@ -1,49 +1,49 @@ -#define PLUGIN_VERSION "2.4-betafixslow" - -#define PLUGIN_NAME "Discord Utilities" -#define PLUGIN_AUTHOR "Cruze" -#define PLUGIN_DESC "Utilities that can be used to integrate gameserver to discord server I guess?" -#define PLUGIN_URL "https://github.com/Cruze03/discord-utilities | http://www.steamcommunity.com/profiles/76561198132924835" - -//#define USE_AutoExecConfig - - -ConVar g_cVerificationChannelID, g_cGuildID, g_cRoleID; -ConVar g_cBotToken, g_cCheckInterval, g_cUseSWGM, g_cServerID; -ConVar g_cLinkCommand, g_cViewIDCommand, g_cInviteLink; -ConVar g_cDiscordPrefix, g_cServerPrefix; -ConVar g_cDatabaseName, g_cTableName, g_cPruneDays; -ConVar g_cPrimaryServer; - -char g_sVerificationChannelID[20], g_sGuildID[20], g_sRoleID[20]; -char g_sBotToken[60]; -char g_sLinkCommand[20], g_sViewIDCommand[20], g_sInviteLink[30]; -char g_sDiscordPrefix[128], g_sServerPrefix[128]; -char g_sTableName[32]; - -char g_sVerificationChannelName[32]; - -bool g_bShavit; - -bool g_bChecked[MAXPLAYERS+1]; -bool g_bMember[MAXPLAYERS+1]; -bool g_bRoleGiven[MAXPLAYERS+1]; -char g_sUserID[MAXPLAYERS+1][20]; -char g_sUniqueCode[MAXPLAYERS+1][36]; - -Handle g_hOnCheckedAccounts, g_hOnLinkedAccount, g_hOnAccountRevoked, g_hOnMemberDataDumped; - -DiscordBot Bot; - -Database g_hDB; - -bool g_bIsMySQl; - -bool g_bLateLoad = false; -//bool g_bIsBotLoaded = false; - -Handle hRateLimit = null; -Handle hRateReset = null; -Handle hRateLeft = null; - +#define PLUGIN_VERSION "2.4-betafixslow" + +#define PLUGIN_NAME "Discord Utilities" +#define PLUGIN_AUTHOR "Cruze" +#define PLUGIN_DESC "Utilities that can be used to integrate gameserver to discord server I guess?" +#define PLUGIN_URL "https://github.com/Cruze03/discord-utilities | http://www.steamcommunity.com/profiles/76561198132924835" + +//#define USE_AutoExecConfig + + +ConVar g_cVerificationChannelID, g_cGuildID, g_cRoleID; +ConVar g_cBotToken, g_cCheckInterval, g_cUseSWGM, g_cServerID; +ConVar g_cLinkCommand, g_cViewIDCommand, g_cInviteLink; +ConVar g_cDiscordPrefix, g_cServerPrefix; +ConVar g_cDatabaseName, g_cTableName, g_cPruneDays; +ConVar g_cPrimaryServer; + +char g_sVerificationChannelID[20], g_sGuildID[20], g_sRoleID[20]; +char g_sBotToken[60]; +char g_sLinkCommand[20], g_sViewIDCommand[20], g_sInviteLink[30]; +char g_sDiscordPrefix[128], g_sServerPrefix[128]; +char g_sTableName[32]; + +char g_sVerificationChannelName[32]; + +bool g_bShavit; + +bool g_bChecked[MAXPLAYERS+1]; +bool g_bMember[MAXPLAYERS+1]; +bool g_bRoleGiven[MAXPLAYERS+1]; +char g_sUserID[MAXPLAYERS+1][20]; +char g_sUniqueCode[MAXPLAYERS+1][36]; + +Handle g_hOnCheckedAccounts, g_hOnLinkedAccount, g_hOnAccountRevoked, g_hOnMemberDataDumped; + +DiscordBot Bot; + +Database g_hDB; + +bool g_bIsMySQl; + +bool g_bLateLoad = false; +//bool g_bIsBotLoaded = false; + +Handle hRateLimit = null; +Handle hRateReset = null; +Handle hRateLeft = null; + Handle hFinalMemberList; \ No newline at end of file diff --git a/include/discord_utilities/helpers.sp b/scripting/discord_utilities/helpers.sp similarity index 96% rename from include/discord_utilities/helpers.sp rename to scripting/discord_utilities/helpers.sp index 532b629..cbd5301 100644 --- a/include/discord_utilities/helpers.sp +++ b/scripting/discord_utilities/helpers.sp @@ -1,680 +1,680 @@ -void AccountsCheck() -{ - Action action = Plugin_Continue; - Call_StartForward(g_hOnCheckedAccounts); - Call_PushString(g_sBotToken); - Call_PushString(g_sGuildID); - Call_PushString(g_sTableName); - Call_Finish(action); - - if(action >= Plugin_Handled) - { - return; - } - /* - if(g_hDB == null) - { - CreateTimer(5.0, Timer_Query_AccountCheck, _, TIMER_FLAG_NO_MAPCHANGE); - } - else - { - char Query[256]; - g_hDB.Format(Query, sizeof(Query), "SELECT userid FROM %s", g_sTableName); - SQL_TQuery(g_hDB, SQLQuery_AccountCheck, Query); - } - */ - delete hFinalMemberList; - Handle hData = json_object(); - hFinalMemberList = json_array(); - json_object_set_new(hData, "limit", json_integer(1000)); - json_object_set_new(hData, "after", json_string("")); - GetMembers(hData); - //PrintToChatAll("AccountsCheck();"); -} - -void GetMembers(Handle hData = INVALID_HANDLE) -{ - if(StrEqual(g_sGuildID, "") && !StrEqual(g_sVerificationChannelID, "")) - { - LogError("[Discord-Utilities] GuildID is not provided. GetMember won't work!"); - delete hData; - delete hFinalMemberList; - return; - } - if(Bot == null) - { - delete hData; - delete hFinalMemberList; - return; - } - int limit = JsonObjectGetInt(hData, "limit"); - char after[32]; - JsonObjectGetString(hData, "after", after, sizeof(after)); - - char url[256]; - if(StrEqual(after, "")) - { - FormatEx(url, sizeof(url), "https://discord.com/api/guilds/%s/members?limit=%i", g_sGuildID, limit); - } - else - { - FormatEx(url, sizeof(url), "https://discord.com/api/guilds/%s/members?limit=%i&after=%s", g_sGuildID, limit, after); - } - - char route[128]; - FormatEx(route, sizeof(route), "guild/%s/members", g_sGuildID); - - DiscordRequest request = new DiscordRequest(url, k_EHTTPMethodGET); - if(request == null) - { - CreateTimer(2.0, SendGetMembers, hData); - return; - } - request.SetCallbacks(HTTPCompleted, MembersDataReceive); - request.SetBot(Bot); - request.SetData(hData, route); - request.Send(route); -} - -public int HTTPCompleted(Handle request, bool failure, bool requestSuccessful, EHTTPStatusCode statuscode, any data, any data2) -{ -} - -public void MembersDataReceive(Handle request, bool failure, int offset, int statuscode, any dp) -{ - if(failure || (statuscode != 200)) - { - if(statuscode == 429 || statuscode == 500) - { - GetMembers(dp); - //delete view_as(dp); - delete request; - return; - } - delete hFinalMemberList; - delete request; - delete view_as(dp); - return; - } - SteamWorks_GetHTTPResponseBodyCallback(request, GetMembersData, dp); - delete request; -} - -public int GetMembersData(const char[] data, any dp) -{ - //PrintToChatAll("GetMembersData();"); - Handle hJson = json_load(data); - //bool returned = json_array_extend(hFinalMemberList, hJson); - json_array_extend(hFinalMemberList, hJson); - //PrintToChatAll("returned %d", returned); - Handle hData = view_as(dp); - - int size = json_array_size(hJson); - int limit = JsonObjectGetInt(hData, "limit"); - //PrintToChatAll("size %d | limit %d", size, limit); - - if(limit == size) - { - char userid[32]; - DiscordGuildUser GuildUser; - DiscordUser user; - - GuildUser = view_as(json_array_get(hJson, limit - 1)); - user = GuildUser.GetUser(); - user.GetID(userid, sizeof(userid)); - delete GuildUser; - delete user; - //PrintToChatAll("userID %s", userid); - - delete hJson; - - json_object_set_new(hData, "after", json_string(userid)); - GetMembers(hData); - return; - } - - OnGetMembersAll(hFinalMemberList); - - delete hJson; - delete hData; - delete hFinalMemberList; -} - -public void OnGetMembersAll(Handle hMemberList) -{ - //DeleteFile("addons/sourcemod/logs/dsmembers.json") - json_dump_file(hMemberList, "addons/sourcemod/logs/dsmembers.json"); - //LogToFile("addons/sourcemod/logs/dsmembers.json", "OnGetMembersAll size %d", json_array_size(hMemberList)); - - Call_StartForward(g_hOnMemberDataDumped); - Call_Finish(); - - char userid[20]; - DiscordGuildUser GuildUser; - DiscordUser user; - bool found; - char Query[256]; - bool[] bUpdate = new bool[MaxClients+1]; - - for(int x = 1; x <= MaxClients; x++) - { - if(!IsClientInGame(x)) - { - continue; - } - if(!g_bMember[x]) - { - continue; - } - found = false; - for(int i = 0; i < json_array_size(hMemberList); i++) - { - GuildUser = view_as(json_array_get(hMemberList, i)); - user = GuildUser.GetUser(); - user.GetID(userid, sizeof(userid)); - if(strcmp(userid, g_sUserID[x]) == 0) - { - found = true; - delete user; - delete GuildUser; - break; - } - delete user; - delete GuildUser; - } - delete user; - delete GuildUser; - if(!found) - { - char steamid[32]; - GetClientAuthId(x, AuthId_Steam2, steamid, sizeof(steamid)); - if(g_bIsMySQl) - { - g_hDB.Format(Query, sizeof(Query), "UPDATE `%s` SET `userid` = '%s', member = '0' WHERE `steamid` = '%s';", g_sTableName, NULL_STRING, steamid); - } - else - { - g_hDB.Format(Query, sizeof(Query), "UPDATE %s SET userid = '%s', member = '0' WHERE steamid = '%s';", g_sTableName, NULL_STRING, steamid); - } - SQL_TQuery(g_hDB, SQLQuery_UpdatePlayer, Query); - bUpdate[x] = true; - CPrintToChat(x, "%s %T", g_sServerPrefix, "DiscordRevoked", x); - - LogToFile("addons/sourcemod/logs/dsmembers_revoke.log", "Player %L got revoked. Memberlist json size: %d", x, json_array_size(hMemberList)); - - Call_StartForward(g_hOnAccountRevoked); - Call_PushCell(x); - Call_PushString(g_sUserID[x]); - Call_Finish(); - } - else - { - if(strlen(g_sRoleID) > 5 && !g_bRoleGiven[x]) - { - ManagingRole(g_sUserID[x], g_sRoleID, k_EHTTPMethodPUT); - g_bRoleGiven[x] = true; - } - } - } - - for(int i = 1; i <= MaxClients; i++) - { - if(!IsClientInGame(i)) - { - continue; - } - if(!bUpdate[i]) - { - continue; - } - OnClientPutInServer(i); - OnClientPreAdminCheck(i); - } -} - -/* -void GetGuildMember(char[] userid) -{ - Handle hData = json_object(); - json_object_set_new(hData, "userID", json_string(userid[0])); - GetMembers(hData); -} -*/ - -void UpdatePlayer(int client) -{ - char steamid[32], szQuery[512]; - - GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); - if(g_bIsMySQl) - { - g_hDB.Format(szQuery, sizeof(szQuery), "UPDATE `%s` SET last_accountuse = '%d' WHERE `steamid` = '%s';", g_sTableName, GetTime(), steamid); - } - else - { - g_hDB.Format(szQuery, sizeof(szQuery), "UPDATE %s SET last_accountuse = '%d' WHERE steamid = '%s'", g_sTableName, GetTime(), steamid); - } - SQL_TQuery(g_hDB, SQLQuery_UpdatePlayer, szQuery, GetClientUserId(client)); -} - -stock void RemoveColors(char[] text, int size) -{ - if(g_bShavit) - { - for(int i = 0; i < sizeof(gS_GlobalColorNames); i++) - { - ReplaceString(text, size, gS_GlobalColorNames[i], ""); - } - for(int i = 0; i < sizeof(gS_GlobalColors); i++) - { - ReplaceString(text, size, gS_GlobalColors[i], ""); - } - for(int i = 0; i < sizeof(gS_CSGOColorNames); i++) - { - ReplaceString(text, size, gS_CSGOColorNames[i], ""); - } - for(int i = 0; i < sizeof(gS_CSGOColors); i++) - { - ReplaceString(text, size, gS_CSGOColors[i], ""); - } - } - else - { - for(int i = 0; i < sizeof(C_Tag); i++) - { - ReplaceString(text, size, C_Tag[i], ""); - } - for(int i = 0; i < sizeof(C_TagCode); i++) - { - ReplaceString(text, size, C_TagCode[i], ""); - } - } -} - -void CreateCvars() -{ - #if defined USE_AutoExecConfig - AutoExecConfig_SetFile("Discord-Utilities"); - AutoExecConfig_SetCreateFile(true); - - g_cVerificationChannelID = AutoExecConfig_CreateConVar("sm_du_verfication_channelid", "", "Channel ID for verfication. Blank to disable."); - g_cGuildID = AutoExecConfig_CreateConVar("sm_du_verification_guildid", "", "Guild ID of your discord server. Blank to disable. Needed for verification module."); - g_cRoleID = AutoExecConfig_CreateConVar("sm_du_verification_roleid", "", "Role ID to give to user when user is verified. Blank to give no role. Verification module needs to be running."); - - g_cBotToken = AutoExecConfig_CreateConVar("sm_du_bottoken", "", "Bot Token. Needed for discord server => gameserver and/or verification module.", FCVAR_PROTECTED); - g_cCheckInterval = AutoExecConfig_CreateConVar("sm_du_accounts_check_interval", "300", "Time in seconds between verifying accounts."); - g_cUseSWGM = AutoExecConfig_CreateConVar("sm_du_use_swgm_file", "0", "Use SWGM config file for restricting commands."); - g_cServerID = AutoExecConfig_CreateConVar("sm_du_server_id", "1", "Increase this with every server you put this plugin in. Prevents multiple replies from the bot in verfication channel."); - g_cPrimaryServer = AutoExecConfig_CreateConVar("sm_du_server_primary", "0", "Is this the primary server in the verification channel? Only this server will respond to generic queries so atleast 1 server should have this 1."); - - g_cLinkCommand = AutoExecConfig_CreateConVar("sm_du_link_command", "!link", "Command to use in text channel."); - g_cViewIDCommand = AutoExecConfig_CreateConVar("sm_du_viewid_command", "sm_viewid", "Command to view id."); - g_cInviteLink = AutoExecConfig_CreateConVar("sm_du_link", "https://discord.gg/83g5xcE", "Invite link of your discord server."); - - g_cDiscordPrefix = AutoExecConfig_CreateConVar("sm_du_discord_prefix", "[{lightgreen}Discord{default}]", "Prefix for discord messages."); - g_cServerPrefix = AutoExecConfig_CreateConVar("sm_du_server_prefix", "[{lightgreen}Discord-Utilities{default}]", "Prefix for chat messages."); - - g_cDatabaseName = AutoExecConfig_CreateConVar("sm_du_database_name", "du", "Section name in databases.cfg."); - g_cTableName = AutoExecConfig_CreateConVar("sm_du_table_name", "du_users", "Table Name."); - g_cPruneDays = AutoExecConfig_CreateConVar("sm_du_prune_days", "60", "Prune database with players whose last connect is X DAYS and he is not member of discord server. 0 to disable."); - - AutoExecConfig_ExecuteFile(); - AutoExecConfig_CleanFile(); - - #else - g_cVerificationChannelID = CreateConVar("sm_du_verfication_channelid", "", "Channel ID for verfication. Blank to disable."); - g_cGuildID = CreateConVar("sm_du_verification_guildid", "", "Guild ID of your discord server. Blank to disable. Needed for verification module."); - g_cRoleID = CreateConVar("sm_du_verification_roleid", "", "Role ID to give to user when user is verified. Blank to give no role. Verification module needs to be running."); - - g_cBotToken = CreateConVar("sm_du_bottoken", "", "Bot Token. Needed for discord server => gameserver and/or verification module.", FCVAR_PROTECTED); - g_cCheckInterval = CreateConVar("sm_du_accounts_check_interval", "300", "Time in seconds between verifying accounts."); - g_cUseSWGM = CreateConVar("sm_du_use_swgm_file", "0", "Use SWGM config file for restricting commands."); - g_cServerID = CreateConVar("sm_du_server_id", "1", "Increase this with every server you put this plugin in. Prevents multiple replies from the bot in verfication channel."); - g_cPrimaryServer = CreateConVar("sm_du_server_primary", "0", "Is this the primary server in the verification channel? Only this server will respond to generic queries so atleast 1 server should have this 1."); - - g_cLinkCommand = CreateConVar("sm_du_link_command", "!link", "Command to use in text channel."); - g_cViewIDCommand = CreateConVar("sm_du_viewid_command", "sm_viewid", "Command to view id."); - g_cInviteLink = CreateConVar("sm_du_link", "https://discord.gg/83g5xcE", "Invite link of your discord server."); - - g_cDiscordPrefix = CreateConVar("sm_du_discord_prefix", "[{lightgreen}Discord{default}]", "Prefix for discord messages."); - g_cServerPrefix = CreateConVar("sm_du_server_prefix", "[{lightgreen}Discord-Utilities{default}]", "Prefix for chat messages."); - - g_cDatabaseName = CreateConVar("sm_du_database_name", "du", "Section name in databases.cfg."); - g_cTableName = CreateConVar("sm_du_table_name", "du_users", "Table Name."); - g_cPruneDays = CreateConVar("sm_du_prune_days", "60", "Prune database with players whose last connect is X DAYS and he is not member of discord server. 0 to disable."); - - AutoExecConfig(true, "Discord-Utilities"); - #endif - - HookConVarChange(g_cVerificationChannelID, OnSettingsChanged); - HookConVarChange(g_cGuildID, OnSettingsChanged); - HookConVarChange(g_cRoleID, OnSettingsChanged); - - HookConVarChange(g_cBotToken, OnSettingsChanged); - - HookConVarChange(g_cLinkCommand, OnSettingsChanged); - HookConVarChange(g_cViewIDCommand, OnSettingsChanged); - HookConVarChange(g_cInviteLink, OnSettingsChanged); - - HookConVarChange(g_cDiscordPrefix, OnSettingsChanged); - HookConVarChange(g_cServerPrefix, OnSettingsChanged); -} - -void LoadCvars() -{ - g_cVerificationChannelID.GetString(g_sVerificationChannelID, sizeof(g_sVerificationChannelID)); - g_cGuildID.GetString(g_sGuildID, sizeof(g_sGuildID)); - g_cRoleID.GetString(g_sRoleID, sizeof(g_sRoleID)); - - g_cBotToken.GetString(g_sBotToken, sizeof(g_sBotToken)); - - g_cLinkCommand.GetString(g_sLinkCommand, sizeof(g_sLinkCommand)); - g_cViewIDCommand.GetString(g_sViewIDCommand, sizeof(g_sViewIDCommand)); - g_cInviteLink.GetString(g_sInviteLink, sizeof(g_sInviteLink)); - - g_cDiscordPrefix.GetString(g_sDiscordPrefix, sizeof(g_sDiscordPrefix)); - g_cServerPrefix.GetString(g_sServerPrefix, sizeof(g_sServerPrefix)); -} - -void ManagingRole(char[] userid, char[] roleid, EHTTPMethod method) -{ - Handle hData = json_object(); - json_object_set_new(hData, "userid", json_string(userid)); - json_object_set_new(hData, "roleid", json_string(roleid)); - json_object_set_new(hData, "method", json_integer(view_as(method))); - ManageRole(hData); -} - -void ManageRole(Handle hData) -{ - if(StrEqual(g_sGuildID, "")) - { - LogError("[Discord-Utilities] GuildID is not provided. Role cannot be provided!"); - delete hData; - return; - } - char userid[128]; - if (!JsonObjectGetString(hData, "userid", userid, sizeof(userid))) - { - LogError("JsonObjectGetString \"userid\" failed"); - delete hData; - return; - } - char roleid[128]; - if (!JsonObjectGetString(hData, "roleid", roleid, sizeof(roleid))) - { - LogError("JsonObjectGetString \"roleid\" failed"); - delete hData; - return; - } - EHTTPMethod method = view_as(JsonObjectGetInt(hData, "method")); - char url[1024]; - FormatEx(url, sizeof(url), "https://discord.com/api/guilds/%s/members/%s/roles/%s", g_sGuildID, userid, roleid); - char route[512]; - FormatEx(route, sizeof(route), "guild/%s/members", g_sGuildID); - DiscordRequest request = new DiscordRequest(url, method); - if (request == null) - { - CreateTimer(2.0, SendManageRole, hData, TIMER_FLAG_NO_MAPCHANGE); - return; - } - request.SetCallbacks(HTTPCompleted, OnManageRoleSent); - request.SetContentSize(); - request.SetBot(Bot); - request.SetData(hData, route); - request.Send(route); -} - - -public void OnManageRoleSent(Handle request, bool failure, int offset, int statuscode, any dp) -{ - if(failure || (statuscode != 200)) - { - if(statuscode == 429 || statuscode == 500) - { - ManageRole(dp); - //delete view_as(dp); - delete request; - //LogError("OnManageRoleSent: Error code %d | Retrying to Managerole(dp)", statuscode); - return; - } - delete request; - delete view_as(dp); - //LogError("OnManageRoleSent: Error code %d | Deleting everything and returning", statuscode); - return; - } - delete request - delete view_as(dp); -} - - -stock void RefreshClients() -{ - for(int i = 1; i <= MaxClients; i++) if(IsClientInGame(i)) - { - OnClientPreAdminCheck(i); - } -} - -void LoadCommands() -{ - char sBuffer[256]; - if(g_cUseSWGM.IntValue == 1) - { - KeyValues kv = new KeyValues("Command_Listener"); - BuildPath(Path_SM, sBuffer, sizeof(sBuffer), "configs/swgm/command_listener.ini"); - if(!FileToKeyValues(kv, sBuffer)) - { - SetFailState("[Discord-Utilities] Missing config file %s. If you don't use SWGM, then change 'sm_du_use_swgm_file' value to 0.", sBuffer); - } - if(kv.GotoFirstSubKey()) - { - do - { - if(kv.GetSectionName(sBuffer, sizeof(sBuffer))) - { - AddCommandListener(Check, sBuffer); - } - } - while (kv.GotoNextKey()); - } - delete kv; - return; - } - BuildPath(Path_SM, sBuffer, sizeof(sBuffer), "configs/du/command_listener.ini"); - - File fFile = OpenFile(sBuffer, "r"); - - if(!FileExists(sBuffer)) - { - fFile.Close(); - fFile = OpenFile(sBuffer, "w+"); - fFile.WriteLine("// Separate each commands with separate lines. DON'T USE SPACE INFRONT OF COMMANDS. Example:"); - fFile.WriteLine("//sm_shop"); - fFile.WriteLine("//sm_store"); - fFile.WriteLine("//Use it without \"//\""); - fFile.Close(); - LogError("[Discord-Utilities] %s file is empty. Add commands to restrict them!", sBuffer); - return; - } - char sReadBuffer[PLATFORM_MAX_PATH]; - - int len; - while(!fFile.EndOfFile() && fFile.ReadLine(sReadBuffer, sizeof(sReadBuffer))) - { - if (sReadBuffer[0] == '/' && sReadBuffer[1] == '/' || IsCharSpace(sReadBuffer[0])) - { - continue; - } - - ReplaceString(sReadBuffer, sizeof(sReadBuffer), "\n", ""); - ReplaceString(sReadBuffer, sizeof(sReadBuffer), "\r", ""); - ReplaceString(sReadBuffer, sizeof(sReadBuffer), "\t", ""); - - len = strlen(sReadBuffer); - - if (len < 3) - { - continue; - } - - AddCommandListener(Check, sReadBuffer); - } - - fFile.Close(); -} - -stock void CreateBot(bool guilds = true, bool listen = true) -{ - if(StrEqual(g_sBotToken, "") || StrEqual(g_sVerificationChannelID, "")) - { - delete Bot; - return; - } - - delete Bot; - - //if(!g_bIsBotLoaded) - //{ - Bot = new DiscordBot(g_sBotToken); - //} - - if(guilds) - { - //if(g_bIsBotLoaded) - //{ - Bot.GetGuilds(GuildList, _, listen); - //} - //else - //{ - // CreateBot(); - //} - } -} - -stock void KillBot() -{ - if(Bot) - { - Bot.StopListeningToChannels(); - Bot.StopListening(); - } - delete Bot; -} - -stock int GetClientFromUniqueCode(const char[] unique) -{ - for(int i = 1; i <= MaxClients; i++) - { - if (!IsClientInGame(i)) continue; - if (StrEqual(g_sUniqueCode[i], unique)) return i; - } - return -1; -} - -stock char ChangePartsInString(char[] input, const char[] from, const char[] to) -{ - char output[64]; - ReplaceString(input, sizeof(output), from, to); - strcopy(output, sizeof(output), input); - return output; -} - -/* -stock void GetGuilds(bool listen = true) -{ - Bot.GetGuilds(GuildList, _, listen); -} -*/ - -stock void Discord_EscapeString(char[] string, int maxlen, bool name = false) -{ - if(name) - { - ReplaceString(string, maxlen, "everyone", "everyone"); - ReplaceString(string, maxlen, "here", "here"); - ReplaceString(string, maxlen, "discordtag", "discordtag"); - } - ReplaceString(string, maxlen, "#", "#"); - ReplaceString(string, maxlen, "@", "@"); - //ReplaceString(string, maxlen, ":", ""); - ReplaceString(string, maxlen, "_", "ˍ"); - ReplaceString(string, maxlen, "'", "'"); - ReplaceString(string, maxlen, "`", "'"); - ReplaceString(string, maxlen, "~", "∽"); - ReplaceString(string, maxlen, "\"", """); -} - -/* TIMERS */ - -public Action VerifyAccounts(Handle timer) -{ - AccountsCheck(); -} - -public Action Cmd_Unlink(int client, int args) -{ - if(g_bMember[client]) - { - char Query[256]; - - char szSteamId[32]; - GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); - - g_hDB.Format(Query, sizeof(Query), "SELECT userid FROM %s WHERE steamid = '%s'", g_sTableName, szSteamId); - SQL_TQuery(g_hDB, SQLQuery_UnlinkAccount, Query, GetClientUserId(client)); - } -} -/* -public Action Timer_Query_AccountCheck(Handle timer) -{ - if(g_hDB == null) - { - CreateTimer(5.0, Timer_Query_AccountCheck, _, TIMER_FLAG_NO_MAPCHANGE); - return; - } - char Query[256]; - g_hDB.Format(Query, sizeof(Query), "SELECT userid FROM %s", g_sTableName); - SQL_TQuery(g_hDB, SQLQuery_AccountCheck, Query); -} -*/ - -public Action SendGetMembers(Handle timer, any data) -{ - GetMembers(view_as(data)); -} - -public Action SendManageRole(Handle timer, any data) -{ - ManageRole(view_as(data)); -} - -public Action SendRequestAgain(Handle timer, DataPack dp) -{ - ResetPack(dp, false); - Handle request = ReadPackCell(dp); - char route[512]; - ReadPackString(dp, route, sizeof(route)); - delete dp; - DiscordSendRequest(request, route); -} - -public Action Timer_RefreshClients(Handle timer) -{ - RefreshClients(); -} - -stock void DU_DeleteMessageID(DiscordMessage discordmessage) -{ - char channelid[64], msgid[64]; - - discordmessage.GetChannelID(channelid, sizeof(channelid)); - discordmessage.GetID(msgid, sizeof(msgid)); - - Bot.DeleteMessageID(channelid, msgid); -} - -stock bool IsClientValid(int client) -{ - return (0 < client <= MaxClients) && IsClientInGame(client) && !IsFakeClient(client); -} +void AccountsCheck() +{ + Action action = Plugin_Continue; + Call_StartForward(g_hOnCheckedAccounts); + Call_PushString(g_sBotToken); + Call_PushString(g_sGuildID); + Call_PushString(g_sTableName); + Call_Finish(action); + + if(action >= Plugin_Handled) + { + return; + } + /* + if(g_hDB == null) + { + CreateTimer(5.0, Timer_Query_AccountCheck, _, TIMER_FLAG_NO_MAPCHANGE); + } + else + { + char Query[256]; + g_hDB.Format(Query, sizeof(Query), "SELECT userid FROM %s", g_sTableName); + SQL_TQuery(g_hDB, SQLQuery_AccountCheck, Query); + } + */ + delete hFinalMemberList; + Handle hData = json_object(); + hFinalMemberList = json_array(); + json_object_set_new(hData, "limit", json_integer(1000)); + json_object_set_new(hData, "after", json_string("")); + GetMembers(hData); + //PrintToChatAll("AccountsCheck();"); +} + +void GetMembers(Handle hData = INVALID_HANDLE) +{ + if(StrEqual(g_sGuildID, "") && !StrEqual(g_sVerificationChannelID, "")) + { + LogError("[Discord-Utilities] GuildID is not provided. GetMember won't work!"); + delete hData; + delete hFinalMemberList; + return; + } + if(Bot == null) + { + delete hData; + delete hFinalMemberList; + return; + } + int limit = JsonObjectGetInt(hData, "limit"); + char after[32]; + JsonObjectGetString(hData, "after", after, sizeof(after)); + + char url[256]; + if(StrEqual(after, "")) + { + FormatEx(url, sizeof(url), "https://discord.com/api/guilds/%s/members?limit=%i", g_sGuildID, limit); + } + else + { + FormatEx(url, sizeof(url), "https://discord.com/api/guilds/%s/members?limit=%i&after=%s", g_sGuildID, limit, after); + } + + char route[128]; + FormatEx(route, sizeof(route), "guild/%s/members", g_sGuildID); + + DiscordRequest request = new DiscordRequest(url, k_EHTTPMethodGET); + if(request == null) + { + CreateTimer(2.0, SendGetMembers, hData); + return; + } + request.SetCallbacks(HTTPCompleted, MembersDataReceive); + request.SetBot(Bot); + request.SetData(hData, route); + request.Send(route); +} + +public int HTTPCompleted(Handle request, bool failure, bool requestSuccessful, EHTTPStatusCode statuscode, any data, any data2) +{ +} + +public void MembersDataReceive(Handle request, bool failure, int offset, int statuscode, any dp) +{ + if(failure || (statuscode != 200)) + { + if(statuscode == 429 || statuscode == 500) + { + GetMembers(dp); + //delete view_as(dp); + delete request; + return; + } + delete hFinalMemberList; + delete request; + delete view_as(dp); + return; + } + SteamWorks_GetHTTPResponseBodyCallback(request, GetMembersData, dp); + delete request; +} + +public int GetMembersData(const char[] data, any dp) +{ + //PrintToChatAll("GetMembersData();"); + Handle hJson = json_load(data); + //bool returned = json_array_extend(hFinalMemberList, hJson); + json_array_extend(hFinalMemberList, hJson); + //PrintToChatAll("returned %d", returned); + Handle hData = view_as(dp); + + int size = json_array_size(hJson); + int limit = JsonObjectGetInt(hData, "limit"); + //PrintToChatAll("size %d | limit %d", size, limit); + + if(limit == size) + { + char userid[32]; + DiscordGuildUser GuildUser; + DiscordUser user; + + GuildUser = view_as(json_array_get(hJson, limit - 1)); + user = GuildUser.GetUser(); + user.GetID(userid, sizeof(userid)); + delete GuildUser; + delete user; + //PrintToChatAll("userID %s", userid); + + delete hJson; + + json_object_set_new(hData, "after", json_string(userid)); + GetMembers(hData); + return; + } + + OnGetMembersAll(hFinalMemberList); + + delete hJson; + delete hData; + delete hFinalMemberList; +} + +public void OnGetMembersAll(Handle hMemberList) +{ + //DeleteFile("addons/sourcemod/logs/dsmembers.json") + json_dump_file(hMemberList, "addons/sourcemod/logs/dsmembers.json"); + //LogToFile("addons/sourcemod/logs/dsmembers.json", "OnGetMembersAll size %d", json_array_size(hMemberList)); + + Call_StartForward(g_hOnMemberDataDumped); + Call_Finish(); + + char userid[20]; + DiscordGuildUser GuildUser; + DiscordUser user; + bool found; + char Query[256]; + bool[] bUpdate = new bool[MaxClients+1]; + + for(int x = 1; x <= MaxClients; x++) + { + if(!IsClientInGame(x)) + { + continue; + } + if(!g_bMember[x]) + { + continue; + } + found = false; + for(int i = 0; i < json_array_size(hMemberList); i++) + { + GuildUser = view_as(json_array_get(hMemberList, i)); + user = GuildUser.GetUser(); + user.GetID(userid, sizeof(userid)); + if(strcmp(userid, g_sUserID[x]) == 0) + { + found = true; + delete user; + delete GuildUser; + break; + } + delete user; + delete GuildUser; + } + delete user; + delete GuildUser; + if(!found) + { + char steamid[32]; + GetClientAuthId(x, AuthId_Steam2, steamid, sizeof(steamid)); + if(g_bIsMySQl) + { + g_hDB.Format(Query, sizeof(Query), "UPDATE `%s` SET `userid` = '%s', member = '0' WHERE `steamid` = '%s';", g_sTableName, NULL_STRING, steamid); + } + else + { + g_hDB.Format(Query, sizeof(Query), "UPDATE %s SET userid = '%s', member = '0' WHERE steamid = '%s';", g_sTableName, NULL_STRING, steamid); + } + SQL_TQuery(g_hDB, SQLQuery_UpdatePlayer, Query); + bUpdate[x] = true; + CPrintToChat(x, "%s %T", g_sServerPrefix, "DiscordRevoked", x); + + LogToFile("addons/sourcemod/logs/dsmembers_revoke.log", "Player %L got revoked. Memberlist json size: %d", x, json_array_size(hMemberList)); + + Call_StartForward(g_hOnAccountRevoked); + Call_PushCell(x); + Call_PushString(g_sUserID[x]); + Call_Finish(); + } + else + { + if(strlen(g_sRoleID) > 5 && !g_bRoleGiven[x]) + { + ManagingRole(g_sUserID[x], g_sRoleID, k_EHTTPMethodPUT); + g_bRoleGiven[x] = true; + } + } + } + + for(int i = 1; i <= MaxClients; i++) + { + if(!IsClientInGame(i)) + { + continue; + } + if(!bUpdate[i]) + { + continue; + } + OnClientPutInServer(i); + OnClientPreAdminCheck(i); + } +} + +/* +void GetGuildMember(char[] userid) +{ + Handle hData = json_object(); + json_object_set_new(hData, "userID", json_string(userid[0])); + GetMembers(hData); +} +*/ + +void UpdatePlayer(int client) +{ + char steamid[32], szQuery[512]; + + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + if(g_bIsMySQl) + { + g_hDB.Format(szQuery, sizeof(szQuery), "UPDATE `%s` SET last_accountuse = '%d' WHERE `steamid` = '%s';", g_sTableName, GetTime(), steamid); + } + else + { + g_hDB.Format(szQuery, sizeof(szQuery), "UPDATE %s SET last_accountuse = '%d' WHERE steamid = '%s'", g_sTableName, GetTime(), steamid); + } + SQL_TQuery(g_hDB, SQLQuery_UpdatePlayer, szQuery, GetClientUserId(client)); +} + +stock void RemoveColors(char[] text, int size) +{ + if(g_bShavit) + { + for(int i = 0; i < sizeof(gS_GlobalColorNames); i++) + { + ReplaceString(text, size, gS_GlobalColorNames[i], ""); + } + for(int i = 0; i < sizeof(gS_GlobalColors); i++) + { + ReplaceString(text, size, gS_GlobalColors[i], ""); + } + for(int i = 0; i < sizeof(gS_CSGOColorNames); i++) + { + ReplaceString(text, size, gS_CSGOColorNames[i], ""); + } + for(int i = 0; i < sizeof(gS_CSGOColors); i++) + { + ReplaceString(text, size, gS_CSGOColors[i], ""); + } + } + else + { + for(int i = 0; i < sizeof(C_Tag); i++) + { + ReplaceString(text, size, C_Tag[i], ""); + } + for(int i = 0; i < sizeof(C_TagCode); i++) + { + ReplaceString(text, size, C_TagCode[i], ""); + } + } +} + +void CreateCvars() +{ + #if defined USE_AutoExecConfig + AutoExecConfig_SetFile("Discord-Utilities"); + AutoExecConfig_SetCreateFile(true); + + g_cVerificationChannelID = AutoExecConfig_CreateConVar("sm_du_verfication_channelid", "", "Channel ID for verfication. Blank to disable."); + g_cGuildID = AutoExecConfig_CreateConVar("sm_du_verification_guildid", "", "Guild ID of your discord server. Blank to disable. Needed for verification module."); + g_cRoleID = AutoExecConfig_CreateConVar("sm_du_verification_roleid", "", "Role ID to give to user when user is verified. Blank to give no role. Verification module needs to be running."); + + g_cBotToken = AutoExecConfig_CreateConVar("sm_du_bottoken", "", "Bot Token. Needed for discord server => gameserver and/or verification module.", FCVAR_PROTECTED); + g_cCheckInterval = AutoExecConfig_CreateConVar("sm_du_accounts_check_interval", "300", "Time in seconds between verifying accounts."); + g_cUseSWGM = AutoExecConfig_CreateConVar("sm_du_use_swgm_file", "0", "Use SWGM config file for restricting commands."); + g_cServerID = AutoExecConfig_CreateConVar("sm_du_server_id", "1", "Increase this with every server you put this plugin in. Prevents multiple replies from the bot in verfication channel."); + g_cPrimaryServer = AutoExecConfig_CreateConVar("sm_du_server_primary", "0", "Is this the primary server in the verification channel? Only this server will respond to generic queries so atleast 1 server should have this 1."); + + g_cLinkCommand = AutoExecConfig_CreateConVar("sm_du_link_command", "!link", "Command to use in text channel."); + g_cViewIDCommand = AutoExecConfig_CreateConVar("sm_du_viewid_command", "sm_viewid", "Command to view id."); + g_cInviteLink = AutoExecConfig_CreateConVar("sm_du_link", "https://discord.gg/83g5xcE", "Invite link of your discord server."); + + g_cDiscordPrefix = AutoExecConfig_CreateConVar("sm_du_discord_prefix", "[{lightgreen}Discord{default}]", "Prefix for discord messages."); + g_cServerPrefix = AutoExecConfig_CreateConVar("sm_du_server_prefix", "[{lightgreen}Discord-Utilities{default}]", "Prefix for chat messages."); + + g_cDatabaseName = AutoExecConfig_CreateConVar("sm_du_database_name", "du", "Section name in databases.cfg."); + g_cTableName = AutoExecConfig_CreateConVar("sm_du_table_name", "du_users", "Table Name."); + g_cPruneDays = AutoExecConfig_CreateConVar("sm_du_prune_days", "60", "Prune database with players whose last connect is X DAYS and he is not member of discord server. 0 to disable."); + + AutoExecConfig_ExecuteFile(); + AutoExecConfig_CleanFile(); + + #else + g_cVerificationChannelID = CreateConVar("sm_du_verfication_channelid", "", "Channel ID for verfication. Blank to disable."); + g_cGuildID = CreateConVar("sm_du_verification_guildid", "", "Guild ID of your discord server. Blank to disable. Needed for verification module."); + g_cRoleID = CreateConVar("sm_du_verification_roleid", "", "Role ID to give to user when user is verified. Blank to give no role. Verification module needs to be running."); + + g_cBotToken = CreateConVar("sm_du_bottoken", "", "Bot Token. Needed for discord server => gameserver and/or verification module.", FCVAR_PROTECTED); + g_cCheckInterval = CreateConVar("sm_du_accounts_check_interval", "300", "Time in seconds between verifying accounts."); + g_cUseSWGM = CreateConVar("sm_du_use_swgm_file", "0", "Use SWGM config file for restricting commands."); + g_cServerID = CreateConVar("sm_du_server_id", "1", "Increase this with every server you put this plugin in. Prevents multiple replies from the bot in verfication channel."); + g_cPrimaryServer = CreateConVar("sm_du_server_primary", "0", "Is this the primary server in the verification channel? Only this server will respond to generic queries so atleast 1 server should have this 1."); + + g_cLinkCommand = CreateConVar("sm_du_link_command", "!link", "Command to use in text channel."); + g_cViewIDCommand = CreateConVar("sm_du_viewid_command", "sm_viewid", "Command to view id."); + g_cInviteLink = CreateConVar("sm_du_link", "https://discord.gg/83g5xcE", "Invite link of your discord server."); + + g_cDiscordPrefix = CreateConVar("sm_du_discord_prefix", "[{lightgreen}Discord{default}]", "Prefix for discord messages."); + g_cServerPrefix = CreateConVar("sm_du_server_prefix", "[{lightgreen}Discord-Utilities{default}]", "Prefix for chat messages."); + + g_cDatabaseName = CreateConVar("sm_du_database_name", "du", "Section name in databases.cfg."); + g_cTableName = CreateConVar("sm_du_table_name", "du_users", "Table Name."); + g_cPruneDays = CreateConVar("sm_du_prune_days", "60", "Prune database with players whose last connect is X DAYS and he is not member of discord server. 0 to disable."); + + AutoExecConfig(true, "Discord-Utilities"); + #endif + + HookConVarChange(g_cVerificationChannelID, OnSettingsChanged); + HookConVarChange(g_cGuildID, OnSettingsChanged); + HookConVarChange(g_cRoleID, OnSettingsChanged); + + HookConVarChange(g_cBotToken, OnSettingsChanged); + + HookConVarChange(g_cLinkCommand, OnSettingsChanged); + HookConVarChange(g_cViewIDCommand, OnSettingsChanged); + HookConVarChange(g_cInviteLink, OnSettingsChanged); + + HookConVarChange(g_cDiscordPrefix, OnSettingsChanged); + HookConVarChange(g_cServerPrefix, OnSettingsChanged); +} + +void LoadCvars() +{ + g_cVerificationChannelID.GetString(g_sVerificationChannelID, sizeof(g_sVerificationChannelID)); + g_cGuildID.GetString(g_sGuildID, sizeof(g_sGuildID)); + g_cRoleID.GetString(g_sRoleID, sizeof(g_sRoleID)); + + g_cBotToken.GetString(g_sBotToken, sizeof(g_sBotToken)); + + g_cLinkCommand.GetString(g_sLinkCommand, sizeof(g_sLinkCommand)); + g_cViewIDCommand.GetString(g_sViewIDCommand, sizeof(g_sViewIDCommand)); + g_cInviteLink.GetString(g_sInviteLink, sizeof(g_sInviteLink)); + + g_cDiscordPrefix.GetString(g_sDiscordPrefix, sizeof(g_sDiscordPrefix)); + g_cServerPrefix.GetString(g_sServerPrefix, sizeof(g_sServerPrefix)); +} + +void ManagingRole(char[] userid, char[] roleid, EHTTPMethod method) +{ + Handle hData = json_object(); + json_object_set_new(hData, "userid", json_string(userid)); + json_object_set_new(hData, "roleid", json_string(roleid)); + json_object_set_new(hData, "method", json_integer(view_as(method))); + ManageRole(hData); +} + +void ManageRole(Handle hData) +{ + if(StrEqual(g_sGuildID, "")) + { + LogError("[Discord-Utilities] GuildID is not provided. Role cannot be provided!"); + delete hData; + return; + } + char userid[128]; + if (!JsonObjectGetString(hData, "userid", userid, sizeof(userid))) + { + LogError("JsonObjectGetString \"userid\" failed"); + delete hData; + return; + } + char roleid[128]; + if (!JsonObjectGetString(hData, "roleid", roleid, sizeof(roleid))) + { + LogError("JsonObjectGetString \"roleid\" failed"); + delete hData; + return; + } + EHTTPMethod method = view_as(JsonObjectGetInt(hData, "method")); + char url[1024]; + FormatEx(url, sizeof(url), "https://discord.com/api/guilds/%s/members/%s/roles/%s", g_sGuildID, userid, roleid); + char route[512]; + FormatEx(route, sizeof(route), "guild/%s/members", g_sGuildID); + DiscordRequest request = new DiscordRequest(url, method); + if (request == null) + { + CreateTimer(2.0, SendManageRole, hData, TIMER_FLAG_NO_MAPCHANGE); + return; + } + request.SetCallbacks(HTTPCompleted, OnManageRoleSent); + request.SetContentSize(); + request.SetBot(Bot); + request.SetData(hData, route); + request.Send(route); +} + + +public void OnManageRoleSent(Handle request, bool failure, int offset, int statuscode, any dp) +{ + if(failure || (statuscode != 200)) + { + if(statuscode == 429 || statuscode == 500) + { + ManageRole(dp); + //delete view_as(dp); + delete request; + //LogError("OnManageRoleSent: Error code %d | Retrying to Managerole(dp)", statuscode); + return; + } + delete request; + delete view_as(dp); + //LogError("OnManageRoleSent: Error code %d | Deleting everything and returning", statuscode); + return; + } + delete request + delete view_as(dp); +} + + +stock void RefreshClients() +{ + for(int i = 1; i <= MaxClients; i++) if(IsClientInGame(i)) + { + OnClientPreAdminCheck(i); + } +} + +void LoadCommands() +{ + char sBuffer[256]; + if(g_cUseSWGM.IntValue == 1) + { + KeyValues kv = new KeyValues("Command_Listener"); + BuildPath(Path_SM, sBuffer, sizeof(sBuffer), "configs/swgm/command_listener.ini"); + if(!FileToKeyValues(kv, sBuffer)) + { + SetFailState("[Discord-Utilities] Missing config file %s. If you don't use SWGM, then change 'sm_du_use_swgm_file' value to 0.", sBuffer); + } + if(kv.GotoFirstSubKey()) + { + do + { + if(kv.GetSectionName(sBuffer, sizeof(sBuffer))) + { + AddCommandListener(Check, sBuffer); + } + } + while (kv.GotoNextKey()); + } + delete kv; + return; + } + BuildPath(Path_SM, sBuffer, sizeof(sBuffer), "configs/du/command_listener.ini"); + + File fFile = OpenFile(sBuffer, "r"); + + if(!FileExists(sBuffer)) + { + fFile.Close(); + fFile = OpenFile(sBuffer, "w+"); + fFile.WriteLine("// Separate each commands with separate lines. DON'T USE SPACE INFRONT OF COMMANDS. Example:"); + fFile.WriteLine("//sm_shop"); + fFile.WriteLine("//sm_store"); + fFile.WriteLine("//Use it without \"//\""); + fFile.Close(); + LogError("[Discord-Utilities] %s file is empty. Add commands to restrict them!", sBuffer); + return; + } + char sReadBuffer[PLATFORM_MAX_PATH]; + + int len; + while(!fFile.EndOfFile() && fFile.ReadLine(sReadBuffer, sizeof(sReadBuffer))) + { + if (sReadBuffer[0] == '/' && sReadBuffer[1] == '/' || IsCharSpace(sReadBuffer[0])) + { + continue; + } + + ReplaceString(sReadBuffer, sizeof(sReadBuffer), "\n", ""); + ReplaceString(sReadBuffer, sizeof(sReadBuffer), "\r", ""); + ReplaceString(sReadBuffer, sizeof(sReadBuffer), "\t", ""); + + len = strlen(sReadBuffer); + + if (len < 3) + { + continue; + } + + AddCommandListener(Check, sReadBuffer); + } + + fFile.Close(); +} + +stock void CreateBot(bool guilds = true, bool listen = true) +{ + if(StrEqual(g_sBotToken, "") || StrEqual(g_sVerificationChannelID, "")) + { + delete Bot; + return; + } + + delete Bot; + + //if(!g_bIsBotLoaded) + //{ + Bot = new DiscordBot(g_sBotToken); + //} + + if(guilds) + { + //if(g_bIsBotLoaded) + //{ + Bot.GetGuilds(GuildList, _, listen); + //} + //else + //{ + // CreateBot(); + //} + } +} + +stock void KillBot() +{ + if(Bot) + { + Bot.StopListeningToChannels(); + Bot.StopListening(); + } + delete Bot; +} + +stock int GetClientFromUniqueCode(const char[] unique) +{ + for(int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i)) continue; + if (StrEqual(g_sUniqueCode[i], unique)) return i; + } + return -1; +} + +stock char ChangePartsInString(char[] input, const char[] from, const char[] to) +{ + char output[64]; + ReplaceString(input, sizeof(output), from, to); + strcopy(output, sizeof(output), input); + return output; +} + +/* +stock void GetGuilds(bool listen = true) +{ + Bot.GetGuilds(GuildList, _, listen); +} +*/ + +stock void Discord_EscapeString(char[] string, int maxlen, bool name = false) +{ + if(name) + { + ReplaceString(string, maxlen, "everyone", "everyone"); + ReplaceString(string, maxlen, "here", "here"); + ReplaceString(string, maxlen, "discordtag", "discordtag"); + } + ReplaceString(string, maxlen, "#", "#"); + ReplaceString(string, maxlen, "@", "@"); + //ReplaceString(string, maxlen, ":", ""); + ReplaceString(string, maxlen, "_", "ˍ"); + ReplaceString(string, maxlen, "'", "'"); + ReplaceString(string, maxlen, "`", "'"); + ReplaceString(string, maxlen, "~", "∽"); + ReplaceString(string, maxlen, "\"", """); +} + +/* TIMERS */ + +public Action VerifyAccounts(Handle timer) +{ + AccountsCheck(); +} + +public Action Cmd_Unlink(int client, int args) +{ + if(g_bMember[client]) + { + char Query[256]; + + char szSteamId[32]; + GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); + + g_hDB.Format(Query, sizeof(Query), "SELECT userid FROM %s WHERE steamid = '%s'", g_sTableName, szSteamId); + SQL_TQuery(g_hDB, SQLQuery_UnlinkAccount, Query, GetClientUserId(client)); + } +} +/* +public Action Timer_Query_AccountCheck(Handle timer) +{ + if(g_hDB == null) + { + CreateTimer(5.0, Timer_Query_AccountCheck, _, TIMER_FLAG_NO_MAPCHANGE); + return; + } + char Query[256]; + g_hDB.Format(Query, sizeof(Query), "SELECT userid FROM %s", g_sTableName); + SQL_TQuery(g_hDB, SQLQuery_AccountCheck, Query); +} +*/ + +public Action SendGetMembers(Handle timer, any data) +{ + GetMembers(view_as(data)); +} + +public Action SendManageRole(Handle timer, any data) +{ + ManageRole(view_as(data)); +} + +public Action SendRequestAgain(Handle timer, DataPack dp) +{ + ResetPack(dp, false); + Handle request = ReadPackCell(dp); + char route[512]; + ReadPackString(dp, route, sizeof(route)); + delete dp; + DiscordSendRequest(request, route); +} + +public Action Timer_RefreshClients(Handle timer) +{ + RefreshClients(); +} + +stock void DU_DeleteMessageID(DiscordMessage discordmessage) +{ + char channelid[64], msgid[64]; + + discordmessage.GetChannelID(channelid, sizeof(channelid)); + discordmessage.GetID(msgid, sizeof(msgid)); + + Bot.DeleteMessageID(channelid, msgid); +} + +stock bool IsClientValid(int client) +{ + return (0 < client <= MaxClients) && IsClientInGame(client) && !IsFakeClient(client); +} diff --git a/include/discord_utilities/modules.sp b/scripting/discord_utilities/modules.sp similarity index 96% rename from include/discord_utilities/modules.sp rename to scripting/discord_utilities/modules.sp index e0ea489..a3f319e 100644 --- a/include/discord_utilities/modules.sp +++ b/scripting/discord_utilities/modules.sp @@ -1,122 +1,122 @@ -public void ChatRelayReceived(DiscordBot bawt, DiscordChannel channel, DiscordMessage discordmessage) -{ - DiscordUser author = discordmessage.GetAuthor(); - if(author.IsBot()) - { - delete author; - return; - } - - char message[512]; - char userName[32], discriminator[6]; - discordmessage.GetContent(message, sizeof(message)); - author.GetUsername(userName, sizeof(userName)); - author.GetDiscriminator(discriminator, sizeof(discriminator)); - delete author; - - CPrintToChatAll("%s %T", g_sDiscordPrefix, "ChatRelayFormat", LANG_SERVER, userName, discriminator, message); -} - -public void OnMessageReceived(DiscordBot bawt, DiscordChannel channel, DiscordMessage discordmessage) -{ - DiscordUser author = discordmessage.GetAuthor(); - if(author.IsBot()) - { - delete author; - return; - } - - char szValues[2][99]; - char szReply[512]; - char message[512]; - char userID[20], userName[32], discriminator[6]; - - discordmessage.GetContent(message, sizeof(message)); - author.GetUsername(userName, sizeof(userName)); - author.GetDiscriminator(discriminator, sizeof(discriminator)); - author.GetID(userID, sizeof(userID)); - delete author; - - int retrieved1 = ExplodeString(message, " ", szValues, sizeof(szValues), sizeof(szValues[])); - TrimString(szValues[1]); - - char _szValues[3][75]; - int retrieved2 = ExplodeString(szValues[1], "-", _szValues, sizeof(_szValues), sizeof(_szValues[])); - - bool bIsPrimary = g_cPrimaryServer.BoolValue; - - if(StrEqual(szValues[0], g_sLinkCommand)) - { - if (retrieved1 < 2) - { - //Prevent multiple replies, only allow the primary server to respond - if (bIsPrimary) - { - Format(szReply, sizeof(szReply), "%T", "DiscordMissingParameters", LANG_SERVER, userID); - Bot.SendMessage(channel, szReply); - DU_DeleteMessageID(discordmessage); - } - return; - } - else if (retrieved2 != 3) - { - if (bIsPrimary) - { - Format(szReply, sizeof(szReply), "%T", "DiscordInvalidID", LANG_SERVER, userID, g_sViewIDCommand); - Bot.SendMessage(channel, szReply); - DU_DeleteMessageID(discordmessage); - } - return; - } - - if(StringToInt(_szValues[0]) != g_cServerID.IntValue) - { - return; //Prevent multiple replies from the bot (for e.g. the plugin is installed on more than 1 server and they're using the same bot & channel) - } - - int client = GetClientFromUniqueCode(szValues[1]); - if(client <= 0) - { - Format(szReply, sizeof(szReply), "%T", "DiscordInvalid", LANG_SERVER, userID); - Bot.SendMessage(channel, szReply); - } - else if (!g_bMember[client]) - { - DataPack datapack = new DataPack(); - datapack.WriteCell(client); - datapack.WriteString(userID); - datapack.WriteString(userName); - datapack.WriteString(discriminator); - //datapack.WriteString(messageID); - - char szSteamId[32]; - GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); - - char Query[512]; - g_hDB.Format(Query, sizeof(Query), "SELECT userid FROM %s WHERE steamid = '%s'", g_sTableName, szSteamId); - SQL_TQuery(g_hDB, SQLQuery_CheckUserData, Query, datapack); - - //Security addition - renew unique code in case another user copies it before query returns (?) - GetClientAuthId(client, AuthId_SteamID64, szSteamId, sizeof(szSteamId)); - int uniqueNum = GetRandomInt(100000, 999999); - Format(g_sUniqueCode[client], sizeof(g_sUniqueCode), "%i-%i-%s", g_cServerID.IntValue, uniqueNum, szSteamId); - - return; //Dont delete this message so user has positive confirmation - } - else - { - //Don't bother querying the DB if user is already a member - Format(szReply, sizeof(szReply), "%T", "DiscordAlreadyLinked", LANG_SERVER, userID); - Bot.SendMessage(channel, szReply); - } - } - else - { - if (bIsPrimary) - { - Format(szReply, sizeof(szReply), "%T", "DiscordInfo", LANG_SERVER, userID, g_sLinkCommand); - Bot.SendMessage(channel, szReply); - } - } - DU_DeleteMessageID(discordmessage); +public void ChatRelayReceived(DiscordBot bawt, DiscordChannel channel, DiscordMessage discordmessage) +{ + DiscordUser author = discordmessage.GetAuthor(); + if(author.IsBot()) + { + delete author; + return; + } + + char message[512]; + char userName[32], discriminator[6]; + discordmessage.GetContent(message, sizeof(message)); + author.GetUsername(userName, sizeof(userName)); + author.GetDiscriminator(discriminator, sizeof(discriminator)); + delete author; + + CPrintToChatAll("%s %T", g_sDiscordPrefix, "ChatRelayFormat", LANG_SERVER, userName, discriminator, message); +} + +public void OnMessageReceived(DiscordBot bawt, DiscordChannel channel, DiscordMessage discordmessage) +{ + DiscordUser author = discordmessage.GetAuthor(); + if(author.IsBot()) + { + delete author; + return; + } + + char szValues[2][99]; + char szReply[512]; + char message[512]; + char userID[20], userName[32], discriminator[6]; + + discordmessage.GetContent(message, sizeof(message)); + author.GetUsername(userName, sizeof(userName)); + author.GetDiscriminator(discriminator, sizeof(discriminator)); + author.GetID(userID, sizeof(userID)); + delete author; + + int retrieved1 = ExplodeString(message, " ", szValues, sizeof(szValues), sizeof(szValues[])); + TrimString(szValues[1]); + + char _szValues[3][75]; + int retrieved2 = ExplodeString(szValues[1], "-", _szValues, sizeof(_szValues), sizeof(_szValues[])); + + bool bIsPrimary = g_cPrimaryServer.BoolValue; + + if(StrEqual(szValues[0], g_sLinkCommand)) + { + if (retrieved1 < 2) + { + //Prevent multiple replies, only allow the primary server to respond + if (bIsPrimary) + { + Format(szReply, sizeof(szReply), "%T", "DiscordMissingParameters", LANG_SERVER, userID); + Bot.SendMessage(channel, szReply); + DU_DeleteMessageID(discordmessage); + } + return; + } + else if (retrieved2 != 3) + { + if (bIsPrimary) + { + Format(szReply, sizeof(szReply), "%T", "DiscordInvalidID", LANG_SERVER, userID, g_sViewIDCommand); + Bot.SendMessage(channel, szReply); + DU_DeleteMessageID(discordmessage); + } + return; + } + + if(StringToInt(_szValues[0]) != g_cServerID.IntValue) + { + return; //Prevent multiple replies from the bot (for e.g. the plugin is installed on more than 1 server and they're using the same bot & channel) + } + + int client = GetClientFromUniqueCode(szValues[1]); + if(client <= 0) + { + Format(szReply, sizeof(szReply), "%T", "DiscordInvalid", LANG_SERVER, userID); + Bot.SendMessage(channel, szReply); + } + else if (!g_bMember[client]) + { + DataPack datapack = new DataPack(); + datapack.WriteCell(client); + datapack.WriteString(userID); + datapack.WriteString(userName); + datapack.WriteString(discriminator); + //datapack.WriteString(messageID); + + char szSteamId[32]; + GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); + + char Query[512]; + g_hDB.Format(Query, sizeof(Query), "SELECT userid FROM %s WHERE steamid = '%s'", g_sTableName, szSteamId); + SQL_TQuery(g_hDB, SQLQuery_CheckUserData, Query, datapack); + + //Security addition - renew unique code in case another user copies it before query returns (?) + GetClientAuthId(client, AuthId_SteamID64, szSteamId, sizeof(szSteamId)); + int uniqueNum = GetRandomInt(100000, 999999); + Format(g_sUniqueCode[client], sizeof(g_sUniqueCode), "%i-%i-%s", g_cServerID.IntValue, uniqueNum, szSteamId); + + return; //Dont delete this message so user has positive confirmation + } + else + { + //Don't bother querying the DB if user is already a member + Format(szReply, sizeof(szReply), "%T", "DiscordAlreadyLinked", LANG_SERVER, userID); + Bot.SendMessage(channel, szReply); + } + } + else + { + if (bIsPrimary) + { + Format(szReply, sizeof(szReply), "%T", "DiscordInfo", LANG_SERVER, userID, g_sLinkCommand); + Bot.SendMessage(channel, szReply); + } + } + DU_DeleteMessageID(discordmessage); } \ No newline at end of file diff --git a/include/discord_utilities/natives.sp b/scripting/discord_utilities/natives.sp similarity index 96% rename from include/discord_utilities/natives.sp rename to scripting/discord_utilities/natives.sp index 5c1f8cc..bf3ab35 100644 --- a/include/discord_utilities/natives.sp +++ b/scripting/discord_utilities/natives.sp @@ -1,80 +1,80 @@ -public int Native_IsChecked(Handle plugin, int numparams) -{ - return g_bChecked[GetNativeCell(1)]; -} - -public int Native_IsDiscordMember(Handle plugin, int numparams) -{ - if(!g_bChecked[GetNativeCell(1)]) - { - return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); - } - return g_bMember[GetNativeCell(1)]; -} - -public int Native_GetUserId(Handle plugin, int numparams) -{ - if(!g_bChecked[GetNativeCell(1)]) - { - return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); - } - if(!g_bMember[GetNativeCell(1)]) - { - return ThrowNativeError(25, "[Discord-Utilities] %N isn't verified.", GetNativeCell(1)); - } - - SetNativeString(2, g_sUserID[GetNativeCell(1)], GetNativeCell(3)); - return 0; -} - -public int Native_RefreshClients(Handle plugin, int numparams) -{ - if(!g_bChecked[GetNativeCell(1)]) - { - return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); - } - RefreshClients(); - return 0; -} - -public int Native_AddRole(Handle plugin, int numparams) -{ - if(!g_bChecked[GetNativeCell(1)]) - { - return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); - } - if(!g_bMember[GetNativeCell(1)]) - { - return ThrowNativeError(25, "[Discord-Utilities] %N isn't verified.", GetNativeCell(1)); - } - if(g_sUserID[GetNativeCell(1)][0] == '\0') - { - return ThrowNativeError(25, "[Discord-Utilities] %N's userid doesn't exist in database.", GetNativeCell(1)); - } - int client = GetNativeCell(1); - char roleid[128]; - GetNativeString(2, roleid, sizeof(roleid)); - ManagingRole(g_sUserID[client], roleid, k_EHTTPMethodPUT); - return 0; -} - -public int Native_DeleteRole(Handle plugin, int numparams) -{ - if(!g_bChecked[GetNativeCell(1)]) - { - return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); - } - if(!g_bMember[GetNativeCell(1)]) - { - return ThrowNativeError(25, "[Discord-Utilities] %N isn't verified.", GetNativeCell(1)); - } - if(g_sUserID[GetNativeCell(1)][0] == '\0') - { - return ThrowNativeError(25, "[Discord-Utilities] %N's userid doesn't exist in database.", GetNativeCell(1)); - } - int client = GetNativeCell(1); - char roleid[128]; - GetNativeString(2, roleid, sizeof(roleid)); - ManagingRole(g_sUserID[client], roleid, k_EHTTPMethodDELETE); - return 0; +public int Native_IsChecked(Handle plugin, int numparams) +{ + return g_bChecked[GetNativeCell(1)]; +} + +public int Native_IsDiscordMember(Handle plugin, int numparams) +{ + if(!g_bChecked[GetNativeCell(1)]) + { + return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); + } + return g_bMember[GetNativeCell(1)]; +} + +public int Native_GetUserId(Handle plugin, int numparams) +{ + if(!g_bChecked[GetNativeCell(1)]) + { + return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); + } + if(!g_bMember[GetNativeCell(1)]) + { + return ThrowNativeError(25, "[Discord-Utilities] %N isn't verified.", GetNativeCell(1)); + } + + SetNativeString(2, g_sUserID[GetNativeCell(1)], GetNativeCell(3)); + return 0; +} + +public int Native_RefreshClients(Handle plugin, int numparams) +{ + if(!g_bChecked[GetNativeCell(1)]) + { + return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); + } + RefreshClients(); + return 0; +} + +public int Native_AddRole(Handle plugin, int numparams) +{ + if(!g_bChecked[GetNativeCell(1)]) + { + return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); + } + if(!g_bMember[GetNativeCell(1)]) + { + return ThrowNativeError(25, "[Discord-Utilities] %N isn't verified.", GetNativeCell(1)); + } + if(g_sUserID[GetNativeCell(1)][0] == '\0') + { + return ThrowNativeError(25, "[Discord-Utilities] %N's userid doesn't exist in database.", GetNativeCell(1)); + } + int client = GetNativeCell(1); + char roleid[128]; + GetNativeString(2, roleid, sizeof(roleid)); + ManagingRole(g_sUserID[client], roleid, k_EHTTPMethodPUT); + return 0; +} + +public int Native_DeleteRole(Handle plugin, int numparams) +{ + if(!g_bChecked[GetNativeCell(1)]) + { + return ThrowNativeError(25, "[Discord-Utilities] %N hasn't been checked. Call this in OnClientPostAdminCheck.", GetNativeCell(1)); + } + if(!g_bMember[GetNativeCell(1)]) + { + return ThrowNativeError(25, "[Discord-Utilities] %N isn't verified.", GetNativeCell(1)); + } + if(g_sUserID[GetNativeCell(1)][0] == '\0') + { + return ThrowNativeError(25, "[Discord-Utilities] %N's userid doesn't exist in database.", GetNativeCell(1)); + } + int client = GetNativeCell(1); + char roleid[128]; + GetNativeString(2, roleid, sizeof(roleid)); + ManagingRole(g_sUserID[client], roleid, k_EHTTPMethodDELETE); + return 0; } \ No newline at end of file diff --git a/include/discord_utilities/sql.sp b/scripting/discord_utilities/sql.sp similarity index 96% rename from include/discord_utilities/sql.sp rename to scripting/discord_utilities/sql.sp index c58d07b..73924fc 100644 --- a/include/discord_utilities/sql.sp +++ b/scripting/discord_utilities/sql.sp @@ -1,312 +1,312 @@ -public int SQLQuery_Connect(Handle owner, Handle hndl, char[] error, any data) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-Connect] Database failure: %s", error); - SetFailState("[Discord Utilities] Failed to connect to database"); - } - else - { - delete g_hDB; - - g_hDB = view_as(hndl); - - char Ident[4096]; - SQL_GetDriverIdent(SQL_ReadDriver(g_hDB), Ident, sizeof(Ident)); - g_bIsMySQl = StrEqual(Ident, "mysql", false) ? true : false; - - if(g_bIsMySQl) - { - g_hDB.Format(Ident, sizeof(Ident), "CREATE TABLE IF NOT EXISTS `%s` (`ID` bigint(20) NOT NULL AUTO_INCREMENT, `userid` varchar(20) COLLATE utf8_bin NOT NULL, `steamid` varchar(20) COLLATE utf8_bin NOT NULL, `member` int(20) NOT NULL, `last_accountuse` int(64) NOT NULL, PRIMARY KEY (`ID`), UNIQUE KEY `steamid` (`steamid`) ) ENGINE = InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;", g_sTableName); - } - else - { - g_hDB.Format(Ident, sizeof(Ident), "CREATE TABLE IF NOT EXISTS %s (userid varchar(20) NOT NULL, steamid varchar(20) PRIMARY KEY NOT NULL, member int(20) NOT NULL, last_accountuse INTEGER)", g_sTableName); - SQL_SetCharset(g_hDB, "utf8"); - } - SQL_TQuery(g_hDB, SQLQuery_ConnectCallback, Ident); - PruneDatabase(); - } - - //For late load - for (int client = 1; client <= MaxClients; client++) - { - if (IsClientInGame(client) && !g_bChecked[client]) - { - OnClientPreAdminCheck(client); - } - } -} - -public int SQLQuery_ConnectCallback(Handle owner, Handle hndl, char[] error, any data) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-ConnectCallback] Database failure: %s", error); - } -} - -public void PruneDatabase() -{ - if(g_hDB == INVALID_HANDLE) - { - LogError("[DU-PruneDatabaseStart] Prune Database cannot connect to database."); - return; - } - if(g_cPruneDays.IntValue <= 0) - { - return; - } - - int maxlastaccuse = GetTime() - (g_cPruneDays.IntValue * 86400); - - char buffer[1024]; - - if(g_bIsMySQl) - g_hDB.Format(buffer, sizeof(buffer), "DELETE FROM `%s` WHERE `last_accountuse`<'%d' AND `last_accountuse`>'0' AND `member` = 0;", g_sTableName, maxlastaccuse); - else - g_hDB.Format(buffer, sizeof(buffer), "DELETE FROM %s WHERE last_accountuse<'%d' AND last_accountuse>'0' AND member = 0;", g_sTableName, maxlastaccuse); - - SQL_TQuery(g_hDB, SQLQuery_PruneDatabase, buffer); -} - -public int SQLQuery_PruneDatabase(Handle owner, Handle hndl, char [] error, any data) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-PruneDatabase] Query failure: %s", error); - } -} - -public int SQLQuery_GetUserData(Handle owner, Handle hndl, char [] error, any data) -{ - int client; - - /* Make sure the client didn't disconnect while the thread was running */ - - if((client = GetClientOfUserId(data)) == 0) - { - return; - } - - if(hndl == INVALID_HANDLE) - { - LogError("[DU-GetUserData] Query failure: %s", error); - return; - } - if(!SQL_GetRowCount(hndl)) - { - char szSteamId[32]; - char Query[256]; - GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); - if(g_bIsMySQl) - { - g_hDB.Format(Query, sizeof(Query), "INSERT INTO `%s`(ID, userid, steamid, member, last_accountuse) VALUES(NULL, '%s', '%s', '0', '0');", g_sTableName, NULL_STRING, szSteamId); - } - else - { - g_hDB.Format(Query, sizeof(Query), "INSERT INTO %s(userid, steamid, member, last_accountuse) VALUES('%s', '%s', '0', '0');", g_sTableName, NULL_STRING, szSteamId); - } - SQL_TQuery(g_hDB, SQLQuery_InsertNewPlayer, Query); - OnClientPreAdminCheck(client); - return; - } - while(SQL_FetchRow(hndl)) - { - SQL_FetchString(hndl, 0, g_sUserID[client], sizeof(g_sUserID)); - g_bMember[client] = !!SQL_FetchInt(hndl, 1); - } - /* - if(g_bMember[client]) - { - if(strlen(g_sRoleID) > 5) - { - ManagingRole(g_sUserID[client], g_sRoleID, k_EHTTPMethodPUT); - } - } - */ - char steamid[32]; - GetClientAuthId(client, AuthId_SteamID64, steamid, sizeof(steamid)); - int uniqueNum = GetRandomInt(100000, 999999); - Format(g_sUniqueCode[client], sizeof(g_sUniqueCode), "%i-%i-%s", g_cServerID.IntValue, uniqueNum, steamid); - g_bChecked[client] = true; -} - -public int SQLQuery_InsertNewPlayer(Handle owner, Handle hndl, char[] error, any data) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-InsertNewPlayer] Query failure: %s", error); - } -} - -/* -public int SQLQuery_AccountCheck(Handle owner, Handle hndl, char [] error, DataPack pack) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-AccountsCheck] Query failure: %s", error); - return; - } - char szUserIdDB[80]; - while (SQL_FetchRow(hndl)) - { - SQL_FetchString(hndl, 0, szUserIdDB, sizeof(szUserIdDB)); - if (strlen(szUserIdDB) > 15) - { - GetGuildMember(szUserIdDB); - } - } -} -*/ - -public int SQLQuery_CheckUserData(Handle owner, Handle hndl, char [] error, DataPack pack) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-CheckUserData] Query failure: %s", error); - return; - } - char szUserIdDB[20]; - while (SQL_FetchRow(hndl)) - { - SQL_FetchString(hndl, 0, szUserIdDB, sizeof(szUserIdDB)); - } - char szUserId[20], szUserName[32], szDiscriminator[6]; - pack.Reset(); - int client = pack.ReadCell(); - pack.ReadString(szUserId, sizeof(szUserId)); - pack.ReadString(szUserName, sizeof(szUserName)); - pack.ReadString(szDiscriminator, sizeof(szDiscriminator)); - delete pack; - - char szReply[512]; - if(!StrEqual(szUserIdDB, szUserId)) - { - if(strlen(g_sRoleID) > 5) - { - ManagingRole(szUserId, g_sRoleID, k_EHTTPMethodPUT); - } - - CPrintToChat(client, "%s %T", g_sServerPrefix, "DiscordVerified", client, szUserName, szDiscriminator); - g_bMember[client] = true; - - Format(g_sUserID[client], sizeof(g_sUserID), szUserId); - Format(szReply, sizeof(szReply), "%T", "DiscordLinked", LANG_SERVER, szUserId); - Bot.SendMessageToChannelID(g_sVerificationChannelID, szReply); - - char szSteamId[20]; - GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); - - char Query[512]; - if(g_bIsMySQl) - { - g_hDB.Format(Query, sizeof(Query), "UPDATE `%s` SET `userid` = '%s', member = 1 WHERE `steamid` = '%s';", g_sTableName, szUserId, szSteamId); - } - else - { - g_hDB.Format(Query, sizeof(Query), "UPDATE %s SET userid = '%s', member = 1 WHERE steamid = '%s'", g_sTableName, szUserId, szSteamId); - } - SQL_TQuery(g_hDB, SQLQuery_LinkedAccount, Query); - - Call_StartForward(g_hOnLinkedAccount); - Call_PushCell(client); - Call_PushString(szUserId); - Call_PushString(szUserName); - Call_PushString(szDiscriminator); - Call_Finish(); - } - else - { - Format(szReply, sizeof(szReply), "%T", "DiscordAlreadyLinked", LANG_SERVER, szUserId); - Bot.SendMessageToChannelID(g_sVerificationChannelID, szReply); - } -} - -public int SQLQuery_LinkedAccount(Handle owner, Handle hndl, char [] error, any data) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-LinkedAccount] Query failure: %s", error); - return; - } -} - - -public int SQLQuery_UnlinkAccount(Handle owner, Handle hndl, char [] error, int userid) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-CheckUserData] Query failure: %s", error); - return; - } - - int client = GetClientOfUserId(userid); - - if(!IsClientValid(client)) - { - return; - } - - char szUserIdDB[20]; - while (SQL_FetchRow(hndl)) - { - SQL_FetchString(hndl, 0, szUserIdDB, sizeof(szUserIdDB)); - } - - if(strlen(g_sRoleID) > 5) - { - ManagingRole(szUserIdDB, g_sRoleID, k_EHTTPMethodDELETE); - } - - char szSteamId[20]; - GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); - - char Query[512]; - if(g_bIsMySQl) - { - g_hDB.Format(Query, sizeof(Query), "UPDATE `%s` SET `userid` = '', member = 0 WHERE `steamid` = '%s';", g_sTableName, szSteamId); - } - else - { - g_hDB.Format(Query, sizeof(Query), "UPDATE %s SET userid = '', member = 0 WHERE steamid = '%s'", g_sTableName, szSteamId); - } - SQL_TQuery(g_hDB, SQLQuery_UnlinkAccount_Callback, Query, userid); -} - -public int SQLQuery_UnlinkAccount_Callback(Handle owner, Handle hndl, char [] error, int userid) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-UnlinkAccount] Query failure: %s", error); - return; - } - - int client = GetClientOfUserId(userid); - - if(!IsClientValid(client)) - { - return; - } - - g_bMember[client] = false; - - Call_StartForward(g_hOnAccountRevoked); - Call_PushCell(client); - Call_PushString(g_sUserID[client]); - Call_Finish(); - - g_sUserID[client][0] = '\0'; - - LogToFile("addons/sourcemod/logs/dsmembers_revoke.log", "Player %L unverified himself.", client); - CPrintToChat(client, "%s - You succesfully unlinked your account.", g_sServerPrefix); -} - -public int SQLQuery_UpdatePlayer(Handle owner, Handle hndl, char [] error, any data) -{ - if(hndl == INVALID_HANDLE) - { - LogError("[DU-UpdatePlayer] Query failure: %s", error); - return; - } -} +public int SQLQuery_Connect(Handle owner, Handle hndl, char[] error, any data) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-Connect] Database failure: %s", error); + SetFailState("[Discord Utilities] Failed to connect to database"); + } + else + { + delete g_hDB; + + g_hDB = view_as(hndl); + + char Ident[4096]; + SQL_GetDriverIdent(SQL_ReadDriver(g_hDB), Ident, sizeof(Ident)); + g_bIsMySQl = StrEqual(Ident, "mysql", false) ? true : false; + + if(g_bIsMySQl) + { + g_hDB.Format(Ident, sizeof(Ident), "CREATE TABLE IF NOT EXISTS `%s` (`ID` bigint(20) NOT NULL AUTO_INCREMENT, `userid` varchar(20) COLLATE utf8_bin NOT NULL, `steamid` varchar(20) COLLATE utf8_bin NOT NULL, `member` int(20) NOT NULL, `last_accountuse` int(64) NOT NULL, PRIMARY KEY (`ID`), UNIQUE KEY `steamid` (`steamid`) ) ENGINE = InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;", g_sTableName); + } + else + { + g_hDB.Format(Ident, sizeof(Ident), "CREATE TABLE IF NOT EXISTS %s (userid varchar(20) NOT NULL, steamid varchar(20) PRIMARY KEY NOT NULL, member int(20) NOT NULL, last_accountuse INTEGER)", g_sTableName); + SQL_SetCharset(g_hDB, "utf8"); + } + SQL_TQuery(g_hDB, SQLQuery_ConnectCallback, Ident); + PruneDatabase(); + } + + //For late load + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client) && !g_bChecked[client]) + { + OnClientPreAdminCheck(client); + } + } +} + +public int SQLQuery_ConnectCallback(Handle owner, Handle hndl, char[] error, any data) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-ConnectCallback] Database failure: %s", error); + } +} + +public void PruneDatabase() +{ + if(g_hDB == INVALID_HANDLE) + { + LogError("[DU-PruneDatabaseStart] Prune Database cannot connect to database."); + return; + } + if(g_cPruneDays.IntValue <= 0) + { + return; + } + + int maxlastaccuse = GetTime() - (g_cPruneDays.IntValue * 86400); + + char buffer[1024]; + + if(g_bIsMySQl) + g_hDB.Format(buffer, sizeof(buffer), "DELETE FROM `%s` WHERE `last_accountuse`<'%d' AND `last_accountuse`>'0' AND `member` = 0;", g_sTableName, maxlastaccuse); + else + g_hDB.Format(buffer, sizeof(buffer), "DELETE FROM %s WHERE last_accountuse<'%d' AND last_accountuse>'0' AND member = 0;", g_sTableName, maxlastaccuse); + + SQL_TQuery(g_hDB, SQLQuery_PruneDatabase, buffer); +} + +public int SQLQuery_PruneDatabase(Handle owner, Handle hndl, char [] error, any data) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-PruneDatabase] Query failure: %s", error); + } +} + +public int SQLQuery_GetUserData(Handle owner, Handle hndl, char [] error, any data) +{ + int client; + + /* Make sure the client didn't disconnect while the thread was running */ + + if((client = GetClientOfUserId(data)) == 0) + { + return; + } + + if(hndl == INVALID_HANDLE) + { + LogError("[DU-GetUserData] Query failure: %s", error); + return; + } + if(!SQL_GetRowCount(hndl)) + { + char szSteamId[32]; + char Query[256]; + GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); + if(g_bIsMySQl) + { + g_hDB.Format(Query, sizeof(Query), "INSERT INTO `%s`(ID, userid, steamid, member, last_accountuse) VALUES(NULL, '%s', '%s', '0', '0');", g_sTableName, NULL_STRING, szSteamId); + } + else + { + g_hDB.Format(Query, sizeof(Query), "INSERT INTO %s(userid, steamid, member, last_accountuse) VALUES('%s', '%s', '0', '0');", g_sTableName, NULL_STRING, szSteamId); + } + SQL_TQuery(g_hDB, SQLQuery_InsertNewPlayer, Query); + OnClientPreAdminCheck(client); + return; + } + while(SQL_FetchRow(hndl)) + { + SQL_FetchString(hndl, 0, g_sUserID[client], sizeof(g_sUserID)); + g_bMember[client] = !!SQL_FetchInt(hndl, 1); + } + /* + if(g_bMember[client]) + { + if(strlen(g_sRoleID) > 5) + { + ManagingRole(g_sUserID[client], g_sRoleID, k_EHTTPMethodPUT); + } + } + */ + char steamid[32]; + GetClientAuthId(client, AuthId_SteamID64, steamid, sizeof(steamid)); + int uniqueNum = GetRandomInt(100000, 999999); + Format(g_sUniqueCode[client], sizeof(g_sUniqueCode), "%i-%i-%s", g_cServerID.IntValue, uniqueNum, steamid); + g_bChecked[client] = true; +} + +public int SQLQuery_InsertNewPlayer(Handle owner, Handle hndl, char[] error, any data) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-InsertNewPlayer] Query failure: %s", error); + } +} + +/* +public int SQLQuery_AccountCheck(Handle owner, Handle hndl, char [] error, DataPack pack) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-AccountsCheck] Query failure: %s", error); + return; + } + char szUserIdDB[80]; + while (SQL_FetchRow(hndl)) + { + SQL_FetchString(hndl, 0, szUserIdDB, sizeof(szUserIdDB)); + if (strlen(szUserIdDB) > 15) + { + GetGuildMember(szUserIdDB); + } + } +} +*/ + +public int SQLQuery_CheckUserData(Handle owner, Handle hndl, char [] error, DataPack pack) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-CheckUserData] Query failure: %s", error); + return; + } + char szUserIdDB[20]; + while (SQL_FetchRow(hndl)) + { + SQL_FetchString(hndl, 0, szUserIdDB, sizeof(szUserIdDB)); + } + char szUserId[20], szUserName[32], szDiscriminator[6]; + pack.Reset(); + int client = pack.ReadCell(); + pack.ReadString(szUserId, sizeof(szUserId)); + pack.ReadString(szUserName, sizeof(szUserName)); + pack.ReadString(szDiscriminator, sizeof(szDiscriminator)); + delete pack; + + char szReply[512]; + if(!StrEqual(szUserIdDB, szUserId)) + { + if(strlen(g_sRoleID) > 5) + { + ManagingRole(szUserId, g_sRoleID, k_EHTTPMethodPUT); + } + + CPrintToChat(client, "%s %T", g_sServerPrefix, "DiscordVerified", client, szUserName, szDiscriminator); + g_bMember[client] = true; + + Format(g_sUserID[client], sizeof(g_sUserID), szUserId); + Format(szReply, sizeof(szReply), "%T", "DiscordLinked", LANG_SERVER, szUserId); + Bot.SendMessageToChannelID(g_sVerificationChannelID, szReply); + + char szSteamId[20]; + GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); + + char Query[512]; + if(g_bIsMySQl) + { + g_hDB.Format(Query, sizeof(Query), "UPDATE `%s` SET `userid` = '%s', member = 1 WHERE `steamid` = '%s';", g_sTableName, szUserId, szSteamId); + } + else + { + g_hDB.Format(Query, sizeof(Query), "UPDATE %s SET userid = '%s', member = 1 WHERE steamid = '%s'", g_sTableName, szUserId, szSteamId); + } + SQL_TQuery(g_hDB, SQLQuery_LinkedAccount, Query); + + Call_StartForward(g_hOnLinkedAccount); + Call_PushCell(client); + Call_PushString(szUserId); + Call_PushString(szUserName); + Call_PushString(szDiscriminator); + Call_Finish(); + } + else + { + Format(szReply, sizeof(szReply), "%T", "DiscordAlreadyLinked", LANG_SERVER, szUserId); + Bot.SendMessageToChannelID(g_sVerificationChannelID, szReply); + } +} + +public int SQLQuery_LinkedAccount(Handle owner, Handle hndl, char [] error, any data) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-LinkedAccount] Query failure: %s", error); + return; + } +} + + +public int SQLQuery_UnlinkAccount(Handle owner, Handle hndl, char [] error, int userid) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-CheckUserData] Query failure: %s", error); + return; + } + + int client = GetClientOfUserId(userid); + + if(!IsClientValid(client)) + { + return; + } + + char szUserIdDB[20]; + while (SQL_FetchRow(hndl)) + { + SQL_FetchString(hndl, 0, szUserIdDB, sizeof(szUserIdDB)); + } + + if(strlen(g_sRoleID) > 5) + { + ManagingRole(szUserIdDB, g_sRoleID, k_EHTTPMethodDELETE); + } + + char szSteamId[20]; + GetClientAuthId(client, AuthId_Steam2, szSteamId, sizeof(szSteamId)); + + char Query[512]; + if(g_bIsMySQl) + { + g_hDB.Format(Query, sizeof(Query), "UPDATE `%s` SET `userid` = '', member = 0 WHERE `steamid` = '%s';", g_sTableName, szSteamId); + } + else + { + g_hDB.Format(Query, sizeof(Query), "UPDATE %s SET userid = '', member = 0 WHERE steamid = '%s'", g_sTableName, szSteamId); + } + SQL_TQuery(g_hDB, SQLQuery_UnlinkAccount_Callback, Query, userid); +} + +public int SQLQuery_UnlinkAccount_Callback(Handle owner, Handle hndl, char [] error, int userid) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-UnlinkAccount] Query failure: %s", error); + return; + } + + int client = GetClientOfUserId(userid); + + if(!IsClientValid(client)) + { + return; + } + + g_bMember[client] = false; + + Call_StartForward(g_hOnAccountRevoked); + Call_PushCell(client); + Call_PushString(g_sUserID[client]); + Call_Finish(); + + g_sUserID[client][0] = '\0'; + + LogToFile("addons/sourcemod/logs/dsmembers_revoke.log", "Player %L unverified himself.", client); + CPrintToChat(client, "%s - You succesfully unlinked your account.", g_sServerPrefix); +} + +public int SQLQuery_UpdatePlayer(Handle owner, Handle hndl, char [] error, any data) +{ + if(hndl == INVALID_HANDLE) + { + LogError("[DU-UpdatePlayer] Query failure: %s", error); + return; + } +} diff --git a/scripting/include/calladmin.inc b/scripting/include/calladmin.inc new file mode 100644 index 0000000..1af2c58 --- /dev/null +++ b/scripting/include/calladmin.inc @@ -0,0 +1,292 @@ +/** Include guard */ +#if defined _calladmin_included + #endinput +#endif +#define _calladmin_included + + + + +/** Global calladmin version, do not change */ +#define CALLADMIN_VERSION "0.1.8" + + + +/** Pass this as a clientindex to the ReportPlayer native if you don't have a client, eg report from server automatically */ +#define REPORTER_CONSOLE 1679124 + + +/** Maximum size a reason string can be in length */ +#define REASON_MAX_LENGTH 128 + + + +/** + * Called when the main CallAdmin client selection menu is about to be drawn for a client. + * Note: CallAdmin will not notify players why or if their menu was blocked. + * + * @param client Client index of the caller. + * @return Plugin_Continue to allow, Plugin_Handled otherwise. + */ +forward Action CallAdmin_OnDrawMenu(int client); + + + + +/** + * Called when the own reason selection is enabled and the select item for it is about to be drawn for a client. + * + * @param client Client index of the caller. + * @return Plugin_Continue to allow, Plugin_Handled otherwise. + */ +forward Action CallAdmin_OnDrawOwnReason(int client); + + + + +/** + * Called when a target is about to be drawn to the target selection menu for a client. + * Note: Called *n-1 times for the client selection menu where n is the amount of valid targets including the caller. + * + * @param client Client index of the caller. + * @param target Client index of the target about to be drawn. + * @return Plugin_Continue to allow the target to be drawn, Plugin_Handled otherwise. + */ +forward Action CallAdmin_OnDrawTarget(int client, int target); + + + + +/** + * Called when the tracker count was changed. + * + * @param oldVal Tracker count before update. + * @param newVal Tracker count after update. + * @noreturn + */ +forward void CallAdmin_OnTrackerCountChanged(int oldVal, int newVal); + + + + +/** + * Called before a client (or module) has reported another client. + * Be sure to check that client != REPORTER_CONSOLE if you expect a valid client index. + * Note: CallAdmin will not notify players why or if their report was blocked. + * + * @param client Client index of the caller. + * @param target Client index of the target. + * @param reason Reason selected by the client for the report. + * @return Plugin_Continue to allow, Plugin_Handled otherwise. + */ +forward Action CallAdmin_OnReportPre(int client, int target, const char[] reason); + + + + +/** + * Called after a client (or module) has reported another client. + * Be sure to check that client != REPORTER_CONSOLE if you expect a valid client index. + * + * @param client Client index of the caller. + * @param target Client index of the target. + * @param reason Reason selected by the client for the report. + * @noreturn + */ +forward void CallAdmin_OnReportPost(int client, int target, const char[] reason); + + + + +/** + * Initiates a report call. + * If you report automatically (via a module for example) set the client index to REPORTER_CONSOLE. + * + * @param client Client index of the caller. + * @param target Client index of the target. + * @param reason Reason for the report. + * @return True if target could be reported, false otherwise. + */ +native bool CallAdmin_ReportClient(int client, int target, const char[] reason); + + + + +/** + * Called when an admin is about to be added to the in-game admin count. + * + * @param client Client index of the admin. + * @return Plugin_Continue to allow, Plugin_Handled otherwise. + */ +forward Action CallAdmin_OnAddToAdminCount(int client); + + + + +/** + * Returns the cached count of current trackers. + * + * @return Count of current trackers. + */ +native int CallAdmin_GetTrackersCount(); + + + + +/** + * Requests a forced refresh of the trackers count. + * Note that most modules work asynchronous and only return their own cached count. + * + * @noreturn + */ +native void CallAdmin_RequestTrackersCountRefresh(); + + + + +/** + * Called when the trackercount of a module is requested. + * This is either called periodically via the base calladmin, or when RequestTrackersCountRefresh is invoked. + * + * @param trackers By ref value of your trackers. + * @noreturn + */ +forward void CallAdmin_OnRequestTrackersCountRefresh(int &trackers); + + + + +enum ServerData +{ + ServerData_HostName, /**< This is the hostname of the server, gathered from the `hostname` convar */ + ServerData_HostIP, /**< This is the hostip of the server, gathered and converted from the `hostip` convar */ + ServerData_HostPort /**< This is the hostport of the server, gathered from the `hostport` convar */ +}; + + + + +/** + * Called when the serverdata data is changed. + * + * @param convar Handle to the convar which was changed. + * @param type Type of data which was changed. + * @param oldVal Value of data before change. + * @param newVal Value of data after change. + * @noreturn + */ +forward void CallAdmin_OnServerDataChanged(ConVar convar, ServerData type, const char[] oldVal, const char[] newVal); + + + + +/** + * Returns the server's hostname. + * + * @param buffer String to copy hostname to. + * @param max_size Maximum size of buffer. + * @noreturn + */ +native void CallAdmin_GetHostName(char[] buffer, int max_size); + + + + +/** + * Returns the server's Ip String. + * + * @param buffer String to copy hostip to. + * @param max_size Maximum size of buffer. + * @noreturn + */ +native void CallAdmin_GetHostIP(char[] buffer, int max_size); + + + + +/** + * Returns the server's port. + * + * @return Port of the server. + */ +native int CallAdmin_GetHostPort(); + + + + +/** + * Logs a message to the calladmin logfile. + * The message has this format "[Pluginname] Message", where the plugin name is detected automatically. + * + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + */ +native void CallAdmin_LogMessage(const char[] format, any ...); + + + + +/** + * Called when a message was logged to the calladmin logfile. + * + * @param plugin Handle to the plugin which logged the message. + * @param message Message that was logged. + * @noreturn + */ +forward void CallAdmin_OnLogMessage(Handle plugin, const char[] message); + + + + +/** + * Returns the server's current report id. + * This is a temporary value and is increased with each successfull report. + * + * @return Current report id of the server. + */ +native int CallAdmin_GetReportID(); + + + + +/** + * Called when a report was handled. + * + * @param client Admin who handled the report. + * @param id Id of the handled report. + * @noreturn + */ +forward void CallAdmin_OnReportHandled(int client, int id); + + + + +/* Do not edit below this line */ +public SharedPlugin __pl_calladmin = +{ + name = "calladmin", + file = "calladmin.smx", +#if defined REQUIRE_PLUGIN + required = 0, +#else + required = 0, +#endif +}; + + + + +#if !defined REQUIRE_PLUGIN +public __pl_calladmin_SetNTVOptional() +{ + MarkNativeAsOptional("CallAdmin_GetTrackersCount"); + MarkNativeAsOptional("CallAdmin_RequestTrackersCountRefresh"); + MarkNativeAsOptional("CallAdmin_GetHostName"); + MarkNativeAsOptional("CallAdmin_GetHostIP"); + MarkNativeAsOptional("CallAdmin_GetHostPort"); + MarkNativeAsOptional("CallAdmin_ReportClient"); + MarkNativeAsOptional("CallAdmin_LogMessage"); + MarkNativeAsOptional("CallAdmin_GetReportID"); +} +#endif diff --git a/include/discord.inc b/scripting/include/discord.inc similarity index 100% rename from include/discord.inc rename to scripting/include/discord.inc diff --git a/include/discord_utilities.inc b/scripting/include/discord_utilities.inc similarity index 96% rename from include/discord_utilities.inc rename to scripting/include/discord_utilities.inc index 5ba8212..9680384 100644 --- a/include/discord_utilities.inc +++ b/scripting/include/discord_utilities.inc @@ -1,124 +1,124 @@ -#if defined _discord_utilities_included - #endinput -#endif -#define _discord_utilities_included - -/** - * Called after a client has verfied his discord account. - * - * @param client Client index of the verified client. - * @param userid Discord userid of the client. - * @param username Discord username of the client. - * @param discriminator Discord discriminator of the client. - * @noreturn - */ -forward void DU_OnLinkedAccount(int client, const char[] userid, const char[] username, const char[] discriminator); - -/** - * Called after a client's member status has been revoked. - * - * @param client Client index. - * @param userid Discord userid of the client. - * @noreturn - */ -forward void DU_OnAccountRevoked(int client, const char[] userid); - -/** - * Called before checking accounts. return Plugin_Handled to block checking. - * - * @param bottoken Token of bot which will be checking accounts. - * @param userid Guildid of your discord server. - * @param tablename Table name of database. - * @noreturn - */ -forward void DU_OnCheckedAccounts(const char[] bottoken, const char[] guildid, const char[] tablename); - - -/** - * Called after member data is dumped (as json) - * - * @noreturn - */ -forward void DU_OnMemberDataDumped(); - - - -/** - * Refreshing every client online in server. Can be used for grasping steam avatar of client and re-checking client in database. - * - * @noreturn - */ -native void DU_RefreshClients(); - -/** - * Discord Database Check - * - * @returns true if client was already checked for member status. - */ -native bool DU_IsChecked(int client); - -/** - * Discord Member Check - * - * @returns true if client is member of discord server else false. - */ -native bool DU_IsMember(int client); - -/** - * Retrieve client's discord userid. - * @param client Client index to get userid from. - * @param output String to store userid in. - * @param maxsize Max size of string. - * @Throws native error if Is_Member == false or client userid is not retrieved. - */ -native char DU_GetUserId(int client, char[] output, int maxsize); - -/** - * Retrieve DNS ip / real ip of server according to sm_du_dns_ip. (If cvar is blank then real ip else DNS ip) - * @param output String to store sererip in. - * @param maxsize Max size of string. - * @noreturn - */ -native void DU_GetIP(char[] output, int maxsize); - -/** - * Assigns role to client according to roleid - * @param client Client index to give role to. - * @param roleid String containing roleid. - * @param maxsize Max size of string. - * @noreturn - */ -native void DU_AddRole(int client, char[] roleid); - -/** - * Deletes role from client according to roleid - * @param client Client index to delete role from. - * @param roleid String containing roleid. - * @param maxsize Max size of string. - * @noreturn - */ -native void DU_DeleteRole(int client, char[] roleid); - -public SharedPlugin __pl_discord_utilities = -{ - name = "DiscordUtilities", - file = "discord_utilities.smx", - #if defined REQUIRE_PLUGIN - required = 1, - #else - required = 0, - #endif -}; - -#if !defined REQUIRE_PLUGIN -public void __pl_discord_utilities_SetNTVOptional() -{ - MarkNativeAsOptional("DU_IsChecked"); - MarkNativeAsOptional("DU_IsMember"); - MarkNativeAsOptional("DU_GetUserId"); - MarkNativeAsOptional("DU_RefreshClients"); - MarkNativeAsOptional("DU_GetIP"); - MarkNativeAsOptional("DU_AddRole"); - MarkNativeAsOptional("DU_DeleteRole"); -} -#endif +#if defined _discord_utilities_included + #endinput +#endif +#define _discord_utilities_included + +/** + * Called after a client has verfied his discord account. + * + * @param client Client index of the verified client. + * @param userid Discord userid of the client. + * @param username Discord username of the client. + * @param discriminator Discord discriminator of the client. + * @noreturn + */ +forward void DU_OnLinkedAccount(int client, const char[] userid, const char[] username, const char[] discriminator); + +/** + * Called after a client's member status has been revoked. + * + * @param client Client index. + * @param userid Discord userid of the client. + * @noreturn + */ +forward void DU_OnAccountRevoked(int client, const char[] userid); + +/** + * Called before checking accounts. return Plugin_Handled to block checking. + * + * @param bottoken Token of bot which will be checking accounts. + * @param userid Guildid of your discord server. + * @param tablename Table name of database. + * @noreturn + */ +forward void DU_OnCheckedAccounts(const char[] bottoken, const char[] guildid, const char[] tablename); + + +/** + * Called after member data is dumped (as json) + * + * @noreturn + */ +forward void DU_OnMemberDataDumped(); + + + +/** + * Refreshing every client online in server. Can be used for grasping steam avatar of client and re-checking client in database. + * + * @noreturn + */ +native void DU_RefreshClients(); + +/** + * Discord Database Check + * + * @returns true if client was already checked for member status. + */ +native bool DU_IsChecked(int client); + +/** + * Discord Member Check + * + * @returns true if client is member of discord server else false. + */ +native bool DU_IsMember(int client); + +/** + * Retrieve client's discord userid. + * @param client Client index to get userid from. + * @param output String to store userid in. + * @param maxsize Max size of string. + * @Throws native error if Is_Member == false or client userid is not retrieved. + */ +native char DU_GetUserId(int client, char[] output, int maxsize); + +/** + * Retrieve DNS ip / real ip of server according to sm_du_dns_ip. (If cvar is blank then real ip else DNS ip) + * @param output String to store sererip in. + * @param maxsize Max size of string. + * @noreturn + */ +native void DU_GetIP(char[] output, int maxsize); + +/** + * Assigns role to client according to roleid + * @param client Client index to give role to. + * @param roleid String containing roleid. + * @param maxsize Max size of string. + * @noreturn + */ +native void DU_AddRole(int client, char[] roleid); + +/** + * Deletes role from client according to roleid + * @param client Client index to delete role from. + * @param roleid String containing roleid. + * @param maxsize Max size of string. + * @noreturn + */ +native void DU_DeleteRole(int client, char[] roleid); + +public SharedPlugin __pl_discord_utilities = +{ + name = "DiscordUtilities", + file = "discord_utilities.smx", + #if defined REQUIRE_PLUGIN + required = 1, + #else + required = 0, + #endif +}; + +#if !defined REQUIRE_PLUGIN +public void __pl_discord_utilities_SetNTVOptional() +{ + MarkNativeAsOptional("DU_IsChecked"); + MarkNativeAsOptional("DU_IsMember"); + MarkNativeAsOptional("DU_GetUserId"); + MarkNativeAsOptional("DU_RefreshClients"); + MarkNativeAsOptional("DU_GetIP"); + MarkNativeAsOptional("DU_AddRole"); + MarkNativeAsOptional("DU_DeleteRole"); +} +#endif diff --git a/Discord-Utilities.phrases.txt b/translations/Discord-Utilities.phrases.txt similarity index 93% rename from Discord-Utilities.phrases.txt rename to translations/Discord-Utilities.phrases.txt index 51fe04a..701e1c7 100644 --- a/Discord-Utilities.phrases.txt +++ b/translations/Discord-Utilities.phrases.txt @@ -183,26 +183,15 @@ "LinkUsage" { - "#format" "{1:s},{2:s}" - "en" "Use {yellow}{1} {2}" - "hu" "Használd a {yellow}{1} {2}" - "fr" "Utiliser {yellow}{1} {2}" - "nl" "Gebruik {yellow}{1} {2}" - "lv" "Izmanto {yellow}{1} {2}" - "ru" "использование {yellow}{1} {2}" - "ro" "Folosește {yellow}{1} {2}" + "#format" "{1:s},{2:s},{3:s}" + "en" "Type {lime}{1} {2} {default}in {orange}#{3}" + "hu" "Használd a {lime}{1} {2} {default}parancsot a {orange}#{3} {default}szobában" } - - "LinkUsage2" + + "CopyPasteFromConsole" { - "#format" "{1:s}" - "en" "in channel {purple}#{1} {default}to get verified!" - "hu" "parancsot a {purple}#{1} {default}szobában a megerősítéshez!" - "fr" "dans le canal {purple}#{1} {default}pour se faire vérifier!" - "nl" "in kanaal {purple}#{1} {default}om geverifieerd te worden!" - "lv" "verifikācijas kodu kanālā {purple}#{1} {default}lai verificētu profilu!" - "ru" "в канале {purple}#{1} {default}пройти проверку!" - "ro" "În chanalul {purple}#{1} {default}pentru a te verifica!" + "en" "You can also {lime}copy-paste{default} from {orange}console{default} :)" + "hu" "Ki is {lime}másolhatod {default}a {orange}konzolból{default} :)" } "CallAdminReportHandledTitle" @@ -475,4 +464,14 @@ "ru" "Вы должны подтвердить свой {blue}Discord {default}учетная запись! Тип {orange}{1}" "ro" "Trebuie să îti asociezi Discord-ul tău! Tastează {orange}{1}" } + + "AlreadyVerified" + { + "en" "You are already verified. Enjoy your benefits :)" + } + + "CanChange" + { + "en" "You can change your verified discord account using {LIME}!unverify{DEFAULT} and {LIME}!verify{DEFAULT} again" + } } From f6ad771d603d3d796e9a62154383bedf1072f814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AiDN=E2=84=A2?= <45371311+originalaidn@users.noreply.github.com> Date: Thu, 27 Jan 2022 13:24:17 +0100 Subject: [PATCH 2/9] Update globals.sp --- scripting/discord_utilities/globals.sp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripting/discord_utilities/globals.sp b/scripting/discord_utilities/globals.sp index 9aef28d..2434ac3 100644 --- a/scripting/discord_utilities/globals.sp +++ b/scripting/discord_utilities/globals.sp @@ -1,4 +1,4 @@ -#define PLUGIN_VERSION "2.4-betafixslow" +#define PLUGIN_VERSION "2.5-betafixslow" #define PLUGIN_NAME "Discord Utilities" #define PLUGIN_AUTHOR "Cruze" From bff3eee3abf7db7929e156017433474b2ad85c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AiDN=E2=84=A2?= <45371311+originalaidn@users.noreply.github.com> Date: Thu, 27 Jan 2022 19:18:10 +0100 Subject: [PATCH 3/9] created calladmin module for DU --- scripting/du_calladmin.sp | 396 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 scripting/du_calladmin.sp diff --git a/scripting/du_calladmin.sp b/scripting/du_calladmin.sp new file mode 100644 index 0000000..ff8fcc5 --- /dev/null +++ b/scripting/du_calladmin.sp @@ -0,0 +1,396 @@ +#include +#include +#include +#include +#include + +#include + +#include + +#define DEFAULT_COLOR "#00FF00" + +#pragma dynamic 500000 +#pragma newdecls required +#pragma semicolon 1 + +ConVar g_cCallAdmin_Webhook, g_cCallAdmin_BotName, g_cCallAdmin_BotAvatar, g_cCallAdmin_Color, g_cCallAdmin_Content, g_cCallAdmin_FooterIcon, g_cDNSServerIP; + +char g_sCallAdmin_Webhook[128], g_sCallAdmin_BotName[32], g_sCallAdmin_BotAvatar[128], g_sCallAdmin_Color[8], g_sCallAdmin_Content[256], g_sCallAdmin_FooterIcon[128], g_sServerIP[128]; + +int g_iLastReportID; + +char g_sServerName[128]; + +ArrayList g_aCallAdmin_ReportedList; + +bool g_bCallAdmin; + +public Plugin myinfo = +{ + name = "Discord Utilities - Calladmin module", + author = "AiDN™", + description = "Calladmin module for the Discord Utilities", + version = "1.0", + url = "https://steamcommunity.com/id/originalaidn" +}; + +public void OnLibraryAdded(const char[] szLibrary) +{ + if(StrEqual(szLibrary, "calladmin")) g_bCallAdmin = true; +} + +public void OnLibraryRemoved(const char[] szLibrary) +{ + if(StrEqual(szLibrary, "calladmin")) g_bCallAdmin = false; +} +public void OnAllPluginsLoaded() +{ + if(!LibraryExists("discord-api")) + { + SetFailState("[Discord-Utilities] This plugin is fully dependant on \"Discord-API\" by Deathknife. (https://github.com/Deathknife/sourcemod-discord)"); + } + + g_bCallAdmin = LibraryExists("calladmin"); +} + +public void OnConfigsExecuted() +{ + LoadCvars(); + if(g_bCallAdmin) + { + CallAdmin_GetHostName(g_sServerName, sizeof(g_sServerName)); + g_aCallAdmin_ReportedList = new ArrayList(64); + } + else + { + FindConVar("hostname").GetString(g_sServerName, sizeof(g_sServerName)); + } +} + +public int OnSettingsChanged(ConVar convar, const char[] oldVal, const char[] newVal) +{ + if(StrEqual(oldVal, newVal, true)) + { + return; + } + if(convar == g_cCallAdmin_Webhook) + { + strcopy(g_sCallAdmin_Webhook, sizeof(g_sCallAdmin_Webhook), newVal); + } + else if(convar == g_cCallAdmin_BotName) + { + strcopy(g_sCallAdmin_BotName, sizeof(g_sCallAdmin_BotName), newVal); + } + else if(convar == g_cCallAdmin_BotAvatar) + { + strcopy(g_sCallAdmin_BotAvatar, sizeof(g_sCallAdmin_BotAvatar), newVal); + } + else if(convar == g_cCallAdmin_Color) + { + strcopy(g_sCallAdmin_Color, sizeof(g_sCallAdmin_Color), newVal); + } + else if(convar == g_cCallAdmin_Content) + { + strcopy(g_sCallAdmin_Content, sizeof(g_sCallAdmin_Content), newVal); + } + else if(convar == g_cCallAdmin_FooterIcon) + { + strcopy(g_sCallAdmin_FooterIcon, sizeof(g_sCallAdmin_FooterIcon), newVal); + } + else if(convar == g_cDNSServerIP) + { + strcopy(g_sServerIP, sizeof(g_sServerIP), newVal); + ServerIP(g_sServerIP, sizeof(g_sServerIP)); + } +} + +public void CallAdmin_OnServerDataChanged(ConVar convar, ServerData type, const char[] oldVal, const char[] newVal) +{ + if (type == ServerData_HostName) + CallAdmin_GetHostName(g_sServerName, sizeof(g_sServerName)); +} + +public void OnPluginStart() +{ + #if defined USE_AutoExecConfig + AutoExecConfig_SetFile("Discord-Utilities"); + AutoExecConfig_SetCreateFile(true); + + g_cCallAdmin_Webhook = AutoExecConfig_CreateConVar("sm_du_calladmin_webhook", "", "Webhook for calladmin reports and report handled print. Blank to disable.", FCVAR_PROTECTED); + g_cCallAdmin_BotName = AutoExecConfig_CreateConVar("sm_du_calladmin_botname", "Discord Utilities", "BotName for calladmin. Blank to use webhook name."); + g_cCallAdmin_BotAvatar = AutoExecConfig_CreateConVar("sm_du_calladmin_avatar", "", "Avatar link for calladmin bot. Blank to use webhook avatar."); + g_cCallAdmin_Color = AutoExecConfig_CreateConVar("sm_du_calladmin_color", "#ff9911", "Color for embed message of calladmin."); + g_cCallAdmin_Content = AutoExecConfig_CreateConVar("sm_du_calladmin_content", "When in-game type !calladmin_handle in chat to handle this report.", "Content for embed message of calladmin. Blank to disable."); + g_cCallAdmin_FooterIcon = AutoExecConfig_CreateConVar("sm_du_calladmin_footericon", "", "Link to footer icon for calladmin. Blank for no footer icon."); + + g_cDNSServerIP = AutoExecConfig_CreateConVar("sm_du_dns_ip", "", "DNS IP address of your game server. Blank to use real IP."); + + #else + g_cCallAdmin_Webhook = CreateConVar("sm_du_calladmin_webhook", "", "Webhook for calladmin reports and report handled print. Blank to disable.", FCVAR_PROTECTED); + g_cCallAdmin_BotName = CreateConVar("sm_du_calladmin_botname", "Discord Utilities", "BotName for calladmin. Blank to use webhook name."); + g_cCallAdmin_BotAvatar = CreateConVar("sm_du_calladmin_avatar", "", "Avatar link for calladmin bot. Blank to use webhook avatar."); + g_cCallAdmin_Color = CreateConVar("sm_du_calladmin_color", "#ff9911", "Color for embed message of calladmin."); + g_cCallAdmin_Content = CreateConVar("sm_du_calladmin_content", "When in-game type !calladmin_handle in chat to handle this report.", "Content for embed message of calladmin. Blank to disable."); + g_cCallAdmin_FooterIcon = CreateConVar("sm_du_calladmin_footericon", "", "Link to footer icon for calladmin. Blank for no footer icon."); + + g_cDNSServerIP = CreateConVar("sm_du_dns_ip", "", "DNS IP address of your game server. Blank to use real IP."); + + AutoExecConfig(true, "Discord-Utilities"); + #endif + + HookConVarChange(g_cCallAdmin_Webhook, OnSettingsChanged); + HookConVarChange(g_cCallAdmin_BotName, OnSettingsChanged); + HookConVarChange(g_cCallAdmin_BotAvatar, OnSettingsChanged); + HookConVarChange(g_cCallAdmin_Color, OnSettingsChanged); + HookConVarChange(g_cCallAdmin_Content, OnSettingsChanged); + HookConVarChange(g_cCallAdmin_FooterIcon, OnSettingsChanged); + + LoadTranslations("Discord-Utilities.phrases"); +} + +void LoadCvars() + { + g_cCallAdmin_Webhook.GetString(g_sCallAdmin_Webhook, sizeof(g_sCallAdmin_Webhook)); + g_cCallAdmin_BotName.GetString(g_sCallAdmin_BotName, sizeof(g_sCallAdmin_BotName)); + g_cCallAdmin_BotAvatar.GetString(g_sCallAdmin_BotAvatar, sizeof(g_sCallAdmin_BotAvatar)); + g_cCallAdmin_Color.GetString(g_sCallAdmin_Color, sizeof(g_sCallAdmin_Color)); + g_cCallAdmin_Content.GetString(g_sCallAdmin_Content, sizeof(g_sCallAdmin_Content)); + g_cCallAdmin_FooterIcon.GetString(g_sCallAdmin_FooterIcon, sizeof(g_sCallAdmin_FooterIcon)); + + g_cDNSServerIP.GetString(g_sServerIP, sizeof(g_sServerIP)); + ServerIP(g_sServerIP, sizeof(g_sServerIP)); + } + +public void CallAdmin_OnReportHandled(int client, int id) +{ + if(StrEqual(g_sCallAdmin_Webhook, "")) + { + return; + } + if(!g_bCallAdmin) + { + return; + } + if (id != g_iLastReportID) + { + return; + } + + char clientName[MAX_NAME_LENGTH], clientAuth[32], clientAuth2[32]; + GetClientName(client, clientName, sizeof(clientName)); + GetClientAuthId(client, AuthId_SteamID64, clientAuth, sizeof(clientAuth)); + GetClientAuthId(client, AuthId_Steam2, clientAuth2, sizeof(clientAuth2)); + Discord_EscapeString(clientName, sizeof(clientName)); + + DiscordWebHook hook = new DiscordWebHook( g_sCallAdmin_Webhook ); + hook.SlackMode = true; + if(g_sCallAdmin_BotName[0]) + { + hook.SetUsername( g_sCallAdmin_BotName ); + } + if(g_sCallAdmin_BotAvatar[0]) + { + hook.SetAvatar( g_sCallAdmin_BotAvatar ); + } + + MessageEmbed embed = new MessageEmbed(); + + if(StrContains(g_sCallAdmin_Color, "#") != -1) + { + embed.SetColor(g_sCallAdmin_Color); + } + else + { + LogError("[Discord-Utilities] CallAdmin ReportHandled is using default color as you've set invalid CallAdmin ReportHandled color."); + embed.SetColor(DEFAULT_COLOR); + } + + char buffer[512], trans[64]; + Format( trans, sizeof( trans ), "%T", "CallAdminReportHandledTitle", LANG_SERVER); + embed.SetTitle( trans ); + + Format( trans, sizeof( trans ), "%T", "CallAdminReportHandlerName", LANG_SERVER); + Format( buffer, sizeof( buffer ), "[%s](http://www.steamcommunity.com/profiles/%s)(%s)", clientName, clientAuth, clientAuth2 ); + embed.AddField( trans, buffer, true ); + + Format( trans, sizeof( trans ), "%T", "CallAdminReportIDField", LANG_SERVER); + Format(buffer, sizeof(buffer), "%d", g_iLastReportID); + embed.AddField( trans, buffer, true ); + + Format( trans, sizeof( trans ), "%T", "DirectConnectField", LANG_SERVER); + Format( buffer, sizeof( buffer ), "steam://connect/%s", g_sServerIP ); + embed.AddField( trans, buffer, false ); + + if(g_sCallAdmin_FooterIcon[0]) + { + embed.SetFooterIcon( g_sCallAdmin_FooterIcon ); + } + Format( buffer, sizeof( buffer ), "%T", "ServerField", LANG_SERVER, g_sServerName ); + embed.SetFooter( buffer ); + + hook.Embed( embed ); + hook.Send(); + delete hook; +} + + +public void CallAdmin_OnReportPost(int client, int target, const char[] reason) +{ + if(StrEqual(g_sCallAdmin_Webhook, "")) + { + return; + } + if(!g_bCallAdmin) + { + return; + } + char sReason[(REASON_MAX_LENGTH + 1) * 2]; + strcopy(sReason, sizeof(sReason), reason); + Discord_EscapeString(sReason, sizeof(sReason)); + + char clientAuth[21]; + char clientAuth2[21]; + char clientName[(MAX_NAME_LENGTH + 1) * 2]; + + if (client == REPORTER_CONSOLE) + { + Format(clientName, sizeof(clientName), "%T", "SERVER", LANG_SERVER); + Format(clientAuth, sizeof(clientAuth), "%T", "CONSOLE", LANG_SERVER); + } + else + { + GetClientAuthId(client, AuthId_SteamID64, clientAuth, sizeof(clientAuth)); + GetClientAuthId(client, AuthId_Steam2, clientAuth2, sizeof(clientAuth2)); + GetClientName(client, clientName, sizeof(clientName)); + Discord_EscapeString(clientName, sizeof(clientName)); + } + + char targetAuth[21]; + char targetAuth2[21]; + char targetName[(MAX_NAME_LENGTH + 1) * 2]; + + GetClientAuthId(target, AuthId_SteamID64, targetAuth, sizeof(targetAuth)); + GetClientAuthId(target, AuthId_Steam2, targetAuth2, sizeof(targetAuth2)); + GetClientName(target, targetName, sizeof(targetName)); + Discord_EscapeString(targetName, sizeof(targetName)); + + int index = g_aCallAdmin_ReportedList.FindString(targetAuth); + + if(index != -1) + { + return; + } + + g_aCallAdmin_ReportedList.PushString(targetAuth); + + DiscordWebHook hook = new DiscordWebHook( g_sCallAdmin_Webhook ); + hook.SlackMode = true; + if(g_sCallAdmin_BotName[0]) + { + hook.SetUsername( g_sCallAdmin_BotName ); + } + if(g_sCallAdmin_BotAvatar[0]) + { + hook.SetAvatar( g_sCallAdmin_BotAvatar ); + } + + MessageEmbed embed = new MessageEmbed(); + + if(StrContains(g_sCallAdmin_Color, "#") != -1) + { + embed.SetColor(g_sCallAdmin_Color); + } + else + { + LogError("[Discord-Utilities] CallAdmin ReportPost is using default color as you've set invalid CallAdmin ReportPost color."); + embed.SetColor(DEFAULT_COLOR); + } + + g_iLastReportID = CallAdmin_GetReportID(); + + char buffer[512], trans[64]; + Format( trans, sizeof( trans ), "%T", "CallAdminReportTitle", LANG_SERVER); + embed.SetTitle( buffer ); + + if (client != REPORTER_CONSOLE) + { + Format( buffer, sizeof( buffer ), "[%s](http://www.steamcommunity.com/profiles/%s) (%s)", clientName, clientAuth, clientAuth2 ); + } + else + { + Format( buffer, sizeof( buffer ), "%s", clientName ); + } + Format(trans, sizeof(trans), "%T", "ReporterField", LANG_SERVER); + embed.AddField( trans, buffer, true ); + + Format(trans, sizeof(trans), "%T", "TargetField", LANG_SERVER); + Format( buffer, sizeof( buffer ), "[%s](http://www.steamcommunity.com/profiles/%s) (%s)", targetName, targetAuth, targetAuth2 ); + embed.AddField( trans, buffer, true ); + + Format(trans, sizeof(trans), "%T", "ReasonField", LANG_SERVER); + embed.AddField( trans, sReason, true ); + + Format(trans, sizeof(trans), "%T", "CallAdminReportIDField", LANG_SERVER); + Format(buffer, sizeof(buffer), "%d", g_iLastReportID); + + embed.AddField( trans, buffer, false ); + + Format(trans, sizeof(trans), "%T", "DirectConnectField", LANG_SERVER); + Format( buffer, sizeof( buffer ), "steam://connect/%s", g_sServerIP ); + embed.AddField( trans, buffer, true ); + + if(g_sCallAdmin_FooterIcon[0]) + { + embed.SetFooterIcon( g_sCallAdmin_FooterIcon ); + } + Format( buffer, sizeof( buffer ), "%T", "ServerField", LANG_SERVER, g_sServerName ); + embed.SetFooter( buffer ); + + if(g_sCallAdmin_Content[0]) + { + hook.SetContent( g_sCallAdmin_Content ); + } + + hook.Embed( embed ); + hook.Send(); + delete hook; +} + +stock void Discord_EscapeString(char[] string, int maxlen, bool name = false) +{ + if(name) + { + ReplaceString(string, maxlen, "everyone", "everyone"); + ReplaceString(string, maxlen, "here", "here"); + ReplaceString(string, maxlen, "discordtag", "discordtag"); + } + ReplaceString(string, maxlen, "#", "#"); + ReplaceString(string, maxlen, "@", "@"); + //ReplaceString(string, maxlen, ":", ""); + ReplaceString(string, maxlen, "_", "ˍ"); + ReplaceString(string, maxlen, "'", "'"); + ReplaceString(string, maxlen, "`", "'"); + ReplaceString(string, maxlen, "~", "∽"); + ReplaceString(string, maxlen, "\"", """); +} + +void ServerIP(char[] sIP, int size) +{ + if(sIP[0]) + { + return; + } + int ip[4]; + int iServerPort = FindConVar("hostport").IntValue; + SteamWorks_GetPublicIP(ip); + if(SteamWorks_GetPublicIP(ip)) + { + Format(sIP, size, "%d.%d.%d.%d:%d", ip[0], ip[1], ip[2], ip[3], iServerPort); + } + else + { + int iServerIP = FindConVar("hostip").IntValue; + Format(sIP, size, "%d.%d.%d.%d:%d", iServerIP >> 24 & 0x000000FF, iServerIP >> 16 & 0x000000FF, iServerIP >> 8 & 0x000000FF, iServerIP & 0x000000FF, iServerPort); + } +} From 4f5638e3b29f1d9d0b6f637ac594651ac9ecf9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AiDN=E2=84=A2?= <45371311+originalaidn@users.noreply.github.com> Date: Fri, 28 Jan 2022 13:35:15 +0100 Subject: [PATCH 4/9] edited forwards, calladmin code from Cruze03 --- scripting/discord_utilities/forwards.sp | 4 ++-- scripting/du_calladmin.sp | 6 +++--- translations/Discord-Utilities.phrases.txt | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripting/discord_utilities/forwards.sp b/scripting/discord_utilities/forwards.sp index 27f7c13..fb94976 100644 --- a/scripting/discord_utilities/forwards.sp +++ b/scripting/discord_utilities/forwards.sp @@ -234,8 +234,8 @@ public Action Command_ViewId(int client, int args) } else { - CPrintToChat(client, "%s - %T", g_sServerPrefix, "AlreadyVerified"); - CPrintToChat(client, "%s - %T", g_sServerPrefix, "CanChange"); + CPrintToChat(client, "%s %T", g_sServerPrefix, "AlreadyVerified", client); + CPrintToChat(client, "%s %T", g_sServerPrefix, "CanChange", client); } diff --git a/scripting/du_calladmin.sp b/scripting/du_calladmin.sp index ff8fcc5..27533bc 100644 --- a/scripting/du_calladmin.sp +++ b/scripting/du_calladmin.sp @@ -29,10 +29,10 @@ bool g_bCallAdmin; public Plugin myinfo = { name = "Discord Utilities - Calladmin module", - author = "AiDN™", - description = "Calladmin module for the Discord Utilities", + author = "AiDN™ & Cruze03", + description = "Calladmin module for the Discord Utilities, code from Cruze03", version = "1.0", - url = "https://steamcommunity.com/id/originalaidn" + url = "https://steamcommunity.com/id/originalaidn & https://github.com/Cruze03/discord-utilities" }; public void OnLibraryAdded(const char[] szLibrary) diff --git a/translations/Discord-Utilities.phrases.txt b/translations/Discord-Utilities.phrases.txt index 701e1c7..e5bd057 100644 --- a/translations/Discord-Utilities.phrases.txt +++ b/translations/Discord-Utilities.phrases.txt @@ -467,11 +467,11 @@ "AlreadyVerified" { - "en" "You are already verified. Enjoy your benefits :)" + "en" "- You are already verified. Enjoy your benefits :)" } "CanChange" { - "en" "You can change your verified discord account using {LIME}!unverify{DEFAULT} and {LIME}!verify{DEFAULT} again" + "en" "- You can change your verified discord account using {LIME}!unverify{DEFAULT} and {LIME}!verify{DEFAULT} again" } } From 768de94ba69182dc7a7aecb45c94921bc32169fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AiDN=E2=84=A2?= <45371311+originalaidn@users.noreply.github.com> Date: Mon, 21 Feb 2022 22:42:06 +0100 Subject: [PATCH 5/9] calladmin, chatrelay, bugreport modules, some edit / bugfix in main plugin, removed plugins folder --- cfg/sourcemod/Discord-Utilities.cfg | 330 +++++ plugins/discord_api.smx | Bin 26616 -> 0 bytes plugins/discord_utilities.smx | Bin 37337 -> 0 bytes scripting/discord_utilities.sp | 1 - scripting/discord_utilities/forwards.sp | 11 +- scripting/discord_utilities/globals.sp | 11 +- scripting/discord_utilities/helpers.sp | 14 +- scripting/discord_utilities/sql.sp | 4 +- scripting/du_bugreport.sp | 290 +++++ scripting/du_calladmin.sp | 6 +- scripting/du_chatrelay.sp | 741 +++++++++++ scripting/include/bugreport.inc | 42 + scripting/include/smjansson.inc | 1328 ++++++++++++++++++++ translations/Discord-Utilities.phrases.txt | 10 +- 14 files changed, 2770 insertions(+), 18 deletions(-) create mode 100644 cfg/sourcemod/Discord-Utilities.cfg delete mode 100644 plugins/discord_api.smx delete mode 100644 plugins/discord_utilities.smx create mode 100644 scripting/du_bugreport.sp create mode 100644 scripting/du_chatrelay.sp create mode 100644 scripting/include/bugreport.inc create mode 100644 scripting/include/smjansson.inc diff --git a/cfg/sourcemod/Discord-Utilities.cfg b/cfg/sourcemod/Discord-Utilities.cfg new file mode 100644 index 0000000..7ecf0e1 --- /dev/null +++ b/cfg/sourcemod/Discord-Utilities.cfg @@ -0,0 +1,330 @@ +// This file was auto-generated by AutoExecConfig v0.1.5 (https://forums.alliedmods.net/showthread.php?t=204254) +// ConVars for plugin "discord_utilities.smx" + + +// Webhook for calladmin reports and report handled print. Blank to disable. +// - +// Default: "" +sm_du_calladmin_webhook "" + +// BotName for calladmin. Blank to use webhook name. +// - +// Default: "Discord Utilities" +sm_du_calladmin_botname "" + +// Avatar link for calladmin bot. Blank to use webhook avatar. +// - +// Default: "" +sm_du_calladmin_avatar "" + +// Color for embed message of calladmin. +// - +// Default: "#ff9911" +sm_du_calladmin_color "#ff9911" + +// Content for embed message of calladmin. Blank to disable. +// - +// Default: "When in-game type !calladmin_handle in chat to handle this report." +sm_du_calladmin_content "When in-game type !calladmin_handle in chat to handle this report." + +// Link to footer icon for calladmin. Blank for no footer icon. +// - +// Default: "" +sm_du_calladmin_footericon "" + +// Webhook for bugreport reports. Blank to disable. +// - +// Default: "" +sm_du_bugreport_webhook "" + +// BotName for bugreport. Blank to use webhook name. +// - +// Default: "Discord Utilities" +sm_du_bugreport_botname "Discord Utilities" + +// Avatar link for bugreport bot. Blank to use webhook avatar. +// - +// Default: "" +sm_du_bugreport_avatar "" + +// Color for embed message of bugreport. +// - +// Default: "#ff9911" +sm_du_bugreport_color "#ff9911" + +// Content for embed message of bugreport. Blank to disable. +// - +// Default: "" +sm_du_bugreport_content "" + +// Link to footer icon for bugreport. Blank for no footer icon. +// - +// Default: "" +sm_du_bugreport_footericon "" + +// Webhook for sourcebans. Blank to disable. +// - +// Default: "" +sm_du_sourcebans_webhook "" + +// BotName for sourcebans. Blank to use webhook name. +// - +// Default: "Discord Utilities" +sm_du_sourcebans_botname "Discord Utilities" + +// Avatar link for sourcebans bot. Blank to use webhook avatar. +// - +// Default: "" +sm_du_sourcebans_avatar "" + +// Color for embed message of sourcebans. +// - +// Default: "#0E40E6" +sm_du_sourcebans_color "#0E40E6" + +// Color for embed message of sourcebans when permanent banned. +// - +// Default: "#f00000" +sm_du_sourcebans_perma_color "#f00000" + +// Content for embed message of sourcebans. Blank to disable. +// - +// Default: "" +sm_du_sourcebans_content "" + +// Link to footer icon for sourcebans. Blank for no footer icon. +// - +// Default: "" +sm_du_sourcebans_footericon "" + +// Webhook for sourcecomms. Blank to disable. +// - +// Default: "" +sm_du_sourcecomms_webhook "" + +// BotName for sourcecomms. Blank to use webhook name. +// - +// Default: "Discord Utilities" +sm_du_sourcecomms_botname "Discord Utilities" + +// Avatar link for sourcecomms bot. Blank to use webhook avatar. +// - +// Default: "" +sm_du_sourcecomms_avatar "" + +// Color for embed message of sourcecomms. +// - +// Default: "#FF69B4" +sm_du_sourcecomms_color "#FF69B4" + +// Color for embed message of sourcecomms when permanent banned. +// - +// Default: "#f00000" +sm_du_sourcecomms_perma_color "#f00000" + +// Content for embed message of sourcecomms. Blank to disable. +// - +// Default: "" +sm_du_sourcecomms_content "" + +// Link to footer icon for sourcecomms. Blank for no footer icon. +// - +// Default: "" +sm_du_sourcecomms_footericon "" + +// Webhook for map notification. Blank to disable. +// - +// Default: "" +sm_du_map_webhook "" + +// BotName for map notification. Blank to use webhook name. +// - +// Default: "Discord Utilities" +sm_du_map_botname "Discord Utilities" + +// Avatar link for map notification bot. Blank to use webhook avatar. +// - +// Default: "" +sm_du_map_avatar "" + +// Color for embed message of map notification. +// - +// Default: "#6a0dad" +sm_du_map_color "#6a0dad" + +// Content for embed message of map notification. Blank to disable. +// - +// Default: "" +sm_du_map_content "" + +// Seconds to wait after mapstart to send the map notification webhook. 0 for no delay. +// - +// Default: "25" +sm_du_map_delay "25" + +// Thumbnail link for map notification. Make sure "MAPNAME" is added in the link just like the default value. Blank for none. +// - +// Default: "https://image.gametracker.com/images/maps/160x120/csgo/MAPNAME.jpg" +sm_du_map_thumbnail "https://image.gametracker.com/images/maps/160x120/csgo/MAPNAME.jpg" + +// Webhook for game server => discord server chat messages. Blank to disable. +// - +// Default: "" +sm_du_chat_webhook "" + +// Text that shouldn't appear in gameserver => discord server chat messages. Separate it with ", " +// - +// Default: "rtv, nominate" +sm_du_chat_blocklist "rtv, nominate" + +// Webhook for game server => discord server chat messages where chat messages are to admins (say_team with @ / sm_chat). Blank to disable. +// - +// Default: "" +sm_du_adminchat_webhook "" + +// Text that shouldn't appear in gameserver => discord server where chat messages are to admin. Separate it with ", " +// - +// Default: "rtv, nominate" +sm_du_adminchat_blocklist "rtv, nominate" + +// Webhook for channel where all admin commands are logged. Blank to disable. +// - +// Default: "" +sm_du_adminlog_webhook "" + +// Log with this string will be ignored. Separate it with ", " +// - +// Default: "slapped, firebombed" +sm_du_adminlog_blocklist "" + +// Channel ID for verfication. Blank to disable. +// - +// Default: "" +sm_du_verfication_channelid "" + +// Channel ID for discord server => game server messages. Blank to disable. +// - +// Default: "" +sm_du_chat_channelid "" + +// Guild ID of your discord server. Blank to disable. Needed for verification module. +// - +// Default: "" +sm_du_verification_guildid "" + +// Role ID to give to user when user is verified. Blank to give no role. Verification module needs to be running. +// - +// Default: "" +sm_du_verification_roleid "826136130202107934" + +// Steam API Key (https://steamcommunity.com/dev/apikey). Needed for gameserver => discord server relay and/or admin chat relay and/or Admin logs. Blank will show default author icon of discord. +// - +// Default: "" +sm_du_apikey "" + +// Bot Token. Needed for discord server => gameserver and/or verification module. +// - +// Default: "" +sm_du_bottoken "" + +// DNS IP address of your game server. Blank to use real IP. +// - +// Default: "" +sm_du_dns_ip "" + +// Time in seconds between verifying accounts. 0 for no check. +// - +// Default: "300" +sm_du_accounts_check_interval "300" + +// Use SWGM config file for restricting commands. +// - +// Default: "0" +sm_du_use_swgm_file "0" + +// Display timestamps? Used in gameserver => discord server relay AND AdminLog +// - +// Default: "0" +sm_du_display_timestamps "0" + +// Increase this with every server you put this plugin in. Prevents multiple replies from the bot in verfication channel. +// - +// Default: "1" +sm_du_server_id "1" + +// Is this the primary server in the verification channel? Only this server will respond to generic queries. +// - +// Default: "1" +sm_du_server_primary "1" + +// Command to use in text channel. +// - +// Default: "!link" +sm_du_link_command "!link" + +// Command to view id. +// - +// Default: "sm_viewid" +sm_du_viewid_command "sm_viewid" + +// Command to unlink. +// - +// Default: "sm_unlink" +sm_du_unlink_command "sm_unlink" + +// Invite link of your discord server. +// - +// Default: "https://discord.gg/83g5xcE" +sm_du_link "https://discord.gg/83g5xcE" + +// Prefix for discord messages. +// - +// Default: "[{lightgreen}Discord{default}]" +sm_du_discord_prefix "{lightgreen}[Discord]{default}" + +// Prefix for chat messages. +// - +// Default: "[{lightgreen}Discord-Utilities{default}]" +sm_du_server_prefix "{lightgreen}[Discord-Utilities]{default}" + +// Section name in databases.cfg. +// - +// Default: "du" +sm_du_database_name "du" + +// Table Name. +// - +// Default: "du_users" +sm_du_table_name "du_users" + +// Prune database with players whose last connect is X DAYS and he is not member of discord server. 0 to disable. +// - +// Default: "60" +sm_du_prune_days "60" + +// Enable log for revoke? +// - +// Default: "1" +sm_du_logrevoke_enabled" "1" + +// Enable to create logs/dsmembers.json? +// - +// Default: "1" +sm_du_dsmembersfile_enabled" "1" + +// Thumbnail link for map notification. Make sure "MAPNAME" is added in the link just like the default value. Blank for none. +// - +// Default: "" +https://image.gametracker.com/images/maps/160x120/csgo/MAPNAME.jpg "" + +// 0 - Only "say_team with @ / sm_chat" +// 0b - "say_team with @ / sm_chat" with discord to game server chat to admin. +// Any admin flag - Show messages of specific flag in channel. +// - +// Default: "0b" +sm_du_adminchat_mode "0b" + +// Channel ID for discord server => game server messages only of admins. Blank to disable. +// - +// Default: "" +sm_du_adminchat_channelid "" diff --git a/plugins/discord_api.smx b/plugins/discord_api.smx deleted file mode 100644 index c61fdcd7c6e588f8e5b9cc33c2b48faff34f2f1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26616 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

%9c5q&b@gpz514D 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 diff --git a/plugins/discord_utilities.smx b/plugins/discord_utilities.smx deleted file mode 100644 index adf28c723d073f7dc613afeb443c7e904db2d930..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37337 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 diff --git a/scripting/discord_utilities.sp b/scripting/discord_utilities.sp index 1402873..d43574c 100644 --- a/scripting/discord_utilities.sp +++ b/scripting/discord_utilities.sp @@ -60,7 +60,6 @@ public void OnPluginStart() CreateCvars(); - RegConsoleCmd("sm_unlink", Cmd_Unlink); RegConsoleCmd("sm_unverify", Cmd_Unlink); LoadTranslations("Discord-Utilities.phrases"); diff --git a/scripting/discord_utilities/forwards.sp b/scripting/discord_utilities/forwards.sp index fb94976..00007c0 100644 --- a/scripting/discord_utilities/forwards.sp +++ b/scripting/discord_utilities/forwards.sp @@ -32,6 +32,7 @@ public void OnConfigsExecuted() if(!CommandExists(g_sViewIDCommand)) { RegConsoleCmd(g_sViewIDCommand, Command_ViewId); + RegConsoleCmd(g_sUnLinkCommand, Cmd_Unlink); RegConsoleCmd("sm_verify", Command_ViewId); } CreateBot(); @@ -126,6 +127,10 @@ public int OnSettingsChanged(ConVar convar, const char[] oldVal, const char[] ne { strcopy(g_sViewIDCommand, sizeof(g_sViewIDCommand), newVal); } + else if(convar == g_cUnLinkCommand) + { + strcopy(g_sUnLinkCommand, sizeof(g_sUnLinkCommand), newVal); + } else if(convar == g_cInviteLink) { strcopy(g_sInviteLink, sizeof(g_sInviteLink), newVal); @@ -234,8 +239,8 @@ public Action Command_ViewId(int client, int args) } else { - CPrintToChat(client, "%s %T", g_sServerPrefix, "AlreadyVerified", client); - CPrintToChat(client, "%s %T", g_sServerPrefix, "CanChange", client); + CPrintToChat(client, "%s - %T", g_sServerPrefix, "AlreadyVerified", client); + CPrintToChat(client, "%s - %T", g_sServerPrefix, "CanChange", client, ChangePartsInString(g_sUnLinkCommand, "sm_", "!"), ChangePartsInString(g_sViewIDCommand, "sm_", "!")); } @@ -245,7 +250,7 @@ public Action Command_ViewId(int client, int args) public Action Check(int client, const char[] command, int args) { - if(!client || client > MaxClients) + if(!client || client > MaxClients && IsClientInGame(client)) { return Plugin_Continue; } diff --git a/scripting/discord_utilities/globals.sp b/scripting/discord_utilities/globals.sp index 2434ac3..9ce1ac8 100644 --- a/scripting/discord_utilities/globals.sp +++ b/scripting/discord_utilities/globals.sp @@ -1,23 +1,24 @@ -#define PLUGIN_VERSION "2.5-betafixslow" +#define PLUGIN_VERSION "2.6" #define PLUGIN_NAME "Discord Utilities" -#define PLUGIN_AUTHOR "Cruze" +#define PLUGIN_AUTHOR "Cruze & xSlow & AiDN™" #define PLUGIN_DESC "Utilities that can be used to integrate gameserver to discord server I guess?" -#define PLUGIN_URL "https://github.com/Cruze03/discord-utilities | http://www.steamcommunity.com/profiles/76561198132924835" +#define PLUGIN_URL "https://github.com/Cruze03/discord-utilities | http://www.steamcommunity.com/profiles/76561198132924835 & http://steamcommunity.com/profiles/76561198192410833 & http://steamcommunity.com/profiles/76561198069218105" //#define USE_AutoExecConfig ConVar g_cVerificationChannelID, g_cGuildID, g_cRoleID; ConVar g_cBotToken, g_cCheckInterval, g_cUseSWGM, g_cServerID; -ConVar g_cLinkCommand, g_cViewIDCommand, g_cInviteLink; +ConVar g_cLinkCommand, g_cViewIDCommand, g_cUnLinkCommand, g_cInviteLink; ConVar g_cDiscordPrefix, g_cServerPrefix; ConVar g_cDatabaseName, g_cTableName, g_cPruneDays; ConVar g_cPrimaryServer; +ConVar g_cLogRevokeEnabled, g_cDsMembersEnabled; char g_sVerificationChannelID[20], g_sGuildID[20], g_sRoleID[20]; char g_sBotToken[60]; -char g_sLinkCommand[20], g_sViewIDCommand[20], g_sInviteLink[30]; +char g_sLinkCommand[20], g_sViewIDCommand[20], g_sUnLinkCommand[20], g_sInviteLink[30]; char g_sDiscordPrefix[128], g_sServerPrefix[128]; char g_sTableName[32]; diff --git a/scripting/discord_utilities/helpers.sp b/scripting/discord_utilities/helpers.sp index cbd5301..7c2068a 100644 --- a/scripting/discord_utilities/helpers.sp +++ b/scripting/discord_utilities/helpers.sp @@ -143,7 +143,7 @@ public int GetMembersData(const char[] data, any dp) public void OnGetMembersAll(Handle hMemberList) { //DeleteFile("addons/sourcemod/logs/dsmembers.json") - json_dump_file(hMemberList, "addons/sourcemod/logs/dsmembers.json"); + if(g_cDsMembersEnabled.IntValue ==1) json_dump_file(hMemberList, "addons/sourcemod/logs/dsmembers.json"); //LogToFile("addons/sourcemod/logs/dsmembers.json", "OnGetMembersAll size %d", json_array_size(hMemberList)); Call_StartForward(g_hOnMemberDataDumped); @@ -200,7 +200,7 @@ public void OnGetMembersAll(Handle hMemberList) bUpdate[x] = true; CPrintToChat(x, "%s %T", g_sServerPrefix, "DiscordRevoked", x); - LogToFile("addons/sourcemod/logs/dsmembers_revoke.log", "Player %L got revoked. Memberlist json size: %d", x, json_array_size(hMemberList)); + if(g_cLogRevokeEnabled.IntValue == 1) LogToFile("addons/sourcemod/logs/dsmembers_revoke.log", "Player %L got revoked. Memberlist json size: %d", x, json_array_size(hMemberList)); Call_StartForward(g_hOnAccountRevoked); Call_PushCell(x); @@ -309,6 +309,7 @@ void CreateCvars() g_cLinkCommand = AutoExecConfig_CreateConVar("sm_du_link_command", "!link", "Command to use in text channel."); g_cViewIDCommand = AutoExecConfig_CreateConVar("sm_du_viewid_command", "sm_viewid", "Command to view id."); + g_cUnLinkCommand = AutoExecConfig_CreateConVar("sm_du_unlink_command", "sm_unlink", "Command to unlink."); g_cInviteLink = AutoExecConfig_CreateConVar("sm_du_link", "https://discord.gg/83g5xcE", "Invite link of your discord server."); g_cDiscordPrefix = AutoExecConfig_CreateConVar("sm_du_discord_prefix", "[{lightgreen}Discord{default}]", "Prefix for discord messages."); @@ -318,6 +319,9 @@ void CreateCvars() g_cTableName = AutoExecConfig_CreateConVar("sm_du_table_name", "du_users", "Table Name."); g_cPruneDays = AutoExecConfig_CreateConVar("sm_du_prune_days", "60", "Prune database with players whose last connect is X DAYS and he is not member of discord server. 0 to disable."); + g_cLogRevokeEnabled = AutoExecConfig_CreateConVar("sm_du_logrevoke_enabled", "1", "Enable log for revoke?"); + g_cDsMembersEnabled = AutoExecConfig_CreateConVar("sm_du_dsmembersfile_enabled", "1", "Enable to create logs/dsmembers.json?"); + AutoExecConfig_ExecuteFile(); AutoExecConfig_CleanFile(); @@ -334,6 +338,7 @@ void CreateCvars() g_cLinkCommand = CreateConVar("sm_du_link_command", "!link", "Command to use in text channel."); g_cViewIDCommand = CreateConVar("sm_du_viewid_command", "sm_viewid", "Command to view id."); + g_cUnLinkCommand = CreateConVar("sm_du_unlink_command", "sm_unlink", "Command to unlink."); g_cInviteLink = CreateConVar("sm_du_link", "https://discord.gg/83g5xcE", "Invite link of your discord server."); g_cDiscordPrefix = CreateConVar("sm_du_discord_prefix", "[{lightgreen}Discord{default}]", "Prefix for discord messages."); @@ -343,6 +348,9 @@ void CreateCvars() g_cTableName = CreateConVar("sm_du_table_name", "du_users", "Table Name."); g_cPruneDays = CreateConVar("sm_du_prune_days", "60", "Prune database with players whose last connect is X DAYS and he is not member of discord server. 0 to disable."); + g_cLogRevokeEnabled = CreateConVar("sm_du_logrevoke_enabled", "1", "Enable log for revoke?"); + g_cDsMembersEnabled = CreateConVar("sm_du_dsmembersfile_enabled", "1", "Enable to create logs/dsmembers.json?"); + AutoExecConfig(true, "Discord-Utilities"); #endif @@ -354,6 +362,7 @@ void CreateCvars() HookConVarChange(g_cLinkCommand, OnSettingsChanged); HookConVarChange(g_cViewIDCommand, OnSettingsChanged); + HookConVarChange(g_cUnLinkCommand, OnSettingsChanged); HookConVarChange(g_cInviteLink, OnSettingsChanged); HookConVarChange(g_cDiscordPrefix, OnSettingsChanged); @@ -370,6 +379,7 @@ void LoadCvars() g_cLinkCommand.GetString(g_sLinkCommand, sizeof(g_sLinkCommand)); g_cViewIDCommand.GetString(g_sViewIDCommand, sizeof(g_sViewIDCommand)); + g_cUnLinkCommand.GetString(g_sUnLinkCommand, sizeof(g_sUnLinkCommand)); g_cInviteLink.GetString(g_sInviteLink, sizeof(g_sInviteLink)); g_cDiscordPrefix.GetString(g_sDiscordPrefix, sizeof(g_sDiscordPrefix)); diff --git a/scripting/discord_utilities/sql.sp b/scripting/discord_utilities/sql.sp index 73924fc..4da82c6 100644 --- a/scripting/discord_utilities/sql.sp +++ b/scripting/discord_utilities/sql.sp @@ -298,8 +298,8 @@ public int SQLQuery_UnlinkAccount_Callback(Handle owner, Handle hndl, char [] er g_sUserID[client][0] = '\0'; - LogToFile("addons/sourcemod/logs/dsmembers_revoke.log", "Player %L unverified himself.", client); - CPrintToChat(client, "%s - You succesfully unlinked your account.", g_sServerPrefix); + if(g_cLogRevokeEnabled.IntValue == 1) LogToFile("addons/sourcemod/logs/dsmembers_revoke.log", "Player %L unverified himself.", client); + CPrintToChat(client, "%s - %T", g_sServerPrefix, "SuccessfullyUnlink", client); } public int SQLQuery_UpdatePlayer(Handle owner, Handle hndl, char [] error, any data) diff --git a/scripting/du_bugreport.sp b/scripting/du_bugreport.sp new file mode 100644 index 0000000..0264868 --- /dev/null +++ b/scripting/du_bugreport.sp @@ -0,0 +1,290 @@ +#include +#include +#include +#include +#include + +#include + +#pragma dynamic 500000 +#pragma newdecls required +#pragma semicolon 1 + +ConVar g_cBugReport_Webhook, g_cBugReport_BotName, g_cBugReport_BotAvatar, g_cBugReport_Color, g_cBugReport_Content, g_cBugReport_FooterIcon, g_cDNSServerIP; + +char g_sBugReport_Webhook[128], g_sBugReport_BotName[32], g_sBugReport_BotAvatar[128], g_sBugReport_Color[8], g_sBugReport_Content[256], g_sBugReport_FooterIcon[128], g_sServerIP[128]; + +char g_sServerName[128]; + +bool g_bBugReport; + +#define REPORTER_CONSOLE 1679124 +#define DEFAULT_COLOR "#00FF00" +#define USE_AutoExecConfig + +public Plugin myinfo = +{ + name = "Discord Utilities - Bugreport module", + author = "AiDN™ & Cruze03", + description = "Bugreport module for the Discord Utilities, code from Cruze03", + version = "1.0", + url = "https://steamcommunity.com/id/originalaidn & https://github.com/Cruze03/discord-utilities" +}; + +public void OnPluginStart() +{ + #if defined USE_AutoExecConfig + AutoExecConfig_SetFile("Discord-Utilities"); + AutoExecConfig_SetCreateFile(true); + + g_cBugReport_Webhook = AutoExecConfig_CreateConVar("sm_du_bugreport_webhook", "", "Webhook for bugreport reports. Blank to disable.", FCVAR_PROTECTED); + g_cBugReport_BotName = AutoExecConfig_CreateConVar("sm_du_bugreport_botname", "Discord Utilities", "BotName for bugreport. Blank to use webhook name."); + g_cBugReport_BotAvatar = AutoExecConfig_CreateConVar("sm_du_bugreport_avatar", "", "Avatar link for bugreport bot. Blank to use webhook avatar."); + g_cBugReport_Color = AutoExecConfig_CreateConVar("sm_du_bugreport_color", "#ff9911", "Color for embed message of bugreport."); + g_cBugReport_Content = AutoExecConfig_CreateConVar("sm_du_bugreport_content", "", "Content for embed message of bugreport. Blank to disable."); + g_cBugReport_FooterIcon = AutoExecConfig_CreateConVar("sm_du_bugreport_footericon", "", "Link to footer icon for bugreport. Blank for no footer icon."); + + g_cDNSServerIP = AutoExecConfig_CreateConVar("sm_du_dns_ip", "", "DNS IP address of your game server. Blank to use real IP."); + + #else + g_cBugReport_Webhook = CreateConVar("sm_du_bugreport_webhook", "", "Webhook for bugreport reports. Blank to disable.", FCVAR_PROTECTED); + g_cBugReport_BotName = CreateConVar("sm_du_bugreport_botname", "Discord Utilities", "BotName for bugreport. Blank to use webhook name."); + g_cBugReport_BotAvatar = CreateConVar("sm_du_bugreport_avatar", "", "Avatar link for bugreport bot. Blank to use webhook avatar."); + g_cBugReport_Color = CreateConVar("sm_du_bugreport_color", "#ff9911", "Color for embed message of bugreport."); + g_cBugReport_Content = CreateConVar("sm_du_bugreport_content", "", "Content for embed message of bugreport. Blank to disable."); + g_cBugReport_FooterIcon = CreateConVar("sm_du_bugreport_footericon", "", "Link to footer icon for bugreport. Blank for no footer icon."); + + g_cDNSServerIP = CreateConVar("sm_du_dns_ip", "", "DNS IP address of your game server. Blank to use real IP."); + + AutoExecConfig(true, "Discord-Utilities"); + #endif + + HookConVarChange(g_cBugReport_Webhook, OnSettingsChanged); + HookConVarChange(g_cBugReport_BotName, OnSettingsChanged); + HookConVarChange(g_cBugReport_BotAvatar, OnSettingsChanged); + HookConVarChange(g_cBugReport_Color, OnSettingsChanged); + HookConVarChange(g_cBugReport_Content, OnSettingsChanged); + HookConVarChange(g_cBugReport_FooterIcon, OnSettingsChanged); + + LoadTranslations("Discord-Utilities.phrases"); +} + +public void OnLibraryAdded(const char[] szLibrary) +{ + if(StrEqual(szLibrary, "bugreport")) g_bBugReport = true; +} + +public void OnLibraryRemoved(const char[] szLibrary) +{ + if(StrEqual(szLibrary, "bugreport")) g_bBugReport = false; +} +public void OnAllPluginsLoaded() +{ + if(!LibraryExists("discord-api")) + { + SetFailState("[Discord-Utilities] This plugin is fully dependant on \"Discord-API\" by Deathknife. (https://github.com/Deathknife/sourcemod-discord)"); + } + + g_bBugReport = LibraryExists("bugreport"); +} + +public void OnConfigsExecuted() +{ + LoadCvars(); + FindConVar("hostname").GetString(g_sServerName, sizeof(g_sServerName)); +} + +void LoadCvars() +{ + g_cBugReport_Webhook.GetString(g_sBugReport_Webhook, sizeof(g_sBugReport_Webhook)); + g_cBugReport_BotName.GetString(g_sBugReport_BotName, sizeof(g_sBugReport_BotName)); + g_cBugReport_BotAvatar.GetString(g_sBugReport_BotAvatar, sizeof(g_sBugReport_BotAvatar)); + g_cBugReport_Color.GetString(g_sBugReport_Color, sizeof(g_sBugReport_Color)); + g_cBugReport_Content.GetString(g_sBugReport_Content, sizeof(g_sBugReport_Content)); + g_cBugReport_FooterIcon.GetString(g_sBugReport_FooterIcon, sizeof(g_sBugReport_FooterIcon)); + + g_cDNSServerIP.GetString(g_sServerIP, sizeof(g_sServerIP)); + ServerIP(g_sServerIP, sizeof(g_sServerIP)); +} + +public int OnSettingsChanged(ConVar convar, const char[] oldVal, const char[] newVal) +{ + if(StrEqual(oldVal, newVal, true)) + { + return; + } + if(convar == g_cBugReport_Webhook) + { + strcopy(g_sBugReport_Webhook, sizeof(g_sBugReport_Webhook), newVal); + } + else if(convar == g_cBugReport_BotName) + { + strcopy(g_sBugReport_BotName, sizeof(g_sBugReport_BotName), newVal); + } + else if(convar == g_cBugReport_BotAvatar) + { + strcopy(g_sBugReport_BotAvatar, sizeof(g_sBugReport_BotAvatar), newVal); + } + else if(convar == g_cBugReport_Color) + { + strcopy(g_sBugReport_Color, sizeof(g_sBugReport_Color), newVal); + } + else if(convar == g_cBugReport_Content) + { + strcopy(g_sBugReport_Content, sizeof(g_sBugReport_Content), newVal); + } + else if(convar == g_cBugReport_FooterIcon) + { + strcopy(g_sBugReport_FooterIcon, sizeof(g_sBugReport_FooterIcon), newVal); + } + else if(convar == g_cDNSServerIP) + { + strcopy(g_sServerIP, sizeof(g_sServerIP), newVal); + ServerIP(g_sServerIP, sizeof(g_sServerIP)); + } +} + +public void BugReport_OnReportPost(int client, const char[] map, const char[] reason, ArrayList array) +{ + if(StrEqual(g_sBugReport_Webhook, "")) + { + return; + } + + if(!g_bBugReport) + { + return; + } + + char sReason[(REASON_MAX_LENGTH + 1) * 2]; + strcopy(sReason, sizeof(sReason), reason); + int index = array.FindString(sReason); + + if(index != -1) + { + LogError("Duplicate Reason. Skipping."); + return; + } + + Discord_EscapeString(sReason, sizeof(sReason)); + + char clientAuth[21]; + char clientAuth2[21]; + char clientName[(MAX_NAME_LENGTH + 1) * 2]; + + if (client == REPORTER_CONSOLE) + { + Format(clientName, sizeof(clientName), "%T", "SERVER", LANG_SERVER); + Format(clientAuth, sizeof(clientAuth), "%T", "CONSOLE", LANG_SERVER); + } + else + { + GetClientAuthId(client, AuthId_SteamID64, clientAuth, sizeof(clientAuth)); + GetClientAuthId(client, AuthId_Steam2, clientAuth2, sizeof(clientAuth2)); + GetClientName(client, clientName, sizeof(clientName)); + Discord_EscapeString(clientName, sizeof(clientName)); + } + + DiscordWebHook hook = new DiscordWebHook( g_sBugReport_Webhook ); + hook.SlackMode = true; + if(g_sBugReport_BotName[0]) + { + hook.SetUsername( g_sBugReport_BotName ); + } + + if(g_sBugReport_BotAvatar[0]) + { + hook.SetAvatar( g_sBugReport_BotAvatar ); + } + + MessageEmbed embed = new MessageEmbed(); + + if(StrContains(g_sBugReport_Color, "#") != -1) + { + embed.SetColor(g_sBugReport_Color); + } + else + { + LogError("[Discord-Utilities] BugReport is using default color as you've set invalid BugReport color."); + embed.SetColor(DEFAULT_COLOR); + } + + char buffer[512], trans[64]; + Format( trans, sizeof( trans ), "%T", "BugReportTitle", LANG_SERVER); + embed.SetTitle( buffer ); + + if (client != REPORTER_CONSOLE) + { + Format( buffer, sizeof( buffer ), "[%s](http://www.steamcommunity.com/profiles/%s) (%s)", clientName, clientAuth, clientAuth2 ); + } + else + { + Format( buffer, sizeof( buffer ), "%s", clientName ); + } + Format( trans, sizeof( trans ), "%T", "ReporterField", LANG_SERVER); + embed.AddField( trans, buffer, true ); + + Format( trans, sizeof( trans ), "%T", "MapField", LANG_SERVER); + embed.AddField( trans, map, true ); + + Format( trans, sizeof( trans ), "%T", "ReasonField", LANG_SERVER); + embed.AddField( trans, sReason, false ); + + Format( trans, sizeof( trans ), "%T", "DirectConnectField", LANG_SERVER); + Format( buffer, sizeof( buffer ), "steam://connect/%s", g_sServerIP ); + embed.AddField( trans, buffer, false ); + + if(g_sBugReport_FooterIcon[0]) + { + embed.SetFooterIcon( g_sBugReport_FooterIcon ); + } + Format( buffer, sizeof( buffer ), "%T", "ServerField", LANG_SERVER, g_sServerName); + embed.SetFooter( buffer ); + + if(g_sBugReport_Content[0]) + { + hook.SetContent(g_sBugReport_Content); + } + + hook.Embed( embed ); + hook.Send(); + delete hook; +} + +stock void Discord_EscapeString(char[] string, int maxlen, bool name = false) +{ + if(name) + { + ReplaceString(string, maxlen, "everyone", "everyone"); + ReplaceString(string, maxlen, "here", "here"); + ReplaceString(string, maxlen, "discordtag", "discordtag"); + } + ReplaceString(string, maxlen, "#", "#"); + ReplaceString(string, maxlen, "@", "@"); + //ReplaceString(string, maxlen, ":", ""); + ReplaceString(string, maxlen, "_", "ˍ"); + ReplaceString(string, maxlen, "'", "'"); + ReplaceString(string, maxlen, "`", "'"); + ReplaceString(string, maxlen, "~", "∽"); + ReplaceString(string, maxlen, "\"", """); +} + +void ServerIP(char[] sIP, int size) +{ + if(sIP[0]) + { + return; + } + int ip[4]; + int iServerPort = FindConVar("hostport").IntValue; + SteamWorks_GetPublicIP(ip); + if(SteamWorks_GetPublicIP(ip)) + { + Format(sIP, size, "%d.%d.%d.%d:%d", ip[0], ip[1], ip[2], ip[3], iServerPort); + } + else + { + int iServerIP = FindConVar("hostip").IntValue; + Format(sIP, size, "%d.%d.%d.%d:%d", iServerIP >> 24 & 0x000000FF, iServerIP >> 16 & 0x000000FF, iServerIP >> 8 & 0x000000FF, iServerIP & 0x000000FF, iServerPort); + } +} \ No newline at end of file diff --git a/scripting/du_calladmin.sp b/scripting/du_calladmin.sp index 27533bc..dc6778c 100644 --- a/scripting/du_calladmin.sp +++ b/scripting/du_calladmin.sp @@ -5,15 +5,15 @@ #include #include - #include -#define DEFAULT_COLOR "#00FF00" - #pragma dynamic 500000 #pragma newdecls required #pragma semicolon 1 +#define DEFAULT_COLOR "#00FF00" +#define USE_AutoExecConfig + ConVar g_cCallAdmin_Webhook, g_cCallAdmin_BotName, g_cCallAdmin_BotAvatar, g_cCallAdmin_Color, g_cCallAdmin_Content, g_cCallAdmin_FooterIcon, g_cDNSServerIP; char g_sCallAdmin_Webhook[128], g_sCallAdmin_BotName[32], g_sCallAdmin_BotAvatar[128], g_sCallAdmin_Color[8], g_sCallAdmin_Content[256], g_sCallAdmin_FooterIcon[128], g_sServerIP[128]; diff --git a/scripting/du_chatrelay.sp b/scripting/du_chatrelay.sp new file mode 100644 index 0000000..0bac00f --- /dev/null +++ b/scripting/du_chatrelay.sp @@ -0,0 +1,741 @@ +#include +#include +#include +#include +#include + +#include +#include + +#pragma dynamic 500000 +#pragma newdecls required +#pragma semicolon 1 + +#define MAX_BLOCKLIST_LIMIT 20 +#define USE_AutoExecConfig + +ConVar g_cChatRelay_Webhook, g_cChatRelay_BlockList, g_cAdminChatRelay_Mode, g_cAdminChatRelay_Webhook, g_cAdminChatRelay_BlockList, g_cAdminLog_Webhook, g_cAdminLog_BlockList; +ConVar g_cChatRelayChannelID, g_cAdminChatRelayChannelID; +ConVar g_cBotToken, g_cTimeStamps, g_cDiscordPrefix, g_cAPIKey; + +char g_sAvatarURL[MAXPLAYERS+1][128]; + +char g_sChatRelay_Webhook[128], g_sChatRelay_BlockList[MAX_BLOCKLIST_LIMIT][64], g_sAdminChatRelay_Mode[16], g_sAdminChatRelay_Webhook[128], g_sAdminChatRelay_BlockList[MAX_BLOCKLIST_LIMIT][64], g_sAdminLog_Webhook[128], g_sAdminLog_BlockList[MAX_BLOCKLIST_LIMIT][64]; +char g_sVerificationChannelID[20], g_sChatRelayChannelID[20], g_sAdminChatRelayChannelID[20]; +char g_sBotToken[60], g_sAPIKey[64]; + +char g_sDiscordPrefix[128]; + +char g_sServerName[128]; + +bool g_bBaseComm, g_bShavit; + +DiscordBot Bot; + +char gS_GlobalColorNames[][] = +{ + "{default}", + "{team}", + "{green}" +}; + +char gS_GlobalColors[][] = +{ + "\x01", + "\x03", + "\x04" +}; + +char gS_CSGOColorNames[][] = +{ + "{blue}", + "{bluegrey}", + "{darkblue}", + "{darkred}", + "{gold}", + "{grey}", + "{grey2}", + "{lightgreen}", + "{lightred}", + "{lime}", + "{orchid}", + "{yellow}", + "{palered}" +}; + +char gS_CSGOColors[][] = +{ + "\x0B", + "\x0A", + "\x0C", + "\x02", + "\x10", + "\x08", + "\x0D", + "\x05", + "\x0F", + "\x06", + "\x0E", + "\x09", + "\x07" +}; + +public Plugin myinfo = +{ + name = "Discord Utilities - Chatrelay module", + author = "AiDN™ & Cruze03", + description = "Chatrelay module for the Discord Utilities, code from Cruze03", + version = "1.0", + url = "https://steamcommunity.com/id/originalaidn & https://github.com/Cruze03/discord-utilities" +}; + +public void OnPluginStart() +{ + AddCommandListener(Command_AdminChat, "sm_chat"); + + #if defined USE_AutoExecConfig + AutoExecConfig_SetFile("Discord-Utilities"); + AutoExecConfig_SetCreateFile(true); + + g_cChatRelay_Webhook = AutoExecConfig_CreateConVar("sm_du_chat_webhook", "", "Webhook for game server => discord server chat messages. Blank to disable.", FCVAR_PROTECTED); + g_cChatRelay_BlockList = AutoExecConfig_CreateConVar("sm_du_chat_blocklist", "rtv, nominate", "Text that shouldn't appear in gameserver => discord server chat messages. Separate it with \", \""); + g_cAdminChatRelay_Mode = AutoExecConfig_CreateConVar("sm_du_adminchat_mode", "0b", "0 - Only \"say_team with @ / sm_chat\"\n0b - \"say_team with @ / sm_chat\" with discord to game server chat to admin.\nAny admin flag - Show messages of specific flag in channel."); + g_cAdminChatRelay_Webhook = AutoExecConfig_CreateConVar("sm_du_adminchat_webhook", "", "Webhook for game server => discord server chat messages where chat messages are to (say_team with @ / sm_chat) / are of admins. Blank to disable.", FCVAR_PROTECTED); + g_cAdminChatRelay_BlockList = AutoExecConfig_CreateConVar("sm_du_adminchat_blocklist", "rtv, nominate", "Text that shouldn't appear in gameserver => discord server where chat messages are to admin. Separate it with \", \""); + g_cAdminLog_Webhook = AutoExecConfig_CreateConVar("sm_du_adminlog_webhook", "", "Webhook for channel where all admin commands are logged. Blank to disable.", FCVAR_PROTECTED); + g_cAdminLog_BlockList = AutoExecConfig_CreateConVar("sm_du_adminlog_blocklist", "slapped, firebombed", "Log with this string will be ignored. Separate it with \", \""); + + g_cChatRelayChannelID = AutoExecConfig_CreateConVar("sm_du_chat_channelid", "", "Channel ID for discord server => game server messages. Blank to disable."); + g_cAdminChatRelayChannelID = AutoExecConfig_CreateConVar("sm_du_adminchat_channelid", "", "Channel ID for discord server => game server messages only of admins. Blank to disable."); + + g_cBotToken = AutoExecConfig_CreateConVar("sm_du_bottoken", "", "Bot Token. Needed for discord server => gameserver and/or verification module.", FCVAR_PROTECTED); + + g_cDiscordPrefix = AutoExecConfig_CreateConVar("sm_du_discord_prefix", "[{lightgreen}Discord{default}]", "Prefix for discord messages."); + + g_cTimeStamps = AutoExecConfig_CreateConVar("sm_du_display_timestamps", "0", "Display timestamps? Used in gameserver => discord server relay AND AdminLog"); + + g_cAPIKey = AutoExecConfig_CreateConVar("sm_du_apikey", "", "Steam API Key (https://steamcommunity.com/dev/apikey). Needed for gameserver => discord server relay and/or admin chat relay and/or Admin logs. Blank will show default author icon of discord.", FCVAR_PROTECTED); + + #else + g_cChatRelay_Webhook = CreateConVar("sm_du_chat_webhook", "", "Webhook for game server => discord server chat messages. Blank to disable.", FCVAR_PROTECTED); + g_cChatRelay_BlockList = CreateConVar("sm_du_chat_blocklist", "rtv, nominate", "Text that shouldn't appear in gameserver => discord server chat messages. Separate it with \", \""); + g_cAdminChatRelay_Mode = CreateConVar("sm_du_adminchat_mode", "0b", "0 - Only \"say_team with @ / sm_chat\"\n0b - \"say_team with @ / sm_chat\" with discord to game server chat to admin.\nAny admin flag - Show messages of specific flag in channel."); + g_cAdminChatRelay_Webhook = CreateConVar("sm_du_adminchat_webhook", "", "Webhook for game server => discord server chat messages where chat messages are to (say_team with @ / sm_chat) / are of admins. Blank to disable.", FCVAR_PROTECTED); + g_cAdminChatRelay_BlockList = CreateConVar("sm_du_adminchat_blocklist", "rtv, nominate", "Text that shouldn't appear in gameserver => discord server where chat messages are to admin. Separate it with \", \""); + g_cAdminLog_Webhook = CreateConVar("sm_du_adminlog_webhook", "", "Webhook for channel where all admin commands are logged. Blank to disable.", FCVAR_PROTECTED); + g_cAdminLog_BlockList = CreateConVar("sm_du_adminlog_blocklist", "slapped, firebombed", "Log with this string will be ignored. Separate it with \", \""); + + g_cChatRelayChannelID = CreateConVar("sm_du_chat_channelid", "", "Channel ID for discord server => game server messages. Blank to disable."); + g_cAdminChatRelayChannelID = CreateConVar("sm_du_adminchat_channelid", "", "Channel ID for discord server => game server messages only of admins. Blank to disable."); + + g_cBotToken = CreateConVar("sm_du_bottoken", "", "Bot Token. Needed for discord server => gameserver and/or verification module.", FCVAR_PROTECTED); + + g_cDiscordPrefix = CreateConVar("sm_du_discord_prefix", "[{lightgreen}Discord{default}]", "Prefix for discord messages."); + + g_cTimeStamps = CreateConVar("sm_du_display_timestamps", "0", "Display timestamps? Used in gameserver => discord server relay AND AdminLog"); + + g_cAPIKey = CreateConVar("sm_du_apikey", "", "Steam API Key (https://steamcommunity.com/dev/apikey). Needed for gameserver => discord server relay and/or admin chat relay and/or Admin logs. Blank will show default author icon of discord.", FCVAR_PROTECTED); + + AutoExecConfig(true, "Discord-Utilities"); + #endif + + HookConVarChange(g_cChatRelay_Webhook, OnSettingsChanged); + HookConVarChange(g_cChatRelay_BlockList, OnSettingsChanged); + HookConVarChange(g_cAdminChatRelay_Mode, OnSettingsChanged); + HookConVarChange(g_cAdminChatRelay_Webhook, OnSettingsChanged); + HookConVarChange(g_cAdminChatRelay_BlockList, OnSettingsChanged); + HookConVarChange(g_cAdminLog_Webhook, OnSettingsChanged); + HookConVarChange(g_cAdminLog_BlockList, OnSettingsChanged); + HookConVarChange(g_cChatRelayChannelID, OnSettingsChanged); + HookConVarChange(g_cAdminChatRelayChannelID, OnSettingsChanged); + + HookConVarChange(g_cBotToken, OnSettingsChanged); + + HookConVarChange(g_cDiscordPrefix, OnSettingsChanged); + + HookConVarChange(g_cAPIKey, OnSettingsChanged); + + LoadTranslations("Discord-Utilities.phrases"); +} + +public void OnLibraryAdded(const char[] szLibrary) +{ + if(StrEqual(szLibrary, "basecomm")) g_bBaseComm = true; + else if(StrEqual(szLibrary, "shavit")) g_bShavit = true; +} + +public void OnLibraryRemoved(const char[] szLibrary) +{ + if(StrEqual(szLibrary, "basecomm")) g_bBaseComm = false; + else if(StrEqual(szLibrary, "shavit")) g_bShavit = false; +} + +public void OnAllPluginsLoaded() +{ + if(!LibraryExists("discord-api")) + { + SetFailState("[Discord-Utilities] This plugin is fully dependant on \"Discord-API\" by Deathknife. (https://github.com/Deathknife/sourcemod-discord)"); + } + + g_bShavit = LibraryExists("shavit"); + g_bBaseComm = LibraryExists("basecomm"); +} + +public void OnConfigsExecuted() +{ + LoadCvars(); + + if(!StrEqual(g_sBotToken, "")) + { + if(Bot == view_as(INVALID_HANDLE)) + { + CreateBot(); + } + } + + FindConVar("hostname").GetString(g_sServerName, sizeof(g_sServerName)); +} + +void LoadCvars() +{ + char sBlockList[PLATFORM_MAX_PATH]; + g_cChatRelay_Webhook.GetString(g_sChatRelay_Webhook, sizeof(g_sChatRelay_Webhook)); + g_cChatRelay_BlockList.GetString(sBlockList, sizeof(sBlockList)); + ExplodeString(sBlockList, ", ", g_sChatRelay_BlockList, MAX_BLOCKLIST_LIMIT, 64); + g_cAdminChatRelay_Mode.GetString(g_sAdminChatRelay_Mode, sizeof(g_sAdminChatRelay_Mode)); + g_cAdminChatRelay_Webhook.GetString(g_sAdminChatRelay_Webhook, sizeof(g_sAdminChatRelay_Webhook)); + g_cAdminChatRelay_BlockList.GetString(sBlockList, sizeof(sBlockList)); + ExplodeString(sBlockList, ", ", g_sAdminChatRelay_BlockList, MAX_BLOCKLIST_LIMIT, 64); + g_cAdminLog_Webhook.GetString(g_sAdminLog_Webhook, sizeof(g_sAdminLog_Webhook)); + g_cAdminLog_BlockList.GetString(sBlockList, sizeof(sBlockList)); + ExplodeString(sBlockList, ", ", g_sAdminLog_BlockList, MAX_BLOCKLIST_LIMIT, 64); + + g_cChatRelayChannelID.GetString(g_sChatRelayChannelID, sizeof(g_sChatRelayChannelID)); + g_cAdminChatRelayChannelID.GetString(g_sAdminChatRelayChannelID, sizeof(g_sAdminChatRelayChannelID)); + + g_cBotToken.GetString(g_sBotToken, sizeof(g_sBotToken)); + + g_cDiscordPrefix.GetString(g_sDiscordPrefix, sizeof(g_sDiscordPrefix)); + + g_cAPIKey.GetString(g_sAPIKey, sizeof(g_sAPIKey)); +} + +public int OnSettingsChanged(ConVar convar, const char[] oldVal, const char[] newVal) +{ + if(StrEqual(oldVal, newVal, true)) + { + return; + } + if(convar == g_cChatRelay_Webhook) + { + strcopy(g_sChatRelay_Webhook, sizeof(g_sChatRelay_Webhook), newVal); + } + else if(convar == g_cChatRelay_BlockList) + { + ExplodeString(newVal, ", ", g_sChatRelay_BlockList, MAX_BLOCKLIST_LIMIT, 64); + } + else if(convar == g_cAdminChatRelay_Webhook) + { + strcopy(g_sAdminChatRelay_Webhook, sizeof(g_sAdminChatRelay_Webhook), newVal); + } + else if(convar == g_cAdminChatRelay_BlockList) + { + ExplodeString(newVal, ", ", g_sAdminChatRelay_BlockList, MAX_BLOCKLIST_LIMIT, 64); + } + else if(convar == g_cChatRelayChannelID) + { + strcopy(g_sChatRelayChannelID, sizeof(g_sChatRelayChannelID), newVal); + } + else if(convar == g_cBotToken) + { + strcopy(g_sBotToken, sizeof(g_sBotToken), newVal); + } + else if(convar == g_cDiscordPrefix) + { + strcopy(g_sDiscordPrefix, sizeof(g_sDiscordPrefix), newVal); + } +} + +public void OnPluginEnd() +{ + KillBot(); +} + +public void OnMapEnd() +{ + KillBot(); +} + +public void OnClientPutInServer(int client) +{ + g_sAvatarURL[client][0] = '\0'; +} + +public Action Command_AdminChat(int client, const char[] command, int argc) +{ + if(g_sAdminChatRelay_Webhook[0] == '\0' || g_sAdminChatRelay_Mode[0] != '0') + { + return Plugin_Continue; + } + if(g_bBaseComm && BaseComm_IsClientGagged(client)) + { + return Plugin_Continue; + } + if(1 <= client <= MaxClients) + { + char sMessage[256]; + GetCmdArgString(sMessage, sizeof(sMessage)); + SendChatRelay(client, sMessage, g_sAdminChatRelay_Webhook); + } + return Plugin_Continue; +} + +public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs) +{ + if(StrEqual(g_sChatRelay_Webhook, "") && StrEqual(g_sAdminChatRelay_Webhook, "")) + { + return Plugin_Continue; + } + if(IsClientInGame(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) + { + return Plugin_Continue; + } + if(1 <= client <= MaxClients) + { + if(strcmp(command, "say") != 0 && strcmp(command, "say_team") != 0) + { + return Plugin_Continue; + } + if(IsChatTrigger()) + { + return Plugin_Continue; + } + if(strcmp(command, "say_team") == 0 && sArgs[0] == '@') + { + bool bAdmin = CheckCommandAccess(client, "", ADMFLAG_GENERIC); + if(g_sAdminChatRelay_Mode[0] == '0') + { + SendChatRelay(client, sArgs[1], g_sAdminChatRelay_Webhook, bAdmin); + } + return Plugin_Continue; + } + if(strcmp(command, "say") == 0 && sArgs[0] == '@') + { + if(g_sChatRelay_Webhook[0]) + { + SendChatRelay(client, sArgs[1], g_sChatRelay_Webhook, true, true); + } + return Plugin_Continue; + } + if(!StrEqual(g_sAdminChatRelay_Mode, "0", false) && !StrEqual(g_sAdminChatRelay_Mode, "", false) && g_sAdminChatRelay_Mode[1] == '\0') + { + if(g_sAdminChatRelay_Webhook[0] && CheckAdminFlags(client, ReadFlagString(g_sAdminChatRelay_Mode))) + SendChatRelay(client, sArgs, g_sAdminChatRelay_Webhook); + } + else if(g_sChatRelay_Webhook[0]) + { + SendChatRelay(client, sArgs, g_sChatRelay_Webhook); + } + } + return Plugin_Continue; +} + +public Action OnLogAction(Handle hSource, Identity ident, int client, int target, const char[] sMsg) +{ + if(StrEqual(g_sAdminLog_Webhook, "")) + { + delete hSource; + return Plugin_Continue; + } + + if(client <= 0) + { + delete hSource; + return Plugin_Continue; + } + + if(StrContains(sMsg, "sm_chat", false) != -1) + { + delete hSource; + return Plugin_Continue;// dont log sm_chat because it's already being showed in admin chat relay channel. + } + + SendAdminLog(client, sMsg); + delete hSource; + + return Plugin_Continue; +} + +public void GuildList(DiscordBot bawt, char[] id, char[] name, char[] icon, bool owner, int permissions, const bool listen) +{ + Bot.GetGuildChannels(id, ChannelList, INVALID_FUNCTION, listen); +} + +public void ChannelList(DiscordBot bawt, const char[] guild, DiscordChannel Channel, const bool listen) +{ + if(StrEqual(g_sBotToken, "") || (StrEqual(g_sChatRelayChannelID, "") && StrEqual(g_sVerificationChannelID, "") && StrEqual(g_sAdminChatRelayChannelID, ""))) + { + return; + } + if(Bot == null || Channel == null) + { + return; + } + if(Bot.IsListeningToChannel(Channel)) + { + //Bot.StopListeningToChannel(Channel); + return; + } + char id[20], name[32]; + Channel.GetID(id, sizeof(id)); + Channel.GetName(name, sizeof(name)); + if(strlen(g_sChatRelayChannelID) > 10) //ChannelID size is around 18-20 char + { + if(StrEqual(id, g_sChatRelayChannelID)) + { + Bot.StartListeningToChannel(Channel, ChatRelayReceived); + } + } + if(strlen(g_sAdminChatRelayChannelID) > 10) + { + if(StrEqual(id, g_sAdminChatRelayChannelID) && !StrEqual(g_sAdminChatRelay_Mode, "", false) && (g_sAdminChatRelay_Mode[0] == '0' && g_sAdminChatRelay_Mode[1] != '\0') || (g_sAdminChatRelay_Mode[0] != '0' && g_sAdminChatRelay_Mode[0] != '\0')) + { + Bot.StartListeningToChannel(Channel, AdminChatRelayReceived); + } + } +} + +public void AdminChatRelayReceived(DiscordBot bawt, DiscordChannel channel, DiscordMessage discordmessage) +{ + if((g_sAdminChatRelay_Mode[0] == '0' && g_sAdminChatRelay_Mode[1] == '\0') || (g_sAdminChatRelay_Mode[0] != '\0' && g_sAdminChatRelay_Mode[0] != '0' && g_sAdminChatRelay_Mode[1] != '\0')) + { + return; + } + DiscordUser author = discordmessage.GetAuthor(); + if(author.IsBot()) + { + delete author; + return; + } + + char message[512]; + char userName[32], discriminator[6]; + discordmessage.GetContent(message, sizeof(message)); + author.GetUsername(userName, sizeof(userName)); + author.GetDiscriminator(discriminator, sizeof(discriminator)); + delete author; + + char sFlag[5]; + if(g_sAdminChatRelay_Mode[0] == '0' && g_sAdminChatRelay_Mode[1] != '\0') + { + sFlag[0] = g_sAdminChatRelay_Mode[1]; + } + else + { + sFlag[0] = g_sAdminChatRelay_Mode[0]; + } + for(int i = 1; i <= MaxClients; i++) if(IsClientInGame(i) && !IsFakeClient(i) && CheckAdminFlags(i, ReadFlagString(sFlag))) + { + CPrintToChat(i, "%s %T", g_sDiscordPrefix, "AdminChatRelayFormat", i, userName, discriminator, message); + } +} + +public void ChatRelayReceived(DiscordBot bawt, DiscordChannel channel, DiscordMessage discordmessage) +{ + DiscordUser author = discordmessage.GetAuthor(); + if(author.IsBot()) + { + delete author; + return; + } + + char message[512]; + char userName[32], discriminator[6]; + discordmessage.GetContent(message, sizeof(message)); + author.GetUsername(userName, sizeof(userName)); + author.GetDiscriminator(discriminator, sizeof(discriminator)); + delete author; + + CPrintToChatAll("%s %T", g_sDiscordPrefix, "ChatRelayFormat", LANG_SERVER, userName, discriminator, message); +} + +void SendChatRelay(int client, const char[] sArgs, char[] url, bool bAdmin = true, bool bAllChat = false) +{ + if(strcmp(url, g_sChatRelay_Webhook) == 0) + { + for(int i = 0; i < sizeof(g_sChatRelay_BlockList); i++) + { + if(strcmp(sArgs, g_sChatRelay_BlockList[i], false) == 0) + { + return; + } + } + } + else if(strcmp(url, g_sAdminChatRelay_Webhook) == 0) + { + for(int i = 0; i < sizeof(g_sAdminChatRelay_BlockList); i++) + { + if(strcmp(sArgs, g_sAdminChatRelay_BlockList[i], false) == 0) + { + return; + } + } + } + char name[MAX_NAME_LENGTH+1], timestamp[32], sMessage[256]; + GetClientName(client, name, sizeof(name)); + TrimString(name); + Discord_EscapeString(name, sizeof(name), true); + + char auth[32]; + if(!GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth))) + { + return; + } + Format(name, sizeof(name), "%s [%s]", name, auth); + + FormatEx(sMessage, sizeof(sMessage), sArgs); + Discord_EscapeString(sMessage, sizeof(sMessage)); + + RemoveColors(sMessage, sizeof(sMessage)); + + if(g_cTimeStamps.BoolValue) + { + FormatTime(timestamp, sizeof(timestamp), "[%I:%M:%S %p] ", GetTime()); + } + + DiscordWebHook hook = new DiscordWebHook( url ); + hook.SlackMode = true; + hook.SetUsername( name ); + if(g_sAvatarURL[client][0]) + { + hook.SetAvatar(g_sAvatarURL[client]); + } + char sPrivateToAdmins[32], sAllChat[32]; + Format(sPrivateToAdmins, sizeof(sPrivateToAdmins), "%T", "ChatRelayPrivateToAdmins", LANG_SERVER); + Format(sAllChat, sizeof(sAllChat), "%T", "ChatRelayAllChat", LANG_SERVER); + if(strcmp(url, g_sAdminChatRelay_Webhook) == 0) + { + Format(sMessage, sizeof(sMessage), "%T", "AdminChatFormat", LANG_SERVER, timestamp, g_sServerName, bAdmin ? "" : sPrivateToAdmins, sMessage); + } + else + { + Format(sMessage, sizeof(sMessage), "%s%s%s", timestamp, bAllChat ? sAllChat : "", sMessage); + } + hook.SetContent(sMessage); + hook.Send(); + + delete hook; +} + +void SendAdminLog(int client, const char[] sArgs) +{ + char name[MAX_NAME_LENGTH+1], timestamp[32], sMessage[256], map[PLATFORM_MAX_PATH], mapdisplay[64]; + GetClientName(client, name, sizeof(name)); + TrimString(name); + Discord_EscapeString(name, sizeof(name), true); + + char auth[32]; + if(!GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth))) + { + return; + } + Format(name, sizeof(name), "%s [%s]", name, auth); + + GetCurrentMap(map, sizeof(map)); + GetMapDisplayName(map, mapdisplay, sizeof(mapdisplay)); + + FormatEx(sMessage, sizeof(sMessage), sArgs); + Discord_EscapeString(sMessage, sizeof(sMessage)); + + + RemoveColors(name, sizeof(name)); + RemoveColors(sMessage, sizeof(sMessage)); + + if(g_cTimeStamps.BoolValue) + { + FormatTime(timestamp, sizeof(timestamp), "[%I:%M:%S %p] ", GetTime()); + } + + DiscordWebHook hook = new DiscordWebHook( g_sAdminLog_Webhook ); + hook.SlackMode = true; + hook.SetUsername( name ); + if(g_sAvatarURL[client][0]) + { + hook.SetAvatar(g_sAvatarURL[client]); + } + Format(sMessage, sizeof(sMessage), "%T", "AdminLogFormat", LANG_SERVER, timestamp, g_sServerName, mapdisplay, sMessage); + hook.SetContent(sMessage); + + hook.Send(); + + delete hook; +} + +stock void RemoveColors(char[] text, int size) +{ + if(g_bShavit) + { + for(int i = 0; i < sizeof(gS_GlobalColorNames); i++) + { + ReplaceString(text, size, gS_GlobalColorNames[i], ""); + } + for(int i = 0; i < sizeof(gS_GlobalColors); i++) + { + ReplaceString(text, size, gS_GlobalColors[i], ""); + } + for(int i = 0; i < sizeof(gS_CSGOColorNames); i++) + { + ReplaceString(text, size, gS_CSGOColorNames[i], ""); + } + for(int i = 0; i < sizeof(gS_CSGOColors); i++) + { + ReplaceString(text, size, gS_CSGOColors[i], ""); + } + } + else + { + for(int i = 0; i < sizeof(C_Tag); i++) + { + ReplaceString(text, size, C_Tag[i], ""); + } + for(int i = 0; i < sizeof(C_TagCode); i++) + { + ReplaceString(text, size, C_TagCode[i], ""); + } + } +} + +stock void Discord_EscapeString(char[] string, int maxlen, bool name = false) +{ + if(name) + { + ReplaceString(string, maxlen, "everyone", "everyone"); + ReplaceString(string, maxlen, "here", "here"); + ReplaceString(string, maxlen, "discordtag", "discordtag"); + } + ReplaceString(string, maxlen, "#", "#"); + ReplaceString(string, maxlen, "@", "@"); + //ReplaceString(string, maxlen, ":", ""); + ReplaceString(string, maxlen, "_", "ˍ"); + ReplaceString(string, maxlen, "'", "'"); + ReplaceString(string, maxlen, "`", "'"); + ReplaceString(string, maxlen, "~", "∽"); + ReplaceString(string, maxlen, "\"", """); +} + +stock void CreateBot() +{ + if(StrEqual(g_sBotToken, "") || StrEqual(g_sChatRelayChannelID, "") && StrEqual(g_sVerificationChannelID, "")) + { + return; + } + KillBot(); + Bot = new DiscordBot(g_sBotToken); + CreateTimer(5.0, Timer_GuildList, _, TIMER_FLAG_NO_MAPCHANGE); +} + +stock void KillBot() +{ + if(Bot) + { + Bot.StopListeningToChannels(); + Bot.StopListening(); + } + delete Bot; +} + +bool CheckAdminFlags(int client, int iFlag) +{ + int iUserFlags = GetUserFlagBits(client); + return (iUserFlags & ADMFLAG_ROOT || (iUserFlags & iFlag) == iFlag); +} + +public Action Timer_GuildList(Handle timer) +{ + GetGuilds(); +} + +public Action OnClientPreAdminCheck(int client) +{ + if(IsFakeClient(client)) + { + return; + } + + + if(StrEqual(g_sAPIKey, "")) + { + return; + } + + char szSteamID64[32]; + if(!GetClientAuthId(client, AuthId_SteamID64, szSteamID64, sizeof(szSteamID64))) + { + return; + } + + static char sRequest[256]; + FormatEx(sRequest, sizeof(sRequest), "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=%s&steamids=%s&format=vdf", g_sAPIKey, szSteamID64); + Handle hRequest = SteamWorks_CreateHTTPRequest(k_EHTTPMethodGET, sRequest); + if(!hRequest || !SteamWorks_SetHTTPRequestContextValue(hRequest, client) || !SteamWorks_SetHTTPCallbacks(hRequest, OnTransferCompleted) || !SteamWorks_SendHTTPRequest(hRequest)) + { + delete hRequest; + } +} + +public int OnTransferCompleted(Handle hRequest, bool bFailure, bool bRequestSuccessful, EHTTPStatusCode eStatusCode, int client) +{ + if (bFailure || !bRequestSuccessful || eStatusCode != k_EHTTPStatusCode200OK) + { + LogError("SteamAPI HTTP Response failed: %d", eStatusCode); + delete hRequest; + return; + } + + int iBodyLength; + SteamWorks_GetHTTPResponseBodySize(hRequest, iBodyLength); + + char[] sData = new char[iBodyLength]; + SteamWorks_GetHTTPResponseBodyData(hRequest, sData, iBodyLength); + + delete hRequest; + + APIWebResponse(sData, client); +} + +public void APIWebResponse(const char[] sData, int client) +{ + KeyValues kvResponse = new KeyValues("SteamAPIResponse"); + + if (!kvResponse.ImportFromString(sData, "SteamAPIResponse")) + { + LogError("kvResponse.ImportFromString(\"SteamAPIResponse\") in APIWebResponse failed. Try updating your steamworks extension."); + + delete kvResponse; + return; + } + + if (!kvResponse.JumpToKey("players")) + { + LogError("kvResponse.JumpToKey(\"players\") in APIWebResponse failed. Try updating your steamworks extension."); + + delete kvResponse; + return; + } + + if (!kvResponse.GotoFirstSubKey()) + { + LogError("kvResponse.GotoFirstSubKey() in APIWebResponse failed. Try updating your steamworks extension."); + + delete kvResponse; + return; + } + + kvResponse.GetString("avatarfull", g_sAvatarURL[client], sizeof(g_sAvatarURL[])); + delete kvResponse; +} + +stock void GetGuilds() +{ + Bot.GetGuilds(GuildList, _, true); +} \ No newline at end of file diff --git a/scripting/include/bugreport.inc b/scripting/include/bugreport.inc new file mode 100644 index 0000000..0817cbf --- /dev/null +++ b/scripting/include/bugreport.inc @@ -0,0 +1,42 @@ +#if defined _bugreport_included + #endinput +#endif +#define _bugreport_included + + +#define REASON_MAX_LENGTH 128 + +#define COOLDOWN 300 //in seconds (default: 300[5 minutes]) + +/** + * Called after a client (or module) has reported a bug for a map. + * Be sure to check that client != 0 if you expect a valid client index. + * + * @param client Client index of the caller. + * @param map DisplayName of map when client reported the bug. + * @param reason Reason selected by the client for the report. + * @param array Array with reasons that have already been reported in the current map. + * @noreturn + */ +forward void BugReport_OnReportPost(int client, const char[] map, const char[] reason, ArrayList array); + + +public SharedPlugin __pl_bugreport = +{ + name = "bugreport", + file = "bugreport.smx", +#if defined REQUIRE_PLUGIN + required = 1, +#else + required = 0, +#endif +}; + + + + +#if !defined REQUIRE_PLUGIN +public __pl_bugreport_SetNTVOptional() +{ +} +#endif diff --git a/scripting/include/smjansson.inc b/scripting/include/smjansson.inc new file mode 100644 index 0000000..e1936b8 --- /dev/null +++ b/scripting/include/smjansson.inc @@ -0,0 +1,1328 @@ +#if defined _jansson_included_ + #endinput +#endif +#define _jansson_included_ + + +/** + * --- Type + * + * The JSON specification (RFC 4627) defines the following data types: + * object, array, string, number, boolean, and null. + * JSON types are used dynamically; arrays and objects can hold any + * other data type, including themselves. For this reason, Jansson�s + * type system is also dynamic in nature. There�s one Handle type to + * represent all JSON values, and the referenced structure knows the + * type of the JSON value it holds. + * + */ +enum json_type { + JSON_OBJECT, + JSON_ARRAY, + JSON_STRING, + JSON_INTEGER, + JSON_REAL, + JSON_TRUE, + JSON_FALSE, + JSON_NULL +} + +/** + * Return the type of the JSON value. + * + * @param hObj Handle to the JSON value + * + * @return json_type of the value. + */ +native json_type:json_typeof(Handle:hObj); + +/** + * The type of a JSON value is queried and tested using these macros + * + * @param %1 Handle to the JSON value + * + * @return True if the value has the correct type. + */ +#define json_is_object(%1) ( json_typeof(%1) == JSON_OBJECT ) +#define json_is_array(%1) ( json_typeof(%1) == JSON_ARRAY ) +#define json_is_string(%1) ( json_typeof(%1) == JSON_STRING ) +#define json_is_integer(%1) ( json_typeof(%1) == JSON_INTEGER ) +#define json_is_real(%1) ( json_typeof(%1) == JSON_REAL ) +#define json_is_true(%1) ( json_typeof(%1) == JSON_TRUE ) +#define json_is_false(%1) ( json_typeof(%1) == JSON_FALSE ) +#define json_is_null(%1) ( json_typeof(%1) == JSON_NULL ) +#define json_is_number(%1) ( json_typeof(%1) == JSON_INTEGER || json_typeof(%1) == JSON_REAL ) +#define json_is_boolean(%1) ( json_typeof(%1) == JSON_TRUE || json_typeof(%1) == JSON_FALSE ) + +/** + * Saves json_type as a String in output + * + * @param input json_type value to convert to string + * @param output Buffer to store the json_type value + * @param maxlength Maximum length of string buffer. + * + * @return False if the type does not exist. + */ +stock bool:Stringify_json_type(json_type:input, String:output[], maxlength) { + switch(input) { + case JSON_OBJECT: strcopy(output, maxlength, "Object"); + case JSON_ARRAY: strcopy(output, maxlength, "Array"); + case JSON_STRING: strcopy(output, maxlength, "String"); + case JSON_INTEGER: strcopy(output, maxlength, "Integer"); + case JSON_REAL: strcopy(output, maxlength, "Real"); + case JSON_TRUE: strcopy(output, maxlength, "True"); + case JSON_FALSE: strcopy(output, maxlength, "False"); + case JSON_NULL: strcopy(output, maxlength, "Null"); + default: return false; + } + + return true; +} + + + +/** + * --- Equality + * + * - Two integer or real values are equal if their contained numeric + * values are equal. An integer value is never equal to a real value, + * though. + * - Two strings are equal if their contained UTF-8 strings are equal, + * byte by byte. Unicode comparison algorithms are not implemented. + * - Two arrays are equal if they have the same number of elements and + * each element in the first array is equal to the corresponding + * element in the second array. + * - Two objects are equal if they have exactly the same keys and the + * value for each key in the first object is equal to the value of + * the corresponding key in the second object. + * - Two true, false or null values have no "contents", so they are + * equal if their types are equal. + * + */ + +/** + * Test whether two JSON values are equal. + * + * @param hObj Handle to the first JSON object + * @param hOther Handle to the second JSON object + * + * @return Returns false if they are inequal or one + * or both of the pointers are NULL. + */ +native bool:json_equal(Handle:hObj, Handle:hOther); + + + + +/** + * --- Copying + * + * Jansson supports two kinds of copying: shallow and deep. There is + * a difference between these methods only for arrays and objects. + * + * Shallow copying only copies the first level value (array or object) + * and uses the same child values in the copied value. + * + * Deep copying makes a fresh copy of the child values, too. Moreover, + * all the child values are deep copied in a recursive fashion. + * + */ + +/** + * Get a shallow copy of the passed object + * + * @param hObj Handle to JSON object to be copied + * + * @return Returns a shallow copy of the object, + * or INVALID_HANDLE on error. + */ +native Handle:json_copy(Handle:hObj); + +/** + * Get a deep copy of the passed object + * + * @param hObj Handle to JSON object to be copied + * + * @return Returns a deep copy of the object, + * or INVALID_HANDLE on error. + */ +native Handle:json_deep_copy(Handle:hObj); + + + + +/** + * --- Objects + * + * A JSON object is a dictionary of key-value pairs, where the + * key is a Unicode string and the value is any JSON value. + * + */ + +/** + * Returns a handle to a new JSON object, or INVALID_HANDLE on error. + * Initially, the object is empty. + * + * @return Handle to a new JSON object. + */ +native Handle:json_object(); + +/** + * Returns the number of elements in hObj + * + * @param hObj Handle to JSON object + * + * @return Number of elements in hObj, + * or 0 if hObj is not a JSON object. + */ +native json_object_size(Handle:hObj); + +/** + * Get a value corresponding to sKey from hObj + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Key to retrieve + * + * @return Handle to a the JSON object or + * INVALID_HANDLE on error. + */ +native Handle:json_object_get(Handle:hObj, const String:sKey[]); + +/** + * Set the value of sKey to hValue in hObj. + * If there already is a value for key, it is replaced by the new value. + * + * @param hObj Handle to JSON object to set a value on + * @param sKey Key to store in the object + * Must be a valid null terminated UTF-8 encoded + * Unicode string. + * @param hValue Value to store in the object + * + * @return True on success. + */ +native bool:json_object_set(Handle:hObj, const String:sKey[], Handle:hValue); + +/** + * Set the value of sKey to hValue in hObj. + * If there already is a value for key, it is replaced by the new value. + * This function automatically closes the Handle to the value object. + * + * @param hObj Handle to JSON object to set a value on + * @param sKey Key to store in the object + * Must be a valid null terminated UTF-8 encoded + * Unicode string. + * @param hValue Value to store in the object + * + * @return True on success. + */ +native bool:json_object_set_new(Handle:hObj, const String:sKey[], Handle:hValue); + +/** + * Delete sKey from hObj if it exists. + * + * @param hObj Handle to JSON object to delete a key from + * @param sKey Key to delete + * + * @return True on success. + */ +native bool:json_object_del(Handle:hObj, const String:sKey[]); + +/** + * Remove all elements from hObj. + * + * @param hObj Handle to JSON object to remove all + * elements from. + * + * @return True on success. + */ +native bool:json_object_clear(Handle:hObj); + +/** + * Update hObj with the key-value pairs from hOther, overwriting + * existing keys. + * + * @param hObj Handle to JSON object to update + * @param hOther Handle to JSON object to get update + * keys/values from. + * + * @return True on success. + */ +native bool:json_object_update(Handle:hObj, Handle:hOther); + +/** + * Like json_object_update(), but only the values of existing keys + * are updated. No new keys are created. + * + * @param hObj Handle to JSON object to update + * @param hOther Handle to JSON object to get update + * keys/values from. + * + * @return True on success. + */ +native bool:json_object_update_existing(Handle:hObj, Handle:hOther); + +/** + * Like json_object_update(), but only new keys are created. + * The value of any existing key is not changed. + * + * @param hObj Handle to JSON object to update + * @param hOther Handle to JSON object to get update + * keys/values from. + * + * @return True on success. + */ +native bool:json_object_update_missing(Handle:hObj, Handle:hOther); + + + + +/** + * Object iteration + * + * Example code: + * - We assume hObj is a Handle to a valid JSON object. + * + * + * new Handle:hIterator = json_object_iter(hObj); + * while(hIterator != INVALID_HANDLE) + * { + * new String:sKey[128]; + * json_object_iter_key(hIterator, sKey, sizeof(sKey)); + * + * new Handle:hValue = json_object_iter_value(hIterator); + * + * // Do something with sKey and hValue + * + * CloseHandle(hValue); + * + * hIterator = json_object_iter_next(hObj, hIterator); + * } + * + */ + +/** + * Returns a handle to an iterator which can be used to iterate over + * all key-value pairs in hObj. + * If you are not iterating to the end of hObj make sure to close the + * handle to the iterator manually. + * + * @param hObj Handle to JSON object to get an iterator + * for. + * + * @return Handle to JSON object iterator, + * or INVALID_HANDLE on error. + */ +native Handle:json_object_iter(Handle:hObj); + +/** + * Like json_object_iter(), but returns an iterator to the key-value + * pair in object whose key is equal to key. + * Iterating forward to the end of object only yields all key-value + * pairs of the object if key happens to be the first key in the + * underlying hash table. + * + * @param hObj Handle to JSON object to get an iterator + * for. + * @param sKey Start key for the iterator + * + * @return Handle to JSON object iterator, + * or INVALID_HANDLE on error. + */ +native Handle:json_object_iter_at(Handle:hObj, const String:key[]); + +/** + * Returns an iterator pointing to the next key-value pair in object. + * This automatically closes the Handle to the iterator hIter. + * + * @param hObj Handle to JSON object. + * @param hIter Handle to JSON object iterator. + * + * @return Handle to JSON object iterator, + * or INVALID_HANDLE on error, or if the + * whole object has been iterated through. + */ +native Handle:json_object_iter_next(Handle:hObj, Handle:hIter); + +/** + * Extracts the associated key of hIter as a null terminated UTF-8 + * encoded string in the passed buffer. + * + * @param hIter Handle to the JSON String object + * @param sKeyBuffer Buffer to store the value of the String. + * @param maxlength Maximum length of string buffer. + * @error Invalid JSON Object Iterator. + * @return Length of the returned string or -1 on error. + */ +native json_object_iter_key(Handle:hIter, String:sKeyBuffer[], maxlength); + +/** + * Returns a handle to the value hIter is pointing at. + * + * @param hIter Handle to JSON object iterator. + * + * @return Handle to value or INVALID_HANDLE on error. + */ +native Handle:json_object_iter_value(Handle:hIter); + +/** + * Set the value of the key-value pair in hObj, that is pointed to + * by hIter, to hValue. + * + * @param hObj Handle to JSON object. + * @param hIter Handle to JSON object iterator. + * @param hValue Handle to JSON value. + * + * @return True on success. + */ +native bool:json_object_iter_set(Handle:hObj, Handle:hIter, Handle:hValue); + +/** + * Set the value of the key-value pair in hObj, that is pointed to + * by hIter, to hValue. + * This function automatically closes the Handle to the value object. + * + * @param hObj Handle to JSON object. + * @param hIter Handle to JSON object iterator. + * @param hValue Handle to JSON value. + * + * @return True on success. + */ +native bool:json_object_iter_set_new(Handle:hObj, Handle:hIter, Handle:hValue); + + + + +/** + * Arrays + * + * A JSON array is an ordered collection of other JSON values. + * + */ + +/** + * Returns a handle to a new JSON array, or INVALID_HANDLE on error. + * + * @return Handle to the new JSON array + */ +native Handle:json_array(); + +/** + * Returns the number of elements in hArray + * + * @param hObj Handle to JSON array + * + * @return Number of elements in hArray, + * or 0 if hObj is not a JSON array. + */ +native json_array_size(Handle:hArray); + +/** + * Returns the element in hArray at position iIndex. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * + * @return Handle to a the JSON object or + * INVALID_HANDLE on error. + */ +native Handle:json_array_get(Handle:hArray, iIndex); + +/** + * Replaces the element in array at position iIndex with hValue. + * The valid range for iIndex is from 0 to the return value of + * json_array_size() minus 1. + * + * @param hArray Handle to JSON array + * @param iIndex Position to replace + * @param hValue Value to store in the array + * + * @return True on success. + */ +native bool:json_array_set(Handle:hArray, iIndex, Handle:hValue); + +/** + * Replaces the element in array at position iIndex with hValue. + * The valid range for iIndex is from 0 to the return value of + * json_array_size() minus 1. + * This function automatically closes the Handle to the value object. + * + * @param hArray Handle to JSON array + * @param iIndex Position to replace + * @param hValue Value to store in the array + * + * @return True on success. + */ +native bool:json_array_set_new(Handle:hArray, iIndex, Handle:hValue); + +/** + * Appends value to the end of array, growing the size of array by 1. + * + * @param hArray Handle to JSON array + * @param hValue Value to append to the array + * + * @return True on success. + */ +native bool:json_array_append(Handle:hArray, Handle:hValue); + +/** + * Appends value to the end of array, growing the size of array by 1. + * This function automatically closes the Handle to the value object. + * + * @param hArray Handle to JSON array + * @param hValue Value to append to the array + * + * @return True on success. + */ +native bool:json_array_append_new(Handle:hArray, Handle:hValue); + +/** + * Inserts value to hArray at position iIndex, shifting the elements at + * iIndex and after it one position towards the end of the array. + * + * @param hArray Handle to JSON array + * @param iIndex Position to insert at + * @param hValue Value to store in the array + * + * @return True on success. + */ +native bool:json_array_insert(Handle:hArray, iIndex, Handle:hValue); + +/** + * Inserts value to hArray at position iIndex, shifting the elements at + * iIndex and after it one position towards the end of the array. + * This function automatically closes the Handle to the value object. + * + * @param hArray Handle to JSON array + * @param iIndex Position to insert at + * @param hValue Value to store in the array + * + * @return True on success. + */ +native bool:json_array_insert_new(Handle:hArray, iIndex, Handle:hValue); + +/** + * Removes the element in hArray at position iIndex, shifting the + * elements after iIndex one position towards the start of the array. + * + * @param hArray Handle to JSON array + * @param iIndex Position to insert at + * + * @return True on success. + */ +native bool:json_array_remove(Handle:hArray, iIndex); + +/** + * Removes all elements from hArray. + * + * @param hArray Handle to JSON array + * + * @return True on success. + */ +native bool:json_array_clear(Handle:hArray); + +/** + * Appends all elements in hOther to the end of hArray. + * + * @param hArray Handle to JSON array to be extended + * @param hOther Handle to JSON array, source to copy from + * + * @return True on success. + */ +native bool:json_array_extend(Handle:hArray, Handle:hOther); + + + + +/** + * Booleans & NULL + * + */ + +/** + * Returns a handle to a new JSON Boolean with value true, + * or INVALID_HANDLE on error. + * + * @return Handle to the new Boolean object + */ +native Handle:json_true(); + +/** + * Returns a handle to a new JSON Boolean with value false, + * or INVALID_HANDLE on error. + * + * @return Handle to the new Boolean object + */ +native Handle:json_false(); + +/** + * Returns a handle to a new JSON Boolean with the value passed + * in bState or INVALID_HANDLE on error. + * + * @param bState Value for the new Boolean object + * @return Handle to the new Boolean object + */ +native Handle:json_boolean(bool:bState); + +/** + * Returns a handle to a new JSON NULL or INVALID_HANDLE on error. + * + * @return Handle to the new NULL object + */ +native Handle:json_null(); + + + + +/** + * Strings + * + * Jansson uses UTF-8 as the character encoding. All JSON strings must + * be valid UTF-8 (or ASCII, as it�s a subset of UTF-8). Normal null + * terminated C strings are used, so JSON strings may not contain + * embedded null characters. + * + */ + +/** + * Returns a handle to a new JSON string, or INVALID_HANDLE on error. + * + * @param sValue Value for the new String object + * Must be a valid UTF-8 encoded Unicode string. + * @return Handle to the new String object + */ +native Handle:json_string(const String:sValue[]); + +/** + * Saves the associated value of hString as a null terminated UTF-8 + * encoded string in the passed buffer. + * + * @param hString Handle to the JSON String object + * @param sValueBuffer Buffer to store the value of the String. + * @param maxlength Maximum length of string buffer. + * @error Invalid JSON String Object. + * @return Length of the returned string or -1 on error. + */ +native json_string_value(Handle:hString, String:sValueBuffer[], maxlength); + +/** + * Sets the associated value of JSON String object to value. + * + * @param hString Handle to the JSON String object + * @param sValue Value to set the object to. + * Must be a valid UTF-8 encoded Unicode string. + * @error Invalid JSON String Object. + * @return True on success. + */ +native bool:json_string_set(Handle:hString, String:sValue[]); + + + + +/** + * Numbers + * + * The JSON specification only contains one numeric type, 'number'. + * The C (and Pawn) programming language has distinct types for integer + * and floating-point numbers, so for practical reasons Jansson also has + * distinct types for the two. They are called 'integer' and 'real', + * respectively. (Whereas 'real' is a 'Float' for Pawn). + * Therefore a number is represented by either a value of the type + * JSON_INTEGER or of the type JSON_REAL. + * + */ + +/** + * Returns a handle to a new JSON integer, or INVALID_HANDLE on error. + * + * @param iValue Value for the new Integer object + * @return Handle to the new Integer object + */ +native Handle:json_integer(iValue); + +/** + * Returns the associated value of a JSON Integer Object. + * + * @param hInteger Handle to the JSON Integer object + * @error Invalid JSON Integer Object. + * @return Value of the hInteger, + * or 0 if hInteger is not a JSON integer. + */ +native json_integer_value(Handle:hInteger); + +/** + * Sets the associated value of JSON Integer to value. + * + * @param hInteger Handle to the JSON Integer object + * @param iValue Value to set the object to. + * @error Invalid JSON Integer Object. + * @return True on success. + */ +native bool:json_integer_set(Handle:hInteger, iValue); + +/** + * Returns a handle to a new JSON real, or INVALID_HANDLE on error. + * + * @param fValue Value for the new Real object + * @return Handle to the new String object + */ +native Handle:json_real(Float:fValue); + +/** + * Returns the associated value of a JSON Real. + * + * @param hReal Handle to the JSON Real object + * @error Invalid JSON Real Object. + * @return Float value of hReal, + * or 0.0 if hReal is not a JSON Real. + */ +native Float:json_real_value(Handle:hReal); + +/** + * Sets the associated value of JSON Real to fValue. + * + * @param hReal Handle to the JSON Integer object + * @param fValue Value to set the object to. + * @error Invalid JSON Real handle. + * @return True on success. + */ +native bool:json_real_set(Handle:hReal, Float:value); + +/** + * Returns the associated value of a JSON integer or a + * JSON Real, cast to Float regardless of the actual type. + * + * @param hNumber Handle to the JSON Number + * @error Not a JSON Real or JSON Integer + * @return Float value of hNumber, + * or 0.0 on error. + */ +native Float:json_number_value(Handle:hNumber); + + + + +/** + * Decoding + * + * This sections describes the functions that can be used to decode JSON text + * to the Jansson representation of JSON data. The JSON specification requires + * that a JSON text is either a serialized array or object, and this + * requirement is also enforced with the following functions. In other words, + * the top level value in the JSON text being decoded must be either array or + * object. + * + */ + +/** + * Decodes the JSON string sJSON and returns the array or object it contains. + * Errors while decoding can be found in the sourcemod error log. + * + * @param sJSON String containing valid JSON + + * @return Handle to JSON object or array. + * or INVALID_HANDLE on error. + */ +native Handle:json_load(const String:sJSON[]); + +/** + * Decodes the JSON string sJSON and returns the array or object it contains. + * This function provides additional error feedback and does not log errors + * to the sourcemod error log. + * + * @param sJSON String containing valid JSON + * @param sErrorText This buffer will be filled with the error + * message. + * @param maxlen Size of the buffer + * @param iLine This int will contain the line of the error + * @param iColumn This int will contain the column of the error + * + * @return Handle to JSON object or array. + * or INVALID_HANDLE on error. + */ +native Handle:json_load_ex(const String:sJSON[], String:sErrorText[], maxlen, &iLine, &iColumn); + +/** + * Decodes the JSON text in file sFilePath and returns the array or object + * it contains. + * Errors while decoding can be found in the sourcemod error log. + * + * @param sFilePath Path to a file containing pure JSON + * + * @return Handle to JSON object or array. + * or INVALID_HANDLE on error. + */ +native Handle:json_load_file(const String:sFilePath[PLATFORM_MAX_PATH]); + +/** + * Decodes the JSON text in file sFilePath and returns the array or object + * it contains. + * This function provides additional error feedback and does not log errors + * to the sourcemod error log. + * + * @param sFilePath Path to a file containing pure JSON + * @param sErrorText This buffer will be filled with the error + * message. + * @param maxlen Size of the buffer + * @param iLine This int will contain the line of the error + * @param iColumn This int will contain the column of the error + * + * @return Handle to JSON object or array. + * or INVALID_HANDLE on error. + */ +native Handle:json_load_file_ex(const String:sFilePath[PLATFORM_MAX_PATH], String:sErrorText[], maxlen, &iLine, &iColumn); + + + +/** + * Encoding + * + * This sections describes the functions that can be used to encode values + * to JSON. By default, only objects and arrays can be encoded directly, + * since they are the only valid root values of a JSON text. + * + */ + +/** + * Saves the JSON representation of hObject in sJSON. + * + * @param hObject String containing valid JSON + * @param sJSON Buffer to store the created JSON string. + * @param maxlength Maximum length of string buffer. + * @param iIndentWidth Indenting with iIndentWidth spaces. + * The valid range for this is between 0 and 31 (inclusive), + * other values result in an undefined output. If this is set + * to 0, no newlines are inserted between array and object items. + * @param bEnsureAscii If this is set, the output is guaranteed + * to consist only of ASCII characters. This is achieved + * by escaping all Unicode characters outside the ASCII range. + * @param bSortKeys If this flag is used, all the objects in output are sorted + * by key. This is useful e.g. if two JSON texts are diffed + * or visually compared. + * @param bPreserveOrder If this flag is used, object keys in the output are sorted + * into the same order in which they were first inserted to + * the object. For example, decoding a JSON text and then + * encoding with this flag preserves the order of object keys. + * @return Length of the returned string or -1 on error. + */ +native json_dump(Handle:hObject, String:sJSON[], maxlength, iIndentWidth = 4, bool:bEnsureAscii = false, bool:bSortKeys = false, bool:bPreserveOrder = false); + +/** + * Write the JSON representation of hObject to the file sFilePath. + * If sFilePath already exists, it is overwritten. + * + * @param hObject String containing valid JSON + * @param sFilePath Buffer to store the created JSON string. + * @param iIndentWidth Indenting with iIndentWidth spaces. + * The valid range for this is between 0 and 31 (inclusive), + * other values result in an undefined output. If this is set + * to 0, no newlines are inserted between array and object items. + * @param bEnsureAscii If this is set, the output is guaranteed + * to consist only of ASCII characters. This is achieved + * by escaping all Unicode characters outside the ASCII range. + * @param bSortKeys If this flag is used, all the objects in output are sorted + * by key. This is useful e.g. if two JSON texts are diffed + * or visually compared. + * @param bPreserveOrder If this flag is used, object keys in the output are sorted + * into the same order in which they were first inserted to + * the object. For example, decoding a JSON text and then + * encoding with this flag preserves the order of object keys. + * @return Length of the returned string or -1 on error. + */ +native bool:json_dump_file(Handle:hObject, const String:sFilePath[], iIndentWidth = 4, bool:bEnsureAscii = false, bool:bSortKeys = false, bool:bPreserveOrder = false); + + + +/** + * Convenience stocks + * + * These are some custom functions to ease the development using this + * extension. + * + */ + +/** + * Returns a handle to a new JSON string, or INVALID_HANDLE on error. + * Formats the string according to the SourceMod format rules. + * The result must be a valid UTF-8 encoded Unicode string. + * + * @param sFormat Formatting rules. + * @param ... Variable number of format parameters. + * @return Handle to the new String object + */ +stock Handle:json_string_format(const String:sFormat[], any:...) { + new String:sTmp[4096]; + VFormat(sTmp, sizeof(sTmp), sFormat, 2); + + return json_string(sTmp); +} + +/** + * Returns a handle to a new JSON string, or INVALID_HANDLE on error. + * This stock allows to specify the size of the temporary buffer used + * to create the string. Use this if the default of 4096 is not enough + * for your string. + * Formats the string according to the SourceMod format rules. + * The result must be a valid UTF-8 encoded Unicode string. + * + * @param tmpBufferLength Size of the temporary buffer + * @param sFormat Formatting rules. + * @param ... Variable number of format parameters. + * @return Handle to the new String object + */ +stock Handle:json_string_format_ex(tmpBufferLength, const String:sFormat[], any:...) { + new String:sTmp[tmpBufferLength]; + VFormat(sTmp, sizeof(sTmp), sFormat, 3); + + return json_string(sTmp); +} + + +/** + * Returns the boolean value of the element in hArray at position iIndex. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * + * @return True if it's a boolean and TRUE, + * false otherwise. + */ +stock bool:json_array_get_bool(Handle:hArray, iIndex) { + new Handle:hElement = json_array_get(hArray, iIndex); + + new bool:bResult = (json_is_true(hElement) ? true : false); + + CloseHandle(hElement); + return bResult; +} + +/** + * Returns the float value of the element in hArray at position iIndex. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * + * @return Float value, + * or 0.0 if element is not a JSON Real. + */ +stock Float:json_array_get_float(Handle:hArray, iIndex) { + new Handle:hElement = json_array_get(hArray, iIndex); + + new Float:fResult = (json_is_number(hElement) ? json_number_value(hElement) : 0.0); + + CloseHandle(hElement); + return fResult; +} + +/** + * Returns the integer value of the element in hArray at position iIndex. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * + * @return Integer value, + * or 0 if element is not a JSON Integer. + */ +stock json_array_get_int(Handle:hArray, iIndex) { + new Handle:hElement = json_array_get(hArray, iIndex); + + new iResult = (json_is_integer(hElement) ? json_integer_value(hElement) : 0); + + CloseHandle(hElement); + return iResult; +} + +/** + * Saves the associated value of the element in hArray at position iIndex + * as a null terminated UTF-8 encoded string in the passed buffer. + * + * @param hArray Handle to JSON array to get a value from + * @param iIndex Position to retrieve + * @param sBuffer Buffer to store the value of the String. + * @param maxlength Maximum length of string buffer. + * + * @error Element is not a JSON String. + * @return Length of the returned string or -1 on error. + */ +stock json_array_get_string(Handle:hArray, iIndex, String:sBuffer[], maxlength) { + new Handle:hElement = json_array_get(hArray, iIndex); + + new iResult = -1; + if(json_is_string(hElement)) { + iResult = json_string_value(hElement, sBuffer, maxlength); + } + CloseHandle(hElement); + + return iResult; +} + +/** + * Returns the boolean value of the element in hObj at entry sKey. + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Entry to retrieve + * + * @return True if it's a boolean and TRUE, + * false otherwise. + */ +stock bool:json_object_get_bool(Handle:hObj, const String:sKey[]) { + new Handle:hElement = json_object_get(hObj, sKey); + + new bool:bResult = (json_is_true(hElement) ? true : false); + + CloseHandle(hElement); + return bResult; +} + +/** + * Returns the float value of the element in hObj at entry sKey. + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Position to retrieve + * + * @return Float value, + * or 0.0 if element is not a JSON Real. + */ +stock Float:json_object_get_float(Handle:hObj, const String:sKey[]) { + new Handle:hElement = json_object_get(hObj, sKey); + + new Float:fResult = (json_is_number(hElement) ? json_number_value(hElement) : 0.0); + + CloseHandle(hElement); + return fResult; +} + +/** + * Returns the integer value of the element in hObj at entry sKey. + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Position to retrieve + * + * @return Integer value, + * or 0 if element is not a JSON Integer. + */ +stock json_object_get_int(Handle:hObj, const String:sKey[]) { + new Handle:hElement = json_object_get(hObj, sKey); + + new iResult = (json_is_integer(hElement) ? json_integer_value(hElement) : 0); + + CloseHandle(hElement); + return iResult; +} + +/** + * Saves the associated value of the element in hObj at entry sKey + * as a null terminated UTF-8 encoded string in the passed buffer. + * + * @param hObj Handle to JSON object to get a value from + * @param sKey Entry to retrieve + * @param sBuffer Buffer to store the value of the String. + * @param maxlength Maximum length of string buffer. + * + * @error Element is not a JSON String. + * @return Length of the returned string or -1 on error. + */ +stock json_object_get_string(Handle:hObj, const String:sKey[], String:sBuffer[], maxlength) { + new Handle:hElement = json_object_get(hObj, sKey); + + new iResult = -1; + if(json_is_string(hElement)) { + iResult = json_string_value(hElement, sBuffer, maxlength); + } + CloseHandle(hElement); + + return iResult; +} + + + +/** + * Pack String Rules + * + * Here�s the full list of format characters: + * n Output a JSON null value. No argument is consumed. + * s Output a JSON string, consuming one argument. + * b Output a JSON bool value, consuming one argument. + * i Output a JSON integer value, consuming one argument. + * f Output a JSON real value, consuming one argument. + * r Output a JSON real value, consuming one argument. + * [] Build an array with contents from the inner format string, + * recursive value building is supported. + * No argument is consumed. + * {} Build an array with contents from the inner format string. + * The first, third, etc. format character represent a key, + * and must be s (as object keys are always strings). The + * second, fourth, etc. format character represent a value. + * Recursive value building is supported. + * No argument is consumed. + * + */ + +/** + * This method can be used to create json objects/arrays directly + * without having to create the structure. + * See 'Pack String Rules' for more details. + * + * @param sPackString Pack string similiar to Format()s fmt. + * See 'Pack String Rules'. + * @param hParams ADT Array containing all keys and values + * in the order they appear in the pack string. + * + * @error Invalid pack string or pack string and + * ADT Array don't match up regarding type + * or size. + * @return Handle to JSON element. + */ +stock Handle:json_pack(const String:sPackString[], Handle:hParams) { + new iPos = 0; + return json_pack_element_(sPackString, iPos, hParams); +} + + + + + +/** +* Internal stocks used by json_pack(). Don't use these directly! +* +*/ +stock Handle:json_pack_array_(const String:sFormat[], &iPos, Handle:hParams) { + new Handle:hObj = json_array(); + new iStrLen = strlen(sFormat); + for(; iPos < iStrLen;) { + new this_char = sFormat[iPos]; + + if(this_char == 32 || this_char == 58 || this_char == 44) { + // Skip whitespace, ',' and ':' + iPos++; + continue; + } + + if(this_char == 93) { + // array end + iPos++; + break; + } + + // Get the next entry as value + // This automatically increments the position! + new Handle:hValue = json_pack_element_(sFormat, iPos, hParams); + + // Append the value to the array. + json_array_append_new(hObj, hValue); + } + + return hObj; +} + +stock Handle:json_pack_object_(const String:sFormat[], &iPos, Handle:hParams) { + new Handle:hObj = json_object(); + new iStrLen = strlen(sFormat); + for(; iPos < iStrLen;) { + new this_char = sFormat[iPos]; + + if(this_char == 32 || this_char == 58 || this_char == 44) { + // Skip whitespace, ',' and ':' + iPos++; + continue; + } + + if(this_char == 125) { + // } --> object end + iPos++; + break; + } + + if(this_char != 115) { + LogError("Object keys must be strings at %d.", iPos); + return INVALID_HANDLE; + } + + // Get the key string for this object from + // the hParams array. + decl String:sKey[255]; + GetArrayString(hParams, 0, sKey, sizeof(sKey)); + RemoveFromArray(hParams, 0); + + // Advance one character in the pack string, + // because we've just read the Key string for + // this object. + iPos++; + + // Get the next entry as value + // This automatically increments the position! + new Handle:hValue = json_pack_element_(sFormat, iPos, hParams); + + // Insert into object + json_object_set_new(hObj, sKey, hValue); + } + + return hObj; +} + +stock Handle:json_pack_element_(const String:sFormat[], &iPos, Handle:hParams) { + new this_char = sFormat[iPos]; + while(this_char == 32 || this_char == 58 || this_char == 44) { + iPos++; + this_char = sFormat[iPos]; + } + + // Advance one character in the pack string + iPos++; + + switch(this_char) { + case 91: { + // { --> Array + return json_pack_array_(sFormat, iPos, hParams); + } + + case 123: { + // { --> Object + return json_pack_object_(sFormat, iPos, hParams); + + } + + case 98: { + // b --> Boolean + new iValue = GetArrayCell(hParams, 0); + RemoveFromArray(hParams, 0); + + return json_boolean(bool:iValue); + } + + case 102, 114: { + // r,f --> Real (Float) + new Float:iValue = GetArrayCell(hParams, 0); + RemoveFromArray(hParams, 0); + + return json_real(iValue); + } + + case 110: { + // n --> NULL + return json_null(); + } + + case 115: { + // s --> String + decl String:sKey[255]; + GetArrayString(hParams, 0, sKey, sizeof(sKey)); + RemoveFromArray(hParams, 0); + + return json_string(sKey); + } + + case 105: { + // i --> Integer + new iValue = GetArrayCell(hParams, 0); + RemoveFromArray(hParams, 0); + + return json_integer(iValue); + } + } + + SetFailState("Invalid pack String '%s'. Type '%s' not supported at %i", sFormat, this_char, iPos); + return json_null(); +} + + + + + +/** + * Not yet implemented + * + * native json_object_foreach(Handle:hObj, ForEachCallback:cb); + * native Handle:json_unpack(const String:sFormat[], ...); + * + */ + + + + + + +/** + * Do not edit below this line! + */ +public Extension:__ext_smjansson = +{ + name = "SMJansson", + file = "smjansson.ext", +#if defined AUTOLOAD_EXTENSIONS + autoload = 1, +#else + autoload = 0, +#endif +#if defined REQUIRE_EXTENSIONS + required = 1, +#else + required = 0, +#endif +}; + +#if !defined REQUIRE_EXTENSIONS +public __ext_smjansson_SetNTVOptional() +{ + MarkNativeAsOptional("json_typeof"); + MarkNativeAsOptional("json_equal"); + + MarkNativeAsOptional("json_copy"); + MarkNativeAsOptional("json_deep_copy"); + + MarkNativeAsOptional("json_object"); + MarkNativeAsOptional("json_object_size"); + MarkNativeAsOptional("json_object_get"); + MarkNativeAsOptional("json_object_set"); + MarkNativeAsOptional("json_object_set_new"); + MarkNativeAsOptional("json_object_del"); + MarkNativeAsOptional("json_object_clear"); + MarkNativeAsOptional("json_object_update"); + MarkNativeAsOptional("json_object_update_existing"); + MarkNativeAsOptional("json_object_update_missing"); + + MarkNativeAsOptional("json_object_iter"); + MarkNativeAsOptional("json_object_iter_at"); + MarkNativeAsOptional("json_object_iter_next"); + MarkNativeAsOptional("json_object_iter_key"); + MarkNativeAsOptional("json_object_iter_value"); + MarkNativeAsOptional("json_object_iter_set"); + MarkNativeAsOptional("json_object_iter_set_new"); + + MarkNativeAsOptional("json_array"); + MarkNativeAsOptional("json_array_size"); + MarkNativeAsOptional("json_array_get"); + MarkNativeAsOptional("json_array_set"); + MarkNativeAsOptional("json_array_set_new"); + MarkNativeAsOptional("json_array_append"); + MarkNativeAsOptional("json_array_append_new"); + MarkNativeAsOptional("json_array_insert"); + MarkNativeAsOptional("json_array_insert_new"); + MarkNativeAsOptional("json_array_remove"); + MarkNativeAsOptional("json_array_clear"); + MarkNativeAsOptional("json_array_extend"); + + MarkNativeAsOptional("json_string"); + MarkNativeAsOptional("json_string_value"); + MarkNativeAsOptional("json_string_set"); + + MarkNativeAsOptional("json_integer"); + MarkNativeAsOptional("json_integer_value"); + MarkNativeAsOptional("json_integer_set"); + + MarkNativeAsOptional("json_real"); + MarkNativeAsOptional("json_real_value"); + MarkNativeAsOptional("json_real_set"); + MarkNativeAsOptional("json_number_value"); + + MarkNativeAsOptional("json_boolean"); + MarkNativeAsOptional("json_true"); + MarkNativeAsOptional("json_false"); + MarkNativeAsOptional("json_null"); + + MarkNativeAsOptional("json_load"); + MarkNativeAsOptional("json_load_file"); + + MarkNativeAsOptional("json_dump"); + MarkNativeAsOptional("json_dump_file"); +} +#endif \ No newline at end of file diff --git a/translations/Discord-Utilities.phrases.txt b/translations/Discord-Utilities.phrases.txt index e5bd057..1983ecf 100644 --- a/translations/Discord-Utilities.phrases.txt +++ b/translations/Discord-Utilities.phrases.txt @@ -467,11 +467,17 @@ "AlreadyVerified" { - "en" "- You are already verified. Enjoy your benefits :)" + "en" "You are already verified. Enjoy your benefits :)" } "CanChange" { - "en" "- You can change your verified discord account using {LIME}!unverify{DEFAULT} and {LIME}!verify{DEFAULT} again" + "#format" "{1:s},{2:s}" + "en" "You can change your verified discord account using {LIME}{1}{DEFAULT} and {LIME}{2}{DEFAULT} again." + } + + "SuccessfullyUnlink" + { + "en" "You successfully unlinked your account." } } From 086cb94af6f9d4bca3e2e84ad84c23bade556e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AiDN=E2=84=A2?= <45371311+originalaidn@users.noreply.github.com> Date: Mon, 11 Apr 2022 21:42:22 +0200 Subject: [PATCH 6/9] fix --- scripting/discord_utilities/forwards.sp | 2 +- scripting/du_chatrelay.sp | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/scripting/discord_utilities/forwards.sp b/scripting/discord_utilities/forwards.sp index 00007c0..8ab9988 100644 --- a/scripting/discord_utilities/forwards.sp +++ b/scripting/discord_utilities/forwards.sp @@ -250,7 +250,7 @@ public Action Command_ViewId(int client, int args) public Action Check(int client, const char[] command, int args) { - if(!client || client > MaxClients && IsClientInGame(client)) + if(IsClientValid(client) && !client || client > MaxClients) { return Plugin_Continue; } diff --git a/scripting/du_chatrelay.sp b/scripting/du_chatrelay.sp index 0bac00f..6efe134 100644 --- a/scripting/du_chatrelay.sp +++ b/scripting/du_chatrelay.sp @@ -85,7 +85,7 @@ public Plugin myinfo = name = "Discord Utilities - Chatrelay module", author = "AiDN™ & Cruze03", description = "Chatrelay module for the Discord Utilities, code from Cruze03", - version = "1.0", + version = "1.1", url = "https://steamcommunity.com/id/originalaidn & https://github.com/Cruze03/discord-utilities" }; @@ -277,7 +277,7 @@ public Action Command_AdminChat(int client, const char[] command, int argc) { return Plugin_Continue; } - if(g_bBaseComm && BaseComm_IsClientGagged(client)) + if(IsValidClient(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) { return Plugin_Continue; } @@ -296,7 +296,7 @@ public Action OnClientSayCommand(int client, const char[] command, const char[] { return Plugin_Continue; } - if(IsClientInGame(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) + if(IsValidClient(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) { return Plugin_Continue; } @@ -738,4 +738,9 @@ public void APIWebResponse(const char[] sData, int client) stock void GetGuilds() { Bot.GetGuilds(GuildList, _, true); -} \ No newline at end of file +} + +stock bool IsValidClient(int client) +{ + return (1 <= client <= MaxClients && IsClientInGame(client)); +} \ No newline at end of file From bda9656d31de923e749ecd41d5343007c285d055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AiDN=E2=84=A2?= <45371311+originalaidn@users.noreply.github.com> Date: Mon, 11 Apr 2022 21:53:50 +0200 Subject: [PATCH 7/9] Revert "fix" This reverts commit 086cb94af6f9d4bca3e2e84ad84c23bade556e7e. --- scripting/discord_utilities/forwards.sp | 2 +- scripting/du_chatrelay.sp | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/scripting/discord_utilities/forwards.sp b/scripting/discord_utilities/forwards.sp index 8ab9988..00007c0 100644 --- a/scripting/discord_utilities/forwards.sp +++ b/scripting/discord_utilities/forwards.sp @@ -250,7 +250,7 @@ public Action Command_ViewId(int client, int args) public Action Check(int client, const char[] command, int args) { - if(IsClientValid(client) && !client || client > MaxClients) + if(!client || client > MaxClients && IsClientInGame(client)) { return Plugin_Continue; } diff --git a/scripting/du_chatrelay.sp b/scripting/du_chatrelay.sp index 6efe134..0bac00f 100644 --- a/scripting/du_chatrelay.sp +++ b/scripting/du_chatrelay.sp @@ -85,7 +85,7 @@ public Plugin myinfo = name = "Discord Utilities - Chatrelay module", author = "AiDN™ & Cruze03", description = "Chatrelay module for the Discord Utilities, code from Cruze03", - version = "1.1", + version = "1.0", url = "https://steamcommunity.com/id/originalaidn & https://github.com/Cruze03/discord-utilities" }; @@ -277,7 +277,7 @@ public Action Command_AdminChat(int client, const char[] command, int argc) { return Plugin_Continue; } - if(IsValidClient(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) + if(g_bBaseComm && BaseComm_IsClientGagged(client)) { return Plugin_Continue; } @@ -296,7 +296,7 @@ public Action OnClientSayCommand(int client, const char[] command, const char[] { return Plugin_Continue; } - if(IsValidClient(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) + if(IsClientInGame(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) { return Plugin_Continue; } @@ -738,9 +738,4 @@ public void APIWebResponse(const char[] sData, int client) stock void GetGuilds() { Bot.GetGuilds(GuildList, _, true); -} - -stock bool IsValidClient(int client) -{ - return (1 <= client <= MaxClients && IsClientInGame(client)); -} \ No newline at end of file +} \ No newline at end of file From 2a2ce8391630858adf3562a39dbb364bad039611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AiDN=E2=84=A2?= <45371311+originalaidn@users.noreply.github.com> Date: Mon, 11 Apr 2022 21:58:53 +0200 Subject: [PATCH 8/9] fix --- scripting/discord_utilities/forwards.sp | 11 ++++++----- scripting/du_chatrelay.sp | 13 +++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/scripting/discord_utilities/forwards.sp b/scripting/discord_utilities/forwards.sp index 00007c0..6ff5aab 100644 --- a/scripting/discord_utilities/forwards.sp +++ b/scripting/discord_utilities/forwards.sp @@ -250,14 +250,15 @@ public Action Command_ViewId(int client, int args) public Action Check(int client, const char[] command, int args) { - if(!client || client > MaxClients && IsClientInGame(client)) - { - return Plugin_Continue; - } - if(!g_bMember[client]) + if(IsValidClient(client) && !g_bMember[client]) { CPrintToChat(client, "%s %T", g_sServerPrefix, "MustVerify", client, ChangePartsInString(g_sViewIDCommand, "sm_", "!")); return Plugin_Stop; } return Plugin_Continue; } + +stock bool IsValidClient(int client) +{ + return (1 <= client <= MaxClients && IsClientInGame(client)); +} \ No newline at end of file diff --git a/scripting/du_chatrelay.sp b/scripting/du_chatrelay.sp index 0bac00f..6efe134 100644 --- a/scripting/du_chatrelay.sp +++ b/scripting/du_chatrelay.sp @@ -85,7 +85,7 @@ public Plugin myinfo = name = "Discord Utilities - Chatrelay module", author = "AiDN™ & Cruze03", description = "Chatrelay module for the Discord Utilities, code from Cruze03", - version = "1.0", + version = "1.1", url = "https://steamcommunity.com/id/originalaidn & https://github.com/Cruze03/discord-utilities" }; @@ -277,7 +277,7 @@ public Action Command_AdminChat(int client, const char[] command, int argc) { return Plugin_Continue; } - if(g_bBaseComm && BaseComm_IsClientGagged(client)) + if(IsValidClient(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) { return Plugin_Continue; } @@ -296,7 +296,7 @@ public Action OnClientSayCommand(int client, const char[] command, const char[] { return Plugin_Continue; } - if(IsClientInGame(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) + if(IsValidClient(client) && g_bBaseComm && BaseComm_IsClientGagged(client)) { return Plugin_Continue; } @@ -738,4 +738,9 @@ public void APIWebResponse(const char[] sData, int client) stock void GetGuilds() { Bot.GetGuilds(GuildList, _, true); -} \ No newline at end of file +} + +stock bool IsValidClient(int client) +{ + return (1 <= client <= MaxClients && IsClientInGame(client)); +} \ No newline at end of file From 3b9923a5ae45f18ae90a37fc6a0ea5ae514e76ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?AiDN=E2=84=A2?= <45371311+originalaidn@users.noreply.github.com> Date: Mon, 11 Apr 2022 22:02:24 +0200 Subject: [PATCH 9/9] fix error compile --- scripting/discord_utilities/forwards.sp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scripting/discord_utilities/forwards.sp b/scripting/discord_utilities/forwards.sp index 6ff5aab..25dab26 100644 --- a/scripting/discord_utilities/forwards.sp +++ b/scripting/discord_utilities/forwards.sp @@ -256,9 +256,4 @@ public Action Check(int client, const char[] command, int args) return Plugin_Stop; } return Plugin_Continue; -} - -stock bool IsValidClient(int client) -{ - return (1 <= client <= MaxClients && IsClientInGame(client)); -} \ No newline at end of file +} \ No newline at end of file