WIP Cleaning up the code base + impl slash commands

This commit is contained in:
2021-10-30 22:05:40 +02:00
parent 425d3a3a92
commit bcf3d957a8
16 changed files with 973 additions and 541 deletions
+48 -49
View File
@@ -2,12 +2,12 @@ import discord
from discord.ext import tasks, commands
import time
from discord.ext.commands import Context
import humanize
import datetime
import lavalink
from utils import metadata
from utils.EmbedGenerator import EmbedGenerator
from utils.paginator import HelpPaginator
from discord import Status
class InformationCog(commands.Cog, name="Information"):
@@ -21,49 +21,49 @@ class InformationCog(commands.Cog, name="Information"):
await self.bot.wait_until_ready()
title = "ck!connect | ck!help"
if self.is_help_msg:
title = (await metadata.fetch_metadata())["name"]
title = "Tfoe broer"
self.is_help_msg = not self.is_help_msg
await self.bot.change_presence(activity=discord.Game(title))
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
@commands.command(description='PONG!', aliases=['pong'])
async def ping(self, ctx):
@commands.command(description="PONG!", aliases=["pong"])
async def ping(self, ctx: Context):
"""Test the latency"""
avatar = ctx.author.avatar_url_as(static_format='jpeg')
emoji = discord.utils.get(ctx.bot.emojis, name='loading')
avatar = ctx.author.avatar.with_static_format("jpeg")
emoji = discord.utils.get(ctx.bot.emojis, name="loading")
start = time.monotonic()
msg = await ctx.send(embed=discord.Embed(
description=f'{emoji} Calculating ping'))
msg = await ctx.send(
embed=discord.Embed(description=f"{emoji} Calculating ping")
)
millis = (time.monotonic() - start) * 1000
heartbeat = ctx.bot.latency * 1000
embed = discord.Embed(color=discord.Color.blue())
embed.add_field(name=':heartbeat: Heartbeat',
value=f'`{heartbeat:,.2f}ms`',
inline=True)
embed.add_field(name=':file_cabinet: ACK',
value=f'`{millis:,.2f}ms`',
inline=True)
embed.set_footer(text=f"Requested by: {ctx.author}",
icon_url=f"{avatar}")
embed.add_field(
name=":heartbeat: Heartbeat", value=f"`{heartbeat:,.2f}ms`", inline=True
)
embed.add_field(
name=":file_cabinet: ACK", value=f"`{millis:,.2f}ms`", inline=True
)
embed.set_footer(text=f"Requested by: {ctx.author}", icon_url=f"{avatar}")
await msg.edit(embed=embed)
@commands.command(name='invite')
@commands.command(
name="invite", description="Gets the invite link!", slash_commands=True
)
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def invite(self, ctx):
async def invite(self, ctx: Context):
"""Gets the invite link!"""
color = self.bot.colors["embed"]
avatar = ctx.author.avatar_url_as(static_format='jpeg')
client_id = ctx.bot.user.id
link = f"https://discord.com/oauth2/authorize?scope=bot&client_id={client_id}&permissions=70642768"
embed = discord.Embed(color=color)
embed.add_field(name='Add our bot to your server:', value=link)
embed.set_footer(text=f"Requested by: {ctx.author}",
icon_url=f"{avatar}")
link = f"https://discord.com/oauth2/authorize?client_id=643555373814382593&permissions=3230720&scope=bot%20applications.commands"
embed = await EmbedGenerator.Message(ctx, "Add our bot to your server:", link)
await ctx.send(embed=embed)
@commands.command(
name="wlinfo",
description="Retrieve various Node/Server/Player information.",
slash_commands=True,
)
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
@commands.command()
async def wlinfo(self, ctx):
async def wlinfo(self, ctx: Context):
"""Retrieve various Node/Server/Player information."""
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
node = player.node
@@ -73,28 +73,29 @@ class InformationCog(commands.Cog, name="Information"):
free = humanize.naturalsize(node.stats.memory_free)
cpu = node.stats.cpu_cores
fmt = f'**WaveLink:** `{lavalink.__version__}`\n\n' \
f'Connected to `{len(self.bot.lavalink.nodes)}` nodes.\n' \
f'Best available Node `{self.bot.lavalink.get_best_node().__repr__()}`\n' \
f'`{len(self.bot.lavalink.players)}` players are distributed on nodes.\n' \
f'`{node.stats.players}` players are distributed on server.\n' \
f'`{node.stats.playing_players}` players are playing on server.\n\n' \
f'Server Memory: `{used}/{total}` | `({free} free)`\n' \
f'Server CPU: `{cpu}`\n\n' \
f'Server Uptime: `{datetime.timedelta(milliseconds=node.stats.uptime)}`'
fmt = (
f"**WaveLink:** `{lavalink.__version__}`\n\n"
f"Connected to `{len(self.bot.lavalink.nodes)}` nodes.\n"
f"Best available Node `{self.bot.lavalink.get_best_node().__repr__()}`\n"
f"`{len(self.bot.lavalink.players)}` players are distributed on nodes.\n"
f"`{node.stats.players}` players are distributed on server.\n"
f"`{node.stats.playing_players}` players are playing on server.\n\n"
f"Server Memory: `{used}/{total}` | `({free} free)`\n"
f"Server CPU: `{cpu}`\n\n"
f"Server Uptime: `{datetime.timedelta(milliseconds=node.stats.uptime)}`"
)
await ctx.send(fmt)
@commands.command(name="help", aliases=["about", "info"])
@commands.command(name="help", aliases=["about", "info"], slash_command=True)
@commands.cooldown(1, 1, commands.BucketType.user)
async def about(self, ctx, command: str = None):
async def about(self, ctx: Context):
"""Exobot command list"""
if command:
if None:
entity = self.bot.get_cog(command) or self.bot.get_command(command)
if entity is None:
clean = command.replace('@', '@\u200b')
return await ctx.send(
f'Command or category "{clean}" not found.')
clean = command.replace("@", "@\u200b")
return await ctx.send(f'Command or category "{clean}" not found.')
elif isinstance(entity, discord.ext.commands.Command):
p = await HelpPaginator.from_command(ctx, entity)
else:
@@ -111,7 +112,7 @@ class InformationCog(commands.Cog, name="Information"):
display_cogs = {
"Information": ":information_source:",
"Music": ":musical_note:",
"Settings": ":gear:"
"Settings": ":gear:",
}
for cog_name, cog_icon in display_cogs.items():
@@ -119,13 +120,11 @@ class InformationCog(commands.Cog, name="Information"):
if not cog:
continue
cogname_str = f"{cog_icon} {cog_name}"
commands = [
f"`{cmd.name}`" for cmd in cog.get_commands() if not cmd.hidden
]
commands = [f"`{cmd.name}`" for cmd in cog.get_commands() if not cmd.hidden]
commands_str = ", ".join(commands)
embed.add_field(name=cogname_str, value=commands_str, inline=False)
avatar = ctx.author.avatar_url_as(static_format='jpeg')
avatar = ctx.author.avatar.with_static_format("jpeg")
embed.set_footer(text=f"Requested by: {ctx.author}", icon_url=avatar)
await ctx.send(embed=embed)
+51 -54
View File
@@ -1,13 +1,10 @@
import discord
from discord import channel
from discord.ext import commands
import time
import lavalink
import re
import random
import asyncio
from utils.database import AutoJoin
from utils import metadata
from bot import ChristmasBot
from utils.EmbedGenerator import EmbedGenerator
@@ -16,12 +13,12 @@ class MusicCog(commands.Cog, name="Music"):
self.bot = bot
self.stream = "https://azuracast.exobot.site/radio/8000/radio.opus"
if not hasattr(bot, 'lavalink'):
if not hasattr(bot, "lavalink"):
bot.lavalink = lavalink.Client(bot.user.id)
bot.lavalink.add_node('de-1.rivalmc.net', 2333, '12345', 'eu',
'poggers')
bot.add_listener(self.bot.lavalink.voice_update_handler,
'on_socket_response')
bot.lavalink.add_node("de-1.rivalmc.net", 2333, "12345", "eu", "poggers")
bot.add_listener(
self.bot.lavalink.voice_update_handler, "on_socket_response"
)
lavalink.add_event_hook(self.track_hook)
@@ -29,58 +26,60 @@ class MusicCog(commands.Cog, name="Music"):
async def async_init(self):
await self.bot.wait_until_ready()
channels = await AutoJoin.get_channels(self.bot)
channels = []
# We startup to fast #NOTPOGGERS
await asyncio.sleep(5)
for x in channels:
guild = self.bot.get_guild(x[0])
player = self.bot.lavalink.player_manager.create(x[0],
endpoint=str(
guild.region))
for channel in channels:
guild = self.bot.get_guild(channel[0])
player = self.bot.lavalink.player_manager.create(
channel[0], endpoint=str(guild.region)
)
track = await player.node.get_tracks(self.stream)
if not player.is_playing:
await player.play(track["tracks"][0])
await self.connect_to(x[0], x[1])
await self.connect_to(channel[0], channel[1])
def cog_unload(self):
""" Cog unload handler. This removes any event hooks that were registered. """
"""Cog unload handler. This removes any event hooks that were registered."""
self.bot.lavalink._event_hooks.clear()
async def cog_before_invoke(self, ctx):
""" Command before-invoke handler. """
"""Command before-invoke handler."""
guild_check = ctx.guild is not None
if guild_check:
await self.ensure_voice(ctx)
return guild_check
async def ensure_voice(self, ctx):
""" This check ensures that the bot and command author are in the same voicechannel. """
player = self.bot.lavalink.player_manager.create(ctx.guild.id,
endpoint=str(
ctx.guild.region))
should_connect = ctx.command.name in ('connect', )
"""This check ensures that the bot and command author are in the same voicechannel."""
player = self.bot.lavalink.player_manager.create(
ctx.guild.id, endpoint=str(ctx.guild.region)
)
should_connect = ctx.command.name in ("connect",)
if not ctx.author.voice or not ctx.author.voice.channel:
raise commands.CommandInvokeError('Join a voicechannel first.')
raise commands.CommandInvokeError("Join a voicechannel first.")
if not player.is_connected:
if not should_connect:
raise commands.CommandInvokeError('Not connected.')
raise commands.CommandInvokeError("Not connected.")
permissions = ctx.author.voice.channel.permissions_for(ctx.me)
if not permissions.connect or not permissions.speak: # Check user limit too?
if (
not permissions.connect or not permissions.speak
): # Check user limit too?
raise commands.CommandInvokeError(
'I need the `CONNECT` and `SPEAK` permissions.')
"I need the `CONNECT` and `SPEAK` permissions."
)
player.store('channel', ctx.channel.id)
await self.connect_to(ctx.guild.id,
str(ctx.author.voice.channel.id))
player.store("channel", ctx.channel.id)
await self.connect_to(ctx.guild.id, str(ctx.author.voice.channel.id))
else:
if int(player.channel_id) != ctx.author.voice.channel.id:
raise commands.CommandInvokeError(
'You need to be in my voicechannel.')
raise commands.CommandInvokeError("You need to be in my voicechannel.")
async def track_hook(self, event):
if isinstance(event, lavalink.events.QueueEndEvent):
@@ -88,60 +87,58 @@ class MusicCog(commands.Cog, name="Music"):
await self.connect_to(guild_id, None)
async def connect_to(self, guild_id: int, channel_id: str):
""" Connects to the given voicechannel ID. A channel_id of `None` means disconnect. """
"""Connects to the given voicechannel ID. A channel_id of `None` means disconnect."""
ws = self.bot._connection._get_websocket(guild_id)
await ws.voice_state(str(guild_id), channel_id)
@commands.command(name='connect')
@commands.command(name="connect")
async def connect(self, ctx):
"""Starts vibing."""
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
results = await player.node.get_tracks(self.stream)
if not results or not results['tracks']:
return await ctx.send('Nothing found!')
if not results or not results["tracks"]:
return await ctx.send("Nothing found!")
if results['loadType'] == 'PLAYLIST_LOADED':
tracks = results['tracks']
if results["loadType"] == "PLAYLIST_LOADED":
tracks = results["tracks"]
for track in tracks:
player.add(requester=ctx.author.id, track=track)
else:
track = results['tracks'][0]
track = lavalink.models.AudioTrack(track,
ctx.author.id,
recommended=True)
track = results["tracks"][0]
track = lavalink.models.AudioTrack(track, ctx.author.id, recommended=True)
player.add(requester=ctx.author.id, track=track)
if not player.is_playing:
await player.play()
@commands.command(aliases=['dc'])
@commands.command(aliases=["dc"])
async def disconnect(self, ctx):
""" Disconnects the player from the voice channel and clears its queue. """
"""Disconnects the player from the voice channel and clears its queue."""
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
if not player.is_connected:
return await ctx.send('Not connected.')
return await ctx.send("Not connected.")
if not ctx.author.voice or (
player.is_connected
and ctx.author.voice.channel.id != int(player.channel_id)):
return await ctx.send('You\'re not in my voicechannel!')
player.is_connected
and ctx.author.voice.channel.id != int(player.channel_id)
):
return await ctx.send("You're not in my voicechannel!")
player.queue.clear()
await player.stop()
await self.connect_to(ctx.guild.id, None)
@commands.command(name='now', aliases=['playing'])
@commands.command(name="now", aliases=["playing"])
async def now_playing(self, ctx):
"""Stop and disconnect the player and controller."""
np = await metadata.fetch_metadata()
em = discord.Embed(color=self.bot.colors["embed"])
em.set_thumbnail(url=np["thumbnail"])
em.add_field(name="Currently playing:", value=np["name"])
# em.set_thumbnail(url=np["thumbnail"])
em.add_field(name="Currently playing:", value="Some song")
await EmbedGenerator.SendWithFooter(ctx, em)
def setup(bot):
def setup(bot: ChristmasBot):
bot.add_cog(MusicCog(bot))
+74 -62
View File
@@ -11,32 +11,36 @@ from io import BytesIO
from platform import python_version
from contextlib import redirect_stdout
from discord.ext.commands.context import Context
from bot import ChristmasBot
class OwnerCog(commands.Cog):
def __init__(self, bot):
def __init__(self, bot: ChristmasBot):
self.bot = bot
self._last_result = None
@staticmethod
def cleanup_code(content):
if content.startswith('```') and content.endswith('```'):
return '\n'.join(content.split('\n')[1:-1])
return content.strip('` \n')
if content.startswith("```") and content.endswith("```"):
return "\n".join(content.split("\n")[1:-1])
return content.strip("` \n")
# Hidden means it won't show up on the default help.
@commands.command(name='load', hidden=True)
@commands.command(name="load", hidden=True)
@commands.is_owner()
async def _cog_load(self, ctx, *, cog: str):
async def _cog_load(self, ctx: Context, *, cog: str):
"""Command which Loads a Module."""
try:
self.bot.load_extension(cog)
except Exception as e:
await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
await ctx.send(f"**`ERROR:`** {type(e).__name__} - {e}")
else:
await ctx.send('**`SUCCESS`**')
await ctx.send("**`SUCCESS`**")
@commands.command(name='unload', hidden=True)
@commands.command(name="unload", hidden=True)
@commands.is_owner()
async def _cog_unload(self, ctx, *, cog: str):
"""Command which Unloads a Module."""
@@ -44,11 +48,11 @@ class OwnerCog(commands.Cog):
try:
self.bot.unload_extension(cog)
except Exception as e:
await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
await ctx.send(f"**`ERROR:`** {type(e).__name__} - {e}")
else:
await ctx.send('**`SUCCESS`**')
await ctx.send("**`SUCCESS`**")
@commands.command(name='reload', hidden=True)
@commands.command(name="reload", hidden=True)
@commands.is_owner()
async def _cog_reload(self, ctx, *, cog: str):
"""Command which Reloads a Module."""
@@ -57,35 +61,29 @@ class OwnerCog(commands.Cog):
self.bot.unload_extension(cog)
self.bot.load_extension(cog)
except Exception as e:
await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
await ctx.send(f"**`ERROR:`** {type(e).__name__} - {e}")
else:
await ctx.send('**`SUCCESS`**')
await ctx.send("**`SUCCESS`**")
@commands.command(name='shutdown', hidden=True)
@commands.command(name="shutdown", hidden=True)
@commands.is_owner()
async def shutdown(self, ctx):
"""Command which shutdowns the bot."""
await ctx.bot.logout()
@commands.is_owner()
@commands.command(pass_context=True,
hidden=True,
name='eval',
aliases=['evaluate'])
@commands.command(pass_context=True, hidden=True, name="eval", aliases=["evaluate"])
async def _eval(self, ctx, *, body: str):
env = {
'bot': self.bot,
'ctx': ctx,
'channel': ctx.channel,
'author': ctx.author,
'guild': ctx.guild,
'message': ctx.message,
'_': self._last_result
"bot": self.bot,
"ctx": ctx,
"channel": ctx.channel,
"author": ctx.author,
"guild": ctx.guild,
"message": ctx.message,
"_": self._last_result,
}
if "import os" in body and ctx.author.id == 190875175460405249:
return await ctx.send("Ah ah ah, you didn't say the magic word.")
env.update(globals())
body = self.cleanup_code(body)
@@ -98,26 +96,34 @@ class OwnerCog(commands.Cog):
exec(to_compile, env)
except Exception as e:
# await ctx.message.add_reaction('naokoerror:447495055603662849')
fooem = discord.Embed(color=0xff0000)
fooem.add_field(name="Code evaluation was not successful.",
value=f'```\n{e.__class__.__name__}: {e}\n```')
fooem.set_footer(text=f"Evaluated using Python {python_version()}",
icon_url="http://i.imgur.com/9EftiVK.png")
fooem = discord.Embed(color=0xFF0000)
fooem.add_field(
name="Code evaluation was not successful.",
value=f"```\n{e.__class__.__name__}: {e}\n```",
)
fooem.set_footer(
text=f"Evaluated using Python {python_version()}",
icon_url="http://i.imgur.com/9EftiVK.png",
)
await ctx.send(embed=fooem)
# await ctx.message.remove_reaction('a:loading:452489773396000778', member=ctx.me)
func = env['func']
func = env["func"]
try:
with redirect_stdout(stdout):
ret = await func()
except Exception as e:
value = stdout.getvalue()
# await ctx.message.add_reaction('naokoerror:447495055603662849')
fooem = discord.Embed(color=0xff0000)
fooem.add_field(name="Code evaluation was not successful.",
value=f'```\n{value}{traceback.format_exc()}\n```')
fooem.set_footer(text=f"Evaluated using Python {python_version()}",
icon_url="http://i.imgur.com/9EftiVK.png")
fooem = discord.Embed(color=0xFF0000)
fooem.add_field(
name="Code evaluation was not successful.",
value=f"```\n{value}{traceback.format_exc()}\n```",
)
fooem.set_footer(
text=f"Evaluated using Python {python_version()}",
icon_url="http://i.imgur.com/9EftiVK.png",
)
await ctx.send(embed=fooem)
try:
# await ctx.message.remove_reaction('a:loading:452489773396000778', member=ctx.me)
@@ -129,7 +135,8 @@ class OwnerCog(commands.Cog):
value = stdout.getvalue()
try:
await ctx.message.remove_reaction(
'a:loading:452489773396000778', member=ctx.me)
"a:loading:452489773396000778", member=ctx.me
)
# await ctx.message.add_reaction('naokotick:447494238872141827')
except Exception:
pass
@@ -137,37 +144,42 @@ class OwnerCog(commands.Cog):
if ret is None:
if value:
sfooem = discord.Embed(color=0x170041)
sfooem.add_field(name="Code evaluation was successful!",
value=f'```\n{value}\n```')
sfooem.add_field(
name="Code evaluation was successful!",
value=f"```\n{value}\n```",
)
sfooem.set_footer(
text=f"Evaluated using Python {python_version()}",
icon_url="http://i.imgur.com/9EftiVK.png")
icon_url="http://i.imgur.com/9EftiVK.png",
)
await ctx.send(embed=sfooem)
else:
self._last_result = ret
ssfooem = discord.Embed(color=0x170041)
ssfooem.add_field(name="Code evaluation was successful!",
value=f'```\n{value}{ret}\n```')
ssfooem.add_field(
name="Code evaluation was successful!",
value=f"```\n{value}{ret}\n```",
)
ssfooem.set_footer(
text=f"Evaluated using Python {python_version()}",
icon_url="http://i.imgur.com/9EftiVK.png")
icon_url="http://i.imgur.com/9EftiVK.png",
)
await ctx.send(embed=ssfooem)
@commands.is_owner()
@commands.command(hidden=True, aliases=['exec'])
@commands.command(hidden=True, aliases=["exec"])
async def execute(self, ctx, *, text: str):
""" Do a shell command. """
"""Do a shell command."""
message = await ctx.send(f"Loading...")
proc = await asyncio.create_subprocess_shell(text,
stdin=None,
stderr=PIPE,
stdout=PIPE)
out = (await proc.stdout.read()).decode('utf-8').strip()
err = (await proc.stderr.read()).decode('utf-8').strip()
proc = await asyncio.create_subprocess_shell(
text, stdin=None, stderr=PIPE, stdout=PIPE
)
out = (await proc.stdout.read()).decode("utf-8").strip()
err = (await proc.stderr.read()).decode("utf-8").strip()
if not out and not err:
await message.delete()
return await ctx.message.add_reaction('👌')
return await ctx.message.add_reaction("👌")
content = ""
@@ -178,12 +190,12 @@ class OwnerCog(commands.Cog):
if len(content) > 1500:
try:
data = BytesIO(content.encode('utf-8'))
data = BytesIO(content.encode("utf-8"))
await message.delete()
await ctx.send(content=f"The result was a bit too long..",
file=discord.File(
data,
filename=f"result_{int(time.time())}.txt"))
await ctx.send(
content=f"The result was a bit too long..",
file=discord.File(data, filename=f"result_{int(time.time())}.txt"),
)
except asyncio.TimeoutError as e:
await message.delete()
return await ctx.send(e)
@@ -191,5 +203,5 @@ class OwnerCog(commands.Cog):
await message.edit(content=f"```fix\n{content}\n```")
def setup(bot):
def setup(bot: ChristmasBot):
bot.add_cog(OwnerCog(bot))
+9 -16
View File
@@ -1,33 +1,26 @@
from discord.ext import commands
from utils.database import AutoJoin
import discord
from utils.EmbedGenerator import EmbedGenerator
from bot import ChristmasBot
from discord.ext.commands import Context
class SettingsCog(commands.Cog, name="Settings"):
def __init__(self, bot):
def __init__(self, bot: ChristmasBot):
self.bot = bot
@commands.command(name='placeholder', hidden=True)
@commands.is_owner()
async def placeholder(self, ctx):
"""Placeholder Command"""
await ctx.send("Pong!")
@commands.group(aliases=["aj"], invoke_without_command=True)
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def autojoin(self, ctx):
async def autojoin(self, ctx: Context):
await EmbedGenerator.Message(
ctx, "Autojoin",
f"Usage:\n\n`{ctx.prefix}autojoin set`\n`{ctx.prefix}autojoin unset`"
ctx,
"Autojoin",
f"Usage:\n\n`{ctx.prefix}autojoin set`\n`{ctx.prefix}autojoin unset`",
)
@autojoin.command(name="set")
@commands.has_permissions(manage_channels=True)
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def autojoin_set(self, ctx):
async def autojoin_set(self, ctx: Context):
vc = ctx.author.voice.channel
await AutoJoin.update_channel(self.bot, ctx.guild.id, vc.id)
await EmbedGenerator.Message(ctx, "Autojoin", "`enabled`")
@@ -35,11 +28,11 @@ class SettingsCog(commands.Cog, name="Settings"):
@autojoin.command(name="unset")
@commands.has_permissions(manage_channels=True)
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def autojoin_del(self, ctx):
async def autojoin_del(self, ctx: Context):
vc = ctx.author.voice.channel
await AutoJoin.del_channel(self.bot, ctx.guild.id)
await EmbedGenerator.Message(ctx, "Autojoin", "`disabled`")
def setup(bot):
def setup(bot: ChristmasBot):
bot.add_cog(SettingsCog(bot))