Java tutorial
/******************************************************************************* * Copyright (c) 2016 Julien Louette & Gal Wittorski * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package com.raspoid.network.pushbullet; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import javax.websocket.ClientEndpointConfig; import javax.websocket.CloseReason; import javax.websocket.ContainerProvider; import javax.websocket.DeploymentException; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; import javax.websocket.Session; import javax.websocket.WebSocketContainer; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import com.google.gson.Gson; import com.raspoid.Tools; import com.raspoid.exceptions.RaspoidException; import com.raspoid.network.Router; /** * <b>This class is an abstraction to easily use some of the Pushbullet services.</b> * * <p>We implemented here the main Pushbullet features that can be useful for your project. * The utilization is really simple. You can for example easily send some requests to your robots * from any of your Pushbullet devices, receive a response from your robot, or even setup a notification * system to be notified when some specific event occurs.</p> * * <p>All you need to use this Pushbullet wrapper is a Pushbullet account, * and an access token for this account.<br> * <i>This access token can easily be retrieved from your Pushbullet settings.</i></p> * * @author Julien Louette & Gaël Wittorski * @version 1.0 */ public class Pushbullet { /** * The websocket session with the pushbullet servers. */ private Session session; /** * The private token used to access pushbullet services. */ private String accessToken; /** * The Gson instance used to decode pushbullet json messages. */ private Gson gson = null; /** * Our device iden on pushbullet servers. */ private String deviceIden; /** * The last timestamp of the last received push (for us or not). * <p>This timestamp is used to ask only new pushes when asking pushes list to pushbullet.</p> */ private double lastPushReceivedTime = MIN_LAST_PUSH_RECEIVED_TIMESTAMP; /** * Needed in case of no push already received/sent by the user. */ private static final double MIN_LAST_PUSH_RECEIVED_TIMESTAMP = 1.4E+9; /** * Constructor for a new Pushbullet instance with a specific access token, a device name * corresponding to the name that your robot will take in your Pushbullet list of devices, * and the Raspoid router to use with this Pushbullet instance. * * <p>The access token can easily be retrieved from your Pushbullet account settings.</p> * * <p>Note that if a device with the specified name already exists, this device will be * retrieved. If no devices with this name exists, a new one will be created.</p> * * <p>As for other types of servers, the router is used to deal with requests * received on this Pushbullet instance.</p> * * @param accessToken the access token used to access Pushbullet services. * @param deviceName the name corresponding to your robot's Pushbullet device. * @param router the router to use to deal with requests received on the deviceName. */ public Pushbullet(String accessToken, String deviceName, Router router) { this.accessToken = accessToken; gson = new Gson(); this.deviceIden = initDevice(deviceName); this.lastPushReceivedTime = initLastPushReceivedTime(); // WebSocket final ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().build(); WebSocketContainer websocketClient = ContainerProvider.getWebSocketContainer(); try { session = websocketClient.connectToServer(new PushbulletClientEndpoint(router), clientEndpointConfig, new URI("wss://stream.pushbullet.com/websocket/" + accessToken)); } catch (DeploymentException | IOException | URISyntaxException e) { throw new RaspoidException("Error when connecting to Pushbullet server.", e); } Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "Websocket closed by client")); } catch (IOException e) { throw new RaspoidException("Error when closing the websocket session with Pushbullet.", e); } })); } /** * Creates a new device if needed and retrieve the corresponding Pushbullet device iden. * * <p>This method checks if a device named deviceName already exists in your Pushbullet devices. * <ul> * <li>If it already exists, it returns the device id.</li> * <li>If it doesn't exist, a new device with this name is created and the new device id is returned.</li> * </ul> * </p> * @param deviceName the name of the Pushbullet device corresponding to this instance of the Pushbullet wrapper. * @return the device id of the Pushbullet device named deviceName. */ private String initDevice(String deviceName) { String deviceId; List<Device> userDevices = getListOfDevices(); for (Device userDevice : userDevices) { if (userDevice.isActive() && userDevice.getNickname().equals(deviceName)) { // the device already exists, we juste need to retrieve the iden deviceId = userDevice.getIden(); Tools.log("[Pushbullet] An active device with this name already exists on pushbullet: (iden)" + deviceId); return deviceId; } } // the device doesn't exists, we need to create it and retrieve the iden Device newDevice = createNewDevice(deviceName); deviceId = newDevice.getIden(); Tools.log("New device created on pushbullet: (name)" + newDevice.getNickname() + " (iden)" + deviceId); return deviceIden; } /** * Retrieve and updates the timestamp of the last push in Pushbullet pushes of the user. */ private double initLastPushReceivedTime() { List<Push> lastPushList = getListOfPushes(MIN_LAST_PUSH_RECEIVED_TIMESTAMP, 1); if (lastPushList.isEmpty()) return MIN_LAST_PUSH_RECEIVED_TIMESTAMP; else return lastPushList.get(0).getLastModificationTimestamp(); } private <T> T deserializePushbulletEntity(String jsonRepresentation, Class<T> targetClass) { return gson.fromJson(jsonRepresentation, targetClass); } /** * Executes an Http Get request and returns the String representation * of the response from the server. * Returns null in case of problem. * @param url the url of the Get request to execute. * @return the String representation of the response from the server. */ public String sendGetRequest(String url) { HttpGet request = new HttpGet(url); request.addHeader("Access-Token", accessToken); return sendHttpRequest(request); } /** * Executes an Http Post request and returns the String representation * of the response from the server. * Returns null in case of problem. * @param url the url of the Post request to execute. * @return the String representation of the response from the server. */ public String sendPostRequest(String url) { return sendPostRequest(url, null); } /** * Executes an Http Post request with specific url parameters * and returns the String representation of the response from the server. * Returns null in case of problem. * @param url the url of the Post request to execute. * @param urlParameters the url parameters to add to the request. * @return the String representation of the response from the server. */ public String sendPostRequest(String url, List<NameValuePair> urlParameters) { HttpPost request = new HttpPost(url); request.addHeader("Access-Token", accessToken); if (urlParameters != null) try { request.setEntity(new UrlEncodedFormEntity(urlParameters, "utf-8")); } catch (UnsupportedEncodingException e) { throw new RaspoidException("[Pushbullet] Error when setting Http request entity.", e); } return sendHttpRequest(request); } /** * Upload a file on the Pushbullet servers. * @param url authorized url to post the file. * @param filePath the local path of the file to post. * @return the response from the HTTP request. */ public int postFile(String url, String filePath) { String cmd = "curl -i -X POST " + url + " -F file=@" + filePath; Process process; try { process = Runtime.getRuntime().exec(cmd); process.waitFor(); return process.exitValue(); } catch (IOException | InterruptedException e) { throw new RaspoidException("[Pushbullet] Error when uploading a file.", e); } } /** * Sends an Http request (Get or Post), and returns the String representation * of the response from the server. * @return the String representation of the response from the server. */ private String sendHttpRequest(HttpUriRequest request) { HttpClient client = HttpClientBuilder.create().build(); try { Tools.debug("[Pushbullet] Http request executed: " + request); HttpResponse response = client.execute(request); Tools.debug("[Pushbullet] Http Response Code: " + response.getStatusLine().getStatusCode()); try (BufferedReader rd = new BufferedReader( new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))) { StringBuilder result = new StringBuilder(); String line; while ((line = rd.readLine()) != null) result.append(line); return result.toString(); } } catch (IOException e) { throw new RaspoidException("Error when executing Http request.", e); } } /** * Retrieve the list of pushes received after a specific timestamp, * with a specific limit in the number of retrieved pushes. * @param modifiedAfter the timestamp from which to retrieve pushes. * @param limit the maximum number of pushes to retrieve on the Pushbullet servers. -1 if no limit. * @return the list of last limit pushes modified after the modifiedAfter timestamp, retrieved on the Pushbullet services. */ public List<Push> getListOfPushes(double modifiedAfter, int limit) { return getListOfPushes(true, modifiedAfter, limit); } /** * Retrieve the list of pushes without any limit in time, with a specific limit * in the number of retrieved pushes. * @param limit the maximum number of pushes to retrieve on the Pushbullet servers. -1 if no limit. * @return the list of last limit pushes, retrieved on the Pushbullet services. */ public List<Push> getListOfPushes(int limit) { return getListOfPushes(false, -1, limit); } /** * Retrieve the list of pushes received after a specific timestamp, if useModifiedAfter is setted to true. * This method allows to avoid to use a comparison for the double modifiedAfter parameter (modifiedAfter == -1), * which is a bad idea, for rounding reasons of double primitive types in java. * @param useModifiedAfter * @param modifiedAfter * @param limit * @return the list of last limit pushes, with a limit is time if useModifiedAfter is set to true. * @see #getListOfPushes(int) * @see #getListOfPushes(double, int) */ private List<Push> getListOfPushes(boolean useModifiedAfter, double modifiedAfter, int limit) { String url = "https://api.pushbullet.com/v2/pushes?active=true"; if (useModifiedAfter) url += "&modified_after=" + modifiedAfter; if (limit != -1) url += "&limit=" + limit; return deserializePushbulletEntity(sendGetRequest(url), ListOfPushes.class).getPushes(); } /** * Retrieve the list of Pushbullet devices linked to the specified access token account. * @return the list of Pusbullet devices linked to the specifiec access token account. */ public List<Device> getListOfDevices() { return deserializePushbulletEntity(sendGetRequest("https://api.pushbullet.com/v2/devices"), ListOfDevices.class).getDevices(); } private Device createNewDevice(String nickname) { String manufacturer = "Raspoid"; String model = "Raspberry Pi"; List<NameValuePair> urlParameters = new ArrayList<>(); urlParameters.add(new BasicNameValuePair("nickname", nickname)); urlParameters.add(new BasicNameValuePair("manufacturer", manufacturer)); urlParameters.add(new BasicNameValuePair("model", model)); return deserializePushbulletEntity(sendPostRequest("https://api.pushbullet.com/v2/devices", urlParameters), Device.class); } /** * Sends a new push with a specific body to the Pushbullet server. * The push is sent in broadcast mode (to all account devices) to the account linked to the specified access token. * @param body the body of the push sent to Pushbullet server. * @return a Push entity representing the newly sent push to the Pushbullet server. */ public Push sendNewPush(String body) { return sendNewPush(null, body, null); } /** * Sends a new push with a specific title and a specific body to the Pushbullet server. * The push is sent in broadcast mode (to all account devices) to the account linked to the specified access token. * @param title the title of the push sent to the Pusbullet server. * @param body the body of the push sent to the Pushbullet server. * @return a Push entity representing the newly sent push to the Pushbullet server. */ public Push sendNewPush(String title, String body) { return sendNewPush(title, body, null); } /** * Sends a new push with a specific title and a specific body to the Pushbullet server. * The push is sent to a specific device, or in broadcast mode (to all account devices) if targetDeviceIden is null, * to the account linked to the specified access token. * @param title the title of the push sent to the Pusbullet server. * @param body the body of the push sent to the Pushbullet server. * @param targetDeviceIden the user's device targeted by the push. null for broadcast. * @return a Push entity representing the newly sent push to the Pushbullet server. */ public Push sendNewPush(String title, String body, String targetDeviceIden) { List<NameValuePair> urlParameters = new ArrayList<>(); urlParameters.add(new BasicNameValuePair("type", "note")); if (title != null) urlParameters.add(new BasicNameValuePair("title", title)); if (body != null) urlParameters.add(new BasicNameValuePair("body", body)); if (targetDeviceIden != null) urlParameters.add(new BasicNameValuePair("device_iden", targetDeviceIden)); urlParameters.add(new BasicNameValuePair("source_device_iden", deviceIden)); String response = sendPostRequest("https://api.pushbullet.com/v2/pushes", urlParameters); return deserializePushbulletEntity(response, Push.class); } /** * Sends a new file to the Pushbullet servers and sends this file through a push. * The push is sent to a specific device, or in broadcast mode if targetDeviceIden is null. * @param filePath the path of the file to send. * @param fileName the name of the file to send. * @param fileType the type of the file to send. * @param targetDeviceIden the user's device targeted by the push. null for broadcast. * @return a Push entity representing the newly sent push. */ public Push sendNewFile(String filePath, String fileName, String fileType, String targetDeviceIden) { // step 1 - request authorization to upload a file List<NameValuePair> url1Parameters = new ArrayList<>(); url1Parameters.add(new BasicNameValuePair("file_name", fileName)); url1Parameters.add(new BasicNameValuePair("file_type", fileType)); String response1 = sendPostRequest("https://api.pushbullet.com/v2/upload-request", url1Parameters); FileUploaded fileUploaded = deserializePushbulletEntity(response1, FileUploaded.class); Tools.log("DEBUG AAAAA: " + fileUploaded.getFileName() + " " + fileUploaded.getUploadUrl()); // step 2 - upload the file int result = postFile(fileUploaded.getUploadUrl(), filePath); Tools.log("FILE POSTED? exit value: " + result); // step 3 - new push List<NameValuePair> urlParameters = new ArrayList<>(); urlParameters.add(new BasicNameValuePair("type", "file")); urlParameters.add(new BasicNameValuePair("file_name", fileUploaded.getFileName())); urlParameters.add(new BasicNameValuePair("file_url", fileUploaded.getFileUrl())); urlParameters.add(new BasicNameValuePair("file_type", fileUploaded.getFileType())); if (targetDeviceIden != null) urlParameters.add(new BasicNameValuePair("device_iden", targetDeviceIden)); urlParameters.add(new BasicNameValuePair("source_device_iden", deviceIden)); Tools.log("URL PARAMETERS: " + urlParameters); String response = sendPostRequest("https://api.pushbullet.com/v2/pushes", urlParameters); return deserializePushbulletEntity(response, Push.class); } /** * The WebSocket client endpoint used to connect to the Pushbullet WebSocket server. */ private class PushbulletClientEndpoint extends Endpoint { Router router; PushbulletClientEndpoint(Router router) { this.router = router; } @Override public void onOpen(Session session, EndpointConfig endpointConfig) { session.addMessageHandler(String.class, (String message) -> { RealtimeEventStreamMessage streamMessage = decodeRealtimeEventStreamMessage(message); if ("tickle".equals(streamMessage.getType()) && "push".equals(streamMessage.getSubtype())) { Tools.debug("[Pushbullet] New tickle push received."); // We then need to check pushes and keep the new ones for our device into account List<Push> newPushes = getListOfPushes(lastPushReceivedTime, -1); for (Push newPush : newPushes) { String targetDeviceIden = newPush.getTargetDeviceIden(); if (targetDeviceIden != null && targetDeviceIden.equals(deviceIden)) { String newPushBody = newPush.getBody(); String response; if (router != null) response = router.getResponse(newPushBody, null); else response = "Sorry, no router is defined. I can't understand your request."; Tools.log("[Pushbullet] New push detected for us: " + newPush.getBody()); sendNewPush("Response", response, newPush.getSourceDeviceIden()); } if (newPush.getLastModificationTimestamp() > lastPushReceivedTime) lastPushReceivedTime = newPush.getLastModificationTimestamp(); } } Tools.debug("[Pushbullet] Realtime Event Stream message received: " + message); }); Tools.log("[Pushbullet] Websocket session opened with Pushbullet servers."); } @Override public void onClose(Session session, CloseReason closeReason) { Tools.log("[Pushbullet] Session closed."); } @Override public void onError(Session session, Throwable thr) { Tools.log("[Pushbullet] Error: " + thr.getMessage()); } private RealtimeEventStreamMessage decodeRealtimeEventStreamMessage(String message) { return deserializePushbulletEntity(message, RealtimeEventStreamMessage.class); } }; }