diff --git a/Apps.App/Actions/Actions.cs b/Apps.App/Actions/Actions.cs deleted file mode 100644 index 5159e7e..0000000 --- a/Apps.App/Actions/Actions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Apps.App.Invocables; -using Blackbird.Applications.Sdk.Common.Actions; -using Blackbird.Applications.Sdk.Common.Invocation; - -namespace Apps.App.Actions; - -[ActionList] -public class Actions : AppInvocable -{ - public Actions(InvocationContext invocationContext) : base(invocationContext) - { - } -} \ No newline at end of file diff --git a/Apps.App/Api/AppClient.cs b/Apps.App/Api/AppClient.cs deleted file mode 100644 index f61ca9b..0000000 --- a/Apps.App/Api/AppClient.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Apps.App.Api; - -public class AppClient -{ - -} \ No newline at end of file diff --git a/Apps.App/Application.cs b/Apps.App/Application.cs deleted file mode 100644 index 715d107..0000000 --- a/Apps.App/Application.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Blackbird.Applications.Sdk.Common; - -namespace Apps.App; - -public class Application : IApplication -{ - public string Name - { - get => "App"; - set { } - } - - public T GetInstance() - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/AccessTokenProvider.cs b/Apps.Microsoft365Calendar/AccessTokenProvider.cs new file mode 100644 index 0000000..1dc9419 --- /dev/null +++ b/Apps.Microsoft365Calendar/AccessTokenProvider.cs @@ -0,0 +1,21 @@ +using Microsoft.Kiota.Abstractions.Authentication; + +namespace Apps.Microsoft365Calendar; + +public class AccessTokenProvider : IAccessTokenProvider +{ + public string Token { get; set; } + + public AccessTokenProvider(string token) + { + Token = token; + } + + public AllowedHostsValidator AllowedHostsValidator => throw new NotImplementedException(); + + public Task GetAuthorizationTokenAsync(Uri uri, Dictionary? additionalAuthenticationContext = null, + CancellationToken cancellationToken = default) + { + return Task.FromResult(Token); + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Actions/CalendarActions.cs b/Apps.Microsoft365Calendar/Actions/CalendarActions.cs new file mode 100644 index 0000000..469978e --- /dev/null +++ b/Apps.Microsoft365Calendar/Actions/CalendarActions.cs @@ -0,0 +1,162 @@ +using Apps.Microsoft365Calendar.Invocables; +using Apps.Microsoft365Calendar.Models.Dtos; +using Apps.MicrosoftOutlook.Models.Calendar.Requests; +using Apps.MicrosoftOutlook.Models.Calendar.Responses; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Actions; +using Blackbird.Applications.Sdk.Common.Authentication; +using Blackbird.Applications.Sdk.Common.Invocation; +using Microsoft.Graph.Models; +using Microsoft.Graph.Models.ODataErrors; + +namespace Apps.Microsoft365Calendar.Actions; + +[ActionList] +public class CalendarActions(InvocationContext invocationContext) : AppInvocable(invocationContext) +{ + #region GET + + [Action("List calendars", Description = "List current user's calendars.")] + public async Task ListCalendars(IEnumerable authenticationCredentialsProviders) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + var calendars = await client.Me.Calendars.GetAsync(); + return new ListCalendarsResponse + { + Calendars = calendars.Value.Select(c => new CalendarDto(c)) + }; + } + + [Action("Get calendar", Description = "Get a calendar. If calendar is not specified, default calendar is returned.")] + public async Task GetCalendar(IEnumerable authenticationCredentialsProviders, + [ActionParameter] GetCalendarRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + Microsoft.Graph.Models.Calendar? calendar; + if (request == null || request.CalendarId == null) + calendar = await client.Me.Calendar.GetAsync(); + else + { + try + { + calendar = await client.Me.Calendars[request.CalendarId].GetAsync(); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + var calendarDto = new CalendarDto(calendar); + return calendarDto; + } + + #endregion + + #region POST + + [Action("Create calendar", Description = "Create a new calendar.")] + public async Task CreateCalendar(IEnumerable authenticationCredentialsProviders, + [ActionParameter] CreateCalendarRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + var requestBody = new Microsoft.Graph.Models.Calendar + { + Name = request.CalendarName + }; + try + { + var createdCalendar = await client.Me.Calendars.PostAsync(requestBody); + var createdCalendarDto = new CalendarDto(createdCalendar); + return createdCalendarDto; + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + [Action("Get users' schedule information", Description = "Get the free/busy availability information for " + + "a collection of users in specified time period.")] + public async Task GetSchedule(IEnumerable authenticationCredentialsProviders, + [ActionParameter] GetScheduleRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + var requestBody = new Microsoft.Graph.Me.Calendar.GetSchedule.GetSchedulePostRequestBody + { + Schedules = request.Emails, + StartTime = new DateTimeTimeZone + { + DateTime = request.StartDateTime.ToString("yyyy-MM-ddTHH:mm:ss"), + TimeZone = TimeZoneInfo.Local.Id + }, + EndTime = new DateTimeTimeZone + { + DateTime = request.EndDateTime.ToString("yyyy-MM-ddTHH:mm:ss"), + TimeZone = TimeZoneInfo.Local.Id + } + }; + try + { + var schedules = await client.Me.Calendar.GetSchedule.PostAsync(requestBody); + var schedulesDto = schedules.Value.Select(s => new ScheduleDto(s)); + return new GetScheduleResponse { Schedules = schedulesDto }; + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + #endregion + + #region PATCH + + [Action("Rename calendar", Description = "Rename a calendar. If calendar is not specified, default " + + "calendar is renamed.")] + public async Task RenameCalendar(IEnumerable authenticationCredentialsProviders, + [ActionParameter] RenameCalendarRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + Microsoft.Graph.Models.Calendar? renamedCalendar; + var requestBody = new Microsoft.Graph.Models.Calendar + { + Name = request.CalendarName + }; + try + { + if (request.CalendarId == null) + renamedCalendar = await client.Me.Calendar.PatchAsync(requestBody); + else + renamedCalendar = await client.Me.Calendars[request.CalendarId].PatchAsync(requestBody); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + + var renamedCalendarDto = new CalendarDto(renamedCalendar); + return renamedCalendarDto; + } + + #endregion + + #region DELETE + + [Action("Delete calendar", Description = "Delete calendar other than the default calendar.")] + public async Task DeleteCalendar(IEnumerable authenticationCredentialsProviders, + [ActionParameter] DeleteCalendarRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + try + { + await client.Me.Calendars[request.CalendarId].DeleteAsync(); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + #endregion +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Actions/EventActions.cs b/Apps.Microsoft365Calendar/Actions/EventActions.cs new file mode 100644 index 0000000..f4bb5df --- /dev/null +++ b/Apps.Microsoft365Calendar/Actions/EventActions.cs @@ -0,0 +1,456 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Apps.Microsoft365Calendar.Models.Dtos; +using Apps.MicrosoftOutlook.Models.Event.Requests; +using Apps.MicrosoftOutlook.Models.Event.Responses; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Actions; +using Blackbird.Applications.Sdk.Common.Authentication; +using Blackbird.Applications.Sdk.Common.Dynamic; +using HtmlAgilityPack; +using Microsoft.Graph.Models; +using Microsoft.Graph.Models.ODataErrors; +using Microsoft.Kiota.Abstractions; + +namespace Apps.Microsoft365Calendar.Actions; + +public class EventActions +{ + private const string EventBodyContentId = "EventBodyContentId"; + + #region GET + + [Action("List events", Description = "Retrieve a list of events in a calendar. If calendar is not " + + "specified, default calendar's events are listed.")] + public async Task ListEvents(IEnumerable authenticationCredentialsProviders, + [ActionParameter] ListEventsRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + EventCollectionResponse? events; + try + { + if (request == null || request.CalendarId == null) + events = await client.Me.Calendar.Events.GetAsync(); + else + events = await client.Me.Calendars[request.CalendarId].Events.GetAsync(); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + return new ListEventsResponse + { + Events = events.Value.Select(e => new EventDto(e)) + }; + } + + [Action("Get event", Description = "Get information about an event.")] + public async Task GetEvent(IEnumerable authenticationCredentialsProviders, + [ActionParameter] GetEventRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + try + { + var eventData = await client.Me.Events[request.EventId].GetAsync(); + var eventDto = new EventDto(eventData); + return eventDto; + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + [Action("List occurrences of event", Description = "Get the occurrences of an event for a specified time range.")] + public async Task ListEventOccurrences(IEnumerable authenticationCredentialsProviders, + [ActionParameter] ListEventOccurrencesRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + try + { + var events = await client.Me.Events[request.EventId].Instances.GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.StartDateTime = request.StartDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss"); + requestConfiguration.QueryParameters.EndDateTime = request.EndDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss"); + }); + return new ListEventsResponse + { + Events = events.Value.Select(e => new EventDto(e)) + }; + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + [Action("List recently created events", Description = "Retrieve a list of events created during past hours. " + + "If number of hours is not specified, events created " + + "during past 24 hours are listed. If calendar is not " + + "specified, default calendar's events are listed.")] + public async Task ListRecentlyCreatedEvents(IEnumerable authenticationCredentialsProviders, + [ActionParameter] ListRecentlyCreatedEventsRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + EventCollectionResponse? events; + var startDateTime = (DateTime.Now - TimeSpan.FromHours(request.Hours ?? 24)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"); + var requestFilter = $"createdDateTime ge {startDateTime}"; + try + { + if (request == null || request.CalendarId == null) + events = await client.Me.Calendar.Events.GetAsync(requestConfiguration => + requestConfiguration.QueryParameters.Filter = requestFilter); + else + events = await client.Me.Calendars[request.CalendarId].Events.GetAsync(requestConfiguration => + requestConfiguration.QueryParameters.Filter = requestFilter); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + return new ListEventsResponse + { + Events = events.Value.Select(e => new EventDto(e)) + }; + } + + [Action("List recently updated events", Description = "Retrieve a list of events updated during past hours. " + + "If number of hours is not specified, events updated " + + "during past 24 hours are listed. If calendar is not " + + "specified, default calendar's events are listed.")] + public async Task ListRecentlyUpdatedEvents(IEnumerable authenticationCredentialsProviders, + [ActionParameter] ListRecentlyUpdatedEventsRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + EventCollectionResponse? events; + var startDateTime = (DateTime.Now - TimeSpan.FromHours(request.Hours ?? 24)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"); + var requestFilter = $"lastModifiedDateTime ge {startDateTime}"; + try + { + if (request == null || request.CalendarId == null) + events = await client.Me.Calendar.Events.GetAsync(requestConfiguration => + requestConfiguration.QueryParameters.Filter = requestFilter); + else + events = await client.Me.Calendars[request.CalendarId].Events.GetAsync(requestConfiguration => + requestConfiguration.QueryParameters.Filter = requestFilter); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + return new ListEventsResponse + { + Events = events.Value.Select(e => new EventDto(e)) + }; + } + + #endregion + + #region POST + + [Action("Create event in a calendar", Description = "Create a new event in a calendar. If calendar is not " + + "specified, the event is created in the default calendar. " + + "If the event is an online meeting, a Microsoft Teams " + + "meeting is automatically created. To create a recurring " + + "event specify recurrence pattern and interval which " + + "can be in days, weeks or months, depending on recurrence " + + "pattern type. If interval is not specified, it is " + + "set to 1. For weekly or monthly patterns provide days " + + "of week on which the event occurs.")] + public async Task CreateEvent(IEnumerable authenticationCredentialsProviders, + [ActionParameter] CreateEventRequest request) + { + var daysOfWeek = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "sunday", DayOfWeekObject.Sunday }, + { "monday", DayOfWeekObject.Monday }, + { "tuesday", DayOfWeekObject.Tuesday }, + { "wednesday", DayOfWeekObject.Wednesday }, + { "thursday", DayOfWeekObject.Thursday }, + { "friday", DayOfWeekObject.Friday }, + { "saturday", DayOfWeekObject.Saturday } + }; + + if (!IsValidTimeFormat(request.StartTime, out TimeSpan startTime) + || !IsValidTimeFormat(request.EndTime, out TimeSpan endTime)) + throw new ArgumentException("Time format is not valid."); + + if (request.RecurrencePattern != null) + { + if (request.Interval < 1) + throw new ArgumentException("Recurrence interval must be greater than zero."); + + if (request.RecurrencePattern != "Daily" && (request.DaysOfWeek == null || !request.DaysOfWeek.Any())) + throw new ArgumentException("For weekly and monthly recurrence patterns days of week should be specified."); + + if (request.RecurrencePattern != "Daily") + { + foreach (var day in request.DaysOfWeek) + { + var isValidDayOfWeek = daysOfWeek.Keys.Any(d => d == day.ToLower()); + if (!isValidDayOfWeek) + throw new ArgumentException($"Day of week '{day}' is not valid."); + } + } + else + request.DaysOfWeek = Array.Empty(); + } + + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + var requestBody = new Event + { + Subject = request.Subject, + Body = new ItemBody + { + ContentType = BodyType.Html, + Content = WrapEventBodyContent(request.BodyContent) + }, + Start = new DateTimeTimeZone + { + DateTime = request.EventDate.ToString("yyyy-MM-dd") + $"T{startTime}", + TimeZone = TimeZoneInfo.Local.Id + }, + End = new DateTimeTimeZone + { + DateTime = request.EventDate.ToString("yyyy-MM-dd") + $"T{endTime}", + TimeZone = TimeZoneInfo.Local.Id + }, + Location = new Location + { + DisplayName = request.Location ?? (request.IsOnlineMeeting ? "Microsoft Teams Meeting" : "No location specified") + }, + IsOnlineMeeting = request.IsOnlineMeeting, + IsReminderOn = request.IsReminderOn, + ReminderMinutesBeforeStart = request.ReminderMinutesBeforeStart ?? (request.IsReminderOn ? 15 : -1), + Attendees = new List(request.AttendeeEmails.Select(email => new Attendee + { + EmailAddress = new EmailAddress { Address = email }, + Type = AttendeeType.Optional + })), + Recurrence = request.RecurrencePattern == null ? null : new PatternedRecurrence + { + Pattern = new RecurrencePattern + { + Type = (RecurrencePatternType)Enum.Parse(typeof(RecurrencePatternType), request.RecurrencePattern), + DaysOfWeek = new List(request.DaysOfWeek.Select(d => daysOfWeek[d] as DayOfWeekObject?)), + Interval = request.Interval ?? 1 + }, + Range = new RecurrenceRange + { + Type = request.RecurrenceEndDate == null ? RecurrenceRangeType.NoEnd : RecurrenceRangeType.EndDate, + StartDate = new Date(request.EventDate.Year, request.EventDate.Month, request.EventDate.Day), + EndDate = request.RecurrenceEndDate == null ? + new Date(DateTime.MinValue.Year, DateTime.MinValue.Month, DateTime.MinValue.Day) + : new Date(request.RecurrenceEndDate.Value.Year, request.RecurrenceEndDate.Value.Month, request.RecurrenceEndDate.Value.Day) + + } + } + }; + + Event? createdEvent; + try + { + if (request.CalendarId == null) + createdEvent = await client.Me.Calendar.Events.PostAsync(requestBody); + else + createdEvent = await client.Me.Calendars[request.CalendarId].Events.PostAsync(requestBody); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + + var createdEventDto = new EventDto(createdEvent); + return createdEventDto; + } + + [Action("Cancel event", Description = "This action allows the organizer of a meeting to send a cancellation " + + "message and cancel the event.")] + public async Task CancelEvent(IEnumerable authenticationCredentialsProviders, + [ActionParameter] [Display("Event")] [DataSource(typeof(EventDataSourceHandler))] string eventId, + [ActionParameter] CancelEventRequest request) + { + await CancelEventOrEventOccurrence(authenticationCredentialsProviders, eventId, request); + } + + [Action("Cancel event occurrence", Description = "This action allows the organizer of a meeting to send " + + "a cancellation message and cancel an occurrence of a " + + "recurring meeting.")] + public async Task CancelEventOccurrence(IEnumerable authenticationCredentialsProviders, + [ActionParameter] [Display("Event occurrence")] [DataSource(typeof(EventOccurrenceDataSourceHandler))] string eventOccurrenceId, + [ActionParameter] CancelEventRequest request) + { + await CancelEventOrEventOccurrence(authenticationCredentialsProviders, eventOccurrenceId, request); + } + + [Action("Forward event", Description = "This action allows the organizer or attendee of a meeting event " + + "to forward the meeting request to a new recipient.")] + public async Task ForwardEvent(IEnumerable authenticationCredentialsProviders, + [ActionParameter] ForwardEventRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + var requestBody = new Microsoft.Graph.Me.Events.Item.Forward.ForwardPostRequestBody + { + ToRecipients = new List + { + new Recipient + { + EmailAddress = new EmailAddress + { + Address = request.RecipientEmail, + Name = request.RecipientName ?? "" + } + } + }, + Comment = request.Comment ?? "" + }; + try + { + await client.Me.Events[request.EventId].Forward.PostAsync(requestBody); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + #endregion + + #region PATCH + + [Action("Update event", Description = "Update an existing event. Specify fields that need to be updated.")] + public async Task UpdateEvent(IEnumerable authenticationCredentialsProviders, + [ActionParameter] [Display("Event")] [DataSource(typeof(EventDataSourceHandler))] string eventId, + [ActionParameter] UpdateEventRequest request) + { + return await UpdateEventOrEventOccurrence(authenticationCredentialsProviders, eventId, request); + } + + [Action("Update event occurrence", Description = "Update an existing occurrence of a recurring event. " + + "Specify fields that need to be updated.")] + public async Task UpdateEventOccurrence(IEnumerable authenticationCredentialsProviders, + [ActionParameter] [Display("Event occurrence")] [DataSource(typeof(EventOccurrenceDataSourceHandler))] string eventOccurrenceId, + [ActionParameter] UpdateEventRequest request) + { + return await UpdateEventOrEventOccurrence(authenticationCredentialsProviders, eventOccurrenceId, request); + } + + #endregion + + #region DELETE + + [Action("Delete event", Description = "Delete an event.")] + public async Task DeleteEvent(IEnumerable authenticationCredentialsProviders, + [ActionParameter] DeleteEventRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + try + { + await client.Me.Events[request.EventId].DeleteAsync(); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + #endregion + + private async Task CancelEventOrEventOccurrence(IEnumerable authenticationCredentialsProviders, + string eventOrEventOccurrenceId, CancelEventRequest request) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + var requestBody = new Microsoft.Graph.Me.Events.Item.Cancel.CancelPostRequestBody + { + Comment = request.Comment ?? "" + }; + try + { + await client.Me.Events[eventOrEventOccurrenceId].Cancel.PostAsync(requestBody); + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + private async Task UpdateEventOrEventOccurrence(IEnumerable authenticationCredentialsProviders, + string eventOrEventOccurrenceId, UpdateEventRequest request) + { + string UpdateBodyContentWithOnlineMeetingInformation(string html, string newContent) + { + var document = new HtmlDocument(); + document.LoadHtml(html); + var bodyContent = document.GetElementbyId(EventBodyContentId); + bodyContent.InnerHtml = newContent; + return document.DocumentNode.InnerHtml; + } + + string RecalculateContent(Event existingEvent) + { + string content; + if (existingEvent.IsOnlineMeeting.Value && + (request.IsOnlineMeeting == null || request.IsOnlineMeeting.Value)) + content = UpdateBodyContentWithOnlineMeetingInformation(existingEvent.Body.Content, request.BodyContent); + else + content = WrapEventBodyContent(request.BodyContent); + return content; + } + + string UpdateDate(string originalDateString, DateTime? newDate, string? newTime) + { + var isValidTimeFormat = !IsValidTimeFormat(newTime, out var parsedTime); + if (newTime != null && isValidTimeFormat) + throw new ArgumentException("Time format is not valid."); + + var originalDateTime = DateTime.Parse(originalDateString).ToLocalTime(); + var updatedDate = (newDate?.ToString("yyyy-MM-dd") ?? originalDateTime.ToString("yyyy-MM-dd")) + "T" + + (newTime != null ? parsedTime : originalDateTime.TimeOfDay); + return updatedDate; + } + + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + try + { + var eventData = await client.Me.Events[eventOrEventOccurrenceId].GetAsync(); + eventData.Subject = request.Subject ?? eventData.Subject; + eventData.Body = request.BodyContent != null + ? new ItemBody { ContentType = BodyType.Html, Content = RecalculateContent(eventData) } + : eventData.Body; + eventData.Start.DateTime = UpdateDate(eventData.Start.DateTime, request.EventDate, request.StartTime); + eventData.Start.TimeZone = TimeZoneInfo.Local.Id; + eventData.End.DateTime = UpdateDate(eventData.End.DateTime, request.EventDate, request.EndTime); + eventData.End.TimeZone = TimeZoneInfo.Local.Id; + eventData.Location = request.Location != null + ? new Location { DisplayName = request.Location } + : eventData.Location; + eventData.IsOnlineMeeting = request.IsOnlineMeeting ?? eventData.IsOnlineMeeting; + eventData.IsReminderOn = request.IsReminderOn ?? eventData.IsReminderOn; + eventData.ReminderMinutesBeforeStart = request.ReminderMinutesBeforeStart ?? eventData.ReminderMinutesBeforeStart; + eventData.Attendees = request.AttendeeEmails != null + ? new List(request.AttendeeEmails.Select(email => new Attendee + { + EmailAddress = new EmailAddress { Address = email }, + Type = AttendeeType.Optional + })) + : eventData.Attendees; + + var updatedEvent = await client.Me.Events[eventOrEventOccurrenceId].PatchAsync(eventData); + var updatedEventDto = new EventDto(updatedEvent); + return updatedEventDto; + } + catch (ODataError error) + { + throw new ArgumentException(error.Error.Message); + } + } + + private string WrapEventBodyContent(string? content) + { + return $"
{content ?? ""}
"; + } + + private bool IsValidTimeFormat(string time, out TimeSpan parsedTime) + {; + return TimeSpan.TryParse(time, out parsedTime); + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/ApplicationConstants.cs b/Apps.Microsoft365Calendar/ApplicationConstants.cs new file mode 100644 index 0000000..d69abda --- /dev/null +++ b/Apps.Microsoft365Calendar/ApplicationConstants.cs @@ -0,0 +1,10 @@ +namespace Apps.Microsoft365Calendar; + +public class ApplicationConstants +{ + public const string ClientId = "#{MSOUTLOOK_CLIENT_ID}#"; + public const string ClientSecret = "#{MSOUTLOOK_SECRET}#"; + public const string Scope = "#{MSOUTLOOK_SCOPE}#"; + public const string ClientState = "#{MSOUTLOOK_CLIENT_STATE}#"; + +} \ No newline at end of file diff --git a/Apps.App/Apps.App.csproj b/Apps.Microsoft365Calendar/Apps.Microsoft365Calendar.csproj similarity index 51% rename from Apps.App/Apps.App.csproj rename to Apps.Microsoft365Calendar/Apps.Microsoft365Calendar.csproj index d3459f5..9e93ed7 100644 --- a/Apps.App/Apps.App.csproj +++ b/Apps.Microsoft365Calendar/Apps.Microsoft365Calendar.csproj @@ -4,19 +4,28 @@ net8.0 enable enable - App - Description + Microsoft 365 Calendar + Microsoft 365 Calendar app that allows you to access and manage your calendar events. 1.0.0 - Apps.App + Apps.Microsoft365Calendar + Apps.Microsoft365Calendar + + + + + README.md + + + diff --git a/Apps.Microsoft365Calendar/Auth/OAuth2/OAuth2AuthorizeService.cs b/Apps.Microsoft365Calendar/Auth/OAuth2/OAuth2AuthorizeService.cs new file mode 100644 index 0000000..6920642 --- /dev/null +++ b/Apps.Microsoft365Calendar/Auth/OAuth2/OAuth2AuthorizeService.cs @@ -0,0 +1,27 @@ +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Authentication.OAuth2; +using Blackbird.Applications.Sdk.Common.Invocation; +using Microsoft.AspNetCore.WebUtilities; + +namespace Apps.Microsoft365Calendar.Auth.OAuth2; + +public class OAuth2AuthorizeService(InvocationContext invocationContext) + : BaseInvocable(invocationContext), IOAuth2AuthorizeService +{ + public string GetAuthorizationUrl(Dictionary values) + { + string bridgeOauthUrl = $"{InvocationContext.UriInfo.BridgeServiceUrl.ToString().TrimEnd('/')}/oauth"; + const string oauthUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; + var parameters = new Dictionary + { + { "client_id", ApplicationConstants.ClientId }, + { "redirect_uri", $"{InvocationContext.UriInfo.BridgeServiceUrl.ToString().TrimEnd('/')}/AuthorizationCode" }, + { "scope", ApplicationConstants.Scope }, + { "state", values["state"] }, + { "response_type", "code" }, + { "authorization_url", oauthUrl}, + { "actual_redirect_uri", InvocationContext.UriInfo.AuthorizationCodeRedirectUri.ToString() }, + }; + return QueryHelpers.AddQueryString(bridgeOauthUrl, parameters); + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Auth/OAuth2/OAuth2TokenService.cs b/Apps.Microsoft365Calendar/Auth/OAuth2/OAuth2TokenService.cs new file mode 100644 index 0000000..68054b0 --- /dev/null +++ b/Apps.Microsoft365Calendar/Auth/OAuth2/OAuth2TokenService.cs @@ -0,0 +1,68 @@ +using System.Text.Json; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Authentication.OAuth2; +using Blackbird.Applications.Sdk.Common.Invocation; + +namespace Apps.Microsoft365Calendar.Auth.OAuth2; + +public class OAuth2TokenService(InvocationContext invocationContext) + : BaseInvocable(invocationContext), IOAuth2TokenService +{ + private const string TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; + private const string ExpiresAtKeyName = "expires_at"; + + public bool IsRefreshToken(Dictionary values) + => values.TryGetValue(ExpiresAtKeyName, out var expireValue) && DateTime.UtcNow > DateTime.Parse(expireValue); + + public async Task> RefreshToken(Dictionary values, + CancellationToken cancellationToken) + { + const string grantType = "refresh_token"; + var bodyParameters = new Dictionary + { + { "grant_type", grantType }, + { "refresh_token", values["refresh_token"] }, + { "client_id", ApplicationConstants.ClientId }, + { "client_secret", ApplicationConstants.ClientSecret } + }; + return await RequestToken(bodyParameters, cancellationToken); + } + + public async Task> RequestToken(string state, string code, + Dictionary values, CancellationToken cancellationToken) + { + const string grantType = "authorization_code"; + var bodyParameters = new Dictionary + { + { "code", code }, + { "grant_type", grantType }, + { "client_id", ApplicationConstants.ClientId }, + { "client_secret", ApplicationConstants.ClientSecret }, + { "redirect_uri", $"{InvocationContext.UriInfo.BridgeServiceUrl.ToString().TrimEnd('/')}/AuthorizationCode" } + }; + return await RequestToken(bodyParameters, cancellationToken); + } + + public Task RevokeToken(Dictionary values) + { + throw new NotImplementedException(); + } + + private async Task> RequestToken(Dictionary bodyParameters, + CancellationToken cancellationToken) + { + var utcNow = DateTime.UtcNow; + using HttpClient httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + using var httpContent = new FormUrlEncodedContent(bodyParameters); + using var response = await httpClient.PostAsync(TokenUrl, httpContent, cancellationToken); + var responseContent = await response.Content.ReadAsStringAsync(); + var resultDictionary = JsonSerializer.Deserialize>(responseContent)? + .ToDictionary(r => r.Key, r => r.Value?.ToString()) + ?? throw new InvalidOperationException($"Invalid response content: {responseContent}"); + var expiresIn = int.Parse(resultDictionary["expires_in"]); + var expiresAt = utcNow.AddSeconds(expiresIn); + resultDictionary.Add(ExpiresAtKeyName, expiresAt.ToString()); + return resultDictionary; + } +} \ No newline at end of file diff --git a/Apps.App/Connections/ConnectionDefinition.cs b/Apps.Microsoft365Calendar/Connections/ConnectionDefinition.cs similarity index 94% rename from Apps.App/Connections/ConnectionDefinition.cs rename to Apps.Microsoft365Calendar/Connections/ConnectionDefinition.cs index 4af014e..f02a864 100644 --- a/Apps.App/Connections/ConnectionDefinition.cs +++ b/Apps.Microsoft365Calendar/Connections/ConnectionDefinition.cs @@ -1,7 +1,7 @@ using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Connections; -namespace Apps.App.Connections; +namespace Apps.Microsoft365Calendar.Connections; public class ConnectionDefinition : IConnectionDefinition { diff --git a/Apps.App/Connections/ConnectionValidator.cs b/Apps.Microsoft365Calendar/Connections/ConnectionValidator.cs similarity index 90% rename from Apps.App/Connections/ConnectionValidator.cs rename to Apps.Microsoft365Calendar/Connections/ConnectionValidator.cs index d494cf6..14ce94e 100644 --- a/Apps.App/Connections/ConnectionValidator.cs +++ b/Apps.Microsoft365Calendar/Connections/ConnectionValidator.cs @@ -1,7 +1,7 @@ using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Connections; -namespace Apps.App.Connections; +namespace Apps.Microsoft365Calendar.Connections; public class ConnectionValidator: IConnectionValidator { diff --git a/Apps.App/Constants/CredsNames.cs b/Apps.Microsoft365Calendar/Constants/CredsNames.cs similarity index 59% rename from Apps.App/Constants/CredsNames.cs rename to Apps.Microsoft365Calendar/Constants/CredsNames.cs index 25b572f..8d966c7 100644 --- a/Apps.App/Constants/CredsNames.cs +++ b/Apps.Microsoft365Calendar/Constants/CredsNames.cs @@ -1,4 +1,4 @@ -namespace Apps.App.Constants; +namespace Apps.Microsoft365Calendar.Constants; public static class CredsNames { diff --git a/Apps.Microsoft365Calendar/DataSourceHandlers/CalendarDataSourceHandler.cs b/Apps.Microsoft365Calendar/DataSourceHandlers/CalendarDataSourceHandler.cs new file mode 100644 index 0000000..867eb03 --- /dev/null +++ b/Apps.Microsoft365Calendar/DataSourceHandlers/CalendarDataSourceHandler.cs @@ -0,0 +1,22 @@ +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; +using Blackbird.Applications.Sdk.Common.Invocation; + +namespace Apps.Microsoft365Calendar.DataSourceHandlers; + +public class CalendarDataSourceHandler(InvocationContext invocationContext) + : BaseInvocable(invocationContext), IAsyncDataSourceHandler +{ + public async Task> GetDataAsync(DataSourceContext context, + CancellationToken cancellationToken) + { + var client = new MicrosoftOutlookClient(InvocationContext.AuthenticationCredentialsProviders); + var calendars = await client.Me.Calendars.GetAsync(requestConfiguration => + requestConfiguration.QueryParameters.Select = ["id", "name"], cancellationToken); + + return calendars.Value + .Where(c => context.SearchString == null + || c.Name.Contains(context.SearchString, StringComparison.OrdinalIgnoreCase)) + .ToDictionary(c => c.Id, c => c.Name); + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/DataSourceHandlers/EventDataSourceHandler.cs b/Apps.Microsoft365Calendar/DataSourceHandlers/EventDataSourceHandler.cs new file mode 100644 index 0000000..00f4359 --- /dev/null +++ b/Apps.Microsoft365Calendar/DataSourceHandlers/EventDataSourceHandler.cs @@ -0,0 +1,54 @@ +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; +using Blackbird.Applications.Sdk.Common.Invocation; +using Microsoft.Graph.Models; + +namespace Apps.Microsoft365Calendar.DataSourceHandlers; + +public class EventDataSourceHandler(InvocationContext invocationContext) + : BaseInvocable(invocationContext), IAsyncDataSourceHandler +{ + public async Task> GetDataAsync(DataSourceContext context, + CancellationToken cancellationToken) + { + IEnumerable events; + if (string.IsNullOrEmpty(context.SearchString)) + events = await GetEventsFromMainCalendar(cancellationToken); + else + events = await GetEventsFromAllCalendars(context.SearchString, cancellationToken); + + return events.ToDictionary(e => e.Id, e => e.Subject); + } + + private async Task> GetEventsFromMainCalendar(CancellationToken cancellationToken) + { + var client = new MicrosoftOutlookClient(InvocationContext.AuthenticationCredentialsProviders); + var events = await client.Me.Calendar.Events.GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Top = 20; + requestConfiguration.QueryParameters.Select = new[] { "id", "subject" }; + }, cancellationToken); + return events.Value; + } + + private async Task> GetEventsFromAllCalendars(string searchString, CancellationToken cancellationToken) + { + var client = new MicrosoftOutlookClient(InvocationContext.AuthenticationCredentialsProviders); + var calendars = await client.Me.Calendars.GetAsync(requestConfiguration => + requestConfiguration.QueryParameters.Select = new[] { "id" }, cancellationToken); + var events = new List(); + + foreach (var calendar in calendars.Value) + { + var calendarEvents = await client.Me.Calendars[calendar.Id].Events.GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Top = 20; + requestConfiguration.QueryParameters.Filter = $"contains(subject, '{searchString}')"; + requestConfiguration.QueryParameters.Select = new[] { "id", "subject" }; + }, cancellationToken); + events.AddRange(calendarEvents.Value); + } + + return events; + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/DataSourceHandlers/EventOccurrenceDataSourceHandler.cs b/Apps.Microsoft365Calendar/DataSourceHandlers/EventOccurrenceDataSourceHandler.cs new file mode 100644 index 0000000..70e4050 --- /dev/null +++ b/Apps.Microsoft365Calendar/DataSourceHandlers/EventOccurrenceDataSourceHandler.cs @@ -0,0 +1,56 @@ +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; +using Blackbird.Applications.Sdk.Common.Invocation; +using Microsoft.Graph.Models; + +namespace Apps.Microsoft365Calendar.DataSourceHandlers; + +public class EventOccurrenceDataSourceHandler(InvocationContext invocationContext) + : BaseInvocable(invocationContext), IAsyncDataSourceHandler +{ + public async Task> GetDataAsync(DataSourceContext context, + CancellationToken cancellationToken) + { + var searchString = context.SearchString; + var events = await GetUpcomingEventOccurrences(cancellationToken); + var filteredEvents = events.Where(e => searchString == null + || e.Subject.Contains(searchString, StringComparison.OrdinalIgnoreCase) + || e.Body.Content.Contains(searchString, StringComparison.OrdinalIgnoreCase) + || e.Start.ToDateTime().ToLocalTime().ToString("MM/dd/yyyy HH:mm") + .Contains(searchString, StringComparison.OrdinalIgnoreCase)).Take(20); + + return filteredEvents.ToDictionary(e => e.Id, + e => $"{e.Start.ToDateTime().ToLocalTime():MM/dd/yyyy HH:mm} {e.Subject}"); + } + + private async Task> GetUpcomingEventOccurrences(CancellationToken cancellationToken) + { + var client = new MicrosoftOutlookClient(InvocationContext.AuthenticationCredentialsProviders); + var calendars = await client.Me.Calendars.GetAsync(requestConfiguration => + requestConfiguration.QueryParameters.Select = new[] { "id" }, cancellationToken); + var events = new List(); + + foreach (var calendar in calendars.Value) + { + EventCollectionResponse? calendarEvents; + var skipEventsAmount = 0; + + do + { + calendarEvents = await client.Me.Calendars[calendar.Id].CalendarView.GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Select = new[] { "id", "subject", "body", "start" }; + requestConfiguration.QueryParameters.Top = 10; + requestConfiguration.QueryParameters.Skip = skipEventsAmount; + requestConfiguration.QueryParameters.StartDateTime = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss"); + requestConfiguration.QueryParameters.EndDateTime = (DateTime.Now + TimeSpan.FromDays(30)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss"); + }, cancellationToken); + events.AddRange(calendarEvents.Value); + skipEventsAmount += 10; + } while (calendarEvents.OdataNextLink != null); + } + + var upcomingEvents = events.OrderBy(e => e.Start.ToDateTime()); + return upcomingEvents; + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/DataSourceHandlers/MailFolderDataSourceHandler.cs b/Apps.Microsoft365Calendar/DataSourceHandlers/MailFolderDataSourceHandler.cs new file mode 100644 index 0000000..dbff353 --- /dev/null +++ b/Apps.Microsoft365Calendar/DataSourceHandlers/MailFolderDataSourceHandler.cs @@ -0,0 +1,23 @@ +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; +using Blackbird.Applications.Sdk.Common.Invocation; + +namespace Apps.Microsoft365Calendar.DataSourceHandlers; + +public class MailFolderDataSourceHandler(InvocationContext invocationContext) + : BaseInvocable(invocationContext), IAsyncDataSourceHandler +{ + public async Task> GetDataAsync(DataSourceContext context, + CancellationToken cancellationToken) + { + var client = new MicrosoftOutlookClient(InvocationContext.AuthenticationCredentialsProviders); + var mailFolders = await client.Me.MailFolders.GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Select = new[] { "id", "displayName" }; + requestConfiguration.QueryParameters.Filter = $"contains(displayName, '{context.SearchString ?? ""}')"; + } + , cancellationToken); + + return mailFolders.Value.ToDictionary(f => f.Id, f => f.DisplayName); + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/DataSourceHandlers/NonDefaultCalendarDataSourceHandler.cs b/Apps.Microsoft365Calendar/DataSourceHandlers/NonDefaultCalendarDataSourceHandler.cs new file mode 100644 index 0000000..607a0d4 --- /dev/null +++ b/Apps.Microsoft365Calendar/DataSourceHandlers/NonDefaultCalendarDataSourceHandler.cs @@ -0,0 +1,28 @@ +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; +using Blackbird.Applications.Sdk.Common.Invocation; + +namespace Apps.Microsoft365Calendar.DataSourceHandlers; + +public class NonDefaultCalendarDataSourceHandler : BaseInvocable, IAsyncDataSourceHandler +{ + public NonDefaultCalendarDataSourceHandler(InvocationContext invocationContext) : base(invocationContext) + { + } + + public async Task> GetDataAsync(DataSourceContext context, + CancellationToken cancellationToken) + { + var client = new MicrosoftOutlookClient(InvocationContext.AuthenticationCredentialsProviders); + var calendars = await client.Me.Calendars.GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Select = new[] { "id", "name" }; + requestConfiguration.QueryParameters.Filter = "isDefaultCalendar eq false"; + }, cancellationToken); + + return calendars.Value + .Where(c => context.SearchString == null + || c.Name.Contains(context.SearchString, StringComparison.OrdinalIgnoreCase)) + .ToDictionary(c => c.Id, c => c.Name); + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/DataSourceHandlers/RecurrencePatternDataSourceHandler.cs b/Apps.Microsoft365Calendar/DataSourceHandlers/RecurrencePatternDataSourceHandler.cs new file mode 100644 index 0000000..b3db4c5 --- /dev/null +++ b/Apps.Microsoft365Calendar/DataSourceHandlers/RecurrencePatternDataSourceHandler.cs @@ -0,0 +1,14 @@ +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; +using Blackbird.Applications.Sdk.Common.Invocation; + +namespace Apps.Microsoft365Calendar.DataSourceHandlers; + +public class RecurrencePatternDataSourceHandler(InvocationContext invocationContext) + : BaseInvocable(invocationContext), IDataSourceHandler +{ + public Dictionary GetData(DataSourceContext context) + { + return new[] { "Daily", "Weekly", "Monthly" }.ToDictionary(p => p, p => p); + } +} \ No newline at end of file diff --git a/Apps.App/Invocables/AppInvocable.cs b/Apps.Microsoft365Calendar/Invocables/AppInvocable.cs similarity index 52% rename from Apps.App/Invocables/AppInvocable.cs rename to Apps.Microsoft365Calendar/Invocables/AppInvocable.cs index ef7ee12..acb3e69 100644 --- a/Apps.App/Invocables/AppInvocable.cs +++ b/Apps.Microsoft365Calendar/Invocables/AppInvocable.cs @@ -1,18 +1,11 @@ -using Apps.App.Api; using Blackbird.Applications.Sdk.Common; using Blackbird.Applications.Sdk.Common.Authentication; using Blackbird.Applications.Sdk.Common.Invocation; -namespace Apps.App.Invocables; +namespace Apps.Microsoft365Calendar.Invocables; -public class AppInvocable : BaseInvocable +public class AppInvocable(InvocationContext invocationContext) : BaseInvocable(invocationContext) { protected AuthenticationCredentialsProvider[] Creds => InvocationContext.AuthenticationCredentialsProviders.ToArray(); - - protected AppClient Client { get; } - public AppInvocable(InvocationContext invocationContext) : base(invocationContext) - { - Client = new(); - } } \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/MicrosoftOutlookApplication.cs b/Apps.Microsoft365Calendar/MicrosoftOutlookApplication.cs new file mode 100644 index 0000000..67b19c3 --- /dev/null +++ b/Apps.Microsoft365Calendar/MicrosoftOutlookApplication.cs @@ -0,0 +1,47 @@ +using Apps.Microsoft365Calendar.Auth.OAuth2; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Authentication.OAuth2; +using Blackbird.Applications.Sdk.Common.Invocation; +using Blackbird.Applications.Sdk.Common.Metadata; + +namespace Apps.Microsoft365Calendar; + +public class MicrosoftOutlookApplication : BaseInvocable, IApplication, ICategoryProvider +{ + private readonly Dictionary _typesInstances; + + public IEnumerable Categories + { + get => [ApplicationCategory.Communication, ApplicationCategory.Microsoft365Apps]; + set { } + } + + public MicrosoftOutlookApplication(InvocationContext invocationContext) : base(invocationContext) + { + _typesInstances = CreateTypesInstances(); + } + + public string Name + { + get => "Microsoft Outlook"; + set { } + } + + public T GetInstance() + { + if (!_typesInstances.TryGetValue(typeof(T), out var value)) + { + throw new InvalidOperationException($"Instance of type '{typeof(T)}' not found"); + } + return (T)value; + } + + private Dictionary CreateTypesInstances() + { + return new Dictionary + { + { typeof(IOAuth2AuthorizeService), new OAuth2AuthorizeService(InvocationContext) }, + { typeof(IOAuth2TokenService), new OAuth2TokenService(InvocationContext) } + }; + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/MicrosoftOutlookClient.cs b/Apps.Microsoft365Calendar/MicrosoftOutlookClient.cs new file mode 100644 index 0000000..b3e5cd7 --- /dev/null +++ b/Apps.Microsoft365Calendar/MicrosoftOutlookClient.cs @@ -0,0 +1,17 @@ +using Blackbird.Applications.Sdk.Common.Authentication; +using Microsoft.Graph; +using Microsoft.Kiota.Abstractions.Authentication; + +namespace Apps.Microsoft365Calendar; + +public class MicrosoftOutlookClient(IEnumerable authenticationCredentialsProviders) + : GraphServiceClient(GetAuthenticationProvider(authenticationCredentialsProviders)) +{ + private static BaseBearerTokenAuthenticationProvider GetAuthenticationProvider( + IEnumerable authenticationCredentialsProviders) + { + var token = authenticationCredentialsProviders.First(p => p.KeyName == "Authorization").Value; + var accessTokenProvider = new AccessTokenProvider(token); + return new BaseBearerTokenAuthenticationProvider(accessTokenProvider); + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Calendar/Requests/CreateCalendarRequest.cs b/Apps.Microsoft365Calendar/Models/Calendar/Requests/CreateCalendarRequest.cs new file mode 100644 index 0000000..ae41d95 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Calendar/Requests/CreateCalendarRequest.cs @@ -0,0 +1,9 @@ +using Blackbird.Applications.Sdk.Common; + +namespace Apps.MicrosoftOutlook.Models.Calendar.Requests; + +public class CreateCalendarRequest +{ + [Display("Calendar name")] + public string CalendarName { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Calendar/Requests/DeleteCalendarRequest.cs b/Apps.Microsoft365Calendar/Models/Calendar/Requests/DeleteCalendarRequest.cs new file mode 100644 index 0000000..fad0978 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Calendar/Requests/DeleteCalendarRequest.cs @@ -0,0 +1,12 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Calendar.Requests; + +public class DeleteCalendarRequest +{ + [Display("Calendar")] + [DataSource(typeof(NonDefaultCalendarDataSourceHandler))] + public string CalendarId { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Calendar/Requests/GetCalendarRequest.cs b/Apps.Microsoft365Calendar/Models/Calendar/Requests/GetCalendarRequest.cs new file mode 100644 index 0000000..8f36acc --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Calendar/Requests/GetCalendarRequest.cs @@ -0,0 +1,12 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Calendar.Requests; + +public class GetCalendarRequest +{ + [Display("Calendar")] + [DataSource(typeof(CalendarDataSourceHandler))] + public string? CalendarId { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Calendar/Requests/GetScheduleRequest.cs b/Apps.Microsoft365Calendar/Models/Calendar/Requests/GetScheduleRequest.cs new file mode 100644 index 0000000..8b558d9 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Calendar/Requests/GetScheduleRequest.cs @@ -0,0 +1,15 @@ +using Blackbird.Applications.Sdk.Common; + +namespace Apps.MicrosoftOutlook.Models.Calendar.Requests; + +public class GetScheduleRequest +{ + [Display("Users' emails")] + public List Emails { get; set; } + + [Display("From")] + public DateTime StartDateTime { get; set; } + + [Display("Till")] + public DateTime EndDateTime { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Calendar/Requests/RenameCalendarRequest.cs b/Apps.Microsoft365Calendar/Models/Calendar/Requests/RenameCalendarRequest.cs new file mode 100644 index 0000000..80b351e --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Calendar/Requests/RenameCalendarRequest.cs @@ -0,0 +1,15 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Calendar.Requests; + +public class RenameCalendarRequest +{ + [Display("Calendar")] + [DataSource(typeof(CalendarDataSourceHandler))] + public string? CalendarId { get; set; } + + [Display("New calendar name")] + public string CalendarName { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Calendar/Responses/GetScheduleResponse.cs b/Apps.Microsoft365Calendar/Models/Calendar/Responses/GetScheduleResponse.cs new file mode 100644 index 0000000..82908af --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Calendar/Responses/GetScheduleResponse.cs @@ -0,0 +1,8 @@ +using Apps.Microsoft365Calendar.Models.Dtos; + +namespace Apps.MicrosoftOutlook.Models.Calendar.Responses; + +public class GetScheduleResponse +{ + public IEnumerable Schedules { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Calendar/Responses/ListCalendarsResponse.cs b/Apps.Microsoft365Calendar/Models/Calendar/Responses/ListCalendarsResponse.cs new file mode 100644 index 0000000..f0727a1 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Calendar/Responses/ListCalendarsResponse.cs @@ -0,0 +1,8 @@ +using Apps.Microsoft365Calendar.Models.Dtos; + +namespace Apps.MicrosoftOutlook.Models.Calendar.Responses; + +public class ListCalendarsResponse +{ + public IEnumerable Calendars { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Dtos/CalendarDto.cs b/Apps.Microsoft365Calendar/Models/Dtos/CalendarDto.cs new file mode 100644 index 0000000..53f3966 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Dtos/CalendarDto.cs @@ -0,0 +1,46 @@ +using Blackbird.Applications.Sdk.Common; +using Microsoft.Graph.Models; + +namespace Apps.Microsoft365Calendar.Models.Dtos; + +public class CalendarDto +{ + public CalendarDto(Calendar calendar) + { + CalendarId = calendar.Id; + Name = calendar.Name; + CanShare = calendar.CanShare; + CanEdit = calendar.CanEdit; + CanViewPrivateItems = calendar.CanViewPrivateItems; + IsDefaultCalendar = calendar.IsDefaultCalendar; + Owner = new OwnerDto { OwnerEmail = calendar.Owner.Address, OwnerName = calendar.Owner.Name }; + } + + [Display("Calendar ID")] + public string CalendarId { get; set; } + + public string Name { get; set; } + + [Display("Can share")] + public bool? CanShare { get; set; } + + [Display("Can edit")] + public bool? CanEdit { get; set; } + + [Display("Can view private items")] + public bool? CanViewPrivateItems { get; set; } + + [Display("Is default calendar")] + public bool? IsDefaultCalendar { get; set; } + + public OwnerDto Owner { get; set; } +} + +public class OwnerDto +{ + [Display("Owner name")] + public string OwnerName { get; set; } + + [Display("Owner email")] + public string OwnerEmail { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Dtos/EventDto.cs b/Apps.Microsoft365Calendar/Models/Dtos/EventDto.cs new file mode 100644 index 0000000..5588b3c --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Dtos/EventDto.cs @@ -0,0 +1,103 @@ +using Blackbird.Applications.Sdk.Common; +using Microsoft.Graph.Models; + +namespace Apps.Microsoft365Calendar.Models.Dtos; + +public class EventDto +{ + public EventDto(Event calendarEvent) + { + EventId = calendarEvent.Id; + Subject = calendarEvent.Subject; + Link = calendarEvent.WebLink; + EventDate = calendarEvent.Start.ToDateTime().Date; + StartTime = calendarEvent.Start.ToDateTime().ToLocalTime().ToLongTimeString(); + EndTime = calendarEvent.End.ToDateTime().ToLocalTime().ToLongTimeString(); + Location = calendarEvent.Location.DisplayName; + Attendees = calendarEvent.Attendees.Select(a => new AttendeeDto + { + Name = a.EmailAddress.Name, + Email = a.EmailAddress.Address, + Type = a.Type.ToString() + }); + JoinUrl = calendarEvent.OnlineMeeting?.JoinUrl ?? "Offline meeting"; + BodyPreview = calendarEvent.BodyPreview; + IsReminderOn = calendarEvent.IsReminderOn; + ReminderMinutesBeforeStart = calendarEvent.ReminderMinutesBeforeStart; + CreatedDateTime = calendarEvent.CreatedDateTime.Value.LocalDateTime; + LastModifiedDateTime = calendarEvent.LastModifiedDateTime.Value.LocalDateTime; + Recurrence = calendarEvent.Recurrence == null + ? null + : new RecurrenceDto + { + RecurrencePattern = calendarEvent.Recurrence.Pattern.Type.Value.ToString(), + Interval = calendarEvent.Recurrence.Pattern.Interval.Value, + DaysOfWeek = calendarEvent.Recurrence.Pattern.DaysOfWeek?.Select(d => d.ToString()), + RecurrenceEndDateTime = calendarEvent.Recurrence.Range.EndDate.Value.DateTime + }; + } + + [Display("Event ID")] + public string EventId { get; set; } + + public string Subject { get; set; } + + public string Link { get; set; } + + public string Location { get; set; } + + public IEnumerable Attendees { get; set; } + + [Display("Date event takes place")] + public DateTime EventDate { get; set; } + + [Display("Start time")] + public string StartTime { get; set; } + + [Display("End time")] + public string EndTime { get; set; } + + [Display("Join url")] + public string? JoinUrl { get; set; } + + [Display("Body preview")] + public string? BodyPreview { get; set; } + + [Display("Is reminder on")] + public bool? IsReminderOn { get; set; } + + [Display("Minutes till event that reminder alert occurs")] + public int? ReminderMinutesBeforeStart { get; set; } + + [Display("Created")] + public DateTime CreatedDateTime { get; set; } + + [Display("Last modified")] + public DateTime LastModifiedDateTime { get; set; } + + public RecurrenceDto? Recurrence { get; set; } +} + +public class AttendeeDto +{ + public string Email { get; set; } + + public string? Name { get; set; } + + public string Type { get; set; } +} + +public class RecurrenceDto +{ + [Display("Recurrence pattern")] + public string RecurrencePattern { get; set; } + + [Display("Recurrence interval")] + public int Interval { get; set; } = 1; + + [Display("Days of week recurrence pattern applies to")] + public IEnumerable? DaysOfWeek { get; set; } + + [Display("Event recurrence end date")] + public DateTime RecurrenceEndDateTime { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Dtos/ScheduleDto.cs b/Apps.Microsoft365Calendar/Models/Dtos/ScheduleDto.cs new file mode 100644 index 0000000..9699af1 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Dtos/ScheduleDto.cs @@ -0,0 +1,39 @@ +using Blackbird.Applications.Sdk.Common; +using Microsoft.Graph.Models; + +namespace Apps.Microsoft365Calendar.Models.Dtos; + +public class ScheduleDto +{ + public ScheduleDto(ScheduleInformation schedule) + { + Email = schedule.ScheduleId; + ScheduleItems = schedule.ScheduleItems.Select(s => new ScheduleItemDto + { + Status = s.Status.ToString(), + Subject = s.Subject, + Location = s.Location, + StartDateTime = s.Start.ToDateTime().ToLocalTime(), + EndDateTime = s.End.ToDateTime().ToLocalTime() + }); + } + + public string Email { get; set; } + + public IEnumerable ScheduleItems { get; set; } +} + +public class ScheduleItemDto +{ + public string Status { get; set; } + + public string? Subject { get; set; } + + public string? Location { get; set; } + + [Display("From")] + public DateTime StartDateTime { get; set; } + + [Display("Till")] + public DateTime EndDateTime { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/CancelEventRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/CancelEventRequest.cs new file mode 100644 index 0000000..1181acd --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/CancelEventRequest.cs @@ -0,0 +1,6 @@ +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class CancelEventRequest +{ + public string? Comment { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/CreateEventRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/CreateEventRequest.cs new file mode 100644 index 0000000..180f379 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/CreateEventRequest.cs @@ -0,0 +1,53 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class CreateEventRequest +{ + [Display("Calendar")] + [DataSource(typeof(CalendarDataSourceHandler))] + public string? CalendarId { get; set; } + + public string Subject { get; set; } + + public string? Location { get; set; } + + [Display("Body content")] + public string? BodyContent { get; set; } + + [Display("Date event takes place")] + public DateTime EventDate { get; set; } + + [Display("Start time in hh:mm format")] + public string StartTime { get; set; } + + [Display("End time in hh:mm format")] + public string EndTime { get; set; } + + [Display("Is online meeting")] + public bool IsOnlineMeeting { get; set; } + + [Display("Is reminder on")] + public bool IsReminderOn { get; set; } + + [Display("Minutes till event that reminder alert occurs")] + public int? ReminderMinutesBeforeStart { get; set; } + + [Display("Attendee emails")] + public IEnumerable AttendeeEmails { get; set; } + + [Display("Recurrence pattern")] + [DataSource(typeof(RecurrencePatternDataSourceHandler))] + public string? RecurrencePattern { get; set; } + + [Display("Recurrence interval")] + public int? Interval { get; set; } + + [Display("Days of week recurrence pattern applies to")] + public IEnumerable? DaysOfWeek { get; set; } + + [Display("Event recurrence end date")] + public DateTime? RecurrenceEndDate { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/DeleteEventRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/DeleteEventRequest.cs new file mode 100644 index 0000000..f8c76b9 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/DeleteEventRequest.cs @@ -0,0 +1,12 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class DeleteEventRequest +{ + [Display("Event")] + [DataSource(typeof(EventDataSourceHandler))] + public string EventId { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/ForwardEventRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/ForwardEventRequest.cs new file mode 100644 index 0000000..1113407 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/ForwardEventRequest.cs @@ -0,0 +1,20 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class ForwardEventRequest +{ + [Display("Event")] + [DataSource(typeof(EventDataSourceHandler))] + public string EventId { get; set; } + + [Display("Recipient email")] + public string RecipientEmail { get; set; } + + [Display("Recipient name")] + public string? RecipientName { get; set; } + + public string? Comment { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/GetEventRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/GetEventRequest.cs new file mode 100644 index 0000000..f57e5fe --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/GetEventRequest.cs @@ -0,0 +1,12 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class GetEventRequest +{ + [Display("Event")] + [DataSource(typeof(EventDataSourceHandler))] + public string EventId { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/ListEventOccurrencesRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/ListEventOccurrencesRequest.cs new file mode 100644 index 0000000..4e355c9 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/ListEventOccurrencesRequest.cs @@ -0,0 +1,18 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class ListEventOccurrencesRequest +{ + [Display("Event")] + [DataSource(typeof(EventDataSourceHandler))] + public string EventId { get; set; } + + [Display("From")] + public DateTime StartDate { get; set; } + + [Display("Till")] + public DateTime EndDate { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/ListEventsRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/ListEventsRequest.cs new file mode 100644 index 0000000..d94ea1f --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/ListEventsRequest.cs @@ -0,0 +1,12 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class ListEventsRequest +{ + [Display("Calendar")] + [DataSource(typeof(CalendarDataSourceHandler))] + public string? CalendarId { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/ListRecentlyCreatedEventsRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/ListRecentlyCreatedEventsRequest.cs new file mode 100644 index 0000000..d5d8690 --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/ListRecentlyCreatedEventsRequest.cs @@ -0,0 +1,14 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class ListRecentlyCreatedEventsRequest +{ + [Display("Calendar")] + [DataSource(typeof(CalendarDataSourceHandler))] + public string? CalendarId { get; set; } + + public int? Hours { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/ListRecentlyUpdatedEventsRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/ListRecentlyUpdatedEventsRequest.cs new file mode 100644 index 0000000..d5ab85c --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/ListRecentlyUpdatedEventsRequest.cs @@ -0,0 +1,14 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class ListRecentlyUpdatedEventsRequest +{ + [Display("Calendar")] + [DataSource(typeof(CalendarDataSourceHandler))] + public string? CalendarId { get; set; } + + public int? Hours { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Requests/UpdateEventRequest.cs b/Apps.Microsoft365Calendar/Models/Event/Requests/UpdateEventRequest.cs new file mode 100644 index 0000000..cefdf1b --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Requests/UpdateEventRequest.cs @@ -0,0 +1,34 @@ +using Blackbird.Applications.Sdk.Common; + +namespace Apps.MicrosoftOutlook.Models.Event.Requests; + +public class UpdateEventRequest +{ + public string? Subject { get; set; } + + public string? Location { get; set; } + + [Display("Body content")] + public string? BodyContent { get; set; } + + [Display("Date event takes place")] + public DateTime? EventDate { get; set; } + + [Display("Start time in hh:mm format")] + public string? StartTime { get; set; } + + [Display("End time in hh:mm format")] + public string? EndTime { get; set; } + + [Display("Is online meeting")] + public bool? IsOnlineMeeting { get; set; } + + [Display("Is reminder on")] + public bool? IsReminderOn { get; set; } + + [Display("Minutes till event that reminder alert occurs")] + public int? ReminderMinutesBeforeStart { get; set; } + + [Display("Attendee emails")] + public IEnumerable? AttendeeEmails { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Models/Event/Responses/ListEventsResponse.cs b/Apps.Microsoft365Calendar/Models/Event/Responses/ListEventsResponse.cs new file mode 100644 index 0000000..ad0063d --- /dev/null +++ b/Apps.Microsoft365Calendar/Models/Event/Responses/ListEventsResponse.cs @@ -0,0 +1,8 @@ +using Apps.Microsoft365Calendar.Models.Dtos; + +namespace Apps.MicrosoftOutlook.Models.Event.Responses; + +public class ListEventsResponse +{ + public IEnumerable Events { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Handlers/BaseWebhookHandler.cs b/Apps.Microsoft365Calendar/Webhooks/Handlers/BaseWebhookHandler.cs new file mode 100644 index 0000000..5e1d5df --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Handlers/BaseWebhookHandler.cs @@ -0,0 +1,84 @@ +using Apps.Microsoft365Calendar.Webhooks.Inputs; +using Blackbird.Applications.Sdk.Common.Authentication; +using Blackbird.Applications.Sdk.Common.Webhooks; +using Microsoft.Graph.Models; + +namespace Apps.Microsoft365Calendar.Webhooks.Handlers; + +public abstract class BaseWebhookHandler : IWebhookEventHandler, IAsyncRenewableWebhookEventHandler +{ + private readonly string _subscriptionEvent; + protected readonly IWebhookInput? WebhookInput; + + protected BaseWebhookHandler(string subscriptionEvent) + { + _subscriptionEvent = subscriptionEvent; + } + + protected BaseWebhookHandler([WebhookParameter(true)] IWebhookInput input, string subscriptionEvent) + : this(subscriptionEvent) + { + WebhookInput = input; + } + + public async Task SubscribeAsync(IEnumerable authenticationCredentialsProviders, + Dictionary values) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + var resource = GetResource(); + + var subscription = new Subscription + { + ChangeType = _subscriptionEvent, + NotificationUrl = values["payloadUrl"], + Resource = resource, + ExpirationDateTime = DateTimeOffset.Now + TimeSpan.FromMinutes(4210), + ClientState = ApplicationConstants.ClientState + }; + await client.Subscriptions.PostAsync(subscription); + + if(WebhookInput.SharedEmails != null) + { + foreach (var sharedContact in WebhookInput.SharedEmails) + { + string subscriptionForSharedContact = resource.Replace("/me", $"/users/{sharedContact}"); + var subscriptionShared = new Subscription + { + ChangeType = _subscriptionEvent, + NotificationUrl = values["payloadUrl"], + Resource = subscriptionForSharedContact, + ExpirationDateTime = DateTimeOffset.Now + TimeSpan.FromMinutes(4210), + ClientState = ApplicationConstants.ClientState + }; + await client.Subscriptions.PostAsync(subscriptionShared); + } + } + } + + public async Task UnsubscribeAsync(IEnumerable authenticationCredentialsProviders, + Dictionary values) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + var subscriptions = (await client.Subscriptions.GetAsync()).Value.Where(s => s.NotificationUrl == values["payloadUrl"]).ToList(); + foreach(var subscription in subscriptions) + { + await client.Subscriptions[subscription.Id].DeleteAsync(); + } + } + + [Period(4200)] + public async Task RenewSubscription(IEnumerable authenticationCredentialsProviders, + Dictionary values) + { + var client = new MicrosoftOutlookClient(authenticationCredentialsProviders); + var subscription = (await client.Subscriptions.GetAsync()).Value.First(s => s.NotificationUrl == values["payloadUrl"]); + + var requestBody = new Subscription + { + ExpirationDateTime = DateTimeOffset.Now + TimeSpan.FromMinutes(4000) + }; + await client.Subscriptions[subscription.Id].PatchAsync(requestBody); + } + + protected abstract string GetResource(); +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Handlers/Events/EventCreatedWebhookHandler.cs b/Apps.Microsoft365Calendar/Webhooks/Handlers/Events/EventCreatedWebhookHandler.cs new file mode 100644 index 0000000..6271897 --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Handlers/Events/EventCreatedWebhookHandler.cs @@ -0,0 +1,17 @@ +using Apps.Microsoft365Calendar.Webhooks.Inputs; +using Blackbird.Applications.Sdk.Common.Webhooks; + +namespace Apps.Microsoft365Calendar.Webhooks.Handlers.Events; + +public class EventCreatedWebhookHandler([WebhookParameter(true)] CalendarInput input) + : BaseWebhookHandler(input, SubscriptionEvent) +{ + private const string SubscriptionEvent = "created"; + + protected override string GetResource() + { + var calendarInput = (CalendarInput)WebhookInput; + var resource = $"/me/calendars/{calendarInput.CalendarId}/events"; + return resource; + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Inputs/CalendarInput.cs b/Apps.Microsoft365Calendar/Webhooks/Inputs/CalendarInput.cs new file mode 100644 index 0000000..00d7437 --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Inputs/CalendarInput.cs @@ -0,0 +1,12 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.Microsoft365Calendar.Webhooks.Inputs; + +public class CalendarInput : IWebhookInput +{ + [Display("Calendar")] + [DataSource(typeof(CalendarDataSourceHandler))] + public string CalendarId { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Inputs/IWebhookInput.cs b/Apps.Microsoft365Calendar/Webhooks/Inputs/IWebhookInput.cs new file mode 100644 index 0000000..7524409 --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Inputs/IWebhookInput.cs @@ -0,0 +1,9 @@ +using Blackbird.Applications.Sdk.Common; + +namespace Apps.Microsoft365Calendar.Webhooks.Inputs; + +public class IWebhookInput +{ + [Display("Shared emails")] + public List? SharedEmails { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Inputs/MailFolderInput.cs b/Apps.Microsoft365Calendar/Webhooks/Inputs/MailFolderInput.cs new file mode 100644 index 0000000..86722cb --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Inputs/MailFolderInput.cs @@ -0,0 +1,12 @@ +using Apps.Microsoft365Calendar.DataSourceHandlers; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Dynamic; + +namespace Apps.Microsoft365Calendar.Webhooks.Inputs; + +public class MailFolderInput : IWebhookInput +{ + [Display("Message folder")] + [DataSource(typeof(MailFolderDataSourceHandler))] + public string MailFolderId { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Inputs/SenderInput.cs b/Apps.Microsoft365Calendar/Webhooks/Inputs/SenderInput.cs new file mode 100644 index 0000000..7b3df7c --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Inputs/SenderInput.cs @@ -0,0 +1,9 @@ +using Blackbird.Applications.Sdk.Common; + +namespace Apps.Microsoft365Calendar.Webhooks.Inputs; + +public class SenderInput +{ + [Display("Sender email")] + public string? Email { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Lists/BaseWebhookList.cs b/Apps.Microsoft365Calendar/Webhooks/Lists/BaseWebhookList.cs new file mode 100644 index 0000000..7a43bba --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Lists/BaseWebhookList.cs @@ -0,0 +1,57 @@ +using System.Net; +using Apps.Microsoft365Calendar.Webhooks.Lists.ItemGetters; +using Apps.MicrosoftOutlook.Webhooks.Payload; +using Blackbird.Applications.Sdk.Common; +using Blackbird.Applications.Sdk.Common.Authentication; +using Blackbird.Applications.Sdk.Common.Invocation; +using Blackbird.Applications.Sdk.Common.Webhooks; +using Newtonsoft.Json; + +namespace Apps.Microsoft365Calendar.Webhooks.Lists; + +[WebhookList] +public abstract class BaseWebhookList(InvocationContext invocationContext) : BaseInvocable(invocationContext) +{ + protected readonly IEnumerable AuthenticationCredentialsProviders = invocationContext.AuthenticationCredentialsProviders; + + protected async Task> HandleWebhookRequest(WebhookRequest request, + ItemGetter itemGetter) where T: class + { + if (request.QueryParameters.TryGetValue("validationToken", out var validationToken)) + { + return new WebhookResponse + { + HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(validationToken) + }, + ReceivedWebhookRequestType = WebhookRequestType.Preflight + }; + } + + var eventPayload = JsonConvert.DeserializeObject(request.Body.ToString(), + new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore }).Value.First(); + + if (eventPayload.ClientState != ApplicationConstants.ClientState) + return new WebhookResponse + { + HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK), + ReceivedWebhookRequestType = WebhookRequestType.Preflight + }; + + var item = await itemGetter.GetItem(eventPayload); + + if (item is null) + return new WebhookResponse + { + HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK), + ReceivedWebhookRequestType = WebhookRequestType.Preflight + }; + + return new WebhookResponse + { + HttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK), + Result = item + }; + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Lists/EventWebhooks.cs b/Apps.Microsoft365Calendar/Webhooks/Lists/EventWebhooks.cs new file mode 100644 index 0000000..c8b8995 --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Lists/EventWebhooks.cs @@ -0,0 +1,18 @@ +using Apps.Microsoft365Calendar.Models.Dtos; +using Apps.Microsoft365Calendar.Webhooks.Handlers.Events; +using Apps.Microsoft365Calendar.Webhooks.Lists.ItemGetters; +using Blackbird.Applications.Sdk.Common.Invocation; +using Blackbird.Applications.Sdk.Common.Webhooks; + +namespace Apps.Microsoft365Calendar.Webhooks.Lists; + +[WebhookList] +public class EventWebhooks(InvocationContext invocationContext) : BaseWebhookList(invocationContext) +{ + [Webhook("On event created", typeof(EventCreatedWebhookHandler), + Description = "This webhook is triggered when a new event is created.")] + public async Task> OnMessageCreated(WebhookRequest request) + { + return await HandleWebhookRequest(request, new EventGetter(AuthenticationCredentialsProviders)); + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Lists/ItemGetters/EventGetter.cs b/Apps.Microsoft365Calendar/Webhooks/Lists/ItemGetters/EventGetter.cs new file mode 100644 index 0000000..51c61a0 --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Lists/ItemGetters/EventGetter.cs @@ -0,0 +1,16 @@ +using Apps.Microsoft365Calendar.Models.Dtos; +using Apps.MicrosoftOutlook.Webhooks.Payload; +using Blackbird.Applications.Sdk.Common.Authentication; + +namespace Apps.Microsoft365Calendar.Webhooks.Lists.ItemGetters; + +public class EventGetter(IEnumerable authenticationCredentialsProviders) + : ItemGetter(authenticationCredentialsProviders) +{ + public override async Task GetItem(EventPayload eventPayload) + { + var client = new MicrosoftOutlookClient(AuthenticationCredentialsProviders); + var calendarEvent = await client.Me.Events[eventPayload.ResourceData.Id].GetAsync(); + return new EventDto(calendarEvent); + } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Lists/ItemGetters/ItemGetter.cs b/Apps.Microsoft365Calendar/Webhooks/Lists/ItemGetters/ItemGetter.cs new file mode 100644 index 0000000..7af608e --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Lists/ItemGetters/ItemGetter.cs @@ -0,0 +1,11 @@ +using Apps.MicrosoftOutlook.Webhooks.Payload; +using Blackbird.Applications.Sdk.Common.Authentication; + +namespace Apps.Microsoft365Calendar.Webhooks.Lists.ItemGetters; + +public abstract class ItemGetter(IEnumerable authenticationCredentialsProviders) +{ + protected readonly IEnumerable AuthenticationCredentialsProviders = authenticationCredentialsProviders; + + public abstract Task GetItem(EventPayload eventPayload); +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/Webhooks/Payload/EventPayload.cs b/Apps.Microsoft365Calendar/Webhooks/Payload/EventPayload.cs new file mode 100644 index 0000000..f39418d --- /dev/null +++ b/Apps.Microsoft365Calendar/Webhooks/Payload/EventPayload.cs @@ -0,0 +1,19 @@ +namespace Apps.MicrosoftOutlook.Webhooks.Payload; + +public class EventPayload +{ + public string SubscriptionId { get; set; } + public string ChangeType { get; set; } + public string ClientState { get; set; } + public ResourceData ResourceData { get; set; } +} + +public class ResourceData +{ + public string Id { get; set; } +} + +public class EventPayloadWrapper +{ + public IEnumerable Value { get; set; } +} \ No newline at end of file diff --git a/Apps.Microsoft365Calendar/image/icon.png b/Apps.Microsoft365Calendar/image/icon.png new file mode 100644 index 0000000..1a29778 Binary files /dev/null and b/Apps.Microsoft365Calendar/image/icon.png differ diff --git a/App.sln b/Microsoft365Calendar.sln similarity index 77% rename from App.sln rename to Microsoft365Calendar.sln index db41e80..ffd762b 100644 --- a/App.sln +++ b/Microsoft365Calendar.sln @@ -1,5 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apps.App", "Apps.App\Apps.App.csproj", "{EB25604E-8EBA-464E-BEDF-30EA95395AD3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apps.Microsoft365Calendar", "Apps.Microsoft365Calendar\Apps.Microsoft365Calendar.csproj", "{EB25604E-8EBA-464E-BEDF-30EA95395AD3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/README.md b/README.md index f392e3f..f9dd4af 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Blackbird.io {{App name}} +# Blackbird.io Microsoft 365 Calendar Blackbird is the new automation backbone for the language technology industry. Blackbird provides enterprise-scale automation and orchestration with a simple no-code/low-code platform. Blackbird enables ambitious organizations to identify, vet and automate as many processes as possible. Not just localization workflows, but any business and IT process. This repository represents an application that is deployable on Blackbird and usable inside the workflow editor. @@ -6,7 +6,47 @@ Blackbird is the new automation backbone for the language technology industry. B -Documentation coming soon. +Microsoft 365 Calendar app that allows you to access and manage your calendar events. + +## Before setting up + +Before you can connect you need to make sure that you have a Microsoft 365 account. + +## Connecting + +1. Navigate to apps and search for Microsoft 365 Calendar. +2. Click _Add Connection_. +3. Name your connection for future reference e.g. 'My organization'. +4. Click _Authorize connection_. +5. Follow the instructions that Microsoft gives you, authorizing Blackbird.io to act on your behalf. +6. When you return to Blackbird, confirm that the connection has appeared and the status is _Connected_. + +![Connecting](image/README/connecting.png) + +## Actions + +- **List calendars** returns a list of current user's calendars. +- **Get calendar** retrieves a calendar. If calendar is not specified, default calendar is returned. +- **Create calendar** +- **Get users' schedule information** returns the free/busy availability information for a collection of users in specified time period. +- **Rename calendar** renames a calendar. If calendar is not specified, default calendar is renamed. +- **Delete calendar** +- **List events** retrieves a list of events in a calendar. If calendar is not specified, default calendar's events are listed. +- **Get event** +- **List occurrences of event** returns the occurrences of an event for a specified time range. +- **List recently created events** retrieves a list of events created during past hours. If number of hours is not specified, events created during past 24 hours are listed. If calendar is not specified, default calendar's events are listed. +- **List recently updated events** retrieves a list of events updated during past hours. If number of hours is not specified, events updated during past 24 hours are listed. If calendar is not specified, default calendar's events are listed. +- **Create event in a calendar** create a new event in a calendar. If calendar is not specified, the event is created in the default calendar. If the event is an online meeting, a Microsoft Teams meeting is automatically created. To create a recurring event specify recurrence pattern and interval which can be in days, weeks or months, depending on recurrence pattern type. If interval is not specified, it is set to 1. For weekly or monthly patterns provide days of week on which the event occurs. +- **Cancel event** sends a cancellation message and cancels the event. Can be performed only by organizer or a meeting. +- **Cancel event occurrence** sends a cancellation message and cancels an occurrence of a recurring meeting. Can be performed only by organizer or a meeting. +- **Forward event** forwards the meeting request to a new recipient +- **Update event** +- **Update event occurrence** +- **Delete event** + +## Events + +- **On event created** is triggered when a new event is created in specified calendar. ## Feedback diff --git a/image/README/connecting.png b/image/README/connecting.png new file mode 100644 index 0000000..889e82a Binary files /dev/null and b/image/README/connecting.png differ