discourse 3/3

This commit is contained in:
babayaga 2025-12-30 20:36:17 +01:00
parent 36f46ca292
commit e1dd99b693
81 changed files with 4301 additions and 647 deletions

1
packages/discourse/dist/src/_cli.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export declare const defaults: () => void;

13
packages/discourse/dist/src/_cli.js vendored Normal file
View File

@ -0,0 +1,13 @@
export const defaults = () => {
// default command
const DefaultCommand = 'info';
if (process.argv.length === 2) {
process.argv.push(DefaultCommand);
}
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
// currently no default handler, display only :
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection, reason: ', reason);
});
};
//# sourceMappingURL=_cli.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"_cli.js","sourceRoot":"","sources":["../../src/_cli.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,EAAE;IACzB,kBAAkB;IAClB,MAAM,cAAc,GAAG,MAAM,CAAC;IAC9B,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE;QAC3B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;KACrC;IAED,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,GAAG,GAAG,CAAC;IAElD,+CAA+C;IAC/C,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,MAAc,EAAE,EAAE;QAChD,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,MAAM,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACP,CAAC,CAAA"}

2
packages/discourse/dist/src/cli.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="yargs" />
export declare const cli: import("yargs").Argv<{}>;

4
packages/discourse/dist/src/cli.js vendored Normal file
View File

@ -0,0 +1,4 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
export const cli = yargs(hideBin(process.argv));
//# sourceMappingURL=cli.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,MAAM,CAAC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA"}

View File

@ -0,0 +1 @@
export declare const MODULE_NAME = "OSR-DISCOURSE";

View File

@ -0,0 +1,2 @@
export const MODULE_NAME = `OSR-DISCOURSE`;
//# sourceMappingURL=constants.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,eAAe,CAAA"}

View File

@ -0,0 +1,6 @@
export * from './constants.js';
export * from './types.js';
export * from './lib/index.js';
export { Logger } from 'tslog';
import { IObjectLiteral } from "@polymech/core";
export declare const substitute: (alt: boolean, template: string, vars?: IObjectLiteral) => any;

7
packages/discourse/dist/src/index.js vendored Normal file
View File

@ -0,0 +1,7 @@
export * from './constants.js';
export * from './types.js';
export * from './lib/index.js';
export { Logger } from 'tslog';
import { substitute as _substitute, substituteAlt as _substituteAlt } from "@polymech/core/strings";
export const substitute = (alt, template, vars = {}) => alt ? _substituteAlt(template, vars) : _substitute(template, vars);
//# sourceMappingURL=index.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,aAAa,IAAI,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEnG,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAY,EAAE,QAAgB,EAAE,OAAuB,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA"}

View File

@ -0,0 +1,11 @@
/// <reference types="node" resolution-mode="require"/>
/// <reference types="node" resolution-mode="require"/>
import { IDiscourseUser } from '../../index.js';
import { Discourser } from '../index.js';
export declare const fileAsBuffer: (path: string) => Buffer;
export declare const cacheCategories: (options: any, discourse: Discourser) => Promise<any>;
export declare const cacheTopics: (options: any, discourse: Discourser) => Promise<void>;
export declare const _getForumUsers: (d: Discourser, page: any, detail: any) => any;
export declare const getForumUsers: (d: any, detail: any) => Promise<IDiscourseUser[]>;
export declare const cacheUsers: (options: any, discourse: Discourser) => Promise<IDiscourseUser[]>;
export declare const cacheTags: (options: any, discourse: Discourser) => Promise<any>;

View File

@ -0,0 +1,110 @@
import { Promise as BPromise } from 'bluebird';
import { sync as read } from '@polymech/fs/read';
import { sync as exists } from '@polymech/fs/exists';
import { sync as write } from '@polymech/fs/write';
import { resolve } from '@polymech/commons';
import { DISCOURSE_CATEGORY_CACHE, DISCOURSE_TAGS_CACHE, DISCOURSE_USER_CACHE, } from '../discourse/constants.js';
import * as path from 'path';
export const fileAsBuffer = (path) => read(path, 'buffer') || Buffer.from("-");
import { get_cached, set_cached } from '@polymech/cache/lib';
import { OSR_CACHE } from '@polymech/commons';
import { MODULE_NAME } from '../../constants.js';
export const cacheCategories = async (options, discourse) => {
const osr_cache = OSR_CACHE();
const cPath = path.resolve(resolve(DISCOURSE_CATEGORY_CACHE));
const cached = exists(cPath) ? await get_cached(cPath, {}, MODULE_NAME) : null;
if (osr_cache && cached && options.cache !== false) {
return JSON.parse(cached);
}
let cats = await discourse.getCategories({
include_subcategories: true
});
write(cPath, cats);
if (osr_cache && options.cache !== false) {
await set_cached(cPath, {}, MODULE_NAME, cats);
}
return cats;
};
export const cacheTopics = async (options, discourse) => {
};
let uPage = 1;
let usersAll = [];
export const _getForumUsers = async (d, page, detail) => {
const uPath = path.resolve(resolve(DISCOURSE_USER_CACHE));
if (uPage == 1) {
usersAll = [];
}
let users = await d.getUsers(page);
if (users.length) {
usersAll = usersAll.concat(users);
uPage++;
return _getForumUsers(d, uPage, detail);
}
else {
uPage = 1;
write(uPath, usersAll);
let fUsers = read(uPath, 'json') || [];
const add = async (u) => {
return new Promise((resolve, reject) => {
let fUser = fUsers.find((fu) => u.id == fu.id);
if (!fUser) {
fUsers.push(u);
fUser = u;
}
if (fUser.detail) {
console.log('Retrieve User Detail ' + u.name);
setTimeout(() => {
d.getUser(fUser.id).then((detail) => {
if (detail) {
fUser.detail = detail;
}
write(uPath, fUsers);
resolve(fUser);
});
}, 200);
}
else {
resolve(fUser);
}
});
};
return await BPromise.resolve(usersAll).map((u) => {
return add(u);
}, { concurrency: 1 });
}
};
export const getForumUsers = async (d, detail) => {
return _getForumUsers(d, uPage, detail);
};
export const cacheUsers = async (options, discourse) => {
const osr_cache = OSR_CACHE();
const uPath = path.resolve(resolve(DISCOURSE_USER_CACHE));
const cached = exists(uPath) ? await get_cached(uPath, {}, MODULE_NAME) : null;
if (osr_cache && options.cache !== false && exists(uPath)) {
return read(uPath, 'json');
}
if (osr_cache && cached && options.cache !== false) {
return JSON.parse(cached);
}
let users = await getForumUsers(discourse, false);
write(uPath, users);
if (osr_cache && options.cache !== false) {
await set_cached(uPath, {}, MODULE_NAME, users);
}
return users;
};
export const cacheTags = async (options, discourse) => {
const osr_cache = OSR_CACHE();
const tPath = path.resolve(resolve(DISCOURSE_TAGS_CACHE));
const cached = exists(tPath) ? await get_cached(tPath, {}, MODULE_NAME) : null;
if (osr_cache && cached && options.cache !== false) {
return JSON.parse(cached);
}
let tags = await discourse.getTags();
write(tPath, tags);
if (osr_cache && options.cache !== false) {
await set_cached(tPath, {}, MODULE_NAME, tags);
}
return tags;
};
//# sourceMappingURL=cache.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../../../src/lib/discourse/cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,UAAU,CAAA;AAC9C,OAAO,EAAE,IAAI,IAAI,IAAI,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,IAAI,IAAI,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,IAAI,IAAI,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAQ3C,OAAO,EACH,wBAAwB,EACxB,oBAAoB,EACpB,oBAAoB,GACvB,MAAM,2BAA2B,CAAA;AAKlC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAE5B,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAW,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAEhG,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAG7C,OAAO,EACH,WAAW,EACd,MAAM,oBAAoB,CAAA;AAG3B,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,OAAY,EAAE,SAAqB,EAAE,EAAE;IAEzE,MAAM,SAAS,GAAG,SAAS,EAAE,CAAA;IAE7B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,CAAA;IAE7D,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAE9E,IAAI,SAAS,IAAI,MAAM,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;KAC5B;IAED,IAAI,IAAI,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC;QACrC,qBAAqB,EAAE,IAAI;KAC9B,CAAC,CAAA;IAEF,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAElB,IAAI,SAAS,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE;QACtC,MAAM,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,CAAA;KACjD;IAED,OAAO,IAAI,CAAA;AACf,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,OAAY,EAAE,SAAqB,EAAE,EAAE;AAEzE,CAAC,CAAA;AAED,IAAI,KAAK,GAAG,CAAC,CAAA;AACb,IAAI,QAAQ,GAAG,EAAE,CAAA;AAEjB,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,CAAa,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAEhE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAA;IAEzD,IAAI,KAAK,IAAI,CAAC,EAAE;QACZ,QAAQ,GAAG,EAAE,CAAA;KAChB;IACD,IAAI,KAAK,GAAQ,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IACvC,IAAI,KAAK,CAAC,MAAM,EAAE;QACd,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACjC,KAAK,EAAE,CAAA;QACP,OAAO,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;KAC1C;SAAM;QACH,KAAK,GAAG,CAAC,CAAA;QAET,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;QAEtB,IAAI,MAAM,GAAqB,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,EAAS,CAAA;QAE/D,MAAM,GAAG,GAAG,KAAK,EAAE,CAAiB,EAAE,EAAE;YACpC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACnC,IAAI,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;gBAC9C,IAAI,CAAC,KAAK,EAAE;oBACR,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;oBACd,KAAK,GAAG,CAAC,CAAA;iBACZ;gBAED,IAAI,KAAK,CAAC,MAAM,EAAE;oBACd,OAAO,CAAC,GAAG,CAAC,uBAAuB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAA;oBAC7C,UAAU,CAAC,GAAG,EAAE;wBACZ,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;4BAChC,IAAI,MAAM,EAAE;gCACR,KAAK,CAAC,MAAM,GAAG,MAAM,CAAA;6BACxB;4BACD,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;4BACpB,OAAO,CAAC,KAAK,CAAC,CAAA;wBAClB,CAAC,CAAC,CAAA;oBACN,CAAC,EAAE,GAAG,CAAC,CAAA;iBACV;qBAAM;oBACH,OAAO,CAAC,KAAK,CAAC,CAAA;iBACjB;YACL,CAAC,CAAC,CAAA;QACN,CAAC,CAAA;QACD,OAAO,MAAM,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAiB,EAAE,EAAE;YAC9D,OAAO,GAAG,CAAC,CAAC,CAAC,CAAA;QACjB,CAAC,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAAA;KAEzB;AACL,CAAC,CAAA;AACD,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,CAAC,EAAE,MAAM,EAA6B,EAAE;IACxE,OAAO,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;AAC3C,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,OAAO,EAAE,SAAqB,EAA6B,EAAE;IAE1F,MAAM,SAAS,GAAG,SAAS,EAAE,CAAA;IAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAA;IAEzD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAE9E,IAAI,SAAS,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE;QACvD,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,CAAQ,CAAA;KACpC;IAED,IAAI,SAAS,IAAI,MAAM,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;KAC5B;IAED,IAAI,KAAK,GAAG,MAAM,aAAa,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;IAEjD,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IAEnB,IAAI,SAAS,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE;QACtC,MAAM,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,CAAA;KAClD;IAED,OAAO,KAAK,CAAA;AAEhB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,OAAO,EAAE,SAAqB,EAAE,EAAE;IAC9D,MAAM,SAAS,GAAG,SAAS,EAAE,CAAA;IAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAA;IAEzD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAE9E,IAAI,SAAS,IAAI,MAAM,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;KAC5B;IAED,IAAI,IAAI,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,CAAA;IACpC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAElB,IAAI,SAAS,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE;QACtC,MAAM,UAAU,CAAC,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,CAAA;KACjD;IAED,OAAO,IAAI,CAAA;AACf,CAAC,CAAA"}

View File

@ -0,0 +1,70 @@
export type EDiscourseConfigKey = 'discourse' | 'discourse_admin';
export declare const SYNC_TRACK_FILENAME = "discourse-sync.json";
export declare const OSR_CACHE_ROOT = "${OSR_CACHE}";
export declare const OSR_DISCOURSE_CACHE = "${OSR_CACHE}/discourse";
export declare const DISCOURSE_CATEGORY_CACHE = "${OSR_CACHE}/discourse/cats.json";
export declare const DISCOURSE_TOPICS_CACHE = "${OSR_CACHE}/discourse/topics.json";
export declare const DISCOURSE_TAGS_CACHE = "${OSR_CACHE}/discourse/tags.json";
export declare const DISCOURSE_USER_CACHE = "${OSR_CACHE}/discourse/users.json";
export declare const OA_DIRECTORY_OVERVIEW_TOPIC = 28873;
export declare const OA_USER_IMPORT_GROUP = 43;
export declare const KB_USERS = "${KB_ROOT}/static/users";
export declare const DATA_PATH = "${OSR_ROOT}/osr-directory/pp";
export declare const LATEST_TRACK = "${OSR_ROOT}/osr-directory/pp/merged.json";
export declare const LATEST_TEST = "./latest_test.json";
export declare const FETCH_DUSERS = false;
export declare const F_USERS_NOW = "./fusers.json";
export declare const F_USERS_ALL = "./fusers-all.json";
export declare const DEFAULT_PASSWORD: () => string;
export declare const HOWTOS_ASSETS_URL: () => string;
export declare const MACHINES_ASSETS_URL: () => string;
export declare const CAT_TEST = 65;
export declare const TAGS_TEST = "plastic, meta";
export declare const DEFAULT_IMPORT_OWNER = 1;
export declare const D_ROOT_CAT = 97;
export declare const D_ROOT_AFRICA = 79;
export declare const D_ROOT_ASIA = 60;
export declare const D_ROOT_EUROPE = 59;
export declare const D_ROOT_AUSTRALIA = 76;
export declare const D_ROOT_NAMERICA = 101;
export declare const D_ROOT_SAMERICA = 102;
export declare const D_ROOT_OCEANIA = 103;
export declare const HT_CAT_ROOT = 54;
export declare const HT_CAT_GUIDES = 86;
export declare const HT_CAT_MACHINES = 87;
export declare const HT_CAT_PRODUCTS = 88;
export declare const HT_CAT_MOULDS = 89;
export declare const HT_CAT_IDS: {
HT_CAT_ROOT: number;
HT_CAT_GUIDES: number;
HT_CAT_MACHINES: number;
HT_CAT_PRODUCTS: number;
HT_CAT_MOULDS: number;
};
export declare const HT_CATS: {
Guides: number;
Machines: number;
Products: number;
Moulds: number;
};
export declare const MACHINE_CAT_INJECTION = 50;
export declare const MACHINE_CAT_EXTRUSION = 51;
export declare const MACHINE_CAT_SHREDDER = 52;
export declare const MACHINE_CAT_SHEETPRESS = 63;
export declare const MACHINE_CAT_3DPRINT = 70;
export declare const MACHINE_CAT_MOULDS = 71;
export declare const MACHINE_CAT_IDS: {
MACHINE_CAT_INJECTION: number;
MACHINE_CAT_EXTRUSION: number;
MACHINE_CAT_SHREDDER: number;
MACHINE_CAT_SHEETPRESS: number;
MACHINE_CAT_3DPRINT: number;
MACHINE_CAT_MOULDS: number;
};
export declare const MACHINE_CATS: {
Injection: number;
Extrusion: number;
Sheetpress: number;
'3DPrint': number;
Moulds: number;
};

View File

@ -0,0 +1,94 @@
//////////////////////////////////////////////////////////////
//
// Keys
//
//////////////////////////////////////////////////////////////
//
// Paths
//
export const SYNC_TRACK_FILENAME = 'discourse-sync.json';
export const OSR_CACHE_ROOT = '${OSR_CACHE}';
export const OSR_DISCOURSE_CACHE = '${OSR_CACHE}/discourse';
export const DISCOURSE_CATEGORY_CACHE = '${OSR_CACHE}/discourse/cats.json';
export const DISCOURSE_TOPICS_CACHE = '${OSR_CACHE}/discourse/topics.json';
export const DISCOURSE_TAGS_CACHE = '${OSR_CACHE}/discourse/tags.json';
export const DISCOURSE_USER_CACHE = '${OSR_CACHE}/discourse/users.json';
//////////////////////////////////////////////////////////////
//
// OA - User Import
//
export const OA_DIRECTORY_OVERVIEW_TOPIC = 28873;
export const OA_USER_IMPORT_GROUP = 43;
export const KB_USERS = '${KB_ROOT}/static/users';
export const DATA_PATH = '${OSR_ROOT}/osr-directory/pp';
export const LATEST_TRACK = '${OSR_ROOT}/osr-directory/pp/merged.json';
export const LATEST_TEST = './latest_test.json';
export const FETCH_DUSERS = false;
export const F_USERS_NOW = './fusers.json';
export const F_USERS_ALL = './fusers-all.json';
export const DEFAULT_PASSWORD = () => '4g0&KPN$e*Un';
export const HOWTOS_ASSETS_URL = () => `https://kb.osr-plastic.org/howtos/`;
export const MACHINES_ASSETS_URL = () => `https://assets.osr-plastic.org/machines/`;
export const CAT_TEST = 65;
export const TAGS_TEST = 'plastic, meta';
export const DEFAULT_IMPORT_OWNER = 1;
//////////////////////////////////////////////////////////////
//
// OA - Directory Import
//
export const D_ROOT_CAT = 97;
export const D_ROOT_AFRICA = 79;
export const D_ROOT_ASIA = 60;
export const D_ROOT_EUROPE = 59;
export const D_ROOT_AUSTRALIA = 76;
export const D_ROOT_NAMERICA = 101;
export const D_ROOT_SAMERICA = 102;
export const D_ROOT_OCEANIA = 103;
//////////////////////////////////////////////////////////////
//
// OA - Howto Import
//
export const HT_CAT_ROOT = 54;
export const HT_CAT_GUIDES = 86;
export const HT_CAT_MACHINES = 87;
export const HT_CAT_PRODUCTS = 88;
export const HT_CAT_MOULDS = 89;
export const HT_CAT_IDS = {
HT_CAT_ROOT,
HT_CAT_GUIDES,
HT_CAT_MACHINES,
HT_CAT_PRODUCTS,
HT_CAT_MOULDS
};
export const HT_CATS = {
'Guides': HT_CAT_GUIDES,
'Machines': HT_CAT_MACHINES,
'Products': HT_CAT_PRODUCTS,
'Moulds': HT_CAT_MOULDS
};
//////////////////////////////////////////////////////////////
//
// OA - Library Import
//
export const MACHINE_CAT_INJECTION = 50;
export const MACHINE_CAT_EXTRUSION = 51;
export const MACHINE_CAT_SHREDDER = 52;
export const MACHINE_CAT_SHEETPRESS = 63;
export const MACHINE_CAT_3DPRINT = 70;
export const MACHINE_CAT_MOULDS = 71;
export const MACHINE_CAT_IDS = {
MACHINE_CAT_INJECTION,
MACHINE_CAT_EXTRUSION,
MACHINE_CAT_SHREDDER,
MACHINE_CAT_SHEETPRESS,
MACHINE_CAT_3DPRINT,
MACHINE_CAT_MOULDS
};
export const MACHINE_CATS = {
'Injection': MACHINE_CAT_INJECTION,
'Extrusion': MACHINE_CAT_EXTRUSION,
'Sheetpress': MACHINE_CAT_SHEETPRESS,
'3DPrint': MACHINE_CAT_3DPRINT,
'Moulds': HT_CAT_MOULDS
};
//# sourceMappingURL=constants.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../../src/lib/discourse/constants.ts"],"names":[],"mappings":"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"}

View File

@ -0,0 +1,180 @@
import { IDiscourseConfig } from '@polymech/commons/types';
import PromisePool from 'native-promise-pool';
export declare const escape: (path: string) => string;
import { TPostStatus, TPostStatusUpdate, UserPreferencesUpdate } from './types.js';
import { Category, CategoriesResponse, PostsResponse, PostResponse, TopicResponse, TopicItem, CategoryResponse, PostItem, PostUpdateItem, IDiscourserConfig, Thread, PostModifier, FetchConfig, FetchOptions, ISearchResult, ICreateUserResponse, IUserDetail, TagsResponse, Tag, TopicUpdateBasicTopic } from './types.js';
import { IDiscourseUser } from '@polymech/commons';
import { EDiscourseConfigKey } from './constants.js';
/**
* Discourser is an API Client for the [Discourse API](https://docs.discourse.org)
* It special features are:
* - TypeScript Types
* - Respecting Rate Limits
* - Optional Heavy Caching
* - Post Modifiers (can be used for global find and replace across all posts on the forum)
*/
export declare class Discourser {
readonly host: string;
readonly key: string;
readonly username: string;
readonly cache?: string;
readonly useCache?: boolean;
readonly dry: boolean;
readonly pool: PromisePool<any>;
/**
* Construct our Discourser instance
* See {@link IDiscourserConfig} for available configuration.
*/
constructor(config: IDiscourserConfig);
/** Get the URL of a topic */
getTopicURL(topic: TopicItem | TopicResponse | number): string;
/** Fetch a discourse API URL, with rate limit concurrency and optional caching */
fetch<T>({ url, useCache, request }: FetchConfig): Promise<T>;
/** Fetch a discourse API URL, with rate limit retries */
private _post;
/** Fetch a discourse API URL, with rate limit retries */
private _fetch;
/**
* API Helper for {@link .search}
* https://docs.discourse.org/#tag/Search/operation/search
*/
search(query: string, params?: string, opts?: FetchOptions): Promise<ISearchResult>;
/**
* API Helper for {@link .getTags}
*/
protected getTagsResponse(opts?: FetchOptions): Promise<TagsResponse>;
/**
* Fetch the whole information, for all categories of the forum
*/
getTags(opts?: FetchOptions): Promise<Tag[]>;
createTag(name: any): Promise<any>;
/**
* API Helper for {@link .getCategories}
*/
protected getCategoriesResponse(opts?: FetchOptions): Promise<CategoriesResponse>;
/**
* Fetch the whole information, for all categories of the forum
*/
getCategories(opts?: FetchOptions): Promise<Category[]>;
/**
* API Helper for {@link .getTopicItemsOfCategory}
* Discourse does not provide an API for fetching category information for a specific category.
* Instead, all that it provides is a way of getting the topics for a specific category.
*/
protected getCategoryResponse(categoryID: number, opts?: FetchOptions): Promise<CategoryResponse>;
/**
* Fetch the partial information, for all topics of a specific category
*/
getTopicItemsOfCategory(categoryID: number, opts?: FetchOptions): Promise<TopicItem[]>;
/**
* Fetch the partial information, for all topics of specific categoires
*/
getTopicItemsOfCategories(categoryIDs: number[], opts?: FetchOptions): Promise<TopicItem[]>;
/**
* Fetch the partial information, for all topics of the forum
*/
getTopicItems(opts?: FetchOptions): Promise<TopicItem[]>;
/**
* Fetch the whole information, for a specific topic of the forum
*/
getTopic(id: number, opts?: FetchOptions): Promise<TopicResponse>;
/**
* Fetch the whole information, for all topics, or specific topics, of the forum
*/
getTopics(topicIDs?: number[] | null, opts?: FetchOptions): Promise<TopicItem[] | TopicResponse[]>;
updateTopicVisibility(topicID: number, listed?: boolean, visible?: TPostStatus): Promise<TPostStatusUpdate>;
updateTopicTimestamp(topicID: number, timestamp: Date | string | number, token: string): Promise<any>;
/**
* API Helper for {@link .getPostItemsOfTopic}
*/
protected getPostItemsOfTopicResponse(topicID: number, opts?: FetchOptions): Promise<PostsResponse>;
_createUser(name: any, email: any, pUserGroup: any): Promise<ICreateUserResponse>;
getUsers(page: any): Promise<IDiscourseUser>;
getUser(id: any): Promise<IUserDetail>;
/**
* Fetch the partial information, for all posts of a specific topic
*/
getPostItemsOfTopic(topicID: number, opts?: FetchOptions): Promise<PostItem[]>;
/**
* Fetch the partial information, for all posts of specific topics
*/
getPostItemsOfTopics(topicIDs: number[], opts?: FetchOptions): Promise<PostItem[]>;
/**
* Fetch the partial information, for all posts of a specific category
*/
getPostItemsOfCategory(categoryID: number, opts?: FetchOptions): Promise<PostItem[]>;
/**
* Fetch the partial information, for all posts of specific categories
*/
getPostItemsOfCategories(categoryIDs: number[], opts?: FetchOptions): Promise<PostItem[]>;
/**
* Fetch the partial information, for all posts of the forum
*/
getPostItems(opts?: FetchOptions): Promise<PostItem[]>;
/**
* Fetch the whole information, for a specific post of the forum
*/
getPost(id: number, opts?: FetchOptions): Promise<PostResponse>;
createReply(postId: any, raw: any, category: any): Promise<any>;
changeOwner(postId: string | number, topicId: string | number, owner: string): Promise<any>;
createUser(data: any): Promise<ICreateUserResponse>;
getUserByUsername(username: any): Promise<IUserDetail>;
setUserAvatar(user_name: any, upload_id: any): Promise<PostResponse[]>;
updateUser(user_name: any, args: any): Promise<PostResponse[]>;
updateGroup(user_name: any, group: any): Promise<any>;
upload(userId: any, file: any): Promise<any[]>;
uploadFile(userId: any, file: any): Promise<PostResponse[]>;
/**
* Fetch the whole information, for all posts, or specific posts, of the forum
*/
getPosts(postIDs?: number[] | null, opts?: FetchOptions): Promise<PostResponse[]>;
createPost(title: string, raw: string, category: number): Promise<unknown>;
/**
* Update a post with the content
* @param postID the identifier of the post to update
* @param content the new raw content for the post
* @param reason the reason, if provided, for modifying the post
* @param old if the old raw content is provided, then the update verified that you are working with the latest post content before applying the update
*/
updatePost(postID: number, content: string, reason?: string, old?: string): Promise<PostUpdateItem>;
/**
* Update post meta
*/
updateTopic(postId: number, category_id: number, title: string, tags?: string[]): Promise<TopicUpdateBasicTopic>;
rebakePost(postID: number): Promise<any>;
/**
* Modify a post using a modifier
*/
modifyPost(post: PostResponse, modifier: PostModifier): Promise<PostUpdateItem | null>;
/**
* Modify a post (via its post identifier) using a modifier
*/
modifyPostID(post: number, modifier: PostModifier): Promise<PostUpdateItem>;
/**
* Modify a post (via fetching the whole post from the partial post identifier) using a modifier
*/
modifyPostItem(post: PostItem, modifier: PostModifier): Promise<PostUpdateItem>;
/**
* Run the post modifier on all specified posts
*/
modifyPosts(posts: PostResponse[], modifier: PostModifier): Promise<PostUpdateItem[]>;
/**
* Fetch the partial information, for all posts of a specific topic
* Alias of {@link .getPostItemsOfTopic}.
*/
getThread(topicID: number, opts?: FetchOptions): Promise<Thread>;
/**
* Fetch the partial information, for all posts of specific topics, grouped by topic
*/
getThreads(topicIDs: number[], opts?: FetchOptions): Promise<Thread[]>;
/**
* Fetch the partial information, for all posts of specific categories, grouped by topic
*/
getThreadsOfCategory(categoryID: number, opts?: FetchOptions): Promise<Thread[]>;
/**
* Fetch the partial information, for all posts of specific categories, grouped by category, then topic
*/
getThreadsOfCategories(categoryIDs: number[], opts?: FetchOptions): Promise<Thread[][]>;
updateUserProfile(userId: any, prefs: UserPreferencesUpdate): Promise<any>;
}
export declare const Instance: (config?: IDiscourseConfig, key?: EDiscourseConfigKey) => Discourser;

View File

@ -0,0 +1,831 @@
import { MODULE_NAME } from '../../constants.js';
import { CONFIG_DEFAULT } from '@polymech/commons';
import { createLogger } from '@polymech/log';
const logger = createLogger(MODULE_NAME);
import { sync as write } from '@polymech/fs/write';
import { sync as read } from '@polymech/fs/write';
import { sync as exists } from '@polymech/fs/exists';
import PromisePool from 'native-promise-pool';
import { join } from 'path';
import * as _axios from 'axios';
const axios = _axios.default || _axios;
import * as fs from 'fs';
import * as path from 'path';
import * as _FormData from 'form-data';
const FormData = _FormData.default || _FormData;
import * as https from 'https';
import * as _fetch from 'isomorphic-unfetch';
const fetch = _fetch.default || _fetch;
export const escape = (path) => path.replace(/[^\w]/g, '-').replace(/-+/, '-');
import { generate } from 'generate-password';
/**
* Discourser is an API Client for the [Discourse API](https://docs.discourse.org)
* It special features are:
* - TypeScript Types
* - Respecting Rate Limits
* - Optional Heavy Caching
* - Post Modifiers (can be used for global find and replace across all posts on the forum)
*/
export class Discourser {
host;
key;
username;
cache;
useCache;
dry;
pool;
/**
* Construct our Discourser instance
* See {@link IDiscourserConfig} for available configuration.
*/
constructor(config) {
this.host = config.host;
this.key = config.key;
this.username = config.username;
this.cache = config.cache;
this.useCache = config.useCache;
this.dry = config.dry || false;
this.pool = new PromisePool(config.rateLimitConcurrency || 60);
}
/** Get the URL of a topic */
getTopicURL(topic) {
if (typeof topic === 'number') {
return `${this.host}/t/${topic}`;
}
return `${this.host}/t/${topic.slug}/${topic.id}`;
}
/** Fetch a discourse API URL, with rate limit concurrency and optional caching */
async fetch({ url, useCache, request }) {
// check if cache is enabled
useCache = false;
const cache = this.cache &&
(request?.method || 'get') === 'get' &&
join(this.cache, escape(url));
// check if we should and can read from cache
if (cache &&
this.useCache !== false &&
useCache !== false &&
(exists(cache))) {
const result = read(cache, 'json');
return result;
}
// fetch
const result = await this.pool.open(() => this._fetch({ url, request }));
// write to cache if cache is enabled
if (cache) {
write(cache, result);
}
// return the result
return result;
}
/** Fetch a discourse API URL, with rate limit retries */
async _post(url, data) {
const opts = {
headers: {
'Api-Key': this.key,
'Api-Username': this.username,
},
};
let d = data;
const res = await axios.post(url, d, {
headers: opts.headers
});
// fetch text then parse as json, so that when errors occur we can output what it was
// rather than being stuck with errors like these:
// FetchError: invalid json response body at https://discuss.bevry.me/posts/507.json reason: Unexpected token < in JSON at position 0
const text = await res.data;
// check if there are errors
if (typeof data.errors !== 'undefined') {
// check if the error is a rate limit
const wait = data.extras?.wait_seconds;
if (wait != null) {
// if it was, try later
// return await retry(wait + 1)
}
// otherwise fail
// logger.debug({ data, url, opts })
return Promise.reject(new Error(`fetch of [${url}] received failed response:\n${data}`));
}
return text;
}
/** Fetch a discourse API URL, with rate limit retries */
async _fetch({ url, request }) {
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
const opts = {
...request,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Api-Key': this.key,
'Api-Username': this.username,
...request?.headers
},
rejectUnauthorized: false,
agent: httpsAgent
};
const retry = (seconds) => {
return new Promise((resolve, reject) => {
setTimeout(() => this._fetch({ url, request })
.then(resolve)
.catch(reject), (seconds || 60) * 1000);
});
};
try {
const res = await fetch(url, opts);
// fetch text then parse as json, so that when errors occur we can output what it was
// rather than being stuck with errors like these:
// FetchError: invalid json response body at https://discuss.bevry.me/posts/507.json reason: Unexpected token < in JSON at position 0
const text = await res.text();
let data;
try {
data = JSON.parse(text);
}
catch (err) {
// check if it was cloudflare reporting that the server has been hit too hard
if (text.includes('Please try again in a few minutes')) {
logger.debug('server has stalled, trying again in a minute');
return await retry(60);
}
// otherwise log the error page and die
// logger.debug({ text, url , opts })
return Promise.reject(logger.error(text, url, opts, err) &&
new Error(`fetch of [${url}] received invalid response:\n${text}`));
}
// check if there are errors
if (typeof data.errors !== 'undefined') {
// check if the error is a rate limit
const wait = data.extras?.wait_seconds;
if (wait != null) {
// if it was, try later
return await retry(wait + 1);
}
// otherwise fail
// logger.debug({ data, url, opts })
return Promise.reject(new Error(`fetch of [${url}] received failed response:\n${data}`));
}
return data;
}
catch (err) {
// logger.debug({ err, url, opts })
return Promise.reject(logger.error(`fetch of [${url}] failed with error`, err));
}
}
// =================================
// Search
/**
* API Helper for {@link .search}
* https://docs.discourse.org/#tag/Search/operation/search
*/
async search(query, params = '', opts = {}) {
let url = `${this.host}/search.json?q=${encodeURIComponent(query)} ${encodeURIComponent(params)}`;
return await this.fetch({ url, ...opts });
}
// =================================
// Tags
/**
* API Helper for {@link .getTags}
*/
async getTagsResponse(opts = {}) {
const url = `${this.host}/tags.json`;
return await this.fetch({ url, ...opts });
}
/**
* Fetch the whole information, for all categories of the forum
*/
async getTags(opts = {}) {
const response = await this.getTagsResponse(opts);
const tags = response.tags;
return tags;
}
async createTag(name) {
const url = `${this.host}/tag_groups.json`;
try {
return await this._post(url, {
name
});
}
catch (error) {
debugger;
}
}
// =================================
// CATEGORIES
/**
* API Helper for {@link .getCategories}
*/
async getCategoriesResponse(opts = {}) {
const url = `${this.host}/categories.json` +
(opts.include_subcategories ? '?include_subcategories=true' : '');
return await this.fetch({ url, ...opts });
}
/**
* Fetch the whole information, for all categories of the forum
*/
async getCategories(opts = {}) {
const response = await this.getCategoriesResponse(opts);
const categories = response.category_list.categories;
return categories;
}
/**
* API Helper for {@link .getTopicItemsOfCategory}
* Discourse does not provide an API for fetching category information for a specific category.
* Instead, all that it provides is a way of getting the topics for a specific category.
*/
async getCategoryResponse(categoryID, opts = {}) {
const url = `${this.host}/c/${categoryID}.json` +
(opts.page !== 0 ? `?page=${opts.page}` : '');
return await this.fetch({ url, ...opts });
}
// =================================
// TOPICS
/**
* Fetch the partial information, for all topics of a specific category
*/
async getTopicItemsOfCategory(categoryID, opts = {}) {
// prepare and fetch
let page = opts.page || 0;
const response = await this.getCategoryResponse(categoryID, {
...opts,
page,
});
let topics = response.topic_list.topics;
// fetch the next page
if (topics.length === response.topic_list.per_page) {
page += 1;
const more = await this.getTopicItemsOfCategory(categoryID, {
...opts,
page,
});
topics.push(...more);
}
// if we are the first page, then output count as we now have all of them
if (page === 0) {
const ids = topics.map((i) => i.id);
}
topics = topics.filter((t) => t.visible === true);
return topics;
}
/**
* Fetch the partial information, for all topics of specific categoires
*/
async getTopicItemsOfCategories(categoryIDs, opts = {}) {
// fetch topic items for specific categories
try {
const topicsOfCategories = await Promise.all(categoryIDs.map((id) => this.getTopicItemsOfCategory(id, opts)));
// @ts-ignore
return topicsOfCategories.flat();
}
catch (error) {
logger.error(error);
}
}
/**
* Fetch the partial information, for all topics of the forum
*/
async getTopicItems(opts = {}) {
const categories = await this.getCategories();
const categoryIDs = categories.map((i) => i.id);
return this.getTopicItemsOfCategories(categoryIDs, opts);
}
/**
* Fetch the whole information, for a specific topic of the forum
*/
getTopic(id, opts = {}) {
const url = `${this.host}/t/${id}.json`;
return this.fetch({ url, ...opts });
}
/**
* Fetch the whole information, for all topics, or specific topics, of the forum
*/
async getTopics(topicIDs, opts = {}) {
// if no topics, use all topics
if (!topicIDs) {
const topics = await this.getTopicItems(opts);
topicIDs = topics.map((i) => i.id);
}
// fetch whole topics
return Promise.all(topicIDs.map((id) => this.getTopic(id, opts)));
}
async updateTopicVisibility(topicID, listed = true, visible = 'visible') {
const url = `${this.host}/t/${topicID}/status`;
let ret = await fetch(url, {
"headers": {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
'Api-Key': this.key,
'Api-Username': this.username
},
"body": `status=${visible}&enabled=${listed}`,
"method": "PUT"
});
return ret;
}
async updateTopicTimestamp(topicID, timestamp, token) {
let time;
if (typeof timestamp === 'number') {
time = timestamp;
}
else if (typeof timestamp === 'number') {
time = Number(timestamp);
}
else if (timestamp instanceof Date) {
// ms to seconds
time = timestamp.getTime() / 1000;
}
else {
return Promise.reject(new Error('invalid timestamp format'));
}
const url = `${this.host}/t/${topicID}/change-timestamp`;
// prepare the request
const request = {
timestamp: time,
};
// send the update
logger.debug('updating', topicID, 'topic timestamp with', request);
//const url = `${this.host}/t/${topicID}/change-timestamp`
const response = await this.fetch({
url,
request: {
method: 'put',
body: `timestamp=${time}`,
headers: {
'Api-Key': this.key,
'Api-Username': this.username,
"content-type": "application/x-www-form-urlencoded; charset=UTF-8"
}
}
});
// check it
if (response.success !== 'OK') {
return Promise.reject(new Error(`timestamp update of topic ${topicID} failed:\n${{
url,
request,
response,
}}`));
}
return response;
}
// =================================
// POSTS
/**
* API Helper for {@link .getPostItemsOfTopic}
*/
async getPostItemsOfTopicResponse(topicID, opts = {}) {
const url = `${this.host}/t/${topicID}/posts.json`;
const response = await this.fetch({ url, ...opts });
return response;
}
async _createUser(name, email, pUserGroup) {
const pwd = generate({
length: 10,
numbers: true
});
let user = await this.createUser({
"name": name,
"email": email,
"password": pwd,
"username": name,
"active": true,
"approved": true,
"user_fields[1]": true
});
if (user && user.user_id) {
await this.updateGroup(name, pUserGroup);
return { ...user, password: pwd };
}
else {
if (user && user.message && user.message == 'Username must be unique\nPrimary email has already been taken') {
return null;
}
else if (user && user.message && user.message == 'Your account is activated and ready to use.') {
if (user.user_id) {
return { ...user, password: pwd };
}
return null;
}
else {
console.log('cant create user ' + name, user);
}
return null;
}
}
async getUsers(page) {
const url = `${this.host}/admin/users/list/active.json?page=` + page;
const response = await this.fetch({ url });
return response;
}
async getUser(id) {
const url = `${this.host}/admin/users/${id}.json`;
const response = await this.fetch({ url });
return response;
}
/**
* Fetch the partial information, for all posts of a specific topic
*/
async getPostItemsOfTopic(topicID, opts = {}) {
const response = await this.getPostItemsOfTopicResponse(topicID, opts);
const posts = response.post_stream.posts;
const ids = posts.map((i) => i.id);
return posts;
}
/**
* Fetch the partial information, for all posts of specific topics
*/
async getPostItemsOfTopics(topicIDs, opts = {}) {
// fetch post items for specific topics
const postItemsOfTopics = await Promise.all(topicIDs.map((id) => this.getPostItemsOfTopic(id, opts)));
// @ts-ignore
return postItemsOfTopics.flat();
}
/**
* Fetch the partial information, for all posts of a specific category
*/
async getPostItemsOfCategory(categoryID, opts = {}) {
// fetch topics for the category
const topics = await this.getTopicItemsOfCategory(categoryID, opts);
const topicIDs = topics.map((i) => i.id);
// fetch
const posts = await this.getPostItemsOfTopics(topicIDs);
const ids = posts.map((i) => i.id);
return posts;
}
/**
* Fetch the partial information, for all posts of specific categories
*/
async getPostItemsOfCategories(categoryIDs, opts = {}) {
// fetch post items for specific categories
const postItemsOfCategories = await Promise.all(categoryIDs.map((id) => this.getPostItemsOfCategory(id, opts)));
// @ts-ignore
return postItemsOfCategories.flat();
}
/**
* Fetch the partial information, for all posts of the forum
*/
async getPostItems(opts = {}) {
const categories = await this.getCategories();
const categoryIDs = categories.map((i) => i.id);
return this.getPostItemsOfCategories(categoryIDs, opts);
}
/**
* Fetch the whole information, for a specific post of the forum
*/
getPost(id, opts = {}) {
const url = `${this.host}/posts/${id}.json`;
return this.fetch({ url, ...opts });
}
async createReply(postId, raw, category) {
const url = `${this.host}/posts.json`;
const data = new URLSearchParams();
data.append('raw', raw);
data.append('unlist_topic', 'false');
data.append('category', String(category));
data.append('topic_id', String(postId));
data.append('is_warning', 'false');
data.append('archetype', 'regular');
data.append('featured_link', '');
data.append('shared_draft', 'false');
data.append('nested_post', 'true');
return await axios.post(url, data.toString(), {
headers: {
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Api-Key': this.key,
'Api-Username': this.username
}
});
}
async changeOwner(postId, topicId, owner) {
const url = `${this.host}/t/${topicId}/change-owner.json`;
const body = `post_ids%5B%5D=${postId}&username=${owner}`;
return await axios.post(url, body, {
headers: {
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Api-Key': this.key,
'Api-Username': this.username
}
});
}
async createUser(data) {
const url = `${this.host}/users`;
return await this._post(url, data);
}
async getUserByUsername(username) {
const url = `${this.host}/u/${username}.json`;
const response = await this.fetch({ url });
return response.user;
}
async setUserAvatar(user_name, upload_id) {
// fetch whole posts
const url = `${this.host}/u/${user_name}/preferences/avatar/pick.json`;
return await axios.put(url, {
upload_id,
username: user_name,
type: 'uploaded'
}, {
headers: {
'accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.8',
'Api-Key': this.key,
'Api-Username': this.username
}
});
}
async updateUser(user_name, args) {
const url = `${this.host}/u/${user_name}.json`;
return await axios.put(url, {
...args
}, {
headers: {
'accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.8',
'Api-Key': this.key,
'Api-Username': this.username
}
});
}
async updateGroup(user_name, group) {
// fetch whole posts
const url = `${this.host}/groups/${group}/members.json`;
const t = axios.put(url, {
usernames: user_name,
notify_users: false
}, {
headers: {
'accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.8',
'Api-Key': this.key,
'Api-Username': this.username
}
});
t.then((d) => {
}).catch((e) => {
//debugger;
});
return t;
}
async upload(userId, file) {
// fetch whole posts
const url = `${this.host}/uploads.json`;
let data = new FormData();
const fsData = path.parse(file);
data.append('file', file, fsData.base);
data.append('user_id', userId);
data.append('upload_type', 'avatar');
data.append('file', fs.createReadStream(file));
return (await axios.post(url, data, {
headers: {
'accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.8',
'Content-Type': `multipart/form-data; boundary=${data._boundary}`,
'Api-Key': this.key,
'Api-Username': this.username
}
})).data;
}
async uploadFile(userId, file) {
// fetch whole posts
const url = `${this.host}/uploads.json`;
let data = new FormData();
const fsData = path.parse(file);
data.append('file', file, fsData.base);
data.append('user_id', userId);
data.append('upload_type', 'composer');
data.append('file', fs.createReadStream(file));
return (await axios.post(url, data, {
headers: {
'accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.8',
'Content-Type': `multipart/form-data; boundary=${data._boundary}`,
'Api-Key': this.key,
'Api-Username': this.username
}
})).data;
}
// =================================
// POSTS: UPDATING
/**
* Fetch the whole information, for all posts, or specific posts, of the forum
*/
async getPosts(postIDs, opts = {}) {
// if no posts, use all
if (!postIDs) {
const posts = await this.getPostItems(opts);
postIDs = posts.map((i) => i.id);
}
// fetch whole posts
return await Promise.all(postIDs.map((id) => this.getPost(id, opts)));
}
async createPost(title, raw, category) {
// fetch whole posts
const url = `${this.host}/posts`;
return new Promise((resolve) => {
return this._post(url, { raw, title, category }).then((d) => {
resolve(d);
}).catch((e) => {
resolve(e.response.data);
});
});
}
/**
* Update a post with the content
* @param postID the identifier of the post to update
* @param content the new raw content for the post
* @param reason the reason, if provided, for modifying the post
* @param old if the old raw content is provided, then the update verified that you are working with the latest post content before applying the update
*/
async updatePost(postID, content, reason = 'api update', old) {
// prepare the request
const data = {
post: {
raw: content,
edit_reason: reason,
},
};
if (old) {
data.post.raw_old = old;
}
// send the update
const url = `${this.host}/posts/${postID}.json`;
const response = await this.fetch({
url,
request: {
method: 'put',
body: JSON.stringify(data),
},
});
// return the response
return response.post;
}
/**
* Update post meta
*/
async updateTopic(postId, category_id, title, tags) {
const data = {
title,
tags: tags || [],
featuredLink: null,
category_id
};
const url = `${this.host}/t/${postId}.json`;
const response = await this.fetch({
url,
request: {
method: 'put',
body: JSON.stringify(data),
},
});
return response.basic_topic;
}
async rebakePost(postID) {
const url = `${this.host}/posts/${postID}/rebake`;
logger.debug('rebaking', postID);
const response = await this.fetch({
url,
request: {
method: 'put'
}
});
logger.debug('rebaked', postID);
return response;
}
/**
* Modify a post using a modifier
*/
async modifyPost(post, modifier) {
// check if we received a post item, instead of a post response
if (post.raw == null) {
post = await this.getPost(post.id);
}
// check
if (!post.raw) {
return Promise.resolve(null);
}
// replace
const { result, reason } = modifier(post);
if (result === post.raw) {
// if (post.cooked) {
// const { result, reason } = modifier(post.cooked)
// if (result !== post.cooked) {
// logger.debug(
// 'replace did have an effect on cooked post',
// postID,
// 'so rebaking it'
// )
// return await this.rebakePost(postID)
// }
// }
return Promise.resolve(null);
}
// dry
if (this.dry) {
return Promise.resolve({
...post,
result,
reason,
});
}
// update
try {
return await this.updatePost(post.id, result, reason, post.raw);
}
catch (err) {
if (err.message.includes('That post was edited by another user and your changes can no longer be saved.')) {
return this.modifyPost(await this.getPost(post.id, { useCache: false }), modifier);
}
logger.error(err);
return Promise.reject(`modifying post ${post.id} failed`);
}
}
/**
* Modify a post (via its post identifier) using a modifier
*/
async modifyPostID(post, modifier) {
return this.modifyPost(await this.getPost(post), modifier);
}
/**
* Modify a post (via fetching the whole post from the partial post identifier) using a modifier
*/
async modifyPostItem(post, modifier) {
return this.modifyPost(await this.getPost(post.id), modifier);
}
/**
* Run the post modifier on all specified posts
*/
async modifyPosts(posts, modifier) {
const updates = await Promise.all(posts.map((post) => this.modifyPost(post, modifier)));
const updated = updates.filter((i) => i);
return updated;
}
// =================================
// THREADS
/**
* Fetch the partial information, for all posts of a specific topic
* Alias of {@link .getPostItemsOfTopic}.
*/
async getThread(topicID, opts = {}) {
const topic = await this.getTopic(topicID, opts);
const [post, ...replies] = await this.getPostItemsOfTopic(topicID, opts);
return {
topic,
post,
replies,
};
}
/**
* Fetch the partial information, for all posts of specific topics, grouped by topic
*/
async getThreads(topicIDs, opts = {}) {
return await Promise.all(topicIDs.map((id) => this.getThread(id, opts)));
}
/**
* Fetch the partial information, for all posts of specific categories, grouped by topic
*/
async getThreadsOfCategory(categoryID, opts = {}) {
// fetch topics for the category
const topics = await this.getTopicItemsOfCategory(categoryID, opts);
const topicIDs = topics.map((i) => i.id);
// return threads
return await this.getThreads(topicIDs);
}
/**
* Fetch the partial information, for all posts of specific categories, grouped by category, then topic
*/
async getThreadsOfCategories(categoryIDs, opts = {}) {
return await Promise.all(categoryIDs.map((id) => this.getThreadsOfCategory(id, opts)));
}
async updateUserProfile(userId, prefs) {
const url = `${this.host}/u/${userId}.json`;
let data = new FormData();
data.append('bio_raw', prefs.bio_raw);
prefs.location && data.append('location', prefs.location);
prefs.website && data.append('website', prefs.website);
return await axios.put(url, data, {
headers: {
'accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.8',
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
'Api-Key': this.key,
'Api-Username': this.username
}
});
}
}
export const Instance = (config, key = 'discourse_admin') => {
return new Discourser(config || CONFIG_DEFAULT()[key]);
/*
d.getTopicItemsOfCategories([cat]).then(posts => {
//console.log('posts', posts)
let content = "<ul>"
posts = posts.map((p) => {
const url = `${config.discourse.host}/t/${p.id}`;
const title = `${p.fancy_title}`;
return `<li><a href="${url}">${title}</a></li>`;
}).join('\n');
content += posts + "</ul>";
resolve(content);
});
*/
};
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,697 @@
export interface Failure {
success: 'OK';
}
export interface Success {
failed: 'FAILED';
}
export type Response = Failure & Success;
export interface Action {
can_act: boolean;
id: number;
count?: number;
hidden?: boolean;
}
export interface Poster {
description: string;
extras: string;
user_id: number;
}
export interface Person {
avatar_template: string;
id: number;
username: string;
}
export interface Participant extends Person {
post_count: number;
}
export interface Link {
url: string;
internal: boolean;
reflection: boolean;
title: string;
clicks: number;
}
/**
* Update a Topic Timestamp
* https://docs.discourse.org/#tag/Topics/paths/~1t~1{id}~1change-timestamp/put
*/
export interface TopicUpdateTimestampRequest {
timestamp: number;
}
export type TopicUpdateTimestampResponse = Response;
/**
* Update a Post
* https://docs.discourse.org/#tag/Posts/paths/~1posts~1{id}.json/put
*/
export interface PostUpdateResponse {
post: PostUpdateItem;
}
export interface TopicUpdateBasicTopic {
fancy_title: string;
id: number;
posts_count: number;
slug: string;
title: string;
}
export interface TopicUpdateResponse {
basic_topic: TopicUpdateBasicTopic;
}
export interface PostUpdateItem {
actions_summary: Array<Action>;
admin: boolean;
avatar_template: string;
avg_time: object;
can_delete: boolean;
can_edit: boolean;
can_recover: boolean;
can_view_edit_history: boolean;
can_wiki: boolean;
cooked: string;
created_at: string;
deleted_at: object;
display_username: string;
draft_sequence: number;
edit_reason: object;
hidden_reason_id: object;
hidden: boolean;
id: number;
incoming_link_count: number;
moderator: boolean;
name: string;
post_number: number;
post_type: number;
primary_group_flair_bg_color: object;
primary_group_flair_color: object;
primary_group_flair_url: object;
primary_group_name: object;
quote_count: number;
reads: number;
reply_count: number;
reply_to_post_number: object;
score: number;
staff: boolean;
topic_id: number;
topic_slug: string;
trust_level: number;
updated_at: string;
user_deleted: boolean;
user_id: number;
user_title: object;
username: string;
version: number;
wiki: boolean;
yours: boolean;
}
export interface PostUpdateRequest {
post: {
raw: string;
raw_old?: string;
edit_reason?: string;
cooked?: string;
};
}
export interface TagsResponse {
tags: Tag[];
extras: TagsExtras;
}
export interface TagsExtras {
categories: any[];
}
export interface Tag {
count: number;
description: null;
id: string;
name: string;
pm_only: boolean;
target_tag: null;
text: string;
}
/** https://docs.discourse.org/#tag/Categories/paths/~1categories.json/get */
export interface CategoriesResponse {
category_list: {
can_create_category: boolean;
can_create_topic: boolean;
categories: Category[];
draft_key: string;
draft_sequence: number;
draft: boolean;
};
}
export interface Category {
background_url: string;
can_edit: boolean;
color: string;
description_excerpt: string;
description_text: string;
description: string;
has_children: boolean;
id: number;
logo_url: string;
name: string;
notification_level: string;
permission: number;
position: number;
post_count: number;
read_restricted: boolean;
slug: string;
text_color: string;
topic_count: number;
topic_template: string;
topic_url: string;
topics_all_time: number;
topics_day: number;
topics_month: number;
topics_week: number;
topics_year: number;
}
/**
* Get Single Topic
* https://docs.discourse.org/#tag/Topics/paths/~1t~1{id}.json/get
*/
export interface TopicResponse {
actions_summary: Array<Action>;
archetype: string;
archived: boolean;
bookmarked: object;
category_id: number;
chunk_size: number;
closed: boolean;
created_at: string;
deleted_at: object;
deleted_by: object;
details: TopicDetails;
draft_key: string;
draft_sequence: object;
draft: object;
fancy_title: string;
has_summary: boolean;
highest_post_number: number;
id: number;
last_posted_at: object;
like_count: number;
participant_count: number;
pinned_at: string;
pinned_globally: boolean;
pinned_until: object;
pinned: boolean;
posts_count: number;
reply_count: number;
slug: string;
tags: string[];
title: string;
unpinned: object;
user_id: number;
views: number;
visible: boolean;
word_count: object;
post_stream: {
posts: Array<PostItem>;
stream: Array<object>;
};
timeline_lookup: [
{
'0': Array<object>;
}
];
}
export interface TopicDetails {
auto_close_at: object;
auto_close_based_on_last_post: boolean;
auto_close_hours: object;
can_flag_topic: boolean;
created_by: Person;
last_poster: Person;
notification_level: number;
participants: Array<Participant>;
suggested_topics: Array<TopicItem>;
}
export interface TopicItem {
archetype: string;
archived: boolean;
bookmarked: object;
bumped_at: string;
bumped: boolean;
category_id: number;
closed: boolean;
created_at: string;
excerpt: string;
fancy_title: string;
has_summary: boolean;
highest_post_number: number;
id: number;
image_url: string;
last_posted_at: string;
last_poster_username: string;
like_count: number;
liked: object;
pinned_globally: boolean;
pinned: boolean;
posters: Array<Poster>;
posts_count: number;
reply_count: number;
slug: string;
title: string;
unpinned: boolean;
unseen: boolean;
views: number;
visible: boolean;
}
/**
* Get Topics for Category
* https://docs.discourse.org/#tag/Categories/paths/~1c~1{id}.json/get
*/
export interface CategoryResponse {
users: Person[];
topic_list: {
can_create_topic: boolean;
draft: boolean;
draft_key: string;
draft_sequence: number;
per_page: number;
topics: Array<TopicItem>;
};
}
/**
* Whole Post Information
* As returned by getting a single Post
* https://docs.discourse.org/#tag/Posts/paths/~1posts~1{id}.json/get
*/
export interface PostResponse {
actions_summary: Array<Action>;
admin: boolean;
avatar_template: string;
avg_time: object;
can_delete: boolean;
can_edit: boolean;
can_recover: boolean;
can_view_edit_history: boolean;
can_wiki: boolean;
cooked: string;
created_at: string;
deleted_at: object;
display_username: string;
edit_reason: object;
hidden_reason_id: object;
hidden: boolean;
id: number;
incoming_link_count: number;
moderator: boolean;
name: string;
post_number: number;
post_type: number;
primary_group_flair_bg_color: object;
primary_group_flair_color: object;
primary_group_flair_url: object;
primary_group_name: object;
quote_count: number;
raw: string;
reads: number;
reply_count: number;
reply_to_post_number: object;
score: number;
staff: boolean;
topic_id: number;
topic_slug: string;
trust_level: number;
updated_at: string;
user_deleted: boolean;
user_id: number;
user_title: object;
username: string;
version: number;
wiki: boolean;
yours: boolean;
}
export interface ICreateUserResponse {
success: boolean;
active: boolean;
message: string;
user_id: number;
password: string;
}
/**
* Get the Posts of a Topic
* https://docs.discourse.org/#tag/Topics/paths/~1t~1{id}~1posts.json/get
*/
export interface PostsResponse {
post_stream: {
posts: Array<PostItem>;
};
id: number;
}
/**
* Partial Post Information
* As returned by a listing
*/
export interface PostItem {
accepted_answer: boolean;
actions_summary: Array<Action>;
admin: boolean;
avatar_template: string;
can_accept_answer: boolean;
can_delete: boolean;
can_edit: boolean;
can_recover: boolean;
can_unaccept_answer: boolean;
can_view_edit_history: boolean;
can_wiki: boolean;
cooked: string;
created_at: string;
deleted_at: null;
display_username: string;
edit_reason: null;
hidden: boolean;
id: number;
incoming_link_count: number;
link_counts: Array<Link>;
moderator: boolean;
name: string;
post_number: number;
post_type: number;
primary_group_flair_bg_color: null | object;
primary_group_flair_color: null | object;
primary_group_flair_url: null | object;
primary_group_name: null | object;
quote_count: number;
read: boolean;
readers_count: number;
reads: number;
reply_count: number;
reply_to_post_number: null | number;
reviewable_id: number;
reviewable_score_count: number;
reviewable_score_pending_count: number;
score: number;
staff: boolean;
topic_id: number;
topic_slug: string;
trust_level: number;
updated_at: string;
user_deleted: boolean;
user_id: number;
user_title: null | object;
username: string;
version: number;
wiki: boolean;
yours: boolean;
}
export type Thread = {
topic: TopicResponse;
post: PostItem;
replies: PostItem[];
};
/** When finding and replacing, determine replacements using a method that matches this */
export type PostModifier = (post: PostResponse) => {
result: string;
reason?: string;
};
/** Configuration for Discourser */
export interface IDiscourserConfig {
/** the discourse hostname to connect to, including protocol */
host: string;
/** the API key to connect with */
key: string;
/** the username to behave as */
username: string;
/** the cache directory to use, if we are caching */
cache?: string;
/** Whether or not we should read from the cache */
useCache?: boolean;
/** whether or not updates should be dry (non-applying) */
dry?: boolean;
/** how many concurrency requests to send to the server at once */
rateLimitConcurrency?: number;
}
export interface FetchOptions {
/** Whether or not we should read from the cache */
useCache?: boolean;
/** Only applicable to fetching topics of category */
page?: number;
/** Any thing to init the fetch call with? */
request?: RequestInit;
include_subcategories?: boolean;
}
export interface FetchConfig extends FetchOptions {
url: string;
}
export interface PostConfig extends FetchOptions {
url: string;
data: any;
}
export interface ISearchPost {
id: number;
name: string;
username: string;
avatar_template: string;
created_at: Date;
like_count: number;
blurb: string;
post_number: number;
topic_id: number;
}
export interface ISearchTagsDescriptions {
}
export interface ISearchTopic {
id: number;
title: string;
fancy_title: string;
slug: string;
posts_count: number;
reply_count: number;
highest_post_number: number;
created_at: Date;
last_posted_at: Date;
bumped: boolean;
bumped_at: Date;
archetype: string;
unseen: boolean;
pinned: boolean;
unpinned?: any;
excerpt: string;
visible: boolean;
closed: boolean;
archived: boolean;
bookmarked?: any;
liked?: any;
tags: any[];
tags_descriptions: ISearchTagsDescriptions;
category_id: number;
has_accepted_answer: boolean;
}
export interface IGroupedSearchResult {
more_posts?: any;
more_users?: any;
more_categories?: any;
term: string;
search_log_id: number;
more_full_page_results?: any;
can_create_topic: boolean;
error?: any;
post_ids: number[];
user_ids: any[];
category_ids: any[];
tag_ids: any[];
group_ids: any[];
}
export interface ISearchResult {
posts: ISearchPost[];
topics: ISearchTopic[];
users: any[];
categories: any[];
tags: any[];
groups: any[];
grouped_search_result: IGroupedSearchResult;
}
export interface IUserDetail {
id: number;
username: string;
name: string;
avatar_template: string;
email: string;
secondary_emails: any[];
active: boolean;
admin: boolean;
moderator: boolean;
last_seen_at: string;
last_emailed_at: string;
created_at: string;
last_seen_age: number;
last_emailed_age: number;
created_at_age: number;
trust_level: number;
manual_locked_trust_level: any;
flag_level: number;
title: string;
time_read: number;
staged: boolean;
days_visited: number;
posts_read_count: number;
topics_entered: number;
post_count: number;
associated_accounts: AssociatedAccount[];
can_send_activation_email: boolean;
can_activate: boolean;
can_deactivate: boolean;
ip_address: string;
registration_ip_address: string;
can_grant_admin: boolean;
can_revoke_admin: boolean;
can_grant_moderation: boolean;
can_revoke_moderation: boolean;
can_impersonate: boolean;
like_count: number;
like_given_count: number;
topic_count: number;
post_edits_count: number;
flags_given_count: number;
flags_received_count: number;
private_topics_count: number;
can_delete_all_posts: boolean;
can_be_deleted: boolean;
can_be_anonymized: boolean;
can_be_merged: boolean;
full_suspend_reason: any;
silence_reason: any;
penalty_counts: PenaltyCounts;
next_penalty: string;
primary_group_id: any;
badge_count: number;
warnings_received_count: number;
user_fields: UserFields;
bounce_score: number;
reset_bounce_score_after: any;
can_view_action_logs: boolean;
can_disable_second_factor: boolean;
can_delete_sso_record: boolean;
api_key_count: number;
external_ids: ExternalIds;
single_sign_on_record: any;
approved_by: ApprovedBy;
suspended_by: any;
silenced_by: any;
tl3_requirements: Tl3Requirements;
groups: Group[];
}
export interface AssociatedAccount {
name: string;
description: string;
}
export interface PenaltyCounts {
silenced: number;
suspended: number;
}
export interface UserFields {
"1": string;
"2": string;
"3": string;
"4": string;
"5": string;
}
export interface ExternalIds {
google_oauth2: string;
}
export interface ApprovedBy {
id: number;
username: string;
name: string;
avatar_template: string;
}
export interface Tl3Requirements {
time_period: number;
requirements_met: boolean;
requirements_lost: boolean;
trust_level_locked: boolean;
on_grace_period: boolean;
days_visited: number;
min_days_visited: number;
num_topics_replied_to: number;
min_topics_replied_to: number;
topics_viewed: number;
min_topics_viewed: number;
posts_read: number;
min_posts_read: number;
topics_viewed_all_time: number;
min_topics_viewed_all_time: number;
posts_read_all_time: number;
min_posts_read_all_time: number;
num_flagged_posts: number;
max_flagged_posts: number;
num_flagged_by_users: number;
max_flagged_by_users: number;
num_likes_given: number;
min_likes_given: number;
num_likes_received: number;
min_likes_received: number;
num_likes_received_days: number;
min_likes_received_days: number;
num_likes_received_users: number;
min_likes_received_users: number;
penalty_counts: PenaltyCounts2;
}
export interface PenaltyCounts2 {
silenced: number;
suspended: number;
total: number;
}
export interface Group {
id: number;
automatic: boolean;
name: string;
display_name?: string;
user_count: number;
mentionable_level: number;
messageable_level: number;
visibility_level: number;
primary_group: boolean;
title: any;
grant_trust_level?: number;
incoming_email: any;
has_messages: boolean;
flair_url: any;
flair_bg_color?: string;
flair_color?: string;
bio_raw?: string;
bio_cooked?: string;
bio_excerpt?: string;
public_admission: boolean;
public_exit: boolean;
allow_membership_requests: boolean;
full_name?: string;
default_notification_level: number;
membership_request_template: any;
members_visibility_level: number;
can_see_members: boolean;
can_admin_group: boolean;
publish_read_state: boolean;
can_edit_group?: boolean;
}
export type TPostStatus = 'visible' | 'archived' | 'pinned' | 'closed';
export interface TPostStatusUpdate {
success: string;
topic_status_update: any;
}
export interface UserPreferencesUpdate {
bio_raw?: string;
website?: string;
location?: string;
custom_fields?: CustomFields;
timezone?: string;
default_calendar?: string;
profile_background_upload_url?: string;
card_background_upload_url?: string;
}
export interface CustomFields {
geo_location: GeoLocation;
}
export interface GeoLocation {
lat: string;
lon: string;
address: string;
countrycode: string;
city: string;
state: string;
country: string;
postalcode: string;
boundingbox: string[];
type: string;
}

View File

@ -0,0 +1,4 @@
// Attempt at TypeScript Types for the Discourse API
// https://docs.discourse.org
export {};
//# sourceMappingURL=types.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/lib/discourse/types.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,6BAA6B"}

View File

@ -0,0 +1,2 @@
export * from './discourse/index.js';
export * from './discourse/types.js';

View File

@ -0,0 +1,3 @@
export * from './discourse/index.js';
export * from './discourse/types.js';
//# sourceMappingURL=index.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,sBAAsB,CAAA"}

View File

@ -0,0 +1,7 @@
import { RegExCallback } from './types.js';
export declare class Pattern {
regex: RegExp;
replacement: RegExCallback;
constructor(regex: RegExp, replacement: any);
apply(raw: string): string;
}

View File

@ -0,0 +1,12 @@
export class Pattern {
regex;
replacement;
constructor(regex, replacement) {
this.regex = regex;
this.replacement = replacement;
}
apply(raw) {
return raw.replace(this.regex, this.replacement);
}
}
//# sourceMappingURL=Pattern.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"Pattern.js","sourceRoot":"","sources":["../../../../src/lib/markdown/Pattern.ts"],"names":[],"mappings":"AACA,MAAM,OAAO,OAAO;IAClB,KAAK,CAAS;IACd,WAAW,CAAe;IAC1B,YAAY,KAAa,EAAE,WAAgB;QACzC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;IAChC,CAAC;IAED,KAAK,CAAC,GAAW;QACf,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;IAClD,CAAC;CACF"}

View File

@ -0,0 +1,7 @@
import { Pattern } from './Pattern.js';
export declare class Rule {
name: string;
patterns: Pattern[];
constructor(name: string, patterns: Pattern[]);
apply(raw: string): string;
}

View File

@ -0,0 +1,12 @@
export class Rule {
name;
patterns;
constructor(name, patterns) {
this.name = name;
this.patterns = patterns;
}
apply(raw) {
return this.patterns.reduce((result, pattern) => pattern.apply(result), raw);
}
}
//# sourceMappingURL=Rule.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"Rule.js","sourceRoot":"","sources":["../../../../src/lib/markdown/Rule.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,IAAI;IACf,IAAI,CAAS;IACb,QAAQ,CAAY;IACpB,YAAY,IAAY,EAAE,QAAmB;QAC3C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,GAAW;QACf,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CACzB,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAC1C,GAAG,CACJ,CAAC;IACJ,CAAC;CACF"}

View File

@ -0,0 +1,14 @@
import { Rule } from './Rule.js';
import { RMarkOptions } from './types.js';
export declare const RE_IMAGES: RegExp;
export declare const RE_LINKS: RegExp;
export declare class RMark {
constructor(options: RMarkOptions);
private rules;
addRuleBefore(rule: Rule, before: string): RMark;
addRule(rule: Rule): RMark;
render(raw: string): string;
}
export { Rule } from './Rule.js';
export { Pattern } from './Pattern.js';
export declare const toHTML: (content: any) => string;

View File

@ -0,0 +1,98 @@
import { Rule } from './Rule.js';
import { Pattern } from './Pattern.js';
export const RE_IMAGES = /\!\[([^\]]+)\]\((\S+)\)/g;
export const RE_LINKS = /\[([^\n]+)\]\(([^\n]+)\)/g;
import markdown from 'markdown-it';
const defaultRules = [
new Rule('header', [
new Pattern(/^#{6}\s?([^\n]+)/gm, '<h6>$1</h6>'),
new Pattern(/^#{5}\s?([^\n]+)/gm, '<h5>$1</h5>'),
new Pattern(/^#{4}\s?([^\n]+)/gm, '<h4>$1</h4>'),
new Pattern(/^#{3}\s?([^\n]+)/gm, '<h3>$1</h3>'),
new Pattern(/^#{2}\s?([^\n]+)/gm, '<h2>$1</h2>'),
new Pattern(/^#{1}\s?([^\n]+)/gm, '<h1>$1</h1>'),
]),
new Rule('bold', [
new Pattern(/\*\*\s?([^\n]+)\*\*/g, '<b>$1</b>'),
new Pattern(/\_\_\s?([^\n]+)\_\_/g, '<b>$1</b>'),
]),
new Rule('italic', [
new Pattern(/\*\s?([^\n]+)\*/g, '<i>$1</i>'),
new Pattern(/\_\s?([^\n]+)\_/g, '<i>$1</i>'),
]),
new Rule('image', [
new Pattern(/\!\[([^\]]+)\]\((\S+)\)/g, '<img src="$2" alt="$1" />'),
]),
new Rule('link', [
new Pattern(/\[([^\n]+)\]\(([^\n]+)\)/g, '<a href2="$2" target="_blank" rel="noopener">$1</a>'),
]),
new Rule('paragraph', [
// this regex can't skip processed HTML
new Pattern(/([^\n]+\n?)/g, '\n<p>$1</p>\n'),
// another possible regex that can't skip processed HTML
// new Pattern(/(?:^|\n)([^\n\<]+(?:\n[^\n\>]+)*)(?:\n|$)/gm, '\n<p>$1</p>\n'),
])
];
const defaultRulesDiscourse = (images, links) => {
return [
new Rule('image', [
new Pattern(RE_LINKS, images)
]) /*,
new Rule('link', [
new Pattern(
RE_LINKS,
links
)
])*/
];
};
export class RMark {
constructor(options) {
this.rules = defaultRulesDiscourse(options.images, options.links);
}
rules;
addRuleBefore(rule, before) {
const index = this.rules.findIndex((r) => r.name === before);
if (index !== -1) {
this.rules.splice(index, 0, rule);
}
return this;
}
addRule(rule) {
this.addRuleBefore(rule, 'paragraph');
return this;
}
render(raw) {
let result = raw;
this.rules.forEach((rule) => {
result = rule.apply(result);
});
return result;
}
}
export { Rule } from './Rule.js';
export { Pattern } from './Pattern.js';
// export const find = (content:string, reg:RegExp) => content.match(reg)
export const toHTML = (content) => {
const md = new markdown({
html: true,
breaks: true
});
return md.render(content);
};
function image_urls(input) {
const regex = /https?:\/\/(?:[a-z0-9\-]+\.)+[a-z]{2,}(?:\/[^\/#\s]*)*\.(?:jpe?g|gif|png|webp)/g;
const matches = input.match(regex);
return matches || [];
}
function image_urls_local(input) {
const regex = /\/(?:[^\/#\s]+\/)*[^\/#\s]+\.(?:jpe?g|gif|png|webp)/g;
const matches = input.match(regex);
return matches || [];
}
function findUploadImageUrls(input) {
const regex = /upload:\/\/[^\s]+?\.(?:jpe?g|gif|png)/gi;
const matches = input.match(regex);
return matches || [];
}
//# sourceMappingURL=index.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/lib/markdown/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAItC,MAAM,CAAC,MAAM,SAAS,GAAW,0BAA0B,CAAA;AAC3D,MAAM,CAAC,MAAM,QAAQ,GAAW,2BAA2B,CAAA;AAE3D,OAAO,QAAQ,MAAM,aAAa,CAAA;AAElC,MAAM,YAAY,GAAW;IAC3B,IAAI,IAAI,CAAC,QAAQ,EAAE;QACjB,IAAI,OAAO,CAAC,oBAAoB,EAAE,aAAa,CAAC;QAChD,IAAI,OAAO,CAAC,oBAAoB,EAAE,aAAa,CAAC;QAChD,IAAI,OAAO,CAAC,oBAAoB,EAAE,aAAa,CAAC;QAChD,IAAI,OAAO,CAAC,oBAAoB,EAAE,aAAa,CAAC;QAChD,IAAI,OAAO,CAAC,oBAAoB,EAAE,aAAa,CAAC;QAChD,IAAI,OAAO,CAAC,oBAAoB,EAAE,aAAa,CAAC;KACjD,CAAC;IACF,IAAI,IAAI,CAAC,MAAM,EAAE;QACf,IAAI,OAAO,CAAC,sBAAsB,EAAE,WAAW,CAAC;QAChD,IAAI,OAAO,CAAC,sBAAsB,EAAE,WAAW,CAAC;KACjD,CAAC;IACF,IAAI,IAAI,CAAC,QAAQ,EAAE;QACjB,IAAI,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC;QAC5C,IAAI,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC;KAC7C,CAAC;IACF,IAAI,IAAI,CAAC,OAAO,EAAE;QAChB,IAAI,OAAO,CAAC,0BAA0B,EAAE,2BAA2B,CAAC;KACrE,CAAC;IACF,IAAI,IAAI,CAAC,MAAM,EAAE;QACf,IAAI,OAAO,CACT,2BAA2B,EAC3B,qDAAqD,CACtD;KACF,CAAC;IACF,IAAI,IAAI,CAAC,WAAW,EAAE;QACpB,uCAAuC;QACvC,IAAI,OAAO,CAAC,cAAc,EAAE,eAAe,CAAC;QAC5C,wDAAwD;QACxD,+EAA+E;KAChF,CAAC;CACH,CAAA;AAED,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;IAC9C,OAAO;QACL,IAAI,IAAI,CAAC,OAAO,EAAE;YAChB,IAAI,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC;SAC9B,CAAC,CAAA;;;;;;YAME;KACL,CAAA;AACH,CAAC,CAAA;AAED,MAAM,OAAO,KAAK;IAEhB,YAAY,OAAqB;QAC/B,IAAI,CAAC,KAAK,GAAG,qBAAqB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;IACnE,CAAC;IAEO,KAAK,CAAQ;IAEd,aAAa,CAAC,IAAU,EAAE,MAAc;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QAC7D,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE;YAChB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;SACnC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,OAAO,CAAC,IAAU;QACvB,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,MAAM,CAAC,GAAW;QACvB,IAAI,MAAM,GAAG,GAAG,CAAC;QACjB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YAC1B,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAED,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,yEAAyE;AAKzE,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,EAAE;IAEhC,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC;QACtB,IAAI,EAAE,IAAI;QACV,MAAM,EAAE,IAAI;KACb,CAAC,CAAA;IAEF,OAAO,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;AAC3B,CAAC,CAAA;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,MAAM,KAAK,GAAG,iFAAiF,CAAA;IAC/F,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAClC,OAAO,OAAO,IAAI,EAAE,CAAA;AACtB,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAa;IACrC,MAAM,KAAK,GAAG,sDAAsD,CAAA;IACpE,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAClC,OAAO,OAAO,IAAI,EAAE,CAAA;AACtB,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAa;IACxC,MAAM,KAAK,GAAG,yCAAyC,CAAC;IACxD,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,OAAO,IAAI,EAAE,CAAC;AACvB,CAAC"}

View File

@ -0,0 +1 @@
export {};

View File

@ -1,5 +1,4 @@
import { Pattern, RMark, Rule } from './index';
import { Pattern, RMark, Rule } from './index';
const sampleText = `# Header 1
## Header 2
### Header 3
@ -19,8 +18,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam ornare erat fac
Sed pellentesque nulla sit amet tincidunt sagittis. Phasellus eget justo nulla. Cras nisi odio, lobortis nec ante eget, commodo euismod
turpis. Cras id orci dolor. Etiam auctor, nisl luctus volutpat lacinia, turpis orci euismod magna, pharetra eleifend massa metus aliquet
`;
`;
const sampleHtml = `
<p><h1>Header 1</h1>
</p>
@ -68,77 +66,47 @@ const sampleHtml = `
<p>turpis. Cras id orci dolor. Etiam auctor, nisl luctus volutpat lacinia, turpis orci euismod magna, pharetra eleifend massa metus aliquet
</p>
`;
describe('testing index file', () => {
test('empty string should render nothing', () => {
expect(new RMark().render('')).toBe('');
});
test('should render paragraph', () => {
expect(
new RMark().render(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
)
).toBe(
'\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>\n'
);
});
test('should render header', () => {
expect(new RMark().render('# Header 1')).toBe(
'\n<p><h1>Header 1</h1></p>\n'
);
expect(new RMark().render('## Header 2')).toBe(
'\n<p><h2>Header 2</h2></p>\n'
);
expect(new RMark().render('### Header 3')).toBe(
'\n<p><h3>Header 3</h3></p>\n'
);
expect(new RMark().render('#### Header 4')).toBe(
'\n<p><h4>Header 4</h4></p>\n'
);
expect(new RMark().render('##### Header 5')).toBe(
'\n<p><h5>Header 5</h5></p>\n'
);
expect(new RMark().render('###### Header 6')).toBe(
'\n<p><h6>Header 6</h6></p>\n'
);
});
test('should render bold', () => {
expect(new RMark().render('**Bold**')).toBe('\n<p><b>Bold</b></p>\n');
expect(new RMark().render('__Bold__')).toBe('\n<p><b>Bold</b></p>\n');
expect(new RMark().render('This is **Bold**')).toBe(
'\n<p>This is <b>Bold</b></p>\n'
);
});
test('should render italic', () => {
expect(new RMark().render('*Italic*')).toBe('\n<p><i>Italic</i></p>\n');
expect(new RMark().render('_Italic_')).toBe('\n<p><i>Italic</i></p>\n');
});
test('should render image', () => {
expect(
new RMark().render(
'![Image](https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png)'
)
).toBe(
'\n<p><img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="Image" /></p>\n'
);
});
test('should render link', () => {
expect(new RMark().render('[Link](https://github.com)')).toBe(
'\n<p><a href="https://github.com" target="_blank" rel="noopener">Link</a></p>\n'
);
});
test('should render paragraph with multiple lines', () => {
expect(new RMark().render(sampleText)).toBe(sampleHtml);
});
test('should work with adding rules', () => {
const rmark = new RMark();
rmark.addRule(
new Rule('horizontal', [
new Pattern(/^(-{3})/gm, '<hr />'),
new Pattern(/^(_{3})/gm, '<hr />'),
])
);
expect(rmark.render('---')).toBe('\n<p><hr /></p>\n');
});
});
`;
describe('testing index file', () => {
test('empty string should render nothing', () => {
expect(new RMark().render('')).toBe('');
});
test('should render paragraph', () => {
expect(new RMark().render('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')).toBe('\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>\n');
});
test('should render header', () => {
expect(new RMark().render('# Header 1')).toBe('\n<p><h1>Header 1</h1></p>\n');
expect(new RMark().render('## Header 2')).toBe('\n<p><h2>Header 2</h2></p>\n');
expect(new RMark().render('### Header 3')).toBe('\n<p><h3>Header 3</h3></p>\n');
expect(new RMark().render('#### Header 4')).toBe('\n<p><h4>Header 4</h4></p>\n');
expect(new RMark().render('##### Header 5')).toBe('\n<p><h5>Header 5</h5></p>\n');
expect(new RMark().render('###### Header 6')).toBe('\n<p><h6>Header 6</h6></p>\n');
});
test('should render bold', () => {
expect(new RMark().render('**Bold**')).toBe('\n<p><b>Bold</b></p>\n');
expect(new RMark().render('__Bold__')).toBe('\n<p><b>Bold</b></p>\n');
expect(new RMark().render('This is **Bold**')).toBe('\n<p>This is <b>Bold</b></p>\n');
});
test('should render italic', () => {
expect(new RMark().render('*Italic*')).toBe('\n<p><i>Italic</i></p>\n');
expect(new RMark().render('_Italic_')).toBe('\n<p><i>Italic</i></p>\n');
});
test('should render image', () => {
expect(new RMark().render('![Image](https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png)')).toBe('\n<p><img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="Image" /></p>\n');
});
test('should render link', () => {
expect(new RMark().render('[Link](https://github.com)')).toBe('\n<p><a href="https://github.com" target="_blank" rel="noopener">Link</a></p>\n');
});
test('should render paragraph with multiple lines', () => {
expect(new RMark().render(sampleText)).toBe(sampleHtml);
});
test('should work with adding rules', () => {
const rmark = new RMark();
rmark.addRule(new Rule('horizontal', [
new Pattern(/^(-{3})/gm, '<hr />'),
new Pattern(/^(_{3})/gm, '<hr />'),
]));
expect(rmark.render('---')).toBe('\n<p><hr /></p>\n');
});
});
//# sourceMappingURL=index.test.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../../../src/lib/markdown/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/C,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;CAmBlB,CAAC;AAEF,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+ClB,CAAC;AAEF,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACnC,MAAM,CACJ,IAAI,KAAK,EAAE,CAAC,MAAM,CAChB,0DAA0D,CAC3D,CACF,CAAC,IAAI,CACJ,qEAAqE,CACtE,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAC3C,8BAA8B,CAC/B,CAAC;QACF,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAC5C,8BAA8B,CAC/B,CAAC;QACF,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAC7C,8BAA8B,CAC/B,CAAC;QACF,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAC9C,8BAA8B,CAC/B,CAAC;QACF,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAC/C,8BAA8B,CAC/B,CAAC;QACF,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAChD,8BAA8B,CAC/B,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QACtE,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QACtE,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,CACjD,gCAAgC,CACjC,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACxE,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC/B,MAAM,CACJ,IAAI,KAAK,EAAE,CAAC,MAAM,CAChB,qFAAqF,CACtF,CACF,CAAC,IAAI,CACJ,gHAAgH,CACjH,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,4BAA4B,CAAC,CAAC,CAAC,IAAI,CAC3D,iFAAiF,CAClF,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACzC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,CAAC,OAAO,CACX,IAAI,IAAI,CAAC,YAAY,EAAE;YACrB,IAAI,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC;YAClC,IAAI,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC;SACnC,CAAC,CACH,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}

View File

@ -0,0 +1 @@
export {};

View File

@ -1,5 +1,4 @@
import { RMark } from '.';
import { RMark } from './index.js';
const sampleText = `# Header 1
## Header 2
### Header 3
@ -19,10 +18,9 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam ornare erat fac
Sed pellentesque nulla sit amet tincidunt sagittis. Phasellus eget justo nulla. Cras nisi odio, lobortis nec ante eget, commodo euismod
turpis. Cras id orci dolor. Etiam auctor, nisl luctus volutpat lacinia, turpis orci euismod magna, pharetra eleifend massa metus aliquet
`;
const page = document.getElementById('page');
if (page) {
page.innerHTML = new RMark().render(sampleText);
}
`;
const page = document.getElementById('page');
if (page) {
page.innerHTML = new RMark().render(sampleText);
}
//# sourceMappingURL=page.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../../../src/lib/markdown/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;CAmBlB,CAAC;AAEF,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;AAE7C,IAAI,IAAI,EAAE;IACR,IAAI,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;CACjD"}

View File

@ -0,0 +1,5 @@
export type RegExCallback = (match: any, capture: any, arg1: any, arg2: any) => string;
export interface RMarkOptions {
images: RegExCallback;
links?: RegExCallback;
}

View File

@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/lib/markdown/types.ts"],"names":[],"mappings":""}

2
packages/discourse/dist/src/main.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};

14
packages/discourse/dist/src/main.js vendored Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env node
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
import { defaults } from './_cli.js';
defaults();
import { cli } from './cli.js';
const argv = cli.argv;
if (argv.help) {
cli.showHelp();
process.exit();
}
else if (argv.v || argv.version) {
process.exit();
}
//# sourceMappingURL=main.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":";AACA,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,GAAG,GAAG,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAAC,QAAQ,EAAE,CAAC;AACjD,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAC9B,MAAM,IAAI,GAAQ,GAAG,CAAC,IAAI,CAAC;AAE3B,IAAI,IAAI,CAAC,IAAI,EAAE;IACX,GAAG,CAAC,QAAQ,EAAE,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,CAAC;CAClB;KAAM,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE;IAC/B,OAAO,CAAC,IAAI,EAAE,CAAC;CAClB"}

View File

@ -0,0 +1,3 @@
export * from './lib/index.js';
import { IOptions } from './types.js';
export declare const parse: (options: IOptions, argv: any) => IOptions;

21
packages/discourse/dist/src/options.js vendored Normal file
View File

@ -0,0 +1,21 @@
import { resolveConfig } from '@polymech/core';
export * from './lib/index.js';
export const parse = (options, argv) => {
for (const k in argv) {
if (!(k in options.variables) && k !== '_'
&& k !== '$0'
&& k !== 'variables'
&& k !== 'src'
&& k !== 'format'
&& k !== 'profile'
&& k !== 'output') {
options.variables[k] = argv[k];
}
}
options.variables['cwd'] = options.variables['cwd'] ? options.variables['cwd'] : options.cwd;
resolveConfig(options.variables);
let variables = {};
options.pathVariables = variables;
return options;
};
//# sourceMappingURL=options.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE9C,cAAc,gBAAgB,CAAA;AAK9B,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,OAAiB,EAAE,IAAS,EAAY,EAAE;IAE5D,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE;QAClB,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,GAAG;eACnC,CAAC,KAAK,IAAI;eACV,CAAC,KAAK,WAAW;eACjB,CAAC,KAAK,KAAK;eACX,CAAC,KAAK,QAAQ;eACd,CAAC,KAAK,SAAS;eACf,CAAC,KAAK,QAAQ,EAAE;YACnB,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;SAClC;KACJ;IAED,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAA;IAC5F,aAAa,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAChC,IAAI,SAAS,GAAG,EAAE,CAAA;IAClB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAA;IACjC,OAAO,OAAO,CAAA;AAClB,CAAC,CAAA"}

157
packages/discourse/dist/src/types.d.ts vendored Normal file
View File

@ -0,0 +1,157 @@
import { PATH_INFO, SRC_VARIABLES } from '@polymech/commons';
import { EDiscourseConfigKey } from './lib/discourse/constants.js';
import { IProcessingNode } from '@polymech/fs/interfaces';
export type TFindFilter = (path: string) => any;
export interface Hash<T> {
[id: string]: T;
}
export type IOptions = {
src: string;
id: string;
cat: string;
track: string;
variables: Hash<any>;
cwd: string;
env: string;
verb: string;
debug: boolean;
disabled: boolean;
dry?: boolean;
all?: boolean;
stdout: boolean;
pathVariables: Hash<any>;
};
/**
* An enumeration to narrow a conflict resolve to a single item or for all following conflicts.
*
* @export
* @enum {number}
*/
export declare enum EResolve {
/**
* Always will use the chose conflict settings for all following conflicts.
*/
ALWAYS = 0,
/**
* 'This' will use the conflict settings for a single conflict so the conflict callback will be triggered again for the next conflict.
*/
THIS = 1
}
/**
* The possible modes to resolve a conflict during a sync
*
* @export
* @enum {number}
*/
export declare enum EResolveMode {
SKIP = 0,
OVERWRITE = 1,
IF_NEWER = 2,
IF_SIZE_DIFFERS = 3,
THROW = 4,
RETRY = 5,
ABORT = 6
}
export interface IConflictSettings {
/**
* How to resolve this conflict/error.
*
* @type {EResolveMode}
* @memberOf IConflictSettings
*/
overwrite: EResolveMode;
/**
* The scope of this conflict resolver: always or this.
*
* @type {EResolve}
* @memberOf IConflictSettings
*/
mode: EResolve;
/**
* Track the origin error type for this settings.
*
* @type {string}
* @memberOf IConflictSettings
*/
error?: string;
}
export type EMergeConflictMode = 'theirs' | 'mine';
export type EPostType = 'post' | 'reply';
export type ISyncNodeReport = IProcessingNode & {};
export interface IDiscoursePostBaseOptions {
title?: string;
id?: string;
cat?: string | number;
tags?: string;
owner?: string | number;
timestamp?: string | number | Date;
uploadLocal?: boolean;
uploadRemote?: boolean;
yaml?: boolean;
post_id?: number;
topic_id?: number;
type?: EPostType;
user_name?: string;
}
export type IOptionsSync = IDiscoursePostBaseOptions & {
debug?: boolean;
verbose?: boolean;
logLevel?: string;
skip?: boolean;
alt?: boolean;
src?: string;
verb: string;
cache?: boolean;
filter?: TFindFilter | string;
config?: string | EDiscourseConfigKey;
pathVariables?: Hash<any>;
variables?: SRC_VARIABLES;
repo?: string;
root?: string;
product_root?: string;
srcInfo?: PATH_INFO;
post_id?: number;
topic_id?: number;
};
export type IOptionsSyncComponent = IOptionsSync & {
format?: string;
module?: string;
plugins?: string;
onCompiled?: () => void;
onCompileDone?: () => void;
cache?: boolean;
skip?: boolean;
};
export interface IDBConfig {
user: string;
password: string;
database: string;
host: string;
prefix: string;
}
export interface IDiscourseUser {
id: number;
username: string;
name: string;
avatar_template: string;
active: boolean;
admin: boolean;
moderator: boolean;
last_seen_at: any;
last_emailed_at: string;
created_at: string;
last_seen_age: any;
last_emailed_age: number;
created_at_age: number;
trust_level: number;
manual_locked_trust_level: any;
flag_level: number;
title: any;
time_read: number;
staged: boolean;
days_visited: number;
posts_read_count: number;
topics_entered: number;
post_count: number;
detail: any;
}

38
packages/discourse/dist/src/types.js vendored Normal file
View File

@ -0,0 +1,38 @@
///////////////////////////////////////////////////////
//
// Sync Types
//
/**
* An enumeration to narrow a conflict resolve to a single item or for all following conflicts.
*
* @export
* @enum {number}
*/
export var EResolve;
(function (EResolve) {
/**
* Always will use the chose conflict settings for all following conflicts.
*/
EResolve[EResolve["ALWAYS"] = 0] = "ALWAYS";
/**
* 'This' will use the conflict settings for a single conflict so the conflict callback will be triggered again for the next conflict.
*/
EResolve[EResolve["THIS"] = 1] = "THIS";
})(EResolve = EResolve || (EResolve = {}));
/**
* The possible modes to resolve a conflict during a sync
*
* @export
* @enum {number}
*/
export var EResolveMode;
(function (EResolveMode) {
EResolveMode[EResolveMode["SKIP"] = 0] = "SKIP";
EResolveMode[EResolveMode["OVERWRITE"] = 1] = "OVERWRITE";
EResolveMode[EResolveMode["IF_NEWER"] = 2] = "IF_NEWER";
EResolveMode[EResolveMode["IF_SIZE_DIFFERS"] = 3] = "IF_SIZE_DIFFERS";
EResolveMode[EResolveMode["THROW"] = 4] = "THROW";
EResolveMode[EResolveMode["RETRY"] = 5] = "RETRY";
EResolveMode[EResolveMode["ABORT"] = 6] = "ABORT";
})(EResolveMode = EResolveMode || (EResolveMode = {}));
//# sourceMappingURL=types.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AA6BA,uDAAuD;AACvD,EAAE;AACF,aAAa;AACb,EAAE;AAEF;;;;;GAKG;AACH,MAAM,CAAN,IAAY,QASX;AATD,WAAY,QAAQ;IAChB;;OAEG;IACH,2CAAM,CAAA;IACN;;OAEG;IACH,uCAAI,CAAA;AACR,CAAC,EATW,QAAQ,GAAR,QAAQ,KAAR,QAAQ,QASnB;AAED;;;;;GAKG;AACH,MAAM,CAAN,IAAY,YAQX;AARD,WAAY,YAAY;IACpB,+CAAQ,CAAA;IACR,yDAAS,CAAA;IACT,uDAAQ,CAAA;IACR,qEAAe,CAAA;IACf,iDAAK,CAAA;IACL,iDAAK,CAAA;IACL,iDAAK,CAAA;AACT,CAAC,EARW,YAAY,GAAZ,YAAY,KAAZ,YAAY,QAQvB"}

View File

@ -0,0 +1,4 @@
import { Discourser } from '../src/lib/discourse/index.js';
import { IDiscourseConfig } from '@polymech/commons/types';
export declare const getDiscourseConfig: () => IDiscourseConfig;
export declare const getClient: () => Discourser;

View File

@ -0,0 +1,15 @@
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
import { Discourser } from '../src/lib/discourse/index.js';
export const getDiscourseConfig = () => {
return {
host: process.env.DISCOURSE_HOST || 'https://forum.polymech.info',
key: process.env.DISCOURSE_API_KEY || 'e8b4adf4770b85cf94ea5e0aacb517ad25ddeb338efa3946559c06c2cdbab2e8',
username: process.env.DISCOURSE_USERNAME || 'jerom',
rateLimitConcurrency: 1
};
};
export const getClient = () => {
const config = getDiscourseConfig();
return new Discourser(config);
};
//# sourceMappingURL=commons.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"commons.js","sourceRoot":"","sources":["../../tests/commons.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,GAAG,GAAG,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAG1D,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAqB,EAAE;IACrD,OAAO;QACH,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,6BAA6B;QACjE,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,kEAAkE;QACxG,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,OAAO;QACnD,oBAAoB,EAAE,CAAC;KAC1B,CAAA;AACL,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,EAAE;IAC1B,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAA;IACnC,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,CAAA;AACjC,CAAC,CAAA"}

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest';
import { getClient } from './commons.js';
describe('Discourse Posts', () => {
const client = getClient();
it('should fetch posts', async () => {
try {
const posts = await client.getPostItems();
expect(posts).toBeDefined();
expect(Array.isArray(posts)).toBe(true);
}
catch (e) {
console.warn('Skipping test due to connection error or config', e);
}
});
});
//# sourceMappingURL=posts.test.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"posts.test.js","sourceRoot":"","sources":["../../tests/posts.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC7B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,EAAE,CAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;QAChC,IAAI;YACA,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,YAAY,EAAE,CAAA;YACzC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAA;YAC3B,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;SAC1C;QAAC,OAAO,CAAC,EAAE;YACR,OAAO,CAAC,IAAI,CAAC,iDAAiD,EAAE,CAAC,CAAC,CAAA;SACrE;IACL,CAAC,CAAC,CAAA;AACN,CAAC,CAAC,CAAA"}

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,37 @@
import { describe, it, expect } from 'vitest';
import { getClient } from './commons.js';
describe('Discourse Topics', () => {
const client = getClient();
it('should fetch latest topics', async () => {
try {
const topics = await client.getTopicItems();
expect(topics).toBeDefined();
expect(Array.isArray(topics)).toBe(true);
if (topics.length > 0) {
expect(topics[0].id).toBeDefined();
}
}
catch (e) {
console.warn('Skipping test due to connection error or config', e);
}
});
it('should fetch a specific topic', async () => {
try {
// we need a valid ID, assume we have one from the previous list or a known one
const topics = await client.getTopicItems({ page: 0 });
if (topics && topics.length > 0) {
const topicId = topics[0].id;
const topic = await client.getTopic(topicId);
expect(topic).toBeDefined();
expect(topic.id).toBe(topicId);
}
else {
console.warn('No topics found to test getTopic');
}
}
catch (e) {
console.warn('Skipping test due to connection error or config', e);
}
}, 15000);
});
//# sourceMappingURL=topics.test.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"topics.test.js","sourceRoot":"","sources":["../../tests/topics.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAC9B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;QACxC,IAAI;YACA,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAA;YAC3C,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAA;YAC5B,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACxC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE;gBACnB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;aACrC;SACJ;QAAC,OAAO,CAAC,EAAE;YACR,OAAO,CAAC,IAAI,CAAC,iDAAiD,EAAE,CAAC,CAAC,CAAA;SACrE;IACL,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QAC3C,IAAI;YACA,+EAA+E;YAC/E,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;YACtD,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE;gBAC7B,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;gBAC5B,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;gBAC5C,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAA;gBAC3B,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;aACjC;iBAAM;gBACH,OAAO,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAA;aACnD;SACJ;QAAC,OAAO,CAAC,EAAE;YACR,OAAO,CAAC,IAAI,CAAC,iDAAiD,EAAE,CAAC,CAAC,CAAA;SACrE;IACL,CAAC,EAAE,KAAK,CAAC,CAAA;AACb,CAAC,CAAC,CAAA"}

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { getClient } from './commons.js';
describe('Discourse Users', () => {
const client = getClient();
it('should fetch active users', async () => {
try {
const users = await client.getUsers(0);
expect(users).toBeDefined();
// Depending on response structure, checking properties
// expect(users).toHaveProperty('users')
}
catch (e) {
console.warn('Skipping test due to connection error or config', e);
}
});
it('should fetch a specific user by username', async () => {
try {
const username = 'system';
const user = await client.getUserByUsername(username);
expect(user).toBeDefined();
expect(user.username.toLowerCase()).toBe(username);
}
catch (e) {
console.warn('Skipping test due to connection error or config', e);
}
});
});
//# sourceMappingURL=users.test.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"users.test.js","sourceRoot":"","sources":["../../tests/users.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC7B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;QACvC,IAAI;YACA,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;YACtC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAA;YAC3B,uDAAuD;YACvD,wCAAwC;SAC3C;QAAC,OAAO,CAAC,EAAE;YACR,OAAO,CAAC,IAAI,CAAC,iDAAiD,EAAE,CAAC,CAAC,CAAA;SACrE;IACL,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACtD,IAAI;YACA,MAAM,QAAQ,GAAG,QAAQ,CAAA;YACzB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAA;YACrD,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAA;YAC1B,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;SACrD;QAAC,OAAO,CAAC,EAAE;YACR,OAAO,CAAC,IAAI,CAAC,iDAAiD,EAAE,CAAC,CAAC,CAAA;SACrE;IACL,CAAC,CAAC,CAAA;AACN,CAAC,CAAC,CAAA"}

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,11 @@
"@polymech/commons": "file:../commons",
"@polymech/core": "file:../core",
"@polymech/fs": "file:../fs",
"@polymech/log": "file:../log",
"@polymech/media": "file:../media",
"@polymech/registry": "file:../registry",
"@types/markdown-it": "^12.2.3",
"@types/node": "^14.17.5",
"@types/node": "^25.0.3",
"@types/yargs": "^17.0.11",
"axios": "^0.27.2",
"bluebird": "^3.7.2",
@ -86,4 +87,4 @@
"keywords": [
"typescript"
]
}
}

View File

@ -1,8 +1,3 @@
import { sync as exists } from '@polymech/fs/exists'
import { IOptions, IOptionsSync } from './types.js'
import { logger } from './index.js'
import * as path from 'path'
export const defaults = () => {
// default command
const DefaultCommand = 'info';
@ -17,58 +12,3 @@ export const defaults = () => {
console.error('Unhandled rejection, reason: ', reason);
});
}
export const sanitize = (argv: any): IOptionsSync | IOptions | boolean => {
let ret: any = {
...argv
}
ret.src = argv.src
let srcInfo
let variables = {}
/*
if (ret.src) {
ret.src = forward_slash(substitute(ret.alt,ret.src,variables))
// in case a file with a glob pattern is provided, strip the glob
// this is a special case, enabling shared scripts in Alt-Tap Salamand
const glob_base = globBase(ret.src)
const file = ret.src.replace(glob_base.glob, '').replace(/\/$/, '')
if(exists(file) && isFile(file)){
ret.src = file
}
srcInfo = pathInfo(resolve(ret.src, ret.alt, variables))
if (srcInfo && srcInfo.FILES && srcInfo.FILES.length) {
ret.srcInfo = srcInfo
for (const key in srcInfo) {
if (Object.prototype.hasOwnProperty.call(srcInfo, key)) {
variables['SRC_' + key] = srcInfo[key];
}
}
} else {
ret.src = resolve(ret.src, ret.alt, variables)
}
}
*/
if (argv.cwd) {
ret.cwd = path.resolve(argv.cwd);
if (!exists((ret.cwd))) {
logger.error(`Invalid working directory ${argv.cwd}`)
}
} else {
ret.cwd = process.cwd()
}
ret = {
...ret,
variables,
srcInfo
}
return ret
}

View File

@ -0,0 +1,4 @@
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
export const cli = yargs(hideBin(process.argv))

View File

@ -1,19 +1,7 @@
export * from './constants.js'
export * from './types.js'
export * from './lib/index.js'
export * from './lib/oa/types.js'
import { logger as _logger } from '@polymech/core/debug'
import { MODULE_NAME } from './constants'
import { ISettingsParam, Logger, ILogObject } from "tslog"
export { Logger } from 'tslog'
let loggers = {};
export const logger = _logger(MODULE_NAME)
import { substitute as _substitute, substituteAlt as _substituteAlt } from "@polymech/core/strings"
import { IObjectLiteral } from "@polymech/core"
export const substitute = (alt: boolean, template: string, vars: IObjectLiteral = {}) => alt ? _substituteAlt(template, vars) : _substitute(template, vars)

View File

@ -4,9 +4,9 @@ import { CONFIG_DEFAULT } from '@polymech/commons'
import { IOSRConfig, IDiscourseConfig } from '@polymech/commons/types'
import { logger as _logger } from '../index.js'
import { createLogger } from '@polymech/log'
export const logger: any = _logger(MODULE_NAME)
const logger = createLogger(MODULE_NAME)
import { sync as write } from '@polymech/fs/write'
import { sync as read } from '@polymech/fs/write'
@ -758,7 +758,7 @@ export class Discourser {
async upload(userId, file): Promise<IDImage[]> {
async upload(userId, file): Promise<any[]> {
// fetch whole posts
const url = `${this.host}/uploads.json`;
@ -1086,78 +1086,6 @@ export class Discourser {
})
}
/*
{
"bio_raw": "about me\n\ntest",
"website": "https://shop.osr-plastic.org",
"location": "Barcelona",
"custom_fields": {
"geo_location": {
"lat": "9.4170689",
"lon": "123.3351935",
"address": "Santander, Cebu, Central Visayas, 6026, Philippines",
"countrycode": "ph",
"city": "Santander",
"state": "Cebu",
"country": "Philippines",
"postalcode": "6026",
"boundingbox": [
"9.3630227",
"9.491731",
"123.2684005",
"123.3642727"
],
"type": "administrative"
}
},
"timezone": "Europe/Berlin",
"default_calendar": "none_selected"
}*/
/*
fetch("https://forum.osr-plastic.org/u/katharinaelleke.json", {
"headers": {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"discourse-logged-in": "true",
"discourse-present": "true",
"sec-ch-ua": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"x-csrf-token": "0Nau_ylBzR68D5OvfxfOHYii6GI_7gSrr_Ci_9S8ZDjqjy5mavF_O_INAPD8xfCQHgJkDH4rdgS0kPfjMAMBFw",
"x-requested-with": "XMLHttpRequest",
"Referer": "https://forum.osr-plastic.org/u/katharinaelleke/preferences/profile",
"Referrer-Policy": "strict-origin-when-cross-origin"
},
"body": "bio_raw=Curious%2C+serious+and+not+so+serious+%3Aslight_smile%3A+%0A&website=https%3A%2F%2Fwww.theflipflopi.com%2F&custom_fields%5Bnotify_me_when_followed%5D=false&custom_fields%5Bnotify_followed_user_when_followed%5D=false&custom_fields%5Bnotify_me_when_followed_replies%5D=false&custom_fields%5Bnotify_me_when_followed_creates_topic%5D=false&custom_fields%5Ballow_people_to_follow_me%5D=false&custom_fields%5Bgeo_location%5D%5Blat%5D=-2.294164&custom_fields%5Bgeo_location%5D%5Blon%5D=40.91501&custom_fields%5Bgeo_location%5D%5Baddress%5D=African+Corner%2C+Lamu%2C+Coast%2C+Kenya&custom_fields%5Bgeo_location%5D%5Bcountrycode%5D=ke&custom_fields%5Bgeo_location%5D%5Bcity%5D=&custom_fields%5Bgeo_location%5D%5Bstate%5D=Coast&custom_fields%5Bgeo_location%5D%5Bcountry%5D=Kenya&custom_fields%5Bgeo_location%5D%5Bpostalcode%5D=&custom_fields%5Bgeo_location%5D%5Bboundingbox%5D%5B%5D=-2.294164&custom_fields%5Bgeo_location%5D%5Bboundingbox%5D%5B%5D=-2.294164&custom_fields%5Bgeo_location%5D%5Bboundingbox%5D%5B%5D=40.91501&custom_fields%5Bgeo_location%5D%5Bboundingbox%5D%5B%5D=40.91501&custom_fields%5Bgeo_location%5D%5Btype%5D=&user_fields%5B1%5D=https%3A%2F%2Fshop.osr-plastic.org%2Fafrica%2F&user_fields%5B2%5D=https%3A%2F%2Fwww.theflipflopi.com%2F&user_fields%5B3%5D=https%3A%2F%2Fwww.instagram.com%2Fkatatungo%2F&user_fields%5B4%5D=unknown&user_fields%5B5%5D=unknown&profile_background_upload_url=https%3A%2F%2Fforum.osr-plastic.org%2Fuploads%2Fdefault%2Foriginal%2F2X%2F0%2F0875e1aea65f41172b9c4ad60b1a372cbaa72b2b.jpeg&card_background_upload_url=https%3A%2F%2Fforum.osr-plastic.org%2Fuploads%2Fdefault%2Foriginal%2F2X%2F3%2F357088c493bd0f66e651211853fb18beee2ec2e4.jpeg&timezone=Africa%2FNairobi&default_calendar=none_selected",
"method": "PUT"
});
fetch("https://forum.osr-plastic.org/u/admin-osr.json", {
"headers": {
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"discourse-logged-in": "true",
"discourse-present": "true",
"sec-ch-ua": "\"Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"115\", \"Chromium\";v=\"115\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-csrf-token": "gjd-DOi_nULFDhLyUh4igqFZENxL6B4rufR3dfbY9H9Soc-YDwOQS1SYSwJlj6f2f7BpDf5CO9y-zyOPBj-E1Q",
"x-requested-with": "XMLHttpRequest",
"cookie": "_ga=GA1.1.983567986.1691921410; _t=onvQ4UWFfLfjFsFkmebQdDot6JXW8%2FAWgFEtJUZ51AqXbTKnI%2BPF80usnZkhYqyYYW9Q%2B1fuMyEy5bDXc9AJpwwkcVIg%2Fu1DTE39y3nGdpXMiiVsmkMqBkyGA%2FvpvZUrM23CDS0xT%2B09VSSlNUbu8lZY27hYWXio4ETx7DRTmBk0xY52d3R6bcmtofynfxldP5KceF0APNtl9AV6iUhWloQ32CK6PmzpSFq4E3hstsS1WPTf7SwLGvESWm4tn3M4EQ2v4RHuLiHaAj%2F0--mv8Byxo5aHGcgKPO--%2F9eiy%2B24FCNmiLF3%2FpGZag%3D%3D; _ga_GVR8PEPG6C=GS1.1.1691940751.2.1.1691940775.0.0.0; _forum_session=bLbNekWpq6fkdK%2B19Nw6ZoKeEk9Ijp8LPfmQAjBiGK0TcnIYzMhZ5LJmLrxmG39e85SgUz%2F7NMI3NXZM9j%2F35eJuO3hxnUAkFvBNwjwwS9LGhDB4kB3ebAjjcBpoLPzUMWqyslQuR9RM8JcjZ%2FfX%2FWw0E61l9jhfrG0dr8Ds97IW7XedjKCmI7x7xinv10R2bRb51%2F72t2Z4dVG86wyCAtI5Spom0yjHoWPHwWBZ7VPYtOCmSBVygusuF%2FW5hpO9wgn1oqc0ZZ%2BXj%2FPiFc0FsADEcKYh3Q%3D%3D--xdEsXmADtTKkFGSc--x%2Fd9m02lRNk0uH%2BPxZTNwQ%3D%3D",
"Referer": "https://forum.osr-plastic.org/u/admin-osr/preferences/profile",
"Referrer-Policy": "strict-origin-when-cross-origin"
},
"body": "bio_raw=%23%23+Test%0A%0A%60%60%60js%0Aconst+t+%3D+0%0A%60%60%60%0A%0A%5Btest%5D(www.test.com)%0A%0A&website=www.test-site.com&location=test-loc&profile_background_upload_url=https%3A%2F%2Fforum.osr-plastic.org%2Fuploads%2Fdefault%2Foriginal%2F2X%2Fd%2Fd097423e34a6c677cdb0933d091fb84bcfbcec2e.jpeg&card_background_upload_url=https%3A%2F%2Fforum.osr-plastic.org%2Fuploads%2Fdefault%2Foriginal%2F2X%2Fb%2Fb4c30d1b981964fd6f936ebaac0f86ad4dc01209.png&timezone=Europe%2FBerlin&default_calendar=none_selected",
"method": "PUT"
});
*/
}

View File

@ -1,4 +1,4 @@
import { RegExCallback } from './types'
import { RegExCallback } from './types.js'
export class Pattern {
regex: RegExp;
replacement: RegExCallback

View File

@ -1,4 +1,4 @@
import { Pattern } from './Pattern';
import { Pattern } from './Pattern.js';
export class Rule {
name: string;

View File

@ -1,14 +1,12 @@
import { Rule } from './Rule'
import { Pattern } from './Pattern'
import { Rule } from './Rule.js'
import { Pattern } from './Pattern.js'
import { RMarkOptions } from './types'
import { RMarkOptions } from './types.js'
export const RE_IMAGES: RegExp = /\!\[([^\]]+)\]\((\S+)\)/g
export const RE_LINKS: RegExp = /\[([^\n]+)\]\(([^\n]+)\)/g
import * as markdown from 'markdown-it'
import Token from "markdown-it/lib/token"
import Renderer from "markdown-it/lib/renderer"
import markdown from 'markdown-it'
const defaultRules: Rule[] = [
new Rule('header', [
@ -88,8 +86,8 @@ export class RMark {
}
}
export { Rule } from './Rule';
export { Pattern } from './Pattern';
export { Rule } from './Rule.js';
export { Pattern } from './Pattern.js';
// export const find = (content:string, reg:RegExp) => content.match(reg)
@ -100,7 +98,7 @@ export const toHTML = (content) => {
const md = new markdown({
html: true,
breaks:true
breaks: true
})
return md.render(content)

View File

@ -1,17 +1,7 @@
#!/usr/bin/env node
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
import { defaults } from './_cli'; defaults();
import * as cli from 'yargs';
import { register as registerInfo } from './commands/info.js'; registerInfo(cli)
import { register as registerQuery } from './commands/query.js'; registerQuery(cli)
import { register as registerOAImport } from './commands/import-oa-users.js'; registerOAImport(cli)
import { register as registerOAImportHowtos } from './commands/import-oa-howtos.js'; registerOAImportHowtos(cli)
import { register as registerSync } from './commands/sync-file.js'; registerSync(cli)
import { register as registerSyncComponent } from './commands/sync-component.js'; registerSyncComponent(cli)
import { defaults } from './_cli.js'; defaults();
import { cli } from './cli.js'
const argv: any = cli.argv;
if (argv.help) {

View File

@ -0,0 +1,17 @@
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
import { Discourser } from '../src/lib/discourse/index.js'
import { IDiscourseConfig } from '@polymech/commons/types'
export const getDiscourseConfig = (): IDiscourseConfig => {
return {
host: process.env.DISCOURSE_HOST || 'https://forum.polymech.info',
key: process.env.DISCOURSE_API_KEY || 'e8b4adf4770b85cf94ea5e0aacb517ad25ddeb338efa3946559c06c2cdbab2e8',
username: process.env.DISCOURSE_USERNAME || 'jerom',
rateLimitConcurrency: 1
}
}
export const getClient = () => {
const config = getDiscourseConfig()
return new Discourser(config)
}

View File

@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest'
import { getClient } from './commons.js'
describe('Discourse Posts', () => {
const client = getClient()
it('should fetch posts', async () => {
try {
const posts = await client.getPostItems()
expect(posts).toBeDefined()
expect(Array.isArray(posts)).toBe(true)
} catch (e) {
console.warn('Skipping test due to connection error or config', e)
}
})
})

View File

@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest'
import { getClient } from './commons.js'
describe('Discourse Topics', () => {
const client = getClient()
it('should fetch latest topics', async () => {
try {
const topics = await client.getTopicItems()
expect(topics).toBeDefined()
expect(Array.isArray(topics)).toBe(true)
if (topics.length > 0) {
expect(topics[0].id).toBeDefined()
}
} catch (e) {
console.warn('Skipping test due to connection error or config', e)
}
})
it('should fetch a specific topic', async () => {
try {
// we need a valid ID, assume we have one from the previous list or a known one
const topics = await client.getTopicItems({ page: 0 })
if (topics && topics.length > 0) {
const topicId = topics[0].id
const topic = await client.getTopic(topicId)
expect(topic).toBeDefined()
expect(topic.id).toBe(topicId)
} else {
console.warn('No topics found to test getTopic')
}
} catch (e) {
console.warn('Skipping test due to connection error or config', e)
}
}, 15000)
})

View File

@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest'
import { getClient } from './commons.js'
describe('Discourse Users', () => {
const client = getClient()
it('should fetch active users', async () => {
try {
const users = await client.getUsers(0)
expect(users).toBeDefined()
// Depending on response structure, checking properties
// expect(users).toHaveProperty('users')
} catch (e) {
console.warn('Skipping test due to connection error or config', e)
}
})
it('should fetch a specific user by username', async () => {
try {
const username = 'system'
const user = await client.getUserByUsername(username)
expect(user).toBeDefined()
expect(user.username.toLowerCase()).toBe(username)
} catch (e) {
console.warn('Skipping test due to connection error or config', e)
}
})
})

View File

@ -1,7 +1,8 @@
{
"extends": "../typescript-config/base.json",
"include": [
"src/**/*.ts"
"src/**/*.ts",
"tests/**/*.ts"
],
"files": [
"src/index.ts"