Auto-posting

This commit is contained in:
Mr. Stallion 2019-06-02 18:57:32 -05:00
parent a5e57cd52c
commit d4d4db89c9
6 changed files with 284 additions and 6 deletions

4
.gitignore vendored
View File

@ -2,4 +2,6 @@ node_modules/
/electron/app
/electron/dist
/mobile/www
/webchat/dist
/webchat/dist
.idea/workspace.xml

View File

@ -35,6 +35,12 @@
<option :value="setting.False">{{l('conversationSettings.false')}}</option>
</select>
</div>
<div class="form-group" v-for="(ad, index) in ads">
<label :for="'ad' + conversation.key + '-' + index" class="control-label">Channel Auto-Posting Ad #{{(index + 1)}}</label>
<input :id="'ad' + conversation.key + '-' + index" class="form-control" v-model="ads[index]" />
</div>
<button class="btn" @click="addAd()">Add Auto-Posting Ad</button>
</modal>
</template>
@ -58,6 +64,7 @@
highlightWords!: string;
joinMessages!: Conversation.Setting;
defaultHighlights!: boolean;
ads!: string[];
load(): void {
const settings = this.conversation.settings;
@ -66,16 +73,29 @@
this.highlightWords = settings.highlightWords.join(',');
this.joinMessages = settings.joinMessages;
this.defaultHighlights = settings.defaultHighlights;
this.ads = settings.adSettings.ads.slice(0);
if (this.ads.length === 0) {
this.ads.push('');
}
}
submit(): void {
this.conversation.settings = {
notify: this.notify,
highlight: this.highlight,
highlightWords: this.highlightWords.split(',').map((x) => x.trim()).filter((x) => x.length),
highlightWords: this.highlightWords.split(',').map((x) => x.trim()).filter((x) => (x.length > 0)),
joinMessages: this.joinMessages,
defaultHighlights: this.defaultHighlights
defaultHighlights: this.defaultHighlights,
adSettings: {
ads: this.ads.filter((ad: string) => (ad.length > 0))
}
};
}
addAd(): void {
this.ads.push('');
}
}
</script>

View File

@ -69,6 +69,16 @@
<a class="btn btn-sm btn-light" style="position:absolute;right:5px;top:50%;transform:translateY(-50%);line-height:0;z-index:10"
@click="hideSearch"><i class="fas fa-times"></i></a>
</div>
<div class="auto-ads" v-show="isAutopostingAds()">
<h4>Auto-Posting Ads</h4>
<div class="update">{{adAutoPostUpdate}}</div>
<div v-show="adAutoPostNextAd" class="next">
<h5>Coming Next</h5>
<div>{{(adAutoPostNextAd ? adAutoPostNextAd.substr(0, 50) : '')}}...</div>
</div>
</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">
<template v-for="message in messages">
@ -114,6 +124,9 @@
<a href="#" :class="{active: conversation.isSendingAds, disabled: conversation.channel.mode != 'both'}"
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>
</li>
</ul>
<div class="btn btn-sm btn-primary" v-show="!settings.enterSend" @click="sendButton">{{l('chat.send')}}</div>
</div>
@ -133,7 +146,7 @@
import {Keys} from '../keys';
import {BBCodeView, Editor} from './bbcode';
import CommandHelp from './CommandHelp.vue';
import {characterImage, getByteLength, getKey} from './common';
import { AdState, characterImage, getByteLength, getKey } from "./common";
import ConversationSettings from './ConversationSettings.vue';
import core from './core';
import {Channel, channelModes, Character, Conversation, Settings} from './interfaces';
@ -177,6 +190,9 @@
ignoreScroll = false;
adCountdown = 0;
adsMode = l('channel.mode.ads');
autoPostingUpdater = 0;
adAutoPostUpdate: string|null = null;
adAutoPostNextAd: string|null = null;
isChannel = Conversation.isChannel;
isPrivate = Conversation.isPrivate;
@ -219,6 +235,8 @@
this.adCountdown = window.setInterval(setAdCountdown, 1000);
setAdCountdown();
});
this.$watch('conversation.adState.active', () => (this.refreshAutoPostingTimer()));
}
@Hook('destroyed')
@ -227,6 +245,8 @@
window.removeEventListener('keydown', this.keydownHandler);
window.removeEventListener('keypress', this.keypressHandler);
clearInterval(this.searchTimer);
clearInterval(this.autoPostingUpdater);
clearInterval(this.adCountdown);
}
hideSearch(): void {
@ -377,6 +397,124 @@
(<ManageChannel>this.$refs['manageDialog']).show();
}
isAutopostingAds(): boolean {
return this.conversation.adState.active;
}
clearAutoPostAds(): void {
if (this.conversation.adState.interval) {
clearTimeout(this.conversation.adState.interval);
}
this.conversation.adState = new AdState();
}
autoPostAds(): void {
if(this.isAutopostingAds()) {
this.clearAutoPostAds();
this.refreshAutoPostingTimer();
return;
}
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();
}
refreshAutoPostingTimer() {
if (this.autoPostingUpdater) {
window.clearInterval(this.autoPostingUpdater);
}
if(this.isAutopostingAds() === false) {
this.adAutoPostUpdate = null;
this.adAutoPostNextAd = null;
return;
}
const updateAutoPostingState = () => {
const adState = this.conversation.adState;
const ads = this.conversation.settings.adSettings.ads;
if(ads.length > 0) {
this.adAutoPostNextAd = ads[(adState.adIndex || 0) % ads.length];
const diff = ((adState.nextPostDue || new Date()).getTime() - Date.now()) / 1000;
const expDiff = ((adState.expireDue || 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`;
}
this.adAutoPostUpdate += `, auto-posting expires in ${Math.floor(expDiff / 60)}m ${Math.floor(expDiff % 60)}s`;
} else {
this.adAutoPostNextAd = null;
this.adAutoPostUpdate = 'No ads have been set up -- auto-posting will be cancelled.';
}
};
this.autoPostingUpdater = window.setInterval(updateAutoPostingState, 1000);
updateAutoPostingState();
}
hasSFC(message: Conversation.Message): message is Conversation.SFCMessage {
return (<Partial<Conversation.SFCMessage>>message).sfc !== undefined;
}
@ -421,6 +559,39 @@
padding: 3px 10px;
}
.auto-ads {
background-color: rgba(255, 128, 32, 0.8);
padding-left: 10px;
padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
margin: 0;
h4 {
font-size: 1.1rem;
margin: 0;
line-height: 100%;
}
.update {
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
}
.next {
margin-top: 0.5rem;
color: rgba(255, 255, 255, 0.4);
font-size: 11px;
h5 {
font-size: 0.8rem;
margin: 0;
line-height: 100%;
}
}
}
@media (max-width: breakpoint-max(sm)) {
.mode-switcher a {
padding: 5px 8px;

View File

@ -44,12 +44,29 @@ export class Settings implements ISettings {
bbCodeBar = true;
}
export class AdSettings implements Conversation.AdSettings {
ads: string[] = [];
}
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;
highlightWords: string[] = [];
joinMessages = Conversation.Setting.Default;
defaultHighlights = true;
adSettings: Conversation.AdSettings = { ads: [] };
}
function pad(num: number): string | number {

View File

@ -1,6 +1,6 @@
import {queuedJoin} from '../fchat/channels';
import {decodeHTML} from '../fchat/common';
import {characterImage, ConversationSettings, EventMessage, Message, messageToString} from './common';
import { AdState, characterImage, ConversationSettings, EventMessage, Message, messageToString } from './common';
import core from './core';
import {Channel, Character, Conversation as Interfaces} from './interfaces';
import l from './localize';
@ -30,6 +30,7 @@ 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[] = [];
@ -49,6 +50,17 @@ 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;
}
@ -65,6 +77,7 @@ abstract class Conversation implements Interfaces.Conversation {
async send(): Promise<void> {
if(this.enteredText.length === 0) return;
if(isCommand(this.enteredText)) {
const parsed = parseCommand(this.enteredText, this.context);
if(typeof parsed === 'string') this.errorText = parsed;
@ -186,6 +199,13 @@ class PrivateConversation extends Conversation implements Interfaces.PrivateConv
this.errorText = l('chat.errorIgnored', this.character.name);
return;
}
if(this.adState.active) {
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);
@ -310,7 +330,17 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
protected async doSend(): Promise<void> {
const isAd = this.isSendingAds;
if(isAd && Date.now() < this.nextAd) return;
if(this.adState.active) {
this.errorText = 'Cannot post ads manually while ad auto-posting is active';
return;
}
if(isAd && Date.now() < this.nextAd) {
this.errorText = 'You must wait at least ten minutes between ad posts on this channel';
return;
}
core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message: this.enteredText});
await this.addMessage(
createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, this.enteredText, new Date()));
@ -318,6 +348,18 @@ class ChannelConversation extends Conversation implements Interfaces.ChannelConv
this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000;
else this.clearText();
}
async sendAd(text: string): Promise<void> {
if (text.length < 1)
return;
await this.addMessage(
createMessage(MessageType.Ad, core.characters.ownCharacter, text, new Date())
);
this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000;
}
}
class ConsoleConversation extends Conversation {
@ -357,6 +399,7 @@ 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();
@ -401,6 +444,12 @@ 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

@ -62,6 +62,7 @@ export namespace Conversation {
mode: Channel.Mode
readonly nextAd: number
isSendingAds: boolean
sendAd(text: string): Promise<void>
}
export function isPrivate(conversation: Conversation): conversation is PrivateConversation {
@ -94,8 +95,25 @@ export namespace Conversation {
readonly highlightWords: ReadonlyArray<string>;
readonly joinMessages: Setting;
readonly defaultHighlights: boolean;
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 {
@ -109,6 +127,7 @@ export namespace Conversation {
readonly key: string
readonly unread: UnreadState
settings: Settings
adState: AdState
send(): Promise<void>
clear(): void
loadLastSent(): void