├── LICENSE ├── README.md ├── composer.json ├── extend.php ├── js ├── admin.js ├── dist │ ├── admin.js │ └── admin.js.map ├── package-lock.json ├── package.json ├── tsconfig.json └── webpack.config.js ├── migrations ├── 2020_12_24_create_webhooks_table.php ├── 2020_12_25_migrate_discord_webhooks.php ├── 2020_12_26_add_extra_text_column.php ├── 2020_12_27_add_tag_column.php ├── 2021_02_07_add_max_post_content_length_column.php ├── 2021_02_07_add_use_plain_text_column.php ├── 2023_06_07_000000_remove_tag_id_constraint.php ├── 2023_06_07_change_tag_id_to_json.php ├── 2024_09_12_add_include_tags_columns.php ├── 2024_09_12_add_name_column.php └── 2024_12_30_increase_webhook_url_length.php ├── phpstan.neon ├── resources ├── less │ └── admin.less └── locale │ └── en.yml └── src ├── Action.php ├── Actions ├── Discussion │ ├── Action.php │ ├── Deleted.php │ ├── Hidden.php │ ├── Renamed.php │ ├── Restored.php │ └── Started.php ├── Group │ ├── Created.php │ ├── Deleted.php │ └── Renamed.php ├── Post │ ├── Action.php │ ├── Approved.php │ ├── Deleted.php │ ├── Hidden.php │ ├── Posted.php │ ├── Restored.php │ └── Revised.php └── User │ ├── Deleted.php │ ├── Registered.php │ └── Renamed.php ├── Adapters ├── Adapter.php ├── Adapters.php ├── Discord │ ├── Adapter.php │ └── DiscordException.php ├── MicrosoftTeams │ ├── Adapter.php │ └── TeamsException.php └── Slack │ ├── Adapter.php │ └── SlackException.php ├── Api ├── Controller │ ├── CreateWebhookController.php │ ├── DeleteWebhookController.php │ ├── ListWebhooksController.php │ └── UpdateWebhookController.php └── Serializer │ └── WebhookSerializer.php ├── Command ├── CreateWebhook.php ├── CreateWebhookHandler.php ├── DeleteWebhook.php ├── DeleteWebhookHandler.php ├── UpdateWebhook.php └── UpdateWebhookHandler.php ├── Extend └── FoFWebhooksExtender.php ├── Helpers └── Post.php ├── Jobs └── HandleEvent.php ├── Legacy └── Extend │ └── ReflarWebhooksExtender.php ├── Listener └── TriggerListener.php ├── Models └── Webhook.php ├── Response.php └── Validator └── WebhookValidator.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 FriendsOfFlarum 4 | Copyright (c) 2019 ReFlar 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FriendsOfFlarum Webhooks 2 | 3 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) [![Latest Stable Version](https://img.shields.io/packagist/v/fof/webhooks.svg)](https://packagist.org/packages/fof/webhooks) [![OpenCollective](https://img.shields.io/badge/opencollective-fof-blue.svg)](https://opencollective.com/fof/donate) [![Donate](https://img.shields.io/badge/donate-datitisev-important.svg)](https://datitisev.me/donate) 4 | 5 | A [Flarum](http://flarum.org) extension. Flarum with webhooks. 6 | 7 | ### Installation 8 | 9 | Install manually with composer: 10 | 11 | ```sh 12 | composer require fof/webhooks 13 | ``` 14 | 15 | ### Updating 16 | 17 | ```sh 18 | composer update fof/webhooks 19 | ``` 20 | 21 | ### Links 22 | 23 | [![OpenCollective](https://img.shields.io/badge/donate-friendsofflarum-44AEE5?style=for-the-badge&logo=open-collective)](https://opencollective.com/fof/donate) [![GitHub](https://img.shields.io/badge/donate-datitisev-ea4aaa?style=for-the-badge&logo=github)](https://datitisev.me/donate/github) 24 | 25 | - [Packagist](https://packagist.org/packages/fof/webhooks) 26 | - [GitHub](https://github.com/friendsofflarum/webhooks) 27 | 28 | An extension by [FriendsOfFlarum](https://github.com/FriendsOfFlarum). 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fof/webhooks", 3 | "description": "Flarum with webhooks", 4 | "keywords": [ 5 | "flarum" 6 | ], 7 | "type": "flarum-extension", 8 | "license": "MIT", 9 | "support": { 10 | "issues": "https://github.com/FriendsOfFlarum/webhooks/issues", 11 | "source": "https://github.com/FriendsOfFlarum/webhooks", 12 | "forum": "https://discuss.flarum.org/d/17812" 13 | }, 14 | "homepage": "https://friendsofflarum.org", 15 | "funding": [ 16 | { 17 | "type": "opencollective", 18 | "url": "https://opencollective.com/fof/donate" 19 | }, 20 | { 21 | "type": "github", 22 | "url": "https://github.com/sponsors/datitisev" 23 | } 24 | ], 25 | "require": { 26 | "flarum/core": "^1.7.0", 27 | "guzzlehttp/guzzle": "7.*", 28 | "ssnepenthe/color-utils": "^0.4.2", 29 | "html2text/html2text": "^4.3.1", 30 | "charescape/serialize-closure": "^3.8" 31 | }, 32 | "replace": { 33 | "reflar/webhooks": "*" 34 | }, 35 | "authors": [ 36 | { 37 | "name": "David Sevilla Martín", 38 | "email": "me+fof@datitisev.me", 39 | "role": "Developer" 40 | } 41 | ], 42 | "autoload": { 43 | "psr-4": { 44 | "FoF\\Webhooks\\": "src/", 45 | "Reflar\\Webhooks\\": "src/Legacy" 46 | } 47 | }, 48 | "extra": { 49 | "flarum-extension": { 50 | "title": "FoF Webhooks", 51 | "category": "feature", 52 | "icon": { 53 | "name": "fas fa-external-link-alt", 54 | "backgroundColor": "#e74c3c", 55 | "color": "#fff" 56 | }, 57 | "optional-dependencies": [ 58 | "flarum/tags", 59 | "flarum/approval" 60 | ] 61 | }, 62 | "flagrow": { 63 | "discuss": "https://discuss.flarum.org/d/17812" 64 | }, 65 | "flarum-cli": { 66 | "modules": { 67 | "githubActions": true 68 | } 69 | } 70 | }, 71 | "scripts": { 72 | "analyse:phpstan": "phpstan analyse", 73 | "clear-cache:phpstan": "phpstan clear-result-cache" 74 | }, 75 | "scripts-descriptions": { 76 | "analyse:phpstan": "Run static analysis" 77 | }, 78 | "require-dev": { 79 | "flarum/phpstan": "*", 80 | "flarum/tags": "*", 81 | "flarum/approval": "*" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /extend.php: -------------------------------------------------------------------------------- 1 | js(__DIR__.'/js/dist/admin.js') 24 | ->css(__DIR__.'/resources/less/admin.less') 25 | ->content(function (Document $document) { 26 | $document->payload['fof-webhooks.services'] = array_keys(Adapters::all()); 27 | $document->payload['fof-webhooks.events'] = array_keys((array) TriggerListener::$listeners); 28 | }), 29 | 30 | new Extend\Locales(__DIR__.'/resources/locale'), 31 | 32 | (new Extend\Routes('api')) 33 | ->get('/fof/webhooks', 'fof.webhooks.index', Api\Controller\ListWebhooksController::class) 34 | ->post('/fof/webhooks', 'fof.webhooks.create', Api\Controller\CreateWebhookController::class) 35 | ->patch('/fof/webhooks/{id}', 'fof.webhooks.update', Api\Controller\UpdateWebhookController::class) 36 | ->delete('/fof/webhooks/{id}', 'fof.webhooks.delete', Api\Controller\DeleteWebhookController::class), 37 | 38 | (new Extend\ApiSerializer(ForumSerializer::class)) 39 | ->hasMany('webhooks', WebhookSerializer::class), 40 | 41 | (new Extend\Event()) 42 | ->subscribe(Listener\TriggerListener::class), 43 | ]; 44 | -------------------------------------------------------------------------------- /js/admin.js: -------------------------------------------------------------------------------- 1 | export * from './src/admin'; 2 | -------------------------------------------------------------------------------- /js/dist/admin.js: -------------------------------------------------------------------------------- 1 | (()=>{var t={n:e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return t.d(o,{a:o}),o},d:(e,o)=>{for(var n in o)t.o(o,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:o[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)};(()=>{"use strict";const e=flarum.core.compat["admin/app"];var o=t.n(e);const n=flarum.core.compat["common/Model"];var s=t.n(n);const a=flarum.core.compat["common/models/Forum"];var r=t.n(a);function i(t,e){return i=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},i(t,e)}function l(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,i(t,e)}var h=function(t){function e(){for(var e,o=arguments.length,n=new Array(o),a=0;an?1:0})).map((function(t){var o=t[0],n=t[1];return n.length?m("div",null,m("h3",null,e.translate(o)),n.map((function(t){return m(D(),{state:e.webhook.events().includes(t.full),onchange:e.onchange.bind(e,t.full)},e.translate(o,t.name.toLowerCase()))}))):null})))}))),m("div",{className:"Form-group"},m(k(),{type:"submit",className:"Button Button--primary",loading:this.loading,disabled:!this.isDirty()},o().translator.trans("core.admin.settings.submit_button")))))},n.translate=function(t,e){return void 0===e&&(e="title"),o().translator.trans("fof-webhooks.admin.settings.actions."+t+"."+e)},n.isDirty=function(){return this.extraText()!=this.webhook.extraText()||this.groupId()!==this.webhook.groupId()||this.usePlainText()!==this.webhook.usePlainText()||this.includeTags()!==this.webhook.includeTags()||this.maxPostContentLength()!=this.webhook.maxPostContentLength()||this.name()!=this.webhook.name()},n.onsubmit=function(t){var e=this;return t.preventDefault(),this.loading=!0,this.webhook.save({extraText:this.extraText(),group_id:this.groupId(),use_plain_text:this.usePlainText(),include_tags:this.includeTags(),max_post_content_length:this.maxPostContentLength()||0,name:this.name()}).then((function(){e.loading=!1,m.redraw()})).catch((function(){e.loading=!1,m.redraw()}))},n.onkeypress=function(t){"Enter"===t.key&&this.onsubmit(t)},n.onchange=function(t,e,o){o.loading=!0;var n=this.webhook.events();return e?n.push(t):n.splice(n.indexOf(t),1),this.attrs.updateWebhook(n).then((function(){o.loading=!1,m.redraw()}))},e}(t.n(z)()),U=function(t){function e(){return t.apply(this,arguments)||this}l(e,t);var n=e.prototype;return n.oninit=function(e){t.prototype.oninit.call(this,e),this.webhook=this.attrs.webhook,this.services=this.attrs.services,this.url=b()(this.webhook.url()),this.service=b()(this.webhook.service()),this.events=b()(this.webhook.events()),this.error=b()(this.webhook.error()),this.loading={}},n.view=function(){var t=this,e=this.webhook,n=this.services,s=o().initializers.has("flarum-tags"),a=e.tags().filter(Boolean),r=e.service(),i=[e.error&&e.error()];n[r]?e.isValid()?s||0===e.tags().length?a.length!==e.attribute("tag_id").length&&i.push(o().translator.trans("fof-webhooks.admin.errors.tag_invalid")):i.push(o().translator.trans("fof-webhooks.admin.errors.tag_disabled")):i.push(o().translator.trans("fof-webhooks.admin.errors.url_invalid")):i.push(o().translator.trans("fof-webhooks.admin.errors.service_not_found",{service:r}));var l=function(){return o().modal.show(L(),{selectedTags:a,onsubmit:function(e){return t.update("tag_id")(e.map((function(t){return t.id()})))}})};return m("div",{className:"Webhooks--row","data-webhook-id":e.id()},m("div",{className:"Webhook-input"},m(w(),{options:n,value:r,onchange:this.update("service"),disabled:this.loading.service}),m("input",{className:"FormControl Webhook-url",type:"url",value:this.url(),onchange:f()("value",this.update("url")),disabled:this.loading.url,placeholder:o().translator.trans("fof-webhooks.admin.settings.help.url")}),s&&(a.length?F()(a,{onclick:l}):m("span",{className:"TagsLabel",onclick:l},o().translator.trans("fof-webhooks.admin.settings.item.tag_any_label"))),m(k(),{type:"button",className:"Button Webhook-button",icon:"fas fa-edit",onclick:function(){return o().modal.show(A,{webhook:e,updateWebhook:t.update("events")})}}),m(k(),{type:"button",className:"Button Button--warning Webhook-button",icon:"fas fa-times",onclick:this.delete.bind(this)})),!this.events().length&&m(C(),{className:"Webhook-error",dismissible:!1},o().translator.trans("fof-webhooks.admin.settings.help.disabled")),i.filter(Boolean).map((function(t){return m(C(),{className:"Webhook-error",type:"error",dismissible:!1},o().translator.trans(t))})))},n.update=function(t){var e=this;return function(o){var n;return e.loading[t]=!0,e.webhook.save((n={},n[t]=o,n)).catch((function(){})).then((function(){e.loading[t]=!1,e[t]&&e[t](o),m.redraw()}))}},n.delete=function(){return this.webhook.delete().then((function(){return m.redraw()}))},e}(N()),G=function(t){function e(){return t.apply(this,arguments)||this}l(e,t);var n=e.prototype;return n.oninit=function(e){t.prototype.oninit.call(this,e),this.values={},this.services=o().data["fof-webhooks.services"].reduce((function(t,e){return t[e]=o().translator.trans("fof-webhooks.admin.settings.services."+e),t}),{}),this.newWebhook={service:b()("discord"),url:b()(""),loading:b()(!1)},this.loadingData=b()(!0)},n.oncreate=function(e){var n=this;t.prototype.oncreate.call(this,e),Promise.all([o().store.find("fof/webhooks"),this.isTagsEnabled()&&o().store.find("tags")]).then((function(){n.loadingData(!1),m.redraw()}))},n.content=function(){var t=this,e=o().store.all("webhooks");return this.loadingData()?m(_(),null):m("div",{className:"WebhookContent"},m("div",{className:"container"},m("div",{className:"Form-group"},this.buildSettingComponent({type:"boolean",setting:"fof-webhooks.debug",label:o().translator.trans("fof-webhooks.admin.settings.debug_label"),help:o().translator.trans("fof-webhooks.admin.settings.debug_help"),loading:this.loading,onchange:this.updateDebug.bind(this)})),m("hr",null),m("form",null,m("p",{className:"helpText"},o().translator.trans("fof-webhooks.admin.settings.help.general")),this.isTagsEnabled()&&m("p",{className:"helpText"},o().translator.trans("fof-webhooks.admin.settings.help.tags")),m("fieldset",null,m("div",{className:"Webhooks--Container"},e.map((function(e){return m(U,{webhook:e,services:t.services})})),m("div",{className:"Webhooks--row"},m("div",{className:"Webhook-input"},m(w(),{options:this.services,value:this.newWebhook.service(),onchange:this.newWebhook.service}),m("input",{className:"FormControl Webhook-url",type:"url",placeholder:o().translator.trans("fof-webhooks.admin.settings.help.url"),onchange:f()("value",this.newWebhook.url),onkeypress:this.onkeypress.bind(this)}),m(k(),{type:"button",loading:this.newWebhook.loading(),className:"Button Button--warning Webhook-button",icon:"fas fa-plus",onclick:this.addWebhook.bind(this)}))))))))},n.addWebhook=function(){var t=this;if(!this.newWebhook.loading())return this.newWebhook.loading(!0),o().store.createRecord("webhooks").save({service:this.newWebhook.service(),url:this.newWebhook.url()}).then((function(){t.newWebhook.service("discord"),t.newWebhook.url(""),t.newWebhook.loading(!1),m.redraw()})).catch((function(){t.newWebhook.loading(!1),m.redraw()}))},n.onkeypress=function(t){"Enter"===t.key&&this.addWebhook()},n.changed=function(){var t=this;return this.fields.some((function(e){return t.values[e]()!==(o().data.settings[t.addPrefix(e)]||"")}))},n.isTagsEnabled=function(){return!!flarum.extensions["flarum-tags"]},n.updateDebug=function(t){return this.setting("fof-webhooks.debug")(t),this.saveSettings(new Event(null))},e}(u());o().initializers.add("fof/webhooks",(function(){o().store.models.webhooks=h,r().prototype.webhooks=s().hasMany("webhooks"),o().extensionData.for("fof-webhooks").registerPage(G)}))})(),module.exports={}})(); 2 | //# sourceMappingURL=admin.js.map -------------------------------------------------------------------------------- /js/dist/admin.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"admin.js","mappings":"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","sources":["webpack://@fof/webhooks/webpack/bootstrap","webpack://@fof/webhooks/webpack/runtime/compat get default export","webpack://@fof/webhooks/webpack/runtime/define property getters","webpack://@fof/webhooks/webpack/runtime/hasOwnProperty shorthand","webpack://@fof/webhooks/external root \"flarum.core.compat['admin/app']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/Model']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/models/Forum']\"","webpack://@fof/webhooks/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js","webpack://@fof/webhooks/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js","webpack://@fof/webhooks/./src/admin/models/Webhook.js","webpack://@fof/webhooks/external root \"flarum.core.compat['admin/components/ExtensionPage']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/utils/Stream']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/utils/withAttr']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/components/Button']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/components/Select']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/components/LoadingIndicator']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/Component']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/components/Dropdown']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/components/Alert']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['tags/common/helpers/tagsLabel']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['tags/common/components/TagSelectionModal']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/components/Switch']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/helpers/icon']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/models/Group']\"","webpack://@fof/webhooks/external root \"flarum.core.compat['common/components/Modal']\"","webpack://@fof/webhooks/./src/admin/components/WebhookEditModal.js","webpack://@fof/webhooks/./src/admin/components/SettingsListItem.js","webpack://@fof/webhooks/./src/admin/components/WebhooksPage.js","webpack://@fof/webhooks/./src/admin/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Model'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/models/Forum'];","export default function _setPrototypeOf(o, p) {\n _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {\n o.__proto__ = p;\n return o;\n };\n return _setPrototypeOf(o, p);\n}","import setPrototypeOf from \"./setPrototypeOf.js\";\nexport default function _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n setPrototypeOf(subClass, superClass);\n}","import app from 'flarum/admin/app';\nimport Model from 'flarum/common/Model';\n\nexport default class Webhook extends Model {\n id = Model.attribute('id');\n service = Model.attribute('service');\n url = Model.attribute('url');\n\n error = Model.attribute('error');\n events = Model.attribute('events');\n\n tagIds = Model.attribute('tag_id');\n\n groupId = Model.attribute('group_id');\n extraText = Model.attribute('extra_text');\n name = Model.attribute('name');\n\n isValid = Model.attribute('is_valid', Boolean);\n\n usePlainText = Model.attribute('use_plain_text', Boolean);\n maxPostContentLength = Model.attribute('max_post_content_length');\n\n includeTags = Model.attribute('include_tags', Boolean);\n\n apiEndpoint() {\n return `/fof/webhooks${this.exists ? `/${this.data.id}` : ''}`;\n }\n\n tags() {\n return this.tagIds().map((id) => app.store.getById('tags', id));\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['admin/components/ExtensionPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/Stream'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/withAttr'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Button'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Select'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LoadingIndicator'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Component'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Dropdown'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Alert'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['tags/common/helpers/tagsLabel'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['tags/common/components/TagSelectionModal'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Switch'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/icon'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/models/Group'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Modal'];","import app from 'flarum/admin/app';\nimport Switch from 'flarum/common/components/Switch';\nimport Button from 'flarum/common/components/Button';\nimport Dropdown from 'flarum/common/components/Dropdown';\nimport icon from 'flarum/common/helpers/icon';\nimport Group from 'flarum/common/models/Group';\nimport Modal from 'flarum/common/components/Modal';\nimport Stream from 'flarum/common/utils/Stream';\n\nconst sortByProp = (prop) => (a, b) => {\n const propA = a[prop].toUpperCase(); // ignore upper and lowercase\n const propB = b[prop].toUpperCase(); // ignore upper and lowercase\n\n return propA < propB ? -1 : propA > propB ? 1 : 0;\n};\n\nconst groupBy = (obj, fn) => {\n const keys = Object.keys(obj);\n const vals = Object.values(obj);\n\n return keys.map(typeof fn === 'function' ? fn : (val) => val[fn]).reduce((acc, val, i) => {\n if (!acc[val]) acc[val] = {};\n\n acc[val][keys[i]] = vals[i];\n\n return acc;\n }, {});\n};\n\nexport default class WebhookEditModal extends Modal {\n oninit(vnode) {\n super.oninit(vnode);\n\n this.webhook = this.attrs.webhook;\n\n const events = app.data['fof-webhooks.events'];\n\n this.groupId = Stream(this.webhook.groupId() || Group.GUEST_ID);\n this.extraText = Stream(this.webhook.extraText() || '');\n this.name = Stream(this.webhook.name() || '');\n this.usePlainText = Stream(this.webhook.usePlainText());\n this.maxPostContentLength = Stream(this.webhook.maxPostContentLength());\n this.includeTags = Stream(this.webhook.includeTags());\n\n this.events = groupBy(\n events.reduce(\n (obj, evt) => {\n console.log(evt);\n const m = /((?:[a-z]\\\\?)+?)\\\\Events?\\\\([a-z]+)/i.exec(evt);\n\n if (!m) {\n obj.other.push({\n full: evt,\n name: evt,\n });\n obj.other = obj.other.sort();\n return obj;\n }\n\n const group = m[1].toLowerCase().replaceAll('\\\\', '.');\n\n if (!obj[group]) obj[group] = [];\n\n obj[group] = obj[group]\n .concat({\n full: evt,\n name: m[2],\n })\n .sort();\n\n return obj;\n },\n { other: [] }\n ),\n (key) => key.split('.')[0]\n );\n }\n\n className() {\n return 'Modal--medium';\n }\n\n title() {\n return app.translator.trans('fof-webhooks.admin.settings.modal.title');\n }\n\n content() {\n const icons = {\n 2: 'fas fa-globe',\n 3: 'fas fa-user',\n };\n\n const group = app.store.getById('groups', this.groupId());\n const isFilteringTags = !!this.webhook.tags()?.length;\n\n return (\n
\n
\n \n {app.translator.trans('fof-webhooks.admin.settings.modal.use_plain_text_label')}\n \n\n
\n \n\n

{app.translator.trans('fof-webhooks.admin.settings.modal.max_post_content_length_help')}

\n\n \n
\n\n
\n \n\n

{app.translator.trans('fof-webhooks.admin.settings.modal.extra_text_help')}

\n\n \n
\n\n
\n \n\n

{app.translator.trans('fof-webhooks.admin.settings.modal.name_help')}

\n\n \n
\n\n
\n \n

{app.translator.trans('fof-webhooks.admin.settings.modal.group_help')}

\n\n \n {app.store\n .all('groups')\n .filter((g) => ['1', '2'].includes(g.id()))\n .map((g) => (\n this.groupId(g.id())}\n type=\"button\"\n >\n {g.namePlural()}\n \n ))}\n \n
\n\n
\n \n

{app.translator.trans('fof-webhooks.admin.settings.modal.description')}

\n {this.webhook.service() !== 'microsoft-teams' && (\n
\n \n {app.translator.trans('fof-webhooks.admin.settings.modal.include_matching_tags_label')}\n \n
\n )}\n {Object.entries(this.events).map(([, events]) => (\n
\n {Object.entries(events)\n .sort(sortByProp(0))\n .map(([group, events]) =>\n events.length ? (\n
\n

{this.translate(group)}

\n {events.map((event) => (\n \n {this.translate(group, event.name.toLowerCase())}\n \n ))}\n
\n ) : null\n )}\n
\n ))}\n
\n\n
\n \n
\n
\n
\n );\n }\n\n translate(group, key = 'title') {\n return app.translator.trans(`fof-webhooks.admin.settings.actions.${group}.${key}`);\n }\n\n isDirty() {\n return (\n this.extraText() != this.webhook.extraText() ||\n this.groupId() !== this.webhook.groupId() ||\n this.usePlainText() !== this.webhook.usePlainText() ||\n this.includeTags() !== this.webhook.includeTags() ||\n this.maxPostContentLength() != this.webhook.maxPostContentLength() ||\n this.name() != this.webhook.name()\n );\n }\n\n onsubmit(e) {\n e.preventDefault();\n\n this.loading = true;\n\n return this.webhook\n .save({\n extraText: this.extraText(),\n group_id: this.groupId(),\n use_plain_text: this.usePlainText(),\n include_tags: this.includeTags(),\n max_post_content_length: this.maxPostContentLength() || 0,\n name: this.name(),\n })\n .then(() => {\n this.loading = false;\n m.redraw();\n })\n .catch(() => {\n this.loading = false;\n m.redraw();\n });\n }\n\n onkeypress(e) {\n if (e.key === 'Enter') {\n this.onsubmit(e);\n }\n }\n\n onchange(event, checked, component) {\n component.loading = true;\n\n let events = this.webhook.events();\n\n if (checked) {\n events.push(event);\n } else {\n events.splice(events.indexOf(event), 1);\n }\n\n return this.attrs.updateWebhook(events).then(() => {\n component.loading = false;\n m.redraw();\n });\n }\n}\n","import app from 'flarum/admin/app';\nimport Component from 'flarum/common/Component';\nimport Dropdown from 'flarum/common/components/Dropdown';\nimport Button from 'flarum/common/components/Button';\nimport Select from 'flarum/common/components/Select';\nimport Alert from 'flarum/common/components/Alert';\nimport Stream from 'flarum/common/utils/Stream';\nimport withAttr from 'flarum/common/utils/withAttr';\n\nimport tagsLabel from 'flarum/tags/common/helpers/tagsLabel';\nimport TagSelectionModal from 'flarum/tags/common/components/TagSelectionModal';\n\nimport WebhookEditModal from './WebhookEditModal';\n\nexport default class SettingsListItem extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n\n this.webhook = this.attrs.webhook;\n this.services = this.attrs.services;\n\n this.url = Stream(this.webhook.url());\n this.service = Stream(this.webhook.service());\n this.events = Stream(this.webhook.events());\n this.error = Stream(this.webhook.error());\n\n this.loading = {};\n }\n\n view() {\n const { webhook, services } = this;\n const isTagsEnabled = app.initializers.has('flarum-tags');\n const tags = webhook.tags().filter(Boolean);\n\n const service = webhook.service();\n const errors = [webhook.error && webhook.error()];\n\n if (!services[service]) {\n errors.push(app.translator.trans('fof-webhooks.admin.errors.service_not_found', { service }));\n } else if (!webhook.isValid()) {\n errors.push(app.translator.trans('fof-webhooks.admin.errors.url_invalid'));\n } else if (!isTagsEnabled && webhook.tags().length !== 0) {\n errors.push(app.translator.trans('fof-webhooks.admin.errors.tag_disabled'));\n } else if (tags.length !== webhook.attribute('tag_id').length) {\n errors.push(app.translator.trans('fof-webhooks.admin.errors.tag_invalid'));\n }\n\n const changeTags = () =>\n app.modal.show(TagSelectionModal, {\n selectedTags: tags,\n onsubmit: (tags) => this.update('tag_id')(tags.map((tag) => tag.id())),\n });\n\n return (\n
\n
\n \n\n \n\n \n
\n
\n \n \n \n \n \n );\n }\n\n addWebhook() {\n if (this.newWebhook.loading()) return;\n\n this.newWebhook.loading(true);\n\n return app.store\n .createRecord('webhooks')\n .save({\n service: this.newWebhook.service(),\n url: this.newWebhook.url(),\n })\n .then(() => {\n this.newWebhook.service('discord');\n this.newWebhook.url('');\n this.newWebhook.loading(false);\n\n m.redraw();\n })\n .catch(() => {\n this.newWebhook.loading(false);\n\n m.redraw();\n });\n }\n\n onkeypress(e) {\n if (e.key === 'Enter') {\n this.addWebhook();\n }\n }\n\n /**\n * @returns boolean\n */\n changed() {\n return this.fields.some((key) => this.values[key]() !== (app.data.settings[this.addPrefix(key)] || ''));\n }\n\n isTagsEnabled() {\n return !!flarum.extensions['flarum-tags'];\n }\n\n updateDebug(state) {\n this.setting('fof-webhooks.debug')(state);\n\n return this.saveSettings(new Event(null));\n }\n}\n","import app from 'flarum/admin/app';\nimport Model from 'flarum/common/Model';\nimport Forum from 'flarum/common/models/Forum';\n\nimport Webhook from './models/Webhook';\nimport WebhooksPage from './components/WebhooksPage';\n\napp.initializers.add('fof/webhooks', () => {\n app.store.models.webhooks = Webhook;\n\n Forum.prototype.webhooks = Model.hasMany('webhooks');\n\n app.extensionData.for('fof-webhooks').registerPage(WebhooksPage);\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","flarum","core","compat","_setPrototypeOf","p","setPrototypeOf","bind","__proto__","_inheritsLoose","subClass","superClass","create","constructor","Webhook","_Model","_this","_len","arguments","length","args","Array","_key","apply","concat","id","Model","service","url","error","events","tagIds","groupId","extraText","name","isValid","Boolean","usePlainText","maxPostContentLength","includeTags","_proto","apiEndpoint","this","exists","data","tags","map","app","getById","WebhookEditModal","_Modal","oninit","vnode","webhook","attrs","fn","keys","vals","Stream","Group","reduce","evt","console","log","m","exec","other","push","full","sort","group","toLowerCase","replaceAll","split","values","acc","val","i","className","title","trans","content","_this$webhook$tags","icons","isFilteringTags","onsubmit","Switch","state","onchange","type","min","bidi","onkeypress","placeholder","attribute","Dropdown","label","icon","namePlural","buttonClassName","all","filter","g","includes","Button","active","disabled","onclick","style","display","marginTop","entries","_ref","b","propA","toUpperCase","propB","_ref2","translate","event","loading","isDirty","e","_this2","preventDefault","save","group_id","use_plain_text","include_tags","max_post_content_length","then","redraw","checked","component","splice","indexOf","updateWebhook","Modal","SettingsListItem","_Component","services","view","isTagsEnabled","has","errors","changeTags","show","TagSelectionModal","selectedTags","update","tag","Select","options","value","withAttr","tagsLabel","Alert","dismissible","field","_this2$webhook$save","Component","WebhooksPage","_ExtensionPage","newWebhook","loadingData","oncreate","Promise","find","webhooks","LoadingIndicator","buildSettingComponent","setting","help","updateDebug","addWebhook","_this3","createRecord","changed","_this4","fields","some","settings","addPrefix","extensions","saveSettings","Event","ExtensionPage","add","models","Forum","registerPage"],"sourceRoot":""} -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fof/webhooks", 3 | "private": true, 4 | "prettier": "@flarum/prettier-config", 5 | "dependencies": { 6 | "@flarum/prettier-config": "^1.0.0", 7 | "flarum-webpack-config": "^2.0.2", 8 | "webpack": "^5.89.0", 9 | "webpack-cli": "^5.1.4" 10 | }, 11 | "scripts": { 12 | "build": "webpack --mode production", 13 | "dev": "webpack --mode development --watch", 14 | "format": "prettier --write src" 15 | }, 16 | "devDependencies": { 17 | "flarum-tsconfig": "^1.0.2", 18 | "prettier": "^3.0.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use Flarum's tsconfig as a starting point 3 | "extends": "flarum-tsconfig", 4 | // This will match all .ts, .tsx, .d.ts, .js, .jsx files 5 | "include": ["src/**/*"], 6 | "compilerOptions": { 7 | // This will output typings to `dist-typings` 8 | "declarationDir": "./dist-typings", 9 | "baseUrl": ".", 10 | "paths": { 11 | "flarum/*": ["../vendor/flarum/core/js/dist-typings/*"] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('flarum-webpack-config')(); 2 | -------------------------------------------------------------------------------- /migrations/2020_12_24_create_webhooks_table.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 18 | if ($schema->hasTable('webhooks')) { 19 | return; 20 | } 21 | 22 | $schema->create('webhooks', function (Blueprint $table) { 23 | $table->increments('id'); 24 | $table->string('service'); 25 | $table->string('url'); 26 | $table->string('error')->nullable(); 27 | $table->binary('events'); 28 | 29 | $table->integer('group_id')->unsigned()->default(Group::GUEST_ID); 30 | }); 31 | }, 32 | 'down' => function (Builder $schema) { 33 | $schema->drop('webhooks'); 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /migrations/2020_12_25_migrate_discord_webhooks.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | Webhook::query() 18 | ->where('service', 'discord') 19 | ->where('url', 'LIKE', '%discordapp.com/%') 20 | ->each(function (Webhook $hook) { 21 | $hook->url = str_replace('discordapp.com/', 'discord.com/', $hook->url); 22 | $hook->save(); 23 | }); 24 | }, 25 | 'down' => function (Builder $schema) { 26 | // 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /migrations/2020_12_26_add_extra_text_column.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | if ($schema->hasColumn('webhooks', 'extra_text')) { 18 | return; 19 | } 20 | 21 | $schema->table('webhooks', function (Blueprint $table) { 22 | $table->string('extra_text', 256)->nullable(); 23 | }); 24 | }, 25 | 'down' => function (Builder $schema) { 26 | $schema->table('webhooks', function (Blueprint $table) { 27 | $table->dropColumn('extra_text'); 28 | }); 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /migrations/2020_12_27_add_tag_column.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->table('webhooks', function (Blueprint $table) { 18 | $table->unsignedInteger('tag_id')->nullable(); 19 | }); 20 | }, 21 | 'down' => function (Builder $schema) { 22 | $schema->table('webhooks', function (Blueprint $table) { 23 | $table->dropColumn('tag_id'); 24 | }); 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /migrations/2021_02_07_add_max_post_content_length_column.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->table('webhooks', function (Blueprint $table) { 18 | $table->unsignedInteger('max_post_content_length')->nullable(); 19 | }); 20 | }, 21 | 'down' => function (Builder $schema) { 22 | $schema->table('webhooks', function (Blueprint $table) { 23 | $table->dropColumn('max_post_content_length'); 24 | }); 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /migrations/2021_02_07_add_use_plain_text_column.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->table('webhooks', function (Blueprint $table) { 18 | $table->boolean('use_plain_text'); 19 | }); 20 | }, 21 | 'down' => function (Builder $schema) { 22 | $schema->table('webhooks', function (Blueprint $table) { 23 | $table->dropColumn('use_plain_text'); 24 | }); 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /migrations/2023_06_07_000000_remove_tag_id_constraint.php: -------------------------------------------------------------------------------- 1 | static function (Builder $schema) { 19 | $schema->table('webhooks', function (Blueprint $table) use ($schema) { 20 | $indexes = $schema->getConnection()->getDoctrineSchemaManager()->listTableIndexes($table->getTable()); 21 | 22 | /** 23 | * @var \Doctrine\DBAL\Schema\Index $index 24 | */ 25 | $index = collect($indexes)->first(function ($index) { 26 | return in_array('tag_id', $index->getColumns(), true); 27 | }); 28 | 29 | if ($index) { 30 | $table->dropForeign(['tag_id']); 31 | $table->dropIndex($index->getName()); 32 | } 33 | }); 34 | }, 35 | 'down' => static function (Builder $schema) { 36 | // 37 | }, 38 | ]; 39 | -------------------------------------------------------------------------------- /migrations/2023_06_07_change_tag_id_to_json.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->table('webhooks', function (Blueprint $table) { 18 | $table->json('tag_id')->change(); 19 | }); 20 | }, 21 | 'down' => function (Builder $schema) { 22 | $schema->table('webhooks', function (Blueprint $table) { 23 | $table->unsignedInteger('tag_id')->change(); 24 | }); 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /migrations/2024_09_12_add_include_tags_columns.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->table('webhooks', function (Blueprint $table) { 18 | $table->boolean('include_tags')->default(false); 19 | }); 20 | }, 21 | 'down' => function (Builder $schema) { 22 | $schema->table('webhooks', function (Blueprint $table) { 23 | $table->dropColumn('include_tags'); 24 | }); 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /migrations/2024_09_12_add_name_column.php: -------------------------------------------------------------------------------- 1 | ['string', 'length' => 128, 'nullable' => true], 16 | ]); 17 | -------------------------------------------------------------------------------- /migrations/2024_12_30_increase_webhook_url_length.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | $schema->table('webhooks', function (Blueprint $table) { 18 | $table->string('url', 511)->change(); 19 | }); 20 | }, 21 | 'down' => function (Builder $schema) { 22 | // no need to revert this change 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/flarum/phpstan/extension.neon 3 | 4 | parameters: 5 | # The level will be increased in Flarum 2.0 6 | level: 5 7 | paths: 8 | - extend.php 9 | - src 10 | excludePaths: 11 | - *.blade.php 12 | checkMissingIterableValueType: false 13 | databaseMigrationsPath: ['migrations'] 14 | -------------------------------------------------------------------------------- /resources/less/admin.less: -------------------------------------------------------------------------------- 1 | .WebhookContent { 2 | margin-top: 20px; 3 | } 4 | 5 | .Webhooks--row { 6 | margin: 10px 0; 7 | 8 | .Webhook-input { 9 | display: flex; 10 | flex-direction: row; 11 | flex-basis: fit-content; 12 | 13 | .TagsLabel { 14 | flex-grow: 1; 15 | white-space: nowrap; 16 | align-self: center; 17 | cursor: pointer; 18 | } 19 | 20 | > .Select, > button, > input, > div, .TagsLabel { 21 | margin: 0 5px; 22 | } 23 | 24 | > :first-child { 25 | margin-left: 0; 26 | } 27 | 28 | > :last-child { 29 | margin-right: 0; 30 | } 31 | 32 | .Select { 33 | flex-shrink: 1; 34 | } 35 | 36 | .Webhook-url { 37 | flex-grow: 1; 38 | 39 | &::placeholder { 40 | font-style: italic; 41 | } 42 | } 43 | 44 | .Webhook-button { 45 | .Button-icon { 46 | margin-right: 0; 47 | } 48 | } 49 | } 50 | 51 | .Webhook-error { 52 | margin: 5px 0; 53 | padding: 5px 10px; 54 | 55 | &:last-of-type { 56 | margin-bottom: 20px; 57 | } 58 | } 59 | 60 | &:last-of-type { 61 | margin-top: 40px; 62 | 63 | &:first-of-type { 64 | margin-top: 10px; 65 | } 66 | } 67 | } 68 | 69 | .FofWebhooksModal { 70 | .Form-group { 71 | margin-top: 10px; 72 | margin-bottom: 5px; 73 | } 74 | 75 | .Form-group.hasLoading { 76 | > div { 77 | position: relative; 78 | } 79 | 80 | .LoadingIndicator { 81 | position: absolute; 82 | top: 8px; 83 | right: 15px; 84 | } 85 | } 86 | 87 | .Dropdown-toggle .icon { 88 | margin-right: 10px; 89 | } 90 | 91 | .label { 92 | margin-top: 20px; 93 | margin-bottom: 0; 94 | } 95 | 96 | .Webhook-events { 97 | > div { 98 | display: flex; 99 | flex-wrap: wrap; 100 | justify-content: space-between; 101 | 102 | h2 { 103 | flex-basis: 100%; 104 | text-transform: uppercase; 105 | margin: 0; 106 | } 107 | 108 | > div { 109 | flex: 50%; 110 | } 111 | } 112 | 113 | .Checkbox { 114 | &:last-of-type { 115 | margin-bottom: 20px; 116 | } 117 | &:first-of-type { 118 | margin-top: -10px; 119 | } 120 | 121 | margin-bottom: 10px; 122 | padding-left: 50px; 123 | 124 | .Checkbox-display { 125 | width: 40px; 126 | height: 20px; 127 | margin-top: -1px; 128 | margin-left: -50px; 129 | 130 | &::before { 131 | width: 18px; 132 | height: 18px; 133 | top: 1px; 134 | left: 1px; 135 | } 136 | } 137 | 138 | &.on { 139 | .Checkbox-display::before { 140 | left: 21px; 141 | } 142 | } 143 | 144 | &.off { 145 | .Checkbox-display { 146 | background: darken(@control-bg, 10%); 147 | } 148 | } 149 | 150 | .spinner { 151 | transform: scale(0.7) !important; 152 | left: -0.5px !important; 153 | top: 35% !important; 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /resources/locale/en.yml: -------------------------------------------------------------------------------- 1 | fof-webhooks: 2 | actions: 3 | discussion: 4 | started: "Started discussion `{1}`" 5 | renamed: 6 | title: "Renamed discussion `{1}`" 7 | description: "New title: {1}" 8 | hidden: "Hid discussion `{1}`" 9 | restored: "Restored discussion `{1}`" 10 | deleted: "Deleted discussion `{1}`" 11 | 12 | group: 13 | created: "Created `{1}` group" 14 | renamed: "Renamed a group to `{1}`" 15 | deleted: "Deleted `{1}` group" 16 | 17 | post: 18 | posted: "New post in `{1}`" 19 | revised: "Edited post in `{1}`" 20 | hidden: "Hid post in `{1}`" 21 | restored: "Restored post in `{1}`" 22 | deleted: "Deleted post in `{1}`" 23 | approved: "Approved post in `{1}`" 24 | 25 | user: 26 | registered: New user registered 27 | renamed: 28 | title: "Changed username of `{1}`" 29 | description: "New username: {1}" 30 | deleted: "Deleted user `{1}`" 31 | 32 | adapters: 33 | errors: 34 | 404: Webhook Not Found 35 | 36 | admin: 37 | errors: 38 | service_not_found: 'The service "{service}" cannot be found.' 39 | url_invalid: URL is not valid for selected service. 40 | tag_invalid: This webhook is restricted to an invalid tag. The restriction will not apply. Make sure this is intended. 41 | tag_disabled: This webhook had a tag restriction configured. The Tags extension is disabled. The restriction will not apply. Make sure this is intended. 42 | 43 | nav: 44 | desc: Customizable outgoing webhooks for your forum. 45 | settings: 46 | debug_label: Debug 47 | debug_help: Add extra logs to the Flarum log file to debug issues with webhooks and/or events not working corectly. 48 | 49 | item: 50 | tag_any_label: Any Tag 51 | 52 | help: 53 | disabled: This webhook is disabled because it doesn't have any events enabled. 54 | general: Here you can add, edit, and remove webhooks from your forum. 55 | tags: You can restrict webhooks to specific tags. If you select any tags, the webhook will only be fired if the event is triggered on a resource that has any of the selected tags. 56 | url: The webhook's endpoint to be executed 57 | 58 | modal: 59 | title: Webhook Settings 60 | description: Here you modify what events the webhook is fired on. 61 | 62 | use_plain_text_label: Reduce amount of formatting in post content 63 | 64 | max_post_content_length_label: Maximum Post Content Length 65 | max_post_content_length_help: Set a number to limit the post content in all post-related events. Change to 0 or nothing to remove limit. 66 | 67 | extra_text_label: Extra Text 68 | extra_text_help: Extra text to include in the sent webhook. 69 | 70 | group_label: Group 71 | group_help: Send resources that can be seen by this group 72 | 73 | name_label: Name 74 | name_help: The name of the webhook. This will be used for things like the Discord username, Slack bot name, etc. 75 | 76 | events_label: Events 77 | 78 | include_matching_tags_label: Include matching tags -- for webhooks restricted to specific tags, show which ones the event matched 79 | 80 | services: 81 | discord: Discord 82 | slack: Slack 83 | microsoft-teams: Microsoft Teams 84 | 85 | actions: 86 | flarum: 87 | discussion: 88 | title: Flarum Discussion 89 | 90 | started: Started 91 | renamed: => fof-webhooks.ref.renamed 92 | hidden: => fof-webhooks.ref.hidden 93 | restored: => fof-webhooks.ref.restored 94 | deleted: => fof-webhooks.ref.deleted 95 | 96 | group: 97 | title: Flarum Group 98 | 99 | created: Created 100 | renamed: => fof-webhooks.ref.renamed 101 | deleted: => fof-webhooks.ref.deleted 102 | 103 | post: 104 | title: Flarum Post 105 | 106 | posted: Posted 107 | revised: Edited 108 | hidden: => fof-webhooks.ref.hidden 109 | restored: => fof-webhooks.ref.restored 110 | deleted: => fof-webhooks.ref.deleted 111 | 112 | user: 113 | title: Flarum User 114 | 115 | registered: Registered 116 | renamed: => fof-webhooks.ref.renamed 117 | deleted: => fof-webhooks.ref.deleted 118 | 119 | approval: 120 | title: Flarum Approval 121 | 122 | postwasapproved: Post Approved 123 | 124 | ref: 125 | deleted: Deleted 126 | hidden: Hidden 127 | renamed: Renamed 128 | restored: Restored 129 | -------------------------------------------------------------------------------- /src/Action.php: -------------------------------------------------------------------------------- 1 | url = resolve(UrlGenerator::class); 35 | $this->translator = resolve(TranslatorInterface::class); 36 | } 37 | 38 | /** 39 | * @param $event 40 | * 41 | * @return Response|null 42 | * 43 | * @deprecated 44 | */ 45 | public function listen($event): ?Response 46 | { 47 | return null; 48 | } 49 | 50 | /** 51 | * @param Webhook $webhook 52 | * @param $event 53 | * 54 | * @return Response|null 55 | * 56 | * @abstract 57 | */ 58 | public function handle(Webhook $webhook, $event): ?Response 59 | { 60 | return $this->listen($event); 61 | } 62 | 63 | /** 64 | * @param $event 65 | * @param Webhook $webhook 66 | * 67 | * @return bool 68 | */ 69 | public function ignore(Webhook $webhook, $event): bool 70 | { 71 | return false; 72 | } 73 | 74 | /** 75 | * @param string $id 76 | * @param $param1 77 | * 78 | * @return string 79 | */ 80 | protected function translate(string $id, $param1 = null): string 81 | { 82 | return $this->translator->trans('fof-webhooks.actions.'.$id, [ 83 | '{1}' => $param1, 84 | ]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Actions/Discussion/Action.php: -------------------------------------------------------------------------------- 1 | discussion; 27 | $post = $discussion->firstPost ?? $discussion->posts()->where('number', 1)->first(); 28 | 29 | if ($webhook->asGuest() && $post && !$post->isVisibleTo(new Guest())) { 30 | return true; 31 | } 32 | 33 | $tagIds = $webhook->tag_id; 34 | $tagsIsEnabled = resolve(ExtensionManager::class)->isEnabled('flarum-tags'); 35 | 36 | /** @phpstan-ignore-next-line */ 37 | if (!empty($tagIds) && $tagsIsEnabled && !$discussion->tags()->whereIn('id', $webhook->tag_id)->exists()) { 38 | return true; 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Actions/Discussion/Deleted.php: -------------------------------------------------------------------------------- 1 | setTitle( 30 | $this->translate('discussion.deleted', $event->discussion->title) 31 | ) 32 | ->setAuthor($event->actor) 33 | ->setColor('fed330') 34 | ->setTimestamp(Carbon::now()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Actions/Discussion/Hidden.php: -------------------------------------------------------------------------------- 1 | discussion->firstPost; 31 | 32 | return Response::build($event) 33 | ->setTitle( 34 | $this->translate('discussion.hidden', $event->discussion->title) 35 | ) 36 | ->setURL('discussion', [ 37 | 'id' => $event->discussion->id, 38 | ]) 39 | ->setDescription(Post::getContent($firstPost, $webhook)) 40 | ->setAuthor($event->actor) 41 | ->setColor('fed330') 42 | ->setTimestamp($event->discussion->hidden_at); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Actions/Discussion/Renamed.php: -------------------------------------------------------------------------------- 1 | setTitle( 29 | $this->translate('discussion.renamed.title', $event->oldTitle) 30 | ) 31 | ->setURL('discussion', [ 32 | 'id' => $event->discussion->id, 33 | ]) 34 | ->setDescription($this->translate('discussion.renamed.description', $event->discussion->title)) 35 | ->setAuthor($event->actor) 36 | ->setColor('fed330') 37 | ->setTimestamp($event->discussion->last_posted_at); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Actions/Discussion/Restored.php: -------------------------------------------------------------------------------- 1 | discussion->firstPost; 32 | 33 | return Response::build($event) 34 | ->setTitle( 35 | $this->translate('discussion.restored', $event->discussion->title) 36 | ) 37 | ->setURL('discussion', [ 38 | 'id' => $event->discussion->id, 39 | ]) 40 | ->setDescription(Post::getContent($firstPost, $webhook)) 41 | ->setAuthor($event->actor) 42 | ->setColor('fed330') 43 | ->setTimestamp(Carbon::now()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Actions/Discussion/Started.php: -------------------------------------------------------------------------------- 1 | setTitle( 32 | $this->translate('discussion.started', $event->discussion->title) 33 | ) 34 | ->setURL('discussion', [ 35 | 'id' => $event->discussion->id, 36 | ]) 37 | ->setDescription(Post::getContent($event->discussion->firstPost, $webhook)) 38 | ->setAuthor($event->actor) 39 | ->setColor('fed330') 40 | ->setTimestamp($event->discussion->created_at); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Actions/Group/Created.php: -------------------------------------------------------------------------------- 1 | setTitle( 31 | $this->translate('group.created', $event->group->name_plural) 32 | ) 33 | ->setAuthor($event->actor) 34 | ->setColor('34495e') 35 | ->setTimestamp(Carbon::now()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Actions/Group/Deleted.php: -------------------------------------------------------------------------------- 1 | setTitle( 31 | $this->translate('group.deleted', $event->group->name_plural) 32 | ) 33 | ->setAuthor($event->actor) 34 | ->setColor('34495e') 35 | ->setTimestamp(Carbon::now()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Actions/Group/Renamed.php: -------------------------------------------------------------------------------- 1 | setTitle( 31 | $this->translate('group.renamed', $event->group->name_singular) 32 | ) 33 | ->setAuthor($event->actor) 34 | ->setColor('34495e') 35 | ->setTimestamp(Carbon::now()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Actions/Post/Action.php: -------------------------------------------------------------------------------- 1 | asGuest() && !$event->post->isVisibleTo(new Guest())) { 23 | return true; 24 | } 25 | 26 | $discussion = $event->post->discussion; 27 | $tagIds = $webhook->tag_id; 28 | $tagsIsEnabled = resolve(ExtensionManager::class)->isEnabled('flarum-tags'); 29 | 30 | if ($discussion && !empty($tagIds) && $tagsIsEnabled && !$discussion->tags()->whereIn('id', $tagIds)->exists()) { 31 | return true; 32 | } 33 | 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Actions/Post/Approved.php: -------------------------------------------------------------------------------- 1 | asGuest() && $event->post->number === 1) { 32 | // Send the 'discussion started' message 33 | return (new DiscussionStartedAction())->handle($webhook, new DiscussionStartedEvent($event->post->discussion, $event->post->user)); 34 | } 35 | 36 | $response = parent::handle($webhook, $event); 37 | 38 | if (!$webhook->asGuest()) { 39 | // Send the 'approved' message as the user who approved the post 40 | $response 41 | ->setTitle( 42 | $this->translate('post.approved', $event->post->discussion->title) 43 | ) 44 | ->setDescription(null); 45 | } else { 46 | // Send the 'new post' message 47 | $response->setAuthor($event->post->user); 48 | } 49 | 50 | return $response; 51 | } 52 | 53 | public function ignore(Webhook $webhook, $event): bool 54 | { 55 | return Action::ignore($webhook, $event); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Actions/Post/Deleted.php: -------------------------------------------------------------------------------- 1 | setTitle( 33 | $this->translate('post.deleted', $event->post->discussion->title) 34 | ) 35 | ->setUrl( 36 | 'discussion', 37 | [ 38 | 'id' => $event->post->discussion->id, 39 | ], 40 | '/'.$event->post->number 41 | ) 42 | ->setDescription(Post::getContent($event->post, $webhook)) 43 | ->setAuthor($event->actor) 44 | ->setColor('26de81') 45 | ->setTimestamp(Carbon::now()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Actions/Post/Hidden.php: -------------------------------------------------------------------------------- 1 | setTitle( 32 | $this->translate('post.hidden', $event->post->discussion->title) 33 | ) 34 | ->setUrl( 35 | 'discussion', 36 | [ 37 | 'id' => $event->post->discussion->id, 38 | ], 39 | '/'.$event->post->number 40 | ) 41 | ->setDescription(Post::getContent($event->post, $webhook)) 42 | ->setAuthor($event->actor) 43 | ->setColor('26de81') 44 | ->setTimestamp($event->post->hidden_at); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Actions/Post/Posted.php: -------------------------------------------------------------------------------- 1 | setTitle( 32 | $this->translate('post.posted', $event->post->discussion->title) 33 | ) 34 | ->setUrl( 35 | 'discussion', 36 | [ 37 | 'id' => $event->post->discussion->id, 38 | ], 39 | '/'.$event->post->number 40 | ) 41 | ->setDescription(Post::getContent($event->post, $webhook)) 42 | ->setAuthor($event->actor) 43 | ->setColor('26de81') 44 | ->setTimestamp($event->post->created_at); 45 | } 46 | 47 | /** 48 | * @param \Flarum\Post\Event\Posted $event 49 | * @param Webhook $webhook 50 | * 51 | * @return bool 52 | */ 53 | public function ignore(Webhook $webhook, $event): bool 54 | { 55 | return parent::ignore($webhook, $event) || !isset($event->post->discussion->first_post_id) || $event->post->id == $event->post->discussion->first_post_id; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Actions/Post/Restored.php: -------------------------------------------------------------------------------- 1 | setTitle( 33 | $this->translate('post.restored', $event->post->discussion->title) 34 | ) 35 | ->setUrl( 36 | 'discussion', 37 | [ 38 | 'id' => $event->post->discussion->id, 39 | ], 40 | '/'.$event->post->number 41 | ) 42 | ->setDescription(Post::getContent($event->post, $webhook)) 43 | ->setAuthor($event->actor) 44 | ->setColor('26de81') 45 | ->setTimestamp(Carbon::now()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Actions/Post/Revised.php: -------------------------------------------------------------------------------- 1 | setTitle( 32 | $this->translate('post.revised', $event->post->discussion->title) 33 | ) 34 | ->setUrl( 35 | 'discussion', 36 | [ 37 | 'id' => $event->post->discussion->id, 38 | ], 39 | '/'.$event->post->number 40 | ) 41 | ->setDescription(Post::getContent($event->post, $webhook)) 42 | ->setAuthor($event->actor) 43 | ->setColor('26de81') 44 | ->setTimestamp($event->post->edited_at); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Actions/User/Deleted.php: -------------------------------------------------------------------------------- 1 | setTitle( 31 | $this->translate('user.deleted', $event->user->display_name) 32 | ) 33 | ->setAuthor($event->actor) 34 | ->setColor('4b7bec') 35 | ->setTimestamp(Carbon::now()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Actions/User/Registered.php: -------------------------------------------------------------------------------- 1 | setTitle( 30 | $this->translate('user.registered') 31 | ) 32 | ->setUrl('user', [ 33 | 'username' => $event->user->username, 34 | ]) 35 | ->setAuthor($event->user) 36 | ->setColor('4b7bec') 37 | ->setTimestamp($event->user->joined_at); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Actions/User/Renamed.php: -------------------------------------------------------------------------------- 1 | setTitle( 31 | $this->translate('user.renamed.title', $event->oldUsername) 32 | ) 33 | ->setURL('user', [ 34 | 'username' => $event->user->username, 35 | ]) 36 | ->setDescription($this->translate('user.renamed.description', $event->user->username)) 37 | ->setAuthor($event->actor) 38 | ->setColor('4b7bec') 39 | ->setTimestamp(Carbon::now()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Adapters/Adapter.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 59 | 60 | $this->client = new Client(); 61 | } 62 | 63 | /** 64 | * @param Webhook $webhook 65 | * @param Response $response 66 | * 67 | * @throws ReflectionException 68 | */ 69 | public function handle(Webhook $webhook, Response $response) 70 | { 71 | try { 72 | $this->send($webhook->url, $response); 73 | 74 | TriggerListener::debug(get_class($response->event).": webhook $webhook->id --> sent"); 75 | 76 | if (isset($webhook->error)) { 77 | $webhook->setAttribute('error', null); 78 | } 79 | } catch (RequestException $e) { 80 | $clazz = new ReflectionClass($this->exception); 81 | 82 | if ($e->hasResponse()) { 83 | $e = $clazz->newInstance($e->getResponse(), $webhook->url); 84 | } 85 | 86 | TriggerListener::debug(get_class($response->event).": webhook $webhook->id --> request error"); 87 | 88 | $this->logException($webhook, $response, $e, true); 89 | 90 | $webhook->setAttribute( 91 | 'error', 92 | $e 93 | ); 94 | } catch (Throwable $e) { 95 | $handled = $e instanceof $this->exception; 96 | 97 | TriggerListener::debug(get_class($response->event).": webhook $webhook->id --> other error"); 98 | 99 | $this->logException($webhook, $response, $e, $handled); 100 | 101 | $webhook->setAttribute( 102 | 'error', 103 | $handled ? $e : $e->getMessage() 104 | ); 105 | } 106 | 107 | $webhook->save(); 108 | } 109 | 110 | /** 111 | * Sends a message through the webhook. 112 | * 113 | * @param string $url 114 | * @param Response $response 115 | * 116 | * @throws RequestException 117 | */ 118 | abstract public function send(string $url, Response $response); 119 | 120 | /** 121 | * @param Response $response 122 | * 123 | * @return array 124 | */ 125 | abstract public function toArray(Response $response): array; 126 | 127 | /** 128 | * @param string $url 129 | * 130 | * @return bool 131 | */ 132 | abstract public static function isValidURL(string $url): bool; 133 | 134 | /** 135 | * @param string $url 136 | * @param array $json 137 | * 138 | * @throws GuzzleException 139 | * 140 | * @return ResponseInterface 141 | */ 142 | protected function request(string $url, array $json): ResponseInterface 143 | { 144 | return $this->client->request('POST', $url, [ 145 | 'json' => mb_convert_encoding($json, 'UTF-8', 'auto'), 146 | 'allow_redirects' => false, 147 | ]); 148 | } 149 | 150 | /** 151 | * @return null|string 152 | */ 153 | protected function getAvatarUrl(): ?string 154 | { 155 | $faviconPath = $this->settings->get('favicon_path'); 156 | $logoPath = $this->settings->get('logo_path'); 157 | $path = $faviconPath ?: $logoPath; 158 | 159 | return isset($path) ? resolve(UrlGenerator::class)->to('forum')->path("assets/$path") : null; 160 | } 161 | 162 | /** 163 | * Get the title of the webhook, used for eg. Discord webhook username. 164 | * Defaults to the forum title. Can be modified by the webhook. 165 | * 166 | * @param Response $response 167 | * 168 | * @return string 169 | */ 170 | protected function getTitle(Response $response): string 171 | { 172 | $webhookTitle = trim($response->getWebhookName() ?? ''); 173 | 174 | return $webhookTitle ?: $this->settings->get('forum_title'); 175 | } 176 | 177 | private function logException(Webhook $webhook, Response $response, Throwable $e, $handled = false) 178 | { 179 | resolve('log')->error( 180 | sprintf( 181 | "[fof/webhooks] %s: %s > %s error 182 | \t- \$webhook = %s 183 | \t- \$response = %s \n 184 | \t%s", 185 | self::NAME, 186 | get_class($response->event), 187 | $handled ? 'webhook' : 'unknown', 188 | $webhook->url, 189 | $response, 190 | $handled 191 | ? $e 192 | : sprintf("%s\n%s", $e->getMessage(), $e->getTraceAsString()) 193 | ) 194 | ); 195 | 196 | // Use reporters (e.g. Sentry) if it's an "unhandled" exception 197 | if (!($e instanceof $this->exception)) { 198 | /** @var Reporter[] $reporters */ 199 | $reporters = Container::getInstance()->tagged(Reporter::class); 200 | 201 | foreach ($reporters as $reporter) { 202 | if ($reporter instanceof LogReporter) { 203 | continue; 204 | } 205 | 206 | $reporter->report($e); 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Adapters/Adapters.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private static $adapters = [ 22 | Discord\Adapter::NAME => Discord\Adapter::class, 23 | Slack\Adapter::NAME => Slack\Adapter::class, 24 | MicrosoftTeams\Adapter::NAME => MicrosoftTeams\Adapter::class, 25 | ]; 26 | 27 | /** 28 | * @param string $name 29 | * @param string $adapter 30 | */ 31 | public static function add(string $name, string $adapter) 32 | { 33 | self::$adapters[$name] = $adapter; 34 | } 35 | 36 | /** 37 | * @param string $name 38 | * 39 | * @return null|Adapter 40 | */ 41 | public static function get(string $name): ?Adapter 42 | { 43 | $adapter = Arr::get(self::$adapters, $name); 44 | 45 | if (isset($adapter)) { 46 | return resolve($adapter); 47 | } 48 | 49 | return null; 50 | } 51 | 52 | public static function length(): int 53 | { 54 | return count(self::$adapters); 55 | } 56 | 57 | public static function all() 58 | { 59 | return self::$adapters; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Adapters/Discord/Adapter.php: -------------------------------------------------------------------------------- 1 | request($url, [ 37 | 'username' => Str::limit($this->getTitle($response), 32, '...'), 38 | 'content' => $response->getExtraText(), 39 | 'embeds' => [ 40 | $this->toArray($response), 41 | ], 42 | ]); 43 | } 44 | 45 | /** 46 | * @param Response $response 47 | * 48 | * @return array 49 | */ 50 | public function toArray(Response $response): array 51 | { 52 | return [ 53 | 'title' => substr($response->title, 0, 256), 54 | 'url' => $response->url, 55 | 'description' => $response->description ? substr($response->description, 0, 2048) : null, 56 | 'author' => $response->author->exists ? [ 57 | 'name' => substr($response->author->display_name, 0, 256), 58 | 'url' => $response->getAuthorUrl(), 59 | 'icon_url' => $response->author->avatar_url, 60 | ] : null, 61 | 'color' => $response->getColor(), 62 | 'fields' => $response->getIncludeTags() ? [ 63 | [ 64 | 'name' => 'Tags', 65 | 'value' => implode(', ', $response->getTags()), 66 | ], 67 | ] : null, 68 | 'timestamp' => $response->timestamp, 69 | 'type' => 'rich', 70 | ]; 71 | } 72 | 73 | /** 74 | * @param string $url 75 | * 76 | * @return bool 77 | */ 78 | public static function isValidURL(string $url): bool 79 | { 80 | return preg_match('/^https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/api\/webhooks\/\d+?\/.+$/', $url); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Adapters/Discord/DiscordException.php: -------------------------------------------------------------------------------- 1 | http = $res->getStatusCode(); 32 | $this->url = $url; 33 | 34 | $contents = $res->getBody()->getContents(); 35 | $body = json_decode($contents); 36 | $message = Arr::get($body, 'message') ?: $res->getReasonPhrase(); 37 | 38 | if (!Arr::get($body, 'message')) { 39 | $message .= ": $contents"; 40 | } 41 | 42 | parent::__construct( 43 | $message, 44 | Arr::get($body, 'code') 45 | ); 46 | } 47 | 48 | public function __toString() 49 | { 50 | return "HTTP $this->http – $this->code $this->message ($this->url)"; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Adapters/MicrosoftTeams/Adapter.php: -------------------------------------------------------------------------------- 1 | settings->get('theme_primary_color')); 41 | 42 | $this->request( 43 | $url, 44 | [ 45 | '@context' => 'https://schema.org/extensions', 46 | '@type' => 'MessageCard', 47 | 'themeColor' => $color->getRgb()->toHexString(), 48 | 49 | 'title' => $this->getTitle($response), 50 | 'text' => $response->getExtraText(), 51 | 'summary' => $response->title, 52 | 53 | 'sections' => [ 54 | $this->toArray($response), 55 | ], 56 | ] 57 | ); 58 | } 59 | 60 | /** 61 | * @param Response $response 62 | * 63 | * @return array 64 | */ 65 | public function toArray(Response $response): array 66 | { 67 | $user = $response->getAuthorUrl(); 68 | 69 | return [ 70 | 'title' => $response->title, 71 | 72 | 'activityImage' => $response->author->avatar_url, 73 | 'activityTitle' => $response->author->display_name, 74 | 'activitySubtitle' => "[$user]($user)", 75 | 'activityText' => $response->description, 76 | 77 | 'potentialAction' => [[ 78 | '@type' => 'OpenUri', 79 | 'name' => 'View', 80 | 'targets' => [[ 81 | 'os' => 'default', 82 | 'uri' => $response->url, 83 | ]], 84 | ]], 85 | ]; 86 | } 87 | 88 | /** 89 | * @param string $url 90 | * 91 | * @return bool 92 | */ 93 | public static function isValidURL(string $url): bool 94 | { 95 | return preg_match('/^https:\/\/\S+\.office\.com\/webhook(b2)?\/\S+@\S+\/IncomingWebhook\/\S+\/\S+$/i', $url); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Adapters/MicrosoftTeams/TeamsException.php: -------------------------------------------------------------------------------- 1 | http = $res->getStatusCode(); 31 | $this->url = $url; 32 | 33 | $error = $res->getBody()->getContents(); 34 | 35 | parent::__construct($error ?: $res->getReasonPhrase()); 36 | } 37 | 38 | public function __toString() 39 | { 40 | return "HTTP $this->http – $this->message ($this->url)"; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Adapters/Slack/Adapter.php: -------------------------------------------------------------------------------- 1 | request($url, [ 40 | 'username' => $this->getTitle($response), 41 | 'avatar_url' => $this->getAvatarUrl(), 42 | 'text' => $response->getExtraText(), 43 | 'attachments' => [ 44 | $this->toArray($response), 45 | ], 46 | ]); 47 | 48 | if ($res->getStatusCode() == 302) { 49 | throw new SlackException($res, $url); 50 | } 51 | } 52 | 53 | /** 54 | * @param Response $response 55 | * 56 | * @return array 57 | */ 58 | public function toArray(Response $response): array 59 | { 60 | $data = [ 61 | 'fallback' => $response->description.($response->author->exists ? ' - '.$response->author->display_name : ''), 62 | 'color' => $response->color, 63 | 'title' => $response->title, 64 | 'title_link' => $response->url, 65 | 'text' => $response->description, 66 | 'footer' => $this->settings->get('forum_title'), 67 | 'fields' => $response->getIncludeTags() ? [ 68 | [ 69 | 'title' => 'Tags', 70 | 'value' => implode(', ', $response->getTags()), 71 | 'short' => false, 72 | ], 73 | ] : null, 74 | ]; 75 | 76 | if ($response->author->exists) { 77 | $data['author_name'] = $response->author->display_name; 78 | $data['author_link'] = $response->getAuthorUrl(); 79 | $data['author_icon'] = $response->author->avatar_url; 80 | } 81 | 82 | return $data; 83 | } 84 | 85 | /** 86 | * @param string $url 87 | * 88 | * @return bool 89 | */ 90 | public static function isValidURL(string $url): bool 91 | { 92 | // allow any URL as multiple services support Slack webhook payloads 93 | return true; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Adapters/Slack/SlackException.php: -------------------------------------------------------------------------------- 1 | http = $res->getStatusCode(); 31 | $this->url = $url; 32 | 33 | if ($this->http == 302) { 34 | $this->http = 404; 35 | } 36 | 37 | $contents = $res->getBody()->getContents(); 38 | $body = json_decode($contents); 39 | 40 | if ($this->http == 404) { 41 | parent::__construct(resolve('translator')->trans('fof-webhooks.adapters.errors.404')); 42 | } else { 43 | parent::__construct(@$body->message ?? $contents, @$body->code); 44 | } 45 | } 46 | 47 | public function __toString() 48 | { 49 | $code = $this->code; 50 | $message = $code ? "$code $this->message" : $this->message; 51 | 52 | return "HTTP $this->http – $message ($this->url)"; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Api/Controller/CreateWebhookController.php: -------------------------------------------------------------------------------- 1 | bus = $bus; 40 | } 41 | 42 | /** 43 | * @param ServerRequestInterface $request 44 | * @param Document $document 45 | * 46 | * @return mixed 47 | */ 48 | protected function data(ServerRequestInterface $request, Document $document) 49 | { 50 | return $this->bus->dispatch( 51 | new CreateWebhook($request->getAttribute('actor'), Arr::get($request->getParsedBody(), 'data', [])) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Api/Controller/DeleteWebhookController.php: -------------------------------------------------------------------------------- 1 | bus = $bus; 33 | } 34 | 35 | /** 36 | * @param ServerRequestInterface $request 37 | */ 38 | protected function delete(ServerRequestInterface $request) 39 | { 40 | $this->bus->dispatch( 41 | new DeleteWebhook(Arr::get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Api/Controller/ListWebhooksController.php: -------------------------------------------------------------------------------- 1 | getAttribute('actor'); 39 | 40 | if (!$actor->isAdmin()) { 41 | throw new PermissionDeniedException(); 42 | } 43 | 44 | return Webhook::all(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Api/Controller/UpdateWebhookController.php: -------------------------------------------------------------------------------- 1 | bus = $bus; 40 | } 41 | 42 | /** 43 | * @param ServerRequestInterface $request 44 | * @param Document $document 45 | * 46 | * @return mixed 47 | */ 48 | protected function data(ServerRequestInterface $request, Document $document) 49 | { 50 | $id = Arr::get($request->getQueryParams(), 'id'); 51 | $actor = $request->getAttribute('actor'); 52 | $data = Arr::get($request->getParsedBody(), 'data', []); 53 | 54 | return $this->bus->dispatch( 55 | new UpdateWebhook($id, $actor, $data) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Api/Serializer/WebhookSerializer.php: -------------------------------------------------------------------------------- 1 | $webhook->id, 38 | 'service' => $webhook->service, 39 | 'url' => $webhook->url, 40 | 'error' => $webhook->error, 41 | 'events' => json_decode($webhook->events) ?: [], 42 | 43 | 'group_id' => $webhook->group_id, 44 | 'tag_id' => $webhook->tag_id, 45 | 'extra_text' => $webhook->extra_text ?: '', 46 | 'name' => $webhook->name ?: '', 47 | 48 | 'use_plain_text' => (bool) $webhook->use_plain_text, 49 | 'include_tags' => (bool) $webhook->include_tags, 50 | 'max_post_content_length' => ((int) $webhook->max_post_content_length) ?: null, 51 | 52 | 'is_valid' => $webhook->isValid(), 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Command/CreateWebhook.php: -------------------------------------------------------------------------------- 1 | actor = $actor; 39 | $this->data = $data; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Command/CreateWebhookHandler.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 32 | } 33 | 34 | /** 35 | * @param CreateWebhook $command 36 | * 37 | * @throws PermissionDeniedException 38 | * @throws \Illuminate\Validation\ValidationException 39 | * 40 | * @return Webhook 41 | */ 42 | public function handle(CreateWebhook $command): Webhook 43 | { 44 | $actor = $command->actor; 45 | $data = $command->data; 46 | 47 | $actor->assertAdmin(); 48 | 49 | $webhook = Webhook::build( 50 | Arr::get($data, 'attributes.service'), 51 | Arr::get($data, 'attributes.url') 52 | ); 53 | 54 | $this->validator->assertValid($webhook->getAttributes()); 55 | 56 | $webhook->save(); 57 | 58 | return $webhook; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Command/DeleteWebhook.php: -------------------------------------------------------------------------------- 1 | webhookId = $webhookId; 49 | $this->actor = $actor; 50 | $this->data = $data; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Command/DeleteWebhookHandler.php: -------------------------------------------------------------------------------- 1 | actor->assertAdmin(); 29 | 30 | Webhook::where('id', $command->webhookId)->first()->delete(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Command/UpdateWebhook.php: -------------------------------------------------------------------------------- 1 | webhookId = $webhookId; 49 | $this->actor = $actor; 50 | $this->data = $data; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Command/UpdateWebhookHandler.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 33 | } 34 | 35 | /** 36 | * @param UpdateWebhook $command 37 | * 38 | * @throws PermissionDeniedException 39 | * @throws \Illuminate\Validation\ValidationException 40 | * 41 | * @return Webhook 42 | */ 43 | public function handle(UpdateWebhook $command): Webhook 44 | { 45 | $actor = $command->actor; 46 | $data = $command->data; 47 | 48 | $actor->assertAdmin(); 49 | 50 | /** 51 | * @var Webhook $webhook 52 | */ 53 | $webhook = Webhook::findOrFail($command->webhookId); 54 | 55 | $service = Arr::get($data, 'attributes.service'); 56 | $url = Arr::get($data, 'attributes.url'); 57 | $events = Arr::get($data, 'attributes.events'); 58 | $groupId = Arr::get($data, 'attributes.group_id'); 59 | $usePlainText = Arr::get($data, 'attributes.use_plain_text'); 60 | $includeTags = Arr::get($data, 'attributes.include_tags'); 61 | $maxPostContentLength = Arr::get($data, 'attributes.max_post_content_length'); 62 | $name = Arr::get($data, 'attributes.name'); 63 | 64 | if (isset($service)) { 65 | $webhook->service = $service; 66 | } 67 | if (isset($url)) { 68 | $webhook->url = $url; 69 | $webhook->error = null; 70 | } 71 | if (isset($events)) { 72 | $webhook->events = is_array($events) ? json_encode($events) : $events; 73 | } 74 | if (isset($groupId)) { 75 | $webhook->group_id = $groupId; 76 | } 77 | 78 | if (Arr::has($data, 'attributes.extraText')) { 79 | $webhook->extra_text = trim(Arr::get($data, 'attributes.extraText')); 80 | } 81 | 82 | if (Arr::has($data, 'attributes.tag_id') && class_exists(Tag::class)) { 83 | $tagIds = Arr::get($data, 'attributes.tag_id'); 84 | 85 | $webhook->tag_id = $tagIds; 86 | } 87 | 88 | if (isset($usePlainText)) { 89 | $webhook->use_plain_text = $usePlainText; 90 | } 91 | 92 | if (isset($includeTags)) { 93 | $webhook->include_tags = $includeTags; 94 | } 95 | 96 | if (isset($maxPostContentLength)) { 97 | $webhook->max_post_content_length = $maxPostContentLength == 0 ? null : $maxPostContentLength; 98 | } 99 | 100 | if (isset($name)) { 101 | $webhook->name = trim($name); 102 | } 103 | 104 | $this->validator->assertValid($webhook->getDirty()); 105 | 106 | $webhook->save(); 107 | 108 | return $webhook; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Extend/FoFWebhooksExtender.php: -------------------------------------------------------------------------------- 1 | listeners[$clazz] = $action; 42 | } 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * @param Adapter|string $adapter 49 | * 50 | * @return $this 51 | */ 52 | public function adapter($adapter): FoFWebhooksExtender 53 | { 54 | $name = @constant("$adapter::NAME"); 55 | 56 | if (isset($name)) { 57 | $this->adapters[$name] = $adapter; 58 | } 59 | 60 | return $this; 61 | } 62 | 63 | public function extend(Container $container, Extension $extension = null) 64 | { 65 | if (TriggerListener::$listeners == null) { 66 | TriggerListener::setupDefaultListeners(); 67 | } 68 | 69 | foreach ($this->listeners as $action) { 70 | TriggerListener::addListener($action); 71 | } 72 | 73 | foreach ($this->adapters as $name => $adapter) { 74 | Adapters::add($name, $adapter); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Helpers/Post.php: -------------------------------------------------------------------------------- 1 | content; 29 | 30 | if (isset($webhook) && $post instanceof CommentPost) { 31 | $maxLength = $webhook->max_post_content_length; 32 | 33 | if ($webhook->use_plain_text) { 34 | $content = (new Html2Text($post->formatContent()))->getText(); 35 | } 36 | 37 | if ($maxLength) { 38 | $origLen = strlen($content); 39 | 40 | $content = trim(substr($content, 0, $maxLength)); 41 | 42 | if ($origLen > $maxLength + 1) { 43 | $content .= '...'; 44 | } 45 | } 46 | } 47 | 48 | return $content; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Jobs/HandleEvent.php: -------------------------------------------------------------------------------- 1 | name = $name; 35 | $this->event = $event; 36 | } 37 | 38 | public function handle() 39 | { 40 | $clazz = TriggerListener::$listeners[$this->name]; 41 | /** @var Action $action */ 42 | $action = (new ReflectionClass($clazz))->newInstance(); 43 | 44 | TriggerListener::debug("{$this->name}: handling with $clazz"); 45 | 46 | $this->send($this->name, $action); 47 | } 48 | 49 | /** 50 | * @param string $event_name 51 | * @param Action $action 52 | * 53 | * @throws \ReflectionException 54 | */ 55 | private function send(string $event_name, Action $action) 56 | { 57 | foreach (Webhook::all() as $webhook) { 58 | if ($webhook->events != null && !in_array($event_name, $webhook->getEvents())) { 59 | TriggerListener::debug("{$this->name}: webhook $webhook->id --> not subscribed"); 60 | continue; 61 | } 62 | 63 | if (!$webhook->isValid() || $action->ignore($webhook, $this->event)) { 64 | TriggerListener::debug("{$this->name}: webhook $webhook->id --> invalid URL / ignored event"); 65 | continue; 66 | } 67 | 68 | $response = $action->handle($webhook, $this->event); 69 | 70 | if (isset($response)) { 71 | TriggerListener::debug("{$this->name}: webhook $webhook->id --> sending response"); 72 | 73 | Adapters\Adapters::get($webhook->service)->handle($webhook, $response->withWebhook($webhook)); 74 | } else { 75 | TriggerListener::debug("{$this->name}: webhook $webhook->id --> no response"); 76 | } 77 | } 78 | } 79 | 80 | public function __serialize(): array 81 | { 82 | return [ 83 | 'name' => $this->name, 84 | 'event' => \Opis\Closure\serialize($this->event), 85 | ]; 86 | } 87 | 88 | public function __unserialize(array $values): void 89 | { 90 | $this->name = Arr::get($values, 'name'); 91 | $this->event = \Opis\Closure\unserialize(Arr::get($values, 'event')); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Legacy/Extend/ReflarWebhooksExtender.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | public static $listeners = null; 37 | 38 | /** 39 | * @var bool|null 40 | */ 41 | protected static $isDebugging = null; 42 | 43 | /** 44 | * EventListener constructor. 45 | * 46 | * @param SettingsRepositoryInterface $settings 47 | * @param Queue $queue 48 | */ 49 | public function __construct(SettingsRepositoryInterface $settings, Queue $queue) 50 | { 51 | $this->settings = $settings; 52 | $this->queue = $queue; 53 | 54 | if (self::$listeners == null) { 55 | self::setupDefaultListeners(); 56 | } 57 | } 58 | 59 | /** 60 | * Subscribes to the Flarum events. 61 | * 62 | * @param Dispatcher $events 63 | */ 64 | public function subscribe(Dispatcher $events) 65 | { 66 | $events->listen('*', [$this, 'run']); 67 | } 68 | 69 | /** 70 | * @param $name 71 | * @param $data 72 | * 73 | * @throws \ReflectionException 74 | */ 75 | public function run($name, $data) 76 | { 77 | $event = Arr::get($data, 0); 78 | 79 | if (!isset($event) || !array_key_exists($name, self::$listeners)) { 80 | return; 81 | } 82 | 83 | self::debug("$name: queuing"); 84 | 85 | $this->queue->push( 86 | new HandleEvent($name, $event) 87 | ); 88 | } 89 | 90 | public static function setupDefaultListeners() 91 | { 92 | self::addListener(Actions\Discussion\Deleted::class); 93 | self::addListener(Actions\Discussion\Hidden::class); 94 | self::addListener(Actions\Discussion\Renamed::class); 95 | self::addListener(Actions\Discussion\Restored::class); 96 | self::addListener(Actions\Discussion\Started::class); 97 | 98 | self::addListener(Actions\Group\Created::class); 99 | self::addListener(Actions\Group\Renamed::class); 100 | self::addListener(Actions\Group\Deleted::class); 101 | 102 | self::addListener(Actions\Post\Posted::class); 103 | self::addListener(Actions\Post\Revised::class); 104 | self::addListener(Actions\Post\Hidden::class); 105 | self::addListener(Actions\Post\Restored::class); 106 | self::addListener(Actions\Post\Deleted::class); 107 | self::addListener(Actions\Post\Approved::class); 108 | 109 | self::addListener(Actions\User\Renamed::class); 110 | self::addListener(Actions\User\Registered::class); 111 | self::addListener(Actions\User\Deleted::class); 112 | } 113 | 114 | public static function addListener(string $action) 115 | { 116 | $clazz = @constant("$action::EVENT"); 117 | 118 | if (isset($clazz) && class_exists($clazz)) { 119 | self::$listeners[$clazz] = $action; 120 | } elseif (!isset($clazz)) { 121 | echo "$action::EVENT does not exist"; 122 | } 123 | } 124 | 125 | public static function debug(string $message) 126 | { 127 | if (is_null(self::$isDebugging)) { 128 | self::$isDebugging = (bool) (int) resolve('flarum.settings')->get('fof-webhooks.debug'); 129 | } 130 | 131 | if (self::$isDebugging) { 132 | resolve('log')->info('[fof/webhooks] #DEBUG# '.$message); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Models/Webhook.php: -------------------------------------------------------------------------------- 1 | service = $service; 52 | $webhook->url = $url; 53 | $webhook->events = '[]'; 54 | 55 | return $webhook; 56 | } 57 | 58 | public function getEvents() 59 | { 60 | return isset($this->events) ? json_decode($this->events) : []; 61 | } 62 | 63 | public function isValid(): bool 64 | { 65 | $adapter = Adapters::get($this->service); 66 | 67 | return isset($adapter) && $adapter::isValidURL($this->url); 68 | } 69 | 70 | public function group(): BelongsTo 71 | { 72 | return $this->belongsTo(Group::class); 73 | } 74 | 75 | public function tags() 76 | { 77 | if (!class_exists(Tag::class)) { 78 | return null; 79 | } 80 | 81 | return Tag::whereIn('id', $this->tag_id)->get(); 82 | } 83 | 84 | public function appliedTags() 85 | { 86 | return Tag::select('name')->whereIn('id', $this->tag_id)->pluck('name')->toArray(); 87 | } 88 | 89 | public function getIncludeTags(): bool 90 | { 91 | return $this->include_tags; 92 | } 93 | 94 | public function asGuest(): bool 95 | { 96 | $group = $this->group; 97 | 98 | return !$group || $group->id == Group::GUEST_ID; 99 | } 100 | 101 | public function getTagIdAttribute($value): array 102 | { 103 | if (is_numeric($value)) { 104 | return [$value]; 105 | } elseif (is_array($value)) { 106 | return $value; 107 | } elseif (!$value) { 108 | return []; 109 | } 110 | 111 | return json_decode($value) ?? []; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | event = $event; 76 | $this->urlGenerator = resolve(UrlGenerator::class); 77 | } 78 | 79 | public function setTitle(string $title): self 80 | { 81 | $this->title = $title; 82 | 83 | return $this; 84 | } 85 | 86 | public function setURL(string $name, array $data = null, ?string $extra = null): self 87 | { 88 | $url = $this->urlGenerator->to('forum')->route($name, $data); 89 | 90 | if (isset($extra)) { 91 | $url = $url.$extra; 92 | } 93 | 94 | $this->url = $url; 95 | 96 | return $this; 97 | } 98 | 99 | public function setDescription(?string $description): self 100 | { 101 | $this->description = $description; 102 | 103 | return $this; 104 | } 105 | 106 | public function setAuthor(User $author): self 107 | { 108 | $this->author = $author; 109 | 110 | return $this; 111 | } 112 | 113 | public function setColor(?string $color): self 114 | { 115 | $this->color = $color; 116 | 117 | return $this; 118 | } 119 | 120 | public function setTimestamp(?string $timestamp): self 121 | { 122 | $this->timestamp = $timestamp ?: Carbon::now(); 123 | 124 | return $this; 125 | } 126 | 127 | public function getColor() 128 | { 129 | return $this->color ? hexdec(substr($this->color, 1)) : null; 130 | } 131 | 132 | public static function build($event): self 133 | { 134 | return new self($event); 135 | } 136 | 137 | public function getAuthorUrl(): ?string 138 | { 139 | return $this->author->exists ? $this->urlGenerator->to('forum')->route('user', [ 140 | 'username' => $this->author->username, 141 | ]) : null; 142 | } 143 | 144 | public function getExtraText(): ?string 145 | { 146 | return $this->webhook->extra_text; 147 | } 148 | 149 | public function getIncludeTags(): bool 150 | { 151 | return $this->webhook->include_tags; 152 | } 153 | 154 | public function getTags(): ?array 155 | { 156 | return $this->webhook->appliedTags(); 157 | } 158 | 159 | public function getWebhookName(): ?string 160 | { 161 | return $this->webhook->name; 162 | } 163 | 164 | public function withWebhook(Webhook $webhook): self 165 | { 166 | $this->setWebhook($webhook); 167 | 168 | return $this; 169 | } 170 | 171 | protected function setWebhook(Webhook $webhook) 172 | { 173 | $this->webhook = $webhook; 174 | } 175 | 176 | public function __toString() 177 | { 178 | return "Response{title=$this->title,url=$this->url,author={$this->author->display_name}}"; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Validator/WebhookValidator.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'required', 21 | 'string', 22 | ], 23 | 'url' => [ 24 | 'required', 25 | 'string', 26 | 'url', 27 | ], 28 | 'group_id' => [ 29 | 'nullable', 30 | 'int', 31 | 'in:1,2', 32 | ], 33 | 'tag_id' => [ 34 | 'nullable', 35 | 'array', 36 | 'exists:tags,id', 37 | ], 38 | 'name' => [ 39 | 'string', 40 | 'nullable', 41 | ], 42 | ]; 43 | } 44 | --------------------------------------------------------------------------------