From 5f8951acdc902d059075bbf37e792dc50872eeaa Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Mon, 15 Jul 2024 10:21:39 +0200
Subject: [PATCH 01/22] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6c4a146..d8d0e34 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ speed, as well as limits the bot to support Discord only.
## Who should use this?
Unifier Micro is built for small communities just wanting to give Unifier a spin, or communities with very limited
-resources to run Unifier. For communities of scale with decent resources, we recommend using the [full-scale
+resources to run Unifier. For larger communities than have decent resources, we recommend using the [full-scale
version](https://github.com/UnifierHQ/unifier) instead.
## Features
From 0c9418bd66c9da9ac712afbed01d8f9e1d2fc951 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Mon, 15 Jul 2024 10:22:19 +0200
Subject: [PATCH 02/22] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d8d0e34..9bb4689 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
Unifier Micro
From 2493487a391ca707c95f6082db831f37b8b7e70d Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Mon, 15 Jul 2024 10:25:31 +0200
Subject: [PATCH 03/22] this logo sucks
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 9bb4689..1d34a35 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
Unifier Micro
From ae6d3f20e9f34b7dc74b4615619fee504cda793b Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Wed, 18 Sep 2024 09:54:43 +0200
Subject: [PATCH 04/22] unifier v3
---
boot/bootloader.py | 167 ++++++++++++++++++++++++++++++++++++++++++
boot/dep_installer.py | 54 ++++++++++++++
boot/installer.py | 160 ++++++++++++++++++++++++++++++++++++++++
boot/internal.json | 8 ++
microfier.py | 149 +++++++++++++++++++++++++++----------
requirements.txt | 4 +-
run.bat | 9 +++
run.sh | 10 +++
8 files changed, 523 insertions(+), 38 deletions(-)
create mode 100644 boot/bootloader.py
create mode 100644 boot/dep_installer.py
create mode 100644 boot/installer.py
create mode 100644 boot/internal.json
create mode 100644 run.bat
create mode 100644 run.sh
diff --git a/boot/bootloader.py b/boot/bootloader.py
new file mode 100644
index 0000000..518d713
--- /dev/null
+++ b/boot/bootloader.py
@@ -0,0 +1,167 @@
+import os
+import sys
+import shutil
+import json
+import time
+
+reinstall = '--reinstall' in sys.argv
+
+install_options = [
+ {
+ 'id': 'stable',
+ 'name': '\U0001F48E Standard',
+ 'description': 'Uses the latest stable Nextcord version.',
+ 'default': True,
+ 'prefix': '',
+ 'color': '\x1b[32'
+ }
+]
+
+if os.getcwd().endswith('/boot'):
+ print('\x1b[31;1mYou are running the bootloader directly. Please run the run.sh file instead.\x1b[0m')
+ sys.exit(1)
+
+with open('boot/internal.json') as file:
+ internal = json.load(file)
+
+
+boot_config = {}
+try:
+ with open('boot_config.json') as file:
+ boot_config = json.load(file)
+except:
+ if os.path.exists('update'):
+ shutil.copy2('update/boot_config.json', 'boot_config.json')
+ with open('boot_config.json') as file:
+ boot_config = json.load(file)
+
+bootloader_config = boot_config.get('bootloader', {})
+
+binary = bootloader_config.get('binary', 'py -3' if sys.platform == 'win32' else 'python3')
+options = bootloader_config.get('options')
+boot_file = bootloader_config.get('boot_file', internal["base_bootfile"])
+autoreboot = bootloader_config.get('autoreboot', False)
+threshold = bootloader_config.get('autoreboot_threshold', 60)
+
+if not options:
+ options = ''
+else:
+ options = ' ' + ' '.join(options)
+
+if not '.install.json' in os.listdir() or reinstall:
+ if os.path.isdir('update') and not reinstall:
+ # unifier was likely updated from v2 or older
+ print('\x1b[33;1mLegacy installation detected, skipping installer.\x1b[0m')
+ with open('.install.json', 'w+') as file:
+ json.dump(
+ {
+ 'product': internal["product"],
+ 'setup': False,
+ 'option': 'optimized'
+ },
+ file
+ )
+ else:
+ # this installation is fresh
+ if not reinstall:
+ print('\x1b[33;1mInstallation not detected, running installer...\x1b[0m')
+
+ if len(install_options) == 1:
+ install_option = install_options[0]['id']
+ else:
+ print(f'\x1b[33;1mYou have {len(install_options)} install options available.\x1b[0m\n')
+
+ 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'\n\x1b[33;1mWhich installation option would you like to install? (0-{len(install_options)-1})\x1b[0m')
+
+ try:
+ install_option = int(input())
+
+ if install_option < 0 or install_option >= len(install_options):
+ raise ValueError()
+ except:
+ print(f'\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
+
+ install_option = install_options[install_option]['id']
+
+ 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}')
+ print(f'- Install directory: {os.getcwd()}')
+ print(f'- Python command/binary: {binary}\n')
+ print('\x1b[33;1mProceed with installation? (y/n)\x1b[0m')
+
+ try:
+ answer = input().lower()
+ except:
+ print(f'\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
+
+ if not answer == 'y':
+ print(f'\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
+
+ exit_code = os.system(f'{binary} boot/dep_installer.py {install_option}{options}')
+ if not exit_code == 0:
+ sys.exit(exit_code)
+
+ exit_code = os.system(f'{binary} boot/installer.py {install_option}{options}')
+
+ if not exit_code == 0:
+ print('\x1b[31;1mInstaller has crashed or has been aborted.\x1b[0m')
+ sys.exit(exit_code)
+
+ # sleep to prevent 429s
+ time.sleep(5)
+
+if not boot_file in os.listdir():
+ if os.path.isdir('update'):
+ print(f'\x1b[33;1m{boot_file} is missing, copying from update folder.\x1b[0m')
+ try:
+ shutil.copy2(f'update/{boot_file}', boot_file)
+ except:
+ print(f'\x1b[31;1mCould not find {boot_file}. Your installation may be corrupted.\x1b[0m')
+ print(f'Please install a fresh copy of {internal["product_name"]} from {internal["repo"]}.')
+ sys.exit(1)
+
+first_boot = False
+last_boot = time.time()
+
+print(f'\x1b[36;1mStarting {internal["product_name"]}...\x1b[0m')
+
+if '.restart' in os.listdir():
+ os.remove('.restart')
+ print('\x1b[33;1mAn incomplete restart was detected.\x1b[0m')
+
+while True:
+ exit_code = os.system(f'{binary} {boot_file}{options}')
+
+ crash_reboot = False
+ if not exit_code == 0:
+ diff = time.time() - last_boot
+ if autoreboot and first_boot and diff > threshold:
+ print(f'\x1b[31;1m{internal["product_name"]} has crashed, restarting...\x1b[0m')
+ crash_reboot = True
+ else:
+ print(f'\x1b[31;1m{internal["product_name"]} has crashed.\x1b[0m')
+ sys.exit(exit_code)
+
+ if crash_reboot or '.restart' in os.listdir():
+ if '.restart' in os.listdir():
+ os.remove('.restart')
+
+ print(f'\x1b[33;1mRestarting {internal["product_name"]}...\x1b[0m')
+ else:
+ print(f'\x1b[36;1m{internal["product_name"]} shutdown successful.\x1b[0m')
+ sys.exit(0)
+
+ first_boot = True
+ last_boot = time.time()
+
+ # sleep to prevent 429s
+ time.sleep(5)
diff --git a/boot/dep_installer.py b/boot/dep_installer.py
new file mode 100644
index 0000000..b085464
--- /dev/null
+++ b/boot/dep_installer.py
@@ -0,0 +1,54 @@
+import json
+import os
+import sys
+
+install_option = sys.argv[1] if len(sys.argv) > 1 else None
+
+install_options = [
+ {
+ 'id': 'stable',
+ 'name': '\U0001F48E Standard',
+ 'description': 'Uses the latest stable Nextcord version.',
+ 'default': True,
+ 'prefix': '',
+ 'color': '\x1b[32'
+ }
+]
+
+prefix = None
+
+if not install_option:
+ for option in install_options:
+ if option['default']:
+ install_option = option['id']
+ break
+else:
+ for option in install_options:
+ if option['id'] == install_option:
+ prefix = option['prefix']
+ if prefix == '':
+ prefix = None
+ break
+
+boot_config = {}
+try:
+ with open('boot_config.json') as file:
+ boot_config = json.load(file)
+except:
+ pass
+
+binary = boot_config['bootloader'].get('binary', 'py -3' if sys.platform == 'win32' else 'python3')
+
+print('\x1b[36;1mInstalling dependencies, this may take a while...\x1b[0m')
+
+if prefix:
+ code = os.system(f'{binary} -m pip install --user -U -r requirements_{prefix}.txt')
+else:
+ code = os.system(f'{binary} -m pip install --user -U -r requirements.txt')
+
+if not code == 0:
+ print('\x1b[31;1mCould not install dependencies.\x1b[0m')
+ sys.exit(code)
+
+print('\x1b[36;1mDependencies successfully installed.\x1b[0m')
+sys.exit(0)
diff --git a/boot/installer.py b/boot/installer.py
new file mode 100644
index 0000000..2b32770
--- /dev/null
+++ b/boot/installer.py
@@ -0,0 +1,160 @@
+import asyncio
+import sys
+import getpass
+import json
+import nextcord
+import tomli
+import tomli_w
+import traceback
+from nextcord.ext import commands
+
+install_option = sys.argv[1] if len(sys.argv) > 1 else None
+
+install_options = [
+ {
+ 'id': 'stable',
+ 'name': '\U0001F48E Standard',
+ 'description': 'Uses the latest stable Nextcord version.',
+ 'default': True,
+ 'prefix': '',
+ 'color': '\x1b[32'
+ }
+]
+
+if not install_option:
+ for option in install_options:
+ if option['default']:
+ install_option = option['id']
+ break
+
+bot = commands.Bot(
+ command_prefix='u!',
+ intents=nextcord.Intents.all()
+)
+
+user_id = 0
+server_id = 0
+
+with open('boot/internal.json') as file:
+ internal = json.load(file)
+
+if sys.version_info.minor < internal['required_py_version']:
+ print(f'\x1b[31;49mCannot install {internal["product_name"]}. Python 3.{internal["required_py_version"]} or later is required.\x1b[0m')
+ sys.exit(1)
+
+@bot.event
+async def on_ready():
+ global server_id
+
+ print(f'\x1b[33;1mIs {bot.user.name} ({bot.user.id}) the correct bot? (y/n)\x1b[0m')
+ answer = input().lower()
+
+ if not answer == 'y':
+ print(f'\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
+
+ print(f'\x1b[36;1mAttempting to DM user {user_id}...\x1b[0m')
+
+ user = bot.get_user(user_id)
+
+ for guild in bot.guilds:
+ for member in guild.members:
+ if member.id == user_id:
+ server_id = guild.id
+ break
+
+ if not server_id == 0:
+ break
+
+ available = 10
+ tries = 0
+
+ while True:
+ try:
+ await user.send('If you can see this message, please return to the console, then type "y".')
+ break
+ except:
+ tries += 1
+
+ if tries >= available:
+ print(f'\x1b[31;1mCould not DM user after {available} attempts, aborting.\x1b[0m')
+ sys.exit(1)
+ if user:
+ print(f'\x1b[33;1mCould not DM user. Please enable your DMs for a server you and the bot share.\x1b[0m')
+ else:
+ print(f'\x1b[33;1mCould not find user. Please add the bot to a server you are in.\x1b[0m')
+ print(f'Use this link to add the bot: https://discord.com/api/oauth2/authorize?client_id={bot.user.id}&permissions=537259008&scope=bot')
+ print(f'\x1b[33;1mTrying again in 30 seconds, {available-tries} tries remaining. Press Ctrl+C to abort.\x1b[0m')
+
+ try:
+ await asyncio.sleep(30)
+ except:
+ print(f'\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
+
+ print(f'\x1b[33;1mDid you receive a DM from the bot? (y/n)\x1b[0m')
+ answer = input().lower()
+
+ if not answer == 'y':
+ print(f'\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
+
+ print('\x1b[36;1mOwner verified successfully, closing bot.\x1b[0m')
+ await bot.close()
+
+print('\x1b[33;1mWe need the ID of the user who will be the instance owner. In most cases this is your user ID.\x1b[0m')
+print(f'\x1b[33;1mThe owner will have access to special commands for maintaining your {internal["product_name"]} instance.\x1b[0m')
+print('\x1b[33;1mTo copy your ID, go to your Discord settings, then Advanced, then enable Developer mode.\x1b[0m')
+
+while True:
+ try:
+ user_id = int(input())
+ break
+ except KeyboardInterrupt:
+ print('\x1b[31;49mAborted.\x1b[0m')
+ sys.exit(1)
+ except:
+ print('\x1b[31;49mThis isn\'t an integer, try again.\x1b[0m')
+
+print('\x1b[33;1mWe will now ask for your bot token.\x1b[0m')
+print('\x1b[33;1mThe user verifier will use this token to log on to Discord.\x1b[0m\n')
+print(f'\x1b[37;41;1mWARNING: DO NOT SHARE THIS TOKEN, NOT EVEN WITH {internal["maintainer"].upper()}.\x1b[0m')
+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()
+
+print('\x1b[36;1mStarting bot...\x1b[0m')
+
+try:
+ bot.run(token)
+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')
+ sys.exit(1)
+
+file = open('.env','w+')
+file.write(f'TOKEN={token}')
+file.close()
+
+with open('config.toml', 'rb') as file:
+ config = tomli.load(file)
+
+config['roles']['owner'] = user_id
+config['moderation']['home_guild'] = server_id
+
+with open('config.toml', 'wb') as file:
+ tomli_w.dump(config, file)
+
+with open('.install.json','w+') as file:
+ json.dump(
+ {
+ 'product': internal["product"],
+ 'setup': False,
+ 'option': install_option
+ },
+ file
+ )
+
+print(f'\x1b[36;1m{internal["product_name"]} installed successfully.\x1b[0m')
diff --git a/boot/internal.json b/boot/internal.json
new file mode 100644
index 0000000..94e3f22
--- /dev/null
+++ b/boot/internal.json
@@ -0,0 +1,8 @@
+{
+ "maintainer": "UnifierHQ",
+ "product": "unifier-micro",
+ "product_name": "Unifier Micro",
+ "base_bootfile": "microfier.py",
+ "repo": "https://github.com/UnifierHQ/unifier-micro",
+ "required_py_version": 9
+}
\ No newline at end of file
diff --git a/microfier.py b/microfier.py
index 77cee5f..b78dd10 100644
--- a/microfier.py
+++ b/microfier.py
@@ -124,7 +124,7 @@ def is_user_admin(id):
def is_room_restricted(room,db):
try:
- if room in db['restricted']:
+ if db['rooms'][room]['meta']['restricted']:
return True
else:
return False
@@ -133,7 +133,7 @@ def is_room_restricted(room,db):
def is_room_locked(room,db):
try:
- if room in db['locked']:
+ if db['rooms'][room]['meta']['locked']:
return True
else:
return False
@@ -146,6 +146,39 @@ async def fetch_message(message_id):
return message
raise ValueError("No message found")
+async def convert_1():
+ """Converts data structure to be v3.0.0-compatible.
+ Eliminates the need for a lot of unneeded keys."""
+ if not 'rules' in db.keys():
+ # conversion is not needed
+ return
+ for room in db['rooms']:
+ db['rooms'][room] = {'meta':{
+ 'rules': db['rules'][room],
+ 'restricted': room in db['restricted'],
+ 'locked': room in db['locked'],
+ 'private': False,
+ 'private_meta': {
+ 'server': None,
+ 'allowed': [],
+ 'invites': [],
+ 'platform': 'discord'
+ },
+ 'emoji': None,
+ 'description': None,
+ 'display_name': None,
+ 'banned': []
+ },'discord': db['rooms'][room]}
+
+ db.pop('rules')
+ db.pop('restricted')
+ db.pop('locked')
+
+ # 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?
+
+ db.save_data()
+
with open('config.json', 'r') as file:
config = json.load(file)
@@ -158,6 +191,17 @@ async def fetch_message(message_id):
logger = log.buildlogger(package,'core',level)
+room_template = {
+ 'rules': [], 'restricted': False, 'locked': False, 'private': False,
+ 'private_meta': {
+ 'server': None,
+ 'allowed': [],
+ 'invites': [],
+ 'platform': 'discord'
+ },
+ 'emoji': None, 'description': None, 'display_name': None, 'banned': []
+}
+
if not '.welcome.txt' in os.listdir():
x = open('.welcome.txt','w+')
x.close()
@@ -616,8 +660,7 @@ async def make(ctx,*,room):
return await ctx.send('Room names may only contain alphabets, numbers, dashes, and underscores.')
if room in list(db['rooms'].keys()):
return await ctx.send('This room already exists!')
- db['rooms'].update({room:{}})
- db['rules'].update({room:[]})
+ db['rooms'].update({room:dict(room_template)})
db.save_data()
await ctx.send(f'Created room `{room}`!')
@@ -626,11 +669,11 @@ async def addrule(ctx,room,*,rule):
if not is_user_admin(ctx.author.id):
return await ctx.send('Only admins can modify rules!')
room = room.lower()
- if not room in list(db['rules'].keys()):
+ if not room in list(db['rooms'].keys()):
return await ctx.send('This room does not exist!')
- if len(db['rules'][room]) >= 25:
+ if len(db['rooms'][room]['meta']['rules']) >= 25:
return await ctx.send('You can only have up to 25 rules in a room!')
- db['rules'][room].append(rule)
+ db['rules'][room]['rules'].append(rule)
db.save_data()
await ctx.send('Added rule!')
@@ -645,9 +688,9 @@ async def delrule(ctx,room,*,rule):
raise ValueError()
except:
return await ctx.send('Rule must be a number higher than 0.')
- if not room in list(db['rules'].keys()):
+ if not room in list(db['rooms'].keys()):
return await ctx.send('This room does not exist!')
- db['rules'][room].pop(rule-1)
+ db['rooms'][room]['meta']['rules'].pop(rule-1)
db.save_data()
await ctx.send('Removed rule!')
@@ -666,11 +709,8 @@ async def rules(ctx, *, room=''):
index = 0
text = ''
- if room in list(db['rules'].keys()):
- rules = db['rules'][room]
- if len(rules) == 0:
- return await ctx.send('The room creator hasn\'t added rules yet. For now, follow `main` room rules.')
- else:
+ rules = db['rooms'][room]['meta']['rules']
+ if len(rules) == 0:
return await ctx.send('The room creator hasn\'t added rules yet. For now, follow `main` room rules.')
for rule in rules:
if text == '':
@@ -686,17 +726,17 @@ async def rules(ctx, *, room=''):
hidden=True,
description='Restricts/unrestricts room. Only admins will be able to collect to this room when restricted.'
)
-async def roomrestrict(ctx,*,room):
+async def restrict(ctx,*,room):
if not is_user_admin(ctx.author.id):
return await ctx.send('Only admins can modify rooms!')
room = room.lower()
if not room in list(db['rooms'].keys()):
return await ctx.send('This room does not exist!')
- if room in db['restricted']:
- db['restricted'].remove(room)
+ if db['rooms'][room]['meta']['restricted']:
+ db['rooms'][room]['meta']['restricted'] = False
await ctx.send(f'Unrestricted `{room}`!')
else:
- db['restricted'].append(room)
+ db['rooms'][room]['meta']['restricted'] = True
await ctx.send(f'Restricted `{room}`!')
db.save_data()
@@ -704,20 +744,40 @@ async def roomrestrict(ctx,*,room):
hidden=True,
description='Locks/unlocks a room. Only moderators and admins will be able to chat in this room when locked.'
)
-async def roomlock(ctx,*,room):
+async def lock(ctx,*,room):
if not is_user_admin(ctx.author.id):
return await ctx.send('Only admins can modify rooms!')
room = room.lower()
if not room in list(db['rooms'].keys()):
return await ctx.send('This room does not exist!')
- if room in db['locked']:
- db['locked'].remove(room)
+ if db['rooms'][room]['meta']['locked']:
+ db['rooms'][room]['meta']['locked'] = False
await ctx.send(f'Unlocked `{room}`!')
else:
- db['locked'].append(room)
+ db['rooms'][room]['meta']['locked'] = True
await ctx.send(f'Locked `{room}`!')
db.save_data()
+@bot.command(name='display-name', hidden=True, description='Sets room display name.')
+async def display_name(ctx, room, *, name=''):
+ if not is_user_admin(ctx.author.id):
+ return await ctx.send('Only admins can modify rooms!')
+ room = room.lower()
+ if not room in list(db['rooms'].keys()):
+ return await ctx.send('This room does not exist!')
+
+ if len(name) == 0:
+ if not db['rooms'][room]['meta']['display_name']:
+ return await ctx.send('There is no display name to reset for this room.')
+ db['rooms'][room]['meta']['display_name'] = None
+ db.save_data()
+ return await ctx.send('Display name removed.')
+ elif len(name) > 32:
+ return await ctx.send('Display name is too long. Please keep it within 32 characters.')
+ db['rooms'][room]['meta']['display_name'] = name
+ db.save_data()
+ await ctx.send(f'Updated display name to `{name}`!')
+
@bot.command(description='Measures bot latency.')
async def ping(ctx):
t = time.time()
@@ -796,6 +856,9 @@ async def rooms(ctx):
if index >= len(roomlist):
break
name = roomlist[index]
+ display_name = (
+ db['rooms'][name]['meta']['display_name'] or name
+ )
emoji = (
'\U0001F527' if is_room_restricted(roomlist[index],db) else
'\U0001F512' if is_room_locked(roomlist[index],db) else
@@ -808,12 +871,15 @@ async def rooms(ctx):
)
embed.add_field(
- name=f'{emoji} `{name}`',
+ name=f'{emoji} '+(
+ f'{display_name} (`{name}`)' if db['rooms'][name]['meta']['display_name'] else
+ f'`{display_name}`'
+ ),
value=description,
inline=False
)
selection.add_option(
- label=name,
+ label=display_name,
emoji=emoji,
description=description,
value=name
@@ -906,6 +972,9 @@ def search_filter(query, query_cmd):
if index >= len(roomlist):
break
room = roomlist[index]
+ display_name = (
+ db['rooms'][room]['meta']['display_name'] or room
+ )
emoji = (
'\U0001F527' if is_room_restricted(roomlist[index], db) else
'\U0001F512' if is_room_locked(roomlist[index], db) else
@@ -917,12 +986,15 @@ def search_filter(query, query_cmd):
'Public room'
)
embed.add_field(
- name=f'{emoji} `{room}`',
+ name=f'{emoji} '+(
+ f'{display_name} (`{room}`)' if db['rooms'][room]['meta']['display_name'] else
+ f'`{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
@@ -1011,6 +1083,9 @@ def search_filter(query, query_cmd):
if was_searching else
f'{bot.user.global_name or bot.user.name} rooms / {roomname}'
)
+ display_name = (
+ db['rooms'][roomname]['meta']['display_name'] or roomname
+ )
description = (
db['descriptions'][roomname]
if roomname in db['descriptions'].keys() else 'This room has no description.'
@@ -1020,7 +1095,10 @@ def search_filter(query, query_cmd):
'\U0001F512' if is_room_locked(roomname, db) else
'\U0001F310'
)
- embed.description = f'# **{emoji} `{roomname}`**\n{description}'
+ if db['rooms'][roomname]['meta']['display_name']:
+ embed.description = f'# **{emoji} {display_name}**\n`{roomname}`\n\n{description}'
+ else:
+ embed.description = f'# **{emoji} `{display_name}`**\n{description}'
components.add_rows(
ui.ActionRow(
nextcord.ui.Button(
@@ -1045,10 +1123,7 @@ def search_filter(query, query_cmd):
)
index = 0
text = ''
- if roomname in list(db['rules'].keys()):
- rules = db['rules'][roomname]
- else:
- rules = []
+ rules = db['rooms'][roomname]['rules']
for rule in rules:
if text == '':
text = f'1. {rule}'
@@ -1190,10 +1265,10 @@ async def bind(ctx, *, room=''):
f'Your server is already linked to this room.\n**Accidentally deleted the webhook?** `{bot.command_prefix}unlink` it then `{bot.command_prefix}link` it back.')
index = 0
text = ''
- if len(db['rules'][room]) == 0:
+ if len(db['rooms'][room]['meta']['rules']) == 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 `{bot.command_prefix}rules {room}`.'
else:
- for rule in db['rules'][room]:
+ for rule in db['rooms'][room]['meta']['rules']:
if text == '':
text = f'1. {rule}'
else:
@@ -1288,8 +1363,8 @@ async def unbind(ctx, *, room=''):
await ctx.send('Something went wrong - check my permissions.')
raise
-@bot.command(aliases=['ban'],description='Blocks a user or server from bridging messages to your server.')
-async def restrict(ctx, *, target):
+@bot.command(description='Blocks a user or server from bridging messages to your server.')
+async def block(ctx, *, target):
if not (ctx.author.guild_permissions.administrator or ctx.author.guild_permissions.kick_members or
ctx.author.guild_permissions.ban_members):
return await ctx.send('You cannot restrict members/servers.')
@@ -1315,8 +1390,8 @@ async def restrict(ctx, *, target):
db.save_data()
await ctx.send('User/server can no longer forward messages to this channel!')
-@bot.command(hidden=True,description='Blocks a user or server from bridging messages through Unifier.')
-async def globalban(ctx, target, duration, *, reason='no reason given'):
+@bot.command(aliases=['globalban'],hidden=True,description='Blocks a user or server from bridging messages through Unifier.')
+async def ban(ctx, target, duration, *, reason='no reason given'):
if not ctx.author.id in db['moderators']:
return
forever = (duration.lower() == 'inf' or duration.lower() == 'infinite' or
diff --git a/requirements.txt b/requirements.txt
index 6dcb91c..ef611ae 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,4 @@
nextcord
-python-dotenv
\ No newline at end of file
+python-dotenv
+tomli
+tomli-w
\ No newline at end of file
diff --git a/run.bat b/run.bat
new file mode 100644
index 0000000..9cfa918
--- /dev/null
+++ b/run.bat
@@ -0,0 +1,9 @@
+@echo off
+py -3 -V >nul 2>&1
+
+if NOT ERRORLEVEL 0 (
+ echo on
+ echo Could not find a Python 3 installation.
+) else (
+ py -3 boot/bootloader.py %*
+)
diff --git a/run.sh b/run.sh
new file mode 100644
index 0000000..8ba0660
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+FILEPATH="$(which python3)"
+
+if [[ -z $FILEPATH ]]; then
+ echo "Could not find a Python 3 installation."
+ exit 1
+fi
+
+python3 ./boot/bootloader.py "$@"
From 40315f7a591a084c6df46f31daa0f1dad9b470df Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Wed, 18 Sep 2024 10:05:31 +0200
Subject: [PATCH 05/22] use config.toml instead
---
microfier.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 53 insertions(+), 4 deletions(-)
diff --git a/microfier.py b/microfier.py
index b78dd10..d8423a7 100644
--- a/microfier.py
+++ b/microfier.py
@@ -28,6 +28,9 @@
import re
from utils import log, ui
import math
+import tomli
+import tomli_w
+import traceback
version = '2.0.1'
@@ -112,10 +115,10 @@ def save_data(self):
with open(self.file_path, 'w') as file:
json.dump(self, file, indent=4)
-def is_user_admin(id):
+def is_user_admin(user_id):
try:
global admin_ids
- if id in admin_ids:
+ if user_id in admin_ids:
return True
else:
return False
@@ -180,8 +183,54 @@ async def convert_1():
db.save_data()
-with open('config.json', 'r') as file:
- config = json.load(file)
+try:
+ with open('.install.json') as file:
+ install_info = json.load(file)
+
+ if not install_info['product'] == 'unifier-micro':
+ print('This installation is not compatible with Unifier Micro.')
+ sys.exit(1)
+except:
+ if sys.platform == 'win32':
+ print('To start the bot, please run "run.bat" instead.')
+ else:
+ print('To start the bot, please run "./run.sh" instead.')
+ print('If you get a "Permission denied" error, run "chmod +x run.sh" and try again.')
+ sys.exit(1)
+
+config_file = 'config.toml'
+if 'devmode' in sys.argv:
+ config_file = 'devconfig.toml'
+
+valid_toml = False
+try:
+ with open(config_file, 'rb') as file:
+ config = tomli.load(file)
+ valid_toml = True
+except:
+ try:
+ with open('config.json') as file:
+ config = json.load(file)
+ except:
+ traceback.print_exc()
+ print('\nFailed to load config.toml file.\nIf the error is a JSONDecodeError, it\'s most likely a syntax error.')
+ sys.exit(1)
+
+ # toml is likely in update files, pull from there
+ with open('update/config.toml', 'rb') as file:
+ newdata = tomli.load(file)
+
+ def update_toml(old, new):
+ for key in new:
+ for newkey in new[key]:
+ if newkey in old.keys():
+ new[key].update({newkey: old[newkey]})
+ return new
+
+ data = update_toml(config, newdata)
+
+ with open(config_file, 'wb+') as file:
+ tomli_w.dump(data, file)
env_loaded = load_dotenv()
From 0eaa8bf626362e0f7370d4663cb6526342c18e26 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Wed, 18 Sep 2024 10:07:07 +0200
Subject: [PATCH 06/22] config.toml fix
---
microfier.py | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/microfier.py b/microfier.py
index d8423a7..fa4ac73 100644
--- a/microfier.py
+++ b/microfier.py
@@ -227,10 +227,24 @@ def update_toml(old, new):
new[key].update({newkey: old[newkey]})
return new
- data = update_toml(config, newdata)
+ config = update_toml(config, newdata)
with open(config_file, 'wb+') as file:
- tomli_w.dump(data, file)
+ tomli_w.dump(config, file)
+
+try:
+ with open('boot_config.json', 'r') as file:
+ boot_data = json.load(file)
+except:
+ boot_data = {}
+
+newdata = {}
+
+for key in config:
+ for newkey in config[key]:
+ newdata.update({newkey: config[key][newkey]})
+
+data = newdata
env_loaded = load_dotenv()
@@ -263,6 +277,10 @@ def update_toml(old, new):
logger.critical('Unifier is licensed under the AGPLv3, meaning you need to make your source code available to users. Please add a repository to the config file under the repo key.')
sys.exit(1)
+if not valid_toml:
+ logger.warning('From v3.0.0, Unifier will use config.toml rather than config.json.')
+ logger.warning('To change your Unifier configuration, please use the new file.')
+
if not env_loaded or not "TOKEN" in os.environ:
logger.critical('Could not find token from .env file! More info: https://unichat-wiki.pixels.onl/setup-selfhosted/getting-started/unifier#set-bot-token')
sys.exit(1)
From 62a117d9caf33d18c84bac9c6594972cb95378da Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Wed, 18 Sep 2024 10:09:35 +0200
Subject: [PATCH 07/22] use config.toml instead of config.json
---
config.json | 9 ---------
config.toml | 14 ++++++++++++++
2 files changed, 14 insertions(+), 9 deletions(-)
delete mode 100644 config.json
create mode 100644 config.toml
diff --git a/config.json b/config.json
deleted file mode 100644
index 9b3adf9..0000000
--- a/config.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "debug": false,
- "package": "microfier",
- "owner": 356456393491873795,
- "prefix": "u!",
- "repo": "https://github.com/greeeen-dev/unifier-micro",
- "ping": 0,
- "admin_ids": [356456393491873795]
-}
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..41e7df9
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,14 @@
+[system]
+debug = false
+package = "microfier"
+
+[roles]
+owner = -1
+admin_ids = []
+
+[bot]
+prefix = "u!"
+ping = 0
+
+[plugin]
+"repo" = "https://github.com/greeeen-dev/unifier-micro"
From 6d829932691bdce894c0c6f8ef04e5ef0f2d8f28 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Wed, 18 Sep 2024 10:11:12 +0200
Subject: [PATCH 08/22] shutdown post-install
---
boot/bootloader.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/boot/bootloader.py b/boot/bootloader.py
index 518d713..42eb0e1 100644
--- a/boot/bootloader.py
+++ b/boot/bootloader.py
@@ -116,8 +116,8 @@
print('\x1b[31;1mInstaller has crashed or has been aborted.\x1b[0m')
sys.exit(exit_code)
- # sleep to prevent 429s
- time.sleep(5)
+ print('\x1b[33;1mPlease re-run the run script after configuring the bot to start the bot.\x1b[0m')
+ sys.exit(0)
if not boot_file in os.listdir():
if os.path.isdir('update'):
From 1a680c0088af5b9e61a74be564a5ab379cbcbdd9 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Wed, 18 Sep 2024 10:12:58 +0200
Subject: [PATCH 09/22] run convert
---
microfier.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/microfier.py b/microfier.py
index fa4ac73..9ed542c 100644
--- a/microfier.py
+++ b/microfier.py
@@ -341,6 +341,8 @@ async def on_ready():
logger.debug(f'Pinging servers every {round(config["ping"])} seconds')
elif config['ping'] <= 0:
logger.debug(f'Periodic pinging disabled')
+ logger.debug('Restructuring room data...')
+ await convert_1()
logger.info('Unifier is ready!')
@bot.event
From 7940ac8d9b24dde564a1541ba68518d2cc9e4ead Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Wed, 18 Sep 2024 10:13:52 +0200
Subject: [PATCH 10/22] version bump
---
microfier.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/microfier.py b/microfier.py
index 9ed542c..e8303a1 100644
--- a/microfier.py
+++ b/microfier.py
@@ -32,7 +32,7 @@
import tomli_w
import traceback
-version = '2.0.1'
+version = '3.0.0'
def timetoint(t,timeoutcap=False):
try:
From 677a251b3ab2214473a082460095464e4079ce3c Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Fri, 20 Sep 2024 21:42:08 +0200
Subject: [PATCH 11/22] some renaming
---
.github/workflows/pylint.yml | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index 380133b..78bb56d 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -4,6 +4,7 @@ on: [push]
jobs:
build-linux:
+ name: "Linux (Standard)"
runs-on: ubuntu-latest
strategy:
matrix:
@@ -19,11 +20,12 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pylint
- - name: Analysing the code with pylint
+ - name: Pylint analysis
run: |
pylint --enable-all-extensions --extension-pkg-allow-list=orjson,ujson --disable=R,C,W $(git ls-files '*.py')
build-windows:
+ name: "Windows (Standard)"
runs-on: windows-latest
strategy:
matrix:
@@ -39,6 +41,6 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pylint
- - name: Analysing the code with pylint
+ - name: Pylint analysis
run: |
pylint --enable-all-extensions --extension-pkg-allow-list=orjson,ujson --disable=R,C,W $(git ls-files '*.py')
From 9106c258a8e3a7069fb63a70226c9ccfe7d68927 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:15:20 +0200
Subject: [PATCH 12/22] bootloader update
---
boot/bootloader.py | 97 +++++++++++++++++++++++++++++-----------------
boot_config.json | 4 ++
2 files changed, 65 insertions(+), 36 deletions(-)
create mode 100644 boot_config.json
diff --git a/boot/bootloader.py b/boot/bootloader.py
index 42eb0e1..630b749 100644
--- a/boot/bootloader.py
+++ b/boot/bootloader.py
@@ -5,6 +5,7 @@
import time
reinstall = '--reinstall' in sys.argv
+depinstall = '--install-deps' in sys.argv
install_options = [
{
@@ -48,11 +49,12 @@
else:
options = ' ' + ' '.join(options)
-if not '.install.json' in os.listdir() or reinstall:
- if os.path.isdir('update') and not reinstall:
+if not '.install.json' in os.listdir() or reinstall or depinstall:
+ if os.path.isdir('update') and not reinstall and not depinstall:
# unifier was likely updated from v2 or older
print('\x1b[33;1mLegacy installation detected, skipping installer.\x1b[0m')
with open('.install.json', 'w+') as file:
+ # noinspection PyTypeChecker
json.dump(
{
'product': internal["product"],
@@ -63,61 +65,77 @@
)
else:
# this installation is fresh
- if not reinstall:
- print('\x1b[33;1mInstallation not detected, running installer...\x1b[0m')
+ if not depinstall:
+ if not reinstall:
+ print('\x1b[33;1mInstallation not detected, running installer...\x1b[0m')
- if len(install_options) == 1:
- install_option = install_options[0]['id']
- else:
- print(f'\x1b[33;1mYou have {len(install_options)} install options available.\x1b[0m\n')
+ if len(install_options) == 1:
+ install_option = install_options[0]['id']
+ else:
+ print(f'\x1b[33;1mYou have {len(install_options)} install options available.\x1b[0m\n')
- 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')
+ 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'\n\x1b[33;1mWhich installation option would you like to install? (0-{len(install_options)-1})\x1b[0m')
+ print(f'\n\x1b[33;1mWhich installation option would you like to install? (0-{len(install_options)-1})\x1b[0m')
- try:
- install_option = int(input())
+ try:
+ install_option = int(input())
+
+ if install_option < 0 or install_option >= len(install_options):
+ raise ValueError()
+ except:
+ print(f'\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
- if install_option < 0 or install_option >= len(install_options):
- raise ValueError()
+ install_option = install_options[install_option]['id']
+
+ 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}')
+ print(f'- Install directory: {os.getcwd()}')
+ print(f'- Python command/binary: {binary}\n')
+ print('\x1b[33;1mProceed with installation? (y/n)\x1b[0m')
+
+ try:
+ answer = input().lower()
except:
print(f'\x1b[31;1mAborting.\x1b[0m')
sys.exit(1)
- install_option = install_options[install_option]['id']
-
- 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}')
- print(f'- Install directory: {os.getcwd()}')
- print(f'- Python command/binary: {binary}\n')
- print('\x1b[33;1mProceed with installation? (y/n)\x1b[0m')
+ if not answer == 'y':
+ print(f'\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
+ else:
+ try:
+ with open('.install.json') as file:
+ install_data = json.load(file)
+ except:
+ print('\x1b[31;1mPlease install Unifier first.\x1b[0m')
+ sys.exit(1)
- try:
- answer = input().lower()
- except:
- print(f'\x1b[31;1mAborting.\x1b[0m')
- sys.exit(1)
+ print('\x1b[33;1mInstalling dependencies...\x1b[0m')
- if not answer == 'y':
- print(f'\x1b[31;1mAborting.\x1b[0m')
- sys.exit(1)
+ install_option = install_data['option']
exit_code = os.system(f'{binary} boot/dep_installer.py {install_option}{options}')
if not exit_code == 0:
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}')
if not exit_code == 0:
print('\x1b[31;1mInstaller has crashed or has been aborted.\x1b[0m')
sys.exit(exit_code)
- print('\x1b[33;1mPlease re-run the run script after configuring the bot to start the bot.\x1b[0m')
- sys.exit(0)
+ # sleep to prevent 429s
+ time.sleep(5)
if not boot_file in os.listdir():
if os.path.isdir('update'):
@@ -138,8 +156,10 @@
os.remove('.restart')
print('\x1b[33;1mAn incomplete restart was detected.\x1b[0m')
+restart_options = ''
+
while True:
- exit_code = os.system(f'{binary} {boot_file}{options}')
+ exit_code = os.system(f'{binary} {boot_file}{restart_options}{options}')
crash_reboot = False
if not exit_code == 0:
@@ -153,6 +173,11 @@
if crash_reboot or '.restart' in os.listdir():
if '.restart' in os.listdir():
+ x = open('.restart', 'r', encoding='utf-8')
+ data = x.read().split(' ')
+ x.close()
+
+ restart_options = (' ' + data[1]) if len(data) > 1 else ''
os.remove('.restart')
print(f'\x1b[33;1mRestarting {internal["product_name"]}...\x1b[0m')
diff --git a/boot_config.json b/boot_config.json
new file mode 100644
index 0000000..d1ad722
--- /dev/null
+++ b/boot_config.json
@@ -0,0 +1,4 @@
+{
+ "note": "Refer to https://wiki.unifierhq.org/setup-selfhosted/configuring-the-bootloader for more information.",
+ "bootloader": {}
+}
\ No newline at end of file
From a7f999bcd71e4827f8ffbb3b644c5f9094c92389 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:27:51 +0200
Subject: [PATCH 13/22] shutdown
---
microfier.py | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 111 insertions(+)
diff --git a/microfier.py b/microfier.py
index e8303a1..824589b 100644
--- a/microfier.py
+++ b/microfier.py
@@ -113,8 +113,23 @@ def load_data(self):
def save_data(self):
with open(self.file_path, 'w') as file:
+ # noinspection PyTypeChecker
json.dump(self, file, indent=4)
+class Colors: # format: 0xHEXCODE
+ greens_hair = 0xa19e78
+ unifier = 0xed4545
+ green = 0x2ecc71
+ dark_green = 0x1f8b4c
+ purple = 0x9b59b6
+ red = 0xe74c3c
+ blurple = 0x7289da
+ gold = 0xd4a62a
+ error = 0xff838c
+ warning = 0xe4aa54
+ success = 0x11ad79
+ critical = 0xff0000
+
def is_user_admin(user_id):
try:
global admin_ids
@@ -291,6 +306,8 @@ def update_toml(old, new):
db = AutoSaveDict({})
db.load_data()
+colors = Colors
+
messages = []
ut_total = round(time.time())
@@ -350,6 +367,86 @@ async def on_disconnect():
global disconnects
disconnects += 1
+async def bot_shutdown(ctx, restart=False):
+ embed = nextcord.Embed(color=colors.warning)
+
+ if restart:
+ embed.title = f':warning: Restart the bot?'
+ embed.description = 'The bot will automatically restart in 60 seconds.'
+ else:
+ embed.title = f':warning: Shut the bot down?'
+ embed.description = 'The bot will automatically shut down in 60 seconds.'
+
+ components = ui.MessageComponents()
+
+ btns_row = ui.ActionRow(
+ nextcord.ui.Button(
+ style=nextcord.ButtonStyle.red,
+ label='Restart' if restart else 'Shut down',
+ custom_id='shutdown'
+ ),
+ nextcord.ui.Button(
+ style=nextcord.ButtonStyle.gray,
+ label='Nevermind',
+ custom_id='cancel'
+ )
+ )
+
+ components.add_row(btns_row)
+
+ msg = await ctx.send(embed=embed, view=components)
+
+ def check(interaction):
+ return interaction.user.id == ctx.author.id and interaction.message.id == msg.id
+
+ try:
+ interaction = await bot.wait_for('interaction', check=check, timeout=60)
+
+ await interaction.response.edit_message(view=None)
+ except:
+ await msg.edit(view=None)
+ return
+
+ if interaction.data['custom_id'] == 'cancel':
+ return
+
+ embed.title = embed.title.replace(':warning:', ':hourglass:', 1)
+ await msg.edit(embed=embed)
+
+ logger.info("Attempting graceful shutdown...")
+ try:
+ logger.info("Backing up data...")
+ db.save_data()
+ logger.info("Backup complete")
+ if restart:
+ embed.title = f':white_check_mark: Restarting...'
+ embed.description = 'Bot will now restart.'
+ else:
+ embed.title = f':white_check_mark: Shutting down...'
+ embed.description = 'Bot will now shut down.'
+ embed.colour = colors.success
+ await msg.edit(embed=embed)
+ except:
+ logger.exception("Graceful shutdown failed")
+ if restart:
+ embed.title = f':x: Restart failed'
+ embed.description = 'The restart failed.'
+ else:
+ embed.title = f':x: Shutdown failed'
+ embed.description = 'The shutdown failed.'
+ embed.colour = colors.error
+ await msg.edit(embed=embed)
+ return
+
+ if restart:
+ x = open('.restart', 'w+')
+ x.write(f'{time.time()}')
+ x.close()
+
+ logger.info("Shutdown complete")
+ await bot.close()
+ sys.exit(0)
+
@bot.command(description='Shows this command.')
async def help(ctx):
show_sysmgr = False
@@ -447,6 +544,7 @@ def search_filter(query, query_cmd):
max_values=1, min_values=1, custom_id='selection', placeholder='Command...'
)
+ # noinspection PyTypeChecker
cmds = await bot.loop.run_in_executor(
None,lambda: sorted(
cmds,
@@ -668,6 +766,18 @@ async def uptime(ctx):
)
await ctx.send(embed=embed)
+@bot.command(aliases=['poweroff'], hidden=True, description='Gracefully shuts the bot down.')
+async def shutdown(ctx):
+ if not ctx.author.id == config['owner']:
+ return
+ await bot_shutdown(ctx)
+
+@bot.command(aliases=['reboot'], hidden=True, description='Gracefully restarts the bot.')
+async def restart(ctx):
+ if not ctx.author.id == config['owner']:
+ return
+ await bot_shutdown(ctx, restart=True)
+
@bot.command(hidden=True,description='Adds a moderator to the instance.')
async def addmod(ctx,*,userid):
if not is_user_admin(ctx.author.id):
@@ -1031,6 +1141,7 @@ def search_filter(query, query_cmd):
max_values=1, min_values=1, custom_id='selection', placeholder='Room...'
)
+ # noinspection PyTypeChecker
roomlist = await bot.loop.run_in_executor(None, lambda: sorted(
roomlist,
key=lambda x: x.lower()
From 139b8780a9b6a8e2248f168063440cc14d924fce Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:31:20 +0200
Subject: [PATCH 14/22] remove unused config
---
boot/installer.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/boot/installer.py b/boot/installer.py
index 2b32770..230600b 100644
--- a/boot/installer.py
+++ b/boot/installer.py
@@ -142,12 +142,12 @@ async def on_ready():
config = tomli.load(file)
config['roles']['owner'] = user_id
-config['moderation']['home_guild'] = server_id
with open('config.toml', 'wb') as file:
tomli_w.dump(config, file)
with open('.install.json','w+') as file:
+ # noinspection PyTypeChecker
json.dump(
{
'product': internal["product"],
From 57b04b871d22c4753803aaf2d9df9b6d0bc1d048 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:33:07 +0200
Subject: [PATCH 15/22] use correct variable
---
microfier.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/microfier.py b/microfier.py
index 824589b..835150c 100644
--- a/microfier.py
+++ b/microfier.py
@@ -259,7 +259,7 @@ def update_toml(old, new):
for newkey in config[key]:
newdata.update({newkey: config[key][newkey]})
-data = newdata
+config = newdata
env_loaded = load_dotenv()
From 821dc1acdda35e61aa45ed0b9d570582e6e80c24 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:35:20 +0200
Subject: [PATCH 16/22] make owner an admin
---
microfier.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/microfier.py b/microfier.py
index 835150c..2a44fc3 100644
--- a/microfier.py
+++ b/microfier.py
@@ -133,7 +133,7 @@ class Colors: # format: 0xHEXCODE
def is_user_admin(user_id):
try:
global admin_ids
- if user_id in admin_ids:
+ if user_id in admin_ids or user_id == config['owner']:
return True
else:
return False
From 5ba4d5d98c2919be6dd1eb81bb4d51576a09b0c0 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:37:24 +0200
Subject: [PATCH 17/22] use meta key
---
microfier.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/microfier.py b/microfier.py
index 2a44fc3..91f2888 100644
--- a/microfier.py
+++ b/microfier.py
@@ -839,7 +839,7 @@ async def make(ctx,*,room):
return await ctx.send('Room names may only contain alphabets, numbers, dashes, and underscores.')
if room in list(db['rooms'].keys()):
return await ctx.send('This room already exists!')
- db['rooms'].update({room:dict(room_template)})
+ db['rooms'].update({room:{'meta': dict(room_template)}})
db.save_data()
await ctx.send(f'Created room `{room}`!')
From 7e8896c296691bfea4218d90db4cb12dcc931738 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:39:17 +0200
Subject: [PATCH 18/22] room disband
---
microfier.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 68 insertions(+)
diff --git a/microfier.py b/microfier.py
index 91f2888..da47d0a 100644
--- a/microfier.py
+++ b/microfier.py
@@ -843,6 +843,74 @@ async def make(ctx,*,room):
db.save_data()
await ctx.send(f'Created room `{room}`!')
+@bot.command(description='Disbands a room.')
+async def disband(ctx, room):
+ room = room.lower()
+ if not room in db['rooms'].keys():
+ return await ctx.send('This room does not exist!')
+
+ if not is_user_admin(ctx.author.id):
+ return await ctx.send('Only admins can disband rooms!')
+
+ embed = nextcord.Embed(
+ title=f':warning: Disband `{room}`?',
+ description='Once the room is disbanded, it\'s gone forever!',
+ color=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 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)
+
+ db['rooms'].pop(room)
+ embed.title = f':white_check_mark: Disbanded `{room}`'
+ embed.description = 'The room was disbanded successfully.'
+ embed.colour = colors.success
+ await interaction.response.edit_message(embed=embed,view=None)
+ # noinspection PyTypeChecker
+ await bot.loop.run_in_executor(None, lambda: db.save_data())
+
@bot.command(hidden=True,description="Adds a given rule to a given room.")
async def addrule(ctx,room,*,rule):
if not is_user_admin(ctx.author.id):
From 9b00857ccd31c6e8838e52774ebe84b95a9ba7e9 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:48:04 +0200
Subject: [PATCH 19/22] fixes
---
microfier.py | 36 +++++++++++++++---------------------
1 file changed, 15 insertions(+), 21 deletions(-)
diff --git a/microfier.py b/microfier.py
index da47d0a..264702e 100644
--- a/microfier.py
+++ b/microfier.py
@@ -97,8 +97,7 @@ def __init__(self, *args, **kwargs):
self.file_path = 'data.json'
# Ensure necessary keys exist
- self.update({'rules': {}, 'rooms': {}, 'restricted': [], 'locked': [],
- 'blocked': {}, 'banned': {}, 'moderators': []})
+ self.update({'rooms': {}, 'blocked': {}, 'banned': {}, 'moderators': []})
# Load data
self.load_data()
@@ -277,7 +276,7 @@ def update_toml(old, new):
'invites': [],
'platform': 'discord'
},
- 'emoji': None, 'description': None, 'display_name': None, 'banned': []
+ 'emoji': None, 'description': None, 'display_name': None, 'banned': [], 'discord': {}
}
if not '.welcome.txt' in os.listdir():
@@ -1459,7 +1458,7 @@ def check(interaction):
@bot.command(aliases=['guilds'],description='Lists all servers connected to a given room.')
async def servers(ctx,*,room='main'):
try:
- data = db['rooms'][room]
+ data = db['rooms'][room]['discord']
except:
return await ctx.send(f'This isn\'t a valid room. Run `{bot.command_prefix}rooms` for a list of all rooms.')
text = ''
@@ -1486,7 +1485,7 @@ async def bind(ctx, *, room=''):
room = 'main'
await ctx.send('**No room was given, defaulting to main**')
try:
- data = db['rooms'][room]
+ data = db['rooms'][room]['discord']
except:
return await ctx.send(f'This isn\'t a valid room. Run `{bot.command_prefix}rooms` for a list of rooms.')
embed = nextcord.Embed(title='Ensuring channel is not connected...', description='This may take a while.')
@@ -1563,10 +1562,7 @@ def check(interaction):
if resp.data['custom_id'] == 'reject':
return
webhook = await ctx.channel.create_webhook(name='Unifier Bridge')
- data = db['rooms'][room]
- guild = [webhook.id]
- data.update({f'{ctx.guild.id}': guild})
- db['rooms'][room] = data
+ db['rooms'][room]['discord'].update({f'{ctx.guild.id}': [webhook.id]})
db.save_data()
await ctx.send(
'# :white_check_mark: Linked channel to Unifier network!\nYou can now send messages to the Unifier network through this channel. Say hi!')
@@ -1585,10 +1581,9 @@ async def unbind(ctx, *, room=''):
if not ctx.author.guild_permissions.manage_channels and not is_user_admin(ctx.author.id):
return await ctx.send('You don\'t have the necessary permissions.')
room = room.lower()
- try:
- data = db['rooms'][room]
- except:
- return await ctx.send('This isn\'t a valid room. Try `main`, `pr`, `prcomments`, or `liveries` instead.')
+ if not room in db['rooms'].keys():
+ return await ctx.send('This isn\'t a valid room!')
+ data = db['rooms'][room]['discord']
try:
try:
hooks = await ctx.guild.webhooks()
@@ -1602,8 +1597,7 @@ async def unbind(ctx, *, room=''):
if webhook.id in hook_ids:
await webhook.delete()
break
- data.pop(f'{ctx.guild.id}')
- db['rooms'][room] = data
+ db['rooms'][room]['discord'].pop(f'{ctx.guild.id}')
db.save_data()
await ctx.send(
'# :white_check_mark: Unlinked channel from Unifier network!\nThis channel is no longer linked, nothing from now will be bridged.')
@@ -1853,7 +1847,7 @@ async def on_message(message):
for room in list(db['rooms'].keys()):
try:
for hook in hooks:
- if hook.id == db['rooms'][room][f'{message.guild.id}'][0]:
+ if hook.id == db['rooms'][room]['discord'][f'{message.guild.id}'][0]:
roomname = room
break
except:
@@ -1864,7 +1858,7 @@ async def on_message(message):
if not roomname:
return
- if ('nextcord.gg/' in message.content or 'nextcord.com/invite/' in message.content or
+ if ('discord.gg/' in message.content or 'discord.com/invite/' in message.content or
'discordapp.com/invite/' in message.content):
try:
await message.delete()
@@ -1922,7 +1916,7 @@ async def on_message(message):
if not trimmed:
donotshow = True
- for guild_id in list(db['rooms'][roomname].keys()):
+ for guild_id in list(db['rooms'][roomname]['discord'].keys()):
if int(guild_id)==message.guild.id:
continue
guild = bot.get_guild(int(guild_id))
@@ -1944,7 +1938,7 @@ async def on_message(message):
except:
files.append(await attachment.to_file(use_cached=True, spoiler=False))
- webhook: nextcord.Webhook = await bot.fetch_webhook(db['rooms'][roomname][f'{guild.id}'][0])
+ webhook: nextcord.Webhook = await bot.fetch_webhook(db['rooms'][roomname]['discord'][f'{guild.id}'][0])
components = None
if reply_msg:
@@ -2081,14 +2075,14 @@ async def on_message_delete(message):
roomname = msg.room
- for guild_id in list(db['rooms'][roomname].keys()):
+ for guild_id in list(db['rooms'][roomname]['discord'].keys()):
if int(guild_id)==message.guild.id:
continue
guild = bot.get_guild(int(guild_id))
try:
msg_id = int(await msg.fetch_id(str(guild.id)))
- webhook = await bot.fetch_webhook(db['rooms'][roomname][f'{guild.id}'][0])
+ webhook = await bot.fetch_webhook(db['rooms'][roomname]['discord'][f'{guild.id}'][0])
except:
continue
From b07f7980de59a0fc052ca8ae95540b07be501e4d Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:49:37 +0200
Subject: [PATCH 20/22] use proper key
---
microfier.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/microfier.py b/microfier.py
index 264702e..dd83811 100644
--- a/microfier.py
+++ b/microfier.py
@@ -276,7 +276,7 @@ def update_toml(old, new):
'invites': [],
'platform': 'discord'
},
- 'emoji': None, 'description': None, 'display_name': None, 'banned': [], 'discord': {}
+ 'emoji': None, 'description': None, 'display_name': None, 'banned': []
}
if not '.welcome.txt' in os.listdir():
@@ -838,7 +838,7 @@ async def make(ctx,*,room):
return await ctx.send('Room names may only contain alphabets, numbers, dashes, and underscores.')
if room in list(db['rooms'].keys()):
return await ctx.send('This room already exists!')
- db['rooms'].update({room:{'meta': dict(room_template)}})
+ db['rooms'].update({room:{'meta': dict(room_template), 'discord': {}}})
db.save_data()
await ctx.send(f'Created room `{room}`!')
From 6ee0673d060f51b6b518da6e54203ca0b07a7b3b Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Sat, 5 Oct 2024 23:31:46 +0200
Subject: [PATCH 21/22] encrypted tokens + new pylint workflow
---
.github/workflows/pylint.yml | 48 +----
.github/workflows/pylint_standard.yml | 31 ++++
boot/bootloader.py | 134 +++++++++++--
boot/dep_installer.py | 39 ++--
boot/installer.py | 76 +++++---
boot/internal.json | 15 +-
boot/tokenmgr.py | 227 ++++++++++++++++++++++
config.toml | 1 +
microfier.py | 14 +-
utils/secrets.py | 258 ++++++++++++++++++++++++++
10 files changed, 752 insertions(+), 91 deletions(-)
create mode 100644 .github/workflows/pylint_standard.yml
create mode 100644 boot/tokenmgr.py
create mode 100644 utils/secrets.py
diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index 78bb56d..4c62514 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -1,46 +1,16 @@
name: Pylint
-on: [push]
+on: [push, pull_request]
jobs:
- build-linux:
+ linux-standard:
name: "Linux (Standard)"
- runs-on: ubuntu-latest
- strategy:
- matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12"]
- steps:
- - uses: actions/checkout@v4
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
- pip install pylint
- - name: Pylint analysis
- run: |
- pylint --enable-all-extensions --extension-pkg-allow-list=orjson,ujson --disable=R,C,W $(git ls-files '*.py')
+ uses: ./.github/workflows/pylint_standard.yml
+ with:
+ os: ubuntu-latest
- build-windows:
+ windows-standard:
name: "Windows (Standard)"
- runs-on: windows-latest
- strategy:
- matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12"]
- steps:
- - uses: actions/checkout@v4
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
- pip install pylint
- - name: Pylint analysis
- run: |
- pylint --enable-all-extensions --extension-pkg-allow-list=orjson,ujson --disable=R,C,W $(git ls-files '*.py')
+ uses: ./.github/workflows/pylint_standard.yml
+ with:
+ os: windows-latest
diff --git a/.github/workflows/pylint_standard.yml b/.github/workflows/pylint_standard.yml
new file mode 100644
index 0000000..da50445
--- /dev/null
+++ b/.github/workflows/pylint_standard.yml
@@ -0,0 +1,31 @@
+name: Pylint
+
+on:
+ workflow_call:
+ inputs:
+ os:
+ type: string
+ description: "The operating system to run the workflow on."
+ default: ubuntu-latest
+
+jobs:
+ build-linux:
+ name: "Standard"
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.9","3.10","3.11","3.12"]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ pip install pylint
+ - name: Pylint analysis
+ run: |
+ pylint --enable-all-extensions --extension-pkg-allow-list=orjson,ujson --disable=R,C,W $(git ls-files '*.py')
diff --git a/boot/bootloader.py b/boot/bootloader.py
index 630b749..427da35 100644
--- a/boot/bootloader.py
+++ b/boot/bootloader.py
@@ -1,22 +1,32 @@
+"""
+Unifier - A sophisticated Discord bot uniting servers and platforms
+Copyright (C) 2024 Green, ItsAsheer
+
+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 .
+"""
+
import os
import sys
import shutil
import json
import time
+import getpass
reinstall = '--reinstall' in sys.argv
depinstall = '--install-deps' in sys.argv
-
-install_options = [
- {
- 'id': 'stable',
- 'name': '\U0001F48E Standard',
- 'description': 'Uses the latest stable Nextcord version.',
- 'default': True,
- 'prefix': '',
- 'color': '\x1b[32'
- }
-]
+manage_tokens = '--tokens' in sys.argv
+clear_tokens = '--clear-tokens' in sys.argv
if os.getcwd().endswith('/boot'):
print('\x1b[31;1mYou are running the bootloader directly. Please run the run.sh file instead.\x1b[0m')
@@ -25,6 +35,7 @@
with open('boot/internal.json') as file:
internal = json.load(file)
+install_options = internal['options']
boot_config = {}
try:
@@ -65,7 +76,10 @@
)
else:
# this installation is fresh
- if not depinstall:
+ if manage_tokens or clear_tokens:
+ print(f'\x1b[31;1mNo Unifier installation was detected.\x1b[0m')
+ sys.exit(1)
+ elif not depinstall:
if not reinstall:
print('\x1b[33;1mInstallation not detected, running installer...\x1b[0m')
@@ -137,6 +151,49 @@
# sleep to prevent 429s
time.sleep(5)
+# tomli should be installed by now
+try:
+ import tomli # pylint: disable=import-error
+except:
+ print('\x1b[31;1mCould not import tomli. It should have been installed, please restart the bootloader.\x1b[0m')
+ sys.exit(1)
+
+with open('config.toml', 'rb') as file:
+ # noinspection PyTypeChecker
+ bot_config = tomli.load(file)
+
+if clear_tokens:
+ print('\x1b[37;41;1mWARNING: ALL TOKENS WILL BE CLEARED!\x1b[0m')
+ print('\x1b[33;1mYou should only clear your tokens if you forgot your password.\x1b[0m')
+ print('\x1b[33;1mThis process is irreversible. Once it\'s done, there\'s no going back!\x1b[0m')
+ print()
+ print('\x1b[33;1mProceed anyways? (y/n)\x1b[0m')
+
+ try:
+ confirm = input().lower()
+ if not confirm == 'y':
+ raise ValueError()
+ except:
+ print('\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
+
+ encryption_password = getpass.getpass('New password: ')
+ confirm_password = getpass.getpass('Confirm new password: ')
+ if not encryption_password == confirm_password:
+ print('\x1b[31;1mPasswords do not match.\x1b[0m')
+ sys.exit(1)
+
+ os.remove('.encryptedenv')
+ os.environ['UNIFIER_ENCPASS'] = str(encryption_password)
+ os.system(f'{binary} boot/tokenmgr.py')
+ sys.exit(0)
+
+if manage_tokens:
+ encryption_password = getpass.getpass('Password: ')
+ os.environ['UNIFIER_ENCPASS'] = str(encryption_password)
+ os.system(f'{binary} boot/tokenmgr.py')
+ sys.exit(0)
+
if not boot_file in os.listdir():
if os.path.isdir('update'):
print(f'\x1b[33;1m{boot_file} is missing, copying from update folder.\x1b[0m')
@@ -158,7 +215,60 @@
restart_options = ''
+choice = None
+
while True:
+ plain = os.path.isfile('.env')
+ 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
+ encryption_password = getpass.getpass('Password: ')
+ os.environ['UNIFIER_ENCPASS'] = str(encryption_password)
+ elif not choice is None:
+ # choice is set and password is correct
+ if choice == 1:
+ # do not reencrypt .env
+ choice = 0
+ elif plain and encrypted:
+ print('\x1b[33;1m.env and .encryptedenv are present. What would you like to do?\x1b[0m')
+ print('\x1b[33m1. Use .encryptedenv')
+ print('2. Replace .encryptedenv with .env\x1b[0m')
+
+ try:
+ choice = int(input()) - 1
+ if choice < 0 or choice > 1:
+ raise ValueError()
+ except:
+ print(f'\x1b[31;1mAborting.\x1b[0m')
+ sys.exit(1)
+ elif plain:
+ print(
+ '\x1b[33;1mNo .encryptedenv file could not be found, but a .env file was found, .env will be encrypted and used.\x1b[0m')
+ choice = 1
+ elif encrypted:
+ choice = 0
+ else:
+ print('\x1b[31;1mNo .env or .encryptedenv file could be found.\x1b[0m')
+ print('More info: https://wiki.unifierhq.org/setup-selfhosted/getting-started/unifier#set-bot-token')
+ sys.exit(1)
+
+ if choice == 0:
+ encryption_password = os.environ.get('UNIFIER_ENCPASS')
+ if not encryption_password:
+ encryption_password = getpass.getpass('Password: ')
+
+ os.environ['UNIFIER_ENCPASS'] = str(encryption_password)
+ del encryption_password
+ elif choice == 1:
+ encryption_password = getpass.getpass('New password: ')
+ confirm_password = getpass.getpass('Confirm password: ')
+ os.environ['UNIFIER_ENCPASS'] = str(encryption_password)
+ del encryption_password
+ del confirm_password
+ should_encrypt = True
+
+ os.environ['UNIFIER_ENCOPTION'] = str(choice)
+
exit_code = os.system(f'{binary} {boot_file}{restart_options}{options}')
crash_reboot = False
diff --git a/boot/dep_installer.py b/boot/dep_installer.py
index b085464..8316fe4 100644
--- a/boot/dep_installer.py
+++ b/boot/dep_installer.py
@@ -1,19 +1,31 @@
+"""
+Unifier - A sophisticated Discord bot uniting servers and platforms
+Copyright (C) 2024 Green, ItsAsheer
+
+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 .
+"""
+
import json
import os
import sys
install_option = sys.argv[1] if len(sys.argv) > 1 else None
-install_options = [
- {
- 'id': 'stable',
- 'name': '\U0001F48E Standard',
- 'description': 'Uses the latest stable Nextcord version.',
- 'default': True,
- 'prefix': '',
- 'color': '\x1b[32'
- }
-]
+with open('boot/internal.json') as file:
+ internal = json.load(file)
+
+install_options = internal['options']
prefix = None
@@ -41,13 +53,16 @@
print('\x1b[36;1mInstalling dependencies, this may take a while...\x1b[0m')
+user_arg = ' --user' if not boot_config['bootloader'].get('global_dep_install',False) else ''
+
if prefix:
- code = os.system(f'{binary} -m pip install --user -U -r requirements_{prefix}.txt')
+ code = os.system(f'{binary} -m pip install{user_arg} -U -r requirements_{prefix}.txt')
else:
- code = os.system(f'{binary} -m pip install --user -U -r requirements.txt')
+ code = os.system(f'{binary} -m pip install{user_arg} -U -r requirements.txt')
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')
sys.exit(code)
print('\x1b[36;1mDependencies successfully installed.\x1b[0m')
diff --git a/boot/installer.py b/boot/installer.py
index 230600b..9e04d3a 100644
--- a/boot/installer.py
+++ b/boot/installer.py
@@ -1,3 +1,21 @@
+"""
+Unifier - A sophisticated Discord bot uniting servers and platforms
+Copyright (C) 2024 Green, ItsAsheer
+
+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 .
+"""
+
import asyncio
import sys
import getpass
@@ -7,19 +25,14 @@
import tomli_w
import traceback
from nextcord.ext import commands
+from utils import secrets
install_option = sys.argv[1] if len(sys.argv) > 1 else None
-install_options = [
- {
- 'id': 'stable',
- 'name': '\U0001F48E Standard',
- 'description': 'Uses the latest stable Nextcord version.',
- 'default': True,
- 'prefix': '',
- 'color': '\x1b[32'
- }
-]
+with open('boot/internal.json') as file:
+ internal = json.load(file)
+
+install_options = internal['options']
if not install_option:
for option in install_options:
@@ -35,9 +48,6 @@
user_id = 0
server_id = 0
-with open('boot/internal.json') as file:
- internal = json.load(file)
-
if sys.version_info.minor < internal['required_py_version']:
print(f'\x1b[31;49mCannot install {internal["product_name"]}. Python 3.{internal["required_py_version"]} or later is required.\x1b[0m')
sys.exit(1)
@@ -124,6 +134,29 @@ async def on_ready():
token = getpass.getpass()
+encryption_password = ''
+salt = ''
+
+print('\x1b[33;1mWe will now ask for the token encryption salt. This must be an integer.\x1b[0m')
+print('\x1b[33;1mAs of Unifier v3.2.0, all tokens must be stored encrypted, even if it\'s stored as an environment variable.\x1b[0m')
+
+while True:
+ try:
+ salt = int(input())
+ break
+ except KeyboardInterrupt:
+ print('\x1b[31;49mAborted.\x1b[0m')
+ sys.exit(1)
+ except:
+ print('\x1b[31;49mThis isn\'t an integer, try again.\x1b[0m')
+
+print('\x1b[33;1mWe will now ask for the token encryption password. This is NOT your bot token.\x1b[0m')
+print(f'\x1b[37;41;1mWARNING: DO NOT SHARE THIS TOKEN, NOT EVEN WITH {internal["maintainer"].upper()}.\x1b[0m')
+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')
+
+encryption_password = getpass.getpass()
+
print('\x1b[36;1mStarting bot...\x1b[0m')
try:
@@ -134,27 +167,22 @@ async def on_ready():
print('\x1b[31;49mMake sure all privileged intents are enabled for the bot.\x1b[0m')
sys.exit(1)
-file = open('.env','w+')
-file.write(f'TOKEN={token}')
-file.close()
+tokenstore = secrets.TokenStore(False, password=encryption_password, salt=salt, content_override={'TOKEN': token})
+print('\x1b[36;1mYour tokens have been stored securely.\x1b[0m')
with open('config.toml', 'rb') as file:
config = tomli.load(file)
config['roles']['owner'] = user_id
+if not internal['skip_server']:
+ config['moderation']['home_guild'] = server_id
+
with open('config.toml', 'wb') as file:
tomli_w.dump(config, file)
with open('.install.json','w+') as file:
# noinspection PyTypeChecker
- json.dump(
- {
- 'product': internal["product"],
- 'setup': False,
- 'option': install_option
- },
- file
- )
+ json.dump({'product': internal["product"],'setup': False,'option': install_option}, file)
print(f'\x1b[36;1m{internal["product_name"]} installed successfully.\x1b[0m')
diff --git a/boot/internal.json b/boot/internal.json
index 94e3f22..2579251 100644
--- a/boot/internal.json
+++ b/boot/internal.json
@@ -3,6 +3,17 @@
"product": "unifier-micro",
"product_name": "Unifier Micro",
"base_bootfile": "microfier.py",
- "repo": "https://github.com/UnifierHQ/unifier-micro",
- "required_py_version": 9
+ "repo": "https://github.com/UnifierHQ/unifie-micror",
+ "required_py_version": 9,
+ "skip_server": true,
+ "options": [
+ {
+ "id": "standard",
+ "name": "\uD83D\uDC8E Standard",
+ "description": "Uses the latest stable Nextcord version.",
+ "default": true,
+ "prefix": "",
+ "color": "\\x1b[32"
+ }
+ ]
}
\ No newline at end of file
diff --git a/boot/tokenmgr.py b/boot/tokenmgr.py
new file mode 100644
index 0000000..57ddd10
--- /dev/null
+++ b/boot/tokenmgr.py
@@ -0,0 +1,227 @@
+"""
+Unifier - A sophisticated Discord bot uniting servers and platforms
+Copyright (C) 2024 Green, ItsAsheer
+
+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 .
+"""
+
+import traceback
+import tomli
+import tomli_w
+import sys
+import os
+import getpass
+
+try:
+ sys.path.insert(0, '.')
+ from utils import secrets
+except:
+ print('\x1b[31;1mSomething went wrong.\x1b[0m')
+ sys.exit(1)
+
+
+with open('config.toml', 'rb') as file:
+ # noinspection PyTypeChecker
+ config = tomli.load(file)
+
+salt = config['system']['encrypted_env_salt']
+
+try:
+ tokenmgr = secrets.TokenStore(True, password=os.environ.get('UNIFIER_ENCPASS'), salt=salt)
+except ValueError:
+ print('\x1b[31;1mYou must provide a password.\x1b[0m')
+ sys.exit(1)
+
+if not tokenmgr.test_decrypt():
+ print('\x1b[31;1mInvalid password. Your encryption password is needed to manage tokens.\x1b[0m')
+ print('\x1b[31;1mIf you\'ve forgot your password, run the bootscript again with --clear-tokens\x1b[0m')
+ sys.exit(1)
+
+def add_token():
+ identifier = input('Token identifier: ').upper()
+ if identifier == '':
+ print('\x1b[31;1mIdentifier cannot be empty.\x1b[0m')
+ return
+ token = getpass.getpass('Token: ')
+
+ try:
+ tokens = tokenmgr.add_token(identifier, token)
+ except KeyError:
+ print('\x1b[31;1mToken already exists.\x1b[0m')
+ return
+
+ print(f'\x1b[36;1mToken added successfully. You now have {tokens-1} tokens.\x1b[0m')
+
+def replace_token():
+ identifier = input('Token identifier: ').upper()
+ if identifier == '':
+ print('\x1b[31;1mIdentifier cannot be empty.\x1b[0m')
+ return
+ token = getpass.getpass('New token: ')
+ password = getpass.getpass('Encryption password: ')
+
+ print('\x1b[37;41;1mWARNING: THIS TOKEN WILL BE REPLACED!\x1b[0m')
+ print('\x1b[33;1mThis process is irreversible. Once it\'s done, there\'s no going back!\x1b[0m')
+ print()
+ print('\x1b[33;1mProceed anyways? (y/n)\x1b[0m')
+
+ try:
+ confirm = input().lower()
+ if not confirm == 'y':
+ raise ValueError()
+ except:
+ print('\x1b[31;1mAborting.\x1b[0m')
+ return
+
+ try:
+ tokenmgr.replace_token(identifier, token, password)
+ except KeyError:
+ print('\x1b[31;1mToken does not exist.\x1b[0m')
+ return
+ except ValueError:
+ print('\x1b[31;1mInvalid password. Your encryption password is needed to replace or delete tokens.\x1b[0m')
+ return
+
+ print('\x1b[36;1mToken replaced successfully.\x1b[0m')
+
+
+def delete_token():
+ identifier = input('Token identifier: ').upper()
+ if identifier == '':
+ print('\x1b[31;1mIdentifier cannot be empty.\x1b[0m')
+ return
+ password = getpass.getpass('Encryption password: ')
+
+ print('\x1b[37;41;1mWARNING: THIS TOKEN WILL BE DELETED!\x1b[0m')
+ print('\x1b[33;1mThis process is irreversible. Once it\'s done, there\'s no going back!\x1b[0m')
+ print()
+ print('\x1b[33;1mProceed anyways? (y/n)\x1b[0m')
+
+ try:
+ confirm = input().lower()
+ if not confirm == 'y':
+ raise ValueError()
+ except:
+ print('\x1b[31;1mAborting.\x1b[0m')
+ return
+
+ try:
+ tokens = tokenmgr.delete_token(identifier, password)
+ except KeyError:
+ print('\x1b[31;1mToken does not exist.\x1b[0m')
+ return
+ except ValueError:
+ print('\x1b[31;1mInvalid password. Your encryption password is needed to replace or delete tokens.\x1b[0m')
+ return
+
+ print(f'\x1b[36;1mToken deleted successfully. You now have {tokens-1} tokens.\x1b[0m')
+
+def list_tokens():
+ print(f'\x1b[36;1mYou have {len(tokenmgr.tokens)} tokens.\x1b[0m')
+
+ for index in range(len(tokenmgr.tokens)):
+ token = tokenmgr.tokens[index]
+ print(f'\x1b[36m{index + 1}. {token}\x1b[0m')
+
+def reencrypt_tokens():
+ salt = input('New salt integer (leave empty to keep current): ')
+ if salt == '':
+ salt = config['system']['encrypted_env_salt']
+ else:
+ try:
+ salt = int(salt)
+ except:
+ print('\x1b[31;1mSalt must be an integer.\x1b[0m')
+ return
+
+ current_password = getpass.getpass('Current encryption password: ')
+ password = getpass.getpass('New encryption password: ')
+ confirm_password = getpass.getpass('Confirm encryption password: ')
+
+ if not password == confirm_password:
+ print('\x1b[31;1mPasswords do not match.\x1b[0m')
+ return
+
+ del confirm_password
+
+ print('\x1b[37;41;1mWARNING: YOUR TOKENS WILL BE RE-ENCRYPTED!\x1b[0m')
+ print('\x1b[33;1mYou will need to use your new encryption password to start Unifier.\x1b[0m')
+ print('\x1b[33;1mThis process is irreversible. Once it\'s done, there\'s no going back!\x1b[0m')
+ print()
+ print('\x1b[33;1mProceed anyways? (y/n)\x1b[0m')
+
+ try:
+ confirm = input().lower()
+ if not confirm == 'y':
+ raise ValueError()
+ except:
+ print('\x1b[31;1mAborting.\x1b[0m')
+ return
+
+ try:
+ tokenmgr.reencrypt(current_password, password, salt)
+ except ValueError:
+ print('\x1b[31;1mInvalid password. Your current encryption password is needed to re-encrypt tokens.\x1b[0m')
+ return
+
+ if not salt == config['system']['encrypted_env_salt']:
+ config['system']['encrypted_env_salt'] = salt
+ with open('config.toml', 'wb') as file:
+ tomli_w.dump(config, file)
+
+ print('\x1b[36;1mTokens have been re-encrypted successfully.\x1b[0m')
+
+def command_help():
+ print('\x1b[36;1mCommands:\x1b[0m')
+ print('\x1b[36madd-token\x1b[0m')
+ print('\x1b[36mreplace-token\x1b[0m')
+ print('\x1b[36mdelete-token\x1b[0m')
+ print('\x1b[36mlist-tokens\x1b[0m')
+ print('\x1b[36mreencrypt-tokens\x1b[0m')
+ print('\x1b[36mhelp\x1b[0m')
+ print('\x1b[36mexit\x1b[0m')
+
+
+list_tokens()
+
+print('Type "help" for a list of commands.')
+
+while True:
+ try:
+ command = input('> ').lower()
+ except KeyboardInterrupt:
+ break
+
+ try:
+ if command == 'add-token':
+ add_token()
+ elif command == 'replace-token':
+ replace_token()
+ elif command == 'delete-token':
+ delete_token()
+ elif command == 'list-tokens':
+ list_tokens()
+ elif command == 'reencrypt-tokens':
+ reencrypt_tokens()
+ elif command == 'help':
+ command_help()
+ elif command == 'exit':
+ break
+ else:
+ print('\x1b[33;1mInvalid command. Type "help" for a list of commands.\x1b[0m')
+ except KeyboardInterrupt:
+ pass
+ except:
+ traceback.print_exc()
+ print('\x1b[31;1mAn error occurred.\x1b[0m')
diff --git a/config.toml b/config.toml
index 41e7df9..2b9a7d1 100644
--- a/config.toml
+++ b/config.toml
@@ -1,6 +1,7 @@
[system]
debug = false
package = "microfier"
+encrypted_env_salt = 10 # change this to whatever you want, as long as the bot doesn't crash
[roles]
owner = -1
diff --git a/microfier.py b/microfier.py
index dd83811..cc44bb5 100644
--- a/microfier.py
+++ b/microfier.py
@@ -26,7 +26,7 @@
import sys
import os
import re
-from utils import log, ui
+from utils import log, ui, secrets
import math
import tomli
import tomli_w
@@ -268,6 +268,13 @@ def update_toml(old, new):
logger = log.buildlogger(package,'core',level)
+should_encrypt = int(os.environ['UNIFIER_ENCOPTION']) == 1
+tokenstore = secrets.TokenStore(not should_encrypt, os.environ['UNIFIER_ENCPASS'], config['encrypted_env_salt'], config['debug'])
+
+if should_encrypt:
+ tokenstore.to_encrypted(os.environ['UNIFIER_ENCPASS'], config['encrypted_env_salt'])
+ os.remove('.env')
+
room_template = {
'rules': [], 'restricted': False, 'locked': False, 'private': False,
'private_meta': {
@@ -2088,4 +2095,7 @@ async def on_message_delete(message):
await webhook.delete_message(msg_id)
-bot.run(os.environ.get('TOKEN'))
+try:
+ bot.run(tokenstore.retrieve('TOKEN'))
+except KeyboardInterrupt:
+ sys.exit(0)
diff --git a/utils/secrets.py b/utils/secrets.py
new file mode 100644
index 0000000..10ec680
--- /dev/null
+++ b/utils/secrets.py
@@ -0,0 +1,258 @@
+"""
+Unifier - A sophisticated Discord bot uniting servers and platforms
+Copyright (C) 2024 Green, ItsAsheer
+
+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 .
+"""
+
+import os
+import json
+import base64
+import traceback
+import string
+from dotenv import load_dotenv
+from Crypto.Random import random
+from Crypto.Protocol.KDF import PBKDF2
+from Crypto.Cipher import AES
+from Crypto import Random as CryptoRandom
+from Crypto.Util.Padding import pad, unpad
+
+# import ujson if installed
+try:
+ import ujson as json # pylint: disable=import-error
+except:
+ pass
+
+class Encryptor:
+ def __init__(self):
+ pass
+
+ def encrypt(self, encoded, password, salt):
+ """Encrypts a given bytes object."""
+ __key = PBKDF2(password, salt, dkLen=32)
+
+ iv = CryptoRandom.get_random_bytes(16)
+ __cipher = AES.new(__key, AES.MODE_CBC, iv=iv)
+ result = __cipher.encrypt(pad(encoded, AES.block_size))
+ del __key
+ del __cipher
+ return result, base64.b64encode(iv).decode('ascii')
+
+ def decrypt(self, encrypted, password, salt, iv_string):
+ """Decrypts a given encrypted bytes object."""
+ iv = base64.b64decode(iv_string)
+ __key = PBKDF2(password, salt, dkLen=32)
+ __cipher = AES.new(__key, AES.MODE_CBC, iv=iv)
+ result = unpad(__cipher.decrypt(encrypted), AES.block_size)
+ del __key
+ del __cipher
+ return result
+
+class TokenStore:
+ def __init__(self, encrypted, password=None, salt=None, debug=False, content_override=None):
+ self.__is_encrypted = encrypted
+ self.__encryptor = Encryptor()
+ self.__password = password
+ self.__salt = salt
+
+ if encrypted:
+ if not password:
+ raise ValueError('encryption password must be provided')
+ if not salt:
+ raise ValueError('encryption salt must be provided')
+
+ # file is in json format
+ try:
+ with open('.encryptedenv', 'r') as file:
+ self.__data = json.load(file)
+ with open('.ivs', 'r') as file:
+ self.__ivs = json.load(file)
+ except:
+ self.__data = {}
+ self.__ivs = {}
+ else:
+ # file is in dotenv format, load using load_dotenv
+ # we will not encapsulate dotenv data for the sake of backwards compatibility
+ if content_override:
+ # content override is a feature only to be used by bootloader
+ self.__data = content_override
+ else:
+ load_dotenv()
+ self.__data = os.environ
+
+ self.__debug = debug
+
+ @property
+ def encrypted(self):
+ return self.__is_encrypted
+
+ @property
+ def ivs(self):
+ # initialization vectors are public, so they can be safely displayed in plaintext
+ return self.__ivs
+
+ @property
+ def debug(self):
+ return self.__debug
+
+ @property
+ def tokens(self):
+ if not self.__is_encrypted:
+ raise ValueError('cannot retrieve keys when tokens are unencrypted')
+
+ tokens = list(self.__data.keys())
+ tokens.remove('test')
+ return tokens
+
+ def to_encrypted(self, password, salt):
+ dotenv = open('.env', 'r')
+ lines = dotenv.readlines()
+ dotenv.close()
+
+ keys = []
+ for line in lines:
+ key = line.split('=', 1)[0]
+ while key.endswith(' '):
+ key = key[:-1]
+ keys.append(key)
+
+ encrypted_env = {'test': None}
+ ivs = {'test': None}
+
+ test_value, test_iv = self.__encryptor.encrypt(str.encode(
+ ''.join([random.choice(string.ascii_letters + string.digits) for _ in range(16)])
+ ), password, salt)
+
+ encrypted_env['test'] = base64.b64encode(test_value).decode('ascii')
+ ivs['test'] = test_iv
+
+ # we can get values from dotenv, since that's been done if TokenStore is not encrypted
+
+ for key in keys:
+ __token = os.environ.get(key)
+ if not __token:
+ continue
+
+ encrypted_value, iv = self.__encryptor.encrypt(str.encode(__token), password, salt)
+ encrypted_env.update({key: base64.b64encode(encrypted_value).decode('ascii')})
+ ivs.update({key: iv})
+ del os.environ[key]
+
+ with open('.encryptedenv', 'w+') as file:
+ # noinspection PyTypeChecker
+ json.dump(encrypted_env, file)
+
+ with open('.ivs', 'w+') as file:
+ # noinspection PyTypeChecker
+ json.dump(ivs, file)
+
+ self.__data = encrypted_env
+ self.__ivs = ivs
+ self.__password = password
+ self.__salt = salt
+ self.__is_encrypted = True
+
+ def test_decrypt(self, password=None):
+ if not self.__is_encrypted:
+ return True
+
+ try:
+ self.__encryptor.decrypt(base64.b64decode(self.__data['test']), password or self.__password, self.__salt, self.__ivs['test'])
+ except:
+ if self.__debug:
+ traceback.print_exc()
+
+ return False
+ return True
+
+ def retrieve(self, identifier):
+ data = str.encode(self.__data[identifier])
+ iv = self.__ivs[identifier]
+ decrypted = self.__encryptor.decrypt(base64.b64decode(data), self.__password, self.__salt, iv)
+ return decrypted.decode('utf-8')
+
+ def add_token(self, identifier, token):
+ if identifier in self.__data.keys():
+ raise KeyError('token already exists')
+
+ encrypted, iv = self.__encryptor.encrypt(str.encode(token), self.__password, self.__salt)
+ self.__data.update({identifier: base64.b64encode(encrypted).decode('ascii')})
+ self.__ivs.update({identifier: iv})
+ self.save('.encryptedenv', '.ivs')
+ return len(self.__data)
+
+ def replace_token(self, identifier, token, password):
+ # password prompt to prevent unauthorized token deletion
+ if not self.test_decrypt(password=password):
+ raise ValueError('invalid password')
+
+ if not identifier in self.tokens:
+ raise KeyError('token does not exist')
+
+ if identifier == 'test':
+ raise ValueError('cannot replace token, this is needed for password verification')
+
+ encrypted, iv = self.__encryptor.encrypt(str.encode(token), self.__password, self.__salt)
+ self.__data.update({identifier: base64.b64encode(encrypted).decode('ascii')})
+ self.__ivs.update({identifier: iv})
+ self.save('.encryptedenv', '.ivs')
+
+ def delete_token(self, identifier, password):
+ # password prompt to prevent unauthorized token deletion
+ if not self.test_decrypt(password=password):
+ raise ValueError('invalid password')
+
+ if not identifier in self.tokens:
+ raise KeyError('token does not exist')
+
+ if identifier == 'test':
+ raise ValueError('cannot delete token, this is needed for password verification')
+
+ del self.__data[identifier]
+ del self.__ivs[identifier]
+ self.save('.encryptedenv', '.ivs')
+ return len(self.__data)
+
+ def reencrypt(self, current_password, password, salt):
+ if not self.test_decrypt(password=current_password):
+ raise ValueError('invalid password')
+
+ for key in self.__data.keys():
+ token = self.retrieve(key)
+ encrypted, iv = self.__encryptor.encrypt(str.encode(token), password, salt)
+ self.__data[key] = base64.b64encode(encrypted).decode('ascii')
+ self.__ivs[key] = iv
+
+ self.__password = password
+ self.__salt = salt
+ self.save('.encryptedenv', '.ivs')
+
+ def save(self, filename, iv_filename):
+ if not self.__is_encrypted:
+ raise ValueError('cannot save unencrypted data')
+
+ test_value, test_iv = self.__encryptor.encrypt(str.encode(
+ ''.join([random.choice(string.ascii_letters + string.digits) for _ in range(16)])
+ ), self.__password, self.__salt)
+
+ self.__data['test'] = base64.b64encode(test_value).decode('ascii')
+ self.__ivs['test'] = test_iv
+
+ with open(filename, 'w+') as file:
+ # noinspection PyTypeChecker
+ json.dump(self.__data, file)
+
+ with open(iv_filename, 'w+') as file:
+ # noinspection PyTypeChecker
+ json.dump(self.__ivs, file)
From fe56d6610f5070a084025ab5a81d851972817731 Mon Sep 17 00:00:00 2001
From: green <41323182+greeeen-dev@users.noreply.github.com>
Date: Sat, 5 Oct 2024 23:32:54 +0200
Subject: [PATCH 22/22] add pycryptodome
---
requirements.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index ef611ae..134153f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
nextcord
python-dotenv
tomli
-tomli-w
\ No newline at end of file
+tomli-w
+pycryptodome
\ No newline at end of file