initial commit

This commit is contained in:
James Alexander 2024-08-02 18:55:55 -04:00
parent 117fe951c5
commit b75e0cdcf4
35 changed files with 11982 additions and 0 deletions

View File

@ -0,0 +1,108 @@
_addon.name = "LinkCloud";
_addon.author = "James Alexander";
_addon.version = "0.1.2w";
_addon.commands = {
"lcloud",
"linkcloud",
"lc"
};
require("logger");
require("chat");
defaults = {};
defaults.HostAddress = "linkcloud.drunken.games";
defaults.HostPort = 6033;
defaults.TCPTimeout = 2;
defaults.AuthKey = "NONE";
defaults.AutoConnect = true;
defaults.AutoReconnect = true;
defaults.Platform = 1;
defaults.ProcessShouts = false;
json = require("lunajson");
files = require("files");
config = require("config");
settings = config.load(defaults);
linkCloud = require("LinkCloudRoutine")
function getPlayerData()
return windower.ffxi.get_player()
end
function getServerData()
return windower.ffxi.get_info()
end
function sendInputCommand(command)
return windower.chat.input(command)
end
function sendNoticeMessage(message)
notice(message)
end
function sendErrorMessage(message)
error(message)
end
function saveSettings()
settings:save()
end
function playerIsLsOwner()
return true
end
linkCloudParams = {
json = json,
settings = settings,
getPlayerData = getPlayerData, --must gave .id, .name
getServerData = getServerData, --must have .time .server
sendInputCommand = sendInputCommand, --method to write a console command
sendNoticeMessage = sendNoticeMessage, --method to send chat notice to player
sendErrorMessage = sendErrorMessage, --method to send error notice to player
playerIsLsOwner = playerIsLsOwner, --method to determine if player is ls owner
saveAddonConfig = saveSettings, --method to save addon config
consoleLog = print
}
linkCloud.hook(linkCloudParams)
windower.register_event("incoming text", function(_, text, mode, modemod, blocked)
if blocked or text == "" then
return;
end;
if mode == 217 then --lsmes2 update
linkCloud.onLSMesUpdate(2, text:strip_format())
elseif mode == 205 then --lsmes update
linkCloud.onLSMesUpdate(1, text:strip_format())
elseif (mode == 6 or mode == 14) then --ls1 chat
linkCloud.onLSMessageReceived(1, text:strip_format())
elseif (mode == 213 or mode == 214) then --ls2 chat
linkCloud.onLSMessageReceived(2, text:strip_format())
elseif mode == 11 then --bot spa...i mean shouts
linkCloud.onShoutReceived(text:strip_format())
end;
end);
windower.register_event("addon command", function(command, ...)
command = command and command:lower() or "help";
local args = T({
...
});
if command == "connect" or command == "c" then
linkCloud.ConnectToDiscordBot();
elseif command == "status" or command == "s" then
linkCloud.showstatus();
elseif command == "test" then
print(windower.ffxi.get_player().linkshell_slot)
elseif command == "addlinkshell" then
linkCloud.sendLinkshellAddRequest(args[1]);
elseif command == "autoconnect" then
linkCloud.cmdToggleAutoConnect()
elseif command == "autoreconnect" then
linkCloud.cmdToggleAutoReconnect()
elseif command == "shouts" then
linkCloud.cmdToggleShouts()
elseif command == "v" or command == "ver" or command == "version" then
linkCloud.cmdShowVersion(_addon.version)
elseif command == "help" or command == "h" then
linkCloud.cmdShowHelpMenu()
else
error(" ***** That is not a valid LinkCloud command. See //lcloud help. *****");
end;
end);

View File

@ -0,0 +1,401 @@
local _version = '0.1.2'
local socket = require("socket");
local nextBuffer = 1;
local connected = false;
local shouldRetryConnection = false;
local authed = false;
--lsOneBuffer = {};
--lsTwoBuffer = {};
local otherbuffer = {};
--lsOne = {};
--lsTwo = {};
local echos = {};
local linkShells = {}
linkShells[1] = {}
linkShells[2] = {}
local lsBuffers = {}
lsBuffers[1] = {}
lsBuffers[2] = {}
local tcp = assert(socket.tcp());
local framework = nil
function hook (params)
framework = params
sendNotice('LinkCloud Version: ' .. _version .. ' loaded.')
if framework.settings.AutoConnect then
ConnectToDiscordBot();
else
sendNotice('AutoConnect is disabled. Use //lc connect to start streaming.')
end;
SendBuffer()
end
-- remove send packet from buffer (called after server confirms receipt)
function removePacketFromBuffers(packetId)
for k, v in pairs(otherbuffer) do
if (framework.json.decode(v)).packetId == packetId then
table.remove(otherbuffer, k);
end;
end;
for k, v in pairs(lsBuffers[2]) do
if (framework.json.decode(v)).packetId == packetId then
table.remove(lsBuffers[2], k);
end;
end;
for k, v in pairs(lsBuffers[1]) do
if (framework.json.decode(v)).packetId == packetId then
table.remove(lsBuffers[1], k);
end;
end;
end;
-- Ticks every 250ms sends the next buffered item to the server
function SendBuffer()
if authed then
if tcp then
nextMessage = nil;
if nextBuffer == 1 and linkShells[1].name ~= nil then
nextMessage = lsBuffers[1][1];
elseif nextBuffer == 2 then
nextMessage = otherbuffer[1];
elseif nextBuffer == 3 and linkShells[2].name ~= nil then
nextMessage = lsBuffers[2][1];
end;
if nextMessage and connected then
sendToBot(nextMessage);
end;
end;
nextBuffer = nextBuffer + 1;
if nextBuffer > 3 then
nextBuffer = 1;
end;
end;
coroutine.schedule(SendBuffer, 0.25);
end;
--Notify the user the connection has been lost.
--If auto reconnect is enabled, retry the connection in 30 seconds
function notifiyConnectionLost()
connected = false;
authed = false;
tcp:close();
if framework.settings.AutoReconnect then
sendError("You have lost connection to the LinkCloud server. The connection attempt will be retried shortly.");
coroutine.schedule(ConnectToDiscordBot, 30);
else
sendError("You have lost connection to the LinkCloud server. Use //lc connect to retry the connection.");
end;
end;
--Parse /lsmes message
function getLSFromLsmes(str)
lsData = {};
lsData.shellNumber = 0;
slugs = split(str, " ");
if string.match(slugs[1], "(%d:)") then
sendError("timestamp addon must be disabled.");
notifiyConnectionLost();
elseif slugs[1] == "[1]<" then
lsData.shellNumber = 1;
lsData.name = string.sub(slugs[2], 0, slugs[2]:len() - 1);
elseif slugs[1] == "[2]<" then
lsData.shellNumber = 2;
lsData.name = string.sub(slugs[2], 0, slugs[2]:len() - 1);
end;
return lsData;
end;
--javascript like split function (thanks google)
function split(s, sep)
local fields = {};
local sep = sep or " ";
local pattern = string.format("([^%s]+)", sep);
string.gsub(s, pattern, function(c)
fields[(#fields) + 1] = c;
end);
return fields;
end;
--uuidv4 style random generator
function uuid()
local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
return string.gsub(template, "[xy]", function(c)
local v = c == "x" and math.random(0, 15) or math.random(8, 11);
return string.format("%x", v);
end);
end;
--Put all the packet peices together into one packet, convert to json
function buildPacket(type, payload)
packet = {};
packet.metaData = metaData();
packet.type = type;
packet.payload = payload;
packet.packetId = uuid();
return framework.json.encode(packet) .. "\n";
end;
--sets the authkey
function buildConnectionPacket()
packet = {};
packet.authId = framework.settings.AuthKey;
return packet;
end;
--builds metadata to send along with the packet
function metaData()
info = framework.getServerData();
data = {};
playerData = framework.getPlayerData()
data.character = playerData and playerData.name;
data.client_id = playerData and playerData.id;
data.server = info.server;
data.gameTime = info.time;
data.clientTime = os.time();
data.platform = framework.settings.Platform;
return data;
end;
--builds a packet containing ls message data
function buildMessagePayload(message, ls)
slugs = split(message, "> ");
slugs = split(slugs[1], " <");
nameparsed = slugs[2];
message = message:gsub("%" .. slugs[1] .. "<" .. nameparsed .. "> ", "");
payload = {};
payload.name = nameparsed:gsub("[\n\r]", "");
payload.message = message:gsub("[\n\r]", "");
payload.linkshellname = ls:gsub("[\n\r]", "");
return payload;
end;
--builds a packet containing shout data
function buildShoutPayload(msg)
slugs = split(msg, ":");
nameArea = slugs[1];
table.remove(slugs, 1);
nameAreaSlugs = split(nameArea, "[");
message = table.concat(slugs, ":");
name = nameAreaSlugs[1];
area = nameAreaSlugs[2]:gsub("]", "");
payload = {};
payload.name = name;
payload.area = area;
payload.message = string.gsub(message, "^%s*(.-)%s*$", "%1");
return payload;
end;
--build the "other" payload (testing only)
function buildOtherPayload(msg, mode, modemod, type)
payload = {};
payload.message = msg;
payload.type = type;
payload.mode = mode;
payload.modemod = modemod;
return payload;
end;
--force the game to display the lsmes for both linkshells (used to determine which ls is in which slot)
function getLSMes()
framework.sendInputCommand("/lsmes")
coroutine.sleep(2);
framework.sendInputCommand("/ls2mes");
end;
--restart the receiver coroutine
function setupReceiver()
RECEIVER_ROUTINE = coroutine.schedule(listen, 0.25);
end;
--closes the tcp connection if its open, resets connection and auth bits, recreate the tcp connection, perform auth challenge if connection is successful
function ConnectToDiscordBot()
tcp:close();
connected = false;
authed = false;
tcp = assert(socket.tcp());
tcp:connect(framework.settings.HostAddress, framework.settings.HostPort);
tcp:setoption("keepalive", true);
tcp:settimeout(framework.settings.TCPTimeout);
connectionpacket = tcp:receive("*l");
if connectionpacket == "CHALLENGE" then
connected = true;
sendToBot(buildPacket("HANDSHAKE", buildConnectionPacket()));
setupReceiver();
elseif framework.settings.AutoReconnect then
sendError("Unable to connect.")
sendError("The discord bot is either offline or your connection information is incorrect.")
sendError("Use //lc autoreconnect to stop reconnection attempts.");
RECONNECT_ROUTINE = coroutine.schedule(ConnectToDiscordBot, 30);
else
sendError("Unable to connect. The discord bot is either offline or your connection information is incorrect.");
end;
end;
--wrapper for tcp:send
function sendToBot(payload)
tcp:send(payload);
end;
--called on a coroutine on an interval when connected to retreive and process new socket data
function handleResponse()
tcp:settimeout(0);
response = tcp:receive("*l");
packet = response and framework.json.decode(response);
if packet then
heartbeatReply = true;
if packet.type == "HANDSHAKE" then
if packet.payload == "ACCEPTED" then
authed = true;
coroutine.schedule(heartbeat, 1);
sendNotice("Streaming to discord enabled.");
getLSMes();
SendBuffer();
else
sendError("LinkCloud handshake failed. Please check your API key and try again.");
end;
elseif packet.type == "SYSTEM_MESSAGE" then
if packet.payload.isError then
sendError(packet.payload.message);
else
sendNotice(packet.payload.message);
end;
elseif packet.type == "LS_ECHO" then
if packet.payload.linkshell == linkShells[1].name then
windower.chat.input("/l [" .. packet.payload.from .. "] " .. packet.payload.message:gsub("[\n\r]", " "));
elseif packet.payload.linkshell == linkShells[2].name then
windower.chat.input("/l2 [" .. packet.payload.from .. "] " .. packet.payload.message:gsub("[\n\r]", " "));
end;
--some stuff to make sure im not sending the message the server told me to send back to the server again
table.insert(echos, "[" .. packet.payload.from .. "] " .. packet.payload.message:gsub("[\n\r]", " "));
if #echos > 10 then
table.remove(echos, 1);
end;
else
removePacketFromBuffers(packet.packetId);
end;
end;
end;
--array.includes
function has_value(tab, val)
for index, value in ipairs(tab) do
if value == val:gsub("[\n\r]", " ") then
return true;
end;
end;
return false;
end;
--listen wrapper
function listen()
handleResponse();
setupReceiver();
end;
--sends a heartbeat every second
function heartbeat()
if heartbeatReply then
heartbeatReply = false;
sendToBot(buildPacket("HEARTBEAT", {}));
coroutine.schedule(heartbeat, 1);
else
notifiyConnectionLost();
end;
end;
--make sure the user is the Linkshell holder before sending the add request for the linkshell to the server
function sendLinkshellAddRequest(requestId)
if framework.playerIsLsOwner() then
playerData = framework.getPlayerData()
sendNotice("Sending request to add " .. playerData.linkshell .. " to LinkCloud...");
sendToBot(buildPacket("ADD_LINKSHELL", {
linkId = requestId,
lsName = playerData.linkshell
}));
else
sendError("You must be the Linkshell owner to use this command. Please ensure you only have YOUR linkshell equipped in the #1 slot.");
end;
end;
function onLSMesUpdate (lsIndex, text)
lsData = getLSFromLsmes(text)
if lsData.shellNumber ~= 0 then
linkShells[lsIndex] = lsData
sendToBot(buildPacket("LINKSHELL_UPDATE", {
linkshellname = linkShells[lsIndex].name
}));
sendNotice("Linkshell #" .. lsIndex .. " changed to " .. linkShells[lsIndex].name)
end
end
function dump(o)
if type(o) == 'table' then
local s = '{ '
for k, v in pairs(o) do
if type(k) ~= 'number' then
k = '\"' .. k .. '\"'
end
s = s .. '[' .. k .. '] = ' .. dump(v) .. ', '
end
return s .. '} '
else
return tostring(o)
end
end
function onLSMessageReceived (lsIndex, text)
if linkShells[lsIndex] and linkShells[lsIndex].name ~= nil then
payload = buildMessagePayload(text, linkShells[lsIndex].name);
if not has_value(echos, payload.message) then
table.insert(lsBuffers[lsIndex], buildPacket("LINKSHELL_MESSAGE", payload));
end;
end
end
function onShoutReceived (text)
if framework.settings.ProcessShouts then
table.insert(otherbuffer, buildPacket("SHOUT", buildShoutPayload(text)));
end
end
function cmdToggleAutoConnect ()
framework.settings.AutoConnect = not framework.settings.AutoConnect;
sendNotice("Auto Connect on Startup: " .. tostring(framework.settings.AutoConnect));
framework.saveAddonConfig();
end
function cmdToggleAutoReconnect ()
framework.settings.AutoReconnect = not framework.settings.AutoReconnect;
sendNotice("Auto Reconnect: " .. tostring(framework.settings.AutoReconnect));
framework.saveAddonConfig();
end
function cmdToggleShouts ()
framework.settings.ProcessShouts = not framework.settings.ProcessShouts;
sendNotice("Sending Shouts: " .. tostring(framework.settings.ProcessShouts));
framework.saveAddonConfig();
end
function cmdShowVersion (engineVer)
sendNotice('Engine Version: ' .. engineVer .. ' | LinkCloud Version: ' .. _version)
end
function sendNotice (msg)
if(framework.sendNoticeMessage ~= nil) then framework.sendNoticeMessage(msg) end
if(framework.consoleLog ~= nil) then framework.consoleLog(msg) end
end
function sendError (msg)
if(framework.sendErrorMessage ~= nil) then framework.sendErrorMessage(msg) end
if(framework.consoleLog ~= nil) then framework.consoleLog('ERROR: ' .. msg) end
end
function cmdShowHelpMenu()
sendNotice(" *** LinkCloud v" .. _version .. " - Author: Twisted ***");
sendNotice(" connect | c --> Attempts to connect to the LinkCloud service.");
sendNotice(" autoconnect --> Toggles Automatic connection when game starts. [Default On]");
sendNotice(" autoreconnect --> Toggles Automatic reconnection when connection is lost. [Default On]");
sendNotice(" shouts --> Toggles sending shouts to LinkCloud. [Default On]")
sendNotice(" version | ver | v --> Displays Version");
sendNotice(" status | s --> Shows connection status to the server");
sendNotice(" help | h --> Displays this message");
end
return {
hook = hook,
onLSMesUpdate = onLSMesUpdate,
onLSMessageReceived = onLSMessageReceived,
onShoutReceived = onShoutReceived,
sendLinkshellAddRequest = sendLinkshellAddRequest
}

View File

@ -0,0 +1,12 @@
<?xml version="1.1" ?>
<settings>
<global>
<AuthKey></AuthKey>
<AutoConnect>true</AutoConnect>
<AutoReconnect>true</AutoReconnect>
<HostAddress>linkcloud.drunken.games</HostAddress>
<HostPort>5050</HostPort>
<ProcessShouts>true</ProcessShouts>
<TCPTimeout>2</TCPTimeout>
</global>
</settings>

20
addon/LinkCloud/lcmem.lua Normal file
View File

@ -0,0 +1,20 @@
local struct_lib = require('struct')
local string = struct_lib.string
local tag = struct_lib.tag
local uint32 = struct_lib.uint32
local uint16 = struct_lib.uint16
local int16 = struct_lib.int16
local pc_name = string(0x10)
local entity_id = tag(uint32, 'entity')
local ip = tag(uint32, 'ip')
function getAccountInfo()
return struct({signature = '538B5C240856578BFB83C9FF33C053F2AEA1'}, {
version = {0x248, string(0x10)},
ip = {0x260, ip},
port = {0x26C, uint16},
id = {0x314, entity_id},
name = {0x318, pc_name},
server_id = {0x390, int16},
})
end

View File

@ -0,0 +1,11 @@
local newdecoder = require 'lunajson.decoder'
local newencoder = require 'lunajson.encoder'
local sax = require 'lunajson.sax'
-- If you need multiple contexts of decoder and/or encoder,
-- you can require lunajson.decoder and/or lunajson.encoder directly.
return {
decode = newdecoder(),
encode = newencoder(),
newparser = sax.newparser,
newfileparser = sax.newfileparser,
}

View File

@ -0,0 +1,515 @@
local setmetatable, tonumber, tostring =
setmetatable, tonumber, tostring
local floor, inf =
math.floor, math.huge
local mininteger, tointeger =
math.mininteger or nil, math.tointeger or nil
local byte, char, find, gsub, match, sub =
string.byte, string.char, string.find, string.gsub, string.match, string.sub
local function _decode_error(pos, errmsg)
error("parse error at " .. pos .. ": " .. errmsg, 2)
end
local f_str_ctrl_pat
if _VERSION == "Lua 5.1" then
-- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly
f_str_ctrl_pat = '[^\32-\255]'
else
f_str_ctrl_pat = '[\0-\31]'
end
local _ENV = nil
local function newdecoder()
local json, pos, nullv, arraylen, rec_depth
-- `f` is the temporary for dispatcher[c] and
-- the dummy for the first return value of `find`
local dispatcher, f
--[[
Helper
--]]
local function decode_error(errmsg)
return _decode_error(pos, errmsg)
end
--[[
Invalid
--]]
local function f_err()
decode_error('invalid value')
end
--[[
Constants
--]]
-- null
local function f_nul()
if sub(json, pos, pos+2) == 'ull' then
pos = pos+3
return nullv
end
decode_error('invalid value')
end
-- false
local function f_fls()
if sub(json, pos, pos+3) == 'alse' then
pos = pos+4
return false
end
decode_error('invalid value')
end
-- true
local function f_tru()
if sub(json, pos, pos+2) == 'rue' then
pos = pos+3
return true
end
decode_error('invalid value')
end
--[[
Numbers
Conceptually, the longest prefix that matches to `[-+.0-9A-Za-z]+` (in regexp)
is captured as a number and its conformance to the JSON spec is checked.
--]]
-- deal with non-standard locales
local radixmark = match(tostring(0.5), '[^0-9]')
local fixedtonumber = tonumber
if radixmark ~= '.' then
if find(radixmark, '%W') then
radixmark = '%' .. radixmark
end
fixedtonumber = function(s)
return tonumber(gsub(s, '.', radixmark))
end
end
local function number_error()
return decode_error('invalid number')
end
-- `0(\.[0-9]*)?([eE][+-]?[0-9]*)?`
local function f_zro(mns)
local num, c = match(json, '^(%.?[0-9]*)([-+.A-Za-z]?)', pos) -- skipping 0
if num == '' then
if c == '' then
if mns then
return -0.0
end
return 0
end
if c == 'e' or c == 'E' then
num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
if c == '' then
pos = pos + #num
if mns then
return -0.0
end
return 0.0
end
end
number_error()
end
if byte(num) ~= 0x2E or byte(num, -1) == 0x2E then
number_error()
end
if c ~= '' then
if c == 'e' or c == 'E' then
num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
end
if c ~= '' then
number_error()
end
end
pos = pos + #num
c = fixedtonumber(num)
if mns then
c = -c
end
return c
end
-- `[1-9][0-9]*(\.[0-9]*)?([eE][+-]?[0-9]*)?`
local function f_num(mns)
pos = pos-1
local num, c = match(json, '^([0-9]+%.?[0-9]*)([-+.A-Za-z]?)', pos)
if byte(num, -1) == 0x2E then -- error if ended with period
number_error()
end
if c ~= '' then
if c ~= 'e' and c ~= 'E' then
number_error()
end
num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
if not num or c ~= '' then
number_error()
end
end
pos = pos + #num
c = fixedtonumber(num)
if mns then
c = -c
if c == mininteger and not find(num, '[^0-9]') then
c = mininteger
end
end
return c
end
-- skip minus sign
local function f_mns()
local c = byte(json, pos)
if c then
pos = pos+1
if c > 0x30 then
if c < 0x3A then
return f_num(true)
end
else
if c > 0x2F then
return f_zro(true)
end
end
end
decode_error('invalid number')
end
--[[
Strings
--]]
local f_str_hextbl = {
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7,
0x8, 0x9, inf, inf, inf, inf, inf, inf,
inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, inf,
inf, inf, inf, inf, inf, inf, inf, inf,
inf, inf, inf, inf, inf, inf, inf, inf,
inf, inf, inf, inf, inf, inf, inf, inf,
inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF,
__index = function()
return inf
end
}
setmetatable(f_str_hextbl, f_str_hextbl)
local f_str_escapetbl = {
['"'] = '"',
['\\'] = '\\',
['/'] = '/',
['b'] = '\b',
['f'] = '\f',
['n'] = '\n',
['r'] = '\r',
['t'] = '\t',
__index = function()
decode_error("invalid escape sequence")
end
}
setmetatable(f_str_escapetbl, f_str_escapetbl)
local function surrogate_first_error()
return decode_error("1st surrogate pair byte not continued by 2nd")
end
local f_str_surrogate_prev = 0
local function f_str_subst(ch, ucode)
if ch == 'u' then
local c1, c2, c3, c4, rest = byte(ucode, 1, 5)
ucode = f_str_hextbl[c1-47] * 0x1000 +
f_str_hextbl[c2-47] * 0x100 +
f_str_hextbl[c3-47] * 0x10 +
f_str_hextbl[c4-47]
if ucode ~= inf then
if ucode < 0x80 then -- 1byte
if rest then
return char(ucode, rest)
end
return char(ucode)
elseif ucode < 0x800 then -- 2bytes
c1 = floor(ucode / 0x40)
c2 = ucode - c1 * 0x40
c1 = c1 + 0xC0
c2 = c2 + 0x80
if rest then
return char(c1, c2, rest)
end
return char(c1, c2)
elseif ucode < 0xD800 or 0xE000 <= ucode then -- 3bytes
c1 = floor(ucode / 0x1000)
ucode = ucode - c1 * 0x1000
c2 = floor(ucode / 0x40)
c3 = ucode - c2 * 0x40
c1 = c1 + 0xE0
c2 = c2 + 0x80
c3 = c3 + 0x80
if rest then
return char(c1, c2, c3, rest)
end
return char(c1, c2, c3)
elseif 0xD800 <= ucode and ucode < 0xDC00 then -- surrogate pair 1st
if f_str_surrogate_prev == 0 then
f_str_surrogate_prev = ucode
if not rest then
return ''
end
surrogate_first_error()
end
f_str_surrogate_prev = 0
surrogate_first_error()
else -- surrogate pair 2nd
if f_str_surrogate_prev ~= 0 then
ucode = 0x10000 +
(f_str_surrogate_prev - 0xD800) * 0x400 +
(ucode - 0xDC00)
f_str_surrogate_prev = 0
c1 = floor(ucode / 0x40000)
ucode = ucode - c1 * 0x40000
c2 = floor(ucode / 0x1000)
ucode = ucode - c2 * 0x1000
c3 = floor(ucode / 0x40)
c4 = ucode - c3 * 0x40
c1 = c1 + 0xF0
c2 = c2 + 0x80
c3 = c3 + 0x80
c4 = c4 + 0x80
if rest then
return char(c1, c2, c3, c4, rest)
end
return char(c1, c2, c3, c4)
end
decode_error("2nd surrogate pair byte appeared without 1st")
end
end
decode_error("invalid unicode codepoint literal")
end
if f_str_surrogate_prev ~= 0 then
f_str_surrogate_prev = 0
surrogate_first_error()
end
return f_str_escapetbl[ch] .. ucode
end
-- caching interpreted keys for speed
local f_str_keycache = setmetatable({}, {__mode="v"})
local function f_str(iskey)
local newpos = pos
local tmppos, c1, c2
repeat
newpos = find(json, '"', newpos, true) -- search '"'
if not newpos then
decode_error("unterminated string")
end
tmppos = newpos-1
newpos = newpos+1
c1, c2 = byte(json, tmppos-1, tmppos)
if c2 == 0x5C and c1 == 0x5C then -- skip preceding '\\'s
repeat
tmppos = tmppos-2
c1, c2 = byte(json, tmppos-1, tmppos)
until c2 ~= 0x5C or c1 ~= 0x5C
tmppos = newpos-2
end
until c2 ~= 0x5C -- leave if '"' is not preceded by '\'
local str = sub(json, pos, tmppos)
pos = newpos
if iskey then -- check key cache
tmppos = f_str_keycache[str] -- reuse tmppos for cache key/val
if tmppos then
return tmppos
end
tmppos = str
end
if find(str, f_str_ctrl_pat) then
decode_error("unescaped control string")
end
if find(str, '\\', 1, true) then -- check whether a backslash exists
-- We need to grab 4 characters after the escape char,
-- for encoding unicode codepoint to UTF-8.
-- As we need to ensure that every first surrogate pair byte is
-- immediately followed by second one, we grab upto 5 characters and
-- check the last for this purpose.
str = gsub(str, '\\(.)([^\\]?[^\\]?[^\\]?[^\\]?[^\\]?)', f_str_subst)
if f_str_surrogate_prev ~= 0 then
f_str_surrogate_prev = 0
decode_error("1st surrogate pair byte not continued by 2nd")
end
end
if iskey then -- commit key cache
f_str_keycache[tmppos] = str
end
return str
end
--[[
Arrays, Objects
--]]
-- array
local function f_ary()
rec_depth = rec_depth + 1
if rec_depth > 1000 then
decode_error('too deeply nested json (> 1000)')
end
local ary = {}
pos = match(json, '^[ \n\r\t]*()', pos)
local i = 0
if byte(json, pos) == 0x5D then -- check closing bracket ']' which means the array empty
pos = pos+1
else
local newpos = pos
repeat
i = i+1
f = dispatcher[byte(json,newpos)] -- parse value
pos = newpos+1
ary[i] = f()
newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos) -- check comma
until not newpos
newpos = match(json, '^[ \n\r\t]*%]()', pos) -- check closing bracket
if not newpos then
decode_error("no closing bracket of an array")
end
pos = newpos
end
if arraylen then -- commit the length of the array if `arraylen` is set
ary[0] = i
end
rec_depth = rec_depth - 1
return ary
end
-- objects
local function f_obj()
rec_depth = rec_depth + 1
if rec_depth > 1000 then
decode_error('too deeply nested json (> 1000)')
end
local obj = {}
pos = match(json, '^[ \n\r\t]*()', pos)
if byte(json, pos) == 0x7D then -- check closing bracket '}' which means the object empty
pos = pos+1
else
local newpos = pos
repeat
if byte(json, newpos) ~= 0x22 then -- check '"'
decode_error("not key")
end
pos = newpos+1
local key = f_str(true) -- parse key
-- optimized for compact json
-- c1, c2 == ':', <the first char of the value> or
-- c1, c2, c3 == ':', ' ', <the first char of the value>
f = f_err
local c1, c2, c3 = byte(json, pos, pos+3)
if c1 == 0x3A then
if c2 ~= 0x20 then
f = dispatcher[c2]
newpos = pos+2
else
f = dispatcher[c3]
newpos = pos+3
end
end
if f == f_err then -- read a colon and arbitrary number of spaces
newpos = match(json, '^[ \n\r\t]*:[ \n\r\t]*()', pos)
if not newpos then
decode_error("no colon after a key")
end
f = dispatcher[byte(json, newpos)]
newpos = newpos+1
end
pos = newpos
obj[key] = f() -- parse value
newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos)
until not newpos
newpos = match(json, '^[ \n\r\t]*}()', pos)
if not newpos then
decode_error("no closing bracket of an object")
end
pos = newpos
end
rec_depth = rec_depth - 1
return obj
end
--[[
The jump table to dispatch a parser for a value,
indexed by the code of the value's first char.
Nil key means the end of json.
--]]
dispatcher = { [0] =
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_str, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_mns, f_err, f_err,
f_zro, f_num, f_num, f_num, f_num, f_num, f_num, f_num,
f_num, f_num, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_ary, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_fls, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_nul, f_err,
f_err, f_err, f_err, f_err, f_tru, f_err, f_err, f_err,
f_err, f_err, f_err, f_obj, f_err, f_err, f_err, f_err,
__index = function()
decode_error("unexpected termination")
end
}
setmetatable(dispatcher, dispatcher)
--[[
run decoder
--]]
local function decode(json_, pos_, nullv_, arraylen_)
json, pos, nullv, arraylen = json_, pos_, nullv_, arraylen_
rec_depth = 0
pos = match(json, '^[ \n\r\t]*()', pos)
f = dispatcher[byte(json, pos)]
pos = pos+1
local v = f()
if pos_ then
return v, pos
else
f, pos = find(json, '^[ \n\r\t]*', pos)
if pos ~= #json then
decode_error('json ended')
end
return v
end
end
return decode
end
return newdecoder

View File

@ -0,0 +1,185 @@
local error = error
local byte, find, format, gsub, match = string.byte, string.find, string.format, string.gsub, string.match
local concat = table.concat
local tostring = tostring
local pairs, type = pairs, type
local setmetatable = setmetatable
local huge, tiny = 1/0, -1/0
local f_string_esc_pat
if _VERSION == "Lua 5.1" then
-- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly
f_string_esc_pat = '[^ -!#-[%]^-\255]'
else
f_string_esc_pat = '[\0-\31"\\]'
end
local _ENV = nil
local function newencoder()
local v, nullv
local i, builder, visited
local function f_tostring(v)
builder[i] = tostring(v)
i = i+1
end
local radixmark = match(tostring(0.5), '[^0-9]')
local delimmark = match(tostring(12345.12345), '[^0-9' .. radixmark .. ']')
if radixmark == '.' then
radixmark = nil
end
local radixordelim
if radixmark or delimmark then
radixordelim = true
if radixmark and find(radixmark, '%W') then
radixmark = '%' .. radixmark
end
if delimmark and find(delimmark, '%W') then
delimmark = '%' .. delimmark
end
end
local f_number = function(n)
if tiny < n and n < huge then
local s = format("%.17g", n)
if radixordelim then
if delimmark then
s = gsub(s, delimmark, '')
end
if radixmark then
s = gsub(s, radixmark, '.')
end
end
builder[i] = s
i = i+1
return
end
error('invalid number')
end
local doencode
local f_string_subst = {
['"'] = '\\"',
['\\'] = '\\\\',
['\b'] = '\\b',
['\f'] = '\\f',
['\n'] = '\\n',
['\r'] = '\\r',
['\t'] = '\\t',
__index = function(_, c)
return format('\\u00%02X', byte(c))
end
}
setmetatable(f_string_subst, f_string_subst)
local function f_string(s)
builder[i] = '"'
if find(s, f_string_esc_pat) then
s = gsub(s, f_string_esc_pat, f_string_subst)
end
builder[i+1] = s
builder[i+2] = '"'
i = i+3
end
local function f_table(o)
if visited[o] then
error("loop detected")
end
visited[o] = true
local tmp = o[0]
if type(tmp) == 'number' then -- arraylen available
builder[i] = '['
i = i+1
for j = 1, tmp do
doencode(o[j])
builder[i] = ','
i = i+1
end
if tmp > 0 then
i = i-1
end
builder[i] = ']'
else
tmp = o[1]
if tmp ~= nil then -- detected as array
builder[i] = '['
i = i+1
local j = 2
repeat
doencode(tmp)
tmp = o[j]
if tmp == nil then
break
end
j = j+1
builder[i] = ','
i = i+1
until false
builder[i] = ']'
else -- detected as object
builder[i] = '{'
i = i+1
local tmp = i
for k, v in pairs(o) do
if type(k) ~= 'string' then
error("non-string key")
end
f_string(k)
builder[i] = ':'
i = i+1
doencode(v)
builder[i] = ','
i = i+1
end
if i > tmp then
i = i-1
end
builder[i] = '}'
end
end
i = i+1
visited[o] = nil
end
local dispatcher = {
boolean = f_tostring,
number = f_number,
string = f_string,
table = f_table,
__index = function()
error("invalid type value")
end
}
setmetatable(dispatcher, dispatcher)
function doencode(v)
if v == nullv then
builder[i] = 'null'
i = i+1
return
end
return dispatcher[type(v)](v)
end
local function encode(v_, nullv_)
v, nullv = v_, nullv_
i, builder, visited = 1, {}, {}
doencode(v)
return concat(builder)
end
return encode
end
return newencoder

View File

@ -0,0 +1,719 @@
local setmetatable, tonumber, tostring =
setmetatable, tonumber, tostring
local floor, inf =
math.floor, math.huge
local mininteger, tointeger =
math.mininteger or nil, math.tointeger or nil
local byte, char, find, gsub, match, sub =
string.byte, string.char, string.find, string.gsub, string.match, string.sub
local function _parse_error(pos, errmsg)
error("parse error at " .. pos .. ": " .. errmsg, 2)
end
local f_str_ctrl_pat
if _VERSION == "Lua 5.1" then
-- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly
f_str_ctrl_pat = '[^\32-\255]'
else
f_str_ctrl_pat = '[\0-\31]'
end
local type, unpack = type, table.unpack or unpack
local open = io.open
local _ENV = nil
local function nop() end
local function newparser(src, saxtbl)
local json, jsonnxt, rec_depth
local jsonlen, pos, acc = 0, 1, 0
-- `f` is the temporary for dispatcher[c] and
-- the dummy for the first return value of `find`
local dispatcher, f
-- initialize
if type(src) == 'string' then
json = src
jsonlen = #json
jsonnxt = function()
json = ''
jsonlen = 0
jsonnxt = nop
end
else
jsonnxt = function()
acc = acc + jsonlen
pos = 1
repeat
json = src()
if not json then
json = ''
jsonlen = 0
jsonnxt = nop
return
end
jsonlen = #json
until jsonlen > 0
end
jsonnxt()
end
local sax_startobject = saxtbl.startobject or nop
local sax_key = saxtbl.key or nop
local sax_endobject = saxtbl.endobject or nop
local sax_startarray = saxtbl.startarray or nop
local sax_endarray = saxtbl.endarray or nop
local sax_string = saxtbl.string or nop
local sax_number = saxtbl.number or nop
local sax_boolean = saxtbl.boolean or nop
local sax_null = saxtbl.null or nop
--[[
Helper
--]]
local function tryc()
local c = byte(json, pos)
if not c then
jsonnxt()
c = byte(json, pos)
end
return c
end
local function parse_error(errmsg)
return _parse_error(acc + pos, errmsg)
end
local function tellc()
return tryc() or parse_error("unexpected termination")
end
local function spaces() -- skip spaces and prepare the next char
while true do
pos = match(json, '^[ \n\r\t]*()', pos)
if pos <= jsonlen then
return
end
if jsonlen == 0 then
parse_error("unexpected termination")
end
jsonnxt()
end
end
--[[
Invalid
--]]
local function f_err()
parse_error('invalid value')
end
--[[
Constants
--]]
-- fallback slow constants parser
local function generic_constant(target, targetlen, ret, sax_f)
for i = 1, targetlen do
local c = tellc()
if byte(target, i) ~= c then
parse_error("invalid char")
end
pos = pos+1
end
return sax_f(ret)
end
-- null
local function f_nul()
if sub(json, pos, pos+2) == 'ull' then
pos = pos+3
return sax_null(nil)
end
return generic_constant('ull', 3, nil, sax_null)
end
-- false
local function f_fls()
if sub(json, pos, pos+3) == 'alse' then
pos = pos+4
return sax_boolean(false)
end
return generic_constant('alse', 4, false, sax_boolean)
end
-- true
local function f_tru()
if sub(json, pos, pos+2) == 'rue' then
pos = pos+3
return sax_boolean(true)
end
return generic_constant('rue', 3, true, sax_boolean)
end
--[[
Numbers
Conceptually, the longest prefix that matches to `[-+.0-9A-Za-z]+` (in regexp)
is captured as a number and its conformance to the JSON spec is checked.
--]]
-- deal with non-standard locales
local radixmark = match(tostring(0.5), '[^0-9]')
local fixedtonumber = tonumber
if radixmark ~= '.' then
if find(radixmark, '%W') then
radixmark = '%' .. radixmark
end
fixedtonumber = function(s)
return tonumber(gsub(s, '.', radixmark))
end
end
local function number_error()
return parse_error('invalid number')
end
-- fallback slow parser
local function generic_number(mns)
local buf = {}
local i = 1
local is_int = true
local c = byte(json, pos)
pos = pos+1
local function nxt()
buf[i] = c
i = i+1
c = tryc()
pos = pos+1
end
if c == 0x30 then
nxt()
if c and 0x30 <= c and c < 0x3A then
number_error()
end
else
repeat nxt() until not (c and 0x30 <= c and c < 0x3A)
end
if c == 0x2E then
is_int = false
nxt()
if not (c and 0x30 <= c and c < 0x3A) then
number_error()
end
repeat nxt() until not (c and 0x30 <= c and c < 0x3A)
end
if c == 0x45 or c == 0x65 then
is_int = false
nxt()
if c == 0x2B or c == 0x2D then
nxt()
end
if not (c and 0x30 <= c and c < 0x3A) then
number_error()
end
repeat nxt() until not (c and 0x30 <= c and c < 0x3A)
end
if c and (0x41 <= c and c <= 0x5B or
0x61 <= c and c <= 0x7B or
c == 0x2B or c == 0x2D or c == 0x2E) then
number_error()
end
pos = pos-1
local num = char(unpack(buf))
num = fixedtonumber(num)
if mns then
num = -num
if num == mininteger and is_int then
num = mininteger
end
end
return sax_number(num)
end
-- `0(\.[0-9]*)?([eE][+-]?[0-9]*)?`
local function f_zro(mns)
local num, c = match(json, '^(%.?[0-9]*)([-+.A-Za-z]?)', pos) -- skipping 0
if num == '' then
if pos > jsonlen then
pos = pos - 1
return generic_number(mns)
end
if c == '' then
if mns then
return sax_number(-0.0)
end
return sax_number(0)
end
if c == 'e' or c == 'E' then
num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
if c == '' then
pos = pos + #num
if pos > jsonlen then
pos = pos - #num - 1
return generic_number(mns)
end
if mns then
return sax_number(-0.0)
end
return sax_number(0.0)
end
end
pos = pos-1
return generic_number(mns)
end
if byte(num) ~= 0x2E or byte(num, -1) == 0x2E then
pos = pos-1
return generic_number(mns)
end
if c ~= '' then
if c == 'e' or c == 'E' then
num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
end
if c ~= '' then
pos = pos-1
return generic_number(mns)
end
end
pos = pos + #num
if pos > jsonlen then
pos = pos - #num - 1
return generic_number(mns)
end
c = fixedtonumber(num)
if mns then
c = -c
end
return sax_number(c)
end
-- `[1-9][0-9]*(\.[0-9]*)?([eE][+-]?[0-9]*)?`
local function f_num(mns)
pos = pos-1
local num, c = match(json, '^([0-9]+%.?[0-9]*)([-+.A-Za-z]?)', pos)
if byte(num, -1) == 0x2E then -- error if ended with period
return generic_number(mns)
end
if c ~= '' then
if c ~= 'e' and c ~= 'E' then
return generic_number(mns)
end
num, c = match(json, '^([^eE]*[eE][-+]?[0-9]+)([-+.A-Za-z]?)', pos)
if not num or c ~= '' then
return generic_number(mns)
end
end
pos = pos + #num
if pos > jsonlen then
pos = pos - #num
return generic_number(mns)
end
c = fixedtonumber(num)
if mns then
c = -c
if c == mininteger and not find(num, '[^0-9]') then
c = mininteger
end
end
return sax_number(c)
end
-- skip minus sign
local function f_mns()
local c = byte(json, pos) or tellc()
if c then
pos = pos+1
if c > 0x30 then
if c < 0x3A then
return f_num(true)
end
else
if c > 0x2F then
return f_zro(true)
end
end
end
parse_error("invalid number")
end
--[[
Strings
--]]
local f_str_hextbl = {
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7,
0x8, 0x9, inf, inf, inf, inf, inf, inf,
inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, inf,
inf, inf, inf, inf, inf, inf, inf, inf,
inf, inf, inf, inf, inf, inf, inf, inf,
inf, inf, inf, inf, inf, inf, inf, inf,
inf, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF,
__index = function()
return inf
end
}
setmetatable(f_str_hextbl, f_str_hextbl)
local f_str_escapetbl = {
['"'] = '"',
['\\'] = '\\',
['/'] = '/',
['b'] = '\b',
['f'] = '\f',
['n'] = '\n',
['r'] = '\r',
['t'] = '\t',
__index = function()
parse_error("invalid escape sequence")
end
}
setmetatable(f_str_escapetbl, f_str_escapetbl)
local function surrogate_first_error()
return parse_error("1st surrogate pair byte not continued by 2nd")
end
local f_str_surrogate_prev = 0
local function f_str_subst(ch, ucode)
if ch == 'u' then
local c1, c2, c3, c4, rest = byte(ucode, 1, 5)
ucode = f_str_hextbl[c1-47] * 0x1000 +
f_str_hextbl[c2-47] * 0x100 +
f_str_hextbl[c3-47] * 0x10 +
f_str_hextbl[c4-47]
if ucode ~= inf then
if ucode < 0x80 then -- 1byte
if rest then
return char(ucode, rest)
end
return char(ucode)
elseif ucode < 0x800 then -- 2bytes
c1 = floor(ucode / 0x40)
c2 = ucode - c1 * 0x40
c1 = c1 + 0xC0
c2 = c2 + 0x80
if rest then
return char(c1, c2, rest)
end
return char(c1, c2)
elseif ucode < 0xD800 or 0xE000 <= ucode then -- 3bytes
c1 = floor(ucode / 0x1000)
ucode = ucode - c1 * 0x1000
c2 = floor(ucode / 0x40)
c3 = ucode - c2 * 0x40
c1 = c1 + 0xE0
c2 = c2 + 0x80
c3 = c3 + 0x80
if rest then
return char(c1, c2, c3, rest)
end
return char(c1, c2, c3)
elseif 0xD800 <= ucode and ucode < 0xDC00 then -- surrogate pair 1st
if f_str_surrogate_prev == 0 then
f_str_surrogate_prev = ucode
if not rest then
return ''
end
surrogate_first_error()
end
f_str_surrogate_prev = 0
surrogate_first_error()
else -- surrogate pair 2nd
if f_str_surrogate_prev ~= 0 then
ucode = 0x10000 +
(f_str_surrogate_prev - 0xD800) * 0x400 +
(ucode - 0xDC00)
f_str_surrogate_prev = 0
c1 = floor(ucode / 0x40000)
ucode = ucode - c1 * 0x40000
c2 = floor(ucode / 0x1000)
ucode = ucode - c2 * 0x1000
c3 = floor(ucode / 0x40)
c4 = ucode - c3 * 0x40
c1 = c1 + 0xF0
c2 = c2 + 0x80
c3 = c3 + 0x80
c4 = c4 + 0x80
if rest then
return char(c1, c2, c3, c4, rest)
end
return char(c1, c2, c3, c4)
end
parse_error("2nd surrogate pair byte appeared without 1st")
end
end
parse_error("invalid unicode codepoint literal")
end
if f_str_surrogate_prev ~= 0 then
f_str_surrogate_prev = 0
surrogate_first_error()
end
return f_str_escapetbl[ch] .. ucode
end
local function f_str(iskey)
local pos2 = pos
local newpos
local str = ''
local bs
while true do
while true do -- search '\' or '"'
newpos = find(json, '[\\"]', pos2)
if newpos then
break
end
str = str .. sub(json, pos, jsonlen)
if pos2 == jsonlen+2 then
pos2 = 2
else
pos2 = 1
end
jsonnxt()
if jsonlen == 0 then
parse_error("unterminated string")
end
end
if byte(json, newpos) == 0x22 then -- break if '"'
break
end
pos2 = newpos+2 -- skip '\<char>'
bs = true -- mark the existence of a backslash
end
str = str .. sub(json, pos, newpos-1)
pos = newpos+1
if find(str, f_str_ctrl_pat) then
parse_error("unescaped control string")
end
if bs then -- a backslash exists
-- We need to grab 4 characters after the escape char,
-- for encoding unicode codepoint to UTF-8.
-- As we need to ensure that every first surrogate pair byte is
-- immediately followed by second one, we grab upto 5 characters and
-- check the last for this purpose.
str = gsub(str, '\\(.)([^\\]?[^\\]?[^\\]?[^\\]?[^\\]?)', f_str_subst)
if f_str_surrogate_prev ~= 0 then
f_str_surrogate_prev = 0
parse_error("1st surrogate pair byte not continued by 2nd")
end
end
if iskey then
return sax_key(str)
end
return sax_string(str)
end
--[[
Arrays, Objects
--]]
-- arrays
local function f_ary()
rec_depth = rec_depth + 1
if rec_depth > 1000 then
parse_error('too deeply nested json (> 1000)')
end
sax_startarray()
spaces()
if byte(json, pos) == 0x5D then -- check closing bracket ']' which means the array empty
pos = pos+1
else
local newpos
while true do
f = dispatcher[byte(json, pos)] -- parse value
pos = pos+1
f()
newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos) -- check comma
if newpos then
pos = newpos
else
newpos = match(json, '^[ \n\r\t]*%]()', pos) -- check closing bracket
if newpos then
pos = newpos
break
end
spaces() -- since the current chunk can be ended, skip spaces toward following chunks
local c = byte(json, pos)
pos = pos+1
if c == 0x2C then -- check comma again
spaces()
elseif c == 0x5D then -- check closing bracket again
break
else
parse_error("no closing bracket of an array")
end
end
if pos > jsonlen then
spaces()
end
end
end
rec_depth = rec_depth - 1
return sax_endarray()
end
-- objects
local function f_obj()
rec_depth = rec_depth + 1
if rec_depth > 1000 then
parse_error('too deeply nested json (> 1000)')
end
sax_startobject()
spaces()
if byte(json, pos) == 0x7D then -- check closing bracket '}' which means the object empty
pos = pos+1
else
local newpos
while true do
if byte(json, pos) ~= 0x22 then
parse_error("not key")
end
pos = pos+1
f_str(true) -- parse key
newpos = match(json, '^[ \n\r\t]*:[ \n\r\t]*()', pos) -- check colon
if newpos then
pos = newpos
else
spaces() -- read spaces through chunks
if byte(json, pos) ~= 0x3A then -- check colon again
parse_error("no colon after a key")
end
pos = pos+1
spaces()
end
if pos > jsonlen then
spaces()
end
f = dispatcher[byte(json, pos)]
pos = pos+1
f() -- parse value
newpos = match(json, '^[ \n\r\t]*,[ \n\r\t]*()', pos) -- check comma
if newpos then
pos = newpos
else
newpos = match(json, '^[ \n\r\t]*}()', pos) -- check closing bracket
if newpos then
pos = newpos
break
end
spaces() -- read spaces through chunks
local c = byte(json, pos)
pos = pos+1
if c == 0x2C then -- check comma again
spaces()
elseif c == 0x7D then -- check closing bracket again
break
else
parse_error("no closing bracket of an object")
end
end
if pos > jsonlen then
spaces()
end
end
end
rec_depth = rec_depth - 1
return sax_endobject()
end
--[[
The jump table to dispatch a parser for a value,
indexed by the code of the value's first char.
Key should be non-nil.
--]]
dispatcher = { [0] =
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_str, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_mns, f_err, f_err,
f_zro, f_num, f_num, f_num, f_num, f_num, f_num, f_num,
f_num, f_num, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_ary, f_err, f_err, f_err, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_fls, f_err,
f_err, f_err, f_err, f_err, f_err, f_err, f_nul, f_err,
f_err, f_err, f_err, f_err, f_tru, f_err, f_err, f_err,
f_err, f_err, f_err, f_obj, f_err, f_err, f_err, f_err,
}
--[[
public funcitons
--]]
local function run()
rec_depth = 0
spaces()
f = dispatcher[byte(json, pos)]
pos = pos+1
f()
end
local function read(n)
if n < 0 then
error("the argument must be non-negative")
end
local pos2 = (pos-1) + n
local str = sub(json, pos, pos2)
while pos2 > jsonlen and jsonlen ~= 0 do
jsonnxt()
pos2 = pos2 - (jsonlen - (pos-1))
str = str .. sub(json, pos, pos2)
end
if jsonlen ~= 0 then
pos = pos2+1
end
return str
end
local function tellpos()
return acc + pos
end
return {
run = run,
tryc = tryc,
read = read,
tellpos = tellpos,
}
end
local function newfileparser(fn, saxtbl)
local fp = open(fn)
local function gen()
local s
if fp then
s = fp:read(8192)
if not s then
fp:close()
fp = nil
end
end
return s
end
return newparser(gen, saxtbl)
end
return {
newparser = newparser,
newfileparser = newfileparser
}

1237
addon/LinkCloud/struct.lua Normal file

File diff suppressed because it is too large Load Diff

4
server/.env Normal file
View File

@ -0,0 +1,4 @@
TCP_LISTEN_PORT=5050
BIND_ADDRESS=0.0.0.0
MAX_CLOCK_SYNC_MISMATCH_SECONDS=2
JWT_SECRET=

82
server/Utility/db.js Normal file
View File

@ -0,0 +1,82 @@
import * as mysql from "mysql2";
import { event } from "./eventHandler.js";
import { Debug, Log, Warn, Err, logger, profilerDone } from "./loggerUtility.js";
let pool;
export let queryCount = 0;
export const setup = ({ user, host, database, password, connectionLimit }) => {
/**
* Creates a MySQL connection pool with the provided configuration options.
* @param {Object} config - The configuration object for creating the pool.
* @param {string} config.host - The host of the MySQL server.
* @param {string} config.user - The user to authenticate as.
* @param {string} config.database - The name of the database to use for the connection.
* @param {string} config.password - The password of the user.
* @param {boolean} config.waitForConnections - Determines if the pool should wait for connections.
* @param {number} config.connectionLimit - The maximum number of connections to create at once.
* @param {number} config.maxIdle - The maximum number of idle connections
*/
pool = mysql.createPool({
host,
user,
database,
password,
waitForConnections: true,
connectionLimit,
maxIdle: 10, // max idle connections, the default value is the same as `connectionLimit`
idleTimeout: 60000, // idle connections timeout, in milliseconds, the default value 60000
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
});
try{
runPrepQuery("SELECT 1 FROM linkshells", [], (r,f,e) => {
if(!e) {
event.emit("DATABASE_CONNECTED");
} else {
Err('MYSQL_FAILED_NO_TABLES')
event.emit("MYSQL_FAILED_NO_TABLES");
event.emit("MYSQL_FAILED_TO_CONNECT");
}
})
} catch (e) {
console.log(e)
event.emit("MYSQL_FAILED_TO_CONNECT");
}
};
export const runPrepQuery = (sql, data, callback) => {
/**
* Establishes a connection to the MySQL database pool and executes a SQL query.
* @param {Error} err - The error object returned from the connection attempt.
* @param {Connection} conn - The connection object to the MySQL database pool.
* @returns None
*/
pool.getConnection((err, conn) => {
if (err) {
Err(`MYSQL ERROR (${err?.errno}): ${err?.code} ${err?.sqlMessage}`);
if (queryCount === 0) {
event.emit("MYSQL_FAILED_TO_CONNECT");
}
}
if (!err) {
conn.execute(sql, data, (e, r, f) => {
if (typeof callback === "function") callback(r, f, e);
queryCount++;
pool.releaseConnection(conn);
});
} else if (typeof callback === "function") callback(false, false, err);
});
};
export const numRows = (rows) => {
/**
* Returns the number of rows if rows exist, otherwise returns false.
* @param {Array} rows - The array of rows to check.
* @returns {number|boolean} The number of rows if rows exist, otherwise false.
*/
if (rows) return rows.length;
return false;
};

View File

@ -0,0 +1,86 @@
import { Client, Collection, Events, GatewayIntentBits, Partials } from 'discord.js'
import infoCommand from '../commands/utility/info.js'
import connectLinkshell from '../commands/utility/connectLinkshell.js'
import joinlinkcloud from '../commands/utility/joinlinkcloud.js'
import { getLSFromGuildId, getLSModel } from "../Utility/lsModel.js"
import linkcloudstatus from '../commands/utility/linkcloudstatus.js'
import { event } from "./eventHandler.js";
import { Err, Log, Debug, Warn } from '../Utility/loggerUtility.js'
import createEcho from '../commands/utility/createEcho.js'
import fsConfig from '../config.json' assert { type: "json" };
const token = fsConfig.token
const servers = {
retail: fsConfig.retail_servers,
private: fsConfig.private_servers
}
export const client = new Client({ intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
],
partials: [
Partials.Channel,
Partials.Message
]
});
const outboundQueue = []
export const botSetup = () => {
client.commands = new Collection();
client.commands.set(infoCommand.data.name, infoCommand);
client.commands.set(connectLinkshell.data.name, connectLinkshell);
client.commands.set(joinlinkcloud.data.name, joinlinkcloud)
client.commands.set(linkcloudstatus.data.name, linkcloudstatus)
client.commands.set(createEcho.data.name, createEcho)
client.once(Events.ClientReady, readyClient => {
Log(`Discord Bot Ready! Logged in as ${readyClient.user.tag}`);
});
client.login(token);
}
client.on(Events.MessageCreate, async message => {
if(!message.guildId) return;
const linkshell = getLSFromGuildId(message.guildId)
const lsChannels = linkshell.channels
if (linkshell) {
for(const channel of lsChannels) {
if(channel.channelId == message.channelId && !message?.author?.bot) {
const guild = client.guilds.cache.get(message.guildId)
let nickname = false
guild.members.cache.forEach((member) => {
if (member.user.id == message.author.id) {
nickname = member.nickname
}
})
const authorName = nickname ? nickname : message?.author?.globalName
//TODO Fix emojis
const messagePacket = {platform: linkshell.ffxiver, server: linkshell.server, from: authorName, message: message.content, lsName: linkshell.name}
event.emit('NEW_DISCORD_ECHO_RECEIVED', messagePacket)
}
}
} else {
Warn('Unable to find linkshell counterpart for this guild.')
}
});
client.on(Events.InteractionCreate, async interaction => {
if (interaction.isModalSubmit()) {
event.emit('on_' + interaction.customId, interaction)
}
if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
Err(`No command matching ${interaction.commandName} was found.`);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
} else {
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
}
});

View File

@ -0,0 +1,6 @@
import { EventEmitter } from "events";
/**
* EventEmitter instance that can be used to emit and listen for events.
*/
export const event = new EventEmitter();

View File

@ -0,0 +1,146 @@
import winston, { addColors } from "winston";
import path from "path";
/**
* Destructures specific formats from the winston library for logging purposes.
* @returns An object containing the destructured formats for logging.
*/
const { combine, timestamp, json, errors, colorize } = winston.format;
const consoleFormat = winston.format.printf((info) => {
/**
* Formats log information into a string with timestamp, log level, metadata, message, and duration.
* @param {object} info - The log information object containing timestamp, level, meta, message, and durationMs.
* @returns {string} A formatted log message string.
*/
return `[${info.timestamp}][${info.level.toUpperCase()}](${info.meta}) ${info.message}${
info.durationMs ? ` Duration: ${info.durationMs}ms` : ""
}`;
});
const consoleTransport = new winston.transports.Console({
/**
* Configures the format of the logs by combining timestamp, consoleFormat, and colorize options.
* @param {Object} timestamp - The timestamp configuration object.
* @param {Object} consoleFormat - The console format configuration object.
* @param {Object} colorize - The colorize configuration object.
* @returns None
*/
format: combine(timestamp({ format: "YY-MM-DD HH:mm:ss" }), consoleFormat, colorize({ all: true })),
handleExceptions: true,
});
/**
* Configures a Winston logger with specified settings and transports for logging.
* @param {string} logger - The name of the logger.
* @param {object} options - The configuration options for the logger.
* @param {string} options.level - The logging level for the logger.
* @param {Array} options.transports - An array of transports for logging.
* @param {Array} options.exceptionHandlers - An array of exception handlers for logging.
* @param {Array} options.rejectionHandlers - An array of rejection handlers for logging.
* @param {boolean} options.exitOnError - Determines if the process should exit on error.
* @returns None
*/
winston.loggers.add("logger", {
level: "debug",
transports: [
consoleTransport,
new winston.transports.File({
filename: "./logs/runtimeLog.json",
format: combine(timestamp(), errors({ stack: true }), json()),
tailable: true,
maxsize: 1000000, //1mb
handleExceptions: true,
}),
],
exceptionHandlers: [
//consoleTransport,
new winston.transports.File({ filename: "./logs/exception.log" }),
],
rejectionHandlers: [
//consoleTransport,
new winston.transports.File({ filename: "./logs/rejections.log" }),
],
exitOnError: false,
});
const _getCallerFile = () => {
/**
* Generates a formatted string representing the file path and line number of the caller.
* @returns {string} A string in the format "directory/filename:lineNumber"
*/
const err = new Error();
Error.prepareStackTrace = (_, stack) => stack;
const stack = err.stack;
Error.prepareStackTrace = undefined;
const dir = path.dirname(stack[2].getFileName()).split("/").pop();
const filename = path.basename(stack[2].getFileName()).replace(".js", "");
return `${dir}/${filename}:${stack[2].getLineNumber()}`;
};
/**
* Retrieves the logger instance from Winston loggers.
* @returns The logger instance for logging purposes.
*/
export const logger = winston.loggers.get("logger");
/**
* Add color styles to different log levels for console output.
* @param {Object} colors - An object containing color styles for different log levels.
* Colors should be specified in the format "style color" (e.g. "bold red").
* Available styles: bold, italic
* Available colors: black, red, green, yellow, blue, magenta, cyan, white
* @returns None
*/
addColors({
info: "bold green",
warn: "bold italic yellow",
error: "bold red",
debug: "magenta",
});
/**
* Set the exitOnError property of the logger to false.
* This property determines whether the logger should exit the process when an error occurs.
*/
logger.exitOnError = false;
/**
* Logs a debug message along with metadata about the caller file.
* @param {string} message - The message to be logged.
* @returns None
*/
export const Debug = (message) => {
logger.debug({ message, meta: _getCallerFile() });
};
/**
* Logs a message using the logger with the caller file information.
* @param {string} message - The message to be logged.
* @returns None
*/
export const Log = (message) => {
logger.info({ message, meta: _getCallerFile() });
};
/**
* Logs a warning message along with metadata about the caller file.
* @param {string} message - The warning message to be logged.
* @returns None
*/
export const Warn = (message) => {
logger.warn({ message, meta: _getCallerFile() });
};
/**
* Logs an error message along with metadata about the caller file.
* @param {string} message - The error message to be logged.
* @returns None
*/
export const Err = (message) => {
logger.error({ message, meta: _getCallerFile() });
};
/**
* Marks the end of profiling and adds metadata about the caller file to the profiler message.
* @param {Profiler} profiler - The profiler object used for profiling.
* @param {object} message - The message object containing profiling data.
* @returns None
*/
export const profilerDone = (profiler, message) => {
profiler.done({ ...message, meta: _getCallerFile() });
};

226
server/Utility/lsModel.js Normal file
View File

@ -0,0 +1,226 @@
import { runPrepQuery, numRows } from "../Utility/db.js";
import { Warn, Log, Err, Debug } from "./loggerUtility.js";
import { Webhook } from 'discord-webhook-node';
import crypto from "crypto";
const linkshells = {};
const reloadModel = (lsName, serverId, ffxiver) => {
runPrepQuery(
"SELECT * FROM linkshells WHERE server = ? AND `name` = ? AND `ffxiver` = ? LIMIT 0,1",
[serverId, lsName, ffxiver],
async (r, f, e) => {
if (!e) {
if (numRows(r)) {
updateMemory(r[0])
} else {
Warn(`Unable to locate linkshell model ${lsName}, ${serverId}, ${ffxiver}`);
}
} else {
console.log(e);
}
}
);
};
export const getLsFromChannelId = (channelId) => {
for(const ls in linkshells) {
for(const channel in linkshells[ls]?.channels) {
if(linkshells[ls]?.channels[channel]?.channelId == channelId) {
return linkshells[ls]
}
}
}
return false
}
const reloadModelById = (id) => {
runPrepQuery("SELECT * FROM linkshells WHERE id = ? LIMIT 0,1", [id], async (r, f, e) => {
if (!e) {
if (numRows(r)) {
updateMemory(r[0])
} else {
Warn(`Unable to locate linkshell model ${id},`);
}
} else {
console.log(e);
}
});
};
const getLSChannels = (linkshellId) => {
return new Promise((resolve, reject) => {
const out = [];
runPrepQuery("SELECT * FROM linkshellchannels WHERE linkshellId = ?", [linkshellId], (r, f, e) => {
if (!e) {
if (numRows(r)) {
for (const row of r) {
row.Webhook = new Webhook(row.webhookUrl)
row.hookHistory = []
out.push(row);
}
}
resolve(out);
} else {
console.log(e);
reject(e);
}
});
});
};
export const createNewLS = async (lsName, serverId, ffxiver, ownerId) => {
return new Promise(async (resolve, reject) => {
if (!(await lsExists(lsName, serverId, ffxiver))) {
runPrepQuery(
"INSERT INTO linkshells (`name`, `server`, `discord_owner_id`, `ffxiver`) VALUES(?,?,?,?)",
[lsName, serverId, ownerId, ffxiver],
(r, f, e) => {
if (!e) {
reloadModel(lsName, serverId, ffxiver);
resolve(true);
} else {
reject(e);
}
}
);
} else {
resolve(false);
}
});
};
export const addChannelForLinkshell = (linkshellId, channelId, guildId, webhookUrl) => {
return new Promise((resolve, reject) => {
runPrepQuery("DELETE FROM linkshellchannels WHERE guildId = ? AND channelId = ? LIMIT 1", [guildId, channelId], (r,f,e) => {
if(!e) {
runPrepQuery(
"INSERT INTO linkshellchannels (channelId, linkshellId, guildId, webhookUrl) VALUES(?,?,?,?)",
[channelId, linkshellId, guildId, webhookUrl],
(r, f, e) => {
if (!e) {
reloadModelById(linkshellId);
resolve(true);
} else {
reject(e);
}
}
);
} else {
reject(e);
}
})
});
};
export const getLSFromGuildId = (gid) => {
for (const platform in linkshells) {
for (const server in linkshells[platform]) {
for (const linkshell in linkshells[platform][server]) {
for (const channel in linkshells[platform][server][linkshell].channels) {
if (linkshells[platform][server][linkshell].channels[channel].guildId == gid) {
return getLSModel(linkshell, platform, server)
}
}
}
}
}
return false;
};
export const getLSModel = (ls, platform, server) => {
try {
return linkshells[platform][server][ls] ? linkshells[platform][server][ls] : false;
} catch (e) {
console.log(e)
return false
}
};
export const updateLSModel = (ls, newModel) => {
if (!linkshells[ls]) return false;
linkshells[ls] = { ...linkshells[ls], ...newModel };
return true;
};
export const lsExists = (ls, server, version) => {
return new Promise((resolve, reject) => {
const lsExistsResult = runPrepQuery(
"SELECT * FROM linkshells WHERE server = ? AND `name` = ? AND `ffxiver` = ? LIMIT 0,1",
[server, ls, version],
(r, f, e) => {
if (e) return reject(e);
if (numRows(r)) {
resolve(true);
} else {
resolve(false);
}
}
);
});
};
export const loadModelFromDB = () => {
return new Promise((resolve, reject) => {
runPrepQuery("SELECT * FROM linkshells", [], async (r, f, e) => {
if (!e) {
if (numRows(r)) {
for(const row of r) {
updateMemory(row)
}
}
} else {
console.log(e);
}
resolve(true)
});
})
};
const updateMemory = async (row) => {
if(!linkshells[row.ffxiver]) linkshells[row.ffxiver] = {}
if(!linkshells[row.ffxiver][row.server]) linkshells[row.ffxiver][row.server] = {}
if(linkshells[row.ffxiver][row.server][row.name]?.webhookTimer) clearTimeout(linkshells[row.ffxiver][row.server][row.name].webhookTimer)
linkshells[row.ffxiver][row.server][row.name] = row
linkshells[row.ffxiver][row.server][row.name].channels = await getLSChannels(row.id);
linkshells[row.ffxiver][row.server][row.name].webhookQueue = []
linkshells[row.ffxiver][row.server][row.name].webhookTimer = processWebhookQueue(linkshells[row.ffxiver][row.server][row.name])
}
const processWebhookQueue = (linkshell) => {
const nextItem = linkshell.webhookQueue.shift()
if(!nextItem) {
return setTimeout(() => { processWebhookQueue(linkshell) }, 100)
}
const packet = nextItem.packet
const trimmedMsg = packet.payload.message.replace(/[\n\r]/g, '').trim()
console.log(`"${packet.payload.name + trimmedMsg}"`)
const msgHash = crypto.createHash("md5").update(packet.payload.name + trimmedMsg).digest("hex");
for (const channel of linkshell.channels) {
if (!findInRecent(msgHash, channel.hookHistory)) {
channel.hookHistory.push({msgHash: msgHash, timeStamp: Date.now()})
Debug(`${msgHash} added to message hashes.`)
channel.Webhook.setUsername(packet.payload.name);
channel.Webhook.setAvatar("https://ui-avatars.com/api/?background=random&name=" + packet.payload.name)
channel.Webhook.send(trimmedMsg);
if(channel.hookHistory.length > 100) {
const removed = channel.hookHistory.shift()
Debug(`${removed.msgHash} removed from hashes.`)
}
}
}
return setTimeout(() => { processWebhookQueue(linkshell) }, 100)
}
const findInRecent = (msgHash, history) => {
for(const idx in history) {
const msgHashData = history[idx]
if(msgHashData.msgHash == msgHash) {
Debug(`Found Message Hash`)
if(msgHashData.timeStamp + 5000 > Date.now()) {
Debug(`Hash Timestamp Not Expired ${msgHashData.timeStamp + 5000} / ${Date.now()}`)
return true
} else {
Debug(`Hash Timestamp Expired ${msgHashData.timeStamp + 5000} / ${Date.now()}`)
}
} else {
Debug(`Message Hash Not Found. ${msgHash}`)
}
}
return false
}

View File

@ -0,0 +1,73 @@
import { runPrepQuery, numRows } from "../Utility/db.js";
import { Warn } from "./loggerUtility.js";
const users = {}
import jwt from 'jsonwebtoken';
const { sign, verify } = jwt;
const addUserToMemory = (userId, authToken) => {
users[userId] = {userId, authToken}
}
const removeUser = (userId) => {
if(users[userId]?.socket) {
socket.destroy()
delete users[userId]
runPrepQuery("DELETE FROM users WHERE userId = ? LIMIT 1", [userId], (r,f,e) => {})
}
}
const getUser = (userId) => {
if(users[userId]) return users[userId]
return false
}
export const getUserFromJwt = (jwt) => {
try {
const decoded = verify(jwt, process.env.JWT_SECRET)
return getUser(decoded.userId)
} catch (e) {
console.log(e)
return false
}
}
export const setUserSocket = (socket, userId) => {
socket.userId = userId
users[userId].socket = socket
}
export const addUser = (userId) => {
return new Promise((resolve, reject) => {
runPrepQuery("SELECT * FROM users WHERE userId = ? LIMIT 0,1", [userId], async (r,f,e) => {
if(!e) {
let token
if(!numRows(r)) {
token = sign({userId: userId}, process.env.JWT_SECRET)
runPrepQuery("INSERT INTO users (authToken, userId) VALUES(?,?)", [token, userId], async (r,f,e) => {
if(!e) {
return resolve({userId, authToken: token})
}
return resolve(false)
})
} else {
return resolve({userId, authToken: token})
}
}
});
})
}
export const loadUsersFromDB = () => {
return new Promise((resolve, reject) => {
runPrepQuery("SELECT * FROM users", [], async (r, f, e) => {
if (!e) {
if (numRows(r)) {
for(const row of r) {
users[row.userId] = row;
}
}
} else {
console.log(e);
}
resolve(true)
});
})
};

View File

@ -0,0 +1,190 @@
import {
SlashCommandBuilder,
ActionRowBuilder,
Events,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
PermissionsBitField,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
ButtonBuilder,
ButtonStyle,
ComponentType,
} from "discord.js";
import { getLSFromGuildId, getLSModel, updateLSModel, createNewLS } from "../../Utility/lsModel.js";
import { event } from "../../Utility/eventHandler.js";
import { client } from "../../Utility/discordClient.js";
import config from "../../config.json" assert { type: "json" };
import uuid4 from "uuid4";
import { runPrepQuery, numRows } from "../../Utility/db.js";
const servers = config.servers;
const options = [];
for (const idx in servers) {
options.push({ name: servers[idx], value: idx });
}
export default {
data: new SlashCommandBuilder()
.setName("lcaddlinkshell")
.setDescription("Add your linkshell to the Link Cloud service.")
/*.addStringOption((option) =>
option
.setName("linkshellname")
.setDescription("The name of your linkshell, exactly as it appears in game.")
.setRequired(true)
)*/
.addStringOption((option) =>
option
.setName("provider")
.setDescription("FFXI Server Type")
.setRequired(true)
.addChoices(...options)
),
async execute(interaction) {
if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
return await interaction.reply({
content: 'You must have the "Administrator" flag to use this command.',
ephemeral: true,
});
}
const version = interaction.options.getString("provider");
const confirm = new ButtonBuilder().setCustomId("confirm").setLabel("Yes").setStyle(ButtonStyle.Success);
const cancel = new ButtonBuilder().setCustomId("cancel").setLabel("No").setStyle(ButtonStyle.Danger);
const response = version > 0 ? await interaction.reply({
content: `Have you set up your LinkCloud addon for FFXI using the \`/lcjoin\` command?`,
components: [new ActionRowBuilder().addComponents(cancel, confirm)],
ephemeral: true,
}) : await interaction.reply({
content: `LinkCloud is not currently supported by this server. Please visit our discord for more info, or to get this server added to the list.`,
ephemeral: true,
})
const collectorFilter = (i) => i.user.id === interaction.user.id;
try {
const confirmation = await response.awaitMessageComponent({ filter: collectorFilter, time: 60_000 });
if (confirmation.customId === "confirm") {
let sqlCreationId = false
const sqlResult = await runPrepQuery("SELECT * FROM `pendinglinks` WHERE `userId` = ? LIMIT 0,1", [interaction.user.id], async (r,f,e) => {
if(numRows(r)) {
sqlCreationId = r[0].linkId
}
const creationId = sqlCreationId ? sqlCreationId : uuid4();
if(!sqlCreationId) {
await runPrepQuery("INSERT INTO `pendinglinks` (`linkId`, `userId`, `ffxiver`) VALUES(?,?, ?)", [creationId, interaction.user.id, version], async(r,f,e) => {
if(e) {
return confirmation.update({
content: `Something Failed. Try again later.`,
components: [],
});
}
})
}
await confirmation.update({
content: `# LinkCloud Linkshell Setup\nPlease follow the steps below.\n\n- Equip the Linkshell you wish to equip in the Linkshell #1 slot.\n- UnEquip any Linkshell equipped in the Linkshell #2 slot.\n- Run the command below in game.\n\n\`\`\`//lc addlinkshell ${creationId}\`\`\`\n\nYou will receive a direct message in discord once the process has completed.`,
components: [],
});
})
} else if (confirmation.customId === "cancel") {
await confirmation.update({
content: "Please run the `/lcjoin` command first, then run this command again.",
components: [],
});
}
} catch (e) {
console.log(e)
await interaction.editReply({
content: "Confirmation not received within 1 minute, cancelling",
components: [],
ephemeral: true,
});
}
/*const response = await interaction.reply({
content: `**Please run the command below in game.**\n\n\`\`\`//lc addlinkshell ${creationId}\`\`\``,
ephemeral: true,
});*/
/*const lsName = interaction.options.getString("linkshellname");
const version = interaction.options.getString("ffxiversion");
let serverName = "";
if (Number(serverId) < 1000) {
serverName = servers.retail[serverId];
} else {
serverName = servers.community[serverId];
}
console.log(serverId, lsName);
const confirm = new ButtonBuilder()
.setCustomId("confirm")
.setLabel("Connect")
.setStyle(ButtonStyle.Success);
const cancel = new ButtonBuilder().setCustomId("cancel").setLabel("Cancel").setStyle(ButtonStyle.Danger);
const response = await interaction.reply({
content: `**Confirm the information below to finish**\n\n**Linkshell Name** \`${lsName}\`\n**FFXI Server** \`${serverName}\``,
components: [new ActionRowBuilder().addComponents(cancel, confirm)],
ephemeral: true,
});
const collectorFilter = (i) => i.user.id === interaction.user.id;
try {
const confirmation = await response.awaitMessageComponent({ filter: collectorFilter, time: 60_000 });
if (confirmation.customId === "confirm") {
await confirmation.update({
content: `Linkshell ${lsName} on ${serverName} has been added to LinkCloud.`,
components: [],
ephemeral: true,
});
} else if (confirmation.customId === "cancel") {
await confirmation.update({ content: "Action cancelled", components: [] });
}
} catch (e) {
await interaction.editReply({
content: "Confirmation not received within 1 minute, cancelling",
components: [],
ephemeral: true,
});
}
*/
},
};
event.on("on_serverId", async (interaction) => {});
/*
console.log(lsName)
try {
let lsModel = getLSModel(lsName.trim().replace(String.fromCharCode(10), '').replace(String.fromCharCode(13), ''))
const channel = await client.channels.fetch(interaction.channelId)
console.log(lsModel)
if (lsModel) {
await interaction.reply({ content: 'Your linkshell already exists.', ephemeral: true });
} else {
let myHook = false
channel.fetchWebhooks().then(async hooks => {
hooks.each(hook => {
if (hook.owner.id == config.clientId) {
myHook = hook
}
})
if(!myHook) {
myHook = await channel.createWebhook({
name: 'LinkCloud',
avatar: 'https://i.imgur.com/AfFp7pu.png',
}).catch(console.error);
}
if(myHook) {
createNewLS(interaction.guildId, interaction.channelId, lsName, interaction.user.id, myHook.url)
await interaction.reply({ content: 'Your linkshell was successfully connected!', ephemeral: true });
} else {
await interaction.reply({ content: 'Failed to create webhook.', ephemeral: true });
}
}).catch(console.error);
}
} catch (e) {
console.log(e)
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}*/

View File

@ -0,0 +1,104 @@
import {
SlashCommandBuilder,
ActionRowBuilder,
Events,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
PermissionsBitField,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
ButtonBuilder,
ButtonStyle,
ComponentType,
} from "discord.js";
import { addChannelForLinkshell } from "../../Utility/lsModel.js";
import { event } from "../../Utility/eventHandler.js";
import { client } from "../../Utility/discordClient.js";
import config from "../../config.json" assert { type: "json" };
import uuid4 from "uuid4";
import { runPrepQuery, numRows } from "../../Utility/db.js";
import servers from "../../resources/servers.json" assert { type: 'json' };
export default {
data: new SlashCommandBuilder()
.setName("lccreateecho")
.setDescription("Connect a linkshell to a text-chat channel."),
async execute(interaction) {
if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
return await interaction.reply({
content: 'You must have the "Administrator" flag to use this command.',
ephemeral: true,
});
}
const options = [];
runPrepQuery('SELECT * FROM linkshells WHERE discord_owner_id = ?', [interaction.user.id], async (r,f,e) => {
if (numRows(r)) {
for(const row of r) {
let lsName = row.name
if (row.ffxiver == 1) {
lsName = `${lsName} on ${servers.se[row.server]}`
} else {
lsName = `${lsName} on ${servers.platforms[ffxiver]}`
}
options.push(new StringSelectMenuOptionBuilder()
.setLabel(row.name)
.setDescription(lsName)
.setValue(String(row.id)))
}
const select = new StringSelectMenuBuilder()
.setCustomId('selectedLinkshell')
.setPlaceholder('Select a Linkshell')
.addOptions(...options);
const row = new ActionRowBuilder()
.addComponents(select);
const response = await interaction.reply({
content: 'Please select the Linkshell you want to echo. Note: This will override any echo currently set up in this channel.',
components: [row],
ephemeral: true
})
const collector = response.createMessageComponentCollector({ componentType: ComponentType.StringSelect, time: 60_000 });
collector.on('collect', async i => {
const linkshellId = i.values[0];
let myHook = false
const channel = await client.channels.fetch(i.channelId)
channel.fetchWebhooks().then(async hooks => {
hooks.each(hook => {
if (hook.owner.id == config.clientId) {
myHook = hook
}
})
if(!myHook) {
myHook = await channel.createWebhook({
name: 'LinkCloud',
avatar: 'https://i.imgur.com/AfFp7pu.png',
}).catch(console.error);
}
if(myHook) {
if(await addChannelForLinkshell(linkshellId, i.channelId, i.guildId, myHook.url)) {
await i.reply({ content: '# Echo Created!\nYou should see messages as soon as stream data for this linkshell has been received.', ephemeral: true });
} else {
await i.reply({ content: 'Failed to create echo.', ephemeral: true });
}
} else {
await i.reply({ content: 'Failed to create webhook.', ephemeral: true });
}
}).catch(console.error);
});
} else {
await interaction.reply({
content: 'You have not added any linkshells. Please use the `/lcaddlinkshell` command to add one.',
ephemeral: true
});
}
})
},
};

View File

@ -0,0 +1,12 @@
import { SlashCommandBuilder } from 'discord.js'
import { getLSFromGuildId, getLSModel } from "../../Utility/lsModel.js"
export default {
data: new SlashCommandBuilder()
.setName('lchelp')
.setDescription('View Bot Info'),
async execute(interaction) {
console.log(interaction)
console.log(getLSFromGuildId(interaction.guildId))
await interaction.reply('Some whitty response!');
},
};

View File

@ -0,0 +1,41 @@
import {
SlashCommandBuilder,
ActionRowBuilder,
ButtonStyle,
ModalBuilder,
TextInputBuilder,
ButtonBuilder,
} from "discord.js";
import { getLSFromGuildId, getLSModel } from "../../Utility/lsModel.js";
import { event } from "../../Utility/eventHandler.js";
import { numRows, runPrepQuery } from "../../Utility/db.js";
import uuid4 from "uuid4";
import { addUser } from "../../Utility/userModel.js";
export default {
data: new SlashCommandBuilder().setName("lcjoin").setDescription("Configure and download the LinkCloud addon."),
async execute(interaction) {
const userString = interaction.user.id;
const user = await addUser(userString);
const button = new ButtonBuilder()
.setLabel("LinkCloud for FFXI Windower Retail")
.setURL(`http://linkcloud.drunken.games:3000/addons/windower/retail/${userString}`)
.setStyle(ButtonStyle.Link);
const button2 = new ButtonBuilder()
.setLabel("LinkCloud for FFXI Ashita Retail")
.setURL(`http://linkcloud.drunken.games:3000/addons/ashita/retail/${userString}`)
.setStyle(ButtonStyle.Link);
if (user) {
await interaction.reply({
content: `# LinkCloud Setup\n### Installing LinkCloud for FFXI Retail (PlayOnline)\nIf you are using FFXI Windower 4, use the "[LinkCloud for FFXI Windower Retail](http://linkcloud.drunken.games:3000/addons/windower/retail/${userString})" button to download the addon\nIf you are using Ashita, use the "[LinkCloud for FFXI Ashita Retail](http://linkcloud.drunken.games:3000/addons/ashita/retail/${userString})" button to download the addon\n- Extract the addon to your \`C:\\Program Files (x86)\\Windower\\addons\` folder.\n- Load the addon with the \`//lua l linkcloud\` command\n- To load this addon on startup, add \`lua load linkcloud\` to your \`C:\\Program Files (x86)\\Windower\\scripts\\init.txt\` file.\n\n### Installing LinkCloud for Horizon XI\nWe are currently working with Horizon XI to have our addon added to the list.\n\n### Manual Installation\nIf you have installed the addon manually, you will need to set the auth token for your client. This is not currently supported in the Alpha testing phase.\n\n# Support the project\nPlease join the project discord for continued support and updates, but mostly just to let Smokey know you care ;)\nhttps://discord.gg/n5VYHSQbhA`,
ephemeral: true,
components: [
new ActionRowBuilder().addComponents(button),
new ActionRowBuilder().addComponents(button2),
],
});
} else {
await interaction.reply({ content: "Failed to create user.", ephemeral: true });
}
},
};

View File

@ -0,0 +1,13 @@
import { SlashCommandBuilder, ActionRowBuilder, Events, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'
import { getLSFromGuildId, getLSModel } from "../../Utility/lsModel.js"
import { event } from "../../Utility/eventHandler.js";
export default {
data: new SlashCommandBuilder()
.setName('lcstatus')
.setDescription('Show the status of LinkCloud.'),
async execute(interaction) {
const userString = interaction.user.id
await interaction.reply({ content: `Link Cloud Status: Online\nYour LinkCloud client is connected to the LinkCloud service.`, ephemeral: true });
},
};

13
server/config.json Normal file
View File

@ -0,0 +1,13 @@
{
"token": "xxx",
"clientId": "xxx",
"guildId": "xxx",
"db": {
"user": "xxx",
"host": "xxx",
"database": "xxx",
"password": "xxx",
"connectionLimit": 10
},
"servers": ["Not Listed", "Square Enix", "Horizon XI"]
}

View File

@ -0,0 +1,50 @@
const { REST, Routes } = require('discord.js');
const { clientId, guildId, token } = require('./config.json');
const fs = require('node:fs');
const path = require('node:path');
const commands = [];
// Grab all the command folders from the commands directory you created earlier
const foldersPath = path.join(__dirname, 'commands');
const commandFolders = fs.readdirSync(foldersPath);
const doStuff = async () => {
for (const folder of commandFolders) {
// Grab all the command files from the commands directory you created earlier
const commandsPath = path.join(foldersPath, folder);
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const c = await import('file://' + filePath);
const command = c.default
console.log(command)
if ('data' in command && 'execute' in command) {
commands.push(command.data.toJSON());
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
}
const rest = new REST().setToken(token);
(async () => {
try {
console.log(`Started refreshing ${commands.length} application (/) commands.`);
// The put method is used to fully refresh all commands in the guild with the current set
const data = await rest.put(
Routes.applicationGuildCommands(clientId, guildId),
{ body: commands },
);
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
} catch (error) {
// And of course, make sure you catch and log any errors!
console.error(error);
}
})();
}
doStuff()
// Construct and prepare an instance of the REST module
// and deploy your commands!

147
server/ffxidiscord.js Normal file
View File

@ -0,0 +1,147 @@
const Net = require('net');
const port = 6033;
const { Webhook } = require('discord-webhook-node');
const hook = new Webhook("https://discord.com/api/webhooks/1157030453807698040/K80q5yE4qvKeAi7W4arGyP5YH82dTiGDALw2ESU7uzQ3b_Nv5ylgKno80sLsOTVnVQV5");
const server = new Net.Server();
const botToken = 'MTE1NzAzNDUyMzAyMDc1OTE3MA.GQH1hm.H4vZIz1IpbX31xtTObuIUZa1sN9m5VrM_f0iU0'
server.listen(port, function() {
console.log(`Server listening for connection requests on socket localhost:${port}`);
});
messageQueue = []
const CryptoJS = require("crypto-js");
const Discord = require("discord.js");
const { Client, GatewayIntentBits, SlashCommandBuilder } = require('discord.js');
const { MessageMentions: { USERS_PATTERN } } = require('discord.js');
const { v4: uuidv4 } = require('uuid');
var hasleader = false
var ignoreBuffer = []
const client = new Discord.Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
})
const commands = {}
client.on("messageCreate", function(message) {
nickname = false
if (message.author.bot) return;
if (message.channelId != "1128902878166269962") return;
guild = client.guilds.cache.get(message.guildId)
guild.members.cache.forEach((member) => {
if (member.user.id == message.author.id) {
nickname = member.nickname
}
})
if (nickname == false || nickname === null) {
nickname = message.author.globalName
}
ignoreBuffer.push("[" + nickname + "] " + message.content.replace(/[\n\r]/g, ''))
messageQueue.push("[" + nickname + "] " + message.content.replace(/[\n\r]/g, ''))
if(ignoreBuffer.length > 1000) {
ignoreBuffer.shift()
}
});
client.login(botToken);
messageBuffer = []
sentMessages = []
var checkDuplicate = (message_hash, time) => {
for (let msg of sentMessages) {
if (msg.hash == message_hash) {
let differential = Math.abs(time - msg.time)
if (differential <= 1) {
return true
}
}
}
return false
}
var processBuffer = () => {
if(messageBuffer.length) {
msg = messageBuffer.shift()
duplicate = true
rawInventoryString = String(msg.metaData.server) + String(msg.payload.name) + String(msg.payload.message.replace(/[\n\r]/g, ''));
msgTime = String(msg.metaData.time)
hashedIS = CryptoJS.MD5(rawInventoryString).toString()
if(!checkDuplicate(hashedIS, msgTime) && !ignoreBuffer.includes(msg.payload.message.replace(/[\n\r]/g, ''))) {
let msgStore = {
"hash": hashedIS,
"time": msgTime
}
sentMessages.push(msgStore)
if (sentMessages.length > 100) {
sentMessages.shift()
}
hook.setUsername(msg.payload.name);
hook.setAvatar("https://ui-avatars.com/api/?background=random&name=" + msg.payload.name)
hook.send(msg.payload.message.replace(/[\n\r]/g, ''));
if(msg.payload.message.toLowerCase().includes("discord info") || msg.payload.message.toLowerCase().includes("discord invite") || msg.payload.message.toLowerCase().includes("invite to discord")) {
msg.metaData.server = 255
msg.payload.message = 'https://discord.gg/9ydGN8AHUu'
messageQueue.push(msg.payload.message)
}
} else {
console.log("Duplicate Detected.")
}
}
setTimeout(processBuffer, 250)
}
var processSocketResponse = (socket) => {
if(messageQueue.length) {
socket.write(messageQueue.shift() + '\n')
} else {
socket.write('PONG\n');
}
socket.lastPingTime = new Date();
}
var removeSocketAsLeader = (socket) => {
if (socket.isLeader) { hasleader = false }
}
setTimeout(processBuffer, 5000)
ffxiClient = {
}
server.on('connection', function(socket) {
socket.uid = uuidv4();
console.log('A new connection has been established.');
socket.write('CONNECTION_ACCEPTED\n');
socket.on('data', function(chunk) {
payload = JSON.parse(chunk.toString())
if(payload['type'] == "MESSAGE") {
messageBuffer.push(payload)
socket.linkshell = payload.payload.linkshellname
socket.write('RECEIVEDOK\n');
console.log(payload);
} else if (payload['type'] == "PING") {
processSocketResponse(socket)
} else if (payload['type'] == "OTHER") {
socket.write('RECEIVEDOK\n');
} else if (payload['type'] == "HANDSHAKE") {
socket.write('WELCOME\n');
}
});
socket.on('end', function() {
removeSocketAsLeader(socket)
console.log('Closing connection with the client');
});
socket.on('error', function(err) {
removeSocketAsLeader(socket)
console.log(`Error: ${err}`);
});
});
//

43
server/logs/combined.log Normal file
View File

@ -0,0 +1,43 @@
{"level":"info","message":"Connecting to MySQL DB...","timestamp":"2024-08-02T05:37:30.066Z"}
{"level":"info","message":"Server bound to 0.0.0.0 on port 5050.","timestamp":"2024-08-02T05:37:30.076Z"}
{"level":"info","message":"Database connected successfully.","timestamp":"2024-08-02T05:37:30.174Z"}
{"level":"info","message":"Connecting to MySQL DB...","timestamp":"2024-08-02T05:40:36.869Z"}
{"level":"info","message":"Server bound to 0.0.0.0 on port 5050.","timestamp":"2024-08-02T05:40:36.889Z"}
{"level":"info","message":"Database connected successfully.","timestamp":"2024-08-02T05:40:36.984Z"}
{"level":"info","message":"Connecting to MySQL DB...","timestamp":"2024-08-02T05:46:42.073Z"}
{"level":"info","message":"Server bound to 0.0.0.0 on port 5050.","timestamp":"2024-08-02T05:46:42.092Z"}
{"level":"info","message":"Database connected successfully.","timestamp":"2024-08-02T05:46:42.185Z"}
{"level":"info","message":"CONNECTED: 24.72.146.204:49705","timestamp":"2024-08-02T05:57:12.452Z"}
{"level":"warn","message":"Clock out of sync: 15 second(s) out.","timestamp":"2024-08-02T05:57:12.530Z"}
{"level":"info","message":"CLOSED: 24.72.146.204 49705","timestamp":"2024-08-02T05:57:12.531Z"}
{"level":"info","message":"CONNECTED: 24.72.146.204:50653","timestamp":"2024-08-02T06:06:31.252Z"}
{"level":"info","message":"[24.72.146.204] connection ACCEPTED.","timestamp":"2024-08-02T06:06:31.334Z"}
{"level":"error","message":"Uncaught exception: read ECONNRESET","timestamp":"2024-08-02T06:06:33.603Z"}
{"level":"info","message":"Connecting to MySQL DB...","timestamp":"2024-08-02T06:06:35.301Z"}
{"level":"info","message":"Server bound to 0.0.0.0 on port 5050.","timestamp":"2024-08-02T06:06:35.308Z"}
{"level":"info","message":"Database connected successfully.","timestamp":"2024-08-02T06:06:35.392Z"}
{"level":"info","message":"CONNECTED: 24.72.146.204:50707","timestamp":"2024-08-02T06:07:03.663Z"}
{"level":"info","message":"[24.72.146.204] connection ACCEPTED.","timestamp":"2024-08-02T06:07:03.744Z"}
{"level":"info","message":"Connecting to MySQL DB...","timestamp":"2024-08-02T06:08:15.712Z"}
{"level":"info","message":"Server bound to 0.0.0.0 on port 5050.","timestamp":"2024-08-02T06:08:15.745Z"}
{"level":"info","message":"Database connected successfully.","timestamp":"2024-08-02T06:08:15.850Z"}
{"level":"info","message":"CONNECTED: 24.72.146.204:50847","timestamp":"2024-08-02T06:08:46.112Z"}
{"level":"info","message":"[24.72.146.204] connection ACCEPTED.","timestamp":"2024-08-02T06:08:46.192Z"}
{"level":"info","message":"Connecting to MySQL DB...","timestamp":"2024-08-02T06:14:51.322Z"}
{"level":"info","message":"Server bound to 0.0.0.0 on port 5050.","timestamp":"2024-08-02T06:14:51.341Z"}
{"level":"info","message":"Database connected successfully.","timestamp":"2024-08-02T06:14:51.450Z"}
{"level":"info","message":"CONNECTED: 24.72.146.204:51416","timestamp":"2024-08-02T06:15:21.941Z"}
{"level":"info","message":"[24.72.146.204] connection ACCEPTED.","timestamp":"2024-08-02T06:15:22.022Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:16:04.140Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:16:04.231Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:16:04.388Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:16:35.453Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:16:35.541Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:17:52.177Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:17:52.258Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:17:58.943Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:17:59.024Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:18:24.260Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:18:24.510Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:18:27.506Z"}
{"level":"info","message":"[24.72.146.204] LS MESSAGE RECEIVED.","timestamp":"2024-08-02T06:18:27.584Z"}

1
server/logs/error.log Normal file
View File

@ -0,0 +1 @@
{"level":"error","message":"Uncaught exception: read ECONNRESET","timestamp":"2024-08-02T06:06:33.603Z"}

56
server/logs/exception.log Normal file
View File

@ -0,0 +1,56 @@
{"date":"Thu Apr 04 2024 09:43:47 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.01,0.02,0],"uptime":14063589.16},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":1004,"memoryUsage":{"arrayBuffers":414379,"external":4718740,"heapTotal":28983296,"heapUsed":26803808,"rss":83767296},"pid":3324100,"uid":1004,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Apr 04 2024 12:50:20 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0,0,0],"uptime":14074781.55},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":616669,"external":4921030,"heapTotal":30031872,"heapUsed":28303960,"rss":87810048},"pid":3329402,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Apr 04 2024 15:16:08 GMT+0000 (Coordinated Universal Time)","error":{},"exception":true,"level":"error","message":"uncaughtException: crypto.createHash is not a function\nTypeError: crypto.createHash is not a function\n at processWebhookQueue (file:///home/ffxi/discordbot/Utility/lsModel.js:197:32)\n at Timeout._onTimeout (file:///home/ffxi/discordbot/Utility/lsModel.js:186:3)\n at listOnTimeout (node:internal/timers:573:17)\n at process.processTimers (node:internal/timers:514:7)","os":{"loadavg":[0.36,0.12,0.04],"uptime":14083530.13},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":311589,"external":4681486,"heapTotal":28721152,"heapUsed":26739104,"rss":84271104},"pid":3335334,"uid":0,"version":"v20.5.1"},"stack":"TypeError: crypto.createHash is not a function\n at processWebhookQueue (file:///home/ffxi/discordbot/Utility/lsModel.js:197:32)\n at Timeout._onTimeout (file:///home/ffxi/discordbot/Utility/lsModel.js:186:3)\n at listOnTimeout (node:internal/timers:573:17)\n at process.processTimers (node:internal/timers:514:7)","trace":[{"column":32,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":"processWebhookQueue","line":197,"method":null,"native":false},{"column":3,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":"Timeout._onTimeout","line":186,"method":"_onTimeout","native":false},{"column":17,"file":"node:internal/timers","function":"listOnTimeout","line":573,"method":null,"native":false},{"column":7,"file":"node:internal/timers","function":"process.processTimers","line":514,"method":"processTimers","native":false}]}
{"date":"Thu Apr 04 2024 15:49:54 GMT+0000 (Coordinated Universal Time)","error":{"address":"::","code":"EADDRINUSE","errno":-98,"port":5050,"syscall":"listen"},"exception":true,"level":"error","message":"uncaughtException: listen EADDRINUSE: address already in use :::5050\nError: listen EADDRINUSE: address already in use :::5050\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at file:///home/ffxi/discordbot/v2.js:162:8\n at ModuleJob.run (node:internal/modules/esm/module_job:192:25)\n at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)\n at async loadESM (node:internal/process/esm_loader:40:7)\n at async handleMainPromise (node:internal/modules/run_main:66:12)","os":{"loadavg":[0,0,0],"uptime":14085555.69},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":266020,"external":4375222,"heapTotal":40259584,"heapUsed":29239920,"rss":89219072},"pid":3336102,"uid":0,"version":"v20.5.1"},"stack":"Error: listen EADDRINUSE: address already in use :::5050\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at file:///home/ffxi/discordbot/v2.js:162:8\n at ModuleJob.run (node:internal/modules/esm/module_job:192:25)\n at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)\n at async loadESM (node:internal/process/esm_loader:40:7)\n at async handleMainPromise (node:internal/modules/run_main:66:12)","trace":[{"column":16,"file":"node:net","function":"Server.setupListenHandle [as _listen2]","line":1872,"method":"setupListenHandle [as _listen2]","native":false},{"column":12,"file":"node:net","function":"listenInCluster","line":1920,"method":null,"native":false},{"column":7,"file":"node:net","function":"Server.listen","line":2008,"method":"listen","native":false},{"column":8,"file":"file:///home/ffxi/discordbot/v2.js","function":null,"line":162,"method":null,"native":false},{"column":25,"file":"node:internal/modules/esm/module_job","function":"ModuleJob.run","line":192,"method":"run","native":false},{"column":24,"file":"node:internal/modules/esm/loader","function":"async DefaultModuleLoader.import","line":228,"method":"import","native":false},{"column":7,"file":"node:internal/process/esm_loader","function":"async loadESM","line":40,"method":null,"native":false},{"column":12,"file":"node:internal/modules/run_main","function":"async handleMainPromise","line":66,"method":null,"native":false}]}
{"date":"Thu Apr 04 2024 16:33:39 GMT+0000 (Coordinated Universal Time)","error":{},"exception":true,"level":"error","message":"uncaughtException: Log is not defined\nReferenceError: Log is not defined\n at findInRecent (file:///home/ffxi/discordbot/Utility/lsModel.js:215:13)\n at processWebhookQueue (file:///home/ffxi/discordbot/Utility/lsModel.js:199:14)\n at Timeout._onTimeout (file:///home/ffxi/discordbot/Utility/lsModel.js:187:3)\n at listOnTimeout (node:internal/timers:573:17)\n at process.processTimers (node:internal/timers:514:7)","os":{"loadavg":[0.03,0.03,0],"uptime":14088180.68},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":373393,"external":4743290,"heapTotal":30818304,"heapUsed":28378384,"rss":91467776},"pid":3336126,"uid":0,"version":"v20.5.1"},"stack":"ReferenceError: Log is not defined\n at findInRecent (file:///home/ffxi/discordbot/Utility/lsModel.js:215:13)\n at processWebhookQueue (file:///home/ffxi/discordbot/Utility/lsModel.js:199:14)\n at Timeout._onTimeout (file:///home/ffxi/discordbot/Utility/lsModel.js:187:3)\n at listOnTimeout (node:internal/timers:573:17)\n at process.processTimers (node:internal/timers:514:7)","trace":[{"column":13,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":"findInRecent","line":215,"method":null,"native":false},{"column":14,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":"processWebhookQueue","line":199,"method":null,"native":false},{"column":3,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":"Timeout._onTimeout","line":187,"method":"_onTimeout","native":false},{"column":17,"file":"node:internal/timers","function":"listOnTimeout","line":573,"method":null,"native":false},{"column":7,"file":"node:internal/timers","function":"process.processTimers","line":514,"method":"processTimers","native":false}]}
{"date":"Thu Apr 04 2024 17:55:10 GMT+0000 (Coordinated Universal Time)","error":{},"exception":true,"level":"error","message":"uncaughtException: Debug is not defined\nReferenceError: Debug is not defined\n at findInRecent (file:///home/ffxi/discordbot/Utility/lsModel.js:211:13)\n at processWebhookQueue (file:///home/ffxi/discordbot/Utility/lsModel.js:195:14)\n at Timeout._onTimeout (file:///home/ffxi/discordbot/Utility/lsModel.js:188:22)\n at listOnTimeout (node:internal/timers:573:17)\n at process.processTimers (node:internal/timers:514:7)","os":{"loadavg":[0.11,0.09,0.03],"uptime":14093072.19},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":348143,"external":4718040,"heapTotal":30556160,"heapUsed":27126560,"rss":84647936},"pid":3339231,"uid":0,"version":"v20.5.1"},"stack":"ReferenceError: Debug is not defined\n at findInRecent (file:///home/ffxi/discordbot/Utility/lsModel.js:211:13)\n at processWebhookQueue (file:///home/ffxi/discordbot/Utility/lsModel.js:195:14)\n at Timeout._onTimeout (file:///home/ffxi/discordbot/Utility/lsModel.js:188:22)\n at listOnTimeout (node:internal/timers:573:17)\n at process.processTimers (node:internal/timers:514:7)","trace":[{"column":13,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":"findInRecent","line":211,"method":null,"native":false},{"column":14,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":"processWebhookQueue","line":195,"method":null,"native":false},{"column":22,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":"Timeout._onTimeout","line":188,"method":"_onTimeout","native":false},{"column":17,"file":"node:internal/timers","function":"listOnTimeout","line":573,"method":null,"native":false},{"column":7,"file":"node:internal/timers","function":"process.processTimers","line":514,"method":"processTimers","native":false}]}
{"date":"Thu Apr 04 2024 20:28:59 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.04,0.04,0.01],"uptime":14102301.17},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":430329,"external":4745282,"heapTotal":31866880,"heapUsed":29872752,"rss":91582464},"pid":3341647,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Fri Apr 05 2024 07:42:11 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0,0.02,0],"uptime":14142692.63},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":1004,"memoryUsage":{"arrayBuffers":428736,"external":4733097,"heapTotal":29769728,"heapUsed":27437224,"rss":85319680},"pid":3349137,"uid":1004,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Fri Apr 05 2024 07:59:46 GMT+0000 (Coordinated Universal Time)","error":{"address":"::","code":"EADDRINUSE","errno":-98,"port":5050,"syscall":"listen"},"exception":true,"level":"error","message":"uncaughtException: listen EADDRINUSE: address already in use :::5050\nError: listen EADDRINUSE: address already in use :::5050\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at file:///home/ffxi/discordbot/v2.js:166:8\n at ModuleJob.run (node:internal/modules/esm/module_job:192:25)\n at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)\n at async loadESM (node:internal/process/esm_loader:40:7)\n at async handleMainPromise (node:internal/modules/run_main:66:12)","os":{"loadavg":[0,0,0],"uptime":14143748.22},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":266020,"external":4375222,"heapTotal":40521728,"heapUsed":29328008,"rss":88952832},"pid":3349433,"uid":0,"version":"v20.5.1"},"stack":"Error: listen EADDRINUSE: address already in use :::5050\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at file:///home/ffxi/discordbot/v2.js:166:8\n at ModuleJob.run (node:internal/modules/esm/module_job:192:25)\n at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)\n at async loadESM (node:internal/process/esm_loader:40:7)\n at async handleMainPromise (node:internal/modules/run_main:66:12)","trace":[{"column":16,"file":"node:net","function":"Server.setupListenHandle [as _listen2]","line":1872,"method":"setupListenHandle [as _listen2]","native":false},{"column":12,"file":"node:net","function":"listenInCluster","line":1920,"method":null,"native":false},{"column":7,"file":"node:net","function":"Server.listen","line":2008,"method":"listen","native":false},{"column":8,"file":"file:///home/ffxi/discordbot/v2.js","function":null,"line":166,"method":null,"native":false},{"column":25,"file":"node:internal/modules/esm/module_job","function":"ModuleJob.run","line":192,"method":"run","native":false},{"column":24,"file":"node:internal/modules/esm/loader","function":"async DefaultModuleLoader.import","line":228,"method":"import","native":false},{"column":7,"file":"node:internal/process/esm_loader","function":"async loadESM","line":40,"method":null,"native":false},{"column":12,"file":"node:internal/modules/run_main","function":"async handleMainPromise","line":66,"method":null,"native":false}]}
{"date":"Fri Apr 05 2024 08:00:06 GMT+0000 (Coordinated Universal Time)","error":{"address":"::","code":"EADDRINUSE","errno":-98,"port":5050,"syscall":"listen"},"exception":true,"level":"error","message":"uncaughtException: listen EADDRINUSE: address already in use :::5050\nError: listen EADDRINUSE: address already in use :::5050\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at file:///home/ffxi/discordbot/v2.js:166:8\n at ModuleJob.run (node:internal/modules/esm/module_job:192:25)\n at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)\n at async loadESM (node:internal/process/esm_loader:40:7)\n at async handleMainPromise (node:internal/modules/run_main:66:12)","os":{"loadavg":[0,0,0],"uptime":14143767.3},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":266020,"external":4375222,"heapTotal":40783872,"heapUsed":29466704,"rss":89710592},"pid":3349455,"uid":0,"version":"v20.5.1"},"stack":"Error: listen EADDRINUSE: address already in use :::5050\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at file:///home/ffxi/discordbot/v2.js:166:8\n at ModuleJob.run (node:internal/modules/esm/module_job:192:25)\n at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)\n at async loadESM (node:internal/process/esm_loader:40:7)\n at async handleMainPromise (node:internal/modules/run_main:66:12)","trace":[{"column":16,"file":"node:net","function":"Server.setupListenHandle [as _listen2]","line":1872,"method":"setupListenHandle [as _listen2]","native":false},{"column":12,"file":"node:net","function":"listenInCluster","line":1920,"method":null,"native":false},{"column":7,"file":"node:net","function":"Server.listen","line":2008,"method":"listen","native":false},{"column":8,"file":"file:///home/ffxi/discordbot/v2.js","function":null,"line":166,"method":null,"native":false},{"column":25,"file":"node:internal/modules/esm/module_job","function":"ModuleJob.run","line":192,"method":"run","native":false},{"column":24,"file":"node:internal/modules/esm/loader","function":"async DefaultModuleLoader.import","line":228,"method":"import","native":false},{"column":7,"file":"node:internal/process/esm_loader","function":"async loadESM","line":40,"method":null,"native":false},{"column":12,"file":"node:internal/modules/run_main","function":"async handleMainPromise","line":66,"method":null,"native":false}]}
{"date":"Fri Apr 05 2024 08:01:59 GMT+0000 (Coordinated Universal Time)","error":{"address":"::","code":"EADDRINUSE","errno":-98,"port":5050,"syscall":"listen"},"exception":true,"level":"error","message":"uncaughtException: listen EADDRINUSE: address already in use :::5050\nError: listen EADDRINUSE: address already in use :::5050\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at file:///home/ffxi/discordbot/v2.js:166:8\n at ModuleJob.run (node:internal/modules/esm/module_job:192:25)\n at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)\n at async loadESM (node:internal/process/esm_loader:40:7)\n at async handleMainPromise (node:internal/modules/run_main:66:12)","os":{"loadavg":[1.29,0.45,0.16],"uptime":56.96},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":210035,"external":4319237,"heapTotal":39206912,"heapUsed":29308944,"rss":90214400},"pid":1643,"uid":0,"version":"v20.5.1"},"stack":"Error: listen EADDRINUSE: address already in use :::5050\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at file:///home/ffxi/discordbot/v2.js:166:8\n at ModuleJob.run (node:internal/modules/esm/module_job:192:25)\n at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)\n at async loadESM (node:internal/process/esm_loader:40:7)\n at async handleMainPromise (node:internal/modules/run_main:66:12)","trace":[{"column":16,"file":"node:net","function":"Server.setupListenHandle [as _listen2]","line":1872,"method":"setupListenHandle [as _listen2]","native":false},{"column":12,"file":"node:net","function":"listenInCluster","line":1920,"method":null,"native":false},{"column":7,"file":"node:net","function":"Server.listen","line":2008,"method":"listen","native":false},{"column":8,"file":"file:///home/ffxi/discordbot/v2.js","function":null,"line":166,"method":null,"native":false},{"column":25,"file":"node:internal/modules/esm/module_job","function":"ModuleJob.run","line":192,"method":"run","native":false},{"column":24,"file":"node:internal/modules/esm/loader","function":"async DefaultModuleLoader.import","line":228,"method":"import","native":false},{"column":7,"file":"node:internal/process/esm_loader","function":"async loadESM","line":40,"method":null,"native":false},{"column":12,"file":"node:internal/modules/run_main","function":"async handleMainPromise","line":66,"method":null,"native":false}]}
{"date":"Sun Apr 07 2024 07:52:20 GMT+0000 (Coordinated Universal Time)","error":{"address":"::","code":"EADDRINUSE","errno":-98,"port":3000,"syscall":"listen"},"exception":true,"level":"error","message":"uncaughtException: listen EADDRINUSE: address already in use :::3000\nError: listen EADDRINUSE: address already in use :::3000\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at Function.listen (/home/ffxi/discordbot/node_modules/express/lib/application.js:635:24)\n at listen (file:///home/ffxi/discordbot/webserver.js:39:9)\n at EventEmitter.<anonymous> (file:///home/ffxi/discordbot/v2.js:27:2)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)","os":{"loadavg":[0.02,0.04,0],"uptime":13258.14},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":307504,"external":4677401,"heapTotal":43401216,"heapUsed":25764720,"rss":92110848},"pid":5271,"uid":0,"version":"v20.5.1"},"stack":"Error: listen EADDRINUSE: address already in use :::3000\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at Function.listen (/home/ffxi/discordbot/node_modules/express/lib/application.js:635:24)\n at listen (file:///home/ffxi/discordbot/webserver.js:39:9)\n at EventEmitter.<anonymous> (file:///home/ffxi/discordbot/v2.js:27:2)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)","trace":[{"column":16,"file":"node:net","function":"Server.setupListenHandle [as _listen2]","line":1872,"method":"setupListenHandle [as _listen2]","native":false},{"column":12,"file":"node:net","function":"listenInCluster","line":1920,"method":null,"native":false},{"column":7,"file":"node:net","function":"Server.listen","line":2008,"method":"listen","native":false},{"column":24,"file":"/home/ffxi/discordbot/node_modules/express/lib/application.js","function":"Function.listen","line":635,"method":"listen","native":false},{"column":9,"file":"file:///home/ffxi/discordbot/webserver.js","function":"listen","line":39,"method":null,"native":false},{"column":2,"file":"file:///home/ffxi/discordbot/v2.js","function":null,"line":27,"method":null,"native":false},{"column":5,"file":"node:internal/process/task_queues","function":"process.processTicksAndRejections","line":95,"method":"processTicksAndRejections","native":false}]}
{"date":"Sun Apr 07 2024 07:52:38 GMT+0000 (Coordinated Universal Time)","error":{"address":"::","code":"EADDRINUSE","errno":-98,"port":3000,"syscall":"listen"},"exception":true,"level":"error","message":"uncaughtException: listen EADDRINUSE: address already in use :::3000\nError: listen EADDRINUSE: address already in use :::3000\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at Function.listen (/home/ffxi/discordbot/node_modules/express/lib/application.js:635:24)\n at listen (file:///home/ffxi/discordbot/webserver.js:39:9)\n at EventEmitter.<anonymous> (file:///home/ffxi/discordbot/v2.js:27:2)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)","os":{"loadavg":[0.33,0.1,0.02],"uptime":13275.99},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":306792,"external":4676689,"heapTotal":43139072,"heapUsed":25819912,"rss":91869184},"pid":5312,"uid":0,"version":"v20.5.1"},"stack":"Error: listen EADDRINUSE: address already in use :::3000\n at Server.setupListenHandle [as _listen2] (node:net:1872:16)\n at listenInCluster (node:net:1920:12)\n at Server.listen (node:net:2008:7)\n at Function.listen (/home/ffxi/discordbot/node_modules/express/lib/application.js:635:24)\n at listen (file:///home/ffxi/discordbot/webserver.js:39:9)\n at EventEmitter.<anonymous> (file:///home/ffxi/discordbot/v2.js:27:2)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)","trace":[{"column":16,"file":"node:net","function":"Server.setupListenHandle [as _listen2]","line":1872,"method":"setupListenHandle [as _listen2]","native":false},{"column":12,"file":"node:net","function":"listenInCluster","line":1920,"method":null,"native":false},{"column":7,"file":"node:net","function":"Server.listen","line":2008,"method":"listen","native":false},{"column":24,"file":"/home/ffxi/discordbot/node_modules/express/lib/application.js","function":"Function.listen","line":635,"method":"listen","native":false},{"column":9,"file":"file:///home/ffxi/discordbot/webserver.js","function":"listen","line":39,"method":null,"native":false},{"column":2,"file":"file:///home/ffxi/discordbot/v2.js","function":null,"line":27,"method":null,"native":false},{"column":5,"file":"node:internal/process/task_queues","function":"process.processTicksAndRejections","line":95,"method":"processTicksAndRejections","native":false}]}
{"date":"Fri Apr 12 2024 20:19:02 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0,0,0],"uptime":476737.36},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":508413,"external":4812774,"heapTotal":30556160,"heapUsed":28088384,"rss":84824064},"pid":185768,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Fri Apr 12 2024 20:39:26 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0,0,0],"uptime":477961.33},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":494042,"external":4798403,"heapTotal":30556160,"heapUsed":27596552,"rss":86097920},"pid":186487,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Fri Apr 12 2024 20:58:19 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.01,0.01,0],"uptime":479093.93},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":535782,"external":4840143,"heapTotal":30031872,"heapUsed":28035832,"rss":85827584},"pid":187277,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Fri Apr 12 2024 21:03:07 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0,0,0],"uptime":479382.33},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":496838,"external":4801199,"heapTotal":30031872,"heapUsed":27744192,"rss":84545536},"pid":187337,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sat Apr 27 2024 18:44:29 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.06,0.1,0.03],"uptime":195741.7},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":467942,"external":4772303,"heapTotal":29507584,"heapUsed":26748240,"rss":83783680},"pid":833943,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sat Apr 27 2024 18:45:20 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.46,0.19,0.07],"uptime":195793.12},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":722517,"external":5026878,"heapTotal":44449792,"heapUsed":29149912,"rss":96395264},"pid":835640,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Fri May 24 2024 03:50:06 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.05,0.06,0.01],"uptime":2474878.9},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":469771,"external":4774132,"heapTotal":29507584,"heapUsed":26633056,"rss":83304448},"pid":2104782,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Wed Jul 24 2024 11:48:01 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.07,0.08,0.02],"uptime":7773953.82},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":476589,"external":4780950,"heapTotal":29507584,"heapUsed":27664792,"rss":71475200},"pid":3534568,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sat Jul 27 2024 07:43:41 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.02,0.13,0.11],"uptime":8018494.26},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":468158,"external":4815485,"heapTotal":31604736,"heapUsed":29202808,"rss":73105408},"pid":427529,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sat Jul 27 2024 15:25:28 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.02,0.02,0],"uptime":8046200.54},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":551330,"external":4898657,"heapTotal":34226176,"heapUsed":32309808,"rss":90439680},"pid":499842,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sat Jul 27 2024 18:28:48 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.19,0.09,0.03],"uptime":8057200.98},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":470333,"external":4817660,"heapTotal":32653312,"heapUsed":30300760,"rss":87105536},"pid":581771,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 03:40:11 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.02,0.05,0.01],"uptime":8090283.99},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":502060,"external":4849387,"heapTotal":33177600,"heapUsed":31766792,"rss":85803008},"pid":710546,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 03:51:14 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.06,0.06,0],"uptime":8090946.51},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":468267,"external":4815594,"heapTotal":31604736,"heapUsed":28886704,"rss":82853888},"pid":747102,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 03:58:34 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.09,0.05,0.01],"uptime":8091386.73},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":469177,"external":4816504,"heapTotal":31080448,"heapUsed":29017064,"rss":86171648},"pid":748538,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 04:14:32 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.29,0.11,0.03],"uptime":8092344.52},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":473825,"external":4821152,"heapTotal":31604736,"heapUsed":29482400,"rss":71237632},"pid":751640,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 04:22:58 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.09,0.08,0.02],"uptime":8092850.89},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":476614,"external":4823941,"heapTotal":31604736,"heapUsed":29060296,"rss":86290432},"pid":754991,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 04:29:17 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.21,0.12,0.04],"uptime":8093229.75},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":468821,"external":4816148,"heapTotal":31866880,"heapUsed":29329184,"rss":85712896},"pid":757723,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 04:33:12 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.16,0.14,0.05],"uptime":8093464.82},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":469694,"external":4817021,"heapTotal":32129024,"heapUsed":28982440,"rss":84611072},"pid":758706,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 05:02:20 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.04,0.08,0.06],"uptime":8095212.77},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":484325,"external":4831652,"heapTotal":32653312,"heapUsed":29939888,"rss":88678400},"pid":763918,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 09:04:49 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.01,0.06,0.02],"uptime":8109762.02},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":633225,"external":4980552,"heapTotal":35536896,"heapUsed":33593640,"rss":85016576},"pid":774312,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 14:16:28 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.25,0.13,0.05],"uptime":8128460.62},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":712588,"external":5059915,"heapTotal":46546944,"heapUsed":31689656,"rss":98271232},"pid":906740,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 17:07:43 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.02,0.04,0],"uptime":8138735.99},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":501258,"external":4848585,"heapTotal":33964032,"heapUsed":32347584,"rss":89595904},"pid":906847,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 18:24:33 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.06,0.03,0],"uptime":8143346.01},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":479983,"external":4827310,"heapTotal":32915456,"heapUsed":29925464,"rss":86872064},"pid":962513,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 18:52:29 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.08,0.03,0],"uptime":8145022.29},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":469823,"external":4817150,"heapTotal":31866880,"heapUsed":29714104,"rss":87707648},"pid":972172,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Sun Jul 28 2024 19:16:27 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.1,0.09,0.03],"uptime":8146460.09},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":518532,"external":4865859,"heapTotal":32915456,"heapUsed":30618584,"rss":89399296},"pid":981469,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Tue Jul 30 2024 15:52:58 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.26,0.12,0.04],"uptime":8307051.46},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":471067,"external":4818394,"heapTotal":32653312,"heapUsed":29960048,"rss":86130688},"pid":1657313,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Wed Jul 31 2024 02:53:02 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.05,0.06,0.08],"uptime":8346655.03},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":567686,"external":4915013,"heapTotal":38682624,"heapUsed":33023920,"rss":68386816},"pid":1682856,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Wed Jul 31 2024 04:16:52 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"write"},"exception":true,"level":"error","message":"uncaughtException: write ECONNRESET\nError: write ECONNRESET\n at afterWriteDispatched (node:internal/stream_base_commons:160:15)\n at writeGeneric (node:internal/stream_base_commons:151:3)\n at Socket._writeGeneric (node:net:952:11)\n at Socket._write (node:net:964:8)\n at writeOrBuffer (node:internal/streams/writable:399:12)\n at _write (node:internal/streams/writable:340:10)\n at Writable.write (node:internal/streams/writable:344:10)\n at writeToClientSocket (file:///home/ffxi/discordbot/v2.js:175:16)\n at Server.<anonymous> (file:///home/ffxi/discordbot/v2.js:161:5)\n at Server.emit (node:events:514:28)","os":{"loadavg":[0.06,0.06,0.04],"uptime":8351685.29},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":406071,"external":4818934,"heapTotal":32653312,"heapUsed":30328648,"rss":86663168},"pid":1881787,"uid":0,"version":"v20.5.1"},"stack":"Error: write ECONNRESET\n at afterWriteDispatched (node:internal/stream_base_commons:160:15)\n at writeGeneric (node:internal/stream_base_commons:151:3)\n at Socket._writeGeneric (node:net:952:11)\n at Socket._write (node:net:964:8)\n at writeOrBuffer (node:internal/streams/writable:399:12)\n at _write (node:internal/streams/writable:340:10)\n at Writable.write (node:internal/streams/writable:344:10)\n at writeToClientSocket (file:///home/ffxi/discordbot/v2.js:175:16)\n at Server.<anonymous> (file:///home/ffxi/discordbot/v2.js:161:5)\n at Server.emit (node:events:514:28)","trace":[{"column":15,"file":"node:internal/stream_base_commons","function":"afterWriteDispatched","line":160,"method":null,"native":false},{"column":3,"file":"node:internal/stream_base_commons","function":"writeGeneric","line":151,"method":null,"native":false},{"column":11,"file":"node:net","function":"Socket._writeGeneric","line":952,"method":"_writeGeneric","native":false},{"column":8,"file":"node:net","function":"Socket._write","line":964,"method":"_write","native":false},{"column":12,"file":"node:internal/streams/writable","function":"writeOrBuffer","line":399,"method":null,"native":false},{"column":10,"file":"node:internal/streams/writable","function":"_write","line":340,"method":null,"native":false},{"column":10,"file":"node:internal/streams/writable","function":"Writable.write","line":344,"method":"write","native":false},{"column":16,"file":"file:///home/ffxi/discordbot/v2.js","function":"writeToClientSocket","line":175,"method":null,"native":false},{"column":5,"file":"file:///home/ffxi/discordbot/v2.js","function":null,"line":161,"method":null,"native":false},{"column":28,"file":"node:events","function":"Server.emit","line":514,"method":"emit","native":false}]}
{"date":"Wed Jul 31 2024 12:56:33 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.03,0.05,0],"uptime":8382865.83},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":516526,"external":4863853,"heapTotal":38420480,"heapUsed":36158136,"rss":70664192},"pid":1902079,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Wed Jul 31 2024 23:00:03 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.05,0.07,0.01],"uptime":8419076.07},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":505161,"external":4852488,"heapTotal":33964032,"heapUsed":32438624,"rss":79953920},"pid":2128002,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 04:18:01 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.03,0.08,0.04],"uptime":8438153.65},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":480469,"external":4827796,"heapTotal":32129024,"heapUsed":29820296,"rss":85983232},"pid":2253837,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 04:36:39 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.08,0.05,0.03],"uptime":8439271.5},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":468772,"external":4816099,"heapTotal":32129024,"heapUsed":29504512,"rss":87814144},"pid":2257396,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 04:38:57 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.07,0.07,0.04],"uptime":8439410.16},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":470239,"external":4817566,"heapTotal":32653312,"heapUsed":29312016,"rss":85970944},"pid":2259537,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 04:53:18 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.06,0.05,0.02],"uptime":8440270.62},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":476888,"external":4824215,"heapTotal":31604736,"heapUsed":29649520,"rss":85463040},"pid":2261908,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 05:14:04 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.01,0.02,0],"uptime":8441516.58},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":468854,"external":4816181,"heapTotal":32653312,"heapUsed":29687416,"rss":86962176},"pid":2263719,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 05:32:24 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0,0.03,0.01],"uptime":8442617.4},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":468506,"external":4815833,"heapTotal":31866880,"heapUsed":29104840,"rss":86831104},"pid":2272510,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 07:04:24 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.04,0.03,0.01],"uptime":8448137.39},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":478088,"external":4825415,"heapTotal":32915456,"heapUsed":30692160,"rss":88076288},"pid":2286975,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 09:52:23 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.07,0.07,0.07],"uptime":8458216.26},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":492710,"external":4840037,"heapTotal":33964032,"heapUsed":31764256,"rss":83718144},"pid":2302189,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 11:04:37 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.03,0.05,0.03],"uptime":8462550.46},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":478642,"external":4825969,"heapTotal":32915456,"heapUsed":30369536,"rss":88555520},"pid":2349695,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 11:15:08 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.1,0.07,0.03],"uptime":8463181.08},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":476687,"external":4824014,"heapTotal":32129024,"heapUsed":29306640,"rss":84676608},"pid":2364232,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Thu Aug 01 2024 11:22:23 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.04,0.1,0.06],"uptime":8463616.32},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":467979,"external":4815306,"heapTotal":32129024,"heapUsed":29089648,"rss":84922368},"pid":2366861,"uid":0,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Fri Aug 02 2024 06:06:33 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"read"},"exception":true,"level":"error","message":"uncaughtException: read ECONNRESET\nError: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","os":{"loadavg":[0.07,0.03,0.04],"uptime":1306.82},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":1004,"memoryUsage":{"arrayBuffers":486532,"external":4833859,"heapTotal":32653312,"heapUsed":30044368,"rss":89743360},"pid":2290,"uid":1004,"version":"v20.5.1"},"stack":"Error: read ECONNRESET\n at TCP.onStreamRead (node:internal/stream_base_commons:217:20)","trace":[{"column":20,"file":"node:internal/stream_base_commons","function":"TCP.onStreamRead","line":217,"method":"onStreamRead","native":false}]}
{"date":"Fri Aug 02 2024 08:06:00 GMT+0000 (Coordinated Universal Time)","error":{"code":"ECONNRESET","errno":-104,"syscall":"write"},"exception":true,"level":"error","message":"uncaughtException: write ECONNRESET\nError: write ECONNRESET\n at afterWriteDispatched (node:internal/stream_base_commons:160:15)\n at writeGeneric (node:internal/stream_base_commons:151:3)\n at Socket._writeGeneric (node:net:952:11)\n at Socket._write (node:net:964:8)\n at writeOrBuffer (node:internal/streams/writable:399:12)\n at _write (node:internal/streams/writable:340:10)\n at Writable.write (node:internal/streams/writable:344:10)\n at writeToClientSocket (file:///home/ffxi/discordbot/v3.js:237:12)\n at Server.<anonymous> (file:///home/ffxi/discordbot/v3.js:216:3)\n at Server.emit (node:events:514:28)","os":{"loadavg":[0,0,0],"uptime":8473.68},"process":{"argv":["/usr/bin/node","/usr/lib/node_modules/pm2/lib/ProcessContainerFork.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":1004,"memoryUsage":{"arrayBuffers":424428,"external":4837291,"heapTotal":33177600,"heapUsed":31130920,"rss":90456064},"pid":18677,"uid":1004,"version":"v20.5.1"},"stack":"Error: write ECONNRESET\n at afterWriteDispatched (node:internal/stream_base_commons:160:15)\n at writeGeneric (node:internal/stream_base_commons:151:3)\n at Socket._writeGeneric (node:net:952:11)\n at Socket._write (node:net:964:8)\n at writeOrBuffer (node:internal/streams/writable:399:12)\n at _write (node:internal/streams/writable:340:10)\n at Writable.write (node:internal/streams/writable:344:10)\n at writeToClientSocket (file:///home/ffxi/discordbot/v3.js:237:12)\n at Server.<anonymous> (file:///home/ffxi/discordbot/v3.js:216:3)\n at Server.emit (node:events:514:28)","trace":[{"column":15,"file":"node:internal/stream_base_commons","function":"afterWriteDispatched","line":160,"method":null,"native":false},{"column":3,"file":"node:internal/stream_base_commons","function":"writeGeneric","line":151,"method":null,"native":false},{"column":11,"file":"node:net","function":"Socket._writeGeneric","line":952,"method":"_writeGeneric","native":false},{"column":8,"file":"node:net","function":"Socket._write","line":964,"method":"_write","native":false},{"column":12,"file":"node:internal/streams/writable","function":"writeOrBuffer","line":399,"method":null,"native":false},{"column":10,"file":"node:internal/streams/writable","function":"_write","line":340,"method":null,"native":false},{"column":10,"file":"node:internal/streams/writable","function":"Writable.write","line":344,"method":"write","native":false},{"column":12,"file":"file:///home/ffxi/discordbot/v3.js","function":"writeToClientSocket","line":237,"method":null,"native":false},{"column":3,"file":"file:///home/ffxi/discordbot/v3.js","function":null,"line":216,"method":null,"native":false},{"column":28,"file":"node:events","function":"Server.emit","line":514,"method":"emit","native":false}]}

View File

@ -0,0 +1,2 @@
{"date":"Thu Apr 04 2024 09:24:58 GMT+0000 (Coordinated Universal Time)","error":{},"level":"error","message":"unhandledRejection: resolve is not defined\nReferenceError: resolve is not defined\n at file:///home/ffxi/discordbot/Utility/userModel.js:43:25\n at Execute.onResult (file:///home/ffxi/discordbot/Utility/db.js:66:41)\n at /home/ffxi/discordbot/node_modules/mysql2/lib/commands/query.js:90:16\n at process.processTicksAndRejections (node:internal/process/task_queues:77:11)","os":{"loadavg":[0.01,0.07,0.03],"uptime":14062459.77},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":1004,"memoryUsage":{"arrayBuffers":320983,"external":4690880,"heapTotal":28721152,"heapUsed":26358312,"rss":84070400},"pid":3323452,"uid":1004,"version":"v20.5.1"},"rejection":true,"stack":"ReferenceError: resolve is not defined\n at file:///home/ffxi/discordbot/Utility/userModel.js:43:25\n at Execute.onResult (file:///home/ffxi/discordbot/Utility/db.js:66:41)\n at /home/ffxi/discordbot/node_modules/mysql2/lib/commands/query.js:90:16\n at process.processTicksAndRejections (node:internal/process/task_queues:77:11)","trace":[{"column":25,"file":"file:///home/ffxi/discordbot/Utility/userModel.js","function":null,"line":43,"method":null,"native":false},{"column":41,"file":"file:///home/ffxi/discordbot/Utility/db.js","function":"Execute.onResult","line":66,"method":"onResult","native":false},{"column":16,"file":"/home/ffxi/discordbot/node_modules/mysql2/lib/commands/query.js","function":null,"line":90,"method":null,"native":false},{"column":11,"file":"node:internal/process/task_queues","function":"process.processTicksAndRejections","line":77,"method":"processTicksAndRejections","native":false}]}
{"date":"Thu Apr 04 2024 18:01:41 GMT+0000 (Coordinated Universal Time)","error":{},"level":"error","message":"unhandledRejection: Cannot read properties of undefined (reading 'webhookTimer')\nTypeError: Cannot read properties of undefined (reading 'webhookTimer')\n at updateMemory (file:///home/ffxi/discordbot/Utility/lsModel.js:180:51)\n at file:///home/ffxi/discordbot/Utility/lsModel.js:165:7\n at Execute.onResult (file:///home/ffxi/discordbot/Utility/db.js:66:41)\n at /home/ffxi/discordbot/node_modules/mysql2/lib/commands/query.js:86:16\n at process.processTicksAndRejections (node:internal/process/task_queues:77:11)","os":{"loadavg":[0,0.05,0.02],"uptime":14093462.31},"process":{"argv":["/usr/bin/node","/home/ffxi/discordbot/v2.js"],"cwd":"/home/ffxi/discordbot","execPath":"/usr/bin/node","gid":0,"memoryUsage":{"arrayBuffers":306613,"external":4676510,"heapTotal":43925504,"heapUsed":25552160,"rss":92241920},"pid":3339975,"uid":0,"version":"v20.5.1"},"rejection":true,"stack":"TypeError: Cannot read properties of undefined (reading 'webhookTimer')\n at updateMemory (file:///home/ffxi/discordbot/Utility/lsModel.js:180:51)\n at file:///home/ffxi/discordbot/Utility/lsModel.js:165:7\n at Execute.onResult (file:///home/ffxi/discordbot/Utility/db.js:66:41)\n at /home/ffxi/discordbot/node_modules/mysql2/lib/commands/query.js:86:16\n at process.processTicksAndRejections (node:internal/process/task_queues:77:11)","trace":[{"column":51,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":"updateMemory","line":180,"method":null,"native":false},{"column":7,"file":"file:///home/ffxi/discordbot/Utility/lsModel.js","function":null,"line":165,"method":null,"native":false},{"column":41,"file":"file:///home/ffxi/discordbot/Utility/db.js","function":"Execute.onResult","line":66,"method":"onResult","native":false},{"column":16,"file":"/home/ffxi/discordbot/node_modules/mysql2/lib/commands/query.js","function":null,"line":86,"method":null,"native":false},{"column":11,"file":"node:internal/process/task_queues","function":"process.processTicksAndRejections","line":77,"method":"processTicksAndRejections","native":false}]}

6651
server/logs/runtimeLog.json Normal file

File diff suppressed because it is too large Load Diff

26
server/package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "ffxidiscord",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"@zip.js/zip.js": "^2.7.41",
"adm-zip": "^0.5.12",
"body-parser": "^1.20.2",
"discord-webhook-node": "^1.1.8",
"discord.js": "^14.13.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.9.3",
"uuid4": "^2.0.3",
"winston": "^3.13.0",
"xml-js": "^1.6.11"
}
}

Binary file not shown.

View File

@ -0,0 +1,25 @@
{
"platforms": {
"1": "se",
"2": "Horizon XI"
},
"se": {
"2":"Undine",
"4":"Bahamut",
"5":"Shiva",
"8":"Phoenix",
"9":"Carbuncle",
"10":"Fenrir",
"11":"Sylph",
"12":"Valefor",
"14":"Leviathan",
"15":"Odin",
"19":"Quetzalcoatl",
"20":"Siren",
"23":"Ragnarok",
"26":"Cerberus",
"28":"Bismarck",
"30":"Lakshmi",
"31":"Asura"
}
}

299
server/v2.js Normal file
View File

@ -0,0 +1,299 @@
import Net from "net";
import "dotenv/config";
import { setup as dbSetup, runPrepQuery, numRows } from "./Utility/db.js";
import { Err, Log, Debug, Warn } from "./Utility/loggerUtility.js";
import crypto from "crypto";
import { botSetup, client } from "./Utility/discordClient.js";
import { event } from "./Utility/eventHandler.js";
import fsConfig from "./config.json" assert { type: "json" };
import { loadModelFromDB, createNewLS } from "./Utility/lsModel.js";
import { listen } from "./webserver.js";
import { getUserFromJwt, setUserSocket, loadUsersFromDB } from "./Utility/userModel.js";
import { getLSModel } from "./Utility/lsModel.js";
import { link, write } from "fs";
const token = fsConfig.token;
Log("Connecting to MySQL DB...");
dbSetup(fsConfig.db);
event.on("DATABASE_CONNECTED", async () => {
Log("Ready!");
await loadUsersFromDB();
await loadModelFromDB();
botSetup();
listen();
});
event.on("MYSQL_FAILED_TO_CONNECT", () => {
Err(
"Failed to launch LinkCloud. Unable to connect to MySQL database. Please check database connection parameters and try again."
);
process.exit();
});
const server = Net.createServer();
const discordPacketBuffer = [];
const lsPacketBuffer = []
const sockets = [];
const discordWebhookQueue = []
const checkTime = (theirTime) => {
const myTime = Math.floor(Date.now() / 1000);
return Number(theirTime - myTime);
};
const hashPacketData = (packet) => {
let base = packet.type;
base += packet.metaData.gameTime;
base += packet.metaData.server;
base += packet.payload.name.replace(/[\n\r]/g, '').trim();
base += packet.payload.message.replace(/[\n\r]/g, '').trim();
base += packet.metaData.platform;
if (packet.payload?.linkshellname) {
base += packet.payload?.linkshellname;
} else if (packet.payload?.area) {
base += packet.payload?.area;
}
return crypto.createHash("md5").update(base).digest("hex");
};
const addToDiscordBuffer = (packet) => {
for (const idx in discordPacketBuffer) {
if (discordPacketBuffer[idx].hash == packet.hash) {
//Debug(`${packet.hash} exists, skipping...`);
return false;
}
}
//Debug(`New hash created ${packet.hash}`)
discordPacketBuffer.push(packet);
if(discordPacketBuffer.length > 1000) {
discordPacketBuffer.shift()
}
};
server.on("connection", (sock) => {
//Debug("CONNECTED: " + sock.remoteAddress + ":" + sock.remotePort);
sock.on("data", async (data) => {
const response = {
error: false,
};
try {
const packets = data.toString().split("\n");
for (const p in packets) {
if (packets[p]) {
const packet = JSON.parse(packets[p]);
const differential = checkTime(packet?.metaData?.clientTime);
let isLsMessage = false;
if (differential <= process.env.MAX_CLOCK_SYNC_MISMATCH_SECONDS) {
response.type = packet?.type?.toUpperCase();
response.packetId = packet?.packetId;
switch (response.type) {
case "HANDSHAKE":
const authed = AuthenticateSocket(packet, sock);
if(!authed) {
response.error = true;
response.errorMsg = "AUTH_FAIL";
response.errorDetails = `You shall not pass!`;
}
response.payload = authed ? "ACCEPTED" : "REJECTED";
response.disconnect = true
Log(`[${sock.remoteAddress}] connection ${response.payload}.`);
break;
case "HEARTBEAT":
response.payload = "PONG";
break;
case "LINKSHELL_MESSAGE":
console.log(packet)
packet.hash = hashPacketData(packet);
ProcessLSMessage(packet, sock)
break;
case "LINKSHELL_UPDATE":
console.log(packet)
ProcessLSUpdate(packet, sock)
break;
case "SHOUT":
packet.hash = hashPacketData(packet);
addToDiscordBuffer(packet);
break;
case "OTHER":
break;
case "ADD_LINKSHELL":
ProcessAddLinkshell(packet, sock);
break;
default:
response.error = true;
response.errorMsg = "UNKNOWN_PACKET_TYPE";
response.errorDetails = `UNKNOWN PACKET TYPE`;
return;
}
} else {
response.error = true;
response.errorMsg = "CLOCK_OUT_OF_SYNC";
response.errorDetails = `This system and the servers clocks are out of sync by ${String(
differential
)} second(s).`;
}
if (sock) writeToClientSocket(sock,JSON.stringify(response) + "\n\r");
if (response.error) {
console.log(data)
console.log(packet)
sock.resetAndDestroy();
}
}
}
//Debug(`TO ${sock.remoteAddress} ${JSON.stringify(response)}`)
} catch (ex) {
Err("Unexpected packet format, unable to parse.");
console.log(ex);
console.log(data.toString());
}
});
sock.on("timeout", (data) => {
removeSocketFromPool(sock)
Debug("TIMEOUT: " + sock.remoteAddress + " " + sock.remotePort);
})
sock.on("close", (data) => {
removeSocketFromPool(sock)
Debug("CLOSED: " + sock.remoteAddress + " " + sock.remotePort);
});
writeToClientSocket(sock,"CHALLENGE\n");
sockets.push(sock);
});
const removeSocketFromPool = (sock) => {
const index = sockets.findIndex(function (o) {
return o.remoteAddress === sock.remoteAddress && o.remotePort === sock.remotePort;
});
if (index !== -1) sockets.splice(index, 1);
}
server.listen(process.env.TCP_LISTEN_PORT, () => {
Log(`Server Bound to ${process.env.BIND_ADDRESS} on port ${process.env.TCP_LISTEN_PORT}.`);
});
const writeToClientSocket = (socket, data, retry = 0) => {
if(!socket?.destroyed && socket?.readyState === 'open') {
socket.write(data, (err) => {
if(err) {
Debug(`Failed to write to socket`)
console.log(err)
}
})
} else {
if(socket?.destroyed) {
Debug(`The socket is dead and cant be written to`)
} else {
if(retry < 5) {
Debug(`Retrying to send packet ${data}`)
setTimeout(() => {writeToClientSocket(socket, data, retry + 1)}, 1000)
}
}
}
}
const ProcessLSMessage = (packet, socket) => {
for (const idx in lsPacketBuffer) {
if (lsPacketBuffer[idx].hash == packet.hash) {
//Debug(`${packet.hash} exists, skipping...`);
return false;
}
}
//Debug(`New hash created ${packet.hash}`)
lsPacketBuffer.push(packet);
if(lsPacketBuffer.length > 1000) {
lsPacketBuffer.shift()
}
const linkshell = ProcessLSUpdate(packet, socket)
if(!linkshell.channels) return false;
const re = /^(\[.+\] .+)/
if(!packet.payload.message.match(re)){
linkshell.webhookQueue.push({
linkshell,
packet
})
}
}
const ProcessLSUpdate = (packet, socket) => {
const linkshell = getLSModel(packet.payload?.linkshellname, packet.metaData.platform, packet.metaData.server);
if(linkshell) {
linkshell.socket = socket
Log(`Linkshell "${packet.payload.linkshellname}" Registered By ${packet.metaData.character}`)
}
return linkshell
}
const sendMessageToClient = (socket, message, error = false) => {
const packet = {};
packet.type = "SYSTEM_MESSAGE";
packet.payload = {};
packet.payload.isError = error;
packet.payload.message = message;
if (socket) writeToClientSocket(socket,JSON.stringify(packet) + "\n\r");
};
const sendLSMessage = (socket, message, from, ls) => {
const packet = {};
packet.type = "LS_ECHO";
packet.payload = {};
packet.payload.from = from.trim();
packet.payload.message = message.trim();
packet.payload.linkshell = ls
if (socket) writeToClientSocket(socket,JSON.stringify(packet) + "\n\r");
}
const AuthenticateSocket = (packet, socket) => {
const authId = packet.payload.authId;
const user = getUserFromJwt(authId);
if (user) {
setUserSocket(socket, user.userId)
return true;
} else {
return false;
}
};
const ProcessAddLinkshell = (packet, socket) => {
const linkId = packet.payload.linkId.replace("\r", "").replace("\n", "");
const serverId = packet.metaData.server;
const lsName = packet.payload.lsName;
const result = runPrepQuery("SELECT * FROM pendinglinks WHERE linkId = ? LIMIT 0,1", [linkId], async (r, f, e) => {
if (numRows(r)) {
const ffxiver = r[0].ffxiver;
const userId = r[0].userId;
const discordUser = client.users.cache.get(userId);
console.log({ linkId, serverId, lsName, ffxiver, userId });
const newLs = await createNewLS(lsName, serverId, ffxiver, userId).catch((e) => {
console.log(e);
sendMessageToClient(socket, "An error occured. Contact Support. [0x10]", true);
});
if (newLs) {
discordUser.send(
`# Success!\nYour Linkshell ${lsName} has been added to LinkCloud!\n\n## What's Next?\n- Set up the chat echo channel in your discord server using the \`/lccreateecho\` command in the channel you want to use for the chat echo.\n- Encourage LS members to use the \`/lcjoin\` command to get started streaming data to LinkCloud. The more streamers you have, the more reliable the echo will be.`
);
Log(`New Linkshell "${lsName}" has been created!`);
sendMessageToClient(
socket,
`Linkshell ${lsName} added successfully to LinkCloud. Check discord for further instruction.`
);
} else {
Err(`Failed to create Linkshell "${lsName}".`);
sendMessageToClient(
socket,
"An error occured. This is most likely because this Linkshell already exists.",
true
);
console.log(e);
discordUser.send(
`# Uh-oh!\nYour Linkshell ${lsName} seems to already exist in our database.\n\n## Need Help?\nYou can reach out to support via discord.\nhttps://discord.gg/n5VYHSQbhA`
);
}
} else {
sendMessageToClient(socket, "An error occured. The supplied token is not valid.", true);
}
});
};
event.on('NEW_DISCORD_ECHO_RECEIVED', message => {
const linkshell = getLSModel(message.lsName, message.platform, message.server)
if(linkshell.socket) {
sendLSMessage(linkshell.socket, message.message, message.from, message.lsName)
}
})

421
server/v3.js Normal file
View File

@ -0,0 +1,421 @@
import Net from "net";
import "dotenv/config";
import { setup as dbSetup, runPrepQuery, numRows } from "./Utility/db.js";
import { Err, Log, Debug, Warn } from "./Utility/loggerUtility.js";
import crypto from "crypto";
import { botSetup, client } from "./Utility/discordClient.js";
import { event } from "./Utility/eventHandler.js";
import fsConfig from "./config.json" assert { type: "json" };
import { loadModelFromDB, createNewLS } from "./Utility/lsModel.js";
import { listen } from "./webserver.js";
import { getUserFromJwt, setUserSocket, loadUsersFromDB } from "./Utility/userModel.js";
import { getLSModel } from "./Utility/lsModel.js";
/**
* Application configuration.
*/
const token = fsConfig.token;
// Log initial message
Log("Connecting to MySQL DB...");
dbSetup(fsConfig.db);
// Event listener for successful database connection
event.on("DATABASE_CONNECTED", async () => {
Log("Ready!");
try {
await loadUsersFromDB();
await loadModelFromDB();
botSetup();
listen();
} catch (error) {
Err("Error during setup after database connection.", error);
}
});
// Event listener for failed database connection
event.on("MYSQL_FAILED_TO_CONNECT", () => {
Err("Failed to launch LinkCloud. Unable to connect to MySQL database. Please check database connection parameters and try again.");
process.exit(1); // Exit with non-zero status for error indication
});
// Global error handlers
process.on("uncaughtException", (error) => {
Err("Uncaught Exception:", error);
//process.exit(1); // Optionally exit with non-zero status
});
process.on("unhandledRejection", (reason, promise) => {
Err("Unhandled Rejection at Promise:", promise, "Reason:", reason);
// Optionally exit process if desired:
// process.exit(1);
});
// Server configuration
const server = Net.createServer();
const discordPacketBuffer = [];
const lsPacketBuffer = [];
const sockets = [];
const MAX_PACKET_BUFFER_SIZE = 1000; // Define constant for buffer size
// Check time difference
const checkTime = (theirTime) => {
const myTime = Math.floor(Date.now() / 1000);
return Number(theirTime - myTime);
};
/**
* Generates a hash value for the given packet data using MD5 algorithm.
* @param {object} packet - The packet data object containing type, metaData, payload, etc.
* @returns {string} - The MD5 hash value of the concatenated packet data.
*/
const hashPacketData = (packet) => {
let base = packet.type;
base += packet.metaData.gameTime;
base += packet.metaData.server;
base += packet.payload.name.replace(/[\n\r]/g, '').trim();
base += packet.payload.message.replace(/[\n\r]/g, '').trim();
base += packet.metaData.platform;
if (packet.payload?.linkshellname) {
base += packet.payload.linkshellname;
} else if (packet.payload?.area) {
base += packet.payload.area;
}
return crypto.createHash("md5").update(base).digest("hex");
};
/**
* Adds a packet to the Discord buffer if it does not already exist in the buffer.
* If the buffer exceeds 1000 packets, the oldest packet is removed.
* @param {Object} packet - The packet to add to the buffer.
* @returns {boolean} - Returns true if packet is added, false if it already exists.
*/
const addToDiscordBuffer = (packet) => {
const exists = discordPacketBuffer.some(p => p.hash === packet.hash);
if (exists) {
Debug(`${packet.hash} exists, skipping...`);
return false;
}
Debug(`New hash created ${packet.hash}`);
discordPacketBuffer.push(packet);
if (discordPacketBuffer.length > MAX_PACKET_BUFFER_SIZE) {
discordPacketBuffer.shift();
}
return true;
};
/**
* Event listener for handling incoming data on a socket connection.
* Parses the incoming data, processes the packets, and sends responses accordingly.
* @param {Socket} sock - The socket object representing the connection.
*/
server.on("connection", (sock) => {
Debug("CONNECTED: " + sock.remoteAddress + ":" + sock.remotePort);
sock.on("data", async (data) => {
const response = { error: false };
try {
const packets = data.toString().split("\n");
for (const packetStr of packets) {
if (packetStr) {
const packet = JSON.parse(packetStr);
const differential = checkTime(packet?.metaData?.clientTime);
if (differential <= process.env.MAX_CLOCK_SYNC_MISMATCH_SECONDS) {
response.type = packet?.type?.toUpperCase();
response.packetId = packet?.packetId;
switch (response.type) {
case "HANDSHAKE":
const authed = AuthenticateSocket(packet, sock);
response.payload = authed ? "ACCEPTED" : "REJECTED";
response.error = !authed;
response.errorMsg = "AUTH_FAIL";
response.errorDetails = "You shall not pass!";
response.disconnect = true;
Log(`[${sock.remoteAddress}] connection ${response.payload}.`);
break;
case "HEARTBEAT":
response.payload = "PONG";
break;
case "LINKSHELL_MESSAGE":
packet.hash = hashPacketData(packet);
Log(`[${sock.remoteAddress}] LS_MESSAGE`)
ProcessLSMessage(packet, sock);
break;
case "LINKSHELL_UPDATE":
ProcessLSUpdate(packet, sock);
Log(`[${sock.remoteAddress}] LS_UPDATE`)
break;
case "SHOUT":
packet.hash = hashPacketData(packet);
addToDiscordBuffer(packet);
break;
case "OTHER":
// Handle other types
break;
case "ADD_LINKSHELL":
Log(`[${sock.remoteAddress}] ADD_LS`)
await ProcessAddLinkshell(packet, sock);
break;
default:
response.error = true;
response.errorMsg = "UNKNOWN_PACKET_TYPE";
response.errorDetails = "UNKNOWN PACKET TYPE";
break;
}
} else {
response.error = true;
response.errorMsg = "CLOCK_OUT_OF_SYNC";
response.errorDetails = `This system and the server clocks are out of sync by ${differential} second(s).`;
}
if (sock) {
writeToClientSocket(sock, JSON.stringify(response) + "\n\r");
}
if (response.error) {
Debug("Error processing packet", packet);
sock.destroy(); // Use destroy instead of resetAndDestroy
}
}
}
// Trace (`trace` level logging is not natively supported)
//Debug(`TRACE: TO ${sock.remoteAddress} ${JSON.stringify(response)}`);
} catch (ex) {
Err("Unexpected packet format, unable to parse.");
Debug(ex);
Debug("Data:", data.toString());
}
});
sock.on("timeout", () => {
removeSocketFromPool(sock);
Debug("TIMEOUT: " + sock.remoteAddress + " " + sock.remotePort);
});
sock.on("close", () => {
removeSocketFromPool(sock);
Debug("CLOSED: " + sock.remoteAddress + " " + sock.remotePort);
});
writeToClientSocket(sock, "CHALLENGE\n");
sockets.push(sock);
});
/**
* Removes a socket from the pool.
* @param {Socket} sock - The socket to remove.
*/
const removeSocketFromPool = (sock) => {
const index = sockets.findIndex((o) => o.remoteAddress === sock.remoteAddress && o.remotePort === sock.remotePort);
if (index !== -1) sockets.splice(index, 1);
};
/**
* Writes data to a client socket with retries.
* @param {Socket} socket - The socket to write to.
* @param {string} data - The data to write.
* @param {number} [retry=0] - The retry count.
*/
const writeToClientSocket = (socket, data, retry = 0) => {
if (!socket?.destroyed && socket?.readyState === 'open') {
socket.write(data, (err) => {
if (err) {
Debug("Failed to write to socket", err);
}
});
} else {
if (socket?.destroyed) {
Debug("The socket is dead and can't be written to");
} else if (retry < 5) {
Debug(`Retrying to send packet ${data}`);
setTimeout(() => writeToClientSocket(socket, data, retry + 1), 1000);
}
}
};
/**
* Processes a linkshell message packet.
* @param {object} packet - The packet data.
* @param {Socket} socket - The socket connection.
* @returns {boolean} - Returns false if packet exists, true otherwise.
*/
const ProcessLSMessage = (packet, socket) => {
const exists = lsPacketBuffer.some(p => p.hash === packet.hash);
if (exists) {
Debug(`${packet.hash} exists, skipping...`);
return false;
}
Debug(`New hash created ${packet.hash}`);
lsPacketBuffer.push(packet);
if (lsPacketBuffer.length > MAX_PACKET_BUFFER_SIZE) {
lsPacketBuffer.shift();
}
const linkshell = ProcessLSUpdate(packet, socket);
if (!linkshell?.channels) return false;
const re = /^(\[.+\] .+)/;
if (!packet.payload.message.match(re)) {
linkshell.webhookQueue.push({
linkshell,
packet,
});
}
return true;
};
/**
* Processes a linkshell update packet.
* @param {object} packet - The packet data.
* @param {Socket} socket - The socket connection.
* @returns {object} - Returns the linkshell object.
*/
const ProcessLSUpdate = (packet, socket) => {
const linkshell = getLSModel(packet.payload?.linkshellname, packet.metaData.platform, packet.metaData.server);
if (linkshell) {
linkshell.socket = socket;
Log(`Linkshell "${packet.payload.linkshellname}" Registered By ${packet.metaData.character}`);
}
return linkshell;
};
/**
* Sends a system message to the client.
* @param {Socket} socket - The client socket.
* @param {string} message - The message to send.
* @param {boolean} [error=false] - Whether the message is an error.
*/
const sendMessageToClient = (socket, message, error = false) => {
const packet = {
type: "SYSTEM_MESSAGE",
payload: {
isError: error,
message,
},
};
if (socket) {
writeToClientSocket(socket, JSON.stringify(packet) + "\n\r");
}
};
/**
* Sends a linkshell echo message to the client.
* @param {Socket} socket - The client socket.
* @param {string} message - The message to send.
* @param {string} from - The sender of the message.
* @param {string} ls - The linkshell name.
*/
const sendLSMessage = (socket, message, from, ls) => {
const packet = {
type: "LS_ECHO",
payload: {
from: from.trim(),
message: message.trim(),
linkshell: ls,
},
};
if (socket) {
writeToClientSocket(socket, JSON.stringify(packet) + "\n\r");
}
};
/**
* Authenticates a socket connection.
* @param {object} packet - The packet data containing authentication info.
* @param {Socket} socket - The socket connection.
* @returns {boolean} - Returns true if authentication is successful, false otherwise.
*/
const AuthenticateSocket = (packet, socket) => {
const authId = packet.payload.authId;
const user = getUserFromJwt(authId);
if (user) {
setUserSocket(socket, user.userId);
return true;
} else {
return false;
}
};
/**
* Processes a request to add a linkshell.
* @param {object} packet - The packet data.
* @param {Socket} socket - The socket connection.
*/
const ProcessAddLinkshell = async (packet, socket) => {
const linkId = packet.payload.linkId.replace("\r", "").replace("\n", "");
const serverId = packet.metaData.server;
const lsName = packet.payload.lsName;
try {
const result = await runPrepQuery("SELECT * FROM pendinglinks WHERE linkId = ? LIMIT 0,1", [linkId]);
if (numRows(result)) {
const { ffxiver, userId } = result[0];
const discordUser = client.users.cache.get(userId);
Debug({ linkId, serverId, lsName, ffxiver, userId });
try {
const newLs = await createNewLS(lsName, serverId, ffxiver, userId);
discordUser.send(
`# Success!\nYour Linkshell ${lsName} has been added to LinkCloud!\n\n## What's Next?\n- Set up the chat echo channel in your discord server using the \`/lccreateecho\` command in the channel you want to use for the chat echo.\n- Encourage LS members to use the \`/lcjoin\` command to get started streaming data to LinkCloud. The more streamers you have, the more reliable the echo will be.`
);
Log(`New Linkshell "${lsName}" has been created!`);
sendMessageToClient(socket, `Linkshell ${lsName} added successfully to LinkCloud. Check discord for further instruction.`);
} catch (error) {
Err(`Failed to create Linkshell "${lsName}".`, error);
sendMessageToClient(socket, "An error occurred. This is most likely because this Linkshell already exists.", true);
discordUser.send(
`# Uh-oh!\nYour Linkshell ${lsName} seems to already exist in our database.\n\n## Need Help?\nYou can reach out to support via discord.\nhttps://discord.gg/n5VYHSQbhA`
);
}
} else {
sendMessageToClient(socket, "An error occurred. The supplied token is not valid.", true);
}
} catch (error) {
Err("Database query failed during ProcessAddLinkshell.", error);
sendMessageToClient(socket, "An error occurred during linkshell processing. Please try again later.", true);
}
};
/**
* Event listener for new Discord echo received.
*/
event.on('NEW_DISCORD_ECHO_RECEIVED', (message) => {
const linkshell = getLSModel(message.lsName, message.platform, message.server);
Log(`[${message.lsName} : ${message.server}] NEW_DISCORD_MESSAGE`)
if (linkshell?.socket) {
sendLSMessage(linkshell.socket, message.message, message.from, message.lsName);
}
});
// Server listening
server.listen(process.env.TCP_LISTEN_PORT, () => {
Log(`Server Bound to ${process.env.BIND_ADDRESS} on port ${process.env.TCP_LISTEN_PORT}.`);
});

57
server/webserver.js Normal file
View File

@ -0,0 +1,57 @@
import express from 'express'
import AdmZip from 'adm-zip'
import { runPrepQuery, numRows } from './Utility/db.js'
const app = express()
const port = 3000
import { json2xml } from "xml-js";
import { BlobReader, BlobWriter, TextReader, TextWriter, ZipReader, ZipWriter } from "@zip.js/zip.js";
const saveFile = {
settings: {
global: {
AuthKey: "NONE",
AutoConnect: true,
AutoReconnect: true,
HostAddress: "linkcloud.drunken.games",
HostPort: 5050,
ProcessShouts: true,
TCPTimeout: 2,
},
},
};
const options = {
compact: true,
ignoreComment: true,
spaces: 4,
};
app.get('/addons/windower/retail/:id', async (req, res) => {
saveFile.settings.global.AuthKey = await getTokenFromId(req.params.id);
const xmlString = `<?xml version="1.1" ?>\n${json2xml(saveFile, options)}`;
const existingZip = new AdmZip('./resources/addon/LinkCloud.zip');
existingZip.addFile("LinkCloud/data/settings.xml", Buffer.from(xmlString, "utf8"), "Auto Generated");
const data = existingZip.toBuffer()
res.set('Content-Type', 'application/zip');
res.set('Content-Disposition', 'attachment; filename=LinkCloud_Latest.zip');
res.set('Content-Length', data.length);
res.send(data);
})
export const listen = () => {
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
}
const getTokenFromId = (id) => {
return new Promise((resolve, reject) => {
runPrepQuery("SELECT * FROM users WHERE userId = ? LIMIT 0,1", [id], (r,f,e) => {
if(!e) {
if(numRows(r)) {
resolve(r[0].authToken)
} else {
resolve(false)
}
} else {
resolve(false)
}
})
})
}