diff --git a/src/modules/cau-notice-watcher/index.ts b/src/modules/cau-notice-watcher/index.ts new file mode 100644 index 00000000..38ba771d --- /dev/null +++ b/src/modules/cau-notice-watcher/index.ts @@ -0,0 +1,146 @@ +import Browser from 'zombie' +import EodiroMailer from '../eodiro-mailer' +import { JSDOM as JsDom } from 'jsdom' +import appRoot from 'app-root-path' +import fs from 'fs' +import wait from '../wait' + +export type TitleBuilder = ( + /** A single notice item */ noticeItemElement: HTMLElement | Element +) => string + +export type FeedOptions = { + /** + * Minutes + * @default 10 + */ + interval?: number +} + +export interface Subscriber { + id: string + link: string + noticeItemSelector: string + titleBuilder: TitleBuilder +} + +export type LastNotice = Record + +const lastNoticeFilePath = appRoot.resolve('/.eodiro/last_notice.json') + +export class CauNoticeWatcher { + private feedOptions: FeedOptions + private subscribers: Subscriber[] = [] + private shouldStop = false + private browser: any + private lastNotice: LastNotice + + constructor(feedOptions?: FeedOptions) { + if (!feedOptions) { + feedOptions = { + interval: 10, + } + } else if (!feedOptions?.interval) { + feedOptions.interval = 10 + } + + this.feedOptions = feedOptions + this.browser = new Browser() + this.lastNotice = this.loadLastNoticeFile() + } + + private loadLastNoticeFile() { + let lastNotice: LastNotice + + if (!fs.existsSync(lastNoticeFilePath)) { + lastNotice = {} + fs.writeFileSync(lastNoticeFilePath, JSON.stringify(lastNotice, null, 2)) + } else { + lastNotice = JSON.parse(fs.readFileSync(lastNoticeFilePath, 'utf8')) + } + + return lastNotice + } + + private writeLastNoticeFile() { + fs.writeFileSync( + lastNoticeFilePath, + JSON.stringify(this.lastNotice, null, 2) + ) + } + + private getLastNotice(subscriber: Subscriber) { + return this.lastNotice[subscriber.id] + } + + private updateLastNotice(subscriber: Subscriber, title: string) { + this.lastNotice[subscriber.id] = title + } + + public subscribe(subscriber: Subscriber) { + this.subscribers.push(subscriber) + } + + public async watch() { + if (this.shouldStop) { + return + } + + for (const subscriber of this.subscribers) { + await this.processSubscriber(subscriber) + } + + // Recursive function call after the interval + await wait(this.feedOptions.interval * 60 * 1000) + this.watch() + } + + private async processSubscriber(subscriber: Subscriber) { + const notices = Array.from(await this.visit(1, subscriber)) + + if (notices.length === 0) { + return + } + + const lastNoticeIndex = notices.indexOf(this.getLastNotice(subscriber)) + if (lastNoticeIndex !== -1) { + for (let i = lastNoticeIndex - 1; i >= 0; i--) { + EodiroMailer.sendMail({ + from: '"어디로 알림" ', + to: 'io@jhaemin.com', + subject: notices[i], + }) + } + } + + this.updateLastNotice(subscriber, notices[0]) + this.writeLastNoticeFile() + } + + private async visit( + page: number, + subscriber: Subscriber + ): Promise> { + return new Promise((resolve) => { + const notices: Set = new Set() + + this.browser.visit(subscriber.link, null, () => { + const body = new JsDom(this.browser.html(subscriber.noticeItemSelector)) + .window.document.body + const noticeElms = body.querySelectorAll(subscriber.noticeItemSelector) + + for (const noticeElm of Array.from(noticeElms)) { + const title = subscriber.titleBuilder(noticeElm) || '' + + notices.add(title) + } + + resolve(notices) + }) + }) + } + + public stop() { + this.shouldStop = true + } +} diff --git a/src/modules/cau-notice-watcher/subscribers/cau.ts b/src/modules/cau-notice-watcher/subscribers/cau.ts new file mode 100644 index 00000000..9501a771 --- /dev/null +++ b/src/modules/cau-notice-watcher/subscribers/cau.ts @@ -0,0 +1,13 @@ +import { Subscriber } from '..' + +export const cau: Subscriber = { + id: 'cau', + link: 'https://www.cau.ac.kr/cms/FR_CON/index.do?MENU_ID=100#page1', + noticeItemSelector: '.typeNoti', + titleBuilder: (noticeElm) => { + const mark = noticeElm.querySelector('.mark_noti').textContent.trim() + const title = noticeElm.querySelector('a').textContent.trim() + + return `${mark} ${title}` + }, +} diff --git a/src/modules/cau-notice-watcher/subscribers/index.ts b/src/modules/cau-notice-watcher/subscribers/index.ts new file mode 100644 index 00000000..c027f712 --- /dev/null +++ b/src/modules/cau-notice-watcher/subscribers/index.ts @@ -0,0 +1 @@ +export * from './cau' diff --git a/src/modules/eodiro-bot/cau-notice-rss-feed.ts b/src/modules/eodiro-bot/cau-notice-rss-feed.ts deleted file mode 100644 index 181c888b..00000000 --- a/src/modules/eodiro-bot/cau-notice-rss-feed.ts +++ /dev/null @@ -1,21 +0,0 @@ -import EodiroMailer from '../eodiro-mailer' -import FeedSub from 'feedsub' - -export const cauNoticeRssFeed = () => { - const reader = new FeedSub( - 'https://www.cau.ac.kr/cms/FR_PRO_CON/BoardRss.do', - { - interval: 10, - } - ) - - reader.on('item', (item) => { - EodiroMailer.sendMail({ - to: 'io@jhaemin.com', - subject: 'CAU Notice', - html: JSON.stringify(item, null, 2), - }) - }) - - reader.start(true) -} diff --git a/src/modules/eodiro-bot/index.ts b/src/modules/eodiro-bot/index.ts index 7e671859..b3f7fa18 100644 --- a/src/modules/eodiro-bot/index.ts +++ b/src/modules/eodiro-bot/index.ts @@ -1,12 +1,14 @@ +import * as Subscribers from '@/modules/cau-notice-watcher/subscribers' + import { UserAttrs, getUser } from '@/database/models/user' import { CTTS } from '@payw/cau-timetable-scraper' import CafeteriaMenusSeeder from '@/db/seeders/cafeteria-menus-seeder' +import { CauNoticeWatcher } from '../cau-notice-watcher' import Config from '@/config' import { CronJob } from 'cron' import Db from '@/db' import EodiroMailer from '../eodiro-mailer' -import { cauNoticeRssFeed } from './cau-notice-rss-feed' import chalk from 'chalk' import dayjs from 'dayjs' import { garbageCollectFiles } from './garbage-collect-files' @@ -32,11 +34,13 @@ export default class EodiroBot { // this.scrapeLectures() this.scrapeCafeteriaMenus() this.garbageCollect() - this.cauNoticeRssFeed() + this.cauNotice() } - private cauNoticeRssFeed() { - cauNoticeRssFeed() + private cauNotice() { + const feed = new CauNoticeWatcher() + feed.subscribe(Subscribers.cau) + feed.watch() } /** diff --git a/src/modules/eodiro-mailer/index.ts b/src/modules/eodiro-mailer/index.ts index 39dfd8fe..984c58dc 100644 --- a/src/modules/eodiro-mailer/index.ts +++ b/src/modules/eodiro-mailer/index.ts @@ -5,6 +5,10 @@ import chalk from 'chalk' const log = console.log interface MailOption { + /** + * "name" \ + */ + from?: string subject: string to: string html?: string @@ -40,10 +44,8 @@ export default class EodiroMailer { // TODO: Asynchronous static sendMail(options: MailOption): void { this.transporter.sendMail({ - from: '"어디로" ', - subject: options.subject, - to: options.to, - html: options.html, + from: '"어디로" ', + ...options, }) } }