diff --git a/checks.py b/checks.py new file mode 100644 index 0000000..6da5d9d --- /dev/null +++ b/checks.py @@ -0,0 +1,69 @@ +import discord + +from discord.ext import commands + +server_liaison = 769659653096472634 +event_host = 769659653096472629 +server_banned = 769659653096472636 +senior_admin = 769659653129896016 +admin = 769659653121900553 +master_builder = 769659653121900550 +reports_channel_id = 769659654791233585 +archived_reports_channel_id = 769659655033978900 +guild_id = 769659653096472627 +mentions_channel_id = 769659654027739151 +discord_admin = 769659653129896025 +discord_mod = 769659653129896023 +devs = [114348811995840515, 147765181903011840] + +class no_permission(commands.MissingPermissions): + pass + +def is_staff(): + def predicate(ctx): + user = ctx.message.author + for role in user.roles: + if role.id in [admin, senior_admin]: + return True + else: + raise no_permission(['IS_STAFF_MEMBER']) + return commands.check(predicate) + +def is_dev(): + def predicate(ctx): + user = ctx.message.author + if user.id in devs: + return True + else: + raise no_permission(['BOT_DEVELOPER']) + return commands.check(predicate) + +def is_mod_or_has_perms(**permissions): + def predicate(ctx): + user = ctx.message.author + for role in user.roles: + if role.id in [discord_mod, discord_admin] or permissions: + return True + else: + raise no_permission(['IS_MOD_OR_HAS_PERMS']) + return commands.check(predicate) + +def is_liaison(): + def predicate(ctx): + user = ctx.message.author + for role in user.roles: + if role.id == server_liaison: + return True + else: + raise no_permission(['IS_SERVER_LIAISON']) + return commands.check(predicate) + +def is_senior(): + def predicate(ctx): + user = ctx.message.author + for role in user.roles: + if role.id == senior_admin: + return True + else: + raise no_permission(['IS_SENIOR_ADMIN']) + return commands.check(predicate) diff --git a/commands/Miscellaneous.py b/commands/Miscellaneous.py new file mode 100644 index 0000000..4d85933 --- /dev/null +++ b/commands/Miscellaneous.py @@ -0,0 +1,34 @@ +import discord +import asyncio + +from discord.ext import commands +from checks import * + +class Miscellaneous(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @is_dev() + @commands.command(pass_context=True) + async def killbot(self, ctx): + em = discord.Embed() + em.description = 'Bot offline.' + await ctx.send(embed=em) + await self.bot.logout() + + @is_dev() + @commands.command(name='debug') + async def debug(self, ctx, *, cmd): + 'Executes a line of code' + try: + result = eval(cmd) + if asyncio.iscoroutine(result): + result = await result + await ctx.send(f'''```py +{result}```''') + except Exception as e: + await ctx.send(f'''```py +{type(e).__name__}: {e}```''') + +def setup(bot): + bot.add_cog(Miscellaneous(bot)) \ No newline at end of file diff --git a/commands/Moderation.py b/commands/Moderation.py new file mode 100644 index 0000000..7b997cc --- /dev/null +++ b/commands/Moderation.py @@ -0,0 +1,79 @@ +import discord +import datetime + +from discord.ext import commands +from checks import * + +muted_role_id = 769659653121900546 + +class Moderation(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.moderator_role_id = 769659653129896023 + + @commands.command() + @commands.has_permissions(kick_members=True) + async def kick(self, ctx, user: discord.Member, *, reason="No reason specified"): + """Kicks a user from the guild.""" + await user.kick(reason=f'{reason}** **by: {ctx.author.name}') + await ctx.send(embed=discord.Embed(embed=f'{user.name} has been kicked by: {ctx.author.name} for reason: {reason}')) + print(f"[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Moderation] Kicked {user.name} from {ctx.guild.name}") + + @commands.command(aliases=['gtfo']) + @commands.has_permissions(ban_members=True) + async def ban(self, ctx, user: discord.Member, *, reason="No reason specified"): + """Bans a user from the guild.""" + await user.ban(reason=f'{reason} || by: {ctx.author.name}', delete_message_days=0) + await ctx.send(embed=discord.Embed(description=f'{user.name} has been banned by: {ctx.author.name} for reason: {reason}')) + print(f"[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Moderation] Banned {user.name} from {ctx.guild.name}") + + @commands.command() + @commands.has_permissions(ban_members=True) + async def unban(self, ctx, user: discord.User, *, reason="No reason specified"): + """Unbans a user from the guild.""" + await ctx.guild.unban(user, reason=f'{reason} || by: {ctx.author.name}') + await ctx.send(embed=discord.Embed(description=f'{user.name} has been unbanned by: {ctx.author.name} for reason: {reason}')) + print(f"[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Moderation] Banned {user.name} from {ctx.guild.name}") + + @commands.command(aliases=['massdelete','purge']) + @commands.has_permissions(manage_messages=True) + async def prune(self, ctx, msgs): + """Purge messages from a channel.""" + channel = ctx.channel + await channel.purge(limit=(int(msgs) + 1)) + await ctx.send(embed=discord.Embed(description=f'{ctx.author.name} deleted {msgs} messages')) + print(f'[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Moderation] {ctx.author.name} purged {msgs} messages in {ctx.guild.name}') + + @commands.command(aliases=['stfu']) + @commands.has_permissions(manage_messages=True) + async def mute(self, ctx, member: discord.Member, *, reason=''): + """Mutes a member of the server.""" + muted_role = ctx.guild.get_role(muted_role_id) + await member.add_roles(muted_role, reason = f'{reason} || by {ctx.author.name}') + if reason == '': + reason = 'No reason specified' + await ctx.send(embed=discord.Embed(description=f'{member} muted by: {ctx.author.name} for: {reason}')) + print(f'[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Moderation] Muted {member} in {ctx.guild.name}') + + @commands.command() + @commands.has_permissions(manage_messages=True) + async def unmute(self, ctx, member: discord.Member, *, reason=''): + """Unmutes a member of the server.""" + muted_role = ctx.guild.get_role(muted_role_id) + await member.remove_roles(muted_role, reason = f'{reason} || by {ctx.author.name}') + await ctx.send(embed=discord.Embed(description=f'{member} unmuted by {ctx.author.name}')) + print(f'[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Moderation] Unmuted {member} in {ctx.guild.name}') + + @commands.command() + @commands.has_permissions(manage_roles=True) + async def setreaction(self, ctx, role : discord.Role=None, msg : discord.Message=None, emoji=None): + if role and msg and emoji : + await msg.add_reaction(emoji) + self.bot.reaction_roles.append((role.id,msg.id,str(emoji.encode('utf-8')))) + + async with aiofiles.open("reactionroles.txt", mode='a') as file: + emoji_utf = emoji.encode('utf-8') + await file.write(f'{role.id} {msg.id} {emoji_utf}\n') + +def setup(bot): + bot.add_cog(Moderation(bot)) \ No newline at end of file diff --git a/commands/Music.py b/commands/Music.py new file mode 100644 index 0000000..c95d79e --- /dev/null +++ b/commands/Music.py @@ -0,0 +1,46 @@ +import discord +from discord.ext import commands +import datetime +import youtube_dl +import os + +class Music(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command() + async def join(self, ctx): + bot = ctx.bot + vc = ctx.author.voice.channel + voiceClient = discord.utils.get(bot.voice_clients, guild=ctx.guild) + if voiceClient and voiceClient.is_connected(): + await voiceClient.move_to(vc) + print(f"[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Music] The bot has moved to {vc} in {ctx.guild.name}\n") + await ctx.send(f'Joined `{vc.name}`') + else: + await vc.connect() + print(f"[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Music] The bot has connected to {vc} in {ctx.guild.name}\n") + await ctx.send(f'Joined `{vc.name}`') + + @commands.command() + async def leave(self, ctx): + bot = ctx.bot + vc = ctx.author.voice.channel + voiceClient = discord.utils.get(bot.voice_clients, guild=ctx.guild) + + if voiceClient and voiceClient.is_connected(): + await voiceClient.disconnect() + await ctx.send(f'Left `{vc.name}`') + print(f"[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Music] The bot has disconnected from {vc.name} in {ctx.guild.name}\n") + else: + await ctx.send(f"`{ctx.author.name}` you fat retard i'm not connected to a vc") + print(f'[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Music] {ctx.author} failed running: {ctx.message.content} in guild: {ctx.guild.name}') + + @commands.command(pass_context=True, aliases=['p']) + async def play(self, ctx, song): + 'Not yet working' + pass + + +def setup(bot): + bot.add_cog(Music(bot)) \ No newline at end of file diff --git a/commands/Server Commands.py b/commands/Server Commands.py new file mode 100644 index 0000000..1799adc --- /dev/null +++ b/commands/Server Commands.py @@ -0,0 +1,166 @@ +import discord + +from checks import * +from discord.ext import commands +from datetime import datetime +import requests +from functions import fix_reports, format_list_entry + +class ServerCommands(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command() + @is_liaison() + async def eventhost(self, ctx, user: discord.Member): + 'Add or remove event host role - liaison only' + eventhostrole = ctx.guild.get_role(event_host) + if eventhostrole in user.roles: + await user.remove_roles(eventhostrole) + await ctx.send(f'```Succesfully took Event Host from {user.name}```') + else: + await user.add_roles(eventhostrole) + await ctx.send(f'```Succesfully added Event Host to {user.name}```') + + @commands.command() + @is_staff() + async def serverban(self, ctx, user: discord.Member): + 'Add or remove server banned role' + serverbannedrole = ctx.guild.get_role(server_banned) + if serverbannedrole in user.roles: + await user.remove_roles(serverbannedrole) + await ctx.send(f'Took Server Banned role from {user.name}') + else: + await user.add_roles(serverbannedrole) + await ctx.send(f'Added Server Banned role to {user.name}') + + @commands.command() + @is_staff() + async def start(self, ctx): + 'Not currently working' + startEmbed = discord.Embed(description='start working out fatass') + await ctx.send(embed=startEmbed) + + @commands.command() + @is_staff() + async def stop(self, ctx): + 'Not currently working' + stopEmbed = discord.Embed(description='stop being so sus') + await ctx.send(embed=stopEmbed) + + @commands.command() + @is_senior() + async def kill(self, ctx): + 'Not currently working' + killEmbed = discord.Embed(description='kill youself') + await ctx.send(embed=killEmbed) + + @commands.command() + @is_staff() + async def restart(self, ctx): + 'Not currently working' + restartEmbed = discord.Embed(description='cant restart a dead server idiot') + await ctx.send(embed=restartEmbed) + + @commands.command() + @is_senior() + async def console(self, ctx,*, command): + 'Not currently working' + await ctx.send(f'```:[{str(datetime.utcnow().replace(microsecond=0))[11:]} INFO]: {ctx.author.name} issued server command: /{command}```') + + + @commands.command(aliases=['status']) + async def state(self, ctx): + 'Gets the current status of the Server' + em = discord.Embed() + try: + json = requests.get("http://play.totalfreedom.me:28966/list?json=true").json() + em.description = 'Server is online' + except ConnectionError: + em.description = 'Server is offline' + await ctx.send(embed=em) + + @commands.command() + async def list(self, ctx): + 'Gives a list of online players.' + em = discord.Embed() + em.title = "Player List" + try: + json = requests.get("http://play.totalfreedom.me:28966/list?json=true").json() + except ConnectionError: + em.description = 'Server is offline' + else: + if json["online"] == 0: + em.description = "There are no online players" + else: + em.description = "There are {} / {} online players".format(json["online"], json["max"]) + owners = json["owners"] + if len(owners) != 0: + em = format_list_entry(em, owners, "Server Owners") + executives = json["executives"] + if len(executives) != 0: + em = format_list_entry(em, executives, "Executives") + developers = json["developers"] + if len(developers) != 0: + em = format_list_entry(em, developers, "Developers") + senior_admins = json["senioradmins"] + if len(senior_admins) != 0: + em = format_list_entry(em, senior_admins, "Senior Admins") + admins = json["admins"] + if len(admins) != 0: + em = format_list_entry(em, admins, "Admins") + #trialadmins = json["trialadmins"] + #if len(trialadmins) != 0: + #em = format_list_entry(em, trialmods, "Trial Mods") + masterbuilders = json["masterbuilders"] + if len(masterbuilders) != 0: + em = format_list_entry(em, masterbuilders, "Master Builders") + operators = json["operators"] + if len(operators) != 0: + em = format_list_entry(em, operators, "Operators") + imposters = json["imposters"] + if len(imposters) != 0: + em = format_list_entry(em, imposters, "Imposters") + await ctx.send(embed=em) + @commands.command() + async def ip(self, ctx): + 'Returns the server IP' + await ctx.send('play.totalfreedom.me') + #pass # discordSRV responds already. + + @commands.command() + @is_staff() + async def archivereports(self, ctx): + """Archive all in-game reports older than 24 hours""" + count = 0 + reports_channel = self.bot.get_channel(reports_channel_id) + archived_reports_channel = self.bot.get_channel(archived_reports_channel_id) + await ctx.channel.trigger_typing() + async for report in reports_channel.history(limit=100): + try: + embed = report.embeds[0] + except: + await report.delete() + time = embed.timestamp + difference = datetime.now() - time + if difference.days >= 0: + await report.delete() + await archived_reports_channel.send("Message archived because it is older than 24 hours", embed=embed) + count += 1 + await ctx.send("Archived **{}** reports that are older than 24 hours".format(count)) + + @commands.command() + @is_mod_or_has_perms() + async def fixreports(self, ctx): + await ctx.channel.trigger_typing() + reports_channel = self.bot.get_channel(reports_channel_id) + messages = await reports_channel.history(limit=500).flatten() + fixed = 0 + for message in messages: + if len(message.reactions) == 0 and message.author == message.guild.me: + await message.add_reaction(clipboard) + fixed += 1 + await ctx.send(f'Fixed **{fixed}** reports') + +def setup(bot): + bot.add_cog(ServerCommands(bot)) \ No newline at end of file diff --git a/commands/help.py b/commands/help.py new file mode 100644 index 0000000..e74ae70 --- /dev/null +++ b/commands/help.py @@ -0,0 +1,53 @@ +import math +import discord + +from discord.ext import commands +from functions import get_avatar + +class Help(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.bot.remove_command('help') + + @commands.command(aliases=['h','?']) + async def help(self, ctx, page=1): + 'Displays the help command' + em = discord.Embed() + em.title = 'Help Command' + command_list = '' + + cog_list = [c for c in self.bot.cogs.keys()] + + page_count = math.ceil(len(cog_list) / 4) + + page = int(page) + if page > page_count or page<1: + await ctx.send(f'Page number \'{page}\' not found.') + return + + cogs_needed = [] + for i in range(4): + x = i + (int(page) - 1) * 4 + try: + cogs_needed.append(cog_list[x]) + except IndexError: + pass + + for cog in cogs_needed: + command_list = '' + for command in self.bot.get_cog(cog).walk_commands(): + if command.hidden: + continue + if command.parent: + continue + + command_list += f'**tf!{command.name}** - {command.help}\n' + command_list += '\n' + + em.add_field(name=cog, value=command_list, inline=False) + + em.set_footer(text=f'Requested by {ctx.message.author}', icon_url=get_avatar(ctx.message.author)) + await ctx.send(embed=em) + +def setup(bot): + bot.add_cog(Help(bot)) \ No newline at end of file diff --git a/functions.py b/functions.py new file mode 100644 index 0000000..a0d3282 --- /dev/null +++ b/functions.py @@ -0,0 +1,27 @@ +import discord + +from discord.ext import commands +from checks import * + +def format_list_entry(embed, list, name): + embed.add_field(name="{} ({})".format(name, len(list)), value=", ".join(list), inline=False) + return embed + +async def fix_reports(): + reports_channel = bot.get_channel(reports_channel_id) + messages = await reports_channel.history(limit=500).flatten() + fixed = 0 + for message in messages: + if len(message.reactions) == 0 and message.author == message.guild.me: + await message.add_reaction(clipboard) + fixed += 1 + return fixed + +def get_avatar(user, animate=True): + if user.avatar_url: + avatar = str(user.avatar_url).replace(".webp", ".png") + else: + avatar = str(user.default_avatar_url) + if not animate: + avatar = avatar.replace(".gif", ".png") + return avatar \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..b7acd1b --- /dev/null +++ b/main.py @@ -0,0 +1,210 @@ +import ast +import discord +import datetime +import os +import time +import random +import aiofiles +import re + +from unicode import * +from discord.ext import commands +from dotenv import load_dotenv +from checks import * +from functions import * + +load_dotenv() +botToken = os.getenv('botToken') + +intents = discord.Intents.all() +bot = commands.Bot(command_prefix=os.getenv('prefix'), description='TotalFreedom bot help command', intents=intents) + +extensions = [ + "commands.Moderation", + "commands.Server Commands", + "commands.help", + "commands.Miscellaneous" +] + + + +if __name__ == '__main__': + for extension in extensions: + try: + bot.load_extension(extension) + print(f"[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Extensions] {extension} loaded successfully") + except Exception as e: + print("[{} INFO]: [Extensions] {} didn't load {}".format(datetime.datetime.utcnow().replace(microsecond=0), extension, e)) + + +@bot.event +async def on_ready(): + bot.reaction_roles = [] + + for file in ['reactionroles.txt']: + async with aiofiles.open(file, mode='a') as temp: + pass + async with aiofiles.open('reactionroles.txt', mode='r') as file: + lines = await file.readlines() + for line in lines: + data = line.split(' ') + bot.reaction_roles.append((int(data[0]), int(data[1]), data[2].strip('\n'))) + + print(f'[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Client] {bot.user.name} is online.') + game = discord.Game('play.totalfreedom.me') + await bot.change_presence(status=discord.Status.online, activity=game) + + guildCount = len(bot.guilds) + print(f'[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Guilds] Bot currently in {guildCount} guilds.') + for guild in bot.guilds: + print(f'[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Guilds] Connected to guild: {guild.name}, Owner: {guild.owner}') + global starttime + starttime = datetime.datetime.utcnow() + +def did_mention_other_user(users, author): + for user in users: + if user is not author: + return True + return False + +def removed_user_mentions(old, new): + users = [] + for user in old: + if user not in new: + users.append(user) + return users + +def removed_role_mentions(old, new): + roles = [] + for role in old: + if role not in new: + roles.append(role) + return roles + +def get_avatar(user, animate=True): + if user.avatar_url: + avatar = str(user.avatar_url).replace(".webp", ".png") + else: + avatar = str(user.default_avatar_url) + if not animate: + avatar = avatar.replace(".gif", ".png") + return avatar + +@bot.event +async def on_message(message): + if message.guild and message.author is message.guild.me and message.channel.id == reports_channel_id: + await message.add_reaction(clipboard) + if message.type == discord.MessageType.new_member: + if re.search('discord\.gg\/[a-zA-z0-9\-]{1,16}', message.author.name.lower()) or re.search('discordapp\.com\/invite\/[a-z0-9]+/ig', message.author.name.lower()): + await message.author.ban(reason="Name is an invite link.") + await message.delete() + bypass_roles = [discord_admin, discord_mod] + bypass = False + for role in message.author.roles: + if role.id in bypass_roles: + bypass = True + if not bypass: + if re.search('discord\.gg\/[a-zA-z0-9\-]{1,16}', message.content) or re.search('discordapp\.com\/invite\/[a-z0-9]+/ig', message.content): + await message.delete() + await message.channel.send(f"{message.author.mention} do not post invite links to other discord servers.") + return + await bot.process_commands(message) + +@bot.event +async def on_message_edit(before, after): + if not isinstance(before.author, discord.Member): + return + if before.guild.id != guild_id\ + : + return + users = removed_user_mentions(before.mentions, after.mentions) + roles = removed_role_mentions(before.role_mentions, after.role_mentions) + if users: + users = ", ".join([str(member) for member in users]) + if roles: + roles = ", ".join([role.name for role in roles]) + if not users and not roles: + return + embed = discord.Embed(description="In {}".format(before.channel.mention)) + if users: + embed.add_field(name="Users", value=users, inline=True) + if roles: + embed.add_field(name="Roles", value=roles, inline=True) + embed.color = 0xFF0000 + embed.title = "Message Edit" + embed.set_footer(text=str(before.author), icon_url=get_avatar(before.author)) + channel = before.guild.get_channel(mentions_channel_id) + await channel.send(embed=embed) + +@bot.event +async def on_message_delete(message): + if not isinstance(message.author, discord.Member): + return + if message.guild.id != guild_id: + return + users = None + roles = None + if did_mention_other_user(message.mentions, message.author): + users = ", ".join([str(member) for member in message.mentions]) + if message.role_mentions: + roles = ", ".join([role.name for role in message.role_mentions]) + if not users and not roles: + return + embed = discord.Embed(description="In {}".format(message.channel.mention)) + if users is not None: + embed.add_field(name="Users", value=users, inline=True) + if roles is not None: + embed.add_field(name="Roles", value=roles, inline=True) + embed.color = 0xFF0000 + embed.title = "Message Deletion" + embed.set_footer(text=str(message.author), icon_url=get_avatar(message.author)) + channel = message.guild.get_channel(mentions_channel_id) + await channel.send(embed=embed) + + +@bot.event +async def on_command_error(ctx, error): + await ctx.send('''```py +{}```'''.format(error)) + print(f'[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Commands] {ctx.author} failed running: {ctx.message.content} in guild: {ctx.guild.name}') + +@bot.event +async def on_command_completion(ctx): + print(f'[{datetime.datetime.utcnow().replace(microsecond=0)} INFO]: [Commands] {ctx.author} ran: {ctx.message.content} in guild: {ctx.guild.name}') + +@bot.event +async def on_raw_reaction_add(payload): + if payload.member == bot.user: + pass + else: + for role_id, msg_id, emoji in bot.reaction_roles: + if msg_id == payload.message_id and emoji == str(payload.emoji.name.encode('utf-8')): + await payload.member.add_roles(bot.get_guild(payload.guild_id).get_role(role_id), reason='reaction') + if payload.channel_id == reports_channel_id: + guild = bot.get_guild(guild_id) + reports_channel = bot.get_channel(reports_channel_id) + report = await reports_channel.fetch_message(payload.message_id) + if report.author == guild.me: + if payload.emoji.name == clipboard: + await report.add_reaction(confirm) + await report.add_reaction(cancel) + elif payload.emoji.name == cancel: + await report.clear_reactions() + await report.add_reaction(clipboard) + elif payload.emoji.name == confirm: + embed = report.embeds[0] + archived_reports_channel = bot.get_channel(archived_reports_channel_id) + await report.delete() + await archived_reports_channel.send("Handled by " + guild.get_member(payload.user_id).mention, embed=embed) + +@bot.event +async def on_raw_reaction_remove(payload): + if payload.member == bot.user: + pass + else: + for role_id, msg_id, emoji in bot.reaction_roles: + if msg_id == payload.message_id and emoji == str(payload.emoji.name.encode('utf-8')): + await bot.get_guild(payload.guild_id).get_member(payload.user_id).remove_roles(bot.get_guild(payload.guild_id).get_role(role_id), reason='reaction') + + +bot.run(botToken) \ No newline at end of file diff --git a/reactionroles.txt b/reactionroles.txt new file mode 100644 index 0000000..8f617ab --- /dev/null +++ b/reactionroles.txt @@ -0,0 +1,4 @@ +769659653121900547 769860347469234188 b'\xf0\x9f\x93\xa2' +769659653121900545 769860347469234188 b'\xf0\x9f\xa7\xa9' +769659653096472632 769860347469234188 b'\xf0\x9f\x8f\x98\xef\xb8\x8f' +769925060412309524 769928118566780978 b'\xe2\x9c\x85' \ No newline at end of file diff --git a/unicode.py b/unicode.py new file mode 100644 index 0000000..0813964 --- /dev/null +++ b/unicode.py @@ -0,0 +1,5 @@ +confirm = "✅" + +clipboard = "📋" + +cancel = "❌"