Search code examples
spring-bootgoogle-oauthgoogle-calendar-api

How to Sync Google Calendar Events Using Push Notifications?


I have completed implementing the watch function and successfully implemented receiving push notification as well. However, I want to synchronize the calendar after receiving the push notification, but I failed here.

My Application is a web server application, so I implemented google oauth2 login and callback using servlet.

The document I referred to for implementing the servlet.

I'll provide explanations along with the code.

Google OAuth2 Login

@WebServlet(urlPatterns = "/oauth2Login")
public class GoogleAuthenticationServlet extends AbstractAuthorizationCodeServlet {
    @Override
    protected AuthorizationCodeFlow initializeFlow() {
        return GoogleApiConfig.initializeFlow();
    }

    @Override
    protected String getRedirectUri(HttpServletRequest httpServletRequest) {
        return GoogleApiConfig.getRedirectUri(httpServletRequest);
    }

    @Override
    protected String getUserId(HttpServletRequest httpServletRequest) {
        return GoogleApiConfig.getClientId(httpServletRequest);
    }
}

Callback

@WebServlet(urlPatterns = "/oauth2callback")
public class Oauth2CallbackServlet extends AbstractAuthorizationCodeCallbackServlet {

    @Override
    protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
            throws IOException {
        String userId = req.getSession().getId();
        resp.sendRedirect("/google/watch?userId=" + userId + "&accessToken=" + credential.getAccessToken());
    }

    ...


    @Override
    protected String getUserId(HttpServletRequest var1) {
        return GoogleApiConfig.getClientId(var1);
    }
}

watch

    @GetMapping("/google/watch")
    public ResponseEntity<Channel> watchCalendar(@RequestParam String userId,
                                                 @RequestParam String accessToken) {
        accessToken = "Bearer " + accessToken;
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", accessToken);

        Channel channel = adateService.executeWatchRequest(userId);
        return ResponseEntity.ok()
                .headers(headers)
                .body(channel);
    }

push notification

    @PostMapping("/notifications")
    public ResponseEntity<List<GoogleCalendarEventResponse>> printNotification(@RequestHeader(WebhookHeaders.RESOURCE_ID) String resourceId,
                                                                               @RequestHeader(WebhookHeaders.RESOURCE_URI) String resourceUri,
                                                                               @RequestHeader(WebhookHeaders.CHANNEL_ID) String channelId,
                                                                               @RequestHeader(WebhookHeaders.CHANNEL_EXPIRATION) String channelExpiration,
                                                                               @RequestHeader(WebhookHeaders.RESOURCE_STATE) String resourceState,
                                                                               @RequestHeader(WebhookHeaders.MESSAGE_NUMBER) String messageNumber,
                                                                               HttpServletRequest request) {

        log.info("Request for calendar sync, channelId=" + channelId + ", expiration=" + channelExpiration + ", messageNumber=" + messageNumber);
        String userId = request.getSession().getId();
        adateService.listEvents(userId);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

I checked the log of watch and push notification and it was successful.

watch log

{
  "expiration": 1714380331000,
  "id": "<id>",
  "kind": "api#channel",
  "resourceId": "<resourceId>",
  "resourceUri": "https://www.googleapis.com/calendar/v3/calendars/primary/events?alt=json",
  "token": "tokenValue"
}

push notification log

2024-03-30T08:06:08.102Z  INFO 957 --- [nio-8080-exec-1] c.f.d.a.c.i.GoogleCalendarControllerImpl : Request for calendar sync, channelId=<calendar-id>, expiration=Mon, 29 Apr 2024 06:30:11 GMT, messageNumber=<message-number>

events sync

    @Transactional
    public void listEvents(String sessionId) {
        try {
            Calendar calendar = googleApiConfig.calendarService(sessionId);
            Calendar.Events.List request = calendar.events().list("primary");
            String syncToken = getNextSyncToken();
            List<Event> events;
            if (syncToken == null) {
                events = request.execute().getItems();
            } else {
                request.setSyncToken(syncToken);
                events = request.execute().getItems();
                googleApiConfig.getSyncSettingsDataStore().set(SYNC_TOKEN_KEY, syncToken);
            }
            syncEvents(events);
        } catch (IOException e) {
            throw new AdateIOException(e);
        }
    }

---

    public Calendar calendarService(String sessionId) {
        Credential credential = getCredentials(sessionId);
        return new Calendar.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
                .setApplicationName(APPLICATION_NAME)
                .build();
    }

---

    public DataStore<String> getSyncSettingsDataStore() {
        return syncSettingsDataStore;
    }

I initialized syncSettingsDataStore using the @PostConstruct annotation

syncSettingsDataStore = GoogleApiConfig.getDataStoreFactory().getDataStore("SyncSettings");

After reviewing the log, it appears that an error occurred within the event sync code and the following error message has been identified.

error log

403 Forbidden<EOL>GET https://www.googleapis.com/calendar/v3/calendars/primary/events<EOL>{<EOL>  "code" : 403,<EOL>  "errors" : [ {<EOL>    "domain" : "global",<EOL>    "message" : "Method doesn't allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API.",<EOL>    "reason" : "forbidden"<EOL>  } ],<EOL>  "message" : "Method doesn't allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API.",<EOL>  "status" : "PERMISSION_DENIED"<EOL>}]

After checking the error code, I found the message 'Please use API Key or other form of API,' which led me to consider passing the accessToken as a parameter. However, I couldn't figure out how to pass the accessToken as a parameter for push notifications, so I couldn't implement it.

I've read the post, but I'm still having trouble understanding it.

it driving me CRAZY...!!!!

I'm still new to posting questions on Stack Overflow, so if there's any incorrect information or if you need additional details, please feel free to leave a comment.

My service is a web application, so I referred to the following document.


Solution

  • The issue was that the project's redirect uri wasn't added to the redirect_uris of the API key. Once I added it, the problem was resolved.

    I won't delete the post because there might be someone who has the same problem as me.