import crypto from 'crypto'; import { knexOwner as knex } from './knex.js'; import { curateEntity } from './entities.js'; import redis from './redis.js'; import initLogger from './logger.js'; const logger = initLogger(); function curateCampaign(campaign) { if (!campaign) { return null; } return { id: campaign.id, url: campaign.url, entity: campaign.entity && curateEntity({ ...campaign.entity, parent: campaign.parent_entity }), banner: campaign.banner && { id: campaign.banner.id, type: campaign.banner.type, width: campaign.banner.width, height: campaign.banner.height, ratio: campaign.banner.ratio, entity: campaign.banner_entity && curateEntity({ ...campaign.banner_entity, parent: campaign.banner_parent_entity }), tags: campaign.banner_tags || [], }, affiliate: campaign.affiliate && { id: campaign.affiliate.id, url: campaign.affiliate.url, parameters: campaign.affiliate.parameters, }, }; } export async function getRandomCampaign(options = {}, context = {}) { const campaigns = options.campaigns || await redis.hGetAll('traxxx:campaigns').then((rawCampaigns) => Object.values(rawCampaigns).map((rawCampaign) => JSON.parse(rawCampaign))); const validCampaigns = campaigns.filter((campaign) => { if (options.minRatio && (!campaign.banner || campaign.banner.ratio < options.minRatio)) { return false; } if (options.maxRatio && (!campaign.banner || campaign.banner.ratio > options.maxRatio)) { return false; } if (options.entityIds && !options.entityIds.some((entityId) => campaign.entity.id === entityId || campaign.entity.parent?.id === entityId)) { return false; } if (context.tagFilter && campaign.banner && campaign.banner.tags.some((tag) => context.tagFilter.includes(tag))) { return false; } return true; }); const campaignsByEntityId = validCampaigns.reduce((acc, campaign) => { const entityId = campaign.entity.parent?.id || campaign.entity.id; if (!acc[entityId]) { acc[entityId] = []; } acc[entityId].push(campaign); return acc; }, {}); // randomize entities first to ensure fair exposure for entities with fewer banners const entityIds = Object.keys(campaignsByEntityId); const randomEntityCampaigns = campaignsByEntityId[entityIds[crypto.randomInt(entityIds.length)]]; const primaryCampaigns = randomEntityCampaigns.filter((campaign) => campaign.entity.id === options.entityIds?.[0]); if (validCampaigns.length > 0) { const randomCampaign = (primaryCampaigns.length > 0 ? primaryCampaigns[crypto.randomInt(primaryCampaigns.length)] : null) || (randomEntityCampaigns.length > 0 ? randomEntityCampaigns[crypto.randomInt(randomEntityCampaigns.length)] : null) || validCampaigns[crypto.randomInt(validCampaigns.length)]; return randomCampaign; } return null; } export async function getRandomCampaigns(allOptions = [], context = {}) { const rawCampaigns = await redis.hGetAll('traxxx:campaigns'); const campaigns = Object.values(rawCampaigns).map((rawCampaign) => JSON.parse(rawCampaign)); return Promise.all(allOptions.map(async (options) => getRandomCampaign({ ...options, campaigns, }, context))); } export async function cacheCampaigns() { const campaigns = await knex('campaigns') .select('campaigns.*') .select( 'campaigns.*', knex.raw('row_to_json(affiliates) as affiliate'), knex.raw('row_to_json(banners) as banner'), knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as parent_entity'), knex.raw('row_to_json(banner_entities) as banner_entity'), knex.raw('row_to_json(banner_parents) as banner_parent_entity'), knex.raw('json_agg(tags.slug) filter (where tags.id is not null) as banner_tags'), ) .leftJoin('affiliates', 'affiliates.id', 'campaigns.affiliate_id') .leftJoin('banners', 'banners.id', 'campaigns.banner_id') .leftJoin('banners_tags', 'banners_tags.banner_id', 'banners.id') .leftJoin('tags', 'tags.id', 'banners_tags.tag_id') .leftJoin('entities', 'entities.id', 'campaigns.entity_id') .leftJoin('entities as parents', 'parents.id', 'entities.parent_id') .leftJoin('entities as banner_entities', 'banner_entities.id', 'banners.entity_id') .leftJoin('entities as banner_parents', 'banner_parents.id', 'banner_entities.parent_id') .groupBy( 'campaigns.id', 'affiliates.id', 'entities.id', 'parents.id', 'banners.id', 'banner_entities.id', 'banner_parents.id', ); await redis.del('traxxx:campaigns'); await Promise.all(campaigns.map(async (campaign) => { const curatedCampaign = curateCampaign(campaign); await redis.hSet('traxxx:campaigns', campaign.id, JSON.stringify(curatedCampaign)); })); logger.info('Cached campaigns'); }