~aleteoryx/sparfnord

6c71e63bb9ef7553e5bc9375cdb8fa5a906e00bd — Aleteoryx 2 days ago
that's the bot
3 files changed, 129 insertions(+), 0 deletions(-)

A .gitignore
A README.md
A sparf.py
A  => .gitignore +4 -0
@@ 1,4 @@
*~
\#*
/venv
/config.json

A  => README.md +3 -0
@@ 1,3 @@
## spar-fnord

minimal, self-hostable starboard

A  => sparf.py +122 -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'])