11import grabFreeGamesGiveawaySite from './site-fetchers/grab-free-games' ;
2- import grabFreeGamesSteamGiveawaySite from './site-fetchers/grab-free-games-steam.js ' ;
2+ import grabFreeGamesSteamGiveawaySite from './site-fetchers/grab-free-games-steam' ;
33import { TextBasedChannel } from 'discord.js' ;
44import { BaseGiveawaySiteFetcher } from './site-fetchers/base' ;
55import { Giveaway } from './giveaway' ;
66import { GiveawayStatus , GiveawayStatusEnum } from './giveaway-status' ;
77import { DB } from '@/database/database' ;
88import { getChannelParentID } from '#lib/discord-fetch' ;
9- import { LogLevel } from '@sapphire/framework' ;
9+ import { BaseService } from '../base-service' ;
10+ import { memoryStore } from 'cache-manager' ;
11+ import { CacheWrapper } from '@/utilities/cache-wrapper' ;
1012
1113type TAvailableSite = 'GrabFreeGames' | 'GrabFreeGamesSteam' ;
1214type TSiteParsers = Record < TAvailableSite , BaseGiveawaySiteFetcher > ;
@@ -15,44 +17,54 @@ const giveawayFetchers: TSiteParsers = {
1517 GrabFreeGamesSteam : grabFreeGamesSteamGiveawaySite ,
1618} ;
1719
18- export class GiveawayService {
20+ interface CacheMap {
21+ giveaways : Giveaway [ ] ;
22+ }
23+ const cache = new CacheWrapper < CacheMap > ( memoryStore ( ) , {
24+ max : 2 ,
25+ ttl : 900_000 ,
26+ } ) ;
27+
28+ /**
29+ * Giveaway service for fetching, filtering, sending giveaways
30+ *
31+ * Will use cache on construction, to fetch
32+ */
33+ export class GiveawayService extends BaseService {
1934 giveaways : Giveaway [ ] ;
2035 latestStatus ?: GiveawayStatus ;
2136
2237 /**
23- * Please use ` .initialize` if ` initialGiveaways` not supplied
38+ * Please use { @link GiveawayService .initialize} if { @link initialGiveaways} not supplied
2439 * @param initialGiveaways **NB:** not a deep copy
2540 */
2641 constructor ( initialGiveaways ?: Giveaway [ ] | undefined ) {
42+ super ( ) ;
2743 this . giveaways = initialGiveaways || [ ] ;
2844 }
2945
30- // Make sure to put into base class if will ever make one
31- protected get logHeader ( ) {
32- return `Service[${ this . constructor . name } ]` ;
33- }
34- protected log ( message : string , logLevel : LogLevel = LogLevel . Info ) {
35- const formattedMessage = `${ this . logHeader } : ${ message } ` ;
36- globalThis . logger . write ( logLevel , formattedMessage ) ;
37- }
38-
3946 /**
4047 * Fetches giveaways if not supplied on construction
4148 */
42- async initialize ( ) {
43- if ( this . giveaways . length === 0 ) {
44- const fetchResult = await this . fetchGiveaways ( ) ;
45- if ( fetchResult instanceof GiveawayStatus ) {
46- this . latestStatus = fetchResult ;
47- } else {
48- this . giveaways = fetchResult ;
49- }
49+ async initialize ( ) : Promise < this> {
50+ if ( this . giveaways . length > 0 ) return this ;
51+ const cachedGiveaways = await cache . get ( 'giveaways' ) ;
52+ if ( cachedGiveaways ) {
53+ this . giveaways = cachedGiveaways ;
54+ return this ;
55+ }
56+
57+ // `this.fetchGiveaways()` already sets `this.giveaways`
58+ const fetchResult = await this . fetchGiveaways ( ) ;
59+ if ( ! ( fetchResult instanceof GiveawayStatus ) ) {
60+ void cache . set ( 'giveaways' , fetchResult ) ;
5061 }
5162 return this ;
5263 }
5364
5465 /**
5566 * Fetches giveaways from old to new
67+ * and stores to {@link GiveawayService.giveaways}
5668 *
5769 * **Note:** old to new is **not** guaranteed -- giveaways are just reversed\
5870 * it is assumed that website displays content from new to old
@@ -61,27 +73,44 @@ export class GiveawayService {
6173 this . latestStatus = undefined ;
6274 const sources = Object . keys ( giveawayFetchers ) ;
6375 for ( const sourceKey of sources ) {
64- const source = giveawayFetchers [ sourceKey as TAvailableSite ] ;
65- const giveaways = await source
66- . getGiveaways ( )
67- . catch ( ( error : Error ) =>
68- globalThis . logger . error ( error , `${ sourceKey } : FAILED` )
69- ) ;
70- if ( ! giveaways || giveaways . length === 0 ) continue ;
71-
72- // might be moved into giveawayFetcher
73- this . giveaways = giveaways . reverse ( ) ;
74- for ( const x of this . giveaways ) x . storeIntoDatabase ( ) ;
75-
76- this . log ( `Fetched ${ this . giveaways . length } giveaways from ${ sourceKey } ` ) ;
76+ const giveaways = await this . fetchGiveawaysFromSource (
77+ sourceKey as TAvailableSite
78+ ) ;
79+ if ( ! giveaways ) continue ;
80+
81+ this . giveaways = giveaways ;
7782 return this . giveaways ;
7883 }
79- return new GiveawayStatus ( GiveawayStatusEnum . NONE_FOUND , true ) ;
84+ this . latestStatus = new GiveawayStatus ( GiveawayStatusEnum . NONE_FOUND , true ) ;
85+ return this . latestStatus ;
86+ }
87+
88+ /**
89+ * Fetches giveaways from old to new
90+ *
91+ * **Note:** old to new is **not** guaranteed -- giveaways are just reversed\
92+ * it is assumed that website displays content from new to old
93+ */
94+ async fetchGiveawaysFromSource ( sourceSite : TAvailableSite ) {
95+ const source = giveawayFetchers [ sourceSite ] ;
96+ let giveaways = await source
97+ . getGiveaways ( )
98+ . catch ( ( error : Error ) =>
99+ globalThis . logger . error ( error , `${ sourceSite } : FAILED` )
100+ ) ;
101+ if ( ! giveaways || giveaways . length === 0 ) return false ;
102+
103+ this . log ( `Fetched ${ giveaways . length } giveaways from ${ sourceSite } ` ) ;
104+ giveaways = giveaways . reverse ( ) ; // to make it from `old to new`
105+ for ( const x of giveaways ) void x . storeIntoDatabase ( ) ;
106+ return giveaways ;
80107 }
81108
82- async filterGiveaways ( channel : TextBasedChannel ) {
109+ /**
110+ * @param channel_container connected to `giveaways_channel_link.channel_container` and {@link getChannelParentID}.id
111+ */
112+ async filterGiveaways ( channel_container : string ) {
83113 // might add a check for `this.latestStatus`
84- const channelParent = getChannelParentID ( channel ) ;
85114 const giveawayTitles = this . giveaways . map ( ( x ) => x . giveaway . title ) ;
86115
87116 const query = DB . selectFrom ( 'giveaways' )
@@ -96,7 +125,7 @@ export class GiveawayService {
96125 . where (
97126 'giveaways_channel_link.channel_container' ,
98127 '=' ,
99- channelParent . id
128+ channel_container
100129 )
101130 ) ,
102131 ] )
@@ -109,36 +138,48 @@ export class GiveawayService {
109138 - 1
110139 ) ;
111140 const delta = filteredGiveaways . length - this . giveaways . length ;
112- this . log ( `Filtered ${ Math . abs ( delta ) } giveaways for ${ channelParent . id } ` ) ;
141+ this . log ( `Filtered ${ Math . abs ( delta ) } giveaways for ${ channel_container } ` ) ;
113142 // might be a good idea to initialize and then set NO_NEW status if delta 0
114143 return new GiveawayService ( filteredGiveaways ) ;
115144 }
116145
117- async sendGiveaways ( channel : TextBasedChannel ) {
118- if ( this . giveaways . length === 0 ) {
119- this . latestStatus = new GiveawayStatus ( GiveawayStatusEnum . NO_NEW ) ;
120- return this . latestStatus ;
121- }
122-
146+ async filterGiveawaysWithChannel ( channel : TextBasedChannel ) {
147+ // might add a check for `this.latestStatus`
123148 const channelParent = getChannelParentID ( channel ) ;
124- for ( const giveaway of this . giveaways ) {
125- await giveaway . sendToChannel ( channel ) ;
126- }
127- this . log ( `Sent ${ this . giveaways . length } giveaways to ${ channelParent . id } ` ) ;
149+ return this . filterGiveaways ( channelParent . id ) ;
150+ }
128151
152+ /**
153+ * @param channel_container connected to `giveaways_channel_link.channel_container` and {@link getChannelParentID}.id
154+ */
155+ async storeSentGiveaways ( channel_container : string ) {
129156 const giveawayTitles = this . giveaways . map ( ( x ) => x . giveaway . title ) ;
130157 const query = DB . replaceInto ( 'giveaways_channel_link' )
131158 . columns ( [ 'channel_container' , 'giveaway_id' ] )
132159 . expression ( ( eb ) =>
133160 eb
134161 . selectFrom ( 'giveaways' )
135162 . select ( ( eb ) => [
136- eb . val ( channelParent . id ) . as ( 'channel_container' ) ,
163+ eb . val ( channel_container ) . as ( 'channel_container' ) ,
137164 'giveaways.id' ,
138165 ] )
139166 . where ( 'giveaways.title' , 'in' , giveawayTitles )
140167 ) ;
141168 await query . execute ( ) ;
169+ }
170+
171+ async sendGiveaways ( channel : TextBasedChannel ) {
172+ if ( this . giveaways . length === 0 ) {
173+ this . latestStatus = new GiveawayStatus ( GiveawayStatusEnum . NO_NEW ) ;
174+ return this . latestStatus ;
175+ }
176+
177+ const channelParent = getChannelParentID ( channel ) ;
178+ for ( const giveaway of this . giveaways ) {
179+ await giveaway . sendToChannel ( channel ) ;
180+ }
181+ this . log ( `Sent ${ this . giveaways . length } giveaways to ${ channelParent . id } ` ) ;
182+ await this . storeSentGiveaways ( channelParent . id ) ;
142183
143184 this . latestStatus = new GiveawayStatus ( GiveawayStatusEnum . SUCCESS ) ;
144185 return this . latestStatus ;
0 commit comments