// check if we're authorized and if we have a calendar id, and prompt the user to set up either if needed // returns true if we're authorized and have a calendar id, returns false if either checks are false public CalendarSyncStatus CheckIfSyncPossible(DiscordServer server) { // check if we have credentials for google apiitem if (server.GoogleUserCredential == null) { return(CalendarSyncStatus.NullCredentials); } if (server.CalendarId == "") { return(CalendarSyncStatus.NullCalendarId); } if (server.CalendarId == null) { return(CalendarSyncStatus.EmptyCalendarId); } // if server object is assigned, the bot is connected, but the bot is not connected to this server, we're probably kicked if (server.DiscordServerObject != null && server.DiscordServerObject.Available && ((SocketGuild)server.DiscordServerObject).IsConnected == false) { // DEBUG Task.Run((async() => { Logger.Log(LogLevel.Debug, $"DEBUG - Name: {server.DiscordServerObject.Name} - Available: {server.DiscordServerObject.Available} " + $"Connected: {((SocketGuild)server.DiscordServerObject).IsConnected} - WE SHOULD NOT SEE THIS. THIS SHOULD BE HANDLED AT THE START OF A TIMER TICK."); })); return(CalendarSyncStatus.ServerUnavailable); } return(CalendarSyncStatus.OK); }
// log in to all servers public async Task Login(DiscordServer server) { var credentialPath = $@"{_credentialPathPrefix}/{server.ServerId}"; using (var stream = new FileStream(_filePath, FileMode.Open, FileAccess.Read)) { // build code flow manager to authenticate token var flowManager = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer { ClientSecrets = GoogleClientSecrets.Load(stream).Secrets, Scopes = _scopes, DataStore = new FileDataStore(credentialPath, true) }); var fileDataStore = new FileDataStore(credentialPath, true); var token = await fileDataStore.GetAsync <TokenResponse>("token"); // load token from file // var token = await flowManager.LoadTokenAsync(_userId, CancellationToken.None).ConfigureAwait(false); // check if we need to get a new token if (flowManager.ShouldForceTokenRetrieval() || token == null || token.RefreshToken == null && token.IsExpired(flowManager.Clock)) { return; } // set credentials to use for syncing server.GoogleUserCredential = new UserCredential(flowManager, _userId, token); } }
// send or modify embed messages listing upcoming events from the raid calendar public async Task SendEvents(DiscordServer server) { // build embed var embed = BuildEventsEmbed(server); // check if we haven't set an embed message yet if (server.EventEmbedMessage == null) { // try to get a pre-existing event embed var oldEmbedMessage = await GetPreviousEmbed(server); // if we found a pre-existing event embed, set it as our current event embed message // and edit it if (oldEmbedMessage != null) { server.EventEmbedMessage = oldEmbedMessage; await server.EventEmbedMessage.ModifyAsync(m => { m.Embed = embed; }); } // otherwise, send a new one and set it as our current event embed message else { // send embed var message = await server.ReminderChannel.SendMessageAsync(null, false, embed); // store message id server.EventEmbedMessage = message; } } // if we have set a current event embed message, edit it else { await server.EventEmbedMessage.ModifyAsync(m => { m.Embed = embed; }); } }
// searches the _reminderChannel for a message from the bot containing the passed param // (this should be the title of an event for which we are looking for a remindermessage to edit) // if it finds one, return that message to the calling method to be modified private async Task <IUserMessage> GetPreviousReminderMessage(DiscordServer server, string messageContains) { // get all messages in reminder channel var messages = await server.ReminderChannel.GetMessagesAsync().FlattenAsync(); // try to get a pre-existing message matching messageContains (so {eventtitle}) //return the results or null var reminderMsg = messages.Where(msg => msg.Author.Id == _discord.CurrentUser.Id).FirstOrDefault(msg => msg.Content.Contains(messageContains)); return((IUserMessage)reminderMsg); }
// called whenever .sync command is used, and at first program launch public async Task <bool> ManualSync(DiscordServer server = null, SocketCommandContext context = null) { // if server is null, context is not null - we're calling via command, so get the right server via context if (server == null && context != null) { server = Servers.ServerList.Find(x => x.DiscordServerObject == context.Guild); } // check if we're authenticated and have a calendar id to sync from var syncStatus = CheckIfSyncPossible(server); if (syncStatus != CalendarSyncStatus.OK) { if (server == null && context != null) { await context.Channel.SendMessageAsync($"Sync failed: {SyncFailedReason(syncStatus)}"); } else { await server.ConfigChannel.SendMessageAsync($"Sync failed: {SyncFailedReason(syncStatus)}"); } return(false); } // perform the actual sync var success = SyncFromGoogleCalendar(server); // handle sync success or failure if (success) { // send message reporting we've synced calendar events string resultMessage = $":calendar: Synced {server.Events.Count} calendar events."; if (context != null) // we only want to send a message announcing sync success if the user sent the command { await _interactiveService.ReplyAndDeleteAsync(context, resultMessage); } } else { // send message reporting there were no calendar events to sync string resultMessage = ":calendar: No events found in calendar."; if (context != null) { await _interactiveService.ReplyAndDeleteAsync(context, resultMessage); } } // send/modify events embed in reminders to reflect newly synced values await _scheduleService.SendEvents(server); return(true); }
// searches the _reminderChannel for a message from the bot containing an embed (how else can we filter this - title?) // if it finds one, return that message to the calling method to be set as _eventEmbedMessage private async Task <IUserMessage> GetPreviousEmbed(DiscordServer server) { // get all messages in reminder channel var messages = await server.ReminderChannel.GetMessagesAsync().FlattenAsync(); // try to get a pre-existing embed message matching our usual event embed parameters // return the results try { var embedMsg = messages.Where(msg => msg.Author.Id == _discord.CurrentUser.Id) .Where(msg => msg.Embeds.Count > 0) .Where(msg => msg.Embeds.First().Title == "Schedule").ToList().First(); return((IUserMessage)embedMsg); } catch { return(null); } }
// logic for pulling data from api and adding it to CalendarEvents list, returns bool representing // if calendar had events or not public bool SyncFromGoogleCalendar(DiscordServer server) { // Set the timespan of events to sync var min = TimezoneAdjustedDateTime.Now.Invoke(); var max = TimezoneAdjustedDateTime.Now.Invoke().AddMonths(1); // pull events from the specified google calendar // string is the calendar id of the calendar to sync with var events = GetCalendarEvents(server.GoogleUserCredential, server.CalendarId, min, max); // declare events to use for list comparisons List <CalendarEvent> oldEventsList = new List <CalendarEvent>(); List <CalendarEvent> newEventsList = new List <CalendarEvent>(); oldEventsList.AddRange(server.Events); server.Events.Clear(); // if there are events, iterate through and add them to our calendarevents list if (events.Any()) { // build a list of the events we pulled from gcal foreach (var eventItem in events) { // api wrapper will always pull times in local time aka eastern because it sucks // so just subtract 3 hours to get pacific time eventItem.Start.DateTime = eventItem.Start.DateTime - TimeSpan.FromHours(7); eventItem.End.DateTime = eventItem.End.DateTime - TimeSpan.FromHours(7); // don't add items from the past if (eventItem.End.DateTime < TimezoneAdjustedDateTime.Now.Invoke()) { continue; } DateTime startDate; DateTime endDate; if (eventItem.Start.DateTime.HasValue == false || eventItem.End.DateTime.HasValue == false) { startDate = DateTime.Parse(eventItem.Start.Date); endDate = DateTime.Parse(eventItem.End.Date); } else { startDate = eventItem.Start.DateTime.Value; endDate = eventItem.End.DateTime.Value; } // build calendar event to be added to our list var calendarEvent = new CalendarEvent() { Name = eventItem.Summary, StartDate = startDate, EndDate = endDate, Timezone = "PST", UniqueId = eventItem.Id }; newEventsList.Add(calendarEvent); } // build our working list of calendarevents, mixing old event items (if any) and new ones if (oldEventsList.Count == 0) { // if calendarevents list (and thus oldeventslist) is empty, we're running for the first time // so just add newEventsList to calendarevents and be done server.Events.AddRange(newEventsList); } else { // match events we just pulled from google to events we have stored already, by start date // store new name (this doesn't matter), start and endgames from new list into CalendarEvents // keep existing alert flags var oldEventsDict = oldEventsList.ToDictionary(n => n.UniqueId); foreach (var n in newEventsList) { CalendarEvent o; if (oldEventsDict.TryGetValue(n.UniqueId, out o)) { var calendarEvent = new CalendarEvent(); calendarEvent.Name = n.Name; calendarEvent.Timezone = o.Timezone; calendarEvent.AlertMessage = o.AlertMessage; calendarEvent.UniqueId = o.UniqueId; // if this event's been manually adjusted, keep the old values if (o.ManuallyAdjusted) { calendarEvent.StartDate = o.StartDate; calendarEvent.EndDate = o.EndDate; } else // else accept the new values { calendarEvent.StartDate = n.StartDate; calendarEvent.EndDate = n.EndDate; } server.Events.Add(calendarEvent); } else { server.Events.Add(n); } } } return(true); // calendar had events, and we added them } return(false); // calendar did not have events }
// send or modify messages alerting the user that an event will be starting soon public async Task HandleReminders(DiscordServer server) { var firstCalendarEvent = server.Events[0]; // look for any existing reminder messages in the reminders channel that contain the first event's name // if one exists, set that message as the first event's alert message // only set the first one so repeated event names don't all get assigned this message var oldReminderMessage = await GetPreviousReminderMessage(server, firstCalendarEvent.Name); if (oldReminderMessage != null && firstCalendarEvent.AlertMessage == null) { firstCalendarEvent.AlertMessage = oldReminderMessage; } foreach (var calendarEvent in server.Events) { // get amount of time between the calendarevent start time and the current time // and round it to the nearest 5m interval, so usually on the 5m interval var timeStartDelta = RoundToNearestMinutes(calendarEvent.StartDate - TimezoneAdjustedDateTime.Now.Invoke(), 5); // if it's less than an hour but more than fifteen minutes, and we haven't sent an alert message, send an alert message if (timeStartDelta.TotalHours < 1 && timeStartDelta.TotalMinutes > 15) { var messageContents = $"{calendarEvent.Name} is starting in {(int)timeStartDelta.TotalMinutes} minutes."; // if there's an alert message already, edit it if (calendarEvent.AlertMessage != null) { await calendarEvent.AlertMessage.ModifyAsync(m => m.Content = messageContents); Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - An event is between 15m and 1h from now and we did have an alert message, editing it."); } // if there wasn't an alert message, send a new message else { var msg = await server.ReminderChannel.SendMessageAsync(messageContents); calendarEvent.AlertMessage = msg; Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - An event is between 15m and 1h from now and we did not have an alert message, sending one."); } } // if it's less than an hour and less or equal to fifteen minutes, try to modify an existing alert message or send a new one if (timeStartDelta.TotalHours < 1 && timeStartDelta.TotalMinutes <= 15) { var messageContents = $"{calendarEvent.Name} is starting shortly. Look for a party finder soon."; // if there's an alert message already, edit it if (calendarEvent.AlertMessage != null) { await calendarEvent.AlertMessage.ModifyAsync(m => m.Content = messageContents); Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event is less than 15m from now and we did have an alert message, editing it."); } // if there wasn't an alert message, send a new message else { var msg = await server.ReminderChannel.SendMessageAsync(messageContents); calendarEvent.AlertMessage = msg; Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event is less than 15m from now and we did not have an alert message, sending one."); } } // if the event is currently active (after start date but before end date) // update the alert message to reflect how much time is left until the event is over if (calendarEvent.StartDate < TimezoneAdjustedDateTime.Now.Invoke() && calendarEvent.EndDate > TimezoneAdjustedDateTime.Now.Invoke()) { // get amount of time between the current time and the calendarevent end time var timeEndDelta = RoundToNearestMinutes(calendarEvent.EndDate - TimezoneAdjustedDateTime.Now.Invoke(), 5); var messageContents = $"{calendarEvent.Name} is underway, ending in" + GetTimeDeltaFormatting(timeEndDelta) + "."; // if there's an alert message already, edit it if (calendarEvent.AlertMessage != null) { await calendarEvent.AlertMessage.ModifyAsync(m => m.Content = messageContents); Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event is underway and we had an alert message, editing it."); } // if there wasn't an alert message, send a new message else { var msg = await server.ReminderChannel.SendMessageAsync(messageContents); calendarEvent.AlertMessage = msg; Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event is underway and we did not have an alert message, sending one."); } } // if the event is almost past, delete the alertmessage if (calendarEvent.EndDate < TimezoneAdjustedDateTime.Now.Invoke() + TimeSpan.FromMinutes(5)) { await calendarEvent.AlertMessage.DeleteAsync(); calendarEvent.AlertMessage = null; Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event end date is less than 5 mins from now, deleting alert message."); } // if the event is over an hour from now and an alert message exists, delete it. if (calendarEvent.StartDate > TimezoneAdjustedDateTime.Now.Invoke() + TimeSpan.FromMinutes(60) && calendarEvent.AlertMessage != null) { // await calendarEvent.AlertMessage.DeleteAsync(); calendarEvent.AlertMessage = null; Logger.Log(LogLevel.Debug, $"DEBUG - {server.ServerName} - The event start date is over an hour away, we would have deleted the alert message."); } if (calendarEvent.AlertMessage != null) { Logger.Log(LogLevel.Debug, $"DEBUG - msg ID: {calendarEvent.AlertMessage.Id} - edited: {calendarEvent.AlertMessage.EditedTimestamp} - contents: {calendarEvent.AlertMessage.Content}"); } } }
// put together the events embed & return it to calling method private Embed BuildEventsEmbed(DiscordServer server) { EmbedBuilder embedBuilder = new EmbedBuilder(); // if there are no items in CalendarEvents, build a field stating so if (server.Events.Count == 0) { embedBuilder.AddField("No raids scheduled.", _textMemeService.GetMemeTextForNoEvents()); } // iterate through each calendar event and build strings from them // if there are no events, the foreach loop is skipped, so no need to check foreach (var calendarEvent in server.Events) { // don't add items from the past if (calendarEvent.EndDate < TimezoneAdjustedDateTime.Now.Invoke()) { continue; } // get the time difference between the event and now // roundtonearestminutes wrapper will round it to closest 5m interval TimeSpan timeDelta; // holy f*****g formatting batman StringBuilder stringBuilder = new StringBuilder(); // if event hasn't started yet if (calendarEvent.StartDate > TimezoneAdjustedDateTime.Now.Invoke()) { stringBuilder.AppendLine($"Starts on {calendarEvent.StartDate,0:M/dd} at {calendarEvent.StartDate,0: h:mm tt} {calendarEvent.Timezone} and ends at {calendarEvent.EndDate,0: h:mm tt} {calendarEvent.Timezone}"); stringBuilder.Append(":watch: Starts in"); timeDelta = RoundToNearestMinutes(calendarEvent.StartDate - TimezoneAdjustedDateTime.Now.Invoke(), 5); } // if event has started but hasn't finished else if (calendarEvent.StartDate < TimezoneAdjustedDateTime.Now.Invoke() && calendarEvent.EndDate > TimezoneAdjustedDateTime.Now.Invoke()) { stringBuilder.AppendLine($"Currently underway, ending at {calendarEvent.EndDate,0: h:mm tt} {calendarEvent.Timezone}"); stringBuilder.Append(":watch: Ends in"); timeDelta = RoundToNearestMinutes(calendarEvent.EndDate - TimezoneAdjustedDateTime.Now.Invoke(), 5); } // get formatting for timedelta stringBuilder.Append(GetTimeDeltaFormatting(timeDelta)); stringBuilder.Append("."); // bundle it all together into a line for the embed embedBuilder.AddField($"{calendarEvent.Name}", stringBuilder.ToString()); } // add the extra little embed bits embedBuilder.WithTitle("Schedule") .WithColor(Color.Blue) .WithFooter("Synced: ") // set the actual datetime value since discord timestamps // are timezone-aware (?) .WithTimestamp(DateTime.Now); // roll it all up and send it to the channel var embed = embedBuilder.Build(); return(embed); }
// converts stored IDs from database into ulongs (mongo can't store ulong ha ha) and use them to // assign our discord objects public void SetServerDiscordObjects(DiscordServer server) { server.DiscordServerObject = _discord.GetGuild(Convert.ToUInt64(server.ServerId)); server.ConfigChannel = _discord.GetChannel(Convert.ToUInt64(server.ConfigChannelId)) as ITextChannel; server.ReminderChannel = _discord.GetChannel(Convert.ToUInt64(server.ReminderChannelId)) as ITextChannel; }