Initial release

This commit is contained in:
2026-05-12 23:57:41 -04:00
parent 524c6d2be7
commit 84691ccc3a
66 changed files with 9689 additions and 23 deletions

43
dist/package.json vendored Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "express-twtkpr-core-plugins",
"version": "0.9.0",
"packageManager": "yarn@4.14.1",
"type": "module",
"main": "./dist/src/index.js",
"module": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"exports": {
".": {
"import": "./dist/src/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && rsync -avm --include '*/' --include '*/client/*.*' --exclude '*' src/ dist/src",
"lint": "eslint --fix src test",
"prepublishOnly": "yarn build",
"test": "vitest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"debug": "^4.4.3",
"express": "^5.2.1",
"express-twtkpr": "^0.9.0",
"formidable": "^3.5.4",
"sharp": "^0.34.5"
},
"devDependencies": {
"@types/debug": "^4.1.13",
"@types/express": "^5.0.6",
"@types/formidable": "^3.5.1",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-security": "^4.0.0",
"prettier": "^3.8.3",
"typescript": "^6.0.3",
"vitest": "^4.1.5"
}
}

1
dist/src/constants.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export declare const __dirname: string;

4
dist/src/constants.js vendored Normal file
View File

@@ -0,0 +1,4 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
export const __dirname = resolve(dirname(fileURLToPath(import.meta.url)), '..');
//# sourceMappingURL=constants.js.map

1
dist/src/constants.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC"}

114
dist/src/emojiButton/client/emoji.css vendored Normal file
View File

@@ -0,0 +1,114 @@
/* Copyright © 2020 Jamie Zawinski <jwz@dnalounge.com>
Permission to use, copy, modify, distribute, and sell this software
and its documentation for any purpose is hereby granted without
fee, provided that the above copyright notice appear in all copies
and that both that copyright notice and this permission notice
appear in supporting documentation. No representations are made
about the suitability of this software for any purpose. It is
provided "as is" without express or implied warranty.
Emoji popup menu. There are many like it. This one is mine.
Created: 24-May-2020
*/
#emoji_blocker {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: none;
z-index: 1000;
}
.emoji_menu {
position: fixed;
display: inline-block;
border: 1px solid;
width: 22em;
text-align: center;
z-index: 1001;
background: #000;
}
.emoji_tab_bar {
display: block;
text-align: center;
}
.emoji_tab {
display: inline-block;
text-align: center;
width: 1.8em;
height: 1.3em;
padding: 0px 2px 8px 2px;
cursor: pointer;
}
.emoji_tab.selected {
background: #040;
}
.emoji_page_box {
height: 16em;
overflow-y: auto;
background: #040;
}
.emoji_page {
display: none;
text-align: center;
width: 100%;
}
.emoji_page > b {
display: block;
font-size: smaller;
margin: 4px;
}
.emoji_square {
display: inline-block;
text-align: center;
width: 1.3em;
height: 1.3em;
margin: 2px;
cursor: pointer;
}
.emoji_button {
display: inline-block;
font-size: smaller;
padding: 0 4px 0 8px;
cursor: pointer;
}
/* Light modifications by Eric Woodward<hey@itsericwoodward,com> */
.emoji_button {
position: absolute;
right: 0.5rem;
bottom: 0.5rem;
}
.emoji_menu {
background: rgb(10, 10, 20, 0.6);
}
.emoji_tab.selected {
background: rgb(27, 27, 39);
}
.emoji_page_box {
background: rgb(27, 27, 39);
}
.twtControls-contentInput {
padding-bottom: 2rem;
}
.twtControls-contentLabel {
position: relative;
}

2160
dist/src/emojiButton/client/emoji.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/src/emojiButton/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export { default } from './plugin.js';

2
dist/src/emojiButton/index.js vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from './plugin.js';
//# sourceMappingURL=index.js.map

1
dist/src/emojiButton/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/emojiButton/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}

3
dist/src/emojiButton/plugin.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import type { TwtKprPluginConfiguration } from 'express-twtkpr';
declare const _default: () => TwtKprPluginConfiguration;
export default _default;

32
dist/src/emojiButton/plugin.js vendored Normal file
View File

@@ -0,0 +1,32 @@
import fs from 'node:fs';
import { join } from 'node:path';
import { getReadStream } from 'express-twtkpr';
import { __dirname } from '../constants.js';
console.log({ __dirname });
const PLUGIN_NAME = 'emojiButton';
export default () => ({
clientCSS: () => {
const cssStream = fs.createReadStream(join(__dirname, 'src', PLUGIN_NAME, 'client', 'emoji.css'));
cssStream.on('error', (err) => {
console.error(err);
cssStream.close();
cssStream.push(null);
});
return cssStream;
},
clientJS: () => {
const jsFileStream = getReadStream(join(__dirname, 'src', PLUGIN_NAME, 'client', 'emoji.js'));
/*
const jsStream = Readable.from(`
const twtSubmitButton = document.querySelector('.twtControls-submitButton');
const emojiTarget = document.querySelector('.emoji_target');
emojiTarget.addEventListener('change', () => {
if (twtSubmitButton) twtSubmitButton.removeAttribute('disabled');
});
`);
*/
return jsFileStream;
},
name: PLUGIN_NAME,
});
//# sourceMappingURL=plugin.js.map

1
dist/src/emojiButton/plugin.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/emojiButton/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;AAE3B,MAAM,WAAW,GAAG,aAAa,CAAC;AAElC,eAAe,GAA8B,EAAE,CAAC,CAAC;IAChD,SAAS,EAAE,GAAG,EAAE;QACf,MAAM,SAAS,GAAG,EAAE,CAAC,gBAAgB,CACpC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,CAC1D,CAAC;QAEF,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YAC7B,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACnB,SAAS,CAAC,KAAK,EAAE,CAAC;YAClB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC,CAAC,CAAC;QAEH,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,QAAQ,EAAE,GAAG,EAAE;QACd,MAAM,YAAY,GAAG,aAAa,CACjC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,CAAC,CACzD,CAAC;QACF;;;;;;;;UAQE;QAEF,OAAO,YAAY,CAAC;IACrB,CAAC;IAED,IAAI,EAAE,WAAW;CACjB,CAAC,CAAC"}

3
dist/src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export { default as emojiButton } from './emojiButton/index.js';
export { default as postToMastodon } from './postToMastodon/index.js';
export { default as uploadButton } from './uploadButton/index.js';

4
dist/src/index.js vendored Normal file
View File

@@ -0,0 +1,4 @@
export { default as emojiButton } from './emojiButton/index.js';
export { default as postToMastodon } from './postToMastodon/index.js';
export { default as uploadButton } from './uploadButton/index.js';
//# sourceMappingURL=index.js.map

1
dist/src/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC"}

View File

@@ -0,0 +1,34 @@
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
(() => {
const twtForm = document.getElementById('twtForm');
/*
document
.querySelector('.twtControls-contentLabel')
?.insertAdjacentHTML('beforebegin', renderUploadButton());
document
.querySelector('#twtControlsEditButton')
?.insertAdjacentHTML('beforebegin', renderUploadButton('small'));
const twtControlsContentInput = document.getElementById(
'twtControlsContentInput'
);
twtControlsContentInput.addEventListener('drop', dropHandler);
twtControlsContentInput.addEventListener('dragover', dragOverHandler);
window.addEventListener('dragover', dragOverWindowHandler);
window.addEventListener('drop', dropWindowHandler);
Array.from(document.querySelectorAll('.twtControls-uploadInput')).forEach(
(uploadInput) => {
uploadInput.addEventListener('change', uploadChangeHandler);
}
);
*/
})();
// @license-end

1
dist/src/postToMastodon/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export { default } from './plugin.js';

2
dist/src/postToMastodon/index.js vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from './plugin.js';
//# sourceMappingURL=index.js.map

1
dist/src/postToMastodon/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/postToMastodon/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}

3
dist/src/postToMastodon/plugin.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import type { TwtKprConfiguration, TwtKprPluginConfiguration } from 'express-twtkpr';
declare const _default: (config: TwtKprConfiguration) => TwtKprPluginConfiguration;
export default _default;

59
dist/src/postToMastodon/plugin.js vendored Normal file
View File

@@ -0,0 +1,59 @@
import Debug from 'debug';
const PLUGIN_NAME = 'postToMastodon';
// curl https://toot.cafe/api/v1/statuses -H 'Authorization: Bearer A0k6vbobTQvG-n9DStsfGV03BKxKkZHL-IhljR3Lvik' -F 'status=Test posting from cURL via the API. Hello from the command line!'
export default (config) => {
const debug = Debug(`twtkprPlugin:${PLUGIN_NAME}`);
const { application_token, server_url } = config?.plugins?.[PLUGIN_NAME] ?? {};
if (!application_token || !server_url)
return {};
return {
onAfterTwt: async (twt) => {
// TODO: add some console.log / error to output things that we can't return a message for
// That way, it shows up _somewhere_
// Don't do anything if the twt is a reply (starts with a hash)
const [, content] = twt.split(/\t/);
if (content.match(/^\(#(\w+)\)/)) {
// it's a reply, skip
return;
}
const formData = new FormData();
formData.append('status', content.replace(/\s*\u2028/g, '\n'));
debug(`Sending message to Mastodon instance at ${server_url}`);
const res = await fetch(`${server_url}${server_url.endsWith('/') ? '' : '/'}api/v1/statuses`, {
method: 'POST',
body: formData,
headers: {
Authorization: `Bearer ${application_token}`,
},
});
if (!res.ok) {
console.error(`Bad response (${res.status}) from Mastodon server ${server_url}: ${res.statusText}`);
return;
}
const result = await res.text();
console.log(`Twt sent to Mastodon server ${server_url}, response:`, {
result,
});
},
// use JS to add a checkbox to the form
// update the Twt post function to send the whole form - that will
// allow you to add new things to it and they will be sent
/*
clientJS: () => {
const jsStream = fs.createReadStream(
path.join(__dirname, 'plugins', PLUGIN_NAME, 'script.js')
);
jsStream.on('error', (err) => {
console.error(err);
jsStream.close();
jsStream.push(null);
});
return jsStream;
},
*/
name: PLUGIN_NAME,
};
};
//# sourceMappingURL=plugin.js.map

1
dist/src/postToMastodon/plugin.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/postToMastodon/plugin.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAErC,6LAA6L;AAE7L,eAAe,CAAC,MAA2B,EAA6B,EAAE;IACzE,MAAM,KAAK,GAAG,KAAK,CAAC,gBAAgB,WAAW,EAAE,CAAC,CAAC;IAEnD,MAAM,EAAE,iBAAiB,EAAE,UAAU,EAAE,GACtC,MAAM,EAAE,OAAO,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;IAEtC,IAAI,CAAC,iBAAiB,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC;IAEjD,OAAO;QACN,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACzB,yFAAyF;YACzF,oCAAoC;YAEpC,+DAA+D;YAC/D,MAAM,CAAC,EAAE,OAAO,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;gBAClC,qBAAqB;gBACrB,OAAO;YACR,CAAC;YAED,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;YAChC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC;YAE/D,KAAK,CAAC,2CAA2C,UAAU,EAAE,CAAC,CAAC;YAE/D,MAAM,GAAG,GAAG,MAAM,KAAK,CACtB,GAAG,UAAU,GAAG,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,iBAAiB,EACpE;gBACC,MAAM,EAAE,MAAM;gBACd,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE;oBACR,aAAa,EAAE,UAAU,iBAAiB,EAAE;iBAC5C;aACD,CACD,CAAC;YAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CACZ,iBAAiB,GAAG,CAAC,MAAM,0BAC1B,UACD,KAAK,GAAG,CAAC,UAAU,EAAE,CACrB,CAAC;gBACF,OAAO;YACR,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;YAEhC,OAAO,CAAC,GAAG,CAAC,+BAA+B,UAAU,aAAa,EAAE;gBACnE,MAAM;aACN,CAAC,CAAC;QACJ,CAAC;QAED,uCAAuC;QACvC,kEAAkE;QAClE,0DAA0D;QAE1D;;;;;;;;;;;;;;UAcQ;QAER,IAAI,EAAE,WAAW;KACjB,CAAC;AACH,CAAC,CAAC"}

154
dist/src/uploadButton/client/script.js vendored Normal file
View File

@@ -0,0 +1,154 @@
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
const injectUploadButton = (route) => {
// const route = '/files';
/**
*
* @param uploadConfiguration
* @param variant
* @returns
*/
const renderUploadButton = (variant = 'normal') => `
<label class="button twtControls-uploadInputLabel twtControls-uploadInputLabel-${variant}"
for="twtControlsUploadInput-${variant}"
>
Upload${variant === 'normal' ? '<br />' : ' '}Files
<input accept="*" class="twtControls-uploadInput"
id="twtControlsUploadInput-${variant}"
multiple type="file" />
</label>
`;
const uploadFiles = async (files, uploadRoute, secondAttempt = false) => {
if (!uploadRoute || !window.token || !window.refreshToken) return;
debug('uploadFiles', token, files, uploadRoute, secondAttempt);
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
try {
const res = await fetch(uploadRoute, {
method: 'POST',
type: 'upload',
body: formData,
headers: {
Authorization: `Bearer ${token}`,
},
credentials: 'include',
});
if (res.ok) {
showToast(`File${files.length !== 1 ? 's' : ''} uploaded`);
const filePath = await res.text();
twtControlsContentInput.value += filePath
.split('\n')
.map((currFilePath) =>
[
' ',
location.protocol,
'//',
location.hostname,
location.protocol !== 'https:' && location.port !== 80
? ':' + location.port
: '',
currFilePath,
].join('')
)
.join('');
return;
}
if (!secondAttempt) {
await refreshToken();
return uploadFiles(files, uploadRoute, true);
}
showToast(
`Unable to upload image${files.length !== 1 ? 's' : ''} refresh token, please try again later.`,
'error'
);
} catch (err) {
console.error(err);
}
};
/* Handlers */
const dragOverHandler = (ev) => {
const files = [...ev.dataTransfer.items].filter(
(item) => item.kind === 'file'
);
if (files.length > 0) {
ev.preventDefault();
ev.dataTransfer.dropEffect = 'copy';
}
};
const dragOverWindowHandler = (ev) => {
const files = [...ev.dataTransfer.items].filter(
(item) => item.kind === 'file'
);
if (files.length > 0) {
ev.preventDefault();
if (!twtControlsContentInput.contains(ev.target)) {
ev.dataTransfer.dropEffect = 'none';
}
}
};
const dropHandler = (ev) => {
ev.preventDefault();
const files = [...ev.dataTransfer.items]
.map((item) => item.getAsFile())
.filter((file) => file);
debug('dropHandler', files);
uploadFiles(files, route);
};
const dropWindowHandler = (ev) => {
if ([...ev.dataTransfer.items].some((item) => item.kind === 'file')) {
ev.preventDefault();
}
};
const uploadChangeHandler = (ev) => {
uploadFiles(ev.target.files, route);
};
document
.querySelector('.twtControls-contentLabel')
?.insertAdjacentHTML('beforebegin', renderUploadButton());
document
.querySelector('#twtControlsEditButton')
?.insertAdjacentHTML('beforebegin', renderUploadButton('small'));
const twtControlsContentInput = document.getElementById(
'twtControlsContentInput'
);
twtControlsContentInput.addEventListener('drop', dropHandler);
twtControlsContentInput.addEventListener('dragover', dragOverHandler);
window.addEventListener('dragover', dragOverWindowHandler);
window.addEventListener('drop', dropWindowHandler);
Array.from(document.querySelectorAll('.twtControls-uploadInput')).forEach(
(uploadInput) => {
uploadInput.addEventListener('change', uploadChangeHandler);
}
);
};
// @license-end

37
dist/src/uploadButton/client/styles.css vendored Normal file
View File

@@ -0,0 +1,37 @@
/**
* Begin TwtKpr UploadPlugin CSS
*/
.twtControls-uploadInputLabel {
align-items: center;
cursor: pointer;
display: flex;
justify-content: center;
max-width: 100%;
text-align: center;
transition: all 0.5s;
}
.twtControls-uploadInputLabel input {
display: none;
}
/*
.twtControls-uploadInputLabel-normal {
display: none;
}
*/
.twtControls-uploadInputLabel-small {
font-size: small;
}
.twtControls-uploadInputLabel:hover {
background-color: var(--bg-hl);
color: var(--fg-hl);
}
/*
.twtControls-uploadInput {
display: none;
}
*/

12
dist/src/uploadButton/defaults.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
declare const _default: {
allowedMimeTypes: string;
directory: string;
imageFit: string;
imageHeight: number;
imageWidth: number;
route: string;
hashAlgorithm: string;
keepExtensions: boolean;
maxFiles: number;
};
export default _default;

39
dist/src/uploadButton/defaults.js vendored Normal file
View File

@@ -0,0 +1,39 @@
/*
uploadConfiguration: {
...uploadConfiguration,
active: uploadActive,
allowEmptyFiles,
allowedMimeTypes: getDestinationByMimeTypeConfiguration(allowedMimeTypes),
createDirsFromUploads,
directory,
encoding,
fileWriteStreamHandler,
filter,
hashAlgorithm: hashAlgorithm as string | false | undefined,
keepExtensions,
maxFields,
maxFileSize,
maxFiles,
maxTotalFileSize,
minFileSize,
route,
},
*/
// falls back to formidable defaults where it can
export default {
// local values
allowedMimeTypes: '',
directory: 'public',
imageFit: 'inside',
imageHeight: 1024,
imageWidth: 1024,
route: 'files',
// uploadEncoding: 'utf-8',
// defaults for formidable
// allowEmptyFiles: false, // same as formidable
// encoding: 'utf-8', // same as formidable
hashAlgorithm: 'sha256',
keepExtensions: true, // TODO: verify this is necessary
maxFiles: 10,
};
//# sourceMappingURL=defaults.js.map

1
dist/src/uploadButton/defaults.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../../src/uploadButton/defaults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;EAoBE;AAEF,iDAAiD;AACjD,eAAe;IACd,eAAe;IACf,gBAAgB,EAAE,EAAE;IACpB,SAAS,EAAE,QAAQ;IACnB,QAAQ,EAAE,QAAQ;IAClB,WAAW,EAAE,IAAI;IACjB,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,OAAO;IACd,2BAA2B;IAE3B,0BAA0B;IAC1B,iDAAiD;IACjD,4CAA4C;IAC5C,aAAa,EAAE,QAAQ;IACvB,cAAc,EAAE,IAAI,EAAE,iCAAiC;IACvD,QAAQ,EAAE,EAAE;CACZ,CAAC"}

1
dist/src/uploadButton/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export { default } from './plugin.js';

2
dist/src/uploadButton/index.js vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from './plugin.js';
//# sourceMappingURL=index.js.map

1
dist/src/uploadButton/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/uploadButton/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}

3
dist/src/uploadButton/plugin.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import type { TwtKprConfiguration, TwtKprPluginConfiguration } from 'express-twtkpr';
declare const _default: (config: TwtKprConfiguration) => TwtKprPluginConfiguration;
export default _default;

167
dist/src/uploadButton/plugin.js vendored Normal file
View File

@@ -0,0 +1,167 @@
import { promises as fsp } from 'node:fs';
import { extname, join } from 'node:path';
import { Readable } from 'node:stream';
import Debug from 'debug';
import { combineStreams, getReadStream } from 'express-twtkpr';
import formidable from 'formidable';
import sharp from 'sharp';
import { __dirname } from '../constants.js';
import defaults from './defaults.js';
import { getDestinationByMimeTypeConfiguration } from './utils.js';
const PLUGIN_NAME = 'uploadButton';
export default (config) => {
const debug = Debug(`twtkprPlugin:${PLUGIN_NAME}}`);
const { route = '/files' } = config?.plugins?.[PLUGIN_NAME] ?? {};
return {
clientCSS: () => {
const stream = getReadStream(join(__dirname, 'src', PLUGIN_NAME, 'client', 'styles.css'));
return stream;
},
clientJS: () => {
const jsStream = getReadStream(join(__dirname, 'src', PLUGIN_NAME, 'client', 'script.js'));
const jsCallerStream = Readable.from(`injectUploadButton('${route}');`);
return combineStreams([jsStream, jsCallerStream]);
},
name: PLUGIN_NAME,
postRoutes: [
{
path: route,
handler: async (req, res, next) => {
const { allowedMimeTypes, directory, imageFit, imageHeight, imageWidth, route, ...otherProps } = Object.assign({}, defaults, config?.plugins?.uploadConfiguration ?? {});
if (Array.isArray(allowedMimeTypes) && !allowedMimeTypes.length) {
next();
return;
}
debug('using configuration: ', {
uploadConfiguration: config?.plugins?.uploadConfiguration,
});
const form = formidable({
uploadDir: directory,
...otherProps,
});
form.parse(req, async (err, fields, files) => {
if (err) {
next(err);
return;
}
const uploadsDir = (route ?? '').replaceAll('/', '');
let hadFileError = false;
const processedFiles = [];
const destinationByMimeType = getDestinationByMimeTypeConfiguration(allowedMimeTypes);
debug(`processing ${(files?.files ?? []).length} files`);
for (const file of files?.files ?? []) {
const { filepath, hash, mimetype, newFilename, originalFilename, } = file ?? {};
if (!(filepath && newFilename && originalFilename))
return;
debug({ file });
let ext = extname(originalFilename).toLocaleLowerCase();
if (ext === '.jpeg')
ext = '.jpg';
let destinationDir = '';
Object.keys(destinationByMimeType).forEach((mimeType) => {
if (file.mimetype?.split('/')?.[0] ===
mimeType.toLocaleLowerCase())
destinationDir =
destinationByMimeType[mimeType].directory ?? '';
});
if (destinationDir === '')
destinationDir =
destinationByMimeType['*']?.directory ?? uploadsDir;
const finalPath = join(process.cwd(), 'public', destinationDir);
const useOriginalName = !(mimetype?.includes('image') || mimetype?.includes('video'));
let hashNameLength = 8;
let finalFilename;
let fileExists;
do {
finalFilename = (!useOriginalName && hash
? `${hash.substring(0, hashNameLength)}${ext}`
: originalFilename)
.replace(/[^\w\.]+/g, '_')
.replace(/_+/g, '_')
.toLocaleLowerCase();
if (!useOriginalName) {
try {
await fsp.stat(join(finalPath, finalFilename));
fileExists = true;
hashNameLength++;
}
catch {
fileExists = false;
}
}
} while (!useOriginalName && fileExists);
debug(`creating '${finalPath}'`);
fsp.mkdir(finalPath, { recursive: true });
const pathToOutputFile = join(finalPath, finalFilename);
let wasRelocated = false;
if (mimetype?.includes('image')) {
// use sharp to shrink
debug(`converting '${filepath}' to '/${pathToOutputFile}'`);
try {
await sharp(filepath)
.autoOrient()
// shrink to 1024 on biggest edge, do not enlarge
.resize({
fit: imageFit,
height: imageHeight,
width: imageWidth,
withoutEnlargement: true,
})
.toFile(pathToOutputFile);
/*
await sharp(filepath)
.metadata()
.then(({ height, width }) =>
sharp(filepath)
// shrink to 1024 on biggest edge, do not enlarge
.resize({
height: height >= width ? 1024 : undefined,
width: width >= height ? 1024 : undefined,
withoutEnlargement: true,
})
.toFile(pathToOutputFile)
);
*/
wasRelocated = true;
}
catch {
// at least we tried
}
}
try {
if (!wasRelocated) {
debug(`copying '${filepath}' to '/${pathToOutputFile}'`);
await fsp.copyFile(filepath, pathToOutputFile);
}
debug(`cleaning up '${filepath}'`);
await fsp.rm(filepath);
debug(`processed successfully`);
processedFiles.push(`/${destinationDir}/${finalFilename}`);
}
catch (err) {
debug(`error!`);
hadFileError = true;
console.error(err);
}
}
debug('generating reply...');
if (hadFileError && processedFiles.length) {
res
.type('text/plain')
.status(206)
.send(processedFiles.join('\n'));
return;
}
if (!processedFiles.length) {
res.type('text/plain').status(500).send('No files processed');
return;
}
res.type('text/plain').status(201).send(processedFiles.join('\n'));
});
},
requiresAuth: true,
},
],
};
};
//# sourceMappingURL=plugin.js.map

1
dist/src/uploadButton/plugin.js.map vendored Normal file

File diff suppressed because one or more lines are too long

13
dist/src/uploadButton/types.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import type { MimeOptions } from 'express-twtkpr';
import formidable from 'formidable';
export interface MimeImageOptions extends MimeOptions {
compression?: string;
maxHeight?: string;
maxWidth?: string;
}
export interface UploadConfiguration extends Partial<Omit<formidable.Options, 'uploadDir'>> {
active: boolean;
directory: string;
allowedMimeTypes: string | string[] | Record<string, MimeOptions>;
route: string;
}

2
dist/src/uploadButton/types.js vendored Normal file
View File

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

1
dist/src/uploadButton/types.js.map vendored Normal file
View File

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

7
dist/src/uploadButton/utils.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
import type { MimeOptions } from 'express-twtkpr';
/**
*
* @param allowedMimeTypes
* @returns
*/
export declare const getDestinationByMimeTypeConfiguration: (allowedMimeTypes?: string | string[] | Record<string, MimeOptions>) => Record<string, MimeOptions>;

48
dist/src/uploadButton/utils.js vendored Normal file
View File

@@ -0,0 +1,48 @@
/**
*
* @param allowedMimeTypes
* @returns
*/
export const getDestinationByMimeTypeConfiguration = (allowedMimeTypes) => {
const fallback = {
audio: {
directory: 'audio',
rename: false,
},
image: {
directory: 'images',
rename: true,
},
video: {
directory: 'videos',
rename: true,
},
'*': {
directory: 'files',
rename: false,
},
};
const mimeTypeArrayReducer = (acc, curr) => {
if (fallback[curr])
acc[curr] = fallback[curr];
else
acc[curr] = {
directory: `${curr}s`,
rename: false,
};
return acc;
};
if (!allowedMimeTypes)
return fallback;
if (typeof allowedMimeTypes === 'string')
return allowedMimeTypes
.split(',')
.map((val) => val.trim())
.reduce(mimeTypeArrayReducer, {});
if (Array.isArray(allowedMimeTypes))
return allowedMimeTypes.reduce(mimeTypeArrayReducer, {});
if (typeof allowedMimeTypes === 'object')
return allowedMimeTypes;
return fallback;
};
//# sourceMappingURL=utils.js.map

1
dist/src/uploadButton/utils.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/uploadButton/utils.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,qCAAqC,GAAG,CACpD,gBAAkE,EACjE,EAAE;IACH,MAAM,QAAQ,GAAgC;QAC7C,KAAK,EAAE;YACN,SAAS,EAAE,OAAO;YAClB,MAAM,EAAE,KAAK;SACb;QACD,KAAK,EAAE;YACN,SAAS,EAAE,QAAQ;YACnB,MAAM,EAAE,IAAI;SACZ;QACD,KAAK,EAAE;YACN,SAAS,EAAE,QAAQ;YACnB,MAAM,EAAE,IAAI;SACZ;QACD,GAAG,EAAE;YACJ,SAAS,EAAE,OAAO;YAClB,MAAM,EAAE,KAAK;SACb;KACD,CAAC;IAEF,MAAM,oBAAoB,GAAG,CAC5B,GAAgC,EAChC,IAAY,EACX,EAAE;QACH,IAAI,QAAQ,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;;YAE9C,GAAG,CAAC,IAAI,CAAC,GAAG;gBACX,SAAS,EAAE,GAAG,IAAI,GAAG;gBACrB,MAAM,EAAE,KAAK;aACb,CAAC;QAEH,OAAO,GAAG,CAAC;IACZ,CAAC,CAAC;IAEF,IAAI,CAAC,gBAAgB;QAAE,OAAO,QAAQ,CAAC;IAEvC,IAAI,OAAO,gBAAgB,KAAK,QAAQ;QACvC,OAAO,gBAAgB;aACrB,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;aACxB,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;IAEpC,IAAI,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC;QAClC,OAAQ,gBAA6B,CAAC,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;IAExE,IAAI,OAAO,gBAAgB,KAAK,QAAQ;QAAE,OAAO,gBAAgB,CAAC;IAElE,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC"}