basic-implementation #1

Open
florent wants to merge 8 commits from basic-implementation into master
17 changed files with 7290 additions and 233 deletions

20
.eslintrc.json Normal file
View File

@ -0,0 +1,20 @@
{
"env": {
"node": true,
"commonjs": true,
"es2021": true
},
"extends": [
"plugin:prettier/recommended",
"plugin:mocha/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12
},
"plugins": [
"@typescript-eslint",
"mocha"
],
"rules": {}
}

1
.prettierrc.json Normal file
View File

@ -0,0 +1 @@
{}

6628
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,10 @@
"dependencies": {
"@types/chai": "^4.2.18",
"@types/mocha": "^8.2.2",
"@types/nock": "^11.1.0",
"@types/node": "^15.3.0",
"@types/node-fetch": "^2.5.10",
"@types/pino": "^6.3.8",
"@types/sinon-chai": "^3.2.5",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
@ -38,16 +41,22 @@
"eslint-plugin-import": "^2.23.2",
"eslint-plugin-mocha": "^8.1.0",
"eslint-plugin-prettier": "^3.4.0",
"form-data": "^3.0.1",
"mocha": "^8.4.0",
"nock": "^13.0.11",
"node-fetch": "^2.6.1",
"pino": "^6.11.3",
"prettier": "^2.3.0",
"sinon-chai": "^3.6.0",
"tmp-promise": "^3.0.2",
"ts-dedent": "^2.1.1",
"ts-node": "^9.1.1",
"ts-sinon": "^2.0.1",
"tsc-watch": "^4.2.9",
"typescript": "^4.2.4"
},
"devDependencies": {
"@types/chai-string": "^1.4.2",
"@types/ws": "^7.4.4"
}
}

135
src/discordParser.ts Normal file
View File

@ -0,0 +1,135 @@
import { Client, Message, TextChannel } from "discord.js";
import { ImplementableApi } from "./implementableApi";
import { once as eventsOnce } from "events";
namespace DiscordParser {
export type Config = ImplementableApi.Config & {
token: string;
channelsId: {
idChannelYtb: string;
idChannelPeerTube: string;
};
keyWord: string;
};
export type ConstructorPattern = ImplementableApi.Config & {
botPrefix: string;
channels: {
ChannelYtb: TextChannel;
ChannelPeerTube: TextChannel;
};
client: Client;
};
}
type ChannelsType = {
ChannelYtb: TextChannel;
ChannelPeerTube: TextChannel;
};
type TBaseDiscordMessageType = ("addListener" | "newEntriesNotify") &
ImplementableApi.TBaseMessageType;
class DiscordParser<
T extends TBaseDiscordMessageType

Toujours utile ?

Toujours utile ?
> extends ImplementableApi<T> {
readonly botPrefix: string;

youtubeChannel / peertubeChannel comme nom

`youtubeChannel` / `peertubeChannel` comme nom
readonly channels: {
[key: string]: TextChannel;
ChannelYtb: TextChannel;
ChannelPeerTube: TextChannel;
type Channels = {
  channelYtb: TextChannel;
  channelPeerTube: TextChannel;
};

static async instanciate(readonly config: DiscordParser.Config) {
  const client = new Client();
  client.login(this.token);
  await eventsOnce(client, 'ready');
  const channels = {
    channelYtb: this.client.channels.resolve("..."),
    channelPeerTube: ...
  };
  return new Discord({
    client,
    channels,
    keyword
  });
```ts type Channels = { channelYtb: TextChannel; channelPeerTube: TextChannel; }; ``` ```ts static async instanciate(readonly config: DiscordParser.Config) { const client = new Client(); client.login(this.token); await eventsOnce(client, 'ready'); const channels = { channelYtb: this.client.channels.resolve("..."), channelPeerTube: ... }; return new Discord({ client, channels, keyword }); ```

I made a thing like this

namespace DiscordParser {
    /*...*/
    export type ConstructorPattern = ImplementableApi.Config & {
        keyWord: string;
        channels: {
            ChannelYtb: TextChannel;
            ChannelPeerTube: TextChannel;
        };
        client: Client;
    };
}
/*...*/
class DiscordParser extends ImplementableApi {
    readonly keyWord: string;
    readonly channels: {
        [key: string]: TextChannel;
        ChannelYtb: TextChannel;
        ChannelPeerTube: TextChannel;
    };
    readonly client: Client;

    constructor(readonly config: DiscordParser.ConstructorPattern) {
        super(config);
        this.keyWord = config.keyWord;
        this.channels = config.channels;
        this.client = config.client;
        this.settingEvents();
    }

    static async instanciate(
            config: DiscordParser.Config
        ): Promise<DiscordParser.ConstructorPattern> {
            const client = new Client();
            client.login(config.token);
            await eventsOnce(client, 'ready');
            const channels: ChannelsType = {
                ChannelPeerTube: this.getTextChannel(
                    client,
                    config.channelsId.idChannelPeerTube
                ),
                ChannelYtb: this.getTextChannel(
                    client,
                    config.channelsId.idChannelYtb
                ),
            };

            return {
                name: config.name,
                channels: channels,
                client: client,
                keyWord: config.keyWord,
            };
        }

        private static getTextChannel(client: Client, id: string): TextChannel {
            const channel = client.channels.resolve(id);
            if (!channel || !(channel instanceof TextChannel))
                throw 'Bad token or bad channel id. Be careful, the channel must be a TextChannel';
            return channel;
        }
    /*...*/
};

The problem with this is for the usage inside the router.
I need to make a thing like this :

class Router {
    api_array: { [key: string]: ImplementableApi } = {};
    readonly routes: Router.Config;

    constructor(readonly config: Router.GlobalConfig) {
        this.routes = config.router;

        this.api_array[config.discord.name] = new DiscordParser(
            await DiscordParser.instanciate(config.discord)
        );
        this.api_array[config.peertubeRequester.name] = new PeerTubeRequester(
            config.peertubeRequester
        );
        this.api_array[config.logWriter.name] = new LogWriter(config.logWriter);
    }
	/*...*/
}

So I have again an async function inside my constructor, and to solved this I need to create the same pattern inside the router class.
And by extension i need to generalize this for my ImplementableAPI class. It's included to force all the users of the ImplementableAPI to make an Dependence Injection Pattern for their API. And it's look a little bit restrictive to me.

We could try maybe an events who's called at the the end of the constructor. (I took this idea from DiscordJS)
Like that we could imagine a thing like this :

class MyClass extends ImplementableAPI {
	constructor(...) {
		super(...);
		const promise = startAnAsynchronousTask(...);
        doSomeSynchronousTask(...);
        promise.then(() = > {
        	this.emit('ready');
        });
        
	}
    
    private startAnAsynchronousTask(...) : Promise<void> {
    	await something(..);
        await somethingElse();
        await events.once(anObject, 'ready');
	}
	/*...*/	
}

With this, the restriction is to call the event 'ready' at the end of the constrctor's tasks. It's looking more friendly than the Dependency Injection for a beginner who would to make his own ImplementableAPI.

I made a thing like this ```ts namespace DiscordParser { /*...*/ export type ConstructorPattern = ImplementableApi.Config & { keyWord: string; channels: { ChannelYtb: TextChannel; ChannelPeerTube: TextChannel; }; client: Client; }; } /*...*/ class DiscordParser extends ImplementableApi { readonly keyWord: string; readonly channels: { [key: string]: TextChannel; ChannelYtb: TextChannel; ChannelPeerTube: TextChannel; }; readonly client: Client; constructor(readonly config: DiscordParser.ConstructorPattern) { super(config); this.keyWord = config.keyWord; this.channels = config.channels; this.client = config.client; this.settingEvents(); } static async instanciate( config: DiscordParser.Config ): Promise<DiscordParser.ConstructorPattern> { const client = new Client(); client.login(config.token); await eventsOnce(client, 'ready'); const channels: ChannelsType = { ChannelPeerTube: this.getTextChannel( client, config.channelsId.idChannelPeerTube ), ChannelYtb: this.getTextChannel( client, config.channelsId.idChannelYtb ), }; return { name: config.name, channels: channels, client: client, keyWord: config.keyWord, }; } private static getTextChannel(client: Client, id: string): TextChannel { const channel = client.channels.resolve(id); if (!channel || !(channel instanceof TextChannel)) throw 'Bad token or bad channel id. Be careful, the channel must be a TextChannel'; return channel; } /*...*/ }; ``` The problem with this is for the usage inside the router. I need to make a thing like this : ```ts class Router { api_array: { [key: string]: ImplementableApi } = {}; readonly routes: Router.Config; constructor(readonly config: Router.GlobalConfig) { this.routes = config.router; this.api_array[config.discord.name] = new DiscordParser( await DiscordParser.instanciate(config.discord) ); this.api_array[config.peertubeRequester.name] = new PeerTubeRequester( config.peertubeRequester ); this.api_array[config.logWriter.name] = new LogWriter(config.logWriter); } /*...*/ } ``` So I have again an async function inside my constructor, and to solved this I need to create the same pattern inside the router class. And by extension i need to generalize this for my ImplementableAPI class. It's included to force all the users of the ImplementableAPI to make an Dependence Injection Pattern for their API. And it's look a little bit restrictive to me. We could try maybe an events who's called at the the end of the constructor. (I took this idea from DiscordJS) Like that we could imagine a thing like this : ```ts class MyClass extends ImplementableAPI { constructor(...) { super(...); const promise = startAnAsynchronousTask(...); doSomeSynchronousTask(...); promise.then(() = > { this.emit('ready'); }); } private startAnAsynchronousTask(...) : Promise<void> { await something(..); await somethingElse(); await events.once(anObject, 'ready'); } /*...*/ } ``` With this, the restriction is to call the event 'ready' at the end of the constrctor's tasks. It's looking more friendly than the Dependency Injection for a beginner who would to make his own ImplementableAPI.

Ok, my bad. I think I missunderstanded the Dependency Injection. Sleep on it.
I'm going to try another thing.
I keep my DiscordParser Class with the instantiate method. And I 'm going to insert the created object inside an method in Router who could be named 'inject' or something in this idea. And who could take an ImplementableAPI instance in params.

Ok, my bad. I think I missunderstanded the Dependency Injection. Sleep on it. I'm going to try another thing. I keep my DiscordParser Class with the instantiate method. And I 'm going to insert the created object inside an method in Router who could be named 'inject' or something in this idea. And who could take an ImplementableAPI instance in params.
};
readonly client: Client;
constructor(readonly config: DiscordParser.ConstructorPattern) {
super(config);
this.botPrefix = config.botPrefix;
this.channels = config.channels;
this.client = config.client;
this.settingEvents();
}
static async instanciate<T extends TBaseDiscordMessageType>(
config: DiscordParser.Config
): Promise<DiscordParser<T>> {
const client = new Client();
client.login(config.token);
await eventsOnce(client, "ready");
const channels: ChannelsType = {
ChannelPeerTube: this.getTextChannel(
client,
config.channelsId.idChannelPeerTube
),
ChannelYtb: this.getTextChannel(client, config.channelsId.idChannelYtb),
};
return new DiscordParser<T>({
name: config.name,
channels: channels,
client: client,
botPrefix: config.keyWord,
});
}
private static getTextChannel(client: Client, id: string): TextChannel {
const channel = client.channels.resolve(id);
if (!channel || !(channel instanceof TextChannel))
throw "Bad token or bad channel id. Be careful, the channel must be a TextChannel";
amaury.joly marked this conversation as resolved Outdated
if (!resp.ChannelPeerTube || !resp.ChannelYtb) {
  throw new Error('Theres an issue concerned the channel check');
}
return {...};
```ts if (!resp.ChannelPeerTube || !resp.ChannelYtb) { throw new Error('Theres an issue concerned the channel check'); } return {...}; ```
return channel;
}
public receivedMessage(message: ImplementableApi.Message<T>) {
switch (message.type) {
case "newEntriesNotify":
this.sendMsgYtb(
`New YouTubes entries received :\n${message.rawContent.items.map(
(item: any) =>
`Author : ${item.author}\nTitle: ${item.title}\nlink: ${item.link}`
)}`
);
default:
break;
}
}
private async sendMsgYtb(message: string) {
const resolvedChannel = await this.channels;
resolvedChannel.ChannelYtb.send(message);
}
private async sendMsgPeerTube(message: string) {
const resolvedChannel = await this.channels;
resolvedChannel.ChannelPeerTube.send(message);
}
private settingEvents(): void {
this.client.on("message", (message: Message) => {
const resolvedChannel = this.channels;
let id_arr: string[] = [];
for (const key in this.channels) id_arr.push(resolvedChannel[key].id);
if (this.channels)
if (id_arr.includes(message.channel.id)) {

Je comprends pas la partie de ce code, il répond à quel cas d'utilisation ?

Sinon :

if ( Object.values(this.channels).some(channel => channel.id === message.channel.id) ) {
Je comprends pas la partie de ce code, il répond à quel cas d'utilisation ? Sinon : ```ts if ( Object.values(this.channels).some(channel => channel.id === message.channel.id) ) { ```
const msg_splitted = message.content.split(" ");
if (msg_splitted[0] === this.botPrefix) {
switch (msg_splitted[1]) {
case "add":
const send_message: ImplementableApi.Message<TBaseDiscordMessageType> =
{
rawContent: {
address: msg_splitted[2],
user: message.author.toString(),
date: message.createdAt.toUTCString(),
},
type: "addListener",
};
message.channel.send("Ceci est un feedback");
this.emit("addListener", send_message);
}
}
}
});
}
}
export { DiscordParser };

View File

@ -1,5 +1,34 @@
import EventEmitter from "events";
export class ImplementableApi extends EventEmitter {
namespace ImplementableApi {
export type Config = {
name: string;
};
/**
* [optional] idListener is the way to identificate the listener who's the src of the newEntries
* the type field permit to know which type of message it is
* rawContent field is the string content of the message
*/
export type Message<T extends TBaseMessageType> = {
idListener?: number;
type: T | "logging";
rawContent: any;
};
export type TBaseMessageType = string;
}
abstract class ImplementableApi<
T extends ImplementableApi.TBaseMessageType
> extends EventEmitter {
readonly name: string;
constructor(readonly config: ImplementableApi.Config) {
super();
this.name = config.name;
}
public abstract receivedMessage(message: ImplementableApi.Message<T>): void;
}
export { ImplementableApi };

View File

@ -1,4 +1,4 @@
// export * as Router from "./router"
export {Router} from "./router"
export { Router } from "./router";
export {ImplementableApi} from "./implementableApi"
export { ImplementableApi } from "./implementableApi";

43
src/logWriter.ts Normal file
View File

@ -0,0 +1,43 @@
import { ImplementableApi } from "./implementableApi";
import pino, { Logger } from "pino";
namespace LogWriter {
export type Config = ImplementableApi.Config & {
path: string;
};
}
/**
* check nodejs buffer et throttle
*/
class LogWriter extends ImplementableApi {
readonly path: string;
readonly logger: Logger;
constructor(readonly config: LogWriter.Config) {
super(config);
this.path = config.path;
this.logger = pino(pino.destination({ dest: config.path }));
}
private writeMsg(message: string): void;
private writeMsg(message: ImplementableApi.Message): void;
private writeMsg(message: ImplementableApi.Message | string) {
if (typeof message !== "string")
message = `[${message.type}] ${
typeof message.rawContent === "string"
? message.rawContent
: JSON.stringify(message.rawContent)
} ${message.idListener ?? `( listener_id : ${message.idListener} )\n`}`;
// const str = `[${new Date().toISOString()}] ${message}\n`;
this.logger.info(message);
}
public receivedMessage(message: ImplementableApi.Message) {
this.writeMsg(message);
}
}
export { LogWriter };

154
src/peertubeRequester.ts Normal file
View File

@ -0,0 +1,154 @@
import { ImplementableApi } from "./implementableApi";
// Api request lib
import fetch, { FetchError, Headers } from "node-fetch";
import { URLSearchParams } from "url";
import FormData from "form-data";
import dedent from "ts-dedent";
namespace PeerTubeRequester {
export type Config = ImplementableApi.Config & {
domain_name: string;
username: string;
password: string;
};
}
type UploadInstruction = {
[key: string]: string;
channelId: string;
targetUrl: string;
};
type ClientToken = {
client_id: string;
client_secret: string;
grant_type: "password";
response_type: "code";
username: string;
password: string;
};
type UserToken = {
access_token: string;
token_type: string;
expires_in: string;
refresh_token: string;
};
class PeerTubeRequester extends ImplementableApi {
readonly domain_name: string;
readonly username: string;
readonly password: string;
constructor(readonly config: PeerTubeRequester.Config) {
super(config);
this.domain_name = config.domain_name;
this.username = config.username;
this.password = config.password;
}
public async receivedMessage(
message: ImplementableApi.Message
): Promise<void> {
switch (message.type) {
case "newEntriesNotify":
await this.newEntriesNotify(message);
break;
default:
break;
}
}
private async newEntriesNotify(
message: ImplementableApi.Message
): Promise<void> {
// parse content
const items = message.rawContent["items"];
if (Array.isArray(items))
for (const item of items) {
const media_group = item["media:group"];
const args: UploadInstruction = {
channelId: "848", // to do binding avec les idDeChaines Skeptikom
targetUrl: item.link,
};
await this.apiRequest(args);
}
}
private async apiRequest(message: UploadInstruction): Promise<void> {
let response = await fetch(
`https://${this.domain_name}/api/v1/oauth-clients/local`
);
if (!response.ok) {
throw new Error(response.statusText); // CRASH
}
const { client_id, client_secret } = await response.json();
const client_info: { [key: string]: string } = {
client_id,
client_secret,
grant_type: "password",
response_type: "code",
username: this.username,
password: this.password,
};
let myParams = new URLSearchParams();
for (const key in client_info) myParams.append(key, client_info[key]);
response = await fetch(`https://${this.domain_name}/api/v1/users/token`, {
method: "post",
body: myParams,
});
if (!response.ok) {
throw new Error(response.statusText); // CRASH
}
const { access_token } = await response.json();
// Upload
const myUploadForm = new URLSearchParams();
const myHeader = new Headers();
myHeader.append("Authorization", `Bearer ${access_token}`);

Un commentaire ici, c'est compenser à mon sens un problème sémantique dans le code.

J'aurais tendance à faire deux fonctions privées this.authenticate() et this.upload()

Un commentaire ici, c'est compenser à mon sens un problème sémantique dans le code. J'aurais tendance à faire deux fonctions privées `this.authenticate()` et `this.upload()`
for (const key in message) myUploadForm.append(key, message[key]);
response = await fetch(
`https://${this.domain_name}/api/v1/videos/imports`,
{
method: "post",
headers: myHeader,
body: myUploadForm,
}
);
if (!response.ok) {
switch (response.status) {
case 400:
throw new Error(
dedent`Your target URL was not accepted by the API.\
Actualy it's : ${message.targetUrl}
${response.statusText}`
);
break;
case 403:
throw new Error(response.statusText);
break;
case 409:
throw new Error(
dedent`Oops, your instance had not allowed the HTTPS import.\
Contact your administrator.
${response.statusText}`
);
break;
default:
throw new Error(
dedent`Oh, you resolved an undocumented issues.\
Please report this on the git if you have the time.
ERROR: ${response.statusText}`
);
break;
}
}
}
}
export { PeerTubeRequester };

View File

@ -1,40 +1,64 @@
import { ImplementableApi } from "./implementableApi";
import EventEmitter from "events";
namespace Router {
export type Config = {
events: {
"name": string,
"type": 0 | 1
}[];
routes: {
"serviceName": string,
"eventAccepted": string[] | undefined,
"eventEmitted": string[] | undefined
}[]
}
export type EventsType = {
name: string;
type: "emit" | "received";
};
export type InternalConfig = {
events: EventsType[];
};
}
class Router {
api_array: ImplementableApi[] = [];
config: Router.Config;
class Router<
MessageType extends ImplementableApi.TBaseMessageType
> extends EventEmitter {
api_array: { [key: string]: ImplementableApi<MessageType> } = {};
events: Router.EventsType[];
constructor(readonly path: string) {
const tmp_config: Router.Config = require(path);
this.config = tmp_config;
// if(tmp_config["events"] === )
}
constructor(readonly config: Router.EventsType[]) {
super();
this.events = config;
}
public addApi<T extends ImplementableApi>(new_api: T) {
const new_api_conf = this.config.routes.find((elmt) => elmt.serviceName === new_api.constructor.name);
// test if fun exist
for(let ev in new_api_conf?.eventAccepted)
if(ev !in new_api)
throw new Error(`The class ${new_api.constructor.name} haven't the expected function`);
//test if events call exist
for(let ev in new_api_conf?.eventEmitted)
if(ev !in new_api.eventNames)
throw `The class ${new_api.constructor.name} haven't the well defined events`;
public injectDependency(api: ImplementableApi<MessageType>): void {
if (api.name in this.api_array)
throw `The api name '${api.name}' is already take`;
this.api_array[api.name] = api;
this.setEvents(api);
this.receivedMessage({
rawContent: `The dependency \`${api.name}\` was well injected into the router`,
type: "logging",
});
}
private setEvents(api: ImplementableApi<MessageType>) {
for (const event of this.events.filter((ev) => ev.type === "received")) {
api.on(event.name, (obj: ImplementableApi.Message<MessageType>) => {
this.receivedMessage({
type: "logging",
rawContent: `A message of type \`${obj.type}\` was emited by \`${api.name}\` with the event \`${event.name}\``,
});
this.emit(event.name, obj);
});
}
}
public receivedMessage(message: ImplementableApi.Message<MessageType>) {
this.redirectMessage({
rawContent: `A message of type \`${message.type}\` was received`,
type: "logging",
});
this.redirectMessage(message);
}
private redirectMessage(message: ImplementableApi.Message<MessageType>) {
for (const api in this.api_array)
this.api_array[api].receivedMessage(message);
}
}
export { Router };

View File

@ -1,2 +0,0 @@
export {};
//# sourceMappingURL=index-spec.d.ts.map

View File

@ -1 +0,0 @@
{"version":3,"file":"index-spec.d.ts","sourceRoot":"","sources":["index-spec.ts"],"names":[],"mappings":""}

View File

@ -1,116 +0,0 @@
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var chai_1 = __importDefault(require("chai"));
var ts_sinon_1 = __importDefault(require("ts-sinon"));
var sinon_chai_1 = __importDefault(require("sinon-chai"));
chai_1.default.use(sinon_chai_1.default);
var expect = chai_1.default.expect;
var tmp_promise_1 = require("tmp-promise");
var fs_1 = require("fs");
var index_1 = require("../src/index");
var path = require("path");
var well_build_routing_file = require(path.join(__dirname, "rsrc/wellBuildedRoutingFile.json"));
describe("testing the routing part", function () {
describe("testing the building part", function () {
it("it will test the constructor with a well format config file", function () {
//given
var fun = function () {
var r = new index_1.Router(path.join(__dirname, "rsrc/wellBuildedRoutingFile.json"));
};
var spy = ts_sinon_1.default.spy(fun);
// when
try {
spy();
}
catch (error) {
// nothing it's a test
}
// assert
expect(spy).to.not.thrown();
});
it("it will test a bad formed file", function () {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, tmp_promise_1.withFile(function (file) { return __awaiter(_this, void 0, void 0, function () {
var fun, spy;
return __generator(this, function (_a) {
//given
fs_1.writeFileSync(file.path, JSON.stringify(__assign(__assign({}, well_build_routing_file), { events: 12 })));
fun = function () {
var r = new index_1.Router(file.path);
console.log(r.config);
};
spy = ts_sinon_1.default.spy(fun);
// when
try {
spy();
}
catch (error) {
// nothing it's a test
}
// assert
expect(spy).to.not.thrown();
return [2 /*return*/];
});
}); }, { postfix: ".json" })];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
});
});
});

View File

@ -1,59 +1,85 @@
import chai from "chai";
import sinon from "ts-sinon";
import sinonChai from "sinon-chai";
import chai from 'chai';
import sinon from 'ts-sinon';
import sinonChai from 'sinon-chai';
chai.use(sinonChai)
chai.use(sinonChai);
const expect = chai.expect;
import { withFile } from "tmp-promise"
import { writeFileSync } from "fs";
import { ImplementableApi, Router } from '../src/index';
import { Router } from "../src/index";
// const well_build_routing_file: Router.GlobalConfig = require('./rsrc/wellBuildedRoutingFile.json');
const events: Router.EventsType[] = [
{
name: 'newEntries',
type: 'emit',
},
{
name: 'addListener',
type: 'received',
},
];
type AllowedEvents = 'newEntries' | 'addListener'; // need to match with the EventsType's names
const path = require("path");
const well_build_routing_file: Router.Config = require(path.join(__dirname, "rsrc/wellBuildedRoutingFile.json"));
class FakeApi extends ImplementableApi<AllowedEvents> {
constructor(conf: ImplementableApi.Config) {
super(conf);
}
receivedMessage(msg: ImplementableApi.Message<AllowedEvents>) {
// don't do anything
}
}
describe.only('testing the routing part', function () {
describe('testing the injection', function () {
it('will test the dependency injection', function () {
// given
const myRouter = new Router<AllowedEvents>(events);
const myObj = new FakeApi({ name: 'myFakeApi' });
// first expect
expect(myRouter.api_array).to.be.empty;
expect(myRouter.events).to.be.empty;
describe("testing the routing part", function () {
describe("testing the building part", function () {
it("it will test the constructor with a well format config file", function () {
//given
const fun = () => {
const r = new Router(path.join(__dirname, "rsrc/wellBuildedRoutingFile.json"));
};
const spy = sinon.spy(fun);
// when
try {
spy();
} catch (error) {
// nothing it's a test
}
// assert
expect(spy).to.not.thrown();
})
it("it will test a bad formed file", async function () {
await withFile(async (file) => {
//given
writeFileSync(file.path, JSON.stringify({...well_build_routing_file, ...{events: 12}}));
const fun = () => {
const r = new Router(file.path);
console.log(r.config);
myRouter.injectDependency(myObj);
// expect
expect(myRouter.api_array).to.have.keys(['myFakeApi']);
expect(myRouter.api_array['myFakeApi']).to.be.instanceOf(FakeApi);
expect(myRouter.events).to.be.empty;
});
});
describe('testing the data transmission', function () {
it('it will emit a upload request message', function () {});
it('it will emit a new listener request', function () {});
});
describe('testing the data reception', function () {
it.only('it will received a new entries notification', function () {
//given
const myMessageWhoWouldBeSend: ImplementableApi.Message<AllowedEvents> =
{
rawContent: 'My content',
type: 'logging',
idListener: 5,
};
const spy = sinon.spy(fun);
const myStub = sinon.stub(FakeApi.prototype, 'receivedMessage');
const myRouter = new Router([]);
const myObj = new FakeApi({ name: 'myFakeApi' });
myRouter.injectDependency(myObj);
// when
try {
spy();
} catch (error) {
// nothing it's a test
}
// when
myRouter.receivedMessage(myMessageWhoWouldBeSend);
// assert
expect(spy).to.not.thrown();
}, {postfix: ".json"});
})
})
// expect
expect(myStub.firstCall).to.have.been.calledWith(
myMessageWhoWouldBeSend
);
expect(myStub.secondCall).to.have.been.calledWith({
rawContent:
'The dependency `myFakeApi` was well injected into the router',
type: 'logging',
});
});
});
});

View File

@ -0,0 +1,123 @@
import chai from 'chai';
import sinon from 'ts-sinon';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
const expect = chai.expect;
import nock, { disableNetConnect, RequestBodyMatcher } from 'nock';
import { ImplementableApi } from '../src';
import { PeerTubeRequester } from '../src/peertubeRequester';
const paramsPeerTube: PeerTubeRequester.Config = {
name: 'testedRequester',
domain_name: 'myApiAddress.yolo',
password: 'mySuperSecretPassphrase',
username: 'myUsername',
};
const newEntriesMessage: ImplementableApi.Message = {
type: 'newEntriesNotify',
rawContent: {
items: [
{
author: 'channel1',
link: 'link1',
title: 'title1',
},
{
author: 'channel2',
link: 'link2',
title: 'title2',
},
{
author: 'channel3',
link: 'link3',
title: 'title3',
},
],
},
};
const UploadInstruction = {
channelId: 'undefined', //todo uncompleted test but incompleted function too
targetUrl: 'myTargerUrl',
};
// nock data
const expectedReplyOauthClient = {
client_id: 'expectedClientID',
client_secret: 'expectedClientSecret',
};
const expectedReplyTokenAddress = {
access_token: 'expectedAccessToken',
};
const bodyTokenRequest: RequestBodyMatcher = {
client_id: expectedReplyOauthClient.client_id,
client_secret: expectedReplyOauthClient.client_secret,
grant_type: 'password',
response_type: 'code',
username: paramsPeerTube.username,
password: paramsPeerTube.password,
};
describe('PeerTube Requester Test', function () {
before(function () {
disableNetConnect();
});
after(function () {
nock.cleanAll();
});
it('it will test the upload function', async function () {
// given
const scope = nock(`https://${paramsPeerTube.domain_name}/api/v1`)
.get(`/oauth-clients/local`)
.times(3)
.reply(200, expectedReplyOauthClient)
.post(`/users/token`, bodyTokenRequest)
.times(3)
.reply(200, expectedReplyTokenAddress);
const import_scope = nock(
`https://${paramsPeerTube.domain_name}/api/v1`
)
.matchHeader(
'authorization',
`Bearer ${expectedReplyTokenAddress.access_token}`
)
.post(`/videos/imports`, (reqBody) => {
let links: string[] = newEntriesMessage.rawContent.items.map(
(item: any) => item.link
);
const body = new URLSearchParams(reqBody);
if (body.get('channelId') === 'undefined') {
const targUrl = body.get('targetUrl');
if (targUrl && links.includes(targUrl)) {
const index = links.findIndex(
(elmt) => elmt === targUrl
);
links.splice(index, 1);
return true;
}
}
return false;
})
.times(3)
.reply(200);
const requester = new PeerTubeRequester(paramsPeerTube);
// when
await requester.receivedMessage(newEntriesMessage);
//expected
// all the scope need to be completed
expect(scope.isDone()).to.be.true;
expect(import_scope.isDone()).to.be.true;
});
});

View File

@ -1,35 +1,14 @@
{
"events": [
{
"name": "newEntriesNotify",
"type": 0
},
{
"name": "uploadRequest",
"type": 0
},
{
"name": "newListenerRequest",
"type": 0
},
{
"name": "removeListenerRequest",
"type": 0
}
],
"routes": [
{
"serviceName": "Discord",
"eventAccepted": ["newEntriesNotify"],
"eventEmitted": ["uploadRequest", "newListenerRequest", "removeListenerRequest"]
},
{
"serviceName": "logWriter",
"eventAccepted": []
},
{
"serviceName": "peertubeRequester",
"eventAccepted": ["uploadRequest"]
}
]
"discord": {
"name": "Discord",
"token": "mysecrettoken"
},
"logWriter": {
"name": "logWriter",
"path": "toto.log"
},
"peertubeRequester": {
"name": "peertubeRequester",
"domain_name": "fake.peertube.com"
}
}

View File

@ -1,4 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true,
"rootDir": "../",
"declarationMap": false
},
"include": ["./"]
}