Add project files.
This commit is contained in:
commit
ae9b621931
11 changed files with 880 additions and 0 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto
|
215
.gitignore
vendored
Normal file
215
.gitignore
vendored
Normal file
|
@ -0,0 +1,215 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
|
||||
# JetBrains Rider cache/options directory
|
||||
.idea/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
## TODO: Comment the next line if you want to checkin your
|
||||
## web deploy settings but do note that will include unencrypted
|
||||
## passwords
|
||||
#*.pubxml
|
||||
|
||||
*.publishproj
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
|
||||
# Windows Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
[Ss]tyle[Cc]op.*
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# LightSwitch generated files
|
||||
GeneratedArtifacts/
|
||||
_Pvt_Extensions/
|
||||
ModelManifest.xml
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# satori
|
||||
|
||||
![](https://mikoto.misaka.nl/i/Dw0mMO_WsAI8Dyu.jpg)
|
26
Satori/FutamiCommon.cs
Normal file
26
Satori/FutamiCommon.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SockChatKeepAlive {
|
||||
public class FutamiCommon {
|
||||
[JsonPropertyName("ping")]
|
||||
public int Ping { get; set; }
|
||||
|
||||
[JsonPropertyName("uiharu")]
|
||||
public string Metadata { get; set; }
|
||||
|
||||
[JsonPropertyName("eeprom")]
|
||||
public string Uploads { get; set; }
|
||||
|
||||
[JsonPropertyName("servers")]
|
||||
public string[] Servers { get; set; }
|
||||
|
||||
public static async Task<FutamiCommon> FetchAsync(HttpClient client, string host) {
|
||||
return JsonSerializer.Deserialize<FutamiCommon>(
|
||||
await client.GetByteArrayAsync(host + "/common.json")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
191
Satori/PersistentData.cs
Normal file
191
Satori/PersistentData.cs
Normal file
|
@ -0,0 +1,191 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SockChatKeepAlive {
|
||||
public class PersistentData : IDisposable {
|
||||
private const int MAGIC = 0x0BB0AFDE;
|
||||
private const byte VERSION = 1;
|
||||
|
||||
private Stream Stream { get; }
|
||||
private bool OwnsStream { get; }
|
||||
|
||||
private const int HEADER_SIZE = 0x10;
|
||||
|
||||
private const int SAT_TOTAL_UPTIME = 0x10;
|
||||
private const int SAT_TOTAL_UPTIME_LENGTH = 0x08;
|
||||
private const int AFK_STR = SAT_TOTAL_UPTIME + SAT_TOTAL_UPTIME_LENGTH;
|
||||
private const int AFK_STR_LENGTH = 0x14;
|
||||
private const int GRACEFUL_DISCON = AFK_STR + AFK_STR_LENGTH;
|
||||
|
||||
private readonly long OffsetStart;
|
||||
|
||||
public long SatoriTotalUptime {
|
||||
get => ReadI64(SAT_TOTAL_UPTIME);
|
||||
set => WriteI64(SAT_TOTAL_UPTIME, value);
|
||||
}
|
||||
|
||||
public string AFKString {
|
||||
get => ReadStr(AFK_STR, AFK_STR_LENGTH);
|
||||
set => WriteStr(AFK_STR, AFK_STR_LENGTH, value);
|
||||
}
|
||||
|
||||
public bool WasGracefulDisconnect {
|
||||
get => ReadU1(GRACEFUL_DISCON);
|
||||
set => WriteU1(GRACEFUL_DISCON, value);
|
||||
}
|
||||
|
||||
private ArrayPool<byte> ArrayPool { get; } = ArrayPool<byte>.Shared;
|
||||
|
||||
public PersistentData(string fileName)
|
||||
: this(
|
||||
new FileStream(
|
||||
fileName ?? throw new ArgumentNullException(nameof(fileName)),
|
||||
FileMode.OpenOrCreate,
|
||||
FileAccess.ReadWrite,
|
||||
FileShare.Read
|
||||
),
|
||||
true
|
||||
) { }
|
||||
|
||||
public PersistentData(Stream stream, bool ownsStream = false) {
|
||||
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
OwnsStream = ownsStream;
|
||||
if(!stream.CanRead)
|
||||
throw new ArgumentException("Stream must be readable.", nameof(stream));
|
||||
if(!stream.CanSeek)
|
||||
throw new ArgumentException("Stream must be seekable.", nameof(stream));
|
||||
if(!stream.CanWrite)
|
||||
throw new ArgumentException("Stream must be writable.", nameof(stream));
|
||||
|
||||
OffsetStart = stream.Position;
|
||||
|
||||
byte[] buffer = ArrayPool.Rent(HEADER_SIZE);
|
||||
int read;
|
||||
|
||||
try {
|
||||
read = stream.Read(buffer, 0, HEADER_SIZE);
|
||||
|
||||
if(read > 0) {
|
||||
if(BitConverter.ToInt32(buffer, 0) != MAGIC)
|
||||
throw new ArgumentException("Stream does not contain a valid persistent data file structure: invalid magic number.", nameof(stream));
|
||||
if(buffer[5] is < 1 or > VERSION)
|
||||
throw new ArgumentException("Stream does not contain a valid persistent data file structure: unsupported version.", nameof(stream));
|
||||
}
|
||||
} finally {
|
||||
ArrayPool.Return(buffer);
|
||||
}
|
||||
|
||||
if(read < 1) {
|
||||
Stream.Seek(OffsetStart, SeekOrigin.Begin);
|
||||
Stream.Write(BitConverter.GetBytes(MAGIC));
|
||||
// intentionally incompatible with satori
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(VERSION);
|
||||
// lol
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(0);
|
||||
Stream.WriteByte(0);
|
||||
Stream.Flush();
|
||||
} else if(read < HEADER_SIZE)
|
||||
throw new ArgumentException("Stream does not contain a valid persistent data file structure: not enough data.", nameof(stream));
|
||||
}
|
||||
|
||||
private void Seek(long address) {
|
||||
Stream.Seek(OffsetStart + address, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
private string ReadStr(long address, int length) {
|
||||
byte[] buffer = ArrayPool.Rent(length);
|
||||
try {
|
||||
Seek(address);
|
||||
Stream.Read(buffer, 0, length);
|
||||
return Encoding.UTF8.GetString(buffer).Trim('\0'); // retarded
|
||||
} finally {
|
||||
ArrayPool.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ReadU1(long address) {
|
||||
return ReadU8(address) > 0;
|
||||
}
|
||||
|
||||
private byte ReadU8(long address) {
|
||||
Seek(address);
|
||||
int value = Stream.ReadByte();
|
||||
return (byte)(value < 1 ? 0 : value);
|
||||
}
|
||||
|
||||
private long ReadI64(long address) {
|
||||
byte[] buffer = ArrayPool.Rent(8);
|
||||
try {
|
||||
Seek(address);
|
||||
return Stream.Read(buffer) < 8 ? 0 : BitConverter.ToInt64(buffer);
|
||||
} finally {
|
||||
ArrayPool.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteStr(long address, int length, string value) {
|
||||
value ??= string.Empty;
|
||||
if(Encoding.UTF8.GetByteCount(value) > length)
|
||||
throw new ArgumentException("Value exceeds maximum length.");
|
||||
|
||||
byte[] buffer = Encoding.UTF8.GetBytes(value);
|
||||
int difference = length - buffer.Length;
|
||||
|
||||
Seek(address);
|
||||
Stream.Write(Encoding.UTF8.GetBytes(value));
|
||||
|
||||
while(difference-- > 0)
|
||||
Stream.WriteByte(0);
|
||||
}
|
||||
|
||||
private void WriteU1(long address, bool value) {
|
||||
WriteU8(address, (byte)(value ? 0x35 : 0));
|
||||
}
|
||||
private void WriteU8(long address, byte number) {
|
||||
Seek(address);
|
||||
Stream.WriteByte(number);
|
||||
}
|
||||
|
||||
private void WriteI64(long address, long number) {
|
||||
Seek(address);
|
||||
Stream.Write(BitConverter.GetBytes(number));
|
||||
}
|
||||
|
||||
public void Flush() {
|
||||
Stream.Flush();
|
||||
}
|
||||
|
||||
private bool IsDisposed;
|
||||
|
||||
~PersistentData() {
|
||||
DoDispose();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
DoDispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void DoDispose() {
|
||||
if(IsDisposed)
|
||||
return;
|
||||
IsDisposed = true;
|
||||
|
||||
Flush();
|
||||
|
||||
if(OwnsStream)
|
||||
Stream.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
80
Satori/Program.cs
Normal file
80
Satori/Program.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SockChatKeepAlive {
|
||||
public static class Program {
|
||||
public const string PERSIST_FILE = "Persist.dat";
|
||||
public const string AUTH_TOKEN = "AuthToken.txt";
|
||||
|
||||
public const string STORAGE_DIR_NAME = ".sochkeal";
|
||||
|
||||
public static readonly ManualResetEvent ManualReset = new(false);
|
||||
|
||||
public static string StorageDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), STORAGE_DIR_NAME);
|
||||
|
||||
public static async Task Main() {
|
||||
Console.WriteLine("Starting Sock Chat Keep Alive...");
|
||||
|
||||
if(!Directory.Exists(StorageDirectory)) {
|
||||
Console.WriteLine("Storage directory does not exist, it will be created...");
|
||||
Directory.CreateDirectory(StorageDirectory).Attributes |= FileAttributes.Hidden;
|
||||
}
|
||||
|
||||
string authTokenPath = Path.Combine(StorageDirectory, AUTH_TOKEN);
|
||||
if(!File.Exists(authTokenPath)) {
|
||||
File.WriteAllLines(authTokenPath, new[] {
|
||||
"Misuzu",
|
||||
"Token goes here",
|
||||
"Futami shared url goes here"
|
||||
});
|
||||
Console.WriteLine("Auth token file not found! Configure it at:");
|
||||
Console.WriteLine(authTokenPath);
|
||||
return;
|
||||
}
|
||||
|
||||
string[] getAuthInfo() { return File.ReadAllLines(authTokenPath); };
|
||||
|
||||
using ManualResetEvent mre = new(false);
|
||||
bool hasCancelled = false;
|
||||
|
||||
void cancelKeyPressHandler(object sender, ConsoleCancelEventArgs ev) {
|
||||
Console.CancelKeyPress -= cancelKeyPressHandler;
|
||||
hasCancelled = true;
|
||||
ev.Cancel = true;
|
||||
mre.Set();
|
||||
};
|
||||
Console.CancelKeyPress += cancelKeyPressHandler;
|
||||
|
||||
if(hasCancelled) return;
|
||||
|
||||
using HttpClient httpClient = new();
|
||||
httpClient.DefaultRequestHeaders.Add("Accept-Language", "en-GB,en;q=0.5");
|
||||
httpClient.DefaultRequestHeaders.Add("DNT", "1");
|
||||
httpClient.DefaultRequestHeaders.Add("User-Agent", "SockChatKeepAlive/20230319 (+https://fii.moe/beans)");
|
||||
|
||||
if(hasCancelled) return;
|
||||
|
||||
Console.WriteLine("Loading persistent data file...");
|
||||
using PersistentData persist = new(Path.Combine(StorageDirectory, PERSIST_FILE));
|
||||
|
||||
if(hasCancelled) return;
|
||||
|
||||
Console.WriteLine("Loading Futami common...");
|
||||
FutamiCommon common = await FutamiCommon.FetchAsync(httpClient, getAuthInfo()[2]);
|
||||
|
||||
if(hasCancelled) return;
|
||||
|
||||
Console.WriteLine("Connecting to Sock Chat server...");
|
||||
using SockChatClient client = new(common, persist, getAuthInfo);
|
||||
client.Connect();
|
||||
|
||||
mre.WaitOne();
|
||||
persist.WasGracefulDisconnect = false;
|
||||
|
||||
Console.WriteLine(@"Bye!");
|
||||
}
|
||||
}
|
||||
}
|
19
Satori/Properties/PublishProfiles/FolderProfile.pubxml
Normal file
19
Satori/Properties/PublishProfiles/FolderProfile.pubxml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net6.0\publish\win-x64\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
32
Satori/RNG.cs
Normal file
32
Satori/RNG.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
|
||||
namespace SockChatKeepAlive {
|
||||
public static class RNG {
|
||||
private static readonly Random random = new();
|
||||
|
||||
public static int Next() {
|
||||
lock(random)
|
||||
return random.Next();
|
||||
}
|
||||
|
||||
public static int Next(int max) {
|
||||
lock(random)
|
||||
return random.Next(max);
|
||||
}
|
||||
|
||||
public static int Next(int min, int max) {
|
||||
lock(random)
|
||||
return random.Next(min, max);
|
||||
}
|
||||
|
||||
public static void NextBytes(byte[] buffer) {
|
||||
lock(random)
|
||||
random.NextBytes(buffer);
|
||||
}
|
||||
|
||||
public static double NextDouble() {
|
||||
lock(random)
|
||||
return random.NextDouble();
|
||||
}
|
||||
}
|
||||
}
|
276
Satori/SockChatClient.cs
Normal file
276
Satori/SockChatClient.cs
Normal file
|
@ -0,0 +1,276 @@
|
|||
using PureWebSockets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
|
||||
namespace SockChatKeepAlive {
|
||||
public sealed class SockChatClient : IDisposable {
|
||||
private readonly FutamiCommon Common;
|
||||
private readonly PersistentData Persist;
|
||||
private readonly Func<string[]> GetAuthInfo;
|
||||
private PureWebSocket WebSocket;
|
||||
private Timer Pinger;
|
||||
|
||||
private readonly object PersistAccess = new();
|
||||
|
||||
public record ChatUser(string Id, string Name, string Colour);
|
||||
private Dictionary<string, ChatUser> Users { get; set; } = new();
|
||||
public ChatUser MyUser { get; private set; }
|
||||
|
||||
private bool IsDisposed = false;
|
||||
|
||||
private DateTimeOffset LastPong = DateTimeOffset.Now;
|
||||
|
||||
public SockChatClient(
|
||||
FutamiCommon common,
|
||||
PersistentData persist,
|
||||
Func<string[]> getAuthInfo
|
||||
) {
|
||||
Common = common;
|
||||
Persist = persist;
|
||||
GetAuthInfo = getAuthInfo;
|
||||
}
|
||||
|
||||
~SockChatClient() {
|
||||
DoDispose();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
DoDispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void DoDispose() {
|
||||
if(IsDisposed)
|
||||
return;
|
||||
IsDisposed = true;
|
||||
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
public void Connect() {
|
||||
string server = Common.Servers[RNG.Next(Common.Servers.Length)];
|
||||
if(server.StartsWith("//"))
|
||||
server = "wss:" + server;
|
||||
Console.WriteLine($"Connecting to {server}...");
|
||||
|
||||
Disconnect();
|
||||
WebSocket = new PureWebSocket(server, new PureWebSocketOptions {
|
||||
SubProtocols = new[] { "sockchat" },
|
||||
});
|
||||
WebSocket.OnOpened += WebSocket_OnOpen;
|
||||
WebSocket.OnClosed += WebSocket_OnClose;
|
||||
WebSocket.OnMessage += WebSocket_OnMessage;
|
||||
WebSocket.Connect();
|
||||
}
|
||||
|
||||
public void Disconnect() {
|
||||
MyUser = null;
|
||||
|
||||
if(Pinger != null) {
|
||||
Pinger.Dispose();
|
||||
Pinger = null;
|
||||
}
|
||||
|
||||
if(WebSocket == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
WebSocket.OnOpened -= WebSocket_OnOpen;
|
||||
WebSocket.OnClosed -= WebSocket_OnClose;
|
||||
WebSocket.OnMessage -= WebSocket_OnMessage;
|
||||
WebSocket.Disconnect();
|
||||
WebSocket = null;
|
||||
} catch { }
|
||||
}
|
||||
|
||||
public void SendMessage(string text) {
|
||||
if(MyUser == null)
|
||||
return;
|
||||
|
||||
Send("2", MyUser.Id, text.Replace("\t", " "));
|
||||
}
|
||||
|
||||
public void SendMessage(object obj)
|
||||
=> SendMessage(obj.ToString());
|
||||
|
||||
public void SendPing() {
|
||||
if(MyUser != null)
|
||||
Send("0", MyUser.Id);
|
||||
}
|
||||
|
||||
public void Send(params object[] args) {
|
||||
WebSocket.Send(string.Join("\t", args));
|
||||
}
|
||||
|
||||
private void WebSocket_OnOpen(object sender) {
|
||||
Console.WriteLine("WebSocket connected.");
|
||||
Console.WriteLine();
|
||||
|
||||
string[] authInfo = GetAuthInfo();
|
||||
Send("1", authInfo[0] ?? string.Empty, authInfo[1] ?? string.Empty);
|
||||
}
|
||||
|
||||
private void WebSocket_OnClose(object sender, WebSocketCloseStatus reason) {
|
||||
Console.WriteLine($"WebSocket disconnected: {reason}");
|
||||
Disconnect();
|
||||
|
||||
if(!IsDisposed) {
|
||||
lock(PersistAccess)
|
||||
Persist.WasGracefulDisconnect = false;
|
||||
|
||||
Thread.Sleep(10000);
|
||||
Connect();
|
||||
}
|
||||
}
|
||||
|
||||
private void WebSocket_OnMessage(object sender, string data) {
|
||||
string[] args = data.Split('\t');
|
||||
if(args.Length < 1)
|
||||
return;
|
||||
|
||||
switch(args[0]) {
|
||||
case "0":
|
||||
TimeSpan pongDiff = DateTimeOffset.Now - LastPong;
|
||||
LastPong = DateTimeOffset.Now;
|
||||
lock(PersistAccess)
|
||||
Persist.SatoriTotalUptime += (long)pongDiff.TotalMilliseconds;
|
||||
break;
|
||||
|
||||
case "1":
|
||||
if(MyUser == null) {
|
||||
if(args[1] == "y") {
|
||||
Pinger = new Timer(x => SendPing(), null, 0, Common.Ping * 1000);
|
||||
} else {
|
||||
Disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Users[args[2]] = new(args[2], args[3], args[4]);
|
||||
|
||||
if(MyUser == null) {
|
||||
MyUser = Users[args[2]];
|
||||
|
||||
lock(PersistAccess) {
|
||||
if(Persist.WasGracefulDisconnect)
|
||||
Persist.AFKString = MyUser.Name.StartsWith("<")
|
||||
? MyUser.Name[4..MyUser.Name.IndexOf(">_")]
|
||||
: string.Empty;
|
||||
else {
|
||||
string afkString = Persist.AFKString;
|
||||
if(!string.IsNullOrWhiteSpace(afkString))
|
||||
SendMessage($"/afk {afkString}");
|
||||
}
|
||||
}
|
||||
} else
|
||||
Console.WriteLine($"!! {args[2]} <{args[3]}> joined.");
|
||||
break;
|
||||
|
||||
case "2":
|
||||
Console.Write($":: {{{args[4]}}} [{DateTimeOffset.FromUnixTimeSeconds(long.Parse(args[1])):G}] ");
|
||||
if(Users.ContainsKey(args[2]))
|
||||
Console.Write($"{Users[args[2]].Id} <{Users[args[2]].Name}>");
|
||||
else
|
||||
Console.Write("*");
|
||||
Console.WriteLine();
|
||||
|
||||
foreach(string line in args[3].Split(" <br/> ")) {
|
||||
Console.Write("> ");
|
||||
Console.WriteLine(line.Replace('\f', '\t').Trim());
|
||||
}
|
||||
Console.WriteLine();
|
||||
break;
|
||||
|
||||
case "3":
|
||||
Users.Remove(args[1]);
|
||||
Console.WriteLine($"!! {args[1]} left.");
|
||||
break;
|
||||
|
||||
case "5":
|
||||
switch(args[1]) {
|
||||
case "0":
|
||||
Users[args[2]] = new(args[2], args[3], args[4]);
|
||||
Console.WriteLine($"!! {args[2]} <{args[3]}> entered channel.");
|
||||
break;
|
||||
|
||||
case "1":
|
||||
Users.Remove(args[2]);
|
||||
Console.WriteLine($"!! {args[2]} switched channel.");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "6":
|
||||
Console.WriteLine($"!! Message {args[1]} was deleted.");
|
||||
break;
|
||||
|
||||
case "7":
|
||||
if(args.Length < 2)
|
||||
break;
|
||||
|
||||
switch(args[1]) {
|
||||
case "0":
|
||||
if(args.Length < 3 || !int.TryParse(args[2], out int amount))
|
||||
break;
|
||||
|
||||
int offset = 3;
|
||||
for(int i = 0; i < amount; i++) {
|
||||
Users[args[offset++]] = new(args[offset], args[offset++], args[offset++]);
|
||||
offset += 2;
|
||||
}
|
||||
break;
|
||||
|
||||
case "1":
|
||||
Console.Write($":: {{{args[8]}}} [{DateTimeOffset.FromUnixTimeSeconds(long.Parse(args[2])):G}] ");
|
||||
if(args[3].Equals("-1", StringComparison.Ordinal))
|
||||
Console.Write("*");
|
||||
else
|
||||
Console.Write($"{args[3]} <{args[4]}>");
|
||||
Console.WriteLine();
|
||||
|
||||
foreach(string line in args[7].Split(" <br/> ")) {
|
||||
Console.Write("> ");
|
||||
Console.WriteLine(line.Replace('\f', '\t').Trim());
|
||||
}
|
||||
Console.WriteLine();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "8":
|
||||
if(args[1] is "0" or "4")
|
||||
Console.WriteLine("!! Message list cleared");
|
||||
|
||||
if(args[1] is "1" or "3" or "4") {
|
||||
Console.WriteLine("!! User list cleared");
|
||||
Users.Clear();
|
||||
Users.Add(MyUser.Id, MyUser);
|
||||
}
|
||||
|
||||
if(args[1] is "2" or "3" or "4")
|
||||
Console.WriteLine("!! Channel list cleared");
|
||||
break;
|
||||
|
||||
case "9":
|
||||
Console.WriteLine($"!! Kicked from server: {args[1]} {args[2]}");
|
||||
break;
|
||||
|
||||
case "10":
|
||||
Users[args[1]] = new(args[1], args[2], args[3]);
|
||||
Console.WriteLine($"!! {args[1]} updated: {args[2]}.");
|
||||
|
||||
if(MyUser.Id == args[1]) {
|
||||
MyUser = Users[args[1]];
|
||||
|
||||
lock(PersistAccess)
|
||||
Persist.AFKString = MyUser.Name.StartsWith("<")
|
||||
? MyUser.Name[4..MyUser.Name.IndexOf(">_")]
|
||||
: string.Empty;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
Satori/SockChatKeepAlive.csproj
Normal file
12
Satori/SockChatKeepAlive.csproj
Normal file
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PureWebSockets" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
25
SockChatKeepAlive.sln
Normal file
25
SockChatKeepAlive.sln
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.33502.453
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SockChatKeepAlive", "Satori\SockChatKeepAlive.csproj", "{7917878E-6D5F-4793-85E0-2D8201F46EEF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{7917878E-6D5F-4793-85E0-2D8201F46EEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7917878E-6D5F-4793-85E0-2D8201F46EEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7917878E-6D5F-4793-85E0-2D8201F46EEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7917878E-6D5F-4793-85E0-2D8201F46EEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {CE6FDC89-9A7B-43B6-895C-CE5F0C4D6087}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
Loading…
Reference in a new issue