Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local webhook testing support #145

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ This section contains changes that have been committed but not yet released.

### Added

* Added local webhook testing support
* Added new enums for `Webhook.Triggers` and `Webhook.State`

### Changed

### Deprecated
Expand Down
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ dependencies {

// SLF4J for logging facade
implementation('org.slf4j:slf4j-api:1.7.30')


// Websocket dependency
implementation('org.java-websocket:Java-WebSocket:1.5.3')

///////////////////////////////////
// Test dependencies

Expand Down
55 changes: 55 additions & 0 deletions src/main/java/com/nylas/Webhook.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Webhook extends RestfulModel {

Expand All @@ -11,6 +13,49 @@ public class Webhook extends RestfulModel {
private String state;
private List<String> triggers;
private String version;

/**
* Enumeration containing the different webhook triggers
*/
public enum Trigger {
AccountConnected("account.connected"),
AccountRunning("account.running"),
AccountStopped("account.stopped"),
AccountInvalid("account.invalid"),
AccountSyncError("account.sync_error"),
MessageBounced("message.bounced"),
MessageCreated("message.created"),
MessageOpened("message.opened"),
MessageUpdated("message.updated"),
MessageLinkClicked("message.link_clicked"),
ThreadReplied("thread.replied"),
ContactCreated("contact.created"),
ContactUpdated("contact.updated"),
ContactDeleted("contact.deleted"),
CalendarCreated("calendar.created"),
CalendarUpdated("calendar.updated"),
CalendarDeleted("calendar.deleted"),
EventCreated("event.created"),
EventUpdated("event.updated"),
EventDeleted("event.deleted"),
JobSuccessful("job.successful"),
JobFailed("job.failed");

private final String name;

Trigger(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

public enum State {
ACTIVE,
INACTIVE;
}

public String getApplicationId() {
return application_id;
Expand Down Expand Up @@ -40,10 +85,20 @@ public void setState(String state) {
this.state = state;
}

public void setState(State state) {
this.state = state.toString().toLowerCase();
}

public void setTriggers(List<String> triggers) {
this.triggers = triggers;
}

public void setTriggers(Trigger... triggers) {
this.triggers = Stream.of(triggers)
.map(Trigger::name)
.collect(Collectors.toList());
}

@Override
public String toString() {
return "Webhook [application_id=" + application_id + ", callback_url=" + callback_url + ", state=" + state
Expand Down
192 changes: 192 additions & 0 deletions src/main/java/com/nylas/services/Tunnel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package com.nylas.services;

import com.nylas.*;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Sets up and registers a websocket connection for local webhook testing
*/
public class Tunnel extends WebSocketClient {

private final NylasApplication app;
private final String tunnelId;
private final WebhookHandler webhookHandler;
private final List<String> triggers;
private final String region;
private static final String websocketDomain = "tunnel.nylas.com";
private static final String callbackDomain = "cb.nylas.com";
private static final Logger log = LoggerFactory.getLogger(Tunnel.class);

public Tunnel(Builder builder) throws URISyntaxException {
super(new URI("wss://" + websocketDomain));
this.webhookHandler = builder.webhookHandler;
this.app = builder.app;
this.tunnelId = UUID.randomUUID().toString();
this.triggers = builder.triggers;
this.region = builder.region;
this.setHeaders();
}

/**
* {@inheritDoc}
* Also registers the webhook with the Nylas API.
*/
@Override
public void connect() {
super.connect();
try {
registerWebhookCallback(app, tunnelId, triggers);
} catch (RequestFailedException | IOException e) {
log.trace("Error encountered while trying to register webhook with the Nylas API");
throw new RuntimeException(e);
}
}

/**
* {@inheritDoc}
* Calls {@link WebhookHandler#onOpen(short)}
*/
@Override
public void onOpen(ServerHandshake handshakedata) {
log.trace("Opening websocket connection");
webhookHandler.onOpen(handshakedata.getHttpStatus());
}

/**
* {@inheritDoc}
* Calls {@link WebhookHandler#onMessage(Notification)}
*/
@Override
public void onMessage(String message) {
log.trace("Messaged received from websocket");
Map<String, Object> response = JsonHelper.jsonToMap(message);
String jsonBody = (String) response.get("body");
if(jsonBody == null || jsonBody.isEmpty()) {
log.trace("Not a valid delta response, skipping.");
return;
}

// Parse notification from JSON body and call onMessage callback
Notification notification = Notification.parseNotification(jsonBody);
webhookHandler.onMessage(notification);
}

/**
* {@inheritDoc}
* Calls {@link WebhookHandler#onClose(int, String, boolean)}
*/
@Override
public void onClose(int code, String reason, boolean remote) {
log.trace("Closing websocket connection");
webhookHandler.onClose(code, reason, remote);
mrashed-dev marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* {@inheritDoc}
* Calls {@link WebhookHandler#onError(Exception)}
*/
@Override
public void onError(Exception ex) {
log.trace("Error encountered during websocket connection");
webhookHandler.onError(ex);
mrashed-dev marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Sets the headers necessary for setting up the websocket tunnel
*/
private void setHeaders() {
this.addHeader("Client-Id", app.getClientId());
this.addHeader("Client-Secret", app.getClientSecret());
this.addHeader("Tunnel-Id", tunnelId);
this.addHeader("Region", region);
}

/**
* Registers the websocket connection and the callback with your Nylas application
* @param app The configured Nylas application
* @param tunnelId The UUID generated for the websocket tunnel
* @param triggers The triggers to subscribe to
*/
private void registerWebhookCallback(NylasApplication app, String tunnelId, List<String> triggers)
throws RequestFailedException, IOException {
Webhook webhook = new Webhook();
webhook.setCallbackUrl(String.format("https://%s/%s", callbackDomain, tunnelId));
webhook.setState(Webhook.State.ACTIVE);
webhook.setTriggers(triggers);
app.webhooks().create(webhook);
}

/**
* A builder for {@link Tunnel}
*/
public static class Builder {
private final NylasApplication app;
private final WebhookHandler webhookHandler;
private String region = "us";
private List<String> triggers = convertTriggersToString(Webhook.Trigger.values());

public Builder(NylasApplication app, WebhookHandler webhookHandler) {
this.app = app;
this.webhookHandler = webhookHandler;
}

/**
* Set the region to configure the websocket to
* @param region The Nylas region
* @return The builder with the region set
*/
public Builder region(String region) {
this.region = region;
return this;
}

/**
* Set the webhook trigger(s) to subscribe to
* @param triggers The webhook trigger(s) to subscribe to
* @return The builder with the trigger(s) set
*/
public Builder triggers(Webhook.Trigger... triggers) {
this.triggers = convertTriggersToString(triggers);
return this;
}

/**
* Builds the Tunnel
* @return The configured Tunnel
*/
public Tunnel build() throws URISyntaxException {
return new Tunnel(this);
}

/**
* Helper method that converts a list of triggers to their string values
* @param triggers The list of triggers to convert
* @return A list of strings with the value of the provided triggers
*/
private static List<String> convertTriggersToString(Webhook.Trigger[] triggers) {
return Stream.of(triggers)
.map(Webhook.Trigger::getName)
.collect(Collectors.toList());
}
}

/**
* An interface for implementing classes to handle events from the {@link Tunnel}
*/
public interface WebhookHandler {
void onOpen(short httpStatusCode);
void onClose(int code, String reason, boolean remote);
void onMessage(Notification notification);
void onError(Exception ex);
}
}