Merge branch 'discord-bot'

This commit is contained in:
zoffline
2021-01-26 20:13:54 -05:00
4 changed files with 148 additions and 15 deletions
+9
View File
@@ -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]
+87
View File
@@ -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
View File
@@ -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
View File
@@ -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)