diff --git a/.env b/.env new file mode 100644 index 00000000..20c92145 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +TOKEN=token_goes_here \ No newline at end of file diff --git a/README.md b/README.md index 46ba4f82..f42a7759 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ all on your messaging app of choice. Using threading and optimizations, Unifier has been tested to be able to send up to 33 messages a second through webhooks, so nobody needs to wait to see your messages. Do note that this speed will vary depending on lots of factors. -### Revolt and Guilded support -With [Revolt](https://github.com/UnifierHQ/unifier-revolt) and [Guilded](https://github.com/UnifierHQ/unifier-guilded) -support plugins, you can bring your communities outside of Discord together, or get a break from Discord without losing your -community. +### External platforms support +With support for external platforms, you can bring your communities outside of Discord together, or get a break from Discord +without losing your community. Unifier supports virtually any platform with the help of support Plugins, so the possibilities +are endless. -Support plugins need to be installed to enable support for these platforms. +We've written a platform support Plugin for [Revolt](https://github.com/UnifierHQ/unifier-revolt), so give that a try! ### Moderation commands After you assign bot admins, they can assign moderators, who can help moderate the chat and take action against bad actors. diff --git a/cogs/badge.py b/cogs/badge.py index b9919203..89872f82 100644 --- a/cogs/badge.py +++ b/cogs/badge.py @@ -1,6 +1,6 @@ """ Unifier - A sophisticated Discord bot uniting servers and platforms -Copyright (C) 2024 Green, ItsAsheer +Copyright (C) 2023-present UnifierHQ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -18,18 +18,21 @@ import nextcord from nextcord.ext import commands -from utils import log, restrictions as r +from utils import log, langmgr, restrictions as r from enum import Enum restrictions = r.Restrictions() +language = langmgr.partial() +language.load() class UserRole(Enum): - OWNER = "the instance\'s **owner**" - ADMIN = "the instance\'s **admin**" - MODERATOR = "the instance\'s **moderator**" - TRUSTED = "a **verified user**" - BANNED = "**BANNED**" - USER = "a **user**" + # let values be None until set by langmgr + OWNER = language.get('owner','badge.roles') + ADMIN = language.get('admin','badge.roles') + MODERATOR = language.get('moderator','badge.roles') + TRUSTED = language.get('trusted','badge.roles') + BANNED = language.get('banned','badge.roles') + USER = language.get('user','badge.roles') class Badge(commands.Cog, name=':medal: Badge'): """Badge contains commands that show you your role in Unifier. @@ -37,8 +40,10 @@ class Badge(commands.Cog, name=':medal: Badge'): Developed by Green and ItsAsheer""" def __init__(self, bot): + global language self.bot = bot self.logger = log.buildlogger(self.bot.package, 'badge', self.bot.loglevel) + language = self.bot.langmgr self.embed_colors = { UserRole.OWNER: ( self.bot.colors.greens_hair if self.bot.user.id==1187093090415149056 else self.bot.colors.unifier @@ -51,9 +56,9 @@ def __init__(self, bot): } restrictions.attach_bot(self.bot) - @commands.command() + @commands.command(description=language.desc('badge.badge')) async def badge(self, ctx, *, user=None): - """Shows your Unifier user badge.""" + selector = language.get_selector(ctx) if user: try: user = self.bot.get_user(int(user.replace('<@','',1).replace('>','',1).replace('!','',1))) @@ -63,7 +68,9 @@ async def badge(self, ctx, *, user=None): user = ctx.author user_role = self.get_user_role(user.id) embed = nextcord.Embed( - description=f"<@{user.id}> is {user_role.value}.", + description=selector.fget("easter_egg", values={ + 'mention':f"<@{user.id}>",'role': user_role.value + }), color=self.embed_colors[user_role] ) embed.set_author( @@ -71,16 +78,17 @@ async def badge(self, ctx, *, user=None): icon_url=user.avatar.url if user.avatar else None ) if user_role==UserRole.BANNED: - embed.set_footer(text='L bozo') + embed.set_footer(text=selector.get("easter_egg")) await ctx.send(embed=embed) - @commands.command(hidden=True,aliases=['trust'],description='Verifies a user.') + @commands.command(hidden=True,aliases=['trust'],description=language.desc('badge.verify')) @restrictions.admin() async def verify(self, ctx, action, user: nextcord.User): + selector = language.get_selector(ctx) action = action.lower() if action not in ['add', 'remove']: - return await ctx.send("Invalid action. Please use 'add' or 'remove'.") + return await ctx.send(selector.get('invalid_action')) if action == 'add': if user.id not in self.bot.trusted_group: @@ -95,7 +103,10 @@ async def verify(self, ctx, action, user: nextcord.User): user_role = UserRole.TRUSTED if action == 'add' else UserRole.USER embed = nextcord.Embed( title="Unifier", - description=f"{'Added' if action == 'add' else 'Removed'} user {user.mention} from the trust group.", + description=( + selector.fget('added',values={'user':user.mention}) if action=='add' else + selector.fget('removed',values={'user':user.mention}) + ), color=self.embed_colors[user_role], ) await ctx.send(embed=embed) diff --git a/cogs/bridge.py b/cogs/bridge.py index bcdd18be..36530daa 100644 --- a/cogs/bridge.py +++ b/cogs/bridge.py @@ -1,6 +1,6 @@ """ Unifier - A sophisticated Discord bot uniting servers and platforms -Copyright (C) 2024 Green, ItsAsheer +Copyright (C) 2023-present UnifierHQ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -19,8 +19,6 @@ import nextcord import hashlib import asyncio -import guilded -import revolt from nextcord.ext import commands import traceback import time @@ -33,14 +31,15 @@ import re import ast import math -from io import BytesIO import os -from utils import log, ui, restrictions as r +from utils import log, langmgr, ui, platform_base, restrictions as r import importlib import emoji as pymoji mentions = nextcord.AllowedMentions(everyone=False, roles=False, users=False) restrictions = r.Restrictions() +language = langmgr.partial() +language.load() multisend_logs = [] plugin_data = {} @@ -57,6 +56,7 @@ '\U00002B1C', '\U00002B1B' ] +arrow_unicode = '\U0000250C' def encrypt_string(hash_string): sha_signature = \ @@ -72,7 +72,7 @@ def genid(): def is_room_locked(room, db): try: - if room in db['locked']: + if db['rooms'][room]['meta']['locked']: return True else: return False @@ -151,10 +151,10 @@ def increment(self,count=1): class UnifierBridge: def __init__(self, bot, logger, webhook_cache=None): - self.bot = bot + self.__bot = bot self.bridged = [] self.prs = {} - self.webhook_cache = webhook_cache or WebhookCacheStore(self.bot) + self.webhook_cache = webhook_cache or WebhookCacheStore(self.__bot) self.restored = False self.raidbans = {} self.possible_raid = {} @@ -166,12 +166,17 @@ def __init__(self, bot, logger, webhook_cache=None): self.msg_stats = {} self.msg_stats_reset = datetime.datetime.now().day self.dedupe = {} + self.__room_template = {'rules': [], 'restricted': False, 'locked': False, 'private': False, 'emoji': None, + 'description': None} + + @property + def room_template(self): + return self.__room_template class UnifierMessage: def __init__(self, author_id, guild_id, channel_id, original, copies, external_copies, urls, source, room, external_urls=None, webhook=False, prehook=None, reply=False, external_bridged=False, - reactions=None, - thread=None): + reactions=None, thread=None, reply_v2=False): self.author_id = author_id self.guild_id = guild_id self.channel_id = channel_id @@ -187,6 +192,7 @@ def __init__(self, author_id, guild_id, channel_id, original, copies, external_c self.reply = reply self.external_bridged = external_bridged, self.thread = thread + self.reply_v2 = reply_v2 if not reactions or not type(reactions) is dict: self.reactions = {} else: @@ -250,17 +256,26 @@ async def fetch_external(self, platform: str, guild_id: str): return ExternalReference(guild_id, self.external_copies[platform][str(guild_id)][0], self.external_copies[platform][str(guild_id)][1]) + class RoomForbiddenError(Exception): + pass + + class RoomNotFoundError(Exception): + pass + + class RoomExistsError(Exception): + pass + def add_modlog(self, action_type, user, reason, moderator): t = time.time() try: - self.bot.db['modlogs'][f'{user}'].append({ + self.__bot.db['modlogs'][f'{user}'].append({ 'type': action_type, 'reason': reason, 'time': t, 'mod': moderator }) except: - self.bot.db['modlogs'].update({ + self.__bot.db['modlogs'].update({ f'{user}': [{ 'type': action_type, 'reason': reason, @@ -268,12 +283,12 @@ def add_modlog(self, action_type, user, reason, moderator): 'mod': moderator }] }) - self.bot.db.save_data() + self.__bot.db.save_data() def get_modlogs(self, user): t = time.time() - if not f'{user}' in list(self.bot.db['modlogs'].keys()): + if not f'{user}' in list(self.__bot.db['modlogs'].keys()): return { 'warns': [], 'bans': [] @@ -283,12 +298,12 @@ def get_modlogs(self, user): } actions = { - 'warns': [log for log in self.bot.db['modlogs'][f'{user}'] if log['type'] == 0], - 'bans': [log for log in self.bot.db['modlogs'][f'{user}'] if log['type'] == 1] + 'warns': [log for log in self.__bot.db['modlogs'][f'{user}'] if log['type'] == 0], + 'bans': [log for log in self.__bot.db['modlogs'][f'{user}'] if log['type'] == 1] } actions_recent = { - 'warns': [log for log in self.bot.db['modlogs'][f'{user}'] if log['type'] == 0 and t - log['time'] <= 2592000], - 'bans': [log for log in self.bot.db['modlogs'][f'{user}'] if log['type'] == 1 and t - log['time'] <= 2592000] + 'warns': [log for log in self.__bot.db['modlogs'][f'{user}'] if log['type'] == 0 and t - log['time'] <= 2592000], + 'bans': [log for log in self.__bot.db['modlogs'][f'{user}'] if log['type'] == 1 and t - log['time'] <= 2592000] } return actions, actions_recent @@ -301,18 +316,201 @@ def get_modlogs_count(self, user): 'warns': len(actions_recent['warns']), 'bans': len(actions_recent['bans']) } + def rooms(self) -> list: + return list(self.__bot.db['rooms'].keys()) + + def get_room(self, room) -> dict or None: + """Gets a Unifier room. + This will be moved to UnifierBridge for a future update.""" + try: + return self.__bot.db['rooms'][room] + except: + return None + + def can_manage_room(self, room, user) -> bool: + roominfo = self.get_room(room) + if roominfo['private']: + if user: + if user.id in self.__bot.moderators: + return True + return user.guild.id == roominfo['private_meta']['server'] and user.guild_permissions.manage_guild + else: + return user.id in self.__bot.admins + + def can_join_room(self, room, user) -> bool: + roominfo = self.get_room(room) + if roominfo['private']: + if user: + if user.id in self.__bot.moderators: + return True + return ( + user.guild.id == roominfo['private_meta']['server'] or + user.guild.id in roominfo['private_meta']['allowed'] + ) and user.guild_permissions.manage_channels + else: + return user.guild_permissions.manage_channels + + def update_room(self, room, roominfo): + if not room in self.__bot.db['rooms'].keys(): + raise self.RoomNotFoundError('invalid room') + + self.__bot.db['rooms'][room] = roominfo + self.__bot.db.save_data() + + def create_room(self, room) -> dict: + if room in self.__bot.db['rooms'].keys(): + raise self.RoomExistsError('room already exists') + + self.__bot.db['rooms'].update({room: self.__room_template}) + return self.__room_template + + def delete_room(self, room): + if not room in self.__bot.db['rooms'].keys(): + raise self.RoomNotFoundError('invalid room') + + self.__bot.db['rooms'].pop(room) + self.__bot.db.save_data() + + def get_invite(self, invite) -> dict or None: + try: + return self.__bot.db['invites'][invite] + except: + return None + + def delete_invite(self, invite): + # TODO: Add InviteNotFoundError, like RoomNotFoundError but for invites. + self.__bot.db['invites'].pop(invite) + self.__bot.db.save_data() + + async def accept_invite(self, user, invite): + # TODO: This is incomplete + invite = self.get_invite(invite) + if not invite: + raise ValueError('invalid invite') + roominfo = self.get_room(invite['room']) + if not roominfo: + raise self.RoomNotFoundError('invalid room') + if not roominfo['private']: + self.__bot.db['invites'].pop(invite) + raise RuntimeError('invite leads to a public room, expired') + + async def join_room(self, user, room, webhook_or_channel, platform='discord'): + roominfo = self.get_room(room) + if not roominfo: + raise self.RoomNotFoundError('invalid room') + + if not self.can_join_room(room, user): + raise self.RoomNotFoundError('cannot join room') + + support = None + + if not platform=='discord': + support = self.__bot.platforms[platform] + + if platform=='discord': + guild_id = user.guild.id + else: + guild_id = support.get_id(support.server(user)) + + if roominfo['private']: + if user: + if platform=='discord': + user_id = user.id + else: + user_id = support.get_id(user) + + if not user_id in self.__bot.moderators: + raise ValueError('forbidden') + if not guild_id in roominfo['private_meta']['allowed']: + raise ValueError('forbidden') + + guild_id = str(guild_id) + webhook_id = support.get_id(webhook_or_channel) + + if not platform in roominfo.keys(): + self.__bot.db['rooms'][room].update({platform:{}}) + + if guild_id in self.__bot.db['rooms'][room][platform].keys(): + raise ValueError('already joined') + + self.__bot.db['rooms'][room][platform].update({guild_id: webhook_id}) + self.__bot.db.save_data() + + async def leave_room(self, guild, room, platform='discord'): + roominfo = self.get_room(room) + if not roominfo: + raise ValueError('invalid room') + + support = None + + if not platform == 'discord': + support = self.__bot.platforms[platform] + + if platform == 'discord': + guild_id = guild.id + else: + guild_id = support.get_id(guild) + + guild_id = str(guild_id) + + if not platform in roominfo.keys(): + raise ValueError('not joined') + + if not guild_id in self.__bot.db['rooms'][room][platform].keys(): + raise ValueError('not joined') + + self.__bot.db['rooms'][room][platform].pop(guild_id) + self.__bot.db.save_data() + async def optimize(self): """Optimizes data to avoid having to fetch webhooks. This decreases latency incuded by message bridging prep.""" - for room in self.bot.db['rooms']: - for guild in self.bot.db['rooms'][room]: - if len(self.bot.db['rooms'][room][guild])==1: + for room in self.__bot.db['rooms']: + for guild in self.__bot.db['rooms'][room]['discord']: + if len(self.__bot.db['rooms'][room]['discord'][guild])==1: try: - hook = await self.bot.fetch_webhook(self.bot.db['rooms'][room][guild][0]) + hook = await self.__bot.fetch_webhook(self.__bot.db['rooms'][room]['discord'][guild][0]) except: continue - self.bot.db['rooms'][room][guild].append(hook.channel_id) - self.bot.db.save_data() + self.__bot.db['rooms'][room]['discord'][guild].append(hook.channel_id) + self.__bot.db.save_data() + + async def convert_1(self): + """Converts data structure to be v2.1.0-compatible. + Eliminates the need for a lot of unneeded keys.""" + if not 'rules' in self.__bot.db.keys(): + # conversion is not needed + return + for room in self.__bot.db['rooms']: + self.__bot.db['rooms'][room] = {'meta':{ + 'rules': self.__bot.db['rules'][room], + 'restricted': room in self.__bot.db['restricted'], + 'locked': room in self.__bot.db['locked'], + 'private': False, + 'private_meta': { + 'server': None, + 'allowed': [] + }, + 'emoji': self.__bot.db['roomemoji'][room] if room in self.__bot.db['roomemoji'].keys() else None, + 'description': self.__bot.db['descriptions'][room] if room in self.__bot.db['descriptions'].keys() else None + },'discord': self.__bot.db['rooms'][room]} + if room in self.__bot.db['rooms_revolt'].keys(): + self.__bot.db['rooms'][room].update({'revolt': self.__bot.db['rooms_revolt'][room]}) + if room in self.__bot.db['rooms_guilded'].keys(): + self.__bot.db['rooms'][room].update({'guilded': self.__bot.db['rooms_guilded'][room]}) + + self.__bot.db.pop('rooms_revolt') + self.__bot.db.pop('rooms_guilded') + self.__bot.db.pop('rules') + self.__bot.db.pop('restricted') + self.__bot.db.pop('locked') + self.__bot.db.pop('roomemoji') + self.__bot.db.pop('descriptions') + + # not sure what to do about the data stored in rooms_revolt key now... + # maybe delete the key entirely? or keep it in case conversion went wrong? + + self.__bot.db.save_data() def is_raidban(self,userid): try: @@ -352,11 +550,11 @@ async def backup(self,filename='bridge.json',limit=10000): code = self.prs[pr_ids[limit - index - 1]] data['posts'].update({pr_ids[limit - index - 1]: code}) - if self.bot.config['compress_cache']: - await self.bot.loop.run_in_executor(None, lambda: compress_json.dump(data,filename+'.lzma')) + if self.__bot.config['compress_cache']: + await self.__bot.loop.run_in_executor(None, lambda: compress_json.dump(data,filename+'.lzma')) else: with open(filename, "w+") as file: - await self.bot.loop.run_in_executor(None, lambda: json.dump(data, file)) + await self.__bot.loop.run_in_executor(None, lambda: json.dump(data, file)) del data self.backup_running = False return @@ -364,7 +562,7 @@ async def backup(self,filename='bridge.json',limit=10000): async def restore(self,filename='bridge.json'): if self.restored: raise RuntimeError('Already restored from backup') - if self.bot.config['compress_cache']: + if self.__bot.config['compress_cache']: data = compress_json.load(filename+'.lzma') else: with open(filename, "r") as file: @@ -384,7 +582,8 @@ async def restore(self,filename='bridge.json'): external_urls=data['messages'][f'{x}']['external_urls'], webhook=data['messages'][f'{x}']['webhook'], prehook=data['messages'][f'{x}']['prehook'], - reactions=data['messages'][f'{x}']['reactions'] if 'reactions' in list(data['messages'][f'{x}'].keys()) else {} + reactions=data['messages'][f'{x}']['reactions'] if 'reactions' in list(data['messages'][f'{x}'].keys()) else {}, + reply_v2=data['messages'][f'{x}']['reply_v2'] if 'reply_v2' in list(data['messages'][f'{x}'].keys()) else False ) self.bridged.append(msg) @@ -397,8 +596,8 @@ async def run_security(self, message): responses = {} unsafe = False - for plugin in self.bot.loaded_plugins: - script = self.bot.loaded_plugins[plugin] + for plugin in self.__bot.loaded_plugins: + script = self.__bot.loaded_plugins[plugin] try: data = plugin_data[plugin] @@ -435,9 +634,9 @@ async def run_stylizing(self, message): return message async def find_thread(self,thread_id): - for thread in self.bot.db['threads']: - if int(thread)==thread_id or int(thread_id) in self.bot.db['threads'][thread].values(): - return {thread: self.bot.db['threads'][thread]} + for thread in self.__bot.db['threads']: + if int(thread)==thread_id or int(thread_id) in self.__bot.db['threads'][thread].values(): + return {thread: self.__bot.db['threads'][thread]} return None async def fetch_message(self,message_id,prehook=False,not_prehook=False): @@ -480,52 +679,56 @@ async def merge_prehook(self,message_id): self.bridged.pop(index_tomerge) async def add_exp(self, user_id): - if not self.bot.config['enable_exp'] or user_id==self.bot.user.id: + if not self.__bot.config['enable_exp'] or user_id==self.__bot.user.id: return 0, False - if not f'{user_id}' in self.bot.db['exp'].keys(): - self.bot.db['exp'].update({f'{user_id}':{'experience':0,'level':1,'progress':0}}) + if not f'{user_id}' in self.__bot.db['exp'].keys(): + self.__bot.db['exp'].update({f'{user_id}':{'experience':0,'level':1,'progress':0}}) t = time.time() if f'{user_id}' in level_cooldown.keys(): if t < level_cooldown[f'{user_id}']: - return self.bot.db['exp'][f'{user_id}']['experience'], self.bot.db['exp'][f'{user_id}']['progress'] >= 1 + return self.__bot.db['exp'][f'{user_id}']['experience'], self.__bot.db['exp'][f'{user_id}']['progress'] >= 1 else: - level_cooldown[f'{user_id}'] = round(time.time()) + self.bot.config['exp_cooldown'] + level_cooldown[f'{user_id}'] = round(time.time()) + self.__bot.config['exp_cooldown'] else: - level_cooldown.update({f'{user_id}': round(time.time()) + self.bot.config['exp_cooldown']}) - self.bot.db['exp'][f'{user_id}']['experience'] += random.randint(80,120) + level_cooldown.update({f'{user_id}': round(time.time()) + self.__bot.config['exp_cooldown']}) + self.__bot.db['exp'][f'{user_id}']['experience'] += random.randint(80,120) ratio, remaining = await self.progression(user_id) if ratio >= 1: - self.bot.db['exp'][f'{user_id}']['experience'] = -remaining - self.bot.db['exp'][f'{user_id}']['level'] += 1 + self.__bot.db['exp'][f'{user_id}']['experience'] = -remaining + self.__bot.db['exp'][f'{user_id}']['level'] += 1 newratio, _remaining = await self.progression(user_id) else: newratio = ratio - self.bot.db['exp'][f'{user_id}']['progress'] = newratio - await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) - return self.bot.db['exp'][f'{user_id}']['experience'], ratio >= 1 + self.__bot.db['exp'][f'{user_id}']['progress'] = newratio + await self.__bot.loop.run_in_executor(None, lambda: self.__bot.db.save_data()) + return self.__bot.db['exp'][f'{user_id}']['experience'], ratio >= 1 async def progression(self, user_id): base = 1000 rate = 1.4 - target = base * (rate ** self.bot.db['exp'][f'{user_id}']['level']) + target = base * (rate ** self.__bot.db['exp'][f'{user_id}']['level']) return ( - self.bot.db['exp'][f'{user_id}']['experience']/target, target-self.bot.db['exp'][f'{user_id}']['experience'] + self.__bot.db['exp'][f'{user_id}']['experience']/target, target-self.__bot.db['exp'][f'{user_id}']['experience'] ) async def roomstats(self, roomname): online = 0 members = 0 guilds = 0 - for guild_id in self.bot.db['rooms'][roomname]: - try: - guild = self.bot.get_guild(int(guild_id)) - online += len(list( - filter(lambda x: (x.status != nextcord.Status.offline and x.status != nextcord.Status.invisible), - guild.members))) - members += len(guild.members) - guilds += 1 - except: - pass + for platform in self.__bot.db['rooms'][roomname]: + for guild_id in self.__bot.db['rooms'][roomname][platform]: + try: + if platform=='revolt': + guild = self.__bot.revolt_client.get_server(int(guild_id)) + else: + guild = self.__bot.get_guild(int(guild_id)) + online += len(list( + filter(lambda x: (x.status != nextcord.Status.offline and x.status != nextcord.Status.invisible), + guild.members))) + members += len(guild.members) + guilds += 1 + except: + pass try: messages = self.msg_stats[roomname] except: @@ -547,18 +750,17 @@ async def dedupe_name(self, username, userid): async def delete_parent(self, message): msg: UnifierBridge.UnifierMessage = await self.fetch_message(message) if msg.source=='discord': - ch = self.bot.get_channel(int(msg.channel_id)) + ch = self.__bot.get_channel(int(msg.channel_id)) todelete = await ch.fetch_message(int(msg.id)) await todelete.delete() - elif msg.source=='guilded': - guild = self.bot.guilded_client.get_server(msg.guild_id) - ch = guild.get_channel(msg.channel_id) - todelete = await ch.fetch_message(msg.id) - await todelete.delete() - elif msg.source=='revolt': - ch = await self.bot.revolt_client.fetch_channel(msg.channel_id) - todelete = await ch.fetch_message(msg.id) - await todelete.delete() + else: + source_support = self.__bot.platforms[msg.source] + try: + ch = source_support.get_channel(msg.channel_id) + except: + ch = await source_support.fetch_channel(msg.channel_id) + todelete = await source_support.fetch_message(ch,msg.id) + await source_support.delete(todelete) async def delete_copies(self, message): msg: UnifierBridge.UnifierMessage = await self.fetch_message(message) @@ -567,20 +769,20 @@ async def delete_copies(self, message): async def delete_discord(msgs): count = 0 threads = [] - for key in list(self.bot.db['rooms'][msg.room].keys()): + for key in list(self.__bot.db['rooms'][msg.room]['discord'].keys()): if not key in list(msgs.keys()): continue - guild = self.bot.get_guild(int(key)) + guild = self.__bot.get_guild(int(key)) try: try: - webhook = self.bot.bridge.webhook_cache.get_webhook([ - f'{self.bot.db["rooms"][msg.room][f"{guild.id}"][0]}' + webhook = self.__bot.bridge.webhook_cache.get_webhook([ + f'{self.__bot.db["rooms"][msg.room]["discord"][f"{guild.id}"][0]}' ]) except: try: - webhook = await self.bot.fetch_webhook(self.bot.db['rooms'][msg.room][key][0]) - self.bot.bridge.webhook_cache.store_webhook(webhook) + webhook = await self.__bot.fetch_webhook(self.__bot.db['rooms'][msg.room]['discord'][key][0]) + self.__bot.bridge.webhook_cache.store_webhook(webhook) except: continue except: @@ -594,66 +796,42 @@ async def delete_discord(msgs): except: # traceback.print_exc() pass - await asyncio.gather(*threads) + try: + await asyncio.gather(*threads) + except: + pass return count - async def delete_guilded(msgs): - if not 'cogs.bridge_guilded' in list(self.bot.extensions.keys()): - return + async def delete_others(msgs, target): count = 0 threads = [] - for key in list(self.bot.db['rooms_guilded'][msg.room].keys()): + support = self.__bot.platforms[target] + for key in list(self.__bot.db['rooms'][msg.room][target].keys()): if not key in list(msgs.keys()): continue - guild = self.bot.guilded_client.get_server(key) - - # Fetch webhook - try: - webhook = await guild.fetch_webhook(self.bot.db['rooms_guilded'][msg.room][key][0]) - except: - continue - + channel = support.get_channel(msgs[key][0]) + todelete = await support.fetch_message(channel, msgs[key][1]) try: threads.append(asyncio.create_task( - webhook.delete_message(msgs[key][1]) + support.delete_message(todelete) )) count += 1 except: - # traceback.print_exc() pass - await asyncio.gather(*threads) - return count - - async def delete_revolt(msgs): - if not 'cogs.bridge_revolt' in list(self.bot.extensions.keys()): - return - count = 0 - for key in list(self.bot.db['rooms_revolt'][msg.room].keys()): - if not key in list(msgs.keys()): - continue - - try: - ch = await self.bot.revolt_client.fetch_channel(msgs[key][0]) - todelete = await ch.fetch_message(msgs[key][1]) - await todelete.delete() - count += 1 - except: - # traceback.print_exc() - continue + try: + await asyncio.gather(*threads) + except: + pass return count if msg.source=='discord': threads.append(asyncio.create_task( delete_discord(msg.copies) )) - elif msg.source=='revolt': - threads.append(asyncio.create_task( - delete_revolt(msg.copies) - )) - elif msg.source=='guilded': + else: threads.append(asyncio.create_task( - delete_guilded(msg.copies) + delete_others(msg.copies,msg.source) )) for platform in list(msg.external_copies.keys()): @@ -661,13 +839,9 @@ async def delete_revolt(msgs): threads.append(asyncio.create_task( delete_discord(msg.external_copies['discord']) )) - elif platform=='revolt': - threads.append(asyncio.create_task( - delete_revolt(msg.external_copies['revolt']) - )) - elif platform=='guilded': + else: threads.append(asyncio.create_task( - delete_guilded(msg.external_copies['guilded']) + delete_others(msg.external_copies[msg.source],msg.source) )) results = await asyncio.gather(*threads) @@ -705,13 +879,13 @@ async def make_friendly(self, text, source): userid = components[offset].split('>', 1)[0] try: if source == 'revolt': - user = self.bot.revolt_client.get_user(userid) + user = self.__bot.revolt_client.get_user(userid) display_name = user.display_name elif source == 'guilded': - user = self.bot.guilded_client.get_user(userid) + user = self.__bot.guilded_client.get_user(userid) display_name = user.display_name else: - user = self.bot.get_user(userid) + user = self.__bot.get_user(userid) display_name = user.global_name if not user: raise ValueError() @@ -737,13 +911,13 @@ async def make_friendly(self, text, source): try: if source == 'revolt': try: - channel = self.bot.revolt_client.get_channel(channelid) + channel = self.__bot.revolt_client.get_channel(channelid) except: - channel = await self.bot.revolt_client.fetch_channel(channelid) + channel = await self.__bot.revolt_client.fetch_channel(channelid) elif source == 'guilded': - channel = self.bot.guilded_client.get_channel(channelid) + channel = self.__bot.guilded_client.get_channel(channelid) else: - channel = self.bot.get_channel(channelid) + channel = self.__bot.get_channel(channelid) if not channel: raise ValueError() except: @@ -793,13 +967,13 @@ async def edit_discord(msgs,friendly=False): else: text = content - for key in list(self.bot.db['rooms'][msg.room].keys()): + for key in list(self.__bot.db['rooms'][msg.room]['discord'].keys()): if not key in list(msgs.keys()): continue # Fetch webhook try: - webhook = await self.bot.fetch_webhook(self.bot.db['rooms'][msg.room][key][0]) + webhook = await self.__bot.fetch_webhook(self.__bot.db['rooms'][msg.room]['discord'][key][0]) except: continue @@ -813,66 +987,39 @@ async def edit_discord(msgs,friendly=False): await asyncio.gather(*threads) - async def edit_revolt(msgs,friendly=False): - if not 'cogs.bridge_revolt' in list(self.bot.extensions.keys()): - return + async def edit_others(msgs,target,friendly=False): + source_support = self.__bot.platforms[msg.source] if msg.source != 'discord' else None + dest_support = self.__bot.platforms[target] if friendly: - text = await self.make_friendly(content, msg.source) + if msg.source == 'discord': + text = await self.make_friendly(content, msg.source) + else: + text = await source_support.make_friendly(content) else: text = content - for key in list(self.bot.db['rooms_revolt'][msg.room].keys()): + for key in list(self.__bot.db['rooms'][msg.room][target].keys()): if not key in list(msgs.keys()): continue try: - ch = await self.bot.revolt_client.fetch_channel(msgs[key][0]) - toedit = await ch.fetch_message(msgs[key][1]) - await toedit.edit(content=text) + try: + ch = dest_support.get_channel(msgs[key][0]) + except: + ch = await dest_support.fetch_channel(msgs[key][0]) + toedit = await dest_support.fetch_message(ch, msgs[key][1]) + await dest_support.edit(toedit, text) except: traceback.print_exc() continue - async def edit_guilded(msgs,friendly=False): - """Guilded does not support editing via webhooks at the moment. - We're just keeping this in case they change this at some point.""" - - threads = [] - if friendly: - text = await self.make_friendly(content, msg.source) - else: - text = content - - for key in list(self.bot.db['rooms_guilded'][msg.room].keys()): - if not key in list(msgs.keys()): - continue - - # Fetch webhook - try: - webhook = await self.bot.guilded_client.fetch_webhook(self.bot.db['rooms_guilded'][msg.room][key][0]) - except: - continue - - try: - toedit = await webhook.fetch_message(msgs[key][1]) - if msg.reply: - text = toedit.content.split('\n',1)[0]+'\n'+text - threads.append(asyncio.create_task( - toedit.edit(content=text) - )) - except: - traceback.print_exc() - pass - - await asyncio.gather(*threads) - if msg.source=='discord': threads.append(asyncio.create_task( edit_discord(msg.copies) )) - elif msg.source=='revolt': + else: threads.append(asyncio.create_task( - edit_revolt(msg.copies) + edit_others(msg.copies, msg.source) )) for platform in list(msg.external_copies.keys()): @@ -880,49 +1027,37 @@ async def edit_guilded(msgs,friendly=False): threads.append(asyncio.create_task( edit_discord(msg.external_copies['discord'],friendly=True) )) - elif platform=='revolt': + else: threads.append(asyncio.create_task( - edit_revolt(msg.external_copies['revolt'],friendly=True) + edit_others(msg.external_copies[platform],platform,friendly=True) )) await asyncio.gather(*threads) - async def send(self, room: str, message: nextcord.Message or revolt.Message, + async def send(self, room: str, message, platform: str = 'discord', system: bool = False, - extbridge=False, id_override=None, ignore=None): - if is_room_locked(room,self.bot.db) and not message.author.id in self.bot.admins: + extbridge=False, id_override=None, ignore=None, source='discord', + content_override=None): + if is_room_locked(room,self.__bot.db) and not message.author.id in self.__bot.admins: return if ignore is None: ignore = [] - source = 'discord' - extlist = list(self.bot.extensions) - if type(message) is revolt.Message: - if not 'cogs.bridge_revolt' in extlist: - raise RuntimeError('Revolt Support not initialized') - source = 'revolt' - if type(message) is guilded.ChatMessage: - if not 'cogs.bridge_guilded' in extlist: - raise RuntimeError('Guilded Support not initialized') - source = 'guilded' - - if platform=='revolt': - if not 'cogs.bridge_revolt' in list(self.bot.extensions.keys()): - return - elif platform=='guilded': - if not 'cogs.bridge_guilded' in list(self.bot.extensions.keys()): - return - elif not platform=='discord': - raise ValueError("Unsupported platform") + selector = language.get_selector('bridge.bridge',userid=message.author.id) - guilds = self.bot.db['rooms'][room] - if platform=='revolt': - guilds = self.bot.db['rooms_revolt'][room] - elif platform=='guilded': - guilds = self.bot.db['rooms_guilded'][room] + source_support = self.__bot.platforms[source] if source != 'discord' else None + dest_support = self.__bot.platforms[platform] if platform != 'discord' else None - is_pr = room == self.bot.config['posts_room'] and ( - self.bot.config['allow_prs'] if 'allow_prs' in list(self.bot.config.keys()) else False or - self.bot.config['allow_posts'] if 'allow_posts' in list(self.bot.config.keys()) else False + if not source in self.__bot.platforms.keys() and not source=='discord': + raise ValueError('invalid platform') + + if not platform in self.__bot.platforms.keys() and not platform=='discord': + raise ValueError('invalid platform') + + guilds = self.__bot.db['rooms'][room][platform] + + is_pr = room == self.__bot.config['posts_room'] and ( + self.__bot.config['allow_prs'] if 'allow_prs' in list(self.__bot.config.keys()) else False or + self.__bot.config['allow_posts'] if 'allow_posts' in list(self.__bot.config.keys()) else False ) is_pr_ref = False pr_id = "" @@ -941,13 +1076,13 @@ async def send(self, room: str, message: nextcord.Message or revolt.Message, is_pr = False # PR ID identification - temp_pr_ref = room == self.bot.config['posts_ref_room'] and ( - self.bot.config['allow_prs'] if 'allow_prs' in list(self.bot.config.keys()) else False or - self.bot.config['allow_posts'] if 'allow_posts' in list(self.bot.config.keys()) else False + temp_pr_ref = room == self.__bot.config['posts_ref_room'] and ( + self.__bot.config['allow_prs'] if 'allow_prs' in list(self.__bot.config.keys()) else False or + self.__bot.config['allow_posts'] if 'allow_posts' in list(self.__bot.config.keys()) else False ) if temp_pr_ref and message.content.startswith('[') and source==platform=='discord' and ( - self.bot.config['allow_prs'] if 'allow_prs' in list(self.bot.config.keys()) else False or - self.bot.config['allow_posts'] if 'allow_posts' in list(self.bot.config.keys()) else False + self.__bot.config['allow_prs'] if 'allow_prs' in list(self.__bot.config.keys()) else False or + self.__bot.config['allow_posts'] if 'allow_posts' in list(self.__bot.config.keys()) else False ): pr_id = None components = message.content.replace('[','',1).split(']') @@ -1001,8 +1136,8 @@ async def send(self, room: str, message: nextcord.Message or revolt.Message, for x in range(index): emoji = nextcord.utils.find( - lambda e: e.name == name and not e.id in skip and e.guild_id in self.bot.db['emojis'], - self.bot.emojis) + lambda e: e.name == name and not e.id in skip and e.guild_id in self.__bot.db['emojis'], + self.__bot.emojis) if emoji == None: failed = True break @@ -1029,45 +1164,48 @@ async def send(self, room: str, message: nextcord.Message or revolt.Message, if should_resend: if not message.channel.permissions_for(message.guild.me).manage_messages: if emojified or is_pr_ref: - await message.channel.send( - 'Parent message could not be deleted. I may be missing the `Manage Messages` permission.') + await message.channel.send(selector.get('delete_fail')) raise SelfDeleteException('Could not delete parent message') elif is_pr: - await message.channel.send(f'Post ID assigned: `{pr_id}`', reference=message) + await message.channel.send(selector.fget('post_id',values={'post_id': pr_id}), reference=message) should_resend = False elif is_pr and source == platform: - if source == 'revolt': - await message.channel.send(f'Post ID assigned: `{pr_id}`', replies=[revolt.MessageReply(message)]) - elif source == 'guilded': - await message.channel.send(f'Post ID assigned: `{pr_id}`', reply_to=[message]) + if not source=='discord': + channel = source_support.channel(message) + await source_support.send(channel, selector.fget('post_id',values={'post_id': pr_id}), reply=message) # Username - if source == 'revolt': - if not message.author.display_name: - author = message.author.name - else: - author = message.author.display_name - elif source == 'guilded': - author = message.author.name - else: + if source == 'discord': author = message.author.global_name if message.author.global_name else message.author.name - if f'{message.author.id}' in list(self.bot.db['nicknames'].keys()): - author = self.bot.db['nicknames'][f'{message.author.id}'] + if f'{message.author.id}' in list(self.__bot.db['nicknames'].keys()): + author = self.__bot.db['nicknames'][f'{message.author.id}'] + else: + author_obj = source_support.author(message) + author = source_support.display_name(author_obj) + if f'{source_support.get_id(author_obj)}' in list(self.__bot.db['nicknames'].keys()): + author = self.__bot.db['nicknames'][f'{source_support.get_id(author_obj)}'] # Get dedupe - dedupe = await self.dedupe_name(author, message.author.id) + if source == 'discord': + author_id = message.author.id + is_bot = message.author.bot + else: + author_id = source_support.get_id(source_support.author(message)) + is_bot = source_support.is_bot(source_support.author(message)) + + dedupe = await self.dedupe_name(author, author_id) should_dedupe = dedupe > -1 # Emoji time useremoji = None - if self.bot.config['enable_emoji_tags'] and not system: + if self.__bot.config['enable_emoji_tags'] and not system: while True: author_split = [*author] if len(author_split) == 1: - if source == 'guilded': - author = 'Moderated username' - else: + if source == 'discord': author = message.author.name + else: + author = source_support.user_name(message.author) break if pymoji.is_emoji(author_split[len(author_split)-1]): author_split.pop(len(author_split)-1) @@ -1077,33 +1215,48 @@ async def send(self, room: str, message: nextcord.Message or revolt.Message, else: break if ( - message.author.id == self.bot.config['owner'] or ( - message.author.id == self.bot.config['owner_external'][source] - if source in self.bot.config['owner_external'].keys() else False + author_id == self.__bot.config['owner'] or ( + author_id == self.__bot.config['owner_external'][source] + if source in self.__bot.config['owner_external'].keys() else False ) ): useremoji = '\U0001F451' - elif message.author.id in self.bot.admins: + elif author_id in self.__bot.admins: useremoji = '\U0001F510' - elif message.author.id in self.bot.moderators: + elif author_id in self.__bot.moderators: useremoji = '\U0001F6E1' - elif message.author.id in self.bot.db['trusted']: + elif author_id in self.__bot.db['trusted']: useremoji = '\U0001F31F' - elif message.author.bot: + elif is_bot: useremoji = '\U0001F916' elif should_dedupe: useremoji = dedupe_emojis[dedupe] + if content_override: + msg_content = content_override + else: + if source=='discord': + msg_content = message.content + else: + msg_content = source_support.content(message) + friendlified = False friendly_content = None if not source == platform: friendlified = True - friendly_content = await self.make_friendly(message.content, source) + if source=='discord': + friendly_content = await self.make_friendly(msg_content, source) + else: + try: + friendly_content = await source_support.make_friendly(msg_content) + except platform_base.MissingImplementation: + friendly_content = msg_content message_ids = {} urls = {} trimmed = '' replying = False + global_reply_v2 = False # Threading thread_urls = {} @@ -1113,76 +1266,89 @@ async def send(self, room: str, message: nextcord.Message or revolt.Message, max_files = 0 # Check attachments size - for attachment in message.attachments: + if source=='discord': + attachments = message.attachments + else: + attachments = source_support.attachments(message) + for attachment in attachments: if system: break - size_total += attachment.size + if source=='discord': + size_total += attachment.size + else: + size_total += source_support.attachment_size(attachment) if size_total > 25000000: - if not self.bot.config['suppress_filesize_warning']: - if source == platform == 'revolt': - await message.channel.send('Your files passed the 25MB limit. Some files will not be sent.', - replies=[revolt.MessageReply(message)]) - elif source == platform == 'guilded': - await message.channel.send('Your files passed the 25MB limit. Some files will not be sent.', - reply_to=message) - elif source == platform: - await message.channel.send('Your files passed the 25MB limit. Some files will not be sent.', + if not self.__bot.config['suppress_filesize_warning'] and source == platform: + if source=='discord': + await message.channel.send(selector.get('filesize_limit'), reference=message) + else: + await source_support.send(source_support.channel(message),selector.get('filesize_limit'), + special={'reply':message}) break max_files += 1 # Broadcast message for guild in list(guilds.keys()): - if source=='revolt' or source=='guilded': - sameguild = (guild == str(message.server.id)) if message.server else False - else: + if source=='discord': + reply_v2 = not (self.__bot.db['settings'][guild]['reply_v2_optout'] if guild in self.__bot.db['settings'].keys() else False) sameguild = (guild == str(message.guild.id)) if message.guild else False + else: + reply_v2 = False + compare_guild = source_support.server(message) + if not compare_guild: + sameguild = False + else: + guild_id = source_support.get_id(compare_guild) + sameguild = (guild == str(guild_id)) try: - bans = self.bot.db['blocked'][str(guild)] + bans = self.__bot.db['blocked'][str(guild)] if source=='discord': guildban = message.guild.id in bans else: - guildban = message.server.id in bans - if (message.author.id in bans or guildban) and not sameguild: + guildban = source_support.server(message) in bans + if (author_id in bans or guildban) and not sameguild: continue except: pass # Destination guild object - destguild = None - if platform == 'discord': - destguild = self.bot.get_guild(int(guild)) - elif platform == 'revolt': - try: - destguild = self.bot.revolt_client.get_server(guild) - except: + destguild = self.__bot.get_guild(int(guild)) + if not destguild: continue - elif platform == 'guilded': + else: try: - destguild = self.bot.guilded_client.get_server(guild) + destguild = dest_support.get_server(guild) + if not destguild: + continue except: continue - if not destguild: - continue - - if destguild.id in ignore: - continue + if platform == 'discord': + if destguild.id in ignore: + continue + else: + if dest_support.get_id(destguild) in ignore: + continue if sameguild and not system: if not should_resend or not platform=='discord': if platform=='discord': urls.update({f'{message.guild.id}':f'https://discord.com/channels/{message.guild.id}/{message.channel.id}/{message.id}'}) - elif platform=='guilded': - urls.update({f'{message.server.id}': message.share_url}) + else: + try: + urls.update({f'{source_support.server(message)}': source_support.url(message)}) + except platform_base.MissingImplementation: + pass continue # Reply processing reply_msg = None components = None pr_actionrow = None + replytext = '' try: if source=='revolt': @@ -1205,7 +1371,8 @@ async def send(self, room: str, message: nextcord.Message or revolt.Message, button_style = nextcord.ButtonStyle.gray if is_pr: pr_actionrow = ui.ActionRow( - nextcord.ui.Button(style=button_style, label=f'Post ID: {pr_id}', + nextcord.ui.Button(style=button_style, + label=selector.fget('post_id',values={'post_id': pr_id}), emoji='\U0001F4AC', disabled=True) ) else: @@ -1218,43 +1385,56 @@ async def send(self, room: str, message: nextcord.Message or revolt.Message, else: try: pr_actionrow = ui.ActionRow( - nextcord.ui.Button(style=nextcord.ButtonStyle.url, label=f'Referencing Post #{pr_id}', + nextcord.ui.Button(style=nextcord.ButtonStyle.url, + label=selector.fget('post_reference',values={'post_id': pr_id}), emoji='\U0001F517',url=await msg.fetch_url(guild)) ) except: pr_actionrow = ui.ActionRow( - nextcord.ui.Button(style=nextcord.ButtonStyle.gray, label=f'Referencing Post #{pr_id}', + nextcord.ui.Button(style=nextcord.ButtonStyle.gray, + label=selector.fget('post_reference',values={'post_id': pr_id}), emoji='\U0001F517', disabled=True) ) if pr_actionrow: components = ui.View() components.add_row(pr_actionrow) if reply_msg: - if True: # message.thread: - # i probably want to research how nextcord threads work first, will come back to this - pass if not trimmed: - is_copy = False try: - if source=='revolt': - content = message.replies[0].content - else: + if source=='discord': content = message.reference.cached_message.content - except: - if source=='revolt': - msg = await message.channel.fetch_message(message.replies[0].id) - elif source=='guilded': - msg = await message.channel.fetch_message(message.replied_to[0].id) - if msg.webhook_id: - is_copy = True else: + # for NUPS, plugins process the content, not unifier + msg = source_support.reply(message) + if type(msg) is str or type(msg) is int: + msg = await source_support.fetch_message( + source_support.channel(message),msg + ) + content = source_support.content(msg) + except: + if source=='discord': msg = await message.channel.fetch_message(message.reference.message_id) + else: + raise content = msg.content - clean_content = nextcord.utils.remove_markdown(content) - if reply_msg.reply and source=='guilded' and is_copy: - clean_content = clean_content.split('\n',1)[1] + if source=='discord': + used_reply_v2 = not (self.__bot.db['settings'][str(message.reference.guild_id)][ + 'reply_v2_optout'] if str(message.reference.guild_id) in self.__bot.db[ + 'settings'].keys() else False) + if reply_msg.reply_v2 and ( + str(message.reference.guild_id) in reply_msg.copies.keys() or + reply_msg.webhook + ) and used_reply_v2: + # remove "replying to" text + content_comp = content.split('\n') + if len(content_comp) == 1: + content = '' + else: + content_comp.pop(0) + content = '\n'.join(content_comp) + clean_content = nextcord.utils.remove_markdown(content) msg_components = clean_content.split('<@') offset = 0 if clean_content.startswith('<@'): @@ -1266,7 +1446,7 @@ async def send(self, room: str, message: nextcord.Message or revolt.Message, except: offset += 1 continue - user = self.bot.get_user(userid) + user = self.__bot.get_user(userid) if user: clean_content = clean_content.replace(f'<@{userid}>', f'@{user.global_name}').replace( @@ -1287,133 +1467,182 @@ async def send(self, room: str, message: nextcord.Message or revolt.Message, button_style = nextcord.ButtonStyle.gray try: - if reply_msg.source=='revolt': - user = self.bot.revolt_client.get_user(reply_msg.author_id) - author_text = f'@{user.display_name or user.name}' - elif reply_msg.source=='guilded': - user = self.bot.guilded_client.get_user(reply_msg.author_id) - author_text = f'@{user.name}' - else: - user = self.bot.get_user(int(reply_msg.author_id)) + if reply_msg.source=='discord': + user = self.__bot.get_user(int(reply_msg.author_id)) author_text = f'@{user.global_name or user.name}' - if f'{reply_msg.author_id}' in list(self.bot.db['nicknames'].keys()): - author_text = '@'+self.bot.db['nicknames'][f'{reply_msg.author_id}'] + else: + user = source_support.get_user(reply_msg.author_id) + author_text = f'@{source_support.display_name(user)}' + if f'{reply_msg.author_id}' in list(self.__bot.db['nicknames'].keys()): + author_text = '@'+self.__bot.db['nicknames'][f'{reply_msg.author_id}'] except: pass # Prevent empty buttons try: - count = len(message.reference.cached_message.embeds) + len(message.reference.cached_message.attachments) - except: - if source == 'revolt': - msg = await message.channel.fetch_message(message.replies[0].id) - elif source == 'guilded': - msg = await message.channel.fetch_message(message.replied_to[0].id) + if source == 'discord': + count = len(message.reference.cached_message.embeds) + len(message.reference.cached_message.attachments) else: + reply_msg_id = source_support.reply(message) + if type(reply_msg_id) is str or type(reply_msg_id) is int: + msg = await source_support.fetch_message( + source_support.channel(message), reply_msg_id + ) + else: + msg = reply_msg_id + count = len(source_support.embeds(msg)) + len(source_support.attachments(msg)) + except: + if source == 'discord': msg = await message.channel.fetch_message(message.reference.message_id) + else: + raise count = len(msg.embeds) + len(msg.attachments) if len(trimmed)==0: content_btn = nextcord.ui.Button( style=button_style,label=f'x{count}', emoji='\U0001F3DE', disabled=True ) + replytext = f'*:park: x{count}*' else: content_btn = nextcord.ui.Button( style=button_style, label=trimmed, disabled=True ) + replytext = f'*{trimmed}*' + + global_reply_v2 = True # Add PR buttons too. if is_pr or is_pr_ref: + components = ui.View() try: - components = ui.View() - components.add_rows( - pr_actionrow, - ui.ActionRow( - nextcord.ui.Button( - style=nextcord.ButtonStyle.url, label='Replying to ' + author_text, - url=await reply_msg.fetch_url(guild) + if not reply_v2: + components.add_rows( + pr_actionrow, + ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.url, + label=selector.fget('replying',values={'user': author_text}), + url=await reply_msg.fetch_url(guild) + ) + ), + ui.ActionRow( + content_btn ) - ), - ui.ActionRow( - content_btn ) + else: + components.add_row( + pr_actionrow + ) + replytext = ( + f'-# {arrow_unicode} ' + + f'[{selector.fget("replying", values={"user": author_text})}](<{await reply_msg.fetch_url(guild)}>)' + + f' - {replytext}' ) except: - components = ui.View() - components.add_rows( - pr_actionrow, - ui.ActionRow( - nextcord.ui.Button( - style=nextcord.ButtonStyle.gray, label='Replying to [unknown]', disabled=True + if not reply_v2: + components.add_rows( + pr_actionrow, + ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.gray, + label=selector.fget('replying',values={'user': '[unknown]'}), + disabled=True + ) ) ) - ) + else: + components.add_row( + pr_actionrow + ) + replytext = f'-# {arrow_unicode} {selector.fget("replying", values={"user": "[unknown"})}\n' else: try: - components = ui.View() - components.add_rows( - ui.ActionRow( - nextcord.ui.Button( - style=nextcord.ButtonStyle.url, label='Replying to '+author_text, - url=await reply_msg.fetch_url(guild) + if not reply_v2: + components = ui.View() + components.add_rows( + ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.url, + label=selector.fget('replying',values={'user': author_text}), + url=await reply_msg.fetch_url(guild) + ) + ), + ui.ActionRow( + content_btn ) - ), - ui.ActionRow( - content_btn ) + replytext = ( + f'-# {arrow_unicode} '+ + f'[{selector.fget("replying",values={"user": author_text})}](<{await reply_msg.fetch_url(guild)}>)'+ + f' - {replytext}\n' ) except: - components = ui.View( - ui.ActionRow( - nextcord.ui.Button( - style=nextcord.ButtonStyle.gray, label='Replying to [unknown]', disabled=True + if not reply_v2: + components = ui.View() + components.add_rows( + ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.gray, + label=selector.fget('replying',values={'user': '[unknown]'}), + disabled=True + ) + ), + ui.ActionRow( + content_btn ) - ), - ui.ActionRow( - content_btn ) - ) + replytext = f'-# {arrow_unicode} {selector.fget("replying", values={"user": "[unknown]"})}\n' elif replying: + global_reply_v2 = True try: - if source == 'revolt': - authid = message.replies[0].author.id - elif source == 'guilded': - authid = message.replied_to[0].author.id - else: + if source == 'discord': if message.reference.cached_message: authid = message.reference.cached_message.author.id else: authmsg = await message.channel.fetch_message(message.reference.message_id) authid = authmsg.author.id + else: + reply_msg_id = source_support.reply(message) + if type(reply_msg_id) is str or type(reply_msg_id) is int: + authmsg = await source_support.fetch_message( + source_support.channel(message), reply_msg_id + ) + else: + authmsg = reply_msg_id + authid = source_support.get_id(source_support.author(authmsg)) except: authid = None - try: - botrvt = authid==self.bot.revolt_client.user.id - except: - botrvt = False - try: - botgld = authid==self.bot.guilded_client.user.id - except: - botgld = False - if authid==self.bot.user.id or botrvt or botgld: + botext = authid == source_support.bot_id() + + if authid==self.__bot.user.id or botext: reply_row = ui.ActionRow( - nextcord.ui.Button(style=nextcord.ButtonStyle.gray, label='Replying to [system]', + nextcord.ui.Button(style=nextcord.ButtonStyle.gray, + label=selector.fget('replying',values={'user': '[system]'}), disabled=True) ) + replytext = f'-# {arrow_unicode} {selector.fget("replying", values={"user": "[system]"})}\n' else: reply_row = ui.ActionRow( - nextcord.ui.Button(style=nextcord.ButtonStyle.gray, label='Replying to [unknown]', + nextcord.ui.Button(style=nextcord.ButtonStyle.gray, + label=selector.fget('replying',values={'user': '[unknown]'}), disabled=True) ) - if pr_actionrow: - components = ui.MessageComponents() - components.add_rows( - pr_actionrow,reply_row - ) - else: + replytext = f'-# {arrow_unicode} {selector.fget("replying", values={"user": "[unknown]"})}\n' + if not reply_v2: + if pr_actionrow: + components = ui.MessageComponents() + components.add_rows( + pr_actionrow,reply_row + ) + else: + components = ui.MessageComponents() + components.add_rows( + reply_row + ) + elif pr_actionrow: components = ui.MessageComponents() components.add_rows( - reply_row + pr_actionrow ) # Attachment processing @@ -1430,48 +1659,39 @@ async def to_file(source_file): return await source_file.to_file(use_cached=True, spoiler=False) except: return await source_file.to_file(use_cached=False, spoiler=False) - elif source == 'revolt': - filebytes = await source_file.read() - return nextcord.File(fp=BytesIO(filebytes), filename=source_file.filename) - elif source == 'guilded': - tempfile = await source_file.to_file() - return nextcord.File(fp=tempfile.fp, filename=source_file.filename) - elif platform == 'revolt': + else: + return await source_support.to_discord_file(source_file) + else: if source == 'discord': - f = await source_file.to_file(use_cached=True) - return revolt.File(f.fp.read(), filename=f.filename) - elif source == 'guilded': - f = await source_file.to_file() - return revolt.File(f.fp.read(), filename=f.filename) - elif source == 'revolt': - filebytes = await source_file.read() - return revolt.File(filebytes, filename=source_file.filename) - elif platform == 'guilded': - if source == 'guilded': - try: - return await source_file.to_file() - except: - return await source_file.to_file() - elif source == 'revolt': - filebytes = await source_file.read() - return guilded.File(fp=BytesIO(filebytes), filename=source_file.filename) - elif source == 'discord': - tempfile = await source_file.to_file(use_cached=True) - return guilded.File(fp=tempfile.fp, filename=source_file.filename) + return await dest_support.to_platform_file(source_file) + else: + # use nextcord.File as a universal file object + return await dest_support.to_platform_file( + await source_support.to_discord_file(source_file) + ) index = 0 for attachment in attachments: if system: break - if source == 'guilded': - if not attachment.file_type.image and not attachment.file_type.video: - continue - else: + if source == 'discord': if (not 'audio' in attachment.content_type and not 'video' in attachment.content_type and not 'image' in attachment.content_type and not 'text/plain' in attachment.content_type and - self.bot.config['safe_filetypes']) or attachment.size > 25000000: + self.__bot.config['safe_filetypes']) or attachment.size > 25000000: continue - files.append(await to_file(attachment)) + else: + attachment_size = source_support.attachment_size(attachment) + content_type = source_support.attachment_size(attachment) + if ( + not 'audio' in content_type and not 'video' in content_type and not 'image' in content.type + and not 'text/plain' in content_type and self.__bot.config['safe_filetypes'] + ) or attachment_size > 25000000 or not dest_support.attachment_type_allowed(content_type): + continue + + try: + files.append(await to_file(attachment)) + except platform_base.MissingImplementation: + continue index += 1 if index >= max_files: break @@ -1480,30 +1700,35 @@ async def to_file(source_file): # Avatar try: - if f'{message.author.id}' in self.bot.db['avatars']: - url = self.bot.db['avatars'][f'{message.author.id}'] + if f'{author_id}' in self.__bot.db['avatars']: + url = self.__bot.db['avatars'][f'{author_id}'] else: - url = message.author.avatar.url + if source == 'discord': + url = message.author.avatar.url + else: + url = source_support.avatar(message.author) except: url = None if system: try: - url = self.bot.user.avatar.url + url = self.__bot.user.avatar.url except: url = None # Add system identifier msg_author = author if system: - msg_author = self.bot.user.global_name if self.bot.user.global_name else self.bot.user.name + ' (system)' + msg_author = ( + self.__bot.user.global_name if self.__bot.user.global_name else self.__bot.user.name + )+ ' (system)' # Send message embeds = message.embeds if not message.author.bot and not system: embeds = [] - if msg_author.lower()==f'{self.bot.user.name} (system)'.lower() and not system: + if msg_author.lower()==f'{self.__bot.user.name} (system)'.lower() and not system: msg_author = '[hidden username]' if platform=='discord': @@ -1518,26 +1743,41 @@ async def to_file(source_file): webhook = None try: - webhook = self.bot.bridge.webhook_cache.get_webhook( - f'{self.bot.db["rooms"][room][guild][0]}' + webhook = self.__bot.bridge.webhook_cache.get_webhook( + f'{self.__bot.db["rooms"][room]["discord"][guild][0]}' ) except: # It'd be better to fetch all instead of individual webhooks here, so they can all be cached hooks = await destguild.webhooks() - self.bot.bridge.webhook_cache.store_webhooks(hooks) + self.__bot.bridge.webhook_cache.store_webhooks(hooks) for hook in hooks: - if hook.id in self.bot.db['rooms'][room][guild]: + if hook.id in self.__bot.db['rooms'][room]['discord'][guild]: webhook = hook break if not webhook: continue + # fun fact: tbsend stands for "threaded bridge send", but we read it + # as "turbo send", because it sounds cooler and tbsend is what lets + # unifier bridge using webhooks with ultra low latency. async def tbsend(webhook,url,msg_author_dc,embeds,message,mentions,components,sameguild, destguild): try: + tosend_content = replytext+(friendly_content if friendlified else msg_content) + if len(tosend_content) > 2000: + tosend_content = tosend_content[:-(len(tosend_content)-2000)] + if not components: + components = ui.MessageComponents() + components.add_row( + ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.gray,label='[Message truncated]',disabled=True + ) + ) + ) files = await get_files(message.attachments) msg = await webhook.send(avatar_url=url, username=msg_author_dc, embeds=embeds, - content=friendly_content if friendlified else message.content, + content=tosend_content, files=files, allowed_mentions=mentions, view=( components if components and not system else ui.MessageComponents() ), wait=True) @@ -1546,7 +1786,8 @@ async def tbsend(webhook,url,msg_author_dc,embeds,message,mentions,components,sa tbresult = [ {f'{destguild.id}': [webhook.channel.id, msg.id]}, {f'{destguild.id}': f'https://discord.com/channels/{destguild.id}/{webhook.channel.id}/{msg.id}'}, - [sameguild, msg.id] + [sameguild, msg.id], + reply_v2 ] return tbresult @@ -1556,9 +1797,21 @@ async def tbsend(webhook,url,msg_author_dc,embeds,message,mentions,components,sa destguild))) else: try: + tosend_content = replytext + (friendly_content if friendlified else msg_content) + if len(tosend_content) > 2000: + tosend_content = tosend_content[:-(len(tosend_content) - 2000)] + if not components: + components = ui.MessageComponents() + components.add_row( + ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.gray, label='[Message truncated]', disabled=True + ) + ) + ) files = await get_files(message.attachments) msg = await webhook.send(avatar_url=url, username=msg_author_dc, embeds=embeds, - content=friendly_content if friendlified else message.content, + content=tosend_content, files=files, allowed_mentions=mentions, view=( components if components and not system else ui.MessageComponents() ), wait=True) @@ -1566,193 +1819,118 @@ async def tbsend(webhook,url,msg_author_dc,embeds,message,mentions,components,sa continue message_ids.update({f'{destguild.id}':[webhook.channel.id,msg.id]}) urls.update({f'{destguild.id}':f'https://discord.com/channels/{destguild.id}/{webhook.channel.id}/{msg.id}'}) - elif platform=='revolt': + else: try: - ch = destguild.get_channel(self.bot.db['rooms_revolt'][room][guild][0]) + ch = dest_support.get_channel(self.__bot.db['rooms'][room][platform][guild][0]) except: - ch = await self.bot.revolt_client.fetch_channel(self.bot.db['rooms_revolt'][room][guild][0]) + ch = await dest_support.fetch_channel(self.__bot.db['rooms'][room][platform][guild][0]) - # Processing replies for Revolt here for efficiency - replies = [] try: if reply_msg: - if reply_msg.source=='revolt': + if reply_msg.source==platform: try: - msg = await ch.fetch_message(await reply_msg.fetch_id(destguild.id)) - replies = [revolt.MessageReply(msg)] + msg = await source_support.fetch_message(ch, await reply_msg.fetch_id(destguild.id)) + reply = msg except: - pass + reply = None else: - msg_ref = await reply_msg.fetch_external('revolt',destguild.id) - msg = await ch.fetch_message(msg_ref.id) - replies = [revolt.MessageReply(msg)] + msg_ref = await reply_msg.fetch_external(platform, destguild.id) + msg = source_support.fetch_message(ch, msg_ref.id) + reply = msg + else: + reply = None except: - pass + reply = None - rvtcolor = None - if str(message.author.id) in list(self.bot.db['colors'].keys()): - color = self.bot.db['colors'][str(message.author.id)] + color = None + if str(author_id) in list(self.__bot.db['colors'].keys()): + color = self.__bot.db['colors'][str(author_id)] if color == 'inherit': - if source=='revolt': - try: - color = message.author.roles[len(message.author.roles) - 1].colour.replace('#', '') - rgbtuple = tuple(int(color[i:i + 2], 16) for i in (0, 2, 4)) - rvtcolor = f'rgb{rgbtuple}' - except: - pass - else: - rvtcolor = f'rgb({message.author.color.r},{message.author.color.g},{message.author.color.b})' - else: - try: - rgbtuple = tuple(int(color[i:i + 2], 16) for i in (0, 2, 4)) - rvtcolor = f'rgb{rgbtuple}' - except: - pass - - msg_author_rv = msg_author - if len(msg_author) > 32: - msg_author_rv = msg_author[:-(len(msg_author)-32)] - if useremoji: - msg_author_rv = msg_author[:-2] - - if useremoji: - msg_author_rv = msg_author_rv + ' ' + useremoji + roles = source_support.roles(source_support.author(message)) + color = source_support.get_hex(roles[len(roles)-1]) - try: - persona = revolt.Masquerade(name=msg_author_rv, avatar=url, colour=rvtcolor) - except: - persona = revolt.Masquerade(name=msg_author_rv, avatar=None, colour=rvtcolor) - try: + async def tbsend(msg_author,url,color,useremoji,reply,content): files = await get_files(message.attachments) - msg = await ch.send( - content=friendly_content if friendlified else message.content, embeds=message.embeds, - attachments=files, replies=replies, masquerade=persona + special = { + 'bridge': { + 'name': msg_author, + 'avatar': url, + 'color': color, + 'emoji': useremoji + }, + 'files': files, + 'embeds': ( + dest_support.convert_embeds(message.embeds) if source=='discord' + else dest_support.convert_embeds( + source_support.convert_embeds_discord( + source_support.embeds(message) + ) + ) + ), + 'reply': None + } + if reply: + special.update({'reply': reply}) + msg = await dest_support.send( + ch, content, special=special ) - except: - continue - - message_ids.update({destguild.id:[ch.id,msg.id]}) - elif platform=='guilded': - try: - webhook = self.bot.bridge.webhook_cache.get_webhook([f'{self.bot.db["rooms_guilded"][room][guild][0]}']) - except: - try: - webhook = await destguild.fetch_webhook(self.bot.db["rooms_guilded"][room][guild][0]) - self.bot.bridge.webhook_cache.store_webhook(webhook) - except: - continue - - # Processing replies for Revolt here for efficiency - replytext = '' - - if not trimmed and reply_msg: - is_copy = False - try: - content = message.reference.cached_message.content - except: - if source == 'revolt': - msg = await message.channel.fetch_message(message.replies[0].id) - elif source == 'guilded': - msg = await message.channel.fetch_message(message.replied_to[0].id) - if msg.webhook_id: - is_copy = True - else: - msg = await message.channel.fetch_message(message.reference.message_id) - content = msg.content - clean_content = nextcord.utils.remove_markdown(content) - - if reply_msg.reply and source == 'guilded' and is_copy: - clean_content = clean_content.split('\n', 1)[1] - - msg_components = clean_content.split('<@') - offset = 0 - if clean_content.startswith('<@'): - offset = 1 - - while offset < len(msg_components): - try: - userid = int(msg_components[offset].split('>', 1)[0]) - except: - offset += 1 - continue - user = self.bot.get_user(userid) - if user: - clean_content = clean_content.replace(f'<@{userid}>', - f'@{user.global_name}').replace( - f'<@!{userid}>', f'@{user.global_name}') - offset += 1 - if len(clean_content) > 80: - trimmed = clean_content[:-(len(clean_content) - 77)] + '...' - else: - trimmed = clean_content - trimmed = trimmed.replace('\n', ' ') - - if reply_msg: - author_text = '[unknown]' - + tbresult = [ + {f'{dest_support.get_id(destguild)}': [ + dest_support.get_id(dest_support.channel(msg)), dest_support.get_id(msg) + ]}, + None, + [sameguild, dest_support.get_id(msg)] + ] try: - if reply_msg.source == 'revolt': - user = self.bot.revolt_client.get_user(reply_msg.author_id) - if not user.display_name: - author_text = f'@{user.name}' - else: - author_text = f'@{user.display_name}' - elif reply_msg.source == 'guilded': - user = self.bot.guilded_client.get_user(reply_msg.author_id) - author_text = f'@{user.name}' - else: - user = self.bot.get_user(int(reply_msg.author_id)) - author_text = f'@{user.global_name}' - if f'{reply_msg.author_id}' in list(self.bot.db['nicknames'].keys()): - author_text = '@' + self.bot.db['nicknames'][f'{reply_msg.author_id}'] - except: + tbresult[1] = { + f'{dest_support.get_id(destguild)}': dest_support.url(msg) + } + except platform_base.MissingImplementation: pass + return tbresult - try: - replytext = f'**[Replying to {author_text}]({reply_msg.urls[destguild.id]})** - *{trimmed}*\n' - except: - replytext = f'**Replying to [unknown]**\n' - - if len(replytext+message.content)==0: - replytext = '[empty message]' - - msg_author_gd = msg_author - if len(msg_author) > 25: - msg_author_gd = msg_author[:-(len(msg_author) - 25)] - - async def tbsend(webhook, url, msg_author_gd, embeds, message, replytext, sameguild, destguild): - files = await get_files(message.attachments) - try: - msg = await webhook.send(avatar_url=url, - username=msg_author_gd.encode("ascii", errors="ignore").decode(), - embeds=embeds, - content=replytext + (friendly_content if friendlified else message.content), - files=files) - except: - return None - - gdresult = [ - {f'{destguild.id}': [msg.channel.id, msg.id]}, - {f'{destguild.id}': msg.share_url}, - [sameguild, msg.id] - ] - return gdresult - - if tb_v2: - threads.append(asyncio.create_task(tbsend(webhook, url, msg_author_gd, embeds, message, replytext, - sameguild, destguild))) + if dest_support.enable_tb: + threads.append(asyncio.create_task(tbsend( + msg_author,url,color,useremoji,reply,friendly_content if friendlified else msg_content + ))) else: try: files = await get_files(message.attachments) - msg = await webhook.send(avatar_url=url, - username=msg_author_gd.encode("ascii", errors="ignore").decode(), - embeds=embeds, - content=replytext+(friendly_content if friendlified else message.content), - files=files) + special = { + 'bridge': { + 'name': msg_author, + 'avatar': url, + 'color': color, + 'emoji': useremoji + }, + 'files': files, + 'embeds': ( + dest_support.convert_embeds(message.embeds) if source=='discord' + else dest_support.convert_embeds( + source_support.convert_embeds_discord( + source_support.embeds(message) + ) + ) + ), + 'reply': None + } + if reply: + special.update({'reply': reply}) + msg = await dest_support.send( + ch, friendly_content if friendlified else msg_content, special=special + ) except: continue - message_ids.update({f'{destguild.id}':[msg.channel.id,msg.id]}) - urls.update({f'{destguild.id}':msg.share_url}) + + message_ids.update({ + str(dest_support.get_id(destguild)): [ + dest_support.get_id(ch),dest_support.get_id(msg) + ] + }) + try: + urls.update({str(dest_support.get_id(destguild)): dest_support.url(msg)}) + except platform_base.MissingImplementation: + pass # Update cache tbv2_results = [] @@ -1767,7 +1945,8 @@ async def tbsend(webhook, url, msg_author_gd, embeds, message, replytext, samegu if not result: continue message_ids.update(result[0]) - urls.update(result[1]) + if result[1]: + urls.update(result[1]) if result[2][0]: parent_id = result[2][1] @@ -1778,7 +1957,7 @@ async def tbsend(webhook, url, msg_author_gd, embeds, message, replytext, samegu self.prs.update({pr_id: parent_id}) if system: - msg_author = self.bot.user.id + msg_author = self.__bot.user.id else: msg_author = message.author.id @@ -1797,6 +1976,7 @@ async def tbsend(webhook, url, msg_author_gd, embeds, message, replytext, samegu except: self.bridged[index].external_copies.update({platform: message_ids}) self.bridged[index].urls = self.bridged[index].urls | urls + self.bridged[index].reply_v2 = global_reply_v2 if not self.bridged[index].reply_v2 else self.bridged[index].reply_v2 except: copies = {} external_copies = {} @@ -1810,7 +1990,7 @@ async def tbsend(webhook, url, msg_author_gd, embeds, message, replytext, samegu server_id = message.guild.id if extbridge: try: - hook = await self.bot.fetch_webhook(message.webhook_id) + hook = await self.__bot.fetch_webhook(message.webhook_id) msg_author = hook.user.id except: pass @@ -1827,7 +2007,8 @@ async def tbsend(webhook, url, msg_author_gd, embeds, message, replytext, samegu prehook=message.id, room=room, reply=replying, - external_bridged=extbridge + external_bridged=extbridge, + reply_v2=global_reply_v2 )) if datetime.datetime.now().day != self.msg_stats_reset: self.msg_stats = {} @@ -1839,10 +2020,10 @@ async def tbsend(webhook, url, msg_author_gd, embeds, message, replytext, samegu class WebhookCacheStore: def __init__(self, bot): - self.bot = bot + self.__bot = bot self.__webhooks = {} - def store_webhook(self, webhook: nextcord.Webhook or guilded.Webhook): + def store_webhook(self, webhook: nextcord.Webhook): if not webhook.guild.id in self.__webhooks.keys(): self.__webhooks.update({webhook.guild.id: {webhook.id: webhook}}) self.__webhooks[webhook.guild.id].update({webhook.id: webhook}) @@ -1887,7 +2068,9 @@ class Bridge(commands.Cog, name=':link: Bridge'): Developed by Green and ItsAsheer""" def __init__(self, bot): + global language self.bot = bot + language = self.bot.langmgr restrictions.attach_bot(self.bot) if not hasattr(self.bot, 'bridged'): self.bot.bridged = [] @@ -1936,75 +2119,79 @@ def __init__(self, bot): if webhook_cache: self.bot.bridge.webhook_cache = webhook_cache - @commands.command(aliases=['colour'],description='Sets Revolt color.') + @commands.command(aliases=['colour'],description=language.desc('bridge.color')) async def color(self,ctx,*,color=''): + selector = language.get_selector(ctx) if color=='': try: current_color = self.bot.db['colors'][f'{ctx.author.id}'] if current_color=='': - current_color = 'Default' + current_color = selector.get('default') embed_color = self.bot.colors.unifier elif current_color=='inherit': - current_color = 'Inherit from role' + current_color = selector.get('inherit') embed_color = ctx.author.color.value else: embed_color = ast.literal_eval('0x'+current_color) except: current_color = 'Default' embed_color = self.bot.colors.unifier - embed = nextcord.Embed(title='Your Revolt color',description=current_color,color=embed_color) + embed = nextcord.Embed(title=selector.get('title'),description=current_color,color=embed_color) await ctx.send(embed=embed) elif color=='inherit': self.bot.db['colors'].update({f'{ctx.author.id}':'inherit'}) await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) - await ctx.send('Your Revolt messages will now inherit your Discord role color.') + await ctx.send(f'{self.bot.ui_emojis.success} '+selector.get('success_inherit')) else: try: tuple(int(color.replace('#','',1)[i:i + 2], 16) for i in (0, 2, 4)) except: - return await ctx.send('Invalid hex code!') + return await ctx.send(selector.get('invalid')) self.bot.db['colors'].update({f'{ctx.author.id}':color}) await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) - await ctx.send('Your Revolt messages will now inherit the custom color.') + await ctx.send(f'{self.bot.ui_emojis.success} '+selector.get('success_custom')) - @commands.command(description='Sets a nickname. An empty provided nickname will reset it.') + @commands.command(description=language.desc('bridge.nickname')) async def nickname(self, ctx, *, nickname=''): + selector = language.get_selector(ctx) if len(nickname) > 33: - return await ctx.send('Please keep your nickname within 33 characters.') + return await ctx.send(selector.get('exceed')) if len(nickname) == 0: self.bot.db['nicknames'].pop(f'{ctx.author.id}', None) else: self.bot.db['nicknames'].update({f'{ctx.author.id}': nickname}) await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) - await ctx.send('Nickname updated.') + await ctx.send(selector.get('success')) - @commands.command(description='Measures bot latency.') + @commands.command(description=language.desc('bridge.ping')) async def ping(self, ctx): + selector = language.get_selector(ctx) t = time.time() - msg = await ctx.send('Ping!') + msg = await ctx.send(selector.get('ping')) diff = round((time.time() - t) * 1000, 1) - text = 'Pong! :ping_pong:' + text = selector.get('pong')+' :ping_pong:' if diff <= 300 and self.bot.latency <= 0.2: - embed = nextcord.Embed(title='Normal - all is well!', - description=f'Roundtrip: {diff}ms\nHeartbeat: {round(self.bot.latency * 1000, 1)}ms\n\nAll is working normally!', + embed = nextcord.Embed(title=selector.get('normal_title'), + description=f'Roundtrip: {diff}ms\nHeartbeat: {round(self.bot.latency * 1000, 1)}ms\n\n'+selector.get('normal_body'), color=self.bot.colors.success) elif diff <= 600 and self.bot.latency <= 0.5: - embed = nextcord.Embed(title='Fair - could be better.', - description=f'Roundtrip: {diff}ms\nHeartbeat: {round(self.bot.latency * 1000, 1)}ms\n\nNothing\'s wrong, but the latency could be better.', + embed = nextcord.Embed(title=selector.get('fair_title'), + description=f'Roundtrip: {diff}ms\nHeartbeat: {round(self.bot.latency * 1000, 1)}ms\n\n'+selector.get('fair_body'), color=self.bot.colors.warning) elif diff <= 2000 and self.bot.latency <= 1.0: - embed = nextcord.Embed(title='SLOW - __**oh no.**__', - description=f'Roundtrip: {diff}ms\nHeartbeat: {round(self.bot.latency * 1000, 1)}ms\n\nBot latency is higher than normal, messages may be slow to arrive.', + embed = nextcord.Embed(title=selector.get('slow_title'), + description=f'Roundtrip: {diff}ms\nHeartbeat: {round(self.bot.latency * 1000, 1)}ms\n\n'+selector.get('slow_body'), color=self.bot.colors.error) else: - text = 'what' - embed = nextcord.Embed(title='**WAY TOO SLOW**', - description=f'Roundtrip: {diff}ms\nHeartbeat: {round(self.bot.latency * 1000, 1)}ms\n\nSomething is DEFINITELY WRONG here. Consider checking [Discord status](https://discordstatus.com) page.', + text = selector.get('what') + embed = nextcord.Embed(title=selector.get('tooslow_title'), + description=f'Roundtrip: {diff}ms\nHeartbeat: {round(self.bot.latency * 1000, 1)}ms\n\n'+selector.get('tooslow_body'), color=self.bot.colors.critical) await msg.edit(content=text, embed=embed) - @commands.command(description='Shows a list of all global emojis available on the instance.') + @commands.command(description=language.desc('bridge.emojis')) async def emojis(self,ctx): + selector = language.get_selector(ctx) panel = 0 limit = 8 page = 0 @@ -2035,10 +2222,10 @@ async def emojis(self,ctx): if interaction: if page > maxpage: page = maxpage - embed.title = f'{self.bot.ui_emojis.emoji} {self.bot.user.global_name or self.bot.user.name} emojis' - embed.description = 'Choose an emoji to view its info!' + embed.title = f'{self.bot.ui_emojis.emoji} '+selector.fget("title",values={"botname": self.bot.user.global_name or self.bot.user.name}) + embed.description = selector.get('body') selection = nextcord.ui.StringSelect( - max_values=1, min_values=1, custom_id='selection', placeholder='Emoji...' + max_values=1, min_values=1, custom_id='selection', placeholder=selector.get('selection_emoji') ) for x in range(limit): @@ -2064,8 +2251,8 @@ async def emojis(self,ctx): ) if len(embed.fields)==0: embed.add_field( - name='No emojis', - value='There\'s no global emojis here!', + name=selector.get('noresults_title'), + value=selector.get('noresults_body_emoji'), inline=False ) selection.add_option( @@ -2081,21 +2268,21 @@ async def emojis(self,ctx): ui.ActionRow( nextcord.ui.Button( style=nextcord.ButtonStyle.blurple, - label='Previous', + label=language.get('prev','commons.navigation',language=selector.language_set), custom_id='prev', disabled=page <= 0 or selection.disabled, emoji=self.bot.ui_emojis.prev ), nextcord.ui.Button( style=nextcord.ButtonStyle.blurple, - label='Next', + label=language.get('next','commons.navigation',language=selector.language_set), custom_id='next', disabled=page >= maxpage or selection.disabled, emoji=self.bot.ui_emojis.next ), nextcord.ui.Button( style=nextcord.ButtonStyle.green, - label='Search', + label=language.get('search','commons.navigation',language=selector.language_set), custom_id='search', emoji=self.bot.ui_emojis.search, disabled=selection.disabled @@ -2117,25 +2304,25 @@ def search_filter(query, query_cmd): offset += 1 embed.title = f'{self.bot.ui_emojis.emoji} {self.bot.user.global_name or self.bot.user.name} emojis / search' - embed.description = 'Choose an emoji to view its info!' + embed.description = selector.get('body') if len(emojis) == 0: maxpage = 0 embed.add_field( - name='No emojis', - value='There are no emojis matching your search query.', + name=selector.get('noresults_title'), + value=selector.get('noresults_body_search'), inline=False ) selection = nextcord.ui.StringSelect( - max_values=1, min_values=1, custom_id='selection', placeholder='Room...', disabled=True + max_values=1, min_values=1, custom_id='selection', placeholder=selector.get('selection_emoji'), disabled=True ) selection.add_option( - label='No emojis' + label=selector.get('noresults_title') ) else: maxpage = math.ceil(len(emojis) / limit) - 1 selection = nextcord.ui.StringSelect( - max_values=1, min_values=1, custom_id='selection', placeholder='Emoji...' + max_values=1, min_values=1, custom_id='selection', placeholder=selector.get('selection_emoji') ) emojis = await self.bot.loop.run_in_executor(None, lambda: sorted( @@ -2165,14 +2352,22 @@ def search_filter(query, query_cmd): description=guild.name ) - embed.description = f'Searching: {query} (**{len(emojis)}** results)' + embed.description = language.fget( + 'search_results','commons.search', + values={'query': query, 'results': len(emojis)}, + language=selector.language_set + ) maxcount = (page + 1) * limit if maxcount > len(emojis): maxcount = len(emojis) embed.set_footer( text=( - f'Page {page + 1} of {maxpage + 1} | {page * limit + 1}-{maxcount} of {len(emojis)}'+ - ' results' + language.fget('page','commons.search',values={ + 'page': page+1, 'maxpage': maxpage+1 + }, language=selector.language_set) + + ' | ' + language.fget('result_count','commons.search',values={ + 'lower':page*limit+1,'upper':maxcount + }, language=selector.language_set) ) ) @@ -2186,21 +2381,21 @@ def search_filter(query, query_cmd): ui.ActionRow( nextcord.ui.Button( style=nextcord.ButtonStyle.blurple, - label='Previous', + label=language.get('prev','commons.navigation',language=selector.language_set), custom_id='prev', disabled=page <= 0, emoji=self.bot.ui_emojis.prev ), nextcord.ui.Button( style=nextcord.ButtonStyle.blurple, - label='Next', + label=language.get('next','commons.navigation',language=selector.language_set), custom_id='next', disabled=page >= maxpage, emoji=self.bot.ui_emojis.next ), nextcord.ui.Button( style=nextcord.ButtonStyle.green, - label='Search', + label=language.get('search','commons.navigation',language=selector.language_set), custom_id='search', emoji=self.bot.ui_emojis.search ) @@ -2210,7 +2405,7 @@ def search_filter(query, query_cmd): ui.ActionRow( nextcord.ui.Button( style=nextcord.ButtonStyle.gray, - label='Back', + label=language.get('back','commons.navigation',language=selector.language_set), custom_id='back', emoji=self.bot.ui_emojis.back ) @@ -2219,24 +2414,24 @@ def search_filter(query, query_cmd): elif panel == 2: emoji_obj = nextcord.utils.get(self.bot.emojis, name=emojiname) embed.title = ( - f'{self.bot.ui_emojis.emoji} {self.bot.user.global_name or self.bot.user.name} emojis / search / {emojiname}' + f'{self.bot.ui_emojis.emoji} '+selector.fget("title",values={"botname": self.bot.user.global_name or self.bot.user.name})+f' / {selector.get("search").lower()} / {emojiname}' if was_searching else - f'{self.bot.ui_emojis.emoji} {self.bot.user.global_name or self.bot.user.name} emojis / {emojiname}' + f'{self.bot.ui_emojis.emoji} '+selector.fget("title",values={"botname": self.bot.user.global_name or self.bot.user.name})+f' / {emojiname}' ) emoji = ( f'' if emoji_obj.animated else f'<:{emojiname}:{emoji_obj.id}>' ) - embed.description = f'# **{emoji} `:{emojiname}:`**\nFrom: {emoji_obj.guild.name}' + embed.description = f'# **{emoji} `:{emojiname}:`**\n'+f'{selector.get("from")} {emoji_obj.guild.name}' embed.add_field( - name='How to use', - value=f'Type `[emoji: {emojiname}]` in your message to use this emoji!', + name=selector.get('instructions_title'), + value=selector.fget('instructions_body',values={'emojiname':emojiname}), inline=False ) components.add_rows( ui.ActionRow( nextcord.ui.Button( style=nextcord.ButtonStyle.gray, - label='Back', + label=language.get('back','commons.navigation',language=selector.language_set), custom_id='back', emoji=self.bot.ui_emojis.back ) @@ -2244,7 +2439,10 @@ def search_filter(query, query_cmd): ) if panel == 0: - embed.set_footer(text=f'Page {page + 1} of {maxpage + 1 if maxpage >= 0 else 1}') + embed.set_footer(text=language.get( + 'page','commons.search',values={'page':page+1,'maxpage':maxpage+1 if maxpage >= 0 else 1}, + language=selector.language_set + )) if not msg: msg = await ctx.send(embed=embed, view=components, reference=ctx.message, mention_author=False) else: @@ -2277,12 +2475,15 @@ def check(interaction): elif interaction.data['custom_id'] == 'next': page += 1 elif interaction.data['custom_id'] == 'search': - modal = nextcord.ui.Modal(title='Search...', auto_defer=False) + modal = nextcord.ui.Modal( + title=language.get('search_title','commons.search',language=selector.language_set), + auto_defer=False + ) modal.add_item( nextcord.ui.TextInput( - label='Search query', + label=language.get('query','commons.search',language=selector.language_set), style=nextcord.TextInputStyle.short, - placeholder='Type something...' + placeholder=language.get('query_prompt','commons.search',language=selector.language_set) ) ) await interaction.response.send_modal(modal) @@ -2293,19 +2494,20 @@ def check(interaction): @commands.command( aliases=['modcall'], - description='Ping all moderators to the chat! Use only when necessary, or else.' + description=language.desc('bridge.modping') ) @commands.cooldown(rate=1, per=1800, type=commands.BucketType.user) async def modping(self,ctx): + selector = language.get_selector(ctx) if not self.bot.config['enable_logging']: - return await ctx.send('Modping is disabled, contact your instance\'s owner.') + return await ctx.send(selector.get('disabled')) found = False room = None # Optimized logic for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{ctx.guild.id}' in list(data.keys()): guilddata = data[f'{ctx.guild.id}'] if len(guilddata) == 1: @@ -2328,7 +2530,7 @@ async def modping(self,ctx): for webhook in hooks: index = 0 for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{ctx.guild.id}' in list(data.keys()): hook_ids = data[f'{ctx.guild.id}'] else: @@ -2341,9 +2543,9 @@ async def modping(self,ctx): break if not found: - return await ctx.send(f'{self.bot.ui_emojis.error} This isn\'t a UniChat room!') + return await ctx.send(f'{self.bot.ui_emojis.error} {selector.get("invalid")}') - hook_id = self.bot.db['rooms'][room][f'{self.bot.config["home_guild"]}'][0] + hook_id = self.bot.db['rooms'][room]['discord'][f'{self.bot.config["home_guild"]}'][0] guild = self.bot.get_guild(self.bot.config['home_guild']) hooks = await guild.webhooks() @@ -2357,16 +2559,17 @@ async def modping(self,ctx): try: role = self.bot.config["moderator_role"] except: - return await ctx.send(f'{self.bot.ui_emojis.error} This instance doesn\'t have a moderator role set up. Contact your Unifier admins.') - await ch.send(f'<@&{role}> **{author}** ({ctx.author.id}) needs your help!\n\nSent from server **{ctx.guild.name}** ({ctx.guild.id})',allowed_mentions=nextcord.AllowedMentions(roles=True,everyone=False,users=False)) - return await ctx.send(f'{self.bot.ui_emojis.success} Moderators called!') + return await ctx.send(f'{self.bot.ui_emojis.error} {selector.get("no_moderator")}') + await ch.send(f'<@&{role}> {selector.fget("needhelp",values={"username":author,"userid":ctx.author.id})}',allowed_mentions=nextcord.AllowedMentions(roles=True,everyone=False,users=False)) + return await ctx.send(f'{self.bot.ui_emojis.success} {selector.get("success")}') - await ctx.send(f'{self.bot.ui_emojis.error} It appears the home guild has configured Unifier wrong, and I cannot ping its UniChat moderators.') + await ctx.send(f'{self.bot.ui_emojis.error} {selector.get("bad_config")}') @nextcord.message_command(name='View reactions') async def reactions_ctx(self, interaction, msg: nextcord.Message): if interaction.user.id in self.bot.db['fullbanned']: return + selector = language.get_selector('bridge.reactions_ctx',userid=interaction.user.id) gbans = self.bot.db['banned'] ct = time.time() if f'{interaction.user.id}' in list(gbans.keys()): @@ -2384,15 +2587,21 @@ async def reactions_ctx(self, interaction, msg: nextcord.Message): else: return if f'{interaction.user.id}' in list(gbans.keys()) or f'{interaction.guild.id}' in list(gbans.keys()): - return await interaction.response.send_message('Your account or your guild is currently **global banned**.', ephemeral=True) + return await interaction.response.send_message( + language.get('banned','commons.interaction',language=selector.language_set), + ephemeral=True + ) msg_id = msg.id try: msg: UnifierBridge.UnifierMessage = await self.bot.bridge.fetch_message(msg_id) except: - return await interaction.response.send_message('Could not find message in cache!', ephemeral=True) + return await interaction.response.send_message( + language.get('not_found','commons.interaction',language=selector.language_set), + ephemeral=True + ) - embed = nextcord.Embed(title=f'{self.bot.ui_emojis.emoji} Reactions',color=self.bot.colors.unifier) + embed = nextcord.Embed(title=f'{self.bot.ui_emojis.emoji} {selector.get("reactions")}',color=self.bot.colors.unifier) index = 0 page = 0 @@ -2405,7 +2614,8 @@ async def reactions_ctx(self, interaction, msg: nextcord.Message): while True: selection = nextcord.ui.StringSelect( - max_values=1, min_values=1, custom_id='selection', placeholder='Emoji...' + max_values=1, min_values=1, custom_id='selection', + placeholder=language.get('selection_emoji','bridge.emojis',language=selector.language_set) ) for x in range(limit): @@ -2433,12 +2643,12 @@ async def reactions_ctx(self, interaction, msg: nextcord.Message): emoji=list(msg.reactions.keys())[x + (page * limit)] if not name=='unknown' and platform=='discord' else None, value=f'{x}', default=x + (page * limit)==index, - description=f'{len(msg.reactions[list(msg.reactions.keys())[x + (page * limit)]].keys())} reactions' + description=selector.fget('reactions_count',values={"count": len(msg.reactions[list(msg.reactions.keys())[x + (page * limit)]].keys())}) ) users = [] if len(msg.reactions.keys()) == 0: - embed.description = f'No reactions yet!' + embed.description = selector.get('no_reactions') else: platform = 'discord' for user in list(msg.reactions[list(msg.reactions.keys())[index]].keys()): @@ -2512,6 +2722,7 @@ def check(interaction): @nextcord.message_command(name='Report message') async def report(self, interaction, msg: nextcord.Message): + selector = language.get_selector('bridge.report',userid=interaction.user.id) if interaction.user.id in self.bot.db['fullbanned']: return gbans = self.bot.db['banned'] @@ -2531,75 +2742,84 @@ async def report(self, interaction, msg: nextcord.Message): else: return if f'{interaction.user.id}' in list(gbans.keys()) or f'{interaction.guild.id}' in list(gbans.keys()): - return await interaction.response.send_message('You or your guild is currently **global banned**.', ephemeral=True) + return await interaction.response.send_message( + language.get('banned','commons.interaction',language=selector.language_set), + ephemeral=True + ) if not self.bot.config['enable_logging']: - return await interaction.response.send_message('Reporting and logging are disabled, contact your instance\'s owner.', ephemeral=True) + return await interaction.response.send_message(selector.get('disabled'), ephemeral=True) try: msgdata = await self.bot.bridge.fetch_message(msg.id) except: - return await interaction.response.send_message('Could not find message in cache!') + return await interaction.response.send_message( + language.get('not_found','commons.interaction',language=selector.language_set) + ) roomname = msgdata.room userid = msgdata.author_id content = copy.deepcopy(msg.content) # Prevent tampering w/ original content btns = ui.ActionRow( - nextcord.ui.Button(style=nextcord.ButtonStyle.blurple, label='Spam', custom_id=f'spam', disabled=False), + nextcord.ui.Button(style=nextcord.ButtonStyle.blurple, label=selector.get('spam'), custom_id=f'spam', disabled=False), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Abuse or harassment', custom_id=f'abuse', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('abuse'), custom_id=f'abuse', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Explicit or dangerous content', custom_id=f'explicit', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('explicit'), custom_id=f'explicit', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Violates other room rules', custom_id=f'other', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('other'), custom_id=f'other', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Something else', custom_id=f'misc', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('misc'), custom_id=f'misc', disabled=False ) ) btns_abuse = ui.ActionRow( nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Impersonation', custom_id=f'abuse_1', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('abuse_1'), custom_id=f'abuse_1', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Harassment', custom_id=f'abuse_2', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('abuse_2'), custom_id=f'abuse_2', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Intentional misinformation', custom_id=f'abuse_3', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('abuse_3'), custom_id=f'abuse_3', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Derogatory language', custom_id=f'abuse_4', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('abuse_4'), custom_id=f'abuse_4', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Other', custom_id=f'abuse_5', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('category_misc'), custom_id=f'abuse_5', disabled=False ) ) btns_explicit = ui.ActionRow( nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Adult content', custom_id=f'explicit_1', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('explicit_1'), custom_id=f'explicit_1', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Graphic/gory content', custom_id=f'explicit_2', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('explicit_2'), custom_id=f'explicit_2', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Encouraging real-world harm', custom_id=f'explicit_3', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('explicit_3'), custom_id=f'explicit_3', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Illegal content', custom_id=f'explicit_4', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('explicit_4'), custom_id=f'explicit_4', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, label='Other', custom_id=f'explicit_5', disabled=False + style=nextcord.ButtonStyle.blurple, label=selector.get('category_misc'), custom_id=f'explicit_5', disabled=False ) ) btns2 = ui.ActionRow( - nextcord.ui.Button(style=nextcord.ButtonStyle.gray, label='Cancel', custom_id=f'cancel', disabled=False) + nextcord.ui.Button( + style=nextcord.ButtonStyle.gray, + label=language.get('cancel','commons.navigation',language=selector.language_set), + custom_id=f'cancel', disabled=False + ) ) components = ui.MessageComponents() components.add_rows(btns, btns2) - msg = await interaction.response.send_message('How does this message violate our rules?', view=components, ephemeral=True) + msg = await interaction.response.send_message(selector.get('question'), view=components, ephemeral=True) msg = await msg.fetch() def check(interaction): @@ -2609,7 +2829,10 @@ def check(interaction): interaction = await self.bot.wait_for('interaction', check=check, timeout=60) except: try: - return await interaction.edit_original_message(content='Timed out.', view=None) + return await interaction.edit_original_message( + content=language.get('timeout','commons.interaction',language=selector.language_set), + view=None + ) except: return @@ -2624,12 +2847,15 @@ def check(interaction): components = ui.MessageComponents() if interaction.data["custom_id"] == 'abuse': components.add_rows(btns_abuse, btns2) - await interaction.response.edit_message(content='In what way?', view=components) + await interaction.response.edit_message(content=selector.get('question_2'), view=components) elif interaction.data["custom_id"] == 'explicit': components.add_rows(btns_explicit, btns2) - await interaction.response.edit_message(content='In what way?', view=components) + await interaction.response.edit_message(content=selector.get('question_2'), view=components) elif interaction.data["custom_id"] == 'cancel': - return await interaction.response.edit_message(content='Cancelled.', view=None) + return await interaction.response.edit_message( + content=language.get('cancel','commons.interaction',language=selector.language_set), + view=None + ) else: asked = False if asked: @@ -2637,7 +2863,10 @@ def check(interaction): interaction = await self.bot.wait_for('interaction', check=check, timeout=60) except: try: - return await interaction.edit_original_message(content='Timed out.', view=None) + return await interaction.edit_original_message( + content=language.get('timeout','commons.interaction',language=selector.language_set), + view=None + ) except: return buttons = msg.components[0].children @@ -2647,41 +2876,43 @@ def check(interaction): cat2 = button.label break if interaction.data["custom_id"] == 'cancel': - return await interaction.response.edit_message(content='Cancelled.', view=None) + return await interaction.response.edit_message(content=language.get('cancel','commons.interaction',language=selector.language_set), view=None) else: cat2 = 'none' self.bot.reports.update({f'{interaction.user.id}_{userid}_{msg.id}': [cat, cat2, content, roomname, msgdata.id]}) reason = nextcord.ui.TextInput( - style=nextcord.TextInputStyle.paragraph, label='Additional details', - placeholder='Add additional context or information that we should know here.', + style=nextcord.TextInputStyle.paragraph, label=selector.get('details_title'), + placeholder=selector.get('details_prompt'), required=False ) signature = nextcord.ui.TextInput( - style=nextcord.TextInputStyle.short, label='Sign with your username', - placeholder='Sign this only if your report is truthful and in good faith.', + style=nextcord.TextInputStyle.short, label=selector.get('sign_title'), + placeholder=selector.get('sign_prompt'), required=True, min_length=len(interaction.user.name), max_length=len(interaction.user.name) ) - modal = nextcord.ui.Modal(title='Report message', custom_id=f'{userid}_{msg.id}', auto_defer=False) + modal = nextcord.ui.Modal(title=selector.get('title'), custom_id=f'{userid}_{msg.id}', auto_defer=False) modal.add_item(reason) modal.add_item(signature) await interaction.response.send_modal(modal) - @commands.command(description='Shows your server\'s plugin restriction status.') + @commands.command(description=language.desc('bridge.serverstatus')) async def serverstatus(self,ctx): + selector = language.get_selector(ctx) embed = nextcord.Embed( - title='Server status', - description='Your server is not restricted by plugins.', + title=selector.get('title'), + description=selector.get('body_ok'), color=self.bot.colors.success ) if f'{ctx.guild.id}' in self.bot.bridge.restricted: - embed.description = 'Your server is currently limited by a plugin.' + embed.description = selector.get('body_restricted') embed.colour = self.bot.colors.warning await ctx.send(embed=embed) - @commands.command(aliases=['exp','lvl','experience'], description='Shows you or someone else\'s level and EXP.') + @commands.command(aliases=['exp','lvl','experience'], description=language.desc('bridge.level')) async def level(self,ctx,*,user=None): + selector = language.get_selector(ctx) if not self.bot.config['enable_exp']: - return await ctx.send('Leveling system is disabled on this instance.') + return await ctx.send(selector.get('disabled')) if not user: user = ctx.author else: @@ -2698,12 +2929,12 @@ async def level(self,ctx,*,user=None): progressbar = '['+(bars*'|')+(empty*' ')+']' embed = nextcord.Embed( title=( - 'Your level' if user.id==ctx.author.id else - f'{user.global_name if user.global_name else user.name}\'s level' + selector.get("title_self") if user.id==ctx.author.id else + selector.fget("title_other", values={"username": user.global_name if user.global_name else user.name}) ), description=( - f'Level {data["level"]} | {round(data["experience"],2)} EXP\n\n'+ - f'`{progressbar}`\n{round(data["progress"]*100)}% towards next level' + f'{selector.fget("level", values={"level": data["level"]})} | {selector.fget("exp",values={"exp": {round(data["experience"],2)}})}\n\n'+ + f'`{progressbar}`\n{selector.fget("progress",values={"progress": round(data["progress"]*100)})}' ), color=self.bot.colors.unifier ) @@ -2713,10 +2944,11 @@ async def level(self,ctx,*,user=None): ) await ctx.send(embed=embed) - @commands.command(aliases=['lb'],description='Shows EXP leaderboard.') + @commands.command(aliases=['lb'],description=language.desc('bridge.leaderboard')) async def leaderboard(self,ctx): + selector = language.get_selector(ctx) if not self.bot.config['enable_exp']: - return await ctx.send('Leveling system is disabled on this instance.') + return await ctx.send(language.get('disabled','bridge.level',language=selector.language_set)) expdata = copy.copy(self.bot.db['exp']) lb_data = await self.bot.loop.run_in_executor(None, lambda: sorted( expdata.items(), @@ -2727,7 +2959,7 @@ async def leaderboard(self,ctx): msg = None interaction = None embed = nextcord.Embed( - title=f'{self.bot.ui_emojis.leaderboard} {self.bot.user.global_name or self.bot.user.name} leaderboard', + title=f'{self.bot.ui_emojis.leaderboard} {selector.fget("title",values={"botname": self.bot.user.global_name or self.bot.user.name})}', color=self.bot.colors.unifier ) page = 1 @@ -2754,8 +2986,8 @@ async def leaderboard(self,ctx): else: username = '[unknown]' lb.append( - f'{placement_emoji[rank]} **{username}**: Level {lb_data[index][1]["level"]}' if rank <= 3 else - f'`{rank}.` **{username}**: Level {lb_data[index][1]["level"]}' + f'{placement_emoji[rank]} **{username}**: {language.fget("level","bridge.level",values={"level": lb_data[index][1]["level"]},language=selector.language_set)}' if rank <= 3 else + f'`{rank}.` **{username}**: {language.fget("level","bridge.level",values={"level": lb_data[index][1]["level"]},language=selector.language_set)}' ) lb_text = '\n'.join(lb) @@ -2820,293 +3052,24 @@ def check(interaction): elif interaction.data['custom_id']=='last': page = max_page - @commands.command(description='Makes a Squad.') - async def makesquad(self,ctx,*,squadname): - if not ctx.author.guild_permissions.manage_guild: - return await ctx.send('Only those with Manage Server permissions can manage their Squad.') - if not self.bot.config['enable_squads']: - return await ctx.send('Squads aren\'t enabled on this instance.') - if str(ctx.guild.id) in self.bot.db['squads'].keys(): - return await ctx.send('Your server already has a Squad! Disband it first to make a new one.') - - embed = nextcord.Embed( - title=f'Creating {squadname}', - description='First, your Squad must have a HQ channel so your members can receive updates on Squad events.', - color=self.bot.colors.unifier - ) - - components = ui.MessageComponents() - components.add_rows( - ui.ActionRow( - nextcord.ui.ChannelSelect( - max_values=1, - min_values=1, - placeholder='Channel...' - ) - ), - ui.ActionRow( - nextcord.ui.Button( - custom_id='cancel', - label='Cancel', - style=nextcord.ButtonStyle.gray - ) - ) - ) - - msg = await ctx.send(embed=embed,view=components) - - def check(interaction): - return interaction.message.id==msg.id and interaction.user.id==ctx.author.id - - try: - interaction = await self.bot.wait_for('interaction',check=check,timeout=60) - except: - return await msg.edit(view=None) - - if interaction.data['component_type']==2: - return await interaction.response.edit_message(view=None) - - hq_id = int(interaction.data['values'][0]) - squad = { - 'name': squadname, - 'suspended': False, - 'suspended_expire': 0, - 'members': [], - 'leader': ctx.author.id, - 'captains': [], - 'invited': [], - 'joinreqs': [], - 'points': 0, - 'hq': hq_id, - 'icon': None - } - - self.bot.db['squads'].update({f'{ctx.guild.id}': squad}) - self.bot.db.save() - - added = 0 - while True: - components = ui.MessageComponents() - components.add_rows( - ui.ActionRow( - nextcord.ui.UserSelect( - max_values=1, - min_values=1, - placeholder='Select users...' - ) - ), - ui.ActionRow( - nextcord.ui.Button( - custom_id='cancel', - label='Add later', - style=nextcord.ButtonStyle.gray - ) - ) - ) - - embed.description = ( - 'Your squad was created, now you need your Squad captains! You\'re already a Squad captain, but you '+ - 'should add two more. They\'ll be sent an invitation if they aren\'t in a Squad yet.'+ - '\n\n**You need 2 more Squad captains.**' if added==0 else '\n\n**You need 1 more Squad captain.**' - ) - - await msg.edit(embed=embed,view=components) - - try: - interaction = await self.bot.wait_for('interaction',check=check,timeout=300) - except: - return await msg.edit(view=None) - - if interaction.data['component_type']==2: - return await interaction.response.edit_message(view=None) - - if added==2: - break - - user = self.bot.get_user(int(interaction.data['values'][0])) - - fail_msg = ( - 'The bot could not send a Squad invitation! This is either because:\n- The user has their DMs with '+ - 'the bot off\n- The user is ignoring Squad invitations from your server' - ) - - try: - if f'{user.id}' in self.bot.db['squads_optout'].keys(): - optout = self.bot.db['squads_optout'][f'{user.id}'] - if optout['all']: - fail_msg = f'{user.global_name or user.name} has opted out of receiving Squad invitations.' - raise ValueError() - elif ctx.guild.id in optout['guilds']: - raise ValueError() - if f'{user.id}' in self.bot.db['squads_joined'].keys(): - if self.bot.db['squads_optout'][f'{user.id}'] is None: - fail_msg = f'{user.global_name or user.name} is already in a Squad!' - raise ValueError() - embed = nextcord.Embed( - title=( - f'{ctx.author.global_name or ctx.author.name} has invited you to join {ctx.guild.name}\'s '+ - f'**{squadname}** Squad!' - ), - description=( - 'You\'ve been invited to join as a **Squad Captain**!\nAs a captain, you may:\n'+ - '- Make submissions for events on your Squad\'s behalf\n'+ - '- Accept and deny join requests for your Squad\n\n'+ - f'To join this squad, run `{self.bot.command_prefix}joinsquad {ctx.guild.id}`!' - ) - ) - embed.set_footer( - text=( - f'Reminder - you can always run {self.bot.command_prefix}ignoresquad {ctx.guild.id} to stop receiving invites from '+ - 'this server\'s Squad.' - ) - ) - await user.send(embed=embed) - added += 1 - if added == 2: - break - else: - embed.description = ( - 'Your squad was created, now you need your Squad captains! You\'re already a Squad captain, but you ' + - 'should add two more. They\'ll be sent an invitation if they aren\'t in a Squad yet.' + - '\n\n**You need 1 more Squad captain.**' - ) - - await msg.edit(embed=embed, view=components) - self.bot.db['squads'][f'{ctx.guild.id}']['invited'].append(user.id) - await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) - await interaction.response.send_message('Invite sent!', ephemeral=True) - except: - await interaction.response.send_message(fail_msg,ephemeral=True) - - await interaction.response.edit_message(embed=embed,view=None) - pass - - @commands.command(description='Disbands your squad.') - async def disbandsquad(self,ctx): - if not ctx.author.guild_permissions.manage_guild: - return await ctx.send('Only those with Manage Server permissions can manage their Squad.') - if not self.bot.config['enable_squads']: - return await ctx.send('Squads aren\'t enabled on this instance.') - - @commands.command(name='squad-leaderboard', aliases=['squadlb'], description='Shows Squad points leaderboard.') - async def squad_leaderboard(self, ctx): - if not self.bot.config['enable_squads']: - return await ctx.send('Squads aren\'t enabled on this instance.') - expdata = copy.copy(self.bot.db['squads']) - lb_data = await self.bot.loop.run_in_executor(None, lambda: sorted( - expdata.items(), - key=lambda x: x[1]['points'], - reverse=True - ) - ) - msg = None - interaction = None - embed = nextcord.Embed( - title=f'{self.bot.user.global_name or self.bot.user.name} Squads leaderboard', - color=self.bot.colors.unifier - ) - page = 1 - limit = 10 - max_page = math.ceil(len(lb_data) / limit) - - placement_emoji = { - 1: ':first_place:', - 2: ':second_place:', - 3: ':third_place:' - } - - while True: - lb = [] - - for x in range(limit): - index = (page - 1) * limit + x - rank = index + 1 - if index >= len(lb_data): - break - username = lb_data[index][1]['name'] - lb.append( - f'{placement_emoji[rank]} **{username}**: {lb_data[index][1]["points"]} points' if rank <= 3 else - f'`{rank}.` **{username}**: {lb_data[index][1]["level"]} points' - ) - - lb_text = '\n'.join(lb) - - embed.description = lb_text - - btns = ui.ActionRow( - nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, - emoji='\U000023EE', - custom_id='first', - disabled=page == 1 - ), - nextcord.ui.Button( - style=nextcord.ButtonStyle.gray, - emoji='\U000025C0', - custom_id='prev', - disabled=page == 1 - ), - nextcord.ui.Button( - style=nextcord.ButtonStyle.gray, - emoji='\U000025B6', - custom_id='next', - disabled=page == max_page - ), - nextcord.ui.Button( - style=nextcord.ButtonStyle.blurple, - emoji='\U000023ED', - custom_id='last', - disabled=page == max_page - ) - ) - - components = ui.MessageComponents() - components.add_row(btns) - - if not msg: - msg = await ctx.send(embed=embed, view=components) - else: - await interaction.response.edit_message(embed=embed, view=components) - - def check(interaction): - return interaction.user.id == ctx.author.id and interaction.message.id == msg.id - - try: - interaction = await self.bot.wait_for('interaction', check=check, timeout=60) - except: - for x in range(len(btns.items)): - btns.items[x].disabled = True - - components = ui.MessageComponents() - components.add_row(btns) - await msg.edit(view=components) - break - - if interaction.data['custom_id'] == 'first': - page = 1 - elif interaction.data['custom_id'] == 'prev': - page -= 1 - elif interaction.data['custom_id'] == 'next': - page += 1 - elif interaction.data['custom_id'] == 'last': - page = max_page - @commands.Cog.listener() async def on_interaction(self, interaction): if interaction.type==nextcord.InteractionType.component: if not 'custom_id' in interaction.data.keys(): return if (interaction.data["custom_id"].startswith('rp') or interaction.data["custom_id"].startswith('ap')) and not interaction.user.id in self.bot.moderators: - return await interaction.response.send_message('buddy you\'re not a global moderator :skull:',ephemeral=True) + selector = language.get_selector('bridge.bridge',interaction.user.id) + return await interaction.response.send_message(language.get("mod_unexpected","commons.interaction",language=selector.language_set),ephemeral=True) if interaction.data["custom_id"].startswith('rpdelete'): + selector = language.get_selector('bridge.bridge', interaction.user.id) msg_id = int(interaction.data["custom_id"].replace('rpdelete_','',1)) btns = ui.ActionRow( nextcord.ui.Button( - style=nextcord.ButtonStyle.red, label='Delete message', + style=nextcord.ButtonStyle.red, label=language.get('delete','commons.moderation',language=selector.language_set), custom_id=f'rpdelete_{interaction.data["custom_id"].split("_")[1]}', disabled=True ), nextcord.ui.Button( - style=nextcord.ButtonStyle.green, label='Mark as reviewed', + style=nextcord.ButtonStyle.green, label=language.get('review','bridge.report',language=selector.language_set), custom_id=f'rpreview_{interaction.data["custom_id"].split("_")[1]}', disabled=False ) ) @@ -3116,35 +3079,36 @@ async def on_interaction(self, interaction): try: msg: UnifierBridge.UnifierMessage = await self.bot.bridge.fetch_message(msg_id) except: - return await interaction.response.send_message('Could not find message in cache!',ephemeral=True) + return await interaction.response.send_message(language.get('not_found','commons.interaction',language=selector.language_set),ephemeral=True) if not interaction.user.id in self.bot.moderators: return await interaction.response.send_message('go away',ephemeral=True) - msg_orig = await interaction.response.send_message("Deleting...",ephemeral=True) + await interaction.response.defer(ephemeral=True,with_message=True) try: await self.bot.bridge.delete_parent(msg_id) if msg.webhook: raise ValueError() await interaction.message.edit(view=components) - return await msg_orig.edit('Deleted message (parent deleted, copies will follow)') + return await interaction.edit_original_message(language.get("parent_delete","moderation.delete",language=selector.language_set)) except: try: deleted = await self.bot.bridge.delete_copies(msg_id) await interaction.message.edit(view=components) - return await msg_orig.edit(f'Deleted message ({deleted} copies deleted)') + return await interaction.edit_original_message(language.fget("children_delete","moderation.delete",values={"count": deleted},language=selector.language_set)) except: traceback.print_exc() - await msg_orig.edit(content=f'Something went wrong.') + await interaction.edit_original_message(content=language.get("error","moderation.delete",language=selector.language_set)) elif interaction.data["custom_id"].startswith('rpreview_'): + selector = language.get_selector('moderation.report',userid=interaction.user.id) btns = ui.ActionRow( nextcord.ui.Button( - style=nextcord.ButtonStyle.red, label='Delete message', + style=nextcord.ButtonStyle.red, label=language.get("delete","commons.moderation",language=selector.language_set), custom_id=f'rpdelete_{interaction.data["custom_id"].split("_")[1]}', disabled=True ), nextcord.ui.Button( - style=nextcord.ButtonStyle.green, label='Mark as reviewed', + style=nextcord.ButtonStyle.green, label=selector.get("review"), custom_id=f'rpreview_{interaction.data["custom_id"].split("_")[1]}', disabled=True ) ) @@ -3155,7 +3119,7 @@ async def on_interaction(self, interaction): author = f'@{interaction.user.name}' if not interaction.user.discriminator == '0': author = f'{interaction.user.name}#{interaction.user.discriminator}' - embed.title = f'This report has been reviewed by {author}!' + embed.title = selector.fget("reviewed_notice",values={"moderator": author}) await interaction.response.defer(ephemeral=True, with_message=True) try: thread = interaction.channel.get_thread( @@ -3171,21 +3135,22 @@ async def on_interaction(self, interaction): ) except: try: - await thread.send('This report has been reviewed.') + await thread.send(selector.get("reviewed_thread")) except: pass self.bot.db['report_threads'].pop(str(interaction.message.id)) await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await interaction.message.edit(embed=embed,view=components) - await interaction.edit_original_message(content='Marked report as reviewed!') + await interaction.edit_original_message(content=selector.get('reviewed')) elif interaction.data["custom_id"].startswith('apaccept_') or interaction.data["custom_id"].startswith('apreject_'): + selector = language.get_selector('moderation.appeal',userid=interaction.user.id) btns = ui.ActionRow( nextcord.ui.Button( style=( nextcord.ButtonStyle.gray if interaction.data["custom_id"].startswith('apaccept_') else nextcord.ButtonStyle.red ), - label='Reject', + label=language.get('reject','commons.navigation',language=selector.language_set), disabled=True, emoji=self.bot.ui_emojis.error ), @@ -3194,7 +3159,7 @@ async def on_interaction(self, interaction): nextcord.ButtonStyle.gray if interaction.data["custom_id"].startswith('apreject_') else nextcord.ButtonStyle.green ), - label='Accept & unban', + label=selector.get('accept'), disabled=True, emoji=self.bot.ui_emojis.success ) @@ -3206,10 +3171,9 @@ async def on_interaction(self, interaction): author = f'@{interaction.user.name}' if not interaction.user.discriminator == '0': author = f'{interaction.user.name}#{interaction.user.discriminator}' - embed.title = ( - 'This appeal was ' + - ('accepted' if interaction.data["custom_id"].startswith('apaccept_') else 'rejected') + - f' by {author}!' + embed.title = selector.fget( + "accepted_notice" if interaction.data["custom_id"].startswith('apaccept_') else 'rejected_notice', + values={'moderator': author} ) await interaction.response.defer(ephemeral=True, with_message=True) try: @@ -3226,7 +3190,7 @@ async def on_interaction(self, interaction): ) except: try: - await thread.send('This appeal has been closed.') + await thread.send(selector.get('reviewed_thread')) except: pass self.bot.db['report_threads'].pop(str(interaction.message.id)) @@ -3245,25 +3209,23 @@ async def on_interaction(self, interaction): except: pass results_embed = nextcord.Embed( - title='Your ban appeal was accepted!', - description=( - 'This ban has been removed from your account and will no longer impact your standing.\n'+ - 'You may now continue chatting!' - ), + title=selector.get('accepted_title'), + description=selector.get('accepted_body'), color=self.bot.colors.success ) else: results_embed = nextcord.Embed( - title='Your ban appeal was denied.', - description='You may continue chatting once the current ban expires.', + title=selector.get('rejected_title'), + description=selector.get('rejected_body'), color=self.bot.colors.error ) user = self.bot.get_user(userid) if user: await user.send(embed=results_embed) await interaction.message.edit(embed=embed,view=components) - await interaction.edit_original_message(content='Marked appeal as reviewed!') + await interaction.edit_original_message(content=selector.get('reviewed')) elif interaction.type == nextcord.InteractionType.modal_submit: + selector = language.get_selector('bridge.report',userid=interaction.user.id) if not interaction.data['custom_id']==f'{interaction.user.id}_{interaction.message.id}': # not a report return @@ -3271,15 +3233,14 @@ async def on_interaction(self, interaction): if not interaction.data['components'][1]['components'][0]['value'].lower() == interaction.user.name.lower(): return if context is None or context == '': - context = 'no context given' + context = selector.get('no_context') author = f'@{interaction.user.name}' if not interaction.user.discriminator == '0': author = f'{interaction.user.name}#{interaction.user.discriminator}' try: report = self.bot.reports[f'{interaction.user.id}_{interaction.data["custom_id"]}'] except: - return await interaction.response.send_message('Something went wrong while submitting the report.', - ephemeral=True) + return await interaction.response.send_message(selector.get('failed'), ephemeral=True) await interaction.response.defer(ephemeral=True,with_message=False) cat = report[0] @@ -3292,22 +3253,22 @@ async def on_interaction(self, interaction): if len(content) > 4096: content = content[:-(len(content) - 4096)] embed = nextcord.Embed( - title='Message report - content is as follows', + title=selector.get('report_title'), description=content, color=self.bot.colors.warning, timestamp=datetime.datetime.now(datetime.UTC) ) - embed.add_field(name="Reason", value=f'{cat} => {cat2}', inline=False) - embed.add_field(name='Context', value=context, inline=False) - embed.add_field(name="Sender ID", value=str(msgdata.author_id), inline=False) - embed.add_field(name="Message room", value=roomname, inline=False) - embed.add_field(name="Message ID", value=str(msgid), inline=False) - embed.add_field(name="Reporter ID", value=str(interaction.user.id), inline=False) + embed.add_field(name=language.get('reason','commons.moderation',language=selector.language_set), value=f'{cat} => {cat2}', inline=False) + embed.add_field(name=language.get('context','commons.moderation',language=selector.language_set), value=context, inline=False) + embed.add_field(name=language.get('sender_id','commons.moderation',language=selector.language_set), value=str(msgdata.author_id), inline=False) + embed.add_field(name=language.get('room','commons.moderation',language=selector.language_set), value=roomname, inline=False) + embed.add_field(name=language.get('message_id','commons.moderation',language=selector.language_set), value=str(msgid), inline=False) + embed.add_field(name=language.get('reporter_id','commons.moderation',language=selector.language_set), value=str(interaction.user.id), inline=False) try: - embed.set_footer(text=f'Submitted by {author} - please do not disclose actions taken against the user.', + embed.set_footer(text=selector.fget('submitted_by',values={'username': author}), icon_url=interaction.user.avatar.url) except: - embed.set_footer(text=f'Submitted by {author} - please do not disclose actions taken against the user.') + embed.set_footer(text=selector.fget('submitted_by',values={'username': author})) try: user = self.bot.get_user(userid) if not user: @@ -3325,10 +3286,10 @@ async def on_interaction(self, interaction): ch = guild.get_channel(self.bot.config['reports_channel']) btns = ui.ActionRow( nextcord.ui.Button( - style=nextcord.ButtonStyle.red, label='Delete message', custom_id=f'rpdelete_{msgid}', + style=nextcord.ButtonStyle.red, label=language.get('delete','commons.moderation',language=selector.language_set), custom_id=f'rpdelete_{msgid}', disabled=False), nextcord.ui.Button( - style=nextcord.ButtonStyle.green, label='Mark as reviewed', custom_id=f'rpreview_{msgid}', + style=nextcord.ButtonStyle.green, label=selector.get('review'), custom_id=f'rpreview_{msgid}', disabled=False ) ) @@ -3339,7 +3300,7 @@ async def on_interaction(self, interaction): ) try: thread = await msg.create_thread( - name=f'Discussion: #{msgid}', + name=selector.get('discussion',values={'message_id': msgid}), auto_archive_duration=10080 ) self.bot.db['report_threads'].update({str(msg.id): thread.id}) @@ -3348,15 +3309,14 @@ async def on_interaction(self, interaction): pass self.bot.reports.pop(f'{interaction.user.id}_{interaction.data["custom_id"]}') return await interaction.edit_original_message( - content="# :white_check_mark: Your report was submitted!\nThanks for your report! Our moderators will have a look at it, then decide what to do.\nFor privacy reasons, we will not disclose actions taken against the user.", + content=f'# {self.bot.ui_emojis.success} {selector.get("success_title")}\n{selector.get("success_body")}', view=None ) - @commands.command(hidden=True,description='Initializes new UnifierBridge object.') + @commands.command(hidden=True,description=language.desc("bridge.initbridge")) @restrictions.owner() async def initbridge(self, ctx, *, args=''): - if not ctx.author.id == self.bot.config['owner']: - return + selector = language.get_selector(ctx) msgs = [] prs = {} if 'preserve' in args: @@ -3367,19 +3327,22 @@ async def initbridge(self, ctx, *, args=''): if 'preserve' in args: self.bot.bridge.bridged = msgs self.bot.bridge.prs = prs - await ctx.send('Bridge initialized') + await ctx.send(selector.get("success")) - @commands.command(hidden=True,description='Sends a message as system.') + @commands.command(hidden=True,description=language.desc("bridge.system")) @restrictions.owner() - async def system(self, ctx, room): - ctx.message.content = ctx.message.content.replace(f'{self.bot.command_prefix}system {room}','',1) - await self.bot.bridge.send(room,ctx.message,'discord',system=True) + async def system(self, ctx, room, *, content): + selector = language.get_selector(ctx) + await self.bot.bridge.send(room,ctx.message,'discord',system=True,content_override=content) for platform in self.bot.config['external']: - await self.bot.bridge.send(room, ctx.message, platform, system=True) - await ctx.send('Sent as system') + await self.bot.bridge.send( + room, ctx.message, platform, system=True, + content_override=content) + await ctx.send(selector.get("success")) @commands.Cog.listener() async def on_message(self, message): + selector = language.get_selector("bridge.bridge",userid=message.author.id) if not type(message.channel) is nextcord.TextChannel: return if message.content.startswith(f'{self.bot.command_prefix}system'): @@ -3443,7 +3406,7 @@ async def on_message(self, message): # Optimized logic for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{message.guild.id}' in list(data.keys()): guilddata = data[f'{message.guild.id}'] if len(guilddata) == 1: @@ -3466,7 +3429,7 @@ async def on_message(self, message): for webhook in hooks: index = 0 for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{message.guild.id}' in list(data.keys()): hook_ids = data[f'{message.guild.id}'] else: @@ -3529,18 +3492,18 @@ async def on_message(self, message): public = True embed = nextcord.Embed( - title='Content blocked', - description='Your message was blocked. Moderators may be able to see the blocked content.', + title=selector.get("blocked_title"), + description=selector.get("blocked_body"), color=self.bot.colors.error ) if public: - embed.add_field(name='Reason',value=public_reason if public_reason else '[unknown]',inline=False) + embed.add_field(name=language.get("reason","commons.moderation",language=selector.language_set),value=public_reason if public_reason else '[unknown]',inline=False) await message.channel.send(embed=embed) embed = nextcord.Embed( - title=f'{self.bot.ui_emojis.warning} Content blocked - content is as follows', + title=f'{self.bot.ui_emojis.warning} {selector.get("blocked_report_title")}', description=message.content[:-(len(message.content)-4096)] if len(message.content) > 4096 else message.content, color=self.bot.colors.error, timestamp=datetime.datetime.now(datetime.UTC) @@ -3556,17 +3519,17 @@ async def on_message(self, message): except: plugname = plugin embed.add_field( - name=plugname + f' ({len(responses[plugin]["target"])} users involved)', + name=plugname + f' ({selector.fget("involved",values={"count":len(responses[plugin]["target"])})})', value=responses[plugin]['description'], inline=False ) if len(embed.fields) == 23: break - embed.add_field(name='Punished user IDs', value=' '.join(list(banned.keys())), inline=False) - embed.add_field(name='Message room', value=roomname, inline=False) + embed.add_field(name=selector.get("punished"), value=' '.join(list(banned.keys())), inline=False) + embed.add_field(name=language.get("room","commons.moderation",language=selector.language_set), value=roomname, inline=False) embed.set_footer( - text='This is an automated action performed by a plugin, always double-check before taking action', + text=selector.get("automated"), icon_url=self.bot.user.avatar.url if self.bot.user.avatar else None ) @@ -3580,14 +3543,14 @@ async def on_message(self, message): user_obj = self.bot.get_user(int(user)) if int(user)==self.bot.config['owner']: try: - await user_obj.send('just as a fyi: this would have banned you') + await user_obj.send(selector.get("owner_immunity")) except: pass continue nt = time.time() + banned[user] embed = nextcord.Embed( - title=f'You\'ve been __banned__ by @Unifier (system)!', - description='Automatic action carried out by security plugins', + title=language.fget("ban_title","commons.moderation",values={"moderator": "@Unifier (system)"},language=selector.language_set), + description=selector.get("ban_reason"), color=self.bot.colors.warning, timestamp=datetime.datetime.now(datetime.UTC) ) @@ -3598,22 +3561,22 @@ async def on_message(self, message): if banned[user]==0: embed.colour = self.bot.colors.critical embed.add_field( - name='Actions taken', - value=f'- :zipper_mouth: Your ability to text and speak have been **restricted indefinitely**. This will not automatically expire.\n- :white_check_mark: You must contact a moderator to appeal this restriction.', + name=language.get('actions_taken','commons.moderation',language=selector.language_set), + value=f'- :zipper_mouth: {language.get("perm_ban","commons.moderation",language=selector.language_set)}\n- :white_check_mark: {language.get("perm_ban_appeal","commons.moderation",language=selector.language_set)}', inline=False ) - embed.add_field(name='Did we make a mistake?', - value=f'If you think we didn\'t make the right call, you can always appeal your ban using `{self.bot.command_prefix}!appeal`.', + embed.add_field(name=language.get('appeal_title','commons.moderation',language=selector.language_set), + value=language.get('appeal_body','commons.moderation',language=selector.language_set), inline=False) await self.bot.loop.run_in_executor(None,lambda: self.bot.bridge.add_modlog(0, user_obj.id, 'Automatic action carried out by security plugins', self.bot.user.id)) else: embed.add_field( - name='Actions taken', - value=f'- :warning: You have been **warned**. Further rule violations may lead to sanctions on the Unified Chat global moderators\' discretion.\n- :zipper_mouth: Your ability to text and speak have been **restricted** until . This will expire .', + name=language.get('actions_taken','commons.moderation',language=selector.language_set), + value=f"- :warning: {language.get('warned','commons.moderation',language=selector.language_set)}\n- :zipper_mouth: {language.fget('temp_ban','commons.moderation',values={'unix': round(nt)},language=selector.language_set)}", inline=False ) - embed.add_field(name='Did we make a mistake?', - value=f'Unfortunately, this ban cannot be appealed using `{self.bot.command_prefix}appeal`. You will need to ask moderators for help.', + embed.add_field(name=language.get('appeal_title','commons.moderation',language=selector.language_set), + value=selector.get('cannot_appeal'), inline=False) try: await user_obj.send(embed=embed) @@ -3634,12 +3597,11 @@ async def on_message(self, message): else: if len(message.content) > self.bot.config['restriction_length']: return await message.channel.send( - ('Your server is currently limited for security. The maximum character limit for now is **'+ - self.bot.config["restriction_length"]+' characters**.') + selector.fget("limited_limit",values={'count': self.bot.config['restriction_length']}) ) elif self.bot.bridge.cooldowned[f'{message.author.id}'] < time.time(): return await message.channel.send( - 'Your server is currently limited for security. Please wait before sending another message.' + selector.get("limited_cooldown") ) multisend = True @@ -3676,13 +3638,11 @@ async def on_message(self, message): if not message.channel.permissions_for(message.guild.me).manage_messages: if emojified or is_pr_ref: - return await message.channel.send( - 'Parent message could not be deleted. I may be missing the `Manage Messages` permission.' - ) + return await message.channel.send(selector.get('delete_fail')) if (message.content.lower().startswith('is unifier down') or message.content.lower().startswith('unifier not working')): - await message.channel.send('no',reference=message) + await message.channel.send(selector.get('is_unifier_down'),reference=message) if multisend: # Multisend @@ -3742,16 +3702,16 @@ async def on_message(self, message): if idmatch: if not ids: - return await message.channel.send('Could not get message IDs.') + return await message.channel.send(selector.get('debug_msg_ids_fail')) if parent_id: ids.append(parent_id) if len(list(set(ids)))==1: - await message.channel.send('All IDs match. ID: '+str(ids[0])) + await message.channel.send(selector.fget('debug_msg_ids_match', values={'message_id': str(ids[0])})) else: text = '' for msgid in ids: text = text + f'\n{msgid}' - await message.channel.send('Mismatch detected.'+text) + await message.channel.send(selector.get('debug_msg_ids_mismatch')+text) if not message.author.bot and self.bot.config['enable_exp']: _newexp, levelup = await self.bot.bridge.add_exp(message.author.id) @@ -3759,14 +3719,13 @@ async def on_message(self, message): if levelup: level = self.bot.db['exp'][f'{message.author.id}']['level'] embed = nextcord.Embed( - title=f'Level {level-1} => __Level {level}__', + title=selector.fget('level_progress',values={'previous': level-1, 'new': level}), color=self.bot.colors.blurple ) embed.set_author( - name=( - f'@{message.author.global_name if message.author.global_name else message.author.name} leveled'+ - ' up!' - ), + name=(selector.fget('level_up',values={ + 'username': message.author.global_name if message.author.global_name else message.author.name + })), icon_url=message.author.avatar.url if message.author.avatar else None ) await message.channel.send(embed=embed) @@ -3815,7 +3774,7 @@ async def on_message_edit(self, before, after): # Optimized logic for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{message.guild.id}' in list(data.keys()): guilddata = data[f'{message.guild.id}'] if len(guilddata) == 1: @@ -3838,7 +3797,7 @@ async def on_message_edit(self, before, after): for webhook in hooks: index = 0 for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{message.guild.id}' in list(data.keys()): hook_ids = data[f'{message.guild.id}'] else: @@ -3911,7 +3870,7 @@ async def on_raw_message_edit(self,payload): # Optimized logic for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{message.guild.id}' in list(data.keys()): guilddata = data[f'{message.guild.id}'] if len(guilddata) == 1: @@ -3931,7 +3890,7 @@ async def on_raw_message_edit(self,payload): for webhook in hooks: index = 0 for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{message.guild.id}' in list(data.keys()): hook_ids = data[f'{message.guild.id}'] else: @@ -3960,6 +3919,7 @@ async def on_raw_message_edit(self,payload): @commands.Cog.listener() async def on_message_delete(self, message): + selector = language.get_selector('bridge.bridge',userid=message.author.id) gbans = self.bot.db['banned'] if f'{message.author.id}' in gbans or f'{message.guild.id}' in gbans: @@ -3977,7 +3937,7 @@ async def on_message_delete(self, message): # Optimized logic for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{message.guild.id}' in list(data.keys()): guilddata = data[f'{message.guild.id}'] if len(guilddata) == 1: @@ -4000,7 +3960,7 @@ async def on_message_delete(self, message): for webhook in hooks: index = 0 for key in self.bot.db['rooms']: - data = self.bot.db['rooms'][key] + data = self.bot.db['rooms'][key]['discord'] if f'{message.guild.id}' in list(data.keys()): hook_ids = data[f'{message.guild.id}'] else: @@ -4036,8 +3996,13 @@ async def on_message_delete(self, message): if len(message.content) == 0: content = '[no content]' - embed = nextcord.Embed(title=f'Message deleted from `{roomname}`', description=content) - embed.add_field(name='Embeds', value=f'{len(message.embeds)} embeds, {len(message.attachments)} files', + embed = nextcord.Embed(title=selector.fget('deleted',values={'roomname':roomname}), description=content) + embed.add_field(name='Embeds', + value=selector.fget( + 'embeds',values={'count': len(message.embeds)} + )+', '+selector.fget( + 'files',values={'count': len(message.attachments)} + ), inline=False) embed.add_field(name='IDs', value=f'MSG: {message.id}\nSVR: {message.guild.id}\nUSR: {message.author.id}', inline=False) diff --git a/cogs/config.py b/cogs/config.py index bc23aea4..bf4bd823 100644 --- a/cogs/config.py +++ b/cogs/config.py @@ -1,6 +1,6 @@ """ Unifier - A sophisticated Discord bot uniting servers and platforms -Copyright (C) 2024 Green, ItsAsheer +Copyright (C) 2023-present UnifierHQ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -26,8 +26,6 @@ restrictions = r.Restrictions() - -# noinspection PyUnresolvedReferences class Config(commands.Cog, name=':construction_worker: Config'): """Config is an extension that lets Unifier admins configure the bot and server moderators set up Unified Chat in their server. @@ -59,13 +57,12 @@ def is_user_admin(self,user_id): else: return False except: - print( - "There was an error in 'is_user_admin(id)', for security reasons permission was resulted into denying!") + traceback.print_exc() return False def is_room_restricted(self, room, db): try: - if room in db['restricted']: + if db['rooms'][room]['meta']['restricted']: return True else: return False @@ -75,7 +72,7 @@ def is_room_restricted(self, room, db): def is_room_locked(self, room, db): try: - if room in db['locked']: + if db['rooms'][room]['meta']['locked']: return True else: return False @@ -139,41 +136,28 @@ async def make(self,ctx,*,room): return await ctx.send(f'{self.bot.ui_emojis.error} Room names may only contain alphabets, numbers, dashes, and underscores.') if room in list(self.bot.db['rooms'].keys()): return await ctx.send(f'{self.bot.ui_emojis.error} This room already exists!') - self.bot.db['rooms'].update({room:{}}) - self.bot.db['rules'].update({room:[]}) - await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) + self.bot.bridge.create_room(room) await ctx.send(f'{self.bot.ui_emojis.success} Created room `{room}`!') @commands.command(hidden=True, description='Renames a room.') @restrictions.admin() async def rename(self, ctx, room, newroom): newroom = newroom.lower() + if not room.lower() in list(self.bot.db['rooms'].keys()): + return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') if not bool(re.match("^[A-Za-z0-9_-]*$", newroom)): return await ctx.send(f'{self.bot.ui_emojis.error} Room names may only contain alphabets, numbers, dashes, and underscores.') if newroom in list(self.bot.db['rooms'].keys()): return await ctx.send(f'{self.bot.ui_emojis.error} This room already exists!') self.bot.db['rooms'].update({newroom: self.bot.db['rooms'][room]}) - self.bot.db['rules'].update({newroom: self.bot.db['rules'][room]}) self.bot.db['rooms'].pop(room) - self.bot.db['rules'].pop(room) - if room in self.bot.db['restricted']: - self.bot.db['restricted'].remove(room) - self.bot.db['restricted'].append(newroom) - if room in self.bot.db['locked']: - self.bot.db['locked'].remove(room) - self.bot.db['locked'].append(newroom) - if room in self.bot.db['roomemojis'].keys(): - self.bot.db['roomemojis'].update({newroom: self.bot.db['roomemojis'][room]}) - self.bot.db['roomemojis'].pop(room) - if room in self.bot.db['rooms_revolt'].keys(): - self.bot.db['rooms_revolt'].update({newroom: self.bot.db['rooms_revolt'][room]}) - self.bot.db['rooms_revolt'].pop(room) await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await ctx.send(f'{self.bot.ui_emojis.success} Room renamed!') @commands.command(hidden=True,description='Creates a new experiment.') @restrictions.admin() async def addexperiment(self, ctx, experiment, *, experiment_name): + # maybe i should remove this...? if experiment in list(self.bot.db['experiments'].keys()): return await ctx.send('This experiment already exists!') self.bot.db['experiments'].update({experiment: []}) @@ -200,20 +184,57 @@ async def experimentdesc(self, ctx, experiment, *, experiment_desc): await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await ctx.send(f'Added description to experiment `{experiment}`!') + @commands.command(hidden=True, description='Sets room display name.') + async def roomdisplay(self, ctx, room, *, name=''): + room = room.lower() + if not room in list(self.bot.db['rooms'].keys()): + return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') + if not self.bot.db['rooms'][room]['meta']['private'] and not ctx.author.id in self.bot.admins: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage public rooms.') + if self.bot.db['rooms'][room]['meta']['private'] and not ctx.author.id in self.bot.moderators and not ctx.author.id in self.bot.admins: + origin_id = self.bot.db['rooms'][room]['meta']['private_meta']['server'] + origin_guild = self.bot.get_guild(origin_id) + if not origin_guild or not origin_guild.id == ctx.guild.id: + if ctx.guild.id in self.bot.db['rooms']['room']['private_meta']['allowed']: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage this private room.') + # pretend like it doesn't exist, since there's no way this server can have access to the room + return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') + if not ctx.author.guild_permissions.manage_guild: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage this private room.') + if len(name) == 0: + if not self.bot.db['rooms'][room]['meta']['display_name']: + return await ctx.send(f'{self.bot.ui_emojis.error} There is no display name to reset for this room.') + self.bot.db['rooms'][room]['meta']['display_name'] = None + await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) + return await ctx.send(f'{self.bot.ui_emojis.success} Display name removed.') + self.bot.db['rooms'][room]['meta']['display_name'] = name + await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) + await ctx.send(f'{self.bot.ui_emojis.success} Updated description!') + @commands.command(hidden=True,description='Sets room description.') - @restrictions.admin() async def roomdesc(self,ctx,room,*,desc=''): room = room.lower() if not room in list(self.bot.db['rooms'].keys()): return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') + if not self.bot.db['rooms'][room]['meta']['private'] and not ctx.author.id in self.bot.admins: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage public rooms.') + if self.bot.db['rooms'][room]['meta']['private'] and not ctx.author.id in self.bot.moderators and not ctx.author.id in self.bot.admins: + origin_id = self.bot.db['rooms'][room]['meta']['private_meta']['server'] + origin_guild = self.bot.get_guild(origin_id) + if not origin_guild or not origin_guild.id == ctx.guild.id: + if ctx.guild.id in self.bot.db['rooms']['room']['private_meta']['allowed']: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage this private room.') + # pretend like it doesn't exist, since there's no way this server can have access to the room + return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') + if not ctx.author.guild_permissions.manage_guild: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage this private room.') if len(desc)==0: - try: - self.bot.db['descriptions'][room].pop() - except: + if not self.bot.db['rooms'][room]['meta']['description']: return await ctx.send(f'{self.bot.ui_emojis.error} There is no description to reset for this room.') + self.bot.db['rooms'][room]['meta']['description'] = None await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) return await ctx.send(f'{self.bot.ui_emojis.success} Description removed.') - self.bot.db['descriptions'].update({room:desc}) + self.bot.db['rooms'][room]['meta']['description'] = desc await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await ctx.send(f'{self.bot.ui_emojis.success} Updated description!') @@ -223,53 +244,144 @@ async def roomemoji(self, ctx, room, *, emoji=''): room = room.lower() if not room in list(self.bot.db['rooms'].keys()): return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') + if not self.bot.db['rooms'][room]['meta']['private'] and not ctx.author.id in self.bot.admins: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage public rooms.') + if self.bot.db['rooms'][room]['meta']['private'] and not ctx.author.id in self.bot.moderators and not ctx.author.id in self.bot.admins: + origin_id = self.bot.db['rooms'][room]['meta']['private_meta']['server'] + origin_guild = self.bot.get_guild(origin_id) + if not origin_guild or not origin_guild.id == ctx.guild.id: + if ctx.guild.id in self.bot.db['rooms']['room']['private_meta']['allowed']: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage this private room.') + # pretend like it doesn't exist, since there's no way this server can have access to the room + return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') + if not ctx.author.guild_permissions.manage_guild: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage this private room.') if len(emoji) == 0: - try: - self.bot.db['roomemojis'].pop(room) - except: + if not self.bot.db['rooms'][room]['meta']['emoji']: return await ctx.send(f'{self.bot.ui_emojis.error} There is no emoji to reset for this room.') + self.bot.db['rooms'][room]['meta']['emoji'] = None await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) return await ctx.send(f'{self.bot.ui_emojis.success} Emoji removed.') if not pymoji.is_emoji(emoji): return await ctx.send(f'{self.bot.ui_emojis.error} This is not a valid emoji.') - self.bot.db['roomemojis'].update({room: emoji}) + self.bot.db['rooms'][room]['meta']['emoji'] = emoji await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await ctx.send('Updated emoji!') @commands.command( hidden=True, - description='Restricts/unrestricts room. Only admins will be able to collect to this room when restricted.' + description='Restricts/unrestricts a room. Only admins will be able to collect to this room when restricted.' ) @restrictions.admin() - async def roomrestrict(self,ctx,room): + async def restrict(self,ctx,room): room = room.lower() if not room in list(self.bot.db['rooms'].keys()): return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') - if room in self.bot.db['restricted']: - self.bot.db['restricted'].remove(room) + if self.bot.db['rooms'][room]['meta']['private']: + return await ctx.send(f'{self.bot.ui_emojis.error} Private rooms cannot be restricted.') + if self.bot.db['rooms'][room]['meta']['restricted']: + self.bot.db['rooms'][room]['meta']['restricted'] = False + await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await ctx.send(f'{self.bot.ui_emojis.success} Unrestricted `{room}`!') else: - self.bot.db['restricted'].append(room) + self.bot.db['rooms'][room]['meta']['restricted'] = True + await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await ctx.send(f'{self.bot.ui_emojis.success} Restricted `{room}`!') - await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) @commands.command( hidden=True, description='Locks/unlocks a room. Only moderators and admins will be able to chat in this room when locked.' ) - @restrictions.admin() - async def roomlock(self,ctx,room): + @restrictions.moderator() + async def lock(self,ctx,room): room = room.lower() if not room in list(self.bot.db['rooms'].keys()): return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') - if room in self.bot.db['locked']: - self.bot.db['locked'].remove(room) + if not self.bot.db['rooms'][room]['meta']['private'] and not ctx.author.id in self.bot.admins: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage public rooms.') + if self.bot.db['rooms'][room]['meta']['locked']: + self.bot.db['rooms'][room]['meta']['locked'] = False await ctx.send(f'{self.bot.ui_emojis.success} Unlocked `{room}`!') else: - self.bot.db['locked'].append(room) + self.bot.db['rooms'][room]['meta']['locked'] = True await ctx.send(f'{self.bot.ui_emojis.success} Locked `{room}`!') await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) + @commands.command(description='Disbands a room.') + async def disband(self, ctx, room): + room = room.lower() + if not room in list(self.bot.db['rooms'].keys()): + return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') + if not self.bot.db['rooms'][room]['meta']['private'] and not ctx.author.id in self.bot.admins: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage public rooms.') + if self.bot.db['rooms'][room]['meta']['private'] and not ctx.author.id in self.bot.moderators and not ctx.author.id in self.bot.admins: + origin_id = self.bot.db['rooms'][room]['meta']['private_meta']['server'] + origin_guild = self.bot.get_guild(origin_id) + if not origin_guild or not origin_guild.id==ctx.guild.id: + if ctx.guild.id in self.bot.db['rooms']['room']['private_meta']['allowed']: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage this private room.') + # pretend like it doesn't exist, since there's no way this server can have access to the room + return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') + if not ctx.author.guild_permissions.manage_guild: + return await ctx.send(f'{self.bot.ui_emojis.error} You cannot manage this private room.') + embed = nextcord.Embed( + title=f'{self.bot.ui_emojis.warning} Disband `{room}`?', + description='Once the room is disbanded, it\'s gone forever!', + color=self.bot.colors.warning + ) + view = ui.MessageComponents() + view.add_row( + ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.red, + label='Disband', + custom_id='disband' + ), + nextcord.ui.Button( + style=nextcord.ButtonStyle.gray, + label='Cancel', + custom_id='cancel' + ) + ) + ) + msg = await ctx.send(embed=embed, view=view) + view.clear_items() + view.row_count = 0 + view.add_row( + ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.red, + label='Disband', + custom_id='disband', + disabled=True + ), + nextcord.ui.Button( + style=nextcord.ButtonStyle.gray, + label='Cancel', + custom_id='cancel', + disabled=True + ) + ) + ) + + def check(interaction): + return interaction.message.id == msg.id and interaction.user.id == ctx.author.id + + try: + interaction = await self.bot.wait_for('interaction',check=check,timeout=60) + except: + return await msg.edit(view=view) + + if interaction.data['custom_id'] == 'cancel': + return await interaction.response.edit_message(view=view) + + self.bot.db['rooms'].pop(room) + embed.title = f'{self.bot.ui_emojis.success} Disbanded `{room}`' + embed.description = 'The room was disbanded successfully.' + embed.colour = self.bot.colors.success + await interaction.response.edit_message(embed=embed,view=None) + await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) + @commands.command( aliases=['experiment'],description='Shows a list of Unifier experiments, and lets you join or leave them.' ) @@ -311,123 +423,40 @@ async def experiments(self,ctx,action='',experiment=''): if len(list(self.bot.db['experiments'].keys()))==0: embed.add_field(name="no experiments? :face_with_raised_eyebrow:",value='There\'s no experiments available yet!',inline=False) await ctx.send(embed=embed) - + @commands.command(aliases=['link','connect','federate','bridge'],description='Connects the channel to a given room.') - @commands.has_permissions(manage_channels=True) async def bind(self,ctx,*,room=''): - room = room.lower() - if self.is_room_restricted(room,self.bot.db) and not self.is_user_admin(ctx.author.id): - return await ctx.send(f'{self.bot.ui_emojis.error} Only admins can bind channels to restricted rooms.') - if room=='' or not room: # Added "not room" as a failback - room = 'main' - await ctx.send(f'{self.bot.ui_emojis.warning} No room was given, defaulting to main') - try: - data = self.bot.db['rooms'][room] - except: - return await ctx.send(f'{self.bot.ui_emojis.error} This isn\'t a valid room. Run `{self.bot.command_prefix}rooms` for a full list of rooms.') - embed = nextcord.Embed( - title=f'{self.bot.ui_emojis.warning} Ensuring channel is not connected...', - description='This may take a while.', - color=self.bot.colors.warning - ) - msg = await ctx.send(embed=embed) - hooks = await ctx.channel.webhooks() - for roomname in list(self.bot.db['rooms'].keys()): - # Prevent duplicate binding - try: - hook_id = self.bot.db['rooms'][roomname][f'{ctx.guild.id}'][0] - except: - continue - for hook in hooks: - if hook.id == hook_id: - embed.title = f'{self.bot.ui_emojis.error} Channel already linked!' - embed.colour = self.bot.colors.error - embed.description = f'This channel is already linked to `{roomname}`!\nRun `{self.bot.command_prefix}unbind {roomname}` to unbind from it.' - return await msg.edit(embed=embed) try: - guild = data[f'{ctx.guild.id}'] + roominfo = self.bot.bridge.get_room(room.lower()) except: - guild = [] - if len(guild) >= 1: - return await ctx.send(f'Your server is already linked to this room.\n**Accidentally deleted the webhook?** `{self.bot.command_prefix}unlink` it then `{self.bot.command_prefix}link` it back.') - index = 0 - text = '' - if len(self.bot.db['rules'][room])==0: - text = f'No rules exist yet for this room! For now, follow the main room\'s rules.\nYou can always view rules if any get added using `{self.bot.command_prefix}rules {room}`.' - else: - for rule in self.bot.db['rules'][room]: - if text=='': - text = f'1. {rule}' - else: - text = f'{text}\n{index}. {rule}' - index += 1 - text = f'{text}\n\nPlease display these rules somewhere accessible.\nThis message will be automatically pinned if you accept these rules.' + roominfo = await self.bot.bridge.accept_invite(room.lower()) + text = [] + for i in range(len(roominfo['rules'])): + text.append(f'{i+1}. '+roominfo['rules'][i]) + text = '\n'.join(text) + embed = nextcord.Embed( - title=f'{self.bot.ui_emojis.rooms} Please agree to the room rules first:', - description=text, + title=f'{self.bot.ui_emojis.rooms} Join {roominfo['meta']['display_name'] or room.lower()}?', + description=(f'{text}\n\nBy joining this room, you and your members agree to these rules.\n'+ + 'This message will be pinned (if possible) for better accessibility to the rules.' + ), color=self.bot.colors.unifier ) - embed.set_footer(text='Failure to follow room rules may result in user or server restrictions.') - btns = ui.ActionRow( - nextcord.ui.Button( - style=nextcord.ButtonStyle.green, label='Accept and bind', custom_id=f'accept',disabled=False - ), - nextcord.ui.Button( - style=nextcord.ButtonStyle.red, label='No thanks', custom_id=f'reject',disabled=False - ) - ) - components = ui.MessageComponents() - components.add_row(btns) - await msg.edit(embed=embed,view=components) - - def check(interaction): - return interaction.user.id==ctx.author.id and ( - interaction.data['custom_id']=='accept' or - interaction.data['custom_id']=='reject' - ) and interaction.channel.id==ctx.channel.id - - try: - resp = await self.bot.wait_for("interaction", check=check, timeout=60.0) - except: - btns.items[0].disabled = True - btns.items[1].disabled = True - components = ui.MessageComponents() - components.add_row(btns) - await msg.edit(view=components) - return await ctx.send('Timed out.') - btns.items[0].disabled = True - btns.items[1].disabled = True - components = ui.MessageComponents() - components.add_row(btns) - await msg.edit(view=components) - await resp.response.defer(with_message=True) - if resp.data['custom_id']=='reject': - return - webhook = await ctx.channel.create_webhook(name='Unifier Bridge') - guild = [webhook.id, ctx.channel.id] - self.bot.db['rooms'][room].update({f'{ctx.guild.id}':guild}) - await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) - await resp.edit_original_message(content=f'# {self.bot.ui_emojis.success} Linked channel to Unifier network!\nYou can now send messages to the Unifier network through this channel. Say hi!') - try: - await msg.pin() - except: - pass + embed.set_footer(text='Failure to follow room rules may lead to user or server restrictions.') @commands.command(aliases=['unlink','disconnect'],description='Disconnects the server from a given room.') @commands.has_permissions(manage_channels=True) - async def unbind(self,ctx,*,room=''): - if room=='': - return await ctx.send(f'{self.bot.ui_emojis.error} You must specify the room to unbind from.') + async def unbind(self,ctx,*,room): room = room.lower() try: - data = self.bot.db['rooms'][room] + data = self.bot.db['rooms'][room]['discord'] except: return await ctx.send(f'{self.bot.ui_emojis.error} This isn\'t a valid room. Run `{self.bot.command_prefix}rooms` for a full list of rooms.') try: try: hooks = await ctx.guild.webhooks() except: - return await ctx.send('I cannot manage webhooks.') + return await ctx.send(f'{self.bot.ui_emojis.error} I cannot manage webhooks.') if f'{ctx.guild.id}' in list(data.keys()): hook_ids = data[f'{ctx.guild.id}'] else: @@ -437,7 +466,7 @@ async def unbind(self,ctx,*,room=''): await webhook.delete() break data.pop(f'{ctx.guild.id}') - self.bot.db['rooms'][room] = data + self.bot.db['rooms'][room]['discord'] = data await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await ctx.send(f'# {self.bot.ui_emojis.success} Unlinked channel from Unifier network!\nThis channel is no longer linked, nothing from now will be bridged.') except: @@ -460,7 +489,7 @@ async def map(self, ctx): for roomname in list(self.bot.db['rooms'].keys()): # Prevent duplicate binding try: - hook_id = self.bot.db['rooms'][roomname][f'{ctx.guild.id}'][0] + hook_id = self.bot.db['rooms'][roomname]['discord'][f'{ctx.guild.id}'][0] except: continue for hook in hooks: @@ -476,7 +505,7 @@ async def map(self, ctx): continue namelist.append(roomname) try: - if len(self.bot.db['rooms'][roomname][f'{ctx.guild.id}']) >= 1: + if len(self.bot.db['rooms'][roomname]['discord'][f'{ctx.guild.id}']) >= 1: continue except: pass @@ -489,6 +518,8 @@ async def map(self, ctx): break interaction = None + restricted = False + locked = False while True: text = '' for channel in channels_enabled: @@ -526,6 +557,39 @@ async def map(self, ctx): selection ), ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.gray, + label='Select first 10' if len(channels) > 10 else 'Select all', + custom_id='selectall' + ), + nextcord.ui.Button( + style=nextcord.ButtonStyle.gray, + label='Deselect all', + custom_id='deselect' + ) + ), + ui.ActionRow( + nextcord.ui.Button( + style=nextcord.ButtonStyle.green, + label='Bind', + custom_id='bind' + ), + nextcord.ui.Button( + style=nextcord.ButtonStyle.blurple, + label='Bind (create as restricted)', + custom_id='bind_restricted' + ), + nextcord.ui.Button( + style=nextcord.ButtonStyle.blurple, + label='Bind (create as locked)', + custom_id='bind_locked' + ), + nextcord.ui.Button( + style=nextcord.ButtonStyle.red, + label='Cancel', + custom_id='cancel' + ) + ) if ctx.author.id in self.bot.admins else ui.ActionRow( nextcord.ui.Button( style=nextcord.ButtonStyle.green, label='Bind', @@ -554,26 +618,43 @@ def check(interaction): except: return await msg.edit(view=ui.MessageComponents()) - if interaction.data['custom_id']=='bind': + if interaction.data['custom_id'].startswith('bind'): await msg.edit(embed=embed, view=ui.MessageComponents()) await interaction.response.defer(with_message=True) + if 'restricted' in interaction.data['custom_id']: + restricted = True + elif 'locked' in interaction.data['custom_id']: + locked = True break - if interaction.data['custom_id']=='selection': + elif interaction.data['custom_id']=='selection': channels_enabled = [] for value in interaction.data['values']: channel = self.bot.get_channel(int(value)) channels_enabled.append(channel) + elif interaction.data['custom_id']=='selectall': + channels_enabled = [] + for channel in channels: + channels_enabled.append(channel) + if len(channels_enabled) >= 10: + break + elif interaction.data['custom_id'] == 'deselect': + channels_enabled = [] for channel in channels_enabled: roomname = re.sub(r'[^a-zA-Z0-9_-]', '', channel.name).lower() if len(roomname) < 3: roomname = str(channel.id) if not roomname in self.bot.db['rooms'].keys(): - self.bot.db['rooms'].update({roomname: {}}) - self.bot.db['rules'].update({roomname: []}) + self.bot.bridge.create_room(roomname) + if restricted: + self.bot.db['rooms'][roomname]['meta']['restricted'] = True + elif locked: + self.bot.db['rooms'][roomname]['meta']['locked'] = True webhook = await channel.create_webhook(name='Unifier Bridge') guild = [webhook.id, channel.id] - self.bot.db['rooms'][roomname].update({f'{ctx.guild.id}': guild}) + if not 'discord' in self.bot.db['rooms'][roomname].keys(): + self.bot.db['rooms'][roomname].update({'discord': {}}) + self.bot.db['rooms'][roomname]['discord'].update({f'{ctx.guild.id}': guild}) await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await interaction.edit_original_message( @@ -589,11 +670,11 @@ async def rules(self,ctx,*,room=''): if not room in list(self.bot.db['rooms'].keys()): return await ctx.send(f'{self.bot.ui_emojis.error} This isn\'t a valid room. Run `{self.bot.command_prefix}rooms` for a full list of rooms.') - + index = 0 text = '' - if room in list(self.bot.db['rules'].keys()): - rules = self.bot.db['rules'][room] + if room in list(self.bot.db['rooms'].keys()): + rules = self.bot.db['rooms'][room]['meta']['rules'] if len(rules)==0: return await ctx.send(f'{self.bot.ui_emojis.error} The admins haven\'t added rules yet. Though, make sure to always use common sense.') else: @@ -612,11 +693,11 @@ async def rules(self,ctx,*,room=''): @restrictions.admin() async def addrule(self,ctx,room,*,rule): room = room.lower() - if not room in list(self.bot.db['rules'].keys()): + if not room in list(self.bot.db['rooms'].keys()): return await ctx.send(f'{self.bot.ui_emojis.error} This isn\'t a valid room. Run `{self.bot.command_prefix}rooms` for a full list of rooms.') - if len(self.bot.db['rules'][room]) >= 25: + if len(self.bot.db['rooms'][room]['meta']['rules']) >= 25: return await ctx.send(f'{self.bot.ui_emojis.error} You can only have up to 25 rules in a room!') - self.bot.db['rules'][room].append(rule) + self.bot.db['rooms'][room]['meta']['rules'].append(rule) await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await ctx.send(f'{self.bot.ui_emojis.success} Added rule!') @@ -630,9 +711,9 @@ async def delrule(self,ctx,room,*,rule): raise ValueError() except: return await ctx.send(f'{self.bot.ui_emojis.error} Rule must be a number higher than 0.') - if not room in list(self.bot.db['rules'].keys()): + if not room in list(self.bot.db['rooms'].keys()): return await ctx.send(f'{self.bot.ui_emojis.error} This isn\'t a valid room. Run `{self.bot.command_prefix}rooms` for a full list of rooms.') - self.bot.db['rules'][room].pop(rule-1) + self.bot.db['rooms'][room]['meta']['rules'].pop(rule-1) await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) await ctx.send(f'{self.bot.ui_emojis.success} Removed rule!') @@ -770,18 +851,22 @@ async def rooms(self,ctx): if index >= len(roomlist): break name = roomlist[index] + display_name = ( + self.bot.db['rooms'][name]['meta']['display_name'] if self.bot.db['rooms'][name]['meta']['display_name'] + else name + ) description = ( - self.bot.db['descriptions'][roomlist[index]] - if roomlist[index] in self.bot.db['descriptions'].keys() else 'This room has no description.' + self.bot.db['rooms'][name]['meta']['description'] + if self.bot.db['rooms'][name]['meta']['description'] else 'This room has no description.' ) emoji = ( '\U0001F527' if self.is_room_restricted(roomlist[index],self.bot.db) else '\U0001F512' if self.is_room_locked(roomlist[index],self.bot.db) else '\U0001F310' - ) if not name in self.bot.db['roomemojis'] else self.bot.db['roomemojis'][name] + ) if not self.bot.db['rooms'][name]['meta']['emoji'] else self.bot.db['rooms'][name]['meta']['emoji'] embed.add_field( - name=f'{emoji} `{name}`', + name=f'{emoji} `{display_name}`', value=description, inline=False ) @@ -839,18 +924,24 @@ async def rooms(self,ctx): def search_filter(query, query_cmd): if match == 0: return ( - query.lower() in query_cmd and namematch or ( - query.lower() in self.bot.db['descriptions'][query_cmd].lower() - if query_cmd in self.bot.db['descriptions'].keys() else False + query.lower() in query_cmd or + query.lower() in self.bot.db['rooms'][query_cmd]['meta']['display_name'] + ) and namematch or + ( + query.lower() in self.bot.db['rooms'][query_cmd]['meta']['description'].lower() + if self.bot.db['rooms'][query_cmd]['meta']['description'] else False ) and descmatch ) elif match == 1: return ( - ((query.lower() in query_cmd and namematch) or not namematch) and + ((( + query.lower() in query_cmd or + query.lower() in self.bot.db['rooms'][query_cmd]['meta']['display_name'] + ) and namematch) or not namematch) and (( - query.lower() in self.bot.db['descriptions'][query_cmd].lower() - if query_cmd in self.bot.db['descriptions'].keys() else False + query.lower() in self.bot.db['rooms'][query_cmd]['meta']['description'].lower() + if self.bot.db['rooms'][query_cmd]['meta']['description'] else False ) and descmatch or not descmatch) ) @@ -896,22 +987,26 @@ def search_filter(query, query_cmd): if index >= len(roomlist): break room = roomlist[index] + display_name = ( + self.bot.db['rooms'][room]['meta']['display_name'] if self.bot.db['rooms'][room]['meta']['display_name'] + else room + ) emoji = ( '\U0001F527' if self.is_room_restricted(roomlist[index], self.bot.db) else '\U0001F512' if self.is_room_locked(roomlist[index], self.bot.db) else '\U0001F310' - ) if not room in self.bot.db['roomemojis'] else self.bot.db['roomemojis'][room] + ) if not self.bot.db['rooms'][room]['meta']['emoji'] else self.bot.db['rooms'][room]['meta']['emoji'] roomdesc = ( - self.bot.db['descriptions'][room] if room in self.bot.db['descriptions'].keys() else - 'This room has no description.' + self.bot.db['rooms'][room]['meta']['description'] + if self.bot.db['rooms'][room]['meta']['description'] else 'This room has no description.' ) embed.add_field( - name=f'{emoji} `{room}`', + name=f'{emoji} `{display_name}`', value=roomdesc, inline=False ) selection.add_option( - label=room, + label=display_name, description=roomdesc if len(roomdesc) <= 100 else roomdesc[:-(len(roomdesc) - 97)] + '...', value=room, emoji=emoji @@ -1003,16 +1098,20 @@ def search_filter(query, query_cmd): if was_searching else f'{self.bot.ui_emojis.rooms} {self.bot.user.global_name or self.bot.user.name} rooms / {roomname}' ) + display_name = ( + self.bot.db['rooms'][roomname]['meta']['display_name'] if self.bot.db['rooms'][roomname]['meta']['display_name'] + else roomname + ) description = ( - self.bot.db['descriptions'][roomname] - if roomname in self.bot.db['descriptions'].keys() else 'This room has no description.' + self.bot.db['rooms'][roomname]['meta']['description'] + if self.bot.db['rooms'][roomname]['meta']['description'] else 'This room has no description.' ) emoji = ( '\U0001F527' if self.is_room_restricted(roomname, self.bot.db) else '\U0001F512' if self.is_room_locked(roomname, self.bot.db) else '\U0001F310' - ) if not roomname in self.bot.db['roomemojis'] else self.bot.db['roomemojis'][roomname] - embed.description = f'# **{emoji} `{roomname}`**\n{description}' + ) if not self.bot.db['rooms'][roomname]['meta']['emoji'] else self.bot.db['rooms'][roomname]['meta']['emoji'] + embed.description = f'# **{emoji} `{display_name}`**\n{description}' stats = await self.bot.bridge.roomstats(roomname) embed.add_field(name='Statistics',value=( f':homes: {stats["guilds"]} servers\n'+ @@ -1132,6 +1231,7 @@ def check(interaction): namematch = True descmatch = True match = 0 + page = 0 @commands.command(description='Enables or disables usage of server emojis as Global Emojis.') @commands.has_permissions(manage_guild=True) @@ -1153,7 +1253,7 @@ async def avatar(self,ctx,*,url=''): avurl = self.bot.db['avatars'][f'{ctx.author.id}'] desc = f'You have a custom avatar! Run `{self.bot.command_prefix}avatar ` to change it, or run `{self.bot.command_prefix}avatar remove` to remove it.' else: - desc = f'You have a default avatar! Run `{self.bot.command_prefix}avatar ` to set a custom one for UniChat.' + desc = f'You have a default avatar! Run `{self.bot.command_prefix}avatar ` to set a custom one.' avurl = ctx.author.avatar.url except: avurl = None diff --git a/cogs/lockdown.py b/cogs/lockdown.py index cd07a46d..26e769c8 100644 --- a/cogs/lockdown.py +++ b/cogs/lockdown.py @@ -1,6 +1,6 @@ """ Unifier - A sophisticated Discord bot uniting servers and platforms -Copyright (C) 2024 Green, ItsAsheer +Copyright (C) 2023-present UnifierHQ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -18,9 +18,11 @@ import nextcord from nextcord.ext import commands -from utils import log, ui, restrictions as r +from utils import log, ui, langmgr, restrictions as r restrictions = r.Restrictions() +language = langmgr.partial() +language.load() class Lockdown(commands.Cog, name=':lock: Lockdown'): """An emergency extension that unloads literally everything. @@ -28,38 +30,41 @@ class Lockdown(commands.Cog, name=':lock: Lockdown'): Developed by Green and ItsAsheer""" def __init__(self,bot): + global language self.bot = bot + language = self.bot.langmgr restrictions.attach_bot(self.bot) if not hasattr(self.bot, "locked"): self.bot.locked = False self.logger = log.buildlogger(self.bot.package,'admin',self.bot.loglevel) - @commands.command(hidden=True,aliases=['globalkill'],description='Locks the entire bot down.') + @commands.command(hidden=True,aliases=['globalkill'],description=language.desc('lockdown.lockdown')) @restrictions.owner() async def lockdown(self,ctx): + selector = language.get_selector(ctx) if self.bot.locked: - return await ctx.send('Bot is already locked down.') + return await ctx.send(selector.get('already_locked')) embed = nextcord.Embed( - title='Activate lockdown?', - description='This will unload ALL EXTENSIONS and lock down the bot until next restart. Continue?', + title=selector.get('warning_title'), + description=selector.get('warning_body'), color=self.bot.colors.error ) btns = ui.ActionRow( nextcord.ui.Button( - style=nextcord.ButtonStyle.red, label='Continue', custom_id=f'accept', disabled=False + style=nextcord.ButtonStyle.red, label=selector.get('continue'), custom_id=f'accept', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.gray, label='Cancel', custom_id=f'reject', disabled=False + style=nextcord.ButtonStyle.gray, label=language.get('cancel','commons.navigation'), custom_id=f'reject', disabled=False ) ) components = ui.MessageComponents() components.add_row(btns) btns2 = ui.ActionRow( nextcord.ui.Button( - style=nextcord.ButtonStyle.red, label='Continue', custom_id=f'accept', disabled=True + style=nextcord.ButtonStyle.red, label=selector.get('continue'), custom_id=f'accept', disabled=True ), nextcord.ui.Button( - style=nextcord.ButtonStyle.gray, label='Cancel', custom_id=f'reject', disabled=True + style=nextcord.ButtonStyle.gray, label=language.get('cancel','commons.navigation'), custom_id=f'reject', disabled=True ) ) components_cancel = ui.MessageComponents() @@ -75,8 +80,8 @@ def check(interaction): return await msg.edit(view=components_cancel) if interaction.data['custom_id']=='reject': return await interaction.response.edit_message(view=components_cancel) - embed.title = ':warning: FINAL WARNING!!! :warning:' - embed.description = '- :warning: All functions of the bot will be disabled.\n- :no_entry_sign: Managing extensions will be unavailable.\n- :arrows_counterclockwise: To restore the bot, a reboot is required.' + embed.title = f':warning: {selector.get("fwarning_title")} :warning:' + embed.description = f'- :warning: {selector.get("fwarning_functions")}\n- :no_entry_sign: {selector.get("fwarning_management")}\n- :arrows_counterclockwise: {selector.get("fwarning_reboot")}' embed.colour = self.bot.colors.critical await interaction.response.edit_message(embed=embed) try: @@ -122,9 +127,9 @@ def check(interaction): self.bot.unload_extension(cog) self.logger.info("Lockdown complete") - embed.title = 'Lockdown activated' - embed.description = 'The bot is now in a crippled state. It cannot recover without a reboot.' + embed.title = selector.get("success_title") + embed.description = selector.get("success_body") return await interaction.response.edit_message(embed=embed,view=components_cancel) def setup(bot): - bot.add_cog(Lockdown(bot)) \ No newline at end of file + bot.add_cog(Lockdown(bot)) diff --git a/cogs/moderation.py b/cogs/moderation.py index 814f439e..c8a9f00d 100644 --- a/cogs/moderation.py +++ b/cogs/moderation.py @@ -1,6 +1,6 @@ """ Unifier - A sophisticated Discord bot uniting servers and platforms -Copyright (C) 2024 Green, ItsAsheer +Copyright (C) 2023-present UnifierHQ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -95,9 +95,9 @@ def __init__(self,bot): self.logger = log.buildlogger(self.bot.package, 'upgrader', self.bot.loglevel) restrictions.attach_bot(self.bot) - @commands.command(aliases=['ban'],description='Blocks a user or server from bridging messages to your server.') + @commands.command(aliases=['block'],description='Blocks a user or server from bridging messages to your server.') @commands.has_permissions(ban_members=True) - async def restrict(self,ctx,*,target): + async def ban(self,ctx,*,target): try: userid = int(target.replace('<@','',1).replace('!','',1).replace('>','',1)) if userid==ctx.author.id: @@ -240,7 +240,6 @@ async def globalban(self, ctx, target, duration=None, *, reason=None): pass content = ctx.message.content - ctx.message.content = '' embed = nextcord.Embed(description='A user was recently banned from Unifier!',color=self.bot.colors.error) if disclose: if not user: @@ -256,9 +255,9 @@ async def globalban(self, ctx, target, duration=None, *, reason=None): ctx.message.embeds = [embed] if not discreet: - await self.bot.bridge.send("main", ctx.message, 'discord', system=True) + await self.bot.bridge.send("main", ctx.message, 'discord', system=True, content_override='') for platform in externals: - await self.bot.bridge.send("main", ctx.message, platform, system=True) + await self.bot.bridge.send("main", ctx.message, platform, system=True, content_override='') ctx.message.embeds = [] ctx.message.content = content @@ -391,9 +390,9 @@ async def fullban(self,ctx,target): self.bot.db['fullbanned'].append(target) await ctx.send(f'{self.bot.ui_emojis.success} User has been banned from the bot.') - @commands.command(aliases=['unban'],description='Unblocks a user or server from bridging messages to your server.') + @commands.command(aliases=['unblock'],description='Unblocks a user or server from bridging messages to your server.') @commands.has_permissions(ban_members=True) - async def unrestrict(self,ctx,target): + async def unban(self,ctx,target): try: userid = int(target.replace('<@','',1).replace('!','',1).replace('>','',1)) except: @@ -584,6 +583,25 @@ def check(interaction): ephemeral=True ) + @commands.command( + hidden=True, + description='Locks/unlocks a private room. Only moderators and admins will be able to chat in this room when locked.' + ) + @restrictions.moderator() + async def lock(self,ctx,room): + room = room.lower() + if not room in list(self.bot.db['rooms'].keys()): + return await ctx.send(f'{self.bot.ui_emojis.error} This room does not exist!') + if not self.bot.db['rooms'][room]['meta']['private']: + return await ctx.send(f'{self.bot.ui_emojis.error} This is a public room.') + if self.bot.db['rooms'][room]['meta']['locked']: + self.bot.db['rooms'][room]['meta']['locked'] = False + await ctx.send(f'{self.bot.ui_emojis.success} Unlocked `{room}`!') + else: + self.bot.db['rooms'][room]['meta']['locked'] = True + await ctx.send(f'{self.bot.ui_emojis.success} Locked `{room}`!') + await self.bot.loop.run_in_executor(None, lambda: self.bot.db.save_data()) + @commands.command(aliases=['account_standing'],description='Shows your instance account standing.') async def standing(self,ctx,*,target=None): if target and not ctx.author.id in self.bot.moderators: diff --git a/cogs/sysmgr.py b/cogs/sysmgr.py index e9a3169d..0e06c17e 100644 --- a/cogs/sysmgr.py +++ b/cogs/sysmgr.py @@ -1,6 +1,6 @@ """ Unifier - A sophisticated Discord bot uniting servers and platforms -Copyright (C) 2024 Green, ItsAsheer +Copyright (C) 2023-present UnifierHQ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -32,7 +32,7 @@ import inspect import textwrap from contextlib import redirect_stdout -from utils import log, ui, restrictions as r +from utils import log, ui, langmgr, restrictions as r import logging import ujson as json import os @@ -59,6 +59,8 @@ import datetime restrictions = r.Restrictions() +language = langmgr.partial() +language.load() # Below are attributions to the works we used to build Unifier (including our own). # If you've modified Unifier to use more works, please add it here. @@ -235,11 +237,11 @@ def __init__(self, *args, **kwargs): self.__save_lock = False # Ensure necessary keys exist - self.update({'rules': {}, 'rooms': {}, 'rooms_revolt': {}, 'rooms_guilded': {}, 'emojis': [], 'nicknames': {}, - 'descriptions': {}, 'restricted': [], 'locked': [], 'blocked': {}, 'banned': {}, 'moderators': [], - 'avatars': {}, 'experiments': {}, 'experiments_info': {}, 'colors': {}, 'external_bridge': [], - 'modlogs': {}, 'spybot': [], 'trusted': [], 'report_threads': {}, 'fullbanned': [], 'exp': {}, - 'squads': {}, 'squads_joined': {}, 'squads_optout': {}, 'appealban': [], 'roomemojis': {}}) + self.update({'rooms': {}, 'emojis': [], 'nicknames': {}, 'blocked': {}, 'banned': {}, + 'moderators': [], 'avatars': {}, 'experiments': {}, 'experiments_info': {}, 'colors': {}, + 'external_bridge': [], 'modlogs': {}, 'spybot': [], 'trusted': [], 'report_threads': {}, + 'fullbanned': [], 'exp': {}, 'squads': {}, 'squads_joined': {}, 'squads_optout': {}, + 'appealban': [], 'languages': {}, 'settings': {}, 'invites': {}}) self.threads = [] # Load data @@ -399,8 +401,6 @@ def check(interaction): self.logger.exception('An error occurred!') await ctx.send(f'{self.bot.ui_emojis.error} An unexpected error occurred while running this command.') - -# noinspection PyUnresolvedReferences class SysManager(commands.Cog, name=':wrench: System Manager'): """An extension that oversees a lot of the bot system. @@ -410,6 +410,7 @@ class SysExtensionLoadFailed(Exception): pass def __init__(self, bot): + global language self.bot = bot if not hasattr(self.bot, 'db'): self.bot.db = AutoSaveDict({}) @@ -452,8 +453,12 @@ def __init__(self, bot): self.bot.package = self.bot.config['package'] self.bot.exhandler = CommandExceptionHandler(self.bot) - self.logger = log.buildlogger(self.bot.package, 'sysmgr', self.bot.loglevel) + + if not hasattr(self.bot, 'langmgr'): + self.bot.langmgr = langmgr.LanguageManager(self.bot) + self.bot.langmgr.load() + language = self.bot.langmgr if not hasattr(self.bot,'loaded_plugins'): self.bot.loaded_plugins = {} if not self.bot.safemode: @@ -461,12 +466,30 @@ def __init__(self, bot): with open('plugins/' + plugin) as file: extinfo = json.load(file) try: - if not 'content_protection' in extinfo['services']: + if not 'content_protection' in extinfo['services'] and not 'content_processing' in extinfo['services']: continue except: continue script = importlib.import_module('utils.' + plugin[:-5] + '_content_protection') self.bot.loaded_plugins.update({plugin[:-5]: script}) + if not hasattr(self.bot,'platforms'): + self.bot.platforms_former = {} + self.bot.platforms = {} + + # This loads the entire plugin script to memory. + # Plugins will need to create the platform support object themselves when the + # bot is ready on the platform. + if not self.bot.safemode: + for plugin in os.listdir('plugins'): + with open('plugins/' + plugin) as file: + extinfo = json.load(file) + try: + if not 'bridge_platform' in extinfo['services']: + continue + except: + continue + script = importlib.import_module('utils.' + plugin[:-5] + '_bridge_platform') + self.bot.platforms_former.update({extinfo['bridge_platform']: script}) if not hasattr(self.bot, "ut_total"): self.bot.ut_total = round(time.time()) if not hasattr(self.bot, "disconnects"): @@ -643,30 +666,42 @@ async def preunload(self, extension): @tasks.loop(seconds=300) async def changestatus(self): status_messages = [ - "with the ban hammer", - "with fire", - "with the API", - "hide and seek", - "with code", - "in debug mode", - "in a parallel universe", - "with commands", - "a game of chess", - "with electrons", - "with the matrix", - "with cookies", - "with the metaverse", - "with emojis", - "with Nevira", - "with green.", - "with ItsAsheer", - "webhooks", + ["playing","with the ban hammer"], + ["playing","with fire"], + ["playing","with the API"], + ["playing","hide and seek"], + ["listening","my own code"], + ["playing","in debug mode"], + ["playing","in a parallel universe"], + ["playing","with commands"], + ["playing","a game of chess"], + ["playing","with electrons"], + ["watching","the matrix"], + ["watching","cookies bake"], + ["playing","with the metaverse"], + ["playing","with emojis"], + ["playing","with Nevira"], + ["playing","with green."], + ["playing","with ItsAsheer"], + ["watching","webhooks"], + ["custom","Unifying servers like they're nothing"], + ["custom","Made for communities, by communities"] ] new_stat = random.choice(status_messages) - if new_stat == "webhooks": - await self.bot.change_presence(activity=nextcord.Activity(type=nextcord.ActivityType.watching, name=new_stat)) - else: - await self.bot.change_presence(activity=nextcord.Game(name=new_stat)) + if new_stat[0] == "watching": + await self.bot.change_presence(activity=nextcord.Activity( + type=nextcord.ActivityType.watching, name=new_stat[1] + )) + elif new_stat[0] == "listening": + await self.bot.change_presence(activity=nextcord.Activity( + type=nextcord.ActivityType.listening, name=new_stat[1] + )) + elif new_stat[0] == "playing": + await self.bot.change_presence(activity=nextcord.Game(name=new_stat[1])) + elif new_stat[0] == "custom": + await self.bot.change_presence(activity=nextcord.CustomActivity( + name="Custom Status", state=new_stat[1] + )) @tasks.loop() async def periodic_backup(self): @@ -738,9 +773,10 @@ async def periodicping(self): async def on_disconnect(self): self.bot.disconnects += 1 - @commands.command(aliases=['reload-services'], hidden=True, description="Reloads bot services.") + @commands.command(aliases=['reload-services'], hidden=True, description=language.desc('sysmgr.reload_services')) @restrictions.owner() async def reload_services(self,ctx,*,services=None): + selector = language.get_selector(ctx) if not services: plugins = self.bot.loaded_plugins else: @@ -749,7 +785,7 @@ async def reload_services(self,ctx,*,services=None): failed = [] errors = [] text = '```diff' - msg = await ctx.send('Reloading services...') + msg = await ctx.send(selector.get('in_progress')) for plugin in plugins: try: importlib.reload(self.bot.loaded_plugins[plugin]) @@ -759,95 +795,89 @@ async def reload_services(self,ctx,*,services=None): failed.append(plugin) errors.append(e) text = text + f'\n- [FAIL] {plugin}' - await msg.edit( - content=f'Reload completed (`{len(plugins) - len(failed)}'f'/{len(plugins)}` successful)\n\n{text}```' - ) + await msg.edit(selector.fget( + 'completed', values={'success': len(plugins)-len(failed), 'total': len(plugins())} + )) text = '' index = 0 for fail in failed: if len(text) == 0: - text = f'Extension `{fail}`\n```{errors[index]}```' + text = f'{selector.get("extension")} `{fail}`\n```{errors[index]}```' else: - text = f'\n\nExtension `{fail}`\n```{errors[index]}```' + text = f'\n\n{selector.get("extension")} `{fail}`\n```{errors[index]}```' index += 1 if not len(failed) == 0: - await ctx.author.send(f'**Fail logs**\n{text}') + await ctx.author.send(f'**{selector.get("fail_logs")}**\n{text}') - @commands.command(hidden=True,description='Evaluates code.') + @commands.command(hidden=True,description=language.desc('sysmgr.eval')) @restrictions.owner() async def eval(self, ctx, *, body): - if ctx.author.id == self.bot.config['owner']: - env = { - 'ctx': ctx, - 'channel': ctx.channel, - 'author': ctx.author, - 'guild': ctx.guild, - 'message': ctx.message, - 'source': inspect.getsource, - 'session': self.bot.session, - 'bot': self.bot - } + selector = language.get_selector(ctx) + env = { + 'ctx': ctx, + 'channel': ctx.channel, + 'author': ctx.author, + 'guild': ctx.guild, + 'message': ctx.message, + 'source': inspect.getsource, + 'session': self.bot.session, + 'bot': self.bot + } - env.update(globals()) + env.update(globals()) - body = cleanup_code(body) - stdout = io.StringIO() + body = cleanup_code(body) + stdout = io.StringIO() - to_compile = f'async def func():\n{textwrap.indent(body, " ")}' + to_compile = f'async def func():\n{textwrap.indent(body, " ")}' - try: - if 'bot.token' in body or 'dotenv' in body or '.env' in body or 'environ' in body: - raise ValueError('Blocked phrase') - exec(to_compile, env) - except: - pass + try: + if 'bot.token' in body or 'dotenv' in body or '.env' in body or 'environ' in body: + raise ValueError('Blocked phrase') + exec(to_compile, env) + except: + pass - try: - func = env['func'] - except Exception as e: - await ctx.send('An error occurred while executing the code.', reference=ctx.message) - await ctx.author.send( - f'```py\n{e.__class__.__name__}: {e}\n```\nIf this is a KeyError, it is most likely a SyntaxError.') - return - token_start = base64.b64encode(bytes(str(self.bot.user.id), 'utf-8')).decode('utf-8') - try: - with redirect_stdout(stdout): - # ret = await func() to return output - await func() - except: - value = await self.bot.loop.run_in_executor(None, lambda: stdout.getvalue()) - await ctx.send('An error occurred while executing the code.', reference=ctx.message) - if token_start in value: - return await ctx.author.send('The error contained your bot\'s token, so it was not sent.') - await ctx.author.send(f'```py\n{value}{traceback.format_exc()}\n```') - else: - value = await self.bot.loop.run_in_executor(None, lambda: stdout.getvalue()) - if token_start in value: - return await ctx.send('The output contained your bot\'s token, so it was not sent.') - if value == '': - pass - else: - # here, cause is if haves value - await ctx.send('```%s```' % value) + try: + func = env['func'] + except Exception as e: + await ctx.send(selector.get('error'), reference=ctx.message) + await ctx.author.send( + f'```py\n{e.__class__.__name__}: {e}\n```\n{selector.get("syntaxerror")}') + return + token_start = base64.b64encode(bytes(str(self.bot.user.id), 'utf-8')).decode('utf-8') + try: + with redirect_stdout(stdout): + # ret = await func() to return output + await func() + except: + value = await self.bot.loop.run_in_executor(None, lambda: stdout.getvalue()) + await ctx.send(selector.get('error'), reference=ctx.message) + if token_start in value: + return await ctx.author.send(selector.get('blocked')) + await ctx.author.send(f'```py\n{value}{traceback.format_exc()}\n```') else: - await ctx.send('Only the owner can execute code.') + value = await self.bot.loop.run_in_executor(None, lambda: stdout.getvalue()) + if token_start in value: + return await ctx.send(selector.get('blocked')) + if value == '': + pass + else: + # here, cause is if haves value + await ctx.send('```%s```' % value) @eval.error async def eval_error(self, ctx, error): - if ctx.author.id == self.bot.config['owner']: - if isinstance(error, commands.MissingRequiredArgument): - await ctx.send('where code :thinking:') - else: - await ctx.send('Something went horribly wrong.') - raise + selector = language.get_selector(ctx) + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send(selector.get('nocode')) else: - await ctx.send('Only the owner can execute code.') + raise - @commands.command(aliases=['stop', 'poweroff', 'kill'], hidden=True, description='Gracefully shuts the bot down.') + @commands.command(aliases=['stop', 'poweroff', 'kill'], hidden=True, description=language.desc('sysmgr.shutdown')) @restrictions.owner() async def shutdown(self, ctx): - if not ctx.author.id == self.bot.config['owner']: - return + selector = language.get_selector(ctx) self.logger.info("Attempting graceful shutdown...") self.bot.bridge.backup_lock = True try: @@ -865,10 +895,10 @@ async def shutdown(self, ctx): self.bot.bridge.backup_lock = False await self.bot.bridge.backup(limit=10000) self.logger.info("Backup complete") - await ctx.send('Shutting down...') + await ctx.send(selector.get('success')) except: self.logger.exception("Graceful shutdown failed") - await ctx.send('Shutdown failed') + await ctx.send(selector.get('failed')) return self.logger.info("Closing bot session") await self.bot.session.close() @@ -876,9 +906,10 @@ async def shutdown(self, ctx): await self.bot.close() sys.exit(0) - @commands.command(hidden=True,description='Lists all installed plugins.') + @commands.command(hidden=True,description=language.desc('sysmgr.plugins')) @restrictions.owner() async def plugins(self, ctx, *, plugin=None): + selector = language.get_selector(ctx) if plugin: plugin = plugin.lower() page = 0 @@ -892,7 +923,7 @@ async def plugins(self, ctx, *, plugin=None): pluglist = [plugin for plugin in os.listdir('plugins') if plugin.endswith('.json')] if not plugin: offset = page * 20 - embed = nextcord.Embed(title='Unifier Plugins', color=self.bot.colors.unifier) + embed = nextcord.Embed(title=selector.get('title'), color=self.bot.colors.unifier) text = '' if offset > len(pluglist): page = len(pluglist) // 20 - 1 @@ -907,7 +938,9 @@ async def plugins(self, ctx, *, plugin=None): else: text = f'{text}\n- {pluginfo["name"]} (`{pluginfo["id"]}`)' embed.description = text - embed.set_footer(text="Page " + str(page + 1)) + embed.set_footer(text=selector.rawfget( + 'page', 'sysmgr.extensions', values={'page': page + 1} + )) return await ctx.send(embed=embed) found = False index = 0 @@ -920,15 +953,15 @@ async def plugins(self, ctx, *, plugin=None): with open('plugins/' + plugin + '.json') as file: pluginfo = json.load(file) else: - return await ctx.send('Could not find extension!') + return await ctx.send(selector.rawget('notfound', 'sysmgr.extensions')) embed = nextcord.Embed( title=pluginfo["name"], - description=("Version " + pluginfo['version'] + ' (`' + str(pluginfo['release']) + '`)\n\n' + - pluginfo["description"]), + description=(selector.fget('version',values={'version':pluginfo['version'],'release':pluginfo['release']}) + + '\n\n' + pluginfo["description"]), color=self.bot.colors.unifier ) if plugin == 'system': - embed.description = embed.description + '\n# SYSTEM PLUGIN\nThis plugin cannot be uninstalled.' + embed.description = embed.description + selector.get('system_plugin') try: embed.url = str(pluginfo['repository'])[:-4] except: @@ -939,19 +972,20 @@ async def plugins(self, ctx, *, plugin=None): modtext = '- ' + module else: modtext = modtext + '\n- ' + module - embed.add_field(name='Modules',value=modtext,inline=False) + embed.add_field(name=selector.get('modules'),value=modtext,inline=False) modtext = 'None' for module in pluginfo['utils']: if modtext == 'None': modtext = '- ' + module else: modtext = modtext + '\n- ' + module - embed.add_field(name='Utilities', value=modtext, inline=False) + embed.add_field(name=selector.get('utilities'), value=modtext, inline=False) await ctx.send(embed=embed) - @commands.command(hidden=True, aliases=['cogs'], description='Lists all loaded extensions.') + @commands.command(hidden=True, aliases=['cogs'], description=language.desc('sysmgr.extensions')) @restrictions.owner() async def extensions(self, ctx, *, extension=None): + selector = language.get_selector(ctx) if extension: extension = extension.lower() page = 0 @@ -964,7 +998,7 @@ async def extensions(self, ctx, *, extension=None): pass if not extension: offset = page * 20 - embed = nextcord.Embed(title='Unifier Extensions', color=self.bot.colors.unifier) + embed = nextcord.Embed(title=selector.get('title'), color=self.bot.colors.unifier) text = '' extlist = list(self.bot.extensions) if offset > len(extlist): @@ -980,7 +1014,7 @@ async def extensions(self, ctx, *, extension=None): else: text = f'{text}\n- {cog.qualified_name} (`{ext}`)' embed.description = text - embed.set_footer(text="Page " + str(page + 1)) + embed.set_footer(text=selector.fget('page',values={'page':page + 1})) return await ctx.send(embed=embed) found = False index = 0 @@ -992,7 +1026,7 @@ async def extensions(self, ctx, *, extension=None): if found: ext_info = self.bot.cogs[list(self.bot.cogs)[index]] else: - return await ctx.send('Could not find extension!') + return await ctx.send(selector.get('notfound')) embed = nextcord.Embed( title=ext_info.qualified_name, description=ext_info.description, @@ -1000,146 +1034,145 @@ async def extensions(self, ctx, *, extension=None): ) if (extension == 'cogs.sysmgr' or extension == 'cogs.lockdown' or extension == 'sysmgr' or extension == 'lockdown'): - embed.description = embed.description + '\n# SYSTEM MODULE\nThis module cannot be unloaded.' + embed.description = embed.description + selector.get('system_module') await ctx.send(embed=embed) - @commands.command(hidden=True,description='Reloads an extension.') + @commands.command(hidden=True,description=language.desc('sysmgr.reload')) @restrictions.owner() async def reload(self, ctx, *, extensions): - if ctx.author.id == self.bot.config['owner']: - if self.bot.update: - return await ctx.send('Plugin management is disabled until restart.') - - extensions = extensions.split(' ') - msg = await ctx.send('Reloading extensions...') - failed = [] - errors = [] - text = '' - for extension in extensions: - try: - if extension == 'lockdown': - raise ValueError('Cannot unload lockdown extension for security purposes.') - await self.preunload(extension) - self.bot.reload_extension(f'cogs.{extension}') - if len(text) == 0: - text = f'```diff\n+ [DONE] {extension}' - else: - text += f'\n+ [DONE] {extension}' - except Exception as e: - failed.append(extension) - errors.append(e) - if len(text) == 0: - text = f'```diff\n- [FAIL] {extension}' - else: - text += f'\n- [FAIL] {extension}' - await msg.edit( - content=f'Reload completed (`{len(extensions) - len(failed)}/{len(extensions)}` successful)\n\n{text}```') - text = '' - index = 0 - for fail in failed: + selector = language.get_selector(ctx) + if self.bot.update: + return await ctx.send(selector.get('disabled')) + + extensions = extensions.split(' ') + msg = await ctx.send(selector.get('in_progress')) + failed = [] + errors = [] + text = '' + for extension in extensions: + try: + if extension == 'lockdown': + raise ValueError('Cannot unload lockdown extension for security purposes.') + await self.preunload(extension) + self.bot.reload_extension(f'cogs.{extension}') if len(text) == 0: - text = f'Extension `{fail}`\n```{errors[index]}```' + text = f'```diff\n+ [DONE] {extension}' else: - text = f'\n\nExtension `{fail}`\n```{errors[index]}```' - index += 1 - if not len(failed) == 0: - await ctx.author.send(f'**Fail logs**\n{text}') - else: - await ctx.send('Only the owner can reload extensions.') + text += f'\n+ [DONE] {extension}' + except Exception as e: + failed.append(extension) + errors.append(e) + if len(text) == 0: + text = f'```diff\n- [FAIL] {extension}' + else: + text += f'\n- [FAIL] {extension}' + await msg.edit(content=selector.rawfget( + 'completed', 'sysmgr.reload_services', values={ + 'success': len(extensions)-len(failed), 'total': len(extensions), 'text': text + } + )) + text = '' + index = 0 + for fail in failed: + if len(text) == 0: + text = f'{selector.rawget("extension","sysmgr.reload_servies")} `{fail}`\n```{errors[index]}```' + else: + text = f'\n\n{selector.rawget("extension","sysmgr.reload_servies")} `{fail}`\n```{errors[index]}```' + index += 1 + if not len(failed) == 0: + await ctx.author.send(f'**{selector.rawget("fail_logs","sysmgr.reload_servies")}**\n{text}') - @commands.command(hidden=True,description='Loads an extension.') + @commands.command(hidden=True,description=language.desc('sysmgr.load')) @restrictions.owner() async def load(self, ctx, *, extensions): - if ctx.author.id == self.bot.config['owner']: - if self.bot.update: - return await ctx.send('Plugin management is disabled until restart.') - - extensions = extensions.split(' ') - msg = await ctx.send('Loading extensions...') - failed = [] - errors = [] - text = '' - for extension in extensions: - try: - self.bot.load_extension(f'cogs.{extension}') - if len(text) == 0: - text = f'```diff\n+ [DONE] {extension}' - else: - text += f'\n+ [DONE] {extension}' - except Exception as e: - failed.append(extension) - errors.append(e) - if len(text) == 0: - text = f'```diff\n- [FAIL] {extension}' - else: - text += f'\n- [FAIL] {extension}' - await msg.edit( - content=f'Load completed (`{len(extensions) - len(failed)}/{len(extensions)}` successful)\n\n{text}```') - text = '' - index = 0 - for fail in failed: + selector = language.get_selector(ctx) + if self.bot.update: + return await ctx.send(selector.rawget('disabled','sysmgr.reload')) + + extensions = extensions.split(' ') + msg = await ctx.send(selector.get('in_progress')) + failed = [] + errors = [] + text = '' + for extension in extensions: + try: + self.bot.load_extension(f'cogs.{extension}') if len(text) == 0: - text = f'Extension `{fail}`\n```{errors[index]}```' + text = f'```diff\n+ [DONE] {extension}' else: - text = f'\n\nExtension `{fail}`\n```{errors[index]}```' - index += 1 - if not len(failed) == 0: - await ctx.author.send(f'**Fail logs**\n{text}') - else: - await ctx.send('Only the owner can load extensions.') + text += f'\n+ [DONE] {extension}' + except Exception as e: + failed.append(extension) + errors.append(e) + if len(text) == 0: + text = f'```diff\n- [FAIL] {extension}' + else: + text += f'\n- [FAIL] {extension}' + await msg.edit(content=selector.fget( + 'completed', + values={'success': len(extensions)-len(failed), 'total': len(extensions), 'text': text} + )) + text = '' + index = 0 + for fail in failed: + if len(text) == 0: + text = f'Extension `{fail}`\n```{errors[index]}```' + else: + text = f'\n\nExtension `{fail}`\n```{errors[index]}```' + index += 1 + if not len(failed) == 0: + await ctx.author.send(f'**{selector.rawget("fail_logs","sysmgr.reload_servies")}**\n{text}') @commands.command(hidden=True,description='Unloads an extension.') @restrictions.owner() async def unload(self, ctx, *, extensions): - if ctx.author.id == self.bot.config['owner']: - if self.bot.update: - return await ctx.send('Plugin management is disabled until restart.') - - extensions = extensions.split(' ') - msg = await ctx.send('Unloading extensions...') - failed = [] - errors = [] - text = '' - for extension in extensions: - try: - if extension == 'sysmgr': - raise ValueError('Cannot unload the sysmgr extension, let\'s not break the bot here!') - if extension == 'lockdown': - raise ValueError('Cannot unload lockdown extension for security purposes.') - await self.preunload(extension) - self.bot.unload_extension(f'cogs.{extension}') - if len(text) == 0: - text = f'```diff\n+ [DONE] {extension}' - else: - text += f'\n+ [DONE] {extension}' - except Exception as e: - failed.append(extension) - errors.append(e) - if len(text) == 0: - text = f'```diff\n- [FAIL] {extension}' - else: - text += f'\n- [FAIL] {extension}' - await msg.edit( - content=f'Unload completed (`{len(extensions) - len(failed)}/{len(extensions)}` successful)\n\n{text}```') - text = '' - index = 0 - for fail in failed: + selector = language.get_selector(ctx) + if self.bot.update: + return await ctx.send(selector.rawget('disabled','sysmgr.reload')) + + extensions = extensions.split(' ') + msg = await ctx.send('Unloading extensions...') + failed = [] + errors = [] + text = '' + for extension in extensions: + try: + if extension == 'sysmgr': + raise ValueError('Cannot unload the sysmgr extension, let\'s not break the bot here!') + if extension == 'lockdown': + raise ValueError('Cannot unload lockdown extension for security purposes.') + await self.preunload(extension) + self.bot.unload_extension(f'cogs.{extension}') if len(text) == 0: - text = f'Extension `{fail}`\n```{errors[index]}```' + text = f'```diff\n+ [DONE] {extension}' else: - text = f'\n\nExtension `{fail}`\n```{errors[index]}```' - index += 1 - if not len(failed) == 0: - await ctx.author.send(f'**Fail logs**\n{text}') - else: - await ctx.send('Only the owner can unload extensions.') + text += f'\n+ [DONE] {extension}' + except Exception as e: + failed.append(extension) + errors.append(e) + if len(text) == 0: + text = f'```diff\n- [FAIL] {extension}' + else: + text += f'\n- [FAIL] {extension}' + await msg.edit(content=selector.fget( + 'completed', + values={'success': len(extensions)-len(failed), 'total': len(extensions), 'text': text} + )) + text = '' + index = 0 + for fail in failed: + if len(text) == 0: + text = f'Extension `{fail}`\n```{errors[index]}```' + else: + text = f'\n\nExtension `{fail}`\n```{errors[index]}```' + index += 1 + if not len(failed) == 0: + await ctx.author.send(f'**{selector.rawget("fail_logs","sysmgr.reload_servies")}**\n{text}') @commands.command(hidden=True,description='Installs a plugin.') @restrictions.owner() async def install(self, ctx, url): - if not ctx.author.id==self.bot.config['owner']: - return - + selector = language.get_selector(ctx) if self.bot.update: return await ctx.send('Plugin management is disabled until restart.') @@ -1156,7 +1189,7 @@ async def install(self, ctx, url): url = url[:-1] if not url.endswith('.git'): url = url + '.git' - embed = nextcord.Embed(title=f'{self.bot.ui_emojis.install} Downloading extension...', description='Getting extension files from remote') + embed = nextcord.Embed(title=f'{self.bot.ui_emojis.install} {selector.get("downloading_title")}', description=selector.get("downloading_body")) embed.set_footer(text='Only install plugins from trusted sources!') msg = await ctx.send(embed=embed) try: @@ -1186,6 +1219,12 @@ async def install(self, ctx, url): minimum = new['minimum'] modules = new['modules'] utilities = new['utils'] + try: + nups_platform = new['bridge_platform'] + if nups_platform == '': + nups_platform = None + except: + nups_platform = None try: services = new['services'] except: @@ -1222,7 +1261,7 @@ async def install(self, ctx, url): embed.description = 'The repository URL or the plugin.json file is invalid.' embed.colour = self.bot.colors.error await msg.edit(embed=embed) - raise + return embed.title = f'{self.bot.ui_emojis.install} Install `{plugin_id}`?' embed.description = f'Name: `{name}`\nVersion: `{version}`\n\n{desc}' embed.colour = 0xffcc00 @@ -1241,6 +1280,16 @@ async def install(self, ctx, url): 'The plugin will be able to modify message content and author information before bridging to '+ 'other servers.' ) + elif service=='bridge_platform': + text = ( + ':handshake: **Bridge platform support**\n'+ + 'The plugin will be able to provide native Unifier Bridgesupport for an external platform.' + ) + if not nups_platform or nups_platform.lower()=='meta': + embed.title = f'{self.bot.ui_emojis.error} Failed to install plugin' + embed.description = 'The plugin provided an invalid platform name.' + embed.colour = self.bot.colors.error + return await msg.edit(embed=embed) elif service=='emojis': text = ( ':joy: **Emojis**\n'+ @@ -1451,14 +1500,15 @@ async def upgrade(self, ctx, plugin='system', *, args=''): if not ctx.author.id == self.bot.config['owner']: return + selector = language.get_selector(ctx) + if self.bot.update: - return await ctx.send('Plugin management is disabled until restart.') + return await ctx.send(selector.rawget('disabled','sysmgr.reload')) if os.name == "nt": embed = nextcord.Embed( - title=f'{self.bot.ui_emojis.error} Can\'t upgrade Unifier', - description=('Unifier cannot upgrade itself on Windows. Please use an OS with the bash console (Linux/'+ - 'macOS/etc).'), + title=f'{self.bot.ui_emojis.error} {selector.get("windows_title")}', + description=selector.get('windows_body'), color=self.bot.colors.error ) return await ctx.send(embed=embed) @@ -1478,8 +1528,8 @@ async def upgrade(self, ctx, plugin='system', *, args=''): if plugin=='system': embed = nextcord.Embed( - title=f'{self.bot.ui_emojis.install} Checking for upgrades...', - description='Getting latest version from remote' + title=f'{self.bot.ui_emojis.install} {selector.get("checking_title")}', + description=selector.get('checking_body') ) msg = await ctx.send(embed=embed) available = [] @@ -1507,13 +1557,13 @@ async def upgrade(self, ctx, plugin='system', *, args=''): index += 1 update_available = len(available) >= 1 except: - embed.title = f'{self.bot.ui_emojis.error} Failed to check for updates' - embed.description = 'Could not find a valid update.json file on remote' + embed.title = f'{self.bot.ui_emojis.error} {selector.get("checkfail_title")}' + embed.description = selector.get("checkfail_body") embed.colour = self.bot.colors.error return await msg.edit(embed=embed) if not update_available: - embed.title = f'{self.bot.ui_emojis.success} No updates available' - embed.description = 'Unifier is up-to-date.' + embed.title = f'{self.bot.ui_emojis.success} {selector.get("noupdates_title")}' + embed.description = selector.get("noupdates_body") embed.colour = self.bot.colors.success return await msg.edit(embed=embed) selected = 0 @@ -1523,8 +1573,10 @@ async def upgrade(self, ctx, plugin='system', *, args=''): version = available[selected][0] legacy = available[selected][3] > -1 reboot = available[selected][4] - embed.title = f'{self.bot.ui_emojis.install} Update available' - embed.description = f'An update is available for Unifier!\n\nCurrent version: {current["version"]} (`{current["release"]}`)\nNew version: {version} (`{release}`)' + embed.title = f'{self.bot.ui_emojis.install} {selector.get("available_title")}' + embed.description = selector.fget('available_body',values={ + 'current_ver':current['version'],'current_rel':current['release'],'new_ver':version,'new_rel':release + }) embed.remove_footer() embed.colour = 0xffcc00 if legacy: @@ -1533,9 +1585,9 @@ async def upgrade(self, ctx, plugin='system', *, args=''): else: should_reboot = reboot >= current['release'] if should_reboot: - embed.set_footer(text='The bot will need to reboot to apply the new update.') + embed.set_footer(text=selector.get("reboot_required")) selection = nextcord.ui.StringSelect( - placeholder='Select version...', + placeholder=selector.get("version"), max_values=1, min_values=1, custom_id='selection', @@ -1552,15 +1604,15 @@ async def upgrade(self, ctx, plugin='system', *, args=''): index += 1 btns = ui.ActionRow( nextcord.ui.Button( - style=nextcord.ButtonStyle.green, label='Upgrade', custom_id=f'accept', + style=nextcord.ButtonStyle.green, label=selector.get("upgrade"), custom_id=f'accept', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.gray, label='Nevermind', custom_id=f'reject', + style=nextcord.ButtonStyle.gray, label=selector.rawget('nevermind','sysmgr.install'), custom_id=f'reject', disabled=False ), nextcord.ui.Button( - style=nextcord.ButtonStyle.link, label='More info', + style=nextcord.ButtonStyle.link, label=selector.get("moreinfo"), url=f'https://github.com/UnifierHQ/unifier/releases/tag/{version}' ) ) @@ -1586,8 +1638,8 @@ def check(interaction): selected = int(interaction.data['values'][0]) self.logger.info('Upgrade confirmed, preparing...') if not no_backup: - embed.title = f'{self.bot.ui_emojis.install} Backing up...' - embed.description = 'Your data is being backed up.' + embed.title = f'{self.bot.ui_emojis.install} {selector.get("backup_title")}' + embed.description = selector.get("backup_body") await interaction.response.edit_message(embed=embed, view=None) try: if no_backup: @@ -1616,21 +1668,21 @@ def check(interaction): except: if no_backup: self.logger.warning('Backup skipped, requesting final confirmation.') - embed.description = '- :x: Your files have **NOT BEEN BACKED UP**! Data loss or system failures may occur if the upgrade fails!\n- :wrench: Any modifications you made to Unifier will be wiped, unless they are a part of the new upgrade.\n- :warning: Once started, you cannot abort the upgrade.' + embed.description = f'- :x: {selector.get("skipped_backup")}\n- :wrench: {selector.get("modification_wipe")}\n- :warning: {selector.get("no_abort")}' elif ignore_backup: self.logger.warning('Backup failed, continuing anyways') - embed.description = '- :x: Your files **COULD NOT BE BACKED UP**! Data loss or system failures may occur if the upgrade fails!\n- :wrench: Any modifications you made to Unifier will be wiped, unless they are a part of the new upgrade.\n- :warning: Once started, you cannot abort the upgrade.' + embed.description = f'- :x: {selector.get("failed_backup")}\n- :wrench: {selector.get("modification_wipe")}\n- :warning: {selector.get("no_abort")}' else: self.logger.error('Backup failed, abort upgrade.') - embed.title = f'{self.bot.ui_emojis.error} Backup failed' - embed.description = 'Unifier could not create a backup. The upgrade has been aborted.' + embed.title = f'{self.bot.ui_emojis.error} {selector.get("backupfail_title")}' + embed.description = selector.get("backupfail_body") embed.colour = self.bot.colors.error await msg.edit(embed=embed) raise else: self.logger.info('Backup complete, requesting final confirmation.') - embed.description = '- :inbox_tray: Your files have been backed up to `[Unifier root directory]/old.`\n- :wrench: Any modifications you made to Unifier will be wiped, unless they are a part of the new upgrade.\n- :warning: Once started, you cannot abort the upgrade.' - embed.title = f'{self.bot.ui_emojis.install} Start the upgrade?' + embed.description = f'- :inbox_tray: {selector.get("normal_backup")}\n- :wrench: {selector.get("modification_wipe")}\n- :warning: {selector.get("no_abort")}' + embed.title = f'{self.bot.ui_emojis.install} {selector.get("start")}' components = ui.MessageComponents() components.add_row(btns) if no_backup: @@ -1652,8 +1704,8 @@ def check(interaction): components.add_row(btns) return await interaction.response.edit_message(view=components) self.logger.debug('Upgrade confirmed, beginning upgrade') - embed.title = f'{self.bot.ui_emojis.install} Upgrading Unifier' - embed.description = ':hourglass_flowing_sand: Downloading updates\n:x: Installing updates\n:x: Reloading modules' + embed.title = f'{self.bot.ui_emojis.install} {selector.get("upgrading")}' + embed.description = f':hourglass_flowing_sand: {selector.get("downloading")}\n:x: {selector.get("installing")}\n:x: {selector.get("reloading")}' await interaction.response.edit_message(embed=embed, view=None) self.logger.info('Starting upgrade') try: @@ -1670,8 +1722,8 @@ def check(interaction): self.logger.debug('Download confirmed, proceeding with upgrade') except: self.logger.exception('Download failed, no rollback required') - embed.title = f'{self.bot.ui_emojis.error} Upgrade failed' - embed.description = 'Could not download updates. No rollback is required.' + embed.title = f'{self.bot.ui_emojis.error} {selector.get("failed")}' + embed.description = selector.get("download_fail") embed.colour = self.bot.colors.error await msg.edit(embed=embed) return @@ -1694,19 +1746,19 @@ def check(interaction): pass if len(newdeps) > 0: self.logger.debug('Installing: ' + ' '.join(newdeps)) - await self.bot.loop.run_in_executor(None, lambda: status(os.system( - 'python3 -m pip install ' + ' '.join(newdeps)) + await self.bot.loop.run_in_executor(None, lambda: status( + os.system('python3 -m pip install ' + ' '.join(newdeps)) )) except: self.logger.exception('Dependency installation failed, no rollback required') - embed.title = f'{self.bot.ui_emojis.error} Upgrade failed' - embed.description = 'Could not install dependencies. No rollback is required.' + embed.title = f'{self.bot.ui_emojis.error} {selector.get("failed")}' + embed.description = selector.get("dependency_fail") embed.colour = self.bot.colors.error await msg.edit(embed=embed) return try: self.logger.info('Installing upgrades') - embed.description = ':white_check_mark: Downloading updates\n:hourglass_flowing_sand: Installing updates\n:x: Reloading modules' + embed.description = f':white_check_mark: {selector.get("downloading")}\n:hourglass_flowing_sand: {selector.get("installing")}\n:x: {selector.get("reloading")}' await msg.edit(embed=embed) self.logger.debug('Installing: ' + os.getcwd() + '/update/unifier.py') status(os.system('cp ' + os.getcwd() + '/update/unifier.py ' + os.getcwd() + '/unifier.py')) @@ -1746,26 +1798,26 @@ def check(interaction): if should_reboot: self.bot.update = True self.logger.info('Upgrade complete, reboot required') - embed.title = f'{self.bot.ui_emojis.success} Restart to apply upgrade' - embed.description = f'The upgrade was successful. Please reboot the bot.' + embed.title = f'{self.bot.ui_emojis.success} {selector.get("restart_title")}' + embed.description =selector.get("restart_body") embed.colour = self.bot.colors.success await msg.edit(embed=embed) else: self.logger.info('Restarting extensions') - embed.description = ':white_check_mark: Downloading updates\n:white_check_mark: Installing updates\n:hourglass_flowing_sand: Reloading modules' + f':white_check_mark: {selector.get("downloading")}\n:white_check_mark: {selector.get("installing")}\n:hourglass_flowing_sand: {selector.get("reloading")}' await msg.edit(embed=embed) for cog in list(self.bot.extensions): self.logger.debug('Restarting extension: ' + cog) await self.preunload(cog) self.bot.reload_extension(cog) self.logger.info('Upgrade complete') - embed.title = f'{self.bot.ui_emojis.success} Upgrade successful' - embed.description = 'The upgrade was successful! :partying_face:' + embed.title = f'{self.bot.ui_emojis.success} {selector.get("success_title")}' + embed.description = selector.get("success_body") embed.colour = self.bot.colors.success await msg.edit(embed=embed) except: self.logger.exception('Upgrade failed, attempting rollback') - embed.title = f'{self.bot.ui_emojis.error} Upgrade failed' + embed.title = f'{self.bot.ui_emojis.error} {selector.get("failed")}' embed.colour = self.bot.colors.error try: self.logger.debug('Reverting: ' + os.getcwd() + '/unifier.py') @@ -1781,29 +1833,29 @@ def check(interaction): status( os.system('cp ' + os.getcwd() + '/old/cogs/' + file + ' ' + os.getcwd() + '/cogs/' + file)) self.logger.info('Rollback success') - embed.description = 'The upgrade failed, and all files have been rolled back.' + embed.description = selector.get("rollback") except: self.logger.exception('Rollback failed') self.logger.critical( 'The rollback failed. Visit https://unichat-wiki.pixels.onl/setup-selfhosted/upgrading-unifier/manual-rollback for recovery steps.') - embed.description = 'The upgrade failed, and the bot may now be in a crippled state.\nPlease check console logs for more info.' + embed.description = selector.get("rollback_fail") await msg.edit(embed=embed) return else: - embed = nextcord.Embed(title=f'{self.bot.ui_emojis.install} Downloading extension...', description='Getting extension files from remote') + embed = nextcord.Embed(title=f'{self.bot.ui_emojis.install} {selector.rawget("downloading_title","sysmgr.install")}', description=selector.rawget("downloading_body",'sysmgr.install')) try: with open('plugins/'+plugin+'.json') as file: plugin_info = json.load(file) except: - embed.title = f'{self.bot.ui_emojis.error} Plugin not found' - embed.description = 'The plugin could not be found.' + embed.title = f'{self.bot.ui_emojis.error} {selector.get("notfound_title")}' + embed.description = selector.get("notfound_body") if plugin=='force': - embed.description = embed.description + f'\n\n**Hint**: If you\'re trying to force upgrade, run `{self.bot.command_prefix}upgrade system force`' + embed.description = embed.description + selector.fget('hint_force',values={'prefix':self.bot.command_prefix}) embed.colour = self.bot.colors.error await ctx.send(embed=embed) return - embed.set_footer(text='Only install plugins from trusted sources!') + embed.set_footer(text=selector.rawget("trust",'sysmgr.install')) msg = await ctx.send(embed=embed) url = plugin_info['repository'] try: @@ -1813,14 +1865,14 @@ def check(interaction): with open('plugin_install/plugin.json', 'r') as file: new = json.load(file) if not bool(re.match("^[a-z0-9_-]*$", new['id'])): - embed.title = f'{self.bot.ui_emojis.error} Invalid plugin.json file' - embed.description = 'Plugin IDs must be alphanumeric and may only contain lowercase letters, numbers, dashes, and underscores.' + embed.title = f'{self.bot.ui_emojis.error} {selector.rawget("alphanumeric_title","sysmgr.install")}' + embed.description = selector.rawget("alphanumeric_body",'sysmgr.install') embed.colour = self.bot.colors.error await msg.edit(embed=embed) return if new['release'] <= plugin_info['release'] and not force: - embed.title = f'{self.bot.ui_emojis.success} Plugin up to date' - embed.description = f'This plugin is already up to date!' + embed.title = f'{self.bot.ui_emojis.success} {selector.get("pnoupdates_title")}' + embed.description = selector.get("pnoupdates_body") embed.colour = self.bot.colors.success await msg.edit(embed=embed) return @@ -1832,17 +1884,17 @@ def check(interaction): utilities = new['utils'] services = new['services'] if 'services' in new.keys() else [] except: - embed.title = f'{self.bot.ui_emojis.error} Failed to update plugin' - embed.description = 'The repository URL or the plugin.json file is invalid.' + embed.title = f'{self.bot.ui_emojis.error} {selector.get("pfailed")}' + embed.description = selector.rawget("invalid_repo",'sysmgr.install') embed.colour = self.bot.colors.error await msg.edit(embed=embed) raise - embed.title = f'{self.bot.ui_emojis.install} Update `{plugin_id}`?' - embed.description = f'Name: `{name}`\nVersion: `{version}`\n\n{desc}' + embed.title = f'{self.bot.ui_emojis.install} {selector.fget("question",values={"plugin":plugin_id})}' + embed.description = selector.rawfget('plugin_info','sysmgr.install',values={'name':name,'version':version,'desc':desc}) embed.colour = 0xffcc00 btns = ui.ActionRow( - nextcord.ui.Button(style=nextcord.ButtonStyle.green, label='Update', custom_id=f'accept', disabled=False), - nextcord.ui.Button(style=nextcord.ButtonStyle.gray, label='Nevermind', custom_id=f'reject', disabled=False) + nextcord.ui.Button(style=nextcord.ButtonStyle.green, label=selector.get("upgrade"), custom_id=f'accept', disabled=False), + nextcord.ui.Button(style=nextcord.ButtonStyle.gray, label=selector.rawfget("nevermind","sysmgr.install"), custom_id=f'reject', disabled=False) ) components = ui.MessageComponents() components.add_row(btns) @@ -1962,14 +2014,14 @@ def check(interaction): await self.preunload(modname) self.bot.reload_extension(modname) self.logger.debug('Upgrade complete') - embed.title = f'{self.bot.ui_emojis.success} Upgrade successful' - embed.description = 'The upgrade was successful! :partying_face:' + embed.title = f'{self.bot.ui_emojis.success} {selector.get("success_title")}' + embed.description = selector.get("success_body") embed.colour = self.bot.colors.success await msg.edit(embed=embed) except: self.logger.exception('Upgrade failed') - embed.title = f'{self.bot.ui_emojis.error} Upgrade failed' - embed.description = 'The upgrade failed.' + embed.title = f'{self.bot.ui_emojis.error} {selector.get("failed")}' + embed.description = selector.get("plugin_fail") embed.colour = self.bot.colors.error await msg.edit(embed=embed) return @@ -1980,14 +2032,13 @@ def check(interaction): ) @restrictions.owner() async def uiemojis(self, ctx, *, emojipack): - if not ctx.author.id == self.bot.config['owner']: - return + selector = language.get_selector(ctx) emojipack = emojipack.lower() if emojipack=='base': os.remove('emojis/current.json') self.bot.ui_emojis = Emojis() - await ctx.send(f'{self.bot.ui_emojis.success} Emoji pack reset to default.') + await ctx.send(f'{self.bot.ui_emojis.success} {selector.get("reset")}') else: try: with open(f'emojis/{emojipack}.json', 'r') as file: @@ -2001,8 +2052,9 @@ async def uiemojis(self, ctx, *, emojipack): self.logger.exception('An error occurred!') await ctx.send(f'{self.bot.ui_emojis.error} Could not activate emoji pack.') - @commands.command(description='Shows this command.') + @commands.command(description=language.desc('sysmgr.help')) async def help(self,ctx): + selector = language.get_selector(ctx) panel = 0 limit = 20 page = 0 @@ -2390,6 +2442,7 @@ def check(interaction): descmatch = True elif interaction.type==nextcord.InteractionType.modal_submit: panel = 1 + page = 0 cogname = 'search' query = interaction.data['components'][0]['components'][0]['value'] namematch = True diff --git a/config.json b/config.json index 572163a5..e6092d4c 100644 --- a/config.json +++ b/config.json @@ -3,6 +3,7 @@ "package": "unifier", "owner": null, "owner_external": {}, + "language": "english", "prefix": "u!", "skip_status_check": false, "ping": 0, diff --git a/languages/english.json b/languages/english.json new file mode 100644 index 00000000..52976d99 --- /dev/null +++ b/languages/english.json @@ -0,0 +1,817 @@ +{ + "language": "English", + "author": "UnifierHQ", + "release": 0, + "minimum": 61, + "strings": { + "commons": { + "navigation": { + "back": "Back", + "cancel": "Cancel", + "prev": "Previous", + "next": "Next", + "accept": "Accept", + "reject": "Reject", + "yes": "Yes", + "no": "No" + }, + "search": { + "search": "Search", + "search_results": "Searching: {query} (**{results}** results)", + "page": "Page {page} of {maxpage}", + "result_count": "{lower}-{upper} of {total} results", + "search_title": "Search...", + "query": "Search query", + "query_prompt": "Type something...", + "match_any": "Matches any of", + "match_both": "Matches both" + }, + "interaction": { + "invalid_message": "Invalid message!", + "timeout": "Timed out.", + "banned": "Your account or your server is currently **global banned**.", + "not_found": "Could not find message in cache!", + "cancel": "Cancelled.", + "what": "what", + "mod_unexpected": "buddy you're not a global moderator :skull:" + }, + "moderation": { + "warning": "Warning", + "ban": "Ban", + "warnings": "Warnings", + "bans": "Bans", + "invalid_user": "Invalid user!", + "invalid_user_or_server": "Invalid user/server!", + "invalid_user_warn": "Invalid user! (servers can't be warned, warn their staff instead)", + "invalid_index": "Invalid index!", + "invalid_duration": "Invalid duration!", + "no_action": "Could not find action!", + "reason": "Reason", + "context": "Context", + "sender_id": "Sender ID", + "room": "Message room", + "message_id": "Message ID", + "reporter_id": "Reporter ID", + "delete": "Delete message", + "deleted": "Original message deleted", + "mod_immunity": "You cannot punish other moderators!", + "warn_title": "You've been __warned__ by {moderator}!", + "ban_title": "You've been __banned__ by {moderator}!", + "offending_message": "Offending message", + "actions_taken": "Actions taken", + "warned": "You have been **warned**. Further rule violations may lead to sanctions on the Unifier moderators' discretion.", + "temp_ban": "Your ability to text and speak have been **restricted** until . This will expire .", + "perm_ban": "Your ability to text and speak have been **restricted indefinitely**. This will not automatically expire.", + "perm_ban_appeal": "You must contact a moderator to appeal this restriction.", + "modlog_info_title": "User modlogs info", + "modlog_info_body": "This user has **{recent_warns}** recent warnings ({total_warns} in total) and **{recent_bans}** recent bans ({total_bans} in total) on record.", + "appeal_title": "Did we make a mistake?", + "appeal_body": "If you think we didn't make the right call, you can always appeal your ban using `{prefix}!appeal`." + } + }, + "sysmgr": { + "error_handler": { + "argument": "`{arg}` is a required argument.", + "permissions": "You do not have permissions to run this command.", + "cooldown": "You're on cooldown. Try again in **{min}** minutes and **{sec}** seconds.", + "unexpected": "An unexpected error occurred while running this command.", + "view": "View error", + "tb_sendfail": "Could not send traceback." + }, + "reload_services": { + "description": "Reloads bot services.", + "in_progress": "Reloading services...", + "completed": "Reload completed (`{success}`/`{total}` successful)\n\n{text}```", + "fail_logs": "Fail logs", + "extension": "Extension" + }, + "eval": { + "description": "Evaluates code.", + "error": "An error occurred while executing the code.", + "blocked": "The error contained your bot's token, so it was not sent.", + "syntaxerror": "If this is a KeyError, it is most likely a SyntaxError.", + "nocode": "where code :thinking:" + }, + "shutdown": { + "description": "Gracefully shuts the bot down.", + "success": "Shutting down...", + "failed": "Shutdown failed" + }, + "plugins": { + "description": "Lists all installed plugins.", + "title": "Unifier Plugins", + "version": "Version {version} (`{release}`)", + "system_plugin": "\n# SYSTEM PLUGIN\nThis plugin cannot be uninstalled.", + "modules": "Modules", + "utilities": "Utilities" + }, + "extensions": { + "description": "Lists all loaded extensions.", + "title": "Unifier Extensions", + "page": "Page {page}", + "notfound": "Could not find extension!", + "system_module": "\n# SYSTEM MODULE\nThis module cannot be unloaded." + }, + "reload": { + "description": "Reloads an extension.", + "disabled": "Plugin management is disabled until restart.", + "in_progress": "Reloading extensions..." + }, + "load": { + "description": "Loads an extension.", + "in_progress": "Loading extensions...", + "completed": "Load completed (`{success}`/`{total}` successful)\n\n{text}```" + }, + "unload": { + "description": "Unloads an extension.", + "in_progress": "Unloading extensions...", + "completed": "Unload completed (`{success}`/`{total}` successful)\n\n{text}```" + }, + "install": { + "description": "Installs a plugin.", + "windows_title": "Can't install Plugins", + "windows_body": "Unifier cannot install Plugins on Windows. Please use an OS with the bash console (Linux/macOS/etc).", + "downloading_title": "Downloading extension...", + "downloading_body": "Getting extension files from remote", + "trust": "Only install plugins from trusted sources!", + "services": "Services", + "install": "Install", + "nevermind": "Nevermind", + "question": "Install plugin `{plugin}`?", + "plugin_info": "Name: `{name}`\nVersion: `{version}`\n\n{desc}", + "success_title": "Installation successful", + "success_body": "The installation was successful! :partying_face:", + "postfail_title": "Installation failed", + "postfail_body": "The installation failed.", + "failed": "Failed to install plugin", + "alphanumeric_title": "Invalid plugin.json file", + "alphanumeric_body": "Plugin IDs must be alphanumeric and may only contain lowercase letters, numbers, dashes, and underscores.", + "exists_title": "Plugin already installed", + "exists_body": "This plugin is already installed!\n\nName: `{name}`\nVersion: `{version}`", + "unsupported": "Your Unifier does not support this plugin. Release `{minimum}` or later is required.", + "conflict": "Conflicting files were found:", + "invalid_repo": "The repository URL or the plugin.json file is invalid.", + "no_emoji_slots": "Your home server does not have enough emoji slots available. {required} is required, but you only have {available}.", + "content_protection_title": "Content protection", + "content_protection_desc": "The plugin will be able to analyze messages for malicious content, as well as ban users if necessary. Non-permanent bans are reset on Bridge reload.", + "content_processing_title": "Content stylizing", + "content_processing_desc": "The plugin will be able to modify message content and author information before bridging to other servers.", + "emojis_title": "Emojis", + "emojis_desc": "The plugin contains an emoji pack which will be installed onto the bot. You can enable the pack using `{prefix}uiemojis {plugin_id}`.", + "unknown_desc": "This is an unknown service." + }, + "uninstall": { + "description": "Uninstalls a plugin.", + "system": "System plugin cannot be uninstalled!", + "warning": "This will uninstall all of the plugin's files. This cannot be undone!", + "notfound_title": "Plugin not found", + "notfound_body": "The plugin could not be found.", + "question": "Uninstall plugin `{plugin}`?", + "uninstall": "Uninstall", + "success_title": "Uninstallation successful", + "success_body": "The plugin was successfully uninstalled.", + "postfail_title": "Uninstallation failed", + "postfail_body": "The uninstallation failed." + }, + "upgrade": { + "description": "Upgrades Unifier or a plugin.", + "windows_title": "Can't upgrade Unifier", + "windows_body": "Unifier cannot upgrade itself on Windows. Please use an OS with the bash console (Linux/macOS/etc).", + "checking_title": "Checking for upgrades...", + "checking_body": "Getting latest version from remote", + "checkfail_title": "Failed to check for updates", + "checkfail_body": "Could not find a valid update.json file on remote", + "noupdates_title": "No updates available", + "noupdates_body": "Unifier is up-to-date.", + "available_title": "Update available", + "available_body": "An update is available for Unifier!\n\nCurrent version: {current_ver} (`{current_rel}`)\nNew version: {new_ver} (`{new_rel}`)", + "reboot_required": "The bot will need to reboot to apply the new update.", + "version": "Select version...", + "upgrade": "Upgrade", + "moreinfo": "More info", + "backup_title": "Backing up...", + "backup_body": "Your data is being backed up.", + "normal_backup": "Your files have been backed up to `[Unifier root directory]/old`.", + "skipped_backup": "Your files have **NOT BEEN BACKED UP**! Data loss or system failures may occur if the upgrade fails!", + "failed_backup": "Your files **COULD NOT BE BACKED UP**! Data loss or system failures may occur if the upgrade fails!", + "modification_wipe": "Any modifications you made to Unifier will be wiped, unless they are a part of the new upgrade.", + "no_abort": "Once started, you cannot abort the upgrade.", + "backupfail_title": "Backup failed", + "backupfail_body": "Unifier could not create a backup. The upgrade has been aborted.", + "start": "Start the upgrade?", + "upgrading": "Upgrading Unifier", + "downloading": "Downloading updates", + "installing": "Installing updates", + "reloading": "Reloading modules", + "failed": "Upgrade failed", + "download_fail": "Could not download updates. No rollback is required.", + "dependency_fail": "Could not install dependencies. No rollback is required.", + "rollback": "The upgrade failed, and all files have been rolled back.", + "rollback_fail": "The upgrade failed, and the bot may now be in a crippled state.\nPlease check console logs for more info.", + "success_title": "Upgrade successful", + "success_body": "The upgrade was successful! :partying_face:", + "restart_title": "Restart to apply upgrade", + "restart_body": "The upgrade was successful. Please reboot the bot.", + "notfound_title": "Plugin not found", + "notfound_body": "The plugin could not be found.", + "hint_force": "**Hint**: If you're trying to force upgrade, run `{prefix}upgrade system force`", + "pnoupdates_title": "Plugin up to date", + "pnoupdates_body": "This plugin is already up to date!", + "pfailed": "Failed to update plugin", + "question": "Upgrade plugin `{plugin}`?", + "plugin_fail": "The upgrade failed." + }, + "uiemojis": { + "description": "Activates an emoji pack. Activating the \"base\" emoji pack resets emojis back to vanilla.", + "reset": "Emoji pack reset to default.", + "activated": "Emoji pack {emojipack} activated.", + "error": "Could not activate emoji pack." + }, + "help": { + "description": "Shows this command.", + "title": "{botname} help", + "search_nav": "search", + "choose_ext": "Choose an extension to get started!", + "selection_ext": "Extension...", + "all": "all", + "all_title": "All commands", + "all_body": "Shows commands from all extensions.", + "no_desc": "No description provided", + "choose_cmd": "Choose a command to view its info!", + "noresults_title": "No commands", + "noresults_body_search": "There are no commands matching your search query.", + "noresults_body_ext": "There are no commands in this extension.", + "selection_cmd": "Command...", + "cmd_name": "Command name", + "cmd_desc": "Command description", + "aliases": "Aliases", + "usage": "Usage" + }, + "forcereg": { + "description": "Registers commands.", + "atoms": "gone, reduced to atoms (hopefully)", + "registered": "Registered commands to bot" + }, + "cloud": { + "description": "Views cloud backup status.", + "fetching_title": "Fetching backup...", + "fetching_body": "Getting backup information from backup servers", + "encrypted": "All your backups are encrypted in transit and at rest.", + "invalid_title": "Failed to fetch backup", + "invalid_body": "The server did not respond or returned an invalid response.", + "nobackup_title": "No backups", + "nobackup_body": "There's no backups yet.", + "info_title": "Backup info", + "backup_body": "Saved at <:t{unix}:F>", + "restore": "Restore", + "question": "Restore this backup?", + "download": "config.json and data.json files will be downloaded from the backup server.", + "overwrite": "Existing config.json and data.json files will be **overwritten**.", + "noundo": "You **cannot** undo this operation.", + "success_title": "Restore completed", + "success_body": "Please reboot the bot for the changes to take effect.", + "failed_title": "Restore failed", + "failed_body": "Data could not be restored. Please ensure your encryption password and salt is correct." + }, + "uptime": { + "description": "Shows bot uptime.", + "title": "{botname} uptime", + "body": "The bot has been up since .", + "total_title": "Total uptime", + "total_body": "`{days}` days, `{hours}` hours, `{minutes}` minutes, `{seconds}` seconds", + "disconnects": "Disconnects/hr" + }, + "about": { + "description": "Shows bot info.", + "developers": "Developers", + "profile_pic": "PFP made by", + "source_code": "View source code", + "oss_attrib": "Open source attribution", + "license": "{license} license", + "repo_link": "Source code" + } + }, + "config": { + "addmod": { + "description": "Adds a moderator to the instance.", + "invalid": "Not a valid user!", + "already_mod": "This user is already a moderator!", + "self_target": "are you fr", + "success": "**{mod}** is now a moderator!" + }, + "removemod": { + "description": "Removes a moderator from the instance.", + "not_mod": "This user is not a moderator!", + "success": "**{mod}** is no longer a moderator!" + }, + "make": { + "description": "Creates a new room.", + "alphanumeric": "Room names may only contain alphabets, numbers, dashes, and underscores.", + "exists": "This room already exists!", + "success": "Created room `{room}`!" + }, + "rename": { + "description": "Renames a room.", + "success": "Room renamed!", + "invalid": "This room does not exist!" + }, + "addexperiment": { + "description": "Creates a new experiment.", + "exists": "This experiment already exists!", + "success": "Created experiment `{experiment}`!" + }, + "removeexperiment": { + "description": "Removes an experiment.", + "invalid": "This experiment does not exist!", + "success": "Deleted experiment `{experiment}`!" + }, + "experimentdesc": { + "description": "Sets experiment description.", + "success": "Added description to experiment `{experiment}`!" + }, + "roomdesc": { + "description": "Sets room description.", + "missing": "There is no description to reset for this room.", + "success_upd": "Updated description!", + "success_del": "Description removed." + }, + "roomemoji": { + "description": "Sets room emoji.", + "not_emoji": "This is not a valid emoji.", + "missing": "There is no emoji to reset for this room.", + "success_upd": "Updated emoji!", + "success_del": "Emoji removed." + }, + "roomrestrict": { + "description": "Restricts/unrestricts room. Only admins will be able to collect to this room when restricted.", + "success_set": "Restricted `{room}`!", + "success_unset": "Unrestricted `{room}`!" + }, + "roomlock": { + "description": "Locks/unlocks a room. Only moderators and admins will be able to chat in this room when locked.", + "success_set": "Locked `{room}`!", + "success_unset": "Unlocked `{room}`!" + }, + "experiments": { + "description": "Shows a list of Unifier experiments, and lets you join or leave them.", + "already_enrolled": "Your server is already a part of this experiment!", + "not_enrolled": "Your server is not a part of this experiment!", + "enrolled": "Enrolled in experiment **{experiment}**!", + "unenrolled": "Unenrolled from experiment **{experiment}**!", + "title": "Experiments", + "body": "Help us test Unifier's experimental features! Run `{prefix}experiment enroll ` to join one.\\n\\n**WARNING**: These features are experimental and may break things, so proceed at your own risk!", + "enrolled_embed": "Your server is enrolled in this experiment!", + "no_experiments_title": "no experiments? :face_with_raised_eyebrow:", + "no_experiments_body": "There's no experiments available yet!" + }, + "bind": { + "description": "Connects the channel to a given room.", + "restricted": "Only admins can bind channels to restricted rooms.", + "fallback": "No room was given, defaulting to {main}", + "invalid": "This isn't a valid room. Run `{prefix}rooms` for a full list of rooms.", + "check_title": "Ensuring channel is not connected...", + "check_body": "This may take a while.", + "already_linked_title": "Channel already linked!", + "already_linked_body": "This channel is already linked to `{room}`!\nRun `{prefix}unbind {room}` to unbind from it.", + "already_room": "Your server is already linked to this room.\n**Accidentally deleted the webhook?** `{prefix}unlink {room}` it then `{prefix}link {room}` it back.", + "no_rules": "No rules exist yet for this room! For now, follow the main room's rules.\nYou can always view rules if any get added using `{prefix}rules {room}`.", + "disclaimer": "Failure to follow room rules may result in user or server restrictions.", + "display": "Please display these rules somewhere accessible.\nThis message will be automatically pinned if you accept these rules.", + "agree": "Please agree to the room rules first:", + "accept": "Accept and bind", + "cancel": "No thanks", + "success_title": "Linked channel to Unifier network!", + "success_body": "You can now send messages to the Unifier network through this channel. Say hi!" + }, + "unbind": { + "description": "Disconnects the server from a given room.", + "cannot_manage": "I cannot manage webhooks.", + "success_title": "Unlinked channel from Unifier network!", + "success_body": "This channel is no longer linked, nothing from now will be bridged.", + "failed": "Something went wrong - check my permissions." + }, + "map": { + "description": "Maps channels to rooms in bulk.", + "check_title": "Checking bindable channels...", + "map_title": "Map channels", + "map_body": "The following channels will be mapped.\nIf the channel does not exist, they will be created automatically.", + "selection_ch": "Channels...", + "selectall": "Select all", + "selectall_over10": "Select first 10", + "deselect": "Deselect all", + "bind": "Bind", + "bind_restricted": "Bind (create as restricted)", + "bind_locked": "Bind (create as locked)", + "success_title": "Linked channels to Unifier network!", + "success_body": "You can now send messages to the Unifier network through the channels. Say hi!" + }, + "rules": { + "description": "Displays room rules for the specified room.", + "no_rules": "The admins haven't added rules yet. Though, make sure to always use common sense.", + "rules_title": "Room rules" + }, + "addrule": { + "description": "Adds a rule to a given room.", + "exceed": "You can only have up to 25 rules in a room!", + "success": "Added rule!" + }, + "delrule": { + "description": "Removes a given rule from a given room.", + "invalid": "Rule must be a number higher than 0.", + "success": "Removed rule!" + }, + "addbridge": { + "description": "Allows given user's webhooks to be bridged.", + "already_exists": "This user is already in the whitelist!", + "allow_title": "Allow @{username} to bridge?", + "allow_body": "This will allow messages sent via webhooks created by this user to be bridged through Unifier.", + "accept": "Allow bridge", + "success_title": "Linked bridge to Unifier network!", + "success_body": "This user's webhooks can now bridge messages through Unifier!" + }, + "delbridge": { + "description": "Prevents given user's webhooks from being bridged.", + "not_whitelist": "This user isn't in the whitelist!", + "remove_title": "Remove @{username} from bridge?", + "remove_body": "This will stop this user's webhooks from bridging messages.", + "accept": "Revoke bridge", + "success_title": "Unlinked bridge from Unifier network!", + "success_body": "This user's webhooks can no longer bridge messages through Unifier." + }, + "rooms": { + "description": "Shows a list of rooms.", + "title": "{botname} rooms", + "body": "Choose a room to view its info!", + "selection_room": "Room...", + "no_desc": "This room has no description.", + "noresults_title": "No rooms", + "noresults_body_search": "There are no rooms matching your search query.", + "noresults_body_room": "There's no rooms here!", + "choose_room": "Choose a room to view its info!", + "room_name": "Room name", + "room_desc": "Room description", + "statistics": "Statistics", + "servers": "{count} servers", + "online": "{count} online", + "members": "{count} members", + "messages": "{count} messages sent today", + "view_rules": "View room rules", + "no_rules": "The room admins haven't added rules for this room yet.\nThough, do remember to use common sense and refrain from doing things that you shouldn't do." + }, + "toggle_emoji": { + "description": "Enables or disables usage of server emojis as Global Emojis.", + "success_set": "All members can now use your emojis!", + "success_unset": "All members can now no longer use your emojis!" + }, + "avatar": { + "description": "Displays or sets custom avatar.", + "no_avatar": "You have no avatar! Run `{prefix}avatar ` or set an avatar in your profile settings.", + "custom_avatar": "You have a custom avatar! Run `{prefix}avatar ` to change it, or run `{prefix}avatar remove` to remove it.", + "default_avatar": "You have a default avatar! Run `{prefix}avatar ` to set a custom one.", + "title": "This is your Unifier avatar!", + "invalid": "Invalid URL!", + "error_missing": "You don't have a custom avatar!", + "confirmation_title": "This is how you'll look!", + "confirmation_body": "If you're satisfied, hit that green Apply button!", + "apply": "Apply", + "change": "To change your avatar, run {prefix}avatar .", + "success_unset": "Custom avatar removed!", + "success_set": "Avatar successfully added!" + } + }, + "bridge": { + "color": { + "description": "Sets Revolt color.", + "title": "Your Revolt color", + "default": "Default", + "inherit": "Inherit from role", + "invalid": "Invalid hex code!", + "success_inherit": "Your Revolt messages will now inherit your Discord role color.", + "success_custom": "Your Revolt messages will now inherit the custom color." + }, + "nickname": { + "description": "Sets a nickname. An empty provided nickname will reset it.", + "exceed": "Please keep your nickname within 33 characters.", + "success": "Nickname updated." + }, + "ping": { + "description": "Measures bot latency.", + "ping": "Ping!", + "pong": "Pong!", + "normal_title": "Normal - all is well!", + "normal_body": "All is working normally!", + "fair_title": "Fair - could be better.", + "fair_body": "Nothing's wrong, but the latency could be better.", + "slow_title": "SLOW - __**oh no.**__", + "slow_body": "Bot latency is higher than normal, messages may be slow to arrive.", + "tooslow_title": "**WAY TOO SLOW**", + "tooslow_body": "Something is DEFINITELY WRONG here. Consider checking [Discord status](https://discordstatus.com) page." + }, + "emojis": { + "description": "Shows a list of all global emojis available on the instance.", + "title": "{botname} emojis", + "body": "Choose an emoji to view its info!", + "selection_emoji": "Emoji...", + "noresults_title": "No emojis", + "noresults_body_search": "There are no emojis matching your search query.", + "noresults_body_emoji": "There's no global emojis here!", + "from": "From:", + "instructions_title": "How to use", + "instructions_body": "Type `[emoji: {emojiname}]` in your message to use this emoji!" + }, + "modping": { + "description": "Pings all moderators to the chat. Use only when necessary.", + "disabled": "Modping is disabled, contact your instance's owner.", + "invalid": "This isn't a Unifier room!", + "no_moderator": "This instance doesn't have a moderator role set up. Contact your Unifier admins.", + "bad_config": "It appears the home guild has configured Unifier wrong, and I cannot ping its moderators.", + "needhelp": "**{username}** ({userid}) needs your help!\n\nSent from: **{guildname}** ({guildid})", + "success": "Moderators called!" + }, + "reactions_ctx": { + "reactions": "Reactions", + "reactions_count": "{count} reactions", + "no_reactions": "No reactions yet!" + }, + "report": { + "disabled": "Reporting and logging are disabled, contact your instance's owner.", + "spam": "Spam", + "abuse": "Abuse or harassment", + "explicit": "Explicit or dangerous content", + "other": "Violates other room rules", + "misc": "Something else", + "abuse_1": "Impersonation", + "abuse_2": "Harassment", + "abuse_3": "Intentional misinformation", + "abuse_4": "Derogatory language", + "explicit_1": "Adult content (NSFW)", + "explicit_2": "Graphic/gory content", + "explicit_3": "Encouraging real-world harm", + "explicit_4": "Illegal content", + "category_misc": "Other", + "question": "How does this message violate our rules?", + "question_2": "In what way?", + "details_title": "Additional details", + "details_prompt": "Add additional context or information we should know here.", + "sign_title": "Sign with your username", + "sign_prompt": "Sign this only if your report is truthful and in good faith.", + "title": "Report message", + "no_context": "no context given", + "report_title": "Message report - content is as follows", + "submitted_by": "Submitted by {username} - please do not disclose actions taken against the user.", + "review": "Mark as reviewed", + "reviewed": "Marked report as reviewed!", + "reviewed_thread": "This report has been reviewed.", + "reviewed_notice": "This report was reviewed by {moderator}!", + "discussion": "Discussion: #{message_id}", + "success_title": "Your report was submitted!", + "success_body": "Thanks for your report! Our moderators will have a look at it, then decide what to do.\nFor privacy reasons, we will not disclose actions taken against the user.", + "failed": "Something went wrong while submitting the report." + }, + "serverstatus": { + "description": "Shows your server's plugin restriction status.", + "title": "Server status", + "body_ok": "Your server is not restricted by plugins.", + "body_restricted": "Your server is currently limited by a plugin." + }, + "level": { + "description": "Shows you or someone else's level and EXP.", + "disabled": "Leveling system is disabled on this instance.", + "title_self": "Your level", + "title_other": "{username}'s level", + "level": "Level {level}", + "exp": "{exp} EXP", + "progress": "{progress}% towards next level" + }, + "leaderboard": { + "description": "Shows EXP leaderboard.", + "title": "{botname} leaderboard" + }, + "initbridge": { + "description": "Initializes new UnifierBridge object.", + "success": "Bridge initialized" + }, + "system": { + "description": "Sends a message as system.", + "success": "Sent as system" + }, + "bridge": { + "blocked_title": "Content blocked", + "blocked_body": "Your message was blocked. Moderators may be able to see the blocked content.", + "blocked_report_title": "Content blocked - content is as follows", + "punished": "Punished user IDs", + "involved": "{count} users involved", + "automated": "This is an automated action performed by a plugin, always double-check before taking action", + "owner_immunity": "just as a fyi: this would have banned you", + "ban_reason": "Automatic action carried out by security plugins", + "cannot_appeal": "Unfortunately, this ban cannot be appealed using `{prefix}appeal`. You will need to ask moderators for help.", + "limited_limit": "Your server is currently limited for security. The maximum character limit for now is **{count} characters**.", + "limited_cooldown": "Your server is currently limited for security. Please wait before sending another message.", + "delete_fail": "Parent message could not be deleted. I may be missing the `Manage Messages` permission.", + "is_unifier_down": "no", + "debug_msg_ids_fail": "Could not get message IDs.", + "debug_msg_ids_match": "All IDs match. ID: {message_id}", + "debug_msg_ids_mismatch": "Mismatch detected.", + "level_up": "@{username} leveled up!", + "level_progress": "Level {previous} => __Level {new}__", + "post_assigned": "Post ID assigned: `{post_id}`", + "post_id": "Post ID: {post_id}", + "post_reference": "Referencing Post #{post_id}", + "moderated": "Moderated username", + "filesize_limit": "Your files passed the 25MB limit. Some files will not be sent.", + "replying": "Replying to {user}", + "deleted": "Message deleted from `{roomname}`", + "embeds": "{count} embeds", + "files": "{count} files" + } + }, + "moderation": { + "restrict": { + "description": "Blocks a user or server from bridging messages to your server.", + "self_target_user": "You can't restrict yourself :thinking", + "self_target_guild": "You can't restrict your own server :thinking", + "mod_immunity": "Unifier moderators are immune to blocks!\n(Though, do feel free to report anyone who abuses this immunity.)", + "already_blocked": "User/server already blocked!", + "success": "User/server can no longer forward messages to this server!" + }, + "unrestrict": { + "description": "Unblocks a user or server from bridging messages through Unifier.", + "not_blocked": "User/server not blocked!", + "success": "User/server can now forward messages to this server!" + }, + "warn": { + "description": "Warns a user.", + "no_reason": "You need to have a reason to warn this user.", + "self_target": "You can't warn yourself :thinking:", + "bot": "...why would you want to warn a bot?", + "warned": "User warned", + "success": "User has been warned and notified.", + "success_nodm": "User has DMs with bot disabled. Warning will be logged." + }, + "globalban": { + "description": "Bans a user or server from the Unifier network.", + "no_reason": "no reason given", + "self_target": "You can't ban yourself :thinking:", + "already_banned": "User/server already banned!", + "success": "User was global banned. They may not use Unifier for the given time period.", + "banned": "User banned", + "expiry": "Expiry", + "never": "never" + }, + "globalunban": { + "description": "Unbans a user or server from the Unifier network.", + "self_target": "You can't unban yourself :thinking:", + "not_banned": "User/server not banned!", + "success": "User has been unbanned." + }, + "delwarn": { + "description": "Deletes a logged warning.", + "title": "Warning deleted", + "success": "Warning was deleted!", + "failed": "Could not find warning - maybe the index was too high?" + }, + "delban": { + "description": "Deletes a logged ban. Does not unban the user.", + "title": "Ban deleted", + "disclaimer": "WARNING: This does NOT unban the user.", + "success": "Ban was deleted!", + "failed": "Could not find ban - maybe the index was too high?" + }, + "fullban": { + "description": "Blocks a user from using Unifier.", + "self_target": "are you fr", + "owner_immunity": "You cannot ban the owner :thinking:", + "success_set": "User has been banned from the bot.", + "success_unset": "User has been unbanned from the bot." + }, + "appealban": { + "description": "Bans a user from appealing their ban.", + "success_set": "User can no longer appeal bans.", + "success_unset": "User can now appeal bans." + }, + "appeal": { + "description": "Appeals your ban, if you have one.", + "no_ban": "You don't have an active ban!", + "banned": "You cannot appeal this ban, contact staff for more info.", + "missing_ban": "You're currently banned, but we couldn't find the ban reason. Contact moderators directly to appeal.", + "ban": "Global ban", + "confirm": "Please confirm that this is the ban you're appealing.", + "reason_title": "Appeal reason", + "reason_prompt": "Why should we consider your appeal?", + "sign_prompt": "Sign this only if your appeal is in good faith.", + "appeal_title": "Ban appeal - reason is as follows", + "appeal_ban": "Original ban reason", + "accept": "Accept and unban", + "discussion": "Discussion: @{username}", + "success_title": "Your appeal was submitted!", + "success_body": "We'll get back to you once the moderators have agreed on a decision. Please be patient and respectful towards moderators while they review your appeal.", + "reviewed": "Marked appeal as reviewed!", + "reviewed_thread": "This appeal has been closed.", + "accepted_notice": "This appeal was accepted by {moderator}!", + "accepted_title": "Your ban appeal was accepted!", + "accepted_body": "This ban has been removed from your account and will no longer impact your standing.\nYou may now continue chatting!", + "rejected_notice": "This appeal was rejected by {moderator}!", + "rejected_title": "Your ban appeal was denied.", + "rejected_body": "You may continue chatting once the current ban expires." + }, + "delete": { + "description": "Deletes a message.", + "no_message": "No message!", + "no_ownership": "You didn't send this message!", + "parent_delete": "Deleted message (parent deleted, copies will follow)", + "children_delete": "Deleted message ({count} copies deleted)", + "error": "Something went wrong." + }, + "standing": { + "description": "Shows your account standing.", + "title": "{identifier}'s account standing", + "allgood_title": "All good!", + "allgood_body": "You're on a clean or good record. Thank you for upholding your Unifier instance's rules!", + "fair_title": "Fair", + "fair_body": "You've broken one or more rules recently. Please follow the rules next time!", + "caution_title": "Caution", + "caution_body": "You've broken many rules recently. Moderators may issue stronger punishments.", + "warning_title": "WARNING", + "warning_body": "You've severely or frequently violated rules. A permanent suspension may be imminent.", + "suspended_title": "SUSPENDED", + "suspended_body": "You've been temporarily suspended from this Unifier instance.", + "suspended_body_perm": "You've been permanently suspended from this Unifier instance.", + "fullban_title": "COMPLETELY SUSPENDED", + "fullban_body": "This user has been completely suspended from the bot.\nUnlike global bans, the user may also not interact with any part of the bot.", + "bot_title": "Bot account", + "bot_body": "This is a bot. Bots cannot have an account standing.", + "recent": "Recent punishments", + "alltime": "All-time punishments", + "info": "Standing is calculated based on recent and all-time punishments. Recent punishments will have a heavier effect on your standing.", + "no_warns_title": "No warnings", + "no_warns_body": "There's no warnings on record. Amazing!", + "no_bans_title": "No bans", + "no_bans_body": "There's no bans on record. Amazing!", + "success": "Your account standing has been DMed to you." + }, + "servers": { + "description": "Lists all servers connected to a given room.", + "title": "Servers connected to `{room}`" + }, + "identify": { + "description": "Identifies the origin of a message." + }, + "anick": { + "description": "Changes a given user's nickname.", + "success": "Nickname updated." + }, + "bridgelock": { + "description" : "Locks Unifier Bridge down.", + "already_locked": "Bridge already locked down.", + "warning_title": "Lock bridge down?", + "warning_body": "This will shut down external platform clients, as well as unload the entire bridge extension.\nLockdown can only be lifted by admins.", + "lockdown": "Lockdown", + "fwarning_title": "FINAL WARNING!!!", + "fwarning_body": "LOCKDOWNS CANNOT BE REVERSED BY NON-ADMINS!\nDo NOT lock down the chat if you don't know what you're doing!", + "success_title": "LOCKDOWN COMPLETED", + "success_body": "Bridge has been locked down." + }, + "bridgeunlock": { + "description" : "Removes Unifier Bridge lockdown.", + "not_locked": "Bridge not locked down.", + "success": "Lockdown removed" + } + }, + "badge": { + "roles": { + "owner": "the instance's **owner**", + "admin": "the instance's **admin**", + "moderator": "the instance's **moderator**", + "trusted": "a **verified user**", + "banned": "**BANNED**", + "user": "a **user**" + }, + "badge": { + "description": "Shows your Unifier user badge.", + "body": "{mention} is {role}.", + "easter_egg": "L bozo" + }, + "verify": { + "description" : "Verifies a user.", + "invalid_action": "Invalid action. Please use `add` or `remove`.", + "added": "Verified {user}!", + "removed": "Unverified {user}!" + } + }, + "lockdown": { + "lockdown": { + "description": "Locks the entire bot down.", + "already_locked": "Bot is already locked down.", + "warning_title": "Activate lockdown?", + "warning_body": "This will unload ALL EXTENSIONS and lock down the bot until next restart. Continue?", + "continue": "Continue", + "fwarning_title": "FINAL WARNING!!!", + "fwarning_functions": "All functions of the bot will be disabled.", + "fwarning_management": "Managing extensions will be unavailable.", + "fwarning_reboot": "To restore the bot, a reboot is required.", + "success_title": "Lockdown activated", + "success_body": "The bot is now in a crippled state. It cannot recover without a reboot." + } + } + } +} diff --git a/plugins/system.json b/plugins/system.json index 0c067f9b..8f355d94 100644 --- a/plugins/system.json +++ b/plugins/system.json @@ -2,8 +2,8 @@ "id": "system", "name": "System extensions", "description": "Unifier system extensions", - "version": "v2.0.6-patch5", - "release": 64, + "version": "v2.1.0", + "release": 65, "minimum": 0, "shutdown": false, "modules": [ diff --git a/unifier.py b/unifier.py index d4725c07..a1a53033 100644 --- a/unifier.py +++ b/unifier.py @@ -215,6 +215,8 @@ async def on_ready(): logger.debug('System extensions loaded') if hasattr(bot, 'bridge'): try: + logger.debug('Restructuring room data...') + await bot.bridge.convert_1() logger.debug('Optimizing room data, this may take a while...') await bot.bridge.optimize() if len(bot.bridge.bridged) == 0: @@ -242,6 +244,9 @@ async def on_command_error(_ctx, _command): @bot.event async def on_message(message): + if not bot.ready: + return + if not message.webhook_id==None: # webhook msg return diff --git a/utils/langmgr.py b/utils/langmgr.py new file mode 100644 index 00000000..ad05922d --- /dev/null +++ b/utils/langmgr.py @@ -0,0 +1,168 @@ +""" +Unifier - A sophisticated Discord bot uniting servers and platforms +Copyright (C) 2023-present UnifierHQ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + +from nextcord.ext import commands +import ujson as json +from typing import Union +from utils import log +import os + +class LanguageManager: + def __init__(self, bot): + self.bot = bot + self.language_base = {} + self.language_custom = {} + self.language_set = 'english' + if bot: + self.logger = log.buildlogger(self.bot.package, 'langmgr', self.bot.loglevel) + self.__loaded = True + + def load(self): + with open('languages/english.json', 'r') as file: + self.language_base = json.load(file) + if self.bot: + for language in os.listdir('languages'): + if language=='english.json': + continue + if not language.endswith('.json'): + continue + with open(f'languages/{language}.json', 'r') as file: + new_lang = json.load(file) + self.language_custom.update({language[:-5]: new_lang}) + self.language_set = self.bot.config['language'] + self.__loaded = True + + def desc(self, parent): + return self.get('description',parent) + + def get(self, string, parent: Union[commands.Context, str], default="[unknown string]", language=None): + if not self.__loaded: + raise RuntimeError('language not loaded, run LanguageManager.load()') + if not language: + language = self.language_set + if isinstance(parent, commands.Context): + extlist = list(self.bot.extensions) + extname = None + cmdname = parent.command.qualified_name + for x in range(len(self.bot.cogs)): + if self.bot.cogs[x]==parent.cog: + extname = extlist[x] + break + else: + extname, cmdname = parent.split('.') + if not extname: + if self.bot: + self.logger.error('Invalid extension in context, something is very wrong here') + return default + try: + try: + if language=='english': + # throw error so it uses english + raise Exception() + return self.language_custom[language]['strings'][extname][cmdname][string] + except: + return self.language_base['strings'][extname][cmdname][string] + except: + if self.bot: + self.logger.exception('An error occurred!') + return default + + def get_formatted(self, + string, + parent: Union[commands.Context, str], + default=None, + values: dict = None, + language=None): + if not self.__loaded: + raise RuntimeError('language not loaded, run LanguageManager.load()') + if not values: + values = {} + if default: + string = self.get(string, parent, default=default, language=language) + else: + string = self.get(string, parent) + return string.format(**values) + + def fget(self, + string, + parent: Union[commands.Context, str], + default=None, + values: dict = None, + language=None): + """Alias for get_formatted""" + if default: + return self.get_formatted(string, parent, default=default, values=values, language=language) + else: + return self.get_formatted(string, parent, default, values=values, language=language) + + def get_selector(self, parent: Union[commands.Context, str], userid: int = None): + if not self.__loaded: + raise RuntimeError('language not loaded, run LanguageManager.load()') + if isinstance(parent, commands.Context): + extlist = list(self.bot.extensions) + extname = None + cmdname = parent.command.qualified_name + for x in range(len(self.bot.cogs)): + if list(self.bot.cogs)[x]==parent.cog.qualified_name: + extname = extlist[x].replace('cogs.','',1) + break + if not userid: + userid = parent.author.id + else: + if not userid: + raise ValueError('userid must be provided if parent is string') + extname, cmdname = parent.split('.') + return Selector(self, extname, cmdname, userid) + +class Selector: + def __init__(self, parent: LanguageManager, extname, cmdname, userid=None): + self.parent = parent + self.extname = extname + self.cmdname = cmdname + self.language_set = ( + self.parent.bot.db['languages'][f'{userid}'] if f'{userid}' in self.parent.bot.db['languages'].keys() + else parent.language_set + ) + self.userid = userid + + def rawget(self, string, parent: Union[commands.Context, str]): + return self.parent.get(string, parent, language=self.language_set) + + def rawget_formatted(self, string, parent: Union[commands.Context, str], values: dict = None): + return self.parent.get_formatted(string, parent, language=self.language_set, values=values) + + def rawfget(self, string, parent: Union[commands.Context, str], values: dict = None): + return self.parent.get_formatted(string, parent, language=self.language_set, values=values) + + def get(self, string): + return self.parent.get(string, f"{self.extname}.{self.cmdname}", language=self.language_set) + + def get_formatted(self, string, values): + return self.parent.get_formatted( + string, f"{self.extname}.{self.cmdname}", values=values, language=self.language_set + ) + + def fget(self, string, values): + """Alias for get_formatted""" + return self.parent.get_formatted( + string, f"{self.extname}.{self.cmdname}", values=values, language=self.language_set + ) + +def partial(): + # Creates a LanguageManager object without a bot + return LanguageManager(None) diff --git a/utils/log.py b/utils/log.py index 5a6ae377..84ccdbf9 100644 --- a/utils/log.py +++ b/utils/log.py @@ -1,6 +1,6 @@ """ Unifier - A sophisticated Discord bot uniting servers and platforms -Copyright (C) 2024 Green, ItsAsheer +Copyright (C) 2023-present UnifierHQ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as diff --git a/utils/platform_base.py b/utils/platform_base.py new file mode 100644 index 00000000..9c9686b2 --- /dev/null +++ b/utils/platform_base.py @@ -0,0 +1,160 @@ +""" +Unifier - A sophisticated Discord bot uniting servers and platforms +Copyright (C) 2023-present UnifierHQ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + +# This program serves as a template class for bridge platform Plugins. +# We recommend all such Plugins inherit this class, so missing implementations +# can be handled gracefully by the bot. + +import nextcord + +class MissingImplementation(Exception): + """An exception used when something isn't implemented. + The bot will gracefully handle this exception""" + pass + +class PlatformBase: + def __init__(self, bot, parent): + self.bot = bot + self.parent = parent + self.enable_tb = False # change this to True to enable threaded bridge + self.uses_webhooks = False # change this to True if webhooks are needed + self.__available = False + self.allowed_content_types = [] + + def is_available(self): + return self.__available + + def attach_bot(self, bot): + """In case a bot object could not be provided, it can be attached here.""" + self.bot = bot + self.__available = True + + def bot_id(self): + raise MissingImplementation() + + def get_server(self, server_id): + """Gets a server from cache.""" + raise MissingImplementation() + + def get_channel(self, channel_id): + """Gets a channel from cache.""" + raise MissingImplementation() + + def channel(self, message): + """Returns the channel object from a message.""" + raise MissingImplementation() + + def server(self, obj): + """Returns the server object from an object.""" + raise MissingImplementation() + + def content(self, message): + """Returns the content from a message.""" + raise MissingImplementation() + + def author(self, message): + """Returns the author object from a message.""" + raise MissingImplementation() + + def embeds(self, message): + raise MissingImplementation() + + def attachments(self, message): + raise MissingImplementation() + + def url(self, message): + """Returns the URL for a message.""" + raise MissingImplementation() + + def get_id(self, obj): + """Returns the ID from any object.""" + raise MissingImplementation() + + def display_name(self, user): + """Returns the display name of a user object, username if no display name is set.""" + raise MissingImplementation() + + def user_name(self, user): + """Returns the username of a user object.""" + raise MissingImplementation() + + def avatar(self, user): + """Returns the avatar URL of a user object.""" + raise MissingImplementation() + + def is_bot(self, user): + """Returns if the user is a bot or not.""" + raise MissingImplementation() + + def attachment_size(self, attachment): + """Returns the size of a given attachment.""" + raise MissingImplementation() + + def attachment_type(self, attachment): + """Returns the content type of a given attachment.""" + raise MissingImplementation() + + def attachment_type_allowed(self, content_type): + """Returns if the content type can be bridged. + If allowed_content_types is empty, this will always return True.""" + return len(self.allowed_content_types) == 0 or content_type in self.allowed_content_types + + def convert_embeds(self, embeds): + raise MissingImplementation() + + def convert_embeds_discord(self, embeds): + raise MissingImplementation() + + def webhook_id(self, message): + """Returns the webhook ID from a message.""" + raise MissingImplementation() + + async def fetch_server(self, server_id): + """Fetches the server from the API.""" + raise MissingImplementation() + + async def fetch_channel(self, channel_id): + """Fetches the channel from the API.""" + raise MissingImplementation() + + async def make_friendly(self, text): + """Converts the message so it's friendly with other platforms. + For example, <@userid> should be converted to @username.""" + raise MissingImplementation() + + async def to_discord_file(self, file): + """Converts an attachment object to a nextcord.File object.""" + raise MissingImplementation() + + async def to_platform_file(self, file: nextcord.Attachment): + """Converts a nextcord.Attachment object to the platform's file object.""" + raise MissingImplementation() + + async def send(self, channel, content, special: dict = None): + """Sends a message to a channel, then returns the message object. + Special features, such as embeds and files, can be specified in special.""" + raise MissingImplementation() + + async def edit(self, message, content, special: dict = None): + """Edits a message. + Special features, such as embeds and files, can be specified in special.""" + raise MissingImplementation() + + async def delete(self, message): + """Deletes a message.""" + raise MissingImplementation() diff --git a/utils/restrictions.py b/utils/restrictions.py index 3d968996..6aa45c0d 100644 --- a/utils/restrictions.py +++ b/utils/restrictions.py @@ -1,3 +1,21 @@ +""" +Unifier - A sophisticated Discord bot uniting servers and platforms +Copyright (C) 2023-present UnifierHQ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + from nextcord.ext import commands class Restrictions: @@ -33,6 +51,40 @@ async def predicate(ctx: commands.Context): return commands.check(predicate) + def join_room(self): + async def predicate(ctx: commands.Context): + index = 0 + + # the below is to be used if we ever make a command + # that has the room arg not in position 0 + # if ctx.command.qualified_name == "name": + # index = 1 + + room = ctx.args[index] + try: + return self.__bot.bridge.can_join_room(room,ctx.author) + except: + return False + + return commands.check(predicate) + + def manage_room(self): + async def predicate(ctx: commands.Context): + index = 0 + + # the below is to be used if we ever make a command + # that has the room arg not in position 0 + # if ctx.command.qualified_name == "name": + # index = 1 + + room = ctx.args[index] + try: + return self.__bot.bridge.can_manage_room(room,ctx.author) + except: + return False + + return commands.check(predicate) + def demo_error(self): """A demo check which will always fail, intended for development use only.""" diff --git a/utils/rolemgr.py b/utils/rolemgr.py new file mode 100644 index 00000000..fe18773b --- /dev/null +++ b/utils/rolemgr.py @@ -0,0 +1,20 @@ +""" +Unifier - A sophisticated Discord bot uniting servers and platforms +Copyright (C) 2023-present UnifierHQ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + +# Work in progress role handler. +# This will probably remain empty until v2.1.1 work starts diff --git a/utils/ui.py b/utils/ui.py index 27a5d0da..f6a3294e 100644 --- a/utils/ui.py +++ b/utils/ui.py @@ -1,7 +1,7 @@ """ MIT License -Copyright (c) 2024 UnifierHQ +Copyright (c) 2024-present UnifierHQ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,10 @@ SOFTWARE. """ +# This part of Unifier is licensed under the MIT license, rather than AGPLv3. +# If you're only using this specific file, you may accept the MIT license +# rather than the AGPLv3 for the full project. + import nextcord class ActionRow: