Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ca56d1f
added a 'filter type' selection for the user to choose for the job re…
Hairum-Qureshi Dec 4, 2024
eefa1d7
added 'null' to the filtyBy property as a valid type
Hairum-Qureshi Dec 4, 2024
3ca9496
added code for retrieving the filter type the user selected when runn…
Hairum-Qureshi Dec 9, 2024
b7fe1c1
modified code inside of formatCurrency function to handle case where …
Hairum-Qureshi Dec 9, 2024
85f15a9
utilized Math.round() method to round the DISTANCE_KM variable
Hairum-Qureshi Dec 9, 2024
b6ddc68
changed 'data-posted' to 'date'
Hairum-Qureshi Dec 9, 2024
e067ede
changed Date Posted value from 'date-posted' to 'date'
Hairum-Qureshi Dec 9, 2024
d411b52
removed '-posted' prefix from 'date' in type assertion statement
Hairum-Qureshi Dec 9, 2024
aca4002
rewrote listJobs function code
Hairum-Qureshi Dec 9, 2024
ff44c1e
created an interface file for Interest
Hairum-Qureshi Dec 9, 2024
d72c999
created an interface file for JobData
Hairum-Qureshi Dec 9, 2024
2dc6f29
removed Interest and JobData interface and now imports them into the …
Hairum-Qureshi Dec 9, 2024
55df6d1
changed import for JobData and Interest interfaces so they now get im…
Hairum-Qureshi Dec 9, 2024
7132ca5
updated import for JobResult interface
Hairum-Qureshi Dec 9, 2024
d2cfc32
removed JobResult and JobListing interfaces from file and imported th…
Hairum-Qureshi Dec 9, 2024
05a302c
created file for JobResult interface
Hairum-Qureshi Dec 9, 2024
e0c1c84
created file for JobListing interface
Hairum-Qureshi Dec 9, 2024
9bfb958
replaced 'int' with 'number' as the valid JavaScript type
Hairum-Qureshi Dec 9, 2024
3a5d627
added appropriate type for 'job' variable that's utilized in the map …
Hairum-Qureshi Dec 9, 2024
bc757be
created interface for the Adzuna Job API response
Hairum-Qureshi Dec 9, 2024
a20674c
added code to filter the average salary if the user wants the jobs so…
Hairum-Qureshi Dec 9, 2024
58d73aa
added conditional logic to handle case where user did not choose to h…
Hairum-Qureshi Dec 9, 2024
2d3edf4
removed unused Job interface import
Hairum-Qureshi Dec 9, 2024
a1a0949
modified stripMarkdown function regex and replace logic and rearrange…
Hairum-Qureshi Dec 9, 2024
3b84b72
modified conditional logic so that if filterBy is set to 'default' it…
Hairum-Qureshi Dec 9, 2024
f6c0f45
added logic to display 'date posted' instead of 'date' when listing w…
Hairum-Qureshi Dec 9, 2024
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
16 changes: 15 additions & 1 deletion src/commands/reminders/remind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ export default class extends Command {
],
type: ApplicationCommandOptionType.String,
required: true
},
{
name: 'filter-type',
description: 'Select what you would like your job/internships filtered by',
choices: [
{ name: 'Relevance', value: 'relevance' },
{ name: 'Salary', value: 'salary' },
{ name: 'Date Posted', value: 'date' },
{ name: 'Default', value: 'default' }
],
type: ApplicationCommandOptionType.String
}
]
}
Expand Down Expand Up @@ -87,12 +98,15 @@ export default class extends Command {
| 'daily'
| 'weekly' || null;

const filterBy = interaction.options.getString('filter-type') as 'relevance' | 'salary' | 'date' | 'default' | null;

const jobReminder: Reminder = {
owner: interaction.user.id,
content: 'Job Reminder',
mode: 'private',
expires: new Date(),
repeat: jobReminderRepeat
repeat: jobReminderRepeat,
filterBy
};
// handling duplicate job reminders
if (await this.checkJobReminder(interaction)) {
Expand Down
16 changes: 16 additions & 0 deletions src/lib/types/AdzunaJobResponse.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable camelcase */
export interface AdzunaJobResponse {
company: {
display_name: string
};
title: string,
description: string,
location: {
display_name: string,
area: string
}
created: string;
salary_max: number | string;
salary_min: number | string;
redirect_url: string;
}
7 changes: 7 additions & 0 deletions src/lib/types/Interest.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Interest {
interest1: string,
interest2: string,
interest3: string,
interest4: string,
interest5: string
}
4 changes: 2 additions & 2 deletions src/lib/types/Job.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export interface Job {
owner: string;
questionSet: int;
questionSet: number;
content: string;
location: string;
questionSet: int;
questionSet: number;
answers: string[];
mode: 'public' | 'private';
}
7 changes: 7 additions & 0 deletions src/lib/types/JobData.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface JobData {
city: string,
preference: string,
jobType: string,
distance: string,
filterBy: string
}
8 changes: 8 additions & 0 deletions src/lib/types/JobListing.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface JobListing {
title: string;
company: string;
location: string;
salary: string;
link: string;
description: string;
}
10 changes: 10 additions & 0 deletions src/lib/types/JobResult.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface JobResult {
company: string;
title: string;
description: string;
location: string;
created: string;
salaryMax: string;
salaryMin: string;
link: string;
}
1 change: 1 addition & 0 deletions src/lib/types/Reminder.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface Reminder {
content: string;
repeat: null | 'daily' | 'weekly';
mode: 'public' | 'private';
filterBy?: 'relevance' | 'salary' | 'date' | 'default' | null
}
40 changes: 13 additions & 27 deletions src/lib/utils/jobUtils/Adzuna_job_search.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
import axios from 'axios';
import { APP_ID, APP_KEY } from '@root/config';
import { JobData, Interest } from '@root/src/pieces/tasks';

interface JobListing {
title: string;
company: string;
location: string;
salary: string;
link: string;
description: string;
}

export interface JobResult {
company: string;
title: string;
description: string;
location: string;
created: string;
salaryMax: string;
salaryMin: string;
link: string;
}
import { JobData } from '../../types/JobData';
import { Interest } from '../../types/Interest';
import { JobListing } from '../../types/JobListing';
import { JobResult } from '../../types/JobResult';
import { AdzunaJobResponse } from '../../types/AdzunaJobResponse';

type JobCache = {
[key: string]: JobListing[] | JobResult[];
Expand Down Expand Up @@ -54,13 +38,15 @@ export default async function fetchJobListings(jobData: JobData, interests?: Int
return jobCache[cacheKey] as JobResult[];
}

const URL = `https://api.adzuna.com/v1/api/jobs/us/search/1?app_id=${APP_ID}&app_key=${APP_KEY}&results_per_page=15&what=${JOB_TYPE}&what_or=${whatInterests}&where=
${LOCATION}&distance=${DISTANCE_KM}`;
// const URL = `https://api.adzuna.com/v1/api/jobs/us/search/1?app_id=${APP_ID}&app_key=${APP_KEY}&results_per_page=15&what=${JOB_TYPE}&what_or=${whatInterests}&where=
// ${LOCATION}&distance=${Math.round(DISTANCE_KM)}&sort_by=${jobData.filterBy}`;

const URL_BASE = `https://api.adzuna.com/v1/api/jobs/us/search/1?app_id=${APP_ID}&app_key=${APP_KEY}&results_per_page=15&what=${JOB_TYPE}&what_or=${whatInterests}&where=\
${LOCATION}&distance=${Math.round(DISTANCE_KM)}`;

try {
console.log('Fetching data from API...');
const response = await axios.get(URL);
const jobResults: JobResult[] = response.data.results.map((job: any) => ({
const response = await axios.get(jobData.filterBy && jobData.filterBy !== 'default' ? `${URL_BASE}&sort_by=${jobData.filterBy}` : URL_BASE);
const jobResults: JobResult[] = response.data.results.map((job: AdzunaJobResponse) => ({
company: job.company?.display_name || 'Not Provided',
title: job.title,
description: job.description || 'No description available',
Expand All @@ -73,7 +59,7 @@ export default async function fetchJobListings(jobData: JobData, interests?: Int

jobCache[cacheKey] = jobResults;

return jobResults;
return jobResults.sort();
} catch (error) {
console.error('API error:', error);
throw error;
Expand Down
1 change: 0 additions & 1 deletion src/pieces/commandManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { SageUser } from '../lib/types/SageUser';
import { CommandError } from '../lib/types/errors';
import { verify } from '../pieces/verification';
import { JobPreferenceAPI } from '../lib/utils/jobUtils/jobDatabase';
import { Job } from '../lib/types/Job';
import { validatePreferences } from '../lib/utils/jobUtils/validatePreferences';

const DELETE_DELAY = 10000;
Expand Down
85 changes: 49 additions & 36 deletions src/pieces/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { Reminder } from '@lib/types/Reminder';
import { Poll, PollResult } from '@lib/types/Poll';
import { MongoClient } from 'mongodb';
import { Job } from '../lib/types/Job';
import fetchJobListings, { JobResult } from '../lib/utils/jobUtils/Adzuna_job_search';
import fetchJobListings from '../lib/utils/jobUtils/Adzuna_job_search';
import { sendToFile } from '../lib/utils/generalUtils';
import { JobData } from '../lib/types/JobData';
import { Interest } from '../lib/types/Interest';
import { JobResult } from '../lib/types/JobResult';

async function register(bot: Client): Promise<void> {
schedule('0/30 * * * * *', () => {
Expand Down Expand Up @@ -81,31 +84,17 @@ async function checkPolls(bot: Client): Promise<void> {
});
}

export interface JobData {
city: string,
preference: string,
jobType: string,
distance: string
}

export interface Interest {
interest1: string,
interest2: string,
interest3: string,
interest4: string,
interest5: string
}

// eslint-disable-next-line no-warning-comments
async function getJobFormData(userID:string):Promise<[JobData, Interest, JobResult[]]> {
async function getJobFormData(userID:string, filterBy: string):Promise<[JobData, Interest, JobResult[]]> {
const client = await MongoClient.connect(DB.CONNECTION, { useUnifiedTopology: true });
const db = client.db(BOT.NAME).collection(DB.JOB_FORMS);
const jobformAnswers:Job[] = await db.find({ owner: userID }).toArray();
const jobData:JobData = {
city: jobformAnswers[0].answers[0],
preference: jobformAnswers[0].answers[1],
jobType: jobformAnswers[0].answers[2],
distance: jobformAnswers[0].answers[3]
distance: jobformAnswers[0].answers[3],
filterBy: filterBy ?? 'default'
};

const interests:Interest = {
Expand All @@ -121,7 +110,7 @@ async function getJobFormData(userID:string):Promise<[JobData, Interest, JobResu
}

function formatCurrency(currency:number): string {
return `${new Intl.NumberFormat('en-US', {
return isNaN(currency) ? 'N/A' : `${new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(Number(currency))}`;
Expand All @@ -131,26 +120,44 @@ function titleCase(jobTitle:string): string {
return jobTitle.toLowerCase().replace(/[()]/g, '').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}

function listJobs(jobData: JobResult[]): string {
function listJobs(jobData: JobResult[], filterBy: string): string {
// Conditionally sort jobs by salary if sortBy is 'salary'
if (filterBy === 'salary') {
jobData.sort((a, b) => {
const avgA = (Number(a.salaryMax) + Number(a.salaryMin)) / 2;
const avgB = (Number(b.salaryMax) + Number(b.salaryMin)) / 2;

// Handle cases where salaryMax or salaryMin is "Not listed"
if (isNaN(avgA)) return 1; // Treat jobs with no salary info as lowest
if (isNaN(avgB)) return -1;

return avgB - avgA; // Descending order
});
}

let jobList = '';
for (let i = 0; i < jobData.length; i++) {
const avgSalary = (Number(jobData[i].salaryMax) + Number(jobData[i].salaryMin)) / 2;
const formattedAvgSalary = formatCurrency(avgSalary);
const formattedSalaryMax = formatCurrency(Number(jobData[i].salaryMax));
const formattedSalaryMin = formatCurrency(Number(jobData[i].salaryMin));
const formattedSalaryMax = formatCurrency(Number(jobData[i].salaryMax)) !== 'N/A' ? formatCurrency(Number(jobData[i].salaryMax)) : '';
const formattedSalaryMin = formatCurrency(Number(jobData[i].salaryMin)) !== 'N/A' ? formatCurrency(Number(jobData[i].salaryMin)) : '';

const salaryDetails = (formattedSalaryMin && formattedSalaryMax)
? `, Min: ${formattedSalaryMin}, Max: ${formattedSalaryMax}`
: formattedAvgSalary;

jobList += `${i + 1}. **${titleCase(jobData[i].title)}**
\t \t * **Salary Average:** ${formattedAvgSalary}\
${formattedAvgSalary !== formattedSalaryMax ? `, Min: ${formattedSalaryMin}, Max: ${formattedSalaryMax}` : ''}
\t \t * **Location:** ${jobData[i].location}
\t \t * **Apply here:** [read more about the job and apply here](${jobData[i].link})
${i !== jobData.length - 1 ? '\n' : ''}`;
\t\t* **Salary Average:** ${formattedAvgSalary}${salaryDetails}
\t\t* **Location:** ${jobData[i].location}
\t\t* **Apply here:** [read more about the job and apply here](${jobData[i].link})
${i !== jobData.length - 1 ? '\n' : ''}`;
}

return jobList || '### Unfortunately, there were no jobs found based on your interests :(. Consider updating your interests or waiting until something is found.';
}

async function jobMessage(reminder: Reminder, userID: string): Promise<string> {
const jobFormData: [JobData, Interest, JobResult[]] = await getJobFormData(userID);
const jobFormData: [JobData, Interest, JobResult[]] = await getJobFormData(userID, reminder.filterBy);
const message = `## Hey <@${reminder.owner}>!
## Here's your list of job/internship recommendations:
Based on your interests in **${jobFormData[1].interest1}**, **${jobFormData[1].interest2}**, \
Expand All @@ -159,7 +166,7 @@ async function jobMessage(reminder: Reminder, userID: string): Promise<string> {
their positions/details/applications/salary WILL be different and this is not a glitch/bug!
Here's your personalized list:

${listJobs(jobFormData[2])}
${listJobs(jobFormData[2], reminder.filterBy)}
---
### **Disclaimer:**
-# Please be aware that the job listings displayed are retrieved from a third-party API. \
Expand All @@ -171,22 +178,27 @@ async function jobMessage(reminder: Reminder, userID: string): Promise<string> {
return message;
}

function stripMarkdown(message:string, owner:string): string {
return message.replace(`## Hey <@${owner}>!
## Here's your list of job/internship recommendations:`, '').replace(/\[read more about the job and apply here\]/g, '').replace(/\((https?:\/\/[^\s)]+)\)/g, '$1')
function stripMarkdown(message: string, owner: string): string {
return message
.replace(new RegExp(`## Hey <@${owner}>!\\s*## Here's your list of job/internship recommendations:?`, 'g'), '') // Remove specific header
.replace(/\[read more about the job and apply here\]/g, '')
.replace(/\((https?:\/\/[^\s)]+)\)/g, '$1')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/##+\s*/g, '')
// eslint-disable-next-line no-useless-escape
.replace(/\*\*([^*]*(?:\*[^*]+)*)\*\*/g, '$1').replace(/(###|-\#)\s*/g, '');
.replace(/###|-\#\s*/g, '')
.trim();
}

function headerMessage(owner:string):string {
function headerMessage(owner:string, filterBy:string):string {
return `## Hey <@${owner}>!
### **__Please read this disclaimer before reading your list of jobs/internships__:**
-# Please be aware that the job listings displayed are retrieved from a third-party API. \
While we strive to provide accurate information, we cannot guarantee the legitimacy or security \
of all postings. Exercise caution when sharing personal information, submitting resumes, or registering \
on external sites. Always verify the authenticity of job applications before proceeding. Additionally, \
some job postings may contain inaccuracies due to API limitations, which are beyond our control. We apologize for any inconvenience this may cause and appreciate your understanding.
## Here's your list of job/internship recommendations:
## Here's your list of job/internship recommendations${filterBy && filterBy !== 'default' ? ` (filtered based on ${filterBy === 'date' ? 'date posted' : filterBy}):` : ':'}
`;
}

Expand All @@ -211,9 +223,10 @@ async function checkReminders(bot: Client): Promise<void> {
} else {
const attachments: AttachmentBuilder[] = [];
attachments.push(await sendToFile(stripMarkdown(message.split('---')[0], reminder.owner), 'txt', 'list-of-jobs-internships', false));
user.send({ content: headerMessage(reminder.owner), files: attachments as AttachmentBuilder[] });
user.send({ content: headerMessage(reminder.owner, reminder.filterBy), files: attachments as AttachmentBuilder[] });
}
}).catch((error) => {
console.log('ERROR CALLED ----------------------------------------------------');
console.error(`Failed to fetch user with ID: ${reminder.owner}`, error);
});
}
Expand Down