diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1605a06 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.github +__pycache__/ +*.py[cod] +*.db +*.sqlite3 +.env +.venv/ +venv/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b053145 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +DISCORD_BOT_TOKEN=your_discord_bot_token +NOTIFICATION_CHANNEL=123456789012345678 + +# Backward-compatible sequential format +CHANNEL_1=123456789012345678 +CHANNEL_2=234567890123456789 + +# Optional alternative to CHANNEL_X +# MONITORED_CHANNEL_IDS=123456789012345678,234567890123456789 + +RICH_PRESENCE=Katchau! +ACTIVITY=listening +BOT_LOCALE=en_US +SHOW_LOG=false +MENTION_EVERYONE_ON_EMPTY_CHANNEL=true +DATABASE_PATH=bot_database.db diff --git a/.env.exemple b/.env.exemple deleted file mode 100644 index c4ec087..0000000 --- a/.env.exemple +++ /dev/null @@ -1,8 +0,0 @@ -DISCORD_BOT_TOKEN=Your_Token_Here # This is the token of your bot, you can get it on the discord developer portal -CHANNEL_1=Your_Channel_ID_Here # The number of channels is limited to 10, you can increment here as you want, just place the new channel ID below with the same format -CHANNEL_2=Your_Channel_ID_Here -CHANNEL_3=Your_Channel_ID_Here -NOTIFICATION_CHANNEL=Your_NOTIFICATION_Channel_ID_Here # This is the channel where the bot will send the notifications -RICH_PRESENCE=Katchau! # This is the rich presence of the bot (example: "Katchau!") as a string -ACTIVITY=listening # This is the activity of the bot, you can use "game", "listening", "watching" or "streaming") -BOT_LOCALE=en_US #(Optional) used to translate the bot messages. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9466d5b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Validate Python syntax + run: python -m py_compile bot.py + + - name: Validate locale JSON + run: | + python -m json.tool locales/en_US.json > /dev/null + python -m json.tool locales/pt_BR.json > /dev/null + + - name: Smoke test imports + run: python -c "import bot, discord; print(discord.__version__)" + + docker: + runs-on: ubuntu-latest + needs: validate + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: false + load: false + tags: everyone-should-know:ci + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 3f53646..0000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Docker Image CI - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Build the Docker image - run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) diff --git a/.github/workflows/pr-release.yml b/.github/workflows/pr-release.yml deleted file mode 100644 index 00486ca..0000000 --- a/.github/workflows/pr-release.yml +++ /dev/null @@ -1,102 +0,0 @@ -# Thanks to https://github.com/museofficial/muse/blob/master/.github/workflows/pr-release.yml for this amazing workflow. - -name: Release snapshot of PR -on: - workflow_run: - workflows: ["Build snapshot of PR"] - types: - - completed - -env: - REGISTRY_IMAGE: ghcr.io/seijinmark/esk - -jobs: - release-and-comment: - name: Release snapshot and comment in PR - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Buildx - uses: docker/setup-buildx-action@v1 - - - name: Download images - uses: actions/download-artifact@v4 - with: - path: /tmp/images - pattern: image-linux-* - merge-multiple: true - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GH_PAT }} - - - name: Load image - shell: bash - run: | - docker load -i /tmp/images/image-linux-amd64.tar - docker load -i /tmp/images/image-linux-arm64.tar - - name: Download SHA - uses: actions/download-artifact@v4 - with: - path: /tmp/SHA - pattern: sha - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GH_PAT }} - - - name: Read SHA - shell: bash - run: | - echo "SHA=$(cat /tmp/SHA/sha/sha.txt | tr -d '\n')" >> $GITHUB_ENV - - name: Push images - run: | - docker push ${{ env.REGISTRY_IMAGE }}:${{ env.SHA }}-linux-amd64 - docker push ${{ env.REGISTRY_IMAGE }}:${{ env.SHA }}-linux-arm64 - - name: Download Docker metadata - uses: actions/download-artifact@v4 - with: - path: /tmp/metadata - pattern: metadata - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GH_PAT }} - - - name: Read the metadata.json file - id: metadata_reader - uses: juliangruber/read-file-action@v1.0.0 - with: - path: /tmp/metadata/metadata/metadata.json - - - name: Download PR number - uses: actions/download-artifact@v4 - with: - path: /tmp/pull_request_number - pattern: pull_request_number - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GH_PAT }} - - - name: Read PR number - shell: bash - run: | - echo "PR_NUMBER=$(cat /tmp/pull_request_number/pull_request_number/pull_request_number.txt | tr -d '\n')" >> $GITHUB_ENV - - name: Create manifest list and push - run: | - docker buildx imagetools create $(cat /tmp/metadata/metadata/metadata.json | jq -cr '.tags | map("-t " + .) | join(" ")') ${{ env.REGISTRY_IMAGE }}:${{ env.SHA }}-linux-amd64 ${{ env.REGISTRY_IMAGE }}:${{ env.SHA }}-linux-arm64 - - name: Create comment - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: "pr-release" - number: ${{ env.PR_NUMBER }} - GITHUB_TOKEN: ${{ secrets.GH_PAT }} - message: | - #### :package: :robot: A new release has been made for this pull request. - To play around with this PR, pull `${{ env.REGISTRY_IMAGE }}:pr-${{ env.PR_NUMBER }}`. - Images are available for x86_64 and ARM64. - > Latest commit: ${{ env.SHA }} \ No newline at end of file diff --git a/.github/workflows/pr-snapshot.yml b/.github/workflows/pr-snapshot.yml deleted file mode 100644 index 0077b72..0000000 --- a/.github/workflows/pr-snapshot.yml +++ /dev/null @@ -1,103 +0,0 @@ -# Thanks to https://github.com/museofficial/muse/blob/master/.github/workflows/pr-snapshot.yml for this amazing workflow. - -name: Build snapshot of PR - -on: pull_request - -env: - REGISTRY_IMAGE: ghcr.io/seijinmark/esk - -jobs: - build: - name: Build snapshot - strategy: - matrix: - runner-platform: - - ubuntu-latest - - namespace-profile-default-arm64 - include: - - runner-platform: ubuntu-latest - build-arch: linux/amd64 - - runner-platform: namespace-profile-default-arm64 - build-arch: linux/arm64 - runs-on: ${{ matrix.runner-platform }} - steps: - - name: Prepare - run: | - platform=${{ matrix.build-arch }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_IMAGE }} - tags: type=ref,event=pr - - - name: Set up Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get current time - uses: josStorer/get-current-time@v2 - id: current-time - - - name: Build - id: build - uses: docker/build-push-action@v6 - with: - outputs: type=docker,dest=/tmp/image-${{ env.PLATFORM_PAIR }}.tar - platforms: ${{ matrix.build-arch }} - tags: | - ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}-${{ env.PLATFORM_PAIR }} - build-args: | - COMMIT_HASH=${{ github.sha }} - BUILD_DATE=${{ steps.current-time.outputs.time }} - - - name: Export Docker meta output - shell: bash - run: echo $DOCKER_METADATA_OUTPUT_JSON > /tmp/metadata.json - - - name: Upload metadata - uses: actions/upload-artifact@v4 - with: - name: metadata - path: /tmp/metadata.json - overwrite: true - - - name: Export SHA - run: | - echo "${{ github.sha }}" > /tmp/sha.txt - - - name: Upload SHA - uses: actions/upload-artifact@v4 - with: - name: sha - path: /tmp/sha.txt - overwrite: true - - - name: Upload image - uses: actions/upload-artifact@v4 - with: - name: image-${{ env.PLATFORM_PAIR }} - path: /tmp/image-${{ env.PLATFORM_PAIR }}.tar - if-no-files-found: error - retention-days: 1 - - - name: Save PR number in artifact - shell: bash - env: - PR_NUMBER: ${{ github.event.number }} - run: echo $PR_NUMBER > /tmp/pull_request_number.txt - - name: Upload PR number - uses: actions/upload-artifact@v4 - with: - name: pull_request_number - path: /tmp/pull_request_number.txt - overwrite: true \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 009b5e8..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,149 +0,0 @@ -# Thanks to https://github.com/museofficial/muse/blob/master/.github/workflows/publish.yml for this amazing workflow. - -name: Make release & publish Docker image - -on: - push: - tags: - - 'v*' - -env: - REGISTRY_IMAGE: ghcr.io/seijinmark/esk - -jobs: - publish: - strategy: - matrix: - runner-platform: - - ubuntu-latest - - namespace-profile-default-arm64 - include: - - runner-platform: ubuntu-latest - build-arch: linux/amd64 - tagged-platform: amd64 - - runner-platform: namespace-profile-default-arm64 - build-arch: linux/arm64 - tagged-platform: arm64 - runs-on: ${{ matrix.runner-platform }} - permissions: - contents: read - packages: write - attestations: write - id-token: write - steps: - - name: Set up Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get current time - uses: josStorer/get-current-time@v2 - id: current-time - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v6 - with: - push: true - tags: | - seijinmark/esk:${{ github.sha }}-${{ matrix.tagged-platform }} - ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}-${{ matrix.tagged-platform }} - platforms: ${{ matrix.build-arch }} - build-args: | - COMMIT_HASH=${{ github.sha }} - BUILD_DATE=${{ steps.current-time.outputs.time }} - - combine: - name: Combine platform tags - runs-on: ubuntu-latest - needs: publish - permissions: - contents: read - packages: write - attestations: write - id-token: write - steps: - - uses: actions/checkout@v1 - - - name: Set up Buildx - uses: docker/setup-buildx-action@v1 - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get tags (Docker Hub) - id: get-tags-dockerhub - uses: Surgo/docker-smart-tag-action@v1 - with: - docker_image: seijinmark/esk - - - name: Get tags (ghcr.io) - id: get-tags-ghcr - uses: Surgo/docker-smart-tag-action@v1 - with: - docker_image: ${{ env.REGISTRY_IMAGE }} - - - name: Combine tags (Docker Hub) - run: docker buildx imagetools create $(echo '${{ steps.get-tags-dockerhub.outputs.tag }}' | tr "," "\0" | xargs -0 printf -- '-t %s ') 'seijinmark/esk:${{ github.sha }}-arm64' 'seijinmark/esk:${{ github.sha }}-amd64' - - - name: Combine tags (GitHub Container Registry) - run: docker buildx imagetools create $(echo '${{ steps.get-tags-ghcr.outputs.tag }}' | tr "," "\0" | xargs -0 printf -- '-t %s ') '${{ env.REGISTRY_IMAGE }}:${{ github.sha }}-arm64' '${{ env.REGISTRY_IMAGE }}:${{ github.sha }}-amd64' - - - name: Update Docker Hub description - uses: peter-evans/dockerhub-description@v2.4.3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - repository: seijinmark/esk - - release: - name: Create GitHub release - runs-on: ubuntu-latest - needs: combine - steps: - - uses: actions/checkout@v2 - - - name: Get version from tag - id: tag_name - run: | - echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v} - shell: bash - - - name: Get Changelog Entry - id: changelog_reader - uses: mindsers/changelog-reader-action@v2 - with: - version: ${{ steps.tag_name.outputs.current_version }} - path: ./CHANGELOG.md - - - name: Create/update release - uses: ncipollo/release-action@v1 - with: - tag: v${{ steps.changelog_reader.outputs.version }} - name: Release v${{ steps.changelog_reader.outputs.version }} - body: ${{ steps.changelog_reader.outputs.changes }} - prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} - draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} - allowUpdates: true - token: ${{ secrets.GH_PAT }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6b6efbf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,60 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +env: + REGISTRY_IMAGE: ghcr.io/${{ github.repository_owner }}/esk + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=ref,event=tag + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Create GitHub release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 148b291..c773701 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ venv/ env/ .env +.venv/ # Log files *.log @@ -16,8 +17,10 @@ env/ *.db *.sqlite3 -# Environment variables file -.env +# Tooling caches +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ # IDE and editor files .vscode/ @@ -27,4 +30,4 @@ env/ # System files .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db diff --git a/Dockerfile b/Dockerfile index 2472fe6..9983ebe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,15 @@ -# Use the Python base image -FROM python:3.9-slim +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 -# Set the working directory inside the container WORKDIR /app -# Copy the necessary files to the bot container -COPY bot.py /app -COPY requirements.txt /app -COPY locales /app/locales +COPY requirements.txt ./ +RUN python -m pip install --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt -# Install dependencies -RUN pip install --no-cache-dir -r requirements.txt +COPY bot.py ./ +COPY locales ./locales -# Default command to run the bot when the container starts -CMD ["python", "bot.py"] \ No newline at end of file +CMD ["python", "bot.py"] diff --git a/README.md b/README.md index 1ba1161..93a8066 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,109 @@

- Everyone Should Know Logo + Everyone Should Know Logo

-
- -
-Versão em Português - -# Bot de Notificação de Chamadas do Discord - -Este é um bot do Discord projetado para notificar os usuários quando alguém entra em um canal de voz. Ele também mantém um registro de quantas vezes cada usuário entrou em chamadas. - -## Funcionalidades +# Everyone Should Know -- Envia uma mensagem quando alguém entra em um canal de voz vazio -- Mantém um registro de quantas vezes cada usuário entrou em chamadas -- Exibe um placar dos usuários que mais entraram em chamadas -- Permite ativar/desativar as notificações de entrada em chamadas +Discord bot that notifies a text channel when someone joins monitored voice channels and keeps a lightweight SQLite history of voice activity. -## Comandos +## What changed in this refresh -- `/leaders`: Mostra quantas vezes cada usuário entrou em chamadas (top 10) -- `/toggle`: Ativa/desativa a funcionalidade de enviar mensagens quando alguém entra em uma chamada -- `/help`: Exibe a lista de comandos disponíveis +- Migrated from the legacy `discord-py-slash-command` stack to modern `discord.py` application commands +- Fixed monitored-channel filtering so `CHANNEL_X` is actually respected +- Added `/stats` for per-member activity lookup +- Cleaned locale files and setup docs +- Simplified dependencies and modernized the Docker image -## Configuração +## Features -1. Clone este repositório -2. Crie um arquivo `.env` baseado no `.env.exemple` e preencha com suas informações: - - `DISCORD_BOT_TOKEN`: Token do seu bot do Discord - - `CHANNEL_X`: IDs dos canais que o bot deve monitorar (até 10 canais) - - `NOTIFICATION_CHANNEL`: ID do canal onde as notificações serão enviadas -3. Instale as dependências: `pip install -r requirements.txt` -4. Execute o bot: `python bot.py` +- Sends a notification when someone joins a monitored voice channel +- Optionally pings `@everyone` when the first person enters an empty monitored channel +- Tracks monitored voice joins in SQLite +- Persists the `/toggle` state across restarts +- Exposes `/leaders`, `/stats`, `/toggle`, and `/help` +- Supports English and Brazilian Portuguese locales -## Uso com Docker +## Commands -1. Construa a imagem Docker: `docker build -t discord-call-bot .` -2. Execute o contêiner: `docker run -d --env-file .env discord-call-bot` +- `/leaders`: shows the top members by monitored voice joins +- `/stats [member]`: shows stats for you or another member +- `/toggle`: enables or disables notifications +- `/help`: shows the command list -## Contribuições +`/toggle` requires the `Manage Server` permission. +Moving between monitored voice channels does not increase the leaderboard. -Contribuições são bem-vindas! Sinta-se à vontade para abrir issues ou enviar pull requests. +## Setup -## Licença +1. Create a Discord application and bot in the Discord Developer Portal. +2. Enable the `Server Members Intent` in the bot settings. +3. Invite the bot to your server with the `applications.commands`, `Send Messages`, and `View Channels` permissions. +4. Copy `.env.example` to `.env`. +5. Fill in at least these values: + - `DISCORD_BOT_TOKEN` + - `NOTIFICATION_CHANNEL` + - `CHANNEL_1` or `MONITORED_CHANNEL_IDS` +6. Install dependencies: -Este projeto está licenciado sob a Licença MIT. A Licença MIT é uma licença de software permissiva que permite o uso, cópia, modificação e distribuição do software, desde que o aviso de direitos autorais e a permissão sejam incluídos em todas as cópias ou partes substanciais do software. Esta licença não oferece garantias e os autores ou detentores dos direitos autorais não são responsáveis por quaisquer reivindicações, danos ou outras responsabilidades decorrentes do uso do software. +```bash +pip install -r requirements.txt +``` -
+7. Run the bot: ---- +```bash +python bot.py +``` -
+## Environment variables -# Everyone Should Know +| Variable | Required | Description | +| --- | --- | --- | +| `DISCORD_BOT_TOKEN` | Yes | Bot token from Discord | +| `NOTIFICATION_CHANNEL` | Yes | Text channel that receives notifications | +| `CHANNEL_1` ... `CHANNEL_10` | Yes* | Backward-compatible monitored voice channel IDs | +| `MONITORED_CHANNEL_IDS` | No | Comma-separated alternative to `CHANNEL_X` | +| `RICH_PRESENCE` | No | Presence text shown on the bot profile | +| `ACTIVITY` | No | `playing`, `listening`, `watching`, `streaming`, or `competing` | +| `BOT_LOCALE` | No | Locale file name, for example `en_US` or `pt_BR` | +| `SHOW_LOG` | No | Enables info logging when `true` | +| `MENTION_EVERYONE_ON_EMPTY_CHANNEL` | No | Controls `@everyone` when the first user joins | +| `DATABASE_PATH` | No | SQLite file path, defaults to `bot_database.db` | -This Discord bot is designed to notify users when someone enters a voice channel. It also keeps a record of how many times each user has entered calls. +\* You must define at least one monitored voice channel either through `CHANNEL_X` or `MONITORED_CHANNEL_IDS`. -## Features +## Docker -- Sends a message when someone enters an empty voice channel -- Keeps a record of how many times each user has entered calls -- Displays a leaderboard of users who have entered calls the most -- Allows enabling/disabling call entry notifications +Build and run: -## Commands +```bash +docker compose up -d --build +``` -- `/leaders`: Shows how many times each user has entered calls (top 10) -- `/toggle`: Enables/disables the functionality of sending messages when someone enters a call -- `/help`: Displays the list of available commands +The compose file mounts `bot_database.db` so voice statistics survive container recreation. -## Setup +## Notes -1. Clone this repository -2. Create a `.env` file based on `.env.exemple` and fill it with your information: - - `DISCORD_BOT_TOKEN`: Your Discord bot token - - `CHANNEL_X`: IDs of the channels the bot should monitor (up to 10 channels) - - `NOTIFICATION_CHANNEL`: ID of the channel where notifications will be sent -3. Install dependencies: `pip install -r requirements.txt` -4. Run the bot: `python bot.py` +- Slash commands can take a minute to appear globally after the first startup. +- Existing databases continue to work. User names are refreshed automatically as members interact again. +- Notification on/off state is stored in SQLite. +- CI and release now use only standard GitHub-hosted runners and GHCR publishing. -## Using with Docker +## Portugues -1. Build the Docker image: `docker build -t discord-call-bot .` -2. Run the container: `docker run -d --env-file .env discord-call-bot` +Bot para Discord que avisa em um canal de texto quando alguem entra em canais de voz monitorados e salva um historico simples em SQLite. -## Contributions +### Funcionalidades -Contributions are welcome! Feel free to open issues or submit pull requests. +- Envia notificacoes quando alguem entra em um canal de voz monitorado +- Pode marcar `@everyone` quando a primeira pessoa entra em uma call vazia +- Registra entradas em canais monitorados +- Mantem o estado do `/toggle` mesmo apos reiniciar +- Possui comandos `/leaders`, `/stats`, `/toggle` e `/help` -## License +### Configuracao rapida -This project is licensed under the MIT License. The MIT License is a permissive software license that allows for use, copying, modification, and distribution of the software, provided that the copyright notice and permission are included in all copies or substantial portions of the software. This license comes with no warranties, and the authors or copyright holders are not liable for any claims, damages, or other liabilities arising from the use of the software. \ No newline at end of file +1. Copie `.env.example` para `.env`. +2. Preencha `DISCORD_BOT_TOKEN`, `NOTIFICATION_CHANNEL` e pelo menos um `CHANNEL_X`. +3. Rode `pip install -r requirements.txt`. +4. Rode `python bot.py`. diff --git a/bot.py b/bot.py index b8a7575..cd67c73 100644 --- a/bot.py +++ b/bot.py @@ -1,170 +1,447 @@ -import discord -from discord.ext import commands -from discord_slash import SlashCommand -from datetime import datetime, timedelta -import operator +from __future__ import annotations + +import logging import os +import sqlite3 import json +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path + +import discord +from discord import app_commands +from discord.ext import commands from dotenv import load_dotenv -import sqlite3 -# Load environment variables -load_dotenv() -intents = discord.Intents.default() -intents.voice_states = True -intents.members = True +BASE_DIR = Path(__file__).resolve().parent +LOCALES_DIR = BASE_DIR / "locales" -client = commands.Bot(command_prefix=None, intents=intents) -slash = SlashCommand(client) -# Load locales -def load_locale(lang=None): - if not lang: - lang = os.getenv('BOT_LOCALE', 'en_US') - +def parse_channel_id(value: str, env_name: str) -> int: try: - with open(f'locales/{lang}.json', 'r', encoding='utf-8') as f: - return json.load(f) + return int(value) + except ValueError as exc: + raise ValueError(f"{env_name} must be a valid Discord channel ID.") from exc + + +def str_to_bool(value: str | None, default: bool = False) -> bool: + if value is None: + return default + return value.strip().lower() in {"1", "true", "yes", "on"} + + +def parse_channel_ids() -> set[int]: + channel_ids: set[int] = set() + + for index in range(1, 11): + value = os.getenv(f"CHANNEL_{index}") + if not value: + continue + channel_ids.add(parse_channel_id(value, f"CHANNEL_{index}")) + + extra_channels = os.getenv("MONITORED_CHANNEL_IDS", "") + for raw_channel_id in extra_channels.split(","): + candidate = raw_channel_id.strip() + if candidate: + channel_ids.add(parse_channel_id(candidate, "MONITORED_CHANNEL_IDS")) + + return channel_ids + + +def load_locale(language_code: str) -> dict[str, str]: + locale_path = LOCALES_DIR / f"{language_code}.json" + fallback_path = LOCALES_DIR / "en_US.json" + + try: + return json.loads(locale_path.read_text(encoding="utf-8")) except FileNotFoundError: - # Fallback to English if locale file not found - with open('locales/en_US.json', 'r', encoding='utf-8') as f: - return json.load(f) - -# Load locale from environment variable -locale = load_locale() - -# Connect to SQLite database -conn = sqlite3.connect('bot_database.db') -cursor = conn.cursor() - -# Create users table if it doesn't exist -cursor.execute(''' - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - name TEXT, - entry_count INTEGER DEFAULT 0, - last_call_time TIMESTAMP - ) -''') - -conn.commit() - -# Dictionary of messages per channel -channel_messages = {} -for i in range(1, 11): # Limit of 10 channels - channel_env = f'CHANNEL_{i}' - channel_id = os.getenv(channel_env) - if channel_id: - channel_messages[channel_id] = locale["user_in_channel"].format(member="{member.name}", channel="{channel_name}") - else: - break # Stop the loop if there are no more defined channels - -# Variable to control the state of sending messages when someone enters a call -send_message_enabled = True - -# Check if logs should be displayed -show_log = os.getenv('SHOW_LOG', 'false').lower() == 'true' - -@client.event -async def on_ready(): - rich_presence = os.getenv('RICH_PRESENCE', 'Katchau!') - activity = os.getenv('ACTIVITY', 'playing').lower() - activity_type = discord.ActivityType.playing - if activity == 'listening': - activity_type = discord.ActivityType.listening - elif activity == 'watching': - activity_type = discord.ActivityType.watching - elif activity == 'streaming': - activity_type = discord.ActivityType.streaming - await client.change_presence(status=discord.Status.online, activity=discord.Activity(name=rich_presence, type=activity_type)) - print('Bot online!') - print(f'Connected as {client.user.name}') - print('------') - -@client.event -async def on_voice_state_update(member, before, after): - global send_message_enabled - - if not send_message_enabled: - return + return json.loads(fallback_path.read_text(encoding="utf-8")) + + +@dataclass(slots=True) +class Settings: + token: str + notification_channel_id: int + monitored_channel_ids: set[int] + locale_code: str + rich_presence: str + activity_type: str + show_log: bool + mention_everyone_on_empty_channel: bool + database_path: Path + + @classmethod + def from_env(cls) -> "Settings": + load_dotenv() + + token = os.getenv("DISCORD_BOT_TOKEN") + notification_channel = os.getenv("NOTIFICATION_CHANNEL") + + if not token: + raise ValueError( + "DISCORD_BOT_TOKEN is required. Create a .env file before starting the bot." + ) + + if not notification_channel: + raise ValueError( + "NOTIFICATION_CHANNEL is required. Set the channel that will receive notifications." + ) + + monitored_channel_ids = parse_channel_ids() + if not monitored_channel_ids: + raise ValueError( + "At least one monitored voice channel is required. " + "Set CHANNEL_1 or MONITORED_CHANNEL_IDS in the .env file." + ) + + return cls( + token=token, + notification_channel_id=parse_channel_id( + notification_channel, "NOTIFICATION_CHANNEL" + ), + monitored_channel_ids=monitored_channel_ids, + locale_code=os.getenv("BOT_LOCALE", "en_US"), + rich_presence=os.getenv("RICH_PRESENCE", "Katchau!"), + activity_type=os.getenv("ACTIVITY", "playing").lower(), + show_log=str_to_bool(os.getenv("SHOW_LOG"), default=False), + mention_everyone_on_empty_channel=str_to_bool( + os.getenv("MENTION_EVERYONE_ON_EMPTY_CHANNEL"), default=True + ), + database_path=BASE_DIR / os.getenv("DATABASE_PATH", "bot_database.db"), + ) + + +class StatsRepository: + def __init__(self, database_path: Path) -> None: + self.connection = sqlite3.connect(database_path) + self.connection.row_factory = sqlite3.Row + self._ensure_schema() + + def _ensure_schema(self) -> None: + with self.connection: + self.connection.execute( + """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + entry_count INTEGER NOT NULL DEFAULT 0, + last_call_time TEXT + ) + """ + ) + self.connection.execute( + """ + CREATE TABLE IF NOT EXISTS bot_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """ + ) + + def record_entry(self, member: discord.Member) -> None: + now = datetime.now(UTC).isoformat(timespec="seconds") + with self.connection: + self.connection.execute( + """ + INSERT INTO users (id, name, entry_count, last_call_time) + VALUES (?, ?, 1, ?) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + entry_count = users.entry_count + 1, + last_call_time = excluded.last_call_time + """, + (member.id, member.display_name, now), + ) + + def get_leaderboard(self, limit: int = 10) -> list[sqlite3.Row]: + cursor = self.connection.execute( + """ + SELECT id, name, entry_count, last_call_time + FROM users + ORDER BY entry_count DESC, last_call_time DESC + LIMIT ? + """, + (limit,), + ) + return cursor.fetchall() + + def get_user_stats(self, user_id: int) -> sqlite3.Row | None: + cursor = self.connection.execute( + """ + SELECT id, name, entry_count, last_call_time + FROM users + WHERE id = ? + """, + (user_id,), + ) + return cursor.fetchone() + + def get_notifications_enabled(self, default: bool = True) -> bool: + cursor = self.connection.execute( + """ + SELECT value + FROM bot_settings + WHERE key = 'notifications_enabled' + """ + ) + row = cursor.fetchone() + if row is None: + return default + return str_to_bool(row["value"], default=default) + + def set_notifications_enabled(self, enabled: bool) -> None: + with self.connection: + self.connection.execute( + """ + INSERT INTO bot_settings (key, value) + VALUES ('notifications_enabled', ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value + """, + ("true" if enabled else "false",), + ) + + def close(self) -> None: + self.connection.close() + + +class EveryoneShouldKnowBot(commands.Bot): + def __init__(self, settings: Settings, locale: dict[str, str], repository: StatsRepository): + intents = discord.Intents.default() + intents.voice_states = True + intents.members = True + + super().__init__(command_prefix=commands.when_mentioned, intents=intents) - if before.channel == after.channel: - return # Ignore updates that don't involve channel changes + self.settings = settings + self.locale = locale + self.repository = repository + self.send_message_enabled = repository.get_notifications_enabled(default=True) + self.logger = logging.getLogger("esk") - notification_channel_id = os.getenv('NOTIFICATION_CHANNEL') - if not notification_channel_id: + async def setup_hook(self) -> None: + self.tree.add_command(leaders) + self.tree.add_command(toggle_notifications) + self.tree.add_command(help_command) + self.tree.add_command(stats) + await self.tree.sync() + + async def close(self) -> None: + self.repository.close() + await super().close() + + async def on_ready(self) -> None: + await self.change_presence( + status=discord.Status.online, + activity=self._build_activity(), + ) + + self.logger.info("Bot online as %s", self.user) + self.logger.info("Monitoring %s voice channel(s)", len(self.settings.monitored_channel_ids)) + self.logger.info("Notifications enabled: %s", self.send_message_enabled) + + async def on_voice_state_update( + self, + member: discord.Member, + before: discord.VoiceState, + after: discord.VoiceState, + ) -> None: + if member.bot or not self.send_message_enabled: + return + + if before.channel == after.channel: + return + + before_channel = before.channel + after_channel = after.channel + monitors = self.settings.monitored_channel_ids + + was_monitored = before_channel is not None and before_channel.id in monitors + is_monitored = after_channel is not None and after_channel.id in monitors + + if not was_monitored and not is_monitored: + return + + notification_channel = self.get_channel(self.settings.notification_channel_id) + if not isinstance(notification_channel, discord.abc.Messageable): + self.logger.warning( + "Notification channel %s is not available.", + self.settings.notification_channel_id, + ) + return + + joined_monitored_from_outside = is_monitored and not was_monitored + if joined_monitored_from_outside: + self.repository.record_entry(member) + + if is_monitored and after_channel is not None: + if was_monitored and before_channel is not None: + await notification_channel.send( + self.locale["user_moved_to_channel"].format( + member=member.display_name, + old_channel=before_channel.name, + new_channel=after_channel.name, + ) + ) + elif len(after_channel.members) == 1: + await notification_channel.send( + self.locale["user_joined_call_everyone"].format(member=member.mention) + if self.settings.mention_everyone_on_empty_channel + else self.locale["user_joined_call"].format(member=member.mention) + ) + else: + await notification_channel.send( + self.locale["user_in_channel"].format( + member=member.display_name, + channel=after_channel.name, + ) + ) + return + + if was_monitored and before_channel is not None: + await notification_channel.send( + self.locale["user_left_channel"].format( + member=member.display_name, + channel=before_channel.name, + ) + ) + + def _build_activity(self) -> discord.BaseActivity: + activity_map: dict[str, discord.ActivityType] = { + "playing": discord.ActivityType.playing, + "listening": discord.ActivityType.listening, + "watching": discord.ActivityType.watching, + "streaming": discord.ActivityType.streaming, + "competing": discord.ActivityType.competing, + } + + activity_type = activity_map.get(self.settings.activity_type, discord.ActivityType.playing) + if activity_type is discord.ActivityType.playing: + return discord.Game(name=self.settings.rich_presence) + + return discord.Activity(name=self.settings.rich_presence, type=activity_type) + + +def command_channel_scope(bot: EveryoneShouldKnowBot) -> str: + channel_count = len(bot.settings.monitored_channel_ids) + return bot.locale["stats_scope"].format(count=channel_count) + + +@app_commands.command( + name="leaders", + description="Show the ranking of users who joined monitored voice channels the most.", +) +async def leaders(interaction: discord.Interaction) -> None: + bot = interaction.client + assert isinstance(bot, EveryoneShouldKnowBot) + + leaderboard = bot.repository.get_leaderboard() + + if not leaderboard: + await interaction.response.send_message(bot.locale["leaderboard_empty"], ephemeral=True) return - - channel = client.get_channel(int(notification_channel_id)) - if not channel: + + lines = [bot.locale["leaderboard_title"], command_channel_scope(bot), ""] + for position, row in enumerate(leaderboard, start=1): + lines.append( + bot.locale["leaderboard_entry"].format( + position=position, + name=row["name"], + count=row["entry_count"], + ) + ) + + await interaction.response.send_message("\n".join(lines)) + + +@app_commands.command( + name="toggle", + description="Enable or disable call notifications for the monitored channels.", +) +@app_commands.default_permissions(manage_guild=True) +async def toggle_notifications(interaction: discord.Interaction) -> None: + bot = interaction.client + assert isinstance(bot, EveryoneShouldKnowBot) + + if interaction.guild and not interaction.user.guild_permissions.manage_guild: + await interaction.response.send_message( + bot.locale["missing_manage_guild_permission"], + ephemeral=True, + ) return - # User joined a voice channel - if after.channel: - if len(after.channel.members) == 1: - # First person in the channel - mark with @everyone - await channel.send(locale["user_joined_call"].format(member=member.mention)) - - # Update entry count and last call time in the database - cursor.execute(''' - INSERT OR REPLACE INTO users (id, name, entry_count, last_call_time) - VALUES (?, ?, COALESCE((SELECT entry_count FROM users WHERE id = ?) + 1, 1), ?) - ''', (member.id, member.name, member.id, datetime.now())) - conn.commit() - - if show_log: - print(f"{member.name} entered a channel.") - else: - # Other people joining - just show channel info - await channel.send(locale["user_in_channel"].format(member=member.name, channel=after.channel.name)) - - # User left a voice channel - if before.channel and not after.channel: - await channel.send(locale["user_left_channel"].format(member=member.name, channel=before.channel.name)) - - # User moved between channels - if before.channel and after.channel and before.channel != after.channel: - await channel.send(locale["user_moved_to_channel"].format( - member=member.name, - old_channel=before.channel.name, - new_channel=after.channel.name - )) - - if show_log: - print(f"{member.name} was moved from {before.channel.name} to {after.channel.name}") - -@slash.slash(name="leaders", description="Shows how many times each user entered calls.") -async def leaders(ctx): - cursor.execute('SELECT id, name, entry_count FROM users ORDER BY entry_count DESC LIMIT 10') - leaderboard = cursor.fetchall() - leaderboard_text = locale["leaderboard_title"] + "\n" - for idx, (user_id, name, count) in enumerate(leaderboard, start=1): - leaderboard_text += locale["leaderboard_entry"].format(position=idx, name=name, count=count) + "\n" - await ctx.send(content=leaderboard_text) - if show_log: - print('Leaderboard command executed.') - -@slash.slash(name="toggle", description="Turns on/off the functionality of sending a message when someone enters a call.") -async def toggle_message(ctx): - global send_message_enabled - send_message_enabled = not send_message_enabled - status = locale["toggle_enabled"] if send_message_enabled else locale["toggle_disabled"] - await ctx.send(content=status) - -@slash.slash(name="help", description="Get a list of commands.") -async def help(ctx): - commands = [ - locale["help_leaders"], - locale["help_toggle"], - locale["help_help"] + bot.send_message_enabled = not bot.send_message_enabled + bot.repository.set_notifications_enabled(bot.send_message_enabled) + status_key = "toggle_enabled" if bot.send_message_enabled else "toggle_disabled" + await interaction.response.send_message(bot.locale[status_key], ephemeral=True) + + +@app_commands.command( + name="help", + description="Show the available slash commands and what each one does.", +) +async def help_command(interaction: discord.Interaction) -> None: + bot = interaction.client + assert isinstance(bot, EveryoneShouldKnowBot) + + commands_list = [ + bot.locale["help_leaders"], + bot.locale["help_stats"], + bot.locale["help_toggle"], + bot.locale["help_help"], ] - command_list = "\n".join(commands) - await ctx.send(content=locale["help_title"].format(author=ctx.author.name) + "\n\n" + command_list) + body = "\n".join(commands_list) + message = bot.locale["help_title"].format(author=interaction.user.display_name) + await interaction.response.send_message(f"{message}\n\n{body}", ephemeral=True) + + +@app_commands.command( + name="stats", + description="Show your call stats or the stats for another member.", +) +@app_commands.describe(member="Optional member to inspect") +async def stats( + interaction: discord.Interaction, + member: discord.Member | None = None, +) -> None: + bot = interaction.client + assert isinstance(bot, EveryoneShouldKnowBot) + + target = member or interaction.user + row = bot.repository.get_user_stats(target.id) + if row is None: + await interaction.response.send_message( + bot.locale["stats_empty"].format(member=target.display_name), + ephemeral=True, + ) + return + + last_call_time = row["last_call_time"] or bot.locale["stats_never"] + message = bot.locale["stats_message"].format( + member=row["name"], + count=row["entry_count"], + last_call_time=last_call_time, + ) + await interaction.response.send_message(message, ephemeral=True) + + +def configure_logging(enabled: bool) -> None: + level = logging.INFO if enabled else logging.WARNING + logging.basicConfig( + level=level, + format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", + ) + + +def main() -> None: + settings = Settings.from_env() + configure_logging(settings.show_log) + locale = load_locale(settings.locale_code) + repository = StatsRepository(settings.database_path) + bot = EveryoneShouldKnowBot(settings, locale, repository) + bot.run(settings.token, log_handler=None) -# Load the bot token from environment variables -TOKEN = os.getenv('DISCORD_BOT_TOKEN') -if not TOKEN: - raise ValueError("Bot token not found. Make sure to set the DISCORD_BOT_TOKEN environment variable.") -client.run(TOKEN) +if __name__ == "__main__": + main() diff --git a/docker-compose.yml b/docker-compose.yml index af9bdcc..1a7e635 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,3 +6,5 @@ services: restart: unless-stopped env_file: - .env + volumes: + - ./bot_database.db:/app/bot_database.db diff --git a/locales/README.md b/locales/README.md index 3afce4a..87000dd 100644 --- a/locales/README.md +++ b/locales/README.md @@ -1,27 +1,33 @@ ## Locales -By default, the bot will use the English locale, and have the portuguese locale available as an alternative. +By default, the bot uses the English locale and ships with Brazilian Portuguese as an alternative. -If you want to use a different locale, you can create a new file in the `locales` folder with the name of the locale you want to use, and then set the `BOT_LOCALE` environment variable to the name of the locale you want to use. +To add another locale, create a new JSON file in the `locales` folder and set `BOT_LOCALE` to that file name without changing the extension. -The locale file must be a JSON file with the following structure as an example: +Use `en_US.json` as the source of truth. Your custom locale should keep the same keys: ```json { - "user_joined_call": "{member} joined the call! @everyone", + "user_joined_call": "{member} joined the call.", + "user_joined_call_everyone": "{member} joined the call! @everyone", "user_in_channel": "{member} joined {channel}", "user_left_channel": "{member} left {channel}", - "user_moved_to_channel": "{member} left {old_channel} and went to {new_channel}", - "leaderboard_title": "Entry Leaderboard:", - "leaderboard_entry": "{position}. {name}: {count} times", - "toggle_enabled": "Functionality to send a message when someone enters a call is now enabled.", - "toggle_disabled": "Functionality to send a message when someone enters a call is now disabled.", - "help_title": "Hello, {author}! Here's the list of available commands:", - "help_leaders": "/leaders - Shows how many times each user entered calls.", - "help_toggle": "/toggle - Turns on/off the functionality of sending a message when someone enters a call.", - "help_help": "/help - Get a list of commands." + "user_moved_to_channel": "{member} moved from {old_channel} to {new_channel}", + "leaderboard_title": "Voice Channel Leaderboard", + "leaderboard_entry": "{position}. {name}: {count} joins", + "leaderboard_empty": "There is no activity recorded yet.", + "toggle_enabled": "Call notifications are now enabled.", + "toggle_disabled": "Call notifications are now disabled.", + "missing_manage_guild_permission": "You need the Manage Server permission to use this command.", + "help_title": "Hello, {author}. Here are the available commands:", + "help_leaders": "/leaders - Shows the ranking of members with the most joins.", + "help_stats": "/stats - Shows your stats or another member's stats.", + "help_toggle": "/toggle - Enables or disables call notifications.", + "help_help": "/help - Shows this command list.", + "stats_scope": "Tracking {count} monitored voice channel(s).", + "stats_message": "{member} joined monitored voice channels {count} time(s). Last activity: {last_call_time}.", + "stats_empty": "No call activity recorded yet for {member}.", + "stats_never": "never" } ``` - -[NOTE] Keep an eye on the `en_US.json` file, as it could be updated in the future and you might need to update your locale file. \ No newline at end of file diff --git a/locales/en_US.json b/locales/en_US.json index 70c8a6a..049d083 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -1,14 +1,22 @@ { - "user_joined_call": "{member} joined the call! @everyone", + "user_joined_call": "{member} joined the call.", + "user_joined_call_everyone": "{member} joined the call! @everyone", "user_in_channel": "{member} joined {channel}", "user_left_channel": "{member} left {channel}", - "user_moved_to_channel": "{member} left {old_channel} and went to {new_channel}", - "leaderboard_title": "Entry Leaderboard:", - "leaderboard_entry": "{position}. {name}: {count} times", - "toggle_enabled": "Functionality to send a message when someone enters a call is now enabled.", - "toggle_disabled": "Functionality to send a message when someone enters a call is now disabled.", - "help_title": "Hello, {author}! Here's the list of available commands:", - "help_leaders": "/leaders - Shows how many times each user entered calls.", - "help_toggle": "/toggle - Turns on/off the functionality of sending a message when someone enters a call.", - "help_help": "/help - Get a list of commands." + "user_moved_to_channel": "{member} moved from {old_channel} to {new_channel}", + "leaderboard_title": "Voice Channel Leaderboard", + "leaderboard_entry": "{position}. {name}: {count} joins", + "leaderboard_empty": "There is no activity recorded yet.", + "toggle_enabled": "Call notifications are now enabled.", + "toggle_disabled": "Call notifications are now disabled.", + "missing_manage_guild_permission": "You need the Manage Server permission to use this command.", + "help_title": "Hello, {author}. Here are the available commands:", + "help_leaders": "/leaders - Shows the ranking of members with the most joins.", + "help_stats": "/stats - Shows your stats or another member's stats.", + "help_toggle": "/toggle - Enables or disables call notifications.", + "help_help": "/help - Shows this command list.", + "stats_scope": "Tracking {count} monitored voice channel(s).", + "stats_message": "{member} joined monitored voice channels {count} time(s). Last activity: {last_call_time}.", + "stats_empty": "No call activity recorded yet for {member}.", + "stats_never": "never" } diff --git a/locales/pt_BR.json b/locales/pt_BR.json index 067d37a..4ee1c7e 100644 --- a/locales/pt_BR.json +++ b/locales/pt_BR.json @@ -1,14 +1,22 @@ { - "user_joined_call": "{member} entrou na call! @everyone", + "user_joined_call": "{member} entrou na call.", + "user_joined_call_everyone": "{member} entrou na call! @everyone", "user_in_channel": "{member} entrou em {channel}", "user_left_channel": "{member} saiu de {channel}", "user_moved_to_channel": "{member} saiu de {old_channel} e foi para {new_channel}", - "leaderboard_title": "Ranking de Entradas:", - "leaderboard_entry": "{position}. {name}: {count} vezes", - "toggle_enabled": "Funcionalidade de enviar mensagem quando alguém entra na call foi ativada.", - "toggle_disabled": "Funcionalidade de enviar mensagem quando alguém entra na call foi desativada.", - "help_title": "Olá, {author}! Aqui está a lista de comandos disponíveis:", - "help_leaders": "/leaders - Mostra quantas vezes cada usuário entrou em calls.", - "help_toggle": "/toggle - Ativa/desativa a funcionalidade de enviar mensagem quando alguém entra na call.", - "help_help": "/help - Obtém a lista de comandos." + "leaderboard_title": "Ranking de Entradas", + "leaderboard_entry": "{position}. {name}: {count} entradas", + "leaderboard_empty": "Ainda nao existe atividade registrada.", + "toggle_enabled": "As notificacoes de call foram ativadas.", + "toggle_disabled": "As notificacoes de call foram desativadas.", + "missing_manage_guild_permission": "Voce precisa da permissao Gerenciar Servidor para usar este comando.", + "help_title": "Ola, {author}. Estes sao os comandos disponiveis:", + "help_leaders": "/leaders - Mostra o ranking de membros com mais entradas.", + "help_stats": "/stats - Mostra suas estatisticas ou as de outro membro.", + "help_toggle": "/toggle - Ativa ou desativa as notificacoes de call.", + "help_help": "/help - Mostra esta lista de comandos.", + "stats_scope": "Monitorando {count} canal(is) de voz.", + "stats_message": "{member} entrou em canais monitorados {count} vez(es). Ultima atividade: {last_call_time}.", + "stats_empty": "Ainda nao existe atividade registrada para {member}.", + "stats_never": "nunca" } diff --git a/requirements.txt b/requirements.txt index dd039e0..b89da03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,2 @@ -discord.py==1.7.3 -pypresence==4.2.0 -discord-py-slash-command==1.0.7 -python-dotenv==0.21.0 +discord.py>=2.5,<3.0 +python-dotenv>=1.1,<2.0