fchat-rising/chat/ads/ad-manager.ts

298 lines
8.5 KiB
TypeScript

import throat from 'throat';
import * as _ from 'lodash';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import core from '../core';
import { Conversation } from '../interfaces';
import Timer = NodeJS.Timer;
import ChannelConversation = Conversation.ChannelConversation;
const adManagerThroat = throat(1);
export interface RecoverableAd {
channel: string;
index: number;
nextPostDue: Date | undefined,
firstPost: Date | undefined,
expireDue: Date | undefined;
}
export class AdManager {
static readonly POSTING_PERIOD = 3 * 60 * 60 * 1000;
static readonly START_VARIANCE = 3 * 60 * 1000;
static readonly POST_VARIANCE = 8 * 60 * 1000;
static readonly POST_DELAY = 1.5 * 60 * 1000;
static readonly POST_MANUAL_THRESHOLD = 5 * 1000; // don't post anything within 5 seconds of other posts
private conversation: Conversation;
private adIndex = 0;
private active = false;
private nextPostDue?: Date;
private expireDue?: Date;
private firstPost?: Date;
private interval?: Timer;
private adMap: number[] = [];
constructor(conversation: Conversation) {
this.conversation = conversation;
}
isActive(): boolean {
return this.active;
}
skipAd(): void {
this.adIndex += 1;
}
// tslint:disable-next-line
private async delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// This makes sure there is a 5s delay between channel posts
private async sendAdToChannel(msg: string, conv: Conversation.ChannelConversation): Promise<void> {
const initTime = Date.now();
await adManagerThroat(
async() => {
const throatTime = Date.now();
const delta = Date.now() - core.cache.getLastPost().getTime();
if ((delta > 0) && (delta < AdManager.POST_MANUAL_THRESHOLD)) {
await this.delay(delta);
}
const delayTime = Date.now();
log.debug(
'adManager.sendAdToChannel',
{
character: core.characters.ownCharacter?.name,
channel: conv.channel.name,
throatDelta: throatTime - initTime,
delayDelta: delayTime - throatTime,
totalWait: delayTime - initTime,
msg
}
);
await conv.sendAd(msg);
}
);
}
private async sendNextPost(): Promise<void> {
const msg = this.getNextAd();
if ((!msg) || ((this.expireDue) && (this.expireDue.getTime() < Date.now()))) {
this.stop();
return;
}
const chanConv = (<Conversation.ChannelConversation>this.conversation);
await this.sendAdToChannel(msg, chanConv);
// post next ad every 12 - 22 minutes
const nextInMs = Math.max(0, (chanConv.nextAd - Date.now())) +
AdManager.POST_DELAY +
Math.random() * AdManager.POST_VARIANCE;
this.adIndex = this.adIndex + 1;
this.nextPostDue = new Date(Math.max(
Date.now() + nextInMs,
(chanConv.settings.adSettings.lastAdTimestamp || 0) + (core.connection.vars.lfrp_flood * 1000)
));
// tslint:disable-next-line: no-unnecessary-type-assertion
this.interval = setTimeout(
async() => {
await this.sendNextPost();
},
nextInMs
) as Timer;
}
generateAdMap(): number[] {
const ads = this.getAds();
const idx = _.range(ads.length);
return this.shouldUseRandomOrder() ? _.shuffle(idx) : idx;
}
shouldUseRandomOrder(): boolean {
return !!this.conversation.settings.adSettings.randomOrder;
}
getAds(): string[] {
return this.conversation.settings.adSettings.ads;
}
getNextAd(): string | undefined {
const ads = this.getAds();
if (ads.length === 0)
return;
if (ads.length !== this.adMap.length) {
log.debug('adManager.regenerate.on-the-fly', ads.length, this.adMap.length);
this.adMap = this.generateAdMap();
}
return ads[this.adMap[this.adIndex % this.adMap.length] % ads.length];
}
getNextPostDue(): Date | undefined {
return this.nextPostDue;
}
getExpireDue(): Date | undefined {
return this.expireDue;
}
getFirstPost(): Date | undefined {
return this.firstPost;
}
start(timeoutMs = AdManager.POSTING_PERIOD): void {
const chanConv = (<Conversation.ChannelConversation>this.conversation);
const initialWait = Math.max(
Math.random() * AdManager.START_VARIANCE,
(chanConv.nextAd - Date.now()) * 1.1,
((this.conversation.settings.adSettings.lastAdTimestamp || 0) + (core.connection.vars.lfrp_flood * 1000)) - Date.now()
);
this.adIndex = 0;
this.active = true;
this.nextPostDue = new Date(Math.max(
Date.now() + initialWait,
(this.conversation.settings.adSettings.lastAdTimestamp || 0) + (core.connection.vars.lfrp_flood * 1000)
));
this.expireDue = new Date(Date.now() + timeoutMs);
this.adMap = this.generateAdMap();
// tslint:disable-next-line: no-unnecessary-type-assertion
this.interval = setTimeout(
async() => {
this.firstPost = new Date();
await this.sendNextPost();
},
initialWait
) as Timer;
}
protected forceTimeout(waitTime: number): void {
if (this.interval) {
clearTimeout(this.interval);
}
// tslint:disable-next-line: no-unnecessary-type-assertion
this.interval = setTimeout(
async() => {
await this.sendNextPost();
},
waitTime
) as Timer;
}
stop(): void {
if (this.interval)
clearTimeout(this.interval);
delete this.interval;
delete this.nextPostDue;
delete this.expireDue;
delete this.firstPost;
this.active = false;
this.adIndex = 0;
// const message = new EventMessage(`Advertisements on channel [channel]${this.conversation.name}[/channel] have expired.`);
// addEventMessage(message);
}
renew(): void {
if (!this.active)
return;
this.expireDue = new Date(Date.now() + 3 * 60 * 60 * 1000);
}
protected static recoverableCharacter = '';
protected static recoverableAds: RecoverableAd[] = [];
static onConnectionClosed(): void {
AdManager.recoverableCharacter = _.get(core, 'characters.ownCharacter.name', '');
AdManager.recoverableAds = _.map(
_.filter(core.conversations.channelConversations, (c) => ((c.adManager) && (c.adManager.isActive()))),
(chanConv): RecoverableAd => {
const adManager = chanConv.adManager;
return {
channel : chanConv.name,
index : adManager.adIndex,
nextPostDue : adManager.nextPostDue,
firstPost : adManager.firstPost,
expireDue : adManager.expireDue
};
}
);
_.each(
_.filter(core.conversations.channelConversations, (c) => ((c.adManager) && (c.adManager.isActive()))),
(c) => c.adManager.stop()
);
}
static onNewChannelAvailable(channel: ChannelConversation): void {
if (AdManager.recoverableCharacter !== core.characters.ownCharacter.name) {
AdManager.recoverableAds = [];
AdManager.recoverableCharacter = '';
return;
}
const ra = _.find(AdManager.recoverableAds, (r) => (r.channel === channel.name));
if (!ra) {
return;
}
const adManager = channel.adManager;
adManager.stop();
adManager.start();
adManager.adIndex = ra.index;
adManager.firstPost = ra.firstPost;
adManager.nextPostDue = adManager.nextPostDue || ra.nextPostDue || new Date();
adManager.expireDue = ra.expireDue;
adManager.forceTimeout(
Math.max(0, adManager.nextPostDue.getTime() - Date.now())
);
AdManager.recoverableAds = _.filter(AdManager.recoverableAds, (r) => (r.channel !== ra.channel));
}
}