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
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)
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
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)
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})
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()}})
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)