Compare commits

..

6 Commits

Author SHA1 Message Date
Enzo Nunes
4a4fb621ad fix cloc output scrapping. add clone progress and python output to docker logs. 2025-12-05 20:12:55 +00:00
Enzo Nunes
1cac1d2e66 fix main file. adapt dockerfile. 2025-12-02 23:15:40 +00:00
06d3098940 Updated docker-compose to use dockerhub 2025-12-02 20:05:42 +00:00
Enzo Nunes
cbcd90732d change scope of tokens to global constants, obtained from env vars 2025-12-02 20:02:35 +00:00
6c7eafb8d3 Added cloc to Dockerfile 2025-11-19 08:52:16 +00:00
Enzo Nunes
cd60dd5653 add env var handling 2025-11-18 21:43:17 +00:00
6 changed files with 148 additions and 125 deletions

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
# Copy this file to .env and fill in your actual tokens
# The .env file is gitignored and will be used for local development
PICABLE_DISCORD_TOKEN=your_discord_token_here
PICABLE_GITHUB_TOKEN=your_github_token_here

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
**/__pycache__/ **/__pycache__/
**/*_tok **/*_tok
**/.idea/ **/.idea/
# Environment variables and local development overrides
.env
docker-compose.override.yml

View File

@@ -1,15 +1,21 @@
FROM python:3.14-slim FROM python:3.14-slim
# Copy your code RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends cloc git; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
RUN pip install --no-cache-dir discord asyncio requests RUN pip install --no-cache-dir discord.py requests
COPY main.py . COPY main.py .
COPY PICable.py . COPY PICable.py .
# Drop privileges (optional but good practice) RUN useradd -m appuser && \
RUN useradd -m appuser mkdir -p /temp && \
chown appuser:appuser /temp
USER appuser USER appuser
# Start the scheduler script CMD ["python", "-u", "main.py"]
CMD ["python", "main.py"]

View File

@@ -1,15 +1,15 @@
import os import os
import subprocess import subprocess
import sys
from datetime import datetime, timedelta from datetime import datetime, timedelta
import requests import requests
LIMIT_COMMITS_30_DAYS = 50 LIMIT_COMMITS_30_DAYS = int(os.getenv("LIMIT_COMMITS_30_DAYS", "50"))
LIMIT_STARS = 200 LIMIT_STARS = int(os.getenv("LIMIT_STARS", "200"))
LIMIT_NLOC = 100000 LIMIT_NLOC = int(os.getenv("LIMIT_NLOC", "100000"))
MARGIN = 0.1 MARGIN = 0.1
CLONE_DIR = "~/temp/PICable" CLONE_DIR = "/temp"
EXCLUDED_LANGUAGES = ["CSV", "diff", "INI", "JSON", "Markdown", "reStructuredText", "SVG", "Text", "YAML"] EXCLUDED_LANGUAGES = ["CSV", "diff", "INI", "JSON", "Markdown", "reStructuredText", "SVG", "Text", "YAML"]
@@ -53,20 +53,28 @@ def get_lines_of_code(owner, repository):
repository_clone_dir = os.path.expanduser(f"{parent_dir}/{repository}") repository_clone_dir = os.path.expanduser(f"{parent_dir}/{repository}")
try: try:
print(f"Cloning {repository_url}...")
subprocess.run( subprocess.run(
["git", "clone", "--depth", "1", repository_url, repository_clone_dir], ["git", "clone", "--depth", "1", "--progress", repository_url, repository_clone_dir],
check=True, check=True,
stdout=sys.stdout,
stderr=sys.stderr,
) )
print("Clone complete. Running cloc...")
result = subprocess.run( result = subprocess.run(
["cloc", f"--exclude-lang={','.join(EXCLUDED_LANGUAGES)}", repository_clone_dir], ["cloc", f"--exclude-lang={','.join(EXCLUDED_LANGUAGES)}", repository_clone_dir],
capture_output=True, capture_output=True,
text=True, text=True,
check=True, check=True,
) )
number_of_lines_of_code = int(result.stdout.split("\n")[-3].split()[-1])
except Exception: # Find the SUM line and extract the last column (code lines)
for line in result.stdout.split("\n"):
if line.strip().startswith("SUM:"):
number_of_lines_of_code = int(line.split()[-1])
break
else:
raise ValueError("Could not find SUM line in cloc output")
except Exception as e:
print(f"Error occurred: {e}")
return -1 return -1
finally: finally:
subprocess.run(["rm", "-rf", parent_dir], check=True) subprocess.run(["rm", "-rf", parent_dir], check=True)

View File

@@ -1,8 +1,8 @@
services: services:
picable: picable:
build: . image: didas72/picable:latest
container_name: picable container_name: picable
environment: environment:
- PICABLE_DISCORD_TOKEN=yourdiscordtoken - PICABLE_DISCORD_TOKEN=${PICABLE_DISCORD_TOKEN}
- PICABLE_GITHUB_TOKEN=yourgithubtoken - PICABLE_GITHUB_TOKEN=${PICABLE_GITHUB_TOKEN}
restart: unless-stopped restart: unless-stopped

54
main.py
View File

@@ -1,6 +1,5 @@
import os
from asyncio import Condition, Lock, create_task, shield, to_thread, wait_for from asyncio import Condition, Lock, create_task, shield, to_thread, wait_for
from sys import argv
from os import getenv
import requests import requests
from discord import Game, Intents, Interaction, app_commands from discord import Game, Intents, Interaction, app_commands
@@ -8,29 +7,26 @@ from discord.ext import commands
from PICable import PICable from PICable import PICable
MAX_STORAGE_KB = 10 * 1024 * 1024 # 10GB MAX_STORAGE_KB = 50 * 1024 * 1024 # 50GB
DISCORD_TOKEN = os.getenv("PICABLE_DISCORD_TOKEN")
GITHUB_TOKEN = os.getenv("PICABLE_GITHUB_TOKEN")
if __name__ == "__main__": current_storage_kb = 0
current_storage_kb = 0 storage_condition = Condition()
storage_condition = Condition() current_repositories = set()
current_repositories = set() current_repositories_lock = Lock()
current_repositories_lock = Lock()
discord_token = getenv("PICABLE_DISCORD_TOKEN") if not DISCORD_TOKEN or not GITHUB_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") print("Discord or Github tokens not set in env. Use variables PICABLE_DISCORD_TOKEN and PICABLE_GITHUB_TOKEN")
exit(1) exit(1)
intents = Intents.default() intents = Intents.default()
intents.message_content = True intents.message_content = True
client = commands.Bot(command_prefix="/", intents=intents) client = commands.Bot(command_prefix="/", intents=intents)
client.run(discord_token)
@client.event @client.event
async def on_ready(): async def on_ready():
print(f"We have logged in as {client.user}") print(f"We have logged in as {client.user}")
await client.change_presence(activity=Game("with PIC ideas")) await client.change_presence(activity=Game("with PIC ideas"))
try: try:
@@ -40,13 +36,13 @@ if __name__ == "__main__":
print(f"Failed to sync commands: {e}") print(f"Failed to sync commands: {e}")
@client.tree.command(name="ping", description="Check the bot's latency.") @client.tree.command(name="ping", description="Check the bot's latency.")
async def ping(interaction: Interaction): async def ping(interaction: Interaction):
latency = client.latency * 1000 latency = client.latency * 1000
await interaction.response.send_message(f"Pong! Latency: {latency:.2f}ms") 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): async def handle_picable(owner: str, repository: str, repository_size_kb: int, github_token: str):
global current_storage_kb global current_storage_kb
async with storage_condition: async with storage_condition:
await storage_condition.wait_for(lambda: current_storage_kb + repository_size_kb <= MAX_STORAGE_KB) await storage_condition.wait_for(lambda: current_storage_kb + repository_size_kb <= MAX_STORAGE_KB)
@@ -56,9 +52,9 @@ if __name__ == "__main__":
return await to_thread(PICable, owner, repository, github_token) return await to_thread(PICable, owner, repository, github_token)
@client.tree.command(name="picable", description="Check if a Github repository is eligible for PIC.") @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") @app_commands.describe(owner="The owner of the repository", repository="The name of the repository")
async def picable(interaction: Interaction, owner: str, repository: str): async def picable(interaction: Interaction, owner: str, repository: str):
await interaction.response.defer(thinking=True) await interaction.response.defer(thinking=True)
repository_full_name = f"{owner}/{repository}" repository_full_name = f"{owner}/{repository}"
@@ -72,12 +68,12 @@ if __name__ == "__main__":
current_repositories.add(repository_full_name) current_repositories.add(repository_full_name)
url = f"https://api.github.com/repos/{owner}/{repository}" url = f"https://api.github.com/repos/{owner}/{repository}"
headers = {"Authorization": f"token {github_token}"} headers = {"Authorization": f"token {GITHUB_TOKEN}"}
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
if not response.status_code == 200: if not response.status_code == 200:
await interaction.followup.send( await interaction.followup.send(
f"Invalid Repository. Please check the repository name and owner and try again. :x:" "Invalid Repository. Please check the repository name and owner and try again. :x:"
) )
return return
@@ -95,7 +91,7 @@ if __name__ == "__main__":
global current_storage_kb global current_storage_kb
try: try:
result_future = create_task(handle_picable(owner, repository, repository_size_kb, github_token)) result_future = create_task(handle_picable(owner, repository, repository_size_kb, GITHUB_TOKEN))
try: try:
result = await wait_for(shield(result_future), timeout=600) result = await wait_for(shield(result_future), timeout=600)
await interaction.followup.send(result) await interaction.followup.send(result)
@@ -114,3 +110,7 @@ if __name__ == "__main__":
async with storage_condition: async with storage_condition:
current_storage_kb -= repository_size_kb current_storage_kb -= repository_size_kb
storage_condition.notify_all() storage_condition.notify_all()
if __name__ == "__main__":
client.run(DISCORD_TOKEN)