├── .deepsource.toml ├── .gitignore ├── DiscordDatabase ├── DiscordDatabase.py ├── __init__.py ├── common_functions.py └── database.py ├── LICENSE ├── README.md ├── images └── guild_id.gif ├── setup.py └── upload /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "secrets" 5 | enabled = true 6 | 7 | [[analyzers]] 8 | name = "python" 9 | enabled = true 10 | 11 | [analyzers.meta] 12 | runtime_version = "3.x.x" 13 | 14 | [[transformers]] 15 | name = "black" 16 | enabled = true 17 | 18 | [[transformers]] 19 | name = "isort" 20 | enabled = true 21 | 22 | [[transformers]] 23 | name = "autopep8" 24 | enabled = true 25 | 26 | [[transformers]] 27 | name = "yapf" 28 | enabled = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .venv 3 | 4 | venv/ 5 | .vscode/ 6 | __pycache__/ 7 | 8 | build/ 9 | dist/ 10 | DiscordDatabase.egg-info/ 11 | 12 | check.py -------------------------------------------------------------------------------- /DiscordDatabase/DiscordDatabase.py: -------------------------------------------------------------------------------- 1 | from DiscordDatabase.common_functions import format_string 2 | from DiscordDatabase.database import Database 3 | 4 | 5 | class DiscordDatabase: 6 | # This class takes care of creating categories and channels for the database 7 | def __init__(self, client, guild_id) -> None: 8 | self.guild_id = guild_id 9 | self.client = client 10 | 11 | async def __create(self, category_name: str, channel_name: str): 12 | category_name = format_string(category_name) # No spaces allowed 13 | channel_name = format_string(channel_name) # No spaces allowed 14 | 15 | if len(category_name) <= 0: 16 | raise ValueError("category_name should atleast have a length of 1") 17 | if len(channel_name) <= 0: 18 | raise ValueError("channel_name should atleast have a length of 1") 19 | 20 | ##### CATEGORY ##### 21 | category = list( 22 | filter(lambda c: c.name.casefold() == category_name, 23 | self.__GUILD.categories)) 24 | # Returns a list of categories which have same name as 'category_name' 25 | # Should return a list containg only one category with a unique name 26 | # Empty list means category does not exists 27 | 28 | if category == []: 29 | # Create category if doesnot exist 30 | category = await self.__GUILD.create_category(category_name) 31 | else: 32 | # Get category object if exists 33 | category = category[0] 34 | 35 | ##### CHANNEL ##### 36 | channel = list( 37 | filter(lambda c: c.name == channel_name, category.channels)) 38 | # Returns a list of channels under the category which have same name as 'channel_name' 39 | # Should return a list containg only one channel with a unique name 40 | # Empty list means channel doesnot exists 41 | 42 | if channel == []: 43 | # Create a new channel under category 44 | channel = await category.create_text_channel(channel_name) 45 | else: 46 | # Get existing channel 47 | channel = channel[0] 48 | 49 | return category, channel 50 | 51 | async def new(self, category_name, channel_name): 52 | await self.client.wait_until_ready() 53 | self.__GUILD = self.client.get_guild(self.guild_id) 54 | category, channel = await self.__create(category_name, channel_name) 55 | # This is the actual class that takes care of setting and getting data from the database 56 | return Database(category, channel) 57 | -------------------------------------------------------------------------------- /DiscordDatabase/__init__.py: -------------------------------------------------------------------------------- 1 | from .DiscordDatabase import DiscordDatabase 2 | -------------------------------------------------------------------------------- /DiscordDatabase/common_functions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from json.decoder import JSONDecodeError 4 | 5 | log = logging.getLogger(__name__) 6 | log.setLevel(logging.INFO) 7 | 8 | ch = logging.StreamHandler() 9 | ch.setLevel(logging.DEBUG) 10 | formatter = logging.Formatter('%(asctime)s - %(message)s') 11 | ch.setFormatter(formatter) 12 | log.addHandler(ch) 13 | 14 | def str_is_illegal(s: str): 15 | # returns True if string contains any illegal characters 16 | legal_chars = ("_", "") 17 | return not all(map(lambda c: c in legal_chars or c.isalnum(), s)) 18 | 19 | 20 | def format_string(s: str): 21 | # replaces spaces and hyphens from string 22 | return s.casefold().replace("-", "").replace(" ", "") 23 | 24 | 25 | def key_check(key: str): 26 | if len(key) <= 0: 27 | raise ValueError("key should atleast have a length of 1") 28 | 29 | if str_is_illegal(key): 30 | # 'key' should contain only alphabets and numbers 31 | raise ValueError("Key should contain only alphanumeric character") 32 | 33 | return True 34 | 35 | async def search_key(key: str, channel): 36 | found_key, in_message = None, None 37 | async for message in channel.history(limit=None): 38 | cnt = message.content 39 | try: 40 | data = json.loads(str(cnt)) 41 | except JSONDecodeError: 42 | logging.info(f"-----\nJSONDecodeerror: {cnt}\n-----") 43 | continue 44 | if str(key) in list(map(lambda a: str(a),list(data.keys()))): 45 | found_key = True 46 | in_message = message 47 | return found_key, in_message, data # return useful data if keyis found 48 | return False, None, None # return this if key is not found 49 | -------------------------------------------------------------------------------- /DiscordDatabase/database.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import cached_property 3 | 4 | from .common_functions import key_check, search_key 5 | 6 | 7 | class Database: 8 | def __init__(self, category_object, channel_object) -> None: 9 | self.__category = category_object 10 | self.__channel = ( 11 | channel_object # Channel in which all key value pairs are stored 12 | ) 13 | 14 | @cached_property 15 | def get_channel_id(self): 16 | return self.__channel.id 17 | 18 | @cached_property 19 | def get_category_id(self): 20 | return self.__category.id 21 | 22 | async def set(self, key: str, value): 23 | key_check(str(key)) 24 | if len(str(value)) <= 0: 25 | raise ValueError("value should atleast have a length of 1") 26 | 27 | # True/False should become 1/0 28 | # integers and floats should become strings 29 | # In the end everything is stored as a string, along with an identifier 30 | """ Every message should look like this 31 | {key:value,type:'...'} 32 | """ 33 | 34 | # conversion into storable string 35 | if isinstance(value, bool): 36 | value = int(value) 37 | if isinstance(value, (float, int)): 38 | value = str(value) 39 | 40 | found_key, in_message, data = await search_key(str(key), self.__channel) 41 | if found_key: 42 | data[str(key)] = value 43 | data["type"] = value.__class__.__name__ 44 | await in_message.edit(content=json.dumps(data)) 45 | else: 46 | data = {key: value, "type": value.__class__.__name__} 47 | await self.__channel.send(json.dumps(data)) 48 | return 49 | 50 | async def get(self, key: str): 51 | key_check(str(key)) 52 | found_key, in_message, data = await search_key(key, self.__channel) 53 | if found_key: 54 | value = data[str(key)] 55 | value_type = data["type"] 56 | 57 | if value_type == "int": 58 | value = int(value) 59 | elif value_type == "float": 60 | value = float(value) 61 | 62 | if value_type == "bool": 63 | value = bool(int(value)) 64 | else: 65 | value = None 66 | return value 67 | 68 | async def delete(self, key: str): 69 | key_check(str(key)) 70 | found_key, in_message, data = await search_key(key, self.__channel) 71 | if found_key: 72 | await in_message.delete() 73 | return data.get(str(key)) # return the value of that key after deleting 74 | return # returns None if key waws not found and nothing was deleted 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2018 ANKUSH SINGH 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Database 2 | 3 | **NOTE: Since there have been many breaking changes with how Discord API and the python library work, this project has been scraped. Apologies for inconvenience** 4 | 5 | A CRUD (Create Read Update Delete) database for python Discord bot developers. All data is stored in key-value pairs directly on discord in the form of text messages, in the text channel that you specify. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | pip3 uninstall discord.py 11 | ``` 12 | 13 | ```bash 14 | pip3 install DiscordDatabase 15 | ``` 16 | 17 | ## Getting Started 18 | 19 | ### Import libraries 20 | 21 | ```python 22 | import discord 23 | from discord.ext import commands 24 | from DiscordDatabase import DiscordDatabase 25 | ``` 26 | 27 | ### Retrieve the `guild_id` of the server where you would like to store the data 28 | 29 | ```python 30 | DB_GUILD_ID = your_guild_id 31 | ``` 32 | 33 |
34 | Click here to see how to get guild_id 35 |
36 | how to get guild_id 37 |
38 |
39 | 40 | ### Initialize bot and database 41 | 42 | ```python 43 | bot = commands.Bot(command_prefix=">") 44 | # OR 45 | bot = discord.Client() 46 | 47 | db = DiscordDatabase(bot, DB_GUILD_ID) 48 | ``` 49 | 50 | db functions can only be used when bot is ready 51 | 52 | ```python 53 | @bot.event 54 | async def on_ready(): 55 | print("Bot is online") 56 | database = await db.new("CATEGORY_NAME","CHANNEL_NAME") 57 | 58 | ... 59 | 60 | 61 | bot.run("TOKEN") 62 | ``` 63 | 64 | Category and Channel will be created if they dont exist.\ 65 | You can create as many databases needed as you want, with a unique channel or category name. 66 | 67 | ### Acessing the database 68 | 69 | Since the scope of `database` object is limited inside `on_ready()` we will use `set()`, `get()` and `delete()` functions inside `on_ready()`.\ 70 | You can set the `database` object to be a global class variable in you bot so you can use it anywhere you want. 71 | 72 | ### Store data in the database 73 | 74 | ```python 75 | await database.set(KEY,VALUE) 76 | ``` 77 | 78 | Everything is stored as key and value pairs in the text channel you set earlier. 79 | 80 | e.g. 81 | 82 | ```python 83 | await database.set("name","Ankush") 84 | 85 | await database.set("name_list",["Ankush","Weeblet","ankooooSH"]) 86 | 87 | await database.set("age",18) 88 | ``` 89 | 90 | If a key already exists it will be updated with the new value 91 | 92 | ### Get data from the database 93 | 94 | ```python 95 | value = await database.get(KEY) 96 | ``` 97 | 98 | returns `None` if key doesnot exist 99 | 100 | e.g. 101 | 102 | ```python 103 | name = await database.get("name") 104 | # returns "Ankush" 105 | 106 | names = await database.get("name_list") 107 | # returns ["Ankush","Weeblet","ankooooSH"] 108 | 109 | age = await database.get("age") 110 | # returns 18 111 | 112 | unknown = await database.get("phone_number") 113 | # returns None because phone_number doesnot exist in database 114 | ``` 115 | 116 | ### Deleting data 117 | 118 | `delete()` returns the value of a key and deletes it. 119 | 120 | ```python 121 | await database.delete(KEY) 122 | ``` 123 | 124 | e.g. 125 | 126 | ```python 127 | name = await database.delete("name") 128 | # returns name and deletes it 129 | 130 | name = await database.delete("name") 131 | #when run twice returns None 132 | ``` 133 | -------------------------------------------------------------------------------- /images/guild_id.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankushKun/DiscordDatabase/fe932d800b1b847cf97b3733a260aeb582b57fde/images/guild_id.gif -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Always prefer setuptools over distutils 2 | # To use a consistent encoding 3 | from codecs import open 4 | from os import path 5 | 6 | from setuptools import find_packages, setup 7 | 8 | # The directory containing this file 9 | HERE = path.abspath(path.dirname(__file__)) 10 | 11 | # Get the long description from the README file 12 | with open(path.join(HERE, "README.md"), encoding="utf-8") as f: 13 | long_description = f.read() 14 | 15 | # This call to setup() does all the work 16 | setup( 17 | name="DiscordDatabase", 18 | version="0.1.3", 19 | description="CRUD database for discord bots, using discord text channels to store data", 20 | long_description=long_description, 21 | long_description_content_type="text/markdown", 22 | url="https://github.com/ankushKun/DiscordDatabase", 23 | author="Ankush Singh", 24 | author_email="ankush4singh@gmail.com", 25 | license="MIT", 26 | classifiers=[ 27 | "Intended Audience :: Developers", 28 | "License :: OSI Approved :: MIT License", 29 | "Programming Language :: Python :: 3", 30 | "Operating System :: OS Independent", 31 | ], 32 | # extras_require = { 33 | # 'py-cord': ["py-cord"] 34 | # }, 35 | packages=find_packages(), 36 | include_package_data=True 37 | ) 38 | -------------------------------------------------------------------------------- /upload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo " 4 | Increment version before running this script" 5 | sleep 2 6 | 7 | echo " 8 | removing old builds" 9 | sleep 2 10 | rm -rf dist 11 | rm -rf build 12 | rm -rf DiscordDatabase.egg-info 13 | 14 | echo " 15 | building sdist and bdist_wheel" 16 | sleep 2 17 | python3 setup.py sdist bdist_wheel 18 | 19 | echo " 20 | checking for errors" 21 | sleep 2 22 | twine check dist/* 23 | 24 | echo " 25 | uploading to PyPi" 26 | sleep 2 27 | twine upload dist/* --verbose 28 | 29 | 30 | echo " 31 | pushing changes to github" 32 | sleep 2 33 | git add . 34 | git commit -m "$1" 35 | git push --------------------------------------------------------------------------------