Skip to content

Commit 693b770

Browse files
authored
Merge pull request #1 from NarwhalChen/main
feat: adding feat of get ng job
2 parents 28f9ff3 + a9499da commit 693b770

File tree

2 files changed

+240
-14
lines changed

2 files changed

+240
-14
lines changed

src/index.ts

+40-12
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { WechatyBuilder, Contact, Room, Message } from 'wechaty';
22
import { ContactImpl } from 'wechaty/impls';
33
import qrcodeTerminal from 'qrcode-terminal';
4-
import { InternJobProvider, Job } from './intern-job-provider';
4+
import { InternJobProvider, NGJobProvider, Job } from './intern-job-provider';
55
import { jobWxBotConfig } from '../package.json';
66

77
const wechaty = WechatyBuilder.build();
88
let targetRooms: Room[] = [];
9-
const jobProvider = new InternJobProvider();
10-
9+
const InternJob = new InternJobProvider();
10+
const NGJob = new NGJobProvider();
1111
function displayStartupBanner() {
1212
const banner = `
1313
____ __ __ _ _ _ ____ ___ _____
@@ -32,14 +32,35 @@ function displayStartupBanner() {
3232
console.log('\x1b[35m%s\x1b[0m', '🔍 Scanning QR Code to log in...\n'); // Magenta color
3333
}
3434

35-
async function sendJobUpdates() {
35+
async function sendInternJobUpdates() {
36+
if (targetRooms.length === 0) {
37+
console.log('No target rooms set');
38+
return;
39+
}
40+
const newInternJobs = await InternJob.getNewJobs();
41+
if (newInternJobs.length > 0) {
42+
const messages = InternJob.formatJobMessages(newInternJobs);
43+
for (const room of targetRooms) {
44+
for (const message of messages) {
45+
await room.say(message);
46+
await new Promise((resolve) => setTimeout(resolve, 1000));
47+
}
48+
}
49+
return true;
50+
} else {
51+
console.log('No new jobs found');
52+
return false;
53+
}
54+
}
55+
56+
async function sendNGJobUpdates() {
3657
if (targetRooms.length === 0) {
3758
console.log('No target rooms set');
3859
return;
3960
}
40-
const newJobs = await jobProvider.getNewJobs();
41-
if (newJobs.length > 0) {
42-
const messages = jobProvider.formatJobMessages(newJobs);
61+
const newNGJobs = await NGJob.getNewJobs();
62+
if (newNGJobs.length > 0) {
63+
const messages = NGJob.formatJobMessages(newNGJobs);
4364
for (const room of targetRooms) {
4465
for (const message of messages) {
4566
await room.say(message);
@@ -76,8 +97,10 @@ wechaty
7697
'\x1b[36m%s\x1b[0m',
7798
`🚀 ${targetRooms.length} target room(s) found. Bot is ready!`,
7899
); // Cyan color
79-
setInterval(sendJobUpdates, jobWxBotConfig.minsCheckInterval * 60 * 1000);
80-
sendJobUpdates();
100+
setInterval(sendInternJobUpdates, jobWxBotConfig.minsCheckInterval * 60 * 1000);
101+
setInterval(sendNGJobUpdates, jobWxBotConfig.minsCheckInterval * 60 * 1000);
102+
sendInternJobUpdates();
103+
sendNGJobUpdates();
81104
} else {
82105
console.log('\x1b[31m%s\x1b[0m', '❌ No target rooms found. Bot cannot operate.'); // Red color
83106
}
@@ -86,9 +109,14 @@ wechaty
86109
//TODO: need commands module
87110
//TOOD: need debug mode
88111
console.log(`Message received: ${message.text()}`);
89-
if (message.text().toLowerCase() === 'jobs') {
90-
if (!(await sendJobUpdates())) {
91-
message.say('No new jobs found for Intern/NG roles');
112+
if (message.text().toLowerCase() === '/internjobs') {
113+
if (!(await sendInternJobUpdates())) {
114+
message.say('No new jobs found for Intern roles');
115+
}
116+
}
117+
if (message.text().toLowerCase() === '/ngjobs') {
118+
if (!(await sendNGJobUpdates())) {
119+
message.say('No new jobs found for ng roles');
92120
}
93121
}
94122
});

src/intern-job-provider.ts

+200-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class InternJobProvider {
3030
if (!fs.existsSync(cacheDir)) {
3131
fs.mkdirSync(cacheDir, { recursive: true });
3232
}
33-
this.sentJobsPath = path.join(cacheDir, 'sent_jobs.json');
33+
this.sentJobsPath = path.join(cacheDir, 'sent_Intern_jobs.json');
3434
this.config = { ...jobWxBotConfig, jobsPerMessage: jobWxBotConfig.jobsPerMessage || 3 };
3535
}
3636

@@ -188,7 +188,7 @@ export class InternJobProvider {
188188
const messages: string[] = [];
189189
for (let i = 0; i < jobs.length; i += this.config.jobsPerMessage) {
190190
const jobGroup = jobs.slice(i, i + this.config.jobsPerMessage);
191-
let message = '📢 New Job Opportunities 📢\n\n';
191+
let message = '📢 New Job Opportunities for INTERN📢\n\n';
192192
jobGroup.forEach((job) => {
193193
message += this.formatJobMessage(job);
194194
});
@@ -204,6 +204,204 @@ export class InternJobProvider {
204204
📍 Location: ${job.location}
205205
🔗 Apply: ${job.applicationLink}
206206
📅 Posted: ${job.datePosted}
207+
🧢 Type: INTERN
208+
`;
209+
if (job.annotations.length > 0) {
210+
message += `⚠️ Note: ${job.annotations.join(', ')}\n`;
211+
}
212+
message += '----------------------------\n';
213+
return message;
214+
}
215+
}
216+
217+
218+
export class NGJobProvider {
219+
private sentJobsPath: string;
220+
private config: Config;
221+
private githubUrl: string =
222+
'https://raw.githubusercontent.com/SimplifyJobs/New-Grad-Positions/dev/README.md';
223+
224+
constructor() {
225+
const homeDir = os.homedir();
226+
const cacheDir = path.join(homeDir, '.job-wx-bot');
227+
if (!fs.existsSync(cacheDir)) {
228+
fs.mkdirSync(cacheDir, { recursive: true });
229+
}
230+
this.sentJobsPath = path.join(cacheDir, 'sent_NG_jobs.json');
231+
this.config = { ...jobWxBotConfig, jobsPerMessage: jobWxBotConfig.jobsPerMessage || 3 };
232+
}
233+
234+
private async fetchJobsFromGithub(): Promise<string> {
235+
try {
236+
const response = await axios.get(this.githubUrl);
237+
return response.data;
238+
} catch (error) {
239+
console.error('Error fetching data from GitHub:', error);
240+
throw error;
241+
}
242+
}
243+
244+
private extractTableFromMarkdown(markdownContent: string): Job[] {
245+
const tablePattern = /\| Company.*?\n([\s\S]*?)\n\n/;
246+
const tableMatch = markdownContent.match(tablePattern);
247+
if (!tableMatch) return [];
248+
249+
const tableContent = tableMatch[1];
250+
const rows = tableContent.trim().split('\n');
251+
252+
let lastValidCompany = '';
253+
return rows
254+
.map((row) => {
255+
const columns = row.split('|');
256+
if (columns.length >= 6) {
257+
const company = this.cleanCompanyName(columns[1].trim());
258+
if (company !== '↳') {
259+
lastValidCompany = company;
260+
}
261+
const role = columns[2].trim();
262+
const annotations = this.getJobAnnotations(role);
263+
264+
// 如果职位已关闭,则跳过
265+
if (annotations.includes('Closed')) {
266+
return null;
267+
}
268+
269+
return {
270+
company: company === '↳' ? lastValidCompany : company,
271+
role: this.cleanRole(role),
272+
location: this.cleanLocation(columns[3].trim()),
273+
applicationLink: this.extractApplicationLink(columns[4].trim()),
274+
datePosted: columns[5].trim(),
275+
annotations: annotations,
276+
};
277+
}
278+
return null;
279+
})
280+
.filter((job): job is Job => job !== null);
281+
}
282+
283+
private getJobAnnotations(role: string): string[] {
284+
const annotations: string[] = [];
285+
if (role.includes('🛂')) annotations.push('No Sponsorship');
286+
if (role.includes('🇺🇸')) annotations.push('U.S. Citizenship Required');
287+
if (role.includes('🔒')) annotations.push('Closed');
288+
return annotations;
289+
}
290+
291+
private cleanRole(role: string): string {
292+
return role.replace(/[🛂🇺🇸🔒]/g, '').trim();
293+
}
294+
295+
private cleanCompanyName(company: string): string {
296+
return company.replace(/\*\*\[(.*?)\].*?\*\*/, '$1');
297+
}
298+
299+
private cleanLocation(location: string): string {
300+
return location
301+
.replace(/<br\s*\/?>/gi, ', ')
302+
.replace(/<[^>]*>/g, '')
303+
.replace(/\s+/g, ' ')
304+
.trim();
305+
}
306+
307+
private extractApplicationLink(htmlString: string): string {
308+
const linkPattern = /href="([^"]*)/;
309+
const match = htmlString.match(linkPattern);
310+
if (match) {
311+
let link = match[1];
312+
313+
link = link.replace(/([?&]utm_source=Simplify)(&ref=Simplify)?($|&)/, '');
314+
link = link.replace(/([?&])utm_source=Simplify(&ref=Simplify)?&/, '$1');
315+
link = link.replace(/[?&]$/, '');
316+
317+
return link;
318+
}
319+
return 'No link available';
320+
}
321+
private filterJobsByDate(jobs: Job[], days: number): Job[] {
322+
const today = new Date();
323+
today.setHours(0, 0, 0, 0);
324+
const cutoffDate = new Date(today);
325+
cutoffDate.setDate(today.getDate() - days);
326+
327+
const months = [
328+
'Jan',
329+
'Feb',
330+
'Mar',
331+
'Apr',
332+
'May',
333+
'Jun',
334+
'Jul',
335+
'Aug',
336+
'Sep',
337+
'Oct',
338+
'Nov',
339+
'Dec',
340+
];
341+
342+
return jobs.filter((job) => {
343+
const [month, day] = job.datePosted.split(' ');
344+
const jobDate = new Date(today.getFullYear(), months.indexOf(month), parseInt(day));
345+
346+
if (jobDate > today) {
347+
jobDate.setFullYear(jobDate.getFullYear() - 1);
348+
}
349+
350+
return jobDate >= cutoffDate;
351+
});
352+
}
353+
354+
public async getNewJobs(): Promise<Job[]> {
355+
const markdownContent = await this.fetchJobsFromGithub();
356+
const allJobs = this.extractTableFromMarkdown(markdownContent);
357+
const filteredJobs = this.filterJobsByDate(allJobs, this.config.maxDays);
358+
359+
let sentJobs: Job[] = [];
360+
try {
361+
sentJobs = JSON.parse(fs.readFileSync(this.sentJobsPath, 'utf8'));
362+
} catch (error) {
363+
console.log('No previous sent jobs found');
364+
}
365+
366+
const newJobs = filteredJobs.filter(
367+
(job) =>
368+
!sentJobs.some(
369+
(sentJob) =>
370+
sentJob.company === job.company &&
371+
sentJob.role === job.role &&
372+
sentJob.datePosted === job.datePosted,
373+
),
374+
);
375+
376+
if (newJobs.length > 0) {
377+
sentJobs = [...sentJobs, ...newJobs];
378+
fs.writeFileSync(this.sentJobsPath, JSON.stringify(sentJobs));
379+
}
380+
381+
return newJobs;
382+
}
383+
384+
public formatJobMessages(jobs: Job[]): string[] {
385+
const messages: string[] = [];
386+
for (let i = 0; i < jobs.length; i += this.config.jobsPerMessage) {
387+
const jobGroup = jobs.slice(i, i + this.config.jobsPerMessage);
388+
let message = '📢 New Job Opportunities for NG 📢\n\n';
389+
jobGroup.forEach((job) => {
390+
message += this.formatJobMessage(job);
391+
});
392+
messages.push(message);
393+
}
394+
return messages;
395+
}
396+
397+
public formatJobMessage(job: Job): string {
398+
let message = `
399+
🏢 Company: ${job.company}
400+
💼 Role: ${job.role}
401+
📍 Location: ${job.location}
402+
🔗 Apply: ${job.applicationLink}
403+
📅 Posted: ${job.datePosted}
404+
🧢 Type: NG
207405
`;
208406
if (job.annotations.length > 0) {
209407
message += `⚠️ Note: ${job.annotations.join(', ')}\n`;

0 commit comments

Comments
 (0)