I want to get notifications on a specific event that I have created from my application. For that, I am using the below code to subscribe to a specific event.
var subscription = new Subscription
{
ChangeType = "updated,deleted",
NotificationUrl = $"{_notificationHost}/listen",
Resource = "me/events/{event-id}",
ClientState = Guid.NewGuid().ToString(),
IncludeResourceData = false,
// Subscription only lasts for one hour
ExpirationDateTime = DateTimeOffset.UtcNow.AddHours(1)
};
But while creating it throws an exception:
Error creating subscription: Code: ExtensionError Message: Operation:
Create; Exception: [Status Code: BadRequest; Reason: The value
'https://outlook.office365.com/api/beta/Users('{userid}')/Events('{event-id}')'
of parameter 'Resource' is not supported.]
Just ran into this same issue. It looks like the Subscription API doesn't support subscriptions on specific events, only the entire calendar. There's an example for subscriptions to events in the "Resources examples" section, which looks like exactly what you want: me/events. The ID of the updated/deleted event is included in the callback, so if you only care about certain events, you can use that.
Related
In my project, I have created Rest API project to create subscription and to listen notification from Microsoft Graph API.
I want subscription for following things:
When I create meeting room event then I want to get notification if any attendees accept/reject that meeting room event.
If whole meeting event is cancelled or moves to another time slot or added any new attendees.
I was expecting all above gets covered by following code but it is not :
var subscription = new Subscription
{
ChangeType = "updated,deleted",
NotificationUrl = $"{_notificationHost}/listen",
Resource = "me/events",
ClientState = Guid.NewGuid().ToString(),
IncludeResourceData = false,
// Subscription only lasts for one hour
ExpirationDateTime = DateTimeOffset.UtcNow.AddHours(1)
};
It does not send notification when accept or reject done by attendee with 'Do not send a response'. How I can get notification for this?
I managed both scenarios :
I am getting notification for attendees's reject/approve response on my listen api : NotificationUrl = $"{_notificationHost}/listen",
If new attendess added/removed or meeting time change or meeting cancelled then also we can get notification in Listen api.
Following this walkthrough, I'm able to get a "subscription" on my Azure Function to a users mailbox.
However when I modify it to try to access a users onedrive, I'm able to access their files in the app, but when attempting to subscribe for a webhook I get the error below. I verified my token has Files.ReadWrite.All permission so I don't understand what I'm missing.
[2020-11-19T16:17:12.327Z] Executed 'SetDocSubscription' (Failed, Id=01410f60-0954-4e37-b9aa-2940cf9d0a17, Duration=2177ms)
[2020-11-19T16:17:12.330Z] System.Private.CoreLib: Exception while executing function: SetDocSubscription. Microsoft.Graph.Core: Code: ExtensionError
[2020-11-19T16:17:12.331Z] Message: Operation: Create; Exception: [Status Code: Forbidden; Reason: Access denied]
[2020-11-19T16:17:12.332Z] Inner error:
[2020-11-19T16:17:12.333Z] AdditionalData:
[2020-11-19T16:17:12.334Z] date: 2020-11-19T16:17:11
[2020-11-19T16:17:12.335Z] request-id: ccd648e7-b3fc-43f6-b1c5-481cbb5dcab6
[2020-11-19T16:17:12.336Z] client-request-id: ccd648e7-b3fc-43f6-b1c5-481cbb5dcab6
[2020-11-19T16:17:12.337Z] ClientRequestId: ccd648e7-b3fc-43f6-b1c5-481cbb5dcab6
...additional detail, calling function looks like this:
// Create a new subscription object
var subscription = new Subscription
{
ChangeType = "updated",
NotificationUrl = $"{notificationHost}/api/DocsNotify",
Resource = $"/users/{payload.UserId}/drive/root/",
ExpirationDateTime = DateTimeOffset.UtcNow.AddDays(2),
ClientState = Notify.ClientState
};
// POST /subscriptions
var createdSubscription = await graphClient.Subscriptions
.Request()
.AddAsync(subscription);
return new OkObjectResult(createdSubscription);
I am suspecting you're exceeding the limits here. When any limit (it can be Azure AD resource limitation as well) is
exceeded, attempts to create a subscription will result in an error
response - 403 Forbidden.
You can see the above error. In your
scenario, you will see the message property which will explain which
limit has been exceeded.
Here's the related doc.
in the Graph Beta Subscriptions API, there is a property that can be set or got on the Subscription object called AdditionalData.
I am trying to use this when creating a subscription to transport data that will be sent back with change notifications and provide more context to my task.
I am finding though that even though I set the property, it does not keep my added dictionary items but replaces with its own additional data.
Not sure if I am using this property for something that I shouldn't be or whether this is a bug or am I just setting it wrong? I am doing something like this:
var subscription = new Subscription
{
Resource = $"users/{userObjectId}/mailFolders('{resource}')/messages",
ChangeType = "created",
NotificationUrl = notificationWebHookUrl,
LifecycleNotificationUrl = lifecycleNotificationWebHookUrl,
AdditionalData = new Dictionary<string, object> { { "test", "123"} },
ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 4200, 0)
};
The subscription object doesn't support storing additional data. You can have custom data passed back to your notification URL when notifications are being delivered two ways:
You can include some of that data in the clientState, although this property is designed to be used as a security feature, you can put anything you want in there.
Any query string parameter included in the notification URL will be passed back when notifications are being delivered. I just authored a pull request to add that missing information.
I am trying to get all the events in a Conference room's calendar with Microsoft graph API, given a startDateTime and endDateTime. I tried the following API's -
1. https://graph.microsoft.com/v1.0/users/{id}/events?startDateTime=2017-03-20T05:00:00.0000000&endDateTime=2017-04-06T21:00:00.0000000
2.https://graph.microsoft.com/v1.0/users/{id}/calendar/calendarView?startDateTime=2017-03-20T05:00:00.0000000&endDateTime=2017-04-06T21:00:00.0000000
The response includes all events with isCancelled=false. How do I fetch events which were Cancelled?
&$filter=isCancelled%20eq%20true also returned empty as there are no events with isCancelled=true in response
By design, when an event is canceled, it is deleted from the calendar. So, there isn't a way today to query list of events that are deleted. We have an item in our backlog for supporting deleted events, but no timeline.
I just tried
https://graph.microsoft.com/v1.0/Users/xx/Calendar/Events/xx/instances
?startDateTime=2020-02-17&endDateTime=2020-03-01&isCancelled=true
and what came back are all the non-cancelled events...
Ditto isCancelled=false
leaving off the &isCancelled=xx parameter returns all the events with the isCancelled attribute set to false for everything.
I was trying to fetch event after getting notification when event cancelled. I am trying with notification resource url and it throws exception:
Microsoft.Graph.ServiceException: Code: ErrorItemNotFound.
What I was expecting that response with notfound status code rather than throwing exception.
My code for fetching event:
var request = new EventRequest(
$"{_graphClient.BaseUrl}/{notification.Resource}",
graphClientApp,
null);
var message = await request.GetResponseAsync();
var response = await message.GetResponseObjectAsync();
Situation
I have a messaging Android app providing following feature(s)
to send message direct message to one selected recipient
to create public announcement that all users using the app receive (except author)
each user sees on his phone a list of messages he got
each message is either unread, read, or deleted
I use Parse.com as back-end
Current implementation
On Android client
When there is a new message, a new messageRequest of the MessageRequest class is created
If the message should be direct, the messageRequest has type 0, otherwise type 1
If the message is direct, there is recipient stored in the messageRequest object
The messageRequest object is stored to parse.com
On parse.com back-end
In the afterSave of the MessageRequest it is checked if the message is direct or public and based on that
in case of direct message - one new message object of the Message class is created and saved
in case of public announcement - for each user except author, a new message object is created and added to a list of messages, then the list is saved
In both cases, the data like content, type, etc. are copied from messageRequest object into the newly created message object(s).
The reason for creating separate message for each user is that each user can have it in another status (unread, read, deleted).
The status column representing the unread, read, deleted status is set (by unread) for the message object.
Problem
When I call the ParseObject.saveAll method in the afterSave of MessageRequest, I get the Execution timed out - Request timed out error
I think the cause is that there are some limits on time in which the request must complete
in cloud code. In my case, I'm creating ca 100 Messages for 1 MessageRequest
This doesn't seem so much to me, but maybe I'm wrong.
Source code
var generateAnnouncement = function(messageRequest, recipients) {
var messageList = [];
for (var i = 0; i < recipients.length; i++) {
var msg = new Message();
msg.set("type", 1);
msg.set("author", messageRequest.get("author"));
msg.set("content", messageRequest.get("content"));
msg.set("recipient", recipients[i]);
msg.set("status", 0)
messageList.push(msg);
}
Parse.Object.saveAll(messageList).then(function(list) {
}, function(error) {
console.error(error.message);
});
}
Parse.Cloud.afterSave("MessageRequest", function(request) {
var mr = request.object;
var type = mr.get("type");
if (type == 0) {
generateDirectMessage(mr);
} else {
var query = new Parse.Query(Parse.User);
query.notEqualTo("objectId", mr.get("author").id);
query.find().then(function(allUsersExceptAuthor) {
generateAnnouncement(mr, allUsersExceptAuthor);
}, function(error) {
console.error(error.message);
});
}
});
How would you suggest to solve this?
Additional thoughts
My only other idea how to solve this is to have only one Message object, and two columns called e.g. viewedBy and deletedFor which would contain lists of users that already viewed the message or have delete the message for them.
In this case, I'm not very sure about the performance of the queries
Also, I know, many of you think Why isn't he using table for splitting the M:N relation between the MessageRequest(which could be actually called Message in that case) and User?
My answer is that I had this solution, but it was harder to work with it in the Android code, more pointers, more includes in queries, etc.
Moreover, I would have to create the same amount of objects representing status for each user in the on parse.com back-end anyway, so I think the problem with Execution time out would be the same in the end
Update - mockup representing user's "Inbox"
In the "inbox" user sees both direct messages and public announcements. They are sorted by chronological order.
Update #2 - using arrays to identify who viewed and who marked as deleted
I have just one Message object, via type I identify if it is direct or public
Two array columns were added
viewedBy - containing users that already viewed the message
deletedFor - containing users that marked the message as deleted for them
Then my query for all messages not deleted by currently logged in user looks like this
//direct messages for me
ParseQuery<Message> queryDirect = ParseQuery.getQuery(Message.class);
queryDirect.whereEqualTo("type", 0);
queryDirect.whereEqualTo("recipient", ParseUser.getCurrentUser());
//public announcements
ParseQuery<Message> queryAnnouncements = ParseQuery.getQuery(Message.class);
queryAnnouncements.whereEqualTo("type", 1);
//I want both direct and public
List<ParseQuery<Message>> queries = new ArrayList<ParseQuery<Message>>();
queries.add(queryDirect);
queries.add(queryAnnouncements);
ParseQuery<Message> queryMessages = ParseQuery.or(queries);
//... but only those which I haven't deleted for myself
queryMessages.whereNotEqualTo("deletedFor", ParseUser.getCurrentUser());
//puting them in correct order
queryMessages.addDescendingOrder("createdAt");
//and attaching the author ParseUser object (to get e.g. his name or URL to photo)
queryMessages.include("author");
queryMessages.findInBackground(new FindCallback<Message>() {/*DO SOMETHING HERE*/});
I would suggest changing your schema to better support public messages.
You should have a single copy of the public message, as there's no changing the message itself.
You should then store just the status for each user if it is anything other than "unread". This would be another table.
When a MessageRequest comes in with type 1, create a new PublicMessage, don't create any status rows as everyone will use the default status of "unread". This makes your afterSave handler work cleanly as it is always creating just one new object, either a Message or a PublicMessage.
As each user reads the message or deletes it, create new PublicMessageStatus row for that user with the correct status.
When showing public messages to a user, you will do two queries:
Query for PublicMessage, probably with some date range
Query for PublicMessageStatus with a filter on user matching the current user and matchesQuery('publicMessage', publicMessageQuery) constraint using a clone of the first query
Client side you'll then need to combine the two to hide/remove those with status "deleted" and mark those with status "read" accordingly.
Update based on feedback
You could choose instead to use a single Message class for public/private messages, and a MessageStatus class to handle status.
Public vs Private would be based on the Message.recipient being empty or not.
To get all messages for the current user:
// JavaScript sample since you haven't specified a language
// assumes Underscore library available
var Message = Parse.Object.extend('Message');
var MessageStatus = Parse.Object.extend('MessageStatus');
var publicMessageQuery = new Parse.Query(Message);
publicMessageQuery.doesNotExist('recipient');
publicMessageQuery.notEqualTo('author', currentUser);
var privateMessageQuery = new Parse.Query(Message);
privateMessageQuery.equalTo('recipient', currentUser);
var messagesQuery = new Parse.Query.or(publicMessageQuery, privateMessageQuery);
messagesQuery.descending('createdAt');
// set any other filters to apply to both queries
var messages = [];
messageQuery.find().then(function(results) {
messages = _(results).map(function (message) {
return { message: message, status: 'unread', messageId: message.objectId };
});
var statusQuery = new Parse.Query(MessageStatus);
statusQuery.containedIn('message', results);
statusQuery.equalTo('user', currentUser);
// process status in order so last applies
statusQuery.ascending('createdAt');
return
}).then(function(results) {
_(results).each(function (messageStatus) {
var messageId = messageStatus.get('message').objectId;
_(messages).findWhere({ messageId: messageId }).status = messageStatus.get('status');
});
});
// optionally filter messages that are deleted
messages = _(messages).filter(function(message) { return message.status !== 'deleted'; });
// feed messages array to UI...