Archived
0
0
Fork 0

Compare commits

...

9 commits

18 changed files with 148 additions and 564 deletions

72
.drone.yml Normal file
View file

@ -0,0 +1,72 @@
kind: pipeline
type: docker
name: default
steps:
- name: install
image: node:14
commands:
- echo ===== INSTALLING DEPENDENCIES =====
- yarn install
- name: build
image: node:14
commands:
- echo ===== BUILDING APPLICATION =====
- yarn build
- name: publish-dev
image: node:14
commands:
- echo ===== PUBLISHING APPLICATION =====
- echo "//registry.npmjs.org/:_authToken=$${NPM_TOKEN}" > ~/.npmrc
- yarn version $${(node -pe \'require("./package.json").version\')}-$${(git rev-parse HEAD)}.$${(date +%s)}
- npm publish --tag dev --access public
environment:
NPM_TOKEN:
from_secret: GITEA_TOKEN
when:
branch:
- master
- refactor/rewrite
event:
- push
- name: publish-stable
image: node:14
commands:
- echo ===== PUBLISHING APPLICATION =====
- yarn semantic-release
environment:
GITEA_URL: "https://code.relms.dev/NeonJS/library"
GITEA_TOKEN:
from_secret: GITEA_TOKEN
NPM_TOKEN:
from_secret: GITEA_TOKEN
GIT_AUTHOR_NAME: "DroneCI"
GIT_AUTHOR_EMAIL: "drone@relms.dev"
GIT_COMMITTER_NAME: "DroneCI"
GIT_COMMITTER_EMAIL: "drone@relms.dev"
when:
branch:
- stable
event:
- push
- name: discord-notification
image: appleboy/drone-discord
settings:
webhook_id:
from_secret: DISCORD_WEBHOOK_ID
webhook_token:
from_secret: DISCORD_WEBHOOK_TOKEN
message: >
{{#success build.status}}
Build {{build.number}} succeeded.
{{else}}
Build {{build.number}} failed.
{{/success}}
when:
status:
- success
- failure

2
.gitignore vendored
View file

@ -674,4 +674,4 @@ DerivedData/
# ---> Project # ---> Project
!.yarn/releases !.yarn/releases
types /types

103
Jenkinsfile vendored
View file

@ -1,103 +0,0 @@
pipeline {
agent {
docker {
image 'node:14'
}
}
environment {
GITEA_URL = 'https://code.relms.dev/NeonJS/library'
GITEA_TOKEN = credentials('GITEA_TOKEN')
NPM_TOKEN = credentials('NPM_TOKEN')
DISCORD_WEBHOOK = credentials('DISCORD_WEBHOOK')
GIT_AUTHOR_NAME = 'JenkinsCI'
GIT_AUTHOR_EMAIL = 'jenkins@relms.dev'
GIT_COMMITTER_NAME = 'JenkinsCI'
GIT_COMMITTER_EMAIL = 'jenkins@relms.dev'
}
stages {
stage('Checkout') {
steps {
script {
echo '==========Checking out=========='
if (sh (script: "git log -1 | grep '.*\\[ci skip\\].*'", returnStatus: true)) {
error "'[ci skip]' found in git commit message. Aborting."
currentBuild.result = 'NOT_BUILT'
}
}
}
}
stage('Dependencies') {
steps {
echo '==========Installing Dependencies=========='
sh 'yarn install'
}
}
stage('Build') {
steps {
echo '==========Bundling Application=========='
sh 'yarn build'
}
}
stage('Publish') {
steps {
script {
echo '==========Publishing to NPM=========='
if (env.BRANCH_NAME == 'master') {
echo '==========Publishing as Development Version=========='
sh 'echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc'
sh 'yarn version $(node -pe \'require("./package.json").version\')-$(git rev-parse HEAD).$(date +%s)'
sh 'npm publish --tag dev --access public'
} else if (env.BRANCH_NAME == 'stable') {
echo '==========Publishing as Stable Version=========='
sh 'semantic-release'
}
}
}
}
}
post {
always {
deleteDir()
cleanWs()
}
success {
sh 'tar -cf - dist/ | xz -9 -c - > neonjs-library.tar.xz'
archiveArtifacts artifacts: 'neonjs-library.tar.xz', fingerprint: true
discordSend description: 'CI build was a Success',
link: env.BUILD_URL,
result: 'SUCCESS',
title: env.JOB_NAME,
webhookURL: env.DISCORD_WEBHOOK
}
unstable {
sh 'tar -cf - dist/ | xz -9 -c - > neonjs-library.tar.xz'
archiveArtifacts artifacts: 'neonjs-library.tar.xz', fingerprint: true
discordSend description: 'CI build was Unstable.',
link: env.BUILD_URL,
result: 'UNSTABLE',
title: env.JOB_NAME,
webhookURL: env.DISCORD_WEBHOOK
}
failure {
discordSend description: 'CI build has Failed.',
link: env.BUILD_URL,
result: 'FAILURE',
title: env.JOB_NAME,
webhookURL: env.DISCORD_WEBHOOK
}
aborted {
discordSend description: 'CI build was Aborted',
link: env.BUILD_URL,
result: 'ABORTED',
title: env.JOB_NAME,
webhookURL: env.DISCORD_WEBHOOK
}
}
}

View file

@ -1,7 +1,7 @@
import { ApiManager } from './apiManager'; import { ApiManager } from './apiManager';
import type { Client } from '../client/client'; import type { Client } from '../client/client';
import type { IRequestOptions } from '../utils/types'; import type { IRequestOptions } from '../utils/types/api';
export class ApiClient { export class ApiClient {
public client: Client; public client: Client;

View file

@ -2,10 +2,11 @@ import { AsyncQueue } from '@sapphire/async-queue';
import { AbortController } from 'abort-controller'; import { AbortController } from 'abort-controller';
import FormData from 'form-data'; import FormData from 'form-data';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import type { Client } from '../client/client';
import { sleep } from '../utils/sleep'; import { sleep } from '../utils/sleep';
import type { IMakeRequestOptions, IRouteIdentifier } from '../utils/types';
import type { ApiManager } from './apiManager'; import type { ApiManager } from './apiManager';
import type { Client } from '../client/client';
import type { IMakeRequestOptions, IRouteIdentifier } from '../utils/types/api';
function calculateReset(reset: number, resetAfter: number, serverDate: number): number { function calculateReset(reset: number, resetAfter: number, serverDate: number): number {

View file

@ -1,7 +1,6 @@
import { ApiClient } from './apiClient'; import { ApiClient } from './apiClient';
import type { Client } from '../client/client'; import type { Client } from '../client/client';
import type { IApiCreateMessage, IApiCreateSlashCommand, IApiUser, IFile } from '../utils/types';
export class ApiHelper { export class ApiHelper {
public apiClient: ApiClient; public apiClient: ApiClient;
@ -15,36 +14,4 @@ export class ApiHelper {
this.apiClient = new ApiClient(this.client, this._token); this.apiClient = new ApiClient(this.client, this._token);
} }
// TODO: Return message object
public async createMessage(channelId: string, options: IApiCreateMessage, files?: IFile[]): Promise<void> {
await this.apiClient.post({
path: `/channels/${channelId}/messages`,
requireAuth: true,
body: options,
files: files,
});
}
public async getCurrentUser(): Promise<IApiUser> {
return await this.apiClient.get<IApiUser>({
path: '/users/@me',
requireAuth: true,
});
}
public async createSlashCommand(options: IApiCreateSlashCommand): Promise<void> {
await this.apiClient.post({
path: `/applications/${this.client.user.id}/commands`,
requireAuth: true,
body: options,
});
}
public async getUser(id: string): Promise<IApiUser> {
return await this.apiClient.get<IApiUser>({
path: `/users/${id}`,
requireAuth: true,
});
}
} }

View file

@ -2,8 +2,7 @@ import { Snowflake } from '../utils/snowflake';
import { ApiHandler } from './apiHandler'; import { ApiHandler } from './apiHandler';
import type { Client } from '../client/client'; import type { Client } from '../client/client';
import type { ApiMethods, IMakeRequestOptions, IRouteIdentifier } from '../utils/types'; import type { ApiMethods, IMakeRequestOptions, IRouteIdentifier } from '../utils/types/api';
export class ApiManager { export class ApiManager {
public client: Client; public client: Client;

View file

@ -1,17 +1,14 @@
import _ from 'lodash'; import _ from 'lodash';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { ApiHelper } from '../api/apiHelper'; import { ApiHelper } from '../api/apiHelper';
import { ClientUser } from '../structures/clientUser';
import { GatewayClient } from '../gateway/gatewayClient';
import { defaults } from '../utils/defaults'; import { defaults } from '../utils/defaults';
import type { DeepRequired, IClientOptions } from '../utils/types'; import type { IClientOptions } from '../utils/types/client';
import type { DeepRequired } from '../utils/types/common';
export class Client extends EventEmitter { export class Client extends EventEmitter {
public readonly api: ApiHelper; public readonly api: ApiHelper;
public readonly options: DeepRequired<IClientOptions>; public readonly options: DeepRequired<IClientOptions>;
public readonly user: ClientUser;
public readonly ws: GatewayClient;
private _token: string; private _token: string;
@ -22,11 +19,5 @@ export class Client extends EventEmitter {
this._token = token; this._token = token;
this.api = new ApiHelper(this, this._token); this.api = new ApiHelper(this, this._token);
this.user = new ClientUser(this);
this.ws = new GatewayClient(this, this._token);
}
public async login(): Promise<void> {
await this.ws.connect();
} }
} }

View file

@ -1,91 +0,0 @@
import zlib from 'fast-zlib';
import os from 'os';
import WebSocket from 'ws';
import type { Client } from '../client/client';
import type { GatewayReceiveMessage, GatewaySendMessage } from '../utils/types';
export class GatewayClient {
public client: Client;
public connection: WebSocket | null;
public inflate: zlib.Inflate;
private _heartbeatInterval: number;
private _heartbeatIntervalTimer: NodeJS.Timer | null;
private _sequence: number;
private _token: string;
public constructor(client: Client, token: string) {
this.client = client;
this.connection = null;
this.inflate = new zlib.Inflate();
this._heartbeatInterval = 0;
this._heartbeatIntervalTimer= null;
this._sequence = 0;
this._token= token;
}
public async close(): Promise<void> {
if (!this.connection) throw new Error('You are not connected to the Discord WebSocket Gateway!');
this.connection.close(1000);
if (this._heartbeatIntervalTimer) clearTimeout(this._heartbeatIntervalTimer);
}
public async connect(): Promise<void> {
if (this.connection) throw new Error('You are already connected to the Discord WebSocket Gateway!');
this.connection = new WebSocket(
`${this.client.options.ws.url}/?v=${this.client.options.ws.version}${this.client.options.ws.compression ? '&compress=zlib-stream' : ''}&encoding=${this.client.options.ws.encoding}`,
);
this.connection.on('message', async (msg: Buffer | string) => {
let parsedMessage: GatewayReceiveMessage;
if (this.client.options.ws.compression && typeof msg === 'object') parsedMessage = JSON.parse(this.inflate.process(msg).toString('utf8'));
else if (!this.client.options.ws.compression && typeof msg === 'string') parsedMessage = JSON.parse(msg);
else parsedMessage = JSON.parse(msg.toString('utf8'));
// if (parsedMessage.s) this._sequence = parsedMessage.s;
this.client.emit('raw', parsedMessage);
switch (parsedMessage.op) {
case 10:
this._heartbeatInterval = parsedMessage.d.heartbeat_interval;
this.send({
op: 2, d: {
compress: this.client.options.ws.compression,
intents: this.client.options.ws.intents,
large_threshold: this.client.options.ws.largeThreshold,
presence: this.client.options.presence,
properties: {
$browser: '@neonjs/library',
$device: '@neonjs/library',
$os: os.platform(),
},
token: this._token,
},
});
this._heartbeatIntervalTimer = setInterval(() => this._sendHeartbeat(), this._heartbeatInterval * 1000);
break;
default:
break;
}
});
}
public send(data: GatewaySendMessage): void {
if (!this.connection) throw new Error('You are not connected to the Discord WebSocket Gateway!');
this.connection.send(JSON.stringify(data));
}
private _sendHeartbeat(): void {
this.send({ op: 1, d: this._sequence });
}
}

View file

@ -6,4 +6,3 @@ export * from './client/client';
export * from './utils/defaults'; export * from './utils/defaults';
export * from './utils/sleep'; export * from './utils/sleep';
export * from './utils/snowflake'; export * from './utils/snowflake';
export * from './utils/types';

View file

@ -1,9 +0,0 @@
import { User } from './user';
import type { Client } from '../client/client';
export class ClientUser extends User {
public constructor(client: Client) {
super(client);
}
}

View file

@ -1,18 +0,0 @@
import type { IApiUser } from '..';
import type { Client } from '../client/client';
export class User {
public client: Client;
public id: string;
private _options: IApiUser;
public constructor(client: Client, options: IApiUser) {
this.client = client;
this._options = options;
}
public async fetch(id: string): Promise<IApiUser> {
return this.client.api.getUser(id);
}
}

View file

@ -1,51 +0,0 @@
import cacheManager from 'cache-manager';
import redisStore from 'cache-manager-ioredis';
import type { Client } from '../client/client';
export class CacheManager {
public client: Client;
private readonly _cacheManager: cacheManager.Cache;
public constructor(client: Client) {
this.client = client;
if (this.client.options.cache.cache === 'redis') this._cacheManager = cacheManager.caching({
store: redisStore,
ttl: this.client.options.cache.ttl,
...this.client.options.cache.options,
});
else this._cacheManager = cacheManager.caching({
store: 'memory',
ttl: this.client.options.cache.ttl,
});
}
public async del(key: string): Promise<void> {
return new Promise((res, rej) => {
this._cacheManager.del(key, (err) => {
if (err) rej(err);
else res();
});
});
}
public async get<T>(key: string): Promise<T | undefined> {
return new Promise((res, rej) => {
this._cacheManager.get<T>(key, (err, result) => {
if (err) rej(err);
else res(result);
});
});
}
public async set<T>(key: string, value: T): Promise<T> {
return new Promise((res, rej) => {
this._cacheManager.set<T>(key, value, this.client.options.cache.ttl, (err) => {
if (err) rej(err);
else res(value);
});
});
}
}

View file

@ -1,4 +1,4 @@
import type { IDefaultOptions } from './types'; import type { IDefaultOptions } from './types/common';
export const defaults: IDefaultOptions = { export const defaults: IDefaultOptions = {
clientOptions: { clientOptions: {
@ -10,18 +10,5 @@ export const defaults: IDefaultOptions = {
url: 'https://discord.com/api', url: 'https://discord.com/api',
version: 9, version: 9,
}, },
cache: {
cache: 'memory',
ttl: 60,
},
presence: {},
ws: {
compression: true,
encoding: 'json',
intents: 0,
largeThreshold: 250,
url: 'wss://gateway.discord.gg',
version: 9,
},
}, },
}; };

View file

@ -1,227 +0,0 @@
import type IORedis from 'ioredis';
// Interfaces
export interface IApiClientOptions {
offset?: number;
requestTimeout?: number;
retryLimit?: number;
sweepInterval?: number;
url?: string;
version?: number;
}
export interface ICacheMemoryClientOptions {
cache?: 'memory';
ttl?: number;
}
export interface ICacheRedisClientOptions {
cache?: 'redis';
ttl?: number;
options: IORedis.RedisOptions;
}
export interface IClientOptions {
api?: IApiClientOptions;
cache?: ICacheMemoryClientOptions | ICacheRedisClientOptions;
presence?: IUpdatePresence;
ws?: IWebSocketClientOptions;
}
export interface IDefaultOptions {
clientOptions: IClientOptions;
}
export interface IFile {
file: Buffer;
name: string;
}
export interface IMakeRequestOptions extends IRequestOptions {
method: ApiMethods;
}
export interface IMessageEmbed {
author?: { name?: string, url?: string };
color?: number;
description?: string;
fields?: { inline?: boolean, name: string, value: string }[];
footer?: { icon_url?: string, text?: string };
image?: { url?: string };
provider?: { name?: string, url?: string };
timestamp?: number;
title?: string;
thumbnail?: { url?: string };
type?: 'rich';
url?: string;
video?: { url?: string };
}
export interface IRequestOptions {
body?: unknown;
files?: IFile[];
headers?: Record<string, string>;
path: string;
reason?: string;
requireAuth?: boolean;
}
export interface IRouteIdentifier {
majorParameter: string;
route: string;
}
export interface IUpdatePresence {
activities?: {
name?: string;
type?: EActivityType;
url?: string;
}[];
afk?: boolean;
since?: number;
status?: EStatus;
}
export interface IWebSocketClientOptions {
compression?: boolean;
encoding?: 'json';
intents?: number;
largeThreshold?: number;
url?: string;
version?: number;
}
// Api Interfaces
// TODO: Add message components
// Dont add the files option. That goes into a separate option when creating a request
export interface IApiCreateMessage {
allowed_mentions?: {
parse?: 'everyone' | 'roles' | 'users'[];
replied_user?: boolean;
roles?: string[];
users?: string[];
};
content?: string;
embeds?: IMessageEmbed[];
message_reference?: {
channel_id?: string;
fail_if_not_exists?: boolean;
guild_id?: string;
message_id?: string;
};
tts?: boolean;
}
export interface IApiUser {
avatar: string;
bot?: boolean;
discriminator: string;
email?: string;
flags?: number;
id: string;
locale?: string;
mfa_enabled?: boolean;
premium_type?: number;
public_flags?: number;
username: string;
system?: boolean;
verified?: boolean;
}
export interface IApiCreateSlashCommand {
allowed_mentions?: {
parse?: 'everyone' | 'roles' | 'users'[];
replied_user?: boolean;
roles?: string[];
users?: string[];
};
content?: string;
embeds?: IMessageEmbed[];
files?: IFile[];
message_reference?: {
channel_id?: string;
fail_if_not_exists?: boolean;
guild_id?: string;
message_id?: string;
};
tts?: boolean;
}
// WebSocket Gateway Message Interfaces
export interface IGatewayHeartbeatSend {
op: 1;
d: number;
}
// TODO: Add sharding when i get to it
export interface IGatewayIdentify {
op: 2;
d: {
compress: boolean;
intents: number;
large_threshold: number;
presence?: IUpdatePresence;
properties: {
$browser: string;
$device: string;
$os: string;
};
token: string;
};
}
export interface IGatewayHeartbeat {
op: 10;
d: {
heartbeat_interval: number;
};
}
export interface IGatewayHeartbeatAck {
op: 11;
}
// Enums
export enum EActivityType {
GAME = 0,
STREAMING = 1,
LISTENING = 2,
WATCHING = 3,
COMPETING = 5,
}
export enum EStatus {
DND = 'dnd',
IDLE = 'idle',
INVISIBLE = 'invisible',
OFFLINE = 'offline',
ONLINE = 'online',
}
// Type Aliases
export type ApiMethods = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT' ;
// eslint-disable-next-line @typescript-eslint/ban-types
export type Builtin = Date | Error | Function | Primitives | RegExp;
export type DeepRequired<T> = T extends Builtin
? NonNullable<T>
: T extends Map<infer K, infer V>
? Map<DeepRequired<K>, DeepRequired<V>>
: T extends ReadonlyMap<infer K, infer V>
? ReadonlyMap<DeepRequired<K>, DeepRequired<V>>
: T extends WeakMap<infer K, infer V>
? WeakMap<DeepRequired<K>, DeepRequired<V>>
: T extends Set<infer U>
? Set<DeepRequired<U>>
: T extends ReadonlySet<infer U>
? ReadonlySet<DeepRequired<U>>
: T extends WeakSet<infer U>
? WeakSet<DeepRequired<U>>
: T extends Promise<infer U>
? Promise<DeepRequired<U>>
// eslint-disable-next-line @typescript-eslint/ban-types
: T extends {}
? { [K in keyof T]-?: DeepRequired<T[K]> }
: NonNullable<T>;
export type GatewayReceiveMessage = IGatewayHeartbeat | IGatewayHeartbeatAck;
export type GatewaySendMessage = IGatewayHeartbeatSend | IGatewayIdentify;
export type Primitives = bigint | boolean | null | number | string | symbol | undefined;

21
src/utils/types/api.ts Normal file
View file

@ -0,0 +1,21 @@
import type { IFile } from './common';
export interface IMakeRequestOptions extends IRequestOptions {
method: ApiMethods;
}
export interface IRequestOptions {
body?: unknown;
files?: IFile[];
headers?: Record<string, string>;
path: string;
reason?: string;
requireAuth?: boolean;
}
export interface IRouteIdentifier {
majorParameter: string;
route: string;
}
export type ApiMethods = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT' ;

12
src/utils/types/client.ts Normal file
View file

@ -0,0 +1,12 @@
export interface IApiClientOptions {
offset?: number;
requestTimeout?: number;
retryLimit?: number;
sweepInterval?: number;
url?: string;
version?: number;
}
export interface IClientOptions {
api?: IApiClientOptions;
}

34
src/utils/types/common.ts Normal file
View file

@ -0,0 +1,34 @@
import type { IClientOptions } from './client';
export interface IDefaultOptions {
clientOptions: IClientOptions;
}
export interface IFile {
file: Buffer;
name: string;
}
// eslint-disable-next-line @typescript-eslint/ban-types
export type Builtin = Date | Error | Function | Primitives | RegExp;
export type DeepRequired<T> = T extends Builtin
? NonNullable<T>
: T extends Map<infer K, infer V>
? Map<DeepRequired<K>, DeepRequired<V>>
: T extends ReadonlyMap<infer K, infer V>
? ReadonlyMap<DeepRequired<K>, DeepRequired<V>>
: T extends WeakMap<infer K, infer V>
? WeakMap<DeepRequired<K>, DeepRequired<V>>
: T extends Set<infer U>
? Set<DeepRequired<U>>
: T extends ReadonlySet<infer U>
? ReadonlySet<DeepRequired<U>>
: T extends WeakSet<infer U>
? WeakSet<DeepRequired<U>>
: T extends Promise<infer U>
? Promise<DeepRequired<U>>
// eslint-disable-next-line @typescript-eslint/ban-types
: T extends {}
? { [K in keyof T]-?: DeepRequired<T[K]> }
: NonNullable<T>;
export type Primitives = bigint | boolean | null | number | string | symbol | undefined;