converted to use aws

This commit is contained in:
Jamie Steiner 2024-11-19 01:07:10 +02:00
parent bed608b97b
commit 686bf8aaec
6 changed files with 2778 additions and 179 deletions

1
declare.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module "@smithy/fetch-http-handler/dist-es/request-timeout";

2289
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,16 +16,20 @@
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"browser-image-compression": "^2.0.2",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"eslint": "^8.49.0",
"eslint-config-standard": "^17.1.0",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4",
"browser-image-compression": "^2.0.2"
"typescript": "4.7.4"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.693.0",
"@aws-sdk/protocol-http": "^3.370.0",
"@aws-sdk/querystring-builder": "^3.370.0",
"@smithy/fetch-http-handler": "^4.1.1",
"csso": "^5.0.5",
"data-uri-to-buffer": "^6.0.1"
}

View file

@ -1,16 +1,24 @@
import { Plugin, requestUrl, setIcon, TFile } from "obsidian";
import { Plugin, requestUrl, setIcon, TFile, RequestUrlParam } from "obsidian";
import { DEFAULT_SETTINGS, SendNoteSettings, SendNoteSettingsTab, YamlField } from "./settings";
import Note, { SharedNote } from "./note";
import API, { parseExistingShareUrl } from "./api";
import StatusMessage, { StatusType } from "./StatusMessage";
import { shortHash, sha256, decryptString } from "./crypto";
import UI from "./UI";
import { S3Client, PutObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
import { HttpHandlerOptions } from "@aws-sdk/types";
import { buildQueryString } from "@aws-sdk/querystring-builder";
import { requestTimeout } from "@smithy/fetch-http-handler/dist-es/request-timeout";
import { FetchHttpHandler, FetchHttpHandlerOptions } from "@smithy/fetch-http-handler";
import { HttpRequest, HttpResponse } from "@aws-sdk/protocol-http";
import * as crypto from "crypto";
export default class SendNotePlugin extends Plugin {
settings: SendNoteSettings;
api: API;
settingsPage: SendNoteSettingsTab;
ui: UI;
s3: S3Client;
// Expose some tools in the plugin object
hash = shortHash;
@ -23,8 +31,28 @@ export default class SendNotePlugin extends Plugin {
this.settingsPage = new SendNoteSettingsTab(this.app, this);
this.addSettingTab(this.settingsPage);
let apiEndpoint = this.settings.useCustomEndpoint
? this.settings.customEndpoint
: `https://s3.${this.settings.region}.amazonaws.com/`;
this.settings.imageUrlPath = this.settings.useCustomImageUrl
? this.settings.customImageUrl
: this.settings.forcePathStyle
? apiEndpoint + this.settings.bucket + "/"
: apiEndpoint.replace("://", `://${this.settings.bucket}.`);
// Initialise the backend API
this.ui = new UI(this.app);
this.s3 = new S3Client({
region: this.settings.region,
credentials: {
// clientConfig: { region: this.settings.region },
accessKeyId: this.settings.accessKey,
secretAccessKey: this.settings.secretKey,
},
endpoint: apiEndpoint,
// forcePathStyle: this.settings.forcePathStyle,
requestHandler: new ObsHttpHandler({ keepAlive: false }),
});
// To get an API key, we send the user to a Cloudflare Turnstile page to verify they are a human,
// as a way to prevent abuse. The key is then sent back to Obsidian via this URI handler.
@ -126,6 +154,43 @@ export default class SendNotePlugin extends Plugin {
);
}
async uploadFile(content: string, key: string): Promise<string> {
// const buf = await file.arrayBuffer();
let folder = this.settings.folder;
const currentDate = new Date();
folder = folder
.replace("${year}", currentDate.getFullYear().toString())
.replace("${month}", String(currentDate.getMonth() + 1).padStart(2, "0"))
.replace("${day}", String(currentDate.getDate()).padStart(2, "0"));
const keyHash = md5Hash(key);
key = folder ? `${folder}/${keyHash}` : keyHash;
const encoder = new TextEncoder();
const utf8Array = encoder.encode(content);
await this.s3.send(
new PutObjectCommand({
Bucket: this.settings.bucket,
Key: key,
Body: utf8Array,
ContentType: "text/plain",
})
);
let urlString = this.settings.imageUrlPath + key;
if (this.settings.queryStringKey && this.settings.queryStringValue) {
let urlObject = new URL(urlString);
// The searchParams property provides methods to manipulate query parameters
urlObject.searchParams.append(this.settings.queryStringKey, this.settings.queryStringValue);
urlString = urlObject.toString();
}
return urlString;
}
onunload() {}
async loadSettings() {
@ -292,33 +357,52 @@ export default class SendNotePlugin extends Plugin {
}
async deletePaste(pasteKey: string) {
const url = "https://pastebin.com/api/api_post.php";
const formData = new URLSearchParams();
formData.append("api_dev_key", this.settings.pastebinApiKey);
formData.append("api_user_key", this.settings.pastebinUserKey);
formData.append("api_paste_key", pasteKey);
formData.append("api_option", "delete");
try {
const response = await requestUrl({
url: url,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formData.toString(),
this.s3
.send(
new DeleteObjectsCommand({
Bucket: this.settings.bucket,
Delete: {
Objects: [
{
Key: pasteKey,
},
],
},
})
)
.then((data) => {
console.log("data", data);
})
.catch((err) => {
console.log("error", err);
});
// const url = "https://pastebin.com/api/api_post.php";
const result = await response.text;
// const formData = new URLSearchParams();
// // formData.append("api_dev_key", this.settings.pastebinApiKey);
// // formData.append("api_user_key", this.settings.pastebinUserKey);
// formData.append("api_paste_key", pasteKey);
// formData.append("api_option", "delete");
if (result.trim() === "Paste Removed") {
} else {
console.error("Error deleting paste:", result);
}
} catch (error) {
console.error("Network or parsing error:", error);
}
// try {
// const response = await requestUrl({
// url: url,
// method: "POST",
// headers: {
// "Content-Type": "application/x-www-form-urlencoded",
// },
// body: formData.toString(),
// });
// const result = await response.text;
// if (result.trim() === "Paste Removed") {
// } else {
// console.error("Error deleting paste:", result);
// }
// } catch (error) {
// console.error("Network or parsing error:", error);
// }
}
private generateRandomString(length = 5) {
@ -349,13 +433,13 @@ export default class SendNotePlugin extends Plugin {
const sendUrlObj = new URL(sendUrlParam);
// Extract the pathname from the URL
const pathname = sendUrlObj.pathname;
// const pathname = sendUrlObj.pathname;
// Extract and return the identifier from the pathname
// Assuming the identifier is the part after the last '/' in the pathname
const identifier = pathname.substring(pathname.lastIndexOf("/") + 1);
// const identifier = pathname.substring(pathname.lastIndexOf("/") + 1);
return identifier;
return sendUrlObj.pathname.slice(1);
} else {
throw new Error("sendurl parameter not found");
}
@ -365,3 +449,108 @@ export default class SendNotePlugin extends Plugin {
}
}
}
/**
* This is close to origin implementation of FetchHttpHandler
* https://github.com/aws/aws-sdk-js-v3/blob/main/packages/fetch-http-handler/src/fetch-http-handler.ts
* that is released under Apache 2 License.
* But this uses Obsidian requestUrl instead.
*/
class ObsHttpHandler extends FetchHttpHandler {
requestTimeoutInMs: number | undefined;
constructor(options?: FetchHttpHandlerOptions) {
super(options);
this.requestTimeoutInMs = options === undefined ? undefined : options.requestTimeout;
}
async handle(request: HttpRequest, { abortSignal }: HttpHandlerOptions = {}): Promise<{ response: HttpResponse }> {
if (abortSignal?.aborted) {
const abortError = new Error("Request aborted");
abortError.name = "AbortError";
return Promise.reject(abortError);
}
let path = request.path;
if (request.query) {
const queryString = buildQueryString(request.query);
if (queryString) {
path += `?${queryString}`;
}
}
const { port, method } = request;
const url = `${request.protocol}//${request.hostname}${port ? `:${port}` : ""}${path}`;
const body = method === "GET" || method === "HEAD" ? undefined : request.body;
const transformedHeaders: Record<string, string> = {};
for (const key of Object.keys(request.headers)) {
const keyLower = key.toLowerCase();
if (keyLower === "host" || keyLower === "content-length") {
continue;
}
transformedHeaders[keyLower] = request.headers[key];
}
let contentType: string | undefined = undefined;
if (transformedHeaders["content-type"] !== undefined) {
contentType = transformedHeaders["content-type"];
}
let transformedBody: any = body;
if (ArrayBuffer.isView(body)) {
transformedBody = bufferToArrayBuffer(body);
}
const param: RequestUrlParam = {
body: transformedBody,
headers: transformedHeaders,
method: method,
url: url,
contentType: contentType,
};
const raceOfPromises = [
requestUrl(param).then((rsp) => {
const headers = rsp.headers;
const headersLower: Record<string, string> = {};
for (const key of Object.keys(headers)) {
headersLower[key.toLowerCase()] = headers[key];
}
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array(rsp.arrayBuffer));
controller.close();
},
});
return {
response: new HttpResponse({
headers: headersLower,
statusCode: rsp.status,
body: stream,
}),
};
}),
requestTimeout(this.requestTimeoutInMs),
];
if (abortSignal) {
raceOfPromises.push(
new Promise<never>((resolve, reject) => {
abortSignal.onabort = () => {
const abortError = new Error("Request aborted");
abortError.name = "AbortError";
reject(abortError);
};
})
);
}
return Promise.race(raceOfPromises);
}
}
const bufferToArrayBuffer = (b: Buffer | Uint8Array | ArrayBufferView) => {
return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
};
function md5Hash(text: string): string {
return crypto.createHash("md5").update(text, "utf8").digest("hex");
}

View file

@ -94,40 +94,34 @@ export default class Note {
}
});
const pastebinData = {
api_dev_key: this.plugin.settings.pastebinApiKey,
api_user_key: this.plugin.settings.pastebinUserKey,
api_option: "paste",
api_paste_private: 1,
api_paste_name: this.file.basename,
api_paste_expire_date: this.plugin.settings.pastebinExpiry,
api_paste_format: "text",
api_paste_code: encodeURIComponent(noteContent),
};
// const pastebinData = {
// api_dev_key: this.plugin.settings.pastebinApiKey,
// api_user_key: this.plugin.settings.pastebinUserKey,
// api_option: "paste",
// api_paste_private: 1,
// api_paste_name: this.file.basename,
// api_paste_expire_date: this.plugin.settings.pastebinExpiry,
// api_paste_format: "text",
// api_paste_code: encodeURIComponent(noteContent),
// };
const body = this.createQueryString(pastebinData);
requestUrl({
url: "https://pastebin.com/api/api_post.php",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: body,
})
// const body = this.createQueryString(pastebinData);
this.plugin
.uploadFile(noteContent, this.file.basename)
.then((res) => {
const responseText = res.text.trim();
console.log(res);
const responseText = res.trim();
if (!responseText.startsWith("http")) {
throw new Error("Invalid URL response from Pastebin");
throw new Error("Invalid URL response from AWS");
}
const urlObj = new URL(responseText);
const pathSegments = urlObj.pathname.split("/");
const suffix = pathSegments[pathSegments.length - 1];
let obsidianUrl = "";
if (shareUnencrypted) {
obsidianUrl = `obsidian://send-note?sendurl=https://pastebin.com/raw/${suffix}&filename=${this.file.basename}.md`;
obsidianUrl = `obsidian://send-note?sendurl=${responseText}&filename=${this.file.basename}.md`;
} else {
obsidianUrl = `obsidian://send-note?sendurl=https://pastebin.com/raw/${suffix}&filename=${this.file.basename}.md&encrypted=true&key=${encryptionKey}`;
obsidianUrl = `obsidian://send-note?sendurl=${responseText}&filename=${this.file.basename}.md&encrypted=true&key=${encryptionKey}`;
}
// console.log(obsidianUrl);
navigator.clipboard.writeText(obsidianUrl);
@ -139,14 +133,49 @@ export default class Note {
this.plugin.addShareIcons();
})
.catch((err) => {
console.log("err", err);
this.status = new StatusMessage(
err +
". The note uploading failed. If you got status 422, it might be because you have exceeded your allowed number of pastes for 24 hours.",
StatusType.Error,
5 * 1000
);
console.error(err);
});
// requestUrl({
// url: "https://pastebin.com/api/api_post.php",
// method: "POST",
// headers: {
// "Content-Type": "application/x-www-form-urlencoded",
// },
// body: body,
// })
// .then((res) => {
// const responseText = res.text.trim();
// if (!responseText.startsWith("http")) {
// throw new Error("Invalid URL response from Pastebin");
// }
// const urlObj = new URL(responseText);
// const pathSegments = urlObj.pathname.split("/");
// const suffix = pathSegments[pathSegments.length - 1];
// let obsidianUrl = "";
// if (shareUnencrypted) {
// obsidianUrl = `obsidian://send-note?sendurl=https://pastebin.com/raw/${suffix}&filename=${this.file.basename}.md`;
// } else {
// obsidianUrl = `obsidian://send-note?sendurl=https://pastebin.com/raw/${suffix}&filename=${this.file.basename}.md&encrypted=true&key=${encryptionKey}`;
// }
// // console.log(obsidianUrl);
// navigator.clipboard.writeText(obsidianUrl);
// this.plugin.app.fileManager.processFrontMatter(this.file, (frontmatter) => {
// if ((frontmatter["send_link"] = true)) {
// frontmatter["send_link"] = obsidianUrl;
// }
// });
// this.plugin.addShareIcons();
// })
// .catch((err) => {
// console.log("err", err);
// this.status = new StatusMessage(
// err +
// ". The note uploading failed. If you got status 422, it might be because you have exceeded your allowed number of pastes for 24 hours.",
// StatusType.Error,
// 5 * 1000
// );
// });
return;
}

View file

@ -1,4 +1,4 @@
import { App, PluginSettingTab, Setting, TextComponent, requestUrl } from "obsidian";
import { App, PluginSettingTab, Setting, TextComponent, requestUrl, setIcon } from "obsidian";
import SendNotePlugin from "./main";
import StatusMessage, { StatusType } from "./StatusMessage";
@ -30,12 +30,21 @@ export interface SendNoteSettings {
clipboard: boolean;
shareUnencrypted: boolean;
debug: number;
pastebinApiKey: string;
pastebinUsername: string;
pastebinPassword: string;
pastebinUserKey: string;
pastebinPublic: string;
pastebinExpiry: string;
accessKey: string;
secretKey: string;
region: string;
bucket: string;
folder: string;
expiry: string;
imageUrlPath: string;
useCustomEndpoint: boolean;
customEndpoint: string;
forcePathStyle: boolean;
useCustomImageUrl: boolean;
customImageUrl: string;
bypassCors: boolean;
queryStringValue: string;
queryStringKey: string;
}
export const DEFAULT_SETTINGS: SendNoteSettings = {
@ -45,12 +54,21 @@ export const DEFAULT_SETTINGS: SendNoteSettings = {
clipboard: true,
shareUnencrypted: false,
debug: 0,
pastebinApiKey: "",
pastebinUsername: "",
pastebinPassword: "",
pastebinUserKey: "",
pastebinPublic: "1",
pastebinExpiry: "1D",
accessKey: "",
secretKey: "",
region: "eu-west-2",
bucket: "",
folder: "",
expiry: "24",
imageUrlPath: "",
useCustomEndpoint: false,
customEndpoint: "",
forcePathStyle: false,
useCustomImageUrl: false,
customImageUrl: "",
bypassCors: false,
queryStringValue: "",
queryStringKey: "",
};
export class SendNoteSettingsTab extends PluginSettingTab {
@ -63,102 +81,175 @@ export class SendNoteSettingsTab extends PluginSettingTab {
this.plugin = plugin;
}
setUserKey(): void {
if (
this.plugin.settings.pastebinApiKey &&
this.plugin.settings.pastebinUsername &&
this.plugin.settings.pastebinPassword
) {
this.getUserKey(
this.plugin.settings.pastebinApiKey,
this.plugin.settings.pastebinUsername,
this.plugin.settings.pastebinPassword
).then((key) => {
this.plugin.settings.pastebinUserKey = key;
this.plugin.saveSettings();
console.log("Set user key", key);
});
}
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// Pastebin API key
new Setting(containerEl)
.setName("Pastebin API key")
.setDesc("Pastebin API key")
.addText((inputEl) => {
inputEl
.setPlaceholder("Pastebin API key")
.setValue(this.plugin.settings.pastebinApiKey)
.setName("AWS Access Key ID")
.setDesc("AWS access key ID for a user with S3 access.")
.addText((text) => {
wrapTextWithPasswordHide(text);
text
.setPlaceholder("access key")
.setValue(this.plugin.settings.accessKey)
.onChange(async (value) => {
this.plugin.settings.pastebinApiKey = value;
await this.plugin.saveSettings();
this.setUserKey();
});
});
// Pastebin username
new Setting(containerEl)
.setName("Pastebin username")
.setDesc("Pastebin username")
.addText((inputEl) => {
inputEl
.setPlaceholder("Pastebin username")
.setValue(this.plugin.settings.pastebinUsername)
.onChange(async (value) => {
this.plugin.settings.pastebinUsername = value;
await this.plugin.saveSettings();
this.setUserKey();
});
});
// Pastebin password
new Setting(containerEl)
.setName("Pastebin password")
.setDesc("Pastebin password")
.addText((inputEl) => {
inputEl
.setPlaceholder("Pastebin password")
.setValue(this.plugin.settings.pastebinPassword)
.onChange(async (value) => {
this.plugin.settings.pastebinPassword = value;
await this.plugin.saveSettings();
this.setUserKey();
});
});
// Pastebin expiry period
new Setting(containerEl)
.setName("Pastebin expiry")
.setDesc("Pastebin expiry period, ie.: N, 1D, 1W, 1M, 1Y")
.addText((inputEl) => {
inputEl
.setPlaceholder("Pastebin expiry period")
.setValue(this.plugin.settings.pastebinExpiry)
.onChange(async (value) => {
this.plugin.settings.pastebinExpiry = value;
this.plugin.settings.accessKey = value.trim();
await this.plugin.saveSettings();
});
});
// Pastebin public setting
new Setting(containerEl)
.setName("Pastebin public setting")
.setDesc("Pastebin public: 0 for public, 1 for unlisted, 2 for private. Default is 1")
.addText((inputEl) => {
inputEl
.setPlaceholder("Pastebinpublic setting")
.setValue(this.plugin.settings.pastebinPublic)
.setName("AWS Secret Key")
.setDesc("AWS secret key for that user.")
.addText((text) => {
wrapTextWithPasswordHide(text);
text
.setPlaceholder("secret key")
.setValue(this.plugin.settings.secretKey)
.onChange(async (value) => {
this.plugin.settings.pastebinPublic = value;
this.plugin.settings.secretKey = value.trim();
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Region")
.setDesc("AWS region of the S3 bucket.")
.addText((text) =>
text
.setPlaceholder("aws region")
.setValue(this.plugin.settings.region)
.onChange(async (value) => {
this.plugin.settings.region = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("S3 Bucket")
.setDesc("S3 bucket name.")
.addText((text) =>
text
.setPlaceholder("bucket name")
.setValue(this.plugin.settings.bucket)
.onChange(async (value) => {
this.plugin.settings.bucket = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Bucket folder")
.setDesc("Optional folder in s3 bucket. Support the use of ${year}, ${month}, and ${day} variables.")
.addText((text) =>
text
.setPlaceholder("folder")
.setValue(this.plugin.settings.folder)
.onChange(async (value) => {
this.plugin.settings.folder = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Use custom endpoint")
.setDesc("Use the custom api endpoint below.")
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.useCustomEndpoint).onChange(async (value) => {
this.plugin.settings.useCustomEndpoint = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Custom S3 Endpoint")
.setDesc("Optionally set a custom endpoint for any S3 compatible storage provider.")
.addText((text) =>
text
.setPlaceholder("https://s3.myhost.com/")
.setValue(this.plugin.settings.customEndpoint)
.onChange(async (value) => {
value = value.match(/https?:\/\//) // Force to start http(s)://
? value
: "https://" + value;
value = value.replace(/([^\/])$/, "$1/"); // Force to end with slash
this.plugin.settings.customEndpoint = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("S3 Path Style URLs")
.setDesc(
"Advanced option to force using (legacy) path-style s3 URLs (s3.myhost.com/bucket) instead of the modern AWS standard host-style (bucket.s3.myhost.com)."
)
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.forcePathStyle).onChange(async (value) => {
this.plugin.settings.forcePathStyle = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Use custom image URL")
.setDesc("Use the custom image URL below.")
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.useCustomImageUrl).onChange(async (value) => {
this.plugin.settings.useCustomImageUrl = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Custom Image URL")
.setDesc("Advanced option to force inserting custom image URLs. This option is helpful if you are using CDN.")
.addText((text) =>
text.setValue(this.plugin.settings.customImageUrl).onChange(async (value) => {
value = value.match(/https?:\/\//) // Force to start http(s)://
? value
: "https://" + value;
value = value.replace(/([^\/])$/, "$1/"); // Force to end with slash
this.plugin.settings.customImageUrl = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Bypass local CORS check")
.setDesc("Bypass local CORS preflight checks - it might work on later versions of Obsidian.")
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.bypassCors).onChange(async (value) => {
this.plugin.settings.bypassCors = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Query String Key")
.setDesc("Appended to the end of the URL. Optional")
.addText((text) =>
text
.setPlaceholder("Empty means no query string key")
.setValue(this.plugin.settings.queryStringKey)
.onChange(async (value) => {
this.plugin.settings.queryStringKey = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Query String Value")
.setDesc("Appended to the end of the URL. Optional")
.addText((text) =>
text
.setPlaceholder("Empty means no query string value")
.setValue(this.plugin.settings.queryStringValue)
.onChange(async (value) => {
this.plugin.settings.queryStringValue = value;
await this.plugin.saveSettings();
})
);
// Local YAML field
new Setting(containerEl)
.setName("Frontmatter property prefix")
@ -215,32 +306,6 @@ export class SendNoteSettingsTab extends PluginSettingTab {
});
// .then((setting) => addDocs(setting, "https://docs.note.sx/notes/encryption"));
}
private async getUserKey(apiKey: string, username: string, password: string): Promise<string> {
const url = "https://pastebin.com/api/api_login.php";
const params = new URLSearchParams();
params.append("api_dev_key", apiKey);
params.append("api_user_name", username);
params.append("api_user_password", password);
const response = await requestUrl({
url: url,
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
});
if (response.status === 200) {
return response.text;
} else {
this.status = new StatusMessage(
"Failed to get user key. Please check your Pastebin API key, username and password.",
StatusType.Error,
5 * 1000
);
throw new Error("Failed to get user key");
}
}
}
function addDocs(setting: Setting, url: string) {
@ -250,3 +315,25 @@ function addDocs(setting: Setting, url: string) {
href: url,
});
}
const wrapTextWithPasswordHide = (text: TextComponent) => {
const hider = text.inputEl.insertAdjacentElement("beforebegin", createSpan());
if (!hider) {
return;
}
setIcon(hider as HTMLElement, "eye-off");
hider.addEventListener("click", () => {
const isText = text.inputEl.getAttribute("type") === "text";
if (isText) {
setIcon(hider as HTMLElement, "eye-off");
text.inputEl.setAttribute("type", "password");
} else {
setIcon(hider as HTMLElement, "eye");
text.inputEl.setAttribute("type", "text");
}
text.inputEl.focus();
});
text.inputEl.setAttribute("type", "password");
return text;
};