latexbot/bot.py

294 lines
14 KiB
Python

import discord
import requests
import json
import urllib.parse
import datetime
import subprocess
import os
import binascii
from loguru import logger
TOKEN = 'OTQwMDY3MDk5NDI1MDA5NzU1.YgB_9Q.u95iaPy2gix5ZqrQFA_rmyK5Bzw'
IS_BOT = False # Set to false if this is a selfbot
DO_TEST = False # Set to True if you want to run test
ACTIVE_PHRASE = '$rtex' # Change to trigger phrase
RAW_ACTIVE_PHRASE = '$rawtex' # Change to trigger phrase for raw mode
EQUATION_ACTIVE_PHRASE = ';;' # Change to trigger phrase for an equation
WOLFRAM_APPKEY = 'X36K7Y-JT8GXU5698' # Change to your Wolfram|Alpha api key
logger.info('initializing', 'bot' if IS_BOT else 'selfbot', 'mode')
client = discord.Client()
def upload_to_term(content):
logger.info('uploading content to pastebin')
try:
c = subprocess.run(['nc', 'termbin.com', '9999'], capture_output=True, check=True, text=True, input=content)
except subprocess.CalledProcessError as e:
logger.error('failed to upload to pastebin: ' + e.content)
return None
return c.stdout
@client.event
async def on_ready():
logger.info('ready')
await client.change_presence(activity=discord.Game('with math | v1.24.1'))
tex_wrapper_start = '\\documentclass{article}\n\\usepackage{xcolor}\n\\usepackage{amsmath}\n\\begin{document}\n\\color{gray}\n'
tex_wrapper_end = '\n\\pagenumbering{gobble}\n\\end{document}'
def wrap_tex(tex):
return tex_wrapper_start + tex + tex_wrapper_end
RTEX = 'https://rtex.probablyaweb.site/'
def render_tex(tex):
logger.info('accepted job')
logger.info('wrapping tex')
tex = wrap_tex(tex)
logger.info('building request to rtex api')
payload = {
"code": tex,
"format": "png"
}
logger.info('build payload', payload)
logger.info(f'contacting api ({RTEX})')
logger.info('waiting for response')
response = requests.post(RTEX + 'api/v2', data=payload)
response.raise_for_status()
logger.info('decoding response')
data = response.json()
c = None
if data['status'] != 'success':
logger.error('job failed')
c = upload_to_term(data['log'])
return (False, c)
logger.info('fetching file')
uri = RTEX + 'api/v2/' + data['filename']
logger.info(f'render job done {uri}')
return (True, uri)
async def wolfram_get_plot_implicit(equ, qurl):
url = f'https://api.wolframalpha.com/v2/query?appid={WOLFRAM_APPKEY}&input={equ}&format=image&output=json&includepodid=ImplicitPlot'
logger.info('querying for implicit plot')
response = requests.get(url)
response.raise_for_status()
logger.info('decoding response')
data = response.json()
if data['queryresult']['success'] != True:
logger.error('query failed:', data['queryresult']['error'])
return (False, 'Sorry, but Wolfram|Alpha returned an error. Try checking on wolframalpha.com: ' + qurl)
if data['queryresult']['numpods'] > 1:
logger.error('unexpectected result, more than 1 pod')
return (False, 'Sorry, but Wolfram|Alpha returned too much data for the bot to process. Try checking on wolframalpha.com: ' + qurl)
if data['queryresult']['numpods'] < 1:
logger.warning('no graph')
return (False, 'Sorry, but I couldn\'t find any plots for that equation.')
plotpod = data['queryresult']['pods'][0]
if plotpod['id'] != 'ImplicitPlot':
logger.error('unexpected result, wrong pod id')
return (False, 'Sorry, but Wolfram|Alpha returned an unexpected response. Try checking on wolframalpha.com: ' + qurl)
plotimg = plotpod['subpods'][0]['img']['src']
return (True, plotimg)
async def wolfram_get_plot(equ, qurl):
url = f'https://api.wolframalpha.com/v2/query?appid={WOLFRAM_APPKEY}&input={equ}&format=image&output=json&includepodid=Plot'
logger.info('querying for plot')
response = requests.get(url)
response.raise_for_status()
logger.info('decoding response')
data = response.json()
if data['queryresult']['success'] != True:
logger.error('query failed:', data['queryresult']['error'])
return (False, 'Sorry, but Wolfram|Alpha returned an error. Try checking on wolframalpha.com: ' + qurl)
if data['queryresult']['numpods'] > 1:
logger.error('unexpectected result, more than 1 pod')
return (False, 'Sorry, but Wolfram|Alpha returned too much data for the bot to process. Try checking on wolframalpha.com: ' + qurl)
if data['queryresult']['numpods'] < 1:
logger.warning('no graph')
return (False, 'Sorry, but I couldn\'t find any plots for that equation.')
plotpod = data['queryresult']['pods'][0]
if plotpod['id'] != 'Plot':
logger.error('unexpected result, wrong pod id')
return (False, 'Sorry, but Wolfram|Alpha returned an unexpected response. Try checking on wolframalpha.com: ' + qurl)
plotimg = plotpod['subpods'][0]['img']['src']
return (True, plotimg)
async def wolfram_get_plot_3d(equ, qurl):
url = f'https://api.wolframalpha.com/v2/query?appid={WOLFRAM_APPKEY}&input={equ}&format=image&output=json&includepodid=3DPlot'
logger.info('querying for plot')
response = requests.get(url)
response.raise_for_status()
logger.info('decoding response')
data = response.json()
if data['queryresult']['success'] != True:
logger.error('query failed:', data['queryresult']['error'])
return (False, 'Sorry, but Wolfram|Alpha returned an error. Try checking on wolframalpha.com: ' + qurl)
if data['queryresult']['numpods'] > 1:
logger.error('unexpectected result, more than 1 pod')
return (False, 'Sorry, but Wolfram|Alpha returned too much data for the bot to process. Try checking on wolframalpha.com: ' + qurl)
if data['queryresult']['numpods'] < 1:
logger.warning('no graph')
return (False, 'Sorry, but I couldn\'t find any plots for that equation.')
plotpod = data['queryresult']['pods'][0]
if plotpod['id'] != '3DPlot':
logger.error('unexpected result, wrong pod id')
return (False, 'Sorry, but Wolfram|Alpha returned an unexpected response. Try checking on wolframalpha.com: ' + qurl)
plotimg = plotpod['subpods'][0]['img']['src']
return (True, plotimg)
async def wolfram_graph3d(equation, ctx):
logger.info('job accepted, equ: ' + equation)
urlencoded = urllib.parse.quote(equation, safe='')
logger.debug('urlencoded is ' + urlencoded)
qurl = 'https://wolframalpha.com/input?i=' + urlencoded
logger.debug('qurl is ' + qurl)
logger.debug('done, creating embed')
success, msg = await wolfram_get_plot_3d(urlencoded, qurl)
if not success:
logger.error('failed')
await ctx.reply('Sorry, but something went wrong while rendering your equation:\n```' + msg + '\n```')
return
else:
await ctx.reply(msg)
return
async def wolfram_graph(equation, ctx):
logger.info('job accepted, equ: ' + equation)
urlencoded = urllib.parse.quote(equation, safe='')
logger.debug('urlencoded is ' + urlencoded)
qurl = 'https://wolframalpha.com/input?i=' + urlencoded
logger.debug('qurl is ' + qurl)
logger.debug('done, creating embed')
success, msg = await wolfram_get_plot(urlencoded, qurl)
if not success:
logger.info('try 1 failed, attempting implicit plotting')
success, msgi = await wolfram_get_plot_implicit(urlencoded, qurl)
if not success:
logger.error('failed')
await ctx.reply('Sorry, but something went wrong while rendering your equation:\n```' + msg + '\n' + msgi + '\n```')
return
logger.info('implicit plot found')
await ctx.reply(msgi)
return
else:
await ctx.reply(msg)
return
# selfbots cant use embeds :(
#imgurl = plotimg
#queryurl = 'https://wolframalpha.com/input?i=' + urlencoded
#embed = discord.Embed(
# title='Graph: ' + equation,
# url=queryurl,
# timestamp=datetime.datetime.now(),
# footer='Made with <3 by c0repwn3r',
# description='Here\'s your plot!'
#)
#embed.set_author(name='Wolfram|Alpha', icon_url='https://wolframalpha.com/favicon.ico')
#embed.set_image(url=imgurl)
#await ctx.channel.send(imgurl)
#return
def test_render_engine():
logger.info('testing rendering engine')
if render_tex('lbtest')[0] == False:
logger.warning('render test 1 failed, attempting render 2')
if render_tex('ftest')[0] == False:
logger.error('render test failed, exiting')
exit(-1)
logger.info('render test successful')
if DO_TEST:
test_render_engine()
else:
logger.info('skipping rendering test')
logger.info('creating msg event handler')
@client.event
async def on_message(message):
if message.author == client.user:
logger.debug('ignoring my own message')
return
if message.content in ['.authtest', '.help', '.embed', '.dumpconfig', '.writeconfig'] or message.content.startswith('.graphequ3d') or message.content.startswith('.writeconfig') or ACTIVE_PHRASE in message.content or RAW_ACTIVE_PHRASE in message.content or len(message.content.split(EQUATION_ACTIVE_PHRASE)) > 2 or message.content.startswith('.graphequ'):
async with message.channel.typing():
await process_message(message)
async def process_message(message):
global config
logger.info('processing message')
if message.author == client.user:
logger.debug('ignoring my own message')
return
if message.content == '.dumpconfig':
await message.reply(json.dumps(config))
return
elif message.content == '.authtest':
await message.reply(f'Hi there! Unfortunately, I\'m not authorized to issue authentication codes for you. Check with core if you should be able to get one. Sorry :/')
# await message.reply(f'Hi! Your auth code is `{binascii.b2a_hex(os.urandom(3))[2:-1]}`. It will be valid for 60 seconds.\n**Nobody should be asking you for this code. Ensure that your browser shows the URL https://hotel.security.internalwg.e3t.cc/discord_3fa, otherwise you might compromise access to the VPN.**')
return
elif message.content.startswith('.writeconfig'):
newconfig = ' '.join(message.content.split(' ')[1:])
config = newconfig
write_config()
await message.reply('Updated config.')
return
elif message.content.startswith('.graphequ3d'):
equation = message.content[12:]
logger.info('job accepted (equation', equation, 'TBR by Wolfram in 3d)')
await wolfram_graph3d(equation, message)
return
elif message.content.startswith('.graphequ'):
equation = message.content[10:]
logger.info('job accepted (equation', equation, 'TBR by Wolfram)')
await wolfram_graph(equation, message)
return
elif message.content == '.embed':
embed=discord.Embed(title="Test Embed", url="https://coredoes.dev", description="This is an embed to figure out what discord.py is doing.")
await message.channel.send(embed=embed)
return
elif message.content == '.help':
await message.channel.send(":wave: **Hi, I'm c0remath3r**\nI'm a math bot with several functions, but primarily rendering LaTeX expressions and querying Wolfram|Alpha right within discord. Here's how it works!\r**To render an entire message as a LaTeX document^**: place \$rtex *somewhere* in your message. It does not matter where.\nAn easier way to do this, however, is to simply **surround the equation you want to render with \;\;^** which will render it as an equation, and the rest of the message as text.\n*^: some preprocessing is applied.*\n\n**To graph an equation:** simply write `.graphequ <equation>`. This will query Wolfram|Alpha to fetch a (implicit, if nessecary) plot and will send it in the channel.\n**For 3-D graphs:** simply use `.graphequ3d <equation>` instead.\n\nHave fun!")
return
content = ''
if RAW_ACTIVE_PHRASE in message.content:
logger.info('found raw trigger phrase, processing')
logger.debug('preprocessing: removing trigger phrase')
content = message.content.replace(RAW_ACTIVE_PHRASE, '')
logger.info('rendering')
url = render_tex(content)
if url[0]:
logger.error('rendering failed, notifying user')
await message.reply('Sorry, but I failed to render your message. Please try again later or contact core.\nYou can also check the render log: ' + url[1])
else:
logger.info('rendered, replying')
await message.reply(url[1])
if len(message.content.split(EQUATION_ACTIVE_PHRASE)) >= 3:
logger.info('found equation message, processing')
logger.debug('preprocessing: modifying equation phrases')
content = message.content.replace(EQUATION_ACTIVE_PHRASE, '$')
logger.info('rendering')
url = render_tex(content)
if url[0] == False:
logger.error('rendering failed, notifying user')
await message.reply('Sorry, but I failed to render your message. Please try again later or contact core.\nYou can also check the render log: ' + url[1])
else:
logger.info('rendered, replying')
await message.reply(url[1])
if ACTIVE_PHRASE in message.content:
logger.info('found trigger phrase, processing')
logger.debug('preprocessing (trigger phrase removal)')
content = message.content.replace(ACTIVE_PHRASE, '')
logger.info('rendering')
url = render_tex(content)
if url[0] == False:
logger.error('rendering failed, notifying user')
await message.reply('Sorry, but I failed to render your message. Please try again later or contact core.\nYou can also check the render log: ' + url[1])
else:
logger.info('rendered, replying')
await message.reply(url[1])
logger.info('ready to initialize, logging in')
client.run(TOKEN)