├── .github ├── FUNDING.yml └── workflows │ └── lint_python.yml ├── .gitignore ├── LICENSE ├── README.md ├── antirp ├── __init__.py ├── antirp.py └── info.json ├── freshmeat ├── __init__.py ├── freshmeat.py └── info.json ├── githubcards ├── __init__.py ├── calls.py ├── converters.py ├── core.py ├── data.py ├── exceptions.py ├── formatters.py ├── http.py └── info.json ├── info.json ├── massmove ├── __init__.py ├── info.json └── massmove.py └── sentryio ├── __init__.py ├── core.py └── info.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: Kowlin 4 | github: [Kowlin] 5 | -------------------------------------------------------------------------------- /.github/workflows/lint_python.yml: -------------------------------------------------------------------------------- 1 | name: Lint Python 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint_python: 6 | name: Lint Python 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/setup-python@v1 11 | with: 12 | python_version: "3.8" 13 | - run: "python -m pip install flake8" 14 | name: Install Flake8 15 | - run: "python -m flake8 . --count --select=E9,F7,F82 --show-source" 16 | name: Flake8 Linting 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .vscode/ 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sentinel 2 | Moderation oriented cogs built for Red-DiscordBot 3 | -------------------------------------------------------------------------------- /antirp/__init__.py: -------------------------------------------------------------------------------- 1 | from .antirp import AntiRP 2 | 3 | __red_end_user_data_statement__ = "AntiRP stores no personal information." 4 | 5 | 6 | async def setup(bot): 7 | await bot.add_cog(AntiRP(bot)) 8 | -------------------------------------------------------------------------------- /antirp/antirp.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | 7 | import discord 8 | from redbot.core import commands, checks, Config 9 | 10 | 11 | class AntiRP(commands.Cog): 12 | """AntiRP: For when hiding buttons in the Discord UI isn't enough.""" 13 | 14 | def_guild = { 15 | "toggle": False, 16 | "whitelist": [] 17 | } 18 | 19 | def __init__(self, bot): 20 | self.bot = bot 21 | self.config = Config.get_conf(self, identifier=25360008) 22 | 23 | self.config.register_guild(**self.def_guild) 24 | 25 | async def red_get_data_for_user(self, **kwargs): 26 | return {} 27 | 28 | async def red_delete_data_for_user(self, **kwargs): 29 | return 30 | 31 | @checks.admin_or_permissions(manage_guild=True) 32 | @commands.group() 33 | async def antirp(self, ctx): 34 | """Manage the settings for AntiRP""" 35 | pass 36 | 37 | @antirp.command() 38 | async def toggle(self, ctx, true_or_false: bool = None): 39 | """Toggle AntiRP on or off""" 40 | if true_or_false is None: 41 | toggle_config = await self.config.guild(ctx.guild).toggle() 42 | if toggle_config is True: 43 | await self.config.guild(ctx.guild).toggle.set(False) 44 | await ctx.send(f"Done! Turned off AntiRP") 45 | else: 46 | await self.config.guild(ctx.guild).toggle.set(True) 47 | await ctx.send(f"Done! Turned on AntiRP") 48 | else: 49 | await self.config.guild(ctx.guild).toggle.set(true_or_false) 50 | await ctx.tick() 51 | 52 | @antirp.command() 53 | async def grabname(self, ctx, channel: discord.TextChannel, messageID: int): 54 | """Grab an application name via RP Invite""" 55 | try: 56 | message = await channel.get_message(messageID) 57 | except discord.NotFound: 58 | return await ctx.send("Couldn't find the message you're looking for.") 59 | 60 | # Since invites all use party IDs we can savely assume that this is a RP invite. 61 | if message.activity is None: 62 | return await ctx.send("This message has no rich presence invite") 63 | 64 | # Check if this is spotify or not... Since spotify is SPECIAL! T_T 65 | if message.activity["party_id"].startswith("spotify:"): 66 | return await ctx.send("Application name: Spotify") 67 | return await ctx.send(f"Application name: {message.application['name']}") 68 | 69 | @antirp.group() 70 | async def whitelist(self, ctx): 71 | """Manage the application whitelist for AntiRP 72 | 73 | When an application is added to the whitelist, 74 | any other applications not matching the name will be removed. 75 | Regardless if the permissions are valid. 76 | 77 | Even though Spotify isn't an application it can be added as a whitelisted application""" 78 | pass 79 | 80 | @whitelist.command(name="add", usage="") 81 | async def wl_add(self, ctx, *, application_name: str): 82 | """Add a new whitelisted application""" 83 | whitelist_config = await self.config.guild(ctx.guild).whitelist() 84 | whitelist_config.append(application_name.lower()) 85 | await self.config.guild(ctx.guild).whitelist.set(whitelist_config) 86 | await ctx.tick() 87 | 88 | @whitelist.command(name="remove", usage="") 89 | async def wl_remove(self, ctx, *, application_name: str): 90 | """Remove a whitelisted application""" 91 | whitelist_config = await self.config.guild(ctx.guild).whitelist() 92 | whitelist_config.remove(application_name.lower()) 93 | await self.config.guild(ctx.guild).whitelist.set(whitelist_config) 94 | await ctx.tick() 95 | 96 | @whitelist.command(name="clear") 97 | async def wl_clear(self, ctx): 98 | """Remove all whitelisted applications""" 99 | await self.config.guild(ctx.guild).whitelist.set([]) 100 | await ctx.tick() 101 | 102 | @whitelist.command(name="list") 103 | async def wl_list(self, ctx): 104 | """List all the whitelisted applications""" 105 | whitelist_config = await self.config.guild(ctx.guild).whitelist() 106 | humanized_whitelist = ", ".join(whitelist_config) 107 | if len(whitelist_config) != 0: 108 | await ctx.send(f"Whitelisted applications:\n{humanized_whitelist}") 109 | else: 110 | await ctx.send("No applications are whitelisted.") 111 | 112 | async def cog_disabled_in_guild(self, guild): 113 | # compatibility layer with Red 3.3.10-3.3.12 114 | func = getattr(self.bot, "cog_disabled_in_guild", None) 115 | if func is None: 116 | return False 117 | return await func(self, guild) 118 | 119 | async def on_message(self, message): 120 | if message.guild is None: 121 | return 122 | 123 | if await self.cog_disabled_in_guild(message.guild): 124 | return 125 | 126 | guild_config = self.config.guild(message.guild) 127 | toggle_config = await guild_config.toggle() 128 | whitelist_config = await guild_config.whitelist() 129 | 130 | if message.guild is None or message.activity is None or toggle_config is False: 131 | return 132 | if await self.bot.is_automod_immune(message.author) is True: 133 | return # End it because we're dealing with a mod. 134 | 135 | if message.channel.permissions_for(message.author).embed_links is False: 136 | try: 137 | return await message.delete() 138 | except discord.Forbidden: 139 | return 140 | 141 | if len(whitelist_config) != 0: 142 | # We got entries in the whitelist, do special checks. 143 | if "spotify" in whitelist_config and message.activity["party_id"].startswith("spotify:"): 144 | return # Deal with spotify in the whitelist 145 | if message.application is not None and message.application["name"].lower() in whitelist_config: 146 | return # Deal with applications in the whitelist 147 | try: 148 | return await message.delete() # Handle not in whitelists. 149 | except: 150 | return 151 | -------------------------------------------------------------------------------- /antirp/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": [ 3 | "Kowlin" 4 | ], 5 | "description": "Block any type of rich presence invites", 6 | "end_user_data_statement": "This cog stores no personal data.", 7 | "short": "Block any type of rich presence invites", 8 | "tags": ["Moderation", "Rich Presence"], 9 | "type": "COG", 10 | "install_msg": "Thanks for installing AntiRP, Don't forget that ``[P]help AntiRP`` is your friend!", 11 | "min_bot_version": "3.5.1" 12 | } 13 | -------------------------------------------------------------------------------- /freshmeat/__init__.py: -------------------------------------------------------------------------------- 1 | from .freshmeat import Freshmeat 2 | 3 | __red_end_user_data_statement__ = "Freshmeat stores no user data." 4 | 5 | 6 | async def setup(bot): 7 | freshmeat = Freshmeat(bot) 8 | await bot.add_cog(freshmeat) 9 | -------------------------------------------------------------------------------- /freshmeat/freshmeat.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | 7 | import discord 8 | 9 | from redbot.core import commands, checks 10 | from redbot.core.utils.chat_formatting import pagify, escape 11 | from redbot.core.utils.menus import menu, DEFAULT_CONTROLS 12 | 13 | import datetime 14 | 15 | BaseCog = getattr(commands, "Cog", object) 16 | 17 | 18 | class Freshmeat(BaseCog): 19 | 20 | def __init__(self, bot): 21 | self.bot = bot 22 | 23 | async def red_get_data_for_user(self, **kwargs): 24 | return {} 25 | 26 | async def red_delete_data_for_user(self, **kwargs): 27 | return 28 | 29 | @commands.command() 30 | @commands.guild_only() 31 | @commands.bot_has_permissions(embed_links=True) 32 | @checks.admin_or_permissions(kick_members=True) 33 | async def freshmeat(self, ctx, hours: int = 24): 34 | """Show the members who joined in the specified timeframe 35 | 36 | `hours`: A number of hours to check for new members, must be above 0""" 37 | if hours < 1: 38 | return await ctx.send("Consider putting hours above 0. Since that helps with searching for members. ;)") 39 | elif hours > 300: 40 | return await ctx.send("Please use something less then 300 hours.") 41 | 42 | member_list = [] 43 | for member in ctx.guild.members: 44 | if ( 45 | member.joined_at is not None 46 | and member.joined_at > (ctx.message.created_at - datetime.timedelta(hours=hours)) 47 | ): 48 | member_list.append([member.display_name, member.id, member.joined_at]) 49 | 50 | member_list.sort(key=lambda member: member[2], reverse=True) 51 | member_string = "" 52 | for member in member_list: 53 | member_string += f"\n{member[0]} ({member[1]})" 54 | 55 | pages = [] 56 | for page in pagify(escape(member_string, formatting=True), page_length=1000): 57 | embed = discord.Embed(description=page) 58 | embed.set_author( 59 | name=f"{ctx.author.display_name}'s freshmeat of the day.", 60 | icon_url=ctx.author.display_avatar, 61 | ) 62 | pages.append(embed) 63 | 64 | page_counter = 1 65 | for page in pages: 66 | page.set_footer(text=f"Page {page_counter} out of {len(pages)}") 67 | page_counter += 1 68 | 69 | if not pages: 70 | return await ctx.send("No new members joined in specified timeframe.") 71 | 72 | await menu( 73 | ctx, 74 | pages=pages, 75 | controls=DEFAULT_CONTROLS, 76 | message=None, 77 | page=0, 78 | timeout=90 79 | ) 80 | -------------------------------------------------------------------------------- /freshmeat/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": [ 3 | "Kowlin", 4 | "JennJenn", 5 | "Irdumb (v2 iteration)" 6 | ], 7 | "description": "Check out the latest bunch of members with Freshmeat", 8 | "end_user_data_statement": "This cog stores no personal data.", 9 | "short": "Check out the latest bunch of members with Freshmeat", 10 | "tags": ["moderation","members"], 11 | "type": "COG", 12 | "install_msg": "Thanks for installing freshmeat, Don't forget that ``[P]help Freshmeat`` is your friend!", 13 | "min_bot_version": "3.5.1" 14 | } 15 | -------------------------------------------------------------------------------- /githubcards/__init__.py: -------------------------------------------------------------------------------- 1 | from redbot.core.bot import Red 2 | 3 | from .core import GitHubCards 4 | 5 | __red_end_user_data_statement__ = "GitHubCards stores no personal information." 6 | 7 | 8 | async def setup(bot: Red) -> None: 9 | cog = GitHubCards(bot) 10 | await cog.initialize() 11 | await bot.add_cog(cog) 12 | -------------------------------------------------------------------------------- /githubcards/calls.py: -------------------------------------------------------------------------------- 1 | # In my opinion, this is a dumb idea, but its a sane dumb idea! 2 | 3 | """ 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | """ 8 | 9 | 10 | class Queries: 11 | """Prebuild GraphQL query calls""" 12 | 13 | validateUser = """ 14 | query ValidateUser { 15 | viewer { 16 | id 17 | login 18 | } 19 | rateLimit { 20 | cost 21 | remaining 22 | limit 23 | resetAt 24 | } 25 | }""" 26 | 27 | validateRepo = """ 28 | query ValidateRepo($repoOwner: String!, $repoName: String!) { 29 | repository(owner: $repoOwner, name: $repoName) { 30 | id 31 | name 32 | } 33 | rateLimit { 34 | cost 35 | remaining 36 | limit 37 | resetAt 38 | } 39 | }""" 40 | 41 | findIssueQuery = """query FindIssueOrPr { 42 | %(repositories)s 43 | }""" 44 | 45 | findIssueRepository = """repo%(idx)s: repository(owner: "%(owner)s", name: "%(repo)s") { 46 | %(issues)s 47 | }""" 48 | 49 | findIssueFullData = """issue%(number)s: issueOrPullRequest(number: %(number)s) { 50 | __typename 51 | ... on PullRequest { 52 | number 53 | title 54 | body 55 | url 56 | createdAt 57 | state 58 | mergeable 59 | isDraft 60 | milestone { 61 | title 62 | } 63 | author { 64 | login 65 | avatarUrl 66 | url 67 | } 68 | repository { 69 | nameWithOwner 70 | } 71 | labels(first:100) { 72 | nodes { 73 | name 74 | } 75 | } 76 | } 77 | ... on Issue { 78 | number 79 | title 80 | body 81 | url 82 | createdAt 83 | state 84 | milestone { 85 | title 86 | } 87 | author { 88 | login 89 | avatarUrl 90 | url 91 | } 92 | repository { 93 | nameWithOwner 94 | } 95 | labels(first:100) { 96 | nodes { 97 | name 98 | } 99 | } 100 | } 101 | }""" 102 | 103 | searchIssues = """ 104 | query SearchIssues($query: String!) { 105 | search(type: ISSUE, query: $query, first: 15) { 106 | issueCount 107 | nodes { 108 | __typename 109 | ... on Issue { 110 | createdAt 111 | state 112 | number 113 | title 114 | url 115 | } 116 | ... on PullRequest { 117 | createdAt 118 | mergeable 119 | isDraft 120 | state 121 | number 122 | title 123 | url 124 | } 125 | } 126 | } 127 | rateLimit { 128 | cost 129 | remaining 130 | limit 131 | resetAt 132 | } 133 | }""" 134 | 135 | 136 | class Mutations: 137 | """Prebuild GraphQL mutation calls""" 138 | -------------------------------------------------------------------------------- /githubcards/converters.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | 7 | 8 | from redbot.core import commands 9 | 10 | 11 | class RepoData(commands.Converter): 12 | async def convert(self, ctx: commands.Context, argument: str) -> dict: 13 | cache = ctx.cog.active_prefix_matchers.get(ctx.guild.id, None) 14 | if cache is None: 15 | raise commands.BadArgument("There are no configured repositories on this server.") 16 | repo_data = cache["data"].get(argument, None) 17 | if repo_data is None: 18 | raise commands.BadArgument( 19 | f"There's no repo with prefix `{argument}` configured on this server" 20 | ) 21 | return repo_data 22 | -------------------------------------------------------------------------------- /githubcards/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | 7 | import asyncio 8 | import logging 9 | import re 10 | from typing import Any, Dict, Mapping, Optional 11 | from datetime import datetime 12 | 13 | import discord 14 | from redbot.core import Config, checks, commands 15 | 16 | from .converters import RepoData 17 | from .data import IssueType, SearchData 18 | from .exceptions import ApiError, Unauthorized 19 | from .formatters import FetchableReposDict, Formatters, Query 20 | from .http import GitHubAPI 21 | 22 | log = logging.getLogger("red.githubcards.core") 23 | 24 | 25 | """ 26 | { 27 | "prefix slug": { 28 | "owner": "Cog-Creators", 29 | "repo": "Red-DiscordBot", 30 | } 31 | } 32 | """ 33 | 34 | 35 | class GitHubCards(commands.Cog): 36 | """GitHub Cards""" 37 | # Oh my god I'm doing it 38 | 39 | def __init__(self, bot): 40 | self.bot = bot 41 | self.config = Config.get_conf(self, identifier=25360017) 42 | self.config.init_custom("REPO", 2) 43 | self.config.register_custom( 44 | "REPO", # + 2 identifiers: (guild_id, prefix) 45 | owner=None, 46 | repo=None, 47 | ) 48 | self.active_prefix_matchers = {} 49 | self.splitter = re.compile(r"[!?().,;:+|&/`\s]") 50 | self._ready = asyncio.Event() 51 | self.http: GitHubAPI = None # assigned in initialize() 52 | 53 | async def initialize(self): 54 | """ cache preloading """ 55 | await self.rebuild_cache_for_guild() 56 | await self._create_client() 57 | self._ready.set() 58 | 59 | async def rebuild_cache_for_guild(self, *guild_ids): 60 | self._ready.clear() 61 | try: 62 | repos = await self.config.custom("REPO").all() 63 | data = {int(k): v for k, v in repos.items()} 64 | if guild_ids: 65 | data = {k: v for k, v in data.items() if k in guild_ids} 66 | 67 | for guild_id, guild_data in data.items(): 68 | partial = "|".join(re.escape(prefix) for prefix in guild_data.keys()) 69 | pattern = re.compile(rf"^({partial})#([0-9]+)$", re.IGNORECASE) 70 | self.active_prefix_matchers[int(guild_id)] = {"pattern": pattern, "data": guild_data} 71 | finally: 72 | self._ready.set() 73 | 74 | async def cog_before_invoke(self, ctx): 75 | await self._ready.wait() 76 | 77 | def cog_unload(self): 78 | self.bot.loop.create_task(self.http.session.close()) 79 | 80 | async def red_get_data_for_user(self, **kwargs): 81 | return {} 82 | 83 | async def red_delete_data_for_user(self, **kwargs): 84 | return 85 | 86 | async def _get_token(self, api_tokens: Optional[Mapping[str, str]] = None) -> str: 87 | """Get GitHub token.""" 88 | if api_tokens is None: 89 | api_tokens = await self.bot.get_shared_api_tokens("github") 90 | 91 | token = api_tokens.get("token", "") 92 | if not token: 93 | log.error("No valid token found") 94 | return token 95 | 96 | async def _create_client(self) -> None: 97 | """Create GitHub API client.""" 98 | self.http = GitHubAPI(token=await self._get_token()) 99 | 100 | @commands.guild_only() 101 | @commands.command(usage=" ") 102 | async def ghsearch(self, ctx, repo_data: RepoData, *, search_query: str): 103 | """Search for issues in GitHub repo. 104 | 105 | Protip: You can also search issues via ``prefix#s ``!""" 106 | async with ctx.channel.typing(): 107 | search_data = await self.http.search_issues( 108 | repo_data["owner"], repo_data["repo"], search_query 109 | ) 110 | embed = Formatters.format_search(search_data) 111 | await ctx.send(embed=embed) 112 | 113 | # Command groups 114 | @commands.guild_only() 115 | @checks.mod_or_permissions(manage_guild=True) 116 | @commands.group(aliases=["ghc"], name="githubcards") 117 | async def ghc_group(self, ctx): 118 | """GitHubCards settings.""" 119 | 120 | @ghc_group.command(name="add") 121 | async def add(self, ctx, prefix: str, github_slug: str): 122 | """Add a new GitHub repository with the given prefix. 123 | 124 | Format for adding a new GitHub repo is "Username/Repository" 125 | """ 126 | prefix = prefix.lower() # Ensure lowering of prefixes, since fuck anything else. 127 | try: 128 | owner, repo = github_slug.split("/") 129 | except ValueError: 130 | await ctx.send('Invalid format. Please use ``Username/Repository``.') 131 | return 132 | 133 | try: 134 | await self.http.validate_repo(owner, repo) 135 | except ApiError: 136 | await ctx.send('The provided GitHub repository doesn\'t exist, or is unable to be accessed due to permissions.') 137 | return 138 | 139 | async with self.config.custom("REPO", ctx.guild.id).all() as repos: 140 | if prefix in repos.keys(): 141 | await ctx.send('This prefix already exists in this server. Please use something else.') 142 | return 143 | 144 | repos[prefix] = {"owner": owner, "repo": repo} 145 | 146 | await self.rebuild_cache_for_guild(ctx.guild.id) 147 | await ctx.send(f"A GitHub repository (``{github_slug}``) added with a prefix ``{prefix}``") 148 | 149 | @ghc_group.command(name="remove", aliases=["delete"]) 150 | async def remove(self, ctx, prefix: str): 151 | """Remove a GitHub repository with its given prefix. 152 | """ 153 | await self.config.custom("REPO", ctx.guild.id, prefix).clear() 154 | await self.rebuild_cache_for_guild(ctx.guild.id) 155 | 156 | # if that prefix doesn't exist, it will still send same message but I don't care 157 | await ctx.send(f"A repository with the prefix ``{prefix}`` has been removed.") 158 | 159 | @ghc_group.command(name="list") 160 | async def list_prefixes(self, ctx): 161 | """List all prefixes for GitHub Cards in this server. 162 | """ 163 | repos = await self.config.custom("REPO", ctx.guild.id).all() 164 | if not repos: 165 | await ctx.send("There are no configured GitHub repositories on this server.") 166 | return 167 | msg = "\n".join( 168 | f"``{prefix}``: ``{repo['owner']}/{repo['repo']}``" for prefix, repo in repos.items() 169 | ) 170 | await ctx.send(f"List of configured prefixes on **{ctx.guild.name}** server:\n{msg}") 171 | 172 | @ghc_group.command(name="instructions") 173 | async def instructions(self, ctx): 174 | """Learn on how to setup GHC 175 | 176 | *This will be the first time that someone will ACTUALLY read instructions*""" 177 | message = """ 178 | Begin by creating a new personal token on your GitHub Account here; 179 | 180 | 181 | If you do not trust this to your own account, its recommended you make a new GitHub account to act for the bot. 182 | No additional permissions are required for public repositories, if you want to fetch from private repositories, you require full "repo" permissions. 183 | 184 | Copy your newly created token and go to your DMs with the bot, and run the following command. 185 | ``[p]set api github token [YOUR NEW TOKEN]`` 186 | 187 | Finally reload the cog with ``[p]reload githubcards`` and you're set to add in new prefixes. 188 | """ 189 | await ctx.send(message) 190 | 191 | async def is_eligible_as_command(self, message: discord.Message) -> bool: 192 | """Check if message is eligible in command-like context.""" 193 | return ( 194 | self.http._token 195 | and not message.author.bot 196 | and message.guild is not None 197 | and await self.bot.message_eligible_as_command(message) 198 | and not await self.bot.cog_disabled_in_guild(self, message.guild) 199 | ) 200 | 201 | def get_matcher_by_message(self, message: discord.Message) -> Optional[Dict[str, Any]]: 202 | """Get matcher from message object. 203 | 204 | This also checks if the message is eligible as command and returns None otherwise. 205 | """ 206 | return self.active_prefix_matchers.get(message.guild.id) 207 | 208 | @commands.Cog.listener() 209 | async def on_red_api_tokens_update( 210 | self, service_name: str, api_tokens: Mapping[str, str] 211 | ): 212 | """Update GitHub token when `[p]set api` command is used.""" 213 | if service_name != "github": 214 | return 215 | await self.http.recreate_session(await self._get_token(api_tokens)) 216 | 217 | @commands.Cog.listener() 218 | async def on_message_without_command(self, message): 219 | await self._ready.wait() 220 | 221 | if not await self.is_eligible_as_command(message): 222 | return 223 | 224 | # --- MODULE FOR SEARCHING! --- 225 | # JSON is cached right... so this should be fine... 226 | # If I really want to *enjoy* this... probs rework this into a pseudo command module 227 | guild_data = await self.config.custom("REPO", message.guild.id).all() 228 | for prefix, data in guild_data.items(): 229 | if message.content.startswith(f"{prefix}#s "): 230 | async with message.channel.typing(): 231 | search_query = message.content.replace(f"{prefix}#s ", "") 232 | if all(issue_type not in search_query for issue_type in ["is:issue", "is:pr", "is:pull-request", "is:pull_request"]): 233 | search_data_issues = await self.http.search_issues( 234 | data["owner"], data["repo"], search_query, type=IssueType.ISSUE 235 | ) 236 | search_data_prs = await self.http.search_issues( 237 | data["owner"], data["repo"], search_query, type=IssueType.PULL_REQUEST 238 | ) 239 | # truncate the results to 15 based on date 240 | combined_results = search_data_issues.results + search_data_prs.results 241 | for result in combined_results: 242 | result["createdAt"] = datetime.strptime(result['createdAt'], '%Y-%m-%dT%H:%M:%SZ') 243 | combined_results.sort(key=lambda x: x["createdAt"], reverse=True) 244 | combined_results = combined_results[:15] 245 | 246 | search_data = SearchData( 247 | total=search_data_issues.total + search_data_prs.total, 248 | results=combined_results, 249 | query=search_query 250 | ) 251 | else: 252 | search_data = await self.http.search_issues( 253 | data["owner"], data["repo"], search_query, None 254 | ) 255 | embed = Formatters.format_search(search_data) 256 | await message.channel.send(embed=embed) 257 | return 258 | 259 | if (matcher := self.get_matcher_by_message(message)) is None: 260 | return 261 | 262 | # --- MODULE FOR GETTING EXISTING PREFIXES --- 263 | fetchable_repos: Dict[str, FetchableReposDict] = {} 264 | for item in self.splitter.split(message.content): 265 | match = matcher["pattern"].match(item) 266 | if match is None: 267 | continue 268 | prefix = match.group(1).lower() 269 | number = int(match.group(2)) 270 | 271 | prefix_data = matcher["data"][prefix] 272 | name_with_owner = (prefix_data['owner'], prefix_data['repo']) 273 | 274 | # Magical fetching aquesition done. 275 | # Ensure that the repo exists as a key 276 | if name_with_owner not in fetchable_repos: 277 | fetchable_repos[name_with_owner] = { 278 | "owner": prefix_data["owner"], 279 | "repo": prefix_data["repo"], 280 | "prefix": prefix, 281 | "fetchable_issues": {}, # using dict instead of a set since it's ordered 282 | } 283 | # No need to post card for same issue number from the same repo in one message twice 284 | if number in fetchable_repos[name_with_owner]['fetchable_issues']: 285 | continue 286 | fetchable_repos[name_with_owner]['fetchable_issues'][number] = None 287 | 288 | if len(fetchable_repos) == 0: 289 | return # End if no repos are found to query over. 290 | 291 | async with message.channel.typing(): 292 | await self._query_and_post(message, fetchable_repos) 293 | 294 | async def _query_and_post(self, message, fetchable_repos): 295 | # --- FETCHING --- 296 | query = Query.build_query(fetchable_repos) 297 | try: 298 | query_data = await self.http.send_query(query.query_string) 299 | except Unauthorized as e: 300 | log.error(e) 301 | return 302 | # Lmao what's error handling 303 | 304 | issue_data_list = [] 305 | for repo_data in query_data["data"].values(): 306 | for issue_data in repo_data.values(): 307 | if issue_data is not None: 308 | issue_data_list.append(Formatters.format_issue_class(issue_data)) 309 | 310 | if not issue_data_list: 311 | # Fetching of all issues has failed somehow. So end it here. 312 | return 313 | 314 | # --- SENDING --- 315 | issue_embeds = [] 316 | overflow = [] 317 | 318 | for index, issue in enumerate(issue_data_list): 319 | if index < 2: 320 | e = Formatters.format_issue(issue) 321 | issue_embeds.append(e) 322 | continue 323 | else: 324 | overflow.append(f"[{issue.name_with_owner}#{issue.number}]({issue.url})") 325 | 326 | for embed in issue_embeds: 327 | await message.channel.send(embed=embed) 328 | if len(overflow) != 0: 329 | embed = discord.Embed() 330 | embed.description = " • ".join(overflow) 331 | await message.channel.send(embed=embed) 332 | -------------------------------------------------------------------------------- /githubcards/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | 7 | from dataclasses import dataclass 8 | from datetime import datetime 9 | from typing import Optional, Tuple 10 | from urllib.parse import quote_plus 11 | from enum import Enum 12 | 13 | 14 | @dataclass(init=True) 15 | class SearchData(object): 16 | total: int # data/search/issueCount 17 | results: list # data/search/nodes 18 | query: str 19 | 20 | @property 21 | def escaped_query(self): 22 | return quote_plus(self.query) 23 | 24 | 25 | @dataclass(init=True) 26 | class IssueData(object): 27 | name_with_owner: str # data/repository 28 | author_name: str # data/issue/author 29 | author_url: str 30 | author_avatar_url: str 31 | issue_type: str 32 | number: int # data/issue 33 | title: str 34 | url: str 35 | body_text: str 36 | state: str 37 | labels: Tuple[str, ...] 38 | created_at: datetime 39 | is_draft: Optional[bool] = None 40 | mergeable_state: Optional[str] = None 41 | milestone: Optional[str] = None 42 | 43 | 44 | @dataclass(init=True) 45 | class IssueStateColour(object): 46 | OPEN: int = 0x6cc644 47 | CLOSED: int = 0xbd2c00 48 | MERGED: int = 0x6e5494 49 | 50 | 51 | class IssueType(Enum): 52 | ISSUE = "is:issue" 53 | PULL_REQUEST = "is:pr" 54 | -------------------------------------------------------------------------------- /githubcards/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | 7 | 8 | class RepoNotFound(Exception): 9 | pass 10 | 11 | 12 | class ApiError(Exception): 13 | pass 14 | 15 | 16 | class Unauthorized(ApiError): 17 | pass 18 | -------------------------------------------------------------------------------- /githubcards/formatters.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | 7 | import discord 8 | from redbot.core.utils.chat_formatting import pagify 9 | 10 | from datetime import datetime 11 | from typing import Dict, List, TypedDict 12 | 13 | from .data import IssueData, SearchData, IssueStateColour 14 | from .calls import Queries 15 | 16 | 17 | class Formatters: 18 | @staticmethod 19 | def format_issue_class(issue: dict) -> IssueData: 20 | mergeable_state = issue.get("mergeable", None) 21 | is_draft = issue.get("isDraft", None) 22 | milestone = issue["milestone"] 23 | if milestone is not None: 24 | milestone_title = milestone["title"] 25 | else: 26 | milestone_title = None 27 | 28 | if issue['author'] is None: 29 | issue['author'] = { 30 | "login": "Ghost", 31 | "url": "https://github.com/ghost", 32 | "avatarUrl": "https://avatars.githubusercontent.com/u/10137?v=4" 33 | } 34 | labels = tuple(label["name"] for label in issue["labels"]["nodes"]) 35 | 36 | data = IssueData( 37 | name_with_owner=issue['repository']['nameWithOwner'], 38 | author_name=issue['author']['login'], 39 | author_url=issue['author']['url'], 40 | author_avatar_url=issue['author']['avatarUrl'], 41 | issue_type=issue['__typename'], 42 | number=issue['number'], 43 | title=issue['title'], 44 | body_text=issue['body'], 45 | url=issue['url'], 46 | state=issue['state'], 47 | is_draft=is_draft, 48 | mergeable_state=mergeable_state, 49 | milestone=milestone_title, 50 | labels=labels, 51 | created_at=datetime.strptime(issue['createdAt'], '%Y-%m-%dT%H:%M:%SZ') 52 | ) 53 | return data 54 | 55 | @staticmethod 56 | def format_issue(issue_data: IssueData) -> discord.Embed: 57 | """Format a single issue into an embed""" 58 | embed = discord.Embed() 59 | embed.set_author( 60 | name=issue_data.author_name, 61 | url=issue_data.author_url, 62 | icon_url=issue_data.author_avatar_url 63 | ) 64 | number_suffix = f" #{issue_data.number}" 65 | max_len = 256 - len(number_suffix) 66 | if len(issue_data.title) > max_len: 67 | embed.title = f"{issue_data.title[:max_len-3]}...{number_suffix}" 68 | else: 69 | embed.title = f"{issue_data.title}{number_suffix}" 70 | embed.url = issue_data.url 71 | if len(issue_data.body_text) > 300: 72 | embed.description = next(pagify(issue_data.body_text, delims=[" ", "\n"], page_length=300, shorten_by=0)) + "..." 73 | else: 74 | embed.description = issue_data.body_text 75 | embed.colour = getattr(IssueStateColour, issue_data.state) 76 | formatted_datetime = issue_data.created_at.strftime('%d %b %Y, %H:%M') 77 | embed.set_footer(text=f"{issue_data.name_with_owner} • Created on {formatted_datetime}") 78 | if issue_data.labels: 79 | embed.add_field( 80 | name=f"Labels [{len(issue_data.labels)}]", 81 | value=", ".join(issue_data.labels[:5]), 82 | ) 83 | if issue_data.mergeable_state is not None and issue_data.state == "OPEN": 84 | mergable_state = issue_data.mergeable_state.capitalize() 85 | if issue_data.is_draft is True: 86 | mergable_state = "Drafted" 87 | embed.add_field(name="Merge Status", value=mergable_state) 88 | if issue_data.milestone: 89 | embed.add_field(name="Milestone", value=issue_data.milestone) 90 | return embed 91 | 92 | @staticmethod 93 | def format_search(search_data: SearchData) -> discord.Embed: 94 | """Format the search results into an embed""" 95 | embed = discord.Embed() 96 | embed_body = "" 97 | if not search_data.results: 98 | embed.description = "Nothing found." 99 | return embed 100 | for entry in search_data.results[:10]: 101 | if entry["state"] == "OPEN": 102 | state = "\N{LARGE GREEN CIRCLE}" 103 | elif entry["state"] == "CLOSED": 104 | state = "\N{LARGE RED CIRCLE}" 105 | else: 106 | state = "\N{LARGE PURPLE CIRCLE}" 107 | 108 | issue_type = ( 109 | "Issue" 110 | if entry["__typename"] == "Issue" 111 | else "Pull Request" 112 | ) 113 | mergeable_state = entry.get("mergeable", None) 114 | is_draft = entry.get("isDraft", None) 115 | if entry["state"] == "OPEN": 116 | if is_draft is True: 117 | state = "\N{PENCIL}\N{VARIATION SELECTOR-16}" 118 | elif mergeable_state == "CONFLICTING": 119 | state = "\N{WARNING SIGN}\N{VARIATION SELECTOR-16}" 120 | elif mergeable_state == "UNKNOWN": 121 | state = "\N{WHITE QUESTION MARK ORNAMENT}" 122 | embed_body += ( 123 | f"\n{state} - **{issue_type}** - **[#{entry['number']}]({entry['url']})**\n" 124 | f"{entry['title']}" 125 | ) 126 | if search_data.total > 10: 127 | embed.set_footer(text=f"Showing the first 10 results, {search_data.total} results in total.") 128 | embed_body += ( 129 | "\n\n[Click here for all the results]" 130 | f"(https://github.com/search?type=Issues&q={search_data.escaped_query})" 131 | ) 132 | embed.description = embed_body 133 | return embed 134 | 135 | 136 | class FetchableReposDict(TypedDict): 137 | owner: str 138 | repo: str 139 | prefix: str 140 | fetchable_issues: Dict[int, None] 141 | 142 | 143 | class Query: 144 | def __init__(self, query_string: str, repos: List[FetchableReposDict]): 145 | self.query_string = query_string 146 | self.repos = repos 147 | 148 | @classmethod 149 | def build_query(cls, fetchable_repos: Dict[str, FetchableReposDict]) -> str: 150 | repo_queries = [] 151 | repos = list(fetchable_repos.values()) 152 | for idx, repo_data in enumerate(repos): 153 | issue_queries = [] 154 | for issue in repo_data['fetchable_issues']: 155 | issue_queries.append(Queries.findIssueFullData % {"number": issue}) 156 | repo_queries.append( 157 | Queries.findIssueRepository 158 | % { 159 | "idx": idx, 160 | "owner": repo_data["owner"], 161 | "repo": repo_data["repo"], 162 | "issues": "\n".join(issue_queries), 163 | } 164 | ) 165 | 166 | query_string = Queries.findIssueQuery % {"repositories": "\n".join(repo_queries)} 167 | 168 | return cls(query_string, repos) 169 | -------------------------------------------------------------------------------- /githubcards/http.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import datetime 10 | import logging 11 | from typing import Any, Callable, Dict, Mapping, Optional, Literal 12 | 13 | import aiohttp 14 | 15 | from .calls import Queries 16 | from .data import SearchData, IssueType 17 | from .exceptions import ApiError, Unauthorized 18 | 19 | baseUrl = "https://api.github.com/graphql" 20 | log = logging.getLogger("red.githubcards.http") 21 | 22 | 23 | class RateLimit: 24 | """ 25 | This is somewhat similar to what's in gidgethub. 26 | 27 | We really should just use that lib already... 28 | """ 29 | 30 | def __init__(self, *, limit: int, remaining: int, reset: float, cost: Optional[int]) -> None: 31 | self.limit = limit 32 | self.remaining = remaining 33 | self.reset = reset 34 | self.cost = cost 35 | 36 | @classmethod 37 | def from_http( 38 | cls, headers: Mapping[str, Any], ratelimit_data: Mapping[str, Any] 39 | ) -> Optional[RateLimit]: 40 | try: 41 | limit = int(headers["x-ratelimit-limit"]) 42 | remaining = int(headers["x-ratelimit-remaining"]) 43 | reset = datetime.datetime.fromtimestamp( 44 | float(headers["x-ratelimit-reset"]), datetime.timezone.utc 45 | ) 46 | except KeyError: 47 | try: 48 | limit = ratelimit_data["limit"] 49 | remaining = ratelimit_data["remaining"] 50 | reset = datetime.strptime(ratelimit_data["resetAt"], '%Y-%m-%dT%H:%M:%SZ') 51 | except KeyError: 52 | return None 53 | else: 54 | cost = ratelimit_data.get("cost") 55 | return cls(limit=limit, remaining=remaining, reset=reset, cost=cost) 56 | 57 | 58 | class GitHubAPI: 59 | def __init__(self, token: str) -> None: 60 | self.session: aiohttp.ClientSession 61 | self._token: str 62 | self._create_session(token) 63 | 64 | async def recreate_session(self, token: str) -> None: 65 | await self.session.close() 66 | self._create_session(token) 67 | 68 | def _create_session(self, token: str) -> None: 69 | headers = { 70 | "Authorization": f"bearer {token}", 71 | "Content-Type": "application/json", 72 | "Accept": "application/vnd.github+json", 73 | "User-Agent": "Py aiohttp - GitHubCards (github.com/Kowlin/sentinel)" 74 | } 75 | self._token = token 76 | self.session = aiohttp.ClientSession(headers=headers) 77 | 78 | async def validate_user(self): 79 | async with self.session.post(baseUrl, json={"query": Queries.validateUser}) as call: 80 | json = await call.json() 81 | if call.status == 401: 82 | raise Unauthorized(json["message"]) 83 | if "errors" in json.keys(): 84 | raise ApiError(json['errors']) 85 | self._log_ratelimit( 86 | self.validate_user, call.headers, ratelimit_data=json['data']['rateLimit'] 87 | ) 88 | return json 89 | 90 | async def validate_repo(self, repoOwner: str, repoName: str): 91 | async with self.session.post( 92 | baseUrl, 93 | json={ 94 | "query": Queries.validateRepo, 95 | "variables": {"repoOwner": repoOwner, "repoName": repoName}, 96 | }, 97 | ) as call: 98 | json = await call.json() 99 | if call.status == 401: 100 | raise Unauthorized(json["message"]) 101 | if "errors" in json.keys(): 102 | raise ApiError(json['errors']) 103 | self._log_ratelimit( 104 | self.validate_repo, call.headers, ratelimit_data=json['data']['rateLimit'] 105 | ) 106 | return json 107 | 108 | async def search_issues(self, repoOwner: str, repoName: str, searchParam: str, type: Optional[Literal[IssueType.ISSUE, IssueType.PULL_REQUEST]]): 109 | if type is IssueType.ISSUE: 110 | searchParam = f"{searchParam} is:issue" 111 | elif type is IssueType.PULL_REQUEST: 112 | searchParam = f"{searchParam} is:pr" 113 | query = f"repo:{repoOwner}/{repoName} {searchParam}" 114 | async with self.session.post( 115 | baseUrl, 116 | json={ 117 | "query": Queries.searchIssues, 118 | "variables": {"query": query} 119 | } 120 | ) as call: 121 | json = await call.json() 122 | if "errors" in json.keys(): 123 | raise ApiError(json['errors']) 124 | self._log_ratelimit( 125 | self.search_issues, call.headers, ratelimit_data=json['data']['rateLimit'] 126 | ) 127 | search_results = json['data']['search'] 128 | 129 | data = SearchData( 130 | total=search_results['issueCount'], 131 | results=search_results['nodes'], 132 | query=query 133 | ) 134 | return data 135 | 136 | async def send_query(self, query: str): 137 | async with self.session.post( 138 | baseUrl, 139 | json={ 140 | "query": query 141 | } 142 | ) as call: 143 | json = await call.json() 144 | if call.status == 401: 145 | raise Unauthorized(json["message"]) 146 | self._log_ratelimit(self.send_query, call.headers) 147 | if call.status == 401: 148 | raise Unauthorized(json["message"]) 149 | return json 150 | 151 | def _log_ratelimit( 152 | self, 153 | func: Callable[[...], Any], 154 | headers: Mapping[str, Any], 155 | *, 156 | ratelimit_data: Dict[str, Any] = {}, 157 | ) -> None: 158 | ratelimit = RateLimit.from_http(headers, ratelimit_data) 159 | if ratelimit is not None: 160 | log.debug( 161 | "%s; cost %s, remaining: %s/%s", 162 | func.__name__, 163 | ratelimit.cost if ratelimit.cost is not None else "not provided", 164 | ratelimit.remaining, 165 | ratelimit.limit, 166 | ) 167 | else: 168 | log.debug("%s; no RL data", func.__name__) 169 | -------------------------------------------------------------------------------- /githubcards/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitHubCards", 3 | "short": "Embed GitHub issues on your discord chat.", 4 | "description": "Ever wanted to embed a github issue?! Well **now** you can! With GitHubCards, this sweet cog allows you to embed any issue from any public repo you like by using a unique prefix!\n*Warning: GithubCards will blow your mind!*", 5 | "end_user_data_statement": "This cog stores no personal data.", 6 | "install_msg": "Thanks for installing GithubCards\n\nBefore you'll be able to use this cog, you need to configure a GitHub token, see `[p]ghc instructions` for instructions.\nFor support go to: \n\nIf you like my work and want to support me, please consider becoming a Patron over at ", 7 | "author": [ 8 | "Kowlin (Kowlin#2536)", 9 | "jack1142 (Jackenmen#6607)", 10 | "mikeshardmind (Sinbad#1871)" 11 | ], 12 | "required_cogs": {}, 13 | "requirements": [], 14 | "tags": [ 15 | "api", 16 | "github", 17 | "utility" 18 | ], 19 | "min_bot_version": "3.5.1", 20 | "hidden": false, 21 | "disabled": false, 22 | "type": "COG" 23 | } 24 | -------------------------------------------------------------------------------- /info.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": ["Kowlin#2536"], 3 | "description": "Moderation oriented cogs built for Red-DiscordBot", 4 | "install_msg": "Hey there, thanks for installing this repository. Enjoy!", 5 | "short": "Moderation oriented cogs built for Red-DiscordBot" 6 | } 7 | -------------------------------------------------------------------------------- /massmove/__init__.py: -------------------------------------------------------------------------------- 1 | from .massmove import Massmove 2 | 3 | __red_end_user_data_statement__ = "Massmove stores no personal information." 4 | 5 | 6 | async def setup(bot): 7 | await bot.add_cog(Massmove(bot)) 8 | -------------------------------------------------------------------------------- /massmove/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": [ 3 | "Kowlin" 4 | ], 5 | "description": "Massmove members out of voice channels.", 6 | "end_user_data_statement": "This cog stores no personal data.", 7 | "short": "Massmove members out of voice channels.", 8 | "tags": ["moderation","massmove","voice","voicemoderation"], 9 | "type": "COG", 10 | "install_msg": "Thanks for installing massmove, Don't forget that ``[P]help Massmove`` is your friend!", 11 | "min_bot_version": "3.5.1" 12 | } 13 | -------------------------------------------------------------------------------- /massmove/massmove.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | 7 | import discord 8 | 9 | from redbot.core import commands, checks 10 | 11 | from typing import Union 12 | 13 | 14 | class Massmove(commands.Cog): 15 | 16 | def __init__(self, bot): 17 | self.bot = bot 18 | 19 | async def red_get_data_for_user(self, **kwargs): 20 | return {} 21 | 22 | async def red_delete_data_for_user(self, **kwargs): 23 | return 24 | 25 | @checks.mod_or_permissions(move_members=True) 26 | @commands.group( 27 | autohelp=False, 28 | invoke_without_command=True, 29 | usage=" " 30 | ) 31 | async def massmove( 32 | self, 33 | ctx, 34 | channel_from: Union[discord.VoiceChannel, discord.StageChannel], 35 | channel_to: Union[discord.VoiceChannel, discord.StageChannel] 36 | ): 37 | """Massmove members from one channel to another. 38 | 39 | To grab the channel easily, mention it with `#!`. 40 | 41 | Arguments: 42 | - `from channel`: The channel members will get moved from 43 | - `to channel`: The channel members will get moved to 44 | """ 45 | await self.move_all_members(ctx, channel_from, channel_to) 46 | 47 | @checks.mod_or_permissions(move_members=True) 48 | @massmove.command( 49 | usage="" 50 | ) 51 | async def afk(self, ctx, channel_from: Union[discord.VoiceChannel, discord.StageChannel]): 52 | """Massmove members to the AFK channel 53 | 54 | To grab the channel easily, mention it with ``#!``. 55 | 56 | Arguments: 57 | - `from channel`: The channel members will get moved from 58 | """ 59 | await self.move_all_members(ctx, channel_from, ctx.guild.afk_channel) 60 | 61 | @checks.mod_or_permissions(move_members=True) 62 | @massmove.command( 63 | usage="" 64 | ) 65 | async def me(self, ctx, channel_to: Union[discord.VoiceChannel, discord.StageChannel]): 66 | """Massmove you and every other member to another channel. 67 | 68 | To grab the channel easily, mention it with ``#!``. 69 | 70 | Arguments: 71 | - `to channel`: The channel members will get moved to 72 | """ 73 | voice = ctx.author.voice 74 | if voice is None: 75 | return await ctx.send("You have to be in an voice channel to use this command.") 76 | await self.move_all_members(ctx, voice.channel, channel_to) 77 | 78 | async def move_all_members(self, ctx, channel_from: discord.VoiceChannel, channel_to: discord.VoiceChannel): 79 | """Internal function for massmoving, massmoves all members to the target channel""" 80 | plural = True 81 | member_amount = len(channel_from.members) 82 | if member_amount == 0: 83 | return await ctx.send(f"{channel_from.mention} doesn't have any members in it.") 84 | elif member_amount == 1: 85 | plural = False 86 | # Check permissions to ensure a smooth transisition 87 | if channel_from.permissions_for(ctx.guild.me).move_members is False: 88 | return await ctx.send(f"I don't have permissions to move members in {channel_from.mention}.") 89 | if channel_to.permissions_for(ctx.guild.me).move_members is False: 90 | return await ctx.send(f"I don't have permissions to move members in {channel_to.mention}.") 91 | # Move the members 92 | for member in channel_from.members: 93 | try: 94 | await member.move_to(channel_to) 95 | except: 96 | pass 97 | await ctx.send( 98 | f"Done, massmoved {member_amount} member{'s' if plural else ''} from **{channel_from.mention}** to **{channel_to.mention}**." 99 | ) 100 | -------------------------------------------------------------------------------- /sentryio/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import SentryIO 2 | 3 | __red_end_user_data_statement__ = "This cog does not record end user data." 4 | 5 | 6 | async def setup(bot): 7 | cog = SentryIO(bot) 8 | try: 9 | await bot.add_cog(cog) 10 | except Exception: 11 | # if adding cog causes an error, we want Sentry client to close itself 12 | cog.cog_unload() 13 | raise 14 | -------------------------------------------------------------------------------- /sentryio/core.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from typing import Mapping, Optional 4 | 5 | import discord 6 | import sentry_sdk 7 | from redbot.core import checks, commands 8 | from redbot.core.bot import Red 9 | from redbot.core.utils.chat_formatting import inline 10 | from redbot.core.utils.views import SetApiView 11 | from sentry_sdk import add_breadcrumb 12 | from sentry_sdk.integrations.aiohttp import AioHttpIntegration 13 | from sentry_sdk.integrations.logging import LoggingIntegration 14 | 15 | log = logging.getLogger("red.sentinel.sentryio.core") 16 | 17 | 18 | class SentryIO(commands.Cog): 19 | """Sentry.IO Logger integration""" 20 | 21 | def __init__(self, bot: Red): 22 | self.bot = bot 23 | 24 | async def cog_load(self) -> None: 25 | asyncio.create_task(self.startup()) 26 | 27 | def cog_unload(self) -> None: 28 | self.close_sentry() 29 | 30 | async def red_get_data_for_user(self, **kwargs): 31 | return {} 32 | 33 | async def red_delete_data_for_user(self, **kwargs): 34 | return 35 | 36 | async def _get_dsn(self, api_tokens: Optional[Mapping[str, str]] = None) -> str: 37 | """Get Sentry DSN.""" 38 | if api_tokens is None: 39 | api_tokens = await self.bot.get_shared_api_tokens("sentry") 40 | 41 | dsn = api_tokens.get("dsn", "") # type: ignore 42 | if not dsn: 43 | log.error("No valid DSN found") 44 | return dsn 45 | 46 | def init_sentry(self, dsn: str) -> None: 47 | self.close_sentry() 48 | if not dsn: 49 | return 50 | log.info("Initializing Sentry with DSN: %s", dsn) 51 | sentry_sdk.init( 52 | dsn, 53 | traces_sample_rate=1.0, 54 | shutdown_timeout=0, 55 | integrations=[ 56 | AioHttpIntegration(), 57 | LoggingIntegration(level=logging.INFO, event_level=logging.ERROR), 58 | ], 59 | ) 60 | 61 | def close_sentry(self) -> None: 62 | client = sentry_sdk.Hub.current.client 63 | if client is not None: 64 | log.info("Closing Sentry client") 65 | client.close(timeout=0) 66 | 67 | async def startup(self) -> None: 68 | self.init_sentry(await self._get_dsn()) 69 | 70 | @commands.group(name="sentryio") # type: ignore 71 | @checks.is_owner() 72 | async def sentry_group(self, ctx): 73 | """Configure Sentry.IO stuffies""" # TODO 74 | 75 | @sentry_group.command(name="status") 76 | async def status(self, ctx): 77 | """Check the status of Sentry.IO integration.""" 78 | client = sentry_sdk.Hub.current.client 79 | if client is None: 80 | await ctx.send("Sentry.IO is not initialized.") 81 | return 82 | await ctx.send( 83 | f"Sentry.IO is initialized with DSN: {inline(client.options['dsn'])}" 84 | ) 85 | 86 | @sentry_group.command(name="instructions") 87 | async def instructions(self, ctx): 88 | """ 89 | Learn on how to setup SentryIO cog. 90 | 91 | *This will be the SECOND time that someone will ACTUALLY read instructions* 92 | """ 93 | message = ( 94 | "1. Go to Settings page of your Sentry Account at \n" 95 | "2. Go to Projects list and select the project you want to use for this bot.\n" 96 | "3. Select Client Keys (DSN) menu entry at the left.\n" 97 | "4. Copy your DSN and click the button below to set your DSN." 98 | ) 99 | api_tokens = await self.bot.get_shared_api_tokens("sentry") 100 | await ctx.send( 101 | message, 102 | view=SetApiView( 103 | default_service="sentry", 104 | default_keys={"dsn": api_tokens.get("dsn", "")}, 105 | ), 106 | ) 107 | 108 | @commands.Cog.listener() 109 | async def on_red_api_tokens_update( 110 | self, service_name: str, api_tokens: Mapping[str, str] 111 | ): 112 | if service_name != "sentry": 113 | return 114 | self.init_sentry(await self._get_dsn(api_tokens)) 115 | 116 | def prepare_crumbs_commands(self, ctx: commands.Context) -> Mapping[str, str]: 117 | crumb_data = { 118 | "command_name": getattr(ctx.command, "qualified_name", "None"), 119 | "cog_name": getattr(ctx.command.cog, "qualified_name", "None"), 120 | "author_id": getattr(ctx.author, "id", "None"), 121 | "guild_id": getattr(ctx.guild, "id", "None"), 122 | "channel_id": getattr(ctx.channel, "id", "None"), 123 | } 124 | for comm_arg, value in ctx.kwargs.items(): 125 | crumb_data[f"command_arg_{comm_arg}"] = value 126 | return crumb_data 127 | 128 | def prepare_crumbs_interactions(self, interaction: discord.Interaction) -> Mapping[str, str]: 129 | crumb_data = { 130 | "interaction_id": getattr(interaction, "id", "None"), 131 | "channel_id": getattr(interaction.channel, "id", "None"), 132 | "guild_id": getattr(interaction.guild, "id", "None"), 133 | "user_id": getattr(interaction.user, "id", "None"), 134 | "message_id": getattr(interaction.message, "id", "None"), 135 | } 136 | return crumb_data 137 | 138 | @commands.Cog.listener() 139 | async def on_command_error(self, ctx: commands.Context, error): 140 | if not ctx.command: 141 | return 142 | 143 | crumb_data = self.prepare_crumbs_commands(ctx) 144 | add_breadcrumb( 145 | type="user", 146 | category="on_command_error", 147 | message=f'Command "{ctx.command.qualified_name}" failed for {ctx.author.name} ({ctx.author.id})', 148 | level="error", 149 | data=crumb_data, 150 | ) 151 | 152 | @commands.Cog.listener() 153 | async def on_command(self, ctx: commands.Context): 154 | crumb_data = self.prepare_crumbs_commands(ctx) 155 | add_breadcrumb( 156 | type="user", 157 | category="on_command", 158 | message=f'Command "{ctx.command.qualified_name}" ran for {ctx.author.name} ({ctx.author.id})', 159 | level="info", 160 | data=crumb_data, 161 | ) 162 | 163 | @commands.Cog.listener() 164 | async def on_command_completion(self, ctx: commands.Context): 165 | crumb_data = self.prepare_crumbs_commands(ctx) 166 | add_breadcrumb( 167 | type="user", 168 | category="on_command_completion", 169 | message=f'Command "{ctx.command.qualified_name}" completed for {ctx.author.name} ({ctx.author.id})', 170 | level="info", 171 | data=crumb_data, 172 | ) 173 | 174 | @commands.Cog.listener() 175 | async def on_interaction(self, interaction: discord.Interaction): 176 | crumb_data = self.prepare_crumbs_interactions(interaction) 177 | add_breadcrumb( 178 | type="user", 179 | category="on_interaction", 180 | message=f'Interaction "{interaction.id}" ran for {interaction.user.name} ({interaction.user.id})', 181 | level="info", 182 | data=crumb_data, 183 | ) 184 | -------------------------------------------------------------------------------- /sentryio/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SentryIO", 3 | "short": "Sentry.IO integration for Red-DiscordBot", 4 | "description": "Sentry.IO integration for Red-DiscordBot. Automatically sends your pesky errors to Sentry.IO for analysis.", 5 | "end_user_data_statement": "This cog does not record end user data.", 6 | "install_msg": "", 7 | "author": [ 8 | "Kowlin (@Kowlin)", 9 | "Jakub Kuczys (https://github.com/Jackenmen)" 10 | ], 11 | "min_bot_version": "3.5.0", 12 | "required_cogs": {}, 13 | "requirements": [ 14 | "sentry-sdk" 15 | ], 16 | "tags": [ 17 | "api", 18 | "logging", 19 | "sentry", 20 | "sentryio" 21 | ], 22 | "hidden": false, 23 | "disabled": false, 24 | "type": "COG" 25 | } 26 | --------------------------------------------------------------------------------