├── plugin
├── .gitignore
├── manifest.json
├── versions.json
├── vite.config.js
├── tsconfig.json
├── package.json
└── main.ts
├── web
├── src
│ ├── vite-env.d.ts
│ ├── index.css
│ ├── main.tsx
│ ├── favicon.svg
│ ├── store.ts
│ ├── logo.svg
│ └── App.tsx
├── .gitignore
├── postcss.config.js
├── vite.config.ts
├── index.html
├── tailwind.config.js
├── tsconfig.json
└── package.json
├── .firebaserc
├── shared
├── tsconfig.json
├── package.json
└── firebase.ts
├── functions
├── .gitignore
├── tsconfig.json
├── package.json
└── src
│ └── index.ts
├── database.rules.json
├── manifest.json
├── firebase.json
├── package.json
├── .eslintrc.js
├── LICENSE
├── README.md
└── .gitignore
/plugin/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/plugin/manifest.json:
--------------------------------------------------------------------------------
1 | ../manifest.json
--------------------------------------------------------------------------------
/web/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/plugin/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "1.0.1": "0.9.12",
3 | "1.0.0": "0.9.7"
4 | }
5 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "obsidian-buffer"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/web/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ES2020",
4 | "moduleResolution": "node"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/web/src/main.tsx:
--------------------------------------------------------------------------------
1 | import "./index.css";
2 | import App from "./App";
3 | import { render } from "solid-js/web";
4 |
5 | render(() => , document.getElementById("root")!);
6 |
--------------------------------------------------------------------------------
/functions/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled JavaScript files
2 | lib/**/*.js
3 | lib/**/*.js.map
4 |
5 | # TypeScript v1 declaration files
6 | typings/
7 |
8 | # Node.js dependency directory
9 | node_modules/
10 |
--------------------------------------------------------------------------------
/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shared",
3 | "version": "0.0.1",
4 | "devDependencies": {
5 | "vite": "^2.6.4"
6 | },
7 | "peerDependencies": {
8 | "firebase": "^9.6.11"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import solidPlugin from "vite-plugin-solid";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [solidPlugin()],
7 | });
8 |
--------------------------------------------------------------------------------
/database.rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "users": {
4 | "$uid": {
5 | ".read": "$uid === auth.uid && auth.provider != 'anonymous'"
6 | }
7 | },
8 | "buffer": {
9 | "$uid": {
10 | ".read": "$uid === auth.uid && auth.provider != 'anonymous'"
11 | }
12 | },
13 | }
14 | }
--------------------------------------------------------------------------------
/functions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "noImplicitReturns": true,
5 | "noUnusedLocals": true,
6 | "outDir": "lib",
7 | "sourceMap": true,
8 | "strict": true,
9 | "target": "es2017"
10 | },
11 | "compileOnSave": true,
12 | "include": [
13 | "src"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-webhooks",
3 | "name": "Webhook Plugin",
4 | "version": "0.0.6",
5 | "minAppVersion": "0.9.12",
6 | "description": "Plugin that connects your notes to the internet of things through webhooks!",
7 | "author": "Stephen Solka",
8 | "authorUrl": "https://obsidian-buffer.web.app",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/plugin/vite.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const { defineConfig } = require("vite");
3 |
4 | module.exports = defineConfig({
5 | build: {
6 | minify: true,
7 | lib: {
8 | entry: path.resolve(__dirname, "main.ts"),
9 | name: "ObsidianWebhooks",
10 | formats: ["cjs"],
11 | },
12 | rollupOptions: {
13 | external: ["obsidian"],
14 | output: {},
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES2019",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "lib": [
13 | "dom",
14 | "es5",
15 | "scripthost",
16 | "es2015"
17 | ]
18 | },
19 | "include": [
20 | "**/*.ts"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./index.html", "./src/*.tsx"],
3 | corePlugins: {
4 | preflight: false,
5 | },
6 | theme: {
7 | extend: {
8 | colors: {
9 | primary: {
10 | 50: "#f2fdfb",
11 | 100: "#e6faf7",
12 | 200: "#bff4ec",
13 | 300: "#99ede0",
14 | 400: "#4ddfc9",
15 | 500: "#00d1b2",
16 | 600: "#00bca0",
17 | 700: "#009d86",
18 | 800: "#007d6b",
19 | 900: "#006657",
20 | },
21 | },
22 | },
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": false,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "preserve",
18 | "jsxImportSource": "solid-js",
19 | },
20 | "include": ["./src"]
21 | }
22 |
--------------------------------------------------------------------------------
/shared/firebase.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { FirebaseOptions, initializeApp } from "firebase/app";
4 |
5 | const firebaseConfig: FirebaseOptions = {
6 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY as string,
7 | authDomain: "obsidian-buffer.firebaseapp.com",
8 | databaseURL: "wss://obsidian-buffer-default-rtdb.firebaseio.com",
9 | projectId: "obsidian-buffer",
10 | storageBucket: "obsidian-buffer.appspot.com",
11 | messagingSenderId: "386398705772",
12 | appId: "1:386398705772:web:4ebb36001ad006dd632049",
13 | measurementId: "G-885V9M0N0C",
14 | };
15 |
16 | const app = initializeApp(firebaseConfig);
17 |
18 | export default app;
19 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database.rules.json"
4 | },
5 | "functions": {
6 | "predeploy": [
7 | "yarn workspace functions build"
8 | ]
9 | },
10 | "hosting": {
11 | "predeploy": "yarn workspace web build",
12 | "public": "web/dist",
13 | "ignore": [
14 | "firebase.json",
15 | "**/.*",
16 | "**/node_modules/**"
17 | ]
18 | },
19 | "emulators": {
20 | "auth": {
21 | "port": 9099
22 | },
23 | "functions": {
24 | "port": 5001
25 | },
26 | "database": {
27 | "port": 9000
28 | },
29 | "hosting": {
30 | "port": 5000
31 | },
32 | "ui": {
33 | "enabled": true
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "tsc && vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "firebase": "^9.6.11",
11 | "react": "^17.0.0",
12 | "react-dom": "^17.0.0",
13 | "shared": "^0.0.1",
14 | "solid-js": "^1.1.6",
15 | "tailwindcss": "^3.0.7"
16 | },
17 | "devDependencies": {
18 | "@fullhuman/postcss-purgecss": "^4.0.3",
19 | "@types/react": "^17.0.0",
20 | "@types/react-dom": "^17.0.0",
21 | "@vitejs/plugin-react": "^1.0.0",
22 | "autoprefixer": "^10.4.0",
23 | "babel-preset-solid": "^1.1.5",
24 | "vite": "^2.6.4",
25 | "vite-plugin-solid": "^2.1.1"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-webhooks",
3 | "version": "0.0.6",
4 | "repository": "git@github.com:trashhalo/obsidian-webhooks.git",
5 | "author": "Stephen Solka ",
6 | "license": "MIT",
7 | "private": true,
8 | "scripts": {
9 | "lint": "eslint --ext .ts ."
10 | },
11 | "workspaces": [
12 | "functions",
13 | "plugin",
14 | "web",
15 | "shared"
16 | ],
17 | "devDependencies": {
18 | "@typescript-eslint/eslint-plugin": "^5.0.0",
19 | "@typescript-eslint/parser": "^5.0.0",
20 | "eslint": "^8.0.1",
21 | "eslint-config-prettier": "^8.3.0",
22 | "eslint-plugin-prettier": "^4.0.0",
23 | "prettier": "^2.4.1",
24 | "typescript": "^4.3.2",
25 | "vite": "^2.6.4"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "scripts": {
4 | "lint": "eslint --ext .js,.ts .",
5 | "build": "tsc",
6 | "serve": "npm run build && firebase emulators:start --only functions",
7 | "shell": "npm run build && firebase functions:shell",
8 | "start": "npm run shell",
9 | "deploy": "firebase deploy --only functions",
10 | "logs": "firebase functions:log"
11 | },
12 | "engines": {
13 | "node": "14"
14 | },
15 | "main": "lib/index.js",
16 | "dependencies": {
17 | "express": "^4.17.1",
18 | "firebase-admin": "^9.8.0",
19 | "firebase-functions": "^3.14.1"
20 | },
21 | "devDependencies": {
22 | "firebase-functions-test": "^0.3.3"
23 | },
24 | "private": true,
25 | "version": "0.0.1"
26 | }
27 |
--------------------------------------------------------------------------------
/plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "plugin",
3 | "version": "0.0.1",
4 | "description": "This plugin connects your notes to the internet of things via webhooks",
5 | "main": "main.js",
6 | "scripts": {
7 | "build": "vite build"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "dependencies": {
12 | "firebase": "^9.6.11",
13 | "shared": "^0.0.1"
14 | },
15 | "devDependencies": {
16 | "@rollup/plugin-commonjs": "^18.0.0",
17 | "@rollup/plugin-node-resolve": "^11.2.1",
18 | "@rollup/plugin-replace": "^3.0.0",
19 | "@rollup/plugin-typescript": "^8.2.1",
20 | "@types/node": "^14.14.37",
21 | "dotenv": "^10.0.0",
22 | "obsidian": "^0.12.0",
23 | "rollup": "^2.32.1",
24 | "tslib": "^2.2.0",
25 | "vite": "^2.6.4"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | extends: ["plugin:@typescript-eslint/recommended", "prettier"],
4 | plugins: ["@typescript-eslint", "prettier"],
5 | rules: {
6 | "@typescript-eslint/ban-ts-comment": "off",
7 | "@typescript-eslint/camelcase": "off",
8 | "@typescript-eslint/explicit-module-boundary-types": "off",
9 | "@typescript-eslint/no-empty-function": "off",
10 | "@typescript-eslint/no-explicit-any": "off",
11 | "@typescript-eslint/no-unused-vars": "off",
12 | "@typescript-eslint/no-use-before-define": "off",
13 | "@typescript-eslint/no-var-requires": "off",
14 | "no-console": "warn",
15 | "no-shadow": "error",
16 | "prefer-const": "off",
17 | "prefer-rest-params": "off",
18 | "require-jsdoc": "off",
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Stephen Solka
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Obsidian Webhooks
2 |
3 | Obsidian plugin and service that connects your editor to the internet of things through webhooks
4 |
5 | ## Example Use cases
6 |
7 | - add quick thoughts to your notes by talking to your Google assistant
8 | - capture a note every time you like a song on Spotify
9 | - capture a note every time you react to a slack message with a pencil emoji
10 | - change or add notes any time you do any action on any other app
11 |
12 | ## Setting up an example rule
13 |
14 | 1. Install the obsidian plugin from releases
15 | 2. Go to https://obsidian-buffer.web.app to signup for the service
16 | 3. Generate a login token and install it into the webhook plugin settings in Obsidian
17 | 4. Use the webhook url on the service website with your favorite automation service
18 | 5. For the spotify example usecase connect IFTTT to spotify
19 | 6. Create an applet that connects `new saved track` event to webhooks service
20 | 7. Paste the webhook url into the service url
21 | 8. Change the content type to text/plain
22 | 9. Change the method type to POST
23 | 10. In the request body you can now type markdown to be appended to a note, be sure to use the ingredients button to reference information from the spotify event.
24 |
25 | My rule is set to append:
26 |
27 | ```markdown
28 | - [[{{Spotify.newSavedTrack.ArtistName}}]] [[{{Spotify.newSavedTrack.AlbumName}}]] - {{Spotify.newSavedTrack.TrackName}}
29 | ```
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | firebase-debug.log*
8 | firebase-debug.*.log*
9 |
10 | # Firebase cache
11 | .firebase/
12 |
13 | # Firebase config
14 |
15 | # Uncomment this if you'd like others to create their own Firebase project.
16 | # For a team working on the same Firebase project(s), it is recommended to leave
17 | # it commented so all members can deploy to the same project(s) in .firebaserc.
18 | # .firebaserc
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Directory for instrumented libs generated by jscoverage/JSCover
27 | lib-cov
28 |
29 | # Coverage directory used by tools like istanbul
30 | coverage
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (http://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 | .yarn
68 | .yarcrc.yml
69 | plugin/main.js
--------------------------------------------------------------------------------
/web/src/favicon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/web/src/store.ts:
--------------------------------------------------------------------------------
1 | import { FirebaseApp } from "firebase/app";
2 | import { User } from "firebase/auth";
3 | import { createContext } from "solid-js";
4 | import { createStore } from "solid-js/store";
5 |
6 | export type Event = {
7 | key: string;
8 | val: unknown;
9 | };
10 |
11 | export type Store = {
12 | app?: FirebaseApp;
13 | currentUser?: User;
14 | obsidianToken?: string;
15 | key?: string;
16 | buffer?: Event[];
17 | loading?: boolean;
18 | };
19 |
20 | export type StoreMutations = {
21 | setApp(app: FirebaseApp): void;
22 | setCurrentUser(user: User | undefined): void;
23 | setObsidianToken(token: string): void;
24 | setLoading(loading: boolean): void;
25 | setKey(key: string): void;
26 | };
27 |
28 | export const AppContext = createContext<[Store, StoreMutations]>([
29 | {},
30 | {
31 | setApp(app: FirebaseApp) {},
32 | setCurrentUser(user: User | undefined) {},
33 | setObsidianToken(token: string) {},
34 | setLoading(loading: boolean) {},
35 | setKey(key: string) {},
36 | },
37 | ]);
38 |
39 | export const createAppStore = (): [Store, StoreMutations] => {
40 | const [state, setState] = createStore({
41 | loading: true,
42 | });
43 |
44 | return [
45 | state,
46 | {
47 | setApp(app) {
48 | setState("app", app);
49 | },
50 | setCurrentUser(user) {
51 | setState("currentUser", user);
52 | },
53 | setObsidianToken(token) {
54 | setState("obsidianToken", token);
55 | },
56 | setLoading(loading) {
57 | setState("loading", loading);
58 | },
59 | setKey(key) {
60 | setState("key", key);
61 | },
62 | },
63 | ];
64 | };
65 |
--------------------------------------------------------------------------------
/web/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/functions/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as functions from "firebase-functions";
2 | import * as admin from "firebase-admin";
3 | import * as express from "express";
4 | import * as crypto from "crypto";
5 |
6 | admin.initializeApp();
7 | const webhookApp = express();
8 | webhookApp.post("/:key", async (req: any, res: any) => {
9 | const user = await (
10 | await admin.database().ref(`/keys/${req.params.key}`).get()
11 | ).val();
12 | if (!user) {
13 | return res.status(403).send("invalid key");
14 | }
15 | const today = new Date();
16 | const exp = new Date(
17 | today.getFullYear(),
18 | today.getMonth(),
19 | today.getDate() + 7
20 | );
21 |
22 | let path: string;
23 | const qPath: unknown = req.query.path;
24 | if (typeof qPath === "string") {
25 | path = qPath;
26 | } else if (Array.isArray(qPath)) {
27 | path = qPath[0];
28 | } else {
29 | return res
30 | .status(422)
31 | .send(
32 | `path not a valid format. expected string recieved ${JSON.stringify(
33 | qPath
34 | )}`
35 | );
36 | }
37 |
38 | const buffer = {
39 | id: crypto.randomBytes(16).toString("hex"),
40 | path,
41 | exp,
42 | data: req.rawBody.toString(),
43 | };
44 | await admin.database().ref(`/buffer/${user}`).push(buffer);
45 | res.send("ok");
46 | });
47 | export const webhook = functions.https.onRequest(webhookApp);
48 |
49 | export const newUser = functions.auth.user().onCreate((user) => {
50 | const key = crypto.randomBytes(24).toString("hex");
51 | admin.database().ref(`/keys/${key}`).set(user.uid);
52 | admin.database().ref(`/users/${user.uid}/key`).set(key);
53 | });
54 |
55 | export const wipe = functions.https.onCall(async (data, context) => {
56 | if (context.auth) {
57 | const user = await admin.auth().getUser(context.auth.uid);
58 | if (user.providerData[0].providerId != "anonymous") {
59 | const db = admin.database();
60 | const ref = db.ref(`/buffer/${user.uid}`);
61 | await ref.transaction((buffer) => {
62 | if (buffer == null) {
63 | return buffer;
64 | }
65 | if (typeof buffer == "object") {
66 | const arr: { id: string }[] = Object.values(buffer);
67 | const index = arr.findIndex((v) => v.id === data.id);
68 | return arr.splice(index + 1);
69 | }
70 | throw new Error(
71 | `buffer not as expected ${typeof buffer} ${JSON.stringify(buffer)}`
72 | );
73 | });
74 | }
75 | }
76 | });
77 |
78 | export const generateObsidianToken = functions.https.onCall(
79 | (_data, context) => {
80 | if (context.auth) {
81 | return admin.auth().createCustomToken(context.auth.uid);
82 | }
83 | throw new Error("authed only");
84 | }
85 | );
86 |
--------------------------------------------------------------------------------
/web/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { onCleanup, onMount, Show, useContext } from "solid-js";
2 | import { getDatabase, onValue, ref } from "firebase/database";
3 | import { getFunctions, httpsCallable } from "firebase/functions";
4 | import { AppContext, createAppStore } from "./store";
5 | import app from "shared/firebase";
6 | import {
7 | Auth,
8 | getAuth,
9 | GoogleAuthProvider,
10 | signInWithPopup,
11 | signOut,
12 | User,
13 | } from "@firebase/auth";
14 |
15 | const Login = () => {
16 | const [store, { setLoading }] = useContext(AppContext);
17 | const provider = new GoogleAuthProvider();
18 | const loginWithGoogle = async (e: Event) => {
19 | e.preventDefault();
20 | setLoading(true);
21 | return signInWithPopup(getAuth(store.app), provider);
22 | };
23 | return (
24 |
29 | );
30 | };
31 | const functions = getFunctions(app);
32 | const generateObsidianToken = httpsCallable(functions, "generateObsidianToken");
33 | const wipe = httpsCallable(functions, "wipe");
34 |
35 | const Authed = () => {
36 | const [store, { setCurrentUser, setObsidianToken, setLoading }] =
37 | useContext(AppContext);
38 |
39 | const handleGenerateClick = async () => {
40 | setLoading(true);
41 | try {
42 | const { data } = await generateObsidianToken();
43 | typeof data === "string" && setObsidianToken(data);
44 | } finally {
45 | setLoading(false);
46 | }
47 | };
48 |
49 | const handleLogoutClick = async (auth: Auth) => {
50 | try {
51 | setLoading(true);
52 | await signOut(auth);
53 | setCurrentUser(undefined);
54 | } finally {
55 | setLoading(false);
56 | }
57 | };
58 |
59 | const handleClearClick = async () => {
60 | try {
61 | setLoading(true);
62 | // clear everything
63 | await wipe({ id: -1 });
64 | } finally {
65 | setLoading(false);
66 | }
67 | };
68 |
69 | return (
70 | <>
71 |
105 | {store.key && (
106 | <>
107 | Webhook URL
108 |
109 |
110 | - Use webhook url in services like IFTTT
111 | -
112 | Query param{" "}
113 | path{" "}
114 | controls which file to update
115 |
116 | - Method type POST
117 | - Body is the markdown to insert into the file
118 |
119 |
120 |
125 | >
126 | )}
127 | {store.buffer && (
128 | <>
129 | {store.buffer.map((v) => (
130 | {JSON.stringify(v.val)}
131 | ))}
132 | >
133 | )}
134 | >
135 | );
136 | };
137 |
138 | function App() {
139 | const store = createAppStore();
140 | const [state, { setApp, setLoading, setKey, setCurrentUser }] = store;
141 |
142 | setApp(app);
143 | const auth = getAuth(state.app);
144 |
145 | let keyUnsubscribe = () => {};
146 | const authUnsubscribe = auth.onAuthStateChanged((user: User | null) => {
147 | keyUnsubscribe();
148 | setCurrentUser(user || undefined);
149 | if (user) {
150 | setLoading(true);
151 | const db = getDatabase(state.app);
152 | keyUnsubscribe = onValue(ref(db, `users/${user.uid}/key`), (value) => {
153 | const val = value.val();
154 | setKey(val);
155 | if (val) {
156 | setLoading(false);
157 | }
158 | });
159 | } else {
160 | setLoading(false);
161 | }
162 | });
163 |
164 | onCleanup(() => {
165 | authUnsubscribe();
166 | keyUnsubscribe();
167 | });
168 |
169 | return (
170 | <>
171 |
172 |
173 |
174 | Obsidian Webhooks
175 | Connect obsidian to the internet of things via webhooks
176 |
177 |
186 |
187 |
192 |
193 |
194 | {state.currentUser ? : }
195 |
196 |
197 |
198 | >
199 | );
200 | }
201 |
202 | export default App;
203 |
--------------------------------------------------------------------------------
/plugin/main.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { App, Notice, Plugin, PluginSettingTab, Setting } from "obsidian";
3 | import {
4 | Auth,
5 | getAuth,
6 | signInWithCustomToken,
7 | signOut,
8 | Unsubscribe,
9 | } from "firebase/auth";
10 | import { FirebaseApp } from "firebase/app";
11 | import {
12 | DataSnapshot,
13 | getDatabase,
14 | goOffline,
15 | goOnline,
16 | onValue,
17 | ref,
18 | } from "firebase/database";
19 | import { getFunctions, httpsCallable } from "firebase/functions";
20 | import app from "shared/firebase";
21 |
22 | enum NewLineType {
23 | Windows = 1,
24 | UnixMac = 2,
25 | }
26 |
27 | interface MyPluginSettings {
28 | token: string;
29 | frequency: string;
30 | triggerOnLoad: boolean;
31 | error?: string;
32 | newLineType?: NewLineType;
33 | }
34 |
35 | const DEFAULT_SETTINGS: MyPluginSettings = {
36 | token: "",
37 | frequency: "0", // manual by default
38 | triggerOnLoad: true,
39 | newLineType: undefined,
40 | };
41 |
42 | export default class ObsidianWebhooksPlugin extends Plugin {
43 | settings: MyPluginSettings;
44 | firebase: FirebaseApp;
45 | loggedIn: boolean;
46 | authUnsubscribe: Unsubscribe;
47 | valUnsubscribe: Unsubscribe;
48 |
49 | async onload() {
50 | console.log("loading plugin");
51 | await this.loadSettings();
52 | this.firebase = app;
53 | this.authUnsubscribe = getAuth(this.firebase).onAuthStateChanged((user) => {
54 | if (this.valUnsubscribe) {
55 | this.valUnsubscribe();
56 | }
57 | if (user) {
58 | const db = getDatabase(this.firebase);
59 | const buffer = ref(db, `buffer/${user.uid}`);
60 | this.valUnsubscribe = onValue(buffer, async (data) => {
61 | try {
62 | await goOffline(db);
63 | await this.onBufferChange(data);
64 | } finally {
65 | await goOnline(db);
66 | }
67 | });
68 | }
69 | });
70 |
71 | this.addSettingTab(new WebhookSettingTab(this.app, this));
72 | }
73 |
74 | async onBufferChange(data: DataSnapshot) {
75 | if (!data.hasChildren()) {
76 | return;
77 | }
78 |
79 | try {
80 | let last: unknown = undefined;
81 | let promiseChain = Promise.resolve();
82 | data.forEach((event) => {
83 | const val = event.val();
84 | last = val;
85 | promiseChain = promiseChain.then(() => this.applyEvent(val));
86 | });
87 | await promiseChain;
88 | await this.wipe(last);
89 | promiseChain.catch((err) => {});
90 |
91 | new Notice("notes updated by webhooks");
92 | } catch (err) {
93 | new Notice("error processing webhook events, " + err.toString());
94 | throw err;
95 | } finally {
96 | }
97 | }
98 |
99 | async wipe(value: unknown) {
100 | const functions = getFunctions(this.firebase);
101 | const wipe = httpsCallable(functions, "wipe");
102 | await wipe(value);
103 | }
104 |
105 | async applyEvent({
106 | data,
107 | path: pathOrArr,
108 | }: {
109 | data: string;
110 | path: string | Array;
111 | }) {
112 | const fs = this.app.vault.adapter;
113 | let path: string;
114 | if (typeof pathOrArr === "string") {
115 | path = pathOrArr;
116 | } else {
117 | path = Object.values(pathOrArr).first();
118 | }
119 |
120 | let dirPath = path.replace(/\/*$/, "").replace(/^(.+)\/[^\/]*?$/, "$1");
121 | if (dirPath !== path) {
122 | // == means its in the root
123 | const exists = await fs.stat(dirPath);
124 | if (!exists) {
125 | await fs.mkdir(dirPath);
126 | }
127 | }
128 | let contentToSave = data;
129 | if (this.settings.newLineType == NewLineType.UnixMac) {
130 | contentToSave += "\n";
131 | } else if (this.settings.newLineType == NewLineType.Windows) {
132 | contentToSave += "\r\n";
133 | }
134 | const pathStat = await fs.stat(path);
135 | console.log("webhook updating path", path, pathStat);
136 | if (pathStat?.type === "folder") {
137 | throw new Error(
138 | `path name exists as a folder. please delete folder: ${path}`
139 | );
140 | } else if (pathStat?.type == "file") {
141 | const existingContent = await fs.read(path);
142 | contentToSave = existingContent + contentToSave;
143 | }
144 | await fs.write(path, contentToSave);
145 | }
146 |
147 | onunload() {
148 | console.log("unloading plugin");
149 | if (this.authUnsubscribe) {
150 | this.authUnsubscribe();
151 | }
152 | if (this.valUnsubscribe) {
153 | this.valUnsubscribe();
154 | }
155 | }
156 |
157 | async loadSettings() {
158 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
159 | }
160 |
161 | async saveSettings() {
162 | await this.saveData(this.settings);
163 | }
164 | }
165 |
166 | class WebhookSettingTab extends PluginSettingTab {
167 | plugin: ObsidianWebhooksPlugin;
168 | auth: Auth;
169 | authObserver: Unsubscribe;
170 |
171 | constructor(oApp: App, plugin: ObsidianWebhooksPlugin) {
172 | super(oApp, plugin);
173 | this.plugin = plugin;
174 | this.auth = getAuth(this.plugin.firebase);
175 | this.authObserver = this.auth.onAuthStateChanged(this.display);
176 | }
177 |
178 | hide(): void {
179 | this.authObserver();
180 | }
181 |
182 | display(): void {
183 | if (!this) {
184 | return;
185 | }
186 |
187 | let { containerEl } = this;
188 |
189 | containerEl.empty();
190 |
191 | containerEl.createEl("h2", { text: "Settings for webhooks" });
192 | containerEl
193 | .createEl("p", { text: "Generate login tokens at " })
194 | .createEl("a", {
195 | text: "Obsidian Webhooks",
196 | href: "https://obsidian-buffer.web.app",
197 | });
198 |
199 | if (this.plugin.settings.error) {
200 | containerEl.createEl("p", {
201 | text: `error: ${this.plugin.settings.error}`,
202 | });
203 | }
204 |
205 | if (this.auth.currentUser) {
206 | new Setting(containerEl)
207 | .setName(`logged in as ${this.auth.currentUser.email}`)
208 | .addButton((button) => {
209 | button
210 | .setButtonText("Logout")
211 | .setCta()
212 | .onClick(async (evt) => {
213 | try {
214 | await signOut(this.auth);
215 | this.plugin.settings.error = undefined;
216 | } catch (err) {
217 | this.plugin.settings.error = err.message;
218 | } finally {
219 | await this.plugin.saveSettings();
220 | this.display();
221 | }
222 | });
223 | });
224 | new Setting(containerEl)
225 | .setName("New Line")
226 | .setDesc("Add new lines between incoming notes")
227 | .addDropdown((dropdown) => {
228 | dropdown.addOption("none", "No new lines");
229 | dropdown.addOption("windows", "Windows style newlines");
230 | dropdown.addOption("unixMac", "Linux, Unix or Mac style new lines");
231 | const { newLineType } = this.plugin.settings;
232 | if (newLineType === undefined) {
233 | dropdown.setValue("none");
234 | } else if (newLineType == NewLineType.Windows) {
235 | dropdown.setValue("windows");
236 | } else if (newLineType == NewLineType.UnixMac) {
237 | dropdown.setValue("unixMac");
238 | }
239 | dropdown.onChange(async (value) => {
240 | if (value == "none") {
241 | this.plugin.settings.newLineType = undefined;
242 | } else if (value == "windows") {
243 | this.plugin.settings.newLineType = NewLineType.Windows;
244 | } else if (value == "unixMac") {
245 | this.plugin.settings.newLineType = NewLineType.UnixMac;
246 | }
247 | await this.plugin.saveSettings();
248 | this.display();
249 | });
250 | });
251 | return;
252 | }
253 |
254 | new Setting(containerEl).setName("Webhook login token").addText((text) =>
255 | text
256 | .setPlaceholder("Paste your token")
257 | .setValue(this.plugin.settings.token)
258 | .onChange(async (value) => {
259 | console.log("Secret: " + value);
260 | this.plugin.settings.token = value;
261 | await this.plugin.saveSettings();
262 | })
263 | );
264 |
265 | new Setting(containerEl)
266 | .setName("Login")
267 | .setDesc("Exchanges webhook token for authenication")
268 | .addButton((button) => {
269 | button
270 | .setButtonText("Login")
271 | .setCta()
272 | .onClick(async (evt) => {
273 | try {
274 | await signInWithCustomToken(
275 | this.auth,
276 | this.plugin.settings.token
277 | );
278 | this.plugin.settings.token = "";
279 | this.plugin.settings.error = undefined;
280 | } catch (err) {
281 | this.plugin.settings.error = err.message;
282 | } finally {
283 | await this.plugin.saveSettings();
284 | this.display();
285 | }
286 | });
287 | });
288 | }
289 | }
290 |
--------------------------------------------------------------------------------