diff --git a/config/integrations/dimension_demo_bot.yaml b/config/integrations/dimension_demo_bot.yaml index 4dd7612..25d7e01 100644 --- a/config/integrations/dimension_demo_bot.yaml +++ b/config/integrations/dimension_demo_bot.yaml @@ -4,7 +4,7 @@ enabled: false userId: "@dimension:t2bot.io" name: "Demo Bot" about: "A bot that has no functionality. This is just a demonstration on the config." -avatar: "/img/avatars/demobot.png" +avatar: "img/avatars/demobot.png" hosted: homeserverUrl: "https://t2bot.io" accessToken: "your_matrix_access_token_here" diff --git a/config/integrations/giphy.yaml b/config/integrations/giphy.yaml index 379474c..f2a78b4 100644 --- a/config/integrations/giphy.yaml +++ b/config/integrations/giphy.yaml @@ -4,7 +4,7 @@ enabled: true userId: "@neb_giphy:matrix.org" name: "Giphy" about: "Use `!giphy query` to find an animated GIF on demand" -avatar: "/img/avatars/giphy.png" +avatar: "img/avatars/giphy.png" upstream: type: "vector" id: "giphy" \ No newline at end of file diff --git a/config/integrations/google.yaml b/config/integrations/google.yaml index 2ca8a09..95aa235 100644 --- a/config/integrations/google.yaml +++ b/config/integrations/google.yaml @@ -4,7 +4,7 @@ enabled: true userId: "@_neb_google:matrix.org" name: "Google" about: "Use `!google image query` to find an image from Google" -avatar: "/img/avatars/google.png" +avatar: "img/avatars/google.png" upstream: type: "vector" id: "google" \ No newline at end of file diff --git a/config/integrations/guggy.yaml b/config/integrations/guggy.yaml index 688c08d..b6c347f 100644 --- a/config/integrations/guggy.yaml +++ b/config/integrations/guggy.yaml @@ -4,7 +4,7 @@ enabled: true userId: "@_neb_guggy:matrix.org" name: "Guggy" about: "Use `!guggy sentence` to create an animated GIF from a sentence" -avatar: "/img/avatars/guggy.png" +avatar: "img/avatars/guggy.png" upstream: type: "vector" id: "guggy" \ No newline at end of file diff --git a/config/integrations/imgur.yaml b/config/integrations/imgur.yaml index 9bfd39e..4035348 100644 --- a/config/integrations/imgur.yaml +++ b/config/integrations/imgur.yaml @@ -4,7 +4,7 @@ enabled: true userId: "@_neb_imgur:matrix.org" name: "Imgur" about: "Use `!imgur query` to find an image from Imgur" -avatar: "/img/avatars/imgur.png" +avatar: "img/avatars/imgur.png" upstream: type: "vector" id: "imgur" \ No newline at end of file diff --git a/config/integrations/irc-bridge.yaml b/config/integrations/irc-bridge.yaml new file mode 100644 index 0000000..33d0b94 --- /dev/null +++ b/config/integrations/irc-bridge.yaml @@ -0,0 +1,10 @@ +type: "bridge" +integrationType: "irc" +enabled: true +name: "IRC" +about: "Bridges IRC channels to the room" +avatar: "img/avatars/irc.png" +requirements: + joinRule: 'public' +upstream: + type: "vector" \ No newline at end of file diff --git a/config/integrations/pollbot.yaml b/config/integrations/pollbot.yaml index b601eeb..25a042f 100644 --- a/config/integrations/pollbot.yaml +++ b/config/integrations/pollbot.yaml @@ -4,7 +4,7 @@ enabled: false userId: "@pollbot:t2bot.io" name: "Poll Bot" about: "A bot to poll users in a room. Use `!pollhelp` for more information" -avatar: "/img/avatars/pollbot.png" +avatar: "img/avatars/pollbot.png" hosted: homeserverUrl: "https://t2bot.io" accessToken: "your_matrix_access_token_here" diff --git a/config/integrations/rssbot.yaml b/config/integrations/rssbot.yaml index b0ae410..332a77f 100644 --- a/config/integrations/rssbot.yaml +++ b/config/integrations/rssbot.yaml @@ -3,6 +3,6 @@ integrationType: "rss" enabled: true name: "RSS Bot" about: "Tracks any Atom/RSS feed and sends new items into this room" -avatar: "/img/avatars/rssbot.png" +avatar: "img/avatars/rssbot.png" upstream: type: "vector" \ No newline at end of file diff --git a/config/integrations/wikipedia.yaml b/config/integrations/wikipedia.yaml index de893dc..4090ec6 100644 --- a/config/integrations/wikipedia.yaml +++ b/config/integrations/wikipedia.yaml @@ -4,7 +4,7 @@ enabled: true userId: "@_neb_wikipedia:matrix.org" name: "Wikipedia" about: "Use `!wikipedia query` to find something from Wikipedia" -avatar: "/img/avatars/wikipedia.png" +avatar: "img/avatars/wikipedia.png" upstream: type: "vector" id: "wikipedia" \ No newline at end of file diff --git a/docs/dimension_api.md b/docs/dimension_api.md index ae109f1..0d96b18 100644 --- a/docs/dimension_api.md +++ b/docs/dimension_api.md @@ -1,5 +1,121 @@ # Dimension API -This document describes the various endpoints of Dimension and how to interact with the API. +Dimension has its own API that allows for management of integrations in Riot/Matrix. -Coming soon? \ No newline at end of file +## Types of integrations + +### Simple Bots + +* Can only be in a room or not +* No state information held + +### Complex Bots + +* Simple Bots that hold state information + +### Bridges + +* Manage their own state through dedicated API endpoints + +## Endpoints + +### `GET /api/v1/dimension/integrations/{roomId}?scalar_token=your_token_here` + +**Parameters** +* `{roomId}` - The room ID to get integrations for +* `scalar_token` - The scalar (dimension) token to authenticate with + +**Example Response** +``` +TODO +``` + +### `DELETE /api/v1/dimension/integrations/{roomId}/{type}/{integrationType}?scalar_token=your_token_here` + +**Parameters** +* `{roomId}` - The room ID to remove the integration from +* `{type}` - The integration type (eg: `bot`, `complex-bot`, `bridge`, etc) +* `{integrationType}` - The integration subtype (eg: `irc`, `rssbot`, `giphy`, etc) +* `scalar_token` - The scalar (dimension) token to authenticate with + +**Example Response** +``` +TODO +``` + +### `PUT /api/v1/dimension/integrations/{roomId}/{type}/{integrationType}/state` + +**Parameters** +* `{roomId}` - The room ID to update the integration state in +* `{type}` - The integration type (eg: `bot`, `complex-bot`, `bridge`, etc) +* `{integrationType}` - The integration subtype (eg: `irc`, `rssbot`, `giphy`, etc) + +**Example Body** +``` +{ + "scalar_token": "your_token_here", + "state": { + // integration specific state goes here + } +} +``` + +### `GET /api/v1/dimension/integrations/{roomId}/{type}/{integrationType}/state` + +**Parameters** +* `{roomId}` - The room ID to get the integration state in +* `{type}` - The integration type (eg: `bot`, `complex-bot`, `bridge`, etc) +* `{integrationType}` - The integration subtype (eg: `irc`, `rssbot`, `giphy`, etc) + +**Response** + +An object representing the integration-specific state. See the documentation for the desired integration for more information. + +## Integration State Information + +### Simple Bots + +Do not hold state. + +### Complex Bots + +#### RSS Bot +``` +{ + // Mutable using state API + "feeds": [ + "https://some.domain.com/feed.rss", + "https://some.domain.com/another_feed.rss" + ], + + // Read only. Controlled by other users. + "immutableFeeds": [ + "https://some.domain.com/third_feed.rss", + "https://some.domain.com/fourth_feed.rss" + ] +} +``` + +### Bridges + +#### IRC +``` +{ + // Read only + "availableNetworks": [ + {"name": "Freenode", "id": "freenode"}, + {"name": "EsperNet", "id": "espernet"}, + {"name": "OFTC", "id": "oftc"} + ], + + // Read only. Use IRC API to mutate + "channels": { + "freenode": [ + "#dimensiontesting", + "#dimensiontest" + ], + "espernet": [], + "oftc": [] + } +} +``` \ No newline at end of file diff --git a/docs/integrations/irc_bridge.md b/docs/integrations/irc_bridge.md new file mode 100644 index 0000000..36fa71c --- /dev/null +++ b/docs/integrations/irc_bridge.md @@ -0,0 +1,47 @@ +# Dimension IRC Bridge API + +As with most bridges, the IRC bridge uses a dedicated set of API endpoints to manage the state of the bridge. The IRC bridge still uses the state API provided by Dimension to report basic state information, but does not allow edits through the regular API. Instead, it is expected that the IRC API be used to mutate the state. + +## Getting available networks/bridged channels + +Make a call to the Dimension state API: `GET /api/v1/dimension/integrations/{roomId}/bridge/irc/state?scalar_token=...`. + +*Example state* +``` +{ + "availableNetworks": [ + {"name": "Freenode", "id": "freenode"}, + {"name": "Espernet", "id": "espernet"}, + {"name": "OFTC", "id": "oftc"} + ], + "channels": { + "freenode": [ + "#dimensiontesting", + "#dimensiontest" + ], + "espernet": [], + "oftc": [] + } +} +``` + +## Getting the OPs in a channel + +IRC API Endpoint: `GET /api/v1/irc/{network}/{channel}/ops?scalar_token=...`. Be sure to encode the channel parameter. + +*Example response* +``` +["turt2live", "johndoe"] +``` + +## Linking a new channel + +IRC API Endpoint: `PUT /api/v1/irc/{roomId}/channels/{network}/{channel}?op=turt2live&scalar_token=...`. Be sure to encode the channel parameter. + +A 200 OK is returned if the request to add the channel was sent. The channel will not appear in the state information until the op has approved the bridge. + +## Unlinking a channel + +IRC API Endpoint: `DELETE /api/v1/irc/{roomId}/channels/{network}/{channel}?scalar_token=...`. Be sure to encode the channel parameter. + +A 200 OK is returned if the delete was successful. \ No newline at end of file diff --git a/docs/scalar_server_api.md b/docs/scalar_server_api.md index 2b0f88f..8195851 100644 --- a/docs/scalar_server_api.md +++ b/docs/scalar_server_api.md @@ -178,4 +178,146 @@ None of these are officially documented, and are subject to change. } } } +``` + +## GET `/api/bridges/irc/_matrix/provision/querynetworks?scalar_token=...` + +**Response** +``` +{ + "replies": [ + { + "rid": "...", + "response": { + "servers": [ + { + "bot_user_id": "@appservice-irc:matrix.org", + "desc": "Freenode", + "fields": { + "domain": "chat.freenode.net" + }, + "icon": "https:\/\/matrix.org\/_matrix\/media\/v1\/download\/matrix.org\/DHLHpDDgWNNejFmrewvwEAHX", + "network_id": "freenode" + } + ] + } + }, + { + "rid": "...", + "response": { + "servers": [ + { + "bot_user_id": "@mozilla-irc:matrix.org", + "desc": "Moznet", + "fields": { + "domain": "irc.mozilla.org" + }, + "icon": "https:\/\/matrix.org\/_matrix\/media\/v1\/download\/matrix.org\/DHLHpDDgWNNejFmrewvwEAHX", + "network_id": "mozilla" + } + ] + } + } + ] +} +``` + +## POST `/api/bridges/irc/_matrix/provision/querylink?rid=...&scalar_token=...` + +**Body** +``` +{ + "remote_room_channel": "#dimensiontesting", + "remote_room_server": "chat.freenode.net" +} +``` + +**Response** +``` +{ + "replies": [{ + "rid": "...", + "response": { + "operators": ["travis-test"] + } + }] +} +``` + +## POST `/api/bridges/irc/_matrix/provision/link?rid=...&scalar_token=...` + +**Body** +``` +{ + "matrix_room_id": "!JmvocvDuPTYUfuvKgs:t2l.io", + "remote_room_channel": "#dimensiontesting", + "remote_room_server": "chat.freenode.net", + "op_nick": "travis-test", + "key": "" +} +``` + +**Response** +``` +{ + "replies": [{ + "rid": "...", + "response":{} + }] +} +``` + +*Note*: This returns 200 OK after sending the request to link. If the link succeeds, `listlinks` will show as such. + +## GET `/api/bridges/irc/_matrix/provision/listlinks/{roomId}?scalar_token=...` + +**Params** +* `{roomId}` - the matrix room id (ie: `!JmvocvDuPTYUfuvKgs:t2l.io`) + +**Response** +``` +{ + "replies": [ + { + "rid": "...", + "response": [ + { + "matrix_room_id": "!JmvocvDuPTYUfuvKgs:t2l.io", + "remote_room_channel": "#dimensiontesting", + "remote_room_server": "chat.freenode.net" + } + ] + }, + { + "rid": "...", + "response": [] + } + ] +} +``` + +*Note*: This is called on a timer in Scalar to show when a user has approved a link. Called every few seconds. + +## POST `/api/bridges/irc/_matrix/provision/unlink?rid=...&scalar_token=...` + +**Body** +``` +{ + "matrix_room_id": "!JmvocvDuPTYUfuvKgs:t2l.io", + "remote_room_channel": "#dimensiontest", + "remote_room_server": "chat.freenode.net", + "rid": "..." +} +``` + +**Response** +``` +{ + "replies": [ + { + "rid": "..", + "response": {} + } + ] +} ``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5daa62f..32ab35d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,48 +4,57 @@ "lockfileVersion": 1, "dependencies": { "@angular/animations": { - "version": "https://registry.npmjs.org/@angular/animations/-/animations-4.1.3.tgz", - "integrity": "sha1-bomh4Pv9bQ6Qvk8q4ZCqxn+DpBE=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-4.2.1.tgz", + "integrity": "sha1-TIeIGS8Ux06wKg/I3Dhxd6ux/yQ=", "dev": true }, "@angular/common": { - "version": "https://registry.npmjs.org/@angular/common/-/common-4.1.3.tgz", - "integrity": "sha1-58R5HjITHPdMI5QowqZ9qrLu8Bc=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-4.2.1.tgz", + "integrity": "sha1-yzNOO3H00+nzS3rZQsVI0/k238g=", "dev": true }, "@angular/compiler": { - "version": "https://registry.npmjs.org/@angular/compiler/-/compiler-4.1.3.tgz", - "integrity": "sha1-0t0whTsM9KVHWLSjFGMsIx+clMM=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-4.2.1.tgz", + "integrity": "sha1-jXe0WOBLiHlH2FrczXVG5KmH1LA=", "dev": true }, "@angular/core": { - "version": "https://registry.npmjs.org/@angular/core/-/core-4.1.3.tgz", - "integrity": "sha1-KFSY64arfQtvmC+Pn0h+9hABOzU=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-4.2.1.tgz", + "integrity": "sha1-CGUwSz30MEaRm2RvAv3tqFoMRoU=", "dev": true }, "@angular/forms": { - "version": "https://registry.npmjs.org/@angular/forms/-/forms-4.1.3.tgz", - "integrity": "sha1-OAq0w6+ExdHXSMKn0EFRx9yOSYI=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-4.2.1.tgz", + "integrity": "sha1-rRcs/TBwdDsqeoFh0sdax/W2H2I=", "dev": true }, "@angular/http": { - "version": "https://registry.npmjs.org/@angular/http/-/http-4.1.3.tgz", - "integrity": "sha1-650cMCoBcoFfmlczENm+C964Ra4=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-4.2.1.tgz", + "integrity": "sha1-2ebjSMh1PTGMdh1/S02fBf7EDvk=", "dev": true }, "@angular/platform-browser": { - "version": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-4.1.3.tgz", - "integrity": "sha1-T6HbURndF4sxXdrlsym+4akypb0=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-4.2.1.tgz", + "integrity": "sha1-HwHU0x+jkCEYJiZs9uOj9imGDx0=", "dev": true }, "@angular/platform-browser-dynamic": { - "version": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.1.3.tgz", - "integrity": "sha1-PBP9z1kdSH9u/cHUaRPygMbYwuw=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.2.1.tgz", + "integrity": "sha1-CDLSG6NIHZH0BWxqlUKfn892Ukw=", "dev": true }, "@angular/router": { - "version": "https://registry.npmjs.org/@angular/router/-/router-4.1.3.tgz", - "integrity": "sha1-3a/UaufMyLH3SQT/tF85TkRiUhY=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-4.2.1.tgz", + "integrity": "sha1-tkoNh+XNAi89PFLsi3urkLLj8Zg=", "dev": true }, "@angularclass/hmr": { @@ -59,7 +68,8 @@ "dev": true }, "@ng-bootstrap/ng-bootstrap": { - "version": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-alpha.26.tgz", + "version": "1.0.0-alpha.26", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-alpha.26.tgz", "integrity": "sha1-89nha1aC7CDts/E5fT1FpXbz9qQ=", "dev": true }, @@ -68,8 +78,9 @@ "integrity": "sha1-sC0QqwKOKSisWSoFGqpJgaGUHQM=" }, "@types/node": { - "version": "https://registry.npmjs.org/@types/node/-/node-7.0.22.tgz", - "integrity": "sha1-RZP02Ci91hKSlHjqQMZ7T0A8olU=", + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.29.tgz", + "integrity": "sha512-+8JrLZny/uR+d/jLK9eaV63buRM7X/gNzQk57q76NS4KNKLSKOmxJYFIlwuP2zDvA7wqZj05POPhSd9Z1hYQpQ==", "dev": true }, "abbrev": { @@ -533,6 +544,11 @@ "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", "dev": true }, + "cls-bluebird": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.0.1.tgz", + "integrity": "sha1-wlmkgK4CwOUGE0MHuxPbMERu4uc=" + }, "co": { "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" @@ -720,17 +736,20 @@ "dev": true }, "css-loader": { - "version": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.3.tgz", - "integrity": "sha1-n9XguMQFtt+Se6EQOIcBXTYGQM4=", + "version": "0.28.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.4.tgz", + "integrity": "sha1-bPNXkZLONV6LONX0Ldeh8uyJjQ8=", "dev": true, "dependencies": { "json5": { - "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, "loader-utils": { - "version": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", "dev": true } @@ -920,10 +939,6 @@ "version": "https://registry.npmjs.org/dotenv/-/dotenv-2.0.0.tgz", "integrity": "sha1-vXWcNXqqcDZeAclrewvsCKbg2Uk=" }, - "dottie": { - "version": "https://registry.npmjs.org/dottie/-/dottie-1.1.1.tgz", - "integrity": "sha1-RcKj9IvWUo7u0memmoSOqspvqmo=" - }, "ecc-jsbn": { "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", @@ -962,6 +977,11 @@ "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", "dev": true }, + "env-cmd": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-5.1.0.tgz", + "integrity": "sha1-AjbbOTw/AzAFIE/NCpLuQHI6nJ4=" + }, "errno": { "version": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", @@ -1089,17 +1109,20 @@ "dev": true }, "file-loader": { - "version": "https://registry.npmjs.org/file-loader/-/file-loader-0.11.1.tgz", - "integrity": "sha1-azKO4SNKcp5OR9Njdd1tNcDh24Q=", + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-0.11.2.tgz", + "integrity": "sha512-N+uhF3mswIFeziHQjGScJ/yHXYt3DiLBeC+9vWW+WjUBiClMSOlV1YrXQi+7KM2aA3Rn4Bybgv+uXFQbfkzpvg==", "dev": true, "dependencies": { "json5": { - "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, "loader-utils": { - "version": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", "dev": true } @@ -1903,10 +1926,6 @@ "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", "dev": true }, - "generic-pool": { - "version": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.2.tgz", - "integrity": "sha1-iGvFvwvrfblugby7oHiBjeWmJoM=" - }, "get-caller-file": { "version": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", @@ -2137,6 +2156,26 @@ "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", "dev": true }, + "icss-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", + "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", + "dev": true, + "dependencies": { + "postcss": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.1.tgz", + "integrity": "sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true + } + } + }, "ieee754": { "version": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", @@ -2208,6 +2247,11 @@ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dev": true }, + "is-bluebird": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", + "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" + }, "is-buffer": { "version": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", @@ -2531,8 +2575,9 @@ "dev": true }, "matrix-js-sdk": { - "version": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.7.8.tgz", - "integrity": "sha1-TUZSCMvzZCZzw2/l8ZFhHFkW8vk=" + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-0.7.10.tgz", + "integrity": "sha1-VPo8xuOwqlkkMOmEnuVl2vfGQEk=" }, "media-typer": { "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2779,11 +2824,6 @@ "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", "dev": true }, - "olm": { - "version": "https://matrix.org/packages/npm/olm/olm-2.2.1.tgz", - "integrity": "sha1-Xl21DQoUK3x6BlDZs9isw9N+aXs=", - "optional": true - }, "on-finished": { "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" @@ -3815,12 +3855,39 @@ } }, "sequelize": { - "version": "https://registry.npmjs.org/sequelize/-/sequelize-3.30.4.tgz", - "integrity": "sha1-vaLfHjGFSwmeQUmhEen8Clyh0aQ=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-4.0.0.tgz", + "integrity": "sha512-sKJHt36Leyzlhsy2g8b2Q5DQxM4F8aIVFOohjnIC6d6rtutWmLjhW/7H2ZEqG8n/3hbK1mfSBnKqZCO7+1sJYA==", "dependencies": { - "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-4.12.0.tgz", - "integrity": "sha1-K9bcRqBA9Z5obJcu0h2T3FkFMlg=" + "@types/node": { + "version": "6.0.78", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.78.tgz", + "integrity": "sha512-+vD6E8ixntRzzZukoF3uP1iV+ZjVN3koTcaeK+BEoc/kSfGbLDIGC7RmCaUgVpUfN6cWvfczFRERCyKM9mkvXg==" + }, + "dottie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.0.tgz", + "integrity": "sha1-2hkZgci41xPKARXViYzzl8Lw3dA=" + }, + "generic-pool": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.1.7.tgz", + "integrity": "sha1-2sIrLHp6BOQXMvfY0tJaMDyI9mI=" + }, + "inflection": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.10.0.tgz", + "integrity": "sha1-W//LEZetPoEFD44X4hZoCH7p6y8=" + }, + "validator": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-6.3.0.tgz", + "integrity": "sha1-R84j7Y1Ord+p1LjvAHG2zxB418g=" + }, + "wkx": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.1.tgz", + "integrity": "sha1-L8FxtenLVcYlb+9L3h8hvkE77+4=" } } }, @@ -3877,8 +3944,9 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "shelljs": { - "version": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.7.tgz", - "integrity": "sha1-svXHfvlxSPS09uImguELuoZnz/E=", + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", "dev": true }, "shimmer": { @@ -4018,17 +4086,20 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "style-loader": { - "version": "https://registry.npmjs.org/style-loader/-/style-loader-0.18.1.tgz", - "integrity": "sha1-avyolTyEKDDl4tyEeWMJiAqX9+g=", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.18.2.tgz", + "integrity": "sha512-WPpJPZGUxWYHWIUMNNOYqql7zh85zGmr84FdTVWq52WTIkqlW9xSxD3QYWi/T31cqn9UNSsietVEgGn2aaSCzw==", "dev": true, "dependencies": { "json5": { - "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, "loader-utils": { - "version": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", "dev": true } @@ -4126,9 +4197,18 @@ "dev": true }, "tslint": { - "version": "https://registry.npmjs.org/tslint/-/tslint-5.3.2.tgz", - "integrity": "sha1-5WRZ+wlacwfxA7hAUhdPXju+9u0=", - "dev": true + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.4.3.tgz", + "integrity": "sha1-dhyEArgONHt3M6BDkKdXslNYBGc=", + "dev": true, + "dependencies": { + "tsutils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.4.0.tgz", + "integrity": "sha1-rUzm26Dlo+2934Ymt8oEB4IYn+o=", + "dev": true + } + } }, "tslint-loader": { "version": "https://registry.npmjs.org/tslint-loader/-/tslint-loader-3.5.3.tgz", @@ -4147,11 +4227,6 @@ } } }, - "tsutils": { - "version": "https://registry.npmjs.org/tsutils/-/tsutils-2.2.0.tgz", - "integrity": "sha1-IYYUZX8hxnfkU2tLp12vjrzhs2c=", - "dev": true - }, "tty-browserify": { "version": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", @@ -4175,8 +4250,9 @@ "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" }, "typescript": { - "version": "https://registry.npmjs.org/typescript/-/typescript-2.3.3.tgz", - "integrity": "sha1-ljnzw7QBSOjKl/4IpR3RiRu2viI=", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.3.4.tgz", + "integrity": "sha1-PTgyGCgjHkNPKHUUlZw3qCtin0I=", "dev": true }, "uglify-js": { @@ -4291,10 +4367,6 @@ "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", "dev": true }, - "validator": { - "version": "https://registry.npmjs.org/validator/-/validator-5.7.0.tgz", - "integrity": "sha1-eoelgUa2laxIYHEUHAxJ1n2gXlw=" - }, "vary": { "version": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=" @@ -4474,10 +4546,6 @@ } } }, - "wkx": { - "version": "https://registry.npmjs.org/wkx/-/wkx-0.2.0.tgz", - "integrity": "sha1-dsJPFqzQzY+TzTSqMx4PeWElboQ=" - }, "wordwrap": { "version": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" @@ -4535,8 +4603,9 @@ } }, "zone.js": { - "version": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.11.tgz", - "integrity": "sha1-dCvvsX+8SaVxcSuMfYfljKJv2IY=", + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.12.tgz", + "integrity": "sha1-hv9QU8mK7CkaC/S7rFAdaUoFz7s=", "dev": true } } diff --git a/package.json b/package.json index b698d5b..0f2c2bc 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "express": "^4.15.2", "js-yaml": "^3.8.2", "lodash": "^4.17.4", - "matrix-js-sdk": "^0.7.8", + "matrix-js-sdk": "^0.7.10", "moment": "^2.18.1", "random-string": "^0.2.0", "request": "^2.81.0", @@ -31,19 +31,19 @@ "winston": "^2.3.1" }, "devDependencies": { - "@angular/animations": "^4.1.3", - "@angular/common": "4.1.3", - "@angular/compiler": "4.1.3", - "@angular/core": "4.1.3", - "@angular/forms": "4.1.3", - "@angular/http": "4.1.3", - "@angular/platform-browser": "4.1.3", - "@angular/platform-browser-dynamic": "4.1.3", - "@angular/router": "4.1.3", + "@angular/animations": "^4.2.0", + "@angular/common": "^4.2.1", + "@angular/compiler": "^4.2.1", + "@angular/core": "^4.2.1", + "@angular/forms": "^4.2.1", + "@angular/http": "^4.2.1", + "@angular/platform-browser": "^4.2.1", + "@angular/platform-browser-dynamic": "^4.2.1", + "@angular/router": "^4.2.1", "@angularclass/hmr": "^1.2.2", "@angularclass/hmr-loader": "^3.0.2", - "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.22", - "@types/node": "^7.0.18", + "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.26", + "@types/node": "^7.0.29", "angular2-modal": "^2.0.3", "angular2-template-loader": "^0.6.2", "angular2-toaster": "^4.0.0", @@ -53,10 +53,10 @@ "codelyzer": "^3.0.1", "copy-webpack-plugin": "^4.0.1", "core-js": "^2.4.1", - "css-loader": "^0.28.0", + "css-loader": "^0.28.4", "cssnano": "^3.10.0", "extract-text-webpack-plugin": "^2.1.2", - "file-loader": "^0.11.1", + "file-loader": "^0.11.2", "html-loader": "^0.4.5", "html-webpack-plugin": "^2.28.0", "jquery": "^3.2.1", @@ -71,14 +71,14 @@ "rimraf": "^2.6.1", "rxjs": "^5.2.0", "sass-loader": "^6.0.3", - "shelljs": "^0.7.0", - "style-loader": "^0.18.1", + "shelljs": "^0.7.8", + "style-loader": "^0.18.2", "ts-helpers": "^1.1.2", - "tslint": "^5.2.0", + "tslint": "^5.4.3", "tslint-loader": "^3.4.3", - "typescript": "^2.2.2", + "typescript": "^2.3.4", "url-loader": "^0.5.8", "webpack": "^2.3.2", - "zone.js": "^0.8.5" + "zone.js": "^0.8.12" } } diff --git a/src/DimensionApi.js b/src/DimensionApi.js index d85ac3d..66cd375 100644 --- a/src/DimensionApi.js +++ b/src/DimensionApi.js @@ -50,14 +50,15 @@ class DimensionApi { var promises = []; _.forEach(integrations, integration => { promises.push(this._getIntegration(integration, roomId, scalarToken).then(builtIntegration => { - return builtIntegration.getUserId().then(userId => { - integration.userId = userId; - return builtIntegration.getState(); - }).then(state => { + return builtIntegration.getState().then(state => { var keys = _.keys(state); for (var key of keys) { integration[key] = state[key]; } + + return builtIntegration.getUserId(); + }).then(userId => { + integration.userId = userId; }); })); }); diff --git a/src/integration/generic_types/Bridge.js b/src/integration/generic_types/Bridge.js new file mode 100644 index 0000000..85e4cd5 --- /dev/null +++ b/src/integration/generic_types/Bridge.js @@ -0,0 +1,30 @@ +var IntegrationStub = require("./IntegrationStub"); + +/** + * Represents a bridge. Normally bridges have enhanced configuration and requirements over bots. + */ +class Bridge extends IntegrationStub { + + /** + * Creates a new bridge + * @param bridgeConfig the configuration for the bridge + */ + constructor(bridgeConfig) { + super(bridgeConfig); + } + + /** + * Registers the API routes for this bridge with the given app. + * @param app the app to register the routes on + */ + registerApi(app) { + // nothing + } + + /*override*/ + getUserId() { + return null; // bridges don't have bot users we care about + } +} + +module.exports = Bridge; \ No newline at end of file diff --git a/src/integration/impl/index.js b/src/integration/impl/index.js index b44c01d..93a1f92 100644 --- a/src/integration/impl/index.js +++ b/src/integration/impl/index.js @@ -2,16 +2,21 @@ var log = require("../../util/LogService"); var StubbedFactory = require("./StubbedFactory"); var SimpleBotFactory = require("./simple_bot/SimpleBotFactory"); var RSSFactory = require("./rss/RSSFactory"); +var IRCFactory = require("./irc/IRCFactory"); var mapping = { "complex-bot": { "rss": RSSFactory + }, + "bridge": { + "irc": IRCFactory } }; var defaultFactories = { "complex-bot": null, - "bot": SimpleBotFactory + "bot": SimpleBotFactory, + "bridge": null }; module.exports = { diff --git a/src/integration/impl/irc/IRCBridge.js b/src/integration/impl/irc/IRCBridge.js new file mode 100644 index 0000000..927f3cb --- /dev/null +++ b/src/integration/impl/irc/IRCBridge.js @@ -0,0 +1,44 @@ +var Bridge = require("../../generic_types/Bridge"); + +/** + * Represents an IRC bridge + */ +class IRCBridge extends Bridge { + + /** + * Creates a new IRC bridge + * @param bridgeConfig the bridge configuration + * @param backbone the backbone powering this bridge + */ + constructor(bridgeConfig, backbone) { + super(bridgeConfig); + this._backbone = backbone; + } + + /*override*/ + getState() { + var response = { + availableNetworks: [], + channels: {} + }; + return this._backbone.getNetworks().then(networks => { + response.availableNetworks = networks; + return this._backbone.getLinkedChannels(); + }).then(channels => { + response.channels = channels; + return response; + }); + } + + /*override*/ + removeFromRoom(roomId) { + return this._backbone.removeFromRoom(roomId); + } + + /*override*/ + updateState(newState) { + throw new Error("State cannot be updated for an IRC bridge. Use the IRC API instead."); + } +} + +module.exports = IRCBridge; \ No newline at end of file diff --git a/src/integration/impl/irc/IRCFactory.js b/src/integration/impl/irc/IRCFactory.js new file mode 100644 index 0000000..fe6c660 --- /dev/null +++ b/src/integration/impl/irc/IRCFactory.js @@ -0,0 +1,12 @@ +var IRCBridge = require("./IRCBridge"); +var VectorIrcBackbone = require("./VectorIrcBackbone"); + +module.exports = (db, integrationConfig, roomId, scalarToken) => { + if (integrationConfig.upstream) { + if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); + return db.getUpstreamToken(scalarToken).then(upstreamToken => { + var backbone = new VectorIrcBackbone(roomId, upstreamToken); + return new IRCBridge(integrationConfig, backbone); + }); + } else throw new Error("Unsupported config"); +}; \ No newline at end of file diff --git a/src/integration/impl/irc/StubbedIrcBackbone.js b/src/integration/impl/irc/StubbedIrcBackbone.js new file mode 100644 index 0000000..fea0e56 --- /dev/null +++ b/src/integration/impl/irc/StubbedIrcBackbone.js @@ -0,0 +1,29 @@ +/** + * Stubbed/placeholder IRC backbone + */ +class StubbedIrcBackbone { + + /** + * Creates a new stubbed IRC backbone + */ + constructor() { + } + + /** + * Gets a list of all available networks + * @returns {Promise<{name: string, id: string}[]>} resolves to the list of available networks + */ + getNetworks() { + return Promise.resolve([]); + } + + /** + * Gets a network representation of the linked channels + * @returns {Promise<{[string]: string[]}>} resolves to the network representation of linked channels + */ + getLinkedChannels() { + return Promise.resolve({}); + } +} + +module.exports = StubbedIrcBackbone; \ No newline at end of file diff --git a/src/integration/impl/irc/VectorIrcBackbone.js b/src/integration/impl/irc/VectorIrcBackbone.js new file mode 100644 index 0000000..9225c13 --- /dev/null +++ b/src/integration/impl/irc/VectorIrcBackbone.js @@ -0,0 +1,69 @@ +var StubbedIrcBackbone = require("./StubbedIrcBackbone"); +var VectorScalarClient = require("../../../scalar/VectorScalarClient"); +var _ = require("lodash"); +var log = require("../../../util/LogService"); + +/** + * Backbone for IRC bridges running on vector.im through scalar + */ +class VectorIrcBackbone extends StubbedIrcBackbone { + + /** + * Creates a new Vector IRC backbone + * @param {string} roomId the room ID to manage + * @param {string} upstreamScalarToken the vector scalar token + */ + constructor(roomId, upstreamScalarToken) { + super(); + this._roomId = roomId; + this._scalarToken = upstreamScalarToken; + this._lastNetworkResponse = null; + } + + /*override*/ + getNetworks() { + return this._getNetworks().then(networks => _.map(networks, n => { + return {name: n.title, id: n.id}; + })); + } + + /*override*/ + getLinkedChannels() { + var networks; + return this._getNetworks().then(n => { + networks = n; + return VectorScalarClient.getIrcLinks(this._roomId, this._scalarToken); + }).then(links => { + var container = {}; + + var ridToServerId = {}; + + for (var network of networks) { + ridToServerId[network.rid] = network.id; + container[network.id] = []; + } + + for (var link of links) { + var server = ridToServerId[link.rid]; + if (!server) { + log.error("VectorIrcBackbone", "Could not find network for RID " + link.rid); + throw new Error("Unexpected RID"); + } + + container[server.id].push(link.channel); + } + + return container; + }); + } + + _getNetworks() { + if (this._lastNetworkResponse !== null) return Promise.resolve(this._lastNetworkResponse); + return VectorScalarClient.getIrcNetworks(this._scalarToken).then(networks => { + this._lastNetworkResponse = networks; + return networks; + }); + } +} + +module.exports = VectorIrcBackbone; \ No newline at end of file diff --git a/src/scalar/VectorScalarClient.js b/src/scalar/VectorScalarClient.js index 2a44aa5..ec685f7 100644 --- a/src/scalar/VectorScalarClient.js +++ b/src/scalar/VectorScalarClient.js @@ -82,7 +82,7 @@ class VectorScalarClient { } /** - * Gets information on + * Gets information for an integration * @param {string} type the type to lookup * @param {string} roomId the room ID to look in * @param {string} scalarToken the scalar token @@ -99,6 +99,66 @@ class VectorScalarClient { }); } + /** + * Gets a list of supported IRC networks + * @param {string} scalarToken the scalar token + * @returns {Promise<{rid: string, title: string, domain: string, id: string}[]>} resolves to the list of IRC networks + */ + getIrcNetworks(scalarToken) { + return this._do("GET", "/bridges/irc/_matrix/provision/querynetworks", {scalar_token: scalarToken}).then((response, body) => { + if (response.statusCode !== 200) { + log.error("VectorScalarClient", response.body); + return Promise.reject(response.body); + } + + response.body = JSON.parse(response.body); + + var results = []; + for (var network of response.body["replies"]) { + var result = { + rid: network["rid"], + // Assumption: All networks have 1 server from vector + id: network["response"]["servers"][0]["network_id"], + title: network["response"]["servers"][0]["desc"], + domain: network["response"]["servers"][0]["fields"]["domain"] + }; + results.push(result); + } + + return results; + }); + } + + /** + * Gets a list of all linked IRC channels for a given room + * @param {string} roomId the room ID to look in + * @param {string} scalarToken the scalar token + * @returns {Promise<{rid: string, server: string, channel: string}>} resolves to a list of linked channels + */ + getIrcLinks(roomId, scalarToken) { + return this._do("GET", "/bridges/irc/_matrix/provision/listlinks/" + roomId, {scalar_token: scalarToken}).then((response, body) => { + if (response.statusCode !== 200) { + log.error("VectorScalarClient", response.body); + return Promise.reject(response.body); + } + + response.body = JSON.parse(response.body); + + var results = []; + for (var linkContainer of response.body["replies"]) { + for (var link of linkContainer["response"]) { + results.push({ + rid: linkContainer["rid"], + server: link["remote_room_server"], + channel: link["remote_room_channel"] + }); + } + } + + return results; + }); + } + _do(method, endpoint, qs = null, body = null) { var url = config.get("upstreams.vector") + endpoint; diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 3757b08..8853005 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -19,6 +19,7 @@ import { IntegrationService } from "./shared/integration.service"; import { BootstrapModalModule } from "angular2-modal/plugins/bootstrap"; import { ModalModule } from "angular2-modal"; import { RssConfigComponent } from "./configs/rss/rss-config.component"; +import { IrcConfigComponent } from "./configs/irc/irc-config.component"; @NgModule({ imports: [ @@ -40,6 +41,7 @@ import { RssConfigComponent } from "./configs/rss/rss-config.component"; IntegrationComponent, ScalarCloseComponent, RssConfigComponent, + IrcConfigComponent, // Vendor ], @@ -53,6 +55,7 @@ import { RssConfigComponent } from "./configs/rss/rss-config.component"; bootstrap: [AppComponent], entryComponents: [ RssConfigComponent, + IrcConfigComponent, ] }) export class AppModule { diff --git a/web/app/configs/irc/irc-config.component.html b/web/app/configs/irc/irc-config.component.html new file mode 100644 index 0000000..f458943 --- /dev/null +++ b/web/app/configs/irc/irc-config.component.html @@ -0,0 +1,10 @@ +
+ +
+ +

Configure IRC Bridge

+
+
+
{{ integration | json }}
+
+
\ No newline at end of file diff --git a/web/app/configs/irc/irc-config.component.scss b/web/app/configs/irc/irc-config.component.scss new file mode 100644 index 0000000..12e299a --- /dev/null +++ b/web/app/configs/irc/irc-config.component.scss @@ -0,0 +1 @@ +// component styles are encapsulated and only applied to their components diff --git a/web/app/configs/irc/irc-config.component.ts b/web/app/configs/irc/irc-config.component.ts new file mode 100644 index 0000000..820e544 --- /dev/null +++ b/web/app/configs/irc/irc-config.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { IRCIntegration } from "../../shared/models/integration"; +import { ModalComponent, DialogRef } from "angular2-modal"; +import { ConfigModalContext } from "../../integration/integration.component"; + +@Component({ + selector: 'my-irc-config', + templateUrl: './irc-config.component.html', + styleUrls: ['./irc-config.component.scss', './../config.component.scss'], +}) +export class IrcConfigComponent implements ModalComponent { + + public integration: IRCIntegration; + + private roomId: string; + private scalarToken: string; + + constructor(public dialog: DialogRef) {// , + // private toaster: ToasterService, + // private api: ApiService) { + this.integration = dialog.context.integration; + this.roomId = dialog.context.roomId; + this.scalarToken = dialog.context.scalarToken; + } +} diff --git a/web/app/integration/integration.component.html b/web/app/integration/integration.component.html index 893e59a..3f9d756 100644 --- a/web/app/integration/integration.component.html +++ b/web/app/integration/integration.component.html @@ -3,9 +3,12 @@
{{ integration.name }}
-
+
+
+ +
diff --git a/web/app/riot/riot.component.ts b/web/app/riot/riot.component.ts index 9b61638..06b68a8 100644 --- a/web/app/riot/riot.component.ts +++ b/web/app/riot/riot.component.ts @@ -54,6 +54,26 @@ export class RiotComponent { private updateIntegrationState(integration: Integration) { integration.hasConfig = IntegrationService.hasConfig(integration); + if (integration.requirements) { + let keys = _.keys(integration.requirements); + let promises = []; + + for (let key of keys) { + let requirement = this.checkRequirement(integration, key); + promises.push(requirement); + } + + return Promise.all(promises).then(() => { + integration.isEnabled = true; + integration.isBroken = false; + }, error => { + console.error(error); + integration.bridgeError = error.message || error; + integration.isEnabled = false; + integration.isBroken = false; + }); + } + return this.scalar.getMembershipState(this.roomId, integration.userId).then(payload => { integration.isBroken = false; @@ -70,6 +90,24 @@ export class RiotComponent { }); } + private checkRequirement(integration: Integration, key: string) { + let requirement = integration.requirements[key]; + + switch (key) { + case "joinRule": + return this.scalar.getJoinRule(this.roomId).then(payload => { + if (!payload.response) { + return Promise.reject("Could not communicate with Riot"); + } + return payload.response.join_rule === requirement + ? Promise.resolve() + : Promise.reject(new Error("The room must be " + requirement + " to use this integration.")); + }); + default: + return Promise.reject(new Error("Requirement '" + key + "' not found")); + } + } + public updateIntegration(integration: Integration) { let promise = null; diff --git a/web/app/shared/integration.service.ts b/web/app/shared/integration.service.ts index 6f8c93f..b68b837 100644 --- a/web/app/shared/integration.service.ts +++ b/web/app/shared/integration.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@angular/core"; import { Integration } from "./models/integration"; import { RssConfigComponent } from "../configs/rss/rss-config.component"; import { ContainerContent } from "angular2-modal"; +import { IrcConfigComponent } from "../configs/irc/irc-config.component"; @Injectable() export class IntegrationService { @@ -10,12 +11,18 @@ export class IntegrationService { "bot": true, "complex-bot": { "rss": true + }, + "bridge": { + "irc": true } }; private static components = { "complex-bot": { "rss": RssConfigComponent + }, + "bridge": { + "irc": IrcConfigComponent } }; diff --git a/web/app/shared/models/integration.ts b/web/app/shared/models/integration.ts index e4a809b..b2fbb32 100644 --- a/web/app/shared/models/integration.ts +++ b/web/app/shared/models/integration.ts @@ -8,9 +8,16 @@ export interface Integration { isEnabled: boolean; isBroken: boolean; hasConfig: boolean; + requirements?: any; // nullable + bridgeError: string; // nullable } export interface RSSIntegration extends Integration { feeds: string[]; immutableFeeds: {url: string, ownerId: string}[]; +} + +export interface IRCIntegration extends Integration { + availableNetworks: {name: string, id: string}; + channels: {[networkId: string]: string[]}; } \ No newline at end of file diff --git a/web/app/shared/models/scalar_responses.ts b/web/app/shared/models/scalar_responses.ts index 93b1231..4b3e4be 100644 --- a/web/app/shared/models/scalar_responses.ts +++ b/web/app/shared/models/scalar_responses.ts @@ -24,4 +24,10 @@ export interface MembershipStateResponse extends ScalarUserResponse { avatar_url: string; displayname: string; }; +} + +export interface JoinRuleStateResponse extends ScalarRoomResponse { + response: { + join_rule: string; + }; } \ No newline at end of file diff --git a/web/app/shared/scalar.service.ts b/web/app/shared/scalar.service.ts index dd00527..abb2cbb 100644 --- a/web/app/shared/scalar.service.ts +++ b/web/app/shared/scalar.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; import * as randomString from "random-string"; -import { MembershipStateResponse, ScalarSuccessResponse } from "./models/scalar_responses"; +import { MembershipStateResponse, ScalarSuccessResponse, JoinRuleStateResponse } from "./models/scalar_responses"; @Injectable() export class ScalarService { @@ -30,6 +30,12 @@ export class ScalarService { }); } + public getJoinRule(roomId: string): Promise { + return this.callAction("join_rules_state", { + room_id: roomId + }); + } + public close(): void { this.callAction("close_scalar", {}); } diff --git a/web/public/img/avatars/irc.png b/web/public/img/avatars/irc.png new file mode 100644 index 0000000..bc24d31 Binary files /dev/null and b/web/public/img/avatars/irc.png differ diff --git a/webpack.config.js b/webpack.config.js index 1bbf927..80a38e8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -122,7 +122,7 @@ module.exports = function () { config.devServer = { contentBase: './web/public', historyApiFallback: true, - quiet: true, + quiet: false, stats: 'minimal', proxy: { '/api': {