diff --git a/docs/en/social-media.md b/docs/en/social-media.md
index 0dd47ed38..fccc7da8e 100644
--- a/docs/en/social-media.md
+++ b/docs/en/social-media.md
@@ -300,6 +300,12 @@ Due to Telegram restrictions, some channels involving pornography, copyright, an
+## TikTok
+
+### User
+
+
+
## Twitter
::: warning
diff --git a/lib/v2/tiktok/router.js b/lib/v2/tiktok/router.js
new file mode 100644
index 000000000..5aabfd076
--- /dev/null
+++ b/lib/v2/tiktok/router.js
@@ -0,0 +1,3 @@
+module.exports = (router) => {
+ router.get('/user/:user', require('./user'));
+};
diff --git a/lib/v2/tiktok/templates/maintainer.js b/lib/v2/tiktok/templates/maintainer.js
new file mode 100644
index 000000000..59dcb464e
--- /dev/null
+++ b/lib/v2/tiktok/templates/maintainer.js
@@ -0,0 +1,3 @@
+module.exports = {
+ '/user/:user': ['TonyRL'],
+};
diff --git a/lib/v2/tiktok/templates/radar.js b/lib/v2/tiktok/templates/radar.js
new file mode 100644
index 000000000..8bfb99d0c
--- /dev/null
+++ b/lib/v2/tiktok/templates/radar.js
@@ -0,0 +1,13 @@
+module.exports = {
+ 'tiktok.com': {
+ _name: 'TikTok',
+ '.': [
+ {
+ title: 'User',
+ docs: 'https://docs.rsshub.app/en/social-media.html#tiktok',
+ source: ['/:user'],
+ target: '/tiktok/user/:user',
+ },
+ ],
+ },
+};
diff --git a/lib/v2/tiktok/templates/user.art b/lib/v2/tiktok/templates/user.art
new file mode 100644
index 000000000..a8e2982d6
--- /dev/null
+++ b/lib/v2/tiktok/templates/user.art
@@ -0,0 +1,3 @@
+
diff --git a/lib/v2/tiktok/user.js b/lib/v2/tiktok/user.js
new file mode 100644
index 000000000..15dd52e23
--- /dev/null
+++ b/lib/v2/tiktok/user.js
@@ -0,0 +1,66 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const config = require('@/config').value;
+const { parseDate } = require('@/utils/parse-date');
+const { art } = require('@/utils/render');
+const path = require('path');
+
+const baseUrl = 'https://www.tiktok.com';
+
+module.exports = async (ctx) => {
+ const { user } = ctx.params;
+
+ const html = await ctx.cache.tryGet(
+ `tiktok:user:${user}`,
+ async () => {
+ const browser = await require('@/utils/puppeteer')();
+ const page = await browser.newPage();
+ await page.goto(`${baseUrl}/${user}`, {
+ waitUntil: 'networkidle2',
+ });
+ const html = await page.evaluate(() => document.documentElement.innerHTML);
+ browser.close();
+ return html;
+ },
+ config.cache.routeExpire,
+ false
+ );
+
+ const $ = cheerio.load(html);
+
+ let items = $('[class*=-DivItemContainerV2]')
+ .toArray()
+ .map((item) => ({ link: $(item).find('[class*=-DivWrapper] a').attr('href') }));
+
+ items = await Promise.all(
+ items.map((item) =>
+ ctx.cache.tryGet(item.link, async () => {
+ const videoId = item.link.substring(item.link.lastIndexOf('/') + 1);
+ const api = (
+ await got(`${baseUrl}/node/share/video/${user}/${videoId}`, {
+ headers: {
+ referer: `${baseUrl}/${user}`,
+ },
+ })
+ ).data;
+
+ item.title = api.itemInfo.itemStruct.desc;
+ item.description = art(path.join(__dirname, 'templates/user.art'), {
+ poster: api.itemInfo.itemStruct.video.cover,
+ source: api.itemInfo.itemStruct.video.playAddr,
+ });
+ item.author = api.itemInfo.itemStruct.author.nickname;
+ item.pubDate = parseDate(api.itemInfo.itemStruct.createTime * 1000);
+
+ return item;
+ })
+ )
+ );
+
+ ctx.state.data = {
+ title: $('title').text(),
+ description: $('[data-e2e=user-bio]').text(),
+ link: `${baseUrl}/${user}`,
+ item: items,
+ };
+};