├── .gitignore ├── tests.sh ├── Makefile ├── requirements.txt ├── junkyard ├── example.py ├── test-bot-key.py ├── tests.rkt ├── janitor-bot.py ├── supervisor.rkt ├── dumb-bot.py ├── graph-bot.py ├── remind-bot.py ├── dotabot.go ├── FishFactBot.js ├── botinfo.py ├── hacker-bot.py └── dota-bot.py ├── config.json ├── LICENSE.md ├── bots ├── eco-bot.py ├── dumb-bot.py ├── dota-bot.py └── Bot.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.swp 3 | *.py[cod] 4 | dev 5 | *.key 6 | botdata/ 7 | node_modules/ 8 | shared/ 9 | *\~ -------------------------------------------------------------------------------- /tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | for f in bots/*.py; 4 | do 5 | python -m py_compile $f 6 | done 7 | 8 | echo "Done" 9 | # end 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | raco superv config.json 3 | virtualenv: 4 | virtualenv dev 5 | setup: 6 | pip install -r requirements.txt 7 | raco pkg install superv 8 | clean: 9 | rm -rf botdata/ 10 | test: 11 | sh tests.sh 12 | copy_keys: 13 | scp -r keys steve@alarmpi:~/discord-bots 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.7.4 2 | appdirs==1.4.3 3 | async-timeout==1.2.1 4 | beautifulsoup4==4.6.0 5 | bs4==0.0.1 6 | chardet==3.0.2 7 | discord.py==0.16.7 8 | multidict==2.1.4 9 | packaging==16.8 10 | pyparsing==2.2.0 11 | requests==2.31.0 12 | six==1.10.0 13 | websockets==9.1 14 | -------------------------------------------------------------------------------- /junkyard/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | from Bot import * 5 | from requests import get, post 6 | 7 | class Example(WebHookBot): 8 | def __init__(self, name): 9 | super(Example, self).__init__(name) 10 | self.logger(f"Endpoint: {self.endpoint}") 11 | self.SLEEP_TIME = 10 12 | 13 | def main(self): 14 | self.logger("I'm something else!") 15 | 16 | ex = Example("example") 17 | ex.run() 18 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "sleep_time": 600, 3 | "sleep_count": 12, 4 | "hard_restarts": true, 5 | "init_sleep": 30, 6 | "reset_sleep": 30, 7 | "programs": [ 8 | { 9 | "name": "Dumb Bot", 10 | "program": "python", 11 | "args": [ 12 | "bots/dumb-bot.py" 13 | ] 14 | }, 15 | { 16 | "name": "Dota2 Match Bot", 17 | "program": "python", 18 | "args": [ 19 | "bots/dota-bot.py" 20 | ] 21 | }, 22 | { 23 | "name": "Economy Bot", 24 | "program": "python", 25 | "args": [ 26 | "bots/eco-bot.py" 27 | ] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /junkyard/test-bot-key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | from botinfo import * 5 | from sys import argv 6 | 7 | client = discord.Client() 8 | 9 | @client.event 10 | async def on_ready(): 11 | print("Logged in") 12 | print("ID: {}, Name: {}".format(client.user.id, client.user.name)) 13 | client.logout() 14 | client.close() 15 | raise Exception 16 | 17 | @client.event 18 | async def on_error(err): 19 | print("Oops") 20 | client.logout() 21 | client.close() 22 | return False 23 | 24 | if __name__ == "__main__": 25 | try: 26 | k = argv[1] 27 | client.run(k) 28 | except Exception as e: 29 | print("{} fail: {}".format(key, e)) 30 | except SystemExit: 31 | pass 32 | except KeyboardInterrupt: 33 | print("Don't touch that!") 34 | finally: 35 | print("Stopping") 36 | quit() 37 | 38 | # end 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Steven Leibrock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /junkyard/tests.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | 3 | (module+ test (require rackunit racket/port "src/supervisor.rkt")) 4 | 5 | (module+ test 6 | (define (debug-args program code) 7 | (match program 8 | ("node" (list "-c" code)) 9 | ("python" (list "-m" "py_compile" code)))) 10 | (define (syscall cmds) 11 | (parameterize ([current-subprocess-custodian-mode 'kill]) 12 | (apply system* (append (list (find-executable-path (car cmds))) (cdr cmds))))) 13 | (define (sys->str cmd) (with-output-to-string (λ () (syscall cmd))))) 14 | 15 | (module+ test 16 | (test-case "Testing ability to kill subprocs" 17 | (let ([subs 18 | (build-list 19 | 10 20 | (lambda (x) 21 | (define-values (a b c d) 22 | (syscall '("sleep" "300000"))) 23 | a))]) 24 | (check = (length subs) 10 "Test size not equal") 25 | (for-each subproc-kill subs) 26 | (sleep 2) ; sleep to wait on the OS process killing 27 | (for-each 28 | (lambda (sub) 29 | (check-not-eq? 'running (subprocess-status sub))) 30 | subs))) 31 | 32 | (test-case "Testing successful compiles on all bots" 33 | (let ([subs 34 | (build-list 35 | total-bots 36 | (lambda (x) 37 | (define-values (interp code) (apply values (vector-ref bots x))) 38 | (define-values (a b c d) (syscall interp (debug-args interp code))) 39 | a))]) 40 | (check = (length subs) total-bots) 41 | (sleep 5) 42 | (for-each (lambda (sub) (check-eq? 0 (subprocess-status sub))) subs) 43 | 0)) 44 | 45 | (test-case "Test whether all packages have been installed" 46 | (let ([pip-out (sys->str '("pip" "freeze" "--disable-pip-version-check"))] 47 | [req-txt (port->lines (open-input-file "requirements.txt"))]) 48 | (displayln "Checking if packages were installed...") 49 | (check <= 50 | (length req-txt) 51 | (length (filter non-empty-string? (string-split pip-out "\n")))))) 52 | ) 53 | ; end 54 | -------------------------------------------------------------------------------- /junkyard/janitor-bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | Janitor-bot 6 | 7 | Will clean up old messages and purge channels of certain things 8 | """ 9 | 10 | from botinfo import * 11 | from random import choice 12 | 13 | bot_name = "janitor-bot" 14 | client = discord.Client() 15 | logger = create_logger(bot_name) 16 | 17 | hooks = { 18 | "text" : lambda m: len(m.attachments) == 0, 19 | "images": lambda m: len(m.attachments) > 0, 20 | } 21 | 22 | jokes = [ 23 | "I was going to attend the clairvoyants meeting, but it was canceled due to unforseen events.", 24 | "Two cannibals are eating a clown. One cannibal turns to the other and asks 'Does this taste funny to you?'", 25 | "Two atoms are in a bar. One says, 'I think I lost an electron.' The other says, 'Are you sure?' To which the other replies 'I'm positive.'", 26 | "A neutron walks into a bar. 'How much for a drink here, anyway?' To which the bartender responds, 'For you, no charge.'", 27 | "I once visited a crematorium that gave discounts for burn victims.", 28 | "Photons have mass? I didn't even know they were Catholic.", 29 | "It's common knowledge that irradiated cats have 18 half-lives.", 30 | "Did you know the best contraceptive for old people is nudity?", 31 | ] 32 | 33 | @register_command 34 | async def perms(msg, mobj): 35 | """ 36 | Check a user's permissions 37 | Print all of the user's permissions in a readable line-by-line format 38 | """ 39 | try: 40 | perms = mobj.channel.permissions_for(mobj.author) 41 | lines = [ 42 | "Administrator? {}".format(perms.administrator), 43 | "Manage server? {}".format(perms.manage_server), 44 | "Manage messages? {}".format(perms.manage_messages), 45 | "Attach files? {}".format(perms.attach_files), 46 | ] 47 | return await client.send_message(mobj.channel, pre_text("\n".join(lines))) 48 | except Exception as ex: 49 | logger("!perms: {}".format(ex)) 50 | return await client.send_message(mobj.channel, "Failed to check permissions") 51 | 52 | @register_command 53 | async def clean(msg, mobj): 54 | """ 55 | This cleans all messages from the input channel 56 | This is only a command allowed for administrators 57 | Optional argument to purge non-images from a channel 58 | """ 59 | try: 60 | perms = mobj.channel.permissions_for(mobj.author) 61 | if not perms.administrator: 62 | return await client.send_message(mobj.channel, "Admins only allowed to purge") 63 | lamb = hooks[msg] if msg in hooks else None 64 | await client.purge_from(mobj.channel, limit=30000, check=lamb) 65 | return 66 | except Exception as ex: 67 | logger("!clean: {}".format(ex)) 68 | return await client.send_message(mobj.channel, "Failed to execute action") 69 | 70 | @register_command 71 | async def joke(msg, mobj): 72 | """ 73 | Returns a random joke 74 | The real joke is this help message *ba-dum tsh* 75 | """ 76 | return await client.send_message(mobj.channel, choice(jokes)) 77 | 78 | 79 | setup_all_events(client, bot_name, logger) 80 | if __name__ == "__main__": 81 | run_the_bot(client, bot_name, logger) 82 | 83 | # end 84 | 85 | -------------------------------------------------------------------------------- /bots/eco-bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | from Bot import ChatBot 5 | from requests import get 6 | from bs4 import BeautifulSoup as BS 7 | 8 | class EcoBot(ChatBot): 9 | ''' 10 | EcoBot (Economy Bot) is a price fetcher upon-request bot 11 | He has a few commands, each one tied to the different market 12 | Searches are implemented based on relative ease and so on 13 | Results should be posted in USD (or whatever currency is relevant) 14 | ''' 15 | 16 | # These have to be set beforehand (and can't be defined locally) 17 | ChatBot.PREFIX = '$' 18 | ChatBot.STATUS = f'{ChatBot.PREFIX}help for info' 19 | 20 | STEAM_SEARCH_URL = 'http://steamcommunity.com/market/search?q={}' 21 | STOCKS_SEARCH_URL = '' 22 | RUNESCAPE_SEARCH_URL = '' 23 | 24 | ERR_NO_ITEMS = 'Failed to find any items' 25 | ERR_ITEM_URL = 'Failed to get target item URL' 26 | ERR_RESP = 'Failed search [response: {}]' 27 | 28 | def __init__(self, name): 29 | super(EcoBot, self).__init__(name) 30 | 31 | @ChatBot.action('') 32 | async def steam(self, args, mobj): 33 | ''' 34 | Search Steam market for an item 35 | Example: $steam redline ak 36 | ''' 37 | url = self.STEAM_SEARCH_URL.format('+'.join(args)) 38 | resp = get(url) 39 | if resp.status_code != 200: 40 | return await self.message(mobj.channel, self.ERR_RESP.format(resp.status_code)) 41 | 42 | search_bs = BS(resp.text, 'html.parser') 43 | item_listing = search_bs.find_all('a', class_='market_listing_row_link') 44 | if not item_listing: 45 | return await self.message(self.ERR_NO_ITEMS) 46 | 47 | url_first_item = item_listing[0].get('href', None) 48 | if url_first_item is None: 49 | return await self.message(self.ERR_ITEM_URL) 50 | 51 | """ 52 | # use this later 53 | resp2 = get(url_first_item) 54 | if resp2.status_code != 200: 55 | return await self.message(mobj.channel, f'Failed search (status: {resp.status_code})') 56 | bsp2 = BS(resp2.text, 'html.parser') 57 | """ 58 | item_name = item_listing[0].find('span', id='result_0_name') 59 | price_span = item_listing[0].find('span', class_='normal_price') 60 | normal_price = price_span.find('span', class_='normal_price') 61 | msg = [ 62 | f"Name: {item_name.text}", 63 | f"Price: {normal_price.text}", 64 | f"URL: {url_first_item}", 65 | ] 66 | return await self.message(mobj.channel, '\n'.join(msg)) 67 | 68 | @ChatBot.action('') 69 | async def scape(self, args, mobj): 70 | ''' 71 | Search 2007scape's Grand Exchange 72 | Example: $scape nature rune 73 | ''' 74 | return await self.message(mobj.channel, 'Not implemented') 75 | 76 | @ChatBot.action('') 77 | async def stock(self, args, mobj): 78 | ''' 79 | Search the actual stock market 80 | Example: $stock GOOG 81 | ''' 82 | return await self.message(mobj.channel, 'Not implemented') 83 | 84 | # end bot def 85 | 86 | if __name__ == '__main__': 87 | EcoBot('eco-bot').run() 88 | pass 89 | 90 | # end 91 | 92 | 93 | -------------------------------------------------------------------------------- /junkyard/supervisor.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | (require racket/system racket/file racket/cmdline) 4 | (require json) 5 | 6 | ;; Used to color the supervisor's message output and color 7 | (define fstring "[\033[38;5;~am~a\033[0m @ ~a:~a:~a] ~a") 8 | 9 | ;; define a range function without using racket/list 10 | (define (span x) 11 | (define (inner x lst) 12 | (if (< x 0) 13 | lst 14 | (inner (sub1 x) (cons x lst)))) 15 | (cond 16 | [(< x 1) '()] 17 | [else (inner (sub1 x) '())])) 18 | 19 | ;; Creates a new function which prints out a message with a name attached 20 | (define (set-logger t-name color) 21 | (λ (str) 22 | (define cd (seconds->date (current-seconds))) 23 | (displayln 24 | (apply (λ (x y z) (format fstring color t-name x y z str)) 25 | (map (λ (i) (if (< i 10) (format "0~a" i) (format "~a" i))) 26 | (list (date-hour cd) (date-minute cd) (date-second cd))))))) 27 | 28 | (define logger (set-logger "supervisor " 9)) 29 | 30 | ;; Create a boty factory which uses a source as a reference point (rather, blueprints) 31 | (define (bot-factory bot-source) 32 | (λ (bot-id) 33 | (define bot (vector-ref bot-source bot-id)) 34 | (define-values (a b c d) 35 | (parameterize ([current-subprocess-custodian-mode 'kill]) 36 | (subprocess 37 | (current-output-port) 38 | #f 39 | 'stdout 40 | (find-executable-path (symbol->string (car bot))) 41 | (cdr bot)))) 42 | (logger (format "Initialized Bot ~a on PID ~a" bot-id (subprocess-pid a))) 43 | a)) 44 | 45 | ;; Revive a subprocess as required 46 | (define (reviver threads res-func) 47 | (λ (bot-id) 48 | (define subp (vector-ref threads bot-id)) 49 | (when (not (eqv? 'running (subprocess-status subp))) 50 | (subprocess-kill subp #t) 51 | (vector-set! threads bot-id (res-func bot-id))))) 52 | 53 | ;; The main program to run with the command-line 54 | (define (supervisor cfg-file) 55 | (define jdata (read-json (open-input-file cfg-file))) 56 | (define hard-restart (hash-ref jdata 'hard_restarts #t)) 57 | (define sleep-count (hash-ref jdata 'sleep_count 12)) 58 | (define sleep-time (hash-ref jdata 'sleep_time 300)) 59 | (define init-sleep (hash-ref jdata 'init_sleep 30)) 60 | (define bots (list->vector (map (compose car hash->list) (hash-ref jdata 'bots)))) 61 | (define total-bots (vector-length bots)) ; store how many have been set up 62 | (define start-bot (bot-factory bots)) 63 | 64 | (define (cust-loop) 65 | (logger "Starting Custodian") 66 | (define cust (make-custodian)) 67 | (parameterize ([current-custodian cust]) 68 | (define threads (build-vector total-bots start-bot)) 69 | (define revive (reviver threads start-bot)) 70 | (logger "Threads initialized") 71 | (sleep init-sleep) 72 | (define (loop x) 73 | (unless (= x sleep-count) 74 | (logger "Checking on bots") 75 | (for-each revive (span total-bots)) 76 | (sleep sleep-time) 77 | (loop (add1 x)))) 78 | (loop 0)) 79 | (when hard-restart 80 | (logger "Shutting down Custodian") 81 | (custodian-shutdown-all cust) 82 | (sleep 30)) 83 | (cust-loop)) 84 | (cust-loop)) 85 | 86 | (module+ main 87 | (command-line 88 | #:program "supervisor" 89 | #:args (config-file) 90 | (supervisor config-file))) 91 | 92 | ; end 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Discord Bots 2 | ============ 3 | 4 | ## Archival Notice 5 | 6 | Hello anyone stumbling across this repo. As it currently stands, this is one of my most starred repositories, and I started it years ago to make it easier to write Discord bots in Python using the `discord.py` API, which works great and I had a fun time doing it. However, it is time for me to archive this repository, as there is no support or new additions ever coming. 7 | 8 | My last "real" commit on this project was over 6 years ago, and I mostly stopped using this many moons ago and no longer plan on contributing code to this. I am still bumping merge requests from the automated systems to bump dependencies, but even that feels unnecessary. I would rather archive this and let it be preserved; anyone interested in supporting this can fork this and make new additions / do whatever they please. It is MIT licensed, you can do as you wish. 9 | 10 | This notice will be the last commit, and I will publish this as a 1.1 tag to make it final. Thank you for all the fish and stars, and I hope my code was helpful to anyone who happened to find it. 11 | 12 | --- 13 | 14 | A collection of Robots for Discord. 15 | 16 | ### Docs 17 | 18 | * [AsyncIO Docs](https://docs.python.org/3.4/library/asyncio.html) 19 | * [Discord.py API](http://discordpy.readthedocs.io/en/latest/api.html) 20 | * [Discord Dev Portal](https://discordapp.com/developers/docs/intro) 21 | 22 | 23 | ### Introduction 24 | 25 | Discord, the popular chat service aimed at gamers, supports a WebSocket API for sending and receiving data. From this we can create Bot users, so this project has the sole focus of creating various bots to be used with the Discord service. 26 | 27 | In it's current state, this project has Python code wrapping around the `discord.py` library itself to aid in the development of bots, as well as bots written in Python with many different goals of doing as much as they can. 28 | 29 | Currently, there's two types of Bots that can be used with Discord: 30 | * Interactive chat bot - a bot that receives and can send messages to channels 31 | * WebHook bot - a bot that can only send data to a channel via a URI endpoint 32 | 33 | 34 | ### Requirements 35 | 36 | To run this project you will need: 37 | 38 | * Python 3.6 39 | * Racket 6.5 (for the `superv` manager) 40 | * Pip for Python 41 | * `virtualenv` installed from Pip 42 | * Your own set of Discord credentials to use with Bots 43 | 44 | 45 | ### Setup 46 | 47 | The code can be cloned entirely from the Git repository and set up with Pip. 48 | 49 | ```bash 50 | git clone https://gitlab.com/sleibrock/discord-bots.git && cd discord-bots 51 | virtualenv dev 52 | source dev/bin/activate # source dev/Scripts/activate for Windows 53 | make setup 54 | make run 55 | ``` 56 | 57 | Each bot needs a key in order to use the Discord API. A Chat Bot requires an access token that is assigned when you create a Bot account under your Discord Developer page. A WebHook Bot requires a WebHook URL to post data to. Keys are stored in JSON format for easy loading, so use the following format and store the keys under a `keys/` folder named `.key`. 58 | ```json 59 | { 60 | "key": "secret_key" # or https:// link for webhook bots 61 | } 62 | ``` 63 | 64 | 65 | ### Bots Maintained Currently 66 | 67 | Here's the list of bots under development. 68 | 69 | * `dumb-bot`, a basic bot to integrate with the rest of the project 70 | * `dota-bot`, an automated daemon to send Dota match info through WebHooks 71 | * `eco-bot`, a game-economy assistant to look up prices (early development stages) 72 | 73 | Other bots are currently being ported from an older library, have been removed, 74 | or are just undergoing plain old experimentation for the time being. Disabled bots 75 | lie in the `junkyard` folder. 76 | -------------------------------------------------------------------------------- /junkyard/dumb-bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | Dumb-bot 6 | 7 | The dumbest bot of them all 8 | Mostly a collection of scraping utilities and 9 | otherwise non-related functions 10 | """ 11 | 12 | from botinfo import * 13 | from bs4 import BeautifulSoup as BS 14 | from requests import get as re_get 15 | from random import randint, choice 16 | 17 | help_msg = """ 18 | The Discord Bot Project 19 | https://github.com/sleibrock/discord-bots 20 | 21 | Command reference 22 | https://github.com/sleibrock/discord-bots/blob/master/docs/bot-command-guide.md 23 | """ 24 | 25 | bot_name = "dumb-bot" 26 | client = discord.Client() 27 | logger = create_logger(bot_name) 28 | shared_data = create_filegen("shared") 29 | 30 | # Twitch info here 31 | TWITCH = "https://api.twitch.tv/kraken" 32 | TKEY = read_key("twitch") 33 | STRAMS = f"{TWITCH}/streams/?game={'{}'}&client_id={TKEY}&limit=1" 34 | 35 | @register_command 36 | async def howto(msg, mobj): 37 | """ 38 | Return a help message 39 | If you came here from !howto help; there's nothing else, sorry 40 | """ 41 | return await client.send_message(mobj.channel, pre_text(help_msg)) 42 | 43 | @register_command 44 | async def rtd(msg, mobj): 45 | """ 46 | Roll a d di[c]e number of times 47 | Example: !rtd 2d10 - rolls two d10 dice 48 | """ 49 | if msg == "": 50 | return await client.send_message(mobj.channel, "You didn't say anything!") 51 | try: 52 | times, sides = list(map(int, msg.lower().split("d"))) 53 | res = [randint(1, sides) for x in range(times)] 54 | return await client.send_message(mobj.channel, ", ".join(map(str, res))) 55 | except Exception as ex: 56 | logger(f"Error: {ex}") 57 | return await client.send_message(mobj.channel, "Error: bad input args") 58 | 59 | @register_command 60 | async def yt(msg, mobj): 61 | """ 62 | Do a youtube search and yield the first result 63 | Example: !yt how do I take a screenshot 64 | """ 65 | try: 66 | if msg == "": 67 | return await client.send_message(mobj.channel, "You didn't search for anything!") 68 | msg.replace(" ", "+") 69 | url = "https://www.youtube.com/results?search_query={}".format(msg) 70 | bs = BS(re_get(url).text, "html.parser") 71 | items = bs.find("div", id="results").find_all("div", class_="yt-lockup-content") 72 | if not items: 73 | return await client.send_message(mobj.channel, "Couldn't find any results") 74 | 75 | # Search for a proper youtube url, has to start with /watch 76 | # TODO: rewrite this with a list comp/filter 77 | i, found = 0, False 78 | while not found and i < 20: 79 | href = items[i].find("a", class_="yt-uix-sessionlink")["href"] 80 | if href.startswith("/watch"): 81 | found = True 82 | i += 1 83 | if not found: 84 | return await client.send_message(mobj.channel, "Couldn't find a link") 85 | return await client.send_message(mobj.channel, f"https://youtube.com{href}") 86 | except Exception as ex: 87 | logger("Fail: {}".format(ex)) 88 | return await client.send_message(mobj.channel, "Failed to request the search") 89 | 90 | @register_command 91 | async def streams(msg, mobj): 92 | pass 93 | 94 | @register_command 95 | async def clipreg(msg, mobj): 96 | """ 97 | Register a Twitch.tv username into the Clips registry 98 | Used to bookmark names to Twitch accounts for ez access 99 | Must test the urls given to make sure it's a real Twitch account 100 | Example: !clipreg bulldog admiralbulldog 101 | """ 102 | pass 103 | 104 | @register_command 105 | async def clips(msg, mobj): 106 | """ 107 | Fetch a clip from a certain registered bookmark 108 | If no bookmark given (but entries exist), get a random clip 109 | Example: !clips summit1g 110 | -> 111 | """ 112 | pass 113 | 114 | @register_command 115 | async def dota_id(msg, mobj): 116 | """ 117 | Registers a user's Discord ID with a Dota 2 player ID 118 | This will be used by the automated Dota 2 match parser service 119 | The string given is tested against OpenDota's API to see if it's valid 120 | """ 121 | if len(msg) > 30: 122 | return await client.send_message(mobj.channel, "Bro that's too long") 123 | 124 | r = re_get(f"{OPENDOTA_API}/players/{msg.strip()}") 125 | if r.status_code != 200: 126 | return await client.send_message(mobj.channel, "Invalid Dota 2 ID") 127 | 128 | fname = shared_data(f"{mobj.author.id}.dota") 129 | with open(fname, 'w') as f: 130 | f.write(msg.strip()) 131 | 132 | return await client.send_message( 133 | mobj.channel, 134 | "Registered Player ID {msg.strip()}" 135 | ) 136 | 137 | # Last step - register events then run 138 | setup_all_events(client, bot_name, logger) 139 | if __name__ == "__main__": 140 | run_the_bot(client, bot_name, logger) 141 | 142 | # end 143 | 144 | -------------------------------------------------------------------------------- /junkyard/graph-bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | Graph-bot 6 | 7 | Creates graphs, stores them in the data folder, 8 | then removes them once uploaded 9 | 10 | This will probably eat up 100MB of memory 11 | """ 12 | 13 | from botinfo import * 14 | import matplotlib 15 | matplotlib.use('Agg') # only way to render without an Xserver running 16 | import matplotlib.pylab as plt 17 | from sympy import sympify, Abs 18 | from sympy.plotting import plot, plot3d 19 | from sympy.utilities.lambdify import lambdify 20 | from mpmath import cplot 21 | from numpy import matrix as np_matrix 22 | from os import remove as f_remove 23 | from ast import literal_eval 24 | 25 | bot_name = "graph-bot" 26 | client = discord.Client() 27 | logger = create_logger(bot_name) 28 | bot_data = create_filegen(bot_name) 29 | sample_size = 2000 # used for complex graphing 30 | 31 | def monkey_patch_function(expr): 32 | """ 33 | Name says it all 34 | Monkey patch any functions with bad func names (abs -> Abs) 35 | If anything else needs to be fixed, goes here 36 | """ 37 | return expr.replace(sympify("abs(x)").func, Abs) 38 | 39 | def create_plot(expr, s, color='b'): 40 | """ 41 | Create a plot based on whether it's 1-variable or 2-variable 42 | Raise Index error if we don't have any variables, or more than 2 43 | """ 44 | if len(expr.free_symbols) == 1: 45 | var1 = list(expr.free_symbols)[0] 46 | p = plot(expr, (var1, s["xmin"], s["xmax"]), 47 | xlim=(s["xmin"], s["xmax"]), 48 | ylim=(s["ymin"], s["ymax"]), 49 | legend=True, show=False, 50 | line_color=color) 51 | elif len(expr.free_symbols) == 2: 52 | var1, var2 = list(expr.free_symbols) 53 | p = plot3d(expr, (var1, s["xmin"], s["xmax"]), 54 | (var2, s["ymin"], s["ymax"]), 55 | xlim=(s["xmin"], s["xmax"]), 56 | ylim=(s["ymin"], s["ymax"]), 57 | title=str(expr), show=False) 58 | else: 59 | raise IndexError("Too many or too little variables") 60 | return p 61 | 62 | @register_command 63 | async def graph(msg, mobj): 64 | """ 65 | Do a 2D-plot of a user's given function 66 | Ex1: !graph cos(x)*10 67 | Ex2: !graph cos(x)*10, xmax=10, xmin=-7 68 | """ 69 | s = {'xmax':10.0, 'xmin':-10.0, 70 | 'ymax':10.0, 'ymin':-10.0,} 71 | fname = bot_data("{}.png".format(mobj.author.id)) 72 | try: 73 | firstp = msg.split(",") # seperate equation from additional args 74 | func = firstp[0] 75 | if len(firstp) > 1: 76 | for p in firstp[1:]: 77 | arg, val = p.strip().split("=") 78 | if arg in s.keys(): 79 | s[arg] = float(val) 80 | else: 81 | return await client.send_message(mobj.channel, "Invalid arguments") 82 | if not all([isinstance(x, float) for x in s.values()]): 83 | return await client.send_message(mobj.channel, "Invalid arguments") 84 | graph = create_plot(monkey_patch_function(sympify(func)), s) 85 | graph.save(fname) 86 | await client.send_file(mobj.channel, fname) 87 | f_remove(fname) 88 | return 89 | except Exception as ex: 90 | logger("!graph: {}".format(ex)) 91 | return await client.send_message(mobj.channel, "Failed to render graph") 92 | 93 | @register_command 94 | async def graphc(msg, mobj): 95 | """ 96 | Graph a complex equation (using mpmath functions and plotting) 97 | Equations should map to the complex domain 98 | Ex: !complex gamma(x) 99 | """ 100 | fname = bot_data("{}.png".format(mobj.author.id)) 101 | try: 102 | firstp = msg.split(",") 103 | func = firstp[0] 104 | expr = sympify(func) 105 | var = list(expr.free_symbols)[0] 106 | lamb = lambdify(var, monkey_patch_function(sympify(func)), modules=["mpmath"]) 107 | cplot(lamb, points=sample_size, file=fname) 108 | await client.send_file(mobj.channel, fname) 109 | f_remove(fname) 110 | return 111 | except Exception as ex: 112 | logger("!graphc: {}".format(ex)) 113 | return await client.send_message(mobj.channel, "Failed to render graph") 114 | 115 | async def calculus(msg, mobj, lam, col): 116 | """ 117 | Base function to perform some kind of calculus 118 | :lam is the type of operation we want, should be a lambda function 119 | :col is the color of the plot we want to have 120 | """ 121 | s = {'xmax':10.0, 'xmin':-10.0, 122 | 'ymax':10.0, 'ymin':-10.0,} 123 | fname = bot_data("{}.png".format(mobj.author.id)) 124 | try: 125 | firstp = msg.split(",") 126 | func = firstp[0] 127 | expr = monkey_patch_function(sympify(func)) 128 | var = list(expr.free_symbols)[0] 129 | graph = create_plot(expr, s) 130 | graph.extend(create_plot(lam(expr), s, color=col)) 131 | graph.save(fname) 132 | await client.send_file(mobj.channel, fname) 133 | f_remove(fname) 134 | return 135 | except Exception as ex: 136 | logger("!calculus: {}".format(ex)) 137 | return await client.send_message(mobj.channel, "Failed to render graph") 138 | 139 | @register_command 140 | def integrate(msg, mobj): 141 | """ 142 | Integrate a given function to yield it's anti-derivative 143 | Function must be continuous, and all results technically have a hidden constant 144 | Ex: !integrate cos(x)**2 145 | """ 146 | return calculus(msg, mobj, lambda e: e.integrate(), 'r') 147 | 148 | @register_command 149 | def derive(msg, mobj): 150 | """ 151 | Derive a given function to yield it's first derivative 152 | Note: must be a continuous function 153 | Ex: !derive x^3 154 | """ 155 | return calculus(msg, mobj, lambda e: e.diff(), 'g') 156 | 157 | @register_command 158 | async def matrix(msg, mobj): 159 | """ 160 | Interpret a user string, convert it to a list and graph it as a matrix 161 | Uses ast.literal_eval to parse input into a list 162 | """ 163 | fname = bot_data("{}.png".format(mobj.author.id)) 164 | try: 165 | list_input = literal_eval(msg) 166 | if not isinstance(list_input, list): 167 | raise ValueError("Not a list") 168 | m = np_matrix(list_input) 169 | fig = plt.figure() 170 | ax = fig.add_subplot(1,1,1) 171 | ax.set_aspect('equal') 172 | plt.imshow(m, interpolation='nearest', cmap=plt.cm.ocean) 173 | plt.colorbar() 174 | plt.savefig(fname) 175 | await client.send_file(mobj.channel, fname) 176 | f_remove(fname) 177 | return 178 | except Exception as ex: 179 | logger("!matrix: {}".format(ex)) 180 | return await client.send_message(mobj.channel, "Failed to render graph") 181 | 182 | setup_all_events(client, bot_name, logger) 183 | if __name__ == "__main__": 184 | run_the_bot(client, bot_name, logger) 185 | 186 | # end 187 | 188 | -------------------------------------------------------------------------------- /junkyard/remind-bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | from botinfo import * 5 | from time import time, mktime 6 | from datetime import datetime, timedelta 7 | 8 | # bot info and whatever who cares 9 | bot_name = "remind-bot" 10 | client = discord.Client() 11 | logger = create_logger(bot_name) 12 | bot_data = create_filegen(bot_name) 13 | 14 | # only support weeks, days and hours right now 15 | # months and years involves specific edge cases and leap years etc 16 | units = ["weeks", "days", "hours", "minutes"] 17 | units.extend([t[:-1] for t in units]) 18 | 19 | # settings - used to mutate state across the module 20 | settings = {"readloop":True, 21 | "max_rem": 10, 22 | } 23 | 24 | helpmsg = pre_text("""Remind-bot 25 | Hold up to 10 reminders for remind-bot to remind you about 26 | Usage: !!