419 lines
11 KiB
Lua
419 lines
11 KiB
Lua
|
---------------------------------------------------
|
|||
|
-- CACHE STORAGE
|
|||
|
---------------------------------------------------
|
|||
|
|
|||
|
cats.currentTickets = cats.currentTickets or {}
|
|||
|
cats.adminDataCache = cats.adminDataCache or {}
|
|||
|
cats.lastNotifiedTickets = cats.lastNotifiedTickets or {}
|
|||
|
|
|||
|
-- build receivers list
|
|||
|
local function getAdmins(steamID)
|
|||
|
|
|||
|
local admins = {}
|
|||
|
for i, ply in ipairs(player.GetAll()) do
|
|||
|
if cats.config.playerCanSeeTicket(ply, steamID) then table.insert(admins, ply) end
|
|||
|
end
|
|||
|
|
|||
|
return admins
|
|||
|
|
|||
|
end
|
|||
|
|
|||
|
---------------------------------------------------
|
|||
|
-- BASE CODE
|
|||
|
---------------------------------------------------
|
|||
|
|
|||
|
-- create api interface
|
|||
|
|
|||
|
cats.api = octolib.api({
|
|||
|
url = 'https://octothorp.team/cats/api',
|
|||
|
headers = { ['Authorization'] = CFG.keys.cats },
|
|||
|
})
|
|||
|
|
|||
|
local ignoreMsgsTime = {}
|
|||
|
|
|||
|
-- log in console
|
|||
|
function cats:Log(text)
|
|||
|
|
|||
|
print('[# CATS] ' .. os.date('%H:%M:%S - ', os.time()) .. text)
|
|||
|
|
|||
|
end
|
|||
|
|
|||
|
local discordMaxEmbedFields = 25
|
|||
|
cats.NotifyDiscord = octolib.func.debounceStart(function(self)
|
|||
|
if table.IsEmpty(self.currentTickets) then return end
|
|||
|
|
|||
|
local admins = octolib.array.toKeys(getAdmins(), 'Не занят')
|
|||
|
local adminDescriptions = table.Copy(admins)
|
|||
|
|
|||
|
-- remove busy admins
|
|||
|
for _, ticket in pairs(self.currentTickets) do
|
|||
|
if IsValid(ticket.admin) then
|
|||
|
admins[ticket.admin] = nil
|
|||
|
adminDescriptions[ticket.admin] = 'Занят жалобой ' .. ticket.userID
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
-- remove afk admins
|
|||
|
for admin in pairs(admins) do
|
|||
|
if admin:GetAFKTime() > CFG.afkAdminNotActive then
|
|||
|
admins[admin] = nil
|
|||
|
adminDescriptions[admin] = 'AFK'
|
|||
|
end
|
|||
|
|
|||
|
if not self.config.actualAdminRanks[admin:GetUserGroup()] then
|
|||
|
admins[admin] = nil
|
|||
|
adminDescriptions[admin] = 'Ранг не обязывает'
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
-- count active tickets
|
|||
|
local notClaimed = {}
|
|||
|
for userID, ticket in pairs(self.currentTickets) do
|
|||
|
if not IsValid(ticket.admin) then
|
|||
|
notClaimed[#notClaimed + 1] = userID
|
|||
|
end
|
|||
|
end
|
|||
|
if not self.config.iDiffers(notClaimed, self.lastNotifiedTickets) then return end
|
|||
|
self.lastNotifiedTickets = notClaimed
|
|||
|
if not notClaimed[1] then return end
|
|||
|
|
|||
|
for _, userID in ipairs(notClaimed) do
|
|||
|
local user = player.GetBySteamID(userID)
|
|||
|
if IsValid(user) then
|
|||
|
self.config.notify(user, self.lang.ticket_noAdmins)
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
local webhook = CFG.webhooks.cats
|
|||
|
if not webhook then return end
|
|||
|
|
|||
|
local fields = octolib.table.mapSequential(notClaimed, function(sid)
|
|||
|
return {
|
|||
|
name = 'https://octothorp.team/cats/' .. sid,
|
|||
|
value = self.currentTickets[sid].chatLog[1][2],
|
|||
|
}
|
|||
|
end)
|
|||
|
if #fields+1 > discordMaxEmbedFields then
|
|||
|
local missing = #fields - (discordMaxEmbedFields-3)
|
|||
|
fields = octolib.array.page(fields, discordMaxEmbedFields - 3)
|
|||
|
fields[#fields + 1] = {
|
|||
|
value = ('+ Еще %d %s'):format(missing, octolib.string.formatCount(missing, 'жалоба', 'жалобы', 'жалоб')),
|
|||
|
}
|
|||
|
end
|
|||
|
|
|||
|
fields[#fields + 1] = { name = '', value = '' } -- divider
|
|||
|
fields[#fields + 1] = {
|
|||
|
name = 'Админы на сервере',
|
|||
|
value = next(adminDescriptions) == nil and 'Никого' or table.concat(octolib.table.mapSequential(adminDescriptions, function(status, admin)
|
|||
|
return admin:SteamName() .. ' – ' .. status
|
|||
|
end), '\n'),
|
|||
|
}
|
|||
|
|
|||
|
local embed = {
|
|||
|
-- author = {
|
|||
|
-- name = GetHostName(),
|
|||
|
-- url = 'https://octothorp.team/join-' .. CFG.serverID,
|
|||
|
-- },
|
|||
|
title = GetHostName(),
|
|||
|
description = ('**%d** %s, но активных админов нет'):format(#notClaimed, octolib.string.formatCount(#notClaimed, 'открытая жалоба', 'открытые жалобы', 'открытых жалоб')),
|
|||
|
fields = fields,
|
|||
|
thumbnail = { url = ('https://img.icons8.com/color/184/%d.png'):format(#notClaimed) },
|
|||
|
}
|
|||
|
|
|||
|
if #notClaimed > 8 then
|
|||
|
embed.color = 0xA52019
|
|||
|
if #notClaimed > 10 then
|
|||
|
embed.footer = {
|
|||
|
text = 'Это какой-то апокалипсис! Пожалуйста, срочно зайдите!',
|
|||
|
icon_url = 'https://img.icons8.com/color/48/skull.png',
|
|||
|
}
|
|||
|
end
|
|||
|
elseif #notClaimed > 4 then
|
|||
|
embed.color = 0xFF4F00
|
|||
|
elseif #notClaimed > 2 then
|
|||
|
embed.color = 0xE5BE01
|
|||
|
end
|
|||
|
|
|||
|
octoservices:post('/discord/webhook/' .. webhook, {
|
|||
|
content = #notClaimed > 5 and CFG.adminMention or nil,
|
|||
|
embeds = { embed },
|
|||
|
})
|
|||
|
|
|||
|
if not table.IsEmpty(admins) then
|
|||
|
RunConsoleCommand('sg', 'a', ('Пожалуйста, разберите %d %s или позовите других админов на помощь'):format(#notClaimed, octolib.string.formatCount(#notClaimed, 'жалобу', 'жалобы', 'жалоб')))
|
|||
|
end
|
|||
|
|
|||
|
end, cats.config.notificationDelay)
|
|||
|
|
|||
|
timer.Create('cats.oldTicketsChecker', 60, 0, function()
|
|||
|
local currentTime = os.time()
|
|||
|
local callAdmins = false
|
|||
|
for userID, ticket in pairs(cats.currentTickets) do
|
|||
|
if currentTime - ticket.createdTime > cats.config.oldTicketTrigger and not IsValid(ticket.admin) then
|
|||
|
callAdmins = true
|
|||
|
break
|
|||
|
end
|
|||
|
end
|
|||
|
if callAdmins then cats:NotifyDiscord() end
|
|||
|
end)
|
|||
|
|
|||
|
-- process & broadcast new ticket message
|
|||
|
function cats:DispatchMessage(sender, steamID, msg, sendToAPI)
|
|||
|
|
|||
|
local ply = player.GetBySteamID(sender)
|
|||
|
if msg == '' then
|
|||
|
ply:Notify('warning', 'Укажи текст админ-запроса')
|
|||
|
return
|
|||
|
end
|
|||
|
local admins = getAdmins(steamID)
|
|||
|
|
|||
|
if self.currentTickets[steamID] then
|
|||
|
-- ticket exists, append chat message
|
|||
|
table.insert(self.currentTickets[steamID].chatLog, {sender, msg, sender ~= steamID})
|
|||
|
|
|||
|
if sendToAPI then
|
|||
|
self.api:post('/msg/' .. steamID, {
|
|||
|
owner = sender,
|
|||
|
text = msg,
|
|||
|
}):Then(function(res)
|
|||
|
local msg = res.data
|
|||
|
if msg and msg.time then
|
|||
|
ignoreMsgsTime[msg.time] = true
|
|||
|
end
|
|||
|
end)
|
|||
|
end
|
|||
|
else
|
|||
|
-- create new ticket
|
|||
|
self.currentTickets[steamID] = {
|
|||
|
createdTime = os.time(),
|
|||
|
createdGameTime = CurTime(),
|
|||
|
chatLog = {{sender, msg}},
|
|||
|
userID = steamID
|
|||
|
}
|
|||
|
|
|||
|
if sendToAPI then
|
|||
|
self.api:post('/chat', {
|
|||
|
owner = steamID,
|
|||
|
text = msg,
|
|||
|
}):Then(function(res)
|
|||
|
local ticket = res.data
|
|||
|
if ticket and ticket.created then
|
|||
|
ignoreMsgsTime[ticket.created] = true
|
|||
|
end
|
|||
|
end)
|
|||
|
end
|
|||
|
|
|||
|
hook.Run('cats.created', sender)
|
|||
|
end
|
|||
|
|
|||
|
hook.Run('cats.message', sender, steamID, msg)
|
|||
|
|
|||
|
-- do networking
|
|||
|
netstream.Start(admins, 'cats.dispatchMessage', steamID, sender, msg)
|
|||
|
|
|||
|
end
|
|||
|
netstream.Hook('cats.dispatchMessage', function(ply, steamID, msg)
|
|||
|
|
|||
|
local user = player.GetBySteamID(steamID)
|
|||
|
|
|||
|
-- check if user exists
|
|||
|
if not IsValid(user) then
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_playerNotFound)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
-- check access
|
|||
|
if not cats.config.playerCanSeeTicket(ply, steamID) then
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_noAccess)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
-- dispatch
|
|||
|
cats:DispatchMessage(ply:SteamID(), steamID, msg, true)
|
|||
|
|
|||
|
end)
|
|||
|
|
|||
|
netstream.Hook('cats.closeTicket', function(ply, steamID)
|
|||
|
|
|||
|
local ticket = cats.currentTickets[steamID]
|
|||
|
|
|||
|
-- ticket doesn't exist
|
|||
|
if not ticket then
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_ticketNotFound)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
-- ticket already ended
|
|||
|
if ticket.ended then
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_ticketEnded)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
if ticket.adminID == ply:SteamID() then
|
|||
|
-- admin closed the ticket
|
|||
|
local user = player.GetBySteamID(steamID)
|
|||
|
if IsValid(user) then
|
|||
|
cats.config.notify(user, cats.lang.ticketClosedBy:format(ticket.admin:Name()))
|
|||
|
end
|
|||
|
cats.config.notify(ply, cats.lang.ticketClosed)
|
|||
|
elseif ticket.userID == ply:SteamID() and not IsValid(ticket.admin) then
|
|||
|
-- user cancelled the ticket
|
|||
|
else
|
|||
|
-- we don't have access to this
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_noAccess)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
cats:CloseTicket(steamID)
|
|||
|
hook.Run('cats.closed', ply:SteamID(), steamID)
|
|||
|
|
|||
|
end)
|
|||
|
|
|||
|
-- close ticket
|
|||
|
function cats:CloseTicket(steamID)
|
|||
|
|
|||
|
-- notify clients
|
|||
|
netstream.Start(getAdmins(steamID), 'cats.closeTicket', steamID)
|
|||
|
|
|||
|
self.currentTickets[steamID] = nil
|
|||
|
self.api:delete('/chat/' .. steamID)
|
|||
|
|
|||
|
end
|
|||
|
|
|||
|
-- claim/unclaim ticket
|
|||
|
function cats:ClaimTicket(steamID, ply, doClaim)
|
|||
|
|
|||
|
local ticket = self.currentTickets[steamID]
|
|||
|
|
|||
|
-- ticket doesn't exist
|
|||
|
if not ticket then
|
|||
|
self:Log('Trying to claim inexistant ticket for ' .. steamID)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
-- check access
|
|||
|
if IsValid(ticket.admin) and ticket.adminID ~= ply:SteamID() then
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_noAccess)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
if doClaim then
|
|||
|
-- claim ticket
|
|||
|
ticket.admin = ply
|
|||
|
ticket.adminID = ply:SteamID()
|
|||
|
ticket.claimTime = os.time()
|
|||
|
else
|
|||
|
-- unclaim ticket
|
|||
|
ticket.admin = nil
|
|||
|
ticket.adminID = nil
|
|||
|
end
|
|||
|
|
|||
|
hook.Run('cats.claim', ply, steamID, doClaim)
|
|||
|
netstream.Start(getAdmins(steamID), 'cats.claimTicket', steamID, ply, doClaim)
|
|||
|
|
|||
|
end
|
|||
|
netstream.Hook('cats.claimTicket', function(ply, steamID, doClaim)
|
|||
|
|
|||
|
local ticket = cats.currentTickets[steamID]
|
|||
|
|
|||
|
-- check access
|
|||
|
if not cats.config.playerCanSeeTicket(ply, steamID) or ply:SteamID() == steamID then
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_noAccess)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
-- ticket doesn't exist
|
|||
|
if not ticket then
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_ticketNotFound)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
local user = player.GetBySteamID(steamID)
|
|||
|
if doClaim then
|
|||
|
-- ticket already claimed
|
|||
|
if IsValid(ticket.admin) then
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_ticketAlreadyClaimed)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
-- notify about claim
|
|||
|
cats.config.notify(ply, cats.lang.ticketClaimed)
|
|||
|
cats.config.notify(user, cats.lang.ticketClaimedBy:format(ply:Name()))
|
|||
|
else
|
|||
|
-- ticket not claimed yet
|
|||
|
if not IsValid(ticket.admin) then
|
|||
|
cats.config.notify(ply, 'warning', cats.lang.error_ticketNotClaimed)
|
|||
|
return
|
|||
|
end
|
|||
|
|
|||
|
-- notify about unclaim
|
|||
|
cats.config.notify(ply, cats.lang.ticketUnclaimed)
|
|||
|
cats.config.notify(user, cats.lang.ticketUnclaimedBy:format(ply:Name()))
|
|||
|
end
|
|||
|
|
|||
|
-- do the thing
|
|||
|
cats:ClaimTicket(steamID, ply, doClaim)
|
|||
|
|
|||
|
end)
|
|||
|
|
|||
|
---------------------------------------------------
|
|||
|
-- GAMEMODE HOOKS
|
|||
|
---------------------------------------------------
|
|||
|
|
|||
|
-- initiate tickets
|
|||
|
hook.Add('PlayerSay', 'cats', function(ply, text)
|
|||
|
|
|||
|
local shouldTrigger, msg = cats.config.triggerText(ply, text)
|
|||
|
if shouldTrigger then
|
|||
|
cats:DispatchMessage(ply:SteamID(), ply:SteamID(), msg, true)
|
|||
|
return ''
|
|||
|
end
|
|||
|
|
|||
|
end)
|
|||
|
|
|||
|
-- deal with disconnected players
|
|||
|
hook.Add('PlayerDisconnected', 'cats', function(ply)
|
|||
|
|
|||
|
for steamID, ticket in pairs(cats.currentTickets) do
|
|||
|
if steamID == ply:SteamID() then
|
|||
|
-- notify admin
|
|||
|
if IsValid(ticket.admin) then
|
|||
|
cats.config.notify(ticket.admin, 'warning', cats.lang.ticketUserLeft)
|
|||
|
end
|
|||
|
|
|||
|
-- delete ticket
|
|||
|
cats:CloseTicket(steamID)
|
|||
|
elseif ticket.adminID == ply:SteamID() then
|
|||
|
-- unclaim ticket
|
|||
|
cats:ClaimTicket(steamID, ply, false)
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
end)
|
|||
|
|
|||
|
hook.Add('PlayerFinishedLoading', 'cats', function(ply)
|
|||
|
|
|||
|
if cats.config.playerCanSeeTicket(ply, '') then
|
|||
|
netstream.Start(ply, 'cats.syncTickets', cats.currentTickets)
|
|||
|
end
|
|||
|
|
|||
|
end)
|
|||
|
|
|||
|
local lastMsgTime = 0
|
|||
|
timer.Create('cats.checkUpdates', 5, 0, function()
|
|||
|
|
|||
|
cats.api:get('/util/updates/' .. lastMsgTime):Then(function(res)
|
|||
|
lastMsgTime = res.data[1]
|
|||
|
for steamID, msgs in pairs(res.data[2]) do
|
|||
|
for i, msg in ipairs(msgs) do
|
|||
|
if not ignoreMsgsTime[msg.time] then
|
|||
|
cats:DispatchMessage(msg.owner, steamID, msg.text)
|
|||
|
else
|
|||
|
ignoreMsgsTime[msg.time] = nil
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
end)
|
|||
|
|
|||
|
end)
|