Guest User

Untitled

a guest
Mar 5th, 2026
78
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 23.23 KB | None | 0 0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Text.RegularExpressions;
  6. using Facepunch;
  7. using Newtonsoft.Json;
  8. using Newtonsoft.Json.Linq;
  9. using Oxide.Core;
  10. using Oxide.Core.Libraries;
  11. using Oxide.Core.Libraries.Covalence;
  12. using Oxide.Core.Plugins;
  13.  
  14. namespace Oxide.Plugins
  15. {
  16. [Info("Discord Report", "misticos", "1.2.1")]
  17. [Description("Send reports from players ingame to a Discord channel")]
  18. class DiscordReport : CovalencePlugin
  19. {
  20. #region Variables
  21.  
  22. [PluginReference("PlaceholderAPI")]
  23. private Plugin _placeholders = null;
  24.  
  25. private Action<IPlayer, StringBuilder, bool> _placeholderProcessor = null;
  26.  
  27. private static DiscordReport _ins;
  28.  
  29. private const string SteamProfileXML = "https://steamcommunity.com/profiles/{0}?xml=1";
  30. private const string SteamProfile = "https://steamcommunity.com/profiles/";
  31.  
  32. private readonly Regex _steamProfileIconRegex =
  33. new Regex(@"(?<=<avatarIcon>[\w\W]+)https://.+\.jpg(?=[\w\W]+<\/avatarIcon>)", RegexOptions.Compiled);
  34.  
  35. private Dictionary<string, uint> _cooldownData = new Dictionary<string, uint>();
  36.  
  37. private Time _time = GetLibrary<Time>();
  38.  
  39. private const string PermissionIgnoreCooldown = "discordreport.ignorecooldown";
  40. private const string PermissionUse = "discordreport.use";
  41. private const string PermissionAdmin = "discordreport.admin";
  42.  
  43. #endregion
  44.  
  45. #region Configuration
  46.  
  47. private Configuration _config;
  48.  
  49. private class Configuration
  50. {
  51. [JsonProperty("Webhook URL")]
  52. public string Webhook = "YOUR WEBHOOK LINK HERE";
  53.  
  54. [JsonProperty("Message Content")]
  55. public string MessageContent = "";
  56.  
  57. [JsonProperty("Embed Title")]
  58. public string EmbedTitle = "My Server Report";
  59.  
  60. [JsonProperty("Embed Description")]
  61. public string EmbedDescription = "Report sent by a player from your server";
  62.  
  63. [JsonProperty("Embed Color")]
  64. public int EmbedColor = 1484265;
  65.  
  66. [JsonProperty("Set Author Icon From Player Profile")]
  67. public bool AuthorIcon = true;
  68.  
  69. [JsonProperty("Use Reporter (True) Or Suspect (False) As Author")]
  70. public bool IsReporterIcon = true;
  71.  
  72. [JsonProperty("Allow Reporting Admins")]
  73. public bool ReportAdmins = false;
  74.  
  75. [JsonProperty("Report Commands", ObjectCreationHandling = ObjectCreationHandling.Replace)]
  76. public List<string> ReportCommands = new List<string> { "report" };
  77.  
  78. [JsonProperty("Allow Only Online Suspects Reports")]
  79. public bool OnlyOnlineSuspects = true;
  80.  
  81. [JsonProperty("Threshold Before Sending Reports")]
  82. public int Threshold = 0;
  83.  
  84. #if RUST
  85. [JsonProperty("Show Recent Suspect Combatlog")]
  86. public bool ShowCombatlog = false;
  87.  
  88. [JsonProperty("Show In-Game Subject")]
  89. public bool ShowInGameSubject = true;
  90.  
  91. [JsonProperty("Minimum In-Game Report Subject Length")]
  92. public int SubjectMinimumGame = 0;
  93.  
  94. [JsonProperty("Minimum In-Game Report Message Length")]
  95. public int MessageMinimumGame = 0;
  96.  
  97. [JsonProperty("Allowed In-Game Report Types", ObjectCreationHandling = ObjectCreationHandling.Replace)]
  98. public string[] AllowedTypes = { "cheat", "abusive", "name", "spam" };
  99.  
  100. [JsonProperty("Recent Combatlog Entries")]
  101. public int CombatlogEntries = 2;
  102. #endif
  103.  
  104. [JsonProperty("Cooldown In Seconds")]
  105. public uint Cooldown = 300;
  106.  
  107. [JsonProperty("User Cache Validity In Seconds")]
  108. public uint UserCacheValidity = 86400;
  109.  
  110. [JsonProperty("Minimum Message Length")]
  111. public int MessageMinimum = 0;
  112. }
  113.  
  114. protected override void LoadConfig()
  115. {
  116. base.LoadConfig();
  117. try
  118. {
  119. _config = Config.ReadObject<Configuration>();
  120. if (_config == null) throw new Exception();
  121. SaveConfig();
  122. }
  123. catch
  124. {
  125. PrintError("Your configuration file contains an error. Using default configuration values.");
  126. LoadDefaultConfig();
  127. }
  128. }
  129.  
  130. protected override void SaveConfig() => Config.WriteObject(_config);
  131.  
  132. protected override void LoadDefaultConfig() => _config = new Configuration();
  133.  
  134. #endregion
  135.  
  136. #region Work with Data
  137.  
  138. private Dictionary<string, PlayerData> _loadedData = new Dictionary<string, PlayerData>();
  139.  
  140. private void SaveData(string id)
  141. {
  142. PlayerData data;
  143. if (!_loadedData.TryGetValue(id, out data))
  144. return;
  145.  
  146. Interface.Oxide.DataFileSystem.WriteObject(nameof(DiscordReport) + '/' + id, data);
  147. }
  148.  
  149. private PlayerData GetOrLoadData(string id)
  150. {
  151. PlayerData data;
  152. if (_loadedData.TryGetValue(id, out data))
  153. return data;
  154.  
  155. try
  156. {
  157. data = Interface.Oxide.DataFileSystem.ReadObject<PlayerData>(nameof(DiscordReport) + '/' + id);
  158. }
  159. catch (Exception e)
  160. {
  161. PrintError(e.ToString());
  162. }
  163.  
  164. return _loadedData[id] = data ?? new PlayerData();
  165. }
  166.  
  167. private class PlayerData
  168. {
  169. public string ImageURL = string.Empty;
  170. public string LastKnownAddress = string.Empty;
  171.  
  172. public HashSet<string> Reporters = new HashSet<string>();
  173.  
  174. public uint LastImageUpdate = 0;
  175. }
  176.  
  177. #endregion
  178.  
  179. #region Discord Classes
  180.  
  181. // ReSharper disable NotAccessedField.Local
  182.  
  183. private class WebhookBody
  184. {
  185. [JsonProperty("embeds")]
  186. public List<EmbedBody> Embeds;
  187.  
  188. [JsonProperty("content")]
  189. public string Content = null;
  190. }
  191.  
  192. private class EmbedBody
  193. {
  194. [JsonProperty("title")]
  195. public string Title;
  196.  
  197. [JsonProperty("type")]
  198. public string Type;
  199.  
  200. [JsonProperty("description")]
  201. public string Description;
  202.  
  203. [JsonProperty("color")]
  204. public int Color;
  205.  
  206. [JsonProperty("author")]
  207. public AuthorBody Author;
  208.  
  209. [JsonProperty("fields")]
  210. public List<FieldBody> Fields;
  211.  
  212. public class AuthorBody
  213. {
  214. [JsonProperty("name")]
  215. public string Name;
  216.  
  217. [JsonProperty("url")]
  218. public string AuthorURL;
  219.  
  220. [JsonProperty("icon_url")]
  221. public string AuthorIconURL;
  222. }
  223.  
  224. public class FieldBody
  225. {
  226. [JsonProperty("name")]
  227. public string Name;
  228.  
  229. [JsonProperty("value")]
  230. public string Value;
  231.  
  232. [JsonProperty("inline")]
  233. public bool Inline;
  234. }
  235. }
  236.  
  237. // ReSharper restore NotAccessedField.Local
  238.  
  239. #endregion
  240.  
  241. #region Hooks
  242.  
  243. protected override void LoadDefaultMessages()
  244. {
  245. lang.RegisterMessages(new Dictionary<string, string>
  246. {
  247. { "Webhook: Reporter Data Title", "Reporter" },
  248. { "Webhook: Suspect Data Title", "Suspect" },
  249. #if RUST
  250. { "Webhook: Combatlog Title", "Suspect's Combatlog #{n}" },
  251. { "Webhook: Combatlog Attacker Title", "Attacker" },
  252. { "Webhook: Combatlog Target Title", "Target" },
  253. { "Webhook: Combatlog Time Title", "Time" },
  254. { "Webhook: Combatlog Weapon Title", "Weapon" },
  255. { "Webhook: Combatlog Ammo Title", "Ammo" },
  256. { "Webhook: Combatlog Distance Title", "Distance" },
  257. { "Webhook: Combatlog Old HP Title", "Old HP" },
  258. { "Webhook: Combatlog New HP Title", "New HP" },
  259. { "Webhook: Combatlog Info Title", "Info" },
  260. #endif
  261. {
  262. "Webhook: Reporter Data", "#{discordreport.total} {name} ({id}). IP: {ip}.\n" +
  263. "Ping: {ping}ms. Connected: {connected}"
  264. },
  265. {
  266. "Webhook: Suspect Data", "#{discordreport.total} {name} ({id}). IP: {ip}.\n" +
  267. "Ping: {ping}ms. Connected: {connected}"
  268. },
  269. { "Webhook: Report Subject", "Report Subject" },
  270. { "Webhook: Report Message", "Report Message" },
  271. {
  272. "Command: Syntax", "Syntax:\n" +
  273. "report (ID / \"Name\") (Message)\n" +
  274. "WARNING! Use quotes for names."
  275. },
  276. { "Command: User Not Found", "We were unable to find this user or multiple were found." },
  277. { "Command: Report Sent", "Thank you for your report, it was sent to our administration." },
  278. { "Command: Exceeded Cooldown", "You have exceeded your cooldown on reports." },
  279. { "Command: Cannot Report Admins", "You cannot report admins." },
  280. { "Command: Cannot Use", "You cannot use this command since you do not have enough permissions." },
  281. { "Command: Message Length", "Please add more information to the message." }
  282. }, this);
  283. }
  284.  
  285. private void Init()
  286. {
  287. _ins = this;
  288.  
  289. permission.RegisterPermission(PermissionIgnoreCooldown, this);
  290. permission.RegisterPermission(PermissionUse, this);
  291. permission.RegisterPermission(PermissionAdmin, this);
  292.  
  293. foreach (var command in _config.ReportCommands)
  294. AddCovalenceCommand(command, nameof(CommandReport));
  295. }
  296.  
  297. private void Loaded()
  298. {
  299. foreach (var player in players.Connected)
  300. OnUserConnected(player);
  301. }
  302.  
  303. private void Unload()
  304. {
  305. _ins = null;
  306. }
  307.  
  308. private void OnUserConnected(IPlayer player)
  309. {
  310. var user = GetOrLoadData(player.Id);
  311.  
  312. if (_config.AuthorIcon)
  313. UpdateCachedImage(player, user);
  314.  
  315. user.LastKnownAddress = player.Address;
  316.  
  317. SaveData(player.Id);
  318. }
  319.  
  320. private void OnPlaceholderAPIReady()
  321. {
  322. _placeholderProcessor =
  323. _placeholders.Call<Action<IPlayer, StringBuilder, bool>>("GetProcessPlaceholders", 1);
  324.  
  325. _placeholders.Call("AddPlaceholder", this, "discordreport.total", new Func<IPlayer, string, object>(
  326. (player, s) => GetOrLoadData(player.Id).Reporters.Count));
  327. }
  328.  
  329. private void OnPluginUnloaded(Plugin plugin)
  330. {
  331. if (plugin?.Name != "PlaceholderAPI")
  332. return;
  333.  
  334. _placeholderProcessor = null;
  335. }
  336.  
  337. #if RUST
  338. private void OnPlayerReported(BasePlayer player, string targetName, string targetId, string subject,
  339. string message, string type)
  340. {
  341. if (player == null)
  342. return;
  343.  
  344. // Empty message will not be shown
  345. if (_config.MessageMinimumGame != 0 && !string.IsNullOrEmpty(message) &&
  346. message.Length < _config.MessageMinimumGame)
  347. return;
  348.  
  349. subject = subject.Substring(3 + type.Length);
  350. if (_config.SubjectMinimumGame != 0 && _config.ShowInGameSubject &&
  351. subject.Length < _config.SubjectMinimumGame)
  352. return;
  353.  
  354. if (_config.AllowedTypes.Length != 0 && !_config.AllowedTypes.Contains(type))
  355. return;
  356.  
  357. var suspect = players.FindPlayer(targetId);
  358. if (suspect == null || player.IPlayer == null)
  359. return;
  360.  
  361. if (!CanUse(player.IPlayer))
  362. return;
  363.  
  364. if (!_config.ReportAdmins && suspect.HasPermission(PermissionAdmin))
  365. return;
  366.  
  367. if (_config.OnlyOnlineSuspects && !suspect.IsConnected)
  368. return;
  369.  
  370. if (ExceedsCooldown(player.IPlayer))
  371. return;
  372.  
  373. SendReport(player.IPlayer, suspect, _config.ShowInGameSubject ? subject : string.Empty, message);
  374. }
  375. #endif
  376.  
  377. #endregion
  378.  
  379. #region Commands
  380.  
  381. private void CommandReport(IPlayer player, string command, string[] args)
  382. {
  383. if (!CanUse(player))
  384. {
  385. player.Reply(GetMsg("Command: Cannot Use", player.Id));
  386. return;
  387. }
  388.  
  389. if (args.Length < 2)
  390. goto syntax;
  391.  
  392. var message = string.Join(" ", args.Skip(1));
  393. if (_config.MessageMinimum != 0 && message.Length < _config.MessageMinimum)
  394. {
  395. player.Reply(GetMsg("Command: Message Length", player.Id));
  396. return;
  397. }
  398.  
  399. var suspect = players.FindPlayer(args[0]);
  400. if (suspect == null || !suspect.IsConnected && _config.OnlyOnlineSuspects)
  401. {
  402. player.Reply(GetMsg("Command: User Not Found", player.Id));
  403. return;
  404. }
  405.  
  406. if (!_config.ReportAdmins && suspect.HasPermission(PermissionAdmin))
  407. {
  408. player.Reply(GetMsg("Command: Cannot Report Admins", player.Id));
  409. return;
  410. }
  411.  
  412. if (ExceedsCooldown(player))
  413. {
  414. player.Reply(GetMsg("Command: Exceeded Cooldown", player.Id));
  415. return;
  416. }
  417.  
  418. SendReport(player, suspect, string.Empty, message);
  419. player.Reply(GetMsg("Command: Report Sent", player.Id));
  420. return;
  421.  
  422. syntax:
  423. player.Reply(GetMsg("Command: Syntax", player.Id));
  424. }
  425.  
  426. #endregion
  427.  
  428. #region Helpers
  429.  
  430. private void UpdateCachedImage(IPlayer player, PlayerData data)
  431. {
  432. var now = _time.GetUnixTimestamp();
  433.  
  434. // If cached and still valid, return
  435. if (!string.IsNullOrEmpty(data.ImageURL) &&
  436. data.LastImageUpdate + _config.UserCacheValidity > now)
  437. return;
  438.  
  439. webrequest.Enqueue(string.Format(SteamProfileXML, player.Id), string.Empty,
  440. (code, result) =>
  441. {
  442. data.ImageURL = _steamProfileIconRegex.Match(result).Value;
  443. data.LastImageUpdate = now;
  444.  
  445. SaveData(player.Id);
  446. },
  447. this);
  448. }
  449.  
  450. #region Webhook
  451.  
  452. private void SendReport(IPlayer reporter, IPlayer suspect, string subject, string message)
  453. {
  454. // Threshold
  455.  
  456. var cached = GetOrLoadData(suspect.Id);
  457.  
  458. cached.Reporters.Add(reporter.Id);
  459. if (cached.Reporters.Count < _config.Threshold)
  460. return;
  461.  
  462. // Author Icon
  463.  
  464. var authorIconURL = string.Empty;
  465. var author = _config.IsReporterIcon ? reporter : suspect;
  466. if (_config.AuthorIcon)
  467. {
  468. var user = GetOrLoadData(author.Id);
  469. if (!string.IsNullOrEmpty(user.ImageURL))
  470. authorIconURL = user.ImageURL;
  471.  
  472. UpdateCachedImage(author, user); // Won't get update now but will be for any other reports for this user
  473. }
  474.  
  475. // Embed data
  476.  
  477. const string type = "rich";
  478. var body = new WebhookBody
  479. {
  480. Embeds = new List<EmbedBody>(),
  481. Content = string.IsNullOrEmpty(_config.MessageContent) ? null : _config.MessageContent
  482. };
  483.  
  484. var fields = new List<EmbedBody.FieldBody>();
  485.  
  486. if (!string.IsNullOrEmpty(subject))
  487. {
  488. fields.Add(new EmbedBody.FieldBody
  489. {
  490. Name = GetMsg("Webhook: Report Subject"),
  491. Value = subject,
  492. Inline = false
  493. });
  494. }
  495.  
  496. if (!string.IsNullOrEmpty(message))
  497. {
  498. fields.Add(new EmbedBody.FieldBody
  499. {
  500. Name = GetMsg("Webhook: Report Message"),
  501. Value = message,
  502. Inline = false
  503. });
  504. }
  505.  
  506. fields.Add(new EmbedBody.FieldBody
  507. {
  508. Name = GetMsg("Webhook: Reporter Data Title"),
  509. Value =
  510. FormatUserDetails(new StringBuilder(GetMsg("Webhook: Reporter Data")), reporter),
  511. Inline = false
  512. });
  513.  
  514. fields.Add(new EmbedBody.FieldBody
  515. {
  516. Name = GetMsg("Webhook: Suspect Data Title"),
  517. Value = FormatUserDetails(new StringBuilder(GetMsg("Webhook: Suspect Data")), suspect),
  518. Inline = false
  519. });
  520.  
  521. body.Embeds.Add(new EmbedBody
  522. {
  523. Title = _config.EmbedTitle,
  524. Description = _config.EmbedDescription,
  525. Type = type,
  526. Color = _config.EmbedColor,
  527. Author = new EmbedBody.AuthorBody
  528. {
  529. AuthorIconURL = authorIconURL,
  530. AuthorURL = SteamProfile + author.Id,
  531. Name = author.Name
  532. },
  533. Fields = fields
  534. });
  535.  
  536. // Rust-specific embed data
  537.  
  538. #if RUST
  539. if (_config.ShowCombatlog && suspect.Object is BasePlayer)
  540. {
  541. var events = CombatLog.Get(((BasePlayer)suspect.Object).userID).ToArray();
  542. for (var i = 1; i <= _config.CombatlogEntries && i <= events.Length; i++)
  543. {
  544. var combat = events[events.Length - i];
  545.  
  546. var combatFields = new List<EmbedBody.FieldBody>();
  547.  
  548. combatFields.Add(new EmbedBody.FieldBody
  549. {
  550. Name = GetMsg("Webhook: Combatlog Attacker Title"),
  551. Value = combat.attacker,
  552. Inline = true
  553. });
  554.  
  555. combatFields.Add(new EmbedBody.FieldBody
  556. {
  557. Name = GetMsg("Webhook: Combatlog Target Title"),
  558. Value = combat.target,
  559. Inline = true
  560. });
  561.  
  562. combatFields.Add(new EmbedBody.FieldBody
  563. {
  564. Name = GetMsg("Webhook: Combatlog Time Title"),
  565. Value = (UnityEngine.Time.realtimeSinceStartup - combat.time).ToString("0.0s"),
  566. Inline = true
  567. });
  568.  
  569. combatFields.Add(new EmbedBody.FieldBody
  570. {
  571. Name = GetMsg("Webhook: Combatlog Weapon Title"),
  572. Value = combat.weapon,
  573. Inline = true
  574. });
  575.  
  576. combatFields.Add(new EmbedBody.FieldBody
  577. {
  578. Name = GetMsg("Webhook: Combatlog Ammo Title"),
  579. Value = combat.ammo,
  580. Inline = true
  581. });
  582.  
  583. combatFields.Add(new EmbedBody.FieldBody
  584. {
  585. Name = GetMsg("Webhook: Combatlog Distance Title"),
  586. Value = combat.distance.ToString("0.0m"),
  587. Inline = true
  588. });
  589.  
  590. combatFields.Add(new EmbedBody.FieldBody
  591. {
  592. Name = GetMsg("Webhook: Combatlog Old HP Title"),
  593. Value = combat.health_old.ToString("0.0"),
  594. Inline = true
  595. });
  596.  
  597. combatFields.Add(new EmbedBody.FieldBody
  598. {
  599. Name = GetMsg("Webhook: Combatlog New HP Title"),
  600. Value = combat.health_new.ToString("0.0"),
  601. Inline = true
  602. });
  603.  
  604. combatFields.Add(new EmbedBody.FieldBody
  605. {
  606. Name = GetMsg("Webhook: Combatlog Info Title"),
  607. Value = string.IsNullOrEmpty(combat.info) ? "none" : combat.info,
  608. Inline = true
  609. });
  610.  
  611. body.Embeds.Add(new EmbedBody
  612. {
  613. Title = GetMsg("Webhook: Combatlog Title").Replace("{n}", $"{i}"),
  614. Type = type,
  615. Color = _config.EmbedColor,
  616. Fields = combatFields
  617. });
  618. }
  619. }
  620. #endif
  621.  
  622. // Send a web request
  623.  
  624. webrequest.Enqueue(_config.Webhook, JObject.FromObject(body).ToString(),
  625. (code, result) =>
  626. {
  627. if (code == 204)
  628. {
  629. SetCooldown(reporter);
  630. }
  631. else
  632. {
  633. PrintWarning($"Discord Webhook returned {code}:\n{result}");
  634. }
  635. }, this, RequestMethod.POST,
  636. new Dictionary<string, string> { { "Content-Type", "application/json" } });
  637. }
  638.  
  639. private string FormatUserDetails(StringBuilder builder, IPlayer player)
  640. {
  641. // Apply placeholders if possible
  642. _placeholderProcessor?.Invoke(player, builder, false);
  643.  
  644. return builder
  645. .Replace("{name}", player.Name).Replace("{id}", player.Id)
  646. .Replace("{ip}", GetOrLoadData(player.Id)?.LastKnownAddress ?? "Unknown")
  647. .Replace("{ping}", player.IsConnected ? player.Ping.ToString() : "0")
  648. .Replace("{connected}", player.IsConnected.ToString()).ToString();
  649. }
  650.  
  651. #endregion
  652.  
  653. #region Cooldown
  654.  
  655. private bool ExceedsCooldown(IPlayer player)
  656. {
  657. if (player.HasPermission(PermissionIgnoreCooldown))
  658. return false;
  659.  
  660. var currentTime = _time.GetUnixTimestamp();
  661. if (_cooldownData.ContainsKey(player.Id))
  662. return _cooldownData[player.Id] - currentTime < _config.Cooldown;
  663.  
  664. return false;
  665. }
  666.  
  667. private void SetCooldown(IPlayer player) => _cooldownData[player.Id] = _time.GetUnixTimestamp();
  668.  
  669. #endregion
  670.  
  671. private bool CanUse(IPlayer player) => player.HasPermission(PermissionUse);
  672.  
  673. private string GetMsg(string key, string userId = null) => lang.GetMessage(key, this, userId);
  674.  
  675. #endregion
  676. }
  677. }
Advertisement
Add Comment
Please, Sign In to add comment