Skip to content

Commit

Permalink
feat(route/discord): add guild messages search with comprehensive par…
Browse files Browse the repository at this point in the history
…ameters (DIYgod#17522)

* feat(route/discord): add guild messages search with comprehensive parameters

* fix(route/discord): enhance search parameter validation and filtering

* Update lib/routes/discord/search.ts

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

---------

Co-authored-by: NekoAria <NekoAria@users.noreply.github.com>
  • Loading branch information
NekoAria and NekoAria authored Nov 11, 2024
1 parent 2e6aef6 commit ef86602
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 2 deletions.
46 changes: 44 additions & 2 deletions lib/routes/discord/discord-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { APIMessage } from 'discord-api-types/v10';
import { RESTGetAPIGuildResult, RESTGetAPIGuildChannelsResult, RESTGetAPIChannelResult, RESTGetAPIChannelMessagesQuery, RESTGetAPIChannelMessagesResult } from 'discord-api-types/rest/v10';

import { config } from '@/config';
import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import { config } from '@/config';
import { RESTGetAPIGuildResult, RESTGetAPIGuildChannelsResult, RESTGetAPIChannelResult, RESTGetAPIChannelMessagesQuery, RESTGetAPIChannelMessagesResult } from 'discord-api-types/rest/v10';

export const baseUrl = 'https://discord.com';
const apiUrl = `${baseUrl}/api/v10`;
Expand Down Expand Up @@ -48,3 +50,43 @@ export const getChannelMessages = (channelId, authorization, limit = 100) =>
config.cache.routeExpire,
false
) as Promise<RESTGetAPIChannelMessagesResult>;

interface SearchGuildMessagesResult {
analytics_id: string;
doing_deep_historical_index: boolean;
total_results: number;
messages: APIMessage[][];
}

export const VALID_HAS_TYPES = new Set(['link', 'embed', 'poll', 'file', 'video', 'image', 'sound', 'sticker', 'snapshot'] as const);

export type HasType = typeof VALID_HAS_TYPES extends Set<infer T> ? T : never;

export interface SearchGuildMessagesParams {
content?: string;
author_id?: string;
mentions?: string;
has?: HasType[];
max_id?: string;
min_id?: string;
channel_id?: string;
pinned?: boolean;
}

export const searchGuildMessages = (guildId: string, authorization: string, params: SearchGuildMessagesParams) =>
cache.tryGet(
`discord:guilds:${guildId}:search:${JSON.stringify(params)}`,
() => {
const queryParams = {
...params,
has: params.has?.length ? params.has : undefined,
};

return ofetch(`${apiUrl}/guilds/${guildId}/messages/search`, {
headers: { authorization },
query: queryParams,
});
},
config.cache.routeExpire,
false
) as Promise<SearchGuildMessagesResult>;
103 changes: 103 additions & 0 deletions lib/routes/discord/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import path from 'node:path';

import { config } from '@/config';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import { Route } from '@/types';
import { parseDate } from '@/utils/parse-date';
import { art } from '@/utils/render';
import ConfigNotFoundError from '@/errors/types/config-not-found';
import { queryToBoolean } from '@/utils/readable-social';

import { baseUrl, getGuild, searchGuildMessages, SearchGuildMessagesParams, HasType, VALID_HAS_TYPES } from './discord-api';

export const route: Route = {
path: '/search/:guildId/:routeParams',
categories: ['social-media'],
example: '/discord/search/302094807046684672/content=friendly&has=image,video',
parameters: {
guildId: 'Guild ID',
routeParams: 'Search parameters, support content, author_id, mentions, has, min_id, max_id, channel_id, pinned',
},
features: {
requireConfig: [
{
name: 'DISCORD_AUTHORIZATION',
description: 'Discord authorization header',
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: 'Guild Search',
maintainers: ['NekoAria'],
handler,
};

const parseSearchParams = (routeParams?: string): SearchGuildMessagesParams => {
const parsed = new URLSearchParams(routeParams);
const hasTypes = parsed.get('has')?.split(',').filter(Boolean);
const validHasTypes = hasTypes?.filter((type) => VALID_HAS_TYPES.has(type as HasType)) as HasType[];

const params = {
content: parsed.get('content') ?? undefined,
author_id: parsed.get('author_id') ?? undefined,
mentions: parsed.get('mentions') ?? undefined,
has: validHasTypes?.length ? validHasTypes : undefined,
min_id: parsed.get('min_id') ?? undefined,
max_id: parsed.get('max_id') ?? undefined,
channel_id: parsed.get('channel_id') ?? undefined,
pinned: parsed.has('pinned') ? queryToBoolean(parsed.get('pinned')) : undefined,
};

return Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined));
};

async function handler(ctx) {
const { authorization } = config.discord || {};
if (!authorization) {
throw new ConfigNotFoundError('Discord RSS is disabled due to the lack of authorization config');
}

const { guildId } = ctx.req.param();
const searchParams = parseSearchParams(ctx.req.param('routeParams'));

if (!Object.keys(searchParams).length) {
throw new InvalidParameterError('At least one valid search parameter is required');
}

const [guildInfo, searchResult] = await Promise.all([getGuild(guildId, authorization), searchGuildMessages(guildId, authorization, searchParams)]);

if (!searchResult?.messages?.length) {
return {
title: `Search Results - ${guildInfo.name}`,
link: `${baseUrl}/channels/${guildId}`,
item: [],
allowEmpty: true,
};
}

const messages = searchResult.messages.flat().map((message) => ({
title: message.content.split('\n')[0] || '(no content)',
description: art(path.join(__dirname, 'templates/message.art'), { message, guildInfo }),
author: message.author.global_name ?? message.author.username,
pubDate: parseDate(message.timestamp),
updated: message.edited_timestamp ? parseDate(message.edited_timestamp) : undefined,
category: [`#${message.channel_id}`],
link: `${baseUrl}/channels/${guildId}/${message.channel_id}/${message.id}`,
}));

const searchDesc = Object.entries(searchParams)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => `${key}:${Array.isArray(value) ? value.join(',') : value}`)
.join(' ');

return {
title: `Search "${searchDesc}" in ${guildInfo.name} - Discord`,
link: `${baseUrl}/channels/${guildId}`,
item: messages,
allowEmpty: true,
};
}

0 comments on commit ef86602

Please sign in to comment.