Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions src/discord/commands/FetchInternships.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
ApplicationCommandOptionType,
CacheType,
ChatInputCommandInteraction,
EmbedBuilder,
PermissionFlagsBits,
} from 'discord.js';
import getInternshipOppertunitiesJob from '../../jobs/fetchInternships';
import Command from '../classes/Command';
import DiscordClient from '../classes/DiscordClient';
import Category from '../enums/Category';
import fetch from 'node-fetch';

async function generateCompaniesText(
companies: { company: string; jobTitle: string; link: string }[],
) {
const totalCount = companies.length;
const today = new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});

const listForModel = companies
.map((c, i) => `${i + 1}. ${c.company}: [${c.jobTitle}](<${c.link}>)`)
.join('\n');

const prompt = `
${today} Internship Postings Summary
We found ${totalCount} new internships posted today!

Here are some of our favorites
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.

Format: \${c.company}: [\${c.jobTitle}](<\${c.link}>)

INSTRUCTIONS FOR THE MODEL:
- From the list below, select up to 10 best internships and fill items 1-10 using the exact Format above.
- Only output the summary in the exact structure shown (date line, count line, "Here are some of our favorites", numbered list up to 10, and the final line about !fetch). Do NOT add any extra explanation, commentary, or anything else.
- If fewer than 10 top picks exist, only output the items available but keep numbering starting from 1.
- ALWAYS use the count ${totalCount} in the "We found" line.

AVAILABLE INTERNSHIPS:
${listForModel}
`.trim();

try {
const res = await fetch(
'https://gemini.googleapis.com/v1/models/gemini-1-mini:generate',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.GEMINI_API_KEY}`,
},
body: JSON.stringify({
prompt: prompt,
max_output_tokens: 800,
temperature: 0.2,
}),
},
);

const json = await res.json();

const text =
(json?.candidates && json.candidates[0]?.content) ||
json?.output?.[0]?.content ||
json?.output_text ||
json?.text ||
(typeof json === 'string' ? json : null);

if (text) return String(text).trim();
} catch (err) {
// fallback to local formatting
}

const fallbackItems = companies
.slice(0, 10)
.map((c, i) => `${i + 1}. ${c.company}: [${c.jobTitle}](<${c.link}>)`)
.join('\n');
return `${today} Internship Postings Summary
We found ${totalCount} new internships posted today!

Here are some of our favorites
${fallbackItems}`;
}

export default class Test extends Command {
constructor(client: DiscordClient) {
super(client, {
name: 'fetchinternships',
description: 'Fetch todays internships from the Simplify Repo.',
category: Category.Utilities,
options: [
{
name: 'category',
description: 'Type of Internship',
type: ApplicationCommandOptionType.String,
required: true,
choices: [
{ name: '💻 Software Engineering Internship Roles', value: 'SWE' },
{
name: '🤖 Data Science, AI & Machine Learning Internship Roles',
value: 'AI',
},
{ name: '📱 Product Management Internship Roles', value: 'PM' },
{
name: '📈 Quantitative Finance Internship Roles',
value: 'QUANT',
},
{ name: '🔧 Hardware Engineering Internship Roles', value: 'HWE' },
{ name: 'All (Will take longer)', value: 'ALL' },
],
},
],
default_member_permissions: PermissionFlagsBits.UseApplicationCommands,
dm_permission: true,
cooldown: 3,
});
}

async Execute(interaction: ChatInputCommandInteraction) {
const category = interaction.options.getString('category', true);
await interaction.deferReply({ ephemeral: true });
try {
const result = await getInternshipOppertunitiesJob(
this.client,
interaction.guild ?? null,
)(category);

const companiesLength = result.companies.length;
if (companiesLength === 0) {
await interaction.followUp({
content: `No new ${category} internships were posted on our DB today.`,
});
return;
}
const companiesText = await generateCompaniesText(result.companies);

// Split into embed if too long
const embed = new EmbedBuilder()
.setTitle('📋 Internship Postings Summary')
.setDescription(companiesText.substring(0, 4096))
.setColor('#0099ff');

await interaction.editReply({ embeds: [embed] });

} catch (err: Error | any) {
if (err instanceof Error) {
await interaction.followUp({
content: `Error fetching internships: ${err.message}`,
});
} else {
await interaction.followUp({
content: `Error fetching internships: ${err}`,
});
}
}
}
}
55 changes: 49 additions & 6 deletions src/discord/events/client/Ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Event from "../../classes/Event";
import Command from "../../classes/Command";
import { RecurrenceRule, scheduleJob } from "node-schedule";
import getDiscordEventsJob from "../../../jobs/syncDiscordEventsJob";
import getInternshipOppertunitiesJob from "../../../jobs/fetchInternships";
import Logger from "../../../utils/Logger";
import { CONFIG } from "../../..";

Expand Down Expand Up @@ -53,6 +54,45 @@ export default class Ready extends Event {

this.client.guilds.cache.forEach(guild => {
scheduleJob(rule, getDiscordEventsJob(this.client, guild));
scheduleJob(rule, async (fireDate: Date) => {
try {
const result = await getInternshipOppertunitiesJob(this.client, guild)("ALL");
console.log('Job executed successfully:', result);

const companiesText = result.companies
.map((c) => `${c.company}: [${c.jobTitle}](<${c.link}>)`)
.join('\n');

const targetChannel = guild.channels.cache.find(
(channel) => channel.name === "opportunities-test" && channel.isTextBased()
);

if (!targetChannel || !targetChannel.isTextBased()) {
console.error("Target channel not found or is not text-based.");
return;
}

if (companiesText.length === 0) {
return;
}

if (companiesText.length < 1900) {
await (targetChannel as TextChannel).send(
`**Here are today's internships:**\n${companiesText}`
);
} else {
const chunks = companiesText.match(/[\s\S]{1,1900}(?=\n|$)/g); // Split into chunks of max 1900 characters, breaking at newlines
if (chunks) {
await (targetChannel as TextChannel).send(`**Here are today's internships:**`);
for (const chunk of chunks) {
await (targetChannel as TextChannel).send(chunk);
}
}
}
} catch (error) {
console.error("Error executing job:", error);
}
});
})

Logger.once("setup", "Successfully set up interval sync.")
Expand All @@ -64,12 +104,15 @@ export default class Ready extends Event {

commands.forEach(command => {
data.push({
name: command.name,
description: command.description,
options: command.options,
default_member_permissions: command.default_member_permissions.toString(),
dm_permission: command.dm_permission,
})
name: command.name,
description: command.description,
options: command.options,
default_member_permissions:
command.default_member_permissions !== undefined
? command.default_member_permissions.toString()
: null,
dm_permission: command.dm_permission,
});
})

return data;
Expand Down
Loading