static void
infinoted_plugin_document_stream_subscribe_done(
  InfinotedPluginDocumentStreamStream* stream,
  InfSessionProxy* proxy)
{
  InfSession* session;
  GParameter params[2] = {
    { "name", { 0 } },
    { "status", { 0 } }
  };

  g_assert(stream->proxy == NULL);
  stream->proxy = proxy;
  g_object_ref(proxy);

  g_object_get(G_OBJECT(proxy), "session", &session, NULL);

  /* User join via document stream only works for chat sessions
   * at the moment. */
  if(stream->username == NULL || *stream->username == '\0' ||
     INF_TEXT_IS_SESSION(session))
  {
    infinoted_plugin_document_stream_start(stream);
  }
  else if(INF_IS_CHAT_SESSION(session))
  {
    g_value_init(&params[0].value, G_TYPE_STRING);
    g_value_set_static_string(&params[0].value, stream->username);

    g_value_init(&params[1].value, INF_TYPE_USER_STATUS);
    g_value_set_enum(&params[1].value, INF_USER_ACTIVE);

    /* Join a user */
    stream->user_request = inf_session_proxy_join_user(
      INF_SESSION_PROXY(proxy),
      2,
      params,
      infinoted_plugin_document_stream_user_join_func,
      stream
    );
  }
  else
  {
    g_assert_not_reached();
  }

  g_object_unref(session);
}
static void
infinoted_plugin_document_stream_start(
  InfinotedPluginDocumentStreamStream* stream)
{
  InfSession* session;
  InfBuffer* buffer;

  g_object_get(G_OBJECT(stream->proxy), "session", &session, NULL);

  buffer = inf_session_get_buffer(session);
  stream->buffer = buffer;
  g_object_ref(buffer);

  if(INF_TEXT_IS_SESSION(session))
  {
    infinoted_plugin_document_stream_sync_text(stream);

    g_signal_connect(
      G_OBJECT(buffer),
      "text-inserted",
      G_CALLBACK(infinoted_plugin_document_stream_text_inserted_cb),
      stream
    );

    g_signal_connect(
      G_OBJECT(buffer),
      "text-erased",
      G_CALLBACK(infinoted_plugin_document_stream_text_erased_cb),
      stream
    );
  }
  else if(INF_IS_CHAT_SESSION(session))
  {
    infinoted_plugin_document_stream_sync_chat(stream);
    
    g_signal_connect_after(
      G_OBJECT(buffer),
      "add-message",
      G_CALLBACK(infinoted_plugin_document_stream_chat_add_message_cb),
      stream
    );
  }

  g_object_unref(session);
}
static void
infinoted_directory_sync_directory_remove_session_cb(InfdDirectory* directory,
                                                     InfdDirectoryIter* iter,
                                                     InfdSessionProxy* proxy,
                                                     gpointer user_data)
{
  InfinotedDirectorySync* dsync;
  InfinotedDirectorySyncSession* session;

  /* Ignore if this is not a text session */
  if(INF_TEXT_IS_SESSION(infd_session_proxy_get_session(proxy)))
  {
    dsync = (InfinotedDirectorySync*)user_data;
    session = infinoted_directory_sync_find_session(dsync, iter);
    g_assert(session != NULL && session->proxy == proxy);

    infinoted_directory_sync_remove_session(dsync, session);
  }
}
static gboolean
infinoted_directory_sync_add_session(InfinotedDirectorySync* dsync,
                                     InfdDirectoryIter* iter,
                                     GError** error)
{
  InfinotedDirectorySyncSession* session;
  InfdSessionProxy* proxy;
  InfBuffer* buffer;
  gchar* iter_path;
#ifdef G_OS_WIN32
  gchar* pos;
#endif
  gchar* full_path;
  gchar* converted;

  g_assert(infinoted_directory_sync_find_session(dsync, iter) == NULL);

  proxy = infd_directory_iter_peek_session(dsync->directory, iter);
  g_assert(proxy != NULL);

  /* Ignore if this is not a text session */
  if(!INF_TEXT_IS_SESSION(infd_session_proxy_get_session(proxy)))
    return TRUE;

  iter_path = infd_directory_iter_get_path(dsync->directory, iter);
#ifdef G_OS_WIN32
  for(pos = iter_path; *pos != '\0'; ++pos)
  {
    if(*pos == '\\')
    {
      g_set_error(
        error,
        infinoted_directory_sync_error_quark(),
        INFINOTED_DIRECTORY_SYNC_ERROR_INVALID_PATH,
        _("Node \"%s\" contains invalid characters"),
        iter_path
      );

      g_free(iter_path);
      return FALSE;
    }
    else if(*pos == '/')
    {
      *pos = '\\';
    }
  }
#endif

  full_path = g_build_filename(dsync->sync_directory, iter_path+1, NULL);
  g_free(iter_path);

  converted = g_filename_from_utf8(full_path, -1, NULL, NULL, error);
  g_free(full_path);
  if(!converted) return FALSE;

  session = g_slice_new(InfinotedDirectorySyncSession);
  session->dsync = dsync;
  session->iter = *iter;

  session->proxy = proxy;
  session->timeout = NULL;
  session->path = converted;

  dsync->sessions = g_slist_prepend(dsync->sessions, session);

  buffer = inf_session_get_buffer(infd_session_proxy_get_session(proxy));

  g_signal_connect(
    G_OBJECT(buffer),
    "text-inserted",
    G_CALLBACK(infinoted_directory_sync_buffer_text_inserted_cb),
    session
  );

  g_signal_connect(
    G_OBJECT(buffer),
    "text-erased",
    G_CALLBACK(infinoted_directory_sync_buffer_text_erased_cb),
    session
  );

  infinoted_directory_sync_session_save(dsync, session);
  return TRUE;
}
static gboolean
infd_note_plugin_text_session_write(InfdStorage* storage,
                                    InfSession* session,
                                    const gchar* path,
                                    gpointer user_data,
                                    GError** error)
{
  InfUserTable* table;
  InfTextBuffer* buffer;
  InfTextBufferIter* iter;
  xmlNodePtr root;
  xmlNodePtr buffer_node;
  xmlNodePtr segment_node;

  guint author;
  gchar* content;
  gsize bytes;

  FILE* stream;
  xmlDocPtr doc;
  xmlErrorPtr xmlerror;

  g_assert(INFD_IS_FILESYSTEM_STORAGE(storage));
  g_assert(INF_TEXT_IS_SESSION(session));

  /* Open stream before exporting buffer to XML so possible errors are
   * catched earlier. */
  stream = infd_filesystem_storage_open(
    INFD_FILESYSTEM_STORAGE(storage),
    "InfText",
    path,
    "w",
    error
  );

  if(stream == NULL)
    return FALSE;

  root = xmlNewNode(NULL, (const xmlChar*)"inf-text-session");
  buffer = INF_TEXT_BUFFER(inf_session_get_buffer(session));
  table = inf_session_get_user_table(session);

  inf_user_table_foreach_user(
    table,
    infd_note_plugin_text_session_write_foreach_user_func,
    root
  );

  buffer_node = xmlNewChild(root, NULL, (const xmlChar*)"buffer", NULL);
  iter = inf_text_buffer_create_iter(buffer);
  if(iter != NULL)
  {
    do
    {
      author = inf_text_buffer_iter_get_author(buffer, iter);
      content = inf_text_buffer_iter_get_text(buffer, iter);
      bytes = inf_text_buffer_iter_get_bytes(buffer, iter);

      segment_node = xmlNewChild(
        buffer_node,
        NULL,
        (const xmlChar*)"segment",
        NULL
      );

      inf_xml_util_set_attribute_uint(segment_node, "author", author);
      inf_xml_util_add_child_text(segment_node, content, bytes);
      g_free(content);
    } while(inf_text_buffer_iter_next(buffer, iter));

    inf_text_buffer_destroy_iter(buffer, iter);
  }

  doc = xmlNewDoc((const xmlChar*)"1.0");
  xmlDocSetRootElement(doc, root);

  if(xmlDocFormatDump(stream, doc, 1) == -1)
  {
    xmlerror = xmlGetLastError();
    fclose(stream);
    xmlFreeDoc(doc);

    g_set_error(
      error,
      g_quark_from_static_string("LIBXML2_OUTPUT_ERROR"),
      xmlerror->code,
      "%s",
      xmlerror->message
    );

    return FALSE;
  }

  fclose(stream);
  xmlFreeDoc(doc);
  return TRUE;
}