Иконка ресурса

FREE PlayerReports : 1.0.0 1.0.0

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries;
using UnityEngine;
namespace Oxide.Plugins {
    [Info("PlayerReports", "Krasnokutskyi", "1.0.0")]
    [Description("In-game report system with localized chat messages, config migration, admin replies and one editable Discord webhook message per report.")]
    public class PlayerReports : RustPlugin {
        private PluginConfig config;
        private StoredData storedData;
        private const string StatusOpen = "open";
        private const string StatusClosed = "closed";
        private const string RolePlayer = "player";
        private const string RoleAdmin = "admin";
        private const string RoleSystem = "system";
        private readonly HashSet<string> supportedLanguages = new HashSet<string> {
            "en", "ru", "uk"
        }
        ;
#region Config
        private class PluginConfig {
            [JsonProperty("Server name shown in Discord")]
            public string ServerName = "My Server";
            [JsonProperty("Chat prefix")]
            public string ChatPrefix = "<color=#ffb347>[Reports]</color>";
            [JsonProperty("Admin permission")]
            public string AdminPermission = "playerreports.admin";
            [JsonProperty("Allow player /report to append to existing open report instead of creating a new one")]
            public bool AppendToExistingOpenReport = true;
            [JsonProperty("Auto-close player reports when no report admins are online")]
            public bool AutoCloseReportsWhenNoAdminsOnline = true;
            [JsonProperty("Discord ticket channel/link shown to players when no admins are online")]
            public string DiscordTicketText = "/discord → open a ticket in Discord";
            [JsonProperty("Maximum message length from player/admin")]
            public int MaxSingleMessageLength = 700;
            [JsonProperty("How many reports to show in /reports")]
            public int ReportsPerPage = 10;
            [JsonProperty("How many conversation lines to show in game chat")]
            public int ChatHistoryLines = 12;
            [JsonProperty("Force language for all report messages, empty = player game language/auto (en, ru, uk)")]
            public string ForceLanguage = "";
            [JsonProperty("Fallback language if player language is unknown (en, ru, uk)")]
            public string FallbackLanguage = "en";
            [JsonProperty("Discord settings")]
            public DiscordConfig Discord = new DiscordConfig();
            public static PluginConfig DefaultConfig() {
                return new PluginConfig();
            }
        }
        private class DiscordConfig {
            [JsonProperty("Enabled")]
            public bool Enabled = true;
            [JsonProperty("Webhook URL")]
            public string WebhookUrl = "";
            [JsonProperty("Webhook username")]
            public string Username = "Player Reports";
            [JsonProperty("Webhook avatar URL")]
            public string AvatarUrl = "";
            [JsonProperty("Mention text on new report, empty = no mention. Example: <@&ROLE_ID>")]
            public string NewReportMention = "";
            [JsonProperty("Disable Discord mentions inside report text")]
            public bool DisableMentions = true;
            [JsonProperty("Max transcript length in Discord embed")]
            public int MaxTranscriptLength = 3800;
            [JsonProperty("Open report embed color")]
            public int OpenColor = 16753920;
            [JsonProperty("Closed report embed color")]
            public int ClosedColor = 8421504;
        }
        protected override void LoadDefaultConfig() {
            Config.WriteObject(PluginConfig.DefaultConfig(), true);
        }
        protected override void LoadConfig() {
            base.LoadConfig();
            try {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception("Config is null");
                bool changed = false;
                if (config.Discord == null) {
                    config.Discord = new DiscordConfig();
                    changed = true;
                }
                if (config.FallbackLanguage == null) {
                    config.FallbackLanguage = "en";
                    changed = true;
                }
                if (config.ForceLanguage == null) {
                    config.ForceLanguage = "";
                    changed = true;
                }
                if (config.DiscordTicketText == null) {
                    config.DiscordTicketText = "/discord → open a ticket in Discord";
                    changed = true;
                }
                // Save config after every successful load so new settings are visible in oxide/config/PlayerReports.json.
                SaveConfig();
            }
            catch {
                PrintWarning("Config file is corrupted or invalid. Creating default config.");
                config = PluginConfig.DefaultConfig();
                SaveConfig();
            }
        }
        protected override void SaveConfig() {
            Config.WriteObject(config, true);
        }
#endregion
#region Localization
        protected override void LoadDefaultMessages() {
            lang.RegisterMessages(new Dictionary<string, string> {
                ["AdminOpenCount"]
                = "There are currently <color=#ffd479>{0}</color> open reports. Command: <color=#ffd479>/reports</color>", ["PlayerOpenReportOnJoin"] = "You have an open report <color=#ffd479>#{0}</color>. Add a message: <color=#ffd479>/report text</color>. View: <color=#ffd479>/myreport {0}</color>", ["ReportUsage"] = "Use: <color=#ffd479>/report issue description</color>", ["ReportCreated"] = "Report <color=#ffd479>#{0}</color> created. The administration will see it in-game and in Discord.", ["ReportCreatedNoAdmins"] = "Report <color=#ffd479>#{0}</color> was logged, but no admins are online right now. Please open a Discord ticket: <color=#ffd479>{1}</color>", ["ReportAutoClosedNoAdminsLine"] = "Report automatically closed: no admins were online. Player was asked to open a Discord ticket: {0}", ["ReportCreatedNoAdminsV2"] = "Report <color=#ffd479>#{0}</color> has been logged, but there are currently no administrators online. To help us review your issue as quickly as possible, please get the Discord link using <color=#ffd479>/discord</color> and open a ticket in the <color=#ffd479>Support</color> section.", ["ReportAutoClosedNoAdminsLineV2"] = "Report automatically closed: no admins were online. Player was asked to get the Discord link with /discord and open a ticket in the Support section.", ["ReportCreatedNoAdminsV3"] = "Report <color=#ffd479>#{0}</color> has been logged, but there are currently no administrators online on the server. To help us review your issue as quickly as possible, please get the Discord link using <color=#ffd479>/discord</color> and open a ticket in the <color=#ffd479>Support</color> section.", ["ReportAutoClosedNoAdminsLineV3"] = "Report automatically closed: no administrators were online on the server. Player was asked to get the Discord link with /discord and open a ticket in the Support section.", ["ReportMessageAdded"] = "Message added to report <color=#ffd479>#{0}</color>.", ["NoMyReports"] = "You do not have any reports yet.", ["MyReportsHeader"] = "Your latest reports:", ["MyReportLine"] = "#{0} | {1} | lines: {2} | /myreport {0}", ["ReportStatusOpen"] = "open", ["ReportStatusClosed"] = "closed", ["MyReportUsage"] = "Use: <color=#ffd479>/myreport ID</color>", ["ReportNotFound"] = "Report not found.", ["PlayerHelp"] = "Player commands:\n/report text — create a report or add to your open report\n/myreports — your reports\n/myreport ID — view conversation", ["NoAccess"] = "You do not have access to this command.", ["ReportsFilterUsage"] = "Filters: <color=#ffd479>/reports</color>, <color=#ffd479>/reports all</color>, <color=#ffd479>/reports closed</color>", ["NoReportsByFilter"] = "There are no reports for filter <color=#ffd479>{0}</color>.", ["ReportsHeader"] = "Reports ({0}), latest {1}:", ["ReportListLine"] = "#{0} | {1} | {2} [{3}] | {4} | /rview {0}", ["ReportViewUsage"] = "Use: <color=#ffd479>/rview ID</color>", ["ReportReplyUsage"] = "Use: <color=#ffd479>/rreply ID reply text</color>", ["ReportClosedNeedOpen"] = "This report is closed. Open it first: <color=#ffd479>/ropen {0}</color>", ["ReplyAdded"] = "Reply added to report <color=#ffd479>#{0}</color>.", ["AdminReplyToPlayer"] = "Admin reply for report <color=#ffd479>#{0}</color>: {1}", ["ReplyInstruction"] = "To reply, use: <color=#ffd479>/report your text</color>", ["ReportCloseUsage"] = "Use: <color=#ffd479>/rclose ID reason</color>", ["ReportAlreadyClosed"] = "This report is already closed.", ["NoReason"] = "No reason", ["ReportClosedByAdminLine"] = "Report closed by admin {0}. Reason: {1}", ["ReportClosedAdmin"] = "Report <color=#ffd479>#{0}</color> closed.", ["ReportClosedPlayer"] = "Your report <color=#ffd479>#{0}</color> has been closed. Reason: {1}", ["ReportOpenUsage"] = "Use: <color=#ffd479>/ropen ID</color>", ["ReportReopenedLine"] = "Report reopened by admin {0}.", ["ReportReopenedAdmin"] = "Report <color=#ffd479>#{0}</color> reopened.", ["AdminHelp"] = "Admin commands:\n/reports — open reports\n/reports all — all reports\n/rview ID — view conversation\n/rreply ID text or /rr ID text — reply\n/rclose ID reason — close\n/ropen ID — reopen", ["AdminNotifyNewReportTitle"] = "New report", ["AdminNotifyNewMessageTitle"] = "New message in report", ["AdminNotifyLine"] = "<color=#ffd479>{0}</color> #{1} from {2} [{3}]: {4}", ["AdminNotifyCommands"] = "Reply: <color=#ffd479>/rreply {0} text</color> | View: <color=#ffd479>/rview {0}</color>", ["ReportHeader"] = "Report <color=#ffd479>#{0}</color> | {1} | Player: {2} [{3}]", ["ChatHistoryTrimmed"] = "Showing the last {0} lines out of {1}.", ["ReportLine"] = "<color=#ffd479>[#{0}]</color> {1} | {2} | {3}: {4}", ["ReportAdminActions"] = "Reply: <color=#ffd479>/rreply {0} text</color> | Close: <color=#ffd479>/rclose {0} reason</color>", ["RolePlayer"] = "Player", ["RoleAdmin"] = "Admin", ["RoleSystem"] = "System", ["NoText"] = "no text"
            }
            , this, "en");
            lang.RegisterMessages(new Dictionary<string, string> {
                ["AdminOpenCount"]
                = "Сейчас открытых репортов: <color=#ffd479>{0}</color>. Команда: <color=#ffd479>/reports</color>", ["PlayerOpenReportOnJoin"] = "У вас есть открытый репорт <color=#ffd479>#{0}</color>. Добавить сообщение: <color=#ffd479>/report текст</color>. Посмотреть: <color=#ffd479>/myreport {0}</color>", ["ReportUsage"] = "Напишите: <color=#ffd479>/report описание проблемы</color>", ["ReportCreated"] = "Репорт <color=#ffd479>#{0}</color> создан. Администрация увидит его в игре и Discord.", ["ReportCreatedNoAdmins"] = "Репорт <color=#ffd479>#{0}</color> записан, но сейчас нет администраторов онлайн. Пожалуйста, откройте тикет в Discord: <color=#ffd479>{1}</color>", ["ReportAutoClosedNoAdminsLine"] = "Репорт автоматически закрыт: администраторов онлайн не было. Игроку предложено открыть тикет в Discord: {0}", ["ReportCreatedNoAdminsV2"] = "Репорт <color=#ffd479>#{0}</color> записан, но сейчас на сервере нет администраторов онлайн. Чтобы мы смогли рассмотреть вашу проблему как можно быстрее, пожалуйста, получите ссылку на наш Discord командой <color=#ffd479>/discord</color> и откройте тикет в разделе <color=#ffd479>Поддержка</color>.", ["ReportAutoClosedNoAdminsLineV2"] = "Репорт автоматически закрыт: администраторов онлайн не было. Игроку предложено получить ссылку командой /discord и открыть тикет в разделе Поддержка.", ["ReportCreatedNoAdminsV3"] = "Репорт <color=#ffd479>#{0}</color> записан, но сейчас на сервере нет администраторов онлайн. Чтобы мы смогли рассмотреть вашу проблему как можно быстрее, пожалуйста, получите ссылку на наш Discord командой <color=#ffd479>/discord</color> и откройте тикет в разделе <color=#ffd479>Поддержка</color>.", ["ReportAutoClosedNoAdminsLineV3"] = "Репорт автоматически закрыт: на сервере не было администраторов онлайн. Игроку предложено получить ссылку на Discord командой /discord и открыть тикет в разделе Поддержка.", ["ReportMessageAdded"] = "Сообщение добавлено в репорт <color=#ffd479>#{0}</color>.", ["NoMyReports"] = "У вас пока нет репортов.", ["MyReportsHeader"] = "Ваши последние репорты:", ["MyReportLine"] = "#{0} | {1} | строк: {2} | /myreport {0}", ["ReportStatusOpen"] = "открыт", ["ReportStatusClosed"] = "закрыт", ["MyReportUsage"] = "Напишите: <color=#ffd479>/myreport ID</color>", ["ReportNotFound"] = "Репорт не найден.", ["PlayerHelp"] = "Команды игроков:\n/report текст — создать репорт или дописать в открытый\n/myreports — ваши репорты\n/myreport ID — посмотреть переписку", ["NoAccess"] = "У вас нет доступа к этой команде.", ["ReportsFilterUsage"] = "Фильтры: <color=#ffd479>/reports</color>, <color=#ffd479>/reports all</color>, <color=#ffd479>/reports closed</color>", ["NoReportsByFilter"] = "Репортов по фильтру <color=#ffd479>{0}</color> нет.", ["ReportsHeader"] = "Репорты ({0}), последние {1}:", ["ReportListLine"] = "#{0} | {1} | {2} [{3}] | {4} | /rview {0}", ["ReportViewUsage"] = "Напишите: <color=#ffd479>/rview ID</color>", ["ReportReplyUsage"] = "Напишите: <color=#ffd479>/rreply ID текст ответа</color>", ["ReportClosedNeedOpen"] = "Репорт закрыт. Сначала откройте: <color=#ffd479>/ropen {0}</color>", ["ReplyAdded"] = "Ответ добавлен в репорт <color=#ffd479>#{0}</color>.", ["AdminReplyToPlayer"] = "Ответ администратора по репорту <color=#ffd479>#{0}</color>: {1}", ["ReplyInstruction"] = "Чтобы ответить, напишите: <color=#ffd479>/report ваш текст</color>", ["ReportCloseUsage"] = "Напишите: <color=#ffd479>/rclose ID причина</color>", ["ReportAlreadyClosed"] = "Этот репорт уже закрыт.", ["NoReason"] = "Без причины", ["ReportClosedByAdminLine"] = "Репорт закрыт администратором {0}. Причина: {1}", ["ReportClosedAdmin"] = "Репорт <color=#ffd479>#{0}</color> закрыт.", ["ReportClosedPlayer"] = "Ваш репорт <color=#ffd479>#{0}</color> закрыт. Причина: {1}", ["ReportOpenUsage"] = "Напишите: <color=#ffd479>/ropen ID</color>", ["ReportReopenedLine"] = "Репорт снова открыт администратором {0}.", ["ReportReopenedAdmin"] = "Репорт <color=#ffd479>#{0}</color> снова открыт.", ["AdminHelp"] = "Команды админа:\n/reports — открытые репорты\n/reports all — все репорты\n/rview ID — посмотреть переписку\n/rreply ID текст или /rr ID текст — ответить\n/rclose ID причина — закрыть\n/ropen ID — открыть снова", ["AdminNotifyNewReportTitle"] = "Новый репорт", ["AdminNotifyNewMessageTitle"] = "Новое сообщение в репорте", ["AdminNotifyLine"] = "<color=#ffd479>{0}</color> #{1} от {2} [{3}]: {4}", ["AdminNotifyCommands"] = "Ответить: <color=#ffd479>/rreply {0} текст</color> | Смотреть: <color=#ffd479>/rview {0}</color>", ["ReportHeader"] = "Репорт <color=#ffd479>#{0}</color> | {1} | Игрок: {2} [{3}]", ["ChatHistoryTrimmed"] = "Показаны последние {0} строк из {1}.", ["ReportLine"] = "<color=#ffd479>[#{0}]</color> {1} | {2} | {3}: {4}", ["ReportAdminActions"] = "Ответить: <color=#ffd479>/rreply {0} текст</color> | Закрыть: <color=#ffd479>/rclose {0} причина</color>", ["RolePlayer"] = "Игрок", ["RoleAdmin"] = "Админ", ["RoleSystem"] = "Система", ["NoText"] = "без текста"
            }
            , this, "ru");
            lang.RegisterMessages(new Dictionary<string, string> {
                ["AdminOpenCount"]
                = "Зараз відкритих репортів: <color=#ffd479>{0}</color>. Команда: <color=#ffd479>/reports</color>", ["PlayerOpenReportOnJoin"] = "У вас є відкритий репорт <color=#ffd479>#{0}</color>. Додати повідомлення: <color=#ffd479>/report текст</color>. Переглянути: <color=#ffd479>/myreport {0}</color>", ["ReportUsage"] = "Напишіть: <color=#ffd479>/report опис проблеми</color>", ["ReportCreated"] = "Репорт <color=#ffd479>#{0}</color> створено. Адміністрація побачить його в грі та Discord.", ["ReportCreatedNoAdmins"] = "Репорт <color=#ffd479>#{0}</color> записано, але зараз немає адміністраторів онлайн. Будь ласка, відкрийте тікет у Discord: <color=#ffd479>{1}</color>", ["ReportAutoClosedNoAdminsLine"] = "Репорт автоматично закрито: адміністраторів онлайн не було. Гравцю запропоновано відкрити тікет у Discord: {0}", ["ReportCreatedNoAdminsV2"] = "Репорт <color=#ffd479>#{0}</color> записано, але зараз на сервері немає адміністраторів онлайн. Щоб ми могли якнайшвидше розглянути вашу проблему, будь ласка, отримайте посилання на наш Discord командою <color=#ffd479>/discord</color> та відкрийте тікет у розділі <color=#ffd479>Підтримка</color>.", ["ReportAutoClosedNoAdminsLineV2"] = "Репорт автоматично закрито: адміністраторів онлайн не було. Гравцю запропоновано отримати посилання командою /discord і відкрити тікет у розділі Підтримка.", ["ReportCreatedNoAdminsV3"] = "Репорт <color=#ffd479>#{0}</color> записано, але зараз на сервері немає адміністраторів онлайн. Щоб ми могли якнайшвидше розглянути вашу проблему, будь ласка, отримайте посилання на наш Discord командою <color=#ffd479>/discord</color> та відкрийте тікет у розділі <color=#ffd479>Підтримка</color>.", ["ReportAutoClosedNoAdminsLineV3"] = "Репорт автоматично закрито: на сервері не було адміністраторів онлайн. Гравцю запропоновано отримати посилання на Discord командою /discord і відкрити тікет у розділі Підтримка.", ["ReportMessageAdded"] = "Повідомлення додано до репорту <color=#ffd479>#{0}</color>.", ["NoMyReports"] = "У вас поки немає репортів.", ["MyReportsHeader"] = "Ваші останні репорти:", ["MyReportLine"] = "#{0} | {1} | рядків: {2} | /myreport {0}", ["ReportStatusOpen"] = "відкритий", ["ReportStatusClosed"] = "закритий", ["MyReportUsage"] = "Напишіть: <color=#ffd479>/myreport ID</color>", ["ReportNotFound"] = "Репорт не знайдено.", ["PlayerHelp"] = "Команди гравців:\n/report текст — створити репорт або дописати у відкритий\n/myreports — ваші репорти\n/myreport ID — переглянути переписку", ["NoAccess"] = "У вас немає доступу до цієї команди.", ["ReportsFilterUsage"] = "Фільтри: <color=#ffd479>/reports</color>, <color=#ffd479>/reports all</color>, <color=#ffd479>/reports closed</color>", ["NoReportsByFilter"] = "Репортів за фільтром <color=#ffd479>{0}</color> немає.", ["ReportsHeader"] = "Репорти ({0}), останні {1}:", ["ReportListLine"] = "#{0} | {1} | {2} [{3}] | {4} | /rview {0}", ["ReportViewUsage"] = "Напишіть: <color=#ffd479>/rview ID</color>", ["ReportReplyUsage"] = "Напишіть: <color=#ffd479>/rreply ID текст відповіді</color>", ["ReportClosedNeedOpen"] = "Репорт закритий. Спочатку відкрийте: <color=#ffd479>/ropen {0}</color>", ["ReplyAdded"] = "Відповідь додано до репорту <color=#ffd479>#{0}</color>.", ["AdminReplyToPlayer"] = "Відповідь адміністратора по репорту <color=#ffd479>#{0}</color>: {1}", ["ReplyInstruction"] = "Щоб відповісти, напишіть: <color=#ffd479>/report ваш текст</color>", ["ReportCloseUsage"] = "Напишіть: <color=#ffd479>/rclose ID причина</color>", ["ReportAlreadyClosed"] = "Цей репорт уже закритий.", ["NoReason"] = "Без причини", ["ReportClosedByAdminLine"] = "Репорт закрито адміністратором {0}. Причина: {1}", ["ReportClosedAdmin"] = "Репорт <color=#ffd479>#{0}</color> закрито.", ["ReportClosedPlayer"] = "Ваш репорт <color=#ffd479>#{0}</color> закрито. Причина: {1}", ["ReportOpenUsage"] = "Напишіть: <color=#ffd479>/ropen ID</color>", ["ReportReopenedLine"] = "Репорт знову відкрито адміністратором {0}.", ["ReportReopenedAdmin"] = "Репорт <color=#ffd479>#{0}</color> знову відкрито.", ["AdminHelp"] = "Команди адміна:\n/reports — відкриті репорти\n/reports all — усі репорти\n/rview ID — переглянути переписку\n/rreply ID текст або /rr ID текст — відповісти\n/rclose ID причина — закрити\n/ropen ID — відкрити знову", ["AdminNotifyNewReportTitle"] = "Новий репорт", ["AdminNotifyNewMessageTitle"] = "Нове повідомлення в репорті", ["AdminNotifyLine"] = "<color=#ffd479>{0}</color> #{1} від {2} [{3}]: {4}", ["AdminNotifyCommands"] = "Відповісти: <color=#ffd479>/rreply {0} текст</color> | Переглянути: <color=#ffd479>/rview {0}</color>", ["ReportHeader"] = "Репорт <color=#ffd479>#{0}</color> | {1} | Гравець: {2} [{3}]", ["ChatHistoryTrimmed"] = "Показано останні {0} рядків із {1}.", ["ReportLine"] = "<color=#ffd479>[#{0}]</color> {1} | {2} | {3}: {4}", ["ReportAdminActions"] = "Відповісти: <color=#ffd479>/rreply {0} текст</color> | Закрити: <color=#ffd479>/rclose {0} причина</color>", ["RolePlayer"] = "Гравець", ["RoleAdmin"] = "Адмін", ["RoleSystem"] = "Система", ["NoText"] = "без тексту"
            }
            , this, "uk");
        }
        private string NormalizeLanguage(string language) {
            if (string.IsNullOrEmpty(language)) return "";
            language = language.Trim().ToLowerInvariant().Replace('_', '-');
            if (language.StartsWith("en")) return "en";
            if (language.StartsWith("ru")) return "ru";
            if (language.StartsWith("uk") || language.StartsWith("ua")) return "uk";
            return language.Length >= 2 ? language.Substring(0, 2) : language;
        }
        private string GetPlayerLanguage(BasePlayer player) {
            string forced = NormalizeLanguage(config.ForceLanguage);
            if (supportedLanguages.Contains(forced)) return forced;
            string clientLanguage = "";
            try {
                if (player != null && player.net != null && player.net.connection != null && player.net.connection.info != null) {
                    clientLanguage = NormalizeLanguage(player.net.connection.info.GetString("global.language", ""));
                }
            }
            catch {
                clientLanguage = "";
            }
            if (supportedLanguages.Contains(clientLanguage)) return clientLanguage;
            try {
                if (player != null && !string.IsNullOrEmpty(player.UserIDString)) {
                    string oxideLanguage = NormalizeLanguage(lang.GetLanguage(player.UserIDString));
                    if (supportedLanguages.Contains(oxideLanguage)) return oxideLanguage;
                }
            }
            catch {
            }
            string fallback = NormalizeLanguage(config.FallbackLanguage);
            return supportedLanguages.Contains(fallback) ? fallback : "en";
        }
        private string Message(BasePlayer player, string key, params object[] args) {
            string language = GetPlayerLanguage(player);
            if (player != null && !string.IsNullOrEmpty(player.UserIDString)) {
                lang.SetLanguage(language, player.UserIDString);
            }
            string template = lang.GetMessage(key, this, player == null ? null : player.UserIDString);
            if (string.IsNullOrEmpty(template)) template = key;
            return args != null && args.Length > 0 ? string.Format(template, args) : template;
        }
        private void Reply(BasePlayer player, string key, params object[] args) {
            if (player == null) return;
            player.ChatMessage($"{config.ChatPrefix} {Message(player, key, args)}");
        }
#endregion
#region Data
        private class StoredData {
            public int LastReportId = 0;
            public Dictionary<int, ReportInfo> Reports = new Dictionary<int, ReportInfo>();
        }
        private class ReportInfo {
            public int Id;
            public ulong PlayerId;
            public string PlayerName;
            public string Status = StatusOpen;
            public long CreatedAt;
            public long UpdatedAt;
            public string DiscordMessageId = "";
            public List<ReportLine> Lines = new List<ReportLine>();
        }
        private class ReportLine {
            public long Time;
            public string Role;
            public string AuthorName;
            public string AuthorId;
            public string Message;
        }
        private void LoadData() {
            try {
                storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
            }
            catch {
                storedData = new StoredData();
            }
            if (storedData == null) storedData = new StoredData();
            if (storedData.Reports == null) storedData.Reports = new Dictionary<int, ReportInfo>();
        }
        private void SaveData() {
            Interface.Oxide.DataFileSystem.WriteObject(Name, storedData);
        }
#endregion
#region Hooks
        private void Init() {
            LoadData();
            permission.RegisterPermission(config.AdminPermission, this);
            Puts($"PlayerReports v1.0.0 loaded. Language config: force='{config.ForceLanguage}', fallback='{config.FallbackLanguage}'. Configuration loaded successfully.");
        }
        private void Unload() {
            SaveData();
        }
        private void OnServerSave() {
            SaveData();
        }
        private void OnPlayerInit(BasePlayer player) {
            if (player == null) return;
            timer.Once(3f, () => {
                if (player == null || !player.IsConnected) return;
                if (IsReportAdmin(player)) {
                    int openCount = storedData.Reports.Values.Count(r => r.Status == StatusOpen);
                    if (openCount > 0) {
                        Reply(player, "AdminOpenCount", openCount);
                    }
                    return;
                }
                ReportInfo playerReport = GetLatestOpenReportByPlayer(player.userID);
                if (playerReport != null) {
                    Reply(player, "PlayerOpenReportOnJoin", playerReport.Id);
                }
            }
            );
        }
#endregion
#region Player Commands
        [ChatCommand("report")]
        private void CmdReport(BasePlayer player, string command, string[] args) {
            if (player == null) return;
            string message = GetArgs(args);
            if (string.IsNullOrEmpty(message)) {
                Reply(player, "ReportUsage");
                return;
            }
            message = CleanMessage(message);
            ReportInfo report = null;
            bool created = false;
            if (config.AppendToExistingOpenReport) {
                report = GetLatestOpenReportByPlayer(player.userID);
            }
            if (report == null) {
                report = CreateReport(player, message);
                created = true;
            }
            else {
                AddLine(report, RolePlayer, player.displayName, player.UserIDString, message);
            }
            bool noAdminsOnline = config.AutoCloseReportsWhenNoAdminsOnline && !HasOnlineReportAdmin(player.userID);
            if (noAdminsOnline && report.Status == StatusOpen) {
                report.Status = StatusClosed;
                AddLine(report, RoleSystem, "System", "0", Message(player, "ReportAutoClosedNoAdminsLineV3"));
                Reply(player, "ReportCreatedNoAdminsV3", report.Id);
            }
            else {
                Reply(player, created ? "ReportCreated" : "ReportMessageAdded", report.Id);
                NotifyAdmins(report, created ? "AdminNotifyNewReportTitle" : "AdminNotifyNewMessageTitle");
            }
            SaveData();
            SendOrUpdateDiscord(report, created);
        }
        [ChatCommand("myreports")]
        private void CmdMyReports(BasePlayer player, string command, string[] args) {
            if (player == null) return;
            List<ReportInfo> reports = storedData.Reports.Values .Where(r => r.PlayerId == player.userID) .OrderByDescending(r => r.UpdatedAt) .Take(8) .ToList();
            if (reports.Count == 0) {
                Reply(player, "NoMyReports");
                return;
            }
            Reply(player, "MyReportsHeader");
            foreach (ReportInfo report in reports) {
                player.ChatMessage(Message(player, "MyReportLine", report.Id, GetStatusName(player, report.Status), report.Lines.Count));
            }
        }
        [ChatCommand("myreport")]
        private void CmdMyReport(BasePlayer player, string command, string[] args) {
            if (player == null) return;
            int id;
            if (args.Length < 1 || !int.TryParse(args[0], out id)) {
                Reply(player, "MyReportUsage");
                return;
            }
            ReportInfo report;
            if (!storedData.Reports.TryGetValue(id, out report) || report.PlayerId != player.userID) {
                Reply(player, "ReportNotFound");
                return;
            }
            SendReportToChat(player, report, false);
        }
        [ChatCommand("reporthelp")]
        private void CmdReportHelp(BasePlayer player, string command, string[] args) {
            if (player == null) return;
            Reply(player, "PlayerHelp");
        }
        [ChatCommand("reportversion")]
        private void CmdReportVersion(BasePlayer player, string command, string[] args) {
            if (player == null) return;
            string detected = GetPlayerLanguage(player);
            string client = "";
            try {
                if (player.net != null && player.net.connection != null && player.net.connection.info != null) {
                    client = player.net.connection.info.GetString("global.language", "");
                }
            }
            catch {
                client = "unknown";
            }
            player.ChatMessage($"{config.ChatPrefix} PlayerReports v1.0.0 | detected language: <color=#ffd479>{detected}</color> | force: <color=#ffd479>{(string.IsNullOrEmpty(config.ForceLanguage) ? "auto" : config.ForceLanguage)}</color> | fallback: <color=#ffd479>{config.FallbackLanguage}</color>");
        }
#endregion
#region Admin Commands
        [ChatCommand("reports")]
        private void CmdReports(BasePlayer player, string command, string[] args) {
            if (!CanUseAdminCommand(player)) return;
            string filter = args.Length > 0 ? args[0].ToLower() : StatusOpen;
            IEnumerable<ReportInfo> query = storedData.Reports.Values;
            if (filter == StatusOpen || filter == "open" || filter == "opened") {
                query = query.Where(r => r.Status == StatusOpen);
                filter = StatusOpen;
            }
            else if (filter == StatusClosed || filter == "close" || filter == "closed") {
                query = query.Where(r => r.Status == StatusClosed);
                filter = StatusClosed;
            }
            else if (filter == "all" || filter == "все") {
                filter = "all";
            }
            else {
                Reply(player, "ReportsFilterUsage");
                return;
            }
            List<ReportInfo> reports = query.OrderByDescending(r => r.UpdatedAt).Take(Math.Max(1, config.ReportsPerPage)).ToList();
            if (reports.Count == 0) {
                Reply(player, "NoReportsByFilter", filter);
                return;
            }
            Reply(player, "ReportsHeader", filter, reports.Count);
            foreach (ReportInfo report in reports) {
                string firstText = report.Lines.Count > 0 ? Short(report.Lines[0].Message, 60) : Message(player, "NoText");
                player.ChatMessage(Message(player, "ReportListLine", report.Id, GetStatusName(player, report.Status), report.PlayerName, report.PlayerId, firstText));
            }
        }
        [ChatCommand("rview")]
        private void CmdReportView(BasePlayer player, string command, string[] args) {
            if (!CanUseAdminCommand(player)) return;
            int id;
            if (args.Length < 1 || !int.TryParse(args[0], out id)) {
                Reply(player, "ReportViewUsage");
                return;
            }
            ReportInfo report;
            if (!storedData.Reports.TryGetValue(id, out report)) {
                Reply(player, "ReportNotFound");
                return;
            }
            SendReportToChat(player, report, true);
        }
        [ChatCommand("rreply")]
        private void CmdReportReply(BasePlayer player, string command, string[] args) {
            AdminReplyCommand(player, args);
        }
        [ChatCommand("rr")]
        private void CmdReportReplyShort(BasePlayer player, string command, string[] args) {
            AdminReplyCommand(player, args);
        }
        private void AdminReplyCommand(BasePlayer player, string[] args) {
            if (!CanUseAdminCommand(player)) return;
            int id;
            if (args.Length < 2 || !int.TryParse(args[0], out id)) {
                Reply(player, "ReportReplyUsage");
                return;
            }
            ReportInfo report;
            if (!storedData.Reports.TryGetValue(id, out report)) {
                Reply(player, "ReportNotFound");
                return;
            }
            if (report.Status == StatusClosed) {
                Reply(player, "ReportClosedNeedOpen", report.Id);
                return;
            }
            string message = CleanMessage(GetArgs(args, 1));
            AddLine(report, RoleAdmin, player.displayName, player.UserIDString, message);
            SaveData();
            Reply(player, "ReplyAdded", report.Id);
            BasePlayer target = BasePlayer.FindByID(report.PlayerId);
            if (target != null && target.IsConnected) {
                Reply(target, "AdminReplyToPlayer", report.Id, message);
                Reply(target, "ReplyInstruction");
            }
            SendOrUpdateDiscord(report, false);
        }
        [ChatCommand("rclose")]
        private void CmdReportClose(BasePlayer player, string command, string[] args) {
            if (!CanUseAdminCommand(player)) return;
            int id;
            if (args.Length < 1 || !int.TryParse(args[0], out id)) {
                Reply(player, "ReportCloseUsage");
                return;
            }
            ReportInfo report;
            if (!storedData.Reports.TryGetValue(id, out report)) {
                Reply(player, "ReportNotFound");
                return;
            }
            if (report.Status == StatusClosed) {
                Reply(player, "ReportAlreadyClosed");
                return;
            }
            string reason = args.Length > 1 ? CleanMessage(GetArgs(args, 1)) : Message(player, "NoReason");
            report.Status = StatusClosed;
            AddLine(report, RoleSystem, "System", "0", Message(player, "ReportClosedByAdminLine", player.displayName, reason));
            SaveData();
            Reply(player, "ReportClosedAdmin", report.Id);
            BasePlayer target = BasePlayer.FindByID(report.PlayerId);
            if (target != null && target.IsConnected) {
                Reply(target, "ReportClosedPlayer", report.Id, reason);
            }
            SendOrUpdateDiscord(report, false);
        }
        [ChatCommand("ropen")]
        private void CmdReportOpen(BasePlayer player, string command, string[] args) {
            if (!CanUseAdminCommand(player)) return;
            int id;
            if (args.Length < 1 || !int.TryParse(args[0], out id)) {
                Reply(player, "ReportOpenUsage");
                return;
            }
            ReportInfo report;
            if (!storedData.Reports.TryGetValue(id, out report)) {
                Reply(player, "ReportNotFound");
                return;
            }
            report.Status = StatusOpen;
            AddLine(report, RoleSystem, "System", "0", Message(player, "ReportReopenedLine", player.displayName));
            SaveData();
            Reply(player, "ReportReopenedAdmin", report.Id);
            SendOrUpdateDiscord(report, false);
        }
        [ChatCommand("rhelp")]
        private void CmdAdminHelp(BasePlayer player, string command, string[] args) {
            if (!CanUseAdminCommand(player)) return;
            Reply(player, "AdminHelp");
        }
#endregion
#region Report Core
        private ReportInfo CreateReport(BasePlayer player, string message) {
            storedData.LastReportId++;
            ReportInfo report = new ReportInfo {
                Id = storedData.LastReportId, PlayerId = player.userID, PlayerName = player.displayName, Status = StatusOpen, CreatedAt = Now(), UpdatedAt = Now(), DiscordMessageId = "", Lines = new List<ReportLine>()
            }
            ;
            AddLine(report, RolePlayer, player.displayName, player.UserIDString, message);
            storedData.Reports[report.Id] = report;
            return report;
        }
        private void AddLine(ReportInfo report, string role, string authorName, string authorId, string message) {
            if (report.Lines == null) report.Lines = new List<ReportLine>();
            report.Lines.Add(new ReportLine {
                Time = Now(), Role = role, AuthorName = authorName, AuthorId = authorId, Message = message
            }
            );
            report.UpdatedAt = Now();
        }
        private ReportInfo GetLatestOpenReportByPlayer(ulong playerId) {
            return storedData.Reports.Values .Where(r => r.PlayerId == playerId && r.Status == StatusOpen) .OrderByDescending(r => r.UpdatedAt) .FirstOrDefault();
        }
        private bool HasOnlineReportAdmin(ulong reportingPlayerId) {
            foreach (BasePlayer admin in BasePlayer.activePlayerList) {
                if (admin == null || !admin.IsConnected) continue;
                if (admin.userID == reportingPlayerId) continue;
                if (IsReportAdmin(admin)) return true;
            }
            return false;
        }
        private void NotifyAdmins(ReportInfo report, string titleKey) {
            foreach (BasePlayer admin in BasePlayer.activePlayerList) {
                if (admin == null || !admin.IsConnected || !IsReportAdmin(admin)) continue;
                string last = report.Lines.Count > 0 ? report.Lines[report.Lines.Count - 1].Message : "";
                playerSafeNotify(admin, "AdminNotifyLine", Message(admin, titleKey), report.Id, report.PlayerName, report.PlayerId, Short(last, 120));
                Reply(admin, "AdminNotifyCommands", report.Id);
            }
            Puts($"{titleKey} #{report.Id} from {report.PlayerName} ({report.PlayerId})");
        }
        private void playerSafeNotify(BasePlayer player, string key, params object[] args) {
            Reply(player, key, args);
        }
        private void SendReportToChat(BasePlayer target, ReportInfo report, bool adminView) {
            Reply(target, "ReportHeader", report.Id, GetStatusName(target, report.Status), report.PlayerName, report.PlayerId);
            int take = Math.Max(1, config.ChatHistoryLines);
            List<ReportLine> lines = report.Lines.OrderBy(l => l.Time).ToList();
            if (lines.Count > take) {
                Reply(target, "ChatHistoryTrimmed", take, lines.Count);
                lines = lines.Skip(lines.Count - take).ToList();
            }
            foreach (ReportLine line in lines) {
                string roleName = GetRoleName(line.Role, target);
                target.ChatMessage(Message(target, "ReportLine", report.Id, FormatTime(line.Time), roleName, line.AuthorName, line.Message));
            }
            if (adminView && report.Status == StatusOpen) {
                Reply(target, "ReportAdminActions", report.Id);
            }
        }
#endregion
#region Discord
        private void SendOrUpdateDiscord(ReportInfo report, bool newReport) {
            if (config.Discord == null || !config.Discord.Enabled) return;
            if (string.IsNullOrEmpty(config.Discord.WebhookUrl)) return;
            string payload = BuildDiscordPayload(report, newReport);
            Dictionary<string, string> headers = new Dictionary<string, string> {
                {
                    "Content-Type", "application/json"
                }
            }
            ;
            if (string.IsNullOrEmpty(report.DiscordMessageId)) {
                string url = AddQuery(config.Discord.WebhookUrl, "wait=true");
                webrequest.Enqueue(url, payload, (code, response) => {
                    if (code >= 200 && code <= 299) {
                        string messageId = ExtractDiscordMessageId(response);
                        if (!string.IsNullOrEmpty(messageId)) {
                            ReportInfo stored;
                            if (storedData.Reports.TryGetValue(report.Id, out stored)) {
                                stored.DiscordMessageId = messageId;
                                SaveData();
                            }
                        }
                    }
                    else {
                        PrintWarning($"Discord webhook send failed for report #{report.Id}. Code: {code}. Response: {Short(response, 500)}");
                    }
                }
                , this, RequestMethod.POST, headers);
            }
            else {
                string editUrl = GetDiscordEditUrl(config.Discord.WebhookUrl, report.DiscordMessageId);
                webrequest.Enqueue(editUrl, payload, (code, response) => {
                    if (code < 200 || code > 299) {
                        PrintWarning($"Discord webhook edit failed for report #{report.Id}. Code: {code}. Response: {Short(response, 500)}");
                    }
                }
                , this, RequestMethod.PATCH, headers);
            }
        }
        private string BuildDiscordPayload(ReportInfo report, bool newReport) {
            string statusText = report.Status == StatusOpen ? "OPEN" : "CLOSED";
            int color = report.Status == StatusOpen ? config.Discord.OpenColor : config.Discord.ClosedColor;
            string transcript = BuildDiscordTranscript(report, Math.Max(500, config.Discord.MaxTranscriptLength));
            JObject embed = new JObject {
                ["title"]
                = $"Report #{report.Id} • {statusText}", ["color"] = color, ["description"] = transcript, ["timestamp"] = DateTime.UtcNow.ToString("o"), ["footer"] = new JObject {
                    ["text"]
                    = "PlayerReports • one Discord message per report"
                }
                , ["fields"] = new JArray {
                    new JObject {
                        ["name"]
                        = "Player", ["value"] = $"{EscapeDiscord(report.PlayerName)}\n`{report.PlayerId}`", ["inline"] = true
                    }
                    , new JObject {
                        ["name"]
                        = "Status", ["value"] = statusText, ["inline"] = true
                    }
                    , new JObject {
                        ["name"]
                        = "Server", ["value"] = EscapeDiscord(config.ServerName), ["inline"] = true
                    }
                    , new JObject {
                        ["name"]
                        = "In-game commands", ["value"] = $"`/rview {report.Id}` • `/rreply {report.Id} text` • `/rclose {report.Id} reason`", ["inline"] = false
                    }
                }
            }
            ;
            JObject payload = new JObject {
                ["username"]
                = config.Discord.Username, ["embeds"] = new JArray {
                    embed
                }
            }
            ;
            if (!string.IsNullOrEmpty(config.Discord.AvatarUrl)) {
                payload["avatar_url"] = config.Discord.AvatarUrl;
            }
            if (newReport && !string.IsNullOrEmpty(config.Discord.NewReportMention)) {
                payload["content"] = config.Discord.NewReportMention;
            }
            else {
                payload["content"] = "";
            }
            if (config.Discord.DisableMentions) {
                if (newReport && !string.IsNullOrEmpty(config.Discord.NewReportMention)) {
                    payload["allowed_mentions"] = new JObject {
                        ["parse"]
                        = new JArray {
                            "users", "roles"
                        }
                    }
                    ;
                }
                else {
                    payload["allowed_mentions"] = new JObject {
                        ["parse"]
                        = new JArray()
                    }
                    ;
                }
            }
            return payload.ToString(Formatting.None);
        }
        private string BuildDiscordTranscript(ReportInfo report, int maxLength) {
            List<string> allLines = new List<string>();
            List<ReportLine> lines = report.Lines.OrderBy(l => l.Time).ToList();
            foreach (ReportLine line in lines) {
                string role = GetRoleName(line.Role, null);
                string time = FormatTime(line.Time);
                string author = EscapeDiscord(line.AuthorName);
                string msg = EscapeDiscord(line.Message);
                allLines.Add($"`{time}` **{role} {author}:** {msg}");
            }
            string result = "";
            for (int i = allLines.Count - 1; i >= 0; i--) {
                string candidate = string.IsNullOrEmpty(result) ? allLines[i] : allLines[i] + "\n" + result;
                if (candidate.Length > maxLength) {
                    result = "_Older messages were hidden because Discord has message/embed limits._\n" + result;
                    break;
                }
                result = candidate;
            }
            if (string.IsNullOrEmpty(result)) result = "No messages.";
            if (result.Length > maxLength) result = result.Substring(result.Length - maxLength);
            return result;
        }
        private string ExtractDiscordMessageId(string response) {
            if (string.IsNullOrEmpty(response)) return "";
            try {
                JObject json = JObject.Parse(response);
                JToken token;
                if (json.TryGetValue("id", out token)) return token.ToString();
            }
            catch {
                return "";
            }
            return "";
        }
        private string AddQuery(string url, string query) {
            if (url.Contains("?")) return url + "&" + query;
            return url + "?" + query;
        }
        private string GetDiscordEditUrl(string webhookUrl, string messageId) {
            string cleanUrl = webhookUrl.Split('?')[0].TrimEnd('/');
            return cleanUrl + "/messages/" + messageId;
        }
#endregion
#region Helpers
        private bool CanUseAdminCommand(BasePlayer player) {
            if (player == null) return false;
            if (!IsReportAdmin(player)) {
                Reply(player, "NoAccess");
                return false;
            }
            return true;
        }
        private bool IsReportAdmin(BasePlayer player) {
            if (player == null) return false;
            if (permission.UserHasPermission(player.UserIDString, config.AdminPermission)) return true;
            if (player.net != null && player.net.connection != null && player.net.connection.authLevel >= 1) return true;
            return false;
        }
        private string GetArgs(string[] args, int start = 0) {
      
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху