hit-mc / psmb_chatbridge

GNU Affero General Public License v3.0
3 stars 0 forks source link

死亡消息可以翻译一下 #3

Open inclyc opened 2 years ago

keuin commented 2 years ago

可以参考一下OneSMP服务器生产环境的代码(这是PyMC2Redis的一个内部fork,因为和咱们自己的需求太耦合了,就没发出来):

# -------------------------------------------------
# PyMC2Redis: Python Minecraft to Redis script (notification-only)
# Author: Keuin
# Version: 1.4 2020.12.27 (notification-only)
# Homepage: https://github.com/keuin/pymc2redis
# -------------------------------------------------

import collections
import json
import re
import threading
import time
from threading import Lock
from threading import Thread
import requests

import redis
from redis import Redis

CONFIG_FILE_NAME = 'pymc2redis.json'
VERSION = '1.4 2020.12.27'

MESSAGE_THREAD_RECEIVE_TIMEOUT_SECONDS = 2  # timeout in redis LPOP operation
MESSAGE_THREAD_SLEEP_SECONDS = 0.5  # time in threading.Event.wait()
MESSAGE_SEND_MINIMUM_INTERVAL_SECONDS = 0.8
RETRY_SLOWDOWN_TARGET_SECONDS = 15
RETRY_SLOWDOWN_TIMES_THRESHOLD = 10
ALTERNATIVE_RETRY_SLOWDOWN_TIMES_THRESHOLD = 5
ALTERNATIVE_RETRY_SLOWDOWN_TARGET_SECONDS = 60

MSG_PREFIX = [' ', '#']
MSG_ENCODING = 'utf-8'
MSG_COLOR = '§7'
MSG_USER_COLOR = '§d'
MSG_MENTION_COLOR = '§b'
MSG_SPLIT_STR = '||'
MSG_AT_TEMPLATE = '\n@{qq_number}\n'

HEAD_PLAYER_LIST = 'SERVER'
HEAD_PLAYER_DIE = '悲報'
HEAD_PLAYER_ADVANCEMENT = '喜報'

COLOR_GREEN = '§a'
COLOR_BLUE = '§9'
COLOR_YELLOW = '§e'
COLOR_RED = '§c'
COLOR_AQUA = '§b'

LOG_COLOR = '§e'
COMMAND_STATUS = '!PYMC'
COMMAND_RESET = '!PYMC reset'
RCOMMAND_LIST = '!LIST'  # Redis command

CFG_REDIS_SERVER = 'redis_server'
CFG_REDIS_SERVER_ADDRESS = 'address'
CFG_REDIS_SERVER_PORT = 'port'
CFG_REDIS_SERVER_PASSWORD = 'password'
CFG_KEY = 'key'
CFG_KEY_SENDER = 'sender'
CFG_KEY_RECEIVER = 'receiver'
CFG_LANGUAGE = 'lang'
CFG_TRANSLATION_SETTING = 'translating'
CFG_TRANSLATION_FROM = 'from'
CFG_TRANSLATION_TO = 'to'
CFG_ID_MAPPING = 'id_mapping'

# Simple logger wrapper
def log(text, prefix='LOG', ingame=False):
    message = '[PyMC2Redis][{pf}] {msg}'.format(pf=prefix, msg=text)
    if ingame and svr:
        svr.say('{color}{msg}'.format(msg=message, color=LOG_COLOR))
    if svr:
        svr.logger.info(message)
    else:
        print(message)  # fallback to STDOUT

def info(text, ingame=False):
    log(text, 'INFO', ingame)

def warn(text, ingame=False):
    log(text, 'WARN', ingame)

def error(text, ingame=False):
    log(text, 'ERROR', ingame)

# Simple text dyer

def green(s) -> str:
    return '{}{}'.format(COLOR_GREEN, s)

def yellow(s) -> str:
    return '{}{}'.format(COLOR_YELLOW, s)

def red(s) -> str:
    return '{}{}'.format(COLOR_RED, s)

def aqua(s) -> str:
    return '{}{}'.format(COLOR_AQUA, s)

# in-game message translator

def translate_format_item_value(a: str):
    """
    Preprocess: we replace all marks like %1$s to regex capture group (\S+)
    param a: The string to be processed.
    :return: The processed string.
    """
    i = 1
    pattern = '%{i}$s'
    desired = r'(\[.+\]|\S+)'
    while pattern.format(i=i) in a:
        a = a.replace(pattern.format(i=i), desired)
        i += 1
    return a

def translate(lang_from: dict, lang_to: dict, text: str):
    """
    Translate a in-game message to a specific language.
    :param lang_from: the origin language.
    :param lang_to: the desired language.
    :param text: the message text to be translated.
    :return: a str object if translated, or None if failed.
    """

    # preprocess: we replace all marks like %1$s to regex capture group (\S+)

    for k, v in lang_from.items():
        lang_from[k] = translate_format_item_value(v)

    # start translating

    universe_key = None
    params = None

    for k, v in lang_from.items():
        # traverse all items and try to fit.
        r = re.fullmatch(v, text)
        if r:
            # The message fits this item!
            universe_key = k  # We use the key to identify this string
            params = r.groups()
            break

    # Now we have universe_key and params. We need to translate them into the target language.

    if not universe_key:
        # Oh no, we haven't found the universe key. Maybe the language setting is wrong?
        warn('Translator failed to match any items in the source language dict.')
        return None

    if universe_key not in lang_to:
        # The target language doesn't contain the key we need. Failed.
        warn('Translator failed to find desired item in the target language.')
        return None

    s = str(lang_to[universe_key])
    for i, real in enumerate(params):
        pattern = '%{i}$s'.format(i=i + 1)
        s = s.replace(pattern, real)

    return s

# Simple edit distance calculator for alias hint

def __edit_distance_dp(str1, str2, m, n):
    # Create a table to store results of sub-problems
    dp = [[0 for x in range(n + 1)] for x in range(m + 1)]

    # Fill d[][] in bottom up manner
    for i in range(m + 1):
        for j in range(n + 1):

            # If first string is empty, only option is to
            # insert all characters of second string
            if i == 0:
                dp[i][j] = j  # Min. operations = j

            # If second string is empty, only option is to
            # remove all characters of second string
            elif j == 0:
                dp[i][j] = i  # Min. operations = i

            # If last characters are same, ignore last char
            # and recur for remaining string
            elif str1[i - 1] == str2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]

                # If last character are different, consider all
            # possibilities and find minimum
            else:
                dp[i][j] = 1 + min(dp[i][j - 1],  # Insert
                                   dp[i - 1][j],  # Remove
                                   dp[i - 1][j - 1])  # Replace

    return dp[m][n]

def edit_distance(a: str, b: str):
    return __edit_distance_dp(a, b, len(a), len(b))

# Redis command related

class RCommand:
    """
    Base Redis command class.
    """
    _reply = None

    @staticmethod
    def from_redis_message(msg: str):
        """
        Build a RCommand object from a Redis message.
        :param msg: a message from the Redis server.
        :return: If the message is a valid Redis command, return a RCommand instance. Otherwise return None.
        """
        if RCOMMAND_LIST.lower() == str(msg).lower():
            return RCList()
        return None

    def get_echo(self):
        """
        Get the formatted echo. Note: is_valid_echo should be called and return True in advance.
        :return: The echo. If failed, return None.
        """
        if self._reply:
            return self._format_reply(self._reply)
        return None

    def is_valid_echo(self, message: str) -> bool:
        """
        After receiving a message from the server console, this method check if the message is a reply to this command instance.
        :return: True if the message is a reply to this command, thus this command instance should be executed and pop out. Otherwise, return False.
        """
        # This is a numb impl.
        # DO NOT RELY ON THIS
        self._reply = message
        return False

    def execute(self, server):
        pass

    def _format_reply(self, reply: str) -> str:
        """
        Format a valid reply from the server console to a friendly form.
        :param reply: The raw reply from server console.
        :return: The formatted message, which should be sent to the Redis as a response.
        """
        return reply

class RCList(RCommand):
    """
    Redis command that shows all online players.
    default command: #!list
    """

    def is_valid_echo(self, message: str) -> bool:
        if re.match(r'There are [0-9]+ of a max [0-9]+ players online:', message) or message == 'No player was found':
            self._reply = message
            return True
        return False

    def _format_reply(self, reply: str) -> str:
        r = re.findall(r'There are [0-9]+ of a max [0-9]+ players online:.*', reply)
        if not r:
            if reply == 'No player was found':
                return 'No players online.'
            else:
                return 'Error: blank reply message.'
        return r[0]

    def execute(self, server):
        server.execute('list')

# Message management related (ADT/threads)

class MessageReceiverThread(Thread):
    """
    This thread receives messages from the Redis server, then print them on the in-game chat menu.
    """
    __quit_event = threading.Event()

    def __init__(self):
        Thread.__init__(self, name='MessageReceiverThread')

    def quit(self):
        self.__quit_event.set()

    def run(self):
        info('MessageReceiverThread is starting.')
        self.__quit_event.clear()
        global enabled, con, retry_counter, counter_message_to_game, rcommand, svr
        while enabled and con:
            try:
                if retry_counter.value() >= RETRY_SLOWDOWN_TIMES_THRESHOLD:
                    time.sleep(RETRY_SLOWDOWN_TARGET_SECONDS)  # cool down

                raw_message = con.brpop(
                    keys=config_keys[CFG_KEY_SENDER],
                    timeout=MESSAGE_THREAD_RECEIVE_TIMEOUT_SECONDS)
                if not raw_message:
                    continue  # Timed out. Possibly not a failure.
                if len(raw_message) != 2:
                    warn('Received invalid message from Redis server. Ignoring. ({})'.format(raw_message))
                    continue
                msg = Message.from_redis_raw_bytes(raw_message[1])
                if not msg:
                    warn('Cannot parse message: {}'.format(raw_message))
                    continue
                log('Received message from Redis server. Sender={sender}, Message={msg}.'.format(
                    sender=msg.get_sender(), msg=msg.get_body()))
                rcmd_instance = RCommand.from_redis_message(msg.get_body())
                if rcmd_instance:
                    # If the message is a valid Redis command
                    log('A valid RCommand instance was created from instruction {}'.format(msg.get_body()))
                    if not rcommand:
                        rcommand = rcmd_instance
                        rcmd_instance.execute(svr)
                    else:
                        warn(
                            'There is already a Redis command waiting for server response. The new command {} will be ignored.'.format(
                                msg.get_body()))
                else:
                    # The message is a normal chat msg. Just repeat it.
                    msg.display()

                # finish processing Redis message
                # update counters
                counter_message_to_game += 1  # The counter cares about all messages.
                retry_counter.reset()  # If we succeed, reset the cool-down counter
            except (ConnectionError, TimeoutError, redis.RedisError) as e:
                error('An exception occurred while waiting for messages from the Redis server: {}'.format(e))
                retry_counter.increment()
            if self.__quit_event.wait(MESSAGE_THREAD_SLEEP_SECONDS):
                break

        info('MessageReceiverThread is quitting.')
        info('MRT enabled={e}, con={c}'.format(e=enabled, c=con))

def get_game_id_from_qq(qq: str):
    """
    Translate QQ to Game ID.
    :param qq: the QQ number.
    :return: game ID. None if failed.
    """
    global config_id_mapping
    id_mapping = config_id_mapping
    if not isinstance(id_mapping, dict):
        return None
    alias_list = id_mapping.get(qq)
    if not isinstance(alias_list, list):
        return None
    if len(alias_list) > 0:
        return alias_list[0]
    return None

class Message:
    __sender = ""
    __body = ""
    __mentioned_players = []  # Game ID

    def __init__(self, sender: str, body: str):
        mentioned_players_raw = re.findall(r'\[@[0-9]+\]', body)
        mentioned_players_qq = [s[2:-1] for s in mentioned_players_raw]
        self.__sender = sender

        for r in zip(mentioned_players_raw, mentioned_players_qq):
            body = body.replace(r[0], '{mention_color}@{id}{msg_color}'.format(id=get_game_id_from_qq(r[1]),
                                                                               mention_color=MSG_MENTION_COLOR,
                                                                               msg_color=MSG_COLOR))
        self.__body = body

        players = []
        for qq in mentioned_players_qq:
            game_id = get_game_id_from_qq(qq)
            if game_id:
                players.append(game_id)
        self.__mentioned_players = players

    def get_sender(self):
        return self.__sender

    def get_body(self):
        return self.__body

    def get_mentioned_players(self):
        """
        Get IDs of mentioned players in this message.
        :return: a game ID list.
        """
        return list(self.__mentioned_players)

    @staticmethod
    def from_redis_raw_bytes(raw_bytes: bytes, encoding: str = MSG_ENCODING):
        """
        Construct a message from raw bytes received from Redis.
        :param raw_bytes: the raw bytes.
        :param encoding: the encoding.
        :return: the Message object. If failed, return None.
        """
        str_ = str(raw_bytes, encoding=encoding)
        log('Raw message string: {}'.format(str_))
        r = re.match(r'([^|]*)(?:\|\|)([\s\S]*)', str_)
        if r and len(r.groups()) == 2:
            g = r.groups()
            sender = g[0]
            body = g[1]
            return Message(sender, body)
        return None

    @staticmethod
    def from_ingame_chat(raw_chat_str_with_prefix: str, sender: str, id_mapping_inv_index=None):
        """
        Build a Message object with in-game chat string and sender ID.
        :param raw_chat_str_with_prefix: the chat string. such as '#Hello!'
        :param sender: the sender.
        :return: A Message and an index list of invalid ats. If the parameter is invalid, return None.
        """
        if not Message.is_outbound_message(raw_chat_str_with_prefix):
            return None
        msg_cleaned = Message.__clean_message(raw_chat_str_with_prefix)
        if not id_mapping_inv_index:
            # If id mapping is not defined
            return msg_cleaned

        msg_alias_processed, invalid_list = Message.__convert_at_ids_to_qq_numbers(msg_cleaned, id_mapping_inv_index)

        return Message(sender, msg_alias_processed), invalid_list

    @staticmethod
    def from_server_console_echo(echo: str, title='SERVER'):
        return Message(title, echo)

    @staticmethod
    def is_outbound_message(s: str) -> bool:
        """
        Check if this string should be transmitted to the Redis server.
        :param s: The string to be checked. Usually a raw chat string.
        :return: True or False.
        """
        for pf in MSG_PREFIX:
            if s.startswith(pf):
                return True
        return False

    def pack(self) -> str:
        """
        Pack the message to a string that can be pushed to Redis server.
        :return: the string.
        """
        return "{sender}{split}{msg}".format(sender=self.__sender, msg=self.__body, split=MSG_SPLIT_STR)

    def display(self):
        """
        Print this message on the in-game chat menu, with the default format.
        """
        if svr:
            svr.say(self.__to_ingame_string())
            # mentioned_players = self.get_mentioned_players()
            # for player_id in mentioned_players:
            #     log('Notifying player {}...'.format(player_id))
            #     for _ in range(3):
            #         svr.execute('playsound minecraft:block.note_block.bell master {game_id}'.format(game_id=player_id))
            #         time.sleep(0.25)
            # if not mentioned_players:
            #     log('No player is mentioned.')
        else:
            error('Server instance "svr" is not available. Cannot display in-game message: {}:{}'.format(
                self.get_sender(), self.get_body()))

    @staticmethod
    def __clean_message(message: str) -> str:
        for pf in MSG_PREFIX:
            if message.startswith(pf):
                return message[len(pf):]
        return message

    @staticmethod
    def __convert_at_ids_to_qq_numbers(msg: str, id_mapping: dict) -> (str, list):
        """
        Convert valid ats to qq numbers. Return invalid ats.
        :param msg: the message.
        :param id_mapping: the mapping.
        :return: message and a index list of invalid ats. The indexes is referencing the original message string.
        """
        # Buggy: aliases must not share a common prefix.
        replace_list = []
        invalid_list = []
        for i in range(len(msg)):
            if msg[i] == '@':
                valid = False
                for alias, qq_number in id_mapping.items():
                    if msg[i + 1:].lower().startswith(alias.lower()):
                        # a valid match
                        replace_list.append((msg[i + 1:i + 1 + len(alias)], qq_number))
                        valid = True
                        break
                if not valid:
                    # the at is invalid
                    invalid_list.append(i)

        for src, dst in replace_list:
            msg = msg.replace('@' + src, MSG_AT_TEMPLATE.format(qq_number=dst), 1)

        return msg, invalid_list

    def __to_ingame_string(self, msg_color=MSG_COLOR, user_color=MSG_USER_COLOR) -> str:
        return '{user_color}<{user}> {msg_color}{msg}'.format(
            msg_color=msg_color,
            msg=self.__body,
            user_color=user_color,
            user=self.__sender)

class MessageSenderThread(Thread):
    """
    Message sender provides a FIFO queue for message transmitting to the Redis server.
    Thus the message can be guaranteed to arrive the target server.
    """

    __queue = collections.deque()
    __queue_lock = Lock()
    __quit_event = threading.Event()

    def __init__(self):
        Thread.__init__(self, name='MessageSenderThread')

    def quit(self):
        self.__quit_event.set()

    def push(self, msg: Message) -> int:
        """
        Add a Message object into the queue.
        :param msg: the message.
        :return: the queue length.
        """
        self.__queue_lock.acquire(blocking=True)
        self.__queue.append(msg)
        size = len(self.__queue)
        self.__queue_lock.release()
        return size

    def length(self) -> int:
        self.__queue_lock.acquire(blocking=True)
        size = len(self.__queue)
        self.__queue_lock.release()
        return size

    def run(self):
        info('MessageSenderThread is starting.')
        self.__quit_event.clear()
        while enabled and con:

            # ---- loop start ----

            # peek
            self.__queue_lock.acquire(blocking=True)
            try:
                msg = self.__queue[0]
            except IndexError:
                msg = None
            self.__queue_lock.release()

            # send message
            if isinstance(msg, Message):
                if redis_send_message(msg):
                    # success, pop out
                    self.__queue_lock.acquire(blocking=True)
                    self.__queue.popleft()
                    self.__queue_lock.release()
            elif msg:
                warn('Bad object in message queue: {}'.format(msg))  # msg is not None and not a Message object
            # otherwise do nothing

            if self.__quit_event.wait(MESSAGE_SEND_MINIMUM_INTERVAL_SECONDS):
                break

            # ---- loop end ----

        info('MessageSenderThread is quitting.')
        info('MST enabled={e}, con={c}'.format(e=enabled, c=con))

class SafeCounter:
    __lock = Lock()
    __counter = 0

    def increment(self, increment: int = 1):
        self.__lock.acquire()
        self.__counter += increment
        self.__lock.release()

    def reset(self):
        self.__lock.acquire()
        self.__counter = 0
        self.__lock.release()

    def value(self) -> int:
        return self.__counter

# Main program

con = None
enabled = False
config_server = dict()
config_keys = dict()
config_id_mapping = dict()
language = {}
translating = {"from": "", "to": ""}
svr = None
receiver_thread = None
sender_thread = None
redis_reconnect_lock = Lock()
retry_counter = SafeCounter()
counter_message_to_game = 0
counter_message_to_redis = 0
counter_send_failure = 0
rcommand = None  # A RCommand waiting for response.
id_mapping_inv_index = dict()

def redis_connect() -> bool:
    """
    Connect to the configured Redis server.
    :return: True if connected, False if failed to connect.
    """
    try:
        global con
        if config_server and CFG_REDIS_SERVER_ADDRESS in config_server and CFG_REDIS_SERVER_PORT in config_server:
            host = config_server[CFG_REDIS_SERVER_ADDRESS]
            port = config_server[CFG_REDIS_SERVER_PORT]
            password = config_server[CFG_REDIS_SERVER_PASSWORD] if CFG_REDIS_SERVER_PASSWORD else None
            info('Connecting to Redis server, host={host}:{port}, password=*****.'.format(host=host, port=port))
            con = Redis(
                host=host,
                port=port,
                password=password,
                socket_timeout=10,
                socket_connect_timeout=10,
                health_check_interval=15)
            info('Pinging...')
            return con.ping()
    except redis.RedisError:
        return False

def redis_reconnect():
    global retry_counter
    try:
        if redis_reconnect_lock.acquire(False):
            if retry_counter.value() >= RETRY_SLOWDOWN_TIMES_THRESHOLD:
                time.sleep(RETRY_SLOWDOWN_TARGET_SECONDS)  # cool down
            elif retry_counter.value() >= ALTERNATIVE_RETRY_SLOWDOWN_TIMES_THRESHOLD:
                time.sleep(ALTERNATIVE_RETRY_SLOWDOWN_TARGET_SECONDS)
            else:
                time.sleep(1)
            warn('Connection lost. Reconnecting to the Redis server... (retry_counter={cnt})'.format(
                cnt=retry_counter.value()), True)
            if redis_connect():
                info('Reconnected. Everything should run smoothly now.', True)
            else:
                info('Failed to reconnect to the specific Redis server.')
            retry_counter.increment()
    except Exception as e:
        error('Unexpected exception occurred while reconnecting: {}'.format(e))

    redis_reconnect_lock.release()

def redis_send_message(msg: Message) -> bool:
    """
    Send a message to Redis server.
    :param msg: the Message object.
    :return: True if success, False if failed.
    """
    global con, counter_message_to_redis, counter_send_failure
    broken_connection = False
    try:
        if con:
            if msg:
                # ---- push message start ----

                info('Pushing: {user}->{msg}'.format(user=msg.get_sender(), msg=msg.get_body()))
                r = con.lpush(config_keys[CFG_KEY_RECEIVER], msg.pack())
                try:
                    if isinstance(r, bytes) or isinstance(r, bytearray):
                        r = str(r, encoding=MSG_ENCODING)
                    numeric = int(r)
                    if numeric > 0:
                        info('Success.')
                        counter_message_to_redis += 1
                        return True
                    else:
                        info('Failed when pushing message: queue_length={}, raw_response={}'.format(numeric, r))
                except ValueError:
                    error('Invalid response: {}'.format(r))

                # ---- push message end ----
            else:
                # msg is None
                error('This should not happen. Please report this to Keuin.')
        else:
            error('Broken connection. Cannot talk to Redis server.', True)
            broken_connection = True

    except (ConnectionError, TimeoutError, redis.RedisError) as e:
        error('Failed to talk to the Redis server: {}.'.format(e))
        broken_connection = True
    except Exception as e:
        error('Unexpected exception: {}'.format(e))

    if broken_connection:
        redis_reconnect()
    counter_send_failure += 1
    return False

def redis_ping() -> bool:
    if not con:
        return False
    try:
        return con.ping()
    except redis.RedisError:
        return False

# def parse_redis_command(message):
#     """
#     Check if a message from Redis is a command. If so, execute it. Otherwise do nothing.
#     :return: A string if the given message is a valid Redis command, and it has been executed. False if the message is not a Redis command.
#     """
#     message = str(message).lower()
#     if message == RCOMMAND_LIST.lower():
#         # list players

def init() -> bool:
    """
    Clean-up, load config file, connect to Redis server and start message threads.
    :return: True if success, False if failed to initialize.
    """
    global con, enabled, config_server, config_keys, language, translating, receiver_thread, sender_thread
    global redis_reconnect_lock, retry_counter, counter_message_to_game, counter_message_to_redis, counter_send_failure
    global rcommand, id_mapping_inv_index, config_id_mapping
    #
    # # reset connection
    # if con:
    #     con.close()
    # con = None
    # if isinstance(receiver_thread, MessageReceiverThread) and receiver_thread.is_alive():
    #     receiver_thread.quit()
    #
    # if isinstance(sender_thread, MessageSenderThread) and sender_thread.is_alive():
    #     sender_thread.quit()
    #
    # # reset globals
    # receiver_thread = None
    # sender_thread = None
    # redis_reconnect_lock = Lock()
    # retry_counter = SafeCounter()
    # counter_message_to_game = 0
    # counter_message_to_redis = 0
    # counter_send_failure = 0
    # rcommand = None
    #
    # read configuration and connect
    try:
        with open(CONFIG_FILE_NAME, 'r', encoding='utf-8') as f:
            config = json.load(f)
    except FileNotFoundError:
        error('Cannot locate configuration file {}.'.format(CONFIG_FILE_NAME))
        return False
    except IOError as e:
        error('Encountered an I/O exception while reading configuration file {f}: {e}'.format(f=CONFIG_FILE_NAME, e=e))
        return False
    #
    # # --- check config ---
    #
    # # check server
    # if CFG_REDIS_SERVER not in config:
    #     error('Cannot read redis server info from {}.'.format(CONFIG_FILE_NAME))
    #     return False
    # config_server = config[CFG_REDIS_SERVER]
    # if CFG_REDIS_SERVER_ADDRESS not in config_server \
    #         or CFG_REDIS_SERVER_PORT not in config_server:
    #     error('Redis server address or port is not defined. Check {} file.'.format(CONFIG_FILE_NAME))
    #     return False
    #
    # # check keys
    # if CFG_KEY not in config:
    #     error('Cannot read keys from {}.'.format(CONFIG_FILE_NAME))
    #     return False
    # config_keys = config[CFG_KEY]
    # if CFG_KEY_SENDER not in config_keys \
    #         or CFG_KEY_RECEIVER not in config_keys:
    #     error('Cannot read keys.sender or keys.receiver from the configuration.')
    #     return False

    # check and read the translation and languages

    language = config[CFG_LANGUAGE]
    translating = config[CFG_TRANSLATION_SETTING]

    if not isinstance(language, dict):
        error('Malformed language dict in the configuration.')
        return False

    if not isinstance(translating, dict) or 'from' not in translating or 'to' not in translating:
        error('Invalid translating setting in the configuration.')
        return False

    # Validate needed languages

    if translating['from'] not in language:
        error('The language {} of translating.from is not defined.'.format(translating['from']))
        return False

    if translating['to'] not in language:
        error('The language {} of translating.to is not defined.'.format(translating['to']))
        return False

    # Load id mappings and create inverted index

    id_mapping = config.get(CFG_ID_MAPPING)
    if not id_mapping:
        id_mapping = dict()
    config_id_mapping = id_mapping

    if isinstance(config_id_mapping, dict):
        # valid mapping config
        info('Loading inverted id-mapping index...')

        inv_index = dict()
        alias_counter = 0
        for k, v in config_id_mapping.items():
            # k: qq, v: alias list
            if isinstance(v, list):
                for alias in v:
                    # alias -> qq
                    inv_index[alias] = k
                    alias_counter += 1
            else:
                warn('Invalid value type of {} in {}: must be a list.'.format(k, CFG_ID_MAPPING))
        id_mapping_inv_index = inv_index
        info('Loaded {} alias(es).'.format(alias_counter))
    else:
        info('ID mapping is not defined. Skip.')
        id_mapping_inv_index = dict()

    # --- check config ---

    # # connect to Redis host
    # if redis_connect():
    #     info('Connected.')
    # else:
    #     error('Failed to connect to Redis server. Please check your settings and network.', True)
    #     con = None
    #     return False
    #
    # # start threads
    # enabled = True
    # receiver_thread = MessageReceiverThread()
    # sender_thread = MessageSenderThread()
    # receiver_thread.start()
    # sender_thread.start()

    return True

def enable():
    """
    Initialize the plugin, and print error messages if failed to enable it.
    :return:
    """
    global enabled
    enabled = init()
    if not enabled:
        error('Due to an earlier error, PyMC2Redis will be disabled.'
              ' Please check your configuration and type "{cmd}" to reset.'.format(cmd=COMMAND_RESET), True)

def disable():
    global enabled, redis_reconnect_lock

    enabled = False

    # if isinstance(sender_thread, MessageSenderThread) and sender_thread.is_alive():
    #     info('Stopping sender thread.')
    #     sender_thread.quit()
    #     sender_thread.join()
    #
    # if isinstance(receiver_thread, MessageReceiverThread) and receiver_thread.is_alive():
    #     info('Stopping receiver thread.')
    #     receiver_thread.quit()
    #     receiver_thread.join()
    #
    # if isinstance(con, Redis):
    #     info('Closing connection.')
    #     con.close()

    redis_reconnect_lock = Lock()  # Generate a new lock to prevent unexpected deadlock.

def on_load(server, old_module):
    global enabled, svr, receiver_thread
    svr = server
    enable()

def on_unload(server):
    global enabled, con, receiver_thread
    if not enabled:
        return
    enabled = False
    # if receiver_thread:
    #     receiver_thread.quit()
    # if con:
    #     con.close()

# def on_info(server, info_):
#     global rcommand
#     if info_.is_user or not isinstance(rcommand, RCommand):
#         # We just care about Redis command echo in this procedure.
#         # For user messages, we process them in on_user_info().
#         return
#     msg = str(info_.content)
#     if rcommand.is_valid_echo(msg):
#         if isinstance(sender_thread, MessageSenderThread):
#             echo = rcommand.get_echo()
#             if echo:
#                 sender_thread.push(Message.from_server_console_echo(echo, HEAD_PLAYER_LIST))
#             else:
#                 error('Invalid echo from the server console.')
#             rcommand = None  # Remove the command
#         else:
#             error('Sender thread is not alive, the pending Redis command cannot be executed.')

# def on_user_info(server, info_):
#     msg = str(info_.content)
#     player = str(info_.player)
#     if Message.is_outbound_message(msg):
#         # If the message sent by a player is a valid outbound message (to Redis).
#         # Message
#         message, invalid_ats = Message.from_ingame_chat(msg, player, id_mapping_inv_index)
#         if isinstance(sender_thread, MessageSenderThread):
#             # If the message sender thread is alive.
#             sender_thread.push(message)
#
#             # ---- Show hints of invalid ats ----
#
#             # this is a list
#             min_distance_of_invalid_ats = []
#             sorted_alias_lists = []
#             for i in invalid_ats:
#                 t = len(msg) - i
#                 # we use msg[i+j:] to traverse all prefix sub strings
#                 sorted_alias_lists.append(
#                     sorted(
#                         list(id_mapping_inv_index.keys()),
#                         key=lambda alias: min([
#                             edit_distance(msg[i + j:], alias) for j in range(t)
#                         ])
#                     )
#                 )
#
#             for sorted_alias_list in sorted_alias_lists:
#                 hints = sorted_alias_list[:4]
#                 server.reply(info_,
#                              'You are mentioning an invalid player id. Do you mean {} ?'.format(str(hints)[1:-1]))
#
#             # --------
#
#         else:
#             error('Message sender thread is not alive. Cannot repeat outbound message.')
#     elif msg.upper() == COMMAND_RESET.upper():
#         # !PYMC reset
#         info(aqua('Disabling...'), True)
#         disable()
#         info(aqua('Enabling...'), True)
#         r = init()
#         if r:
#             info(aqua('Reloaded. Type "{cmd}" to check working status.'.format(cmd=COMMAND_STATUS)), True)
#         else:
#             info(red('Failed to reload. Report this to Keuin.'), True)
#     elif msg.upper() == COMMAND_STATUS.upper():
#         # !PYMC
#         server.say('Waiting for ping response...')
#
#         # ping
#         ping = redis_ping()
#         if ping:
#             ping = green('Fine')
#         else:
#             ping = red('Timed Out')
#
#         # check threads
#
#         sender_alive = green('Alive')
#         if not isinstance(sender_thread, MessageSenderThread):
#             sender_alive = red('N/A')
#         elif not sender_thread.is_alive():
#             sender_alive = red('Dead')
#
#         receiver_alive = green('Alive')
#         if not isinstance(receiver_thread, MessageReceiverThread):
#             receiver_alive = red('N/A')
#         elif not sender_thread.is_alive():
#             receiver_alive = red('Dead')
#
#         queue_len = red('N/A')
#         if isinstance(sender_thread, MessageSenderThread):
#             queue_len = sender_thread.length()
#             if queue_len < 0 or queue_len > 4:
#                 queue_len = red(queue_len)
#             elif queue_len <= 1:
#                 queue_len = green(queue_len)
#             else:
#                 queue_len = yellow(queue_len)
#
#         server.say((''
#                     '==== PyMC2Redis Status ====\n'
#                     'Version: {ver}\n'
#                     'Ping: {ping}\n'
#                     'Sender Thread: {sender}\n'
#                     'Receiver Thread: {receiver}\n'
#                     'Queue Length: {queue_len}\n'
#                     'Counter (in/out/failed): {counter_in}/{counter_out}/{counter_failed}\n'
#                     '==== PyMC2Redis Status ====').format(
#             ver=VERSION,
#             ping=ping,
#             counter_in=counter_message_to_game,
#             counter_out=counter_message_to_redis,
#             counter_failed=counter_send_failure,
#             queue_len=queue_len,
#             sender=sender_alive,
#             receiver=receiver_alive
#         ))

# def on_player_joined(server, player):
#     if not enabled:
#         return
#     msg = Message.from_server_console_echo('{} joined the game.'.format(player), '登录')
#     if isinstance(sender_thread, MessageSenderThread):
#         # If the message sender thread is alive.
#         sender_thread.push(msg)
#
#
# def on_player_left(server, player):
#     if not enabled:
#         return
#     msg = Message.from_server_console_echo('{} left the game.'.format(player), '离开')
#     if isinstance(sender_thread, MessageSenderThread):
#         # If the message sender thread is alive.
#         sender_thread.push(msg)

def on_death_message(server, death_message):
    # if not enabled:
    #     return
    translated_death_message = translate(language[translating[CFG_TRANSLATION_FROM]],
                                         language[translating[CFG_TRANSLATION_TO]], death_message)
    log('translation: {} -> {}'.format(death_message, translated_death_message))
    msg = Message.from_server_console_echo(translated_death_message if translated_death_message else death_message,
                                           HEAD_PLAYER_DIE)
    if not translated_death_message:
        warn('Failed to translate the death message. Use origin message instead.')
    microapi_message(msg)
    # if isinstance(sender_thread, MessageSenderThread):
    #     # If the message sender thread is alive.
    #     sender_thread.push(msg)

def on_player_made_advancement(server, player, advancement):
    # if not enabled:
    #     return
    translated_advancement = translate(language[translating[CFG_TRANSLATION_FROM]],
                                       language[translating[CFG_TRANSLATION_TO]], advancement)
    log('translation: {} -> {}'.format(advancement, translated_advancement))
    if not translated_advancement:
        warn('Failed to translate the advancement name. Use origin name instead.')
    msg = Message.from_server_console_echo(
        '{player_id}达成成就{advancement}'.format(
            player_id=player,
            advancement=translated_advancement if translated_advancement else advancement
        ),
        HEAD_PLAYER_ADVANCEMENT)
    microapi_message(msg)
    # if isinstance(sender_thread, MessageSenderThread):
    #     # If the message sender thread is alive.
    #     sender_thread.push(msg)

def microapi_message(message: Message):
    if not isinstance(message, Message):
        raise ValueError('message must be a Message')
    try:
        requests.post('http://localhost:7000/message', data=json.dumps({
            'sender': message.get_sender(),
            'message': message.get_body()
        }))
    except IOError as e:
        error(f'Failed to send to MicroApi: {e}')