In GAE Channel API the onmessage is not called - google-app-engine

I am building an app for GAE using python API. It is running here. It is a multi-player game. I use the Channel API to communicate game state between players.
But in the app engine the onmessage handler of the channel is not called. The onopen handler is called. onerror or onclose are not called as well. Weird thing is this works perfectly in the local development server.
Is it possible that something like this can work on the development server but not in the app engine itself?
I'll be really really glad if someone can look into following description of my app and help me to figure out what has happened. Thank you.
I looked into this and this questions, but I haven't done those mistakes.
<script>
sendMessage = function(path, opt_param, opt_param2) {
path += '?g=' + state.game_key;
if (opt_param) {
path += '&' + opt_param;
}
if (opt_param2) {
path += '&' + opt_param2;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', path, true);
xhr.send();
};
Above function is used to make a post request to the server.
onOpened = function() {
sendMessage('/resp');
console.log('channel opened');
};
Above is the function I want to be called when the channel is open for the first time. I send a post to the '/resp' address.
onMessage = function(m) {
console.log('message received');
message = JSON.parse(m.data);
//do stuff with message here
};
I want to process the response I get from that request in the above function.
following are onerror and onclose handlers.
onError = function() {
console.log('error occured');
channel = new goog.appengine.Channel('{{ token }}');
socket = channel.open();
};
onClose = function() {
console.log('channel closed');
};
channel = new goog.appengine.Channel('{{ token }}');
socket = channel.open();
socket.onopen = onOpened;
socket.onmessage = onMessage;
socket.onclose = onClose;
socket.onerror = onError;
</script>
This script is at the top of body tag. This works fine in my local development server. But on the app engine,
onOpen function is called.
I can see the request to /resp in the sever logs.
but onMessage is never called. The log 'message received' is not present in the console.
this is the server side.
token = channel.create_channel(user.user_id() + game.user1.user_id() )
url = users.create_logout_url(self.request.uri)
template_values = {
'token' : token,
'id' : pid,
'game_key' : str(game.user1.user_id()),
'url': url
}
path = os.path.join(os.path.dirname(__file__), 'game.html')
self.response.out.write(template.render(path, template_values))
and this is in the request handler for '/resp' request. My application is a multi-player card game. And I want to inform other players that a new player is connected. Even the newly connected player will also get this message.
class Responder(webapp2.RequestHandler):
def post(self):
user = users.get_current_user()
game = OmiGame.get_by_key_name(self.request.get('g'))
if game.user1:
channel.send_message(game.user1.user_id() + game.user1.user_id() , create_message('%s joined.' % user.nickname()))
if game.user2:
channel.send_message(game.user2.user_id() + game.user1.user_id() , create_message('%s joined.' % user.nickname()))
EDIT : user1 is the user who created the game. I want tokens of other players' to be created by adding the user1's user_id and the relevant users user_id. Could something go wrong here?
So when I try this on the local dev server I get these messages perfectly fine. But on the GAE onMessage is not called. This is my app. When the create button is clicked page with above script is loaded and "playernickname connected" should be displayed.

The channel behavior on the dev server and production are somewhat different. On the dev server, the channel client just polls http requests frequently. On production, comet style long polling is used.
I suspect there may be a problem with making the XHR call inside the onOpened handler. In Chrome at least, I see that the next talkgadget GET request used by the channel API is cancelled.
Try calling sendMessage('/resp') outside of the onMessage function. Perhaps enqueue it to get run by using setTimeout so it's called later after you return.

Related

Trigger an event to private channel in react app

I want to trigger an event to pusher private channel and my server side language is laravel I reviewed a lot of resources, but I did not find a comprehensive approach which covers both the server side and the front side Finally I got this solution
in the first step :
export const SendChat = () => {
try {
var pusher = new Pusher('YOUR_APP_KEY', {
cluster: 'ap2',
forceTLS: true,
authTransport: 'jsonp',
authEndpoint: `${baseUrl}pusher/auth`,
});
var channel = pusher.subscribe('private-channel');
channel.bind('pusher:subscription_succeeded', function() {
var triggered = channel.trigger('client-EVENT_NAME', { 'message': 'Hi ....' });
console.log(triggered)
});
} catch (error) {
console.error(error);
}
}
and call it somewhere
<Button onClick={this.props.SendChat} waves='light' >Send</Button>
you must Enable client events in pusher account setting
login to your pusher account -> select the channel ->App Settings -> select Enable client events -> update
add your app key, channel name and event name after that we need authorization in server side this is sample laravel code first add this route in web.php
Route::get('pusher/auth', 'PusherController#pusherAuth');
make PusherController.php like this :
public function pusherAuth()
{
$user = auth()->user();
if ($user) {
$pusher = new Pusher('auth_key', 'secret', 'app_id');
$auth= $pusher->socket_auth(Input::get('channel_name'), Input::get('socket_id'));
$callback = str_replace('\\', '', $_GET['callback']);
header('Content-Type: application/javascript');
echo($callback . '(' . $auth . ');');
return;
}else {
header('', true, 403);
echo "Forbidden";
return;
}
}
test it you should see something like this
Pusher : State changed : connecting -> connected with new socket ID 3953.****556
Pusher : Event sent : {"event":"pusher:subscribe","data":{"auth":"83045ed1350e63c912f5:328fb78165d01f7d6ef3bb6d4a30e07c9c0ad0283751fc2c34d484d4fd744be2","channel":"private-chat"}}
Pusher : Event sent : {"event":"client-MessageSent","data":{"message":"Hi ...."},"channel":"private-chat"}
true
It doesn't matter much which client-side language you are using. Angular, Vue, React they all are JS framework and libraries. And, you can consider using a generic JS code which you can place in all 3 apps.
Let me try to give you a detailed answer I can give as per my knowledge.
In order to get started, you should first complete try to complete Chat scenario without pusher. i.e: user should be able to send a message from front-end via the API and it should be stored inside the database.
Once you have done this it is very easy to include pusher in the flow. ( In simple words, you'll have to broadcast an event and that'll inform the Socket Server to broadcast a message to all/other user(s) on the channel )
For Pusher Authentication, you don't need to explicitly create a route and a method. Once you have uncommented BroadcastServiceProvider inside config/app.php. You can run:
php artisan route:list
and, you'll see a route for broadcast broadcasting/auth.
You can use this route to authenticate. Although, you can make few changes and prepend /api before this.
Go into BroadcastServiceProvider.php and replace your boot method with:
public function boot()
{
Broadcast::routes(
[
'prefix' => 'api',
'as' => 'api.broadcasting.auth',
'middleware' => ['auth:sanctum'],
]
);
require base_path('routes/channels.php');
}
I assume you're using Laravel Sanctum for Authentication. If not you need to change the authentication middleware to your provider.
Once done, you can authenticate from frontend using this auth route. So, what I have done is created a service in ReactJS and in the constructor I have created a Pusher instance :
this.pusher = new Pusher(PUSHER_APP_KEY, {
authEndpoint: 'http:localhost:8000/api/broadcasting/auth',
cluster: PUSHER_CLUSTER,
useTLS: true,
auth: {
headers: {
Authorization: 'Bearer ' + authHeader
}
}
});
You only need to instantiate your Pusher once and use this instance throughout the app. So, that's why I have created a service class for Pusher.
If you want things to be simple for now you need to execute this code on the page where you will use pusher. Once the Page load, you need to call this code. So, you'll do:
let pusher = null;
useEffect(() => {
pusher = new Pusher(PUSHER_APP_KEY, {
authEndpoint: 'http:localhost:8000/api/broadcasting/auth',
cluster: PUSHER_CLUSTER,
useTLS: true,
auth: {
headers: {
Authorization: 'Bearer ' + authHeader
}
}
});
}, []);
So, this way we have an instance of Pusher in our functional component or page.
Now, we need to subscribe to channel.
Using this instance of pusher we can subscribe to channels. If you have followed the useEffect approach on the same page then, right after getting the instance you can subscribe to channels and bind to events using this code:
const channel = pusher.subscribe('private-chat.' + channelName)
And, to bind to an event you can do:
channel.bind('event.name', function(data) {
console.log(data);
});
Make sure to replace "channelName" and "event.name" with your channel and event name respectively.
Now you'll be able to listen to your event once broadcasted from the backend.
So, you'll do something like this from the backend. You'll have a method that will store the message inside the database so, let's say that code is:
public function sendMessage (Request $request){
//.... Rest of the logic
$user = $request->user();
// Store the message
$chatMessage = $chat->messages()->create([
'message' => $message,
'sender_id' => $user->id
]);
broadcast(new NewMessage($user, $chatMessage))->toOthers();
//... Rest of the logic
}
This broadcast message will send this message to other user in the chat.
I hope this answer gives you a good idea and direction.
For work with WebSockets via Pusher on Laravel, I recommended using the package Laravel Echo for React part. And on the backend side in config/broadcasting.php setup configuration for Pusher.
See more detail on official documentation Laravel how to use Pusher on the backend side and frontend side.
https://laravel.com/docs/8.x/broadcasting#pusher-channels

Action cable: How to broadcast the chat messages only the specific chat room in Rails 5?

I currently have a Rails 5 application acting as my back-end,we can call this the "Core." I also have another Angular 1.6.4 application acting as my front-end, which is serving up Angular client side,And integrate with backed-end application through angular-actionable we can call this the "Front". These are two completely separate applications with completely different domains.
Basically, I am trying to integrate Action Cable through the Core and have it talk to the Front. I'm using this service here for the Front: enter link description here. As far as the Core, that's just basic Action Cable set up.
I have a list of chat rooms on admin side.
Problem: I sent message from client side but it broadcast message to all the chat rooms in admin side.I try to give the specific path of chat room in stream but still it broadcast message to all chat rooms.
I want to broadcast the message to specific chat room
Core
chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from stream_name
end
def unsubscribed
stop_all_streams
end
def receive(data)
ActionCable.server.broadcast stream_name, data.fetch('message')
end
private
def stream_name
"chat_channel_#{chat_id}"
end
def chat_id
params.fetch('data').fetch('chat')
end
end
Fornt
chatCtrl.js
app.run(function (ActionCableConfig){
ActionCableConfig.debug = true;
ActionCableConfig.wsUri= "ws://localhost:3000/cable";
});
app.controller('chatCtrl', ['$scope', 'ActionCableChannel',
function($scope, ActionCableChannel) {
var consumer = new ActionCableChannel("ChatChannel", { chat: 'localhost:3000/#!/chat/1'});
var callback = function(message) {
$scope.myData.push(message);
};
consumer.subscribe(callback).then(function(){
$scope.sendToMyChannel = function(message){
consumer.send(message);
};
$scope.$on("$destroy", function(){
consumer.unsubscribe().then(function(){ $scope.sendToMyChannel = undefined; });
});
});
}
]);
this is the solution for my problem, very similar to yours, but I did not use cofeescript, just javascript.. so I attach also my code if you need it.. I just check that the attribute data-chat-room-id from the DOM is equal to the one I am passing with the message:
The message controller create the message then with ActionCable.server.broadcast calls with those parameters (message, user, chatroom_id, lastuser) my js code in messages.js
class MessagesController < ApplicationController
def create
message = Message.new(message_params)
message.user = current_user
chatroom = message.chatroom
if message.save
ActionCable.server.broadcast 'messages',
message: message.content,
user: message.user.name,
chatroom_id: message.chatroom_id,
lastuser: chatroom.messages.last(2)[0].user.name
head :ok
end
end
end
Inside app/assets/javascrips/channels/messages.js I do a check that data.chatroom_id (the element from the DOM) equals chat_room_id (the element sent from the controller, of the object message just saved)
App.messages = App.cable.subscriptions.create('MessagesChannel', {
received: function(data) {
messages = $('#chatroom_id');
chat_room_id = messages.data('chat-room-id');
if (data.chatroom_id == chat_room_id) {
$('#messages').append(this.renderMessage(data));
};
},
renderMessage: function(data) {
return "<br><p> <strong>" + data.user + ": </strong>" + data.message + "</p>";
}
});
This is the article that gave me this answer
Action Cable chat Ilya Bodrov-Krukowski
Ilya Bodrov-Krukowski on Github
Another problem to tackle here is providing our script with the room’s id. Let’s solve it with the help of HTML data- attribute:
views/chat_rooms/show.html.erb
<div id="messages" data-chat-room-id="<%= #chat_room.id %>">
<%= render #chat_room.messages %>
</div>
Having this in place, we can use room’s id in the script:
javascripts/channels/rooms.coffee
jQuery(document).on 'turbolinks:load', ->
messages = $('#messages')
if $('#messages').length > 0
App.global_chat = App.cable.subscriptions.create {
channel: "ChatRoomsChannel"
chat_room_id: messages.data('chat-room-id')
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
# Data received
send_message: (message, chat_room_id) ->
#perform 'send_message', message: message, chat_room_id: chat_room_id

Send Notification properly using Socket.io Redis server

OBJECTIVE: When post is added to perticular channel send notification to all connected subscribed users for that channel.
Relational Database tables over view: Post, Channels, Users, Channels_Users
Post: id, title, content, channel_id(Indicate post is related to what channel)
Channels: id, name (List of Channels)
Users: id, username (User Table)
Channels_Users: id, channel_id, user_id (This table indicate which channels user is subscribed to.)
Goal: When post is created, send notification to perticular user group.
I have tried serveral ways and it is working as aspected. But I am looking for the correct way of doing this.
Server Side: socket.js
Naming conventions:
Channel name:
channel-1, channel-2, channel-3, ....
Namespace name('namespace-'+[channelName]) :
namespace-channel-1, namespace-channel-2, ...
var app = require('express')();
var request = require('request');
var http = require('http').Server(app);
var io = require('socket.io').listen(http);
var Redis = require('ioredis');
var redis = new Redis(####, 'localhost');
// Get all channels from Database and loop through it
// subscribe to redis channel
// initialize namespace inorder to send message to it
for(var i=1; i=<50; i++) {
redis.subscribe('channel-'+i);
io.of('/namespace-channel-'+i).on('connection', function(socket){
console.log('user connected');
});
}
// We have been subscribed to redis channel so
// this block will hear if there is any new message from redis server
redis.on('message', function(channel, message) {
message = JSON.parse(message);
// send message to specific namespace, with event = newPost
io.of('/namespace-'+channel).emit('newPost', message.data);
});
//Listen
http.listen(3000, function(){
console.log('Listening on Port 3000');
});
Client side: Angularjs Socket factory code: (Mentioning main socket code)
// userChannels: user subscribed channels array
// Loop through all channel and hear for new Post and
// display notification if there is newPost event
userChannels.forEach(function (c) {
let socket = {};
console.info('Attempting to connect to namespace'+c);
socket = io.connect(SOCKET_URL + ':3000/namespace-'+c, {query: "Authorization=token"});
socket.on('connect',function(){
if(socket.connected){
console.info("namespace-" + c + " Connected!");
// On New post event for this name space
socket.on('newPost', function(data) {
/* DISPLAY DESKTOP NOTIFICATION */
});
}
});
});
Publish data to Redis server, at the time of new post created
$data = [
'event' => 'newPost',
'data' => [
'title' => $postTitle,
'content' => $postContent
]
];
$redisChannel = "channel-" . $channelId; // As per our naming conventions
Redis::publish($redisChannel, json_encode($data));
The user is getting notification correctly for the channels they have been subscribed.
Problem 1: I am not sure this is the best solution to implement this notification thing.
Problem 2: When a user opens the same application in more than one browser tabs, it gets the notification for all of them. It should send only one notification. This is something related to user management at server side on redis end. I am not sure about this part too.
I really appreciate your suggestion/help on this. Thank you in advance.

Firebase: How can i use onDisconnect during logout?

How can i detect when a user logs out of firebase (either facebook, google or password) and trigger the onDisconnect method in the firebase presence system. .unauth() is not working. I would like to show a users online and offline status when they login and out, minimize the app (idle) - not just when the power off their device and remove the app from active applications on the device.
I'm using firebase simple login for angularjs/ angularfire
Im using code based off of this tutorial on the firebase site.
https://www.firebase.com/blog/2013-06-17-howto-build-a-presence-system.html
Please i need help with this!
Presence code:
var connectedRef = new Firebase(fb_connections);
var presenceRef = new Firebase(fb_url + 'presence/');
var presenceUserRef = new Firebase(fb_url + 'presence/'+ userID + '/status');
var currentUserPresenceRef = new Firebase(fb_url + 'users/'+ userID + '/status');
connectedRef.on("value", function(isOnline) {
if (isOnline.val()) {
// If we lose our internet connection, we want ourselves removed from the list.
presenceUserRef.onDisconnect().remove();
currentUserPresenceRef.onDisconnect().set("<span class='balanced'>☆</span>");
// Set our initial online status.
presenceUserRef.set("<span class='balanced'>★</span>");
currentUserPresenceRef.set("<span class='balanced'>★</span>");
}
});
Logout function:
var ref = new Firebase(fb_url);
var usersRef = ref.child('users');
service.logout = function(loginData) {
ref.unauth();
//Firebase.goOffline(); //not working
loggedIn = false;
seedUser = {};
clearLoginFromStorage();
saveLoginToStorage();
auth.logout();
};
The onDisconnect() code that you provide, will run automatically on the Firebase servers when the connection to the client is lost. To force the client to disconnect, you can call Firebase.goOffline().
Note that calling unauth() will simply sign the user out from the Firebase connection. It does not disconnect, since there might be data that the user still has access to.
Update
This works for me:
var fb_url = 'https://yours.firebaseio.com/';
var ref = new Firebase(fb_url);
function connect() {
Firebase.goOnline();
ref.authAnonymously(function(error, authData) {
if (!error) {
ref.child(authData.uid).set(true);
ref.child(authData.uid).onDisconnect().remove();
}
});
setTimeout(disconnect, 5000);
}
function disconnect() {
ref.unauth();
Firebase.goOffline();
setTimeout(connect, 5000);
}
connect();

Message failure in channel api

I am running a gae web app on localhost.
I have successfully generated a token from goog.channel and send it to client. Where client is able to accept the token and tries to open connection. The problem is that, I am sending a message from my servlet class and nothing is happening in client side.
Below are my codes:
Server Side:
//for generating token
ChannelService channelService=ChannelServiceFactory.getChannelService();
token = channelService.createChannel(userid);
//for sending message
ChannelService channelService=ChannelServiceFactory.getChannelService();
channelService.sendMessage(new ChannelMessage(userid, message));
//in appengine-web.xml
<inbound-services>
<service>channel_presence</service>
</inbound-services>
Javascript:
function getToken(){
var xmlhttpreq=new XMLHttpRequest();
xmlhttpreq.open('GET',host+'/channelapi_token?q='+user,false);
xmlhttpreq.send();
xmlhttpreq.onreadystatechange=alert(xmlhttpreq.responseText);
token=xmlhttpreq.responseText;
setChannel();
}
function setChannel(){
alert(token);//iam receiving right token here
channel=new goog.appengine.Channel(token);
socket=channel.open();
socket.open=alert('socket opened');//this message alerts
socket.onmessage=alert('socket onmessage');//this message alerts
socket.onerror=alert('socket onerror');//this message alerts
socket.onclose=alert('socket onclose');//this message alerts
}
There are no exceptions while sending message from channelservice.
Also the client side is repeatly making a get request to my server:
http://localhost:8888/_ah/channel/dev?command=poll&channel=channel-h1yphg-vivems#gmail.com&client=connection-3
What's the wrong happening here?
Thanks in advance.
You're calling alert(...) and assigning its return value to your message handlers. You should assign a function to these handlers instead:
socket.onopen = function() {
alert('socket opened');
};
// etc
// Note that message takes a parameter:
socket.onmessage = function(evt) {
alert('got message: ' + evt.data);
};
Note you can also do this like:
function onMessage(evt) {
// do something
}
socket.onmessage = onMessage;
Note that you're not assigning onMessage(), which will call onMessage and assign its return value.

Resources