bot: add system member list commands
This commit is contained in:
		| @@ -97,9 +97,9 @@ namespace PluralKit.Bot | ||||
|  | ||||
|             // Deliberately wrapping in an async function *without* awaiting, we don't want to "block" since this'd hold up the main loop | ||||
|             // These handlers return Task so we gotta be careful not to return the Task itself (which would then be awaited) - kinda weird design but eh | ||||
|             _client.MessageReceived += async (msg) => MessageReceived(msg); | ||||
|             _client.ReactionAdded += async (message, channel, reaction) => _proxy.HandleReactionAddedAsync(message, channel, reaction); | ||||
|             _client.MessageDeleted += async (message, channel) => _proxy.HandleMessageDeletedAsync(message, channel); | ||||
|             _client.MessageReceived += async (msg) => MessageReceived(msg).CatchException(HandleRuntimeError); | ||||
|             _client.ReactionAdded += async (message, channel, reaction) => _proxy.HandleReactionAddedAsync(message, channel, reaction).CatchException(HandleRuntimeError); | ||||
|             _client.MessageDeleted += async (message, channel) => _proxy.HandleMessageDeletedAsync(message, channel).CatchException(HandleRuntimeError); | ||||
|         } | ||||
|  | ||||
|         private async Task UpdatePeriodic() | ||||
| @@ -110,7 +110,7 @@ namespace PluralKit.Bot | ||||
|  | ||||
|         private async Task Ready() | ||||
|         { | ||||
|             _updateTimer = new Timer((_) => Task.Run(this.UpdatePeriodic), null, 0, 60*1000); | ||||
|             _updateTimer = new Timer((_) => this.UpdatePeriodic(), null, 0, 60*1000); | ||||
|  | ||||
|             Console.WriteLine($"Shard #{_client.ShardId} connected to {_client.Guilds.Sum(g => g.Channels.Count)} channels in {_client.Guilds.Count} guilds."); | ||||
|             Console.WriteLine($"PluralKit started as {_client.CurrentUser.Username}#{_client.CurrentUser.Discriminator} ({_client.CurrentUser.Id})."); | ||||
| @@ -129,7 +129,7 @@ namespace PluralKit.Bot | ||||
|                     } else if (exception is TimeoutException) { | ||||
|                         await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} Operation timed out. Try being faster next time :)"); | ||||
|                     } else { | ||||
|                         HandleRuntimeError(ctx.Message as SocketMessage, (_result as ExecuteResult?)?.Exception); | ||||
|                         HandleRuntimeError((_result as ExecuteResult?)?.Exception); | ||||
|                     } | ||||
|                 } else if ((_result.Error == CommandError.BadArgCount || _result.Error == CommandError.MultipleMatches) && cmd.IsSpecified) { | ||||
|                     await ctx.Message.Channel.SendMessageAsync($"{Emojis.Error} {_result.ErrorReason}\n**Usage: **pk;{cmd.Value.Remarks}"); | ||||
| @@ -141,36 +141,31 @@ namespace PluralKit.Bot | ||||
|  | ||||
|         private async Task MessageReceived(SocketMessage _arg) | ||||
|         { | ||||
|             try { | ||||
|                 // Ignore system messages (member joined, message pinned, etc) | ||||
|                 var arg = _arg as SocketUserMessage; | ||||
|                 if (arg == null) return; | ||||
|             // Ignore system messages (member joined, message pinned, etc) | ||||
|             var arg = _arg as SocketUserMessage; | ||||
|             if (arg == null) return; | ||||
|  | ||||
|                 // Ignore bot messages | ||||
|                 if (arg.Author.IsBot || arg.Author.IsWebhook) return; | ||||
|             // Ignore bot messages | ||||
|             if (arg.Author.IsBot || arg.Author.IsWebhook) return; | ||||
|  | ||||
|                 int argPos = 0; | ||||
|                 // Check if message starts with the command prefix | ||||
|                 if (arg.HasStringPrefix("pk;", ref argPos) || arg.HasStringPrefix("pk!", ref argPos) || arg.HasMentionPrefix(_client.CurrentUser, ref argPos)) | ||||
|                 { | ||||
|                     // If it does, fetch the sender's system (because most commands need that) into the context, | ||||
|                     // and start command execution | ||||
|                     // Note system may be null if user has no system, hence `OrDefault` | ||||
|                     var system = await _connection.QueryFirstOrDefaultAsync<PKSystem>("select systems.* from systems, accounts where accounts.uid = @Id and systems.id = accounts.system", new { Id = arg.Author.Id }); | ||||
|                     await _commands.ExecuteAsync(new PKCommandContext(_client, arg as SocketUserMessage, _connection, system), argPos, _services); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     // If not, try proxying anyway | ||||
|                     await _proxy.HandleMessageAsync(arg); | ||||
|                 } | ||||
|             } catch (Exception e) { | ||||
|                 // Generic exception handler | ||||
|                 HandleRuntimeError(_arg, e); | ||||
|             int argPos = 0; | ||||
|             // Check if message starts with the command prefix | ||||
|             if (arg.HasStringPrefix("pk;", ref argPos) || arg.HasStringPrefix("pk!", ref argPos) || arg.HasMentionPrefix(_client.CurrentUser, ref argPos)) | ||||
|             { | ||||
|                 // If it does, fetch the sender's system (because most commands need that) into the context, | ||||
|                 // and start command execution | ||||
|                 // Note system may be null if user has no system, hence `OrDefault` | ||||
|                 var system = await _connection.QueryFirstOrDefaultAsync<PKSystem>("select systems.* from systems, accounts where accounts.uid = @Id and systems.id = accounts.system", new { Id = arg.Author.Id }); | ||||
|                 await _commands.ExecuteAsync(new PKCommandContext(_client, arg as SocketUserMessage, _connection, system), argPos, _services); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // If not, try proxying anyway | ||||
|                 await _proxy.HandleMessageAsync(arg); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void HandleRuntimeError(SocketMessage arg, Exception e) | ||||
|         private void HandleRuntimeError(Exception e) | ||||
|         { | ||||
|             Console.Error.WriteLine(e); | ||||
|         } | ||||
|   | ||||
| @@ -33,7 +33,6 @@ namespace PluralKit.Bot.Commands | ||||
|  | ||||
|             var system = await Systems.Create(systemName); | ||||
|             await Systems.Link(system, Context.User.Id); | ||||
|  | ||||
|             await Context.Channel.SendMessageAsync($"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;help` for more information about commands you can use now."); | ||||
|         } | ||||
|  | ||||
| @@ -91,6 +90,50 @@ namespace PluralKit.Bot.Commands | ||||
|             await Context.Channel.SendMessageAsync($"{Emojis.Success} System deleted."); | ||||
|         } | ||||
|  | ||||
|         [Group("list")] | ||||
|         public class SystemListCommands: ModuleBase<PKCommandContext> { | ||||
|             public MemberStore Members { get; set; } | ||||
|  | ||||
|             [Command] | ||||
|             public async Task MemberShortList() { | ||||
|                 var system = Context.GetContextEntity<PKSystem>() ?? Context.SenderSystem; | ||||
|                 if (system == null) Context.RaiseNoSystemError(); | ||||
|  | ||||
|                 var members = await Members.GetBySystem(system); | ||||
|                 var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`"; | ||||
|                 await Context.Paginate<PKMember>( | ||||
|                     members.ToList(), | ||||
|                     25, | ||||
|                     embedTitle, | ||||
|                     (eb, ms) => eb.Description = string.Join("\n", ms.Select((m) => $"[`{m.Hid}`] **{m.Name}** *({m.Prefix ?? ""}text{m.Suffix ?? ""})*")) | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             [Command("full")] | ||||
|             [Alias("big", "details", "long")] | ||||
|             public async Task MemberLongList() { | ||||
|                 var system = Context.GetContextEntity<PKSystem>() ?? Context.SenderSystem; | ||||
|                 if (system == null) Context.RaiseNoSystemError(); | ||||
|  | ||||
|                 var members = await Members.GetBySystem(system); | ||||
|                 var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`"; | ||||
|                 await Context.Paginate<PKMember>( | ||||
|                     members.ToList(), | ||||
|                     10, | ||||
|                     embedTitle, | ||||
|                     (eb, ms) => { | ||||
|                         foreach (var member in ms) { | ||||
|                             var profile = $"**ID**: {member.Hid}"; | ||||
|                             if (member.Pronouns != null) profile += $"\n**Pronouns**: {member.Pronouns}"; | ||||
|                             if (member.Birthday != null) profile += $"\n**Birthdate**: {member.BirthdayString}"; | ||||
|                             if (member.Description != null) profile += $"\n\n{member.Description}"; | ||||
|                             eb.AddField(member.Name, profile); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public override async Task<PKSystem> ReadContextParameterAsync(string value) | ||||
|         { | ||||
|             var res = await new PKSystemTypeReader().ReadAsync(Context, value, _services); | ||||
|   | ||||
							
								
								
									
										101
									
								
								PluralKit/Bot/ContextUtils.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								PluralKit/Bot/ContextUtils.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Discord; | ||||
| using Discord.Commands; | ||||
| using Discord.WebSocket; | ||||
|  | ||||
| namespace PluralKit.Bot { | ||||
|     public static class ContextUtils { | ||||
|                 public static async Task<bool> PromptYesNo(this ICommandContext ctx, IUserMessage message, TimeSpan? timeout = null) { | ||||
|             await message.AddReactionsAsync(new[] {new Emoji(Emojis.Success), new Emoji(Emojis.Error)}); | ||||
|             var reaction = await ctx.AwaitReaction(message, ctx.User, (r) => r.Emote.Name == Emojis.Success || r.Emote.Name == Emojis.Error, timeout ?? TimeSpan.FromMinutes(1)); | ||||
|             return reaction.Emote.Name == Emojis.Success; | ||||
|         } | ||||
|  | ||||
|         public static async Task<SocketReaction> AwaitReaction(this ICommandContext ctx, IUserMessage message, IUser user = null, Func<SocketReaction, bool> predicate = null, TimeSpan? timeout = null) { | ||||
|             var tcs = new TaskCompletionSource<SocketReaction>(); | ||||
|             Task Inner(Cacheable<IUserMessage, ulong> _message, ISocketMessageChannel _channel, SocketReaction reaction) { | ||||
|                 if (message.Id != _message.Id) return Task.CompletedTask; // Ignore reactions for different messages | ||||
|                 if (user != null && user.Id != reaction.UserId) return Task.CompletedTask; // Ignore messages from other users if a user was defined | ||||
|                 if (predicate != null && !predicate.Invoke(reaction)) return Task.CompletedTask; // Check predicate | ||||
|                 tcs.SetResult(reaction); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
|  | ||||
|             (ctx.Client as BaseSocketClient).ReactionAdded += Inner; | ||||
|             try { | ||||
|                 return await (tcs.Task.TimeoutAfter(timeout)); | ||||
|             } finally { | ||||
|                 (ctx.Client as BaseSocketClient).ReactionAdded -= Inner; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public static async Task<IUserMessage> AwaitMessage(this ICommandContext ctx, IMessageChannel channel, IUser user = null, Func<SocketMessage, bool> predicate = null, TimeSpan? timeout = null) { | ||||
|             var tcs = new TaskCompletionSource<IUserMessage>(); | ||||
|             Task Inner(SocketMessage msg) { | ||||
|                 if (channel != msg.Channel) return Task.CompletedTask; // Ignore messages in a different channel | ||||
|                 if (user != null && user != msg.Author) return Task.CompletedTask; // Ignore messages from other users | ||||
|                 if (predicate != null && !predicate.Invoke(msg)) return Task.CompletedTask; // Check predicate | ||||
|  | ||||
|                 (ctx.Client as BaseSocketClient).MessageReceived -= Inner; | ||||
|                 tcs.SetResult(msg as IUserMessage); | ||||
|                  | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
|  | ||||
|             (ctx.Client as BaseSocketClient).MessageReceived += Inner; | ||||
|             return await (tcs.Task.TimeoutAfter(timeout)); | ||||
|         } | ||||
|  | ||||
|         public static async Task Paginate<T>(this ICommandContext ctx, ICollection<T> items, int itemsPerPage, string title, Action<EmbedBuilder, IEnumerable<T>> renderer) { | ||||
|             var pageCount = (items.Count / itemsPerPage) + 1; | ||||
|             Embed MakeEmbedForPage(int page) { | ||||
|                 var eb = new EmbedBuilder(); | ||||
|                 eb.Title = pageCount > 1 ? $"[{page+1}/{pageCount}] {title}" : title; | ||||
|                 renderer(eb, items.Skip(page*itemsPerPage).Take(itemsPerPage)); | ||||
|                 return eb.Build(); | ||||
|             } | ||||
|  | ||||
|             var msg = await ctx.Channel.SendMessageAsync(embed: MakeEmbedForPage(0)); | ||||
|             var botEmojis = new[] { new Emoji("\u23EA"), new Emoji("\u2B05"), new Emoji("\u27A1"), new Emoji("\u23E9"), new Emoji(Emojis.Error) }; | ||||
|             await msg.AddReactionsAsync(botEmojis); | ||||
|  | ||||
|             try { | ||||
|                 var currentPage = 0; | ||||
|                 while (true) { | ||||
|                     var reaction = await ctx.AwaitReaction(msg, ctx.User, timeout: TimeSpan.FromMinutes(5)); | ||||
|  | ||||
|                     // Increment/decrement page counter based on which reaction was clicked | ||||
|                     if (reaction.Emote.Name == "\u23EA") currentPage = 0; // << | ||||
|                     if (reaction.Emote.Name == "\u2B05") currentPage = (currentPage - 1) % pageCount; // < | ||||
|                     if (reaction.Emote.Name == "\u27A1") currentPage = (currentPage + 1) % pageCount; // > | ||||
|                     if (reaction.Emote.Name == "\u23E9") currentPage = pageCount - 1; // >> | ||||
|                     if (reaction.Emote.Name == Emojis.Error) break; // X | ||||
|                      | ||||
|                     // If we can, remove the user's reaction (so they can press again quickly) | ||||
|                     if (await ctx.HasPermission(ChannelPermission.ManageMessages) && reaction.User.IsSpecified) await msg.RemoveReactionAsync(reaction.Emote, reaction.User.Value); | ||||
|                      | ||||
|                     // Edit the embed with the new page | ||||
|                     await msg.ModifyAsync((mp) => mp.Embed = MakeEmbedForPage(currentPage)); | ||||
|                 } | ||||
|             } catch (TimeoutException) { | ||||
|                 // "escape hatch", clean up as if we hit X | ||||
|             } | ||||
|  | ||||
|             if (await ctx.HasPermission(ChannelPermission.ManageMessages)) await msg.RemoveAllReactionsAsync(); | ||||
|             else await msg.RemoveReactionsAsync(ctx.Client.CurrentUser, botEmojis); | ||||
|         } | ||||
|  | ||||
|         public static async Task<ChannelPermissions> Permissions(this ICommandContext ctx) { | ||||
|             if (ctx.Channel is IGuildChannel) { | ||||
|                 var gu = await ctx.Guild.GetCurrentUserAsync(); | ||||
|                 return gu.GetPermissions(ctx.Channel as IGuildChannel); | ||||
|             } | ||||
|             return ChannelPermissions.DM; | ||||
|         } | ||||
|  | ||||
|         public static async Task<bool> HasPermission(this ICommandContext ctx, ChannelPermission permission) => (await Permissions(ctx)).Has(permission); | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Data; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using Dapper; | ||||
| using Discord; | ||||
| @@ -168,50 +170,6 @@ namespace PluralKit.Bot | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class ContextExt { | ||||
|         public static async Task<bool> PromptYesNo(this ICommandContext ctx, IUserMessage message, TimeSpan? timeout = null) { | ||||
|             await message.AddReactionsAsync(new[] {new Emoji(Emojis.Success), new Emoji(Emojis.Error)}); | ||||
|             var reaction = await ctx.AwaitReaction(message, ctx.User, (r) => r.Emote.Name == Emojis.Success || r.Emote.Name == Emojis.Error, timeout ?? TimeSpan.FromMinutes(1)); | ||||
|             return reaction.Emote.Name == Emojis.Success; | ||||
|         } | ||||
|  | ||||
|         public static async Task<SocketReaction> AwaitReaction(this ICommandContext ctx, IUserMessage message, IUser user = null, Func<SocketReaction, bool> predicate = null, TimeSpan? timeout = null) { | ||||
|             var tcs = new TaskCompletionSource<SocketReaction>(); | ||||
|             Task Inner(Cacheable<IUserMessage, ulong> _message, ISocketMessageChannel _channel, SocketReaction reaction) { | ||||
|                 if (message.Id != _message.Id) return Task.CompletedTask; // Ignore reactions for different messages | ||||
|                 if (user != null && user.Id != reaction.UserId) return Task.CompletedTask; // Ignore messages from other users if a user was defined | ||||
|                 if (predicate != null && !predicate.Invoke(reaction)) return Task.CompletedTask; // Check predicate | ||||
|                 tcs.SetResult(reaction); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
|  | ||||
|             (ctx.Client as BaseSocketClient).ReactionAdded += Inner; | ||||
|             try { | ||||
|                 return await (tcs.Task.TimeoutAfter(timeout)); | ||||
|             } finally { | ||||
|                 (ctx.Client as BaseSocketClient).ReactionAdded -= Inner; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public static async Task<IUserMessage> AwaitMessage(this ICommandContext ctx, IMessageChannel channel, IUser user = null, Func<SocketMessage, bool> predicate = null, TimeSpan? timeout = null) { | ||||
|             var tcs = new TaskCompletionSource<IUserMessage>(); | ||||
|             Task Inner(SocketMessage msg) { | ||||
|                 if (channel != msg.Channel) return Task.CompletedTask; // Ignore messages in a different channel | ||||
|                 if (user != null && user != msg.Author) return Task.CompletedTask; // Ignore messages from other users | ||||
|                 if (predicate != null && !predicate.Invoke(msg)) return Task.CompletedTask; // Check predicate | ||||
|                 tcs.SetResult(msg as IUserMessage); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
|  | ||||
|             (ctx.Client as BaseSocketClient).MessageReceived += Inner; | ||||
|             try { | ||||
|                 return await (tcs.Task.TimeoutAfter(timeout)); | ||||
|             } finally { | ||||
|                 (ctx.Client as BaseSocketClient).MessageReceived -= Inner; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     class PKError : Exception | ||||
|     { | ||||
|         public PKError(string message) : base(message) | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| using System; | ||||
| using Dapper.Contrib.Extensions; | ||||
|  | ||||
| namespace PluralKit { | ||||
| namespace PluralKit | ||||
| { | ||||
|     [Table("systems")] | ||||
|     public class PKSystem { | ||||
|     public class PKSystem | ||||
|     { | ||||
|         [Key] | ||||
|         public int Id { get; set; } | ||||
|         public string Hid { get; set; } | ||||
| @@ -17,18 +19,30 @@ namespace PluralKit { | ||||
|     } | ||||
|  | ||||
|     [Table("members")] | ||||
|     public class PKMember { | ||||
|     public class PKMember | ||||
|     { | ||||
|         public int Id { get; set; } | ||||
|         public string Hid { get; set; } | ||||
|         public int System { get; set; } | ||||
|         public string Color { get; set; } | ||||
|         public string AvatarUrl { get; set; } | ||||
|         public string Name { get; set; } | ||||
|         public DateTime Date { get; set; } | ||||
|         public DateTime? Birthday { get; set; } | ||||
|         public string Pronouns { get; set; } | ||||
|         public string Description { get; set; } | ||||
|         public string Prefix { get; set; } | ||||
|         public string Suffix { get; set; } | ||||
|         public DateTime Created { get; set; } | ||||
|  | ||||
|         /// Returns a formatted string representing the member's birthday, taking into account that a year of "0001" is hidden | ||||
|         public string BirthdayString | ||||
|         { | ||||
|             get | ||||
|             { | ||||
|                 if (Birthday == null) return null; | ||||
|                 if (Birthday?.Year == 1) return Birthday?.ToString("MMMM dd"); | ||||
|                 return Birthday?.ToString("MMMM dd, yyyy"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -125,7 +125,7 @@ namespace PluralKit { | ||||
|                 msg.System = system; | ||||
|                 msg.Member = member; | ||||
|                 return msg; | ||||
|             }, new { Id = id })).First(); | ||||
|             }, new { Id = id })).FirstOrDefault(); | ||||
|         } | ||||
|          | ||||
|         public async Task Delete(ulong id) { | ||||
|   | ||||
							
								
								
									
										28
									
								
								PluralKit/TaskUtils.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								PluralKit/TaskUtils.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| using System; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace PluralKit { | ||||
|     public static class TaskUtils { | ||||
|         public static async Task CatchException(this Task task, Action<Exception> handler) { | ||||
|             try { | ||||
|                 await task; | ||||
|             } catch (Exception e) { | ||||
|                 handler(e); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan? timeout) { | ||||
|             // https://stackoverflow.com/a/22078975 | ||||
|             using (var timeoutCancellationTokenSource = new CancellationTokenSource()) { | ||||
|                 var completedTask = await Task.WhenAny(task, Task.Delay(timeout ?? TimeSpan.FromMilliseconds(-1), timeoutCancellationTokenSource.Token)); | ||||
|                 if (completedTask == task) { | ||||
|                     timeoutCancellationTokenSource.Cancel(); | ||||
|                     return await task;  // Very important in order to propagate exceptions | ||||
|                 } else { | ||||
|                     throw new TimeoutException(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -29,19 +29,6 @@ namespace PluralKit | ||||
|             if (str.Length < maxLength) return str; | ||||
|             return str.Substring(0, maxLength - ellipsis.Length) + ellipsis; | ||||
|         } | ||||
|  | ||||
|         public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan? timeout) { | ||||
|             // https://stackoverflow.com/a/22078975 | ||||
|             using (var timeoutCancellationTokenSource = new CancellationTokenSource()) { | ||||
|                 var completedTask = await Task.WhenAny(task, Task.Delay(timeout ?? TimeSpan.FromMilliseconds(-1), timeoutCancellationTokenSource.Token)); | ||||
|                 if (completedTask == task) { | ||||
|                     timeoutCancellationTokenSource.Cancel(); | ||||
|                     return await task;  // Very important in order to propagate exceptions | ||||
|                 } else { | ||||
|                     throw new TimeoutException(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static class Emojis { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user