using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using SleepeyDev.OnTogetherPomoNotify.Notify; using SleepeyDev.OnTogetherPomoNotify.Pomodoro; using SleepeyDev.OnTogetherPomoNotify.UI; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("SleepeyDev")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+65ccdec13cf2eefc4f6e6c5978198ed29bf47af6")] [assembly: AssemblyProduct("OnTogetherPomoNotify")] [assembly: AssemblyTitle("OnTogetherPomoNotify")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace SleepeyDev.OnTogetherPomoNotify { public static class ModConfig { public static ConfigEntry Enabled; public static ConfigEntry BotToken; public static ConfigEntry ChatId; public static ConfigEntry Prefix; public static ConfigEntry NotifyPhaseChange; public static ConfigEntry NotifyWarning; public static ConfigEntry WarningMinutes; public static ConfigEntry NotifyStartStop; public static ConfigEntry NotifySummary; public static ConfigEntry MenuKey; public static ConfigEntry UiScale; public static ConfigEntry WindowX; public static ConfigEntry WindowY; public static ConfigEntry VerboseLogging; public static void Bind(ConfigFile cfg) { //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Expected O, but got Unknown Enabled = cfg.Bind("Telegram", "Enabled", true, "Master switch. Off means nothing is sent and no network call is made at all."); BotToken = cfg.Bind("Telegram", "BotToken", "", "Bot token from @BotFather, of the form 8123456789:AA...\nTREAT THIS AS A PASSWORD. Anyone who has it can read everything your bot receives\nand post as it. It is stored here in plain text because there is nowhere else to put\nit; do not paste this file, or a screenshot of it, into a chat or a bug report.\nThe mod never writes the token to the log."); ChatId = cfg.Bind("Telegram", "ChatId", "", "Where to send. Your own numeric id for a private chat, or -100... for a channel.\nMessage your bot once, then press 'Find chat id' in the mod's window and it fills\nthis in for you."); Prefix = cfg.Bind("Telegram", "Prefix", "", "Put in front of every message. Useful if one chat receives several sources."); NotifyPhaseChange = cfg.Bind("Notify", "PhaseChange", true, "Focus ending and a break starting are the same moment, and get one message, not two."); NotifyWarning = cfg.Bind("Notify", "Warning", true, "Send a heads-up shortly before the current phase ends."); WarningMinutes = cfg.Bind("Notify", "WarningMinutes", "5, 1", "Minutes-remaining marks that trigger a warning. Comma separated; 1 to 180.\nA mark is skipped when the timer jumps past it - syncing with another player moves\nthe clock, and 'five minutes left' arriving with forty seconds left helps nobody."); NotifyStartStop = cfg.Bind("Notify", "StartStop", true, "Send when the timer is started, stopped, paused or resumed."); NotifySummary = cfg.Bind("Notify", "Summary", true, "Append what the run has achieved to the long-break and stopped messages."); MenuKey = cfg.Bind("UI", "MenuKey", (KeyCode)282, "Opens the settings window. F3 is taken by Local Playlist, F2 by Pomodoro Sound,\nF4-F6 and F10-F11 by Day and Night, F7-F9 by FrameCare and BlueSage."); UiScale = cfg.Bind("UI", "UiScale", 0f, new ConfigDescription("Size of the settings window. 0 picks a size from your screen height, which is what\nyou want on anything above 1080p.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 4f), Array.Empty())); WindowX = cfg.Bind("UI", "WindowX", 60f, "Remembered window position, in unscaled points. Moving the window writes these."); WindowY = cfg.Bind("UI", "WindowY", 60f, "See WindowX."); VerboseLogging = cfg.Bind("Diagnostics", "VerboseLogging", false, "Log every snapshot decision and every send. Noisy; for bug reports.\nThe bot token is redacted out of anything logged, verbose or not."); } public static bool On(ConfigEntry entry) { return entry?.Value ?? false; } public static string Str(ConfigEntry entry) { if (entry != null && entry.Value != null) { return entry.Value; } return ""; } public static PlannerSettings Planner() { return new PlannerSettings { PhaseChange = On(NotifyPhaseChange), Warning = On(NotifyWarning), StartStop = On(NotifyStartStop), WarningMinutes = PlannerSettings.ParseMinutes(Str(WarningMinutes)) }; } } public sealed class ModRoot : MonoBehaviour { public static ModRoot Instance { get; private set; } public static void Create() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown if (!((Object)(object)Instance != (Object)null)) { GameObject val = new GameObject("OTPN_ModRoot") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)val); Instance = val.AddComponent(); } } public static Coroutine Run(IEnumerator routine) { if ((Object)(object)Instance == (Object)null || routine == null) { return null; } return ((MonoBehaviour)Instance).StartCoroutine(routine); } private void Update() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.MenuKey != null && Input.GetKeyDown(ModConfig.MenuKey.Value)) { SettingsWindow.Toggle(); } try { PhaseWatcher.Tick(); } catch (Exception ex) { Plugin.Log.LogError((object)("PhaseWatcher.Tick threw, notifications off: " + ex)); PhaseWatcher.Disable(); } try { Telegram.Tick(); } catch (Exception ex2) { Plugin.Log.LogError((object)("Telegram.Tick threw: " + ex2)); } } private void OnGUI() { try { SettingsWindow.Draw(); } catch (Exception ex) { Plugin.Log.LogError((object)("Settings window failed, closing it: " + ex)); SettingsWindow.Close(); } } private void OnDestroy() { InputGate.Release(); } private void OnApplicationQuit() { InputGate.Release(); } } [BepInPlugin("com.sleepeydev.ontogether.pomonotify", "On-Together PomoNotify", "1.0.0")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "com.sleepeydev.ontogether.pomonotify"; public static ManualLogSource Log { get; private set; } private void Awake() { //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) Log = ((BaseUnityPlugin)this).Logger; try { ModConfig.Bind(((BaseUnityPlugin)this).Config); } catch (Exception ex) { Log.LogError((object)("Config binding failed - some features will switch themselves off: " + ex)); } ModRoot.Create(); SceneManager.activeSceneChanged += OnSceneChanged; Log.LogInfo((object)("PomoNotify " + ((BaseUnityPlugin)this).Info.Metadata.Version?.ToString() + " ready. Press " + ((ModConfig.MenuKey != null) ? ((object)ModConfig.MenuKey.Value/*cast due to .constrained prefix*/).ToString() : "F1") + " for its settings.")); Refl.LogHealth(); } private void OnSceneChanged(Scene from, Scene to) { try { PhaseWatcher.Forget(); Telegram.Clear(); InputGate.Release(); } catch (Exception ex) { Log.LogWarning((object)("Scene change handling: " + ex.Message)); } } private void OnDestroy() { SceneManager.activeSceneChanged -= OnSceneChanged; } } public static class Refl { private static readonly List Missing = new List(); private static readonly MethodInfo GetInfo = Method(typeof(PomodoroController), "GetPomodoroInfo"); private static readonly PropertyInfo WritingProp = Property(typeof(TaskManager), "IsWriting"); private static readonly FieldInfo FType = Field(typeof(PomodoroInfo), "Type"); private static readonly FieldInfo FMinute = Field(typeof(PomodoroInfo), "CurrentMinute"); private static readonly FieldInfo FSecond = Field(typeof(PomodoroInfo), "CurrentSecond"); private static readonly FieldInfo FTotalSession = Field(typeof(PomodoroInfo), "TotalSession"); private static readonly FieldInfo FSession = Field(typeof(PomodoroInfo), "CurrentSession"); private static readonly FieldInfo FBreak = Field(typeof(PomodoroInfo), "BreakTime"); private static readonly FieldInfo FLongBreak = Field(typeof(PomodoroInfo), "LongBreakTime"); private static readonly FieldInfo FStudy = Field(typeof(PomodoroInfo), "StudyTime"); private static readonly FieldInfo FPaused = Field(typeof(PomodoroInfo), "IsPaused"); public static bool Ready { get { if (GetInfo != null && FType != null && FMinute != null && FSecond != null && FTotalSession != null && FSession != null && FBreak != null && FLongBreak != null && FStudy != null) { return FPaused != null; } return false; } } public static bool CanGateInput { get { if (WritingProp != null) { return WritingProp.CanWrite; } return false; } } public static PomodoroSnapshot Read() { PomodoroSnapshot result = default(PomodoroSnapshot); if (!Ready) { return result; } PomodoroController i = MonoSingleton.I; if ((Object)(object)i == (Object)null) { return result; } object obj = GetInfo.Invoke(i, null); if (obj == null) { return result; } result.Present = true; result.Phase = (Phase)(int)FType.GetValue(obj); result.Minute = (int)FMinute.GetValue(obj); result.Second = (float)FSecond.GetValue(obj); result.SessionsPerLongBreak = (int)FTotalSession.GetValue(obj); result.Session = (int)FSession.GetValue(obj); result.BreakMinutes = (int)FBreak.GetValue(obj); result.LongBreakMinutes = (int)FLongBreak.GetValue(obj); result.StudyMinutes = (int)FStudy.GetValue(obj); result.Paused = (bool)FPaused.GetValue(obj); return result; } public static bool GetWriting() { try { TaskManager i = MonoSingleton.I; if ((Object)(object)i == (Object)null || WritingProp == null) { return false; } return (bool)WritingProp.GetValue(i, null); } catch (Exception) { return false; } } public static void SetWriting(bool value) { try { TaskManager i = MonoSingleton.I; if (!((Object)(object)i == (Object)null) && !(WritingProp == null) && WritingProp.CanWrite) { WritingProp.SetValue(i, value, null); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not set the game's typing flag: " + ex.Message)); } } private static MethodInfo Method(Type owner, string name) { MethodInfo methodInfo = AccessTools.Method(owner, name, (Type[])null, (Type[])null); if (methodInfo == null) { Missing.Add(owner.Name + "." + name + "()"); } return methodInfo; } private static PropertyInfo Property(Type owner, string name) { PropertyInfo propertyInfo = AccessTools.Property(owner, name); if (propertyInfo == null) { Missing.Add(owner.Name + "." + name); } return propertyInfo; } private static FieldInfo Field(Type owner, string name) { FieldInfo fieldInfo = AccessTools.Field(owner, name); if (fieldInfo == null) { Missing.Add(owner.Name + "." + name); } return fieldInfo; } public static void LogHealth() { if (Missing.Count == 0) { Plugin.Log.LogInfo((object)"Game handles: all resolved."); return; } Plugin.Log.LogWarning((object)("Game handles missing (the game was probably updated): " + string.Join(", ", Missing.ToArray()))); Plugin.Log.LogWarning((object)("Feature availability - notifications: " + (Ready ? "on" : "OFF") + ", input blocking while the window is open: " + (CanGateInput ? "on" : "OFF"))); } } } namespace SleepeyDev.OnTogetherPomoNotify.UI { public static class InputGate { private static bool _ours; public static void Hold() { if (Refl.CanGateInput && !Refl.GetWriting()) { Refl.SetWriting(value: true); _ours = true; } } public static void Release() { if (_ours) { _ours = false; if (Refl.CanGateInput && Refl.GetWriting()) { Refl.SetWriting(value: false); } } } } public static class SettingsWindow { [CompilerGenerated] private static class <>O { public static WindowFunction <0>__Body; } private const int WindowId = 1879; private const float Width = 440f; private const float LabelWidth = 74f; private static Rect _rect = new Rect(60f, 60f, 440f, 120f); private static bool _placed; private static bool _showToken; private static string _status = ""; private static string _token = ""; private static string _chat = ""; private static string _prefix = ""; private static string _warn = ""; public static bool Open { get; private set; } private static bool Usable { get { if (ModConfig.Enabled != null && ModConfig.BotToken != null && ModConfig.ChatId != null && ModConfig.Prefix != null && ModConfig.WarningMinutes != null && ModConfig.NotifyPhaseChange != null && ModConfig.NotifyStartStop != null && ModConfig.NotifySummary != null && ModConfig.NotifyWarning != null && ModConfig.WindowX != null && ModConfig.WindowY != null) { return ModConfig.UiScale != null; } return false; } } public static void Toggle() { if (Open) { Close(); } else { Show(); } } public static void Show() { if (!Usable) { Plugin.Log.LogWarning((object)"The settings window needs the config, and binding it failed. Edit com.sleepeydev.ontogether.pomonotify.cfg by hand."); return; } if (!_placed) { ((Rect)(ref _rect)).x = ModConfig.WindowX.Value; ((Rect)(ref _rect)).y = ModConfig.WindowY.Value; _placed = true; } _token = ModConfig.Str(ModConfig.BotToken); _chat = ModConfig.Str(ModConfig.ChatId); _prefix = ModConfig.Str(ModConfig.Prefix); _warn = ModConfig.Str(ModConfig.WarningMinutes); _showToken = false; _status = ""; Open = true; } public static void Close() { if (Open) { Open = false; Commit(); InputGate.Release(); } } private static void Commit() { Set(ModConfig.BotToken, _token.Trim()); Set(ModConfig.ChatId, _chat.Trim()); Set(ModConfig.Prefix, _prefix); Set(ModConfig.WarningMinutes, _warn); if (!Mathf.Approximately(ModConfig.WindowX.Value, ((Rect)(ref _rect)).x)) { ModConfig.WindowX.Value = ((Rect)(ref _rect)).x; } if (!Mathf.Approximately(ModConfig.WindowY.Value, ((Rect)(ref _rect)).y)) { ModConfig.WindowY.Value = ((Rect)(ref _rect)).y; } } private static void Set(ConfigEntry entry, string value) { if (entry != null && entry.Value != value) { entry.Value = value; } } public static void Draw() { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Expected O, but got Unknown if (Open && Usable) { InputGate.Hold(); float num = ((ModConfig.UiScale.Value > 0f) ? ModConfig.UiScale.Value : Mathf.Max(1f, Mathf.Round((float)Screen.height / 900f * 2f) / 2f)); Matrix4x4 matrix = GUI.matrix; GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(num, num, 1f)); ((Rect)(ref _rect)).width = 440f; Rect rect = _rect; object obj = <>O.<0>__Body; if (obj == null) { WindowFunction val = Body; <>O.<0>__Body = val; obj = (object)val; } _rect = GUILayout.Window(1879, rect, (WindowFunction)obj, "Pomodoro → Telegram", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(440f) }); ((Rect)(ref _rect)).x = Mathf.Clamp(((Rect)(ref _rect)).x, -300f, Mathf.Max(0f, (float)Screen.width / num - 90f)); ((Rect)(ref _rect)).y = Mathf.Clamp(((Rect)(ref _rect)).y, 0f, Mathf.Max(0f, (float)Screen.height / num - 40f)); GUI.matrix = matrix; } } private static void Body(int id) { //IL_03c0: Unknown result type (might be due to invalid IL or missing references) GUILayout.Space(4f); ModConfig.Enabled.Value = GUILayout.Toggle(ModConfig.Enabled.Value, " Send notifications", Array.Empty()); GUILayout.Space(8f); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Bot token", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(74f) }); _token = (_showToken ? GUILayout.TextField(_token, 100, Array.Empty()) : GUILayout.PasswordField(_token, '*', 100, Array.Empty())); _showToken = GUILayout.Toggle(_showToken, "Show", GUI.skin.button, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(52f) }); GUILayout.EndHorizontal(); if (_token.Trim().Length > 0 && !Secret.LooksLikeToken(_token.Trim())) { Note("That does not look like a token. BotFather gives you 8123456789:AA…"); } GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Chat id", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(74f) }); _chat = GUILayout.TextField(_chat, 32, Array.Empty()); if (GUILayout.Button("Find", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(52f) })) { Commit(); _status = "Asking Telegram…"; Telegram.FindChatId(delegate(bool ok, string msg) { _status = msg; if (ok) { _chat = ModConfig.Str(ModConfig.ChatId); } }); } GUILayout.EndHorizontal(); Note("Write anything to your bot in Telegram, then press Find."); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Prefix", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(74f) }); _prefix = GUILayout.TextField(_prefix, 40, Array.Empty()); GUILayout.EndHorizontal(); GUILayout.Space(10f); GUILayout.Label("Send me", Array.Empty()); ModConfig.NotifyPhaseChange.Value = GUILayout.Toggle(ModConfig.NotifyPhaseChange.Value, " focus and break changes", Array.Empty()); ModConfig.NotifyStartStop.Value = GUILayout.Toggle(ModConfig.NotifyStartStop.Value, " start, stop, pause and resume", Array.Empty()); ModConfig.NotifySummary.Value = GUILayout.Toggle(ModConfig.NotifySummary.Value, " a summary at the long break and at the end", Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); ModConfig.NotifyWarning.Value = GUILayout.Toggle(ModConfig.NotifyWarning.Value, " a warning at", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(110f) }); _warn = GUILayout.TextField(_warn, 24, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(70f) }); GUILayout.Label("minutes left", Array.Empty()); GUILayout.EndHorizontal(); if (ModConfig.NotifyWarning.Value && PlannerSettings.ParseMinutes(_warn).Length == 0) { Note("No usable numbers there, so no warnings would be sent."); } GUILayout.Space(10f); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button("Send a test message", Array.Empty())) { Commit(); _status = "Sending…"; Telegram.SendNow("✅ On-Together is connected to this chat.", delegate(bool ok, string msg) { _status = msg; }); } if (GUILayout.Button("Close", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(70f) })) { Close(); } GUILayout.EndHorizontal(); GUILayout.Space(6f); GUILayout.Label(string.IsNullOrEmpty(_status) ? Readiness() : _status, Array.Empty()); GUILayout.Label(TimerLine(), Array.Empty()); GUI.DragWindow(new Rect(0f, 0f, 10000f, 20f)); } private static void Note(string text) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) Color color = GUI.color; GUI.color = new Color(color.r, color.g, color.b, color.a * 0.6f); GUILayout.Label(text, Array.Empty()); GUI.color = color; } private static string Readiness() { if (!Telegram.Configured(out var why)) { return why; } return "Ready."; } private static string TimerLine() { PomodoroSnapshot current = PhaseWatcher.Current; if (!current.Running) { return "Timer: not running."; } string text = ((current.Phase == Phase.Study) ? "focus" : ((current.Phase == Phase.Break) ? "break" : "long break")); string text2 = "Timer: " + text + ", " + current.RemainingClock() + " left" + (current.Paused ? " (paused)" : ""); RunTally progress = PhaseWatcher.Progress; if (progress.Any) { text2 = text2 + " · " + progress.FocusBlocks + " done, " + progress.FocusMinutes + " min"; } return text2; } } } namespace SleepeyDev.OnTogetherPomoNotify.Pomodoro { public static class MessageText { public static string Render(NotifyEvent e, RunTally tally, bool includeSummary, string prefix) { StringBuilder stringBuilder = new StringBuilder(); if (!string.IsNullOrEmpty(prefix)) { stringBuilder.Append(prefix.Trim()).Append(' '); } switch (e.Kind) { case NotifyKind.Started: stringBuilder.Append("▶\ufe0f ").Append(Starting(e.To, e.ToMinutes)); break; case NotifyKind.PhaseChanged: stringBuilder.Append(Changed(e, tally)); break; case NotifyKind.Warning: stringBuilder.Append("⏳ ").Append(e.WarningMinutes).Append((e.WarningMinutes == 1) ? " minute left of " : " minutes left of ") .Append(Of(e.To)); break; case NotifyKind.Paused: stringBuilder.Append("⏸\ufe0f Paused — ").Append(e.Clock).Append(" left of ") .Append(Of(e.To)); break; case NotifyKind.Resumed: stringBuilder.Append("▶\ufe0f Resumed — ").Append(e.Clock).Append(" left of ") .Append(Of(e.To)); break; case NotifyKind.Stopped: stringBuilder.Append("⏹\ufe0f Timer stopped."); break; } bool flag = e.Kind == NotifyKind.Stopped || (e.Kind == NotifyKind.PhaseChanged && e.To == Phase.LongBreak); if (includeSummary && flag && tally.Any) { stringBuilder.Append('\n').Append(Summary(tally)); } return stringBuilder.ToString(); } public static string Summary(RunTally t) { return "\ud83d\udcca " + t.FocusBlocks + ((t.FocusBlocks == 1) ? " focus block, " : " focus blocks, ") + t.FocusMinutes + " min focused"; } private static string Changed(NotifyEvent e, RunTally tally) { return e.To switch { Phase.Break => "\ud83c\udf45 Focus done" + OfCycle(tally, e) + ". Break: " + e.ToMinutes + " min", Phase.LongBreak => "\ud83c\udf45 Focus done. Long break: " + e.ToMinutes + " min", Phase.Study => ((e.From == Phase.LongBreak) ? "\ud83c\udf34 Long break over — " : "☕ Break over — ") + "focus for " + e.ToMinutes + " min", _ => "⏹\ufe0f Timer stopped.", }; } private static string OfCycle(RunTally tally, NotifyEvent e) { if (e.SessionsPerLongBreak <= 1 || tally.FocusBlocks <= 0) { return ""; } return " — block " + tally.FocusBlocks + " of " + e.SessionsPerLongBreak; } private static string Starting(Phase p, int minutes) { return p switch { Phase.Study => "Focus started — " + minutes + " min", Phase.Break => "Break started — " + minutes + " min", Phase.LongBreak => "Long break started — " + minutes + " min", _ => "Timer started", }; } private static string Of(Phase p) { return p switch { Phase.Study => "focus", Phase.Break => "the break", Phase.LongBreak => "the long break", _ => "the timer", }; } } public enum NotifyKind { Started, PhaseChanged, Warning, Paused, Resumed, Stopped } public struct NotifyEvent { public NotifyKind Kind; public Phase From; public Phase To; public int ToMinutes; public int WarningMinutes; public int Session; public int SessionsPerLongBreak; public string Clock; } public struct PlannerSettings { public bool PhaseChange; public bool Warning; public bool StartStop; public int[] WarningMinutes; public static PlannerSettings Everything(params int[] warnings) { return new PlannerSettings { PhaseChange = true, Warning = true, StartStop = true, WarningMinutes = (warnings ?? new int[0]) }; } public static int[] ParseMinutes(string csv) { List list = new List(); if (!string.IsNullOrEmpty(csv)) { string[] array = csv.Split(',', ';', ' '); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0 && int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && result > 0 && result <= 180 && !list.Contains(result)) { list.Add(result); } } } list.Sort(); list.Reverse(); return list.ToArray(); } } public static class NotificationPlanner { public const double StepSlackSeconds = 5.0; public static List Plan(PomodoroSnapshot prev, PomodoroSnapshot cur, PlannerSettings s, double elapsedSeconds) { List list = new List(2); if (!prev.Present || !cur.Present) { return list; } if (!prev.Running && cur.Running) { if (s.StartStop) { list.Add(Start(cur)); } return list; } if (prev.Running && !cur.Running) { if (s.StartStop) { list.Add(new NotifyEvent { Kind = NotifyKind.Stopped, From = prev.Phase }); } return list; } if (!cur.Running) { return list; } if (prev.Phase != cur.Phase) { if (s.PhaseChange) { list.Add(Change(prev, cur)); } return list; } if (prev.Paused != cur.Paused) { if (s.StartStop) { list.Add(new NotifyEvent { Kind = (cur.Paused ? NotifyKind.Paused : NotifyKind.Resumed), To = cur.Phase, Clock = cur.RemainingClock() }); } return list; } if (cur.Paused) { return list; } if (s.Warning) { int num = CrossedThreshold(prev, cur, s.WarningMinutes, elapsedSeconds); if (num > 0) { list.Add(new NotifyEvent { Kind = NotifyKind.Warning, To = cur.Phase, WarningMinutes = num, Clock = cur.RemainingClock() }); } } return list; } private static NotifyEvent Start(PomodoroSnapshot cur) { return new NotifyEvent { Kind = NotifyKind.Started, To = cur.Phase, ToMinutes = cur.PhaseLengthMinutes, Session = cur.Session, SessionsPerLongBreak = cur.SessionsPerLongBreak, Clock = cur.RemainingClock() }; } private static NotifyEvent Change(PomodoroSnapshot prev, PomodoroSnapshot cur) { return new NotifyEvent { Kind = NotifyKind.PhaseChanged, From = prev.Phase, To = cur.Phase, ToMinutes = cur.PhaseLengthMinutes, Session = cur.Session, SessionsPerLongBreak = cur.SessionsPerLongBreak, Clock = cur.RemainingClock() }; } private static int CrossedThreshold(PomodoroSnapshot prev, PomodoroSnapshot cur, int[] thresholds, double elapsedSeconds) { if (thresholds == null || thresholds.Length == 0) { return 0; } double remainingSeconds = prev.RemainingSeconds; double remainingSeconds2 = cur.RemainingSeconds; if (remainingSeconds2 >= remainingSeconds) { return 0; } double num = Math.Max(0.0, elapsedSeconds) * 2.0 + 5.0; if (remainingSeconds - remainingSeconds2 > num) { return 0; } int num2 = 0; foreach (int num3 in thresholds) { double num4 = (double)num3 * 60.0; if (remainingSeconds > num4 && remainingSeconds2 <= num4 && (num2 == 0 || num3 < num2)) { num2 = num3; } } return num2; } } public static class PhaseWatcher { private const float PollInterval = 1f; private static bool _disabled; private static bool _havePrev; private static PomodoroSnapshot _prev; private static double _prevAt; private static RunTally _tally; private static float _nextPoll; public static PomodoroSnapshot Current => _prev; public static RunTally Progress => _tally; public static void Disable() { _disabled = true; } public static void Tick() { if (_disabled || !Refl.Ready || Time.unscaledTime < _nextPoll) { return; } _nextPoll = Time.unscaledTime + 1f; PomodoroSnapshot pomodoroSnapshot = Refl.Read(); double realtimeSinceStartupAsDouble = Time.realtimeSinceStartupAsDouble; if (!_havePrev || !_prev.Present || !pomodoroSnapshot.Present) { if (!pomodoroSnapshot.Present) { _tally = default(RunTally); } _prev = pomodoroSnapshot; _prevAt = realtimeSinceStartupAsDouble; _havePrev = true; return; } double elapsedSeconds = realtimeSinceStartupAsDouble - _prevAt; RunTally tally = _tally; _tally = Tally.Advance(_tally, _prev, pomodoroSnapshot); List list; try { list = NotificationPlanner.Plan(_prev, pomodoroSnapshot, ModConfig.Planner(), elapsedSeconds); } catch (Exception ex) { Plugin.Log.LogError((object)("Planning failed, notifications off: " + ex)); _disabled = true; return; } _prev = pomodoroSnapshot; _prevAt = realtimeSinceStartupAsDouble; if (list.Count == 0) { return; } bool includeSummary = ModConfig.On(ModConfig.NotifySummary); string prefix = ModConfig.Str(ModConfig.Prefix); foreach (NotifyEvent item in list) { RunTally tally2 = ((item.Kind == NotifyKind.Stopped) ? tally : _tally); string text = MessageText.Render(item, tally2, includeSummary, prefix); if (ModConfig.On(ModConfig.VerboseLogging)) { ManualLogSource log = Plugin.Log; NotifyKind kind = item.Kind; log.LogInfo((object)("Event " + kind.ToString() + " -> " + text.Replace("\n", " / "))); } Telegram.Queue(text); } } public static void Forget() { _havePrev = false; _prev = default(PomodoroSnapshot); _tally = default(RunTally); } } public enum Phase { None, Study, Break, LongBreak } public struct PomodoroSnapshot { public bool Present; public Phase Phase; public bool Paused; public int Minute; public float Second; public int Session; public int SessionsPerLongBreak; public int StudyMinutes; public int BreakMinutes; public int LongBreakMinutes; public bool Running { get { if (Present) { return Phase != Phase.None; } return false; } } public int PhaseLengthMinutes => Phase switch { Phase.Study => StudyMinutes, Phase.Break => BreakMinutes, Phase.LongBreak => LongBreakMinutes, _ => 0, }; public double ElapsedSeconds => (double)Minute * 60.0 + (double)Second; public double RemainingSeconds { get { if (!Running) { return 0.0; } double num = (double)PhaseLengthMinutes * 60.0 - ElapsedSeconds; if (!(num > 0.0)) { return 0.0; } return num; } } public string RemainingClock() { int num = (int)Math.Round(RemainingSeconds); int num2 = num / 60; int num3 = num % 60; return ((num2 < 10) ? "0" : "") + num2 + ":" + ((num3 < 10) ? "0" : "") + num3; } } public struct RunTally { public int FocusBlocks; public int FocusMinutes; public bool Any => FocusBlocks > 0; } public static class Tally { public static RunTally Advance(RunTally t, PomodoroSnapshot prev, PomodoroSnapshot cur) { if (!cur.Running) { return default(RunTally); } if (!prev.Present || !cur.Present) { return t; } if (!prev.Running) { return default(RunTally); } if (prev.Phase != Phase.Study || cur.Phase == Phase.Study) { return t; } if (prev.StudyMinutes <= 0 || prev.Minute + 1 < prev.StudyMinutes) { return t; } t.FocusBlocks++; t.FocusMinutes += prev.StudyMinutes; return t; } } } namespace SleepeyDev.OnTogetherPomoNotify.Notify { public static class Secret { public static bool LooksLikeToken(string token) { if (string.IsNullOrEmpty(token)) { return false; } int num = token.IndexOf(':'); if (num < 3 || num > 16) { return false; } if (num + 20 > token.Length) { return false; } for (int i = 0; i < num; i++) { if (token[i] < '0' || token[i] > '9') { return false; } } for (int j = num + 1; j < token.Length; j++) { char c = token[j]; if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_' && c != '-') { return false; } } return true; } public static string Mask(string token) { if (string.IsNullOrEmpty(token)) { return ""; } int num = token.IndexOf(':'); if (num <= 0) { return new string('*', Math.Min(token.Length, 12)); } return token.Substring(0, num) + ":" + new string('*', 8); } public static string Redact(string text, string token) { if (string.IsNullOrEmpty(text)) { return text; } if (!string.IsNullOrEmpty(token) && token.Length >= 8) { text = text.Replace(token, ""); } return RedactBotPath(text); } private static string RedactBotPath(string text) { int num = text.IndexOf("/bot", StringComparison.Ordinal); if (num < 0) { return text; } StringBuilder stringBuilder = new StringBuilder(text.Length); int num2 = 0; while (num >= 0) { int num3 = num + "/bot".Length; int i; for (i = num3; i < text.Length && text[i] >= '0' && text[i] <= '9'; i++) { } if (i > num3 && i < text.Length && text[i] == ':') { int j; for (j = i + 1; j < text.Length && text[j] != '/' && text[j] != '?' && text[j] != ' ' && text[j] != '"'; j++) { } stringBuilder.Append(text, num2, num - num2).Append("/bot"); num2 = j; } num = text.IndexOf("/bot", num + "/bot".Length, StringComparison.Ordinal); } stringBuilder.Append(text, num2, text.Length - num2); return stringBuilder.ToString(); } } public static class Telegram { private const string Api = "https://api.telegram.org/bot"; private const float MinIntervalSeconds = 1.2f; private const int MaxQueued = 20; private const int MaxAttempts = 3; private const int TimeoutSeconds = 15; private static readonly Queue Pending = new Queue(); private static bool _busy; private static float _nextSendAt; private static bool _warnedFull; public static string Status { get; private set; } = ""; public static int Queued => Pending.Count; public static void Queue(string text) { if (string.IsNullOrEmpty(text)) { return; } if (!Configured(out var why)) { Status = why; Plugin.Log.LogWarning((object)("Not sending: " + why)); return; } if (Pending.Count >= 20) { Pending.Dequeue(); if (!_warnedFull) { _warnedFull = true; Plugin.Log.LogWarning((object)"Telegram queue is full; dropping the oldest message. Something is wrong with the connection, not with the timer."); } } Pending.Enqueue(text); } public static void Tick() { if (!_busy && Pending.Count != 0 && !(Time.unscaledTime < _nextSendAt) && !((Object)(object)ModRoot.Instance == (Object)null)) { string text = Pending.Dequeue(); _busy = true; ModRoot.Run(SendRoutine(text, null, ownsQueue: true)); } } public static void SendNow(string text, Action done) { if (!Configured(out var why)) { Status = why; done?.Invoke(arg1: false, why); } else if ((Object)(object)ModRoot.Instance == (Object)null) { done?.Invoke(arg1: false, "The mod is not running yet."); } else { ModRoot.Run(SendRoutine(text, done, ownsQueue: false)); } } public static void FindChatId(Action done) { string token = Token(); if (!Secret.LooksLikeToken(token)) { done?.Invoke(arg1: false, "Enter the bot token first."); } else if ((Object)(object)ModRoot.Instance == (Object)null) { done?.Invoke(arg1: false, "The mod is not running yet."); } else { ModRoot.Run(FindChatIdRoutine(token, done)); } } private static IEnumerator SendRoutine(string text, Action done, bool ownsQueue) { string token = Token(); string chat = ModConfig.Str(ModConfig.ChatId).Trim(); string url = "https://api.telegram.org/bot" + token + "/sendMessage"; bool ok = false; string message = ""; for (int attempt = 1; attempt <= 3; attempt++) { WWWForm val = new WWWForm(); val.AddField("chat_id", chat); val.AddField("text", text, Encoding.UTF8); val.AddField("disable_web_page_preview", "true"); UnityWebRequest req = UnityWebRequest.Post(url, val); try { req.timeout = 15; yield return req.SendWebRequest(); string body = ""; try { body = ((req.downloadHandler != null) ? req.downloadHandler.text : ""); } catch (Exception) { } if ((int)req.result == 1 && TelegramJson.IsOk(body)) { ok = true; message = "Sent."; break; } message = Describe(req, body, token); if (((int)req.result == 2 || req.responseCode >= 500 || req.responseCode == 429) && attempt != 3) { goto IL_01bf; } } finally { ((IDisposable)req)?.Dispose(); } break; IL_01bf: yield return (object)new WaitForSecondsRealtime((float)attempt * 5f); } if (ownsQueue) { _busy = false; } _nextSendAt = Time.unscaledTime + 1.2f; Status = (ok ? ("Sent " + Stamp()) : message); if (!ok) { Plugin.Log.LogWarning((object)("Telegram send failed: " + message)); } else if (ModConfig.On(ModConfig.VerboseLogging)) { Plugin.Log.LogInfo((object)"Telegram: sent."); } done?.Invoke(ok, message); } private static IEnumerator FindChatIdRoutine(string token, Action done) { string text = "https://api.telegram.org/bot" + token + "/getUpdates?limit=10&timeout=0"; UnityWebRequest req = UnityWebRequest.Get(text); try { req.timeout = 15; yield return req.SendWebRequest(); string body = ((req.downloadHandler != null) ? req.downloadHandler.text : ""); if ((int)req.result != 1 || !TelegramJson.IsOk(body)) { string arg = (Status = Describe(req, body, token)); done?.Invoke(arg1: false, arg); yield break; } string text3 = TelegramJson.LastChatId(body); if (string.IsNullOrEmpty(text3)) { Status = "No messages yet. Write anything to your bot in Telegram, then press this again."; done?.Invoke(arg1: false, "No messages yet. Write anything to your bot in Telegram, then press this again."); yield break; } ModConfig.ChatId.Value = text3; Status = "Chat id found: " + text3; Plugin.Log.LogInfo((object)"Chat id resolved from getUpdates."); done?.Invoke(arg1: true, text3); } finally { ((IDisposable)req)?.Dispose(); } } private static string Token() { return ModConfig.Str(ModConfig.BotToken).Trim(); } public static bool Configured(out string why) { if (!ModConfig.On(ModConfig.Enabled)) { why = "Notifications are switched off."; return false; } if (!Secret.LooksLikeToken(Token())) { why = "No bot token, or it does not look like one."; return false; } if (ModConfig.Str(ModConfig.ChatId).Trim().Length == 0) { why = "No chat id yet."; return false; } why = ""; return true; } private static string Describe(UnityWebRequest req, string body, string token) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Invalid comparison between Unknown and I4 string text = TelegramJson.Description(body); if (!string.IsNullOrEmpty(text)) { return "Telegram says: " + Secret.Redact(text, token) + ((req.responseCode > 0) ? (" (" + req.responseCode + ")") : ""); } if ((int)req.result == 2) { return "Could not reach Telegram: " + Secret.Redact(req.error ?? "no connection", token); } return "Telegram refused the message" + ((req.responseCode > 0) ? (" (" + req.responseCode + ")") : "") + (string.IsNullOrEmpty(req.error) ? "" : (": " + Secret.Redact(req.error, token))); } private static string Stamp() { return DateTime.Now.ToString("HH:mm:ss"); } public static void Clear() { Pending.Clear(); _warnedFull = false; } } public static class TelegramJson { public static string Description(string body) { return StringField(body, "\"description\""); } public static bool IsOk(string body) { if (string.IsNullOrEmpty(body)) { return false; } int num = body.IndexOf("\"ok\"", StringComparison.Ordinal); if (num < 0) { return false; } num = SkipToValue(body, num + 4); if (num >= 0 && num + 4 <= body.Length) { return body.Substring(num, 4) == "true"; } return false; } public static string LastChatId(string body) { if (string.IsNullOrEmpty(body)) { return null; } string result = null; int startIndex = 0; while (true) { startIndex = body.IndexOf("\"chat\":", startIndex, StringComparison.Ordinal); if (startIndex < 0) { break; } int num = SkipWhitespace(body, startIndex + "\"chat\":".Length); if (num >= 0 && num < body.Length && body[num] == '{') { int num2 = body.IndexOf("\"id\"", num, StringComparison.Ordinal); if (num2 >= 0) { string text = NumberField(body, num2); if (text != null) { result = text; } } } startIndex += "\"chat\":".Length; } return result; } private static string StringField(string body, string key) { if (string.IsNullOrEmpty(body)) { return null; } int num = body.IndexOf(key, StringComparison.Ordinal); if (num < 0) { return null; } num = SkipToValue(body, num + key.Length); if (num < 0 || num >= body.Length || body[num] != '"') { return null; } num++; StringBuilder stringBuilder = new StringBuilder(); for (; num < body.Length && body[num] != '"'; num++) { if (body[num] == '\\' && num + 1 < body.Length) { num++; } stringBuilder.Append(body[num]); } return stringBuilder.ToString(); } private static string NumberField(string body, int keyAt) { int i = SkipToValue(body, keyAt + 4); if (i < 0) { return null; } int num = i; if (i < body.Length && body[i] == '-') { i++; } for (; i < body.Length && body[i] >= '0' && body[i] <= '9'; i++) { } if (i <= num || (i == num + 1 && body[num] == '-')) { return null; } return body.Substring(num, i - num); } private static int SkipToValue(string body, int at) { at = SkipWhitespace(body, at); if (at < 0 || at >= body.Length || body[at] != ':') { return -1; } return SkipWhitespace(body, at + 1); } private static int SkipWhitespace(string body, int at) { while (at < body.Length && (body[at] == ' ' || body[at] == '\t' || body[at] == '\r' || body[at] == '\n')) { at++; } return at; } } }