Source: utils/parseLogString.js

/**
 * Parses the log strings returned from ShowAdminLog into objects.
 *
 * @param {string} string
 * @returns {Object}
 */
const parseLogString = (string) => {

  // Escape any unescaped line ending characters
  const escapeRegex = /\n(?!\[.+? \(\d+\)\])/g;
  string = string.replace(escapeRegex, "\\n");

  // Parses the header into its segments.
  // Example log header: [31:53 min (1747596221)] KILL
  // 31:53      -> \1
  // min        -> \2
  // 1747596221 -> \3
  // KILL       -> \4
  const headerRegExp = /^\[(\d*|\d*\.\d*|\d*:\d*|\d*:\d*:\d*) (ms|sec|min|hours) \((\d*)\)\] (CONNECTED|DISCONNECTED|KILL|TEAM KILL|MATCH START|MATCH ENDED|TEAMSWITCH|MESSAGE|BAN|KICK|Player|CHAT\[Team\]|CHAT\[Unit\]|VOTESYS)/;

  if (!headerRegExp.test(string))
    throw new Error(`Invalid log header: ${string}`);

  const [relativeTime, relativeTimeFormat, rawTimestamp, logType] = headerRegExp.exec(string).slice(1, 5);
  const timestamp = Number(rawTimestamp);

  switch (logType) {
    case "BAN": {
      const banRegex = /BAN: \[(.*?)\] has been banned\. \[((PERMANENTLY BANNED|BANNED FOR ([,0-9]*?) HOURS) BY (VOTE|THE ADMINISTRATOR)!(?:\\n\\n(.*?))?)\]/;

      let [playerName, fullReason, banType, duration, bannedBy, customReason] = banRegex.exec(string).slice(1);

      duration = duration ? Number(duration.replaceAll(/,/g, "")) : null;

      banType = banType === "PERMANENTLY BANNED" ? "permanent" : "temporary";

      return {
        type: "playerBanned",
        banType,
        timestamp,
        temporary: !!duration,
        duration,
        bannedBy,
        playerName,
        fullReason,
        customReason
      };
    }

    case "CHAT[Team]": {
      const teamChatRegex = /CHAT\[Team\]\[(.*?)\((Axis|Allies)\/(.*?)\)\]: (.*)/;
      const [playerName, playerFaction, playerId, msg] = teamChatRegex.exec(string).slice(1);

      return {
        type: "teamChat",
        timestamp,
        playerName,
        playerId,
        playerFaction,
        message: msg
      };
    }

    case "CHAT[Unit]": {
      const unitChatRegExp = /CHAT\[Unit\]\[(.*?)\((Axis|Allies)\/(.*?)\)\]: (.*)/;
      const [playerName, playerFaction, playerId, msg] = unitChatRegExp.exec(string).slice(1);

      return {
        type: "unitChat",
        timestamp,
        playerName,
        playerFaction,
        playerId,
        message: msg
      };
    }

    case "CONNECTED": {
      const playerConnectedRegExp = /CONNECTED (.*?) \((.*?)\)/;
      const [playerName, playerId] = playerConnectedRegExp.exec(string).slice(1);

      return {
        type: "playerConnected",
        timestamp,
        playerId,
        playerName
      };
    }

    case "DISCONNECTED": {
      const playerDisconnectedRegExp = /DISCONNECTED (.*?) \((.*?)\)/;
      const [playerName, playerId] = playerDisconnectedRegExp.exec(string).slice(1);

      return {
        type: "playerDisconnected",
        timestamp,
        playerId,
        playerName
      };
    }

    case "KICK": {
      const kickRegExp = /KICK: \[(.*?)\] has been kicked\. \[(.*?)\]/;
      const [playerName, reason] = kickRegExp.exec(string).slice(1);

      return {
        type: "playerKicked",
        timestamp,
        playerName,
        reason
      };
    }

    case "KILL": {
      const playerKillRegExp = /KILL: (.*?)\((Axis|Allies)\/(.*?)\) -> (.*?)\((Axis|Allies)\/(.*?)\) with (.*)/;
      const [killerName, killerFaction, killerId, victimName, victimFaction, victimId, weapon] = playerKillRegExp.exec(string).slice(1);

      return {
        type: "playerKilled",
        timestamp,
        killerName,
        killerFaction,
        killerId,
        victimName,
        victimFaction,
        victimId,
        weapon
      };
    }

    case "MATCH ENDED": {
      const matchEndedRegExp = /`(.*?)` ALLIED \((\d) - (\d)\) AXIS/;
      const [mapName, alliesScore, axisScore] = matchEndedRegExp.exec(string).slice(1);

      return {
        type: "matchEnded",
        timestamp,
        mapName,
        alliesScore: Number(alliesScore),
        axisScore: Number(axisScore)
      };
    }

    case "MATCH START": {
      const matchStartRegExp = /MATCH START (.*)/;
      const mapName = matchStartRegExp.exec(string)[1];

      return {
        type: "matchStart",
        timestamp,
        mapName
      };
    }

    case "MESSAGE": {
      const messageRegExp = /MESSAGE: player \[(.*?)\((.*?)\)\], content \[(.*)\]/;
      const [playerName, playerId, msg] = messageRegExp.exec(string).slice(1);

      return {
        type: "message",
        timestamp,
        playerName,
        playerId,
        message: msg
      };
    }

    // Admin camera
    case "Player": {
      const adminCameraRegExp = /Player \[(.*?) \((.*?)\)\] (Entered Admin Camera|Left Admin Camera)/;
      const [playerName, playerId, msg] = adminCameraRegExp.exec(string).slice(1);

      const action = msg === "Entered Admin Camera";

      return {
        type: action ? "playerEnteredAdminCamera" : "playerLeftAdminCamera",
        timestamp,
        playerId,
        playerName
      };
    }

    case "TEAMSWITCH": {
      const teamSwitchRegExp = /TEAMSWITCH (.*?) \((None|Allies|Axis) > (None|Allies|Axis)\)/;
      const [playerName, oldFaction, newFaction] = teamSwitchRegExp.exec(string).slice(1);

      return {
        type: "playerSwitchFaction",
        timestamp,
        playerName,
        oldFaction,
        newFaction
      };
    }

    case "TEAM KILL": {
      const teamKillRegExp = /TEAM KILL: (.*?)\((Axis|Allies)\/(.*?)\) -> (.*?)\((Axis|Allies)\/(.*?)\) with (.*)/;
      const [killerName, killerFaction, killerId, victimName, victimFaction, victimId, weapon] = teamKillRegExp.exec(string).slice(1);

      return {
        type: "playerTeamKilled",
        timestamp,
        killerName,
        killerFaction,
        killerId,
        victimName,
        victimFaction,
        victimId,
        weapon
      };
    }

    case "VOTESYS": {
      const voteStartedRegExp = /Player \[(.*?)\] Started a vote of type \((PVR_Kick_Abuse|PVR_Kick_Cheating)\) against \[(.*?)\]. VoteID: \[(\d*?)\]/;
      const voteCastRegExp = /VOTESYS: Player \[(.*?)\] voted \[(PV_Favour|PV_Ignored|PV_Against)\] for VoteID\[(\d*?)\]/;
      const voteExpiredRegExp = /VOTESYS: Vote \[(\d*?)\] expired before completion./;
      const prematurelyExpiredRegExp = /VOTESYS: Vote \[(\d*?)\] prematurely expired./;
      const voteCompletedRegExp = /VOTESYS: Vote \[(\d*?)\] completed. Result: (PVR_Passed|PVR_ExpiredOrCancelled)/;
      const votePassedRegExp = /VOTESYS: Vote Kick \{(.*?)\} successfully passed. \[For: (\d.*?)\/(\d.*?) - Against: (\d.*?)\]/;

      if (voteStartedRegExp.test(string)) {
        const res = voteStartedRegExp.exec(string);
        const [executorName, type, targetName, voteId] = res.slice(1, 5);

        return {
          type: "voteStarted",
          timestamp,
          voteId: Number(voteId),
          voteType: type,
          executorName,
          targetName
        };
      }

      if (voteCastRegExp.test(string)) {
        const res = voteCastRegExp.exec(string);
        const [playerName, action, voteId] = res.slice(1, 4);

        return {
          type: "voteCast",
          timestamp,
          voteId: Number(voteId),
          playerName,
          action
        };
      }

      if (voteExpiredRegExp.test(string)) {
        const voteId = voteExpiredRegExp.exec(string)[1];

        return {
          type: "voteExpiredBeforeCompletion",
          timestamp,
          voteId: Number(voteId)
        };
      }

      if (prematurelyExpiredRegExp.test(string)) {
        const voteId = prematurelyExpiredRegExp.exec(string)[1];

        return {
          type: "votePrematurelyExpired",
          timestamp,
          voteId: Number(voteId)
        };
      }

      if (voteCompletedRegExp.test(string)) {
        const res = voteCompletedRegExp.exec(string);
        const [voteId, result] = res.slice(1, 3);

        return {
          type: "voteCompleted",
          timestamp,
          voteId: Number(voteId),
          result
        };
      }

      if (votePassedRegExp.test(string)) {
        const res = votePassedRegExp.exec(string);
        const [playerName, forVotes, requiredVotes, againstVotes] = res.slice(1, 5);

        return {
          type: "votePassed",
          timestamp,
          playerName,
          forVotes: Number(forVotes),
          requiredVotes: Number(requiredVotes),
          againstVotes: Number(againstVotes)
        };
      }
    }
  }
};

module.exports = parseLogString;