@@ -3,11 +3,12 @@ import { pathToFileURL } from "node:url";
33import { createPluginRegistry } from "@logicsrc/plugin-core" ;
44import { c0mputePlugin } from "@logicsrc/plugin-c0mpute" ;
55import { coinPayPlugin } from "@logicsrc/plugin-coinpay" ;
6+ import { discoverFeeds , feedDiscoveryPlugin , listFeedProviders , renderAtom , renderJsonFeed , renderOpml , renderRss , type FeedKind } from "@logicsrc/plugin-feed-discovery" ;
67import { sh1ptPlugin } from "@logicsrc/plugin-sh1pt" ;
78import { uGigPlugin } from "@logicsrc/plugin-ugig" ;
89import { schemas , validate } from "@logicsrc/validators" ;
910
10- const registry = createPluginRegistry ( [ coinPayPlugin , uGigPlugin , sh1ptPlugin , c0mputePlugin ] ) ;
11+ const registry = createPluginRegistry ( [ coinPayPlugin , uGigPlugin , sh1ptPlugin , c0mputePlugin , feedDiscoveryPlugin ] ) ;
1112
1213const boards = [
1314 { path : "/general" , title : "General" , description : "CommandBoard.run general discussion." } ,
@@ -69,7 +70,7 @@ async function route(request: IncomingMessage, response: ServerResponse) {
6970 json ( response , 200 , {
7071 ok : true ,
7172 service : "commandboard-api" ,
72- endpoints : [ "/health" , "/api/boards" , "/api/tasks" , "/api/plugins" , "/api/schemas" ]
73+ endpoints : [ "/health" , "/api/boards" , "/api/tasks" , "/api/plugins" , "/api/schemas" , "/api/feeds/discover" , "/api/feeds/providers" , "/rss/discover/:keyword.xml" , "/opml/discover/:keyword.xml" , "/atom/discover/:keyword.xml" , "/json-feed/discover/:keyword.json" ]
7374 } ) ;
7475 return ;
7576 }
@@ -113,6 +114,53 @@ async function route(request: IncomingMessage, response: ServerResponse) {
113114 return ;
114115 }
115116
117+ if ( request . method === "GET" && ( url . pathname === "/api/feeds/providers" || url . pathname === "/api/rss/providers" ) ) {
118+ json ( response , 200 , { providers : listFeedProviders ( ) } ) ;
119+ return ;
120+ }
121+
122+ if ( request . method === "GET" && ( url . pathname === "/api/feeds/discover" || url . pathname === "/api/rss/discover" ) ) {
123+ const query = url . searchParams . get ( "q" ) ;
124+ if ( ! query ) {
125+ json ( response , 422 , { error : "Expected q query parameter" } ) ;
126+ return ;
127+ }
128+ const result = await discoverFeeds ( {
129+ q : query ,
130+ type : ( url . searchParams . get ( "type" ) ?? "all" ) as FeedKind | "all" ,
131+ limit : numberParam ( url . searchParams . get ( "limit" ) ) ,
132+ providers : listParam ( url . searchParams . get ( "providers" ) ) ,
133+ includeUnvalidated : url . searchParams . get ( "includeUnvalidated" ) === "true"
134+ } ) ;
135+ json ( response , 200 , result ) ;
136+ return ;
137+ }
138+
139+ const formattedDiscover = matchFormattedDiscoverPath ( url . pathname ) ;
140+ if ( request . method === "GET" && formattedDiscover ) {
141+ const result = await discoverFeeds ( {
142+ q : formattedDiscover . keyword ,
143+ type : "all" ,
144+ limit : numberParam ( url . searchParams . get ( "limit" ) ) ,
145+ providers : listParam ( url . searchParams . get ( "providers" ) ) ,
146+ includeUnvalidated : url . searchParams . get ( "includeUnvalidated" ) === "true"
147+ } ) ;
148+ if ( formattedDiscover . format === "rss" ) {
149+ text ( response , 200 , "application/rss+xml; charset=utf-8" , renderRss ( result ) ) ;
150+ return ;
151+ }
152+ if ( formattedDiscover . format === "opml" ) {
153+ text ( response , 200 , "text/x-opml; charset=utf-8" , renderOpml ( result ) ) ;
154+ return ;
155+ }
156+ if ( formattedDiscover . format === "atom" ) {
157+ text ( response , 200 , "application/atom+xml; charset=utf-8" , renderAtom ( result ) ) ;
158+ return ;
159+ }
160+ text ( response , 200 , "application/feed+json; charset=utf-8" , renderJsonFeed ( result ) ) ;
161+ return ;
162+ }
163+
116164 if ( request . method === "GET" && url . pathname === "/api/plugins/sh1pt/projects" ) {
117165 json ( response , 200 , { projects : sh1ptProjects } ) ;
118166 return ;
@@ -194,6 +242,11 @@ function json(response: ServerResponse, status: number, data: unknown) {
194242 response . end ( JSON . stringify ( data , null , 2 ) ) ;
195243}
196244
245+ function text ( response : ServerResponse , status : number , contentType : string , body : string ) {
246+ response . writeHead ( status , { "content-type" : contentType } ) ;
247+ response . end ( body ) ;
248+ }
249+
197250async function readJson ( request : IncomingMessage ) {
198251 const chunks : Buffer [ ] = [ ] ;
199252 for await ( const chunk of request ) {
@@ -207,6 +260,41 @@ function isRecord(value: unknown): value is Record<string, unknown> {
207260 return typeof value === "object" && value !== null && ! Array . isArray ( value ) ;
208261}
209262
263+ function numberParam ( value : string | null ) {
264+ if ( ! value ) {
265+ return undefined ;
266+ }
267+ const parsed = Number ( value ) ;
268+ return Number . isFinite ( parsed ) ? parsed : undefined ;
269+ }
270+
271+ function listParam ( value : string | null ) {
272+ return value
273+ ?. split ( "," )
274+ . map ( ( entry ) => entry . trim ( ) )
275+ . filter ( Boolean ) ;
276+ }
277+
278+ function matchFormattedDiscoverPath ( pathname : string ) {
279+ const rss = / ^ \/ r s s \/ d i s c o v e r \/ ( .+ ) \. x m l $ / . exec ( pathname ) ;
280+ if ( rss ) {
281+ return { format : "rss" as const , keyword : decodeURIComponent ( rss [ 1 ] ) } ;
282+ }
283+ const opml = / ^ \/ o p m l \/ d i s c o v e r \/ ( .+ ) \. x m l $ / . exec ( pathname ) ?? / ^ \/ r s s \/ d i s c o v e r \/ ( .+ ) \. o p m l $ / . exec ( pathname ) ;
284+ if ( opml ) {
285+ return { format : "opml" as const , keyword : decodeURIComponent ( opml [ 1 ] ) } ;
286+ }
287+ const atom = / ^ \/ a t o m \/ d i s c o v e r \/ ( .+ ) \. x m l $ / . exec ( pathname ) ;
288+ if ( atom ) {
289+ return { format : "atom" as const , keyword : decodeURIComponent ( atom [ 1 ] ) } ;
290+ }
291+ const jsonFeed = / ^ \/ j s o n - f e e d \/ d i s c o v e r \/ ( .+ ) \. j s o n $ / . exec ( pathname ) ;
292+ if ( jsonFeed ) {
293+ return { format : "json-feed" as const , keyword : decodeURIComponent ( jsonFeed [ 1 ] ) } ;
294+ }
295+ return undefined ;
296+ }
297+
210298export function startCommandBoardServer ( port = Number ( process . env . PORT ?? 4010 ) ) {
211299 const server = createCommandBoardServer ( ) ;
212300 server . listen ( port , ( ) => {
0 commit comments