Ejemplo n.º 1
0
class MessageQueryResponseDispatcher():

    logger = LoggingServer.getInstance()

    def __init__(self, bot, latexConverter, resourceManager):
        self._bot = bot
        self._latexConverter = latexConverter
        self._resourceManager = resourceManager

    def dispatchMessageQueryResponse(self, message):

        self.logger.debug("Received message: "+message.text+\
                   ", id: "+str(message.message_id)+", from: "+str(message.chat.id))

        responder = Process(target=self.respondToMessageQuery, args=[message])
        responder.start()
        Thread(target=self.joinProcess, args=[responder]).start()

    def joinProcess(self, process):
        process.join()

    def respondToMessageQuery(self, message):
        senderId = message.from_user.id
        chatId = message.chat.id
        messageId = message.message_id
        expression = message.text

        errorMessage = None
        try:
            imageStream, pdfStream = self._latexConverter.convertExpression(
                expression,
                senderId,
                str(messageId) + str(senderId),
                returnPdf=True)
            self._bot.sendDocument(chatId,
                                   pdfStream,
                                   filename="expression.pdf")
            self._bot.sendPhoto(chatId, imageStream)
        except ValueError as err:
            errorMessage = self.getWrongSyntaxResult(expression, err.args[0])
        except TelegramError as err:
            errorMessage = self._resourceManager.getString(
                "telegram_error") + str(err)
            self.logger.warn(errorMessage)
        except Exception as err:
            self.logger.warn("Uncaught exception: " + str(err))
        finally:
            if not errorMessage is None:
                self._bot.sendMessage(chatId, errorMessage)
            self.logger.debug(
                "Answered to message from %d, chatId %d, expression: %s",
                senderId, chatId, expression)

    def getWrongSyntaxResult(self, message, latexError):
        self.logger.debug("Wrong syntax in the message")
        errorMessage = self._resourceManager.getString("latex_syntax_error")
        return errorMessage + "\n" + latexError
Ejemplo n.º 2
0
class LatexConverter():

    logger = LoggingServer.getInstance()

    def __init__(self, preambleManager, userOptionsManager):
        self._preambleManager = preambleManager
        self._userOptionsManager = userOptionsManager

    def extractBoundingBox(self, dpi, pathToPdf):
        bbox = check_output("gs -q -dBATCH -dNOPAUSE -sDEVICE=bbox " +
                            pathToPdf,
                            stderr=STDOUT,
                            shell=True).decode("ascii")
        bounds = [
            int(_)
            for _ in bbox[bbox.index(":") + 2:bbox.index("\n")].split(" ")
        ]
        llc = bounds[:2]
        ruc = bounds[2:]
        size_factor = dpi / 72
        width = (ruc[0] - llc[0]) * size_factor
        height = (ruc[1] - llc[1]) * size_factor
        translation_x = llc[0]
        translation_y = llc[1]
        if width == 0 or height == 0:
            self.logger.warn("Expression had zero width/height bbox!")
            raise ValueError("Empty expression!")
        return width, height, -translation_x, -translation_y

    def correctBoundingBoxAspectRaito(self,
                                      dpi,
                                      boundingBox,
                                      maxWidthToHeight=3,
                                      maxHeightToWidth=1):
        width, height, translation_x, translation_y = boundingBox
        size_factor = dpi / 72
        if width > maxWidthToHeight * height:
            translation_y += (width / maxWidthToHeight -
                              height) / 2 / size_factor
            height = width / maxWidthToHeight
        elif height > maxHeightToWidth * width:
            translation_x += (height / maxHeightToWidth -
                              width) / 2 / size_factor
            width = height / maxHeightToWidth
        return width, height, translation_x, translation_y

    def getError(self, log):
        for idx, line in enumerate(log):
            if line[:2] == "! ":
                return "".join(log[idx:idx + 2])

    def pdflatex(self, fileName):
        try:
            check_output([
                'pdflatex', "-interaction=nonstopmode", "-output-directory",
                "build", fileName
            ],
                         stderr=STDOUT,
                         timeout=5).decode("ascii")
        except CalledProcessError as inst:
            with open(fileName[:-3] + "log", "r") as f:
                msg = self.getError(f.readlines())
                self.logger.debug(msg)
                raise ValueError(msg)
        except TimeoutExpired:
            msg = "Pdflatex has likely hung up and had to be killed. Congratulations!"
            raise ValueError(msg)

    def cropPdf(self, sessionId):
        bbox = check_output(
            "gs -q -dBATCH -dNOPAUSE -sDEVICE=bbox build/expression_file_%s.pdf"
            % sessionId,
            stderr=STDOUT,
            shell=True).decode("ascii")
        bounds = tuple([
            int(_)
            for _ in bbox[bbox.index(":") + 2:bbox.index("\n")].split(" ")
        ])

        command_crop = 'gs -o build/expression_file_cropped_%s.pdf -sDEVICE=pdfwrite\
                         -c "[/CropBox [%d %d %d %d]"   -c " /PAGES pdfmark" -f build/expression_file_%s.pdf'\
                         %((sessionId,)+bounds+(sessionId,))
        check_output(command_crop, stderr=STDOUT, shell=True)

    def convertPdfToPng(self, dpi, sessionId, bbox):
        command = 'gs  -o build/expression_%s.png -r%d -sDEVICE=pngalpha  -g%dx%d  -dLastPage=1 \
                    -c "<</Install {%d %d translate}>> setpagedevice" -f build/expression_file_%s.pdf'\
                    %((sessionId, dpi)+bbox+(sessionId,))
        check_output(command, stderr=STDOUT, shell=True)

    def convertExpressionToPng(self,
                               expression,
                               userId,
                               sessionId,
                               returnPdf=False):

        preamble = ""
        try:
            preamble = self._preambleManager.getPreambleFromDatabase(userId)
            self.logger.debug("Preamble for userId %d found", userId)
        except KeyError:
            self.logger.debug(
                "Preamble for userId %d not found, using default preamble",
                userId)
            preamble = self._preambleManager.getDefaultPreamble()

        fileString = preamble + "\n\\begin{document}\n" + expression + "\n\\end{document}"

        with open("build/expression_file_%s.tex" % sessionId, "w+") as f:
            f.write(fileString)

        dpi = self._userOptionsManager.getDpiOption(userId)

        try:
            self.pdflatex("build/expression_file_%s.tex" % sessionId)

            bbox = self.extractBoundingBox(
                dpi, "build/expression_file_%s.pdf" % sessionId)
            bbox = self.correctBoundingBoxAspectRaito(dpi, bbox)
            self.convertPdfToPng(dpi, sessionId, bbox)

            self.logger.debug("Generated image for %s", expression)

            with open("build/expression_%s.png" % sessionId, "rb") as f:
                imageBinaryStream = io.BytesIO(f.read())

            if returnPdf:
                self.cropPdf(sessionId)
                with open("build/expression_file_cropped_%s.pdf" % sessionId,
                          "rb") as f:
                    pdfBinaryStream = io.BytesIO(f.read())
                return imageBinaryStream, pdfBinaryStream
            else:
                return imageBinaryStream

        finally:
            check_output(["rm build/*_%s.*" % sessionId],
                         stderr=STDOUT,
                         shell=True)
Ejemplo n.º 3
0
class InLaTeXbot():

    logger = LoggingServer.getInstance()

    def __init__(self, updater, devnullChatId=-1):

        self._updater = updater
        self._resourceManager = ResourceManager()
        self._userOptionsManager = UserOptionsManager()
        self._usersManager = UsersManager()
        self._preambleManager = PreambleManager(self._resourceManager)
        self._latexConverter = LatexConverter(self._preambleManager, self._userOptionsManager)
        self._inlineQueryResponseDispatcher = InlineQueryResponseDispatcher(updater.bot, self._latexConverter, self._resourceManager, self._userOptionsManager, devnullChatId)
        self._messageQueryResponseDispatcher = MessageQueryResponseDispatcher(updater.bot, self._latexConverter, self._resourceManager)
        self._devnullChatId = devnullChatId
        self._messageFilters = []

        self._updater.dispatcher.add_handler(CommandHandler('start', self.onStart))
        self._updater.dispatcher.add_handler(CommandHandler('abort', self.onAbort))
        self._updater.dispatcher.add_handler(CommandHandler("help", self.onHelp))
        self._updater.dispatcher.add_handler(CommandHandler('setcustompreamble', self.onSetCustomPreamble))
        self._updater.dispatcher.add_handler(CommandHandler('getmypreamble', self.onGetMyPreamble))
        self._updater.dispatcher.add_handler(CommandHandler('getdefaultpreamble', self.onGetDefaultPreamble))
        self._updater.dispatcher.add_handler(CommandHandler('setcodeincaptionon', self.onSetCodeInCaptionOn))
        self._updater.dispatcher.add_handler(CommandHandler('setcodeincaptionoff', self.onSetCodeInCaptionOff))
        self._updater.dispatcher.add_handler(CommandHandler("setdpi", self.onSetDpi))
        self._updater.dispatcher.add_handler(MessageHandler(Filters.text, self.dispatchTextMessage), 1)
        
        self._messageFilters.append(self.filterPreamble)
        self._messageFilters.append(self.filterExpression)
        
        inline_handler = InlineQueryHandler(self.onInlineQuery)
        self._updater.dispatcher.add_handler(inline_handler)
        
        self._usersRequestedCustomPreambleRegistration = set()
        
    def launch(self):
        self._updater.start_polling()
        
    def stop(self):
        self._updater.stop()
    
    def onStart(self, bot, update):
        senderId = update.message.from_user.id 
        if not senderId in self._usersManager.getKnownUsers():
            self._usersManager.setUser(senderId, {})
            self.logger.debug("Added a new user to database")
            
        update.message.reply_text(self._resourceManager.getString("greeting_line_one"))
        with open("resources/demo.png", "rb") as f:
            self._updater.bot.sendPhoto(update.message.from_user.id, f)
        update.message.reply_text(self._resourceManager.getString("greeting_line_two"))

    def onAbort(self, bot, update):
        senderId = update.message.from_user.id
        try:
            self._usersRequestedCustomPreambleRegistration.remove(senderId)
            update.message.reply_text(self._resourceManager.getString("preamble_registration_aborted"))            
        except ValueError:
            update.message.reply_text(self._resourceManager.getString("nothing_to_abort"))            
            
    def onHelp(self, bot, update):
        with open("resources/available_commands.html", "r") as f:
            update.message.reply_text(f.read(), parse_mode="HTML")
        
    def onGetMyPreamble(self, bot, update):
        try:
            preamble = self._preambleManager.getPreambleFromDatabase(update.message.from_user.id)
            update.message.reply_text(self._resourceManager.getString("your_preamble_custom")+preamble)
        except KeyError:
            preamble = self._preambleManager.getDefaultPreamble()
            update.message.reply_text(self._resourceManager.getString("your_preamble_default")+preamble)
            
    def onGetDefaultPreamble(self, bot, update):
        preamble = self._preambleManager.getDefaultPreamble()
        update.message.reply_text(self._resourceManager.getString("default_preamble")+preamble)
    
    def onSetCustomPreamble(self, bot, update):
        self._usersRequestedCustomPreambleRegistration.add(update.message.from_user.id)
        update.message.reply_text(self._resourceManager.getString("register_preamble"))
    
    def dispatchTextMessage(self, bot, messageUpdate):
        for messageFilter in self._messageFilters:
            messageUpdate = messageFilter(bot, messageUpdate)
            if messageUpdate is None:
                # Update consumed
                return
                
    def filterPreamble(self, bot, update):
        senderId = update.message.from_user.id
        if senderId in self._usersRequestedCustomPreambleRegistration:
            self.logger.debug("Filtered preamble text message")
            self.onPreambleArrived(bot, update)
        else:
            return update
    
    def onPreambleArrived(self, bot, update):
        preamble = update.message.text
        senderId = update.message.from_user.id
        update.message.reply_text(self._resourceManager.getString("checking_preamble"))
        valid, preamble_error_message = self._preambleManager.validatePreamble(preamble)
        if valid:
            self.logger.debug("Registering preamble for user %d", senderId)
            self._preambleManager.putPreambleToDatabase(senderId, preamble)
            update.message.reply_text(self._resourceManager.getString("preamble_registered"))
            self._usersRequestedCustomPreambleRegistration.remove(senderId)
        else:
            update.message.reply_text(preamble_error_message)
            
    def filterExpression(self, bot, update):
        # Always consumes, last filter
        self.logger.debug("Filtered expression text message")
        self.onExpressionArrived(bot, update)
    
    def onExpressionArrived(self, bot, update):
        self._messageQueryResponseDispatcher.dispatchMessageQueryResponse(update.message)
        
    def onSetCodeInCaptionOn(self, bot, update):
        userId  = update.message.from_user.id
        self._userOptionsManager.setCodeInCaptionOption(userId, True)
    
    def onSetCodeInCaptionOff(self, bot, update):
        userId = update.message.from_user.id
        self._userOptionsManager.setCodeInCaptionOption(userId, False)
    
    def onSetDpi(self, bot, update):
        userId = update.message.from_user.id
        try:
            dpi = int(update.message.text[8:])
            if not 100<=dpi<=1000:
                raise ValueError("Incorrect dpi value")
            self._userOptionsManager.setDpiOption(userId, dpi)
            update.message.reply_text(self._resourceManager.getString("dpi_set")%dpi)
        except ValueError:
            update.message.reply_text(self._resourceManager.getString("dpi_value_error"))
        
    def onInlineQuery(self, bot, update):
        if not update.inline_query.query:
            return
        self._inlineQueryResponseDispatcher.dispatchInlineQueryResponse(update.inline_query)
        
    def broadcastHTMLMessage(self, message):
        var = input("Are you sure? yes/[no]: ")
        if var != "yes":
            print("Aborting!")
            return
            
        for userId in self._usersManager.getKnownUsers():
            try:
                self._updater.bot.sendMessage(userId, message, parse_mode="HTML")
            except TelegramError as err:
                self.logger.warn("Could not broadcast message for %d, error: %s", userId, str(err))
class InlineQueryResponseDispatcher():

    logger = LoggingServer.getInstance()

    def __init__(self, bot, latexConverter, resourceManager,
                 userOptionsManager, devnullChatId):
        self._bot = bot
        self._latexConverter = latexConverter
        self._resourceManager = resourceManager
        self._userOptionsManager = userOptionsManager
        self._devnullChatId = devnullChatId
        self._nextQueryArrivedEvents = {}

    def dispatchInlineQueryResponse(self, inline_query):

        self.logger.debug("Received inline query: "+inline_query.query+\
                                ", id: "+str(inline_query.id)+", from user: "******"telegram_error") + str(err)
            self.logger.warn(errorMessage)
            result = InlineQueryResultArticle(
                0, errorMessage, InputTextMessageContent(expression))
        finally:
            if not self.skipForNewerQuery(nextQueryArrivedEvent, senderId,
                                          expression):
                self._bot.answerInlineQuery(queryId, [result], cache_time=0)
                self.logger.debug(
                    "Answered to inline query from %d, expression: %s",
                    senderId, expression)

    def skipForNewerQuery(self, nextQueryArrivedEvent, senderId, expression):
        if nextQueryArrivedEvent.is_set():
            self.logger.debug(
                "Skipped answering query from %d, expression: %s; newer query arrived",
                senderId, expression)
            return True
        return False

    def getWrongSyntaxResult(self, query, latexError):
        if len(query) >= 250:
            self.logger.debug("Query may be too long")
            errorMessage = self._resourceManager.getString(
                "inline_query_too_long")
        else:
            self.logger.debug("Wrong syntax in the query")
            errorMessage = self._resourceManager.getString(
                "latex_syntax_error")
        return InlineQueryResultArticle(0,
                                        errorMessage,
                                        InputTextMessageContent(query),
                                        description=latexError)

    def uploadImage(self, image, expression, caption):
        attempts = 0
        errorMessage = None

        while attempts < 3:
            try:
                latex_picture_id = self._bot.sendPhoto(self._devnullChatId,
                                                       image).photo[0].file_id
                self.logger.debug("Image successfully uploaded for %s",
                                  expression)
                return InlineQueryResultCachedPhoto(
                    0, photo_file_id=latex_picture_id, caption=caption)
            except TelegramError as err:
                errorMessage = self._resourceManager.getString(
                    "telegram_error") + str(err)
                self.logger.warn(errorMessage)
                attempts += 1

        return InlineQueryResultArticle(0, errorMessage,
                                        InputTextMessageContent(expression))

    def generateCaption(self, senderId, expression):
        if self._userOptionsManager.getCodeInCaptionOption(senderId) is True:
            return expression[:200]
        else:
            return ""
class InlineQueryResponseDispatcher():
    logger = LoggingServer.getInstance()

    def __init__(self, bot, latexConverter, resourceManager,
                 userOptionsManager, devnullChatId):
        self._bot = bot
        self._latexConverter = latexConverter
        self._resourceManager = resourceManager
        self._userOptionsManager = userOptionsManager
        self._devnullChatId = devnullChatId
        self._nextQueryArrivedEvents = {}

    def dispatchInlineQueryResponse(self, inline_query):

        self.logger.debug("Received inline query: " + inline_query.query + \
                          ", id: " + str(inline_query.id) + ", from user: "******"_" + str(senderId))
            if not nextQueryArrivedEvent.is_set():
                result = self.uploadImage(
                    expressionPngFileStream, expression, caption,
                    self._userOptionsManager.getCodeInCaptionOption(senderId))
        except ValueError as err:
            result = self.getWrongSyntaxResult(expression, err.args[0])
        except TelegramError as err:
            errorMessage = self._resourceManager.getString(
                "telegram_error") + str(err)
            self.logger.warn(errorMessage)
            result = InlineQueryResultArticle(
                0, errorMessage, InputTextMessageContent(expression))
        finally:
            if not self.skipForNewerQuery(nextQueryArrivedEvent, senderId,
                                          expression):
                self._bot.answerInlineQuery(queryId, [result], cache_time=0)
                self.logger.debug(
                    "Answered to inline query from %d, expression: %s",
                    senderId, expression)

    def skipForNewerQuery(self, nextQueryArrivedEvent, senderId, expression):
        if nextQueryArrivedEvent.is_set():
            self.logger.debug(
                "Skipped answering query from %d, expression: %s; newer query arrived",
                senderId, expression)
            return True
        return False

    def getWrongSyntaxResult(self, query, latexError):
        if len(query) >= 250:
            self.logger.debug("Query may be too long")
            errorMessage = self._resourceManager.getString(
                "inline_query_too_long")
        else:
            self.logger.debug("Wrong syntax in the query")
            errorMessage = self._resourceManager.getString(
                "latex_syntax_error")
        return InlineQueryResultArticle(0,
                                        errorMessage,
                                        InputTextMessageContent(query),
                                        description=latexError)

    def uploadImage(self, image, expression, caption, code_in_caption):
        attempts = 0
        errorMessage = None

        while attempts < 3:
            try:
                latex_picture_id = self._bot.sendPhoto(self._devnullChatId,
                                                       image).photo[0].file_id
                self.logger.debug("Image successfully uploaded for %s",
                                  expression)

                return InlineQueryResultCachedPhoto(
                    0,
                    photo_file_id=latex_picture_id,
                    caption=caption,
                    parse_mode=ParseMode.MARKDOWN
                    if not code_in_caption else None)
            except TelegramError as err:
                errorMessage = self._resourceManager.getString(
                    "telegram_error") + str(err)
                self.logger.warn(errorMessage)
                attempts += 1

        return InlineQueryResultArticle(0, errorMessage,
                                        InputTextMessageContent(expression))

    def processMultilineComments(self, senderId, expression):
        if self._userOptionsManager.getCodeInCaptionOption(senderId) is True:
            return expression
        else:
            regex = r"^%\*"
            expression = re.sub(regex,
                                r"\\iffalse inlatexbot",
                                expression,
                                flags=re.MULTILINE)

            regex = r"\*%"
            return re.sub(regex,
                          r"inlatexbot \\fi",
                          expression,
                          flags=re.MULTILINE)

    def generateCaption(self, senderId, expression):
        if self._userOptionsManager.getCodeInCaptionOption(senderId) is True:
            return expression[:
                              200]  # no comments, return everything (max 200 symbols)
        else:
            regex = r"^%( *\S+.*?)$|\\iffalse inlatexbot\n(.+?)inlatexbot \\fi"  # searching for comments, which are then only included

            groups = re.findall(regex, expression, re.MULTILINE | re.DOTALL)

            if len(groups) == 0:
                return ""
            else:
                caption = ""
                for group in groups:
                    caption += "".join(group) + "\n"
                return caption[:-1]
Ejemplo n.º 6
0
class PreambleManager():

    logger = LoggingServer.getInstance()

    def __init__(self,
                 resourceManager,
                 preamblesFile="./resources/preambles.pkl"):
        self._resourceManager = resourceManager
        self._preamblesFile = preamblesFile
        #        self._defaultPreamble = self.readDefaultPreamble()
        self._lock = Lock()

#    def getDefaultPreamble(self):
#        return self._defaultPreamble

    def getDefaultPreamble(self):
        with open("./resources/default_preamble.txt", "r") as f:
            return f.read()

    def getPreambleFromDatabase(self, preambleId):
        with self._lock:
            with open(self._preamblesFile, "rb") as f:
                preambles = pkl.load(f)
            return preambles[preambleId]

    def putPreambleToDatabase(self, preambleId, preamble):
        with self._lock:
            with open(self._preamblesFile, "rb") as f:
                preambles = pkl.load(f)
            preambles[preambleId] = preamble
            with open(self._preamblesFile, "wb") as f:
                pkl.dump(preambles, f)

    def getError(self, log):
        for idx, line in enumerate(log):
            if line[:2] == "! ":
                return "".join(log[idx:idx + 2])

    def validatePreamble(self, preamble):
        if len(preamble) > self._resourceManager.getNumber(
                "max_preamble_length"):
            return False, self._resourceManager.getString(
                "preamble_too_long") % self._resourceManager.getNumber(
                    "max_preamble_length")

        document = preamble + "\n\\begin{document}TEST PREAMBLE\\end{document}"
        with open("./build/validate_preamble.tex", "w+") as f:
            f.write(document)
        try:
            check_output([
                'pdflatex', "-interaction=nonstopmode", "-draftmode",
                "-output-directory", "./build", "./build/validate_preamble.tex"
            ],
                         stderr=STDOUT)
            return True, ""
        except CalledProcessError as inst:
            with open("./build/validate_preamble.log", "r") as f:
                msg = self.getError(f.readlines())[:-1]
                self.logger.debug(msg)
            return False, self._resourceManager.getString(
                "preamble_invalid") + "\n" + msg
        finally:
            check_output(["rm ./build/validate_preamble.*"],
                         stderr=STDOUT,
                         shell=True)