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/Import.ts deleted file mode 100644 index 1eec57e3..00000000 --- a/src/commands/Lastfm/RateYourMusic/Import.ts +++ /dev/null @@ -1,175 +0,0 @@ -import fetch from "node-fetch"; -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"; - -const args = { - input: new StringArgument({ index: { start: 0 }, slashCommandOption: false }), -} satisfies ArgumentsMap; - -export class ImportRatings extends RateYourMusicIndexingChildCommand< - ImportRatingsResponse, - ImportRatingsParams, - typeof args -> { - connector = new ImportRatingsConnector(); - - idSeed = "sonamoo high d"; - aliases = ["rymimport", "rymsimport"]; - description = - "Import your rateyourmusic ratings. See ryms help for more info on how to import"; - - arguments = args; - - 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" - ) - ); - - return; - } - - await this.getMentions({ - senderRequired: true, - syncedRequired: true, - }); - - 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 errors = this.parseErrors(response); - - if (errors) { - throw new UnknownRatingsImportError(); - } - - const embed = new SuccessEmbed().setDescription( - `RateYourMusic ratings imported succesfully! ${Emoji.gowonRated}` - ); - - await this.reply(embed); - } - - private async getRatings(): Promise { - let ratings: string; - - if (!this.parsedArguments.input) { - ratings = await this.getRatingsFromAttached(); - } else { - ratings = this.getRatingsFromContent(); - } - - ratings = ratings.trim(); - - if (!ratings.startsWith("RYM Album,")) { - throw new WrongFileFormatAttachedError(); - } - - return ratings; - } - - private async getRatingsFromAttached(): Promise { - if (this.payload.isInteraction()) { - throw new CannotBeUsedAsASlashCommand(); - } else if (this.payload.isMessage()) { - const attachments = this.payload.source.attachments; - - if (attachments.size > 1) { - throw new TooManyAttachmentsError(); - } - - const attachment = attachments.first(); - - if (!attachment) { - throw new NoRatingsFileAttatchedError(this.prefix); - } - - const file = await fetch(attachment.url); - - const fileContent = await streamToString(file.body); - - return fileContent; - } - - return ""; - } - - private getRatingsFromContent(): string { - if (this.payload.isInteraction()) { - throw new CannotBeUsedAsASlashCommand(); - } else if (this.payload.isMessage()) { - const rawMessageContent = this.gowonService.removeCommandName( - this.ctx, - this.payload.source.content - ); - - return rawMessageContent; - } - - return ""; - } - - private unregisterConcurrency() { - this.concurrencyService.unregisterUser( - this.ctx, - ConcurrentAction.RYMImport, - this.author.id - ); - } -} diff --git a/src/commands/Lastfm/RateYourMusic/ImportRatings.ts b/src/commands/Lastfm/RateYourMusic/ImportRatings.ts new file mode 100644 index 00000000..957fd7f0 --- /dev/null +++ b/src/commands/Lastfm/RateYourMusic/ImportRatings.ts @@ -0,0 +1,73 @@ +import fetch from "node-fetch"; +import streamToString from "stream-to-string"; +import { WrongFileFormatAttachedError } from "../../../errors/commands/library"; +import { AttachmentArgument } from "../../../lib/context/arguments/argumentTypes/discord/AttachmentArgument"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { RatingsImportProgressView } from "../../../lib/ui/views/RatingsImportProgressView"; +import { RateYourMusicChildCommand } from "./RateYourMusicChildCommand"; + +const args = { + ratings: new AttachmentArgument({ + index: 0, + description: "The file containing your RateYourMusic ratings", + required: { + customMessage: `Please attach your ratings! (See \`rym help\` for more info)`, + }, + }), +} satisfies ArgumentsMap; + +export class ImportRatings extends RateYourMusicChildCommand { + idSeed = "sonamoo high d"; + aliases = ["rymimport", "rymsimport"]; + description = + "Import your RateYourMusic ratings. See rym help for more info on how to import"; + + arguments = args; + + slashCommand = true; + + async run() { + await this.getMentions({ + senderRequired: true, + syncedRequired: true, + }); + + const ratings = await this.getRatings(); + + const ratingsProgress = this.lilacRatingsService.importProgress(this.ctx, { + discordID: this.author.id, + }); + + const embed = this.minimalEmbed().setDescription(`Preparing import...`); + + await this.reply(embed); + + const syncProgressView = new RatingsImportProgressView( + this.ctx, + embed, + ratingsProgress + ); + + syncProgressView.subscribeToObservable(); + + await this.lilacRatingsService.import(this.ctx, ratings, { + discordID: this.author.id, + }); + } + + private async getRatings(): Promise { + const ratingsAttachment = this.parsedArguments.ratings; + + const file = await fetch(ratingsAttachment.url); + + const fileContent = await streamToString(file.body); + + const ratings = fileContent.trim(); + + if (!ratings.startsWith("RYM Album,")) { + throw new WrongFileFormatAttachedError(); + } + + return ratings; + } +} 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/commands/library.ts b/src/errors/commands/library.ts index 547bbf17..b340d454 100644 --- a/src/errors/commands/library.ts +++ b/src/errors/commands/library.ts @@ -70,34 +70,12 @@ export class TooManySearchResultsError extends ClientError { } } -export class UnknownRatingsImportError extends ClientError { - constructor() { - super("Something went wrong when importing your ratings"); - } -} - export class WrongFileFormatAttachedError extends ClientError { constructor() { super("Please attach a file with the correct format"); } } -export class TooManyAttachmentsError extends ClientError { - constructor() { - super( - "Too many attachments! Please attach only one file with your ratings" - ); - } -} - -export class NoRatingsFileAttatchedError extends ClientError { - constructor(prefix: string) { - super( - `Please attach your ratings! (See \`${prefix}rym help\` for more info)` - ); - } -} - export class CouldNotFindRatingError extends ClientError { constructor() { super("Couldn't find that album in your ratings!"); 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/context/arguments/argumentTypes/SlashCommandTypes.ts b/src/lib/context/arguments/argumentTypes/SlashCommandTypes.ts index 1f4a4816..9d59f8d0 100644 --- a/src/lib/context/arguments/argumentTypes/SlashCommandTypes.ts +++ b/src/lib/context/arguments/argumentTypes/SlashCommandTypes.ts @@ -1,27 +1,28 @@ import { SlashCommandBuilder as _SlashCommandBuilder, - SlashCommandRoleOption, - SlashCommandUserOption, - SlashCommandNumberOption, - SlashCommandStringOption, + SlashCommandAttachmentOption, SlashCommandBooleanOption, SlashCommandChannelOption, SlashCommandIntegerOption, SlashCommandMentionableOption, + SlashCommandNumberOption, + SlashCommandRoleOption, + SlashCommandStringOption, + SlashCommandUserOption, } from "@discordjs/builders"; // Discord.js doesn't export any interfaces to help deal with building slash commands // so I created them myself export { - SlashCommandRoleOption, - SlashCommandUserOption, - SlashCommandNumberOption, - SlashCommandStringOption, SlashCommandBooleanOption, SlashCommandChannelOption, SlashCommandIntegerOption, SlashCommandMentionableOption, + SlashCommandNumberOption, + SlashCommandRoleOption, + SlashCommandStringOption, + SlashCommandUserOption, }; // Any type of slash command option @@ -33,7 +34,8 @@ export type SlashCommandOption = | SlashCommandBooleanOption | SlashCommandChannelOption | SlashCommandIntegerOption - | SlashCommandMentionableOption; + | SlashCommandMentionableOption + | SlashCommandAttachmentOption; export type SlashCommandBuilder = _SlashCommandBuilder; export type SlashCommandBuilderReturn = diff --git a/src/lib/context/arguments/argumentTypes/discord/AttachmentArgument.ts b/src/lib/context/arguments/argumentTypes/discord/AttachmentArgument.ts new file mode 100644 index 00000000..dbb9daa1 --- /dev/null +++ b/src/lib/context/arguments/argumentTypes/discord/AttachmentArgument.ts @@ -0,0 +1,41 @@ +import { CommandInteraction, Message, MessageAttachment } from "discord.js"; +import { GowonContext } from "../../../Context"; +import { + BaseArgument, + BaseArgumentOptions, + IndexableArgumentOptions, + defaultIndexableOptions, +} from "../BaseArgument"; +import { SlashCommandBuilder } from "../SlashCommandTypes"; + +export interface AttachmentArgumentOptions + extends BaseArgumentOptions, + IndexableArgumentOptions {} + +export class AttachmentArgument< + OptionsT extends Partial +> extends BaseArgument { + constructor(options?: OptionsT) { + super({ ...defaultIndexableOptions, ...(options ?? {}) } as OptionsT); + } + + parseFromMessage(message: Message, _: string): MessageAttachment | undefined { + const attachments = Array.from(message.attachments.values()); + + return this.getElementFromIndex(attachments, this.options.index); + } + + parseFromInteraction( + interaction: CommandInteraction, + _: GowonContext, + argumentName: string + ): MessageAttachment | undefined { + return interaction.options.getAttachment(argumentName) ?? undefined; + } + + addAsOption(slashCommand: SlashCommandBuilder, argumentName: string) { + return slashCommand.addAttachmentOption((option) => + this.baseOption(option, argumentName) + ); + } +} diff --git a/src/lib/context/arguments/types.ts b/src/lib/context/arguments/types.ts index bb410915..7f80d562 100644 --- a/src/lib/context/arguments/types.ts +++ b/src/lib/context/arguments/types.ts @@ -4,6 +4,7 @@ import { Flag } from "./argumentTypes/Flag"; import { NumberArgument } from "./argumentTypes/NumberArgument"; import { StringArgument } from "./argumentTypes/StringArgument"; import { StringArrayArgument } from "./argumentTypes/StringArrayArgument"; +import { AttachmentArgument } from "./argumentTypes/discord/AttachmentArgument"; import { ChannelArgument } from "./argumentTypes/discord/ChannelArgument"; import { DiscordRoleArgument } from "./argumentTypes/discord/DiscordRoleArgument"; import { EmojisArgument } from "./argumentTypes/discord/EmojisArgument"; @@ -27,7 +28,8 @@ export type ImplementedOptions> = | EmojisArgument | DateArgument | ChannelArgument - | DiscordRoleArgument; + | DiscordRoleArgument + | AttachmentArgument; export type UnwrapProvidedOptions> = T extends ImplementedOptions ? U : {}; 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 + ); + } }