@@ -2,96 +2,167 @@ import { Command } from "../../interfaces";
22import { SlashCommandBuilder } from "@discordjs/builders" ;
33import Doc , { sources } from "discord.js-docs" ;
44import { checkEmbedLimits } from "../../utils/EmbedUtils" ;
5- import { deleteButton } from "../../utils/CommandUtils" ;
5+ import { deleteButton , deleteButtonHandler } from "../../utils/CommandUtils" ;
66import { MessageActionRow , MessageSelectMenu } from "discord.js" ;
77import type { APIEmbed } from "discord-api-types" ;
88
99const supportedBranches = Object . keys ( sources ) . map ( ( branch ) => [ capitalize ( branch ) , branch ] as [ string , string ] ) ;
1010
1111const command : Command = {
12- data : new SlashCommandBuilder ( )
13- . setName ( "djs" )
14- . setDescription ( "Search discord.js documentations, supports builders, voice, collection and rpc documentations" )
15- . addStringOption ( ( option ) =>
16- option
17- . setName ( "query" )
18- . setDescription ( "Enter the phrase you'd like to search for, e.g: client#isready" )
19- . setRequired ( true )
20- . setAutocomplete ( true ) ,
21- )
22- . addStringOption ( ( option ) =>
23- option
24- . setName ( "source" )
25- . setDescription ( "Select which branch/repository to get documentation off of (Default: stable)" )
26- . addChoices ( supportedBranches )
27- . setRequired ( false ) ,
28- )
29- . addBooleanOption ( ( option ) =>
30- option
31- . setName ( "private" )
32- . setDescription ( "Whether or not to search private elements (default false)" )
33- . setRequired ( false ) ,
34- ) ,
35- async execute ( interaction ) {
36- const deleteButtonRow = new MessageActionRow ( ) . addComponents ( [ deleteButton ( interaction . user . id ) ] ) ;
37- const query = interaction . options . getString ( "query" ) ;
38- // The Default source should be stable
39- const source : keyof typeof sources =
40- ( interaction . options . getString ( "source" ) as keyof typeof sources ) || "stable" ;
41- // Whether to include private elements on the search results, by default false, shows private elements if the search returns an exact result;
42- const searchPrivate = interaction . options . getBoolean ( "private" ) || false ;
43- const doc = await Doc . fetch ( source , { force : true } ) . catch ( console . error ) ;
44-
45- if ( ! doc ) {
46- await interaction . editReply ( { content : "Couldn't fetch docs" } ) . catch ( console . error ) ;
47- return ;
48- }
49- // const resultEmbed = doc.resolveEmbed(query, { excludePrivateElements: !searchPrivate });
50- const result = searchDJSDoc ( doc , query , searchPrivate ) ;
51- // If a result wasn't found
52- if ( ! result || ( result as APIEmbed ) . description === "" ) {
53- const notFoundEmbed = doc . baseEmbed ( ) ;
54- notFoundEmbed . description = "Didn't find any results for that query" ;
55-
56- const timeStampDate = new Date ( notFoundEmbed . timestamp ) ;
57- // Satisfies the method's MessageEmbedOption type
58- const embedObj = { ...notFoundEmbed , timestamp : timeStampDate } ;
59-
60- await interaction . editReply ( { embeds : [ embedObj ] } ) . catch ( console . error ) ;
61- return ;
62- } else if ( Array . isArray ( result ) ) {
63- // If there are multiple results, send a select menu from which the user can choose which one to send
64-
65- const selectMenuRow = new MessageActionRow ( ) . addComponents (
66- new MessageSelectMenu ( )
67- . setCustomId ( `djsselect/${ source } /${ searchPrivate } /${ interaction . user . id } ` )
68- . addOptions ( result )
69- . setPlaceholder ( "Select documentation to send" ) ,
70- ) ;
71- await interaction
72- . editReply ( {
73- content : "Didn't find an exact match, please select one from below" ,
74- components : [ selectMenuRow ] ,
75- } )
76- . catch ( console . error ) ;
12+ slashCommand : {
13+ data : new SlashCommandBuilder ( )
14+ . setName ( "djs" )
15+ . setDescription (
16+ "Search discord.js documentations, supports builders, voice, collection and rpc documentations" ,
17+ )
18+ . addStringOption ( ( option ) =>
19+ option
20+ . setName ( "query" )
21+ . setDescription ( "Enter the phrase you'd like to search for, e.g: client#isready" )
22+ . setRequired ( true )
23+ . setAutocomplete ( true ) ,
24+ )
25+ . addStringOption ( ( option ) =>
26+ option
27+ . setName ( "source" )
28+ . setDescription ( "Select which branch/repository to get documentation off of (Default: stable)" )
29+ . addChoices ( supportedBranches )
30+ . setRequired ( false ) ,
31+ )
32+ . addBooleanOption ( ( option ) =>
33+ option
34+ . setName ( "private" )
35+ . setDescription ( "Whether or not to search private elements (default false)" )
36+ . setRequired ( false ) ,
37+ ) ,
38+ async run ( interaction ) {
39+ const deleteButtonRow = new MessageActionRow ( ) . addComponents ( [ deleteButton ( interaction . user . id ) ] ) ;
40+ const queryOption = interaction . options . getString ( "query" ) ;
41+ const sourceOption = interaction . options . getString ( "source" ) as keyof typeof sources ;
42+
43+ let { Source : source = "stable" , Query : query } =
44+ queryOption . match ( / (?: (?< Source > [ ^ / ] * ) \/ ) ? (?< Query > (?: .| \s ) * ) / i) ?. groups ?? { } ;
45+ // The Default source should be stable
46+ if ( ! sources [ source ] ) source = "stable" ;
47+
48+ if ( sourceOption ) source = sourceOption ;
49+ // Whether to include private elements on the search results, by default false, shows private elements if the search returns an exact result;
50+ const searchPrivate = interaction . options . getBoolean ( "private" ) || false ;
51+ const doc = await Doc . fetch ( source , { force : true } ) . catch ( console . error ) ;
52+
53+ if ( ! doc ) {
54+ await interaction . editReply ( { content : "Couldn't fetch docs" } ) . catch ( console . error ) ;
55+ return ;
56+ }
57+ // const resultEmbed = doc.resolveEmbed(query, { excludePrivateElements: !searchPrivate });
58+ const result = searchDJSDoc ( doc , query , searchPrivate ) ;
59+ // If a result wasn't found
60+ if ( ! result || ( result as APIEmbed ) . description === "" ) {
61+ const notFoundEmbed = doc . baseEmbed ( ) ;
62+ notFoundEmbed . description = "Didn't find any results for that query" ;
63+
64+ const timeStampDate = new Date ( notFoundEmbed . timestamp ) ;
65+ // Satisfies the method's MessageEmbedOption type
66+ const embedObj = { ...notFoundEmbed , timestamp : timeStampDate } ;
67+
68+ await interaction . editReply ( { embeds : [ embedObj ] } ) . catch ( console . error ) ;
69+ return ;
70+ } else if ( Array . isArray ( result ) ) {
71+ // If there are multiple results, send a select menu from which the user can choose which one to send
72+
73+ const selectMenuRow = new MessageActionRow ( ) . addComponents (
74+ new MessageSelectMenu ( )
75+ . setCustomId ( `djsselect/${ source } /${ searchPrivate } /${ interaction . user . id } ` )
76+ . addOptions ( result )
77+ . setPlaceholder ( "Select documentation to send" ) ,
78+ ) ;
79+ await interaction
80+ . editReply ( {
81+ content : "Didn't find an exact match, please select one from below" ,
82+ components : [ selectMenuRow ] ,
83+ } )
84+ . catch ( console . error ) ;
85+ return ;
86+ }
87+ const resultEmbed = result ;
88+ const timeStampDate = new Date ( resultEmbed . timestamp ) ;
89+ const embedObj = { ...resultEmbed , timestamp : timeStampDate } ;
90+
91+ //! "checkEmbedLimits" does not support MessageEmbed objects due to the properties being null by default, use a raw embed object for this method
92+ // Check if the embed exceeds any of the limits
93+ if ( ! checkEmbedLimits ( [ resultEmbed ] ) ) {
94+ // The final field should be the View Source button
95+ embedObj . fields = [ embedObj . fields ?. at ( - 1 ) ] ;
96+ }
97+ await interaction . editReply ( {
98+ content : "Sent documentations for " + ( query . length >= 100 ? query . slice ( 0 , 100 ) + "..." : query ) ,
99+ } ) ;
100+ await interaction . followUp ( { embeds : [ embedObj ] , components : [ deleteButtonRow ] } ) . catch ( console . error ) ;
77101 return ;
78- }
79- const resultEmbed = result ;
80- const timeStampDate = new Date ( resultEmbed . timestamp ) ;
81- const embedObj = { ...resultEmbed , timestamp : timeStampDate } ;
82-
83- //! "checkEmbedLimits" does not support MessageEmbed objects due to the properties being null by default, use a raw embed object for this method
84- // Check if the embed exceeds any of the limits
85- if ( ! checkEmbedLimits ( [ resultEmbed ] ) ) {
86- // The final field should be the View Source button
87- embedObj . fields = [ embedObj . fields ?. at ( - 1 ) ] ;
88- }
89- await interaction . editReply ( {
90- content : "Sent documentations for " + ( query . length >= 100 ? query . slice ( 0 , 100 ) + "..." : query ) ,
91- } ) ;
92- await interaction . followUp ( { embeds : [ embedObj ] , components : [ deleteButtonRow ] } ) . catch ( console . error ) ;
93- return ;
102+ } ,
94103 } ,
104+ buttons : [ { custom_id : "deletebtn" , run : deleteButtonHandler } ] ,
105+ selectMenus : [
106+ {
107+ custom_id : "djsselect" ,
108+ async run ( interaction ) {
109+ const selectedValue = interaction . values [ 0 ] ;
110+ const [ , source , searchPrivate , Initiator ] = interaction . customId . split ( "/" ) ;
111+ const deleteButtonRow = new MessageActionRow ( ) . addComponents ( [ deleteButton ( Initiator ) ] ) ;
112+
113+ const doc = await Doc . fetch ( source , { force : true } ) ;
114+
115+ const resultEmbed = searchDJSDoc ( doc , selectedValue , searchPrivate === "true" ) as APIEmbed ;
116+
117+ // Remove the menu and update the ephemeral message
118+ await interaction
119+ . editReply ( { content : "Sent documentations for " + selectedValue , components : [ ] } )
120+ . catch ( console . error ) ;
121+ // Send documentation
122+ await interaction
123+ . followUp ( { embeds : [ resultEmbed ] , components : [ deleteButtonRow ] } )
124+ . catch ( console . error ) ;
125+ } ,
126+ } ,
127+ ] ,
128+ autocomplete : [
129+ {
130+ focusedOption : "query" ,
131+ async run ( interaction , focusedOption ) {
132+ const focusedOptionValue = focusedOption . value as string ;
133+ let { Branch : branchOrProject = "stable" , Query : query } =
134+ focusedOptionValue . match ( / (?: (?< Branch > [ ^ / ] * ) \/ ) ? (?< Query > (?: .| \s ) * ) / i) ?. groups ?? { } ;
135+ if ( ! sources [ branchOrProject ] ) branchOrProject = "stable" ;
136+
137+ const doc = await Doc . fetch ( branchOrProject , { force : false } ) ;
138+ const singleElement = doc . get ( ...query . split ( / \. | # / ) ) ;
139+ if ( singleElement ) {
140+ await interaction
141+ . respond ( [
142+ {
143+ name : singleElement . formattedName ,
144+ value : branchOrProject + "/" + singleElement . formattedName ,
145+ } ,
146+ ] )
147+ . catch ( console . error ) ;
148+ return ;
149+ }
150+ const searchResults = doc . search ( query , { excludePrivateElements : false } ) ;
151+ if ( ! searchResults ) {
152+ await interaction . respond ( [ ] ) . catch ( console . error ) ;
153+ return ;
154+ }
155+ await interaction
156+ . respond (
157+ searchResults . map ( ( elem ) => ( {
158+ name : elem . formattedName ,
159+ value : branchOrProject + "/" + elem . formattedName ,
160+ } ) ) ,
161+ )
162+ . catch ( console . error ) ;
163+ } ,
164+ } ,
165+ ] ,
95166} ;
96167
97168function capitalize ( str : string ) {
@@ -112,8 +183,9 @@ export function searchDJSDoc(doc: Doc, query: string, searchPrivate?: boolean) {
112183 const searchResults = doc . search ( query , options ) ;
113184 if ( ! searchResults ) return null ;
114185 return searchResults . map ( ( res ) => {
186+ const parsedDescription = res . description ?. trim ?.( ) ?? "No description provided" ;
115187 // Labels and values have a limit of 100 characters
116- const description = res . description . length >= 99 ? res . description . slice ( 0 , 96 ) + "..." : res . description ;
188+ const description = parsedDescription . length >= 99 ? parsedDescription . slice ( 0 , 96 ) + "..." : parsedDescription ;
117189 return {
118190 label : res . formattedName ,
119191 description,
0 commit comments