From 3699856b6b486807e47f4e53dc33bcbf981cf9d4 Mon Sep 17 00:00:00 2001 From: Noah Heague Date: Sun, 5 Sep 2021 13:32:28 +1000 Subject: [PATCH] Stripped repo down to bare minimum --- bot/.gitignore | 1 + bot/CONTRIBUTING.md | 62 +++++++++++ bot/README.md | 23 +--- bot/package.json | 4 +- bot/src/config.ts | 41 +------ bot/src/createEmbed/createEmbed.ts | 135 ------------------------ bot/src/getSongLink/getSongLink.test.ts | 89 ---------------- bot/src/getSongLink/getSongLink.ts | 79 -------------- bot/src/getSongLink/metadata/deezer.ts | 52 --------- bot/src/help/help.ts | 2 +- bot/src/index.ts | 28 +---- bot/src/slashCommands/register.ts | 3 +- bot/src/utils/utils.ts | 5 - 13 files changed, 73 insertions(+), 451 deletions(-) create mode 100644 bot/CONTRIBUTING.md delete mode 100644 bot/src/createEmbed/createEmbed.ts delete mode 100644 bot/src/getSongLink/getSongLink.test.ts delete mode 100644 bot/src/getSongLink/getSongLink.ts delete mode 100644 bot/src/getSongLink/metadata/deezer.ts delete mode 100644 bot/src/utils/utils.ts diff --git a/bot/.gitignore b/bot/.gitignore index f3c728e5b..e44143f63 100644 --- a/bot/.gitignore +++ b/bot/.gitignore @@ -1,3 +1,4 @@ node_modules .idea +.vscode yarn-error.log diff --git a/bot/CONTRIBUTING.md b/bot/CONTRIBUTING.md new file mode 100644 index 000000000..817521d72 --- /dev/null +++ b/bot/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing +A basic overview of code-style & preferred methods of doing things. + +If you have any questions or suggestions, please create a ticket. We are open to suggestions. + +# Styleguides +## Git commit messages +- Be explicit about what you changed. ("Fixed bug" vs "Fixed bug relating to incorrect import") +- reference issue numbers if possible ("Fixed incorrect env var as reported in #3") + +## Code preferences +These are not strictly necessary however it is nice to have a codebase where everyone's code looks the same + +Verboseness is preferred, be explicit with your types and interfaces. +If you use an object more than once or pass it around, create an interface or type. + +### Enums +enums are preferred due to their nicer accessor patterns as well as making potential refactors easier. + +```ts +/** Preferred */ +export enum LoggerLayers { + client = "client", + slashCommands = "slashCommands" +} + +const logger = createLayeredLogger(LoggerLayers.client) +``` +```ts +/** Allowed but not preferred */ +export type loggerLayer = "client" | "server" +const logger = createLayeredLogger("client") +``` + +### Prettier +Prettier is not installed due to its habit of making code look terrible. We trust that contributors will format their code to match the style in the codebase. A new developer should not be able to tell code apart by contributor. + +Potentially at some-point ESLint rules may be configured to further enforce code style however as it is now, We trust contributors will format their code to match the style in the codebase. + +### ESLint +Eslint should be run on save and before committing. + +### Try & Catch +Functions where any form of complex actions occur should be wrapped in try catch blocks. Errors preferably must be handled. +```ts +const myComplexFunc = async (): myReturnValue => { + try { + logger.info("Describe whats happening"); + + const foo = await callNetworkingFunction("bar", "baz"); + return foo.zoo + } catch (e) { + /** if 'e' is more complex handle different error messages */ + logger.error("Describe what went wrong", e); + } finally { + logger.info("Describe what succeeded"); + } +} +``` + +### Tabs vs Spaces +Tabs, ESLint enforces this. diff --git a/bot/README.md b/bot/README.md index 4226ba4a9..d70b3cec2 100644 --- a/bot/README.md +++ b/bot/README.md @@ -1,25 +1,6 @@ -# Music Linkr +# Tachi-Bot -Automatically translate streaming service links. - -> [Invite the bot](https://discord.com/api/oauth2/authorize?client_id=686462477956808742&permissions=0&scope=applications.commands%20bot) - -> [Join the Server](https://discord.gg/a5a7NQV) - -## Platform Support - -This bot supports every platform the [song.link](https://song.link) platform supports as it takes advantage of its API. - -Currently, it detects the following URL Schemes: - -- Spotify -- Apple Music -- Deezer -- Soundcloud -- Google Music -- Youtube (Prepend links with `?`) - -![Apple Music](https://i.imgur.com/lwFDkUS.png) +Integrate with Tachi Services ## Support & Maintenance diff --git a/bot/package.json b/bot/package.json index 60eb36e7a..09b76960a 100644 --- a/bot/package.json +++ b/bot/package.json @@ -1,8 +1,8 @@ { - "name": "music_linkr", + "name": "tachi-bot", "version": "1.0.0", "main": "src/index.ts", - "license": "MIT", + "license": "AGPL3", "scripts": { "start": "ts-node src/index.ts" }, diff --git a/bot/src/config.ts b/bot/src/config.ts index 99a8ebac9..f0e9df828 100644 --- a/bot/src/config.ts +++ b/bot/src/config.ts @@ -1,4 +1,4 @@ -// noinspection JSUnusedGlobalSymbols +/** @TODO Potentially re-work this to only run on PROD env, handy for DEV env! **/ /** @deprecated replace with database call & cache */ export const allowedChannels = [ "876321394005254214" @@ -6,44 +6,5 @@ export const allowedChannels = [ export enum LoggerLayers { client = "client", - songLink = "songLink", - embedGenerator = "embedGenerator", - deezerMetadata = "deezerMetadata", slashCommands = "slashCommands" } - -export const platformRegex = [ - "open.spotify.com", - "play.google.com/music/m", - "music.apple.com", - "soundcloud.com", - "deezer.com", - "^\\?(https?://)?(www.)?(youtube.com|youtu.?be)/.+$", -]; - -export const platformData: Record = { - spotify: { - emoji: "686481957730779146", - prettyName: "Spotify", - }, - soundcloud: { - emoji: "686481619405766706", - prettyName: "Soundcloud", - }, - google: { - emoji: "686482178464284674", - prettyName: "Google Play", - }, - appleMusic: { - emoji: "686482030120140886", - prettyName: "Apple Music", - }, - youtube: { - emoji: "689044959290196020", - prettyName: "Youtube", - }, - deezer: { - emoji: "699195540180566026", - prettyName: "Deezer", - }, -}; diff --git a/bot/src/createEmbed/createEmbed.ts b/bot/src/createEmbed/createEmbed.ts deleted file mode 100644 index 5638d2c23..000000000 --- a/bot/src/createEmbed/createEmbed.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { EmbedFieldData, MessageActionRow, MessageButton, MessageEmbed, ReplyMessageOptions } from "discord.js"; -import { LoggerLayers, platformData } from "../config"; -import { SongLinkData, SongLinkDataLinks } from "../getSongLink/getSongLink"; -import { createLayeredLogger } from "../utils/logger"; - -const logger = createLayeredLogger(LoggerLayers.embedGenerator); - -const generateEmbedFields = (data: SongLinkData): EmbedFieldData[] => { - const fields = []; - - try { - if (data.metaData.genres) { - fields.push({ - name: "Genres", - value: data.metaData.genres.length ? data.metaData.genres.join(", ") : "Unknown?" - }); - } - - if (data.metaData.releaseYear) { - fields.push({ - name: "Release Date", - value: data.metaData.releaseYear || "Unknown?" - }); - } - } catch (e) { - logger.error("Unable to generate embed fields", e); - } - - return fields; -}; - -const createEmbed = (data: SongLinkData): MessageEmbed => { - logger.info(`Creating embed for ${data.metaData.title}`); - - try { - return new MessageEmbed({ - title: data.metaData.title || "Unknown?", - description: data.metaData.artistName || "Unknown?", - thumbnail: { - url: data.metaData.artwork - }, - fields: generateEmbedFields(data) - }); - } catch (e) { - logger.error("Unable to create embed", e); - } -}; - -type IterableSongLinkDataLinks = Record - -const createPlatformButton = (links: IterableSongLinkDataLinks, item: string) => { - logger.verbose(`Creating button for ${platformData[item].prettyName}`); - - try { - return new MessageButton({ - label: platformData[item].prettyName, - disabled: false, - url: links[item].url, - style: "LINK", - }).setEmoji(platformData[item].emoji); - } catch (e) { - logger.error("Unabe to create platform button", e); - } -}; - -const createComponents = (data: SongLinkData): MessageActionRow[] => { - logger.info(`Creating components for ${data.metaData.title}`); - - try { - const itemsPerRow = 2; - const links: IterableSongLinkDataLinks = data.links; - const objectKeys = Object.keys(data.links).filter(key => platformData[key]); - const rowCount = Math.ceil(objectKeys.length / itemsPerRow); - - const allRows = []; - - for (let row = 0; row < rowCount; row++) { - const buttonRow = new MessageActionRow(); - - const offset = itemsPerRow * row; - const itemsInRow = objectKeys.slice(offset, offset + itemsPerRow); - - buttonRow.addComponents( - itemsInRow.map(item => { - return createPlatformButton(links, item); - })); - - allRows.push(buttonRow); - } - - // Append the "View more" button - const lastRow = allRows.length - 1; - if (allRows[lastRow].components.length < itemsPerRow) { - allRows[lastRow].components.push( - new MessageButton({ - label: "View more", - disabled: false, - url: data.moreUrl, - style: "LINK" - })); - } else { - allRows.push(new MessageActionRow({ - components: [ - new MessageButton({ - label: "View more", - disabled: false, - url: data.moreUrl, - style: "LINK" - }) - ] - })); - } - - return allRows; - } catch (e) { - logger.error("Unable to generate components", e); - } -} -; - -export const createLinkReply = (data: SongLinkData): ReplyMessageOptions => { - logger.info(`Creating message for ${data.metaData.title}`); - - try { - const embed = createEmbed(data); - const components = createComponents(data); - return { - content: " ", - embeds: [embed], - components: components - }; - } catch (e) { - logger.error("Unable to create reply:", e); - } -}; diff --git a/bot/src/getSongLink/getSongLink.test.ts b/bot/src/getSongLink/getSongLink.test.ts deleted file mode 100644 index ed6448275..000000000 --- a/bot/src/getSongLink/getSongLink.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { SonglinkResponse } from "songlink-api/lib/types/Response"; -import { getDetailedMetadata, getIdFromEntity } from "./getSongLink"; - -interface MockSonglinkResponse extends Omit, "entitiesByUniqueId"> { - linksByPlatform: { - deezer?: { - entityUniqueId: string; - url: string; - }; - appleMusic?: { - entityUniqueId: string; - url: string; - }; - } -} - -describe("getIdFromEntity", () => { - test("Should return a valid entity id", () => { - expect(getIdFromEntity("DEEZER::uniqueSnowflake")).toEqual("uniqueSnowflake"); - }); -}); - -const mockSongData: MockSonglinkResponse = { - entityUniqueId: "DEEZER_SONG::17128360", - userCountry: "", - pageUrl: "", - linksByPlatform: { - "deezer": { - entityUniqueId: "DEEZER_SONG::17128360", - url: "", - } - } -}; - -const mockAlbumData: MockSonglinkResponse = { - entityUniqueId: "DEEZER_ALBUM::1602407", - userCountry: "", - pageUrl: "", - linksByPlatform: { - "deezer": { - entityUniqueId: "DEEZER_ALBUM::1602407", - url: "", - } - } -}; - -const nonDeezerMockAlbumData: MockSonglinkResponse = { - entityUniqueId: "ITUNES_ALBUM::1150202719", - userCountry: "", - pageUrl: "", - linksByPlatform: { - "appleMusic": { - entityUniqueId: "ITUNES_ALBUM::1150202719", - url: "", - } - } -}; - -const nonDeezerMockSongData: MockSonglinkResponse = { - entityUniqueId: "ITUNES_SONG::1150202854", - userCountry: "", - pageUrl: "", - linksByPlatform: { - "appleMusic": { - entityUniqueId: "ITUNES_SONG::1150202854", - url: "", - } - } -}; - - -describe("getDetailedMetadata", () => { - test("Should fetch detailed metadata for a song", async () => { - expect(await getDetailedMetadata(mockSongData as SonglinkResponse)) - .toHaveProperty("releaseYear", "2005-09-06"); - }); - test("Should fetch detailed metadata for an album", async () => { - expect(await getDetailedMetadata(mockAlbumData as SonglinkResponse)) - .toHaveProperty("releaseYear", "2005-09-06"); - }); - test("Should fail nicely when fetching a non-deezer album", async () => { - expect(await getDetailedMetadata(nonDeezerMockAlbumData as SonglinkResponse)) - .toEqual(undefined); - }); - test("Should fail nicely when fetching a non-deezer song", async () => { - expect(await getDetailedMetadata(nonDeezerMockSongData as SonglinkResponse)) - .toEqual(undefined); - }); -}); diff --git a/bot/src/getSongLink/getSongLink.ts b/bot/src/getSongLink/getSongLink.ts deleted file mode 100644 index dc327f1b7..000000000 --- a/bot/src/getSongLink/getSongLink.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { getLinks } from "songlink-api"; -import { SonglinkPlatform } from "songlink-api/lib/types/Platform"; -import { SonglinkResponse } from "songlink-api/lib/types/Response"; -import { LoggerLayers } from "../config"; -import { createLayeredLogger } from "../utils/logger"; -import { getDeezerMetadata } from "./metadata/deezer"; - -const logger = createLayeredLogger(LoggerLayers.songLink); - -export interface MetaData { - title?: string; - artistName?: string; - artwork?: string; - genres?: string[] - releaseYear?: string; -} - -export interface SongLinkDataLinks { - entityUniqueId: string; - url: string; - nativeAppUriMobile?: string; - nativeAppUriDesktop?: string; -} - -export interface SongLinkData { - links: { - [P in SonglinkPlatform]: SongLinkDataLinks - } - moreUrl: string; - metaData: MetaData; -} - -export const getIdFromEntity = (entity: string): string => { - try { - return entity.split("::")[1]; - } catch (e) { - logger.error("Unable to getIdFromEntity:", e); - } -}; - -export const getDetailedMetadata = async (data: SonglinkResponse): Promise => { - if (data.linksByPlatform.deezer) { - return await getDeezerMetadata(data); - } else { - logger.warn("Requested metaData but it is not on any supported streaming services"); - } -}; - -export const getSongLinkResponse = async (url: string): Promise => { - logger.info("Fetching songLink"); - - try { - const rawData = await getLinks({ url: url }, { apiKey: process.env.SONGLINKAPIKEY }); - - const firstEntity = rawData.entitiesByUniqueId[Object.keys(rawData.entitiesByUniqueId)[0]]; - const basicMetadata: MetaData = { - title: firstEntity.title, - artistName: firstEntity.artistName, - artwork: firstEntity.thumbnailUrl - }; - - const detailedMetadata = await getDetailedMetadata(rawData); - - return { - links: rawData.linksByPlatform, - moreUrl: rawData.pageUrl, - metaData: { - title: detailedMetadata?.title || basicMetadata.title, - artistName: detailedMetadata?.artistName || basicMetadata.artistName, - artwork: detailedMetadata?.artwork || basicMetadata.artwork, - genres: detailedMetadata?.genres, - releaseYear: detailedMetadata?.releaseYear - } - }; - } catch (e) { - logger.error("Unable to fetch songLink response:", e); - } -}; - diff --git a/bot/src/getSongLink/metadata/deezer.ts b/bot/src/getSongLink/metadata/deezer.ts deleted file mode 100644 index 9ba18db67..000000000 --- a/bot/src/getSongLink/metadata/deezer.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as deezerApi from "deezer-api-ts"; -import { SonglinkResponse } from "songlink-api/lib/types/Response"; -import { LoggerLayers } from "../../config"; -import { createLayeredLogger } from "../../utils/logger"; -import { getIdFromEntity, MetaData } from "../getSongLink"; - -const logger = createLayeredLogger(LoggerLayers.deezerMetadata); - -export const getDeezerMetadata = async (data: SonglinkResponse): Promise => { - try { - logger.info("Fetching metadata from Deezer"); - - const entityId = getIdFromEntity(data.linksByPlatform.deezer.entityUniqueId); - - let metaData; - if (data.linksByPlatform.deezer.entityUniqueId.includes("SONG")) { - const songMetaData = await deezerApi.getTrack(parseInt(entityId)); - let albumMetadata; - if (songMetaData.album.id) { - albumMetadata = await deezerApi.getAlbum(songMetaData.album.id); - } - - metaData = { - title: songMetaData.title, - artist: songMetaData.artist, - artwork: songMetaData.album?.cover || "", - genres: albumMetadata?.genres || undefined, - release_date: albumMetadata?.release_date || undefined - }; - } else { - metaData = await deezerApi.getAlbum(parseInt(entityId)); - } - - - if (!metaData.title) { - logger.warn("Attempted to fetch metadata from deezer but received empty response"); - return; - } - - logger.info(`Successfully fetched metadata for ${metaData.title}`); - - return { - title: metaData.title, - artistName: metaData.artist.name, - artwork: metaData.cover, - genres: metaData.genres?.data.map(genre => genre.name), - releaseYear: metaData.release_date - }; - } catch (e) { - logger.error("Unable to fetch metadata from Deezer:", e); - } -}; diff --git a/bot/src/help/help.ts b/bot/src/help/help.ts index f2151f795..efce3b6ce 100644 --- a/bot/src/help/help.ts +++ b/bot/src/help/help.ts @@ -1,5 +1,5 @@ import { CommandInteraction } from "discord.js"; export const help = async (interaction: CommandInteraction): Promise => { - await interaction.reply("Send a link to a streaming service and I will reply with a rich embed.\nPrepend youtube links with `?`\n\nGithub: https://github.com/Puffycheeses/music_linkr\nDiscord: https://discord.gg/a5a7NQV"); + await interaction.reply("This is an example help command"); }; diff --git a/bot/src/index.ts b/bot/src/index.ts index 06dbbb987..33fcccf59 100644 --- a/bot/src/index.ts +++ b/bot/src/index.ts @@ -1,10 +1,7 @@ import { Client, Intents } from "discord.js"; -import { LoggerLayers, platformRegex } from "./config"; -import { createLinkReply } from "./createEmbed/createEmbed"; -import { getSongLinkResponse } from "./getSongLink/getSongLink"; +import { LoggerLayers } from "./config"; import { registerSlashCommands, slashCommands, tidyGuildCommands, SlashCommand } from "./slashCommands/register"; import { createLayeredLogger } from "./utils/logger"; -import { shouldReply } from "./utils/utils"; const logger = createLayeredLogger(LoggerLayers.client); @@ -16,30 +13,9 @@ const client = new Client({ ] }); -client.on("messageCreate", async (message) => { - try { - if (shouldReply(message)) { - if (new RegExp(platformRegex.join("|")).test(message.content)) { - logger.info("Received valid message"); - const services = message.content.match(/\bhttps?:\/\/\S+/gi); - - for (let i = 0; i < services.length; i++) { - const data = await getSongLinkResponse(services[i]); - await message.channel.send(createLinkReply(data)); - } - - logger.info("Complete \n"); - } - } - } catch (e) { - logger.error("messageCreate Error:", e); - } - - return; -}); - client.on("interactionCreate", async (interaction) => { if (!interaction.isCommand()) return; + try { const command = slashCommands.find((command: SlashCommand) => { return command.info.name === interaction.commandName; diff --git a/bot/src/slashCommands/register.ts b/bot/src/slashCommands/register.ts index 7689cba8f..3b498ce21 100644 --- a/bot/src/slashCommands/register.ts +++ b/bot/src/slashCommands/register.ts @@ -22,7 +22,7 @@ export const slashCommands: SlashCommand[] = [ { info: new SlashCommandBuilder() .setName("help") - .setDescription("Shows information about Music Linkr") + .setDescription("Shows information about this bot") .toJSON(), exec: async (interaction: CommandInteraction) => await help(interaction), } @@ -45,6 +45,7 @@ export const registerSlashCommands = async (client: Client): Promise => { } }; +/** @TODO Potentially re-work this to only run on PROD env, handy for DEV env! **/ /** @deprecated Remove once no guilds have legacy commands */ export const tidyGuildCommands = async (client: Client): Promise => { try { diff --git a/bot/src/utils/utils.ts b/bot/src/utils/utils.ts deleted file mode 100644 index 42b3a415b..000000000 --- a/bot/src/utils/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Message } from "discord.js"; - -export const shouldReply = (message: Message): boolean => - message.author.id !== "876320894295887944" && - !message.author.bot;