✅ C# - SCUM FTP Server Cawling Discord Bot

OFCC-4K·2024년 8월 9일

🔵 Portfolio :: C#

목록 보기
1/1

🔸Run Screen

🔹Composition Screen


🔹Server Screen


🔹Client Screen


🔸Source Code

🔹Program.cs

using Discord;
using Discord.Commands;
using Discord.WebSocket;

namespace Scum_Log_Bot
{
    class Program
    {
        static string BOT_TOKEN = "YOUR_BOT_TOKEN";

        static ulong raidChannelId = 0000000000000000000; // YOUR CHANNEL ID
        static ulong chatChannelId = 0000000000000000000; // YOUR CHANNEL ID

        static DiscordSocketClient client;
        static CommandService commandService;

        Dictionary<string, KeyValuePair<string, ulong>> infos = new Dictionary<string, KeyValuePair<string, ulong>>();

        static Dictionary<string, LoggingTask> tasks = new Dictionary<string, LoggingTask>();

        static void Main(string[] args)
        {
            Console.WriteLine("SCUM BOT - ON (By. 4K)\r\n");

            new Program().BotMain().GetAwaiter().GetResult();
        }

        private async Task BotMain()
        {
            client = new DiscordSocketClient(new DiscordSocketConfig()
            {
                LogLevel = LogSeverity.Debug
            });
            commandService = new CommandService(new CommandServiceConfig()
            {
                LogLevel = LogSeverity.Debug
            });

            client.Log += OnClientLogReceived;
            client.Ready += OnReady;
            client.MessageReceived += OnClientMessage;
            client.MessageDeleted += OnClientMessageDeleted;
            commandService.Log += OnClientLogReceived;

            await client.LoginAsync(TokenType.Bot, BOT_TOKEN);
            await client.StartAsync();

            await Task.Delay(-1);
        }

        private Task OnClientLogReceived(LogMessage msg)
        {
            //Console.WriteLine(msg.ToString());

            return Task.CompletedTask;
        }

        private async Task OnReady()
        {
            infos.Add("admin", new KeyValuePair<string, ulong>("어드민 명령어", 0000000000000000000)); // YOUR CHANNEL ID
            infos.Add("login", new KeyValuePair<string, ulong>("유저 입장", 0000000000000000000)); // YOUR CHANNEL ID
            infos.Add("lockpick", new KeyValuePair<string, ulong>("락픽", 0000000000000000000)); // YOUR CHANNEL ID
            infos.Add("vehicle_destruction", new KeyValuePair<string, ulong>("차량 파괴", 0000000000000000000)); // YOUR CHANNEL ID
            infos.Add("kill", new KeyValuePair<string, ulong>("킬", 0000000000000000000)); // YOUR CHANNEL ID
            infos.Add("chat", new KeyValuePair<string, ulong>("인게임 채팅", chatChannelId)); // YOUR CHANNEL ID
            infos.Add("violations", new KeyValuePair<string, ulong>("벤", 0000000000000000000)); // YOUR CHANNEL ID

            foreach (KeyValuePair<string, KeyValuePair<string, ulong>> info in infos)
            {
                tasks.Add(info.Key, new LoggingTask(client, info));

                Console.WriteLine($"● {info.Value.Key} 로그 활성화");
            }

            Console.WriteLine();
        }

        private async Task OnClientMessage(SocketMessage arg)
        {
            var message = arg as SocketUserMessage;

            if (message == null)
            {
                return;
            }

            if (message.Author.IsBot && !message.Content.Contains("➡️ 채팅 타입 - Admin") && message.Content.Contains("➡️ 채팅 내용 - !raid") && message.Channel.Id == chatChannelId)
            {
                await ((IMessageChannel)client.GetChannel(raidChannelId)).SendMessageAsync(message.Content.Replace("📊 PK | ", "📊 FK | "));
            }
        }

        private async Task OnClientMessageDeleted(Cacheable<IMessage, ulong> cachedMessage, Cacheable<IMessageChannel, ulong> channel)
        {
            foreach (var info in infos)
            {
                if (info.Value.Value == channel.Id)
                {
                    foreach (var task in tasks)
                    {
                        task.Value.run = false;
                    }

                    Console.WriteLine($"\r\n- 디스코드 '{channel.Value.Name.Substring(3)}' 채널에서 로그 삭제 이벤트가 발생 되었습니다. 프로그램을 재실행 해주세요.");
                    Console.Write("이 창을 닫으려면 아무 키나 누르세요...");

                    Console.ReadKey();

                    Environment.Exit(0);
                }
            }
        }
    }
}

🔹LoggingTask.cs

using Discord;
using Discord.WebSocket;
using System.Globalization;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;

namespace Scum_Log_Bot
{
    class LoggingTask
    {
        public bool run = true;

        private DiscordSocketClient client;
        private IMessageChannel channel;
        private KeyValuePair<string, KeyValuePair<string, ulong>> info;

        private string programPath;

        private ulong newLogCount;

        private readonly string host = "ftp://YOUR_FTP_IP:YOUR_FTP_PORT";
        private readonly string user = "YOUR_FTP_USER_ID";
        private readonly string password = "YOUR_FTP_USER_PASSWORD";
        private readonly string remote = "/SCUM/Saved/SaveFiles/Logs/";

        private string[] logFiles = new string[0];

        private ulong lastFileDate;
        private ulong lastFileLine;

        public LoggingTask(DiscordSocketClient client, KeyValuePair<string, KeyValuePair<string, ulong>> info)
        {
            this.client = client;
            this.info = info;

            new Thread(Run).Start();
        }

        private async void Run()
        {
            channel = client.GetChannel(info.Value.Value) as IMessageChannel;

            programPath = Directory.GetCurrentDirectory() + "/Logs/" + info.Key;

            if (!Directory.Exists(programPath))
            {
                Directory.CreateDirectory(programPath);
            }

            await ReadLastFileAsync();

            while (run)
            {
                await PrintSpecificFilesFromFtp(host, user, password, remote);
            }
        }

        private async Task ReadLastFileAsync()
        {
            try
            {
                lastFileDate = ulong.Parse(File.ReadAllText(programPath + "/LastProgramFileDate.log"));
            }
            catch (Exception ex)
            {
                //Console.WriteLine($"Error: {ex.StackTrace}");

                lastFileDate = 0;
            }

            try
            {
                lastFileLine = ulong.Parse(File.ReadAllText(programPath + "/LastProgramFileLine.log"));
            } catch (Exception ex)
            {
                //Console.WriteLine($"Error: {ex.StackTrace}");

                lastFileLine = 0;
            }
        }

        private async Task WriteLastFileAsync()
        {
            try
            {
                File.WriteAllText(programPath + "/LastProgramFileDate.log", lastFileDate + "");
            }
            catch (Exception ex)
            {
                //Console.WriteLine($"Error: {ex.StackTrace}");
            }

            try
            {
                File.WriteAllText(programPath + "/LastProgramFileLine.log", lastFileLine + "");
            }
            catch (Exception ex)
            {
                //Console.WriteLine($"Error: {ex.StackTrace}");
            }
        }

        private async Task PrintSpecificFilesFromFtp(string ftpServer, string ftpUsername, string ftpPassword, string remotePath)
        {
            try
            {
                await ListFilesOnFtpServer(ftpServer, ftpUsername, ftpPassword, remotePath);

                newLogCount = 0;
                foreach (string file in logFiles)
                {
                    ulong currentFileDate = ulong.Parse(file.Substring(file.Length - 18, 14));

                    if (currentFileDate < lastFileDate)
                    {
                        continue;
                    }

                    string remoteFilePath = $"{remotePath}{file}";
                    await PrintFileFromFtp(ftpServer, ftpUsername, ftpPassword, remoteFilePath, currentFileDate);
                }

                if (newLogCount > 0)
                {
                    Console.WriteLine($"- 디스코드 '{channel.Name.Substring(3)}' 채널에 새로운 로그 {newLogCount}개가 갱신 되었습니다.");
                }
            }
            catch (Exception ex)
            {
                //Console.WriteLine($"Error: {ex.StackTrace}");
            }
        }

        private async Task ListFilesOnFtpServer(string ftpServer, string ftpUsername, string ftpPassword, string remotePath)
        {
            string ftpUri = $"{ftpServer}{remotePath}";
            FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUri);
            request.Method = WebRequestMethods.Ftp.ListDirectoryDetails;
            request.Credentials = new NetworkCredential(ftpUsername, ftpPassword);
            request.UsePassive = true;

            try
            {
                using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
                using (Stream responseStream = response.GetResponseStream())
                using (StreamReader reader = new StreamReader(responseStream))
                {
                    string line;
                    var files = new List<string>();
                    while ((line = reader.ReadLine()) != null)
                    {
                        string[] parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                        string fileName = parts[parts.Length - 1];
                        if (fileName.StartsWith(ExceptionKey()))
                        {
                            files.Add(fileName);
                        }
                    }

                    logFiles = files.ToArray();
                }
            }
            catch (WebException ex)
            {
                //Console.WriteLine($"Error: {ex.StackTrace}");

                logFiles = new string[0];
            }
        }

        private async Task PrintFileFromFtp(string ftpServer, string ftpUsername, string ftpPassword, string remoteFilePath, ulong currentFileDate)
        {
            ulong currentFileLine = 1;

            string ftpUri = $"{ftpServer}{remoteFilePath}";
            FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUri);
            request.Method = WebRequestMethods.Ftp.DownloadFile;
            request.Credentials = new NetworkCredential(ftpUsername, ftpPassword);
            request.UsePassive = true;

            try
            {
                using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
                using (Stream responseStream = response.GetResponseStream())
                using (StreamReader reader = new StreamReader(responseStream, Encoding.Unicode))
                {
                    string allData = reader.ReadToEnd();
                    string[] contents = allData.Split("\n");

                    int size = contents.Length;
                    for (int i = 0; i < size; i++)
                    {
                        if (contents[i].Length > 0 && !contents[i].Contains("Game version"))
                        {
                            if (!ExceptionConditionCheck(contents[i]))
                            {
                                continue;
                            }

                            if (currentFileDate > lastFileDate)
                            {
                                lastFileLine = 0;
                            }

                            if (currentFileLine <= lastFileLine)
                            {
                                currentFileLine++;
                                continue;
                            }

                            string data = ParseData(contents[i], currentFileLine);

                            await channel.SendMessageAsync($"```{data}```");

                            lastFileDate = currentFileDate;
                            lastFileLine = currentFileLine;

                            await WriteLastFileAsync();

                            currentFileLine++;
                            newLogCount++;

                            await Task.Delay(1000);
                        }
                    }
                }
            }
            catch (WebException ex)
            {
                //Console.WriteLine($"Error: {ex.StackTrace}");
            }
        }

        private string ParseData(string data, ulong currentFileLine)
        {
            string pData = "📊 PK | " + info.Value.Key + " | " + DateTime.ParseExact(data.Substring(0, 19), "yyyy.MM.dd-HH.mm.ss", CultureInfo.InvariantCulture).AddHours(9).ToString("yyyy.MM.dd-HH:mm:ss", CultureInfo.InvariantCulture) + " | " + currentFileLine + "\r\n";

            switch (info.Key)
            {
                case "admin":
                    string userPattern = @"'(\d+):(.+?)\((\d+)\)'";

                    string commandPattern = @"Command:\s'(.+?)'";
                    string teleportPattern = @"Target\sof\sTeleportTo:\s'(\d+):(.+?)\((\d+)\)'\sLocation:\sX=(-?\d+\.\d+)\sY=(-?\d+\.\d+)\sZ=(-?\d+\.\d+)";
                    string mapClickPattern = @"Used\smap\sclick\steleport\sto\s(player|vehicle):\s(?:'(\d+):(.+?)\((\d+)\)'|(.+?))\sLocation:\sX=(-?\d+\.\d+)\sY=(-?\d+\.\d+)\sZ=(-?\d+\.\d+)";

                    var userMatch = Regex.Match(data, userPattern);

                    if (userMatch.Success)
                    {
                        pData += $"➡️ 사용자 정보 - {userMatch.Groups[2].Value} ({userMatch.Groups[1].Value})\r\n";

                        var commandMatch = Regex.Match(data, commandPattern);
                        var teleportMatch = Regex.Match(data, teleportPattern);
                        var mapClickMatch = Regex.Match(data, mapClickPattern);

                        if (commandMatch.Success)
                        {
                            pData += $"➡️ 명령어 타입 - 채팅\r\n";
                            pData += $"➡️ 명령어 정보 - {commandMatch.Groups[1].Value}";
                        }
                        else if (teleportMatch.Success)
                        {
                            pData += $"➡️ 명령어 타입 - 맵 클릭\r\n";
                            pData += $"➡️ 텔레포트 대상 - {teleportMatch.Groups[2].Value} ({teleportMatch.Groups[1].Value})\r\n";
                            pData += $"➡️ 텔레포트 좌표 - X={teleportMatch.Groups[4].Value} Y={teleportMatch.Groups[5].Value} Z={teleportMatch.Groups[6].Value}";
                        }
                        else if (mapClickMatch.Success)
                        {
                            string steamId = mapClickMatch.Groups[2].Value;

                            pData += $"➡️ 명령어 타입 - 맵 클릭\r\n";

                            if (!string.IsNullOrEmpty(steamId))
                            {
                                pData += $"➡️ 텔레포트 대상 - {mapClickMatch.Groups[3].Value} ({steamId})\r\n";
                            } else
                            {
                                pData += $"➡️ 텔레포트 대상 - {mapClickMatch.Groups[5].Value.Replace("'", "")}\r\n";
                            }

                            pData += $"➡️ 텔레포트 좌표 - X={mapClickMatch.Groups[6].Value} Y={mapClickMatch.Groups[7].Value} Z={mapClickMatch.Groups[8].Value}";
                        }
                        else
                        {
                            pData = $"⚠️ 오류 정보 - 로그 기록 중 예기치 못한 오류가 발생 했습니다. (Error Code. 2)";
                        }
                    }
                    else
                    {
                        pData = $"⚠️ 오류 정보 - 로그 기록 중 예기치 못한 오류가 발생 했습니다. (Error Code. 1)";
                    }
                    break;
                case "login":
                    string loginPattern = @"'(\d+\.\d+\.\d+\.\d+)\s(\d+):(.+?)\((\d+)\)' logged in at:\sX=(-?\d+\.\d+)\sY=(-?\d+\.\d+)\sZ=(-?\d+\.\d+)";

                    var loginMatch = Regex.Match(data, loginPattern);

                    if (loginMatch.Success)
                    {
                        pData += $"➡️ 계정 정보 - {loginMatch.Groups[3].Value} ({loginMatch.Groups[2].Value})\r\n";
                        pData += $"➡️ IP 정보 - {loginMatch.Groups[1].Value}\r\n";
                        pData += $"➡️ 입장 좌표 - X={loginMatch.Groups[5].Value} Y={loginMatch.Groups[6].Value} Z={loginMatch.Groups[7].Value}";
                    }
                    else
                    {
                        pData = $"⚠️ 오류 정보 - 로그 기록 중 예기치 못한 오류가 발생 했습니다. (Error Code. 3)";
                    }
                    break;
                case "lockpick":
                    string lockpickPattern = @"\[LogMinigame\]\s\[(?:BP_DialLockMinigame_C|LockpickingMinigame_C)\]\sUser:\s(.+?)\s\((\d+),\s(\d+)\)\.\sSuccess:\s(\w+)\.\sElapsed\stime:\s([\d\.]+)\.\sFailed\sattempts:\s(\d+)\.\sTarget\sobject:\s(.+?)\(ID:\s(.+?)\)\.\sLock\stype:\s(.+?)\.\sUser\sowner:\s(.+?)\.\sLocation:\sX=(-?\d+\.\d+)\sY=(-?\d+\.\d+)\sZ=(-?\d+\.\d+)";

                    var lockpickMatch = Regex.Match(data, lockpickPattern);

                    if (lockpickMatch.Success)
                    {
                        string lockType = lockpickMatch.Groups[9].Value;

                        pData += $"➡️ 계정 정보 - {lockpickMatch.Groups[1].Value} ({lockpickMatch.Groups[3].Value})\r\n";
                        pData += $"➡️ 대상 정보 - {lockpickMatch.Groups[7].Value} ({lockpickMatch.Groups[8].Value})\r\n";
                        pData += $"➡️ 대상 주인 - {lockpickMatch.Groups[10].Value}\r\n";
                        pData += $"➡️ 대상 좌표 - X={lockpickMatch.Groups[11].Value} Y={lockpickMatch.Groups[12].Value} Z={lockpickMatch.Groups[13].Value}\r\n";
                        pData += $"➡️ 잠금 종류 - {(lockType.Equals("DialLock") ? "다이얼 락" : lockType.Equals("Advanced") ? "금장" : lockType.Equals("Medium") ? "은장" : lockType.Equals("Basic") ? "동장" : "똥장")}\r\n";
                        pData += $"➡️ 성공 여부 - {(lockpickMatch.Groups[4].Value.Equals("Yes") ? "⭕" : "❌")}\r\n";
                        pData += $"➡️ 실패 횟수 - {lockpickMatch.Groups[6].Value}회\r\n";
                        pData += $"➡️ 소요 시간 - {lockpickMatch.Groups[5].Value}초";
                    }
                    else
                    {
                        pData = $"⚠️ 오류 정보 - 로그 기록 중 예기치 못한 오류가 발생 했습니다. (Error Code. 4)";
                    }
                    break;
                case "vehicle_destruction":
                    string vdPattern = @"\[Destroyed\]\s([\w_]+)\.\sVehicleId:\s(\d+)\.\sOwner:\s([^\.]+)\.\sLocation:\sX=([-?\d.]+)\sY=([-?\d.]+)\sZ=([-?\d.]+)";

                    var vdMatch = Regex.Match(data, vdPattern);

                    if (vdMatch.Success)
                    {
                        pData += $"➡️ 차량 정보 - {vdMatch.Groups[1].Value} ({vdMatch.Groups[2].Value})\r\n";
                        pData += $"➡️ 차량 주인 - {vdMatch.Groups[3].Value}\r\n";
                        pData += $"➡️ 파괴 좌표 - X={vdMatch.Groups[4].Value} Y={vdMatch.Groups[5].Value} Z={vdMatch.Groups[6].Value}";
                    }
                    else
                    {
                        pData = $"⚠️ 오류 정보 - 로그 기록 중 예기치 못한 오류가 발생 했습니다. (Error Code. 5)";
                    }
                    break;
                case "kill":
                    string killerPattern = @"Killer:\s(.+?)\s\((\d+)\)";
                    string diedPattern = @"Died:\s(.+?)\s\((\d+)\)";
                    string weaponPattern = @"Weapon:\s(.+?)\s\[(.+?)\]";
                    string locationsPattern = @"KillerLoc\s:\s([\d\.\-]+),\s([\d\.\-]+),\s([\d\.\-]+)\sVictimLoc:\s([\d\.\-]+),\s([\d\.\-]+),\s([\d\.\-]+),\sDistance:\s([\d\.\-]+)\sm";

                    var killerMatch = Regex.Match(data, killerPattern);
                    var diedMatch = Regex.Match(data, diedPattern);
                    var weaponMatch = Regex.Match(data, weaponPattern);
                    var locationsMatch = Regex.Match(data, locationsPattern);

                    if (killerMatch.Success && diedMatch.Success && weaponMatch.Success && locationsMatch.Success)
                    {
                        pData += $"➡️ 공격자 정보 - {killerMatch.Groups[1].Value} ({killerMatch.Groups[2].Value})\r\n";
                        pData += $"➡️ 공격자 좌표 - X={locationsMatch.Groups[1].Value} Y={locationsMatch.Groups[2].Value} Z={locationsMatch.Groups[3].Value}\r\n";
                        pData += $"➡️ 피해자 정보 - {diedMatch.Groups[1].Value} ({diedMatch.Groups[2].Value})\r\n";
                        pData += $"➡️ 피해자 좌표 - X={locationsMatch.Groups[4].Value} Y={locationsMatch.Groups[5].Value} Z={locationsMatch.Groups[6].Value}\r\n";
                        pData += $"➡️ 무기 정보 - {weaponMatch.Groups[1].Value} ({weaponMatch.Groups[2].Value})\r\n";
                        pData += $"➡️ 거리 정보 - {locationsMatch.Groups[7].Value}m";
                    }
                    else
                    {
                        pData = $"⚠️ 오류 정보 - 로그 기록 중 예기치 못한 오류가 발생 했습니다. (Error Code. 6)";
                    }
                    break;
                case "chat":
                    string chatPattern = @"'(\d{17}):([^']*)' '([^:]*):\s(.*)'";

                    var chatMatch = Regex.Match(data, chatPattern);

                    if (chatMatch.Success)
                    {
                        pData += $"➡️ 계정 정보 - {chatMatch.Groups[2].Value} ({chatMatch.Groups[1].Value})\r\n";
                        pData += $"➡️ 채팅 타입 - {chatMatch.Groups[3].Value}\r\n";
                        pData += $"➡️ 채팅 내용 - {chatMatch.Groups[4].Value}";
                    }
                    else
                    {
                        pData = $"⚠️ 오류 정보 - 로그 기록 중 예기치 못한 오류가 발생 했습니다. (Error Code. 7)";
                    }
                    break;
                case "violations":
                    string violationPattern = @"AConZGameMode::BanPlayerById:\sUser id:\s'(\d{17})'";

                    var violationMatch = Regex.Match(data, violationPattern);

                    if (violationMatch.Success)
                    {
                        pData += $"➡️ 계정 정보 - {violationMatch.Groups[1].Value}";
                    }
                    else
                    {
                        pData = $"⚠️ 오류 정보 - 로그 기록 중 예기치 못한 오류가 발생 했습니다. (Error Code. 8)";
                    }
                    break;
            }

            return pData;
        }

        private string ExceptionKey()
        {
            return
                info.Key.Equals("lockpick") ? "gameplay" :
                info.Key;
        }

        private bool ExceptionConditionCheck(string data)
        {
            if (info.Key.Equals("admin") && !data.Contains("Command") && !data.Contains("Location"))
            {
                return false;
            }

            if (info.Key.Equals("login") && !data.Contains("logged in"))
            {
                return false;
            }

            if (info.Key.Equals("lockpick") && !data.Contains("LockpickingMinigame_C") && !data.Contains("BP_DialLockMinigame_C"))
            {
                return false;
            }

            if (info.Key.Equals("vehicle_destruction") && !data.Contains("Destroyed"))
            {
                return false;
            }

            if (info.Key.Equals("kill") && !data.Contains("Died"))
            {
                return false;
            }

            if (info.Key.Equals("violations") && !data.Contains("BanPlayerById"))
            {
                return false;
            }

            return true;
        }
    }
}

🔸Comment

🔹프로그램 목적

➡️ SCUM 게임 서버 관리자가 직접 GPORTAL 사의 FTP 서버에 연결하지 않고도 실시간으로 손쉽게 로그 확인을 할 수 있도록 하기 위해 제작

🔹동작 원리

➡️ SCUM 이라는 게임의 서버 호스팅을 제공하는 GPORTAL 사의 FTP 서버와 연결해 실시간으로 로그 파일을 크롤링 하고 그 데이터를 디스코드 봇을 통해 메세지 형태로 시각화 하는 방식으로 동작

🔹주요 기술

➡️ FTP 연결 라이브러리
➡️ Discord 봇 API
➡️ 데이터 크롤링 및 파싱
➡️ 데이터 캡슐화
➡️ 멀티 쓰레딩
➡️ 자원 비동기화
➡️ 파일 입출력

0개의 댓글