Compare commits
	
		
			No commits in common. "master" and "adbbc6352b3bc7b1dcfc034b38aee848efb2121f" have entirely different histories.
		
	
	
		
			master
			...
			adbbc6352b
		
	
		
					 28 changed files with 129 additions and 819 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,2 +1 @@
 | 
				
			||||||
/config.json
 | 
					config.json
 | 
				
			||||||
/data/
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										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):
 | 
					    def __init__(self, bot):
 | 
				
			||||||
        self.bot = bot
 | 
					        self.bot = bot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @discord.slash_command(description="Greets the world.")
 | 
					    @discord.slash_command()
 | 
				
			||||||
    async def sayhello(self, ctx):
 | 
					    async def sayhello(self, ctx):
 | 
				
			||||||
        await ctx.respond('Hello world!')
 | 
					        await ctx.respond('Hello world!')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @discord.slash_command(description="Sends the bot's latency.")
 | 
					    @discord.slash_command()
 | 
				
			||||||
    async def ping(self, ctx):
 | 
					    async def ping(self, ctx):
 | 
				
			||||||
        await ctx.respond('My ping is {:.0f}ms'.format(self.bot.latency*1000), ephemeral=True)
 | 
					        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