mirror of
https://github.com/zoffline/zwift-offline.git
synced 2026-08-02 08:57:57 -07:00
Merge branch 'discord-bot'
This commit is contained in:
@@ -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: ``pip3 install discord``` and 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]
|
||||
|
||||
@@ -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()
|
||||
+18
-4
@@ -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
|
||||
@@ -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
|
||||
@@ -519,7 +520,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
|
||||
zdiscord.send_message('%s riders online' % len(online))
|
||||
|
||||
#Remove ghosts entries for inactive players (disconnected?)
|
||||
keys = global_ghosts.keys()
|
||||
@@ -640,4 +645,13 @@ 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)
|
||||
if os.path.isfile(DISCORD_CONFIG_FILE):
|
||||
from discord_bot import DiscordThread
|
||||
discord = DiscordThread(DISCORD_CONFIG_FILE)
|
||||
else:
|
||||
class DummyDiscord():
|
||||
def send_message(msg, sender_id=None):
|
||||
pass
|
||||
discord = DummyDiscord()
|
||||
|
||||
zwift_offline.run_standalone(online, global_pace_partners, global_bots, ghosts_enabled, save_ghost, player_update_queue, discord)
|
||||
|
||||
+34
-11
@@ -462,7 +462,8 @@ 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
|
||||
@@ -487,6 +488,7 @@ def send_message_to_all_online(message):
|
||||
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
|
||||
@@ -495,10 +497,13 @@ 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)
|
||||
discord.send_message(message)
|
||||
time.sleep(6)
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
|
||||
@app.route("/restart")
|
||||
@login_required
|
||||
def restart_server():
|
||||
@@ -509,8 +514,10 @@ def restart_server():
|
||||
restarting_in_minutes = 10
|
||||
send_restarting_message_thread = threading.Thread(target=send_restarting_message)
|
||||
send_restarting_message_thread.start()
|
||||
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():
|
||||
@@ -519,9 +526,12 @@ 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)
|
||||
discord.send_message(message)
|
||||
return redirect('/user/%s/' % current_user.username)
|
||||
|
||||
|
||||
@app.route("/reloadbots")
|
||||
@login_required
|
||||
def reload_bots():
|
||||
@@ -530,6 +540,7 @@ def reload_bots():
|
||||
reload_pacer_bots = True
|
||||
return redirect('/user/%s/' % current_user.username)
|
||||
|
||||
|
||||
@app.route("/upload/<username>/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def upload(username):
|
||||
@@ -695,6 +706,7 @@ def logout(player_id):
|
||||
online.pop(player_id)
|
||||
if player_id in player_partial_profiles:
|
||||
player_partial_profiles.pop(player_id)
|
||||
discord.send_message('%s riders online' % len(online))
|
||||
|
||||
|
||||
@app.route('/api/users/logout', methods=['POST'])
|
||||
@@ -1053,6 +1065,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 + '!'
|
||||
discord.send_message(message, sending_player_id)
|
||||
return '{}', 200
|
||||
|
||||
|
||||
@@ -1270,6 +1286,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)
|
||||
discord.send_message(chat_message.message, chat_message.rider_id)
|
||||
return '{}', 200
|
||||
else: # protobuf request
|
||||
worlds = world_pb2.Worlds()
|
||||
@@ -1553,7 +1573,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)
|
||||
discord.send_message(message)
|
||||
|
||||
|
||||
@app.before_first_request
|
||||
@@ -1564,8 +1586,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()
|
||||
|
||||
|
||||
####################
|
||||
@@ -1697,13 +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):
|
||||
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
|
||||
global login_manager
|
||||
online = passed_online
|
||||
global_pace_partners = passed_global_pace_partners
|
||||
@@ -1711,6 +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 = passed_discord
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'login'
|
||||
login_manager.session_protection = None
|
||||
@@ -1722,10 +1744,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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user