├── client └── acp │ ├── model │ ├── constants.js │ ├── selectors.js │ ├── action-types.js │ ├── socket-methods.js │ ├── reducers.js │ └── store.js │ ├── .eslintrc │ ├── util │ ├── is-rule-valid.js │ └── get-sanitized-name.js │ ├── controller │ ├── change-rule-field.js │ ├── change-new-rule-field.js │ └── actions.js │ ├── package.json │ ├── webpack.config.js │ ├── view │ ├── utils.js │ ├── rule-details.js │ ├── rule-create.js │ ├── admin.js │ ├── form-actions.js │ ├── rules.js │ └── rule-form.js │ ├── index.js │ ├── service │ └── socket-service.js │ └── package-lock.json ├── screenshot.png ├── public ├── templates │ └── admin │ │ └── plugins │ │ └── embed.tpl ├── css │ └── acp.css └── js │ └── acp.js.LICENSE.txt ├── plugin ├── constants.js ├── utils.js ├── logger.js ├── filters.js ├── index.js ├── sockets.js ├── rules.js ├── nodebb.js ├── database.js └── controller.js ├── style ├── _vars.scss ├── forum.scss ├── main.scss └── _palette.scss ├── .github ├── workflows │ └── nodejs.yml └── FUNDING.yml ├── LICENSE ├── package.json ├── README.md ├── plugin.json ├── .gitignore ├── CHANGELOG.md ├── data └── default-rules.json ├── docs └── community-rules.md └── test └── regexRules.spec.js /client/acp/model/constants.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_RULE_ACTION = 'createRule'; 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NicolasSiver/nodebb-plugin-ns-embed/HEAD/screenshot.png -------------------------------------------------------------------------------- /public/templates/admin/plugins/embed.tpl: -------------------------------------------------------------------------------- 1 | 2 |
-------------------------------------------------------------------------------- /plugin/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = Object.freeze({ 2 | 'COUNTER' : 'nextNsEmbedRule', 3 | 'NAMESPACE' : 'ns:embed', 4 | 'SOCKET_NAMESPACE': 'ns-embed' 5 | }); 6 | -------------------------------------------------------------------------------- /client/acp/model/selectors.js: -------------------------------------------------------------------------------- 1 | export const getNewRule = state => state.newRule; 2 | 3 | export const getRules = state => state.rules; 4 | 5 | export const getSelectedRule = state => state.selectedRule; 6 | -------------------------------------------------------------------------------- /style/_vars.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | $rhythm: 8px; 4 | 5 | $margin-xs: math.div($rhythm, 2); 6 | $margin-s: $rhythm; 7 | $margin: $rhythm * 2; 8 | $margin-l: $rhythm * 3; 9 | $margin-xl: $rhythm * 4; -------------------------------------------------------------------------------- /client/acp/model/action-types.js: -------------------------------------------------------------------------------- 1 | export const NEW_RULE_DID_CHANGE = 'newRuleDidChange'; 2 | 3 | export const RULES_DID_CHANGE = 'rulesDidChange'; 4 | 5 | export const SELECTED_RULE_DID_CHANGE = 'selectedRuleDidChange'; 6 | -------------------------------------------------------------------------------- /client/acp/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "rules": { 6 | "no-multi-spaces": 0, 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ] 11 | }, 12 | "ecmaFeatures": { 13 | "modules": true, 14 | "jsx": true 15 | } 16 | } -------------------------------------------------------------------------------- /client/acp/util/is-rule-valid.js: -------------------------------------------------------------------------------- 1 | export function isRuleValid(...fields) { 2 | for (let field of fields) { 3 | if (field === null || field === undefined || typeof field === 'string' && field.length === 0) { 4 | return false; 5 | } 6 | } 7 | 8 | return true; 9 | } 10 | -------------------------------------------------------------------------------- /client/acp/util/get-sanitized-name.js: -------------------------------------------------------------------------------- 1 | const specialChars = /[^\w]/gi; 2 | 3 | export function getSanitizedName(value) { 4 | let result = value; 5 | 6 | if (value !== null && value !== undefined) { 7 | value = value.toLowerCase(); 8 | value = value.replace(specialChars, ''); 9 | result = value; 10 | } 11 | 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /client/acp/model/socket-methods.js: -------------------------------------------------------------------------------- 1 | export const CREATE_RULE = 'admin.plugins.ns-embed.ruleCreate'; 2 | 3 | export const DELETE_RULE = 'admin.plugins.ns-embed.ruleDelete'; 4 | 5 | export const GET_ALL_RULES = 'admin.plugins.ns-embed.embedRulesGet'; 6 | 7 | export const INSTALL_DEFAULT_RULES = 'admin.plugins.ns-embed.defaultRulesInstall'; 8 | 9 | export const SAVE_RULE = 'admin.plugins.ns-embed.ruleSave'; 10 | -------------------------------------------------------------------------------- /client/acp/controller/change-rule-field.js: -------------------------------------------------------------------------------- 1 | import {setRules} from './actions'; 2 | import {getRules} from '../model/selectors'; 3 | 4 | export function changeRuleField(ruleSelected, field, value, store) { 5 | let rules = getRules(store.getState()).slice(); 6 | let editRule = rules.find(rule => rule.rid === ruleSelected.rid); 7 | 8 | editRule[field] = value; 9 | 10 | store.dispatch(setRules(rules)); 11 | } 12 | -------------------------------------------------------------------------------- /client/acp/controller/change-new-rule-field.js: -------------------------------------------------------------------------------- 1 | import {setNewRule} from './actions'; 2 | import {getSanitizedName} from '../util/get-sanitized-name'; 3 | import {getNewRule} from '../model/selectors'; 4 | 5 | export function changeNewRuleField(field, value, store) { 6 | let rule = {...getNewRule(store.getState())}; 7 | 8 | rule[field] = field === 'name' ? getSanitizedName(value) : value; 9 | 10 | store.dispatch(setNewRule(rule)); 11 | } 12 | -------------------------------------------------------------------------------- /style/forum.scss: -------------------------------------------------------------------------------- 1 | .embed-wrapper { 2 | width: 100%; 3 | max-width: 640px; 4 | overflow: hidden; 5 | 6 | &.embed-vine { 7 | max-width: 480px; 8 | 9 | .embed-container { 10 | padding-bottom: 100%; 11 | } 12 | } 13 | 14 | .embed-container { 15 | position: relative; 16 | padding-bottom: 56.25%; /* 16:9 */ 17 | height: 0; 18 | 19 | iframe { 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | width: 100%; 24 | height: 100%; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/acp/controller/actions.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../model/action-types'; 2 | 3 | export function setNewRule(rule) { 4 | return { 5 | type : ActionTypes.NEW_RULE_DID_CHANGE, 6 | payload: rule 7 | }; 8 | } 9 | 10 | export function setRules(rules) { 11 | return { 12 | type : ActionTypes.RULES_DID_CHANGE, 13 | payload: rules 14 | }; 15 | } 16 | 17 | export function setSelectedRule(rule) { 18 | return { 19 | type : ActionTypes.SELECTED_RULE_DID_CHANGE, 20 | payload: rule 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | branches: [ main, master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [14] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | cache: 'npm' 22 | - run: npm install 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: NicolasSiver 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /public/css/acp.css: -------------------------------------------------------------------------------- 1 | /** 2 | * google-material-color v1.2.6 3 | * https://github.com/danlevan/google-material-color 4 | */ 5 | .plugin-embed .actions button + button { 6 | margin-left: 8px; 7 | } 8 | .plugin-embed .rules { 9 | display: flex; 10 | flex-flow: row wrap; 11 | } 12 | .plugin-embed .rules .item { 13 | cursor: pointer; 14 | border-radius: 4px; 15 | border: 1px solid #EEEEEE; 16 | width: 160px; 17 | padding: 16px; 18 | margin: 4px; 19 | flex: 0 1 auto; 20 | } 21 | .plugin-embed .rules .item.selected { 22 | border: 1px solid #1976D2; 23 | background-color: #2196F3; 24 | color: #ffffff; 25 | } 26 | .plugin-embed .utils-info { 27 | font-size: 0.8em; 28 | line-height: 1.2em; 29 | color: #455A64; 30 | } 31 | -------------------------------------------------------------------------------- /plugin/utils.js: -------------------------------------------------------------------------------- 1 | function isInList(field, value, list) { 2 | let i, listItem; 3 | let result = false; 4 | let len = list.length; 5 | 6 | for (i = 0; i < len; ++i) { 7 | listItem = list[i]; 8 | 9 | if (listItem[field] === value) { 10 | result = true; 11 | break; 12 | } 13 | } 14 | 15 | return result; 16 | } 17 | 18 | function payloadToRule(payload) { 19 | let rule = {}; 20 | 21 | // TODO Validation? 22 | 23 | rule.name = payload.name; 24 | rule.displayName = payload.displayName; 25 | rule.regex = payload.regex; 26 | rule.replacement = payload.replacement; 27 | rule.icon = payload.icon || 'fa-cogs'; 28 | 29 | return rule; 30 | } 31 | 32 | module.exports = {isInList, payloadToRule}; -------------------------------------------------------------------------------- /plugin/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Nicolas on 10/25/15. 3 | */ 4 | (function (Module) { 5 | 'use strict'; 6 | 7 | var winston = require('winston'); 8 | 9 | Module.exports = new (winston.Logger)({ 10 | transports: [ 11 | new (winston.transports.Console)({ 12 | colorize : true, 13 | timestamp: function () { 14 | var date = new Date(); 15 | return date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0, 5) + ' [' + global.process.pid + ']'; 16 | }, 17 | level : global.env === 'production' ? 'info' : 'verbose', 18 | label : 'plugins/embed' 19 | }) 20 | ] 21 | }); 22 | 23 | })(module); -------------------------------------------------------------------------------- /client/acp/model/reducers.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './action-types'; 2 | 3 | export function newRule(state, action) { 4 | switch (action.type) { 5 | case ActionTypes.NEW_RULE_DID_CHANGE: 6 | return action.payload; 7 | default: 8 | return state; 9 | } 10 | } 11 | 12 | export function rules(state, action) { 13 | switch (action.type) { 14 | case ActionTypes.RULES_DID_CHANGE: 15 | return action.payload; 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export function selectedRule(state, action) { 22 | switch (action.type) { 23 | case ActionTypes.SELECTED_RULE_DID_CHANGE: 24 | return action.payload; 25 | default: 26 | return state; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /style/main.scss: -------------------------------------------------------------------------------- 1 | @import "palette"; 2 | @import "vars"; 3 | 4 | .plugin-embed { 5 | .actions { 6 | button + button { 7 | margin-left: $margin-s; 8 | } 9 | } 10 | 11 | .rules { 12 | display: flex; 13 | flex-flow: row wrap; 14 | 15 | .item { 16 | cursor: pointer; 17 | border-radius: $margin-xs; 18 | border: 1px solid palette(Grey, 200); 19 | width: 160px; 20 | padding: $margin; 21 | margin: $margin-xs; 22 | flex: 0 1 auto; 23 | 24 | &.selected { 25 | border: 1px solid palette(Blue, 700); 26 | background-color: palette(Blue, 500); 27 | color: palette(White, 500); 28 | } 29 | 30 | } 31 | } 32 | 33 | .utils-info { 34 | font-size: 0.8em; 35 | line-height: 1.2em; 36 | color: palette(Blue Grey, 700); 37 | } 38 | } -------------------------------------------------------------------------------- /client/acp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "embed-acp", 3 | "version": "2.1.0", 4 | "description": "ACP for Embed Plugin - control available embeds, edit rules, etc.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --env production", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "watch": "webpack watch" 10 | }, 11 | "keywords": [ 12 | "acp", 13 | "ux", 14 | "ui" 15 | ], 16 | "author": "Nicolas Siver", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@babel/core": "^7.14.3", 20 | "@babel/preset-react": "^7.13.13", 21 | "babel-loader": "^8.2.2", 22 | "webpack": "^5.38.1", 23 | "webpack-cli": "^4.7.0" 24 | }, 25 | "dependencies": { 26 | "classnames": "^2.2.3", 27 | "react": "^17.0.2", 28 | "react-dom": "^17.0.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/acp/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = env => { 4 | return { 5 | entry : "./index.js", 6 | mode : env.production === true ? 'production' : 'development', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.jsx?$/, 11 | use : { 12 | loader : 'babel-loader', 13 | options: { 14 | exclude: /node_modules/, 15 | presets: ['@babel/preset-react'] 16 | } 17 | } 18 | } 19 | ] 20 | }, 21 | output: { 22 | path : path.resolve(__dirname, '../../public/js'), 23 | filename : "acp.js", 24 | libraryTarget: "amd", 25 | library : "admin/plugins/embed" 26 | }, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /client/acp/view/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Utils = props => { 4 | return ( 5 |
6 |
Utils
7 |
8 |

9 | Install the rules shipped with the plugin, such as YouTube and Vimeo. 10 | Please, check the plugin documentation for the full list. 11 | If a rule with the same name is installed already, the rule from the default pack will be skipped. 12 |

13 | 14 | 20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /client/acp/view/rule-details.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {FormActions} from './form-actions'; 4 | import {RuleForm} from './rule-form'; 5 | 6 | export const RuleDetails = props => { 7 | let name = 'Rule: ' + props.rule.displayName; 8 | 9 | return ( 10 |
11 |
{name}
12 |
13 | props.fieldWillChange(props.rule, property, value)} 15 | {...props}/> 16 | 17 | props.ruleWillDelete(props.rule)} 20 | dangerValid={true} 21 | okButton="Save" 22 | okButtonClick={() => props.ruleWillSave(props.rule)} 23 | okValid={true}/> 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /public/js/acp.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2016 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /** @license React v0.20.2 14 | * scheduler.production.min.js 15 | * 16 | * Copyright (c) Facebook, Inc. and its affiliates. 17 | * 18 | * This source code is licensed under the MIT license found in the 19 | * LICENSE file in the root directory of this source tree. 20 | */ 21 | 22 | /** @license React v17.0.2 23 | * react-dom.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** @license React v17.0.2 32 | * react.production.min.js 33 | * 34 | * Copyright (c) Facebook, Inc. and its affiliates. 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ 39 | -------------------------------------------------------------------------------- /client/acp/view/rule-create.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {FormActions} from './form-actions'; 4 | import {isRuleValid} from '../util/is-rule-valid'; 5 | import {RuleForm} from './rule-form'; 6 | 7 | export const RuleCreate = props => { 8 | let valid = isRuleValid(props.rule.name, props.rule.displayName, props.rule.regex, props.rule.replacement); 9 | 10 | return ( 11 |
12 |
Create Rule
13 |
14 | props.newRuleFieldWillChange(property, value)} 16 | {...props}/> 17 | 18 | props.ruleWillCreate()} 21 | okValid={valid} 22 | warningButton="Reset" 23 | warningButtonClick={() => props.ruleWillReset()} 24 | warningValid={true}/> 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nicolas Siver 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 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodebb-plugin-ns-embed", 3 | "version": "7.0.0", 4 | "description": "Embed media and rich content in posts: youtube, vimeo, twitch etc. All embeds are based on the rules. You are encouraged to build your own rules to embed everything what is embeddable.", 5 | "main": "./plugin/index.js", 6 | "scripts": { 7 | "compile-acp-styles": "sass --no-source-map ./style/main.scss ./public/css/acp.css", 8 | "test": "mocha --recursive" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/NicolasSiver/nodebb-plugin-ns-embed.git" 13 | }, 14 | "keywords": [ 15 | "embed", 16 | "youtube", 17 | "vimeo", 18 | "video", 19 | "audio", 20 | "ria" 21 | ], 22 | "author": "Nicolas Siver", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/NicolasSiver/nodebb-plugin-ns-embed/issues" 26 | }, 27 | "homepage": "https://github.com/NicolasSiver/nodebb-plugin-ns-embed#readme", 28 | "dependencies": { 29 | "async": "^3.1.0", 30 | "winston": "^2.3.1" 31 | }, 32 | "devDependencies": { 33 | "chai": "^4.3.4", 34 | "mocha": "^8.4.0", 35 | "sass": "^1.34.0" 36 | }, 37 | "nbbpm": { 38 | "compatibility": "^3.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /plugin/filters.js: -------------------------------------------------------------------------------- 1 | (function (Filters) { 2 | 'use strict'; 3 | 4 | let controller = require('./controller'); 5 | 6 | Filters.adminHeaderBuild = function (header, callback) { 7 | header.plugins.push({ 8 | route: '/plugins/embed', 9 | icon : 'fa-share-alt', 10 | name : 'Embed' 11 | }); 12 | callback(null, header); 13 | }; 14 | 15 | Filters.adminScripts = function (list, callback) { 16 | list.push('https://checkout.stripe.com/checkout.js'); 17 | callback(null, list); 18 | }; 19 | 20 | Filters.parsePost = function (payload, callback) { 21 | controller.parsePost(payload, callback); 22 | }; 23 | 24 | Filters.parseRaw = function (payload, callback) { 25 | controller.parseContent(payload, callback); 26 | }; 27 | 28 | // Full list of the attributes: https://github.com/NodeBB/NodeBB/blob/21c992242e1219c8d726ddc5b3b661adc9fd44c2/src/posts/parse.js#L21 29 | Filters.sanitizeConfig = function (payload, callback) { 30 | let iframeConfig = payload.allowedAttributes.iframe; 31 | 32 | iframeConfig.push('allowfullscreen'); 33 | iframeConfig.push('frameborder'); 34 | callback(null, payload); 35 | }; 36 | 37 | })(module.exports); 38 | -------------------------------------------------------------------------------- /client/acp/view/admin.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | 3 | import * as Constants from '../model/constants'; 4 | import {RuleCreate} from './rule-create'; 5 | import {RuleDetails} from './rule-details'; 6 | import {Rules} from './rules'; 7 | import {getNewRule, getSelectedRule} from '../model/selectors'; 8 | import {StoreContext} from '../model/store'; 9 | import {Utils} from './utils'; 10 | 11 | export const Admin = props => { 12 | let {store} = useContext(StoreContext); 13 | 14 | function renderExtendedView() { 15 | let view = null; 16 | let state = store.getState(); 17 | let selectedRule = getSelectedRule(state); 18 | 19 | if (selectedRule !== null) { 20 | if (selectedRule.name === Constants.DEFAULT_RULE_ACTION) { 21 | view = ; 22 | } else { 23 | view = ; 24 | } 25 | } 26 | 27 | return view; 28 | } 29 | 30 | return ( 31 |
32 |
33 | 34 | 35 |
36 |
37 | {renderExtendedView()} 38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /client/acp/view/form-actions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const FormActions = props => { 4 | let okButton = (props.okButton) ? createButton( 5 | props.okButton, 6 | 'btn btn-primary', 7 | props.okValid, 8 | props.okButtonClick 9 | ) : null; 10 | let warningButton = (props.warningButton) ? createButton( 11 | props.warningButton, 12 | 'btn btn-warning', 13 | props.warningValid, 14 | props.warningButtonClick 15 | ) : null; 16 | let dangerButton = (props.dangerButton) ? createButton( 17 | props.dangerButton, 18 | 'btn btn-danger', 19 | props.dangerValid, 20 | props.dangerButtonClick 21 | ) : null; 22 | 23 | function createButton(text, style, valid, callback) { 24 | callback = callback || (event => { 25 | console.warn('Action Callback is not provided'); 26 | }); 27 | return ( 28 | 35 | ); 36 | } 37 | 38 | return ( 39 |
40 | {okButton} 41 | {warningButton} 42 | {dangerButton} 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /plugin/index.js: -------------------------------------------------------------------------------- 1 | (function (Plugin) { 2 | 'use strict'; 3 | 4 | var async = require('async'), 5 | 6 | filters = require('./filters'), 7 | rules = require('./rules'), 8 | sockets = require('./sockets'); 9 | 10 | //NodeBB list of Hooks: https://github.com/NodeBB/NodeBB/wiki/Hooks 11 | Plugin.hooks = { 12 | filters: filters, 13 | statics: { 14 | load: function (params, callback) { 15 | var router = params.router, 16 | middleware = params.middleware, 17 | controllers = params.controllers, 18 | pluginUri = '/admin/plugins/embed', 19 | apiUri = '/api' + pluginUri, 20 | renderAdmin = function (req, res, next) { 21 | res.render( 22 | 'admin/plugins/embed', { 23 | title:'Embed', 24 | } 25 | ); 26 | }; 27 | 28 | router.get(pluginUri, middleware.admin.buildHeader, renderAdmin); 29 | router.get(apiUri, renderAdmin); 30 | 31 | async.series([ 32 | async.apply(sockets.init), 33 | async.apply(rules.invalidate) 34 | ], callback); 35 | } 36 | } 37 | }; 38 | 39 | })(module.exports); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeBB Embed 2 | 3 | Embed media and rich content in posts: youtube, vimeo, twitch etc. All embeds are based on the rules. You are encouraged to build your own rules to embed everything what is embeddable. 4 | 5 | ![Version](https://img.shields.io/npm/v/nodebb-plugin-ns-embed.svg) 6 | ![Dependencies](https://img.shields.io/librariesio/github/NicolasSiver/nodebb-plugin-ns-embed) 7 | ![GitHub Actions](https://github.com/NicolasSiver/nodebb-plugin-ns-embed/actions/workflows/nodejs.yml/badge.svg) 8 | 9 | ## Table of Contents 10 | 11 | 12 | 13 | 14 | 15 | - [Embeds Supported by Default](#embeds-supported-by-default) 16 | - [Screenshots](#screenshots) 17 | 18 | 19 | 20 | The plugin works well with default `Markdown` plugin or without it, i.e. there is no alteration of links in user's posts. 21 | 22 | ## Embeds Supported by Default 23 | 24 | Most embeds are responsive with a limited width to `640px` 25 | 26 | - Coub 27 | - Twitch (video and channel) 28 | - Vimeo 29 | - Vine 30 | - Youtube (short and normal URL) 31 | 32 | If you would like to have more embeddable services, please refer to the [Community Embed Rules](docs/community-rules.md). 33 | 34 | ## Screenshots 35 | 36 | ![Admin Panel View](screenshot.png) -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "nodebb-plugin-ns-embed", 3 | "name": "NodeBB Embed", 4 | "description": "Embed media and rich content in posts: Youtube, Vimeo, Twitch etc. All embeds are based on the rules. You are encouraged to build your own rules to embed everything what is embeddable.", 5 | "url": "https://github.com/NicolasSiver/nodebb-plugin-ns-embed", 6 | "library": "./plugin/index.js", 7 | "hooks": [ 8 | { 9 | "hook": "filter:admin.header.build", 10 | "method": "hooks.filters.adminHeaderBuild" 11 | }, 12 | { 13 | "hook": "filter:admin.scripts.get", 14 | "method": "hooks.filters.adminScripts" 15 | }, 16 | { 17 | "hook": "static:app.load", 18 | "method": "hooks.statics.load" 19 | }, 20 | { 21 | "hook": "filter:parse.post", 22 | "method": "hooks.filters.parsePost", 23 | "priority": 8 24 | }, 25 | { 26 | "hook": "filter:parse.raw", 27 | "method": "hooks.filters.parseRaw" 28 | }, 29 | { 30 | "hook": "filter:sanitize.config", 31 | "method": "hooks.filters.sanitizeConfig" 32 | } 33 | ], 34 | "scss": [ 35 | "style/forum.scss" 36 | ], 37 | "modules": { 38 | "../admin/plugins/embed.js": "./public/js/acp.js" 39 | }, 40 | "scripts": [], 41 | "staticDirs": { 42 | "acp": "./client/acp", 43 | "css": "./public/css", 44 | "js": "./public/js" 45 | }, 46 | "templates": "./public/templates" 47 | } 48 | -------------------------------------------------------------------------------- /client/acp/model/store.js: -------------------------------------------------------------------------------- 1 | import React, {createContext, useReducer} from 'react'; 2 | 3 | import {newRule, rules, selectedRule} from './reducers'; 4 | 5 | export const StoreContext = createContext(null); 6 | 7 | export function createInitialState() { 8 | return { 9 | newRule : {}, 10 | rules : [], 11 | selectedRule: null 12 | }; 13 | } 14 | 15 | /** 16 | * Experimental Store Implementation to represent the possibility to have lightweight Store solution with centralized reducer like Redux 17 | */ 18 | export function createStore(initialState) { 19 | let state, dispatch; 20 | 21 | function invalidate() { 22 | let [currentState, dispatchRef] = useReducer((state, action) => { 23 | return { 24 | newRule : newRule(state.newRule, action), 25 | rules : rules(state.rules, action), 26 | selectedRule: selectedRule(state.selectedRule, action) 27 | }; 28 | }, initialState); 29 | 30 | state = currentState; 31 | dispatch = dispatchRef; 32 | } 33 | 34 | return { 35 | dispatch : action => dispatch(action), 36 | getState : () => state, 37 | invalidate: () => invalidate() 38 | }; 39 | } 40 | 41 | export function createStoreProvider(store) { 42 | return ({children}) => { 43 | store.invalidate(); 44 | 45 | // A component calling useContext will always re-render when the context value changes. 46 | return {children}; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /client/acp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | 4 | import { setNewRule } from './controller/actions'; 5 | import { Admin } from './view/admin'; 6 | import { changeNewRuleField } from './controller/change-new-rule-field'; 7 | import { changeRuleField } from './controller/change-rule-field'; 8 | import { getNewRule } from './model/selectors'; 9 | import { SocketService } from './service/socket-service'; 10 | import { createInitialState, createStore, createStoreProvider } from './model/store'; 11 | 12 | export const init = async () => { 13 | console.info('Initiate ACP: Embed'); 14 | 15 | let alerts = await window.app.require('alerts'); 16 | let store = createStore(createInitialState()); 17 | let Provider = createStoreProvider(store); 18 | let socketService = new SocketService(store, alerts); 19 | 20 | ReactDom.render( 21 | 22 | changeRuleField(rule, field, value, store)} 24 | installDefaultRules={() => socketService.installDefaultRules()} 25 | newRuleFieldWillChange={(field, value) => changeNewRuleField(field, value, store)} 26 | ruleWillCreate={() => socketService.createNewRule(getNewRule(store.getState()))} 27 | ruleWillDelete={rule => socketService.deleteRule(rule)} 28 | ruleWillReset={() => store.dispatch(setNewRule({}))} 29 | ruleWillSave={rule => socketService.saveRule(rule)} /> 30 | , 31 | document.getElementById('acpEmbedContainer') 32 | ); 33 | 34 | socketService.getAllRules(); 35 | }; 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 30 | 31 | *.iml 32 | 33 | ## Directory-based project format: 34 | .idea/ 35 | # if you remove the above rule, at least ignore the following: 36 | 37 | # User-specific stuff: 38 | # .idea/workspace.xml 39 | # .idea/tasks.xml 40 | # .idea/dictionaries 41 | # .idea/shelf 42 | 43 | # Sensitive or high-churn files: 44 | # .idea/dataSources.ids 45 | # .idea/dataSources.xml 46 | # .idea/sqlDataSources.xml 47 | # .idea/dynamic.xml 48 | # .idea/uiDesigner.xml 49 | 50 | # Gradle: 51 | # .idea/gradle.xml 52 | # .idea/libraries 53 | 54 | # Mongo Explorer plugin: 55 | # .idea/mongoSettings.xml 56 | 57 | ## File-based project format: 58 | *.ipr 59 | *.iws 60 | 61 | ## Plugin-specific files: 62 | 63 | # IntelliJ 64 | /out/ 65 | 66 | # mpeltonen/sbt-idea plugin 67 | .idea_modules/ 68 | 69 | # JIRA plugin 70 | atlassian-ide-plugin.xml 71 | 72 | # Crashlytics plugin (for Android Studio and IntelliJ) 73 | com_crashlytics_export_strings.xml 74 | crashlytics.properties 75 | crashlytics-build.properties 76 | fabric.properties -------------------------------------------------------------------------------- /client/acp/view/rules.js: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, {useContext} from 'react'; 3 | 4 | import {setSelectedRule} from '../controller/actions'; 5 | import * as Constants from '../model/constants'; 6 | import {getSelectedRule, getRules} from '../model/selectors'; 7 | import {StoreContext} from '../model/store'; 8 | 9 | export const Rules = () => { 10 | let {store} = useContext(StoreContext); 11 | let state = store.getState(); 12 | let selectedRule = getSelectedRule(state); 13 | let rules = getRules(state); 14 | 15 | let RuleItem = data => { 16 | let icon = classNames('fa', data.icon || 'fa-cogs'); 17 | let item = classNames('item', { 18 | selected: selectedRule !== null && data.name === selectedRule.name 19 | }); 20 | 21 | return ( 22 |
store.dispatch(setSelectedRule(data))}> 25 | {data.displayName} 26 |
27 | ); 28 | }; 29 | 30 | function renderRules(rules) { 31 | let ruleCreate = RuleItem({ 32 | displayName: 'Create Rule', 33 | icon : 'fa-plus', 34 | name : Constants.DEFAULT_RULE_ACTION, 35 | rid : Constants.DEFAULT_RULE_ACTION 36 | }); 37 | let ruleComponents = rules.map(rule => RuleItem(rule)); 38 | 39 | return [ruleCreate, ...ruleComponents]; 40 | } 41 | 42 | return ( 43 |
44 |
Installed Rules
45 |
46 |
47 | {renderRules(rules)} 48 |
49 |
50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /plugin/sockets.js: -------------------------------------------------------------------------------- 1 | (function (Sockets) { 2 | 'use strict'; 3 | 4 | var constants = require('./constants'), 5 | controller = require('./controller'), 6 | nodebb = require('./nodebb'); 7 | 8 | var adminSockets = nodebb.adminSockets, 9 | serverSockets = nodebb.serverSockets, 10 | emitNamespace = 'admin.plugins.' + constants.SOCKET_NAMESPACE + '.'; 11 | 12 | Sockets.init = function (callback) { 13 | adminSockets[constants.SOCKET_NAMESPACE] = {}; 14 | 15 | //Acknowledgements 16 | adminSockets[constants.SOCKET_NAMESPACE].defaultRulesInstall = Sockets.defaultRulesInstall; 17 | adminSockets[constants.SOCKET_NAMESPACE].embedRulesGet = Sockets.embedRulesGet; 18 | adminSockets[constants.SOCKET_NAMESPACE].ruleCreate = Sockets.ruleCreate; 19 | adminSockets[constants.SOCKET_NAMESPACE].ruleDelete = Sockets.ruleDelete; 20 | adminSockets[constants.SOCKET_NAMESPACE].ruleSave = Sockets.ruleSave; 21 | 22 | callback(); 23 | }; 24 | 25 | Sockets.defaultRulesInstall = function (socket, payload, callback) { 26 | controller.installDefaultRules(callback); 27 | }; 28 | 29 | Sockets.embedRulesGet = function (socket, payload, callback) { 30 | controller.getAllRules(callback); 31 | }; 32 | 33 | Sockets.emit = function (eventName, payload) { 34 | serverSockets.emit(emitNamespace + eventName, payload); 35 | }; 36 | 37 | Sockets.ruleCreate = function (socket, payload, callback) { 38 | controller.createRule(payload, callback); 39 | }; 40 | 41 | Sockets.ruleDelete = function (socket, payload, callback) { 42 | controller.deleteRule(payload, callback); 43 | }; 44 | 45 | Sockets.ruleSave = function (socket, payload, callback) { 46 | controller.saveRule(payload, callback); 47 | }; 48 | 49 | })(module.exports); -------------------------------------------------------------------------------- /plugin/rules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Nicolas on 10/25/15. 3 | */ 4 | (function (Rules) { 5 | 'use strict'; 6 | 7 | var database = require('./database'), 8 | logger = require('./logger'), 9 | nodebb = require('./nodebb'); 10 | 11 | var cache = nodebb.cache; 12 | 13 | var rulesList = []; 14 | 15 | Rules.invalidate = function (done) { 16 | database.getRules(function (error, rules) { 17 | if (error) { 18 | return done(error); 19 | } 20 | 21 | logger.log('verbose', 'Updating rules...'); 22 | 23 | // Re-compile regular expressions 24 | var i, len = rules.length, rule, ruleEntity; 25 | rulesList.length = 0; 26 | for (i = 0; i < len; ++i) { 27 | rule = rules[i]; 28 | try { 29 | ruleEntity = { 30 | match : new RegExp(rule.regex, "g"), 31 | replacement: rule.replacement 32 | }; 33 | rulesList.push(ruleEntity); 34 | } catch (e) { 35 | console.error('Rule is skipped', e); 36 | } 37 | } 38 | 39 | cache.reset(); 40 | 41 | logger.log('verbose', 'Updating rule list, total rules: %d', rulesList.length); 42 | 43 | done(); 44 | }); 45 | }; 46 | 47 | Rules.parse = function (content, done) { 48 | if (content) { 49 | var i = 0, len = rulesList.length, rule; 50 | 51 | for (i; i < len; ++i) { 52 | rule = rulesList[i]; 53 | content = content.replace(rule.match, rule.replacement); 54 | } 55 | 56 | done(null, content); 57 | } else { 58 | done(null, content); 59 | } 60 | }; 61 | 62 | })(module.exports); 63 | -------------------------------------------------------------------------------- /plugin/nodebb.js: -------------------------------------------------------------------------------- 1 | (function (Module, NodeBB) { 2 | 'use strict'; 3 | 4 | Module.exports = { 5 | adminSockets : NodeBB.require('./src/socket.io/admin').plugins, 6 | cache : NodeBB.require('./src/posts/cache'), 7 | db : NodeBB.require('./src/database'), 8 | groups : NodeBB.require('./src/groups'), 9 | meta : NodeBB.require('./src/meta'), 10 | pluginSockets: NodeBB.require('./src/socket.io/plugins'), 11 | postTools : NodeBB.require('./src/posts/tools'), 12 | serverSockets: NodeBB.require('./src/socket.io').server.sockets, 13 | settings : NodeBB.require('./src/settings'), 14 | socketIndex : NodeBB.require('./src/socket.io/index'), 15 | topics : NodeBB.require('./src/topics'), 16 | user : NodeBB.require('./src/user'), 17 | 18 | utils : NodeBB.require('./src/utils'), 19 | helpers: NodeBB.require('./src/controllers/helpers'), 20 | 21 | /** 22 | * List is incomplete 23 | * 24 | * base_dir: '/path/to/NodeBB', 25 | * themes_path: '/path/to/NodeBB/node_modules', 26 | * views_dir: '/path/to/NodeBB/public/templates', 27 | * version: 'NodeBB Version', 28 | * url: 'http://localhost:4567', 29 | * core_templates_path: '/path/to/NodeBB/src/views', 30 | * base_templates_path: '/path/to/NodeBB/node_modules/nodebb-theme-vanilla/templates', 31 | * upload_path: '/public/uploads', 32 | * relative_path: '', 33 | * port: '4567', 34 | * upload_url: '/uploads/', 35 | * theme_templates_path: '/path/to/NodeBB/node_modules/nodebb-theme-lavender/templates', 36 | * theme_config: '/path/to/NodeBB/node_modules/nodebb-theme-lavender/theme.json', 37 | * NODE_ENV: 'development' 38 | */ 39 | nconf : NodeBB.require('nconf'), 40 | passport: NodeBB.require('passport'), 41 | express : NodeBB.require('express') 42 | }; 43 | })(module, require.main); 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [7.0.0] - 2025-06-11 9 | 10 | - Changed compatibility with NodeBB v3.x (kudos to Barış Soner Uşaklı) 11 | - Changed ACP to use NodeBB v3 common UI patterns (kudos to Barış Soner Uşaklı) 12 | - Changed ACP to resolve all vulnerabilities 13 | 14 | ## [6.0.0] - 2022-07-02 15 | 16 | - Changed compatibility with NodeBB v2.2.x (kudos to Barış Soner Uşaklı) 17 | - Changed ACP to use modules 18 | 19 | ## [5.0.0] - 2021-06-02 20 | 21 | - Added new notifications in ACP for manipulations around rules 22 | - Changed project dependencies to rely on fewer libraries 23 | - Changed integration with NodeBB to preserve ability to enter Fullscreen (kudos to Revir Yang) 24 | - Changed compatibility with NodeBB v1.17.x 25 | - Changed ACP to rely on latest UI library 26 | - Removed Babel for unit tests around the rules 27 | - Removed Gulp as orchestration tool for SASS styles 28 | 29 | ## [4.0.0] - 2019-12-29 30 | 31 | - Added support for post preview of the embedded content 32 | - Changed dependencies to comply with the most security updates 33 | - Changed compatibility with NodeBB v1.13.x 34 | 35 | ## [3.0.1] - 2018-12-26 36 | 37 | - Added community embed rules 38 | - Removed Code Climate integration 39 | 40 | ## [3.0.0] - 2018-11-29 41 | 42 | - Changed compatibility for NodeBB v1.11.0 43 | - Removed Emitter NodeBB dependency 44 | 45 | ## [2.1.1] - 2017-02-26 46 | 47 | - Changed Youtube default rule to ignore Youtube channels 48 | 49 | ## [2.1.0] - 2017-01-11 50 | 51 | - Added Twitch Default rule for Live content 52 | - Added Twitch Default rule for VoD 53 | 54 | ## [2.0.1] - 2016-05-02 55 | 56 | - Changed Youtube default rule to ignore Youtube profiles 57 | 58 | ## [2.0.0] - 2016-03-22 59 | 60 | - Added compatibility with Markdown plugin 61 | - Added compatibility with content plugins 62 | - Changed ACP scripts to follow best practice 63 | - Changed all dependencies 64 | 65 | ## [1.1.0] - 2015-12-16 66 | 67 | - Added ability to skip invalid regular expressions 68 | - Changed default Youtube rule 69 | 70 | ## [1.0.0] - 2015-11-29 71 | 72 | - Initial release with predefined set of embeds 73 | -------------------------------------------------------------------------------- /plugin/database.js: -------------------------------------------------------------------------------- 1 | (function (Database) { 2 | 'use strict'; 3 | 4 | const async = require('async'); 5 | 6 | const nodebb = require('./nodebb'), 7 | constants = require('./constants'); 8 | 9 | const db = nodebb.db; 10 | 11 | Database.createRule = function (data, done) { 12 | async.waterfall([ 13 | async.apply(db.incrObjectField, 'global', constants.COUNTER), 14 | function (id, next) { 15 | let createTime = Date.now(); 16 | let additionalData = { 17 | rid : id, 18 | createtime: createTime 19 | }; 20 | let ruleData = Object.assign({}, data, additionalData); 21 | 22 | async.parallel([ 23 | async.apply(db.sortedSetAdd, constants.NAMESPACE + ':rule', createTime, id), 24 | async.apply(db.setObject, constants.NAMESPACE + ':rule:' + id, ruleData) 25 | ], function (error) { 26 | if (error) { 27 | return next(error); 28 | } 29 | next(null, ruleData); 30 | }); 31 | } 32 | ], done); 33 | }; 34 | 35 | Database.deleteRule = function (id, done) { 36 | async.parallel([ 37 | async.apply(db.delete, constants.NAMESPACE + ':rule:' + id), 38 | async.apply(db.sortedSetRemove, constants.NAMESPACE + ':rule', id) 39 | ], function (error) { 40 | if (error) { 41 | return done(error); 42 | } 43 | //Filter null responses from DB delete methods 44 | done(null); 45 | }); 46 | }; 47 | 48 | Database.getRule = function (id, done) { 49 | db.getObject(constants.NAMESPACE + ':rule:' + id, done); 50 | }; 51 | 52 | Database.getRules = function (done) { 53 | async.waterfall([ 54 | async.apply(db.getSortedSetRange, constants.NAMESPACE + ':rule', 0, -1), 55 | function (ids, next) { 56 | if (!ids.length) { 57 | return next(null, ids); 58 | } 59 | db.getObjects(ids.map(function (id) { 60 | return constants.NAMESPACE + ':rule:' + id; 61 | }), next); 62 | } 63 | ], done); 64 | }; 65 | 66 | Database.updateRule = function (id, data, done) { 67 | db.setObject(constants.NAMESPACE + ':rule:' + id, data, done); 68 | }; 69 | 70 | })(module.exports); 71 | -------------------------------------------------------------------------------- /data/default-rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "name": "youtube", 5 | "displayName": "Youtube", 6 | "icon": "fa-youtube", 7 | "regex": "(?:)?", 8 | "replacement": "
" 9 | }, 10 | { 11 | "name": "vimeo", 12 | "displayName": "Vimeo", 13 | "icon": "fa-vimeo", 14 | "regex": "(?:)?", 15 | "replacement": "
" 16 | }, 17 | { 18 | "name": "vine", 19 | "displayName": "Vine", 20 | "icon": "fa-vine", 21 | "regex": "(?:)?", 22 | "replacement": "
" 23 | }, 24 | { 25 | "name": "coub", 26 | "displayName": "Coub", 27 | "icon": "fa-cube", 28 | "regex": "(?:)?", 29 | "replacement": "
" 30 | }, 31 | { 32 | "name": "twitch-live", 33 | "displayName": "Twitch Live", 34 | "icon": "fa-twitch", 35 | "regex": "(?:)?", 36 | "replacement": "
" 37 | }, 38 | { 39 | "name": "twitch-vod", 40 | "displayName": "Twitch VoD", 41 | "icon": "fa-twitch", 42 | "regex": "(?:)?", 43 | "replacement": "
" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /client/acp/service/socket-service.js: -------------------------------------------------------------------------------- 1 | import {setNewRule, setRules, setSelectedRule} from '../controller/actions'; 2 | import * as SocketMethods from '../model/socket-methods'; 3 | 4 | export class SocketService { 5 | constructor(store, alerts) { 6 | this.store = store; 7 | this.alerts = alerts; 8 | } 9 | 10 | createNewRule(rule) { 11 | window.socket.emit( 12 | SocketMethods.CREATE_RULE, 13 | rule, 14 | (error, rule) => { 15 | if (error) { 16 | return this.alerts.error(error.message); 17 | } 18 | 19 | this.alerts.success('Rule "' + rule.displayName + '" has been created'); 20 | this.store.dispatch(setNewRule({})); 21 | this.getAllRules(); 22 | } 23 | ); 24 | } 25 | 26 | deleteRule(rule) { 27 | window.socket.emit( 28 | SocketMethods.DELETE_RULE, 29 | rule, 30 | (error, rule) => { 31 | if (error) { 32 | return this.alerts.error(error.message); 33 | } 34 | 35 | this.alerts.success('Rule "' + rule.displayName + '" is deleted'); 36 | this.store.dispatch(setSelectedRule(null)); 37 | this.getAllRules(); 38 | } 39 | ); 40 | } 41 | 42 | getAllRules() { 43 | window.socket.emit( 44 | SocketMethods.GET_ALL_RULES, 45 | {}, 46 | (error, rules) => { 47 | if (error) { 48 | return this.alerts.error(error.message); 49 | } 50 | 51 | this.store.dispatch(setRules(rules)); 52 | } 53 | ); 54 | } 55 | 56 | installDefaultRules() { 57 | console.info('Installing Default rules...'); 58 | 59 | window.socket.emit( 60 | SocketMethods.INSTALL_DEFAULT_RULES, 61 | {}, 62 | (error, installedRules) => { 63 | if (error) { 64 | return this.alerts.error(error.message); 65 | } 66 | 67 | this.alerts.success('Installed rules: ' + installedRules.join(', ')); 68 | this.getAllRules(); 69 | } 70 | ); 71 | } 72 | 73 | saveRule(rule) { 74 | window.socket.emit( 75 | SocketMethods.SAVE_RULE, 76 | rule, 77 | (error, rule) => { 78 | if (error) { 79 | return this.alerts.error(error.message); 80 | } 81 | 82 | this.alerts.success('Rule "' + rule.displayName + '" is updated'); 83 | this.getAllRules(); 84 | } 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /client/acp/view/rule-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const RuleForm = props => { 4 | return ( 5 |
6 |
7 |
8 |
9 | 10 | props.propDidChange('name', event.target.value)} 15 | value={props.rule.name || ''} 16 | placeholder="name (Ex: youtube)"/> 17 |
18 |
19 |
20 |
21 | 22 | props.propDidChange('displayName', event.target.value)} 27 | value={props.rule.displayName || ''} 28 | placeholder="Display Name (Ex: Youtube)"/> 29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 | props.propDidChange('regex', event.target.value)} 42 | value={props.rule.regex || ''} 43 | placeholder="Regular expression"/> 44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 | 52 |