Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rocket.Chat Apps #6890

Open
mrsimpson opened this issue May 5, 2017 · 30 comments

Comments

Projects
None yet
@mrsimpson
Copy link
Contributor

commented May 5, 2017

The Big Picture

Rocket.Chat already offers options to integrate external tools and systems on multiple levels:

  • Incoming and outgoing webhooks allow sending (http-) messages back and forth to an external system
  • For some external services, a native implementation of the other system's API is even shipped along with Rocket.Chat. e. g. for Zapier, you don't even need to know the API, you simply configure it.

However, what's missing is a comfortable, update-safe option to create custom applications living inside the Rocket.Chat Main application which bring along their own GUI and can be shipped (and maintained) for multiple Rocket.Chat instances.

At DB, we do have such a usecase: We integrate an NLP engine analyzing a conversation in order to speed-up communication within the room. We implemented this (painfully) as a modification of Rocket.Chat. Though there are quite some APIs which reduce the number of necessary places for modifications, each release of Rocket.Chat urges us to merge and check for merge-conflicts. Initially this issue was about providing a more flexible integration of AI-providers, but discussing this issue it turned out that actually the necessary achitecture is not limited to AI: It affects all cases where a microservice-like application shall be integrated into Rocket.Chat

Thus, dedicated Plugins-APIs which allow for modification-free enhancements which can be shipped along with Rocket.Chat is what this issue is about.

Use case

Rocket.Chat comes with a sample AI-integration for api.ai.
Natural-language-integration into a communication is a feature which is not only hyped, but can significantly speed-up-communication.
However, there are plenty of other AI-provides. This requirement aims at making Rocket.Chat ready for an easy integration with other "AI-providers".

Features

A Registry shall provide an API for connection of other AI-adapters.

  • On the server, it basically allows to implement an interface with the following features:
    • initialize(): is called on startup and after a change of the configuration
    • onMessage(message): Allows triggering of an asynchronous request for analysis
    • onClose(room): Allows AI to learn from the conversation
    • onMessageUpdated(message): Allows AI to invalidate results
    • getSettings(): creates settings in the AI group and observes them for changes
  • Client-side, there are basically two options how results of the AI can be visualized: Either using a mashup-mechanism (some globally registered script which is attached to a DOM node) or using a visualization built-into Rocket.Chat (Meteor).
    • onStartup(): Hook for loading an external script
    • setTabbar(): If a custom Blaze-template shall visualize the results, this template shall be loaded to the tabbar. The room and the data retrieved by the plugin from the external system shall be passed to the template.

Ideas for updating the external services' results

(in our case the NLP analysis)

The tricky part (such as I can see) is to actually make the UI update. This challenge does not only apply to the AI-UI, but to all third party applications which shall live inside the tabbar. Basically there are two options:

  • The integrated UI opens an own websocket connection to its very own backend
  • +Very clear separation of concerns
  • -Overall resource consumption
  • -The client's websocket limit might be exceeded
  • Callback mechanism (Webhook): The API for registering the (AI) provider creates a webhook which can be passed to the foreign system.
  • -Overhead at the RC server
  • +Resource consumption: Re-use of the existing websocket connection
  • +A server-side collection-based cache can be provided generically -> performance

Why not use an outgoing integration?

what's the difference for a full-stack-integrated application

  • An external application might be stateful
  • Multiple integrations (one for each callback event) were needed => lack of reuse
  • Comfort of shipping a package > shipping multiple integrations
  • High volume - custom logging might be useful
  • Visualization is necessary (potentially based on other frameworks than Meteor)
  • There is no option to ship more complex integrations-configuration
@JSzaszvari

This comment has been minimized.

Copy link
Member

commented May 5, 2017

I know you said not to use a outgoing integration, but it sounds like if they were slightly modified or had more featured added then it would work perfect.

We're currently using API.AI and WIT.AI with no issues at all through the existing interface.

@mrsimpson

This comment has been minimized.

Copy link
Contributor Author

commented May 5, 2017

@JSzaszvari How did you implement the wit.ai-integration?

@JSzaszvari

This comment has been minimized.

Copy link
Member

commented May 5, 2017

@mrsimpson

This comment has been minimized.

Copy link
Contributor Author

commented May 5, 2017

Well, using hubot as API has its limitations (e. g. with respect to visualization of results, interaction with it). I hopefully can share some ideas on features and visualization I'm aiming for soon.

@JSzaszvari

This comment has been minimized.

Copy link
Member

commented May 5, 2017

@mrsimpson

This comment has been minimized.

Copy link
Contributor Author

commented May 12, 2017

@engelgabriel @graywolf336 After discussion with colleagues providing the NLP-application, I updated the issue with thoughts about updating data in the integrated 3rd party application.
I do think that many of the questions to be answered actually are not specific to AI-integration, but apply to all third-party-applications living in the tabbar.

@mrsimpson

This comment has been minimized.

Copy link
Contributor Author

commented Jun 9, 2017

@engelgabriel while I was discussing potential design with colleagues, we noticed that we should update the issue's description so that it's obvious from the beginning that the requirement is valid not only for AI integration, but for general app integration.
I updated the description accordingly and will add some sketches of potential implementation

@mrsimpson mrsimpson changed the title API for AI providers External applications as plug-ins (fka "API for AI providers") Jun 9, 2017

@mrsimpson

This comment has been minimized.

Copy link
Contributor Author

commented Jun 9, 2017

Some sketches how plugins could be integrated.
Don't take UML and OO-design for granted ;) It's just meant as a sketch as a basis for discussion

Server side integration

plugins-server

Client side integration

plugins-client

mrsimpson added a commit to mrsimpson/Rocket.Chat that referenced this issue Jun 15, 2017

initial ideas git plugins -
Not implemented yet, just some code-visualization as per sequence diagram in RocketChat#6890
@graywolf336

This comment has been minimized.

Copy link
Member

commented Jun 15, 2017

This sounds like a fantastic idea and is very similar to an idea I have been working on.

Thanks to some creative brainstorming from @geekgonecrazy, I have coined this idea "Rocketlets" instead of plugins or integrations as those two terms are extremely generic and can mean a lot of other things to other people while using our own term will let us define what it means for us and other people.

The goal of Rocketlets is to allow people to fully customize how their Rocket.Chat instance works without having to fork Rocket.Chat along with Rocketlets being fully documented with how it works including the code being in TypeScript so that everything passed in has a type (and we can use decorators). The ability to customize a Rocket.Chat instance was started with integrations and how they now have different triggers (events) which can fire them, however even those are currently limited as they just allow reacting to a trigger after it has happened. With Rocketlets my idea is to have two different actions per trigger (event) which will allow them to customize the Rocket.Chat functionality.

Let's take the message sending event for example. A Rocketlet there can define pre_messageSent and post_messageSent methods. The pre_ methods will be called pre/before saving of the event and thus before the events are officially considered sent to others. The post_ methods will be called post/after the saving of the events. Thanks to using TypeScript, each method will be able to use a decorator which will specify the priority of the method on handling the event with a range from LOWEST to HIGHEST and then MONITOR. The lowest items will be called first and the highest will be called last, this way the highest priority items have the last say on an event than the lowest.

Incoming webhooks will also be merged into Rocketlets and they will be just another method that is call on the Rocketlet. When a Rocketlet gets created it will have a url generated for it which can be called (GET, POST, PUT, DELETE, etc) and use the data in along with providing data out. The method on the Rocketlet webhook_event will be called with two parameters passed, "method" and "request".

Now for the client, I want to allow Rocketlets to be able to fully utilize the action button type of message, along with defining tab bar functionality as you have highlighted in the above comments, along with full control over reactions and everything else. Another piece that Rocketlets will be able to do is provided new slash commands to the clients and show a preview of items to the client, see my comment here for an example.

Each Rocketlet will be able to provide a list of settings it provides to the user which the user can configure. The settings provided should have defaults unless the Rocketlet wants some to be configured before it can be enabled. This will allow for high configurability, for example let's say we create a GitHub Rocketlet and we want the user to be able to select which style of commits show up inside Rocket.Chat then they can select from a drop down either simple, extended, or advanced.

Eventually Rocketlets will get to the point that we can have a directory of Rocketlets that servers can click a single button to install them then elect to be updated automatically whenever a new Rocketlet comes out or manually approve the update. Each Rocketlet would then have a changelog so the users can see what changed between the versions inside of their Rocket.Chat. As a result of this global idea, each publicly available Rocketlet will need register with us, Rocket.Chat, to get an id however there will be a range of ids available for private usage so that private and public Rocketlets will never conflict with each other.

I am missing quite a bit of details. However, I wanted you to be aware of my idea as I noticied with your commit that our structure is basically the same and we would be duplicating work. I would be more than happy to work with you @mrsimpson so we can combine our efforts and get something crafted up that is simply awesome. Below is my base class idea for the server (or click here):

import { IRequest, IResponse, RequestMethod, ResponseStatus } from './interfaces/Webhook';
import { ISetting } from './interfaces/ISetting';
import { priority, PriorityValue } from './decorators/priority';
import { IMessage } from './interfaces/IMessage';
import { IUser } from './interfaces/IUser';
import { IRoom } from './interfaces/IRoom';

export class BaseRocketlet {
	/**
	 * Create a new Rocketlet, this is called whenever the server starts up and initiates the Rocketlets. Note, your implementation of this class should call `super(name, id, version)` so we have it.
	 */
	constructor(public name: string, public id: number, public version: string) {
		console.log(`Constructed the Rocketlet ${this.name} (${this.id}) v${this.version}!`);
	}

	/**
	 * Get the name of this Rocketlet.
	 *
	 * @return {string} the name
	 */
	getName() {
		return this.name;
	}

	/**
	 * Get the version of this Rocketlet, using http://semver.org/.
	 *
	 * @return {string} the version
	 */
	getVersion() {
		return this.version;
	}

	/**
	 * Get the ID of this Rocketlet, please see <link> for how to obtain an ID for your Rocketlet.
	 *
	 * @return {number} the ID
	 */
	getID() {
		return this.version;
	}

	/**
	 * The Rocketlet should return an array of settings which it provies to the user to allow them to configure anything.
	 *
	 * @return {array} the settings this Rocketlet provides
	 */
	getSettings(): Array<ISetting> {
		return new Array<ISetting>();
	}

	/**
	 * Gets the setting from the persistant storage, this will be provided to the Rocketlets and you don't have to implement it.
	 *
	 * @param id the id of the setting to retrieve
	 * @return the setting or undefined if it doesn't exist
	 */
	getSetting(id: string): ISetting {
		return undefined;
	}

	/**
	 * Gets the value of the setting from the persistant storage, this will be provided to the Rocketlets and you don't have to implement it.
	 *
	 * @param id the id of the setting to get the value for
	 * @return the value of the setting if it is defined, will be undefined if it doesn't exist
	 */
	getSettingValue(id: string): any {
		return undefined;
	}

	/**
	 * Method called when before the message is sent to other clients. Return the message object with your changes to it.
	 *
	 * @param room The room where the message is being sent to
	 * @param user The user who is sending the message
	 * @param message The message which is being sent
	 */
	@priority(PriorityValue.HIGHEST)
	pre_messageSent(room: IRoom, user: IUser, message: IMessage): IMessage {
		// Handle data before the message is saved to the database
		return message;
	}

	/**
	 * Method called *after* the message is sent to the other clients.
	 *
	 * @param room The room where the message was sent to
	 * @param user The user who sent the message
	 * @param message The message which was sent
	 */
	@priority(PriorityValue.MONITOR)
	post_messageSent(room: IRoom, user: IUser, message: IMessage): void {
		// Handle data *after* the message is saved to the database
	}

	/**
	 * Called whenever the publically accessible url for this Rocketlet is called, if you handle the methods differently then split it out so your code doesn't get too big.
	 *
	 * @param method the method this was called with (GET, POST, etc)
	 * @param request the actual request made
	 * @return the response to send to the client
	 */
	webhook_event(method: RequestMethod, request: IRequest): IResponse {
		return {
			status: ResponseStatus.UNIMPLEMENTED
		}
	}
}
@mrsimpson

This comment has been minimized.

Copy link
Contributor Author

commented Jun 16, 2017

@graywolf336 awesome!
In short:
👍 for typescript. Having untyped interface definitions is kind of ridiculous.
👍 for plugin settings
👍 for client side parts being able to utilize more than just the tabbar
👍 for "Rocketlet" I like the term
👍 👍 👍 for you implementing that instead of me

Comments from my side:

  • I'd judge the Rocketlet framework's quality by two aspects: Stability of the API and ease of consumption.
  • With respect to ease of consumption, I believe that as many commonly used services should be provided as APIs to the plugins.
    • Settings: Creates a configuration section for the plugin
    • Persistency: Provides access to a generic plugin data collection (keys: pluginName, correlationId (the room/message/user upon the plugin operates on) )
    • Message creation (including attachments)
    • Visualization (create tabstrips, custom action button, maybe even a form within the messages such as in Facebook messenger)
    • Slash commands
    • Communication via HTTP (issuing outgoing requests. Could generically add headers e. g. in order to transport metadata such as response URLs, authorization information or timestamp and timeout)
    • Migration (I am sure that also plugins will need to be able to migrate, both settings as well as data)
  • In order to keep that stable, I'd use dependency injection. However, constructor injection might not be ideal. I can imagine that APIs particularly of those helpers will change over time. One fix could be to have one abstract super class which holds a "RocketletFrameworkVersion" member. After construction of the plugin, this version could be used in an init() in order to inject the proper dependency versions. Just as a thought, you surely have you own which are equally valid ;)
  • I do not think it should replace "integrations"- Imho, integrations are even more light weight. This means easier to use with less abilities (e. g. no own configuration, no UI except messages, only post-callbacks). However, Integrations could be one special type of plugin consuming the same framework.

In general, your thoughts an code look as if you had thought about that for some time now. For our case, we only need a subset of those features. We'd be however most happy to be an early adopter even if we needed to adopt our Rocketlet as design evolves.

With respect to contribution I believe I c an best contribute with ideas and consumption, less with code. you are 200% faster and also more mature in that area - and it's really an important topic to Rocket.Chat as a whole.

Looking forward to reading from you

@graywolf336

This comment has been minimized.

Copy link
Member

commented Jun 19, 2017

To everyone following this thread, we will post an update to this sometime this week and I am not ignoring @mrsimpson's response - we are having an architecture meeting to hopefully get this ironed out. Updates will be posted along with an implementation plan! Exciting stuff is coming, I wish I could express my excitement in more than words. 😊

@timkinnane

This comment has been minimized.

Copy link
Member

commented Jul 7, 2017

This thinking is a truly massive step forward imo. I wanna thank you all for doing the hard yards on gathering consensus on approach and significant thinking on adoption. I've been looking for this in Rocket.Chat since making my first custom UI extensions in May 2016 and struggling to maintain private features with the public codebase.

I've got lots of reading to catch up on, but hope to be an active contributor to this issue. I've got some initial thoughts that are drawn mostly from prior experience as a Wordpress dev. Say what you will about WP as a platform, it has a mature and thriving ecosystem of extensions (over 50,000, not counting private plugins and themes). I think there's a lot to learn there and some of the challenges already identified above may find elegant solutions waiting to be translated.

For example I'd look at the WP approach of Actions and Filters. Instead of using rigid semantic method naming (which requires unrealistic foresight) for extensions to hook into, they have some generic utilities for registering, ordering and attaching to hooks by any name at any significant logic juncture in the code base. There's a registry and docs of all the core hooks, but any contributor can easily add custom hooks, making all extensions extensible themselves.

The main problem you face when you take code outside the codebase, is ensuring that standards, security and methodology are being adopted, without access to the established workflows for CI and review. For that, I think there's two approaches that can work together...

Firstly, the official register of Rocketlets would need a working group (like the Wordpress Plugin Review Team), to manually review compliance, against publicly available guides (that they also would have authored). This sounds arduous, but it's extremely helpful for adoption and evangelising among the community of contributors, some of whom may be more junior, allowing them to learn and propagate the "right" way, instead of simply cargo-culting. Good thing you've already built a chatops platform that is perfect for this kind of job :P

Secondly, make official generators for starter template Rocketlets. Using something like Yeoman or an npm global create-rocketlet-app. I've been wanting to extricate some of the more esoteric contributions from the codebase for some time and maybe this is the pathway for that. I'd love to start modularising those pieces of logic as Rocketlets so they can be removed from core and repeating that process a few times would probably tease out the common components of a Rocketlet template.

BTW I think it would also provide a simpler evolution path for core features and cut out A LOT of the logjam with contributors currently submitting contentious PRs. Just create your own solution outside core, then if it ends up being widely used, there can be a process to bring it in (see also #5679).

Thanks again RC amigos! <3 <3 <3 <3

@graywolf336

This comment has been minimized.

Copy link
Member

commented Jul 7, 2017

@timkinnane Excited to have you on board and we would love your thoughts, as you and I have discussed privately. Good luck reading up on everything and getting up to speed, we would love all your feedback you can give.

Tomorrow I will take a look at the Actions and Filters to see how WordPress approached them as I completely agree, WordPress has done an amazing job with it's approach to being highly extensible!

As far as the store/registry goes, we are wanting to have a store where inside of Rocket.Chat people can view a list of available Rocketlets and then click install....who knows, maybe even be able to sell them somehow. 🤔

A generator is one approach that we can look into, however the approach I am currently taking is an entire development environment with examples but we can expand this to include a generator. 🥇 I really like that idea.

Heh, my short term goal once Rocketlets is installed is to get a majority of the slash commands out and be a Rocketlet which people can change how it works or we can issue an update in the store/registry and people click update there instead of having to update the entire Rocket.Chat. 📈

@timkinnane

This comment has been minimized.

Copy link
Member

commented Jul 7, 2017

Yes! Some of those slash commands can be kind of embarrassing within a professional context.
I'm looking at you /lennyface

@graywolf336

This comment has been minimized.

Copy link
Member

commented Jul 7, 2017

Exactly. 😉 The assciiart commands is actually going to be one of my example Rocketlets which I have already started on. 🎉

@antgel

This comment has been minimized.

Copy link
Collaborator

commented Jul 7, 2017

Rocklet > Rocketlet? :D

@graywolf336

This comment has been minimized.

Copy link
Member

commented Jul 14, 2017

For some more reading: #1859

@adis-io

This comment has been minimized.

Copy link

commented Jul 15, 2017

@mrsimpson sorry for oftopic, but you used some tool to draw that sketches?

@mrsimpson

This comment has been minimized.

Copy link
Contributor Author

commented Jul 15, 2017

@adisos yes. Multiple: A Surface 4, OneNote, a stylus and my right hand ;-)

@mnlbox

This comment has been minimized.

Copy link

commented Aug 27, 2017

@graywolf336 what is the Rocketlet state now?
Is it ready to create plug-ins based on it?

@engelgabriel engelgabriel changed the title External applications as plug-ins (fka "API for AI providers") Rocket.Chat Apps Sep 20, 2017

@cyclops24

This comment has been minimized.

Copy link
Contributor

commented Oct 8, 2017

@graywolf336 Any news about this?

@veerjainATgmail

This comment has been minimized.

Copy link

commented Oct 10, 2017

Hi, I would like to join this effort. What is the current status and development branch? thanks.

@graywolf336

This comment has been minimized.

Copy link
Member

commented Dec 19, 2017

This week I will be working on flow diagrams which show how various flows inside of the Rocket.Chat Apps works and will work. :)

Then once #9128, #9126, and #9125 are implemented you will see a pull request shortly thereafter. 👍

@chemalopezp

This comment has been minimized.

Copy link

commented Jan 15, 2018

Hi @graywolf336, I'm interested in adding bots to rocket.chat following a similar approach of slack and facebook messenger, ideally integrating with any of the leading bot platforms around (botkit, ms bot or meya.ai). If I have understood correctly, it seems a similar approach (provide webhook so bots can send messages received and gather responses). Does it complement the current webhook integration with new functionalities or is it a completely different feature?
Thanks for the clarification.

@bizzbyster

This comment has been minimized.

Copy link

commented Mar 7, 2018

@chemalopezp did you get an answer to your question? I don't see a follow-up anywhere. I'm also interested in adding support for bots in a way that is similar to facebook messenger and slack. On the server-side, it would be great if we could support slack and facebook messenger webhook API (https://developers.facebook.com/docs/messenger-platform/webhook) so that an admin could point Rocket.chat webhook to the same running instance of a chatbot that is being used for these other chat services. From a user perspective, subscribing to a bot should be one click just like Facebook and Slack. See https://www.bing.com/search?q=travel+bots&qs=n&form=QBLH&sp=-1&pq=travel+bots&sc=8-10&sk=&cvid=EF545C6E8DF840589CE9D1B717A83DDF for an example.

@kkumaresan

This comment has been minimized.

Copy link

commented Apr 9, 2018

Hi, we at Mongrov have developed a react-native mobile client library for Rocket.Chat. Please check this url: https://github.com/mongrov/roverz . Works both on iOS and Android. If you get a chance, do check this project for a comprehensive native mobile app using Rocket.Chat. We welcome your feedback 😃

@graywolf336

This comment has been minimized.

Copy link
Member

commented Apr 9, 2018

@kkumaresan this issue isn't about mobile applications.

@mnlbox

This comment has been minimized.

Copy link

commented Sep 13, 2018

@graywolf336 Any docs for RC Apps?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.