From 000b1f4fe246a0090b15dfbeb6ebe982c41d2869 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 31 Mar 2025 14:29:48 -0400 Subject: [PATCH] fix type errors from SponsorsService --- packages/backend/src/core/SponsorsService.ts | 57 +++++++++++++------ .../src/server/api/endpoints/sponsors.ts | 53 +++++++++++++++-- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/packages/backend/src/core/SponsorsService.ts b/packages/backend/src/core/SponsorsService.ts index 77dd6f81a4..020c4ab3c6 100644 --- a/packages/backend/src/core/SponsorsService.ts +++ b/packages/backend/src/core/SponsorsService.ts @@ -4,24 +4,47 @@ */ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import type { MiMeta } from '@/models/_.js'; import * as Redis from 'ioredis'; +import type { MiMeta } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { RedisKVCache } from '@/misc/cache.js'; import { bindThis } from '@/decorators.js'; +export interface Sponsor { + MemberId: number; + createdAt: string; + type: string; + role: string; + tier: string; + isActive: boolean; + totalAmountDonated: number; + currency: string; + lastTransactionAt: string; + lastTransactionAmount: number; + profile: string; + name: string; + company: string | null; + description: string | null; + image: string | null; + email: string | null; + newsletterOptIn: unknown | null; + twitter: string | null; + github: string | null; + website: string | null; +} + @Injectable() export class SponsorsService implements OnApplicationShutdown { - private cache: RedisKVCache; + private readonly cache: RedisKVCache; constructor( @Inject(DI.meta) - private meta: MiMeta, + private readonly meta: MiMeta, @Inject(DI.redis) - private redisClient: Redis.Redis, + redisClient: Redis.Redis, ) { - this.cache = new RedisKVCache(this.redisClient, 'sponsors', { + this.cache = new RedisKVCache(redisClient, 'sponsors', { lifetime: 1000 * 60 * 60, memoryCacheLifetime: 1000 * 60, fetcher: (key) => { @@ -34,54 +57,54 @@ export class SponsorsService implements OnApplicationShutdown { } @bindThis - private async fetchInstanceSponsors() { + private async fetchInstanceSponsors(): Promise { if (!(this.meta.donationUrl && this.meta.donationUrl.includes('opencollective.com'))) { return []; } try { - const backers = await fetch(`${this.meta.donationUrl}/members/users.json`).then((response) => response.json()); + const backers: Sponsor[] = await fetch(`${this.meta.donationUrl}/members/users.json`).then((response) => response.json()); // Merge both together into one array and make sure it only has Active subscriptions - const allSponsors = [...backers].filter(sponsor => sponsor.isActive === true && sponsor.role === 'BACKER' && sponsor.tier); + const allSponsors = [...backers].filter(sponsor => sponsor.isActive && sponsor.role === 'BACKER' && sponsor.tier); // Remove possible duplicates return [...new Map(allSponsors.map(v => [v.profile, v])).values()]; - } catch (error) { + } catch { return []; } } @bindThis - private async fetchSharkeySponsors() { + private async fetchSharkeySponsors(): Promise { try { - const backers = await fetch('https://opencollective.com/sharkey/tiers/backer/all.json').then((response) => response.json()); - const sponsorsOC = await fetch('https://opencollective.com/sharkey/tiers/sponsor/all.json').then((response) => response.json()); + const backers: Sponsor[] = await fetch('https://opencollective.com/sharkey/tiers/backer/all.json').then((response) => response.json()); + const sponsorsOC: Sponsor[] = await fetch('https://opencollective.com/sharkey/tiers/sponsor/all.json').then((response) => response.json()); // Merge both together into one array and make sure it only has Active subscriptions - const allSponsors = [...sponsorsOC, ...backers].filter(sponsor => sponsor.isActive === true); + const allSponsors = [...sponsorsOC, ...backers].filter(sponsor => sponsor.isActive); // Remove possible duplicates return [...new Map(allSponsors.map(v => [v.profile, v])).values()]; - } catch (error) { + } catch { return []; } } @bindThis public async instanceSponsors(forceUpdate: boolean) { - if (forceUpdate) this.cache.refresh('instance'); + if (forceUpdate) await this.cache.refresh('instance'); return this.cache.fetch('instance'); } @bindThis public async sharkeySponsors(forceUpdate: boolean) { - if (forceUpdate) this.cache.refresh('sharkey'); + if (forceUpdate) await this.cache.refresh('sharkey'); return this.cache.fetch('sharkey'); } @bindThis - public onApplicationShutdown(signal?: string | undefined): void { + public onApplicationShutdown(): void { this.cache.dispose(); } } diff --git a/packages/backend/src/server/api/endpoints/sponsors.ts b/packages/backend/src/server/api/endpoints/sponsors.ts index 401d9292bc..8e1ac749d5 100644 --- a/packages/backend/src/server/api/endpoints/sponsors.ts +++ b/packages/backend/src/server/api/endpoints/sponsors.ts @@ -14,6 +14,39 @@ export const meta = { requireCredential: false, requireCredentialPrivateMode: false, + res: { + type: 'object', + nullable: false, optional: false, + properties: { + sponsor_data: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'object', + nullable: false, optional: false, + properties: { + name: { + type: 'string', + nullable: false, optional: false, + }, + image: { + type: 'string', + nullable: true, optional: false, + }, + website: { + type: 'string', + nullable: true, optional: false, + }, + profile: { + type: 'string', + nullable: false, optional: false, + }, + }, + }, + }, + }, + }, + // 2 calls per second limit: { duration: 1000, @@ -24,6 +57,7 @@ export const meta = { export const paramDef = { type: 'object', properties: { + // TODO remove this or make staff-only to prevent DoS forceUpdate: { type: 'boolean', default: false }, instance: { type: 'boolean', default: false }, }, @@ -35,12 +69,19 @@ export default class extends Endpoint { // eslint- constructor( private sponsorsService: SponsorsService, ) { - super(meta, paramDef, async (ps, me) => { - if (ps.instance) { - return { sponsor_data: await this.sponsorsService.instanceSponsors(ps.forceUpdate) }; - } else { - return { sponsor_data: await this.sponsorsService.sharkeySponsors(ps.forceUpdate) }; - } + super(meta, paramDef, async (ps) => { + const sponsors = ps.instance + ? await this.sponsorsService.instanceSponsors(ps.forceUpdate) + : await this.sponsorsService.sharkeySponsors(ps.forceUpdate); + + return { + sponsor_data: sponsors.map(s => ({ + name: s.name, + image: s.image, + website: s.website, + profile: s.profile, + })), + }; }); } }