From 74e1a14b53bab7793fed39124b50c818afc94c97 Mon Sep 17 00:00:00 2001 From: Can Sirin Date: Fri, 22 Dec 2023 20:01:45 -0800 Subject: [PATCH] Enhance commands and clean up (#14) * Update gunun sorusu flow update gunun sorusu flow * update for dev env * wip * wip * enhance commands --------- Co-authored-by: Can Sirin <8138047+cansirin@users.noreply.github.com> --- .env.example | 3 + .github/workflows/fly-dev.yml | 15 +++ .github/workflows/fly.yml | 2 +- config.ts | 3 - deploy-commands.ts | 7 +- fly.dev.toml | 23 +++++ package.json | 4 +- src/commands/assign-role.ts | 17 +--- src/commands/daily-morning-question.ts | 107 +++++++++++++------ src/commands/get-all-members.ts | 1 + src/commands/read-all-messages.ts | 130 +++++------------------- src/commands/recycle-v2-roles.ts | 56 ++++++---- src/events/gunaydin-message-create.ts | 3 +- src/events/soru-cevap-message-create.ts | 5 +- src/features/fetch-messages.ts | 68 +++++++++++++ src/features/is-daily-msg-exist.ts | 17 ++++ src/utils/index.ts | 13 +++ 17 files changed, 297 insertions(+), 177 deletions(-) create mode 100644 .github/workflows/fly-dev.yml delete mode 100644 config.ts create mode 100644 fly.dev.toml create mode 100644 src/features/fetch-messages.ts create mode 100644 src/features/is-daily-msg-exist.ts diff --git a/.env.example b/.env.example index 68ef4a7..f2f3884 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,8 @@ CLIENT_ID=client-id +GUILD_ID=guild-id DISCORD_TOKEN=discord-token CHATGPT_API_KEY=get-your-chatgpt-key GIDENLER_CHANNEL_ID=channel-id GIDENLER_VIDEO_LINK=youtube-link +SORU_CEVAP_CHANNEL_ID=channel-id +GUNAYDIN_CHANNEL_ID=channel-id diff --git a/.github/workflows/fly-dev.yml b/.github/workflows/fly-dev.yml new file mode 100644 index 0000000..1f3b3f8 --- /dev/null +++ b/.github/workflows/fly-dev.yml @@ -0,0 +1,15 @@ +name: Fly deploy +on: + push: + branches: + - dev + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --app bot-dev --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.github/workflows/fly.yml b/.github/workflows/fly.yml index 9b54d36..68b647f 100644 --- a/.github/workflows/fly.yml +++ b/.github/workflows/fly.yml @@ -10,6 +10,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: superfly/flyctl-actions/setup-flyctl@master - - run: flyctl deploy --remote-only + - run: flyctl deploy --app kampus-discord-bot --remote-only env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/config.ts b/config.ts deleted file mode 100644 index 305470c..0000000 --- a/config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const KAMPUS_GUILD_ID = "717812863414042624"; -export const GUNAYDIN_CHANNEL_ID = "1158288028511522877"; -export const SORU_CEVAP_CHANNEL_ID = "1019649988550197248"; diff --git a/deploy-commands.ts b/deploy-commands.ts index 7c3dde3..601c739 100644 --- a/deploy-commands.ts +++ b/deploy-commands.ts @@ -1,6 +1,5 @@ import { REST, Routes } from "discord.js"; import { readdirSync } from "fs"; -import { KAMPUS_GUILD_ID } from "./config"; import dotenv from "dotenv"; dotenv.config(); @@ -18,17 +17,21 @@ for (const file of commandFiles) { if (process.env.DISCORD_TOKEN === undefined) { throw new Error("DISCORD_TOKEN is undefined"); } + const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); (async (env) => { if (process.env.CLIENT_ID === undefined) { throw new Error("DISCORD_TOKEN is undefined"); } + if (process.env.GUILD_ID === undefined) { + throw new Error("GUILD_ID is undefined"); + } try { console.log("Started refreshing application (/) commands."); if (env === "production") { console.log("Started refreshing guild (/) commands."); - await rest.put(Routes.applicationGuildCommands(process.env.CLIENT_ID, KAMPUS_GUILD_ID), { + await rest.put(Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID), { body: commands, }); } diff --git a/fly.dev.toml b/fly.dev.toml new file mode 100644 index 0000000..9504998 --- /dev/null +++ b/fly.dev.toml @@ -0,0 +1,23 @@ +# fly.toml app configuration file generated for kampus-discord-bot on 2023-11-26T10:45:21-08:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = "bot-dev" +primary_region = "sjc" + +[build] +dockerfile = "Dockerfile" + +[http_service] +internal_port = 3000 +force_https = true +auto_stop_machines = true +auto_start_machines = true +min_machines_running = 1 +processes = ["app"] + +[[vm]] +cpu_kind = "shared" +cpus = 1 +memory_mb = 512 diff --git a/package.json b/package.json index 787294e..80a8526 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "scripts": { "start": "nodemon index.ts", "test": "echo \"Error: no test specified\" && exit 1", - "dev": "tsx ./index.ts" + "dev": "tsx ./index.ts", + "set-secrets-dev": "flyctl secrets set --app bot-dev", + "set-secrets": "flyctl secrets set --app kampus-discord-bot" }, "keywords": [], "author": "", diff --git a/src/commands/assign-role.ts b/src/commands/assign-role.ts index 10a104b..1dceb70 100644 --- a/src/commands/assign-role.ts +++ b/src/commands/assign-role.ts @@ -1,6 +1,4 @@ import { Client, CommandInteraction, SlashCommandBuilder } from "discord.js"; -// @ts-ignore -import members from "../../grouped.json" assert { type: "json" }; export async function assignRole(client: Client, guildID: string, userID: string) { const guild = client.guilds.cache.get(guildID); // Replace with your guild ID @@ -8,10 +6,6 @@ export async function assignRole(client: Client, guildID: string, userID: string console.error("Guild not found."); return; } - if (!members) { - console.error("You should run get-all-members first"); - return; - } try { const role = guild.roles.cache.find((r) => r.name === "v2"); // Replace with the role name @@ -36,7 +30,8 @@ export async function assignRole(client: Client, guildID: string, userID: string const assignRoleCommand = { data: new SlashCommandBuilder() .setName("assign-role") - .setDescription("Assignes a role to a user"), + .setDescription("Assignes a role to a user") + .addUserOption((option) => option.setName("user").setDescription("User to assign role to")), async execute(interaction: CommandInteraction) { const client = interaction.client; const guildID = interaction.guildId; @@ -45,12 +40,10 @@ const assignRoleCommand = { return; } - for (const member of Object.keys(members)) { - await assignRole(client, guildID, member); - } + const member = interaction.options.getUser("user"); - console.log("Assigned roles so far: ", Object.keys(members).length); - console.log("Finished assigning roles."); + await assignRole(client, guildID, member?.id ?? ""); + console.log("Finished assigning role."); }, }; diff --git a/src/commands/daily-morning-question.ts b/src/commands/daily-morning-question.ts index 732e78e..d095294 100644 --- a/src/commands/daily-morning-question.ts +++ b/src/commands/daily-morning-question.ts @@ -1,36 +1,83 @@ -import { CommandInteraction, Constants, SlashCommandBuilder, TextChannel } from "discord.js"; -import { GUNAYDIN_CHANNEL_ID } from "../../config"; +import _ from "lodash"; +import { + CommandInteraction, + Message, + SlashCommandBuilder, + TextChannel, + messageLink, +} from "discord.js"; +import { fetchMessages } from "../features/fetch-messages"; +import { isTodayDailyQuestionFound } from "../features/is-daily-msg-exist"; -module.exports = { +const dailyMorningQuestion = { data: new SlashCommandBuilder() .setName("gunun-sorusu") - .setDescription("Günün sorusunu oluşturur."), + .setDescription("Creates 'Günün sorusu' thread.") + .addNumberOption((option) => option.setName("count").setDescription("Günün sorusu sayısı")), async execute(interaction: CommandInteraction) { - const channel = (await interaction.client.channels.fetch(GUNAYDIN_CHANNEL_ID)) as TextChannel; - - const today = new Date(); - const options = { - month: "long", - day: "numeric", - } as Intl.DateTimeFormatOptions; - const formattedDate = new Intl.DateTimeFormat("tr-TR", options).format(today); - - const thread = await channel.threads.create({ - name: "Günün Sorusu - " + formattedDate, - }); - - const geyik = `<:geyik:1161771822350602260>`; - const threadMention = `<#${thread.id}>`; - const message = await channel.send(threadMention); - const threadMessage = await thread.send( - `Volkan abi ve geyikler günün sorusunu soruyor!\n ${geyik} ${geyik} ${geyik} ${geyik} ${geyik} ${geyik}` - ); - const messagePin = await message.pin(); - Promise.all([threadMessage, messagePin, message]); - - interaction.reply({ - content: `Günün sorusu başarıyla oluşturuldu! Thread icin: ${threadMention}`, - ephemeral: true, - }); + if (!process.env.GUNAYDIN_CHANNEL_ID) { + throw new Error("GUNAYDIN_CHANNEL_ID environment variable is not set."); + } + + const channel = (await interaction.client.channels.fetch( + process.env.GUNAYDIN_CHANNEL_ID ?? "" + )) as TextChannel; + const botId = "1183616536682958899"; + + let messages = (await fetchMessages( + interaction.client, + process.env.GUNAYDIN_CHANNEL_ID, + 1, + botId + )) as Message[]; + + if (!messages) { + console.log("No messages found sent by the bot."); + messages = []; + } + + const [isFound, m, c] = isTodayDailyQuestionFound(messages); + const count = (interaction.options.get("count")?.value as number) ?? c; + + if (isFound && m) { + await interaction.deferReply({ + ephemeral: true, + }); + await interaction.editReply({ + content: `Günün sorusu zaten oluşturulmuş! ${messageLink( + m.channelId, + m.id, + m.guildId ?? "" + )}`, + }); + } else { + const questionTitle = `Günün Sorusu - ${count}`; + const threadMention = buildDailyMorningQuestion(channel, questionTitle); + await interaction.deferReply({ + ephemeral: true, + }); + await interaction.editReply({ + content: `Günün sorusu başarıyla oluşturuldu! Thread icin: ${threadMention}`, + }); + } }, }; + +export default dailyMorningQuestion; + +const buildDailyMorningQuestion = async (channel: TextChannel, questionTitle: string) => { + const thread = await channel.threads.create({ + name: questionTitle, + }); + + const geyik = `<:geyik:1161771822350602260>`; + const threadMention = `<#${thread.id}>`; + const message = await channel.send(threadMention); + const threadMessage = await thread.send( + `Volkan abi ve geyikler günün sorusunu soruyor!\n ${geyik} ${geyik} ${geyik} ${geyik} ${geyik} ${geyik}` + ); + const messagePin = await message.pin(); + Promise.all([threadMessage, messagePin, message]); + + return threadMention; +}; diff --git a/src/commands/get-all-members.ts b/src/commands/get-all-members.ts index fffc4aa..cc8f8bb 100644 --- a/src/commands/get-all-members.ts +++ b/src/commands/get-all-members.ts @@ -29,6 +29,7 @@ export async function fetchAllMembers(client: Client, guildID: string) { }); console.log("Finished fetching members. Total members:", membersCollected.length); writeToFile(membersCollected); + return membersCollected; } catch (error) { console.error("Error fetching members:", error); } diff --git a/src/commands/read-all-messages.ts b/src/commands/read-all-messages.ts index dfdf1f1..94a66dc 100644 --- a/src/commands/read-all-messages.ts +++ b/src/commands/read-all-messages.ts @@ -1,108 +1,7 @@ -import { Client, CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; -import { setTimeout } from "timers/promises"; -import { writeFile } from "fs"; -import { formatDate } from "../utils"; +import { CommandInteraction, SlashCommandBuilder } from "discord.js"; import _ from "lodash"; - -type Message = { - author: string; - content: string; - uid: string; - createdAt: string; -}; - -const getDaysAgo = (n: number) => { - const today = new Date(); - const endDate = new Date(today.getTime() - n * 24 * 60 * 60 * 1000); - endDate.setHours(0, 0, 0, 0); - return endDate.getTime(); -}; - -async function fetchMessages( - client: Client, - channelID: string, - days = 7, - userID: string | undefined -) { - const channel = (await client.channels.fetch(channelID)) as TextChannel; - // get ${days} days ago from today in milliseconds from 00:00:00 - const nDaysAgo = getDaysAgo(days); - - let lastId: string | undefined; - let messagesCollected: Message[] = []; - - while (true) { - const messages = await channel.messages.fetch({ - limit: 100, - before: lastId, - }); - - // Stop if no more messages are retrieved - if (messages.size === 0) { - // if there are no messages in the chat - if (messagesCollected.length === 0) { - console.log("No messages collected."); - return; - } - // if there are no more messages to fetch - console.log("No more messages to fetch."); - break; - } - - // Filter and collect messages - const filteredMessages = messages.filter((m) => { - return m.createdTimestamp > nDaysAgo; - }); - filteredMessages.toJSON().forEach((m) => - messagesCollected.push({ - author: m.author.username, - content: m.content, - uid: m.author.id, - createdAt: formatDate(m.createdTimestamp), - }) - ); - - if (filteredMessages.size === 0) { - const grouped = _.groupBy(messagesCollected, "uid"); - if (userID) { - const formattedDate = new Intl.DateTimeFormat("tr-TR", { - month: "2-digit", - year: "2-digit", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - }).format(new Date()); - writeToFile( - grouped[userID], - `./messages/u:${userID}-c:${channelID}-a:${days}-d:${formattedDate}.json` - ); - return grouped[userID]; - } - writeToFile(grouped, "grouped.json"); - return grouped; - } - - // Update lastId for the next iteration - lastId = messages.lastKey(); - console.log(messagesCollected.length, " messages collected so far."); - - await setTimeout(1000); // Respect rate limits - } -} - -async function writeToFile( - messages: any, - fileName = `messages_${new Date().toUTCString()}${Object.keys(messages).length}.json` -) { - writeFile(fileName, JSON.stringify(messages, null, 2), (err) => { - if (err) { - console.error("Error writing file:", err); - } else { - console.log("Messages saved to messages.json"); - } - }); -} +import { fetchMessages } from "../features/fetch-messages"; +import { writeToFile } from "../utils"; const getMessages = { data: new SlashCommandBuilder() @@ -120,10 +19,31 @@ const getMessages = { .addUserOption((option) => option.setName("user-name").setDescription("user name")), async execute(interaction: CommandInteraction) { const client = interaction.client; + // typing with as since they are required by the command const channelID = interaction.options.get("channel-name")?.value as string; - const userID = interaction.options.get("user-name")?.value?.toString(); const days = interaction.options.get("days")?.value as number; + const userID = interaction.options.get("user-name")?.value?.toString(); const messages = await fetchMessages(client, channelID, days, userID); + let fileName = "grouped.json"; + + if (userID) { + const formattedDate = new Intl.DateTimeFormat("tr-TR", { + month: "2-digit", + year: "2-digit", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }).format(new Date()); + fileName = `./u:${userID}-c:${channelID}-a:${days}-d:${formattedDate}.json`; + } + + const [saved, err] = await writeToFile(messages, fileName); + if (saved) { + console.log(`Messages are written to ${saved}`); + } else { + console.log(err); + } }, }; diff --git a/src/commands/recycle-v2-roles.ts b/src/commands/recycle-v2-roles.ts index cbe15c6..bdca2ad 100644 --- a/src/commands/recycle-v2-roles.ts +++ b/src/commands/recycle-v2-roles.ts @@ -1,49 +1,67 @@ import { CommandInteraction, SlashCommandBuilder, TextChannel } from "discord.js"; // @ts-ignore -import members from "../../grouped.json" assert { type: "json" }; +import { fetchMessages } from "../features/fetch-messages"; +import { client } from "../../createDiscordClient"; const recycleV2Roles = { data: new SlashCommandBuilder() - .setDescription("Recycle v2 roles. Before running this command run get-messages") - .setName("recycle-v2-roles"), + .setDescription("Recycle v2 roles.") + .setName("recycle-v2-roles") + .addBooleanOption((option) => option.setName("dry-run").setDescription("Dry run")), async execute(interaction: CommandInteraction) { + const dryRun = interaction.options.get("dry-run")?.value as boolean; const { guild } = interaction; - if (!guild) return; + if (!guild) { + console.error("Guild not found."); + return; + } - const rolesToDelete = guild.roles.cache.find((role) => role.name.startsWith("v2")); - if (!rolesToDelete) { + const roleToDelete = guild.roles.cache.find((role) => role.name.startsWith("v2")); + if (!roleToDelete) { console.error("Role not found."); return; } // find all members with v2 role const membersWithV2Role = guild.members.cache.filter((member) => - member.roles.cache.some((role) => role.name.startsWith("v2")) + member.roles.cache.has(roleToDelete.id) ); - // remove v2 role from members who are not in the grouped.json file + if (!process.env.GUNAYDIN_CHANNEL_ID || !process.env.GIDENLER_CHANNEL_ID) { + throw new Error("GUNAYDIN_CHANNEL_ID environment variable is not set."); + } + + const membersWithMessages = await fetchMessages(client, process.env.GUNAYDIN_CHANNEL_ID, 14); + let count = 0; let removedUsers: String[] = []; membersWithV2Role.forEach((member) => { - const memberData = members[member.user.id]; + const memberData = membersWithMessages && membersWithMessages[member.user.id]; if (!memberData) { - console.log(`Removing v2 role from ${member.user.tag}`); - member.roles.remove(rolesToDelete); + if (!dryRun) { + console.log(`Removing v2 role from ${member.user.tag}`); + member.roles.remove(roleToDelete); + } removedUsers.push(member.user.tag); count++; } }); + const removedUsersList = + removedUsers.length > 0 + ? `These are returned users: ${removedUsers.map((user) => { + return `\n${user}`; + })}` + : ""; + + let message = dryRun + ? `[DRYRUN] Done. ${count} members will be purged from v2 role.` + : `Done. ${count} members were removed from v2 role.`; + const channel = interaction.client.channels.cache.get( - process.env.GIDENLER_CHANNEL_ID ?? "" + process.env.GIDENLER_CHANNEL_ID ) as TextChannel; if (!channel) return; - await channel.send( - `Done. ${count} members were removed from v2 role. These are removed users: ${removedUsers.map( - (user) => { - return `\n${user}`; - } - )}` - ); + await channel.send(`${message} ${removedUsersList}`); }, }; diff --git a/src/events/gunaydin-message-create.ts b/src/events/gunaydin-message-create.ts index a8a2ed7..1caa1d8 100644 --- a/src/events/gunaydin-message-create.ts +++ b/src/events/gunaydin-message-create.ts @@ -1,6 +1,5 @@ import { Events, Message } from "discord.js"; import { assignRole } from "../commands/assign-role"; -import { GUNAYDIN_CHANNEL_ID } from "../../config"; const messageCreateInGunaydinChannel = { name: Events.MessageCreate, @@ -12,7 +11,7 @@ const messageCreateInGunaydinChannel = { return; } - if (message.channel.id === GUNAYDIN_CHANNEL_ID) { + if (message.channel.id === process.env.GUNAYDIN_CHANNEL_ID) { const userRoles = message.member?.roles.cache.map((role) => role.name); // if user has v2 role, do nothing diff --git a/src/events/soru-cevap-message-create.ts b/src/events/soru-cevap-message-create.ts index aa085fa..976b034 100644 --- a/src/events/soru-cevap-message-create.ts +++ b/src/events/soru-cevap-message-create.ts @@ -1,9 +1,10 @@ import { Events, Message, ThreadChannel, bold } from "discord.js"; -import { SORU_CEVAP_CHANNEL_ID } from "../../config"; const isFirstMessageInSoruCevapChannel = (msg: Message, channel: ThreadChannel) => { return ( - channel.messageCount === 1 && msg.position === 0 && channel.parentId === SORU_CEVAP_CHANNEL_ID + channel.messageCount === 1 && + msg.position === 0 && + channel.parentId === process.env.SORU_CEVAP_CHANNEL_ID ); }; diff --git a/src/features/fetch-messages.ts b/src/features/fetch-messages.ts new file mode 100644 index 0000000..480b4c0 --- /dev/null +++ b/src/features/fetch-messages.ts @@ -0,0 +1,68 @@ +import { Client, TextChannel, Message } from "discord.js"; +import { formatDate } from "../utils"; +import { setTimeout } from "timers/promises"; +import _ from "lodash"; + +const MESSAGE_FETCH_LIMIT = 100; + +const getDaysAgo = (n: number) => { + const today = new Date(); + const endDate = new Date(today.getTime() - n * 24 * 60 * 60 * 1000); + endDate.setHours(0, 0, 0, 0); + return endDate.getTime(); +}; + +const fetchMessagesFromChannel = async (channel: TextChannel, beforeId?: string) => { + try { + return await channel.messages.fetch({ + limit: MESSAGE_FETCH_LIMIT, + before: beforeId, + }); + } catch (error) { + console.error("Error fetching messages:", error); + throw error; + } +}; + +export async function fetchMessages(client: Client, channelID: string, days = 7, userID?: string) { + try { + const channel = (await client.channels.fetch(channelID)) as TextChannel; + const nDaysAgo = getDaysAgo(days); + + let lastId: string | undefined; + let messagesCollected: any[] = []; + + while (true) { + const messages = await fetchMessagesFromChannel(channel, lastId); + + if (messages.size === 0) break; + + messages.forEach((m) => { + if (m.createdTimestamp > nDaysAgo) { + messagesCollected.push({ + id: m.id, + guildId: m.guildId, + channelId: m.channelId, + author: m.author, + content: m.content, + uid: m.author.id, + createdAt: formatDate(m.createdTimestamp), + }); + } + }); + + // Update lastId for the next iteration + lastId = messages.lastKey(); + console.log(messagesCollected.length, " messages collected so far."); + + await setTimeout(1000); // Respect rate limits + } + + console.log("Done. Messages collected:", messagesCollected.length); + const groupedMessages = _.groupBy(messagesCollected, "uid"); + return userID ? groupedMessages[userID] : groupedMessages; + } catch (error) { + console.error("Error in fetchMessages:", error); + return []; + } +} diff --git a/src/features/is-daily-msg-exist.ts b/src/features/is-daily-msg-exist.ts new file mode 100644 index 0000000..3ca69de --- /dev/null +++ b/src/features/is-daily-msg-exist.ts @@ -0,0 +1,17 @@ +import { Message } from "discord.js"; + +export const isTodayDailyQuestionFound = (messages: Message[]) => { + const gununSorusuAll = messages.filter((message) => message.content.includes("Günün Sorusu")); + const message = gununSorusuAll[0]; + + if (!message) { + return [false, undefined, 0] as const; + } + + const count = message.content.split("-")[1].trim(); + // createdAt thinks it's a Date but in reality it's a string + const day = (message.createdAt as unknown as string).split(" ")[0]; + const isFound = day === new Date().getDate().toString(); + + return [isFound, message, parseInt(count) + 1] as const; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 94fa16f..0b8aaf6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,7 @@ import { DiscordClient } from "../../createDiscordClient"; import path from "path"; import { fileURLToPath } from "url"; +import { writeFile } from "fs"; export const isUserInThisGuild = async (client: DiscordClient, userID: string, guildID: string) => { const guild = await client.guilds.fetch(guildID); @@ -43,3 +44,15 @@ export function formatDate(date: Date | number | string) { return `${days} ${monthName} ${year}, ${hours}:${minutes}:${seconds}`; } + +export async function writeToFile(messages: any, fileName: string) { + let result: (NodeJS.ErrnoException | null)[] | (string | null)[] = []; + writeFile(fileName, JSON.stringify(messages, null, 2), (err) => { + if (err) { + result = [null, err]; + } else { + result = [fileName, null]; + } + }); + return result; +}