mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
570 lines
22 KiB
Python
570 lines
22 KiB
Python
import datetime
|
||
import time
|
||
from enum import Enum
|
||
from random import randint, choice
|
||
from typing import Final
|
||
import urllib.parse
|
||
import aiohttp
|
||
import discord
|
||
from redbot.core import commands
|
||
from redbot.core.i18n import Translator, cog_i18n
|
||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
|
||
from redbot.core.utils.chat_formatting import (
|
||
bold,
|
||
escape,
|
||
italics,
|
||
humanize_number,
|
||
humanize_timedelta,
|
||
)
|
||
|
||
_ = T_ = Translator("General", __file__)
|
||
|
||
|
||
class RPS(Enum):
|
||
rock = "\N{MOYAI}"
|
||
paper = "\N{PAGE FACING UP}"
|
||
scissors = "\N{BLACK SCISSORS}\N{VARIATION SELECTOR-16}"
|
||
|
||
|
||
class RPSParser:
|
||
def __init__(self, argument):
|
||
argument = argument.lower()
|
||
if argument == "rock":
|
||
self.choice = RPS.rock
|
||
elif argument == "paper":
|
||
self.choice = RPS.paper
|
||
elif argument == "scissors":
|
||
self.choice = RPS.scissors
|
||
else:
|
||
self.choice = None
|
||
|
||
|
||
MAX_ROLL: Final[int] = 2 ** 64 - 1
|
||
|
||
|
||
@cog_i18n(_)
|
||
class General(commands.Cog):
|
||
"""General commands."""
|
||
|
||
global _
|
||
_ = lambda s: s
|
||
ball = [
|
||
_("As I see it, yes"),
|
||
_("It is certain"),
|
||
_("It is decidedly so"),
|
||
_("Most likely"),
|
||
_("Outlook good"),
|
||
_("Signs point to yes"),
|
||
_("Without a doubt"),
|
||
_("Yes"),
|
||
_("Yes – definitely"),
|
||
_("You may rely on it"),
|
||
_("Reply hazy, try again"),
|
||
_("Ask again later"),
|
||
_("Better not tell you now"),
|
||
_("Cannot predict now"),
|
||
_("Concentrate and ask again"),
|
||
_("Don't count on it"),
|
||
_("My reply is no"),
|
||
_("My sources say no"),
|
||
_("Outlook not so good"),
|
||
_("Very doubtful"),
|
||
]
|
||
_ = T_
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.stopwatches = {}
|
||
|
||
async def red_delete_data_for_user(self, **kwargs):
|
||
""" Nothing to delete """
|
||
return
|
||
|
||
@commands.command(usage="<choice> <choices...>")
|
||
async def choose(self, ctx, *choices):
|
||
"""Choose between multiple options.
|
||
|
||
To denote options which include whitespace, you should use
|
||
double quotes.
|
||
"""
|
||
choices = [escape(c, mass_mentions=True) for c in choices if c]
|
||
if len(choices) < 2:
|
||
await ctx.send(_("Not enough options to pick from."))
|
||
else:
|
||
await ctx.send(choice(choices))
|
||
|
||
@commands.command()
|
||
async def roll(self, ctx, number: int = 100):
|
||
"""Roll a random number.
|
||
|
||
The result will be between 1 and `<number>`.
|
||
|
||
`<number>` defaults to 100.
|
||
"""
|
||
author = ctx.author
|
||
if 1 < number <= MAX_ROLL:
|
||
n = randint(1, number)
|
||
await ctx.send(
|
||
"{author.mention} :game_die: {n} :game_die:".format(
|
||
author=author, n=humanize_number(n)
|
||
)
|
||
)
|
||
elif number <= 1:
|
||
await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author))
|
||
else:
|
||
await ctx.send(
|
||
_("{author.mention} Max allowed number is {maxamount}.").format(
|
||
author=author, maxamount=humanize_number(MAX_ROLL)
|
||
)
|
||
)
|
||
|
||
@commands.command()
|
||
async def flip(self, ctx, user: discord.Member = None):
|
||
"""Flip a coin... or a user.
|
||
|
||
Defaults to a coin.
|
||
"""
|
||
if user is not None:
|
||
msg = ""
|
||
if user.id == ctx.bot.user.id:
|
||
user = ctx.author
|
||
msg = _("Nice try. You think this is funny?\n How about *this* instead:\n\n")
|
||
char = "abcdefghijklmnopqrstuvwxyz"
|
||
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
|
||
table = str.maketrans(char, tran)
|
||
name = user.display_name.translate(table)
|
||
char = char.upper()
|
||
tran = "∀qƆpƎℲפHIſʞ˥WNOԀQᴚS┴∩ΛMX⅄Z"
|
||
table = str.maketrans(char, tran)
|
||
name = name.translate(table)
|
||
await ctx.send(msg + "(╯°□°)╯︵ " + name[::-1])
|
||
else:
|
||
await ctx.send(_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")]))
|
||
|
||
@commands.command()
|
||
async def rps(self, ctx, your_choice: RPSParser):
|
||
"""Play Rock Paper Scissors."""
|
||
author = ctx.author
|
||
player_choice = your_choice.choice
|
||
if not player_choice:
|
||
return await ctx.send(
|
||
_("This isn't a valid option. Try {r}, {p}, or {s}.").format(
|
||
r="rock", p="paper", s="scissors"
|
||
)
|
||
)
|
||
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
||
cond = {
|
||
(RPS.rock, RPS.paper): False,
|
||
(RPS.rock, RPS.scissors): True,
|
||
(RPS.paper, RPS.rock): True,
|
||
(RPS.paper, RPS.scissors): False,
|
||
(RPS.scissors, RPS.rock): False,
|
||
(RPS.scissors, RPS.paper): True,
|
||
}
|
||
|
||
if red_choice == player_choice:
|
||
outcome = None # Tie
|
||
else:
|
||
outcome = cond[(player_choice, red_choice)]
|
||
|
||
if outcome is True:
|
||
await ctx.send(
|
||
_("{choice} You win {author.mention}!").format(
|
||
choice=red_choice.value, author=author
|
||
)
|
||
)
|
||
elif outcome is False:
|
||
await ctx.send(
|
||
_("{choice} You lose {author.mention}!").format(
|
||
choice=red_choice.value, author=author
|
||
)
|
||
)
|
||
else:
|
||
await ctx.send(
|
||
_("{choice} We're square {author.mention}!").format(
|
||
choice=red_choice.value, author=author
|
||
)
|
||
)
|
||
|
||
@commands.command(name="8", aliases=["8ball"])
|
||
async def _8ball(self, ctx, *, question: str):
|
||
"""Ask 8 ball a question.
|
||
|
||
Question must end with a question mark.
|
||
"""
|
||
if question.endswith("?") and question != "?":
|
||
await ctx.send("`" + T_(choice(self.ball)) + "`")
|
||
else:
|
||
await ctx.send(_("That doesn't look like a question."))
|
||
|
||
@commands.command(aliases=["sw"])
|
||
async def stopwatch(self, ctx):
|
||
"""Start or stop the stopwatch."""
|
||
author = ctx.author
|
||
if author.id not in self.stopwatches:
|
||
self.stopwatches[author.id] = int(time.perf_counter())
|
||
await ctx.send(author.mention + _(" Stopwatch started!"))
|
||
else:
|
||
tmp = abs(self.stopwatches[author.id] - int(time.perf_counter()))
|
||
tmp = str(datetime.timedelta(seconds=tmp))
|
||
await ctx.send(
|
||
author.mention + _(" Stopwatch stopped! Time: **{seconds}**").format(seconds=tmp)
|
||
)
|
||
self.stopwatches.pop(author.id, None)
|
||
|
||
@commands.command()
|
||
async def lmgtfy(self, ctx, *, search_terms: str):
|
||
"""Create a lmgtfy link."""
|
||
search_terms = escape(urllib.parse.quote_plus(search_terms), mass_mentions=True)
|
||
await ctx.send("https://lmgtfy.app/?q={}".format(search_terms))
|
||
|
||
@commands.command(hidden=True)
|
||
@commands.guild_only()
|
||
async def hug(self, ctx, user: discord.Member, intensity: int = 1):
|
||
"""Because everyone likes hugs!
|
||
|
||
Up to 10 intensity levels.
|
||
"""
|
||
name = italics(user.display_name)
|
||
if intensity <= 0:
|
||
msg = "(っ˘̩╭╮˘̩)っ" + name
|
||
elif intensity <= 3:
|
||
msg = "(っ´▽`)っ" + name
|
||
elif intensity <= 6:
|
||
msg = "╰(*´︶`*)╯" + name
|
||
elif intensity <= 9:
|
||
msg = "(つ≧▽≦)つ" + name
|
||
elif intensity >= 10:
|
||
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
||
else:
|
||
# For the purposes of "msg might not be defined" linter errors
|
||
raise RuntimeError
|
||
await ctx.send(msg)
|
||
|
||
@commands.command()
|
||
@commands.guild_only()
|
||
@commands.bot_has_permissions(embed_links=True)
|
||
async def serverinfo(self, ctx, details: bool = False):
|
||
"""
|
||
Show server information.
|
||
|
||
`details`: Shows more information when set to `True`.
|
||
Default to False.
|
||
"""
|
||
guild = ctx.guild
|
||
passed = (ctx.message.created_at - guild.created_at).days
|
||
created_at = _("Created on {date}. That's over {num} days ago!").format(
|
||
date=guild.created_at.strftime("%d %b %Y %H:%M"),
|
||
num=humanize_number(passed),
|
||
)
|
||
online = humanize_number(
|
||
len([m.status for m in guild.members if m.status != discord.Status.offline])
|
||
)
|
||
total_users = humanize_number(guild.member_count)
|
||
text_channels = humanize_number(len(guild.text_channels))
|
||
voice_channels = humanize_number(len(guild.voice_channels))
|
||
if not details:
|
||
data = discord.Embed(description=created_at, colour=await ctx.embed_colour())
|
||
data.add_field(name=_("Region"), value=str(guild.region))
|
||
data.add_field(name=_("Users online"), value=f"{online}/{total_users}")
|
||
data.add_field(name=_("Text Channels"), value=text_channels)
|
||
data.add_field(name=_("Voice Channels"), value=voice_channels)
|
||
data.add_field(name=_("Roles"), value=humanize_number(len(guild.roles)))
|
||
data.add_field(name=_("Owner"), value=str(guild.owner))
|
||
data.set_footer(
|
||
text=_("Server ID: ")
|
||
+ str(guild.id)
|
||
+ _(" • Use {command} for more info on the server.").format(
|
||
command=f"{ctx.clean_prefix}serverinfo 1"
|
||
)
|
||
)
|
||
if guild.icon_url:
|
||
data.set_author(name=guild.name, url=guild.icon_url)
|
||
data.set_thumbnail(url=guild.icon_url)
|
||
else:
|
||
data.set_author(name=guild.name)
|
||
else:
|
||
|
||
def _size(num: int):
|
||
for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]:
|
||
if abs(num) < 1024.0:
|
||
return "{0:.1f}{1}".format(num, unit)
|
||
num /= 1024.0
|
||
return "{0:.1f}{1}".format(num, "YB")
|
||
|
||
def _bitsize(num: int):
|
||
for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]:
|
||
if abs(num) < 1000.0:
|
||
return "{0:.1f}{1}".format(num, unit)
|
||
num /= 1000.0
|
||
return "{0:.1f}{1}".format(num, "YB")
|
||
|
||
shard_info = (
|
||
_("\nShard ID: **{shard_id}/{shard_count}**").format(
|
||
shard_id=humanize_number(guild.shard_id + 1),
|
||
shard_count=humanize_number(ctx.bot.shard_count),
|
||
)
|
||
if ctx.bot.shard_count > 1
|
||
else ""
|
||
)
|
||
# Logic from: https://github.com/TrustyJAID/Trusty-cogs/blob/master/serverstats/serverstats.py#L159
|
||
online_stats = {
|
||
_("Humans: "): lambda x: not x.bot,
|
||
_(" • Bots: "): lambda x: x.bot,
|
||
"\N{LARGE GREEN CIRCLE}": lambda x: x.status is discord.Status.online,
|
||
"\N{LARGE ORANGE CIRCLE}": lambda x: x.status is discord.Status.idle,
|
||
"\N{LARGE RED CIRCLE}": lambda x: x.status is discord.Status.do_not_disturb,
|
||
"\N{MEDIUM WHITE CIRCLE}\N{VARIATION SELECTOR-16}": lambda x: (
|
||
x.status is discord.Status.offline
|
||
),
|
||
"\N{LARGE PURPLE CIRCLE}": lambda x: any(
|
||
a.type is discord.ActivityType.streaming for a in x.activities
|
||
),
|
||
"\N{MOBILE PHONE}": lambda x: x.is_on_mobile(),
|
||
}
|
||
member_msg = _("Users online: **{online}/{total_users}**\n").format(
|
||
online=online, total_users=total_users
|
||
)
|
||
count = 1
|
||
for emoji, value in online_stats.items():
|
||
try:
|
||
num = len([m for m in guild.members if value(m)])
|
||
except Exception as error:
|
||
print(error)
|
||
continue
|
||
else:
|
||
member_msg += f"{emoji} {bold(humanize_number(num))} " + (
|
||
"\n" if count % 2 == 0 else ""
|
||
)
|
||
count += 1
|
||
|
||
vc_regions = {
|
||
"vip-us-east": _("__VIP__ US East ") + "\U0001F1FA\U0001F1F8",
|
||
"vip-us-west": _("__VIP__ US West ") + "\U0001F1FA\U0001F1F8",
|
||
"vip-amsterdam": _("__VIP__ Amsterdam ") + "\U0001F1F3\U0001F1F1",
|
||
"eu-west": _("EU West ") + "\U0001F1EA\U0001F1FA",
|
||
"eu-central": _("EU Central ") + "\U0001F1EA\U0001F1FA",
|
||
"europe": _("Europe ") + "\U0001F1EA\U0001F1FA",
|
||
"london": _("London ") + "\U0001F1EC\U0001F1E7",
|
||
"frankfurt": _("Frankfurt ") + "\U0001F1E9\U0001F1EA",
|
||
"amsterdam": _("Amsterdam ") + "\U0001F1F3\U0001F1F1",
|
||
"us-west": _("US West ") + "\U0001F1FA\U0001F1F8",
|
||
"us-east": _("US East ") + "\U0001F1FA\U0001F1F8",
|
||
"us-south": _("US South ") + "\U0001F1FA\U0001F1F8",
|
||
"us-central": _("US Central ") + "\U0001F1FA\U0001F1F8",
|
||
"singapore": _("Singapore ") + "\U0001F1F8\U0001F1EC",
|
||
"sydney": _("Sydney ") + "\U0001F1E6\U0001F1FA",
|
||
"brazil": _("Brazil ") + "\U0001F1E7\U0001F1F7",
|
||
"hongkong": _("Hong Kong ") + "\U0001F1ED\U0001F1F0",
|
||
"russia": _("Russia ") + "\U0001F1F7\U0001F1FA",
|
||
"japan": _("Japan ") + "\U0001F1EF\U0001F1F5",
|
||
"southafrica": _("South Africa ") + "\U0001F1FF\U0001F1E6",
|
||
"india": _("India ") + "\U0001F1EE\U0001F1F3",
|
||
"dubai": _("Dubai ") + "\U0001F1E6\U0001F1EA",
|
||
"south-korea": _("South Korea ") + "\U0001f1f0\U0001f1f7",
|
||
}
|
||
verif = {
|
||
"none": _("0 - None"),
|
||
"low": _("1 - Low"),
|
||
"medium": _("2 - Medium"),
|
||
"high": _("3 - High"),
|
||
"extreme": _("4 - Extreme"),
|
||
}
|
||
|
||
features = {
|
||
"ANIMATED_ICON": _("Animated Icon"),
|
||
"BANNER": _("Banner Image"),
|
||
"COMMERCE": _("Commerce"),
|
||
"COMMUNITY": _("Community"),
|
||
"DISCOVERABLE": _("Server Discovery"),
|
||
"FEATURABLE": _("Featurable"),
|
||
"INVITE_SPLASH": _("Splash Invite"),
|
||
"MEMBER_LIST_DISABLED": _("Member list disabled"),
|
||
"MEMBER_VERIFICATION_GATE_ENABLED": _("Membership Screening enabled"),
|
||
"MORE_EMOJI": _("More Emojis"),
|
||
"NEWS": _("News Channels"),
|
||
"PARTNERED": _("Partnered"),
|
||
"PREVIEW_ENABLED": _("Preview enabled"),
|
||
"PUBLIC_DISABLED": _("Public disabled"),
|
||
"VANITY_URL": _("Vanity URL"),
|
||
"VERIFIED": _("Verified"),
|
||
"VIP_REGIONS": _("VIP Voice Servers"),
|
||
"WELCOME_SCREEN_ENABLED": _("Welcome Screen enabled"),
|
||
}
|
||
guild_features_list = [
|
||
f"\N{WHITE HEAVY CHECK MARK} {name}"
|
||
for feature, name in features.items()
|
||
if feature in guild.features
|
||
]
|
||
|
||
joined_on = _(
|
||
"{bot_name} joined this server on {bot_join}. That's over {since_join} days ago!"
|
||
).format(
|
||
bot_name=ctx.bot.user.name,
|
||
bot_join=guild.me.joined_at.strftime("%d %b %Y %H:%M:%S"),
|
||
since_join=humanize_number((ctx.message.created_at - guild.me.joined_at).days),
|
||
)
|
||
|
||
data = discord.Embed(
|
||
description=(f"{guild.description}\n\n" if guild.description else "") + created_at,
|
||
colour=await ctx.embed_colour(),
|
||
)
|
||
data.set_author(
|
||
name=guild.name,
|
||
icon_url="https://cdn.discordapp.com/emojis/457879292152381443.png"
|
||
if "VERIFIED" in guild.features
|
||
else "https://cdn.discordapp.com/emojis/508929941610430464.png"
|
||
if "PARTNERED" in guild.features
|
||
else discord.Embed.Empty,
|
||
)
|
||
if guild.icon_url:
|
||
data.set_thumbnail(url=guild.icon_url)
|
||
data.add_field(name=_("Members:"), value=member_msg)
|
||
data.add_field(
|
||
name=_("Channels:"),
|
||
value=_(
|
||
"\N{SPEECH BALLOON} Text: {text}\n"
|
||
"\N{SPEAKER WITH THREE SOUND WAVES} Voice: {voice}"
|
||
).format(text=bold(text_channels), voice=bold(voice_channels)),
|
||
)
|
||
data.add_field(
|
||
name=_("Utility:"),
|
||
value=_(
|
||
"Owner: {owner}\nVoice region: {region}\nVerif. level: {verif}\nServer ID: {id}{shard_info}"
|
||
).format(
|
||
owner=bold(str(guild.owner)),
|
||
region=f"**{vc_regions.get(str(guild.region)) or str(guild.region)}**",
|
||
verif=bold(verif[str(guild.verification_level)]),
|
||
id=bold(str(guild.id)),
|
||
shard_info=shard_info,
|
||
),
|
||
inline=False,
|
||
)
|
||
data.add_field(
|
||
name=_("Misc:"),
|
||
value=_(
|
||
"AFK channel: {afk_chan}\nAFK timeout: {afk_timeout}\nCustom emojis: {emoji_count}\nRoles: {role_count}"
|
||
).format(
|
||
afk_chan=bold(str(guild.afk_channel))
|
||
if guild.afk_channel
|
||
else bold(_("Not set")),
|
||
afk_timeout=bold(humanize_timedelta(seconds=guild.afk_timeout)),
|
||
emoji_count=bold(humanize_number(len(guild.emojis))),
|
||
role_count=bold(humanize_number(len(guild.roles))),
|
||
),
|
||
inline=False,
|
||
)
|
||
if guild_features_list:
|
||
data.add_field(name=_("Server features:"), value="\n".join(guild_features_list))
|
||
if guild.premium_tier != 0:
|
||
nitro_boost = _(
|
||
"Tier {boostlevel} with {nitroboosters} boosts\n"
|
||
"File size limit: {filelimit}\n"
|
||
"Emoji limit: {emojis_limit}\n"
|
||
"VCs max bitrate: {bitrate}"
|
||
).format(
|
||
boostlevel=bold(str(guild.premium_tier)),
|
||
nitroboosters=bold(humanize_number(guild.premium_subscription_count)),
|
||
filelimit=bold(_size(guild.filesize_limit)),
|
||
emojis_limit=bold(str(guild.emoji_limit)),
|
||
bitrate=bold(_bitsize(guild.bitrate_limit)),
|
||
)
|
||
data.add_field(name=_("Nitro Boost:"), value=nitro_boost)
|
||
if guild.splash:
|
||
data.set_image(url=guild.splash_url_as(format="png"))
|
||
data.set_footer(text=joined_on)
|
||
|
||
await ctx.send(embed=data)
|
||
|
||
@commands.command()
|
||
async def urban(self, ctx, *, word):
|
||
"""Search the Urban Dictionary.
|
||
|
||
This uses the unofficial Urban Dictionary API.
|
||
"""
|
||
|
||
try:
|
||
url = "https://api.urbandictionary.com/v0/define"
|
||
|
||
params = {"term": str(word).lower()}
|
||
|
||
headers = {"content-type": "application/json"}
|
||
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.get(url, headers=headers, params=params) as response:
|
||
data = await response.json()
|
||
|
||
except aiohttp.ClientError:
|
||
await ctx.send(
|
||
_("No Urban Dictionary entries were found, or there was an error in the process.")
|
||
)
|
||
return
|
||
|
||
if data.get("error") != 404:
|
||
if not data.get("list"):
|
||
return await ctx.send(_("No Urban Dictionary entries were found."))
|
||
if await ctx.embed_requested():
|
||
# a list of embeds
|
||
embeds = []
|
||
for ud in data["list"]:
|
||
embed = discord.Embed()
|
||
title = _("{word} by {author}").format(
|
||
word=ud["word"].capitalize(), author=ud["author"]
|
||
)
|
||
if len(title) > 256:
|
||
title = "{}...".format(title[:253])
|
||
embed.title = title
|
||
embed.url = ud["permalink"]
|
||
|
||
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||
if len(description) > 2048:
|
||
description = "{}...".format(description[:2045])
|
||
embed.description = description
|
||
|
||
embed.set_footer(
|
||
text=_(
|
||
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
|
||
).format(**ud)
|
||
)
|
||
embeds.append(embed)
|
||
|
||
if embeds is not None and len(embeds) > 0:
|
||
await menu(
|
||
ctx,
|
||
pages=embeds,
|
||
controls=DEFAULT_CONTROLS,
|
||
message=None,
|
||
page=0,
|
||
timeout=30,
|
||
)
|
||
else:
|
||
messages = []
|
||
for ud in data["list"]:
|
||
ud.setdefault("example", "N/A")
|
||
message = _(
|
||
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
|
||
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
|
||
).format(word=ud.pop("word").capitalize(), description="{description}", **ud)
|
||
max_desc_len = 2000 - len(message)
|
||
|
||
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||
if len(description) > max_desc_len:
|
||
description = "{}...".format(description[: max_desc_len - 3])
|
||
|
||
message = message.format(description=description)
|
||
messages.append(message)
|
||
|
||
if messages is not None and len(messages) > 0:
|
||
await menu(
|
||
ctx,
|
||
pages=messages,
|
||
controls=DEFAULT_CONTROLS,
|
||
message=None,
|
||
page=0,
|
||
timeout=30,
|
||
)
|
||
else:
|
||
await ctx.send(
|
||
_("No Urban Dictionary entries were found, or there was an error in the process.")
|
||
)
|