using System.Collections.Generic;using System.Globalization;using System.Security.Cryptography;using System.Text.RegularExpressions; static readonly TimeSpan Every = TimeSpan.FromMinutes(10); static readonly TimeSpan EveryStopped = TimeSpan.FromHours(1); static readonly TimeSpan Gap = TimeSpan.FromSeconds(20); const int Timeout = 20000, ClosingTimeout = 2500; public static readonly string[] RarityKeys = { "common", "uncommon", "rare", "epic", "legendary", "mythic", "secret", "forbidden" }; internal const string OurServer = "1549472576743415838"; readonly Action<string> log; readonly DateTime openedAt = DateTime.UtcNow; readonly object gate = new object(), sendLock = new object(); readonly AutoResetEvent wake = new AutoResetEvent(false); int level, levelRead = -1, levelSame; DateTime jumpSince = DateTime.MinValue; readonly List<double> cashReads = new List<double>(), bankReads = new List<double>(); string sentState, sentCore; DateTime sentAt = DateTime.MinValue, nextTry = DateTime.MinValue; volatile bool forced, accountChanged; string lastLink, lastMessage, lastAccount, lastState; volatile string trouble, troubleLink; volatile string sentAtText; public event Action Changed; public StatsCard(Bot bot, Settings settings, Action<string> log, Action save) level = Math.Max(0, s.LastLevel); bot.HeaderRead += NoteHeader; bot.CrewRead += c => { lock (gate) crew = c; }; bot.SafehouseRead += i => { lock (gate) safehouse = i; }; bot.AccountChanged += a => lock (gate) { level = Math.Max(0, s.LastLevel); levelRead = -1; levelSame = 0; jumpSince = DateTime.MinValue; cashReads.Clear(); bankReads.Clear(); safehouse = null; crew = null; } public static bool IsLink(string link) { return Discord.IsLink(link); } string Link { get { return (s.StatsLink ?? "").Trim(); } } public bool On { get { string l = Link; return IsLink(l) && l != deadLink; } } public bool Ours { get { return On && guildLink == Link && guild == OurServer; } } public bool NotOurs { get { return On && guildLink == Link && guild != null && guild != OurServer; } } public string Trouble { get { string t = trouble; return t != null && troubleLink == Link ? t : null; } } return at != null ? "On. Sent at " + at + ". Type /stats in our Discord to see it." : "On. It sends in a moment."; public void Begin() { if (On) StartThread(); } public void Removed(string oldLink) oldLink = (oldLink ?? "").Trim(); string mid = MessageOf(oldLink); if (!IsLink(oldLink) || mid == null || oldLink == deadLink) { if (off) log("Stats in our Discord: off"); return; } int code = Call("DELETE", Base(oldLink) + "/messages/" + mid, null, Timeout, out answer, out after); if (code == 204 || code == 200) log("Stats in our Discord: " + (off ? "off, and your stats there deleted" : "the old link's stats deleted")); else if (off) log("Stats in our Discord: off" + (code == 404 || code == 401 ? "" : " (your stats there couldn't be deleted: " + Why(code, answer, oldLink) + ")")); }) { IsBackground = true, Name = "stats remove" }; public void Closing(string closedBy) string by = closedBy ?? ""; if (!(by.StartsWith("you") || by.StartsWith("--exit")) || !On) return; if (!Monitor.TryEnter(sendLock, ClosingTimeout)) return; try { Send("closed", ClosingTimeout); } finally { Monitor.Exit(sendLock); } public string Preview() { return Json(StateNow(), true); } string link = Link, mid = lastMessage, account = lastAccount; if (mid == null || lastLink != link || account == null || account == Fingerprint(s.Account) || (lastState != "playing" && lastState != "offline")) return; string json = "{\n \"imb\": 1,\n \"app\": " + Q(Program.Version) + ",\n \"account\": " + Q(account) + ",\n \"state\": \"stopped\"\n}"; int code = Call("PATCH", Base(link) + "/messages/" + mid, Payload(json), Timeout, out answer, out after); if (code == 200 || code == 204) log("Stats in our Discord: the other account's stats say stopped"); void NoteHeader(Header h) if (h.Level > 0) NoteLevel(h.Level, DateTime.UtcNow); if (h.Cash >= 0) Keep(cashReads, h.Cash); if (h.Banked >= 0) Keep(bankReads, h.Banked); internal void NoteLevel(int read, DateTime now) levelSame = read == levelRead ? levelSame + 1 : 1; if (level <= 0 || read == level) { if (levelSame >= 2) level = read; jumpSince = DateTime.MinValue; return; } if (read > level && SevensForOnes(read, level)) return; if (read < level && SevensForOnes(level, read)) { jumpSince = DateTime.MinValue; if (levelSame >= 2) level = read; return; } if (read < level) { jumpSince = DateTime.MinValue; if (levelSame >= 3) level = read; return; } if (read <= level + 10) { jumpSince = DateTime.MinValue; if (levelSame >= 2) level = read; return; } if (jumpSince == DateTime.MinValue) jumpSince = now; if (levelSame >= 2 && read <= level + Math.Ceiling(15 * (now - jumpSince).TotalHours)) { level = read; jumpSince = DateTime.MinValue; } internal static bool SevensForOnes(int read, int other) if (Math.Abs(read - other) <= 10) return false; string digits = read.ToString(); var sevens = new List<int>(); for (int i = 0; i < digits.Length; i++) if (digits[i] == '7') sevens.Add(i); for (int mask = 1; mask < (1 << sevens.Count); mask++) char[] chars = digits.ToCharArray(); for (int bit = 0; bit < sevens.Count; bit++) if ((mask & (1 << bit)) != 0) chars[sevens[bit]] = '1'; if (Math.Abs(int.Parse(new string(chars)) - other) <= 10) return true; static void Keep(List<double> reads, double v) { reads.Add(v); if (reads.Count > 3) reads.RemoveAt(0); } static double Middle(List<double> reads) if (reads.Count == 0) return -1; var o = reads.OrderBy(x => x).ToList(); return o[(o.Count - 1) / 2]; string StateNow() { return !bot.Running ? "stopped" : bot.OutOfGame ? "offline" : "playing"; } internal string NumbersLine() var parts = new List<string>(); if (level > 0) parts.Add("level " + level); double cash = Middle(cashReads), bank = Middle(bankReads); if (cash >= 0) parts.Add(SafehouseInfo.Money(cash) + " cash"); if (bank >= 0) parts.Add(SafehouseInfo.Money(bank) + " in the bank"); return string.Join(", ", parts); internal string Json(string state, bool session) { return Json(state, session, Fingerprint(s.Account)); } string Json(string state, bool session, string acct) var lines = new List<string>(); Action<string, string> add = (k, v) => lines.Add(" " + Q(k) + ": " + v); add("app", Q(Program.Version)); if (acct != null) add("account", Q(acct)); if (level > 0) add("level", N(level)); if (s.LastPrestige >= 0) add("prestige", N(s.LastPrestige)); double cash = Middle(cashReads), bank = Middle(bankReads); if (cash >= 0) add("cash", N(cash)); if (bank >= 0) add("bank", N(bank)); if (sh.Respect >= 0) add("respect", N(sh.Respect)); if (sh.AttackPower >= 0) add("attack", N(sh.AttackPower)); if (sh.DefensePower >= 0) add("defense", N(sh.DefensePower)); if (sh.Income >= 0) add("income", N(sh.Income)); if (c != null && c.ByRarity.Sum() > 0) var parts = new List<string>(); if (c.Henchmen >= 0) parts.Add(Q("henchmen") + ": " + N(c.Henchmen)); if (c.Slots > 0) parts.Add(Q("slots") + ": " + N(c.Slots)); if (c.Attack >= 0) parts.Add(Q("attack") + ": " + N(c.Attack)); if (c.Defense >= 0) parts.Add(Q("defense") + ": " + N(c.Defense)); var rar = new List<string>(); for (int r = 0; r < RarityKeys.Length && r < c.ByRarity.Length; r++) if (c.ByRarity[r] > 0) rar.Add(Q(RarityKeys[r]) + ": " + N(c.ByRarity[r])); parts.Add(Q("rarities") + ": { " + string.Join(", ", rar) + " }"); add("crew", "{ " + string.Join(", ", parts) + " }"); add("session", "{ " + Q("minutes") + ": " + N(Math.Floor((DateTime.UtcNow - openedAt).TotalMinutes)) + ", " + Q("jobs") + ": " + N(k.Jobs) + ", " + Q("fights") + ": " + N(k.Fights) + ", " + Q("bosses") + ": " + N(k.BossesBeaten) + ", " + Q("properties") + ": " + N(k.Properties) + ", " + Q("rerolls") + ": " + N(k.Rerolls) + " }"); add("alltime", "{ " + Q("jobs") + ": " + N(t.Jobs) + ", " + Q("fights") + ": " + N(t.Fights) + ", " + Q("bosses") + ": " + N(t.BossesBeaten) + ", " + Q("properties") + ": " + N(t.Properties) + ", " + Q("rerolls") + ": " + N(t.Rerolls) + " }"); return "{\n" + string.Join(",\n", lines) + "\n}"; internal static string Fingerprint(string account) account = (account ?? "").Trim(); if (!Accounts.Plausible(account)) return null; using (var sha = SHA256.Create()) var b = sha.ComputeHash(Encoding.UTF8.GetBytes("idle mafia bot stats: " + account.ToUpperInvariant())); return string.Concat(b.Take(5).Select(x => x.ToString("x2", CultureInfo.InvariantCulture))); static string N(double v) { return Math.Round(Math.Min(v, 1e21)).ToString("0", CultureInfo.InvariantCulture); } internal static string Q(string t) var sb = new StringBuilder("\""); foreach (char c in t ?? "") if (c == '"') sb.Append("\\\""); else if (c == '\\') sb.Append("\\\\"); else if (c == '\n') sb.Append("\\n"); else if (c < ' ') sb.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); return sb.Append('"').ToString(); static string Payload(string json) return "{\"username\":\"Idle Mafia Bot\",\"allowed_mentions\":{\"parse\":[]},\"content\":" + Q("```json\n" + json + "\n```") + "}"; if (thread != null) return; thread = new Thread(Loop) { IsBackground = true, Name = "stats" }; try { lock (sendLock) Tick(); } if (accountChanged) { accountChanged = false; Retire(); } var now = DateTime.UtcNow; if (!force && now < nextTry) return; string state = StateNow(); bool due = force || sentState == null || state != sentState || (state != "stopped" && now - sentAt >= Every) || (now - sentAt >= EveryStopped && Json(state, false) != sentCore); if (!due || (!force && now - sentAt < Gap)) return; void Send(string state, int timeoutMs) if (!IsLink(link) || link == deadLink) return; if (guildLink != link) { guildLink = link; guildAsked = false; guild = null; } if (!guildAsked && timeoutMs > ClosingTimeout) AskGuild(link, timeoutMs); string account = s.Account, fp = Fingerprint(account), wid = WebhookOf(link), mid = MessageOf(link); string json = Json(state, true, fp), core = Json(state, false, fp); string body = Payload(json), answer = "", after = null; code = Call("PATCH", Base(link) + "/messages/" + mid, body, timeoutMs, out answer, out after); if (code == 404 && ErrorCode(answer) != 10015) mid = null; code = Call("POST", Base(link) + "?wait=true", body, timeoutMs, out answer, out after); string id = code == 200 ? TopLevel(answer, "id") : null; if (id != null && Regex.IsMatch(id, @"^\d{5,30}$")) if (s.Account == account && Link == link) { s.StatsMessage = wid + "/" + id; save(); } var now = DateTime.UtcNow; if (code == 200 || code == 204) bool first = sentState == null, changed = state != sentState; lastLink = link; lastMessage = mid; lastAccount = fp; lastState = state; sentState = state; sentCore = core; sentAt = now; fails = 0; nextTry = DateTime.MinValue; sentAtText = DateTime.Now.ToString("HH:mm", CultureInfo.InvariantCulture); if (failing) { failing = false; log("Stats in our Discord: sending works again"); } else if (first || changed) log("Stats in our Discord: sent (" + state + ")"); SetTrouble(link, null, true); if (code == 401 || code == 404) why = "Discord says this link doesn't work any more (deleted with /unlink, or not copied whole). Type /link in our Discord for your link."; log("Stats in our Discord: the link doesn't work any more (/unlink, or a new one from /link?) - nothing more is sent to it"); int waitMin = code == 429 ? 0 : code < 0 ? (fails == 1 ? 1 : 5) : code >= 500 ? 2 : 10; nextTry = now.AddMinutes(waitMin); if (code == 429 && after != null && double.TryParse(after, NumberStyles.Float, CultureInfo.InvariantCulture, out sec)) nextTry = now.AddSeconds(Math.Min(600, sec + 1)); else if (code == 429) nextTry = now.AddSeconds(30); why = "The last update didn't go out: " + Why(code, answer, link) + ". It tries again shortly."; if (!failing) log("Stats in our Discord: couldn't send (" + Why(code, answer, link) + ")"); SetTrouble(link, why, false); void AskGuild(string link, int timeoutMs) string guildBefore = guild; int code = Call("GET", Base(link), null, timeoutMs, out answer, out after); if (code == 200) { guild = TopLevel(answer, "guild_id") ?? ""; guildAsked = true; } else if (code == 401 || code == 404) { guild = null; guildAsked = true; } if (guild != guildBefore) { var h = Changed; if (h != null) h(); } void SetTrouble(string link, string why, bool sent) if (!sent && trouble == why && troubleLink == link) return; trouble = why; troubleLink = link; internal static string Why(int code, string answer, string link) if (code < 0) return "couldn't reach Discord (" + (answer ?? "").Replace(link, "the link").Trim().TrimEnd('.') + ")"; if (code == 429) return "Discord asked to slow down"; if (code == 403) return "Discord refused it (403), which a VPN or a school or work network can cause"; if (code == 407) return "a proxy on this PC blocked it (407)"; if (code >= 400 && code < 500) return "Discord refused it (" + code + ")"; return "Discord had a problem (" + code + ")"; internal static string WebhookOf(string link) var m = Regex.Match(link ?? "", @"/webhooks/(\d+)/"); return m.Success ? m.Groups[1].Value : ""; string MessageOf(string link) string saved = (s.StatsMessage ?? "").Trim(), wid = 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 static string Base(string link) { string b = link.Trim().TrimEnd('/'); return Rebase != null ? Rebase(b) : b; } internal static Func<string, string> Rebase = null; internal static int Call(string method, string url, string json, int timeoutMs, out string answer, out string retryAfter, byte[] picture = null, string fileName = "game.jpg", string fileType = "image/jpeg") answer = ""; retryAfter = null; ServicePointManager.SecurityProtocol |= (SecurityProtocolType)3072; var req = (HttpWebRequest)WebRequest.Create(url); req.UserAgent = "IdleMafiaBot/" + Program.Version; req.ReadWriteTimeout = timeoutMs; var body = Encoding.UTF8.GetBytes(json); req.ContentType = "application/json"; string boundary = "----IdleMafiaBot" + DateTime.UtcNow.Ticks.ToString("x", CultureInfo.InvariantCulture); var parts = new MemoryStream(); Action<string> write = t => { var b = Encoding.UTF8.GetBytes(t); parts.Write(b, 0, b.Length); }; write("--" + boundary + "\r\nContent-Disposition: form-data; name=\"payload_json\"\r\nContent-Type: application/json\r\n\r\n"); parts.Write(body, 0, body.Length); write("\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"files[0]\"; filename=\"" + fileName + "\"\r\nContent-Type: " + fileType + "\r\n\r\n"); parts.Write(picture, 0, picture.Length); write("\r\n--" + boundary + "--\r\n"); req.ContentType = "multipart/form-data; boundary=" + boundary; req.ContentLength = body.Length; using (var rs = req.GetRequestStream()) rs.Write(body, 0, body.Length); using (var resp = (HttpWebResponse)req.GetResponse()) { answer = ReadAll(resp); return (int)resp.StatusCode; } var r = e.Response as HttpWebResponse; if (r == null) { answer = e.Message; return -1; } using (r) { answer = ReadAll(r); retryAfter = r.Headers["Retry-After"]; return (int)r.StatusCode; } catch (Exception e) { answer = e.Message; return -1; } static string ReadAll(HttpWebResponse r) using (var st = r.GetResponseStream()) using (var ms = new MemoryStream()) var buf = new byte[8192]; while (ms.Length < 65536 && (n = st.Read(buf, 0, buf.Length)) > 0) ms.Write(buf, 0, n); return Encoding.UTF8.GetString(ms.ToArray()); catch (Exception) { return ""; } internal static int ErrorCode(string answer) return int.TryParse(TopLevel(answer, "code"), NumberStyles.Integer, CultureInfo.InvariantCulture, out n) ? n : -1; internal static string TopLevel(string json, string key) if (string.IsNullOrEmpty(json)) return null; int i = json.IndexOf('{'); if (i >= json.Length || json[i] != '"') return null; string k = ReadString(json, ref i); if (k == null || i >= json.Length || json[i] != ':') return null; if (i >= json.Length) return null; if (json[i] == '"') v = ReadString(json, ref i); if (!SkipValue(json, ref i)) return null; v = json.Substring(start, i - start).Trim(); if (i < json.Length && json[i] == ',') { i++; continue; } static void SkipSpace(string t, ref int i) { while (i < t.Length && char.IsWhiteSpace(t[i])) i++; } static string ReadString(string t, ref int i) var sb = new StringBuilder(); for (i++; i < t.Length; i++) if (c == '"') { i++; return sb.ToString(); } if (c != '\\') { sb.Append(c); continue; } if (++i >= t.Length) return null; if (e == 'u' && i + 4 < t.Length) if (int.TryParse(t.Substring(i + 1, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out u)) sb.Append((char)u); else sb.Append(e == 'n' ? '\n' : e == 't' ? '\t' : e == 'r' ? '\r' : e == 'b' ? '\b' : e == 'f' ? '\f' : e); static bool SkipValue(string t, ref int i) for (; i < t.Length; i++) if (c == '"') { if (ReadString(t, ref i) == null) return false; i--; continue; } if (c == '{' || c == '[') depth++; else if (c == '}' || c == ']') if (depth == 0) return true; if (--depth == 0) { i++; return true; } else if (c == ',' && depth == 0) return true;