From 1c833364568aa71d65be05ed6009e61652cddf57 Mon Sep 17 00:00:00 2001 From: John Ivison Date: Thu, 28 Nov 2024 03:26:11 -0800 Subject: [PATCH] Switch rymimport to use Lilac --- .../NowPlaying/NowPlayingBaseCommand.ts | 1 + .../{Import.ts => ImportRatings.ts} | 85 ++++--------------- .../RateYourMusicParentCommand.ts | 2 +- .../Lastfm/RateYourMusic/connectors.ts | 24 ------ src/errors/lilac.ts | 4 +- src/lib/command/Command.ts | 2 +- src/lib/indexing/MirrorballCommands.ts | 10 +-- src/lib/ui/embeds/SuccessEmbed.ts | 11 ++- src/lib/ui/views/RatingsImportProgressView.ts | 67 +++++++++++++++ src/services/ConcurrencyService.ts | 56 ------------ src/services/ServicesRegistry.ts | 4 +- ...ingService.ts => ErrorReportingService.ts} | 0 src/services/lilac/LilacAPIService.types.ts | 12 +++ src/services/lilac/LilacRatingsService.ts | 39 +++++++++ 14 files changed, 155 insertions(+), 162 deletions(-) rename src/commands/Lastfm/RateYourMusic/{Import.ts => ImportRatings.ts} (57%) delete mode 100644 src/commands/Lastfm/RateYourMusic/connectors.ts create mode 100644 src/lib/ui/views/RatingsImportProgressView.ts delete mode 100644 src/services/ConcurrencyService.ts rename src/services/analytics/{ReportingService.ts => ErrorReportingService.ts} (100%) diff --git a/src/commands/Lastfm/NowPlaying/NowPlayingBaseCommand.ts b/src/commands/Lastfm/NowPlaying/NowPlayingBaseCommand.ts index 3d893ebb..78d620ed 100644 --- a/src/commands/Lastfm/NowPlaying/NowPlayingBaseCommand.ts +++ b/src/commands/Lastfm/NowPlaying/NowPlayingBaseCommand.ts @@ -68,6 +68,7 @@ export abstract class NowPlayingBaseCommand< dbUser, username ); + const renderedComponents = await this.nowPlayingService.renderComponents( this.ctx, await Promise.resolve(this.getConfig(senderUser!)), diff --git a/src/commands/Lastfm/RateYourMusic/Import.ts b/src/commands/Lastfm/RateYourMusic/ImportRatings.ts similarity index 57% rename from src/commands/Lastfm/RateYourMusic/Import.ts rename to src/commands/Lastfm/RateYourMusic/ImportRatings.ts index 1eec57e3..c1e52530 100644 --- a/src/commands/Lastfm/RateYourMusic/Import.ts +++ b/src/commands/Lastfm/RateYourMusic/ImportRatings.ts @@ -3,35 +3,20 @@ import streamToString from "stream-to-string"; import { NoRatingsFileAttatchedError, TooManyAttachmentsError, - UnknownRatingsImportError, WrongFileFormatAttachedError, } from "../../../errors/commands/library"; import { CannotBeUsedAsASlashCommand } from "../../../errors/errors"; -import { AlreadyImportingRatingsError } from "../../../errors/external/rateYourMusic"; import { StringArgument } from "../../../lib/context/arguments/argumentTypes/StringArgument"; import { ArgumentsMap } from "../../../lib/context/arguments/types"; -import { Emoji } from "../../../lib/emoji/Emoji"; -import { SuccessEmbed } from "../../../lib/ui/embeds/SuccessEmbed"; import { WarningEmbed } from "../../../lib/ui/embeds/WarningEmbed"; -import { ConcurrentAction } from "../../../services/ConcurrencyService"; -import { RateYourMusicIndexingChildCommand } from "./RateYourMusicChildCommand"; -import { - ImportRatingsConnector, - ImportRatingsParams, - ImportRatingsResponse, -} from "./connectors"; +import { RatingsImportProgressView } from "../../../lib/ui/views/RatingsImportProgressView"; +import { RateYourMusicChildCommand } from "./RateYourMusicChildCommand"; const args = { input: new StringArgument({ index: { start: 0 }, slashCommandOption: false }), } satisfies ArgumentsMap; -export class ImportRatings extends RateYourMusicIndexingChildCommand< - ImportRatingsResponse, - ImportRatingsParams, - typeof args -> { - connector = new ImportRatingsConnector(); - +export class ImportRatings extends RateYourMusicChildCommand { idSeed = "sonamoo high d"; aliases = ["rymimport", "rymsimport"]; description = @@ -41,22 +26,11 @@ export class ImportRatings extends RateYourMusicIndexingChildCommand< slashCommand = true; - async beforeRun() { - if ( - await this.concurrencyService.isUserDoingAction( - this.author.id, - ConcurrentAction.RYMImport - ) - ) { - throw new AlreadyImportingRatingsError(); - } - } - async run() { if (this.payload.isInteraction()) { await this.reply( new WarningEmbed().setDescription( - "As of right now, you cannot import with slash commands.\n\nPlease go to https://gowon.bot/import-ratings to import, or use message commands" + `As of right now, you cannot import with slash commands.\n\nPlease use the message command \`${this.prefix}rymimport\`` ) ); @@ -70,40 +44,25 @@ export class ImportRatings extends RateYourMusicIndexingChildCommand< const ratings = await this.getRatings(); - if (this.payload.isMessage()) { - this.payload.source.react(Emoji.loading); - } - - this.concurrencyService.registerUser( - this.ctx, - ConcurrentAction.RYMImport, - this.author.id - ); - - let response: ImportRatingsResponse; - - try { - response = await this.query({ - csv: ratings, - user: { discordID: this.author.id }, - }); - this.unregisterConcurrency(); - } catch (e) { - this.unregisterConcurrency(); - throw e; - } + const ratingsProgress = this.lilacRatingsService.importProgress(this.ctx, { + discordID: this.author.id, + }); - const errors = this.parseErrors(response); + const embed = this.minimalEmbed().setDescription(`Preparing import...`); - if (errors) { - throw new UnknownRatingsImportError(); - } + await this.reply(embed); - const embed = new SuccessEmbed().setDescription( - `RateYourMusic ratings imported succesfully! ${Emoji.gowonRated}` + const syncProgressView = new RatingsImportProgressView( + this.ctx, + embed, + ratingsProgress ); - await this.reply(embed); + syncProgressView.subscribeToObservable(); + + await this.lilacRatingsService.import(this.ctx, ratings, { + discordID: this.author.id, + }); } private async getRatings(): Promise { @@ -164,12 +123,4 @@ export class ImportRatings extends RateYourMusicIndexingChildCommand< return ""; } - - private unregisterConcurrency() { - this.concurrencyService.unregisterUser( - this.ctx, - ConcurrentAction.RYMImport, - this.author.id - ); - } } diff --git a/src/commands/Lastfm/RateYourMusic/RateYourMusicParentCommand.ts b/src/commands/Lastfm/RateYourMusic/RateYourMusicParentCommand.ts index ed2f052e..648b7734 100644 --- a/src/commands/Lastfm/RateYourMusic/RateYourMusicParentCommand.ts +++ b/src/commands/Lastfm/RateYourMusic/RateYourMusicParentCommand.ts @@ -2,7 +2,7 @@ import { CommandGroup } from "../../../lib/command/CommandGroup"; import { ParentCommand } from "../../../lib/command/ParentCommand"; import { ArtistRatings } from "./ArtistRatings"; import { Help } from "./Help"; -import { ImportRatings } from "./Import"; +import { ImportRatings } from "./ImportRatings"; import { Link } from "./Link"; import { Rating } from "./Rating"; import { Ratings } from "./Ratings"; diff --git a/src/commands/Lastfm/RateYourMusic/connectors.ts b/src/commands/Lastfm/RateYourMusic/connectors.ts deleted file mode 100644 index ad0a856e..00000000 --- a/src/commands/Lastfm/RateYourMusic/connectors.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { gql } from "apollo-server-express"; -import { BaseConnector } from "../../../lib/indexing/BaseConnector"; -import { UserInput } from "../../../services/mirrorball/MirrorballTypes"; - -// ImportRatings -export interface ImportRatingsResponse { - importRatings: {}; -} - -export interface ImportRatingsParams { - user: UserInput; - csv: string; -} - -export class ImportRatingsConnector extends BaseConnector< - ImportRatingsResponse, - ImportRatingsParams -> { - query = gql` - mutation importRatings($user: UserInput!, $csv: String!) { - importRatings(user: $user, csv: $csv) - } - `; -} diff --git a/src/errors/lilac.ts b/src/errors/lilac.ts index 1fd25898..7051f4dc 100644 --- a/src/errors/lilac.ts +++ b/src/errors/lilac.ts @@ -1,5 +1,5 @@ import { ApolloError, ServerError } from "@apollo/client"; -import { ErrorWithSupernovaID } from "../services/analytics/ReportingService"; +import { ErrorWithSupernovaID } from "../services/analytics/ErrorReportingService"; import { ClientError } from "./errors"; export function parseLilacError(error: Error): Error { @@ -9,7 +9,7 @@ export function parseLilacError(error: Error): Error { error instanceof ApolloError && isServerError(error.networkError) ) { - if (error.networkError.result.supernova_id) { + if (error?.networkError?.result?.supernova_id) { return new LilacError( error.message, error.networkError.result.supernova_id diff --git a/src/lib/command/Command.ts b/src/lib/command/Command.ts index a706cf1c..d956aeb1 100644 --- a/src/lib/command/Command.ts +++ b/src/lib/command/Command.ts @@ -15,7 +15,7 @@ import { GowonService } from "../../services/GowonService"; import { NowPlayingEmbedParsingService } from "../../services/NowPlayingEmbedParsingService"; import { ServiceRegistry } from "../../services/ServicesRegistry"; import { TrackingService } from "../../services/TrackingService"; -import { ErrorReportingService } from "../../services/analytics/ReportingService"; +import { ErrorReportingService } from "../../services/analytics/ErrorReportingService"; import { ArgumentParsingService } from "../../services/arguments/ArgumentsParsingService"; import { MentionsService } from "../../services/arguments/mentions/MentionsService"; import { diff --git a/src/lib/indexing/MirrorballCommands.ts b/src/lib/indexing/MirrorballCommands.ts index a3338762..3b2675bc 100644 --- a/src/lib/indexing/MirrorballCommands.ts +++ b/src/lib/indexing/MirrorballCommands.ts @@ -1,10 +1,9 @@ -import { Command } from "../command/Command"; -import { Connector } from "./BaseConnector"; -import { ArgumentsMap } from "../context/arguments/types"; -import { LastFMService } from "../../services/LastFM/LastFMService"; -import { ConcurrencyService } from "../../services/ConcurrencyService"; import { LastFMArguments } from "../../services/LastFM/LastFMArguments"; +import { LastFMService } from "../../services/LastFM/LastFMService"; import { ServiceRegistry } from "../../services/ServicesRegistry"; +import { Command } from "../command/Command"; +import { ArgumentsMap } from "../context/arguments/types"; +import { Connector } from "./BaseConnector"; export interface ErrorResponse { errors: { message: string }[]; @@ -26,7 +25,6 @@ export abstract class MirrorballBaseCommand< abstract connector: Connector; lastFMService = ServiceRegistry.get(LastFMService); lastFMArguments = ServiceRegistry.get(LastFMArguments); - concurrencyService = ServiceRegistry.get(ConcurrencyService); protected readonly progressBarWidth = 15; diff --git a/src/lib/ui/embeds/SuccessEmbed.ts b/src/lib/ui/embeds/SuccessEmbed.ts index e01339a8..21168c8d 100644 --- a/src/lib/ui/embeds/SuccessEmbed.ts +++ b/src/lib/ui/embeds/SuccessEmbed.ts @@ -4,10 +4,17 @@ import { EmbedView } from "../views/EmbedView"; export const successColour = "#02BCA1"; export class SuccessEmbed extends EmbedView { - asDiscordSendable(): EmbedView { + private successEmoji: string = Emoji.checkmark; + + public asDiscordSendable(): EmbedView { return super .asDiscordSendable() .setColour(successColour) - .setDescription(`${Emoji.checkmark} ${this.getDescription()}`); + .setDescription(`${this.successEmoji} ${this.getDescription()}`); + } + + public setSuccessEmoji(emoji: string): this { + this.successEmoji = emoji; + return this; } } diff --git a/src/lib/ui/views/RatingsImportProgressView.ts b/src/lib/ui/views/RatingsImportProgressView.ts new file mode 100644 index 00000000..12671205 --- /dev/null +++ b/src/lib/ui/views/RatingsImportProgressView.ts @@ -0,0 +1,67 @@ +import { Observable, ObservableSubscription } from "@apollo/client"; +import { italic } from "../../../helpers/discord"; +import { + LilacRatingsImportStage, + RatingsImportProgress, +} from "../../../services/lilac/LilacAPIService.types"; +import { GowonContext } from "../../context/Context"; +import { Emoji } from "../../emoji/Emoji"; +import { displayNumber } from "../displays"; +import { ErrorEmbed } from "../embeds/ErrorEmbed"; +import { SuccessEmbed } from "../embeds/SuccessEmbed"; +import { EmbedView } from "./EmbedView"; +import { UnsendableView } from "./View"; + +export class RatingsImportProgressView extends UnsendableView { + private subscription: ObservableSubscription | undefined; + + constructor( + private ctx: GowonContext, + private embed: EmbedView, + private observable: Observable + ) { + super(); + } + + public subscribeToObservable(): this { + this.subscription = this.observable.subscribe(async (progress) => { + await this.handleProgress(progress); + }); + + return this; + } + + private async handleProgress(progress: RatingsImportProgress) { + if (progress.stage === LilacRatingsImportStage.Started) { + this.embed + .setDescription( + `${Emoji.loading} Starting import of ${displayNumber( + progress.count, + "rating" + )}...` + ) + .editMessage(this.ctx); + } else if (progress.stage === LilacRatingsImportStage.Finished) { + this.embed + .convert(SuccessEmbed) + .setSuccessEmoji(Emoji.gowonRated) + .setDescription( + `Successfully imported ${displayNumber(progress.count, "rating")}!` + ) + .editMessage(this.ctx); + + this.subscription?.unsubscribe(); + } else if (progress.stage === LilacRatingsImportStage.Errored) { + this.embed + .convert(ErrorEmbed) + .setDescription( + `Something went wrong importing your ratings:\n\n${italic( + progress.error + )}` + ) + .editMessage(this.ctx); + + this.subscription?.unsubscribe(); + } + } +} diff --git a/src/services/ConcurrencyService.ts b/src/services/ConcurrencyService.ts deleted file mode 100644 index d579d25e..00000000 --- a/src/services/ConcurrencyService.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { GowonContext } from "../lib/context/Context"; -import { BaseService } from "./BaseService"; - -export enum ConcurrentAction { - Indexing = "Indexing", - Updating = "Updating", - RYMImport = "RYMImport", - Minting = "Minting", -} - -interface ConcurrencyCache { - [action: string]: Set; -} - -export class ConcurrencyService extends BaseService { - readonly defaultTimeout = 10 * 60 * 60; - - cache: ConcurrencyCache = {}; - - constructor() { - super(); - for (const action of Object.values(ConcurrentAction)) { - this.cache[action] = new Set(); - } - } - - registerUser(ctx: GowonContext, action: ConcurrentAction, discordID: string) { - this.log(ctx, `Registering user ${discordID} as doing ${action}`); - this.cache[action].add(discordID); - this.makeEphemeral(action, discordID); - } - - unregisterUser( - ctx: GowonContext, - action: ConcurrentAction, - discordID: string - ) { - this.log(ctx, `Unregistering user ${discordID} as doing ${action}`); - this.cache[action].delete(discordID); - } - - async isUserDoingAction( - discordID: string, - ...actions: ConcurrentAction[] - ): Promise { - return actions.some((action) => { - return this.cache[action].has(discordID); - }); - } - - private makeEphemeral(action: ConcurrentAction, discordID: string) { - setTimeout(() => { - this.cache[action].delete(discordID); - }, this.defaultTimeout); - } -} diff --git a/src/services/ServicesRegistry.ts b/src/services/ServicesRegistry.ts index d76053a1..32bebf3b 100644 --- a/src/services/ServicesRegistry.ts +++ b/src/services/ServicesRegistry.ts @@ -4,7 +4,6 @@ import { PermissionsCacheService } from "../lib/permissions/PermissionsCacheServ import { PermissionsService } from "../lib/permissions/PermissionsService"; import { SettingsService } from "../lib/settings/SettingsService"; import { BaseService } from "./BaseService"; -import { ConcurrencyService } from "./ConcurrencyService"; import { DiscordService } from "./Discord/DiscordService"; import { EmojiService } from "./Discord/EmojiService"; import { GuildEventService } from "./Discord/GuildEventService"; @@ -23,7 +22,7 @@ import { SpotifyService } from "./Spotify/SpotifyService"; import { TimeAndDateService } from "./TimeAndDateService"; import { TrackingService } from "./TrackingService"; import { WordBlacklistService } from "./WordBlacklistService"; -import { ErrorReportingService } from "./analytics/ReportingService"; +import { ErrorReportingService } from "./analytics/ErrorReportingService"; import { ArgumentParsingService } from "./arguments/ArgumentsParsingService"; import { MentionsService } from "./arguments/mentions/MentionsService"; import { BotStatsService } from "./dbservices/BotStatsService"; @@ -70,7 +69,6 @@ const services: Service[] = [ CrownsHistoryService, CrownsUserService, ComboService, - ConcurrencyService, DatasourceService, DiscordService, EmojiService, diff --git a/src/services/analytics/ReportingService.ts b/src/services/analytics/ErrorReportingService.ts similarity index 100% rename from src/services/analytics/ReportingService.ts rename to src/services/analytics/ErrorReportingService.ts diff --git a/src/services/lilac/LilacAPIService.types.ts b/src/services/lilac/LilacAPIService.types.ts index e9c99b9c..a1c7bb76 100644 --- a/src/services/lilac/LilacAPIService.types.ts +++ b/src/services/lilac/LilacAPIService.types.ts @@ -140,6 +140,18 @@ export interface SyncProgress< total: number; } +export enum LilacRatingsImportStage { + Started = "started", + Finished = "finished", + Errored = "errored", +} + +export interface RatingsImportProgress { + stage: LilacRatingsImportStage; + count?: number; + error?: string; +} + export interface LilacPagination { currentPage: number; perPage: number; diff --git a/src/services/lilac/LilacRatingsService.ts b/src/services/lilac/LilacRatingsService.ts index 7ed42162..8dd22cd8 100644 --- a/src/services/lilac/LilacRatingsService.ts +++ b/src/services/lilac/LilacRatingsService.ts @@ -1,3 +1,4 @@ +import { Observable } from "@apollo/client"; import { gql } from "apollo-server-express"; import { GowonContext } from "../../lib/context/Context"; import { LilacAPIService } from "./LilacAPIService"; @@ -6,7 +7,9 @@ import { LilacRatingsFilters, LilacRatingsPage, LilacUserInput, + RatingsImportProgress, } from "./LilacAPIService.types"; +import { userToUserInput } from "./helpers"; export class LilacRatingsService extends LilacAPIService { async ratings( @@ -114,4 +117,40 @@ export class LilacRatingsService extends LilacAPIService { return response; } + + public async import( + ctx: GowonContext, + csv: string, + user: LilacUserInput + ): Promise { + const mutation = gql` + mutation importRatings($ratingsCsv: String!, $user: UserInput!) { + importRatings(ratingsCsv: $ratingsCsv, user: $user) + } + `; + + await this.mutate(ctx, mutation, { ratingsCsv: csv, user }); + } + + public importProgress( + ctx: GowonContext, + user: LilacUserInput + ): Observable { + const subscription = gql` + subscription RatingsImportProgress($user: UserInput!) { + ratingsImport(user: $user) { + stage + count + error + } + } + `; + + return this.subscribe< + { ratingsImport: RatingsImportProgress }, + { user: LilacUserInput } + >(ctx, subscription, { user: userToUserInput(user) }).map( + (data) => data.ratingsImport + ); + } }