lateximgbot

[python] Latex image Telegram bot
git clone https://git.torresjrjr.com/lateximgbot.git
Log | Files | Refs | README | LICENSE

bot.py (6356B)


      1 #!/usr/bin/python
      2 """
      3 This is a Telegram Bot which returns rendered images of LaTeX using an API.
      4 """
      5 
      6 import telegram
      7 from telegram.ext import Updater, MessageHandler, CommandHandler, Filters
      8 from telegram.error import (TelegramError, Unauthorized, BadRequest, 
      9                             TimedOut, ChatMigrated, NetworkError)
     10 import datetime
     11 import urllib
     12 import logging
     13 import os
     14 import signal
     15 import random
     16 
     17 logging.basicConfig(
     18     filename="log", filemode='a',
     19     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
     20     level=logging.INFO,
     21 )
     22 
     23 
     24 # CONSTANTS
     25 
     26 VERSION = "0.0.1"
     27 
     28 try:
     29     with open('token') as TokenFile:
     30         TOKEN = TokenFile.read().splitlines()[0]
     31 except FileNotFoundError:
     32     print(
     33 """
     34 You need a bot token to run an instance of this Telegram bot.
     35 Please make a file named 'token' with your bot token in there,
     36 in the same folder as this bot file.
     37 
     38 Learn more at t.me/botfather
     39 """ )
     40     quit()
     41 
     42 IMAGE_TYPE = "png"
     43 IMAGE_DPI  = "512"
     44 API = f"https://latex.codecogs.com/{IMAGE_TYPE}.latex?%%5C{IMAGE_DPI}dpi%%20%s"
     45 EXAMPLE_LATEX = r"\text{The quadratic formula} \\ " + "\n" + \
     46                 r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"
     47 
     48 
     49 # UTIL
     50 
     51 # Returns a iso-formatted string of datetime at moment of call.
     52 dt_now = lambda: datetime.datetime.now().isoformat(timespec="seconds")
     53 
     54 def logger_dec(old_cb_func):
     55 
     56     def new_cb_func(upd, ctx):
     57         msg     = upd.message.text
     58         name    = upd.message.from_user.name
     59         log_msg = f"{name}\t:: {msg}"
     60         logging.info(log_msg)
     61         print(log_msg)
     62 
     63         return old_cb_func(upd, ctx)
     64 
     65     return new_cb_func
     66 
     67 
     68 def get_random_example():
     69     example_lines  = []
     70     recording      = False
     71 
     72     with open("examples.tex", 'r') as File:
     73         line = next(File)
     74         if not line.startswith("%TOTAL"):
     75             raise Exception("No '%TOTAL N' on first line of examples.tex")
     76         else:
     77             total_examples = int( line.split()[1] )
     78             example_number = random.randint(1, total_examples)
     79 
     80         for line in File:
     81             if line.startswith(f"%BEGIN {example_number}"):
     82                 recording = True
     83                 continue
     84 
     85             if line.startswith("%END") and recording:
     86                 recording = False
     87                 break
     88 
     89             if recording:
     90                 example_lines += [line]
     91 
     92     example = ''.join(example_lines)
     93     return example
     94 
     95 
     96 # CALLBACK HANDLERS
     97 
     98 @logger_dec
     99 def cb_start(upd, ctx):
    100     bot_username = ctx.bot.get_me().username.replace('_', '\\_')
    101 
    102     # outgoing text
    103     out_msg = f"""\
    104 @{bot_username} renders _LaTeX_ text and formulae.
    105 
    106 Try:  `\\texttt{{Hello, world!}}`
    107 
    108 See /random for examples
    109 See /help for more
    110 See /about
    111 """
    112 
    113     # send message
    114     upd.message.reply_text(
    115         out_msg,
    116         parse_mode=telegram.ParseMode.MARKDOWN,
    117     )
    118 
    119 
    120 def cb_help(upd, ctx):
    121     cb_start(upd, ctx)
    122 
    123 
    124 @logger_dec
    125 def cb_about(upd, ctx):
    126     """
    127     /about callback
    128     """
    129     bot_username = ctx.bot.get_me().username.replace('_', '\\_')
    130 
    131     # Outgoing text
    132     out_msg = f"""\
    133 @{bot_username} (@LatexImgBot\_updates)
    134 
    135 Version
    136     {VERSION}
    137 Source code
    138     https://sr.ht/~torresjrjr/linkchanbot
    139 Maintainer
    140     @torresjrjr <b@torresjrjr.com>
    141 License
    142     GNU Affero General Public License
    143 """
    144 
    145     # Send message
    146     upd.message.reply_text(
    147         out_msg,
    148         parse_mode=telegram.ParseMode.MARKDOWN,
    149     )
    150 
    151 
    152 @logger_dec
    153 def cb_random(upd, ctx):
    154     latex = get_random_example()
    155 
    156     encoded_latex = urllib.parse.quote(latex)
    157     latex_url     = API % encoded_latex
    158 
    159     caption = f"`{latex}`"
    160 
    161     ctx.bot.send_photo(
    162         chat_id    = upd.effective_chat.id,
    163         photo      = latex_url,
    164         caption    = caption,
    165         parse_mode = telegram.ParseMode.MARKDOWN,
    166     )
    167 
    168 
    169 @logger_dec
    170 def handler(upd, ctx, kind="standard"):
    171     msg = upd.message.text
    172 
    173     if kind == "link": latex = msg.replace("/link","")
    174     else             : latex = msg
    175 
    176     encoded_latex = urllib.parse.quote(latex)
    177     latex_url     = API % encoded_latex
    178 
    179     if   kind == "standard": caption = f"`{latex}`"
    180     elif kind == "link"    : caption = f"`{latex}`\n" + latex_url
    181 
    182     ctx.bot.send_photo(
    183         chat_id    = upd.effective_chat.id,
    184         photo      = latex_url,
    185         caption    = caption,
    186         parse_mode = telegram.ParseMode.MARKDOWN,
    187     )
    188 
    189 
    190 cb_link    = lambda upd, ctx: handler(upd, ctx, kind="link")
    191 cb_handler = lambda upd, ctx: handler(upd, ctx)
    192 
    193 @logger_dec
    194 def cb_admin(upd, ctx):
    195     msg     = upd.message.text
    196     name    = upd.message.from_user.name
    197     log_msg = f"{name}\t:: {msg}"
    198     print(log_msg)
    199 
    200     username = upd.message.from_user.username
    201     if username == "torresjrjr":
    202         upd.message.reply_text(
    203             "Admin authorised. Sending SIGINT...",
    204             parse_mode=telegram.ParseMode.MARKDOWN,
    205         )
    206         os.kill(os.getpid(), signal.SIGINT)
    207 
    208 
    209 def cb_error(update, context):
    210     try:
    211         raise context.error
    212     except Unauthorized as e:
    213         print("Unauthorized Error:", e)  # remove update.message.chat_id from conversation list
    214     except BadRequest as e:
    215         print("BadRequest Error:", e)  # handle malformed requests - read more below!
    216     except TimedOut as e:
    217         print("TimedOut Error:", e)  # handle slow connection problems
    218     except NetworkError as e:
    219         print("NetworkError Error:", e)  # handle other connection problems
    220     except ChatMigrated as e:
    221         print("ChatMigrated Error:", e)  # the chat_id of a group has changed, use e.new_chat_id instead
    222     except TelegramError as e:
    223         print("TelegramError Error:", e)  # handle all other telegram related errors
    224 
    225 
    226 # MAIN
    227 
    228 def main():
    229     print("Starting bot...")
    230 
    231     updater = Updater(TOKEN, use_context=True)
    232 
    233     dp = updater.dispatcher
    234     dp.add_error_handler(cb_error)
    235     dp.add_handler(CommandHandler('start'     , cb_start))
    236     dp.add_handler(CommandHandler('help'      , cb_help))
    237     dp.add_handler(CommandHandler('about'     , cb_about))
    238     dp.add_handler(CommandHandler('link'      , cb_link))
    239     dp.add_handler(CommandHandler('random'    , cb_random))
    240     dp.add_handler(CommandHandler('admin'     , cb_admin))
    241     dp.add_handler(MessageHandler(Filters.text, cb_handler))
    242 
    243     print(dt_now(), "Serving...")
    244 
    245     updater.start_polling()
    246     updater.idle()
    247 
    248     print(dt_now(), "Ended.")
    249 
    250 
    251 if __name__=='__main__':
    252     main()