Skip to content

Commit

Permalink
Merge pull request square#2935 from square/jwilson.1022.slack_client_…
Browse files Browse the repository at this point in the history
…example

A WebSockets sample that uses the Slack API.
  • Loading branch information
swankjesse authored Oct 23, 2016
2 parents 643ee3f + ce695b6 commit 7de5351
Show file tree
Hide file tree
Showing 10 changed files with 548 additions and 1 deletion.
4 changes: 4 additions & 0 deletions checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">

<module name="Checker">
<module name="SuppressWarningsFilter"/>
<module name="NewlineAtEndOfFile"/>
<module name="FileLength"/>
<module name="FileTabCharacter"/>
Expand Down Expand Up @@ -139,5 +140,8 @@
<!--module name="FinalParameters"/-->
<!--module name="TodoComment"/-->
<module name="UpperEll"/>

<!-- Make the @SuppressWarnings annotations available to Checkstyle -->
<module name="SuppressWarningsHolder"/>
</module>
</module>
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>2.10</version>
<version>2.12</version>
<configuration>
<failsOnError>true</failsOnError>
<configLocation>checkstyle.xml</configLocation>
Expand Down
1 change: 1 addition & 0 deletions samples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<module>guide</module>
<module>crawler</module>
<module>simple-client</module>
<module>slack</module>
<module>static-server</module>
</modules>

Expand Down
26 changes: 26 additions & 0 deletions samples/slack/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.squareup.okhttp3.sample</groupId>
<artifactId>sample-parent</artifactId>
<version>3.5.0-SNAPSHOT</version>
</parent>

<artifactId>slack</artifactId>
<name>Sample: Slack</name>

<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
</dependency>
</dependencies>
</project>
42 changes: 42 additions & 0 deletions samples/slack/src/main/java/okhttp3/slack/OAuthSession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.slack;

/** Authorization for an application to make Slack API calls on behalf of a user. */
@SuppressWarnings("checkstyle:membername")
public final class OAuthSession {
public final boolean ok;
public final String access_token;
public final String scope;
public final String user_id;
public final String team_name;
public final String team_id;

public OAuthSession(
boolean ok, String accessToken, String scope, String userId, String teamName, String teamId) {
this.ok = ok;
this.access_token = accessToken;
this.scope = scope;
this.user_id = userId;
this.team_name = teamName;
this.team_id = teamId;
}

@Override public String toString() {
return String.format("(ok=%s, access_token=%s, scope=%s, user_id=%s, team_name=%s, team_id=%s)",
ok, access_token, scope, user_id, team_name, team_id);
}
}
126 changes: 126 additions & 0 deletions samples/slack/src/main/java/okhttp3/slack/OAuthSessionFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.slack;

import java.io.Closeable;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.LinkedHashMap;
import java.util.Map;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.ByteString;

/**
* Runs a MockWebServer on localhost and uses it as the backend to receive an OAuth session.
*
* <p>Clients should call {@link #start}, {@link #newAuthorizeUrl} and {@link #close} in that order.
* Clients may request multiple sessions.
*/
public final class OAuthSessionFactory extends Dispatcher implements Closeable {
private final SecureRandom secureRandom = new SecureRandom();

private final SlackApi slackApi;
private MockWebServer mockWebServer;

/** Guarded by this. */
private Map<ByteString, Listener> listeners = new LinkedHashMap<>();

public OAuthSessionFactory(SlackApi slackApi) {
this.slackApi = slackApi;
}

public void start() throws Exception {
if (mockWebServer != null) throw new IllegalStateException();

mockWebServer = new MockWebServer();
mockWebServer.setDispatcher(this);
mockWebServer.start(slackApi.port);
}

public HttpUrl newAuthorizeUrl(String scopes, String team, Listener listener) {
if (mockWebServer == null) throw new IllegalStateException();

ByteString state = randomToken();
synchronized (this) {
listeners.put(state, listener);
}

return slackApi.authorizeUrl(scopes, redirectUrl(), state, team);
}

private ByteString randomToken() {
byte[] bytes = new byte[16];
secureRandom.nextBytes(bytes);
return ByteString.of(bytes);
}

private HttpUrl redirectUrl() {
return mockWebServer.url("/oauth/");
}

/** When the browser hits the redirect URL, use the provied code to ask Slack for a session. */
@Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
HttpUrl requestUrl = mockWebServer.url(request.getPath());
String code = requestUrl.queryParameter("code");
String stateString = requestUrl.queryParameter("state");
ByteString state = stateString != null ? ByteString.decodeBase64(stateString) : null;

Listener listener;
synchronized (this) {
listener = listeners.get(state);
}

if (code == null || listener == null) {
return new MockResponse()
.setResponseCode(404)
.setBody("unexpected request");
}

try {
OAuthSession session = slackApi.exchangeCode(code, redirectUrl());
listener.sessionGranted(session);
} catch (IOException e) {
return new MockResponse()
.setResponseCode(400)
.setBody("code exchange failed: " + e.getMessage());
}

synchronized (this) {
listeners.remove(state);
}

// Success!
return new MockResponse()
.setResponseCode(302)
.addHeader("Location", "https://twitter.com/CuteEmergency/status/789457462864863232");
}

public interface Listener {
void sessionGranted(OAuthSession session);
}

@Override public void close() {
if (mockWebServer == null) throw new IllegalStateException();
try {
mockWebServer.close();
} catch (IOException ignored) {
}
}
}
89 changes: 89 additions & 0 deletions samples/slack/src/main/java/okhttp3/slack/RtmSession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.slack;

import java.io.Closeable;
import java.io.IOException;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.WebSocket;
import okhttp3.WebSocketCall;
import okhttp3.WebSocketListener;
import okio.ByteString;

/** A realtime messaging session. */
public final class RtmSession implements WebSocketListener, Closeable {
private final SlackApi slackApi;
private WebSocketCall webSocketCall;

/** Guarded by this. */
private WebSocket webSocket;

public RtmSession(SlackApi slackApi) {
this.slackApi = slackApi;
}

public void open(String accessToken) throws IOException {
if (webSocketCall != null) throw new IllegalStateException();

RtmStartResponse rtmStartResponse = slackApi.rtmStart(accessToken);
webSocketCall = slackApi.rtm(rtmStartResponse.url);
webSocketCall.enqueue(this);
}

// TODO(jwilson): can I read the response body? Do I have to?
// the body from slack is a 0-byte-buffer
@Override public synchronized void onOpen(WebSocket webSocket, Response response) {
System.out.println("onOpen: " + response);
this.webSocket = webSocket;
}

// TOOD(jwilson): decode incoming messages and dispatch them somewhere.
@Override public void onMessage(ResponseBody message) throws IOException {
System.out.println("onMessage: " + message.string());
}

@Override public void onPong(ByteString payload) {
System.out.println("onPong: " + payload);
}

@Override public void onClose(int code, String reason) {
System.out.println("onClose (" + code + "): " + reason);
}

// TODO(jwilson): can I read the response body? Do I have to?
@Override public void onFailure(Throwable t, Response response) {
System.out.println("onFailure " + response);
}

@Override public void close() throws IOException {
if (webSocketCall == null) return;

WebSocket webSocket;
synchronized (this) {
webSocket = this.webSocket;
}

// TODO(jwilson): Racy? Is there an interleaving of events where the websocket is not closed?
// Our docs say we can’t close if we have an active writer: that seems like it
// could cause problems?
if (webSocket != null) {
webSocket.close(1000, "bye");
} else {
webSocketCall.cancel();
}
}
}
32 changes: 32 additions & 0 deletions samples/slack/src/main/java/okhttp3/slack/RtmStartResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.slack;

import java.util.List;
import okhttp3.HttpUrl;

/** See https://api.slack.com/methods/rtm.start. */
public final class RtmStartResponse {
HttpUrl url;
Object self;
Object team;
List<Object> users;
List<Object> channels;
List<Object> groups;
List<Object> mpims;
List<Object> ims;
List<Object> bots;
}
Loading

0 comments on commit 7de5351

Please sign in to comment.