From e21532d31950219148b0f2510690aae6ed01c339 Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 11 Jan 2021 15:19:17 -0300 Subject: [PATCH 1/3] Add Discord bot --- standalone.py | 75 +++++++++++++++++++++++++++++++++++++++++++++--- zwift_offline.py | 55 ++++++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/standalone.py b/standalone.py index ea092da..33520e4 100755 --- a/standalone.py +++ b/standalone.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from gevent import monkey -monkey.patch_all() import os import signal import struct @@ -17,10 +15,12 @@ if sys.version_info[0] > 2: import socketserver from http.server import SimpleHTTPRequestHandler from http.cookies import SimpleCookie + from configparser import ConfigParser else: import SocketServer as socketserver from SimpleHTTPServer import SimpleHTTPRequestHandler from Cookie import SimpleCookie + from ConfigParser import ConfigParser import zwift_offline import protobuf.udp_node_msgs_pb2 as udp_node_msgs_pb2 @@ -519,7 +519,11 @@ class UDPHandler(socketserver.BaseRequestHandler): for p_id in remove_players: online.pop(p_id) if state.roadTime: - online[player_id] = state + if player_id in online.keys(): + online[player_id] = state + else: + online[player_id] = state + zwift_offline.send_message_to_discord('%s riders online' % len(online)) #Remove ghosts entries for inactive players (disconnected?) keys = global_ghosts.keys() @@ -640,4 +644,67 @@ botthreadevent = threading.Event() bot = threading.Thread(target=play_bots) bot.start() -zwift_offline.run_standalone(online, global_pace_partners, global_bots, ghosts_enabled, save_ghost, player_update_queue) +import discord +import asyncio + +intents = discord.Intents.default() +intents.members = True + +DISCORD_CONFIG_FILE = "%s/discord.cfg" % STORAGE_DIR +if os.path.isfile(DISCORD_CONFIG_FILE): + CONFIG = ConfigParser() + SECTION = 'discord' + if sys.version_info[0] > 2: + CONFIG.read(DISCORD_CONFIG_FILE) + else: + file = open(DISCORD_CONFIG_FILE, 'rb') + CONFIG.readfp(file) + discord_token = CONFIG.get(SECTION, 'token') + discord_webhook = CONFIG.get(SECTION, 'webhook') + discord_channel = CONFIG.getint(SECTION, 'channel') + discord_welcome_message = CONFIG.get(SECTION, 'welcome_message') + discord_help_message = CONFIG.get(SECTION, 'help_message') +else: + discord_token = None + discord_webhook = None + +class DiscordBot(discord.Client): + async def on_ready(self): + self.channel = self.get_channel(discord_channel) + + async def on_member_join(self, member): + if discord_welcome_message: + await self.channel.send('%s\n%s' % (member.mention, discord_welcome_message)) + + async def on_message(self, message): + if message.author.id == self.user.id: + return + if message.content == '!online': + await message.channel.send('%s riders online' % len(online)) + elif message.content == '!help' and discord_help_message: + await message.channel.send(discord_help_message) + elif message.content == '!ping': + await message.channel.send('pong') + elif message.channel == self.channel and not message.author.bot and not message.content.startswith('!'): + zwift_offline.send_message_to_all_online(message.content, message.author.name) + +class DiscordThread(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.loop = asyncio.get_event_loop() + self.start() + + async def starter(self): + self.discord_bot = DiscordBot(intents=intents) + await self.discord_bot.start(discord_token) + + def run(self): + try: + self.loop.run_until_complete(self.starter()) + except BaseException: + time.sleep(5) + +if discord_token: + discord_thread = DiscordThread() + +zwift_offline.run_standalone(online, global_pace_partners, global_bots, ghosts_enabled, save_ghost, player_update_queue, discord_webhook) diff --git a/zwift_offline.py b/zwift_offline.py index 76ec48d..f021617 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -142,6 +142,7 @@ save_ghost = None restarting = False restarting_in_minutes = 0 reload_pacer_bots = False +discord_webhook = None class User(UserMixin, db.Model): player_id = db.Column(db.Integer, primary_key=True) @@ -462,7 +463,7 @@ def user_home(username): return render_template("user_home.html", username=current_user.username, enable_ghosts=bool(current_user.enable_ghosts), online=get_online(), is_admin=current_user.is_admin, restarting=restarting, restarting_in_minutes=restarting_in_minutes) -def send_message_to_all_online(message): +def send_message_to_all_online(message, sender='Server'): player_update = udp_node_msgs_pb2.PlayerUpdate() player_update.f2 = 1 player_update.type = 5 #chat message type @@ -475,7 +476,7 @@ def send_message_to_all_online(message): chat_message.rider_id = 0 chat_message.to_rider_id = 0 chat_message.f3 = 1 - chat_message.firstName = 'Server' + chat_message.firstName = sender chat_message.lastName = '' chat_message.message = message chat_message.countryCode = 0 @@ -495,7 +496,9 @@ def send_restarting_message(): time.sleep(60) restarting_in_minutes -= 1 if restarting and restarting_in_minutes == 0: - send_message_to_all_online('See you later! Look for the back online message.') + message = 'See you later! Look for the back online message.' + send_message_to_all_online(message) + send_message_to_discord(message) time.sleep(6) os.kill(os.getpid(), signal.SIGINT) @@ -509,6 +512,7 @@ def restart_server(): restarting_in_minutes = 10 send_restarting_message_thread = threading.Thread(target=send_restarting_message) send_restarting_message_thread.start() + send_message_to_discord('Restarting / Shutting down in %s minutes. Save your progress or continue riding until server is back online' % restarting_in_minutes) return redirect('/user/%s/' % current_user.username) @app.route("/cancelrestart") @@ -519,7 +523,9 @@ def cancel_restart_server(): if bool(current_user.is_admin): restarting = False restarting_in_minutes = 0 - send_message_to_all_online('Restart of the server has been cancelled. Ride on!') + message = 'Restart of the server has been cancelled. Ride on!' + send_message_to_all_online(message) + send_message_to_discord(message) return redirect('/user/%s/' % current_user.username) @app.route("/reloadbots") @@ -695,6 +701,7 @@ def logout(player_id): online.pop(player_id) if player_id in player_partial_profiles: player_partial_profiles.pop(player_id) + send_message_to_discord('%s riders online' % len(online)) @app.route('/api/users/logout', methods=['POST']) @@ -1053,6 +1060,10 @@ def api_profiles_activities_rideon(recieving_player_id): if not recieving_player_id in player_update_queue: player_update_queue[recieving_player_id] = list() player_update_queue[recieving_player_id].append(player_update.SerializeToString()) + + receiver = get_partial_profile(recieving_player_id) + message = 'Ride on ' + receiver.first_name + ' ' + receiver.last_name + '!' + send_message_to_discord(message, sending_player_id) return '{}', 200 @@ -1207,6 +1218,21 @@ def add_player_to_world(player, course_world, is_pace_partner): course_world[course_id].f5 += 1 +import requests +import json + +def send_message_to_discord(message, sender_id=None): + if not discord_webhook: return + if sender_id is not None: + profile = get_partial_profile(sender_id) + sender = profile.first_name + ' ' + profile.last_name + else: sender = 'Server' + data = {} + data["content"] = message + data["username"] = sender + requests.post(discord_webhook, data=json.dumps(data), headers={"Content-Type": "application/json"}) + + def relay_worlds_generic(world_id=None): courses = courses_lookup.keys() # Android client also requests a JSON version @@ -1270,6 +1296,10 @@ def relay_worlds_generic(world_id=None): if not recieving_player_id in player_update_queue: player_update_queue[recieving_player_id] = list() player_update_queue[recieving_player_id].append(player_update.SerializeToString()) + if player_update.type == 5: + chat_message = udp_node_msgs_pb2.ChatMessage() + chat_message.ParseFromString(player_update.payload) + send_message_to_discord(chat_message.message, chat_message.rider_id) return '{}', 200 else: # protobuf request worlds = world_pb2.Worlds() @@ -1553,7 +1583,9 @@ def check_columns(): def send_server_back_online_message(): time.sleep(30) - send_message_to_all_online("We're back online. Ride on!") + message = "We're back online. Ride on!" + send_message_to_all_online(message) + send_message_to_discord(message) @app.before_first_request @@ -1564,8 +1596,6 @@ def before_first_request(): db.session.commit() # in case create_all created a table check_columns() db.session.close() - send_message_thread = threading.Thread(target=send_server_back_online_message) - send_message_thread.start() #################### @@ -1694,13 +1724,14 @@ def auth_realms_zwift_tokens_access_codes(): return FAKE_JWT, 200 -def run_standalone(passed_online, passed_global_pace_partners, passed_global_bots, passed_ghosts_enabled, passed_save_ghost, passed_player_update_queue): +def run_standalone(passed_online, passed_global_pace_partners, passed_global_bots, passed_ghosts_enabled, passed_save_ghost, passed_player_update_queue, passed_discord_webhook): global online global global_pace_partners global global_bots global ghosts_enabled global save_ghost global player_update_queue + global discord_webhook global login_manager online = passed_online global_pace_partners = passed_global_pace_partners @@ -1708,6 +1739,7 @@ def run_standalone(passed_online, passed_global_pace_partners, passed_global_bot ghosts_enabled = passed_ghosts_enabled save_ghost = passed_save_ghost player_update_queue = passed_player_update_queue + discord_webhook = passed_discord_webhook login_manager = LoginManager() login_manager.login_view = 'login' login_manager.session_protection = None @@ -1719,10 +1751,11 @@ def run_standalone(passed_online, passed_global_pace_partners, passed_global_bot def load_user(uid): return User.query.get(int(uid)) - server = WSGIServer(('0.0.0.0', 443), app, certfile='%s/cert-zwift-com.pem' % SSL_DIR, keyfile='%s/key-zwift-com.pem' % SSL_DIR, log=logger) - thread = threading.Thread(target=server.serve_forever) - thread.start() + send_message_thread = threading.Thread(target=send_server_back_online_message) + send_message_thread.start() logger.info("Server is running.") + server = WSGIServer(('0.0.0.0', 443), app, certfile='%s/cert-zwift-com.pem' % SSL_DIR, keyfile='%s/key-zwift-com.pem' % SSL_DIR, log=logger) + server.serve_forever() # app.run(ssl_context=('%s/cert-zwift-com.pem' % SSL_DIR, '%s/key-zwift-com.pem' % SSL_DIR), port=443, threaded=True, host='0.0.0.0') # debug=True, use_reload=False) From c635d853c42b4ef129c85a35d0b2c5f28c5d2d20 Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 11 Jan 2021 15:27:12 -0300 Subject: [PATCH 2/3] Update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 1c827be..fdb2413 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,15 @@ To enable support for multiple users perform the steps below. zoffline's previou * To obtain the official map schedule and update files from Zwift server: create a ``cdn-proxy.txt`` file in the ``storage`` directory. This can only work if you are running zoffline on a different machine than the Zwift client. * To enable the password reset feature when multiplayer is enabled: create a ``gmail_credentials.txt`` file in the ``storage`` directory containing the login credentials of a Gmail account. You need to enable the "Less secure app access" in the account settings and you may need to access https://accounts.google.com/DisplayUnlockCaptcha to allow the login from the server. +* To enable the Discord bridge bot: create a ``discord.cfg`` file in the ``storage`` directory containing + ``` + [discord] + token = + webhook = + channel = + welcome_message = + help_message = + ``` * If the Zwift client is having issues connecting to the Linux server ("The request was aborted: Could not create SSL/TLS secure channel." or "The underlying connection was closed: An unexpected error occurred on a send. Received an unexpected EOF or 0 bytes from the transport stream."): change MinProtocol in /etc/ssl/openssl.cnf to TLSv1.0 ``` [system_default_sect] From ff810a967f44da81760d975bb533c6d0da94c436 Mon Sep 17 00:00:00 2001 From: zoffline Date: Tue, 26 Jan 2021 20:10:23 -0500 Subject: [PATCH 3/3] Separate Discord bot a bit more and remove dep on 'discord' --- README.md | 2 +- discord_bot.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++++ standalone.py | 71 +++++---------------------------------- zwift_offline.py | 42 +++++++++-------------- 4 files changed, 113 insertions(+), 89 deletions(-) create mode 100644 discord_bot.py diff --git a/README.md b/README.md index 3666d9e..92ac8d0 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ To enable support for multiple users perform the steps below. zoffline's previou * To obtain the official map schedule and update files from Zwift server: create a ``cdn-proxy.txt`` file in the ``storage`` directory. This can only work if you are running zoffline on a different machine than the Zwift client. * To enable the password reset feature when multiplayer is enabled: create a ``gmail_credentials.txt`` file in the ``storage`` directory containing the login credentials of a Gmail account. You need to enable the "Less secure app access" in the account settings and you may need to access https://accounts.google.com/DisplayUnlockCaptcha to allow the login from the server. -* To enable the Discord bridge bot: create a ``discord.cfg`` file in the ``storage`` directory containing +* To enable the Discord bridge bot: ``pip3 install discord``` and create a ``discord.cfg`` file in the ``storage`` directory containing ``` [discord] token = diff --git a/discord_bot.py b/discord_bot.py new file mode 100644 index 0000000..5037dd2 --- /dev/null +++ b/discord_bot.py @@ -0,0 +1,87 @@ +# Python 3 only (asyncio) + +import json +import os +import threading +import time + +from configparser import ConfigParser +from urllib import request + +import asyncio +import discord + +import zwift_offline + + +class DiscordBot(discord.Client): + # TODO: this should be part of __init__() + def set_vars(self, channel, welcome_msg, help_msg): + self.channel = channel + self.welcome_msg = welcome_msg + self.help_msg = help_msg + + async def on_ready(self): + self.channel = self.get_channel(self.channel) + + async def on_member_join(self, member): + if self.welcome_msg: + await self.channel.send('%s\n%s' % (member.mention, self.welcome_msg)) + + async def on_message(self, message): + if message.author.id == self.user.id: + return + if message.content == '!online': + await message.channel.send('%s riders online' % len(online)) + elif message.content == '!help' and self.help_msg: + await message.channel.send(self.help_msg) + elif message.content == '!ping': + await message.channel.send('pong') + elif message.channel == self.channel and not message.author.bot and not message.content.startswith('!'): + zwift_offline.send_message_to_all_online(message.content, message.author.name) + + +class DiscordThread(threading.Thread): + def __init__(self, config_file): + threading.Thread.__init__(self) + if not os.path.isfile(config_file): + raise Exception("DiscordThread invoked without a configuration file") + + self.CONFIG = ConfigParser() + SECTION = 'discord' + self.CONFIG.read(config_file) + self.token = self.CONFIG.get(SECTION, 'token') + self.webhook = self.CONFIG.get(SECTION, 'webhook') + self.channel = self.CONFIG.getint(SECTION, 'channel') + self.welcome_msg = self.CONFIG.get(SECTION, 'welcome_message') + self.help_msg = self.CONFIG.get(SECTION, 'help_message') + + self.intents = discord.Intents.default() + self.intents.members = True + self.loop = asyncio.get_event_loop() + self.start() + + async def starter(self): + self.discord_bot = DiscordBot(intents=self.intents) + self.discord_bot.set_vars(self.channel, self.welcome_msg, self.help_msg) + await self.discord_bot.start(self.token) + + def run(self): + try: + self.loop.run_until_complete(self.starter()) + except BaseException: + time.sleep(5) + + def send_message(self, message, sender_id=None): + if sender_id is not None: + profile = get_partial_profile(sender_id) + sender = profile.first_name + ' ' + profile.last_name + else: + sender = 'Server' + data = {} + data["content"] = message + data["username"] = sender + + req = request.Request(self.webhook, data=json.dumps(data).encode('UTF-8'), + headers={"Content-Type": "application/json"}) + request.urlopen(req).read() diff --git a/standalone.py b/standalone.py index 33520e4..c828c11 100755 --- a/standalone.py +++ b/standalone.py @@ -45,6 +45,7 @@ BOTS_DIR = '%s/bots' % SCRIPT_DIR PROXYPASS_FILE = "%s/cdn-proxy.txt" % STORAGE_DIR SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR +DISCORD_CONFIG_FILE = "%s/discord.cfg" % STORAGE_DIR MAP_OVERRIDE = deque(maxlen=16) ghost_update_freq = 3 @@ -523,7 +524,7 @@ class UDPHandler(socketserver.BaseRequestHandler): online[player_id] = state else: online[player_id] = state - zwift_offline.send_message_to_discord('%s riders online' % len(online)) + zdiscord.send_message('%s riders online' % len(online)) #Remove ghosts entries for inactive players (disconnected?) keys = global_ghosts.keys() @@ -644,67 +645,13 @@ botthreadevent = threading.Event() bot = threading.Thread(target=play_bots) bot.start() -import discord -import asyncio - -intents = discord.Intents.default() -intents.members = True - -DISCORD_CONFIG_FILE = "%s/discord.cfg" % STORAGE_DIR if os.path.isfile(DISCORD_CONFIG_FILE): - CONFIG = ConfigParser() - SECTION = 'discord' - if sys.version_info[0] > 2: - CONFIG.read(DISCORD_CONFIG_FILE) - else: - file = open(DISCORD_CONFIG_FILE, 'rb') - CONFIG.readfp(file) - discord_token = CONFIG.get(SECTION, 'token') - discord_webhook = CONFIG.get(SECTION, 'webhook') - discord_channel = CONFIG.getint(SECTION, 'channel') - discord_welcome_message = CONFIG.get(SECTION, 'welcome_message') - discord_help_message = CONFIG.get(SECTION, 'help_message') + from discord_bot import DiscordThread + discord = DiscordThread(DISCORD_CONFIG_FILE) else: - discord_token = None - discord_webhook = None + class DummyDiscord(): + def send_message(msg, sender_id=None): + pass + discord = DummyDiscord() -class DiscordBot(discord.Client): - async def on_ready(self): - self.channel = self.get_channel(discord_channel) - - async def on_member_join(self, member): - if discord_welcome_message: - await self.channel.send('%s\n%s' % (member.mention, discord_welcome_message)) - - async def on_message(self, message): - if message.author.id == self.user.id: - return - if message.content == '!online': - await message.channel.send('%s riders online' % len(online)) - elif message.content == '!help' and discord_help_message: - await message.channel.send(discord_help_message) - elif message.content == '!ping': - await message.channel.send('pong') - elif message.channel == self.channel and not message.author.bot and not message.content.startswith('!'): - zwift_offline.send_message_to_all_online(message.content, message.author.name) - -class DiscordThread(threading.Thread): - def __init__(self): - threading.Thread.__init__(self) - self.loop = asyncio.get_event_loop() - self.start() - - async def starter(self): - self.discord_bot = DiscordBot(intents=intents) - await self.discord_bot.start(discord_token) - - def run(self): - try: - self.loop.run_until_complete(self.starter()) - except BaseException: - time.sleep(5) - -if discord_token: - discord_thread = DiscordThread() - -zwift_offline.run_standalone(online, global_pace_partners, global_bots, ghosts_enabled, save_ghost, player_update_queue, discord_webhook) +zwift_offline.run_standalone(online, global_pace_partners, global_bots, ghosts_enabled, save_ghost, player_update_queue, discord) diff --git a/zwift_offline.py b/zwift_offline.py index 51cda26..783e543 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -142,7 +142,6 @@ save_ghost = None restarting = False restarting_in_minutes = 0 reload_pacer_bots = False -discord_webhook = None class User(UserMixin, db.Model): player_id = db.Column(db.Integer, primary_key=True) @@ -463,6 +462,7 @@ def user_home(username): return render_template("user_home.html", username=current_user.username, enable_ghosts=bool(current_user.enable_ghosts), online=get_online(), is_admin=current_user.is_admin, restarting=restarting, restarting_in_minutes=restarting_in_minutes) + def send_message_to_all_online(message, sender='Server'): player_update = udp_node_msgs_pb2.PlayerUpdate() player_update.f2 = 1 @@ -488,6 +488,7 @@ def send_message_to_all_online(message, sender='Server'): player_update_queue[recieving_player_id] = list() player_update_queue[recieving_player_id].append(player_update.SerializeToString()) + def send_restarting_message(): global restarting global restarting_in_minutes @@ -498,10 +499,11 @@ def send_restarting_message(): if restarting and restarting_in_minutes == 0: message = 'See you later! Look for the back online message.' send_message_to_all_online(message) - send_message_to_discord(message) + discord.send_message(message) time.sleep(6) os.kill(os.getpid(), signal.SIGINT) + @app.route("/restart") @login_required def restart_server(): @@ -512,9 +514,10 @@ def restart_server(): restarting_in_minutes = 10 send_restarting_message_thread = threading.Thread(target=send_restarting_message) send_restarting_message_thread.start() - send_message_to_discord('Restarting / Shutting down in %s minutes. Save your progress or continue riding until server is back online' % restarting_in_minutes) + discord.send_message('Restarting / Shutting down in %s minutes. Save your progress or continue riding until server is back online' % restarting_in_minutes) return redirect('/user/%s/' % current_user.username) + @app.route("/cancelrestart") @login_required def cancel_restart_server(): @@ -525,9 +528,10 @@ def cancel_restart_server(): restarting_in_minutes = 0 message = 'Restart of the server has been cancelled. Ride on!' send_message_to_all_online(message) - send_message_to_discord(message) + discord.send_message(message) return redirect('/user/%s/' % current_user.username) + @app.route("/reloadbots") @login_required def reload_bots(): @@ -536,6 +540,7 @@ def reload_bots(): reload_pacer_bots = True return redirect('/user/%s/' % current_user.username) + @app.route("/upload//", methods=["GET", "POST"]) @login_required def upload(username): @@ -701,7 +706,7 @@ def logout(player_id): online.pop(player_id) if player_id in player_partial_profiles: player_partial_profiles.pop(player_id) - send_message_to_discord('%s riders online' % len(online)) + discord.send_message('%s riders online' % len(online)) @app.route('/api/users/logout', methods=['POST']) @@ -1063,7 +1068,7 @@ def api_profiles_activities_rideon(recieving_player_id): receiver = get_partial_profile(recieving_player_id) message = 'Ride on ' + receiver.first_name + ' ' + receiver.last_name + '!' - send_message_to_discord(message, sending_player_id) + discord.send_message(message, sending_player_id) return '{}', 200 @@ -1218,21 +1223,6 @@ def add_player_to_world(player, course_world, is_pace_partner): course_world[course_id].f5 += 1 -import requests -import json - -def send_message_to_discord(message, sender_id=None): - if not discord_webhook: return - if sender_id is not None: - profile = get_partial_profile(sender_id) - sender = profile.first_name + ' ' + profile.last_name - else: sender = 'Server' - data = {} - data["content"] = message - data["username"] = sender - requests.post(discord_webhook, data=json.dumps(data), headers={"Content-Type": "application/json"}) - - def relay_worlds_generic(world_id=None): courses = courses_lookup.keys() # Android client also requests a JSON version @@ -1299,7 +1289,7 @@ def relay_worlds_generic(world_id=None): if player_update.type == 5: chat_message = udp_node_msgs_pb2.ChatMessage() chat_message.ParseFromString(player_update.payload) - send_message_to_discord(chat_message.message, chat_message.rider_id) + discord.send_message(chat_message.message, chat_message.rider_id) return '{}', 200 else: # protobuf request worlds = world_pb2.Worlds() @@ -1585,7 +1575,7 @@ def send_server_back_online_message(): time.sleep(30) message = "We're back online. Ride on!" send_message_to_all_online(message) - send_message_to_discord(message) + discord.send_message(message) @app.before_first_request @@ -1727,14 +1717,14 @@ def auth_realms_zwift_tokens_access_codes(): return FAKE_JWT, 200 -def run_standalone(passed_online, passed_global_pace_partners, passed_global_bots, passed_ghosts_enabled, passed_save_ghost, passed_player_update_queue, passed_discord_webhook): +def run_standalone(passed_online, passed_global_pace_partners, passed_global_bots, passed_ghosts_enabled, passed_save_ghost, passed_player_update_queue, passed_discord): global online global global_pace_partners global global_bots global ghosts_enabled global save_ghost global player_update_queue - global discord_webhook + global discord global login_manager online = passed_online global_pace_partners = passed_global_pace_partners @@ -1742,7 +1732,7 @@ def run_standalone(passed_online, passed_global_pace_partners, passed_global_bot ghosts_enabled = passed_ghosts_enabled save_ghost = passed_save_ghost player_update_queue = passed_player_update_queue - discord_webhook = passed_discord_webhook + discord = passed_discord login_manager = LoginManager() login_manager.login_view = 'login' login_manager.session_protection = None