diff --git a/appengine/pom.xml b/appengine/pom.xml index 4a7b7e11161..b3120f174cc 100644 --- a/appengine/pom.xml +++ b/appengine/pom.xml @@ -72,6 +72,7 @@ memcache multitenancy oauth2 + pusher-chat requests search sendgrid diff --git a/appengine/pusher-chat/README.md b/appengine/pusher-chat/README.md new file mode 100644 index 00000000000..659018d7fb0 --- /dev/null +++ b/appengine/pusher-chat/README.md @@ -0,0 +1,47 @@ +# Pusher sample for Google App Engine + +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. +- 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` diff --git a/appengine/pusher-chat/pom.xml b/appengine/pusher-chat/pom.xml new file mode 100644 index 00000000000..285c8625273 --- /dev/null +++ b/appengine/pusher-chat/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + war + 1.0-SNAPSHOT + com.example.appengine + appengine-pusher-chat + + + com.google.cloud + appengine-doc-samples + 1.0.0 + .. + + + + 1.7 + 1.7 + + + + + com.pusher + pusher-http-java + 1.0.0 + + + com.google.guava + guava + 20.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.8.8 + + + javax.servlet + servlet-api + 2.5 + provided + + + com.google.appengine + appengine-api-1.0-sdk + 1.9.54 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + com.google.cloud.tools + appengine-maven-plugin + 1.3.1 + + + + diff --git a/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/AuthorizeServlet.java b/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/AuthorizeServlet.java new file mode 100644 index 00000000000..e6e217de2f4 --- /dev/null +++ b/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/AuthorizeServlet.java @@ -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 Pusher Authentication Docs + */ +// [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 data = splitQuery(query); + String socketId = data.get("socket_id"); + String channelId = data.get("channel_name"); + + // Presence channels (presence-*) require user identification for authentication + Map 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 splitQuery(String query) throws UnsupportedEncodingException { + Map 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] diff --git a/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/ChatServlet.java b/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/ChatServlet.java new file mode 100644 index 00000000000..1333bee3f4a --- /dev/null +++ b/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/ChatServlet.java @@ -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("

Please sign in.

"); + return; + } + + // user is already logged in + if (room != null) { + req.setAttribute("room", room); + } + getServletContext().getRequestDispatcher("/WEB-INF/view/chat.jsp").forward(req, resp); + } +} diff --git a/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/PusherService.java b/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/PusherService.java new file mode 100644 index 00000000000..f733fdf7218 --- /dev/null +++ b/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/PusherService.java @@ -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] diff --git a/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/SendMessageServlet.java b/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/SendMessageServlet.java new file mode 100644 index 00000000000..4119439bdbc --- /dev/null +++ b/appengine/pusher-chat/src/main/java/com/example/appengine/pusher/SendMessageServlet.java @@ -0,0 +1,82 @@ +/* + * 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.fasterxml.jackson.core.type.TypeReference; +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserServiceFactory; +import com.google.common.io.CharStreams; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.pusher.rest.data.Result; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Submit a chat message over a channel. Note : we use socket_id to exclude the sender from + * receiving the message // {@see + * Excluding + * Recipients} + */ +// [START pusher_server_send_message] +public class SendMessageServlet extends HttpServlet { + + private Gson gson = new GsonBuilder().create(); + private TypeReference> typeReference = + new TypeReference>() {}; + + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + // Parse POST request body received in the format : + // [{"message": "my-message", "socket_id": "1232.24", "channel": "presence-my-channel"}] + + String body = CharStreams.readLines(request.getReader()).toString(); + String json = body.replaceFirst("^\\[", "").replaceFirst("\\]$", ""); + Map data = gson.fromJson(json, typeReference.getType()); + String message = data.get("message"); + String socketId = data.get("socket_id"); + String channelId = data.get("channel_id"); + + User user = UserServiceFactory.getUserService().getCurrentUser(); + // user email prefix as display name for current logged in user + String displayName = user.getNickname().replaceFirst("@.*", ""); + + // Create a message including the user email prefix to display in the chat window + String taggedMessage = "<" + displayName + "> " + message; + Map messageData = new HashMap<>(); + messageData.put("message", taggedMessage); + + // Send a message over the Pusher channel (maximum size of a message is 10KB) + Result result = + PusherService.getDefaultInstance() + .trigger( + channelId, + "new_message", // name of event + messageData, + socketId); // (optional) use client socket_id to exclude the sender from receiving the message + + // result.getStatus() == SUCCESS indicates successful transmission + messageData.put("status", result.getStatus().name()); + + response.getWriter().println(gson.toJson(messageData)); + } +} +// [END pusher_server_send_message] diff --git a/appengine/pusher-chat/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/pusher-chat/src/main/webapp/WEB-INF/appengine-web.xml new file mode 100644 index 00000000000..514fde8434d --- /dev/null +++ b/appengine/pusher-chat/src/main/webapp/WEB-INF/appengine-web.xml @@ -0,0 +1,22 @@ + + + + true + + + + + + + \ No newline at end of file diff --git a/appengine/pusher-chat/src/main/webapp/WEB-INF/view/chat.jsp b/appengine/pusher-chat/src/main/webapp/WEB-INF/view/chat.jsp new file mode 100644 index 00000000000..a9f32e1210e --- /dev/null +++ b/appengine/pusher-chat/src/main/webapp/WEB-INF/view/chat.jsp @@ -0,0 +1,162 @@ + +<%@ page import="com.example.appengine.pusher.ChatServlet" %> +<%@ page import="com.example.appengine.pusher.PusherService" %> +<%@ page import="java.util.Date" %> +<%-- + 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. +--%> + + + + + + + +
+
+
+
+
+
+
+

Room()

+

Online (0)

+
    +
  • +
+
+
+
+
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/appengine/pusher-chat/src/main/webapp/WEB-INF/web.xml b/appengine/pusher-chat/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..478514feda0 --- /dev/null +++ b/appengine/pusher-chat/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,42 @@ + + + + + chat + com.example.appengine.pusher.ChatServlet + + + authorize + com.example.appengine.pusher.AuthorizeServlet + + + sendMessage + com.example.appengine.pusher.SendMessageServlet + + + chat + / + + + authorize + /authorize + + + sendMessage + /message + + diff --git a/appengine/pusher-chat/src/main/webapp/static/chat.css b/appengine/pusher-chat/src/main/webapp/static/chat.css new file mode 100644 index 00000000000..12d563a979e --- /dev/null +++ b/appengine/pusher-chat/src/main/webapp/static/chat.css @@ -0,0 +1,23 @@ +#chat_widget_container{padding:20px 20px 5px 20px; background-color:powderblue; border:5px solid dodgerblue; + width:500px; font-size:11px; font-family: Arial,Verdana,sans-serif; + position:fixed; top:10%; left:20%} + +#chat_widget_main_container{display:none} + +#chat_widget_messages_container{float:left; width:300px; border:1px solid #DDD; height:200px; overflow:auto; + padding:5px; background-color:#FFF; position:relative} + +#chat_widget_messages{overflow-x:hidden; overflow-y:auto; position:absolute; bottom:0px} + +#chat_widget_online{width:150px; height:210px; float:left; padding:0px 10px; border:1px solid #DDD; + border-left:0px; background-color:#FFF; overflow: auto;} + +#chat_widget_online_list{list-style:none; padding:0px} + +#chat_widget_online_list >li{margin-left:0px} + +#chat_widget_input_container{margin-top:10px; text-align:left} + +#chat_widget_input{width:260px; margin-right:10px; border:1px solid #DDD; padding:2px 5px} + +.clear{clear:both} \ No newline at end of file