• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Geoffery10 / Stoat-Sync / 22371847595

24 Feb 2026 09:58PM UTC coverage: 85.276% (+0.07%) from 85.208%
22371847595

push

github

Geoffery10
Changed delete errors to warnings

133 of 171 branches covered (77.78%)

Branch coverage included in aggregate %.

2 of 3 new or added lines in 2 files covered. (66.67%)

284 of 318 relevant lines covered (89.31%)

2.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.16
/src/messageHandler.js
1
import { logger } from './logger.js';
2
import axios from 'axios';
3
import fs from 'fs/promises';
4
import { formatMessageForDiscord } from './messageFormatter.js';
5
import { formatMessageForStoat } from './messageFormatter.js';
6
import path from 'path';
7
import FormData from 'form-data';
8
import { createOrGetWebhook } from './webhookHandler.js';
9

10
// Message mapping storage (Stoat message ID -> Discord message ID)
11
export const stoatToDiscordMapping = new Map();
7✔
12
// Message mapping storage (Discord channel ID -> Map of Discord message ID -> Stoat message ID)
13
export const discordToStoatMapping = new Map();
7✔
14

15
export async function uploadAttachmentToStoat(filePath, STOAT_AUTUMN_URL, STOAT_BOT_TOKEN) {
16
    try {
5✔
17
        if (!await fs.access(filePath).then(() => true).catch(() => false)) {
5✔
18
            logger.info(`[!] Attachment not found locally: ${filePath}`);
1✔
19
            return null;
1✔
20
        }
21

22
        const form = new FormData();
4✔
23
        form.append('file', await fs.readFile(filePath), {
4✔
24
            filename: path.basename(filePath),
25
            contentType: 'application/octet-stream'
26
        });
27

28
        const response = await axios.post(`${STOAT_AUTUMN_URL}/attachments`, form, {
4✔
29
            headers: {
30
                ...form.getHeaders(),
31
                'x-bot-token': STOAT_BOT_TOKEN
32
            }
33
        });
34

35
        return response.data?.id || null;
3✔
36
    } catch (error) {
37
        logger.error(`[!] Error uploading file: ${error.message}`);
1✔
38
        return null;
1✔
39
    }
40
}
41

42
export async function sendMessageToDiscord(message, discordChannel, config) {
43
    // Format the message
44
    const formattedContent = await formatMessageForDiscord(message, config);
2✔
45

46
    // Handle attachments
47
    const attachmentFiles = [];
2✔
48
    if (message.attachments && typeof message.attachments === 'object') {
2!
49
        const attachments = Array.isArray(message.attachments) ? message.attachments : [message.attachments];
2!
50
        for (const attachment of attachments) {
2✔
51
            try {
2✔
52
                const response = await fetch(`${config.STOAT_BASE_URL}/autumn/attachments/${attachment.id}`);
2✔
53
                const buffer = await response.arrayBuffer();
2✔
54
                attachmentFiles.push({
2✔
55
                    attachment: Buffer.from(buffer),
56
                    name: attachment.name
57
                });
58
            } catch (error) {
59
                logger.error(`Error downloading attachment ${attachment.name}: ${error.message}`);
×
60
            }
61
        }
62
    }
63

64
    // Get or create webhook
65
    const webhook = await createOrGetWebhook(discordChannel);
2✔
66
    if (!webhook) {
2✔
67
        logger.error('Failed to get or create webhook');
1✔
68
        return null;
1✔
69
    }
70

71
    // Prepare webhook message options
72
    const webhookOptions = {
1✔
73
        content: formattedContent,
74
        username: message.author?.username || 'Stoat User',
1!
75
        avatarURL: message.author?.avatar
1!
76
            ? `${message.author.avatarURL}`
77
            : 'https://i.imgur.com/ykjd3JO.jpeg', // Default Stoat avatar
78
        files: attachmentFiles
79
    };
80

81
    // Send the message via webhook
82
    try {
1✔
83
        const sentMessage = await webhook.send(webhookOptions);
1✔
84
        return sentMessage;
1✔
85
    } catch (error) {
86
        logger.error(`Failed to send message via webhook to Discord: ${error.message}`);
×
87
        return null;
×
88
    }
89
}
90

91
export async function editMessageInDiscord(discordChannel, discordMessageId, message, config) {
92
    // Format the updated message
93
    const formattedContent = await formatMessageForDiscord(message, config);
2✔
94

95
    try {
2✔
96
        // Get the Discord message
97
        const discordMessage = await discordChannel.messages.fetch(discordMessageId);
2✔
98

99
        // Check if the message is from a webhook
100
        if (discordMessage.webhookId) {
2✔
101
            // Get the webhook
102
            const webhook = await createOrGetWebhook(discordChannel);
1✔
103
            if (!webhook) {
1!
104
                logger.error('Failed to get webhook for editing message');
×
105
                return false;
×
106
            }
107

108
            // Edit the webhook message
109
            await webhook.editMessage(discordMessageId, {
1✔
110
                content: formattedContent,
111
                username: message.author?.username || 'Stoat User',
1!
112
                avatarURL: message.author?.avatar
1!
113
                    ? `${message.author.avatarURL}`
114
                    : 'https://i.imgur.com/ykjd3JO.jpeg'
115
            });
116
        } else {
117
            // Edit regular message if not from webhook
118
            await discordMessage.edit(formattedContent);
1✔
119
        }
120

121
        return true;
2✔
122
    } catch (error) {
123
        logger.error(`Failed to update message in Discord: ${error.message}`);
×
124
        return false;
×
125
    }
126
}
127

128
export async function deleteMessageInDiscord(discordChannel, discordMessageId, messageId) {
129
    try {
1✔
130
        // Get the Discord channel and delete the message
131
        const discordMessage = await discordChannel.messages.fetch(discordMessageId);
1✔
132
        await discordMessage.delete();
1✔
133

134
        // Remove from our mapping
135
        stoatToDiscordMapping.delete(messageId);
1✔
136
        return true;
1✔
137
    } catch (error) {
NEW
138
        logger.warn(`Failed to delete message in Discord: ${error.message}`);
×
139
        return false;
×
140
    }
141
}
142

143
export async function sendMessageToStoat(message, stoatChannelId, config) {
144
    // Format the message
145
    const formattedContent = await formatMessageForStoat(message, config);
1✔
146

147
    // Handle attachments
148
    const attachmentIds = [];
1✔
149
    for (const attachment of message.attachments.values()) {
1✔
150
        const filePath = `temp_${attachment.id}_${attachment.name}`;
1✔
151
        try {
1✔
152
            // Download the attachment
153
            const response = await axios.get(attachment.url, { responseType: 'arraybuffer' });
1✔
154
            await fs.writeFile(filePath, response.data);
1✔
155

156
            // Upload to Stoat
157
            const uploadedId = await uploadAttachmentToStoat(filePath, config.STOAT_AUTUMN_URL, config.STOAT_BOT_TOKEN);
1✔
158
            if (uploadedId) {
1!
159
                attachmentIds.push(uploadedId);
×
160
            }
161

162
            // Clean up
163
            await fs.unlink(filePath);
1✔
164
        } catch (error) {
165
            logger.error(`[!] Error uploading file: ${error.message} (File): ${attachment.name}`);
×
166
            if (await fs.access(filePath).then(() => true).catch(() => false)) {
×
167
                await fs.unlink(filePath);
×
168
            }
169
        }
170
    }
171

172
    // Get the best available avatar URL
173
    let avatarUrl;
174
    if (message.member && message.member.avatar) {
1!
175
        // Use server-specific avatar if available
176
        avatarUrl = message.member.avatarURL({ dynamic: true, size: 256 });
1✔
177
    } else if (message.author && message.author.avatar) {
×
178
        // Fall back to global avatar
179
        avatarUrl = message.author.avatarURL({ dynamic: true, size: 256 });
×
180
    }
181

182
    // Prepare payload with masquerade
183
    const payload = {
1✔
184
        content: formattedContent,
185
        attachments: attachmentIds,
186
        masquerade: {
187
            name: message.author?.username || 'Unknown User',
1!
188
            avatar: avatarUrl || undefined
1!
189
        }
190
    };
191

192
    // Send to Stoat
193
    try {
1✔
194
        const response = await axios.post(
1✔
195
            `${config.STOAT_API_URL}/channels/${stoatChannelId}/messages`,
196
            payload,
197
            {
198
                headers: {
199
                    'x-bot-token': config.STOAT_BOT_TOKEN,
200
                    'Content-Type': 'application/json'
201
                }
202
            }
203
        );
204

205
        return response.data?._id;
1✔
206
    } catch (error) {
207
        logger.error(`Failed to send message: ${error.response?.status || 'Unknown'} - ${error.message}`);
×
208
        return null;
×
209
    }
210
}
211

212
export async function editMessageInStoat(stoatChannelId, stoatMessageId, message, config) {
213
    // Format the edited message
214
    const formattedContent = await formatMessageForStoat(message, config);
1✔
215

216
    // Prepare payload
217
    const payload = {
1✔
218
        content: formattedContent
219
    };
220

221
    // Send the edit to Stoat
222
    try {
1✔
223
        await axios.patch(
1✔
224
            `${config.STOAT_API_URL}/channels/${stoatChannelId}/messages/${stoatMessageId}`,
225
            payload,
226
            {
227
                headers: {
228
                    'x-bot-token': config.STOAT_BOT_TOKEN,
229
                    'Content-Type': 'application/json'
230
                }
231
            }
232
        );
233
        return true;
1✔
234
    } catch (error) {
235
        logger.error(`Failed to edit message: ${error.response?.status || 'Unknown'} - ${error.message}`);
×
236
        return false;
×
237
    }
238
}
239

240
export async function deleteMessageInStoat(stoatChannelId, stoatMessageId, config) {
241
    // Delete the message in Stoat
242
    try {
1✔
243
        await axios.delete(
1✔
244
            `${config.STOAT_API_URL}/channels/${stoatChannelId}/messages/${stoatMessageId}`,
245
            {
246
                headers: {
247
                    'x-bot-token': config.STOAT_BOT_TOKEN
248
                }
249
            }
250
        );
251
        return true;
1✔
252
    } catch (error) {
253
        logger.error(`Failed to delete message: ${error.response?.status || 'Unknown'} - ${error.message}`);
×
254
        return false;
×
255
    }
256
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc