@ -1,18 +1,19 @@
import logging
import logging
from datetime import datetime , timedelta
from datetime import datetime , timedelta
from typing import Dict , List , Union
from typing import Dict , List , Optional, Tuple , Union
import discord
import discord
import pytz
from apscheduler . triggers . base import BaseTrigger
from apscheduler . triggers . base import BaseTrigger
from apscheduler . triggers . combining import OrTrigger
from apscheduler . triggers . combining import OrTrigger
from apscheduler . triggers . cron import CronTrigger
from apscheduler . triggers . cron import CronTrigger
from apscheduler . triggers . date import DateTrigger
from apscheduler . triggers . interval import IntervalTrigger
from apscheduler . triggers . interval import IntervalTrigger
from discord . utils import time_snowflake
from discord . utils import time_snowflake
from pytz import timezone
from redbot . core import Config , commands
from redbot . core import Config , commands
from redbot . core . bot import Red
from redbot . core . bot import Red
from fifo . date_trigger import CustomDateTrigger
log = logging . getLogger ( " red.fox_v3.fifo.task " )
log = logging . getLogger ( " red.fox_v3.fifo.task " )
@ -26,7 +27,7 @@ def get_trigger(data):
return IntervalTrigger ( days = parsed_time . days , seconds = parsed_time . seconds )
return IntervalTrigger ( days = parsed_time . days , seconds = parsed_time . seconds )
if data [ " type " ] == " date " :
if data [ " type " ] == " date " :
return DateTrigger( data [ " time_data " ] , timezone = data [ " tzinfo " ] )
return Custom DateTrigger( data [ " time_data " ] , timezone = data [ " tzinfo " ] )
if data [ " type " ] == " cron " :
if data [ " type " ] == " cron " :
return CronTrigger . from_crontab ( data [ " time_data " ] , timezone = data [ " tzinfo " ] )
return CronTrigger . from_crontab ( data [ " time_data " ] , timezone = data [ " tzinfo " ] )
@ -34,20 +35,126 @@ def get_trigger(data):
return False
return False
def check_expired_trigger ( trigger : BaseTrigger ) :
return trigger . get_next_fire_time ( None , datetime . now ( pytz . utc ) ) is None
def parse_triggers ( data : Union [ Dict , None ] ) :
def parse_triggers ( data : Union [ Dict , None ] ) :
if data is None or not data . get ( " triggers " , False ) : # No triggers
if data is None or not data . get ( " triggers " , False ) : # No triggers
return None
return None
if len ( data [ " triggers " ] ) > 1 : # Multiple triggers
if len ( data [ " triggers " ] ) > 1 : # Multiple triggers
return OrTrigger ( [ get_trigger ( t_data ) for t_data in data [ " triggers " ] ] )
triggers_list = [ get_trigger ( t_data ) for t_data in data [ " triggers " ] ]
triggers_list = [ t for t in triggers_list if not check_expired_trigger ( t ) ]
if not triggers_list :
return None
return OrTrigger ( triggers_list )
else :
else :
return get_trigger ( data [ " triggers " ] [ 0 ] )
trigger = get_trigger ( data [ " triggers " ] [ 0 ] )
if check_expired_trigger ( trigger ) :
return None
return trigger
# class FakeMessage:
# def __init__(self, message: discord.Message):
# d = {k: getattr(message, k, None) for k in dir(message)}
# self.__dict__.update(**d)
# Potential FakeMessage subclass of Message
# class DeleteSlots(type):
# @classmethod
# def __prepare__(metacls, name, bases):
# """Borrowed a bit from https://stackoverflow.com/q/56579348"""
# super_prepared = super().__prepare__(name, bases)
# print(super_prepared)
# return super_prepared
things_for_fakemessage_to_steal = [
" _state " ,
" id " ,
" webhook_id " ,
# "reactions",
# "attachments",
" embeds " ,
" application " ,
" activity " ,
" channel " ,
" _edited_time " ,
" type " ,
" pinned " ,
" flags " ,
" mention_everyone " ,
" tts " ,
" content " ,
" nonce " ,
" reference " ,
]
things_fakemessage_sets_by_default = {
" attachments " : [ ] ,
" reactions " : [ ] ,
}
class FakeMessage ( discord . Message ) :
def __init__ ( self , * args , message : discord . Message , * * kwargs ) :
d = { k : getattr ( message , k , None ) for k in things_for_fakemessage_to_steal }
d . update ( things_fakemessage_sets_by_default )
for k , v in d . items ( ) :
try :
# log.debug(f"{k=} {v=}")
setattr ( self , k , v )
except TypeError :
# log.exception("This is fine")
pass
except AttributeError :
# log.exception("This is fine")
pass
self . id = time_snowflake ( datetime . utcnow ( ) , high = False ) # Pretend to be now
self . type = discord . MessageType . default
def process_the_rest (
self ,
author : discord . Member ,
channel : discord . TextChannel ,
content ,
) :
# self.content = content
# log.debug(self.content)
# for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'):
# try:
# getattr(self, '_handle_%s' % handler)(data[handler])
# except KeyError:
# continue
self . author = author
# self._handle_author(author._user._to_minimal_user_json())
# self._handle_member(author)
self . _rebind_channel_reference ( channel )
self . _update (
{
" content " : content ,
}
)
self . _update (
{
" mention_roles " : self . raw_role_mentions ,
" mentions " : self . raw_mentions ,
}
)
# self._handle_content(content)
# log.debug(self.content)
self . mention_everyone = " @everyone " in self . content or " @here " in self . content
class FakeMessage :
# self._handle_mention_roles(self.raw_role_mentions)
def __init__ ( self , message : discord . Message ) :
# self._handle_mentions(self.raw_mentions)
d = { k : getattr ( message , k , None ) for k in dir ( message ) }
self . __dict__ . update ( * * d )
# self.__dict__.update(**d )
def neuter_message ( message : FakeMessage ) :
def neuter_message ( message : FakeMessage ) :
@ -66,11 +173,11 @@ def neuter_message(message: FakeMessage):
class Task :
class Task :
default_task_data = { " triggers " : [ ] , " command_str " : " " }
default_task_data = { " triggers " : [ ] , " command_str " : " " , " expired_triggers " : [ ] }
default_trigger = {
default_trigger = {
" type " : " " ,
" type " : " " ,
" time_data " : None , # Used for Interval and Date Triggers
" time_data " : None ,
" tzinfo " : None ,
" tzinfo " : None ,
}
}
@ -87,9 +194,10 @@ class Task:
async def _encode_time_triggers ( self ) :
async def _encode_time_triggers ( self ) :
if not self . data or not self . data . get ( " triggers " , None ) :
if not self . data or not self . data . get ( " triggers " , None ) :
return [ ]
return [ ] , [ ]
triggers = [ ]
triggers = [ ]
expired_triggers = [ ]
for t in self . data [ " triggers " ] :
for t in self . data [ " triggers " ] :
if t [ " type " ] == " interval " : # Convert into timedelta
if t [ " type " ] == " interval " : # Convert into timedelta
td : timedelta = t [ " time_data " ]
td : timedelta = t [ " time_data " ]
@ -101,13 +209,15 @@ class Task:
if t [ " type " ] == " date " : # Convert into datetime
if t [ " type " ] == " date " : # Convert into datetime
dt : datetime = t [ " time_data " ]
dt : datetime = t [ " time_data " ]
triggers . append (
data_to_append = {
{
" type " : t [ " type " ] ,
" type " : t [ " type " ] ,
" time_data " : dt . isoformat ( ) ,
" time_data " : dt . isoformat ( ) ,
" tzinfo " : getattr ( t [ " tzinfo " ] , " zone " , None ) ,
" tzinfo " : getattr ( t [ " tzinfo " ] , " zone " , None ) ,
}
}
if dt < datetime . now ( pytz . utc ) :
)
expired_triggers . append ( data_to_append )
else :
triggers . append ( data_to_append )
continue
continue
if t [ " type " ] == " cron " :
if t [ " type " ] == " cron " :
@ -125,7 +235,7 @@ class Task:
raise NotImplemented
raise NotImplemented
return triggers
return triggers , expired_triggers
async def _decode_time_triggers ( self ) :
async def _decode_time_triggers ( self ) :
if not self . data or not self . data . get ( " triggers " , None ) :
if not self . data or not self . data . get ( " triggers " , None ) :
@ -138,7 +248,7 @@ class Task:
# First decode timezone if there is one
# First decode timezone if there is one
if t [ " tzinfo " ] is not None :
if t [ " tzinfo " ] is not None :
t [ " tzinfo " ] = timezone( t [ " tzinfo " ] )
t [ " tzinfo " ] = pytz. timezone( t [ " tzinfo " ] )
if t [ " type " ] == " interval " : # Convert into timedelta
if t [ " type " ] == " interval " : # Convert into timedelta
t [ " time_data " ] = timedelta ( * * t [ " time_data " ] )
t [ " time_data " ] = timedelta ( * * t [ " time_data " ] )
@ -174,14 +284,23 @@ class Task:
await self . _decode_time_triggers ( )
await self . _decode_time_triggers ( )
return self . data
return self . data
async def get_triggers ( self ) - > List[ Union [ IntervalTrigger , Dat eTrigger] ] :
async def get_triggers ( self ) - > Tuple[ List [ BaseTrigger ] , List [ Bas eTrigger] ] :
if not self . data :
if not self . data :
await self . load_from_config ( )
await self . load_from_config ( )
if self . data is None or " triggers " not in self . data : # No triggers
if self . data is None or " triggers " not in self . data : # No triggers
return [ ]
return [ ] , [ ]
return [ get_trigger ( t ) for t in self . data [ " triggers " ] ]
trigs = [ ]
expired_trigs = [ ]
for t in self . data [ " triggers " ] :
trig = get_trigger ( t )
if check_expired_trigger ( trig ) :
expired_trigs . append ( t )
else :
trigs . append ( t )
return trigs , expired_trigs
async def get_combined_trigger ( self ) - > Union [ BaseTrigger , None ] :
async def get_combined_trigger ( self ) - > Union [ BaseTrigger , None ] :
if not self . data :
if not self . data :
@ -201,7 +320,10 @@ class Task:
data_to_save = self . default_task_data . copy ( )
data_to_save = self . default_task_data . copy ( )
if self . data :
if self . data :
data_to_save [ " command_str " ] = self . get_command_str ( )
data_to_save [ " command_str " ] = self . get_command_str ( )
data_to_save [ " triggers " ] = await self . _encode_time_triggers ( )
(
data_to_save [ " triggers " ] ,
data_to_save [ " expired_triggers " ] ,
) = await self . _encode_time_triggers ( )
to_save = {
to_save = {
" guild_id " : self . guild_id ,
" guild_id " : self . guild_id ,
@ -217,7 +339,10 @@ class Task:
return
return
data_to_save = self . data . copy ( )
data_to_save = self . data . copy ( )
data_to_save [ " triggers " ] = await self . _encode_time_triggers ( )
(
data_to_save [ " triggers " ] ,
data_to_save [ " expired_triggers " ] ,
) = await self . _encode_time_triggers ( )
await self . config . guild_from_id ( self . guild_id ) . tasks . set_raw (
await self . config . guild_from_id ( self . guild_id ) . tasks . set_raw (
self . name , " data " , value = data_to_save
self . name , " data " , value = data_to_save
@ -240,19 +365,23 @@ class Task:
f " Could not execute Task[ { self . name } ] due to missing channel: { self . channel_id } "
f " Could not execute Task[ { self . name } ] due to missing channel: { self . channel_id } "
)
)
return False
return False
author : discord . Us er = guild . get_member ( self . author_id )
author : discord . Memb er = guild . get_member ( self . author_id )
if author is None :
if author is None :
log . warning (
log . warning (
f " Could not execute Task[ { self . name } ] due to missing author: { self . author_id } "
f " Could not execute Task[ { self . name } ] due to missing author: { self . author_id } "
)
)
return False
return False
actual_message : discord. Message = channel . last_message
actual_message : Optional[ discord. Message ] = channel . last_message
# I'd like to present you my chain of increasingly desperate message fetching attempts
# I'd like to present you my chain of increasingly desperate message fetching attempts
if actual_message is None :
if actual_message is None :
# log.warning("No message found in channel cache yet, skipping execution")
# log.warning("No message found in channel cache yet, skipping execution")
# return
# return
actual_message = await channel . fetch_message ( channel . last_message_id )
if channel . last_message_id is not None :
try :
actual_message = await channel . fetch_message ( channel . last_message_id )
except discord . NotFound :
actual_message = None
if actual_message is None : # last_message_id was an invalid message I guess
if actual_message is None : # last_message_id was an invalid message I guess
actual_message = await channel . history ( limit = 1 ) . flatten ( )
actual_message = await channel . history ( limit = 1 ) . flatten ( )
if not actual_message : # Basically only happens if the channel has no messages
if not actual_message : # Basically only happens if the channel has no messages
@ -262,22 +391,27 @@ class Task:
return False
return False
actual_message = actual_message [ 0 ]
actual_message = actual_message [ 0 ]
message = FakeMessage ( actual_message )
# message._handle_author(author) # Option when message is subclass
# message = FakeMessage2
# message ._state = self.bot._get_state()
message . author = author
# Time to set the relevant attributes
message . guild = guild # Just in case we got desperate, see above
# message.author = author
message . channel = channel
# Don't need guild with subclass, guild is just channel.guild
message . id = time_snowflake ( datetime . utcnow ( ) , high = False ) # Pretend to be now
# message.guild = guild # Just in case we got desperate, see above
message = neuter_message ( message )
# message.channel = channel
# absolutely weird that this takes a message object instead of guild
# absolutely weird that this takes a message object instead of guild
prefixes = await self . bot . get_prefix ( message)
prefixes = await self . bot . get_prefix ( actual_ message)
if isinstance ( prefixes , str ) :
if isinstance ( prefixes , str ) :
prefix = prefixes
prefix = prefixes
else :
else :
prefix = prefixes [ 0 ]
prefix = prefixes [ 0 ]
message . content = f " { prefix } { self . get_command_str ( ) } "
new_content = f " { prefix } { self . get_command_str ( ) } "
# log.debug(f"{new_content=}")
message = FakeMessage ( message = actual_message )
message = neuter_message ( message )
message . process_the_rest ( author = author , channel = channel , content = new_content )
if (
if (
not message . guild
not message . guild
@ -285,7 +419,10 @@ class Task:
or not message . content
or not message . content
or message . content == prefix
or message . content == prefix
) :
) :
log . warning ( f " Could not execute Task[ { self . name } ] due to message problem: { message } " )
log . warning (
f " Could not execute Task[ { self . name } ] due to message problem: "
f " { message . guild =} , { message . author =} , { message . content =} "
)
return False
return False
new_ctx : commands . Context = await self . bot . get_context ( message )
new_ctx : commands . Context = await self . bot . get_context ( message )