commit
200bee2c40
@ -1,3 +0,0 @@ |
||||
if __name__ == "__main__": |
||||
from . import minecraft_discord_bridge |
||||
minecraft_discord_bridge.main() |
@ -1,82 +0,0 @@ |
||||
from datetime import datetime |
||||
|
||||
from quarry.net.server import ServerFactory, ServerProtocol |
||||
|
||||
from .database import AccountLinkToken, MinecraftAccount, DiscordAccount |
||||
from . import database_session |
||||
|
||||
|
||||
class AuthProtocol(ServerProtocol): |
||||
def player_joined(self): |
||||
# This method gets called when a player successfully joins the server. |
||||
# If we're in online mode (the default), this means auth with the |
||||
# session server was successful and the user definitely owns the |
||||
# display name they claim to. |
||||
|
||||
# Call super. This switches us to "play" mode, marks the player as |
||||
# in-game, and does some logging. |
||||
ServerProtocol.player_joined(self) |
||||
|
||||
# Define your own logic here. It could be an HTTP request to an API, |
||||
# or perhaps an update to a database table. |
||||
display_name = self.display_name |
||||
uuid = self.uuid |
||||
|
||||
# Monkey Patch for Forge deciding to append "\x00FML\x00" to the connection string |
||||
if self.connect_host.endswith("\x00FML\x00"): |
||||
ip_addr = self.connect_host[:-5] |
||||
else: |
||||
ip_addr = self.connect_host |
||||
|
||||
connect_port = self.connect_port |
||||
|
||||
self.logger.info("[AUTH SERVER] {} ({}) connected to address {}:{}".format( |
||||
display_name, uuid, ip_addr, connect_port)) |
||||
try: |
||||
connection_token = ip_addr.split(".")[0] |
||||
session = database_session.get_session() |
||||
token = session.query(AccountLinkToken).filter_by(token=connection_token).first() |
||||
if not token: |
||||
self.close("You have connected with an invalid token!") |
||||
session.close() |
||||
return |
||||
discord_account = session.query(DiscordAccount).filter_by(link_token_id=token.id).first() |
||||
if not discord_account: |
||||
self.close("You have connected with an invalid token!") |
||||
session.close() |
||||
return |
||||
if datetime.utcnow() < token.expiry: |
||||
# Check if they already have a linked account and are re-linking |
||||
if discord_account.minecraft_account_id is not None: |
||||
existing_account = session.query(MinecraftAccount).filter_by( |
||||
id=discord_account.minecraft_account_id).first() |
||||
self.logger.info("unlinking existing {} account and replacing it with {}".format( |
||||
existing_account.minecraft_uuid, str(uuid))) |
||||
session.delete(existing_account) |
||||
mc_account = MinecraftAccount(str(uuid), discord_account.id) |
||||
discord_account.minecraft_account = mc_account |
||||
session.add(mc_account) |
||||
session.delete(token) |
||||
session.commit() |
||||
session.close() |
||||
self.close("Your minecraft account has successfully been linked to your discord account!") |
||||
return |
||||
else: |
||||
session.delete(token) |
||||
session.commit() |
||||
session.close() |
||||
self.close("You have connected with an expired token! " |
||||
"Please run the mc!register command again to get a new token.") |
||||
return |
||||
|
||||
except Exception as e: |
||||
self.logger.error(e) |
||||
session.close() |
||||
|
||||
# Kick the player. |
||||
self.close("This shouldn't happen!") |
||||
|
||||
|
||||
class AuthFactory(ServerFactory): |
||||
protocol = AuthProtocol |
||||
motd = "Auth Server" |
@ -1,18 +0,0 @@ |
||||
from sqlalchemy import create_engine |
||||
from sqlalchemy.ext.declarative import declarative_base |
||||
from sqlalchemy.orm import sessionmaker |
||||
|
||||
_engine = None |
||||
Base = declarative_base() |
||||
|
||||
|
||||
def initialize(config): |
||||
global _engine |
||||
_connection_string = config.database_connection_string |
||||
_engine = create_engine(_connection_string) |
||||
Base.metadata.create_all(_engine) |
||||
|
||||
|
||||
def get_session(): |
||||
Session = sessionmaker(bind=_engine)() |
||||
return Session |
@ -1,73 +0,0 @@ |
||||
import time |
||||
from enum import Enum |
||||
import logging |
||||
|
||||
import requests |
||||
|
||||
_username = None |
||||
_password = None |
||||
_auth = None |
||||
_url = None |
||||
|
||||
log = logging.getLogger("bridge.elasticsearch") |
||||
|
||||
|
||||
def initialize(config): |
||||
global _username, _password, _url, _auth |
||||
if config.es_auth: |
||||
_auth = True |
||||
_username = config.es_username |
||||
_password = config.es_password |
||||
_url = config.es_url |
||||
|
||||
|
||||
def log_connection(uuid, reason, count=0): |
||||
if ConnectionReason(reason).name != "SEEN": |
||||
es_payload = { |
||||
"uuid": uuid, |
||||
"time": (lambda: int(round(time.time() * 1000)))(), |
||||
"reason": ConnectionReason(reason).name, |
||||
"count": count, |
||||
} |
||||
else: |
||||
es_payload = { |
||||
"uuid": uuid, |
||||
"time": (lambda: int(round(time.time() * 1000)))(), |
||||
"reason": ConnectionReason(reason).name, |
||||
} |
||||
post_request("connections/_doc/", es_payload) |
||||
|
||||
|
||||
def log_chat_message(uuid, display_name, message, message_unformatted): |
||||
es_payload = { |
||||
"uuid": uuid, |
||||
"display_name": display_name, |
||||
"message": message, |
||||
"message_unformatted": message_unformatted, |
||||
"time": (lambda: int(round(time.time() * 1000)))(), |
||||
} |
||||
post_request("chat_messages/_doc/", es_payload) |
||||
|
||||
|
||||
def log_raw_message(type, message): |
||||
es_payload = { |
||||
"time": (lambda: int(round(time.time() * 1000)))(), |
||||
"type": type, |
||||
"message": message, |
||||
} |
||||
post_request("raw_messages/_doc/", es_payload) |
||||
|
||||
|
||||
def post_request(endpoint, payload): |
||||
the_url = "{}{}".format(_url, endpoint) |
||||
if _auth: |
||||
post = requests.post(the_url, auth=(_username, _password), json=payload) |
||||
else: |
||||
post = requests.post(the_url, json=payload) |
||||
log.debug(post.text) |
||||
|
||||
|
||||
class ConnectionReason(Enum): |
||||
CONNECTED = "CONNECTED" |
||||
DISCONNECTED = "DISCONNECTED" |
||||
SEEN = "SEEN" |
@ -1,714 +0,0 @@ |
||||
#!/usr/bin/env python |
||||
|
||||
from __future__ import print_function |
||||
|
||||
import sys |
||||
import re |
||||
from enum import Enum |
||||
|
||||
import requests |
||||
import json |
||||
import time |
||||
import logging |
||||
import random |
||||
import string |
||||
import uuid |
||||
from threading import Thread |
||||
from .config import Configuration |
||||
from .database import DiscordChannel, AccountLinkToken, DiscordAccount |
||||
from . import database_session |
||||
|
||||
from datetime import datetime, timedelta, timezone |
||||
from . import elasticsearch_logger as el |
||||
from minecraft import authentication |
||||
from minecraft.exceptions import YggdrasilError |
||||
from minecraft.networking.connection import Connection |
||||
from minecraft.networking.packets import clientbound, serverbound |
||||
|
||||
import discord |
||||
import asyncio |
||||
|
||||
from mcstatus import MinecraftServer |
||||
|
||||
from bidict import bidict |
||||
|
||||
log = logging.getLogger("bridge") |
||||
|
||||
SESSION_TOKEN = "" |
||||
UUID_CACHE = bidict() |
||||
WEBHOOKS = [] |
||||
BOT_USERNAME = "" |
||||
NEXT_MESSAGE_TIME = datetime.now(timezone.utc) |
||||
PREVIOUS_MESSAGE = "" |
||||
PLAYER_LIST = bidict() |
||||
PREVIOUS_PLAYER_LIST = bidict() |
||||
ACCEPT_JOIN_EVENTS = False |
||||
TAB_HEADER = "" |
||||
TAB_FOOTER = "" |
||||
|
||||
|
||||
def mc_uuid_to_username(uuid): |
||||
if uuid not in UUID_CACHE: |
||||
try: |
||||
short_uuid = uuid.replace("-", "") |
||||
mojang_response = requests.get("https://api.mojang.com/user/profiles/{}/names".format(short_uuid)).json() |
||||
if len(mojang_response) > 1: |
||||
# Multiple name changes |
||||
player_username = mojang_response[-1]["name"] |
||||
else: |
||||
# Only one name |
||||
player_username = mojang_response[0]["name"] |
||||
UUID_CACHE[uuid] = player_username |
||||
return player_username |
||||
except Exception as e: |
||||
log.error(e, exc_info=True) |
||||
log.error("Failed to lookup {}'s username using the Mojang API.".format(uuid)) |
||||
else: |
||||
return UUID_CACHE[uuid] |
||||
|
||||
|
||||
def mc_username_to_uuid(username): |
||||
if username not in UUID_CACHE.inv: |
||||
try: |
||||
player_uuid = requests.get( |
||||
"https://api.mojang.com/users/profiles/minecraft/{}".format(username)).json()["id"] |
||||
long_uuid = uuid.UUID(player_uuid) |
||||
UUID_CACHE.inv[username] = str(long_uuid) |
||||
return player_uuid |
||||
except requests.RequestException: |
||||
log.error("Failed to lookup {}'s UUID using the Mojang API.".format(username)) |
||||
else: |
||||
return UUID_CACHE.inv[username] |
||||
|
||||
|
||||
def get_discord_help_string(): |
||||
help_str = ("Admin commands:\n" |
||||
"`mc!chathere`: Starts outputting server messages in this channel\n" |
||||
"`mc!stopchathere`: Stops outputting server messages in this channel\n" |
||||
"User commands:\n" |
||||
"`mc!tab`: Sends you the content of the server's player/tab list\n" |
||||
"`mc!register`: Starts the minecraft account registration process\n" |
||||
"To start chatting on the minecraft server, please register your account using `mc!register`.") |
||||
return help_str |
||||
|
||||
|
||||
# https://stackoverflow.com/questions/33404752/removing-emojis-from-a-string-in-python |
||||
def remove_emoji(string): |
||||
emoji_pattern = re.compile( |
||||
"[" |
||||
u"\U0001F600-\U0001F64F" # emoticons |
||||
u"\U0001F300-\U0001F5FF" # symbols & pictographs |
||||
u"\U0001F680-\U0001F6FF" # transport & map symbols |
||||
u"\U0001F1E0-\U0001F1FF" # flags (iOS) |
||||
u"\U0001F900-\U0001FAFF" # CJK Compatibility Ideographs |
||||
# u"\U00002702-\U000027B0" |
||||
# u"\U000024C2-\U0001F251" |
||||
"]+", flags=re.UNICODE) |
||||
return emoji_pattern.sub(r'', string) |
||||
|
||||
|
||||
def escape_markdown(string): |
||||
# Absolutely needs to go first or it will replace our escaping slashes! |
||||
string = string.replace("\\", "\\\\") |
||||
string = string.replace("_", "\\_") |
||||
string = string.replace("*", "\\*") |
||||
return string |
||||
|
||||
|
||||
def strip_colour(string): |
||||
colour_pattern = re.compile( |
||||
u"\U000000A7" # selection symbol |
||||
".", flags=re.UNICODE) |
||||
return colour_pattern.sub(r'', string) |
||||
|
||||
|
||||
def setup_logging(level): |
||||
if level.lower() == "debug": |
||||
log_level = logging.DEBUG |
||||
else: |
||||
log_level = logging.INFO |
||||
log_format = "%(asctime)s:%(name)s:%(levelname)s:%(message)s" |
||||
logging.basicConfig(filename="bridge_log.log", format=log_format, level=log_level) |
||||
stdout_logger = logging.StreamHandler(sys.stdout) |
||||
stdout_logger.setFormatter(logging.Formatter(log_format)) |
||||
logging.getLogger().addHandler(stdout_logger) |
||||
|
||||
|
||||
def run_auth_server(port): |
||||
# We need to import twisted after setting up the logger because twisted hijacks our logging |
||||
# TODO: Fix this in a cleaner way |
||||
from twisted.internet import reactor |
||||
from .auth_server import AuthFactory |
||||
|
||||
# Create factory |
||||
factory = AuthFactory() |
||||
|
||||
# Listen |
||||
log.info("Starting authentication server on port {}".format(port)) |
||||
|
||||
factory.listen("", port) |
||||
try: |
||||
reactor.run(installSignalHandlers=False) |
||||
except KeyboardInterrupt: |
||||
reactor.stop() |
||||
|
||||
|
||||
def generate_random_auth_token(length): |
||||
letters = string.ascii_lowercase + string.digits + string.ascii_uppercase |
||||
return ''.join(random.choice(letters) for i in range(length)) |
||||
|
||||
|
||||
# TODO: Get rid of this when pycraft's enum becomes usable |
||||
class ChatType(Enum): |
||||
CHAT = 0 # A player-initiated chat message. |
||||
SYSTEM = 1 # The result of running a command. |
||||
GAME_INFO = 2 # Displayed above the hotbar in vanilla clients. |
||||
|
||||
|
||||
def main(): |
||||
global BOT_USERNAME |
||||
config = Configuration("config.json") |
||||
setup_logging(config.logging_level) |
||||
|
||||
database_session.initialize(config) |
||||
if config.es_enabled: |
||||
el.initialize(config) |
||||
|
||||
reactor_thread = Thread(target=run_auth_server, args=(config.auth_port,)) |
||||
reactor_thread.start() |
||||
|
||||
def handle_disconnect(): |
||||
log.info('Disconnected.') |
||||
global PLAYER_LIST, PREVIOUS_PLAYER_LIST, ACCEPT_JOIN_EVENTS |
||||
PREVIOUS_PLAYER_LIST = PLAYER_LIST.copy() |
||||
ACCEPT_JOIN_EVENTS = False |
||||
PLAYER_LIST = bidict() |
||||
if connection.connected: |
||||
log.info("Forced a disconnection because the connection is still connected.") |
||||
connection.disconnect(immediate=True) |
||||
time.sleep(15) |
||||
while not is_server_online(): |
||||
log.info('Not reconnecting to server because it appears to be offline.') |
||||
time.sleep(15) |
||||
log.info('Reconnecting.') |
||||
connection.connect() |
||||
|
||||
def handle_disconnect_packet(join_game_packet): |
||||
handle_disconnect() |
||||
|
||||
def minecraft_handle_exception(exception, exc_info): |
||||
log.error("A minecraft exception occured! {}:".format(exception), exc_info=exc_info) |
||||
handle_disconnect() |
||||
|
||||
def is_server_online(): |
||||
server = MinecraftServer.lookup("{}:{}".format(config.mc_server, config.mc_port)) |
||||
try: |
||||
status = server.status() |
||||
del status |
||||
return True |
||||
except ConnectionRefusedError: |
||||
return False |
||||
# AttributeError: 'TCPSocketConnection' object has no attribute 'socket' |
||||
# This might not be required as it happens upstream |
||||
except AttributeError: |
||||
return False |
||||
|
||||
log.debug("Checking if the server {} is online before connecting.") |
||||
|
||||
if not config.mc_online: |
||||
log.info("Connecting in offline mode...") |
||||
while not is_server_online(): |
||||
log.info('Not connecting to server because it appears to be offline.') |
||||
time.sleep(15) |
||||
BOT_USERNAME = config.mc_username |
||||
connection = Connection( |
||||
config.mc_server, config.mc_port, username=config.mc_username, |
||||
handle_exception=minecraft_handle_exception) |
||||
else: |
||||
auth_token = authentication.AuthenticationToken() |
||||
try: |
||||
auth_token.authenticate(config.mc_username, config.mc_password) |
||||
except YggdrasilError as e: |
||||
log.info(e) |
||||
sys.exit() |
||||
BOT_USERNAME = auth_token.profile.name |
||||
log.info("Logged in as %s..." % auth_token.profile.name) |
||||
while not is_server_online(): |
||||
log.info('Not connecting to server because it appears to be offline.') |
||||
time.sleep(15) |
||||
connection = Connection( |
||||
config.mc_server, config.mc_port, auth_token=auth_token, |
||||
handle_exception=minecraft_handle_exception) |
||||
|
||||
# Initialize the discord part |
||||
discord_bot = discord.Client() |
||||
|
||||
def register_handlers(connection): |
||||
connection.register_packet_listener( |
||||
handle_join_game, clientbound.play.JoinGamePacket) |
||||
|
||||
connection.register_packet_listener( |
||||
handle_chat, clientbound.play.ChatMessagePacket) |
||||
|
||||
connection.register_packet_listener( |
||||
handle_health_update, clientbound.play.UpdateHealthPacket) |
||||
|
||||
connection.register_packet_listener( |
||||
handle_disconnect_packet, clientbound.play.DisconnectPacket) |
||||
|
||||
connection.register_packet_listener( |
||||
handle_tab_list, clientbound.play.PlayerListItemPacket) |
||||
|
||||
connection.register_packet_listener( |
||||
handle_player_list_header_and_footer_update, clientbound.play.PlayerListHeaderAndFooterPacket) |
||||
|
||||
def handle_player_list_header_and_footer_update(header_footer_packet): |
||||
global TAB_FOOTER, TAB_HEADER |
||||
log.debug("Got Tablist H/F Update: header={}".format(header_footer_packet.header)) |
||||
log.debug("Got Tablist H/F Update: footer={}".format(header_footer_packet.footer)) |
||||
TAB_HEADER = json.loads(header_footer_packet.header)["text"] |
||||
TAB_FOOTER = json.loads(header_footer_packet.footer)["text"] |
||||
|
||||
def handle_tab_list(tab_list_packet): |
||||
global ACCEPT_JOIN_EVENTS |
||||
log.debug("Processing tab list packet") |
||||
for action in tab_list_packet.actions: |
||||
if isinstance(action, clientbound.play.PlayerListItemPacket.AddPlayerAction): |
||||
log.debug( |
||||
"Processing AddPlayerAction tab list packet, name: {}, uuid: {}".format(action.name, action.uuid)) |
||||
username = action.name |
||||
player_uuid = action.uuid |
||||
if action.name not in PLAYER_LIST.inv: |
||||
PLAYER_LIST.inv[action.name] = action.uuid |
||||
else: |
||||
# Sometimes we get a duplicate add packet on join idk why |
||||
return |
||||
if action.name not in UUID_CACHE.inv: |
||||
UUID_CACHE.inv[action.name] = action.uuid |
||||
# Initial tablist backfill |
||||
if ACCEPT_JOIN_EVENTS: |
||||
webhook_payload = { |
||||
'username': username, |
||||
'avatar_url': "https://visage.surgeplay.com/face/160/{}".format(player_uuid), |
||||
'content': '', |
||||
'embeds': [{'color': 65280, 'title': '**Joined the game**'}] |
||||
} |
||||
for webhook in WEBHOOKS: |
||||
requests.post(webhook, json=webhook_payload) |
||||
if config.es_enabled: |
||||
el.log_connection( |
||||
uuid=action.uuid, reason=el.ConnectionReason.CONNECTED, count=len(PLAYER_LIST)) |
||||
return |
||||
else: |
||||
# The bot's name is sent last after the initial back-fill |
||||
if action.name == BOT_USERNAME: |
||||
ACCEPT_JOIN_EVENTS = True |
||||
if config.es_enabled: |
||||
diff = set(PREVIOUS_PLAYER_LIST.keys()) - set(PLAYER_LIST.keys()) |
||||
for idx, player_uuid in enumerate(diff): |
||||
el.log_connection(uuid=player_uuid, reason=el.ConnectionReason.DISCONNECTED, |
||||
count=len(PREVIOUS_PLAYER_LIST) - (idx + 1)) |
||||
# Don't bother announcing the bot's own join message (who cares) but log it for analytics still |
||||
if config.es_enabled: |
||||
el.log_connection( |
||||
uuid=action.uuid, reason=el.ConnectionReason.CONNECTED, count=len(PLAYER_LIST)) |
||||
|
||||
if config.es_enabled: |
||||
el.log_connection(uuid=action.uuid, reason=el.ConnectionReason.SEEN) |
||||
if isinstance(action, clientbound.play.PlayerListItemPacket.RemovePlayerAction): |
||||
log.debug("Processing RemovePlayerAction tab list packet, uuid: {}".format(action.uuid)) |
||||
username = mc_uuid_to_username(action.uuid) |
||||
player_uuid = action.uuid |
||||
webhook_payload = { |
||||
'username': username, |
||||
'avatar_url': "https://visage.surgeplay.com/face/160/{}".format(player_uuid), |
||||
'content': '', |
||||
'embeds': [{'color': 16711680, 'title': '**Left the game**'}] |
||||
} |
||||
for webhook in WEBHOOKS: |
||||
requests.post(webhook, json=webhook_payload) |
||||
del UUID_CACHE[action.uuid] |
||||
del PLAYER_LIST[action.uuid] |
||||
if config.es_enabled: |
||||
el.log_connection(uuid=action.uuid, reason=el.ConnectionReason.DISCONNECTED, count=len(PLAYER_LIST)) |
||||
|
||||
def handle_join_game(join_game_packet): |
||||
global PLAYER_LIST |
||||
log.info('Connected.') |
||||
PLAYER_LIST = bidict() |
||||
|
||||
def handle_chat(chat_packet): |
||||
json_data = json.loads(chat_packet.json_data) |
||||
if "extra" not in json_data: |
||||
return |
||||
chat_string = "" |
||||
for chat_component in json_data["extra"]: |
||||
chat_string += chat_component["text"] |
||||
|
||||
# Handle chat message |
||||
regexp_match = re.match("<(.*?)> (.*)", chat_string, re.M | re.I) |
||||
if regexp_match: |
||||
username = regexp_match.group(1) |
||||
original_message = regexp_match.group(2) |
||||
player_uuid = mc_username_to_uuid(username) |
||||
if username.lower() == BOT_USERNAME.lower(): |
||||
# Don't relay our own messages |
||||
if config.es_enabled: |
||||
bot_message_match = re.match("<{}> (.*?): (.*)".format( |
||||
BOT_USERNAME.lower()), chat_string, re.M | re.I) |
||||
if bot_message_match: |
||||
el.log_chat_message( |
||||
uuid=mc_username_to_uuid(bot_message_match.group(1)), |
||||
display_name=bot_message_match.group(1), |
||||
message=bot_message_match.group(2), |
||||
message_unformatted=chat_string) |
||||
el.log_raw_message(type=ChatType(chat_packet.position).name, message=chat_packet.json_data) |
||||
return |
||||
log.info("Incoming message from minecraft: Username: {} Message: {}".format(username, original_message)) |
||||
log.debug("msg: {}".format(repr(original_message))) |
||||
message = escape_markdown(remove_emoji(original_message.strip().replace("@", "@\N{zero width space}"))) |
||||
webhook_payload = { |
||||
'username': username, |
||||
'avatar_url': "https://visage.surgeplay.com/face/160/{}".format(player_uuid), |
||||
'content': '{}'.format(message) |
||||
} |
||||
for webhook in WEBHOOKS: |
||||
requests.post(webhook, json=webhook_payload) |
||||
if config.es_enabled: |
||||
el.log_chat_message( |
||||
uuid=player_uuid, display_name=username, message=original_message, message_unformatted=chat_string) |
||||
if config.es_enabled: |
||||
el.log_raw_message(type=ChatType(chat_packet.position).name, message=chat_packet.json_data) |
||||
|
||||
def handle_health_update(health_update_packet): |
||||
if health_update_packet.health <= 0: |
||||
log.debug("Respawned the player because it died") |
||||
packet = serverbound.play.ClientStatusPacket() |
||||
packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN |
||||
connection.write_packet(packet) |
||||
|
||||
register_handlers(connection) |
||||
|
||||
connection.connect() |
||||
|
||||
@discord_bot.event |
||||
async def on_ready(): |
||||
log.info("Discord bot logged in as {} ({})".format(discord_bot.user.name, discord_bot.user.id)) |
||||
global WEBHOOKS |
||||
WEBHOOKS = [] |
||||
session = database_session.get_session() |
||||
channels = session.query(DiscordChannel).all() |
||||
session.close() |
||||
for channel in channels: |
||||
channel_id = channel.channel_id |
||||
discord_channel = discord_bot.get_channel(channel_id) |
||||
channel_webhooks = await discord_channel.webhooks() |
||||
found = False |
||||
for webhook in channel_webhooks: |
||||
if webhook.name == "_minecraft" and webhook.user == discord_bot.user: |
||||
WEBHOOKS.append(webhook.url) |
||||
found = True |
||||
log.debug("Found webhook {} in channel {}".format(webhook.name, discord_channel.name)) |
||||
if not found: |
||||
# Create the hook |
||||
await discord_channel.create_webhook(name="_minecraft") |
||||
|
||||
@discord_bot.event |
||||
async def on_message(message): |
||||
# We do not want the bot to reply to itself |
||||
if message.author == discord_bot.user: |
||||
return |
||||
this_channel = message.channel.id |
||||
global WEBHOOKS |
||||
|
||||
# PM Commands |
||||
if message.content.startswith("mc!help"): |
||||
try: |
||||
send_channel = message.channel |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
await message.delete() |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
msg = get_discord_help_string() |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
finally: |
||||
return |
||||
|
||||
elif message.content.startswith("mc!register"): |
||||
try: |
||||
# TODO: Catch the Forbidden error in a smart way before running application logic |
||||
send_channel = message.channel |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
await message.delete() |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
session = database_session.get_session() |
||||
discord_account = session.query(DiscordAccount).filter_by(discord_id=message.author.id).first() |
||||
if not discord_account: |
||||
new_discord_account = DiscordAccount(message.author.id) |
||||
session.add(new_discord_account) |
||||
session.commit() |
||||
discord_account = session.query(DiscordAccount).filter_by(discord_id=message.author.id).first() |
||||
|
||||
new_token = generate_random_auth_token(16) |
||||
account_link_token = AccountLinkToken(message.author.id, new_token) |
||||
discord_account.link_token = account_link_token |
||||
session.add(account_link_token) |
||||
session.commit() |
||||
msg = "Please connect your minecraft account to `{}.{}:{}` in order to link it to this bridge!"\ |
||||
.format(new_token, config.auth_dns, config.auth_port) |
||||
session.close() |
||||
del session |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
finally: |
||||
return |
||||
|
||||
# Global Commands |
||||
elif message.content.startswith("mc!chathere"): |
||||
if isinstance(message.channel, discord.abc.PrivateChannel): |
||||
msg = "Sorry, this command is only available in public channels." |
||||
await message.channel.send(msg) |
||||
return |
||||
if message.author.id not in config.admin_users: |
||||
await message.delete() |
||||
try: |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
dm_channel = message.author.dm_channel |
||||
msg = "Sorry, you do not have permission to execute that command!" |
||||
await dm_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
finally: |
||||
return |
||||
session = database_session.get_session() |
||||
channels = session.query(DiscordChannel).filter_by(channel_id=this_channel).all() |
||||
if not channels: |
||||
new_channel = DiscordChannel(this_channel) |
||||
session.add(new_channel) |
||||
session.commit() |
||||
session.close() |
||||
del session |
||||
webhook = await message.channel.create_webhook(name="_minecraft") |
||||
WEBHOOKS.append(webhook.url) |
||||
msg = "The bot will now start chatting here! To stop this, run `mc!stopchathere`." |
||||
await message.channel.send(msg) |
||||
else: |
||||
msg = "The bot is already chatting in this channel! To stop this, run `mc!stopchathere`." |
||||
await message.channel.send(msg) |
||||
return |
||||
|
||||
elif message.content.startswith("mc!stopchathere"): |
||||
if isinstance(message.channel, discord.abc.PrivateChannel): |
||||
msg = "Sorry, this command is only available in public channels." |
||||
await message.channel.send(msg) |
||||
return |
||||
if message.author.id not in config.admin_users: |
||||
await message.delete() |
||||
try: |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
dm_channel = message.author.dm_channel |
||||
msg = "Sorry, you do not have permission to execute that command!" |
||||
await dm_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
finally: |
||||
return |
||||
session = database_session.get_session() |
||||
deleted = session.query(DiscordChannel).filter_by(channel_id=this_channel).delete() |
||||
session.commit() |
||||
session.close() |
||||
for webhook in await message.channel.webhooks(): |
||||
if webhook.name == "_minecraft" and webhook.user == discord_bot.user: |
||||
# Copy the list to avoid some problems since |
||||
# we're deleting indicies form it as we loop |
||||
# through it |
||||
if webhook.url in WEBHOOKS[:]: |
||||
WEBHOOKS.remove(webhook.url) |
||||
await webhook.delete() |
||||
if deleted < 1: |
||||
msg = "The bot was not chatting here!" |
||||
await message.channel.send(msg) |
||||
return |
||||
else: |
||||
msg = "The bot will no longer chat here!" |
||||
await message.channel.send(msg) |
||||
return |
||||
|
||||
elif message.content.startswith("mc!tab"): |
||||
send_channel = message.channel |
||||
try: |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
await message.delete() |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
player_list = ", ".join(list(map(lambda x: x[1], PLAYER_LIST.items()))) |
||||
msg = "{}\n" \ |
||||
"Players online: {}\n" \ |
||||
"{}".format(escape_markdown( |
||||
strip_colour(TAB_HEADER)), escape_markdown( |
||||
strip_colour(player_list)), escape_markdown( |
||||
strip_colour(TAB_FOOTER))) |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
finally: |
||||
return |
||||
|
||||
elif message.content.startswith("mc!"): |
||||
# Catch-all |
||||
send_channel = message.channel |
||||
try: |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
await message.delete() |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
msg = "Unknown command, type `mc!help` for a list of commands." |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
finally: |
||||
return |
||||
|
||||
elif not message.author.bot: |
||||
session = database_session.get_session() |
||||
channel_should_chat = session.query(DiscordChannel).filter_by(channel_id=this_channel).first() |
||||
if channel_should_chat: |
||||
await message.delete() |
||||
discord_user = session.query(DiscordAccount).filter_by(discord_id=message.author.id).first() |
||||
if discord_user: |
||||
if discord_user.minecraft_account: |
||||
minecraft_uuid = discord_user.minecraft_account.minecraft_uuid |
||||
session.close() |
||||
del session |
||||
minecraft_username = mc_uuid_to_username(minecraft_uuid) |
||||
|
||||
# Max chat message length: 256, bot username does not count towards this |
||||
# Does not count|Counts |
||||
# <BOT_USERNAME> minecraft_username: message |
||||
padding = 2 + len(minecraft_username) |
||||
|
||||
message_to_send = remove_emoji( |
||||
message.clean_content.encode('utf-8').decode('ascii', 'replace')).strip() |
||||
message_to_discord = escape_markdown(message.clean_content) |
||||
|
||||
total_len = padding + len(message_to_send) |
||||
if total_len > 256: |
||||
message_to_send = message_to_send[:(256 - padding)] |
||||
message_to_discord = message_to_discord[:(256 - padding)] |
||||
elif len(message_to_send) <= 0: |
||||
return |
||||
|
||||
session = database_session.get_session() |
||||
channels = session.query(DiscordChannel).all() |
||||
session.close() |
||||
del session |
||||
global PREVIOUS_MESSAGE, NEXT_MESSAGE_TIME |
||||
if message_to_send == PREVIOUS_MESSAGE or \ |
||||
datetime.now(timezone.utc) < NEXT_MESSAGE_TIME: |
||||
send_channel = message.channel |
||||
try: |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
msg = "Your message \"{}\" has been rate-limited.".format(message.clean_content) |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format( |
||||
message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
finally: |
||||
return |
||||
|
||||
PREVIOUS_MESSAGE = message_to_send |
||||
NEXT_MESSAGE_TIME = datetime.now(timezone.utc) + timedelta(seconds=config.message_delay) |
||||
|
||||
log.info("Outgoing message from discord: Username: {} Message: {}".format( |
||||
minecraft_username, message_to_send)) |
||||
|
||||
for channel in channels: |
||||
webhooks = await discord_bot.get_channel(channel.channel_id).webhooks() |
||||
for webhook in webhooks: |
||||
if webhook.name == "_minecraft": |
||||
await webhook.send( |
||||
username=minecraft_username, |
||||
avatar_url="https://visage.surgeplay.com/face/160/{}".format(minecraft_uuid), |
||||
content=message_to_discord) |
||||
|
||||
packet = serverbound.play.ChatPacket() |
||||
packet.message = "{}: {}".format(minecraft_username, message_to_send) |
||||
connection.write_packet(packet) |
||||
else: |
||||
send_channel = message.channel |
||||
try: |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
msg = "Unable to send chat message: there is no Minecraft account linked to this discord " \ |
||||
"account, please run `mc!register`." |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
finally: |
||||
session.close() |
||||
del session |
||||
return |
||||
else: |
||||
session.close() |
||||
del session |
||||
|
||||
discord_bot.run(config.discord_token) |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
@ -0,0 +1 @@ |
||||
__version__ = "0.0.1" |
@ -0,0 +1,3 @@ |
||||
if __name__ == "__main__": |
||||
from minecraft_discord_bridge import minecraft_discord_bridge |
||||
minecraft_discord_bridge.main() |
@ -0,0 +1,79 @@ |
||||
from datetime import datetime |
||||
|
||||
from quarry.net.server import ServerFactory, ServerProtocol |
||||
|
||||
from .database import AccountLinkToken, MinecraftAccount, DiscordAccount |
||||
|
||||
DATABASE_SESSION = None |
||||
|
||||
|
||||
class AuthProtocol(ServerProtocol): |
||||
def player_joined(self): |
||||
# This method gets called when a player successfully joins the server. |
||||
# If we're in online mode (the default), this means auth with the |
||||
# session server was successful and the user definitely owns the |
||||
# display name they claim to. |
||||
|
||||
# Call super. This switches us to "play" mode, marks the player as |
||||
# in-game, and does some logging. |
||||
ServerProtocol.player_joined(self) |
||||
|
||||
# Define your own logic here. It could be an HTTP request to an API, |
||||
# or perhaps an update to a database table. |
||||
display_name = self.display_name |
||||
uuid = self.uuid |
||||
|
||||
# Monkey Patch for Forge deciding to append "\x00FML\x00" to the connection string |
||||
if self.connect_host.endswith("\x00FML\x00"): |
||||
ip_addr = self.connect_host[:-5] |
||||
else: |
||||
ip_addr = self.connect_host |
||||
|
||||
connect_port = self.connect_port |
||||
|
||||
self.logger.info("[AUTH SERVER] %s (%s) connected to address %s:%s", |
||||
display_name, uuid, ip_addr, connect_port) |
||||
|
||||
connection_token = ip_addr.split(".")[0] |
||||
session = DATABASE_SESSION.get_session() |
||||
token = session.query(AccountLinkToken).filter_by(token=connection_token).first() |
||||
if not token: |
||||
self.close("You have connected with an invalid token!") |
||||
session.close() |
||||
return |
||||
discord_account = session.query(DiscordAccount).filter_by(link_token_id=token.id).first() |
||||
if not discord_account: |
||||
self.close("You have connected with an invalid token!") |
||||
session.close() |
||||
return |
||||
if datetime.utcnow() < token.expiry: |
||||
# Check if they already have a linked account and are re-linking |
||||
if discord_account.minecraft_account_id is not None: |
||||
existing_account = session.query(MinecraftAccount).filter_by( |
||||
id=discord_account.minecraft_account_id).first() |
||||
self.logger.info("unlinking existing %s account and replacing it with %s", |
||||
existing_account.minecraft_uuid, str(uuid)) |
||||
session.delete(existing_account) |
||||
mc_account = MinecraftAccount(str(uuid), discord_account.id) |
||||
discord_account.minecraft_account = mc_account |
||||
session.add(mc_account) |
||||
session.delete(token) |
||||
session.commit() |
||||
session.close() |
||||
self.close("Your minecraft account has successfully been linked to your discord account!") |
||||
return |
||||
else: |
||||
session.delete(token) |
||||
session.commit() |
||||
session.close() |
||||
self.close("You have connected with an expired token! " |
||||
"Please run the mc!register command again to get a new token.") |
||||
return |
||||
|
||||
# Kick the player. |
||||
self.close("This shouldn't happen!") |
||||
|
||||
|
||||
class AuthFactory(ServerFactory): |
||||
protocol = AuthProtocol |
||||
motd = "Auth Server" |
@ -0,0 +1,20 @@ |
||||
from sqlalchemy import create_engine |
||||
from sqlalchemy.ext.declarative import declarative_base |
||||
from sqlalchemy.orm import sessionmaker |
||||
|
||||
Base = declarative_base() |
||||
|
||||
|
||||
class DatabaseSession(): |
||||
def __init__(self): |
||||
self._engine = None |
||||
self.connection_string = None |
||||
|
||||
def initialize(self, config): |
||||
self.connection_string = config.database_connection_string |
||||
self._engine = create_engine(self.connection_string) |
||||
Base.metadata.create_all(self._engine) |
||||
|
||||
def get_session(self): |
||||
session = sessionmaker(bind=self._engine)() |
||||
return session |
@ -0,0 +1,61 @@ |
||||
import time |
||||
from enum import Enum |
||||
import logging |
||||
|
||||
import requests |
||||
|
||||
|
||||
class ElasticsearchLogger(): |
||||
def __init__(self, url: str, username: str = "", password: str = ""): |
||||
self.url = url |
||||
self.username = username |
||||
self.password = password |
||||
self.log = logging.getLogger("bridge.elasticsearch") |
||||
|
||||
def log_connection(self, uuid, reason, count=0): |
||||
if ConnectionReason(reason).name != "SEEN": |
||||
es_payload = { |
||||
"uuid": uuid, |
||||
"time": (lambda: int(round(time.time() * 1000)))(), |
||||
"reason": ConnectionReason(reason).name, |
||||
"count": count, |
||||
} |
||||
else: |
||||
es_payload = { |
||||
"uuid": uuid, |
||||
"time": (lambda: int(round(time.time() * 1000)))(), |
||||
"reason": ConnectionReason(reason).name, |
||||
} |
||||
self.post_request("connections/_doc/", es_payload) |
||||
|
||||
def log_chat_message(self, uuid, display_name, message, message_unformatted): |
||||
es_payload = { |
||||
"uuid": uuid, |
||||
"display_name": display_name, |
||||
"message": message, |
||||
"message_unformatted": message_unformatted, |
||||
"time": (lambda: int(round(time.time() * 1000)))(), |
||||
} |
||||
self.post_request("chat_messages/_doc/", es_payload) |
||||
|
||||
def log_raw_message(self, msg_type, message): |
||||
es_payload = { |
||||
"time": (lambda: int(round(time.time() * 1000)))(), |
||||
"type": msg_type, |
||||
"message": message, |
||||
} |
||||
self.post_request("raw_messages/_doc/", es_payload) |
||||
|
||||
def post_request(self, endpoint, payload): |
||||
the_url = "{}{}".format(self.url, endpoint) |
||||
if self.username and self.password: |
||||
post = requests.post(the_url, auth=(self.username, self.password), json=payload) |
||||
else: |
||||
post = requests.post(the_url, json=payload) |
||||
self.log.debug(post.text) |
||||
|
||||
|
||||
class ConnectionReason(Enum): |
||||
CONNECTED = "CONNECTED" |
||||
DISCONNECTED = "DISCONNECTED" |
||||
SEEN = "SEEN" |
@ -0,0 +1,700 @@ |
||||
#!/usr/bin/env python |
||||
|
||||
from __future__ import print_function |
||||
|
||||
import sys |
||||
import re |
||||
import json |
||||
import time |
||||
import logging |
||||
import random |
||||
import string |
||||
import uuid |
||||
import asyncio |
||||
from threading import Thread |
||||
from datetime import datetime, timedelta, timezone |
||||
|
||||
import requests |
||||
from minecraft import authentication |
||||
from minecraft.exceptions import YggdrasilError |
||||
from minecraft.networking.connection import Connection |
||||
from minecraft.networking.packets import clientbound, serverbound |
||||
import discord |
||||
from mcstatus import MinecraftServer |
||||
from bidict import bidict |
||||
|
||||
from .database_session import DatabaseSession |
||||
from .elasticsearch_logger import ElasticsearchLogger, ConnectionReason |
||||
from .config import Configuration |
||||
from .database import DiscordChannel, AccountLinkToken, DiscordAccount |
||||
|
||||
|
||||
class MinecraftDiscordBridge(): |
||||
def __init__(self): |
||||
self.session_token = "" |
||||
self.uuid_cache = bidict() |
||||
self.webhooks = [] |
||||
self.bot_username = "" |
||||
self.next_message_time = datetime.now(timezone.utc) |
||||
self.previous_message = "" |
||||
self.player_list = bidict() |
||||
self.previous_player_list = bidict() |
||||
self.accept_join_events = False |
||||
self.tab_header = "" |
||||
self.tab_footer = "" |
||||
# Initialize the discord part |
||||
self.discord_bot = discord.Client() |
||||
self.config = Configuration("config.json") |
||||
self.auth_token = None |
||||
self.connection = None |
||||
self.setup_logging(self.config.logging_level) |
||||
self.database_session = DatabaseSession() |
||||
self.logger = logging.getLogger("bridge") |
||||
self.database_session.initialize(self.config) |
||||
# We need to import twisted after setting up the logger because twisted hijacks our logging |
||||
from . import auth_server |
||||
auth_server.DATABASE_SESSION = self.database_session |
||||
if self.config.es_enabled: |
||||
if self.config.es_auth: |
||||
self.es_logger = ElasticsearchLogger( |
||||
self.config.es_url, self.config.es_username, self.config.es_password) |
||||
else: |
||||
self.es_logger = ElasticsearchLogger(self.config.es_url) |
||||
|
||||
@self.discord_bot.event |
||||
async def on_ready(): # pylint: disable=W0612 |
||||
self.logger.info("Discord bot logged in as %s (%s)", self.discord_bot.user.name, self.discord_bot.user.id) |
||||
self.webhooks = [] |
||||
session = self.database_session.get_session() |
||||
channels = session.query(DiscordChannel).all() |
||||
session.close() |
||||
for channel in channels: |
||||
channel_id = channel.channel_id |
||||
discord_channel = self.discord_bot.get_channel(channel_id) |
||||
channel_webhooks = await discord_channel.webhooks() |
||||
found = False |
||||
for webhook in channel_webhooks: |
||||
if webhook.name == "_minecraft" and webhook.user == self.discord_bot.user: |
||||
self.webhooks.append(webhook.url) |
||||
found = True |
||||
self.logger.debug("Found webhook %s in channel %s", webhook.name, discord_channel.name) |
||||
if not found: |
||||
# Create the hook |
||||
await discord_channel.create_webhook(name="_minecraft") |
||||
|
||||
@self.discord_bot.event |
||||
async def on_message(message): # pylint: disable=W0612 |
||||
# We do not want the bot to reply to itself |
||||
if message.author == self.discord_bot.user: |
||||
return |
||||
this_channel = message.channel.id |
||||
|
||||
# PM Commands |
||||
if message.content.startswith("mc!help"): |
||||
try: |
||||
send_channel = message.channel |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
await message.delete() |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
msg = self.get_discord_help_string() |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
return |
||||
|
||||
elif message.content.startswith("mc!register"): |
||||
try: |
||||
send_channel = message.channel |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
await message.delete() |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
session = self.database_session.get_session() |
||||
discord_account = session.query(DiscordAccount).filter_by(discord_id=message.author.id).first() |
||||
if not discord_account: |
||||
new_discord_account = DiscordAccount(message.author.id) |
||||
session.add(new_discord_account) |
||||
session.commit() |
||||
discord_account = session.query(DiscordAccount).filter_by(discord_id=message.author.id).first() |
||||
|
||||
new_token = self.generate_random_auth_token(16) |
||||
account_link_token = AccountLinkToken(message.author.id, new_token) |
||||
discord_account.link_token = account_link_token |
||||
session.add(account_link_token) |
||||
session.commit() |
||||
msg = "Please connect your minecraft account to `{}.{}:{}` in order to link it to this bridge!"\ |
||||
.format(new_token, self.config.auth_dns, self.config.auth_port) |
||||
session.close() |
||||
del session |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
return |
||||
|
||||
# Global Commands |
||||
elif message.content.startswith("mc!chathere"): |
||||
if isinstance(message.channel, discord.abc.PrivateChannel): |
||||
msg = "Sorry, this command is only available in public channels." |
||||
await message.channel.send(msg) |
||||
return |
||||
if message.author.id not in self.config.admin_users: |
||||
await message.delete() |
||||
try: |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
dm_channel = message.author.dm_channel |
||||
msg = "Sorry, you do not have permission to execute that command!" |
||||
await dm_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
return |
||||
session = self.database_session.get_session() |
||||
channels = session.query(DiscordChannel).filter_by(channel_id=this_channel).all() |
||||
if not channels: |
||||
new_channel = DiscordChannel(this_channel) |
||||
session.add(new_channel) |
||||
session.commit() |
||||
session.close() |
||||
del session |
||||
webhook = await message.channel.create_webhook(name="_minecraft") |
||||
self.webhooks.append(webhook.url) |
||||
msg = "The bot will now start chatting here! To stop this, run `mc!stopchathere`." |
||||
await message.channel.send(msg) |
||||
else: |
||||
msg = "The bot is already chatting in this channel! To stop this, run `mc!stopchathere`." |
||||
await message.channel.send(msg) |
||||
return |
||||
|
||||
elif message.content.startswith("mc!stopchathere"): |
||||
if isinstance(message.channel, discord.abc.PrivateChannel): |
||||
msg = "Sorry, this command is only available in public channels." |
||||
await message.channel.send(msg) |
||||
return |
||||
if message.author.id not in self.config.admin_users: |
||||
await message.delete() |
||||
try: |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
dm_channel = message.author.dm_channel |
||||
msg = "Sorry, you do not have permission to execute that command!" |
||||
await dm_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
return |
||||
session = self.database_session.get_session() |
||||
deleted = session.query(DiscordChannel).filter_by(channel_id=this_channel).delete() |
||||
session.commit() |
||||
session.close() |
||||
for webhook in await message.channel.webhooks(): |
||||
if webhook.name == "_minecraft" and webhook.user == self.discord_bot.user: |
||||
# Copy the list to avoid some problems since |
||||
# we're deleting indicies form it as we loop |
||||
# through it |
||||
if webhook.url in self.webhooks[:]: |
||||
self.webhooks.remove(webhook.url) |
||||
await webhook.delete() |
||||
if deleted < 1: |
||||
msg = "The bot was not chatting here!" |
||||
await message.channel.send(msg) |
||||
return |
||||
else: |
||||
msg = "The bot will no longer chat here!" |
||||
await message.channel.send(msg) |
||||
return |
||||
|
||||
elif message.content.startswith("mc!tab"): |
||||
send_channel = message.channel |
||||
try: |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
await message.delete() |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
player_list = ", ".join(list(map(lambda x: x[1], self.player_list.items()))) |
||||
msg = "{}\n" \ |
||||
"Players online: {}\n" \ |
||||
"{}".format(self.escape_markdown( |
||||
self.strip_colour(self.tab_header)), self.escape_markdown( |
||||
self.strip_colour(player_list)), self.escape_markdown( |
||||
self.strip_colour(self.tab_footer))) |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
return |
||||
|
||||
elif message.content.startswith("mc!"): |
||||
# Catch-all |
||||
send_channel = message.channel |
||||
try: |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
await message.delete() |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
msg = "Unknown command, type `mc!help` for a list of commands." |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
return |
||||
|
||||
elif not message.author.bot: |
||||
session = self.database_session.get_session() |
||||
channel_should_chat = session.query(DiscordChannel).filter_by(channel_id=this_channel).first() |
||||
if channel_should_chat: |
||||
await message.delete() |
||||
discord_user = session.query(DiscordAccount).filter_by(discord_id=message.author.id).first() |
||||
if discord_user: |
||||
if discord_user.minecraft_account: |
||||
minecraft_uuid = discord_user.minecraft_account.minecraft_uuid |
||||
session.close() |
||||
del session |
||||
minecraft_username = self.mc_uuid_to_username(minecraft_uuid) |
||||
|
||||
# Max chat message length: 256, bot username does not count towards this |
||||
# Does not count|Counts |
||||
# <BOT_USERNAME> minecraft_username: message |
||||
padding = 2 + len(minecraft_username) |
||||
|
||||
message_to_send = self.remove_emoji( |
||||
message.clean_content.encode('utf-8').decode('ascii', 'replace')).strip() |
||||
message_to_discord = self.escape_markdown(message.clean_content) |
||||
|
||||
total_len = padding + len(message_to_send) |
||||
if total_len > 256: |
||||
message_to_send = message_to_send[:(256 - padding)] |
||||
message_to_discord = message_to_discord[:(256 - padding)] |
||||
elif not message_to_send: |
||||
return |
||||
|
||||
session = self.database_session.get_session() |
||||
channels = session.query(DiscordChannel).all() |
||||
session.close() |
||||
del session |
||||
if message_to_send == self.previous_message or \ |
||||
datetime.now(timezone.utc) < self.next_message_time: |
||||
send_channel = message.channel |
||||
try: |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
msg = "Your message \"{}\" has been rate-limited.".format(message.clean_content) |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format( |
||||
message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
return |
||||
|
||||
self.previous_message = message_to_send |
||||
self.next_message_time = datetime.now(timezone.utc) + timedelta( |
||||
seconds=self.config.message_delay) |
||||
|
||||
self.logger.info("Outgoing message from discord: Username: %s Message: %s", |
||||
minecraft_username, message_to_send) |
||||
|
||||
for channel in channels: |
||||
webhooks = await self.discord_bot.get_channel(channel.channel_id).webhooks() |
||||
for webhook in webhooks: |
||||
if webhook.name == "_minecraft": |
||||
await webhook.send( |
||||
username=minecraft_username, |
||||
avatar_url="https://visage.surgeplay.com/face/160/{}".format( |
||||
minecraft_uuid), |
||||
content=message_to_discord) |
||||
|
||||
packet = serverbound.play.ChatPacket() |
||||
packet.message = "{}: {}".format(minecraft_username, message_to_send) |
||||
self.connection.write_packet(packet) |
||||
else: |
||||
send_channel = message.channel |
||||
try: |
||||
if isinstance(message.channel, discord.abc.GuildChannel): |
||||
dm_channel = message.author.dm_channel |
||||
if not dm_channel: |
||||
await message.author.create_dm() |
||||
send_channel = message.author.dm_channel |
||||
msg = "Unable to send chat message: there is no Minecraft account linked to this discord " \ |
||||
"account, please run `mc!register`." |
||||
await send_channel.send(msg) |
||||
except discord.errors.Forbidden: |
||||
if isinstance(message.author, discord.abc.User): |
||||
msg = "{}, please allow private messages from this bot.".format(message.author.mention) |
||||
error_msg = await message.channel.send(msg) |
||||
await asyncio.sleep(3) |
||||
await error_msg.delete() |
||||
return |
||||
finally: |
||||
session.close() |
||||
del session |
||||
else: |
||||
session.close() |
||||
del session |
||||
|
||||
def run(self): |
||||
reactor_thread = Thread(target=self.run_auth_server, args=(self.config.auth_port,)) |
||||
reactor_thread.start() |
||||
|
||||
self.logger.debug("Checking if the server {} is online before connecting.") |
||||
|
||||
if not self.config.mc_online: |
||||
self.logger.info("Connecting in offline mode...") |
||||
while not self.is_server_online(): |
||||
self.logger.info('Not connecting to server because it appears to be offline.') |
||||
time.sleep(15) |
||||
self.bot_username = self.config.mc_username |
||||
self.connection = Connection( |
||||
self.config.mc_server, self.config.mc_port, username=self.config.mc_username, |
||||
handle_exception=self.minecraft_handle_exception) |
||||
else: |
||||
self.auth_token = authentication.AuthenticationToken() |
||||
try: |
||||
self.auth_token.authenticate(self.config.mc_username, self.config.mc_password) |
||||
except YggdrasilError as ex: |
||||
self.logger.info(ex) |
||||
sys.exit() |
||||
self.bot_username = self.auth_token.profile.name |
||||
self.logger.info("Logged in as %s...", self.auth_token.profile.name) |
||||
while not self.is_server_online(): |
||||
self.logger.info('Not connecting to server because it appears to be offline.') |
||||
time.sleep(15) |
||||
self.connection = Connection( |
||||
self.config.mc_server, self.config.mc_port, auth_token=self.auth_token, |
||||
handle_exception=self.minecraft_handle_exception) |
||||
|
||||
self.register_handlers(self.connection) |
||||
self.connection.connect() |
||||
self.discord_bot.run(self.config.discord_token) |
||||
|
||||
def mc_uuid_to_username(self, mc_uuid: str): |
||||
if mc_uuid not in self.uuid_cache: |
||||
try: |
||||
short_uuid = mc_uuid.replace("-", "") |
||||
mojang_response = requests.get("https://api.mojang.com/user/profiles/{}/names".format( |
||||
short_uuid)).json() |
||||
if len(mojang_response) > 1: |
||||
# Multiple name changes |
||||
player_username = mojang_response[-1]["name"] |
||||
else: |
||||
# Only one name |
||||
player_username = mojang_response[0]["name"] |
||||
self.uuid_cache[mc_uuid] = player_username |
||||
return player_username |
||||
except requests.RequestException as ex: |
||||
self.logger.error(ex, exc_info=True) |
||||
self.logger.error("Failed to lookup %s's username using the Mojang API.", mc_uuid) |
||||
else: |
||||
return self.uuid_cache[mc_uuid] |
||||
|
||||
def mc_username_to_uuid(self, username: str): |
||||
if username not in self.uuid_cache.inv: |
||||
try: |
||||
player_uuid = requests.get( |
||||
"https://api.mojang.com/users/profiles/minecraft/{}".format(username)).json()["id"] |
||||
long_uuid = uuid.UUID(player_uuid) |
||||
self.uuid_cache.inv[username] = str(long_uuid) |
||||
return player_uuid |
||||
except requests.RequestException: |
||||
self.logger.error("Failed to lookup %s's username using the Mojang API.", username) |
||||
else: |
||||
return self.uuid_cache.inv[username] |
||||
|
||||
def get_discord_help_string(self): |
||||
help_str = ("Admin commands:\n" |
||||
"`mc!chathere`: Starts outputting server messages in this channel\n" |
||||
"`mc!stopchathere`: Stops outputting server messages in this channel\n" |
||||
"User commands:\n" |
||||
"`mc!tab`: Sends you the content of the server's player/tab list\n" |
||||
"`mc!register`: Starts the minecraft account registration process\n" |
||||
"To start chatting on the minecraft server, please register your account using `mc!register`.") |
||||
return help_str |
||||
|
||||
# https://stackoverflow.com/questions/33404752/removing-emojis-from-a-string-in-python |
||||
def remove_emoji(self, dirty_string): |
||||
emoji_pattern = re.compile( |
||||
"[" |
||||
u"\U0001F600-\U0001F64F" # emoticons |
||||
u"\U0001F300-\U0001F5FF" # symbols & pictographs |
||||
u"\U0001F680-\U0001F6FF" # transport & map symbols |
||||
u"\U0001F1E0-\U0001F1FF" # flags (iOS) |
||||
u"\U0001F900-\U0001FAFF" # CJK Compatibility Ideographs |
||||
# u"\U00002702-\U000027B0" |
||||
# u"\U000024C2-\U0001F251" |
||||
"]+", flags=re.UNICODE) |
||||
return emoji_pattern.sub(r'', dirty_string) |
||||
|
||||
def escape_markdown(self, md_string): |
||||
# Absolutely needs to go first or it will replace our escaping slashes! |
||||
escaped_string = md_string.replace("\\", "\\\\") |
||||
escaped_string = escaped_string.replace("_", "\\_") |
||||
escaped_string = escaped_string.replace("*", "\\*") |
||||
return escaped_string |
||||
|
||||
def strip_colour(self, dirty_string): |
||||
colour_pattern = re.compile( |
||||
u"\U000000A7" # selection symbol |
||||
".", flags=re.UNICODE) |
||||
return colour_pattern.sub(r'', dirty_string) |
||||
|
||||
def setup_logging(self, level): |
||||
if level.lower() == "debug": |
||||
log_level = logging.DEBUG |
||||
else: |
||||
log_level = logging.INFO |
||||
log_format = "%(asctime)s:%(name)s:%(levelname)s:%(message)s" |
||||
logging.basicConfig(filename="bridge_log.log", format=log_format, level=log_level) |
||||
stdout_logger = logging.StreamHandler(sys.stdout) |
||||
stdout_logger.setFormatter(logging.Formatter(log_format)) |
||||
logging.getLogger().addHandler(stdout_logger) |
||||
|
||||
def run_auth_server(self, port): |
||||
# We need to import twisted after setting up the logger because twisted hijacks our logging |
||||
from twisted.internet import reactor |
||||
from .auth_server import AuthFactory |
||||
|
||||
# Create factory |
||||
factory = AuthFactory() |
||||
|
||||
# Listen |
||||
self.logger.info("Starting authentication server on port %d", port) |
||||
|
||||
factory.listen("", port) |
||||
try: |
||||
reactor.run(installSignalHandlers=False) |
||||
except KeyboardInterrupt: |
||||
reactor.stop() |
||||
|
||||
def generate_random_auth_token(self, length): |
||||
letters = string.ascii_lowercase + string.digits + string.ascii_uppercase |
||||
return ''.join(random.choice(letters) for i in range(length)) |
||||
|
||||
def handle_disconnect(self, json_data=""): |
||||
self.logger.info('Disconnected.') |
||||
if json_data: |
||||
self.logger.info("Disconnect json data: %s", json_data) |
||||
self.previous_player_list = self.player_list.copy() |
||||
self.accept_join_events = False |
||||
self.player_list = bidict() |
||||
if self.connection.connected: |
||||
self.logger.info("Forced a disconnection because the connection is still connected.") |
||||
self.connection.disconnect(immediate=True) |
||||
time.sleep(15) |
||||
while not self.is_server_online(): |
||||
self.logger.info('Not reconnecting to server because it appears to be offline.') |
||||
time.sleep(15) |
||||
self.logger.info('Reconnecting.') |
||||
self.connection.connect() |
||||
|
||||
def handle_disconnect_packet(self, disconnect_packet): |
||||
self.handle_disconnect(disconnect_packet.json_data) |
||||
|
||||
def minecraft_handle_exception(self, exception, exc_info): |
||||
self.logger.error("A minecraft exception occured! %s:", exception, exc_info=exc_info) |
||||
self.handle_disconnect() |
||||
|
||||
def is_server_online(self): |
||||
server = MinecraftServer.lookup("{}:{}".format(self.config.mc_server, self.config.mc_port)) |
||||
try: |
||||
status = server.status() |
||||
del status |
||||
return True |
||||
except ConnectionRefusedError: |
||||
return False |
||||
# AttributeError: 'TCPSocketConnection' object has no attribute 'socket' |
||||
# This might not be required as it happens upstream |
||||
except AttributeError: |
||||
return False |
||||
|
||||
def register_handlers(self, connection): |
||||
connection.register_packet_listener( |
||||
self.handle_join_game, clientbound.play.JoinGamePacket) |
||||
|
||||
connection.register_packet_listener( |
||||
self.handle_chat, clientbound.play.ChatMessagePacket) |
||||
|
||||
connection.register_packet_listener( |
||||
self.handle_health_update, clientbound.play.UpdateHealthPacket) |
||||
|
||||
connection.register_packet_listener( |
||||
self.handle_disconnect_packet, clientbound.play.DisconnectPacket) |
||||
|
||||
connection.register_packet_listener( |
||||
self.handle_tab_list, clientbound.play.PlayerListItemPacket) |
||||
|
||||
connection.register_packet_listener( |
||||
self.handle_player_list_header_and_footer_update, clientbound.play.PlayerListHeaderAndFooterPacket) |
||||
|
||||
def handle_player_list_header_and_footer_update(self, header_footer_packet): |
||||
self.logger.debug("Got Tablist H/F Update: header=%s", header_footer_packet.header) |
||||
self.logger.debug("Got Tablist H/F Update: footer=%s", header_footer_packet.footer) |
||||
self.tab_header = json.loads(header_footer_packet.header)["text"] |
||||
self.tab_footer = json.loads(header_footer_packet.footer)["text"] |
||||
|
||||
def handle_tab_list(self, tab_list_packet): |
||||
self.logger.debug("Processing tab list packet") |
||||
for action in tab_list_packet.actions: |
||||
if isinstance(action, clientbound.play.PlayerListItemPacket.AddPlayerAction): |
||||
self.logger.debug( |
||||
"Processing AddPlayerAction tab list packet, name: %s, uuid: %s", action.name, action.uuid) |
||||
username = action.name |
||||
player_uuid = action.uuid |
||||
if action.name not in self.player_list.inv: |
||||
self.player_list.inv[action.name] = action.uuid |
||||
else: |
||||
# Sometimes we get a duplicate add packet on join idk why |
||||
return |
||||
if action.name not in self.uuid_cache.inv: |
||||
self.uuid_cache.inv[action.name] = action.uuid |
||||
# Initial tablist backfill |
||||
if self.accept_join_events: |
||||
webhook_payload = { |
||||
'username': username, |
||||
'avatar_url': "https://visage.surgeplay.com/face/160/{}".format(player_uuid), |
||||
'content': '', |
||||
'embeds': [{'color': 65280, 'title': '**Joined the game**'}] |
||||
} |
||||
for webhook in self.webhooks: |
||||
requests.post(webhook, json=webhook_payload) |
||||
if self.config.es_enabled: |
||||
self.es_logger.log_connection( |
||||
uuid=action.uuid, reason=ConnectionReason.CONNECTED, count=len(self.player_list)) |
||||
return |
||||
else: |
||||
# The bot's name is sent last after the initial back-fill |
||||
if action.name == self.bot_username: |
||||
self.accept_join_events = True |
||||
if self.config.es_enabled: |
||||
diff = set(self.previous_player_list.keys()) - set(self.player_list.keys()) |
||||
for idx, player_uuid in enumerate(diff): |
||||
self.es_logger.log_connection( |
||||
uuid=player_uuid, reason=ConnectionReason.DISCONNECTED, |
||||
count=len(self.previous_player_list) - (idx + 1)) |
||||
# Don't bother announcing the bot's own join message (who cares) but log it for analytics still |
||||
if self.config.es_enabled: |
||||
self.es_logger.log_connection( |
||||
uuid=action.uuid, reason=ConnectionReason.CONNECTED, count=len(self.player_list)) |
||||
|
||||
if self.config.es_enabled: |
||||
self.es_logger.log_connection(uuid=action.uuid, reason=ConnectionReason.SEEN) |
||||
if isinstance(action, clientbound.play.PlayerListItemPacket.RemovePlayerAction): |
||||
self.logger.debug("Processing RemovePlayerAction tab list packet, uuid: %s", action.uuid) |
||||
username = self.mc_uuid_to_username(action.uuid) |
||||
player_uuid = action.uuid |
||||
webhook_payload = { |
||||
'username': username, |
||||
'avatar_url': "https://visage.surgeplay.com/face/160/{}".format(player_uuid), |
||||
'content': '', |
||||
'embeds': [{'color': 16711680, 'title': '**Left the game**'}] |
||||
} |
||||
for webhook in self.webhooks: |
||||
requests.post(webhook, json=webhook_payload) |
||||
del self.uuid_cache[action.uuid] |
||||
del self.player_list[action.uuid] |
||||
if self.config.es_enabled: |
||||
self.es_logger.log_connection( |
||||
uuid=action.uuid, reason=ConnectionReason.DISCONNECTED, count=len(self.player_list)) |
||||
|
||||
def handle_join_game(self, join_game_packet): |
||||
self.logger.info('Connected and joined game as entity id %d', join_game_packet.entity_id) |
||||
self.player_list = bidict() |
||||
|
||||
def handle_chat(self, chat_packet): |
||||
json_data = json.loads(chat_packet.json_data) |
||||
if "extra" not in json_data: |
||||
return |
||||
chat_string = "" |
||||
for chat_component in json_data["extra"]: |
||||
chat_string += chat_component["text"] |
||||
|
||||
# Handle chat message |
||||
regexp_match = re.match("<(.*?)> (.*)", chat_string, re.M | re.I) |
||||
if regexp_match: |
||||
username = regexp_match.group(1) |
||||
original_message = regexp_match.group(2) |
||||
player_uuid = self.mc_username_to_uuid(username) |
||||
if username.lower() == self.bot_username.lower(): |
||||
# Don't relay our own messages |
||||
if self.config.es_enabled: |
||||
bot_message_match = re.match("<{}> (.*?): (.*)".format( |
||||
self.bot_username.lower()), chat_string, re.M | re.I) |
||||
if bot_message_match: |
||||
self.es_logger.log_chat_message( |
||||
uuid=self.mc_username_to_uuid(bot_message_match.group(1)), |
||||
display_name=bot_message_match.group(1), |
||||
message=bot_message_match.group(2), |
||||
message_unformatted=chat_string) |
||||
self.es_logger.log_raw_message( |
||||
msg_type=chat_packet.Position.name_from_value(chat_packet.position), |
||||
message=chat_packet.json_data) |
||||
return |
||||
self.logger.info("Incoming message from minecraft: Username: %s Message: %s", username, original_message) |
||||
self.logger.debug("msg: %s", repr(original_message)) |
||||
message = self.escape_markdown(self.remove_emoji(original_message.strip().replace( |
||||
"@", "@\N{zero width space}"))) |
||||
webhook_payload = { |
||||
'username': username, |
||||
'avatar_url': "https://visage.surgeplay.com/face/160/{}".format(player_uuid), |
||||
'content': '{}'.format(message) |
||||
} |
||||
for webhook in self.webhooks: |
||||
requests.post(webhook, json=webhook_payload) |
||||
if self.config.es_enabled: |
||||
self.es_logger.log_chat_message( |
||||
uuid=player_uuid, display_name=username, message=original_message, message_unformatted=chat_string) |
||||
if self.config.es_enabled: |
||||
self.es_logger.log_raw_message( |
||||
msg_type=chat_packet.Position.name_from_value(chat_packet.position), |
||||
message=chat_packet.json_data) |
||||
|
||||
def handle_health_update(self, health_update_packet): |
||||
if health_update_packet.health <= 0: |
||||
self.logger.debug("Respawned the player because it died") |
||||
packet = serverbound.play.ClientStatusPacket() |
||||
packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN |
||||
self.connection.write_packet(packet) |
||||
|
||||
|
||||
def main(): |
||||
bridge = MinecraftDiscordBridge() |
||||
bridge.run() |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
Loading…
Reference in new issue