├── lib ├── cogs │ ├── afk.json_example │ ├── blacklisted_users.json_example │ ├── stat.py │ ├── servercount.py │ ├── welcome.py │ ├── github.py │ ├── bio.py │ ├── help.py │ ├── fnbr.py │ ├── reactions.py │ ├── links.py │ ├── exp.py │ ├── info.py │ ├── gamestats.py │ ├── twitch.py │ ├── log.py │ ├── meta.py │ ├── misc.py │ └── lastfm.py ├── db │ ├── __init__.py │ └── db.py ├── NOT IN USE │ ├── README.md │ ├── slash.py │ ├── jumpscare.py │ └── osu.py └── bot │ └── __init__.py ├── .github ├── config.yml ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── contributors.yml │ └── lint.yml ├── images └── logo.png ├── .gitattributes ├── launcher.py ├── logs_example └── README.md ├── .replit ├── .vscode ├── settings.json ├── logchannel.code-snippets ├── cdc.code-snippets ├── jal.code-snippets ├── launch.json └── cncog.code-snippets ├── config.json ├── .gitignore ├── SECURITY.md ├── NOT IN USE └── rewrite readme.yml ├── COMMANDS_TO_KEEP.md ├── .env_example ├── PRIVACYPOLICY.md ├── data └── db │ └── build.sql ├── CODE_OF_CONDUCT.md ├── README.md └── CHANGELOG.md /lib/cogs/afk.json_example: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | todo: 2 | keyword: "TODO" -------------------------------------------------------------------------------- /lib/db/__init__.py: -------------------------------------------------------------------------------- 1 | from . import db 2 | 3 | db.build() 4 | -------------------------------------------------------------------------------- /lib/cogs/blacklisted_users.json_example: -------------------------------------------------------------------------------- 1 | { 2 | "blacklist" : [] 3 | } -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoobDev/Doob/HEAD/images/logo.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /launcher.py: -------------------------------------------------------------------------------- 1 | from lib.bot import bot 2 | 3 | VERSION = "2.9.10" 4 | 5 | bot.run(VERSION) 6 | -------------------------------------------------------------------------------- /logs_example/README.md: -------------------------------------------------------------------------------- 1 | # Change the name of this folder to `logs`, and then delete this file!~ -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | language = "python3" 2 | run = "pip install -U -r requirements.txt; python3 launcher.py" 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deepscan.ignoreConfirmWarning": true, 3 | "python.formatting.provider" :"black" 4 | } -------------------------------------------------------------------------------- /lib/NOT IN USE/README.md: -------------------------------------------------------------------------------- 1 | # This directory exists so I can move unused files either for testing or for deprecated commands/files. 2 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "homeGuild_id" : 702352937980133386, 3 | "patreonRole_id" : 757041749716893739, 4 | "owner_ids" : [308000668181069824], 5 | "ShortLinkLogs" : 843657332083654726, 6 | "dev_mode" : false 7 | } 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: doobdev # Support Doob Dev monthly and get perks! 2 | ko_fi: mmatt # Support Doob Dev directly with a one time payment, no perks. 3 | liberapay: mmatt # Support Doob Dev monthly with no perks. 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Doob Discord Support Server 4 | url: https://discord.gg/ryTYWjD 5 | about: Need help with using the bot? Join the server! 6 | -------------------------------------------------------------------------------- /.vscode/logchannel.code-snippets: -------------------------------------------------------------------------------- 1 | {"logchannel": { 2 | "prefix": "logchannel", 3 | "body": [ 4 | "logchannel = await self.bot.fetch_channel(db.field(\"SELECT LogChannel FROM guilds WHERE GuildID = ?\", ctx.guild.id))" 5 | ], 6 | "description": "logchannel" 7 | }} -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "dependencies" 9 | assignees: 10 | - "mmattbtw" 11 | commit-message: 12 | prefix: "🔼" 13 | -------------------------------------------------------------------------------- /.vscode/cdc.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Create Discord Command": { 3 | "prefix": "cdc", 4 | "body": [ 5 | " @command(name=\"${1:name}\")", 6 | " async def ${1:name}_command(self, ctx):", 7 | " await ctx.reply(f\"${1:message}\")" 8 | ], 9 | "description": "Create Discord Command" 10 | } 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | old v1/cogs/__pycache__ 2 | __pycache__ 3 | lib/bot/token.txt 4 | *.db 5 | *.db-journal 6 | lib/bot/topgg.txt 7 | lib/bot/webhook.txt 8 | .env 9 | pythonenv3.8 10 | lib/bot/statcord.txt 11 | lib/bot/trackergg api.txt 12 | .idea 13 | pyproject.toml 14 | poetry.lock 15 | .DS_store 16 | logs/ 17 | test-venv/ 18 | lib/cogs/afk.json 19 | lib/cogs/blacklisted_users.json 20 | node_modules 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe what your pull request adds or fixes. 2 | 3 | ### Pre-Merge checklist 4 | - [ ] - Made sure there were no bugs by testing the bot on your own Bot Application. 5 | 6 | - [ ] - Added a `CHANGELOG.md` entry if applicable. 7 | 8 | - [ ] - Edited the version number in `launcher.py` 9 | 10 | - [ ] - Added any dependencies in `requirements.txt` if needed 11 | -------------------------------------------------------------------------------- /.vscode/jal.code-snippets: -------------------------------------------------------------------------------- 1 | {"Join a list into a string.": { 2 | "prefix": "jal", 3 | "body": [ 4 | " comma = \", \"", 5 | " tNames = []", 6 | " for ${1:name} in ${2:list}:", 7 | " tNames.append(f\"{${1:name}.name}\")", 8 | "", 9 | " log.info(f\"{comma.join(tNames)}\")" 10 | ], 11 | "description": "Join a list into a string." 12 | }} -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 2.x.x+ | :white_check_mark: | 8 | | 1.x.x | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Report Vulnerabilities in the [Issues Page on GitHub](https://github.com/doobdev/doob/issues), if you know how to fix it [file a PR on GitHub](https://github.com/doobdev/doob/pulls) and [send me an email](mailto:mmattbtw@pm.me) if it needs to be confidential. 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python launcher.py", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "launcher.py", 12 | "console": "integratedTerminal" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: mmattbtw 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/workflows/contributors.yml: -------------------------------------------------------------------------------- 1 | name: Add contributors 2 | on: [push] 3 | 4 | jobs: 5 | add-contributors: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: BobAnkh/add-contributors@master 10 | with: 11 | REPO_NAME: 'DoobDev/Doob' 12 | CONTRIBUTOR: '### `🙌` Code Contributors' 13 | COLUMN_PER_ROW: '6' 14 | ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}} 15 | IMG_WIDTH: '100' 16 | FONT_SIZE: '14' 17 | PATH: '/README.md' 18 | COMMIT_MESSAGE: '🔼Update contributors' 19 | AVATAR_SHAPE: 'round' 20 | IGNORED_CONTRIBUTORS: 'Codacy Badger, mmattisinschool, WhiteSource Renovate, Voidy Devleoper' 21 | -------------------------------------------------------------------------------- /lib/cogs/stat.py: -------------------------------------------------------------------------------- 1 | """ 2 | UPDATES Doob's Statcord PAGE 3 | """ 4 | from discord.ext import commands 5 | 6 | import statcord 7 | 8 | import os 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv() 12 | 13 | 14 | class stat(commands.Cog): 15 | def __init__(self, bot): 16 | self.bot = bot 17 | 18 | self.api = statcord.Client(self.bot, os.environ.get("stat")) 19 | self.api.start_loop() 20 | 21 | @commands.Cog.listener() 22 | async def on_command(self, ctx): 23 | self.api.command_run(ctx) 24 | 25 | @commands.Cog.listener() 26 | async def on_ready(self): 27 | if not self.bot.ready: 28 | self.bot.cogs_ready.ready_up("stat") 29 | 30 | 31 | def setup(bot): 32 | bot.add_cog(stat(bot)) 33 | -------------------------------------------------------------------------------- /NOT IN USE/rewrite readme.yml: -------------------------------------------------------------------------------- 1 | name: "rewrite readme" 2 | 3 | on: 4 | issues: 5 | push: 6 | branches: master 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: rewriteReadme 13 | uses: seed-of-apricot/issue-list-readme@master 14 | with: 15 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 16 | pattern: "" 17 | state: "open" 18 | - name: add-and-commit 19 | uses: EndBug/add-and-commit@v4 20 | with: 21 | message: README.md has been re-written 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | - name: push 25 | uses: ad-m/github-push-action@master 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature_request 6 | assignees: mmattbtw 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | - [ ] I have acknowledged that this is a command/feature that will benefit the user base. 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run-linters: 7 | name: Run linters 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Check out Git repository 12 | uses: actions/checkout@v2 13 | 14 | - name: Set up Python 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.8 18 | 19 | - name: Install Python dependencies 20 | run: pip install black 21 | 22 | - name: Run linters 23 | uses: wearerequired/lint-action@v1 24 | with: 25 | black: true 26 | github_token: ${{ secrets.GITHUB_TOKEN }} 27 | auto_fix: true 28 | git_name: 'Doob Linting BOT' 29 | git_email: 'matt@mmatt.net' 30 | commit_message: '✨ [auto lint]' 31 | -------------------------------------------------------------------------------- /lib/cogs/servercount.py: -------------------------------------------------------------------------------- 1 | """ 2 | UPDATES Doob's SERVER COUNT ON TOPGG 3 | """ 4 | import logging 5 | import dbl 6 | 7 | from discord.ext import commands 8 | 9 | import os 10 | from dotenv import load_dotenv 11 | 12 | load_dotenv() 13 | log = logging.getLogger() 14 | 15 | 16 | class servercount(commands.Cog): 17 | """Handles interactions with the top.gg API""" 18 | 19 | def __init__(self, bot): 20 | self.bot = bot 21 | 22 | self.dblpy = dbl.DBLClient( 23 | self.bot, os.environ.get("topgg"), autopost=True 24 | ) # Autopost will post your guild count every 30 minutes 25 | log.info("\nTop.gg updated\n") 26 | 27 | @commands.Cog.listener() 28 | async def on_ready(self): 29 | if not self.bot.ready: 30 | self.bot.cogs_ready.ready_up("servercount") 31 | 32 | 33 | def setup(bot): 34 | bot.add_cog(servercount(bot)) 35 | -------------------------------------------------------------------------------- /.vscode/cncog.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Create New D.py Cog": { 3 | "prefix": "cncog", 4 | "body": [ 5 | "from discord.ext.commands import Cog, command, cooldown, BucketType", 6 | "", 7 | "from ..db import db # pylint: disable=relative-beyond-top-level", 8 | "", 9 | "import json", 10 | "", 11 | "with open(\"config.json\") as config_file:", 12 | " config = json.load(config_file)", 13 | "", 14 | "", 15 | "class ${1:File_Name}(Cog):", 16 | " def __init__(self, bot):", 17 | " self.bot = bot", 18 | "", 19 | " @Cog.listener()", 20 | " async def on_ready(self):", 21 | " if not self.bot.ready:", 22 | " self.bot.cogs_ready.ready_up(\"${1:File_Name}\")", 23 | "", 24 | "", 25 | "def setup(bot):", 26 | " bot.add_cog(${1:File_Name}(bot))", 27 | "" 28 | ], 29 | "description": "Create New D.py Cog" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/NOT IN USE/slash.py: -------------------------------------------------------------------------------- 1 | """ 2 | NEVER GOING TO BE IN USE. 3 | FROM TESTING SLASH COMMANDS 4 | """ 5 | 6 | from discord.ext.commands import Cog, command, cooldown, BucketType 7 | 8 | import discord 9 | 10 | from ..db import db # pylint: disable=relative-beyond-top-level 11 | 12 | import json 13 | 14 | from discord_slash import cog_ext, SlashContext 15 | 16 | 17 | with open("config.json") as config_file: 18 | config = json.load(config_file) 19 | 20 | guild_ids = [702352937980133386] 21 | 22 | 23 | class Slash(Cog): 24 | def __init__(self, bot): 25 | self.bot = bot 26 | 27 | @Cog.listener() 28 | async def on_ready(self): 29 | if not self.bot.ready: 30 | self.bot.cogs_ready.ready_up("slash") 31 | 32 | @cog_ext.cog_slash(name="test", guild_ids=guild_ids) 33 | async def _test(self, ctx: SlashContext): 34 | embed = discord.Embed(title="embed test") 35 | await ctx.send(content="test", embeds=[embed]) 36 | 37 | 38 | def setup(bot): 39 | bot.add_cog(Slash(bot)) 40 | -------------------------------------------------------------------------------- /COMMANDS_TO_KEEP.md: -------------------------------------------------------------------------------- 1 | 1. bio (bio.py) 2 | 2. dogfact (fun.py) 3 | 3. luckydog (fun.py) 4 | 4. dog (fun.py) 5 | 5. coinflip (fun.py) 6 | 6. coinfliptimes (fun.py) 7 | 7. all github.py commands 8 | 8. help command lol (probably dont import from 2.x.x) 9 | 9. info command (info.py) 10 | 10. user info command (info.py) 11 | 11. server info command (info.py) 12 | 12. lastfm commands (lastfm.py) 13 | 13. link (links.py) 14 | 14. support logging 15 | 15. support (meta.py) 16 | 16. invite (meta.py) 17 | 17. ping (meta.py) 18 | 18. shutdown/restart (meta.py) 19 | 19. patreon (meta.py) 20 | 20. eval command (maybe) (meta.py) 21 | 21. blacklist (meta.py) 22 | 22. unblacklist (meta.py) 23 | 23. afk (misc.py) 24 | 24. per server prefixs 25 | 25. poll (misc.py) 26 | 26. endpoll (misc.py) 27 | 27. giveaway (misc.py) 28 | 28. endgiveaway (misc.py) 29 | 29. timebomb (misc.py) 30 | 30. vote (misc.py) 31 | 31. ownerprefix (misc.py) 32 | 32. overlay (misc.py) 33 | 33. emote (misc.py) 34 | 34. most of everything in mod.py 35 | 35. starboard 36 | 36. twitch (move to v5 ) 37 | -------------------------------------------------------------------------------- /.env_example: -------------------------------------------------------------------------------- 1 | TOKEN= 2 | # This is your Discord Bot Token, get this at https://discord.dev 3 | 4 | stat= 5 | # This is for "Statcord", get this at https://statcord.com/add 6 | # If you don't want this, move "stat.py" to the "NOT IN USE" 7 | 8 | topgg= 9 | # This is for Top.gg, You shouldn't be able to get this, since you are just running source from another bot, so move "servercount.py" to the "NOT IN USE" folder. 10 | 11 | twitchclientid= 12 | # This is for the Twitch API, get this at https://dev.twitch.tv/console/apps 13 | twitchsecret= 14 | # This is also from the Twitch API, get this at https://dev.twitch.tv/console/apps 15 | 16 | lastfmkey= 17 | # This is from the Last.fm API, get this at https://www.last.fm/api/account/create 18 | 19 | github= 20 | # This is for the GitHub issues command, you can get your Personal Access Token by going to https://github.com/settings/tokens/new 21 | 22 | 23 | rebrandly= 24 | # This is used for Link Shortening, get this at https://app.rebrandly.com/account/api-keys 25 | rebrandly_workspace= 26 | # This is also used for Link Shortening, get this at https://app.rebrandly.com/workspaces 27 | 28 | fnbr_key= 29 | # Get this at https://fnbr.co/api/docs 30 | 31 | 32 | # (Make sure to delete all of these comments before you start the bot) -------------------------------------------------------------------------------- /lib/db/db.py: -------------------------------------------------------------------------------- 1 | from os.path import isfile 2 | from sqlite3 import connect 3 | 4 | from apscheduler.triggers.cron import CronTrigger 5 | 6 | DB_PATH = "./data/db/database.db" 7 | BUILD_PATH = "./data/db/build.sql" 8 | 9 | cxn = connect(DB_PATH, check_same_thread=False) 10 | cur = cxn.cursor() 11 | 12 | 13 | def with_commit(func): 14 | def inner(*args, **kwargs): 15 | func(*args, **kwargs) 16 | commit() 17 | 18 | return inner 19 | 20 | 21 | @with_commit 22 | def build(): 23 | if isfile(BUILD_PATH): 24 | scriptexec(BUILD_PATH) 25 | 26 | 27 | def commit(): 28 | cxn.commit() 29 | 30 | 31 | def autosave(sched): 32 | sched.add_job(commit, CronTrigger(second=0)) 33 | 34 | 35 | def close(): 36 | cxn.close() 37 | 38 | 39 | def field(command, *values): 40 | cur.execute(command, tuple(values)) 41 | 42 | if (fetch := cur.fetchone()) is not None: 43 | return fetch[0] 44 | 45 | 46 | def record(command, *values): 47 | cur.execute(command, tuple(values)) 48 | 49 | return cur.fetchone() 50 | 51 | 52 | def records(command, *values): 53 | cur.execute(command, tuple(values)) 54 | 55 | return cur.fetchall() 56 | 57 | 58 | def column(command, *values): 59 | cur.execute(command, tuple(values)) 60 | 61 | return [item[0] for item in cur.fetchall()] 62 | 63 | 64 | def execute(command, *values): 65 | cur.execute(command, tuple(values)) 66 | 67 | 68 | def multiexec(command, valueset): 69 | cur.executemany(command, valueset) 70 | 71 | 72 | def scriptexec(path): 73 | with open(path, "r", encoding="utf-8") as script: 74 | cur.executescript(script.read()) 75 | -------------------------------------------------------------------------------- /PRIVACYPOLICY.md: -------------------------------------------------------------------------------- 1 | 1) **What data does this bot store?** 2 | 3 | - Guild IDs 4 | - Channel IDs 5 | - User IDs 6 | - Role IDs 7 | 8 | 1.5) **We optionally store:** (only stored if **YOU** set them) 9 | - Overwatch Username 10 | - Overwatch Platform 11 | - Overwatch Region 12 | - Last.fm Username 13 | - osu! Username 14 | 15 | 2) **Why do we need the data, and why do we use this data?** 16 | 17 | a) Guilds IDs' are stored for configuration settings (such as logging, configuration settings and such.) 18 | 19 | b) Channel IDs' are stored for logging settings (same as above, it is stored in the DB only if you use a setlogchannel) 20 | 21 | c) User IDs' are stored for moderation actions and the global leveling system (such as levels, ranks, mutes) 22 | 23 | d) Role IDs' are stored for mute role purposes. 24 | 25 | e) Overwatch Username, Platform, and Regions are all stored for using the http://ow-api.com/ in the `d!owstats` command. 26 | 27 | f) Last.fm username is stored for using the Last.fm API in the `d!fm` group of commands. 28 | 29 | g) osu! username is stored for using the osu! api in the `d!osu` command. 30 | 31 | 3) **Other than Discord, do we share your data with any 3rd parties?** 32 | No, we do not share data with any 3rd parties! 33 | - What might count is that we use https://statcord.com/ 34 | - Which is public over at https://statcord.com/bot/680606346952966177 35 | 36 | 4) **How can users get data removed, or how can users contact the bot owner?** 37 | They can contact me, `mmatt#0001` on Discord, and `mmattbtw@pm.me` via email. 38 | 39 | 5) **Where can I find everything that is stored in your DB?** 40 | You can't see our DB, however you can see everything that is potentially stored here: https://github.com/DoobDev/Doob/blob/master/data/db/build.sql 41 | 42 | ## Please let me know if you see something that I am missing, I don't want to be wrong. 43 | -------------------------------------------------------------------------------- /data/db/build.sql: -------------------------------------------------------------------------------- 1 | /* Builds the database using the following tables */ 2 | 3 | CREATE TABLE IF NOT EXISTS guilds( 4 | GuildID integer PRIMARY KEY, 5 | Prefix text DEFAULT "d!", 6 | LogChannel text, 7 | MutedRole text, 8 | StarBoardChannel text, 9 | LevelMessages text DEFAULT "no", 10 | YesNoReaction text DEFAULT "no" 11 | ); 12 | 13 | CREATE TABLE IF NOT EXISTS users( 14 | UserID integer PRIMARY KEY, 15 | XP integer DEFAULT 0, 16 | Level integer DEFAULT 0, 17 | XPLock text DEFAULT CURRENT_TIMESTAMP, 18 | OverwatchUsername text, 19 | OverwatchPlatform text, 20 | OverwatchRegion text, 21 | LastfmUsername text, 22 | osuUsername text, 23 | ShortLinkAmount integer DEFAULT 0 24 | ); 25 | 26 | CREATE TABLE IF NOT EXISTS "mutes" ( 27 | UserID integer, 28 | GuildID integer, 29 | RoleIDs text, 30 | PRIMARY KEY("UserID","GuildID") 31 | ); 32 | 33 | CREATE TABLE IF NOT EXISTS votes( 34 | UserID integer PRIMARY KEY, 35 | HAVEVOTED text DEFAULT "no", 36 | VoteLock text DEFAULT CURRENT_TIMESTAMP 37 | ); 38 | 39 | CREATE TABLE IF NOT EXISTS guildexp( 40 | GuildID integer, 41 | UserID integer, 42 | XP integer DEFAULT 0, 43 | Level integer DEFAULT 0, 44 | XPLock text DEFAULT CURRENT_TIMESTAMP, 45 | PRIMARY KEY("GuildID", "UserID") 46 | ); 47 | 48 | CREATE TABLE IF NOT EXISTS luckydogs( 49 | UserID integer, 50 | LuckyDogs integer DEFAULT 0, 51 | LastUpdated text DEFAULT CURRENT_TIMESTAMP, 52 | PRIMARY KEY("UserID") 53 | ); 54 | 55 | CREATE TABLE IF NOT EXISTS starboard( 56 | MessageID integer, 57 | StarMessageID integer, 58 | Stars integer DEFAULT 1, 59 | GuildID integer, 60 | PRIMARY KEY("MessageID", "GuildID") 61 | ); 62 | 63 | CREATE TABLE IF NOT EXISTS warns( 64 | UserID integer, 65 | Warns integer DEFAULT 0, 66 | GuildID integer, 67 | PRIMARY KEY("UserID", "GuildID") 68 | ); 69 | 70 | CREATE TABLE IF NOT EXISTS globalwarns( 71 | UserID integer, 72 | Warns integer DEFAULT 0, 73 | PRIMARY KEY("UserID") 74 | ); 75 | 76 | CREATE TABLE IF NOT EXISTS blacklist( 77 | UserID integer, 78 | Reason text DEFAULT "Unknown", 79 | PRIMARY KEY("UserID") 80 | ) -------------------------------------------------------------------------------- /lib/cogs/welcome.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from discord.ext.commands import Cog 3 | 4 | from ..db import db # pylint: disable=relative-beyond-top-level 5 | 6 | log = logging.getLogger() 7 | 8 | 9 | class Welcome(Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | 13 | @Cog.listener() 14 | async def on_ready(self): 15 | if not self.bot.ready: 16 | self.bot.cogs_ready.ready_up("welcome") 17 | 18 | @Cog.listener() 19 | async def on_member_join(self, member): 20 | db.execute("INSERT INTO users (UserID) VALUES (?)", member.id) 21 | log.info(f"{member.username} (member/user) have been added into the users DB") 22 | db.execute( 23 | "INSERT INTO guildexp (UserID, GuildID) VALUES (?, ?)", 24 | member.id, 25 | member.guild.id, 26 | ) 27 | log.info( 28 | f"{member.username} (member/user) have been added into the server exp DB" 29 | ) 30 | db.execute("INSERT INTO luckydogs (UserID) VALUES (?)", member.id) 31 | log.info( 32 | f"{member.username} (member/user) has been added into the LuckyDogs DB" 33 | ) 34 | db.execute( 35 | f"INSERT OR IGNORE INTO warns (UserID, GuildID) VALUES (?, ?)", 36 | member.id, 37 | member.guild.id, 38 | ) 39 | log.info(f"{member.username} (member/user) has been added into the Warns DB.") 40 | db.execute(f"INSERT OR IGNORE INTO globalwarns (UserID) VALUES (?)", member.id) 41 | log.info( 42 | f"{member.username} (member/user) have been added into the global warns DB." 43 | ) 44 | db.commit() 45 | 46 | @Cog.listener() 47 | async def on_guild_join(self, guild): 48 | db.execute("INSERT INTO guilds (GuildID) VALUES (?)", guild.id) 49 | log.info(f"{guild.name} (guild) have been added into the DB") 50 | db.multiexec( 51 | "INSERT OR IGNORE INTO users (UserID) VALUES (?)", 52 | ((member.id,) for member in guild.members if not member.bot), 53 | ) 54 | log.info(f"{guild.name} users have been added into the users DB") 55 | db.commit() 56 | 57 | @Cog.listener() 58 | async def on_message(self, message): 59 | if message.guild: 60 | return 61 | 62 | if message.content == "help": 63 | await message.author.send( 64 | "Type `@Doob help` in a server with @Doob to get a command list!\nSupport Server: https://discord.gg/hgQTTU7" 65 | ) 66 | 67 | if message.content == "donate": 68 | await message.author.send("How generous!\nhttps://patreon.com/doobdev") 69 | 70 | else: 71 | await message.author.send( 72 | "Most commands can only be used in servers, not DMs!" 73 | ) 74 | 75 | 76 | def setup(bot): 77 | bot.add_cog(Welcome(bot)) 78 | -------------------------------------------------------------------------------- /lib/cogs/github.py: -------------------------------------------------------------------------------- 1 | from discord.embeds import Embed 2 | from discord.ext import commands 3 | from discord.ext.commands import Cog, group 4 | 5 | import json 6 | 7 | from typing import Optional 8 | 9 | with open("config.json") as config_file: 10 | config = json.load(config_file) 11 | 12 | from github import Github 13 | 14 | import os 15 | from dotenv import load_dotenv 16 | 17 | load_dotenv() 18 | 19 | owner_id = config["owner_ids"][0] 20 | 21 | token = os.environ.get("github") 22 | 23 | 24 | class GitHub(Cog): 25 | def __init__(self, bot): 26 | self.bot = bot 27 | 28 | async def show_github_issues(self, ctx): 29 | ghclient = Github(token) 30 | repo = ghclient.get_repo("DoobDev/Doob") 31 | 32 | open_issues = repo.get_issues(state="open") 33 | 34 | llist = [f"[‣ #{i.number} - {i.title}]({i.html_url})" for i in open_issues] 35 | 36 | if llist: 37 | embed = Embed( 38 | title="Open Issues in `DoobDev/Doob`", 39 | description="\n".join(llist), 40 | colour=ctx.author.colour, 41 | ) 42 | await ctx.reply(embed=embed) 43 | 44 | else: 45 | await ctx.reply("⚠ No Opened Issues found in `DoobDev/Doob`") 46 | 47 | @group(name="issue", aliases=["issues"]) 48 | async def issue(self, ctx): 49 | if ctx.invoked_subcommand is None: 50 | await self.show_github_issues(ctx) 51 | 52 | @issue.command(name="-create", aliases=["-c"]) 53 | @commands.is_owner() 54 | async def create_github_issue( 55 | self, ctx, label: str, priority_label: str, *, title: str 56 | ): 57 | ghclient = Github(token) 58 | repo = ghclient.get_repo("DoobDev/Doob") 59 | 60 | if priority_label == "high": 61 | gh_priority_label = "High Priority" 62 | 63 | elif priority_label == "medium": 64 | gh_priority_label = "Medium Priority" 65 | 66 | elif priority_label == "low": 67 | gh_priority_label = "Low Priority" 68 | 69 | issue = repo.create_issue( 70 | title=title, body="`Issue Created via Doob for Discord`", labels=[label] 71 | ) 72 | 73 | issue.add_to_labels(gh_priority_label) 74 | 75 | await ctx.reply(f"Issue Created. {issue.html_url}") 76 | 77 | @issue.command(name="-close", aliases=["-d", "-cl"]) 78 | @commands.is_owner() 79 | async def close_github_issue( 80 | self, ctx, issue_number: int, *, reason: Optional[str] 81 | ): 82 | if reason is None: 83 | reason = "" 84 | 85 | ghclient = Github(token) 86 | repo = ghclient.get_repo("DoobDev/Doob") 87 | 88 | issue = repo.get_issue(issue_number) 89 | 90 | issue.edit(state="closed") 91 | issue.create_comment(f"{reason}\n\n`Issue Closed via Doob for Discord`") 92 | 93 | await ctx.reply(f"Issue closed. {issue.html_url}") 94 | 95 | @Cog.listener() 96 | async def on_ready(self): 97 | if not self.bot.ready: 98 | self.bot.cogs_ready.ready_up("github") 99 | 100 | 101 | def setup(bot): 102 | bot.add_cog(GitHub(bot)) 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at matt@mmatt.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /lib/cogs/bio.py: -------------------------------------------------------------------------------- 1 | from discord.ext.commands import Cog, command, BucketType, cooldown 2 | from discord import Embed, Member 3 | 4 | from aiohttp import request 5 | 6 | from datetime import datetime 7 | 8 | 9 | from typing import Optional 10 | 11 | from dotenv import load_dotenv 12 | 13 | load_dotenv() 14 | 15 | 16 | class Bio(Cog): 17 | def __init__(self, bot): 18 | self.bot = bot 19 | 20 | @command( 21 | name="bio", 22 | aliases=["dbio", "discordbio"], 23 | brief="Get Discord.bio profile information.", 24 | ) 25 | @cooldown(1, 5, BucketType.user) 26 | async def discord_bio_command(self, ctx, *, target: Optional[Member]): 27 | """Lookup your Discord.bio profile information here.""" 28 | 29 | # Target = the target the user specified, if there is nobody they specified, it defaults back to the author. 30 | target = target or ctx.author 31 | 32 | # Discord.bio's API URL 33 | User_URL = f"https://api.discord.bio/user/details/{target.id}" 34 | 35 | async with request("GET", User_URL) as response: 36 | if response.status == 200: # This is to make sure the API is working. 37 | data = (await response.json())["payload"][ 38 | "user" 39 | ] # This gets the user's information from the .json file the API gives you. 40 | 41 | title = f"{target.display_name}'s discord.bio profile" # Sets the title for the embed 42 | desc = f"https://dsc.bio/{data['details']['slug']}" # Sets the description for the embed 43 | 44 | if ( 45 | data["details"]["premium"] == True 46 | ): # If they are a premium subscriber to Discord.bio 47 | title = f"{target.display_name}'s discord.bio profile 💎" # Give them this title/description 48 | desc = f"💎 https://dsc.bio/{data['details']['slug']} 💎" 49 | 50 | embed = Embed( 51 | title=title, 52 | description=desc, 53 | colour=target.colour, 54 | timestamp=datetime.utcnow(), 55 | ) # Embed setup. 56 | 57 | # This is where all the data comes in, pretty self explainitory if you look through the .json file 58 | # given to you by the API. 59 | 60 | fields = [ 61 | ("Bio", data["details"]["description"], False), 62 | ("Location", data["details"]["location"], True), 63 | ("Likes", f"💙 {data['details']['likes']}", True), 64 | ("Verified Status", data["details"]["verified"], True), 65 | ("Premium Status", data["details"]["premium"], True), 66 | ("Occupation", data["details"]["occupation"], True), 67 | ("Birthday", data["details"]["birthday"], True), 68 | ("Email", f"||{data['details']['email']}||", False), 69 | ] 70 | 71 | # Adds all the items from the `fields` variable into the embed. 72 | for name, value, inline in fields: 73 | embed.add_field(name=name, value=value, inline=inline) 74 | 75 | # Sets the embed thumbnail to the target's avatar on Discord 76 | embed.set_thumbnail(url=target.avatar_url) 77 | 78 | # If they have a banner on Discord.bio, show it as the "image" in the embed. 79 | if data["details"]["banner"] != None: 80 | embed.set_image(url=data["details"]["banner"]) 81 | 82 | await ctx.reply(embed=embed) 83 | 84 | else: # If the API status is something other then a 200, it sends you a message telling you which status it sent 85 | await ctx.reply(f"Discord.bio API returned a {response.status} status.") 86 | 87 | @Cog.listener() 88 | async def on_ready(self): 89 | if not self.bot.ready: 90 | self.bot.cogs_ready.ready_up("bio") 91 | 92 | 93 | def setup(bot): 94 | bot.add_cog(Bio(bot)) 95 | -------------------------------------------------------------------------------- /lib/NOT IN USE/jumpscare.py: -------------------------------------------------------------------------------- 1 | """ 2 | FROM HALLOWEEN 2020 3 | """ 4 | from random import randint 5 | 6 | from discord import Member, Embed, Colour 7 | from discord.ext.commands import Cog, command, cooldown, BucketType 8 | 9 | 10 | class Jumpscare(Cog): 11 | def __init__(self, bot): 12 | self.bot = bot 13 | 14 | @command(name="jumpscare", aliases=["js", "scare"], brief="scare friends [temp]") 15 | async def jumpscare_command(self, ctx, *, target: Member): 16 | jumpscare = randint(1, 6) 17 | 18 | if jumpscare == 1: 19 | embed = Embed( 20 | title="boo", 21 | description=f"{ctx.author.mention} fnaf jumpscare", 22 | colour=Colour.red(), 23 | ) 24 | embed.set_footer( 25 | text=f"{target.display_name} boo", icon_url=target.avatar_url 26 | ) 27 | embed.set_image( 28 | url="https://i.pinimg.com/originals/6d/f2/56/6df256a505c2c0851f0a906c00d7da93.gif" 29 | ) 30 | await ctx.reply(f"{target.mention} lol") 31 | await ctx.reply(embed=embed) 32 | 33 | elif jumpscare == 2: 34 | embed = Embed( 35 | title="boo", 36 | description=f"{ctx.author.mention} fnaf jumpscare", 37 | colour=Colour.red(), 38 | ) 39 | embed.set_footer( 40 | text=f"{target.display_name} boo", icon_url=target.avatar_url 41 | ) 42 | embed.set_image( 43 | url="https://i.pinimg.com/originals/ac/fd/40/acfd400be3e5a65503464e395e75947e.gif" 44 | ) 45 | await ctx.reply(f"{target.mention} lol") 46 | await ctx.reply(embed=embed) 47 | 48 | elif jumpscare == 3: 49 | embed = Embed( 50 | title="boo", 51 | description=f"{ctx.author.mention} fnaf jumpscare", 52 | colour=Colour.red(), 53 | ) 54 | embed.set_footer( 55 | text=f"{target.display_name} boo", icon_url=target.avatar_url 56 | ) 57 | embed.set_image( 58 | url="https://static.wikia.nocookie.net/fnafapedia/images/e/e4/Fnaf4_jumpscare_chicaindoorway.gif/revision/latest/top-crop/width/220/height/220?cb=20151031010318" 59 | ) 60 | await ctx.reply(f"{target.mention} lol") 61 | await ctx.reply(embed=embed) 62 | 63 | elif jumpscare == 4: 64 | embed = Embed( 65 | title="boo", 66 | description=f"{ctx.author.mention} fnaf jumpscare", 67 | colour=Colour.red(), 68 | ) 69 | embed.set_footer( 70 | text=f"{target.display_name} boo", icon_url=target.avatar_url 71 | ) 72 | embed.set_image( 73 | url="https://thumbs.gfycat.com/PertinentSevereKrill-max-1mb.gif" 74 | ) 75 | await ctx.reply(f"{target.mention} lol") 76 | await ctx.reply(embed=embed) 77 | 78 | elif jumpscare == 5: 79 | embed = Embed( 80 | title="boo", 81 | description=f"{ctx.author.mention} fnaf jumpscare", 82 | colour=Colour.red(), 83 | ) 84 | embed.set_footer( 85 | text=f"{target.display_name} boo", icon_url=target.avatar_url 86 | ) 87 | embed.set_image(url="https://i.makeagif.com/media/7-08-2016/RuqUem.gif") 88 | await ctx.reply(f"{target.mention} lol") 89 | await ctx.reply(embed=embed) 90 | 91 | else: 92 | embed = Embed( 93 | title="boo", 94 | description=f"{ctx.author.mention} fnaf jumpscare", 95 | colour=Colour.red(), 96 | ) 97 | embed.set_footer( 98 | text=f"{target.display_name} boo", icon_url=target.avatar_url 99 | ) 100 | embed.set_image( 101 | url="https://thumbs.gfycat.com/ShamelessVainCrossbill-max-1mb.gif" 102 | ) 103 | await ctx.reply(f"{target.mention} lol") 104 | await ctx.reply(embed=embed) 105 | 106 | @Cog.listener() 107 | async def on_ready(self): 108 | if not self.bot.ready: 109 | self.bot.cogs_ready.ready_up("jumpscare") 110 | 111 | 112 | def setup(bot): 113 | bot.add_cog(Jumpscare(bot)) 114 | -------------------------------------------------------------------------------- /lib/cogs/help.py: -------------------------------------------------------------------------------- 1 | from discord import Embed 2 | import asyncio 3 | from discord.ext.commands import Cog, command 4 | import re 5 | import math 6 | 7 | import re 8 | import math 9 | 10 | import json 11 | 12 | with open("config.json") as config_file: 13 | config = json.load(config_file) 14 | 15 | from ..db import db 16 | 17 | 18 | class Help(Cog): 19 | def __init__(self, bot): 20 | self.bot = bot 21 | 22 | @command(name="help", aliases=["commands"], brief="Shows this help message") 23 | async def help_command(self, ctx, cog="1"): # sourcery no-metrics 24 | helpEmbed = Embed(title="Help", color=ctx.author.color) 25 | helpEmbed.set_thumbnail(url=ctx.guild.me.avatar_url) 26 | prefix = db.field("SELECT Prefix FROM guilds WHERE GuildID = ?", ctx.guild.id) 27 | 28 | cogs = [c for c in self.bot.cogs.keys()] 29 | 30 | cogs.remove("Welcome") 31 | cogs.remove("Reactions") 32 | cogs.remove("Log") 33 | 34 | if ctx.author.id not in self.bot.owner_ids: 35 | cogs.remove("Jishaku") 36 | 37 | if not config["dev_mode"]: 38 | cogs.remove("servercount") 39 | cogs.remove("stat") 40 | 41 | totalPages = math.ceil(len(cogs) / 4) 42 | 43 | if re.search(f"\d", str(cog)): 44 | cog = int(cog) 45 | if cog > totalPages or cog < 1: 46 | await ctx.send( 47 | f"Invalid argument: `{cog}`\nPlease pick from {totalPages} pages.\nAlternatively, simply run `help` to see page one or type `help [category]` to see that categories help command!" 48 | ) 49 | return 50 | 51 | helpEmbed.set_footer( 52 | text=f"<> - Required, [] - Optional | Page {cog} of {totalPages} | use `{prefix}help (page number)` to flip pages." 53 | ) 54 | 55 | neededCogs = [] 56 | for i in range(4): 57 | x = i + (int(cog) - 1) * 4 58 | try: 59 | neededCogs.append(cogs[x]) 60 | except IndexError: 61 | pass 62 | 63 | for cog2 in neededCogs: 64 | commandList = "".join( 65 | f"`{command.name}` - {command.brief}\n" 66 | for command in self.bot.get_cog(cog2).walk_commands() 67 | if not command.hidden 68 | ) 69 | 70 | commandList += "\n" 71 | 72 | helpEmbed.add_field(name=cog2, value=commandList, inline=False) 73 | 74 | await ctx.send(embed=helpEmbed) 75 | 76 | elif re.search(r"[a-zA-Z]", str(cog)): 77 | lowerCogs = [c.lower() for c in cogs] 78 | if cog.lower() not in lowerCogs: 79 | await ctx.send( 80 | f"Invalid argument: `{cog}`\nPlease pick from {totalPages} pages.\nAlternatively, simply run `help` to see page one or type `help [category]` to see that categories help command!" 81 | ) 82 | return 83 | 84 | helpEmbed.set_footer( 85 | text=f"<> - Required, [] - Optional | Cog {(lowerCogs.index(cog.lower())+1)} of {len(lowerCogs)}" 86 | ) 87 | 88 | helpText = "" 89 | 90 | for command in self.bot.get_cog( 91 | cogs[lowerCogs.index(cog.lower())] 92 | ).walk_commands(): 93 | if command.hidden: 94 | continue 95 | 96 | params = [ 97 | f"[{key}]" if "NoneType" in str(value) else f"<{key}>" 98 | for key, value in command.params.items() 99 | if key not in ("self", "ctx") 100 | ] 101 | 102 | params = " ".join(params) 103 | helpText += f"**{command.name}**\n{command.brief}\n" 104 | 105 | if len(command.aliases) > 0: 106 | helpText += f"⠀‣ Aliases: `{', '.join(command.aliases)}`" 107 | 108 | helpText += "\n" 109 | 110 | if command.parent is not None: 111 | helpText += f"⠀‣ Format: `{prefix}{command.parent.name} {command.name} {params}`\n\n" 112 | else: 113 | helpText += f"⠀‣ Format: `{prefix}{command.name} {params}`\n\n" 114 | 115 | helpEmbed.description = helpText 116 | await ctx.send(embed=helpEmbed) 117 | else: 118 | await ctx.send( 119 | f"Invalid argument: `{cog}`\nPlease pick from {totalPages} pages.\nAlternatively, simply run `help` to see page one or type `help [category]` to see that categories help command!" 120 | ) 121 | 122 | @Cog.listener() 123 | async def on_ready(self): 124 | if not self.bot.ready: 125 | self.bot.cogs_ready.ready_up("help") 126 | 127 | 128 | def setup(bot): 129 | bot.add_cog(Help(bot)) 130 | -------------------------------------------------------------------------------- /lib/cogs/fnbr.py: -------------------------------------------------------------------------------- 1 | from discord.ext.commands import Cog, command, cooldown, BucketType 2 | 3 | from discord import Embed 4 | 5 | from aiohttp import request 6 | 7 | import json 8 | 9 | with open("config.json") as config_file: 10 | config = json.load(config_file) 11 | 12 | import os 13 | from dotenv import load_dotenv 14 | 15 | load_dotenv() 16 | 17 | 18 | class Fnbr(Cog): 19 | def __init__(self, bot): 20 | self.bot = bot 21 | 22 | @Cog.listener() 23 | async def on_ready(self): 24 | if not self.bot.ready: 25 | self.bot.cogs_ready.ready_up("fnbr") 26 | 27 | @command(name="fortniteskin", aliases=["skinsearch", "skin"]) 28 | @cooldown(1, 5, BucketType.user) 29 | async def fort_skin_search(self, ctx, *, skin: str): 30 | """Search a Fortnite skin using fnbr.co""" 31 | # common, uncommon, rare, epic, legendary, shadow, icon series 32 | URL = f"https://fnbr.co/api/images?search={skin}" 33 | 34 | async with request( 35 | "GET", 36 | URL, 37 | headers={ 38 | "x-api-key": os.environ.get("fnbr_key"), 39 | "Content-Type": "application/json", 40 | }, 41 | ) as response: 42 | if response.status == 200: 43 | data1 = (await response.json())["data"] 44 | data2 = (await response.json())["data"][0] 45 | 46 | if data2["rarity"] == "common": 47 | color = 0xB3B3B3 48 | 49 | elif data2["rarity"] == "uncommon": 50 | color = 0x7EE700 51 | 52 | elif data2["rarity"] == "rare": 53 | color = 0x00EEFC 54 | 55 | elif data2["rarity"] == "epic": 56 | color = 0xDB33FF 57 | 58 | elif data2["rarity"] == "legendary": 59 | color = 0xFBA14C 60 | 61 | elif data2["rarity"] == "shadow": 62 | color = 0xEB37A7 63 | 64 | elif data2["rarity"] == "icon_series": 65 | color = 0x6DDAEB 66 | 67 | else: 68 | color = ctx.author.color 69 | 70 | embed = Embed( 71 | title=data1[0]["name"], 72 | description="<:icon_vbucks:851675167993102356> Price: " 73 | + data2["price"] 74 | + "\nType: " 75 | + data2["readableType"] 76 | + "\nRarity: " 77 | + data2["rarity"].capitalize() 78 | + "\nDescription: " 79 | + data2["description"], 80 | colour=color, 81 | ) 82 | 83 | embed.set_image(url=data2["images"]["featured"]) 84 | embed.set_thumbnail(url=data2["images"]["icon"]) 85 | 86 | embed.set_author( 87 | name="Provided by fnbr.co!", 88 | icon_url="https://image.fnbr.co/logo/logo_75x.png", 89 | url="https://fnbr.co", 90 | ) 91 | 92 | embed.set_footer( 93 | text="use code 'matt'! #EpicPartner", icon_url=ctx.author.avatar_url 94 | ) 95 | 96 | await ctx.send(embed=embed) 97 | else: 98 | await ctx.send(f"The fnbr.co API sent a {response.status} status :/") 99 | 100 | # @command( 101 | # name="fortniteshop", 102 | # aliases=["fnshop", "shop"], 103 | # brief="Shows the current Fortnite Shop.", 104 | # ) 105 | # async def fortnite_shop_command(self, ctx): 106 | # """Shows the current Fortnite Item Shop courtacy of https://fnbr.co\nuse code `matt` in the fortnite item shop :O #ad""" 107 | 108 | # URL = "https://fnbr.co/api/shop" 109 | 110 | # async with request( 111 | # "GET", 112 | # URL, 113 | # headers={ 114 | # "x-api-key": os.environ.get("fnbr_key"), 115 | # "Content-Type": "application/json", 116 | # }, 117 | # ) as response: 118 | # if response.status == 200: 119 | # data = (await response.json())["data"] 120 | 121 | # embed = Embed( 122 | # title="The Current Fortnite Item Shop", 123 | # colour=ctx.author.color, 124 | # ) 125 | 126 | # for item in data["featured"]: 127 | # embed.add_field( 128 | # name=item["name"], 129 | # value="Price: " 130 | # + item["price"] 131 | # + "\nType: " 132 | # + item["readableType"] 133 | # + "\nRarity: " 134 | # + item["rarity"].capitalize(), 135 | # ) 136 | 137 | # await ctx.send(embed=embed) 138 | 139 | # else: 140 | # await ctx.send(f"The fnbr.co API sent a {response.status} status :/") 141 | 142 | 143 | def setup(bot): 144 | bot.add_cog(Fnbr(bot)) 145 | -------------------------------------------------------------------------------- /lib/NOT IN USE/osu.py: -------------------------------------------------------------------------------- 1 | """ 2 | SADLY PYOSU NO LONGER WORKS (iirc) 3 | """ 4 | import asyncio 5 | from pyosu import OsuApi 6 | 7 | from discord.ext.commands import Cog, command, BucketType, cooldown 8 | from discord import Embed, Colour 9 | 10 | from ..db import db # pylint: disable=relative-beyond-top-level 11 | 12 | from datetime import datetime 13 | 14 | from typing import Optional 15 | 16 | import os 17 | from dotenv import load_dotenv 18 | 19 | load_dotenv() 20 | 21 | 22 | class osu(Cog): 23 | def __init__(self, bot): 24 | self.bot = bot 25 | 26 | @command( 27 | name="osu", 28 | aliases=["osuprofile"], 29 | brief="Get some information about your osu! account!", 30 | ) 31 | @cooldown(1, 5, BucketType.user) 32 | async def osu_command(self, ctx, username: Optional[str]): 33 | prefix = db.record("SELECT Prefix from guilds WHERE GuildID = ?", ctx.guild.id) 34 | 35 | if ( 36 | db.record("SELECT osuUsername FROM users WHERE UserID = ?", ctx.author.id)[ 37 | 0 38 | ] 39 | == None 40 | ): 41 | await ctx.reply( 42 | f"Your osu! username is set to None\nSet it to your username by doing `{prefix[0]}setosu`" 43 | ) 44 | return 45 | 46 | username = ( 47 | username 48 | or db.record( 49 | "SELECT osuUsername FROM users WHERE UserID = ?", ctx.author.id 50 | )[0] 51 | ) 52 | 53 | api = OsuApi(os.environ.get("osu_api")) 54 | 55 | user = await api.get_user(user=username, mode=0, type_str="string") 56 | 57 | embed = Embed( 58 | title=f"{user.username}'s osu! profile", 59 | description=f"https://osu.ppy.sh/u/{user.username}", 60 | colour=ctx.author.colour, 61 | timestamp=datetime.utcnow(), 62 | ) 63 | 64 | fields = [ 65 | ("Play Count", "{:,}".format(user.playcount), True), 66 | ("Rank", "{:,}".format(user.pp_rank), True), 67 | ("Country Rank", "{:,}".format(user.pp_country_rank), True), 68 | ("Total Score", "{:,}".format(round(user.total_score)), False), 69 | ("Accuracy", "{:,}".format(round(user.accuracy, 2)), False), 70 | ("Level", "{:,}".format(round(user.level)), True), 71 | ("Country", user.country, False), 72 | ( 73 | "<:rankingA:779734932519387136> Ranks", 74 | "{:,}".format(user.count_rank_a), 75 | True, 76 | ), 77 | ( 78 | "<:rankingS:779734932850343958> Ranks", 79 | "{:,}".format(user.count_rank_s), 80 | True, 81 | ), 82 | ( 83 | "<:rankingSd:779734932795686943> Ranks", 84 | "{:,}".format(user.count_rank_sh), 85 | True, 86 | ), 87 | ( 88 | "<:rankingSS:779734933178417163> Ranks", 89 | "{:,}".format(user.count_rank_ss), 90 | True, 91 | ), 92 | ( 93 | "<:rankingSSd:779734933077884968> Ranks", 94 | "{:,}".format(user.count_rank_ssh), 95 | True, 96 | ), 97 | ] 98 | 99 | for name, value, inline in fields: 100 | embed.add_field(name=name, value=value, inline=inline) 101 | 102 | embed.set_thumbnail(url=f"https://a.ppy.sh/{user.user_id}") 103 | 104 | await ctx.reply(embed=embed) 105 | 106 | @command( 107 | name="setosu", 108 | aliases=["setosuusername", "setosuuser", "sou", "osuset"], 109 | brief="Set your osu! username!", 110 | ) 111 | @cooldown(1, 5, BucketType.user) 112 | async def osu_set_command(self, ctx, username: Optional[str]): 113 | if username != None: 114 | api = OsuApi(os.environ.get("osu_api")) 115 | user = await api.get_user(user=username, mode=0, type_str="string") 116 | 117 | embed = Embed( 118 | title="Setting osu! username:", 119 | description=username, 120 | colour=ctx.author.colour, 121 | ) 122 | 123 | embed.set_thumbnail(url=f"https://a.ppy.sh/{user.user_id}") 124 | 125 | db.execute( 126 | "UPDATE users SET osuUsername = ? WHERE UserID = ?", 127 | username, 128 | ctx.author.id, 129 | ) 130 | db.commit() 131 | 132 | await ctx.reply(embed=embed) 133 | 134 | else: 135 | username = db.record( 136 | "SELECT osuUsername FROM users WHERE UserID = ?", ctx.author.id 137 | )[0] 138 | api = OsuApi(os.environ.get("osu_api")) 139 | user = await api.get_user(user=username, mode=0, type_str="string") 140 | 141 | embed = Embed( 142 | title="Your osu! username", 143 | description=username, 144 | colour=ctx.author.colour, 145 | ) 146 | 147 | embed.set_thumbnail(url=f"https://a.ppy.sh/{user.user_id}") 148 | 149 | await ctx.reply(embed=embed) 150 | 151 | @Cog.listener() 152 | async def on_ready(self): 153 | if not self.bot.ready: 154 | self.bot.cogs_ready.ready_up("osu") 155 | 156 | 157 | def setup(bot): 158 | bot.add_cog(osu(bot)) 159 | -------------------------------------------------------------------------------- /lib/cogs/reactions.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from discord import Embed 4 | from discord.ext.commands import Cog 5 | 6 | from ..db import db # pylint: disable=relative-beyond-top-level 7 | 8 | 9 | class Reactions(Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | 13 | @Cog.listener() 14 | async def on_raw_reaction_add(self, payload): 15 | if payload.emoji.name != "⭐": 16 | return 17 | guild = self.bot.get_guild(payload.guild_id) 18 | starboardchannel = await self.bot.fetch_channel( 19 | db.field("SELECT StarBoardChannel from guilds WHERE GuildID = ?", guild.id) 20 | ) 21 | message = await self.bot.get_channel(payload.channel_id).fetch_message( 22 | payload.message_id 23 | ) 24 | 25 | if not message.author.bot and payload.member.id != message.author.id: 26 | msg_id, stars = ( 27 | db.record( 28 | "SELECT StarMessageID, Stars from starboard WHERE (GuildID, MessageID) = (?, ?)", 29 | guild.id, 30 | message.id, 31 | ) 32 | or (None, 0) 33 | ) 34 | embed = Embed( 35 | title=f"⭐ x{stars+1}", 36 | colour=message.author.colour, 37 | timestamp=datetime.utcnow(), 38 | ) 39 | 40 | embed.set_thumbnail(url=message.author.avatar_url) 41 | embed.set_footer(text=f"⭐ x{stars+1}") 42 | 43 | fields = [ 44 | ("Author", message.author.mention, False), 45 | ("Content", message.content or "Image", False), 46 | ( 47 | "Jump To Link", 48 | f"[Jump](https://discord.com/channels/{message.guild.id}/{message.channel.id}/{message.id})", 49 | False, 50 | ), 51 | ] 52 | 53 | for name, value, inline in fields: 54 | embed.add_field(name=name, value=value, inline=inline) 55 | 56 | if len(message.attachments): 57 | embed.set_image(url=message.attachments[0].url) 58 | 59 | if not stars: 60 | star_message = await starboardchannel.send(embed=embed) 61 | db.execute( 62 | "INSERT INTO starboard (MessageID, StarMessageID, GuildID) VALUES (?, ?, ?)", 63 | message.id, 64 | star_message.id, 65 | message.guild.id, 66 | ) 67 | else: 68 | star_message = await starboardchannel.fetch_message(msg_id) 69 | await star_message.edit(embed=embed) 70 | db.execute( 71 | "UPDATE starboard SET Stars = Stars + 1 WHERE (GuildID, MessageID) = (?, ?)", 72 | message.guild.id, 73 | message.id, 74 | ) 75 | db.commit() 76 | 77 | else: 78 | await message.remove_reaction(payload.emoji, payload.member) 79 | 80 | # async def on_starboard_remove(self, payload): 81 | # if payload.emoji.name == "⭐": 82 | # guild = self.bot.get_guild(payload.guild_id) 83 | # starboardchannel = await self.bot.fetch_channel(db.field("SELECT StarBoardChannel from guilds WHERE GuildID = ?", guild.id)) 84 | # message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) 85 | 86 | # if not message.author.bot and payload.member.id != message.author.id: 87 | # msg_id, stars = db.record("SELECT StarMessageID, Stars from starboard WHERE (GuildID, MessageID) = (?, ?)", guild.id, message.id) or (None, 0) 88 | # embed = Embed(title=f"⭐ x{stars-1}", colour=message.author.colour, timestamp=datetime.utcnow()) 89 | # embed.set_thumbnail(url=message.author.avatar_url) 90 | # embed.set_footer(text=f"⭐ x{stars-1}") 91 | # #pip ( gotta get that proper indentation B) ) 92 | # fields = [("Author", message.author.mention, False), 93 | # ("Content", message.content or "Image", False), 94 | # ("Jump To Link", f"[Jump](https://discord.com/channels/{message.guild.id}/{message.channel.id}/{message.id})", False)] 95 | 96 | # for name, value, inline in fields: 97 | # embed.add_field(name=name, value=value, inline=inline) 98 | 99 | # if len(message.attachments): 100 | # embed.set_image(url=message.attachments[0].url) 101 | 102 | # if not stars: 103 | # star_message = await starboardchannel.send(embed=embed) 104 | # db.execute("INSERT INTO starboard (MessageID, StarMessageID, GuildID) VALUES (?, ?, ?)", message.id, star_message.id, message.guild.id) 105 | # db.commit() 106 | 107 | # else: 108 | # star_message = await starboardchannel.fetch_message(msg_id) 109 | # await star_message.edit(embed=embed) 110 | # db.execute("UPDATE starboard SET Stars = Stars - 1 WHERE (GuildID, MessageID) = (?, ?)", message.guild.id, message.id) 111 | # db.commit() 112 | 113 | # else: 114 | # await message.remove_reaction(payload.emoji, payload.member) 115 | 116 | # @Cog.listener() 117 | # async def on_reaction_remove(self, payload): 118 | # await self.on_starboard_remove(payload) 119 | 120 | # @Cog.listener() 121 | # async def on_reaction_clear_emoji(self, reaction): 122 | # await self.on_starboard_remove(payload=reaction) 123 | 124 | # @Cog.listener() 125 | # async def on_reaction_clear(self, reaction): 126 | # await self.on_starboard_remove(payload=reaction) 127 | 128 | @Cog.listener() 129 | async def on_ready(self): 130 | if not self.bot.ready: 131 | self.bot.cogs_ready.ready_up("reactions") 132 | 133 | 134 | def setup(bot): 135 | bot.add_cog(Reactions(bot)) 136 | -------------------------------------------------------------------------------- /lib/cogs/links.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from discord.ext.commands import Cog, command, cooldown, BucketType 4 | from discord.utils import get 5 | 6 | from discord_slash.utils.manage_commands import create_option 7 | from discord_slash import cog_ext 8 | 9 | from ..db import db # pylint: disable=relative-beyond-top-level 10 | 11 | import requests 12 | import json 13 | 14 | import os 15 | from dotenv import load_dotenv 16 | 17 | load_dotenv() 18 | 19 | with open("config.json") as config_file: 20 | config = json.load(config_file) 21 | 22 | 23 | class Links(Cog): 24 | def __init__(self, bot): 25 | self.bot = bot 26 | 27 | @Cog.listener() 28 | async def on_ready(self): 29 | if not self.bot.ready: 30 | self.bot.cogs_ready.ready_up("links") 31 | 32 | async def shorten_link_func(self, ctx, url: str, vanity: Optional[str]): 33 | ShortLinkAmount = db.records( 34 | "SELECT ShortLinkAmount FROM users WHERE UserID = ?", ctx.author.id 35 | ) 36 | 37 | homeGuild = self.bot.get_guild(config["homeGuild_id"]) # Support Server ID. 38 | patreonRole = get( 39 | homeGuild.roles, id=config["patreonRole_id"] 40 | ) # Patreon role ID. 41 | 42 | member = [] 43 | 44 | # Checks if user is a Patron 45 | for pledger in homeGuild.members: 46 | if pledger == ctx.author: 47 | member = pledger 48 | 49 | if ctx.author in homeGuild.members: 50 | if patreonRole in member.roles: 51 | if ctx.author.id in config["owner_ids"]: 52 | await self.after_check_shorten_link_func(ctx, url, vanity) 53 | elif ShortLinkAmount[0][0] >= 12: 54 | await ctx.send("You have too many short links! (12)") 55 | else: 56 | await self.after_check_shorten_link_func(ctx, url, vanity) 57 | elif ShortLinkAmount[0][0] >= 6: 58 | await ctx.send("You have too many short links! (6)") 59 | 60 | elif ShortLinkAmount[0][0] >= 6: 61 | await ctx.send("You have too many short links! (6)") 62 | else: 63 | await self.after_check_shorten_link_func(ctx, url, vanity) 64 | 65 | async def after_check_shorten_link_func(self, ctx, url: str, vanity: Optional[str]): 66 | ShortLinkAmount = db.records( 67 | "SELECT ShortLinkAmount FROM users WHERE UserID = ?", ctx.author.id 68 | ) 69 | 70 | log_channel = await self.bot.fetch_channel(config["ShortLinkLogs"]) 71 | 72 | try: 73 | homeGuild = self.bot.get_guild(config["homeGuild_id"]) # Support Server ID. 74 | patreonRole = get( 75 | homeGuild.roles, id=config["patreonRole_id"] 76 | ) # Patreon role ID. 77 | 78 | member = [] 79 | 80 | # Checks if user is a Patron 81 | for pledger in homeGuild.members: 82 | if pledger == ctx.author: 83 | member = pledger 84 | 85 | # If patron, give them access to vanity 86 | if ctx.author in homeGuild.members and patreonRole in member.roles: 87 | linkRequest = { 88 | "destination": url, 89 | "domain": {"fullName": "doob.link"}, 90 | "slashtag": vanity, 91 | } 92 | else: 93 | linkRequest = { 94 | "destination": url, 95 | "domain": {"fullName": "doob.link"}, 96 | } 97 | requestHeaders = { 98 | "Content-type": "application/json", 99 | "apikey": os.environ.get("rebrandly"), 100 | "workspace": os.environ.get("rebrandly_workspace"), 101 | } 102 | 103 | r = requests.post( 104 | "https://api.rebrandly.com/v1/links", 105 | data=json.dumps(linkRequest), 106 | headers=requestHeaders, 107 | ) 108 | 109 | if r.status_code == requests.codes.ok: 110 | link = r.json() 111 | shortUrl = link["shortUrl"] 112 | destination = link["destination"] 113 | await ctx.send(f"Boom! Shortened: :link: ") 114 | 115 | db.execute( 116 | "UPDATE users SET (ShortLinkAmount) = (?) WHERE UserID = ?", 117 | ShortLinkAmount[0][0] + 1, 118 | ctx.author.id, 119 | ) 120 | db.commit() 121 | 122 | owner_id = config["owner_ids"][0] 123 | await log_channel.send( 124 | f"New Short Link <<@{owner_id}>>\nFrom: <@{ctx.author.id}> (Username: {ctx.author.name}#{ctx.author.discriminator} // ID: {ctx.author.id})\n<:doob:754762131085459498> :link: \nLong :link: {destination}\n(scan with `-trace`)" 125 | ) 126 | 127 | elif r.status_code == 403: 128 | await ctx.send( 129 | "Seems like your vanity URL is already being used, try again!" 130 | ) 131 | else: 132 | await ctx.send(f"Rebrandly API sent an error :/ ({r.status_code})") 133 | 134 | except UnboundLocalError: 135 | await ctx.send( 136 | "You aren't a Patron! You can't use vanities.\nSubscribe to get access ;) " 137 | ) 138 | 139 | @command( 140 | name="link", aliases=["shortenlink"], brief="Shorten a link using doob.link!" 141 | ) 142 | @cooldown(1, 10, BucketType.user) 143 | async def shorten_link_command(self, ctx, url: str, vanity: Optional[str]): 144 | """Vanity URLs are only available to [Patrons](https://patreon.com/doobdev)\nYou get 6 short links\n(upgrade to 12 when you [subscribe](https://patreon.com/doobdev))""" 145 | await self.shorten_link_func(ctx, url, vanity) 146 | 147 | @cog_ext.cog_slash( 148 | name="link", 149 | description="[Patreon Only, for now] Shorten a link using doob.link!", 150 | options=[ 151 | create_option( 152 | name="url", 153 | description="Link you would like to shorten.", 154 | option_type=3, 155 | required=True, 156 | ), 157 | create_option( 158 | name="vanity", 159 | description="[Patreon Only] Vanity link for your short URL", 160 | option_type=3, 161 | required=True, 162 | ), 163 | ], 164 | ) 165 | async def shorten_link_slash(self, SlashContext, url: str, vanity: str): 166 | ctx = SlashContext 167 | await self.shorten_link_func(ctx, url, vanity) 168 | 169 | 170 | def setup(bot): 171 | bot.add_cog(Links(bot)) 172 | -------------------------------------------------------------------------------- /lib/cogs/exp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from random import randint 3 | from typing import Optional 4 | 5 | from discord import Member, Embed 6 | from discord.ext.commands import Cog 7 | from discord.ext.commands import command, has_permissions 8 | from discord.ext.menus import MenuPages, ListPageSource 9 | from lib.bot import bot # pylint: disable=no-name-in-module, import-error 10 | from ..db import db # pylint: disable=relative-beyond-top-level 11 | import json 12 | 13 | import os 14 | 15 | absolute_path = os.path.dirname(os.path.abspath(__file__)) 16 | # Or: file_path = os.path.join(absolute_path, 'folder', 'my_file.py') 17 | 18 | 19 | def get_path(filename): 20 | return absolute_path + f"/{filename}.json" 21 | 22 | 23 | def read_json(filename): 24 | with open(get_path(filename), "r") as file: 25 | data = json.load(file) 26 | return data 27 | 28 | 29 | BLACKLISTED_USERS = read_json("blacklisted_users") 30 | 31 | 32 | class Exp(Cog): 33 | def __init__(self, bot): 34 | self.bot = bot 35 | 36 | async def process_xp(self, message): 37 | if message.author.id not in BLACKLISTED_USERS["blacklist"]: 38 | xp, lvl, xplock = db.record( 39 | "SELECT XP, Level, XPLock FROM users WHERE UserID = ?", 40 | message.author.id, 41 | ) 42 | xp_g, lvl_g, xplock_g = db.record( 43 | "SELECT XP, Level, XPLock FROM guildexp WHERE UserID = ? AND GuildID = ?", 44 | message.author.id, 45 | message.guild.id, 46 | ) 47 | 48 | if datetime.utcnow() > datetime.fromisoformat(xplock): 49 | await self.add_xp(message, xp, lvl) 50 | 51 | if datetime.utcnow() > datetime.fromisoformat(xplock_g): 52 | await self.add_gxp(message, xp_g, lvl_g) 53 | 54 | async def add_xp(self, message, xp, lvl): 55 | if message.author.id not in BLACKLISTED_USERS["blacklist"]: 56 | xp_to_add = randint(10, 20) 57 | level_up_messages = db.record( 58 | "SELECT LevelMessages FROM guilds WHERE GuildID = ?", message.guild.id 59 | )[0] 60 | 61 | new_lvl = int(((xp + xp_to_add) // 42) ** 0.55) 62 | 63 | db.execute( 64 | "UPDATE users SET XP = XP + ?, Level = ?, XPLock = ? WHERE UserID = ?", 65 | xp_to_add, 66 | new_lvl, 67 | (datetime.utcnow() + timedelta(seconds=50)).isoformat(), 68 | message.author.id, 69 | ) 70 | 71 | if new_lvl > lvl and level_up_messages in ["yes", "Yes"]: 72 | await message.channel.send( 73 | f"{message.author.mention} leveled up to {new_lvl:,}!", 74 | delete_after=10, 75 | ) 76 | 77 | async def add_gxp(self, message, xp, lvl): 78 | if message.author.id not in BLACKLISTED_USERS["blacklist"]: 79 | xp_to_add = randint(10, 20) 80 | level_up_messages = db.record( 81 | "SELECT LevelMessages FROM guilds WHERE GuildID = ?", message.guild.id 82 | )[0] 83 | 84 | new_lvl = int(((xp + xp_to_add) // 42) ** 0.55) 85 | 86 | db.execute( 87 | f"UPDATE guildexp SET XP = XP + ?, Level = ?, XPLock = ? WHERE UserID = ? AND GuildID = ?", 88 | xp_to_add, 89 | new_lvl, 90 | (datetime.utcnow() + timedelta(seconds=50)).isoformat(), 91 | message.author.id, 92 | message.guild.id, 93 | ) 94 | db.commit() 95 | 96 | if new_lvl > lvl and level_up_messages in ["yes", "Yes"]: 97 | await message.channel.send( 98 | f"{message.author.mention} leveled up to server level {new_lvl:,}!", 99 | delete_after=10, 100 | ) 101 | 102 | @command(name="level", aliases=["rank", "lvl"], brief="Shows your level, and rank.") 103 | async def display_level(self, ctx, target: Optional[Member]): 104 | """Shows your Global+Server Doob level, rank and XP!""" 105 | target = target or ctx.author 106 | 107 | ids = db.column("SELECT UserID FROM users ORDER BY XP DESC") 108 | ids_g = db.column( 109 | "SELECT UserID from guildexp WHERE GuildID = (?) ORDER BY XP DESC", 110 | ctx.guild.id, 111 | ) 112 | # ids_g = db.column("SELECT UserID FROM users ORDER BY XP DESC WHERE GuildID = ?", ctx.guild.id) 113 | xp, lvl = db.record( 114 | "SELECT XP, Level FROM users WHERE UserID = ?", target.id 115 | ) or (None, None) 116 | xp_g, lvl_g = ( 117 | db.record( 118 | "SELECT XP, Level FROM guildexp WHERE (UserID, GuildID) = (?, ?)", 119 | target.id, 120 | ctx.guild.id, 121 | ) 122 | or (None, None) 123 | ) 124 | 125 | if lvl is not None: 126 | to_next_level = int((lvl + 1) ** (20 / 11) * 42) - xp 127 | embed = Embed( 128 | title=f"{target.display_name} is level {lvl:,}", 129 | description=f"XP: {xp:,}\nXP to next level {to_next_level:,}" 130 | + f"\n\nServer XP: {xp_g:,}\nServer level: {lvl_g:,}", 131 | colour=ctx.author.color, 132 | ) 133 | 134 | fields = [ 135 | ("Global Rank:", f"{ids.index(target.id)+1:,} of {len(ids):,}", False), 136 | ( 137 | f"Server Rank:", 138 | f"{ids_g.index(target.id)+1:,} of {len(ids_g):,}", 139 | False, 140 | ), 141 | ] 142 | 143 | for field in fields: 144 | embed.add_field(name=field[0], value=field[1], inline=field[2]) 145 | 146 | embed.set_thumbnail(url=target.avatar_url) 147 | 148 | await ctx.send(embed=embed) 149 | 150 | else: 151 | ctx.send("That member is not in the XP Database.") 152 | 153 | @command( 154 | name="levelmessages", 155 | aliases=["slm", "lm", "setlevelmessages"], 156 | brief="Set the server's level messages", 157 | ) 158 | @has_permissions(manage_guild=True) 159 | async def set_level_messages(self, ctx, *, yes_or_no: Optional[str]): 160 | """PLEASE, put 'yes' if you DO want level messages\n`Manage Server` permission required.""" 161 | levelmessages = db.records( 162 | "SELECT LevelMessages FROM guilds WHERE GuildID = ?", ctx.guild.id 163 | ) or (None) 164 | prefix = db.records("SELECT Prefix FROM guilds WHERE GuildID = ?", ctx.guild.id) 165 | 166 | if yes_or_no in ["Yes", "yes", "no", "No"]: 167 | db.execute( 168 | "UPDATE guilds SET LevelMessages = ? WHERE GuildID = ?", 169 | yes_or_no, 170 | ctx.guild.id, 171 | ) 172 | db.commit() 173 | await ctx.send(f"Level messages set to `{yes_or_no}`.") 174 | 175 | else: 176 | await ctx.send( 177 | f"The current setting for Level Messages is: `{levelmessages[0][0]}`\nTo change it, type `{prefix[0][0]}levelmessages (yes or no)`" 178 | ) 179 | 180 | @Cog.listener() 181 | async def on_ready(self): 182 | if not self.bot.ready: 183 | self.bot.cogs_ready.ready_up("exp") 184 | 185 | @Cog.listener() 186 | async def on_message(self, message): 187 | if not message.author.bot: 188 | await self.process_xp(message) 189 | 190 | 191 | def setup(bot): 192 | bot.add_cog(Exp(bot)) 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Doob has been archived. 2 | see this note: 3 | 4 | ***announcement of Doob's shutdown*** 5 | hey there! its mmatt, and i think it's finally time for this 6 | 7 | as time has gone on, i've just slowly and slowly lost interest in developing doob (and other discord bots). a multitude of reasons could be to blame, api changes, out of the blue library changes, my loss in interest in discord as a platform, etc. but i think its finally the time to just call it quits on this project, to give me more time for other ones down the road. 8 | 9 | if you couldn't tell, i haven't actively developed doob in a **loooooooooooooong** time. (actively developed = work on doob's codebase every 1-2 days about). it started slowing down at the end of v2. but then i thought my interest would be peaked with learning a new language with v3, but that wasn't the reality. the reality was that i was tired of discord/doob, and didn't want to port everything over yet again. its the sad truth, but the motivation for the project has just been declining since about the middle of last year. doob used to be my big *main* project, but lately its just a side thing that i did, and stopped working on. so that's why i think its time to just finally get it over with, and shut it down. 10 | 11 | doob was my first ever *large* (take that with a grain of salt) developing project. it taught me how to learn python, javascript, databases, apis, error handling, programming logic, libraries, community management, server management, and probably more. i am forever thankful for every experience i've had with this project. but at the same time, i am so happy to let it go. no longer will i feel the burden of "oh i need to work on v3 this weekend" or "oh i have free time right now, i need to work on doob". i can finally feel free, do what i want, and work on other things that will excite me more :) i'm probably not done working with chatbots, but here on discord, most likely. (twitch :eyes:) 12 | 13 | that all being said, this server (and the bot) will most likely be shut down in a couple days. if you need to contact me, there is a billion ways to here https://mm.omg.lol 14 | 15 |

16 | Doob Logo 17 |

18 | 19 |

20 | GitHub license 21 | GitHub issues 22 | GitHub forks 23 | GitHub stars 24 | Code style: black 25 | Discord 26 | Patreon donate button 27 | 28 | CodeFactor 29 |

30 | 31 |

32 | 33 | Doob 34 | 35 |

36 | 37 | ## `📚` About 38 | Doob is a multipurpose Discord bot with a built-in **leveling system**, great **logging features**, actual fun, **fun commands**, and best of all, the developer is very active, and develops the bot frequently. 39 | 40 | ## `🔮` Main Features 41 | Doob has some great features, such as: 42 | * Logging 43 | * A Global (and server specific) Leveling System (with leaderboard) 44 | * Server + User info 45 | * Fun Commands 46 | * Customizable Server Features [Don't like Level Messages? Turn them off!] 47 | * Robust Moderation Commands 48 | * Twitch Stream Integration 49 | * Last.fm Integration 50 | * Discord.bio Integration 51 | * AND MORE! 52 | 53 | ## `🌐` Doob on the web. 54 | * You can visit the Doob website at [https://doobbot.com](https://doobbot.com) 55 | * Visit the Dev blog at [https://doobdev.github.io](https://doobdev.github.io) 56 | 57 | ## `🔷` What commands are there? 58 | Use the d!help command on Discord! 59 | 60 | ## `🚧` Want to submit issues? Or fix existing ones? 61 | ##### Issue is for Bug Reports + Feature Requests | Pull requests are for improving/adding code. 62 | Submit an [Issue](https://github.com/doobdev/doob/issues) or a [Pull request](https://github.com/doobdev/doob/pulls). 63 | 64 | ## `🙌` Credits 65 | * Doob's Emojis: Use both [Font Awesome](https://fontawesome.com/) and [Material Design](https://materialdesignicons.com/) icons. 66 | * Doob Avatar: [@KittyKay000](https://twitter.com/KittyKay000) 67 | 68 | ### `🙌` Code Contributors 69 | 70 | 71 | 72 | 79 | 86 | 93 | 100 | 107 | 108 |
73 | 74 | matt/ 75 |
76 | matt 77 |
78 |
80 | 81 | Daniel/ 82 |
83 | Daniel 84 |
85 |
87 | 88 | Andy 89 |
90 | Andy Chan 91 |
92 |
94 | 95 | CyberPop/ 96 |
97 | CyberPop 98 |
99 |
101 | 102 | ALazyMeme/ 103 |
104 | ALazyMeme 105 |
106 |
109 | 110 | ###### built with ♥, youtube tutorials, yelling at daniel in discord, stack overflow and twitch chat 111 | -------------------------------------------------------------------------------- /lib/cogs/info.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from typing import Optional 4 | 5 | from discord import Embed, Member 6 | from discord.ext.commands import Cog, command, cooldown, BucketType 7 | from discord.utils import get 8 | 9 | from discord_slash.utils.manage_commands import create_option 10 | from discord_slash import cog_ext 11 | 12 | from ..db import db # pylint: disable=relative-beyond-top-level 13 | 14 | import json 15 | import os 16 | 17 | with open("config.json") as config_file: 18 | config = json.load(config_file) 19 | 20 | absolute_path = os.path.dirname(os.path.abspath(__file__)) 21 | 22 | 23 | def get_path(filename): 24 | return absolute_path + f"/{filename}.json" 25 | 26 | 27 | def read_json(filename): 28 | with open(get_path(filename), "r") as file: 29 | data = json.load(file) 30 | return data 31 | 32 | 33 | BLACKLISTED_USERS = read_json("blacklisted_users") 34 | 35 | 36 | class Info(Cog): 37 | def __init__(self, bot): 38 | self.bot = bot 39 | 40 | async def user_info(self, ctx, target, patreon_status, blacklisted): 41 | ids = db.column("SELECT UserID FROM users ORDER BY XP DESC") 42 | xp, lvl = db.record( 43 | "SELECT XP, Level FROM users WHERE UserID = ?", target.id 44 | ) or (None, None) 45 | warnings = db.records( 46 | f"SELECT Warns FROM warns WHERE UserID = {target.id} AND GuildID = {ctx.guild.id}" 47 | )[0][0] 48 | globalwarns = db.records(f"SELECT Warns FROM warns WHERE UserID = {target.id}")[ 49 | 0 50 | ][0] 51 | 52 | embed = Embed( 53 | title=f"{target.name}'s info", 54 | colour=target.colour, 55 | timestamp=datetime.utcnow(), 56 | ) 57 | 58 | patreon = "Yes" if patreon_status == True else "No" 59 | fields = [ 60 | ("Username", target.mention, True), 61 | ("ID", target.id, True), 62 | ("Doob Global XP", xp, False), 63 | ("Doob Global Level", lvl, True), 64 | ( 65 | "Doob Global Rank", 66 | f"{ids.index(target.id)+1} of {len(ids):,} users globally.", 67 | True, 68 | ), 69 | ("Warnings (Server)", warnings, False), 70 | ("Warnings (Global)", globalwarns, True), 71 | ("Bot", target.bot, False), 72 | ("Top role", target.top_role.mention, True), 73 | ("Status", str(target.status).title(), True), 74 | ( 75 | "Activity", 76 | f"{str(target.activity.type).split('.')[-1].title() if target.activity else 'N/A'} - {target.activity.name if target.activity else ''}", 77 | True, 78 | ), 79 | ( 80 | "Account creation date", 81 | target.created_at.strftime("%m/%d/%Y %H:%M;%S"), 82 | True, 83 | ), 84 | ( 85 | "Joined the server at", 86 | target.joined_at.strftime("%m/%d/%Y %H:%M;%S"), 87 | True, 88 | ), 89 | ("Boosted this server", bool(target.premium_since), True), 90 | ("Patron of Doob?", patreon, True), 91 | ] 92 | 93 | for name, value, inline in fields: 94 | embed.add_field(name=name, value=value, inline=inline) 95 | 96 | if blacklisted is True: 97 | embed.add_field( 98 | name="Blacklisted?", 99 | value="✅ This user has been blacklisted from using Doob.", 100 | inline=False, 101 | ) 102 | else: 103 | embed.add_field( 104 | name="Blacklisted?", 105 | value="❌ This user is not blacklisted from using Doob.", 106 | inline=False, 107 | ) 108 | 109 | embed.set_thumbnail(url=target.avatar_url) 110 | await ctx.send(embed=embed) 111 | 112 | @command( 113 | name="userinfo", 114 | aliases=["member", "user", "profile", "ui", "whois"], 115 | brief="Gives info about a specific user.", 116 | ) 117 | @cooldown(1, 10, BucketType.user) 118 | async def user_info_command(self, ctx, target: Optional[Member]): 119 | """Gives you info about a user.""" 120 | target = target or ctx.author 121 | 122 | homeGuild = self.bot.get_guild(config["homeGuild_id"]) # Support Server ID. 123 | patreonRole = get( 124 | homeGuild.roles, id=config["patreonRole_id"] 125 | ) # Patreon role ID. 126 | 127 | member = [] 128 | 129 | for pledger in homeGuild.members: 130 | if pledger == target: 131 | member = pledger 132 | 133 | blacklisted = target.id in BLACKLISTED_USERS["blacklist"] 134 | if target in homeGuild.members and patreonRole in member.roles: 135 | patreon_status = True 136 | await self.user_info(ctx, target, patreon_status, blacklisted) 137 | 138 | else: 139 | await self.user_info( 140 | ctx, target, patreon_status=False, blacklisted=blacklisted 141 | ) 142 | 143 | @cog_ext.cog_slash( 144 | name="userinfo", 145 | description="Gives you info about a user.", 146 | options=[ 147 | create_option( 148 | name="target", 149 | description="User you would like to recieve info on.", 150 | option_type=6, 151 | required=True, 152 | ) 153 | ], 154 | ) 155 | async def user_info_slash(self, ctx, target: Optional[Member]): 156 | homeGuild = self.bot.get_guild(config["homeGuild_id"]) # Support Server ID. 157 | patreonRole = get( 158 | homeGuild.roles, id=config["patreonRole_id"] 159 | ) # Patreon role ID. 160 | 161 | member = [] 162 | 163 | for pledger in homeGuild.members: 164 | if pledger == target: 165 | member = pledger 166 | 167 | blacklisted = target.id in BLACKLISTED_USERS["blacklist"] 168 | if target in homeGuild.members and patreonRole in member.roles: 169 | patreon_status = True 170 | await self.user_info(ctx, target, patreon_status, blacklisted) 171 | 172 | else: 173 | await self.user_info( 174 | ctx, target, patreon_status=False, blacklisted=blacklisted 175 | ) 176 | 177 | async def server_info(self, ctx, banned_members): 178 | embed = Embed( 179 | title="Server's info", 180 | colour=ctx.guild.owner.colour, 181 | timestamp=datetime.utcnow(), 182 | ) 183 | 184 | # statuses = [ 185 | # len(list(filter(lambda m: str(m.status) == "online", ctx.guild.members))), 186 | # len(list(filter(lambda m: str(m.status) == "idle", ctx.guild.members))), 187 | # len(list(filter(lambda m: str(m.status) == "dnd", ctx.guild.members))), 188 | # len(list(filter(lambda m: str(m.status) == "offline", ctx.guild.members))), 189 | # ] 190 | 191 | fields = [ 192 | ("ID", ctx.guild.id, True), 193 | ("Owner", ctx.guild.owner, True), 194 | ("Region", ctx.guild.region, True), 195 | ("Created at", ctx.guild.created_at.strftime("%d/%m/%Y %H:%M:%S"), True), 196 | ("Members", len(ctx.guild.members), True), 197 | ("Humans", len(list(filter(lambda m: not m.bot, ctx.guild.members))), True), 198 | ("Bots", len(list(filter(lambda m: m.bot, ctx.guild.members))), True), 199 | # ( 200 | # "Statuses", 201 | # f"🟢 {statuses[0]} 🟠 {statuses[1]} 🔴 {statuses[2]} ⚪ {statuses[3]}", 202 | # True, 203 | # ), 204 | ("Text channels", len(ctx.guild.text_channels), True), 205 | ("Voice channels", len(ctx.guild.voice_channels), True), 206 | ("Categories", len(ctx.guild.categories), True), 207 | ("Roles", len(ctx.guild.roles), True), 208 | ("\u200b", "\u200b", True), 209 | ] 210 | 211 | embed.set_thumbnail(url=ctx.guild.icon_url) 212 | 213 | if ctx.guild.banner: 214 | embed.set_image(url=ctx.guild.banner_url) 215 | 216 | for name, value, inline in fields: 217 | embed.add_field(name=name, value=value, inline=inline) 218 | 219 | if banned_members == True: 220 | embed.add_field( 221 | name="Banned members", value=len(await ctx.guild.bans()), inline=True 222 | ) 223 | await ctx.send(embed=embed) 224 | 225 | @command( 226 | name="serverinfo", 227 | aliases=["guildinfo", "gi", "si"], 228 | brief="Gives info about the server.", 229 | ) 230 | @cooldown(1, 10, BucketType.user) 231 | async def server_info_command(self, ctx): 232 | """Gives you info about the server the command is executed in.""" 233 | if ctx.guild.me.guild_permissions.administrator == True: 234 | await self.server_info(ctx, banned_members=True) 235 | 236 | else: 237 | await self.server_info(ctx, banned_members=False) 238 | 239 | @cog_ext.cog_slash( 240 | name="serverinfo", 241 | description="Gives you info about your server.", 242 | ) 243 | async def server_info_slash_command(self, ctx): 244 | if ctx.guild.me.guild_permissions.administrator == True: 245 | await self.server_info(ctx, banned_members=True) 246 | 247 | else: 248 | await self.server_info(ctx, banned_members=False) 249 | 250 | @Cog.listener() 251 | async def on_ready(self): 252 | if not self.bot.ready: 253 | self.bot.cogs_ready.ready_up("info") 254 | 255 | 256 | def setup(bot): 257 | bot.add_cog(Info(bot)) 258 | -------------------------------------------------------------------------------- /lib/cogs/gamestats.py: -------------------------------------------------------------------------------- 1 | from discord.ext.commands import Cog, command, BucketType, cooldown 2 | 3 | from discord import Member, Embed 4 | 5 | from typing import Optional 6 | from aiohttp import request 7 | 8 | from ..db import db # pylint: disable=relative-beyond-top-level 9 | 10 | import os 11 | from dotenv import load_dotenv 12 | 13 | load_dotenv() 14 | 15 | 16 | class gamestats(Cog): 17 | def __init__(self, bot): 18 | self.bot = bot 19 | 20 | @command( 21 | name="owstats", 22 | aliases=["overwatch", "owprofile", "ow"], 23 | brief="Gets your Overwatch stats.", 24 | ) 25 | @cooldown(1, 5, BucketType.user) 26 | async def overwatch_stats(self, ctx, target: Optional[Member]): 27 | """Gets your Overwatch stats from ow-api.com!\nSet your overwatch profile by doing `d!setowprofile {platform} {username} {region}`""" 28 | target = target or ctx.author 29 | 30 | # Grabs the platform, username, and region from the database in which the user set before running the command. 31 | platform = db.record( 32 | "SELECT OverwatchPlatform FROM users WHERE UserID = ?", target.id 33 | ) 34 | platformUserIdentifier = db.record( 35 | "SELECT OverwatchUsername FROM users WHERE UserID = ?", target.id 36 | ) 37 | platformRegion = db.record( 38 | "SELECT OverwatchRegion FROM users WHERE UserID = ?", target.id 39 | ) 40 | 41 | # URL of the stats API. 42 | URL = f"https://ow-api.com/v1/stats/{platform[0]}/{platformRegion[0]}/{platformUserIdentifier[0]}/complete" 43 | 44 | async with request("GET", URL) as response: 45 | if response.status == 200: 46 | 47 | data = (await response.json())["competitiveStats"]["careerStats"][ 48 | "allHeroes" 49 | ] 50 | 51 | embed = Embed( 52 | title=f"{target.display_name}'s Overwatch Stats!", 53 | description="Competitive Stats", 54 | colour=ctx.author.colour, 55 | ) 56 | # All of the stats from the API, all into the fields. 57 | fields = [ 58 | ( 59 | "Below are per 10 min average stats.", 60 | "-----------------------------------------", 61 | False, 62 | ), 63 | ( 64 | "Eliminations", 65 | f"{data['average']['eliminationsAvgPer10Min']}", 66 | True, 67 | ), 68 | ( 69 | "All Damage Done", 70 | f"{data['average']['allDamageDoneAvgPer10Min']}", 71 | True, 72 | ), 73 | ( 74 | "Final Blows", 75 | f"{data['average']['finalBlowsAvgPer10Min']}", 76 | True, 77 | ), 78 | ("Solo Kills", f"{data['average']['soloKillsAvgPer10Min']}", True), 79 | ( 80 | "Time spent on fire", 81 | f"{data['average']['timeSpentOnFireAvgPer10Min']}", 82 | True, 83 | ), 84 | ( 85 | "Healing done", 86 | f"{data['average']['healingDoneAvgPer10Min']}", 87 | True, 88 | ), 89 | ("Deaths", f"{data['average']['deathsAvgPer10Min']}", True), 90 | ( 91 | "Hero Damage Done", 92 | f"{data['average']['heroDamageDoneAvgPer10Min']}", 93 | True, 94 | ), 95 | ("Below are 'best' stats", "-------------------------", False), 96 | ("Eliminations", f"{data['best']['eliminationsMostInGame']}", True), 97 | ( 98 | "All Damage Done", 99 | f"{data['best']['allDamageDoneMostInGame']}", 100 | True, 101 | ), 102 | ("Final Blows", f"{data['best']['finalBlowsMostInGame']}", True), 103 | ("Solo Kills", f"{data['best']['soloKillsMostInGame']}", True), 104 | ( 105 | "Time spent on fire", 106 | f"{data['best']['timeSpentOnFireMostInGame']}", 107 | True, 108 | ), 109 | ("Healing done", f"{data['best']['healingDoneMostInGame']}", True), 110 | ( 111 | "Hero Damage Done", 112 | f"{data['best']['heroDamageDoneMostInGame']}", 113 | True, 114 | ), 115 | ] 116 | 117 | # Adds the fields. 118 | for name, value, inline in fields: 119 | embed.add_field(name=name, value=value, inline=inline) 120 | 121 | embed.set_footer( 122 | text="Sourced from ow-api.com", icon_url=ctx.author.avatar_url 123 | ) 124 | embed.set_thumbnail(url=(await response.json())["icon"]) 125 | await ctx.reply(embed=embed) 126 | 127 | else: # If the API status is not 200, then send out this. 128 | await ctx.reply( 129 | f"Overwatch stats [ow-api.com] API sent a {response.status} status." 130 | ) 131 | 132 | @command( 133 | name="setowusername", 134 | aliases=["setplatform", "sowp", "sowu", "setusername"], 135 | brief="Sets your Overwatch username+platform.", 136 | ) 137 | @cooldown(1, 5, BucketType.user) 138 | async def set_overwatch_profile( 139 | self, 140 | ctx, 141 | platform: Optional[str], 142 | username: Optional[str], 143 | region: Optional[str], 144 | ): 145 | """Sets your Overwatch platform + username for `d!owstats`\nOnly acceptable platforms are `pc` `xbl` and `psn`\nOnly acceptable regions are `us` `eu` or `asia`\nFor battletags, make sure you do `{username}-{numbers}` NOT `{username}#{numbers}`""" 146 | 147 | # If the platform they chose was PSN, set their Username, Platform, and Region to what they said when they executed the command 148 | if platform == "psn": 149 | embed = Embed( 150 | title="Setting Overwatch Profile:", 151 | description=f"PSN", 152 | colour=ctx.author.colour, 153 | ) 154 | 155 | embed.add_field(name="Overwatch Username", value=username) 156 | embed.set_thumbnail(url=ctx.author.avatar_url) 157 | 158 | db.execute( 159 | "UPDATE users SET OverwatchUsername = ? WHERE UserID = ?", 160 | username, 161 | ctx.author.id, 162 | ) 163 | db.execute( 164 | "UPDATE users SET OverwatchPlatform = ? WHERE UserID = ?", 165 | platform, 166 | ctx.author.id, 167 | ) 168 | db.execute( 169 | "UPDATE users SET OverwatchRegion = ? WHERE UserID = ?", 170 | region, 171 | ctx.author.id, 172 | ) 173 | db.commit() 174 | 175 | elif platform == "pc": 176 | embed = Embed( 177 | title="Setting Overwatch Profile:", 178 | description=f"Battle.net", 179 | colour=ctx.author.colour, 180 | ) 181 | 182 | embed.add_field(name="Overwatch Username", value=username) 183 | embed.set_thumbnail(url=ctx.author.avatar_url) 184 | 185 | db.execute( 186 | "UPDATE users SET OverwatchUsername = ? WHERE UserID = ?", 187 | username, 188 | ctx.author.id, 189 | ) 190 | db.execute( 191 | "UPDATE users SET OverwatchPlatform = ? WHERE UserID = ?", 192 | platform, 193 | ctx.author.id, 194 | ) 195 | db.execute( 196 | "UPDATE users SET OverwatchRegion = ? WHERE UserID = ?", 197 | region, 198 | ctx.author.id, 199 | ) 200 | db.commit() 201 | 202 | elif platform == "xbl": 203 | embed = Embed( 204 | title="Setting Overwatch Profile:", 205 | description=f"Xbox Live", 206 | colour=ctx.author.colour, 207 | ) 208 | 209 | embed.add_field(name="Overwatch Username", value=username) 210 | embed.set_thumbnail(url=ctx.author.avatar_url) 211 | 212 | db.execute( 213 | "UPDATE users SET OverwatchUsername = ? WHERE UserID = ?", 214 | username, 215 | ctx.author.id, 216 | ) 217 | db.execute( 218 | "UPDATE users SET OverwatchPlatform = ? WHERE UserID = ?", 219 | platform, 220 | ctx.author.id, 221 | ) 222 | db.execute( 223 | "UPDATE users SET OverwatchRegion = ? WHERE UserID = ?", 224 | region, 225 | ctx.author.id, 226 | ) 227 | db.commit() 228 | 229 | else: 230 | platform = db.record( 231 | "SELECT OverwatchPlatform FROM users WHERE UserID = ?", ctx.author.id 232 | ) 233 | username = db.record( 234 | "SELECT OverwatchUsername FROM users WHERE UserID = ?", ctx.author.id 235 | ) 236 | region = db.record( 237 | "SELECT OverwatchRegion FROM users WHERE UserID = ?", ctx.author.id 238 | ) 239 | embed = Embed(title="Your Overwatch Profile", colour=ctx.author.colour) 240 | 241 | embed.add_field(name="Overwatch Username", value=username[0]) 242 | embed.add_field(name="Overwatch Platform", value=platform[0]) 243 | embed.add_field(name="Overwatch Region", value=region[0]) 244 | embed.set_thumbnail(url=ctx.author.avatar_url) 245 | 246 | await ctx.reply(embed=embed) 247 | 248 | @Cog.listener() 249 | async def on_ready(self): 250 | if not self.bot.ready: 251 | self.bot.cogs_ready.ready_up("gamestats") 252 | 253 | 254 | def setup(bot): 255 | bot.add_cog(gamestats(bot)) 256 | -------------------------------------------------------------------------------- /lib/bot/__init__.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep 2 | import datetime 3 | from rich.logging import RichHandler 4 | 5 | from glob import glob 6 | 7 | import discord 8 | from discord import Embed, Colour, Client, Intents 9 | 10 | import logging 11 | 12 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 13 | from discord.errors import Forbidden 14 | from pathlib import Path 15 | 16 | from discord.ext.commands import Bot as BotBase 17 | from discord.ext.commands import Context, when_mentioned_or, has_permissions 18 | from discord.ext.commands import ( 19 | CommandNotFound, 20 | BadArgument, 21 | MissingRequiredArgument, 22 | CommandOnCooldown, 23 | MissingPermissions, 24 | EmojiNotFound, 25 | NotOwner, 26 | AutoShardedBot, 27 | ) 28 | 29 | import time as t 30 | 31 | import os 32 | 33 | from discord_slash import SlashCommand 34 | 35 | from ..db import db # pylint: disable=relative-beyond-top-level 36 | 37 | from dotenv import load_dotenv 38 | 39 | import json 40 | 41 | with open("config.json") as config_file: 42 | config = json.load(config_file) 43 | 44 | cwd = Path(__file__).parents[0] 45 | cwd = str(cwd) 46 | 47 | # Loads the .env file from ./.env 48 | load_dotenv() 49 | 50 | # Put Owner's Discord IDs into the list below 51 | OWNER_IDS = config["owner_ids"] 52 | # Loads the cogs from the path 53 | COGS = [path.split(os.sep)[-1][:-3] for path in glob("./lib/cogs/*.py")] 54 | IGNORE_EXCEPTIONS = (CommandNotFound, BadArgument) 55 | 56 | # Gets the prefix from the DB 57 | def get_prefix(bot, message): 58 | prefix = db.field("SELECT Prefix FROM guilds WHERE GuildID = ?", message.guild.id) 59 | return when_mentioned_or(prefix)(bot, message) 60 | 61 | 62 | log_level = logging.DEBUG if config["dev_mode"] else logging.INFO 63 | log = logging.getLogger() 64 | 65 | logging.basicConfig( 66 | level=log_level, 67 | format="%(name)s - %(message)s", 68 | datefmt="%X", 69 | handlers=[RichHandler()], 70 | ) 71 | 72 | 73 | class NoRunningFilter(logging.Filter): 74 | def filter(self, record): 75 | return not record.msg.startswith("Running job") 76 | 77 | 78 | class NoRunningFilter2(logging.Filter): 79 | def filter(self, record): 80 | return not record.msg.startswith("Job") 81 | 82 | 83 | running_job_filter = NoRunningFilter() 84 | job_filter = NoRunningFilter2() 85 | logging.getLogger("apscheduler.executors.default").addFilter(running_job_filter) 86 | logging.getLogger("apscheduler.executors.default").addFilter(job_filter) 87 | 88 | 89 | class Ready(object): 90 | def __init__(self): 91 | for cog in COGS: 92 | setattr(self, cog, False) 93 | 94 | def ready_up(self, cog): 95 | setattr(self, cog, True) 96 | log.info(f"{cog} cog ready") 97 | 98 | def all_ready(self): 99 | return all(getattr(self, cog) for cog in COGS) 100 | 101 | 102 | class AutoShardedBot(AutoShardedBot): 103 | def __init__(self): 104 | self.ready = False 105 | self.cogs_ready = Ready() 106 | 107 | self.guild = None 108 | self.scheduler = AsyncIOScheduler() 109 | 110 | db.autosave(self.scheduler) 111 | 112 | # Gives access to Discord's intents. 113 | # If you have a bot with over 75 servers, you will need to get whitelisted to use these. If not, you can enable them in your developer dashboard at https://discord.dev 114 | intents = discord.Intents.default() 115 | intents.members = True 116 | intents.presences = False 117 | 118 | super().__init__( 119 | command_prefix=get_prefix, 120 | owner_ids=OWNER_IDS, 121 | chunk_guilds_at_startup=True, 122 | intents=intents, 123 | case_insensitive=True, 124 | help_command=None, 125 | ) 126 | 127 | def setup(self): 128 | for cog in COGS: 129 | self.load_extension(f"lib.cogs.{cog}") 130 | log.info(f"[COGS] {cog} cog loaded!") 131 | 132 | log.info("Setup done!") 133 | 134 | def update_db(self): 135 | db.multiexec( 136 | "INSERT OR IGNORE INTO guilds (GuildID) VALUES (?)", 137 | ((guild.id,) for guild in self.guilds), 138 | ) 139 | db.commit() 140 | 141 | def run(self, version): 142 | self.VERSION = version 143 | 144 | log.info("Running setup!") 145 | self.setup() 146 | log.info("Authenticated...") 147 | log.info("Starting up") 148 | # Gets the token from the .env to authenticate the bot. 149 | 150 | # The following "fmt" comments are so that black, Doob's code style of choice, doesn't touch this line, if black touches it, then some Ubuntu machines can't run the bot. 151 | 152 | # fmt: off 153 | super().run(os.environ.get('TOKEN'), reconnect=True) 154 | # fmt: on 155 | 156 | async def process_commands(self, message): 157 | ctx = await self.get_context(message, cls=Context) 158 | 159 | if ctx.command is not None and ctx.guild is not None: 160 | if self.ready: 161 | await self.invoke(ctx) 162 | 163 | else: 164 | await ctx.reply( 165 | "Please wait, Doob hasn't fully started up yet ", 166 | delete_after=10, 167 | ) 168 | 169 | async def on_connect(self): 170 | self.update_db() 171 | log.info("Doob Connected") 172 | 173 | async def on_disconnect(self): 174 | log.info("Doob Disconnected") 175 | 176 | async def on_error(self, err, *args, **kwargs): 177 | if err == "on_command_error": 178 | raise err 179 | 180 | # Basic error handling for Doob 181 | async def on_command_error(self, ctx, exc): 182 | # if any([isinstance(exc, error) for error in IGNORE_EXCEPTIONS]): 183 | # await ctx.reply( 184 | # f"Something went wrong!\n\nError: {exc.original}", delete_after=10 185 | # ) 186 | 187 | if isinstance(exc, MissingRequiredArgument): 188 | await ctx.reply("Required arguments missing.", delete_after=10) 189 | 190 | elif isinstance(exc, CommandOnCooldown): 191 | await ctx.reply( 192 | f'That command is on a {str(exc.cooldown.type).split(".")[-1]} cooldown! Try again in {exc.retry_after:,.2f} seconds.', 193 | delete_after=exc.retry_after, 194 | ) 195 | 196 | elif isinstance(exc, MissingPermissions): 197 | await ctx.reply("You don't have permissions for that.", delete_after=10) 198 | 199 | elif isinstance(exc, EmojiNotFound): 200 | await ctx.reply( 201 | "This emote could not be found. This is likely because Doob isn't in the same server as this emote.", 202 | delete_after=10, 203 | ) 204 | 205 | elif isinstance(exc, NotOwner): 206 | await ctx.reply( 207 | "This command is only available to the bot owner.", 208 | delete_after=10, 209 | ) 210 | 211 | elif hasattr(exc, "original"): 212 | if isinstance(exc.original, Forbidden): 213 | await ctx.reply( 214 | "Doob doesn't have permissions to do that.", delete_after=10 215 | ) 216 | 217 | else: 218 | raise exc.original 219 | 220 | else: 221 | raise exc 222 | 223 | async def on_ready(self): 224 | if not self.ready: 225 | self.scheduler.start() 226 | while not self.cogs_ready.all_ready(): 227 | await sleep(1.0) 228 | 229 | # Puts all users into the users DB 230 | # db.multiexec( 231 | # "INSERT OR IGNORE INTO users (UserID) VALUES (?)", 232 | # ( 233 | # (member.id,) 234 | # for guild in self.guilds 235 | # for member in guild.members 236 | # if not member.bot 237 | # ), 238 | # ) 239 | # log.info("Updated users table.") 240 | 241 | # db.multiexec(f"INSERT OR IGNORE INTO globalwarns (UserID) VALUES (?)", member.id) 242 | # db.multiexec( 243 | # "INSERT OR IGNORE INTO globalwarns (UserID) VALUES (?)", 244 | # ( 245 | # (member.id,) 246 | # for guild in self.guilds 247 | # for member in guild.members 248 | # if not member.bot 249 | # ), 250 | # ) 251 | # log.info("Updated global warns table.") 252 | 253 | # # Puts all users in the votes DB 254 | # db.multiexec( 255 | # "INSERT OR IGNORE INTO votes (UserID) VALUES (?)", 256 | # ( 257 | # (member.id,) 258 | # for guild in self.guilds 259 | # for member in guild.members 260 | # if not member.bot 261 | # ), 262 | # ) 263 | # log.info("Updated votes table.") 264 | 265 | self.ready = True 266 | self.update_db 267 | 268 | log.info("Updated DB") 269 | log.info("Doob Ready") 270 | 271 | meta = self.get_cog("Meta") 272 | await meta.set() 273 | 274 | else: 275 | log.info("Doob Reconnected") 276 | 277 | async def on_message(self, message): 278 | def read_json(filename): 279 | with open(f"./lib/cogs/{filename}.json", "r") as file: 280 | data = json.load(file) 281 | return data 282 | 283 | def write_json(data, filename): 284 | with open(f"./lib/cogs/{filename}.json", "w") as file: 285 | data = json.dump(data, file) 286 | return data 287 | 288 | blacklisted_users = read_json("blacklisted_users") 289 | afk = read_json("afk") 290 | 291 | if ( 292 | not message.author.bot 293 | and message.author.id not in blacklisted_users["blacklist"] 294 | ): 295 | await self.process_commands(message) 296 | # If someone types a message, then they get inserted into the guildexp and luckydogs DB 297 | db.execute( 298 | "INSERT OR IGNORE INTO guildexp (UserID, GuildID) VALUES (?, ?)", 299 | message.author.id, 300 | message.guild.id, 301 | ) 302 | db.execute( 303 | "INSERT OR IGNORE INTO luckydogs (UserID) VALUES (?)", message.author.id 304 | ) 305 | 306 | db.execute( 307 | f"INSERT OR IGNORE INTO warns (UserID, GuildID) VALUES (?, ?)", 308 | message.author.id, 309 | message.guild.id, 310 | ) 311 | db.execute( 312 | f"INSERT OR IGNORE INTO globalwarns (UserID) VALUES (?)", 313 | message.author.id, 314 | ) 315 | 316 | db.commit() 317 | 318 | if message.author.id in blacklisted_users[ 319 | "blacklist" 320 | ] and message.content.startswith( 321 | db.field("SELECT Prefix FROM guilds WHERE GuildID = ?", message.guild.id) 322 | ): 323 | await message.channel.send( 324 | "You are blacklisted from using Doob commands.", delete_after=10 325 | ) 326 | 327 | if ( 328 | db.field( 329 | "SELECT YesNoReaction FROM guilds WHERE GuildID = ?", message.guild.id 330 | ) 331 | == "yes" 332 | ): 333 | if "y/n" in message.content.lower(): 334 | emojis = ["✅", "❌"] 335 | 336 | for emoji in emojis: 337 | await message.add_reaction(emoji) 338 | 339 | if str(message.author.id) in afk: 340 | message_afk = afk.pop(str(message.author.id)) 341 | 342 | write_json(afk, "afk") 343 | await message.channel.send( 344 | f"{message.author.mention} is no longer AFK\nMessage: {message_afk['message']}" 345 | ) 346 | 347 | 348 | bot = AutoShardedBot() 349 | bot.load_extension("jishaku") 350 | slash = SlashCommand(bot, sync_commands=True, sync_on_cog_reload=True) 351 | -------------------------------------------------------------------------------- /lib/cogs/twitch.py: -------------------------------------------------------------------------------- 1 | from discord.ext.commands import Cog, command, BucketType, cooldown, group 2 | from discord import Embed, Colour 3 | 4 | from ..db import db # pylint: disable=relative-beyond-top-level 5 | 6 | from aiohttp import request 7 | 8 | from datetime import datetime 9 | 10 | from typing import Optional 11 | 12 | import os 13 | from dotenv import load_dotenv 14 | 15 | load_dotenv() 16 | 17 | 18 | class Twitch(Cog): 19 | def __init__(self, bot): 20 | self.bot = bot 21 | 22 | async def twitch_search(self, ctx, username: str): 23 | User_URL = f"https://api.twitch.tv/kraken/users?login={username}" 24 | async with request( 25 | "GET", 26 | User_URL, 27 | headers={ 28 | "Client-ID": os.environ.get("twitchclientid"), 29 | "Accept": "application/vnd.twitchtv.v5+json", 30 | }, 31 | ) as response: 32 | if response.status == 200: 33 | User_ID = (await response.json())["users"][0]["_id"] 34 | 35 | StreamInfo_URL = f"https://api.twitch.tv/kraken/streams/{User_ID}" 36 | 37 | async with request( 38 | "GET", 39 | StreamInfo_URL, 40 | headers={ 41 | "Client-ID": os.environ.get("twitchclientid"), 42 | "Accept": "application/vnd.twitchtv.v5+json", 43 | }, 44 | ) as response2: 45 | if response2.status == 200: 46 | if (await response2.json())["stream"] is None: 47 | UserInfo_URL = ( 48 | f"https://api.twitch.tv/kraken/channels/{User_ID}" 49 | ) 50 | UserInfo2_URL = ( 51 | f"https://api.twitch.tv/kraken/users/{User_ID}" 52 | ) 53 | async with request( 54 | "GET", 55 | UserInfo_URL, 56 | headers={ 57 | "Client-ID": os.environ.get("twitchclientid"), 58 | "Accept": "application/vnd.twitchtv.v5+json", 59 | }, 60 | ) as response3: 61 | async with request( 62 | "GET", 63 | UserInfo2_URL, 64 | headers={ 65 | "Client-ID": os.environ.get("twitchclientid"), 66 | "Accept": "application/vnd.twitchtv.v5+json", 67 | }, 68 | ) as response4: 69 | embed = Embed( 70 | title=f"{(await response3.json())['display_name']} User Info", 71 | colour=ctx.author.colour, 72 | timestamp=datetime.utcnow(), 73 | ) 74 | 75 | fields = [ 76 | ( 77 | "Name", 78 | (await response3.json())["display_name"], 79 | False, 80 | ), 81 | ("Bio", (await response4.json())["bio"], False), 82 | ( 83 | "Account Type", 84 | (await response4.json())["type"], 85 | True, 86 | ), 87 | ( 88 | "Creation Date", 89 | (await response3.json())["created_at"], 90 | False, 91 | ), 92 | ( 93 | "Last Updated", 94 | (await response3.json())["updated_at"], 95 | False, 96 | ), 97 | ( 98 | "Followers", 99 | (await response3.json())["followers"], 100 | True, 101 | ), 102 | ( 103 | "Partner Status", 104 | (await response3.json())["partner"], 105 | True, 106 | ), 107 | ( 108 | "Language", 109 | (await response3.json())["language"], 110 | True, 111 | ), 112 | ("URL", (await response3.json())["url"], False), 113 | ] 114 | 115 | for name, value, inline in fields: 116 | embed.add_field( 117 | name=name, value=value, inline=inline 118 | ) 119 | 120 | embed.set_thumbnail( 121 | url=(await response3.json())["logo"] 122 | ) 123 | 124 | if (await response3.json())[ 125 | "profile_banner" 126 | ] != None: 127 | embed.set_image( 128 | url=(await response3.json())[ 129 | "profile_banner" 130 | ] 131 | ) 132 | 133 | await ctx.reply(embed=embed) 134 | 135 | else: 136 | embed = Embed( 137 | title=f"{(await response2.json())['stream']['channel']['display_name']} Stream Info", 138 | colour=Colour.dark_purple(), 139 | timestamp=datetime.utcnow(), 140 | ) 141 | 142 | fields = [ 143 | ( 144 | "Name", 145 | f"{(await response2.json())['stream']['channel']['display_name']}", 146 | False, 147 | ), 148 | ( 149 | "Title", 150 | f"{(await response2.json())['stream']['channel']['status']}", 151 | True, 152 | ), 153 | ( 154 | "Game", 155 | f"{(await response2.json())['stream']['channel']['game']}", 156 | True, 157 | ), 158 | ( 159 | "Viewers", 160 | f"{(await response2.json())['stream']['viewers']}", 161 | True, 162 | ), 163 | ( 164 | "Lagnuage", 165 | f"{(await response2.json())['stream']['channel']['broadcaster_language']}", 166 | True, 167 | ), 168 | ( 169 | "Followers", 170 | f"{(await response2.json())['stream']['channel']['followers']}", 171 | True, 172 | ), 173 | ( 174 | "Patner Status", 175 | f"{(await response2.json())['stream']['channel']['partner']}", 176 | True, 177 | ), 178 | ( 179 | "Went live at:", 180 | f"{(await response2.json())['stream']['created_at']}", 181 | True, 182 | ), 183 | ( 184 | "URL", 185 | (await response2.json())["stream"]["channel"][ 186 | "url" 187 | ], 188 | False, 189 | ), 190 | ] 191 | 192 | for name, value, inline in fields: 193 | embed.add_field(name=name, value=value, inline=inline) 194 | 195 | embed.set_image( 196 | url=(await response2.json())["stream"]["preview"][ 197 | "large" 198 | ] 199 | ) 200 | embed.set_thumbnail( 201 | url=(await response2.json())["stream"]["channel"][ 202 | "logo" 203 | ] 204 | ) 205 | 206 | await ctx.reply(embed=embed) 207 | 208 | @group( 209 | name="twitch", 210 | aliases=["lookup", "streamlookup", "twitchsearch", "twitchlookup", "stream"], 211 | brief="Get Twitch stream information.", 212 | ) 213 | @cooldown(1, 5, BucketType.user) 214 | async def twitch(self, ctx): 215 | """Request some information on a specific Twitch Stream/User!\n`Username` = Twitch Username""" 216 | prefix = db.field("SELECT Prefix FROM guilds WHERE GuildID = ?", ctx.guild.id) 217 | if ctx.invoked_subcommand is None: 218 | await ctx.reply( 219 | f"Doing `{prefix}twitch` doesn't work anymore! Looking to search someone? Try `{prefix}twitch -search {username}`" 220 | ) 221 | 222 | @twitch.command(name="-search", aliases=["-s"]) 223 | async def twitch_search_command(self, ctx, username: str): 224 | await self.twitch_search(ctx, username=username) 225 | 226 | @twitch_search_command.error 227 | async def twitch_search_command_error(self, ctx, exc): 228 | if hasattr(exc, "original") and isinstance(exc.original, IndexError): 229 | await ctx.reply("User does not seem to exist on Twitch.tv") 230 | 231 | @twitch.command(name="-title", aliases=["-t"]) 232 | async def twitch_title_command(self, ctx, username: str): 233 | User_URL = f"https://api.twitch.tv/kraken/users?login={username}" 234 | async with request( 235 | "GET", 236 | User_URL, 237 | headers={ 238 | "Client-ID": os.environ.get("twitchclientid"), 239 | "Accept": "application/vnd.twitchtv.v5+json", 240 | }, 241 | ) as response: 242 | if response.status == 200: 243 | User_ID = (await response.json())["users"][0]["_id"] 244 | 245 | StreamInfo_URL = f"https://api.twitch.tv/kraken/streams/{User_ID}" 246 | 247 | async with request( 248 | "GET", 249 | StreamInfo_URL, 250 | headers={ 251 | "Client-ID": os.environ.get("twitchclientid"), 252 | "Accept": "application/vnd.twitchtv.v5+json", 253 | }, 254 | ) as response2: 255 | if ( 256 | response2.status == 200 257 | and (await response2.json())["stream"] != None 258 | ): 259 | embed = Embed( 260 | title=f"{(await response2.json())['stream']['channel']['display_name']} Stream Info", 261 | colour=Colour.dark_purple(), 262 | timestamp=datetime.utcnow(), 263 | ) 264 | 265 | fields = [ 266 | ( 267 | "Title", 268 | f"{(await response2.json())['stream']['channel']['status']}", 269 | True, 270 | ), 271 | ( 272 | "Game", 273 | f"{(await response2.json())['stream']['channel']['game']}", 274 | True, 275 | ), 276 | ] 277 | 278 | for name, value, inline in fields: 279 | embed.add_field(name=name, value=value, inline=inline) 280 | 281 | embed.set_thumbnail( 282 | url=(await response2.json())["stream"]["channel"]["logo"] 283 | ) 284 | 285 | await ctx.reply(embed=embed) 286 | 287 | @Cog.listener() 288 | async def on_ready(self): 289 | if not self.bot.ready: 290 | self.bot.cogs_ready.ready_up("twitch") 291 | 292 | 293 | def setup(bot): 294 | bot.add_cog(Twitch(bot)) 295 | -------------------------------------------------------------------------------- /lib/cogs/log.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from discord import Embed 4 | from discord.ext.commands import Cog 5 | 6 | from ..db import db # pylint: disable=relative-beyond-top-level 7 | 8 | 9 | class Log(Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | 13 | @Cog.listener() 14 | async def on_ready(self): 15 | if not self.bot.ready: 16 | self.bot.cogs_ready.ready_up("log") 17 | 18 | @Cog.listener() 19 | async def on_member_join(self, member): 20 | logchannel = await self.bot.fetch_channel( 21 | db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", member.guild.id) 22 | ) 23 | 24 | creation_date = member.created_at.strftime("%m/%d/%Y %H:%M;%S") 25 | 26 | globalwarns = db.records(f"SELECT Warns FROM warns WHERE UserID = {member.id}")[ 27 | 0 28 | ][0] 29 | 30 | embed = Embed( 31 | title="New Member!", 32 | description=f"{member.display_name} has joined the server!" 33 | + f"\n\n‣ ID: {member.id}" 34 | + f"\n‣ Account Creation Date: {creation_date}" 35 | + f"\n‣ Global Doob Warns: {globalwarns}" 36 | + f"\n‣ Bot?: {member.bot}", 37 | colour=0x00D138, 38 | timestamp=datetime.utcnow(), 39 | ) 40 | 41 | embed.set_thumbnail(url=member.avatar_url) 42 | 43 | await logchannel.send(embed=embed) 44 | 45 | @Cog.listener() 46 | async def on_member_remove(self, member): 47 | logchannel = await self.bot.fetch_channel( 48 | db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", member.guild.id) 49 | ) 50 | 51 | embed = Embed( 52 | title="Member Removed.", 53 | description=f"{member.display_name} has been removed from the server." 54 | + f"\n\n‣ ID: {member.id}" 55 | + f"\n‣ Bot?: {member.bot}", 56 | colour=0xD10003, 57 | timestamp=datetime.utcnow(), 58 | ) 59 | 60 | embed.set_thumbnail(url=member.avatar_url) 61 | 62 | await logchannel.send(embed=embed) 63 | 64 | @Cog.listener() 65 | async def on_member_update(self, before, after): 66 | if before.display_name != after.display_name: 67 | logchannel = await self.bot.fetch_channel( 68 | db.field( 69 | "SELECT LogChannel FROM guilds WHERE GuildID = ?", after.guild.id 70 | ) 71 | ) 72 | embed = Embed( 73 | title="Member update", 74 | description=f"{before.name}'s Nickname has been changed.", 75 | colour=after.colour, 76 | timestamp=datetime.utcnow(), 77 | ) 78 | 79 | fields = [ 80 | ("Before", before.display_name, False), 81 | ("After", after.display_name, False), 82 | ] 83 | 84 | for name, value, inline in fields: 85 | embed.add_field(name=name, value=value, inline=inline) 86 | embed.set_thumbnail(url=before.avatar_url) 87 | await logchannel.send(embed=embed) 88 | 89 | elif before.roles != after.roles: 90 | embed = Embed( 91 | title="Member update", 92 | description=f"{before.name}'s Roles has been changed.", 93 | colour=after.colour, 94 | timestamp=datetime.utcnow(), 95 | ) 96 | logchannel = await self.bot.fetch_channel( 97 | db.field( 98 | "SELECT LogChannel FROM guilds WHERE GuildID = ?", after.guild.id 99 | ) 100 | ) 101 | 102 | fields = [ 103 | ("Before", ", ".join(r.mention for r in before.roles), False), 104 | ("After", ", ".join(r.mention for r in after.roles), False), 105 | ] 106 | 107 | for name, value, inline in fields: 108 | embed.add_field(name=name, value=value, inline=inline) 109 | embed.set_thumbnail(url=before.avatar_url) 110 | await logchannel.send(embed=embed) 111 | 112 | # Currently broken due to me not knowing how to grab the guild id from an on_user_update event, fix coming 113 | 114 | # @Cog.listener() 115 | # async def on_user_update(self, before, after): 116 | # if before.avatar_url != after.avatar_url: 117 | # logchannel = await self.bot.fetch_channel(db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", before.guild.id)) 118 | # embed = Embed(title=f"{before.name}'s avatar has been changed.'", description="Avatar has been changed. BEFORE -->", timestamp=datetime.utcnow()) 119 | # embed.add_field(name="AFTER", value="v (below this)") 120 | # embed.set_thumbnail(url=before.avatar_url) 121 | # embed.set_image(url=after.avatar_url) # KEEP THIS AS SET_IMAGE 122 | # await logchannel.send(embed=embed) 123 | 124 | # if before.name != after.name: 125 | # logchannel = await self.bot.fetch_channel(db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", before.guild.id)) 126 | # embed = Embed(title="Member update", description=f"{before.name}'s name has been changed.", timestamp=datetime.utcnow()) 127 | 128 | # fields = [("Before", before.name, False), 129 | # ("After", after.name, False)] 130 | 131 | # for name, value, inline in fields: 132 | # embed.add_field(name=name, value=value, inline=inline) 133 | # embed.set_thumbnail(url=before.author.avatar_url) 134 | # await logchannel.send(embed=embed) 135 | 136 | # if before.discriminator != after.discriminator: 137 | # logchannel = await self.bot.fetch_channel(db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", before.guild.id)) 138 | # embed = Embed(title="Member update", description=f"{before.name}'s Discriminator has been changed.", timestamp=datetime.utcnow()) 139 | 140 | # fields = [("Before", before.discriminator, False), 141 | # ("After", after.discriminator, False)] 142 | 143 | # for name, value, inline in fields: 144 | # embed.add_field(name=name, value=value, inline=inline) 145 | # embed.set_thumbnail(url=before.author.avatar_url) 146 | # await logchannel.send(embed=embed) 147 | 148 | @Cog.listener() 149 | async def on_message_edit(self, before, after): 150 | if not after.author.bot and before.content != after.content: 151 | logchannel = await self.bot.fetch_channel( 152 | db.field( 153 | "SELECT LogChannel FROM guilds WHERE GuildID = ?", 154 | after.guild.id, 155 | ) 156 | ) 157 | embed = Embed( 158 | title="Message update", 159 | description=f"Message from: {after.author.name}", 160 | colour=after.author.colour, 161 | timestamp=datetime.utcnow(), 162 | ) 163 | 164 | fields = [ 165 | ("Before", before.content, False), 166 | ("After", after.content, False), 167 | ("Channel", after.channel, False), 168 | ] 169 | 170 | for name, value, inline in fields: 171 | embed.add_field(name=name, value=value, inline=inline) 172 | 173 | embed.set_thumbnail(url=before.author.avatar_url) 174 | await logchannel.send(embed=embed) 175 | 176 | @Cog.listener() 177 | async def on_message_delete(self, message): 178 | if not message.author.bot: 179 | logchannel = await self.bot.fetch_channel( 180 | db.field( 181 | "SELECT LogChannel FROM guilds WHERE GuildID = ?", message.guild.id 182 | ) 183 | ) 184 | embed = Embed( 185 | title="Message Deleted", 186 | description=f"Message from: {message.author.name}", 187 | colour=message.author.colour, 188 | timestamp=datetime.utcnow(), 189 | ) 190 | 191 | embed.add_field(name="Message:", value=message.content, inline=False) 192 | embed.add_field(name="Channel:", value=message.channel, inline=False) 193 | embed.set_thumbnail(url=message.author.avatar_url) 194 | 195 | await logchannel.send(embed=embed) 196 | 197 | @Cog.listener() 198 | async def on_guild_channel_create(self, channel): 199 | logchannel = await self.bot.fetch_channel( 200 | db.field( 201 | "SELECT LogChannel FROM guilds WHERE GuildID = ?", channel.guild.id 202 | ) 203 | ) 204 | 205 | embed = Embed( 206 | title="New Channel!", 207 | description=f"Name: {channel.mention}\nCategory: {channel.category}", 208 | colour=0x00D138, 209 | timestamp=datetime.utcnow(), 210 | ) 211 | 212 | embed.set_thumbnail(url=channel.guild.icon_url) 213 | 214 | await logchannel.send(embed=embed) 215 | 216 | @Cog.listener() 217 | async def on_guild_channel_delete(self, channel): 218 | logchannel = await self.bot.fetch_channel( 219 | db.field( 220 | "SELECT LogChannel FROM guilds WHERE GuildID = ?", channel.guild.id 221 | ) 222 | ) 223 | 224 | embed = Embed( 225 | title="Channel Removed.", 226 | description=f"Name: {channel.name}\nCategory: {channel.category}", 227 | colour=0xD10003, 228 | timestamp=datetime.utcnow(), 229 | ) 230 | 231 | embed.set_thumbnail(url=channel.guild.icon_url) 232 | 233 | await logchannel.send(embed=embed) 234 | 235 | @Cog.listener() 236 | async def on_guild_channel_update(self, before, after): 237 | logchannel = await self.bot.fetch_channel( 238 | db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", before.guild.id) 239 | ) 240 | 241 | embed = Embed( 242 | title="Channel Updated", colour=0xFFC31F, timestamp=datetime.utcnow() 243 | ) 244 | 245 | embed.set_thumbnail(url=before.guild.icon_url) 246 | 247 | if before.name != after.name: 248 | embed.add_field(name="Before Name:", value=before.name) 249 | embed.add_field(name="After Name:", value=after.name) 250 | await logchannel.send(embed=embed) 251 | 252 | if before.category != after.category: 253 | embed.add_field(name="Before Category:", value=before.category) 254 | embed.add_field(name="After Category:", value=after.category) 255 | await logchannel.send(embed=embed) 256 | 257 | @Cog.listener() 258 | async def on_guild_role_create(self, role): 259 | logchannel = await self.bot.fetch_channel( 260 | db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", role.guild.id) 261 | ) 262 | 263 | embed = Embed( 264 | title="New Role!", 265 | description=f"Name: {role.mention}\nID: {role.id}\nMentionable: {role.mentionable}\nPosition: {role.position}", 266 | colour=role.colour, 267 | timestamp=datetime.utcnow(), 268 | ) 269 | 270 | embed.set_thumbnail(url=role.guild.icon_url) 271 | 272 | await logchannel.send(embed=embed) 273 | 274 | @Cog.listener() 275 | async def on_guild_role_delete(self, role): 276 | logchannel = await self.bot.fetch_channel( 277 | db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", role.guild.id) 278 | ) 279 | 280 | embed = Embed( 281 | title="Role Deleted.", 282 | description=f"Name: {role.name}\nID: {role.id}\nMentionable: {role.mentionable}\nPosition: {role.position}", 283 | colour=role.colour, 284 | timestamp=datetime.utcnow(), 285 | ) 286 | 287 | embed.set_thumbnail(url=role.guild.icon_url) 288 | 289 | await logchannel.send(embed=embed) 290 | 291 | @Cog.listener() 292 | async def on_invite_create(self, invite): 293 | logchannel = await self.bot.fetch_channel( 294 | db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", invite.guild.id) 295 | ) 296 | 297 | embed = Embed( 298 | title="New Invite!", 299 | description=f"URL: {invite.url}\nInviter: {invite.inviter}\nMax Age: {invite.max_age}\nMax Uses: {invite.max_uses}", 300 | colour=0x00D138, 301 | timestamp=datetime.utcnow(), 302 | ) 303 | 304 | embed.set_thumbnail(url=invite.guild.icon_url) 305 | 306 | await logchannel.send(embed=embed) 307 | 308 | @Cog.listener() 309 | async def on_invite_delete(self, invite): 310 | logchannel = await self.bot.fetch_channel( 311 | db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", invite.guild.id) 312 | ) 313 | 314 | embed = Embed( 315 | title="Invite Deleted", 316 | description=f"URL: {invite.url}\nInviter: {invite.inviter}\nMax Age: {invite.max_age}\nMax Uses: {invite.max_uses}", 317 | colour=0xD10003, 318 | timestamp=datetime.utcnow(), 319 | ) 320 | 321 | embed.set_thumbnail(url=invite.guild.icon_url) 322 | 323 | await logchannel.send(embed=embed) 324 | 325 | @Cog.listener() 326 | async def on_member_ban(self, member): 327 | logchannel = await self.bot.fetch_channel( 328 | db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", member.guild.id) 329 | ) 330 | 331 | embed = Embed( 332 | title="Member Banned.", 333 | description=f"{member.display_name} has been banned from the server." 334 | + f"\n\n‣ ID: {member.id}" 335 | + f"\n‣ Bot?: {member.bot}", 336 | colour=0xD10003, 337 | timestamp=datetime.utcnow(), 338 | ) 339 | 340 | embed.set_thumbnail(url=member.avatar_url) 341 | 342 | await logchannel.send(embed=embed) 343 | 344 | @Cog.listener() 345 | async def on_member_unban(self, member): 346 | logchannel = await self.bot.fetch_channel( 347 | db.field("SELECT LogChannel FROM guilds WHERE GuildID = ?", member.guild.id) 348 | ) 349 | 350 | embed = Embed( 351 | title="Member Unbanned.", 352 | description=f"{member.display_name} has been unbanned from the server." 353 | + f"\n\n‣ ID: {member.id}" 354 | + f"\n‣ Bot?: {member.bot}", 355 | colour=0x00D138, 356 | timestamp=datetime.utcnow(), 357 | ) 358 | 359 | embed.set_thumbnail(url=member.avatar_url) 360 | 361 | await logchannel.send(embed=embed) 362 | 363 | 364 | def setup(bot): 365 | bot.add_cog(Log(bot)) 366 | -------------------------------------------------------------------------------- /lib/cogs/meta.py: -------------------------------------------------------------------------------- 1 | import io 2 | import logging 3 | from time import time 4 | from datetime import datetime, timedelta 5 | from typing import Optional 6 | import discord 7 | import textwrap 8 | from discord.ext import commands 9 | from traceback import format_exception 10 | from psutil import Process, virtual_memory 11 | from platform import python_version 12 | from apscheduler.triggers.cron import CronTrigger 13 | from discord.ext.buttons import Paginator 14 | import contextlib 15 | from asyncio import sleep 16 | from pathlib import Path 17 | 18 | from discord import Activity, ActivityType, Embed, Member 19 | from discord import __version__ as discord_version 20 | 21 | from discord.ext.commands import Cog, command, BucketType, cooldown, Greedy 22 | 23 | from discord.utils import get 24 | 25 | from discord_slash.utils.manage_commands import create_option 26 | from discord_slash import cog_ext, SlashContext 27 | 28 | from ..db import db # pylint: disable=relative-beyond-top-level 29 | 30 | import os 31 | 32 | log = logging.getLogger() 33 | 34 | cwd = Path(__file__).parents[0] 35 | cwd = str(cwd) 36 | 37 | import json 38 | 39 | with open("config.json") as config_file: 40 | config = json.load(config_file) 41 | 42 | owner_id = config["owner_ids"][0] 43 | 44 | 45 | class Meta(Cog): 46 | def __init__(self, bot): 47 | self.bot = bot 48 | 49 | self.message = "playing @Doob help | {users:,} members in {guilds:,} servers. Version - {VERSION}" 50 | 51 | bot.scheduler.add_job(self.set, CronTrigger(second=0)) 52 | 53 | @property 54 | def message(self): 55 | return self._message.format( 56 | users=len(self.bot.users), 57 | guilds=len(self.bot.guilds), 58 | VERSION=self.bot.VERSION, 59 | ) 60 | 61 | @message.setter 62 | def message(self, value): 63 | if value.split(" ")[0] not in ("playing", "watching", "listening", "streaming"): 64 | raise ValueError("Invalid activity.") 65 | 66 | self._message = value 67 | 68 | async def set(self): 69 | _type, _name = self.message.split(" ", maxsplit=1) 70 | await self.bot.change_presence( 71 | activity=Activity( 72 | name=_name, type=getattr(ActivityType, _type, ActivityType.playing) 73 | ) 74 | ) 75 | 76 | @command(name="setactivity", brief="Owner Only Command - Set the bot's activity") 77 | @commands.is_owner() 78 | async def set_activity_message(self, ctx, *, text: str): 79 | """Set the bot's `playing` or `watching`, etc status.\n`Owner` permission required.""" 80 | self.message = text 81 | await self.set() 82 | await ctx.reply(f"Bot Status has been updated to {text}") 83 | 84 | @command( 85 | name="support", 86 | aliases=["supportserver"], 87 | brief="Get a link to the Doob support server.", 88 | ) 89 | async def support_server_link(self, ctx): 90 | """Gives a link to the Doob Support Server where you can get help from the developer!""" 91 | await ctx.reply("Join the support server at: :link: https://discord.gg/hgQTTU7") 92 | 93 | @cog_ext.cog_slash( 94 | name="support", description="Get a link to the Doob support server." 95 | ) 96 | async def support_server_link_slashcmd(self, ctx): 97 | await ctx.send("Join the support server at: :link: https://discord.gg/hgQTTU7") 98 | 99 | @command( 100 | name="invite", 101 | aliases=["invitebot", "inv", "botinvite"], 102 | brief="Gives a link to invite Doob to your server.", 103 | ) 104 | async def doob_invite_link(self, ctx): 105 | """Gives you a link to invite Doob to another server!""" 106 | await ctx.reply( 107 | "You can invite the bot here! :link: " 108 | ) 109 | 110 | @cog_ext.cog_slash( 111 | name="invite", description="Gives a link to invite Doob to your server." 112 | ) 113 | async def doob_invite_link_slashcmd(self, ctx): 114 | await ctx.send("You can invite the bot here! :link: ") 115 | 116 | @command(name="ping", brief="Shows the bot's latency.") 117 | @cooldown(1, 10, BucketType.user) 118 | async def ping(self, ctx): 119 | """Ping Pong!~\nShows the bot latency and response time.""" 120 | start = time() 121 | message = await ctx.reply("Loading... ") 122 | end = time() 123 | await message.edit( 124 | content=f"Pong! :ping_pong: Latency: {self.bot.latency*1000:,.0f} ms. Response time: {(end-start)*1000:,.0f} ms." 125 | ) 126 | 127 | @command( 128 | name="shutdown", brief="Owner Only Command to shutdown the bot and save the DB." 129 | ) 130 | @commands.is_owner() 131 | async def shutdown(self, ctx): 132 | """Command to shutdown the bot and save it's database.\n`Owner` permission required""" 133 | await ctx.reply("Shutting down") 134 | 135 | db.commit() 136 | self.bot.scheduler.shutdown() 137 | await self.bot.logout() 138 | 139 | @command(name="restart", brief="Owner Only Command to restart the bot.") 140 | @commands.is_owner() 141 | async def restart(self, ctx): 142 | """Command to restart, and update the bot to its latest version.\n`Owner` permission required""" 143 | await ctx.reply("Restarting...") 144 | 145 | db.commit() 146 | self.bot.scheduler.shutdown() 147 | await self.bot.logout() 148 | 149 | log.info("Fetching latest version from doobdev/doob@master") 150 | os.system("git pull origin master") 151 | log.info("Installing requirements.txt") 152 | os.system("python3.9 -m pip install -r requirements.txt --force-reinstall") 153 | log.info("Starting bot.") 154 | os.system("python3.9 launcher.py") 155 | 156 | @command( 157 | name="update", brief="Owner Only Command to give a pretty embed for updates." 158 | ) 159 | @commands.is_owner() 160 | async def update_command(self, ctx, *, update: str): 161 | """Command to give people updates on why bot was going down / brief patch notes\n`Owner` permission required""" 162 | 163 | prefix = db.records("SELECT Prefix from guilds WHERE GuildID = ?", ctx.guild.id) 164 | 165 | with ctx.channel.typing(): 166 | await ctx.message.delete() 167 | embed = Embed(title="Update:", description=update, colour=ctx.author.colour) 168 | embed.set_author( 169 | name=f"All the patch notes for {self.bot.VERSION} available here.", 170 | url=f"https://github.com/doobdev/doob/blob/master/CHANGELOG.md#v{self.bot.VERSION.replace('.', '')}", 171 | ) 172 | embed.set_footer( 173 | text=f"Authored by: {ctx.author.display_name}", 174 | icon_url=ctx.author.avatar_url, 175 | ) 176 | await ctx.send(embed=embed) 177 | 178 | async def show_bot_info(self, ctx, patreon_status): 179 | embed = Embed( 180 | title="Doob Info <:doob:754762131085459498>", 181 | colour=ctx.author.colour, 182 | timestamp=datetime.utcnow(), 183 | ) 184 | 185 | bot_version = self.bot.VERSION 186 | 187 | proc = Process() 188 | with proc.oneshot(): 189 | uptime = timedelta(seconds=time() - proc.create_time()) 190 | cpu_time = timedelta( 191 | seconds=(cpu := proc.cpu_times()).system + cpu.user 192 | ) # pylint: disable=used-before-assignment 193 | mem_total = virtual_memory().total / (1025 ** 2) 194 | mem_of_total = proc.memory_percent() 195 | mem_usg = mem_total * (mem_of_total / 100) 196 | 197 | fields = [ 198 | ("Name", "Doob <:doob:754762131085459498>", False), 199 | ( 200 | "Description", 201 | "The multipurpose Discord Bot with global leveling and powerful logging tools for your server.", 202 | False, 203 | ), 204 | ("Developers", "<@308000668181069824>", False), 205 | ("Doob's Server Count", f"{str(len(self.bot.guilds))}", True), 206 | ("Doob's Member Count", f"{str(len(self.bot.users))}", True), 207 | ( 208 | "The ping for Doob is...", 209 | f" :ping_pong: {round(self.bot.latency * 1000)} ms", 210 | False, 211 | ), 212 | ("Python Version", python_version(), True), 213 | ("Uptime", uptime, True), 214 | ("CPU Time", cpu_time, True), 215 | ( 216 | "Memory Usage", 217 | f"{mem_usg:,.3f} MiB / {mem_total:,.0f} MiB ({mem_of_total:.0f}%)", 218 | True, 219 | ), 220 | ("Library", f"discord.py {discord_version}", True), 221 | ( 222 | "Bot Version", 223 | f"{self.bot.VERSION} - [Changelog](https://github.com/doobdev/doob/blob/master/CHANGELOG.md#v{bot_version.replace('.', '')})", 224 | True, 225 | ), 226 | ("Top.gg Link", "https://top.gg/bot/680606346952966177", False), 227 | ( 228 | "Invite Link", 229 | "[Invite Link Here](https://doob.link/invite)", 230 | True, 231 | ), 232 | ( 233 | "GitHub Repository", 234 | "[Click Here](https://github.com/doobdev/doob)", 235 | True, 236 | ), 237 | ] 238 | 239 | for name, value, inline in fields: 240 | embed.add_field(name=name, value=value, inline=inline) 241 | 242 | embed.set_thumbnail(url=ctx.guild.me.avatar_url) 243 | embed.set_footer( 244 | text=f"{ctx.author.name} requested Doob's information", 245 | icon_url=ctx.author.avatar_url, 246 | ) 247 | 248 | if patreon_status == True: 249 | embed.add_field( 250 | name="Patreon", 251 | value=f"Thanks for [Donating](https://patreon.com/doobdev) {ctx.author.display_name}! :white_check_mark:", 252 | inline=False, 253 | ) 254 | await ctx.reply(embed=embed) 255 | 256 | if patreon_status == False: 257 | embed.add_field( 258 | name="Patreon", 259 | value="[Click Here for Patreon](https://patreon.com/doobdev)", 260 | inline=False, 261 | ) 262 | await ctx.reply(embed=embed) 263 | 264 | @command(name="info", aliases=["botinfo"], brief="Gives basic info about Doob.") 265 | async def show_bot_info_command(self, ctx): 266 | """Gives basic info about Doob.""" 267 | 268 | homeGuild = self.bot.get_guild(config["homeGuild_id"]) # Support Server ID. 269 | patreonRole = get( 270 | homeGuild.roles, id=config["patreonRole_id"] 271 | ) # Patreon role ID. 272 | 273 | member = [] 274 | 275 | for pledger in homeGuild.members: 276 | if pledger == ctx.author: 277 | member = pledger 278 | 279 | if ctx.author in homeGuild.members and patreonRole in member.roles: 280 | await self.show_bot_info(ctx, patreon_status=True) 281 | 282 | else: 283 | await self.show_bot_info(ctx, patreon_status=False) 284 | 285 | @command( 286 | name="patreon", 287 | aliases=["donate", "donation"], 288 | brief="Show support to Doob Dev!", 289 | ) 290 | async def patreon_link(self, ctx): 291 | """Gives a link to the Patreon for Doob!\nWe apprecieate your support!~""" 292 | homeGuild = self.bot.get_guild(config["homeGuild_id"]) # Support Server ID. 293 | patreonRole = get( 294 | homeGuild.roles, id=config["patreonRole_id"] 295 | ) # Patreon role ID. 296 | 297 | member = [] 298 | 299 | for pledger in homeGuild.members: 300 | if pledger == ctx.author: 301 | member = pledger 302 | 303 | if ctx.author in homeGuild.members and patreonRole in member.roles: 304 | await ctx.reply( 305 | f"Thanks for supporting {ctx.author.mention}!\n" 306 | ) 307 | 308 | else: 309 | await ctx.reply( 310 | "You can support Doob Dev by subscribing at !" 311 | ) 312 | 313 | class Pag(Paginator): 314 | async def teardown(self): 315 | try: 316 | await self.page.clear_reactions() 317 | except discord.HTTPException: 318 | pass 319 | 320 | @command(name="eval", hidden=True) 321 | @commands.is_owner() 322 | async def _eval(self, ctx, *, code: str): 323 | def clean_code(content): 324 | if content.startswith("```") and content.endswith("```"): 325 | return "\n".join(content.split("\n")[1:])[:-3] 326 | else: 327 | return content 328 | 329 | code = clean_code(code) 330 | 331 | local_variables = { 332 | "discord": discord, 333 | "commands": commands, 334 | "bot": self.bot, 335 | "ctx": ctx, 336 | "channel": ctx.channel, 337 | "author": ctx.author, 338 | "guild": ctx.guild, 339 | "message": ctx.message, 340 | "self": self, 341 | } 342 | 343 | stdout = io.StringIO() 344 | 345 | try: 346 | with contextlib.redirect_stdout(stdout): 347 | exec( 348 | f"async def func():\n{textwrap.indent(code, ' ')}", 349 | local_variables, 350 | ) 351 | 352 | obj = await local_variables["func"]() 353 | result = f"{stdout.getvalue()}\n-- {obj}\n" 354 | 355 | except Exception as e: 356 | result = "".join(format_exception(e, e, e.__traceback__)) 357 | 358 | pager = self.Pag( 359 | timeout=100, 360 | entries=[result[i : i + 2000] for i in range(0, len(result), 2000)], 361 | length=1, 362 | prefix="```py\n", 363 | suffix="```", 364 | ) 365 | 366 | await pager.start(ctx) 367 | 368 | @command(name="blacklist", brief="Adds a user to the Doob blacklist.") 369 | @commands.is_owner() 370 | async def blacklist_user( 371 | self, ctx, users: Greedy[Member], *, reason: Optional[str] 372 | ): 373 | data = self.read_json("blacklisted_users") 374 | 375 | for user in users: 376 | if user.id not in data["blacklist"]: 377 | db.execute( 378 | "INSERT OR IGNORE INTO blacklist (UserID, Reason) VALUES (?, ?)", 379 | user.id, 380 | reason, 381 | ) 382 | db.commit() 383 | 384 | data["blacklist"].append(user.id) 385 | self.write_json(data, "blacklisted_users") 386 | await ctx.send(f"{user.name} has been blacklisted for {reason}.") 387 | await user.send( 388 | f"❌You have been blacklisted from using Doob for {reason}" 389 | ) 390 | else: 391 | await ctx.send(f"{user.name} is already blacklisted.") 392 | 393 | @command(name="unblacklist", brief="Removes a user from the Doob blacklist.") 394 | @commands.is_owner() 395 | async def unblacklist_user(self, ctx, users: Greedy[Member]): 396 | data = self.read_json("blacklisted_users") 397 | 398 | for user in users: 399 | if user.id in data["blacklist"]: 400 | db.execute("DELETE FROM blacklist WHERE UserID = ?", user.id) 401 | db.commit() 402 | 403 | data["blacklist"].remove(user.id) 404 | self.write_json(data, "blacklisted_users") 405 | 406 | await ctx.send(f"{user.name} has been unblacklisted.") 407 | await user.send(f"✅You have been unblacklisted from using Doob.") 408 | else: 409 | await ctx.send(f"{user.name} is not blacklisted.") 410 | 411 | def read_json(self, filename): 412 | with open(f"{cwd}/{filename}.json", "r") as file: 413 | data = json.load(file) 414 | return data 415 | 416 | def write_json(self, data, filename): 417 | with open(f"{cwd}/{filename}.json", "w") as file: 418 | json.dump(data, file, indent=4) 419 | 420 | @Cog.listener() 421 | async def on_ready(self): 422 | if not self.bot.ready: 423 | self.bot.cogs_ready.ready_up("meta") 424 | 425 | 426 | def setup(bot): 427 | bot.add_cog(Meta(bot)) 428 | -------------------------------------------------------------------------------- /lib/cogs/misc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from typing import Optional 4 | from discord.ext import commands 5 | from discord.ext.commands import Cog 6 | from discord.ext.commands import CheckFailure 7 | from discord.ext.commands import command, has_permissions, cooldown, BucketType 8 | from discord import Embed, Message, Emoji, TextChannel 9 | 10 | from discord_slash.utils.manage_commands import create_option 11 | from discord_slash import cog_ext 12 | 13 | from discord.utils import get 14 | 15 | from datetime import datetime 16 | 17 | import json 18 | 19 | with open("config.json") as config_file: 20 | config = json.load(config_file) 21 | 22 | import random 23 | 24 | from ..db import db # pylint: disable=relative-beyond-top-level 25 | 26 | owner_id = config["owner_ids"][0] 27 | 28 | import os 29 | from dotenv import load_dotenv 30 | import time as t 31 | 32 | load_dotenv() 33 | log = logging.getLogger() 34 | 35 | cwd = Path(__file__).parents[0] 36 | cwd = str(cwd) 37 | 38 | 39 | def read_json(filename): 40 | with open(f"./lib/cogs/{filename}.json", "r") as file: 41 | data = json.load(file) 42 | return data 43 | 44 | 45 | def write_json(data, filename): 46 | with open(f"{cwd}/{filename}.json", "w") as file: 47 | json.dump(data, file, indent=4) 48 | 49 | 50 | class Misc(Cog): 51 | def __init__(self, bot): 52 | self.bot = bot 53 | 54 | @command(name="afk", aliases=["away", "brb"]) 55 | @cooldown(1, 10, BucketType.user) 56 | async def afk_command(self, ctx, *, message: Optional[str] = "brb"): 57 | afk = read_json("afk") 58 | 59 | time = datetime.now() 60 | time_real = t.mktime(time.timetuple()) + time.microsecond / 1e6 61 | 62 | afk[str(ctx.author.id)] = {"message": message, "time": str(time_real)} 63 | 64 | write_json(afk, "afk") 65 | 66 | await ctx.send(f"{ctx.author.mention} is now AFK: {message}") 67 | 68 | @command(name="prefix", aliases=["ChangePrefix"], brief="Changes the prefix.") 69 | @has_permissions(manage_guild=True) 70 | async def change_prefix(self, ctx, new: str): 71 | """Changes the prefix for the server.\n`Manage Server` permission required.""" 72 | if len(new) > 10: 73 | await ctx.reply( 74 | "The prefix can not be more than 10 characters.", delete_after=10 75 | ) 76 | 77 | else: 78 | db.execute( 79 | "UPDATE guilds SET Prefix = ? WHERE GuildID = ?", new, ctx.guild.id 80 | ) 81 | embed = Embed( 82 | title="Prefix Changed", 83 | description=f"Prefix has been changed to `{new}`", 84 | ) 85 | await ctx.reply(embed=embed) 86 | 87 | @change_prefix.error 88 | async def change_prefix_error(self, ctx, exc): 89 | if isinstance(exc, CheckFailure): 90 | await ctx.reply( 91 | "You need the Manage Server permission to change the prefix.", 92 | delete_after=10, 93 | ) 94 | 95 | @command(name="poll", brief="Lets the user create a poll.") 96 | @cooldown(1, 4, BucketType.user) 97 | async def start_poll_command(self, ctx, *, question: str): 98 | """Starts a poll with the question/name the user wants!""" 99 | embed = Embed( 100 | title="Poll Started!", description=question, colour=ctx.author.colour 101 | ) 102 | embed.set_footer( 103 | text=f"{ctx.author} started this poll.", icon_url=ctx.author.avatar_url 104 | ) 105 | message = await ctx.send(embed=embed) 106 | 107 | emojis = ["✅", "❌"] 108 | 109 | for emoji in emojis: 110 | await message.add_reaction(emoji) 111 | 112 | @cog_ext.cog_slash( 113 | name="poll", 114 | description="Start a poll!", 115 | options=[ 116 | create_option( 117 | name="question", 118 | description="What you want to ask the users.", 119 | option_type=3, 120 | required=True, 121 | ) 122 | ], 123 | ) 124 | async def start_poll_slash(self, ctx, question: str): 125 | embed = Embed( 126 | title="Poll Started!", description=question, colour=ctx.author.colour 127 | ) 128 | embed.set_footer( 129 | text=f"{ctx.author} started this poll.", icon_url=ctx.author.avatar_url 130 | ) 131 | message = await ctx.send(embed=embed) 132 | 133 | emojis = ["✅", "❌"] 134 | 135 | for emoji in emojis: 136 | await message.add_reaction(emoji) 137 | 138 | @command(name="endpoll", brief="Lets a user end a poll.") 139 | @cooldown(1, 5, BucketType.user) 140 | async def end_poll_command(self, ctx, *, message_id: Message): 141 | """Ends the poll and shows results.""" 142 | channel = self.bot.get_channel(message_id.channel.id) 143 | message = await channel.fetch_message(message_id.id) 144 | 145 | reaction1 = get(message.reactions, emoji="✅") 146 | reaction2 = get(message.reactions, emoji="❌") 147 | 148 | if reaction1.count > reaction2.count: 149 | winner = "✅" 150 | winner_count = reaction1.count 151 | loser_count = reaction2.count 152 | elif reaction2.count > reaction1.count: 153 | winner = "❌" 154 | winner_count = reaction2.count 155 | loser_count = reaction1.count 156 | 157 | else: 158 | winner = "Tie!" 159 | 160 | if winner == "Tie!": 161 | embed = Embed( 162 | title="Poll ended!", description="Poll ended in a tie!", colour=0xFFFF00 163 | ) 164 | 165 | elif winner == "❌": 166 | embed = Embed( 167 | title="Poll ended!", 168 | description=f"{winner} has won by {winner_count-loser_count} votes!", 169 | colour=0xAE0700, 170 | ) 171 | 172 | elif winner == "✅": 173 | embed = Embed( 174 | title="Poll ended!", 175 | description=f"{winner} has won by {winner_count-loser_count} votes!", 176 | colour=0x66FF00, 177 | ) 178 | 179 | embed.set_footer( 180 | text=f"Poll ended by: {ctx.author.display_name}", 181 | icon_url=ctx.author.avatar_url, 182 | ) 183 | 184 | await message.edit(embed=embed) 185 | 186 | @cog_ext.cog_slash( 187 | name="endpoll", 188 | description="End an existing poll!", 189 | options=[ 190 | create_option( 191 | name="message_id", 192 | description="The poll's message ID you would like to end.", 193 | option_type=3, 194 | required=True, 195 | ), 196 | create_option( 197 | name="channel", 198 | description="Channel the poll was sent in.", 199 | option_type=7, 200 | required=True, 201 | ), 202 | ], 203 | ) 204 | async def end_poll_slash(self, ctx, channel: TextChannel, message_id: str): 205 | channel = self.bot.get_channel(channel.id) 206 | message = await channel.fetch_message(message_id) 207 | 208 | reaction1 = get(message.reactions, emoji="✅") 209 | reaction2 = get(message.reactions, emoji="❌") 210 | 211 | if reaction1.count > reaction2.count: 212 | winner = "✅" 213 | winner_count = reaction1.count 214 | loser_count = reaction2.count 215 | elif reaction2.count > reaction1.count: 216 | winner = "❌" 217 | winner_count = reaction2.count 218 | loser_count = reaction1.count 219 | 220 | else: 221 | winner = "Tie!" 222 | 223 | if winner == "Tie!": 224 | embed = Embed( 225 | title="Poll ended!", description="Poll ended in a tie!", colour=0xFFFF00 226 | ) 227 | 228 | elif winner == "❌": 229 | embed = Embed( 230 | title="Poll ended!", 231 | description=f"{winner} has won by {winner_count-loser_count} votes!", 232 | colour=0xAE0700, 233 | ) 234 | 235 | elif winner == "✅": 236 | embed = Embed( 237 | title="Poll ended!", 238 | description=f"{winner} has won by {winner_count-loser_count} votes!", 239 | colour=0x66FF00, 240 | ) 241 | 242 | embed.set_footer( 243 | text=f"Poll ended by: {ctx.author.display_name}", 244 | icon_url=ctx.author.avatar_url, 245 | ) 246 | 247 | await message.edit(embed=embed) 248 | 249 | await ctx.send("Poll Completed!", hidden=True) 250 | 251 | @command( 252 | name="giveaway", 253 | aliases=["startgiveaway"], 254 | brief="Creates a giveaway, that will choose a winner!", 255 | ) 256 | @cooldown(1, 5, BucketType.user) 257 | @has_permissions(administrator=True) 258 | async def start_giveaway(self, ctx, *, prize: str): 259 | """Lets a Server Admin start a giveaway\n`Server Administrator` permission required.""" 260 | embed = Embed( 261 | title=f"{prize} giveaway!", 262 | description=f"{ctx.author.display_name} is giving away {prize}!\nReact with 🎁!", 263 | ) 264 | embed.set_footer( 265 | text=f"{ctx.author} started this giveaway.", icon_url=ctx.author.avatar_url 266 | ) 267 | 268 | message = await ctx.reply(embed=embed) 269 | 270 | await message.add_reaction("🎁") 271 | 272 | @command( 273 | name="stopgiveaway", 274 | aliases=["endgiveaway"], 275 | brief="Stops the giveaway, this chooses the winner!", 276 | ) 277 | @cooldown(1, 5, BucketType.user) 278 | @has_permissions(administrator=True) 279 | async def stop_giveaway(self, ctx, *, message_id: Message): 280 | """Lets a Server Admin stop a giveaway, this chooses the winner at random!\n`Server Administrator` permission required.""" 281 | 282 | channel = self.bot.get_channel(message_id.channel.id) 283 | message = await channel.fetch_message(message_id.id) 284 | 285 | users = set() 286 | 287 | for reaction in message.reactions: 288 | async for user in reaction.users(): 289 | if not user.bot: 290 | users.add(user) 291 | 292 | entries = [] 293 | 294 | for user in users: 295 | if not user.bot: 296 | entries.append(user.id) 297 | 298 | winner = random.choice(entries) 299 | 300 | log.info(entries) 301 | 302 | await channel.send(f"<@{winner}> won the giveaway!") 303 | await channel.send( 304 | f"{ctx.author.mention}, the giveaway has been ended.", delete_after=30 305 | ) 306 | 307 | user = self.bot.get_user(winner) 308 | 309 | await user.send( 310 | f"You won the giveaway from <#{channel.id}> in {channel.guild.name}!" 311 | ) 312 | 313 | @command( 314 | name="timebomb", 315 | aliases=["tbmsg", "tb", "timebombmessage"], 316 | brief="Send a message with a timelimit on it.", 317 | ) 318 | @cooldown(1, 4, BucketType.user) 319 | async def time_bomb_command(self, ctx, time: int, *, message: str): 320 | """Makes a message with a timelimit on it, the time is in seconds.""" 321 | if time < 1000: 322 | await ctx.message.delete() 323 | 324 | embed = Embed( 325 | title="Time Message", description=message, colour=ctx.author.colour 326 | ) 327 | embed.set_thumbnail(url=ctx.author.avatar_url) 328 | embed.set_footer( 329 | text=f"{ctx.author} | This message only lasts {str(time)} seconds.", 330 | icon_url=ctx.author.avatar_url, 331 | ) 332 | 333 | await ctx.send(embed=embed, delete_after=time) 334 | 335 | else: 336 | await ctx.reply( 337 | f"Please try again with a lower time, {time} is too big.", 338 | delete_after=15, 339 | ) 340 | 341 | @command(name="vote", aliases=["upvote"], brief="Vote for Doob on Top.gg!") 342 | @cooldown(1, 4, BucketType.user) 343 | async def topgg_upvote_command(self, ctx): 344 | await ctx.reply("Vote for Doob at: https://top.gg/bot/680606346952966177/vote") 345 | 346 | @command(name="phone", aliases=["iphone"], brief="phone") 347 | @cooldown(1, 4, BucketType.user) 348 | async def phone_command(self, ctx): 349 | """phone\n`Patreon Only`""" 350 | homeGuild = self.bot.get_guild(config["homeGuild_id"]) # Support Server ID. 351 | patreonRole = get( 352 | homeGuild.roles, id=config["patreonRole_id"] 353 | ) # Patreon role ID. 354 | 355 | member = [] 356 | 357 | for pledger in homeGuild.members: 358 | if pledger == ctx.author: 359 | member = pledger 360 | 361 | if ctx.author in homeGuild.members and patreonRole in member.roles: 362 | await ctx.reply( 363 | "https://cdn.discordapp.com/attachments/721514198000992350/794840218514751499/IYUimA7gyac7sxrB3uKu9Mb1ZZOJVtgAAAA.png" 364 | ) 365 | 366 | else: 367 | await ctx.reply( 368 | "You are not a Patron to Doob, subscribe to any of the tiers at to gain access to this command." 369 | ) 370 | 371 | @Cog.listener() 372 | async def on_ready(self): 373 | if not self.bot.ready: 374 | self.bot.cogs_ready.ready_up("misc") 375 | 376 | @command( 377 | name="ownerprefix", 378 | aliases=["opo"], 379 | brief="Changes the prefix. | Bot Owner Override.", 380 | ) 381 | @commands.is_owner() 382 | async def override_change_prefix(self, ctx, new: str): 383 | """Changes the prefix for the server.\nOnly the bot owner can use the override command.""" 384 | 385 | prefix = db.records("SELECT Prefix FROM guilds WHERE GuildID = ?", ctx.guild.id) 386 | 387 | db.execute("UPDATE guilds SET Prefix = ? WHERE GuildID = ?", new, ctx.guild.id) 388 | embed = Embed( 389 | title="Prefix Changed", 390 | description=f"Prefix has been changed to `{new}`", 391 | ) 392 | 393 | await ctx.reply(embed=embed) 394 | 395 | @command( 396 | name="overlay", aliases=["streamkit"], brief="Gives Discord Streamkit Overlay." 397 | ) 398 | @cooldown(1, 5, BucketType.user) 399 | async def streamkit_overlay_command(self, ctx): 400 | """Gives a Discord Streamkit Overlay link for your Livestreams""" 401 | 402 | # super long URL so im just making it into a variable 403 | streamkit_url = f"https://streamkit.discord.com/overlay/voice/{ctx.guild.id}/{ctx.author.voice.channel.id}?icon=true&online=true&logo=white&text_color=%23ffffff&text_size=14&text_outline_color=%23000000&text_outline_size=0&text_shadow_color=%23000000&text_shadow_size=0&bg_color=%231e2124&bg_opacity=0.95&bg_shadow_color=%23000000&bg_shadow_size=0&invite_code=&limit_speaking=false&small_avatars=false&hide_names=false&fade_chat=0" 404 | 405 | embed = Embed( 406 | title="Streamkit Overlay:", 407 | description=streamkit_url, 408 | colour=ctx.author.colour, 409 | timestamp=datetime.utcnow(), 410 | ) 411 | embed.set_footer( 412 | text=f"Requested By: {ctx.author}", icon_url=ctx.message.author.avatar_url 413 | ) 414 | embed.set_thumbnail( 415 | url="https://upload.wikimedia.org/wikipedia/commons/thumb/7/78/OBS.svg/1024px-OBS.svg.png" 416 | ) 417 | await ctx.send(embed=embed) 418 | 419 | @streamkit_overlay_command.error 420 | async def streamkit_overlay_command_error(self, ctx, exc): 421 | if hasattr(exc, "original") and isinstance(exc.original, AttributeError): 422 | await ctx.message.delete() 423 | await ctx.send( 424 | "Please join a voice channel before running the `overlay` command.", 425 | delete_after=15, 426 | ) 427 | 428 | @command(name="emote", aliases=["emoji"], brief="Gets Emote info.") 429 | async def get_emote_command(self, ctx, emote: Emoji): 430 | embed = Embed( 431 | title=f"{emote.name} Info", 432 | colour=ctx.author.colour, 433 | timestamp=datetime.utcnow(), 434 | ) 435 | 436 | fields = [ 437 | ("Name", emote.name, False), 438 | ("ID", emote.id, False), 439 | ("Does the emote require colons?", emote.require_colons, False), 440 | ("Animated?", emote.animated, False), 441 | ("Twitch Sub Emote?", emote.managed, False), 442 | ("Guild", emote.guild.name, False), 443 | # ("Creator", emote.user.username, False), 444 | ("Created At", emote.created_at, False), 445 | ("URL", emote.url, False), 446 | ] 447 | 448 | for name, value, inline in fields: 449 | embed.add_field(name=name, value=value, inline=inline) 450 | 451 | embed.set_image(url=emote.url) 452 | 453 | await ctx.reply(embed=embed) 454 | 455 | 456 | def setup(bot): 457 | bot.add_cog(Misc(bot)) 458 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.9.10 2 | * Added y/n reaction messages [(#280)](https://github.com/DoobDev/Doob/issues/280) 3 | * Added `d!brb` command [(#291)](https://github.com/DoobDev/Doob/pull/291) 4 | 5 | ## v2.9.9 6 | * Changed most occurances of `d!` has been changed to the prefix set in the command. 7 | * Added config command [(#281)](https://github.com/DoobDev/Doob/issues/281) 8 | 9 | ## v2.9.8 10 | * Added Jishaku (Owner Only) [(#279)](https://github.com/DoobDev/Doob/issues/279) 11 | 12 | ## v2.9.7 13 | * Removed `d!adveq` 14 | 15 | ## v2.9.6 16 | * Added `d!deletetextchannel` 17 | * Added `d!deletevoicechannel` 18 | 19 | ## v2.9.5 20 | * Added a blacklisted field to `d!userinfo` 21 | 22 | ## v2.9.4 23 | * Added blacklisting 24 | 25 | ## v2.9.3 26 | * New Help Menu! [(#140)](https://github.com/DoobDev/issues/140) 27 | - Fun Fact! The issue referencing this was from Feburary! 28 | 29 | ## v2.9.2 30 | * Added `d!volume mute` 31 | 32 | ## v2.9.1 33 | * Added volume commands 34 | * `d!volume up` 35 | * `d!volume down` 36 | * `d!volume {percent}` 37 | * Added d!lryics 38 | * Added EQ commands 39 | * `d!eq` 40 | * `d!adveq` 41 | 42 | ## v2.9.0 43 | * Added buttons to `d!help` 44 | 45 | ## v2.8.10 46 | * Added new logtypes [(#242)](https://github.com/DoobDev/issues/242) 47 | 48 | ## v2.8.9 49 | * Added /dog [(#248)](https://github.com/DoobDev/Doob/issues/248) 50 | 51 | ## v2.8.8 52 | * Added member join logs. 53 | * Added member leave (remove) logs. 54 | 55 | ## v2.8.7 56 | * Added d!fortniteskin [(#231)](https://github.com/DoobDev/Doob/issues/231) 57 | 58 | ## v2.8.6 59 | * Added /poll [(#213)](https://github.com/DoobDev/Doob/issues/213) 60 | * Added /endpoll [(#213)](https://github.com/DoobDev/Doob/issues/213) 61 | * Reverted the November 2020 Log Changes (from v2.1.0) [(#211)](https://github.com/DoobDev/Doob/issues/211) 62 | * Shows Server XP Rank on on `d!level` [(#244)](https://github.com/DoobDev/Doob/issues/244) 63 | 64 | ## v2.8.5 65 | * Added "xp to next level" to `d!level` [(#221)](https://github.com/DoobDev/Doob/issues/221) 66 | 67 | ## v2.8.4 68 | * Added who made the link to short link logs ([#209](https://github.com/doobdev/Doob/issues/209)) 69 | * Added end poll command (d!endpoll) ([#212](https://github.com/doobdev/Doob/issues/212)) 70 | 71 | ## v2.8.3 72 | * Added unlimited short links for owners ([#207](https://github.com/doobdev/Doob/issues/207)) 73 | * Made a better description for d!link 74 | 75 | ## v2.8.2 76 | * Added short link logs ([#206](https://github.com/doobdev/Doob/issues/206)) 77 | 78 | ## v2.8.1 79 | * Changed invite link. 80 | 81 | ## v2.8.0 82 | * Added `d!coinfliptimes` 83 | - Flips a coin the amount of times you want, and shows you the outcome. 84 | * Added `d!link` 85 | - Shorten a link with doob.link! 86 | - 6 links for regular people 87 | - 12 links for [Patrons](https://patreon.com/doobdev/) 88 | * Added /link 89 | 90 | ## v2.7.2 91 | * Added /support 92 | * Added /invite 93 | 94 | ## v2.7.1 95 | * Added Error handling for EmojiNotFound 96 | 97 | ## v2.7.0 98 | Too many changes, forgot to docuement, sorry. 99 | - https://github.com/DoobDev/Doob/milestone/1?closed=1 100 | 101 | ## v2.6.8 102 | * Added `d!dogehouse` 103 | 104 | ## v2.6.7 105 | * Fixed `d!bio` command description. ([#160](https://github.com/doobdev/Doob/issues/160)) 106 | * Added `d!owroll` ([#163](https://github.com/doobdev/Doob/issues/163)) 107 | * Added `d!valroll` subcommands. ([#164](https://github.com/doobdev/Doob/issues/164)) 108 | - Documented at: https://docs.doobbot.com/#d-valroll-subcommands 109 | * Added priority labels to `d!issue -c` 110 | 111 | ## v2.6.6 112 | * Added `d!issue` 113 | * Added `d!issue` subcommands. 114 | - Documented at: https://docs.doobbot.com/#d-issue-subcommands 115 | - Owner Only 116 | 117 | ## v2.6.5 118 | * Added `d!owoify` 119 | 120 | ## v2.6.4 121 | * Added `d!role` 122 | * Added `d!role` subcommands. 123 | - Documented at: https://docs.doobbot.com/#d-role-subcommands 124 | 125 | ## v2.6.3 126 | * Added `d!warn` 127 | * Added `d!warnings` 128 | 129 | ## v2.6.2 130 | * Added `d!twitch` subcommands. 131 | - Documented at: https://docs.doobbot.com/#d-twitch-subcommands 132 | 133 | ## v2.6.1 134 | * Added a description for `d!fight` command. 135 | * Added `d!overlay` 136 | - From: https://github.com/DoobDev/Krinio/blob/master/lib/cogs/overlay.py 137 | 138 | ## v2.6.0 139 | * Added `d!battle` ([#132](https://github.com/doobdev/Doob/pull/132)) 140 | - Battle a friend or foe! 141 | 142 | ## v2.5.1 143 | * Added `d!valroll` 144 | - Rolls a random VALORANT character for you to play. 145 | 146 | ## v2.5.0 147 | * Changed Doob's default prefix to `d!`. 148 | - The only reason it was `doob/` to begin with was because Top.gg muted Doob for having too much of a unique prefix "back then". Now, Top.gg removed all bots from their server, so there is no reason to have a 5 character prefix. 149 | #### Note: Only servers that are new to Doob will be affected, old servers will keep their `doob/` prefix unless they change it themselves. (Custom prefixes have not been touched with this update.) 150 | 151 | ## v2.4.5 152 | * Added `d!ownerprefix` 153 | 154 | ## v2.4.4 155 | * Added basic error handling for not having permissions for a command. 156 | * Removed the error handling from Ban/Kick/Mute commands, because of the change above. 157 | 158 | ## v2.4.3 159 | * Added `d!createtextchannel` 160 | * Added `d!createvoicechannel` 161 | 162 | ## v2.4.2 163 | * Now running on Discord.py 1.6.0 164 | * Instead of the commands just sending, it replies to you. 165 | 166 | ## v2.4.1 167 | * Added command descriptions for music commands. 168 | 169 | ## v2.4.0 170 | ### Added music support to Doob! 171 | * Added `d!connect` 172 | - Connects to a Voice Channel 173 | * Added `d!disconnect` 174 | - Disconnects from a Voice Channel 175 | * Added `d!play` 176 | - Plays a song either linked from YouTube, or searched. 177 | * Added `d!pause` 178 | - Pauses the current song. 179 | * Added `d!resume` 180 | - Resumes the current song. 181 | - (Using `d!play` with nothing after it does the same thing.) 182 | * Added `d!stop` 183 | - Stops the music, and clears the queue. 184 | * Added `d!next` 185 | - Skips the current track. 186 | * Added `d!previous` 187 | - Goes to the previous track in the queue and plays it. 188 | * Added `d!shuffle` 189 | - Shuffles the queue. 190 | * Added `d!repeat` 191 | - Allows you to repeat using `none`, `1`, or `all`. 192 | - `none` = Stops repeating 193 | - `1` = 1 track repeating 194 | - `all` = Queue repeating. 195 | * Added `d!queue` 196 | - Lets you see the queue for that server. 197 | 198 | ## I worked super hard for this to come out, however I must provide a disclaimer that at this time (1/4/2021) this is in **OPEN BETA**, ~~meaning this *might* be on the public bot (meaning by the time this changelog is out, I might not have figured out how to install lavalink [the "music"/"voice" ""server""] on my VPS.)~~, but it still might be buggy and stuff like command descriptions aren't finished yet. 199 | (Music is available on the public bot.) 200 | 201 | ## v2.3.4 202 | * Added `Patreon Only` command `d!phone`. 203 | 204 | ## v2.3.3 205 | * Added some DM commands 206 | - DM the bot, "donate" and it gives you a donation link. 207 | - DM the bot, "help" and it gives you instructions on how to get help. 208 | - DM the bot anything else, and it tells you that most commands can't be DMd 209 | 210 | ## v2.3.2 211 | * Fixed `d!timebomb`'s lower time error. 212 | 213 | ## v2.3.1 214 | * Forgot to move to `users` table instead of `exp` table on 2 lines. 215 | - This fixes `d!fm --np` 216 | 217 | ## v2.2.1 218 | ### This should technically be v2.3.0, but I'm dumb lol 219 | ### (bigger update then usual, because the main bot was offline while I was working on this stuff, so I just wanted it to be 1 update instead of speread across a bunch of different updates) 220 | * Added rounding to: 221 | - osu! commands 222 | * Made `d!setosu` show your profile picture from osu! instead of from Discord. 223 | * (if all went well) Added `d!remind` 224 | - Lets you set a reminder! 225 | 226 | ## v2.2.0 227 | * `d!timebomb` now has a timelimit of 1000 seconds. 228 | 229 | ## v2.1.6 230 | * Added a **HUGE** cooldown to `d!russianroulette` 231 | - 1 command per 30 seconds in a guild. 232 | 233 | ## v2.1.5 234 | * Added `d!fm --np` 235 | * Added `d!russianroulette` 236 | - Put people in a russian roulette to be banned. 237 | 238 | ## v2.1.4 239 | * Added `d!startgiveaway` 240 | - Starts a giveaway! 241 | * Added `d!stopgiveaway` 242 | - Stops a giveaway! 243 | * Removed cooldown on `d!echo` 244 | * Made the message delete itself on `d!echo` 245 | - Ex: `d!echo Hello` <== That message would be deleted. 246 | 247 | ## v2.1.3 248 | * Made `d!fm artist search` only when the response status is 200 249 | * Added `d!vote` 250 | 251 | ## v2.1.2 252 | * `d!osu` checks if you have no username saved 253 | - If you don't, it tells you how to add your username. 254 | * `d!osuset` is now an alias for `d!setosu` 255 | * Added `d!fm artist search`! 256 | - Search for artists and get their 257 | 1. Listeners 258 | 2. Play Count 259 | 3. Wiki Link 260 | 4. Similar Artists 261 | 5. Top Tracks 262 | 6. Top Albums 263 | 264 | ## v2.1.1 265 | * Added `d!osu` 266 | - See your osu! profile! 267 | * Added `d!setosu` 268 | - Set your osu! profile for `d!osu`! 269 | 270 | ## v2.1.0 271 | * Per-Server XP Leaderboards (`d!serverleaderboard`) 272 | * Removed `d!slap` 273 | - There has been an exploit discovered to ping everyone, decided to go ahead and remove the command, wasn't used at all. 274 | * New `d!timebomb` command 275 | - Lets you have a message with a time limit! 276 | * Doob Logging now sends in regular messages instead of embeds. 277 | - I saw [stageosu/Kaguya](https://github.com/stageosu/kaguya) do this recently, and I love the look of it, go check out Stage's bot, its great :D 278 | * Added `d!streamlookup` command! 279 | - Lookup your favorite streamers, see if they are online or not, and if they are online, see information like what game they are playing! 280 | * Added `d!owstats` command! 281 | - Check your (or your friends) Overwatch stats in Discord! 282 | * Added `d!setowusername` command! 283 | - Add your platform, username, and region to be able to look up your stats on Doob! 284 | - Only acceptable platforms are `pc` `xbl` and `psn` 285 | - Only acceptable regions are `us` `eu` or `asia` 286 | - For battletags, make sure you do `{username}-{numbers}` NOT `{username}#{numbers}` 287 | * Changed around some of the fields on `d!streamlookup` 288 | * Added `d!bio` command! 289 | - Lookup your/your friend's Discord.bio profile in Discord! 290 | * Added `d!lastfm` command! 291 | - Lookup your/your friend's Last.fm profiles directly in Discord! 292 | * Added `d!setlastfm` command! 293 | - Set your default last.fm username, so you don't have to type your username every time you want to use a Last.fm command. 294 | * Added some Doob emojis to `d!info` 295 | * `d!lastfm` only does your profile now. 296 | - To do other people's do `d!lastfm search {username}` 297 | * Added `d!lastfm recent`! 298 | - See the last 5 songs you or someone else has listened to! 299 | * Added `d!lastfm top albums`! 300 | - Get your top 10 most listened to albums on Last.fm 301 | * Added `d!lastfm artist charts`! 302 | - See the top 10 artists on Last.fm! 303 | * Added `d!lastfm top tracks`! 304 | - See the top 10 tracks you have played on last.fm! 305 | * Added `d!lastfm top artists`! 306 | - See the top 10 artists you have played on last.fm! 307 | 308 | ## v2.0.9 309 | #### The muting update! 310 | * Finally! Doob has muting! 311 | * New `d!mute` command! 312 | - Mutes a member from your server! 313 | - Example: `d!mute @X Daniel lol` 314 | - Make sure to set your "Muted Role" by doing `d!setmutedrole` 315 | * New `d!unmute` command! 316 | - Unmutes a member from your server! 317 | - Example: `d!unmute @X Daniel` 318 | * Per-Server XP is now avaliable! 319 | * Updated `d!level` to include Server XP 320 | * Updated `d!profile` to say "Doob Global XP/Level/Rank". 321 | * Made unmute/mute commands have embeds. 322 | * Fixed Level Messages not working 323 | * Fixed `d!slc` and `d!slm` so it no longer shows "None" 324 | * Instead of help messages and such saying `@Doob ` for the prefix, it now says your server's prefix. 325 | * Removed the description for the Among Us Commands. 326 | * You can still find them in `d!help {cmd}` 327 | * Added a new Patreon only command! `d!luckydog` 328 | * Gives you instant access to all the lucky dog's avaliable. 329 | * Lucky Dog Nitro Giveaways are BACK! 330 | - Every month, whoever gets the most Lucky Dogs, gets 1 month of Nitro Classic! 331 | * New `d!luckydogs` command! 332 | - Check how many lucky dogs you have "caught" over the month. 333 | * Updated `d!userinfo` "Bot or not" field. 334 | - "Bot or not." ==> "Bot" 335 | * New `d!unban` command! 336 | * Starboards! 337 | - New `d!setstarboardchannel` command! 338 | * Updated starboard embed to have a "Jump to link" 339 | - Lets you jump to the message that was starred. 340 | * Really bad `d!jumpscare` command lol. 341 | * Removed jumpscare 342 | * Lucky Dogs now occur 1/500 343 | * Updated `d!info` to have system stats. 344 | 345 | ## v2.0.8 346 | * Updated description (on the help command) for 347 | - `d!Leaderboard` 348 | * New `d!patreon` command! 349 | * Added some debugging for d!dog 350 | - It gives me the roll numbers for P_AD and L_DOG if I need to check if it is working or not. 351 | * Default Level Messages are off. 352 | - This feature is on the live bot now. 353 | * Default error message gives support server. 354 | * `d!setlevelmessages` now: 355 | - Tells you if you have level messages on or off if you just type `d!setlevelmessages` 356 | - Only sets your level message setting if you type 357 | - `yes` 358 | - `Yes` 359 | - `no` 360 | - `No` 361 | * `d!slap` should no longer gives you a Bad Argument error. 362 | - Instead, it defaults to slapping yourself, silly you! 363 | - If you try to pass a reason in when doing `d!slap` with no member found, it says that you accidently slapped yourself instead of the person you meant to slap. 364 | - This also means it removes the default reason. 365 | - For instance, instead of it saying "for no reason!" it will just say "Reason - None!" 366 | * `d!serverinfo` no longer requires server permissions! 367 | - The reason it needed server permissions last time is because of the "Banned Members" field, if Doob has Administrator permissions in a server, it will still show the "Banned Members" field. 368 | * `d!prefix` and `d!setlevelmessages` can no longer be abused to do @everyone pings. 369 | * `d!setmutedrole` and `d!setlogchannel` no longer have to be IDs anymore. 370 | - I believe they now **are required** to be pings. 371 | - Example: `d!setmutedrole @Muted` and `d!setlogchannel #logs` 372 | - Instead of from 2.0.7 (this is outdated, don't do this anymore.) `d!setmutedrole {role id}` and `d!setlogchannel {channel id}` 373 | * Logs have been removed from `d!kick` and `d!ban`. 374 | - This will most likely be changed next patch, but for now they have been removed. 375 | - Why?: If you didn't have a log channel set, it would error out, but still kick / ban the person. 376 | * `d!ai` and `d!nai` doesn't ping everyone anymore 377 | * `d!dog` now has a lower chance of getting a Lucky Dog 378 | - Instead of 1/100 it is now 1/1000 379 | - The reason I did this is because people were getting them too often, and for the nitro giveaway in the support server, at least like 2/3 people a day got a lucky dog every single day since the bot hit about 70+ servers, so I knew it would be unfair to keep it 1/100 so I changed it, if this change has any problem, we might bump it back up to 1/500. Hopefully you understand the change. 380 | * `d!kick` and `d!ban` now check for role hierarchys 381 | - For example, a Helper can't kick a Moderator. 382 | * No more random "This Command didn't work" error. 383 | * Changed the message for the "Forbidden" error. 384 | * Removed the "HTTPException" error handling, for now. 385 | * `d!slap` no longer sends 2 messages. 386 | * `d!prefix` is now in an embed 387 | * Added [@KittyKay000](https://twitter.com/kittykay000)'s concept drawing of Doob for a Lucky Dog! 388 | * Made the cooldown for `d!dog` longer. 389 | - Someone suggested in the [Doob Support Discord](https://discord.gg/hgQTTU7) to make a cooldown on the command, little did they know, it had a command, appearently it wasn't long enough so I have increased it. 390 | - You can only use it 2 times every 5 seconds. 391 | * Added owner only `d!nitrogiveaway` command to tell users on how Doob's Nitro Classic giveaways work. 392 | * New dog API! 393 | - Now there are so many more regular dogs! 394 | * Statcord integration! 395 | * Added polls (hi mr. jones) 396 | * Added actual command descriptions for when you do `d!help {cmd}` 397 | - Example: `d!help ban` shows a description + the usage. 398 | * Decreased the command cooldown for `d!help` 399 | * Made it so it only clears reactions on the `d!help` timeout, and not delete the message. 400 | * Made a new owner only `d!update` command for giving brief updates about the bot. 401 | * Updated some help commands so that the permissions that are required are on a new line, instead of the same line, makes it look nicer imo. 402 | 403 | ## v2.0.7 404 | * Added d!support - Gives link to the Doob support server. 405 | * Added d!invite - Gives link to invite Doob to another server. 406 | * 1 in 50 chance of a Lucky Dog for [Patrons](https://patreon.com/doobdev) instead of 1 in 100 407 | - With this change, I added an ad on the top of the embed that only appear sometiems. 408 | * Added a [Patreon](https://patreon.com/doobdev) field to d!info 409 | - If you are a patron it thanks you! :) 410 | * Echo is now a [Patreon](https://patreon.com/doobdev) only command! 411 | - "BUT WHY, BUT WHY" 412 | 1. Moderation 413 | 2. I don't want random people to say random garbage on my bot 414 | 3. Rack in those patrons EZ 415 | * Patron status is now displayed in d!profile 416 | 417 | ## v2.0.6 418 | * New Lucky Dog! (Kevin from [@Weesterner](https://twitter.com/weesterner)!) 419 | * Changed Description on the Lucky Dog embeds. 420 | - Added Twitter links to the description. 421 | * Decreased the odds so they are actually 1 in 100 422 | - With me adding more Lucky Dogs, it has actually increased the chance for you to get one, so now it is actually a 1 in 100 chance to get a lucky dog! 423 | * Removed message deletes for errors. 424 | * Moved old v1 folder to a [new repository](https://github.com/doobdev/old-doob). 425 | * Added some fun Among Us commands! 426 | - d!notanimposter {user} - gives an ascii of an among us crewmate floating through space with text saying "{user} was not an imposter" 427 | - d!animposter {user} - gives the same ascii except it says "{user} was an imposter" 428 | * XP Leaderboard is BACK! (fixed by [@X Daniel](https://github.com/x-daniel-17)) 429 | 430 | ## v2.0.5 431 | * Added d!levelmessages. 432 | - Made a way to disable level messages in your server, Do `d!levelmessages no` if you want to disable them, `d!levelmessages yes` to re-enable them. 433 | 434 | ## v2.0.4 435 | * Deleted Message logs have been added. 436 | * Made log embeds look nicer. 437 | * Updated some of the lingo used on the Lucky Dogs descriptions. 438 | * Changelog link on d!info now automatically updates with the latest GitHub changelog "jump to" link. 439 | * Added channels to message update / message delete logs 440 | * Updated the d!level message to say that you are ranked based on the amount of users globally. 441 | * d!profile now shows XP and Rank 442 | * Disabled on_user_update logging events for now, as I don't know how to grab a guild id 443 | 444 | ## v2.0.3 445 | * os.sep in `__init__.py` for cogs 446 | * adds server to DB when bot is added to said server. 447 | * multiple bug fixes 448 | 449 | ## v2.0.2 450 | * Added another lucky dog! (mmatt's GAMING server icon!) 451 | 452 | ## v2.0.1: 453 | * Added a Lucky dog! (Koda from [@Mendo](https://twitter.com/mendo)!) 454 | 455 | 456 | ------------------------------ 457 | 458 | 459 | ## Old Doob V1 changelog below. 460 | ## v1.0.1: 461 | * Patreon added 462 | - You can now purchase a subscription to help support Doob! Head on over to [patreon.com/doobdev](https://patreon.com/doobdev) and get your subscription! 463 | - Tier 1 gives you: 464 | 1. Patron-only posts and messages 465 | 2. Priority Support 466 | 3. General Support 467 | 4. Exclusive Doob commands to use! 468 | - Tier 2 gives you: 469 | 1. Everything from Tier 1 470 | 2. Hosting the Among Us Bot in your server! 471 | - Tier 3 gives you: 472 | 1. Everything from Tiers 1/2 473 | 2. Doob Beta Bot in your server! 474 | -------------------------------------------------------------------------------- /lib/cogs/lastfm.py: -------------------------------------------------------------------------------- 1 | from discord.ext.commands import Cog, command, BucketType, cooldown, group 2 | from discord import Embed, Colour 3 | 4 | from ..db import db # pylint: disable=relative-beyond-top-level 5 | 6 | from aiohttp import request 7 | 8 | from datetime import datetime 9 | 10 | from typing import Optional 11 | 12 | import os 13 | from dotenv import load_dotenv 14 | 15 | load_dotenv() 16 | 17 | 18 | class LastFM(Cog): 19 | def __init__(self, bot): 20 | self.bot = bot 21 | 22 | async def fm_search(self, ctx, username: Optional[str]): 23 | username = ( 24 | username 25 | or db.record( 26 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 27 | )[0] 28 | ) 29 | prefix = db.record("SELECT Prefix from guilds WHERE GuildID = ?", ctx.guild.id) 30 | 31 | if ( 32 | db.record( 33 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 34 | )[0] 35 | == None 36 | ): 37 | await ctx.reply( 38 | f"Your Last.fm username is set to None\nSet it to your username by doing `{prefix[0]}setlastfm`" 39 | ) 40 | return 41 | 42 | User_URL = f"https://ws.audioscrobbler.com/2.0/?method=user.getinfo&user={username}&api_key={os.environ.get('lastfmkey')}&format=json" 43 | top_tracks_url = f"https://ws.audioscrobbler.com/2.0/?method=user.gettoptags&user={username}&api_key={os.environ.get('lastfmkey')}&limit=5&format=json" 44 | loved_tracks_url = f"https://ws.audioscrobbler.com/2.0/?method=user.getlovedtracks&user={username}&api_key={os.environ.get('lastfmkey')}&format=json" 45 | recent_tracks_url = f"https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user={username}&api_key={os.environ.get('lastfmkey')}&format=json&limit=1" 46 | 47 | async with request("GET", User_URL) as response: 48 | if response.status == 200: 49 | data = (await response.json())["user"] 50 | 51 | embed = Embed( 52 | title=f"{data['name']}'s Last.fm profile", 53 | description=data["url"], 54 | colour=ctx.author.colour, 55 | ) 56 | 57 | ts = int(data["registered"]["unixtime"]) 58 | 59 | fields = [ 60 | ("Play Count:", f"{data['playcount']}", True), 61 | ("Country:", data["country"], True), 62 | ( 63 | "Registered since:", 64 | datetime.utcfromtimestamp(ts).strftime("%m-%d-%Y | %H:%M:%S"), 65 | True, 66 | ), 67 | ] 68 | 69 | for name, value, inline in fields: 70 | embed.add_field(name=name, value=value, inline=inline) 71 | 72 | async with request("GET", loved_tracks_url) as loved: 73 | loved_data = (await loved.json())["lovedtracks"]["@attr"] 74 | 75 | embed.add_field( 76 | name="Loved Tracks:", value=loved_data["total"], inline=True 77 | ) 78 | 79 | async with request("GET", top_tracks_url) as tags: 80 | tags_data = (await tags.json())["toptags"] 81 | 82 | tags_list = list() 83 | 84 | for i in tags_data["tag"]: 85 | tags_list.append(i["name"]) 86 | 87 | if tags_list: 88 | embed.add_field(name="Top Tags:", value=", ".join(tags_list)) 89 | 90 | async with request("GET", recent_tracks_url) as recent: 91 | recent_data = (await recent.json())["recenttracks"] 92 | 93 | embed.add_field( 94 | name="Most Recent Track:", 95 | value=f"{recent_data['track'][0]['artist']['#text']} - {recent_data['track'][0]['name']}", 96 | inline=False, 97 | ) 98 | 99 | if data["type"] == "subscriber": 100 | embed.add_field( 101 | name="Last.fm Pro Status", value="Subscribed", inline=False 102 | ) 103 | 104 | if data["image"][3]["#text"] != "": 105 | embed.set_thumbnail(url=data["image"][3]["#text"]) 106 | 107 | await ctx.reply(embed=embed) 108 | 109 | else: 110 | await ctx.reply(f"The Last.fm API returned a {response.status} status.") 111 | 112 | @group(name="lastfm", aliases=["fm"], brief="Get your Last.fm information.") 113 | # @cooldown(1, 5, BucketType.user) 114 | async def lastfm(self, ctx): 115 | """Request some information on a specific Last.fm User!\n`Username` = Last.fm Username""" 116 | if ctx.invoked_subcommand is None: 117 | await self.fm_search( 118 | ctx, 119 | username=db.record( 120 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 121 | )[0], 122 | ) 123 | 124 | @lastfm.command(name="--np", aliases=["-np", "np"]) 125 | async def now_playing_command(self, ctx, username: Optional[str]): 126 | username = ( 127 | username 128 | or db.record( 129 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 130 | )[0] 131 | ) 132 | prefix = db.record("SELECT Prefix from guilds WHERE GuildID = ?", ctx.guild.id) 133 | 134 | if ( 135 | db.record( 136 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 137 | )[0] 138 | == None 139 | ): 140 | await ctx.reply( 141 | f"Your Last.fm username is set to None\nSet it to your username by doing `{prefix[0]}setlastfm`" 142 | ) 143 | return 144 | 145 | now_playing_url = f"https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user={username}&api_key={os.environ.get('lastfmkey')}&format=json&limit=1" 146 | 147 | async with request("GET", now_playing_url) as response: 148 | data = (await response.json())["recenttracks"] 149 | 150 | llist = list() 151 | 152 | for i in data["track"]: 153 | llist.append(f"‣ {i['name']} - {i['artist']['#text']}") 154 | 155 | if llist: 156 | embed = Embed( 157 | title=f"{data['@attr']['user']}'s most recent track", 158 | description="\n".join(llist), 159 | colour=ctx.author.colour, 160 | ) 161 | 162 | await ctx.reply(embed=embed) 163 | 164 | else: 165 | embed = Embed( 166 | title=f"⚠ - No recent tracks found for {data['@attr']['user']}.", 167 | colour=ctx.author.colour, 168 | ) 169 | 170 | message = await ctx.reply(embed=embed) 171 | 172 | await message.add_reaction("⚠️") 173 | 174 | @lastfm.command( 175 | name="recent", 176 | aliases=["recenttracks", "recentracks"], 177 | brief="Gives you the 5 most recent tracks from a user.", 178 | ) 179 | async def recent_tracks_command(self, ctx, username: Optional[str]): 180 | username = ( 181 | username 182 | or db.record( 183 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 184 | )[0] 185 | ) 186 | prefix = db.record("SELECT Prefix from guilds WHERE GuildID = ?", ctx.guild.id) 187 | 188 | if ( 189 | db.record( 190 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 191 | )[0] 192 | == None 193 | ): 194 | await ctx.reply( 195 | f"Your Last.fm username is set to None\nSet it to your username by doing `{prefix[0]}setlastfm`" 196 | ) 197 | return 198 | 199 | recent_tracks_url = f"https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user={username}&api_key={os.environ.get('lastfmkey')}&format=json&limit=5" 200 | 201 | async with request("GET", recent_tracks_url) as response: 202 | data = (await response.json())["recenttracks"] 203 | 204 | llist = list() 205 | 206 | for i in data["track"]: 207 | llist.append(f"‣ {i['name']} - {i['artist']['#text']}") 208 | 209 | if llist: 210 | embed = Embed( 211 | title=f"{data['@attr']['user']}'s 5 recent tracks", 212 | description="\n".join(llist), 213 | colour=ctx.author.colour, 214 | ) 215 | 216 | await ctx.reply(embed=embed) 217 | 218 | else: 219 | embed = Embed( 220 | title=f"⚠ - No recent tracks found for {data['@attr']['user']}.", 221 | colour=ctx.author.colour, 222 | ) 223 | 224 | message = await ctx.reply(embed=embed) 225 | 226 | await message.add_reaction("⚠️") 227 | 228 | @lastfm.group(name="top") 229 | async def top_group(self, ctx): 230 | subcommand = ctx.invoked_subcommand 231 | if ( 232 | subcommand != self.top_albums_command 233 | and subcommand != self.top_tracks_command 234 | and subcommand != self.top_artist_command 235 | ): 236 | prefix = db.record( 237 | "SELECT Prefix from guilds WHERE GuildID = ?", ctx.guild.id 238 | ) 239 | await ctx.reply( 240 | f"Try these commands instead.\n`{prefix[0]}fm top albums`\n`{prefix[0]}fm top tracks`\n`{prefix[0]}fm top artists`" 241 | ) 242 | 243 | @top_group.command(name="albums") 244 | async def top_albums_command(self, ctx, username: Optional[str]): 245 | username = ( 246 | username 247 | or db.record( 248 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 249 | )[0] 250 | ) 251 | 252 | top_albums_url = f"https://ws.audioscrobbler.com/2.0/?method=user.gettopalbums&user={username}&api_key={os.environ.get('lastfmkey')}&format=json&limit=10" 253 | User_URL = f"https://ws.audioscrobbler.com/2.0/?method=user.getinfo&user={username}&api_key={os.environ.get('lastfmkey')}&format=json" 254 | 255 | async with request("GET", top_albums_url) as response: 256 | data = (await response.json())["topalbums"] 257 | 258 | llist = list() 259 | 260 | for i in data["album"]: 261 | llist.append( 262 | f"‣ #{i['@attr']['rank']} - {i['artist']['name']} - {i['name']} ({i['playcount']} plays)" 263 | ) 264 | 265 | if llist: 266 | embed = Embed( 267 | title=f"{data['@attr']['user']}'s 10 top albums", 268 | description="\n".join(llist), 269 | colour=ctx.author.colour, 270 | ) 271 | 272 | embed.set_thumbnail(url=data["album"][0]["image"][3]["#text"]) 273 | 274 | async with request("GET", User_URL) as icon: 275 | icon_url = (await icon.json())["user"] 276 | 277 | if icon_url["image"][3]["#text"] != "": 278 | embed.set_footer( 279 | text=f"last.fm/users/{data['@attr']['user']}", 280 | icon_url=icon_url["image"][3]["#text"], 281 | ) 282 | 283 | await ctx.reply(embed=embed) 284 | 285 | else: 286 | embed = Embed( 287 | title=f"⚠ - No top albums found for {data['@attr']['user']}.", 288 | colour=ctx.author.colour, 289 | ) 290 | 291 | message = await ctx.reply(embed=embed) 292 | 293 | await message.add_reaction("⚠️") 294 | 295 | @top_group.command(name="tracks") 296 | async def top_tracks_command(self, ctx, username: Optional[str]): 297 | username = ( 298 | username 299 | or db.record( 300 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 301 | )[0] 302 | ) 303 | 304 | top_tracks_url = f"https://ws.audioscrobbler.com/2.0/?method=user.gettoptracks&user={username}&api_key={os.environ.get('lastfmkey')}&format=json&limit=10" 305 | User_URL = f"https://ws.audioscrobbler.com/2.0/?method=user.getinfo&user={username}&api_key={os.environ.get('lastfmkey')}&format=json" 306 | 307 | async with request("GET", top_tracks_url) as response: 308 | data = (await response.json())["toptracks"] 309 | 310 | llist = list() 311 | 312 | for i in data["track"]: 313 | llist.append( 314 | f"‣ #{i['@attr']['rank']} - {i['artist']['name']} - {i['name']} ({i['playcount']} plays)" 315 | ) 316 | 317 | if llist: 318 | embed = Embed( 319 | title=f"{data['@attr']['user']}'s 10 top tracks", 320 | description="\n".join(llist), 321 | colour=ctx.author.colour, 322 | ) 323 | 324 | embed.set_thumbnail(url=data["track"][0]["image"][3]["#text"]) 325 | 326 | async with request("GET", User_URL) as icon: 327 | icon_url = (await icon.json())["user"] 328 | 329 | if icon_url["image"][3]["#text"] != "": 330 | embed.set_footer( 331 | text=f"last.fm/users/{data['@attr']['user']}", 332 | icon_url=icon_url["image"][3]["#text"], 333 | ) 334 | 335 | await ctx.reply(embed=embed) 336 | 337 | else: 338 | embed = Embed( 339 | title=f"⚠ - No top tracks found for {data['@attr']['user']}.", 340 | colour=ctx.author.colour, 341 | ) 342 | 343 | message = await ctx.reply(embed=embed) 344 | 345 | await message.add_reaction("⚠️") 346 | 347 | @top_group.command(name="artists", aliases=["artist"]) 348 | async def top_artist_command(self, ctx, username: Optional[str]): 349 | username = ( 350 | username 351 | or db.record( 352 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 353 | )[0] 354 | ) 355 | 356 | top_artists_url = f"https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user={username}&api_key={os.environ.get('lastfmkey')}&format=json&limit=10" 357 | User_URL = f"https://ws.audioscrobbler.com/2.0/?method=user.getinfo&user={username}&api_key={os.environ.get('lastfmkey')}&format=json" 358 | 359 | async with request("GET", top_artists_url) as response: 360 | data = (await response.json())["topartists"] 361 | 362 | llist = list() 363 | 364 | for i in data["artist"]: 365 | llist.append( 366 | f"‣ #{i['@attr']['rank']} - {i['name']} - ({i['playcount']} plays)" 367 | ) 368 | 369 | if llist: 370 | embed = Embed( 371 | title=f"{data['@attr']['user']}'s 10 top artists", 372 | description="\n".join(llist), 373 | colour=ctx.author.colour, 374 | ) 375 | 376 | embed.set_thumbnail(url=data["artist"][0]["image"][3]["#text"]) 377 | 378 | async with request("GET", User_URL) as icon: 379 | icon_url = (await icon.json())["user"] 380 | 381 | if icon_url["image"][3]["#text"] != "": 382 | embed.set_footer( 383 | text=f"last.fm/users/{data['@attr']['user']}", 384 | icon_url=icon_url["image"][3]["#text"], 385 | ) 386 | 387 | await ctx.reply(embed=embed) 388 | 389 | else: 390 | embed = Embed( 391 | title=f"⚠ - No top artists found for {data['@attr']['user']}.", 392 | colour=ctx.author.colour, 393 | ) 394 | 395 | message = await ctx.reply(embed=embed) 396 | 397 | await message.add_reaction("⚠️") 398 | 399 | @lastfm.command(name="search", brief="Search a last.fm account.") 400 | async def search_command(self, ctx, username: Optional[str]): 401 | username = ( 402 | username 403 | or db.record( 404 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 405 | )[0] 406 | ) 407 | 408 | await self.fm_search(ctx, username) 409 | 410 | @lastfm.group(name="artist") 411 | async def artist_group(self, ctx): 412 | subcommand = ctx.invoked_subcommand 413 | if ( 414 | subcommand != self.artist_charts_command 415 | and subcommand != self.artist_search_command 416 | ): 417 | prefix = db.record( 418 | "SELECT Prefix from guilds WHERE GuildID = ?", ctx.guild.id 419 | ) 420 | await ctx.reply( 421 | f"Try these commands instead.\n`{prefix[0]}fm artist charts`\n`{prefix[0]}fm artist search" 422 | ) 423 | 424 | @artist_group.command(name="charts") 425 | async def artist_charts_command(self, ctx): 426 | charts_url = f"https://ws.audioscrobbler.com/2.0/?method=chart.gettopartists&api_key={os.environ.get('lastfmkey')}&format=json&limit=10" 427 | 428 | async with request("GET", charts_url) as response: 429 | data = (await response.json())["artists"] 430 | 431 | llist = list() 432 | 433 | for i in data["artist"]: 434 | llist.append(f"‣ {i['name']}") 435 | 436 | if llist: 437 | embed = Embed( 438 | title="Top 10 artists on Last.fm", 439 | description="\n".join(llist), 440 | colour=ctx.author.colour, 441 | ) 442 | 443 | await ctx.reply(embed=embed) 444 | 445 | @artist_group.command(name="search") 446 | async def artist_search_command(self, ctx, *, artist: str): 447 | info_url = f"https://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artist}&api_key={os.environ.get('lastfmkey')}&format=json" 448 | toptracks = f"https://ws.audioscrobbler.com/2.0/?method=artist.gettoptracks&artist={artist}&api_key={os.environ.get('lastfmkey')}&format=json&limit=5" 449 | topalbums = f"https://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist={artist}&api_key={os.environ.get('lastfmkey')}&format=json&limit=5" 450 | # spotify_search = f"https://api.spotify.com/v1/search&type=artist&q={artist}&limit=1" 451 | 452 | async with request("GET", info_url) as response: 453 | if response.status == 200: 454 | data = (await response.json())["artist"] 455 | 456 | embed = Embed( 457 | title=f"{data['name']} info on Last.fm", 458 | description=data["url"], 459 | colour=ctx.author.colour, 460 | ) 461 | 462 | fields = [ 463 | ("Listeners:", data["stats"]["listeners"], True), 464 | ("Play Count:", data["stats"]["playcount"], True), 465 | ("Wiki:", data["bio"]["links"]["link"]["href"], False), 466 | ] 467 | 468 | for name, value, inline in fields: 469 | embed.add_field(name=name, value=value, inline=inline) 470 | 471 | similar = list() 472 | 473 | for i in data["similar"]["artist"]: 474 | similar.append(i["name"]) 475 | 476 | if similar: 477 | embed.add_field(name="Similar Artists:", value=", ".join(similar)) 478 | 479 | async with request("GET", toptracks) as response: 480 | if response.status == 200: 481 | toptracks = (await response.json())["toptracks"] 482 | 483 | tracks = list() 484 | 485 | for i in toptracks["track"]: 486 | tracks.append( 487 | f"‣ #{i['@attr']['rank']} {i['name']}: Play Count ‣ {i['playcount']}" 488 | ) 489 | 490 | if tracks: 491 | embed.add_field( 492 | name="Top Tracks:", 493 | value="\n".join(tracks), 494 | inline=False, 495 | ) 496 | 497 | async with request("GET", topalbums) as response: 498 | if response.status == 200: 499 | topalbums = (await response.json())["topalbums"] 500 | 501 | albums = list() 502 | 503 | for i in topalbums["album"]: 504 | albums.append( 505 | f"‣ {i['name']}: Play Count ‣ {i['playcount']}" 506 | ) 507 | 508 | if albums: 509 | embed.add_field( 510 | name="Top Albums:", 511 | value="\n".join(albums), 512 | inline=False, 513 | ) 514 | 515 | # async with request("GET", spotify_search, headers={"Authorization Bearer:": os.environ.get('spotify_id'), 'Content_Type': 'application/json'}) as response: 516 | # if response.status == 200: 517 | # data = (await response.json(content_type=None))['artists']['items'][0] 518 | 519 | # embed.set_thumbnail(url=data['images'][0]['url']) 520 | 521 | # else: 522 | # await ctx.reply(f"spotify sucks {response.status} error") 523 | 524 | await ctx.reply(embed=embed) 525 | 526 | @command(name="setlastfm", aliases=["setfm"], brief="Sets your Last.fm username.") 527 | @cooldown(1, 5, BucketType.user) 528 | async def set_lastfm_username(self, ctx, username: Optional[str]): 529 | """Sets your Last.fm username for `d!lastfm`""" 530 | 531 | if username != None: 532 | embed = Embed( 533 | title="Setting Last.fm username:", 534 | description=username, 535 | colour=ctx.author.colour, 536 | ) 537 | 538 | embed.set_thumbnail(url=ctx.author.avatar_url) 539 | 540 | db.execute( 541 | "UPDATE users SET LastfmUsername = ? WHERE UserID = ?", 542 | username, 543 | ctx.author.id, 544 | ) 545 | db.commit() 546 | 547 | await ctx.reply(embed=embed) 548 | 549 | else: 550 | username = db.record( 551 | "SELECT LastfmUsername FROM users WHERE UserID = ?", ctx.author.id 552 | ) 553 | embed = Embed( 554 | title="Your Last.fm username", 555 | description=username[0], 556 | colour=ctx.author.colour, 557 | ) 558 | 559 | embed.set_thumbnail(url=ctx.author.avatar_url) 560 | 561 | await ctx.reply(embed=embed) 562 | 563 | @Cog.listener() 564 | async def on_ready(self): 565 | if not self.bot.ready: 566 | self.bot.cogs_ready.ready_up("lastfm") 567 | 568 | 569 | def setup(bot): 570 | bot.add_cog(LastFM(bot)) 571 | --------------------------------------------------------------------------------