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