Enforce 5s cooldown between ads across all connected characters

This commit is contained in:
Mr. Stallion 2020-06-29 19:59:00 -05:00
parent 9879195a41
commit d2de87b554
9 changed files with 118 additions and 13 deletions

View File

@ -2,7 +2,10 @@
## Canary
* Fix caching issue that causes cache misses on charater page metadata
* URL preview fixes for Redgifs, Gelbooru, Tumblr, and Gifmixxx
* Fix ad posting issue that sometimes disconnects characters if multiple characters are in use
* URL preview fixes for Redgifs, Gelbooru, Tumblr, and Gifmixxx
* All dependencies are now up to date
## 1.0.1

View File

@ -126,8 +126,8 @@ import {InlineDisplayMode} from '../interfaces';
core.connection.onEvent('closed', (isReconnect) => {
if(process.env.NODE_ENV !== 'production') {
log.debug(
'connection.closed',
{
type: 'connection.closed',
character: core.characters.ownCharacter?.name,
error: this.error,
isReconnect
@ -141,6 +141,7 @@ import {InlineDisplayMode} from '../interfaces';
this.connecting = false;
AdManager.onConnectionClosed();
core.adCoordinator.clear();
document.title = l('title');
});
@ -149,8 +150,8 @@ import {InlineDisplayMode} from '../interfaces';
if(process.env.NODE_ENV !== 'production') {
log.debug(
'connection.connecting',
{
type: 'connection.connecting',
character: core.characters.ownCharacter?.name
}
);
@ -165,8 +166,8 @@ import {InlineDisplayMode} from '../interfaces';
core.connection.onEvent('connected', () => {
if(process.env.NODE_ENV !== 'production') {
log.debug(
'connection.connected',
{
type: 'connection.connected',
character: core.characters.ownCharacter?.name
}
);
@ -185,8 +186,8 @@ import {InlineDisplayMode} from '../interfaces';
core.connection.onError((e) => {
if(process.env.NODE_ENV !== 'production') {
log.debug(
'connection.error',
{
type: 'connection.error',
error: errorToString(e),
character: core.characters.ownCharacter?.name
}
@ -214,7 +215,7 @@ import {InlineDisplayMode} from '../interfaces';
// skipping await
// tslint:disable-next-line: no-floating-promises
await core.notifications.initSounds(['attention', 'login', 'logout', 'modalert', 'newnote']);
core.notifications.initSounds(['attention', 'login', 'logout', 'modalert', 'newnote']);
core.connection.connect(this.selectedCharacter.name);
}

View File

@ -0,0 +1,51 @@
import _ from 'lodash';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import core from '../core';
export class AdCoordinatorGuest {
protected pendingAds: Record<string, any> = {};
protected adCounter = 0;
constructor() {
ipcRenderer.on('grant-send-ad', (_event: IpcRendererEvent, adId: string) => this.processPendingAd(adId));
}
processPendingAd(adId: string): void {
if (!(adId in this.pendingAds)) {
log.debug('adid.pending.miss', {adId, character: core.characters.ownCharacter?.name});
return;
}
log.debug('adid.pending.process', {adId, character: core.characters.ownCharacter?.name});
this.pendingAds[adId].resolve();
delete this.pendingAds[adId];
}
requestTurnToPostAd(): Promise<void> {
return new Promise(
(resolve, reject) => {
const adId = `${Math.round(Math.random() * 1000000)}-${this.adCounter++}-${Date.now()}`;
this.pendingAds[adId] = { resolve, reject, from: Date.now() };
log.debug('adid.request', {adId, character: core.characters.ownCharacter?.name});
ipcRenderer.send('request-send-ad', adId);
}
);
}
clear(): void {
_.each(this.pendingAds, (pa) => (pa.reject()));
console.debug('adid.clear', _.keys(this.pendingAds), core.characters.ownCharacter?.name);
this.pendingAds = {};
}
}

View File

@ -0,0 +1,31 @@
import throat from 'throat';
import Bluebird from 'bluebird';
import { IpcMainEvent } from 'electron';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
const adCoordinatorThroat = throat(1);
export class AdCoordinatorHost {
static readonly MIN_DISTANCE = 5000;
private lastPost = Date.now();
async processAdRequest(event: IpcMainEvent, adId: string) {
await adCoordinatorThroat(
async() => {
const sinceLastPost = Date.now() - this.lastPost;
const waitTime = Math.max(0, AdCoordinatorHost.MIN_DISTANCE - sinceLastPost);
log.debug('adid.request.host', {adId, sinceLastPost, waitTime});
await Bluebird.delay(waitTime);
log.debug('adid.request.host.grant', {adId, sinceLastPost, waitTime});
event.reply('grant-send-ad', adId);
this.lastPost = Date.now();
}
);
}
}

View File

@ -71,8 +71,8 @@ export class AdManager {
if (process.env.NODE_ENV !== 'production') {
log.debug(
'adManager.sendAdToChannel',
{
type: 'sendAdToChannel',
character: core.characters.ownCharacter?.name,
channel: conv.channel.name,
throatDelta: throatTime - initTime,

View File

@ -147,6 +147,7 @@ abstract class Conversation implements Interfaces.Conversation {
}
}
class PrivateConversation extends Conversation implements Interfaces.PrivateConversation {
readonly name = this.character.name;
readonly context = CommandContext.Private;
@ -402,7 +403,12 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
async() => {
const throatTime = Date.now();
await Conversation.testPostDelay();
await Promise.all(
[
await Conversation.testPostDelay(),
await core.adCoordinator.requestTurnToPostAd()
]
);
const delayTime = Date.now();
@ -411,8 +417,8 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
if (process.env.NODE_ENV !== 'production') {
log.debug(
'conversation.sendAd',
{
type: 'sendAd',
character: core.characters.ownCharacter?.name,
channel: this.channel.name,
throatDelta: throatTime - initTime,

View File

@ -5,6 +5,7 @@ import BBCodeParser from './bbcode';
import {Settings as SettingsImpl} from './common';
import Conversations from './conversations';
import {Channel, Character, Connection, Conversation, Logs, Notifications, Settings, State as StateInterface} from './interfaces';
import { AdCoordinatorGuest } from './ads/ad-coordinator-guest';
function createBBCodeParser(): BBCodeParser {
const parser = new BBCodeParser();
@ -65,6 +66,8 @@ const data = {
characters: <Character.State | undefined>undefined,
notifications: <Notifications | undefined>undefined,
cache: <CacheManager | undefined>undefined,
adCoordinator: <AdCoordinatorGuest | undefined>undefined,
register<K extends 'characters' | 'conversations' | 'channels'>(module: K, subState: VueState[K]): void {
Vue.set(vue, module, subState);
(<VueState[K]>data[module]) = subState;
@ -86,6 +89,7 @@ export function init(this: any, connection: Connection, logsClass: new() => Logs
data.settingsStore = new settingsClass();
data.notifications = new notificationsClass();
data.cache = new CacheManager();
data.adCoordinator = new AdCoordinatorGuest();
// tslint:disable-next-line no-floating-promises
data.cache.start();
@ -111,6 +115,7 @@ export interface Core {
readonly bbCodeParser: BBCodeParser
readonly notifications: Notifications
readonly cache: CacheManager
readonly adCoordinator: AdCoordinatorGuest;
watch<T>(getter: (this: VueState) => T, callback: WatchHandler<T>): void
}

View File

@ -50,6 +50,8 @@ import fetch from 'node-fetch';
import MenuItemConstructorOptions = Electron.MenuItemConstructorOptions;
import * as _ from 'lodash';
import DownloadItem = Electron.DownloadItem;
import { AdCoordinatorHost } from '../chat/ads/ad-coordinator-host';
import { IpcMainEvent } from 'electron';
//tslint:disable-next-line:no-require-imports
const pck = require('./package.json');
@ -542,6 +544,12 @@ function onReady(): void {
const index = characters.indexOf(character);
if(index !== -1) characters.splice(index, 1);
});
const adCoordinator = new AdCoordinatorHost();
electron.ipcMain.on('request-send-ad', (event: IpcMainEvent, adId: string) => (adCoordinator.processAdRequest(event, adId)));
const emptyBadge = electron.nativeImage.createEmpty();
//tslint:disable-next-line:no-require-imports no-unsafe-any

View File

@ -131,8 +131,8 @@ export default class Connection implements Interfaces.Connection {
if(res.error === 'Invalid ticket.' || res.error === 'Your login ticket has expired (five minutes) or no ticket requested.') {
log.debug(
'api.ticket.loss',
{
type: 'api.ticket.loss',
error: res.error,
character: core.characters.ownCharacter?.name,
deltaToLastApiCall: Date.now() - lastFetch,
@ -146,8 +146,8 @@ export default class Connection implements Interfaces.Connection {
if(res.error !== '') {
log.debug(
'error.api.query',
{
type: 'error.api.query',
error: res.error,
endpoint,
character: core.characters.ownCharacter?.name,
@ -237,8 +237,8 @@ export default class Connection implements Interfaces.Connection {
console.log(`https://www.f-list.net/json/getApiTicket.php, gap: ${Date.now() - lastApiTicketFetch}ms`);
log.debug(
'api.getTicket',
{
type: 'api.getTicket',
character: core.characters.ownCharacter?.name,
deltaToLastApiCall: Date.now() - lastFetch,
deltaToLastApiTicket: Date.now() - lastApiTicketFetch
@ -257,8 +257,8 @@ export default class Connection implements Interfaces.Connection {
console.error('API Ticket Error', data.error);
log.error(
'error.api.getTicket',
{
type: 'error.api.getTicket',
character: core.characters.ownCharacter.name,
error: data.error,
deltaToLastApiCall: Date.now() - lastFetch,