├── 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 |
25 | 28 |
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 |
72 |
73 | {!store.obsidianToken && ( 74 | 81 | )} 82 | 90 | 97 |
98 | {store.obsidianToken && ( 99 | <> 100 |

Copy token and paste into plugin settings

101 | 102 | 103 | )} 104 |
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 |
178 | 179 | Buy Me a Coffee at ko-fi.com 184 | 185 |
186 | 187 |
188 |
189 | 190 |
191 |
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 | --------------------------------------------------------------------------------