Initial release
This commit is contained in:
43
dist/package.json
vendored
Normal file
43
dist/package.json
vendored
Normal 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
1
dist/src/constants.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export declare const __dirname: string;
|
||||
4
dist/src/constants.js
vendored
Normal file
4
dist/src/constants.js
vendored
Normal 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
1
dist/src/constants.js.map
vendored
Normal 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
114
dist/src/emojiButton/client/emoji.css
vendored
Normal 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
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
1
dist/src/emojiButton/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './plugin.js';
|
||||
2
dist/src/emojiButton/index.js
vendored
Normal file
2
dist/src/emojiButton/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './plugin.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
dist/src/emojiButton/index.js.map
vendored
Normal file
1
dist/src/emojiButton/index.js.map
vendored
Normal 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
3
dist/src/emojiButton/plugin.d.ts
vendored
Normal 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
32
dist/src/emojiButton/plugin.js
vendored
Normal 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
1
dist/src/emojiButton/plugin.js.map
vendored
Normal 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
3
dist/src/index.d.ts
vendored
Normal 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
4
dist/src/index.js
vendored
Normal 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
1
dist/src/index.js.map
vendored
Normal 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"}
|
||||
34
dist/src/postToMastodon/client/script.js
vendored
Normal file
34
dist/src/postToMastodon/client/script.js
vendored
Normal 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
1
dist/src/postToMastodon/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './plugin.js';
|
||||
2
dist/src/postToMastodon/index.js
vendored
Normal file
2
dist/src/postToMastodon/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './plugin.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
dist/src/postToMastodon/index.js.map
vendored
Normal file
1
dist/src/postToMastodon/index.js.map
vendored
Normal 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
3
dist/src/postToMastodon/plugin.d.ts
vendored
Normal 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
59
dist/src/postToMastodon/plugin.js
vendored
Normal 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
1
dist/src/postToMastodon/plugin.js.map
vendored
Normal 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
154
dist/src/uploadButton/client/script.js
vendored
Normal 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
37
dist/src/uploadButton/client/styles.css
vendored
Normal 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
12
dist/src/uploadButton/defaults.d.ts
vendored
Normal 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
39
dist/src/uploadButton/defaults.js
vendored
Normal 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
1
dist/src/uploadButton/defaults.js.map
vendored
Normal 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
1
dist/src/uploadButton/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './plugin.js';
|
||||
2
dist/src/uploadButton/index.js
vendored
Normal file
2
dist/src/uploadButton/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './plugin.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
dist/src/uploadButton/index.js.map
vendored
Normal file
1
dist/src/uploadButton/index.js.map
vendored
Normal 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
3
dist/src/uploadButton/plugin.d.ts
vendored
Normal 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
167
dist/src/uploadButton/plugin.js
vendored
Normal 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
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
13
dist/src/uploadButton/types.d.ts
vendored
Normal 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
2
dist/src/uploadButton/types.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
1
dist/src/uploadButton/types.js.map
vendored
Normal file
1
dist/src/uploadButton/types.js.map
vendored
Normal 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
7
dist/src/uploadButton/utils.d.ts
vendored
Normal 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
48
dist/src/uploadButton/utils.js
vendored
Normal 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
1
dist/src/uploadButton/utils.js.map
vendored
Normal 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"}
|
||||
Reference in New Issue
Block a user