├── .gitignore ├── template.tex ├── settings_default.json ├── chanrestrict.py ├── README.md └── latexbot.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Settings File 2 | settings.json 3 | 4 | # files used in image generation 5 | *.tex 6 | *.dvi 7 | *.log 8 | *.png 9 | 10 | # Python 11 | __pycache__ 12 | 13 | # Remove ignore for template file 14 | !template.tex 15 | 16 | #PyCharm 17 | .idea/ 18 | -------------------------------------------------------------------------------- /template.tex: -------------------------------------------------------------------------------- 1 | \documentclass[varwidth=true]{standalone} 2 | \usepackage{amsmath} 3 | \usepackage{color} 4 | \usepackage[usenames,dvipsnames,svgnames,table]{xcolor} 5 | \usepackage[utf8]{inputenc} 6 | 7 | \definecolor{dstext}{HTML}{__TEXTCOLOUR__} 8 | \definecolor{dsbackground}{HTML}{__BGCOLOUR__} 9 | 10 | \begin{document} 11 | 12 | \color{dstext} 13 | \pagecolor{dsbackground} 14 | \begin{align*} 15 | __DATA__ 16 | \end{align*} 17 | 18 | \end{document} 19 | -------------------------------------------------------------------------------- /settings_default.json: -------------------------------------------------------------------------------- 1 | { 2 | "login_method": "token", 3 | "renderer": "external", 4 | "verbose": true, 5 | 6 | "login": { 7 | "email": "user@domain.com", 8 | "password": "pa$$w0rd", 9 | "token": "your token here" 10 | }, 11 | "channels": { 12 | "whitelist": [], 13 | "blacklist": [] 14 | }, 15 | "commands": { 16 | "render": ["!tex "], 17 | "help": ["!help", "!help tex"] 18 | }, 19 | "latex": { 20 | "background-colour": "36393E", 21 | "text-colour": "DBDBDB", 22 | "dpi": "200" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /chanrestrict.py: -------------------------------------------------------------------------------- 1 | # Used to restrict the servers and channels that the bot may access. 2 | 3 | # An empty whitelist assumes the bot may access everything that's not on the blacklist. 4 | # Channel rules 'server#channel' override server rules 'server'. 5 | 6 | white = [] 7 | black = [] 8 | private = True 9 | 10 | def setup(whitelist, blacklist, allow_private = True): 11 | global white 12 | global black 13 | global private 14 | white = [i.strip().lower() for i in whitelist] 15 | black = [i.strip().lower() for i in blacklist] 16 | private = allow_private 17 | bset = set(blacklist) 18 | for i in white: 19 | if i in bset: 20 | raise ValueError('{} is in the blacklist and the whitelist'.format(i)) 21 | 22 | def check(message): 23 | allow = False 24 | if message.channel.is_private: 25 | allow = private 26 | else: 27 | ser = message.server.name.lower() 28 | chn = ser + '#' + message.channel.name.lower() 29 | if len(white) == 0: 30 | allow = True 31 | if ser in white: 32 | allow = True 33 | if ser in black: 34 | allow = False 35 | if chn in white: 36 | allow = True 37 | if chn in black: 38 | allow = False 39 | return allow -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **This project has been halted in favour of [MathBot](https://github.com/DXsmiley/mathbot).** I will still look at and accept pull requests, however it is not under active development. 2 | 3 | # Discord LaTeX Bot 4 | 5 | This is a bot for [Discord](https://discordapp.com/) that automatically renders LaTeX formulas. 6 | 7 | ## Invocation 8 | 9 | By default, the bot can be invoked with `!tex [latex code]`. Using `!help` or `!help tex` will private message the help. 10 | 11 | Example: `!tex \sqrt{a^2 + b^2} = c` 12 | 13 | ## Running 14 | 15 | To run the bot, you need [Python3](https://www.python.org/) and [discord.py](https://github.com/Rapptz/discord.py). 16 | 17 | Running the bot for the first time will produce the `settings.json` file. You should edit this. 18 | 19 | ## Settings 20 | 21 | ### Login 22 | You need either an email and passsword to login or a bot token. You can read more about that [here](https://discordapp.com/developers/docs/topics/oauth2#bot-vs-user-accounts) 23 | 24 | If you're using an email and password, under "login_method", change "token" to "account" 25 | 26 | Under "login", if you're using the account login method, change "email" and "password" to their respective value . If you're using a bot token, set "token" to be the token that discord auto generated for you. 27 | 28 | ### Channels 29 | 30 | The list of servers and channels that the bot may access. The rules are as follows: 31 | 32 | 1. If the whitelist is empty, the bot may access all channels on all servers. 33 | 2. If the whitelist is not empty, the bot may access only the *servers* on the whitelist. 34 | 3. The bot may not access any *server* on the blacklist. 35 | 4. The bot may access any *channel* on the whitelist. 36 | 5. The bot may not access any *channel* on the blacklist. 37 | 38 | Rules with larger numbers overrule the smaller ones. 39 | 40 | ### Renderer 41 | 42 | `remote` will use an external server to render the LaTeX. **I do not own or maintain this server.** 43 | Consider finding a different server. If too many people abuse it, it will be shut down. 44 | 45 | `local` will attempt to use the programs `latex` and `dvipng` to render the LaTeX locally. 46 | -------------------------------------------------------------------------------- /latexbot.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import urllib.request 3 | import random 4 | import os 5 | import json 6 | import shutil 7 | import asyncio 8 | import sys 9 | 10 | import chanrestrict 11 | 12 | LATEX_TEMPLATE="template.tex" 13 | 14 | HELP_MESSAGE = r""" 15 | Hello! I'm the *LaTeX* math bot! 16 | 17 | You can type mathematical *LaTeX* into the chat and I'll automatically render it! 18 | 19 | Simply use the `!tex` command. 20 | 21 | **Examples** 22 | 23 | `!tex x = 7` 24 | 25 | `!tex \sqrt{a^2 + b^2} = c` 26 | 27 | `!tex \int_0^{2\pi} \sin{(4\theta)} \mathrm{d}\theta` 28 | 29 | **Notes** 30 | 31 | Using the `\begin` or `\end` in the *LaTeX* will probably result in something failing. 32 | 33 | """ 34 | 35 | 36 | class LatexBot(discord.Client): 37 | #TODO: Check for bad token or login credentials using try catch 38 | def __init__(self): 39 | super().__init__() 40 | 41 | self.check_for_config() 42 | self.settings = json.loads(open('settings.json').read()) 43 | 44 | # Quick and dirty defaults of colour settings, if not already present in the settings 45 | if 'latex' not in self.settings: 46 | self.settings['latex'] = { 47 | 'background-colour': '36393E', 48 | 'text-colour': 'DBDBDB', 49 | 'dpi': '200' 50 | } 51 | 52 | chanrestrict.setup(self.settings['channels']['whitelist'], 53 | self.settings['channels']['blacklist']) 54 | 55 | # Check if user is using a token or login 56 | if self.settings['login_method'] == 'token': 57 | self.run(self.settings['login']['token']) 58 | elif self.settings['login_method'] == 'account': 59 | self.login(self.settings['login']['email'], self.settings['login']['password']) 60 | self.run() 61 | else: 62 | raise Exception('Bad config: "login_method" should set to "login" or "token"') 63 | 64 | # Check that config exists 65 | def check_for_config(self): 66 | if not os.path.isfile('settings.json'): 67 | shutil.copyfile('settings_default.json', 'settings.json') 68 | print('Now you can go and edit `settings.json`.') 69 | print('See README.md for more information on these settings.') 70 | 71 | def vprint(self, *args, **kwargs): 72 | if self.settings.get('verbose', False): 73 | print(*args, **kwargs) 74 | 75 | # Outputs bot info to user 76 | @asyncio.coroutine 77 | def on_ready(self): 78 | print('------') 79 | print('Logged in as') 80 | print(self.user.name) 81 | print(self.user.id) 82 | print('------') 83 | 84 | async def on_message(self, message): 85 | if chanrestrict.check(message): 86 | 87 | msg = message.content 88 | 89 | for c in self.settings['commands']['render']: 90 | if msg.startswith(c): 91 | latex = msg[len(c):].strip() 92 | self.vprint('Latex:', latex) 93 | 94 | num = str(random.randint(0, 2 ** 31)) 95 | if self.settings['renderer'] == 'external': 96 | fn = self.generate_image_online(latex) 97 | if self.settings['renderer'] == 'local': 98 | fn = self.generate_image(latex, num) 99 | # raise Exception('TODO: Renable local generation') 100 | 101 | if fn and os.path.getsize(fn) > 0: 102 | await self.send_file(message.channel, fn) 103 | self.cleanup_output_files(num) 104 | self.vprint('Success!') 105 | else: 106 | await self.send_message(message.channel, 'Something broke. Check the syntax of your message. :frowning:') 107 | self.cleanup_output_files(num) 108 | self.vprint('Failure.') 109 | 110 | break 111 | 112 | if msg in self.settings['commands']['help']: 113 | self.vprint('Showing help') 114 | await self.send_message(message.author, HELP_MESSAGE) 115 | 116 | # Generate LaTeX locally. Is there such things as rogue LaTeX code? 117 | def generate_image(self, latex, name): 118 | 119 | latex_file = name + '.tex' 120 | dvi_file = name + '.dvi' 121 | png_file = name + '1.png' 122 | 123 | with open(LATEX_TEMPLATE, 'r') as textemplatefile: 124 | textemplate = textemplatefile.read() 125 | 126 | with open(latex_file, 'w') as tex: 127 | backgroundcolour = self.settings['latex']['background-colour'] 128 | textcolour = self.settings['latex']['text-colour'] 129 | latex = textemplate.replace('__DATA__', latex).replace('__BGCOLOUR__', backgroundcolour).replace('__TEXTCOLOUR__', textcolour) 130 | 131 | tex.write(latex) 132 | tex.flush() 133 | tex.close() 134 | 135 | imagedpi = self.settings['latex']['dpi'] 136 | latexsuccess = os.system('latex -quiet -interaction=nonstopmode ' + latex_file) 137 | if latexsuccess == 0: 138 | os.system('dvipng -q* -D {0} -T tight '.format(imagedpi) + dvi_file) 139 | return png_file 140 | else: 141 | return '' 142 | 143 | # More unpredictable, but probably safer for my computer. 144 | def generate_image_online(self, latex): 145 | url = 'http://frog.isima.fr/cgi-bin/bruno/tex2png--10.cgi?' 146 | url += urllib.parse.quote(latex, safe='') 147 | fn = str(random.randint(0, 2 ** 31)) + '.png' 148 | urllib.request.urlretrieve(url, fn) 149 | return fn 150 | 151 | # Removes the generated output files for a given name 152 | def cleanup_output_files(self, outputnum): 153 | try: 154 | os.remove(outputnum + '.tex') 155 | os.remove(outputnum + '.dvi') 156 | os.remove(outputnum + '.aux') 157 | os.remove(outputnum + '.log') 158 | os.remove(outputnum + '1.png') 159 | except OSError: 160 | pass 161 | 162 | 163 | if __name__ == "__main__": 164 | LatexBot() 165 | --------------------------------------------------------------------------------