diff --git a/docs/bbs.md b/docs/bbs.md
index 86d225a45..c400a63b0 100644
--- a/docs/bbs.md
+++ b/docs/bbs.md
@@ -250,6 +250,16 @@ pageClass: routes
+## Meteor
+
+### 看板
+
+
+
+### 看板列表
+
+
+
## Mobilism
### 论坛
diff --git a/lib/v2/meteor/boards.js b/lib/v2/meteor/boards.js
new file mode 100644
index 000000000..bcd2a74c7
--- /dev/null
+++ b/lib/v2/meteor/boards.js
@@ -0,0 +1,11 @@
+const { baseUrl, getBoards } = require('./utils');
+
+module.exports = async (ctx) => {
+ const items = await getBoards(ctx.cache.tryGet);
+
+ ctx.state.data = {
+ title: '看板列表',
+ link: `${baseUrl}/board/all`,
+ item: items,
+ };
+};
diff --git a/lib/v2/meteor/index.js b/lib/v2/meteor/index.js
new file mode 100644
index 000000000..af11f68c3
--- /dev/null
+++ b/lib/v2/meteor/index.js
@@ -0,0 +1,45 @@
+const got = require('@/utils/got');
+const { parseDate } = require('@/utils/parse-date');
+const { baseUrl, getBoards, renderDesc } = require('./utils');
+
+module.exports = async (ctx) => {
+ let { board = 'all' } = ctx.params;
+
+ const boards = await getBoards(ctx.cache.tryGet);
+ let boardInfo;
+ if (board !== 'all') {
+ boardInfo = boards.find((b) => b.id === board || b.alias === board);
+ board = boardInfo.id;
+ }
+
+ const { data: response } = await got.post(`${baseUrl}/article/get_new_articles`, {
+ json: {
+ boardId: board,
+ isCollege: false,
+ page: 0,
+ pageSize: ctx.query.limit ? Number(ctx.query.limit) : 30,
+ },
+ });
+
+ const result = JSON.parse(decodeURIComponent(response.result));
+
+ const items = await Promise.all(
+ result.map((item) =>
+ ctx.cache.tryGet(`meteor:${item.id}`, () => ({
+ title: item.title,
+ description: renderDesc(item.content),
+ link: `${baseUrl}/article/${item.shortId}`,
+ author: item.authorAlias,
+ pubDate: parseDate(item.createdAt),
+ }))
+ )
+ );
+
+ ctx.state.data = {
+ title: `${board !== 'all' ? boardInfo.title : '全部看板'} | Meteor 學生社群`,
+ description: board !== 'all' ? boardInfo.feedDescription : null,
+ image: board !== 'all' ? (boardInfo.imgUrl !== 'not_set' ? boardInfo.imgUrl : null) : null,
+ link: `${board !== 'all' ? boardInfo.link : `${baseUrl}/board/all`}/new`,
+ item: items,
+ };
+};
diff --git a/lib/v2/meteor/maintainer.js b/lib/v2/meteor/maintainer.js
new file mode 100644
index 000000000..e7167842b
--- /dev/null
+++ b/lib/v2/meteor/maintainer.js
@@ -0,0 +1,4 @@
+module.exports = {
+ '/boards': ['TonyRL'],
+ '/:board': ['TonyRL'],
+};
diff --git a/lib/v2/meteor/radar.js b/lib/v2/meteor/radar.js
new file mode 100644
index 000000000..62ddb22e1
--- /dev/null
+++ b/lib/v2/meteor/radar.js
@@ -0,0 +1,19 @@
+module.exports = {
+ 'meteor.today': {
+ _name: 'Meteor',
+ '.': [
+ {
+ title: '看板',
+ docs: 'https://docs.rsshub.app/bbs.html#meteor',
+ source: ['/board/:board', '/board/:board/new'],
+ target: '/meteor/board/:board',
+ },
+ {
+ title: '看板列表',
+ docs: 'https://docs.rsshub.app/bbs.html#meteor',
+ source: ['/'],
+ target: '/meteor/boards',
+ },
+ ],
+ },
+};
diff --git a/lib/v2/meteor/router.js b/lib/v2/meteor/router.js
new file mode 100644
index 000000000..a864c3020
--- /dev/null
+++ b/lib/v2/meteor/router.js
@@ -0,0 +1,4 @@
+module.exports = (router) => {
+ router.get('/boards', require('./boards'));
+ router.get('/:board', require('./index'));
+};
diff --git a/lib/v2/meteor/templates/desc.art b/lib/v2/meteor/templates/desc.art
new file mode 100644
index 000000000..9639bc1c1
--- /dev/null
+++ b/lib/v2/meteor/templates/desc.art
@@ -0,0 +1,7 @@
+{{ if youTube }}
+
+{{ else if img }}
+
+{{ else if video }}
+
+{{ /if }}
diff --git a/lib/v2/meteor/utils.js b/lib/v2/meteor/utils.js
new file mode 100644
index 000000000..451b07ddd
--- /dev/null
+++ b/lib/v2/meteor/utils.js
@@ -0,0 +1,90 @@
+const got = require('@/utils/got');
+const { art } = require('@/utils/render');
+const path = require('path');
+const baseUrl = 'https://meteor.today';
+
+const getBoards = (tryGet) =>
+ tryGet('meteor:boards', async () => {
+ const { data: response } = await got.post(`${baseUrl}/board/get_boards`, {
+ json: {
+ isCollege: 'false',
+ },
+ });
+
+ return JSON.parse(decodeURIComponent(response.result)).map((item) => ({
+ title: `${item.category ? `${item.category} - ` : ''}${item.name}`,
+ description: item.id,
+ feedDescription: item.description,
+ category: item.articleCategory,
+ link: `${baseUrl}/board/${item.alias ? item.alias : item.name}`,
+ alias: item.alias,
+ imgUrl: item.imageUrl,
+ id: item.id,
+ }));
+ });
+
+const renderDesc = (desc) => {
+ const youTube = /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)&?/g;
+ const matchYouTube = desc.match(youTube);
+ const matchImgur = desc.match(/https:\/\/i.imgur.com\/\w*.(jpg|png|gif|jpeg)/g);
+ const matchVideo = desc.match(/(https:\/\/storage\.meteor\.today\/video\/[0-9a-fA-F]{24}\.)(mp4|mov|avi|flv|wmv|mpeg|mkv)/gi);
+ const matchSticker = desc.match(/assets\/images\/stickers\/(duck|ep2|ep1)\/\w*.(jpg|png|gif|jpeg)/g);
+ const matchEmoji = desc.match(/assets\/images\/emoji\/\w*.(jpg|png|gif|jpeg)/g);
+
+ if (matchYouTube) {
+ desc = desc.replace(
+ youTube,
+ art(path.join(__dirname, 'templates/desc.art'), {
+ youTube: '$1',
+ })
+ );
+ }
+ if (matchImgur) {
+ for (const img of matchImgur) {
+ desc = desc.replace(
+ img,
+ art(path.join(__dirname, 'templates/desc.art'), {
+ img,
+ })
+ );
+ }
+ }
+ if (matchVideo) {
+ for (const video of matchVideo) {
+ desc = desc.replace(
+ video,
+ art(path.join(__dirname, 'templates/desc.art'), {
+ video,
+ })
+ );
+ }
+ }
+ if (matchSticker) {
+ for (const sticker of matchSticker) {
+ desc = desc.replace(
+ sticker,
+ art(path.join(__dirname, 'templates/desc.art'), {
+ img: sticker,
+ })
+ );
+ }
+ }
+ if (matchEmoji) {
+ for (const emoji of matchEmoji) {
+ desc = desc.replace(
+ emoji,
+ art(path.join(__dirname, 'templates/desc.art'), {
+ img: emoji,
+ })
+ );
+ }
+ }
+
+ return desc.replace(/\n/g, '
');
+};
+
+module.exports = {
+ baseUrl,
+ getBoards,
+ renderDesc,
+};