diff --git a/docs/university.md b/docs/university.md
index 59682036bf..3eca5f4024 100644
--- a/docs/university.md
+++ b/docs/university.md
@@ -104,6 +104,16 @@ pageClass: routes
+### 信息门户
+
+
+
+::: warning 注意
+信息门户的通知需要通过统一身份认证后才能获取,因此需要在校园网或校园 VPN 环境下自建。
+
+设置环境变量: `BUPT_USERNAME` 用户名为学号, `BUPT_PASSWORD` 统一身份认证的密码。
+:::
+
## 常州大学
### 教务处
diff --git a/lib/config.js b/lib/config.js
index f3e5b4cdd9..744017db76 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -98,6 +98,10 @@ const calculateValue = () => {
chuiniu: {
member: envs.CHUINIU_MEMBER,
},
+ bupt: {
+ username: envs.BUPT_USERNAME,
+ password: envs.BUPT_PASSWORD,
+ },
};
};
calculateValue();
diff --git a/lib/router.js b/lib/router.js
index 36d171b14b..d6965d7b47 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -1309,6 +1309,7 @@ router.get('/21caijing/channel/:name', require('./routes/21caijing/channel'));
// 北京邮电大学
router.get('/bupt/yz/:type', require('./routes/universities/bupt/yz'));
router.get('/bupt/grs', require('./routes/universities/bupt/grs'));
+router.get('/bupt/portal', require('./routes/universities/bupt/portal'));
// VOCUS 方格子
router.get('/vocus/publication/:id', require('./routes/vocus/publication'));
diff --git a/lib/routes/universities/bupt/portal.js b/lib/routes/universities/bupt/portal.js
new file mode 100644
index 0000000000..a65b2e32f8
--- /dev/null
+++ b/lib/routes/universities/bupt/portal.js
@@ -0,0 +1,172 @@
+const config = require('@/config').value;
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const url = require('url');
+
+function isToday(time) {
+ return new Date().getTime() - new Date(time).getTime() < 86400000;
+}
+
+function getPubDate(time) {
+ return isToday(time) ? new Date() : new Date(time + ' 08:00:00');
+}
+
+const base = 'http://my.bupt.edu.cn';
+const sourceTimezoneOffset = -8;
+
+let portalCookie = null;
+let authCookie = null;
+let castgc = null;
+
+module.exports = async (ctx) => {
+ if (!config.bupt || !config.bupt.username || !config.bupt.password) {
+ throw 'BUPT Portal RSS is disabled due to the lack of relevant config';
+ }
+
+ const reqUrl = url.resolve(base, '/index.portal?.pn=p1778');
+ let reqRes = await got({
+ method: 'get',
+ followRedirect: false,
+ url: reqUrl,
+ headers: {
+ cookie: portalCookie,
+ },
+ });
+
+ // Login
+ if (reqRes.statusCode === 302) {
+ if (reqRes.headers['set-cookie'] === undefined) {
+ portalCookie = null;
+ throw 'Can not obtain portalCookie';
+ }
+ portalCookie = reqRes.headers['set-cookie'].toString().match(/JSESSIONID=.{37}/)[0];
+
+ const authRes = await got({
+ method: 'get',
+ followRedirect: false,
+ url: reqRes.headers.location,
+ headers: {
+ cookie: `${authCookie}; ${castgc}`,
+ },
+ });
+ if (authRes.statusCode === 200) {
+ authCookie = authRes.headers['set-cookie'].toString().match(/JSESSIONID=.{37}/)[0];
+ const authPage = cheerio.load(authRes.data);
+
+ const loginRes = await got({
+ method: 'post',
+ followRedirect: false,
+ url: reqRes.headers.location,
+ headers: {
+ cookie: authCookie,
+ referer: reqRes.headers.location,
+ },
+ form: true,
+ data: {
+ username: config.bupt.username,
+ password: config.bupt.password,
+ lt: authPage('[name=lt]').attr('value'),
+ execution: authPage('[name=execution]').attr('value'),
+ _eventId: authPage('[name=_eventId]').attr('value'),
+ rmShown: 1,
+ },
+ });
+ if (loginRes.headers['set-cookie'] === undefined) {
+ authCookie = null;
+ castgc = null;
+ throw 'Can not obtain castgc';
+ }
+ castgc = loginRes.headers['set-cookie'].toString().match(/CASTGC=.{85}/)[0];
+
+ await got({
+ method: 'get',
+ followRedirect: false,
+ url: loginRes.headers.location,
+ headers: {
+ cookie: portalCookie,
+ },
+ });
+ } else if (authRes.statusCode === 302) {
+ await got({
+ method: 'get',
+ followRedirect: false,
+ url: authRes.headers.location,
+ headers: {
+ cookie: portalCookie,
+ },
+ });
+ } else {
+ throw 'BUPT auth login failed';
+ }
+
+ reqRes = await got({
+ method: 'get',
+ followRedirect: false,
+ url: reqUrl,
+ headers: {
+ cookie: portalCookie,
+ },
+ });
+ }
+
+ const $ = cheerio.load(reqRes.data);
+ const list = $('.newslist li').get();
+ const out = await Promise.all(
+ list.map(async (i) => {
+ const item = $(i);
+ const itemUrl = url.resolve(
+ base,
+ $(item)
+ .find('a')
+ .attr('href')
+ );
+ const cache = await ctx.cache.get(itemUrl);
+ if (cache) {
+ return Promise.resolve(JSON.parse(cache));
+ }
+
+ const title = $(item)
+ .find('a')
+ .text();
+ const author = $(item)
+ .find('.author')
+ .text();
+ const time = getPubDate(
+ $(item)
+ .find('.time')
+ .text()
+ );
+ time.setTime(time.getTime() + (sourceTimezoneOffset - time.getTimezoneOffset() / 60) * 60 * 60 * 1000);
+
+ const itemResponse = await got({
+ method: 'get',
+ url: itemUrl,
+ headers: {
+ cookie: portalCookie,
+ },
+ });
+ const itemElement = cheerio.load(itemResponse.data);
+ // Remove useless print button
+ itemElement('table.Noprint').remove();
+ const description = itemElement('.singleexpert').html();
+
+ const single = {
+ title: title,
+ author: author,
+ description: description,
+ pubDate: time.toUTCString(),
+ link: itemUrl,
+ guid: itemUrl,
+ };
+
+ ctx.cache.set(itemUrl, JSON.stringify(single));
+ return Promise.resolve(single);
+ })
+ );
+
+ ctx.state.data = {
+ title: '北京邮电大学信息门户',
+ link: reqUrl,
+ item: out,
+ };
+};