@zogstrip @lightyear @sam
First let me say I am fairly new to Discourse but so far loving the community. I wanted to add a few thoughts about how plugin development might turn out after reading some of the code and posts in this category.
In framing the architecture I've taken some ideas from WordPress as well as what plugins people are wanting to develop right now. I've then tried to think through what calls the plugin might need.
Currently plugin development is kind of fragmented (as it should be...we are still coding it!). I see people monkey patching left and right on both the frontend javascript and backend rails code. I don't think this is maintainable.
That said here are some wants I'd expect from a mature Discourse plugin environment:
- It'd be nice to dynamically install and uninstall plugins. To do
this we'd need some sort of plugin registration. This may be
something good to think about before the plugin ecosystem explodes.
- You should not have to know (too much) Ember.js to code plugins.
- Not having to rely on JavaScript events via the use of trigger(). I rather have plugins register their functions and have the frontend call out to them. With Javascript you can always do crazy monkey patching but I rather the status-quo be you registering your method via a hook.
In light of this here is what I think a Discourse plugin system might look like:
- There are server-side hooks and client-side hooks that plugins will have access to.
- The Ember code has lots of calls to the function apply_filters everywhere things happen that a plugin might want to modify:
user = apply_filters('afterUserLogin', user);
- The backend rails code has the same thing:
user = apply_filters('after_user_login', user)
apply_filters() basically finds all functions registered as a filter for the given name and calls out to them one after another taking the return value of one and putting it into the next.
Frontend hooks are camel cased. beforePostCreate, afterPostCreate and backend hooks use underscores. before_post_save, after_post_save
Plugins will be as easy as creating the file discourse/plugins/[plugin_name]/plugin.rb
This file is where all the registrations for hooks happen.
Example plugin.rb for emoji:
plugin_name = 'emoji'
register_plugin_asset(plugin_name, 'javascripts/emoji.js.erb')
register_plugin_asset(plugin_name, 'stylesheets/emoji.css')
def add_emoji_class(allowed_classes):
return allowed_classes << "emoji"
end
DiscoursePlugin.register_backend_filter('post_white_listed_image_classes', add_emoji_class)
This is what I envision the emoji plugin to look like in a mature Discourse plugin environment. Mostly javascript and css. The 'post_white_listed_image_classes' method is called from within Rails. It passes the currently list of classes and expects a list back of the classes that are allowed.
@santouras
How about a plugin that adds extra fields to the composer window. If you wanted to add a field such as "Favorite Color" to each post a person makes and make the field required. Client and server side hooks are needed because you need to display the field and then validate and save it.
Example plugin.rb for fav_color:
plugin_name = 'fav_color'
register_plugin_asset(plugin_name, 'javascripts/fav_color.js.erb')
register_plugin_asset(plugin_name, 'stylesheets/fav_color.css')
def save_favorite_color(post):
return post
end
DiscoursePlugin.register_backend_filter('before_post_save', save_favorite_color)
In fav_color.js:
function addFavoriteColor(post_data) {
// Get the value of the favorite color from the form field
post_data['color'] = document.getElementById('favorite-color').val();
return post_data;
}
Discourse.register_frontend_filter('before_post_save', addFavoriteColor)
I'd put 4 hooks here (not sure about the naming...best I could come up with since I am not that experienced inside the Discourse code yet):
https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/models/post.js#L158 - savePost
https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/models/composer.js#L467 - composeNewPost
https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/controllers/composer_controller.js#L139 - composerSavePost
https://github.com/discourse/discourse/blob/master/app/controllers/posts_controller.rb#L251 - allowed_post_parameters
// Ideally run through all the filters that have registered with this filter name
data = apply_filters('before_post_save', data);
The ugly side is that your plugin would have to register upwards of 4-5 hooks to get it working. I actually don't think this is bad as long as the hooks are well separated and they might have the chance of being reused by other plugin developers.
One last example is a plugin I wanted to write. When a user registers using a certain domain then add them to a group. Probably don't need a client a side hook. The server side hook I'd want is 'on_user_create'. So the plugin.rb would simply register a callback.
As far as callbacks go on ActiveRecord objects I think we should avoid monkey patching them. I rather see all the callbacks have associated apply_filters on them if really needed. This way it all goes through a central method and we can control it better.