Feature/larynx (#40)

This commit is contained in:
Brandon Sturgeon 2023-09-20 18:51:30 -07:00 committed by GitHub
parent 8d142056b9
commit bcd5f73e9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 981 additions and 22 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.env
.idea

View File

@ -0,0 +1 @@
return include("cfc_chat_transit/client/init.lua")

View File

@ -0,0 +1,6 @@
require("steamlookup")
include("cfc_chat_transit/server/init.lua")
include("cfc_chat_transit/server/remote_messages.lua")
AddCSLuaFile("cfc_chat_transit/client/init.lua")
AddCSLuaFile("cfc_chat_transit/client/menu.lua")
return AddCSLuaFile("cfc_chat_transit/client/receive_remote_message.lua")

View File

@ -0,0 +1,2 @@
ChatTransit = { }
return include("receive_remote_message.lua")

View File

@ -0,0 +1,31 @@
local alertPreference
alertPreference = function(val)
net.Start("CFC_ChatTransit_RemoteMessagePreference")
net.WriteBool(val)
return net.SendToServer()
end
local initHookName = "CFC_ChatTransit_AlertRemoteMessagePreference"
hook.Add("Think", initHookName, function()
hook.Remove("Think", initHookName)
alertPreference(ChatTransit.shouldReceiveRemoteMessages:GetBool())
return nil
end)
local populatePanel
populatePanel = function(panel)
local label = "Should show remote messages"
do
local _with_0 = panel:CheckBox(label, "cfc_chat_transit_remote_messages")
_with_0.OnChange = function(_, val)
return ChatTransit.alertPreference(val)
end
return _with_0
end
end
hook.Add("AddToolMenuCategories", "CFC_ChatTransit_MenuCategory", function()
return AddToolCategory("Options", "CFC", "CFC")
end)
return hook.Add("PopulateToolMenu", "CFC_ChatTransit_MenuOption", function()
return AddToolMenuOption("Options", "CFC", "should_receive_remote_messages", "Remote Messages", "", "", function(panel)
return populatePanel(panel)
end)
end)

View File

@ -0,0 +1,73 @@
local Start, Receive, ReadBool, ReadColor, ReadString, WriteBool, SendToServer
do
local _obj_0 = net
Start, Receive, ReadBool, ReadColor, ReadString, WriteBool, SendToServer = _obj_0.Start, _obj_0.Receive, _obj_0.ReadBool, _obj_0.ReadColor, _obj_0.ReadString, _obj_0.WriteBool, _obj_0.SendToServer
end
local AddToolCategory, AddToolMenuOption
do
local _obj_0 = spawnmenu
AddToolCategory, AddToolMenuOption = _obj_0.AddToolCategory, _obj_0.AddToolMenuOption
end
local shouldReceiveRemoteMessages = CreateConVar("cfc_chat_transit_remote_messages", 1, FCVAR_ARCHIVE, "Should receive remote messges in chat", 0, 1)
local colors = {
white = Color(255, 255, 255),
blurple = Color(142, 163, 247)
}
Receive("CFC_ChatTransit_RemoteMessageReceive", function()
if not (shouldReceiveRemoteMessages:GetBool()) then
return
end
local author = ReadString()
local authorColor = ReadColor()
local message = ReadString()
if not (author) then
return
end
if not (authorColor) then
return
end
if not (message) then
return
end
local addTextParams = {
colors.blurple,
"[Discord] ",
authorColor,
author,
colors.white,
": " .. tostring(message)
}
hook.Run("CFC_ChatTransit_RemoteMessageReceive", addTextParams)
return chat.AddText(unpack(addTextParams))
end)
local alertPreference
alertPreference = function(val)
Start("CFC_ChatTransit_RemoteMessagePreference")
WriteBool(val)
return SendToServer()
end
local initHookName = "CFC_ChatTransit_AlertRemoteMessagePreference"
hook.Add("Think", initHookName, function()
hook.Remove("Think", initHookName)
alertPreference(shouldReceiveRemoteMessages:GetBool())
return nil
end)
local populatePanel
populatePanel = function(panel)
local label = "Should show remote messages"
do
local _with_0 = panel:CheckBox(label, "cfc_chat_transit_remote_messages")
_with_0.OnChange = function(_, val)
return alertPreference(val)
end
return _with_0
end
end
hook.Add("AddToolMenuCategories", "CFC_ChatTransit_MenuCategory", function()
return AddToolCategory("Options", "CFC", "CFC")
end)
return hook.Add("PopulateToolMenu", "CFC_ChatTransit_MenuOption", function()
return AddToolMenuOption("Options", "CFC", "should_receive_remote_messages", "Remote Messages", "", "", function(panel)
return populatePanel(panel)
end)
end)

View File

@ -0,0 +1,90 @@
local TableToJSON
TableToJSON = util.TableToJSON
local HTTP = HTTP
local avatarServiceAddress = CreateConVar("cfc_avatar_service_address", "", FCVAR_ARCHIVE + FCVAR_PROTECTED)
local AvatarService
do
local _class_0
local _base_0 = {
getAvatar = function(self, steamID64)
local url = steamID64 and "https://avatarservice.cfcservers.org/avatars/" .. tostring(steamID64) .. ".png" or nil
if self.processedIds[steamID64] then
url = url and tostring(url) .. "?processed=true"
end
return url
end,
processAvatar = function(self, avatarUrl, outlineColor, steamID64)
local body = TableToJSON({
avatarUrl = avatarUrl,
outlineColor = outlineColor,
steamID = steamID64
})
self.logger:debug("Sending data to outliner: ", body)
local failed
do
local _base_1 = self.logger
local _fn_0 = _base_1.error
failed = function(...)
return _fn_0(_base_1, ...)
end
end
local success
success = function(code, body)
self.logger:debug("Avatar request succeeded with code: " .. tostring(code) .. " | Body: " .. tostring(body))
self.processedIds[steamID64] = true
end
return HTTP({
success = success,
failed = failed,
body = body,
url = self.outlinerUrl,
method = "POST",
type = "application/json"
})
end,
outlineAvatar = function(self, ply, data)
self.logger:debug("Received request to outline avatar for ply: " .. tostring(ply:Nick()))
local avatar = data.response.players[1].avatarfull
local outlineColor = ChatTransit:GetTeamColor(ply:Team())
local steamID64 = ply:SteamID64()
return self:processAvatar(avatar, outlineColor, steamID64)
end
}
_base_0.__index = _base_0
_class_0 = setmetatable({
__init = function(self, logger)
self.logger = logger:scope("AvatarService")
self.outlinerUrl = tostring(avatarServiceAddress:GetString()) .. "/outline"
self.processedIds = { }
end,
__base = _base_0,
__name = "AvatarService"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
AvatarService = _class_0
end
hook.Add("InitPostEntity", "CFC_ChatTrahsit_AvatarServiceInit", function()
ChatTransit.AvatarService = AvatarService(ChatTransit.Logger)
end)
return hook.Add("CFC_SteamLookup_SuccessfulPlayerData", "CFC_ChatTransit_AvatarService", function(dataName, ply, data)
if not (dataName == "PlayerSummary") then
return
end
if not (data) then
return
end
local success, err = pcall(function()
return ChatTransit.AvatarService:outlineAvatar(ply, data)
end)
if not (success) then
ErrorNoHaltWithStack(err, dataName, ply)
end
return nil
end)

View File

@ -0,0 +1,89 @@
require("gwsockets")
require("logger")
ChatTransit = {
Logger = Logger("ChatTransit")
}
include("cfc_chat_transit/server/avatar_service.lua")
include("cfc_chat_transit/server/player_count.lua")
local Read
Read = file.Read
local GetColor
GetColor = team.GetColor
local TableToJSON
TableToJSON = util.TableToJSON
local logger = ChatTransit.Logger
local relayHost = CreateConVar("cfc_relay_host", "", FCVAR_NONE)
local loadHook = "ChatTransit_WebsocketLoad"
hook.Add("Think", loadHook, function()
hook.Remove("Think", loadHook)
ChatTransit.WebSocket = GWSockets.createWebSocket("wss://" .. tostring(relayHost:GetString()) .. "/relay", false)
ChatTransit.Realm = CreateConVar("cfc_realm", "unknown", FCVAR_REPLICATED + FCVAR_ARCHIVE, "The Realm Name")
do
local _with_0 = ChatTransit.WebSocket
_with_0.reconnectTimerName = "CFC_ChatTransit_WebsocketReconnect"
_with_0.onConnected = function(self)
logger:info("Established websocket connection")
return timer.Remove(_with_0.reconnectTimerName)
end
_with_0.onDisconnected = function(self)
logger:warn("Lost websocket connection!")
if timer.Exists(_with_0.reconnectTimerName) then
return logger:warn("Will retry " .. tostring(timer.RepsLeft(_with_0.reconnectTimerName)) .. " more times")
end
return timer.Create(_with_0.reconnectTimerName, 2, 30, function()
return _with_0:open()
end)
end
_with_0.onError = function(self, message)
return logger:error("Websocket Error!", message)
end
_with_0:open()
end
return nil
end)
ChatTransit.Send = function(self, data)
logger:debug("Sending '" .. tostring(data.Type) .. "'")
local steamID64 = data.Data.SteamId
data.Data.Avatar = data.Data.Avatar or ChatTransit.AvatarService:getAvatar(steamID64)
data.Realm = self.Realm:GetString()
data.Data.SteamId = data.Data.SteamId or ""
return self.WebSocket:write(TableToJSON(data))
end
ChatTransit.TeamColorCache = { }
ChatTransit.GetTeamColor = function(self, teamName)
if self.TeamColorCache[teamName] then
return self.TeamColorCache[teamName]
end
local teamColor = tostring(GetColor(teamName))
self.TeamColorCache[teamName] = teamColor
return teamColor
end
ChatTransit.guard = function(f, delay)
return function(...)
local args = {
...
}
local action
action = function()
local success, err = pcall(function()
return f(unpack(args))
end)
if not (success) then
return ErrorNoHaltWithStack(err)
end
end
if delay then
timer.Simple(delay, action)
else
action()
end
return nil
end
end
logger:info("Loading modules...")
local _list_0 = file.Find("cfc_chat_transit/server/modules/*.lua", "LUA")
for _index_0 = 1, #_list_0 do
local f = _list_0[_index_0]
logger:info("Loading modules/" .. tostring(f))
include("modules/" .. tostring(f))
end

View File

@ -0,0 +1,37 @@
local guard
guard = ChatTransit.guard
local isstring
isstring = _G.isstring
ChatTransit.AnticrashEvent = function(self, eventText)
if not (isstring(eventText)) then
eventText = "Heavy lag detected!"
end
return self:Send({
Type = "anticrash_event",
Data = {
Content = eventText,
SteamName = "CFC Anticrash"
}
})
end
hook.Add("z_anticrash_LagDetect", "CFC_ChatTransit_AnticrashEventListener", guard((function()
local _base_0 = ChatTransit
local _fn_0 = _base_0.AnticrashEvent
return function(...)
return _fn_0(_base_0, ...)
end
end)()))
hook.Add("z_anticrash_LagStuck", "CFC_ChatTransit_AnticrashEventListener", guard((function()
local _base_0 = ChatTransit
local _fn_0 = _base_0.AnticrashEvent
return function(...)
return _fn_0(_base_0, ...)
end
end)()))
return hook.Add("z_anticrash_CrashPrevented", "CFC_ChatTransit_AnticrashEventListener", guard((function()
local _base_0 = ChatTransit
local _fn_0 = _base_0.AnticrashEvent
return function(...)
return _fn_0(_base_0, ...)
end
end)()))

View File

@ -0,0 +1,34 @@
local guard
guard = ChatTransit.guard
ChatTransit.ReceiveMessage = function(self, ply, text, teamChat)
local shouldRelay = hook.Run("CFC_ChatTransit_ShouldRelayChatMessage", ply, text, teamChat)
if shouldRelay == false then
return
end
if teamChat then
return
end
if not (text) then
return
end
if text == "" then
return
end
return self:Send({
Type = "message",
Data = {
Type = "message",
Content = text,
SteamName = ply:Nick(),
SteamId = ply:SteamID64(),
IrisId = "none"
}
})
end
return hook.Add("PlayerSay", "CFC_ChatTransit_MessageListener", guard((function()
local _base_0 = ChatTransit
local _fn_0 = _base_0.ReceiveMessage
return function(...)
return _fn_0(_base_0, ...)
end
end)()), HOOK_MONITOR_LOW)

View File

@ -0,0 +1,48 @@
local delay, guard
do
local _obj_0 = ChatTransit
delay, guard = _obj_0.delay, _obj_0.guard
end
local SteamIDTo64
SteamIDTo64 = util.SteamIDTo64
ChatTransit.PlayerConnect = function(self, data)
local bot, name, steamId
bot, name, steamId = data.bot, data.name, data.networkid
bot = tobool(bot)
if bot then
steamId = nil
end
return self:Send({
Type = "connect",
Data = {
SteamName = name,
SteamId = steamId and SteamIDTo64(steamId),
PlayerCountCurrent = ChatTransit.playerCount,
PlayerCountMax = game:MaxPlayers()
}
})
end
ChatTransit.PlayerInitialSpawn = function(self, ply)
return self:Send({
Type = "spawn",
Data = {
SteamName = ply:Nick(),
SteamId = ply:SteamID64()
}
})
end
gameevent.Listen("player_connect")
hook.Add("player_connect", "CFC_ChatTransit_SpawnListener", guard((function()
local _base_0 = ChatTransit
local _fn_0 = _base_0.PlayerConnect
return function(...)
return _fn_0(_base_0, ...)
end
end)(), 0))
return hook.Add("PlayerInitialSpawn", "CFC_ChatTransit_SpawnListener", guard((function()
local _base_0 = ChatTransit
local _fn_0 = _base_0.PlayerInitialSpawn
return function(...)
return _fn_0(_base_0, ...)
end
end)()))

View File

@ -0,0 +1,32 @@
local delay, guard
do
local _obj_0 = ChatTransit
delay, guard = _obj_0.delay, _obj_0.guard
end
local GetBySteamID
GetBySteamID = player.GetBySteamID
local SteamIDTo64
SteamIDTo64 = util.SteamIDTo64
ChatTransit.PlayerDisconnected = function(self, data)
local name, reason, steamId
name, reason, steamId = data.name, data.reason, data.networkid
local ply = GetBySteamID(steamId)
return self:Send({
Type = "disconnect",
Data = {
SteamName = ply and ply:Nick() or name,
SteamId = ply and ply:SteamID64() or SteamIDTo64(steamId),
PlayerCountCurrent = ChatTransit.playerCount,
PlayerCountMax = game:MaxPlayers(),
Content = reason
}
})
end
gameevent.Listen("player_disconnect")
return hook.Add("player_disconnect", "CFC_ChatTransit_DisconnectListener", guard((function()
local _base_0 = ChatTransit
local _fn_0 = _base_0.PlayerDisconnected
return function(...)
return _fn_0(_base_0, ...)
end
end)(), 0))

View File

@ -0,0 +1,17 @@
local guard
guard = ChatTransit.guard
ChatTransit.PvPEvent = function(self, ply, newMode)
local eventText = tostring(ply:Nick()) .. " has entered " .. tostring(newMode) .. " mode"
return self:Send({
Type = "pvp_status_change",
Data = {
Content = eventText
}
})
end
hook.Add("CFC_PvP_PlayerEnterPvp", "CFC_ChatTransit_Relay", guard(function(ply)
return ChatTransit:PvPEvent(ply, "PvP")
end))
return hook.Add("CFC_PvP_PlayerExitPvp", "CFC_ChatTransit_Relay", guard(function(ply)
return ChatTransit:PvPEvent(ply, "Build")
end))

View File

@ -0,0 +1,26 @@
local guard
guard = ChatTransit.guard
local GetMap
GetMap = game.GetMap
ChatTransit.MapStartup = function(self, data)
local eventText = ""
local map = GetMap()
if SysTime() > 500 then
eventText = "Map switched to " .. map
else
eventText = "Server restarted! Current map is " .. map
end
return self:Send({
Type = "map_init",
Data = {
Content = eventText
}
})
end
return hook.Add("InitPostEntity", "CFC_ChatTransit_StartListener", timer.Simple(1, guard((function()
local _base_0 = ChatTransit
local _fn_0 = _base_0.MapStartup
return function(...)
return _fn_0(_base_0, ...)
end
end)())))

View File

@ -0,0 +1,40 @@
_Msg = Msg
local guard
guard = ChatTransit.guard
local isstring
isstring = _G.isstring
local Replace
Replace = string.Replace
ChatTransit.ReceiveULXAction = function(self, msg)
msg = Replace(msg, "\n", "")
self.Logger:debug("Received global ULX message", msg)
return self:Send({
Type = "ulx_action",
Data = {
Type = "ulx_action",
Content = msg
}
})
end
local M
M = function(...)
_Msg(...)
return ChatTransit:ReceiveULXAction(...)
end
return hook.Add("InitPostEntity", "ChatTransit_WrapUlxLog", guard(function()
if not (ulx) then
return
end
ulx._ChatTransit_fancyLogAdmin = ulx._ChatTransit_fancyLogAdmin or ulx.fancyLogAdmin
ulx.fancyLogAdmin = function(...)
local args = {
...
}
if not (isstring(args[2])) then
return ulx._ChatTransit_fancyLogAdmin(...)
end
Msg = M
ulx._ChatTransit_fancyLogAdmin(...)
Msg = _Msg
end
end))

View File

@ -0,0 +1,42 @@
local IsValid
IsValid = _G.IsValid
local guard
guard = ChatTransit.guard
local proxVoice
local proxVoiceEnabled
proxVoiceEnabled = function()
proxVoice = proxVoice or GetConVar("force_proximity_voice")
if not (proxVoice) then
return false
end
return proxVoice:GetBool()
end
ChatTransit.ReceiveVoiceTranscript = function(self, steamID64, data)
if proxVoiceEnabled() then
return
end
local ply = player.GetBySteamID64(steamID64)
if not (IsValid(ply)) then
return
end
if ply.ulx_gagged then
return
end
if ply:GetInfoNum("proximity_voice_enabled", 0) ~= 0 then
return
end
local shouldRelay = hook.Run("CFC_ChatTransit_ShouldRelayTranscript", ply, transcript)
if shouldRelay == false then
return
end
return self:Send({
Type = "voice_transcript",
Data = {
Type = "voice_transcript",
Content = data,
SteamName = ply:Nick(),
SteamId = ply:SteamID64(),
IrisId = "none"
}
})
end

View File

@ -0,0 +1,19 @@
ChatTransit.playerCount = 0
local increment
increment = function()
ChatTransit.playerCount = ChatTransit.playerCount + 1
end
local decrement
decrement = function()
ChatTransit.playerCount = ChatTransit.playerCount - 1
end
hook.Add("ClientSignOnStateChanged", "ChatTransit_PlayerCount", function(_, oldstate)
if not (oldstate == 7) then
return
end
return increment()
end)
gameevent.Listen("player_connect")
hook.Add("player_connect", "ChatTransit_PlayerCount", increment)
gameevent.Listen("player_disconnect")
return hook.Add("player_disconnect", "ChatTransit_PlayerCount", decrement)

View File

@ -0,0 +1,58 @@
local IsValid
IsValid = _G.IsValid
local AddNetworkString
AddNetworkString = util.AddNetworkString
local Start, Receive, ReadBool, WriteColor, WriteString, Send
do
local _obj_0 = net
Start, Receive, ReadBool, WriteColor, WriteString, Send = _obj_0.Start, _obj_0.Receive, _obj_0.ReadBool, _obj_0.WriteColor, _obj_0.WriteString, _obj_0.Send
end
local ToColor
ToColor = string.ToColor
AddNetworkString("CFC_ChatTransit_RemoteMessagePreference")
AddNetworkString("CFC_ChatTransit_RemoteMessageReceive")
local recipients = RecipientFilter()
local adminRecipients = RecipientFilter()
local shouldTransmit = CreateConVar("cfc_chat_transit_should_transmit_remote", 1, FCVAR_ARCHIVE, "Should transmit remote messages", 0, 1)
local adminOnly = CreateConVar("cfc_chat_transit_transmit_admin_only", 1, FCVAR_ARCHIVE, "Should only transmit to Admins?", 0, 1)
Receive("CFC_ChatTransit_RemoteMessagePreference", function(_, ply)
local shouldReceive = ReadBool()
if shouldReceive then
recipients:AddPlayer(ply)
if adminOnly:GetBool() and ply:IsAdmin() then
adminRecipients:AddPlayer(ply)
end
return
end
recipients:RemovePlayer(ply)
return adminRecipients:RemovePlayer(ply)
end)
local broadcastMessage
broadcastMessage = function(ply, cmd, args, argStr)
if not (shouldTransmit:GetBool()) then
return
end
if IsValid(ply) then
return
end
local author = rawget(args, 1)
local authorColor = rawget(args, 2)
local message = rawget(args, 3)
if not (author) then
return
end
if not (authorColor) then
return
end
if not (message) then
return
end
authorColor = ToColor(authorColor)
local sendingTo = adminOnly:GetBool() and adminRecipients or recipients
Start("CFC_ChatTransit_RemoteMessageReceive")
WriteString(author)
WriteColor(authorColor)
WriteString(message)
return Send(sendingTo)
end
return concommand.Add("chat_transit", broadcastMessage)

View File

@ -7,7 +7,7 @@ class AvatarService
new: (logger) =>
@logger = logger\scope "AvatarService"
@outlinerUrl = "http://#{avatarServiceAddress\GetString!}/outline"
@outlinerUrl = "#{avatarServiceAddress\GetString!}/outline"
@processedIds = {}
getAvatar: (steamID64) =>
@ -18,11 +18,11 @@ class AvatarService
processAvatar: (avatarUrl, outlineColor, steamID64) =>
body = TableToJSON { :avatarUrl, :outlineColor, steamID: steamID64 }
@logger\info "Sending data to outliner: ", body
@logger\debug "Sending data to outliner: ", body
failed = @logger\error
success = (code, body) ->
@logger\info "Avatar request succeeded with code: #{code} | Body: #{body}"
@logger\debug "Avatar request succeeded with code: #{code} | Body: #{body}"
@processedIds[steamID64] = true
HTTP
@ -34,7 +34,7 @@ class AvatarService
type: "application/json"
outlineAvatar: (ply, data) =>
@logger\info "Received request to outline avatar for ply: #{ply\Nick!}"
@logger\debug "Received request to outline avatar for ply: #{ply\Nick!}"
avatar = data.response.players[1].avatarfull
outlineColor = ChatTransit\GetTeamColor ply\Team!
steamID64 = ply\SteamID64!

View File

@ -16,8 +16,8 @@ loadHook = "ChatTransit_WebsocketLoad"
hook.Add "Think", loadHook, ->
hook.Remove "Think", loadHook
ChatTransit.WebSocket = GWSockets.createWebSocket "ws://#{relayHost\GetString!}/relay"
ChatTransit.Realm = CreateConVar "cfc_realm", "", FCVAR_NONE, "CFC Realm Name"
ChatTransit.WebSocket = GWSockets.createWebSocket "wss://#{relayHost\GetString!}/relay", false
ChatTransit.Realm = CreateConVar "cfc_realm", "unknown", FCVAR_REPLICATED + FCVAR_ARCHIVE, "The Realm Name"
with ChatTransit.WebSocket
.reconnectTimerName = "CFC_ChatTransit_WebsocketReconnect"
@ -41,7 +41,7 @@ hook.Add "Think", loadHook, ->
return nil
ChatTransit.Send = (data) =>
logger\info "Sending '#{data.Type}'"
logger\debug "Sending '#{data.Type}'"
steamID64 = data.Data.SteamId
data.Data.Avatar or= ChatTransit.AvatarService\getAvatar steamID64

View File

@ -0,0 +1,30 @@
import IsValid from _G
import guard from ChatTransit
local proxVoice
proxVoiceEnabled = ->
proxVoice or= GetConVar "force_proximity_voice"
return false unless proxVoice
return proxVoice\GetBool!
ChatTransit.ReceiveVoiceTranscript = (steamID64, data) =>
return if proxVoiceEnabled!
ply = player.GetBySteamID64 steamID64
return unless IsValid ply
return if ply.ulx_gagged
return if ply\GetInfoNum("proximity_voice_enabled", 0) ~= 0
shouldRelay = hook.Run "CFC_ChatTransit_ShouldRelayTranscript", ply, transcript
return if shouldRelay == false
@Send
Type: "voice_transcript"
Data:
Type: "voice_transcript"
Content: data
SteamName: ply\Nick!
SteamId: ply\SteamID64!
IrisId: "none"

View File

@ -1,13 +1,14 @@
FROM python:3.9.0
RUN mkdir /avatars
RUN mkdir /avatar-service
WORKDIR /avatar-service
COPY . /avatar-service
RUN pip install -U --upgrade pip
RUN mkdir /avatars
WORKDIR /avatar-service
COPY requirements.txt .
RUN pip install --upgrade -r ./requirements.txt
COPY . .
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

View File

@ -12,7 +12,7 @@ services:
service:
build:
context: .
dockerfile: Dockerfile.service
dockerfile: Dockerfile
command: flask run --host 0.0.0.0 --port 8080
container_name: "${REALM}_chat_transit_avatar_service"
ports:
@ -22,5 +22,4 @@ services:
env_file:
- .env
volumes:
- ./:/build/src
- "$AVATARS_DIR:/avatars:z"

0
web/avatar_service/entrypoint.sh Normal file → Executable file
View File

View File

@ -4,6 +4,10 @@ ENV GOPATH=/build
ENV GOBIN=/usr/local/go/bin
WORKDIR $GOPATH/src
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
ARG BRANCH=HEAD

View File

@ -9,3 +9,4 @@ services:
- .env
volumes:
- ./:/build/src
restart: always

View File

@ -3,7 +3,8 @@ module github.com/cfc-servers/cfc_chat_transit
go 1.15
require (
github.com/bwmarrin/discordgo v0.23.2
github.com/bwmarrin/discordgo v0.25.0
github.com/getsentry/sentry-go v0.9.0
github.com/gorilla/websocket v1.4.2
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
)

View File

@ -9,6 +9,8 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs=
github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
@ -96,6 +98,8 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -141,6 +145,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -150,6 +156,7 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -159,8 +166,12 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=

View File

@ -9,6 +9,7 @@ import (
"strings"
"github.com/bwmarrin/discordgo"
"github.com/cfc-servers/cfc_chat_transit/voice"
)
var discord *discordgo.Session
@ -30,11 +31,25 @@ type EventData struct {
PlayerCountCurrent float32
}
var MessageQueue = make(chan []byte, 100)
type VoiceMessageOperation struct {
Message string
MessageId string
SteamId string
SteamName string
Avatar string
IsFinal bool
}
var MessageQueue = make(chan []byte, 10000)
var WebhookId string = os.Getenv("WEBHOOK_ID")
var WebhookSecret string = os.Getenv("WEBHOOK_SECRET")
var VoiceWebhookId string = os.Getenv("VOICE_WEBHOOK_ID")
var VoiceWebhookSecret string = os.Getenv("VOICE_WEBHOOK_SECRET")
var DiscordToken string = os.Getenv("DISCORD_TOKEN")
const urlRegexString = `https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`
const (
@ -43,9 +58,11 @@ const (
EMOJI_HALTED = "<:halted:398133588010336259>"
EMOJI_BUILD = "<:build:933512140395012107>"
EMOJI_PVP = "<:bk:812130062379515906>"
EMOJI_PLAY = "<:playbuttonsmaller:1017716044485382154>"
EMOJI_MAP = "🗺️"
EMOJI_CONNECT = "📡"
EMOJI_ULX = "⌨️"
EMOJI_VOICE = "🗣️"
COLOR_RED = 0xE7373E
COLOR_GREEN = 0x37E73E
@ -89,7 +106,7 @@ func sendMessage(discord *discordgo.Session, message EventStruct) {
}
}
func sendEvent(discord *discordgo.Session, event EventStruct, eventText string, color int, emoji string) {
func sendEvent(discord *discordgo.Session, event EventStruct, eventText string, color int, emoji string) *discordgo.Message {
params := &discordgo.WebhookParams{
AllowedMentions: &discordgo.MessageAllowedMentions{
Parse: []discordgo.AllowedMentionType{},
@ -104,11 +121,13 @@ func sendEvent(discord *discordgo.Session, event EventStruct, eventText string,
},
}
_, err := discord.WebhookExecute(WebhookId, WebhookSecret, true, params)
message, err := discord.WebhookExecute(WebhookId, WebhookSecret, true, params)
if err != nil {
log.Println(err)
}
return message
}
func sendConnectMessage(discord *discordgo.Session, event EventStruct) {
@ -170,10 +189,88 @@ func sendPvpStatusChange(discord *discordgo.Session, event EventStruct) {
sendEvent(discord, event, event.Data.Content, color, emoji)
}
func queueGroomer() {
discord, err := discordgo.New("")
func sendVoiceText(discord *discordgo.Session, data *voice.Session) string {
transcript := data.Message
steamName := data.SteamName
avatar := data.Avatar
// fileName := data.FileName
messageId := data.MessageId
// isFinal := data.Finished
log.Println(WebhookId, WebhookSecret)
// var voiceLink string
// if len(fileName) > 0 {
// voiceLink = fmt.Sprintf("https://larynx.cfcservers.org/%v.ogg", fileName)
// }
// var description string
// if isFinal && len(voiceLink) > 0 {
// description = fmt.Sprintf("%v [%v](%v) %v", EMOJI_VOICE, EMOJI_PLAY, voiceLink, transcript)
// } else {
// description = fmt.Sprintf("%v %v", EMOJI_VOICE, transcript)
// }
description := fmt.Sprintf("%v %v", EMOJI_VOICE, transcript)
embeds := []*discordgo.MessageEmbed{
{
Description: description,
Color: COLOR_BLUE,
},
}
if len(messageId) == 0 {
log.Println("Creating new message for voice")
params := &discordgo.WebhookParams{
AllowedMentions: &discordgo.MessageAllowedMentions{
Parse: []discordgo.AllowedMentionType{},
},
Username: steamName,
AvatarURL: avatar,
Embeds: embeds,
}
message, err := discord.WebhookExecute(VoiceWebhookId, VoiceWebhookSecret, true, params)
if err != nil {
log.Println("Error sending webhook message create")
log.Println(err)
return ""
}
return message.ID
} else {
log.Println("Updating existing message for voice")
params := &discordgo.WebhookEdit{
Embeds: embeds,
}
log.Println(messageId)
log.Println(params.Embeds[0].Description)
message, err := discord.WebhookMessageEdit(VoiceWebhookId, VoiceWebhookSecret, messageId, params)
if err != nil {
log.Println("Error sending webhook message edit")
log.Println(err)
}
return message.ID
}
}
func processVoiceText(queueVoiceText func(string, string, string, string), event EventStruct) {
steamId := event.Data.SteamId
steamName := event.Data.SteamName
avatar := event.Data.Avatar
data := event.Data.Content
log.Println(data)
queueVoiceText(steamId, steamName, avatar, data)
}
func queueGroomer() {
discord, err := discordgo.New(DiscordToken)
voiceManager := voice.NewManager(discord, sendVoiceText)
if err != nil {
log.Fatal("error connecting:", err)
@ -211,6 +308,9 @@ func queueGroomer() {
sendUlxAction(discord, message)
case "pvp_status_change":
sendPvpStatusChange(discord, message)
}
case "voice_transcript":
processVoiceText(voiceManager.ReceiveVoiceTranscript, message)
}
}
}

View File

@ -0,0 +1,166 @@
package voice
import (
"encoding/json"
"github.com/bwmarrin/discordgo"
"log"
"sync"
"time"
)
type Session struct {
SteamId string
SteamName string
Message string
MessageId string
FileName string
Avatar string
Finished bool
}
type Operation struct {
Session *Session
}
type Manager struct {
Sessions map[string]*Session
Operations []*Operation
discord *discordgo.Session
opmutex *sync.Mutex
sendMessage func(*discordgo.Session, *Session) string
}
// MessageData The "Content" key of the Event that comes into the Main package
type MessageData struct {
Message string `json:"transcript"`
FileName string `json:"file_name"`
SessionID string `json:"session_id"`
IsFinal bool `json:"is_final"`
}
func (v *Manager) runSendQueue() {
bucket := v.discord.Ratelimiter.GetBucket("voiceTranscriptions")
wait := func() {
time.Sleep(time.Millisecond * 100)
}
for {
wait()
// This should be a blocking call
// Unblocks when bucket has bandwidth
v.discord.Ratelimiter.LockBucketObject(bucket).Unlock()
if len(v.Operations) == 0 {
wait()
continue
}
v.opmutex.Lock()
firstOperation := v.Operations[0]
log.Printf("Processing message for session %v", firstOperation.Session.FileName)
v.Operations = v.Operations[1:]
session := firstOperation.Session
description := session.Message
if len(description) == 0 {
log.Printf("No description for session %v", session.FileName)
// What is this? Why are you showing this to me
v.opmutex.Unlock()
continue
}
messageId := v.sendMessage(v.discord, session)
if len(messageId) == 0 {
log.Println("Received no message ID from sendMessage")
// Failed to send/update message, send to back of queue
v.Operations = append(v.Operations, firstOperation)
v.opmutex.Unlock()
continue
} else {
session.MessageId = messageId
}
v.opmutex.Unlock()
}
}
func (v *Manager) ReceiveVoiceTranscript(steamId string, steamName string, avatar string, data string) {
messageData := MessageData{}
err := json.Unmarshal([]byte(data), &messageData)
if err != nil {
log.Printf("Error parsing voice message: %v", err)
return
}
newMessage := messageData.Message
isFinal := messageData.IsFinal
fileName := messageData.FileName
sessionId := messageData.SessionID
session, ok := v.Sessions[sessionId]
if !ok {
session = &Session{
SteamId: steamId,
SteamName: steamName,
Message: newMessage,
MessageId: "",
Finished: isFinal,
Avatar: avatar,
FileName: fileName,
}
v.Sessions[sessionId] = session
} else {
// Nothing changed - we don't need to queue an Operation for this
if !isFinal && session.Message == newMessage {
return
}
session.Message = newMessage
session.Finished = isFinal
session.FileName = fileName
}
if isFinal {
// Final message for this transcription, we can stop tracking it
// TODO: do I use delete here?
v.Sessions[sessionId] = nil
}
v.opmutex.Lock()
defer v.opmutex.Unlock()
if ok {
// Session already existed for sessionID
// Do we already have a pending Operation?
for _, operation := range v.Operations {
if operation.Session == session {
// If so, we don't need to add another operation
return
}
}
}
v.Operations = append(v.Operations, &Operation{
Session: session,
})
}
func NewManager(discord *discordgo.Session, sendMessage func(*discordgo.Session, *Session) string) *Manager {
manager := &Manager{
Sessions: make(map[string]*Session),
Operations: make([]*Operation, 0),
discord: discord,
opmutex: &sync.Mutex{},
sendMessage: sendMessage,
}
go manager.runSendQueue()
return manager
}