Compare commits
17 Commits
automated-
...
f29459aadb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f29459aadb | ||
|
|
2cd5eee940 | ||
|
|
24296c2ae4 | ||
|
|
db065c3ef0 | ||
|
|
697025ec1e | ||
|
|
3350b9f191 | ||
|
|
961a7cc2bd | ||
|
|
2c3cc18474 | ||
|
|
424d5ae749 | ||
|
|
5c86185ef5 | ||
|
|
e6de0f4453 | ||
|
|
e8cfca1d8d | ||
|
|
dce0086fdb | ||
|
|
ad45d8f0ea | ||
|
|
2c5cf3dac0 | ||
|
|
a3223b716e | ||
|
|
7a246f9e2a |
@@ -25,14 +25,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with: # replace it with your local IP
|
||||
config-inline: |
|
||||
[registry."${{ secrets.LOCAL_GITEA_IP}}:3000"]
|
||||
http = true
|
||||
insecure = true
|
||||
[registry."git.vpn.cusano.net"]
|
||||
http = false
|
||||
insecure = false
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ secrets.LOCAL_GITEA_IP}}:3000 # replace it with your local IP
|
||||
registry: git.vpn.cusano.net # replace it with your local IP
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
@@ -52,5 +52,5 @@ jobs:
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: | # replace it with your local IP and tags
|
||||
${{ secrets.LOCAL_GITEA_IP}}:3000/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||
${{ secrets.LOCAL_GITEA_IP}}:3000/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||
git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||
git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||
@@ -15,7 +15,7 @@ RUN npm install
|
||||
COPY . .
|
||||
|
||||
# Expose the port on which your Node.js application will run
|
||||
EXPOSE 3000
|
||||
EXPOSE 3420
|
||||
|
||||
# Command to run the Node.js application
|
||||
CMD ["node", "."]
|
||||
|
||||
53
discordBot/addons/gptInteraction.mjs
Normal file
53
discordBot/addons/gptInteraction.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
import { DebugBuilder } from "../../modules/debugger.mjs";
|
||||
const log = new DebugBuilder("server", "discordBot.addons.gptInteraction");
|
||||
import { gptHandler } from "../modules/gptHandler.mjs";
|
||||
|
||||
export const gptInteraction = async (nodeIo, message) => {
|
||||
let conversation = [];
|
||||
|
||||
let prevMessages = await message.channel.messages.fetch({ limit: 10 });
|
||||
prevMessages.reverse();
|
||||
|
||||
prevMessages.forEach((msg) => {
|
||||
// Check if the message was sent within the last 24 hours
|
||||
if (new Date().getTime() - msg.createdTimestamp > (24 * 60 * 60 * 1000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's from a bot other than the server
|
||||
if (msg.author.bot && msg.author.id !== nodeIo.serverClient.user.id) return;
|
||||
|
||||
const username = msg.author.username.replace(/\s+/g, '_').replace(/[^\w\s]/gi, '');
|
||||
|
||||
if (msg.author.id === nodeIo.serverClient.user.id) {
|
||||
conversation.push({
|
||||
role: 'assistant',
|
||||
//name: msg.author.id,
|
||||
content: msg.content,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.push({
|
||||
role: 'user',
|
||||
//name: msg.author.id,
|
||||
content: msg.content.replace(`<@${nodeIo.serverClient.user.id}>`, ''),
|
||||
});
|
||||
});
|
||||
const response = await gptHandler(conversation);
|
||||
if (response) {
|
||||
const responseMessage = response;
|
||||
const chunkSize = 2500;
|
||||
|
||||
for (let i = 0; i < responseMessage.length; i += chunkSize) {
|
||||
const chunk = responseMessage.substring(i, i + chunkSize);
|
||||
|
||||
log.DEBUG("Sending message chunk:", chunk);
|
||||
|
||||
await message.reply(chunk);
|
||||
}
|
||||
} else {
|
||||
message.channel.send('Sorry, I encountered an error while processing your request.');
|
||||
}
|
||||
}
|
||||
83
discordBot/addons/linkCop.mjs
Normal file
83
discordBot/addons/linkCop.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
import { DebugBuilder } from "../../modules/debugger.mjs";
|
||||
const log = new DebugBuilder("server", "discordBot.addons.linkCop");
|
||||
import { gptHandler } from "../modules/gptHandler.mjs";
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const approvedLinksChannel = "767303243285790721";
|
||||
const restrictedChannelIds = process.env.LINKCOP_RESTRICTED_CHANNEL_IDS.split(',');
|
||||
|
||||
const linkRegExp = /(?:http[s]?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/g
|
||||
|
||||
export const linkCop = async (nodeIo, message) => {
|
||||
if (message.channel.id == approvedLinksChannel || !restrictedChannelIds.includes(message.channel.id)) return false;
|
||||
|
||||
const urls = String(message.content).matchAll(linkRegExp);
|
||||
if (!urls || urls.length === 0) return false;
|
||||
log.DEBUG("Found URLs: ", urls);
|
||||
|
||||
let conversation = [];
|
||||
|
||||
let prevMessages = await message.channel.messages.fetch({ limit: 2 });
|
||||
prevMessages.reverse();
|
||||
|
||||
prevMessages.forEach((msg) => {
|
||||
// Check if the message was sent within the last 5 minutes
|
||||
if (new Date().getTime() - msg.createdTimestamp > (5 * 60 * 1000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's from a bot other than the server
|
||||
if (msg.author.bot && msg.author.id !== nodeIo.serverClient.user.id) return;
|
||||
|
||||
const username = msg.author.username.replace(/\s+/g, '_').replace(/[^\w\s]/gi, '');
|
||||
|
||||
if (msg.author.id === nodeIo.serverClient.user.id) {
|
||||
conversation.push({
|
||||
role: 'assistant',
|
||||
//name: msg.author.id,
|
||||
content: msg.content,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.push({
|
||||
role: 'user',
|
||||
//name: msg.author.id,
|
||||
content: msg.content.replace(`<@${nodeIo.serverClient.user.id}>`, ''),
|
||||
});
|
||||
});
|
||||
|
||||
conversation.push({
|
||||
role: 'assistant',
|
||||
content: `There has been a link posted to a channel that links are not allowed in. The above messages are from the channel that links are not allowed including the message with the link. The message with the link is going to be deleted and moved to the '#links' channels. You are replying to the message with the link to let the user know.`
|
||||
});
|
||||
|
||||
const response = await gptHandler(conversation);
|
||||
|
||||
if (response) {
|
||||
const responseMessage = response;
|
||||
const chunkSize = 2000;
|
||||
|
||||
for (let i = 0; i < responseMessage.length; i += chunkSize) {
|
||||
const chunk = responseMessage.substring(i, i + chunkSize);
|
||||
|
||||
log.DEBUG("Sending message chunk:", chunk);
|
||||
|
||||
await message.reply(chunk);
|
||||
}
|
||||
|
||||
const messageContent = {
|
||||
'author': message.author,
|
||||
'content': `<@${message.author.id}> - ${String(message.content)}`,
|
||||
'channelId': message.channelId,
|
||||
'links': urls
|
||||
}
|
||||
|
||||
await message.delete();
|
||||
log.DEBUG("Message content: ", messageContent);
|
||||
|
||||
message.client.channels.cache.get(approvedLinksChannel).send(messageContent);
|
||||
}
|
||||
}
|
||||
52
discordBot/commands/connections.mjs
Normal file
52
discordBot/commands/connections.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import { DebugBuilder } from "../../modules/debugger.mjs";
|
||||
const log = new DebugBuilder("server", "discordBot.command.ping");
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
|
||||
// Exporting data property that contains the command structure for discord including any params
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('connections')
|
||||
.setDescription('Check to see what bots are online.');
|
||||
|
||||
// Exporting other properties
|
||||
export const example = "/connections"; // An example of how the command would be run in discord chat, this will be used for the help command
|
||||
export const deferInitialReply = false; // If we the initial reply in discord should be deferred. This gives extra time to respond, however the method of replying is different.
|
||||
|
||||
/**
|
||||
* Function to give the user auto-reply suggestions
|
||||
* @param {any} nodeIo The nodeIO server for manipulation of sockets
|
||||
* @param {any} interaction The interaction object
|
||||
*/
|
||||
/*
|
||||
export async function autocomplete(nodeIo, interaction) {
|
||||
const focusedValue = interaction.options.getFocused();
|
||||
const choices = []; // The array to be filled with the autocorrect values
|
||||
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
|
||||
log.INFO(focusedValue, choices, filtered);
|
||||
await interaction.respond(filtered.map(choice => ({name: choice.name, value: choice.name})));
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The function to run when the command is called by a discord user
|
||||
* @param {any} nodeIo The nodeIO server for manipulation of sockets
|
||||
* @param {any} interaction The interaction object
|
||||
*/
|
||||
export const execute = async (nodeIo, interaction) => {
|
||||
try {
|
||||
const sockets = await nodeIo.allSockets();
|
||||
log.DEBUG("All open sockets: ",sockets);
|
||||
let socketMessage = "";
|
||||
|
||||
// Create the message for discord with each socket on a new line
|
||||
sockets.forEach(socket => {
|
||||
socketMessage += `\n${socket}`
|
||||
});
|
||||
|
||||
await interaction.reply(`**Online Sockets: '${socketMessage}'**`);
|
||||
//await interaction.reply('**Pong.**');
|
||||
//await interaction.channel.send('**Pong.**');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
@@ -54,18 +54,19 @@ export const execute = async (nodeIo, interaction) => {
|
||||
var category = interaction.options.getString('category');
|
||||
|
||||
if (!category) category = "ALL";
|
||||
await interaction.reply(`Adding ${title} to the list of RSS sources, please wait...`);
|
||||
|
||||
await addSource(title, link, category, interaction.guildId, interaction.channelId, (err, result) => {
|
||||
log.DEBUG("Result from adding entry", result);
|
||||
|
||||
if (result) {
|
||||
interaction.reply(`Successfully added ${title} to the list of RSS sources`);
|
||||
interaction.editReply(`Successfully added ${title} to the list of RSS sources`);
|
||||
} else {
|
||||
interaction.reply(`${title} already exists in the list of RSS sources`);
|
||||
interaction.editReply(`${title} already exists in the list of RSS sources`);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
log.ERROR(err)
|
||||
await interaction.reply(err.toString());
|
||||
await interaction.editReply(err.toString());
|
||||
}
|
||||
}
|
||||
@@ -42,17 +42,17 @@ export async function autocomplete(nodeIo, interaction) {
|
||||
export const execute = async (nodeIo, interaction) => {
|
||||
try {
|
||||
var title = interaction.options.getString('title');
|
||||
interaction.reply(`Removing ${title} from the list of RSS sources, please wait...`);
|
||||
await interaction.reply(`Removing ${title} from the list of RSS sources, please wait...`);
|
||||
|
||||
const results = await deleteFeedByTitle(title);
|
||||
if (!results) {
|
||||
log.WARN(`Failed to remove source: ${title}`);
|
||||
interaction.editReply(`Failed to remove source: '${title}'`);
|
||||
await interaction.editReply(`Failed to remove source: '${title}'`);
|
||||
return;
|
||||
}
|
||||
interaction.editReply(`${title} was successfully removed from the RSS sources.`)
|
||||
await interaction.editReply(`${title} was successfully removed from the RSS sources.`)
|
||||
} catch (err) {
|
||||
log.ERROR(err)
|
||||
interaction.editReply(err.toString());
|
||||
await interaction.editReply(err.toString());
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ export const execute = async (nodeIo, interaction) => {
|
||||
//await interaction.reply(`**Online Sockets: '${sockets}'**`);
|
||||
await interaction.reply('Triggering RSS update');
|
||||
await updateFeeds(interaction.client);
|
||||
await interaction.editReply('RSS Update Completed');
|
||||
//await interaction.channel.send('**Pong.**');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -79,7 +79,7 @@ export function addEnabledEventListeners(serverClient, _eventsPath = "./events")
|
||||
}
|
||||
|
||||
// The discord client
|
||||
export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
|
||||
export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildPresences] });
|
||||
|
||||
// Run when the bot is ready
|
||||
serverClient.on('ready', async () => {
|
||||
|
||||
16
discordBot/events/guildCreate.mjs
Normal file
16
discordBot/events/guildCreate.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { DebugBuilder } from "../../modules/debugger.mjs";
|
||||
const log = new DebugBuilder("server", "discordBot.events.guildCreate");
|
||||
import { Events } from 'discord.js';
|
||||
import { addEnabledCommands, addEnabledEventListeners } from "../discordBot.mjs";
|
||||
|
||||
export const name = Events.GuildMemberAdd;
|
||||
|
||||
export async function execute(nodeIo, guild) {
|
||||
log.INFO("Bot has joined a new server", guild);
|
||||
|
||||
log.DEBUG("Refreshing commands enabled");
|
||||
await addEnabledCommands(nodeIo.serverClient);
|
||||
|
||||
log.DEBUG("Refreshing events enabled");
|
||||
await addEnabledEventListeners(nodeIo.serverClient);
|
||||
}
|
||||
34
discordBot/events/guildMemberAdd.mjs
Normal file
34
discordBot/events/guildMemberAdd.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { DebugBuilder } from "../../modules/debugger.mjs";
|
||||
const log = new DebugBuilder("server", "discordBot.events.guildMemberAdd");
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
import { Events } from 'discord.js';
|
||||
import { gptHandler } from "../modules/gptHandler.mjs";
|
||||
|
||||
const welcomeChannel = process.env.WELCOME_CHANNEL_ID;
|
||||
|
||||
export const name = Events.GuildMemberAdd;
|
||||
|
||||
export async function execute(nodeIo, member) {
|
||||
log.INFO("New user joined the server", member);
|
||||
let conversation = [];
|
||||
conversation.push({
|
||||
role: 'system',
|
||||
content: `There has been a new user that joined. Their name is '<@${member.id}>'. Please welcome them to the server and remind them about the rules.`
|
||||
})
|
||||
|
||||
const response = await gptHandler(conversation);
|
||||
if (response) {
|
||||
const responseMessage = response.choices[0].message.content;
|
||||
const chunkSize = 2500;
|
||||
|
||||
for (let i = 0; i < responseMessage.length; i += chunkSize) {
|
||||
const chunk = responseMessage.substring(i, i + chunkSize);
|
||||
|
||||
log.DEBUG("Sending message chunk:", chunk);
|
||||
|
||||
await nodeIo.serverClient.channels.cache.get(welcomeChannel).send(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
discordBot/events/messageCreate.mjs
Normal file
29
discordBot/events/messageCreate.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import { DebugBuilder } from "../../modules/debugger.mjs";
|
||||
const log = new DebugBuilder("server", "discordBot.events.messageCreate");
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
import { Events } from 'discord.js';
|
||||
import { gptInteraction } from '../addons/gptInteraction.mjs';
|
||||
import { linkCop } from '../addons/linkCop.mjs';
|
||||
|
||||
const IGNORED_CHANNELS = process.env.IGNORED_CHANNEL_IDS.split(',');
|
||||
|
||||
export const name = Events.MessageCreate;
|
||||
|
||||
export async function execute(nodeIo, message) {
|
||||
// Ignore ignored channels
|
||||
if (IGNORED_CHANNELS.includes(message.channel.id)) return;
|
||||
|
||||
// Ignore messages from a bot
|
||||
if (message.author.bot) return;
|
||||
|
||||
log.INFO("Message create", message);
|
||||
|
||||
// Check if the message mentions the bot
|
||||
if (message.mentions.users.has(nodeIo.serverClient.user.id)) {
|
||||
return await gptInteraction(nodeIo, message);
|
||||
}
|
||||
|
||||
// Check if the message contains a link in a channel it shouldn't
|
||||
if (await linkCop(nodeIo, message)) return;
|
||||
}
|
||||
122
discordBot/modules/gptHandler.mjs
Normal file
122
discordBot/modules/gptHandler.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
import { DebugBuilder } from "../../modules/debugger.mjs";
|
||||
const log = new DebugBuilder("server", "discordBot.modules.gptHandler");
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import { OpenAI } from 'openai';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const openai = new OpenAI(process.env.OPENAI_API_KEY);
|
||||
|
||||
const assistant = await openai.beta.assistants.create({
|
||||
name: "Emmelia",
|
||||
instructions: process.env.DRB_SERVER_INITIAL_PROMPT,
|
||||
model: "gpt-4o",
|
||||
});
|
||||
|
||||
class EventHandler extends EventEmitter {
|
||||
constructor(client) {
|
||||
super();
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async onEvent(event) {
|
||||
try {
|
||||
console.log(event);
|
||||
// Retrieve events that are denoted with 'requires_action'
|
||||
// since these will have our tool_calls
|
||||
if (event.event === "thread.run.requires_action") {
|
||||
await this.handleRequiresAction(
|
||||
event.data,
|
||||
event.data.id,
|
||||
event.data.thread_id,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling event:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRequiresAction(data, runId, threadId) {
|
||||
try {
|
||||
const toolOutputs =
|
||||
data.required_action.submit_tool_outputs.tool_calls.map((toolCall) => {
|
||||
// Call the function
|
||||
switch (toolCall.function.name) {
|
||||
case "getCurrentTemperature": return {
|
||||
tool_call_id: toolCall.id,
|
||||
output: "57",
|
||||
};
|
||||
}
|
||||
});
|
||||
// Submit all the tool outputs at the same time
|
||||
await this.submitToolOutputs(toolOutputs, runId, threadId);
|
||||
} catch (error) {
|
||||
console.error("Error processing required action:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async submitToolOutputs(toolOutputs, runId, threadId) {
|
||||
try {
|
||||
// Use the submitToolOutputsStream helper
|
||||
const stream = this.client.beta.threads.runs.submitToolOutputsStream(
|
||||
threadId,
|
||||
runId,
|
||||
{ tool_outputs: toolOutputs },
|
||||
);
|
||||
for await (const event of stream) {
|
||||
this.emit("event", event);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting tool outputs:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eventHandler = new EventHandler(openai);
|
||||
eventHandler.on("event", eventHandler.onEvent.bind(eventHandler));
|
||||
|
||||
export const gptHandler = async (additionalMessages) => {
|
||||
const thread = await openai.beta.threads.create();
|
||||
|
||||
// Add the additional messages to the conversation
|
||||
for (const msgObj of additionalMessages) {
|
||||
await openai.beta.threads.messages.create(
|
||||
thread.id,
|
||||
msgObj
|
||||
);
|
||||
}
|
||||
|
||||
log.DEBUG("AI Conversation:", thread);
|
||||
|
||||
// Run the thread to get a response
|
||||
try {
|
||||
const stream = await openai.beta.threads.runs.stream(
|
||||
thread.id,
|
||||
{ assistant_id: assistant.id },
|
||||
eventHandler,
|
||||
);
|
||||
|
||||
for await (const event of stream) {
|
||||
eventHandler.emit("event", event);
|
||||
}
|
||||
|
||||
let response;
|
||||
const messages = await openai.beta.threads.messages.list(
|
||||
thread.id
|
||||
);
|
||||
response = messages.data[0].content[0].text.value;
|
||||
|
||||
log.DEBUG("AI Response:", response);
|
||||
|
||||
if (!response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating response:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
829
package-lock.json
generated
829
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -11,19 +11,20 @@
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"chai": "^5.1.0",
|
||||
"chai": "^5.1.1",
|
||||
"mocha": "^10.4.0",
|
||||
"socket.io-client": "^4.7.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"discord.js": "^14.14.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"mongodb": "^6.3.0",
|
||||
"discord.js": "^14.15.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"mongodb": "^6.7.0",
|
||||
"morgan": "^1.10.0",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"openai": "^4.47.3",
|
||||
"rss-parser": "^3.13.0",
|
||||
"socket.io": "^4.7.2",
|
||||
"user-agents": "^1.1.208"
|
||||
"socket.io": "^4.7.5",
|
||||
"user-agents": "^1.1.222"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DebugBuilder } from "../modules/debugger.mjs";
|
||||
const log = new DebugBuilder("server", "sourceManager");
|
||||
import { createFeed, getFeedByLink, deleteFeedByLink } from '../modules/mongo-wrappers/mongoFeedsWrappers.mjs';
|
||||
|
||||
class SourceManager {
|
||||
|
||||
Reference in New Issue
Block a user