298 lines
8.5 KiB
TypeScript
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));
|
|
}
|
|
}
|
|
|