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'])