using System.Collections.Generic;using System.Globalization;using System.Text.RegularExpressions; sealed class RemoteControl static readonly TimeSpan Every = TimeSpan.FromSeconds(15); static readonly TimeSpan TooOld = TimeSpan.FromMinutes(2); const int Timeout = 20000; public static readonly string[] Commands = { "start", "stop", "status", "preset", "report", "addpreset", "shared", "screenshot" }; readonly Action<string> log; readonly StatsCard stats; readonly AutoResetEvent wake = new AutoResetEvent(false); readonly object gate = new object(); public Action<string, string, Action<bool, string>> Handler; public Func<List<string>> PresetNames; public delegate byte[] Picture(out string why); public volatile string ReportZip; internal const long ZipMax = 24L * 1024 * 1024; readonly HashSet<string> seen = new HashSet<string>(); DateTime nextTry = DateTime.MinValue; string shareId, shareCode; public Action<string> ShareLost; public RemoteControl(Settings settings, Action<string> log, Action save, StatsCard stats) string Link { get { return (s.StatsLink ?? "").Trim(); } } public bool On { get { return Wanted && stats.Ours; } } bool Wanted { get { return s.RemoteControl && stats.On; } } bool Stale { get { return !s.RemoteControl && MessageOf(Link) != null; } } else if (Stale) { Start(); wake.Set(); } if (Wanted || Stale) Start(); if (thread != null) return; thread = new Thread(Loop) { IsBackground = true, Name = "remote" }; bool wasOn = false, saidNotOurs = false; if (on != wasOn) log(on ? "Remote control from Discord: on (it looks for a /remote command every 15 seconds)" : "Remote control from Discord: off"); if (Wanted && stats.NotOurs && !saidNotOurs) log("Remote control from Discord: off - the stats link isn't from our Discord (only /link there gives one it listens to)"); saidNotOurs = Wanted && stats.NotOurs; if (!on && !wasOn && Stale) { Retire(); log("Remote control from Discord: off - its message in our Discord deleted"); } if (!on && wasOn) Retire(); if (on && DateTime.UtcNow >= nextTry) Tick(); public string Share(string code) shareId = "s" + Guid.NewGuid().ToString("N").Substring(0, 12); shareAt = DateTime.UtcNow; if (Json(null, false, "").Length > 1850) { lock (gate) shareId = shareCode = null; return "too big"; } string MessageOf(string link) string saved = (s.RemoteMessage ?? "").Trim(), wid = StatsCard.WebhookOf(link); int slash = saved.IndexOf('/'); if (slash <= 0 || wid.Length == 0 || saved.Substring(0, slash) != wid) return null; string mid = saved.Substring(slash + 1); return Regex.IsMatch(mid, @"^\d{5,30}$") ? mid : null; internal string Json(string doneId, bool ok, string text, bool picture = false, bool zip = false) var names = PresetNames != null ? PresetNames() : new List<string>(); string json = "{\n \"imb\": 1,\n \"remote\": 1,\n \"app\": " + StatsCard.Q(Program.Version) + ",\n \"presets\": [" + string.Join(", ", names.Take(25).Select(n => StatsCard.Q(n.Length > 60 ? n.Substring(0, 60) : n))) + "]"; lock (gate) { sid = shareId; scode = shareCode; } if (sid != null) json += ",\n \"share\": { \"id\": " + StatsCard.Q(sid) + ", \"code\": " + StatsCard.Q(scode) + " }"; if (doneId != null) json += ",\n \"done\": { \"id\": " + StatsCard.Q(doneId) + ", \"ok\": " + (ok ? "true" : "false") + ", \"text\": " + StatsCard.Q(text.Length > 900 ? text.Substring(0, 900) : text) + (picture ? ", \"picture\": true" : "") + (zip ? ", \"zip\": true" : "") + " }"; static string Payload(string json, string files = null) return "{\"username\":\"Idle Mafia Bot\",\"allowed_mentions\":{\"parse\":[]},\"content\":" + StatsCard.Q("```json\n" + json + "\n```") + (files != null ? ",\"attachments\":" + files : "") + "}"; string link = Link, mid = MessageOf(link), answer, after; string core = Json(null, false, ""); int code = StatsCard.Call("POST", StatsCard.Base(link) + "?wait=true", Payload(core), Timeout, out answer, out after); string id = code == 200 ? StatsCard.TopLevel(answer, "id") : null; if (id == null || !Regex.IsMatch(id, @"^\d{5,30}$")) { Failed(code, answer, link); return; } s.RemoteMessage = StatsCard.WebhookOf(link) + "/" + id; int got = Get(StatsCard.Base(link) + "/messages/" + mid, out answer, out discordNow); if (got == 404 && StatsCard.ErrorCode(answer) != 10015) { s.RemoteMessage = ""; save(); return; } if (got != 200) { Failed(got, answer, link); return; } if (failing) { failing = false; log("Remote control from Discord: works again"); } nextTry = DateTime.MinValue; Command c = ReadCommand(StatsCard.TopLevel(answer, "content")); lost = shareId != null && (c == null || c.Do != "shared") && DateTime.UtcNow - shareAt > TooOld; if (lost || (c != null && c.Do == "shared" && c.Id != lastDone && !seen.Contains(c.Id))) { shareId = shareCode = null; dirty = true; } if (lost && ShareLost != null) ShareLost("No answer from our Discord about the share. Try again, or press Copy code and type /preset share there."); if (c != null && c.Id != lastDone && !seen.Contains(c.Id)) if (seen.Count >= 200) seen.Clear(); if (discordNow - c.At > TooOld || c.At - discordNow > TooOld) Answer(link, mid, c.Id, false, "That command came in while the bot was off or couldn't reach Discord, so it wasn't done. Send it again."); log("Remote control from Discord: " + c.Do + (c.Arg.Length > 0 ? " " + c.Arg : "")); if (c.Do == "screenshot") string why = "The bot's window isn't ready yet. Try again in a minute."; try { if (Shot != null) jpg = Shot(out why); } catch (Exception) { why = "Roblox couldn't be read just now. Try again in a minute."; } Answer(link, mid, c.Id, jpg != null, jpg != null ? "Your game right now." : why, jpg); var done = new ManualResetEvent(false); string text = "The bot didn't answer."; if (h == null) text = "The bot's window isn't ready yet. Try again in a minute."; h(c.Do, c.Arg, (o, t) => { ok = o; text = t; done.Set(); }); done.WaitOne(TimeSpan.FromSeconds(c.Do == "report" ? 120 : 20)); byte[] zip = ok && c.Do == "report" ? ZipOf(ReportZip) : null; if (zip != null && Answer(link, mid, c.Id, true, text, zip, Path.GetFileName(ReportZip))) { log("Remote control from Discord: the report went to Discord"); return; } Answer(link, mid, c.Id, ok, text); if (!dirty && !pictureUp) return; string now = Json(null, false, ""); if (now != postedCore || pictureUp) int code = StatsCard.Call("PATCH", StatsCard.Base(link) + "/messages/" + mid, Payload(now, pictureUp ? "[]" : null), Timeout, out answer, out after); if (code != 200 && code != 204) { Failed(code, answer, link); return; } bool Answer(string link, string mid, string id, bool ok, string text, byte[] file = null, string zipName = null) string core = Json(null, false, ""); string name = zipName != null ? Regex.Replace(zipName, @"[^\w\-. ()]", "_") : "game.jpg"; string files = file != null ? "[{\"id\":0,\"filename\":" + StatsCard.Q(name) + "}]" : pictureUp ? "[]" : null; int code = StatsCard.Call("PATCH", StatsCard.Base(link) + "/messages/" + mid, Payload(Json(id, ok, text, file != null && zipName == null, file != null && zipName != null), files), zipName != null ? 120000 : Timeout, out answer, out after, file, name, zipName != null ? "application/zip" : "image/jpeg"); if (code == 200 || code == 204) { postedCore = core; pictureUp = file != null; return true; } log("Remote control from Discord: the answer didn't go (" + StatsCard.Why(code, answer, link) + ")" + (zipName != null ? " - the report is sent without its zip" : "")); byte[] ZipOf(string path) if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null; if (new FileInfo(path).Length > ZipMax) { log("Remote control from Discord: the report is too big to send, it stays on the PC"); return null; } return File.ReadAllBytes(path); catch (Exception) { return null; } void Failed(int code, string answer, string link) var now = DateTime.UtcNow; nextTry = now.AddMinutes(code == 429 ? 0.5 : code < 0 ? 1 : code >= 500 ? 2 : 10); if (!failing) log("Remote control from Discord: couldn't reach its message (" + StatsCard.Why(code, answer, link) + ")"); string link = Link, mid = MessageOf(link), answer, after; if (mid == null || !StatsCard.IsLink(link)) return; StatsCard.Call("DELETE", StatsCard.Base(link) + "/messages/" + mid, null, Timeout, out answer, out after); internal sealed class Command { public string Id, Do, Arg; public DateTime At; } internal static Command ReadCommand(string content) string obj = StatsCard.TopLevel(content, "command"); if (string.IsNullOrEmpty(obj) || obj[0] != '{') return null; string id = StatsCard.TopLevel(obj, "id"), what = StatsCard.TopLevel(obj, "do"), arg = StatsCard.TopLevel(obj, "arg") ?? "", at = StatsCard.TopLevel(obj, "at"); if (id == null || !Regex.IsMatch(id, @"^[A-Za-z0-9_-]{4,40}$") || Array.IndexOf(Commands, what) < 0 || !DateTime.TryParse(at, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out when)) return null; int max = what == "addpreset" ? 1800 : what == "shared" ? 300 : 60; return new Command { Id = id, Do = what, Arg = arg.Length > max ? arg.Substring(0, max) : arg, At = when }; static int Get(string url, out string answer, out DateTime discordNow) answer = ""; discordNow = DateTime.UtcNow; ServicePointManager.SecurityProtocol |= (SecurityProtocolType)3072; var req = (HttpWebRequest)WebRequest.Create(url); req.UserAgent = "IdleMafiaBot/" + Program.Version; req.ReadWriteTimeout = Timeout; using (var resp = (HttpWebResponse)req.GetResponse()) if (DateTime.TryParse(resp.Headers["Date"], CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out d)) discordNow = d; using (var st = resp.GetResponseStream()) using (var r = new StreamReader(st, Encoding.UTF8)) answer = r.ReadToEnd(); return (int)resp.StatusCode; var r = e.Response as HttpWebResponse; if (r == null) { answer = e.Message; return -1; } using (var st = r.GetResponseStream()) using (var sr = new StreamReader(st, Encoding.UTF8)) { answer = sr.ReadToEnd(); return (int)r.StatusCode; } catch (Exception e) { answer = e.Message; return -1; }