Vjudge Contest Scheduler

All lines of code written in this project came from me... and maybe some small pieces from stackoverflow. This project is about webautomation with nodejs.


Context Information

This bot was created to aid me with administrating contests for the Doral Academy Computer Science Club. In our club we use Vjudge to host online competitve programming contests. These contests are made for training and evaluating our members for future competitive programming contests. Our club also uses a Discord Server for communcation between the members of the competitive programming team.

The Problem

Any member with the "manager" permission in the club's vjudge group can see contest problems before the contest has started. This is obviously an unfair advantage over other members. This project fixes that issue by adding all the problems into the contest right when it starts.

The Goals

I want a Discord Bot that will add problems into a vjudge contest at a time relative to the start of the contest. I will do this via GET and POST requests as opposed to something like selenium for two reasons: 1) I want this bot to be quick and lightweight, 2) I want to learn more in depth how the browsers use these requests to facilitate the entire user-experience on any given website. There are a few other goals which I'll list simply here:

  • Discord Bot as the user interface
  • Custom Permission Levels so that only some members can use these commands.
  • Announcing Contest start times in a specified Discord Channel
  • Adding problems to a contest via GET and POST requests

How to use it

Back to top

I won't go into the details of how to use discord. However, once you have the bot in your server and it is running, you can list commands with the !help command

There are a total of 4 commands. The first two are simple. So they won't be gone over.

!qcontest will announce a contest's start time 20 hours, 1 hour, 30 minutes before the contest and at the start time of the contest. Those intervals are hard-coded in because there was no need to make them custom in the UI.

!addproblem is the command which will add the problem into the contest. The default behavior is to immediately add the problem if seconds is not specified, if seconds is 0 it will also immediately add the problem. The channel is the Discord Channel where it will be announced that the problem has been added.

You may be wondering "what about the other arguments?!?" It is better to explain them with pictures!

First I'll create a contest in vjudge. I'll do that in the background as this isn't a vjudge tutorial.

Now we'll get the first argument, the contest ID. This can be found in the link of the contest (at the top). In this case it is 476338.

Let's take a quick glance at the update screen to see how we edit the problem set as  a user.

Now I can explain the second and third argument of the !addproblem command. The OJ is the "Original Judge" AKA the "source" of the problem. The ProbNum is the "problem_id", which can be a string as you can see.

I'll use the Kattis problems "Backspace" and "MovieCollection" to demonstrate the command in action.

Going back into discord, I'll tell the bot to add Backspace to the contest immediately and announce the addition of the problem in the current channel which is called test.

Well that was a bit uneventful, but if we refresh the contest page...

Huzzah! It works!

Sidenote: It's been about a year since I initially wrote this code, glad it still works perfectly without tweaking.

So now let's try adding "MovieCollections" in after the contest starts!

And the timestamps tell it all! It adds the problem in a second after the contest started.

I won't go over the !qcontest command because it should now be straight forward how it works. I also did not show how permission levels work because they are configured in the code. (Bad practice but this isn't production code and I'm not making a UI no-one will use)

How it works

Back to top

I won't go over all sections of the code but I will go over the general structure as well as a bit of how the code came to be. I'll also only go over the code relevant to !addProblem as that was the main purpose of this entire project.

There are a few steps which the code goes through in order to update the contest:

  1. Log In
  2. Get the current ProblemSet
  3. Modify ProblemSet
  4. Post the modified ProblemSet

I will make a note here that for all the http requests I opted to use node-libcurl after extensive searching for a library that would cater to my needs.

So, in order to login we need to perform a POST request to the URL 'https://vjudge.net/user/login' I found this URL by logging in while looking at the Network Tab of the Dev Tools.

Looking  at the "Request" section you can see my username and password sent as Form Data in plain text! No need to hash I guess!

I didn't know that form data existed before this project, but after some searching I found out how to specify form data AKA postFields in node-libcurl. A lot of scrambling to get the post to work and viola! I have my login function.

/**
 * 
 * @param {UserSession} session 
 * @param {string} username 
 * @param {string} password
 * @returns {Promise<boolean>}
 */
async function login(session,username,password){
    console.log(`Logging in. Cookies: ${session.getCookies()}`);
    let formdata = `username=${username}&password=${password}`;
    let response = await doRequest(false,session,'https://vjudge.net/user/login',{
        postFields: formdata,
        httpHeader: [
            'accept: */*',
            'accept-language: en-US,en;q=0.9',
            'content-type: application/x-www-form-urlencoded; charset=UTF-8',
            `cookie: ${session.getCookies()}`,
            `referer: https://vjudge.net/`,
            'origin: https://vjudge.net',
            'sec-fetch-site: same-origin',
            'content-type: application/x-www-form-urlencoded; charset=UTF-8',
            `content-length: ${formdata.length}`
        ]
    }); 
    console.log("Posted login");
    console.log(`Status Code: ${response.statusCode}`);
    console.log(`Response: ${response.data}`);
    if(response.data==='success'){
        //console.log(`Cookies are now: ${session.getCookies()}`);
        return true;
    }
    return false;
}

In the backend I made it possible to have mutliple users logged in at once, which is why there is the UserSession object. I wrote the front-end for it, but took it out as I was the only one adminstrating the contests and the front end process of logging into vjudge through direct messaging the discord bot was just an extra step I didn't care for.

After a lot of sifting through the network tab while navigating the site, I learned that vjudge likes to represent all the metadata about a contest's structure with a single JSON object. The problemset is also stored in this object.

This object can only be access while logged in as it is meant for maintenance. When clicking on the "update" in the contest page you can find the request we need.

Some more playing later and now I have a way to retrieve the JSON object.

/**
 * 
 * @param {UserSession} session 
 * @param {number} contest_id
 * @returns Object or null if the user needs to login.
 */
async function getProblemsetOBJ(session,contest_id){
    let url = `https://vjudge.net/contest/update/${contest_id}`;
    let response = await doRequest(true,session,url,{
        httpHeader: [
            `cookie: ${session.getCookies()}`
        ]
    });
    console.log(response.data);
    if(response.statusCode==200&&response.data) {
        let obj = response.data;
        if(obj.errMsg&&obj.errMsg==='Please login first'){
            return null;
        }
        return obj;
    }
    throw new Error(`Something went wrong.\nStatus: ${response.statusCode}\nData:\n${response.data}`);
}

For the last two steps I just simply edit problems field of the JSON Object and then send it back as the postField/formdata to the url "https://vjudge.net/contest/edit".

Developing the last two steps of that 4 step plan is very much the same as the other two. I won't bore anyone by saying "I looked at the network logs while playing with the website" for the third time already. So here's the code:

/**
 * 
 * @param {Object} problemset 
 * @param {Object} problem 
 * @param {UserSession} session  
 */
async function addProblem(session,problemset,problem){
    problemset.problems[problemset.problems.length] = {
        oj: problem[3],
        probNum: problem[4],
        descId: 0,
        pid: problem[0],
        alias: "",
        weight: 1
    };
    console.log(problemset);
    let url = "https://vjudge.net/contest/edit";
    let requestData = JSON.stringify(problemset);
    let {statusCode, data} = await doRequest(false,session,url,{
        postFields: requestData,
        httpHeader: [
            'accept: application/json, text/javascript, */*; q=0.01',
            'accept-language: en-US,en;q=0.9,sh;q=0.8,hr;q=0.7,sr;q=0.6,ru;q=0.5',
            `cookie: ${session.getCookies()}`,
            `content-length: ${requestData.length}`,
            'content-type: application/json',
            'origin: https://vjudge.net',
            `referer: https://vjudge.net/contest/${problemset.contestId}`
        ]
    });
    if(statusCode==200&&data){
        if(data.error){
            console.log(`Did not update: ${data.error}`);
        }
        return data;
    }
    throw new Error(`Error while editing contest.\nStatus: ${statusCode}\nData:\n${data}`);
}

As always, there are some finer details I didn't mention, like verifying that a problem actually exists before adding it! These things can be looked into further at the reader's own discretion. The link to the source code can be found below. Email for any questions!

The Conclusion

Back to top

Overall this project was a lot of fun, getting dirty with raw requests always is. I don't use this bot anymore because I am no longer in Highschool and am no longer in charge of administrating those online contests. However, I do continue to use the skills I gained from this in any webscrappers I make today, and it is indeed very nice to be able to make a fast, lightweight webscrapper.

The Code

Back to top

The source code for the entire project can be found here

The code is divided into 4 files:

  1. index.js - Main program, handles the front end experience and basic tasks
  2. contestutils.js - Utility Code, houses all the backend functions and classes for communicating with VJudge.
  3. fileutils.js - Utility Code, backend for any file operations
  4. constants.js - Where I store all sensitive and tedious data like passwords and IDs

You can learn a lot more about the source code by visiting it with the link given above.