diff --git a/src/main/java/net/Broken/Api/Controllers/AudioController.java b/src/main/java/net/Broken/Api/Controllers/AudioController.java index 5539ffb..0cbb69f 100644 --- a/src/main/java/net/Broken/Api/Controllers/AudioController.java +++ b/src/main/java/net/Broken/Api/Controllers/AudioController.java @@ -42,4 +42,32 @@ public class AudioController { JwtPrincipal principal = (JwtPrincipal) authentication.getPrincipal(); return audioService.disconnect(guildId, principal.user().getDiscordId()); } + + @PostMapping("/{guildId}/resume") + @PreAuthorize("isInGuild(#guildId) && canInteractWithVoiceChannel(#guildId)") + public ResponseEntity resume(@PathVariable String guildId, Authentication authentication) { + JwtPrincipal principal = (JwtPrincipal) authentication.getPrincipal(); + return audioService.resume(guildId, principal.user().getDiscordId()); + } + + @PostMapping("/{guildId}/pause") + @PreAuthorize("isInGuild(#guildId) && canInteractWithVoiceChannel(#guildId)") + public ResponseEntity pause(@PathVariable String guildId, Authentication authentication) { + JwtPrincipal principal = (JwtPrincipal) authentication.getPrincipal(); + return audioService.pause(guildId, principal.user().getDiscordId()); + } + + @PostMapping("/{guildId}/skip") + @PreAuthorize("isInGuild(#guildId) && canInteractWithVoiceChannel(#guildId)") + public ResponseEntity skip(@PathVariable String guildId, Authentication authentication) { + JwtPrincipal principal = (JwtPrincipal) authentication.getPrincipal(); + return audioService.skip(guildId, principal.user().getDiscordId()); + } + + @PostMapping("/{guildId}/stop") + @PreAuthorize("isInGuild(#guildId) && canInteractWithVoiceChannel(#guildId)") + public ResponseEntity stop(@PathVariable String guildId, Authentication authentication) { + JwtPrincipal principal = (JwtPrincipal) authentication.getPrincipal(); + return audioService.stop(guildId, principal.user().getDiscordId()); + } } diff --git a/src/main/java/net/Broken/Api/Security/Expression/CustomMethodSecurityExpressionRoot.java b/src/main/java/net/Broken/Api/Security/Expression/CustomMethodSecurityExpressionRoot.java index 85fc3f9..2bacdf8 100644 --- a/src/main/java/net/Broken/Api/Security/Expression/CustomMethodSecurityExpressionRoot.java +++ b/src/main/java/net/Broken/Api/Security/Expression/CustomMethodSecurityExpressionRoot.java @@ -2,7 +2,7 @@ package net.Broken.Api.Security.Expression; import net.Broken.Api.Data.Music.Connect; import net.Broken.Api.Security.Data.JwtPrincipal; -import net.Broken.Audio.GuildAudioWrapper; +import net.Broken.Audio.GuildAudioBotService; import net.Broken.MainBot; import net.Broken.Tools.CacheTools; import net.dv8tion.jda.api.Permission; @@ -61,7 +61,7 @@ public class CustomMethodSecurityExpressionRoot public boolean canInteractWithVoiceChannel(String guildId) { JwtPrincipal jwtPrincipal = (JwtPrincipal) authentication.getPrincipal(); Guild guild = MainBot.jda.getGuildById(guildId); - GuildAudioWrapper guildAudioWrapper = GuildAudioWrapper.getInstance(guild); + GuildAudioBotService guildAudioBotService = GuildAudioBotService.getInstance(guild); VoiceChannel channel = guild.getAudioManager().getConnectedChannel(); if (channel == null) { diff --git a/src/main/java/net/Broken/Api/Services/AudioService.java b/src/main/java/net/Broken/Api/Services/AudioService.java index 9d4af52..cfa4842 100644 --- a/src/main/java/net/Broken/Api/Services/AudioService.java +++ b/src/main/java/net/Broken/Api/Services/AudioService.java @@ -5,7 +5,7 @@ import net.Broken.Api.Data.Music.Connect; import net.Broken.Api.Data.Music.PlayBackInfo; import net.Broken.Api.Data.Music.Status; import net.Broken.Api.Data.Music.TrackInfo; -import net.Broken.Audio.GuildAudioWrapper; +import net.Broken.Audio.GuildAudioBotService; import net.Broken.Audio.UserAudioTrack; import net.Broken.MainBot; import net.dv8tion.jda.api.Permission; @@ -38,7 +38,7 @@ public class AudioService { boolean canView = member.hasPermission(channel, Permission.VIEW_CHANNEL) || (member.getVoiceState() != null && member.getVoiceState().getChannel() == channel); - GuildAudioWrapper guildAudioWrapper = GuildAudioWrapper.getInstance(guild); + GuildAudioBotService guildAudioBotService = GuildAudioBotService.getInstance(guild); if (canView) { // The user can interact with the audio if: @@ -51,12 +51,12 @@ public class AudioService { && member.hasPermission(channel, Permission.VOICE_SPEAK); - boolean stopped = guildAudioWrapper.getGuidAudioManager().player.getPlayingTrack() == null; + boolean stopped = guildAudioBotService.getGuidAudioManager().player.getPlayingTrack() == null; PlayBackInfo playBackInfo; if (!stopped) { - boolean paused = guildAudioWrapper.getGuidAudioManager().player.isPaused(); - long position = guildAudioWrapper.getGuidAudioManager().player.getPlayingTrack().getPosition(); - UserAudioTrack userAudioTrack = guildAudioWrapper.getGuidAudioManager().scheduler.getCurrentPlayingTrack(); + boolean paused = guildAudioBotService.getGuidAudioManager().player.isPaused(); + long position = guildAudioBotService.getGuidAudioManager().player.getPlayingTrack().getPosition(); + UserAudioTrack userAudioTrack = guildAudioBotService.getGuidAudioManager().scheduler.getCurrentPlayingTrack(); playBackInfo = new PlayBackInfo(paused, false, position, new TrackInfo(userAudioTrack)); @@ -75,18 +75,45 @@ public class AudioService { public ResponseEntity connect(String guildId, Connect body, String userId) { Guild guild = MainBot.jda.getGuildById(guildId); - GuildAudioWrapper guildAudioWrapper = GuildAudioWrapper.getInstance(guild); VoiceChannel voiceChannel = guild.getVoiceChannelById(body.channelId()); - guildAudioWrapper.getGuidAudioManager(); - guild.getAudioManager().openAudioConnection(voiceChannel); + GuildAudioBotService.getInstance(guild).connect(voiceChannel); Status status = getGuildAudioStatus(guildId, userId); return new ResponseEntity<>(status, HttpStatus.OK); } public ResponseEntity disconnect(String guildId, String userId) { Guild guild = MainBot.jda.getGuildById(guildId); - GuildAudioWrapper guildAudioWrapper = GuildAudioWrapper.getInstance(guild); - guildAudioWrapper.disconnect(); + GuildAudioBotService guildAudioBotService = GuildAudioBotService.getInstance(guild); + guildAudioBotService.disconnect(); + Status status = getGuildAudioStatus(guildId, userId); + return new ResponseEntity<>(status, HttpStatus.OK); + } + + + public ResponseEntity pause(String guildId, String userId){ + Guild guild = MainBot.jda.getGuildById(guildId); + GuildAudioBotService.getInstance(guild).pause(); + Status status = getGuildAudioStatus(guildId, userId); + return new ResponseEntity<>(status, HttpStatus.OK); + } + + public ResponseEntity resume(String guildId, String userId){ + Guild guild = MainBot.jda.getGuildById(guildId); + GuildAudioBotService.getInstance(guild).resume(); + Status status = getGuildAudioStatus(guildId, userId); + return new ResponseEntity<>(status, HttpStatus.OK); + } + + public ResponseEntity skip(String guildId, String userId){ + Guild guild = MainBot.jda.getGuildById(guildId); + GuildAudioBotService.getInstance(guild).skipTrack(); + Status status = getGuildAudioStatus(guildId, userId); + return new ResponseEntity<>(status, HttpStatus.OK); + } + + public ResponseEntity stop(String guildId, String userId){ + Guild guild = MainBot.jda.getGuildById(guildId); + GuildAudioBotService.getInstance(guild).stop(); Status status = getGuildAudioStatus(guildId, userId); return new ResponseEntity<>(status, HttpStatus.OK); } diff --git a/src/main/java/net/Broken/Audio/GuildAudioWrapper.java b/src/main/java/net/Broken/Audio/GuildAudioBotService.java similarity index 84% rename from src/main/java/net/Broken/Audio/GuildAudioWrapper.java rename to src/main/java/net/Broken/Audio/GuildAudioBotService.java index 85555b5..759a1a8 100644 --- a/src/main/java/net/Broken/Audio/GuildAudioWrapper.java +++ b/src/main/java/net/Broken/Audio/GuildAudioBotService.java @@ -26,9 +26,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -public class GuildAudioWrapper { +public class GuildAudioBotService { - private static final HashMap INSTANCES = new HashMap<>(); + private static final HashMap INSTANCES = new HashMap<>(); private final GuildAudioManager guildAudioManager; @@ -44,7 +44,7 @@ public class GuildAudioWrapper { private Message lastMessageWithButton; - private GuildAudioWrapper(Guild guild) { + private GuildAudioBotService(Guild guild) { this.audioPlayerManager = new DefaultAudioPlayerManager(); AudioSourceManagers.registerRemoteSources(audioPlayerManager); AudioSourceManagers.registerLocalSource(audioPlayerManager); @@ -53,9 +53,9 @@ public class GuildAudioWrapper { this.guild = guild; } - public static GuildAudioWrapper getInstance(Guild guild) { + public static GuildAudioBotService getInstance(Guild guild) { if (!INSTANCES.containsKey(guild)) { - INSTANCES.put(guild, new GuildAudioWrapper(guild)); + INSTANCES.put(guild, new GuildAudioBotService(guild)); } return INSTANCES.get(guild); } @@ -63,16 +63,13 @@ public class GuildAudioWrapper { /** * Load audio track from url, connect to chanel if not connected * - * @param event * @param voiceChannel Voice channel to connect if no connected * @param trackUrl Audio track url * @param playlistLimit Limit of playlist * @param onHead True for adding audio track on top of playlist */ public void loadAndPlay(SlashCommandEvent event, VoiceChannel voiceChannel, final String trackUrl, int playlistLimit, boolean onHead) { - GuildAudioManager guidAudioManager = getGuidAudioManager(); - - audioPlayerManager.loadItemOrdered(guidAudioManager, trackUrl, new AudioLoadResultHandler() { + audioPlayerManager.loadItemOrdered(guildAudioManager, trackUrl, new AudioLoadResultHandler() { @Override public void trackLoaded(AudioTrack track) { logger.info("[" + guild + "] Single Track detected!"); @@ -82,7 +79,7 @@ public class GuildAudioWrapper { .build(); clearLastButton(); lastMessageWithButton = event.getHook().sendMessage(message).addActionRow(getActionButton()).complete(); - play(guild, voiceChannel, guidAudioManager, uat, onHead); + play(guild, voiceChannel, guildAudioManager, uat, onHead); } @Override @@ -173,8 +170,6 @@ public class GuildAudioWrapper { } - - /** * Add single track to playlist, auto-connect if not connected to vocal chanel * @@ -193,32 +188,21 @@ public class GuildAudioWrapper { musicManager.scheduler.addNext(track); } - /** - * Skip current track - * - * @param event - */ - public void skipTrack(GenericInteractionCreateEvent event) { - GuildAudioManager musicManager = getGuidAudioManager(); - musicManager.scheduler.nextTrack(); - Message message = new MessageBuilder().setEmbeds( - EmbedMessageUtils.buildStandar( - new EmbedBuilder() - .setTitle(":track_next: Next Track") - .setColor(Color.green) - )).build(); - clearLastButton(); - lastMessageWithButton = event.getHook().sendMessage(message).addActionRow(getActionButton()).complete(); + public void add(SlashCommandEvent event, String url, int playListLimit, boolean onHead) { + if (guild.getAudioManager().isConnected()) { + loadAndPlay(event, guild.getAudioManager().getConnectedChannel(), url, playListLimit, onHead); + } else { + Message message = new MessageBuilder().setEmbeds(EmbedMessageUtils.getMusicError("Not connected to vocal chanel !")).build(); + event.getHook().setEphemeral(true).sendMessage(message).queue(); + } + } + + public void connect(VoiceChannel voiceChannel) { + guild.getAudioManager().openAudioConnection(voiceChannel); } - /** - * Pause current track - * - * @param event - */ public void pause(GenericInteractionCreateEvent event) { - GuildAudioManager musicManager = getGuidAudioManager(); - musicManager.scheduler.pause(); + pause(); Message message = new MessageBuilder().setEmbeds( EmbedMessageUtils.buildStandar( new EmbedBuilder() @@ -227,19 +211,15 @@ public class GuildAudioWrapper { )).build(); clearLastButton(); lastMessageWithButton = event.getHook().sendMessage(message).addActionRow(getActionButton()).complete(); - - } - /** - * Resume paused track - * - * @param event - */ + public void pause() { + guildAudioManager.scheduler.pause(); + } + public void resume(GenericInteractionCreateEvent event) { - GuildAudioManager musicManager = getGuidAudioManager(); Message message; - if (musicManager.player.getPlayingTrack() == null) { + if (guildAudioManager.player.getPlayingTrack() == null) { message = new MessageBuilder().setEmbeds( EmbedMessageUtils.buildStandar( new EmbedBuilder() @@ -247,7 +227,7 @@ public class GuildAudioWrapper { .setColor(Color.green) )).build(); } else { - musicManager.scheduler.resume(); + resume(); message = new MessageBuilder().setEmbeds( EmbedMessageUtils.buildStandar( new EmbedBuilder() @@ -259,23 +239,73 @@ public class GuildAudioWrapper { lastMessageWithButton = event.getHook().sendMessage(message).addActionRow(getActionButton()).complete(); } - /** - * Print current played track info - * - * @param event - */ + public void resume() { + guildAudioManager.scheduler.resume(); + } + + public void skipTrack(GenericInteractionCreateEvent event) { + skipTrack(); + Message message = new MessageBuilder().setEmbeds( + EmbedMessageUtils.buildStandar( + new EmbedBuilder() + .setTitle(":track_next: Next Track") + .setColor(Color.green) + )).build(); + clearLastButton(); + lastMessageWithButton = event.getHook().sendMessage(message).addActionRow(getActionButton()).complete(); + } + + public void skipTrack() { + guildAudioManager.scheduler.nextTrack(); + } + + public void stop(GenericInteractionCreateEvent event) { + stop(); + Message message = new MessageBuilder().setEmbeds( + EmbedMessageUtils.buildStandar( + new EmbedBuilder() + .setTitle(":stop_button: Playback stopped") + .setColor(Color.green) + )).build(); + clearLastButton(); + lastMessageWithButton = event.getHook().sendMessage(message).addActionRow(getActionButton()).complete(); + + } + + public void stop() { + guildAudioManager.scheduler.stop(); + guildAudioManager.scheduler.flush(); + clearLastButton(); + } + + public void disconnect(GenericInteractionCreateEvent event) { + disconnect(); + Message message = new MessageBuilder().setEmbeds( + EmbedMessageUtils.buildStandar( + new EmbedBuilder() + .setTitle(":eject: Disconnected") + .setColor(Color.green) + )).build(); + clearLastButton(); + event.getHook().sendMessage(message).queue(); + } + + public void disconnect() { + guildAudioManager.scheduler.stop(); + guildAudioManager.scheduler.flush(); + guild.getAudioManager().closeAudioConnection(); + clearLastButton(); + } public void info(GenericInteractionCreateEvent event) { - GuildAudioManager musicManager = getGuidAudioManager(); - AudioTrackInfo info = musicManager.scheduler.getInfo(); - UserAudioTrack userAudioTrack = musicManager.scheduler.getCurrentPlayingTrack(); + AudioTrackInfo info = guildAudioManager.scheduler.getInfo(); + UserAudioTrack userAudioTrack = guildAudioManager.scheduler.getCurrentPlayingTrack(); Message message = new MessageBuilder().setEmbeds(EmbedMessageUtils.getMusicInfo(info, userAudioTrack)).build(); clearLastButton(); lastMessageWithButton = event.getHook().sendMessage(message).addActionRow(getActionButton()).complete(); } public void flush(GenericInteractionCreateEvent event) { - GuildAudioManager musicManager = getGuidAudioManager(); - musicManager.scheduler.flush(); + guildAudioManager.scheduler.flush(); Message message = new MessageBuilder().setEmbeds( EmbedMessageUtils.buildStandar( new EmbedBuilder() @@ -292,8 +322,7 @@ public class GuildAudioWrapper { * @param event */ public void list(GenericInteractionCreateEvent event) { - GuildAudioManager musicManager = getGuidAudioManager(); - List list = musicManager.scheduler.getList(); + List list = guildAudioManager.scheduler.getList(); if (list.size() == 0) { Message message = new MessageBuilder().setEmbeds( @@ -333,85 +362,15 @@ public class GuildAudioWrapper { } - /** - * Called by //add, only if already connected - * - * @param event - * @param url Audio track url - * @param playListLimit Limit of playlist - * @param onHead True for adding audio track on top of playlist - */ - public void add(SlashCommandEvent event, String url, int playListLimit, boolean onHead) { - if (guild.getAudioManager().isConnected()) { - loadAndPlay(event, guild.getAudioManager().getConnectedChannel(), url, playListLimit, onHead); - } else { - Message message = new MessageBuilder().setEmbeds(EmbedMessageUtils.getMusicError("Not connected to vocal chanel !")).build(); - event.getHook().setEphemeral(true).sendMessage(message).queue(); - } - } - - /** - * Stop current playing track and flush playlist - * - * @param event - */ - public void stop(GenericInteractionCreateEvent event) { - guildAudioManager.scheduler.stop(); - guildAudioManager.scheduler.flush(); - - if (event != null) { - Message message = new MessageBuilder().setEmbeds( - EmbedMessageUtils.buildStandar( - new EmbedBuilder() - .setTitle(":stop_button: Playback stopped") - .setColor(Color.green) - )).build(); - clearLastButton(); - lastMessageWithButton = event.getHook().sendMessage(message).addActionRow(getActionButton()).complete(); - } - } - - public void stop() { - - GuildAudioManager musicManager = getGuidAudioManager(); - musicManager.scheduler.stop(); - musicManager.scheduler.flush(); - clearLastButton(); - } - - public void disconnect(GenericInteractionCreateEvent event) { - disconnect(); - Message message = new MessageBuilder().setEmbeds( - EmbedMessageUtils.buildStandar( - new EmbedBuilder() - .setTitle(":eject: Disconnected") - .setColor(Color.green) - )).build(); - clearLastButton(); - event.getHook().sendMessage(message).queue(); - } - - public void disconnect() { - GuildAudioManager musicManager = getGuidAudioManager(); - musicManager.scheduler.stop(); - musicManager.scheduler.flush(); - guild.getAudioManager().closeAudioConnection(); - clearLastButton(); - } - public Guild getGuild() { return guild; } - public AudioPlayerManager getAudioPlayerManager() { - return audioPlayerManager; - } public GuildAudioManager getGuidAudioManager() { return guildAudioManager; } - public void clearLastButton() { if (lastMessageWithButton != null) { this.lastMessageWithButton.editMessageComponents(new ArrayList<>()).queue(); diff --git a/src/main/java/net/Broken/Audio/TrackScheduler.java b/src/main/java/net/Broken/Audio/TrackScheduler.java index 2b19d97..7ee4433 100644 --- a/src/main/java/net/Broken/Audio/TrackScheduler.java +++ b/src/main/java/net/Broken/Audio/TrackScheduler.java @@ -147,7 +147,7 @@ public class TrackScheduler extends AudioEventAdapter { if (endReason.mayStartNext) { if(queue.isEmpty()){ logger.debug("[" + guild.getName() + "] End of track, Playlist empty."); - GuildAudioWrapper.getInstance(guild).updateLastButton(); + GuildAudioBotService.getInstance(guild).updateLastButton(); }else{ logger.debug("[" + guild.getName() + "] End of track, start next."); nextTrack(); @@ -160,30 +160,30 @@ public class TrackScheduler extends AudioEventAdapter { @Override public void onTrackStart(AudioPlayer player, AudioTrack track) { super.onTrackStart(player, track); - GuildAudioWrapper.getInstance(guild).updateLastButton(); + GuildAudioBotService.getInstance(guild).updateLastButton(); } @Override public void onPlayerPause(AudioPlayer player) { super.onPlayerPause(player); - GuildAudioWrapper.getInstance(guild).updateLastButton(); + GuildAudioBotService.getInstance(guild).updateLastButton(); } @Override public void onPlayerResume(AudioPlayer player) { super.onPlayerResume(player); - GuildAudioWrapper.getInstance(guild).updateLastButton(); + GuildAudioBotService.getInstance(guild).updateLastButton(); } @Override public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) { super.onTrackException(player, track, exception); - GuildAudioWrapper.getInstance(guild).updateLastButton(); + GuildAudioBotService.getInstance(guild).updateLastButton(); } @Override public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) { super.onTrackStuck(player, track, thresholdMs); - GuildAudioWrapper.getInstance(guild).updateLastButton(); + GuildAudioBotService.getInstance(guild).updateLastButton(); } } diff --git a/src/main/java/net/Broken/BotListener.java b/src/main/java/net/Broken/BotListener.java index d9d3679..5fe53c9 100644 --- a/src/main/java/net/Broken/BotListener.java +++ b/src/main/java/net/Broken/BotListener.java @@ -1,6 +1,6 @@ package net.Broken; -import net.Broken.Audio.GuildAudioWrapper; +import net.Broken.Audio.GuildAudioBotService; import net.Broken.DB.Entity.GuildPreferenceEntity; import net.Broken.DB.Repository.GuildPreferenceRepository; import net.Broken.Tools.AutoVoiceChannel; @@ -119,10 +119,10 @@ public class BotListener extends ListenerAdapter { if (event.getGuild().getAudioManager().getConnectedChannel().getMembers().size() == 1) { logger.debug("I'm alone, close audio connection."); - GuildAudioWrapper.getInstance(event.getGuild()).stop(); + GuildAudioBotService.getInstance(event.getGuild()).stop(); } } else if (event.getMember().getUser() == MainBot.jda.getSelfUser()) { - GuildAudioWrapper.getInstance(event.getGuild()).clearLastButton(); + GuildAudioBotService.getInstance(event.getGuild()).clearLastButton(); } AutoVoiceChannel autoVoiceChannel = AutoVoiceChannel.getInstance(event.getGuild()); autoVoiceChannel.leave(event.getChannelLeft()); @@ -147,13 +147,13 @@ public class BotListener extends ListenerAdapter { public void onButtonClick(@NotNull ButtonClickEvent event) { super.onButtonClick(event); event.deferReply().queue(); - GuildAudioWrapper guildAudioWrapper = GuildAudioWrapper.getInstance(event.getGuild()); + GuildAudioBotService guildAudioBotService = GuildAudioBotService.getInstance(event.getGuild()); switch (event.getComponentId()) { - case "pause" -> guildAudioWrapper.pause(event); - case "play" -> guildAudioWrapper.resume(event); - case "next" -> guildAudioWrapper.skipTrack(event); - case "stop" -> guildAudioWrapper.stop(event); - case "disconnect" -> guildAudioWrapper.disconnect(event); + case "pause" -> guildAudioBotService.pause(event); + case "play" -> guildAudioBotService.resume(event); + case "next" -> guildAudioBotService.skipTrack(event); + case "stop" -> guildAudioBotService.stop(event); + case "disconnect" -> guildAudioBotService.disconnect(event); } } diff --git a/src/main/java/net/Broken/SlashCommands/Music.java b/src/main/java/net/Broken/SlashCommands/Music.java index f4eb8d4..30aa6a6 100644 --- a/src/main/java/net/Broken/SlashCommands/Music.java +++ b/src/main/java/net/Broken/SlashCommands/Music.java @@ -1,7 +1,7 @@ package net.Broken.SlashCommands; -import net.Broken.Audio.GuildAudioWrapper; +import net.Broken.Audio.GuildAudioBotService; import net.Broken.SlashCommand; import net.Broken.Tools.EmbedMessageUtils; import net.dv8tion.jda.api.MessageBuilder; @@ -26,7 +26,7 @@ public class Music implements SlashCommand { @Override public void action(SlashCommandEvent event) { - GuildAudioWrapper audio = GuildAudioWrapper.getInstance(event.getGuild()); + GuildAudioBotService audio = GuildAudioBotService.getInstance(event.getGuild()); String action = event.getSubcommandName(); event.deferReply().queue(); switch (action) {