Ejemplo n.º 1
0
 def read_schedule(self):
     """
     Reads the schedule that is saved to the storage.
     :return: The read schedule.
     """
     try:
         filepath = self.__root_path.joinpath(PLANNING_FILE)
         data = filepath.read_bytes()
         bundle = Schedule()
         bundle.ParseFromString(data)
     except:
         raise IOError("Impossible de lire le planning du stockage.")
     return bundle
Ejemplo n.º 2
0
    def test_schedule_write_then_read(self):
        """Writes then reads a schedule to/from the storage."""
        schedule = Schedule()
        player1 = RatedPlayer()
        player1.gamer_tag = "Walnut Waffle"
        player1.rating = RatedPlayer.Rating.EXPERIENCED
        player2 = RatedPlayer()
        player2.gamer_tag = "Oby1Chick"
        player2.rating = RatedPlayer.Rating.INTERMEDIATE

        activity1 = Activity()
        activity1.id.type = ActivityID.Type.GARDEN_OF_SALVATION
        activity1.id.when.datetime = '2020-9-1'
        activity1.state = Activity.State.NOT_STARTED
        activity1.squad.players.append(player1)
        schedule.activities.append(activity1)

        activity2 = Activity()
        activity2.id.type = ActivityID.Type.LEVIATHAN_PRESTIGE
        activity2.state = Activity.State.FINISHED
        activity2.squad.substitutes.append(player2)
        schedule.activities.append(activity2)
        self.sut.write_schedule(schedule)
        schedule2 = self.sut.read_schedule()
        self.assertEqual(schedule, schedule2)
        self.assertRaises(IOError, self.sut.read_api_bundle)
Ejemplo n.º 3
0
 def test_clearall(self):
     """Verifies clearall intents are properly executed."""
     (feedback, images) = self.execute("!cortana clearall")
     expectation = Schedule()
     self.assertEqual(self.storage.read_schedule(), expectation)
     self.assertEqual(
         feedback,
         "Toutes les activités du planning sont désormais supprimées.")
     self.assertIsNone(images)
Ejemplo n.º 4
0
    async def handle_message(self, message):
        """Real message handler."""
        if message.author == self.user or not message.content.startswith(
                "!cortana "):
            return
        print()
        print(message.guild.name)
        print(message.author)
        print(message.content)

        # Init storage for current guild.
        now = datetime.now(TIMEZONE)
        storage = Storage(ROOT_DIRECTORY.joinpath(str(message.guild.id)))

        # Make sure a basic bundle is available.
        bundle = None
        try:
            bundle = storage.read_api_bundle()
        except:
            await self.answer_message(
                "Une synchronisation des joueurs doit être effectuée.\nVeuillez patienter...",
                message)
            bundle = self.__fetcher.fetch(now)
            storage.write_api_bundle(bundle)
            await self.answer_message("Synchronisation terminée", message)

        # Make sure a basic schedule is available.
        try:
            storage.read_schedule()
        except:
            storage.write_schedule(Schedule())

        # Parse intent.
        intent = self.__parser.parse(message.content, bundle, now)

        # Execute intent.
        if intent.HasField(
                'global_intent') and intent.global_intent.sync_bundle:
            await self.answer_message(
                "Synchronisation en cours.\nVeuillez patienter...", message)
        executor = Executor(storage, self.__fetcher, self.__generator)
        (feedback, images) = executor.execute(intent, now)

        # Post message.
        await self.answer_message(feedback, message)

        # Post images.
        if not images:
            return
        for index, image in enumerate(images):
            file = discord.File(fp=image,
                                filename="Affiche" + str(index) + ".gif")
            await message.channel.send(file=file)
Ejemplo n.º 5
0
    def test_clearpast(self):
        """Verifies clearpast intents are properly executed."""
        schedule_before = self.storage.read_schedule()
        (feedback, images) = self.execute("!cortana clearpast")
        expected_activities = schedule_before.activities
        expected_activities.pop(0)
        expectation = Schedule()
        expectation.activities.extend(expected_activities)

        self.assertTrue(
            feedback.startswith(
                "Les activités des semaines précédentes ont été suprimées."))
        self.assertIsNone(images)
        self.assertEqual(self.storage.read_schedule(), expectation)
Ejemplo n.º 6
0
def make_schedule():
    """Constructs a schedule for test cases."""
    s = Schedule()
    a = s.activities.add()
    a.state = Activity.State.FINISHED
    a.id.type = ActivityID.Type.LEVIATHAN
    a.id.when.datetime = '2020-08-09T21:15:00+02:00'
    a.id.when.time_specified = True
    (a.squad.players.add()).gamer_tag = "Oby1Chick"
    a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
    (a.squad.players.add()).gamer_tag = "dark0l1ght"
    a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
    (a.squad.players.add()).gamer_tag = "Cosa58"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.players.add()).gamer_tag = "Franstuk"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.players.add()).gamer_tag = "Walnut Waffle"
    a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
    (a.squad.players.add()).gamer_tag = "SuperFayaChonch"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.substitutes.add()).gamer_tag = "croptus"
    a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.substitutes.add()).gamer_tag = "affectevil"
    a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER

    a = s.activities.add()
    a.state = Activity.State.NOT_STARTED
    a.id.type = ActivityID.Type.SPIRE_OF_STARS_PRESTIGE
    a.id.when.datetime = '2020-08-12'
    a.id.when.time_specified = False
    (a.squad.players.add()).gamer_tag = "snippro34"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.players.add()).gamer_tag = "Jezehbell"
    a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
    (a.squad.substitutes.add()).gamer_tag = "NaughtySoft"
    a.squad.substitutes[-1].rating = RatedPlayer.Rating.INTERMEDIATE

    a = s.activities.add()
    a.state = Activity.State.MILESTONED
    a.milestone = "Save au boss"
    a.id.type = ActivityID.Type.SCOURGE_OF_THE_PAST
    a.id.when.datetime = '2020-08-16T14:30:00+02:00'
    a.id.when.time_specified = True
    (a.squad.players.add()).gamer_tag = "Duality Cobra"
    a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
    (a.squad.players.add()).gamer_tag = "Walnut Waffle"
    a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
    (a.squad.players.add()).gamer_tag = "Oby1Chick"
    a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
    (a.squad.players.add()).gamer_tag = "Franstuk"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.players.add()).gamer_tag = "dark0l1ght"
    a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
    (a.squad.players.add()).gamer_tag = "croptus"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER

    a = s.activities.add()
    a.state = Activity.State.NOT_STARTED
    a.id.type = ActivityID.Type.GARDEN_OF_SALVATION
    a.id.when.datetime = '2020-08-17T21:15:00+02:00'
    a.id.when.time_specified = True
    (a.squad.players.add()).gamer_tag = "Hartog31"
    a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
    (a.squad.players.add()).gamer_tag = "Oby1Chick"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.players.add()).gamer_tag = "Walnut Waffle"
    a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
    (a.squad.players.add()).gamer_tag = "Franstuk"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.players.add()).gamer_tag = "dark0l1ght"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.players.add()).gamer_tag = "Cosa58"
    a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
    (a.squad.substitutes.add()).gamer_tag = "croptus"
    a.squad.substitutes[-1].rating = RatedPlayer.Rating.EXPERIENCED
    (a.squad.substitutes.add()).gamer_tag = "affectevil"
    a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER

    a = s.activities.add()
    a.state = Activity.State.NOT_STARTED
    a.id.type = ActivityID.Type.GARDEN_OF_SALVATION
    a.id.when.datetime = '2020-08-25'
    a.id.when.time_specified = False
    (a.squad.players.add()).gamer_tag = "Cosa58"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    (a.squad.players.add()).gamer_tag = "Walnut Waffle"
    a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
    return s
Ejemplo n.º 7
0
    def test_schedule(self):
        """
        Verifies the hash of generated GIFs to ensure the images stay the same.
        """
        s = Schedule()
        a = s.activities.add()
        a.id.type = ActivityID.Type.LEVIATHAN
        a.id.when.datetime = '2020-07-14T23:55:00+02:00'
        a.id.when.time_specified = True
        a.state = Activity.State.NOT_STARTED
        (a.squad.players.add()).gamer_tag = "Cosa58"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.players.add()).gamer_tag = "Walnut Waffle"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.players.add()).gamer_tag = "Oby1Chick"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Franstuck"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Dark0l1ght"
        a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        (a.squad.players.add()).gamer_tag = "SuperFayaChonch"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.substitutes.add()).gamer_tag = "croptus7490"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.substitutes.add()).gamer_tag = "Affectevil"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER
        a = s.activities.add()
        a.id.type = ActivityID.Type.SPIRE_OF_STARS_PRESTIGE
        a.id.when.datetime = '2020-07-23'
        a.id.when.time_specified = False
        a.state = Activity.State.NOT_STARTED
        (a.squad.players.add()).gamer_tag = "Cosa58"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Walnut Waffle"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.players.add()).gamer_tag = "Oby1Chick"
        a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        (a.squad.players.add()).gamer_tag = "Franstuck"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Dark0l1ght"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "SuperFayaChonch"
        a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        (a.squad.substitutes.add()).gamer_tag = "croptus7490"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.substitutes.add()).gamer_tag = "Affectevil"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        a = s.activities.add()
        a.id.type = ActivityID.Type.SCOURGE_OF_THE_PAST
        a.id.when.datetime = '2020-10-25T16:30:00+01:00'
        a.id.when.time_specified = True
        a.state = Activity.State.MILESTONED
        a.milestone = "save au boss"
        (a.squad.players.add()).gamer_tag = "Cosa58"
        a.squad.players[-1].rating = RatedPlayer.Rating.UNKNOWN
        (a.squad.players.add()).gamer_tag = "Walnut Waffle"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Oby1Chick"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.players.add()).gamer_tag = "Franstuck"
        a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        (a.squad.players.add()).gamer_tag = "Dark0l1ght"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.players.add()).gamer_tag = "SuperFayaChonch"
        a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        (a.squad.substitutes.add()).gamer_tag = "croptus7490"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.substitutes.add()).gamer_tag = "Affectevil"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        a = s.activities.add()
        a.id.type = ActivityID.Type.GARDEN_OF_SALVATION
        a.id.when.datetime = '2020-08-25T22:35:00+02:00'
        a.id.when.time_specified = True
        a.state = Activity.State.FINISHED
        (a.squad.players.add()).gamer_tag = "Cosa58"
        a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        (a.squad.players.add()).gamer_tag = "Walnut Waffle"
        a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        (a.squad.players.add()).gamer_tag = "Oby1Chick"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Franstuck"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Dark0l1ght"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "SuperFayaChonch"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.substitutes.add()).gamer_tag = "croptus7490"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.substitutes.add()).gamer_tag = "Affectevil"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER
        a = s.activities.add()
        a.id.type = ActivityID.Type.WRATH_OF_THE_MACHINE
        a.id.when.datetime = '2019-01-25T04:42:00+01:00'
        a.id.when.time_specified = True
        a.state = Activity.State.NOT_STARTED
        (a.squad.players.add()).gamer_tag = "Cosa58"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Walnut Waffle"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Oby1Chick"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.players.add()).gamer_tag = "Franstuck"
        a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        (a.squad.players.add()).gamer_tag = "Dark0l1ght"
        a.squad.players[-1].rating = RatedPlayer.Rating.INTERMEDIATE
        (a.squad.players.add()).gamer_tag = "SuperFayaChonch"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.substitutes.add()).gamer_tag = "croptus7490"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.substitutes.add()).gamer_tag = "Affectevil"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER
        a = s.activities.add()
        a.id.type = ActivityID.Type.LAST_WISH
        a.state = Activity.State.NOT_STARTED
        (a.squad.players.add()).gamer_tag = "snippro34"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.players.add()).gamer_tag = "Jezehbell"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.players.add()).gamer_tag = "SuperFayaChonch"
        a.squad.players[-1].rating = RatedPlayer.Rating.BEGINNER
        (a.squad.players.add()).gamer_tag = "Carnage"
        a.squad.players[-1].rating = RatedPlayer.Rating.EXPERIENCED
        (a.squad.substitutes.add()).gamer_tag = "Oby1Chick"
        a.squad.substitutes[-1].rating = RatedPlayer.Rating.BEGINNER

        gen = img_generator.Generator(tz.gettz('Europe/Paris'), "fr")
        gifs = gen.generate_images(s)

        hash_reference = {
            "Darwin": [
                "6c3a69069413955d2007ebf10fee7d289769aedec2b015fa0b56e742735d4beb",
                "941aa1047be76e8e98ecfe7dff9ad6bd2083e5b2ca1c6d913bd3fd634a0b9e8d"
            ],
            "Linux": [
                "0d4939f0a14a4e8ce0982f93eaaa2f37b244cf3a2354fa3fe351d84874b07c1f",
                "5713ac4a16f7701ca7eba86fd8063899cc95753412edbd5a19b96a14b0b2b64e"
            ]
        }

        detected_os = system()
        self.assertTrue(detected_os in hash_reference.keys(), "OS not supported to run tests")

        gif_hashes = []
        for gif in gifs:
            self.assertEqual(gif.tell(), 0)
            gif.seek(0, io.SEEK_END)
            self.assertLess(gif.tell(), 8*1024*1024)
            gif.seek(0, io.SEEK_SET)
            with tempfile.NamedTemporaryFile(delete=False, suffix='.gif') as temp:
                temp.write(gif.getbuffer())
                print("NOTE - the result of this test can be visualized here: ", temp.name)
            gif_hashes.append(hashlib.sha256(gif.getbuffer()).hexdigest())
        self.assertEqual(gif_hashes, hash_reference[detected_os], True)
Ejemplo n.º 8
0
 def test_no_permission(self):
     """Verifies the storage raises an IOError when encountering permission errors."""
     s = storage.Storage(Path('/'))
     self.assertRaises(IOError, s.write_schedule, Schedule())
     self.assertRaises(IOError, s.write_api_bundle, APIBundle())
Ejemplo n.º 9
0
    def execute_global_intent(self, global_intent, now):
        """
        :param global_intent: The global_intent to execute.
        :param now: Now as a datetime.
        :return: An (execution feedback message, generated BytesIO images or None) tuple.
        :raises: If the intent could be executed for some reason.
        """
        if global_intent.HasField('credits'):
            #! raid credits
            return "Mes créateurs sont Walnut & Oby ❤️", None

        if global_intent.HasField('help'):
            # !cortana help
            help_str = "Guide d'utilisation:\n" \
                "Cortana gère et génère les affiches d'un planning d'activités Destiny.\n" \
                "Elle connaît les noms et les niveaux d'expérience des joueurs de la FE11 " \
                "et des clans alliés.\nCe guide détaille les commandes pour utiliser Cortana.\n" \
                "Dans toutes les commandes détaillées ci-dessous il faut savoir que: \n\n" \
                "-[nom activité] " \
                "doit être remplacé par un vrai nom, style calus, fleche, fleau, jds, couronne, " \
                "etc... Plusieurs formats sont supportés (e.g. leviathan, jardin du salut, " \
                "couronne du malheur, flèche prestige, flèche d'étoiles prestige marchent aussi)." \
                " Les fautes d'orthographe sont aussi gérées dans la limite du raisonnable. " \
                "(e.g. kalus, fleo, devorer, etc... sont aussi supportés).\n\n" \
                "- (date) indique une date optionnelle. Plusieurs formats de " \
                "date sont gérés (e.g. mercredi, 17/08, dimanche 21h, demain 22h30, 20/8 20h30, "\
                "etc...)\nLa date est utile pour 2 cas de figures:\n" \
                "1) Afficher la date sur les affiches.\n" \
                "2) Différencier deux activités du même type dans une commande.\n" \
                "La date peut généralement être ignorée dans une commande qui modifie " \
                "les détails d'une activité qui est la seule de son type dans le planning.\n\n" \
                "- [date] indique une date obligatoire. Par exemple pour la commande de " \
                "modification d'une date d'activité.\n\n" \
                "- [gamer_tags] indique une liste de gamer tags (au minimum 1 gamer tag).\n" \
                "Cortana est capable de comprendre et corriger " \
                "les gamer tags même si ils sont mal ortographiés. " \
                "(e.g. cosa, croptus, darklight, franstuck, hartok etc...)\n\n\n" \
                "Liste des commandes: \n\n" \
                "!cortana [nom activité] (date) [gamer_tag1, ...] => Créé/modifie une escouade " \
                "pour/d'une activité. Précédez un gamer tag par '-' pour signaler que le joueur " \
                "doit être retiré et non ajouté.\n\n" \
                "!cortana backup [nom activité] (date) [gamer_tag1, ...] => Pareil que la " \
                "commande du dessus mais pour gérer les remplaçants.\n\n" \
                "!cortana date [nom activité] (ancienne date) [nouvelle date] => Ajoute ou " \
                "modifie une date à une activité.\n\n" \
                "!cortana milestone [nom activité] (date) => Précise une save ou un statut pour " \
                "l'activité concernée. (e.g. !cortana milestone jds save au boss, " \
                "!cortana milestone leviathan prestige reporté)." \
                "!cortana finish [nom activité] (date) => Marque l'activité comme terminée.\n\n" \
                "!cortana info [nom activité] (date) => Affiche les détails de l'activité dans " \
                "un format texte.\n\n" \
                "!cortana clear [nom activité] (date) => Supprime l'activité du planning.\n\n" \
                "!cortana images => Génère les affiches pour toutes les activités " \
                "du planning.\n\n" \
                "!cortana help => Affiche le guide d'utilisation.\n\n" \
                "!cortana infoall => Affiche tout le planning dans un format texte.\n\n" \
                "!cortana clearpast => Supprime toutes les activités des semaines " \
                "précédentes.\n\n" \
                "!cortana clearall => Supprime toutes les activités.\n\n" \
                "!cortana sync => Synchronise la liste des joueurs et leurs stats.\n" \
                "Utile quand des nouveaux membres rejoignent le clan.\n" \
                "Utile pour que Cortana soit au courant des changements de niveaux d'expérience " \
                "des joueurs.\nAttention toutefois, les changements de niveaux d'expérience ne " \
                "sont pas directement reflétés dans les affiches. " \
                "Ils seront appliqués à partir des prochaines commandes (nouvelle création " \
                "d'activité ou mise à jour d'escouade).\n\n" \
                "!cortana lastsync => Affiche la dernière date de synchronisation.\n\n" \
                "!cortana credits => Affiche les noms de mes créateurs."
            return help_str, None

        if global_intent.HasField('generate_images'):
            # !cortana images
            schedule = self.__storage.read_schedule()
            images = self.__img_generator.generate_images(schedule)
            if len(images) == 0:
                return "Il n'y a aucune activité dans le planning pour le moment.", []
            return "Affiches pour les activités en cours:", images

        if global_intent.HasField('sync_bundle'):
            # !cortana sync
            bundle = self.__api_fetcher.fetch(now)
            self.__storage.write_api_bundle(bundle)
            return "Joueurs et niveaux d'experiences synchronisés.", None

        if global_intent.HasField('get_last_bundle_sync_datetime'):
            # !cortana lastsync
            bundle = self.__storage.read_api_bundle()
            return "Dernière synchronisation: " + bundle.last_sync_datetime, None

        if global_intent.HasField('clear_all'):
            # !cortana clearall
            schedule = Schedule()
            self.__storage.write_schedule(schedule)
            return "Toutes les activités du planning sont désormais supprimées.", None

        if global_intent.HasField('clear_past'):
            # !cortana clearpast
            schedule = self.__storage.read_schedule()
            threshold = dateparser.parse(CLEARPAST_WEEKDAY,
                                         settings={
                                             'PREFER_DATES_FROM': 'past',
                                             'RELATIVE_BASE': now
                                         })
            threshold = threshold.replace(hour=CLEARPAST_HOUR)
            threshold = threshold.replace(tzinfo=TIMEZONE)
            activities = filter(
                lambda a: not a.id.when or threshold <= to_datetime(
                    a.id.when, now.tzinfo), schedule.activities)
            schedule = Schedule()
            schedule.activities.extend(activities)
            self.__storage.write_schedule(schedule)
            feedback = "Les activités des semaines précédentes ont été suprimées.\n"
            feedback += "Les activités restantes dans le planning sont:\n"
            feedback += str(schedule)
            return feedback, None

        if global_intent.HasField('info_all'):
            # !cortana infoall
            schedule = self.__storage.read_schedule()
            return str(schedule) if len(
                schedule.activities) > 0 else "Le planning est vide.", None

        raise ValueError("Commande invalide")