Skip to content

Commit

Permalink
Merge pull request #183 from UnifierHQ/dev
Browse files Browse the repository at this point in the history
v3.7.0: QoL and patches
greeeen-dev authored Nov 19, 2024
2 parents 1b93435 + 4af15a8 commit c62bbf6
Showing 15 changed files with 596 additions and 260 deletions.
237 changes: 234 additions & 3 deletions boot/bootloader.py
Original file line number Diff line number Diff line change
@@ -17,11 +17,18 @@
"""

import os
import platform
import sys
import shutil
import json
import time
import getpass
import subprocess
from pathlib import Path

if sys.version_info.major < 3 or sys.version_info.minor < 9:
print('\x1b[31;1mThis version of Python is unsupported. You need Python 3.9 or newer.\x1b[0m')
sys.exit(1)

reinstall = '--reinstall' in sys.argv
depinstall = '--install-deps' in sys.argv
@@ -55,6 +62,132 @@
autoreboot = bootloader_config.get('autoreboot', False)
threshold = bootloader_config.get('autoreboot_threshold', 60)

cgroup = Path('/proc/self/cgroup')
uses_docker = Path('/.dockerenv').is_file() or cgroup.is_file() and 'docker' in cgroup.read_text()

class PythonInstallation:
def __init__(self, major, minor, patch, filepath, default=False, emulated=False, venv=False):
self.__version = (major, minor, patch)
self.__filepath = filepath
self.__default = default
self.__emulated = emulated
self.__venv = venv

def __str__(self):
extras = ''
if self.__default:
extras += ' \U00002B50'
if self.__emulated:
extras += ' (Emulated x86_64)'
return f'Python {self.__version[0]}.{self.__version[1]}.{self.__version[2]} @ {self.__filepath}{extras}'

@property
def version(self):
return self.__version

@property
def emulated(self):
return self.__emulated

@property
def filepath(self):
return self.__filepath

@property
def default(self):
return self.__default

@property
def venv(self):
return self.__venv

@property
def supported(self):
return self.__version >= (3, 9, 0)

def check_for_python(path, found=None, venv=False):
versions = []
blacklist = ['-config', 't', 't-config', 't-intel64']

if not found:
found = []

defaults = [
file for file in
subprocess.check_output([f'whereis', 'python3']).decode('utf-8').replace('\n','').split(' ')
if not file == 'python3:'
]

try:
for item in os.listdir(path):
emulated = False

if not item.startswith('python'):
continue
if True in [item.endswith(blacklisted) for blacklisted in blacklist]:
continue

if item.endswith('-intel64'):
if not platform.uname().machine == 'x86_64':
emulated = True

try:
output = subprocess.check_output([f'{path}/{item}', '--version'])
except:
continue

versiontext = output.decode('utf-8').replace('\n','').split(' ')[1]
major, minor, patch = versiontext.split('.')

if not patch.isdigit():
fixed = ''
for char in patch:
if not char.isdigit():
return
fixed += char
patch = fixed

installation = PythonInstallation(
int(major), int(minor), int(patch), f'{path}/{item}', default=f'{path}/{item}' in defaults,
emulated=emulated, venv=venv
)
if not installation in versions and not installation in found:
versions.append(installation)
except FileNotFoundError:
return []

return versions or []


if boot_config.get('ptero') is None and uses_docker:
print('\x1b[33;1mWe detected that you are running Unifier in a Docker container.\x1b[0m')
print('\x1b[33;1mThis may mean that you are using Pterodactyl/Pelican panel to run your server.\x1b[0m')
print('\x1b[33;1mIf this is the case, we recommend enabling Pterodactyl support.\x1b[0m')
print('\x1b[33;1mEnable Pterodactyl support? (y/n)\x1b[0m')

try:
answer = input().lower()
enable_ptero = answer == 'y'

if answer == 'y':
boot_config['ptero'] = True
elif answer == 'n':
boot_config['ptero'] = False
else:
print('\x1b[33;1mInvalid answer, defaulting to no. We will ask you again on next boot.\x1b[0m')

if answer == 'y' or answer == 'n':
with open('boot_config.json', 'w+') as file:
# noinspection PyTypeChecker
json.dump(boot_config, file, indent=4)
except KeyboardInterrupt:
print('\x1b[31;1mAborting.\x1b[0m')
sys.exit(1)

ptero_support = uses_docker and enable_ptero
else:
ptero_support = uses_docker and boot_config.get('ptero', False)

if not options:
options = ''
else:
@@ -90,8 +223,8 @@

for index in range(len(install_options)):
option = install_options[index]
print(f'{option["color"]};1m{option["name"]} (option {index})\x1b[0m')
print(f'{option["color"]}m{option["description"]}\x1b[0m')
print(f'\x1b[{option["color"]};1m{option["name"]} (option {index})\x1b[0m')
print(f'\x1b[{option["color"]}m{option["description"]}\x1b[0m')

print(f'\n\x1b[33;1mWhich installation option would you like to install? (0-{len(install_options)-1})\x1b[0m')

@@ -106,6 +239,103 @@

install_option = install_options[install_option]['id']

if not uses_docker and not sys.platform == 'win32':
print('\x1b[33;1mDetecting python installations...\x1b[0m')
installed = []
installed.extend(check_for_python('/usr/bin') or [])
installed.extend(check_for_python('/usr/local/bin', found=installed) or [])

if os.path.exists('/Library/Frameworks/Python.framework/Versions'):
for item in os.listdir('/Library/Frameworks/Python.framework/Versions'):
installed.extend(check_for_python(f'/Library/Frameworks/Python.framework/Versions/{item}/bin', found=installed) or [])

for item in os.listdir():
if not os.path.isdir(item):
continue

installed.extend(check_for_python(f'{item}/bin', found=installed, venv=True))

if len(installed) <= 1:
print('\x1b[33;1mOnly the default Python installation was detected, using default.\x1b[0m')
else:
installed.sort(key=lambda x: x.version, reverse=True)
print(f'\x1b[33;1mFound {len(installed)} available Python installations:\x1b[0m')
for index in range(len(installed)):
print(f'\x1b[33m({index+1}) {installed[index]}\x1b[0m')

print(f'\n\x1b[33;1mWhich version would you like to use? (1-{len(installed)})\x1b[0m')

choice = None
while True:
try:
choice = int(input()) - 1
if choice < 0 or choice >= len(installed):
raise ValueError()
except ValueError:
print(f'\x1b[31;1mInvalid answer, try again.\x1b[0m')
continue
except KeyboardInterrupt:
print(f'\x1b[31;1mAborting.\x1b[0m')
sys.exit(1)
break

binary = installed[choice].filepath
if installed[choice].venv:
if not boot_config.get('bootloader'):
boot_config.update({'bootloader': {}})
boot_config['bootloader'].update({'binary': binary})
boot_config['bootloader'].update({'global_dep_install': True})
with open('boot_config.json', 'w+') as file:
# noinspection PyTypeChecker
json.dump(boot_config, file, indent=4)
else:
bootloader_exists = os.path.isdir('.venv')

if bootloader_exists:
print('\n\x1b[33;1mAn existing virtual environment was found. Would you like to use it? (y/n)\x1b[0m')
else:
print('\n\x1b[33;1mWould you like to use a virtual environment? This is highly recommended as this prevents\x1b[0m')
print('\x1b[33;1mglobal packages from breaking and allows for easier recovery. (y/n)\x1b[0m')

use_venv = False
while True:
try:
choice = input().lower()
if not choice == 'y' and not choice == 'n':
raise ValueError()
except ValueError:
print(f'\x1b[31;1mInvalid answer, try again.\x1b[0m')
continue
except KeyboardInterrupt:
print(f'\x1b[31;1mAborting.\x1b[0m')
sys.exit(1)

use_venv = choice == 'y'
break

if not boot_config.get('bootloader'):
boot_config.update({'bootloader': {}})

if use_venv:
if not bootloader_exists:
print(f'\n\x1b[33;1mCreating virtual environment in {os.getcwd()}/.venv...\x1b[0m')
code = os.system(f'{binary} -m venv .venv')
if code == 0:
print(f'\x1b[36;1mVirtual environment created successfully.\x1b[0m')
else:
print(f'\x1b[31;1mFailed to create virtual environment, aborting.\x1b[0m')
sys.exit(1)
binary = './.venv/bin/python'
boot_config['bootloader'].update({'binary': binary})
boot_config['bootloader'].update({'global_dep_install': True})
else:
boot_config['bootloader'].update({'binary': binary})
boot_config['bootloader'].update({'global_dep_install': False})

with open('boot_config.json', 'w+') as file:
# noinspection PyTypeChecker
json.dump(boot_config, file, indent=4)

print('\x1b[33;1mPlease review the following before continuing:\x1b[0m')
print(f'- Product to install: {internal["product_name"]}')
print(f'- Installation option: {install_option}')
@@ -139,7 +369,6 @@
sys.exit(exit_code)

if depinstall:
print('\x1b[36;1mDependencies installed successfully.\x1b[0m')
sys.exit(0)

exit_code = os.system(f'{binary} boot/installer.py {install_option}{options}')
@@ -213,6 +442,8 @@
encrypted = os.path.isfile('.encryptedenv')
if not choice is None and os.environ.get('UNIFIER_ENCPASS') is None:
# choice is set but not the password, likely due to wrong password
if ptero_support:
print(f'\x1b[36;1mPlease enter your encryption password using the console input.\x1b[0m')
encryption_password = str(getpass.getpass('Password: '))
os.environ['UNIFIER_ENCPASS'] = str(encryption_password)
elif not choice is None:
15 changes: 15 additions & 0 deletions boot/dep_installer.py
Original file line number Diff line number Diff line change
@@ -60,6 +60,21 @@
else:
code = os.system(f'{binary} -m pip install{user_arg} -U -r requirements.txt')

for plugin in os.listdir('plugins'):
if not plugin.endswith('.json') or plugin == 'system.json':
continue

try:
with open(f'plugins/{plugin}') as file:
plugin_data = json.load(file)
except:
continue

if len(plugin_data['requirements']) > 0:
code = os.system(f'{binary} -m pip install{user_arg} -U {" ".join(plugin_data["requirements"])}')
if not code == 0:
break

if not code == 0:
print('\x1b[31;1mCould not install dependencies.\x1b[0m')
print('\x1b[31;1mIf you\'re using a virtualenv, you might want to set global_dep_install to true in bootloader configuration to fix this.\x1b[0m')
36 changes: 33 additions & 3 deletions boot/installer.py
Original file line number Diff line number Diff line change
@@ -25,13 +25,23 @@
import tomli_w
import traceback
from nextcord.ext import commands
from pathlib import Path
from typing_extensions import Self

try:
sys.path.insert(0, '.')
from utils import secrets
except:
raise

with open('boot_config.json') as file:
boot_config = json.load(file)

cgroup = Path('/proc/self/cgroup')
uses_docker = Path('/.dockerenv').is_file() or cgroup.is_file() and 'docker' in cgroup.read_text()

ptero_support = uses_docker and boot_config.get('ptero', False)

install_option = sys.argv[1] if len(sys.argv) > 1 else None

with open('boot/internal.json') as file:
@@ -45,9 +55,23 @@
install_option = option['id']
break

class Intents(nextcord.Intents):
def __init__(self, **kwargs):
super().__init__(**kwargs)

@classmethod
def no_presence(cls) -> Self:
"""A factory method that creates a :class:`Intents` with everything enabled
except :attr:`presences`, :attr:`members`, and :attr:`message_content`.
"""
self = cls.all()
self.presences = False
return self


bot = commands.Bot(
command_prefix='u!',
intents=nextcord.Intents.all()
intents=Intents.no_presence()
)

user_id = 0
@@ -137,7 +161,10 @@ async def on_ready():
print(f'\x1b[31;49m{internal["maintainer"]} will NEVER ask for your token. Please keep this token to yourself and only share it with trusted instance maintainers.\x1b[0m')
print('\x1b[31;49mFor security reasons, the installer will hide the input.\x1b[0m')

token = getpass.getpass()
if ptero_support:
print(f'\x1b[36;1mPlease enter your bot token using the console input.\x1b[0m')

token = getpass.getpass('Token: ')

encryption_password = ''
salt = ''
@@ -160,6 +187,9 @@ async def on_ready():
print(f'\x1b[31;49m{internal["maintainer"]} will NEVER ask for your encryption password. Please keep this password to yourself and only share it with trusted instance maintainers.\x1b[0m')
print('\x1b[31;49mFor security reasons, the installer will hide the input.\x1b[0m')

if ptero_support:
print(f'\x1b[36;1mPlease enter your encryption password using the console input.\x1b[0m')

encryption_password = getpass.getpass()

print('\x1b[36;1mStarting bot...\x1b[0m')
@@ -169,7 +199,7 @@ async def on_ready():
except:
traceback.print_exc()
print('\x1b[31;49mLogin failed. Perhaps your token is invalid?\x1b[0m')
print('\x1b[31;49mMake sure all privileged intents are enabled for the bot.\x1b[0m')
print('\x1b[31;49mMake sure Server Members and Message Content intents are enabled for the bot.\x1b[0m')
sys.exit(1)

tokenstore = secrets.TokenStore(True, password=encryption_password, salt=salt, content_override={'TOKEN': token})
10 changes: 5 additions & 5 deletions boot/internal.json
Original file line number Diff line number Diff line change
@@ -10,21 +10,21 @@
"options": [
{
"id": "optimized",
"name": "\u26a1 Optimized",
"name": "\u26a1\ufe0f Optimized",
"description": "Uses the latest Nextcord and performance optimizations for blazing fast bridging.",
"default": true,
"prefix": "",
"required_py_version": 12,
"color": "\\x1b[35"
"color": "35"
},
{
"id": "balanced",
"name": "\u26a1 Balanced",
"name": "\u2764\ufe0f Balanced",
"description": "Uses performance optimizations for balanced performance and stability. Recommended for most users.",
"default": false,
"prefix": "balanced",
"required_py_version": 12,
"color": "\\x1b[34"
"color": "34"
},
{
"id": "stable",
@@ -33,7 +33,7 @@
"default": false,
"prefix": "stable",
"required_py_version": 9,
"color": "\\x1b[32"
"color": "32"
}
]
}
4 changes: 1 addition & 3 deletions cogs/badge.py
Original file line number Diff line number Diff line change
@@ -37,9 +37,7 @@ class UserRole(Enum):
USER = language.get('user','badge.roles')

class Badge(commands.Cog, name=':medal: Badge'):
"""Badge contains commands that show you your role in Unifier.
Developed by Green and ItsAsheer"""
"""Badge contains commands that show you your role in Unifier."""

def __init__(self, bot):
global language
214 changes: 134 additions & 80 deletions cogs/bridge.py

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions cogs/config.py
Original file line number Diff line number Diff line change
@@ -64,9 +64,7 @@ def timetoint(t):
return total

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.
Developed by Green and ItsAsheer"""
"""Config is an extension that lets Unifier admins configure the bot and server moderators set up Unified Chat in their server."""

def __init__(self,bot):
global language
4 changes: 1 addition & 3 deletions cogs/lockdown.py
Original file line number Diff line number Diff line change
@@ -33,9 +33,7 @@
language.load()

class Lockdown(commands.Cog, name=':lock: Lockdown'):
"""An emergency extension that unloads literally everything.
Developed by Green and ItsAsheer"""
"""An emergency extension that unloads literally everything."""

def __init__(self,bot):
global language
4 changes: 1 addition & 3 deletions cogs/moderation.py
Original file line number Diff line number Diff line change
@@ -83,9 +83,7 @@ def timetoint(t,timeoutcap=False):
return total

class Moderation(commands.Cog, name=":shield: Moderation"):
"""Moderation allows server moderators and instance moderators to punish bad actors.
Developed by Green and ItsAsheer"""
"""Moderation allows server moderators and instance moderators to punish bad actors."""

def __init__(self,bot):
global language
8 changes: 6 additions & 2 deletions cogs/setup.py
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ def __init__(self, bot):
self.__bot = bot
self.embed = nextcord.Embed()
self.language = self.__bot.langmgr
self.use_language = None
self.use_language = 'english'
self.message = None
self.user = self.__bot.get_user(self.__bot.owner)

@@ -431,6 +431,8 @@ async def custom(self, title, description, image_url=None, modal=None, defaults=
return None
if modal.custom_id == 'summon_modal':
return None
if not isinstance(defaults, list):
defaults = [defaults]

self.update(title, description, image_url=image_url)

@@ -477,7 +479,9 @@ async def custom(self, title, description, image_url=None, modal=None, defaults=

return values

class Setup(commands.Cog):
class Setup(commands.Cog, name=":beginner: Setup"):
"""Setup provides a guided setup process for the bot for both instance owners and server owners and admins."""

def __init__(self, bot):
self.bot = bot
self.logger = log.buildlogger(self.bot.package, 'setup', self.bot.loglevel)
300 changes: 150 additions & 150 deletions cogs/sysmgr.py
Original file line number Diff line number Diff line change
@@ -58,6 +58,8 @@
import time
import shutil
import datetime
from enum import Enum
from typing import Union

# import ujson if installed
try:
@@ -474,10 +476,13 @@ def check(interaction):
self.logger.exception('An error occurred!')
await respond(f'{self.bot.ui_emojis.error} {selector.get("handler_error")}')

class SysManager(commands.Cog, name=':wrench: System Manager'):
"""An extension that oversees a lot of the bot system.
class CogAction(Enum):
load = 0
reload = 1
unload = 2

Developed by Green"""
class SysManager(commands.Cog, name=':wrench: System Manager'):
"""An extension that oversees a lot of the bot system."""

class SysExtensionLoadFailed(Exception):
pass
@@ -923,6 +928,143 @@ def check(interaction):
await self.bot.close()
sys.exit(0)

async def manage_cog(self, cogs: list, action: CogAction):
toload = []
skip = []
success = []
failed = {}

async def run_check(plugin_name, plugin):
if not plugin['shutdown']:
return

script = importlib.import_module('utils.' + plugin_name + '_check')
await script.check(self.bot)

for cog in cogs:
cog_exists = f'{cog}.py' in os.listdir('cogs')
plugin_exists = f'{cog}.json' in os.listdir('plugins')

plugin_data = {}

if plugin_exists:
with open(f'plugins/{cog}.json') as file:
plugin_data = json.load(file)

if plugin_exists and cog_exists:
if self.bot.config['plugin_priority']:
toload.extend(plugin_data['modules'])
await run_check(cog, plugin_data)
else:
toload.append(f'cogs.{cog}')
elif plugin_exists:
toload.extend([f'cogs.{module[:-3]}' for module in plugin_data['modules']])

if not action == CogAction.load:
try:
await run_check(cog, plugin_data)
except:
skip.extend([f'cogs.{module}' for module in plugin_data['modules']])
for child_cog in [f'cogs.{module}' for module in plugin_data['modules']]:
failed.update({child_cog: 'Could not run pre-unload script.'})
elif cog_exists:
toload.append(f'cogs.{cog}')
else:
toload.append(f'cogs.{cog}')
skip.append(f'cogs.{cog}')
failed.update({cog: 'The cog or plugin does not exist.'})
for toload_cog in toload:
if toload_cog in skip:
continue
try:
if action == CogAction.load:
self.bot.load_extension(toload_cog)
elif action == CogAction.reload:
self.bot.reload_extension(toload_cog)
elif action == CogAction.unload:
self.bot.unload_extension(toload_cog)
success.append(toload_cog)
except:
e = traceback.format_exc()
failed.update({toload_cog: e})
return len(toload), success, failed

async def manage_cog_cmd(self, ctx: Union[commands.Context, nextcord.Interaction], action: CogAction, cogs: str):
if type(ctx) is commands.Context:
selector = language.get_selector('sysmgr.manage_cog', userid=ctx.author.id)
author = ctx.author
else:
selector = language.get_selector('sysmgr.manage_cog', userid=ctx.user.id)
author = ctx.user

if self.bot.update:
return await ctx.send(selector.get('disabled'))

cogs = cogs.split(' ')

if action == CogAction.load:
action_str = 'load'
elif action == CogAction.reload:
action_str = 'reload'
elif action == CogAction.unload:
action_str = 'unload'
else:
# default to load
action_str = 'load'

msg = await ctx.send(f'{self.bot.ui_emojis.loading} {selector.get(action_str)}')
if type(ctx) is nextcord.Interaction:
msg = await msg.fetch()

total, success, failed = await self.manage_cog(cogs, action)

components = ui.MessageComponents()
if failed:
selection = nextcord.ui.StringSelect(
placeholder=selector.get("viewerror"),
max_values=1,
min_values=1,
custom_id='selection'
)

for fail in failed.keys():
selection.add_option(
label=fail,
value=fail
)

components.add_row(
ui.ActionRow(selection)
)

touse_emoji = self.bot.ui_emojis.success if len(success) == total else self.bot.ui_emojis.warning

await msg.edit(
content=f'{touse_emoji} {selector.fget("completed", values={"total":total, "success": len(success)})}',
view=components
)

if not failed:
return

while True:
def check(interaction):
if not interaction.message:
return

return interaction.message.id == msg.id and interaction.user.id == author.id

try:
interaction = await self.bot.wait_for('interaction',check=check,timeout=60)
except asyncio.TimeoutError:
return await msg.edit(view=None)

selected = interaction.data['values'][0]
error = failed[selected]
await interaction.response.send_message(
f'{self.bot.ui_emojis.error} {selected}\n```\n{error}```', ephemeral=True
)

@tasks.loop(seconds=300)
async def changestatus(self):
dt = datetime.datetime.now(datetime.timezone.utc)
@@ -1277,161 +1419,17 @@ async def extensions(self, ctx, *, extension=None):
@commands.command(hidden=True,description=language.desc('sysmgr.reload'))
@restrictions_legacy.owner()
async def reload(self, ctx, *, extensions):
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 = []
error_objs = []
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 error:
e = traceback.format_exc()
failed.append(extension)
errors.append(e)
error_objs.append(error)
if len(text) == 0:
text = f'```diff\n- [FAIL] {extension}'
else:
text += f'\n- [FAIL] {extension}'
if len(extensions) - len(failed) > 0:
await self.bot.discover_application_commands()
await self.bot.register_new_application_commands()
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_services")} `{fail}`\n```{errors[index]}```'
else:
text = f'\n\n{selector.rawget("extension","sysmgr.reload_services")} `{fail}`\n```{errors[index]}```'
index += 1
if not len(failed) == 0:
if len(text) > 2000:
for error in error_objs:
self.logger.exception('An error occurred!', exc_info=error)
return await ctx.author.send(selector.rawget("too_long", "sysmgr.reload_services"))
await ctx.author.send(f'**{selector.rawget("fail_logs","sysmgr.reload_services")}**\n{text}')
await self.manage_cog_cmd(ctx, CogAction.reload, extensions)

@commands.command(hidden=True,description=language.desc('sysmgr.load'))
@restrictions_legacy.owner()
async def load(self, ctx, *, extensions):
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 = []
error_objs = []
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 error:
e = traceback.format_exc()
failed.append(extension)
errors.append(e)
error_objs.append(error)
if len(text) == 0:
text = f'```diff\n- [FAIL] {extension}'
else:
text += f'\n- [FAIL] {extension}'
if len(extensions) - len(failed) > 0:
await self.bot.discover_application_commands()
await self.bot.register_new_application_commands()
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:
if len(text) > 2000:
for error in error_objs:
self.logger.exception('An error occurred!', exc_info=error)
return await ctx.author.send(selector.rawget("too_long", "sysmgr.reload_services"))
await ctx.author.send(f'**{selector.rawget("fail_logs","sysmgr.reload_services")}**\n{text}')
await self.manage_cog_cmd(ctx, CogAction.load, extensions)

@commands.command(hidden=True,description='Unloads an extension.')
@restrictions_legacy.owner()
async def unload(self, ctx, *, extensions):
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 = []
error_objs = []
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 error:
e = traceback.format_exc()
failed.append(extension)
errors.append(e)
error_objs.append(error)
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:
if len(text) > 2000:
for error in error_objs:
self.logger.exception('An error occurred!', exc_info=error)
return await ctx.author.send(selector.rawget("too_long", "sysmgr.reload_services"))
await ctx.author.send(f'**{selector.rawget("fail_logs","sysmgr.reload_services")}**\n{text}')
await self.manage_cog_cmd(ctx, CogAction.unload, extensions)

@commands.command(hidden=True,description='Installs a plugin.')
@restrictions_legacy.owner()
@@ -3239,6 +3237,8 @@ async def about(self, ctx: nextcord.Interaction):
else:
footer_text = "Unknown version | Made with \u2764\ufe0f by UnifierHQ"

footer_text += f'\nUsing Nextcord {nextcord.__version__} on Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'

while True:
embed = nextcord.Embed(
title=self.bot.user.global_name or self.bot.user.name,
1 change: 1 addition & 0 deletions config.toml
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ package = "unifier"
language = "english"
skip_status_check = false
encrypted_env_salt = 10 # change this to whatever you want, as long as the bot doesn't crash
plugin_priority = true # set this to false if you want to load cogs instead of modifiers with the same name

[roles]
owner = -1
13 changes: 11 additions & 2 deletions languages/english.json
Original file line number Diff line number Diff line change
@@ -172,6 +172,14 @@
"notfound": "Could not find extension!",
"system_module": "\n# SYSTEM MODULE\nThis module cannot be unloaded."
},
"manage_cog": {
"reload": "Reloading extensions...",
"load": "Loading extensions...",
"unload": "Unloading extensions...",
"completed": "Completed ({success}/{total} successful)",
"disabled": "Extensions and Modifier management is disabled until restart.",
"viewerror": "Select failed job..."
},
"reload": {
"description": "Reloads an extension.",
"disabled": "Extensions and Modifier management is disabled until restart.",
@@ -611,7 +619,6 @@
"check_body": "We're checking if this channel is already connected. Give us a moment...",
"already_linked_title": "Already connected",
"already_linked_body": "This channel is already connected to `{room}`!\nRun `/bridge unbind {room}` to disconnect the channel.",
"already_room": "Your server is already linked to this room.\n**Accidentally deleted the webhook?** `/bridge unbind {room}` it then `/bridge bind {room}` it back.",
"join_title": "Join {roomname}?",
"no_rules": "This room doesn't have any rules yet! For now, follow the main room's rules (and common sense).\nYou can always view rules if any get added using `/bridge rules {room}`.",
"disclaimer": "Failure to follow room rules may result in user or server restrictions.",
@@ -621,7 +628,8 @@
"failed": "Failed to connect.",
"invalid_invite": "Invite is invalid.",
"room_banned": "You are banned from this room.",
"too_many": "Server has reached maximum number of connections.",
"too_many": "Server has reached maximum number of Private Rooms connections.",
"already_linked_server": "Your server is already binded to this room. Unbind the room and try again.",
"success": "Connected to room!",
"say_hi": "You're now connected! Say hi!"
},
@@ -1326,6 +1334,7 @@
"welcome_body": "Welcome to Unifier - an open source, fast, and versatile bridge bot.",
"welcome_continue": "First, let's choose your system language, so we can get your Unifier set up and running!",
"welcome_upgraded": "An upgraded installation was detected. If you've used this Unifier instance before, press \"Skip\".",
"select_language": "Select a language...",
"prefix_title": "Command prefix",
"prefix_body": "A prefix is the text that comes before the command. For example, if the prefix is `!`, users will need to run `!command`.",
"prefix_default": "Default value: `u!`",
4 changes: 2 additions & 2 deletions plugins/system.json
Original file line number Diff line number Diff line change
@@ -2,8 +2,8 @@
"id": "system",
"name": "System extensions",
"description": "Unifier system extensions",
"version": "v3.6.7",
"release": 136,
"version": "v3.7.0",
"release": 137,
"minimum": 0,
"shutdown": false,
"modules": [
2 changes: 1 addition & 1 deletion unifier.py
Original file line number Diff line number Diff line change
@@ -110,7 +110,7 @@
try:
# as only winloop or uvloop will be installed depending on the system,
# we will ask pylint to ignore importerrors for both
if os.name == "win32":
if sys.platform == "win32":
# noinspection PyUnresolvedReferences
import winloop as uvloop # pylint: disable=import-error
else:

0 comments on commit c62bbf6

Please sign in to comment.