diff --git a/config/config.exs b/config/config.exs
index 9982acc95..5626222b5 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -94,6 +94,18 @@ config :geolix,
     }
   ]
 
+config :auto_linker,
+  opts: [
+    scheme: true,
+    extra: true,
+    # TODO: Set to :no_scheme when it works properly
+    validate_tld: true,
+    class: false,
+    strip_prefix: false,
+    new_window: false,
+    rel: false
+  ]
+
 config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
 config :phoenix, :json_library, Jason
 
diff --git a/js/src/App.vue b/js/src/App.vue
index ef438c0c6..183adb48c 100644
--- a/js/src/App.vue
+++ b/js/src/App.vue
@@ -72,6 +72,7 @@ export default class App extends Vue {
   @import "~bulma/sass/components/modal.sass";
   @import "~bulma/sass/components/navbar.sass";
   @import "~bulma/sass/components/pagination.sass";
+  @import "~bulma/sass/components/dropdown.sass";
   @import "~bulma/sass/elements/box.sass";
   @import "~bulma/sass/elements/button.sass";
   @import "~bulma/sass/elements/container.sass";
@@ -91,9 +92,11 @@ export default class App extends Vue {
   @import "~buefy/src/scss/components/datepicker";
   @import "~buefy/src/scss/components/notices";
   @import "~buefy/src/scss/components/dropdown";
+  @import "~buefy/src/scss/components/autocomplete";
   @import "~buefy/src/scss/components/form";
   @import "~buefy/src/scss/components/modal";
   @import "~buefy/src/scss/components/tag";
+  @import "~buefy/src/scss/components/taginput";
   @import "~buefy/src/scss/components/upload";
 
 .router-enter-active,
diff --git a/js/src/components/Event/TagInput.vue b/js/src/components/Event/TagInput.vue
new file mode 100644
index 000000000..0a5f556df
--- /dev/null
+++ b/js/src/components/Event/TagInput.vue
@@ -0,0 +1,54 @@
+<template>
+    <b-field label="Enter some tags">
+        <b-taginput
+                v-model="tags"
+                :data="filteredTags"
+                autocomplete
+                :allow-new="true"
+                :field="path"
+                icon="label"
+                placeholder="Add a tag"
+                @typing="getFilteredTags">
+        </b-taginput>
+    </b-field>
+</template>
+<script lang="ts">
+import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
+import { get } from 'lodash';
+import { ITag } from '@/types/tag.model';
+@Component
+export default class TagInput extends Vue {
+
+  @Prop({ required: false, default: () => [] }) data!: object[];
+  @Prop({ required: true, default: 'value' }) path!: string;
+  @Prop({ required: true }) value!: string;
+
+  filteredTags: object[] = [];
+  tags: object[] = [];
+
+  getFilteredTags(text) {
+    this.filteredTags = this.data.filter((option) => {
+      return get(option, this.path)
+                .toString()
+                .toLowerCase()
+                .indexOf(text.toLowerCase()) >= 0;
+    });
+  }
+
+  @Watch('tags')
+  onTagsChanged (tags) {
+    const tagEntities = tags.map((tag) => {
+      if (TagInput.isTag(tag)) {
+        return tag;
+      }
+      return { title: tag, slug: tag } as ITag;
+    });
+    console.log('tags changed', tagEntities);
+    this.$emit('input', tagEntities);
+  }
+
+  static isTag(x: any): x is ITag {
+    return x.slug !== undefined;
+  }
+}
+</script>
diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts
index 563419a0b..a7174dcb5 100644
--- a/js/src/graphql/event.ts
+++ b/js/src/graphql/event.ts
@@ -143,7 +143,8 @@ export const CREATE_EVENT = gql`
   $organizerActorId: ID!,
   $category: String!,
   $beginsOn: DateTime!,
-  $picture: PictureInput!
+  $picture: PictureInput,
+  $tags: [String]
   ) {
     createEvent(
       title: $title,
@@ -151,7 +152,8 @@ export const CREATE_EVENT = gql`
       beginsOn: $beginsOn,
       organizerActorId: $organizerActorId,
       category: $category,
-      picture: $picture
+      picture: $picture,
+      tags: $tags
     ) {
       id,
       uuid,
@@ -203,8 +205,10 @@ export const LEAVE_EVENT = gql`
 export const DELETE_EVENT = gql`
   mutation DeleteEvent($id: Int!, $actorId: Int!) {
     deleteEvent(
-      id: $id,
+      eventId: $id,
       actorId: $actorId
-    )
+    ) {
+        id
+    }
   }
 `;
diff --git a/js/src/graphql/tags.ts b/js/src/graphql/tags.ts
new file mode 100644
index 000000000..86da14f34
--- /dev/null
+++ b/js/src/graphql/tags.ts
@@ -0,0 +1,16 @@
+import gql from 'graphql-tag';
+
+export const TAGS = gql`
+query {
+  tags {
+    id,
+    related {
+        id,
+        slug,
+        title
+    }
+    slug,
+    title
+  }
+}
+`;
diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts
index bcd702e24..50f1eda79 100644
--- a/js/src/types/event.model.ts
+++ b/js/src/types/event.model.ts
@@ -75,6 +75,8 @@ export interface IEvent {
   onlineAddress?: string;
   phoneAddress?: string;
   physicalAddress?: IAddress;
+
+  tags: ITag[];
 }
 
 
@@ -99,4 +101,5 @@ export class EventModel implements IEvent {
   onlineAddress: string = '';
   phoneAddress: string = '';
   picture: IAbstractPicture|null = null;
+  tags: ITag[] = [];
 }
diff --git a/js/src/views/Event/Create.vue b/js/src/views/Event/Create.vue
index c79991b0d..4c0ce83e5 100644
--- a/js/src/views/Event/Create.vue
+++ b/js/src/views/Event/Create.vue
@@ -6,10 +6,14 @@
     <div v-if="$apollo.loading">Loading...</div>
     <div class="columns is-centered" v-else>
       <form class="column is-two-thirds-desktop" @submit="createEvent">
+        <picture-upload v-model="pictureFile" />
+
         <b-field :label="$gettext('Title')">
           <b-input aria-required="true" required v-model="event.title" maxlength="64" />
         </b-field>
 
+        <tag-input v-model="event.tags" :data="tags" path="title" />
+
         <date-time-picker v-model="event.beginsOn" :label="$gettext('Starts on…')" :step="15"/>
         <date-time-picker v-model="event.endsOn" :label="$gettext('Ends on…')" :step="15" />
 
@@ -28,8 +32,6 @@
           </b-select>
         </b-field>
 
-        <picture-upload v-model="pictureFile" />
-
         <button class="button is-primary">
           <translate>Create my event</translate>
         </button>
@@ -52,13 +54,19 @@ import { IPerson, Person } from '@/types/actor';
 import PictureUpload from '@/components/PictureUpload.vue';
 import Editor from '@/components/Editor.vue';
 import DateTimePicker from '@/components/Event/DateTimePicker.vue';
+import TagInput from '@/components/Event/TagInput.vue';
+import { TAGS } from '@/graphql/tags';
+import { ITag } from '@/types/tag.model';
 
 @Component({
-  components: { DateTimePicker, PictureUpload, Editor },
+  components: { TagInput, DateTimePicker, PictureUpload, Editor },
   apollo: {
     loggedPerson: {
       query: LOGGED_PERSON,
     },
+    tags: {
+      query: TAGS,
+    },
   },
 })
 export default class CreateEvent extends Vue {
@@ -123,11 +131,12 @@ export default class CreateEvent extends Vue {
      * Transform general variables
      */
     let pictureObj = {};
-    let obj = {
+    const obj = {
       organizerActorId: this.loggedPerson.id,
       beginsOn: this.event.beginsOn.toISOString(),
+      tags: this.event.tags.map((tag: ITag) => tag.title),
     };
-    let res = Object.assign({}, this.event, obj);
+    const res = Object.assign({}, this.event, obj);
 
     /**
      * Transform picture files
diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex
index 2bc7442fd..95005bb72 100644
--- a/lib/mobilizon/events/event.ex
+++ b/lib/mobilizon/events/event.ex
@@ -86,7 +86,6 @@ defmodule Mobilizon.Events.Event do
       :uuid,
       :picture_id
     ])
-    |> cast_assoc(:tags)
     |> cast_assoc(:physical_address)
     |> validate_required([
       :title,
diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex
index f02a998bb..31908182d 100644
--- a/lib/mobilizon/events/events.ex
+++ b/lib/mobilizon/events/events.ex
@@ -367,7 +367,7 @@ defmodule Mobilizon.Events do
 
   """
   def create_event(attrs \\ %{}) do
-    with {:ok, %Event{} = event} <- %Event{} |> Event.changeset(attrs) |> Repo.insert(),
+    with %Event{} = event <- do_create_event(attrs),
          {:ok, %Participant{} = _participant} <-
            %Participant{}
            |> Participant.changeset(%{
@@ -376,7 +376,24 @@ defmodule Mobilizon.Events do
              event_id: event.id
            })
            |> Repo.insert() do
-      {:ok, Repo.preload(event, [:organizer_actor])}
+      {:ok, event}
+    end
+  end
+
+  defp do_create_event(attrs) do
+    with {:ok, %Event{} = event} <- %Event{} |> Event.changeset(attrs) |> Repo.insert(),
+         %Event{} = event <- event |> Repo.preload([:tags, :organizer_actor]),
+         {:has_tags, true, _} <- {:has_tags, Map.has_key?(attrs, "tags"), event} do
+      event
+      |> Ecto.Changeset.change()
+      |> Ecto.Changeset.put_assoc(:tags, attrs["tags"])
+      |> Repo.update()
+    else
+      {:has_tags, false, event} ->
+        event
+
+      error ->
+        error
     end
   end
 
@@ -491,6 +508,22 @@ defmodule Mobilizon.Events do
   """
   def get_tag!(id), do: Repo.get!(Tag, id)
 
+  def get_tag(id), do: Repo.get(Tag, id)
+
+  @doc """
+  Get an existing tag or create one
+  """
+  @spec get_or_create_tag(String.t()) :: {:ok, Tag.t()} | {:error, any()}
+  def get_or_create_tag(title) do
+    case Repo.get_by(Tag, title: title) do
+      %Tag{} = tag ->
+        {:ok, tag}
+
+      nil ->
+        create_tag(%{"title" => title})
+    end
+  end
+
   @doc """
   Creates a tag.
 
diff --git a/lib/mobilizon_web/api/comments.ex b/lib/mobilizon_web/api/comments.ex
index ad310de74..6680bd420 100644
--- a/lib/mobilizon_web/api/comments.ex
+++ b/lib/mobilizon_web/api/comments.ex
@@ -6,10 +6,9 @@ defmodule MobilizonWeb.API.Comments do
   alias Mobilizon.Actors
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Events.Comment
-  alias Mobilizon.Service.Formatter
   alias Mobilizon.Service.ActivityPub
   alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
-  import MobilizonWeb.API.Utils
+  alias MobilizonWeb.API.Utils
 
   @doc """
   Create a comment
@@ -20,23 +19,14 @@ defmodule MobilizonWeb.API.Comments do
   def create_comment(
         from_username,
         status,
-        visibility \\ "public",
+        visibility \\ :public,
         in_reply_to_comment_URL \\ nil
       ) do
     with {:local_actor, %Actor{url: url} = actor} <-
            {:local_actor, Actors.get_local_actor_by_name(from_username)},
-         status <- String.trim(status),
-         mentions <- Formatter.parse_mentions(status),
          in_reply_to_comment <- get_in_reply_to_comment(in_reply_to_comment_URL),
-         {to, cc} <- to_for_actor_and_mentions(actor, mentions, in_reply_to_comment, visibility),
-         tags <- Formatter.parse_tags(status),
-         content_html <-
-           make_content_html(
-             status,
-             mentions,
-             tags,
-             "text/plain"
-           ),
+         {content_html, tags, to, cc} <-
+           Utils.prepare_content(actor, status, visibility, [], in_reply_to_comment),
          comment <-
            ActivityPubUtils.make_comment_data(
              url,
diff --git a/lib/mobilizon_web/api/events.ex b/lib/mobilizon_web/api/events.ex
index e5873800a..aface5cf2 100644
--- a/lib/mobilizon_web/api/events.ex
+++ b/lib/mobilizon_web/api/events.ex
@@ -4,10 +4,9 @@ defmodule MobilizonWeb.API.Events do
   """
   alias Mobilizon.Actors
   alias Mobilizon.Actors.Actor
-  alias Mobilizon.Service.Formatter
   alias Mobilizon.Service.ActivityPub
   alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
-  import MobilizonWeb.API.Utils
+  alias MobilizonWeb.API.Utils
 
   @doc """
   Create an event
@@ -19,24 +18,19 @@ defmodule MobilizonWeb.API.Events do
           description: description,
           organizer_actor_id: organizer_actor_id,
           begins_on: begins_on,
-          category: category
+          category: category,
+          tags: tags
         } = args
       ) do
+    require Logger
+
     with %Actor{url: url} = actor <-
            Actors.get_local_actor_with_everything(organizer_actor_id),
          title <- String.trim(title),
-         mentions <- Formatter.parse_mentions(description),
          visibility <- Map.get(args, :visibility, :public),
-         {to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, Atom.to_string(visibility)),
-         tags <- Formatter.parse_tags(description),
          picture <- Map.get(args, :picture, nil),
-         content_html <-
-           make_content_html(
-             description,
-             mentions,
-             tags,
-             "text/plain"
-           ),
+         {content_html, tags, to, cc} <-
+           Utils.prepare_content(actor, description, visibility, tags, nil),
          event <-
            ActivityPubUtils.make_event_data(
              url,
diff --git a/lib/mobilizon_web/api/groups.ex b/lib/mobilizon_web/api/groups.ex
index 71c556a8f..7daf2aa7b 100644
--- a/lib/mobilizon_web/api/groups.ex
+++ b/lib/mobilizon_web/api/groups.ex
@@ -4,10 +4,9 @@ defmodule MobilizonWeb.API.Groups do
   """
   alias Mobilizon.Actors
   alias Mobilizon.Actors.Actor
-  alias Mobilizon.Service.Formatter
   alias Mobilizon.Service.ActivityPub
   alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
-  import MobilizonWeb.API.Utils
+  alias MobilizonWeb.API.Utils
 
   @doc """
   Create a group
@@ -24,17 +23,9 @@ defmodule MobilizonWeb.API.Groups do
            {:bad_actor, Actors.get_local_actor_by_name(admin_actor_username)},
          {:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
          title <- String.trim(title),
-         mentions <- Formatter.parse_mentions(description),
-         visibility <- Map.get(args, :visibility, "public"),
-         {to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, visibility),
-         tags <- Formatter.parse_tags(description),
-         content_html <-
-           make_content_html(
-             description,
-             mentions,
-             tags,
-             "text/plain"
-           ),
+         visibility <- Map.get(args, :visibility, :public),
+         {content_html, tags, to, cc} <-
+           Utils.prepare_content(actor, description, visibility, [], nil),
          group <-
            ActivityPubUtils.make_group_data(
              url,
diff --git a/lib/mobilizon_web/api/utils.ex b/lib/mobilizon_web/api/utils.ex
index c06e641a4..49d3cfb1a 100644
--- a/lib/mobilizon_web/api/utils.ex
+++ b/lib/mobilizon_web/api/utils.ex
@@ -12,11 +12,9 @@ defmodule MobilizonWeb.API.Utils do
     * `to` : the mentionned actors, the eventual actor we're replying to and the public
     * `cc` : the actor's followers
   """
-  @spec to_for_actor_and_mentions(Actor.t(), list(), map(), String.t()) :: {list(), list()}
-  def to_for_actor_and_mentions(%Actor{} = actor, mentions, inReplyTo, "public") do
-    mentioned_actors = Enum.map(mentions, fn {_, %{url: url}} -> url end)
-
-    to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_actors]
+  @spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
+  def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :public) do
+    to = ["https://www.w3.org/ns/activitystreams#Public" | mentions]
     cc = [actor.followers_url]
 
     if inReplyTo do
@@ -33,11 +31,9 @@ defmodule MobilizonWeb.API.Utils do
     * `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
     * `cc` : public
   """
-  @spec to_for_actor_and_mentions(Actor.t(), list(), map(), String.t()) :: {list(), list()}
-  def to_for_actor_and_mentions(%Actor{} = actor, mentions, inReplyTo, "unlisted") do
-    mentioned_actors = Enum.map(mentions, fn {_, %{url: url}} -> url end)
-
-    to = [actor.followers_url | mentioned_actors]
+  @spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
+  def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :unlisted) do
+    to = [actor.followers_url | mentions]
     cc = ["https://www.w3.org/ns/activitystreams#Public"]
 
     if inReplyTo do
@@ -54,9 +50,9 @@ defmodule MobilizonWeb.API.Utils do
     * `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
     * `cc` : none
   """
-  @spec to_for_actor_and_mentions(Actor.t(), list(), map(), String.t()) :: {list(), list()}
-  def to_for_actor_and_mentions(%Actor{} = actor, mentions, inReplyTo, "private") do
-    {to, cc} = to_for_actor_and_mentions(actor, mentions, inReplyTo, "direct")
+  @spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
+  def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :private) do
+    {to, cc} = get_to_and_cc(actor, mentions, inReplyTo, :direct)
     {[actor.followers_url | to], cc}
   end
 
@@ -67,59 +63,62 @@ defmodule MobilizonWeb.API.Utils do
     * `to` : the mentionned actors and the eventual actor we're replying to
     * `cc` : none
   """
-  @spec to_for_actor_and_mentions(Actor.t(), list(), map(), String.t()) :: {list(), list()}
-  def to_for_actor_and_mentions(_actor, mentions, inReplyTo, "direct") do
-    mentioned_actors = Enum.map(mentions, fn {_, %{url: url}} -> url end)
-
+  @spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
+  def get_to_and_cc(_actor, mentions, inReplyTo, :direct) do
     if inReplyTo do
-      {Enum.uniq([inReplyTo.actor | mentioned_actors]), []}
+      {Enum.uniq([inReplyTo.actor | mentions]), []}
     else
-      {mentioned_actors, []}
+      {mentions, []}
     end
   end
 
+  def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []}
+
+  #  def get_addressed_users(_, to) when is_list(to) do
+  #    Actors.get(to)
+  #  end
+
+  def get_addressed_users(mentioned_users, _), do: mentioned_users
+
   @doc """
   Creates HTML content from text and mentions
   """
-  @spec make_content_html(String.t(), list(), list(), String.t()) :: String.t()
+  @spec make_content_html(String.t(), list(), String.t()) :: String.t()
   def make_content_html(
-        status,
-        mentions,
-        tags,
+        text,
+        additional_tags,
         content_type
-      ),
-      do: format_input(status, mentions, tags, content_type)
+      ) do
+    with {text, mentions, tags} <- format_input(text, content_type, []) do
+      {text, mentions, additional_tags ++ Enum.map(tags, fn {_, tag} -> tag end)}
+    end
+  end
 
-  def format_input(text, mentions, tags, "text/plain") do
+  def format_input(text, "text/plain", options) do
     text
     |> Formatter.html_escape("text/plain")
-    |> String.replace(~r/\r?\n/, "<br>")
-    |> (&{[], &1}).()
-    |> Formatter.add_links()
-    |> Formatter.add_actor_links(mentions)
-    |> Formatter.add_hashtag_links(tags)
-    |> Formatter.finalize()
+    |> Formatter.linkify(options)
+    |> (fn {text, mentions, tags} ->
+          {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
+        end).()
   end
 
-  def format_input(text, mentions, _tags, "text/html") do
+  def format_input(text, "text/html", options) do
     text
     |> Formatter.html_escape("text/html")
-    |> String.replace(~r/\r?\n/, "<br>")
-    |> (&{[], &1}).()
-    |> Formatter.add_actor_links(mentions)
-    |> Formatter.finalize()
+    |> Formatter.linkify(options)
   end
 
-  # def format_input(text, mentions, tags, "text/markdown") do
-  #   text
-  #   |> Earmark.as_html!()
-  #   |> Formatter.html_escape("text/html")
-  #   |> String.replace(~r/\r?\n/, "")
-  #   |> (&{[], &1}).()
-  #   |> Formatter.add_actor_links(mentions)
-  #   |> Formatter.add_hashtag_links(tags)
-  #   |> Formatter.finalize()
-  # end
+  #  @doc """
+  #  Formatting text to markdown.
+  #  """
+  #  def format_input(text, "text/markdown", options) do
+  #    text
+  #    |> Formatter.mentions_escape(options)
+  #    |> Earmark.as_html!()
+  #    |> Formatter.linkify(options)
+  #    |> Formatter.html_escape("text/html")
+  #  end
 
   def make_report_content_html(nil), do: {:ok, {nil, [], []}}
 
@@ -132,4 +131,19 @@ defmodule MobilizonWeb.API.Utils do
       {:error, "Comment must be up to #{max_size} characters"}
     end
   end
+
+  def prepare_content(actor, content, visibility, tags, in_reply_to) do
+    with content <- String.trim(content),
+         {content_html, mentions, tags} <-
+           make_content_html(
+             content,
+             tags,
+             "text/plain"
+           ),
+         mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.url),
+         addressed_users <- get_addressed_users(mentioned_users, nil),
+         {to, cc} <- get_to_and_cc(actor, addressed_users, in_reply_to, visibility) do
+      {content_html, tags, to, cc}
+    end
+  end
 end
diff --git a/lib/mobilizon_web/resolvers/tag.ex b/lib/mobilizon_web/resolvers/tag.ex
index 2de78e304..ecbef7720 100644
--- a/lib/mobilizon_web/resolvers/tag.ex
+++ b/lib/mobilizon_web/resolvers/tag.ex
@@ -2,7 +2,7 @@ defmodule MobilizonWeb.Resolvers.Tag do
   @moduledoc """
   Handles the tag-related GraphQL calls
   """
-  require Logger
+  alias Mobilizon.Events
   alias Mobilizon.Events.Event
   alias Mobilizon.Events.Tag
 
@@ -19,6 +19,15 @@ defmodule MobilizonWeb.Resolvers.Tag do
     {:ok, Mobilizon.Events.list_tags_for_event(id)}
   end
 
+  @doc """
+  Retrieve the list of tags for an event
+  """
+  def list_tags_for_event(%{url: url}, _args, _resolution) do
+    with %Event{id: event_id} <- Events.get_event_by_url(url) do
+      {:ok, Mobilizon.Events.list_tags_for_event(event_id)}
+    end
+  end
+
   #  @doc """
   #  Retrieve the list of related tags for a given tag ID
   #  """
diff --git a/lib/mobilizon_web/schema/event.ex b/lib/mobilizon_web/schema/event.ex
index 83f6bdff5..12285a7a4 100644
--- a/lib/mobilizon_web/schema/event.ex
+++ b/lib/mobilizon_web/schema/event.ex
@@ -117,6 +117,11 @@ defmodule MobilizonWeb.Schema.EventType do
       arg(:public, :boolean)
       arg(:visibility, :event_visibility, default_value: :private)
 
+      arg(:tags, list_of(:string),
+        default_value: [],
+        description: "The list of tags associated to the event"
+      )
+
       arg(:picture, :picture_input,
         description:
           "The picture for the event, either as an object or directly the ID of an existing Picture"
diff --git a/lib/service/activity_pub/converters/event.ex b/lib/service/activity_pub/converters/event.ex
index 1898f894a..64021611a 100644
--- a/lib/service/activity_pub/converters/event.ex
+++ b/lib/service/activity_pub/converters/event.ex
@@ -10,6 +10,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Events.Event, as: EventModel
   alias Mobilizon.Service.ActivityPub.Converter
+  alias Mobilizon.Events
+  alias Mobilizon.Events.Tag
 
   @behaviour Converter
 
@@ -19,7 +21,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
   @impl Converter
   @spec as_to_model_data(map()) :: map()
   def as_to_model_data(object) do
-    with {:ok, %Actor{id: actor_id}} <- Actors.get_actor_by_url(object["actor"]) do
+    with {:ok, %Actor{id: actor_id}} <- Actors.get_actor_by_url(object["actor"]),
+         tags <- fetch_tags(object["tag"]) do
       picture_id =
         with true <- Map.has_key?(object, "attachment"),
              %Picture{id: picture_id} <-
@@ -43,11 +46,24 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
         "begins_on" => object["begins_on"],
         "category" => object["category"],
         "url" => object["id"],
-        "uuid" => object["uuid"]
+        "uuid" => object["uuid"],
+        "tags" => tags
       }
     end
   end
 
+  defp fetch_tags(tags) do
+    Enum.reduce(tags, [], fn tag, acc ->
+      case Events.get_or_create_tag(tag) do
+        {:ok, %Tag{} = tag} ->
+          acc ++ [tag]
+
+        _ ->
+          acc
+      end
+    end)
+  end
+
   @doc """
   Convert an event struct to an ActivityStream representation
   """
diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex
index 4c31d3193..8f1b9b5ca 100644
--- a/lib/service/activity_pub/utils.ex
+++ b/lib/service/activity_pub/utils.ex
@@ -296,7 +296,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
       "actor" => actor,
       "id" => Routes.page_url(Endpoint, :event, uuid),
       "uuid" => uuid,
-      "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
+      "tag" => tags |> Enum.uniq()
     }
 
     if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)])
@@ -328,7 +328,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
       "actor" => actor,
       "id" => Routes.page_url(Endpoint, :comment, uuid),
       "uuid" => uuid,
-      "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
+      "tag" => tags |> Enum.uniq()
     }
 
     if inReplyTo do
diff --git a/lib/service/formatter/formatter.ex b/lib/service/formatter/formatter.ex
index 905d8d577..93d3779e9 100644
--- a/lib/service/formatter/formatter.ex
+++ b/lib/service/formatter/formatter.ex
@@ -1,5 +1,5 @@
 # Portions of this file are derived from Pleroma:
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
 # SPDX-License-Identifier: AGPL-3.0-only
 # Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/formatter.ex
 
@@ -10,68 +10,86 @@ defmodule Mobilizon.Service.Formatter do
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Actors
 
-  @tag_regex ~r/\#\w+/u
-  def parse_tags(text, data \\ %{}) do
-    Regex.scan(@tag_regex, text)
-    |> Enum.map(fn ["#" <> tag = full_tag] -> {full_tag, String.downcase(tag)} end)
-    |> (fn map ->
-          if data["sensitive"] in [true, "True", "true", "1"],
-            do: [{"#nsfw", "nsfw"}] ++ map,
-            else: map
-        end).()
+  @link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
+  @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
+
+  @auto_linker_config hashtag: true,
+                      hashtag_handler: &Mobilizon.Service.Formatter.hashtag_handler/4,
+                      mention: true,
+                      mention_handler: &Mobilizon.Service.Formatter.mention_handler/4
+
+  def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
+    case Mobilizon.Actors.get_actor_by_name(nickname) do
+      %Actor{} ->
+        # escape markdown characters with `\\`
+        # (we don't want something like @user__name to be parsed by markdown)
+        String.replace(mention, @markdown_characters_regex, "\\\\\\1")
+
+      _ ->
+        buffer
+    end
   end
 
-  def parse_mentions(text) do
-    # Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
-    regex =
-      ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
+  def mention_handler("@" <> nickname, buffer, _opts, acc) do
+    case Actors.get_actor_by_name(nickname) do
+      %Actor{id: id, url: url, preferred_username: preferred_username} = actor ->
+        link =
+          "<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{url}'>@<span>#{
+            preferred_username
+          }</span></a></span>"
 
-    Regex.scan(regex, text)
-    |> List.flatten()
-    |> Enum.uniq()
-    |> Enum.map(fn "@" <> match = full_match ->
-      {full_match, Actors.get_actor_by_name(match)}
-    end)
-    |> Enum.filter(fn {_match, user} -> user end)
+        {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}}
+
+      _ ->
+        {buffer, acc}
+    end
   end
 
-  # def emojify(text) do
-  #   emojify(text, Emoji.get_all())
-  # end
+  def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
+    tag = String.downcase(tag)
+    url = "#{MobilizonWeb.Endpoint.url()}/tag/#{tag}"
+    link = "<a class='hashtag' data-tag='#{tag}' href='#{url}' rel='tag'>#{tag_text}</a>"
 
-  # def emojify(text, nil), do: text
+    {link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
+  end
 
-  # def emojify(text, emoji) do
-  #   Enum.reduce(emoji, text, fn {emoji, file}, text ->
-  #     emoji = HTML.strip_tags(emoji)
-  #     file = HTML.strip_tags(file)
+  @doc """
+  Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
 
-  #     String.replace(
-  #       text,
-  #       ":#{emoji}:",
-  #       "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
-  #         MediaProxy.url(file)
-  #       }' />"
-  #     )
-  #     |> HTML.filter_tags()
-  #   end)
-  # end
+  If the 'safe_mention' option is given, only consecutive mentions at the start the post are actually mentioned.
+  """
+  @spec linkify(String.t(), keyword()) ::
+          {String.t(), [{String.t(), Actor.t()}], [{String.t(), String.t()}]}
+  def linkify(text, options \\ []) do
+    options = options ++ @auto_linker_config
 
-  # def get_emoji(text) when is_binary(text) do
-  #   Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
-  # end
+    acc = %{mentions: MapSet.new(), tags: MapSet.new()}
+    {text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
 
-  # def get_emoji(_), do: []
+    {text, MapSet.to_list(mentions), MapSet.to_list(tags)}
+  end
 
-  @link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
+  @doc """
+  Escapes a special characters in mention names.
+  """
+  def mentions_escape(text, options \\ []) do
+    options =
+      Keyword.merge(options,
+        mention: true,
+        url: false,
+        mention_handler: &escape_mention_handler/4
+      )
 
-  @uri_schemes Application.get_env(:mobilizon, :uri_schemes, [])
-  @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
+    AutoLinker.link(text, options)
+  end
 
-  # # TODO: make it use something other than @link_regex
-  # def html_escape(text, "text/html") do
-  #   HTML.filter_tags(text)
-  # end
+  def html_escape({text, mentions, hashtags}, type) do
+    {html_escape(text, type), mentions, hashtags}
+  end
+
+  def html_escape(_text, "text/html") do
+    #    HTML.filter_tags(text)
+  end
 
   def html_escape(text, "text/plain") do
     Regex.split(@link_regex, text, include_captures: true)
@@ -82,84 +100,15 @@ defmodule Mobilizon.Service.Formatter do
     |> Enum.join("")
   end
 
-  @doc "changes scheme:... urls to html links"
-  def add_links({subs, text}) do
-    links =
+  def truncate(text, max_length \\ 200, omission \\ "...") do
+    # Remove trailing whitespace
+    text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
+
+    if String.length(text) < max_length do
       text
-      |> String.split([" ", "\t", "<br>"])
-      |> Enum.filter(fn word -> String.starts_with?(word, @valid_schemes) end)
-      |> Enum.filter(fn word -> Regex.match?(@link_regex, word) end)
-      |> Enum.map(fn url -> {Ecto.UUID.generate(), url} end)
-      |> Enum.sort_by(fn {_, url} -> -String.length(url) end)
-
-    uuid_text =
-      links
-      |> Enum.reduce(text, fn {uuid, url}, acc -> String.replace(acc, url, uuid) end)
-
-    subs =
-      subs ++
-        Enum.map(links, fn {uuid, url} ->
-          {uuid, "<a href=\"#{url}\">#{url}</a>"}
-        end)
-
-    {subs, uuid_text}
-  end
-
-  @doc "Adds the links to mentioned actors"
-  def add_actor_links({subs, text}, mentions) do
-    mentions =
-      mentions
-      |> Enum.sort_by(fn {name, _} -> -String.length(name) end)
-      |> Enum.map(fn {name, actor} -> {name, actor, Ecto.UUID.generate()} end)
-
-    uuid_text =
-      mentions
-      |> Enum.reduce(text, fn {match, _actor, uuid}, text ->
-        String.replace(text, match, uuid)
-      end)
-
-    subs =
-      subs ++
-        Enum.map(mentions, fn {match, %Actor{id: id, url: url}, uuid} ->
-          short_match = String.split(match, "@") |> tl() |> hd()
-
-          {uuid,
-           "<span><a data-user='#{id}' class='mention' href='#{url}'>@<span>#{short_match}</span></a></span>"}
-        end)
-
-    {subs, uuid_text}
-  end
-
-  @doc "Adds the hashtag links"
-  def add_hashtag_links({subs, text}, tags) do
-    tags =
-      tags
-      |> Enum.sort_by(fn {name, _} -> -String.length(name) end)
-      |> Enum.map(fn {name, short} -> {name, short, Ecto.UUID.generate()} end)
-
-    uuid_text =
-      tags
-      |> Enum.reduce(text, fn {match, _short, uuid}, text ->
-        String.replace(text, match, uuid)
-      end)
-
-    subs =
-      subs ++
-        Enum.map(tags, fn {tag_text, tag, uuid} ->
-          url =
-            "<a data-tag='#{tag}' href='#{MobilizonWeb.Endpoint.url()}/tag/#{tag}' rel='tag'>#{
-              tag_text
-            }</a>"
-
-          {uuid, url}
-        end)
-
-    {subs, uuid_text}
-  end
-
-  def finalize({subs, text}) do
-    Enum.reduce(subs, text, fn {uuid, replacement}, result_text ->
-      String.replace(result_text, uuid, replacement)
-    end)
+    else
+      length_with_omission = max_length - String.length(omission)
+      String.slice(text, 0, length_with_omission) <> omission
+    end
   end
 end
diff --git a/mix.exs b/mix.exs
index 42eb57979..a3e2c19d8 100644
--- a/mix.exs
+++ b/mix.exs
@@ -90,6 +90,9 @@ defmodule Mobilizon.Mixfile do
       {:earmark, "~> 1.3.1"},
       {:geohax, "~> 0.3.0"},
       {:mogrify, "~> 0.7.2"},
+      {:auto_linker,
+       git: "https://git.pleroma.social/pleroma/auto_linker.git",
+       ref: "95e8188490e97505c56636c1379ffdf036c1fdde"},
       # Dev and test dependencies
       {:phoenix_live_reload, "~> 1.2", only: :dev},
       {:ex_machina, "~> 2.3", only: [:dev, :test]},
diff --git a/mix.lock b/mix.lock
index a5d589ffa..286bbc004 100644
--- a/mix.lock
+++ b/mix.lock
@@ -7,6 +7,7 @@
   "arc_ecto": {:git, "https://github.com/tcitworld/arc_ecto.git", "e0d8db119c564744404cff68157417e2a83941af", []},
   "argon2_elixir": {:hex, :argon2_elixir, "2.0.5", "0073a87d755c7e63fc4f9d08b1d1646585b93f144cecde126e15061b24240b20", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
   "atomex": {:hex, :atomex, "0.3.0", "19b5d1a2aef8706dbd307385f7d5d9f6f273869226d317492c396c7bacf26402", [:mix], [{:xml_builder, "~> 2.0.0", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm"},
+  "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "95e8188490e97505c56636c1379ffdf036c1fdde", [ref: "95e8188490e97505c56636c1379ffdf036c1fdde"]},
   "bamboo": {:hex, :bamboo, "1.2.0", "8aebd24f7c606c32d0163c398004a11608ca1028182a169b2e527793bfab7561", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
   "bamboo_smtp": {:hex, :bamboo_smtp, "1.7.0", "f0d213e18ced1f08b551a72221e9b8cfbf23d592b684e9aa1ef5250f4943ef9b", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.14.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"},
   "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
diff --git a/schema.graphql b/schema.graphql
index bf8c681c8..57cf761a6 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -1,663 +1,994 @@
-# This file was generated based on ".graphqlconfig.yaml". Do not edit manually.
+# source: http://localhost:4000/api
+# timestamp: Fri Jul 26 2019 11:28:32 GMT+0200 (GMT+02:00)
 
 schema {
-    query: RootQueryType
-    mutation: RootMutationType
+  query: RootQueryType
+  mutation: RootMutationType
 }
 
-"An ActivityPub actor"
+"""An action log"""
+type ActionLog {
+  """The action that was done"""
+  action: String
+
+  """The actor that acted"""
+  actor: Actor
+
+  """Internal ID for this comment"""
+  id: ID
+
+  """The object that was acted upon"""
+  object: ActionLogObject
+}
+
+"""The objects that can be in an action log"""
+interface ActionLogObject {
+  """Internal ID for this object"""
+  id: ID
+}
+
+"""An ActivityPub actor"""
 interface Actor {
-    "The actor's avatar picture"
-    avatar: Picture
-    "The actor's banner picture"
-    banner: Picture
-    "The actor's domain if (null if it's this instance)"
-    domain: String
-    "List of followers"
-    followers: [Follower]
-    "Number of followers for this actor"
-    followersCount: Int
-    "List of followings"
-    following: [Follower]
-    "Number of actors following this actor"
-    followingCount: Int
-    "Internal ID for this actor"
-    id: Int
-    "The actors RSA Keys"
-    keys: String
-    "If the actor is from this instance"
-    local: Boolean
-    "Whether the actors manually approves followers"
-    manuallyApprovesFollowers: Boolean
-    "The actor's displayed name"
-    name: String
-    "A list of the events this actor has organized"
-    organizedEvents: [Event]
-    "The actor's preferred username"
-    preferredUsername: String
-    "The actor's summary"
-    summary: String
-    "If the actor is suspended"
-    suspended: Boolean
-    "The type of Actor (Person, Group,…)"
-    type: ActorType
-    "The ActivityPub actor's URL"
-    url: String
+  """The actor's avatar picture"""
+  avatar: Picture
+
+  """The actor's banner picture"""
+  banner: Picture
+
+  """The actor's domain if (null if it's this instance)"""
+  domain: String
+
+  """List of followers"""
+  followers: [Follower]
+
+  """Number of followers for this actor"""
+  followersCount: Int
+
+  """List of followings"""
+  following: [Follower]
+
+  """Number of actors following this actor"""
+  followingCount: Int
+
+  """Internal ID for this actor"""
+  id: Int
+
+  """The actors RSA Keys"""
+  keys: String
+
+  """If the actor is from this instance"""
+  local: Boolean
+
+  """Whether the actors manually approves followers"""
+  manuallyApprovesFollowers: Boolean
+
+  """The actor's displayed name"""
+  name: String
+
+  """A list of the events this actor has organized"""
+  organizedEvents: [Event]
+
+  """The actor's preferred username"""
+  preferredUsername: String
+
+  """The actor's summary"""
+  summary: String
+
+  """If the actor is suspended"""
+  suspended: Boolean
+
+  """The type of Actor (Person, Group,…)"""
+  type: ActorType
+
+  """The ActivityPub actor's URL"""
+  url: String
+}
+
+"""The list of types an actor can be"""
+enum ActorType {
+  """An ActivityPub Application"""
+  APPLICATION
+
+  """An ActivityPub Group"""
+  GROUP
+
+  """An ActivityPub Organization"""
+  ORGANIZATION
+
+  """An ActivityPub Person"""
+  PERSON
+
+  """An ActivityPub Service"""
+  SERVICE
 }
 
 type Address {
-    country: String
-    description: String
-    "The floor this event is at"
-    floor: String
-    "The geocoordinates for the point where this address is"
-    geom: Point
-    "The address's locality"
-    locality: String
-    postalCode: String
-    region: String
-    "The address's street name (with number)"
-    street: String
+  country: String
+  description: String
+
+  """The floor this event is at"""
+  floor: String
+
+  """The geocoordinates for the point where this address is"""
+  geom: Point
+
+  """The address's locality"""
+  locality: String
+  postalCode: String
+  region: String
+
+  """The address's street name (with number)"""
+  street: String
 }
 
-"A comment"
+"""A comment"""
 type Comment {
-    "Internal ID for this comment"
-    id: ID
-    local: Boolean
-    primaryLanguage: String
-    replies: [Comment]
-    text: String
-    threadLanguages: [String]!
-    url: String
-    uuid: UUID
-    visibility: CommentVisibility
+  """Internal ID for this comment"""
+  id: ID
+  local: Boolean
+  primaryLanguage: String
+  replies: [Comment]
+  text: String
+  threadLanguages: [String]!
+  url: String
+  uuid: UUID
+  visibility: CommentVisibility
 }
 
-"A config object"
-type Config {
-    description: String
-    name: String
-    registrationsOpen: Boolean
-}
-
-"Represents a deleted feed_token"
-type DeletedFeedToken {
-    actor: DeletedObject
-    user: DeletedObject
-}
-
-"Represents a deleted member"
-type DeletedMember {
-    actor: DeletedObject
-    parent: DeletedObject
-}
-
-"A struct containing the id of the deleted object"
-type DeletedObject {
-    id: Int
-}
-
-"Represents a deleted participant"
-type DeletedParticipant {
-    actor: DeletedObject
-    event: DeletedObject
-}
-
-"An event"
-type Event {
-    "Who the event is attributed to (often a group)"
-    attributedTo: Actor
-    "Datetime for when the event begins"
-    beginsOn: DateTime
-    "The event's category"
-    category: String
-    "When the event was created"
-    createdAt: DateTime
-    "The event's description"
-    description: String
-    "Datetime for when the event ends"
-    endsOn: DateTime
-    "Internal ID for this event"
-    id: Int
-    "Whether the event is local or not"
-    local: Boolean
-    "Online address of the event"
-    onlineAddress: OnlineAddress
-    "The event's organizer (as a person)"
-    organizerActor: Actor
-    "The event's participants"
-    participants: [Participant]
-    "Phone address for the event"
-    phoneAddress: PhoneAddress
-    "The type of the event's address"
-    physicalAddress: Address
-    "The event's picture"
-    picture: Picture
-    "When the event was published"
-    publishAt: DateTime
-    "Events related to this one"
-    relatedEvents: [Event]
-    "The event's description's slug"
-    slug: String
-    "Status of the event"
-    status: EventStatus
-    "The event's tags"
-    tags: [Tag]
-    "The event's title"
-    title: String
-    "When the event was last updated"
-    updatedAt: DateTime
-    "The ActivityPub Event URL"
-    url: String
-    "The Event UUID"
-    uuid: UUID
-    "The event's visibility"
-    visibility: EventVisibility
-}
-
-"Search events result"
-type Events {
-    "Event elements"
-    elements: [Event]!
-    "Total elements"
-    total: Int!
-}
-
-"Represents a participant to an event"
-type FeedToken {
-    "The event which the actor participates in"
-    actor: Actor
-    "The role of this actor at this event"
-    token: String
-    "The actor that participates to the event"
-    user: User
-}
-
-"""
-Represents an actor's follower
-"""
-type Follower {
-    "Which profile follows"
-    actor: Actor
-    "Whether the follow has been approved by the target actor"
-    approved: Boolean
-    "What or who the profile follows"
-    targetActor: Actor
-}
-
-"""
-Represents a group of actors
-"""
-type Group implements Actor {
-    "The actor's avatar picture"
-    avatar: Picture
-    "The actor's banner picture"
-    banner: Picture
-    "The actor's domain if (null if it's this instance)"
-    domain: String
-    "List of followers"
-    followers: [Follower]
-    "Number of followers for this actor"
-    followersCount: Int
-    "List of followings"
-    following: [Follower]
-    "Number of actors following this actor"
-    followingCount: Int
-    "Internal ID for this group"
-    id: Int
-    "The actors RSA Keys"
-    keys: String
-    "If the actor is from this instance"
-    local: Boolean
-    "Whether the actors manually approves followers"
-    manuallyApprovesFollowers: Boolean
-    "List of group members"
-    members: [Member]!
-    "The actor's displayed name"
-    name: String
-    "Whether the group is opened to all or has restricted access"
-    openness: Openness
-    "A list of the events this actor has organized"
-    organizedEvents: [Event]
-    "The actor's preferred username"
-    preferredUsername: String
-    "The actor's summary"
-    summary: String
-    "If the actor is suspended"
-    suspended: Boolean
-    "The type of Actor (Person, Group,…)"
-    type: ActorType
-    "The type of group : Group, Community,…"
-    types: GroupType
-    "The ActivityPub actor's URL"
-    url: String
-}
-
-"Search groups result"
-type Groups {
-    "Group elements"
-    elements: [Group]!
-    "Total elements"
-    total: Int!
-}
-
-"A JWT and the associated user ID"
-type Login {
-    "A JWT Token for this session"
-    token: String!
-    "The user associated to this session"
-    user: User!
-}
-
-"""
-Represents a member of a group
-"""
-type Member {
-    "Which profile is member of"
-    actor: Person
-    "Of which the profile is member"
-    parent: Group
-    "The role of this membership"
-    role: Int
-}
-
-type OnlineAddress {
-    info: String
-    url: String
-}
-
-"Represents a participant to an event"
-type Participant {
-    "The actor that participates to the event"
-    actor: Actor
-    "The event which the actor participates in"
-    event: Event
-    "The role of this actor at this event"
-    role: Int
-}
-
-"""
-Represents a person identity
-"""
-type Person implements Actor {
-    "The actor's avatar picture"
-    avatar: Picture
-    "The actor's banner picture"
-    banner: Picture
-    "The actor's domain if (null if it's this instance)"
-    domain: String
-    "A list of the feed tokens for this person"
-    feedTokens: [FeedToken]
-    "List of followers"
-    followers: [Follower]
-    "Number of followers for this actor"
-    followersCount: Int
-    "List of followings"
-    following: [Follower]
-    "Number of actors following this actor"
-    followingCount: Int
-    "The list of events this person goes to"
-    goingToEvents: [Event]
-    "Internal ID for this person"
-    id: Int
-    "The actors RSA Keys"
-    keys: String
-    "If the actor is from this instance"
-    local: Boolean
-    "Whether the actors manually approves followers"
-    manuallyApprovesFollowers: Boolean
-    "The list of groups this person is member of"
-    memberOf: [Member]
-    "The actor's displayed name"
-    name: String
-    "A list of the events this actor has organized"
-    organizedEvents: [Event]
-    "The actor's preferred username"
-    preferredUsername: String
-    "The actor's summary"
-    summary: String
-    "If the actor is suspended"
-    suspended: Boolean
-    "The type of Actor (Person, Group,…)"
-    type: ActorType
-    "The ActivityPub actor's URL"
-    url: String
-    "The user this actor is associated to"
-    user: User
-}
-
-"Search persons result"
-type Persons {
-    "Person elements"
-    elements: [Person]!
-    "Total elements"
-    total: Int!
-}
-
-type PhoneAddress {
-    info: String
-    phone: String
-}
-
-"A picture"
-type Picture {
-    "The picture's alternative text"
-    alt: String
-    "The picture's ID"
-    id: ID
-    "The picture's name"
-    name: String
-    "The picture's full URL"
-    url: String
-}
-
-type RootMutationType {
-    "Change default actor for user"
-    changeDefaultActor(preferredUsername: String!): User
-    "Create a comment"
-    createComment(actorUsername: String!, text: String!): Comment
-    "Create an event"
-    createEvent(
-        beginsOn: DateTime!,
-        category: String!,
-        description: String!,
-        endsOn: DateTime,
-        onlineAddress: String,
-        organizerActorId: ID!,
-        phoneAddress: String,
-        #The picture for the event, either as an object or directly the ID of an existing Picture
-        picture: PictureInput,
-        public: Boolean,
-        publishAt: DateTime,
-        state: Int,
-        status: Int,
-        title: String!,
-        visibility: EventVisibility = PRIVATE
-    ): Event
-    "Create a Feed Token"
-    createFeedToken(actorId: Int): FeedToken
-    "Create a group"
-    createGroup(
-        #The actor's username which will be the admin (otherwise user's default one)
-        adminActorUsername: String,
-        #The avatar for the group, either as an object or directly the ID of an existing Picture
-        avatar: PictureInput,
-        #The banner for the group, either as an object or directly the ID of an existing Picture
-        banner: PictureInput,
-        #The summary for the group
-        description: String = "",
-        #The displayed name for the group
-        name: String,
-        #The name for the group
-        preferredUsername: String!
-    ): Group
-    "Create a new person for user"
-    createPerson(
-        #The avatar for the profile, either as an object or directly the ID of an existing Picture
-        avatar: PictureInput,
-        #The banner for the profile, either as an object or directly the ID of an existing Picture
-        banner: PictureInput,
-        #The displayed name for the new profile
-        name: String = "",
-        preferredUsername: String!,
-        #The summary for the new profile
-        summary: String = ""
-    ): Person
-    "Create an user"
-    createUser(email: String!, password: String!): User
-    "Delete an event"
-    deleteEvent(actorId: Int!, eventId: Int!): DeletedObject
-    "Delete a feed token"
-    deleteFeedToken(token: String!): DeletedFeedToken
-    "Delete a group"
-    deleteGroup(actorId: Int!, groupId: Int!): DeletedObject
-    "Join an event"
-    joinEvent(actorId: Int!, eventId: Int!): Participant
-    "Join a group"
-    joinGroup(actorId: Int!, groupId: Int!): Member
-    "Leave an event"
-    leaveEvent(actorId: Int!, eventId: Int!): DeletedParticipant
-    "Leave an event"
-    leaveGroup(actorId: Int!, groupId: Int!): DeletedMember
-    "Login an user"
-    login(email: String!, password: String!): Login
-    "Register a first profile on registration"
-    registerPerson(
-        #The avatar for the profile, either as an object or directly the ID of an existing Picture
-        avatar: PictureInput,
-        #The banner for the profile, either as an object or directly the ID of an existing Picture
-        banner: PictureInput,
-        #The email from the user previously created
-        email: String!,
-        #The displayed name for the new profile
-        name: String = "",
-        preferredUsername: String!,
-        #The summary for the new profile
-        summary: String = ""
-    ): Person
-    "Resend registration confirmation token"
-    resendConfirmationEmail(email: String!, locale: String = "en"): String
-    "Reset user password"
-    resetPassword(locale: String = "en", password: String!, token: String!): Login
-    "Send a link through email to reset user password"
-    sendResetPassword(email: String!, locale: String = "en"): String
-    "Upload a picture"
-    uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture
-    "Validate an user after registration"
-    validateUser(token: String!): Login
-}
-
-"""
-Root Query
-"""
-type RootQueryType {
-    "Get the instance config"
-    config: Config
-    "Get an event by uuid"
-    event(uuid: UUID!): Event
-    "Get all events"
-    events(limit: Int = 10, page: Int = 1): [Event]
-    "Get a group by it's preferred username"
-    group(preferredUsername: String!): Group
-    "Get all groups"
-    groups(limit: Int = 10, page: Int = 1): [Group]
-    "Get the persons for an user"
-    identities: [Person]
-    "Get the current actor for the logged-in user"
-    loggedPerson: Person
-    "Get the current user"
-    loggedUser: User
-    "Get all participants for an event uuid"
-    participants(limit: Int = 10, page: Int = 1, uuid: UUID!): [Participant]
-    "Get a person by it's preferred username"
-    person(preferredUsername: String!): Person
-    "Get a picture"
-    picture(id: String!): Picture
-    "Reverse geocode coordinates"
-    reverseGeocode(latitude: Float!, longitude: Float!): [Address]
-    "Search for an address"
-    searchAddress(query: String!): [Address]
-    "Search events"
-    searchEvents(limit: Int = 10, page: Int = 1, search: String!): Events
-    "Search groups"
-    searchGroups(limit: Int = 10, page: Int = 1, search: String!): Groups
-    "Search persons"
-    searchPersons(limit: Int = 10, page: Int = 1, search: String!): Persons
-    "Get the list of tags"
-    tags(limit: Int = 10, page: Int = 1): [Tag]!
-    "Get an user"
-    user(id: ID!): User
-    "List instance users"
-    users(direction: SortDirection = DESC, limit: Int = 10, page: Int = 1, sort: SortableUserField = ID): Users
-}
-
-"A tag"
-type Tag {
-    "The tag's ID"
-    id: ID
-    "Related tags to this tag"
-    related: [Tag]
-    "The tags's slug"
-    slug: String
-    "The tag's title"
-    title: String
-}
-
-"A local user of Mobilizon"
-type User {
-    "The datetime the last activation/confirmation token was sent"
-    confirmationSentAt: DateTime
-    "The account activation/confirmation token"
-    confirmationToken: String
-    "The datetime when the user was confirmed/activated"
-    confirmedAt: DateTime
-    "The user's default actor"
-    defaultActor: Person
-    "The user's email"
-    email: String!
-    "A list of the feed tokens for this user"
-    feedTokens: [FeedToken]
-    "The user's ID"
-    id: ID!
-    "The user's list of profiles (identities)"
-    profiles: [Person]!
-    "The datetime last reset password email was sent"
-    resetPasswordSentAt: DateTime
-    "The token sent when requesting password token"
-    resetPasswordToken: String
-}
-
-"Users list"
-type Users {
-    "User elements"
-    elements: [User]!
-    "Total elements"
-    total: Int!
-}
-
-"The list of types an actor can be"
-enum ActorType {
-    #An ActivityPub Application
-    APPLICATION
-    #An ActivityPub Group
-    GROUP
-    #An ActivityPub Organization
-    ORGANIZATION
-    #An ActivityPub Person
-    PERSON
-    #An ActivityPub Service
-    SERVICE
-}
-
-"The list of visibility options for a comment"
+"""The list of visibility options for a comment"""
 enum CommentVisibility {
-    #visible only to people invited
-    INVITE
-    #Visible only after a moderator accepted
-    MODERATED
-    #Visible only to people members of the group or followers of the person
-    PRIVATE
-    #Publically listed and federated. Can be shared.
-    PUBLIC
-    #Visible only to people with the link - or invited
-    UNLISTED
+  """visible only to people invited"""
+  INVITE
+
+  """Visible only after a moderator accepted"""
+  MODERATED
+
+  """Visible only to people members of the group or followers of the person"""
+  PRIVATE
+
+  """Publically listed and federated. Can be shared."""
+  PUBLIC
+
+  """Visible only to people with the link - or invited"""
+  UNLISTED
 }
 
-"The list of possible options for the event's status"
-enum EventStatus {
-    #The event is cancelled
-    CANCELLED
-    #The event is confirmed
-    CONFIRMED
-    #The event is tentative
-    TENTATIVE
+"""A config object"""
+type Config {
+  description: String
+  name: String
+  registrationsOpen: Boolean
 }
 
-"The list of visibility options for an event"
-enum EventVisibility {
-    #visible only to people invited
-    INVITE
-    #Visible only after a moderator accepted
-    MODERATED
-    #Visible only to people members of the group or followers of the person
-    PRIVATE
-    #Publically listed and federated. Can be shared.
-    PUBLIC
-    #Visible only to people with the link - or invited
-    UNLISTED
-}
-
-"""
-The types of Group that exist
-"""
-enum GroupType {
-    #A public group of many actors
-    COMMUNITY
-    #A private group of persons
-    GROUP
-}
-
-"""
-Describes how an actor is opened to follows
-"""
-enum Openness {
-    #The actor can only be followed by invitation
-    INVITE_ONLY
-    #The actor needs to accept the following before it's effective
-    MODERATED
-    #The actor is open to followings
-    OPEN
-}
-
-"Available sort directions"
-enum SortDirection {
-    ASC
-    DESC
-}
-
-"The list of possible options for the event's status"
-enum SortableUserField {
-    ID
-}
-
-"An attached picture or a link to a picture"
-input PictureInput {
-    picture: PictureInputObject
-    pictureId: String
-}
-
-"An attached picture"
-input PictureInputObject {
-    actorId: ID
-    alt: String
-    file: Upload!
-    name: String!
-}
-
-
-"""
-The `Point` scalar type represents Point geographic information compliant string data,
-represented as floats separated by a semi-colon. The geodetic system is WGS 84
-"""
-scalar Point
-
 """
 The `DateTime` scalar type represents a date and time in the UTC
 timezone. The DateTime appears in a JSON response as an ISO8601 formatted
-string, including UTC timezone (\"Z\"). The parsed date and time string will
+string, including UTC timezone ("Z"). The parsed date and time string will
 be converted to UTC and any UTC offset other than 0 will be rejected.
 """
 scalar DateTime
 
+"""Represents a deleted feed_token"""
+type DeletedFeedToken {
+  actor: DeletedObject
+  user: DeletedObject
+}
+
+"""Represents a deleted member"""
+type DeletedMember {
+  actor: DeletedObject
+  parent: DeletedObject
+}
+
+"""A struct containing the id of the deleted object"""
+type DeletedObject {
+  id: Int
+}
+
+"""Represents a deleted participant"""
+type DeletedParticipant {
+  actor: DeletedObject
+  event: DeletedObject
+}
+
+"""An event"""
+type Event {
+  """Who the event is attributed to (often a group)"""
+  attributedTo: Actor
+
+  """Datetime for when the event begins"""
+  beginsOn: DateTime
+
+  """The event's category"""
+  category: String
+
+  """When the event was created"""
+  createdAt: DateTime
+
+  """The event's description"""
+  description: String
+
+  """Datetime for when the event ends"""
+  endsOn: DateTime
+
+  """Internal ID for this event"""
+  id: Int
+
+  """Whether the event is local or not"""
+  local: Boolean
+
+  """Online address of the event"""
+  onlineAddress: OnlineAddress
+
+  """The event's organizer (as a person)"""
+  organizerActor: Actor
+
+  """The event's participants"""
+  participants: [Participant]
+
+  """Phone address for the event"""
+  phoneAddress: PhoneAddress
+
+  """The type of the event's address"""
+  physicalAddress: Address
+
+  """The event's picture"""
+  picture: Picture
+
+  """When the event was published"""
+  publishAt: DateTime
+
+  """Events related to this one"""
+  relatedEvents: [Event]
+
+  """The event's description's slug"""
+  slug: String
+
+  """Status of the event"""
+  status: EventStatus
+
+  """The event's tags"""
+  tags: [Tag]
+
+  """The event's title"""
+  title: String
+
+  """When the event was last updated"""
+  updatedAt: DateTime
+
+  """The ActivityPub Event URL"""
+  url: String
+
+  """The Event UUID"""
+  uuid: UUID
+
+  """The event's visibility"""
+  visibility: EventVisibility
+}
+
+"""Search events result"""
+type Events {
+  """Event elements"""
+  elements: [Event]!
+
+  """Total elements"""
+  total: Int!
+}
+
+"""The list of possible options for the event's status"""
+enum EventStatus {
+  """The event is cancelled"""
+  CANCELLED
+
+  """The event is confirmed"""
+  CONFIRMED
+
+  """The event is tentative"""
+  TENTATIVE
+}
+
+"""The list of visibility options for an event"""
+enum EventVisibility {
+  """visible only to people invited"""
+  INVITE
+
+  """Visible only after a moderator accepted"""
+  MODERATED
+
+  """Visible only to people members of the group or followers of the person"""
+  PRIVATE
+
+  """Publically listed and federated. Can be shared."""
+  PUBLIC
+
+  """Visible only to people with the link - or invited"""
+  UNLISTED
+}
+
+"""Represents a participant to an event"""
+type FeedToken {
+  """The event which the actor participates in"""
+  actor: Actor
+
+  """The role of this actor at this event"""
+  token: String
+
+  """The actor that participates to the event"""
+  user: User
+}
+
+"""
+Represents an actor's follower
+
+"""
+type Follower {
+  """Which profile follows"""
+  actor: Actor
+
+  """Whether the follow has been approved by the target actor"""
+  approved: Boolean
+
+  """What or who the profile follows"""
+  targetActor: Actor
+}
+
+"""
+Represents a group of actors
+
+"""
+type Group implements Actor {
+  """The actor's avatar picture"""
+  avatar: Picture
+
+  """The actor's banner picture"""
+  banner: Picture
+
+  """The actor's domain if (null if it's this instance)"""
+  domain: String
+
+  """List of followers"""
+  followers: [Follower]
+
+  """Number of followers for this actor"""
+  followersCount: Int
+
+  """List of followings"""
+  following: [Follower]
+
+  """Number of actors following this actor"""
+  followingCount: Int
+
+  """Internal ID for this group"""
+  id: Int
+
+  """The actors RSA Keys"""
+  keys: String
+
+  """If the actor is from this instance"""
+  local: Boolean
+
+  """Whether the actors manually approves followers"""
+  manuallyApprovesFollowers: Boolean
+
+  """List of group members"""
+  members: [Member]!
+
+  """The actor's displayed name"""
+  name: String
+
+  """Whether the group is opened to all or has restricted access"""
+  openness: Openness
+
+  """A list of the events this actor has organized"""
+  organizedEvents: [Event]
+
+  """The actor's preferred username"""
+  preferredUsername: String
+
+  """The actor's summary"""
+  summary: String
+
+  """If the actor is suspended"""
+  suspended: Boolean
+
+  """The type of Actor (Person, Group,…)"""
+  type: ActorType
+
+  """The type of group : Group, Community,…"""
+  types: GroupType
+
+  """The ActivityPub actor's URL"""
+  url: String
+}
+
+"""Search groups result"""
+type Groups {
+  """Group elements"""
+  elements: [Group]!
+
+  """Total elements"""
+  total: Int!
+}
+
+"""
+The types of Group that exist
+
+"""
+enum GroupType {
+  """A public group of many actors"""
+  COMMUNITY
+
+  """A private group of persons"""
+  GROUP
+}
+
+"""A JWT and the associated user ID"""
+type Login {
+  """A JWT Token for this session"""
+  token: String!
+
+  """The user associated to this session"""
+  user: User!
+}
+
+"""
+Represents a member of a group
+
+"""
+type Member {
+  """Which profile is member of"""
+  actor: Person
+
+  """Of which the profile is member"""
+  parent: Group
+
+  """The role of this membership"""
+  role: Int
+}
+
+type OnlineAddress {
+  info: String
+  url: String
+}
+
+"""
+Describes how an actor is opened to follows
+
+"""
+enum Openness {
+  """The actor can only be followed by invitation"""
+  INVITE_ONLY
+
+  """The actor needs to accept the following before it's effective"""
+  MODERATED
+
+  """The actor is open to followings"""
+  OPEN
+}
+
+"""Represents a participant to an event"""
+type Participant {
+  """The actor that participates to the event"""
+  actor: Actor
+
+  """The event which the actor participates in"""
+  event: Event
+
+  """The role of this actor at this event"""
+  role: Int
+}
+
+"""
+Represents a person identity
+
+"""
+type Person implements Actor {
+  """The actor's avatar picture"""
+  avatar: Picture
+
+  """The actor's banner picture"""
+  banner: Picture
+
+  """The actor's domain if (null if it's this instance)"""
+  domain: String
+
+  """A list of the feed tokens for this person"""
+  feedTokens: [FeedToken]
+
+  """List of followers"""
+  followers: [Follower]
+
+  """Number of followers for this actor"""
+  followersCount: Int
+
+  """List of followings"""
+  following: [Follower]
+
+  """Number of actors following this actor"""
+  followingCount: Int
+
+  """The list of events this person goes to"""
+  goingToEvents: [Event]
+
+  """Internal ID for this person"""
+  id: Int
+
+  """The actors RSA Keys"""
+  keys: String
+
+  """If the actor is from this instance"""
+  local: Boolean
+
+  """Whether the actors manually approves followers"""
+  manuallyApprovesFollowers: Boolean
+
+  """The list of groups this person is member of"""
+  memberOf: [Member]
+
+  """The actor's displayed name"""
+  name: String
+
+  """A list of the events this actor has organized"""
+  organizedEvents: [Event]
+
+  """The actor's preferred username"""
+  preferredUsername: String
+
+  """The actor's summary"""
+  summary: String
+
+  """If the actor is suspended"""
+  suspended: Boolean
+
+  """The type of Actor (Person, Group,…)"""
+  type: ActorType
+
+  """The ActivityPub actor's URL"""
+  url: String
+
+  """The user this actor is associated to"""
+  user: User
+}
+
+"""Search persons result"""
+type Persons {
+  """Person elements"""
+  elements: [Person]!
+
+  """Total elements"""
+  total: Int!
+}
+
+type PhoneAddress {
+  info: String
+  phone: String
+}
+
+"""A picture"""
+type Picture {
+  """The picture's alternative text"""
+  alt: String
+
+  """The picture's detected content type"""
+  contentType: String
+
+  """The picture's ID"""
+  id: ID
+
+  """The picture's name"""
+  name: String
+
+  """The picture's size"""
+  size: Int
+
+  """The picture's full URL"""
+  url: String
+}
+
+"""An attached picture or a link to a picture"""
+input PictureInput {
+  picture: PictureInputObject
+  pictureId: String
+}
+
+"""An attached picture"""
+input PictureInputObject {
+  actorId: ID
+  alt: String
+  file: Upload!
+  name: String!
+}
+
+"""
+The `Point` scalar type represents Point geographic information compliant string data, 
+represented as floats separated by a semi-colon. The geodetic system is WGS 84
+"""
+scalar Point
+
+"""A report object"""
+type Report implements ActionLogObject {
+  """The comments that are reported"""
+  comments: [Comment]
+
+  """The comment the reporter added about this report"""
+  content: String
+
+  """The event that is being reported"""
+  event: Event
+
+  """The internal ID of the report"""
+  id: ID
+
+  """The actor that is being reported"""
+  reported: Actor
+
+  """The actor that created the report"""
+  reporter: Actor
+
+  """Whether the report is still active"""
+  status: ReportStatus
+
+  """The URI of the report"""
+  uri: String
+}
+
+"""A report note object"""
+type ReportNote implements ActionLogObject {
+  """The content of the note"""
+  content: String
+
+  """The internal ID of the report note"""
+  id: ID
+
+  """The moderator who added the note"""
+  moderator: Actor
+
+  """The report on which this note is added"""
+  report: Report
+}
+
+"""The list of possible statuses for a report object"""
+enum ReportStatus {
+  """The report has been closed"""
+  CLOSED
+
+  """The report has been opened"""
+  OPEN
+
+  """The report has been marked as resolved"""
+  RESOLVED
+}
+
+type RootMutationType {
+  """Change default actor for user"""
+  changeDefaultActor(preferredUsername: String!): User
+
+  """Create a comment"""
+  createComment(actorUsername: String!, text: String!): Comment
+
+  """Create an event"""
+  createEvent(
+    beginsOn: DateTime!
+    category: String!
+    description: String!
+    endsOn: DateTime
+    onlineAddress: String
+    organizerActorId: ID!
+    phoneAddress: String
+
+    """
+    The picture for the event, either as an object or directly the ID of an existing Picture
+    """
+    picture: PictureInput
+    public: Boolean
+    publishAt: DateTime
+    state: Int
+    status: Int
+
+    """The list of tags associated to the event"""
+    tags: [String] = [""]
+    title: String!
+    visibility: EventVisibility = PRIVATE
+  ): Event
+
+  """Create a Feed Token"""
+  createFeedToken(actorId: Int): FeedToken
+
+  """Create a group"""
+  createGroup(
+    """
+    The actor's username which will be the admin (otherwise user's default one)
+    """
+    adminActorUsername: String
+
+    """
+    The avatar for the group, either as an object or directly the ID of an existing Picture
+    """
+    avatar: PictureInput
+
+    """
+    The banner for the group, either as an object or directly the ID of an existing Picture
+    """
+    banner: PictureInput
+
+    """The summary for the group"""
+    description: String = ""
+
+    """The displayed name for the group"""
+    name: String
+
+    """The name for the group"""
+    preferredUsername: String!
+  ): Group
+
+  """Create a new person for user"""
+  createPerson(
+    """
+    The avatar for the profile, either as an object or directly the ID of an existing Picture
+    """
+    avatar: PictureInput
+
+    """
+    The banner for the profile, either as an object or directly the ID of an existing Picture
+    """
+    banner: PictureInput
+
+    """The displayed name for the new profile"""
+    name: String = ""
+    preferredUsername: String!
+
+    """The summary for the new profile"""
+    summary: String = ""
+  ): Person
+
+  """Create a report"""
+  createReport(commentsIds: [ID] = [""], eventId: ID, reportContent: String, reportedActorId: ID!, reporterActorId: ID!): Report
+
+  """Create a note on a report"""
+  createReportNote(content: String, moderatorId: ID!, reportId: ID!): ReportNote
+
+  """Create an user"""
+  createUser(email: String!, password: String!): User
+
+  """Delete an event"""
+  deleteEvent(actorId: Int!, eventId: Int!): DeletedObject
+
+  """Delete a feed token"""
+  deleteFeedToken(token: String!): DeletedFeedToken
+
+  """Delete a group"""
+  deleteGroup(actorId: Int!, groupId: Int!): DeletedObject
+
+  """Delete an identity"""
+  deletePerson(preferredUsername: String!): Person
+  deleteReportNote(moderatorId: ID!, noteId: ID!): DeletedObject
+
+  """Join an event"""
+  joinEvent(actorId: Int!, eventId: Int!): Participant
+
+  """Join a group"""
+  joinGroup(actorId: Int!, groupId: Int!): Member
+
+  """Leave an event"""
+  leaveEvent(actorId: Int!, eventId: Int!): DeletedParticipant
+
+  """Leave an event"""
+  leaveGroup(actorId: Int!, groupId: Int!): DeletedMember
+
+  """Login an user"""
+  login(email: String!, password: String!): Login
+
+  """Register a first profile on registration"""
+  registerPerson(
+    """
+    The avatar for the profile, either as an object or directly the ID of an existing Picture
+    """
+    avatar: PictureInput
+
+    """
+    The banner for the profile, either as an object or directly the ID of an existing Picture
+    """
+    banner: PictureInput
+
+    """The email from the user previously created"""
+    email: String!
+
+    """The displayed name for the new profile"""
+    name: String = ""
+    preferredUsername: String!
+
+    """The summary for the new profile"""
+    summary: String = ""
+  ): Person
+
+  """Resend registration confirmation token"""
+  resendConfirmationEmail(email: String!, locale: String = "en"): String
+
+  """Reset user password"""
+  resetPassword(locale: String = "en", password: String!, token: String!): Login
+
+  """Send a link through email to reset user password"""
+  sendResetPassword(email: String!, locale: String = "en"): String
+
+  """Update an identity"""
+  updatePerson(
+    """
+    The avatar for the profile, either as an object or directly the ID of an existing Picture
+    """
+    avatar: PictureInput
+
+    """
+    The banner for the profile, either as an object or directly the ID of an existing Picture
+    """
+    banner: PictureInput
+
+    """The displayed name for this profile"""
+    name: String
+    preferredUsername: String!
+
+    """The summary for this profile"""
+    summary: String
+  ): Person
+
+  """Update a report"""
+  updateReportStatus(moderatorId: ID!, reportId: ID!, status: ReportStatus!): Report
+
+  """Upload a picture"""
+  uploadPicture(actorId: ID!, alt: String, file: Upload!, name: String!): Picture
+
+  """Validate an user after registration"""
+  validateUser(token: String!): Login
+}
+
+"""
+Root Query
+
+"""
+type RootQueryType {
+  """Get the list of action logs"""
+  actionLogs(limit: Int = 10, page: Int = 1): [ActionLog]
+
+  """Get the instance config"""
+  config: Config
+
+  """Get an event by uuid"""
+  event(uuid: UUID!): Event
+
+  """Get all events"""
+  events(limit: Int = 10, page: Int = 1): [Event]
+
+  """Get a group by it's preferred username"""
+  group(preferredUsername: String!): Group
+
+  """Get all groups"""
+  groups(limit: Int = 10, page: Int = 1): [Group]
+
+  """Get the persons for an user"""
+  identities: [Person]
+
+  """Get the current actor for the logged-in user"""
+  loggedPerson: Person
+
+  """Get the current user"""
+  loggedUser: User
+
+  """Get all participants for an event uuid"""
+  participants(limit: Int = 10, page: Int = 1, uuid: UUID!): [Participant]
+
+  """Get a person by it's preferred username"""
+  person(preferredUsername: String!): Person
+
+  """Get a picture"""
+  picture(id: String!): Picture
+
+  """Get a report by id"""
+  report(id: ID!): Report
+
+  """Get all reports"""
+  reports(limit: Int = 10, page: Int = 1): [Report]
+
+  """Reverse geocode coordinates"""
+  reverseGeocode(latitude: Float!, longitude: Float!): [Address]
+
+  """Search for an address"""
+  searchAddress(query: String!): [Address]
+
+  """Search events"""
+  searchEvents(limit: Int = 10, page: Int = 1, search: String!): Events
+
+  """Search groups"""
+  searchGroups(limit: Int = 10, page: Int = 1, search: String!): Groups
+
+  """Search persons"""
+  searchPersons(limit: Int = 10, page: Int = 1, search: String!): Persons
+
+  """Get the list of tags"""
+  tags(limit: Int = 10, page: Int = 1): [Tag]!
+
+  """Get an user"""
+  user(id: ID!): User
+
+  """List instance users"""
+  users(direction: SortDirection = DESC, limit: Int = 10, page: Int = 1, sort: SortableUserField = ID): Users
+}
+
+"""The list of possible options for the event's status"""
+enum SortableUserField {
+  ID
+}
+
+"""Available sort directions"""
+enum SortDirection {
+  ASC
+  DESC
+}
+
+"""A tag"""
+type Tag {
+  """The tag's ID"""
+  id: ID
+
+  """Related tags to this tag"""
+  related: [Tag]
+
+  """The tags's slug"""
+  slug: String
+
+  """The tag's title"""
+  title: String
+}
+
+"""
+Represents an uploaded file.
+
+"""
+scalar Upload
+
+"""A local user of Mobilizon"""
+type User {
+  """The datetime the last activation/confirmation token was sent"""
+  confirmationSentAt: DateTime
+
+  """The account activation/confirmation token"""
+  confirmationToken: String
+
+  """The datetime when the user was confirmed/activated"""
+  confirmedAt: DateTime
+
+  """The user's default actor"""
+  defaultActor: Person
+
+  """The user's email"""
+  email: String!
+
+  """A list of the feed tokens for this user"""
+  feedTokens: [FeedToken]
+
+  """The user's ID"""
+  id: ID!
+
+  """The user's list of profiles (identities)"""
+  profiles: [Person]!
+
+  """The datetime last reset password email was sent"""
+  resetPasswordSentAt: DateTime
+
+  """The token sent when requesting password token"""
+  resetPasswordToken: String
+}
+
+"""Users list"""
+type Users {
+  """User elements"""
+  elements: [User]!
+
+  """Total elements"""
+  total: Int!
+}
+
 """
 The `UUID` scalar type represents UUID4 compliant string data, represented as UTF-8
 character sequences. The UUID4 type is most often used to represent unique
 human-readable ID strings.
 """
 scalar UUID
-
-"""
-Represents an uploaded file.
-"""
-scalar Upload
diff --git a/test/mobilizon/service/formatter/formatter_test.exs b/test/mobilizon/service/formatter/formatter_test.exs
new file mode 100644
index 000000000..cc3dcfa40
--- /dev/null
+++ b/test/mobilizon/service/formatter/formatter_test.exs
@@ -0,0 +1,205 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mobilizon.Service.FormatterTest do
+  alias Mobilizon.Service.Formatter
+  use Mobilizon.DataCase
+
+  import Mobilizon.Factory
+
+  describe ".add_hashtag_links" do
+    test "turns hashtags into links" do
+      text = "I love #cofe and #2hu"
+
+      expected_text =
+        "I love <a class='hashtag' data-tag='cofe' href='http://mobilizon.test/tag/cofe' rel='tag'>#cofe</a> and <a class='hashtag' data-tag='2hu' href='http://mobilizon.test/tag/2hu' rel='tag'>#2hu</a>"
+
+      assert {^expected_text, [], _tags} = Formatter.linkify(text)
+    end
+
+    test "does not turn html characters to tags" do
+      text = "#fact_3: pleroma does what mastodon't"
+
+      expected_text =
+        "<a class='hashtag' data-tag='fact_3' href='http://mobilizon.test/tag/fact_3' rel='tag'>#fact_3</a>: pleroma does what mastodon't"
+
+      assert {^expected_text, [], _tags} = Formatter.linkify(text)
+    end
+  end
+
+  describe ".add_links" do
+    test "turning urls into links" do
+      text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
+
+      expected =
+        "Hey, check out <a href=\"https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla\">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> ."
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "https://mastodon.social/@lambadalambda"
+
+      expected =
+        "<a href=\"https://mastodon.social/@lambadalambda\">https://mastodon.social/@lambadalambda</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "https://mastodon.social:4000/@lambadalambda"
+
+      expected =
+        "<a href=\"https://mastodon.social:4000/@lambadalambda\">https://mastodon.social:4000/@lambadalambda</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "@lambadalambda"
+      expected = "@lambadalambda"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "http://www.cs.vu.nl/~ast/intel/"
+      expected = "<a href=\"http://www.cs.vu.nl/~ast/intel/\">http://www.cs.vu.nl/~ast/intel/</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
+
+      expected =
+        "<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
+
+      expected =
+        "<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "https://www.google.co.jp/search?q=Nasim+Aghdam"
+
+      expected =
+        "<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "https://en.wikipedia.org/wiki/Duff's_device"
+
+      expected =
+        "<a href=\"https://en.wikipedia.org/wiki/Duff's_device\">https://en.wikipedia.org/wiki/Duff's_device</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "https://pleroma.com https://pleroma.com/sucks"
+
+      expected =
+        "<a href=\"https://pleroma.com\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\">https://pleroma.com/sucks</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text = "xmpp:contact@hacktivis.me"
+
+      expected = "<a href=\"xmpp:contact@hacktivis.me\">xmpp:contact@hacktivis.me</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+
+      text =
+        "magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com"
+
+      expected = "<a href=\"#{text}\">#{text}</a>"
+
+      assert {^expected, [], []} = Formatter.linkify(text)
+    end
+  end
+
+  describe "add_user_links" do
+    test "gives a replacement for user links, using local nicknames in user links text" do
+      text = "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme@archae.me"
+      gsimg = insert(:actor, preferred_username: "gsimg")
+
+      archaeme =
+        insert(:actor, preferred_username: "archa_eme_", url: "https://archeme/@archa_eme_")
+
+      archaeme_remote = insert(:actor, preferred_username: "archaeme", domain: "archae.me")
+
+      {text, mentions, []} = Formatter.linkify(text)
+
+      assert length(mentions) == 3
+
+      expected_text =
+        "<span class='h-card'><a data-user='#{gsimg.id}' class='u-url mention' href='#{gsimg.url}'>@<span>gsimg</span></a></span> According to <span class='h-card'><a data-user='#{
+          archaeme.id
+        }' class='u-url mention' href='#{"https://archeme/@archa_eme_"}'>@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class='h-card'><a data-user='#{
+          archaeme_remote.id
+        }' class='u-url mention' href='#{archaeme_remote.url}'>@<span>archaeme</span></a></span>"
+
+      assert expected_text == text
+    end
+
+    test "gives a replacement for single-character local nicknames" do
+      text = "@o hi"
+      o = insert(:actor, preferred_username: "o")
+
+      {text, mentions, []} = Formatter.linkify(text)
+
+      assert length(mentions) == 1
+
+      expected_text =
+        "<span class='h-card'><a data-user='#{o.id}' class='u-url mention' href='#{o.url}'>@<span>o</span></a></span> hi"
+
+      assert expected_text == text
+    end
+
+    test "does not give a replacement for single-character local nicknames who don't exist" do
+      text = "@a hi"
+
+      expected_text = "@a hi"
+      assert {^expected_text, [] = _mentions, [] = _tags} = Formatter.linkify(text)
+    end
+  end
+
+  describe ".parse_tags" do
+    test "parses tags in the text" do
+      text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。"
+
+      expected_tags = [
+        {"#Test", "test"},
+        {"#working", "working"},
+        {"#は", "は"},
+        {"#漢字", "漢字"}
+      ]
+
+      assert {_text, [], ^expected_tags} = Formatter.linkify(text)
+    end
+  end
+
+  test "it can parse mentions and return the relevant users" do
+    text =
+      "@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm"
+
+    o = insert(:actor, preferred_username: "o")
+    jimm = insert(:actor, preferred_username: "jimm")
+    gsimg = insert(:actor, preferred_username: "gsimg")
+    archaeme = insert(:actor, preferred_username: "archaeme")
+    archaeme_remote = insert(:actor, preferred_username: "archaeme", domain: "archae.me")
+
+    expected_mentions = [
+      {"@archaeme", archaeme.id},
+      {"@archaeme@archae.me", archaeme_remote.id},
+      {"@gsimg", gsimg.id},
+      {"@jimm", jimm.id},
+      {"@o", o.id}
+    ]
+
+    {_text, mentions, []} = Formatter.linkify(text)
+
+    assert expected_mentions ==
+             Enum.map(mentions, fn {username, actor} -> {username, actor.id} end)
+  end
+
+  test "it escapes HTML in plain text" do
+    text = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
+    expected = "hello &amp; world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
+
+    assert Formatter.html_escape(text, "text/plain") == expected
+  end
+end
diff --git a/test/mobilizon_web/resolvers/event_resolver_test.exs b/test/mobilizon_web/resolvers/event_resolver_test.exs
index 013a54260..3cce2fd09 100644
--- a/test/mobilizon_web/resolvers/event_resolver_test.exs
+++ b/test/mobilizon_web/resolvers/event_resolver_test.exs
@@ -84,6 +84,44 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
       assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event"
     end
 
+    test "create_event/3 creates an event with tags", %{conn: conn, actor: actor, user: user} do
+      mutation = """
+          mutation {
+              createEvent(
+                  title: "my event is referenced",
+                  description: "with tags!",
+                  begins_on: "#{
+        DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
+      }",
+                  organizer_actor_id: "#{actor.id}",
+                  category: "birthday",
+                  tags: ["nicolas", "birthday", "bad tag"]
+              ) {
+                title,
+                uuid,
+                tags {
+                  title,
+                  slug
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert json_response(res, 200)["errors"] == nil
+      assert json_response(res, 200)["data"]["createEvent"]["title"] == "my event is referenced"
+
+      assert json_response(res, 200)["data"]["createEvent"]["tags"] == [
+               %{"slug" => "nicolas", "title" => "nicolas"},
+               %{"slug" => "birthday", "title" => "birthday"},
+               %{"slug" => "bad-tag", "title" => "bad tag"}
+             ]
+    end
+
     test "create_event/3 creates an event with an attached picture", %{
       conn: conn,
       actor: actor,