What kind of events do reviews listen to and emit

Hi, I am developing an application that can play moves in a demo board. I checked the RT api doc but there are only events related to games and not reviews. Does anyone know where I can get the list of events for reviews?

I had a quick look at the client code that is used when we are clicking around reviews, and it’s fairly scary from the point of view of what you are trying to achieve.

The user-experience of a “review” is implemented by the “Game” view.

The challenge of reverse engineering the API for this is that the Game view instantiates a Goban, and it’s the Goban that has the connection to the server.

So if you search in Game.tsx for \.on\( you can find all the Goban events that the Game view listens to.

Then you’d have to look in Goban for what server events causes those…

… or at least, that’s how it looks to me :sweat_smile:

I guess on the up-side … game events and review events are probably highly overlapping, given the way this is working (via Goban) :thinking:

1 Like

Yeah I’d recommend avoiding the code in this case (or at least make it a secondary reference). Here’s how I’d try to reverse engineer the API:

  1. Open network tab (and refresh if the socket is not there)
  2. do what you want to do (e.g. play a move)
  3. use the network tab to see what message comes over the socket connection (e.g. review/move)
  4. It may be obvious what everything means, but if it’s not, then look up the message in the code
    • As GaJ mentioned, the messages you’re interested in probably live in goban, not the react repo
  5. Try sending that message from your code. tweak parameters, and see how that changes the behavior. Maybe you don’t need a full understanding of the message to get it to do what you need it to.
  6. If still unclear, ask here, we’d be happy to try and figure this out together! (as long as you’ve done due diligence first :yum:)
3 Likes

I agree with @benjito’s approach. It’s the best way to figure out how to use the API.

1 Like

Hi @GreenAsJade @benjito @wolfeystudios, I need some help with appending moves to a demo board. According to the socket traffic in network tab, I need to authenticate first with chat_auth token.

I might or might not need to connect to the review. Probably not because … just because I think. I tried connecting to it in my code and it didn’t work.

In review append it’s asking me these data, and I included them all in my code.

However, I am not able to add moves to the review still. I am not sure what’s missing. I am connected to OGS socket because I can get response from the hostinfo event emission.

Here are the source code of my four files, three library files and one index.js file.

index.js

require('dotenv').config();

const ogsapi = require('./ogsapi.js');
const listener = require('./socket-event-listener.js');
const emitter = require('./socket-event-emitter.js');

const USERNAME = process.env.USERNAME;
const PASSWORD = process.env.PASSWORD;
const CLIENT_ID = process.env.CLIENT_ID;
const GRANT_TYPE = process.env.GRANT_TYPE;

async function main() {
	let accessToken = await ogsapi.generateAccessToken(USERNAME, PASSWORD, CLIENT_ID, GRANT_TYPE);
	
	let config = await ogsapi.getUIConfig(accessToken);
	let chatAuth = config['chat_auth'];
	let notificationAuth = config['notification_auth'];
	let incidentAuth = config['incident_auth'];
	let userID = config['user']['id'];
	let jwt = config['user_jwt'];

	let reviewID = await ogsapi.createDemoBoard(accessToken, 'test1', 'bplayer', 10, 'wplayer', 10, 19, 19, 'japanese', 'false');

	let socket = await ogsapi.generateOGSSocketHandler();

	listener.listenHostinfo(socket);
	
	emitter.emitAuthenticate(socket, chatAuth, userID, USERNAME, jwt);

	emitter.emitConnectReview(socket, chatAuth, reviewID, userID);

	emitter.emitReviewAppendMove(socket, 'abcdefgh', reviewID, userID);

	emitter.emitHostinfo(socket);
}

main();

ogsapi.js

require('dotenv').config();
const axios = require('axios');
const FormData = require('form-data');
const { io } = require("socket.io-client");

async function generateAccessToken(username, password, clientID, grantType) {
	let url = 'https://online-go.com/oauth2/token/';
	let data = new FormData();
	data.append('username', username);
	data.append('password', password);
	data.append('client_id', clientID);
	data.append('grant_type', grantType);

	let res = await axios.post(url, data);
	return res['data']['access_token']
}

async function getUIConfig(accessToken) {
	let url = 'https://online-go.com/api/v1/ui/config';

	let res = await axios.get(url, {
		headers: {
			Authorization: `Bearer ${accessToken}`
		}
	});
	return res['data']
}

async function createDemoBoard(accessToken, boardName, bName, bRank, wName, wRank, width, height, rules, isPrivate) {
	let url = 'https://online-go.com/api/v1/demos';
	let data = new FormData();
	data.append('black_name', bName);
	// data.append('black_pro', 1); // only if ranking is pro
	data.append('black_ranking', bRank);
	data.append('height', height);
	data.append('name', boardName);
	data.append('private', isPrivate);
	data.append('rules', rules);
	data.append('white_name', wName);
	// data.append('white_pro', 1); // only if ranking is pro
	data.append('white_ranking', wRank);
	data.append('width', width);

	let res = await axios.post(url, data, {
		headers: {
			Authorization: `Bearer ${accessToken}`
		}
	});
	console.log('demo board created', res['data']['id']);
	return res['data']['id']
}

function generateOGSSocketHandler() {
	let url = 'https://online-go.com';
	let params = {
		transports: ['websocket']
	};

	let socket = io(url, params);

	socket.on('connect', () => {
		console.log('connected to OGS socket!');
	});
	
	return socket
}

module.exports = {
	generateAccessToken,
	getUIConfig,
	generateOGSSocketHandler,
	createDemoBoard
}

socket-event-listener.js

function listenReviewFullState(socket, reviewID) {
	let eventName = `review/${reviewID}/full_state`;
	socket.on(eventName, (msg) => {
		console.log(msg);
	});
	console.log('listening to listenReviewFullState');
}

function listenReviewMoveResponse(socket, reviewID) {
	let eventName = `review/${reviewID}/r`;
	socket.on(eventName, (msg) => {
		console.log(msg);
	});
	console.log('listening to listenReviewMoveResponse');
}

function listenHostinfo(socket) {
	let eventName = 'hostinfo';
	socket.on(eventName, (msg) => {
		console.log(msg);
	});
	console.log('listening to listenHostinfo');
}

module.exports = {
	listenReviewFullState,
	listenReviewMoveResponse,
	listenHostinfo
}

socket-event-emitter.js

function emitAuthenticate(socket, chatAuth, playerID, username, jwt) {
	let eventName = 'authenticate';
	let data = {
		auth: chatAuth,
		player_id: playerID,
		username: username,
		jwt
	};
	socket.emit(eventName, data);
	console.log('emitting emitAuthenticate');
}

function emitConnectReview(socket, chatAuth, reviewID, playerID) {
	let eventName = 'review/connect';
	let data = {
		auth: chatAuth,
		review_id: reviewID,
		player_id: playerID
	};
	socket.emit(eventName, data);
	console.log('emitting emitConnectReview');
}

function emitHostinfo(socket) {
	let eventName = 'hostinfo';
	socket.emit(eventName);
	console.log('emitting emitHostinfo');
}

function emitConnectChat() {

}

function emitConnectNotification() {

}

function emitConnectIncident() {

}

function emitReviewAppendMove(socket, updatedMoveString, reviewID, playerID) {
	let eventName = 'review/append';
	let data = {
		f: 0,
		t: '',
		m: updatedMoveString,
		k: {},
		review_id: reviewID,
		player_id: playerID
	};
	socket.emit(eventName, data);
	console.log('emitting emitReviewAppendMove');
}

module.exports = {
	emitAuthenticate,
	emitConnectReview,
	emitReviewAppendMove,
	emitHostinfo
}

p.s. I am not sure if there is any info I shouldn’t have included in the image. Please let me know if there is.

Thanks!

1 Like

This is excellent detail, thanks! I don’t see anything wrong with the code immediately, so I will need to try running this later. (no chance of a git repo? I would love to git clone ... && npm run ... :sweat_smile:)

A couple of thoughts now that might give you something to try before I get this running:

  • socket.io allows catchall, so you could console.log every message you get back: socket.onAny()
  • I notice your browser tab just sends "missing-auth" in the “review/connect” message. Maybe try that instead (I don’t think this matters, but always good to try to do exactly what OGS does and then modifying once you know what works.)
  • “chat/join” to join the review chat might help. I don’t really know, but it seems like chat auth is intermeshed with game auth.
  • Are you getting anything back besides hostinfo? Like does the server at least send the “full state” message?

Thanks for the response! I will try socket.onAny() to see what gets printed out. There was a point in time when my codes worked occasionally, ie was able to make a move in a demo board but I couldn’t get it to work now. I can get the full state from a demo board.

1 Like

Oh hey that’s great news - means we’re close haha

The one other thing - if you’re noticing it works, then doesn’t without any code changes, it could be rate limiting at play. Hard to say. With the REST api you’d get a clear error, but with socket api I dunno.

Rate limiting doesn’t seem to make sense because I was making moves more frequently in the browser than when I tested my codes. I guess I will just rely on console logging all the incoming messages.

1 Like

onAny unfortunately didn’t provide any error message for me to triage where the problem is…

It did give me an expected response when I make a move from the website though.

1 Like

@benjito @wolfeystudios @GreenAsJade sorry to spam you again. I created a github repo for convenience’ sake and included the latest findings in the readme file. I found the pattern how the code might work occasionally but cannot explain why that is.

It would be nice if you can take a look and comment on the outcome.

Thanks!

1 Like

Will run the code later!

Interesting findings in the readme- so I guess somehow interacting with the browser somehow gets it working for your script for some amount of time?

I’m trying to figure out what could cause that. I know one token we haven’t talked about yet is CSRF. I thought that only affected REST requests, but maybe needs to be set for socket connections as well. I vaguely remember having similar things in REST-only scripts where I needed to log in via browser first. But in those cases I didn’t even attempt to log in from the scripts

I didn’t include any CSRF token in my script though and it worked from time to time. I feel it’s because I am not sending RT ping messages. According to OGS API notes,

the client should send RT ping messages every so often (20 seconds works) to keep the RT connection alive.

However, I can still receive the response from the server when I play a move in the browser without sending the ping message. That means the socket connection is still there. I am not sure what the browser is doing and the code is not…

This issue has been resolved. lvankeulen pointed out that the problem happened because I didn’t give server enough time to process the authentication web socket event before using an api call that requires authentication. After some sleep functions got incorporated into the program, appending moves starts to work properly. In reality, humans need time to play moves so there isn’t this issue.

Here is the code to add to indextest.js make the test program work:

listener.listenReviewAppendResponse(socket, reviewID);
  await new Promise(resolve => setTimeout(resolve, 1000));
  emitter.emitAuthenticate(socket, chatAuth, userID, USERNAME, jwt);
  await new Promise(resolve => setTimeout(resolve, 1000));
  emitter.emitConnectReview(socket, chatAuth, reviewID, userID);
  await new Promise(resolve => setTimeout(resolve, 1000));
  emitter.emitReviewAppendMove(socket, 'fgggffgfifigkgkfjfjgjhijjjkjihhhdd', reviewID, userID);
  await new Promise(resolve => setTimeout(resolve, 1000));
  emitter.emitHostinfo(socket);

I will probably refactor my program a little, but having the communication to OGS successful is a really big step forward. I really appreciate all of you guys’ help!! Time for some arduino / serial port work XD

2 Likes

Here is the comment from Ivankeulen regarding his termsuji project Termsuji: Play Go in your terminal (if you really want to) and what I am trying to achieve using NodeJS:

There’s some sleep time in termsuji, since after connecting to the board you have to manually input the move. This could take the user a few seconds. On the online-go website, the time between page load/socket authentication and the moment a board position is actually clicked is enough time as well, but right now during testing all the socket emissions are instant. I don’t know how online-go handles those.

The refresh token is for Oauth purposes. If you call /oauth2/token you’ll get an authentication token and a refresh token. The authentication token is the one you use for API calls, but it will expire (typically after a few hours). The refresh token does not expire (unless access is revoked in some way, I think changing your password does this) and can be used once to get a new authentication+refresh token. I only use it so if it’s still valid, the user doesn’t have to input their password again - it’s technically unnecessary in my case.

Hope this can be helpful to people.

1 Like

Wonderful! I was just about to come back here and tell you that I ran it, but was thoroughly stumped!

The sleep stuff makes sense, but kind of weird that having a browser open changes the behavior. I wonder if it slows down the network calls, or if auth from the browser “counts” for the script as well. Great find @zoodiabloc and @lvankeulen!

The sleep is most likely not needed if I want to use the codes in a production setting because between authenticating the user and playing the first move there is usually some time. That happens in the browser too, after the browser is ready to display stuff, enough time already passed to finish authenticating users. It’s not working in my code because I instantly append new moves after calling the api to authenticate users.

I want to thank you all for helping! It would be impossible for me to understand how the api works without you guys’ input and guidance. :smiley:

2 Likes