From 2ff90a80d453e33caee2cc39f27149d1d7386ee1 Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Mon, 29 Apr 2024 15:36:01 +0900
Subject: [PATCH] fix(backend): add detailed schema to `fetch-rss` endpoint
 (#13764)

---
 .../src/server/api/endpoints/fetch-rss.ts     | 179 +++++++++++++++++-
 .../src/ui/_common_/statusbar-rss.vue         |   5 +-
 packages/frontend/src/widgets/WidgetRss.vue   |   7 +-
 .../frontend/src/widgets/WidgetRssTicker.vue  |   7 +-
 packages/misskey-js/src/autogen/types.ts      |  47 ++++-
 5 files changed, 234 insertions(+), 11 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts
index 2085b06365..ba48b0119e 100644
--- a/packages/backend/src/server/api/endpoints/fetch-rss.ts
+++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts
@@ -20,13 +20,188 @@ export const meta = {
 	res: {
 		type: 'object',
 		properties: {
+			image: {
+				type: 'object',
+				optional: true,
+				properties: {
+					link: {
+						type: 'string',
+						optional: true,
+					},
+					url: {
+						type: 'string',
+						optional: false,
+					},
+					title: {
+						type: 'string',
+						optional: true,
+					},
+				},
+			},
+			paginationLinks: {
+				type: 'object',
+				optional: true,
+				properties: {
+					self: {
+						type: 'string',
+						optional: true,
+					},
+					first: {
+						type: 'string',
+						optional: true,
+					},
+					next: {
+						type: 'string',
+						optional: true,
+					},
+					last: {
+						type: 'string',
+						optional: true,
+					},
+					prev: {
+						type: 'string',
+						optional: true,
+					},
+				},
+			},
+			link: {
+				type: 'string',
+				optional: true,
+			},
+			title: {
+				type: 'string',
+				optional: true,
+			},
 			items: {
 				type: 'array',
+				optional: false,
 				items: {
 					type: 'object',
+					properties: {
+						link: {
+							type: 'string',
+							optional: true,
+						},
+						guid: {
+							type: 'string',
+							optional: true,
+						},
+						title: {
+							type: 'string',
+							optional: true,
+						},
+						pubDate: {
+							type: 'string',
+							optional: true,
+						},
+						creator: {
+							type: 'string',
+							optional: true,
+						},
+						summary: {
+							type: 'string',
+							optional: true,
+						},
+						content: {
+							type: 'string',
+							optional: true,
+						},
+						isoDate: {
+							type: 'string',
+							optional: true,
+						},
+						categories: {
+							type: 'array',
+							optional: true,
+							items: {
+								type: 'string',
+							},
+						},
+						contentSnippet: {
+							type: 'string',
+							optional: true,
+						},
+						enclosure: {
+							type: 'object',
+							optional: true,
+							properties: {
+								url: {
+									type: 'string',
+									optional: false,
+								},
+								length: {
+									type: 'number',
+									optional: true,
+								},
+								type: {
+									type: 'string',
+									optional: true,
+								},
+							},
+						},
+					},
 				},
-			}
-		}
+			},
+			feedUrl: {
+				type: 'string',
+				optional: true,
+			},
+			description: {
+				type: 'string',
+				optional: true,
+			},
+			itunes: {
+				type: 'object',
+				optional: true,
+				additionalProperties: true,
+				properties: {
+					image: {
+						type: 'string',
+						optional: true,
+					},
+					owner: {
+						type: 'object',
+						optional: true,
+						properties: {
+							name: {
+								type: 'string',
+								optional: true,
+							},
+							email: {
+								type: 'string',
+								optional: true,
+							},
+						},
+					},
+					author: {
+						type: 'string',
+						optional: true,
+					},
+					summary: {
+						type: 'string',
+						optional: true,
+					},
+					explicit: {
+						type: 'string',
+						optional: true,
+					},
+					categories: {
+						type: 'array',
+						optional: true,
+						items: {
+							type: 'string',
+						},
+					},
+					keywords: {
+						type: 'array',
+						optional: true,
+						items: {
+							type: 'string',
+						},
+					},
+				},
+			},
+		},
 	},
 } as const;
 
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index b973a4fd6b..6e1d06eec1 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
 import { useInterval } from '@/scripts/use-interval.js';
 import { shuffle } from '@/scripts/shuffle.js';
@@ -42,13 +43,13 @@ const props = defineProps<{
 	refreshIntervalSec?: number;
 }>();
 
-const items = ref([]);
+const items = ref<Misskey.entities.FetchRssResponse['items']>([]);
 const fetching = ref(true);
 const key = ref(0);
 
 const tick = () => {
 	window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
-		res.json().then(feed => {
+		res.json().then((feed: Misskey.entities.FetchRssResponse) => {
 			if (props.shuffle) {
 				shuffle(feed.items);
 			}
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 5d5c1188aa..e5758662cc 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, watch, computed } from 'vue';
+import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
@@ -64,7 +65,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const rawItems = ref([]);
+const rawItems = ref<Misskey.entities.FetchRssResponse['items']>([]);
 const items = computed(() => rawItems.value.slice(0, widgetProps.maxEntries));
 const fetching = ref(true);
 const fetchEndpoint = computed(() => {
@@ -79,8 +80,8 @@ const tick = () => {
 
 	window.fetch(fetchEndpoint.value, {})
 		.then(res => res.json())
-		.then(feed => {
-			rawItems.value = feed.items ?? [];
+		.then((feed: Misskey.entities.FetchRssResponse) => {
+			rawItems.value = feed.items;
 			fetching.value = false;
 		});
 };
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index af220f95e2..16306ef5ba 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, watch, computed } from 'vue';
+import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import MarqueeText from '@/components/MkMarquee.vue';
 import { GetFormResultType } from '@/scripts/form.js';
@@ -87,7 +88,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const rawItems = ref([]);
+const rawItems = ref<Misskey.entities.FetchRssResponse['items']>([]);
 const items = computed(() => {
 	const newItems = rawItems.value.slice(0, widgetProps.maxEntries);
 	if (widgetProps.shuffle) {
@@ -110,8 +111,8 @@ const tick = () => {
 
 	window.fetch(fetchEndpoint.value, {})
 		.then(res => res.json())
-		.then(feed => {
-			rawItems.value = feed.items ?? [];
+		.then((feed: Misskey.entities.FetchRssResponse) => {
+			rawItems.value = feed.items;
 			fetching.value = false;
 			key.value++;
 		});
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 131d20f09b..1b9f1304d5 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -26065,7 +26065,52 @@ export type operations = {
       200: {
         content: {
           'application/json': {
-            items: Record<string, never>[];
+            image?: {
+              link?: string;
+              url: string;
+              title?: string;
+            };
+            paginationLinks?: {
+              self?: string;
+              first?: string;
+              next?: string;
+              last?: string;
+              prev?: string;
+            };
+            link?: string;
+            title?: string;
+            items: {
+                link?: string;
+                guid?: string;
+                title?: string;
+                pubDate?: string;
+                creator?: string;
+                summary?: string;
+                content?: string;
+                isoDate?: string;
+                categories?: string[];
+                contentSnippet?: string;
+                enclosure?: {
+                  url: string;
+                  length?: number;
+                  type?: string;
+                };
+              }[];
+            feedUrl?: string;
+            description?: string;
+            itunes?: {
+              image?: string;
+              owner?: {
+                name?: string;
+                email?: string;
+              };
+              author?: string;
+              summary?: string;
+              explicit?: string;
+              categories?: string[];
+              keywords?: string[];
+              [key: string]: unknown;
+            };
           };
         };
       };