all the shit
This commit is contained in:
commit
41834c461e
15 changed files with 1084 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
|
.idea/
|
22
CollabVMSharp.sln
Normal file
22
CollabVMSharp.sln
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CollabVMSharp", "CollabVMSharp\CollabVMSharp.csproj", "{71E96501-6FF7-48B4-BCB5-4D777ED300D2}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cvmsharptest", "..\cvmsharptest\cvmsharptest\cvmsharptest.csproj", "{3285A3D2-F8CB-43D6-93BC-61FB3C73F990}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{71E96501-6FF7-48B4-BCB5-4D777ED300D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{71E96501-6FF7-48B4-BCB5-4D777ED300D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{71E96501-6FF7-48B4-BCB5-4D777ED300D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{71E96501-6FF7-48B4-BCB5-4D777ED300D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3285A3D2-F8CB-43D6-93BC-61FB3C73F990}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3285A3D2-F8CB-43D6-93BC-61FB3C73F990}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3285A3D2-F8CB-43D6-93BC-61FB3C73F990}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3285A3D2-F8CB-43D6-93BC-61FB3C73F990}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
6
CollabVMSharp/ChatMessage.cs
Normal file
6
CollabVMSharp/ChatMessage.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
|
||||||
|
public class ChatMessage {
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
}
|
652
CollabVMSharp/CollabVMClient.cs
Normal file
652
CollabVMSharp/CollabVMClient.cs
Normal file
|
@ -0,0 +1,652 @@
|
||||||
|
#nullable enable
|
||||||
|
#pragma warning disable CS4014
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Security.Authentication;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
// ReSharper disable FieldCanBeMadeReadOnly.Local
|
||||||
|
// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident
|
||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
// ReSharper disable ArrangeObjectCreationWhenTypeEvident
|
||||||
|
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
public class CollabVMClient {
|
||||||
|
// Fields
|
||||||
|
private Uri url;
|
||||||
|
private string? username;
|
||||||
|
private string? node;
|
||||||
|
private Rank _rank;
|
||||||
|
private Permissions _perms;
|
||||||
|
private bool _connected;
|
||||||
|
private bool _connectedToVM;
|
||||||
|
private ClientWebSocket socket;
|
||||||
|
private List<User> _users;
|
||||||
|
private Timer NOPRecieve;
|
||||||
|
private Image framebuffer;
|
||||||
|
private TurnStatus _turnStatus;
|
||||||
|
private Mouse mouse;
|
||||||
|
// Tasks
|
||||||
|
private TaskCompletionSource<Node[]> GotNodeList;
|
||||||
|
private TaskCompletionSource<bool> GotConnectionToNode;
|
||||||
|
private TaskCompletionSource<int> GotTurn;
|
||||||
|
private TaskCompletionSource<Rank> GotStaff;
|
||||||
|
private List<GetIPTask> GotIPTasks;
|
||||||
|
// Properties
|
||||||
|
public Rank Rank { get { return this._rank; } }
|
||||||
|
public Permissions Permissions { get { return this._perms; } }
|
||||||
|
public bool Connected { get { return this._connected; } }
|
||||||
|
public bool ConnectedToVM { get { return this._connectedToVM; } }
|
||||||
|
public User[] Users => _users.ToArray();
|
||||||
|
public TurnStatus TurnStatus { get { return this._turnStatus; } }
|
||||||
|
// Events
|
||||||
|
public event EventHandler<ChatMessage> Chat;
|
||||||
|
public event EventHandler<ChatMessage[]> ChatHistory;
|
||||||
|
public event EventHandler ConnectedToNode;
|
||||||
|
public event EventHandler NodeConnectFailed;
|
||||||
|
public event EventHandler<RectEventArgs> Rect;
|
||||||
|
public event EventHandler<string> Renamed;
|
||||||
|
public event EventHandler<UserRenamedEventArgs> UserRenamed;
|
||||||
|
public event EventHandler<User> UserJoined;
|
||||||
|
public event EventHandler<User> UserLeft;
|
||||||
|
public event EventHandler<VoteUpdateEventArgs> VoteUpdate;
|
||||||
|
public event EventHandler VoteEnded;
|
||||||
|
public event EventHandler<int> VoteCooldown;
|
||||||
|
public event EventHandler<TurnUpdateEventArgs> TurnUpdate;
|
||||||
|
public event EventHandler ConnectionClosed;
|
||||||
|
/// <summary>
|
||||||
|
/// Client for the CollabVM 1.x Server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">URL of the CollabVM Server to connect to (Should start with ws:// or wss://)</param>
|
||||||
|
/// <param name="username">Username to join the VM as. If null, the server will assign a guest name.</param>
|
||||||
|
/// <param name="node">Node to connect to. If null, a VM will not be automatically joined.</param>
|
||||||
|
/// <param name="proxy">HTTP proxy to connect with. If null, a proxy will not be used.</param>
|
||||||
|
public CollabVMClient(string url, string? username = null, string? node = null, string? proxy = null) {
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out this.url)) {
|
||||||
|
throw new UriFormatException("An invalid URI string was passed.");
|
||||||
|
}
|
||||||
|
if (this.url.Scheme != "ws" && this.url.Scheme != "wss") {
|
||||||
|
throw new UriFormatException("The URL must have a valid websocket scheme (ws or wss)");
|
||||||
|
}
|
||||||
|
this.username = username;
|
||||||
|
this.node = node;
|
||||||
|
this._rank = Rank.Unregistered;
|
||||||
|
this._perms = Permissions.None;
|
||||||
|
this._connected = false;
|
||||||
|
this._connectedToVM = false;
|
||||||
|
this.framebuffer = new Image<Rgba32>(1, 1);
|
||||||
|
this.socket = new();
|
||||||
|
this.socket.Options.AddSubProtocol("guacamole");
|
||||||
|
this.socket.Options.SetRequestHeader("Origin", "https://computernewb.com");
|
||||||
|
if (proxy != null) {
|
||||||
|
this.socket.Options.Proxy = new WebProxy(proxy);
|
||||||
|
}
|
||||||
|
this.NOPRecieve = new(10000);
|
||||||
|
this.NOPRecieve.AutoReset = false;
|
||||||
|
this.NOPRecieve.Elapsed += delegate { this.Disconnect(); };
|
||||||
|
this._users = new();
|
||||||
|
this.mouse = new();
|
||||||
|
this.GotNodeList = new();
|
||||||
|
this.GotConnectionToNode = new();
|
||||||
|
this.GotTurn = new();
|
||||||
|
this.GotStaff = new();
|
||||||
|
this.GotIPTasks = new();
|
||||||
|
// Assign empty handlers
|
||||||
|
Chat += delegate { };
|
||||||
|
ChatHistory += delegate { };
|
||||||
|
ConnectedToNode += delegate { };
|
||||||
|
NodeConnectFailed += delegate { };
|
||||||
|
Rect += delegate { };
|
||||||
|
Renamed += delegate { };
|
||||||
|
UserRenamed += delegate { };
|
||||||
|
UserJoined += delegate { };
|
||||||
|
UserLeft += delegate { };
|
||||||
|
VoteUpdate += delegate { };
|
||||||
|
VoteEnded += delegate { };
|
||||||
|
VoteCooldown += delegate { };
|
||||||
|
TurnUpdate += delegate { };
|
||||||
|
ConnectionClosed += delegate { };
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Connect to the CollabVM Server
|
||||||
|
/// </summary>
|
||||||
|
public async Task Connect() {
|
||||||
|
await this.socket.ConnectAsync(this.url, CancellationToken.None);
|
||||||
|
this._connected = true;
|
||||||
|
if (this.username != null)
|
||||||
|
this.SendMsg(Guacutils.Encode("rename", this.username));
|
||||||
|
else
|
||||||
|
this.SendMsg(Guacutils.Encode("rename"));
|
||||||
|
if (this.node != null)
|
||||||
|
this.SendMsg(Guacutils.Encode("connect", this.node));
|
||||||
|
this.NOPRecieve.Start();
|
||||||
|
this.WebSocketLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void WebSocketLoop() {
|
||||||
|
ArraySegment<byte> receivebuffer = new ArraySegment<byte>(new byte[8192]);
|
||||||
|
do {
|
||||||
|
MemoryStream ms = new();
|
||||||
|
WebSocketReceiveResult res;
|
||||||
|
do {
|
||||||
|
res = await socket.ReceiveAsync(receivebuffer, CancellationToken.None);
|
||||||
|
if (res.MessageType == WebSocketMessageType.Close) {
|
||||||
|
this.Disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ms.WriteAsync(receivebuffer.Array, 0, res.Count);
|
||||||
|
} while (!res.EndOfMessage);
|
||||||
|
string msg;
|
||||||
|
try {
|
||||||
|
msg = Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
} catch (Exception e) {
|
||||||
|
#if DEBUG
|
||||||
|
await Console.Error.WriteLineAsync($"Failed to read message from socket: {e.Message}");
|
||||||
|
#endif
|
||||||
|
continue;
|
||||||
|
} finally {ms.Dispose();}
|
||||||
|
this.ProcessMessage(msg);
|
||||||
|
} while (socket.State == WebSocketState.Open);
|
||||||
|
this.Cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ProcessMessage(string msg) {
|
||||||
|
string[] msgArr;
|
||||||
|
try {
|
||||||
|
msgArr = Guacutils.Decode(msg);
|
||||||
|
} catch (Exception e) {
|
||||||
|
#if DEBUG
|
||||||
|
await Console.Error.WriteLineAsync($"Failed to decode incoming message: {e.Message}");
|
||||||
|
#endif
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msgArr.Length < 1) return;
|
||||||
|
this.NOPRecieve.Stop();
|
||||||
|
this.NOPRecieve.Interval = 10000;
|
||||||
|
this.NOPRecieve.Start();
|
||||||
|
switch (msgArr[0]) {
|
||||||
|
case "nop": {
|
||||||
|
this.SendMsg("3.nop;");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "connect": {
|
||||||
|
switch (msgArr[1]) {
|
||||||
|
case "0":
|
||||||
|
this.NodeConnectFailed.Invoke(this, EventArgs.Empty);
|
||||||
|
this.GotConnectionToNode.TrySetResult(false);
|
||||||
|
break;
|
||||||
|
case "1":
|
||||||
|
this._connectedToVM = true;
|
||||||
|
this.ConnectedToNode.Invoke(this, EventArgs.Empty);
|
||||||
|
this.GotConnectionToNode.TrySetResult(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "chat": {
|
||||||
|
if (msgArr.Length > 3) {
|
||||||
|
List<ChatMessage> msgs = new();
|
||||||
|
for (int i = 1; i < msgArr.Length; i += 2) {
|
||||||
|
msgs.Add(new ChatMessage {
|
||||||
|
Username = msgArr[i],
|
||||||
|
Message = WebUtility.HtmlDecode(msgArr[i+1])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ChatHistory.Invoke(this, msgs.ToArray());
|
||||||
|
// I should probably add a config option for whether or not the message should be HTML encoded
|
||||||
|
} else Chat.Invoke(this, new ChatMessage {Username = msgArr[1], Message = WebUtility.HtmlDecode(msgArr[2])});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "captcha": {
|
||||||
|
throw new Exception("This VM requires a captcha to connect to. Please do not attempt to bypass this. Contact the CollabVM admins (or the owner of the server you're connecting to) to request to be added to the bot whitelist.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "list": {
|
||||||
|
List<Node> nodes = new();
|
||||||
|
for (var i = 1; i < msgArr.Length; i += 3) {
|
||||||
|
nodes.Add(new Node {
|
||||||
|
ID = msgArr[i],
|
||||||
|
Name = msgArr[i+1],
|
||||||
|
Thumbnail = Convert.FromBase64String(msgArr[i+2])
|
||||||
|
});
|
||||||
|
this.GotNodeList.TrySetResult(nodes.ToArray());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "size": {
|
||||||
|
if (msgArr[1] != "0") return;
|
||||||
|
this.framebuffer = new Image<Rgba32>(int.Parse(msgArr[2]), int.Parse(msgArr[3]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "png": {
|
||||||
|
if (msgArr[2] != "0") return;
|
||||||
|
Image rect = Image.Load(Convert.FromBase64String(msgArr[5]));
|
||||||
|
framebuffer.Mutate(f => f.DrawImage(rect, new Point(int.Parse(msgArr[3]), int.Parse(msgArr[4])), 1));
|
||||||
|
this.Rect.Invoke(this, new RectEventArgs {
|
||||||
|
X = int.Parse(msgArr[3]),
|
||||||
|
Y = int.Parse(msgArr[4]),
|
||||||
|
Data = rect,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "rename": {
|
||||||
|
switch (msgArr[1]) {
|
||||||
|
case "0": {
|
||||||
|
var user = _users.Find(u => u.Username == username);
|
||||||
|
if (user != null) {
|
||||||
|
user.Username = msgArr[3];
|
||||||
|
}
|
||||||
|
this.username = msgArr[3];
|
||||||
|
this.Renamed.Invoke(this, msgArr[3]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
var user = _users.Find(u => u.Username == msgArr[2]);
|
||||||
|
user.Username = msgArr[3];
|
||||||
|
this.UserRenamed.Invoke(this, new UserRenamedEventArgs {
|
||||||
|
OldName = msgArr[2],
|
||||||
|
NewName = msgArr[3],
|
||||||
|
User = user
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "adduser": {
|
||||||
|
for (int i = 2; i < msgArr.Length; i += 2) {
|
||||||
|
// This can happen when a user logs in
|
||||||
|
_users.RemoveAll(u => u.Username == msgArr[i]);
|
||||||
|
var user = new User {
|
||||||
|
Username = msgArr[i],
|
||||||
|
Rank = msgArr[i + 1] switch {
|
||||||
|
"0" => Rank.Unregistered,
|
||||||
|
"2" => Rank.Admin,
|
||||||
|
"3" => Rank.Moderator,
|
||||||
|
_ => Rank.Unregistered
|
||||||
|
},
|
||||||
|
Turn = TurnStatus.None
|
||||||
|
};
|
||||||
|
this.UserJoined.Invoke(this, user);
|
||||||
|
this._users.Add(user);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "remuser": {
|
||||||
|
for (int i = 2; i < msgArr.Length; i++) {
|
||||||
|
var user = this._users.Find(u=>u.Username==msgArr[i]);
|
||||||
|
this._users.Remove(user);
|
||||||
|
UserLeft.Invoke(this, user);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "vote": {
|
||||||
|
switch (msgArr[1]) {
|
||||||
|
case "0":
|
||||||
|
case "1":
|
||||||
|
this.VoteUpdate.Invoke(this, new VoteUpdateEventArgs {
|
||||||
|
No = int.Parse(msgArr[4]),
|
||||||
|
Yes = int.Parse(msgArr[3]),
|
||||||
|
Status = msgArr[1] switch {"0" => VoteStatus.Started, "1" => VoteStatus.Update},
|
||||||
|
TimeToVoteEnd = int.Parse(msgArr[2])
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "2":
|
||||||
|
this.VoteEnded.Invoke(this, EventArgs.Empty);
|
||||||
|
break;
|
||||||
|
case "3":
|
||||||
|
this.VoteCooldown.Invoke(this, int.Parse(msgArr[2]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "turn": {
|
||||||
|
List<User> queue = new();
|
||||||
|
// Reset turn data
|
||||||
|
this._users.ForEach(u => u.Turn = TurnStatus.None);
|
||||||
|
this._turnStatus = TurnStatus.None;
|
||||||
|
int queuedUsers = int.Parse(msgArr[2]);
|
||||||
|
if (queuedUsers == 0) {
|
||||||
|
this.TurnUpdate.Invoke(this, new TurnUpdateEventArgs {
|
||||||
|
Queue = Array.Empty<User>(),
|
||||||
|
TurnTimer = null,
|
||||||
|
QueueTimer = null
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var currentTurnUser = _users.First(u => u.Username == msgArr[3]);
|
||||||
|
if (msgArr[3] == this.username) {
|
||||||
|
this._turnStatus = TurnStatus.HasTurn;
|
||||||
|
this.GotTurn.TrySetResult(int.Parse(msgArr[1]));
|
||||||
|
}
|
||||||
|
currentTurnUser.Turn = TurnStatus.HasTurn;
|
||||||
|
queue.Add(currentTurnUser);
|
||||||
|
if (queuedUsers > 1) {
|
||||||
|
for (int i = 1; i < queuedUsers; i++) {
|
||||||
|
var user = _users.Find(u => u.Username == msgArr[i + 3]);
|
||||||
|
user.Turn = TurnStatus.Waiting;
|
||||||
|
if (msgArr[i + 3] == this.username)
|
||||||
|
this._turnStatus = TurnStatus.Waiting;
|
||||||
|
queue.Add(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.TurnUpdate.Invoke(this, new TurnUpdateEventArgs {
|
||||||
|
Queue = queue.ToArray(),
|
||||||
|
TurnTimer = (this._turnStatus == TurnStatus.HasTurn) ? int.Parse(msgArr[1]) : null,
|
||||||
|
QueueTimer = (this._turnStatus == TurnStatus.Waiting) ? int.Parse(msgArr[msgArr.Length-1]) : null,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "admin": {
|
||||||
|
switch (msgArr[1]) {
|
||||||
|
case "0": {
|
||||||
|
switch (msgArr[2]) {
|
||||||
|
case "0":
|
||||||
|
throw new InvalidCredentialException("The provided password was incorrect.");
|
||||||
|
break;
|
||||||
|
case "1":
|
||||||
|
this._rank = Rank.Admin;
|
||||||
|
this._perms = Permissions.All;
|
||||||
|
this.GotStaff.TrySetResult(Rank.Admin);
|
||||||
|
break;
|
||||||
|
case "3":
|
||||||
|
this._rank = Rank.Moderator;
|
||||||
|
this._perms = new Permissions(int.Parse(msgArr[3]));
|
||||||
|
this.GotStaff.TrySetResult(Rank.Moderator);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "19":
|
||||||
|
var tsk = this.GotIPTasks.Find(x => x.username == msgArr[2]);
|
||||||
|
if (tsk == null) return;
|
||||||
|
tsk.IPTask.TrySetResult(msgArr[3]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Close the connection to the server
|
||||||
|
/// </summary>
|
||||||
|
public async Task Disconnect() {
|
||||||
|
this._connected = false;
|
||||||
|
if (this.socket.State == WebSocketState.Open)
|
||||||
|
await this.SendMsg("10.disconnect;");
|
||||||
|
await this.socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
this.Cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cleanup(bool fireDisconnect = true) {
|
||||||
|
this._users.Clear();
|
||||||
|
this._connected = false;
|
||||||
|
this._connectedToVM = false;
|
||||||
|
this._rank = Rank.Unregistered;
|
||||||
|
this._perms = Permissions.None;
|
||||||
|
this.NOPRecieve.Stop();
|
||||||
|
this.NOPRecieve.Interval = 10000;
|
||||||
|
if (fireDisconnect)
|
||||||
|
this.ConnectionClosed.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a raw string message over the socket
|
||||||
|
/// </summary>
|
||||||
|
public Task SendMsg(string msg) {
|
||||||
|
if (!this._connected) throw new WebSocketException("Cannot send a message while the socket is closed");
|
||||||
|
return this.socket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(msg)), WebSocketMessageType.Text,
|
||||||
|
true, CancellationToken.None);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Request a list of VMs from the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A list of VMs</returns>
|
||||||
|
public async Task<Node[]> ListVMs() {
|
||||||
|
GotNodeList = new();
|
||||||
|
SendMsg(Guacutils.Encode("list"));
|
||||||
|
return await GotNodeList.Task;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Attempt to connect to a VM
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node">ID of the VM to connect to</param>
|
||||||
|
/// <returns>True if successful, false otherwise</returns>
|
||||||
|
public async Task<bool> ConnectToVM(string node) {
|
||||||
|
if (this._connectedToVM)
|
||||||
|
throw new Exception("Already connected to a node. You must disconnect and reconnect to switch nodes.");
|
||||||
|
this.GotConnectionToNode = new();
|
||||||
|
this.SendMsg(Guacutils.Encode("connect", node));
|
||||||
|
return await this.GotConnectionToNode.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a key to the VM. If you don't have the turn, nothing will happen
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keysym">X11 Keysym of the key to send.</param>
|
||||||
|
/// <param name="down">Whether or not the key is pressed</param>
|
||||||
|
public Task SendKey(int keysym, bool down) => this.SendMsg(Guacutils.Encode("key", keysym.ToString(), down ? "1" : "0"));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move the mouse
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x">Horizontal position or offset of the mouse</param>
|
||||||
|
/// <param name="y">Vertical position or offset of the mouse</param>
|
||||||
|
/// <param name="relative">If true, mouse is moved relative to it's current position. If false, the mouse will be moved to the exact coordinates given</param>
|
||||||
|
public async Task MoveMouse(int x, int y, bool relative = false) {
|
||||||
|
if (relative) {
|
||||||
|
this.mouse.X += x;
|
||||||
|
this.mouse.Y += y;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.mouse.X = x;
|
||||||
|
this.mouse.Y = y;
|
||||||
|
}
|
||||||
|
this.sendMouse();
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Set the pressed mouse buttons to the given mask
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mask">The button mask, representing the pressed or released status of each mouse button.</param>
|
||||||
|
public async Task MouseBtn(int mask) {
|
||||||
|
this.mouse.LoadMask(mask);
|
||||||
|
await this.sendMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Press or release a mouse button
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="btn">The button to change the state of</param>
|
||||||
|
/// <param name="down">True if the button is down, false if it's up</param>
|
||||||
|
public async Task MouseBtn(MouseButton btn, bool down) {
|
||||||
|
switch (btn) {
|
||||||
|
case MouseButton.Left:
|
||||||
|
this.mouse.Left = down;
|
||||||
|
break;
|
||||||
|
case MouseButton.Middle:
|
||||||
|
this.mouse.Middle = down;
|
||||||
|
break;
|
||||||
|
case MouseButton.Right:
|
||||||
|
this.mouse.Right = down;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await this.sendMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type a specified character to the VM
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="c">The character to type</param>
|
||||||
|
/// <param name="down">Whether the key is pressed or released, or null to press and then release</param>
|
||||||
|
public async Task SendChar(char c, bool? down = null) {
|
||||||
|
int keysym = Keyboard.KeyMap[c];
|
||||||
|
if (down != null)
|
||||||
|
await this.SendKey(keysym, (bool)down);
|
||||||
|
else {
|
||||||
|
await this.SendKey(keysym, true);
|
||||||
|
await Task.Delay(20);
|
||||||
|
await this.SendKey(keysym, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Press a special key on the VM
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Key to send</param>
|
||||||
|
/// <param name="down">Whether the key is pressed or released, or null to press and then release</param>
|
||||||
|
public async Task SendSpecialKey(SpecialKey key, bool? down = null) {
|
||||||
|
int keysym = (int)key;
|
||||||
|
if (down != null)
|
||||||
|
await this.SendKey(keysym, (bool)down);
|
||||||
|
else {
|
||||||
|
await this.SendKey(keysym, true);
|
||||||
|
await Task.Delay(20);
|
||||||
|
await this.SendKey(keysym, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type a string into the VM
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str">String to type</param>
|
||||||
|
public async Task TypeString(string str) {
|
||||||
|
foreach (char c in str) {
|
||||||
|
await SendChar(c);
|
||||||
|
await Task.Delay(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Request or cancel a turn
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="take">True to request a turn, false to cancel</param>
|
||||||
|
public Task Turn(bool take) => this.SendMsg(Guacutils.Encode("turn", take ? "1" : "0"));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request a turn and returns once the turn is received.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>How long you have the turn, in milliseconds</returns>
|
||||||
|
public async Task<int> GetTurn() {
|
||||||
|
if (this._turnStatus == TurnStatus.HasTurn)
|
||||||
|
throw new Exception("You already have the turn");
|
||||||
|
this.GotTurn = new();
|
||||||
|
this.Turn(true);
|
||||||
|
return await this.GotTurn.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log in as an Admin or Moderator
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="password">Password to log in with</param>
|
||||||
|
/// <returns>The rank received</returns>
|
||||||
|
public async Task<Rank> Login(string password) {
|
||||||
|
this.GotStaff = new();
|
||||||
|
this.SendMsg(Guacutils.Encode("admin", "2", password));
|
||||||
|
return await this.GotStaff.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a message to the VM chat
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="msg">Message to send</param>`
|
||||||
|
public Task SendChat(string msg) => this.SendMsg(Guacutils.Encode("chat", msg));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restore the VM
|
||||||
|
/// </summary>
|
||||||
|
public async Task Restore() {
|
||||||
|
if (!this._perms.Restore)
|
||||||
|
throw new NoPermissionException("Restore VM");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("Restore VM");
|
||||||
|
await this.SendMsg(Guacutils.Encode("admin", "8", this.node));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Reboot() {
|
||||||
|
if (!this._perms.Reboot)
|
||||||
|
throw new NoPermissionException("Reboot VM");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("Reboot VM");
|
||||||
|
await this.SendMsg(Guacutils.Encode("admin", "10", this.node));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearTurnQueue() {
|
||||||
|
if (!this._perms.BypassAndEndTurns)
|
||||||
|
throw new NoPermissionException("Clear the turn queue");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("Clear the turn queue");
|
||||||
|
await this.SendMsg(Guacutils.Encode("admin", "17", this.node));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BypassTurn() {
|
||||||
|
if (!this._perms.BypassAndEndTurns)
|
||||||
|
throw new NoPermissionException("Bypass Turn");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("Bypass Turn");
|
||||||
|
await this.SendMsg(Guacutils.Encode("admin", "20"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EndTurn(string user) {
|
||||||
|
if (!this._perms.BypassAndEndTurns)
|
||||||
|
throw new NoPermissionException("End Turn");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("End Turn");
|
||||||
|
await this.SendMsg(Guacutils.Encode("admin", "16", user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Ban(string user) {
|
||||||
|
if (!this._perms.Ban)
|
||||||
|
throw new NoPermissionException("Ban");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("Ban");
|
||||||
|
await this.SendMsg(Guacutils.Encode("admin", "12", user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Kick(string user) {
|
||||||
|
if (!this._perms.Kick)
|
||||||
|
throw new NoPermissionException("Kick");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("Kick");
|
||||||
|
await this.SendMsg(Guacutils.Encode("admin", "15", user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RenameUser(string user, string newname) {
|
||||||
|
if (!this._perms.Rename)
|
||||||
|
throw new NoPermissionException("Rename user");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("Rename user");
|
||||||
|
await this.SendMsg(Guacutils.Encode("admin", "18", user, newname));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MuteUser(string user, bool permanent) {
|
||||||
|
if (!this._perms.Mute)
|
||||||
|
throw new NoPermissionException("Mute user");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("Mute user");
|
||||||
|
await this.SendMsg(Guacutils.Encode("admin", "14", user, permanent ? "1" : "0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetIP(string user) {
|
||||||
|
if (!this._perms.GetIP)
|
||||||
|
throw new NoPermissionException("Get IP");
|
||||||
|
if (!this._connectedToVM)
|
||||||
|
throw new NotConnectedToNodeException("Get IP");
|
||||||
|
var tsk = new GetIPTask {
|
||||||
|
IPTask = new(),
|
||||||
|
username = user
|
||||||
|
};
|
||||||
|
this.GotIPTasks.Add(tsk);
|
||||||
|
this.SendMsg(Guacutils.Encode("admin", "19", user));
|
||||||
|
return await tsk.IPTask.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task sendMouse() => this.SendMsg(Guacutils.Encode("mouse", mouse.X.ToString(), mouse.Y.ToString(), mouse.MakeMask().ToString()));
|
||||||
|
}
|
12
CollabVMSharp/CollabVMSharp.csproj
Normal file
12
CollabVMSharp/CollabVMSharp.csproj
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>10</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
60
CollabVMSharp/EventArgs.cs
Normal file
60
CollabVMSharp/EventArgs.cs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
|
||||||
|
public class UserRenamedEventArgs {
|
||||||
|
public string OldName { get; set; }
|
||||||
|
public string NewName { get; set; }
|
||||||
|
public User User;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VoteUpdateEventArgs {
|
||||||
|
public int Yes { get; set; }
|
||||||
|
public int No { get; set; }
|
||||||
|
public VoteStatus Status { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Amount of time until the vote ends, in milliseconds
|
||||||
|
/// </summary>
|
||||||
|
public int TimeToVoteEnd { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SendVoteResult {
|
||||||
|
/// <summary>
|
||||||
|
/// True if your vote was sent successfully, false if there's a cooldown.
|
||||||
|
/// </summary>
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public int? CooldownTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TurnUpdateEventArgs {
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of time left on your turn in milliseconds. Null if you don't have the turn.
|
||||||
|
/// </summary>
|
||||||
|
public int? TurnTimer;
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of time left before you get your turn in milliseconds. Null if you aren't waiting.
|
||||||
|
/// </summary>
|
||||||
|
public int? QueueTimer;
|
||||||
|
/// <summary>
|
||||||
|
/// The turn queue. The first element (index 0) has the turn, all following elements are the waiting users in order
|
||||||
|
/// </summary>
|
||||||
|
public User[] Queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RectEventArgs {
|
||||||
|
public int X { get; set; }
|
||||||
|
public int Y { get; set; }
|
||||||
|
public Image Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// this might not be the best place for this IDK
|
||||||
|
public enum VoteStatus {
|
||||||
|
Started,
|
||||||
|
Update,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetIPTask {
|
||||||
|
public string username { get; set; }
|
||||||
|
public TaskCompletionSource<string> IPTask;
|
||||||
|
}
|
12
CollabVMSharp/Exceptions.cs
Normal file
12
CollabVMSharp/Exceptions.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
|
||||||
|
public class NoPermissionException : Exception {
|
||||||
|
public NoPermissionException(string action) : base($"You do not have permission to {action} on this VM.") {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotConnectedToNodeException : Exception {
|
||||||
|
public NotConnectedToNodeException(string action) : base($"You must be connected to a node to {action}") {
|
||||||
|
}
|
||||||
|
}
|
45
CollabVMSharp/Guacutils.cs
Normal file
45
CollabVMSharp/Guacutils.cs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CollabVMSharp {
|
||||||
|
/// <summary>
|
||||||
|
/// Utilities for converting lists of strings to and from Guacamole format
|
||||||
|
/// </summary>
|
||||||
|
public static class Guacutils {
|
||||||
|
/// <summary>
|
||||||
|
/// Encode an array of strings to guacamole format
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="msgArr">List of strings to be encoded</param>
|
||||||
|
/// <returns>A guacamole string array containing the provided strings</returns>
|
||||||
|
public static string Encode(params string[] msgArr) {
|
||||||
|
string res = "";
|
||||||
|
int i = 0;
|
||||||
|
foreach (string s in msgArr) {
|
||||||
|
res += s.Length.ToString();
|
||||||
|
res += ".";
|
||||||
|
res += s;
|
||||||
|
if (i == msgArr.Length - 1) res += ";";
|
||||||
|
else res += ",";
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Decode a guacamole string array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="msg">String containing a guacamole array</param>
|
||||||
|
/// <returns>An array of strings</returns>
|
||||||
|
public static string[] Decode(string msg) {
|
||||||
|
List<string> outArr = new List<string>();
|
||||||
|
int pos = 0;
|
||||||
|
while (pos < msg.Length - 1) {
|
||||||
|
int dotpos = msg.IndexOf('.', pos + 1);
|
||||||
|
string lenstr = msg.Substring(pos, dotpos - pos);
|
||||||
|
int len = int.Parse(lenstr);
|
||||||
|
string str = msg.Substring(dotpos + 1, len);
|
||||||
|
outArr.Add(str);
|
||||||
|
pos = dotpos + len + 2;
|
||||||
|
}
|
||||||
|
return outArr.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
CollabVMSharp/Keyboard.cs
Normal file
144
CollabVMSharp/Keyboard.cs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
|
||||||
|
public enum SpecialKey : int {
|
||||||
|
Back = 0xff08,
|
||||||
|
Tab = 0xff09,
|
||||||
|
Clear = 0xff0b,
|
||||||
|
Return = 0xff0d,
|
||||||
|
Pause = 0xff13,
|
||||||
|
Scroll = 0xff14,
|
||||||
|
Escape = 0xff1b,
|
||||||
|
Delete = 0xffff,
|
||||||
|
Home = 0xff58,
|
||||||
|
Left = 0xff51,
|
||||||
|
Up = 0xff52,
|
||||||
|
Right = 0xff53,
|
||||||
|
Down = 0xff54,
|
||||||
|
PageUp = 0xff55,
|
||||||
|
PageDown = 0xff56,
|
||||||
|
End = 0xff57,
|
||||||
|
Insert = 0xff63,
|
||||||
|
F1 = 0xffbe,
|
||||||
|
F2 = 0xffbf,
|
||||||
|
F3 = 0xffc0,
|
||||||
|
F4 = 0xffc1,
|
||||||
|
F5 = 0xffc2,
|
||||||
|
F6 = 0xffc3,
|
||||||
|
F7 =0xffc4,
|
||||||
|
F8 = 0xffc5,
|
||||||
|
F9 = 0xffc6,
|
||||||
|
F10 = 0xffc7,
|
||||||
|
F11 = 0xffc8,
|
||||||
|
F12 = 0xffc9,
|
||||||
|
LeftShift = 0xffe1,
|
||||||
|
RightShift = 0xffe2,
|
||||||
|
LeftCtrl = 0xffe3,
|
||||||
|
RightCtrl = 0xffe4,
|
||||||
|
CapsLock = 0xffe5,
|
||||||
|
LWin = 0xffe7,
|
||||||
|
LeftAlt = 0xffe9,
|
||||||
|
RightAlt = 0xffea,
|
||||||
|
};
|
||||||
|
public static class Keyboard {
|
||||||
|
public static IReadOnlyDictionary<char, int> KeyMap = new Dictionary<char, int>() {
|
||||||
|
{' ', 0x0020 },
|
||||||
|
{'!', 0x0021 },
|
||||||
|
{'"', 0x0022 },
|
||||||
|
{'#', 0x0023 },
|
||||||
|
{'$', 0x0024 },
|
||||||
|
{'%', 0x0025 },
|
||||||
|
{'&', 0x0026 },
|
||||||
|
{'\'', 0x0027 },
|
||||||
|
{'(', 0x0028 },
|
||||||
|
{')', 0x0029 },
|
||||||
|
{'*', 0x002a },
|
||||||
|
{'+', 0x002b },
|
||||||
|
{',', 0x002c },
|
||||||
|
{'-', 0x002d },
|
||||||
|
{'.', 0x002e },
|
||||||
|
{'/', 0x002f },
|
||||||
|
{'0', 0x0030 },
|
||||||
|
{'1', 0x0031 },
|
||||||
|
{'2', 0x0032 },
|
||||||
|
{'3', 0x0033 },
|
||||||
|
{'4', 0x0034 },
|
||||||
|
{'5', 0x0035 },
|
||||||
|
{'6', 0x0036 },
|
||||||
|
{'7', 0x0037 },
|
||||||
|
{'8', 0x0038 },
|
||||||
|
{'9', 0x0039 },
|
||||||
|
{':', 0x003a },
|
||||||
|
{';', 0x003b },
|
||||||
|
{'<', 0x003c },
|
||||||
|
{'=', 0x003d },
|
||||||
|
{'>', 0x003e },
|
||||||
|
{'?', 0x003f },
|
||||||
|
{'@', 0x0040 },
|
||||||
|
{'A', 0x0041 },
|
||||||
|
{'B', 0x0042 },
|
||||||
|
{'C', 0x0043 },
|
||||||
|
{'D', 0x0044 },
|
||||||
|
{'E', 0x0045 },
|
||||||
|
{'F', 0x0046 },
|
||||||
|
{'G', 0x0047 },
|
||||||
|
{'H', 0x0048 },
|
||||||
|
{'I', 0x0049 },
|
||||||
|
{'J', 0x004a },
|
||||||
|
{'K', 0x004b },
|
||||||
|
{'L', 0x004c },
|
||||||
|
{'M', 0x004d },
|
||||||
|
{'N', 0x004e },
|
||||||
|
{'O', 0x004f },
|
||||||
|
{'P', 0x0050 },
|
||||||
|
{'Q', 0x0051 },
|
||||||
|
{'R', 0x0052 },
|
||||||
|
{'S', 0x0053 },
|
||||||
|
{'T', 0x0054 },
|
||||||
|
{'U', 0x0055 },
|
||||||
|
{'V', 0x0056 },
|
||||||
|
{'W', 0x0057 },
|
||||||
|
{'X', 0x0058 },
|
||||||
|
{'Y', 0x0059 },
|
||||||
|
{'Z', 0x005a },
|
||||||
|
{'[', 0x005b },
|
||||||
|
{'\\', 0x005c },
|
||||||
|
{']', 0x005d },
|
||||||
|
{'^', 0x005e },
|
||||||
|
{'_', 0x005f },
|
||||||
|
{'`', 0x0060 },
|
||||||
|
{'a', 0x0061 },
|
||||||
|
{'b', 0x0062 },
|
||||||
|
{'c', 0x0063 },
|
||||||
|
{'d', 0x0064 },
|
||||||
|
{'e', 0x0065 },
|
||||||
|
{'f', 0x0066 },
|
||||||
|
{'g', 0x0067 },
|
||||||
|
{'h', 0x0068 },
|
||||||
|
{'i', 0x0069 },
|
||||||
|
{'j', 0x006a },
|
||||||
|
{'k', 0x006b },
|
||||||
|
{'l', 0x006c },
|
||||||
|
{'m', 0x006d },
|
||||||
|
{'n', 0x006e },
|
||||||
|
{'o', 0x006f },
|
||||||
|
{'p', 0x0070 },
|
||||||
|
{'q', 0x0071 },
|
||||||
|
{'r', 0x0072 },
|
||||||
|
{'s', 0x0073 },
|
||||||
|
{'t', 0x0074 },
|
||||||
|
{'u', 0x0075 },
|
||||||
|
{'v', 0x0076 },
|
||||||
|
{'w', 0x0077 },
|
||||||
|
{'x', 0x0078 },
|
||||||
|
{'y', 0x0079 },
|
||||||
|
{'z', 0x007a },
|
||||||
|
{'{', 0x007b },
|
||||||
|
{'|', 0x007c },
|
||||||
|
{'}', 0x007d },
|
||||||
|
{'~', 0x007e },
|
||||||
|
{'\n', 0xff0d},
|
||||||
|
{'\t', 0xff09}
|
||||||
|
};
|
||||||
|
}
|
38
CollabVMSharp/Mouse.cs
Normal file
38
CollabVMSharp/Mouse.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
|
||||||
|
public class Mouse {
|
||||||
|
public bool Left { get; set; }
|
||||||
|
public bool Middle { get; set; }
|
||||||
|
public bool Right { get; set; }
|
||||||
|
public int X { get; set; }
|
||||||
|
public int Y { get; set; }
|
||||||
|
public Mouse() {
|
||||||
|
Left = false;
|
||||||
|
Middle = false;
|
||||||
|
Right = false;
|
||||||
|
X = 0;
|
||||||
|
Y = 0;
|
||||||
|
}
|
||||||
|
public int MakeMask() {
|
||||||
|
int mask = 0;
|
||||||
|
if (this.Left) mask |= 1;
|
||||||
|
if (this.Middle) mask |= 2;
|
||||||
|
if (this.Right) mask |= 4;
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadMask(int mask) {
|
||||||
|
this.Left = false;
|
||||||
|
this.Middle = false;
|
||||||
|
this.Right = false;
|
||||||
|
if ((mask & 1) != 0) this.Left = true;
|
||||||
|
if ((mask & 2) != 0) this.Middle = true;
|
||||||
|
if ((mask & 4) != 0) this.Right = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MouseButton {
|
||||||
|
Left,
|
||||||
|
Middle,
|
||||||
|
Right
|
||||||
|
}
|
19
CollabVMSharp/Node.cs
Normal file
19
CollabVMSharp/Node.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A VM recieved from the list opcode
|
||||||
|
/// </summary>
|
||||||
|
public class Node {
|
||||||
|
/// <summary>
|
||||||
|
/// ID of the VM
|
||||||
|
/// </summary>
|
||||||
|
public string ID { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Display name of the VM. May contain HTML.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// JPEG thumbnail of the VM, usually in 400x300 resolution
|
||||||
|
/// </summary>
|
||||||
|
public byte[] Thumbnail { get; set; }
|
||||||
|
}
|
47
CollabVMSharp/Permissions.cs
Normal file
47
CollabVMSharp/Permissions.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
|
||||||
|
public class Permissions {
|
||||||
|
private bool restore;
|
||||||
|
private bool reboot;
|
||||||
|
private bool ban;
|
||||||
|
private bool kick;
|
||||||
|
private bool mute;
|
||||||
|
private bool forcevote;
|
||||||
|
private bool bypassendturn;
|
||||||
|
private bool rename;
|
||||||
|
private bool getip;
|
||||||
|
private bool xss;
|
||||||
|
|
||||||
|
public bool Restore { get { return restore; } }
|
||||||
|
public bool Reboot { get { return reboot; } }
|
||||||
|
public bool Ban { get { return ban; } }
|
||||||
|
public bool Kick { get { return kick; } }
|
||||||
|
public bool Mute { get { return mute; } }
|
||||||
|
public bool ForceVote { get { return forcevote; } }
|
||||||
|
public bool BypassAndEndTurns { get { return bypassendturn; } }
|
||||||
|
public bool Rename { get { return rename; } }
|
||||||
|
public bool GetIP { get { return getip; } }
|
||||||
|
public bool XSS { get { return xss; } }
|
||||||
|
|
||||||
|
public Permissions(int permissionvalue) {
|
||||||
|
if ((permissionvalue & 1) != 0) restore = true;
|
||||||
|
if ((permissionvalue & 2) != 0) reboot = true;
|
||||||
|
if ((permissionvalue & 4) != 0) ban = true;
|
||||||
|
if ((permissionvalue & 8) != 0) forcevote = true;
|
||||||
|
if ((permissionvalue & 16) != 0) mute = true;
|
||||||
|
if ((permissionvalue & 32) != 0) kick = true;
|
||||||
|
if ((permissionvalue & 64) != 0) bypassendturn = true;
|
||||||
|
if ((permissionvalue & 128) != 0) rename = true;
|
||||||
|
if ((permissionvalue & 256) != 0) getip = true;
|
||||||
|
if ((permissionvalue & 512) != 0) xss = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Permissions All => new Permissions(65535);
|
||||||
|
public static Permissions None => new Permissions(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Rank {
|
||||||
|
Unregistered,
|
||||||
|
Moderator,
|
||||||
|
Admin
|
||||||
|
}
|
7
CollabVMSharp/TurnStatus.cs
Normal file
7
CollabVMSharp/TurnStatus.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
|
||||||
|
public enum TurnStatus {
|
||||||
|
None,
|
||||||
|
Waiting,
|
||||||
|
HasTurn
|
||||||
|
}
|
7
CollabVMSharp/User.cs
Normal file
7
CollabVMSharp/User.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace CollabVMSharp;
|
||||||
|
|
||||||
|
public class User {
|
||||||
|
public string Username { get; set; }
|
||||||
|
public Rank Rank { get; set; }
|
||||||
|
public TurnStatus Turn { get; set; }
|
||||||
|
}
|
7
global.json
Normal file
7
global.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"rollForward": "latestMinor",
|
||||||
|
"allowPrerelease": false
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue