@@ -30,7 +30,7 @@ export class InternJobProvider {
30
30
if ( ! fs . existsSync ( cacheDir ) ) {
31
31
fs . mkdirSync ( cacheDir , { recursive : true } ) ;
32
32
}
33
- this . sentJobsPath = path . join ( cacheDir , 'sent_jobs .json' ) ;
33
+ this . sentJobsPath = path . join ( cacheDir , 'sent_Intern_jobs .json' ) ;
34
34
this . config = { ...jobWxBotConfig , jobsPerMessage : jobWxBotConfig . jobsPerMessage || 3 } ;
35
35
}
36
36
@@ -188,7 +188,7 @@ export class InternJobProvider {
188
188
const messages : string [ ] = [ ] ;
189
189
for ( let i = 0 ; i < jobs . length ; i += this . config . jobsPerMessage ) {
190
190
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' ;
192
192
jobGroup . forEach ( ( job ) => {
193
193
message += this . formatJobMessage ( job ) ;
194
194
} ) ;
@@ -204,6 +204,204 @@ export class InternJobProvider {
204
204
📍 Location: ${ job . location }
205
205
🔗 Apply: ${ job . applicationLink }
206
206
📅 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 = / \| C o m p a n y .* ?\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 ( / < b r \s * \/ ? > / gi, ', ' )
302
+ . replace ( / < [ ^ > ] * > / g, '' )
303
+ . replace ( / \s + / g, ' ' )
304
+ . trim ( ) ;
305
+ }
306
+
307
+ private extractApplicationLink ( htmlString : string ) : string {
308
+ const linkPattern = / h r e f = " ( [ ^ " ] * ) / ;
309
+ const match = htmlString . match ( linkPattern ) ;
310
+ if ( match ) {
311
+ let link = match [ 1 ] ;
312
+
313
+ link = link . replace ( / ( [ ? & ] u t m _ s o u r c e = S i m p l i f y ) ( & r e f = S i m p l i f y ) ? ( $ | & ) / , '' ) ;
314
+ link = link . replace ( / ( [ ? & ] ) u t m _ s o u r c e = S i m p l i f y ( & r e f = S i m p l i f y ) ? & / , '$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
207
405
` ;
208
406
if ( job . annotations . length > 0 ) {
209
407
message += `⚠️ Note: ${ job . annotations . join ( ', ' ) } \n` ;
0 commit comments