diff --git a/commands/interaction/slash/search/duckduckgo.js b/commands/interaction/slash/search/duckduckgo.js new file mode 100644 index 0000000..a885369 --- /dev/null +++ b/commands/interaction/slash/search/duckduckgo.js @@ -0,0 +1,148 @@ +const { duckduckgo } = require('#api'); +const { paginator } = require('#client'); +const { PERMISSION_GROUPS } = require('#constants'); + +const { createEmbed, formatPaginationEmbeds, page } = require('#utils/embed'); +const { acknowledge } = require('#utils/interactions'); +const { citation, link, favicon} = require('#utils/markdown') +const { editOrReply } = require('#utils/message') +const { STATICS } = require('#utils/statics'); +const { ApplicationCommandOptionTypes, ApplicationIntegrationTypes, InteractionContextTypes } = require('detritus-client/lib/constants'); + +function renderFooter(context, bang){ + if(!bang) return { + iconUrl: STATICS.duckduckgo, + text: `DuckDuckGo • ${context.application.name}` + } + + return { + iconUrl: favicon(bang.site), + text: `${bang.name} • DuckDuckGo • ${context.application.name}` + } +} + +// `type` can be found in search_service/endpoints/duckduckgo.js +function createSearchResultPage(context, entry, bang){ + let res; + switch(entry.type){ + case 1: // WebPage + res = page(createEmbed("default", context, { + author: { + iconUrl: favicon(entry.result.url), + name: new URL(entry.result.url).host, + url: entry.result.url + }, + url: entry.result.url, + title: entry.result.title, + fields: [], + description: `${entry.result.snippet}`, + footer: renderFooter(context, bang) + })) + if(entry.result.image) res.embeds[0].thumbnail = { url: entry.result.image } + if(entry.result.deepLinks) { + let fl = entry.result.deepLinks; + while(fl.length >= 1){ + fields = fl.splice(0, 4) + fields = fields.map((f)=>link(f.url, f.title)) + res.embeds[0].fields.push({ + name: "​", + value: fields.join('\n'), + inline: true + }) + } + } + break; + case 2: // Entity + res = page(createEmbed("default", context, { + author: { + iconUrl: favicon(entry.result.url), + name: entry.result.title, + url: entry.result.url + }, + description: `${entry.result.description}`, + fields: [], + footer: renderFooter(context, bang) + })) + if(entry.result.sources.description) res.embeds[0].description += citation(1, entry.result.sources.description.url, `Source: ${entry.result.sources.description.title}`) + if(entry.result.image) res.embeds[0].thumbnail = { url: entry.result.image } + if(entry.result.fields){ + // only up to 6 fields + for(const f of entry.result.fields.splice(0, 6)){ + if(f.url){ + res.embeds[0].fields.push({ + name: f.title, + value: f.value, + inline: true + }) + continue; + } + res.embeds[0].fields.push({ + name: f.title, + value: f.value, + inline: true + }) + } + } + break; + default: + break; + } + + return res; +} + + +module.exports = { + name: 'duckduckgo', + description: 'Search on DuckDuckGo. Supports !bangs.', + contexts: [ + InteractionContextTypes.GUILD, + InteractionContextTypes.PRIVATE_CHANNEL, + InteractionContextTypes.BOT_DM + ], + integrationTypes: [ + ApplicationIntegrationTypes.USER_INSTALL + ], + options: [ + { + name: 'query', + description: 'Search query.', + type: ApplicationCommandOptionTypes.TEXT, + required: true + }, + { + name: 'incognito', + description: 'Makes the response only visible to you.', + type: ApplicationCommandOptionTypes.BOOLEAN, + required: false, + default: false + } + ], + run: async (context, args) => { + await acknowledge(context, args.incognito, [...PERMISSION_GROUPS.baseline_slash]); + + if(!args.query) return editOrReply(context, createEmbed("warning", context, `Missing Parameter (query).`)) + try{ + let search = await duckduckgo(context, args.query, context.channel.nsfw) + search = search.response + + if(search.body.status == 2) return editOrReply(context, createEmbed("error", context, search.body.message)) + + let pages = [] + for(const res of search.body.results){ + let sp = createSearchResultPage(context, res, search.body.bang) + if(sp) pages.push(sp) + } + + if(!pages.length) return editOrReply(context, createEmbed("warning", context, `No results found.`)) + + await paginator.createPaginator({ + context, + pages: formatPaginationEmbeds(pages) + }); + }catch(e){ + console.log(e) + return editOrReply(context, createEmbed("error", context, `Unable to perform DuckDuckGo search.`)) + } + }, +}; \ No newline at end of file diff --git a/commands/message/search/duckduckgo.js b/commands/message/search/duckduckgo.js new file mode 100644 index 0000000..e3501c0 --- /dev/null +++ b/commands/message/search/duckduckgo.js @@ -0,0 +1,132 @@ +const { duckduckgo } = require('#api'); +const { paginator } = require('#client'); +const { PERMISSION_GROUPS } = require('#constants'); + +const { createEmbed, formatPaginationEmbeds, page } = require('#utils/embed'); +const { acknowledge } = require('#utils/interactions'); +const { citation, link, favicon} = require('#utils/markdown') +const { editOrReply } = require('#utils/message') +const { STATICS } = require('#utils/statics') + +function renderFooter(context, bang){ + if(!bang) return { + iconUrl: STATICS.duckduckgo, + text: `DuckDuckGo • ${context.application.name}` + } + + return { + iconUrl: favicon(bang.site), + text: `${bang.name} • DuckDuckGo • ${context.application.name}` + } +} + +// `type` can be found in search_service/endpoints/duckduckgo.js +function createSearchResultPage(context, entry, bang){ + let res; + switch(entry.type){ + case 1: // WebPage + res = page(createEmbed("default", context, { + author: { + iconUrl: favicon(entry.result.url), + name: new URL(entry.result.url).host, + url: entry.result.url + }, + url: entry.result.url, + title: entry.result.title, + fields: [], + description: `${entry.result.snippet}`, + footer: renderFooter(context, bang) + })) + if(entry.result.image) res.embeds[0].thumbnail = { url: entry.result.image } + if(entry.result.deepLinks) { + let fl = entry.result.deepLinks; + while(fl.length >= 1){ + fields = fl.splice(0, 4) + fields = fields.map((f)=>link(f.url, f.title)) + res.embeds[0].fields.push({ + name: "​", + value: fields.join('\n'), + inline: true + }) + } + } + break; + case 2: // Entity + res = page(createEmbed("default", context, { + author: { + iconUrl: favicon(entry.result.url), + name: entry.result.title, + url: entry.result.url + }, + description: `${entry.result.description}`, + fields: [], + footer: renderFooter(context, bang) + })) + if(entry.result.sources.description) res.embeds[0].description += citation(1, entry.result.sources.description.url, `Source: ${entry.result.sources.description.title}`) + if(entry.result.image) res.embeds[0].thumbnail = { url: entry.result.image } + if(entry.result.fields){ + // only up to 6 fields + for(const f of entry.result.fields.splice(0, 6)){ + if(f.url){ + res.embeds[0].fields.push({ + name: f.title, + value: f.value, + inline: true + }) + continue; + } + res.embeds[0].fields.push({ + name: f.title, + value: f.value, + inline: true + }) + } + } + break; + default: + break; + } + + return res; +} + +module.exports = { + name: 'duckduckgo', + label: 'query', + aliases: ['ddg'], + metadata: { + description: 'Returns search results from DuckDuckGo.\n\nSupports Bangs.', + description_short: 'Search on DuckDuckGo', + examples: ['duckduckgo Eurasian Small Clawed Otter','ddg otters !w','ddg markiplier !yt'], + category: 'search', + usage: 'duckduckgo ' + }, + permissionsClient: [...PERMISSION_GROUPS.baseline], + run: async (context, args) => { + await acknowledge(context); + + if(!args.query) return editOrReply(context, createEmbed("warning", context, `Missing Parameter (query).`)) + try{ + let search = await duckduckgo(context, args.query, context.channel.nsfw) + search = search.response + + if(search.body.status == 2) return editOrReply(context, createEmbed("error", context, search.body.message)) + + let pages = [] + for(const res of search.body.results){ + let sp = createSearchResultPage(context, res, search.body.bang) + if(sp) pages.push(sp) + } + + if(!pages.length) return editOrReply(context, createEmbed("warning", context, `No results found.`)) + + await paginator.createPaginator({ + context, + pages: formatPaginationEmbeds(pages) + }); + }catch(e){ + console.log(e) + return editOrReply(context, createEmbed("error", context, `Unable to perform DuckDuckGo search.`)) + } + }, +}; \ No newline at end of file diff --git a/labscore/api/endpoints.js b/labscore/api/endpoints.js index bbffa30..969a615 100644 --- a/labscore/api/endpoints.js +++ b/labscore/api/endpoints.js @@ -38,6 +38,7 @@ const Api = Object.freeze({ SEARCH_BING: '/search/bing', SEARCH_BING_IMAGES: '/search/bing-images', + SEARCH_DUCKDUCKGO: '/search/duckduckgo', SEARCH_GOOGLE: '/search/google', SEARCH_GOOGLE_IMAGES: '/search/google-images', SEARCH_GOOGLE_NEWS: '/search/google-news', diff --git a/labscore/api/index.js b/labscore/api/index.js index 18feec6..73382e2 100644 --- a/labscore/api/index.js +++ b/labscore/api/index.js @@ -196,6 +196,13 @@ module.exports.bingImages = async function(context, query, nsfw){ }) } +module.exports.duckduckgo = async function(context, query, nsfw){ + return await request(Api.SEARCH_DUCKDUCKGO, "GET", {}, { + q: query, + nsfw: nsfw + }) +} + module.exports.reverseImageSearch = async function(context, url){ return await request(Api.SEARCH_REVERSE_IMAGE, "GET", {}, { url: url diff --git a/labscore/utils/statics.js b/labscore/utils/statics.js index 9bcf733..6d07bcc 100644 --- a/labscore/utils/statics.js +++ b/labscore/utils/statics.js @@ -64,6 +64,10 @@ const Statics = Object.freeze({ file: "brands/chatgpt.png", revision: 1 }, + duckduckgo: { + file: "brands/duckduckgo.png", + revision: 0 + }, emojipedia: { file: "brands/emojipedia.png", revision: 3 @@ -255,6 +259,7 @@ module.exports.STATICS = Object.freeze({ bard: staticAsset(Statics.brands.bard), bing: staticAsset(Statics.brands.bing), chatgpt: staticAsset(Statics.brands.chatgpt), + duckduckgo: staticAsset(Statics.brands.duckduckgo), genius: staticAsset(Statics.brands.genius), google: staticAsset(Statics.brands.google), googlelens: staticAsset(Statics.brands.googlelens),