Refactored with better posting rules

This commit is contained in:
Mr. Stallion 2019-06-07 14:31:42 -05:00
parent cf91fe1627
commit 75f099ce89
8 changed files with 224 additions and 135 deletions

View File

@ -70,14 +70,15 @@
@click="hideSearch"><i class="fas fa-times"></i></a>
</div>
<div class="auto-ads" v-show="isAutopostingAds()">
<h4>Auto-Posting Ads</h4>
<h4>{{l('admgr.activeHeader')}}</h4>
<div class="update">{{adAutoPostUpdate}}</div>
<div v-show="adAutoPostNextAd" class="next">
<h5>Coming Next</h5>
<h5>{{l('admgr.comingNext')}}</h5>
<div>{{(adAutoPostNextAd ? adAutoPostNextAd.substr(0, 50) : '')}}...</div>
</div>
<a class="btn btn-sm btn-outline-primary renew-autoposts" @click="renewAutoPosting()">{{l('admgr.renew')}}</a>
</div>
<div class="border-top messages" :class="isChannel(conversation) ? 'messages-' + conversation.mode : undefined" ref="messages"
@scroll="onMessagesScroll" style="flex:1;overflow:auto;margin-top:2px">
@ -117,15 +118,15 @@
<ul class="nav nav-pills send-ads-switcher" v-if="isChannel(conversation)"
style="position:relative;z-index:10;margin-right:5px">
<li class="nav-item">
<a href="#" :class="{active: !conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
<a href="#" :class="{active: !conversation.isSendingAds, disabled: (conversation.channel.mode != 'both') || (conversation.adManager.isActive())}"
class="nav-link" @click.prevent="setSendingAds(false)">{{l('channel.mode.chat')}}</a>
</li>
<li class="nav-item">
<a href="#" :class="{active: conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
<a href="#" :class="{active: conversation.isSendingAds, disabled: (conversation.channel.mode != 'both') || (conversation.adManager.isActive())}"
class="nav-link" @click.prevent="setSendingAds(true)">{{adsMode}}</a>
</li>
<li class="nav-item">
<a href="#" :class="{active: conversation.adState.active}" class="nav-link" @click="autoPostAds()">Auto-Post Ads</a>
<a href="#" :class="{active: conversation.adManager.isActive()}" class="nav-link toggle-autopost" @click="toggleAutoPostAds()">{{l('admgr.toggleAutoPost')}}</a>
</li>
</ul>
<div class="btn btn-sm btn-primary" v-show="!settings.enterSend" @click="sendButton">{{l('chat.send')}}</div>
@ -146,7 +147,7 @@
import {Keys} from '../keys';
import {BBCodeView, Editor} from './bbcode';
import CommandHelp from './CommandHelp.vue';
import { AdState, characterImage, getByteLength, getKey } from "./common";
import { characterImage, getByteLength, getKey } from "./common";
import ConversationSettings from './ConversationSettings.vue';
import core from './core';
import {Channel, channelModes, Character, Conversation, Settings} from './interfaces';
@ -191,8 +192,8 @@
adCountdown = 0;
adsMode = l('channel.mode.ads');
autoPostingUpdater = 0;
adAutoPostUpdate: string|null = null;
adAutoPostNextAd: string|null = null;
adAutoPostUpdate: string | null = null;
adAutoPostNextAd: string | null = null;
isChannel = Conversation.isChannel;
isPrivate = Conversation.isPrivate;
@ -236,7 +237,8 @@
setAdCountdown();
});
this.$watch('conversation.adState.active', () => (this.refreshAutoPostingTimer()));
this.$watch(() => this.conversation.adManager.isActive(), () => (this.refreshAutoPostingTimer()));
this.refreshAutoPostingTimer();
}
@Hook('destroyed')
@ -273,6 +275,7 @@
if(!anyDialogsShown) (<Editor>this.$refs['textBox']).focus();
this.$nextTick(() => setTimeout(() => this.messageView.scrollTop = this.messageView.scrollHeight));
this.scrolledDown = true;
this.refreshAutoPostingTimer();
}
@Watch('conversation.messages')
@ -399,78 +402,29 @@
isAutopostingAds(): boolean {
return this.conversation.adState.active;
return this.conversation.adManager.isActive();
}
clearAutoPostAds(): void {
if (this.conversation.adState.interval) {
clearTimeout(this.conversation.adState.interval);
}
this.conversation.adState = new AdState();
stopAutoPostAds(): void {
this.conversation.adManager.stop();
}
autoPostAds(): void {
renewAutoPosting(): void {
this.conversation.adManager.renew();
this.refreshAutoPostingTimer();
}
toggleAutoPostAds(): void {
if(this.isAutopostingAds()) {
this.clearAutoPostAds();
this.refreshAutoPostingTimer();
return;
this.stopAutoPostAds();
} else {
this.conversation.adManager.start();
}
const conversation = this.conversation;
/**** Do not use 'this' keyword below this line, it will operate differently than you expect ****/
const chanConv = (<Conversation.ChannelConversation>conversation);
const adState = conversation.adState;
const initialWait = Math.max(0, chanConv.nextAd - Date.now()) * 1.1;
adState.adIndex = 0;
const sendNextPost = async () => {
const ads = conversation.settings.adSettings.ads;
const index = (adState.adIndex || 0);
if ((ads.length === 0) || ((adState.expireDue) && (adState.expireDue.getTime() < Date.now()))) {
conversation.adState = new AdState();
return;
}
const msg = ads[index % ads.length];
await chanConv.sendAd(msg);
const nextInMs = Math.max(0, (chanConv.nextAd - Date.now())) * 1.1;
adState.adIndex = index + 1;
adState.nextPostDue = new Date(Date.now() + nextInMs);
adState.interval = setTimeout(
async () => {
await sendNextPost();
},
nextInMs
);
};
adState.active = true;
adState.nextPostDue = new Date(Date.now() + initialWait);
adState.expireDue = new Date(Date.now() + 2 * 60 * 60 * 1000);
adState.interval = setTimeout(
async () => {
adState.firstPost = new Date();
await sendNextPost();
},
initialWait
);
this.refreshAutoPostingTimer();
}
@ -487,25 +441,28 @@
}
const updateAutoPostingState = () => {
const adState = this.conversation.adState;
const ads = this.conversation.settings.adSettings.ads;
const adManager = this.conversation.adManager;
if(ads.length > 0) {
this.adAutoPostNextAd = ads[(adState.adIndex || 0) % ads.length];
this.adAutoPostNextAd = adManager.getNextAd() || null;
const diff = ((adState.nextPostDue || new Date()).getTime() - Date.now()) / 1000;
const expDiff = ((adState.expireDue || new Date()).getTime() - Date.now()) / 1000;
if(this.adAutoPostNextAd) {
const diff = ((adManager.getNextPostDue() || new Date()).getTime() - Date.now()) / 1000;
const expDiff = ((adManager.getExpireDue() || new Date()).getTime() - Date.now()) / 1000;
if((adState.nextPostDue) && (!adState.firstPost)) {
this.adAutoPostUpdate = `Posting beings in ${Math.floor(diff / 60)}m ${Math.floor(diff % 60)}s`;
} else {
this.adAutoPostUpdate = `Next ad in ${Math.floor(diff / 60)}m ${Math.floor(diff % 60)}s`;
}
const diffMins = Math.floor(diff / 60);
const diffSecs = Math.floor(diff % 60);
const expDiffMins = Math.floor(expDiff / 60);
const expDiffSecs = Math.floor(expDiff % 60);
this.adAutoPostUpdate += `, auto-posting expires in ${Math.floor(expDiff / 60)}m ${Math.floor(expDiff % 60)}s`;
this.adAutoPostUpdate = l(
((adManager.getNextPostDue()) && (!adManager.getFirstPost())) ? 'admgr.postingBegins' : 'admgr.nextPostDue',
diffMins,
diffSecs
) + l('admgr.expiresIn', expDiffMins, expDiffSecs);
} else {
this.adAutoPostNextAd = null;
this.adAutoPostUpdate = 'No ads have been set up -- auto-posting will be cancelled.';
this.adAutoPostUpdate = l('admgr.noAds');
}
};
@ -514,7 +471,6 @@
updateAutoPostingState();
}
hasSFC(message: Conversation.Message): message is Conversation.SFCMessage {
return (<Partial<Conversation.SFCMessage>>message).sfc !== undefined;
}
@ -559,6 +515,12 @@
padding: 3px 10px;
}
.toggle-autopost {
margin-left: 1px;
}
.auto-ads {
background-color: rgba(255, 128, 32, 0.8);
padding-left: 10px;
@ -566,6 +528,29 @@
padding-top: 5px;
padding-bottom: 5px;
margin: 0;
position: relative;
.renew-autoposts {
display: block;
float: right;
/* margin-top: auto; */
/* margin-bottom: auto; */
position: absolute;
/* bottom: 1px; */
right: 10px;
top: 50%;
transform: translateY(-50%);
border-color: rgba(255, 255, 255, 0.5);
color: rgba(255, 255, 255, 0.9);
&:hover {
background-color: rgba(255, 255, 255, 0.3);
}
&:active {
background-color: rgba(255, 255, 255, 0.6);
}
}
h4 {
font-size: 1.1rem;

View File

@ -1,7 +1,7 @@
import {WebSocketConnection} from '../fchat';
export default class Socket implements WebSocketConnection {
static host = 'wss://chat.f-list.net:9799';
static host = 'wss://chat.f-list.net/chat2';
private socket: WebSocket;
private lastHandler: Promise<void> = Promise.resolve();

120
chat/ad-manager.ts Normal file
View File

@ -0,0 +1,120 @@
import { Conversation } from './interfaces';
export class AdManager {
static readonly POSTING_PERIOD = 3 * 60 * 60 * 1000;
static readonly START_VARIANCE = 3 * 60 * 1000;
static readonly POST_VARIANCE = 10 * 60 * 1000;
static readonly POST_DELAY = 2 * 60 * 1000;
private conversation: Conversation;
private adIndex = 0;
private active = false;
private nextPostDue?: Date;
private expireDue?: Date;
private firstPost?: Date;
private interval?: any;
constructor(conversation: Conversation) {
this.conversation = conversation;
}
isActive(): boolean {
return this.active;
}
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 chanConv.sendAd(msg);
// 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(Date.now() + nextInMs);
this.interval = setTimeout(
async() => {
await this.sendNextPost();
},
nextInMs
);
}
getAds(): string[] {
return this.conversation.settings.adSettings.ads;
}
getNextAd(): string | undefined {
const ads = this.getAds();
if (ads.length === 0)
return;
return ads[this.adIndex % ads.length];
}
getNextPostDue(): Date | undefined {
return this.nextPostDue;
}
getExpireDue(): Date | undefined {
return this.expireDue;
}
getFirstPost(): Date | undefined {
return this.firstPost;
}
start(): void {
const chanConv = (<Conversation.ChannelConversation>this.conversation);
const initialWait = Math.max(Math.random() * AdManager.START_VARIANCE, (chanConv.nextAd - Date.now()) * 1.1);
this.adIndex = 0;
this.active = true;
this.nextPostDue = new Date(Date.now() + initialWait);
this.expireDue = new Date(Date.now() + AdManager.POSTING_PERIOD);
this.interval = setTimeout(
async() => {
this.firstPost = new Date();
await this.sendNextPost();
},
initialWait
);
}
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);
}
}

View File

@ -50,16 +50,6 @@ export class AdSettings implements Conversation.AdSettings {
}
export class AdState implements Conversation.AdState {
active = false;
firstPost?: Date = undefined;
nextPostDue?: Date = undefined;
interval?: any = undefined;
adIndex?: number = undefined;
expireDue?: Date = undefined;
}
export class ConversationSettings implements Conversation.Settings {
notify = Conversation.Setting.Default;
highlight = Conversation.Setting.Default;

View File

@ -1,6 +1,7 @@
import {queuedJoin} from '../fchat/channels';
import {decodeHTML} from '../fchat/common';
import { AdState, characterImage, ConversationSettings, EventMessage, Message, messageToString } from './common';
import { AdManager } from './ad-manager';
import { characterImage, ConversationSettings, EventMessage, Message, messageToString } from './common';
import core from './core';
import {Channel, Character, Conversation as Interfaces} from './interfaces';
import l from './localize';
@ -30,14 +31,15 @@ abstract class Conversation implements Interfaces.Conversation {
infoText = '';
abstract readonly maxMessageLength: number | undefined;
_settings: Interfaces.Settings | undefined;
_adState: Interfaces.AdState | undefined;
protected abstract context: CommandContext;
protected maxMessages = 50;
protected allMessages: Interfaces.Message[] = [];
readonly reportMessages: Interfaces.Message[] = [];
private lastSent = '';
adManager: AdManager;
constructor(readonly key: string, public _isPinned: boolean) {
this.adManager = new AdManager(this);
}
get settings(): Interfaces.Settings {
@ -50,17 +52,6 @@ abstract class Conversation implements Interfaces.Conversation {
state.setSettings(this.key, value); //tslint:disable-line:no-floating-promises
}
get adState(): Interfaces.AdState {
//tslint:disable-next-line:strict-boolean-expressions
return this._adState || (this._adState = state.adStates[this.key] || new AdState());
}
set adState(value: Interfaces.AdState) {
this._adState = value;
state.setAdState(this.key, value); //tslint:disable-line:no-floating-promises
}
get isPinned(): boolean {
return this._isPinned;
}
@ -200,12 +191,11 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
return;
}
if(this.adState.active) {
if(this.adManager.isActive()) {
this.errorText = 'Cannot send ads manually while ad auto-posting is active';
return;
}
core.connection.send('PRI', {recipient: this.name, message: this.enteredText});
const message = createMessage(MessageType.Message, core.characters.ownCharacter, this.enteredText);
this.safeAddMessage(message);
@ -331,7 +321,7 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
protected async doSend(): Promise<void> {
const isAd = this.isSendingAds;
if(this.adState.active) {
if(this.adManager.isActive()) {
this.errorText = 'Cannot post ads manually while ad auto-posting is active';
return;
}
@ -349,7 +339,6 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
else this.clearText();
}
async sendAd(text: string): Promise<void> {
if (text.length < 1)
return;
@ -399,7 +388,6 @@ class State implements Interfaces.State {
recentChannels: Interfaces.RecentChannelConversation[] = [];
pinned!: {channels: string[], private: string[]};
settings!: {[key: string]: Interfaces.Settings};
adStates: {[key: string]: Interfaces.AdState} = {};
modes!: {[key: string]: Channel.Mode | undefined};
windowFocused = document.hasFocus();
@ -444,12 +432,6 @@ class State implements Interfaces.State {
await core.settingsStore.set('conversationSettings', this.settings);
}
setAdState(key: string, value: Interfaces.AdState): void {
this.adStates[key] = value;
}
show(conversation: Conversation): void {
this.selectedConversation.onHide();
conversation.unread = Interfaces.UnreadState.None;

View File

@ -2,6 +2,7 @@
import {Connection} from '../fchat';
import {Channel, Character} from '../fchat/interfaces';
import { AdManager } from './ad-manager';
export {Connection, Channel, Character} from '../fchat/interfaces';
export const userStatuses: ReadonlyArray<Character.Status> = ['online', 'looking', 'away', 'busy', 'dnd'];
export const channelModes: ReadonlyArray<Channel.Mode> = ['chat', 'ads', 'both'];
@ -98,22 +99,10 @@ export namespace Conversation {
readonly adSettings: AdSettings;
}
export interface AdSettings {
readonly ads: string[];
}
export interface AdState {
active: boolean;
firstPost?: Date;
nextPostDue?: Date;
expireDue?: Date;
interval?: any;
adIndex?: number;
}
export const enum UnreadState { None, Unread, Mention }
export interface Conversation {
@ -127,7 +116,7 @@ export namespace Conversation {
readonly key: string
readonly unread: UnreadState
settings: Settings
adState: AdState
readonly adManager: AdManager;
send(): Promise<void>
clear(): void
loadLastSent(): void

View File

@ -17,6 +17,14 @@ const strings: {[key: string]: string | undefined} = {
'action.updateAvailable': 'UPDATE AVAILABLE',
'action.update': 'Restart now!',
'action.cancel': 'Cancel',
'admgr.postingBegins': 'Posting beings in {0}m {1}s',
'admgr.nextPostDue': 'Next ad in {0}m {1}s',
'admgr.expiresIn': ', auto-posting expires in {0}m {1}s',
'admgr.noAds': 'No ads have been set up -- auto-posting will be cancelled.',
'admgr.activeHeader': 'Auto-Posting Ads',
'admgr.comingNext': 'Coming Next',
'admgr.renew': 'Renew',
'admgr.toggleAutoPost': 'Auto-Post Ads',
'consoleWarning.head': 'THIS IS THE DANGER ZONE.',
'consoleWarning.body': `ANYTHING YOU WRITE OR PASTE IN HERE COULD BE USED TO STEAL YOUR PASSWORDS OR TAKE OVER YOUR ENTIRE COMPUTER. This is where happiness goes to die. If you aren't a developer or a special kind of daredevil, please get out of here!`,
'help': 'Help',

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"sourceMap": true,
"allowJs": true,
"noEmitHelpers": true,
"importHelpers": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["./electron/main.ts"]
}