Example #1
0
    def _get_reminders(self,
                       member_id: int = None,
                       include_complete: bool = True) -> List[Reminder]:
        rem_keys = self.bot.redis_helper.keys(redis_id='reminders')
        if not rem_keys:
            return []

        reminders: List[Reminder] = []

        for rem_id in rem_keys:
            rem = cast(
                Reminder,
                Reminder.fetch(self.bot.redis_helper,
                               redis_id='reminders',
                               redis_name=rem_id))

            if not rem:
                logger.warning(f'Unexpectedly missing reminder for "{rem_id}"')
                continue

            if not include_complete and rem.is_complete:  # type: ignore[attr-defined]
                logger.info(
                    f'Skipping reminder for "{rem_id}" since reminder is marked complete'
                )
                continue

            reminders.append(rem)

        return reminders
Example #2
0
    async def _reminders_add(self,
                             ctx: SlashContext,
                             when: str,
                             content: str,
                             timezone: str = None) -> None:
        if not self.bot.init_done:
            await ctx.send(
                'Sorry, the bot is not yet loaded.. Try again in a few moments'
            )
            return

        if not timezone:
            timezone = self.bot.bot_config.get_user_setting(
                ctx.author.id, 'timezone', default=None) or 'UTC'

        if not Timezone.is_valid_timezone(timezone):
            await ctx.send(
                f'Invalid timezone provided "{timezone}".. :slight_frown:')
            return

        user_tz = Timezone.build(timezone)

        fuzzy_when = FuzzyTime.build(provided_when=when, use_timezone=user_tz)
        reminder = Reminder.build(fuzzy_when,
                                  member=ctx.author,
                                  channel=ctx.channel,
                                  content=content)  # type: ignore[arg-type]

        reminder.store(self.bot.redis_helper)
        reminder_md = cast(discord.Embed,
                           reminder.as_markdown(
                               ctx.author,
                               as_embed=True))  # type: ignore[arg-type]
        logger.info(
            f'Successfully created a new reminder for "{ctx.author.name}" via slash command'
        )
        logger.debug(f'Slash Command Reminder Reminder:\n{reminder.dump()}')

        self.bot.scheduler.add_job(self._process_reminder,
                                   kwargs={
                                       'reminder': reminder,
                                       'added_at': datetime.now()
                                   },
                                   trigger='date',
                                   run_date=reminder.trigger_dt,
                                   id=reminder.redis_name)
        logger.info(
            f'Scheduled new reminder job at "{reminder.trigger_dt.ctime()}"')

        await ctx.send(
            f'Adding new reminder for `{fuzzy_when.resolved_time.ctime()}` :wink:',
            embed=reminder_md)
Example #3
0
    async def _process_reminder(self,
                                reminder: Reminder,
                                added_at: datetime = None) -> bool:
        added_at = added_at or datetime.now()
        author, channel = None, None

        channel = await self.bot.fetch_channel(
            reminder.channel_id) if reminder.channel_id else None
        author = await self.bot.fetch_user(reminder.member_id)

        if not channel:
            logger.info(
                f'Reminder has no associated channel, DMing "{author.name}" instead'
            )

        msg_target = channel if channel else author
        msg_out = f':wave: {author.mention if channel else author.name}, here is your reminder:\n{reminder.as_markdown(author, channel=channel)}'
        logger.info(
            f'Triggered reminder response for "{msg_target}":\n{reminder.dump()}'
        )

        try:
            await msg_target.send(msg_out)
        except Exception as ex:
            logger.error(f'Error sending reminder to "{msg_target}": {ex}')
            logger.debug(f'Dumped reminder:\n{reminder.dump()}')
            return False
        else:
            reminder.user_notified = True
            reminder.store(self.bot.redis_helper)
            logger.info(
                f'Successfully marked reminder for "{reminder.member_name}" complete'
            )

        logger.info('Finished scheduled reminder check.')
        return True
Example #4
0
    async def add_reminder(self, ctx: commands.Context,
                           fuzzy_when: FuzzyTimeConverter, *,
                           content: str) -> None:
        dt_now = datetime.now()
        reminder = Reminder.build(fuzzy_when,
                                  member=ctx.author,
                                  channel=ctx.channel,
                                  content=content)  # type: ignore[arg-type]

        reminder.store(self.bot.redis_helper)
        reminder_md = cast(
            discord.Embed,
            reminder.as_markdown(ctx.author, ctx.channel,
                                 as_embed=True))  # type: ignore[arg-type]
        logger.info(
            f'Successfully created a new reminder for "{ctx.author.name}"')
        logger.debug(f'Reminder:\n{reminder.dump()}')

        confirm = await ConfirmMenu(
            f'Create reminder at `{reminder.trigger_dt.ctime()}` for `{content}`?'
        ).prompt(ctx)

        if not confirm:
            logger.info(
                f'Canceling reminder for {ctx.author.name} based on prompt response'
            )
            return

        self.bot.scheduler.add_job(self._process_reminder,
                                   kwargs={
                                       'reminder': reminder,
                                       'added_at': dt_now
                                   },
                                   trigger='date',
                                   run_date=reminder.trigger_dt,
                                   id=reminder.redis_name)
        logger.info(
            f'Scheduled new reminder job at "{reminder.trigger_dt.ctime()}"')

        await ctx.send(
            f'Adding new reminder for {ctx.author.mention} at {reminder.trigger_dt.ctime()}`',
            embed=reminder_md)
Example #5
0
def reminder_by_id(id):
    try:
        rem = Reminder.fetch(current_app.redis_helper, redis_id='reminders', redis_name=id)
    except Exception as ex:
        raise MinderWebError(f'Error fetching reminder ID "{id}": {ex}', status_code=500, payload={'id': id}, base_exception=ex) from ex

    if not rem:
        raise MinderWebError(f'No reminder found with ID "{id}"', status_code=404, payload={'id': id})

    # Handle HTTP GET for fetching individual reminder
    if request.method == 'GET':
        return jsonify(rem.as_dict())

    # Handle HTTP DELETE for removing a particular reminder
    if request.method == 'DELETE':
        logger.info(f'Deleting reminder "{id}" as requested via API by "{request.remote_addr}"')
        rem.delete(current_app.redis_helper)

        return jsonify({'message': f'Deleted reminder "{id}" successfully', 'data': {}, 'is_error': False})

    # Lastly, handle HTTP PATCH for updating a particular reminder

    form_dict = request.form.to_dict()
    reminder_fields = Reminder.get_entry_fields(include_redis_fields=False, include_internal_fields=False)

    for attr_name, attr_val in form_dict.items():
        if attr_name not in reminder_fields:
            raise MinderWebError(f'Invalid attribute provided in PATCH of reminder ID {id}: Invalid attribute "{attr_name}"', payload=form_dict)

        # TODO: This is already too complicated to be nested in a for loop. Break this out into a private method
        # This is also a good chance to offer some decorator validation optoins within redisent to help with this

        fld = reminder_fields[attr_name]
        attr_type = fld.type
        do_cast = True

        if isinstance(attr_type, str):
            if not fld.default_factory or fld.default_factory == dataclasses._MISSING_TYPE:
                logger.info(f'Found field type of "{attr_type}" for "{attr_name}" and no default factory. Skipping validation.')
                do_cast = False
            else:
                attr_type = fld.default_factory

        if do_cast:
            if isinstance(attr_type, typing._GenericAlias):
                # If using a Optional, Union, etc then all subclasses will share this root generic alias
                # base class. If the field type is one of these, the first item in the "__args__" entry
                # will be the actual type
                attr_type = attr_type.__args__[0]

            try:
                logger.info(f'casting "{attr_val}" as "{attr_type}"')
                attr_test = attr_type(attr_val)
                attr_val = attr_test
            except Exception as ex:
                err_message = f'Invalid attribute value provided for "{attr_name}". Field is of the type "{attr_type}" and the validation failed with: {ex}'
                raise MinderWebError(err_message, payload=form_dict, base_exception=ex) from ex

        setattr(rem, attr_name, attr_val)

    try:
        rem.store(current_app.redis_helper)
    except Exception as ex:
        raise MinderWebError(f'Error with Redis while storing updated reminder ID {id}: {ex}', payload=form_dict, base_exception=ex) from ex

    msg_out = f'Successfully updated reminder ID {id} with new attributes.'
    return jsonify({'message': msg_out, 'data': {'form': form_dict, 'new_reminder': rem.as_dict()}, 'is_error': False})
Example #6
0
def reminders():
    if request.method == 'GET':
        exclude = [ex_attr.lower() for ex_attr in request.args.get('exclude', '').split(',')]
        member_id = request.args.get('member_id', None)
        channel_id = request.args.get('channel_id', None)

        rems = []

        for r_key in current_app.redis_helper.keys(redis_id='reminders', use_encoding='utf-8'):
            rem = Reminder.fetch(current_app.redis_helper, redis_id='reminders', redis_name=r_key)

            if 'complete' in exclude and rem.is_complete:
                continue

            if 'notified' in exclude and rem.user_notified:
                continue

            if member_id and int(member_id) != rem.member_id:
                continue

            if channel_id and int(channel_id) != rem.channel_id:
                continue

            rems.append(rem.as_dict())

        msg_out = 'No reminders found' if not rems else f'Found #{len(rems)} reminders'
        return jsonify({'message': msg_out, 'count': len(rems), 'is_error': False, 'data': rems})

    # Handle POST / PUT request
    form_dict = request.form.to_dict()
    req_attrs = ['when', 'content', 'member_id', 'member_name']

    if 'channel_id' in request.form:
        req_attrs += ['channel_id', 'channel_name']

    for attr in req_attrs:
        if attr not in request.form:
            raise MinderWebError(f'No value for "{attr}" provided when attemptingn to create reminder', status_code=400, payload=form_dict)

    opts = {attr: request.form.get(attr) for attr in req_attrs}

    for o_name, o_val in opts.items():
        if not o_val:
            raise MinderWebError(f'No value provided for "{attr}" (value: "{o_val}")', status_code=400, payload=form_dict)

        if o_name.endswith('_id'):
            opts[o_name] = int(o_val)

    opts['timezone'] = request.form.get('timezone', None)
    when = opts['when']

    member_dict = {'id': opts['member_id'], 'name': opts['member_name']}

    channel_dict = {'id': opts['channel_id'], 'name': opts['channel_name']} if 'channel_id' in opts else None

    try:
        fuz_time = FuzzyTime.build(when, use_timezone=opts['timezone'])
    except Exception as ex:
        raise MinderWebError(f'Error parsing fuzzy timestamp for "{when}": {ex}', status_code=400, payload=form_dict, base_exception=ex) from ex

    try:
        rem = Reminder.build(fuz_time, member=member_dict, content=opts['content'], channel=channel_dict, use_timezone=opts['timezone'])
    except Exception as ex:
        raise MinderWebError(f'Error building new reminder for "{when}": {ex}', status_code=500, payload=form_dict, base_exception=ex) from ex

    try:
        rem.store(current_app.redis_helper)
    except Exception as ex:
        raise MinderWebError(f'Error storing new reminder for "{when}" in Redis: {ex}', status_code=500, payload=form_dict, base_exception=ex) from ex

    msg_out = f'Successfully built new reminder for "{when}" at "{rem.trigger_dt.ctime()}"'
    return jsonify({'message': msg_out, 'is_error': False, 'data': {'reminder': rem.as_dict()}})
Example #7
0
def fake_dm_reminder(fake_member, fake_dm_channel):
    return Reminder.build(trigger_time='in 5 minutes', member=fake_member, content='pytest test DM reminder', channel=fake_dm_channel)