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
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)
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]
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)