From 9d715597a600710c20f75412d3dcd8cdb7b3c39e Mon Sep 17 00:00:00 2001 From: vantezzen Date: Sun, 8 Mar 2020 11:10:17 +0100 Subject: Add documentation --- README.md | 18 +----- docs/README.md | 7 +++ docs/backend_api.md | 90 +++++++++++++++++++++++++++ docs/configuration.md | 92 +++++++++++++++++++++++++++ docs/frontend_api.md | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++ docs/integration.md | 148 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 508 insertions(+), 16 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/backend_api.md create mode 100644 docs/configuration.md create mode 100644 docs/frontend_api.md create mode 100644 docs/integration.md diff --git a/README.md b/README.md index 895663f..3bc7a84 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,5 @@ Recipes for Ferdi. Copyright on these recipes is on their original creators. -## Adding your own recipes -After creating your own recipe using [Franz's guide](https://github.com/meetfranz/plugins/blob/master/docs/integration.md) you can add them to this repository. - -1. Copy your recipe files into `scripts/recipe_src` -2. Open a terminal at `scripts/` -3. Run `yarn install` to install all dependencies -4. Run `yarn package` to package your new recipe -5. Create a PR to with your new changes - -## Importing recipes from GitHub -If you've uploaded your recipe to GitHub, you can easily import it into the Ferdi recipe repository using the `add_github` script: - -1. Open a terminal at `scripts/` -2. Run `yarn install` to install all dependencies -3. Run `yarn github [GitHub URL]`, e.g. `yarn github https://github.com/vantezzen/franz-recipe-standardnotes`, to add your new recipe -4. Create a PR to with your new changes \ No newline at end of file +## Creating and adding your own recipes +Please refer to our documentation at https://github.com/getferdi/recipes/blob/master/docs/integration.md. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..689ecd5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Ferdi Integration Documentation +Create your own [Ferdi](https://getferdi.com) service integration within a few minutes. + +* [Overview / How to create a Ferdi integration](integration.md) +* [Configuration (package.json)](configuration.md) +* [Frontend API (webview.js)](frontend_api.md) +* [Backend API (index.js)](backend_api.md) diff --git a/docs/backend_api.md b/docs/backend_api.md new file mode 100644 index 0000000..dfc9f22 --- /dev/null +++ b/docs/backend_api.md @@ -0,0 +1,90 @@ +# Backend API + +Provides a set of helper functions to integrate the recipe into [Ferdi](https://getferdi.com). + +## Ferdi Backend Class Methods +* [validateUrl](#user-content-validateurl) +* [overrideUserAgent](#user-content-overrideuseragent) + +## Events +* [webview events](#user-content-events) + +### validateUrl(URL) +Validate if the given URL is a valid service instance. + +#### Arguments +1. `string` URL + +#### Returns +[`Promise`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) + +#### Usage + +```js +// RocketChat integration +module.exports = Ferdi => class RocketChat extends Ferdi { + async validateUrl(url) { + try { + const resp = await window.fetch(`${url}/api/info`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await resp.json(); + + return Object.hasOwnProperty.call(data, 'version'); + } catch (err) { + console.error(err); + } + + return false; + } +}; +``` + +### overrideUserAgent() +Validate if the given URL is a valid service instance. + +#### Returns +`Boolean` + +#### Usage + +```js +// Discord integration +module.exports = Ferdi => class Discord extends Ferdi { + overrideUserAgent() { + const useragent = window.navigator.userAgent; + + // Quick and dirty hackfix + const parts = useragent.split('(KHTML, like Gecko)'); + + return parts.join('(KHTML, like Gecko) discord/0.0.248').replace('Electron', 'Discord').replace('Ferdi', 'Discord'); + } +}; + +``` + +### Events +Ferdi recipes can hook into the [electron webview events](https://electron.atom.io/docs/api/webview-tag/#dom-events) to trigger custom functions. + +This is necessary for services like TweetDeck where custom URL forwarding is needed during login. + +#### Usage +```js +module.exports = Ferdi => class Tweetdeck extends Ferdi { + events = { + 'did-get-redirect-request': '_redirectFix', + } + + _redirectFix(event) { + if (event.newURL !== undefined && event.oldURL !== undefined && event.isMainFrame) { + if (event.isMainFrame) { + setTimeout(() => this.send('redirect-url', event.newURL), 100); + event.preventDefault(); + } + } + } +}; +``` diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..923ed19 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,92 @@ +# Integration Config + +A [Ferdi](https://getferdi.com) recipe is a node module. In order to learn more about node modules and their configuration check the official [Node.js documentation](https://nodejs.org/api/modules.html) / [npm package.json documentation](https://docs.npmjs.com/files/package.json). + +## Table of Contents +* [Config flags](#user-content-config-flags) +* [Examples](#user-content-examples) + +## Config flags + +`string` **id**
+Unique identifier name of the plugin. The name of the plugin folder has to be the same. + +This ID cannot contain any special characters or spaces. + +`string` **name**
+Display name of the service. + +`string` **version**
+Version number. Will be used for auto updating the integrations. The version number must be in a semver compatible format: `1.0.0`. +**important:** the version will be used to figure out if a new recipe update should be deployed to the user. If you make changes to a recipe, **always** increase the version number or Ferdi won't update your recipe. + +`string` **description**
+Short description about your integration. Not currently used. + +`string` **main**
+The plugins main entry point. In our case `index.js`. + +`string` **author**
+Author of the integration. Not currently used. + +`string` **license**
+The license of the integration. We prefer MIT, but here is a list of all the available SPDX licenses http://spdx.org/licenses/ + +`string` **repository**
+Link to your Github, Gitlab or Bitbucket public repository + +`object` **config**
+This is the Ferdi specific integration config. + +* `string` **serviceURL**
+Defines the URL that should be loaded into the Ferdi webview. +

+If you want to load a simple URL like `https://www.messenger.com`, you can simply define it via the `serviceURL` parameter. If your service URL is team based, e.g. Slack or HipChat you can use `https://{teamId}.slack.com`. +

+If your service works with custom URLs, just leave this empty. +

+**Examples** +```json +{ + "serviceURL": "https://www.messenger.com" +} +``` +
+ +```json +{ + "serviceURL": "https://{teamId}.slack.com" +} +``` +* `boolean` **hasTeamId** _default: true_
+Is this a team based service? If true, the interface to add the service will require a team identifier. e.g. `[teamId]`.slack.com +* `boolean` **urlInputSuffix**
+This option is only used in combination with `hasTeamId: true` in order to display the value of `urlInputSuffix` after the input for TeamId to make it obvious to the user what input is required from him. Eg. _<TeamID>.hipchat.com_ +* `boolean` **hasCustomUrl** _default: false_
+On premise services like HipChat, Mattermost, ... require a custom URL. This option enables the user to enter a custom URL when adding the service. +* `boolean` **hasNotificationSound** _default: false_
+Some services provide their own notification sound. In order to avoid multiple sounds when the user receives a message set this to `true`. If the service has no built in notification sound set this to `false`. +* `boolean` **hasIndirectMessages** _default: false_
+Services like Slack or HipChat have direct messages e.g. a mention or message to every user in a channel (@channel) and indirect messages e.g. general discussion in a channel. If this flag is set to `true`, the user can enable/disable if there should be a badge for indirect messages. +* `string` **message**
+Info message that will be displayed in the add/edit service interface. + +## Example +### Mattermost configuration +```json +{ + "id": "mattermost", + "name": "Mattermost", + "version": "1.0.0", + "description": "Mattermost", + "main": "index.js", + "author": "Stefan Malzner ", + "license": "MIT", + "repository": "https://github.com/meetFerdi/recipe-mattermost", + "config": { + "hasNotificationSound": true, + "hasIndirectMessages": true, + "hasCustomUrl": true + } +} +``` diff --git a/docs/frontend_api.md b/docs/frontend_api.md new file mode 100644 index 0000000..c1d35ce --- /dev/null +++ b/docs/frontend_api.md @@ -0,0 +1,169 @@ +# Frontend API + +Provides a set of helper functions to integrate the service into [Ferdi](https://getferdi.com). + +## Ferdi Class Methods +* [setBadge](#user-content-setbadge) +* [injectCSS](#user-content-injectcss) +* [loop](#user-content-loop) +* [onNotify](#user-content-onnotify) +* [handleDarkMode](#user-content-handleDarkMode) + +### setBadge(directMessages, [indirectMessages]) +Sets the unread message badge + +#### Arguments +1. `int` directMessages + * sets the count of direct messages eg. Slack direct mentions, or a message to @channel +2. `int` indirectMessages (optional) + * Set a badge that defines there are new messages but they do not involve me directly to me eg. in a channel + +#### Usage + +```js +Ferdi.setBadge(4, 2); + +// or + +Ferdi.setBadge(3); +``` + +### injectCSS(pathToCssFile) +Injects the contents of one or more CSS files into the current webview + +#### Arguments +1. `string` cssFile + * CSS files that should be injected. This must be an absolute path to the file + +#### Usage + +```js +const path = require('path'); + +// inject a single css file +Ferdi.injectCSS(path.join(__dirname, 'style.css')); + +// inject multiple css files +const globalStyles = path.join(__dirname, 'global.css'); +const focusModeStyles = path.join(__dirname, 'focusmode.css'); + +Ferdi.injectCSS(globalStyles, focusModeStyles); +``` + +### loop(action) +Runs an action every X milliseconds (Ferdi default is currently 1s) + +#### Arguments +1. `function` action + +#### Usage + +```js +// slack integration +const path = require('path'); + +module.exports = (Ferdi) => { + const getMessages = () => { + const directMessages = $('.unread_highlights, .unread_highlight').not('.hidden').length; + const indirectMessages = $('.unread').length - directMessages; + + Ferdi.setBadge(directMessages, indirectMessages); + } + + Ferdi.loop(getMessages); + + Ferdi.injectCSS(path.join(__dirname, 'style.css')); +} +``` + +### onNotify(fn) +Runs `fn` on every notification created by the service before sending them to the host (Useful if you want to update information of the notification before showing it to the user) + +#### Arguments +1. `function` fn + +#### Usage + +```js +// messenger integration +module.exports = (Ferdi) => { + const getMessages = function getMessages() { + let count = document.querySelectorAll('._5fx8:not(._569x),._1ht3:not(._569x)').length; + const messageRequestsElement = document.querySelector('._5nxf'); + if (messageRequestsElement) { + count += parseInt(messageRequestsElement.innerHTML, 10); + } + + Ferdi.setBadge(count); + }; + + Ferdi.loop(getMessages); + + Ferdi.onNotify(notification => { + + if (typeof notification.title !== 'string') { + notification.title = ((notification.title.props || {}).content || [])[0] || 'Messenger'; + } + + return notification; + + }); +}; +``` + +### handleDarkMode(callback) +You can use a `darkmode.css` to automatically get the service into a dark theme. If your service already supports its own dark mode (e.g. Reddit and YouTube have build-in dark modes) you can use a custom dark mode handler instead. + +This handler should take the nesessary steps to (de-)activate dark mode on the page, e.g. by clicking a button or flipping a switch. + +Ferdi won't activate DarkReader or inject `darkmode.css` if the recipe has defined a custom handler. If you still need to do this, you can use the `injectDarkModeStyle` or `enableDarkMode` function provided as the second argument. + +#### Arguments +1. `function` callback + +#### Callback function arguments +1. `boolean` isEnabled: Is Dark Mode currently enabled? +2. `object` helpers: Helper functions that you can use in your function: + `enableDarkMode` - Enable DarkReader + `injectDarkModeStyle` - Inject darkmode.css + `removeDarkModeStyle` - Remove service's darkmode.css + `disableDarkMode` - Disable DarkReader + `isDarkModeStyleInjected` - Function that returns true if darkmode.css is injected into the page + +#### Usage +```JavaScript +// Handler that works for Reddit +Ferdi.handleDarkMode((isEnabled, helpers) => { + // Open dropdown menu if not already open + const menu = document.querySelector('#USER_DROPDOWN_ID'); + if (menu.getAttribute('aria-expanded') === 'false') { + menu.click(); + } + + setTimeout(() => { + // Check if service is already in right mode + const btn = document.querySelector('[role=menu] button button'); + const checked = btn.getAttribute('aria-checked') === 'true'; + + if ((checked && !isEnabled) || (!checked && isEnabled)) { + // Click the button to switch between modes + btn.click(); + } + }, 50); +}); + +// --- or --- + +// Helper that activates DarkReader and injects your darkmode.css at the same time +Ferdi.handleDarkMode((isEnabled, helpers) => { + if (isEnabled) { + helpers.enableDarkMode(); + if (!helpers.isDarkModeStyleInjected()) { + helpers.injectDarkModeStyle(); + } + } else { + helpers.disableDarkMode(); + helpers.removeDarkModeStyle(); + } +}) +``` \ No newline at end of file diff --git a/docs/integration.md b/docs/integration.md new file mode 100644 index 0000000..ddfc35c --- /dev/null +++ b/docs/integration.md @@ -0,0 +1,148 @@ +# Ferdi Recipe Documentation / Overview + +A Ferdi recipe is basically nothing else than a node module and is currently initialized on `dom-ready`. You access all of the [electron](http://electron.atom.io) modules as well. + +Recipes are responsible for providing the connection between the service itself (e.g. WhatsApp) and Ferdi, providing information like the number of current notifications or handling dark mode. + +## Table of Contents +* [Installation](#user-content-installation) +* [Plugin structure](#user-content-recipe-structure) +* [Configuration (package.json)](#user-content-packagejson) +* [Backend (index.js)](#user-content-indexjs) +* [Frontend (webview.js)](#user-content-webviewjs) +* [Icons](#user-content-icons) +* [Dark Mode](#user-content-dark-mode) +* [Debugging](#user-content-debugging) +* [Deployment](#user-content-deployment) + +## Installation +1. To install a new recipe for testing, download the recipe folder e.g `whatsapp` or simply create an empty one with the name of your new recipe (we recommend using a recipe like `whatsapp` as a template though). +2. Open the development Ferdi Plugins folder on your machine (note that the `dev` directory may not exist yet, and you must create it): + * Mac: `~/Library/Application Support/Ferdi/recipes/dev/` + * Windows: `%appdata%/Ferdi/recipes/dev/` + * Linux: `~/.config/Ferdi/recipes/dev` +3. Copy the recipe folder into this folder +4. Reload Ferdi (`CMD/CTRL + SHIFT + R`) + +## Recipe structure +Every recipe needs a specific file structure in order to work as a Ferdi recipe + +* icon.svg - Icon for the service in SVG form +* icon.png - Icon for the service in PNG form (1024x1024px) +* index.js - Backend script, this script is NOT included in the service webview but only in Ferdi itself +* package.json - Information about the recipe +* webview.js - Frontend script, this script is injected into the service itself but still has access to all NodeJS APIs +* darkmode.css - CSS File that gets included when dark mode is activated + +### package.json +The package.json is structured like any other node module and allows to completely configure the service. + +```json +{ + "id": "tweetdeck", + "name": "Tweetdeck", + "version": "1.0.1", + "description": "Tweetdeck", + "main": "index.js", + "author": "Stefan Malzner ", + "license": "MIT", + "repository": "https://github.com/meetfranz/recipe-tweetdeck", + "config": { + "serviceURL": "https://tweetdeck.twitter.com/" + } +} +``` + +To get more information about all the provided configuration flags, check the [config docs](configuration.md). + +Please note that the fields `id`, `name`, `version` and `config` and required. + + +### index.js +This is your "backend" code. Right now the options are very limited and most of the services don't need a custom handling here. If your service is relatively straight forward and has a static URL eg. _messenger.com_, _`[TEAMID]`.slack.com_ or _web.skype.com_ all you need to do to return the Ferdi Class: + +```js +module.exports = Ferdi => Ferdi; +``` + +If your service can be hosted on custom servers, you can validate the given URL to detect if it's your server and not e.g. google.com. To enable validation you can override the function `validateServer` +```js +// RocketChat integration +module.exports = Ferdi => class RocketChat extends Ferdi { + async validateUrl(url) { + try { + const resp = await window.fetch(`${url}/api/info`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await resp.json(); + + return Object.hasOwnProperty.call(data, 'version'); + } catch (err) { + console.error(err); + } + + return false; + } +}; +``` + +`validateServer` needs to return a [`Promise`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise), otherwise validation will fail. + +### webview.js +The webview.js is the actual script that will be loaded into the webview. Here you can do whatever you want to do in order perfectly integrate the service into Ferdi. For convenience, we have provided a very simple set of functions to set unread message badges (`Ferdi.setBadge()`) and inject CSS files (`Ferdi.injectCSS()`). + + +```js +// orat.io integration +module.exports = (Ferdi) => { + function getMessages() { + let direct = 0; + let indirect = 0; + const FerdiData = document.querySelector('#FerdiMessages').dataset; + if (FerdiData) { + direct = FerdiData.direct; + indirect = FerdiData.indirect; + } + + Ferdi.setBadge(direct, indirect); + } + + Ferdi.loop(getMessages); +} +``` + +To get more information about the provided functions, check the [API docs](frontend_api.md). + +### Icons +In order to show every service icon crystal clear within the Ferdi UI, we require a .svg and .png in 1024x1024px. + +### Dark Mode +You can provide a custom Dark Mode Theme for your recipes just by putting the `darkmode.css` into your recipe folder. Once the `darkmode.css` exists, you can enable the Dark Mode in your service settings. + +Recipe Dark Mode is only supported by Ferdi 5.0.0-beta.19+ + +### Debugging +In order to debug your service integration, open Ferdi and use the shortcut `Cmd/Ctrl+Alt+Shift+i` to open the recipes developer tools. + +### Publishing +Ferdi uses its recipe repository at to publish recipes to all clients. + +To add your own recipe to the repository: +- If you already uploaded the recipe to GitHub: + 1. Fork https://github.com/getferdi/recipes and clone it to your computer + 2. Open a terminal in the `scripts/` folder of that repository + 3. Run `yarn install` to install all dependencies + 4. Run `yarn github [GitHub URL]`, e.g. `yarn github https://github.com/vantezzen/franz-recipe-standardnotes`, to add your new recipe + 5. Create a PR to with your new changes + +- If you don't have it uploaded it GitHub: + ℹ️ We recommend uploading your recipes to GitHub before adding them to provide a platform for users to report issues. + 1. Fork https://github.com/getferdi/recipes and clone it to your computer + 2. Copy your recipe files into `scripts/recipe_src` + 3. Open a terminal at `scripts/` + 4. Run `yarn install` to install all dependencies + 5. Run `yarn package` to package your new recipe + 6. Create a PR to with your new changes -- cgit v1.2.3-54-g00ecf