Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.0.0: Data structure and customization updates #23

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<h1 align=center>
<img width=64 src=https://github.com/UnifierHQ/unifier-micro/assets/41323182/16f7ca32-bc5a-47d2-89ae-50ebef8ef70d>
<img width=64 src=https://github.com/user-attachments/assets/7b4609ec-476e-44bd-92b9-1d284e1a47e7>

Unifier Micro</h1>

@@ -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
167 changes: 167 additions & 0 deletions boot/bootloader.py
Original file line number Diff line number Diff line change
@@ -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)
54 changes: 54 additions & 0 deletions boot/dep_installer.py
Original file line number Diff line number Diff line change
@@ -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)
160 changes: 160 additions & 0 deletions boot/installer.py
Original file line number Diff line number Diff line change
@@ -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')
Loading