- add remaining staff options

- fix IndexOutOfRangeException in vote handler
- add properties for latest vote and turn update
- add property for node name
- fix hang bug when reconnecting after disconnecting
This commit is contained in:
elijahr2411 2023-03-08 18:59:58 -05:00
parent a56a246f67
commit 811fd766a9
3 changed files with 163 additions and 16 deletions

View file

@ -35,12 +35,17 @@ public class CollabVMClient {
private Image framebuffer; private Image framebuffer;
private TurnStatus _turnStatus; private TurnStatus _turnStatus;
private Mouse mouse; private Mouse mouse;
// Tasks private TurnUpdateEventArgs _currentturn;
private VoteUpdateEventArgs _currentvote;
private WebProxy? _proxy;
// Tasks and related
private TaskCompletionSource<Node[]> GotNodeList; private TaskCompletionSource<Node[]> GotNodeList;
private TaskCompletionSource<bool> GotConnectionToNode; private TaskCompletionSource<bool> GotConnectionToNode;
private TaskCompletionSource<int> GotTurn; private TaskCompletionSource<int> GotTurn;
private TaskCompletionSource<Rank> GotStaff; private TaskCompletionSource<Rank> GotStaff;
private List<GetIPTask> GotIPTasks; private List<GetIPTask> GotIPTasks;
private SemaphoreSlim QEMUMonitorSemaphore;
private TaskCompletionSource<string> QEMUMonitorResult;
// Properties // Properties
public Rank Rank { get { return this._rank; } } public Rank Rank { get { return this._rank; } }
public Permissions Permissions { get { return this._perms; } } public Permissions Permissions { get { return this._perms; } }
@ -48,6 +53,9 @@ public class CollabVMClient {
public bool ConnectedToVM { get { return this._connectedToVM; } } public bool ConnectedToVM { get { return this._connectedToVM; } }
public User[] Users => _users.ToArray(); public User[] Users => _users.ToArray();
public TurnStatus TurnStatus { get { return this._turnStatus; } } public TurnStatus TurnStatus { get { return this._turnStatus; } }
public VoteUpdateEventArgs CurrentVote { get { return this._currentvote; } }
public TurnUpdateEventArgs CurrentTurn { get { return this._currentturn; } }
public string Node { get { return this.node; } }
// Events // Events
public event EventHandler<ChatMessage> Chat; public event EventHandler<ChatMessage> Chat;
public event EventHandler<ChatMessage[]> ChatHistory; public event EventHandler<ChatMessage[]> ChatHistory;
@ -88,19 +96,32 @@ public class CollabVMClient {
this.socket.Options.AddSubProtocol("guacamole"); this.socket.Options.AddSubProtocol("guacamole");
this.socket.Options.SetRequestHeader("Origin", "https://computernewb.com"); this.socket.Options.SetRequestHeader("Origin", "https://computernewb.com");
if (proxy != null) { if (proxy != null) {
this.socket.Options.Proxy = new WebProxy(proxy); this._proxy = new WebProxy(proxy);
this.socket.Options.Proxy = this._proxy;
} }
this.NOPRecieve = new(10000); this.NOPRecieve = new(10000);
this.NOPRecieve.AutoReset = false; this.NOPRecieve.AutoReset = false;
this.NOPRecieve.Elapsed += delegate { this.Disconnect(); }; this.NOPRecieve.Elapsed += delegate { this.Disconnect(); };
this._users = new(); this._users = new();
this._currentturn = new TurnUpdateEventArgs {
Queue = Array.Empty<User>(),
TurnTimer = 0,
QueueTimer = 0,
};
this._currentvote = new VoteUpdateEventArgs {
No = 0,
Yes = 0,
Status = VoteStatus.None
};
this.mouse = new(); this.mouse = new();
this.GotNodeList = new(); this.GotNodeList = new();
this.GotConnectionToNode = new(); this.GotConnectionToNode = new();
this.GotTurn = new(); this.GotTurn = new();
this.GotStaff = new(); this.GotStaff = new();
this.GotIPTasks = new(); this.GotIPTasks = new();
// Assign empty handlers this.QEMUMonitorResult = new();
this.QEMUMonitorSemaphore = new(1, 1);
// Assign empty handlers to prevent exception
Chat += delegate { }; Chat += delegate { };
ChatHistory += delegate { }; ChatHistory += delegate { };
ConnectedToNode += delegate { }; ConnectedToNode += delegate { };
@ -292,15 +313,23 @@ public class CollabVMClient {
case "vote": { case "vote": {
switch (msgArr[1]) { switch (msgArr[1]) {
case "0": case "0":
if (msgArr.Length < 4) return;
goto case "1";
case "1": case "1":
this.VoteUpdate.Invoke(this, new VoteUpdateEventArgs { this._currentvote = new VoteUpdateEventArgs {
No = int.Parse(msgArr[4]), No = int.Parse(msgArr[4]),
Yes = int.Parse(msgArr[3]), Yes = int.Parse(msgArr[3]),
Status = msgArr[1] switch {"0" => VoteStatus.Started, "1" => VoteStatus.Update}, Status = msgArr[1] switch {"0" => VoteStatus.Started, "1" => VoteStatus.Update},
TimeToVoteEnd = int.Parse(msgArr[2]) TimeToVoteEnd = int.Parse(msgArr[2])
}); };
this.VoteUpdate.Invoke(this, this._currentvote);
break; break;
case "2": case "2":
this._currentvote = new VoteUpdateEventArgs {
Yes = 0,
No = 0,
Status = VoteStatus.None,
};
this.VoteEnded.Invoke(this, EventArgs.Empty); this.VoteEnded.Invoke(this, EventArgs.Empty);
break; break;
case "3": case "3":
@ -316,11 +345,12 @@ public class CollabVMClient {
this._turnStatus = TurnStatus.None; this._turnStatus = TurnStatus.None;
int queuedUsers = int.Parse(msgArr[2]); int queuedUsers = int.Parse(msgArr[2]);
if (queuedUsers == 0) { if (queuedUsers == 0) {
this.TurnUpdate.Invoke(this, new TurnUpdateEventArgs { this._currentturn = new TurnUpdateEventArgs {
Queue = Array.Empty<User>(), Queue = Array.Empty<User>(),
QueueTimer = null,
TurnTimer = null, TurnTimer = null,
QueueTimer = null };
}); TurnUpdate.Invoke(this, this._currentturn);
return; return;
} }
var currentTurnUser = _users.First(u => u.Username == msgArr[3]); var currentTurnUser = _users.First(u => u.Username == msgArr[3]);
@ -339,11 +369,13 @@ public class CollabVMClient {
queue.Add(user); queue.Add(user);
} }
} }
this.TurnUpdate.Invoke(this, new TurnUpdateEventArgs {
this._currentturn = new TurnUpdateEventArgs {
Queue = queue.ToArray(), Queue = queue.ToArray(),
TurnTimer = (this._turnStatus == TurnStatus.HasTurn) ? int.Parse(msgArr[1]) : null, TurnTimer = (this._turnStatus == TurnStatus.HasTurn) ? int.Parse(msgArr[1]) : null,
QueueTimer = (this._turnStatus == TurnStatus.Waiting) ? int.Parse(msgArr[msgArr.Length - 1]) : null, QueueTimer = (this._turnStatus == TurnStatus.Waiting) ? int.Parse(msgArr[msgArr.Length - 1]) : null,
}); };
this.TurnUpdate.Invoke(this, this._currentturn );
break; break;
} }
case "admin": { case "admin": {
@ -366,6 +398,9 @@ public class CollabVMClient {
} }
break; break;
} }
case "2":
this.QEMUMonitorResult.TrySetResult(msgArr[2]);
break;
case "19": case "19":
var tsk = this.GotIPTasks.Find(x => x.username == msgArr[2]); var tsk = this.GotIPTasks.Find(x => x.username == msgArr[2]);
if (tsk == null) return; if (tsk == null) return;
@ -380,9 +415,9 @@ public class CollabVMClient {
/// Close the connection to the server /// Close the connection to the server
/// </summary> /// </summary>
public async Task Disconnect() { public async Task Disconnect() {
this._connected = false;
if (this.socket.State == WebSocketState.Open) if (this.socket.State == WebSocketState.Open)
await this.SendMsg("10.disconnect;"); await this.SendMsg("10.disconnect;");
this._connected = false;
await this.socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); await this.socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
this.Cleanup(); this.Cleanup();
return; return;
@ -396,6 +431,12 @@ public class CollabVMClient {
this._perms = Permissions.None; this._perms = Permissions.None;
this.NOPRecieve.Stop(); this.NOPRecieve.Stop();
this.NOPRecieve.Interval = 10000; this.NOPRecieve.Interval = 10000;
this.socket = new();
this.socket.Options.AddSubProtocol("guacamole");
this.socket.Options.SetRequestHeader("Origin", "https://computernewb.com");
if (_proxy != null) {
this.socket.Options.Proxy = this._proxy;
}
if (fireDisconnect) if (fireDisconnect)
this.ConnectionClosed.Invoke(this, EventArgs.Empty); this.ConnectionClosed.Invoke(this, EventArgs.Empty);
} }
@ -558,6 +599,18 @@ public class CollabVMClient {
/// </summary> /// </summary>
/// <param name="msg">Message to send</param>` /// <param name="msg">Message to send</param>`
public Task SendChat(string msg) => this.SendMsg(Guacutils.Encode("chat", msg)); public Task SendChat(string msg) => this.SendMsg(Guacutils.Encode("chat", msg));
/// <summary>
/// Send an XSS (Not HTML sanitized) message to the VM chat
/// </summary>
/// <param name="msg">Message to send</param>
public async Task SendXSSChat(string msg) {
if (!this._perms.XSS)
throw new NoPermissionException("Send XSS Message");
if (!this.ConnectedToVM)
throw new NotConnectedToNodeException("Send XSS Message");
await this.SendMsg(Guacutils.Encode("admin", "21", msg));
return;
}
/// <summary> /// <summary>
/// Restore the VM /// Restore the VM
@ -570,6 +623,9 @@ public class CollabVMClient {
await this.SendMsg(Guacutils.Encode("admin", "8", this.node)); await this.SendMsg(Guacutils.Encode("admin", "8", this.node));
} }
/// <summary>
/// Reboot the VM
/// </summary>
public async Task Reboot() { public async Task Reboot() {
if (!this._perms.Reboot) if (!this._perms.Reboot)
throw new NoPermissionException("Reboot VM"); throw new NoPermissionException("Reboot VM");
@ -578,6 +634,9 @@ public class CollabVMClient {
await this.SendMsg(Guacutils.Encode("admin", "10", this.node)); await this.SendMsg(Guacutils.Encode("admin", "10", this.node));
} }
/// <summary>
/// Clear the VM Turn Queue
/// </summary>
public async Task ClearTurnQueue() { public async Task ClearTurnQueue() {
if (!this._perms.BypassAndEndTurns) if (!this._perms.BypassAndEndTurns)
throw new NoPermissionException("Clear the turn queue"); throw new NoPermissionException("Clear the turn queue");
@ -586,6 +645,9 @@ public class CollabVMClient {
await this.SendMsg(Guacutils.Encode("admin", "17", this.node)); await this.SendMsg(Guacutils.Encode("admin", "17", this.node));
} }
/// <summary>
/// Steal the turn from the current user
/// </summary>
public async Task BypassTurn() { public async Task BypassTurn() {
if (!this._perms.BypassAndEndTurns) if (!this._perms.BypassAndEndTurns)
throw new NoPermissionException("Bypass Turn"); throw new NoPermissionException("Bypass Turn");
@ -594,6 +656,10 @@ public class CollabVMClient {
await this.SendMsg(Guacutils.Encode("admin", "20")); await this.SendMsg(Guacutils.Encode("admin", "20"));
} }
/// <summary>
/// End a user's turn or remove them from the queue
/// </summary>
/// <param name="user">Username to remove</param>
public async Task EndTurn(string user) { public async Task EndTurn(string user) {
if (!this._perms.BypassAndEndTurns) if (!this._perms.BypassAndEndTurns)
throw new NoPermissionException("End Turn"); throw new NoPermissionException("End Turn");
@ -602,6 +668,10 @@ public class CollabVMClient {
await this.SendMsg(Guacutils.Encode("admin", "16", user)); await this.SendMsg(Guacutils.Encode("admin", "16", user));
} }
/// <summary>
/// Ban a user from the VM
/// </summary>
/// <param name="user">Username to ban</param>
public async Task Ban(string user) { public async Task Ban(string user) {
if (!this._perms.Ban) if (!this._perms.Ban)
throw new NoPermissionException("Ban"); throw new NoPermissionException("Ban");
@ -609,7 +679,10 @@ public class CollabVMClient {
throw new NotConnectedToNodeException("Ban"); throw new NotConnectedToNodeException("Ban");
await this.SendMsg(Guacutils.Encode("admin", "12", user)); await this.SendMsg(Guacutils.Encode("admin", "12", user));
} }
/// <summary>
/// Kick a user from the VM
/// </summary>
/// <param name="user">Username to kick</param>
public async Task Kick(string user) { public async Task Kick(string user) {
if (!this._perms.Kick) if (!this._perms.Kick)
throw new NoPermissionException("Kick"); throw new NoPermissionException("Kick");
@ -618,6 +691,11 @@ public class CollabVMClient {
await this.SendMsg(Guacutils.Encode("admin", "15", user)); await this.SendMsg(Guacutils.Encode("admin", "15", user));
} }
/// <summary>
/// Rename a user
/// </summary>
/// <param name="user">The user to rename</param>
/// <param name="newname">New username</param>
public async Task RenameUser(string user, string newname) { public async Task RenameUser(string user, string newname) {
if (!this._perms.Rename) if (!this._perms.Rename)
throw new NoPermissionException("Rename user"); throw new NoPermissionException("Rename user");
@ -626,6 +704,11 @@ public class CollabVMClient {
await this.SendMsg(Guacutils.Encode("admin", "18", user, newname)); await this.SendMsg(Guacutils.Encode("admin", "18", user, newname));
} }
/// <summary>
/// Mute a user, preventing them from chatting or taking turns
/// </summary>
/// <param name="user">User to mute</param>
/// <param name="permanent">True to permanently mute, false to mute temporarily (30 seconds by default)</param>
public async Task MuteUser(string user, bool permanent) { public async Task MuteUser(string user, bool permanent) {
if (!this._perms.Mute) if (!this._perms.Mute)
throw new NoPermissionException("Mute user"); throw new NoPermissionException("Mute user");
@ -634,6 +717,11 @@ public class CollabVMClient {
await this.SendMsg(Guacutils.Encode("admin", "14", user, permanent ? "1" : "0")); await this.SendMsg(Guacutils.Encode("admin", "14", user, permanent ? "1" : "0"));
} }
/// <summary>
/// Get a user's IP address
/// </summary>
/// <param name="user">User to get the IP from</param>
/// <returns>The user's IP address</returns>
public async Task<string> GetIP(string user) { public async Task<string> GetIP(string user) {
if (!this._perms.GetIP) if (!this._perms.GetIP)
throw new NoPermissionException("Get IP"); throw new NoPermissionException("Get IP");
@ -648,5 +736,63 @@ public class CollabVMClient {
return await tsk.IPTask.Task; return await tsk.IPTask.Task;
} }
private Task sendMouse() => this.SendMsg(Guacutils.Encode("mouse", mouse.X.ToString(), mouse.Y.ToString(), mouse.MakeMask().ToString())); /// <summary>
/// Send a command to the QEMU monitor of the VM
/// </summary>
/// <param name="cmd">Monitor command to send</param>
/// <returns>Response from QEMU</returns>
public async Task<string> QEMUMonitor(string cmd) {
if (this._rank != Rank.Admin)
throw new NoPermissionException("Run QEMU Monitor Command");
if (!this._connectedToVM)
throw new NotConnectedToNodeException("Run QEMU Monitor Command");
await this.QEMUMonitorSemaphore.WaitAsync();
this.QEMUMonitorResult = new();
this.SendMsg(Guacutils.Encode("admin", "5", this.node, cmd));
string result = await this.QEMUMonitorResult.Task;
this.QEMUMonitorSemaphore.Release(1);
return result;
}
/// <summary>
/// Force end a vote reset
/// </summary>
/// <param name="reset">True to reset the VM, false to cancel the vote</param>
public async Task ForceVote(bool reset) {
if (!this._perms.ForceVote)
throw new NoPermissionException("Force Vote");
if (!this._connectedToVM)
throw new NotConnectedToNodeException("Force Vote");
await this.SendMsg(Guacutils.Encode("admin", "13", reset ? "1" : "0"));
}
/// <summary>
/// Toggle turns on or off
/// </summary>
/// <param name="status">True to enable turns, false to restrict them to staff</param>
public async Task ToggleTurns(bool status) {
if (this._rank != Rank.Admin)
throw new NoPermissionException("Toggle Turns");
if (!this.ConnectedToVM)
throw new NotConnectedToNodeException("Toggle Turns");
await this.SendMsg(Guacutils.Encode("admin", "22", status ? "1" : "0"));
}
/// <summary>
/// Take an indefinite turn. Can be ended by calling Turn(false)
/// </summary>
/// <exception cref="NoPermissionException"></exception>
/// <exception cref="NotConnectedToNodeException"></exception>
public async Task IndefiniteTurn() {
if (this._rank != Rank.Admin)
throw new NoPermissionException("Take Indefinite Turn");
if (!this.ConnectedToVM)
throw new NotConnectedToNodeException("Take Indefinite Turn");
await this.SendMsg(Guacutils.Encode("admin", "23"));
}
public Image GetFramebuffer() => framebuffer.CloneAs<Rgba32>();
private Task sendMouse() => this.SendMsg(Guacutils.Encode("mouse", mouse.X.ToString(), mouse.Y.ToString(), mouse.MakeMask().ToString()));
} }

View file

@ -52,6 +52,7 @@ public class RectEventArgs {
public enum VoteStatus { public enum VoteStatus {
Started, Started,
Update, Update,
None,
} }
public class GetIPTask { public class GetIPTask {

View file

@ -41,7 +41,7 @@ public class Permissions {
} }
public enum Rank { public enum Rank {
Unregistered, Unregistered = 0,
Moderator, Moderator = 3,
Admin Admin = 2
} }