diff --git a/.idea/.idea.Foxcord/.idea/misc.xml b/.idea/.idea.Foxcord/.idea/discord.xml
similarity index 73%
rename from .idea/.idea.Foxcord/.idea/misc.xml
rename to .idea/.idea.Foxcord/.idea/discord.xml
index 30bab2a..d8e9561 100644
--- a/.idea/.idea.Foxcord/.idea/misc.xml
+++ b/.idea/.idea.Foxcord/.idea/discord.xml
@@ -1,7 +1,7 @@
-
+
\ No newline at end of file
diff --git a/Foxcord/Gateway/DiscordGatewayClient.Dispatch.cs b/Foxcord/Gateway/DiscordGatewayClient.Dispatch.cs
deleted file mode 100644
index 37ae970..0000000
--- a/Foxcord/Gateway/DiscordGatewayClient.Dispatch.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System.Text.Json;
-using Foxcord.Gateway.Events.Dispatch;
-using Foxcord.Rest;
-using Foxcord.Rest.Types;
-
-namespace Foxcord.Gateway;
-
-public partial class DiscordGatewayClient
-{
- private IDispatch ParseDispatchEvent(string rawType, JsonElement rawPayload)
- {
- switch (rawType)
- {
- case DispatchEventTypeName.Ready:
- return rawPayload.Deserialize(_jsonSerializerOptions)!;
- case DispatchEventTypeName.GuildCreate:
- return rawPayload.Deserialize(_jsonSerializerOptions)!;
- case DispatchEventTypeName.MessageCreate:
- return rawPayload.Deserialize(_jsonSerializerOptions)!;
- default:
- throw new ArgumentOutOfRangeException(nameof(rawType), $"Unknown dispatch event '{rawType}'");
- }
- }
-
- private async Task HandleDispatch(IDispatch dispatch, CancellationToken ct = default)
- {
- switch (dispatch)
- {
- case ReadyEvent ready:
- _logger.Debug("Received READY! API version: {Version}, user: {UserId}, shard: {Id}/{Total}",
- ready.Version, ready.User.Id, ready.Shard.ShardId, ready.Shard.NumShards);
- break;
- case GuildCreateEvent guildCreate:
- _logger.Debug("Received guild create for guild {Id} / {Name}", guildCreate.Id, guildCreate.Name);
- break;
- case MessageCreateEvent m:
- _logger.Debug("Received message create from {User} in {Channel}. Content: {Content}", m.Author.Tag,
- m.ChannelId, m.Content);
- if (m.Content == "!ping")
- {
- var rest = new DiscordRestClient(_logger,
- new DiscordRestClientOptions { Token = _token["Bot ".Length..] });
- await rest.CreateMessageAsync(m.ChannelId,
- new CreateMessageParams(Content: $"Pong! Latency: {Latency.TotalMilliseconds}ms"), ct);
- }
-
- break;
- default:
- _logger.Debug("Received dispatch event {DispatchType}", dispatch.GetType().Name);
- break;
- }
- }
-}
\ No newline at end of file
diff --git a/Foxcord/Gateway/DiscordGatewayClient.Events.cs b/Foxcord/Gateway/DiscordGatewayClient.Events.cs
index 6d90c4b..4caaf22 100644
--- a/Foxcord/Gateway/DiscordGatewayClient.Events.cs
+++ b/Foxcord/Gateway/DiscordGatewayClient.Events.cs
@@ -1,3 +1,9 @@
+using System.Text.Json;
+using Foxcord.Gateway.Events.Commands;
+using Foxcord.Gateway.Events.Dispatch;
+using Foxcord.Rest;
+using Foxcord.Rest.Types;
+
namespace Foxcord.Gateway;
public partial class DiscordGatewayClient
@@ -11,7 +17,51 @@ public partial class DiscordGatewayClient
private async Task HandleHeartbeatRequest(CancellationToken ct = default)
{
_logger.Information("Early heartbeat requested, sending heartbeat");
- await WritePacket(new GatewayPacket { Opcode = GatewayOpcode.Heartbeat, Payload = _lastSequence }, ct);
+ await SendCommandAsync(new HeartbeatCommand(_lastSequence), ct);
_lastHeartbeatSend = DateTimeOffset.UtcNow;
}
+
+ private IDispatch ParseDispatchEvent(string rawType, JsonElement rawPayload)
+ {
+ switch (rawType)
+ {
+ case DispatchEventTypeName.Ready:
+ return rawPayload.Deserialize(_jsonSerializerOptions)!;
+ case DispatchEventTypeName.GuildCreate:
+ return rawPayload.Deserialize(_jsonSerializerOptions)!;
+ case DispatchEventTypeName.MessageCreate:
+ return rawPayload.Deserialize(_jsonSerializerOptions)!;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(rawType), $"Unknown dispatch event '{rawType}'");
+ }
+ }
+
+ private async Task HandleDispatch(IDispatch dispatch, CancellationToken ct = default)
+ {
+ switch (dispatch)
+ {
+ case ReadyEvent ready:
+ _logger.Debug("Received READY! API version: {Version}, user: {UserId}, shard: {Id}/{Total}",
+ ready.Version, ready.User.Id, ready.Shard.ShardId, ready.Shard.NumShards);
+ break;
+ case GuildCreateEvent guildCreate:
+ _logger.Debug("Received guild create for guild {Id} / {Name}", guildCreate.Id, guildCreate.Name);
+ break;
+ case MessageCreateEvent m:
+ _logger.Debug("Received message create from {User} in {Channel}. Content: {Content}", m.Author.Tag,
+ m.ChannelId, m.Content);
+ if (m.Content == "!ping")
+ {
+ var rest = new DiscordRestClient(_logger,
+ new DiscordRestClientOptions { Token = _token["Bot ".Length..] });
+ await rest.CreateMessageAsync(m.ChannelId,
+ new CreateMessageParams(Content: $"Pong! Latency: {Latency.TotalMilliseconds}ms"), ct);
+ }
+
+ break;
+ default:
+ _logger.Debug("Received dispatch event {DispatchType}", dispatch.GetType().Name);
+ break;
+ }
+ }
}
\ No newline at end of file
diff --git a/Foxcord/Gateway/DiscordGatewayClient.cs b/Foxcord/Gateway/DiscordGatewayClient.cs
index 8e472b2..72c3346 100644
--- a/Foxcord/Gateway/DiscordGatewayClient.cs
+++ b/Foxcord/Gateway/DiscordGatewayClient.cs
@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Net.WebSockets;
using System.Text.Json;
using Foxcord.Gateway.Events;
+using Foxcord.Gateway.Events.Commands;
using Foxcord.Serialization;
using Serilog;
@@ -18,9 +19,12 @@ public partial class DiscordGatewayClient
private readonly string _token;
private readonly Uri _gatewayUri;
private readonly GatewayIntent _intents;
+ private readonly IdentifyProperties? _properties;
+ private readonly int[]? _shardInfo;
+ private readonly PresenceUpdateCommand? _initialPresence;
private readonly JsonSerializerOptions _jsonSerializerOptions = JsonSerializerExtensions.CreateSerializer();
- private long? _lastSequence = null;
+ private long? _lastSequence;
private DateTimeOffset _lastHeartbeatSend = DateTimeOffset.UnixEpoch;
private DateTimeOffset _lastHeartbeatAck = DateTimeOffset.UnixEpoch;
public TimeSpan Latency => _lastHeartbeatAck - _lastHeartbeatSend;
@@ -35,6 +39,9 @@ public partial class DiscordGatewayClient
};
_gatewayUri = uriBuilder.Uri;
_intents = opts.Intents;
+ _properties = opts.IdentifyProperties;
+ _shardInfo = opts.Shards;
+ _initialPresence = opts.InitialPresence;
}
public ConnectionStatus Status { get; private set; } = ConnectionStatus.Dead;
@@ -44,7 +51,6 @@ public partial class DiscordGatewayClient
/// ct is stored by the client and cancelling it will close the connection.
/// The caller must pause indefinitely or the bot will shut down immediately.
///
- [MemberNotNull("_ws")]
public async Task ConnectAsync(CancellationToken ct = default)
{
if (Status != ConnectionStatus.Dead)
@@ -59,7 +65,7 @@ public partial class DiscordGatewayClient
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(30));
- var (rawHelloPacketType, rawHelloPacket) = await ReadPacket(cts.Token);
+ var (rawHelloPacketType, rawHelloPacket) = await ReadPacketAsync(cts.Token);
if (rawHelloPacketType == WebSocketMessageType.Close)
throw new DiscordGatewayRequestError("First packet received was a close message");
@@ -73,10 +79,17 @@ public partial class DiscordGatewayClient
var __ = ReceiveLoopAsync(ct);
_logger.Debug("Sending IDENTIFY");
- await WritePacket(new GatewayPacket
+ await WritePacketAsync(new GatewayPacket
{
Opcode = GatewayOpcode.Identify,
- Payload = new IdentifyEvent { Token = _token, Intents = _intents }
+ Payload = new IdentifyEvent
+ {
+ Token = _token,
+ Intents = _intents,
+ Properties = _properties ?? new IdentifyProperties(),
+ Shards = _shardInfo,
+ Presence = _initialPresence?.ToPayload()
+ }
}, ct);
}
catch (Exception e)
@@ -98,7 +111,7 @@ public partial class DiscordGatewayClient
{
_logger.Debug("Sending heartbeat with sequence {Sequence}", _lastSequence);
_lastHeartbeatSend = DateTimeOffset.UtcNow;
- await WritePacket(new GatewayPacket { Opcode = GatewayOpcode.Heartbeat, Payload = _lastSequence }, ct);
+ await SendCommandAsync(new HeartbeatCommand(_lastSequence), ct);
}
}
@@ -108,7 +121,7 @@ public partial class DiscordGatewayClient
{
try
{
- var (type, packet) = await ReadPacket(ct);
+ var (type, packet) = await ReadPacketAsync(ct);
if (type == WebSocketMessageType.Close || packet == null)
{
// TODO: close websocket
@@ -146,14 +159,17 @@ public partial class DiscordGatewayClient
}
}
- private async ValueTask WritePacket(GatewayPacket packet, CancellationToken ct = default)
+ public async ValueTask SendCommandAsync(IGatewayCommand command, CancellationToken ct = default) =>
+ await WritePacketAsync(command.ToGatewayPacket(), ct);
+
+ private async ValueTask WritePacketAsync(GatewayPacket packet, CancellationToken ct = default)
{
using var buf = MemoryPool.Shared.Rent(BufferSize);
var json = JsonSerializer.SerializeToUtf8Bytes(packet, _jsonSerializerOptions);
await _ws!.SendAsync(json.AsMemory(), WebSocketMessageType.Text, true, ct);
}
- private async ValueTask<(WebSocketMessageType type, GatewayPacket? packet)> ReadPacket(
+ private async ValueTask<(WebSocketMessageType type, GatewayPacket? packet)> ReadPacketAsync(
CancellationToken ct = default)
{
using var buf = MemoryPool.Shared.Rent(BufferSize);
@@ -163,10 +179,10 @@ public partial class DiscordGatewayClient
if (res.EndOfMessage)
return DeserializePacket(res, buf.Memory.Span[..res.Count]);
- return await DeserializeMultipleBuffer(res, buf);
+ return await DeserializeMultipleBufferAsync(res, buf);
}
- private async Task<(WebSocketMessageType type, GatewayPacket packet)> DeserializeMultipleBuffer(
+ private async Task<(WebSocketMessageType type, GatewayPacket packet)> DeserializeMultipleBufferAsync(
ValueWebSocketReceiveResult res, IMemoryOwner buf)
{
await using var stream = new MemoryStream(BufferSize * 4);
@@ -228,4 +244,10 @@ public class DiscordGatewayClientOptions
public required string Token { get; init; }
public required string Uri { get; init; }
public required GatewayIntent Intents { get; init; }
+ public IdentifyProperties? IdentifyProperties { get; init; }
+ public int? ShardId { get; init; }
+ public int? ShardCount { get; init; }
+ public PresenceUpdateCommand? InitialPresence { get; init; }
+
+ internal int[]? Shards => ShardId != null && ShardCount != null ? [ShardId.Value, ShardCount.Value] : null;
}
\ No newline at end of file
diff --git a/Foxcord/Gateway/Events/Commands/HeartbeatCommand.cs b/Foxcord/Gateway/Events/Commands/HeartbeatCommand.cs
new file mode 100644
index 0000000..dd5f43b
--- /dev/null
+++ b/Foxcord/Gateway/Events/Commands/HeartbeatCommand.cs
@@ -0,0 +1,13 @@
+namespace Foxcord.Gateway.Events.Commands;
+
+public record HeartbeatCommand(long? Sequence) : IGatewayCommand
+{
+ public GatewayPacket ToGatewayPacket()
+ {
+ return new GatewayPacket
+ {
+ Opcode = GatewayOpcode.Heartbeat,
+ Payload = Sequence
+ };
+ }
+}
\ No newline at end of file
diff --git a/Foxcord/Gateway/Events/Commands/IGatewayCommand.cs b/Foxcord/Gateway/Events/Commands/IGatewayCommand.cs
new file mode 100644
index 0000000..a7a70e4
--- /dev/null
+++ b/Foxcord/Gateway/Events/Commands/IGatewayCommand.cs
@@ -0,0 +1,6 @@
+namespace Foxcord.Gateway.Events.Commands;
+
+public interface IGatewayCommand
+{
+ public GatewayPacket ToGatewayPacket();
+}
\ No newline at end of file
diff --git a/Foxcord/Gateway/Events/Commands/PresenceUpdateCommand.cs b/Foxcord/Gateway/Events/Commands/PresenceUpdateCommand.cs
new file mode 100644
index 0000000..71c31cb
--- /dev/null
+++ b/Foxcord/Gateway/Events/Commands/PresenceUpdateCommand.cs
@@ -0,0 +1,46 @@
+using Foxcord.Models;
+
+namespace Foxcord.Gateway.Events.Commands;
+
+public class PresenceUpdateCommand : IGatewayCommand
+{
+ public long? Since { get; init; }
+ public Activity[] Activities { get; init; } = [];
+ public PresenceStatusType Status { get; init; }
+ public bool Afk { get; init; }
+
+ public GatewayPacket ToGatewayPacket()
+ {
+ return new GatewayPacket
+ {
+ Opcode = GatewayOpcode.PresenceUpdate,
+ Payload = ToPayload()
+ };
+ }
+
+ public PresenceUpdatePayload ToPayload()
+ {
+ var status = Status switch
+ {
+ PresenceStatusType.Online => "online",
+ PresenceStatusType.Dnd => "dnd",
+ PresenceStatusType.Idle => "idle",
+ PresenceStatusType.Invisible => "invisible",
+ PresenceStatusType.Offline => "offline",
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ return new PresenceUpdatePayload(Since, Activities, status, Afk);
+ }
+
+ public record PresenceUpdatePayload(long? Since, Activity[] Activities, string Status, bool Afk);
+}
+
+public enum PresenceStatusType
+{
+ Online,
+ Dnd,
+ Idle,
+ Invisible,
+ Offline
+}
\ No newline at end of file
diff --git a/Foxcord/Gateway/Events/IdentifyEvent.cs b/Foxcord/Gateway/Events/IdentifyEvent.cs
index 7a11e8b..2648241 100644
--- a/Foxcord/Gateway/Events/IdentifyEvent.cs
+++ b/Foxcord/Gateway/Events/IdentifyEvent.cs
@@ -1,3 +1,6 @@
+using System.Text.Json.Serialization;
+using Foxcord.Gateway.Events.Commands;
+
namespace Foxcord.Gateway.Events;
public class IdentifyEvent : IGatewayEvent
@@ -5,6 +8,10 @@ public class IdentifyEvent : IGatewayEvent
public required string Token { get; init; }
public IdentifyProperties Properties { get; init; } = new();
public GatewayIntent Intents { get; init; }
+ public PresenceUpdateCommand.PresenceUpdatePayload? Presence { get; init; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int[]? Shards { get; init; }
}
public class IdentifyProperties
diff --git a/Foxcord/Models/Activity.cs b/Foxcord/Models/Activity.cs
new file mode 100644
index 0000000..65c814a
--- /dev/null
+++ b/Foxcord/Models/Activity.cs
@@ -0,0 +1,24 @@
+namespace Foxcord.Models;
+
+public record Activity(
+ string Name,
+ ActivityType Type,
+ string? Url = null,
+ DateTimeOffset CreatedAt = default,
+ ActivityTimestamps? Timestamps = null,
+ ActivityEmoji? Emoji = null,
+ string? State = null);
+
+public record ActivityTimestamps(int? Start = null, int? End = null);
+
+public record ActivityEmoji(string Name, Snowflake? Id = null, bool? Animated = null);
+
+public enum ActivityType
+{
+ Playing = 0,
+ Streaming = 1,
+ Listening = 2,
+ Watching = 3,
+ Custom = 4,
+ Competing = 5,
+}
\ No newline at end of file
diff --git a/Foxcord/Models/User.cs b/Foxcord/Models/User.cs
index ec0a169..055fb09 100644
--- a/Foxcord/Models/User.cs
+++ b/Foxcord/Models/User.cs
@@ -2,7 +2,7 @@ using System.Text.Json.Serialization;
namespace Foxcord.Models;
-public class User
+public record User
{
public Snowflake Id { get; init; }
public required string Username { get; init; }
diff --git a/FoxcordTest/Program.cs b/FoxcordTest/Program.cs
index 76f237f..6b5f9f5 100644
--- a/FoxcordTest/Program.cs
+++ b/FoxcordTest/Program.cs
@@ -1,4 +1,7 @@
using Foxcord.Gateway;
+using Foxcord.Gateway.Events;
+using Foxcord.Gateway.Events.Commands;
+using Foxcord.Models;
using Serilog;
using Foxcord.Rest;
@@ -18,7 +21,15 @@ var gateway = new DiscordGatewayClient(Log.Logger, new DiscordGatewayClientOptio
{
Token = Environment.GetEnvironmentVariable("TOKEN")!,
Uri = gatewayBot.Url,
- Intents = GatewayIntent.Guilds | GatewayIntent.GuildMessages | GatewayIntent.MessageContent
+ Intents = GatewayIntent.Guilds | GatewayIntent.GuildMessages | GatewayIntent.MessageContent,
+ InitialPresence = new PresenceUpdateCommand
+ {
+ Activities = [new Activity("balls", ActivityType.Custom, State: "gay gay homosexual gay")]
+ },
+ IdentifyProperties = new IdentifyProperties
+ {
+ Browser = "Discord iOS"
+ }
});
await gateway.ConnectAsync();