diff --git a/config/integrations/rssbot.yaml b/config/integrations/rssbot.yaml index 194ce8b..b0ae410 100644 --- a/config/integrations/rssbot.yaml +++ b/config/integrations/rssbot.yaml @@ -5,5 +5,4 @@ name: "RSS Bot" about: "Tracks any Atom/RSS feed and sends new items into this room" avatar: "/img/avatars/rssbot.png" upstream: - type: "vector" - id: "rssbot" \ No newline at end of file + type: "vector" \ No newline at end of file diff --git a/package.json b/package.json index 901a7c8..ba5757a 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@angularclass/hmr-loader": "^3.0.2", "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.22", "@types/node": "^7.0.18", + "angular2-modal": "^2.0.3", "angular2-template-loader": "^0.6.2", "angular2-toaster": "^4.0.0", "angular2-ui-switch": "^1.2.0", diff --git a/src/DimensionApi.js b/src/DimensionApi.js index 8e2e6e0..d85ac3d 100644 --- a/src/DimensionApi.js +++ b/src/DimensionApi.js @@ -24,6 +24,7 @@ class DimensionApi { app.get("/api/v1/dimension/integrations/:roomId", this._getIntegrations.bind(this)); app.delete("/api/v1/dimension/integrations/:roomId/:type/:integrationType", this._removeIntegration.bind(this)); + app.put("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._updateIntegrationState.bind(this)); } _getIntegration(integrationConfig, roomId, scalarToken) { @@ -103,6 +104,38 @@ class DimensionApi { res.status(500).send({error: err.message}); }); } + + _updateIntegrationState(req, res) { + var roomId = req.params.roomId; + var scalarToken = req.body.scalar_token; + var type = req.params.type; + var integrationType = req.params.integrationType; + + if (!roomId || !scalarToken || !type || !integrationType) { + res.status(400).send({error: "Missing room, integration type, type, or token"}); + return; + } + + var integrationConfig = Integrations.byType[type][integrationType]; + if (!integrationConfig) { + res.status(400).send({error: "Unknown integration"}); + return; + } + + log.info("DimensionApi", "Update state requested for " + type + " (" + integrationType + ") in room " + roomId); + + this._db.checkToken(scalarToken).then(() => { + return this._getIntegration(integrationConfig, roomId, scalarToken); + }).then(integration => { + return integration.updateState(req.body.state); + }).then(newState => { + res.status(200).send(newState); + }).catch(err => { + log.error("DimensionApi", err); + console.error(err); + res.status(500).send({error: err.message}); + }); + } } module.exports = new DimensionApi(); diff --git a/src/integration/generic_types/IntegrationStub.js b/src/integration/generic_types/IntegrationStub.js index ddda32e..d867532 100644 --- a/src/integration/generic_types/IntegrationStub.js +++ b/src/integration/generic_types/IntegrationStub.js @@ -30,6 +30,15 @@ class IntegrationStub { removeFromRoom(roomId) { throw new Error("Not implemented"); } + + /** + * Updates the state information for this integration. The data passed is an implementation detail. + * @param {*} newState the new state + * @returns {Promise<*>} resolves when completed, with the new state of the integration + */ + updateState(newState) { + return Promise.resolve({}); + } } module.exports = IntegrationStub; diff --git a/src/integration/impl/rss/RSSBot.js b/src/integration/impl/rss/RSSBot.js index 5a9959e..f3384b2 100644 --- a/src/integration/impl/rss/RSSBot.js +++ b/src/integration/impl/rss/RSSBot.js @@ -22,8 +22,16 @@ class RSSBot extends ComplexBot { /*override*/ getState() { + var response = { + feeds: [], + immutableFeeds: [] + }; return this._backbone.getFeeds().then(feeds => { - return {feeds: feeds}; + response.feeds = feeds; + return this._backbone.getImmutableFeeds(); + }).then(feeds => { + response.immutableFeeds = feeds; + return response; }); } @@ -31,6 +39,11 @@ class RSSBot extends ComplexBot { removeFromRoom(roomId) { return this._backbone.removeFromRoom(roomId); } + + /*override*/ + updateState(newState) { + return this._backbone.setFeeds(newState.feeds).then(() => this.getState()); + } } module.exports = RSSBot; \ No newline at end of file diff --git a/src/integration/impl/rss/StubbedRssBackbone.js b/src/integration/impl/rss/StubbedRssBackbone.js index 0870ad3..6e51205 100644 --- a/src/integration/impl/rss/StubbedRssBackbone.js +++ b/src/integration/impl/rss/StubbedRssBackbone.js @@ -25,6 +25,23 @@ class StubbedRssBackbone { throw new Error("Not implemented"); } + /** + * Sets the new feeds for this backbone + * @param {string[]} newFeeds the new feed URLs + * @returns {Promise<>} resolves when complete + */ + setFeeds(newFeeds) { + throw new Error("Not implemented"); + } + + /** + * Gets the immutable feeds for this backbone + * @returns {Promise<{url:string,ownerId:string}>} resolves to the collection of immutable feeds + */ + getImmutableFeeds() { + throw new Error("Not implemented"); + } + /** * Removes the bot from the given room * @param {string} roomId the room ID to remove the bot from diff --git a/src/integration/impl/rss/VectorRssBackbone.js b/src/integration/impl/rss/VectorRssBackbone.js index 0584e1c..d4a37f6 100644 --- a/src/integration/impl/rss/VectorRssBackbone.js +++ b/src/integration/impl/rss/VectorRssBackbone.js @@ -18,6 +18,7 @@ class VectorRssBackbone extends StubbedRssBackbone { this._roomId = roomId; this._scalarToken = upstreamScalarToken; this._info = null; + this._otherFeeds = []; } /*override*/ @@ -35,15 +36,46 @@ class VectorRssBackbone extends StubbedRssBackbone { }); } + /*override*/ + setFeeds(newFeeds) { + var feedConfig = {}; + for (var feed of newFeeds) feedConfig[feed] = {}; + + return VectorScalarClient.configureIntegration("rssbot", this._scalarToken, { + feeds: feedConfig, + room_id: this._roomId + }); + } + + /*override*/ + getImmutableFeeds() { + return (this._info ? Promise.resolve() : this._getInfo()).then(() => { + return this._otherFeeds; + }); + } + _getInfo() { - return VectorScalarClient.getIntegration("rssbot", this._roomId, this._scalarToken).then(info => { + return VectorScalarClient.getIntegrationsForRoom(this._roomId, this._scalarToken).then(integrations => { + this._otherFeeds = []; + for (var integration of integrations) { + if (integration.self) continue; // skip - we're not looking for ones we know about + if (integration.type == "rssbot") { + var urls = _.keys(integration.config.feeds); + for (var url of urls) { + this._otherFeeds.push({url: url, ownerId: integration.user_id}); + } + } + } + + return VectorScalarClient.getIntegration("rssbot", this._roomId, this._scalarToken); + }).then(info => { this._info = info; }); } /*override*/ removeFromRoom(roomId) { - return VectorScalarClient.removeIntegration(this._config.upstream.id, roomId, this._upstreamToken); + return VectorScalarClient.removeIntegration("rssbot", roomId, this._scalarToken); } } diff --git a/src/integration/impl/simple_bot/SimpleBotFactory.js b/src/integration/impl/simple_bot/SimpleBotFactory.js index a99068b..65d9678 100644 --- a/src/integration/impl/simple_bot/SimpleBotFactory.js +++ b/src/integration/impl/simple_bot/SimpleBotFactory.js @@ -6,7 +6,7 @@ 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 VectorSimpleBackbone(roomId, upstreamToken); + var backbone = new VectorSimpleBackbone(integrationConfig, upstreamToken); return new SimpleBot(integrationConfig, backbone); }); } else if (integrationConfig.hosted) { diff --git a/src/scalar/VectorScalarClient.js b/src/scalar/VectorScalarClient.js index 4c1f05d..2a44aa5 100644 --- a/src/scalar/VectorScalarClient.js +++ b/src/scalar/VectorScalarClient.js @@ -54,7 +54,7 @@ class VectorScalarClient { * @return {Promise<>} resolves when completed */ configureIntegration(type, scalarToken, config) { - return this._do("POST", "/integrations/"+type+"/configureService", {scalar_token:scalarToken}, config).then((response, body) => { + return this._do("POST", "/integrations/" + type + "/configureService", {scalar_token: scalarToken}, config).then((response, body) => { if (response.statusCode !== 200) { log.error("VectorScalarClient", response.body); return Promise.reject(response.body); @@ -64,6 +64,23 @@ class VectorScalarClient { }); } + /** + * Gets all of the integrations currently in a room + * @param {string} roomId the room ID + * @param {string} scalarToken the scalar token to use + * @returns {Promise<*[]>} resolves a collection of integrations + */ + getIntegrationsForRoom(roomId, scalarToken) { + return this._do("POST", "/integrations", {scalar_token: scalarToken}, {RoomId: roomId}).then((response, body) => { + if (response.statusCode !== 200) { + log.error("VectorScalarClient", response.body); + return Promise.reject(response.body); + } + + return response.body.integrations; + }); + } + /** * Gets information on * @param {string} type the type to lookup @@ -72,7 +89,7 @@ class VectorScalarClient { * @return {Promise<{bot_user_id:string,integrations:[]}>} resolves to the integration information */ getIntegration(type, roomId, scalarToken) { - return this._do("POST", "/integrations/"+type,{scalar_token:scalarToken}, {room_id:roomId}).then((response, body) => { + return this._do("POST", "/integrations/" + type, {scalar_token: scalarToken}, {room_id: roomId}).then((response, body) => { if (response.statusCode !== 200) { log.error("VectorScalarClient", response.body); return Promise.reject(response.body); diff --git a/web/app/app.module.ts b/web/app/app.module.ts index fc2d104..3757b08 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -15,6 +15,10 @@ import { ToasterModule } from "angular2-toaster"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { IntegrationComponent } from "./integration/integration.component"; import { ScalarCloseComponent } from "./riot/scalar-close/scalar-close.component"; +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"; @NgModule({ imports: [ @@ -26,6 +30,8 @@ import { ScalarCloseComponent } from "./riot/scalar-close/scalar-close.component UiSwitchModule, ToasterModule, BrowserAnimationsModule, + ModalModule.forRoot(), + BootstrapModalModule, ], declarations: [ AppComponent, @@ -33,17 +39,21 @@ import { ScalarCloseComponent } from "./riot/scalar-close/scalar-close.component RiotComponent, IntegrationComponent, ScalarCloseComponent, + RssConfigComponent, // Vendor ], providers: [ ApiService, ScalarService, + IntegrationService, // Vendor ], bootstrap: [AppComponent], - entryComponents: [] + entryComponents: [ + RssConfigComponent, + ] }) export class AppModule { constructor(public appRef: ApplicationRef) { diff --git a/web/app/configs/config.component.scss b/web/app/configs/config.component.scss new file mode 100644 index 0000000..ce39ba5 --- /dev/null +++ b/web/app/configs/config.component.scss @@ -0,0 +1,32 @@ +// shared styling for all config screens +.config-wrapper { + padding: 25px; +} + +.config-header { + padding-bottom: 8px; + margin-bottom: 14px; + border-bottom: 1px solid #dadada; +} + +.config-header h4 { + display: inline-block; + vertical-align: middle; +} + +.config-header img { + margin-right: 7px; + width: 35px; + height: 35px; + border-radius: 35px; +} + +.close-icon { + float: right; + margin: -17px; + cursor: pointer; +} + +.config-content { + display: block; +} \ No newline at end of file diff --git a/web/app/configs/rss/rss-config.component.html b/web/app/configs/rss/rss-config.component.html new file mode 100644 index 0000000..6ff1044 --- /dev/null +++ b/web/app/configs/rss/rss-config.component.html @@ -0,0 +1,38 @@ +
Turn on anything you like. If an integration has some special configuration, it will have a configuration icon next to it.
+Turn on anything you like. If an integration has some special configuration, it will have a configuration + icon next to it.