[feat] Heavy push to Docker
This commit is contained in:
parent
bfd5087aa9
commit
5d164ede9b
14 changed files with 1319 additions and 1646 deletions
|
@ -7,7 +7,8 @@
|
||||||
!/jarvis.png
|
!/jarvis.png
|
||||||
!/jarvis_small.png
|
!/jarvis_small.png
|
||||||
!/run.py
|
!/run.py
|
||||||
!/config.yaml
|
!/README.md
|
||||||
|
!/poetry.lock
|
||||||
# Needed for jarvis-compose
|
# Needed for jarvis-compose
|
||||||
!/.git
|
!/.git
|
||||||
|
|
||||||
|
|
8
.gitlab-ci.yml
Normal file
8
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
build dev:
|
||||||
|
stage: build
|
||||||
|
image: docker
|
||||||
|
before_script:
|
||||||
|
- echo "$ZEVARYX_REGISTRY_PASSWORD" | docker login -u "$ZEVARYX_REGISTRY_USERNAME" --password-stdin "$ZEVARYX_REGISTRY_URL"
|
||||||
|
script:
|
||||||
|
- docker build -t $ZEVARYX_REGISTRY_URL/jarvis-bot:$CI_COMMIT_BRANCH .
|
||||||
|
- docker push $ZEVARYX_REGISTRY_URL/jarvis-bot:$CI_COMMIT_BRANCH
|
33
Dockerfile
33
Dockerfile
|
@ -1,11 +1,34 @@
|
||||||
FROM python:3.10
|
FROM python:3.12-bookworm as builder
|
||||||
|
|
||||||
RUN apt-get update
|
RUN pip install poetry==1.7.1
|
||||||
RUN apt-get install ffmpeg libsm6 libxext6 -y
|
|
||||||
|
ENV POETRY_NO_INTERACTION=1 \
|
||||||
|
POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
||||||
|
POETRY_VIRTUALENVS_CREATE=1 \
|
||||||
|
POETRY_CACHE_DIR=/tmp/poetry_cache
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . /app
|
COPY pyproject.toml poetry.lock README.md ./
|
||||||
RUN pip install --no-cache-dir .
|
|
||||||
|
RUN poetry install --without dev --no-root && rm -rf $POETRY_CACHE_DIR
|
||||||
|
|
||||||
|
FROM python:3.12-slim-bookworm as runtime
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y \
|
||||||
|
libjpeg-dev \
|
||||||
|
libopenjp2-7-dev \
|
||||||
|
libgl-dev \
|
||||||
|
libglib2.0-dev
|
||||||
|
|
||||||
|
ENV VIRTUAL_ENV=/app/.venv \
|
||||||
|
PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||||
|
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
CMD [ "python", "run.py" ]
|
CMD [ "python", "run.py" ]
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
---
|
|
||||||
token: bot token
|
|
||||||
twitter:
|
|
||||||
consumer_key: key
|
|
||||||
consumer_secret: secret
|
|
||||||
access_token: access token
|
|
||||||
access_secret: access secret
|
|
||||||
mongo:
|
|
||||||
connect:
|
|
||||||
username: username
|
|
||||||
password: password
|
|
||||||
host: hostname
|
|
||||||
port: 27017
|
|
||||||
database: database
|
|
||||||
urls:
|
|
||||||
extra: urls
|
|
||||||
max_messages: 10000
|
|
||||||
gitlab_token: token
|
|
||||||
cogs:
|
|
||||||
- admin
|
|
||||||
- autoreact
|
|
||||||
- dev
|
|
||||||
- image
|
|
||||||
- gl
|
|
||||||
- remindme
|
|
||||||
- rolegiver
|
|
||||||
# - settings
|
|
||||||
- starboard
|
|
||||||
- twitter
|
|
||||||
- util
|
|
||||||
- verify
|
|
||||||
log_level: INFO
|
|
||||||
sync: false
|
|
||||||
#sync_commands: True
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""JARVIS bot utility commands."""
|
"""JARVIS bot utility commands."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
|
@ -13,10 +14,8 @@ from interactions.models.discord.components import Button
|
||||||
from interactions.models.discord.embed import EmbedField
|
from interactions.models.discord.embed import EmbedField
|
||||||
from interactions.models.discord.enums import ButtonStyle
|
from interactions.models.discord.enums import ButtonStyle
|
||||||
from interactions.models.discord.file import File
|
from interactions.models.discord.file import File
|
||||||
from rich.console import Console
|
|
||||||
|
|
||||||
from jarvis.utils import build_embed
|
from jarvis.utils import build_embed
|
||||||
from jarvis.utils.updates import update
|
|
||||||
|
|
||||||
|
|
||||||
class BotutilCog(Extension):
|
class BotutilCog(Extension):
|
||||||
|
@ -36,12 +35,6 @@ class BotutilCog(Extension):
|
||||||
await ctx.send(content)
|
await ctx.send(content)
|
||||||
await ctx.message.delete()
|
await ctx.message.delete()
|
||||||
|
|
||||||
@prefixed_command(name="stop")
|
|
||||||
async def _stop(self, ctx: PrefixedContext) -> None:
|
|
||||||
await ctx.send("Shutting down now")
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
loop.stop()
|
|
||||||
|
|
||||||
@prefixed_command(name="tail")
|
@prefixed_command(name="tail")
|
||||||
async def _tail(self, ctx: PrefixedContext, count: int = 10) -> None:
|
async def _tail(self, ctx: PrefixedContext, count: int = 10) -> None:
|
||||||
lines = []
|
lines = []
|
||||||
|
@ -56,7 +49,9 @@ class BotutilCog(Extension):
|
||||||
file_bytes.write(log.encode("UTF8"))
|
file_bytes.write(log.encode("UTF8"))
|
||||||
file_bytes.seek(0)
|
file_bytes.seek(0)
|
||||||
log = File(file_bytes, file_name=f"tail_{count}.log")
|
log = File(file_bytes, file_name=f"tail_{count}.log")
|
||||||
await ctx.reply(content=f"Here's the last {count} lines of the log", file=log)
|
await ctx.reply(
|
||||||
|
content=f"Here's the last {count} lines of the log", file=log
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.reply(content=f"```\n{log}\n```")
|
await ctx.reply(content=f"```\n{log}\n```")
|
||||||
|
|
||||||
|
@ -79,62 +74,25 @@ class BotutilCog(Extension):
|
||||||
st_ts = int(self.bot.start_time.timestamp())
|
st_ts = int(self.bot.start_time.timestamp())
|
||||||
ut_ts = int(psutil.boot_time())
|
ut_ts = int(psutil.boot_time())
|
||||||
fields = (
|
fields = (
|
||||||
EmbedField(name="Operation System", value=platform.system() or "Unknown", inline=False),
|
EmbedField(
|
||||||
|
name="Operation System",
|
||||||
|
value=platform.system() or "Unknown",
|
||||||
|
inline=False,
|
||||||
|
),
|
||||||
EmbedField(name="Version", value=platform.release() or "N/A", inline=False),
|
EmbedField(name="Version", value=platform.release() or "N/A", inline=False),
|
||||||
EmbedField(name="System Start Time", value=f"<t:{ut_ts}:F> (<t:{ut_ts}:R>)"),
|
EmbedField(
|
||||||
|
name="System Start Time", value=f"<t:{ut_ts}:F> (<t:{ut_ts}:R>)"
|
||||||
|
),
|
||||||
EmbedField(name="Python Version", value=platform.python_version()),
|
EmbedField(name="Python Version", value=platform.python_version()),
|
||||||
EmbedField(name="Bot Start Time", value=f"<t:{st_ts}:F> (<t:{st_ts}:R>)"),
|
EmbedField(name="Bot Start Time", value=f"<t:{st_ts}:F> (<t:{st_ts}:R>)"),
|
||||||
)
|
)
|
||||||
embed = build_embed(title="System Info", description="", fields=fields)
|
embed = build_embed(title="System Info", description="", fields=fields)
|
||||||
embed.set_image(url=self.bot.user.avatar.url)
|
embed.set_image(url=self.bot.user.avatar.url)
|
||||||
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
|
components = Button(
|
||||||
|
style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}"
|
||||||
|
)
|
||||||
await ctx.send(embeds=embed, components=components)
|
await ctx.send(embeds=embed, components=components)
|
||||||
|
|
||||||
@prefixed_command(name="update")
|
|
||||||
async def _update(self, ctx: PrefixedContext) -> None:
|
|
||||||
status = await update(self.bot)
|
|
||||||
if status:
|
|
||||||
console = Console()
|
|
||||||
with console.capture() as capture:
|
|
||||||
console.print(status.table)
|
|
||||||
self.logger.debug(capture.get())
|
|
||||||
self.logger.debug(len(capture.get()))
|
|
||||||
added = "\n".join(status.added)
|
|
||||||
removed = "\n".join(status.removed)
|
|
||||||
changed = "\n".join(status.changed)
|
|
||||||
|
|
||||||
fields = [
|
|
||||||
EmbedField(name="Old Commit", value=status.old_hash),
|
|
||||||
EmbedField(name="New Commit", value=status.new_hash),
|
|
||||||
]
|
|
||||||
if added:
|
|
||||||
fields.append(EmbedField(name="New Modules", value=f"```\n{added}\n```"))
|
|
||||||
if removed:
|
|
||||||
fields.append(EmbedField(name="Removed Modules", value=f"```\n{removed}\n```"))
|
|
||||||
if changed:
|
|
||||||
fields.append(EmbedField(name="Changed Modules", value=f"```\n{changed}\n```"))
|
|
||||||
|
|
||||||
embed = build_embed("Update Status", description="Updates have been applied", fields=fields)
|
|
||||||
embed.set_thumbnail(url="https://dev.zevaryx.com/git.png")
|
|
||||||
|
|
||||||
self.logger.info("Updates applied")
|
|
||||||
content = f"```ansi\n{capture.get()}\n```"
|
|
||||||
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
|
|
||||||
if len(content) < 3000:
|
|
||||||
await ctx.reply(content, embeds=embed, components=components)
|
|
||||||
else:
|
|
||||||
await ctx.reply(
|
|
||||||
f"Total Changes: {status.lines['total_lines']}",
|
|
||||||
embeds=embed,
|
|
||||||
components=components,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
embed = build_embed(title="Update Status", description="No changes applied", fields=[])
|
|
||||||
embed.set_thumbnail(url="https://dev.zevaryx.com/git.png")
|
|
||||||
components = Button(style=ButtonStyle.DANGER, emoji="🗑️", custom_id=f"delete|{ctx.author.id}")
|
|
||||||
await ctx.reply(embeds=embed, components=components)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Client) -> None:
|
def setup(bot: Client) -> None:
|
||||||
"""Add BotutilCog to JARVIS"""
|
"""Add BotutilCog to JARVIS"""
|
||||||
|
|
|
@ -36,7 +36,7 @@ from tzlocal import get_localzone
|
||||||
from jarvis import const as jconst
|
from jarvis import const as jconst
|
||||||
from jarvis.data import pigpen
|
from jarvis.data import pigpen
|
||||||
from jarvis.data.robotcamo import emotes, hk, names
|
from jarvis.data.robotcamo import emotes, hk, names
|
||||||
from jarvis.utils import build_embed, get_repo_hash
|
from jarvis.utils import build_embed
|
||||||
|
|
||||||
JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA")
|
JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA")
|
||||||
|
|
||||||
|
@ -148,14 +148,7 @@ Tips will be used to pay server costs, and any excess will go to local animal sh
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.bot.logger.debug("Getting repo information")
|
self.bot.logger.debug("Getting repo information")
|
||||||
repo_url = f"https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/-/tree/{get_repo_hash()}"
|
repo_url = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/"
|
||||||
fields.append(
|
|
||||||
EmbedField(
|
|
||||||
name="Git Hash",
|
|
||||||
value=f"[{get_repo_hash()[:7]}]({repo_url})",
|
|
||||||
inline=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
fields.append(
|
fields.append(
|
||||||
EmbedField(name="Online Since", value=f"<t:{uptime}:F>", inline=False)
|
EmbedField(name="Online Since", value=f"<t:{uptime}:F>", inline=False)
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,6 +11,7 @@ No longer being a part of the community and hearing about *certain users* talkin
|
||||||
that request said features, means that I am no longer putting any energy into this. If you want to keep
|
that request said features, means that I am no longer putting any energy into this. If you want to keep
|
||||||
it running, make a PR to fix things.
|
it running, make a PR to fix things.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
@ -97,7 +98,7 @@ class DbrandCog(Extension):
|
||||||
self.base_url = "https://dbrand.com/"
|
self.base_url = "https://dbrand.com/"
|
||||||
self._session = aiohttp.ClientSession()
|
self._session = aiohttp.ClientSession()
|
||||||
self._session.headers.update({"Content-Type": "application/json"})
|
self._session.headers.update({"Content-Type": "application/json"})
|
||||||
self.api_url = load_config().urls["dbrand_shipping"]
|
# self.api_url = load_config().urls["dbrand_shipping"]
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
@ -401,7 +402,4 @@ class DbrandCog(Extension):
|
||||||
|
|
||||||
def setup(bot: Client) -> None:
|
def setup(bot: Client) -> None:
|
||||||
"""Add dbrandcog to JARVIS"""
|
"""Add dbrandcog to JARVIS"""
|
||||||
if load_config().urls.get("dbrand_shipping"):
|
DbrandCog(bot)
|
||||||
DbrandCog(bot)
|
|
||||||
else:
|
|
||||||
bot.logger.info("Missing dbrand shipping URL, not loading dbrand cog")
|
|
||||||
|
|
124
jarvis/config.py
124
jarvis/config.py
|
@ -1,22 +1,12 @@
|
||||||
"""Load the config for JARVIS"""
|
"""Load the config for JARVIS"""
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from os import environ
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import orjson as json
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
import yaml
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from jarvis_core.util import find_all
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
try:
|
|
||||||
from yaml import CLoader as Loader
|
|
||||||
except ImportError:
|
|
||||||
from yaml import Loader
|
|
||||||
|
|
||||||
|
|
||||||
class Mongo(BaseModel):
|
class Mongo(BaseSettings):
|
||||||
"""MongoDB config."""
|
"""MongoDB config."""
|
||||||
|
|
||||||
host: list[str] | str = "localhost"
|
host: list[str] | str = "localhost"
|
||||||
|
@ -25,7 +15,7 @@ class Mongo(BaseModel):
|
||||||
port: int = 27017
|
port: int = 27017
|
||||||
|
|
||||||
|
|
||||||
class Redis(BaseModel):
|
class Redis(BaseSettings):
|
||||||
"""Redis config."""
|
"""Redis config."""
|
||||||
|
|
||||||
host: str = "localhost"
|
host: str = "localhost"
|
||||||
|
@ -33,7 +23,7 @@ class Redis(BaseModel):
|
||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Mastodon(BaseModel):
|
class Mastodon(BaseSettings):
|
||||||
"""Mastodon config."""
|
"""Mastodon config."""
|
||||||
|
|
||||||
token: str
|
token: str
|
||||||
|
@ -47,7 +37,7 @@ class Environment(Enum):
|
||||||
develop = "develop"
|
develop = "develop"
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseSettings, case_sensitive=False):
|
||||||
"""JARVIS config model."""
|
"""JARVIS config model."""
|
||||||
|
|
||||||
token: str
|
token: str
|
||||||
|
@ -55,109 +45,19 @@ class Config(BaseModel):
|
||||||
erapi: str
|
erapi: str
|
||||||
"""exchangerate-api.org API token"""
|
"""exchangerate-api.org API token"""
|
||||||
environment: Environment = Environment.develop
|
environment: Environment = Environment.develop
|
||||||
mongo: Mongo
|
mongo: Mongo = Mongo()
|
||||||
redis: Redis
|
redis: Redis = Redis()
|
||||||
mastodon: Optional[Mastodon] = None
|
|
||||||
urls: Optional[dict[str, str]] = None
|
|
||||||
sync: bool = False
|
sync: bool = False
|
||||||
log_level: str = "INFO"
|
log_level: str = "INFO"
|
||||||
jurigged: bool = False
|
jurigged: bool = False
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
_config: Config = None
|
env_file=".env", env_file_encoding="utf-8", env_nested_delimiter="."
|
||||||
|
|
||||||
|
|
||||||
def _load_json() -> Config | None:
|
|
||||||
path = Path("config.json")
|
|
||||||
config = None
|
|
||||||
if path.exists():
|
|
||||||
with path.open() as f:
|
|
||||||
j = json.loads(f.read())
|
|
||||||
config = Config(**j)
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def _load_yaml() -> Config | None:
|
|
||||||
path = Path("config.yaml")
|
|
||||||
config = None
|
|
||||||
if path.exists():
|
|
||||||
with path.open() as f:
|
|
||||||
y = yaml.load(f.read(), Loader=Loader)
|
|
||||||
config = Config(**y)
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def _load_env() -> Config | None:
|
|
||||||
load_dotenv()
|
|
||||||
data = {}
|
|
||||||
mongo = {}
|
|
||||||
redis = {}
|
|
||||||
mastodon = {}
|
|
||||||
urls = {}
|
|
||||||
mongo_keys = find_all(lambda x: x.upper().startswith("MONGO"), environ.keys())
|
|
||||||
redis_keys = find_all(lambda x: x.upper().startswith("REDIS"), environ.keys())
|
|
||||||
mastodon_keys = find_all(lambda x: x.upper().startswith("MASTODON"), environ.keys())
|
|
||||||
url_keys = find_all(lambda x: x.upper().startswith("URLS"), environ.keys())
|
|
||||||
|
|
||||||
config_keys = (
|
|
||||||
mongo_keys
|
|
||||||
+ redis_keys
|
|
||||||
+ mastodon_keys
|
|
||||||
+ url_keys
|
|
||||||
+ ["TOKEN", "SYNC", "LOG_LEVEL", "JURIGGED"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for item, value in environ.items():
|
|
||||||
if item not in config_keys:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if item in mongo_keys:
|
def load_config() -> Config:
|
||||||
key = "_".join(item.split("_")[1:]).lower()
|
|
||||||
mongo[key] = value
|
|
||||||
elif item in redis_keys:
|
|
||||||
key = "_".join(item.split("_")[1:]).lower()
|
|
||||||
redis[key] = value
|
|
||||||
elif item in mastodon_keys:
|
|
||||||
key = "_".join(item.split("_")[1:]).lower()
|
|
||||||
mastodon[key] = value
|
|
||||||
elif item in url_keys:
|
|
||||||
key = "_".join(item.split("_")[1:]).lower()
|
|
||||||
urls[key] = value
|
|
||||||
else:
|
|
||||||
if item == "SYNC":
|
|
||||||
value = value.lower() in ["yes", "true"]
|
|
||||||
data[item.lower()] = value
|
|
||||||
|
|
||||||
data["mongo"] = mongo
|
|
||||||
data["redis"] = redis
|
|
||||||
if all(x is not None for x in mastodon.values()):
|
|
||||||
data["mastodon"] = mastodon
|
|
||||||
data["urls"] = {k: v for k, v in urls if v}
|
|
||||||
|
|
||||||
return Config(**data)
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(method: Optional[str] = None) -> Config:
|
|
||||||
"""
|
"""
|
||||||
Load the config using the specified method first
|
Load the config using the specified method first
|
||||||
|
|
||||||
Args:
|
|
||||||
method: Method to use first
|
|
||||||
"""
|
"""
|
||||||
global _config
|
return Config()
|
||||||
if _config is not None:
|
|
||||||
return _config
|
|
||||||
|
|
||||||
methods = {"yaml": _load_yaml, "json": _load_json, "env": _load_env}
|
|
||||||
method_names = list(methods.keys())
|
|
||||||
if method and method in method_names:
|
|
||||||
method_names.remove(method)
|
|
||||||
method_names.insert(0, method)
|
|
||||||
|
|
||||||
for method in method_names:
|
|
||||||
if _config := methods[method]():
|
|
||||||
return _config
|
|
||||||
|
|
||||||
raise FileNotFoundError("Missing one of: config.yaml, config.json, .env")
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
"""JARVIS constants."""
|
"""JARVIS constants."""
|
||||||
|
|
||||||
from importlib.metadata import version as _v
|
from importlib.metadata import version as _v
|
||||||
|
|
||||||
try:
|
__version__ = "2.5.2"
|
||||||
__version__ = _v("jarvis")
|
|
||||||
except Exception:
|
|
||||||
__version__ = "0.0.0"
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pkgutil import iter_modules
|
from pkgutil import iter_modules
|
||||||
|
|
||||||
import git
|
|
||||||
from interactions.models.discord.embed import Embed, EmbedField
|
from interactions.models.discord.embed import Embed, EmbedField
|
||||||
from interactions.models.discord.guild import AuditLogEntry
|
from interactions.models.discord.guild import AuditLogEntry
|
||||||
from interactions.models.discord.user import Member
|
from interactions.models.discord.user import Member
|
||||||
|
@ -67,24 +66,3 @@ def get_extensions(path: str) -> list:
|
||||||
"""Get JARVIS cogs."""
|
"""Get JARVIS cogs."""
|
||||||
vals = [x.name for x in iter_modules(path)]
|
vals = [x.name for x in iter_modules(path)]
|
||||||
return [f"jarvis.cogs.{x}" for x in vals]
|
return [f"jarvis.cogs.{x}" for x in vals]
|
||||||
|
|
||||||
|
|
||||||
def update() -> int:
|
|
||||||
"""JARVIS update utility."""
|
|
||||||
repo = git.Repo(".")
|
|
||||||
dirty = repo.is_dirty()
|
|
||||||
current_hash = repo.head.object.hexsha
|
|
||||||
origin = repo.remotes.origin
|
|
||||||
origin.fetch()
|
|
||||||
if current_hash != origin.refs["main"].object.hexsha:
|
|
||||||
if dirty:
|
|
||||||
return 2
|
|
||||||
origin.pull()
|
|
||||||
return 0
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def get_repo_hash() -> str:
|
|
||||||
"""JARVIS current branch hash."""
|
|
||||||
repo = git.Repo(".")
|
|
||||||
return repo.head.object.hexsha
|
|
||||||
|
|
|
@ -1,27 +1,19 @@
|
||||||
"""JARVIS update handler."""
|
"""JARVIS update handler."""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from inspect import getmembers, isclass
|
from inspect import getmembers, isclass
|
||||||
from pkgutil import iter_modules
|
from pkgutil import iter_modules
|
||||||
from types import FunctionType, ModuleType
|
from types import FunctionType, ModuleType
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List
|
||||||
|
|
||||||
import git
|
from interactions.client.utils.misc_utils import find_all
|
||||||
from interactions.client.errors import ExtensionNotFound
|
|
||||||
from interactions.client.utils.misc_utils import find, find_all
|
|
||||||
from interactions.models.internal.application_commands import SlashCommand
|
from interactions.models.internal.application_commands import SlashCommand
|
||||||
from interactions.models.internal.extension import Extension
|
from interactions.models.internal.extension import Extension
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
import jarvis.cogs
|
import jarvis.cogs
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from interactions.client.client import Client
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UpdateResult:
|
class UpdateResult:
|
||||||
|
@ -61,153 +53,3 @@ def get_all_commands(module: ModuleType = jarvis.cogs) -> Dict[str, Callable]:
|
||||||
values = cog.__dict__.values()
|
values = cog.__dict__.values()
|
||||||
commands[cog.__module__] = find_all(lambda x: isinstance(x, SlashCommand), values)
|
commands[cog.__module__] = find_all(lambda x: isinstance(x, SlashCommand), values)
|
||||||
return {k: v for k, v in commands.items() if v}
|
return {k: v for k, v in commands.items() if v}
|
||||||
|
|
||||||
|
|
||||||
def get_git_changes(repo: git.Repo) -> dict:
|
|
||||||
"""Get all Git changes"""
|
|
||||||
logger = _logger
|
|
||||||
logger.debug("Getting all git changes")
|
|
||||||
current_hash = repo.head.ref.object.hexsha
|
|
||||||
tracking = repo.head.ref.tracking_branch()
|
|
||||||
|
|
||||||
file_changes = {}
|
|
||||||
for commit in tracking.commit.iter_items(repo, f"{repo.head.ref.path}..{tracking.path}"):
|
|
||||||
if commit.hexsha == current_hash:
|
|
||||||
break
|
|
||||||
files = commit.stats.files
|
|
||||||
file_changes.update({key: {"insertions": 0, "deletions": 0, "lines": 0} for key in files.keys()})
|
|
||||||
for file, stats in files.items():
|
|
||||||
if file not in file_changes:
|
|
||||||
file_changes[file] = {"insertions": 0, "deletions": 0, "lines": 0}
|
|
||||||
for key, val in stats.items():
|
|
||||||
file_changes[file][key] += val
|
|
||||||
logger.debug("Found %i changed files", len(file_changes))
|
|
||||||
|
|
||||||
table = Table(title="File Changes")
|
|
||||||
|
|
||||||
table.add_column("File", justify="left", style="white", no_wrap=True)
|
|
||||||
table.add_column("Insertions", justify="center", style="green")
|
|
||||||
table.add_column("Deletions", justify="center", style="red")
|
|
||||||
table.add_column("Lines", justify="center", style="magenta")
|
|
||||||
|
|
||||||
i_total = 0
|
|
||||||
d_total = 0
|
|
||||||
l_total = 0
|
|
||||||
for file, stats in file_changes.items():
|
|
||||||
i_total += stats["insertions"]
|
|
||||||
d_total += stats["deletions"]
|
|
||||||
l_total += stats["lines"]
|
|
||||||
table.add_row(
|
|
||||||
file,
|
|
||||||
str(stats["insertions"]),
|
|
||||||
str(stats["deletions"]),
|
|
||||||
str(stats["lines"]),
|
|
||||||
)
|
|
||||||
logger.debug("%i insertions, %i deletions, %i total", i_total, d_total, l_total)
|
|
||||||
|
|
||||||
table.add_row("Total", str(i_total), str(d_total), str(l_total))
|
|
||||||
return {
|
|
||||||
"table": table,
|
|
||||||
"lines": {"inserted_lines": i_total, "deleted_lines": d_total, "total_lines": l_total},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def update(bot: "Client") -> Optional[UpdateResult]:
|
|
||||||
"""
|
|
||||||
Update JARVIS and return an UpdateResult.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bot: Bot instance
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
UpdateResult object
|
|
||||||
"""
|
|
||||||
logger = _logger
|
|
||||||
repo = git.Repo(".")
|
|
||||||
current_hash = repo.head.object.hexsha
|
|
||||||
origin = repo.remotes.origin
|
|
||||||
origin.fetch()
|
|
||||||
remote_hash = origin.refs[repo.active_branch.name].object.hexsha
|
|
||||||
|
|
||||||
if current_hash != remote_hash:
|
|
||||||
logger.info("Updating from %s to %s", current_hash, remote_hash)
|
|
||||||
current_commands = get_all_commands()
|
|
||||||
changes = get_git_changes(repo)
|
|
||||||
|
|
||||||
origin.pull()
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
|
|
||||||
new_commands = get_all_commands()
|
|
||||||
|
|
||||||
logger.info("Checking if any modules need reloaded...")
|
|
||||||
|
|
||||||
reloaded = []
|
|
||||||
loaded = []
|
|
||||||
unloaded = []
|
|
||||||
|
|
||||||
logger.debug("Checking for removed cogs")
|
|
||||||
for module in current_commands.keys():
|
|
||||||
if module not in new_commands:
|
|
||||||
logger.debug("Module %s removed after update", module)
|
|
||||||
bot.unload_extension(module)
|
|
||||||
unloaded.append(module)
|
|
||||||
|
|
||||||
logger.debug("Checking for new/modified commands")
|
|
||||||
for module, commands in new_commands.items():
|
|
||||||
logger.debug("Processing %s", module)
|
|
||||||
if module not in current_commands:
|
|
||||||
bot.load_extension(module)
|
|
||||||
loaded.append(module)
|
|
||||||
elif len(current_commands[module]) != len(commands):
|
|
||||||
try:
|
|
||||||
bot.reload_extension(module)
|
|
||||||
except ExtensionNotFound:
|
|
||||||
bot.load_extension(module)
|
|
||||||
reloaded.append(module)
|
|
||||||
else:
|
|
||||||
for command in commands:
|
|
||||||
old_command = find(lambda x: x.resolved_name == command.resolved_name, current_commands[module])
|
|
||||||
|
|
||||||
# Extract useful info
|
|
||||||
old_args = old_command.options
|
|
||||||
if old_args:
|
|
||||||
old_arg_names = [x.name for x in old_args]
|
|
||||||
new_args = command.options
|
|
||||||
if new_args:
|
|
||||||
new_arg_names = [x.name for x in new_args]
|
|
||||||
|
|
||||||
# No changes
|
|
||||||
if not old_args and not new_args:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if number arguments have changed
|
|
||||||
if len(old_args) != len(new_args):
|
|
||||||
try:
|
|
||||||
bot.reload_extension(module)
|
|
||||||
except ExtensionNotFound:
|
|
||||||
bot.load_extension(module)
|
|
||||||
reloaded.append(module)
|
|
||||||
elif any(x not in old_arg_names for x in new_arg_names) or any(
|
|
||||||
x not in new_arg_names for x in old_arg_names
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
bot.reload_extension(module)
|
|
||||||
except ExtensionNotFound:
|
|
||||||
bot.load_extension(module)
|
|
||||||
reloaded.append(module)
|
|
||||||
elif any(new_args[idx].type != x.type for idx, x in enumerate(old_args)):
|
|
||||||
try:
|
|
||||||
bot.reload_extension(module)
|
|
||||||
except ExtensionNotFound:
|
|
||||||
bot.load_extension(module)
|
|
||||||
reloaded.append(module)
|
|
||||||
|
|
||||||
return UpdateResult(
|
|
||||||
old_hash=current_hash,
|
|
||||||
new_hash=remote_hash,
|
|
||||||
added=loaded,
|
|
||||||
removed=unloaded,
|
|
||||||
changed=reloaded,
|
|
||||||
**changes,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
2457
poetry.lock
generated
2457
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "jarvis"
|
name = "jarvis"
|
||||||
version = "2.5.2"
|
version = "2.5.3"
|
||||||
description = "JARVIS admin bot"
|
description = "JARVIS admin bot"
|
||||||
authors = ["Zevaryx <zevaryx@gmail.com>"]
|
authors = ["Zevaryx <zevaryx@gmail.com>"]
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ pydantic = ">=2.3.0,<3"
|
||||||
orjson = "^3.8.8"
|
orjson = "^3.8.8"
|
||||||
croniter = "^1.4.1"
|
croniter = "^1.4.1"
|
||||||
erapi = { git = "https://git.zevaryx.com/zevaryx-technologies/erapi.git" }
|
erapi = { git = "https://git.zevaryx.com/zevaryx-technologies/erapi.git" }
|
||||||
|
pydantic-settings = "^2.2.1"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pre-commit = "^2.21.0"
|
pre-commit = "^2.21.0"
|
||||||
|
|
20
sample.env
20
sample.env
|
@ -1,5 +1,6 @@
|
||||||
# Base Config, required
|
# Base Config, required
|
||||||
TOKEN=
|
TOKEN=
|
||||||
|
ERAPI=
|
||||||
|
|
||||||
# Base Config, optional
|
# Base Config, optional
|
||||||
ENVIRONMENT=develop
|
ENVIRONMENT=develop
|
||||||
|
@ -17,22 +18,3 @@ MONGO_PORT=27017
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_USERNAME=
|
REDIS_USERNAME=
|
||||||
REDIS_PASSWORD=
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
# Mastodon, optional
|
|
||||||
MASTODON_TOKEN=
|
|
||||||
MASTODON_URL=
|
|
||||||
|
|
||||||
# Reddit, optional
|
|
||||||
REDDIT_USER_AGENT=
|
|
||||||
REDDIT_CLIENT_SECRET=
|
|
||||||
REDDIT_CLIENT_ID=
|
|
||||||
|
|
||||||
# Twitter, optional
|
|
||||||
TWITTER_CONSUMER_KEY=
|
|
||||||
TWITTER_CONSUMER_SECRET=
|
|
||||||
TWITTER_ACCESS_TOKEN=
|
|
||||||
TWITTER_ACCESS_SECRET=
|
|
||||||
TWITTER_BEARER_TOKEN=
|
|
||||||
|
|
||||||
# URLs, optional
|
|
||||||
URL_DBRAND=
|
|
Loading…
Add table
Reference in a new issue