From 139430c2f886ed15a1786e6d9cbb9947d944848e Mon Sep 17 00:00:00 2001 From: Future Date: Sun, 7 Jul 2024 16:59:47 +0200 Subject: [PATCH] init --- .github/workflows/docker-publish.yml | 47 ++++++ .github/workflows/lint.yml | 32 ++++ Dockerfile | 13 ++ README.md | 2 + aw.py | 231 +++++++++++++++++++++++++++ patterns.json | 18 +++ requirements.txt | 3 + 7 files changed, 346 insertions(+) create mode 100644 .github/workflows/docker-publish.yml create mode 100644 .github/workflows/lint.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 aw.py create mode 100644 patterns.json create mode 100644 requirements.txt diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..e7348ea --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,47 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + docker: + name: Create Docker Image + runs-on: ubuntu-latest + if: github.ref_type == 'tag' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.4.0 + + - name: Log in to DockerHub + uses: docker/login-action@v3.2.0 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - id: meta + uses: docker/metadata-action@v5.5.1 + with: + images: | + alterware/aw-bot + tags: | + ${{ github.ref_name }} + latest + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6.3.0 + with: + context: . + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..8292c53 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,32 @@ +name: Lint + +on: [push, pull_request] + +env: + PIP_ROOT_USER_ACTION: "ignore" + +jobs: + lint: + name: Lint Python code + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install pip + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + + - name: Upgrade pip + run: | + python3 -m pip install --upgrade pip + pip --version + + - name: Install dependencies + run: | + pip install -r requirements.txt + + - name: Run flake8 + run: | + flake8 . diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fb87c9a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:alpine + +WORKDIR /aw-bot + +COPY . /aw-bot + +RUN pip install --no-cache-dir -r requirements.txt + +COPY patterns.json /aw-bot + +ENV BOT_TOKEN="" + +CMD ["python", "aw.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8239a4 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# AlterWare's Discord Bot +This repo contains the AlterWare Bot's source code. It is written in Python using discord.py diff --git a/aw.py b/aw.py new file mode 100644 index 0000000..bc30e89 --- /dev/null +++ b/aw.py @@ -0,0 +1,231 @@ +import datetime +import json +import os +import re +from typing import Literal + +import discord +import requests +from discord import app_commands +from discord.ext import commands, tasks + +GUILD_ID = 1110531063161299074 +BOT_LOG = 1112049391482703873 + +# Define the channel IDs where auto responds are allowed +ALLOWED_CHANNELS = [ + 1110531063744303138, + 1112048063448617142, + 1145458108190163014, + 1145456435518525611, + 1145469136919613551, + 1145459788151537804, + 1145469106133401682, + 1117540484085194833, +] +GENERAL_CHANNEL = 1110531063744303138 + +# Load existing patterns from file +try: + with open("patterns.json", "r") as f: + patterns = json.load(f) +except FileNotFoundError: + patterns = [] + +bot = commands.Bot(command_prefix="!", intents=discord.Intents.all()) +tree = bot.tree + + +def fetch_api_data(): + response = requests.get("https://api.getserve.rs/v1/servers/alterware") + if response.status_code == 200: + return response.json() + return {} + + +async def fetch_game_stats(game: str): + url = f"https://api.getserve.rs/v1/servers/alterware/{game}" + response = requests.get(url) + if response.status_code == 200: + return response.json() + else: + return None + + +async def compile_stats(): + games = ["iw4", "s1", "iw6"] + stats_message = "**Stats for all games:**\n" + for game in games: + data = await fetch_game_stats(game) + if data: + count_servers = data.get("countServers", "N/A") + count_players = data.get("countPlayers", "N/A") + stats_message += f"**{game.upper()}:** Total Servers: {count_servers}, Total Players: {count_players}\n" # noqa + else: + stats_message += f"**{game.upper()}:** Failed to fetch stats.\n" + return stats_message + + +async def perform_search(query: str): + data = fetch_api_data() + servers = data.get("servers", []) + matching_servers = [ + server + for server in servers + if query.lower() in server.get("hostnameDisplay", "").lower() + or query.lower() in server.get("ip", "").lower() + ] + + if not matching_servers: + return "No servers found matching your query." + + max_results = 5 + message = f'Top {min(len(matching_servers), max_results)} servers matching "{query}":\n' # noqa + for server in matching_servers[:max_results]: + message += ( + f"- **{server['hostnameDisplay']}** | {server['gameDisplay']} | " + f"**Gametype**: {server['gametypeDisplay']} | **Map**: {server['mapDisplay']} | " # noqa + f"**Players**: {server['realClients']}/{server['maxplayers']}\n" + ) + return message + + +@tree.command( + name="search", + description="Search for servers by hostname or IP.", + guild=discord.Object(id=GUILD_ID), +) +async def slash_search(interaction: discord.Interaction, query: str): + results = await perform_search(query) + await interaction.response.send_message(results) + + +@app_commands.checks.cooldown(1, 60, key=lambda i: (i.guild_id, i.user.id)) +@tree.command( + name="stats", + description="Get stats for a specific game or all games", + guild=discord.Object(id=GUILD_ID), +) +async def stats( + interaction: discord.Interaction, game: Literal["iw4", "s1", "iw6", "all"] +): + if game == "all": + stats_message = await compile_stats() + else: + data = await fetch_game_stats(game) + if data: + stats_message = f"**Stats for {game.upper()}:**\n" + count_servers = data.get("countServers", "N/A") + count_players = data.get("countPlayers", "N/A") + stats_message += f"Total Servers: {count_servers}\n" # noqa + stats_message += f"Total Players: {count_players}\n" # noqa + else: + stats_message = ( + "Failed to fetch game stats. Please try again later." # noqa + ) + + await interaction.response.send_message(stats_message, ephemeral=True) + # await interaction.delete_original_response() + + +async def on_tree_error( + interaction: discord.Interaction, error: app_commands.AppCommandError +): + if isinstance(error, app_commands.CommandOnCooldown): + return await interaction.response.send_message( + f"Command is currently on cooldown! Try again in **{error.retry_after:.2f}** seconds!" # noqa + ) + elif isinstance(error, app_commands.MissingPermissions): + return await interaction.response.send_message( + "You are missing permissions to use that" + ) + else: + raise error + + +bot.tree.on_error = on_tree_error + + +@bot.event +async def on_message_delete(message): + channel = bot.get_channel(BOT_LOG) + if channel: + embed = discord.Embed( + title="Deleted Message", + description="A message was deleted.", + color=0xDD2E44, + ) + embed.add_field( + name="Author", value=message.author.mention, inline=True + ) # noqa + embed.add_field( + name="Channel", value=message.channel.mention, inline=True + ) # noqa + if message.content: + embed.add_field(name="Content", value=message.content, inline=False) # noqa + embed.set_footer( + text=f"Message ID: {message.id} | Author ID: {message.author.id}" + ) + + await channel.send(embed=embed) + + +@bot.event +async def on_message(message): + if message.author == bot.user: + return + + # Too many mentions + if len(message.mentions) >= 3: + await message.delete() + member = message.guild.get_member(message.author.id) + if member: + # Timeout the member for 60 seconds + await member.timeout_for( + discord.utils.utcnow() + datetime.timedelta(seconds=60) + ) + return + + # Auto delete torrent if post in chat. + for file in message.attachments: + if file.filename.endswith((".torrent", ".TORRENT")): + await message.delete() + + # Check if the message is in an allowed channel + if message.channel.id not in ALLOWED_CHANNELS: + return + + # Check if any of the patterns match the message + # print('Checking for patterns...') + for pattern in patterns: + if re.search(pattern["regex"], message.content, re.IGNORECASE): + # print('Checking message content:', message.content, re.IGNORECASE) # noqa + # print('Matching pattern regex:', pattern['regex']) # noqa + # print('Pattern match:', re.search(pattern['regex'], message.content, re.IGNORECASE)) # noqa + response = pattern["response"] + await message.channel.send(response) + break + + +# Update Player Counts from API +@tasks.loop(minutes=10) +async def update_status(): + data = fetch_api_data() + countPlayers = data.get("countPlayers", 0) + countServers = data.get("countServers", 0) + activity = discord.Game( + name=f"with {countPlayers} players on {countServers} servers" + ) + await bot.change_presence(activity=activity) + + +@bot.event +async def on_ready(): + print(f"{bot.user.name} has connected to Discord!") + await tree.sync( + guild=discord.Object(id=GUILD_ID) + ) # Sync commands for a specific guild. + update_status.start() + + +bot.run(os.getenv("BOT_TOKEN")) diff --git a/patterns.json b/patterns.json new file mode 100644 index 0000000..7377081 --- /dev/null +++ b/patterns.json @@ -0,0 +1,18 @@ +[ + { + "regex": "(?:do\\s+|if\\s+i\\s+don't\\s+)?(?:i|you)?\\s*(?:really\\s+)?(?:need\\s+to|have\\s+to|must|gotta|can)\\s+own\\s+(?:an?\\s+)?(?:official\\s+)?(?:copy\\s+of\\s+)?(?:the\\s+|this\\s+|said\\s+|these\\s+|those\\s+)?(?:cod\\s+)?games?\\s*(?:on\\s+steam|on\\s+alterware)?", + "response": "Yes, it is required to own a copy of the game to utilize our clients" + }, + { + "regex": "\bhow can I open the console ingame\b", + "response": "Press the tilde key (~) or grave key (`) to enter console.\n https://i.imgur.com/drPaC0f.png" + }, + { + "regex": "(.*)(HOW|WHERE)(.*)(CHANGE|MODIFY)(.*)(USERNAME|NAME|IGN)(.*)", + "response": "``/name WhateverNameYouWant`` on the in-game console.\nYou open the in-game console with the ~ or ` key (underneath ESC). If you're on a 60% keyboard it's Fn+ESC.\nhttps://i.imgur.com/drPaC0f.png" + }, + { + "regex": "online profile is invalid", + "response": "Close your game, locate your Modern Warfare 2 game folder, and delete the `players` folder" + } +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9f5e11d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +discord.py +flake8 +requests