From 6c71e63bb9ef7553e5bc9375cdb8fa5a906e00bd Mon Sep 17 00:00:00 2001 From: Aleteoryx Date: Tue, 30 Dec 2025 19:51:03 -0500 Subject: [PATCH] that's the bot --- .gitignore | 4 ++ README.md | 3 ++ sparf.py | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 sparf.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0ae857efc647bd3c5df235b2879692eb6d11f5a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*~ +\#* +/venv +/config.json diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..02d5833f97dd840a3d802f6ebcc25f48d9cb9d39 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## spar-fnord + +minimal, self-hostable starboard diff --git a/sparf.py b/sparf.py new file mode 100644 index 0000000000000000000000000000000000000000..4a2585e0a43775f0fd34cd7002e2bbb1bb157caa --- /dev/null +++ b/sparf.py @@ -0,0 +1,122 @@ +import discord + +import json +import logging +import logging.config +from pathlib import Path +from dataclasses import dataclass +from sys import argv, stderr, exit + + +STAR = '⭐' + +log = logging.getLogger('sparf') +logging.basicConfig(stream=stderr, level=logging.INFO, format='[%(asctime)s %(levelname)s %(name)s] %(msg)s') + + +argv0 = 'sparf.py' +def usage(): + print(f'usage: {argv0} config.json', file=stderr) + exit(-1) + + +class ConfigError(Exception): + pass + +class Config: + def __init__(self, path): + self.log = logging.getLogger('sparf.config') + self.path = path + self.load() + + if 'guilds' not in self.data: + self.data['guilds'] = {} + self.save() + + if 'token' not in self.data: + raise ConfigError('missing config.token!') + + def __setitem__(self, k, v): + self.data[k] = v + def __getitem__(self, k): + return self.data[k] + def __contains__(self, k): + return k in self.data + + def load(self): + self.log.debug('reloading config...') + with open(self.path, 'rt') as fp: + self.data = json.load(fp) + self.log.debug('config loaded!') + + def save(self): + self.log.debug('saving config...') + with open(self.path, 'wt') as fp: + json.dump(self.data, fp, indent=2) + self.log.debug('config saved!') + + +cli = discord.Bot(intents=discord.Intents.default()) + + +@cli.event +async def on_raw_reaction_add(evt): + if evt.emoji.name != STAR: + return + + if str(evt.guild_id) not in config['guilds']: + return + gconf = config['guilds'][str(evt.guild_id)] + if 'threshold' not in gconf or 'channel' not in gconf: + log.info(f'would log {evt.message_id} in {evt.guild_id}, but I don\'t know where!') + return + thresh = gconf['threshold'] + chan = cli.get_partial_messageable(int(gconf['channel'])) + + if chan.id == evt.channel_id: + return + + msg = cli.get_message(evt.message_id) + if msg is None: + srcchan = cli.get_partial_messageable(evt.channel_id) + msg = await srcchan.fetch_message(evt.message_id) + + for reaction in msg.reactions: + if reaction.emoji == STAR and reaction.count >= thresh: + async for who in reaction.users(): + if who.id == cli.application_id: + return + + fwd = await msg.forward_to(chan) + await chan.send(msg.author.mention, reference=fwd.to_reference()) + await msg.add_reaction(STAR) + log.info(f'logged {evt.message_id} in {evt.guild_id}') + return + +@cli.slash_command(description='Configure the bot.') +@discord.default_permissions(administrator=True) +async def setup(ctx, threshold: discord.Option(int, description='# of stars needed'), channel: discord.Option(discord.TextChannel, description='where to forward messages')): + if str(ctx.guild_id) not in config['guilds']: + config['guilds'][str(ctx.guild_id)] = {} + + config['guilds'][str(ctx.guild_id)]['threshold'] = threshold + config['guilds'][str(ctx.guild_id)]['channel'] = str(channel.id) + config.save() + + log.info(f'/setup in {ctx.guild_id}: thresh={threshold} channel={channel.id}') + await ctx.send_response(f'Okay! Threshold is {threshold} reacts and channel is {channel.mention}.', ephemeral=True) + + +if len(argv) > 0: + argv0 = argv[0] + argv = argv[1:] +if len(argv) != 1: + usage() + +path = Path(argv[0]) +if not path.exists(): + print(f'fatal: {str(path)} does not exist', file=stderr) + exit(-2) + +config = Config(path) +cli.run(config['token'])