Skip to content

Commit 5d23aeb

Browse files
authored
Implement entry point command interaction (#123)
* Add support for entry point command interaction * Fix formatting * Improve entry point command UX * Update NetCord.Hosting.Services to include `AddEntryPointCommand` * Fix formatting * Add "The" * Improve consistency of xml docs
1 parent 9d4aeaa commit 5d23aeb

21 files changed

Lines changed: 392 additions & 3 deletions

Hosting/NetCord.Hosting.Services/ApplicationCommands/ApplicationCommandServiceHostExtensions.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,32 @@ public static IHost AddMessageCommand(this IHost host,
146146
return host;
147147
}
148148

149+
public static IHost AddEntryPointCommand(this IHost host,
150+
string name,
151+
string description,
152+
Delegate? handler = null,
153+
Permissions? defaultGuildUserPermissions = null,
154+
bool? dMPermission = null,
155+
bool defaultPermission = true,
156+
IEnumerable<ApplicationIntegrationType>? integrationTypes = null,
157+
IEnumerable<InteractionContextType>? contexts = null,
158+
bool nsfw = false,
159+
ulong? guildId = null)
160+
{
161+
var service = ServiceProviderServiceHelper.GetSingle<IApplicationCommandService>(host.Services);
162+
service.AddEntryPointCommand(name,
163+
description,
164+
handler,
165+
defaultGuildUserPermissions,
166+
dMPermission,
167+
defaultPermission,
168+
integrationTypes,
169+
contexts,
170+
nsfw,
171+
guildId);
172+
return host;
173+
}
174+
149175
public static IHost AddSlashCommand<TContext>(this IHost host,
150176
string name,
151177
string description,
@@ -245,4 +271,30 @@ public static IHost AddMessageCommand<TContext>(this IHost host,
245271
guildId);
246272
return host;
247273
}
274+
275+
public static IHost AddEntryPointCommand<TContext>(this IHost host,
276+
string name,
277+
string description,
278+
Delegate? handler = null,
279+
Permissions? defaultGuildUserPermissions = null,
280+
bool? dMPermission = null,
281+
bool defaultPermission = true,
282+
IEnumerable<ApplicationIntegrationType>? integrationTypes = null,
283+
IEnumerable<InteractionContextType>? contexts = null,
284+
bool nsfw = false,
285+
ulong? guildId = null) where TContext : IApplicationCommandContext
286+
{
287+
var service = host.Services.GetRequiredService<ApplicationCommandService<TContext>>();
288+
service.AddEntryPointCommand(name,
289+
description,
290+
handler,
291+
defaultGuildUserPermissions,
292+
dMPermission,
293+
defaultPermission,
294+
integrationTypes,
295+
contexts,
296+
nsfw,
297+
guildId);
298+
return host;
299+
}
248300
}

NetCord.Services/ApplicationCommands/ApplicationCommandContexts.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,36 @@ public class HttpMessageCommandContext(MessageCommandInteraction interaction, Re
136136
public User User => Interaction.User;
137137
public RestMessage Target => Interaction.Data.TargetMessage;
138138
}
139+
140+
public class BaseEntryPointCommandContext(EntryPointCommandInteraction interaction) : IApplicationCommandContext
141+
{
142+
public EntryPointCommandInteraction Interaction => interaction;
143+
144+
ApplicationCommandInteraction IApplicationCommandContext.Interaction => interaction;
145+
}
146+
147+
public class EntryPointCommandContext(EntryPointCommandInteraction interaction, GatewayClient client)
148+
: BaseEntryPointCommandContext(interaction),
149+
IGatewayClientContext,
150+
IGuildContext,
151+
IChannelContext,
152+
IUserContext
153+
{
154+
public GatewayClient Client => client;
155+
public Guild? Guild => Interaction.Guild;
156+
public TextChannel Channel => Interaction.Channel;
157+
public User User => Interaction.User;
158+
159+
ulong? IGuildContext.GuildId => Interaction.GuildId;
160+
}
161+
162+
public class HttpEntryPointCommandContext(EntryPointCommandInteraction interaction, RestClient client)
163+
: BaseEntryPointCommandContext(interaction),
164+
IRestClientContext,
165+
IChannelContext,
166+
IUserContext
167+
{
168+
public RestClient Client => client;
169+
public TextChannel Channel => Interaction.Channel;
170+
public User User => Interaction.User;
171+
}

NetCord.Services/ApplicationCommands/ApplicationCommandService.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,20 @@ private void AddModuleCore([DynamicallyAccessedMembers(DynamicallyAccessedMember
7676
slashCommandGroup = true;
7777
}
7878

79-
if (slashCommandGroup)
79+
bool entryPointCommand = false;
80+
81+
foreach (var entryPointCommandAttribute in type.GetCustomAttributes<EntryPointCommandAttribute>())
82+
{
83+
if (slashCommandGroup)
84+
throw new InvalidOperationException($"The type '{type}' cannot have both a slash command and an entry point command defined.");
85+
86+
EntryPointCommandInfo<TContext> entryPointCommandInfo = new(entryPointCommandAttribute, configuration);
87+
AddCommandInfo(entryPointCommandInfo);
88+
89+
entryPointCommand = true;
90+
}
91+
92+
if (slashCommandGroup || entryPointCommand)
8093
return;
8194

8295
foreach (var method in type.GetMethods())
@@ -95,6 +108,9 @@ private void AddModuleCore([DynamicallyAccessedMembers(DynamicallyAccessedMember
95108

96109
if (applicationCommandAttribute is MessageCommandAttribute messageCommandAttribute)
97110
AddCommandInfo(new MessageCommandInfo<TContext>(method, type, messageCommandAttribute, configuration));
111+
112+
if (applicationCommandAttribute is EntryPointCommandAttribute entryPointCommandAttribute)
113+
AddCommandInfo(new EntryPointCommandInfo<TContext>(method, type, entryPointCommandAttribute, configuration));
98114
}
99115
}
100116
}
@@ -196,6 +212,30 @@ public void AddMessageCommand(string name,
196212
_configuration));
197213
}
198214

215+
public void AddEntryPointCommand(string name,
216+
string description,
217+
Delegate? handler = null,
218+
Permissions? defaultGuildUserPermissions = null,
219+
bool? dMPermission = null,
220+
bool defaultPermission = true,
221+
IEnumerable<ApplicationIntegrationType>? integrationTypes = null,
222+
IEnumerable<InteractionContextType>? contexts = null,
223+
bool nsfw = false,
224+
ulong? guildId = null)
225+
{
226+
AddCommandInfo(new EntryPointCommandInfo<TContext>(name,
227+
description,
228+
handler,
229+
defaultGuildUserPermissions,
230+
dMPermission,
231+
defaultPermission,
232+
integrationTypes,
233+
contexts,
234+
nsfw,
235+
guildId,
236+
_configuration));
237+
}
238+
199239
void IApplicationCommandService.SetCommands(IEnumerable<KeyValuePair<ulong, IApplicationCommandInfo>> commands)
200240
{
201241
_commands = commands.ToFrozenDictionary(c => c.Key, c => (ApplicationCommandInfo<TContext>)c.Value);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace NetCord.Services.ApplicationCommands;
2+
3+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
4+
public class EntryPointCommandAttribute(string name, string description) : ApplicationCommandAttribute(name)
5+
{
6+
public string Description { get; } = description;
7+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Reflection;
3+
4+
using NetCord.Rest;
5+
using NetCord.Services.Helpers;
6+
7+
namespace NetCord.Services.ApplicationCommands;
8+
9+
internal class EntryPointCommandInfo<TContext> : ApplicationCommandInfo<TContext> where TContext : IApplicationCommandContext
10+
{
11+
internal EntryPointCommandInfo(MethodInfo method,
12+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type declaringType,
13+
EntryPointCommandAttribute attribute,
14+
ApplicationCommandServiceConfiguration<TContext> configuration) : base(attribute, configuration)
15+
{
16+
Description = attribute.Description;
17+
18+
Handler = EntryPointCommandHandler.ApplicationHandler;
19+
20+
MethodHelper.EnsureNoParameters(method);
21+
22+
Preconditions = PreconditionsHelper.GetPreconditions<TContext>(declaringType, method);
23+
24+
_invokeAsync = InvocationHelper.CreateModuleDelegate(method, declaringType, [], configuration.ResultResolverProvider, configuration.ServiceResolverProvider);
25+
}
26+
27+
internal EntryPointCommandInfo(string name,
28+
string description,
29+
Delegate? handler,
30+
Permissions? defaultGuildUserPermissions,
31+
bool? dMPermission,
32+
bool defaultPermission,
33+
IEnumerable<ApplicationIntegrationType>? integrationTypes,
34+
IEnumerable<InteractionContextType>? contexts,
35+
bool nsfw,
36+
ulong? guildId,
37+
ApplicationCommandServiceConfiguration<TContext> configuration) : base(name,
38+
defaultGuildUserPermissions,
39+
dMPermission,
40+
defaultPermission,
41+
integrationTypes,
42+
contexts,
43+
nsfw,
44+
guildId,
45+
configuration)
46+
{
47+
Description = description;
48+
49+
if (handler is null)
50+
{
51+
Handler = EntryPointCommandHandler.DiscordLaunchActivity;
52+
Preconditions = [];
53+
_invokeAsync = EmptyInvokeAsync;
54+
}
55+
else
56+
{
57+
Handler = EntryPointCommandHandler.ApplicationHandler;
58+
59+
var method = handler.Method;
60+
61+
var split = ParametersHelper.SplitHandlerParameters<TContext>(method);
62+
63+
MethodHelper.EnsureNoParameters(split.Parameters, method);
64+
65+
Preconditions = PreconditionsHelper.GetPreconditions<TContext>(method);
66+
67+
_invokeAsync = InvocationHelper.CreateHandlerDelegate(handler, split.Services, split.HasContext, [], configuration.ResultResolverProvider, configuration.ServiceResolverProvider);
68+
}
69+
}
70+
71+
internal EntryPointCommandInfo(EntryPointCommandAttribute attribute,
72+
ApplicationCommandServiceConfiguration<TContext> configuration) : base(attribute.Name,
73+
attribute._defaultGuildUserPermissions,
74+
attribute._dMPermission,
75+
#pragma warning disable CS0618 // Type or member is obsolete
76+
attribute.DefaultPermission,
77+
#pragma warning restore CS0618 // Type or member is obsolete
78+
attribute.IntegrationTypes,
79+
attribute.Contexts,
80+
attribute.Nsfw,
81+
attribute._guildId,
82+
configuration)
83+
{
84+
Description = attribute.Description;
85+
86+
Handler = EntryPointCommandHandler.DiscordLaunchActivity;
87+
88+
Preconditions = [];
89+
90+
_invokeAsync = EmptyInvokeAsync;
91+
}
92+
93+
private static ValueTask EmptyInvokeAsync(object?[]? parameters, TContext context, IServiceProvider? serviceProvider) => default;
94+
95+
public string Description { get; }
96+
public EntryPointCommandHandler Handler { get; }
97+
public IReadOnlyList<PreconditionAttribute<TContext>> Preconditions { get; }
98+
99+
private readonly Func<object?[]?, TContext, IServiceProvider?, ValueTask> _invokeAsync;
100+
101+
public override async ValueTask<IExecutionResult> InvokeAsync(TContext context, ApplicationCommandServiceConfiguration<TContext> configuration, IServiceProvider? serviceProvider)
102+
{
103+
var preconditionResult = await PreconditionsHelper.EnsureCanExecuteAsync(Preconditions, context, serviceProvider).ConfigureAwait(false);
104+
if (preconditionResult is IFailResult)
105+
return preconditionResult;
106+
107+
try
108+
{
109+
await _invokeAsync(null, context, serviceProvider).ConfigureAwait(false);
110+
}
111+
catch (Exception ex)
112+
{
113+
return new ExecutionExceptionResult(ex);
114+
}
115+
116+
return SuccessResult.Instance;
117+
}
118+
119+
public override async ValueTask<ApplicationCommandProperties> GetRawValueAsync(CancellationToken cancellationToken = default)
120+
{
121+
#pragma warning disable CS0618 // Type or member is obsolete
122+
return new EntryPointCommandProperties(Name, Description, Handler)
123+
{
124+
NameLocalizations = LocalizationsProvider is null ? null : await LocalizationsProvider.GetLocalizationsAsync(LocalizationPath.Add(NameLocalizationPathSegment.Instance), cancellationToken).ConfigureAwait(false),
125+
DescriptionLocalizations = LocalizationsProvider is null ? null : await LocalizationsProvider.GetLocalizationsAsync(LocalizationPath.Add(DescriptionLocalizationPathSegment.Instance), cancellationToken).ConfigureAwait(false),
126+
DefaultGuildUserPermissions = DefaultGuildUserPermissions,
127+
DMPermission = DMPermission,
128+
DefaultPermission = DefaultPermission,
129+
IntegrationTypes = IntegrationTypes,
130+
Contexts = Contexts,
131+
Nsfw = Nsfw,
132+
};
133+
#pragma warning restore CS0618 // Type or member is obsolete
134+
}
135+
}

NetCord.Services/ApplicationCommands/IApplicationCommandService.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,15 @@ public void AddMessageCommand(string name,
5757
IEnumerable<InteractionContextType>? contexts = null,
5858
bool nsfw = false,
5959
ulong? guildId = null);
60+
61+
public void AddEntryPointCommand(string name,
62+
string description,
63+
Delegate? handler = null,
64+
Permissions? defaultGuildUserPermissions = null,
65+
bool? dMPermission = null,
66+
bool defaultPermission = true,
67+
IEnumerable<ApplicationIntegrationType>? integrationTypes = null,
68+
IEnumerable<InteractionContextType>? contexts = null,
69+
bool nsfw = false,
70+
ulong? guildId = null);
6071
}

NetCord.Services/Helpers/MethodHelper.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,15 @@ public static bool EnsureSingleParameterOfTypeOrNone(ReadOnlySpan<ParameterInfo>
2323
throw new InvalidDefinitionException($"The command must have no parameters or a single parameter of type '{parameterType}'.", method);
2424
}
2525
}
26+
27+
public static void EnsureNoParameters(MethodInfo method)
28+
{
29+
EnsureNoParameters(method.GetParameters(), method);
30+
}
31+
32+
public static void EnsureNoParameters(ReadOnlySpan<ParameterInfo> parameters, MethodInfo method)
33+
{
34+
if (parameters.Length is not 0)
35+
throw new InvalidDefinitionException("The command must have no parameters.", method);
36+
}
2637
}

NetCord/ApplicationCommandInteractionData.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@ public class ApplicationCommandInteractionData(JsonModels.JsonInteractionData js
1919
/// The invoked <see cref="Rest.ApplicationCommand"/>'s type.
2020
/// </summary>
2121
public ApplicationCommandType Type => _jsonModel.Type.GetValueOrDefault();
22+
23+
/// <summary>
24+
/// The ID of the guild the <see cref="Rest.ApplicationCommand"/> is registered to.
25+
/// </summary>
26+
public ulong? GuildId => _jsonModel.GuildId;
2227
}

NetCord/ApplicationCommandType.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@ public enum ApplicationCommandType
1919
/// UI-based. Displayed when right clicking or tapping on a message.
2020
/// </summary>
2121
Message = 3,
22+
23+
/// <summary>
24+
/// UI-based. Represents the primary way to invoke an app's Activity.
25+
/// </summary>
26+
EntryPoint = 4,
2227
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using NetCord.Gateway;
2+
using NetCord.Rest;
3+
4+
namespace NetCord;
5+
6+
public class EntryPointCommandInteraction(JsonModels.JsonInteraction jsonModel, Guild? guild, InteractionResponseDelegate sendResponseAsync, RestClient client) : ApplicationCommandInteraction(jsonModel, guild, sendResponseAsync, client)
7+
{
8+
public override EntryPointCommandInteractionData Data { get; } = new(jsonModel.Data!);
9+
}

0 commit comments

Comments
 (0)