Skip to content

Commit

Permalink
feat: presence (#345)
Browse files Browse the repository at this point in the history
* presence

* from event presence and refactoring

* refine presence api

* add tests and more comments

* sss

---------

Co-authored-by: SrIzan10 <[email protected]>
  • Loading branch information
jacoobes and SrIzan10 authored Dec 27, 2023
1 parent efe4939 commit 7458bef
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 18 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"author": "SernDevs",
"license": "MIT",
"dependencies": {
"callsites": "^3.1.0",
"iti": "^0.6.0",
"rxjs": "^7.8.0",
"ts-results-es": "^4.0.0"
Expand Down
16 changes: 15 additions & 1 deletion src/core/module-loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ import assert from 'assert';
import { createRequire } from 'node:module';
import type { ImportPayload, Wrapper } from '../types/core';
import type { Module } from '../types/core-modules';
import { existsSync } from 'fs';
import { fileURLToPath } from 'node:url';

export const shouldHandle = (path: string, fpath: string) => {
const newPath = new URL(fpath+extname(path), path).href;
return {
exists: existsSync(fileURLToPath(newPath)),
path: newPath
}
}

export type ModuleResult<T> = Promise<ImportPayload<T>>;

export type ModuleResult<T> = Promise<ImportPayload<T>>;
/**
* Import any module based on the absolute path.
* This can accept four types of exported modules
Expand Down Expand Up @@ -66,6 +76,7 @@ const isSkippable = (filename: string) => {
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
return filename[0] === '!' || !validExtensions.includes(extname(filename));
};

async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return {
Expand All @@ -74,6 +85,7 @@ async function deriveFileInfo(dir: string, file: string) {
base: basename(file),
};
}

async function* readPaths(dir: string): AsyncGenerator<string> {
try {
const files = await readdir(dir);
Expand Down Expand Up @@ -118,6 +130,8 @@ export function loadConfig(wrapper: Wrapper | 'file'): Wrapper {
eventsPath = makePath('events');
console.log('Events path is set to', eventsPath);
}


return {
defaultPrefix: config.defaultPrefix,
commands: commandsPath,
Expand Down
17 changes: 9 additions & 8 deletions src/core/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,19 @@ export function discordEvent<T extends keyof ClientEvents>(mod: {
});
}


/**
* @deprecated
*/
function prepareClassPlugins(c: Module) {
const [onEvent, initPlugins] = partitionPlugins(c.plugins);
c.plugins = initPlugins as InitPlugin[];
c.onEvent = onEvent as ControlPlugin[];
}
//
// Class modules:
// Can be refactored.
// Both implement singleton, could I make them inherit a singleton parent class?

/**
* @Experimental
* Will be refactored / changed in future
* @deprecated
* Will be removed in future
*/
export abstract class CommandExecutable<const Type extends CommandType = CommandType> {
abstract type: Type;
Expand All @@ -92,8 +93,8 @@ export abstract class CommandExecutable<const Type extends CommandType = Command
}

/**
* @Experimental
* Will be refactored in future
* @deprecated
* Will be removed in future
*/
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
Expand Down
70 changes: 70 additions & 0 deletions src/core/presences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { ActivitiesOptions } from "discord.js";
import type { IntoDependencies } from "../types/ioc";
import type { Emitter } from "./contracts/emitter";

type Status = 'online' | 'idle' | 'invisible' | 'dnd'
type PresenceReduce = (previous: Result) => Result;

export interface Result {
status?: Status;
afk?: boolean;
activities?: ActivitiesOptions[];
shardId?: number[];
repeat?: number | [Emitter, string];
onRepeat?: (previous: Result) => Result;
}

export type Config <T extends (keyof Dependencies)[]> =
{
inject?: [...T]
execute: (...v: IntoDependencies<T>) => Result;
};

/**
* A small wrapper to provide type inference.
* Create a Presence module which **MUST** be put in a file called presence.<language-extension>
* adjacent to the file where **Sern.init** is CALLED.
*/
export function module<T extends (keyof Dependencies)[]>
(conf: Config<T>) {
return conf;
}


/**
* Create a Presence body which can be either:
* - once, the presence is activated only once.
* - repeated, per cycle or event, the presence can be changed.
*/
export function of(root: Omit<Result, 'repeat' | 'onRepeat'>) {
return {
/**
* @example
* Presence
* .of({
* activities: [{ name: "deez nuts" }]
* }) //starts the presence with "deez nuts".
* .repeated(prev => {
* return {
* afk: true,
* activities: prev.activities?.map(s => ({ ...s, name: s.name+"s" }))
* };
* }, 10000)) //every 10 s, the callback sets the presence to the returned one.
*/
repeated: (onRepeat: PresenceReduce, repeat: number | [Emitter, string]) => {
return { repeat, onRepeat, ...root }
},
/**
* @example
* Presence
* .of({
* activities: [
* { name: "Chilling out" }
* ]
* })
* .once() // Sets the presence once, with what's provided in '.of()'
*/
once: () => root
};
}

46 changes: 46 additions & 0 deletions src/handlers/presence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { concatMap, from, interval, of, map, scan, startWith, fromEvent, take } from "rxjs"
import { Files } from "../core/_internal";
import * as Presence from "../core/presences";
import { Services } from "../core/ioc";
import assert from "node:assert";

type SetPresence = (conf: Presence.Result) => Promise<unknown>

const parseConfig = async (conf: Promise<Presence.Result>) => {
return conf.then(s => {
if('repeat' in s) {
const { onRepeat, repeat } = s;
assert(repeat !== undefined, "repeat option is undefined");
assert(onRepeat !== undefined, "onRepeat callback is undefined, but repeat exists");
const src$ = typeof repeat === 'number'
? interval(repeat)
: fromEvent(...repeat);
return src$
.pipe(scan(onRepeat, s),
startWith(s));
}
//take 1?
return of(s).pipe(take(1));
})
};

export const presenceHandler = (path: string, setPresence: SetPresence) => {
interface PresenceModule {
module: Presence.Config<(keyof Dependencies)[]>
}
const presence = Files
.importModule<PresenceModule>(path)
.then(({ module }) => {
//fetch services with the order preserved, passing it to the execute fn
const fetchedServices = Services(...module.inject ?? []);
return async () => module.execute(...fetchedServices);
})
const module$ = from(presence);
return module$.pipe(
//compose:.
//call the execute function, passing that result into parseConfig.
//concatMap resolves the promise, and passes it to the next concatMap.
concatMap(fn => parseConfig(fn())),
// subscribe to the observable parseConfig yields, and set the presence.
concatMap(conf => conf.pipe(map(setPresence))))
}
3 changes: 1 addition & 2 deletions src/handlers/ready-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ export function startReadyEvent(

const once = () => pipe(
first(),
ignoreElements()
)
ignoreElements())


function register<T extends Processed<AnyModule>>(
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export {
CommandExecutable,
} from './core/modules';

export * as Presence from './core/presences'

export {
useContainerRaw
} from './core/_internal'
Expand Down
22 changes: 16 additions & 6 deletions src/sern.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { handleCrash } from './handlers/_internal';
import callsites from 'callsites';
import { err, ok, Files } from './core/_internal';
import { merge } from 'rxjs';
import { Services } from './core/ioc';
Expand All @@ -7,6 +8,8 @@ import { eventsHandler } from './handlers/user-defined-events';
import { startReadyEvent } from './handlers/ready-event';
import { messageHandler } from './handlers/message-event';
import { interactionHandler } from './handlers/interaction-event';
import { presenceHandler } from './handlers/presence';
import { Client } from 'discord.js';

/**
* @since 1.0.0
Expand All @@ -31,14 +34,21 @@ export function init(maybeWrapper: Wrapper | 'file') {
if (wrapper.events !== undefined) {
eventsHandler(dependencies, Files.getFullPathTree(wrapper.events));
}
const initCallsite = callsites()[1].getFileName();
const presencePath = Files.shouldHandle(initCallsite!, "presence");
//Ready event: load all modules and when finished, time should be taken and logged
startReadyEvent(dependencies, Files.getFullPathTree(wrapper.commands)).add(() => {
const time = ((performance.now() - startTime) / 1000).toFixed(2);
dependencies[0].emit('modulesLoaded');
logger?.info({
message: `sern: registered all modules in ${time} s`,
startReadyEvent(dependencies, Files.getFullPathTree(wrapper.commands))
.add(() => {
const time = ((performance.now() - startTime) / 1000).toFixed(2);
dependencies[0].emit('modulesLoaded');
logger?.info({ message: `sern: registered all modules in ${time} s`, });
if(presencePath.exists) {
const setPresence = async (p: any) => {
return (dependencies[4] as Client).user?.setPresence(p);
}
presenceHandler(presencePath.path, setPresence).subscribe();
}
});
});

const messages$ = messageHandler(dependencies, wrapper.defaultPrefix);
const interactions$ = interactionHandler(dependencies);
Expand Down
57 changes: 57 additions & 0 deletions test/core/presence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it, vi } from 'vitest';
import { Presence } from '../../src';


// Example test suite for the module function
describe('module function', () => {
it('should return a valid configuration', () => {
const config: Presence.Config<['dependency1', 'dependency2']> = Presence.module({
inject: ['dependency1', 'dependency2'],
execute: vi.fn(),
});

expect(config).toBeDefined();
expect(config.inject).toEqual(['dependency1', 'dependency2']);
expect(typeof config.execute).toBe('function');
});
});


describe('of function', () => {
it('should return a valid presence configuration without repeat and onRepeat', () => {
const presenceConfig = Presence.of({
status: 'online',
afk: false,
activities: [{ name: 'Test Activity' }],
shardId: [1, 2, 3],
}).once();

expect(presenceConfig).toBeDefined();
//@ts-ignore Maybe fix?
expect(presenceConfig.repeat).toBeUndefined();
//@ts-ignore Maybe fix?
expect(presenceConfig.onRepeat).toBeUndefined();
expect(presenceConfig).toMatchObject({
status: 'online',
afk: false,
activities: [{ name: 'Test Activity' }],
shardId: [1, 2, 3],
});
});

it('should return a valid presence configuration with repeat and onRepeat', () => {
const onRepeatCallback = vi.fn();
const presenceConfig = Presence.of({
status: 'idle',
activities: [{ name: 'Another Test Activity' }],
}).repeated(onRepeatCallback, 5000);

expect(presenceConfig).toBeDefined();
expect(presenceConfig.repeat).toBe(5000);
expect(presenceConfig.onRepeat).toBe(onRepeatCallback);
expect(presenceConfig).toMatchObject({
status: 'idle',
activities: [{ name: 'Another Test Activity' }],
});
});
})
3 changes: 2 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ __metadata:
"@types/node": ^18.15.11
"@typescript-eslint/eslint-plugin": 5.58.0
"@typescript-eslint/parser": 5.59.1
callsites: ^3.1.0
discord.js: ^14.11.0
esbuild: ^0.17.0
eslint: 8.39.0
Expand Down Expand Up @@ -1186,7 +1187,7 @@ __metadata:
languageName: node
linkType: hard

"callsites@npm:^3.0.0":
"callsites@npm:^3.0.0, callsites@npm:^3.1.0":
version: 3.1.0
resolution: "callsites@npm:3.1.0"
checksum: 072d17b6abb459c2ba96598918b55868af677154bec7e73d222ef95a8fdb9bbf7dae96a8421085cdad8cd190d86653b5b6dc55a4484f2e5b2e27d5e0c3fc15b3
Expand Down

0 comments on commit 7458bef

Please sign in to comment.