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