Misago defines number of custom extension points that plugins can use.
Hooks
misago.hooks
module defines bunch of hooks that plugins can extend. All hooks are python lists. To add new item to a hook, append
or extend
it to it on your plugin's ready
method:
# myplugin/apps.py file contents
from django.apps import AppConfig
from django.urls import include, path
from misago.hooks import urlpatterns
class MyPluginConfig(AppConfig):
name = 'myplugin'
verbose_name = "My plugin"
def ready(self):
# Add extra links to Misago
urlpatterns.append(path("", include("myplugin.urls")))
apipatterns
- custom API urls
This hook is a list of extra urlpatterns
that should be prepended to standard API urls that are included under the api/
path and api:
namespace.
Because those extra patterns are prepended to standard patterns, its possible for plugin to override built-in API endpoint by defining custom one with same url pattern.
urlpatterns
- custom urls
This hook is a list of extra urlpatterns
that should be prepended to standard urlpatterns
that are defined in the urls.py
.
Because those extra patterns are prepended to standard patterns, its possible for plugin to override built-in url by defining custom one with same url pattern.
context_processors
- custom context processors
List of custom context-processor functions that should be called in addition to default ones.
new_registrations_validators
- custom validator functions to run for newly registered accounts
List of validators to run on data provided to new user registration API. Validator functions are called with three arguments:
request
cleaned_data
add_error
If value in cleaned_data
is invalid, add_error
should be called with first argument as field name (or None
for whole form) and validation error. Contents of cleaned_data
will differ between registration method (Single Sign-On, Social Auth, registration form).
# myplugin/apps.py file contents
from django.apps import AppConfig
from django.core.exceptions import ValidationError
from misago.hooks import new_registrations_validators
class MyPluginConfig(AppConfig):
name = 'myplugin'
verbose_name = "My plugin"
def ready(self):
new_registrations_validators.extend([
validate_user_ip, validate_user_email,
])
def validate_user_ip(request, cleaned_data, add_error):
if request.user_ip == "172.0.0.1":
add_error(None, ValidationError("Registration is not currently available."))
def validate_user_email(request, cleaned_data, add_error):
if not cleaned_data.get("email"):
return
if cleaned_data["email"].lower().endswith(".spam"):
add_error("email", ValidationError("This e-mail is not available."))
Note: this hook is not fired for users created through OAuth2 client. To add custom logic for validating those those see the oauth2_validators
hook.
oauth2_user_data_filters
- custom filters functions to run on user data from OAuth2 server
OAuth2 user data filters are functions called with an user data retrieved from the OAuth2 provider. Those functions purpose is to convert user data from format its stored in provider to a format that is valid for Misago. For example, default user data filter from Misago removes characters outside of latin alphabet from user names and replaces spaces with underscores. It also adds random sequence of characters at the end of username if such username would conflict with other user.
Those functions are called with three arguments:
Function should return updated user_data
or None
, in which unmodified user_data
is passed to next filter. If user data belongs to an user that already has an account on site, this account will be passed as user
argument. Otherwise user
will be None
. user_data
is a typed dict:
class OAuth2UserData:
id: str
name: str
email: str
avatar: str | None
id
, name
and email
are guaranteed to be strings. name
and email
can be empty strings. email
can contain any string, not just valid e-mail.
# myplugin/apps.py file contents
import random
from django.apps import AppConfig
from django.core.exceptions import ValidationError
from misago.hooks import oauth2_user_data_filters
from misago.oauth2.validation import filter_name
class MyPluginConfig(AppConfig):
name = 'myplugin'
verbose_name = "My plugin"
def ready(self):
oauth2_user_data_filters.extend([
remove_dots_from_gmail_email, filter_user_name,
])
def remove_dots_from_gmail_email(request, user, user_data):
email = user_data["email"]
if not "@" in email:
return # No guarantees that email value is e-mail address
if not email.lower().endswith("@gmail.com"):
return
identifier, _ = email.split("@", 1)
identifier = identifier.replace(".", ")
user_data["email"] = f"{identifier}@gmail.com"
return user_data
def filter_user_name(request, user, user_data):
# Misago's default name filter is not used when custom OAuth2 filters are set!
user_data["name"] = filter_name(user, user_data["name"])
# Completely invalid usernames are replaced with User_RANDOM_STRING by the filter
if user_data["name"].startswith("User_"):
# Replace placeholder name with something like "Solid_Snake"
user_data["name"] = "%s_%s" % (
random.choice(["Swift", "Sneaky", "Silent", "Solid", "Alert"]),
random.choice(["Ocelot", "Snake", "Bobcat", "Bear", "Kitten"]),
)
return user_data
oauth2_validators
- custom validators for OAuth2 users
OAuth2 validators are functions called with an user data retrieved from the OAuth2 provider. Those functions purpose is to validate both user data and original user payload from the server and either allow or disallow them from signing on the site through OAuth server.
Those functions are called with four arguments:
request
user
: None
or pre-existing user model instance from Misago's database if this is another authentication for this user.
user_data
: Filtered user data, same as in oauth2_user_data_filters
.
original_json
: Original JSON data with coplete payload returned by user retrieval API.
Function should return nothing or raise the django.core.exceptions.ValidationError
to prevent user from signing in.
Example plugin that prevents new users from signing on the site if they don't have "forum" group:
# myplugin/apps.py file contents
import random
from django.apps import AppConfig
from django.core.exceptions import ValidationError
from misago.hooks import oauth2_validators
from misago.oauth2.validation import filter_name
class MyPluginConfig(AppConfig):
name = 'myplugin'
verbose_name = "My plugin"
def ready(self):
oauth2_validators.extend([
validate_oauth2_new_user_group
])
def validate_oauth2_new_user_group(request, user, user_data, original_json):
if user:
return # Pre-existing users can always sign in
user_groups = original_json.get("groups") or []
if "forum" not in user_groups:
raise ValidationError("You need permission to access the forum!")
## `post_search_filters` - custom filters to run on search queries
Search filters are functions called with single argument: string with search query. They are supposed to return updated search query string. If nothing or `None` is returned, original search query is passed to either next search filter, or search engine.
```python
# myplugin/apps.py file contents
from django.apps import AppConfig
from misago.hooks import post_search_filters
class MyPluginConfig(AppConfig):
name = 'myplugin'
verbose_name = "My plugin"
def ready(self):
post_search_filters.append(filter_search)
def filter_search(search):
clean_search = []
for word in search.split(" "):
if "auto" in word.lower():
clean_search.append("car")
else:
clean_search.append(word)
return " ".join(clean_search)
post_validators
- custom validators to run when new message is posted
Post validators are functions called with two arguments:
context
: dict with context in which post was created/updated, it varies
data
: dict with data sent to API by client
Validator should raise ValidationError
to interrupt posting process or return data otherwise. Data can be mutated by validator. If nothing or None
is returned, original data is reused to call next validator or save changes.
# myplugin/apps.py file contents
from django.apps import AppConfig
from rest_framework.serializers import ValidationError
from misago.hooks import post_validators
class MyPluginConfig(AppConfig):
name = 'myplugin'
verbose_name = "My plugin"
def ready(self):
post_validators.append(validate_post)
def validate_post(context, data):
if context["user"].is_staff:
return data # skip admins
if "test" in data["post"].lower():
raise ValidationError({"post": "Don't post testing messages!"})
return data # Return valid data
markdown_extensions
- custom functions that extend markup parser
Markdown extensions are functions called with single argument, instance of Markdown
class used by Misago to parse messages:
# myplugin/apps.py file contents
from django.apps import AppConfig
from misago.hooks import markdown_extensions
class MyPluginConfig(AppConfig):
name = 'myplugin'
verbose_name = "My plugin"
def ready(self):
markdown_extensions.append(extend_markdown)
def extend_markdown(md):
... # Do something with md instance, eg. register new preprocessor
parsing_result_processors
- custom filters for parsing result
Parsing result is a dictionary containing parsed text of a message posted by user, and additional metadata:
{
"original_text": text,
"parsed_text": "",
"markdown": md,
"mentions": [],
"images": [],
"internal_links": [],
"outgoing_links": [],
}
parsing_result_processors
is a list of functions that are called with two arguments (parsing_result
and RootNode
instance created from parsing result["parsed_text"]
):
# myplugin/apps.py file contents
from django.apps import AppConfig
from misago.hooks import parsing_result_processors
class MyPluginConfig(AppConfig):
name = 'myplugin'
verbose_name = "My plugin"
def ready(self):
parsing_result_processors.append(proccess_parsing_result)
def proccess_parsing_result(result, html_root_node):
return result # Do something with parsing result, eg. replace or remove spam urls