quake3 server connector | remove ctrl characters | spectate q3 server | Search

The code is a Quake 3 server management tool that queries server status, captures all stats, and logs chat messages. It uses various modules and functions to interact with the server, including the gamedig module for querying server status and the sendRcon function for sending RCON commands to the server.

Run example

npm run import -- "quake3 server status"

quake3 server status

var importer = require('../Core')
var gamedig = require('gamedig')
var serverApi = importer.import("quake 3 server commands")
var { sendRcon, nextAllResponses, udpClient } = importer.import("quake 3 server commands")
var discordApi = importer.import("discord api")
var {authorizeGateway} = importer.import("authorize discord")
var {parseConfigStr} = importer.import("quake 3 server responses")
var removeCtrlChars = importer.import("remove ctrl characters")

async function getStatus(ip, port) {
    return gamedig.query({
        type: 'quake3',
        host: ip,
        port: port
    }).then((state) => {
        return state
    }).catch((error) => {
        console.log('Server is offline', error)
    })
}

async function captureAllStats() {
    var masters = await serverApi.listMasters('master.ioquake3.org', void 0, false)
    //var status = await getStatus(masters[1].ip, masters[1].port)
    var status = await getStatus('45.32.237.139', 27960)
    console.log(status.bots)
}

//typedef enum {
var SV_EVENT = {
	MAPCHANGE: 0,
    CLIENTSAY: 1,
    MATCHEND: 2,
    CALLADMIN: 3,
    CLIENTDIED: 4,
    CLIENTWEAPON: 5,
    CLIENTRESPAWN: 6,
    CLIENTAWARD: 7,
    GETSTATUS: 8,
    SERVERINFO: 9,
    CONNECTED: 10,
    DISCONNECT: 11,
}
//} recentEvent_t;


async function getChats(channelId) {
    var match = (/^(.*?):*([0-9]+)*$/ig).exec()
    await sendRcon('127.0.0.1', 27960, '', 'recentPassword')
    var response = await nextAllResponses()

    if(!response) return

    var maxTime = 0
    var parsed = response.map(function (r) {
        return JSON.parse(r.content)
    })
    var chats = parsed.filter(function (r) {
        if(r.timestamp > maxTime)
            maxTime = r.timestamp
        return r.type == SV_EVENT.CLIENTSAY
    })
    
    var call = parsed.filter(function (r) {
        return r.type == SV_EVENT.CALLADMIN
    })
    
    var status = parsed.filter(function (r) {
        return r.type == SV_EVENT.GETSTATUS
    })
    var server = {}
    if(status.length) {
        Object.assign(server, parseConfigStr(status[0].value))
    }

    var info = parsed.filter(function (r) {
        return r.type == SV_EVENT.SERVERINFO
    })
    if(info.length) {
        Object.assign(server, parseConfigStr(info[0].value))
    }
    
    var match = parsed.filter(function (r) {
        return r.type == SV_EVENT.MATCHEND
    })
    if(match.length) {
        // TODO: save to SQL database
        console.log(match[match.length-1])
    }

    var discordSocket = await authorizeGateway()
    //console.log(await discordApi.getGuildRoles('752561748611039362'))
    if(call.length) {
        await discordApi.triggerTyping(channelId)        
    }
    for(var i = 0; i < call.length; i++) {
        try {
            //console.log('Say: ' + call[i].value)
            await discordApi.createMessage({
                embed: {
                    title: removeCtrlChars(server.hostname || server.sv_hostname || server.gamename),
                    description: server.ip + ':' + server.port,
                    color: 0xdda60f,
                    fields: [
                        {
                            name: call[i].value,
                            value: `<@&752605581029802155> [Connect](https://quake.games/?connect%20${'127.0.0.1:27960'})`,
                            inline: false
                        },
                    ]
                },
                allowed_mentions: {
                    parse: ['users', 'roles'],
                    users: [],
                    roles: []
                }
            }, channelId)
            //await discordApi.createMessage(`@admin ${call[i].value}`, channelId)
        } catch (e) {
            console.log(e)
        }
    }
}

module.exports = getChats

What the code could have been:

// Import required modules
const importer = require('../Core');
const { join } = require('path');
const gamedig = require('gamedig');
const serverApi = importer.import('quake 3 server commands');
const {
  sendRcon,
  nextAllResponses,
  udpClient,
} = serverApi;
const discordApi = importer.import('discord api');
const { authorizeGateway } = importer.import('authorize discord');
const { parseConfigStr } = importer.import('quake 3 server responses');
const removeCtrlChars = importer.import('remove ctrl characters');

// Define constants
const SV_EVENT = Object.freeze({
  MAPCHANGE: 0,
  CLIENTSAY: 1,
  MATCHEND: 2,
  CALLADMIN: 3,
  CLIENTDIED: 4,
  CLIENTWEAPON: 5,
  CLIENTRESPAWN: 6,
  CLIENTAWARD: 7,
  GETSTATUS: 8,
  SERVERINFO: 9,
  CONNECTED: 10,
  DISCONNECT: 11,
});

// Define type for recent event
type RecentEvent = 'MAPCHANGE' | 'CLIENTSAY' | 'MATCHEND' | 'CALLADMIN' | 'CLIENTDIED' | 'CLIENTWEAPON' | 'CLIENTRESPAWN' | 'CLIENTAWARD' | 'GETSTATUS' | 'SERVERINFO' | 'CONNECTED' | 'DISCONNECT';

// Define a function to get server status
async function getStatus(ip: string, port: number) {
  try {
    const state = await gamedig.query({
      type: 'quake3',
      host: ip,
      port: port,
    });
    return state;
  } catch (error) {
    console.log('Server is offline:', error);
    return null;
  }
}

// Define a function to capture all stats
async function captureAllStats() {
  const masters = await serverApi.listMasters('master.ioquake3.org', void 0, false);
  const status = await getStatus('45.32.237.139', 27960);
  console.log(status?.bots);
}

// Define a function to get chats
async function getChats(channelId: string) {
  // Send RCON command to get recent events
  await sendRcon('127.0.0.1', 27960, '','recentPassword');

  // Get all responses
  const responses = await nextAllResponses();

  // Check if any responses were received
  if (!responses) return;

  // Parse responses into JSON
  const parsedResponses = responses.map((r) => JSON.parse(r.content));

  // Get events
  const events = parsedResponses.filter((r) => r.type ==='recentEvent');

  // Get specific events
  const chats = events.filter((r) => r.event.type === SV_EVENT.CLIENTSAY);
  const calls = events.filter((r) => r.event.type === SV_EVENT.CALLADMIN);
  const status = events.filter((r) => r.event.type === SV_EVENT.GETSTATUS);
  const server = {};
  if (status.length) {
    Object.assign(server, parseConfigStr(status[0].value));
  }

  // Get server info
  const info = parsedResponses.filter((r) => r.type === SV_EVENT.SERVERINFO);
  if (info.length) {
    Object.assign(server, parseConfigStr(info[0].value));
  }

  // Get match end events
  const matches = parsedResponses.filter((r) => r.type === SV_EVENT.MATCHEND);
  if (matches.length) {
    // TODO: save to SQL database
    console.log(matches[matches.length - 1]);
  }

  // Get Discord socket
  const discordSocket = await authorizeGateway();

  // Trigger typing
  if (calls.length) {
    await discordApi.triggerTyping(channelId);
  }

  // Create messages
  for (const call of calls) {
    try {
      await discordApi.createMessage({
        embed: {
          title: removeCtrlChars(server.hostname || server.sv_hostname || server.gamename),
          description: server.ip + ':' + server.port,
          color: 0xdda60f,
          fields: [
            {
              name: call.value,
              value: `<@&752605581029802155> [Connect](https://quake.games/?connect%20${'127.0.0.1:27960'})`,
              inline: false,
            },
          ],
        },
        allowed_mentions: {
          parse: ['users', 'roles'],
          users: [],
          roles: [],
        },
      }, channelId);
    } catch (e) {
      console.log(e);
    }
  }
}

module.exports = getChats;

Code Breakdown: Quake 3 Server Status and Chat Capture

Dependencies and Imports

The code imports various modules and functions from other files using the require function:

var importer = require('../Core')
var gamedig = require('gamedig')
var serverApi = importer.import('quake 3 server commands')
var { sendRcon, nextAllResponses, udpClient } = importer.import('quake 3 server commands')
var discordApi = importer.import('discord api')
var {authorizeGateway} = importer.import('authorize discord')
var {parseConfigStr} = importer.import('quake 3 server responses')
var removeCtrlChars = importer.import('remove ctrl characters')

Server Status Function

The getStatus function uses the gamedig module to query the Quake 3 server status:

async function getStatus(ip, port) {
    return gamedig.query({
        type: 'quake3',
        host: ip,
        port: port
    }).then((state) => {
        return state
    }).catch((error) => {
        console.log('Server is offline', error)
    })
}

Capture All Stats Function

The captureAllStats function uses the getStatus function to retrieve the server status and logs the bots array:

async function captureAllStats() {
    var masters = await serverApi.listMasters('master.ioquake3.org', void 0, false)
    var status = await getStatus('45.32.237.139', 27960)
    console.log(status.bots)
}

Enum Definition

The code defines an enum type SV_EVENT with various event types:

var SV_EVENT = {
    MAPCHANGE: 0,
    CLIENTSAY: 1,
    MATCHEND: 2,
    CALLADMIN: 3,
    CLIENTDIED: 4,
    CLIENTWEAPON: 5,
    CLIENTRESPAWN: 6,
    CLIENTAWARD: 7,
    GETSTATUS: 8,
    SERVERINFO: 9,
    CONNECTED: 10,
    DISCONNECT: 11,
}

Get Chats Function

The getChats function uses the sendRcon function to send a password to the server, retrieves the response using nextAllResponses, and parses the chat messages:

async function getChats(channelId) {
    //...
}

However, the implementation of the getChats function is incomplete and contains some errors (e.g., parseConfi should be parseConfigStr).