move plugin to separate repo

This commit is contained in:
Maxime Cannoodt 2022-07-11 11:34:53 +02:00
parent 94e4a527fb
commit 8372ff9a85
23 changed files with 0 additions and 6156 deletions

View File

@ -1,10 +0,0 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

View File

@ -1,2 +0,0 @@
npm node_modules
build

View File

@ -1,23 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

22
plugin/.gitignore vendored
View File

@ -1,22 +0,0 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store

View File

@ -1 +0,0 @@
tag-version-prefix=""

View File

@ -1,73 +0,0 @@
# Obsidian Sample Plugin
This is a sample plugin for Obsidian (https://obsidian.md).
This project uses Typescript to provide type checking and documentation.
The repo depends on the latest plugin API (obsidian.d.ts) in Typescript Definition format, which contains TSDoc comments describing what it does.
**Note:** The Obsidian API is still in early alpha and is subject to change at any time!
This sample plugin demonstrates some of the basic functionality the plugin API can do.
- Changes the default font color to red using `styles.css`.
- Adds a ribbon icon, which shows a Notice when clicked.
- Adds a command "Open Sample Modal" which opens a Modal.
- Adds a plugin setting tab to the settings page.
- Registers a global click event and output 'click' to the console.
- Registers a global interval which logs 'setInterval' to the console.
## First time developing plugins?
Quick starting guide for new plugin devs:
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
- Install NodeJS, then run `npm i` in the command line under your repo folder.
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
- Reload Obsidian to load the new version of your plugin.
- Enable plugin in settings window.
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
## Releasing new releases
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
- Publish the release.
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
## Adding your plugin to the community plugin list
- Check https://github.com/obsidianmd/obsidian-releases/blob/master/plugin-review.md
- Publish an initial version.
- Make sure you have a `README.md` file in the root of your repo.
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
## How to use
- Clone this repo.
- `npm i` or `yarn` to install dependencies
- `npm run dev` to start compilation in watch mode.
## Manually installing the plugin
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
## Improve code quality with eslint (optional)
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
- To use eslint with this project, make sure to install eslint from terminal:
- `npm install -g eslint`
- To use eslint to analyze this project use this command:
- `eslint main.ts`
- eslint will then create a report with suggestions for code improvement by file and line number.
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
- `eslint .\src\`
## API Documentation
See https://github.com/obsidianmd/obsidian-api

View File

@ -1,63 +0,0 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import esbuildSvelte from "esbuild-svelte";
import sveltePreprocess from "svelte-preprocess";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = process.argv[2] === "production";
esbuild
.build({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
plugins: [
esbuildSvelte({
compilerOptions: { css: true },
preprocess: sveltePreprocess(),
}),
],
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/closebrackets",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/comment",
"@codemirror/fold",
"@codemirror/gutter",
"@codemirror/highlight",
"@codemirror/history",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/matchbrackets",
"@codemirror/panel",
"@codemirror/rangeset",
"@codemirror/rectangular-selection",
"@codemirror/search",
"@codemirror/state",
"@codemirror/stream-parser",
"@codemirror/text",
"@codemirror/tooltip",
"@codemirror/view",
...builtins,
],
format: "cjs",
watch: !prod,
target: "es2016",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
})
.catch(() => process.exit(1));

View File

@ -1,98 +0,0 @@
import {
MarkdownView,
Menu,
Notice,
Plugin,
TAbstractFile,
TFile,
} from "obsidian";
import { NoteSharingService } from "src/NoteSharingService";
import { DEFAULT_SETTINGS } from "src/obsidian/PluginSettings";
import SettingsTab from "src/obsidian/SettingsTab";
import { SharedNoteSuccessModal } from "src/ui/SharedNoteSuccessModal";
import type { EventRef } from "obsidian";
import type { PluginSettings } from "src/obsidian/PluginSettings";
export default class NoteSharingPlugin extends Plugin {
public settings: PluginSettings;
private noteSharingService: NoteSharingService;
private eventRef: EventRef;
async onload() {
await this.loadSettings();
this.noteSharingService = new NoteSharingService(
this.settings.serverUrl
);
// Init settings tab
this.addSettingTab(new SettingsTab(this.app, this));
// Add note sharing command
this.addCommands();
this.eventRef = this.app.workspace.on(
"file-menu",
(menu, file, source) => this.onMenuOpenCallback(menu, file, source)
);
this.registerEvent(this.eventRef);
}
onunload() {}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings() {
await this.saveData(this.settings);
this.noteSharingService.serverUrl = this.settings.serverUrl;
}
addCommands() {
this.addCommand({
id: "obsidian-note-sharing-share-note",
name: "Create share link",
checkCallback: (checking: boolean) => {
// Only works on Markdown views
const activeView =
this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) return false;
if (checking) return true;
this.shareNote(activeView.getViewData());
},
});
}
// https://github.dev/platers/obsidian-linter/blob/c30ceb17dcf2c003ca97862d94cbb0fd47b83d52/src/main.ts#L139-L149
onMenuOpenCallback(menu: Menu, file: TAbstractFile, source: string) {
if (file instanceof TFile && file.extension === "md") {
menu.addItem((item) => {
item.setIcon("paper-plane-glyph");
item.setTitle("Share note online");
item.onClick(async (evt) => {
this.shareNote(await this.app.vault.read(file));
});
});
}
}
async shareNote(mdText: string) {
this.noteSharingService
.shareNote(mdText)
.then((res) => {
new SharedNoteSuccessModal(
this,
res.view_url,
res.expire_time
).open();
})
.catch((err: Error) => {
console.error(err);
new Notice(err.message, 7500);
});
}
}

View File

@ -1,10 +0,0 @@
{
"id": "obsidian-sample-plugin",
"name": "Obsidian Note Sharing",
"version": "1.0.1",
"minAppVersion": "0.12.0",
"description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.",
"author": "Obsidian",
"authorUrl": "https://obsidian.md",
"isDesktopOnly": false
}

5413
plugin/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +0,0 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.1",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"test-watch": "vitest",
"test": "vitest run",
"coverage": "vitest run --coverage",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@tsconfig/svelte": "^3.0.0",
"@types/crypto-js": "^4.1.1",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^5.2.0",
"builtin-modules": "^3.2.0",
"c8": "^7.11.3",
"esbuild": "0.13.12",
"esbuild-svelte": "^0.7.1",
"obsidian": "latest",
"svelte": "^3.48.0",
"svelte-preprocess": "^4.10.7",
"tslib": "2.3.1",
"typescript": "~4.5.0",
"vitest": "^0.15.1"
},
"dependencies": {
"crypto-js": "^4.1.1"
}
}

View File

@ -1,69 +0,0 @@
import moment, { type Moment } from "moment";
import { requestUrl } from "obsidian";
import { encryptMarkdown } from "./crypto/encryption";
type Response = {
view_url: string;
expire_time: Moment;
};
export class NoteSharingService {
private _url: string;
constructor(serverUrl: string) {
this.serverUrl = serverUrl;
}
/**
* @param mdText Markdown file to share.
* @returns link to shared note with attached decryption key.
*/
public async shareNote(mdText: string): Promise<Response> {
mdText = this.sanitizeNote(mdText);
const cryptData = encryptMarkdown(mdText);
const res = await this.postNote(cryptData.ciphertext, cryptData.hmac);
res.view_url += `#${cryptData.key}`;
console.log(`Note shared: ${res.view_url}`);
return res;
}
private async postNote(
ciphertext: string,
hmac: string
): Promise<Response> {
const res = await requestUrl({
url: `${this._url}/api/note`,
method: "POST",
contentType: "application/json",
body: JSON.stringify({ ciphertext, hmac }),
});
if (res.status == 200 && res.json != null) {
const returnValue = res.json;
returnValue.expire_time = moment(returnValue.expire_time);
return <Response>returnValue;
}
throw Error(
`Error uploading encrypted note (${res.status}): ${res.text}`
);
}
private sanitizeNote(mdText: string): string {
mdText = mdText.trim();
const match = mdText.match(
/^(?:---\s*\n)(?:(?:.*?\n)*?)(?:---)((?:.|\n|\r)*)/
);
if (match) {
mdText = match[1].trim();
}
return mdText;
}
public set serverUrl(newUrl: string) {
newUrl = newUrl.replace(/([^:]\/)\/+/g, "$1");
if (newUrl[newUrl.length - 1] == "/") {
newUrl = newUrl.substring(0, newUrl.length - 1);
}
this._url = newUrl;
}
}

View File

@ -1,38 +0,0 @@
import { describe, expect, it } from "vitest";
import { encryptString, decryptString, generateKey } from "./crypto";
const testKey = "0123456789ABCDEF";
const testData = "This is the test data.";
const derivedTestKey = "1UPCi_Wvhl8EsfW2cERtOL9KB5RbZkmmIa5wMrLLz6E";
describe("Encryption suite", () => {
it("should generate 256-bit keys", () => {
const key = generateKey(testData);
expect(key).toHaveLength(43);
});
it("should generate deterministic 256-bit keys from seed material", () => {
const key = generateKey(testData);
expect(key).toEqual(derivedTestKey);
});
it("should encrypt", () => {
const encryptedData = encryptString(testData, testKey);
expect(encryptedData).toHaveProperty("ciphertext");
expect(encryptedData).toHaveProperty("hmac");
});
it("should decrypt encrypted data with the correct key", () => {
const encryptedData = encryptString(testData, testKey);
const data = decryptString(encryptedData, testKey);
expect(data).toEqual(testData);
});
it("should fail decrypting with wrong key", () => {
const ciphertext = encryptString(testData, testKey);
const tempKey = generateKey("wrong key");
expect(() => {
decryptString(ciphertext, tempKey);
}).toThrowError(/Cannot decrypt ciphertext with this key./g);
});
});

View File

@ -1,40 +0,0 @@
import { AES, PBKDF2, HmacSHA256, enc } from "crypto-js";
interface EncryptedData {
ciphertext: string;
hmac: string;
}
/**
* Generates a 256-bit key from a
* Note: I don't add a salt because the key will be derived from a different
* passphrase for every shared note anyways..
* @param seed passphrase-like data to generate the key from.
*/
export function generateKey(seed: string): string {
const salt = "";
const key256 = PBKDF2(seed, salt, { keySize: 256 / 32 });
return key256.toString(enc.Base64url);
}
export function encryptString(md: string, key: string): EncryptedData {
// Generate message authentication code
const msg = enc.Utf8.parse(md);
const ciphertext = AES.encrypt(msg, key).toString();
const hmac = HmacSHA256(ciphertext, key).toString();
return { ciphertext, hmac };
}
export function decryptString(
{ ciphertext, hmac }: EncryptedData,
key: string
): string {
const hmac_calculated = HmacSHA256(ciphertext, key).toString();
const is_authentic = hmac_calculated == hmac;
if (!is_authentic) {
throw Error("Cannot decrypt ciphertext with this key.");
}
const md = AES.decrypt(ciphertext, key).toString(enc.Utf8);
return md;
}

View File

@ -1,14 +0,0 @@
import { moment } from "obsidian";
import { generateKey, encryptString } from "./crypto";
export interface EncryptedMarkdown {
ciphertext: string;
hmac: string;
key: string;
}
export function encryptMarkdown(plaintext: string): EncryptedMarkdown {
const key = generateKey(moment.now() + plaintext);
const { ciphertext, hmac } = encryptString(plaintext, key);
return { ciphertext, hmac, key };
}

View File

@ -1,9 +0,0 @@
export interface PluginSettings {
serverUrl: string;
selfHosted: boolean;
}
export const DEFAULT_SETTINGS: PluginSettings = {
serverUrl: "https://noteshare.space",
selfHosted: false,
};

View File

@ -1,71 +0,0 @@
import type NoteSharingPlugin from "main";
import { App, PluginSettingTab, Setting, TextComponent } from "obsidian";
import { DEFAULT_SETTINGS } from "./PluginSettings";
export default class SettingsTab extends PluginSettingTab {
plugin: NoteSharingPlugin;
private selfHostSettings: HTMLElement;
private hideSelfHosted: boolean;
private selfHostedUrl: TextComponent;
constructor(app: App, plugin: NoteSharingPlugin) {
super(app, plugin);
this.plugin = plugin;
this.hideSelfHosted = !plugin.settings.selfHosted;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Obsidian Note Sharing" });
new Setting(containerEl)
.setName("Use noteshare.space")
.setDesc(
"Noteshare.space is the official service for hosting your encrypted notes. Uncheck if you want to self-host."
)
.addToggle((text) =>
text
.setValue(!this.plugin.settings.selfHosted)
.onChange(async (value) => {
this.plugin.settings.selfHosted = !value;
this.showSelfhostedSettings(
this.plugin.settings.selfHosted
);
if (value === false) {
this.plugin.settings.serverUrl =
DEFAULT_SETTINGS.serverUrl;
this.selfHostedUrl.setValue(
this.plugin.settings.serverUrl
);
}
await this.plugin.saveSettings();
})
);
this.selfHostSettings = containerEl.createDiv();
this.selfHostSettings.createEl("h3", { text: "Self-hosting options" });
new Setting(this.selfHostSettings)
.setName("Server URL")
.setDesc("Server URL hosting the encrypted notes.")
.addText((text) => {
this.selfHostedUrl = text;
text.setPlaceholder("enter URL")
.setValue(this.plugin.settings.serverUrl)
.onChange(async (value) => {
this.plugin.settings.serverUrl = value;
await this.plugin.saveSettings();
});
});
this.showSelfhostedSettings(this.plugin.settings.selfHosted);
}
private showSelfhostedSettings(show: boolean) {
this.selfHostSettings.hidden = !show;
}
}

View File

@ -1,85 +0,0 @@
<script lang="ts">
import moment, { type Moment } from "moment";
export let url: string;
export let expireTime: Moment;
let buttonText = "Copy";
let buttonTextTimeout: string | number | NodeJS.Timeout;
function onCopy() {
clearTimeout(buttonTextTimeout);
navigator.clipboard.writeText(url);
buttonText = "Copied";
buttonTextTimeout = setTimeout(() => {
buttonText = "copy";
}, 1500);
}
function onOpen() {
window.open(url, "_blank");
}
</script>
<div class="share-plugin-modal-container">
<p id="copytext">
Your note has been encrypted and stored in the cloud. Only people with
this share link can decrypt and read this file.
</p>
<div id="url">
<input disabled id="url-input" type="text" bind:value={url} />
<button
class="url-button"
aria-label="Copy link to clipboard"
on:click={onCopy}>{buttonText}</button
>
<button
class="url-button"
aria-label="Copy link to clipboard"
on:click={onOpen}>Open</button
>
</div>
<p id="subtext">
🔐 End-to-end encrypted • Expires in {expireTime.diff(
moment(),
"days"
) + 1}
days
</p>
</div>
<style scoped>
.share-plugin-modal-container {
max-width: 560px;
}
#copytext {
width: 100%;
}
#url {
width: 100%;
display: flex;
gap: 0.5em;
}
#url-input {
flex-grow: 1;
text-decoration-line: underline;
cursor: pointer !important;
}
.url-button {
margin: 0px;
width: 5em;
}
#subtext {
/* container */
margin: 0.5em 1px;
/* text styling */
color: var(--text-muted);
font-size: 0.9em;
}
</style>

View File

@ -1,35 +0,0 @@
import type NoteSharingPlugin from "main";
import { Modal } from "obsidian";
import type { Moment } from "moment";
import Component from "./SharedNoteSuccessComponent.svelte";
export class SharedNoteSuccessModal extends Modal {
private url: string;
private component: Component;
private expire_time: Moment;
constructor(plugin: NoteSharingPlugin, url: string, expire_time: Moment) {
super(plugin.app);
this.url = url;
this.expire_time = expire_time;
this.render();
}
render() {
this.titleEl.innerText = "Shared note";
}
async onOpen() {
this.component = new Component({
target: this.contentEl,
props: {
url: this.url,
expireTime: this.expire_time,
},
});
}
async onClose() {
this.component.$destroy();
}
}

View File

@ -1,18 +0,0 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"types": ["svelte", "node"],
"baseUrl": ".",
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"outDir": "./build" /* Specify an output folder for all emitted files. */,
"lib": ["DOM", "ES5", "ES6", "ES7"]
},
"include": ["**/*.ts"]
}

View File

@ -1,14 +0,0 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

View File

@ -1,4 +0,0 @@
{
"1.0.0": "0.9.7",
"1.0.1": "0.12.0"
}

View File

@ -1,7 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// ...
},
});