Skip to content

Pusher on App Engine sample #798

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

Merged
merged 15 commits into from
Aug 9, 2017
1 change: 1 addition & 0 deletions appengine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<module>memcache</module>
<module>multitenancy</module>
<module>oauth2</module>
<module>pusher-chat</module>
<module>requests</module>
<module>search</module>
<module>sendgrid</module>
Expand Down
47 changes: 47 additions & 0 deletions appengine/pusher-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Pusher sample for Google App Engine
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this go in J8

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, I will leave it here for now, but will submit a separate PR for J8


This sample demonstrates how to use the [Pusher][pusher] on [Google App Engine][ae-docs].
Pusher enables you to create public / private channels with presence information for real time messaging.
This application demonstrates presence channels in Pusher using chat rooms.
All users joining the chat room are authenticated using the `/authorize` endpoint.
All users currently in the chat room receive updates of users joining / leaving the room.
[Java HTTP library](https://github.com/pusher/pusher-http-java) is used for publishing messages to the channel
and the [JS Websocket library](https://github.com/pusher/pusher-js) is used for subscribing.

[pusher]: https://pusher.com
[ae-docs]: https://cloud.google.com/appengine/docs/java/

## Setup

Install the [Google Cloud SDK](https://cloud.google.com/sdk/) and run:
```
gcloud init
```
If this is your first time creating an App engine application:
```
gcloud app create
```

#### Setup Pusher

- Create a [Pusher] application and note down the `app_id`, `app_key`, `app_secret` and the cluster.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is [Pusher] enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that seems to work.

- Update [appengine-web.xml](src/main/webapp/WEB-INF/appengine-web.xml) with these credentials.

## Running locally

```
mvn clean appengine:run
```

Access [http://localhost:8080](http://localhost:8080) via the browser, login and join the chat room.
The chat window will contain a link you can use to join the room as a different user in another browser.
You should now be able to view both the users within the chat application window and send messages to one another.

## Deploying

- Deploy the application to the project
```
mvn clean appengine:deploy

```
Access `https://YOUR_PROJECT_ID.appspot.com`
75 changes: 75 additions & 0 deletions appengine/pusher-chat/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!--
Copyright 2017 Google 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.
-->
<project>
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<groupId>com.example.appengine</groupId>
<artifactId>appengine-pusher-chat</artifactId>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parent pom?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd missed that, just added.

<parent>
<groupId>com.google.cloud</groupId>
<artifactId>appengine-doc-samples</artifactId>
<version>1.0.0</version>
<relativePath>..</relativePath>
</parent>

<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>com.pusher</groupId>
<artifactId>pusher-http-java</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

22?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for support with 1.7, 20.0 is the last version we can use.

</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.1 and target GAE Std J8?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline

<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.appengine</groupId>
<artifactId>appengine-api-1.0-sdk</artifactId>
<version>1.9.54</version>
</dependency>
</dependencies>

<build>
<!-- for hot reload of the web application -->
<outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/classes</outputDirectory>
<plugins>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>appengine-maven-plugin</artifactId>
<version>1.3.1</version>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2017 Google 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 com.example.appengine.pusher;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.common.io.CharStreams;
import com.pusher.rest.Pusher;
import com.pusher.rest.data.PresenceUser;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Authorization endpoint that is automatically triggered on `Pusher.subscribe` for private,
* presence channels. Successful authentication returns valid authorization token with user
* information.
*
* @see <a href="https://pusher.com/docs/authenticating_users">Pusher Authentication Docs</a>
*/
// [START pusher_authorize]
public class AuthorizeServlet extends HttpServlet {

@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {

// Instantiate a pusher connection
Pusher pusher = PusherService.getDefaultInstance();
// Get current logged in user credentials
User user = UserServiceFactory.getUserService().getCurrentUser();

// redirect to homepage if user is not authorized
if (user == null) {
response.sendRedirect("/");
return;
}
String currentUserId = user.getUserId();
String displayName = user.getNickname().replaceFirst("@.*", "");

String query = CharStreams.toString(request.getReader());
// socket_id, channel_name parameters are automatically set in the POST body of the request
// eg.socket_id=1232.12&channel_name=presence-my-channel
Map<String, String> data = splitQuery(query);
String socketId = data.get("socket_id");
String channelId = data.get("channel_name");

// Presence channels (presence-*) require user identification for authentication
Map<String, String> userInfo = new HashMap<>();
userInfo.put("displayName", displayName);

// Inject custom authentication code for your application here to allow /deny current request

String auth =
pusher.authenticate(socketId, channelId, new PresenceUser(currentUserId, userInfo));
// if successful, returns authorization in the format
// {
// "auth":"49e26cb8e9dde3dfc009:a8cf1d3deefbb1bdc6a9d1547640d49d94b4b512320e2597c257a740edd1788f",
// "channel_data":"{\"user_id\":\"23423435252\",\"user_info\":{\"displayName\":\"John Doe\"}}"
// }

response.getWriter().append(auth);
}

private static Map<String, String> splitQuery(String query) throws UnsupportedEncodingException {
Map<String, String> query_pairs = new HashMap<>();
String[] pairs = query.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
query_pairs.put(
URLDecoder.decode(pair.substring(0, idx), "UTF-8"),
URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
}
return query_pairs;
}
}
// [END pusher_authorize]
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2017 Google 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 com.example.appengine.pusher;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/** Homepage of chat application, redirects user to login page if not authorized. */
public class ChatServlet extends HttpServlet {

public static String getUriWithChatRoom(HttpServletRequest request, String chatRoom) {
try {
String query = "";
if (chatRoom != null) {
query = "room=" + chatRoom;
}
URI thisUri = new URI(request.getRequestURL().toString());
URI uriWithOptionalRoomParam =
new URI(
thisUri.getScheme(),
thisUri.getUserInfo(),
thisUri.getHost(),
thisUri.getPort(),
"/",
query,
"");
return uriWithOptionalRoomParam.toString();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
final UserService userService = UserServiceFactory.getUserService();
User currentUser = userService.getCurrentUser();
String room = req.getParameter("room");
// Show login link if user is not logged in.
if (currentUser == null) {
String loginUrl = userService.createLoginURL(getUriWithChatRoom(req, room));
resp.getWriter().println("<p>Please <a href=\"" + loginUrl + "\">sign in</a>.</p>");
return;
}

// user is already logged in
if (room != null) {
req.setAttribute("room", room);
}
getServletContext().getRequestDispatcher("/WEB-INF/view/chat.jsp").forward(req, resp);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2017 Google 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 com.example.appengine.pusher;

import com.pusher.rest.Pusher;

// [START pusher_server_initialize]
public abstract class PusherService {

public static final String APP_KEY = System.getenv("PUSHER_APP_KEY");
public static final String CLUSTER = System.getenv("PUSHER_CLUSTER");

private static final String APP_ID = System.getenv("PUSHER_APP_ID");
private static final String APP_SECRET = System.getenv("PUSHER_APP_SECRET");

private static Pusher instance;

static Pusher getDefaultInstance() {
if (instance != null) {
return instance;
} // Instantiate a pusher
Pusher pusher = new Pusher(APP_ID, APP_KEY, APP_SECRET);
pusher.setCluster(CLUSTER); // required, if not default mt1 (us-east-1)
pusher.setEncrypted(true); // optional, ensure subscriber also matches these settings
instance = pusher;
return pusher;
}
}
// [END pusher_server_initialize]
Loading