├── .gitignore ├── CONTRIBUTING.md ├── Clear pycache.bat ├── Create portable Modis environment.bat ├── GenerateModuleDoc.py ├── GenerateReadme.py ├── LICENSE ├── LauncherCMD.py ├── LauncherGUI.pyw ├── MANIFEST.in ├── README.md ├── modis ├── __init__.py ├── api │ ├── github.py │ ├── soundcloud.py │ ├── spotify.py │ └── youtube.py ├── assets │ ├── 128t.png │ ├── 2048t.png │ ├── 512t.png │ ├── 64t.png │ ├── modis.ico │ └── palette.png ├── gui │ ├── tabs │ │ ├── core.py │ │ ├── database.py │ │ ├── download.py │ │ └── modules.py │ └── window.py ├── main.py ├── modules │ └── !core │ │ ├── __info.py │ │ ├── _data.py │ │ ├── api_core.py │ │ ├── on_guild_available.py │ │ ├── on_guild_join.py │ │ ├── on_guild_remove.py │ │ ├── on_message.py │ │ └── on_ready.py └── tools │ ├── api.py │ ├── config.py │ ├── data.py │ ├── embed.py │ ├── help.py │ ├── log.py │ ├── moduledb.py │ └── version.py ├── requirements.txt ├── setup.py └── venv-resources ├── #START MODIS.bat ├── Activate.ps1 ├── activate ├── activate.bat └── ffmpeg.exe /.gitignore: -------------------------------------------------------------------------------- 1 | ######## MODIS ######## 2 | *.log 3 | TestModis.py 4 | OfficialModis.py 5 | data.json 6 | data.json.old 7 | modis/discord_modis/modules/pingpong/ 8 | .songcache/ 9 | modis-venv/ 10 | modis/modules/* 11 | !modis/modules/!core 12 | 13 | ######## PYCHARM ######## 14 | .idea 15 | .fleet 16 | 17 | ######## GITHUB ######## 18 | docs/_site/ 19 | 20 | ######## WINDOWS ######## 21 | # Windows thumbnail cache files 22 | Thumbs.db 23 | ehthumbs.db 24 | ehthumbs_vista.db 25 | 26 | # Windows batch and build files 27 | # *.bat 28 | *.vbs 29 | 30 | # Dump file 31 | *.stackdump 32 | 33 | # Folder config file 34 | Desktop.ini 35 | 36 | # Recycle Bin used on file shares 37 | $RECYCLE.BIN/ 38 | 39 | # Windows Installer files 40 | *.cab 41 | *.msi 42 | *.msm 43 | *.msp 44 | 45 | # Windows shortcuts 46 | *.lnk 47 | 48 | ######## PYTHON ######## 49 | # Byte-compiled / optimized / DLL files 50 | __pycache__/ 51 | *.py[cod] 52 | *$py.class 53 | 54 | # C extensions 55 | *.so 56 | 57 | # Distribution / packaging 58 | .Python 59 | build/ 60 | develop-eggs/ 61 | dist/ 62 | downloads/ 63 | eggs/ 64 | .eggs/ 65 | lib/ 66 | lib64/ 67 | parts/ 68 | sdist/ 69 | var/ 70 | wheels/ 71 | *.egg-info/ 72 | .installed.cfg 73 | *.egg 74 | 75 | # PyInstaller 76 | # Usually these files are written by a python script from a template 77 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 78 | *.manifest 79 | *.spec 80 | 81 | # Installer logs 82 | pip-log.txt 83 | pip-delete-this-directory.txt 84 | 85 | # Unit test / coverage reports 86 | htmlcov/ 87 | .tox/ 88 | .coverage 89 | .coverage.* 90 | .cache 91 | nosetests.xml 92 | coverage.xml 93 | *.cover 94 | .hypothesis/ 95 | 96 | # Translations 97 | *.mo 98 | *.pot 99 | 100 | # Django stuff: 101 | *.log 102 | local_settings.py 103 | 104 | # Flask stuff: 105 | instance/ 106 | .webassets-cache 107 | 108 | # Scrapy stuff: 109 | .scrapy 110 | 111 | # Sphinx documentation 112 | docs/_build/ 113 | 114 | # PyBuilder 115 | target/ 116 | 117 | # Jupyter Notebook 118 | .ipynb_checkpoints 119 | 120 | # pyenv 121 | .python-version 122 | 123 | # celery beat schedule file 124 | celerybeat-schedule 125 | 126 | # SageMath parsed files 127 | *.sage.py 128 | 129 | # Environments 130 | .env 131 | .venv 132 | env/ 133 | venv/ 134 | ENV/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /_site 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | 150 | ######## ATOM ######## 151 | .atom-build.* 152 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Helping out 4 | 5 | We always welcome people looking to help make Modis bigger and better! To get started, check the issues for anything marked 'help wanted' without any assignees. If there's nothing there, then asking over in our [Discord Server](https://discordapp.com/invite/z83UGvK) for things to do, or checking the 'projects' tab are good places to find things to work on. Remember: you should ask the before starting work on something, to make sure that you aren't doing the same work as somebody else. 6 | 7 | ### New Features 8 | 9 | You have a new idea for something you want in Modis? Great! If you want it included in Modis, it's best to message the devs and check that it's ok. We'll probably say yes but we also don't want to have blocks of code thrown at us in case it isn't something we want in our core codebase. If your new feature isn't going to be something that everyone wants, then you should make the feature easy to opt in/out of. All modules should support the `activated` flag set by the `manager` module, while new features for modules should use the `_data.py` and `data.json` to store settings on a per-server basis. 10 | 11 | ### Making changes 12 | 13 | First, you'll want to clone Modis locally to your own computer. Create a new branch for whatever it is you're working on from the 'unstable' branch, and name is something useful (like module/my-module, or bugfix/my-bugfix). Work on your code in that branch, and when you're ready to submit your changes make a pull request back into 'unstable' from your branch. 14 | 15 | ## Issues 16 | 17 | ### Bugs 18 | 19 | When submitting bugs to issues make sure that you provide steps on how to reproduce the bug, as well as any relevant system information. 20 | 21 | > For example: a bad bug report would be "Music module crashes". A better one would be "Music module crashes when playing geo-locked videos", and giving steps and a link in the description. 22 | 23 | ### Features 24 | 25 | Features can be either additions to existing modules, or new modules you'd like to see added. When suggesting features, make sure to give a clear idea of what you're asking for: descriptions, links, and images help! 26 | 27 | ### Questions/Problems 28 | 29 | For questions, or for things that might be related to your installation of Modis rather than the codebase, check out our [Discord Server](https://discordapp.com/invite/z83UGvK) to get help with setting up Modis. 30 | -------------------------------------------------------------------------------- /Clear pycache.bat: -------------------------------------------------------------------------------- 1 | for /d /r . %%d in (__pycache__) do @if exist "%%d" echo "%%d" && rd /s/q "%%d" 2 | pause -------------------------------------------------------------------------------- /Create portable Modis environment.bat: -------------------------------------------------------------------------------- 1 | echo off 2 | cls 3 | echo Creating Python virtual environment... 4 | set VENVPATH=.\modis-venv 5 | echo( 6 | echo Activating virtual environment... 7 | python -m venv "%VENVPATH%" 8 | call "%VENVPATH%"\Scripts\activate.bat 9 | echo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | echo( 11 | echo Updating pip... 12 | python -m pip install --upgrade pip 13 | echo( 14 | echo Installing Modis Python package requirements... 15 | pip install wheel 16 | pip install tkinter 17 | pip install -U discord.py[voice] 18 | pip install PyGithub 19 | pip install youtube-dl 20 | pip install soundcloud 21 | pip install pynacl 22 | pip install google-api-python-client 23 | pip install requests 24 | pip install lxml 25 | pip install praw 26 | pip install spotipy 27 | echo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | echo( 29 | echo Copying ffmpeg binary... 30 | xcopy .\venv-resources\ffmpeg.exe "%VENVPATH%"\Scripts 31 | echo( 32 | echo Copying modis files... 33 | xcopy .\modis "%VENVPATH%"\modis /s /e /i 34 | xcopy .\LauncherCMD.py "%VENVPATH%" 35 | xcopy .\LauncherGUI.pyw "%VENVPATH%" 36 | xcopy ".\venv-resources\#START MODIS.bat" "%VENVPATH%" 37 | echo( 38 | echo Replacing scripts with version with relative paths 39 | xcopy .\venv-resources\activate "%VENVPATH%"\Scripts /y 40 | xcopy .\venv-resources\activate.bat "%VENVPATH%"\Scripts /y 41 | xcopy .\venv-resources\Activate.ps1 "%VENVPATH%"\Scripts /y 42 | echo( 43 | echo deleting __pycache__ folders and .pyc files 44 | for /d /r "%VENVPATH%" %%d in (__pycache__) do @if exist "%%d" echo "%%d" && rd /s/q "%%d" 45 | echo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | echo( 47 | echo Portable Modis creation complete. 48 | pause -------------------------------------------------------------------------------- /GenerateModuleDoc.py: -------------------------------------------------------------------------------- 1 | """Generate a README.md file for Modis""" 2 | 3 | from modis.tools import help, config 4 | 5 | 6 | def add_md(text, s, level=0): 7 | """Appends text to a markdown. 8 | 9 | Args: 10 | text (str): The old text. 11 | s (str): The text to append. 12 | level (int): The markdown level of the appended text. 13 | 14 | Returns: 15 | text (str): The updated text 16 | """ 17 | 18 | if level > 0: 19 | if text != "": 20 | text += "\n" 21 | text += "#" * level 22 | text += " " 23 | 24 | text += s + "\n" 25 | 26 | if level > 0: 27 | text += "\n" 28 | 29 | return text 30 | 31 | 32 | moduledoc = "" 33 | 34 | # Get module 35 | module_name = input("Module name: ") 36 | 37 | # Get help data 38 | datapacks = help.get_formatted(module_name, "!") 39 | if datapacks: 40 | moduledoc = add_md(moduledoc, module_name, 1) 41 | for d in datapacks: 42 | moduledoc = add_md(moduledoc, d[0], 2) 43 | moduledoc = add_md(moduledoc, d[1]) 44 | 45 | print(moduledoc) 46 | newreadme_path = "{}/../{}.md".format(config.ROOT_DIR, module_name) 47 | with open(newreadme_path, 'w') as file: 48 | file.write(moduledoc) 49 | -------------------------------------------------------------------------------- /GenerateReadme.py: -------------------------------------------------------------------------------- 1 | """Generate a README.md file for Modis""" 2 | 3 | from modis.tools import config, moduledb, version 4 | 5 | 6 | def add_md(text, s, level=0): 7 | """Appends text to a markdown. 8 | 9 | Args: 10 | text (str): The old text. 11 | s (str): The text to append. 12 | level (int): The markdown level of the appended text. 13 | 14 | Returns: 15 | text (str): The updated text 16 | """ 17 | 18 | if level > 0: 19 | if text != "": 20 | text += "\n" 21 | text += "#" * level 22 | text += " " 23 | 24 | text += s + "\n" 25 | 26 | if level > 0: 27 | text += "\n" 28 | 29 | return text 30 | 31 | 32 | def add_ul(text, ul): 33 | """Appends an unordered list to a markdown. 34 | 35 | Args: 36 | text (str): The old text. 37 | ul (list): The list to append. 38 | 39 | Returns: 40 | text (str): The updated text. 41 | """ 42 | 43 | text += "\n" 44 | for li in ul: 45 | text += "- " + li + "\n" 46 | text += "\n" 47 | 48 | return text 49 | 50 | 51 | readme = "" 52 | 53 | # Title 54 | readme = add_md(readme, "MODIS", 1) 55 | readme = add_md(readme, "Latest release: [v{0}{1}]({2}/{0})".format( 56 | version.VERSION, 57 | " Beta" if version.VERSION[0] < "1" else "", 58 | "https://github.com/Infraxion/modis/releases/tag" 59 | )) 60 | 61 | # About 62 | readme = add_md(readme, "About Modis", 2) 63 | readme = add_md(readme, "Modis is an highly modular, open-source Discord bot " 64 | "that runs with a console GUI. Our goal is to make " 65 | "Modis as easy to host as possible, so that any " 66 | "Discord user can host their own bot. Modis is also " 67 | "designed to be very easy to develop for; it's " 68 | "modularised in a way that makes it very easy to " 69 | "understand for anyone familiar with the discord.py " 70 | "Python library.\n\n" 71 | "We hope that this bot introduces more novices to the " 72 | "painful world of software development and networking, " 73 | "and provides seasoned devs with something to " 74 | "procrastinate their deadline on. Have fun!") 75 | 76 | # Module list 77 | module_names = moduledb.get_names() 78 | readme = add_md(readme, "Current Modules", 2) 79 | readme = add_md(readme, "There are currently {} available modules:".format(len(module_names))) 80 | 81 | module_list = [] 82 | for module_name in module_names: 83 | info = moduledb.get_import_specific("__info", module_name) 84 | if not info: 85 | module_list.append("`{}`".format(module_name)) 86 | continue 87 | if not info.BLURB: 88 | module_list.append("`{}`".format(module_name)) 89 | continue 90 | module_list.append("`{}` - {}".format(module_name, info.BLURB)) 91 | readme = add_ul(readme, module_list) 92 | 93 | readme = add_md(readme, "More detailed information about each module and how " 94 | "to use them can be found in the [docs](https://" 95 | "infraxion.github.io/modis/documentation/#modules).") 96 | 97 | # Write file 98 | print(readme) 99 | newreadme_path = "{}/../README.md".format(config.ROOT_DIR) 100 | with open(newreadme_path, 'w') as file: 101 | file.write(readme) 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Ango Zhu and Joel Launder 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LauncherCMD.py: -------------------------------------------------------------------------------- 1 | import modis 2 | 3 | modis.cmd() 4 | -------------------------------------------------------------------------------- /LauncherGUI.pyw: -------------------------------------------------------------------------------- 1 | import modis 2 | 3 | modis.gui() 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include modis * 2 | prune modis/**/__pycache__ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MODIS 2 | 3 | Latest release: [v0.4.0 Beta](https://github.com/Infraxion/modis/releases/tag/0.4.0) 4 | 5 | ## About Modis 6 | 7 | Modis is a Discord bot that runs with a graphical UI and is designed to be both easy to host and easy to customise. Anyone with some basic Python knowledge can quickly and easily create new bot modules, while Modis' extensive library of internal APIs makes creating complex modules with embed UIs and permissioned message commands a breeze for experienced developers. Anyone familiar with the discord.py library will be able to pick up Modis module development in seconds. 8 | 9 | For those who aren't interested in development, Modis' graphical console window makes hosting your own fully featured Discord bot easy and fun, with the stock modules providing incredible features such as the music module's in-channel graphical interface, complete with progress bars and buttons. 10 | 11 | ## Installation 12 | 13 | temp 14 | 15 | ## ModisWorks Modules 16 | 17 | ModisWorks develops 4 core modules for Modis that will give you base functionality for your bot. 18 | - `!core` - Manages all the behind the scenes stuff that runs the internal APIs the modules use. Be careful not to remove this module! A lot of other modules need this module to work properly. 19 | - `help` - Modis with a bunch of modules becomes a big bot very quickly, so this module helps to alleviate the learning cliff by allowing easy access to command definitions inside Discord. 20 | - `manager` - Provides essential server management tools for server owners, such as activating and deactivating modules, changing the command prefix, and various moderation tools. 21 | - `music` - Modis' flagship module - a music player featuring a live-updating GUI with a progress bar, queue display, and more. The GUI also has working media buttons for easy control without needing to know any commands. The player supports songs and playlists for YouTube, Spotify, and SoundCloud, and can play most online audio sources. 22 | 23 | We also make a bunch of other modules to show off what you can do with Modis: 24 | - `bethebot` - Modis can't really have conversations with your server members, but you can fake it by taking control! 25 | - `hex` - Visually displays hex colours if it sees them in your messages. Handy for graphic designers and programmers. 26 | - `gamedeals` - Posts current hot posts above an upvote threshold of a particular subreddit. It's set to /r/gamedeals by default so you can make sure you don't miss any sales! 27 | - `replies` - Allows server owners to easily set the bot to reply to specific phrases. 28 | - `rocketleague` - Looks up your Rocket League rank and stats; currently supported for Steam, XBox, and PS4 players. 29 | - `tableflip` - The best module. 30 | 31 | More detailed information about each module and how to use them can be found in the [docs](https://infraxion.github.io/modis/documentation/#modules). 32 | 33 | ## Third-Party Modules 34 | 35 | Modis has a module downloader built in that can pull modules from GitHub repos and automatically install them. Here's a list of developers with their own module libraries that you can download and try: 36 | 37 | - temp 38 | 39 | If you'd like even more modules, join our [Discord] and ask around! 40 | -------------------------------------------------------------------------------- /modis/__init__.py: -------------------------------------------------------------------------------- 1 | """WELCOME TO MODIS 2 | 3 | These docstrings will guide you through how Modis' internals work, and get you started with developing for Modis. 4 | 5 | For more help, go to our website at https://modisworks.github.io/ or join our discord (click "connect" in the Discord embed on the website) where other developers will be glad to help you out :) 6 | 7 | Have fun! 8 | """ 9 | 10 | import logging 11 | from modis.tools import data, config, log 12 | 13 | # Update data.json cache 14 | data.pull() 15 | 16 | # Setup logging 17 | logger = logging.getLogger(__name__) 18 | log.init_print(logger) 19 | log.init_file(logger) 20 | 21 | 22 | def cmd() -> None: 23 | """Starts Modis without the GUI.""" 24 | 25 | logger.info("Starting Modis") 26 | 27 | # Setup Modis event loop 28 | import asyncio 29 | from modis import main 30 | 31 | loop = asyncio.get_event_loop() 32 | asyncio.set_event_loop(loop) 33 | 34 | # Start Modis 35 | main.start(loop) 36 | 37 | 38 | def gui() -> None: 39 | """Starts Modis with the GUI.""" 40 | 41 | logger.info("Starting Modis console GUI") 42 | 43 | # Start Modis console GUI 44 | import tkinter as tk 45 | from modis.gui import window 46 | 47 | # Setup the root window 48 | root = tk.Tk() 49 | root.minsize(width=800, height=400) 50 | root.geometry("800x600") 51 | root.title("Modis Control Panel") 52 | try: 53 | root.iconbitmap("{}/assets/modis.ico".format(__file__[:-11])) 54 | except tk.TclError: 55 | logger.warning("Could not resolve asset path") 56 | 57 | # Add elements 58 | main = window.RootFrame(root) 59 | 60 | # Grid elements 61 | main.grid(column=0, row=0, padx=0, pady=0, sticky="W E N S") 62 | 63 | # Configure stretch ratios 64 | root.columnconfigure(0, weight=1) 65 | root.rowconfigure(0, weight=1) 66 | main.columnconfigure(0, weight=1) 67 | main.rowconfigure(0, weight=1) 68 | 69 | # Run the root window 70 | root.mainloop() 71 | -------------------------------------------------------------------------------- /modis/api/github.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/api/github.py -------------------------------------------------------------------------------- /modis/api/soundcloud.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/api/soundcloud.py -------------------------------------------------------------------------------- /modis/api/spotify.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/api/spotify.py -------------------------------------------------------------------------------- /modis/api/youtube.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/api/youtube.py -------------------------------------------------------------------------------- /modis/assets/128t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/assets/128t.png -------------------------------------------------------------------------------- /modis/assets/2048t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/assets/2048t.png -------------------------------------------------------------------------------- /modis/assets/512t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/assets/512t.png -------------------------------------------------------------------------------- /modis/assets/64t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/assets/64t.png -------------------------------------------------------------------------------- /modis/assets/modis.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/assets/modis.ico -------------------------------------------------------------------------------- /modis/assets/palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/modis/assets/palette.png -------------------------------------------------------------------------------- /modis/gui/tabs/core.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import threading 4 | import tkinter as tk 5 | import tkinter.ttk as ttk 6 | from tkinter import filedialog, messagebox 7 | import webbrowser 8 | import json 9 | 10 | from modis.tools import data, config 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class Frame(ttk.Frame): 16 | """A tab containing the core controls of the bot""" 17 | 18 | def __init__(self, parent): 19 | """Create the frame. 20 | 21 | Args: 22 | parent: A tk or ttk object. 23 | """ 24 | 25 | super(Frame, self).__init__(parent, padding=8) 26 | 27 | # Add elements 28 | info = self.Info(self) 29 | control = self.Control(self) 30 | log = self.Log(self) 31 | 32 | # Grid elements 33 | info.grid(column=0, row=0, padx=8, pady=8, stick="W E N S") 34 | control.grid(column=1, row=0, padx=8, pady=8, sticky="W E N S") 35 | log.grid(column=0, columnspan=2, row=1, padx=8, pady=8, sticky="W E N S") 36 | 37 | # Configure stretch ratios 38 | self.columnconfigure(0, weight=0) 39 | self.columnconfigure(1, weight=1) 40 | self.rowconfigure(0, weight=0) 41 | self.rowconfigure(1, weight=1) 42 | 43 | class Info(ttk.LabelFrame): 44 | """The control panel for the Modis bot.""" 45 | 46 | def __init__(self, parent): 47 | """Create the frame. 48 | 49 | Args: 50 | parent: A tk or ttk object. 51 | """ 52 | 53 | super(Frame.Info, self).__init__(parent, padding=8, text="Info") 54 | 55 | # Variables 56 | self.invite_text = tk.StringVar(value="Paste Client ID here for invite link") 57 | 58 | # Add elements 59 | def hyperlink_website(event): 60 | webbrowser.open_new("https://modisworks.github.io/") 61 | 62 | def hyperlink_discord(event): 63 | webbrowser.open_new("https://modisworks.github.io/#getting-started") 64 | 65 | def hyperlink_invite(event): 66 | client_id = self.invite_text.get() 67 | 68 | if len(client_id) != 18: 69 | messagebox.showerror(title="Invalid Client ID", message="Client ID should be an 18 digit number.") 70 | return 71 | 72 | try: 73 | int(client_id) 74 | except ValueError: 75 | messagebox.showerror(title="Invalid Client ID", message="Client ID should be an 18 digit number.") 76 | return 77 | 78 | webbrowser.open_new("https://discordapp.com/oauth2/authorize?client_id={}&scope=bot&permissions=0".format(client_id)) 79 | 80 | image = tk.PhotoImage(file=__file__[:-16] + "assets/64t.png") 81 | logo = tk.Label(self, image=image) 82 | logo.image = image 83 | 84 | name = tk.Label(self, text="Welcome to Modis c:", justify="left") 85 | website = tk.Label(self, text="Website", fg="blue", cursor="hand2") 86 | website.bind("", hyperlink_website) 87 | discord = tk.Label(self, text="Discord server", fg="blue", cursor="hand2") 88 | discord.bind("", hyperlink_discord) 89 | 90 | clientid_entry = ttk.Entry(self, textvariable=self.invite_text) 91 | invite_link = tk.Label(self, text="Invite bot to server", fg="blue", cursor="hand2") 92 | invite_link.bind("", hyperlink_invite) 93 | 94 | # Grid elements 95 | logo.grid(column=0, row=0, rowspan=3, padx=4, pady=4, sticky="W") 96 | 97 | name.grid(column=1, row=0, padx=4, pady=4, sticky="W") 98 | website.grid(column=1, row=1, padx=4, pady=0, sticky="W") 99 | discord.grid(column=1, row=2, padx=4, pady=0, sticky="W") 100 | 101 | clientid_entry.grid(column=0, columnspan=2, row=3, padx=4, pady=4, sticky="W E") 102 | invite_link.grid(column=0, columnspan=2, row=4, padx=4, pady=0, sticky="W") 103 | 104 | # Configure stretch ratios 105 | self.columnconfigure(0, weight=0) 106 | self.columnconfigure(1, weight=1) 107 | self.rowconfigure(0, weight=0) 108 | self.rowconfigure(1, weight=0) 109 | self.rowconfigure(2, weight=0) 110 | self.rowconfigure(3, weight=0) 111 | self.rowconfigure(4, weight=0) 112 | 113 | class Control(ttk.Labelframe): 114 | """The control panel for the Modis bot.""" 115 | 116 | def __init__(self, parent): 117 | """Create the frame. 118 | 119 | Args: 120 | parent: A tk or ttk object. 121 | """ 122 | 123 | super(Frame.Control, self).__init__(parent, padding=8, text="Control") 124 | 125 | # Variables 126 | self.thread = None 127 | 128 | self.datapath = tk.StringVar(value=config.DATAFILE) 129 | 130 | self.token = tk.StringVar(value=data.cache["keys"]["discord_token"]) 131 | 132 | self.state = "off" 133 | self.button_text = tk.StringVar(value="Start Modis") 134 | 135 | # Add elements 136 | datapath_label = ttk.Label(self, text="Data file path:") 137 | datapath_entry = ttk.Entry(self, textvariable=self.datapath, state="readonly") 138 | datapath_button = ttk.Button(self, command=self.set_data_location, text="Change") 139 | 140 | token_label = ttk.Label(self, text="Discord bot token:") 141 | token_entry = ttk.Entry(self, textvariable=self.token, show="\u25cf") 142 | 143 | start_button = ttk.Button(self, command=self.toggle, textvariable=self.button_text) 144 | 145 | # Grid elements 146 | datapath_label.grid(column=0, row=0, padx=4, pady=4, stick="E") 147 | datapath_entry.grid(column=1, row=0, padx=4, pady=4, sticky="W E") 148 | datapath_button.grid(column=2, row=0, padx=4, pady=4, sticky="E") 149 | 150 | token_label.grid(column=0, row=1, padx=4, pady=4, sticky="E") 151 | token_entry.grid(column=1, columnspan=2, row=1, padx=4, pady=4, sticky="W E") 152 | 153 | start_button.grid(column=2, columnspan=3, row=3, padx=4, pady=4, sticky="E") 154 | 155 | # Configure stretch ratios 156 | self.columnconfigure(0, weight=0) 157 | self.columnconfigure(1, weight=1) 158 | self.columnconfigure(2, weight=0) 159 | self.rowconfigure(0, weight=0) 160 | self.rowconfigure(1, weight=0) 161 | self.rowconfigure(2, weight=1) 162 | self.rowconfigure(3, weight=0) 163 | 164 | def set_data_location(self): 165 | newpath = filedialog.askopenfile() 166 | oldpath = config.DATAFILE 167 | 168 | try: 169 | newpath = newpath.name 170 | except AttributeError: 171 | # Window was closed 172 | logger.warning("Data file not changed") 173 | return 174 | 175 | if not messagebox.askokcancel(title="Change data file path", message="Change data file to:\n{}".format(newpath)): 176 | # User cancelled path change 177 | messagebox.showinfo(title="Change data file path", message="Data file not changed.") 178 | return 179 | 180 | # Change the path 181 | config.DATAFILE = newpath 182 | 183 | try: 184 | data.pull() 185 | except json.decoder.JSONDecodeError: 186 | # Chosen file invalid 187 | logger.error("Chosen file is not a valid json; reverting changes") 188 | messagebox.showerror(title="Change data file path", message="Chosen file is not a valid json.") 189 | 190 | # Try again 191 | config.DATAFILE = oldpath 192 | data.pull() 193 | self.set_data_location() 194 | return 195 | 196 | # Successful change 197 | self.datapath.set(newpath) 198 | logger.warning("data file changed to " + config.DATAFILE) 199 | messagebox.showinfo(title="Change data file path", message="Data file change successful.") 200 | 201 | def toggle(self): 202 | """Toggle Modis on or off.""" 203 | 204 | if self.state == 'off': 205 | self.start() 206 | elif self.state == 'on': 207 | self.stop() 208 | 209 | def start(self): 210 | """Start Modis and log it into Discord.""" 211 | 212 | self.button_text.set("Stop Modis") 213 | self.state = "on" 214 | 215 | logger.warning("Starting Modis") 216 | statuslog = logging.getLogger("globalstatus") 217 | statuslog.info("1") 218 | 219 | data.cache["keys"]["discord_token"] = self.token.get() 220 | data.push() 221 | 222 | from modis import main 223 | loop = asyncio.new_event_loop() 224 | asyncio.set_event_loop(loop) 225 | self.thread = threading.Thread(target=main.start, args=[loop]) 226 | self.thread.start() 227 | 228 | def stop(self): 229 | """Stop Modis and log out of Discord.""" 230 | 231 | self.button_text.set("Start Modis") 232 | self.state = "off" 233 | 234 | logger.warning("Stopping Modis") 235 | statuslog = logging.getLogger("globalstatus") 236 | statuslog.info("0") 237 | 238 | from modis.main import client 239 | 240 | # Logout 241 | try: 242 | asyncio.run_coroutine_threadsafe(client.logout(), client.loop) 243 | except AttributeError: 244 | # Client object no longer exists 245 | pass 246 | 247 | try: 248 | self.thread.stop() 249 | except AttributeError: 250 | # Thread no longer exists 251 | return 252 | 253 | # Cancel all pending tasks 254 | # TODO Fix this 255 | # try: 256 | # pending = asyncio.Task.all_tasks(loop=client.loop) 257 | # gathered = asyncio.gather(*pending, loop=client.loop) 258 | # gathered.cancel() 259 | # client.loop.run_until_complete(gathered) 260 | # gathered.exception() 261 | # except Exception as e: 262 | # logger.exception(e) 263 | 264 | class Log(ttk.Labelframe): 265 | """The text box showing the logging output""" 266 | 267 | def __init__(self, parent): 268 | """Create the frame. 269 | 270 | Args: 271 | parent: A tk or ttk object. 272 | """ 273 | 274 | super(Frame.Log, self).__init__(parent, padding=8, text="Log") 275 | 276 | # Add elements 277 | log_panel = tk.Text(self, wrap="none") 278 | 279 | formatter = logging.Formatter("{levelname:8} {name} - {message}", style="{") 280 | handler = self.PanelHandler(log_panel) 281 | handler.setFormatter(formatter) 282 | 283 | root_logger = logging.getLogger("modis") 284 | root_logger.addHandler(handler) 285 | 286 | log_panel.configure(background="#202020") 287 | log_panel.tag_config('CRITICAL', foreground="#FF00AA") 288 | log_panel.tag_config('ERROR', foreground="#FFAA00") 289 | log_panel.tag_config('WARNING', foreground="#00AAFF") 290 | log_panel.tag_config('INFO', foreground="#AAAAAA") 291 | log_panel.tag_config('DEBUG', foreground="#444444") 292 | 293 | yscrollbar = ttk.Scrollbar(self, orient="vertical", command=log_panel.yview) 294 | xscrollbar = ttk.Scrollbar(self, orient="horizontal", command=log_panel.xview) 295 | log_panel['yscrollcommand'] = yscrollbar.set 296 | log_panel['xscrollcommand'] = xscrollbar.set 297 | 298 | # Grid elements 299 | log_panel.grid(column=0, row=0, sticky="W E N S") 300 | yscrollbar.grid(column=1, row=0, sticky="N S") 301 | xscrollbar.grid(column=0, row=1, sticky="W E") 302 | 303 | # Configure stretch ratios 304 | self.columnconfigure(0, weight=1) 305 | self.rowconfigure(0, weight=1) 306 | 307 | class PanelHandler(logging.Handler): 308 | def __init__(self, text_widget): 309 | logging.Handler.__init__(self) 310 | 311 | self.text_widget = text_widget 312 | self.text_widget.config(state=tk.DISABLED) 313 | 314 | def emit(self, record): 315 | msg = self.format(record) 316 | msg_level = logging.Formatter("{levelname}", style="{").format(record) 317 | # Remove '.modis' from start of logs 318 | msg = msg[:9] + msg[15:] 319 | # Exceptions 320 | if msg_level.startswith("ERROR"): 321 | msg_level = "ERROR" 322 | 323 | self.text_widget.config(state=tk.NORMAL) 324 | self.text_widget.insert("end", msg + "\n", msg_level) 325 | self.text_widget.config(state=tk.DISABLED) 326 | self.text_widget.see("end") 327 | -------------------------------------------------------------------------------- /modis/gui/tabs/database.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import tkinter as tk 4 | import tkinter.ttk as ttk 5 | from tkinter import filedialog, messagebox 6 | 7 | from modis.tools import config, data 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Frame(ttk.Frame): 13 | """A tab containing controls for the data.json""" 14 | 15 | def __init__(self, parent): 16 | """Create the frame. 17 | 18 | Args: 19 | parent: A tk or ttk object. 20 | """ 21 | 22 | super(Frame, self).__init__(parent, padding=8) 23 | 24 | # Add elements 25 | tree = self.DataTree(self) 26 | 27 | # Grid elements 28 | tree.grid(column=0, row=0, padx=8, pady=8, sticky="W E N S") 29 | 30 | # Configure stretch ratios 31 | self.columnconfigure(0, weight=1) 32 | self.rowconfigure(0, weight=1) 33 | 34 | class DataTree(ttk.LabelFrame): 35 | """The database display""" 36 | 37 | def __init__(self, parent): 38 | """Create the tree. 39 | 40 | Args: 41 | parent: A tk or ttk object. 42 | """ 43 | 44 | super(Frame.DataTree, self).__init__(parent, padding=8, text="Database tree") 45 | 46 | # Define variables 47 | self.datapath = tk.StringVar(value=config.DATAFILE) 48 | self.selected_key_var = tk.StringVar(value="Select a variable to edit") 49 | self.selected_val_var = tk.StringVar(value="") 50 | self.all_items = [] 51 | self.selected_path = [] 52 | 53 | # Add elements 54 | frame_path = ttk.Frame(self) 55 | datapath_label = ttk.Label(frame_path, text="Data file path:") 56 | datapath_entry = ttk.Entry(frame_path, textvariable=self.datapath, state="readonly") 57 | datapath_button = ttk.Button(frame_path, command=self.set_data_location, text="Change") 58 | 59 | frame_control = ttk.Frame(self) 60 | button_refresh = ttk.Button(frame_control, text="Refresh", command=self.tree_update) 61 | button_expand = ttk.Button(frame_control, text="Expand all", command=self.tree_expand) 62 | button_collapse = ttk.Button(frame_control, text="Collapse all", command=self.tree_collapse) 63 | 64 | frame_tree = ttk.Frame(self) 65 | self.tree = ttk.Treeview(frame_tree, columns="val") 66 | self.tree.bind("<>", self.tree_select) 67 | self.tree.column("#0", width=50) 68 | self.tree.heading("#0", text="Key") 69 | self.tree.heading("val", text="Value") 70 | self.tree_update() 71 | yscrollbar = ttk.Scrollbar(frame_tree, orient="vertical", command=self.tree.yview) 72 | xscrollbar = ttk.Scrollbar(frame_tree, orient="horizontal", command=self.tree.xview) 73 | self.tree['yscrollcommand'] = yscrollbar.set 74 | self.tree['xscrollcommand'] = xscrollbar.set 75 | 76 | frame_edit = ttk.Frame(self) 77 | label_key = ttk.Label(frame_edit, text="Selected key:") 78 | label_val = ttk.Label(frame_edit, text="Change to:") 79 | selected_key = ttk.Entry(frame_edit, textvariable=self.selected_key_var, state="readonly") 80 | selected_val = ttk.Entry(frame_edit, textvariable=self.selected_val_var) 81 | self.selected_enter = ttk.Button(frame_edit, text="Enter", command=self.tree_edit, state="disabled") 82 | 83 | # Grid elements 84 | datapath_label.grid(column=0, row=0, padx=4, pady=4, stick="E") 85 | datapath_entry.grid(column=1, row=0, padx=4, pady=4, sticky="W E") 86 | datapath_button.grid(column=2, row=0, padx=4, pady=4, sticky="E") 87 | 88 | button_refresh.grid(column=0, row=0, padx=4, pady=4, sticky="W") 89 | button_expand.grid(column=1, row=0, padx=4, pady=4, sticky="W") 90 | button_collapse.grid(column=2, row=0, padx=4, pady=4, sticky="W") 91 | 92 | self.tree.grid(column=0, row=0, sticky="W E N S") 93 | yscrollbar.grid(column=1, row=0, sticky="N S") 94 | xscrollbar.grid(column=0, row=1, sticky="W E") 95 | 96 | label_key.grid(column=0, row=0, padx=4, pady=4, sticky="E") 97 | selected_key.grid(column=1, columnspan=2, row=0, padx=4, pady=4, sticky="W E") 98 | label_val.grid(column=0, row=1, padx=4, pady=4, sticky="E") 99 | selected_val.grid(column=1, row=1, padx=4, pady=4, sticky="W E") 100 | self.selected_enter.grid(column=2, row=1, padx=4, pady=4, sticky="E") 101 | 102 | frame_path.grid(column=0, row=0, sticky="W E N S") 103 | frame_control.grid(column=0, row=1, sticky="W E N S") 104 | frame_tree.grid(column=0, row=2, sticky="W E N S") 105 | frame_edit.grid(column=0, row=3, sticky="W E N S") 106 | 107 | # Configure stretch ratios 108 | self.columnconfigure(0, weight=1) 109 | self.rowconfigure(0, weight=0) 110 | self.rowconfigure(1, weight=0) 111 | self.rowconfigure(2, weight=1) 112 | self.rowconfigure(3, weight=0) 113 | 114 | frame_path.columnconfigure(0, weight=0) 115 | frame_path.columnconfigure(1, weight=1) 116 | frame_path.columnconfigure(2, weight=0) 117 | frame_path.rowconfigure(0, weight=0) 118 | 119 | frame_control.columnconfigure(0, weight=0) 120 | frame_control.rowconfigure(0, weight=0) 121 | frame_control.rowconfigure(1, weight=0) 122 | frame_control.rowconfigure(2, weight=0) 123 | 124 | frame_tree.columnconfigure(0, weight=1) 125 | frame_tree.columnconfigure(1, weight=0) 126 | frame_tree.rowconfigure(0, weight=1) 127 | frame_tree.rowconfigure(1, weight=0) 128 | 129 | frame_edit.columnconfigure(0, weight=0) 130 | frame_edit.columnconfigure(1, weight=1) 131 | frame_edit.columnconfigure(2, weight=0) 132 | frame_edit.rowconfigure(0, weight=0) 133 | frame_edit.rowconfigure(0, weight=0) 134 | 135 | def tree_update(self): 136 | for item in self.all_items: 137 | try: 138 | self.tree.delete(item) 139 | except tk.TclError: 140 | pass 141 | self.all_items = [] 142 | 143 | from modis.tools import data 144 | 145 | def recursive_add(entry, parent=""): 146 | for key in entry: 147 | if type(entry[key]) is dict: 148 | new_item = self.tree.insert(parent, "end", text=key) 149 | self.all_items.append(new_item) 150 | recursive_add(entry[key], new_item) 151 | else: 152 | new_item = self.tree.insert(parent, "end", text=key, values=str(entry[key])) 153 | self.all_items.append(new_item) 154 | 155 | recursive_add(data.cache) 156 | 157 | def tree_expand(self): 158 | for item in self.all_items: 159 | self.tree.item(item, open=True) 160 | 161 | def tree_collapse(self): 162 | for item in self.all_items: 163 | self.tree.item(item, open=False) 164 | 165 | def tree_select(self, event): 166 | selected = self.tree.focus() 167 | self.selected_key_var.set(self.tree.item(selected)["text"]) 168 | 169 | if self.tree.item(selected)["values"]: 170 | self.selected_val_var.set(self.tree.item(selected)["values"][0]) 171 | self.selected_enter.config(state="enabled") 172 | elif not self.tree.get_children(selected): 173 | self.selected_val_var.set("") 174 | self.selected_enter.config(state="enabled") 175 | else: 176 | self.selected_val_var.set("") 177 | self.selected_enter.config(state="disabled") 178 | 179 | self.selected_path = [] 180 | 181 | def recursive_get(item): 182 | parent_item = self.tree.parent(item) 183 | parent_name = self.tree.item(parent_item)["text"] 184 | self.selected_path.insert(0, self.tree.item(item)["text"]) 185 | if parent_name: 186 | recursive_get(parent_item) 187 | 188 | recursive_get(selected) 189 | self.selected_key_var.set(".".join(self.selected_path)) 190 | 191 | def tree_edit(self): 192 | if not messagebox.askokcancel(title="Edit value", message="Set {} to:\n{}".format(self.selected_key_var.get(), self.selected_val_var.get())): 193 | # User cancelled value edit 194 | messagebox.showinfo(title="Edit value", message="Value not changed.") 195 | return 196 | 197 | from modis.tools import data 198 | 199 | pathstr = "" 200 | for item in self.selected_path: 201 | pathstr += """["{}"]""".format(item) 202 | 203 | logger.warning("Updating {} to {}".format(self.selected_key_var.get(), self.selected_val_var.get())) 204 | exec("data.cache{} = self.selected_val_var.get()".format(pathstr)) 205 | 206 | data.push() 207 | self.tree_update() 208 | messagebox.showinfo(title="Edit value", message="Edit successful.") 209 | 210 | def set_data_location(self): 211 | newpath = filedialog.askopenfile() 212 | oldpath = config.DATAFILE 213 | 214 | try: 215 | newpath = newpath.name 216 | except AttributeError: 217 | # Window was closed 218 | logger.warning("Data file not changed") 219 | return 220 | 221 | if not messagebox.askokcancel(title="Change data file path", message="Change data file to:\n{}".format(newpath)): 222 | # User cancelled path change 223 | messagebox.showinfo(title="Change data file path", message="Data file not changed.") 224 | return 225 | 226 | # Change the path 227 | config.DATAFILE = newpath 228 | 229 | try: 230 | data.pull() 231 | except json.decoder.JSONDecodeError: 232 | # Chosen file invalid 233 | logger.error("Chosen file is not a valid json; reverting changes") 234 | messagebox.showerror(title="Change data file path", message="Chosen file is not a valid json.") 235 | 236 | # Try again 237 | config.DATAFILE = oldpath 238 | data.pull() 239 | self.set_data_location() 240 | return 241 | 242 | # Successful change 243 | self.datapath.set(newpath) 244 | self.tree_update() 245 | logger.warning("data file changed to " + config.DATAFILE) 246 | messagebox.showinfo(title="Change data file path", message="Data file change successful.") 247 | -------------------------------------------------------------------------------- /modis/gui/tabs/download.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import tkinter as tk 3 | import tkinter.ttk as ttk 4 | import threading 5 | 6 | from modis.tools import data 7 | 8 | import github 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | githubapi = github.Github() 13 | 14 | # TODO pip requirements listing, installation and uninstallation 15 | # TODO live module refresh 16 | 17 | # import subprocess 18 | # import sys 19 | # 20 | # def install(package): 21 | # subprocess.call([sys.executable, "-m", "pip", "install", package]) 22 | 23 | 24 | class Frame(ttk.Frame): 25 | """A tab containing tools to install and manage modules.""" 26 | 27 | def __init__(self, parent): 28 | """Create the frame. 29 | 30 | Args: 31 | parent: A tk or ttk object. 32 | """ 33 | 34 | super(Frame, self).__init__(parent, padding=8) 35 | 36 | # Add elements 37 | find = self.Find(self) 38 | info = self.Info(self) 39 | installed = self.Installed(self) 40 | 41 | # Grid elements 42 | find.grid(column=0, row=0, padx=8, pady=8, sticky="W E N S") 43 | info.grid(column=1, row=0, padx=8, pady=8, sticky="W E N S") 44 | installed.grid(column=2, row=0, padx=8, pady=8, sticky="W E N S") 45 | 46 | # Configure stretch ratios 47 | self.columnconfigure(0, weight=1) 48 | self.columnconfigure(1, weight=1) 49 | self.columnconfigure(2, weight=1) 50 | self.rowconfigure(0, weight=1) 51 | 52 | class Find(ttk.LabelFrame): 53 | """A panel with tools to find modules on GitHub.""" 54 | 55 | def __init__(self, parent): 56 | """Create the frame. 57 | 58 | Args: 59 | parent: A tk or ttk object. 60 | """ 61 | 62 | super(Frame.Find, self).__init__(parent, padding=8, text="Find new modules") 63 | 64 | # Variables 65 | self.repo = tk.StringVar(value="Enter or select a GitHub user/organisation") 66 | 67 | # Add elements 68 | self.name_entry = ttk.Combobox(self, textvariable=self.repo, values=["modisworks", "infraxion"]) 69 | self.scan_button = ttk.Button(self, command=self.scan, text="Scan user/org") 70 | self.loadingbar = ttk.Progressbar(self) 71 | 72 | self.panel_listing = self.PanelListing(self) 73 | 74 | self.install_button = ttk.Button(self, command=self.install, text="Install") 75 | 76 | # Grid elements 77 | self.name_entry.grid(column=0, row=0, padx=4, pady=4, sticky="W E N") 78 | self.scan_button.grid(column=1, row=0, padx=4, pady=4, sticky="E N") 79 | self.loadingbar.grid(column=0, columnspan=2, row=1, padx=4, pady=4, sticky="W E") 80 | 81 | self.panel_listing.grid(column=0, columnspan=2, row=2, padx=4, pady=4, sticky="W E N S") 82 | 83 | self.install_button.grid(column=1, row=3, padx=4, pady=4, sticky="E S") 84 | 85 | # Configure stretch ratios 86 | self.columnconfigure(0, weight=1) 87 | self.columnconfigure(1, weight=0) 88 | self.rowconfigure(0, weight=0) 89 | self.rowconfigure(1, weight=0) 90 | self.rowconfigure(2, weight=1) 91 | self.rowconfigure(3, weight=0) 92 | 93 | def scan(self): 94 | """Initiate a GitHub search for the entered user/organisation's repositories.""" 95 | self.loadingbar.start(50) 96 | 97 | self.name_entry.state(statespec=["disabled"]) 98 | self.scan_button.state(statespec=["disabled"]) 99 | self.panel_listing.grid_remove() 100 | 101 | repo_scanner = self.RepoScanner(self.name_entry.get(), self.scan_done) 102 | repo_scanner.start() 103 | 104 | def scan_done(self, repos): 105 | """Pass found repos to the repo listing panel. 106 | 107 | Args: 108 | repos (list): Repos to give to the listing panel. 109 | """ 110 | self.panel_listing.tree.delete(*self.panel_listing.tree.get_children()) 111 | for repo in repos: 112 | self.panel_listing.tree.insert("", "end", text=repo.name) 113 | 114 | self.panel_listing.grid() 115 | 116 | self.loadingbar.stop() 117 | self.loadingbar.configure(value=100) 118 | self.scan_button.state(statespec=["!disabled"]) 119 | self.name_entry.state(statespec=["!disabled"]) 120 | 121 | def install(self): 122 | """Install the selected module.""" 123 | pass 124 | 125 | class PanelListing(ttk.Frame): 126 | """Panel listing found repositories for the selected user/organisation.""" 127 | 128 | def __init__(self, parent): 129 | """Create the frame. 130 | 131 | Args: 132 | parent: A tk or ttk object. 133 | """ 134 | 135 | super(Frame.Find.PanelListing, self).__init__(parent) 136 | 137 | # Add elements 138 | self.tree = ttk.Treeview(self, show="tree") 139 | self.tree.column("#0", width=100) 140 | yscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview) 141 | self.tree['yscrollcommand'] = yscrollbar.set 142 | 143 | # Grid elements 144 | self.tree.grid(column=0, row=0, padx=(4, 0), pady=4, sticky="W E N S") 145 | yscrollbar.grid(column=1, row=0, padx=(0, 4), pady=4, sticky="N S") 146 | 147 | # Configure stretch ratios 148 | self.columnconfigure(0, weight=1) 149 | self.columnconfigure(1, weight=0) 150 | self.rowconfigure(0, weight=1) 151 | 152 | class PanelLoading(ttk.Frame): 153 | """Panel shown when fetching repos from GitHub""" 154 | 155 | def __init__(self, parent): 156 | """Create the frame. 157 | 158 | Args: 159 | parent: A tk or ttk object. 160 | """ 161 | 162 | super(Frame.Find.PanelLoading, self).__init__(parent) 163 | 164 | # Add elements 165 | self.spinner = ttk.Progressbar(self) 166 | 167 | # Grid elements 168 | self.spinner.grid(column=0, row=1, padx=4, pady=4, sticky="W E N S") 169 | 170 | # Configure stretch ratios 171 | self.columnconfigure(0, weight=1) 172 | self.rowconfigure(0, weight=1) 173 | self.rowconfigure(1, weight=0) 174 | self.rowconfigure(2, weight=1) 175 | 176 | class PanelText(ttk.Frame): 177 | """Panel shown when """ 178 | 179 | class RepoScanner(threading.Thread): 180 | """Find module repos belonging to the specified user/organisation.""" 181 | 182 | def __init__(self, query, after): 183 | """Create the thread. 184 | 185 | Args: 186 | query (str): The user or organisation to search for modules in. 187 | after (func): The function to call after completing the search. 188 | """ 189 | 190 | threading.Thread.__init__(self) 191 | self.query = query 192 | self.after = after 193 | 194 | def run(self): 195 | """Start the thread.""" 196 | 197 | try: 198 | repos = githubapi.get_user(self.query).get_repos() 199 | except github.UnknownObjectException: 200 | # TODO Bad user indicator 201 | # self.bad_user() 202 | print("Bad user") 203 | except github.RateLimitExceededException: 204 | # TODO Rate limit indicator 205 | # self.rate_limit 206 | print("Slow down!") 207 | else: 208 | self.after(repos) 209 | 210 | class Info(ttk.LabelFrame): 211 | """A panel displaying info about the currently selected module.""" 212 | 213 | def __init__(self, parent): 214 | """Create the frame. 215 | 216 | Args: 217 | parent: A tk or ttk object. 218 | """ 219 | 220 | super(Frame.Info, self).__init__(parent, padding=8, text="Module info") 221 | 222 | class Installed(ttk.LabelFrame): 223 | """A panel displaying a list of installed modules along with management controls.""" 224 | 225 | def __init__(self, parent): 226 | """Create the frame. 227 | 228 | Args: 229 | parent: A tk or ttk object. 230 | """ 231 | 232 | super(Frame.Installed, self).__init__(parent, padding=8, text="Installed modules") 233 | -------------------------------------------------------------------------------- /modis/gui/tabs/modules.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import tkinter as tk 3 | import tkinter.ttk as ttk 4 | 5 | from github import Github 6 | 7 | from modis.tools import help, moduledb 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | github = Github("9dcdf9fc8a323cc12a8c3f511b09d2c855a3d7d7") 12 | 13 | 14 | class Frame(ttk.Frame): 15 | """A tab containing tools to install and manage modules""" 16 | 17 | def __init__(self, parent): 18 | """Create the frame. 19 | 20 | Args: 21 | parent: A tk or ttk object. 22 | """ 23 | 24 | super(Frame, self).__init__(parent, padding=0) 25 | 26 | # Variables 27 | self.moduledb = {} 28 | self.module_ui_cache = {} 29 | 30 | # Add elements 31 | self.module_list = ttk.Treeview(self, show="tree") 32 | self.module_list.bind("<>", self.module_select) 33 | self.module_list.column("#0", width=128) 34 | yscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.module_list.yview) 35 | self.module_list['yscrollcommand'] = yscrollbar.set 36 | 37 | self.module_ui_container = ttk.Frame(self) 38 | self.module_ui_container.columnconfigure(0, weight=1) 39 | self.module_ui_container.rowconfigure(0, weight=1) 40 | 41 | # Grid elements 42 | self.module_list.grid(column=0, row=0, padx=0, pady=0, sticky="W N S") 43 | yscrollbar.grid(column=1, row=0, padx=0, pady=0, sticky="W N S") 44 | self.module_ui_container.grid(column=2, row=0, padx=8, pady=8, sticky="W E N S") 45 | 46 | # Configure stretch ratios 47 | self.columnconfigure(0, weight=0) 48 | self.columnconfigure(1, weight=0) 49 | self.columnconfigure(2, weight=1) 50 | self.rowconfigure(0, weight=1) 51 | 52 | # Import module UIs 53 | self.moduledb = moduledb.get_imports(["__ui"]) 54 | 55 | for module_name in self.moduledb.keys(): 56 | # Register module into module list 57 | self.module_list.insert('', 'end', module_name, text=module_name) 58 | 59 | # Create module frame 60 | self.module_ui_cache[module_name] = ttk.Frame(self.module_ui_container) 61 | self.module_ui_cache[module_name].grid(row=0, column=0, sticky="W E N S") 62 | self.module_ui_cache[module_name].lower() 63 | 64 | # Scan module database for module ui 65 | if "__ui" in self.moduledb[module_name].keys(): 66 | logger.debug("Module UI found for " + module_name) 67 | 68 | # Add elements 69 | module_ui = self.moduledb[module_name]["__ui"].ModuleUIFrame(self.module_ui_cache[module_name]) 70 | 71 | # Grid elements 72 | module_ui.grid(row=0, column=0, sticky="W E N S") 73 | 74 | # Add elements 75 | nav = ttk.Notebook(self.module_ui_cache[module_name]) 76 | help_frame = self.HelpFrame(nav, module_name) 77 | log_frame = self.LogFrame(nav, module_name) 78 | nav.add(help_frame, text="Help") 79 | nav.add(log_frame, text="Log") 80 | 81 | # Grid elements 82 | nav.grid(row=1, column=0, padx=8, pady=8, sticky="W E N S") 83 | 84 | # Configure stretch ratios 85 | self.module_ui_cache[module_name].columnconfigure(0, weight=1) 86 | self.module_ui_cache[module_name].rowconfigure(0, weight=0) 87 | self.module_ui_cache[module_name].rowconfigure(1, weight=1) 88 | 89 | def module_select(self, event): 90 | selected = self.module_list.focus() 91 | self.module_ui_cache[selected].lift() 92 | 93 | class HelpFrame(ttk.Frame): 94 | def __init__(self, parent, module_name): 95 | 96 | super(Frame.HelpFrame, self).__init__(parent, padding=8) 97 | 98 | # Add elements 99 | help_panel = tk.Text(self) 100 | 101 | help_panel.configure(background="#202020", wrap="word") 102 | help_panel.tag_config("title", font="TkHeadingFont 20 bold", justify="center", foreground="#AAAAAA") 103 | help_panel.tag_config("heading", font="TkHeadingFont 14 bold italic", foreground="#AAAAAA") 104 | help_panel.tag_config("command", font="TkFixedFont", foreground="#FFAA00") 105 | help_panel.tag_config("param", font="TkFixedFont", foreground="#00AAFF") 106 | help_panel.tag_config("description", font="TkTextFont", foreground="#AAAAAA") 107 | 108 | yscrollbar = ttk.Scrollbar(self, orient="vertical", command=help_panel.yview) 109 | xscrollbar = ttk.Scrollbar(self, orient="horizontal", command=help_panel.xview) 110 | help_panel['yscrollcommand'] = yscrollbar.set 111 | help_panel['xscrollcommand'] = xscrollbar.set 112 | 113 | # Grid elements 114 | help_panel.grid(column=0, row=0, sticky="W E N S") 115 | yscrollbar.grid(column=1, row=0, sticky="N S") 116 | xscrollbar.grid(column=0, row=1, sticky="W E") 117 | 118 | # Configure stretch ratios 119 | self.columnconfigure(0, weight=1) 120 | self.columnconfigure(1, weight=0) 121 | self.rowconfigure(0, weight=1) 122 | self.rowconfigure(1, weight=0) 123 | 124 | help_contents = help.get_raw(module_name) 125 | help_panel.insert("end", "\n{}\n".format(module_name), "title") 126 | for d in help_contents: 127 | help_panel.insert("end", "\n\n {}\n\n".format(d), "heading") 128 | 129 | if "commands" not in d.lower(): 130 | help_panel.insert("end", help_contents[d] + "\n\n", "desc") 131 | continue 132 | 133 | for c in help_contents[d]: 134 | if "name" not in c: 135 | continue 136 | 137 | command = "!" + c["name"] 138 | help_panel.insert("end", command, ("command", "desc")) 139 | if "params" in c: 140 | for param in c["params"]: 141 | help_panel.insert("end", " -" + param, ("param", "desc")) 142 | help_panel.insert('end', " - ", "description") 143 | if "description" in c: 144 | help_panel.insert("end", c["description"], "description") 145 | 146 | help_panel.insert("end", "\n") 147 | 148 | help_panel.config(state=tk.DISABLED) 149 | 150 | class LogFrame(ttk.Frame): 151 | """The text box showing the logging output""" 152 | 153 | def __init__(self, parent, module_name): 154 | """Create the frame. 155 | 156 | Args: 157 | parent: A tk or ttk object. 158 | """ 159 | 160 | super(Frame.LogFrame, self).__init__(parent, padding=8) 161 | 162 | # Add elements 163 | log_panel = tk.Text(self, wrap="none") 164 | 165 | formatter = logging.Formatter( 166 | "{levelname:8} {name} - {message}", style="{") 167 | handler = self.PanelHandler(log_panel) 168 | handler.setFormatter(formatter) 169 | 170 | root_logger = logging.getLogger("modis.modules." + module_name) 171 | root_logger.addHandler(handler) 172 | 173 | log_panel.configure(background="#202020") 174 | log_panel.tag_config('CRITICAL', foreground="#FF00AA") 175 | log_panel.tag_config('ERROR', foreground="#FFAA00") 176 | log_panel.tag_config('WARNING', foreground="#00AAFF") 177 | log_panel.tag_config('INFO', foreground="#AAAAAA") 178 | log_panel.tag_config('DEBUG', foreground="#444444") 179 | 180 | yscrollbar = ttk.Scrollbar(self, orient="vertical", command=log_panel.yview) 181 | xscrollbar = ttk.Scrollbar(self, orient="horizontal", command=log_panel.xview) 182 | log_panel['yscrollcommand'] = yscrollbar.set 183 | log_panel['xscrollcommand'] = xscrollbar.set 184 | 185 | # Grid elements 186 | log_panel.grid(column=0, row=0, sticky="W E N S") 187 | yscrollbar.grid(column=1, row=0, sticky="N S") 188 | xscrollbar.grid(column=0, row=1, sticky="W E") 189 | 190 | # Configure stretch ratios 191 | self.columnconfigure(0, weight=1) 192 | self.columnconfigure(1, weight=0) 193 | self.rowconfigure(0, weight=1) 194 | self.rowconfigure(1, weight=0) 195 | 196 | class PanelHandler(logging.Handler): 197 | def __init__(self, text_widget): 198 | logging.Handler.__init__(self) 199 | 200 | self.text_widget = text_widget 201 | self.text_widget.config(state=tk.DISABLED) 202 | 203 | def emit(self, record): 204 | msg = self.format(record) 205 | msg_level = logging.Formatter("{levelname}", style="{").format(record) 206 | 207 | # Remove '.modis' from start of logs 208 | msg = msg[:9] + msg[23:] 209 | 210 | # Exceptions 211 | if msg_level.startswith("ERROR"): 212 | msg_level = "ERROR" 213 | 214 | self.text_widget.config(state=tk.NORMAL) 215 | self.text_widget.insert("end", msg + "\n", msg_level) 216 | self.text_widget.config(state=tk.DISABLED) 217 | self.text_widget.see("end") 218 | -------------------------------------------------------------------------------- /modis/gui/window.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import tkinter as tk 4 | import tkinter.ttk as ttk 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class RootFrame(ttk.Frame): 10 | """The main window frame for Modis.""" 11 | 12 | def __init__(self, parent): 13 | """Create the frame. 14 | 15 | Args: 16 | parent: A tk or ttk object. 17 | """ 18 | 19 | super(RootFrame, self).__init__(parent) 20 | 21 | # Define window close action 22 | def on_closing(): 23 | try: 24 | from modis import main 25 | if main.client and main.client.loop: 26 | asyncio.run_coroutine_threadsafe(main.client.logout(), main.client.loop) 27 | except AttributeError: 28 | # Client no longer exists 29 | pass 30 | except RuntimeError: 31 | # TODO work out what causes this one 32 | pass 33 | except Exception as e: 34 | logger.exception(e) 35 | 36 | parent.destroy() 37 | import sys 38 | sys.exit(0) 39 | parent.protocol("WM_DELETE_WINDOW", on_closing) 40 | 41 | # Configure styles 42 | s = ttk.Style() 43 | s.configure( 44 | "modis1.TNotebook", 45 | tabmargins=[0, 0, -1, 0], 46 | tabposition="wn" 47 | ) 48 | s.configure( 49 | "modis1.TNotebook.Tab", 50 | padding=8, 51 | width=10 52 | ) 53 | s.map( 54 | "modis1.TNotebook.Tab", 55 | expand=[ 56 | ("selected", [0, 0, 1, 0]), 57 | ("active", [0, 0, 1, 0]) 58 | ] 59 | ) 60 | 61 | # Add elements 62 | statusbar = StatusBar(self) 63 | 64 | nav = ttk.Notebook(self, style="modis1.TNotebook") 65 | from modis.gui.tabs import core, download, modules, database 66 | nav.add(core.Frame(nav), text="Home") 67 | nav.add(download.Frame(nav), text="Download") 68 | nav.add(modules.Frame(nav), text="Modules") 69 | nav.add(database.Frame(nav), text="Database") 70 | 71 | # Grid elements 72 | statusbar.grid(column=0, row=1, sticky="W E S") 73 | nav.grid(column=0, row=0, sticky="W E N S") 74 | 75 | # Configure stretch ratios 76 | self.columnconfigure(0, weight=1) 77 | self.rowconfigure(0, weight=1) 78 | self.rowconfigure(1, weight=0) 79 | 80 | 81 | class StatusBar(ttk.Frame): 82 | """The status bar at the bottom of the UI.""" 83 | 84 | def __init__(self, parent): 85 | """reate the status bar. 86 | 87 | Args: 88 | parent: A tk or ttk object. 89 | """ 90 | 91 | super(StatusBar, self).__init__(parent) 92 | 93 | # Add elements 94 | self.status = tk.StringVar() 95 | self.statusbar = ttk.Label(self, textvariable=self.status, padding=2, anchor="center") 96 | statuslog = logging.getLogger("globalstatus") 97 | statuslog.setLevel("INFO") 98 | statushandler = self.StatusLogHandler(self.statusbar, self.status) 99 | statuslog.addHandler(statushandler) 100 | 101 | # Grid elements 102 | self.statusbar.grid(column=0, row=0, sticky="W E") 103 | 104 | # Configure stretch ratios 105 | self.columnconfigure(0, weight=1) 106 | 107 | # Set default status 108 | statuslog.info("0") 109 | 110 | class StatusLogHandler(logging.Handler): 111 | def __init__(self, statusbar, stringvar): 112 | """Update the global status via a log handler 113 | 114 | Args: 115 | statusbar (ttk.Label): The statusbar to manage. 116 | stringvar (tk.StringVar): The status text variable. 117 | """ 118 | 119 | logging.Handler.__init__(self) 120 | 121 | self.statusbar = statusbar 122 | self.stringvar = stringvar 123 | self.text = "" 124 | self.colour = "#FFFFFF" 125 | 126 | def emit(self, record): 127 | record = self.format(record) 128 | if record == "0": 129 | self.text = "OFFLINE" 130 | self.colour = "#FFAA00" 131 | elif record == "1": 132 | self.text = "STARTING" 133 | self.colour = "#00AAFF" 134 | elif record == "2": 135 | self.text = "ONLINE" 136 | self.colour = "#AAFF00" 137 | elif record == "3": 138 | self.text = "ERROR" 139 | self.colour = "#FF00AA" 140 | 141 | self.stringvar.set(self.text) 142 | self.statusbar.config(background=self.colour) 143 | -------------------------------------------------------------------------------- /modis/main.py: -------------------------------------------------------------------------------- 1 | """MAIN.PY - THE HUB 2 | 3 | This is the hub from which Modis runs all its modules. 4 | 5 | start() is called by __init__.py if Modis is running in command line, or by the start button in the GUI if Modis is running in GUI. 6 | 7 | start() will then import all the event handlers of all the modules, and use _eh_create() to compile all event handlers of one type into one function. After that's done, it will add those compiled event handlers to the `client` object, and then log in to Discord. 8 | 9 | That's it! Whenever an event is triggered on the client object, it will now be sent to all modules that have an event handler for that specific event. 10 | """ 11 | 12 | import logging 13 | import asyncio 14 | import discord 15 | 16 | logger = logging.getLogger(__name__) 17 | statuslog = logging.getLogger("globalstatus") 18 | 19 | 20 | # Create the client object 21 | client = discord.Client 22 | """In Discord.py 1.0+, the client object is used for global Discord stuff, like listing guilds you're part of etc. 23 | To use it, do `from modis import main`, and then use `main.client`. 24 | """ 25 | 26 | 27 | def start(loop: asyncio.AbstractEventLoop) -> None: 28 | """Starts the Discord client and logs Modis into Discord. 29 | 30 | Args: 31 | loop (asyncio.AbstractEventLoop): An asyncio event loop for the bot to run on. 32 | """ 33 | 34 | logger.info("Loading Modis...") 35 | 36 | from modis.tools import config, data, moduledb, version 37 | 38 | # Update data.json cache 39 | data.pull() 40 | 41 | # Check the current version 42 | # TODO implement version check and display 43 | # logger.info(version.infostr()) 44 | 45 | # Create client 46 | logger.debug("Creating Discord client") 47 | asyncio.set_event_loop(loop) 48 | intents = discord.Intents.default() 49 | intents.message_content = True 50 | global client 51 | client = discord.Client(intents=intents) 52 | 53 | # Import event handlers 54 | logger.info("Importing modules...") 55 | eh_lib = moduledb.get_imports(config.EH_TYPES) 56 | 57 | # Register event handlers 58 | logger.debug("Registering event handlers") 59 | for eh_type in config.EH_TYPES: 60 | eh_list = [] 61 | for module_name in eh_lib.keys(): 62 | if eh_type in eh_lib[module_name].keys(): 63 | eh_list.append(eh_lib[module_name][eh_type]) 64 | if eh_list: 65 | client.event(_eh_create(eh_type, eh_list)) 66 | 67 | # CONNECTION STACK 68 | logger.info("Logging in...") 69 | 70 | async def client_start(): 71 | async with client: 72 | await client.start(token) 73 | 74 | async def client_close(): 75 | async with client: 76 | await client.loop.close() 77 | 78 | async def client_run_until_complete(): 79 | async with client: 80 | await client.loop.run_until_complete(client.connect(reconnect=True)) 81 | 82 | try: 83 | # Attempt login 84 | token = data.cache["keys"]["discord_token"] 85 | asyncio.run(client_start()) 86 | # client.loop.run_until_complete(client.login(token)) 87 | except Exception as e: 88 | # Login failed 89 | logger.critical("Login failed") 90 | logger.exception(e) 91 | statuslog.info("3") 92 | asyncio.run(client_close()) 93 | else: 94 | # Login successful 95 | logger.info("Connecting...") 96 | asyncio.run(client_run_until_complete()) 97 | 98 | 99 | def _eh_create(eh_type: str, eh_list: list) -> staticmethod: 100 | """Creates a compiled event handler 101 | 102 | Creates a function that combines all the event handlers of a specific type into one. 103 | 104 | Args: 105 | eh_type (str): The event handler type to be bundled. 106 | eh_list (list): The library of event handlers to pull from. 107 | 108 | Returns: 109 | func (staticmethod) A combined event handler function consisting of all the event handlers in its category. 110 | """ 111 | 112 | # Create the combiner function 113 | async def func(*args, **kwargs) -> None: 114 | """A function that executes a specific event handler in all modules""" 115 | for eh in eh_list: 116 | try: 117 | module_event_handler_func = getattr(eh, eh_type) 118 | await module_event_handler_func(*args, **kwargs) 119 | except Exception as e: 120 | logger.warning("An uncaught error occured in " + eh.__name__) 121 | logger.exception(e) 122 | 123 | # Rename the combiner function to the name of the event handler 124 | func.__name__ = eh_type 125 | 126 | return func 127 | -------------------------------------------------------------------------------- /modis/modules/!core/__info.py: -------------------------------------------------------------------------------- 1 | NAME = "core" 2 | CONTRIBUTORS = { 3 | "@YtnomSnrub": "Original module", 4 | "@Infraxion": "Commands and permissions API" 5 | } 6 | BLURB = "Manages all the behind the scenes stuff that runs the internal APIs the modules use." 7 | 8 | COMMANDS = {} 9 | DATA_GUILD = {} 10 | DATA_GLOBAL = {} 11 | 12 | HELP_DATAPACKS = {} 13 | HELP_MARKDOWN = """""" 14 | -------------------------------------------------------------------------------- /modis/modules/!core/_data.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | cmd_db = {} 4 | 5 | perm_db = { 6 | "administrator": discord.Permissions(8), 7 | "view_audit_logs": discord.Permissions(128), 8 | "manage_server": discord.Permissions(32), 9 | "manage_roles": discord.Permissions(268435456), 10 | "manage_channels": discord.Permissions(16), 11 | "kick_members": discord.Permissions(2), 12 | "ban_members": discord.Permissions(4), 13 | "create_instant_invite": discord.Permissions(1), 14 | "change_nickname": discord.Permissions(67108864), 15 | "manage_nicknames": discord.Permissions(134217728), 16 | "manage_emojis": discord.Permissions(1073741824), 17 | "manage_webhooks": discord.Permissions(536870912), 18 | "view_channels": discord.Permissions(1024), 19 | "send_messages": discord.Permissions(2048), 20 | "seng_tts_messages": discord.Permissions(4096), 21 | "manage_messages": discord.Permissions(8192), 22 | "embed_links": discord.Permissions(16384), 23 | "attach_files": discord.Permissions(32768), 24 | "read_message_history": discord.Permissions(65536), 25 | "mention_everyone": discord.Permissions(131072), 26 | "use_external_emojis": discord.Permissions(262144), 27 | "add_reactions": discord.Permissions(64), 28 | "connect": discord.Permissions(1048576), 29 | "speak": discord.Permissions(2097152), 30 | "mute_members": discord.Permissions(4194304), 31 | "deafen_members": discord.Permissions(8388608), 32 | "use_members": discord.Permissions(16777216), 33 | "use_voice_activation": discord.Permissions(33554432), 34 | "priority_speaker": discord.Permissions(256) 35 | } 36 | -------------------------------------------------------------------------------- /modis/modules/!core/api_core.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from modis import main 4 | from modis.tools import data, config, moduledb 5 | from . import _data 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def guild_update(guild_id): 11 | """Updates a guild's info in the database. 12 | 13 | Args: 14 | guild_id (int): The guild to update. 15 | """ 16 | 17 | logger.debug("Updating guild {}".format(guild_id)) 18 | 19 | # Add the guild to database if it doesn't yet exist 20 | if str(guild_id) not in data.cache["guilds"]: 21 | logger.debug("Adding guild {} to database".format(guild_id)) 22 | data.cache["guilds"][str(guild_id)] = config.GUILD_TEMPLATE 23 | 24 | # Register slots for per-guild module specific data 25 | module_names = moduledb.get_names() 26 | for module_name in module_names: 27 | info = moduledb.get_import_specific("__info", module_name) 28 | try: 29 | if info.DATA_GUILD: 30 | data.cache["guilds"][str(guild_id)]["modules"][module_name] = info.DATA_GUILD 31 | except AttributeError: 32 | logger.debug("Guild data slot not requested for " + module_name) 33 | 34 | print(data) 35 | 36 | data.push() 37 | 38 | 39 | def guild_remove(guild_id): 40 | """Removes a guild from the database. 41 | 42 | Args: 43 | guild_id (int): The guild to remove. 44 | """ 45 | 46 | logger.debug("Removing guild {} from database".format(guild_id)) 47 | 48 | try: 49 | data.cache["guilds"].pop(str(guild_id)) 50 | except KeyError: 51 | logger.warning("Guild {} is nonexistent in database".format(guild_id)) 52 | else: 53 | data.push() 54 | 55 | 56 | def guild_clean(): 57 | """Removes from the database guilds that Modis is no longer part of.""" 58 | 59 | logger.debug("Cleaning guilds...") 60 | 61 | guilds_old = list(data.cache["guilds"].keys()) 62 | guilds_new = [str(guild.id) for guild in main.client.guilds] 63 | 64 | for guild_id in guilds_old: 65 | if guild_id not in guilds_new: 66 | guild_remove(guild_id) 67 | 68 | for guild_id in guilds_new: 69 | if guild_id not in guilds_old: 70 | guild_update(int(guild_id)) 71 | 72 | 73 | def cmd_db_update(): 74 | """Updates the command database""" 75 | 76 | logger.debug("Updating command database") 77 | 78 | # Retrive module "header" files and functions 79 | cmd_db = moduledb.get_imports(["__info", "on_command"]) 80 | 81 | for module_name in cmd_db.keys(): 82 | _data.cmd_db[module_name] = {} 83 | 84 | if "on_command" in cmd_db[module_name].keys(): 85 | # Enter module functions into database 86 | _data.cmd_db[module_name]["eh"] = cmd_db[module_name]["on_command"].on_command 87 | 88 | if "__info" in cmd_db[module_name].keys(): 89 | # Enter module "header" file into database 90 | _data.cmd_db[module_name]["cmd"] = cmd_db[module_name]["__info"].COMMANDS 91 | # TODO also add to static database, implement checks to see if its already in there 92 | -------------------------------------------------------------------------------- /modis/modules/!core/on_guild_available.py: -------------------------------------------------------------------------------- 1 | from . import api_core 2 | 3 | 4 | async def on_guild_available(guild): 5 | api_core.guild_update(guild.id) 6 | -------------------------------------------------------------------------------- /modis/modules/!core/on_guild_join.py: -------------------------------------------------------------------------------- 1 | from . import api_core 2 | 3 | 4 | async def on_guild_join(guild): 5 | api_core.guild_update(guild.id) 6 | -------------------------------------------------------------------------------- /modis/modules/!core/on_guild_remove.py: -------------------------------------------------------------------------------- 1 | from . import api_core 2 | 3 | 4 | async def on_guild_remove(guild): 5 | api_core.guild_remove(guild.id) 6 | -------------------------------------------------------------------------------- /modis/modules/!core/on_message.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from modis import main 4 | from modis.tools import data 5 | 6 | from . import _data 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | async def on_message(msgobj): 12 | """Parses commands into arrays 13 | 14 | Args: 15 | msgobj: (discord.Message): Input message 16 | """ 17 | 18 | # Don't reply to myself 19 | if msgobj.author == main.client.user: 20 | return 21 | 22 | # Check prefix 23 | prefix = data.cache["guilds"][str(msgobj.guild.id)]["prefix"] 24 | if not msgobj.content.startswith(prefix): 25 | return 26 | 27 | # Parse msgobj 28 | package = msgobj.content.split(" ") 29 | root = package.pop(0)[len(prefix):] 30 | aux = [] 31 | while len(package) > 0: 32 | if not package[0].startswith("-"): 33 | break 34 | aux.append(package.pop(0)[1:]) 35 | query = " ".join(package) 36 | 37 | # Process command 38 | for module_name in _data.cmd_db.keys(): 39 | # Check for commands existing for this module 40 | if "cmd" not in _data.cmd_db[module_name]: 41 | continue 42 | 43 | # Check for this command in list of commands for this module 44 | if root not in _data.cmd_db[module_name]["cmd"].keys(): 45 | continue 46 | 47 | # Check for this command having permissions defined 48 | if "level" not in _data.cmd_db[module_name]["cmd"][root].keys(): 49 | await _data.cmd_db[module_name]["eh"](root, aux, query, msgobj) 50 | continue 51 | 52 | await _data.cmd_db[module_name]["eh"](root, aux, query, msgobj) 53 | continue 54 | # TODO ^^^ why is this suddenly needed now??? 55 | 56 | # Check permissions 57 | level = _data.cmd_db[module_name]["cmd"][root]["level"] 58 | 59 | if isinstance(level, int): 60 | # Permission is specified as role ranking 61 | if msgobj.author.guild.owner == msgobj.author: 62 | role = 0 63 | else: 64 | role = len(msgobj.guild.roles) - msgobj.author.top_role.position 65 | # Highest role = 1, guild owner = 0, everyone = -1 66 | 67 | if level < 0 or role <= level: 68 | await _data.cmd_db[module_name]["eh"](root, aux, query, msgobj) 69 | else: 70 | # TODO permission notification gui 71 | continue 72 | 73 | elif isinstance(level, str): 74 | # Permission is specified as specific Discord permission 75 | if _data.perm_db[level] <= msgobj.channel.permissions_for(msgobj.author): 76 | await _data.cmd_db[module_name]["eh"](root, aux, query, msgobj) 77 | else: 78 | # TODO permission notification gui 79 | continue 80 | 81 | else: 82 | # TODO bad perm definition handling 83 | continue 84 | -------------------------------------------------------------------------------- /modis/modules/!core/on_ready.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from . import api_core 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | async def on_ready(): 9 | logger.info("Updating...") 10 | 11 | api_core.guild_clean() 12 | api_core.cmd_db_update() 13 | 14 | logger.info("Modis is ready") 15 | statuslog = logging.getLogger("globalstatus") 16 | statuslog.info("2") 17 | -------------------------------------------------------------------------------- /modis/tools/api.py: -------------------------------------------------------------------------------- 1 | """This tool provides easy access to various external APIs, such as Google API, GitHub API etc.""" 2 | 3 | import logging 4 | 5 | import googleapiclient.discovery 6 | import soundcloud 7 | import spotipy 8 | from spotipy.oauth2 import SpotifyClientCredentials 9 | 10 | from modis.tools import data 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def build_youtube(): 16 | """Builds the YouTube API. 17 | 18 | Returns: 19 | built_api (googleapiclient.discovery.Resource): Built YouTube API if successful, False if not. 20 | """ 21 | 22 | logger.debug("Building YouTube API") 23 | 24 | if "google_api_key" not in data.cache["keys"]: 25 | logger.warning("No API key found with name 'google_api_key'. Please enter your Google API key in the music module console to use YouTube for music.") 26 | return False 27 | 28 | try: 29 | ytdevkey = data.cache["keys"]["google_api_key"] 30 | built_api = googleapiclient.discovery.build("youtube", "v3", developerKey=ytdevkey) 31 | logger.debug("YouTube API build successful") 32 | return built_api 33 | except Exception as e: 34 | logger.exception(e) 35 | logger.error("Error building YouTube API; YouTube won't be available") 36 | return False 37 | 38 | 39 | def build_soundcloud(): 40 | """Builds the SoundCloud API. 41 | 42 | Returns: 43 | built_api (soundcloud.Client): Built SoundCloud API if successful, False if not. 44 | """ 45 | 46 | logger.debug("Building SoundCloud API") 47 | 48 | if "soundcloud_client_id" not in data.cache["keys"]: 49 | logger.warning("No API key found with name 'soundcloud_client_id'. Please enter your SoundCloud client id in the music module console to use SoundCloud for music.") 50 | return False 51 | 52 | try: 53 | built_api = soundcloud.Client(client_id=data.cache["keys"]["soundcloud_client_id"]) 54 | logger.debug("SoundCloud API build successful") 55 | return built_api 56 | except Exception as e: 57 | logger.exception(e) 58 | logger.error("Error building SoundCloud API; SoundCloud won't be available") 59 | return False 60 | 61 | 62 | def build_spotify(): 63 | """Builds the Spotify API. 64 | 65 | Returns: 66 | built_api (spotify.Spotify): Built Spotify API if successful, False if not. 67 | """ 68 | 69 | logger.debug("Building Spotify API") 70 | 71 | if "spotify_client_id" not in data.cache["keys"]: 72 | logger.warning("No API key found with name 'spotify_client_id'. Please enter your Spotify client id in the music module console to use Spotify for music.") 73 | return False 74 | if "spotify_client_secret" not in data.cache["keys"]: 75 | logger.warning("No API key found with name 'spotify_client_secret'. Please enter your Spotify client secret in the music module console to use Spotify for music.") 76 | return False 77 | 78 | try: 79 | client_credentials_manager = SpotifyClientCredentials( 80 | data.cache["keys"]["spotify_client_id"], 81 | data.cache["keys"]["spotify_client_secret"]) 82 | built_api = spotipy.Spotify(client_credentials_manager=client_credentials_manager) 83 | logger.debug("Spotify API build successful") 84 | return built_api 85 | except Exception as e: 86 | logger.exception(e) 87 | logger.error("Error building Spotify API; Spotify won't be available") 88 | return False 89 | 90 | 91 | client_youtube = build_youtube() 92 | client_soundcloud = build_soundcloud() 93 | client_spotify = build_spotify() 94 | -------------------------------------------------------------------------------- /modis/tools/config.py: -------------------------------------------------------------------------------- 1 | """THE CONFIGURATION FILE 2 | 3 | This file can be thought of as a read-only save file, as opposed to the read-and-write save file that is data.json. This file makes it easy to store global variables for all modules and framework to access. 4 | """ 5 | 6 | import os as _os 7 | 8 | # About 9 | VERSION = "0.4.0" 10 | NICKNAME = "CHOPIN" 11 | 12 | # Directory 13 | ROOT_DIR = _os.path.dirname(_os.path.dirname(_os.path.realpath(__file__))) 14 | WORK_DIR = _os.getcwd() 15 | MODULES_DIR = ROOT_DIR + "/modules" 16 | LOGS_DIR = WORK_DIR + "/logs" 17 | 18 | # Discord 19 | EH_TYPES = [ 20 | "on_connect", 21 | "on_disconnect", 22 | "on_ready", 23 | "on_shard_ready", 24 | "on_resumed", 25 | "on_error", 26 | "on_socket_raw_receive", 27 | "on_socket_raw_send", 28 | "on_typing", 29 | "on_message", 30 | "on_message_delete", 31 | "on_bulk_message_delete", 32 | "on_raw_message_delete", 33 | "on_raw_bulk_message_delete", 34 | "on_message_edit", 35 | "on_raw_message_edit", 36 | "on_reaction_add", 37 | "on_raw_reaction_add", 38 | "on_reaction_remove", 39 | "on_raw_reaction_remove", 40 | "on_reaction_clear", 41 | "on_raw_reaction_clear", 42 | "on_private_channel_delete", 43 | "on_private_channel_create", 44 | "on_private_channel_update", 45 | "on_private_channel_pins_update", 46 | "on_guild_channel_delete", 47 | "on_guild_channel_create", 48 | "on_guild_channel_update", 49 | "on_guild_channel_pins_update", 50 | "on_webhooks_update", 51 | "on_member_join", 52 | "on_member_remove", 53 | "on_member_update", 54 | "on_guild_join", 55 | "on_guild_remove", 56 | "on_guild_update", 57 | "on_guild_role_create", 58 | "on_guild_role_delete", 59 | "on_guild_role_update", 60 | "on_guild_emojis_update", 61 | "on_guild_available", 62 | "on_guild_unavailable", 63 | "on_voice_state_update", 64 | "on_member_ban", 65 | "on_member_unban", 66 | "on_group_join", 67 | "on_group_remove", 68 | "on_relationship_add", 69 | "on_relationship_remove", 70 | "on_relationship_update" 71 | ] 72 | 73 | # data.json 74 | DATAFILE = "{}/data.json".format(WORK_DIR) 75 | ROOT_TEMPLATE = { 76 | "log_level": "INFO", 77 | "keys": { 78 | "discord_token": "" 79 | }, 80 | "guilds": {} 81 | } 82 | GUILD_TEMPLATE = { 83 | "prefix": "!", 84 | "activation": {}, 85 | "commands": {}, 86 | "modules": {} 87 | } 88 | 89 | # Logging 90 | LOG_FORMAT = "{asctime} {levelname:8} {name} - {message}" 91 | -------------------------------------------------------------------------------- /modis/tools/data.py: -------------------------------------------------------------------------------- 1 | """THE DATABASE HANDLER 2 | 3 | This tool handles reading and editing of the data.json database. Modis uses the JSON protocol to store data to ensure easy readability and accessibility. The various functions in this file make it easy for modules to read and edit the database, and also provides an easy way to expand into more complex database technologies such as mongodb in the future. 4 | """ 5 | 6 | import typing 7 | import json 8 | import logging 9 | import os 10 | from collections import OrderedDict 11 | 12 | from modis.tools import config 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | # Create the cache object 17 | cache: typing.Dict[str, typing.Any] = {} 18 | # The cache dict is an exact copy of database.json, but stored in RAM. It makes it much easier for modules and framework to access the database without requiring disk reads and writes every time. 19 | 20 | # TODO Implement exception handling 21 | 22 | 23 | def get(guild: int, 24 | module: str, 25 | path: typing.List[str] = None) -> typing.Any: 26 | """Gets a database entry. 27 | 28 | Retreives a specific database entry belonging to a module. Under normal usage, this and `data.edit()` should be the only functions you need. 29 | 30 | Args: 31 | guild (int): Guild ID of the guild data to read. 32 | module (str): Module name of the module data to read. 33 | path (list): List of strings describing the path to the desired database entry. 34 | 35 | Returns: 36 | The retrieved database entry. 37 | """ 38 | 39 | global cache 40 | 41 | entry = cache["guilds"][str(guild)]["modules"][module] 42 | 43 | if not path: 44 | return entry 45 | 46 | for key in path: 47 | entry = entry[key] 48 | return entry 49 | 50 | 51 | def edit(guild: int, 52 | module: str, 53 | value, 54 | path: typing.List[str] = None) -> None: 55 | """Edits a database entry. 56 | 57 | Edits a specific database entry belonging to a module. Under normal usage, this and `data.get()` should be the only functions you need. 58 | 59 | Args: 60 | guild (int): Guild ID of the guild data to edit. 61 | module (str): Module name of the module data to edit. 62 | value: Value to change the database entry to. 63 | path (list): List of strings describing the path to the desired database entry. 64 | """ 65 | 66 | global cache 67 | 68 | if path: 69 | entry = cache["guilds"][str(guild)]["modules"] 70 | for key in path[:-1]: 71 | entry = entry[key] 72 | entry[path[-1]] = value 73 | else: 74 | cache["guilds"][guild]["modules"][module] = value 75 | 76 | push() 77 | 78 | 79 | def pull() -> None: 80 | """Updates cache from disk 81 | 82 | Updates the `cache` object with the current state of the data.json file. 83 | """ 84 | 85 | logger.debug("Pulling database from file") 86 | 87 | global cache 88 | 89 | if not os.path.exists(config.DATAFILE): 90 | # data.json does not exist 91 | logger.warning("data.json file not found. An empty one will be created.") 92 | _create(config.ROOT_TEMPLATE) 93 | return 94 | 95 | with open(config.DATAFILE, 'r') as file: 96 | try: 97 | cache = json.load(file) 98 | invalid_datafile = False 99 | except FileNotFoundError: 100 | invalid_datafile = True 101 | if invalid_datafile or not _validate(cache): 102 | # data.json is not a valid current Modis data file 103 | logger.warning("data.json file is outdated or invalid. A new one will be created and the old file will be renamed to data.json.old") 104 | 105 | # Don't overwrite existing files 106 | num = 1 107 | while os.path.exists(config.DATAFILE + ".old" + str(num)): 108 | num += 1 109 | os.rename(config.DATAFILE, config.DATAFILE + ".old" + str(num)) 110 | _create(config.ROOT_TEMPLATE) 111 | 112 | 113 | def push(new_data: dict = None) -> None: 114 | """Updates disk from cache 115 | 116 | Updates the data.json file with the current state of the `cache` object. 117 | 118 | Args: 119 | new_data (dict): A custom dict to set data.json to. This argument is DANGEROUS and you could easily destroy your data.json if you're not careful. 120 | """ 121 | 122 | logger.debug("Pushing database to file") 123 | 124 | global cache 125 | 126 | if not os.path.exists(config.DATAFILE): 127 | # data.json does not exist 128 | logger.warning("data.json file not found. An empty one will be created.") 129 | _create(config.ROOT_TEMPLATE) 130 | return 131 | 132 | if new_data: 133 | cache = new_data 134 | 135 | with open(config.DATAFILE, 'w') as file: 136 | json.dump(_sort(cache), file, indent=2) 137 | 138 | 139 | def _create(_template: dict) -> None: 140 | """Creates a new data.json file. 141 | 142 | Creates a new data.json file from the template defined in modis.tools.config, or uses the `template` argument to create a custom data.json file. 143 | 144 | Args: 145 | _template (dict): The template dict to create data.json with. 146 | """ 147 | 148 | logger.info("Creating new data.json") 149 | 150 | global cache 151 | 152 | cache = _template 153 | 154 | with open(config.DATAFILE, 'w') as file: 155 | json.dump(cache, file, indent=2) 156 | 157 | 158 | def _validate(_template: dict) -> bool: 159 | """Validates a data.json dictionary 160 | 161 | Check if the dictionary `_template` is in the right format for a data.json file using a hard-coded method, that will be changed to use the template defined in modis.tools.config in the future. 162 | 163 | Args: 164 | _template (dict): The data dictionary to validate. 165 | 166 | Returns: 167 | Bool of whether or not the database is valid 168 | """ 169 | 170 | # TODO make this not hard-coded 171 | if "keys" not in _template: 172 | return False 173 | if "discord_token" not in _template["keys"]: 174 | return False 175 | return True 176 | 177 | 178 | def _sort(_dict: dict) -> OrderedDict: 179 | """Sorts a dictionary. 180 | 181 | Recursively sorts all elements in a dictionary to produce an OrderedDict. 182 | 183 | Args: 184 | _dict (dict): The dictionary to sort. 185 | 186 | Returns: 187 | An OrderedDict with its items sorted. 188 | """ 189 | 190 | newdict = {} 191 | 192 | for i in _dict.items(): 193 | if type(i[1]) is dict: 194 | newdict[i[0]] = _sort(i[1]) 195 | else: 196 | newdict[i[0]] = i[1] 197 | 198 | # TODO check if it should be _compare(type(item[1]), type(item[0])) 199 | return OrderedDict(sorted(newdict.items(), key=lambda item: (_compare(type(item[1])), item[0]))) 200 | 201 | 202 | def _compare(_type: type) -> int: 203 | """Defines a type order for a dictionary. 204 | 205 | Args: 206 | _type (type): The type to compare. 207 | 208 | Returns: 209 | An integer where 1 = dict/OrderedDict, and 0 = other 210 | """ 211 | 212 | if _type in [dict, OrderedDict]: 213 | return 1 214 | 215 | return 0 216 | -------------------------------------------------------------------------------- /modis/tools/embed.py: -------------------------------------------------------------------------------- 1 | """ 2 | This tool is Modis' embed API. It allows Modis modules to easily create 3 | fancy looking GUIs in the Discord client. 4 | """ 5 | 6 | import logging 7 | 8 | import discord 9 | 10 | from modis import main 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | URL = "https://musicbyango.com/modis/" 15 | ICON = "http://musicbyango.com/modis/dp/modis64t.png" 16 | # TODO Update to ModisWorks url 17 | 18 | 19 | class UI: 20 | """Enables easy management of Discord embeds.""" 21 | 22 | def __init__(self, channel, title, description, modulename="Modis", 23 | colour=0xAAFF00, thumbnail=None, image=None, datapacks=()): 24 | """Initialise variables and build the embed. 25 | 26 | Args: 27 | channel (discord.TextChannel): Channel to lock UI to 28 | title (str): GUI title, in bold 29 | description (str): GUI description 30 | modulename (str): Name of your module, default "Modis" 31 | colour (int): Colour of line on left, default 0xAAFF00 32 | thumbnail (str): URL to picture shown in top right corner, default None 33 | datapacks (list): Contains tuples of (title str, data str, inline bool) 34 | """ 35 | 36 | self.channel = channel 37 | self.title = title 38 | self.description = description 39 | self.modulename = modulename 40 | self.colour = colour 41 | self.thumbnail = thumbnail 42 | self.image = image 43 | self.datapacks = datapacks 44 | self.datapack_lines = {} 45 | 46 | self.built_embed = self.build() 47 | self.sent_embed = None 48 | 49 | def build(self): 50 | """Build the embed. 51 | 52 | Returns: 53 | discord.Embed: The built embed. 54 | """ 55 | 56 | embed = discord.Embed( 57 | title=self.title, 58 | type='rich', 59 | description=self.description, 60 | colour=self.colour) 61 | 62 | embed.set_author( 63 | name="Modis", 64 | url=URL, 65 | icon_url=ICON) 66 | 67 | if self.thumbnail: 68 | embed.set_thumbnail(url=self.thumbnail) 69 | 70 | if self.image: 71 | embed.set_image(url=self.image) 72 | 73 | self.datapack_lines = {} 74 | for pack in self.datapacks: 75 | embed.add_field(name=pack[0], value=pack[1], inline=pack[2]) 76 | self.datapack_lines[pack[0]] = pack 77 | 78 | return embed 79 | 80 | async def send(self): 81 | """Send the embed message.""" 82 | 83 | await self.channel.trigger_typing() 84 | self.sent_embed = await self.channel.send(embed=self.built_embed) 85 | 86 | async def usend(self): 87 | """Update the existing embed.""" 88 | 89 | try: 90 | await self.sent_embed.edit(embed=self.built_embed) 91 | except Exception as e: 92 | # TODO Add exceptions 93 | logger.exception(e) 94 | 95 | async def delete(self): 96 | """Delete the existing embed.""" 97 | 98 | try: 99 | await self.sent_embed.delete() 100 | except Exception as e: 101 | # TODO Add exceptions 102 | logger.exception(e) 103 | 104 | self.sent_embed = None 105 | 106 | def update_field(self, title, data): 107 | """Update a particular field's data. 108 | 109 | Args: 110 | title (str): The title of the field to update. 111 | data (str): The new value to set for this datapack. 112 | """ 113 | 114 | if title in self.datapack_lines: 115 | self.update_data(self.datapack_lines[title], data) 116 | else: 117 | logger.warning("No field with title '{}'".format(title)) 118 | 119 | def update_colour(self, new_colour): 120 | """Update the embed's colour. 121 | 122 | Args: 123 | new_colour (discord.Colour): The new colour for the embed. 124 | """ 125 | 126 | self.built_embed.colour = new_colour 127 | 128 | def update_data(self, index, data): 129 | """Update a particular datapack's data. 130 | 131 | Args: 132 | index (int): The index of the datapack. 133 | data (str): The new value to set for this datapack. 134 | """ 135 | 136 | datapack = self.built_embed.to_dict()["fields"][index] 137 | self.built_embed.set_field_at(index, name=datapack["name"], value=data, inline=datapack["inline"]) 138 | -------------------------------------------------------------------------------- /modis/tools/help.py: -------------------------------------------------------------------------------- 1 | """ 2 | This tool retrieves help data from the __info.py files in each module. 3 | """ 4 | 5 | import logging 6 | 7 | from modis.tools import moduledb 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def get_raw(module_name): 13 | """Get a dict from a __info.py. 14 | 15 | Args: 16 | module_name (str): The name of the module to get help for. 17 | 18 | Returns: 19 | data (OrderedDict): The dict of the help.json 20 | """ 21 | 22 | info = moduledb.get_import_specific("__info", module_name) 23 | 24 | if not info: 25 | # Info file does not exist in module 26 | return {} 27 | if not info.HELP_DATAPACKS: 28 | # Info file does not contain help data 29 | return {} 30 | return info.HELP_DATAPACKS 31 | 32 | 33 | def get_md(module_name, prefix="!"): 34 | """Load help text from a __info.py and format into markdown datapacks. 35 | 36 | Args: 37 | module_name (str): The name of the module to get help for. 38 | prefix (str): The prefix to use for commands. 39 | 40 | Returns: 41 | datapacks (list): The formatted data. 42 | """ 43 | 44 | info = moduledb.get_import_specific("__info", module_name) 45 | if not info: 46 | # Info file does not exist in module 47 | return [] 48 | elif not info.HELP_DATAPACKS: 49 | # Info file does not contain help data 50 | return [] 51 | help_contents = info.HELP_DATAPACKS 52 | 53 | # Format the content 54 | datapacks = [] 55 | for heading in help_contents.keys(): 56 | content = "" 57 | if "commands" not in heading.lower(): 58 | # Format as regular string 59 | content += help_contents[heading] 60 | else: 61 | # Format as command description 62 | for command in help_contents[heading]: 63 | if "name" not in command: 64 | # Entry is not a command 65 | continue 66 | 67 | content += "- `" + prefix + command["name"] 68 | if "params" in command: 69 | # Entry contains extra parameters 70 | for param in command["params"]: 71 | content += " -{}".format(param) 72 | content += "`: " 73 | 74 | if "description" in command: 75 | # Entry contains a command description 76 | content += command["description"] 77 | content += "\n" 78 | datapacks.append((heading, content, False)) 79 | 80 | return datapacks 81 | -------------------------------------------------------------------------------- /modis/tools/log.py: -------------------------------------------------------------------------------- 1 | """ 2 | This tool handles logging in Modis, making sure everything prints where it's 3 | meant to go (GUI log, console, etc), and handles Unicode encoding. 4 | 5 | Modis uses the logging package for logging. To create a new logger first import 6 | logging, then define a logger with "logger = logging.getLogger(__name__)". 7 | """ 8 | 9 | import os 10 | import time 11 | import logging 12 | import sys 13 | 14 | from modis.tools import config 15 | from modis.tools import data 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class UnicodeStreamHandler(logging.StreamHandler): 21 | """A logging handler that supports Unicode characters.""" 22 | 23 | def __init__(self, stream, stream_err): 24 | """Create a new Unicode stream handler. 25 | 26 | Args: 27 | stream: The stream to attatch the handler to. 28 | stream_err: The error stream to attatch the handler to. 29 | """ 30 | 31 | super(UnicodeStreamHandler, self).__init__(stream) 32 | 33 | if not stream_err: 34 | stream_err = sys.stderr 35 | self.stream_err = stream_err 36 | 37 | def emit(self, record): 38 | """Send a formatted record into the logger output 39 | 40 | Args: 41 | record: The record to emit. 42 | """ 43 | 44 | try: 45 | # Retrieve information from record 46 | msg = self.format(record) 47 | level = record.levelname 48 | 49 | # Set the stream based on the record's urgency level 50 | stream = self.stream 51 | if level in ["WARNING", "CRITICAL", "ERROR"]: 52 | stream = self.stream_err 53 | 54 | # Write to the stream 55 | try: 56 | stream.write(msg) 57 | except (UnicodeError, UnicodeEncodeError): 58 | stream.write(msg.encode("UTF-8")) 59 | 60 | # Exit 61 | stream.write(self.terminator) 62 | self.flush() 63 | except (KeyboardInterrupt, SystemExit): 64 | raise 65 | except: 66 | self.handleError(record) 67 | 68 | 69 | def init_print(target_logger): 70 | """Adds a print handler to a logger. 71 | 72 | Args: 73 | target_logger (logging.logger): The logger to add the print handler to. 74 | """ 75 | 76 | # Create logging directory 77 | if not os.path.isdir(config.LOGS_DIR): 78 | os.mkdir(config.LOGS_DIR) 79 | 80 | # Set log level 81 | if "log_level" not in data.cache: 82 | data.cache["log_level"] = "INFO" 83 | data.push() 84 | target_logger.setLevel(data.cache["log_level"]) 85 | 86 | # Setup format 87 | formatter = logging.Formatter(config.LOG_FORMAT, style="{") 88 | 89 | # Setup handler 90 | handler = UnicodeStreamHandler(sys.stdout, sys.stderr) 91 | handler.setFormatter(formatter) 92 | 93 | # Add handler 94 | target_logger.addHandler(handler) 95 | 96 | 97 | def init_file(target_logger): 98 | """Adds a file handler to a logger. 99 | 100 | Args: 101 | target_logger (logging.logger): The logger to add the file handler to. 102 | """ 103 | 104 | # Create logging directory 105 | if not os.path.isdir(config.LOGS_DIR): 106 | os.mkdir(config.LOGS_DIR) 107 | 108 | # Set log level 109 | if "log_level" not in data.cache: 110 | data.cache["log_level"] = "INFO" 111 | data.push() 112 | target_logger.setLevel(data.cache["log_level"]) 113 | 114 | # Setup format 115 | formatter = logging.Formatter(config.LOG_FORMAT, style="{") 116 | 117 | # Setup handlers 118 | handler = logging.FileHandler("{}/{}.log".format(config.LOGS_DIR, time.time()), encoding="UTF-8") 119 | handler.setFormatter(formatter) 120 | 121 | # Add handler 122 | target_logger.addHandler(handler) 123 | -------------------------------------------------------------------------------- /modis/tools/moduledb.py: -------------------------------------------------------------------------------- 1 | """ 2 | This tool handles all the module management functions in Modis, including 3 | scanning the module database and importing module files. 4 | """ 5 | 6 | import importlib 7 | import logging 8 | import os 9 | 10 | from modis.tools import config 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def get_names(): 16 | """Get a list of the names of all available modules. 17 | 18 | Returns: 19 | module_names (list): List of strings of module names. 20 | """ 21 | 22 | module_names = [] 23 | for module_folder in os.listdir(config.MODULES_DIR): 24 | if not os.path.isdir("{}/{}".format(config.MODULES_DIR, module_folder)): 25 | # Is a file, not a folder 26 | continue 27 | if module_folder.startswith("_"): 28 | # Module is manually deactivated 29 | continue 30 | module_names.append(module_folder) 31 | 32 | return module_names 33 | 34 | 35 | def get_imports(filenames): 36 | """Get a dictionary with imported Python module files organised by name. 37 | 38 | Args: 39 | filenames (list): The names to scan. 40 | 41 | Returns: 42 | imports (dict): The imported files. 43 | """ 44 | 45 | # Setup imports dict 46 | imports = {} 47 | 48 | # Import requested files for each module 49 | for module_name in get_names(): 50 | imports[module_name] = {} 51 | for file in os.listdir("{}/{}".format(config.MODULES_DIR, module_name)): 52 | file = file[:-3] 53 | if file not in filenames: 54 | # Requested file does not exist in module 55 | continue 56 | import_name = ".modules.{}.{}".format(module_name, file) 57 | logger.debug("Importing {} from {}".format(file, module_name)) 58 | try: 59 | imported_file = importlib.import_module(import_name, "modis") 60 | except Exception as e: 61 | logger.error("{} from {} failed to import".format(file, module_name)) 62 | logger.exception(e) 63 | continue 64 | imports[module_name][file] = imported_file 65 | 66 | return imports 67 | 68 | 69 | # TODO Deprecate 70 | def get_import_specific(eh_name, module_name): 71 | """Get a specific import from a module. 72 | 73 | Args: 74 | eh_name (str): The file to import. 75 | module_name (str) : The module to import from. 76 | 77 | Returns: 78 | imports (file): The imported file. 79 | """ 80 | 81 | if module_name not in os.listdir(config.MODULES_DIR): 82 | # Module does not exist 83 | return 84 | if eh_name + ".py" not in os.listdir("{}/{}".format(config.MODULES_DIR, module_name)): 85 | # File does not exist 86 | return 87 | import_name = ".modules.{}.{}".format(module_name, eh_name) 88 | return importlib.import_module(import_name, "modis") 89 | -------------------------------------------------------------------------------- /modis/tools/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | This tool checks GitHub for the latest version of Modis and can produce the name 3 | of the current official version and the difference between that version and the 4 | version currently being used. 5 | """ 6 | 7 | import logging 8 | import requests 9 | 10 | from modis.tools import config 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def infostr(): 16 | """ 17 | Get the version comparison info. 18 | 19 | Returns: 20 | version_str (str): A friendly response detailing the current version 21 | """ 22 | 23 | latest = _get() 24 | state = _compare(latest) 25 | if state == -1: 26 | return "A new version of Modis is available (v{})".format(latest) 27 | elif state == 0: 28 | return "You are running the latest version of Modis (v{})".format(config.VERSION) 29 | elif state == 1: 30 | return "You are running a preview version of Modis (v{} pre-release)".format(config.VERSION) 31 | 32 | 33 | def _get(): 34 | """Compare the current version to the latest on GitHub. 35 | 36 | Returns: 37 | version (list): The latest live version numbers 38 | """ 39 | 40 | logger.debug("Checking version...") 41 | 42 | # Get version info from GitHub 43 | try: 44 | r = requests.get("https://api.github.com/repos/Infraxion/Modis/releases/latest").json() 45 | if "message" not in r or r["message"] == "Not Found": 46 | r = requests.get("https://api.github.com/repos/Infraxion/Modis/releases").json()[0] 47 | except requests.ConnectionError: 48 | logger.warning("Could not connect to GitHub for version info") 49 | return [] 50 | 51 | # Parse version info 52 | if "tag_name" in r: 53 | version_name = r["tag_name"] 54 | version_tag = version_name.split('.') 55 | return version_tag 56 | else: 57 | return [] 58 | 59 | 60 | def _compare(release_version): 61 | """Compare the current version to the latest on GitHub. 62 | 63 | Args: 64 | release_version (list): The latest live version numbers 65 | 66 | Returns: 67 | comparison (int): -1=behind, 0=latest, 1=ahead 68 | """ 69 | 70 | current_version = config.VERSION.split('.') 71 | 72 | for vi in range(len(release_version)): 73 | if len(current_version) > vi: 74 | if current_version[vi] > release_version[vi]: 75 | return 1 76 | elif current_version[vi] < release_version[vi]: 77 | return -1 78 | elif release_version[vi] != "0": 79 | return -1 80 | 81 | return 0 82 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Quick install list: 2 | # pip install: 3 | discord.py[voice] 4 | youtube-dl 5 | soundcloud 6 | pynacl 7 | google-api-python-client 8 | requests 9 | # lxml 10 | praw 11 | # ffmpeg/bin/ in system PATH variables 12 | 13 | 14 | # core: 15 | discord.py[voice] 16 | youtube-dl 17 | # ffmpeg/bin/ in system PATH variables 18 | PyGithub 19 | 20 | # music module: 21 | youtube-dl 22 | soundcloud 23 | spotipy 24 | pynacl 25 | google-api-python-client 26 | 27 | # chatbot module: 28 | requests 29 | # lxml 30 | 31 | # gamedeals module: 32 | praw 33 | 34 | # rocketleague module: 35 | requests 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | See: 3 | https://packaging.python.org/en/latest/distributing.html 4 | https://github.com/pypa/sampleproject 5 | """ 6 | 7 | # To use a consistent encoding 8 | from codecs import open 9 | from os import path 10 | 11 | # Always prefer setuptools over distutils 12 | from setuptools import find_packages, setup 13 | 14 | from modis.tools import version 15 | 16 | here = path.abspath(path.dirname(__file__)) 17 | 18 | # Get the long description from the README file 19 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 20 | long_description = f.read() 21 | 22 | setup( 23 | name='modis', 24 | 25 | # Versions should comply with PEP440. For a discussion on single-sourcing 26 | # the version across setup.py and the project code, see 27 | # https://packaging.python.org/en/latest/single_source_version.html 28 | version=version.VERSION, 29 | 30 | description='A modular Discord bot', 31 | long_description=long_description, 32 | 33 | # The project's main homepage. 34 | url='https://github.com/Infraxion/modis/', 35 | 36 | # Author details 37 | author='Infraxion and YtnomSnrub', 38 | author_email='jalaunder@gmail.com', 39 | 40 | # Choose your license 41 | license='Apache', 42 | 43 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 44 | classifiers=[ 45 | # How mature is this project? Common values are 46 | # 3 - Alpha 47 | # 4 - Beta 48 | # 5 - Production/Stable 49 | 'Development Status :: 4 - Beta', 50 | 51 | # Indicate who your project is intended for 52 | 'Intended Audience :: Developers', 53 | 'Topic :: Communications :: Chat', 54 | 55 | # Pick your license as you wish (should match "license" above) 56 | 'License :: OSI Approved :: Apache Software License', 57 | 58 | # Specify the Python versions you support here. In particular, ensure 59 | # that you indicate whether you support Python 2, Python 3 or both. 60 | 'Programming Language :: Python :: 3.6', 61 | ], 62 | 63 | # What does your project relate to? 64 | keywords='modis discord bot music', 65 | 66 | # You can just specify the packages manually here if your project is 67 | # simple. Or you can use find_packages(). 68 | include_package_data=True, 69 | packages=find_packages(exclude=["__pycache__"]), 70 | 71 | # Alternatively, if you want to distribute just a my_module.py, uncomment 72 | # this: 73 | # py_modules=["my_module"], 74 | 75 | # List run-time dependencies here. These will be installed by pip when 76 | # your project is installed. For an analysis of "install_requires" vs pip's 77 | # requirements files see: 78 | # https://packaging.python.org/en/latest/requirements.html 79 | install_requires=[ 80 | 'discord.py[voice]>=0.16.12,<1', 81 | 'youtube-dl>=2017.11.15', 82 | 'pynacl>=1.0.1', 83 | 'google-api-python-client>=1.6.4', 84 | 'requests>=2.18.4', 85 | 'lxml>=4.1.1', 86 | 'praw>=5.2.0', 87 | 'soundcloud>=0.5.0', 88 | 'spotipy>=2.4.4' 89 | ], 90 | 91 | python_requires=">=3.6, <4", 92 | 93 | # List additional groups of dependencies here (e.g. development 94 | # dependencies). You can install these using the following syntax, 95 | # for example: 96 | # $ pip install -e .[dev,test] 97 | extras_require={}, 98 | 99 | # If there are data files included in your packages that need to be 100 | # installed, specify them here. If using Python 2.6 or less, then these 101 | # have to be included in MANIFEST.in as well. 102 | package_data={}, 103 | 104 | # Although 'package_data' is the preferred approach, in some case you may 105 | # need to place data files outside of your packages. See: 106 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 107 | # In this case, 'data_file' will be installed into '/my_data' 108 | data_files=[], 109 | 110 | # To provide executable scripts, use entry points in preference to the 111 | # "scripts" keyword. Entry points provide cross-platform support and allow 112 | # pip to create the appropriate form of executable for the target platform. 113 | entry_points={}, 114 | ) 115 | -------------------------------------------------------------------------------- /venv-resources/#START MODIS.bat: -------------------------------------------------------------------------------- 1 | call .\Scripts\activate.bat 2 | start pythonw.exe LauncherGUI.pyw -------------------------------------------------------------------------------- /venv-resources/Activate.ps1: -------------------------------------------------------------------------------- 1 | function global:deactivate ([switch]$NonDestructive) { 2 | # Revert to original values 3 | if (Test-Path function:_OLD_VIRTUAL_PROMPT) { 4 | copy-item function:_OLD_VIRTUAL_PROMPT function:prompt 5 | remove-item function:_OLD_VIRTUAL_PROMPT 6 | } 7 | 8 | if (Test-Path env:_OLD_VIRTUAL_PYTHONHOME) { 9 | copy-item env:_OLD_VIRTUAL_PYTHONHOME env:PYTHONHOME 10 | remove-item env:_OLD_VIRTUAL_PYTHONHOME 11 | } 12 | 13 | if (Test-Path env:_OLD_VIRTUAL_PATH) { 14 | copy-item env:_OLD_VIRTUAL_PATH env:PATH 15 | remove-item env:_OLD_VIRTUAL_PATH 16 | } 17 | 18 | if (Test-Path env:VIRTUAL_ENV) { 19 | remove-item env:VIRTUAL_ENV 20 | } 21 | 22 | if (!$NonDestructive) { 23 | # Self destruct! 24 | remove-item function:deactivate 25 | } 26 | } 27 | 28 | deactivate -nondestructive 29 | 30 | $env:VIRTUAL_ENV="%~dp0%..\" 31 | 32 | if (! $env:VIRTUAL_ENV_DISABLE_PROMPT) { 33 | # Set the prompt to include the env name 34 | # Make sure _OLD_VIRTUAL_PROMPT is global 35 | function global:_OLD_VIRTUAL_PROMPT {""} 36 | copy-item function:prompt function:_OLD_VIRTUAL_PROMPT 37 | function global:prompt { 38 | Write-Host -NoNewline -ForegroundColor Green '(modis-venv) ' 39 | _OLD_VIRTUAL_PROMPT 40 | } 41 | } 42 | 43 | # Clear PYTHONHOME 44 | if (Test-Path env:PYTHONHOME) { 45 | copy-item env:PYTHONHOME env:_OLD_VIRTUAL_PYTHONHOME 46 | remove-item env:PYTHONHOME 47 | } 48 | 49 | # Add the venv to the PATH 50 | copy-item env:PATH env:_OLD_VIRTUAL_PATH 51 | $env:PATH = "$env:VIRTUAL_ENV\Scripts;$env:PATH" 52 | -------------------------------------------------------------------------------- /venv-resources/activate: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate" *from bash* 2 | # you cannot run it directly 3 | 4 | deactivate () { 5 | # reset old environment variables 6 | if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then 7 | PATH="${_OLD_VIRTUAL_PATH:-}" 8 | export PATH 9 | unset _OLD_VIRTUAL_PATH 10 | fi 11 | if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then 12 | PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" 13 | export PYTHONHOME 14 | unset _OLD_VIRTUAL_PYTHONHOME 15 | fi 16 | 17 | # This should detect bash and zsh, which have a hash command that must 18 | # be called to get it to forget past commands. Without forgetting 19 | # past commands the $PATH changes we made may not be respected 20 | if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then 21 | hash -r 22 | fi 23 | 24 | if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then 25 | PS1="${_OLD_VIRTUAL_PS1:-}" 26 | export PS1 27 | unset _OLD_VIRTUAL_PS1 28 | fi 29 | 30 | unset VIRTUAL_ENV 31 | if [ ! "$1" = "nondestructive" ] ; then 32 | # Self destruct! 33 | unset -f deactivate 34 | fi 35 | } 36 | 37 | # unset irrelevant variables 38 | deactivate nondestructive 39 | 40 | VIRTUAL_ENV="%~dp0%..\" 41 | export VIRTUAL_ENV 42 | 43 | _OLD_VIRTUAL_PATH="$PATH" 44 | PATH="$VIRTUAL_ENV/Scripts:$PATH" 45 | export PATH 46 | 47 | # unset PYTHONHOME if set 48 | # this will fail if PYTHONHOME is set to the empty string (which is bad anyway) 49 | # could use `if (set -u; : $PYTHONHOME) ;` in bash 50 | if [ -n "${PYTHONHOME:-}" ] ; then 51 | _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" 52 | unset PYTHONHOME 53 | fi 54 | 55 | if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then 56 | _OLD_VIRTUAL_PS1="${PS1:-}" 57 | if [ "x(modis-venv) " != x ] ; then 58 | PS1="(modis-venv) ${PS1:-}" 59 | else 60 | if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then 61 | # special case for Aspen magic directories 62 | # see http://www.zetadev.com/software/aspen/ 63 | PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1" 64 | else 65 | PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1" 66 | fi 67 | fi 68 | export PS1 69 | fi 70 | 71 | # This should detect bash and zsh, which have a hash command that must 72 | # be called to get it to forget past commands. Without forgetting 73 | # past commands the $PATH changes we made may not be respected 74 | if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then 75 | hash -r 76 | fi 77 | -------------------------------------------------------------------------------- /venv-resources/activate.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem This file is UTF-8 encoded, so we need to update the current code page while executing it 4 | for /f "tokens=2 delims=:" %%a in ('"%SystemRoot%\System32\chcp.com"') do ( 5 | set "_OLD_CODEPAGE=%%a" 6 | ) 7 | if defined _OLD_CODEPAGE ( 8 | "%SystemRoot%\System32\chcp.com" 65001 > nul 9 | ) 10 | 11 | set "VIRTUAL_ENV=%~dp0%..\" 12 | 13 | if not defined PROMPT ( 14 | set "PROMPT=$P$G" 15 | ) 16 | 17 | if defined _OLD_VIRTUAL_PROMPT ( 18 | set "PROMPT=%_OLD_VIRTUAL_PROMPT%" 19 | ) 20 | 21 | if defined _OLD_VIRTUAL_PYTHONHOME ( 22 | set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" 23 | ) 24 | 25 | set "_OLD_VIRTUAL_PROMPT=%PROMPT%" 26 | set "PROMPT=(modis-venv) %PROMPT%" 27 | 28 | if defined PYTHONHOME ( 29 | set "_OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%" 30 | set PYTHONHOME= 31 | ) 32 | 33 | if defined _OLD_VIRTUAL_PATH ( 34 | set "PATH=%_OLD_VIRTUAL_PATH%" 35 | ) else ( 36 | set "_OLD_VIRTUAL_PATH=%PATH%" 37 | ) 38 | 39 | set "PATH=%VIRTUAL_ENV%\Scripts;%PATH%" 40 | 41 | :END 42 | if defined _OLD_CODEPAGE ( 43 | "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul 44 | set "_OLD_CODEPAGE=" 45 | ) 46 | -------------------------------------------------------------------------------- /venv-resources/ffmpeg.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModisWorks/modis/1cd27d6f16acdb67c68584ee70056b1a5c3d35e1/venv-resources/ffmpeg.exe --------------------------------------------------------------------------------