Commits

Kevin Wetzels committed 7c0a2db

Initial version of Bold.io's Java client.

Comments (0)

Files changed (12)

+*.class
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+*.iml
+
+target/
+.idea/
+build:
+	mvn clean install && cp target/bold-client-1.0-SNAPSHOT.jar bin/
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>io.bold</groupId>
+    <artifactId>bold-client</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <name>bold-client</name>
+    <description>Java client for Bold.io SMS hub.</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.googlecode.libphonenumber</groupId>
+            <artifactId>libphonenumber</artifactId>
+            <version>5.3</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>14.0-rc1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>20090211</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <version>1.7.1</version>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.8.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.1.0</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.6</source>
+                    <target>1.6</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <configuration>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

src/demo/java/io/bold/WebhookHandler.java

+package io.bold;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.util.List;
+
+/**
+ * Demo of a web server handling the webhook action from Bold.
+ */
+public class WebhookHandler implements HttpHandler {
+
+    @Override
+    public void handle(HttpExchange httpExchange) throws IOException {
+        System.out.println("Incoming request...");
+        InputStream in = httpExchange.getRequestBody();
+        try {
+            ObjectMapper mapper = new ObjectMapper();
+            InboundSms sms = mapper.readValue(in, InboundSms.class);
+            System.out.println("Received a message:\n\t" + sms);
+            httpExchange.sendResponseHeaders(200, 0);
+            httpExchange.getResponseBody().close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            String response = "That's no good";
+            httpExchange.sendResponseHeaders(403, response.length());
+            httpExchange.getResponseBody().write(response.getBytes());
+            httpExchange.getResponseBody().close();
+        }
+    }
+
+    public static void main(String[] args) throws IOException {
+        String addr = "0.0.0.0";
+        int port = 9000;
+        if (args.length > 0) {
+            List<String> parts = Lists.newArrayList(Splitter.on(':').trimResults().split(args[0]));
+            addr = parts.get(0);
+            if (parts.size() > 1) {
+                port = Integer.parseInt(parts.get(1));
+            }
+        }
+        HttpServer server = HttpServer.create(new InetSocketAddress(addr, port), 2);
+        server.createContext("/", new WebhookHandler());
+        server.setExecutor(null); // creates a default executor
+        server.start();
+    }
+
+}

src/main/java/io/bold/Bucket.java

+package io.bold;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * A bucket (keyword) from Bold.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Bucket {
+
+    /**
+     * The hub the bucket belongs to.
+     */
+    public Hub hub;
+
+    /**
+     * The keyword for the bucket (optional).
+     */
+    public String keyword;
+
+    /**
+     * Whether the bucket is a fallback bucket or not (in which case the keyword will be blank or null).
+     */
+    public boolean fallback;
+
+    public Hub getHub() {
+        return hub;
+    }
+
+    public void setHub(Hub hub) {
+        this.hub = hub;
+    }
+
+    public String getKeyword() {
+        return keyword;
+    }
+
+    public void setKeyword(String keyword) {
+        this.keyword = keyword;
+    }
+
+    public boolean isFallback() {
+        return fallback;
+    }
+
+    public void setFallback(boolean fallback) {
+        this.fallback = fallback;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("<Bucket: hub=%s, keyword=%s, fallback=%b>", hub, keyword, fallback);
+    }
+
+}

src/main/java/io/bold/Client.java

+package io.bold;
+
+import com.google.i18n.phonenumbers.Phonenumber;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Client interface for Bold.io's SMS hub.
+ */
+public interface Client {
+
+    /**
+     * Find the subscriptions for the number.
+     *
+     * @param number number in E.164 format
+     * @return the list of subscriptions
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    List<Subscription> findSubscriptions(String number) throws IOException, BoldException;
+
+    /**
+     * Finds the subscriptions for the number.
+     *
+     * @param number number
+     * @return the list of subscriptions
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    List<Subscription> findSubscriptions(Phonenumber.PhoneNumber number) throws IOException, BoldException;
+
+    /**
+     * Subscribe the number to the subscription.
+     *
+     * @param number           number to subscribe (E.164 format)
+     * @param subscriptionName subscription to subscribe to
+     * @return <code>false</code> in case the number was already subscribed
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    boolean subscribe(String number, String subscriptionName) throws IOException, BoldException;
+
+    /**
+     * Subscribe the number to the subscription.
+     *
+     * @param number           number
+     * @param subscriptionName subscription to subscribe to
+     * @return <code>false</code> in case the number was already subscribed
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    boolean subscribe(Phonenumber.PhoneNumber number, String subscriptionName) throws IOException, BoldException;
+
+    /**
+     * Unsubscribe the number.
+     *
+     * @param number           number to unsubscribe (E.164 format)
+     * @param subscriptionName name of the subscription
+     * @return <code>false</code> if the number was not subscribed in the first place
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    boolean unsubscribe(String number, String subscriptionName) throws IOException, BoldException;
+
+    /**
+     * Unsubscribe the number.
+     *
+     * @param number           number to unsubscribe
+     * @param subscriptionName name of the subscription
+     * @return <code>false</code> if the number was not subscribed in the first place
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    boolean unsubscribe(Phonenumber.PhoneNumber number, String subscriptionName) throws IOException, BoldException;
+
+    /**
+     * Send a message.
+     *
+     * @param hubId                    id of the hub
+     * @param number                   number to send to
+     * @param content                  content to send
+     * @param requiredSubscriptionName name of the subscription required to send this message (optional)
+     * @return the outbound sms or null in case the message could not be sent (not subscribed, blocked, ...)
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    OutboundSms send(String hubId, String number, String content, String requiredSubscriptionName) throws IOException, BoldException;
+
+    /**
+     * Send a message.
+     *
+     * @param hubId                    id of the hub
+     * @param number                   number to send to
+     * @param content                  content to send
+     * @param requiredSubscriptionName name of the subscription required to send this message (optional)
+     * @return the outbound sms or null in case the message could not be sent (not subscribed, blocked, ...)
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    OutboundSms send(String hubId, Phonenumber.PhoneNumber number, String content, String requiredSubscriptionName) throws IOException, BoldException;
+
+    /**
+     * Send a message.
+     *
+     * @param hubId                    id of the hub
+     * @param contactId                id of the contact to send to
+     * @param content                  content to send
+     * @param requiredSubscriptionName name of the subscription required to send this message (optional)
+     * @return the outbound sms or null in case the message could not be sent (not subscribed, blocked, ...)
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    OutboundSms send(String hubId, long contactId, String content, String requiredSubscriptionName) throws IOException, BoldException;
+
+    /**
+     * Reply to a message.
+     *
+     * @param messageId id of the message to reply to
+     * @param content   content to send
+     * @return the outbound sms or null when the original message does not exist
+     * @throws IOException   when something goes wrong contact the Bold app
+     * @throws BoldException when something is wrong with the request
+     */
+    OutboundSms reply(long messageId, String content) throws IOException, BoldException;
+
+    class BoldException extends RuntimeException {
+
+        public BoldException() {
+            super();
+        }
+
+        public BoldException(String msg) {
+            super(msg);
+        }
+
+        public BoldException(Throwable t) {
+            super(t);
+        }
+
+        public BoldException(String msg, Throwable t) {
+            super(msg, t);
+        }
+    }
+
+
+}

src/main/java/io/bold/Contact.java

+package io.bold;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * A contact registered/to register in Bold.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Contact {
+
+    /**
+     * The id of the contact.
+     */
+    private Long id;
+
+    /**
+     * The name of the contact.
+     */
+    private String name;
+
+    /**
+     * The number of the contact, in E.164 format.
+     */
+    private String number;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getNumber() {
+        return number;
+    }
+
+    public void setNumber(String number) {
+        this.number = number;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("<Contact: id=%d, name=%s, number=%s>", id, name, number);
+    }
+}

src/main/java/io/bold/DefaultClient.java

+package io.bold;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Charsets;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.io.BaseEncoding;
+import com.google.common.io.Closeables;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.*;
+import java.util.List;
+
+/**
+ * Default implementation of {@link Client}.
+ */
+public class DefaultClient implements Client {
+
+    public static final int DEFAULT_TIMEOUT = 5000;
+
+    protected Proxy proxy;
+    protected String endpoint;
+    protected String username;
+    protected String password;
+    protected int timeoutMs;
+
+    public DefaultClient(String endpoint, String username, String password) {
+        this(endpoint, username, password, DEFAULT_TIMEOUT);
+    }
+
+    public DefaultClient(String endpoint, String username, String password, int timeoutMs) {
+        this(Proxy.NO_PROXY, endpoint, username, password, timeoutMs);
+    }
+
+    public DefaultClient(Proxy proxy, String endpoint, String username, String password, int timeoutMs) {
+        this.proxy = proxy;
+        this.endpoint = createApiEndPoint(endpoint);
+        this.username = username;
+        this.password = password;
+        this.timeoutMs = timeoutMs;
+    }
+
+    @Override
+    public List<Subscription> findSubscriptions(String number) throws IOException {
+        try {
+            return findSubscriptions(PhoneNumberUtil.getInstance().parse(number, null));
+        } catch (NumberParseException e) {
+            throw new BoldException(e);
+        }
+    }
+
+    @Override
+    public List<Subscription> findSubscriptions(Phonenumber.PhoneNumber number) throws IOException {
+        String phoneNr = PhoneNumberUtil.getInstance().format(number, PhoneNumberUtil.PhoneNumberFormat.E164);
+        phoneNr = phoneNr.substring(1);
+        HttpURLConnection connection = openConnection("contacts/%s/subscriptions/", phoneNr);
+        connection.connect();
+        int statusCode = connection.getResponseCode();
+        InputStream response = connection.getInputStream();
+        try {
+            if (statusCode < 200 || statusCode >= 300) {
+                throw new BoldException("Encountered status " + statusCode);
+            }
+            ObjectMapper mapper = new ObjectMapper();
+            JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, Subscription.class);
+            return mapper.readValue(response, type);
+        } finally {
+            Closeables.close(response, true);
+        }
+    }
+
+    @Override
+    public boolean subscribe(String number, String subscriptionName) throws IOException {
+        try {
+            return subscribe(PhoneNumberUtil.getInstance().parse(number, null), subscriptionName);
+        } catch (NumberParseException e) {
+            throw new BoldException(e);
+        }
+    }
+
+    @Override
+    public boolean subscribe(Phonenumber.PhoneNumber number, String subscriptionName) throws IOException {
+        String phoneNr = PhoneNumberUtil.getInstance().format(number, PhoneNumberUtil.PhoneNumberFormat.E164);
+        phoneNr = phoneNr.substring(1);
+        HttpURLConnection connection = openConnection("contacts/%s/subscriptions/", phoneNr);
+        connection.setRequestMethod("POST");
+        connection.setDoOutput(true);
+        ObjectMapper mapper = new ObjectMapper();
+        OutputStream out = connection.getOutputStream();
+        mapper.writeValue(out, new Subscription(subscriptionName));
+        out.close();
+        connection.connect();
+        int statusCode = connection.getResponseCode();
+        if (statusCode == 200) {
+            // Already subscribed
+            return false;
+        }
+        if (statusCode == 201) {
+            return true;
+        }
+        throw new BoldException("Error subscribing " + number + " to " + subscriptionName);
+    }
+
+    @Override
+    public boolean unsubscribe(String number, String subscriptionName) throws IOException {
+        try {
+            return unsubscribe(PhoneNumberUtil.getInstance().parse(number, null), subscriptionName);
+        } catch (NumberParseException e) {
+            throw new BoldException(e);
+        }
+    }
+
+    @Override
+    public boolean unsubscribe(Phonenumber.PhoneNumber number, String subscriptionName) throws IOException {
+        String phoneNr = PhoneNumberUtil.getInstance().format(number, PhoneNumberUtil.PhoneNumberFormat.E164);
+        phoneNr = phoneNr.substring(1);
+        String name = URLEncoder.encode(subscriptionName.trim().toUpperCase(), Charsets.UTF_8.name());
+        HttpURLConnection connection = openConnection("contacts/%s/subscriptions/%s/", phoneNr, name);
+        connection.setRequestMethod("DELETE");
+        connection.connect();
+        int statusCode = connection.getResponseCode();
+        if (statusCode == 204) {
+            return true;
+        }
+        if (statusCode == 404) {
+            // No such contact or subscription for the contact
+            return false;
+        }
+        throw new BoldException("Error unsubscribing " + number + " from " + subscriptionName);
+    }
+
+    @Override
+    public OutboundSms send(String hubId, String number, String content, String requiredSubscriptionName) throws IOException {
+        try {
+            Phonenumber.PhoneNumber nr = PhoneNumberUtil.getInstance().parse(number, null);
+            return send(hubId, nr, content, requiredSubscriptionName);
+        } catch (NumberParseException e) {
+            throw new BoldException(e);
+        }
+    }
+
+    @Override
+    public OutboundSms send(String hubId, Phonenumber.PhoneNumber number, String content, String requiredSubscriptionName) throws IOException {
+        String phoneNr = PhoneNumberUtil.getInstance().format(number, PhoneNumberUtil.PhoneNumberFormat.E164);
+        SendSms sms = new SendSms();
+        sms.content = content;
+        sms.destinationNr = phoneNr;
+        sms.hubId = hubId;
+        sms.requiredSubscription = requiredSubscriptionName;
+        return send(sms);
+    }
+
+    @Override
+    public OutboundSms send(String hubId, long contactId, String content, String requiredSubscriptionName) throws IOException {
+        SendSms sms = new SendSms();
+        sms.content = content;
+        sms.destinationId = contactId;
+        sms.hubId = hubId;
+        sms.requiredSubscription = requiredSubscriptionName;
+        return send(sms);
+    }
+
+    @Override
+    public OutboundSms reply(long messageId, String content) throws IOException {
+        HttpURLConnection connection = openConnection("messages/in/%d/reply/", messageId);
+        connection.setRequestMethod("POST");
+        connection.setDoOutput(true);
+        ObjectMapper mapper = new ObjectMapper();
+        OutputStream out = connection.getOutputStream();
+        mapper.writeValue(out, new Reply(content));
+        out.close();
+        connection.connect();
+        int statusCode = connection.getResponseCode();
+        if (statusCode == 404) {
+            // Original message not found
+            return null;
+        }
+        if (statusCode == 201) {
+            InputStream response = connection.getInputStream();
+            try {
+                return mapper.readValue(response, OutboundSms.class);
+            } finally {
+                Closeables.close(response, true);
+            }
+        }
+        throw new BoldException("Error sending message");
+    }
+
+    protected OutboundSms send(SendSms sms) throws IOException {
+        HttpURLConnection connection = openConnection("messages/out/");
+        connection.setRequestMethod("POST");
+        connection.setDoOutput(true);
+        ObjectMapper mapper = new ObjectMapper();
+        OutputStream out = connection.getOutputStream();
+        mapper.writeValue(out, sms);
+        out.close();
+        connection.connect();
+        int statusCode = connection.getResponseCode();
+        if (statusCode == 200) {
+            // Contact cannot receive message based on criteria (blocked or no matching subscription if supplied)
+            return null;
+        }
+        if (statusCode == 201) {
+            InputStream response = connection.getInputStream();
+            try {
+                return mapper.readValue(response, OutboundSms.class);
+            } finally {
+                Closeables.close(response, true);
+            }
+        }
+        throw new BoldException("Error sending message");
+    }
+
+    protected HttpURLConnection openConnection(String fmt, Object... args) throws IOException {
+        URL url = createUrl(fmt, args);
+        String auth = BaseEncoding.base64Url().encode((username + ":" + password).getBytes());
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        connection.setRequestProperty("Authorization", "Basic " + auth);
+        connection.setRequestProperty("User-Agent", "Bold.io Java Client");
+        connection.setRequestProperty("Content-Type", "application/json");
+        connection.setRequestProperty("Accept", "application/json");
+        connection.setConnectTimeout(timeoutMs);
+        connection.setReadTimeout(timeoutMs);
+        return connection;
+    }
+
+    protected URL createUrl(String fmt, Object[] args) throws IOException {
+        try {
+            return new URL(String.format(endpoint + fmt, args));
+        } catch (MalformedURLException e) {
+            throw new IOException(e);
+        }
+    }
+
+    public static String createApiEndPoint(String base) {
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(base), "An endpoint is required");
+        base = base.trim();
+        return base + (base.endsWith("/") ? "" : "/") + "api/";
+    }
+
+    public class SendSms {
+        @JsonProperty("hub_id")
+        public String hubId;
+        @JsonProperty("destination_nr")
+        public String destinationNr;
+        @JsonProperty("destination_id")
+        public Long destinationId;
+        public String content;
+        @JsonProperty("required_subscription")
+        public String requiredSubscription;
+    }
+
+    public class Reply {
+        public String content;
+
+        public Reply() {
+            super();
+        }
+
+        public Reply(String content) {
+            this.content = content;
+        }
+
+    }
+
+    public static void main(String[] args) throws IOException {
+        Client client = new DefaultClient("http://localhost:8000", "kevin", "kevin");
+        String number = "+32475435169";
+        String subscription = "TOOLS";
+        System.out.println(client.findSubscriptions(number));
+        System.out.println(client.subscribe(number, subscription));
+        System.out.println(client.findSubscriptions(number));
+        System.out.println(client.unsubscribe(number, subscription));
+        System.out.println(client.findSubscriptions(number));
+        System.out.println(client.send("5", number, "Thank you", subscription));
+        System.out.println(client.subscribe(number, subscription));
+        System.out.println(client.send("5", number, "Thank you", subscription));
+        System.out.println(client.unsubscribe(number, subscription));
+    }
+
+}

src/main/java/io/bold/Hub.java

+package io.bold;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A hub from Bold.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Hub {
+
+    /**
+     * The hub's id.
+     */
+    private Long id;
+
+    /**
+     * The name of the hub.
+     */
+    private String name;
+
+    /**
+     * The number of the hub.
+     */
+    @JsonProperty("nr")
+    private String number;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getNumber() {
+        return number;
+    }
+
+    public void setNumber(String number) {
+        this.number = number;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("<Hub: id=%s, name=%s, number=%s>", id, name, number);
+    }
+}

src/main/java/io/bold/InboundSms.java

+package io.bold;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Date;
+
+/**
+ * An inbound SMS, useful for actions invoked through a webhook.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class InboundSms {
+
+    /**
+     * Id of the message.
+     */
+    public long id;
+
+    /**
+     * Sender of the message.
+     */
+    public Contact sender;
+
+    /**
+     * Hub that received the message.
+     */
+    public Hub hub;
+
+    /**
+     * Bucket that received the message.
+     */
+    public Bucket bucket;
+
+    /**
+     * Full content of the message.
+     */
+    public String content;
+
+    /**
+     * Processed content of the message (message without the keyword).
+     */
+    @JsonProperty("processed_content")
+    public String processedContent;
+
+    /**
+     * The keyword; should match the keyword of the bucket.
+     */
+    public String keyword;
+
+    /**
+     * Date the message was received.
+     */
+    public Date received;
+
+    /**
+     * Date the message was processed.
+     */
+    public Date processed;
+
+    /**
+     * Link to send a reply--does not include the hostname.
+     */
+    @JsonProperty("link_reply")
+    public String replyLink;
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public Contact getSender() {
+        return sender;
+    }
+
+    public void setSender(Contact sender) {
+        this.sender = sender;
+    }
+
+    public Hub getHub() {
+        return hub;
+    }
+
+    public void setHub(Hub hub) {
+        this.hub = hub;
+    }
+
+    public Bucket getBucket() {
+        return bucket;
+    }
+
+    public void setBucket(Bucket bucket) {
+        this.bucket = bucket;
+    }
+
+    public String getProcessedContent() {
+        return processedContent;
+    }
+
+    public void setProcessedContent(String processedContent) {
+        this.processedContent = processedContent;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public String getKeyword() {
+        return keyword;
+    }
+
+    public void setKeyword(String keyword) {
+        this.keyword = keyword;
+    }
+
+    public Date getReceived() {
+        return received;
+    }
+
+    public void setReceived(Date received) {
+        this.received = received;
+    }
+
+    public Date getProcessed() {
+        return processed;
+    }
+
+    public void setProcessed(Date processed) {
+        this.processed = processed;
+    }
+
+    public String getReplyLink() {
+        return replyLink;
+    }
+
+    public void setReplyLink(String replyLink) {
+        this.replyLink = replyLink;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("<InboundSms: id=%d, hub=%s, bucket=%s, sender=%s, content=%s, processedContent=%s, received=%s, processed=%s, replyLink=%s>", id, hub, bucket, sender, content, processedContent, received, processed, replyLink);
+    }
+}

src/main/java/io/bold/OutboundSms.java

+package io.bold;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * An outbound SMS message.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class OutboundSms {
+
+    /**
+     * Id of the sms.
+     */
+    private Long id;
+
+    /**
+     * The hub the SMS should be/is sent through.
+     */
+    private Hub hub;
+
+    /**
+     * The destination contact.
+     */
+    private Contact destination;
+
+    /**
+     * The content of the message.
+     */
+    private String content;
+
+    /**
+     * When the message was read.
+     */
+    @JsonProperty("read")
+    private Long readTimestamp;
+
+    /**
+     * The status of the message.
+     */
+    private String status;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Hub getHub() {
+        return hub;
+    }
+
+    public void setHub(Hub hub) {
+        this.hub = hub;
+    }
+
+    public Contact getDestination() {
+        return destination;
+    }
+
+    public void setDestination(Contact destination) {
+        this.destination = destination;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public Long getReadTimestamp() {
+        return readTimestamp;
+    }
+
+    public void setReadTimestamp(Long readTimestamp) {
+        this.readTimestamp = readTimestamp;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("<OutboundSms: id=%d, hub=%s, destination=%s, content=%s, status=%s>", id, hub, destination, content, status);
+    }
+}

src/main/java/io/bold/Subscription.java

+package io.bold;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * A subscription for a specific contact.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Subscription {
+
+    /**
+     * Name of the subscription.
+     */
+    private String name;
+
+    /**
+     * The contact that is subscribed to this subscription.
+     */
+    private Contact contact;
+
+    public Subscription() {
+        super();
+    }
+
+    public Subscription(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Contact getContact() {
+        return contact;
+    }
+
+    public void setContact(Contact contact) {
+        this.contact = contact;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("<Subscription: name=%s, contact=%s>", name, contact);
+    }
+}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.