diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a122147 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.14-slim + +# Copy your code +WORKDIR /app +RUN pip install --no-cache-dir discord asyncio requests + +COPY main.py . +COPY PICable.py . + +# Drop privileges (optional but good practice) +RUN useradd -m appuser +USER appuser + +# Start the scheduler script +CMD ["python", "main.py"] diff --git a/README.md b/README.md index 3c67282..2d8656d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # PICable + Simple script to determine if a project is eligible as the final PIC of LEIC-T. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5c43b42 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + picable: + build: . + container_name: picable + environment: + - PICABLE_DISCORD_TOKEN=yourdiscordtoken + - PICABLE_GITHUB_TOKEN=yourgithubtoken + restart: unless-stopped diff --git a/main.py b/main.py index 9136b37..6af23af 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ from asyncio import Condition, Lock, create_task, shield, to_thread, wait_for from sys import argv +from os import getenv import requests from discord import Game, Intents, Interaction, app_commands @@ -8,107 +9,108 @@ from discord.ext import commands from PICable import PICable MAX_STORAGE_KB = 10 * 1024 * 1024 # 10GB -current_storage_kb = 0 -storage_condition = Condition() -current_repositories = set() -current_repositories_lock = Lock() -if len(argv) != 3: - print("Usage:\n\t" + argv[0] + " ") - exit(1) -discord_token = argv[1] -github_token = argv[2] +if __name__ == "__main__": + current_storage_kb = 0 + storage_condition = Condition() + current_repositories = set() + current_repositories_lock = Lock() -intents = Intents.default() -intents.message_content = True -client = commands.Bot(command_prefix="/", intents=intents) + discord_token = getenv("PICABLE_DISCORD_TOKEN") + github_token = getenv("PICABLE_GITHUB_TOKEN") + + if not discord_token or not github_token: + print("Discord or Github tokens not set in env. Use variables PICABLE_DISCORD_TOKEN and PICABLE_GITHUB_TOKEN") + exit(1) + + intents = Intents.default() + intents.message_content = True + client = commands.Bot(command_prefix="/", intents=intents) + client.run(discord_token) -@client.event -async def on_ready(): - print(f"We have logged in as {client.user}") - await client.change_presence(activity=Game("with PIC ideas")) - try: - synced = await client.tree.sync() - print(f"Synced {len(synced)} command(s)") - except Exception as e: - print(f"Failed to sync commands: {e}") + @client.event + async def on_ready(): + print(f"We have logged in as {client.user}") + await client.change_presence(activity=Game("with PIC ideas")) + try: + synced = await client.tree.sync() + print(f"Synced {len(synced)} command(s)") + except Exception as e: + print(f"Failed to sync commands: {e}") -@client.tree.command(name="ping", description="Check the bot's latency.") -async def ping(interaction: Interaction): - latency = client.latency * 1000 - await interaction.response.send_message(f"Pong! Latency: {latency:.2f}ms") + @client.tree.command(name="ping", description="Check the bot's latency.") + async def ping(interaction: Interaction): + latency = client.latency * 1000 + await interaction.response.send_message(f"Pong! Latency: {latency:.2f}ms") -async def handle_picable(owner: str, repository: str, repository_size_kb: int, github_token: str): - global current_storage_kb - async with storage_condition: - await storage_condition.wait_for(lambda: current_storage_kb + repository_size_kb <= MAX_STORAGE_KB) - current_storage_kb += repository_size_kb + async def handle_picable(owner: str, repository: str, repository_size_kb: int, github_token: str): + global current_storage_kb + async with storage_condition: + await storage_condition.wait_for(lambda: current_storage_kb + repository_size_kb <= MAX_STORAGE_KB) + current_storage_kb += repository_size_kb - print(f"Storage is {current_storage_kb / MAX_STORAGE_KB * 100:.2f}% full.") - return await to_thread(PICable, owner, repository, github_token) + print(f"Storage is {current_storage_kb / MAX_STORAGE_KB * 100:.2f}% full.") + return await to_thread(PICable, owner, repository, github_token) -@client.tree.command(name="picable", description="Check if a Github repository is eligible for PIC.") -@app_commands.describe(owner="The owner of the repository", repository="The name of the repository") -async def picable(interaction: Interaction, owner: str, repository: str): - await interaction.response.defer(thinking=True) + @client.tree.command(name="picable", description="Check if a Github repository is eligible for PIC.") + @app_commands.describe(owner="The owner of the repository", repository="The name of the repository") + async def picable(interaction: Interaction, owner: str, repository: str): + await interaction.response.defer(thinking=True) - repository_full_name = f"{owner}/{repository}" - async with current_repositories_lock: - if repository_full_name in current_repositories: + repository_full_name = f"{owner}/{repository}" + async with current_repositories_lock: + if repository_full_name in current_repositories: + await interaction.followup.send( + f"The repository {repository_full_name} is already being analyzed. Please wait for the current analysis to complete. :warning:" + ) + return + + current_repositories.add(repository_full_name) + + url = f"https://api.github.com/repos/{owner}/{repository}" + headers = {"Authorization": f"token {github_token}"} + response = requests.get(url, headers=headers) + + if not response.status_code == 200: await interaction.followup.send( - f"The repository {repository_full_name} is already being analyzed. Please wait for the current analysis to complete. :warning:" + f"Invalid Repository. Please check the repository name and owner and try again. :x:" ) return - current_repositories.add(repository_full_name) + response.raise_for_status() + repo_info = response.json() + repository_size_kb = repo_info["size"] + print(f"Repository size: {repository_size_kb / 1024} MB") - url = f"https://api.github.com/repos/{owner}/{repository}" - headers = {"Authorization": f"token {github_token}"} - response = requests.get(url, headers=headers) - - if not response.status_code == 200: - await interaction.followup.send( - f"Invalid Repository. Please check the repository name and owner and try again. :x:" - ) - return - - response.raise_for_status() - repo_info = response.json() - repository_size_kb = repo_info["size"] - print(f"Repository size: {repository_size_kb / 1024} MB") - - if repository_size_kb > MAX_STORAGE_KB: - await interaction.followup.send( - f"The repository {repository_full_name} is too large to analyze. Please try a smaller repository. :warning:" - ) - current_repositories.remove(repository_full_name) - return - - global current_storage_kb - try: - result_future = create_task(handle_picable(owner, repository, repository_size_kb, github_token)) - try: - result = await wait_for(shield(result_future), timeout=600) - await interaction.followup.send(result) - except TimeoutError: + if repository_size_kb > MAX_STORAGE_KB: await interaction.followup.send( - f"The analysis for {repository_full_name} is still taking place. The results will be posted here once the analysis is complete. :clock4:" + f"The repository {repository_full_name} is too large to analyze. Please try a smaller repository. :warning:" ) - result = await result_future - await interaction.channel.send(f"{interaction.user.mention}\n{result}") - except Exception as e: - print(f"Error processing PICable check: {e}") - await interaction.channel.send("An error occurred while processing your request. Please try again later.") - finally: - async with current_repositories_lock: current_repositories.remove(repository_full_name) - async with storage_condition: - current_storage_kb -= repository_size_kb - storage_condition.notify_all() + return - -client.run(discord_token) + global current_storage_kb + try: + result_future = create_task(handle_picable(owner, repository, repository_size_kb, github_token)) + try: + result = await wait_for(shield(result_future), timeout=600) + await interaction.followup.send(result) + except TimeoutError: + await interaction.followup.send( + f"The analysis for {repository_full_name} is still taking place. The results will be posted here once the analysis is complete. :clock4:" + ) + result = await result_future + await interaction.channel.send(f"{interaction.user.mention}\n{result}") + except Exception as e: + print(f"Error processing PICable check: {e}") + await interaction.channel.send("An error occurred while processing your request. Please try again later.") + finally: + async with current_repositories_lock: + current_repositories.remove(repository_full_name) + async with storage_condition: + current_storage_kb -= repository_size_kb + storage_condition.notify_all() diff --git a/run.sh b/run.sh deleted file mode 100755 index a8c6a34..0000000 --- a/run.sh +++ /dev/null @@ -1 +0,0 @@ -python3 main.py MTM0MzMzNzAzMzkyOTU4ODg5MA.Gx2pEq.uLdXFORUFlYurLUF6Lnsuh_EWFQRI8SAKFebDM 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