├── .dockerignore ├── .env ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ └── disabled │ └── GearBotUpdater.yml ├── .gitignore ├── Bootloader.bat ├── Bootloader.sh ├── BuildCraft ├── FAQs.png └── uploader │ ├── .gitignore │ ├── build.gradle │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── Dockerfile ├── GearBot ├── Bot │ ├── GearBot.py │ ├── Reloader.py │ └── TheRealGearBot.py ├── Cogs │ ├── Admin.py │ ├── AntiRaid.py │ ├── AntiSpam.py │ ├── BCVersionChecker.py │ ├── BaseCog.py │ ├── Basic.py │ ├── Censor.py │ ├── CustCommands.py │ ├── DMMessages.py │ ├── DashLink.py │ ├── Emoji.py │ ├── Fun.py │ ├── Infractions.py │ ├── Interactions.py │ ├── Minecraft.py │ ├── ModLog.py │ ├── Moderation.py │ ├── PromMonitoring.py │ ├── ReactionHandler.py │ ├── Reload.py │ ├── Reminders.py │ └── ServerAdmin.py ├── GearBot.py ├── Util │ ├── Actions.py │ ├── Archive.py │ ├── Configuration.py │ ├── Converters.py │ ├── DashConfig.py │ ├── DashUtils.py │ ├── DocUtils.py │ ├── Emoji.py │ ├── Enums.py │ ├── Features.py │ ├── GearbotLogging.py │ ├── HelpGenerator.py │ ├── InfractionUtils.py │ ├── JumboGenerator.py │ ├── Matchers.py │ ├── MessageUtils.py │ ├── Pages.py │ ├── Permissioncheckers.py │ ├── PromMonitors.py │ ├── Questions.py │ ├── RaidHandling │ │ ├── RaidActions.py │ │ ├── RaidShield.py │ │ └── __init__.py │ ├── ReactionManager.py │ ├── Selfroles.py │ ├── SpamBucket.py │ ├── Translator.py │ ├── Update.py │ ├── Utils.py │ ├── VersionInfo.py │ └── server_info.py ├── database │ ├── DBUtils.py │ └── DatabaseConnector.py └── views │ ├── Buttons.py │ ├── Confirm.py │ ├── EphemeralInfSearch.py │ ├── ExtendMute.py │ ├── GlobalInfSearch.py │ ├── Help.py │ ├── InfSearch.py │ ├── PagedText.py │ ├── Reminder.py │ ├── SelfRole.py │ └── SimplePager.py ├── LICENSE ├── README.md ├── SECURITY.md ├── clusterloader.sh ├── config ├── .gitignore └── master.json.example ├── datamove.py ├── docs ├── pages │ ├── 01.home │ │ └── home.md │ └── 03.docs │ │ ├── 02.setup │ │ ├── 01.adding_gearbot │ │ │ └── doc.md │ │ ├── 02.configuring_prefix │ │ │ └── doc.md │ │ ├── 03.intro_permissions │ │ │ └── doc.md │ │ ├── 04.language │ │ │ └── doc.md │ │ ├── 05.roles │ │ │ └── doc.md │ │ ├── 06.command_requirements │ │ │ └── doc.md │ │ ├── 07.logging │ │ │ └── doc.md │ │ ├── 08.censoring │ │ │ └── doc.md │ │ ├── 09.self_roles │ │ │ └── doc.md │ │ ├── 10.custom_commands │ │ │ └── doc.md │ │ ├── 11.ignoring_channels │ │ │ └── doc.md │ │ ├── 12.misc │ │ │ └── doc.md │ │ └── doc.md │ │ ├── 03.guides │ │ ├── 01.archiving │ │ │ └── doc.md │ │ ├── 02.infractions │ │ │ └── doc.md │ │ └── doc.md │ │ ├── 04.supporting │ │ └── doc.md │ │ └── doc.md └── theme │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── blueprints.yaml │ ├── gearbot.php │ ├── gearbot.yaml │ ├── images │ ├── favicon.ico │ ├── gear.svg │ ├── gearGreen.png │ ├── gearYellow.png │ ├── gearbot.png │ ├── logo.gif │ └── logo.png │ ├── screenshot.jpg │ ├── scss │ ├── base.scss │ ├── docs.scss │ └── home.scss │ ├── templates │ ├── default.html.twig │ ├── doc.html.twig │ ├── error.html.twig │ ├── home.html.twig │ └── partials │ │ ├── base.html.twig │ │ ├── doc_navigation.html.twig │ │ ├── langswitcher.html.twig │ │ ├── metadata.html.twig │ │ └── navigation.html.twig │ └── thumbnail.jpg ├── lang ├── .gitignore ├── bot.json ├── en_US.json └── shared.json ├── migration ├── __init__.py ├── add_config_table.sql ├── infractions.py └── rowboat.py ├── requirements.txt └── template.json /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !requirements.txt 3 | !GearBot/* 4 | !lang/* 5 | !template.json -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=GearBot/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 'Report a bug ' 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Steps To Reproduce** 14 | 1. Do this 15 | 2. Do that 16 | 4. See problem 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: new feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **What does this PR add/fix/improve/...** 2 | Please give some general info about the PR 3 | 4 | **Migration** 5 | If any migration is required for already setup instances please provide instructions/requirements here 6 | 7 | **Dependencies** 8 | If this requires any other PRs to be merged and deployed in other GearBot repositories please link them here 9 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 12 * * 2' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | # We must fetch at least the immediate parents so that if this is 19 | # a pull request then we can checkout the head. 20 | fetch-depth: 2 21 | 22 | # If this run was triggered by a pull request event, then checkout 23 | # the head of the pull request instead of the merge commit. 24 | - run: git checkout HEAD^2 25 | if: ${{ github.event_name == 'pull_request' }} 26 | 27 | # Initializes the CodeQL tools for scanning. 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v1 30 | # Override language selection by uncommenting this and choosing your languages 31 | with: 32 | languages: python 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v1 35 | -------------------------------------------------------------------------------- /.github/workflows/disabled/GearBotUpdater.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: GearBot Updater 6 | jobs: 7 | deploy: 8 | name: GearBot Updater 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Determine update type 12 | uses: gearbot/update-action@master 13 | - name: Trigger update 14 | uses: AEnterprise/discord-webhook@master 15 | env: 16 | DATA: '{"key": "${{ secrets.UPDATE_KEY }}","type": "_UPDATE_STRATEGY_"}' 17 | DISCORD_WEBHOOK: ${{ secrets.UPDATE_URL }} 18 | PLACEHOLDER_KEY: '_UPDATE_STRATEGY_' 19 | 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | venv/ 5 | 6 | site/ 7 | .idea/ 8 | .gradle/ 9 | logs/ 10 | *.json 11 | !template.json 12 | -------------------------------------------------------------------------------- /Bootloader.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | IF EXIST upgradeRequest ( 4 | git pull origin 5 | py -3 -m pip install -U -r requirements.txt --user 6 | del upgradeRequest 7 | ) 8 | IF NOT EXIST stage_3.txt ( 9 | py -3 GearBot/GearBot.py 10 | ) 11 | -------------------------------------------------------------------------------- /Bootloader.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | if [ -s upgradeRequest ]; then 3 | git pull origin 4 | python3 -m pip install -U -r requirements.txt --user 5 | rm -rf upgradeRequest 6 | fi 7 | if ! [ -s stage_3.txt ]; then 8 | python3 GearBot/GearBot.py 9 | fi 10 | -------------------------------------------------------------------------------- /BuildCraft/FAQs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gearbot/GearBot/b91c48eafde0ef4612442b33f10e70d2475c5489/BuildCraft/FAQs.png -------------------------------------------------------------------------------- /BuildCraft/uploader/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | gradle 3 | bc_download_artifacts 4 | api_key.txt -------------------------------------------------------------------------------- /BuildCraft/uploader/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /BuildCraft/uploader/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /BuildCraft/uploader/settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This settings file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * In a single project build this file can be empty or even removed. 6 | * 7 | * Detailed information about configuring a multi-project build in Gradle can be found 8 | * in the user guide at https://docs.gradle.org/3.5/userguide/multi_project_builds.html 9 | */ 10 | 11 | /* 12 | // To declare projects as part of a multi-project build use the 'include' method 13 | include 'shared' 14 | include 'api' 15 | include 'services:webservice' 16 | */ 17 | 18 | rootProject.name = 'BC uploader' 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | WORKDIR /GearBot 3 | COPY requirements.txt ./ 4 | RUN pip3 install --no-cache-dir -r requirements.txt 5 | COPY . . 6 | CMD ["python", "./GearBot/GearBot.py"] -------------------------------------------------------------------------------- /GearBot/Bot/GearBot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from asyncio import Queue 4 | from collections import deque 5 | 6 | from disnake.ext.commands import AutoShardedBot 7 | from prometheus_client import CollectorRegistry 8 | 9 | from Bot import TheRealGearBot 10 | from Util.PromMonitors import PromMonitors 11 | 12 | 13 | class GearBot(AutoShardedBot): 14 | STARTUP_COMPLETE = False 15 | user_messages = 0 16 | bot_messages = 0 17 | self_messages = 0 18 | commandCount = 0 19 | custom_command_count = 0 20 | errors = 0 21 | eaten = 0 22 | database_errors = 0, 23 | database_connection = None 24 | locked = True 25 | redis_pool = None 26 | aiosession = None 27 | being_cleaned = dict() 28 | metrics_reg = CollectorRegistry() 29 | version = "" 30 | dash_guild_users = set() 31 | dash_guild_watchers = dict() 32 | cluster = 0 33 | shard_count = 1 34 | shard_ids = [], 35 | chunker_active = False 36 | chunker_pending = False 37 | chunker_should_terminate = False 38 | chunker_queue = Queue() 39 | deleted_messages = deque(maxlen=500) 40 | 41 | def __init__(self, *args, loop=None, **kwargs): 42 | super().__init__(*args, loop=loop, **kwargs) 43 | self.metrics = PromMonitors(self, kwargs.get("monitoring_prefix", "gearbot")) 44 | self.cluster = kwargs.get("cluster", 0) 45 | self.total_shards = kwargs.get("shard_count", 1) 46 | self.shard_ids = kwargs.get("shard_ids", [0]) 47 | 48 | def dispatch(self, event_name, *args, **kwargs): 49 | if "socket" not in event_name not in ["message_edit"]: 50 | self.metrics.bot_event_counts.labels(event_name=event_name, cluster=self.cluster).inc() 51 | super().dispatch(event_name, *args, **kwargs) 52 | 53 | #### event handlers, basically bouncing everything to TheRealGearBot file so we can hotreload our listeners 54 | 55 | async def on_connect(self): 56 | await TheRealGearBot.on_connect(self) 57 | 58 | async def on_ready(self): 59 | await TheRealGearBot.on_ready(self) 60 | 61 | async def on_message(self, message): 62 | await TheRealGearBot.on_message(self, message) 63 | 64 | async def on_guild_join(self, guild): 65 | await TheRealGearBot.on_guild_join(self, guild) 66 | 67 | async def on_guild_remove(self, guild): 68 | await TheRealGearBot.on_guild_remove(self, guild) 69 | 70 | async def on_command_error(self, ctx, error): 71 | await TheRealGearBot.on_command_error(self, ctx, error) 72 | 73 | async def on_error(self, event, *args, **kwargs): 74 | await TheRealGearBot.on_error(self, event, *args, **kwargs) 75 | 76 | async def on_guild_update(self, before, after): 77 | await TheRealGearBot.on_guild_update(before, after) 78 | 79 | #### reloading 80 | -------------------------------------------------------------------------------- /GearBot/Bot/Reloader.py: -------------------------------------------------------------------------------- 1 | from Bot import TheRealGearBot 2 | from Cogs import BaseCog 3 | from Util import Configuration, GearbotLogging, Emoji, Pages, Utils, Translator, Converters, Permissioncheckers, \ 4 | VersionInfo, HelpGenerator, InfractionUtils, Archive, DocUtils, JumboGenerator, MessageUtils, Enums, \ 5 | Matchers, Selfroles, ReactionManager, server_info, DashConfig, Update, DashUtils, Actions, Features 6 | from Util.RaidHandling import RaidActions, RaidShield 7 | from database import DBUtils 8 | 9 | components = [ 10 | Configuration, 11 | GearbotLogging, 12 | Permissioncheckers, 13 | Utils, 14 | VersionInfo, 15 | Emoji, 16 | HelpGenerator, 17 | Pages, 18 | InfractionUtils, 19 | Archive, 20 | Translator, 21 | DocUtils, 22 | JumboGenerator, 23 | MessageUtils, 24 | TheRealGearBot, 25 | Converters, 26 | Enums, 27 | Matchers, 28 | RaidActions, 29 | RaidShield, 30 | ReactionManager, 31 | Selfroles, 32 | DBUtils, 33 | server_info, 34 | DashConfig, 35 | Update, 36 | BaseCog, 37 | DashUtils, 38 | Actions, 39 | Features 40 | ] 41 | -------------------------------------------------------------------------------- /GearBot/Cogs/BaseCog.py: -------------------------------------------------------------------------------- 1 | from disnake.ext import commands 2 | 3 | from Bot.GearBot import GearBot 4 | from Util import Permissioncheckers 5 | 6 | 7 | class BaseCog(commands.Cog): 8 | def __init__(self, bot): 9 | self.bot: GearBot = bot 10 | name = self.__class__.__name__ 11 | self.permissions = cog_permissions[name] if name in cog_permissions else None 12 | 13 | async def cog_check(self, ctx): 14 | return await Permissioncheckers.check_permission(ctx.command, ctx.guild, ctx.author, self.bot) 15 | 16 | 17 | # Reference Permissions: 18 | """ 19 | ╔════╦═════════════════╦═══════════════════════════════════════════════════╗ 20 | ║ Nr ║ Name ║ Requirement ║ 21 | ╠════╬═════════════════╬═══════════════════════════════════════════════════╣ 22 | ║ 0 ║ Public ║ Everyone ║ 23 | ║ 1 ║ Trusted ║ People with a trusted role or mod+ ║ 24 | ║ 2 ║ Mod ║ People with ban permissions or admin+ ║ 25 | ║ 3 ║ Admin ║ People with administrator perms or an admin role ║ 26 | ║ 4 ║ Specific people ║ People you added to the whitelist for a command ║ 27 | ║ 5 ║ Server owner ║ The person who owns the server ║ 28 | ║ 6 ║ Disabled ║ Perm level nobody can get, used to disable stuff ║ 29 | ╚════╩═════════════════╩═══════════════════════════════════════════════════╝ 30 | """ 31 | 32 | # All cog permissions lookup table, sorted alphabetically 33 | # The keys are the class name, with identical capitalization 34 | # The above allows for fancy, clean, lookups for what permissions to use 35 | cog_permissions = { 36 | "AntiRaid": { 37 | "min": 1, 38 | "max": 6, 39 | "required": 2, 40 | "commands": { 41 | "enable": {"required": 3, "min": 1, "max": 6, "commands": {}}, 42 | "disable": {"required": 3, "min": 1, "max": 6, "commands": {}}, 43 | } 44 | }, 45 | 46 | "Basic": { 47 | "min": 0, 48 | "max": 6, 49 | "required": 0, 50 | "commands": {} 51 | }, 52 | 53 | "BCVersionChecker": { 54 | "min": 0, 55 | "max": 6, 56 | "required": 0, 57 | "commands": {} 58 | }, 59 | 60 | "CustCommands": { 61 | "min": 0, 62 | "max": 6, 63 | "required": 0, 64 | "commands": { 65 | "commands": { 66 | "required": 0, 67 | "min": 0, 68 | "max": 6, 69 | "commands": { 70 | "create": {"required": 2, "min": 1, "max": 6, "commands": {}}, 71 | "remove": {"required": 2, "min": 1, "max": 6, "commands": {}}, 72 | "update": {"required": 2, "min": 1, "max": 6, "commands": {}}, 73 | } 74 | } 75 | } 76 | }, 77 | 78 | "Emoji": { 79 | "min": 1, 80 | "max": 6, 81 | "required": 3, 82 | "commands": { 83 | "emoji": { 84 | "min": 1, 85 | "max": 6, 86 | "required": 3, 87 | "commands": { 88 | "list": { 89 | "min": 0, 90 | "max": 6, 91 | "required": 3 92 | } 93 | } 94 | } 95 | } 96 | }, 97 | 98 | "Fun": { 99 | "min": 0, 100 | "max": 6, 101 | "required": 0, 102 | "commands": {} 103 | }, 104 | 105 | "Infractions": { 106 | "min": 1, 107 | "max": 6, 108 | "required": 2, 109 | "commands": { 110 | "inf": { 111 | "required": 2, 112 | "min": 1, 113 | "max": 6, 114 | "commands": { 115 | "delete": {"required": 5, "min": 1, "max": 6} 116 | } 117 | } 118 | } 119 | }, 120 | 121 | "Minecraft": { 122 | "min": 0, 123 | "max": 6, 124 | "required": 0, 125 | "commands": {} 126 | }, 127 | 128 | "Moderation": { 129 | "min": 1, 130 | "max": 6, 131 | "required": 2, 132 | "commands": { 133 | "userinfo": {"required": 2, "min": 0, "max": 6}, 134 | "serverinfo": {"required": 2, "min": 0, "max": 6}, 135 | "roles": {"required": 2, "min": 0, "max": 6}, 136 | "verification": {"required": 3, "min": 2, "max": 6}, 137 | } 138 | }, 139 | 140 | "Reminders": { 141 | "min": 0, 142 | "max": 6, 143 | "required": 0, 144 | "commands": {} 145 | }, 146 | 147 | "ServerAdmin": { 148 | "min": 2, 149 | "max": 5, 150 | "required": 3, 151 | "commands": { 152 | "configure": { 153 | "min": 2, 154 | "max": 5, 155 | "required": 3, 156 | "commands": { 157 | "lvl4": {"required": 5, "min": 4, "max": 6} 158 | } 159 | }, 160 | "reset_guild_cache": {"required": 2, "min": 1, "max": 6} 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /GearBot/Cogs/DMMessages.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake.ext import commands 3 | 4 | from Cogs.BaseCog import BaseCog 5 | from Util import Configuration 6 | 7 | 8 | class DMMessages(BaseCog): 9 | 10 | @commands.Cog.listener() 11 | async def on_message(self, message: disnake.Message): 12 | if message.guild is not None or len(message.content) > 1800 or message.author.id == self.bot.user.id: 13 | return 14 | ctx: commands.Context = await self.bot.get_context(message) 15 | if ctx.command is None: 16 | channel = self.bot.get_channel(Configuration.get_master_var("inbox", 0)) 17 | if channel is not None: 18 | await channel.send(f"[`{message.created_at.strftime('%c')}`] {message.author} (`{message.author.id}`) said: {message.clean_content}") 19 | for attachement in message.attachments: 20 | await channel.send(attachement.url) 21 | 22 | 23 | def setup(bot): 24 | bot.add_cog(DMMessages(bot)) -------------------------------------------------------------------------------- /GearBot/Cogs/Fun.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import datetime 4 | 5 | import disnake 6 | from disnake.ext import commands 7 | 8 | from Cogs.BaseCog import BaseCog 9 | from Util import Configuration, MessageUtils, Translator, Utils 10 | from Util.Converters import ApexPlatform 11 | from Util.JumboGenerator import JumboGenerator 12 | 13 | 14 | class Fun(BaseCog): 15 | 16 | def __init__(self, bot): 17 | super().__init__(bot) 18 | 19 | to_remove = { 20 | "CAT_KEY": "cat", 21 | "DOG_KEY": "dog", 22 | "APEX_KEY": "apexstats" 23 | } 24 | for k, v in to_remove.items(): 25 | if Configuration.get_master_var(k, "0") == "0": 26 | bot.remove_command(v) 27 | 28 | @commands.command() 29 | @commands.bot_has_permissions(embed_links=True) 30 | async def apexstats(self, ctx, platform: ApexPlatform, *, username): 31 | """about_apexstats""" 32 | headers = {"TRN-Api-Key": Configuration.get_master_var("APEX_KEY")} 33 | url = "https://public-api.tracker.gg/apex/v1/standard/profile/" + platform + "/" + (username) 34 | async with self.bot.aiosession.get(url, headers=headers) as resp: 35 | if resp.status == 404: 36 | await MessageUtils.send_to(ctx, "NO", "apexstats_user_not_found") 37 | return 38 | elif not resp.status == 200: 39 | await MessageUtils.send_to(ctx, "NO", "apexstats_api_error") 40 | return 41 | else: 42 | responsejson = await resp.json() 43 | embed = disnake.Embed(colour=disnake.Colour(0x00cea2), timestamp=datetime.datetime.utcfromtimestamp(time.time()).replace(tzinfo=datetime.timezone.utc)) 44 | embed.add_field(name=Translator.translate('apexstats_username', ctx), value=await Utils.clean(responsejson["data"]["metadata"]["platformUserHandle"])) 45 | for stat_type in responsejson["data"]["stats"]: 46 | type_key_name = stat_type["metadata"]["key"] 47 | type_key_value = stat_type["displayValue"] 48 | embed.add_field(name=Translator.translate(f'apexstats_key_{type_key_name}', ctx), value=type_key_value) 49 | await ctx.send(embed=embed) 50 | 51 | @commands.command() 52 | @commands.bot_has_permissions(embed_links=True) 53 | async def dog(self, ctx): 54 | """dog_help""" 55 | await ctx.trigger_typing() 56 | future_fact = self.get_json("https://animal.gearbot.rocks/dog/fact") 57 | key = Configuration.get_master_var("DOG_KEY", "") 58 | future_dog = self.get_json("https://api.thedogapi.com/v1/images/search?limit=1&size=full", {'x-api-key': key}) 59 | fact_json, dog_json = await asyncio.gather(future_fact, future_dog) 60 | embed = disnake.Embed(description=fact_json["content"]) 61 | if key != "": 62 | embed.set_image(url=dog_json[0]["url"]) 63 | await ctx.send(embed=embed) 64 | 65 | @commands.command() 66 | @commands.bot_has_permissions(embed_links=True) 67 | async def cat(self, ctx): 68 | """cat_help""" 69 | await ctx.trigger_typing() 70 | future_fact = self.get_json("https://animal.gearbot.rocks/cat/fact") 71 | key = Configuration.get_master_var("CAT_KEY", "") 72 | future_cat = self.get_json("https://api.thecatapi.com/v1/images/search?limit=1&size=full", {'x-api-key': key}) 73 | fact_json, cat_json = await asyncio.gather(future_fact, future_cat) 74 | embed = disnake.Embed(description=fact_json["content"]) 75 | if key != "": 76 | embed.set_image(url=cat_json[0]["url"]) 77 | await ctx.send(embed=embed) 78 | 79 | async def get_json(self, link, headers=None): 80 | async with self.bot.aiosession.get(link, headers=headers) as reply: 81 | return await reply.json() 82 | 83 | 84 | @commands.command() 85 | @commands.bot_has_permissions(attach_files=True) 86 | async def jumbo(self, ctx, *, emojis: str): 87 | """jumbo_help""" 88 | await JumboGenerator(ctx, emojis).generate() 89 | 90 | def setup(bot): 91 | bot.add_cog(Fun(bot)) 92 | -------------------------------------------------------------------------------- /GearBot/Cogs/PromMonitoring.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import disnake 4 | from aiohttp import web 5 | from disnake.ext import commands 6 | from prometheus_client.exposition import generate_latest 7 | 8 | from Cogs.BaseCog import BaseCog 9 | 10 | 11 | class PromMonitoring(BaseCog): 12 | 13 | 14 | def __init__(self, bot): 15 | super().__init__(bot) 16 | self.running = True 17 | self.bot.loop.create_task(self.create_site()) 18 | 19 | def cog_unload(self): 20 | self.running = False 21 | self.bot.loop.create_task(self.metric_server.stop()) 22 | 23 | 24 | @commands.Cog.listener() 25 | async def on_command_completion(self, ctx): 26 | self.bot.metrics.command_counter.labels( 27 | cluster=self.bot.cluster, 28 | command_name= ctx.command.qualified_name, 29 | ).inc() 30 | 31 | @commands.Cog.listener() 32 | async def on_message(self, message: disnake.Message): 33 | m = self.bot.metrics 34 | 35 | 36 | (m.own_message_raw_count if message.author.id == self.bot.user.id else m.bot_message_raw_count if message.author.bot else m.user_message_raw_count).labels(cluster=self.bot.cluster).inc() 37 | 38 | async def create_site(self): 39 | await asyncio.sleep(15) 40 | metrics_app = web.Application() 41 | metrics_app.add_routes([web.get("/metrics", self.serve_metrics)]) 42 | 43 | runner = web.AppRunner(metrics_app) 44 | await self.bot.loop.create_task(runner.setup()) 45 | site = web.TCPSite(runner, host='0.0.0.0', port=8090) 46 | 47 | await site.start() 48 | 49 | self.metric_server = site 50 | 51 | async def serve_metrics(self, request): 52 | self.bot.metrics.bot_users.labels(cluster=self.bot.cluster).set(sum(len(g.members) for g in self.bot.guilds)) 53 | self.bot.metrics.bot_users_unique.labels(cluster=self.bot.cluster).set(len(self.bot.users)) 54 | self.bot.metrics.bot_guilds.labels(cluster=self.bot.cluster).set(len(self.bot.guilds)) 55 | self.bot.metrics.bot_latency.labels(cluster=self.bot.cluster).set((self.bot.latency)) 56 | 57 | metrics_to_server = generate_latest(self.bot.metrics_reg).decode("utf-8") 58 | return web.Response(text=metrics_to_server, content_type="text/plain") 59 | 60 | 61 | def setup(bot): 62 | bot.add_cog(PromMonitoring(bot)) 63 | -------------------------------------------------------------------------------- /GearBot/Cogs/ReactionHandler.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake.ext import commands 3 | 4 | from Cogs.BaseCog import BaseCog 5 | from Util import ReactionManager 6 | 7 | 8 | class ReactionHandler(BaseCog): 9 | 10 | @commands.Cog.listener() 11 | async def on_raw_reaction_add(self, payload: disnake.RawReactionActionEvent): 12 | await ReactionManager.on_reaction(self.bot, payload.message_id, payload.channel_id, payload.user_id, payload.emoji) 13 | 14 | 15 | @commands.Cog.listener() 16 | async def on_guild_remove(self, guild): 17 | pipe = self.bot.redis_pool.pipeline() 18 | pipe.unlink(f"joins:{guild.id}") 19 | pipe.unlink(f"inf_track:{guild.id}") 20 | await pipe.execute() 21 | 22 | def setup(bot): 23 | bot.add_cog(ReactionHandler(bot)) -------------------------------------------------------------------------------- /GearBot/Cogs/Reload.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from disnake.ext import commands 4 | 5 | from Cogs.BaseCog import BaseCog 6 | from Util import GearbotLogging, Emoji, Utils, Pages, Configuration, DocUtils, Update 7 | 8 | 9 | class Reload(BaseCog): 10 | 11 | def __init__(self, bot): 12 | super().__init__(bot) 13 | # Pages.register("pull", self.init_pull, self.update_pull) 14 | 15 | async def cog_check (self, ctx): 16 | return await ctx.bot.is_owner(ctx.author) or ctx.author.id in Configuration.get_master_var("BOT_ADMINS", []) 17 | 18 | @commands.command(hidden=True) 19 | async def reload(self, ctx, *, cog: str): 20 | cogs = [] 21 | for c in ctx.bot.cogs: 22 | cogs.append(c.replace('Cog', '')) 23 | 24 | if cog in cogs: 25 | self.bot.unload_extension(f"Cogs.{cog}") 26 | self.bot.load_extension(f"Cogs.{cog}") 27 | await ctx.send(f'**{cog}** has been reloaded.') 28 | await GearbotLogging.bot_log(f'**{cog}** has been reloaded by {ctx.author.name}.') 29 | else: 30 | await ctx.send(f"{Emoji.get_chat_emoji('NO')} I can't find that cog.") 31 | 32 | @commands.command(hidden=True) 33 | async def load(self, ctx, cog: str): 34 | if os.path.isfile(f"Cogs/{cog}.py") or os.path.isfile(f"GearBot/Cogs/{cog}.py"): 35 | self.bot.load_extension(f"Cogs.{cog}") 36 | await ctx.send(f"**{cog}** has been loaded!") 37 | await GearbotLogging.bot_log(f"**{cog}** has been loaded by {ctx.author.name}.") 38 | GearbotLogging.info(f"{cog} has been loaded") 39 | else: 40 | await ctx.send(f"{Emoji.get_chat_emoji('NO')} I can't find that cog.") 41 | 42 | @commands.command(hidden=True) 43 | async def unload(self, ctx, cog: str): 44 | if cog in ctx.bot.cogs: 45 | self.bot.unload_extension(f"Cogs.{cog}") 46 | await ctx.send(f'**{cog}** has been unloaded.') 47 | await GearbotLogging.bot_log(f'**{cog}** has been unloaded by {ctx.author.name}') 48 | GearbotLogging.info(f"{cog} has been unloaded") 49 | else: 50 | await ctx.send(f"{Emoji.get_chat_emoji('NO')} I can't find that cog.") 51 | 52 | @commands.command(hidden=True) 53 | async def hotreload(self, ctx:commands.Context): 54 | ctx_message = await ctx.send(f"{Emoji.get_chat_emoji('REFRESH')} Hot reload in progress...") 55 | await Update.update(ctx.author.name, self.bot) 56 | m = f"{Emoji.get_chat_emoji('YES')} Hot reload complete, now running on {self.bot.version}" 57 | await ctx_message.edit(content=m) 58 | 59 | @commands.command() 60 | async def update_site(self, ctx): 61 | GearbotLogging.info("Site update initiated") 62 | message = await ctx.send(f"{Emoji.get_chat_emoji('REFRESH')} Updating site") 63 | await DocUtils.generate_command_list(ctx.bot, message) 64 | cloudflare_info = Configuration.get_master_var("CLOUDFLARE", {}) 65 | if 'ZONE' in cloudflare_info: 66 | headers = { 67 | "X-Auth-Email": cloudflare_info["EMAIL"], 68 | "X-Auth-Key": cloudflare_info["KEY"], 69 | "Content-Type": "application/json" 70 | } 71 | async with self.bot.aiosession.post( 72 | f"https://api.cloudflare.com/client/v4/zones/{cloudflare_info['ZONE']}/purge_cache", 73 | json=dict(purge_everything=True), headers=headers) as reply: 74 | content = await reply.json() 75 | GearbotLogging.info(f"Cloudflare purge response: {content}") 76 | if content["success"]: 77 | await message.edit(content=f"{Emoji.get_chat_emoji('YES')} Site has been updated and cloudflare cache has been purged") 78 | else: 79 | await message.edit(content=f"{Emoji.get_chat_emoji('NO')} Cloudflare cache purge failed") 80 | 81 | # @commands.command() 82 | async def pull(self, ctx): 83 | """Pulls from github so an upgrade can be performed without full restart""" 84 | async with ctx.typing(): 85 | code, out, error = await Utils.execute(["git pull origin master"]) 86 | if code == 0: 87 | await Pages.create_new(self.bot, "pull", ctx, title=f"{Emoji.get_chat_emoji('YES')} Pull completed with exit code {code}", pages="----NEW PAGE----".join(Pages.paginate(out))) 88 | else: 89 | await ctx.send(f"{Emoji.get_chat_emoji('NO')} Pull completed with exit code {code}```yaml\n{out}\n{error}```") 90 | 91 | async def init_pull(self, ctx, title, pages): 92 | pages = pages.split("----NEW PAGE----") 93 | page = pages[0] 94 | num = len(pages) 95 | return f"**{title} (1/{num})**\n```yaml\n{page}```", None, num > 1, 96 | 97 | async def update_pull(self, ctx, message, page_num, action, data): 98 | pages = data["pages"].split("----NEW PAGE----") 99 | title = data["title"] 100 | page, page_num = Pages.basic_pages(pages, page_num, action) 101 | data["page"] = page_num 102 | return f"**{title} ({page_num + 1}/{len(pages)})**\n```yaml\n{page}```", None, data 103 | 104 | def setup(bot): 105 | bot.add_cog(Reload(bot)) -------------------------------------------------------------------------------- /GearBot/Cogs/Reminders.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import datetime 4 | 5 | from disnake import Embed, User, NotFound, Forbidden, DMChannel, MessageReference 6 | from disnake.ext import commands 7 | 8 | from Bot import TheRealGearBot 9 | from Cogs.BaseCog import BaseCog 10 | from Util import Utils, GearbotLogging, Emoji, Translator, MessageUtils, server_info 11 | from Util.Converters import Duration, ReminderText 12 | from Util.Utils import assemble_jumplink 13 | from database.DatabaseConnector import Reminder 14 | from views.Reminder import ReminderView 15 | 16 | 17 | class Reminders(BaseCog): 18 | 19 | def __init__(self, bot) -> None: 20 | super().__init__(bot) 21 | 22 | self.running = True 23 | self.handling = set() 24 | self.bot.loop.create_task(self.delivery_service()) 25 | 26 | def cog_unload(self): 27 | self.running = False 28 | 29 | @commands.group(aliases=["r", "reminder"]) 30 | async def remind(self, ctx): 31 | """remind_help""" 32 | if ctx.invoked_subcommand is None: 33 | await ctx.invoke(self.bot.get_command("help"), query="remind") 34 | 35 | @remind.command("me", aliases=["add", "m", "a"]) 36 | async def remind_me(self, ctx, duration: Duration, *, reminder: ReminderText): 37 | """remind_me_help""" 38 | if duration.unit is None: 39 | parts = reminder.split(" ") 40 | duration.unit = parts[0] 41 | reminder = " ".join(parts[1:]) 42 | duration_seconds = duration.to_seconds(ctx) 43 | if duration_seconds <= 0: 44 | await MessageUtils.send_to(ctx, "NO", "reminder_time_travel") 45 | return 46 | if ctx.guild is not None: 47 | message = f'{Emoji.get_chat_emoji("QUESTION")} {Translator.translate("remind_question", ctx)}' 48 | 49 | async def timeout(): 50 | if m is not None: 51 | await m.edit(content=MessageUtils.assemble(ctx, 'NO', 'command_canceled'), view=None) 52 | 53 | m = await ctx.send(message, view=ReminderView(guild_id=ctx.guild.id if ctx.guild is not None else "@me", reminder=reminder, channel_id=ctx.channel.id, user_id=ctx.author.id, message_id=ctx.message.id, duration=duration_seconds, timeout_callback=timeout)) 54 | 55 | 56 | async def delivery_service(self): 57 | # only let cluster 0 do this one 58 | if self.bot.cluster != 0: 59 | return 60 | GearbotLogging.info("📬 Starting reminder delivery background task 📬") 61 | while self.running: 62 | now = time.time() 63 | limit = datetime.datetime.fromtimestamp(time.time() + 30).timestamp() 64 | 65 | for r in await Reminder.filter(time__lt = limit, status = 1): 66 | if r.id not in self.handling: 67 | self.handling.add(r.id) 68 | self.bot.loop.create_task( 69 | self.run_after(r.time - now, self.deliver(r))) 70 | await asyncio.sleep(25) 71 | GearbotLogging.info("📪 Reminder delivery background task terminated 📪") 72 | 73 | async def run_after(self, delay, action): 74 | if delay > 0: 75 | await asyncio.sleep(delay) 76 | if self.running: # cog got terminated, new cog is now in charge of making sure this gets handled 77 | await action 78 | 79 | async def deliver(self, r): 80 | channel = None 81 | try: 82 | channel = await self.bot.fetch_channel(r.channel_id) 83 | except (Forbidden, NotFound): 84 | pass 85 | dm = await self.bot.fetch_user(r.user_id) 86 | first = dm if r.dm else channel 87 | alternative = channel if r.dm else dm 88 | 89 | if not await self.attempt_delivery(first, r): 90 | await self.attempt_delivery(alternative, r) 91 | await r.delete() 92 | 93 | async def attempt_delivery(self, location, package): 94 | try: 95 | if location is None: 96 | return False 97 | 98 | 99 | 100 | tloc = None if isinstance(location, User) or isinstance(location, DMChannel) else location 101 | now = datetime.datetime.fromtimestamp(time.time()) 102 | send_time = datetime.datetime.fromtimestamp(package.send) 103 | desc = Translator.translate('reminder_delivery', tloc, date=send_time.strftime('%c'), timediff=server_info.time_difference(now, send_time, tloc)) + f"```\n{package.to_remind}\n```" 104 | desc = Utils.trim_message(desc, 2048) 105 | embed = Embed( 106 | color=16698189, 107 | title=Translator.translate('reminder_delivery_title', tloc), 108 | description=desc 109 | ) 110 | if location.id == package.channel_id or package.guild_id == '@me': 111 | ref = MessageReference(guild_id=package.guild_id if package.guild_id != '@me' else None, channel_id=package.channel_id, message_id=package.message_id, fail_if_not_exists=False) 112 | else: 113 | ref = None 114 | embed.add_field(name=Translator.translate('jump_link', tloc), value=f'[Click me!]({assemble_jumplink(package.guild_id, package.channel_id, package.message_id)})') 115 | 116 | try: 117 | await location.send(embed=embed, reference=ref) 118 | except (Forbidden, NotFound): 119 | return False 120 | else: 121 | return True 122 | except Exception as ex: 123 | await TheRealGearBot.handle_exception("Reminder delivery", self.bot, ex, None, None, None, location, package) 124 | return False 125 | 126 | 127 | def setup(bot): 128 | bot.add_cog(Reminders(bot)) 129 | -------------------------------------------------------------------------------- /GearBot/Util/Actions.py: -------------------------------------------------------------------------------- 1 | from disnake import Member 2 | 3 | from Util import Translator, MessageUtils, Utils, Emoji 4 | 5 | 6 | class ActionFailed(Exception): 7 | 8 | def __init__(self, message) -> None: 9 | super().__init__() 10 | self.message = message 11 | 12 | 13 | async def act(ctx, name, target, handler, allow_bots=True, require_on_server=True, send_message=True, check_bot_ability=True, **kwargs): 14 | user = await Utils.get_member(ctx.bot, ctx.guild, target) 15 | if user is None: 16 | if require_on_server: 17 | message = Translator.translate('user_not_on_server', ctx.guild.id) 18 | if send_message: 19 | await ctx.send(f"{Emoji.get_chat_emoji('NO')} {message}") 20 | return False, message 21 | else: 22 | user = ctx.bot.get_user(target) 23 | if user is None and not require_on_server: 24 | user = await Utils.get_user(target) 25 | if user is None: 26 | return False, "Unknown user" 27 | allowed, message = can_act(name, ctx, user, require_on_server=require_on_server, action_bot=allow_bots, check_bot_ability=check_bot_ability) 28 | if allowed: 29 | try: 30 | await handler(ctx, user, **kwargs) 31 | return True, None 32 | except ActionFailed as ex: 33 | return False, ex.message 34 | 35 | else: 36 | if send_message: 37 | await ctx.send(f"{Emoji.get_chat_emoji('NO')} {message}") 38 | return False, message 39 | 40 | 41 | async def mass_action(ctx, name, targets, handler, allow_duplicates=False, allow_bots=True, max_targets=None, require_on_server=True, **kwargs): 42 | if max_targets is not None and len(targets) > max_targets: 43 | await MessageUtils.send_to(ctx, "NO", "mass_action_too_many_people", max=max_targets) 44 | return 45 | failed = [] 46 | handled = set() 47 | if kwargs["dm_action"] and len(targets) > 5: 48 | await MessageUtils.send_to(ctx, "NO", "mass_action_too_many_people_dm", max=5) 49 | kwargs["dm_action"]=False 50 | for target in targets: 51 | if not allow_duplicates and target in handled: 52 | failed.append(f"{target}: {Translator.translate('mass_action_duplicate', ctx)}") 53 | else: 54 | done, error = await act(ctx, name, target, handler, allow_bots, require_on_server=require_on_server, send_message=False, **kwargs) 55 | if not done: 56 | failed.append(f"{target}: {error}") 57 | else: 58 | handled.add(target) 59 | 60 | return failed 61 | 62 | 63 | def can_act(action, ctx, user, require_on_server=True, action_bot=True, check_bot_ability=True): 64 | is_member = isinstance(user, Member) 65 | if not require_on_server and not is_member: 66 | return True, None 67 | if (not is_member) and require_on_server: 68 | return False, Translator.translate("user_not_on_server", ctx.guild.id) 69 | 70 | if check_bot_ability and user.top_role >= ctx.guild.me.top_role: 71 | return False, Translator.translate(f'{action}_unable', ctx.guild.id, user=Utils.clean_user(user)) 72 | 73 | if ((ctx.author != user and ctx.author.top_role > user.top_role) or ( 74 | ctx.guild.owner == ctx.author)) and user != ctx.guild.owner and user != ctx.bot.user and ctx.author != user: 75 | return True, None 76 | if user.bot and not action_bot: 77 | return False, Translator.translate(f"cant_{action}_bot", ctx.guild.id, user=user) 78 | 79 | return False, Translator.translate(f'{action}_not_allowed', ctx.guild.id, user=user) 80 | -------------------------------------------------------------------------------- /GearBot/Util/Archive.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | import io 4 | import os 5 | 6 | import disnake 7 | import pytz 8 | 9 | from Util import Utils, GearbotLogging, Translator, Emoji, Configuration 10 | 11 | archive_counter = 0 12 | 13 | async def archive_purge(bot, guild_id, messages): 14 | global archive_counter 15 | archive_counter += 1 16 | channel = bot.get_channel(list(messages.values())[0].channel) 17 | timestamp = datetime.datetime.strftime(datetime.datetime.now().astimezone( 18 | pytz.timezone(await Configuration.get_var(guild_id, 'GENERAL', 'TIMEZONE'))), '%H:%M:%S') 19 | out = f"purged at {timestamp} from {channel.name}\n" 20 | out += await pack_messages(messages.values(), guild_id) 21 | buffer = io.BytesIO() 22 | buffer.write(out.encode()) 23 | GearbotLogging.log_key(guild_id, 'purged_log', count=len(messages), channel=channel.mention, file=(buffer, "Purged messages archive.txt")) 24 | 25 | async def pack_messages(messages, guild_id): 26 | out = "" 27 | for message in messages: 28 | name = await Utils.username(message.author, clean=False) 29 | reply = "" 30 | if message.reply_to is not None: 31 | reply = f" | In reply to https://discord.com/channels/{message.server}/{message.channel}/{message.reply_to}" 32 | timestamp = datetime.datetime.strftime(disnake.Object(message.messageid).created_at.astimezone(pytz.timezone(await Configuration.get_var(guild_id, 'GENERAL', 'TIMEZONE'))),'%H:%M:%S') 33 | out += f"{timestamp} {message.server} - {message.channel} - {message.messageid} | {name} ({message.author}) | {message.content}{reply} | {(', '.join(Utils.assemble_attachment(message.channel, attachment.id, attachment.name) for attachment in message.attachments))}\r\n" 34 | return out 35 | 36 | async def ship_messages(ctx, messages, t, filename="Message archive", filtered=False): 37 | addendum = "" 38 | if filtered: 39 | addendum = f"\n{Emoji.get_chat_emoji('WARNING')} {Translator.translate('archive_message_filtered', ctx)}" 40 | if len(messages) > 0: 41 | global archive_counter 42 | archive_counter += 1 43 | message_list = dict() 44 | for message in messages: 45 | message_list[message.messageid] = message 46 | messages = [] 47 | for mid, message in sorted(message_list.items()): 48 | messages.append(message) 49 | out = await pack_messages(messages, ctx.guild.id) 50 | buffer = io.BytesIO() 51 | buffer.write(out.encode()) 52 | buffer.seek(0) 53 | 54 | await ctx.send(f"{Emoji.get_chat_emoji('YES')} {Translator.translate('archived_count', ctx, count=len(messages))} {addendum}", file=disnake.File(fp=buffer, filename=f"{filename}.txt")) 55 | else: 56 | await ctx.send(f"{Emoji.get_chat_emoji('WARNING')} {Translator.translate(f'archive_empty_{t}', ctx)} {addendum}") -------------------------------------------------------------------------------- /GearBot/Util/DashUtils.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from Util import Permissioncheckers, Configuration, server_info 4 | 5 | 6 | class DASH_PERMS: 7 | ACCESS = (1 << 0) 8 | VIEW_INFRACTIONS = (1 << 1) 9 | VIEW_CONFIG = (1 << 2) 10 | ALTER_CONFIG = (1 << 3) 11 | 12 | 13 | def get_user_guilds(bot, user_id): 14 | info = dict() 15 | for guild in bot.guilds: 16 | guid = guild.id 17 | permission = get_guild_perms(guild.get_member(user_id)) 18 | if permission > 0: 19 | info[str(guid)] = { 20 | "id": str(guid), 21 | "name": guild.name, 22 | "permissions": permission, 23 | "icon": guild.icon 24 | } 25 | 26 | return OrderedDict(sorted(info.items())) 27 | 28 | 29 | def get_guild_perms(member): 30 | if member is None: 31 | return 0 32 | 33 | mappings = { 34 | "ACCESS": DASH_PERMS.ACCESS, 35 | "INFRACTION": DASH_PERMS.VIEW_INFRACTIONS, 36 | "VIEW_CONFIG": DASH_PERMS.VIEW_CONFIG, 37 | "ALTER_CONFIG": DASH_PERMS.ALTER_CONFIG 38 | } 39 | 40 | permission = 0 41 | user_lvl = Permissioncheckers.user_lvl(member) 42 | for k, v in mappings.items(): 43 | if user_lvl >= Configuration.legacy_get_var(member.guild.id, "DASH_SECURITY", k): 44 | permission |= v 45 | 46 | return permission 47 | 48 | 49 | async def assemble_guild_info(bot, member): 50 | return { 51 | "guild_info": await server_info.server_info_raw(bot, member.guild), 52 | "user_perms": { 53 | "user_dash_perms": get_guild_perms(member), 54 | "user_level": Permissioncheckers.user_lvl(member) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /GearBot/Util/DocUtils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from Util import Configuration, Pages, GearbotLogging, Permissioncheckers, Translator, Utils 5 | 6 | image_pattern = re.compile("(?:!\[)([A-z ]+)(?:\]\()(?:\.*/*)(.*)(?:\))(.*)") 7 | 8 | async def send_buffer(channel, buffer): 9 | pages = Pages.paginate(buffer, max_lines=500) 10 | for page in pages: 11 | await channel.send(page) 12 | 13 | async def generate_command_list(bot, message): 14 | ctx = await bot.get_context(message) 15 | ctx.prefix = "!" 16 | bot.help_command.context = ctx 17 | for code in Translator.LANGS.keys(): 18 | page = "" 19 | handled = set() 20 | for cog in sorted(bot.cogs): 21 | cogo = bot.get_cog(cog) 22 | if cogo.permissions is not None: 23 | perm_lvl = cogo.permissions["required"] 24 | default = Translator.translate_by_code("help_page_default_perm", code) 25 | plvl = Translator.translate_by_code(f'perm_lvl_{perm_lvl}', code) 26 | c = Translator.translate_by_code("command", code) 27 | default_lvl = Translator.translate_by_code("help_page_default_lvl", code) 28 | explanation = Translator.translate_by_code("explanation", code) 29 | page += f"# {cog}\n{default}: {plvl} ({perm_lvl})\n\n| {c} | {default_lvl} | {explanation} |\n| ----------------|--------|-------------------------------------------------------|\n" 30 | for command in sorted([c for c in cogo.walk_commands()], key= lambda c:c.qualified_name): 31 | if command.qualified_name not in handled: 32 | page += gen_command_listing(bot, cogo, command, code) 33 | handled.add(command.qualified_name) 34 | page += "\n\n" 35 | folder = Configuration.get_master_var("WEBSITE_ROOT", "") + f"/pages/03.docs/01.commands" 36 | if not os.path.exists(folder): 37 | os.makedirs(folder) 38 | with open(f"{folder}/doc.{code}.md", "w", encoding="utf-8") as file: 39 | file.write(page) 40 | 41 | def gen_command_listing(bot, cog, command, code): 42 | try: 43 | perm_lvl = Permissioncheckers.get_perm_dict(command.qualified_name.split(' '), cog.permissions)['required'] 44 | listing = f"| | | {Translator.translate_by_code(command.short_doc, code)} |\n" 45 | listing += f"|{command.qualified_name}|{Translator.translate_by_code(f'perm_lvl_{perm_lvl}', code)} ({perm_lvl})| |\n" 46 | signature = bot.help_command.get_command_signature(command).replace("|", "ǀ") 47 | listing += f"| | |{Translator.translate_by_code('example', code)}: ``{signature}``|\n" 48 | except Exception as ex: 49 | GearbotLogging.error(command.qualified_name) 50 | raise ex 51 | return listing 52 | 53 | 54 | 55 | async def generate_command_list2(bot, message): 56 | ctx = await bot.get_context(message) 57 | ctx.prefix = "!" 58 | bot.help_command.context = ctx 59 | # for code in Translator.LANGS.keys(): 60 | out = dict() 61 | handled = set() 62 | for cog in sorted(bot.cogs): 63 | cog_commands = dict() 64 | cogo = bot.get_cog(cog) 65 | if cogo.permissions is not None: 66 | for command in sorted([c for c in cogo.walk_commands()], key= lambda c:c.qualified_name): 67 | if command.qualified_name not in handled: 68 | location = cog_commands 69 | for c in command.full_parent_name.split(' '): 70 | if c == '': 71 | break 72 | location = location[c]["subcommands"] 73 | location[command.name] = gen_command_listing2(bot, cogo, command) 74 | handled.add(command.qualified_name) 75 | if len(cog_commands) > 0: 76 | out[cog] = cog_commands 77 | Utils.save_to_disk("temp", out) 78 | 79 | def gen_command_listing2(bot, cog, command): 80 | command_listing = dict() 81 | try: 82 | perm_lvl = Permissioncheckers.get_perm_dict(command.qualified_name.split(' '), cog.permissions)['required'] 83 | command_listing["commandlevel"] = perm_lvl 84 | command_listing["description"] = command.short_doc 85 | command_listing["aliases"] = command.aliases 86 | example = bot.help_command.get_command_signature(command).strip() 87 | parts = str(example).split(' ') 88 | parts[0] = ''.join(parts[0][1:]) 89 | for i in range(0, len(parts)): 90 | if "[" == parts[i][0] and "|" in parts[i]: 91 | parts[i] = ''.join(parts[i].split('|')[0][1:]) 92 | command_listing["example"] = '!' + ' '.join(parts) 93 | command_listing["subcommands"] = {} 94 | return command_listing 95 | except Exception as ex: 96 | GearbotLogging.error(command.qualified_name) 97 | raise ex 98 | -------------------------------------------------------------------------------- /GearBot/Util/Emoji.py: -------------------------------------------------------------------------------- 1 | from disnake import utils 2 | 3 | from Util import Configuration, GearbotLogging 4 | 5 | emojis = dict() 6 | 7 | BACKUPS = { 8 | "1": "1⃣", 9 | "2": "2⃣", 10 | "3": "3⃣", 11 | "4": "4⃣", 12 | "5": "5⃣", 13 | "6": "6⃣", 14 | "7": "7⃣", 15 | "8": "8⃣", 16 | "9": "9⃣", 17 | "10": "🔟", 18 | "AE": "🐉", 19 | "ALTER": "🛠", 20 | "BAD_USER": "😶", 21 | "BAN": "🚪", 22 | "BEAN": "🌱", 23 | "BOOT": "👢", 24 | "BUG": "🐛", 25 | "CATEGORY": "📚", 26 | "CHANNEL": "📝", 27 | "CLOCK": "⏰", 28 | "CREATE": "🔨", 29 | "DELETE": "⛏", 30 | "DIAMOND": "⚙", 31 | "DND": "❤", 32 | "EDIT": "📝", 33 | "EYES": "👀", 34 | "GAMING": "🎮", 35 | "GOLD": "⚙", 36 | "IDLE": "💛", 37 | "INNOCENT": "😇", 38 | "IRON": "⚙", 39 | "JOIN": "📥", 40 | "LEAVE": "📤", 41 | "LEFT": "⬅️", 42 | "LOADING": "⏳", 43 | "LOCK": "🔒", 44 | "MUSIC": "🎵", 45 | "MUTE": "😶", 46 | "NAMETAG": "📛", 47 | "NICKTAG": "📛", 48 | "NO": "🚫", 49 | "OFFLINE": "💙", 50 | "ONLINE": "💚", 51 | "PIN": "📌", 52 | "PING": "🏓", 53 | "QUESTION": "❓", 54 | "REFRESH": "🔁", 55 | "RIGHT": "➡️", 56 | "ROLE_ADD": "🛫", 57 | "ROLE_REMOVE": "🛬", 58 | "SEARCH": "🔎", 59 | "SINISTER": "😈", 60 | "SPY": "🕵", 61 | "STONE": "⚙", 62 | "STREAMING": "💜", 63 | "TACO": "🌮", 64 | "THINK": "🤔", 65 | "TODO": "📋", 66 | "TRASH": "🗑", 67 | "VOICE": "🔊", 68 | "WARNING": "⚠", 69 | "WATCHING": "📺", 70 | "WHAT": "☹", 71 | "WINK": "😉", 72 | "WOOD": "⚙", 73 | "WRENCH": "🔧", 74 | "YES": "✅" 75 | } 76 | 77 | 78 | async def initialize(bot): 79 | emoji_guild = await bot.fetch_guild(Configuration.get_master_var("EMOJI_GUILD")) 80 | failed = [] 81 | for name, eid in Configuration.get_master_var("EMOJI", {}).items(): 82 | e = utils.get(emoji_guild.emojis, id=eid) 83 | if e is not None: 84 | emojis[name] = e 85 | else: 86 | failed.append(name) 87 | 88 | if len(failed) > 0: 89 | await GearbotLogging.bot_log("Failed to load the following emoji: " + ",".join(failed)) 90 | 91 | 92 | def get_chat_emoji(name): 93 | return str(get_emoji(name)) 94 | 95 | 96 | def get_emoji(name): 97 | if name in emojis: 98 | return emojis[name] 99 | else: 100 | return BACKUPS[name] 101 | -------------------------------------------------------------------------------- /GearBot/Util/Enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | 3 | 4 | class ReminderStatus(IntEnum): 5 | Pending = 1 6 | Delivered = 2 7 | Failed = 3 -------------------------------------------------------------------------------- /GearBot/Util/Features.py: -------------------------------------------------------------------------------- 1 | from Util import Configuration 2 | 3 | LOG_MAP = dict() 4 | 5 | 6 | async def check_server(guild_id): 7 | enabled = set() 8 | for cid, info in (await Configuration.get_var(guild_id, "LOG_CHANNELS")).items(): 9 | enabled.update(info["CATEGORIES"]) 10 | LOG_MAP[guild_id] = enabled 11 | 12 | 13 | def is_logged(guild, feature): 14 | return guild in LOG_MAP and feature in LOG_MAP[guild] 15 | 16 | 17 | requires_logging = { 18 | "CENSOR_MESSAGES": "CENSORING", 19 | "EDIT_LOGS": "MESSAGE_LOGS" 20 | } 21 | 22 | 23 | def can_enable(guild, feature): 24 | return feature not in requires_logging or is_logged(guild, requires_logging[feature]) -------------------------------------------------------------------------------- /GearBot/Util/HelpGenerator.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from disnake.ext.commands import GroupMixin 4 | 5 | from Util import Utils, Pages, Translator, Permissioncheckers, Configuration 6 | 7 | 8 | async def command_list(bot, member, guild): 9 | command_tree = dict() 10 | longest = 0 11 | for cog in bot.cogs: 12 | commands, l = await cog_commands(bot, cog, member, guild) 13 | if commands is not None: 14 | command_tree[cog] = commands 15 | if l > longest: 16 | longest = l 17 | command_tree = collections.OrderedDict(sorted(command_tree.items())) 18 | 19 | 20 | output_tree = collections.OrderedDict() 21 | for cog, commands in command_tree.items(): 22 | output = f'- {cog}\n' 23 | for command_name, info in commands.items(): 24 | output += " " + command_name + (" " * (longest - len(command_name) + 2)) + info + "\n" 25 | output_tree[cog] = output 26 | # sometimes we get a null prefix for some reason? 27 | prefix = await Configuration.get_var(guild.id, "GENERAL", "PREFIX") if guild is not None else '!' 28 | return dict_to_pages(output_tree, f"You can get more info about a command (params and subcommands) by using '{prefix}help '\nCommands followed by ↪ have subcommands") 29 | 30 | 31 | async def cog_commands(bot, cog, member, guild): 32 | commands = bot.get_cog(cog).get_commands() 33 | if len(commands) == 0: 34 | return None, None 35 | return await gen_commands_list(bot, member, guild, commands) 36 | 37 | async def gen_commands_list(bot, member, guild, list): 38 | longest = 0 39 | command_list = dict() 40 | for command in list: 41 | runnable = member is not None and await Permissioncheckers.check_permission(command, guild, member, bot) 42 | if not command.hidden and runnable: 43 | indicator = "\n ↪" if isinstance(command, GroupMixin) else "" 44 | command_list[command.name] = Utils.trim_message(Translator.translate(command.short_doc, guild), 120) + indicator 45 | if len(command.name) > longest: 46 | longest = len(command.name) 47 | if len(command_list) > 0: 48 | return collections.OrderedDict(sorted(command_list.items())), longest 49 | else: 50 | return None, None 51 | 52 | 53 | async def gen_cog_help(bot, cog, member, guild): 54 | commands, longest = await cog_commands(bot, cog, member, guild) 55 | output = f'- {cog}\n' 56 | if commands is not None: 57 | for command_name, info in commands.items(): 58 | output += command_name + (" " * (longest - len(command_name) + 4)) + info + "\n" 59 | return Pages.paginate(output) 60 | else: 61 | return None 62 | 63 | async def gen_command_help(bot, member, guild, command): 64 | signature = "" 65 | parent = command.parent 66 | while parent is not None: 67 | if not parent.signature or parent.invoke_without_command: 68 | signature = f"{parent.name} {signature}" 69 | else: 70 | signature = f"{parent.name} {parent.signature} {signature}" 71 | parent = parent.parent 72 | 73 | if len(command.aliases) > 0: 74 | aliases = '|'.join(command.aliases) 75 | signature = f"{signature} [{command.name}|{aliases}]" 76 | else: 77 | signature = f"{signature} {command.name}" 78 | prefix = await Configuration.get_var(guild.id, "GENERAL", "PREFIX") if guild is not None else "!" 79 | usage = f"{prefix}{signature}" 80 | sub_info = None 81 | if isinstance(command, GroupMixin) and hasattr(command, "all_commands"): 82 | subcommands, longest = await gen_commands_list(bot, member, guild, command.all_commands.values()) 83 | if subcommands is not None: 84 | sub_info = "\nSub commands:\n" 85 | for command_name, info in subcommands.items(): 86 | sub_info += " " + command_name + (" " * (longest - len(command_name) + 4)) + info + "\n" 87 | sub_info += Translator.translate('help_footer', guild, prefix=prefix, signature=signature) 88 | 89 | return Pages.paginate(f"{usage}\n\n{Translator.translate(command.help, guild)}\n{'' if sub_info is None else sub_info}".replace(bot.user.mention, f"@{bot.user.name}")) 90 | 91 | def dict_to_pages(dict, suffix=""): 92 | pages = [] 93 | output = "" 94 | for out in dict.values(): 95 | if len(output) + len(out) > 1000: 96 | pages.append(f"{output}\n{suffix}") 97 | output = out 98 | else: 99 | if output == "": 100 | output = Utils.trim_message(out, 2000 - 15 - len(suffix)) 101 | else: 102 | output += out + "\n" 103 | pages.append(f"{output}\n{suffix}") 104 | # if some page does end up over 2k, split it 105 | real_pages = [] 106 | for p in pages: 107 | for page in Pages.paginate(p, max_lines=100): 108 | real_pages.append(page) 109 | return real_pages -------------------------------------------------------------------------------- /GearBot/Util/Matchers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | ID_MATCHER = re.compile("<@!?([0-9]{15,20})>") 4 | ROLE_ID_MATCHER = re.compile("<@&([0-9]{15,20})>") 5 | CHANNEL_ID_MATCHER = re.compile("<#([0-9]{15,20})>") 6 | MENTION_MATCHER = re.compile("<@[!&]?\\d+>") 7 | URL_MATCHER = re.compile(r'((?:https?://)[a-z0-9]+(?:[-._][a-z0-9]+)*\.[a-z]{2,5}(?::[0-9]{1,5})?(?:/[^ \n<>]*)?)', re.IGNORECASE) 8 | EMOJI_MATCHER = re.compile('<(a?):([^: \n]+):([0-9]{15,20})>') 9 | JUMP_LINK_MATCHER = re.compile(r"https://(?:canary|ptb)?\.?discord(?:app)?.com/channels/\d{15,20}/(\d{15,20})/(\d{15,20})") 10 | MODIFIER_MATCHER = re.compile(r"^\[(.*):(.*)\]$") 11 | NUMBER_MATCHER = re.compile(r"\d+") 12 | ID_NUMBER_MATCHER = re.compile(r"\d{15,19}") 13 | START_WITH_NUMBER_MATCHER = re.compile(r"^(\d+)") 14 | INVITE_MATCHER = re.compile(r"(?:https?://)?(?:www\.)?(?:discord(?:\.| |\[?\(?\"?'?dot'?\"?\)?\]?)?(?:gg|io|me|li)|discord(?:app)?\.com/invite)/+((?:(?!https?)[\w\d-])+)", flags=re.IGNORECASE) -------------------------------------------------------------------------------- /GearBot/Util/MessageUtils.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import time 3 | from collections import namedtuple 4 | import datetime 5 | 6 | from disnake import Object, HTTPException, MessageType, AllowedMentions 7 | from disnake.utils import time_snowflake 8 | 9 | from Util import Translator, Emoji, Archive, GearbotLogging 10 | from database import DBUtils 11 | from database.DBUtils import fakeLoggedMessage 12 | from database.DatabaseConnector import LoggedMessage 13 | 14 | Message = namedtuple("Message", "messageid author content channel server attachments type pinned reply_to") 15 | 16 | 17 | 18 | attachment = namedtuple("attachment", "id name") 19 | 20 | async def get_message_data(bot, message_id): 21 | message = None 22 | if not Object(message_id).created_at <= datetime.datetime.utcfromtimestamp(time.time() - 1 * 60).replace(tzinfo=datetime.timezone.utc): 23 | parts = await bot.redis_pool.hgetall(f"messages:{message_id}") 24 | if len(parts) == 7: 25 | reply = int(parts["reply"]) 26 | message = Message(message_id, int(parts["author"]), parts["content"], int(parts["channel"]), int(parts["server"]), [attachment(a.split("/")[0], a.split("/")[1]) for a in parts["attachments"].split("|")] if len(parts["attachments"]) > 0 else [], type=int(parts["type"]) if "type" in parts else None, pinned=parts["pinned"] == '1', reply_to=reply if reply != 0 else None) 27 | if message is None: 28 | message = await LoggedMessage.get_or_none(messageid = message_id).prefetch_related("attachments") 29 | return message 30 | 31 | async def insert_message(bot, message, redis=True): 32 | message_type = message.type 33 | if message_type == MessageType.default: 34 | message_type = None 35 | else: 36 | if not isinstance(message_type, int): 37 | message_type = message_type.value 38 | if redis: 39 | pipe = bot.redis_pool.pipeline() 40 | is_reply = message.reference is not None and message.reference.channel_id == message.channel.id 41 | pipe.hmset_dict(f"messages:{message.id}", author=message.author.id, content=message.content, 42 | channel=message.channel.id, server=message.guild.id, pinned=1 if message.pinned else 0, attachments='|'.join((f"{str(a.id)}/{str(a.filename)}" for a in message.attachments)), reply=message.reference.message_id if is_reply else 0) 43 | if message_type is not None: 44 | pipe.hmset_dict(f"messages:{message.id}", type=message_type) 45 | pipe.expire(f"messages:{message.id}", 1*60) 46 | await pipe.execute() 47 | await DBUtils.insert_message(message) 48 | 49 | async def update_message(bot, message_id, content, pinned): 50 | if not Object(message_id).created_at <= datetime.datetime.utcfromtimestamp(time.time() - 1 * 60).replace(tzinfo=datetime.timezone.utc): 51 | pipe = bot.redis_pool.pipeline() 52 | pipe.hmset_dict(f"messages:{message_id}", content=content) 53 | pipe.hmset_dict(f"messages:{message_id}", pinned=(1 if pinned else 0)) 54 | await pipe.execute() 55 | if message_id in DBUtils.batch: 56 | old = DBUtils.batch[message_id] 57 | DBUtils.batch[message_id] = fakeLoggedMessage(message_id, content, old.author, old.channel, old.server, old.type, pinned, old.attachments) 58 | elif message_id > time_snowflake(datetime.datetime.utcfromtimestamp(time.time() - 60*60*24*7*6).replace(tzinfo=datetime.timezone.utc)): 59 | await LoggedMessage.filter(messageid=message_id).update(content=content, pinned=pinned) 60 | 61 | def assemble(destination, emoji, m, translate=True, **kwargs): 62 | translated = Translator.translate(m, destination, **kwargs) if translate else m 63 | return f"{Emoji.get_chat_emoji(emoji)} {translated}" 64 | 65 | async def archive_purge(bot, id_list, guild_id): 66 | message_list = dict() 67 | for mid in id_list: 68 | message = await get_message_data(bot, mid) 69 | if message is not None: 70 | message_list[mid] = message 71 | if len(message_list) > 0: 72 | await Archive.archive_purge(bot, guild_id, 73 | collections.OrderedDict(sorted(message_list.items()))) 74 | 75 | 76 | async def send_to(destination, emoji, message, translate=True, embed=None, attachment=None, **kwargs): 77 | translated = Translator.translate(message, destination.guild, **kwargs) if translate else message 78 | return await destination.send(f"{Emoji.get_chat_emoji(emoji)} {translated}", embed=embed, allowed_mentions=AllowedMentions(everyone=False, users=True, roles=False), file=attachment) 79 | 80 | async def try_edit(message, emoji: str, string_name: str, embed=None, **kwargs): 81 | translated = Translator.translate(string_name, message.channel, **kwargs) 82 | try: 83 | return await message.edit(content=f'{Emoji.get_chat_emoji(emoji)} {translated}', embed=embed) 84 | except HTTPException: 85 | return await send_to(message.channel, emoji, string_name, embed=embed, **kwargs) 86 | 87 | 88 | def day_difference(a, b, location): 89 | diff = a - b 90 | return Translator.translate('days_ago', location, days=diff.days, date=a) 91 | 92 | def construct_jumplink(guild_id, channel_id, message_id): 93 | return f"https://discord.com/channels/{guild_id}/{channel_id}/{message_id}" 94 | -------------------------------------------------------------------------------- /GearBot/Util/Pages.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake import NotFound, Forbidden 3 | 4 | from Util import Emoji, ReactionManager, MessageUtils 5 | 6 | page_handlers = dict() 7 | 8 | 9 | def register(type, init, update): 10 | page_handlers[type] = { 11 | "init": init, 12 | "update": update, 13 | } 14 | 15 | 16 | def unregister(type_handler): 17 | if type_handler in page_handlers.keys(): 18 | del page_handlers[type_handler] 19 | 20 | 21 | async def create_new(bot, type, ctx, **kwargs): 22 | text, embed, has_pages = await page_handlers[type]["init"](ctx, **kwargs) 23 | message: disnake.Message = await ctx.channel.send(text, embed=embed) 24 | if has_pages: 25 | await ReactionManager.register(bot, message.id, message.channel.id, "paged", subtype=type, **kwargs) 26 | try: 27 | if has_pages: await message.add_reaction(Emoji.get_emoji('LEFT')) 28 | if has_pages: await message.add_reaction(Emoji.get_emoji('RIGHT')) 29 | except disnake.Forbidden: 30 | await MessageUtils.send_to(ctx, 'WARNING', 'paginator_missing_perms', prev=Emoji.get_chat_emoji('LEFT'), 31 | next=Emoji.get_chat_emoji('RIGHT')) 32 | except disnake.NotFound: 33 | await MessageUtils.send_to(ctx, 'WARNING', 'fix_censor') 34 | 35 | 36 | async def update(bot, message, action, user, **kwargs): 37 | subtype = kwargs.get("subtype", "") 38 | if subtype in page_handlers.keys(): 39 | if "sender" not in kwargs or int(user) == int(kwargs["sender"]): 40 | page_num = kwargs.get("page", 0) 41 | ctx = None 42 | if "trigger" in kwargs: 43 | try: 44 | trigger_message = await message.channel.fetch_message(kwargs["trigger"]) 45 | ctx = await bot.get_context(trigger_message) 46 | except (NotFound, Forbidden): 47 | pass 48 | text, embed, info = await page_handlers[subtype]["update"](ctx, message, int(page_num), action, kwargs) 49 | try: 50 | await message.edit(content=text, embed=embed) 51 | except (NotFound, Forbidden): 52 | pass # weird shit but happens sometimes 53 | return info 54 | return 55 | 56 | 57 | def basic_pages(pages, page_num, action): 58 | if action == "PREV": 59 | page_num -= 1 60 | elif action == "NEXT": 61 | page_num += 1 62 | if page_num < 0: 63 | page_num = len(pages) - 1 64 | if page_num >= len(pages): 65 | page_num = 0 66 | page = pages[page_num] 67 | return page, page_num 68 | 69 | 70 | def paginate(input, max_lines=20, max_chars=1900, prefix="", suffix=""): 71 | max_chars -= len(prefix) + len(suffix) 72 | lines = str(input).splitlines(keepends=True) 73 | pages = [] 74 | page = "" 75 | count = 0 76 | for line in lines: 77 | if len(page) + len(line) > max_chars or count == max_lines: 78 | if page == "": 79 | # single 2k line, split smaller 80 | words = line.split(" ") 81 | for word in words: 82 | if len(page) + len(word) > max_chars: 83 | pages.append(f"{prefix}{page}{suffix}") 84 | page = f"{word} " 85 | else: 86 | page += f"{word} " 87 | else: 88 | pages.append(f"{prefix}{page}{suffix}") 89 | page = line 90 | count = 1 91 | else: 92 | page += line 93 | count += 1 94 | pages.append(f"{prefix}{page}{suffix}") 95 | return pages 96 | 97 | 98 | def paginate_fields(input): 99 | pages = [] 100 | for page in input: 101 | page_fields = dict() 102 | for name, content in page.items(): 103 | page_fields[name] = paginate(content, max_chars=1024) 104 | pages.append(page_fields) 105 | real_pages = [] 106 | for page in pages: 107 | page_count = 0 108 | page_fields = dict() 109 | for name, parts in page.items(): 110 | base_name = name 111 | if len(parts) == 1: 112 | if page_count + len(name) + len(parts[0]) > 4000: 113 | real_pages.append(page_fields) 114 | page_fields = dict() 115 | page_count = 0 116 | page_fields[name] = parts[0] 117 | page_count += len(name) + len(parts[0]) 118 | else: 119 | for i in range(len(parts)): 120 | part = parts[i] 121 | name = f"{base_name} ({i + 1}/{len(parts)})" 122 | if page_count + len(name) + len(part) > 3000: 123 | real_pages.append(page_fields) 124 | page_fields = dict() 125 | page_count = 0 126 | page_fields[name] = part 127 | page_count += len(name) + len(part) 128 | real_pages.append(page_fields) 129 | return real_pages 130 | -------------------------------------------------------------------------------- /GearBot/Util/Permissioncheckers.py: -------------------------------------------------------------------------------- 1 | from disnake.ext import commands 2 | from disnake.ext.commands import NoPrivateMessage, BotMissingPermissions, CheckFailure 3 | 4 | from Util import Configuration, Utils 5 | 6 | 7 | def is_owner(): 8 | async def predicate(ctx): 9 | return ctx.bot.is_owner(ctx.author) 10 | 11 | return commands.check(predicate) 12 | 13 | 14 | def is_trusted(member): 15 | return is_user("TRUSTED", member) 16 | 17 | 18 | def is_mod(member): 19 | return is_user("MOD", member) or (hasattr(member, "roles") and member.guild_permissions.ban_members) 20 | 21 | 22 | def is_admin(member): 23 | return is_user("ADMIN", member) or (hasattr(member, "roles") and member.guild_permissions.administrator) 24 | 25 | 26 | def is_lvl4(member): 27 | return is_user("LVL4", member) 28 | 29 | 30 | def is_server_owner(ctx): 31 | return ctx.guild is not None and ctx.author == ctx.guild.owner 32 | 33 | 34 | def is_user(perm_type, member): 35 | if not hasattr(member, "guild") or member.guild is None: 36 | return False 37 | if not hasattr(member, "roles"): 38 | return False 39 | 40 | roles = Configuration.legacy_get_var(member.guild.id, "PERMISSIONS", f"{perm_type}_ROLES") 41 | users = Configuration.legacy_get_var(member.guild.id, "PERMISSIONS", f"{perm_type}_USERS") 42 | 43 | if member.id in users: 44 | return True 45 | 46 | for role in member.roles: 47 | if role.id in roles: 48 | return True 49 | return False 50 | 51 | 52 | def mod_only(): 53 | async def predicate(ctx): 54 | return is_mod(ctx.author) or is_admin(ctx.author) 55 | 56 | return commands.check(predicate) 57 | 58 | 59 | def is_server(ctx, id): 60 | return ctx.guild is not None and ctx.guild.id == id 61 | 62 | 63 | def bc_only(): 64 | async def predicate(ctx): 65 | return is_server(ctx, 309218657798455298) 66 | 67 | return commands.check(predicate) 68 | 69 | class NotCachedException(CheckFailure): 70 | pass 71 | 72 | 73 | def require_cache(): 74 | async def predicate(ctx): 75 | if ctx.guild is not None and ctx.guild.id in ctx.bot.missing_guilds: 76 | raise NotCachedException 77 | return True 78 | return commands.check(predicate) 79 | 80 | async def check_permission(command_object, guild, member, bot): 81 | if not hasattr(member, '_roles') and guild is not None: 82 | member = await Utils.get_member(bot, guild, member.id) 83 | if guild is None: 84 | return 0 >= get_required(command_object, command_object.cog.permissions) 85 | else: 86 | overrides = await Configuration.get_var(guild.id, "PERM_OVERRIDES") 87 | cog_name = type(command_object.cog).__name__ 88 | required = -1 89 | if cog_name in overrides: 90 | required = get_required(command_object, overrides[cog_name]) 91 | if required == -1: 92 | required = get_required(command_object, command_object.cog.permissions) 93 | return await get_user_lvl(guild, member, command_object) >= (command_object.cog.permissions["required"] if required == -1 else required) 94 | 95 | 96 | def get_command_pieces(command_object): 97 | return command_object.qualified_name.lower().split(" ") if command_object is not None else [] 98 | 99 | 100 | def get_required(command_object, perm_dict): 101 | if perm_dict is None: 102 | return 6 103 | pieces = get_command_pieces(command_object) 104 | required = perm_dict["required"] 105 | found = True 106 | while len(pieces) > 0 and found: 107 | found = False 108 | if "commands" in perm_dict.keys(): 109 | for entry, value in perm_dict["commands"].items(): 110 | if pieces[0] in entry.split("|"): 111 | r = value["required"] 112 | if r != -1: 113 | required = r 114 | perm_dict = value 115 | pieces.pop(0) 116 | found = True 117 | break 118 | return required 119 | 120 | 121 | def get_perm_dict(pieces, perm_dict, strict=False): 122 | found = True 123 | while len(pieces) > 0 and found: 124 | found = False 125 | if "commands" in perm_dict.keys(): 126 | for entry, value in perm_dict["commands"].items(): 127 | if pieces[0] in entry.split("|"): 128 | perm_dict = value 129 | pieces.pop(0) 130 | found = True 131 | break 132 | if not found and len(pieces) > 0 and strict: 133 | return None 134 | return perm_dict 135 | 136 | 137 | async def get_user_lvl(guild, member, command_object=None): 138 | if guild.owner is not None and guild.owner.id == member.id: 139 | return 5 140 | 141 | if is_lvl4(member): 142 | return 4 143 | 144 | if command_object is not None: 145 | cog_name = type(command_object.cog).__name__ 146 | overrides = await Configuration.get_var(guild.id, "PERM_OVERRIDES") 147 | if cog_name in overrides: 148 | target = overrides[cog_name] 149 | pieces = get_command_pieces(command_object) 150 | while len(pieces) > 0 and "commands" in target and pieces[0] in target["commands"]: 151 | target = target["commands"][pieces.pop(0)] 152 | if member.id in target["people"]: 153 | return 4 154 | if is_admin(member): 155 | return 3 156 | if is_mod(member): 157 | return 2 158 | if is_trusted(member): 159 | return 1 160 | return 0 161 | 162 | 163 | def user_lvl(member): 164 | if member.guild.owner.id == member.id: 165 | return 5 166 | if is_lvl4(member): 167 | return 4 168 | if is_admin(member): 169 | return 3 170 | if is_mod(member): 171 | return 2 172 | if is_trusted(member): 173 | return 1 174 | return 0 175 | 176 | 177 | def bot_has_guild_permission(**kwargs): 178 | async def predicate(ctx): 179 | if ctx.guild is None: 180 | raise NoPrivateMessage() 181 | 182 | permissions = ctx.guild.me.guild_permissions 183 | missing = [perm for perm, value in kwargs.items() if getattr(permissions, perm, None) != value] 184 | 185 | if not missing: 186 | return True 187 | 188 | raise BotMissingPermissions(missing) 189 | 190 | return commands.check(predicate) 191 | 192 | -------------------------------------------------------------------------------- /GearBot/Util/PromMonitors.py: -------------------------------------------------------------------------------- 1 | import prometheus_client as prom 2 | from prometheus_client.metrics import Info 3 | 4 | 5 | class PromMonitors: 6 | def __init__(self, bot, prefix) -> None: 7 | self.command_counter = prom.Counter(f"{prefix}_commands_ran", "How many times commands were ran", [ 8 | "command_name", 9 | "cluster" 10 | ]) 11 | 12 | 13 | self.user_message_raw_count = prom.Counter(f"{prefix}_user_message_raw_count", "Raw count of how many messages we have seen from users", ["cluster"]) 14 | self.bot_message_raw_count = prom.Counter(f"{prefix}_bot_message_raw_count", 15 | "Raw count of how many messages we have seen from bots", ["cluster"]) 16 | self.own_message_raw_count = prom.Counter(f"{prefix}_own_message_raw_count", "Raw count of how many messages GearBot has send", ["cluster"]) 17 | 18 | self.bot_guilds = prom.Gauge(f"{prefix}_guilds", "How many guilds the bot is in", ["cluster"]) 19 | 20 | self.bot_users = prom.Gauge(f"{prefix}_users", "How many users the bot can see", ["cluster"]) 21 | self.bot_users_unique = prom.Gauge(f"{prefix}_users_unique", "How many unique users the bot can see", ["cluster"]) 22 | self.bot_event_counts = prom.Counter(f"{prefix}_event_counts", "How much each event occurred", ["event_name", "cluster"]) 23 | 24 | self.bot_latency = prom.Gauge(f"{prefix}_latency", "Current bot latency", ["cluster"]) 25 | 26 | self.uid_usage = prom.Counter(f"{prefix}_context_uid_usage", "Times uid was used from the context command", ["type", "cluster"]) 27 | self.userinfo_usage = prom.Counter(f"{prefix}_context_userinfo_usage", "Times userinfo was used from the context command", ["type", "cluster"]) 28 | self.inf_search_usage = prom.Counter(f"{prefix}_context_inf_search_usage", "Times inf serach was used from the context command", ["type", "cluster"]) 29 | 30 | bot.metrics_reg.register(self.command_counter) 31 | bot.metrics_reg.register(self.user_message_raw_count) 32 | bot.metrics_reg.register(self.bot_message_raw_count) 33 | bot.metrics_reg.register(self.bot_guilds) 34 | bot.metrics_reg.register(self.bot_users) 35 | bot.metrics_reg.register(self.bot_users_unique) 36 | bot.metrics_reg.register(self.bot_event_counts) 37 | bot.metrics_reg.register(self.own_message_raw_count) 38 | bot.metrics_reg.register(self.bot_latency) 39 | bot.metrics_reg.register(self.uid_usage) 40 | bot.metrics_reg.register(self.userinfo_usage) 41 | bot.metrics_reg.register(self.inf_search_usage) 42 | -------------------------------------------------------------------------------- /GearBot/Util/Questions.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections import namedtuple 3 | 4 | from disnake import Embed, Reaction, NotFound 5 | 6 | from Util import MessageUtils 7 | 8 | Option = namedtuple("Option", "emoji text handler") 9 | 10 | async def ask(ctx, text, options, timeout=60): 11 | embed = Embed(color=0x68a910, description='\n'.join(f"{option.emoji} {option.text}" for option in options)) 12 | message = await ctx.send(text, embed=embed) 13 | handlers = dict() 14 | for option in options: 15 | await message.add_reaction(option.emoji) 16 | handlers[str(option.emoji)] = option.handler 17 | def check(reaction): 18 | return reaction.user_id == ctx.message.author.id and str(reaction.emoji) in handlers.keys() and reaction.message_id == message.id 19 | try: 20 | reaction = await ctx.bot.wait_for('raw_reaction_add', timeout=timeout, check=check) 21 | except asyncio.TimeoutError: 22 | await MessageUtils.send_to(ctx, "NO", "confirmation_timeout", timeout=30) 23 | return 24 | else: 25 | await handlers[str(reaction.emoji)]() 26 | finally: 27 | try: 28 | await message.delete() 29 | except NotFound: 30 | pass -------------------------------------------------------------------------------- /GearBot/Util/RaidHandling/RaidShield.py: -------------------------------------------------------------------------------- 1 | from Util import GearbotLogging 2 | from Util.RaidHandling import RaidActions 3 | 4 | 5 | class RaidShield: 6 | 7 | def __init__(self, shield_info) -> None: 8 | self.shield_name=shield_info["name"] 9 | self.start_actions = [action for action in shield_info["actions"]["triggered"]] 10 | self.raider_actions = [action for action in shield_info["actions"]["raider"]] 11 | self.termination_actions = [action for action in shield_info["actions"]["terminated"]] 12 | 13 | async def raid_detected(self, bot, guild, raid_id, raider_ids, shield): 14 | GearbotLogging.log_key(guild.id, "raid_shield_triggered", raid_id=raid_id, name=self.shield_name) 15 | await self.handle_actions(self.start_actions, bot, guild, raid_id, raider_ids, shield) 16 | 17 | async def handle_raider(self, bot, raider, raid_id, raider_ids, shield): 18 | await self.handle_actions(self.raider_actions, bot, raider, raid_id, raider_ids, shield) 19 | 20 | async def shield_terminated(self, bot, guild, raid_id, raider_ids, shield): 21 | GearbotLogging.log_key(guild.id, "raid_shield_terminated", raid_id=raid_id, name=self.shield_name) 22 | await self.handle_actions(self.termination_actions, bot, guild, raid_id, raider_ids, shield) 23 | 24 | async def handle_actions(self, actions, bot, o, raid_id, raider_ids, shield): 25 | for a in actions: 26 | try: 27 | action = RaidActions.handlers[a["type"]] 28 | except KeyError: 29 | await GearbotLogging.bot_log(f"Corrupt action in shield on guild {o.id}") 30 | else: 31 | await action.execute(bot, o, a["action_data"], raid_id, raider_ids, shield) 32 | -------------------------------------------------------------------------------- /GearBot/Util/RaidHandling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gearbot/GearBot/b91c48eafde0ef4612442b33f10e70d2475c5489/GearBot/Util/RaidHandling/__init__.py -------------------------------------------------------------------------------- /GearBot/Util/ReactionManager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from disnake import Forbidden, NotFound, Embed, Colour, Object 4 | 5 | from Util import Emoji, Pages, InfractionUtils, Selfroles, Translator, Configuration, MessageUtils, Utils 6 | from views.SelfRole import SelfRoleView 7 | 8 | 9 | async def paged(bot, message, user_id, reaction, **kwargs): 10 | user = await Utils.get_member(bot, message.channel.guild, user_id) 11 | if user is None: 12 | await remove_reaction(message, reaction, Object(user_id)) 13 | return 14 | left = Emoji.get_chat_emoji('LEFT') 15 | right = Emoji.get_chat_emoji('RIGHT') 16 | refresh = Emoji.get_chat_emoji('REFRESH') 17 | r2 = "🔁" 18 | if str(reaction) not in [left, right, refresh, r2]: 19 | return kwargs 20 | action = "REFRESH" 21 | if str(reaction) == left: 22 | action = "PREV" 23 | elif str(reaction) == right: 24 | action = "NEXT" 25 | bot.loop.create_task(remove_reaction(message, reaction, user)) 26 | return await Pages.update(bot, message, action, user_id, **kwargs) 27 | 28 | 29 | async def self_roles(bot, message, user_id, reaction, **kwargs): 30 | v = SelfRoleView(guild=message.guild, page=0) 31 | await message.edit( 32 | Translator.translate("assignable_roles", message.guild, server_name=message.guild.name, page_num=1, 33 | page_count=v.pages), view=v, embed=None) 34 | await unregister(bot, message.id) 35 | try: 36 | await message.clear_reactions() 37 | except Exception as e: 38 | pass 39 | 40 | 41 | async def inf_search(bot, message, user_id, reaction, **kwargs): 42 | user = await Utils.get_member(bot, message.channel.guild, user_id) 43 | left = Emoji.get_chat_emoji('LEFT') 44 | right = Emoji.get_chat_emoji('RIGHT') 45 | refresh = Emoji.get_chat_emoji('REFRESH') 46 | r2 = "🔁" 47 | if str(reaction) not in [left, right, refresh, r2]: 48 | return kwargs 49 | page_num = int(kwargs.get("page_num", 0)) 50 | if str(reaction) == left: 51 | page_num -= 1 52 | elif str(reaction) == right: 53 | page_num += 1 54 | if user is not None: 55 | bot.loop.create_task(remove_reaction(message, reaction, user)) 56 | # return await InfractionUtils.inf_update(message, kwargs.get("query", None), kwargs.get("fields", "").split("-"), kwargs.get("amount", 100), page_num) 57 | 58 | 59 | async def register(bot, message_id, channel_id, type, pipe=None, **kwargs): 60 | if pipe is None: 61 | pipe = bot.redis_pool.pipeline() 62 | key = f"reactor:{message_id}" 63 | pipe.hmset_dict(key, message_id=message_id, channel_id=channel_id, type=type, **kwargs) 64 | pipe.expire(key, kwargs.get("duration", 60 * 60 * 8)) 65 | await pipe.execute() 66 | 67 | async def unregister(bot, message_id): 68 | await bot.redis_pool.unlink(f"reactor:{message_id}") 69 | 70 | 71 | handlers = { 72 | "paged": paged, 73 | "self_role": self_roles, 74 | } 75 | 76 | 77 | async def on_reaction(bot, message_id, channel_id, user_id, reaction): 78 | if user_id == bot.user.id: 79 | return 80 | key = f"reactor:{message_id}" 81 | info = await bot.redis_pool.hgetall(key) 82 | if len(info) >= 3: 83 | type = info["type"] 84 | del info["type"] 85 | 86 | # got to love races 87 | channel = bot.get_channel(channel_id) 88 | if channel is None: 89 | # let's clean a bit while we're here anyways 90 | await bot.redis_pool.unlink(key) 91 | return 92 | try: 93 | message = await channel.fetch_message(message_id) 94 | except (NotFound, Forbidden): 95 | # yay for more races and weird permission setups 96 | await bot.redis_pool.unlink(key) 97 | else: 98 | new_info = await handlers[type](bot, message, user_id, reaction, **info) 99 | if new_info is not None: 100 | pipeline = bot.redis_pool.pipeline() 101 | pipeline.hmset_dict(key, **new_info) 102 | pipeline.expire(key, int(info.get("duration", 60 * 60 * 24))) 103 | pipeline.expire(f"inf_track:{channel.guild.id}", 60 * 60 * 24) 104 | await pipeline.execute() 105 | 106 | 107 | async def remove_reaction(message, reaction, user): 108 | if user is None: 109 | return 110 | try: 111 | await message.remove_reaction(reaction, user) 112 | except (Forbidden, NotFound): 113 | pass 114 | -------------------------------------------------------------------------------- /GearBot/Util/Selfroles.py: -------------------------------------------------------------------------------- 1 | from disnake import Embed, Colour, Forbidden 2 | 3 | from Util import Configuration, Pages, Translator, ReactionManager, Emoji 4 | 5 | 6 | def validate_self_roles(bot, guild): 7 | roles = Configuration.legacy_get_var(guild.id, "ROLES", "SELF_ROLES") 8 | to_remove = set(role for role in roles if guild.get_role(role) is None) 9 | if len(to_remove) > 0: 10 | Configuration.set_var(guild.id, "ROLES", "SELF_ROLES", set(roles) - to_remove) 11 | bot.dispatch("self_roles_update", guild.id) 12 | 13 | 14 | async def create_self_roles(bot, ctx): 15 | # create and send 16 | pages = await gen_role_pages(ctx.guild) 17 | embed = Embed(title=Translator.translate("assignable_roles", ctx, server_name=ctx.guild.name, page_num=1, 18 | page_count=len(pages)), colour=Colour(0xbffdd), description=pages[0]) 19 | message = await ctx.send(embed=embed) 20 | # track in redis 21 | pipe = bot.redis_pool.pipeline() 22 | pipe.sadd(f"self_role:{ctx.guild.id}", message.id) 23 | pipe.expire(f"self_role:{ctx.guild.id}", 60 * 60 * 24 * 7) 24 | bot.loop.create_task( 25 | ReactionManager.register(bot, message.id, ctx.channel.id, "self_role", duration=60 * 60 * 24 * 7, pipe=pipe)) 26 | bot.loop.create_task(update_reactions(message, pages[0], len(pages) > 1)) 27 | 28 | # cleanup 29 | bot.loop.create_task(self_cleaner(bot, ctx.guild.id)) 30 | 31 | 32 | async def update_reactions(message, page, has_multiple): 33 | left = Emoji.get_emoji("LEFT") 34 | if has_multiple and not any(left == r.emoji and r.me for r in message.reactions): 35 | await message.add_reaction(left) 36 | # add numbered reactions 37 | needed = int(len(page.splitlines()) / 2) 38 | added = False 39 | try: 40 | for i in range(10): 41 | reaction = Emoji.get_emoji(str(i + 1)) 42 | if i < needed: 43 | added = True 44 | await message.add_reaction(reaction) 45 | elif any(reaction == r.emoji and r.me for r in message.reactions): 46 | await message.remove_reaction(reaction, message.channel.guild.me) 47 | 48 | right = Emoji.get_emoji("RIGHT") 49 | has_right = any(right == r.emoji and r.me for r in message.reactions) 50 | if added and has_right: 51 | await message.remove_reaction(right, message.channel.guild.me) 52 | has_right = False 53 | if not has_right and has_multiple: 54 | await message.add_reaction(right) 55 | 56 | has_left = any(left == r.emoji and r.me for r in message.reactions) 57 | if has_left and has_multiple: 58 | await message.remove_reaction(left, message.channel.guild.me) 59 | except Forbidden: 60 | pass # we lost access 61 | 62 | 63 | async def self_cleaner(bot, guild_id): 64 | pipeline = bot.redis_pool.pipeline() 65 | key = f"self_role:{guild_id}" 66 | reactors = await bot.redis_pool.smembers(key) 67 | for reactor in reactors: 68 | pipeline.hget(f"reactor:{reactor}", "channel_id") 69 | bits = await pipeline.execute() 70 | out = list() 71 | pipeline = bot.redis_pool.pipeline() 72 | for i in range(len(reactors)): 73 | if bits[i] is None: 74 | pipeline.srem(key, reactors[i]) 75 | else: 76 | out.append((reactors[i], int(bits[i]))) 77 | bot.loop.create_task(pipeline.execute()) 78 | return out 79 | 80 | 81 | async def gen_role_pages(guild): 82 | roles = await Configuration.get_var(guild.id, "ROLES", "SELF_ROLES") 83 | current_roles = "" 84 | count = 1 85 | for role in roles: 86 | current_roles += f"{count}) <@&{role}>\n\n" 87 | count += 1 88 | if count > 10: 89 | count = 1 90 | return Pages.paginate(current_roles, max_lines=20) 91 | -------------------------------------------------------------------------------- /GearBot/Util/SpamBucket.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def ms_time(): 5 | return int(time.time() * 1000) 6 | 7 | 8 | class SpamBucket: 9 | 10 | def __init__(self, redis, key_format, max_actions, period): 11 | self.redis = redis 12 | self.key_format = key_format 13 | self.max_actions = max_actions 14 | self.period = period 15 | 16 | async def incr(self, key, current_time, *, message, channel, user, amt=1, expire=True): 17 | if expire: 18 | await self._remove_expired_keys(key, current_time) 19 | k = self.key_format.format(key) 20 | for i in range(0, amt): 21 | await self.redis.zadd(k, current_time, f"{message}-{channel}-{user}-{i}") 22 | await self.redis.expire(k, self.period) 23 | return await self.redis.zcount(k) 24 | 25 | async def check(self, key, current_time, amount, *, message, channel, user, expire=True): 26 | amt = await self.incr(key, current_time, amt=amount, message=message, channel=channel, user=user, expire=expire) 27 | return amt >= self.max_actions 28 | 29 | async def count(self, key, current_time, expire=True): 30 | if expire: 31 | await self._remove_expired_keys(key, current_time) 32 | k = self.key_format.format(key) 33 | return await self.redis.zcount(k) 34 | 35 | async def get(self, key, current_time, expire=True): 36 | if expire: 37 | await self._remove_expired_keys(key, current_time) 38 | k = self.key_format.format(key) 39 | return await self.redis.zrangebyscore(k) 40 | 41 | async def size(self, key, current_time, expire=True): 42 | if expire: 43 | await self._remove_expired_keys(key, current_time) 44 | k = self.key_format.format(key) 45 | values = await self.redis.zrangebyscore(k) 46 | if len(values) <= 1: 47 | return 0 48 | return (await self.redis.zscore(k, values[-1])) - (await self.redis.zscore(k, values[0])) 49 | 50 | async def clear(self, key): 51 | k = self.key_format.format(key) 52 | await self.redis.zremrangebyscore(k) 53 | 54 | async def _remove_expired_keys(self, key, current_time): 55 | await self.redis.zremrangebyscore(self.key_format.format(key), max=(current_time - self.period)) 56 | -------------------------------------------------------------------------------- /GearBot/Util/Translator.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | import json 4 | import threading 5 | 6 | import urllib.parse 7 | import requests 8 | from parsimonious import ParseError, VisitationError 9 | from pyseeyou import format 10 | 11 | from Util import Configuration, GearbotLogging, Emoji, Utils 12 | 13 | LANGS = dict() 14 | LANG_NAMES = dict(en_US= "English") 15 | LANG_CODES = dict(English="en_US") 16 | BOT = None 17 | untranlatable = {"Sets a playing/streaming/listening/watching status", "Reloads all server configs from disk", "Reset the cache", "Make a role pingable for announcements", "Pulls from github so an upgrade can be performed without full restart", ''} 18 | 19 | async def initialize(bot_in): 20 | global BOT 21 | BOT = bot_in 22 | await load_codes() 23 | await update_all() 24 | for lang in LANG_CODES.values(): 25 | load_translations(lang) 26 | 27 | def load_translations(lang): 28 | LANGS[lang] = Utils.fetch_from_disk(f"lang/{lang}") 29 | 30 | def translate(key, location, **kwargs): 31 | lid = None 32 | if location is not None: 33 | if hasattr(location, "guild"): 34 | location = location.guild 35 | if location is not None and hasattr(location, "id"): 36 | lid = location.id 37 | else: 38 | lid = location 39 | 40 | if lid is None or lid == 0 or lid == "@me": 41 | lang_key = "en_US" 42 | else: 43 | lang_key = Configuration.legacy_get_var(lid, "GENERAL", "LANG") 44 | translated = key 45 | if lang_key not in LANGS: 46 | lang_key = "en_US" 47 | if key not in LANGS[lang_key]: 48 | if key not in untranlatable: 49 | BOT.loop.create_task(tranlator_log('WARNING', f'Untranslatable string detected in {lang_key}: {key}\n')) 50 | untranlatable.add(key) 51 | return key if key not in LANGS["en_US"] else format(LANGS['en_US'][key], kwargs, 'en_US') 52 | try: 53 | translated = format(LANGS[lang_key][key], kwargs, lang_key) 54 | except (KeyError, ValueError, ParseError, VisitationError) as ex: 55 | BOT.loop.create_task(tranlator_log('NO', f'Corrupt translation detected!\n**Lang code:** {lang_key}\n**Translation key:** {key}\n```\n{LANGS[lang_key][key]}```')) 56 | GearbotLogging.exception("Corrupt translation", ex) 57 | if key in LANGS["en_US"].keys(): 58 | try: 59 | translated = format(LANGS['en_US'][key], kwargs, 'en_US') 60 | except (KeyError, ValueError, ParseError, VisitationError) as ex: 61 | BOT.loop.create_task(tranlator_log('NO', f'Corrupt English source string detected!\n**Translation key:** {key}\n```\n{LANGS["en_US"][key]}```')) 62 | GearbotLogging.exception('Corrupt translation', ex) 63 | return translated 64 | 65 | 66 | def translate_by_code(key, code, **kwargs): 67 | if key not in LANGS[code]: 68 | return key 69 | return format(LANGS[code][key], kwargs, code) 70 | 71 | 72 | async def upload(): 73 | t_info = Configuration.get_master_var("TRANSLATIONS", dict(SOURCE="SITE", CHANNEL=0, KEY="", LOGIN="", WEBROOT="")) 74 | if t_info["SOURCE"] == "DISABLED": return 75 | new = hashlib.md5(open(f"lang/en_US.json", 'rb').read()).hexdigest() 76 | old = Configuration.get_persistent_var('lang_hash', '') 77 | if old == new: 78 | return 79 | Configuration.set_persistent_var('lang_hash', new) 80 | message = await tranlator_log('REFRESH', 'Uploading translation file') 81 | t = threading.Thread(target=upload_file) 82 | t.start() 83 | while t.is_alive(): 84 | await asyncio.sleep(1) 85 | await message.edit(content=f"{Emoji.get_chat_emoji('YES')} Translations file has been uploaded") 86 | await update_all() 87 | 88 | 89 | def upload_file(): 90 | data = {'files[/bot/commands.json]': open('lang/en_US.json', 'r')} 91 | crowdin_data = Configuration.get_master_var("TRANSLATIONS", dict(SOURCE="SITE", CHANNEL=0, KEY= "", LOGIN="", WEBROOT="")) 92 | reply = requests.post(f"https://api.crowdin.com/api/project/gearbot/update-file?login={crowdin_data['LOGIN']}&account-key={crowdin_data['KEY']}&json", files=data) 93 | GearbotLogging.info(reply) 94 | 95 | 96 | async def load_codes(): 97 | t_info = Configuration.get_master_var("TRANSLATIONS", dict(SOURCE="SITE", CHANNEL=0, KEY= "", LOGIN="", WEBROOT="")) 98 | if t_info["SOURCE"] == "DISABLED": return 99 | GearbotLogging.info(f"Getting all translations from {t_info['SOURCE']}...") 100 | # set the links for where to get stuff 101 | if t_info["SOURCE"] == "CROWDIN": 102 | list_link = f"https://api.crowdin.com/api/project/gearbot/status?login={t_info['LOGIN']}&account-key={t_info['KEY']}&json" 103 | else: 104 | list_link = "https://gearbot.rocks/lang/langs.json" 105 | 106 | async with BOT.aiosession.get(list_link) as resp: 107 | info = await resp.json() 108 | l = list() 109 | for lang in info: 110 | l.append(dict(name=lang["name"], code=lang["code"])) 111 | LANG_NAMES[lang["code"]] = lang["name"] 112 | LANG_CODES[lang["name"]] = lang["code"] 113 | Utils.save_to_disk("lang/langs", l) 114 | 115 | async def update_all(): 116 | futures = [update_lang(lang) for lang in LANG_CODES.values() if lang != "en_US"] 117 | for chunk in Utils.chunks(futures, 20): 118 | await asyncio.gather(*chunk) 119 | 120 | 121 | async def update_lang(lang, retry=True): 122 | t_info = Configuration.get_master_var("TRANSLATIONS") 123 | if t_info["SOURCE"] == "DISABLED": return 124 | if t_info["SOURCE"] == "CROWDIN": 125 | download_link = f"https://api.crowdin.com/api/project/gearbot/export-file?login={t_info['LOGIN']}&account-key={t_info['KEY']}&json&file={urllib.parse.quote('/bot/commands.json', safe='')}&language={lang}" 126 | else: 127 | download_link = f"https://gearbot.rocks/lang/{lang}.json" 128 | GearbotLogging.info(f"Updating {lang} ({LANG_NAMES[lang]}) file...") 129 | async with BOT.aiosession.get(download_link) as response: 130 | content = await response.text() 131 | content = json.loads(content) 132 | if "success" in content: 133 | if retry: 134 | GearbotLogging.warn(f"Failed to update {lang} ({LANG_NAMES[lang]}), trying again in 3 seconds") 135 | await asyncio.sleep(3) 136 | await update_lang(lang, False) 137 | else: 138 | await tranlator_log('NO', f"Failed to update {lang} ({LANG_NAMES[lang]}) from {t_info['SOURCE']}") 139 | Utils.save_to_disk(f'lang/{lang}', content) 140 | LANGS[lang] = content 141 | GearbotLogging.info(f"Updated {lang} ({LANG_NAMES[lang]})!") 142 | 143 | 144 | async def tranlator_log(emoji, message, embed=None): 145 | m = f'{Emoji.get_chat_emoji(emoji)} {message}' 146 | return await get_translator_log_channel()(m, embed=embed) 147 | 148 | def get_translator_log_channel(): 149 | crowdin = Configuration.get_master_var("TRANSLATIONS", dict(SOURCE="SITE", CHANNEL=0, KEY= "", LOGIN="", WEBROOT="")) 150 | channel = BOT.get_channel(crowdin["CHANNEL"]) if crowdin is not None else None 151 | return channel.send if channel is not None else GearbotLogging.bot_log -------------------------------------------------------------------------------- /GearBot/Util/Update.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from Bot import Reloader, TheRealGearBot 4 | from Util import GearbotLogging, Emoji, Utils, Translator, Configuration 5 | 6 | 7 | async def upgrade(name, bot): 8 | await GearbotLogging.bot_log(f"{Emoji.get_chat_emoji('REFRESH')} Upgrade initiated by {name}") 9 | GearbotLogging.info(f"Upgrade initiated by {name}") 10 | file = open("upgradeRequest", "w") 11 | file.write("upgrade requested") 12 | file.close() 13 | await bot.logout() 14 | 15 | 16 | async def update(name, bot): 17 | message = await GearbotLogging.bot_log(f"{Emoji.get_chat_emoji('REFRESH')} Hot reload in progress... (initiated by {name})") 18 | await Utils.execute(["git pull origin master"]) 19 | GearbotLogging.info("Initiating hot reload") 20 | antiraid = bot.get_cog('AntiRaid') 21 | trackers = None 22 | if antiraid is not None: 23 | trackers = antiraid.raid_trackers 24 | untranslatable = Translator.untranlatable 25 | importlib.reload(Reloader) 26 | for c in Reloader.components: 27 | importlib.reload(c) 28 | Translator.untranlatable = untranslatable 29 | GearbotLogging.info("Reloading all cogs...") 30 | temp = [] 31 | for cog in bot.cogs: 32 | temp.append(cog) 33 | for cog in temp: 34 | bot.unload_extension(f"Cogs.{cog}") 35 | GearbotLogging.info(f'{cog} has been unloaded.') 36 | bot.load_extension(f"Cogs.{cog}") 37 | GearbotLogging.info(f'{cog} has been loaded.') 38 | to_unload = Configuration.get_master_var("DISABLED_COMMANDS", []) 39 | for c in to_unload: 40 | bot.remove_command(c) 41 | 42 | antiraid = bot.get_cog('AntiRaid') 43 | if antiraid is not None and trackers is not None: 44 | antiraid.raid_trackers = trackers 45 | 46 | await TheRealGearBot.initialize(bot) 47 | c = await Utils.get_commit() 48 | GearbotLogging.info(f"Hot reload complete, now running on {c}") 49 | bot.version = c 50 | bot.hot_reloading = False 51 | m = f"{Emoji.get_chat_emoji('YES')} Hot reload complete, now running on {bot.version} (update initiated by {name})" 52 | await message.edit(content=m) 53 | bot.loop.create_task(Translator.upload()) -------------------------------------------------------------------------------- /GearBot/Util/VersionInfo.py: -------------------------------------------------------------------------------- 1 | from distutils.version import LooseVersion 2 | 3 | 4 | def compareVersions(v1, v2): 5 | return LooseVersion(v1 if v1 != 'unknown' else 0) > LooseVersion(v2 if v2 != 'unknown' else 0) 6 | 7 | def cmp_to_key(mycmp): 8 | 'Convert a cmp= function into a key= function' 9 | class K(object): 10 | def __init__(self, obj, *args): 11 | self.obj = obj 12 | def __lt__(self, other): 13 | return mycmp(self.obj, other.obj) > 0 14 | def __gt__(self, other): 15 | return mycmp(self.obj, other.obj) < 0 16 | def __eq__(self, other): 17 | return mycmp(self.obj, other.obj) == 0 18 | def __le__(self, other): 19 | return mycmp(self.obj, other.obj) >= 0 20 | def __ge__(self, other): 21 | return mycmp(self.obj, other.obj) <= 0 22 | def __ne__(self, other): 23 | return mycmp(self.obj, other.obj) != 0 24 | return K 25 | 26 | 27 | def getSortedVersions(versions): 28 | return sorted(list(versions), key=cmp_to_key(compareVersions)) 29 | 30 | def getLatest(versions): 31 | result = getSortedVersions(versions) 32 | return result[0] if len(result) > 0 else None -------------------------------------------------------------------------------- /GearBot/Util/server_info.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | 4 | import disnake 5 | from disnake import utils 6 | 7 | from Util import Translator, Emoji, Utils, Configuration 8 | 9 | 10 | def server_info_embed(guild, request_guild=None): 11 | guild_features = ", ".join(guild.features).title().replace("_", " ") 12 | if guild_features == "": 13 | guild_features = None 14 | guild_made = guild.created_at 15 | embed = disnake.Embed(color=guild.roles[-1].color, timestamp=datetime.datetime.utcfromtimestamp(time.time()).replace(tzinfo=datetime.timezone.utc)) 16 | if guild.icon is not None: 17 | embed.set_thumbnail(url=guild.icon.url) 18 | embed.add_field(name=Translator.translate('server_name', request_guild), value=guild.name, inline=True) 19 | embed.add_field(name=Translator.translate('id', request_guild), value=guild.id, inline=True) 20 | embed.add_field(name=Translator.translate('owner', request_guild), value=guild.owner, inline=True) 21 | embed.add_field(name=Translator.translate('members', request_guild), value=guild.member_count, inline=True) 22 | 23 | embed.add_field( 24 | name=Translator.translate('channels', request_guild), 25 | value=f"{Emoji.get_chat_emoji('CATEGORY')} {Translator.translate('categories', request_guild)}: {str(len(guild.categories))}\n" 26 | f"{Emoji.get_chat_emoji('CHANNEL')} {Translator.translate('text_channels', request_guild)}: {str(len(guild.text_channels))}\n" 27 | f"{Emoji.get_chat_emoji('VOICE')} {Translator.translate('voice_channels', request_guild)}: {str(len(guild.voice_channels))}\n" 28 | f"{Translator.translate('total_channel', request_guild)}: {str(len(guild.text_channels) + len(guild.voice_channels))}", 29 | inline=True 30 | ) 31 | embed.add_field( 32 | name=Translator.translate('created_at', request_guild), 33 | value=f"{utils.format_dt(guild_made, 'F')} ({utils.format_dt(guild_made, 'R')})", 34 | inline=True 35 | ) 36 | embed.add_field( 37 | name=Translator.translate('vip_features', request_guild), 38 | value=guild_features, 39 | inline=True 40 | ) 41 | if guild.icon is not None: 42 | embed.add_field( 43 | name=Translator.translate('server_icon', request_guild), 44 | value=f"[{Translator.translate('server_icon', request_guild)}]({guild.icon.url})", 45 | inline=True 46 | ) 47 | 48 | roles = ", ".join(role.name for role in guild.roles) 49 | embed.add_field( 50 | name=Translator.translate('all_roles', request_guild), 51 | value=roles if len(roles) < 1024 else f"{len(guild.roles)} roles", 52 | inline=False 53 | ) 54 | 55 | if guild.emojis: 56 | emoji = "".join(str(e) for e in guild.emojis) 57 | embed.add_field( 58 | name=Translator.translate('emoji', request_guild), 59 | value=emoji if len(emoji) < 1024 else f"{len(guild.emojis)} emoji" 60 | ) 61 | 62 | if guild.splash is not None: 63 | embed.set_image(url=guild.splash.url) 64 | if guild.banner is not None: 65 | embed.set_image(url=guild.banner.url) 66 | 67 | return embed 68 | 69 | 70 | async def server_info_raw(bot, guild): 71 | statuses = dict(online=0, idle=0, dnd=0, offline=0) 72 | for m in guild.members: 73 | statuses[str(m.status)] += 1 74 | extra = dict() 75 | for g in await Configuration.get_var(guild.id, "SERVER_LINKS"): 76 | extra.update(**{str(k): v for k, v in get_server_channels(bot.get_guild(g)).items()}) 77 | server_info = dict( 78 | name=guild.name, 79 | id=str(guild.id), # send as string, js can't deal with it otherwise 80 | icon=str(guild.icon), 81 | owner={ 82 | "id": str(guild.owner.id), 83 | "name": Utils.clean_user(guild.owner) 84 | }, 85 | members=guild.member_count, 86 | text_channels=get_server_channels(guild), 87 | additional_text_channels=extra, 88 | voice_channels=len(guild.voice_channels), 89 | creation_date=guild.created_at.strftime("%d-%m-%Y"), # TODO: maybe date and have the client do the displaying? 90 | age_days=(datetime.datetime.fromtimestamp(time.time()) - guild.created_at).days, 91 | vip_features=guild.features, 92 | role_list={ 93 | r.id: { 94 | "id": str(r.id), 95 | "name": r.name, 96 | "color": '#{:0>6x}'.format(r.color.value), 97 | "members": len(r.members), 98 | "is_admin": r.permissions.administrator, 99 | "is_mod": r.permissions.ban_members, 100 | "can_be_self_role": not r.managed and guild.me.top_role > r and r.id != guild.id, 101 | } for r in guild.roles}, 102 | emojis=[ 103 | { 104 | "id": str(e.id), 105 | "name": e.name 106 | } 107 | for e in guild.emojis], 108 | member_statuses=statuses 109 | ) 110 | 111 | return server_info 112 | 113 | 114 | def get_server_channels(guild): 115 | return { 116 | str(c.id): { 117 | 'name': c.name, 118 | 'can_log': c.permissions_for(c.guild.me).send_messages and c.permissions_for( 119 | c.guild.me).attach_files and c.permissions_for(c.guild.me).embed_links 120 | } for c in guild.text_channels 121 | } 122 | 123 | 124 | def time_difference(begin, end, location): 125 | diff = begin - end 126 | minutes, seconds = divmod(diff.days * 86400 + diff.seconds, 60) 127 | hours, minutes = divmod(minutes, 60) 128 | if diff.days > 0: 129 | return Translator.translate("days", location, amount=diff.days) 130 | else: 131 | return Translator.translate( 132 | "hours", 133 | location, 134 | hours=hours, 135 | minutes=minutes 136 | ) 137 | -------------------------------------------------------------------------------- /GearBot/database/DBUtils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import datetime 3 | 4 | from disnake import MessageType 5 | from tortoise.exceptions import IntegrityError 6 | from tortoise.transactions import in_transaction 7 | 8 | from Bot import TheRealGearBot 9 | from Util import GearbotLogging 10 | from database.DatabaseConnector import LoggedMessage, LoggedAttachment 11 | from collections import namedtuple 12 | 13 | batch = dict() 14 | recent_list = set() 15 | previous_list = set() 16 | last_flush = datetime.datetime.now() 17 | fakeLoggedMessage = namedtuple("BufferedMessage", "messageid content author channel server type pinned attachments") 18 | 19 | violation_regex = re.compile("duplicate key value violates unique constraint .* DETAIL: Key \(id\)=\((\d)\) already exists.*") 20 | 21 | async def insert_message(message): 22 | # if message.id not in recent_list and message.id not in previous_list: 23 | message_type = message.type 24 | if message_type == MessageType.default: 25 | message_type = None 26 | else: 27 | if not isinstance(message_type, int): 28 | message_type = message_type.value 29 | # m = fakeLoggedMessage(messageid=message.id, content=message.content, 30 | # author=message.author.id, 31 | # channel=message.channel.id, server=message.guild.id, 32 | # type=message_type, pinned=message.pinned, attachments=[LoggedAttachment(id=a.id, name=a.filename, 33 | # isimage=(a.width is not None or a.width is 0), 34 | # message_id=message.id) for a in message.attachments]) 35 | # batch[message.id] = m 36 | # 37 | # recent_list.add(message.id) 38 | # if len(batch) >= 1000: 39 | # asyncio.create_task(flush(force=True)) 40 | 41 | try: 42 | async with in_transaction(): 43 | is_reply = message.reference is not None and message.reference.channel_id == message.channel.id 44 | logged = await LoggedMessage.create(messageid=message.id, content=message.content.replace('\x00', ''), 45 | author=message.author.id, 46 | channel=message.channel.id, server=message.guild.id, 47 | type=message_type, pinned=message.pinned, 48 | reply_to=message.reference.message_id if is_reply else None) 49 | for a in message.attachments: 50 | await LoggedAttachment.create(id=a.id, name=a.filename, 51 | isimage=(a.width is not None or a.width == 0), 52 | message=logged) 53 | 54 | except IntegrityError: 55 | return message 56 | return message 57 | 58 | 59 | 60 | async def flush(force=False): 61 | try: 62 | if force or (datetime.datetime.now() - last_flush).total_seconds() > 4 * 60: 63 | await do_flush() 64 | except Exception as e: 65 | await TheRealGearBot.handle_exception("Message flushing", None, e) 66 | 67 | 68 | async def do_flush(): 69 | global batch, recent_list, previous_list, last_flush 70 | 71 | mine = batch 72 | batch = dict() 73 | previous_list = recent_list 74 | recent_list = set() 75 | 76 | excluded = set() 77 | while len(excluded) < len(mine): 78 | try: 79 | to_insert = set() 80 | to_insert_attachements = set() 81 | for message in mine.values(): 82 | if message.messageid in excluded: 83 | continue 84 | to_insert.add(LoggedMessage(messageid=message.messageid, content=message.content, 85 | author=message.author, 86 | channel=message.channel, server=message.server, 87 | type=message.type, pinned=message.pinned)) 88 | for a in message.attachments: 89 | if a.id not in excluded: 90 | to_insert_attachements.add(a) 91 | 92 | async with in_transaction(): 93 | await LoggedMessage.bulk_create(to_insert) 94 | await LoggedAttachment.bulk_create(to_insert_attachements) 95 | last_flush = datetime.now() 96 | return 97 | except IntegrityError as e: 98 | match = re.match(violation_regex, str(e)) 99 | if match is not None: 100 | excluded.add(int(match.group(1))) 101 | GearbotLogging.log_key(f"Failed to propagate, duplicate {int(match.group(1))}") 102 | else: 103 | raise e 104 | 105 | 106 | def get_messages_for_channel(channel_id): 107 | return [message for message in batch.values() if message.channel == channel_id] 108 | 109 | def get_messages_for_user_in_guild(user_id, guild_id): 110 | return [message for message in batch.values() if message.server == guild_id and message.author == user_id] -------------------------------------------------------------------------------- /GearBot/database/DatabaseConnector.py: -------------------------------------------------------------------------------- 1 | from tortoise.models import Model 2 | from tortoise import fields, Tortoise 3 | 4 | from Util import Configuration, GearbotLogging 5 | 6 | 7 | class LoggedMessage(Model): 8 | messageid = fields.BigIntField(pk=True, generated=False) 9 | content = fields.CharField(max_length=2000, collation="utf8mb4_general_ci", null=True) 10 | author = fields.BigIntField() 11 | channel = fields.BigIntField() 12 | server = fields.BigIntField() 13 | type = fields.IntField(null=True) 14 | pinned = fields.BooleanField(default=False) 15 | reply_to = fields.BigIntField(null=True) 16 | 17 | 18 | class LoggedAttachment(Model): 19 | id = fields.BigIntField(pk=True, generated=False) 20 | name = fields.CharField(max_length=100) 21 | isimage = fields.BooleanField() 22 | message = fields.ForeignKeyField("models.LoggedMessage", related_name='attachments', source_field='message_id') 23 | 24 | 25 | class CustomCommand(Model): 26 | id = fields.IntField(pk=True, generated=True) 27 | serverid = fields.BigIntField() 28 | trigger = fields.CharField(max_length=20, collation="utf8mb4_general_ci") 29 | response = fields.CharField(max_length=2000, collation="utf8mb4_general_ci") 30 | created_by = fields.BigIntField(null=True) 31 | 32 | 33 | 34 | class Infraction(Model): 35 | id = fields.IntField(pk=True, generated=True) 36 | guild_id = fields.BigIntField() 37 | user_id = fields.BigIntField() 38 | mod_id = fields.BigIntField() 39 | type = fields.CharField(max_length=10, collation="utf8mb4_general_ci") 40 | reason = fields.CharField(max_length=2000, collation="utf8mb4_general_ci") 41 | start = fields.BigIntField() 42 | end = fields.BigIntField(null=True) 43 | active = fields.BooleanField(default=True) 44 | 45 | 46 | class Reminder(Model): 47 | id = fields.IntField(pk=True, generated=True) 48 | user_id = fields.BigIntField() 49 | channel_id = fields.BigIntField() 50 | guild_id = fields.CharField(max_length=20) 51 | message_id = fields.BigIntField() 52 | dm = fields.BooleanField() 53 | to_remind = fields.CharField(max_length=1800, collation="utf8mb4_general_ci") 54 | send = fields.BigIntField(null=True) 55 | time = fields.BigIntField() 56 | status = fields.IntField() 57 | 58 | 59 | 60 | class Raid(Model): 61 | id = fields.IntField(pk=True, generated=True) 62 | guild_id = fields.BigIntField() 63 | start = fields.BigIntField() 64 | end = fields.BigIntField(null=True) 65 | 66 | 67 | class Raider(Model): 68 | id = fields.IntField(pk=True, generated=True) 69 | raid = fields.ForeignKeyField("models.Raid", related_name="raiders", source_field="raid_id") 70 | user_id = fields.BigIntField() 71 | joined_at = fields.BigIntField() 72 | 73 | 74 | class RaidAction(Model): 75 | id = fields.IntField(pk=True, generated=True) 76 | raider = fields.ForeignKeyField("models.Raider", related_name="actions_taken", source_field="raider_id") 77 | action = fields.CharField(max_length=20) 78 | infraction = fields.ForeignKeyField("models.Infraction", related_name="RaiderAction", source_field="infraction_id", null=True) 79 | 80 | class Node(Model): 81 | hostname = fields.CharField(max_length=50, pk=True) 82 | generation = fields.IntField() 83 | shard = fields.IntField() 84 | resource_version = fields.CharField(max_length=50) 85 | 86 | class GuildConfig(Model): 87 | guild_id = fields.BigIntField(pk=True) 88 | guild_config = fields.JSONField() 89 | 90 | class Meta: 91 | table = "guild_config" 92 | 93 | async def init(): 94 | GearbotLogging.info("Connecting to the database...") 95 | await Tortoise.init( 96 | db_url=Configuration.get_master_var('DATABASE'), 97 | modules={"models": ["database.DatabaseConnector"]} 98 | ) 99 | await Tortoise.generate_schemas() 100 | -------------------------------------------------------------------------------- /GearBot/views/Buttons.py: -------------------------------------------------------------------------------- 1 | from disnake import ButtonStyle, Interaction 2 | from disnake.ui import Button 3 | 4 | from Util import MessageUtils 5 | 6 | 7 | class CallbackButton(Button): 8 | def __init__(self, label, callback, cid=None, disabled=False, emoji=None, style=ButtonStyle.blurple): 9 | super().__init__(style=style, label=label, custom_id=cid, disabled=disabled, emoji=emoji) 10 | self.to_callback = callback 11 | 12 | async def callback(self, interaction: Interaction): 13 | await self.to_callback(interaction) 14 | 15 | 16 | class InvokerOnlyCallbackButton(Button): 17 | def __init__(self, user_id, guild_id, label, callback, cid=None, disabled=False, emoji=None, style=ButtonStyle.blurple): 18 | super().__init__(style=style, label=label, custom_id=cid, disabled=disabled, emoji=emoji) 19 | 20 | self.to_callback = callback 21 | self.user_id=user_id 22 | self.guild_id=guild_id 23 | 24 | async def callback(self, interaction: Interaction): 25 | if self.user_id == interaction.user.id: 26 | await self.to_callback(interaction) 27 | self.view.stop() 28 | else: 29 | interaction.response.send_message(MessageUtils.assemble(self.guild_id, "NO", "wrong_interactor"), 30 | ephemeral=True) 31 | await self.to_callback(interaction) -------------------------------------------------------------------------------- /GearBot/views/Confirm.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake import ButtonStyle, Interaction 3 | from disnake.ui import Button 4 | 5 | from Util import MessageUtils 6 | 7 | 8 | class YesButton(Button['Confirm']): 9 | def __init__(self): 10 | super().__init__(style=ButtonStyle.green, label="Yes") 11 | 12 | async def callback(self, interaction: Interaction): 13 | await self.view.yes_callback(interaction) 14 | 15 | 16 | class NoButton(Button): 17 | def __init__(self): 18 | super().__init__(style=ButtonStyle.red, label="No") 19 | 20 | async def callback(self, interaction: Interaction): 21 | await self.view.no_callback(interaction) 22 | 23 | 24 | class Confirm(disnake.ui.View): 25 | def __init__(self, guild_id, on_yes, on_no, on_timeout, check, timeout=30): 26 | super().__init__(timeout=timeout) 27 | self.add_item(YesButton()) 28 | self.add_item(NoButton()) 29 | self.guild_id = guild_id 30 | self.on_yes = on_yes 31 | self.on_no = on_no 32 | self.timeout_callback = on_timeout 33 | self.check = check 34 | 35 | async def on_timeout(self) -> None: 36 | await self.timeout_callback() 37 | 38 | async def yes_callback(self, interaction: Interaction): 39 | if await self.execution_check(interaction): 40 | await self.on_yes(interaction) 41 | self.stop() 42 | 43 | async def no_callback(self, interaction: Interaction): 44 | if await self.execution_check(interaction): 45 | await self.on_no(interaction) 46 | self.stop() 47 | 48 | async def execution_check(self, interaction: Interaction): 49 | if not self.check: 50 | return True 51 | if self.check(interaction): 52 | return True 53 | await self.refuse(interaction) 54 | return False 55 | 56 | async def refuse(self, interaction: Interaction): 57 | interaction.response.send_message(MessageUtils.assemble(self.guild_id, "NO", "wrong_interactor"), 58 | ephemeral=True) 59 | -------------------------------------------------------------------------------- /GearBot/views/EphemeralInfSearch.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from disnake import ButtonStyle, Interaction 4 | from disnake.ui import View, Button 5 | 6 | from Util import Emoji, Utils, MessageUtils, InfractionUtils, Translator 7 | from Util.InfractionUtils import fetch_infraction_pages, get_key 8 | from views.Buttons import CallbackButton 9 | 10 | 11 | class EphemeralInfSearch(View): 12 | def __init__(self, filters, pages, guild_id, userid, current_page=0): 13 | super().__init__(timeout=None) 14 | self.id="" 15 | if pages > 0: 16 | start = f"einf_search:{userid}:{current_page}" 17 | 18 | self.add_item(Button(label=Translator.translate('first_page', guild_id), custom_id=f'{start}:first_page', disabled=current_page == 0, style=ButtonStyle.blurple)) 19 | self.add_item(Button(label=Translator.translate('prev_page', guild_id), custom_id=f'{start}:prev_page', disabled=current_page == 0, style=ButtonStyle.blurple)) 20 | self.add_item(Button(emoji=Emoji.get_emoji('AE'), style=ButtonStyle.grey, custom_id=f'{start}:blank', label=None)) 21 | self.add_item(Button(label=Translator.translate('next_page', guild_id), custom_id=f'{start}:next_page', disabled=current_page >= pages-1, style=ButtonStyle.blurple)) 22 | self.add_item(Button(label=Translator.translate('last_page', guild_id), custom_id=f'{start}:last_page', disabled=current_page >= pages-1, style=ButtonStyle.blurple)) 23 | self.stop() 24 | # self.add_item(CallbackButton(label='User', style=ButtonStyle.green if '[user]' in filters else ButtonStyle.red, cid=f'{start}:user', callback=self.on_toggle_user)) 25 | # self.add_item(CallbackButton(label='Mod', style=ButtonStyle.green if '[mod]' in filters else ButtonStyle.red, cid=f'{start}:mod', callback=self.on_toggle_mod)) 26 | # self.add_item(CallbackButton(label='reason', style=ButtonStyle.green if '[reason]' in filters else ButtonStyle.red, cid=f'{start}:reason', callback=self.on_toggle_reason)) 27 | 28 | 29 | 30 | async def get_ephemeral_cached_page(interaction, userid, new_page): 31 | key = get_key(interaction.guild_id, userid, ["[user]", "[mod]", "[reason]"], 100) 32 | count = await Utils.BOT.redis_pool.llen(key) 33 | page_num = new_page 34 | if count == 0: 35 | count = await fetch_infraction_pages(interaction.guild_id, userid, 100, ["[user]", "[mod]", "[reason]"], 36 | new_page) 37 | if page_num >= count: 38 | page_num = 0 39 | elif page_num < 0: 40 | page_num = count - 1 41 | page = (await Utils.BOT.wait_for("page_assembled", check=lambda l: l["key"] == key and l["page_num"] == page_num))["page"] 42 | else: 43 | if page_num == 1000: 44 | page_num = count-1 45 | elif page_num >= count: 46 | page_num = 0 47 | elif page_num < 0: 48 | page_num = count - 1 49 | page = await Utils.BOT.redis_pool.lindex(key, page_num) 50 | return page, page_num, count, userid, ["[user]", "[mod]", "[reason]"] 51 | -------------------------------------------------------------------------------- /GearBot/views/ExtendMute.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake import ButtonStyle 3 | 4 | from Util import Translator 5 | from views.Buttons import InvokerOnlyCallbackButton 6 | 7 | 8 | class ExtendMuteView(disnake.ui.View): 9 | def __init__(self, extend, until, overwrite, duration, guild_id, user_id): 10 | super().__init__(timeout=30) 11 | 12 | self.add_item(InvokerOnlyCallbackButton(user_id=user_id, guild_id=guild_id, label=Translator.translate('mute_option_extend', guild_id, duration=duration), callback=extend, style=ButtonStyle.blurple)) 13 | self.add_item(InvokerOnlyCallbackButton(user_id=user_id, guild_id=guild_id, label=Translator.translate('mute_option_until', guild_id, duration=duration), callback=until, style=ButtonStyle.blurple)) 14 | self.add_item(InvokerOnlyCallbackButton(user_id=user_id, guild_id=guild_id, label=Translator.translate('mute_option_overwrite', guild_id, duration=duration), callback=overwrite, style=ButtonStyle.blurple)) 15 | -------------------------------------------------------------------------------- /GearBot/views/GlobalInfSearch.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gearbot/GearBot/b91c48eafde0ef4612442b33f10e70d2475c5489/GearBot/views/GlobalInfSearch.py -------------------------------------------------------------------------------- /GearBot/views/Help.py: -------------------------------------------------------------------------------- 1 | from disnake import Interaction, ButtonStyle, SelectOption, SelectMenu 2 | from disnake.ui import Button, View, Select 3 | 4 | from Cogs import BaseCog 5 | from Util import Translator, Emoji, HelpGenerator, Utils, Pages 6 | from views.Buttons import CallbackButton 7 | 8 | 9 | class HelpView(View): 10 | def __init__(self, bot, guild_id, query, page, pages, with_select): 11 | super().__init__(timeout=None) 12 | set_components(self, guild_id, page, pages, query, bot, with_select) 13 | self.stop() 14 | 15 | 16 | def set_components(view: View, guild_id, page, pages, query, bot, with_select): 17 | view.children.clear() 18 | if with_select: 19 | options = [] 20 | options.append(SelectOption(label=Translator.translate('all', guild_id), value='None', 21 | description=Translator.translate('all_description', guild_id), 22 | default=query is None)) 23 | for cog in bot.cogs: 24 | if cog in BaseCog.cog_permissions: 25 | options.append(SelectOption(label=cog, value=cog.lower(), 26 | description=Translator.translate('help_cog_description', guild_id, cog=cog), 27 | default=query == cog.lower())) 28 | view.add_item(Select(custom_id='help:selector', options=options)) 29 | if pages > 1: 30 | view.add_item( 31 | Button(label=Translator.translate('first_page', guild_id), disabled=page == 0, 32 | custom_id=f"help:page:0:{query}")) 33 | view.add_item(Button(label=Translator.translate('prev_page', guild_id), disabled=page == 0, 34 | custom_id=f"help:page:0{page - 1}:{query}")) 35 | view.add_item(Button(label=Translator.translate('next_page', guild_id), disabled=page == pages - 1, 36 | custom_id=f"help:page:{page + 1}:{query}")) 37 | view.add_item(Button(label=Translator.translate('last_page', guild_id), disabled=page == pages - 1, 38 | custom_id=f"help:page:00{pages - 1}:{query}")) 39 | 40 | 41 | async def message_parts(bot, query, guild, member, page_num): 42 | view = None 43 | raw_pages = await get_help_pages(query, guild, member, bot) 44 | if raw_pages is None: 45 | if query in [cog.lower() for cog in bot.cogs]: 46 | raw_pages = [Translator.translate('no_runnable_commands', guild)] 47 | else: 48 | return Translator.translate("help_not_found" if len(query) < 1500 else "help_no_wall_allowed", guild, 49 | query=await Utils.clean(query, emoji=False)), None 50 | if page_num >= len(raw_pages): 51 | page_num = 0 52 | eyes = Emoji.get_chat_emoji('EYES') 53 | content = f"{eyes} **{Translator.translate('help_title', guild, page_num=page_num + 1, pages=len(raw_pages))}** {eyes}```diff\n{raw_pages[page_num]}```" 54 | cog_names = [cog.lower() for cog in bot.cogs] 55 | if query is None or query.lower() in cog_names or len(raw_pages) > 1: 56 | view = HelpView(bot, guild, query, page_num, len(raw_pages), True) 57 | return content, view 58 | 59 | 60 | async def get_help_pages(query, guild, member, bot): 61 | if query is None: 62 | return await HelpGenerator.command_list(bot, member, guild) 63 | else: 64 | for cog in bot.cogs: 65 | if query == cog.lower(): 66 | return await HelpGenerator.gen_cog_help(bot, cog, member, guild) 67 | target = bot 68 | layers = query.split(" ") 69 | while len(layers) > 0: 70 | layer = layers.pop(0) 71 | if hasattr(target, "all_commands") and layer in target.all_commands.keys(): 72 | target = target.all_commands[layer] 73 | else: 74 | target = None 75 | break 76 | if target is not None and target is not bot.all_commands: 77 | return await HelpGenerator.gen_command_help(bot, member, guild, target) 78 | -------------------------------------------------------------------------------- /GearBot/views/InfSearch.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from disnake import ButtonStyle, Interaction 4 | from disnake.ui import View, Button 5 | 6 | from Util import Emoji, Utils, MessageUtils, InfractionUtils, Translator 7 | from Util.InfractionUtils import fetch_infraction_pages, get_key 8 | from views.Buttons import CallbackButton 9 | 10 | 11 | class InfSearch(View): 12 | def __init__(self, filters, pages, guild_id, current_page=0, ephemeral=False, userid=""): 13 | super().__init__(timeout=None) 14 | self.id="" 15 | if pages > 0: 16 | self.add_item(CallbackButton(Translator.translate('first_page', guild_id), self.on_first_page, 'inf_search:first_page', disabled=current_page == 0)) 17 | self.add_item(CallbackButton(Translator.translate('prev_page', guild_id), self.on_prev_page, 'inf_search:prev_page', disabled=current_page == 0)) 18 | self.add_item(CallbackButton(emoji=Emoji.get_emoji('AE'), style=ButtonStyle.grey, cid='inf_search:blank', callback=self.hi, label=None)) 19 | self.add_item(CallbackButton(Translator.translate('next_page', guild_id), self.on_next_page, 'inf_search:next_page', disabled=current_page >= pages-1)) 20 | self.add_item(CallbackButton(Translator.translate('last_page', guild_id), self.on_last_page, 'inf_search:last_page', disabled=current_page >= pages-1)) 21 | # self.add_item(CallbackButton(label='User', style=ButtonStyle.green if '[user]' in filters else ButtonStyle.red, cid='inf_search:user', callback=self.on_toggle_user)) 22 | # self.add_item(CallbackButton(label='Mod', style=ButtonStyle.green if '[mod]' in filters else ButtonStyle.red, cid='inf_search:mod', callback=self.on_toggle_mod)) 23 | # self.add_item(CallbackButton(label='reason', style=ButtonStyle.green if '[reason]' in filters else ButtonStyle.red, cid='inf_search:reason', callback=self.on_toggle_reason)) 24 | 25 | 26 | @staticmethod 27 | async def on_first_page(interaction: Interaction): 28 | page, current, pages, query, fields = await get_cached_page(interaction, 100) 29 | await interaction.response.edit_message( 30 | content=await InfractionUtils.assemble_message(interaction.guild_id, page, query, current, pages), 31 | view=InfSearch(filters=fields, pages=pages, current_page=current, guild_id=interaction.guild_id) 32 | ) 33 | 34 | 35 | @staticmethod 36 | async def on_prev_page(interaction: Interaction): 37 | page, current, pages, query, fields = await get_cached_page(interaction, -1) 38 | await interaction.response.edit_message( 39 | content=await InfractionUtils.assemble_message(interaction.guild_id, page, query, current, pages), 40 | view=InfSearch(filters=fields, pages=pages, current_page=current, guild_id=interaction.guild_id) 41 | ) 42 | 43 | @staticmethod 44 | async def on_next_page(interaction: Interaction): 45 | page, current, pages, query, fields = await get_cached_page(interaction, 1) 46 | await interaction.response.edit_message( 47 | content=await InfractionUtils.assemble_message(interaction.guild_id, page, query, current, pages), 48 | view=InfSearch(filters=fields, pages=pages, current_page=current, guild_id=interaction.guild_id) 49 | ) 50 | 51 | 52 | 53 | 54 | @staticmethod 55 | async def on_last_page(interaction: Interaction): 56 | page, current, pages, query, fields = await get_cached_page(interaction, -100) 57 | await interaction.response.edit_message( 58 | content=await InfractionUtils.assemble_message(interaction.guild_id, page, query, current, pages), 59 | view=InfSearch(filters=fields, pages=pages, current_page=current, guild_id=interaction.guild_id) 60 | ) 61 | 62 | @staticmethod 63 | async def on_toggle_user(interaction: Interaction): 64 | pass 65 | 66 | @staticmethod 67 | async def on_toggle_mod(interaction: Interaction): 68 | pass 69 | 70 | @staticmethod 71 | async def on_toggle_reason(interaction: Interaction): 72 | pass 73 | 74 | @staticmethod 75 | async def hi(interaction: Interaction): 76 | await interaction.response.send_message(Emoji.get_chat_emoji('AE'), ephemeral=True) 77 | 78 | 79 | async def get_meta(mid): 80 | return await Utils.BOT.redis_pool.get(f'inf_meta:{mid}') 81 | 82 | async def get_cached_page(interaction: Interaction, diff): 83 | meta = await get_meta(interaction.message.id) 84 | if meta is None: 85 | await interaction.response.send_message(MessageUtils.assemble(interaction.guild_id, 'NO', 'no_longer_valid'), 86 | ephemeral=True) 87 | return 88 | m = json.loads(meta) 89 | key = get_key(interaction.guild_id, m['query'], m['fields'].split(' '), m['amount']) 90 | count = await Utils.BOT.redis_pool.llen(key) 91 | page_num = m['current_page'] + diff 92 | if count == 0: 93 | count = await fetch_infraction_pages(interaction.guild_id, m['query'], m['amount'], m['fields'].split(' '), 94 | meta['current_page']) 95 | if page_num >= count: 96 | page_num = 0 97 | elif page_num < 0: 98 | page_num = count - 1 99 | page = (await Utils.BOT.wait_for("page_assembled", check=lambda l: l["key"] == key and l["page_num"] == page_num))["page"] 100 | else: 101 | if page_num >= count: 102 | page_num = 0 103 | elif page_num < 0: 104 | page_num = count - 1 105 | page = await Utils.BOT.redis_pool.lindex(key, page_num) 106 | pipe = Utils.BOT.redis_pool.pipeline() 107 | m['current_page'] = page_num 108 | pipe.set(f"inf_meta:{interaction.message.id}", json.dumps(m)) 109 | pipe.expire(f"inf_meta:{interaction.message.id}", 60 * 60 * 12) 110 | await pipe.execute() 111 | return page, page_num, count, m['query'], m['fields'].split(' ') 112 | -------------------------------------------------------------------------------- /GearBot/views/PagedText.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gearbot/GearBot/b91c48eafde0ef4612442b33f10e70d2475c5489/GearBot/views/PagedText.py -------------------------------------------------------------------------------- /GearBot/views/Reminder.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | 4 | import disnake 5 | from disnake import Interaction, ButtonStyle 6 | 7 | from Util import MessageUtils, Translator, Utils 8 | from database.DatabaseConnector import Reminder 9 | from views.InfSearch import CallbackButton 10 | 11 | 12 | class ReminderView(disnake.ui.View): 13 | def __init__(self, guild_id, channel_id, reminder, user_id, timeout_callback, message_id, duration): 14 | super().__init__(timeout=30) 15 | self.user_id = user_id 16 | self.timeout_callback = timeout_callback 17 | self.channel_id = channel_id 18 | self.reminder = reminder 19 | self.mid = message_id 20 | self.duration = duration 21 | self.guild_id = guild_id 22 | self.add_item(CallbackButton(label=Translator.translate('remind_option_dm', guild_id), callback=self.dm, 23 | style=ButtonStyle.blurple)) 24 | self.add_item(CallbackButton(label=Translator.translate('remind_option_here', guild_id), callback=self.channel, 25 | style=ButtonStyle.blurple)) 26 | self.add_item( 27 | CallbackButton(label=Translator.translate('cancel', guild_id), callback=self.cancel, style=ButtonStyle.red)) 28 | 29 | async def on_timeout(self) -> None: 30 | await self.timeout_callback() 31 | 32 | async def dm(self, interaction: Interaction): 33 | if await self.execution_check(interaction): 34 | await Reminder.create(user_id=self.user_id, channel_id=self.channel_id, dm=True, 35 | to_remind=self.reminder, 36 | time=time.time() + self.duration, send=datetime.now().timestamp(), 37 | status=1, 38 | guild_id=self.guild_id, message_id=self.mid) 39 | await interaction.response.edit_message(content=MessageUtils.assemble(self.guild_id, "YES", f"reminder_confirmation_dm", duration=Utils.to_pretty_time(self.duration, self.guild_id)), view=None) 40 | self.stop() 41 | 42 | async def channel(self, interaction: Interaction): 43 | if await self.execution_check(interaction): 44 | await Reminder.create(user_id=self.user_id, channel_id=self.channel_id, dm=False, 45 | to_remind=self.reminder, 46 | time=time.time() + self.duration, send=datetime.now().timestamp(), 47 | status=1, 48 | guild_id=self.guild_id, message_id=self.mid) 49 | await interaction.response.edit_message( 50 | content=MessageUtils.assemble(self.guild_id, "YES", f"reminder_confirmation_here", 51 | duration=Utils.to_pretty_time(self.duration, self.guild_id)), view=None) 52 | self.stop() 53 | 54 | async def cancel(self, interaction: Interaction): 55 | if await self.execution_check(interaction): 56 | await interaction.response.edit_message(content=MessageUtils.assemble(self.guild_id, 'NO', 'command_canceled'), view=None) 57 | self.stop() 58 | 59 | async def execution_check(self, interaction: Interaction): 60 | if self.user_id == interaction.user.id: 61 | return True 62 | await self.refuse(interaction) 63 | return False 64 | 65 | async def refuse(self, interaction: Interaction): 66 | interaction.response.send_message(MessageUtils.assemble(self.guild_id, "NO", "wrong_interactor"), 67 | ephemeral=True) 68 | -------------------------------------------------------------------------------- /GearBot/views/SelfRole.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import disnake 4 | from disnake import ButtonStyle, Interaction 5 | from disnake.ui import Button 6 | 7 | from Util import Translator, Configuration, Utils 8 | 9 | 10 | class SelfRoleView(disnake.ui.View): 11 | def __init__(self, guild, page): 12 | super().__init__(timeout=None) 13 | set_buttons(self, guild, page) 14 | self.stop() 15 | 16 | def set_buttons(view: disnake.ui.View, guild, page): 17 | view.children.clear() 18 | roles = [role for role in (guild.get_role(r) for r in Configuration.legacy_get_var(guild.id, "ROLES", "SELF_ROLES")) if 19 | role is not None] 20 | pages = [p for p in Utils.chunks(roles, 20)] 21 | view.pages = len(pages) 22 | if len(pages) == 0: 23 | return 24 | if page > len(pages) or page < 0: 25 | page = 0 26 | p = pages[page] 27 | count = 0 28 | for role in p: 29 | view.add_item(Button(label=role.name, style=ButtonStyle.blurple, custom_id=f"self_role:role:{role.id}")) 30 | count += 1 31 | if len(pages) > 1: 32 | row = math.ceil(count / 5.0) 33 | view.add_item(Button(label=Translator.translate('prev_page', guild.id), style=ButtonStyle.grey, custom_id=f"self_role:page:{page - 1}", disabled=page == 0, row=row)) 34 | view.add_item(Button(label=Translator.translate('next_page', guild.id), style=ButtonStyle.grey, custom_id=f"self_role:page:{page + 1}", disabled=page == len(pages), row=row)) 35 | -------------------------------------------------------------------------------- /GearBot/views/SimplePager.py: -------------------------------------------------------------------------------- 1 | from disnake.ui import View, Button 2 | 3 | from Util import Translator 4 | 5 | 6 | class SimplePagerView(View): 7 | def __init__(self, guild_id, pages, page, t): 8 | super().__init__(timeout=None) 9 | set_components(self, pages, guild_id, page, t) 10 | self.stop() 11 | 12 | 13 | def set_components(view, pages, guild_id, page, t): 14 | if pages > 2: 15 | view.add_item( 16 | Button(label=Translator.translate('first_page', guild_id), disabled=page == 0, custom_id=f"pager:00:{t}")) 17 | view.add_item(Button(label=Translator.translate('prev_page', guild_id), disabled=page == 0, 18 | custom_id=f"pager:{page - 1}:{t}")) 19 | view.add_item(Button(label=Translator.translate('next_page', guild_id), disabled=page == pages - 1, 20 | custom_id=f"pager:{page + 1}:{t}")) 21 | if pages > 2: 22 | view.add_item(Button(label=Translator.translate('last_page', guild_id), disabled=page == pages - 1, 23 | custom_id=f"pager:0{pages - 1}:{t}")) 24 | 25 | def get_parts(pages, page_num, guild_id, t): 26 | if page_num == len(pages): 27 | page_num = 0 28 | view = None 29 | content = pages[page_num] 30 | if len(pages) > 1: 31 | view = SimplePagerView(guild_id, len(pages), page_num, t) 32 | return content, view, page_num 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 AEnterprise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gearbot 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # GearBot (And Services) Security Policy 2 | 3 | ## About 4 | 5 | GearBot helps out a large number of Discord guilds, ranging from small and personal to large and heavily moderated. He is primarily designed to make server moderation easier on server staff, but this means that he usually has higher permissions and abilities that could cause nasty repercussions if misused or exploited. These issues could come from a third-party library issue, bad validation, or just a simple mistake inside the code. With the web dashboard and its associated API being brought to life, its more important then ever to ensure that any possible security bugs are caught fast and resolved even quicker. 6 | 7 | ## Reporting Policy 8 | 9 | While transparency is always a good thing, when another user's data is potentially at risk its usually better to keep things private until the issue has been fixed and the risk eliminated. For this reason, we ask that you do **not** publically detail any security vulnerabilities to anyone except the GearBot core team until the issue has been confirmed to be fixed and deployed. 10 | 11 | - We go by a 30 day disclosure policy. Once that timeframe is up and if for some reason the bug has not been fixed, the reporter holds all rights to disclose the bug publicly with no restrictions. 12 | 13 | - Issues can, and will, be disclosed sooner if they are fixed before then and there is confirmation that no risk remains to any user from the bug. Please keep in mind that it might include some time where we inform people who are running their own instance of the bot and choose to receive notifications will be informed first so they can quietly patch their instances before any information is made public. 14 | 15 | - The reporter holds all rights to the bug they find, no NDAs or other blanket covering will ever occur. All that is required is to wait the required 30 days (unless the team gives you permission to disclose sooner). 16 | 17 | - Please ensure that any testing and reports are made based off the latest version of the code. Updates are frequent, so ensure that you are on the latest commit. 18 | 19 | - Testing should only occur with approval of whoever runs the instance you are testing against. 20 | 21 | - Testing should occur in a way that does not impact or minimizes impact on other users of the same instance. This means no trying to get data of specific guilds without their consent. Use your own server and accounts (or make some) wherever possible. If an issue exposes data of random users without control of it, then that is a different matter altogether. However, targeted attacks without consent should not be attempted. Similarly do not attempt any exploits that might result in downtime or other disruptions in the target instance's ability to continue normal operations without explicit permission from whoever runs the instance. 22 | 23 | - This is a open source project not backed by any company or organization. As such we do not have the funds to provide any sort of monetary compensation. We do have virtual hugs and gratitude (plus our shiny wall of fame, if you're into that thing) 24 | 25 | - Any issue found with libraries GearBot uses that are not maintained by this team does not fall under this policy but the policy of said library, but we would appreciate a heads up if at all possible. 26 | 27 | - If you have *any* questions, concerns, or comments about the policy, including if doing something is OK, feel free to reach out for help. 28 | 29 | 30 | 31 | ## How to report 32 | 33 | - Contact AEnterprise#4693 (userid 106354106196570112) or BlackHoleFox#1527 (userid 105076359188979712) on Discord. You can find us on the [GearBot Discord Server](https://discordapp.com/invite/vddW3D9). 34 | - Alternatively you can report via email at: gearbot@gearbot.rocks 35 | - You will receive a reply confirming we received your report within 48h. If you do not get such confirmation it means the person you tried to contact is unavailable for some reason. Please try one of the other methods instead to make sure we receive your report. 36 | 37 | ## Rules: 38 | 39 | 1. Adhere strictly to the reporting policy outlined above 40 | 2. No brute forcing is permitted. It is unneeded, even for fuzzing. All the code is open source so you can run your own local instance instead for these tasks. 41 | 3. No automated scanning tools are permitted for testing. 42 | 4. If you encounter another user's data or server, do not interact with it or attempt any modifications. Stop and report the issue immediately! 43 | 5. No testing issues on a instance of GearBot you don't own without proper permission. Get permission first or run your own instance. 44 | 6. If you are caught violating any of the above rules, you will be banned from the program for the indefinite future and access to GearBot (the public instance) can be revoked. 45 | 46 | ## Scope and Impact 47 | 48 | ### Out of scope 49 | 50 | - Phishing of any variety 51 | - Denial of Service attacks or DDoS attacks 52 | - Issues on instances *not* hosted by the core team caused by misconfiguration or modifications. 53 | 54 | ### Testing on the public instance 55 | 56 | **Note**: Due to the open source nature of the services, it is always preferable to clone the service locally from the latest master branch for most testing. 57 | 58 | The following domains are in scope and can be tested against (provided the rules outlined above are followed, as well as any other additional restrictions outlined below): 59 | 60 | - gearbot.rocks 61 | 62 | - dash.gearbot.rocks (Do not test any endpoint under /api/admin without explicit permission) 63 | 64 | - animal.gearbot.rocks 65 | 66 | **WARNING**: Anything found outside of these areas and domains is considered out-of-scope and does not fall under the program's safe harbor. There might be instances found under other subdomains but these are usually dev versions with WIP stuff that isn't public or meant to be secured yet (or even online much) 67 | 68 | ### High impact areas 69 | 70 | These are the types of issues that have the highest impact and we consider having the highest risks 71 | 72 | - ### GearBot Discord Bot: 73 | 74 | - Escalation of privileges, running commands that your user shouldn't be able to normally. 75 | 76 | - ### The Dashboard API and its associated frontend: 77 | 78 | - Escalation of privileges, performing actions you shouldn't be authorized for 79 | - Gaining access as another user, getting their oauth token, etc... 80 | - Any way to access another user's data 81 | 82 | - ### GearBot's public facing animal fact API 83 | 84 | - The admin REST interface for modifying the fact lists 85 | -------------------------------------------------------------------------------- /clusterloader.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | if [ -s upgradeRequest ]; then 3 | git pull origin 4 | python3 -m pip install -U -r requirements.txt --user 5 | rm -rf upgradeRequest 6 | fi 7 | SHARDS=2 8 | CLUSTERS=2 9 | COUNT=0 10 | TOTAL_SHARDS=$(($SHARDS * $CLUSTERS)) 11 | LAST=$(($SHARDS-1)) 12 | while [[ $COUNT < $LAST ]]; do 13 | OFFSET=$((SHARDS*$COUNT)) 14 | echo "Starting GearBot cluster $COUNT with $SHARDS shards (offset $OFFSET)" 15 | $(python3 GearBot/GearBot.py --total_shards $TOTAL_SHARDS --num_shards $SHARDS --offset $OFFSET &) 16 | sleep $((5*$SHARDS)) 17 | COUNT=$(($COUNT+1)) 18 | done 19 | $(python3 GearBot/GearBot.py --total_shards $TOTAL_SHARDS --num_shards $SHARDS --offset $OFFSET) -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | !../template.json 3 | !*.example -------------------------------------------------------------------------------- /config/master.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "LOGIN_TOKEN": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", 3 | "BOT_LOG_CHANNEL": 365908831328403456, 4 | "CROWDIN_KEY": null, 5 | "DATABASE": "postgres://gearbot:gearbot@localhost:5432/gearbot", 6 | "APEX_KEY": "", 7 | "REDIS_HOST": "localhost", 8 | "REDIS_PORT": 6379, 9 | "REDIS_SOCKET": "", 10 | "SENTRY_DSN": "", 11 | "EMOJI": {}, 12 | "EMOJI_GUILD": 365498559174410241, 13 | "GUIDES": 0, 14 | "inbox": 0, 15 | "COGS": [ 16 | "Basic", 17 | "Admin", 18 | "Moderation", 19 | "ServerAdmin", 20 | "ModLog", 21 | "CustCommands", 22 | "BCVersionChecker", 23 | "Reload", 24 | "ReactionHandler", 25 | "Censor", 26 | "Infractions", 27 | "Interactions", 28 | "Minecraft", 29 | "DMMessages", 30 | "Reminders", 31 | "Emoji", 32 | "AntiSpam" 33 | ], 34 | "TRANSLATIONS": { 35 | "CHANNEL": 0, 36 | "KEY": "", 37 | "LOGIN": "", 38 | "SOURCE": "DISABLED", 39 | "WEBROOT": "" 40 | }, 41 | "DOCS": true, 42 | "DISABLED_COMMANDS": [], 43 | "DASH_OUTAGE": { 44 | "outage_detection": false, 45 | "max_bot_outage_warnings": 1, 46 | "dash_outage_channel": 999999999, 47 | "dash_outage_pinged_roles": [], 48 | "dash_outage_message": "The Dashboard went down! Please look into it!", 49 | "dash_outage_embed": { 50 | "title": "Dashboard Outage Detected", 51 | "timestamp": "", 52 | "color": "FF0000", 53 | "description": "The Dashboard is suspected to be down, it hasn't responded in over 3 minutes!", 54 | "author": { 55 | "name": "Gearbot Dashboard Monitor" 56 | }, 57 | "fields": [ 58 | { 59 | "name": "Alert Count", 60 | "value": "{warnings_sent}/{MAX_BOT_OUTAGE_WARNINGS}", 61 | "inline": true 62 | } 63 | ] 64 | } 65 | }, 66 | "global_inf_counter": true, 67 | "min_cached_users": 0, 68 | "purge_db": true 69 | } 70 | -------------------------------------------------------------------------------- /docs/pages/01.home/home.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | --- 4 | Keeps the gears turning smoothly -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/01.adding_gearbot/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding GearBot 3 | --- 4 | # Adding the bot 5 | The first step is to add GearBot to the server and prepare a few things so he can work. If you click on the following link you can add him with the permissions he needs. Below is an explanation of why he needs what permissions if you don't want him to have all these permissions. 6 | [Click here to add GearBot](https://discordapp.com/oauth2/authorize?client_id=349977940198555660&scope=bot&permissions=1342565590). 7 | 8 | # Permissions 9 | - Manage roles 10 | - This allows him to add/remove roles from people, this is used for selfroles, muting and the mod role command to add/remove roles from people 11 | - Manage channels 12 | - Needed to create the channel overrides that makes the mute role work, both on current channels and new channels 13 | - Kick members 14 | - Kick people with the kick command 15 | - Ban members 16 | - Ban people with the ban command 17 | - View audit log 18 | - Allows accessing the audit log to convert manual kicks/bans to infractions, as well as enhancing a lot of logging to show who did the action 19 | - Read messages/Send messages/Embed links/Attach files/Read message history/Add reactions/Use external emoji 20 | - Just basic permissions to interact with chat, not all servers give these permissions to the everyone role 21 | - Manage messages 22 | - Allows removing messages with the clean commands and cleaning command trigger messages in some cases 23 | 24 | # Positioning the role 25 | Next up is repositioning the GearBot role that got created when the bot entered the server. You want to move this role (or another role if you give it an additional bots role) to be above all the roles you want him able to add/remove from people (mute role for example) and who's members you want to be able to kick with the bot (if you have cosmetic roles, like a member role, and that is above GearBot's highest role he won't be able to kick/ban them). 26 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/02.configuring_prefix/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuring the prefix 3 | --- 4 | # Prefix 5 | GearBot's default prefix is ``!``, but this is one used by many bots. If you have any other bots that also reply to this, you can give GearBot a new prefix (if you are a lvl 3 (admin) user, see the [permissions guide](../intro_permissions))! 6 | It will also respond to you mentioning him (``@GearBot#7326``) instead of using ``!`` regardless of his configured server prefix. 7 | 8 | Anyways, here's the command, just replace ```` with what you want GearBot to respond to (unless you want GearBot to respond to ````, if so that's fine, don't replace it): 9 | ``` 10 | !configure prefix 11 | ``` 12 | 13 | If you changed GearBots prefix please replace ``!`` with the new prefix in further commands, for this guide all of the commands will be using my default prefix. 14 | 15 | Don't remember GearBot's prefix? No problem, he can tell you what it is if you don't give him a new one: 16 | ``` 17 | @GearBot#7326 configure prefix 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/03.intro_permissions/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Permissions introduction 3 | --- 4 | # Introduction to permissions 5 | At it's core, GearBot uses a very basic permissions system that determines who is able to run what commands. 6 | 7 | ``` 8 | ╔════╦═════════════════╦═══════════════════════════════════════════════════╗ 9 | ║ Nr ║ Name ║ Requirement ║ 10 | ╠════╬═════════════════╬═══════════════════════════════════════════════════╣ 11 | ║ 0 ║ Public ║ Everyone ║ 12 | ║ 1 ║ Trusted ║ People with a trusted role or mod+ ║ 13 | ║ 2 ║ Mod ║ People with ban permissions or admin+ ║ 14 | ║ 3 ║ Admin ║ People with administrator perms or an admin role ║ 15 | ║ 4 ║ Specific people ║ People you added to the whitelist for a command ║ 16 | ║ 5 ║ Server owner ║ The person who owns the server ║ 17 | ║ 6 ║ Disabled ║ Perm level nobody can get, used to disable stuff ║ 18 | ╚════╩═════════════════╩═══════════════════════════════════════════════════╝ 19 | ``` 20 | **NOTE**: Bot owner commands to manage and update the bot fall outside of this permissions system and are handled separately, similarly bot ownership is not part of these permissions checks and has no effect on that. 21 | 22 | For example: all commands in the basic cog have a default permission requirement of 0, allowing anyone to run them. This can be changed with an override (see [Reconfiguring command requirements](../command_requirements) for details later on) to only allow mods, or even nobody to run these commands. 23 | 24 | And similarly you can also lower the requirement for some commands (up to a point), commands like userinfo are by default mod only but can be made public. 25 | 26 | See [the command list](../../commands) for a the default permission requirement per command. 27 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/04.language/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setting a language 3 | --- 4 | # Setting a language 5 | If you prefer you can use GearBot in another language than English. Be warned that not all translations are 100% complete (some are very far from it) and if a string is not available in the language you chose, it will fall back to English. 6 | **NOTE:** This only affects the replies you get from GearBot, not the commands themselves. 7 | You can also help with translating GearBot to your language by joining the Crowdin project [here](https://i18n.gearbot.rocks). More languages can also be added if there is interest and people willing to translate to the language. 8 | 9 | To change the language GearBot replies in: 10 | ``` 11 | !configure language 12 | ``` 13 | 14 | Currently available language codes: 15 | 16 | - ar_SA 17 | - bg_BG 18 | - cs_CZ 19 | - da_DK 20 | - de_DE 21 | - el_GR 22 | - en_AU 23 | - en_PT 24 | - en_US 25 | - es_ES 26 | - fi_FI 27 | - fr_FR 28 | - he_IL 29 | - hr_HR 30 | - hu_HU 31 | - id_ID 32 | - it_IT 33 | - ja_JP 34 | - ko_KR 35 | - lt_LT 36 | - nl_BE 37 | - nl_NL 38 | - no_NO 39 | - pl_PL 40 | - pt_BR 41 | - ro_RO 42 | - ru_RU 43 | - sr_CS 44 | - sv_SE 45 | - tr_TR 46 | - uk_UA 47 | - zh_CN 48 | - zh_TW 49 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/05.roles/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuring roles 3 | --- 4 | # Assigning special roles 5 | **NOTE**: On most servers only the mute role needs to be configured. 6 | 7 | ## Admin roles 8 | If you have any admin roles, for GearBot this means roles that are lvl 3 and should be allowed to run level 3 commands (configure commands by default), but do not have administrator permission enabled on that role. Then you need to mark those roles as admin roles. 9 | 10 | To add a role to the admin role list: 11 | ``` 12 | !configure admin_roles add 13 | ``` 14 | Where ```` is either the full role name (case sensitive), the role ID, or a role mention (not recommended unless you want to summon all your admins at once). 15 | 16 | And if you later want to remove the role from the list again: 17 | ``` 18 | !configure admin_roles remove 19 | ``` 20 | 21 | ## Moderator roles 22 | Very similar, but for level 2 commands: moderation commands. 23 | 24 | If you need to add any roles to this list you might want to rethink your server permissions. While this bot is very reliable and I do my best to achieve 24/7 uptime, small interruptions are always possible for different reasons (Discord outage disconnecting bots?). If your mods do not have ban member permissions during this time, things might end badly if a troll decides to stop by. 25 | 26 | Regardless, the commands are very similar: 27 | ``` 28 | !configure mod_roles add 29 | ``` 30 | and 31 | ``` 32 | !configure mod_roles remove 33 | ``` 34 | 35 | ## Trusted roles 36 | This is nothing GearBot can detect, and not always needed, this is mostly so you can have fun commands that some people like to abuse and use a little too much (cat, dog, coinflip, ...) not public but gated behind a role for only some users. 37 | 38 | Same deal here: 39 | ``` 40 | !configure trusted_roles add 41 | ``` 42 | and: 43 | ``` 44 | !configure trusted_roles remove 45 | ``` 46 | 47 | ## Muted role 48 | And to finish it up: a role that is and works completely different! 49 | 50 | The mute role is added to people to mute them. When you configure the role it adds a permission override on all channels denying it send messages and add reaction permissions. This is also done for new channels created later. 51 | 52 | If someone tries to dodge the mute while one is active (through the bot, manually applying the role won't work for this) the role is just re-added upon joining. 53 | ``` 54 | !configure mute_role 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/06.command_requirements/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reconfiguring command requirements 3 | --- 4 | # Reconfiguring command requirements 5 | **Note:** While this is possible it is not required to do so on all servers, if the defaults work for you, there is no need to add any overrides. 6 | 7 | To determine the permission level required to run a command it looks in the following order, and uses the first one it finds: 8 | 1) a command override for that specific command 9 | 2) a command override for the parent command (recursive) if this is a subcommand (so if you add an override to the `` role`` command, but not to the ``role add`` subcommand, the top first will apply) 10 | 3) a cog override for the cog this command belongs to 11 | 4) default permission requirement for the command (most do not have one, only a few have this) 12 | 5) default permission requirement for the cog this command belongs to 13 | 14 | 15 | ## About cogs 16 | Commands are grouped into cogs, groups of commands if you will. If you look at the [command list](../../commands), the commands are listed there, together with their default command level. 17 | 18 | # Cog overrides 19 | If you want to add a cog override: 20 | ``` 21 | !configure cog_overrides add 22 | ``` 23 | **Note:** Cog names are case sensitive! 24 | 25 | To remove it later: 26 | ``` 27 | !configure cog_overrides remove 28 | ``` 29 | 30 | You can also get a list of all configured cog overrides: 31 | ``` 32 | !configure cog_overrides 33 | ``` 34 | 35 | # Command overrides 36 | If cog overrides are to big for what you want to adjust, you can also adjust it for individual commands (or subcommands if you wrap it in quotation marks): 37 | ``` 38 | !configure command_overrides add 39 | ``` 40 | You can also remove them again: 41 | ``` 42 | !configure command_overrides remove 43 | ``` 44 | And view the list of what you have configured: 45 | ``` 46 | !configure command_overrides 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/07.logging/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuring logging 3 | --- 4 | # Advanced logging 5 | So you want to do more than just logging everything to a single channel? Then this guide is for you. 6 | 7 | GearBot can log the following things (these are also the keys you use in the configure commands): 8 | "MOD_ACTIONS", 9 | "CHANNEL_CHANGES", 10 | "ROLE_CHANGES", 11 | "MISC", 12 | "TRAVEL_LOGS", 13 | "NAME_CHANGES", 14 | "MESSAGE_LOGS", 15 | "VOICE_CHANGES_DETAILED", 16 | "VOICE_CHANGES", 17 | "SPAM_VIOLATION", 18 | "CONFIG_CHANGES", 19 | "FUTURE_LOGS" 20 | - ``RAID_LOGS`` 21 | - ``CENSORED_MESSAGES`` 22 | - ``MOD_ACTIONS`` 23 | - ``CHANNEL_CHANGES`` 24 | - ``ROLE_CHANGES`` 25 | - ``TRAVEL_LOGS`` 26 | - ``NAME_CHANGES`` 27 | - ``MESSAGE_LOGS`` 28 | - ``VOICE_CHANGES`` 29 | - ``VOICE_CHANGES_DETAILED`` 30 | - ``SPAM_VIOLATION`` 31 | - ``CONFIG_CHANGES`` 32 | - ``FUTURE_LOGS`` 33 | - ``MISC`` 34 | 35 | ``FUTURE_LOGS`` is a special one, this one doesn't log anything, it is merely a placeholder. When new logging types are added, having this one configured means they will automatically be added and enabled. 36 | Another special case is "EVERYTHING", this isn't a key on it's own, but when running a command this will get replaced by the full list of available keys. So if you want all of them, you do not have to type them one by one (also means that if you want all but one type, you can add everything, and then just remove the on you don't want). 37 | All keys are case insensitive so you can type them in upper or lower case when using in the commands. 38 | 39 | These commands are also made to be user friendly and help you in figuring out why things do not work. As such things can be a bit verbose and when you try to enable/disable things that are already enabled/disabled it will inform you. It will also let you know if you specified any invalid logging keys (but still process the valid ones). 40 | 41 | ## Adding logging to channels 42 | To add logging to a channel you can simply use the following command: 43 | ``` 44 | !configure logging add 45 | ``` 46 | Types can be a single type, everything, or a list of types. 47 | 48 | ### Some examples 49 | ``` 50 | !configure logging add #mod-logs everything 51 | ``` 52 | 53 | ``` 54 | !configure logging add #mod-logs EDIT_LOGS, NAME_CHANGES, ROLE_CHANGES 55 | ``` 56 | 57 | ## Verifying logging status 58 | If you for some reason are unsure of what exactly is configured to be logged or not, you can use the following commands to help you figure out what is going on. 59 | ``` 60 | !configure logging 61 | ``` 62 | This will show you all the currently configured channels, if all required permissions to log are set correctly and what will be logged. 63 | 64 | ``` 65 | !configure features 66 | ``` 67 | Shows the enabled features, for most logging there is no special feature that needs to be enabled, but things like edit and censor logs are linked to features. 68 | 69 | 70 | ## Removing logging from a channel 71 | If you no longer want some keys to be logged to the channel you can remove them again: 72 | ``` 73 | !configure logging remove 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/08.censoring/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Censoring 3 | --- 4 | # Censoring 5 | 6 | GearBot currently supports 2 types of censoring: 7 | 8 | - invite censoring 9 | - text censoring 10 | 11 | But none of these apply to people who have permission level 2 or higher (mods and admins), level 4 does not count as this only applies to specific commands, not globally. 12 | ## Initial setup 13 | First you need to make sure there is a logging channel that has the ``CENSOR_MESSAGES`` key so that censor actions are logged there, this is a requirement to being able to enable censoring of messages. 14 | 15 | Next up is making sure the feature is enabled, you can do this by running: 16 | ``` 17 | !configure features enable CENSOR_MESSAGES 18 | ``` 19 | 20 | ## Setting up invite censoring 21 | Invite censoring does not kick in until there are servers on the whitelist. If you do not want any external invites to be posted you can just add your server to the whitelist and no others. 22 | 23 | **Note:** All these commmands uses server IDs and do not validate if these are actual, valid server IDs. This is due to that the bot cannot request a server from the API with its ID if the bot is not on that server. 24 | 25 | To add a server to the whitelist: 26 | ``` 27 | !configure allowed_invites add 28 | ``` 29 | Similar story to remove one: 30 | ``` 31 | !configure allowed_invites remove 32 | ``` 33 | And to view the entire list: 34 | ``` 35 | !configure allowed_invites 36 | ``` 37 | 38 | ## Setting up text censoring 39 | Text censoring works with partial text matches and is case insensitive. 40 | 41 | **WARNING:** Be very careful for partial matches as this does not differentiate if it's in the middle of a word or a word on it's own. 42 | 43 | To add something to the censor_list: 44 | ``` 45 | !configure censor_list add 46 | ``` 47 | To remove: 48 | ``` 49 | !configure censor_list remove 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/09.self_roles/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Selfroles 3 | --- 4 | # What are selfroles? 5 | Selfroles are roles that can be self-assigned by a user by using the `!self_roles` command. The `!self_roles` command can be used by everyone by default (The permission level is 0), so only make roles selfroles if you want anyone to be able to add the role to themselves. 6 | 7 | Selfroles can be useful for roles such as: 8 | - Pingable announcement roles 9 | - Language roles 10 | - Giveaway participator roles 11 | - etc. 12 | # How do I add selfroles? 13 | To add or remove selfroles, you'll need a permission level of 3 by default (Administrator permission or Adminstrator role). To add a selfrole, use this command (mention not recommended). 14 | 15 | Role can either be a role ID, a role name (case sensitive), or a role mention, 16 | ``` 17 | !configure self_role add 18 | ``` 19 | Once you've done this, users can add the role to themselves by using the `!self_roles` command. 20 | To remove a selfrole, use this command instead. 21 | ``` 22 | !configure self_role remove 23 | ``` 24 | # How do I use selfroles? 25 | To display a list of the server's selfroles, use the `!self_roles` command. 26 | ``` 27 | !self_roles 28 | ``` 29 | 30 | To add or remove a selfrole to yourself, use the `!self_roles` command with the role name/ID/mention (mention not recommended) argument. 31 | ``` 32 | !self_roles 33 | ``` 34 | Do not confuse the `!self_roles` command with `!roles` command, that shows all the server's roles instead. 35 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/10.custom_commands/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom commands 3 | --- 4 | # What are custom commands? 5 | Custom commands are very basic commands you can make yourself (provided you have permission lvl 2 or higher for these commands). You give GearBot a trigger and a reply. When someone does `!`, GearBot will reply with the reply you gave him. 6 | # How do I create custom commands? 7 | You can create a custom command using the `!commands create` command. You may also use `!commands new` or `!commands add`, which are variations of this command that do the same function. 8 | You will need to provide both a trigger and a reply to create custom commands. 9 | ``` 10 | !commands create 11 | ``` 12 | Please mind the trigger can not have spaces in it. 13 | 14 | Once done this, you may use the command by saying: 15 | ``` 16 | ! 17 | ``` 18 | To change the reply of a command, use this command. 19 | ``` 20 | !commands update 21 | ``` 22 | However, using `!commands create/add/new` instead of `!commands update` will work (requires confirmation), same as using `!commands update` instead of `!commands create/add/new`. 23 | 24 | To remove a command, use: 25 | ``` 26 | !commands remove 27 | ``` 28 | # Who can use custom commands? 29 | Everyone can use them, everyone can also request the list of all custom commands by just executing ``!commands``. 30 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/11.ignoring_channels/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ignoring channels 3 | --- 4 | # Ignored channels 5 | 6 | These features are only intended to be used if you have music bots or so that are spamming your logs with constant channel topic changes. For bots with constant edits please consider adding them as ignored user instead, as this will disable logging and can thus be abused by others to hide what they are doing. 7 | 8 | ## Ignoring channel changes 9 | Some music bots edit a channel topic to match what they are playing. While this is neat, this results in a lot of spam in the modlogs. And if you still want changes logged about other channels, disabling it is a bit tricky. Instead GearBot can just ignore the changes to that particular (set of) channel(s). 10 | 11 | To see all channels currently being ignored for change logs: 12 | ``` 13 | !configure ignored_channels changes list 14 | ``` 15 | 16 | Adding a channel can be done with: 17 | ``` 18 | !configure ignored_channels changes add 19 | ``` 20 | 21 | Removing it from the list again is very similar: 22 | ``` 23 | !configure ignored_channels changes remove 24 | ``` 25 | 26 | # Ignoring channel edit logs 27 | Be VERY careful with ignoring edits from channels completely, anyone can send nasty messages and then edit or remove them after, with no trace in the logs. 28 | But the commands are very similar: 29 | ``` 30 | !configure ignored_channels edits list 31 | !configure ignored_channels edits add 32 | !configure ignored_channels edits remove 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/12.misc/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Misc 3 | --- 4 | # Miscellaneous 5 | This page contains some other miscellaneous settings that do not really fit anywhere else. 6 | 7 | ## Permissions denied message 8 | By default GearBot will tell people when they are not authorized to execute a command, this can be disabled: 9 | ``` 10 | !configure perm_denied_message off 11 | ``` 12 | To enable it again afterwards: 13 | ``` 14 | !configure perm_denied_message on 15 | ``` 16 | 17 | # Timezones 18 | You can configure the timezone used for logging!: 19 | ``` 20 | !configure timezone 21 | ``` 22 | For a full list of timezones see: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones . The input for the bot is listed in the ``TZ database name`` column 23 | 24 | ## DM users warnings 25 | When you warn a user, GearBot can DM them that warning, this behaviour is disabled by default. 26 | To enable this: 27 | ``` 28 | !configure dm_on_warn on 29 | ``` 30 | 31 | If you want to disable it again later: 32 | ``` 33 | !configure dm_on_warn off 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /docs/pages/03.docs/02.setup/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup 3 | --- 4 | In this category you will find guides to help you setup GearBot -------------------------------------------------------------------------------- /docs/pages/03.docs/03.guides/01.archiving/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Archiving 3 | --- 4 | # What is archiving? 5 | Archiving gives you the ability to collect messages by a specific user or in a channel and keep them in a text file format. By default this is available to moderators and above (permissions lvl 2 and higher). 6 | # How can I archive? 7 | First of all, with archiving there are two options: you can either archive messages sent by a user or messages sent in a channel. 8 | When you run the command, the bot sends a text file with all the messages that you specified to archive. The file includes all information such as timestamps, IDs, usernames and messsage contents. This also means the bot needs file upload perms in the channel you use it in (and if you don't want the result archive to be public, don't use it in a public channel). 9 | 10 | # Arguments that can be passed in: 11 | - [user] - A user's messages to archive (can be a mention or an ID) 12 | - [channel] - A channel's messages to archive (can be a mention or an ID) 13 | - [amount] - The amount of messages to archive (up to 5000) 14 | 15 | How to use the command for channels: 16 | ```!archive channel ``` 17 | When archiving a user's messages: 18 | ```!archive user ``` 19 | In both cases the amount is optional (defaults to 100 when omitted), for archiving a channel you can also omit the channel and it will default to doing 100 messages in the current channel. 20 | # Examples: 21 | Archiving a user's messages: 22 | ```!archive user @AEnterprise#4693 ``` 23 | Archiving a channel's messages: 24 | ```!archive channel #test-channel 5000``` 25 | -------------------------------------------------------------------------------- /docs/pages/03.docs/03.guides/02.infractions/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Infractions 3 | --- 4 | # Infractions 5 | 6 | There are several types of infractions: 7 | 8 | - Warn 9 | - Added by the warn command 10 | - Kick 11 | - Added when someone gets kicked (manual or through the `kick` command) 12 | - Ban 13 | - Added when someone gets banned (manual or throught the `ban` command) 14 | - Forced ban 15 | - Added when someone was banned by the ``forceban`` command 16 | - Mute 17 | - Added when someone gets muted with the ``mute`` command 18 | - Mute role is automatically removed again once the mute expires or the ``unmute`` command has been used 19 | - Unban 20 | - Added when a ban is lifted (manual or through the ``unban`` command) 21 | - Unmute 22 | - Added when someone is unmuted before a mute expires 23 | - Tempban 24 | - Added when someone is temp banned 25 | - Ban is automatically lifted when the ban expires (unless you unban+ban or forceban the user again before the time is up) 26 | 27 | 28 | # Finding infractions 29 | To find infractions there is one simple yet powerful command: ``inf search [fields...] [query] [amount]`` 30 | All parameters are optional, if you don't give any you get the last 100 infractions for the guild. 31 | ### Fields 32 | Optional multi param, determines where to look. 33 | Possible values the [] are part of the param, to avoid confusing them with the query: 34 | - [mod] 35 | - [user] 36 | - [reason] 37 | 38 | Multiple values can be passed. If omitted it defaults to ``[mod] [user] [reason]``. 39 | Also see the **Examples** section below. 40 | 41 | ### Query 42 | Optional param, can be any userID, mention, full username (only if the user is on the server) or plain text. 43 | It will use this query to search the fields specified with the fields param. Also see **Examples** below 44 | 45 | ### Amount 46 | The simplest param of them all, how many infractions you want to see. If the last thing in your command is a number between 1 and 500 (inclusive), this will be used as max amount of infractions to show. Defaults to 100. 47 | If you instead want to do a reason search for something that is or ends with a number between 1 and 25, simply add another number as amount after it. 48 | 49 | ### Examples 50 | The section where it all becomes clear! 51 | 52 | Show the last 100 infractions from the server: 53 | ``` 54 | !inf search 55 | ``` 56 | 57 | Show the last 3 infractions from the server: 58 | ``` 59 | !inf search 3 60 | ``` 61 | 62 | Show the last 100 infractions related to a user (so either as mod, user, or where the ID is part of the reason): 63 | ``` 64 | !inf search 106354106196570112 65 | ``` 66 | 67 | Show the last 5 infractions handed out by a specific mod: 68 | ``` 69 | !inf search [mod] 106354106196570112 5 70 | ``` 71 | 72 | Show the last 500 infractions of a specific user, and any infractions where you used that user's ID in the reason: 73 | ``` 74 | !inf search [user] [reason] 106354106196570112 500 75 | ``` 76 | 77 | Show the last 10 bans for ban evasion: 78 | ``` 79 | !inf search ban evasion 10 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/pages/03.docs/03.guides/doc.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gearbot/GearBot/b91c48eafde0ef4612442b33f10e70d2475c5489/docs/pages/03.docs/03.guides/doc.md -------------------------------------------------------------------------------- /docs/pages/03.docs/04.supporting/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Supporting 3 | --- 4 | # Supporting GearBot 5 | 6 | GearBot is free and open source, and will always be like that. 7 | 8 | That being said I do host him myself and that has costs, if you want to help with those to make things easier, get some (minor, I don't like paywalls) perks you can check out my Patreon: [https://www.patreon.com/AEnterprise](https://www.patreon.com/AEnterprise). 9 | 10 | You can also make a donation on PayPal if you want, you can get the supporter role on discord as well if you put your discord tag in the notes. 11 |
12 | 13 | 14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /docs/pages/03.docs/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Docs 3 | --- 4 | 5 | Ah, the documentation, this is where everything makes sense (or that's the goal at least, do let me know if parts are unclear or missing so I can improve them). 6 | 7 | Navigation is pretty straightforward, pages are listed on the left, point and click to go there! 8 | 9 | -------------------------------------------------------------------------------- /docs/theme/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.1.0 2 | ## 04/01/2019 3 | 4 | 1. [](#new) 5 | * ChangeLog started... 6 | -------------------------------------------------------------------------------- /docs/theme/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 AEnterprise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/theme/README.md: -------------------------------------------------------------------------------- 1 | # GearBot Theme 2 | 3 | The **GearBot** Theme is for [Grav CMS](http://github.com/getgrav/grav). This README.md file should be modified to describe the features, installation, configuration, and general usage of this theme. 4 | 5 | ## Description 6 | 7 | Theme for the GearBot site 8 | -------------------------------------------------------------------------------- /docs/theme/blueprints.yaml: -------------------------------------------------------------------------------- 1 | name: GearBot 2 | version: 0.1.0 3 | description: Theme for the GearBot site 4 | icon: rebel 5 | author: 6 | name: AEnterprise 7 | email: aenterprise@aenterprise.info 8 | homepage: https://gearbot.rocks 9 | demo: https://gearbot.rocks 10 | keywords: grav, theme, etc 11 | bugs: https://github.com/aenterprise/gearbot/issues 12 | readme: https://github.com/aenterprise/gearbot/blob/master/README.md 13 | license: MIT 14 | 15 | form: 16 | validation: loose 17 | fields: 18 | dropdown.enabled: 19 | type: toggle 20 | label: Dropdown in Menu 21 | highlight: 1 22 | default: 1 23 | options: 24 | 1: PLUGIN_ADMIN.ENABLED 25 | 0: PLUGIN_ADMIN.DISABLED 26 | validate: 27 | type: bool 28 | -------------------------------------------------------------------------------- /docs/theme/gearbot.php: -------------------------------------------------------------------------------- 1 | 9 | {% include 'partials/doc_navigation.html.twig' %} 10 |
11 | {{ page.content }} 12 |
13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /docs/theme/templates/error.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'partials/base.html.twig' %} 2 | 3 | {% block content %} 4 |
5 |

Error!

6 | {{ page.content }} 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /docs/theme/templates/home.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'partials/base.html.twig' %} 2 | 3 | 4 | {% block stylesheets %} 5 | {% do assets.addCss('theme://compiled-css/home.css', 97) %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | 11 |
12 |

GearBot

13 | {{ page.content }} 14 |
15 | 16 | 17 | 18 |
19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /docs/theme/templates/partials/base.html.twig: -------------------------------------------------------------------------------- 1 | {% set theme_config = attribute(config.themes, config.system.pages.theme) %} 2 | 3 | 4 | 5 | {% block head %} 6 | 7 | {% if header.title %}{{ header.title|e('html') }} | {% endif %}{{ site.title|e('html') }} 8 | 9 | 10 | 11 | {% include 'partials/metadata.html.twig' %} 12 | 13 | 14 | 15 | 16 | {% do assets.addCss('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css', 99) %} 17 | {% do assets.addCss('theme://compiled-css/base.css', 98) %} 18 | {% block stylesheets %} 19 | 20 | {% endblock %} 21 | {{ assets.css() }} 22 | 23 | {% block javascripts %} 24 | {% endblock %} 25 | {{ assets.js() }} 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endblock head %} 33 | 34 | 35 |
36 | {% block header %} 37 |
38 | 39 |
40 | 41 |
42 | {% block header_navigation %} 43 | 46 | {% endblock %} 47 | 48 |

{{ page.header.title }}

49 |
50 | {% endblock %} 51 | {% block body %} 52 |
53 |
54 | {% block content %}{% endblock %} 55 |
56 |
57 | {% endblock %} 58 | 59 | {% block footer %} 60 | 62 | {% endblock %} 63 | {% include 'partials/langswitcher.html.twig' %} 64 |
65 | {% block bottom %} 66 | {{ assets.js('bottom') }} 67 | {% endblock %} 68 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/theme/templates/partials/doc_navigation.html.twig: -------------------------------------------------------------------------------- 1 | {% macro loop(page) %} 2 | {% for p in page.children.visible %} 3 | {% set current_page = (p.active or p.activeChild) ? 'active' : '' %} 4 | {% if p.children.visible.count > 0 %} 5 |
  • 6 | 7 | {% if p.header.icon %}{% endif %} 8 | {{ p.menu }} 9 | 10 |
      11 | {{ _self.loop(p) }} 12 |
    13 |
  • 14 | {% else %} 15 |
  • 16 | 17 | {% if p.header.icon %}{% endif %} 18 | {{ p.menu }} 19 | 20 |
  • 21 | {% endif %} 22 | {% endfor %} 23 | {% endmacro %} 24 | -------------------------------------------------------------------------------- /docs/theme/templates/partials/langswitcher.html.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/theme/templates/partials/metadata.html.twig: -------------------------------------------------------------------------------- 1 | {% for meta in page.metadata %} 2 | 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /docs/theme/templates/partials/navigation.html.twig: -------------------------------------------------------------------------------- 1 | {% macro loop(page) %} 2 | {% for p in page.children.visible %} 3 | {% set current_page = (p.active or p.activeChild) ? 'active' : '' %} 4 | {% if p.children.visible.count > 0 %} 5 |
  • 6 | 7 | {% if p.header.icon %}{% endif %} 8 | {{ p.menu }} 9 | 10 | 11 |
  • 12 | {% else %} 13 |
  • 14 | 15 | {% if p.header.icon %}{% endif %} 16 | {{ p.menu }} 17 | 18 |
  • 19 | {% endif %} 20 | {% endfor %} 21 | {% endmacro %} 22 | 23 | 46 | 47 | -------------------------------------------------------------------------------- /docs/theme/thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gearbot/GearBot/b91c48eafde0ef4612442b33f10e70d2475c5489/docs/theme/thumbnail.jpg -------------------------------------------------------------------------------- /lang/.gitignore: -------------------------------------------------------------------------------- 1 | !bot.json 2 | !shared.json 3 | *.json -------------------------------------------------------------------------------- /lang/bot.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /lang/shared.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /migration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gearbot/GearBot/b91c48eafde0ef4612442b33f10e70d2475c5489/migration/__init__.py -------------------------------------------------------------------------------- /migration/add_config_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS config 2 | ( 3 | guild_id bigint NOT NULL, 4 | config json NOT NULL, 5 | PRIMARY KEY (guild_id) 6 | ); 7 | -------------------------------------------------------------------------------- /migration/infractions.py: -------------------------------------------------------------------------------- 1 | from playhouse.migrate import MySQLMigrator, MySQLDatabase, TimestampField, BooleanField, migrate 2 | 3 | from Util import Configuration 4 | from database.DatabaseConnector import Infraction 5 | 6 | connection = MySQLDatabase(Configuration.get_master_var("DATABASE_NAME"), 7 | user=Configuration.get_master_var("DATABASE_USER"), 8 | password=Configuration.get_master_var("DATABASE_PASS"), 9 | host=Configuration.get_master_var("DATABASE_HOST"), 10 | port=Configuration.get_master_var("DATABASE_PORT"), use_unicode=True, charset="utf8mb4") 11 | 12 | #make connection 13 | migrator = MySQLMigrator(connection) 14 | 15 | #run everything in a transaction so we don't turn the database into 💩 if something goes wrong 16 | with connection.atomic(): 17 | #fields to add 18 | end = TimestampField(null=True) 19 | active = BooleanField(default=True) 20 | #add fields 21 | migrate( 22 | migrator.add_column("infraction", "end", end), 23 | migrator.add_column("infraction", "active", active), 24 | migrator.rename_column("infraction", "timestamp", "start"), 25 | ) 26 | 27 | #some infractions are not active anymore 28 | Infraction.update(active=False).where((Infraction.type == "Mute") | (Infraction.type == "Kick")).execute() 29 | -------------------------------------------------------------------------------- /migration/rowboat.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | import time 4 | from datetime import datetime 5 | 6 | from peewee import PrimaryKeyField, Model, BigIntegerField, CharField, TimestampField, BooleanField, MySQLDatabase, \ 7 | IntegrityError 8 | 9 | 10 | def fetch_from_disk(filename, alternative=None): 11 | try: 12 | with open(f"{filename}.json") as file: 13 | return json.load(file) 14 | except FileNotFoundError: 15 | if alternative is not None: 16 | fetch_from_disk(alternative) 17 | return dict() 18 | 19 | 20 | c = fetch_from_disk("../config/master") 21 | 22 | connection = MySQLDatabase(c["DATABASE_NAME"], 23 | user=c["DATABASE_USER"], 24 | password=c["DATABASE_PASS"], 25 | host=c["DATABASE_HOST"], 26 | port=c["DATABASE_PORT"], use_unicode=True, charset="utf8mb4") 27 | 28 | 29 | class Infraction(Model): 30 | id = PrimaryKeyField() 31 | guild_id = BigIntegerField() 32 | user_id = BigIntegerField() 33 | mod_id = BigIntegerField() 34 | type = CharField(max_length=10, collation="utf8mb4_general_ci") 35 | reason = CharField(max_length=2000, collation="utf8mb4_general_ci") 36 | start = TimestampField() 37 | end = TimestampField(null=True) 38 | active = BooleanField(default=True) 39 | 40 | class Meta: 41 | database = connection 42 | 43 | 44 | infractions = fetch_from_disk("infractions")["infractions"] 45 | print(f"Importing {len(infractions)}, this can take a while...") 46 | done = 0 47 | dupes = 0 48 | moved = 0 49 | last_reported = -1 50 | t = time.time() 51 | for i in infractions: 52 | # extract info 53 | start = datetime.strptime(i["created_at"], "%a, %d %b %Y %H:%M:%S %Z") 54 | end = datetime.strptime(i["expires_at"], "%a, %d %b %Y %H:%M:%S %Z") if i["expires_at"] is not None else None 55 | reason = i["reason"] if i["reason"] is not None else "No reason specified" 56 | active = i["active"] == 'true' 57 | guild_id = int(i["guild"]["id"]) 58 | user_id = int(i["user"]["id"]) 59 | mod_id = int(i["actor"]["id"]) 60 | type = i["type"]["name"] 61 | try: 62 | # attempt insertion 63 | Infraction.create(id=i["id"], guild_id=guild_id, user_id=user_id, mod_id=mod_id, type=type, reason=reason, 64 | start=start, end=end, active=active) 65 | except IntegrityError: 66 | # failed, retrieve infraction occupying this 67 | infraction = Infraction.get_by_id(i["id"]) 68 | 69 | # make sure it's not a dupe 70 | if infraction.guild_id != guild_id or infraction.user_id != user_id or infraction.mod_id != mod_id or type != infraction.type \ 71 | or infraction.reason != reason or infraction.start != start or infraction.end != end or infraction.active != active: 72 | Infraction.create(guild_id=guild_id, user_id=user_id, mod_id=mod_id, type=type, reason=reason, 73 | start=start, end=end, active=active) 74 | moved += 1 75 | else: 76 | dupes += 1 77 | 78 | done += 1 79 | percent = math.floor((done / len(infractions)) * 100) 80 | if percent > last_reported: 81 | last_reported = percent 82 | print(f"{percent}% done, {len(infractions) - done} to go") 83 | 84 | #reporting 85 | print(f"Initial import completed in {round((time.time() - t))} seconds") 86 | print(f"Imported: {done - dupes}") 87 | print(f"Dupes: {dupes}") 88 | print(f"{moved} reports had to be assigned a new ID") 89 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/AEnterprise/disnake@083fa3c156c60d965df107bcba2dd11c7de79a66 2 | tortoise-orm==0.16.17 3 | Pillow==7.2.0 4 | requests==2.24.0 5 | aioredis==1.3.1 6 | sentry-sdk==0.19.2 7 | pytz==2020.4 8 | pyseeyou==1.0.1 9 | prometheus_client==0.8.0 10 | asyncpg==0.27.0 11 | emoji==0.6.0 12 | kubernetes==12.0.1 -------------------------------------------------------------------------------- /template.json: -------------------------------------------------------------------------------- 1 | { 2 | "VERSION": 37, 3 | "GENERAL": { 4 | "LANG": "en_US", 5 | "PERM_DENIED_MESSAGE": true, 6 | "PREFIX": "!", 7 | "TIMESTAMPS": true, 8 | "NEW_USER_THRESHOLD": 86400, 9 | "TIMEZONE": "Europe/Brussels", 10 | "GHOST_MESSAGE_THRESHOLD": 10, 11 | "GHOST_PING_THRESHOLD": 20, 12 | "BOT_DELETED_STILL_GHOSTS": true 13 | }, 14 | "ROLES": { 15 | "SELF_ROLES": [], 16 | "ROLE_LIST": [], 17 | "ROLE_LIST_MODE": true, 18 | "MUTE_ROLE": 0 19 | }, 20 | "LOG_CHANNELS": {}, 21 | "MESSAGE_LOGS": { 22 | "ENABLED": false, 23 | "IGNORED_CHANNELS_CHANGES": [], 24 | "IGNORED_CHANNELS_OTHER": [], 25 | "IGNORED_USERS": [], 26 | "EMBED": false, 27 | "MESSAGE_ID": false 28 | }, 29 | "CENSORING": { 30 | "ENABLED": false, 31 | "ALLOW_TRUSTED_BYPASS": false, 32 | "ALLOW_TRUSTED_CENSOR_BYPASS": false, 33 | "WORD_CENSORLIST": [], 34 | "TOKEN_CENSORLIST": [], 35 | "ALLOWED_INVITE_LIST": [], 36 | "DOMAIN_LIST_ALLOWED": false, 37 | "DOMAIN_LIST": [], 38 | "FULL_MESSAGE_LIST": [], 39 | "CENSOR_EMOJI_ONLY_MESSAGES": false, 40 | "IGNORE_IDS": false, 41 | "MAX_LIST_LENGTH": 400 42 | }, 43 | "FLAGGING": { 44 | "WORD_LIST": [], 45 | "TOKEN_LIST": [], 46 | "TRUSTED_BYPASS": false, 47 | "IGNORE_IDS": false, 48 | "MAX_LIST_LENGTH": 400 49 | }, 50 | "INFRACTIONS": { 51 | "DM_ON_WARN": false, 52 | "DM_ON_UNMUTE": false, 53 | "DM_ON_MUTE": false, 54 | "DM_ON_KICK": false, 55 | "DM_ON_BAN": false, 56 | "DM_ON_TEMPBAN": false, 57 | "WARNING": null, 58 | "UNMUTE": null, 59 | "MUTE": null, 60 | "KICK": null, 61 | "BAN": null, 62 | "TEMPBAN": null 63 | }, 64 | "PERM_OVERRIDES": {}, 65 | "RAID_HANDLING": { 66 | "ENABLED": false, 67 | "HANDLERS": [], 68 | "INVITE": "" 69 | }, 70 | "ANTI_SPAM": { 71 | "CLEAN": false, 72 | "ENABLED": false, 73 | "EXEMPT_ROLES": [], 74 | "EXEMPT_USERS": [], 75 | "BUCKETS": [] 76 | }, 77 | "DASH_SECURITY": { 78 | "ACCESS": 2, 79 | "INFRACTION": 2, 80 | "VIEW_CONFIG": 2, 81 | "ALTER_CONFIG": 3 82 | }, 83 | "PERMISSIONS": { 84 | "LVL4_ROLES": [], 85 | "LVL4_USERS": [], 86 | "ADMIN_ROLES": [], 87 | "ADMIN_USERS": [], 88 | "MOD_ROLES": [], 89 | "MOD_USERS": [], 90 | "TRUSTED_ROLES": [], 91 | "TRUSTED_USERS": [] 92 | }, 93 | "SERVER_LINKS": [], 94 | "CUSTOM_COMMANDS": { 95 | "ROLES": [], 96 | "ROLE_REQUIRED": false, 97 | "CHANNELS": [], 98 | "CHANNELS_IGNORED": true, 99 | "MOD_BYPASS": true 100 | } 101 | } 102 | --------------------------------------------------------------------------------