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