Backend support for the RSS bot.

Part of #13
This commit is contained in:
turt2live 2017-05-28 14:33:57 -06:00
parent e50eb7003e
commit ebc77b7a07
17 changed files with 341 additions and 15 deletions

View File

@ -0,0 +1,9 @@
type: "complex-bot"
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"
upstream:
type: "vector"
id: "rssbot"

View File

@ -4,6 +4,67 @@ Scalar has a server-side component to assist in managing integrations. The known
None of these are officially documented, and are subject to change.
## POST `/api/integrations?scalar_token=...`
**Body**:
```
{
"RoomID": "!JmvocvDuPTYUfuvKgs:t2l.io"
}
```
*Note*: Case difference appears to be intentional.
**Response**:
```
{
"integrations": [{
"type": "rssbot",
"user_id": "@travis:t2l.io",
"config": {
"feeds": {
"https://ci.t2l.io/view/all/rssAll": {
"poll_interval_mins": 0,
"is_failing": false,
"last_updated_ts_secs": 1495995601,
"rooms": ["!JmvocvDuPTYUfuvKgs:t2l.io"]
}
}
},
"self": false
},{
"type": "rssbot",
"user_id": "@travis:tang.ents.ca",
"config": {
"feeds": {
"https://ci.t2l.io/job/java-simple-eventemitter/rssAll": {
"poll_interval_mins": 0,
"is_failing": false,
"last_updated_ts_secs": 1495995618,
"rooms": ["!JmvocvDuPTYUfuvKgs:t2l.io"]
}
}
},
"self": true
},{
"type": "travis-ci",
"user_id": "@travis:t2l.io",
"config": {
"webhook_url": "https://scalar.vector.im/api/neb/services/hooks/some_long_string",
"rooms": {
"!JmvocvDuPTYUfuvKgs:t2l.io": {
"repos": {
"turt2live/matrix-dimension": {
"template": "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}\n Change view : %{compare_url}\n Build details : %{build_url}\n"
}
}
}
}
},
"self": false
}]
}
```
## POST `/api/integrations/{type}?scalar_token=...`
**Params**:

View File

@ -12,6 +12,7 @@ var integrations = require("./integration");
var _ = require("lodash");
var UpstreamIntegration = require("./integration/UpstreamIntegration");
var HostedIntegration = require("./integration/HostedIntegration");
var IntegrationImpl = require("./integration/impl");
/**
* Primary entry point for Dimension
@ -52,7 +53,7 @@ class Dimension {
this._app.post("/api/v1/scalar/register", this._scalarRegister.bind(this));
this._app.get("/api/v1/scalar/checkToken", this._checkScalarToken.bind(this));
this._app.get("/api/v1/dimension/integrations", this._getIntegrations.bind(this));
this._app.get("/api/v1/dimension/integrations/:roomId", this._getIntegrations.bind(this));
this._app.post("/api/v1/dimension/removeIntegration", this._removeIntegration.bind(this));
}
@ -89,14 +90,46 @@ class Dimension {
_getIntegrations(req, res) {
res.setHeader("Content-Type", "application/json");
var results = _.map(integrations.all, i => {
var integration = JSON.parse(JSON.stringify(i));
integration.upstream = undefined;
integration.hosted = undefined;
return integration;
});
var scalarToken = req.query.scalar_token;
this._db.checkToken(scalarToken).then(() => {
var roomId = req.params.roomId;
if (!roomId) {
res.status(400).send({error: 'Missing room ID'});
return;
}
res.send(results);
var results = _.map(integrations.all, i => JSON.parse(JSON.stringify(i)));
var promises = [];
_.forEach(results, i => {
if (IntegrationImpl[i.type]) {
var confs = IntegrationImpl[i.type];
if (confs[i.integrationType]) {
log.info("Dimension", "Using special configuration for " + i.type + " (" + i.integrationType + ")");
promises.push(confs[i.integrationType](this._db, i, roomId, scalarToken).then(integration => {
return integration.getUserId().then(userId=> {
i.userId = userId;
return integration.getState();
}).then(state=> {
for (var key in state) {
i[key] = state[key];
}
});
}))
} else log.verbose("Dimension", "No special configuration needs for " + i.type + " (" + i.integrationType + ")");
} else log.verbose("Dimension", "No special implementation type for " + i.type);
});
Promise.all(promises).then(() => res.send(_.map(results, integration => {
integration.upstream = undefined;
integration.hosted = undefined;
return integration;
})));
}).catch(err => {
log.error("Dimension", err);
res.status(500).send({error: err});
});
}
_checkScalarToken(req, res) {

View File

@ -0,0 +1,11 @@
/**
* Creates an integration using the given
* @param {DimensionStore} db the database
* @param {*} integrationConfig the integration configuration
* @param {string} roomId the room ID
* @param {string} scalarToken the scalar token
* @returns {Promise<*>} resolves to the configured integration
*/
module.exports = (db, integrationConfig, roomId, scalarToken) => {
throw new Error("Not implemented");
};

View File

@ -0,0 +1,7 @@
var RSSFactory = require("./rss/RSSFactory");
module.exports = {
"complex-bot": {
"rss": RSSFactory
}
};

View File

@ -0,0 +1,35 @@
var ComplexBot = require("../../type/ComplexBot");
/**
* Represents an RSS bot
*/
class RSSBot extends ComplexBot {
/**
* Creates a new RSS bot
* @param botConfig the bot configuration
* @param backbone the backbone powering this bot
*/
constructor(botConfig, backbone) {
super(botConfig);
this._backbone = backbone;
}
/*override*/
getUserId() {
return this._backbone.getUserId();
}
getFeeds() {
return this._backbone.getFeeds();
}
/*override*/
getState() {
return this.getFeeds().then(feeds => {
return {feeds: feeds};
});
}
}
module.exports = RSSBot;

View File

@ -0,0 +1,12 @@
var RSSBot = require("./RSSBot");
var VectorRssBackbone = require("./VectorRssBackbone");
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 VectorRssBackbone(roomId, upstreamToken);
return new RSSBot(integrationConfig, backbone);
});
} else throw new Error("Unsupported config");
};

View File

@ -0,0 +1,29 @@
/**
* Stubbed/placeholder RSS backbone
*/
class StubbedRssBackbone {
/**
* Creates a new stubbed RSS backbone
*/
constructor() {
}
/**
* Gets the user ID for this backbone
* @returns {Promise<string>} resolves to the user ID
*/
getUserId() {
throw new Error("Not implemented");
}
/**
* Gets the feeds for this backbone
* @returns {Promise<string[]>} resolves to the collection of feeds
*/
getFeeds() {
throw new Error("Not implemented");
}
}
module.exports = StubbedRssBackbone;

View File

@ -0,0 +1,45 @@
var StubbedRssBackbone = require("./StubbedRssBackbone");
var VectorScalarClient = require("../../../scalar/VectorScalarClient");
var _ = require("lodash");
/**
* Backbone for RSS bots running on vector.im through scalar
*/
class VectorRssBackbone extends StubbedRssBackbone {
/**
* Creates a new Vector RSS 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._info = null;
}
/*override*/
getUserId() {
return (this._info ? Promise.resolve() : this._getInfo()).then(() => {
return this._info.bot_user_id;
});
}
/*override*/
getFeeds() {
return (this._info ? Promise.resolve() : this._getInfo()).then(() => {
if (this._info.integrations.length == 0) return [];
return _.keys(this._info.integrations[0].config.feeds);
});
}
_getInfo() {
return VectorScalarClient.getIntegration("rssbot", this._roomId, this._scalarToken).then(info => {
this._info = info;
});
}
}
module.exports = VectorRssBackbone;

View File

@ -0,0 +1,18 @@
var IntegrationStub = require("./IntegrationStub");
/**
* Represents a bot with additional configuration or setup needs. Normally indicates a bot needs
* more than a simple invite to the room.
*/
class ComplexBot extends IntegrationStub {
/**
* Creates a new complex bot
* @param botConfig the configuration for the bot
*/
constructor(botConfig) {
super(botConfig);
}
}
module.exports = ComplexBot;

View File

@ -0,0 +1,26 @@
/**
* Stub for an Integration
*/
class IntegrationStub {
constructor(botConfig) {
this._config = botConfig;
}
/**
* Gets the user ID for this bot
* @return {Promise<string>} resolves to the user ID
*/
getUserId() {
return Promise.resolve(this._config.userId);
}
/**
* Gets state information that represents how this bot is operating.
* @return {Promise<*>} resolves to the state information
*/
getState() {
return Promise.resolve({});
}
}
module.exports = IntegrationStub;

View File

@ -46,6 +46,42 @@ class VectorScalarClient {
});
}
/**
* Configures an Integration on Vector
* @param {string} type the integration tpye
* @param {string} scalarToken the scalar token
* @param {*} config the config to POST to the service
* @return {Promise<>} resolves when completed
*/
configureIntegration(type, scalarToken, config) {
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);
}
// no success processing
});
}
/**
* Gets information on
* @param {string} type the type to lookup
* @param {string} roomId the room ID to look in
* @param {string} scalarToken the scalar token
* @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) => {
if (response.statusCode !== 200) {
log.error("VectorScalarClient", response.body);
return Promise.reject(response.body);
}
return response.body;
});
}
_do(method, endpoint, qs = null, body = null) {
var url = config.get("upstreams.vector") + endpoint;

View File

@ -86,7 +86,7 @@ class DimensionStore {
*/
checkToken(scalarToken) {
return this.__Tokens.find({where: {scalarToken: scalarToken}}).then(token => {
if (!token) return Promise.reject();
if (!token) return Promise.reject(new Error("Token not found"));
//if (moment().isAfter(moment(token.expires))) return this.__Tokens.destroy({where: {id: token.id}}).then(() => Promise.reject());
return Promise.resolve();
});

View File

@ -40,7 +40,7 @@ export class RiotComponent {
}
private init() {
this.api.getIntegrations().then(integrations => {
this.api.getIntegrations(this.roomId, this.scalarToken).then(integrations => {
this.integrations = integrations;
let promises = integrations.map(b => this.updateIntegrationState(b));
return Promise.all(promises);

View File

@ -7,18 +7,22 @@ export class ApiService {
constructor(private http: Http) {
}
checkScalarToken(token): Promise<boolean> {
return this.http.get("/api/v1/scalar/checkToken", {params: {scalar_token: token}})
checkScalarToken(scalarToken): Promise<boolean> {
return this.http.get("/api/v1/scalar/checkToken", {params: {scalar_token: scalarToken}})
.map(res => res.status === 200).toPromise();
}
getIntegrations(): Promise<Integration[]> {
return this.http.get("/api/v1/dimension/integrations")
getIntegrations(roomId, scalarToken): Promise<Integration[]> {
return this.http.get("/api/v1/dimension/integrations/" + roomId, {params: {scalar_token: scalarToken}})
.map(res => res.json()).toPromise();
}
removeIntegration(roomId: string, userId: string, scalarToken: string): Promise<any> {
return this.http.post("/api/v1/dimension/removeIntegration", {roomId: roomId, userId: userId, scalarToken: scalarToken})
return this.http.post("/api/v1/dimension/removeIntegration", {
roomId: roomId,
userId: userId,
scalarToken: scalarToken
})
.map(res => res.json()).toPromise();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB