This commit is contained in:
2024-07-07 16:59:47 +02:00
commit 139430c2f8
7 changed files with 346 additions and 0 deletions

47
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -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

32
.github/workflows/lint.yml vendored Normal file
View File

@@ -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 .

13
Dockerfile Normal file
View File

@@ -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"]

2
README.md Normal file
View File

@@ -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

231
aw.py Normal file
View File

@@ -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"))

18
patterns.json Normal file
View File

@@ -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"
}
]

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
discord.py
flake8
requests