Add project files.

This commit is contained in:
flash 2023-06-28 22:12:23 +02:00
commit ae9b621931
11 changed files with 880 additions and 0 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto

215
.gitignore vendored Normal file
View 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
View file

@ -0,0 +1,3 @@
# satori
![](https://mikoto.misaka.nl/i/Dw0mMO_WsAI8Dyu.jpg)

26
Satori/FutamiCommon.cs Normal file
View 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
View 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
View 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!");
}
}
}

View 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
View 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
View 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("&lt;")
? MyUser.Name[4..MyUser.Name.IndexOf("&gt;_")]
: 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("&lt;")
? MyUser.Name[4..MyUser.Name.IndexOf("&gt;_")]
: string.Empty;
}
break;
}
}
}
}

View 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
View 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