public MainDialog(BotServices services, ConversationState conversationState, UserState userState, IBotTelemetryClient telemetryClient) : base(nameof(MainDialog)) { _services = services ?? throw new ArgumentNullException(nameof(services)); _conversationState = conversationState; _userState = userState; TelemetryClient = telemetryClient; // QnA Maker dialog options _qnaMakerOptions = new QnAMakerOptions { Top = 3, ScoreThreshold = 0.03F, }; AddDialog(new OnboardingDialog(_services, _userState.CreateProperty <OnboardingState>(nameof(OnboardingState)), telemetryClient)); AddDialog(new EscalateDialog(_services)); AddDialog(new QnADialog(_services, QnAMakerKey)); }
public Task GetChannel(CommandContext ctx, [Description("Channel to retrieve information from")][RemainingText] DiscordChannel channel = null) { // Set the current channel for viewing if one isn't provided by the user channel = channel ?? ctx.Channel; // Check that the user has the permission in the channel to view its information if (!ctx.Member.PermissionsIn(channel).HasPermission(Permissions.AccessChannels)) { return(BotServices.SendEmbedAsync(ctx, "You are not allowed to see this channel!", EmbedType.Warning)); } // Create the base embed message var output = new DiscordEmbedBuilder() .WithTitle(channel.Name) .WithDescription("ID: " + channel.Id) .AddField("Topic", channel.Topic ?? string.Empty) .AddField("Type", channel.Type.ToString(), true) .AddField("Private", channel.IsPrivate ? "Yes" : "No", true) .AddField("NSFW", channel.IsNSFW ? "Yes" : "No", true) .WithThumbnailUrl(ctx.Guild.IconUrl) .WithFooter("Created on " + channel.CreationTimestamp.DateTime.ToString(CultureInfo.InvariantCulture)) .WithColor(SharedData.DefaultColor); // Add additional fields depending on the channel type switch (channel.Type) { case ChannelType.Voice: output.AddField("Bitrate", channel.Bitrate.ToString() ?? "Unknown", true); output.AddField("User limit", (channel.UserLimit > 0) ? channel.UserLimit.ToString() : "No limit.", true); break; case ChannelType.Category: var channels = new StringBuilder(); foreach (var chn in channel.Children) { channels.Append($"[`{chn.Name}`]"); } output.AddField("Channels", (channels.Length > 0) ? channels.ToString() : "None", true); break; } return(ctx.RespondAsync(embed: output.Build())); }
public async Task LeaveAsync(CommandContext ctx) { await ctx.RespondAsync($"Are you sure you want {SharedData.Name} to leave this server?").ConfigureAwait(false); var message = await ctx.RespondAsync("Respond with **yes** to proceed or wait 10 seconds to cancel this operation.").ConfigureAwait(false); var interactivity = await BotServices.GetUserInteractivity(ctx, "yes", 10).ConfigureAwait(false); if (interactivity.Result is null) { await message.ModifyAsync("~~" + message.Content + "~~ " + Resources.REQUEST_TIMEOUT).ConfigureAwait(false); } else { await BotServices.SendEmbedAsync(ctx, "Thank you for using " + SharedData.Name).ConfigureAwait(false); await ctx.Guild.LeaveAsync().ConfigureAwait(false); } }
public OnboardingDialog(BotServices botServices, IStatePropertyAccessor <OnboardingState> accessor) : base(botServices, nameof(OnboardingDialog)) { _accessor = accessor; InitialDialogId = nameof(OnboardingDialog); var onboarding = new WaterfallStep[] { AskForName, AskForEmail, AskForLocation, FinishOnboardingDialog, }; AddDialog(new WaterfallDialog(InitialDialogId, onboarding)); AddDialog(new TextPrompt(NamePrompt)); AddDialog(new TextPrompt(EmailPrompt)); AddDialog(new TextPrompt(LocationPrompt)); }
public FindStoreDialog( BotServices services, IStatePropertyAccessor <CustomerSupportTemplateState> stateAccessor) : base(services, nameof(FindStoreDialog)) { _client = new DemoServiceClient(); _services = services; _stateAccessor = stateAccessor; var findStore = new WaterfallStep[] { PromptForZipCode, ShowStores, }; InitialDialogId = nameof(FindStoreDialog); AddDialog(new WaterfallDialog(InitialDialogId, findStore)); AddDialog(new TextPrompt(DialogIds.ZipCodePrompt, SharedValidators.ZipCodeValidator)); }
public CreateTicketDialog( BotSettings settings, BotServices services, ResponseManager responseManager, ConversationState conversationState, IServiceManager serviceManager, IBotTelemetryClient telemetryClient) : base(nameof(CreateTicketDialog), settings, services, responseManager, conversationState, serviceManager, telemetryClient) { var createTicket = new WaterfallStep[] { CheckDescription, InputDescription, SetDescription, DisplayExistingLoop, CheckUrgency, InputUrgency, SetUrgency, GetAuthToken, AfterGetAuthToken, CreateTicket }; var displayExisting = new WaterfallStep[] { GetAuthToken, AfterGetAuthToken, ShowKnowledge, IfKnowledgeHelp }; AddDialog(new WaterfallDialog(Actions.CreateTicket, createTicket)); AddDialog(new WaterfallDialog(Actions.DisplayExisting, displayExisting)); InitialDialogId = Actions.CreateTicket; // intended null // ShowKnowledgeNoResponse ShowKnowledgeEndResponse = KnowledgeResponses.KnowledgeEnd; ShowKnowledgeResponse = TicketResponses.IfExistingSolve; ShowKnowledgePrompt = Actions.NavigateYesNoPrompt; KnowledgeHelpLoop = Actions.DisplayExisting; }
public Search_by_Subject(BotServices botServices, UserState userState, IBotTelemetryClient telemetryClient) : base(nameof(Search_by_Subject)) { // This array defines how the Waterfall will execute. var waterfallSteps = new WaterfallStep[] { CheckSearchEntitiesAsync, ReturnResultsAsync, ReturnTimeResultsAsync, }; // Add named dialogs to the DialogSet. These names are saved in the dialog state. AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps)); AddDialog(new TextPrompt(nameof(TextPrompt))); // The initial child Dialog to run. InitialDialogId = nameof(WaterfallDialog); }
public UpdateShippingAddressDialog( BotServices services, IStatePropertyAccessor <CustomerSupportTemplateState> stateAccessor) : base(services, nameof(UpdateShippingAddressDialog)) { _services = services; _stateAccessor = stateAccessor; var updateShippingAddress = new WaterfallStep[] { ShowPolicy, PromptToEscalate, HandleEscalationResponse, }; InitialDialogId = nameof(UpdateShippingAddressDialog); AddDialog(new WaterfallDialog(InitialDialogId, updateShippingAddress)); AddDialog(new ConfirmPrompt(DialogIds.EscalatePrompt, SharedValidators.ConfirmValidator)); }
public MainDialog(BotServices luisRecognizer, BookingDialog bookingDialog, ILogger <MainDialog> logger, IBotServices botServices, FlightBookingRecognizer luisRecognizers) : base(nameof(MainDialog)) { _luisRecognizer = luisRecognizer; _logger = logger; _botServices = botServices; _luisRecognizers = luisRecognizers; AddDialog(new TextPrompt(nameof(TextPrompt))); AddDialog(bookingDialog); AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] { IntroStepAsync, ActStepAsync, FinalStepAsync, })); InitialDialogId = nameof(WaterfallDialog); }
public MainLuisDialog( BotServices botServices, ITeacherService teacherService, IOpenDataService openDataService, ISearchService searchService, ISpellCheckService spellCheckService, IUnexFacilitiesService unexFacilitiesService, string dialogId = null, IEnumerable <WaterfallStep> steps = null) : base(dialogId ?? nameof(MainLuisDialog)) { string fullPath = Path.Combine(new string[] { ".", ".", "Resources", "MainLuisDialog.lg" }); _lgEngine = Templates.ParseFile(fullPath); _services = botServices ?? throw new ArgumentNullException(nameof(botServices)); if (!_services.LuisServices.ContainsKey(LuisServiceConfiguration.LuisKey)) { throw new ArgumentException($"La configuración no es correcta. Por favor comprueba que existe en tu fichero '.bot' un servicio LUIS llamado '{LuisServiceConfiguration.LuisKey}'."); } AddDialog(new LanguageNotValidDialog(nameof(LanguageNotValidDialog))); AddDialog(new GoodByeDialog(nameof(GoodByeDialog))); AddDialog(new HelpDialog(nameof(HelpDialog))); AddDialog(new GratitudeDialog(nameof(GratitudeDialog))); AddDialog(new NegationDialog(nameof(NegationDialog))); AddDialog(new QuestionDialog(nameof(QuestionDialog), botServices, spellCheckService, searchService)); AddDialog(new SubjectDialog(nameof(SubjectDialog), openDataService)); AddDialog(new TeacherDialog(nameof(TeacherDialog), teacherService)); AddDialog(new UnexFacilitiesDialog(nameof(UnexFacilitiesDialog), unexFacilitiesService)); AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] { IntroStepAsync, ActStepAsync, FinalStepAsync, })); AddDialog(new TextPrompt(nameof(TextPrompt))); AddDialog(new ChoicePrompt(nameof(ChoicePrompt))); InitialDialogId = nameof(WaterfallDialog); }
public MainDialog(BotServices services, ConversationState conversationState, UserState userState, ProactiveState proactiveState, EndpointService endpointService, IBotTelemetryClient telemetryClient, IBackgroundTaskQueue backgroundTaskQueue) : base(nameof(MainDialog), telemetryClient) { _services = services ?? throw new ArgumentNullException(nameof(services)); _conversationState = conversationState; _userState = userState; _proactiveState = proactiveState; _endpointService = endpointService; TelemetryClient = telemetryClient; _backgroundTaskQueue = backgroundTaskQueue; _onboardingState = _userState.CreateProperty <OnboardingState>(nameof(OnboardingState)); _parametersAccessor = _userState.CreateProperty <Dictionary <string, object> >("userInfo"); _virtualAssistantState = _conversationState.CreateProperty <VirtualAssistantState>(nameof(VirtualAssistantState)); AddDialog(new OnboardingDialog(_services, _onboardingState, telemetryClient)); AddDialog(new EscalateDialog(_services, telemetryClient)); RegisterSkills(_services.SkillDefinitions); }
public PlayMusicDialog( BotSettings settings, BotServices services, ResponseManager responseManager, ConversationState conversationState, IBotTelemetryClient telemetryClient) : base(nameof(PlayMusicDialog), settings, services, responseManager, conversationState, telemetryClient) { _responseManager = responseManager; var sample = new WaterfallStep[] { GetAndSendMusicResult, }; AddDialog(new WaterfallDialog(nameof(PlayMusicDialog), sample)); InitialDialogId = nameof(PlayMusicDialog); }
public async Task RemoveVoiceChannel(CommandContext ctx, [RemainingText] DiscordChannel channel) { if (channel == null) { await BotServices.SendErrorEmbedAsync(ctx, ":mag: Channel not found in this server!"); } else if (channel.Type != ChannelType.Voice) { await BotServices.SendErrorEmbedAsync(ctx, ":warning: This is not a voice channel, use **.deletetext** instead!"); } else { await ctx.TriggerTypingAsync(); await channel.DeleteAsync(); await ctx.RespondAsync($"Voice Channel **{channel.Name}** has been **removed**"); } }
public async Task Ban(CommandContext ctx, [Description("Server user to ban.")] DiscordMember member, [Description("Reason for the ban.")][RemainingText] string reason = null) { if (ctx.Member.Id == member.Id) { await ctx.RespondAsync("You cannot ban yourself.").ConfigureAwait(false); return; } await ctx.Guild.BanMemberAsync(member, 7, reason).ConfigureAwait(false); await BotServices.RemoveMessage(ctx.Message).ConfigureAwait(false); await BotServices.SendUserStateChangeAsync(ctx, UserStateChange.Ban, member, reason ?? "No reason provided.").ConfigureAwait(false); }
public MainDialog( BotSettings settings, BotServices services, ResponseManager responseManager, ConversationState conversationState, OutgoingCallDialog outgoingCallDialog, IBotTelemetryClient telemetryClient) : base(nameof(MainDialog), telemetryClient) { this.outgoingCallDialog = outgoingCallDialog; _settings = settings; _services = services; _responseManager = responseManager; _conversationState = conversationState; TelemetryClient = telemetryClient; _stateAccessor = _conversationState.CreateProperty <PhoneSkillState>(nameof(PhoneSkillState)); AddDialog(outgoingCallDialog ?? throw new ArgumentNullException(nameof(outgoingCallDialog))); }
public MainDialog( BotServices services, ResponseManager responseManager, ConversationState conversationState, VehicleSettingsDialog vehicleSettingsDialog, IBotTelemetryClient telemetryClient) : base(nameof(MainDialog), telemetryClient) { _services = services; _responseManager = responseManager; _conversationState = conversationState; TelemetryClient = telemetryClient; // Initialize state accessor _stateAccessor = _conversationState.CreateProperty <AutomotiveSkillState>(nameof(AutomotiveSkillState)); // Register dialogs AddDialog(vehicleSettingsDialog ?? throw new ArgumentNullException(nameof(vehicleSettingsDialog))); }
public async Task SetBotActivity(CommandContext ctx, [Description("Name of the activity")][RemainingText] string activity) { if (string.IsNullOrWhiteSpace(activity)) { await ctx.Client.UpdateStatusAsync(activity : null).ConfigureAwait(false); await BotServices.SendEmbedAsync(ctx, SharedData.Name + " activity has been changed to Normal").ConfigureAwait(false); } else { // TODO: Set the activity type var game = new DiscordActivity(activity); await ctx.Client.UpdateStatusAsync(activity : game).ConfigureAwait(false); await BotServices.SendEmbedAsync(ctx, SharedData.Name + " activity has been changed to Playing " + game.Name, EmbedType.Good).ConfigureAwait(false); } }
public RouteDialog( BotSettings settings, BotServices services, ResponseManager responseManager, ConversationState conversationState, IServiceManager serviceManager, IBotTelemetryClient telemetryClient, IHttpContextAccessor httpContext) : base(nameof(RouteDialog), settings, services, responseManager, conversationState, serviceManager, telemetryClient, httpContext) { TelemetryClient = telemetryClient; var checkCurrentLocation = new WaterfallStep[] { CheckForCurrentCoordinatesBeforeRoute, ConfirmCurrentLocation, ProcessCurrentLocationSelection, RouteToFindPointOfInterestDialog, }; var findRouteToActiveLocation = new WaterfallStep[] { GetRoutesToDestination, ResponseToStartRoutePrompt, }; // Define the conversation flow using a waterfall model. AddDialog(new WaterfallDialog(Actions.CheckForCurrentLocation, checkCurrentLocation) { TelemetryClient = telemetryClient }); AddDialog(new WaterfallDialog(Actions.FindRouteToActiveLocation, findRouteToActiveLocation) { TelemetryClient = telemetryClient }); AddDialog(new ConfirmPrompt(Actions.StartNavigationPrompt, ValidateStartNavigationPrompt) { Style = ListStyle.None }); // Set starting dialog for component InitialDialogId = Actions.CheckForCurrentLocation; }
public async Task GetDog(CommandContext ctx) { await ctx.TriggerTypingAsync(); var results = MiscService.GetDogPhotoAsync().Result; if (results.Status != "success") { await BotServices.SendResponseAsync(ctx, Resources.ERR_API_CONNECTION, ResponseType.Warning) .ConfigureAwait(false); return; } var output = new DiscordEmbedBuilder() .WithImageUrl(results.Message) .WithColor(DiscordColor.Brown); await ctx.RespondAsync(output.Build()).ConfigureAwait(false); }
public async Task Twitch(CommandContext ctx, [RemainingText] string stream) { if (string.IsNullOrWhiteSpace(stream)) { await BotServices.SendErrorEmbedAsync(ctx, ":warning: An existing Twitch channel name is required!"); } else { try { await ctx.TriggerTypingAsync(); var http = new HttpClient(); var service = new BotServices(); var twitchUrl = $"https://api.twitch.tv/kraken/streams/{stream.ToLower()}?client_id={service.GetAPIToken("twitch")}"; var response = await http.GetStringAsync(twitchUrl).ConfigureAwait(false); var twitch = JsonConvert.DeserializeObject <TwitchService>(response); twitch.Url = twitchUrl; if (!twitch.IsLive) { await BotServices.SendErrorEmbedAsync(ctx, "Channel is **Offline** (or doesn't exist) :pensive:"); } else { var output = new DiscordEmbedBuilder() .WithTitle($"{twitch.Stream.Channel.display_name} is live streaming on Twitch!") .AddField("Now Playing", twitch.Game) .AddField("Stream Title", twitch.Title) .AddField("Followers", twitch.Followers.ToString(), true) .AddField("Viewers", twitch.Viewers.ToString(), true) .WithThumbnailUrl(twitch.Icon) .WithUrl(twitch.Url) .WithColor(DiscordColor.Purple); await ctx.RespondAsync(embed : output.Build()); } } catch { await BotServices.SendErrorEmbedAsync(ctx, ":warning: Unable to determine channel status, please do not include special characters in your input!"); } } }
public FindPointOfInterestDialog( BotSettings settings, BotServices services, ResponseManager responseManager, ConversationState conversationState, RouteDialog routeDialog, IServiceManager serviceManager, IBotTelemetryClient telemetryClient, IHttpContextAccessor httpContext) : base(nameof(FindPointOfInterestDialog), settings, services, responseManager, conversationState, serviceManager, telemetryClient, httpContext) { TelemetryClient = telemetryClient; var checkCurrentLocation = new WaterfallStep[] { CheckForCurrentCoordinatesBeforeFindPointOfInterest, ConfirmCurrentLocation, ProcessCurrentLocationSelection, RouteToFindPointOfInterestDialog }; var findPointOfInterest = new WaterfallStep[] { GetPointOfInterestLocations, ProcessPointOfInterestSelection, ProcessPointOfInterestAction, }; // Define the conversation flow using a waterfall model. AddDialog(new WaterfallDialog(Actions.CheckForCurrentLocation, checkCurrentLocation) { TelemetryClient = telemetryClient }); AddDialog(new WaterfallDialog(Actions.FindPointOfInterest, findPointOfInterest) { TelemetryClient = telemetryClient }); AddDialog(routeDialog ?? throw new ArgumentNullException(nameof(routeDialog))); // Set starting dialog for component InitialDialogId = Actions.CheckForCurrentLocation; }
public async Task Wikipedia(CommandContext ctx, [Description("Articles to find on Wikipedia.")][RemainingText] string query) { if (string.IsNullOrWhiteSpace(query)) { return; } await ctx.TriggerTypingAsync(); var results = WikipediaService.GetWikipediaDataAsync(query); if (results.Error != null || results.Search?.Count == 0) { await BotServices.SendResponseAsync(ctx, Resources.NOT_FOUND_WIKIPEDIA, ResponseType.Missing) .ConfigureAwait(false); return; } if (results.Search?.Count <= 1) { await BotServices.SendResponseAsync(ctx, Resources.NOT_FOUND_COMMON, ResponseType.Missing) .ConfigureAwait(false); return; } while (results.Search?.Count > 0) { var output = new DiscordEmbedBuilder() .WithColor(new DiscordColor("#6B6B6B")) .WithFooter(results.Search.Count - 5 >= 5 ? "Type 'next' within 10 seconds for the next five articles." : "There articles are retrieved using WikipediaNET."); foreach (var result in results.Search.Take(5)) { var desc = Regex.Replace( result.Snippet.Length <= 300 ? string.IsNullOrEmpty(result.Snippet) ? "Article has not content." : result.Snippet : result.Snippet[..150] + "...", "<[^>]*>", "");
public async Task Math(CommandContext ctx, double num1, string operation, double num2) { try { double result; switch (operation) { case "+": result = num1 + num2; break; case "-": result = num1 - num2; break; case "*": result = num1 * num2; break; case "/": result = num1 / num2; break; case "%": result = num1 % num2; break; default: result = num1 + num2; break; } var output = new DiscordEmbedBuilder() .WithTitle($":1234: The result is {result:#,##0.00}") .WithColor(DiscordColor.CornflowerBlue); await ctx.RespondAsync(embed : output.Build()); } catch { await BotServices.SendErrorEmbedAsync(ctx, ":warning: Error calculating math equation, make sure your values are integers and the operation is valid!"); } }
public async Task Math(CommandContext ctx, [Description("First operand")] double num1, [Description("Operator")] string operation, [Description("Second operand")] double num2) { try { double result = 0; switch (operation) { default: //case "+": result = num1 + num2; break; case "-": result = num1 - num2; break; case "*": case "x": result = num1 * num2; break; case "/": result = num1 / num2; break; case "%": result = num1 % num2; break; } var output = new DiscordEmbedBuilder() .WithDescription($":1234: The result is {result:#,##0.00}") .WithColor(DiscordColor.CornflowerBlue); await ctx.RespondAsync(embed : output.Build()).ConfigureAwait(false); } catch { await BotServices.SendEmbedAsync(ctx, Resources.ERR_MATH_EQUATION, EmbedType.Warning).ConfigureAwait(false); } }
public OnboardingDialog( IServiceProvider serviceProvider) : base(nameof(OnboardingDialog)) { _templateManager = serviceProvider.GetService <LocaleTemplateManager>(); var userState = serviceProvider.GetService <UserState>(); _accessor = userState.CreateProperty <UserProfileState>(nameof(UserProfileState)); _services = serviceProvider.GetService <BotServices>(); var onboarding = new WaterfallStep[] { AskForNameAsync, FinishOnboardingDialogAsync, }; AddDialog(new WaterfallDialog(nameof(onboarding), onboarding)); AddDialog(new TextPrompt(DialogIds.NamePrompt)); }
public async Task SetChannelTopic(CommandContext ctx, [Description("New channel topic.")][RemainingText] string topic = "") { if (topic.Length > 1024) { await BotServices.SendResponseAsync(ctx, Resources.ERR_CHANNEL_TOPIC, ResponseType.Warning) .ConfigureAwait(false); return; } await ctx.Channel.ModifyAsync(chn => chn.Topic = topic).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(topic)) { await ctx.RespondAsync("Successfully changed the channel topic to " + Formatter.Bold(topic)) .ConfigureAwait(false); } }
public FindPromoCodeDialog( BotServices services, IStatePropertyAccessor <CustomerSupportTemplateState> stateAccessor) : base(services, nameof(FindPromoCodeDialog)) { _client = new DemoServiceClient(); _services = services; _stateAccessor = stateAccessor; var findPromoCode = new WaterfallStep[] { ShowCurrentPromos, PromptForCartId, ShowRelevantPromos, }; InitialDialogId = nameof(FindPromoCodeDialog); AddDialog(new WaterfallDialog(InitialDialogId, findPromoCode)); AddDialog(new TextPrompt(DialogIds.CartIdPrompt, SharedValidators.CartIdValidator)); }
public FindEventsDialog( BotSettings settings, BotServices services, ResponseManager responseManager, ConversationState conversationState, UserState userState, IBotTelemetryClient telemetryClient) : base(nameof(FindEventsDialog), settings, services, responseManager, conversationState, userState, telemetryClient) { var findEvents = new WaterfallStep[] { GetLocation, FindEvents }; _eventbriteService = new EventbriteService(settings); AddDialog(new WaterfallDialog(nameof(FindEventsDialog), findEvents)); AddDialog(new TextPrompt(DialogIds.LocationPrompt, ValidateLocationPrompt)); }
public async Task DeafenUser(CommandContext ctx, [Description("Server user to deafen.")] DiscordMember member, [Description("Reason for the deafen.")][RemainingText] string reason = null) { if (member.IsDeafened) { await ctx.RespondAsync($"{member.DisplayName}#{member.Discriminator} is already **deafened**.") .ConfigureAwait(false); return; } await member.SetDeafAsync(true, reason).ConfigureAwait(false); await BotServices.RemoveMessage(ctx.Message).ConfigureAwait(false); await BotServices.SendUserStateChangeAsync(ctx, UserStateChange.Deafen, member, reason ?? "No reason provided.").ConfigureAwait(false); }
public GetReservationDialog( BotSettings settings, BotServices services, ResponseManager responseManager, ConversationState conversationState, UserState userState, IHotelService hotelService, IBotTelemetryClient telemetryClient) : base(nameof(GetReservationDialog), settings, services, responseManager, conversationState, userState, hotelService, telemetryClient) { var getReservation = new WaterfallStep[] { HasCheckedOut, ShowReservation }; HotelService = hotelService; AddDialog(new WaterfallDialog(nameof(GetReservationDialog), getReservation)); }