Skip to content

Commit 2532c07

Browse files
committed
feat(muti-rooms): diff rooms now has diff memo
1 parent 3340efd commit 2532c07

7 files changed

+162
-81
lines changed

src/command-handler.ts

+67-34
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import { Message, Room, Contact } from 'wechaty';
2-
import { InternJobProvider } from './providers/internship-job-provider';
3-
import { NGJobProvider } from './providers/new-graduate-job-provider';
2+
import { InternshipJobProvider } from './providers/internship-job-provider';
3+
import { NewGraduateJobProvider } from './providers/new-graduate-job-provider';
44
import { FileSystemService } from './file-system-service';
55

66
interface Command {
77
name: string;
88
aliases: string[];
99
description: string;
10-
execute: (message: Message, args: string[]) => Promise<void>;
10+
execute: (message: Message, args: string[], silent?: boolean) => Promise<void>;
1111
}
1212

1313
export class CommandHandler {
1414
private commands: Command[];
15-
private internJob: InternJobProvider;
16-
private newGradJob: NGJobProvider;
15+
private internJob: InternshipJobProvider;
16+
private newGradJob: NewGraduateJobProvider;
1717
private targetRooms: Room[];
1818

19-
constructor(internJob: InternJobProvider, newGradJob: NGJobProvider, targetRooms: Room[]) {
19+
constructor(
20+
internJob: InternshipJobProvider,
21+
newGradJob: NewGraduateJobProvider,
22+
targetRooms: Room[],
23+
) {
2024
this.internJob = internJob;
2125
this.newGradJob = newGradJob;
2226
this.targetRooms = targetRooms;
@@ -33,9 +37,13 @@ export class CommandHandler {
3337
if (room && sender && (await this.isRoomOwnerOrAdmin(room, sender))) {
3438
const roomTopic = await room.topic();
3539
await this.addRoomToRegistry(roomTopic);
36-
await message.say(`Room "${roomTopic}" has been added to the bot's target list.`);
40+
await this.sendMessage(
41+
message,
42+
`Room "${roomTopic}" has been added to the bot's target list.`,
43+
);
3744
} else {
38-
await message.say(
45+
await this.sendMessage(
46+
message,
3947
"Sorry, only room owners or admins can add rooms to the bot's target list.",
4048
);
4149
}
@@ -45,19 +53,33 @@ export class CommandHandler {
4553
name: 'intern',
4654
aliases: ['internjobs'],
4755
description: 'Get new intern job postings',
48-
execute: async (message: Message) => {
49-
if (!(await this.sendJobUpdates(this.internJob))) {
50-
await message.say('No new jobs found for Intern roles');
56+
execute: async (message: Message, args: string[], silent = false) => {
57+
const room = message.room();
58+
if (room) {
59+
if (!(await this.sendJobUpdates(this.internJob, room))) {
60+
if (!silent) {
61+
await this.sendMessage(message, 'No new jobs found for Intern roles');
62+
}
63+
}
64+
} else {
65+
await this.sendMessage(message, 'This command can only be used in a room.');
5166
}
5267
},
5368
},
5469
{
5570
name: 'ng',
5671
aliases: ['ngjobs'],
5772
description: 'Get new graduate job postings',
58-
execute: async (message: Message) => {
59-
if (!(await this.sendJobUpdates(this.newGradJob))) {
60-
await message.say('No new jobs found for New Graduate roles');
73+
execute: async (message: Message, args: string[], silent = false) => {
74+
const room = message.room();
75+
if (room) {
76+
if (!(await this.sendJobUpdates(this.newGradJob, room))) {
77+
if (!silent) {
78+
await this.sendMessage(message, 'No new jobs found for New Graduate roles');
79+
}
80+
}
81+
} else {
82+
await this.sendMessage(message, 'This command can only be used in a room.');
6183
}
6284
},
6385
},
@@ -73,13 +95,13 @@ export class CommandHandler {
7395
helpMessage += ` Aliases: ${command.aliases.join(', ')}\n`;
7496
}
7597
});
76-
await message.say(helpMessage);
98+
await this.sendMessage(message, helpMessage);
7799
},
78100
},
79101
];
80102
}
81103

82-
async handleCommand(message: Message) {
104+
async handleCommand(message: Message, silent = false) {
83105
const text = message.text().toLowerCase();
84106
const commandMatch = text.match(/@\S+\s+(.+)/);
85107
const commandName = commandMatch ? commandMatch[1].split(' ')[0] : '';
@@ -90,9 +112,21 @@ export class CommandHandler {
90112

91113
if (command) {
92114
const args = commandMatch ? commandMatch[1].split(' ').slice(1) : [];
93-
await command.execute(message, args);
115+
await command.execute(message, args, silent);
116+
} else if (!silent) {
117+
await this.sendMessage(
118+
message,
119+
'Unrecognized command. Use "@BOT help" for available commands.',
120+
);
121+
}
122+
}
123+
124+
private async sendMessage(message: Message, content: string) {
125+
const room = message.room();
126+
if (room) {
127+
await room.say(content);
94128
} else {
95-
await message.say('Unrecognized command. Use "@BOT help" for available commands.');
129+
await message.say(content);
96130
}
97131
}
98132

@@ -105,37 +139,36 @@ export class CommandHandler {
105139
const registeredTopicsPath = 'registered-topics.json';
106140
let topicsLocal: { topics: string[] } = { topics: [] };
107141

108-
if (FileSystemService.fileExists(registeredTopicsPath)) {
109-
topicsLocal = FileSystemService.readJSON<{ topics: string[] }>(registeredTopicsPath);
142+
if (FileSystemService.globalFileExists(registeredTopicsPath)) {
143+
topicsLocal = FileSystemService.readGlobalJSON<{ topics: string[] }>(registeredTopicsPath);
110144
}
111145

112146
if (!topicsLocal.topics.includes(roomTopic)) {
113147
topicsLocal.topics.push(roomTopic);
114-
FileSystemService.writeJSON(registeredTopicsPath, topicsLocal);
148+
FileSystemService.writeGlobalJSON(registeredTopicsPath, topicsLocal);
115149
console.log(`Added room "${roomTopic}" to the registry.`);
116150
} else {
117151
console.log(`Room "${roomTopic}" is already in the registry.`);
118152
}
119153
}
120154

121-
private async sendJobUpdates(provider: InternJobProvider | NGJobProvider) {
122-
console.log(`Checking for new ${provider.jobType} jobs... now !`);
123-
if (this.targetRooms.length === 0) {
124-
console.log('No target rooms set');
125-
return false;
126-
}
127-
const newJobs = await provider.getNewJobs();
155+
private async sendJobUpdates(
156+
provider: InternshipJobProvider | NewGraduateJobProvider,
157+
room: Room,
158+
) {
159+
const roomTopic = await room.topic();
160+
console.log(`Checking for new ${provider.jobType} jobs for room ${roomTopic}... now !`);
161+
162+
const newJobs = await provider.getNewJobs(roomTopic);
128163
if (newJobs.length > 0) {
129164
const messages = provider.formatJobMessages(newJobs);
130-
for (const room of this.targetRooms) {
131-
for (const message of messages) {
132-
await room.say(message);
133-
await new Promise((resolve) => setTimeout(resolve, 1000));
134-
}
165+
for (const message of messages) {
166+
await room.say(message);
167+
await new Promise((resolve) => setTimeout(resolve, 1000));
135168
}
136169
return true;
137170
} else {
138-
console.log('No new jobs found');
171+
console.log(`No new ${provider.jobType} jobs found for room ${roomTopic}`);
139172
return false;
140173
}
141174
}

src/file-system-service.ts

+56-13
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,81 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33
import * as os from 'os';
4+
import * as crypto from 'crypto';
45

56
export class FileSystemService {
6-
private static configDir: string = path.join(os.homedir(), '.job-wx-bot');
7+
private static baseDir: string = path.join(os.homedir(), '.job-wx-bot');
8+
private static roomsDir: string = path.join(FileSystemService.baseDir, 'rooms');
79

810
static initialize(): void {
9-
if (!fs.existsSync(this.configDir)) {
10-
fs.mkdirSync(this.configDir, { recursive: true });
11+
if (!fs.existsSync(this.baseDir)) {
12+
fs.mkdirSync(this.baseDir, { recursive: true });
1113
}
14+
if (!fs.existsSync(this.roomsDir)) {
15+
fs.mkdirSync(this.roomsDir, { recursive: true });
16+
}
17+
}
18+
19+
private static getRoomHash(roomTopic: string): string {
20+
return crypto.createHash('md5').update(roomTopic).digest('hex');
21+
}
22+
23+
private static getRoomDir(roomTopic: string): string {
24+
const roomHash = this.getRoomHash(roomTopic);
25+
const roomDir = path.join(this.roomsDir, roomHash);
26+
if (!fs.existsSync(roomDir)) {
27+
fs.mkdirSync(roomDir, { recursive: true });
28+
}
29+
return roomDir;
1230
}
1331

14-
static readFile(fileName: string): string {
15-
const filePath = path.join(this.configDir, fileName);
32+
static readFile(roomTopic: string, fileName: string): string {
33+
const filePath = path.join(this.getRoomDir(roomTopic), fileName);
1634
return fs.readFileSync(filePath, 'utf8');
1735
}
1836

19-
static writeFile(fileName: string, data: string): void {
20-
const filePath = path.join(this.configDir, fileName);
37+
static writeFile(roomTopic: string, fileName: string, data: string): void {
38+
const filePath = path.join(this.getRoomDir(roomTopic), fileName);
2139
fs.writeFileSync(filePath, data);
2240
}
2341

24-
static fileExists(fileName: string): boolean {
25-
const filePath = path.join(this.configDir, fileName);
42+
static fileExists(roomTopic: string, fileName: string): boolean {
43+
const filePath = path.join(this.getRoomDir(roomTopic), fileName);
2644
return fs.existsSync(filePath);
2745
}
2846

29-
static readJSON<T>(fileName: string): T {
30-
const data = this.readFile(fileName);
47+
static readJSON<T>(roomTopic: string, fileName: string): T {
48+
const data = this.readFile(roomTopic, fileName);
3149
return JSON.parse(data) as T;
3250
}
3351

34-
static writeJSON(fileName: string, data: any): void {
52+
static writeJSON(roomTopic: string, fileName: string, data: any): void {
3553
const jsonData = JSON.stringify(data, null, 2);
36-
this.writeFile(fileName, jsonData);
54+
this.writeFile(roomTopic, fileName, jsonData);
55+
}
56+
57+
static writeGlobalFile(fileName: string, data: string): void {
58+
const filePath = path.join(this.baseDir, fileName);
59+
fs.writeFileSync(filePath, data);
60+
}
61+
62+
static readGlobalFile(fileName: string): string {
63+
const filePath = path.join(this.baseDir, fileName);
64+
return fs.readFileSync(filePath, 'utf8');
65+
}
66+
67+
static globalFileExists(fileName: string): boolean {
68+
const filePath = path.join(this.baseDir, fileName);
69+
return fs.existsSync(filePath);
70+
}
71+
72+
static writeGlobalJSON(fileName: string, data: any): void {
73+
const jsonData = JSON.stringify(data, null, 2);
74+
this.writeGlobalFile(fileName, jsonData);
75+
}
76+
77+
static readGlobalJSON<T>(fileName: string): T {
78+
const data = this.readGlobalFile(fileName);
79+
return JSON.parse(data) as T;
3780
}
3881
}

src/index.ts

+27-22
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,19 @@ import { WechatyBuilder, Contact, Room, Message } from 'wechaty';
22
import { ContactImpl } from 'wechaty/impls';
33
import qrcodeTerminal from 'qrcode-terminal';
44
import { jobWxBotConfig } from '../package.json';
5-
import { InternJobProvider } from './providers/internship-job-provider';
6-
import { NGJobProvider } from './providers/new-graduate-job-provider';
7-
import path from 'path';
8-
import os from 'os';
9-
import * as fs from 'fs';
5+
import { InternshipJobProvider } from './providers/internship-job-provider';
6+
import { NewGraduateJobProvider } from './providers/new-graduate-job-provider';
7+
import { FileSystemService } from './file-system-service';
108
import { TopicsLocal } from './types';
119
import { CommandHandler } from './command-handler';
1210

1311
const wechaty = WechatyBuilder.build();
1412
let targetRooms: Room[] = [];
15-
const internJob = new InternJobProvider(jobWxBotConfig);
16-
const newGradJob = new NGJobProvider(jobWxBotConfig);
13+
const internJob = new InternshipJobProvider(jobWxBotConfig);
14+
const newGradJob = new NewGraduateJobProvider(jobWxBotConfig);
1715

18-
const homeDir = os.homedir();
19-
const configDir = path.join(homeDir, '.job-wx-bot');
20-
const registeredTopicsPath = path.join(configDir, 'registered-topics.json');
21-
22-
if (!fs.existsSync(configDir)) {
23-
fs.mkdirSync(configDir, { recursive: true });
24-
}
16+
// Initialize FileSystemService
17+
FileSystemService.initialize();
2518

2619
function displayStartupBanner() {
2720
const banner = `
@@ -49,15 +42,14 @@ async function updateRegisteredTopics(validRooms: Room[]) {
4942
const validTopics: TopicsLocal = {
5043
topics: await Promise.all(validRooms.map(async (room) => await room.topic())),
5144
};
52-
fs.writeFileSync(registeredTopicsPath, JSON.stringify(validTopics, null, 2));
45+
FileSystemService.writeGlobalJSON('registered-topics.json', validTopics);
5346
}
5447

5548
async function getTargetRooms(): Promise<Room[]> {
5649
let roomTopics = new Set(jobWxBotConfig.rooms);
5750

58-
if (fs.existsSync(registeredTopicsPath)) {
59-
const data = fs.readFileSync(registeredTopicsPath, 'utf8');
60-
const topicsLocal: TopicsLocal = JSON.parse(data);
51+
if (FileSystemService.globalFileExists('registered-topics.json')) {
52+
const topicsLocal = FileSystemService.readGlobalJSON<TopicsLocal>('registered-topics.json');
6153
if (topicsLocal.topics) topicsLocal.topics.forEach((topic: string) => roomTopics.add(topic));
6254
}
6355

@@ -97,16 +89,29 @@ wechaty
9789
'\x1b[36m%s\x1b[0m',
9890
`🚀 ${targetRooms.length} target room(s) found. Bot is ready!`,
9991
);
92+
93+
const executeCommandInAllRooms = async (command: string, silent: boolean) => {
94+
for (const room of targetRooms) {
95+
await commandHandler.handleCommand(
96+
{
97+
text: () => `@BOT ${command}`,
98+
room: () => room,
99+
} as any,
100+
silent,
101+
);
102+
}
103+
};
104+
100105
setInterval(
101-
() => commandHandler.handleCommand({ command: 'intern' } as any),
106+
() => executeCommandInAllRooms('intern', true),
102107
jobWxBotConfig.minsCheckInterval * 60 * 1000,
103108
);
104109
setInterval(
105-
() => commandHandler.handleCommand({ command: 'ng' } as any),
110+
() => executeCommandInAllRooms('ng', true),
106111
jobWxBotConfig.minsCheckInterval * 60 * 1000,
107112
);
108-
await commandHandler.handleCommand({ command: 'intern' } as any);
109-
await commandHandler.handleCommand({ command: 'ng' } as any);
113+
await executeCommandInAllRooms('intern', true);
114+
await executeCommandInAllRooms('ng', true);
110115
} else {
111116
console.log('\x1b[31m%s\x1b[0m', '❌ No target rooms found. Bot cannot operate.');
112117
}

src/providers/internship-job-provider.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { Job } from '../types';
33
import { BaseJobProvider } from './job-provider-base';
44

55
/**
6-
* @class InternJobProvider
6+
* @class InternshipJobProvider
77
* @extends {BaseJobProvider}
88
* @description Provides internship job listings for students
99
* @source https://github.com/SimplifyJobs/Summer2025-Internships
1010
*/
11-
export class InternJobProvider extends BaseJobProvider {
11+
export class InternshipJobProvider extends BaseJobProvider {
1212
readonly jobType = 'INTERN';
1313
protected githubUrl =
1414
'https://raw.githubusercontent.com/SimplifyJobs/Summer2025-Internships/dev/README.md';
@@ -84,7 +84,7 @@ export class InternJobProvider extends BaseJobProvider {
8484
return role.replace(/[🛂🇺🇸🔒]/g, '').trim();
8585
}
8686

87-
private cleanCompanyName(company: string): string {
87+
protected cleanCompanyName(company: string): string {
8888
return company.replace(/\*\*\[(.*?)\].*?\*\*/, '$1');
8989
}
9090

0 commit comments

Comments
 (0)