Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export const timezone = 'Europe/Athens';
export const speed = {
minDelay: 5,
maxDelay: 15,
speedMethod: 'divide',
speedFactor: 60,
baseWpm: 200,
};

export const statuses = ['online', 'idle', 'dnd', 'offline'];
Expand Down
2 changes: 2 additions & 0 deletions src/events/message-create/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { reply as staggeredReply } from '@/utils/delay';
import { getTrigger } from '@/utils/triggers';
import { logTrigger, logIncoming, logReply } from '@/utils/log';
import logger from '@/lib/logger';
import { updateUserTyping } from '@/utils/global-state';

export const name = Events.MessageCreate;
export const once = false;
Expand Down Expand Up @@ -54,6 +55,7 @@ export async function execute(message: Message) {
const ctxId = isDM ? `dm:${author.id}` : guild.id;

logIncoming(ctxId, author.username, content);
updateUserTyping(author.id, content);

if (!(await canReply(ctxId))) return;

Expand Down
42 changes: 15 additions & 27 deletions src/utils/delay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,17 @@ import { speed as speedConfig } from '@/config';
import { sentences, normalize } from './tokenize-messages';
import { DMChannel, Message, TextChannel, ThreadChannel } from 'discord.js';
import logger from '@/lib/logger';
import state, { getUserWpm } from '@/utils/global-state';

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

function calculateDelay(text: string): number {
const { speedMethod, speedFactor } = speedConfig;

const length = text.length;
const baseSeconds = (() => {
switch (speedMethod) {
case 'multiply':
return length * speedFactor;
case 'add':
return length + speedFactor;
case 'divide':
return length / speedFactor;
case 'subtract':
return length - speedFactor;
default:
return length;
}
})();

const punctuationCount = text
.split(' ')
.filter((w) => /[.!?]$/.test(w)).length;
const extraMs = punctuationCount * 500;

const totalMs = baseSeconds * 1000 + extraMs;
return Math.max(totalMs, 100);
function calculateDelay(text: string, userId?: string): number {
const baseWpm = state.speed.baseWpm;
const userWpm = userId ? getUserWpm(userId) : baseWpm;
const wpm = (baseWpm + userWpm) / 2;
const words = text.split(/\s+/).filter(Boolean).length;
const ms = (words / wpm) * 60 * 1000;
return Math.min(ms, 14000);
}

export async function reply(message: Message, reply: string): Promise<void> {
Expand All @@ -48,6 +30,11 @@ export async function reply(message: Message, reply: string): Promise<void> {
const segments = normalize(sentences(reply));
let isFirst = true;

while (state.isTyping) {
await sleep(500);
}
state.isTyping = true;

for (const raw of segments) {
const text = raw.toLowerCase().trim().replace(/\.$/, '');
if (!text) continue;
Expand All @@ -58,7 +45,7 @@ export async function reply(message: Message, reply: string): Promise<void> {

try {
await channel.sendTyping();
await sleep(calculateDelay(text));
await sleep(calculateDelay(text, message.author.id));

if (isFirst && Math.random() < 0.5) {
await message.reply(text);
Expand All @@ -71,4 +58,5 @@ export async function reply(message: Message, reply: string): Promise<void> {
break;
}
}
state.isTyping = false;
}
48 changes: 48 additions & 0 deletions src/utils/global-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { speed as speedConfig } from '@/config';

export interface UserTypingStats {
lastTimestamp: number;
wpm: number;
}

export interface SpeedState {
baseWpm: number;
}

export interface GlobalState {
speed: SpeedState;
userTyping: Map<string, UserTypingStats>;
isTyping: boolean;
}

const state: GlobalState = {
speed: {
baseWpm: speedConfig.baseWpm,
},
userTyping: new Map(),
isTyping: false,
};

export default state;

export function updateUserTyping(userId: string, message: string) {
const now = Date.now();
const words = message.trim().split(/\s+/).filter(Boolean).length;
const prev = state.userTyping.get(userId);
if (prev) {
const deltaMin = (now - prev.lastTimestamp) / 60000;
if (deltaMin > 0) {
const wpm = words / deltaMin;
prev.wpm = wpm;
}
prev.lastTimestamp = now;
} else {
state.userTyping.set(userId, { lastTimestamp: now, wpm: state.speed.baseWpm });
}
}

export function getUserWpm(userId: string): number {
const stats = state.userTyping.get(userId);
return stats ? stats.wpm : state.speed.baseWpm;
}