[feat] Heavy push to Docker

This commit is contained in:
Zeva Rose 2024-03-04 17:35:59 -07:00
parent bfd5087aa9
commit 5d164ede9b
14 changed files with 1319 additions and 1646 deletions

View file

@ -7,7 +7,8 @@
!/jarvis.png
!/jarvis_small.png
!/run.py
!/config.yaml
!/README.md
!/poetry.lock
# Needed for jarvis-compose
!/.git

8
.gitlab-ci.yml Normal file
View 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

View file

@ -1,11 +1,34 @@
FROM python:3.10
FROM python:3.12-bookworm as builder
RUN apt-get update
RUN apt-get install ffmpeg libsm6 libxext6 -y
RUN pip install poetry==1.7.1
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir .
COPY pyproject.toml poetry.lock README.md ./
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" ]

View file

@ -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

View file

@ -1,4 +1,5 @@
"""JARVIS bot utility commands."""
import asyncio
import logging
import platform
@ -13,10 +14,8 @@ from interactions.models.discord.components import Button
from interactions.models.discord.embed import EmbedField
from interactions.models.discord.enums import ButtonStyle
from interactions.models.discord.file import File
from rich.console import Console
from jarvis.utils import build_embed
from jarvis.utils.updates import update
class BotutilCog(Extension):
@ -36,12 +35,6 @@ class BotutilCog(Extension):
await ctx.send(content)
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")
async def _tail(self, ctx: PrefixedContext, count: int = 10) -> None:
lines = []
@ -56,7 +49,9 @@ class BotutilCog(Extension):
file_bytes.write(log.encode("UTF8"))
file_bytes.seek(0)
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:
await ctx.reply(content=f"```\n{log}\n```")
@ -79,62 +74,25 @@ class BotutilCog(Extension):
st_ts = int(self.bot.start_time.timestamp())
ut_ts = int(psutil.boot_time())
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="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="Bot Start Time", value=f"<t:{st_ts}:F> (<t:{st_ts}:R>)"),
)
embed = build_embed(title="System Info", description="", fields=fields)
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)
@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:
"""Add BotutilCog to JARVIS"""

View file

@ -36,7 +36,7 @@ from tzlocal import get_localzone
from jarvis import const as jconst
from jarvis.data import pigpen
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")
@ -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")
repo_url = f"https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/-/tree/{get_repo_hash()}"
fields.append(
EmbedField(
name="Git Hash",
value=f"[{get_repo_hash()[:7]}]({repo_url})",
inline=True,
)
)
repo_url = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-bot/"
fields.append(
EmbedField(name="Online Since", value=f"<t:{uptime}:F>", inline=False)
)

View file

@ -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
it running, make a PR to fix things.
"""
import logging
import re
from datetime import datetime, timedelta, timezone
@ -97,7 +98,7 @@ class DbrandCog(Extension):
self.base_url = "https://dbrand.com/"
self._session = aiohttp.ClientSession()
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 = {}
def __del__(self):
@ -401,7 +402,4 @@ class DbrandCog(Extension):
def setup(bot: Client) -> None:
"""Add dbrandcog to JARVIS"""
if load_config().urls.get("dbrand_shipping"):
DbrandCog(bot)
else:
bot.logger.info("Missing dbrand shipping URL, not loading dbrand cog")
DbrandCog(bot)

View file

@ -1,22 +1,12 @@
"""Load the config for JARVIS"""
from enum import Enum
from os import environ
from pathlib import Path
from typing import Optional
import orjson as json
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
from pydantic_settings import BaseSettings, SettingsConfigDict
class Mongo(BaseModel):
class Mongo(BaseSettings):
"""MongoDB config."""
host: list[str] | str = "localhost"
@ -25,7 +15,7 @@ class Mongo(BaseModel):
port: int = 27017
class Redis(BaseModel):
class Redis(BaseSettings):
"""Redis config."""
host: str = "localhost"
@ -33,7 +23,7 @@ class Redis(BaseModel):
password: Optional[str] = None
class Mastodon(BaseModel):
class Mastodon(BaseSettings):
"""Mastodon config."""
token: str
@ -47,7 +37,7 @@ class Environment(Enum):
develop = "develop"
class Config(BaseModel):
class Config(BaseSettings, case_sensitive=False):
"""JARVIS config model."""
token: str
@ -55,109 +45,19 @@ class Config(BaseModel):
erapi: str
"""exchangerate-api.org API token"""
environment: Environment = Environment.develop
mongo: Mongo
redis: Redis
mastodon: Optional[Mastodon] = None
urls: Optional[dict[str, str]] = None
mongo: Mongo = Mongo()
redis: Redis = Redis()
sync: bool = False
log_level: str = "INFO"
jurigged: bool = False
_config: Config = None
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"]
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", env_nested_delimiter="."
)
for item, value in environ.items():
if item not in config_keys:
continue
if item in mongo_keys:
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:
def load_config() -> Config:
"""
Load the config using the specified method first
Args:
method: Method to use first
"""
global _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")
return Config()

View file

@ -1,7 +1,5 @@
"""JARVIS constants."""
from importlib.metadata import version as _v
try:
__version__ = _v("jarvis")
except Exception:
__version__ = "0.0.0"
__version__ = "2.5.2"

View file

@ -2,7 +2,6 @@
from datetime import datetime, timezone
from pkgutil import iter_modules
import git
from interactions.models.discord.embed import Embed, EmbedField
from interactions.models.discord.guild import AuditLogEntry
from interactions.models.discord.user import Member
@ -66,25 +65,4 @@ def modlog_embed(
def get_extensions(path: str) -> list:
"""Get JARVIS cogs."""
vals = [x.name for x in iter_modules(path)]
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
return [f"jarvis.cogs.{x}" for x in vals]

View file

@ -1,27 +1,19 @@
"""JARVIS update handler."""
import asyncio
import logging
from dataclasses import dataclass
from importlib import import_module
from inspect import getmembers, isclass
from pkgutil import iter_modules
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.errors import ExtensionNotFound
from interactions.client.utils.misc_utils import find, find_all
from interactions.client.utils.misc_utils import find_all
from interactions.models.internal.application_commands import SlashCommand
from interactions.models.internal.extension import Extension
from rich.table import Table
import jarvis.cogs
if TYPE_CHECKING:
from interactions.client.client import Client
_logger = logging.getLogger(__name__)
@dataclass
class UpdateResult:
@ -61,153 +53,3 @@ def get_all_commands(module: ModuleType = jarvis.cogs) -> Dict[str, Callable]:
values = cog.__dict__.values()
commands[cog.__module__] = find_all(lambda x: isinstance(x, SlashCommand), values)
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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "jarvis"
version = "2.5.2"
version = "2.5.3"
description = "JARVIS admin bot"
authors = ["Zevaryx <zevaryx@gmail.com>"]
@ -37,6 +37,7 @@ pydantic = ">=2.3.0,<3"
orjson = "^3.8.8"
croniter = "^1.4.1"
erapi = { git = "https://git.zevaryx.com/zevaryx-technologies/erapi.git" }
pydantic-settings = "^2.2.1"
[tool.poetry.group.dev.dependencies]
pre-commit = "^2.21.0"

View file

@ -1,5 +1,6 @@
# Base Config, required
TOKEN=
ERAPI=
# Base Config, optional
ENVIRONMENT=develop
@ -17,22 +18,3 @@ MONGO_PORT=27017
REDIS_HOST=localhost
REDIS_USERNAME=
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=