{
  "name": "DynamicServerMessages",
  "author": "Limon",
  "supportedGames": [
    "all"
  ],
  "takaroVersion": "main",
  "versions": [
    {
      "tag": "latest",
      "description": "# Dynamic CronJob Manager: Automated Game Server Command Scheduler\n\n---\n🎥 **SETUP TUTORIAL**  \n**[► Watch the Complete Guide](https://youtu.be/NlmRnMvPSLg?si=Y-bKwt4St_Wjn51L)**  \n*Learn how to set up and configure dynamic cronjobs*\n---\n\nCreate and manage dynamic cronjobs for executing game server commands based on flexible schedules. Define commands and their cron expressions to automate tasks.  \n\n  -   **Dynamic Updates:** The module's `cronJobGenerator` cronjob automatically checks for and adapts to changes in the user configuration. This ensures that any modifications to commands or schedules are applied dynamically.\n  -   **Cron Syntax Validation:** The module validates the syntax of cron expressions. If an invalid expression is provided, that specific cronjob will be skipped, preventing errors and ensuring only valid schedules are applied.  For assistance with cron syntax, you can use a tool like [crontab.guru](https://crontab.guru/).\n  -   **Command Sequencing:** You can define multiple commands within a single cronjob entry, separated by semicolons. These commands will be executed sequentially according to the defined schedule.\n  -   **Game Server Specificity:** It is crucial to install the latest version of this module and create a **separate copy for each game server**. This ensures that cronjobs are executed on the intended server and avoids unintended cross-server execution.\n  -   **Automated Management:** The module handles the creation, updating, and cleanup of cronjobs. It removes any cronjobs that are no longer present in the configuration, maintaining a clean and accurate schedule.\n  -   **Internal Synchronization:** To ensure proper synchronization of cron schedules, the module may perform internal operations to update its configuration. This process is automated and does not require manual intervention from the user.",
      "configSchema": "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"cronjobs\":{\"type\":\"array\",\"title\":\"Cronjobs\",\"description\":\"List of cronjobs and their schedules\",\"items\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"Optional name for the cronjob\",\"minLength\":1},\"command\":{\"type\":\"string\",\"description\":\"Command to execute\",\"minLength\":1},\"temporalValue\":{\"type\":\"string\",\"description\":\"Cron expression for execution schedule\",\"minLength\":1}},\"required\":[\"command\",\"temporalValue\"]}}},\"additionalProperties\":false}",
      "uiSchema": "{}",
      "commands": [],
      "hooks": [],
      "cronJobs": [
        {
          "name": "ServerMessagesGenerator",
          "temporalValue": "5 4 * * *",
          "description": "",
          "function": "import { takaro, data } from '@takaro/helpers';\n\nasync function main() {\n    const { gameServerId, module: mod } = data;\n    const cronjobs = mod.userConfig.cronjobs || [];\n\n    // First get existing cronjobs for this module \n    const existingCronjobs = (await takaro.cronjob.cronJobControllerSearch({\n        filters: {\n            versionId: [mod.versionId]\n        }\n    })).data.data;\n\n    // Track what we've processed to handle cleanup later\n    const processedJobs = new Set();\n\n    // Create or update cronjobs from config\n    for (let i = 0; i < cronjobs.length; i++) {\n        const job = cronjobs[i];\n        // Use the provided name if available, otherwise use default number\n        const jobNameSuffix = job.name ? `-${job.name}` : '';\n        const jobName = `sm-${i + 1}${jobNameSuffix}`; // Start counting from 1\n\n        processedJobs.add(jobName);\n\n        const existingJob = existingCronjobs.find(j => j.name === jobName);\n\n        const jobFunction = `\n      import { takaro, data } from '@takaro/helpers';\n      async function main() {\n        const { gameServerId } = data;\n        \n        // Get online players through PlayerOnGameServer search\n        const currentPlayers = (await takaro.playerOnGameserver.playerOnGameServerControllerSearch({\n            filters: {\n                gameServerId: [gameServerId],\n                online: [true]\n            }\n        })).data.meta;\n\n        // If no players online, exit early\n        if (currentPlayers.total === 0) {\n            return;\n        }\n        \n        await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {\n          message: \\`${job.command}\\`,\n        });\n      }\n      await main();\n    `.trim();\n\n        try {\n            if (existingJob) {\n                // Update without versionId\n                await takaro.cronjob.cronJobControllerUpdate(existingJob.id, {\n                    name: jobName,\n                    temporalValue: job.temporalValue,\n                    function: jobFunction\n                });\n            } else {\n                try {\n                    // Create includes versionId\n                    await takaro.cronjob.cronJobControllerCreate({\n                        name: jobName,\n                        temporalValue: job.temporalValue,\n                        versionId: mod.versionId,\n                        function: jobFunction\n                    });\n                } catch (createError) {\n                    if (createError.response?.status === 409) {\n                        const conflictJobSearch = await takaro.cronjob.cronJobControllerSearch({\n                            filters: {\n                                name: [jobName]\n                            }\n                        });\n\n                        if (conflictJobSearch.data.data.length > 0) {\n                            const conflictJob = conflictJobSearch.data.data[0];\n                            // Update without versionId\n                            await takaro.cronjob.cronJobControllerUpdate(conflictJob.id, {\n                                name: jobName,\n                                temporalValue: job.temporalValue,\n                                function: jobFunction\n                            });\n                        } else {\n                            throw createError;\n                        }\n                    } else {\n                        throw createError;\n                    }\n                }\n            }\n        } catch (error) {\n            // Error handling with no logging\n        }\n    }\n\n    // Clean up old cronjobs that are no longer in config\n    for (const existingJob of existingCronjobs) {\n        // Check if the name matches our pattern and if it's not in the processed list\n        if (/^sm-\\d+(-.*)?$/.test(existingJob.name) && !processedJobs.has(existingJob.name)) {\n            try {\n                await takaro.cronjob.cronJobControllerRemove(existingJob.id);\n            } catch (error) {\n                // Error handling with no logging\n            }\n        }\n    }\n}\n\nawait main();"
        }
      ],
      "functions": [],
      "permissions": []
    }
  ]
}