Compare commits

..

No commits in common. "master" and "adbbc6352b3bc7b1dcfc034b38aee848efb2121f" have entirely different histories.

28 changed files with 129 additions and 819 deletions

3
.gitignore vendored
View file

@ -1,2 +1 @@
/config.json
/data/
config.json

View file

@ -1,78 +0,0 @@
# Hrochobot
Discord bot for KSP discord server.
## Installation
First install all required libraries:
```sh
pip install -r requirements.txt
```
Then create ``data`` folder from ``data.example``:
```sh
cp data.example data -r
```
Same for ``config.json``:
```sh
cp config.example.json config.json -r
```
Lastly paste your discord bot token into ``config.json``.
If you do not have any discord bot, learn how to create one here:
https://discordjs.guide/preparations/setting-up-a-bot-application.html#your-bot-s-token
## Running
To run the bot simply execute ``main.py``:
```sh
./main.py
```
## Commands
### Basic
#### `/sayhello`
Prints ``Hello world!``
#### `/ping`
Shows the bot's latency.
### Messages
#### `/set_forward_channel <#channel>`
Sets channel to forward messages to.
#### `>forward`
Forwards message to set channel
### Roles
#### `/secretroles`
Manages roles locked behind a password.
Lock ``@role`` behind a password ``supersecret``:
```
/secretroles add @role supersecret
```
Delete password ``supersecret``, so role is no longer obtainable with it:
```
/secretroles delete supersecret
```
List all current passwords and their roles:
```
/secretroles list
```
### KSP
Ksp related commands
#### `/task`
Generates urls for given task.
#### `/deadlines`
Shows deadlines of currently running series.
### News
#### `/news set_channel <#channel>`
Set channel for posting news.
#### `/news post_news <id>`
Post news with given `id` to set channel.

View file

@ -1,30 +0,0 @@
#!/usr/bin/env python3
from discord.ext import commands
import hrochobot.utils.data as data
import logging
import os
data.DATA_FOLDER = os.environ.get("HROCHOBOT_DATA", 'data')
LOG_FOLDER = os.environ.get("HROCHOBOT_LOG", '.')
CONFIG_FOLDER = os.environ.get("HROCHOBOT_ETC", '.')
logger = logging.getLogger('hrochobot')
logger.setLevel(logging.INFO)
handler = logging.FileHandler(filename=os.path.join(LOG_FOLDER, 'hrochobot.log'), encoding='utf-8', mode='w')
handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
logger.addHandler(handler)
CONFIG = data.load_json(os.path.join(CONFIG_FOLDER, "config"))
bot = commands.Bot()
cogs_list = CONFIG["enabled_cogs"]
for cog in cogs_list:
bot.load_extension(f'hrochobot.cogs.{cog}')
@bot.listen('on_interaction')
async def statistics(interaction):
logger.info(f"{interaction.user} ({interaction.user.id}) used command {interaction.data['name']}.")
bot.run(CONFIG["token"])

View file

@ -1,12 +0,0 @@
progress "Installing Hrochobot"
echo "Creating virtual environment"
install-pkgs python3-venv
python3 -m venv /srv/hrochobot
. /srv/hrochobot/bin/activate
echo "Installing package"
cd /build/src
pip install .

View file

@ -1,16 +0,0 @@
[Unit]
Description="Hrochobot"
After=network.target
[Service]
Type=exec
ExecStartPre=mkdir -p /data/hrochobot
ExecStart=/srv/hrochobot/bin/hrochobot
Environment=HROCHOBOT_DATA=/data/hrochobot
Environment=HROCHOBOT_ETC=/data/hrochobot
Environment=HROCHOBOT_LOG=/data/log
Restart=on-failure
RestartSec=5min
[Install]
WantedBy=multi-user.target

View file

@ -1,4 +0,0 @@
progress "Configuring hrochobot.service"
install-config hrochobot.service /etc/systemd/system/
systemctl enable hrochobot

View file

@ -1,4 +0,0 @@
FROM docker://registry.ks.matfyz.cz/gimli/base:bookworm
COPY bin /build/src/bin
COPY hrochobot /build/src/hrochobot
COPY setup.py /build/src

View file

@ -1,6 +0,0 @@
#!/bin/bash
set -e
common/build cobuild --tag hrochobot-test
podman rm -if hrochobot-test
podman create --name hrochobot-test --hostname hrochobot-test hrochobot-test
podman start hrochobot-test

View file

@ -5,11 +5,11 @@ class Basic(commands.Cog):
def __init__(self, bot):
self.bot = bot
@discord.slash_command(description="Greets the world.")
@discord.slash_command()
async def sayhello(self, ctx):
await ctx.respond('Hello world!')
@discord.slash_command(description="Sends the bot's latency.")
@discord.slash_command()
async def ping(self, ctx):
await ctx.respond('My ping is {:.0f}ms'.format(self.bot.latency*1000), ephemeral=True)

19
cogs/ksp.py Normal file
View file

@ -0,0 +1,19 @@
import discord
from discord.ext import commands
from utils.ksp_utils import *
class Ksp(commands.Cog):
def __init__(self, bot):
self.bot = bot
@discord.slash_command()
async def task(self, ctx, task_code):
await ctx.respond(
f'**{task_code}**\n'
f'Task: {task_link(task_code, solution=False)}\n'
f'Solution: {task_link(task_code, solution=True)}',
ephemeral=True
)
def setup(bot):
bot.add_cog(Ksp(bot))

65
cogs/roles.py Normal file
View file

@ -0,0 +1,65 @@
import discord
import utils.data as data
from discord.ext import commands
from discord.utils import get
ROLES_JSON = "roles"
class Roles(commands.Cog):
def __init__(self, bot):
self.bot = bot
secret_roles = discord.SlashCommandGroup(
"secretroles",
checks=[commands.has_permissions(manage_roles=True)]
)
@secret_roles.command()
async def add(self, ctx, role: discord.role.Role, password: str):
roles = data.load_data(ROLES_JSON)
if password in roles["secret_roles"]:
return await ctx.respond(f"Password ``{password}`` is already used.", ephemeral=True)
roles["secret_roles"][password] = role.id
data.dump_data(ROLES_JSON, roles)
return await ctx.respond(f"Secret role {role.mention} added with password {password}.", ephemeral=True)
@secret_roles.command()
async def list(self, ctx):
roles = data.load_data(ROLES_JSON)
if len(roles["secret_roles"]) == 0:
return await ctx.respond(f"No current secret roles.", ephemeral=True)
msg = ""
for passwd, role in roles["secret_roles"].items():
role = get(ctx.author.guild.roles, id=role)
msg += f"``{passwd}``: {role.mention}\n"
return await ctx.respond(msg, ephemeral=True)
@secret_roles.command()
async def delete(self, ctx, password: str):
roles = data.load_data(ROLES_JSON)
if password not in roles["secret_roles"]:
return await ctx.respond(f"Role with passowrd {password} does not exist.", ephemeral=True)
role = get(ctx.author.guild.roles, id=roles["secret_roles"][password])
del roles["secret_roles"][password]
data.dump_data(ROLES_JSON, roles)
return await ctx.respond(
f"Secret role {role.mention} no longer obtainable with password {password}.",
ephemeral=True
)
@discord.slash_command()
async def secretrole(self, ctx, password: str):
roles = data.load_data(ROLES_JSON)
if password in roles["secret_roles"]:
author = ctx.author
role = get(author.guild.roles, id=roles["secret_roles"][password])
await author.add_roles(role, reason="Roles assigned for password knowledge.")
return await ctx.respond(f"You now have role {role.mention}.", ephemeral=True)
else:
return await ctx.respond("Incorrect password.", ephemeral=True)
def setup(bot):
bot.add_cog(Roles(bot))

View file

@ -1,73 +0,0 @@
#!/bin/bash
set -euo pipefail
gen-docker-file ()
{
if [ -f $src/Dockerfile.top ] ; then
cat $src/Dockerfile.top
fi
echo "COPY common /build/common"
for stage in $(cd $src && echo [0-9]*.[a-z]*) ; do
case $stage in
*.docker)
cat $src/$stage
;;
*.d|*.sh)
echo "COPY $src/$stage /build/$stage"
echo "RUN /build/common/run $stage"
;;
*)
echo >&2 "ERROR: Unrecognized build stage name $stage"
exit 1
;;
esac
done
if [ -f $src/Dockerfile.bottom ] ; then
cat $src/Dockerfile.bottom
fi
echo "RUN rm -rf /build /data"
}
if [ $# = 0 ] ; then
echo >&2 "Usage: $0 <src-directory> [<podman build options>]"
exit 1
fi
src=$1
shift
if [ ! -v http_proxy ] ; then
PROXY=
eval "$(apt-config shell PROXY Acquire::http::proxy)"
if [ -n "$PROXY" ] ; then
export http_proxy=$PROXY
fi
fi
if [ -v http_proxy -a ! -v https_proxy ] ; then
export https_proxy=$http_proxy
fi
CACHE_DIR=${XDG_CACHE_HOME:-$HOME/.cache}/container-build
if [ -d $CACHE_DIR ] ; then
echo "Using cache $CACHE_DIR"
else
echo "Creating cache $CACHE_DIR"
mkdir -p $CACHE_DIR
fi
mkdir -p $CACHE_DIR/download
gen-docker-file | podman build \
--file - \
--layers \
--http-proxy \
--volume=$CACHE_DIR:/root/.cache \
"$@" \
.
echo -n "Cache usage: "
du -sh $CACHE_DIR | cut -d ' ' -f1

View file

@ -1,152 +0,0 @@
C_RED="$(echo -e '\e[31m')"
C_GREEN="$(echo -e '\e[32m')"
C_YELLOW="$(echo -e '\e[33m')"
C_NORMAL="$(echo -e '\e[0m')"
export DEBIAN_FRONTEND=noninteractive
B_APT_UPDATED=
progress ()
{
echo "${C_GREEN}$1${C_NORMAL}" >&2
}
note ()
{
echo "${C_YELLOW}$1${C_NORMAL}" >&2
}
warn ()
{
echo "${C_RED}WARNING: $1${C_NORMAL}" >&2
}
die ()
{
echo "${C_RED}ERROR: $1${C_NORMAL}" >&2
exit 1
}
update-pkgs ()
{
if [ -z "$B_APT_UPDATED" ] ; then
apt update
B_APT_UPDATED=1
fi
}
install-pkgs ()
{
update-pkgs
apt install -y --no-install-recommends --no-install-suggests "$@"
}
install-config ()
{
[ $# = 2 ] || die "Usage: install-config <source> (<target-file> | <target-dir>/)"
local S=$1
local T=$2
if [ ${T%/} != $T ] ; then
T=$T$(basename $S)
fi
local B=$BUILD_CONFIG$T
mkdir -p $(dirname $B) $(dirname $T)
if [ -f $T ] ; then
if [ ! -f $B.orig ] ; then
echo "Backing up $T to $B.orig"
cp $T $B.orig
else
echo "Backup of $T already present in $B.orig"
fi
fi
if [ -f $S ] ; then
# Just a new file: overwrite
echo "Overwriting $T"
cp $S $T
cp $S $B.new
elif [ -f $S.orig -a -f $S.new ] ; then
# Try 3-way merge
if cmp --quiet $S.new $T ; then
warn "Skipping merge of $T: changes already present"
else
echo "Merging $T"
if diff3 -m $S.new $S.orig $T >$B.new ; then
cp $B.new $T
else
die "Merge failed, please review $B.new"
fi
fi
else
die "Do not know how to install config $T"
fi
}
sed-config ()
{
[ $# = 2 ] || die "Usage: sed-config <target> <sed-script>"
local T=$1
local SCRIPT=$2
local B=$BUILD_CONFIG$T
mkdir -p $(dirname $B)
[ -f $T ] || die "Want to sed $T, but it is missing"
if [ ! -f $B.orig ] ; then
echo "Backing up $T to $B.orig"
cp $T $B.orig
else
echo "Backup of $T already present in $B.orig"
fi
sed -ri "$SCRIPT" $T
cp $T $B.new
}
pipe-config ()
{
[ $# = 1 ] || die "Usage: <command> | pipe-config <target>"
local TMP=$(mktemp)
cat >$TMP
install-config $TMP $1
rm $TMP
}
rm-config ()
{
[ $# = 1 ] || die "Usage: rm-config <target>"
local T=$1
local B=$BUILD_CONFIG$T
if [ -f $T ] ; then
if [ ! -f $B.orig ] ; then
echo "Backing up $T to $B.orig"
cp $T $B.orig
fi
echo "Removing $T"
rm -f $T
else
echo "Wanted to remove $T, but it does not exist"
fi
}
download ()
{
[ $# = 3 ] || die "Usage: download <URL> <hash-type> <hash>"
local URL=$1
local HTYPE=$2
local HASH=$3
local DEST=$(basename $URL)
local CACHE=/root/.cache/download
if [ -d $CACHE ] ; then
if [ -f $CACHE/$DEST ] ; then
echo "Using cached $DEST"
else
echo "Downloading $URL with caching"
curl $URL >$CACHE/$DEST.tmp
mv $CACHE/$DEST.tmp $CACHE/$DEST
fi
ln -s $CACHE/$DEST .
else
warn "Cache $CACHE not found: downloading directly"
echo "Downloading $URL"
curl $URL >$DEST
fi
local H=$(${HTYPE}sum $DEST | cut -d' ' -f1)
[ $H == $HASH ] || die "Mismatched hash: got $H, want $HASH"
}

View file

@ -1,50 +0,0 @@
#!/bin/bash
set -euo pipefail
if [ $# != 1 ] ; then
echo >&2 "Usage: $0 <stage-directory>"
exit 1
fi
STAGE=$1
STAGE=/build/$STAGE
BUILD_COMMON=/build/common
BUILD_CONFIG=/build/config-tmp
. $BUILD_COMMON/lib.sh
note "Running $STAGE"
if [ ! -v http_proxy -o ! -v https_proxy ] ; then
warn "No HTTP(S) proxy is set"
fi
case "$STAGE" in
*.sh)
. $STAGE
exit 0
;;
*.d)
;;
*)
die "Unrecognized stage name $STAGE"
esac
cd $STAGE
if [ -f run.sh ] ; then
. run.sh
else
for a in [0-9]* ; do
case "$a" in
*.sh)
( . $a )
;;
*.d)
( cd $a && . run.sh )
;;
*)
warn "Unrecognized build step file $a"
esac
done
fi

View file

@ -1,10 +0,0 @@
{
"token": "Paste your token here.",
"enabled_cogs": [
"basic",
"roles",
"messages",
"ksp",
"news"
]
}

View file

@ -1,3 +0,0 @@
{
"secret_roles": {}
}

View file

@ -1,32 +0,0 @@
import discord
from discord.ext import commands
from hrochobot.utils.ksp_utils import *
class Ksp(commands.Cog):
def __init__(self, bot):
self.bot = bot
@discord.slash_command(description="Generates urls for given task.")
async def task(self, ctx, task_code: str):
await ctx.respond(
f'**{task_code}**\n'
f'Task: {task_link(task_code, solution=False)}\n'
f'Solution: {task_link(task_code, solution=True)}',
ephemeral=True
)
@discord.slash_command(description="Shows deadlines of currently running series.")
async def deadlines(self, ctx):
a_deadlines = active_deadlines()
if len(a_deadlines) == 0:
return await ctx.respond("No running series.", ephemeral=True)
msg = ""
for series_id, deadline in a_deadlines:
msg += f"Sérii **{series_id}** odevzdávejte do " + \
deadline.strftime('%-d. %-m. %Y %-H:%M') + '\n'
return await ctx.respond(msg, ephemeral=True)
def setup(bot):
bot.add_cog(Ksp(bot))

View file

@ -1,37 +0,0 @@
import discord
from discord.ext import commands
from discord.utils import get
import hrochobot.utils.data as data
MESSAGES_JSON = "messages"
def as_reply(message: str):
return "\n".join(map(lambda x: f"> {x}", message.split('\n')))
class Messages(commands.Cog):
def __init__(self, bot):
self.bot = bot
@discord.message_command()
async def forward(self, ctx, message: discord.Message):
messages_json = data.load_guild_data(ctx.guild.id, MESSAGES_JSON)
if "forward_channel" not in messages_json:
return await ctx.respond(f"Forwarding channel not set.", ephemeral=True)
forward_channel = get(ctx.guild.channels, id=messages_json["forward_channel"])
header_text = f"{ctx.author.mention} forwarded:\n{message.author.mention} wrote in {message.channel.mention}:\n"
await forward_channel.send(f"{header_text}{as_reply(message.clean_content)}")
return await ctx.respond("Message successfully forwarded.", ephemeral=True)
@discord.slash_command(description="Sets channel for forwarding.")
@discord.option("channel", discord.TextChannel, description="Channel for forwarding.")
@discord.default_permissions(administrator=True)
async def set_forward_channel(self, ctx, channel: discord.TextChannel):
messages_json = data.load_guild_data(ctx.guild.id, MESSAGES_JSON)
messages_json["forward_channel"] = channel.id
data.dump_guild_data(ctx.guild.id, MESSAGES_JSON, messages_json)
return await ctx.respond(f"Forwarding channel set to {channel.mention}.\n" + \
"Remember to set permissions for forwarding.", ephemeral=True)
def setup(bot):
bot.add_cog(Messages(bot))

View file

@ -1,118 +0,0 @@
from datetime import datetime
import discord
from discord.ext import commands
from discord.utils import get
from markdownify import markdownify
import re
from bs4 import BeautifulSoup
from hrochobot.utils.ksp_utils import ksp_feed, strip_id, KSP_URL
import hrochobot.utils.data as data
NEWS_JSON = "news"
async def get_news_ids():
feed = await ksp_feed()
return list(map(lambda e: e.id, feed.entries))
async def autocomplete_news_ids(ctx):
value = ctx.value.lower()
options = []
for id_ in map(strip_id, await get_news_ids()):
lid = id_.lower()
if lid.startswith(value) or lid.split("_", 1)[1].startswith(value):
options.append(id_)
return options
def guess_color(title):
"""
Automagically guess color of given post.
Not always reliable as all things automagic.
"""
def contains(*regexes):
return any(re.search(regex, title) for regex in regexes)
if contains(r"(\d+)-Z(\d+)", "začátečnic", "KSP-Z"):
return discord.Color.green()
elif contains(r"(\d+)-(\d+)", "seriál", "série", "KSP-H"):
return discord.Color.blue()
else:
return discord.Color.dark_purple()
def format_entry(entry, author=None):
content = "\n\n".join(map(lambda x: x.replace('\n', ' '), entry.summary.split("\n\n")))
embed = discord.Embed(
title=entry.title,
url=entry.link,
description=markdownify(content, strip=["img"]),
color=guess_color(entry.title),
)
soup = BeautifulSoup(content, 'html.parser')
img = soup.find('img')
if img:
embed.set_image(url=img['src'])
if author:
embed.set_author(name=author)
embed.set_thumbnail(url=f"{KSP_URL}/img/hippo_head.png")
date = datetime.fromisoformat(entry.published)
embed.set_footer(text=date.strftime("%-d. %-m. %Y"))
return embed
async def post_news(bot, guild, entry_id):
news_json = data.load_guild_data(guild.id, NEWS_JSON)
if "news_channel" not in news_json:
return "News channel not set."
channel = get(guild.channels, id=news_json["news_channel"])
feed = await ksp_feed()
entries_with_id = list(filter(lambda e: strip_id(e.id) == entry_id, feed.entries))
if len(entries_with_id) == 0:
return f"Entry with id ``{entry_id}`` not found."
await channel.send(embed=format_entry(entries_with_id[0], author=feed.feed.author))
return None
class News(commands.Cog):
def __init__(self, bot):
self.bot = bot
news = discord.SlashCommandGroup(
"news",
"Commands for management of ksp news.",
guild_only=True,
checks=[commands.has_permissions(manage_guild=True)]
)
@news.command(description="Sets channel for posting news.")
@discord.option("channel_id", str, description="Id of the channel for sending news.")
async def set_channel(self, ctx, channel_id: str):
try:
channel_id = int(channel_id)
except ValueError:
return await ctx.respond(f"Channel id must be int.", ephemeral=True)
if not (channel := get(ctx.guild.channels, id=channel_id)):
return await ctx.respond(f"No channel with id ``{channel_id}``.", ephemeral=True)
news_json = data.load_guild_data(ctx.guild.id, NEWS_JSON)
news_json["news_channel"] = channel_id
data.dump_guild_data(ctx.guild.id, NEWS_JSON, news_json)
return await ctx.respond(f"News channel set to {channel.mention}.", ephemeral=True)
@news.command(description="Posts news of given id to set channel.")
@discord.option("id", str, description="Id of entry to send.", autocomplete=autocomplete_news_ids)
async def post_news(self, ctx, id: int):
await ctx.defer(ephemeral=True)
err = await post_news(self.bot, ctx.guild, id)
if err:
return await ctx.respond(err, ephemeral=True)
return await ctx.respond(f"News posted.")
def setup(bot):
bot.add_cog(News(bot))

View file

@ -1,75 +0,0 @@
import discord
import hrochobot.utils.data as data
from discord.ext import commands
from discord.utils import get
ROLES_JSON = "roles"
def role_mention(role):
return role.mention if role else "@deleted-role"
class Roles(commands.Cog):
def __init__(self, bot):
self.bot = bot
secret_roles = discord.SlashCommandGroup(
"secretroles",
"Commands for management of secret roles.",
guild_only=True,
checks=[commands.has_permissions(manage_roles=True)]
)
@secret_roles.command(description="Adds a new secret role.")
@discord.option("role", discord.role.Role, description="Role locked behind a password.")
@discord.option("password", str, description="Password for given role.")
async def add(self, ctx, role, password):
roles = data.load_guild_data(ctx.author.guild.id, ROLES_JSON)
if password in roles["secret_roles"]:
return await ctx.respond(f"Password ``{password}`` is already used.", ephemeral=True)
roles["secret_roles"][password] = role.id
data.dump_guild_data(ctx.author.guild.id, ROLES_JSON, roles)
return await ctx.respond(f"Secret role {role.mention} added with password {password}.", ephemeral=True)
@secret_roles.command(description="Lists all passwords and their secret roles.")
async def list(self, ctx):
roles = data.load_guild_data(ctx.author.guild.id, ROLES_JSON)
if len(roles["secret_roles"]) == 0:
return await ctx.respond(f"No current secret roles.", ephemeral=True)
msg = ""
for passwd, role in roles["secret_roles"].items():
role = get(ctx.author.guild.roles, id=role)
msg += f"``{passwd}``: {role_mention(role)}\n"
return await ctx.respond(msg, ephemeral=True)
@secret_roles.command(description="Deletes given password and its secret role.")
@discord.option("password", str, description="Password to be deleted.")
async def delete(self, ctx, password):
roles = data.load_guild_data(ctx.author.guild.id, ROLES_JSON)
if password not in roles["secret_roles"]:
return await ctx.respond(f"Role with passowrd {password} does not exist.", ephemeral=True)
role = get(ctx.author.guild.roles, id=roles["secret_roles"][password])
del roles["secret_roles"][password]
data.dump_guild_data(ctx.author.guild.id, ROLES_JSON, roles)
return await ctx.respond(
f"Secret role {role_mention(role)} no longer obtainable with password {password}.",
ephemeral=True
)
@discord.slash_command(description="Gives a secret role locked by a password.", guild_only=True)
@discord.option("password", str, description="Password for secret role.")
async def secretrole(self, ctx, password):
roles = data.load_guild_data(ctx.author.guild.id, ROLES_JSON)
if password in roles["secret_roles"]:
author = ctx.author
role = get(author.guild.roles, id=roles["secret_roles"][password])
if role is None:
return await ctx.respond(f"Role for this password was deleted.", ephemeral=True)
await author.add_roles(role, reason="Role assigned for password knowledge.")
return await ctx.respond(f"You now have role {role.mention}.", ephemeral=True)
else:
return await ctx.respond("Incorrect password.", ephemeral=True)
def setup(bot):
bot.add_cog(Roles(bot))

View file

@ -1,38 +0,0 @@
from typing import Any
import json
import os.path
from hrochobot.utils.json_templates import TEMPLATES
EXAMPLE_DATA = "data.example"
DATA_FOLDER = "TODO"
def load_json(filename: str):
filename += ".json"
if not os.path.exists(filename):
return json.loads(TEMPLATES[os.path.basename(filename)])
with open(filename) as f:
content = json.load(f)
return content
def dump_json(filename: str, data: Any):
filename += ".json"
path = os.path.dirname(filename)
if not os.path.exists(path):
os.makedirs(path)
with open(filename, "w") as f:
json.dump(data, f)
def load_data(filename: str):
return load_json(os.path.join(DATA_FOLDER, filename))
def dump_data(filename: str, data: Any):
return dump_json(os.path.join(DATA_FOLDER, filename), data)
def load_guild_data(guild: int, filename: str):
return load_data(os.path.join(str(guild), filename))
def dump_guild_data(guild: int, filename: str, data: Any):
return dump_data(os.path.join(str(guild), filename), data)

View file

@ -1,5 +0,0 @@
TEMPLATES = {
'roles.json' : '{"secret_roles": {}}',
'messages.json' : '{}',
'news.json' : '{}',
}

View file

@ -1,45 +0,0 @@
import feedparser
from typing import List, Tuple, Optional
from datetime import datetime
import requests
import json
KSP_URL = "https://ksp.mff.cuni.cz"
def task_link(task_code: str, solution : bool = False):
return f"{KSP_URL}/viz/{task_code}/{'reseni' if solution else ''}"
def catalog(year : Optional[int] = None, tasks : bool = False):
year_str = ""
if year is not None:
year_str = f"year={year}"
tasks_str = f"tasks={str(tasks).lower()}"
page = requests.get(f"{KSP_URL}/api/tasks/catalog?{year_str}&{tasks_str}")
return json.loads(page.text)
def active_deadlines() -> List[Tuple[str, datetime]]:
deadlines = []
now = datetime.now().astimezone()
cat = catalog()
for series in cat:
series_deadlines = [series["deadline"]]
if "deadline2" in series:
series_deadlines.append(series["deadline2"])
series_deadlines = list(map(datetime.fromisoformat, series_deadlines))
for series_deadline in series_deadlines:
if now < series_deadline:
deadlines.append((series["id"], series_deadline))
break
return deadlines
async def ksp_feed() -> feedparser.util.FeedParserDict:
return feedparser.parse(f"{KSP_URL}/ksp.feed")
def strip_id(id_):
text = f"{KSP_URL}/"
if id_.startswith(text):
id_ = id_[len(text):]
return id_

18
main.py Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env python3
from discord.ext import commands
import utils.data as data
CONFIG = data.load_json("config")
bot = commands.Bot()
cogs_list = [
'basic',
'roles',
'ksp',
]
for cog in cogs_list:
bot.load_extension(f'cogs.{cog}')
bot.run(CONFIG["token"])

View file

@ -1,5 +0,0 @@
py-cord
requests
feedparser
markdownify
beautifulsoup4

View file

@ -1,22 +0,0 @@
#!/usr/bin/env python3
import setuptools
setuptools.setup(
name='hrochbot',
version='1.0',
description='Discordový robot pro KSP',
packages=['hrochobot', 'hrochobot/cogs', 'hrochobot/utils'],
scripts=[
'bin/hrochobot',
],
include_package_data=True,
zip_safe=False,
install_requires=[
'py-cord',
'requests',
'feedparser',
'markdownify',
'beautifulsoup4',
],
)

20
utils/data.py Normal file
View file

@ -0,0 +1,20 @@
from typing import Any
import json
import os.path
DATA_FOLDER = "data"
def load_json(filename: str):
with open(filename + ".json") as f:
content = json.load(f)
return content
def dump_json(filename: str, data: Any):
with open(filename + ".json", "w") as f:
json.dump(data, f)
def load_data(filename: str):
return load_json(os.path.join(DATA_FOLDER, filename))
def dump_data(filename: str, data: Any):
return dump_json(os.path.join(DATA_FOLDER, filename), data)

4
utils/ksp_utils.py Normal file
View file

@ -0,0 +1,4 @@
KSP_URL = "https://ksp.mff.cuni.cz"
def task_link(task_code: str, solution : bool = False):
return f"{KSP_URL}/viz/{task_code}/{'reseni' if solution else ''}"