mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-10-24 02:08:55 +08:00
feat(route): mixi2 (#19359)
* feat(route): mixi2 * feat(route/mixi2): 添加参数描述 * feat(route/mixi2): 添加 feed 标题 * feat(route/mixi2): discovery 订阅路由添加图片 * feat(route/mixi2): 添加view属性 * fix(route/mixi2): 修复类型错误 * Update lib/routes/mixi2/utils.ts Co-authored-by: Tony <TonyRL@users.noreply.github.com> * Update lib/routes/mixi2/community.ts Co-authored-by: Tony <TonyRL@users.noreply.github.com> * Update lib/routes/mixi2/home.ts Co-authored-by: Tony <TonyRL@users.noreply.github.com> * Update lib/routes/mixi2/discovery.ts Co-authored-by: Tony <TonyRL@users.noreply.github.com> * add requireConfig * update * add post filter * fix the filter
This commit is contained in:
@@ -255,6 +255,10 @@ export type Config = {
|
||||
misskey: {
|
||||
accessToken?: string;
|
||||
};
|
||||
mixi2: {
|
||||
authToken?: string;
|
||||
authKey?: string;
|
||||
};
|
||||
mox: {
|
||||
cookie: string;
|
||||
};
|
||||
@@ -706,6 +710,10 @@ const calculateValue = () => {
|
||||
misskey: {
|
||||
accessToken: envs.MISSKEY_ACCESS_TOKEN,
|
||||
},
|
||||
mixi2: {
|
||||
authToken: envs.MIXI2_AUTH_TOKEN,
|
||||
authKey: envs.MIXI2_AUTH_KEY,
|
||||
},
|
||||
mox: {
|
||||
cookie: envs.MOX_COOKIE,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { type Data, type Route, ViewType } from '@/types';
|
||||
import type { Context } from 'hono';
|
||||
import { CONFIG_OPTIONS, generatePostDataItem, getClient, postFilter } from './utils';
|
||||
|
||||
const handler = async (ctx: Context) => {
|
||||
const limit = Number.parseInt(ctx.req.query('limit') ?? '20', 10);
|
||||
const communityId = ctx.req.param('id');
|
||||
const mediaOnly = ctx.req.param('media') === 'media';
|
||||
|
||||
const client = getClient();
|
||||
|
||||
const [communityInfo, postsData] = await Promise.all([
|
||||
client.getCommunity({
|
||||
communityId,
|
||||
}),
|
||||
client.getCommunityTimeline({
|
||||
communityId,
|
||||
limit,
|
||||
mediaOnly,
|
||||
}),
|
||||
]);
|
||||
|
||||
const personasData = await client.getPersonas({
|
||||
personaIds: postsData?.posts.map((post) => post.personaId),
|
||||
});
|
||||
|
||||
return {
|
||||
title: `${communityInfo.community.name} - ${mediaOnly ? 'メディア' : 'ポスト'}`,
|
||||
description: communityInfo.community.purpose.replaceAll('\n', ' '),
|
||||
link: `https://mixi.social/communities/${communityId}/about`,
|
||||
image: communityInfo.community.coverImage.postImage?.largeImageUrl,
|
||||
item:
|
||||
postsData?.posts?.filter(postFilter).map((post) => ({
|
||||
title: communityInfo.community.name,
|
||||
...generatePostDataItem(post, personasData.personas),
|
||||
})) ?? [],
|
||||
} as Data;
|
||||
};
|
||||
|
||||
export const route: Route = {
|
||||
path: '/community/:id/:media?',
|
||||
name: 'コミュニティ',
|
||||
categories: ['social-media'],
|
||||
example: '/mixi2/community/62e7e813-d242-4c54-a0ee-0aab5b2bbad2',
|
||||
parameters: {
|
||||
id: {
|
||||
description: 'コミュニティID',
|
||||
},
|
||||
media: {
|
||||
description: '`media`を入力するとメディアを含むポストのみを取得、デフォルトは空で全てのポストを取得',
|
||||
},
|
||||
},
|
||||
features: {
|
||||
supportRadar: true,
|
||||
requireConfig: CONFIG_OPTIONS,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['mixi.social/communities/:id', 'mixi.social/communities/:id/about'],
|
||||
target: '/community/:id',
|
||||
title: 'コミュニティ - ポスト',
|
||||
},
|
||||
{
|
||||
source: ['mixi.social/communities/:id', 'mixi.social/communities/:id/about'],
|
||||
target: '/community/:id/media',
|
||||
title: 'コミュニティ - メディア',
|
||||
},
|
||||
],
|
||||
view: ViewType.SocialMedia,
|
||||
handler,
|
||||
maintainers: ['KarasuShin'],
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { type Data, type Route, ViewType } from '@/types';
|
||||
import type { Context } from 'hono';
|
||||
import { CONFIG_OPTIONS, generatePostDataItem, getClient, postFilter } from './utils';
|
||||
|
||||
const handler = async (ctx: Context) => {
|
||||
const limit = Number.parseInt(ctx.req.query('limit') ?? '20', 10);
|
||||
|
||||
const client = getClient();
|
||||
|
||||
const data = await client.getRecommendedTimeline({
|
||||
limit,
|
||||
});
|
||||
|
||||
const personasData = await client.getPersonas({
|
||||
personaIds: data?.posts?.map((post) => post.personaId) ?? [],
|
||||
});
|
||||
|
||||
return {
|
||||
title: '発見',
|
||||
link: 'https://mixi.social/home/discovery',
|
||||
image: 'https://mixi.social/_next/static/media/image_logo.8bb36f11.svg',
|
||||
item:
|
||||
data?.posts?.filter(postFilter).map((post) => {
|
||||
const author = personasData.personas.find((persona) => persona.personaId === post.personaId);
|
||||
return {
|
||||
title: `@${author?.name}`,
|
||||
...generatePostDataItem(post, personasData.personas),
|
||||
};
|
||||
}) ?? [],
|
||||
} as Data;
|
||||
};
|
||||
|
||||
export const route: Route = {
|
||||
path: '/discovery',
|
||||
name: '発見',
|
||||
categories: ['social-media'],
|
||||
example: '/mixi2/discovery',
|
||||
features: {
|
||||
supportRadar: true,
|
||||
requireConfig: CONFIG_OPTIONS,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['mixi.social/home/discovery'],
|
||||
target: '/discovery',
|
||||
title: '発見',
|
||||
},
|
||||
],
|
||||
view: ViewType.SocialMedia,
|
||||
handler,
|
||||
maintainers: ['KarasuShin'],
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { type Data, type Route, ViewType } from '@/types';
|
||||
import type { Context } from 'hono';
|
||||
import { CONFIG_OPTIONS, generatePostDataItem, getClient, postFilter } from './utils';
|
||||
|
||||
const handler = async (ctx: Context) => {
|
||||
const limit = Number.parseInt(ctx.req.query('limit') ?? '20', 10);
|
||||
|
||||
const client = getClient();
|
||||
|
||||
const data = await client.getSubscribingFeeds({
|
||||
limit,
|
||||
});
|
||||
|
||||
const personasData = await client.getPersonas({
|
||||
personaIds: data?.feeds?.map((feed) => feed.post.personaId) ?? [],
|
||||
});
|
||||
|
||||
return {
|
||||
title: 'フォロー中',
|
||||
link: 'https://mixi.social/home',
|
||||
image: 'https://mixi.social/_next/static/media/image_logo.8bb36f11.svg',
|
||||
item:
|
||||
data?.feeds
|
||||
?.filter((feed) => postFilter(feed.post))
|
||||
.map((feed) => ({
|
||||
title: `@${personasData.personas.find((persona) => persona.personaId === feed.post.personaId)?.name}`,
|
||||
...generatePostDataItem(feed.post, personasData.personas),
|
||||
})) ?? [],
|
||||
} as Data;
|
||||
};
|
||||
|
||||
export const route: Route = {
|
||||
path: '/home',
|
||||
name: 'フォロー中',
|
||||
categories: ['social-media'],
|
||||
example: '/mixi2/home',
|
||||
features: {
|
||||
requireConfig: CONFIG_OPTIONS,
|
||||
supportRadar: true,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['mixi.social/home'],
|
||||
target: '/home',
|
||||
title: 'フォロー中',
|
||||
},
|
||||
],
|
||||
view: ViewType.SocialMedia,
|
||||
handler,
|
||||
maintainers: ['KarasuShin'],
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Namespace } from '@/types';
|
||||
|
||||
export const namespace: Namespace = {
|
||||
name: 'mixi2',
|
||||
url: 'mixi.social',
|
||||
lang: 'ja',
|
||||
categories: ['social-media'],
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { type Data, type Route, ViewType } from '@/types';
|
||||
import type { Context } from 'hono';
|
||||
import { CONFIG_OPTIONS, getClient, parsePost, postFilter } from './utils';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import InvalidParameterError from '@/errors/types/invalid-parameter';
|
||||
|
||||
const handler = async (ctx: Context) => {
|
||||
const limit = Number.parseInt(ctx.req.query('limit') ?? '20', 10);
|
||||
const name = ctx.req.param('name');
|
||||
const mediaOnly = ctx.req.param('media') === 'media';
|
||||
|
||||
if (!name.startsWith('@')) {
|
||||
throw new InvalidParameterError('ユーザー名は@で始まる必要があります');
|
||||
}
|
||||
|
||||
const client = getClient();
|
||||
|
||||
const userInfo = await client.getPersonaByName({
|
||||
name: name.slice(1),
|
||||
});
|
||||
|
||||
const persona = userInfo.persona;
|
||||
|
||||
const data = await client.getPersonalTimeline({
|
||||
personaId: persona?.personaId,
|
||||
limit,
|
||||
mediaOnly,
|
||||
});
|
||||
|
||||
return {
|
||||
title: `${persona?.name} - ${mediaOnly ? 'メディア' : 'ポスト'}`,
|
||||
image: persona?.avatarUrl,
|
||||
item:
|
||||
data?.posts?.filter(postFilter).map((post) => ({
|
||||
title: `@${persona?.name}`,
|
||||
description: parsePost(post),
|
||||
pubDate: parseDate(post.createdAt.seconds * 1e3),
|
||||
guid: post.postId,
|
||||
author: persona?.name,
|
||||
link: `https://mixi.social/@${persona?.name}/posts/${post.postId}`,
|
||||
})) ?? [],
|
||||
} as Data;
|
||||
};
|
||||
|
||||
export const route: Route = {
|
||||
path: '/user/:name/:media?',
|
||||
name: 'ユーザー',
|
||||
categories: ['social-media'],
|
||||
example: '/mixi2/user/@deyo',
|
||||
parameters: {
|
||||
name: {
|
||||
description: '@で始まるユーザー名',
|
||||
},
|
||||
media: {
|
||||
description: '`media`を入力するとメディアを含むポストのみを取得、デフォルトは空で全てのポストを取得',
|
||||
},
|
||||
},
|
||||
features: {
|
||||
requireConfig: CONFIG_OPTIONS,
|
||||
supportRadar: true,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['mixi.social/:id'],
|
||||
target: '/user/:id',
|
||||
title: 'ユーザー - ポスト',
|
||||
},
|
||||
{
|
||||
source: ['mixi.social/:id'],
|
||||
target: '/user/:id/media',
|
||||
title: 'ユーザー - メディア',
|
||||
},
|
||||
],
|
||||
view: ViewType.SocialMedia,
|
||||
handler,
|
||||
maintainers: ['KarasuShin'],
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Category, MixiClient, Persona, type Post } from 'mixi2';
|
||||
import { config } from '@/config';
|
||||
import ConfigNotFoundError from '@/errors/types/config-not-found';
|
||||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
export function getClient() {
|
||||
const { authToken, authKey } = config.mixi2;
|
||||
|
||||
if (!authToken || !authKey) {
|
||||
throw new ConfigNotFoundError('MIXI2_AUTH_TOKEN and MIXI2_AUTH_KEY are required');
|
||||
}
|
||||
|
||||
return new MixiClient(`auth_token=${authToken}`, authKey, {
|
||||
httpAdapter: ofetch,
|
||||
});
|
||||
}
|
||||
|
||||
export function parsePost(post: Post) {
|
||||
let description = post.text ? `<p>${post.text}</p>` : '';
|
||||
|
||||
for (const media of post.medias ?? []) {
|
||||
if (media.category === Category.CATEGORY_POST_IMAGE) {
|
||||
description += `<img src="${media.postImage?.largeImageUrl ?? media.postImage?.smallImageUrl}"${media.description ? `alt="${media.description}"` : ''} />`;
|
||||
} else if (media.category === Category.CATEGORY_POST_VIDEO) {
|
||||
description += `<img src="${media.postVideo?.previewImageUrl}" alt="${media.description}" />`;
|
||||
}
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
export function generatePostDataItem(post: Post, personas: Persona[]) {
|
||||
const author = personas.find((persona) => persona.personaId === post.personaId);
|
||||
return {
|
||||
description: parsePost(post),
|
||||
pubDate: parseDate(post.createdAt.seconds * 1e3),
|
||||
guid: post.postId,
|
||||
author: author?.name,
|
||||
link: `https://mixi.social/@${author?.name}/posts/${post.postId}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const CONFIG_OPTIONS = [
|
||||
{
|
||||
name: 'MIXI2_AUTH_TOKEN',
|
||||
description: 'mixi2ログイン後の情報。ブラウザのコンソールでクッキーから `auth_token` の値を取得してください',
|
||||
},
|
||||
{
|
||||
name: 'MIXI2_AUTH_KEY',
|
||||
description: 'mixi2ログイン後の情報。ブラウザのコンソールでリクエストヘッダーから `x-auth-key` の値を取得してください',
|
||||
},
|
||||
];
|
||||
|
||||
export function postFilter(post: Post) {
|
||||
return !post.isDeleted && post.personaId;
|
||||
}
|
||||
@@ -101,6 +101,7 @@
|
||||
"lz-string": "1.5.0",
|
||||
"mailparser": "3.7.3",
|
||||
"markdown-it": "14.1.0",
|
||||
"mixi2": "0.2.2",
|
||||
"module-alias": "2.2.3",
|
||||
"narou": "1.2.0",
|
||||
"notion-to-md": "3.1.9",
|
||||
|
||||
Generated
+10
@@ -191,6 +191,9 @@ importers:
|
||||
markdown-it:
|
||||
specifier: 14.1.0
|
||||
version: 14.1.0
|
||||
mixi2:
|
||||
specifier: 0.2.2
|
||||
version: 0.2.2
|
||||
module-alias:
|
||||
specifier: 2.2.3
|
||||
version: 2.2.3
|
||||
@@ -4688,6 +4691,9 @@ packages:
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
mixi2@0.2.2:
|
||||
resolution: {integrity: sha512-8rgPNsuKIMzLctZ8oeo9idLTahvJCamljcdVth1Tx/Gqp7d3vAWzKYUsBNCr+wqD5zf/0rrYJPZCo0WZqadcbQ==}
|
||||
|
||||
mixin-object@2.0.1:
|
||||
resolution: {integrity: sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -10979,6 +10985,10 @@ snapshots:
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
mixi2@0.2.2:
|
||||
dependencies:
|
||||
protobufjs: 7.5.3
|
||||
|
||||
mixin-object@2.0.1:
|
||||
dependencies:
|
||||
for-in: 0.1.8
|
||||
|
||||
Reference in New Issue
Block a user