You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
682 lines
30 KiB
682 lines
30 KiB
#!/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
|
|
from threading import Thread
|
|
from config import Configuration
|
|
from database import DiscordChannel, AccountLinkToken, DiscordAccount
|
|
import database_session
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
import elasticsearch_logger
|
|
from elasticsearch_logger import es_chat_message, es_connection, es_raw_message, ConnectionReason
|
|
|
|
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
|
|
|
|
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:
|
|
logging.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"]
|
|
UUID_CACHE.inv[username] = player_uuid
|
|
return player_uuid
|
|
except:
|
|
logging.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:%(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
|
|
logging.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:
|
|
elasticsearch_logger.initialize(config)
|
|
|
|
reactor_thread = Thread(target=run_auth_server, args=(config.auth_port,))
|
|
reactor_thread.start()
|
|
|
|
def handle_disconnect():
|
|
logging.info('Disconnected.')
|
|
global PLAYER_LIST, PREVIOUS_PLAYER_LIST, ACCEPT_JOIN_EVENTS
|
|
PREVIOUS_PLAYER_LIST = PLAYER_LIST.copy()
|
|
ACCEPT_JOIN_EVENTS = False
|
|
PLAYER_LIST = bidict()
|
|
connection.disconnect(immediate=True)
|
|
time.sleep(15)
|
|
while not is_server_online():
|
|
logging.info('Not reconnecting to server because it appears to be offline.')
|
|
time.sleep(15)
|
|
logging.info('Reconnecting.')
|
|
connection.connect()
|
|
|
|
def handle_disconnect_packet(join_game_packet):
|
|
handle_disconnect()
|
|
|
|
def minecraft_handle_exception(exception, exc_info):
|
|
logging.info("{}: {}".format(exception, 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
|
|
|
|
logging.debug("Checking if the server {} is online before connecting.")
|
|
|
|
if not config.mc_online:
|
|
logging.info("Connecting in offline mode...")
|
|
while not is_server_online():
|
|
logging.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:
|
|
logging.info(e)
|
|
sys.exit()
|
|
BOT_USERNAME = auth_token.profile.name
|
|
logging.info("Logged in as %s..." % auth_token.profile.name)
|
|
while not is_server_online():
|
|
logging.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
|
|
logging.debug("Got Tablist H/F Update: header={}".format(header_footer_packet.header))
|
|
logging.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
|
|
logging.debug("Processing tab list packet")
|
|
for action in tab_list_packet.actions:
|
|
if isinstance(action, clientbound.play.PlayerListItemPacket.AddPlayerAction):
|
|
logging.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:
|
|
post = requests.post(webhook,json=webhook_payload)
|
|
if config.es_enabled:
|
|
es_connection(uuid=action.uuid, reason=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, uuid in enumerate(diff):
|
|
es_connection(uuid=uuid, reason=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:
|
|
es_connection(uuid=action.uuid, reason=ConnectionReason.CONNECTED, count=len(PLAYER_LIST))
|
|
|
|
if config.es_enabled:
|
|
es_connection(uuid=action.uuid, reason=ConnectionReason.SEEN)
|
|
if isinstance(action, clientbound.play.PlayerListItemPacket.RemovePlayerAction):
|
|
logging.debug("Processing RemovePlayerAction tab list packet, uuid: {}".format(action.uuid))
|
|
username = UUID_CACHE[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:
|
|
post = requests.post(webhook,json=webhook_payload)
|
|
del UUID_CACHE[action.uuid]
|
|
del PLAYER_LIST[action.uuid]
|
|
if config.es_enabled:
|
|
es_connection(uuid=action.uuid, reason=ConnectionReason.DISCONNECTED, count=len(PLAYER_LIST))
|
|
|
|
def handle_join_game(join_game_packet):
|
|
global PLAYER_LIST
|
|
logging.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:
|
|
es_chat_message(
|
|
uuid=UUID_CACHE.inv[bot_message_match.group(1)],
|
|
display_name=bot_message_match.group(1),
|
|
message=bot_message_match.group(2),
|
|
message_unformatted=chat_string)
|
|
es_raw_message(type=ChatType(chat_packet.position).name, message=chat_packet.json_data)
|
|
return
|
|
logging.info("Username: {} Message: {}".format(username, original_message))
|
|
logging.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:
|
|
post = requests.post(webhook, json=webhook_payload)
|
|
if config.es_enabled:
|
|
es_chat_message(
|
|
uuid=player_uuid, display_name=username, message=original_message, message_unformatted=chat_string)
|
|
if config.es_enabled:
|
|
es_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:
|
|
logging.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():
|
|
logging.info("Discord bot logged in as {} ({})".format(discord_bot.user.name, discord_bot.user.id))
|
|
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":
|
|
global WEBHOOKS
|
|
WEBHOOKS.append(webhook.url)
|
|
found = True
|
|
logging.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 message.channel:
|
|
if webhook.name == "_minecraft":
|
|
del WEBHOOKS[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 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)
|
|
|
|
logging.info(str(len(message_to_send)) + " " + repr(message_to_send))
|
|
|
|
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
|
|
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)
|
|
return
|
|
|
|
PREVIOUS_MESSAGE = message_to_send
|
|
NEXT_MESSAGE_TIME = datetime.now(timezone.utc) + timedelta(seconds=config.message_delay)
|
|
|
|
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
|
|
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)
|
|
session.close()
|
|
del session
|
|
else:
|
|
session.close()
|
|
del session
|
|
|
|
discord_bot.run(config.discord_token)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|