Compare commits

..

3 Commits

Author SHA1 Message Date
26ce03cdf6 Update 2024-10-20 16:17:47 +02:00
a791803aec typo in readme 2024-06-14 03:33:28 +02:00
ef3c2fc1d3 Updating event listeners and API to use the new library 2024-06-14 03:30:10 +02:00
9 changed files with 401 additions and 73 deletions

34
.dockerignore Normal file
View File

@@ -0,0 +1,34 @@
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7
ARG PYTHON_VERSION=3.11.9
FROM python:${PYTHON_VERSION}-slim as base
# Prevents Python from writing pyc files.
ENV PYTHONDONTWRITEBYTECODE=1
# Keeps Python from buffering stdout and stderr to avoid situations where
# the application crashes without emitting any logs due to buffering.
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
python -m pip install -r requirements.txt
# Switch to the non-privileged user to run the application.
USER appuser
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD python main.py

22
README.Docker.md Normal file
View File

@@ -0,0 +1,22 @@
### Building and running your application
When you're ready, start your application by running:
`docker compose up --build`.
Your application will be available at http://localhost:8000.
### Deploying your application to the cloud
First, build your image, e.g.: `docker build -t myapp .`.
If your cloud uses a different CPU architecture than your development
machine (e.g., you are on a Mac M1 and your cloud provider is amd64),
you'll want to build the image for that platform, e.g.:
`docker build --platform=linux/amd64 -t myapp .`.
Then, push it to your registry, e.g. `docker push myregistry.com/myapp`.
Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/)
docs for more detail on building and pushing.
### References
* [Docker's Python guide](https://docs.docker.com/language/python/)

View File

@@ -1,5 +1,5 @@
## This is the API Branch of this project.
The mail goal is to provide an RCON backend that can be used by another container (Werbserver)
The main goal is to provide an RCON backend that can be used by another container (Werbserver)
## DayZ Server Management Tool

View File

@@ -47,29 +47,14 @@ async def get_rcon_manager():
# FastAPI Endpoint to List Players
@app.get("/players")
async def list_players(rcon: rcon.AsyncRCONClient = Depends(get_rcon_manager)):
try:
response = await rcon.send_command("players")
players = []
for line in response.splitlines():
parts = line.split()
if len(parts) >= 2:
name = parts[0]
try:
score = int(parts[1])
except ValueError:
score = 0
player = Player(name=name, score=score)
players.append(player)
return players
except Exception as e:
log.error(f"Error fetching players: {e}")
raise HTTPException(status_code=500, detail="Error fetching players")
response = await rcon.fetch_players()
return {"response": response}
# Kick Player Endpoint
@app.post("/kick")
async def kick_player(action: PlayerAction, rcon: rcon.AsyncRCONClient = Depends(get_rcon_manager)):
try:
response = await rcon.send_command(f'kick {action.playerid} {action.reason}')
response = await rcon.kick(f'{action.playerid} {action.reason}')
return {"response": response}
except Exception as e:
log.error(f"Error kicking player: {e}")
@@ -80,7 +65,7 @@ async def kick_player(action: PlayerAction, rcon: rcon.AsyncRCONClient = Depends
async def ban_player(action: PlayerAction, rcon: rcon.AsyncRCONClient = Depends(get_rcon_manager)):
try:
duration_str = f"{action.duration} " if action.duration else ""
response = await rcon.send_command(f'ban {action.playerid} {duration_str}"{action.reason}"')
response = await rcon.ban(f'{action.playerid} {duration_str}"{action.reason}"')
return {"response": response}
except Exception as e:
log.error(f"Error banning player: {e}")
@@ -100,7 +85,7 @@ async def unlock_server(rcon: rcon.AsyncRCONClient = Depends(get_rcon_manager)):
@app.post("/global_message")
async def global_message(message: str, rcon: rcon.AsyncRCONClient = Depends(get_rcon_manager)):
try:
response = await rcon.send_command(f'Say -1 {message}')
response = await rcon.send(f'{message}')
return {"response": response}
except Exception as e:
log.error(f"Error sending global message: {e}")
@@ -110,7 +95,7 @@ async def global_message(message: str, rcon: rcon.AsyncRCONClient = Depends(get_
@app.post("/direct_message")
async def direct_message(playerid: str, message: str, rcon: rcon.AsyncRCONClient = Depends(get_rcon_manager)):
try:
response = await rcon.send_command(f'Say {playerid} {message}')
response = await rcon.whisper(f'{playerid} {message}')
return {"response": response}
except Exception as e:
log.error(f"Error sending direct message: {e}")
@@ -144,3 +129,4 @@ async def server_response_to_command(response: str):
# Main Function to Run FastAPI App
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
asyncio.run(get_rcon_manager())

49
compose.yaml Normal file
View File

@@ -0,0 +1,49 @@
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 8000:8000
# The commented out section below is an example of how to define a PostgreSQL
# database that your application can use. `depends_on` tells Docker Compose to
# start the database before your application. The `db-data` volume persists the
# database data between container restarts. The `db-password` secret is used
# to set the database password. You must create `db/password.txt` and add
# a password of your choosing to it before running `docker compose up`.
# depends_on:
# db:
# condition: service_healthy
# db:
# image: postgres
# restart: always
# user: postgres
# secrets:
# - db-password
# volumes:
# - db-data:/var/lib/postgresql/data
# environment:
# - POSTGRES_DB=example
# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
# expose:
# - 5432
# healthcheck:
# test: [ "CMD", "pg_isready" ]
# interval: 10s
# timeout: 5s
# retries: 5
# volumes:
# db-data:
# secrets:
# db-password:
# file: db/password.txt

View File

@@ -1,51 +0,0 @@
"""Listens to an RCON server for messages."""
import asyncio
import logging
import math
import berconpy as rcon
IP = "217.72.204.88"
PORT = 2313
PASSWORD = "u1BSlMM4LH0"
log = logging.getLogger("berconpy")
log.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
log.addHandler(handler)
client = rcon.AsyncRCONClient()
@client.dispatch.on_login
async def on_login():
print("on_login")
@client.dispatch.on_message
async def on_message(message: str):
print("on_message:", message)
@client.dispatch.on_command
async def server_response_to_command(response: str):
# this event also includes keep alive commands we send to the server;
# for handling commands, reading the return value of
# `await client.send_command()` is the recommended method
if not response:
return print("on_command: <empty>")
print("on_command:", response)
# Other events are documented in AsyncRCONClient
async def main():
async with client.connect(IP, PORT, PASSWORD):
await asyncio.sleep(math.inf)
if __name__ == "__main__":
asyncio.run(main())

139
main.py Normal file
View File

@@ -0,0 +1,139 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import json
import os
import asyncio
from typing import List, Dict, Any
from fastapi.responses import JSONResponse
import berconpy as rcon
# RCON Server Details
credentials_file = os.path.join(os.path.dirname(__file__), "credentials.json")
try:
with open(credentials_file) as f:
credentials = json.load(f)
except FileNotFoundError:
raise Exception(f"Credentials file not found: {credentials_file}")
except json.JSONDecodeError:
raise Exception(f"Error decoding credentials file: {credentials_file}")
IP_ADDR = credentials["SERVER_ADDRESS"]
PORT = credentials["SERVER_PORT"]
PASSWORD = credentials["RCON_PASSWORD"]
# Initialize FastAPI app
app = FastAPI()
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins
allow_credentials=True,
allow_methods=["*"], # Allow all methods
allow_headers=["*"], # Allow all headers
)
# Initialize RCON client
client = rcon.AsyncRCONClient()
# Message store
messages = []
player_connect_messages = []
# Event handler for player connect messages
@client.dispatch.on_message
async def on_player_connect(player_connect_message: str):
print('Player connected:', player_connect_message)
player_connect_messages.append(player_connect_message)
@client.dispatch.on_command
async def server_response_to_command(response: str):
print('Command response:', response)
messages.append(response)
# Background task to run the RCON client
async def run_rcon_client():
try:
async with client.connect(IP_ADDR, PORT, PASSWORD):
while True:
await asyncio.sleep(60) # Sleep for an hour and then check the connection
except Exception as e:
print(f"Error in RCON client: {e}")
finally:
await client.close()
# API endpoint to get messages
@app.get("/loginmessages", response_class=JSONResponse)
async def get_messages() -> List[str]:
return player_connect_messages
# API endpoint to get players list
@app.get("/players")
async def get_players() -> List[Dict[str, Any]]:
try:
players = await client.fetch_players()
player_list = [{"id": player.id, "name": player.name} for player in players]
return JSONResponse(content=player_list)
except Exception as e:
print(f"Error fetching players: {e}")
return JSONResponse(status_code=500, content={"message": "Error fetching players"})
# API endpoint to kick a player
@app.post("/kick")
async def kick_player(player_id: int, reason: str = ""):
try:
response = await client.kick(player_id, reason)
return JSONResponse(content={"message": response})
except Exception as e:
print(f"Error kicking player: {e}")
return JSONResponse(status_code=500, content={"message": "Error kicking player"})
# API endpoint to ban a player
@app.post("/ban")
async def ban_player(addr: str, duration: int = 0, reason: str = ""):
try:
response = await client.ban(addr, duration, reason)
return JSONResponse(content={"message": response})
except Exception as e:
print(f"Error banning player: {e}")
return JSONResponse(status_code=500, content={"message": "Error banning player"})
# API endpoint to send a global message
@app.post("/message")
async def send_global_message(message: str):
try:
response = await client.send(message)
return JSONResponse(content={"message": response})
except Exception as e:
print(f"Error sending message: {e}")
return JSONResponse(status_code=500, content={"message": "Error sending message"})
# API endpoint to unban a player
@app.post("/unban")
async def unban_player(ban_id: int):
try:
response = await client.unban(ban_id)
return JSONResponse(content={"message": response})
except Exception as e:
print(f"Error unbanning player: {e}")
return JSONResponse(status_code=500, content={"message": "Error unbanning player"})
# API endpoint to send a private message (whisper) to a player
@app.post("/whisper")
async def whisper_player(player_id: int, message: str):
try:
response = await client.whisper(player_id, message)
return JSONResponse(content={"message": response})
except Exception as e:
print(f"Error sending whisper: {e}")
return JSONResponse(status_code=500, content={"message": "Error sending whisper"})
# Start the RCON client in the background when the app starts
@app.on_event("startup")
async def startup_event():
asyncio.create_task(run_rcon_client())
# Main entry point for running the FastAPI app
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

98
tester.html Normal file
View File

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Player Connect Messages</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
#messages {
border: 1px solid #ccc;
padding: 10px;
height: 400px;
overflow-y: scroll;
}
.message {
border-bottom: 1px solid #eee;
padding: 5px 0;
}
#player-count {
font-size: 20px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1>Player Connect Messages</h1>
<div id="player-count">Current Players: 0</div>
<div id="messages"></div>
<script>
function fetchMessages() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8000/loginmessages', true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log('Fetched messages:', data);
updateMessages(data);
} else {
console.error('Error fetching messages:', xhr.statusText);
}
};
xhr.onerror = function() {
console.error('Network error while fetching messages');
};
xhr.send();
}
function fetchPlayerCount() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8000/players', true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log('Fetched player count:', data.length);
updatePlayerCount(data.length);
} else {
console.error('Error fetching player count:', xhr.statusText);
}
};
xhr.onerror = function() {
console.error('Network error while fetching player count');
};
xhr.send();
}
function updateMessages(messages) {
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML = '';
messages.forEach(message => {
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
messageDiv.textContent = message;
messagesDiv.appendChild(messageDiv);
});
console.log('Updated messages displayed');
}
function updatePlayerCount(count) {
const playerCountDiv = document.getElementById('player-count');
playerCountDiv.textContent = `Current Players: ${count}`;
}
// Fetch messages every 5 seconds
setInterval(fetchMessages, 5000);
// Fetch player count every 5 seconds
setInterval(fetchPlayerCount, 5000);
// Initial fetch
fetchMessages();
fetchPlayerCount();
</script>
</body>
</html>