mirror of
https://github.com/allthingslinux/tux.git
synced 2024-10-02 16:43:12 +00:00
Merge branch 'main' of github.com:allthingslinux/tux into setup
This commit is contained in:
commit
2327ef69f5
51 changed files with 1875 additions and 552 deletions
8
.github/CODE_OF_CONDUCT.md
vendored
8
.github/CODE_OF_CONDUCT.md
vendored
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
allthingslinux@proton.me.
|
||||
<mailto:allthingslinux@proton.me>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
@ -116,7 +116,7 @@ the community.
|
|||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
@ -124,5 +124,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
|
|||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
<https://www.contributor-covenant.org/faq>. Translations are available at
|
||||
<https://www.contributor-covenant.org/translations>.
|
||||
|
|
37
.github/CONTRIBUTING.md
vendored
37
.github/CONTRIBUTING.md
vendored
|
@ -1,38 +1,43 @@
|
|||
# Contributing to Tux 🐧
|
||||
|
||||
## Topics
|
||||
- [Contributing Flow](#contributing-flow)
|
||||
- [Issues](#issues)
|
||||
- [Branch Naming Conventions](#branch-naming-conventions)
|
||||
|
||||
- [Contributing to Tux 🐧](#contributing-to-tux-)
|
||||
- [Topics](#topics)
|
||||
- [Contributing Flow](#contributing-flow)
|
||||
- [Issues](#issues)
|
||||
- [Branch Naming Conventions](#branch-naming-conventions)
|
||||
|
||||
## Contributing Flow
|
||||
|
||||
1. See [Issues](#issues) topic.
|
||||
2. Fork the project.
|
||||
3. Create a new branch (please, see [Branch Naming Conventions](#branch-naming-conventions) topic if you don't know our conventions).
|
||||
|
||||
|
||||
4. After done with modifications, time to commit and push. Example:
|
||||
|
||||
```
|
||||
git add tux/help.py
|
||||
git commit -m "feat(tux): add help command" -m "Help command description"
|
||||
git push origin feat/add-help-command
|
||||
```
|
||||
```bash
|
||||
git add tux/help.py
|
||||
git commit -m "feat(tux): add help command" -m "Help command description"
|
||||
git push origin feat/add-help-command
|
||||
```
|
||||
|
||||
5. Send a Pull Request (PR) with the modifications, referencing the `main` branch.
|
||||
6. Your contribution will be reviewed by the maintainers.
|
||||
|
||||
|
||||
After merge:
|
||||
|
||||
- Delete the branch used to commit:
|
||||
```
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git push origin --delete feat/add-help-command
|
||||
git branch -D feat/add-help-command
|
||||
```
|
||||
|
||||
- Update your fork:
|
||||
```
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/allthingslinux/tux.git
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
|
@ -40,10 +45,12 @@ git push -f origin main
|
|||
```
|
||||
|
||||
## Issues
|
||||
Before submitting a large PR, please open an [issue](https://github.com/allthingslinux/tux/issues/new) so we can discuss the idea.
|
||||
|
||||
## Branch Naming Conventions
|
||||
Before submitting a large PR, please open an [issue](https://github.com/allthingslinux/tux/issues/new) so we can discuss the idea.
|
||||
|
||||
## Branch Naming Conventions
|
||||
|
||||
- Documentation: `git checkout -b docs/contributing`
|
||||
- Modifications: `git checkout -b chore/update-dependencies`
|
||||
- Features: `git checkout -b feat/add-help-command`
|
||||
- Fixing: `git checkout -b fix/help-command`
|
||||
- Fixing: `git checkout -b fix/help-command`
|
||||
|
|
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] - "
|
||||
labels: "type: bug"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe the Bug
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## To Reproduce
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
Please include code snippets or screenshots where applicable.
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Screenshots
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## Environment (please complete the following information)
|
||||
|
||||
- OS: [e.g. Ubuntu 20.04]
|
||||
- Python version: [e.g. Python 3.12]
|
||||
- Discord.py version: [e.g. 2.0.0]
|
||||
- Tux version: [e.g. v1.0.0]
|
||||
|
||||
## Additional Context
|
||||
|
||||
Add any other context about the problem here, such as logs, configuration files, etc.
|
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] - "
|
||||
labels: "type: feature-request"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Is your feature request related to a problem? Please describe
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
## Describe the solution you'd like
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
## Describe alternatives you've considered
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
## Additional Context
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
40
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
40
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Pull Request Template
|
||||
|
||||
## Description
|
||||
|
||||
Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Documentation update
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||
|
||||
## How Has This Been Tested?
|
||||
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.
|
||||
|
||||
- [ ] Test A
|
||||
- [ ] Test B
|
||||
|
||||
## Screenshots (if applicable)
|
||||
|
||||
Please add screenshots to help explain your changes.
|
||||
|
||||
## Additional Information
|
||||
|
||||
Please add any other information that is important to this PR.
|
0
SECURITY.md → .github/SECURITY.md
vendored
0
SECURITY.md → .github/SECURITY.md
vendored
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -164,6 +164,7 @@ github-private-key.pem
|
|||
# Miscellaneous
|
||||
/debug.csv
|
||||
config/settings.json
|
||||
config/settings.yml
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
|
266
.markdownlint.yaml
Normal file
266
.markdownlint.yaml
Normal file
|
@ -0,0 +1,266 @@
|
|||
# Example markdownlint configuration with all properties set to their default value
|
||||
|
||||
# Default state for all rules
|
||||
default: true
|
||||
|
||||
# Path to configuration file to extend
|
||||
extends: null
|
||||
|
||||
# MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md001.md
|
||||
MD001: true
|
||||
|
||||
# MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md003.md
|
||||
MD003:
|
||||
# Heading style
|
||||
style: "consistent"
|
||||
|
||||
# MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md
|
||||
MD004:
|
||||
# List style
|
||||
style: "consistent"
|
||||
|
||||
# MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md005.md
|
||||
MD005: true
|
||||
|
||||
# MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md007.md
|
||||
MD007:
|
||||
# Spaces for indent
|
||||
indent: 2
|
||||
# Whether to indent the first level of the list
|
||||
start_indented: false
|
||||
# Spaces for first level indent (when start_indented is set)
|
||||
start_indent: 2
|
||||
|
||||
# MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md009.md
|
||||
MD009:
|
||||
# Spaces for line break
|
||||
br_spaces: 2
|
||||
# Allow spaces for empty lines in list items
|
||||
list_item_empty_lines: false
|
||||
# Include unnecessary breaks
|
||||
strict: false
|
||||
|
||||
# MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md
|
||||
MD010:
|
||||
# Include code blocks
|
||||
code_blocks: true
|
||||
# Fenced code languages to ignore
|
||||
ignore_code_languages: []
|
||||
# Number of spaces for each hard tab
|
||||
spaces_per_tab: 1
|
||||
|
||||
# MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md011.md
|
||||
MD011: true
|
||||
|
||||
# MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md012.md
|
||||
MD012:
|
||||
# Consecutive blank lines
|
||||
maximum: 1
|
||||
|
||||
# MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md
|
||||
MD013:
|
||||
# Number of characters
|
||||
line_length: 200
|
||||
# Number of characters for headings
|
||||
heading_line_length: 80
|
||||
# Number of characters for code blocks
|
||||
code_block_line_length: 80
|
||||
# Include code blocks
|
||||
code_blocks: true
|
||||
# Include tables
|
||||
tables: true
|
||||
# Include headings
|
||||
headings: true
|
||||
# Strict length checking
|
||||
strict: false
|
||||
# Stern length checking
|
||||
stern: false
|
||||
|
||||
# MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md014.md
|
||||
MD014: true
|
||||
|
||||
# MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md018.md
|
||||
MD018: true
|
||||
|
||||
# MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md019.md
|
||||
MD019: true
|
||||
|
||||
# MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md020.md
|
||||
MD020: true
|
||||
|
||||
# MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md021.md
|
||||
MD021: true
|
||||
|
||||
# MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md
|
||||
MD022:
|
||||
# Blank lines above heading
|
||||
lines_above: 1
|
||||
# Blank lines below heading
|
||||
lines_below: 1
|
||||
|
||||
# MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md023.md
|
||||
MD023: true
|
||||
|
||||
# MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md024.md
|
||||
MD024:
|
||||
# Only check sibling headings
|
||||
siblings_only: false
|
||||
|
||||
# MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md025.md
|
||||
MD025:
|
||||
# Heading level
|
||||
level: 1
|
||||
# RegExp for matching title in front matter
|
||||
front_matter_title: "^\\s*title\\s*[:=]"
|
||||
|
||||
# MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md026.md
|
||||
MD026:
|
||||
# Punctuation characters
|
||||
punctuation: ".,;:!。,;:!"
|
||||
|
||||
# MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md027.md
|
||||
MD027: true
|
||||
|
||||
# MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md028.md
|
||||
MD028: false
|
||||
|
||||
# MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md029.md
|
||||
MD029:
|
||||
# List style
|
||||
style: "one_or_ordered"
|
||||
|
||||
# MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md030.md
|
||||
MD030:
|
||||
# Spaces for single-line unordered list items
|
||||
ul_single: 1
|
||||
# Spaces for single-line ordered list items
|
||||
ol_single: 1
|
||||
# Spaces for multi-line unordered list items
|
||||
ul_multi: 1
|
||||
# Spaces for multi-line ordered list items
|
||||
ol_multi: 1
|
||||
|
||||
# MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md031.md
|
||||
MD031:
|
||||
# Include list items
|
||||
list_items: true
|
||||
|
||||
# MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md032.md
|
||||
MD032: true
|
||||
|
||||
# MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md033.md
|
||||
MD033: false
|
||||
|
||||
# MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md034.md
|
||||
MD034: true
|
||||
|
||||
# MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md035.md
|
||||
MD035:
|
||||
# Horizontal rule style
|
||||
style: "consistent"
|
||||
|
||||
# MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md036.md
|
||||
MD036:
|
||||
# Punctuation characters
|
||||
punctuation: ".,;:!?。,;:!?"
|
||||
|
||||
# MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md037.md
|
||||
MD037: true
|
||||
|
||||
# MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md038.md
|
||||
MD038: true
|
||||
|
||||
# MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md039.md
|
||||
MD039: true
|
||||
|
||||
# MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md040.md
|
||||
MD040:
|
||||
# List of languages
|
||||
allowed_languages: []
|
||||
# Require language only
|
||||
language_only: false
|
||||
|
||||
# MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md041.md
|
||||
MD041:
|
||||
# Heading level
|
||||
level: 1
|
||||
# RegExp for matching title in front matter
|
||||
front_matter_title: "^\\s*title\\s*[:=]"
|
||||
|
||||
# MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md042.md
|
||||
MD042: true
|
||||
|
||||
# MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md043.md
|
||||
MD043: false
|
||||
|
||||
# MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md044.md
|
||||
MD044:
|
||||
# List of proper names
|
||||
names: []
|
||||
# Include code blocks
|
||||
code_blocks: true
|
||||
# Include HTML elements
|
||||
html_elements: true
|
||||
|
||||
# MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md045.md
|
||||
MD045: true
|
||||
|
||||
# MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md046.md
|
||||
MD046:
|
||||
# Block style
|
||||
style: "consistent"
|
||||
|
||||
# MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md047.md
|
||||
MD047: true
|
||||
|
||||
# MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md048.md
|
||||
MD048:
|
||||
# Code fence style
|
||||
style: "consistent"
|
||||
|
||||
# MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md049.md
|
||||
MD049:
|
||||
# Emphasis style
|
||||
style: "consistent"
|
||||
|
||||
# MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md050.md
|
||||
MD050:
|
||||
# Strong style
|
||||
style: "consistent"
|
||||
|
||||
# MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md051.md
|
||||
MD051: true
|
||||
|
||||
# MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md052.md
|
||||
MD052:
|
||||
# Include shortcut syntax
|
||||
shortcut_syntax: false
|
||||
|
||||
# MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md053.md
|
||||
MD053:
|
||||
# Ignored definitions
|
||||
ignored_definitions:
|
||||
- "//"
|
||||
|
||||
# MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md054.md
|
||||
MD054:
|
||||
# Allow autolinks
|
||||
autolink: true
|
||||
# Allow inline links and images
|
||||
inline: true
|
||||
# Allow full reference links and images
|
||||
full: true
|
||||
# Allow collapsed reference links and images
|
||||
collapsed: true
|
||||
# Allow shortcut reference links and images
|
||||
shortcut: true
|
||||
# Allow URLs as inline links
|
||||
url_inline: true
|
||||
|
||||
# MD055/table-pipe-style : Table pipe style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md055.md
|
||||
MD055:
|
||||
# Table pipe style
|
||||
style: "consistent"
|
||||
|
||||
# MD056/table-column-count : Table column count : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md056.md
|
||||
MD056: true
|
|
@ -12,7 +12,7 @@ repos:
|
|||
- id: add-trailing-comma
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.1
|
||||
rev: v0.6.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
|
81
README.md
81
README.md
|
@ -1,8 +1,6 @@
|
|||
<div align="center">
|
||||
<img src="docs/resources/tux.gif" width=128 height=128></img>
|
||||
<h1>Tux</h1>
|
||||
<h3><b>A Discord bot for the All Things Linux Discord server</b></h3>
|
||||
</div>
|
||||
# Tux
|
||||
|
||||
## A Discord bot for the All Things Linux Discord server
|
||||
|
||||
<div align="center">
|
||||
<p align="center">
|
||||
|
@ -12,21 +10,23 @@
|
|||
<img alt="Repo size" src="https://img.shields.io/github/repo-size/allthingslinux/tux?style=for-the-badge&logo=github&color=FAB387&logoColor=FAB387&labelColor=302D41"/></a>
|
||||
<a href="https://github.com/allthingslinux/tux/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/allthingslinux/tux?style=for-the-badge&logo=githubactions&color=F9E2AF&logoColor=F9E2AF&labelColor=302D41"></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html">
|
||||
<img alt="License" src="https://img.shields.io/github/license/allthingslinux/tux?style=for-the-badge&logo=gitbook&color=A6E3A1&logoColor=A6E3A1&labelColor=302D41"></a>
|
||||
<a href="https://discord.gg/linux">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/1172245377395728464?style=for-the-badge&logo=discord&color=B4BEFE&logoColor=B4BEFE&labelColor=302D41"></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
# NOTE: This bot (without plenty of tweaking) is not ready for multi-server use, we recommend against using it until it is more complete
|
||||
> [!WARNING]
|
||||
**This bot (without plenty of tweaking) is not ready for production use, we suggest against using it until announced. Join our support server: [atl.dev](https://discord.gg/gpmSjcjQxg) for more info!**
|
||||
|
||||
## About
|
||||
|
||||
Tux is a Discord bot for the All Things Linux Discord server. It is designed to provide a variety of features to the server, including moderation, support, utility, and various fun commands. The bot is written in Python using the discord.py library.
|
||||
Tux is an all in one Discord bot for the All Things Linux Discord server.
|
||||
|
||||
It is designed to provide a variety of features to the server, including moderation, support, utility, and various fun commands.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Python 3.12 alongside the Discord.py library
|
||||
- Poetry for dependency management
|
||||
- Docker and Docker Compose for development and deployment
|
||||
- Strict typing with Pyright and type hints
|
||||
|
@ -36,8 +36,10 @@ Tux is a Discord bot for the All Things Linux Discord server. It is designed to
|
|||
- Justfile for easy CLI commands
|
||||
- Beautiful logging with Loguru
|
||||
- Exception handling with Sentry
|
||||
- Request handling with HTTPX
|
||||
|
||||
## Bot Features
|
||||
|
||||
- Asynchronous codebase
|
||||
- Hybrid command system with both slash commands and traditional commands
|
||||
- Cog loading system with hot reloading
|
||||
|
@ -51,71 +53,100 @@ Tux is a Discord bot for the All Things Linux Discord server. It is designed to
|
|||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.12
|
||||
- [Poetry](https://python-poetry.org/docs/)
|
||||
- Optional: [Docker](https://docs.docker.com/get-docker/) if you want to run the bot in a container.
|
||||
- Optional: [Docker Compose](https://docs.docker.com/compose/install/) if you want to define the container environment in a `docker-compose.yml` file.
|
||||
- Optional: [Just](https://github.com/casey/just/) if you want to use the Justfile for easy CLI commands.
|
||||
- [Supabase](https://supabase.io/)
|
||||
- Optional: [Docker](https://docs.docker.com/get-docker/)
|
||||
- Optional: [Docker Compose](https://docs.docker.com/compose/install/)
|
||||
- Optional: [Just](https://github.com/casey/just/)
|
||||
|
||||
### Steps to Install
|
||||
|
||||
Assuming you have the prerequisites installed, follow these steps to get started with the development of the project:
|
||||
|
||||
Further detailed instructions can be found in the [development guide](docs/development.md).
|
||||
|
||||
### Steps
|
||||
1. Clone the repository
|
||||
|
||||
|
||||
```bash
|
||||
git clone https://github.com/allthingslinux/tux && cd tux
|
||||
```
|
||||
|
||||
2. Install the dependencies
|
||||
2. Install the project's dependencies
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
3. Activate the virtual environment
|
||||
|
||||
```bash
|
||||
poetry shell
|
||||
```
|
||||
|
||||
4. Install the pre-commit hooks
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
5. Generate the prisma client
|
||||
|
||||
```bash
|
||||
prisma generate
|
||||
```
|
||||
|
||||
6. Copy the `.env.example` file to `.env` and fill in the required values
|
||||
Currently, you will need to have a Supabase database set up and the URL set in the `DATABASE_URL` environment variable.
|
||||
|
||||
In the future, we will provide a way to use a local database. We can provide a dev database on request.
|
||||
|
||||
6. Copy the `.env.example` file to `.env` and fill in the required values.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
7. Copy the `config/settings.json.example` file to `config/settings.json` and fill in the required values
|
||||
You'll need to fill in your Discord bot token here, as well as the Sentry DSN if you want to use Sentry for error tracking.
|
||||
|
||||
We offer dev tokens on request in our Discord server.
|
||||
|
||||
7. Copy the `config/settings.yml.example` file to `config/settings.yml` and fill in the required values.
|
||||
|
||||
```bash
|
||||
cp config/settings.json.example config/settings.json
|
||||
cp config/settings.yml.example config/settings.yml
|
||||
```
|
||||
|
||||
8. Run the bot
|
||||
Be sure to add your Discord user ID to the `BOT_OWNER` key in the settings file.
|
||||
|
||||
You can also add your custom prefix here.
|
||||
|
||||
8. Start the bot!
|
||||
|
||||
```bash
|
||||
poetry run python tux/main.py
|
||||
```
|
||||
|
||||
9. Run the sync command in the server to sync the slash command tree.
|
||||
```
|
||||
|
||||
```bash
|
||||
{prefix}dev sync <server id>
|
||||
```
|
||||
|
||||
## Development Notes
|
||||
|
||||
> [!NOTE]
|
||||
> Make sure to add your discord ID to the sys admin list if you are testing locally.
|
||||
> [!NOTE]
|
||||
Make sure to add your discord ID to the sys admin list if you are testing locally.
|
||||
|
||||
> [!NOTE]
|
||||
> Make sure to set the prisma schema database ENV variable to the DEV database URL.
|
||||
Make sure to set the prisma schema database ENV variable to the DEV database URL.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the terms of the The GNU General Public License v3.0. See the [LICENSE](LICENSE.md) file for details.
|
||||
This project is licensed under the terms of the The GNU General Public License v3.0.
|
||||
|
||||
See [LICENSE](LICENSE.md) for details.
|
||||
|
||||
## Metrics
|
||||
|
||||
![Alt](https://repobeats.axiom.co/api/embed/cd24c48127e0b6fbc9467711d6d4bd74b30ff8d2.svg "Repobeats analytics image")
|
||||
![Alt](https://repobeats.axiom.co/api/embed/b988ba04401b7c68edf9def00f5132cd2a7f3735.svg "Repobeats analytics image")
|
||||
|
|
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 3.4 MiB |
BIN
assets/emojis/tux_notify.png
Normal file
BIN
assets/emojis/tux_notify.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7 KiB |
BIN
assets/emojis/tux_tag.png
Normal file
BIN
assets/emojis/tux_tag.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 30 KiB |
|
@ -1,58 +0,0 @@
|
|||
{
|
||||
"PREFIX": {
|
||||
"PROD": "$",
|
||||
"DEV": "$"
|
||||
},
|
||||
"ROLES": {
|
||||
"ADMIN": 123456789012345679,
|
||||
"MOD": 123456789012345679,
|
||||
"JR_MOD": 123456789012345679,
|
||||
"OWNER": 123456789012345679,
|
||||
"TESTING": 123456789012345679
|
||||
},
|
||||
"USER_IDS": {
|
||||
"SYSADMINS": [
|
||||
123456789012345679,
|
||||
123456789012345679
|
||||
],
|
||||
"BOT_OWNER": 123456789012345679
|
||||
},
|
||||
"TEMPVC_CATEGORY_ID": 1235096247442870292,
|
||||
"TEMPVC_CHANNEL_ID": 1235096247442870292,
|
||||
"LOG_CHANNELS": {
|
||||
"AUDIT": 1235096271350399076,
|
||||
"MOD": 1235096291672068106,
|
||||
"REPORTS": 1235096305160814652,
|
||||
"GATE": 1235096247442870292,
|
||||
"DEV": 1235095919788167269,
|
||||
"PRIVATE": 1235108340791513129
|
||||
},
|
||||
"EMBED_COLORS": {
|
||||
"DEFAULT": 16044058,
|
||||
"INFO": 12634869,
|
||||
"WARNING": 16634507,
|
||||
"ERROR": 16067173,
|
||||
"SUCCESS": 10407530,
|
||||
"POLL": 14724968,
|
||||
"CASE": 16217742,
|
||||
"NOTE": 16752228
|
||||
},
|
||||
"EMBED_ICONS": {
|
||||
"DEFAULT": "https://i.imgur.com/owW4EZk.png",
|
||||
"INFO": "https://i.imgur.com/8GRtR2G.png",
|
||||
"SUCCESS": "https://i.imgur.com/JsNbN7D.png",
|
||||
"ERROR": "https://i.imgur.com/zZjuWaU.png",
|
||||
"CASE": "https://i.imgur.com/c43cwnV.png",
|
||||
"NOTE": "https://i.imgur.com/VqPFbil.png",
|
||||
"POLL": "https://i.imgur.com/pkPeG5q.png",
|
||||
"ACTIVE_CASE": "https://github.com/allthingslinux/tux/blob/main/assets/embeds/active_case.png?raw=true",
|
||||
"INACTIVE_CASE": "https://github.com/allthingslinux/tux/blob/main/assets/embeds/inactive_case.png?raw=true",
|
||||
"ADD": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/added.png?raw=true",
|
||||
"REMOVE": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/removed.png?raw=true",
|
||||
"BAN": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/ban.png?raw=true",
|
||||
"JAIL": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/jail.png?raw=true",
|
||||
"KICK": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/kick.png?raw=true",
|
||||
"TIMEOUT": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/timeout.png?raw=true",
|
||||
"WARN": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/warn.png?raw=true"
|
||||
}
|
||||
}
|
42
config/settings.yml.example
Normal file
42
config/settings.yml.example
Normal file
|
@ -0,0 +1,42 @@
|
|||
# config/settings.yml
|
||||
|
||||
PREFIX:
|
||||
PROD: "$"
|
||||
DEV: "~"
|
||||
|
||||
USER_IDS:
|
||||
SYSADMINS:
|
||||
- 123456789012345679
|
||||
- 123456789012345679
|
||||
BOT_OWNER: 123456789012345679
|
||||
|
||||
TEMPVC_CATEGORY_ID: 123456789012345679
|
||||
TEMPVC_CHANNEL_ID: 123456789012345679
|
||||
|
||||
EMBED_COLORS:
|
||||
DEFAULT: 16044058
|
||||
INFO: 12634869
|
||||
WARNING: 16634507
|
||||
ERROR: 16067173
|
||||
SUCCESS: 10407530
|
||||
POLL: 14724968
|
||||
CASE: 16217742
|
||||
NOTE: 16752228
|
||||
|
||||
EMBED_ICONS:
|
||||
DEFAULT: "https://i.imgur.com/owW4EZk.png"
|
||||
INFO: "https://i.imgur.com/8GRtR2G.png"
|
||||
SUCCESS: "https://i.imgur.com/JsNbN7D.png"
|
||||
ERROR: "https://i.imgur.com/zZjuWaU.png"
|
||||
CASE: "https://i.imgur.com/c43cwnV.png"
|
||||
NOTE: "https://i.imgur.com/VqPFbil.png"
|
||||
POLL: "https://i.imgur.com/pkPeG5q.png"
|
||||
ACTIVE_CASE: "https://github.com/allthingslinux/tux/blob/main/assets/embeds/active_case.png?raw=true"
|
||||
INACTIVE_CASE: "https://github.com/allthingslinux/tux/blob/main/assets/embeds/inactive_case.png?raw=true"
|
||||
ADD: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/added.png?raw=true"
|
||||
REMOVE: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/removed.png?raw=true"
|
||||
BAN: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/ban.png?raw=true"
|
||||
JAIL: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/jail.png?raw=true"
|
||||
KICK: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/kick.png?raw=true"
|
||||
TIMEOUT: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/timeout.png?raw=true"
|
||||
WARN: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/warn.png?raw=true"
|
|
@ -1,6 +1,8 @@
|
|||
# Project Documentation
|
||||
# Project Documentation
|
||||
|
||||
This document outlines the essential commands and workflows needed for the installation, development, and management of this project. Each section provides relevant commands and instructions for specific tasks.
|
||||
This document outlines the essential commands and workflows needed for the installation, development, and management of this project.
|
||||
|
||||
Each section provides relevant commands and instructions for specific tasks.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
@ -116,4 +118,4 @@ git commit -m "Your commit message"
|
|||
|
||||
# Push changes to the remote repository.
|
||||
git push
|
||||
```
|
||||
```
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
- Moderation
|
||||
- Utility
|
||||
|
||||
|
||||
## Commands
|
||||
|
||||
### Admin
|
||||
|
@ -94,4 +93,4 @@
|
|||
- `remindme`
|
||||
- `snippets`
|
||||
- `tldr`
|
||||
- `wiki`
|
||||
- `wiki`
|
||||
|
|
|
@ -2,17 +2,13 @@
|
|||
|
||||
## Introduction
|
||||
|
||||
This document is intended to provide a guide for developers who want to contribute to the development of the project. It is assumed that the reader has a basic understanding of the project and its goals and has a working knowledge of the tools and technologies used in the project.
|
||||
This document is intended to provide a guide for developers who want to contribute to the development of the project.
|
||||
|
||||
It is assumed that the reader has a basic understanding of the project and its goals as well as the tools and technologies used in the project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started with the development of the project, you will need to have the following tools and technologies installed on your system:
|
||||
|
||||
- Python 3.12
|
||||
- [Poetry](https://python-poetry.org/docs/)
|
||||
- Optional: [Docker](https://docs.docker.com/get-docker/) if you want to run the bot in a container.
|
||||
- Optional: [Docker Compose](https://docs.docker.com/compose/install/) if you want to define the container environment in a `docker-compose.yml` file.
|
||||
- Optional: [Just](https://github.com/casey/just/) if you want to use the Justfile for easy CLI commands.
|
||||
To get started with the development of the project, refer to the installation instructions in the project [README](../README.md).
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -53,11 +49,13 @@ The project is structured as follows:
|
|||
- `help.py`: The help command class definition.
|
||||
|
||||
### Configuration
|
||||
- `.env.example`: The example environment file containing the environment variables required for the bot.
|
||||
|
||||
- `.env`: The environment file containing the secret environment variables for the bot.
|
||||
- `config/`: The config directory containing the configuration files for the bot.
|
||||
- `settings.json`: The settings file containing the bot settings and configuration.
|
||||
- `settings.yml`: The settings file containing the bot settings and configuration.
|
||||
|
||||
### Documentation
|
||||
|
||||
- `docs/`: The documentation directory containing the project documentation.
|
||||
- `CONTRIBUTING.md`: The contributing guidelines for the project.
|
||||
- `README.md`: The project README file containing the project overview and installation instructions.
|
||||
|
@ -66,6 +64,7 @@ The project is structured as follows:
|
|||
- `LICENSE.md`: The license file containing the project license information.
|
||||
|
||||
### Development
|
||||
|
||||
- `pyproject.toml`: The Poetry configuration file containing the project metadata and dependencies for the bot.
|
||||
- `Dockerfile`: The Dockerfile containing the container configuration for the bot.
|
||||
- `docker-compose.yml`: The Docker Compose file containing the container environment configuration for the bot.
|
||||
|
@ -73,13 +72,37 @@ The project is structured as follows:
|
|||
- `.gitignore`: The Git ignore file containing the files and directories to be ignored by Git.
|
||||
|
||||
### CI/CD
|
||||
|
||||
- `.pre-commit-config.yaml`: The pre-commit configuration file containing the pre-commit hooks for the bot.
|
||||
- `.github/workflows/`: The GitHub Actions directory containing the CI/CD workflows for the bot.
|
||||
- `renovate.json`: The Renovate configuration file containing the dependency update settings for the bot.
|
||||
|
||||
## Cogs Primer
|
||||
|
||||
TODO: Add cogs primer
|
||||
There comes a point in your bot’s development when you want to organize a collection of commands, listeners, and some state into one class. Cogs allow you to do just that.
|
||||
|
||||
It should be noted that cogs are typically used alongside with Extensions.
|
||||
|
||||
An extension at its core is a python file with an entry point called setup. This setup function must be a Python coroutine. It takes a single parameter of the Bot that loads the extension.
|
||||
|
||||
With regards to Tux, we typically define one cog per extension. This allows us to keep our code organized and modular.
|
||||
|
||||
Furthermore, we have a `CogLoader` class that loads our cogs (technically, extensions) from the `cogs` directory and registers them with the bot at startup.
|
||||
|
||||
### Cog Essentials
|
||||
|
||||
- Each cog is a Python class that subclasses commands.Cog.
|
||||
- Every regular command or "prefix" is marked with the `@commands.command()` decorator.
|
||||
- Every app or "slash" command is marked with the `@app_commands.command()` decorator.
|
||||
- Every hybrid command is marked with the `@commands.hybrid_command()` decorator.
|
||||
- Every listener is marked with the `@commands.Cog.listener()` decorator.
|
||||
|
||||
tl;dr - Extensions are imported "modules", cogs are classes that are subclasses of `commands.Cog`.
|
||||
|
||||
Referance:
|
||||
|
||||
- [discord.py - Cogs](https://discordpy.readthedocs.io/en/stable/ext/commands/cogs.html)
|
||||
- [discord.py - Extensions](https://discordpy.readthedocs.io/en/stable/ext/commands/extensions.html)
|
||||
|
||||
## Database Primer
|
||||
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
# Permissions Management
|
||||
|
||||
Tux employs a level-based permissions system to control command execution. Each command is associated with a specific permission level, ensuring that only users with the necessary clearance can execute it.
|
||||
Tux employs a level-based permissions system to control command execution.
|
||||
|
||||
Each command is associated with a specific permission level, ensuring that only users with the necessary clearance can execute it.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
When setting up Tux for a new server, the server owner can assign one or multiple roles to each permission level. Users then inherit the highest permission level from their assigned roles. For instance, if a user has one role with a permission level of 2 and another with a level of 3, their effective permission level will be 3.
|
||||
When setting up Tux for a new server, the server owner can assign one or multiple roles to each permission level. Users then inherit the highest permission level from their assigned roles.
|
||||
|
||||
For instance, if a user has one role with a permission level of 2 and another with a level of 3, their effective permission level will be 3.
|
||||
|
||||
## Advantages
|
||||
|
||||
The level-based system allows Tux to manage command execution efficiently across different servers. It offers a more flexible solution than just relying on Discord's built-in permissions, avoiding the need to hardcode permissions into the bot. This flexibility makes it easier to modify permissions without changing the bot’s underlying code, accommodating servers with custom role names seamlessly.
|
||||
The level-based system allows Tux to manage command execution efficiently across different servers.
|
||||
|
||||
It offers a more flexible solution than just relying on Discord's built-in permissions, avoiding the need to hardcode permissions into the bot.
|
||||
|
||||
This flexibility makes it easier to modify permissions without changing the bot’s underlying code, accommodating servers with custom role names seamlessly.
|
||||
|
||||
## Available Permission Levels
|
||||
|
||||
|
@ -25,4 +33,5 @@ Below is the hierarchy of permission levels available in Tux:
|
|||
- **8: Sys Admin** (User ID list in `config.json`)
|
||||
- **9: Bot Owner** (User ID in `config.json`)
|
||||
|
||||
By leveraging these permission levels, Tux provides a robust and adaptable way to manage who can execute specific commands, making it suitable for various server environments.
|
||||
By leveraging these permission levels, Tux provides a robust and adaptable way to manage who can execute specific commands,
|
||||
making it suitable for various server environments.
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
## Overview
|
||||
|
||||
Services within the context of this bot are background tasks that run continuously and provide various functionalities to the server. They are typically not directly interacted with by users, but rather provide additional features to the server that are not covered by commands.
|
||||
Services within the context of this bot are background tasks that run continuously and provide various functionalities to the server.
|
||||
|
||||
They are typically not directly interacted with by users, but rather provide additional features to the server that are not covered by commands.
|
||||
|
||||
## Available Services
|
||||
|
||||
|
@ -15,4 +17,4 @@ Services within the context of this bot are background tasks that run continuous
|
|||
- **Auto Welcome**: Sends a welcome message to new members when they join the server.
|
||||
- **Auto Role**: Assigns a role to new members when they join the server.
|
||||
- **Auto Mod**: Automatically moderates the server by enforcing rules and restrictions
|
||||
- **Auto Unban**: Automatically unbans users after a specified duration
|
||||
- **Auto Unban**: Automatically unbans users after a specified duration
|
||||
|
|
134
poetry.lock
generated
134
poetry.lock
generated
|
@ -190,6 +190,21 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin
|
|||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||
trio = ["trio (>=0.23)"]
|
||||
|
||||
[[package]]
|
||||
name = "astunparse"
|
||||
version = "1.6.3"
|
||||
description = "An AST unparser for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"},
|
||||
{file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.6.1,<2.0"
|
||||
wheel = ">=0.23.0,<1.0"
|
||||
|
||||
[[package]]
|
||||
name = "asynctempfile"
|
||||
version = "0.5.0"
|
||||
|
@ -237,6 +252,17 @@ files = [
|
|||
[package.extras]
|
||||
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "braceexpand"
|
||||
version = "0.1.7"
|
||||
description = "Bash-style brace expansion for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "braceexpand-0.1.7-py2.py3-none-any.whl", hash = "sha256:91332d53de7828103dcae5773fb43bc34950b0c8160e35e0f44c4427a3b85014"},
|
||||
{file = "braceexpand-0.1.7.tar.gz", hash = "sha256:e6e539bd20eaea53547472ff94f4fb5c3d3bf9d0a89388c4b56663aba765f705"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairocffi"
|
||||
version = "1.7.1"
|
||||
|
@ -881,6 +907,23 @@ files = [
|
|||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "import-expression"
|
||||
version = "1.1.5"
|
||||
description = "Parses a superset of Python allowing for inline module import expressions"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "import_expression-1.1.5-py3-none-any.whl", hash = "sha256:f60c3765dbf2f41928b9c6ef79d632209b6705fc8f30e281ed1a492ed026b10f"},
|
||||
{file = "import_expression-1.1.5.tar.gz", hash = "sha256:9959588fcfc8dcb144a0725176cfef6c28c7db1fc2d683625025e687516d40c1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
astunparse = ">=1.6.3,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.4"
|
||||
|
@ -898,6 +941,31 @@ MarkupSafe = ">=2.0"
|
|||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "jishaku"
|
||||
version = "2.5.2"
|
||||
description = "A discord.py extension including useful tools for bot development and debugging."
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "jishaku-2.5.2-py3-none-any.whl", hash = "sha256:87f34942ee44865f5ce08e36723b7c74a313d8a13a4db8a6b7cc12618cc3496c"},
|
||||
{file = "jishaku-2.5.2.tar.gz", hash = "sha256:56d38c333036e37481df5e3c9e81d6033b5097738f0d171a81e2752124f0df5c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
braceexpand = ">=0.1.7"
|
||||
click = ">=8.0.1"
|
||||
import-expression = ">=1.0.0,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
discordpy = ["discord.py (>=1.7.3)"]
|
||||
docs = ["Sphinx (>=4.4.0)", "sphinxcontrib-trio (>=1.1.2)"]
|
||||
procinfo = ["psutil (>=5.8.0)"]
|
||||
profiling = ["line-profiler (>=3.5.1)"]
|
||||
publish = ["Jinja2 (>=3.0.3)"]
|
||||
test = ["coverage (>=6.3.2)", "flake8 (>=4.0.1)", "isort (>=5.10.1)", "pylint (>=2.11.1)", "pytest (>=7.0.1)", "pytest-asyncio (>=0.18.1)", "pytest-cov (>=3.0.0)", "pytest-mock (>=3.7.0)"]
|
||||
voice = ["yt-dlp (>=2022.3.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.2"
|
||||
|
@ -1059,13 +1127,13 @@ pyyaml = ">=5.1"
|
|||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.5.31"
|
||||
version = "9.5.32"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocs_material-9.5.31-py3-none-any.whl", hash = "sha256:1b1f49066fdb3824c1e96d6bacd2d4375de4ac74580b47e79ff44c4d835c5fcb"},
|
||||
{file = "mkdocs_material-9.5.31.tar.gz", hash = "sha256:31833ec664772669f5856f4f276bf3fdf0e642a445e64491eda459249c3a1ca8"},
|
||||
{file = "mkdocs_material-9.5.32-py3-none-any.whl", hash = "sha256:f3704f46b63d31b3cd35c0055a72280bed825786eccaf19c655b44e0cd2c6b3f"},
|
||||
{file = "mkdocs_material-9.5.32.tar.gz", hash = "sha256:38ed66e6d6768dde4edde022554553e48b2db0d26d1320b19e2e2b9da0be1120"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1649,13 +1717,13 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
|
|||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.376"
|
||||
version = "1.1.377"
|
||||
description = "Command line wrapper for pyright"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyright-1.1.376-py3-none-any.whl", hash = "sha256:0f2473b12c15c46b3207f0eec224c3cea2bdc07cd45dd4a037687cbbca0fbeff"},
|
||||
{file = "pyright-1.1.376.tar.gz", hash = "sha256:bffd63b197cd0810395bb3245c06b01f95a85ddf6bfa0e5644ed69c841e954dd"},
|
||||
{file = "pyright-1.1.377-py3-none-any.whl", hash = "sha256:af0dd2b6b636c383a6569a083f8c5a8748ae4dcde5df7914b3f3f267e14dd162"},
|
||||
{file = "pyright-1.1.377.tar.gz", hash = "sha256:aabc30fedce0ded34baa0c49b24f10e68f4bfc8f68ae7f3d175c4b0f256b4fcf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1919,29 +1987,29 @@ pyasn1 = ">=0.1.3"
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9"},
|
||||
{file = "ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae"},
|
||||
{file = "ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850"},
|
||||
{file = "ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf"},
|
||||
{file = "ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9"},
|
||||
{file = "ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5"},
|
||||
{file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024"},
|
||||
{file = "ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18"},
|
||||
{file = "ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e"},
|
||||
{file = "ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1"},
|
||||
{file = "ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631"},
|
||||
{file = "ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9"},
|
||||
{file = "ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15"},
|
||||
{file = "ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb"},
|
||||
{file = "ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4"},
|
||||
{file = "ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b"},
|
||||
{file = "ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014"},
|
||||
{file = "ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436"},
|
||||
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
|
||||
{file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
|
||||
{file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
|
||||
{file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
|
||||
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2204,6 +2272,20 @@ files = [
|
|||
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wheel"
|
||||
version = "0.44.0"
|
||||
description = "A built-package format for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"},
|
||||
{file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.1.0"
|
||||
|
@ -2324,4 +2406,4 @@ multidict = ">=4.0"
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.12,<4"
|
||||
content-hash = "30272c710183da26e169ea88fd077632a69fac2a891ba3f9a862bc815f368141"
|
||||
content-hash = "e773f58b7548a102f8e9ebc152ec2390d1149a549fe9f77e9c248b37e896f56a"
|
||||
|
|
|
@ -28,11 +28,13 @@ enum CaseType {
|
|||
HACKBAN
|
||||
TEMPBAN
|
||||
KICK
|
||||
SNIPPETBAN
|
||||
TIMEOUT
|
||||
UNTIMEOUT
|
||||
WARN
|
||||
JAIL
|
||||
UNJAIL
|
||||
SNIPPETUNBAN
|
||||
}
|
||||
|
||||
// Docs: https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-models
|
||||
|
@ -106,6 +108,8 @@ model Snippet {
|
|||
snippet_created_at DateTime @default(now())
|
||||
guild_id BigInt
|
||||
guild Guild @relation(fields: [guild_id], references: [guild_id])
|
||||
uses BigInt @default(0)
|
||||
locked Boolean @default(false)
|
||||
|
||||
@@unique([snippet_name, guild_id])
|
||||
@@index([snippet_name, guild_id])
|
||||
|
|
|
@ -27,6 +27,7 @@ pynacl = "^1.5.0"
|
|||
pyright = "^1.1.358"
|
||||
python = ">=3.12,<4"
|
||||
python-dotenv = "^1.0.1"
|
||||
pyyaml = "^6.0.2"
|
||||
reactionmenu = "^3.1.7"
|
||||
rsa = "^4.9"
|
||||
ruff = "^0.6.0"
|
||||
|
@ -34,6 +35,8 @@ sentry-sdk = {extras = ["httpx", "loguru"], version = "^2.7.0"}
|
|||
types-aiofiles = "^24.1.0.20240626"
|
||||
types-psutil = "^6.0.0.20240621"
|
||||
typing-extensions = "^4.12.2"
|
||||
jishaku = "^2.5.2"
|
||||
pytz = "^2024.1"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = "^9.5.30"
|
||||
|
@ -86,8 +89,7 @@ target-version = "py312"
|
|||
[tool.ruff.lint]
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
fixable = ["ALL"]
|
||||
ignore = ["E501", "N814"]
|
||||
|
||||
ignore = ["E501", "N814", "PLR0913", "PLR2004"]
|
||||
select = [
|
||||
"I", # isort
|
||||
"E", # pycodestyle-error
|
||||
|
@ -96,6 +98,8 @@ select = [
|
|||
"N", # pep8-naming
|
||||
"TRY", # tryceratops
|
||||
"UP", # pyupgrade
|
||||
"FURB", # refurb
|
||||
"PL", # pylint
|
||||
"B", # flake8-bugbear
|
||||
"SIM", # flake8-simplify
|
||||
"ASYNC", # flake8-async
|
||||
|
|
|
@ -32,6 +32,8 @@ class Tux(commands.Bot):
|
|||
logger.critical(f"An error occurred while connecting to the database: {e}")
|
||||
return
|
||||
|
||||
# Load Jishaku for debugging
|
||||
await self.load_extension("jishaku")
|
||||
# Load cogs via CogLoader
|
||||
await self.load_cogs()
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ from discord.ext import commands
|
|||
from loguru import logger
|
||||
|
||||
from tux.utils import checks
|
||||
from tux.utils.constants import Constants as CONST
|
||||
from tux.utils.embeds import EmbedCreator
|
||||
|
||||
|
||||
|
@ -48,7 +47,7 @@ class Eval(commands.Cog):
|
|||
usage="eval [expression]",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(9)
|
||||
@checks.has_pl(8) # sysadmin or higher
|
||||
async def eval(self, ctx: commands.Context[commands.Bot], *, cmd: str) -> None:
|
||||
"""
|
||||
Evaluate a Python expression. (Owner only)
|
||||
|
@ -61,10 +60,15 @@ class Eval(commands.Cog):
|
|||
The Python expression to evaluate.
|
||||
"""
|
||||
|
||||
# Check if the user is the bot owner
|
||||
if ctx.author.id != CONST.BOT_OWNER_ID:
|
||||
# Check if the user is in the discord.py owner_ids list in the bot instance
|
||||
if self.bot.owner_ids is None:
|
||||
logger.warning("Bot owner IDs are not set.")
|
||||
await ctx.send("Bot owner IDs are not set. Better luck next time!", ephemeral=True, delete_after=30)
|
||||
return
|
||||
|
||||
if ctx.author.id not in self.bot.owner_ids:
|
||||
logger.warning(
|
||||
f"{ctx.author} tried to run eval but is not the bot owner. (Owner ID: {self.bot.owner_id}, User ID: {ctx.author.id})",
|
||||
f"{ctx.author} tried to run eval but is not the bot owner. (User ID: {ctx.author.id})",
|
||||
)
|
||||
await ctx.send("You are not the bot owner. Better luck next time!", ephemeral=True, delete_after=30)
|
||||
return
|
||||
|
|
|
@ -54,7 +54,7 @@ distro_ids = [
|
|||
[1182152672447569972, "_slackware"],
|
||||
[1178347123905929316, "_popos"],
|
||||
[1175177750143848520, "_kisslinux"],
|
||||
[1180570700734546031, "tux"],
|
||||
[1180570700734546031, "_lfs"],
|
||||
[1191106506276479067, "_garuda"],
|
||||
[1192177499413684226, "_asahi"],
|
||||
[1207599112585740309, "_fedoraatomic"],
|
||||
|
|
|
@ -1,83 +1,83 @@
|
|||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
from tux.utils.embeds import EmbedCreator
|
||||
|
||||
|
||||
class Info(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
|
||||
info = app_commands.Group(name="info", description="Information commands.")
|
||||
@commands.hybrid_group(
|
||||
name="info",
|
||||
aliases=["i"],
|
||||
usage="info <subcommand>",
|
||||
)
|
||||
async def info(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
Information commands.
|
||||
|
||||
@info.command(name="server")
|
||||
async def server(self, interaction: discord.Interaction) -> None:
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The discord context object.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help("info")
|
||||
|
||||
@info.command(
|
||||
name="server",
|
||||
aliases=["s"],
|
||||
usage="info server",
|
||||
)
|
||||
async def server(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
Show information about the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The discord interaction object.
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The discord context object.
|
||||
"""
|
||||
if not interaction.guild:
|
||||
|
||||
if not ctx.guild:
|
||||
return
|
||||
guild = ctx.guild
|
||||
|
||||
guild = interaction.guild
|
||||
owner = str(guild.owner) if guild.owner else "Unknown"
|
||||
|
||||
embed = EmbedCreator.create_info_embed(
|
||||
title=guild.name,
|
||||
description="Here is some information about the server.",
|
||||
interaction=interaction,
|
||||
embed = discord.Embed(
|
||||
title=ctx.guild.name,
|
||||
description=guild.description or "No description available.",
|
||||
color=discord.Color.blurple(),
|
||||
)
|
||||
|
||||
embed.add_field(name="Members", value=str(guild.member_count))
|
||||
bots = sum(member.bot for member in guild.members if member.bot)
|
||||
embed.add_field(name="Bots", value=str(bots))
|
||||
embed.add_field(name="Boosts", value=str(guild.premium_subscription_count))
|
||||
embed.add_field(name="Vanity URL", value=str(guild.vanity_url_code or "None"))
|
||||
embed.add_field(name="Owner", value=owner)
|
||||
embed.add_field(name="Created", value=guild.created_at.strftime("%d/%m/%Y"))
|
||||
embed.add_field(name="ID", value=str(guild.id))
|
||||
embed.set_author(name="Server Information", icon_url=guild.icon)
|
||||
embed.add_field(name="Owner", value=str(guild.owner.mention) if guild.owner else "Unknown")
|
||||
embed.add_field(name="Vanity URL", value=guild.vanity_url_code or "None")
|
||||
embed.add_field(name="Boosts", value=guild.premium_subscription_count)
|
||||
embed.add_field(name="Text Channels", value=len(guild.text_channels))
|
||||
embed.add_field(name="Voice Channels", value=len(guild.voice_channels))
|
||||
embed.add_field(name="Forum Channels", value=len(guild.forums))
|
||||
embed.add_field(name="Emojis", value=f"{len(guild.emojis)}/{guild.emoji_limit}")
|
||||
embed.add_field(name="Stickers", value=f"{len(guild.stickers)}/{guild.sticker_limit}")
|
||||
embed.add_field(name="Roles", value=len(guild.roles))
|
||||
embed.add_field(name="Humans", value=sum(not member.bot for member in guild.members))
|
||||
embed.add_field(name="Bots", value=sum(member.bot for member in guild.members))
|
||||
embed.add_field(name="Bans", value=len([entry async for entry in guild.bans(limit=2000)]))
|
||||
embed.set_footer(text=f"ID: {guild.id} | Created: {guild.created_at.strftime('%B %d, %Y')}")
|
||||
|
||||
embed.set_thumbnail(url=guild.icon)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@info.command(name="tux", description="Shows information about Tux.")
|
||||
async def tux(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Show information about Tux.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The discord interaction object.
|
||||
"""
|
||||
|
||||
embed = EmbedCreator.create_info_embed(
|
||||
title="Tux",
|
||||
description="Tux is a Discord bot written in Python using discord.py.",
|
||||
interaction=interaction,
|
||||
)
|
||||
embed.add_field(
|
||||
name="GitHub",
|
||||
value="[View the source code](https://github.com/allthingslinux/tux)",
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@info.command(name="member", description="Shows information about a member.")
|
||||
async def member(self, interaction: discord.Interaction, member: discord.Member) -> None:
|
||||
@info.command(
|
||||
name="member",
|
||||
aliases=["m", "user", "u"],
|
||||
usage="info member [member]",
|
||||
)
|
||||
async def member(self, ctx: commands.Context[commands.Bot], member: discord.Member) -> None:
|
||||
"""
|
||||
Show information about a member.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The discord interaction object.
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The discord context object.
|
||||
member : discord.Member
|
||||
The member to get information about.
|
||||
"""
|
||||
|
@ -86,15 +86,16 @@ class Info(commands.Cog):
|
|||
joined = discord.utils.format_dt(member.joined_at, "R") if member.joined_at else "Unknown"
|
||||
created = discord.utils.format_dt(member.created_at, "R") if member.created_at else "Unknown"
|
||||
roles = ", ".join(role.mention for role in member.roles[1:]) if member.roles[1:] else "No roles"
|
||||
|
||||
fetched_member = await self.bot.fetch_user(member.id)
|
||||
|
||||
embed = EmbedCreator.create_info_embed(
|
||||
embed = discord.Embed(
|
||||
title=member.display_name,
|
||||
description="Here is some information about the member.",
|
||||
interaction=interaction,
|
||||
color=discord.Color.blurple(),
|
||||
)
|
||||
|
||||
embed.set_thumbnail(url=member.display_avatar.url)
|
||||
embed.set_image(url=fetched_member.banner)
|
||||
embed.add_field(name="Bot?", value=bot_status, inline=False)
|
||||
embed.add_field(name="Username", value=member.name, inline=False)
|
||||
embed.add_field(name="ID", value=str(member.id), inline=False)
|
||||
|
@ -102,10 +103,67 @@ class Info(commands.Cog):
|
|||
embed.add_field(name="Registered", value=created, inline=False)
|
||||
embed.add_field(name="Roles", value=roles, inline=False)
|
||||
|
||||
embed.set_thumbnail(url=member.display_avatar.url)
|
||||
embed.set_image(url=fetched_member.banner)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
@info.command(
|
||||
name="roles",
|
||||
aliases=["r"],
|
||||
usage="info roles",
|
||||
)
|
||||
async def roles(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
List all roles in the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The discord context object.
|
||||
"""
|
||||
if not ctx.guild:
|
||||
return
|
||||
|
||||
guild = ctx.guild
|
||||
roles = [role.mention for role in guild.roles]
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Server Roles",
|
||||
description=f"Role list for {guild.name}",
|
||||
color=discord.Color.blurple(),
|
||||
)
|
||||
|
||||
embed.add_field(name="Roles", value=", ".join(roles), inline=False)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@info.command(
|
||||
name="emotes",
|
||||
aliases=["e"],
|
||||
usage="info emotes",
|
||||
)
|
||||
async def emotes(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
List all emotes in the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The discord context object.
|
||||
"""
|
||||
if not ctx.guild:
|
||||
return
|
||||
|
||||
guild = ctx.guild
|
||||
emotes = [str(emote) for emote in guild.emojis]
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Server Emotes",
|
||||
description=f"Emote list for {guild.name}",
|
||||
color=discord.Color.blurple(),
|
||||
)
|
||||
|
||||
embed.add_field(name="Emotes", value=" ".join(emotes) if emotes else "No emotes available", inline=False)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
|
|
|
@ -18,7 +18,7 @@ class Ban(ModerationCogBase):
|
|||
@commands.hybrid_command(
|
||||
name="ban",
|
||||
aliases=["b"],
|
||||
usage="ban [target] <flags>",
|
||||
usage="ban [target] [reason] <purge_days> <silent>",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(3)
|
||||
|
@ -30,7 +30,7 @@ class Ban(ModerationCogBase):
|
|||
flags: BanFlags,
|
||||
) -> None:
|
||||
"""
|
||||
Ban a user from the server.
|
||||
Ban a member from the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
@ -39,7 +39,7 @@ class Ban(ModerationCogBase):
|
|||
target : discord.Member
|
||||
The member to ban.
|
||||
flags : BanFlags
|
||||
The flags for the command. (reason: str, purge_days: int, silent: bool)
|
||||
The flags for the command. (reason: str, purge_days: int (< 7), silent: bool)
|
||||
|
||||
Raises
|
||||
------
|
||||
|
@ -58,7 +58,7 @@ class Ban(ModerationCogBase):
|
|||
return
|
||||
|
||||
try:
|
||||
await self.send_dm(ctx, flags.silent, target, flags.reason, "banned")
|
||||
await self.send_dm(ctx, flags.silent, target, flags.reason, action="banned")
|
||||
await ctx.guild.ban(target, reason=flags.reason, delete_message_days=flags.purge_days)
|
||||
|
||||
except (discord.Forbidden, discord.HTTPException) as e:
|
||||
|
|
|
@ -23,6 +23,8 @@ emojis: dict[str, int] = {
|
|||
"timeout": 1268115809083981886,
|
||||
"warn": 1268115764498399264,
|
||||
"jail": 1268115750392954880,
|
||||
"snippetban": 1275782294363312172, # Placeholder
|
||||
"snippetunban": 1275782294363312172, # Placeholder
|
||||
}
|
||||
|
||||
|
||||
|
@ -47,7 +49,7 @@ class Cases(ModerationCogBase):
|
|||
@cases.command(
|
||||
name="view",
|
||||
aliases=["v", "ls", "list"],
|
||||
usage="cases view <case_number> <flags>",
|
||||
usage="cases view <case_number> <type> <target> <moderator>",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(2)
|
||||
|
@ -83,7 +85,7 @@ class Cases(ModerationCogBase):
|
|||
@cases.command(
|
||||
name="modify",
|
||||
aliases=["m", "edit"],
|
||||
usage="cases modify [case_number] <flags>",
|
||||
usage="cases modify [case_number] <status> <reason>",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(2)
|
||||
|
@ -150,10 +152,7 @@ class Cases(ModerationCogBase):
|
|||
await ctx.send("Case not found.", delete_after=30)
|
||||
return
|
||||
|
||||
target = await commands.MemberConverter().convert(
|
||||
ctx,
|
||||
str(case.case_target_id),
|
||||
) or await commands.UserConverter().convert(ctx, str(case.case_target_id))
|
||||
target = await commands.UserConverter().convert(ctx, str(case.case_target_id))
|
||||
|
||||
await self._handle_case_response(ctx, case, "viewed", case.case_reason, target)
|
||||
|
||||
|
@ -233,10 +232,7 @@ class Cases(ModerationCogBase):
|
|||
await ctx.send("Failed to update case.", delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
target = await commands.MemberConverter().convert(
|
||||
ctx,
|
||||
str(case.case_target_id),
|
||||
) or await commands.UserConverter().convert(ctx, str(case.case_target_id))
|
||||
target = await commands.UserConverter().convert(ctx, str(case.case_target_id))
|
||||
|
||||
await self._handle_case_response(ctx, updated_case, "updated", updated_case.case_reason, target)
|
||||
|
||||
|
@ -266,10 +262,10 @@ class Cases(ModerationCogBase):
|
|||
"""
|
||||
|
||||
if case is not None:
|
||||
moderator = await commands.MemberConverter().convert(
|
||||
ctx,
|
||||
str(case.case_moderator_id),
|
||||
) or await commands.UserConverter().convert(ctx, str(case.case_moderator_id))
|
||||
moderator = ctx.author
|
||||
|
||||
if not isinstance(moderator, discord.Member):
|
||||
moderator = await commands.MemberConverter().convert(ctx, str(case.case_moderator_id))
|
||||
|
||||
fields = self._create_case_fields(moderator, target, reason)
|
||||
|
||||
|
@ -298,7 +294,7 @@ class Cases(ModerationCogBase):
|
|||
cases: list[Case],
|
||||
total_cases: int,
|
||||
) -> None:
|
||||
menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed)
|
||||
menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed, all_can_click=True, delete_on_timeout=True)
|
||||
|
||||
if not cases:
|
||||
embed = discord.Embed(
|
||||
|
@ -314,9 +310,16 @@ class Cases(ModerationCogBase):
|
|||
embed = self._create_case_list_embed(ctx, cases[i : i + cases_per_page], total_cases)
|
||||
menu.add_page(embed)
|
||||
|
||||
menu.add_button(ViewButton.back())
|
||||
menu.add_button(ViewButton.next())
|
||||
menu.add_button(ViewButton.end_session())
|
||||
menu.add_button(
|
||||
ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_FIRST_PAGE, emoji="⏮️"),
|
||||
)
|
||||
menu.add_button(
|
||||
ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_PREVIOUS_PAGE, emoji="⏪"),
|
||||
)
|
||||
menu.add_button(ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_NEXT_PAGE, emoji="⏩"))
|
||||
menu.add_button(
|
||||
ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_LAST_PAGE, emoji="⏭️"),
|
||||
)
|
||||
|
||||
await menu.start()
|
||||
|
||||
|
@ -373,6 +376,8 @@ class Cases(ModerationCogBase):
|
|||
CaseType.WARN: "warn",
|
||||
CaseType.JAIL: "jail",
|
||||
CaseType.UNJAIL: "jail",
|
||||
CaseType.SNIPPETBAN: "snippetban",
|
||||
CaseType.SNIPPETUNBAN: "snippetunban",
|
||||
}
|
||||
emoji_name = emoji_map.get(case_type)
|
||||
if emoji_name is not None:
|
||||
|
@ -384,9 +389,10 @@ class Cases(ModerationCogBase):
|
|||
def _get_case_action_emoji(self, case_type: CaseType) -> discord.Emoji | None:
|
||||
action = (
|
||||
"added"
|
||||
if case_type in [CaseType.BAN, CaseType.KICK, CaseType.TIMEOUT, CaseType.WARN, CaseType.JAIL]
|
||||
if case_type
|
||||
in [CaseType.BAN, CaseType.KICK, CaseType.TIMEOUT, CaseType.WARN, CaseType.JAIL, CaseType.SNIPPETBAN]
|
||||
else "removed"
|
||||
if case_type in [CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL]
|
||||
if case_type in [CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL, CaseType.SNIPPETUNBAN]
|
||||
else None
|
||||
)
|
||||
if action is not None:
|
||||
|
@ -403,12 +409,14 @@ class Cases(ModerationCogBase):
|
|||
case_action_emoji: str,
|
||||
) -> str:
|
||||
case_type_and_action = (
|
||||
f"{case_action_emoji} {case_type_emoji}" if case_action_emoji and case_type_emoji else "?"
|
||||
f"{case_action_emoji} {case_type_emoji}"
|
||||
if case_action_emoji and case_type_emoji
|
||||
else ":interrobang: :interrobang:"
|
||||
)
|
||||
case_date = discord.utils.format_dt(case.case_created_at, "R") if case.case_created_at else "?"
|
||||
case_date = discord.utils.format_dt(case.case_created_at, "R") if case.case_created_at else ":interrobang:"
|
||||
case_number = f"{case.case_number:04d}"
|
||||
|
||||
return f"{case_status_emoji} `{case_number}`\u2002\u2002 {case_type_and_action} \u2002\u2002*{case_date}*\n"
|
||||
return f"{case_status_emoji} `{case_number}`\u2002\u2002 {case_type_and_action} \u2002\u2002__{case_date}__\n"
|
||||
|
||||
def _add_case_to_embed(self, embed: discord.Embed, case: Case) -> None:
|
||||
case_status_emoji = self._format_emoji(self._get_case_status_emoji(case.case_status))
|
||||
|
|
|
@ -12,25 +12,25 @@ class Slowmode(commands.Cog):
|
|||
@commands.hybrid_command(
|
||||
name="slowmode",
|
||||
aliases=["sm"],
|
||||
usage="slowmode [delay] <channel>",
|
||||
usage="slowmode [delay|get] <channel>",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(2)
|
||||
async def slowmode(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
delay: str,
|
||||
action: str,
|
||||
channel: discord.TextChannel | discord.Thread | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Sets slowmode for the current channel or specified channel.
|
||||
Set or get the slowmode for a channel.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
delay : int
|
||||
The slowmode time in seconds, max is 21600.
|
||||
action : str
|
||||
Either 'get' to get the current slowmode or the slowmode time in seconds, max is 21600.
|
||||
channel : discord.TextChannel | discord.Thread | None
|
||||
The channel to set the slowmode in.
|
||||
"""
|
||||
|
@ -38,10 +38,9 @@ class Slowmode(commands.Cog):
|
|||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
# If the channel is not specified, default to the current channe
|
||||
# Default to the current channel if none is specified
|
||||
if channel is None:
|
||||
# Check if the current channel is a text channel
|
||||
if not isinstance(ctx.channel, discord.TextChannel | discord.Thread):
|
||||
if not isinstance(ctx.channel, (discord.TextChannel | discord.Thread)):
|
||||
await ctx.send(
|
||||
"Invalid channel type, must be a text channel or thread.",
|
||||
delete_after=30,
|
||||
|
@ -50,31 +49,49 @@ class Slowmode(commands.Cog):
|
|||
return
|
||||
channel = ctx.channel
|
||||
|
||||
# Unsure of how to type hint this properly as it can be a string or int
|
||||
# and I can't use a Union for the argument because discord.py nagging?
|
||||
try:
|
||||
if delay[-1] in ["s"]:
|
||||
delay = delay[:-1]
|
||||
if delay[-1] == "m":
|
||||
delay = delay[:-1]
|
||||
delay = int(delay) * 60 # type: ignore
|
||||
if action.lower() in {"get", "g"}:
|
||||
try:
|
||||
await ctx.send(
|
||||
f"The slowmode for {channel.mention} is {channel.slowmode_delay} seconds.",
|
||||
delete_after=30,
|
||||
ephemeral=True,
|
||||
)
|
||||
except Exception as error:
|
||||
await ctx.send(f"Failed to get slowmode. Error: {error}", delete_after=30, ephemeral=True)
|
||||
logger.error(f"Failed to get slowmode. Error: {error}")
|
||||
else:
|
||||
delay = action
|
||||
try:
|
||||
if delay[-1] in ["s"]:
|
||||
delay = delay[:-1]
|
||||
if delay[-1] == "m":
|
||||
delay = delay[:-1]
|
||||
delay = int(delay) * 60 # type: ignore
|
||||
|
||||
delay = int(delay) # type: ignore
|
||||
except ValueError:
|
||||
await ctx.send("Invalid delay value, must be an integer.", delete_after=30, ephemeral=True)
|
||||
return
|
||||
delay = int(delay) # type: ignore
|
||||
except ValueError:
|
||||
await ctx.send("Invalid delay value, must be an integer.", delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
if delay < 0 or delay > 21600: # type: ignore
|
||||
await ctx.send("The slowmode delay must be between 0 and 21600 seconds.", delete_after=30, ephemeral=True)
|
||||
return
|
||||
if delay < 0 or delay > 21600: # type: ignore
|
||||
await ctx.send(
|
||||
"The slowmode delay must be between 0 and 21600 seconds.",
|
||||
delete_after=30,
|
||||
ephemeral=True,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await channel.edit(slowmode_delay=delay) # type: ignore
|
||||
await ctx.send(f"Slowmode set to {delay} seconds in {channel.mention}.", delete_after=30, ephemeral=True)
|
||||
try:
|
||||
await channel.edit(slowmode_delay=delay) # type: ignore
|
||||
await ctx.send(
|
||||
f"Slowmode set to {delay} seconds in {channel.mention}.",
|
||||
delete_after=30,
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
await ctx.send(f"Failed to set slowmode. Error: {error}", delete_after=30, ephemeral=True)
|
||||
logger.error(f"Failed to set slowmode. Error: {error}")
|
||||
except Exception as error:
|
||||
await ctx.send(f"Failed to set slowmode. Error: {error}", delete_after=30, ephemeral=True)
|
||||
logger.error(f"Failed to set slowmode. Error: {error}")
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
|
|
134
tux/cogs/moderation/snippetban.py
Normal file
134
tux/cogs/moderation/snippetban.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
from prisma.enums import CaseType
|
||||
from prisma.models import Case
|
||||
from tux.database.controllers.case import CaseController
|
||||
from tux.utils import checks
|
||||
from tux.utils.constants import Constants as CONST
|
||||
from tux.utils.flags import SnippetBanFlags
|
||||
|
||||
from . import ModerationCogBase
|
||||
|
||||
|
||||
class SnippetBan(ModerationCogBase):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
super().__init__(bot)
|
||||
self.case_controller = CaseController()
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="snippetban",
|
||||
aliases=["sb"],
|
||||
usage="snippetban [target]",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(3)
|
||||
async def snippet_ban(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.Member,
|
||||
*,
|
||||
flags: SnippetBanFlags,
|
||||
) -> None:
|
||||
"""
|
||||
Ban a user from creating snippets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context object.
|
||||
target : discord.Member
|
||||
The member to snippet ban.
|
||||
flags : SnippetBanFlags
|
||||
The flags for the command. (reason: str, silent: bool)
|
||||
"""
|
||||
|
||||
if ctx.guild is None:
|
||||
logger.warning("Snippet ban command used outside of a guild context.")
|
||||
return
|
||||
|
||||
if await self.is_snippetbanned(ctx.guild.id, target.id):
|
||||
await ctx.send("User is already snippet banned.", delete_after=30)
|
||||
return
|
||||
|
||||
case = await self.db.case.insert_case(
|
||||
case_target_id=target.id,
|
||||
case_moderator_id=ctx.author.id,
|
||||
case_type=CaseType.SNIPPETBAN,
|
||||
case_reason=flags.reason,
|
||||
guild_id=ctx.guild.id,
|
||||
)
|
||||
|
||||
await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet banned")
|
||||
await self.handle_case_response(ctx, case, "created", flags.reason, target)
|
||||
|
||||
async def handle_case_response(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
case: Case | None,
|
||||
action: str,
|
||||
reason: str,
|
||||
target: discord.Member | discord.User,
|
||||
previous_reason: str | None = None,
|
||||
) -> None:
|
||||
moderator = ctx.author
|
||||
|
||||
fields = [
|
||||
("Moderator", f"__{moderator}__\n`{moderator.id}`", True),
|
||||
("Target", f"__{target}__\n`{target.id}`", True),
|
||||
("Reason", f"> {reason}", False),
|
||||
]
|
||||
|
||||
if previous_reason:
|
||||
fields.append(("Previous Reason", f"> {previous_reason}", False))
|
||||
|
||||
if case is not None:
|
||||
embed = await self.create_embed(
|
||||
ctx,
|
||||
title=f"Case #{case.case_number} ({case.case_type}) {action}",
|
||||
fields=fields,
|
||||
color=CONST.EMBED_COLORS["CASE"],
|
||||
icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"],
|
||||
)
|
||||
embed.set_thumbnail(url=target.avatar)
|
||||
else:
|
||||
embed = await self.create_embed(
|
||||
ctx,
|
||||
title=f"Case {action} ({CaseType.SNIPPETBAN})",
|
||||
fields=fields,
|
||||
color=CONST.EMBED_COLORS["CASE"],
|
||||
icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"],
|
||||
)
|
||||
|
||||
await self.send_embed(ctx, embed, log_type="mod")
|
||||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
|
||||
async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool:
|
||||
"""
|
||||
Check if a user is snippet banned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
guild_id : int
|
||||
The ID of the guild to check in.
|
||||
user_id : int
|
||||
The ID of the user to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the user is snippet banned, False otherwise.
|
||||
"""
|
||||
|
||||
ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN)
|
||||
unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN)
|
||||
|
||||
ban_count = sum(case.case_target_id == user_id for case in ban_cases)
|
||||
unban_count = sum(case.case_target_id == user_id for case in unban_cases)
|
||||
|
||||
return ban_count > unban_count
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(SnippetBan(bot))
|
135
tux/cogs/moderation/snippetunban.py
Normal file
135
tux/cogs/moderation/snippetunban.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
from prisma.enums import CaseType
|
||||
from prisma.models import Case
|
||||
from tux.database.controllers.case import CaseController
|
||||
from tux.utils import checks
|
||||
from tux.utils.constants import Constants as CONST
|
||||
from tux.utils.flags import SnippetUnbanFlags
|
||||
|
||||
from . import ModerationCogBase
|
||||
|
||||
|
||||
class SnippetUnban(ModerationCogBase):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
super().__init__(bot)
|
||||
self.case_controller = CaseController()
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="snippetunban",
|
||||
aliases=["sub"],
|
||||
usage="snippetunban [target]",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(3)
|
||||
async def snippet_unban(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.Member,
|
||||
*,
|
||||
flags: SnippetUnbanFlags,
|
||||
):
|
||||
"""
|
||||
Unban a user from creating snippets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context object.
|
||||
target : discord.Member
|
||||
The member to snippet unban.
|
||||
flags : SnippetUnbanFlags
|
||||
The flags for the command. (reason: str, silent: bool)
|
||||
"""
|
||||
|
||||
if ctx.guild is None:
|
||||
logger.warning("Snippet ban command used outside of a guild context.")
|
||||
return
|
||||
|
||||
# Check if the user is already snippet banned
|
||||
if not await self.is_snippetbanned(ctx.guild.id, target.id):
|
||||
await ctx.send("User is not snippet banned.", delete_after=30)
|
||||
return
|
||||
|
||||
case = await self.db.case.insert_case(
|
||||
case_target_id=target.id,
|
||||
case_moderator_id=ctx.author.id,
|
||||
case_type=CaseType.SNIPPETUNBAN,
|
||||
case_reason=flags.reason,
|
||||
guild_id=ctx.guild.id,
|
||||
)
|
||||
|
||||
await self.send_dm(ctx, flags.silent, target, flags.reason, "Snippet unbanned")
|
||||
await self.handle_case_response(ctx, case, "created", flags.reason, target)
|
||||
|
||||
async def handle_case_response(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
case: Case | None,
|
||||
action: str,
|
||||
reason: str,
|
||||
target: discord.Member | discord.User,
|
||||
previous_reason: str | None = None,
|
||||
) -> None:
|
||||
moderator = ctx.author
|
||||
|
||||
fields = [
|
||||
("Moderator", f"__{moderator}__\n`{moderator.id}`", True),
|
||||
("Target", f"__{target}__\n`{target.id}`", True),
|
||||
("Reason", f"> {reason}", False),
|
||||
]
|
||||
|
||||
if previous_reason:
|
||||
fields.append(("Previous Reason", f"> {previous_reason}", False))
|
||||
|
||||
if case is not None:
|
||||
embed = await self.create_embed(
|
||||
ctx,
|
||||
title=f"Case #{case.case_number} ({case.case_type}) {action}",
|
||||
fields=fields,
|
||||
color=CONST.EMBED_COLORS["CASE"],
|
||||
icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"],
|
||||
)
|
||||
embed.set_thumbnail(url=target.avatar)
|
||||
else:
|
||||
embed = await self.create_embed(
|
||||
ctx,
|
||||
title=f"Case {action} ({CaseType.SNIPPETUNBAN})",
|
||||
fields=fields,
|
||||
color=CONST.EMBED_COLORS["CASE"],
|
||||
icon_url=CONST.EMBED_ICONS["ACTIVE_CASE"],
|
||||
)
|
||||
|
||||
await self.send_embed(ctx, embed, log_type="mod")
|
||||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
|
||||
async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool:
|
||||
"""
|
||||
Check if a user is snippet banned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
guild_id : int
|
||||
The ID of the guild to check in.
|
||||
user_id : int
|
||||
The ID of the user to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the user is snippet banned, False otherwise.
|
||||
"""
|
||||
|
||||
ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN)
|
||||
unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN)
|
||||
|
||||
ban_count = sum(case.case_target_id == user_id for case in ban_cases)
|
||||
unban_count = sum(case.case_target_id == user_id for case in unban_cases)
|
||||
|
||||
return ban_count > unban_count
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(SnippetUnban(bot))
|
|
@ -73,14 +73,14 @@ class Timeout(ModerationCogBase):
|
|||
flags: TimeoutFlags,
|
||||
) -> None:
|
||||
"""
|
||||
Timeout a user from the server.
|
||||
Timeout a member from the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context in which the command is being invoked.
|
||||
target : discord.Member
|
||||
The user to timeout.
|
||||
The member to timeout.
|
||||
flags : TimeoutFlags
|
||||
The flags for the command.
|
||||
|
||||
|
@ -89,6 +89,7 @@ class Timeout(ModerationCogBase):
|
|||
discord.DiscordException
|
||||
If an error occurs while timing out the user.
|
||||
"""
|
||||
|
||||
if ctx.guild is None:
|
||||
logger.warning("Timeout command used outside of a guild context.")
|
||||
return
|
||||
|
|
|
@ -18,7 +18,7 @@ class Unban(ModerationCogBase):
|
|||
@commands.hybrid_command(
|
||||
name="unban",
|
||||
aliases=["ub"],
|
||||
usage="unban [target] [reason]",
|
||||
usage="unban [username_or_id] [reason]",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(3)
|
||||
|
@ -38,7 +38,7 @@ class Unban(ModerationCogBase):
|
|||
target : discord.Member
|
||||
The member to unban.
|
||||
flags : UnbanFlags
|
||||
The flags for the command.
|
||||
The flags for the command (username_or_id: str, reason: str).
|
||||
|
||||
Raises
|
||||
------
|
||||
|
@ -69,11 +69,11 @@ class Unban(ModerationCogBase):
|
|||
return
|
||||
|
||||
case = await self.db.case.insert_case(
|
||||
guild_id=ctx.guild.id,
|
||||
case_target_id=user.id,
|
||||
case_moderator_id=ctx.author.id,
|
||||
case_type=CaseType.UNBAN,
|
||||
case_reason=flags.reason,
|
||||
guild_id=ctx.guild.id,
|
||||
)
|
||||
|
||||
await self.handle_case_response(ctx, case, "created", flags.reason, user)
|
||||
|
|
|
@ -30,7 +30,7 @@ class Unjail(ModerationCogBase):
|
|||
flags: UnjailFlags,
|
||||
) -> None:
|
||||
"""
|
||||
Unjail a user in the server.
|
||||
Unjail a member in the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
@ -41,6 +41,7 @@ class Unjail(ModerationCogBase):
|
|||
flags : UnjailFlags
|
||||
The flags for the command. (reason: str, silent: bool)
|
||||
"""
|
||||
|
||||
if not ctx.guild:
|
||||
logger.warning("Unjail command used outside of a guild context.")
|
||||
return
|
||||
|
@ -55,7 +56,7 @@ class Unjail(ModerationCogBase):
|
|||
return
|
||||
|
||||
if jail_role not in target.roles:
|
||||
await ctx.send("The user is not jailed.", delete_after=30, ephemeral=True)
|
||||
await ctx.send("The member is not jailed.", delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
if not await self._check_jail_channel(ctx):
|
||||
|
@ -63,7 +64,7 @@ class Unjail(ModerationCogBase):
|
|||
|
||||
case = await self.db.case.get_last_jail_case_by_target_id(ctx.guild.id, target.id)
|
||||
if not case:
|
||||
await ctx.send("No jail case found for the user.", delete_after=30, ephemeral=True)
|
||||
await ctx.send("No jail case found for this member.", delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
await self._unjail_user(ctx, target, jail_role, case, flags.reason)
|
||||
|
@ -121,11 +122,11 @@ class Unjail(ModerationCogBase):
|
|||
if previous_roles:
|
||||
await target.add_roles(*previous_roles, reason=reason, atomic=False)
|
||||
else:
|
||||
await ctx.send("No previous roles found for the user.", delete_after=30, ephemeral=True)
|
||||
await ctx.send("No previous roles found for the member.", delete_after=30, ephemeral=True)
|
||||
|
||||
except (discord.Forbidden, discord.HTTPException) as e:
|
||||
logger.error(f"Failed to unjail user {target}. {e}")
|
||||
await ctx.send(f"Failed to unjail user {target}. {e}", delete_after=30, ephemeral=True)
|
||||
logger.error(f"Failed to unjail member {target}. {e}")
|
||||
await ctx.send(f"Failed to unjail member {target}. {e}", delete_after=30, ephemeral=True)
|
||||
|
||||
async def _insert_unjail_case(
|
||||
self,
|
||||
|
|
|
@ -18,7 +18,7 @@ class Untimeout(ModerationCogBase):
|
|||
@commands.hybrid_command(
|
||||
name="untimeout",
|
||||
aliases=["ut", "uto", "unmute"],
|
||||
usage="untimeout [target] [reason]",
|
||||
usage="untimeout [target] [reason] <silent>",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(2)
|
||||
|
@ -30,16 +30,16 @@ class Untimeout(ModerationCogBase):
|
|||
flags: UntimeoutFlags,
|
||||
) -> None:
|
||||
"""
|
||||
Untimeout a user from the server.
|
||||
Untimeout a member from the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context in which the command is being invoked.
|
||||
target : discord.Member
|
||||
The user to timeout.
|
||||
The member to untimeout.
|
||||
flags : UntimeoutFlags
|
||||
The flags for the command.
|
||||
The flags for the command (reason: str, silent: bool).
|
||||
|
||||
Raises
|
||||
------
|
||||
|
|
|
@ -18,7 +18,7 @@ class Warn(ModerationCogBase):
|
|||
@commands.hybrid_command(
|
||||
name="warn",
|
||||
aliases=["w"],
|
||||
usage="warn [target] <flags>",
|
||||
usage="warn [target] [reason] <silent>",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(2)
|
||||
|
@ -30,7 +30,7 @@ class Warn(ModerationCogBase):
|
|||
flags: WarnFlags,
|
||||
) -> None:
|
||||
"""
|
||||
Warn a user from the server.
|
||||
Warn a member from the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import contextlib
|
||||
import datetime
|
||||
import string
|
||||
|
||||
|
@ -7,8 +8,9 @@ from discord.ext import commands
|
|||
from loguru import logger
|
||||
from reactionmenu import ViewButton, ViewMenu
|
||||
|
||||
from prisma.enums import CaseType
|
||||
from prisma.models import Snippet
|
||||
from tux.database.controllers import DatabaseController
|
||||
from tux.database.controllers import CaseController, DatabaseController
|
||||
from tux.utils import checks
|
||||
from tux.utils.constants import Constants as CONST
|
||||
from tux.utils.embeds import EmbedCreator, create_embed_footer, create_error_embed
|
||||
|
@ -19,6 +21,16 @@ class Snippets(commands.Cog):
|
|||
self.bot = bot
|
||||
self.db = DatabaseController().snippet
|
||||
self.config = DatabaseController().guild_config
|
||||
self.case_controller = CaseController()
|
||||
|
||||
async def is_snippetbanned(self, guild_id: int, user_id: int) -> bool:
|
||||
ban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETBAN)
|
||||
unban_cases = await self.case_controller.get_all_cases_by_type(guild_id, CaseType.SNIPPETUNBAN)
|
||||
|
||||
ban_count = sum(1 for case in ban_cases if case.case_target_id == user_id)
|
||||
unban_count = sum(1 for case in unban_cases if case.case_target_id == user_id)
|
||||
|
||||
return ban_count > unban_count
|
||||
|
||||
@commands.command(
|
||||
name="snippets",
|
||||
|
@ -101,6 +113,58 @@ class Snippets(commands.Cog):
|
|||
|
||||
return embed
|
||||
|
||||
@commands.command(
|
||||
name="topsnippets",
|
||||
aliases=["ts"],
|
||||
usage="topsnippets",
|
||||
)
|
||||
@commands.guild_only()
|
||||
async def top_snippets(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
List top snippets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context object.
|
||||
"""
|
||||
|
||||
# find the top 10 snippets by uses
|
||||
snippets: list[Snippet] = await self.db.get_all_snippets_by_guild_id(ctx.guild.id) # type: ignore # wio
|
||||
|
||||
# If there are no snippets, send an error message
|
||||
if not snippets:
|
||||
embed = EmbedCreator.create_error_embed(
|
||||
title="Error",
|
||||
description="No snippets found.",
|
||||
ctx=ctx,
|
||||
)
|
||||
await ctx.send(embed=embed, delete_after=30)
|
||||
return
|
||||
|
||||
# sort the snippets by uses
|
||||
snippets.sort(key=lambda x: x.uses, reverse=True)
|
||||
|
||||
# print in this format
|
||||
# 1. snippet_name | uses: 10
|
||||
# 2. snippet_name | uses: 9
|
||||
# 3. snippet_name | uses: 8
|
||||
# ...
|
||||
|
||||
text = "```\n"
|
||||
for i, snippet in enumerate(snippets[:10]):
|
||||
text += f"{i+1}. {snippet.snippet_name.ljust(20)} | uses: {snippet.uses}\n"
|
||||
text += "```"
|
||||
|
||||
# only show top 10, no pagination
|
||||
embed = discord.Embed(
|
||||
title="Top Snippets",
|
||||
description=text,
|
||||
color=CONST.EMBED_COLORS["DEFAULT"],
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(
|
||||
name="deletesnippet",
|
||||
aliases=["ds"],
|
||||
|
@ -130,6 +194,14 @@ class Snippets(commands.Cog):
|
|||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
# check if the snippet is locked
|
||||
if snippet.locked:
|
||||
embed = create_error_embed(
|
||||
error="This snippet is locked and cannot be deleted. If you are a moderator you can use the `forcedeletesnippet` command.",
|
||||
)
|
||||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
# Check if the author of the snippet is the same as the user who wants to delete it and if theres no author don't allow deletion
|
||||
author_id = snippet.snippet_user_id or 0
|
||||
if author_id != ctx.author.id:
|
||||
|
@ -214,6 +286,9 @@ class Snippets(commands.Cog):
|
|||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
# increment the usage count of the snippet
|
||||
await self.db.increment_snippet_uses(snippet.snippet_id)
|
||||
|
||||
text = f"`/snippets/{snippet.snippet_name}.txt` || {snippet.snippet_content}"
|
||||
|
||||
await ctx.send(text, allowed_mentions=AllowedMentions.none())
|
||||
|
@ -263,6 +338,8 @@ class Snippets(commands.Cog):
|
|||
embed.add_field(name="Name", value=snippet.snippet_name, inline=False)
|
||||
embed.add_field(name="Author", value=f"{author.mention}", inline=False)
|
||||
embed.add_field(name="Content", value=f"> {snippet.snippet_content}", inline=False)
|
||||
embed.add_field(name="Uses", value=snippet.uses, inline=False)
|
||||
embed.add_field(name="Locked", value="Yes" if snippet.locked else "No", inline=False)
|
||||
|
||||
embed.timestamp = snippet.snippet_created_at or datetime.datetime.fromtimestamp(
|
||||
0,
|
||||
|
@ -293,6 +370,10 @@ class Snippets(commands.Cog):
|
|||
await ctx.send("This command cannot be used in direct messages.")
|
||||
return
|
||||
|
||||
if await self.is_snippetbanned(ctx.guild.id, ctx.author.id):
|
||||
await ctx.send("You are banned from using snippets.")
|
||||
return
|
||||
|
||||
args = arg.split(" ")
|
||||
if len(args) < 2:
|
||||
embed = create_error_embed(error="Please provide a name and content for the snippet.")
|
||||
|
@ -333,6 +414,127 @@ class Snippets(commands.Cog):
|
|||
await ctx.send("Snippet created.", delete_after=30, ephemeral=True)
|
||||
logger.info(f"{ctx.author} created a snippet with the name {name} and content {content}.")
|
||||
|
||||
@commands.command(
|
||||
name="editsnippet",
|
||||
aliases=["es"],
|
||||
usage="editsnippet [name]",
|
||||
)
|
||||
@commands.guild_only()
|
||||
async def edit_snippet(self, ctx: commands.Context[commands.Bot], *, arg: str) -> None:
|
||||
"""
|
||||
Edit a snippet.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context object.
|
||||
arg : str
|
||||
The name and content of the snippet.
|
||||
"""
|
||||
|
||||
if ctx.guild is None:
|
||||
await ctx.send("This command cannot be used in direct messages.")
|
||||
return
|
||||
|
||||
args = arg.split(" ")
|
||||
if len(args) < 2:
|
||||
embed = create_error_embed(error="Please provide a name and content for the snippet.")
|
||||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
name = args[0]
|
||||
content = " ".join(args[1:])
|
||||
author_id = ctx.author.id
|
||||
snippet = await self.db.get_snippet_by_name_and_guild_id(name, ctx.guild.id)
|
||||
|
||||
if snippet is None:
|
||||
embed = create_error_embed(error="Snippet not found.")
|
||||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
# Check if the author of the snippet is the same as the user who wants to edit it and if theres no author don't allow editing
|
||||
author_id = snippet.snippet_user_id or 0
|
||||
if author_id != ctx.author.id:
|
||||
embed = create_error_embed(error="You can only edit your own snippets.")
|
||||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
# check if the snippet is locked
|
||||
if snippet.locked:
|
||||
logger.info(
|
||||
f"{ctx.author} is trying to edit a snippet with the name {name} and content {content}. Checking if they have the permission level to edit locked snippets.",
|
||||
)
|
||||
# dont make the check send its own error message
|
||||
try:
|
||||
await checks.has_pl(2).predicate(ctx)
|
||||
except commands.CheckFailure:
|
||||
embed = create_error_embed(
|
||||
error="This snippet is locked and cannot be edited. If you are a moderator you can use the `forcedeletesnippet` command.",
|
||||
)
|
||||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
return
|
||||
logger.info(f"{ctx.author} has the permission level to edit locked snippets.")
|
||||
|
||||
await self.db.update_snippet_by_id(
|
||||
snippet.snippet_id,
|
||||
snippet_content=content,
|
||||
)
|
||||
|
||||
await ctx.send("Snippet Edited.", delete_after=30, ephemeral=True) # Correct indentation
|
||||
logger.info(f"{ctx.author} Edited a snippet with the name {name} and content {content}.") # Correct indentation
|
||||
|
||||
@commands.command(
|
||||
name="togglesnippetlock",
|
||||
aliases=["tsl"],
|
||||
usage="togglesnippetlock [name]",
|
||||
)
|
||||
@commands.guild_only()
|
||||
@checks.has_pl(2)
|
||||
async def toggle_snippet_lock(self, ctx: commands.Context[commands.Bot], name: str) -> None:
|
||||
"""
|
||||
Toggle a snippet lock.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context object.
|
||||
name : str
|
||||
The name of the snippet.
|
||||
"""
|
||||
|
||||
if ctx.guild is None:
|
||||
await ctx.send("This command cannot be used in direct messages.")
|
||||
return
|
||||
|
||||
snippet = await self.db.get_snippet_by_name_and_guild_id(name, ctx.guild.id)
|
||||
|
||||
if snippet is None:
|
||||
embed = create_error_embed(error="Snippet not found.")
|
||||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
status = await self.db.toggle_snippet_lock_by_id(snippet.snippet_id)
|
||||
|
||||
if status is None:
|
||||
embed = create_error_embed(error="No return value from locking the snippet. It may still have been locked.")
|
||||
await ctx.send(embed=embed, delete_after=30, ephemeral=True)
|
||||
return
|
||||
|
||||
if author := self.bot.get_user(snippet.snippet_user_id):
|
||||
with contextlib.suppress(discord.Forbidden):
|
||||
await author.send(
|
||||
f"""Your snippet `{snippet.snippet_name}` has been {'locked' if status.locked else 'unlocked'}.
|
||||
|
||||
**What does this mean?**
|
||||
If a snippet is locked, it cannot be edited by anyone other than moderators. This means that you can no longer edit this snippet.
|
||||
|
||||
**Why was it locked?**
|
||||
Snippets are usually locked by moderators if they are important to usual use of the server. Changes or deletions to these snippets can have a big impact on the server. If you believe this was done in error, please open a ticket with /ticket.""",
|
||||
)
|
||||
|
||||
await ctx.send("Snippet lock toggled.", delete_after=30, ephemeral=True)
|
||||
logger.info(f"{ctx.author} toggled the lock of the snippet with the name {name}.")
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Snippets(bot))
|
||||
|
|
140
tux/cogs/utility/timezones.py
Normal file
140
tux/cogs/utility/timezones.py
Normal file
|
@ -0,0 +1,140 @@
|
|||
from datetime import UTC, datetime
|
||||
|
||||
import discord
|
||||
import pytz
|
||||
from discord.ext import commands
|
||||
from reactionmenu import Page, ViewButton, ViewMenu, ViewSelect
|
||||
|
||||
timezones = {
|
||||
"North America": [
|
||||
("🇺🇸", "US", "Pacific/Honolulu", "HST", -10),
|
||||
("🇺🇸", "US", "America/Anchorage", "AKST", -9),
|
||||
("🇺🇸", "US", "America/Los_Angeles", "PST", -8),
|
||||
("🇺🇸", "US", "America/Denver", "MST", -7),
|
||||
("🇺🇸", "US", "America/Chicago", "CST", -6),
|
||||
("🇺🇸", "US", "America/New_York", "EST", -5),
|
||||
("🇲🇽", "MX", "America/Mexico_City", "CST", -6),
|
||||
("🇨🇦", "CA", "America/Toronto", "EST", -5),
|
||||
("🇨🇦", "CA", "America/Vancouver", "PST", -8),
|
||||
],
|
||||
"South America": [
|
||||
("🇧🇷", "BR", "America/Sao_Paulo", "BRT", -3),
|
||||
("🇦🇷", "AR", "America/Argentina/Buenos_Aires", "ART", -3),
|
||||
("🇨🇱", "CL", "America/Santiago", "CLT", -3),
|
||||
("🇵🇪", "PE", "America/Lima", "PET", -5),
|
||||
("🇨🇴", "CO", "America/Bogota", "COT", -5),
|
||||
("🇻🇪", "VE", "America/Caracas", "VET", -4),
|
||||
("🇧🇴", "BO", "America/La_Paz", "BOT", -4),
|
||||
("🇵🇾", "PY", "America/Asuncion", "PYT", -4),
|
||||
("🇺🇾", "UY", "America/Montevideo", "UYT", -3),
|
||||
],
|
||||
"Africa": [
|
||||
("🇬🇭", "GH", "Africa/Accra", "GMT", 0),
|
||||
("🇳🇬", "NG", "Africa/Lagos", "WAT", 1),
|
||||
("🇿🇦", "ZA", "Africa/Johannesburg", "SAST", 2),
|
||||
("🇪🇬", "EG", "Africa/Cairo", "EET", 2),
|
||||
("🇰🇪", "KE", "Africa/Nairobi", "EAT", 3),
|
||||
("🇲🇦", "MA", "Africa/Casablanca", "WET", 0),
|
||||
("🇹🇿", "TZ", "Africa/Dar_es_Salaam", "EAT", 3),
|
||||
("🇩🇿", "DZ", "Africa/Algiers", "CET", 1),
|
||||
("🇳🇦", "NA", "Africa/Windhoek", "CAT", 2),
|
||||
],
|
||||
"Europe": [
|
||||
("🇬🇧", "GB", "Europe/London", "GMT", 0),
|
||||
("🇩🇪", "DE", "Europe/Berlin", "CET", 1),
|
||||
("🇫🇷", "FR", "Europe/Paris", "CET", 1),
|
||||
("🇮🇹", "IT", "Europe/Rome", "CET", 1),
|
||||
("🇪🇸", "ES", "Europe/Madrid", "CET", 1),
|
||||
("🇳🇱", "NL", "Europe/Amsterdam", "CET", 1),
|
||||
("🇧🇪", "BE", "Europe/Brussels", "CET", 1),
|
||||
("🇷🇺", "RU", "Europe/Moscow", "MSK", 3),
|
||||
("🇬🇷", "GR", "Europe/Athens", "EET", 2),
|
||||
],
|
||||
"Asia": [
|
||||
("🇦🇪", "AE", "Asia/Dubai", "GST", 4),
|
||||
("🇮🇳", "IN", "Asia/Kolkata", "IST", 5.5),
|
||||
("🇧🇩", "BD", "Asia/Dhaka", "BST", 6),
|
||||
("🇲🇲", "MM", "Asia/Yangon", "MMT", 6.5),
|
||||
("🇹🇭", "TH", "Asia/Bangkok", "ICT", 7),
|
||||
("🇻🇳", "VN", "Asia/Ho_Chi_Minh", "ICT", 7),
|
||||
("🇨🇳", "CN", "Asia/Shanghai", "CST", 8),
|
||||
("🇭🇰", "HK", "Asia/Hong_Kong", "HKT", 8),
|
||||
("🇯🇵", "JP", "Asia/Tokyo", "JST", 9),
|
||||
],
|
||||
"Australia/Oceania": [
|
||||
("🇦🇺", "AU", "Australia/Perth", "AWST", 8),
|
||||
("🇦🇺", "AU", "Australia/Sydney", "AEST", 10),
|
||||
("🇫🇯", "FJ", "Pacific/Fiji", "FJT", 12),
|
||||
("🇳🇿", "NZ", "Pacific/Auckland", "NZDT", 13),
|
||||
("🇵🇬", "PG", "Pacific/Port_Moresby", "PGT", 10),
|
||||
("🇼🇸", "WS", "Pacific/Apia", "WSST", 13),
|
||||
("🇸🇧", "SB", "Pacific/Guadalcanal", "SBT", 11),
|
||||
("🇻🇺", "VU", "Pacific/Efate", "VUT", 11),
|
||||
("🇵🇫", "PF", "Pacific/Tahiti", "THAT", -10),
|
||||
],
|
||||
}
|
||||
|
||||
continent_emojis = {
|
||||
"North America": "🌎",
|
||||
"South America": "🌎",
|
||||
"Africa": "🌍",
|
||||
"Europe": "🌍",
|
||||
"Asia": "🌏",
|
||||
"Australia/Oceania": "🌏",
|
||||
}
|
||||
|
||||
|
||||
class Timezones(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="timezones",
|
||||
aliases=["tz"],
|
||||
usage="timezones",
|
||||
)
|
||||
async def timezones(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
utc_now = datetime.now(UTC)
|
||||
|
||||
menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed)
|
||||
|
||||
default_embeds: list[discord.Embed] = []
|
||||
options: dict[discord.SelectOption, list[Page]] = {}
|
||||
|
||||
for continent, tz_list in timezones.items():
|
||||
embeds: list[discord.Embed] = []
|
||||
pages = [tz_list[i : i + 9] for i in range(0, len(tz_list), 9)]
|
||||
|
||||
for page in pages:
|
||||
embed = discord.Embed(title=f"Timezones in {continent}", color=discord.Color.blurple())
|
||||
|
||||
for flag, _country, tz_name, abbr, utc_offset in page:
|
||||
tz = pytz.timezone(tz_name)
|
||||
local_time = utc_now.astimezone(tz)
|
||||
time_24hr = local_time.strftime("%H:%M")
|
||||
time_12hr = local_time.strftime("%I:%M %p")
|
||||
|
||||
embed.add_field(
|
||||
name=f"{flag} {abbr} (UTC{utc_offset:+.2f})",
|
||||
value=f"`{time_24hr} | {time_12hr}`",
|
||||
inline=True,
|
||||
)
|
||||
|
||||
embeds.append(embed)
|
||||
|
||||
default_embeds.extend(embeds)
|
||||
|
||||
options[discord.SelectOption(label=continent, emoji=continent_emojis[continent])] = Page.from_embeds(embeds)
|
||||
|
||||
for embed in default_embeds:
|
||||
menu.add_page(embed)
|
||||
|
||||
select = ViewSelect(title="Select Continent", options=options)
|
||||
menu.add_select(select)
|
||||
menu.add_button(ViewButton.end_session())
|
||||
|
||||
await menu.start()
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Timezones(bot))
|
|
@ -18,6 +18,9 @@ class SnippetController:
|
|||
async def get_all_snippets(self) -> list[Snippet]:
|
||||
return await self.table.find_many()
|
||||
|
||||
async def get_all_snippets_by_guild_id(self, guild_id: int) -> list[Snippet]:
|
||||
return await self.table.find_many(where={"guild_id": guild_id})
|
||||
|
||||
async def get_all_snippets_sorted(self, newestfirst: bool = True) -> list[Snippet]:
|
||||
return await self.table.find_many(
|
||||
order={"snippet_created_at": "desc" if newestfirst else "asc"},
|
||||
|
@ -63,3 +66,35 @@ class SnippetController:
|
|||
where={"snippet_id": snippet_id},
|
||||
data={"snippet_content": snippet_content},
|
||||
)
|
||||
|
||||
async def increment_snippet_uses(self, snippet_id: int) -> Snippet | None:
|
||||
snippet = await self.table.find_first(where={"snippet_id": snippet_id})
|
||||
if snippet is None:
|
||||
return None
|
||||
|
||||
return await self.table.update(
|
||||
where={"snippet_id": snippet_id},
|
||||
data={"uses": snippet.uses + 1},
|
||||
)
|
||||
|
||||
async def lock_snippet_by_id(self, snippet_id: int) -> Snippet | None:
|
||||
return await self.table.update(
|
||||
where={"snippet_id": snippet_id},
|
||||
data={"locked": True},
|
||||
)
|
||||
|
||||
async def unlock_snippet_by_id(self, snippet_id: int) -> Snippet | None:
|
||||
return await self.table.update(
|
||||
where={"snippet_id": snippet_id},
|
||||
data={"locked": False},
|
||||
)
|
||||
|
||||
async def toggle_snippet_lock_by_id(self, snippet_id: int) -> Snippet | None:
|
||||
snippet = await self.table.find_first(where={"snippet_id": snippet_id})
|
||||
if snippet is None:
|
||||
return None
|
||||
|
||||
return await self.table.update(
|
||||
where={"snippet_id": snippet_id},
|
||||
data={"locked": not snippet.locked},
|
||||
)
|
||||
|
|
|
@ -6,21 +6,8 @@ from discord import app_commands
|
|||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
import tux.handlers.error as error
|
||||
from tux.utils.embeds import create_error_embed
|
||||
|
||||
|
||||
class PermissionLevelError(commands.CheckFailure):
|
||||
def __init__(self, permission: str) -> None:
|
||||
self.permission = permission
|
||||
super().__init__(f"User does not have the required permission: {permission}")
|
||||
|
||||
|
||||
class AppCommandPermissionLevelError(app_commands.CheckFailure):
|
||||
def __init__(self, permission: str) -> None:
|
||||
self.permission = permission
|
||||
super().__init__(f"User does not have the required permission: {permission}")
|
||||
|
||||
from tux.utils.exceptions import AppCommandPermissionLevelError, PermissionLevelError
|
||||
|
||||
error_map: dict[type[Exception], str] = {
|
||||
# app_commands
|
||||
|
@ -51,8 +38,8 @@ error_map: dict[type[Exception], str] = {
|
|||
commands.NotOwner: "User not in sudoers file. This incident will be reported. (Not Owner)",
|
||||
commands.BotMissingPermissions: "User not in sudoers file. This incident will be reported. (Bot Missing Permissions)",
|
||||
# Custom errors
|
||||
error.PermissionLevelError: "User not in sudoers file. This incident will be reported. (Missing required permission: {error.permission})",
|
||||
error.AppCommandPermissionLevelError: "User not in sudoers file. This incident will be reported. (Missing required permission: {error.permission})",
|
||||
PermissionLevelError: "User not in sudoers file. This incident will be reported. (Missing required permission: {error.permission})",
|
||||
AppCommandPermissionLevelError: "User not in sudoers file. This incident will be reported. (Missing required permission: {error.permission})",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -43,9 +43,7 @@ class EventHandler(commands.Cog):
|
|||
flag_list = ["🏳️🌈", "🏳️⚧️"]
|
||||
|
||||
user = self.bot.get_user(payload.user_id)
|
||||
if user is None:
|
||||
return
|
||||
if user.bot:
|
||||
if user is None or user.bot:
|
||||
return
|
||||
|
||||
if payload.guild_id is None:
|
||||
|
@ -59,26 +57,46 @@ class EventHandler(commands.Cog):
|
|||
return
|
||||
|
||||
channel = self.bot.get_channel(payload.channel_id)
|
||||
if channel is None:
|
||||
return
|
||||
if channel.id != 1172343581495795752:
|
||||
return
|
||||
if not isinstance(channel, discord.TextChannel):
|
||||
if channel is None or channel.id != 1172343581495795752 or not isinstance(channel, discord.TextChannel):
|
||||
return
|
||||
|
||||
message = await channel.fetch_message(payload.message_id)
|
||||
|
||||
emoji = payload.emoji
|
||||
if any(0x1F1E3 <= ord(char) <= 0x1F1FF for char in emoji.name):
|
||||
await message.remove_reaction(emoji, member)
|
||||
return
|
||||
if "flag" in emoji.name.lower():
|
||||
await message.remove_reaction(emoji, member)
|
||||
return
|
||||
if emoji.name in flag_list:
|
||||
if (
|
||||
any(0x1F1E3 <= ord(char) <= 0x1F1FF for char in emoji.name)
|
||||
or "flag" in emoji.name.lower()
|
||||
or emoji.name in flag_list
|
||||
):
|
||||
await message.remove_reaction(emoji, member)
|
||||
return
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_thread_create(self, thread: discord.Thread) -> None:
|
||||
# TODO: Add database configuration for primmary support forum
|
||||
support_forum = 1172312653797007461
|
||||
|
||||
if thread.parent_id == support_forum:
|
||||
owner_mention = thread.owner.mention if thread.owner else {thread.owner_id}
|
||||
|
||||
if tags := [tag.name for tag in thread.applied_tags]:
|
||||
tag_list = ", ".join(tags)
|
||||
msg = f"<:tux_notify:1274504953666474025> **New support thread created** - help is appreciated!\n{thread.mention} by {owner_mention}\n<:tux_tag:1274504955163709525> **Tags**: `{tag_list}`"
|
||||
|
||||
else:
|
||||
msg = f"<:tux_notify:1274504953666474025> **New support thread created** - help is appreciated!\n{thread.mention} by {owner_mention}"
|
||||
|
||||
embed = discord.Embed(description=msg, color=discord.Color.random())
|
||||
|
||||
general_chat = 1172245377395728467
|
||||
channel = self.bot.get_channel(general_chat)
|
||||
|
||||
if channel is not None and isinstance(channel, discord.TextChannel):
|
||||
# TODO: Add database configuration for primary support role
|
||||
support_role = "<@&1274823545087590533>"
|
||||
|
||||
await channel.send(content=support_role, embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(EventHandler(bot))
|
||||
|
|
|
@ -225,7 +225,7 @@ class TuxHelp(commands.HelpCommand):
|
|||
)
|
||||
embed.add_field(
|
||||
name="Flag Help",
|
||||
value=f"Flags in `[]` are required and `<>` are optional. Most flags have aliases that can be used.\n> e.g. `{prefix}ban @user --reason spamming` or `{prefix}b @user -r spamming`",
|
||||
value=f"Flags in `[]` are required and `<>` are optional. Most flags have aliases that can be used.\n> e.g. `{prefix}ban @user -reason spamming` or `{prefix}b @user -r spamming`",
|
||||
inline=False,
|
||||
)
|
||||
embed.add_field(
|
||||
|
|
|
@ -33,8 +33,9 @@ async def main() -> None:
|
|||
bot = Tux(
|
||||
command_prefix=CONST.PREFIX,
|
||||
strip_after_prefix=True,
|
||||
case_insensitive=True,
|
||||
intents=discord.Intents.all(),
|
||||
owner_id=CONST.BOT_OWNER_ID,
|
||||
owner_ids=[*CONST.SYSADMIN_IDS, CONST.BOT_OWNER_ID],
|
||||
allowed_mentions=discord.AllowedMentions(everyone=False),
|
||||
)
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ from discord.ext import commands
|
|||
from loguru import logger
|
||||
|
||||
from tux.database.controllers import DatabaseController
|
||||
from tux.handlers.error import AppCommandPermissionLevelError, PermissionLevelError
|
||||
from tux.utils.constants import CONST
|
||||
from tux.utils.exceptions import AppCommandPermissionLevelError, PermissionLevelError
|
||||
|
||||
db = DatabaseController().guild_config
|
||||
|
||||
|
@ -18,12 +18,12 @@ async def has_permission(
|
|||
higher_bound: int | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a user has a permission level.
|
||||
Check if the source has the required permission level.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : commands.Context[commands.Bot] | discord.Interaction
|
||||
The source object for the command.
|
||||
The source of the command.
|
||||
lower_bound : int
|
||||
The lower bound of the permission level.
|
||||
higher_bound : int | None, optional
|
||||
|
@ -32,82 +32,152 @@ async def has_permission(
|
|||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the user has the permission level.
|
||||
Whether the source has the required permission level.
|
||||
"""
|
||||
|
||||
# If higher_bound is None, set it to lower_bound
|
||||
higher_bound = higher_bound or lower_bound
|
||||
|
||||
# If the source is a context object and the guild is None, return False if the lower bound is not 0
|
||||
if source.guild is None:
|
||||
logger.debug("Guild is None, returning False if lower bound is not 0")
|
||||
return lower_bound == 0
|
||||
|
||||
# If the source is a context object, set the context object to source
|
||||
ctx = source if isinstance(source, commands.Context) else None
|
||||
# If the source is an interaction object, set the interaction object to source
|
||||
interaction = source if isinstance(source, discord.Interaction) else None
|
||||
|
||||
# Initialize the list of roles to an empty list to avoid type errors
|
||||
roles: list[Any] = []
|
||||
roles = await get_roles_for_bounds(source, lower_bound, higher_bound)
|
||||
|
||||
try:
|
||||
if higher_bound == lower_bound:
|
||||
# Get the role ID for the permission level
|
||||
role_id = await get_perm_level_role_id(source, f"perm_level_{lower_bound}_role_id")
|
||||
author = await get_author_from_source(ctx, interaction)
|
||||
|
||||
# If the role ID exists, append it to the list of roles
|
||||
if role_id:
|
||||
roles.append(role_id)
|
||||
else:
|
||||
logger.debug(f"No Role ID fetched for perm_level_{lower_bound}_role_id")
|
||||
|
||||
else:
|
||||
# Get the role IDs for the permission levels
|
||||
fetched_roles = await get_perm_level_roles(source, lower_bound)
|
||||
|
||||
# If the role IDs exist, extend the list of roles with the fetched roles
|
||||
if fetched_roles:
|
||||
roles.extend(fetched_roles)
|
||||
|
||||
else:
|
||||
logger.debug(f"No roles fetched for levels above and equal to {lower_bound}")
|
||||
|
||||
# Set the author to None to avoid type errors
|
||||
author = None
|
||||
|
||||
# If the context object or interaction object exists and the guild is not None, fetch the author
|
||||
if ctx and ctx.guild:
|
||||
author = await ctx.guild.fetch_member(ctx.author.id)
|
||||
elif interaction and interaction.guild:
|
||||
author = await interaction.guild.fetch_member(interaction.user.id)
|
||||
|
||||
# If the author is not None and the author has any of the roles in the list of roles, return True
|
||||
if author and any(role.id in roles for role in author.roles):
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in permission check: {e}")
|
||||
|
||||
logger.debug("All checks failed, checking for sysadmin or bot owner status")
|
||||
return await check_sysadmin_or_owner(ctx, interaction, lower_bound, higher_bound)
|
||||
|
||||
|
||||
async def get_roles_for_bounds(
|
||||
source: Any,
|
||||
lower_bound: int,
|
||||
higher_bound: int,
|
||||
) -> list[int]:
|
||||
"""
|
||||
Get the roles for the given bounds.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : Any
|
||||
The source of the command.
|
||||
lower_bound : int
|
||||
The lower bound of the permission level.
|
||||
higher_bound : int
|
||||
The higher bound of the permission level.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[int]
|
||||
The list of role IDs for the given bounds.
|
||||
"""
|
||||
|
||||
roles: list[Any] = []
|
||||
|
||||
try:
|
||||
# Get the user ID from the context object or interaction object
|
||||
user_id = ctx.author.id if ctx else interaction.user.id if interaction else None
|
||||
if higher_bound == lower_bound:
|
||||
role_id = await get_perm_level_role_id(source, f"perm_level_{lower_bound}_role_id")
|
||||
|
||||
if role_id:
|
||||
roles.append(role_id)
|
||||
else:
|
||||
logger.debug(f"No Role ID fetched for perm_level_{lower_bound}_role_id")
|
||||
|
||||
else:
|
||||
fetched_roles = await get_perm_level_roles(source, lower_bound)
|
||||
roles.extend(fetched_roles or [])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching roles: {e}")
|
||||
|
||||
return roles
|
||||
|
||||
|
||||
async def get_author_from_source(
|
||||
ctx: Any,
|
||||
interaction: Any,
|
||||
) -> Any:
|
||||
"""
|
||||
Get the author from the source.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : Any
|
||||
The context of the command.
|
||||
interaction : Any
|
||||
The interaction of the command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Any
|
||||
The author of the command.
|
||||
"""
|
||||
|
||||
try:
|
||||
if ctx and ctx.guild:
|
||||
return await ctx.guild.fetch_member(ctx.author.id)
|
||||
|
||||
if interaction and interaction.guild:
|
||||
return await interaction.guild.fetch_member(interaction.user.id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching author: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def check_sysadmin_or_owner(
|
||||
ctx: Any,
|
||||
interaction: Any,
|
||||
lower_bound: int,
|
||||
higher_bound: int,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the user is a sysadmin or bot owner.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : Any
|
||||
The context of the command.
|
||||
interaction : Any
|
||||
The interaction of the command.
|
||||
lower_bound : int
|
||||
The lower bound of the permission level.
|
||||
higher_bound : int
|
||||
The higher bound of the permission level.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the user is a sysadmin or bot owner.
|
||||
"""
|
||||
|
||||
try:
|
||||
user_id = ctx.author.id if ctx else (interaction.user.id if interaction else None)
|
||||
|
||||
if user_id:
|
||||
# Check if the user is a sysadmin or the bot owner
|
||||
if 8 in range(lower_bound, higher_bound + 1) and user_id in CONST.SYSADMIN_IDS:
|
||||
logger.debug("User is a sysadmin")
|
||||
|
||||
return True
|
||||
|
||||
if 9 in range(lower_bound, higher_bound + 1) and user_id == CONST.BOT_OWNER_ID:
|
||||
logger.debug("User is the bot owner")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception while checking sysadmin or bot owner status: {e}")
|
||||
|
||||
logger.debug("All checks failed, returning False")
|
||||
return False
|
||||
|
||||
|
||||
|
@ -117,14 +187,16 @@ async def level_to_name(
|
|||
or_higher: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Convert a permission level to a name.
|
||||
Get the name of the permission level.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : commands.Context[commands.Bot] | discord.Interaction
|
||||
The source of the command.
|
||||
level : int
|
||||
The permission level to convert.
|
||||
The permission level.
|
||||
or_higher : bool, optional
|
||||
Whether the user should have the permission level or higher, by default False
|
||||
Whether to include "or higher" in the name, by default False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
@ -132,34 +204,15 @@ async def level_to_name(
|
|||
The name of the permission level.
|
||||
"""
|
||||
|
||||
# Check if the level is 8 or 9 and return the corresponding name
|
||||
if level in {8, 9}:
|
||||
return "Sys Admin" if level == 8 else "Bot Owner"
|
||||
|
||||
# Check if the source is a context object and the guild is not None
|
||||
if isinstance(source, commands.Context):
|
||||
ctx = source
|
||||
if ctx.guild is None:
|
||||
return "Error"
|
||||
role_name = await get_role_name_from_source(source, level)
|
||||
|
||||
# Get the role ID from the database for the guild and the role field
|
||||
role_id = await db.get_perm_level_role(ctx.guild.id, f"perm_level_{level}_role_id")
|
||||
if role_id and (role := ctx.guild.get_role(role_id)):
|
||||
return f"{role.name} or higher" if or_higher else role.name
|
||||
if role_name:
|
||||
return f"{role_name} or higher" if or_higher else role_name
|
||||
|
||||
else:
|
||||
# Get the interaction object and check if it exists and is in a guild
|
||||
interaction = source
|
||||
if not interaction or not interaction.guild:
|
||||
return "Error"
|
||||
|
||||
# Get the role ID from the database for the guild and the role field
|
||||
role_id = await db.get_perm_level_role(interaction.guild.id, f"perm_level_{level}_role_id")
|
||||
if role_id and (role := interaction.guild.get_role(role_id)):
|
||||
return f"{role.name} or higher" if or_higher else role.name
|
||||
|
||||
# Dictionary of permission levels with the level as the key and the name as the value
|
||||
dictionary = {
|
||||
default_names = {
|
||||
0: "Member",
|
||||
1: "Support",
|
||||
2: "Junior Moderator",
|
||||
|
@ -172,9 +225,35 @@ async def level_to_name(
|
|||
9: "Bot Owner",
|
||||
}
|
||||
|
||||
# Return the name of the permission level from the dictionary
|
||||
# or the name of the permission level with "or higher" appended if or_higher is True
|
||||
return f"{dictionary[level]} or higher" if or_higher else dictionary[level]
|
||||
return f"{default_names[level]} or higher" if or_higher else default_names[level]
|
||||
|
||||
|
||||
async def get_role_name_from_source(
|
||||
source: Any,
|
||||
level: int,
|
||||
) -> str | None:
|
||||
"""
|
||||
Get the name of the role for the given level from the source.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : Any
|
||||
The source of the command.
|
||||
level : int
|
||||
The permission level.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str | None
|
||||
The name of the role for the given level.
|
||||
"""
|
||||
|
||||
role_id = await db.get_perm_level_role(source.guild.id, f"perm_level_{level}_role_id")
|
||||
|
||||
if role_id and (role := source.guild.get_role(role_id)):
|
||||
return role.name
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def get_perm_level_role_id(
|
||||
|
@ -182,47 +261,28 @@ async def get_perm_level_role_id(
|
|||
level: str,
|
||||
) -> int | None:
|
||||
"""
|
||||
Get the role ID for a permission level.
|
||||
Get the role ID for the given permission level.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : commands.Context[commands.Bot] | discord.Interaction
|
||||
The source object for the command.
|
||||
The source of the command.
|
||||
level : str
|
||||
The permission level to get the role ID for.
|
||||
The permission level.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int | None
|
||||
The role ID for the permission level or None if it does not exist.
|
||||
The role ID for the given permission level or None.
|
||||
"""
|
||||
|
||||
# Initialize the role ID to None to avoid type errors
|
||||
role_id = None
|
||||
|
||||
try:
|
||||
# Check if the source is a context object and if the guild is not None
|
||||
if isinstance(source, commands.Context):
|
||||
ctx = source
|
||||
if ctx.guild is None:
|
||||
return None
|
||||
|
||||
# Get the role ID from the database for the guild and the role field
|
||||
role_id = await db.get_perm_level_role(ctx.guild.id, level)
|
||||
|
||||
else:
|
||||
# Get the interaction object and check if it exists and is in a guild
|
||||
interaction = source
|
||||
if not interaction or not interaction.guild:
|
||||
return None
|
||||
|
||||
# Get the role ID from the database for the guild and the role field
|
||||
role_id = await db.get_perm_level_role(interaction.guild.id, level)
|
||||
guild = source.guild
|
||||
return await db.get_perm_level_role(guild.id, level) if guild else None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving role ID for level {level}: {e}")
|
||||
|
||||
return role_id
|
||||
return None
|
||||
|
||||
|
||||
async def get_perm_level_roles(
|
||||
|
@ -230,23 +290,22 @@ async def get_perm_level_roles(
|
|||
lower_bound: int,
|
||||
) -> list[int] | None:
|
||||
"""
|
||||
Get the role IDs for a range of permission levels.
|
||||
Get the role IDs for the given permission levels.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source : commands.Context[commands.Bot] | discord.Interaction
|
||||
The source object for the command.
|
||||
The source of the command.
|
||||
lower_bound : int
|
||||
The lower bound of the permission level.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[int] | None
|
||||
The list of role IDs for the permission levels or None if they do not exist.
|
||||
The role IDs for the given permission levels or None.
|
||||
"""
|
||||
|
||||
# Dictionary of permission level roles with the level as the key and the field name as the value
|
||||
perm_level_roles: dict[int, str] = {
|
||||
perm_level_roles = {
|
||||
0: "perm_level_0_role_id",
|
||||
1: "perm_level_1_role_id",
|
||||
2: "perm_level_2_role_id",
|
||||
|
@ -257,29 +316,21 @@ async def get_perm_level_roles(
|
|||
7: "perm_level_7_role_id",
|
||||
}
|
||||
|
||||
# Initialize the list of role IDs to an empty list to avoid type errors
|
||||
role_ids: list[int] = []
|
||||
role_ids: list[Any] = []
|
||||
|
||||
try:
|
||||
# For each level in the range of the lower bound to 8
|
||||
for level in range(lower_bound, 8):
|
||||
# If the role field exists, get the role ID by the field name (e.g. perm_level_1_role_id)
|
||||
if role_field := perm_level_roles.get(level):
|
||||
# Get the role ID from the database for the guild and the role field
|
||||
role_id = await db.get_guild_config_field_value(source.guild.id, role_field) # type: ignore
|
||||
# If the role ID exists, append it to the list of role IDs
|
||||
|
||||
if role_id:
|
||||
role_ids.append(role_id)
|
||||
|
||||
else:
|
||||
logger.debug(f"No role ID found for {role_field}, skipping")
|
||||
|
||||
# Catch any exceptions that occur while getting the role IDs
|
||||
except KeyError as e:
|
||||
logger.error(f"Key error when accessing role field: {e}")
|
||||
except AttributeError as e:
|
||||
logger.error(f"Attribute error, likely due to accessing a wrong attribute: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"General error getting perm level roles: {e}")
|
||||
logger.error(f"Error getting perm level roles: {e}")
|
||||
return None
|
||||
|
||||
return role_ids
|
||||
|
@ -287,52 +338,26 @@ async def get_perm_level_roles(
|
|||
|
||||
def has_pl(level: int, or_higher: bool = True):
|
||||
"""
|
||||
Check if a user has a permission level via a decorator for prefix and hybrid commands.
|
||||
Check if the source has the required permission level. This is a decorator for traditional "prefix" commands.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
level : int
|
||||
The permission level to check.
|
||||
The permission level required.
|
||||
or_higher : bool, optional
|
||||
Whether the user should have the permission level or higher, by default True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
commands.check
|
||||
The check for the permission level
|
||||
Whether to include "or higher" in the name, by default True.
|
||||
"""
|
||||
|
||||
async def predicate(ctx: commands.Context[commands.Bot] | discord.Interaction) -> bool:
|
||||
"""
|
||||
Check if the user has the permission level.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot] | discord.Interaction
|
||||
The context or interaction object for the command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the user has the permission level.
|
||||
|
||||
Raises
|
||||
------
|
||||
PermissionLevelError
|
||||
If the user does not have the permission level.
|
||||
"""
|
||||
|
||||
if isinstance(ctx, discord.Interaction):
|
||||
logger.error("Incorrect checks decorator used. Please use ac_has_pl instead.")
|
||||
msg = "Incorrect checks decorator used. Please use ac_has_pl instead and report this as a issue."
|
||||
|
||||
raise PermissionLevelError(msg)
|
||||
|
||||
if not await has_permission(ctx, level, 9 if or_higher else None):
|
||||
logger.error(
|
||||
logger.info(
|
||||
f"{ctx.author} tried to run a command without perms. Command: {ctx.command}, Perm Level: {level} or higher: {or_higher}",
|
||||
)
|
||||
|
||||
raise PermissionLevelError(await level_to_name(ctx, level, or_higher))
|
||||
|
||||
logger.info(
|
||||
|
@ -346,52 +371,26 @@ def has_pl(level: int, or_higher: bool = True):
|
|||
|
||||
def ac_has_pl(level: int, or_higher: bool = True):
|
||||
"""
|
||||
Check if a user has a permission level via a decorator for app commands.
|
||||
Check if the source has the required permission level. This is a decorator for application "slash" commands.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
level : int
|
||||
The permission level to check.
|
||||
The permission level required.
|
||||
or_higher : bool, optional
|
||||
Whether the user should have the permission level or higher, by default True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
app_commands.check
|
||||
The check for the permission level
|
||||
Whether to include "or higher" in the name, by default True.
|
||||
"""
|
||||
|
||||
async def predicate(ctx: commands.Context[commands.Bot] | discord.Interaction) -> bool:
|
||||
"""
|
||||
Check if the user has the permission level.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot] | discord.Interaction
|
||||
The context or interaction object for the command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the user has the permission level.
|
||||
|
||||
Raises
|
||||
------
|
||||
AppCommandPermissionLevelError
|
||||
If the user does not have the permission level.
|
||||
"""
|
||||
|
||||
if isinstance(ctx, commands.Context):
|
||||
logger.error("Incorrect checks decorator used. Please use has_pl instead.")
|
||||
msg = "Incorrect checks decorator used. Please use has_pl instead and report this as a issue."
|
||||
|
||||
raise AppCommandPermissionLevelError(msg)
|
||||
|
||||
if not await has_permission(ctx, level, 9 if or_higher else None):
|
||||
logger.error(
|
||||
logger.info(
|
||||
f"{ctx.user} tried to run a command without perms. Command: {ctx.command}, Perm Level: {level} or higher: {or_higher}",
|
||||
)
|
||||
|
||||
raise AppCommandPermissionLevelError(await level_to_name(ctx, level, or_higher))
|
||||
|
||||
logger.info(
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import base64
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import yaml
|
||||
from dotenv import load_dotenv, set_key
|
||||
|
||||
load_dotenv(verbose=True)
|
||||
|
||||
config_file = Path("config/settings.json")
|
||||
config = json.loads(config_file.read_text())
|
||||
config_file = Path("config/settings.yml")
|
||||
config = yaml.safe_load(config_file.read_text())
|
||||
|
||||
|
||||
class Constants:
|
||||
|
@ -29,7 +29,7 @@ class Constants:
|
|||
DEV_COG_IGNORE_LIST: Final[set[str]] = set(os.getenv("DEV_COG_IGNORE_LIST", "").split(","))
|
||||
|
||||
# Debug env constants
|
||||
DEBUG: Final[bool] = bool(os.getenv("DEBUG", True))
|
||||
DEBUG: Final[bool] = bool(os.getenv("DEBUG", "True"))
|
||||
|
||||
# Final env constants
|
||||
TOKEN: Final[str] = DEV_TOKEN if DEV and DEV.lower() == "true" else PROD_TOKEN
|
||||
|
@ -37,7 +37,7 @@ class Constants:
|
|||
COG_IGNORE_LIST: Final[set[str]] = DEV_COG_IGNORE_LIST if DEV and DEV.lower() == "true" else PROD_COG_IGNORE_LIST
|
||||
|
||||
# Sentry-related constants
|
||||
SENTRY_URL: Final[str | None] = os.getenv("SENTRY_URL")
|
||||
SENTRY_URL: Final[str | None] = os.getenv("SENTRY_URL", "")
|
||||
|
||||
# Database constants
|
||||
PROD_DATABASE_URL: Final[str] = os.getenv("PROD_DATABASE_URL", "")
|
||||
|
@ -52,26 +52,21 @@ class Constants:
|
|||
GITHUB_REPO_OWNER: Final[str] = os.getenv("GITHUB_REPO_OWNER", "")
|
||||
GITHUB_REPO: Final[str] = os.getenv("GITHUB_REPO", "")
|
||||
GITHUB_TOKEN: Final[str] = os.getenv("GITHUB_TOKEN", "")
|
||||
GITHUB_APP_ID: Final[int] = int(os.getenv("GITHUB_APP_ID", 0))
|
||||
GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID")
|
||||
GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")
|
||||
GITHUB_PUBLIC_KEY = os.getenv("GITHUB_PUBLIC_KEY")
|
||||
GITHUB_INSTALLATION_ID: Final[int] = int(os.getenv("GITHUB_INSTALLATION_ID", 0))
|
||||
GITHUB_PRIVATE_KEY: str = base64.b64decode(os.getenv("GITHUB_PRIVATE_KEY_BASE64", "")).decode(
|
||||
"utf-8",
|
||||
GITHUB_APP_ID: Final[int] = int(os.getenv("GITHUB_APP_ID", "0"))
|
||||
GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID", "")
|
||||
GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET", "")
|
||||
GITHUB_PUBLIC_KEY = os.getenv("GITHUB_PUBLIC_KEY", "")
|
||||
GITHUB_INSTALLATION_ID: Final[str] = os.getenv("GITHUB_INSTALLATION_ID", "0")
|
||||
GITHUB_PRIVATE_KEY: str = (
|
||||
base64.b64decode(os.getenv("GITHUB_PRIVATE_KEY_BASE64", "")).decode("utf-8")
|
||||
if os.getenv("GITHUB_PRIVATE_KEY_BASE64")
|
||||
else ""
|
||||
)
|
||||
|
||||
# Mailcow constants
|
||||
MAILCOW_API_KEY: Final[str] = os.getenv("MAILCOW_API_KEY", "")
|
||||
MAILCOW_API_URL: Final[str] = os.getenv("MAILCOW_API_URL", "")
|
||||
|
||||
# Channel constants
|
||||
LOG_CHANNELS: Final[dict[str, int]] = config["LOG_CHANNELS"].copy()
|
||||
|
||||
if DEV and DEV.lower() == "true":
|
||||
for key in LOG_CHANNELS:
|
||||
LOG_CHANNELS[key] = LOG_CHANNELS["DEV"]
|
||||
|
||||
# Temp VC constants
|
||||
TEMPVC_CATEGORY_ID: Final[str | None] = config["TEMPVC_CATEGORY_ID"]
|
||||
TEMPVC_CHANNEL_ID: Final[str | None] = config["TEMPVC_CHANNEL_ID"]
|
||||
|
|
14
tux/utils/converters.py
Normal file
14
tux/utils/converters.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from typing import Any
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from prisma.enums import CaseType
|
||||
|
||||
|
||||
class CaseTypeConverter(commands.Converter[CaseType]):
|
||||
async def convert(self, ctx: commands.Context[Any], argument: str) -> CaseType:
|
||||
try:
|
||||
return CaseType[argument.upper()]
|
||||
except KeyError as e:
|
||||
msg = f"Invalid CaseType: {argument}"
|
||||
raise commands.BadArgument(msg) from e
|
14
tux/utils/exceptions.py
Normal file
14
tux/utils/exceptions.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
class PermissionLevelError(commands.CheckFailure):
|
||||
def __init__(self, permission: str) -> None:
|
||||
self.permission = permission
|
||||
super().__init__(f"User does not have the required permission: {permission}")
|
||||
|
||||
|
||||
class AppCommandPermissionLevelError(app_commands.CheckFailure):
|
||||
def __init__(self, permission: str) -> None:
|
||||
self.permission = permission
|
||||
super().__init__(f"User does not have the required permission: {permission}")
|
|
@ -3,18 +3,19 @@ from discord.ext import commands
|
|||
from discord.utils import MISSING
|
||||
|
||||
from prisma.enums import CaseType
|
||||
from tux.utils.converters import CaseTypeConverter
|
||||
|
||||
|
||||
class BanFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class BanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the member ban.",
|
||||
description="The reason for the ban.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
purge_days: int = commands.flag(
|
||||
name="purge_days",
|
||||
description="Number of days in messages",
|
||||
description="The number of days (< 7) to purge in messages.",
|
||||
aliases=["p", "purge"],
|
||||
default=0,
|
||||
)
|
||||
|
@ -26,17 +27,17 @@ class BanFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
|||
)
|
||||
|
||||
|
||||
class TempBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class TempBanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the member temp ban.",
|
||||
description="The reason for the temp ban.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
expires_at: int = commands.flag(
|
||||
name="expires_at",
|
||||
description="The time in days the ban will last for.",
|
||||
aliases=["t", "d", "e"],
|
||||
aliases=["t", "d", "e", "duration", "expires", "time"],
|
||||
)
|
||||
purge_days: int = commands.flag(
|
||||
name="purge_days",
|
||||
|
@ -44,12 +45,18 @@ class TempBanFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
|||
aliases=["p"],
|
||||
default=0,
|
||||
)
|
||||
silent: bool = commands.flag(
|
||||
name="silent",
|
||||
description="Do not send a DM to the target.",
|
||||
aliases=["s", "quiet"],
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class KickFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class KickFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the member kick.",
|
||||
description="The reason for the kick.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
|
@ -61,16 +68,16 @@ class KickFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
|||
)
|
||||
|
||||
|
||||
class TimeoutFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class TimeoutFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
duration: str = commands.flag(
|
||||
name="duration",
|
||||
description="The duration of the timeout.",
|
||||
description="The duration of the timeout. (e.g. 1d, 1h, 1m)",
|
||||
aliases=["d"],
|
||||
default=MISSING,
|
||||
)
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the member ban. (e.g. 1d, 1h, 1m)",
|
||||
description="The reason for the timeout.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
|
@ -82,10 +89,10 @@ class TimeoutFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
|||
)
|
||||
|
||||
|
||||
class UntimeoutFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class UntimeoutFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the member ban.",
|
||||
description="The reason for the untimeout.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
|
@ -97,26 +104,26 @@ class UntimeoutFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
|||
)
|
||||
|
||||
|
||||
class UnbanFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class UnbanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
username_or_id: str = commands.flag(
|
||||
name="username_or_id",
|
||||
description="The username or ID of the user to ban.",
|
||||
description="The username or ID of the user.",
|
||||
aliases=["u"],
|
||||
default=MISSING,
|
||||
positional=True,
|
||||
)
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the member ban.",
|
||||
description="The reason for the unban.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
|
||||
|
||||
class JailFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class JailFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the member jail.",
|
||||
description="The reason for the jail.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
|
@ -128,10 +135,10 @@ class JailFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
|||
)
|
||||
|
||||
|
||||
class UnjailFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class UnjailFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the member unjail.",
|
||||
description="The reason for the unjail.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
|
@ -143,20 +150,21 @@ class UnjailFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
|||
)
|
||||
|
||||
|
||||
class CasesViewFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class CasesViewFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
type: CaseType = commands.flag(
|
||||
name="case_type",
|
||||
description="The case type to view.",
|
||||
aliases=["t"],
|
||||
default=None,
|
||||
converter=CaseTypeConverter,
|
||||
)
|
||||
target: discord.Member = commands.flag(
|
||||
target: discord.User = commands.flag(
|
||||
name="case_target",
|
||||
description="The member to view cases for.",
|
||||
aliases=["memb", "m", "user", "u"],
|
||||
description="The user to view cases for.",
|
||||
aliases=["user", "u", "member", "memb", "m"],
|
||||
default=None,
|
||||
)
|
||||
moderator: discord.Member = commands.flag(
|
||||
moderator: discord.User = commands.flag(
|
||||
name="case_moderator",
|
||||
description="The moderator to view cases for.",
|
||||
aliases=["mod"],
|
||||
|
@ -164,7 +172,7 @@ class CasesViewFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
|||
)
|
||||
|
||||
|
||||
class CaseModifyFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class CaseModifyFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
status: bool | None = commands.flag(
|
||||
name="case_status",
|
||||
description="The status of the case.",
|
||||
|
@ -177,10 +185,40 @@ class CaseModifyFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
|||
)
|
||||
|
||||
|
||||
class WarnFlags(commands.FlagConverter, delimiter=" ", prefix="-"):
|
||||
class WarnFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the member warn.",
|
||||
description="The reason for the warn.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
silent: bool = commands.flag(
|
||||
name="silent",
|
||||
description="Do not send a DM to the target.",
|
||||
aliases=["s", "quiet"],
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class SnippetBanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the snippet ban.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
silent: bool = commands.flag(
|
||||
name="silent",
|
||||
description="Do not send a DM to the target.",
|
||||
aliases=["s", "quiet"],
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class SnippetUnbanFlags(commands.FlagConverter, case_insensitive=True, delimiter=" ", prefix="-"):
|
||||
reason: str = commands.flag(
|
||||
name="reason",
|
||||
description="The reason for the snippet unban.",
|
||||
aliases=["r"],
|
||||
default=MISSING,
|
||||
)
|
||||
|
|
|
@ -17,7 +17,7 @@ class GithubService:
|
|||
AppInstallationAuthStrategy(
|
||||
CONST.GITHUB_APP_ID,
|
||||
CONST.GITHUB_PRIVATE_KEY,
|
||||
CONST.GITHUB_INSTALLATION_ID,
|
||||
int(CONST.GITHUB_INSTALLATION_ID),
|
||||
CONST.GITHUB_CLIENT_ID,
|
||||
CONST.GITHUB_CLIENT_SECRET,
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue