Compare commits
No commits in common. "master" and "adbbc6352b3bc7b1dcfc034b38aee848efb2121f" have entirely different histories.
master
...
adbbc6352b
28 changed files with 129 additions and 819 deletions
.gitignoreREADME.md
bin
cobuild
cogs
common
config.example.jsondata.example
hrochobot
main.pyrequirements.txtsetup.pyutils
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1 @@
|
|||
/config.json
|
||||
/data/
|
||||
config.json
|
||||
|
|
78
README.md
78
README.md
|
@ -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.
|
|
@ -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"])
|
|
@ -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 .
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
progress "Configuring hrochobot.service"
|
||||
|
||||
install-config hrochobot.service /etc/systemd/system/
|
||||
systemctl enable hrochobot
|
|
@ -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
|
|
@ -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
|
|
@ -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
19
cogs/ksp.py
Normal 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
65
cogs/roles.py
Normal 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))
|
73
common/build
73
common/build
|
@ -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
|
152
common/lib.sh
152
common/lib.sh
|
@ -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"
|
||||
}
|
50
common/run
50
common/run
|
@ -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
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"token": "Paste your token here.",
|
||||
"enabled_cogs": [
|
||||
"basic",
|
||||
"roles",
|
||||
"messages",
|
||||
"ksp",
|
||||
"news"
|
||||
]
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"secret_roles": {}
|
||||
}
|
|
@ -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))
|
|
@ -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))
|
|
@ -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))
|
|
@ -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))
|
|
@ -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)
|
|
@ -1,5 +0,0 @@
|
|||
TEMPLATES = {
|
||||
'roles.json' : '{"secret_roles": {}}',
|
||||
'messages.json' : '{}',
|
||||
'news.json' : '{}',
|
||||
}
|
|
@ -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
18
main.py
Executable 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"])
|
|
@ -1,5 +0,0 @@
|
|||
py-cord
|
||||
requests
|
||||
feedparser
|
||||
markdownify
|
||||
beautifulsoup4
|
22
setup.py
22
setup.py
|
@ -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
20
utils/data.py
Normal 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
4
utils/ksp_utils.py
Normal 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 ''}"
|
Loading…
Reference in a new issue