GoogleCalendarAPI accept/decline event - google-api

I am working on GoogleCalendar API and using node.js as a platform to build my application.
I am able to create events using authentication procedure and creating the calendar event using the access token generated while authenticating.
My question is that suppose if we have any attendee in the event and I want to accept/decline the event using the calendar API from the attendee's side, how can we do that?
I have tried fetching the calendar event of the attendee and matching it with the iCalUID of the event which was originally created and then modifying the event using update event on the attendee's calendar.

Event creator or owner cannot modify the response of attendees. Only attendees can modify their status.
To update the status on the side of the user, You may use the Event.update API and provide value for 'attendees.responseStatus'. Attendee's response status has 4 (four) possible value (described below).
'needsAction' - has not responded to the invitation.
'declined' - has declined the invitation.
'tentative' - has tentatively accepted the invitation
'accepted' - has accepted the invitation.
In addition to this, You can use the word "primary" as value for the calendar id to represent the currently logged in user
CalendarId: Calendar identifier. To retrieve calendar IDs call the calendarList.list method. If you want to access the primary calendar of the currently logged in user, use the "primary" keyword. (string).
For the id, you need to use the "id" returned by the Events.list API not the "iCalUID". Those two are different as described here.
Other fields that you need to provide are the email (of the attendee), startdate and enddate.
For more information, you may view the official documentation, link below:
https://developers.google.com/google-apps/calendar/v3/reference/events

Here is an example in java, using PATCH. Create an event object with the just the information you want to change, in this case the attendee and the response status. This code is running as the attendee.
final Event event = new Event()
.setAttendees(Arrays.asList(new EventAttendee().setEmail(email)
.setResponseStatus("declined")));
try
getCalendarService(googleAccountCredential).events()
.patch(CALENDAR_PRIMARY, calendarEventId, event)
.setSendNotifications(true)
.setOauthToken(googleAccountCredential.getToken()).execute();
return true;
} catch (final Exception ex) {
...
return false;
}
}

Like Android Enthusiast discussed, only the attendee can update his or her calendar from the attendee's side. You should also check the documentation as he suggested. The answer below is a working example for node.js and python
To update the event, you need to have the eventId and the user email. Get the event from the calendar(with the eventID),
loop through all the attendees, change responseStatus for
that particular attendee and then update the google calendar
For node js using the google api
const { google } = require('googleapis');
const calendar = google.calendar({ version: 'v3', auth: 'YOUR-API-KEY-HERE' });
#get the event to be updated
let theEvent = calendar.events.get({ calendarId: 'primary', eventId: eventId })
#loop through the whole attendee
for (let i = 0, i < theEvent['atendees'].length; i++){
if (theEvent['atendees'][i]['email'] === userEmail){
theEvent['atendees'][i]['responseStatus'] = 'accepted'
}
}
#update the google event
calendar.events.update({ calendarId: 'primary', eventId: theEventId, body: theEvent}, function(err, event) {
  if (err) {
    console.log('there was an error');
    return;
  }
  console.log('Event updated');
});
For python using googleapiclient
from googleapiclient.discovery import build
calendar = build('calendar', 'v3', credentials=credential)
event = calendar.events().get(calendarId='primary', eventId='eventId').execute()
For attendee in event['atendees']:
if atendee['email'] == user_email:
attendee['responseStatus'] = 'accepted'
break
#update the google event
udated_event = calendar.events().update(calendarId='primary', eventId=eventId, body=event).execute()

Lets suppose that you already have the event payload with the attendees key, then you need to get the ID for the created event:
created_event = gcal_service.events().insert(
calendarId='primary', body=event_payload
).execute()
then copy the attendees in a new object
accepted_attendees = {}
accepted_attendees['attendees'] = event_payload['attendees'].copy()
and now what you need to do is submit a patch to the attendees calendar based on the event_id, like this:
for attendee in event_payload['attendees']:
attendee_pos = accepted_attendees['attendees'].index(attendee)
accepted_attendees['attendees'].pop(attendee_pos)
accepted_attendees['attendees'].append({
'email': attendee['email'],
'self': True,
'responseStatus': 'accepted',
'additionalGuests': 0,
})
gcal_service.events().patch(
calendarId='primary',
eventId=created_event['id'],
body=accepted_attendees
)
And that's all, all the other attendees, now have accepted the event, hope it helps.

To respond, you need to get the event with the same event id from the attendee's calendar and then perform a patch or an update operation changing the response status of this attendee from needsAction to accepted / declined.
A bit of documentation on how events are copied between attendees and organizers:
https://developers.google.com/google-apps/calendar/concepts/sharing

Here is an example in python for Google Calendar Api v3. You can either use update or patch. Both of them are working.
all_attendees = event['attendees']
event['attendees'] = [{
'email': 'you#example.com',
'self': True,
'responseStatus': 'accepted',
'additionalGuests': 0,
}]
updated_event = service.events().patch(calendarId=calendar_id, eventId=event_id, body=event).execute()
Have fun

Related

Google API is not returning the events created from UI

I am using googleapis in NodeJS to create & fetch the calendar events. I am using the following method to get the list of events.
const getEvents = async (dateTimeStart, dateTimeEnd,timeZone) => {
console.log("Date Start : " + dateTimeStart + " date end :" + dateTimeEnd + " time zone " + timeZone);
try {
let response = await calendar.events.list({
auth: auth,
calendarId: CALENDER_ID,
timeMin: (new Date(dateTimeStart)).toISOString(),
timeMax: (new Date(dateTimeEnd)).toISOString(),
timeZone: timeZone,
singleEvents: true,
maxResults: 9999,
orderBy: 'startTime
});
let items = response['data']['items'];
console.log(items);
return items;
} catch (error) {
console.log(`Error at getEvents --> ${error}`);
return 0;
}
};
The above method returns only events that are created programmatically via googleapis. If I create the events directly on the calendar from the browser this does not return those events.
Any idea how to fetch all events even if they are created from browser.
Based on what you were explaining about the behavior of the events being created by the service account instead of the actual users I think the problem is that the events created through API are being created under the Calendar ID of the service account, and the ones created by the users through API may have a different Calendar ID, therefore when you try to get the list of events, since you are probably using the Calendar ID from the service account you get only those events created using the API and not the ones created by the users through the web UI.
In this case it may be necessary to make sure that every event is being created under the exact same calendar ID through the web UI and the API so that all the events no matter if they were created through the API or web UI get listed as expected.
Let me know if this is useful, otherwise I can edit the response to add more clarification depending on your specific situation.

Field Service: correct way to cancel a BookableResourceBooking via SDK/API?

The BookableResourceBooking entity is documented here:
https://learn.microsoft.com/en-us/dynamics365/customerengagement/on-premises/developer/entities/bookableresourcebooking
I'd like to able to cancel a booking but I can't seem to find any SDK or API docs that explain how to do so. Would changing the bookingstatus value to "canceled" be sufficient to cancel a booking? Where would I input the reason code?
You can do this using SDK as well as API.
When you see Bookable Resource Booking in CRM, you can see Deactivate button. Clicking on it will deactivte the Bookable Resource Booking.
Now there is one more clean way to manage data, you can set Booking status to cancelled and then deactivate record in this way you can capture complete data as why Bookable Resource Booking record is cancelled/deactivated.
var entity = {};
entity["bookingstatus#odata.bind"] = "/bookingstatuses(bbda588b-013a-eb11-a813-000d3a25bbe9)"; /* cancelled booking status ID*/
entity.statecode = 1; /*Inactive*/
entity.statuscode = 2; /*Inactive*/
Xrm.WebApi.online.updateRecord("bookableresourcebooking", "bbda588b-013a-eb11-a813-000d3a25bbe9", entity).then(
function success(result) {
var updatedEntityId = result.id;
},
function(error) {
Xrm.Utility.alertDialog(error.message);
}
);

Microsoft BotFramework-WebChat is getting two welcome messages

I am using code based on https://github.com/Microsoft/BotFramework-WebChat/blob/master/samples/15.d.backchannel-send-welcome-event/index.html
When I load the web page I get two of the welcome messages. Looking at the console output of my bot I can see two conversation updates happening.
This doesn't happen with the Bot framework emulator, which only shows one welcome message.
The only place where my code differs from the sample is in rendering:
window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({ token }),
store,
styleOptions,
userID: guid(),
}, document.getElementById('webchat'));
Why is this hapening? Why is the web channel sending two "join" events for the user?
My code handling conversation updates looks like this:
} else if (turnContext.activity.type === ActivityTypes.ConversationUpdate) {
if (DEBUG) { console.log("ConversationUpdate"); }
// Do we have any new members added to the conversation?
if (turnContext.activity.membersAdded.length !== 0) {
// Iterate over all new members added to the conversation
for (var idx in turnContext.activity.membersAdded) {
console.log(turnContext.activity.membersAdded);
// Greet anyone that was not the target (recipient) of this message
// the 'bot' is the recipient for events from the channel,
// turnContext.activity.membersAdded == turnContext.activity.recipient.Id indicates the
// bot was added to the conversation.
if (turnContext.activity.membersAdded[idx].id != turnContext.activity.recipient.id) {
if (DEBUG) {console.log("Starting MASTER_DIALOG");}
const user = await this.userProfile.get(turnContext, {});
user.id = this.guid();
await this.userProfile.set(turnContext, user);
await this.userState.saveChanges(turnContext);
return await dialogContext.beginDialog(MASTER_DIALOG)
}
}
}
}
Using the ConversationUpdate event for sending a welcome message is not recommended. Read more about how to properly send a greeting message.
There will be two ConversationUpdate events per connection. One for when bot joins the conversation and one for when a (human) user joins the conversation. In your current code you are iterating over all new members, where you have to filter out the bot itself.
A better option would be to make use of a custom event sent using the backchannel. In the example you mention, you already have this functionality. It will sent a new event webchat/join to your bot, which even includes the browser language by default.

Get conversation ID in directline bot framework inline

I am using directline for webchat.
I want to include a refresh button on top of the chat and for that I need the conversation ID. How can I get the ID? Is it possible by using inline webchat ?
This the refresh button that I am trying to implement
I was facing the same problem, as I wanted to pass the conversation ID to my custom controller for initial authentication and correspondingly, push a custom auth data to the conversation stack of the bot framework relating to that specific conversation ID.
My hunting got me to this issue post on Github:
In the 3rd post by inmarktech he mentions the below code:
var params = BotChat.queryParams(location.search);
var my_token = params['my_token'];
var botConnection = new BotChat.DirectLine({
secret: 'DIRECTLINE_SECRET'
});
BotChat.App({
botConnection: botConnection
,user: { id: 'USER_ID', name: 'User' } // user.id auto updates after first user message
}, document.getElementById("bot"));
botConnection.connectionStatus$.subscribe(function (status) {
if (status == 2) { // wait for connection is 'OnLine' to send data to bot
var convID = botConnection.conversationId;
botConnection.postActivity({
from: { id: convID } // because first time user ID == conversation ID
,type: 'event'
,name: 'registerUserData' // event name as we need
,value: my_token // data attached to event
}).subscribe(function (activityId) {
// This subscription is a MUST
// If I remove this handler the postActivity not reaches the bot
});
}
});
as you can see he is subscribing to botConnection.connectionStatus$ and as and when the status property is equal to 2(Online) you can then fetch the Conversation ID from the botConnection object.
Hope that helps :)

Updating an Appointment causes it to change to a Meeting in EWS 1.1

Here's what I'm trying to do:
get all items on a user's calendar between two dates
update the Location or Subject for some items
I get the items with:
FindItemsResults<Appointment> findResults = calendar.FindAppointments(new CalendarView(startDate, endDate));
This query works fine. But whenever I call Update to save the item I get an exception:
Microsoft.Exchange.WebServices.Data.ServiceResponseException: One or more recipients are invalid.
Even though I get an exception, the item is saved and gets changed to have IsMeeting set to true! Now the updated item is a meeting with an organizer etc... This is, effectively, data corruption for me.
Here's the code. It is no more complicated than this. I've tested it by just changing Location or Subject and both cause the problem.
Appointment a = Appointment.Bind(_service, new ItemId(id));
a.Location = newLocation
a.Update(ConflictResolutionMode.AlwaysOverwrite);
Am I missing some concept or something? This seems like a pretty egregious problem.
FWIW, this is EWS 1.1 against an Office 365 server.
I figured it out with help from this Question:
Exchange Appointment Types
The key is the Update method needs to be called with the SendInvitationsOrCancellationsMode.SendToNone flag set in the 2nd parameter.
Like this:
a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToNone);
So tig's answer works when you never want to send out appointment updates to the other attendees. However to answer this properly you actually need to get the attendee state loaded.
By default it is trying to send appointment updates to the attendees, however your appointment object doesn't have the attendee state loaded and is hence blowing up. When you do the bind you should load the attendee properties. You should probably also load the organizer as well to cover another edge case:
AppointmentSchema.RequiredAttendees
AppointmentSchema.OptionalAttendees
AppointmentSchema.Resources
AppointmentSchema.Organizer
This will get the attendees populated if you want to do an update that sends out updates to the attendees.
However there is then another edge case that you have to worry about. If you have an appointment with no attendees added to it (just the organizer), then EWS may still complain and throw this error. It will actually work for appointments in some states, but fail in other states.
So the most complete solution is a combination of:
Loading the attendee state.
Inspecting the attendee state to see if there are any attendees other than the organizer (depending on how the appointment was created the organizer may or may not appear in the RequiredAttendees collection). If there are not then you must use SendInvitationsOrCancellationsMode.SendToNone.
So the full sample would look something like:
Appointment a = Appointment.Bind(_service, new ItemId(id), new PropertySet(AppointmentSchema.RequiredAttendees, AppointmentSchema.OptionalAttendees, AppointmentSchema.Resources, AppointmentSchema.Organizer));
a.Location = newLocation
// Check if the appointment has attendees other than the organizer. The organizer may
// or may not appear in the required attendees list.
if (HasNoOtherAttendee(a.Organizer.Address, a.RequiredAttendees) &&
(a.OptionalAttendees.Count == 0) && (a.Resources.Count == 0))
{
a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToNone);
}
else
{
// We have other attendees in the appointment, so we can use SendToAllAndSaveCopy so
// they will see the update.
a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy);
}
bool HasNoOtherAttendee(string email, AttendeeCollection attendees)
{
bool emptyOrOnlyMe = true;
foreach (var a in attendees)
{
if (!string.Equals(email, a.Address, StringComparison.OrdinalIgnoreCase))
{
emptyOrOnlyMe = false;
break;
}
}
return emptyOrOnlyMe;
}
To answer this bit of the question
"Even though I get an exception, the item is saved and gets changed to
have IsMeeting set to true! Now the updated item is a meeting with an
organizer etc... This is, effectively, data corruption for me."
The Microsoft documentation states, in the small print, "A meeting request is just an appointment that has attendees. You can convert an appointment into a meeting request by adding required attendees, optional attendees, or resources to the appointment" - as seen here
http://msdn.microsoft.com/en-us/library/office/dd633641%28v=exchg.80%29.aspx
In other words, as soon as you have any attendees, Exchange converts it to a meeting automatically.
public static bool UpdateAppointment(ExchangeCredential credentials,
ItemId appointmentId, string newLocation, string newSubject,
DateTime startTime,
DateTime endTime)
{
ExchangeService service = GetExchangeService(credentials);
try
{
Appointment appt = Appointment.Bind(service, appointmentId,
new PropertySet(BasePropertySet.IdOnly, AppointmentSchema.Start,
AppointmentSchema.ReminderDueBy, AppointmentSchema.End, AppointmentSchema.StartTimeZone,
AppointmentSchema.TimeZone));
appt.Location = newLocation;
appt.Start = startTime;
appt.End = endTime;
appt.Subject = newSubject;
// very important! you must load the new timeZone
appt.StartTimeZone = TimeZoneInfo.Local;
//appt.Body.Text = newBody; //if needed
appt.Update(ConflictResolutionMode.AlwaysOverwrite);
}
catch (Exception ex)
{
throw ex;
}
return true;
}

Resources