From e87b93f19ca74148aa5b80563e65e50870d75d75 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 12 Dec 2022 20:13:45 +0100 Subject: [PATCH] Feature/add logo endpoint (#1506) * Add logo endpoint * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/app.module.ts | 2 + .../src/app/benchmark/benchmark.controller.ts | 2 +- apps/api/src/app/logo/logo.controller.ts | 54 ++++++++++++++++++ apps/api/src/app/logo/logo.module.ts | 13 +++++ apps/api/src/app/logo/logo.service.ts | 55 +++++++++++++++++++ .../src/app/portfolio/portfolio.controller.ts | 2 + .../positions-table.component.html | 4 +- .../symbol-icon/symbol-icon.component.html | 5 +- .../symbol-icon/symbol-icon.component.ts | 17 +++++- .../portfolio-public-details.interface.ts | 1 + 11 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 apps/api/src/app/logo/logo.controller.ts create mode 100644 apps/api/src/app/logo/logo.module.ts create mode 100644 apps/api/src/app/logo/logo.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index acdd804c..74fd4c60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added the date of the first activity to the positions table +- Added an endpoint to fetch the logo of an asset or a platform ### Changed diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 47c19991..9f8eb28b 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -27,6 +27,7 @@ import { ExportModule } from './export/export.module'; import { FrontendMiddleware } from './frontend.middleware'; import { ImportModule } from './import/import.module'; import { InfoModule } from './info/info.module'; +import { LogoModule } from './logo/logo.module'; import { OrderModule } from './order/order.module'; import { PortfolioModule } from './portfolio/portfolio.module'; import { SubscriptionModule } from './subscription/subscription.module'; @@ -58,6 +59,7 @@ import { UserModule } from './user/user.module'; ExportModule, ImportModule, InfoModule, + LogoModule, OrderModule, PortfolioModule, PrismaModule, diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/benchmark/benchmark.controller.ts index b5562bf8..4daa1400 100644 --- a/apps/api/src/app/benchmark/benchmark.controller.ts +++ b/apps/api/src/app/benchmark/benchmark.controller.ts @@ -30,8 +30,8 @@ export class BenchmarkController { } @Get(':dataSource/:symbol/:startDateString') - @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) public async getBenchmarkMarketDataBySymbol( @Param('dataSource') dataSource: DataSource, @Param('startDateString') startDateString: string, diff --git a/apps/api/src/app/logo/logo.controller.ts b/apps/api/src/app/logo/logo.controller.ts new file mode 100644 index 00000000..22bafc06 --- /dev/null +++ b/apps/api/src/app/logo/logo.controller.ts @@ -0,0 +1,54 @@ +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; +import { + Controller, + Get, + HttpStatus, + Param, + Query, + Res, + UseInterceptors +} from '@nestjs/common'; +import { DataSource } from '@prisma/client'; +import { Response } from 'express'; + +import { LogoService } from './logo.service'; + +@Controller('logo') +export class LogoController { + public constructor(private readonly logoService: LogoService) {} + + @Get(':dataSource/:symbol') + @UseInterceptors(TransformDataSourceInRequestInterceptor) + public async getLogoByDataSourceAndSymbol( + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string, + @Res() response: Response + ) { + try { + const buffer = await this.logoService.getLogoByDataSourceAndSymbol({ + dataSource, + symbol + }); + + response.contentType('image/png'); + response.send(buffer); + } catch { + response.status(HttpStatus.NOT_FOUND).send(); + } + } + + @Get() + public async getLogoByUrl( + @Query('url') url: string, + @Res() response: Response + ) { + try { + const buffer = await this.logoService.getLogoByUrl(url); + + response.contentType('image/png'); + response.send(buffer); + } catch { + response.status(HttpStatus.NOT_FOUND).send(); + } + } +} diff --git a/apps/api/src/app/logo/logo.module.ts b/apps/api/src/app/logo/logo.module.ts new file mode 100644 index 00000000..518010f3 --- /dev/null +++ b/apps/api/src/app/logo/logo.module.ts @@ -0,0 +1,13 @@ +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; +import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module'; +import { Module } from '@nestjs/common'; + +import { LogoController } from './logo.controller'; +import { LogoService } from './logo.service'; + +@Module({ + controllers: [LogoController], + imports: [ConfigurationModule, SymbolProfileModule], + providers: [LogoService] +}) +export class LogoModule {} diff --git a/apps/api/src/app/logo/logo.service.ts b/apps/api/src/app/logo/logo.service.ts new file mode 100644 index 00000000..3bea44ca --- /dev/null +++ b/apps/api/src/app/logo/logo.service.ts @@ -0,0 +1,55 @@ +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { HttpException, Injectable } from '@nestjs/common'; +import { DataSource } from '@prisma/client'; +import * as bent from 'bent'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; + +@Injectable() +export class LogoService { + public constructor( + private readonly symbolProfileService: SymbolProfileService + ) {} + + public async getLogoByDataSourceAndSymbol({ + dataSource, + symbol + }: UniqueAsset) { + if (!DataSource[dataSource]) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ + { dataSource, symbol } + ]); + + if (!assetProfile) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + return this.getBuffer(assetProfile.url); + } + + public async getLogoByUrl(aUrl: string) { + return this.getBuffer(aUrl); + } + + private getBuffer(aUrl: string) { + const get = bent( + `https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`, + 'GET', + 'buffer', + 200, + { + 'User-Agent': 'request' + } + ); + return get(); + } +} diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 46f55564..49ba3c84 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -373,6 +373,7 @@ export class PortfolioController { } @Get('public/:accessId') + @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPublic( @Param('accessId') accessId ): Promise { @@ -422,6 +423,7 @@ export class PortfolioController { allocationCurrent: portfolioPosition.value / totalValue, countries: hasDetails ? portfolioPosition.countries : [], currency: hasDetails ? portfolioPosition.currency : undefined, + dataSource: portfolioPosition.dataSource, dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, markets: hasDetails ? portfolioPosition.markets : undefined, name: portfolioPosition.name, diff --git a/apps/client/src/app/components/positions-table/positions-table.component.html b/apps/client/src/app/components/positions-table/positions-table.component.html index cb72cddb..e981de53 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.html +++ b/apps/client/src/app/components/positions-table/positions-table.component.html @@ -10,9 +10,9 @@ diff --git a/apps/client/src/app/components/symbol-icon/symbol-icon.component.html b/apps/client/src/app/components/symbol-icon/symbol-icon.component.html index a544ddef..0aebd0e5 100644 --- a/apps/client/src/app/components/symbol-icon/symbol-icon.component.html +++ b/apps/client/src/app/components/symbol-icon/symbol-icon.component.html @@ -1,6 +1,7 @@ diff --git a/apps/client/src/app/components/symbol-icon/symbol-icon.component.ts b/apps/client/src/app/components/symbol-icon/symbol-icon.component.ts index 4a281732..a6fa0901 100644 --- a/apps/client/src/app/components/symbol-icon/symbol-icon.component.ts +++ b/apps/client/src/app/components/symbol-icon/symbol-icon.component.ts @@ -2,8 +2,9 @@ import { ChangeDetectionStrategy, Component, Input, - OnInit + OnChanges } from '@angular/core'; +import { DataSource } from '@prisma/client'; @Component({ selector: 'gf-symbol-icon', @@ -11,12 +12,22 @@ import { templateUrl: './symbol-icon.component.html', styleUrls: ['./symbol-icon.component.scss'] }) -export class SymbolIconComponent implements OnInit { +export class SymbolIconComponent implements OnChanges { + @Input() dataSource: DataSource; @Input() size: 'large'; + @Input() symbol: string; @Input() tooltip: string; @Input() url: string; + public src: string; + public constructor() {} - public ngOnInit() {} + public ngOnChanges() { + if (this.dataSource && this.symbol) { + this.src = `../api/v1/logo/${this.dataSource}/${this.symbol}`; + } else if (this.url) { + this.src = `../api/v1/logo?url=${this.url}`; + } + } } diff --git a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts index 6fd63adb..52dddae3 100644 --- a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts @@ -9,6 +9,7 @@ export interface PortfolioPublicDetails { | 'allocationCurrent' | 'countries' | 'currency' + | 'dataSource' | 'dateOfFirstActivity' | 'markets' | 'name'