diff --git a/src/core/game_events/betika/index.ts b/src/core/game_events/betika/index.ts new file mode 100644 index 0000000..c212a91 --- /dev/null +++ b/src/core/game_events/betika/index.ts @@ -0,0 +1,43 @@ +import { BaseGameEventsProcessor } from ".."; +import { getConfig } from "../../.."; +import { BetProvider } from "../../../bet_providers"; +import { BetikaProvider } from "../../../bet_providers/betika"; +import { RedisSingleton } from "../../../datastores/redis"; +import { getRedisProcessedEventsChannelName } from "../../../utils/redis"; +import { ProcessedGameEvents } from "../../../utils/types/common"; +import { Result } from "../../../utils/types/result_type"; + +const {logger} = getConfig(); + +export class BetikaGameEventsProcessor extends BaseGameEventsProcessor { + public override betProvider: BetProvider; + + constructor() { + super(); + this.betProvider = new BetikaProvider(); + } + + public async subscribeToChannels(): Promise> { + const getBetProviderConfigResult = await this.betProvider.getConfig(); + if (getBetProviderConfigResult.result === "error") { + logger.error("Events processor failed to load config for provider: ", this.betProvider.name); + return getBetProviderConfigResult; + } + + const getRedisSubscriberResult = await RedisSingleton.getSubscriber(); + if (getRedisSubscriberResult.result === "success") { + const betProviderConfig = getBetProviderConfigResult.value; + const results = betProviderConfig.games.map(async game => { + await getRedisSubscriberResult.value.subscribe(getRedisProcessedEventsChannelName(this.betProvider, game.name, game.betType), message => { + const parsedMessage = JSON.parse(message) as ProcessedGameEvents; + logger.trace("redis subscriber message received. ", parsedMessage) + }); + }); + await Promise.all(results); + return {result: "success", value: true}; + } else { + logger.error("Events processor failed to connect to redis subscriber for provider: ", this.betProvider.name); + return getRedisSubscriberResult; + } + } +} \ No newline at end of file diff --git a/src/core/game_events/index.ts b/src/core/game_events/index.ts index e69de29..142e552 100644 --- a/src/core/game_events/index.ts +++ b/src/core/game_events/index.ts @@ -0,0 +1,5 @@ +import { BetProvider } from "../../bet_providers"; + +export abstract class BaseGameEventsProcessor { + public abstract betProvider: BetProvider +} diff --git a/src/core/parsers/betika/index.ts b/src/core/parsers/betika/index.ts index d8b7979..6983a5c 100644 --- a/src/core/parsers/betika/index.ts +++ b/src/core/parsers/betika/index.ts @@ -3,8 +3,8 @@ import { getConfig } from "../../.."; import { BetProvider } from "../../../bet_providers"; import { BetikaProvider } from "../../../bet_providers/betika"; import { RedisSingleton } from "../../../datastores/redis"; -import { getRedisHtmlParserChannelName } from "../../../utils/redis"; -import { BetTypes, RawHtmlForProcessingMessage } from "../../../utils/types/common"; +import { getRedisProcessedEventsChannelName, getRedisHtmlParserChannelName } from "../../../utils/redis"; +import { BetTypes, ProcessedThreeWayGameEvent, ProcessedTwoWayGameEvent, RawHtmlForProcessingMessage } from "../../../utils/types/common"; import { Result } from "../../../utils/types/result_type"; import { processBetikaThreeWayGamesHtml, processBetikaTwoWayGamesHtml } from "./parser_types"; @@ -57,14 +57,46 @@ export class BetikaParser extends BaseParser { } } - private processRawHtmlMessage(parsedMessage: RawHtmlForProcessingMessage): void { + private async processRawHtmlMessage(parsedMessage: RawHtmlForProcessingMessage): Promise { let results2; + let parsedResults: ProcessedTwoWayGameEvent[] | ProcessedThreeWayGameEvent[]; switch (parsedMessage.betType) { case BetTypes.TWO_WAY: results2 = processBetikaTwoWayGamesHtml(parsedMessage.rawHtml); + if (results2.result === "success") { + parsedResults = results2.value.map(item => { + return { + oddsAWin: item.oddsAWin, + oddsBWin: item.oddsBWin, + league: item.league, + estimatedStartTimeUtc: item.estimatedStartTimeUtc, + meta: JSON.stringify({ + link: item.link + }) + } as ProcessedTwoWayGameEvent; + }); + } else { + throw new Error("Failed to process Betika two way games html"); + } break; case BetTypes.THREE_WAY: results2 = processBetikaThreeWayGamesHtml(parsedMessage.rawHtml); + if (results2.result === "success") { + parsedResults = results2.value.map(item => { + return { + oddsAWin: item.oddsAWin, + oddsBWin: item.oddsBWin, + oddsDraw: item.oddsDraw, + league: item.league, + estimatedStartTimeUtc: item.estimatedStartTimeUtc, + meta: JSON.stringify({ + link: item.link + }) + } as ProcessedThreeWayGameEvent; + }); + } else { + throw new Error("Failed to process Betika Three way games html"); + } break; default: const message = "Unknown bet type provided"; @@ -77,16 +109,28 @@ export class BetikaParser extends BaseParser { throw new Error(`Unknown bet type provided for provider: ${this.betProvider.name}`); } - if (results2.result === "success") { - logger.info("Successfully fetched games: ", results2.value); + logger.info("Successfully fetched games: ", results2.value); + const getRedisPublisherResult = await RedisSingleton.getPublisher(); + + if (getRedisPublisherResult.result === "success") { + this.publishProcessedGameEvents( + getRedisPublisherResult.value, + getRedisProcessedEventsChannelName(this.betProvider, parsedMessage.gameName, parsedMessage.betType), + { + betProviderName: parsedMessage.betProviderName, + betType: parsedMessage.betType, + gameName: parsedMessage.gameName, + data: parsedResults + }); } else { - logger.error("Failed to parse html into games: ", { + const message = "Failed to get redis publisher to send processed events: "; + logger.error(message, { betProviderName: parsedMessage.betProviderName, betType: parsedMessage.betType, fromUrl: parsedMessage.fromUrl, gameName: parsedMessage.gameName, - errorMessage: results2.value.message - }); + errorMessage: getRedisPublisherResult.value.message + }) } } } \ No newline at end of file diff --git a/src/core/parsers/index.ts b/src/core/parsers/index.ts index c18c908..3be8801 100644 --- a/src/core/parsers/index.ts +++ b/src/core/parsers/index.ts @@ -1,5 +1,15 @@ +import { RedisClientType } from "redis"; import { BetProvider } from "../../bet_providers"; +import { ProcessedGameEvents } from "../../utils/types/common"; export abstract class BaseParser { public abstract betProvider: BetProvider; + + protected async publishProcessedGameEvents( + redisPublisher: RedisClientType, + channelName: string, + data: ProcessedGameEvents + ) { + await redisPublisher.publish(channelName, JSON.stringify(data)); + } } diff --git a/src/datastores/postgres/entities/index.ts b/src/datastores/postgres/entities/index.ts index 45592b6..a1e6f8b 100644 --- a/src/datastores/postgres/entities/index.ts +++ b/src/datastores/postgres/entities/index.ts @@ -1,9 +1,11 @@ import { Entity, Column, PrimaryGeneratedColumn, Index} from "typeorm"; +import { BetProviders, Games } from "../../../utils/types/common"; /** * Table representing game events where there is no possibility of a draw. * A good example is tennis. */ +@Index(["bet_provider_id", "bet_provider_name"], {unique: true}) @Entity({name: "two_way_game_event"}) export class TwoWayGameEventEntity { @PrimaryGeneratedColumn() @@ -25,11 +27,18 @@ export class TwoWayGameEventEntity { @Column("decimal", {nullable: false}) odds_b_win: number + @Index("two_way_game_event_bet_provider_idx") @Column("varchar", {length: 100, nullable: false}) - bet_provider_name: string + bet_provider_name: BetProviders @Column("varchar", {length: 100, nullable: false}) - game_name: string + game_name: Games + + @Column("varchar", {length: 100, nullable: false}) + league: string + + @Column("json", {nullable: false}) + meta_data: string @Index("two_way_game_event_created_at_idx") @Column("timestamptz", {nullable: false, default: () => "CURRENT_TIMESTAMP"}) @@ -43,6 +52,7 @@ export class TwoWayGameEventEntity { * Table representing game events where the is possibility of a draw. A great * example is football. */ +@Index(["bet_provider_id", "bet_provider_name"], {unique: true}) @Entity({name: "three_way_game_event"}) export class ThreeWayGameEventEntity { @PrimaryGeneratedColumn() @@ -67,11 +77,18 @@ export class ThreeWayGameEventEntity { @Column("decimal", {nullable: false}) odds_draw: number + @Index("three_way_game_event_bet_provider_idx") @Column("varchar", {length: 100, nullable: false}) - bet_provider: string + bet_provider: BetProviders @Column("varchar", {length: 100, nullable: false}) - game_name: string + game_name: Games + + @Column("varchar", {length: 100, nullable: false}) + league: string + + @Column("json", {nullable: false}) + meta_data: string @Index("three_way_game_event_created_at_idx") @Column("timestamptz", {nullable: false, default: () => "CURRENT_TIMESTAMP"}) diff --git a/src/datastores/postgres/queries/index.ts b/src/datastores/postgres/queries/index.ts deleted file mode 100644 index 3897c4a..0000000 --- a/src/datastores/postgres/queries/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {DataSource} from "typeorm"; -import { ThreeWayGameEvent, TwoWayGameEvent } from "../../../utils/types/db"; -import { ThreeWayGameEventEntity, TwoWayGameEventEntity } from "../entities"; - -export const insertTwoWayGameEvent = async ( - dataSource: DataSource, - data: TwoWayGameEvent -) => { - const toDataBase = { - bet_provider_id: data.betProviderId, - bet_provider_name: data.betProviderName, - club_a: data.clubA, - club_b: data.clubB, - odds_a_win: data.oddsAWin, - odds_b_win: data.oddsBWin, - game_name: data.gameName - }; - await dataSource.createQueryBuilder() - .insert() - .into(TwoWayGameEventEntity) - .values(toDataBase) - .execute(); -}; - -export const insertThreeWayGameEvent = async ( - dataSource: DataSource, - data: ThreeWayGameEvent -) => { - const toDataBase = { - bet_provider_id: data.betProviderId, - bet_provider_name: data.betProviderName, - club_a: data.clubA, - club_b: data.clubB, - odds_a_win: data.oddsAWin, - odds_b_win: data.oddsBWin, - game_name: data.gameName - } - await dataSource.createQueryBuilder() - .insert() - .into(ThreeWayGameEventEntity) - .values(toDataBase) - .execute(); -}; \ No newline at end of file diff --git a/src/datastores/postgres/queries/three_way_game_event/index.ts b/src/datastores/postgres/queries/three_way_game_event/index.ts new file mode 100644 index 0000000..f8c32be --- /dev/null +++ b/src/datastores/postgres/queries/three_way_game_event/index.ts @@ -0,0 +1,65 @@ +import { DataSource, InsertResult, UpdateResult } from "typeorm"; +import { BetProviders } from "../../../../utils/types/common"; +import { ThreeWayGameEvent } from "../../../../utils/types/db"; +import { ThreeWayGameEventEntity } from "../../entities"; + +/** + * Useful for checking whether a three way game event already exists for a provider. + * It can then either be created if not exists, updated if the odds have changed, or ignored if no changes. + * @param dataSource + * @param betProviderId + * @param betProviderName + * @returns + */ +export const getThreeWayGame = async ( + dataSource: DataSource, + betProviderId: string, + betProviderName: BetProviders +): Promise => { + return await dataSource.createQueryBuilder() + .select("three_way_game_event") + .from(ThreeWayGameEventEntity, "three_way_game_event") + .where("bet_provider_id = :betProviderId", {betProviderId: betProviderId}) + .andWhere("bet_provider_name = :betProviderName", {betProviderName}) + .getOne(); +}; + +export const insertThreeWayGameEvent = async ( + dataSource: DataSource, + data: ThreeWayGameEvent +): Promise => { + const toDataBase = { + bet_provider_id: data.betProviderId, + bet_provider_name: data.betProviderName, + club_a: data.clubA, + club_b: data.clubB, + odds_a_win: data.oddsAWin, + odds_b_win: data.oddsBWin, + game_name: data.gameName + } + return await dataSource.createQueryBuilder() + .insert() + .into(ThreeWayGameEventEntity) + .values(toDataBase) + .execute(); +}; + +export const updateThreeWayGameEventOdds = async ( + dataSource: DataSource, + betProviderId: string, + betProviderName: BetProviders, + oddsAWin: number, + oddsBWin: number, + oddsDraw: number +): Promise => { + return await dataSource.createQueryBuilder() + .update(ThreeWayGameEventEntity) + .set({ + odds_a_win: oddsAWin, + odds_b_win: oddsBWin, + odds_draw: oddsDraw + }) + .where("bet_provider_id = :betProviderId", {betProviderId: betProviderId}) + .andWhere("bet_provider_name = :betProviderName", {betProviderName}) + .execute(); +}; diff --git a/src/datastores/postgres/queries/two_way_game_event/index.ts b/src/datastores/postgres/queries/two_way_game_event/index.ts new file mode 100644 index 0000000..e19d24b --- /dev/null +++ b/src/datastores/postgres/queries/two_way_game_event/index.ts @@ -0,0 +1,63 @@ +import { DataSource, InsertResult, UpdateResult } from "typeorm"; +import { BetProviders } from "../../../../utils/types/common"; +import { TwoWayGameEvent } from "../../../../utils/types/db"; +import { TwoWayGameEventEntity } from "../../entities"; + +/** + * Useful for checking whether a two way game event already exists for a provider. + * It can then either be created if not exists, updated if the odds have changed, or ignored if no changes. + * @param dataSource + * @param betProviderId + * @param betProviderName + * @returns + */ +export const getTwoWayGame = async ( + dataSource: DataSource, + betProviderId: string, + betProviderName: BetProviders +): Promise => { + return await dataSource.createQueryBuilder() + .select("two_way_game_event") + .from(TwoWayGameEventEntity, "two_way_game_event") + .where("bet_provider_id = :betProviderId", {betProviderId: betProviderId}) + .andWhere("bet_provider_name = :betProviderName", {betProviderName}) + .getOne(); +}; + +export const insertTwoWayGameEvent = async ( + dataSource: DataSource, + data: TwoWayGameEvent +): Promise => { + const toDataBase = { + bet_provider_id: data.betProviderId, + bet_provider_name: data.betProviderName, + club_a: data.clubA, + club_b: data.clubB, + odds_a_win: data.oddsAWin, + odds_b_win: data.oddsBWin, + game_name: data.gameName + }; + return await dataSource.createQueryBuilder() + .insert() + .into(TwoWayGameEventEntity) + .values(toDataBase) + .execute(); +}; + +export const updateTwoWayGameEventOdds = async ( + dataSource: DataSource, + betProviderId: string, + betProviderName: BetProviders, + oddsAWin: number, + oddsBWin: number +): Promise => { + return await dataSource.createQueryBuilder() + .update(TwoWayGameEventEntity) + .set({ + odds_a_win: oddsAWin, + odds_b_win: oddsBWin + }) + .where("bet_provider_id = :betProviderId", {betProviderId: betProviderId}) + .andWhere("bet_provider_name = :betProviderName", {betProviderName}) + .execute(); +}; diff --git a/src/utils/redis/index.ts b/src/utils/redis/index.ts index 2fd7851..d0e5859 100644 --- a/src/utils/redis/index.ts +++ b/src/utils/redis/index.ts @@ -1,5 +1,5 @@ import { BetProvider } from "../../bet_providers"; -import { BetProviderGameConfig } from "../types/common"; +import { BetProviderGameConfig, BetTypes, Games } from "../types/common"; /** * Generate the Redis pub/sub channel name for publishing raw HTML, and subscribers will parse the HTML into game events. @@ -12,6 +12,6 @@ export function getRedisHtmlParserChannelName(betProvider: BetProvider, gameConf return `${betProvider.name}_${gameConfig.name.replace(" ", "")}_${gameConfig.betType.replace(" ", "")}`; } -export function getRedisEventsChannelName(betProvider: BetProvider, gameConfig: BetProviderGameConfig): string { - return `event:${betProvider.name}_${gameConfig.name.replace(" ", "")}_${gameConfig.betType.replace(" ", "")}`; +export function getRedisProcessedEventsChannelName(betProvider: BetProvider, gameName: Games, betType: BetTypes): string { + return `event:${betProvider.name}_${gameName.replace(" ", "")}_${betType.replace(" ", "")}`; } diff --git a/src/utils/types/common/index.ts b/src/utils/types/common/index.ts index 1fb19cc..cd7c44f 100644 --- a/src/utils/types/common/index.ts +++ b/src/utils/types/common/index.ts @@ -51,6 +51,38 @@ export interface RawHtmlForProcessingMessage { rawHtml: string; } +export interface ProcessedHtmlMessage { + value: any[] +} + +export interface BaseProcessedGameEvent { + clubA: string, + clubB: string, + estimatedStartTimeUtc: Date, + league: string + meta: any // Store any additional metadata about a specific provider you would want here +} + +export interface ProcessedTwoWayGameEvent extends BaseProcessedGameEvent { + type: BetTypes.TWO_WAY; + oddsAWin: number; + oddsBWin: number; +} + +export interface ProcessedThreeWayGameEvent extends BaseProcessedGameEvent { + type: BetTypes.THREE_WAY; + oddsAWin: number; + oddsBWin: number; + oddsDraw: number; +} + +export interface ProcessedGameEvents { + betProviderName: BetProviders; + betType: BetTypes; + gameName: Games; + data: ProcessedTwoWayGameEvent[] | ProcessedThreeWayGameEvent[]; +} + /** * Corresponds to moment.js timezones */ diff --git a/src/utils/types/db/index.ts b/src/utils/types/db/index.ts index 9d8909e..fa946fc 100644 --- a/src/utils/types/db/index.ts +++ b/src/utils/types/db/index.ts @@ -14,6 +14,8 @@ export interface TwoWayGameEvent { oddsAWin: number; oddsBWin: number; gameName: Games; + league: string; + metaData: string; } export interface ThreeWayGameEvent { @@ -25,4 +27,6 @@ export interface ThreeWayGameEvent { oddsBWin: number; oddsDraw: number; gameName: Games; + league: string; + metaData: string; }