feat: identify, presence update commands
This commit is contained in:
parent
b3bf3a7c16
commit
6c335568f5
11 changed files with 194 additions and 68 deletions
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
|
@ -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<ReadyEvent>(_jsonSerializerOptions)!;
|
||||
case DispatchEventTypeName.GuildCreate:
|
||||
return rawPayload.Deserialize<GuildCreateEvent>(_jsonSerializerOptions)!;
|
||||
case DispatchEventTypeName.MessageCreate:
|
||||
return rawPayload.Deserialize<MessageCreateEvent>(_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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ReadyEvent>(_jsonSerializerOptions)!;
|
||||
case DispatchEventTypeName.GuildCreate:
|
||||
return rawPayload.Deserialize<GuildCreateEvent>(_jsonSerializerOptions)!;
|
||||
case DispatchEventTypeName.MessageCreate:
|
||||
return rawPayload.Deserialize<MessageCreateEvent>(_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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|||
/// <c>ct</c> is stored by the client and cancelling it will close the connection.
|
||||
/// The caller must pause indefinitely or the bot will shut down immediately.
|
||||
/// </summary>
|
||||
[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<byte>.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<byte>.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<byte> 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;
|
||||
}
|
13
Foxcord/Gateway/Events/Commands/HeartbeatCommand.cs
Normal file
13
Foxcord/Gateway/Events/Commands/HeartbeatCommand.cs
Normal file
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
6
Foxcord/Gateway/Events/Commands/IGatewayCommand.cs
Normal file
6
Foxcord/Gateway/Events/Commands/IGatewayCommand.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace Foxcord.Gateway.Events.Commands;
|
||||
|
||||
public interface IGatewayCommand
|
||||
{
|
||||
public GatewayPacket ToGatewayPacket();
|
||||
}
|
46
Foxcord/Gateway/Events/Commands/PresenceUpdateCommand.cs
Normal file
46
Foxcord/Gateway/Events/Commands/PresenceUpdateCommand.cs
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
24
Foxcord/Models/Activity.cs
Normal file
24
Foxcord/Models/Activity.cs
Normal file
|
@ -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,
|
||||
}
|
|
@ -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; }
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue