Auto-posting
This commit is contained in:
parent
a5e57cd52c
commit
d4d4db89c9
|
@ -2,4 +2,6 @@ node_modules/
|
|||
/electron/app
|
||||
/electron/dist
|
||||
/mobile/www
|
||||
/webchat/dist
|
||||
/webchat/dist
|
||||
|
||||
.idea/workspace.xml
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue