I am writing a Python script to automate the updates of mods.
The script retrieves the credentials to get the token for logging in.
However, I keep getting a 403 error code when it tries to download a mod.
Code: Select all
import os
import re
import requests
import json
import sys
import platform
import traceback
from datetime import datetime
from cryptography.fernet import Fernet
import getpass
# Check if the operating system is Linux
if platform.system() != "Linux":
print("Error: This script is only compatible with Linux.")
sys.exit(1)
# Paths and static variables
FACTORIO_BINARY_PATH = os.path.join("bin", "x64", "factorio")
LOG_FILE = os.path.expanduser("~/mod_updater.log")
CREDENTIALS_FILE = os.path.expanduser("~/.factorio_credentials")
MODS_DIR = os.path.join(os.getcwd(), "mods")
MOD_LIST_FILE = os.path.join(MODS_DIR, "mod-list.json")
MODS_TO_IGNORE = ["base", "elevated_rails", "quality", "space_age"]
# Function to write to the log file
def log_error(message):
try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message = f"{timestamp} - {message}\n"
with open(LOG_FILE, 'a') as f:
f.write(log_message)
except Exception as e:
print(f"Error while writing to the log file: {str(e)}")
# Function to check read and write permissions
def check_permissions(path, mode):
return os.access(path, mode)
# Function to check if sudo is installed
def is_sudo_installed():
return os.system("which sudo > /dev/null 2>&1") == 0
# Function to restart the script with sudo
def restart_with_sudo():
os.execvp("sudo", ["sudo", "python3"] + sys.argv)
# Generate an encryption key
def generate_key():
return Fernet.generate_key()
# Encrypt credentials
def encrypt_credentials(key, credentials):
fernet = Fernet(key)
return fernet.encrypt(credentials.encode())
# Decrypt credentials
def decrypt_credentials(key, encrypted_credentials):
fernet = Fernet(key)
return fernet.decrypt(encrypted_credentials).decode()
# Request credentials
def get_credentials():
username = input("Username: ")
password = getpass.getpass("Password: ")
return f"{username}:{password}"
# Save encrypted credentials
def save_credentials(key, encrypted_credentials):
with open(CREDENTIALS_FILE, 'wb') as f:
f.write(key + b'\n' + encrypted_credentials)
# Load encrypted credentials
def load_credentials():
with open(CREDENTIALS_FILE, 'rb') as f:
key = f.readline().strip()
encrypted_credentials = f.readline().strip()
return key, encrypted_credentials
# Get the authentication token
def get_auth_token(username, password):
auth_url = "https://auth.factorio.com/api-login"
response = requests.post(auth_url, data={"username": username, "password": password})
if response.status_code == 200:
try:
# Check if the response is valid JSON
response_json = response.json()
if isinstance(response_json, list) and len(response_json) > 0:
return response_json[0] # Retrieve the first element of the list as the token
else:
error_message = f"Error: Unexpected API response: {response_json}"
print(error_message)
log_error(error_message)
return None
except ValueError:
error_message = f"Error: Non-JSON API response: {response.text}"
print(error_message)
log_error(error_message)
return None
else:
error_message = f"Error during authentication: {response.status_code} {response.text}"
print(error_message)
log_error(error_message)
return None
# Check if credentials are already saved
if os.path.exists(CREDENTIALS_FILE):
key, encrypted_credentials = load_credentials()
credentials = decrypt_credentials(key, encrypted_credentials)
else:
credentials = get_credentials()
key = generate_key()
encrypted_credentials = encrypt_credentials(key, credentials)
save_credentials(key, encrypted_credentials)
# Use credentials for authentication
username, password = credentials.split(':')
auth_token = get_auth_token(username, password)
if not auth_token:
print("Error: Unable to obtain the authentication token.")
sys.exit(1)
# Function to download a file
def download_file(url, destination, token):
try:
if not check_permissions(os.path.dirname(destination), os.W_OK):
error_message = f"Error: Write permission denied for {destination}"
print(error_message)
log_error(error_message)
return False
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
response.raise_for_status() # Check for HTTP errors
with open(destination, 'wb') as f:
f.write(response.content)
return True
except Exception as e:
error_message = f"Error while downloading {url}: {str(e)}"
print(error_message)
log_error(error_message)
return False
# Function to get the latest version of a mod
def get_latest_mod_version(mod_name, token):
try:
api_url = f"https://mods.factorio.com/api/mods/{mod_name}"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(api_url, headers=headers)
response.raise_for_status() # Check for HTTP errors
if response.status_code == 200:
mod_info = response.json()
if "releases" in mod_info and mod_info["releases"]:
return mod_info["releases"][-1]["version"] # Get the latest version
else:
error_message = f"Error: No version found for {mod_name}"
print(error_message)
log_error(error_message)
return None
else:
error_message = f"Error: Unable to retrieve the latest version for {mod_name}"
print(error_message)
log_error(error_message)
return None
except Exception as e:
error_message = f"Error while retrieving the latest version for {mod_name}: {str(e)}"
print(error_message)
log_error(error_message)
return None
# Function to extract the mod name and version from the zip file name
def extract_mod_name_and_version(filename):
# Pattern to extract the mod name and version
mod_pattern = re.compile(r"^([A-Za-z0-9_-]+)_(\d+\.\d+\.\d+)\.zip$")
match = mod_pattern.match(filename)
if match:
return match.group(1), match.group(2)
return None, None
# Function to check if a mod is present in the mods directory
def is_mod_present(mod_name):
mod_zip_pattern = re.compile(r"^([A-Za-z0-9_-]+)_(\d+\.\d+\.\d+)\.zip$")
for file in os.listdir(MODS_DIR):
match = mod_zip_pattern.match(file)
if match and match.group(1) == mod_name:
return True
return False
# Function to extract the version from the zip file name
def get_current_mod_version(mod_name):
if not check_permissions(MODS_DIR, os.R_OK):
error_message = f"Error: Read permission denied for {MODS_DIR}"
print(error_message)
log_error(error_message)
return None
mod_zip_pattern = re.compile(r"^([A-Za-z0-9_-]+)_(\d+\.\d+\.\d+)\.zip$")
for file in os.listdir(MODS_DIR):
match = mod_zip_pattern.match(file)
if match and match.group(1) == mod_name:
return match.group(2)
return None
# Function to delete the old version of a mod
def delete_old_mod_version(mod_name, current_version):
old_mod_zip_path = os.path.join(MODS_DIR, f"{mod_name}_{current_version}.zip")
if os.path.exists(old_mod_zip_path):
if not check_permissions(MODS_DIR, os.W_OK):
error_message = f"Error: Write permission denied for {MODS_DIR}"
print(error_message)
log_error(error_message)
return False
os.remove(old_mod_zip_path)
print(f"Old version of {mod_name} deleted.")
return True
return False
# Function to download a mod
def download_mod(mod_name, mod_version, token):
try:
# Get the download URL via the Factorio API
api_url = f"https://mods.factorio.com/api/mods/{mod_name}"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(api_url, headers=headers)
response.raise_for_status() # Check for HTTP errors
if response.status_code == 200:
mod_info = response.json()
# Find the specific version in the releases
for release in mod_info["releases"]:
if release["version"] == mod_version:
download_url = release["download_url"]
break
else:
error_message = f"Error: Version {mod_version} not found for {mod_name}"
print(error_message)
log_error(error_message)
return None
# Ensure the download URL is correctly formatted
if not download_url.startswith("https://mods.factorio.com/"):
download_url = f"https://mods.factorio.com{download_url}"
mod_zip_path = os.path.join(MODS_DIR, f"{mod_name}_{mod_version}.zip")
print(f"Downloading {mod_name} version {mod_version}...")
try:
if download_file(download_url, mod_zip_path, token):
return mod_zip_path
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
error_message = f"Error 403: Access forbidden for {download_url}. Check permissions or authentication."
print(error_message)
log_error(error_message)
else:
error_message = f"Error while downloading {mod_name}: {str(e)}"
print(error_message)
log_error(error_message)
return None
else:
error_message = f"Error: Unable to retrieve the download URL for {mod_name}"
print(error_message)
log_error(error_message)
return None
except Exception as e:
error_message = f"Error while downloading {mod_name}: {str(e)}"
print(error_message)
log_error(error_message)
return None
# Function to check and update mods
def check_and_update_mods(token):
if not os.path.exists(MODS_DIR) or not os.path.exists(MOD_LIST_FILE):
print("No mods are installed.")
return
if not check_permissions(MOD_LIST_FILE, os.R_OK):
error_message = f"Error: Read permission denied for {MOD_LIST_FILE}"
print(error_message)
log_error(error_message)
return
with open(MOD_LIST_FILE, 'r') as f:
mods_data = json.load(f)
if "mods" not in mods_data or not mods_data["mods"]:
print("No mods are installed.")
return
for mod in mods_data["mods"]:
if isinstance(mod, dict) and "name" in mod:
mod_name = mod["name"]
# Ignore specific mods
if mod_name in MODS_TO_IGNORE:
print(f"Ignoring mod {mod_name}.")
continue
latest_version = get_latest_mod_version(mod_name, token)
if not is_mod_present(mod_name):
print(f"The mod {mod_name} is not present. Downloading the latest version...")
download_mod(mod_name, latest_version, token)
else:
current_version = get_current_mod_version(mod_name)
if latest_version and latest_version != current_version:
new_mod_zip_path = download_mod(mod_name, latest_version, token)
if new_mod_zip_path and current_version:
delete_old_mod_version(mod_name, current_version)
else:
print(f"The mod {mod_name} is already up to date or unable to find the latest version.")
else:
error_message = f"Invalid element in mod-list.json: {mod}"
print(error_message)
log_error(error_message)
print("Mod updates completed.")
# Check if the script is run from the root of the Factorio directory
if not os.path.exists(FACTORIO_BINARY_PATH):
error_message = "Error: The script must be executed from the root of the Factorio directory."
print(error_message)
log_error(error_message)
sys.exit(1)
# Check permissions
if not check_permissions(MODS_DIR, os.R_OK | os.W_OK) or not check_permissions(MOD_LIST_FILE, os.R_OK):
if is_sudo_installed():
print("Restarting the script with sudo...")
restart_with_sudo()
else:
error_message = "Error: The sudo package is not installed. Please install sudo and restart the script in superuser mode."
print(error_message)
log_error(error_message)
sys.exit(1)
else:
check_and_update_mods(auth_token)