# HG changeset patch
# Node ID d11ff136705e
# Parent  77c928fde86b
diff --git a/ChangeLog.API b/ChangeLog.API
--- a/ChangeLog.API
+++ b/ChangeLog.API
@@ -331,6 +331,8 @@
 		* network-configuration-changed signal
 		* PurpleAccount->ui_data
 		* PurpleAccountPrefsUiOps
+		* PurpleAccountRequestType
+		* PurpleAccountUiOps:request_authorization
 		* purple_account_add_buddies_with_invite
 		* purple_account_add_buddy_with_invite
 		* purple_account_destroy_log
@@ -343,6 +345,8 @@
 		* purple_account_get_ui_string
 		* purple_account_get_ui_bool
 		* purple_account_notify_added
+		* purple_account_request_authorization
+		* purple_account_request_close
 		* purple_account_set_check_mail
 		* purple_account_set_current_error
 		* purple_account_set_password.  Use
diff --git a/finch/finchnotifications.c b/finch/finchnotifications.c
--- a/finch/finchnotifications.c
+++ b/finch/finchnotifications.c
@@ -63,8 +63,9 @@
 	if(PURPLE_IS_NOTIFICATION(notification)) {
 		PurpleNotificationManager *manager = NULL;
 
+		purple_notification_delete(notification);
+
 		manager = purple_notification_manager_get_default();
-
 		purple_notification_manager_remove(manager, notification);
 	}
 }
@@ -123,6 +124,59 @@
 	gnt_widget_destroy(GNT_WIDGET(data));
 }
 
+static void
+finch_notification_contact_authorize(G_GNUC_UNUSED GntWidget *widget,
+                                     gpointer data)
+{
+	PurpleAccount *account = NULL;
+	PurpleNotification *notification = NULL;
+	PurpleNotificationManager *manager = NULL;
+	PurpleAuthorizationRequest *auth_request = NULL;
+	const gchar *alias = NULL, *username = NULL;
+
+	/* Get the notification and authorization request from the data. */
+	notification = g_object_get_data(data, "notification");
+	auth_request = purple_notification_get_data(notification);
+
+	/* Accept the authorization request. */
+	purple_authorization_request_accept(auth_request);
+
+	/* Remove the notification from the manager. */
+	manager = purple_notification_manager_get_default();
+	purple_notification_manager_remove(manager, notification);
+
+	/* Request the user to add the person they just authorized. */
+	account = purple_authorization_request_get_account(auth_request);
+	alias = purple_authorization_request_get_alias(auth_request);
+	username = purple_authorization_request_get_username(auth_request);
+	purple_blist_request_add_buddy(account, username, NULL, alias);
+
+	/* Destroy the dialog. */
+	gnt_widget_destroy(GNT_WIDGET(data));
+}
+
+static void
+finch_notification_contact_deny(G_GNUC_UNUSED GntWidget *widget, gpointer data)
+{
+	PurpleNotification *notification = NULL;
+	PurpleNotificationManager *manager = NULL;
+	PurpleAuthorizationRequest *auth_request = NULL;
+
+	/* Get the notification and authorization request from the data. */
+	notification = g_object_get_data(data, "notification");
+	auth_request = purple_notification_get_data(notification);
+
+	/* Deny the request. */
+	purple_authorization_request_deny(auth_request, NULL);
+
+	/* Remove the notification from the manager. */
+	manager = purple_notification_manager_get_default();
+	purple_notification_manager_remove(manager, notification);
+
+	/* Destroy the dialog. */
+	gnt_widget_destroy(GNT_WIDGET(data));
+}
+
 /*******************************************************************************
  * Finch Notification API
  ******************************************************************************/
@@ -155,7 +209,7 @@
 	} else if(type == PURPLE_NOTIFICATION_TYPE_CONNECTION_ERROR) {
 		PurpleConnectionErrorInfo *info = data;
 
-		/* Set the title and name */
+		/* Set the title. */
 		gnt_box_set_title(GNT_BOX(dialog), _("Connection Error"));
 
 		/* Add the connection error reason. */
@@ -181,6 +235,33 @@
 		                 G_CALLBACK(finch_notification_modify_account),
 		                 dialog);
 		gnt_box_add_widget(GNT_BOX(hbox), button);
+	} else if(type == PURPLE_NOTIFICATION_TYPE_AUTHORIZATION_REQUEST) {
+		PurpleAuthorizationRequest *auth_request = NULL;
+		const gchar *message = NULL;
+
+		/* Set the title. */
+		gnt_box_set_title(GNT_BOX(dialog), _("Authorization Request"));
+
+		auth_request = purple_notification_get_data(notification);
+		message = purple_authorization_request_get_message(auth_request);
+
+		/* Add the message if we have one. */
+		if(message != NULL && *message != '\0') {
+			label = gnt_label_new(message);
+			gnt_box_add_widget(GNT_BOX(dialog), label);
+		}
+
+		/* Add the buttons. */
+		button = gnt_button_new(_("Authorize"));
+		g_signal_connect(button, "activate",
+		                 G_CALLBACK(finch_notification_contact_authorize),
+		                 dialog);
+		gnt_box_add_widget(GNT_BOX(hbox), button);
+
+		button = gnt_button_new(_("Deny"));
+		g_signal_connect(button, "activate",
+		                 G_CALLBACK(finch_notification_contact_deny), dialog);
+		gnt_box_add_widget(GNT_BOX(hbox), button);
 	}
 
 	gnt_box_add_widget(GNT_BOX(dialog), hbox);
diff --git a/finch/gntaccount.c b/finch/gntaccount.c
--- a/finch/gntaccount.c
+++ b/finch/gntaccount.c
@@ -651,6 +651,7 @@
 static void
 really_delete_account(PurpleAccount *account)
 {
+	PurpleNotificationManager *manager = NULL;
 	GList *iter;
 	for (iter = accountdialogs; iter; iter = iter->next)
 	{
@@ -661,7 +662,10 @@
 			break;
 		}
 	}
-	purple_request_close_with_handle(account); /* Close any other opened delete window */
+
+	manager = purple_notification_manager_get_default();
+	purple_notification_manager_remove_with_account(manager, account);
+
 	purple_accounts_delete(account);
 }
 
@@ -982,124 +986,6 @@
 } auth_and_add;
 
 static void
-free_auth_and_add(auth_and_add *aa)
-{
-	g_free(aa->username);
-	g_free(aa->alias);
-	g_free(aa);
-}
-
-static void
-authorize_and_add_cb(auth_and_add *aa)
-{
-	aa->auth_cb(NULL, aa->data);
-	purple_blist_request_add_buddy(aa->account, aa->username,
-	 	                    NULL, aa->alias);
-}
-
-static void
-deny_no_add_cb(auth_and_add *aa)
-{
-	aa->deny_cb(NULL, aa->data);
-}
-
-static void *
-finch_request_authorize(PurpleAccount *account,
-                        const char *remote_user,
-                        const char *id,
-                        const char *alias,
-                        const char *message,
-                        gboolean on_list,
-                        PurpleAccountRequestAuthorizationCb auth_cb,
-                        PurpleAccountRequestAuthorizationCb deny_cb,
-                        void *user_data)
-{
-	char *buffer;
-	PurpleConnection *gc;
-	void *uihandle;
-
-	gc = purple_account_get_connection(account);
-	if (message != NULL && *message == '\0')
-		message = NULL;
-
-	buffer = g_strdup_printf(_("%s%s%s%s wants to add %s to his or her buddy list%s%s"),
-				remote_user,
-	 	                (alias != NULL ? " ("  : ""),
-		                (alias != NULL ? alias : ""),
-		                (alias != NULL ? ")"   : ""),
-		                (id != NULL
-		                ? id
-		                : (purple_connection_get_display_name(gc) != NULL
-		                ? purple_connection_get_display_name(gc)
-		                : purple_account_get_username(account))),
-		                (message != NULL ? ": " : "."),
-		                (message != NULL ? message  : ""));
-	if (!on_list) {
-		GntWidget *widget;
-		GList *iter;
-		auth_and_add *aa = g_new(auth_and_add, 1);
-
-		aa->auth_cb = auth_cb;
-		aa->deny_cb = deny_cb;
-		aa->data = user_data;
-		aa->username = g_strdup(remote_user);
-		aa->alias = g_strdup(alias);
-		aa->account = account;
-
-		uihandle = gnt_vwindow_new(FALSE);
-		gnt_box_set_title(GNT_BOX(uihandle), _("Authorize buddy?"));
-		gnt_box_set_pad(GNT_BOX(uihandle), 0);
-
-		widget = purple_request_action(NULL, _("Authorize buddy?"), buffer, NULL,
-			PURPLE_DEFAULT_ACTION_NONE,
-			purple_request_cpar_from_account(account),
-			aa, 2,
-			_("Authorize"), authorize_and_add_cb,
-			_("Deny"), deny_no_add_cb);
-		/* Since GntWindow is a GntBox, hide it so it's unmapped, then
-		 * add it to the outer window, and make it visible again. */
-		gnt_widget_hide(widget);
-		gnt_box_set_toplevel(GNT_BOX(widget), FALSE);
-		gnt_box_add_widget(GNT_BOX(uihandle), widget);
-		gnt_widget_set_visible(widget, TRUE);
-
-		gnt_box_add_widget(GNT_BOX(uihandle), gnt_hline_new());
-
-		widget = finch_retrieve_user_info(purple_account_get_connection(account), remote_user);
-		for (iter = gnt_box_get_children(GNT_BOX(widget)); iter;
-		     iter = g_list_delete_link(iter, iter)) {
-			if (GNT_IS_BUTTON(iter->data)) {
-				gnt_widget_destroy(iter->data);
-				gnt_box_remove(GNT_BOX(widget), iter->data);
-				g_list_free(iter);
-				break;
-			}
-		}
-		/* Since GntWindow is a GntBox, hide it so it's unmapped, then
-		 * add it to the outer window, and make it visible again. */
-		gnt_widget_hide(widget);
-		gnt_box_set_toplevel(GNT_BOX(widget), FALSE);
-		gnt_box_add_widget(GNT_BOX(uihandle), widget);
-		gnt_widget_set_visible(widget, TRUE);
-
-		gnt_widget_show(uihandle);
-
-		g_signal_connect_swapped(G_OBJECT(uihandle), "destroy", G_CALLBACK(free_auth_and_add), aa);
-	} else {
-		uihandle = purple_request_action(NULL, _("Authorize buddy?"), buffer, NULL,
-			PURPLE_DEFAULT_ACTION_NONE,
-			purple_request_cpar_from_account(account),
-			user_data, 2,
-			_("Authorize"), auth_cb,
-			_("Deny"), deny_cb);
-	}
-	g_signal_connect(G_OBJECT(uihandle), "destroy",
-		G_CALLBACK(purple_account_request_close), NULL);
-	g_free(buffer);
-	return uihandle;
-}
-
-static void
 finch_request_close(void *uihandle)
 {
 	purple_request_close(PURPLE_REQUEST_ACTION, uihandle);
@@ -1108,7 +994,6 @@
 static PurpleAccountUiOps ui_ops =
 {
 	.request_add = request_add,
-	.request_authorize = finch_request_authorize,
 	.close_account_request = finch_request_close,
 };
 
diff --git a/libpurple/account.h b/libpurple/account.h
--- a/libpurple/account.h
+++ b/libpurple/account.h
@@ -52,17 +52,6 @@
 #include "xmlnode.h"
 
 /**
- * PurpleAccountRequestType:
- * @PURPLE_ACCOUNT_REQUEST_AUTHORIZATION: Account authorization request
- *
- * Account request types.
- */
-typedef enum
-{
-	PURPLE_ACCOUNT_REQUEST_AUTHORIZATION = 0
-} PurpleAccountRequestType;
-
-/**
  * PurpleAccountRequestResponse:
  * @PURPLE_ACCOUNT_RESPONSE_IGNORE: Silently ignore the request.
  * @PURPLE_ACCOUNT_RESPONSE_DENY: Block the request potentially informing the
@@ -233,30 +222,6 @@
                               const char *message);
 
 /**
- * purple_account_request_authorization:
- * @account:      The account that was added
- * @remote_user:  The name of the user that added this account.
- * @id:           The optional ID of the local account. Rarely used.
- * @alias:        The optional alias of the remote user.
- * @message:      The optional message sent by the user wanting to add you.
- * @on_list:      Is the remote user already on the buddy list?
- * @auth_cb:      (scope call): The callback called when the local user accepts
- * @deny_cb:      (scope call): The callback called when the local user rejects
- * @user_data:    Data to be passed back to the above callbacks
- *
- * Notifies the user that a remote user has wants to add the local user
- * to his or her buddy list and requires authorization to do so.
- *
- * This will present a dialog informing the user of this and ask if the
- * user authorizes or denies the remote user from adding him.
- *
- * Returns: A UI-specific handle.
- */
-void *purple_account_request_authorization(PurpleAccount *account, const char *remote_user,
-					const char *id, const char *alias, const char *message, gboolean on_list,
-					PurpleAccountRequestAuthorizationCb auth_cb, PurpleAccountRequestAuthorizationCb deny_cb, void *user_data);
-
-/**
  * purple_account_request_close_with_account:
  * @account:	   The account for which requests should be closed
  *
diff --git a/libpurple/account.c b/libpurple/account.c
--- a/libpurple/account.c
+++ b/libpurple/account.c
@@ -109,18 +109,6 @@
 
 typedef struct
 {
-	PurpleAccountRequestType type;
-	PurpleAccount *account;
-	void *ui_handle;
-	char *user;
-	gpointer userdata;
-	PurpleAccountRequestAuthorizationCb auth_cb;
-	PurpleAccountRequestAuthorizationCb deny_cb;
-	guint ref;
-} PurpleAccountRequestInfo;
-
-typedef struct
-{
 	PurpleAccount *account;
 	GCallback cb;
 	gpointer data;
@@ -144,7 +132,6 @@
 };
 
 static GParamSpec    *properties[PROP_LAST];
-static GList         *handles = NULL;
 
 G_DEFINE_TYPE_WITH_PRIVATE(PurpleAccount, purple_account, G_TYPE_OBJECT);
 
@@ -355,63 +342,6 @@
 	g_free(password);
 }
 
-static PurpleAccountRequestInfo *
-purple_account_request_info_unref(PurpleAccountRequestInfo *info)
-{
-	if (--info->ref)
-		return info;
-
-	/* TODO: This will leak info->user_data, but there is no callback to just clean that up */
-	g_free(info->user);
-	g_free(info);
-	return NULL;
-}
-
-static void
-purple_account_request_close_info(PurpleAccountRequestInfo *info)
-{
-	PurpleAccountUiOps *ops;
-
-	ops = purple_accounts_get_ui_ops();
-
-	if (ops != NULL && ops->close_account_request != NULL)
-		ops->close_account_request(info->ui_handle);
-
-	purple_account_request_info_unref(info);
-}
-
-static void
-request_auth_cb(const char *message, void *data)
-{
-	PurpleAccountRequestInfo *info = data;
-
-	handles = g_list_remove(handles, info);
-
-	if (info->auth_cb != NULL)
-		info->auth_cb(message, info->userdata);
-
-	purple_signal_emit(purple_accounts_get_handle(),
-			"account-authorization-granted", info->account, info->user, message);
-
-	purple_account_request_info_unref(info);
-}
-
-static void
-request_deny_cb(const char *message, void *data)
-{
-	PurpleAccountRequestInfo *info = data;
-
-	handles = g_list_remove(handles, info);
-
-	if (info->deny_cb != NULL)
-		info->deny_cb(message, info->userdata);
-
-	purple_signal_emit(purple_accounts_get_handle(),
-			"account-authorization-denied", info->account, info->user, message);
-
-	purple_account_request_info_unref(info);
-}
-
 static void
 change_password_cb(PurpleAccount *account, PurpleRequestFields *fields)
 {
@@ -1247,108 +1177,14 @@
 		ui_ops->request_add(account, remote_user, id, alias, message);
 }
 
-void *
-purple_account_request_authorization(PurpleAccount *account, const char *remote_user,
-				     const char *id, const char *alias, const char *message, gboolean on_list,
-				     PurpleAccountRequestAuthorizationCb auth_cb, PurpleAccountRequestAuthorizationCb deny_cb, void *user_data)
-{
-	PurpleAccountUiOps *ui_ops;
-	PurpleAccountRequestInfo *info;
-	int plugin_return;
-	char *response = NULL;
-
-	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
-	g_return_val_if_fail(remote_user != NULL, NULL);
-
-	ui_ops = purple_accounts_get_ui_ops();
-
-	plugin_return = GPOINTER_TO_INT(
-			purple_signal_emit_return_1(
-				purple_accounts_get_handle(),
-				"account-authorization-requested",
-				account, remote_user, message, &response
-			));
-
-	switch (plugin_return)
-	{
-		case PURPLE_ACCOUNT_RESPONSE_IGNORE:
-			g_free(response);
-			return NULL;
-		case PURPLE_ACCOUNT_RESPONSE_ACCEPT:
-			if (auth_cb != NULL)
-				auth_cb(response, user_data);
-			g_free(response);
-			return NULL;
-		case PURPLE_ACCOUNT_RESPONSE_DENY:
-			if (deny_cb != NULL)
-				deny_cb(response, user_data);
-			g_free(response);
-			return NULL;
-	}
-
-	g_free(response);
-
-	if (ui_ops != NULL && ui_ops->request_authorize != NULL) {
-		info            = g_new0(PurpleAccountRequestInfo, 1);
-		info->type      = PURPLE_ACCOUNT_REQUEST_AUTHORIZATION;
-		info->account   = account;
-		info->auth_cb   = auth_cb;
-		info->deny_cb   = deny_cb;
-		info->userdata  = user_data;
-		info->user      = g_strdup(remote_user);
-		info->ref       = 2;  /* We hold an extra ref to make sure info remains valid
-		                         if any of the callbacks are called synchronously. We
-		                         unref it after the function call */
-
-		info->ui_handle = ui_ops->request_authorize(account, remote_user, id, alias, message,
-							    on_list, request_auth_cb, request_deny_cb, info);
-
-		info = purple_account_request_info_unref(info);
-		if (info) {
-			handles = g_list_append(handles, info);
-			return info->ui_handle;
-		}
-	}
-
-	return NULL;
-}
-
 void
-purple_account_request_close_with_account(PurpleAccount *account)
-{
-	GList *l, *l_next;
+purple_account_request_close_with_account(PurpleAccount *account) {
+	PurpleNotificationManager *manager = NULL;
 
 	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
 
-	for (l = handles; l != NULL; l = l_next) {
-		PurpleAccountRequestInfo *info = l->data;
-
-		l_next = l->next;
-
-		if (info->account == account) {
-			handles = g_list_delete_link(handles, l);
-			purple_account_request_close_info(info);
-		}
-	}
-}
-
-void
-purple_account_request_close(void *ui_handle)
-{
-	GList *l, *l_next;
-
-	g_return_if_fail(ui_handle != NULL);
-
-	for (l = handles; l != NULL; l = l_next) {
-		PurpleAccountRequestInfo *info = l->data;
-
-		l_next = l->next;
-
-		if (info->ui_handle == ui_handle) {
-			handles = g_list_delete_link(handles, l);
-			purple_account_request_close_info(info);
-		}
-	}
+	manager = purple_notification_manager_get_default();
+	purple_notification_manager_remove_with_account(manager, account);
 }
 
 void
diff --git a/libpurple/accounts.h b/libpurple/accounts.h
--- a/libpurple/accounts.h
+++ b/libpurple/accounts.h
@@ -38,13 +38,6 @@
  * @status_changed:        This account's status changed.
  * @request_add:           Someone we don't have on our list added us; prompt
  *                         to add them.
- * @request_authorize:     Prompt for authorization when someone adds this
- *                         account to their buddy list.  To authorize them to
- *                         see this account's presence, call
- *                         @authorize_cb (@message, @user_data) otherwise call
- *                         @deny_cb (@message, @user_data).
- *                         <sbr/>Returns: A UI-specific handle, as passed to
- *                         @close_account_request.
  * @close_account_request: Close a pending request for authorization.
  *                         @ui_handle is a handle as returned by
  *                         @request_authorize.
@@ -69,16 +62,6 @@
 	                    const char *alias,
 	                    const char *message);
 
-	void *(*request_authorize)(PurpleAccount *account,
-	                           const char *remote_user,
-	                           const char *id,
-	                           const char *alias,
-	                           const char *message,
-	                           gboolean on_list,
-	                           PurpleAccountRequestAuthorizationCb authorize_cb,
-	                           PurpleAccountRequestAuthorizationCb deny_cb,
-	                           void *user_data);
-
 	void (*close_account_request)(void *ui_handle);
 
 	void (*permit_added)(PurpleAccount *account, const char *name);
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -41,6 +41,7 @@
 	'purpleaccountpresence.c',
 	'purpleaccountusersplit.c',
 	'purpleattachment.c',
+	'purpleauthorizationrequest.c',
 	'purplebuddypresence.c',
 	'purplechatconversation.c',
 	'purplechatuser.c',
@@ -141,6 +142,7 @@
 	'purpleaccountoption.h',
 	'purpleaccountpresence.h',
 	'purpleaccountusersplit.h',
+	'purpleauthorizationrequest.h',
 	'purplebuddypresence.h',
 	'purplechatconversation.h',
 	'purplechatuser.h',
diff --git a/libpurple/protocols/jabber/presence.c b/libpurple/protocols/jabber/presence.c
--- a/libpurple/protocols/jabber/presence.c
+++ b/libpurple/protocols/jabber/presence.c
@@ -346,7 +346,9 @@
 	char *who;
 };
 
-static void authorize_add_cb(const char *message, gpointer data)
+static void
+authorize_add_cb(G_GNUC_UNUSED PurpleAuthorizationRequest *request,
+                 gpointer data)
 {
 	struct _jabber_add_permit *jap = data;
 
@@ -359,7 +361,9 @@
 	g_free(jap);
 }
 
-static void deny_add_cb(const char *message, gpointer data)
+static void
+deny_add_cb(G_GNUC_UNUSED PurpleAuthorizationRequest *request,
+            const char *message, gpointer data)
 {
 	struct _jabber_add_permit *jap = data;
 
@@ -927,6 +931,9 @@
 		struct _jabber_add_permit *jap = g_new0(struct _jabber_add_permit, 1);
 		gboolean onlist = FALSE;
 		PurpleAccount *account;
+		PurpleAuthorizationRequest *request = NULL;
+		PurpleNotification *notification = NULL;
+		PurpleNotificationManager *manager = NULL;
 		PurpleBuddy *buddy;
 		PurpleXmlNode *nick;
 
@@ -945,8 +952,18 @@
 		jap->who = g_strdup(presence.from);
 		jap->js = js;
 
-		purple_account_request_authorization(account, presence.from, NULL, presence.nickname,
-				NULL, onlist, authorize_add_cb, deny_add_cb, jap);
+		request = purple_authorization_request_new(account, presence.from);
+		purple_authorization_request_set_alias(request, presence.nickname);
+		purple_authorization_request_set_add(request, !onlist);
+		g_signal_connect(request, "accepted", G_CALLBACK(authorize_add_cb),
+		                 jap);
+		g_signal_connect(request, "denied", G_CALLBACK(deny_add_cb), jap);
+
+		notification = purple_notification_new_from_authorization_request(request);
+		manager = purple_notification_manager_get_default();
+		purple_notification_manager_add(manager, notification);
+
+		g_object_unref(notification);
 
 		goto out;
 	} else if (presence.type == JABBER_PRESENCE_SUBSCRIBED) {
diff --git a/libpurple/purpleauthorizationrequest.h b/libpurple/purpleauthorizationrequest.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleauthorizationrequest.h
@@ -0,0 +1,192 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_AUTHORIZATION_REQUEST_H
+#define PURPLE_AUTHORIZATION_REQUEST_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "account.h"
+
+G_BEGIN_DECLS
+
+/**
+ * PurpleAuthorizationRequest:
+ *
+ * #PurpleAuthorizationRequest is a data structure that contains all of the
+ * information when someone has requested authorization to add you to their
+ * contact list.
+ *
+ * Since: 3.0.0
+ */
+
+#define PURPLE_TYPE_AUTHORIZATION_REQUEST (purple_authorization_request_get_type())
+
+G_DECLARE_FINAL_TYPE(PurpleAuthorizationRequest, purple_authorization_request,
+                     PURPLE, AUTHORIZATION_REQUEST, GObject)
+
+/**
+ * purple_authorization_request_new:
+ * @account: The account that this request is for.
+ * @username: The username of the user requesting authorization.
+ *
+ * Creates a new [class@Purple.AuthorizationRequest] for @username on @account.
+ *
+ * This is typically only used by libpurple itself.
+ *
+ * Returns: The new instance.
+ *
+ * Since: 3.0.0
+ */
+PurpleAuthorizationRequest *purple_authorization_request_new(PurpleAccount *account, const gchar *username);
+
+/**
+ * purple_authorization_request_get_account:
+ * @request: The instance.
+ *
+ * Gets the [class@Account] for @request.
+ *
+ * Returns: (transfer none): The account.
+ *
+ * Since: 3.0.0
+ */
+PurpleAccount *purple_authorization_request_get_account(PurpleAuthorizationRequest *request);
+
+/**
+ * purple_authorization_request_get_remote_username:
+ * @request: The instance.
+ *
+ * Gets the username for the user requesting authorization.
+ *
+ * Returns: The username of the remote user.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_authorization_request_get_username(PurpleAuthorizationRequest *request);
+
+/**
+ * purple_authorization_request_set_alias:
+ * @request: The instance.
+ * @alias: (nullable): The alias of the remote user.
+ *
+ * Sets the alias of the remote user to @alias. User interfaces can use this
+ * when presenting the authorization request to the end user.
+ *
+ * Since: 3.0.0
+ */
+void purple_authorization_request_set_alias(PurpleAuthorizationRequest *request, const gchar *alias);
+
+/**
+ * purple_authorization_request_get_alias:
+ * @request: The instance.
+ *
+ * Gets the alias of the remote user if one was set.
+ *
+ * Returns: (nullable): The alias if one was set.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_authorization_request_get_alias(PurpleAuthorizationRequest *request);
+
+/**
+ * purple_authorization_request_set_message:
+ * @request: The instance.
+ * @message: (nullable): An optional message from the remote user.
+ *
+ * Sets an optional message from remote user, that the user interface can
+ * display to the end user.
+ *
+ * Since: 3.0.0
+ */
+void purple_authorization_request_set_message(PurpleAuthorizationRequest *request, const gchar *message);
+
+/**
+ * purple_authorization_request_get_message:
+ * @request: The instance.
+ *
+ * Gets the message that was optionally sent by the remote user.
+ *
+ * Returns: (nullable): The optional message.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_authorization_request_get_message(PurpleAuthorizationRequest *request);
+
+/**
+ * purple_authorization_request_set_add:
+ * @request: The instance.
+ * @add: Whether or not to ask the user to add the remote user back.
+ *
+ * Sets whether or not the user interface should ask the end user to add the
+ * remote user if the remote user was accepted.
+ *
+ * Since: 3.0.0
+ */
+void purple_authorization_request_set_add(PurpleAuthorizationRequest *request, gboolean add);
+
+/**
+ * purple_authorization_request_get_add:
+ * @request: The instance.
+ *
+ * Gets whether or not the user interface should ask the end user to add the
+ * remote user if the end user accepted the remote user's friend request.
+ *
+ * Returns: %TRUE if the user interface should request the end user to add the
+ *          remote user back.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_authorization_request_get_add(PurpleAuthorizationRequest *request);
+
+/**
+ * purple_authorization_request_accept:
+ * @request: The instance.
+ *
+ * Emits the [signal@AuthorizationRequest::accepted] signal. This is typically
+ * called by the user interface when the user has clicked the accept button.
+ *
+ * If this is called multiple times, or called after
+ * [method@AuthorizationRequest.deny] then this does nothing.
+ *
+ * Since: 3.0.0
+ */
+void purple_authorization_request_accept(PurpleAuthorizationRequest *request);
+
+/**
+ * purple_authorization_request_deny:
+ * @request: The instance.
+ * @message: (nullable): An optional denial message.
+ *
+ * Emits the [signal@AuthorizationRequest::denied] signal. This is typically
+ * called by the user interface when the user has clicked the deny button.
+ *
+ * If this is called multiple times, or called after
+ * [method@AuthorizationRequest.accept] then this does nothing.
+ *
+ * Since: 3.0.0
+ */
+void purple_authorization_request_deny(PurpleAuthorizationRequest *request, const gchar *message);
+
+G_END_DECLS
+
+#endif /* PURPLE_AUTHORIZATION_REQUEST */
diff --git a/libpurple/purpleauthorizationrequest.c b/libpurple/purpleauthorizationrequest.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleauthorizationrequest.c
@@ -0,0 +1,409 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "purpleauthorizationrequest.h"
+
+struct _PurpleAuthorizationRequest {
+	GObject parent;
+
+	PurpleAccount *account;
+
+	gchar *username;
+	gchar *alias;
+	gchar *message;
+	gboolean add;
+
+	/* This tracks whether _accept or _deny have been called. */
+	gboolean handled;
+};
+
+enum {
+	SIG_ACCEPTED,
+	SIG_DENIED,
+	N_SIGNALS
+};
+static guint signals[N_SIGNALS] = {0, };
+
+enum {
+	PROP_0,
+	PROP_ACCOUNT,
+	PROP_USERNAME,
+	PROP_ALIAS,
+	PROP_MESSAGE,
+	PROP_ADD,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+G_DEFINE_TYPE(PurpleAuthorizationRequest, purple_authorization_request,
+              G_TYPE_OBJECT)
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_authorization_request_set_account(PurpleAuthorizationRequest *request,
+                                         PurpleAccount *account)
+{
+	g_return_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
+
+	if(g_set_object(&request->account, account)) {
+		g_object_notify_by_pspec(G_OBJECT(request), properties[PROP_ACCOUNT]);
+	}
+}
+
+static void
+purple_authorization_request_set_username(PurpleAuthorizationRequest *request,
+                                          const gchar *username)
+{
+	g_return_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	g_free(request->username);
+	request->username = g_strdup(username);
+
+	g_object_notify_by_pspec(G_OBJECT(request), properties[PROP_USERNAME]);
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+purple_authorization_request_get_property(GObject *obj, guint param_id,
+                                          GValue *value, GParamSpec *pspec)
+{
+	PurpleAuthorizationRequest *request = PURPLE_AUTHORIZATION_REQUEST(obj);
+
+	switch(param_id) {
+		case PROP_ACCOUNT:
+			g_value_set_object(value,
+			                   purple_authorization_request_get_account(request));
+			break;
+		case PROP_USERNAME:
+			g_value_set_string(value,
+			                   purple_authorization_request_get_username(request));
+			break;
+		case PROP_ALIAS:
+			g_value_set_string(value,
+			                   purple_authorization_request_get_alias(request));
+			break;
+		case PROP_MESSAGE:
+			g_value_set_string(value,
+			                   purple_authorization_request_get_message(request));
+			break;
+		case PROP_ADD:
+			g_value_set_boolean(value,
+			                    purple_authorization_request_get_add(request));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+purple_authorization_request_set_property(GObject *obj, guint param_id,
+                                          const GValue *value,
+                                          GParamSpec *pspec)
+{
+	PurpleAuthorizationRequest *request = PURPLE_AUTHORIZATION_REQUEST(obj);
+
+	switch(param_id) {
+		case PROP_ACCOUNT:
+			purple_authorization_request_set_account(request,
+			                                         g_value_get_object(value));
+			break;
+		case PROP_USERNAME:
+			purple_authorization_request_set_username(request,
+			                                          g_value_get_string(value));
+			break;
+		case PROP_ALIAS:
+			purple_authorization_request_set_alias(request,
+			                                       g_value_get_string(value));
+			break;
+		case PROP_MESSAGE:
+			purple_authorization_request_set_message(request,
+			                                         g_value_get_string(value));
+			break;
+		case PROP_ADD:
+			purple_authorization_request_set_add(request,
+			                                     g_value_get_boolean(value));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+purple_authorization_request_dispose(GObject *obj) {
+	PurpleAuthorizationRequest *request = PURPLE_AUTHORIZATION_REQUEST(obj);
+
+	g_clear_object(&request->account);
+
+	G_OBJECT_CLASS(purple_authorization_request_parent_class)->dispose(obj);
+}
+
+static void
+purple_authorization_request_finalize(GObject *obj) {
+	PurpleAuthorizationRequest *request = PURPLE_AUTHORIZATION_REQUEST(obj);
+
+	g_clear_pointer(&request->username, g_free);
+	g_clear_pointer(&request->alias, g_free);
+	g_clear_pointer(&request->message, g_free);
+
+	G_OBJECT_CLASS(purple_authorization_request_parent_class)->finalize(obj);
+}
+
+static void
+purple_authorization_request_init(PurpleAuthorizationRequest *request) {
+}
+
+static void
+purple_authorization_request_class_init(PurpleAuthorizationRequestClass *klass)
+{
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->get_property = purple_authorization_request_get_property;
+	obj_class->set_property = purple_authorization_request_set_property;
+	obj_class->dispose = purple_authorization_request_dispose;
+	obj_class->finalize = purple_authorization_request_finalize;
+
+	/**
+	 * PurpleAuthorizationRequest::accepted:
+	 * @request: The [class@AuthorizationRequest] instance.
+	 *
+	 * Emitted when the user has accepted @request. This is typically emitted
+	 * by the user interface calling [method@AuthorizationRequest.accept].
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_ACCEPTED] = g_signal_new_class_handler(
+		"accepted",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		0);
+
+	/**
+	 * PurpleAuthorizationRequest::denied:
+	 * @request: The [class@AuthorizationRequest] instance.
+	 * @message: (nullable): An optional denial message.
+	 *
+	 * Emitted when the user has denied @request. This is typically emitted
+	 * by the user interface calling [method@AuthorizationRequest.deny].
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_DENIED] = g_signal_new_class_handler(
+		"denied",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		G_TYPE_STRING);
+
+	/**
+	 * PurpleAuthorizationRequest:account: (attributes org.gtk.Property.get=purple_authorization_request_get_account)
+	 *
+	 * The account that this authorization request is for.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ACCOUNT] = g_param_spec_object(
+		"account", "account",
+		"The account for this authorization request",
+		PURPLE_TYPE_ACCOUNT,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleAuthorizationRequest:username: (attributes org.gtk.Property.get=purple_authorization_request_get_remote_user)
+	 *
+	 * The username of the remote user requesting authorization.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_USERNAME] = g_param_spec_string(
+		"username", "username",
+		"The username of the remote user requesting authorization",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleAuthorizationRequest:alias: (attributes org.gtk.Property.get=purple_authorization_request_get_remote_alias org.gtk.Property.set=purple_authorization_request_set_remote_alias)
+	 *
+	 * The alias of the remote user request authorization.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ALIAS] = g_param_spec_string(
+		"alias", "alias",
+		"The alias of the remote user request authorization",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleAuthorizationRequest:message: (attributes org.gtk.Property.get=purple_authorization_request_get_message org.gtk.Property.set=purple_authorization_request_set_message)
+	 *
+	 * The optional message sent from the remote user.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_MESSAGE] = g_param_spec_string(
+		"message", "message",
+		"The optional message sent by the remote user",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleAuthorizationRequest:add:
+	 *
+	 * Whether or not the user interface should ask the end user to add the
+	 * remote user after accepting the end user's friend request.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ADD] = g_param_spec_boolean(
+		"add", "add",
+		"Whether or not to add the remote user back",
+		FALSE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleAuthorizationRequest *
+purple_authorization_request_new(PurpleAccount *account, const gchar *username)
+{
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
+	g_return_val_if_fail(username != NULL, NULL);
+
+	return g_object_new(
+		PURPLE_TYPE_AUTHORIZATION_REQUEST,
+		"account", account,
+		"username", username,
+		NULL);
+}
+
+PurpleAccount *
+purple_authorization_request_get_account(PurpleAuthorizationRequest *request) {
+	g_return_val_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request), NULL);
+
+	return request->account;
+}
+
+const gchar *
+purple_authorization_request_get_username(PurpleAuthorizationRequest *request)
+{
+	g_return_val_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request), NULL);
+
+	return request->username;
+}
+
+void
+purple_authorization_request_set_alias(PurpleAuthorizationRequest *request,
+                                       const gchar *alias)
+{
+	g_return_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	g_free(request->alias);
+	request->alias = g_strdup(alias);
+
+	g_object_notify_by_pspec(G_OBJECT(request), properties[PROP_ALIAS]);
+}
+
+const gchar *
+purple_authorization_request_get_alias(PurpleAuthorizationRequest *request) {
+	g_return_val_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request), NULL);
+
+	return request->alias;
+}
+
+void
+purple_authorization_request_set_message(PurpleAuthorizationRequest *request,
+                                         const gchar *message)
+{
+	g_return_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	g_free(request->message);
+	request->message = g_strdup(message);
+
+	g_object_notify_by_pspec(G_OBJECT(request), properties[PROP_MESSAGE]);
+}
+
+const gchar *
+purple_authorization_request_get_message(PurpleAuthorizationRequest *request) {
+	g_return_val_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request), NULL);
+
+	return request->message;
+}
+
+void
+purple_authorization_request_set_add(PurpleAuthorizationRequest *request,
+                                     gboolean add)
+{
+	g_return_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	request->add = add;
+
+	g_object_notify_by_pspec(G_OBJECT(request), properties[PROP_ADD]);
+}
+
+gboolean
+purple_authorization_request_get_add(PurpleAuthorizationRequest *request) {
+	g_return_val_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request), FALSE);
+
+	return request->add;
+}
+
+void
+purple_authorization_request_accept(PurpleAuthorizationRequest *request) {
+	g_return_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	/* Calling this multiple times is a programming error. */
+	g_return_if_fail(request->handled == FALSE);
+
+	request->handled = TRUE;
+
+	g_signal_emit(request, signals[SIG_ACCEPTED], 0);
+}
+
+void
+purple_authorization_request_deny(PurpleAuthorizationRequest *request,
+                                  const gchar *message)
+{
+	g_return_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	/* Calling this multiple times is a programming error. */
+	g_return_if_fail(request->handled == FALSE);
+
+	request->handled = TRUE;
+
+	g_signal_emit(request, signals[SIG_DENIED], 0, message);
+}
diff --git a/libpurple/purplenotification.h b/libpurple/purplenotification.h
--- a/libpurple/purplenotification.h
+++ b/libpurple/purplenotification.h
@@ -27,6 +27,7 @@
 #include <glib-object.h>
 
 #include "account.h"
+#include "purpleauthorizationrequest.h"
 
 G_BEGIN_DECLS
 
@@ -39,7 +40,7 @@
     PURPLE_NOTIFICATION_TYPE_UNKNOWN,
     PURPLE_NOTIFICATION_TYPE_GENERIC,
     PURPLE_NOTIFICATION_TYPE_CONNECTION_ERROR,
-    PURPLE_NOTIFICATION_TYPE_CONTACT_AUTHORIZATION,
+    PURPLE_NOTIFICATION_TYPE_AUTHORIZATION_REQUEST,
     PURPLE_NOTIFICATION_TYPE_FILE_TRANSFER,
     PURPLE_NOTIFICATION_TYPE_CHAT_INVITE,
     PURPLE_NOTIFICATION_TYPE_MENTION,
@@ -78,6 +79,21 @@
 PurpleNotification *purple_notification_new(PurpleNotificationType type, PurpleAccount *account, gpointer data, GDestroyNotify data_destroy_func);
 
 /**
+ * purple_notification_new_from_authorization_request:
+ * @authorization_request: (transfer full): The [class@AuthorizationRequest]
+ *                         instance.
+ *
+ * Creates a new [class@PurpleNotification] for the @authorization_request. This
+ * helper will automatically fill out the notification according to the
+ * information in @authorization_request.
+ *
+ * Returns: (transfer full): The new notification.
+ *
+ * Since: 3.0.0
+ */
+PurpleNotification *purple_notification_new_from_authorization_request(PurpleAuthorizationRequest *authorization_request);
+
+/**
  * purple_notification_get_id:
  * @notification: The instance.
  *
@@ -260,6 +276,20 @@
  */
 gint purple_notification_compare(gconstpointer a, gconstpointer b);
 
+/**
+ * purple_notification_delete:
+ * @notification: The instance.
+ *
+ * Emits the [signal@PurpleNotification::deleted] signal. This is typically
+ * called by a user interface when the user has deleted a notification.
+ *
+ * If this is called more than once for @notification, the signal will not be
+ * emitted.
+ *
+ * Since: 3.0.0
+ */
+void purple_notification_delete(PurpleNotification *notification);
+
 G_END_DECLS
 
 #endif /* PURPLE_NOTIFICATION */
diff --git a/libpurple/purplenotification.c b/libpurple/purplenotification.c
--- a/libpurple/purplenotification.c
+++ b/libpurple/purplenotification.c
@@ -16,6 +16,8 @@
  * License along with this library; if not, see <https://www.gnu.org/licenses/>.
  */
 
+#include <glib/gi18n-lib.h>
+
 #include "purplenotification.h"
 
 #include "purpleenums.h"
@@ -35,9 +37,17 @@
 
 	gpointer data;
 	GDestroyNotify data_destroy_func;
+
+	gboolean deleted;
 };
 
 enum {
+	SIG_DELETED,
+	N_SIGNALS
+};
+static guint signals[N_SIGNALS] = {0, };
+
+enum {
 	PROP_0,
 	PROP_ID,
 	PROP_TYPE,
@@ -116,6 +126,18 @@
 }
 
 /******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+purple_notification_authorization_request_deleted_cb(G_GNUC_UNUSED PurpleNotification *notification,
+                                                     gpointer data)
+{
+	PurpleAuthorizationRequest *auth_request = data;
+
+	purple_authorization_request_deny(auth_request, NULL);
+}
+
+/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
 static void
@@ -256,7 +278,27 @@
 	obj_class->finalize = purple_notification_finalize;
 
 	/**
-	 * PurpleNotification::id:
+	 * PurpleNotification::deleted:
+	 * @notification: The instance.
+	 *
+	 * Emitted when the notification is deleted. This is typically done by a
+	 * user interface calling [method@PurpleNotification.delete].
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_DELETED] = g_signal_new_class_handler(
+		"deleted",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		0);
+
+	/**
+	 * PurpleNotification:id:
 	 *
 	 * The ID of the notification. Used for things that need to address it.
 	 * This is auto populated at creation time.
@@ -271,7 +313,7 @@
 	);
 
 	/**
-	 * PurpleNotification::type:
+	 * PurpleNotification:type:
 	 *
 	 * The [enum@NotificationType] of this notification.
 	 *
@@ -285,7 +327,7 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
 	/**
-	 * PurpleNotification::account:
+	 * PurpleNotification:account:
 	 *
 	 * An optional [class@Account] that this notification is for.
 	 *
@@ -298,7 +340,7 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
 	/**
-	 * PurpleNotification::created-timestamp:
+	 * PurpleNotification:created-timestamp:
 	 *
 	 * The creation time of this notification. This always represented as UTC
 	 * internally, and will be set to UTC now by default.
@@ -312,7 +354,7 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
 
 	/**
-	 * PurpleNotification::title:
+	 * PurpleNotification:title:
 	 *
 	 * An optional title for this notification. A user interface may or may not
 	 * choose to use this when displaying the notification. Regardless, this
@@ -327,7 +369,7 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
 
 	/**
-	 * PurpleNotification::icon-name:
+	 * PurpleNotification:icon-name:
 	 *
 	 * The icon-name in the icon theme to use for the notification. A user
 	 * interface may or may not choose to use this when display the
@@ -342,7 +384,7 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
 
 	/**
-	 * PurpleNotification::read:
+	 * PurpleNotification:read:
 	 *
 	 * Whether or not the notification has been read.
 	 *
@@ -355,7 +397,7 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
 
 	/**
-	 * PurpleNotification::interactive:
+	 * PurpleNotification:interactive:
 	 *
 	 * Whether or not the notification can be interacted with.
 	 *
@@ -368,7 +410,7 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
 
 	/**
-	 * PurpleNotification::data:
+	 * PurpleNotification:data:
 	 *
 	 * Data specific to the [enum@NotificationType] for the notification.
 	 *
@@ -380,7 +422,7 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
 	/**
-	 * PurpleNotification::data-destroy-func:
+	 * PurpleNotification:data-destroy-func:
 	 *
 	 * A [func@GLib.DestroyFunc] to call to free
 	 * [property@PurpleNotification:data].
@@ -393,6 +435,7 @@
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
 }
 
 /******************************************************************************
@@ -410,6 +453,45 @@
 	                    NULL);
 }
 
+PurpleNotification *
+purple_notification_new_from_authorization_request(PurpleAuthorizationRequest *authorization_request)
+{
+	PurpleAccount *account = NULL;
+	PurpleNotification *notification = NULL;
+	gchar *title = NULL;
+	const gchar *alias = NULL, *username = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_AUTHORIZATION_REQUEST(authorization_request),
+	                     NULL);
+
+	account = purple_authorization_request_get_account(authorization_request);
+	notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_AUTHORIZATION_REQUEST,
+	                                       account, authorization_request,
+	                                       g_object_unref);
+	g_signal_connect_object(notification, "deleted",
+	                        G_CALLBACK(purple_notification_authorization_request_deleted_cb),
+	                        authorization_request, 0);
+
+	username = purple_authorization_request_get_username(authorization_request);
+	alias = purple_authorization_request_get_alias(authorization_request);
+
+	if(alias != NULL && *alias != '\0') {
+		title = g_strdup_printf(_("%s (%s) would like to add %s to their"
+		                          " contact list"),
+		                        alias, username,
+		                        purple_account_get_username(account));
+	} else {
+		title = g_strdup_printf(_("%s would like to add %s to their contact"
+		                          " list"),
+		                        username, purple_account_get_username(account));
+	}
+
+	purple_notification_set_title(notification, title);
+	g_free(title);
+
+	return notification;
+}
+
 const gchar *
 purple_notification_get_id(PurpleNotification *notification) {
 	g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL);
@@ -565,3 +647,15 @@
 	return g_date_time_compare(notification_a->created_timestamp,
 	                           notification_b->created_timestamp);
 }
+
+void
+purple_notification_delete(PurpleNotification *notification) {
+	g_return_if_fail(PURPLE_IS_NOTIFICATION(notification));
+
+	/* Calling this multiple times is a programming error. */
+	g_return_if_fail(notification->deleted == FALSE);
+
+	notification->deleted = TRUE;
+
+	g_signal_emit(notification, signals[SIG_DELETED], 0);
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -1,6 +1,7 @@
 PROGS = [
     'account_option',
     'account_manager',
+    'authorization_request',
     'circular_buffer',
     'credential_manager',
     'credential_provider',
diff --git a/libpurple/tests/test_authorization_request.c b/libpurple/tests/test_authorization_request.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_authorization_request.c
@@ -0,0 +1,329 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_purple_authorization_request_accepted_counter_cb(G_GNUC_UNUSED PurpleAuthorizationRequest *request,
+                                                      gpointer data)
+{
+	gint *counter = data;
+
+	*counter = *counter + 1;
+}
+
+static void
+test_purple_authorization_request_denied_counter_cb(G_GNUC_UNUSED PurpleAuthorizationRequest *request,
+                                                    G_GNUC_UNUSED const gchar *message,
+                                                    gpointer data)
+{
+	gint *counter = data;
+
+	*counter = *counter + 1;
+}
+
+static void
+test_purple_authorization_request_denied_message_cb(G_GNUC_UNUSED PurpleAuthorizationRequest *request,
+                                                    const gchar *message,
+                                                    gpointer data)
+{
+	gchar *expected = data;
+
+	g_assert_cmpstr(message, ==, expected);
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_authorization_request_new(void) {
+	PurpleAccount *account1 = NULL, *account2 = NULL;
+	PurpleAuthorizationRequest *request = NULL;
+	const gchar *username = NULL;
+
+	account1 = purple_account_new("test", "test");
+
+	request = purple_authorization_request_new(account1, "remote-username");
+
+	/* Make sure we got a valid authorization request. */
+	g_assert_true(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	/* Verify the account is set properly. */
+	account2 = purple_authorization_request_get_account(request);
+	g_assert_nonnull(account2);
+	g_assert_true(account1 == account2);
+
+	/* Verify the username set properly. */
+	username = purple_authorization_request_get_username(request);
+	g_assert_cmpstr(username, ==, "remote-username");
+
+	/* Unref it to destroy it. */
+	g_clear_object(&request);
+
+	/* Clean up the account. */
+	g_clear_object(&account1);
+}
+
+static void
+test_purple_authorization_request_properties(void) {
+	PurpleAccount *account = NULL;
+	PurpleAuthorizationRequest *request = NULL;
+
+	account = purple_account_new("test", "test");
+	request = purple_authorization_request_new(account, "username");
+
+	g_assert_true(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	/* Verify the alias property works and is nullable. */
+	purple_authorization_request_set_alias(request, "alias");
+	g_assert_cmpstr(purple_authorization_request_get_alias(request), ==,
+	                "alias");
+	purple_authorization_request_set_alias(request, NULL);
+	g_assert_null(purple_authorization_request_get_alias(request));
+
+	/* Verify the message property works and is nullable. */
+	purple_authorization_request_set_message(request, "message");
+	g_assert_cmpstr(purple_authorization_request_get_message(request), ==,
+	                "message");
+	purple_authorization_request_set_message(request, NULL);
+	g_assert_null(purple_authorization_request_get_message(request));
+
+	/* Cleanup. */
+	g_clear_object(&request);
+	g_clear_object(&account);
+}
+
+static void
+test_purple_authorization_request_accept(void) {
+	if(g_test_subprocess()) {
+		PurpleAccount *account = NULL;
+		PurpleAuthorizationRequest *request = NULL;
+		gint counter = 0;
+
+		account = purple_account_new("test", "test");
+		request = purple_authorization_request_new(account, "username");
+
+		g_assert_true(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+		g_signal_connect(request, "accepted",
+		                 G_CALLBACK(test_purple_authorization_request_accepted_counter_cb),
+		                 &counter);
+
+		/* Accept the request and verify that the callback was called. */
+		purple_authorization_request_accept(request);
+		g_assert_cmpint(counter, ==, 1);
+
+		/* Accept the request again to trigger the critical. */
+		purple_authorization_request_accept(request);
+		g_assert_cmpint(counter, ==, 1);
+
+		/* Cleanup. */
+		g_clear_object(&account);
+		g_clear_object(&request);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-CRITICAL*request->handled*failed*");
+}
+
+static void
+test_purple_authorization_request_accept_deny(void) {
+	if(g_test_subprocess()) {
+		PurpleAccount *account = NULL;
+		PurpleAuthorizationRequest *request = NULL;
+		gint counter = 0;
+
+		account = purple_account_new("test", "test");
+		request = purple_authorization_request_new(account, "username");
+
+		g_assert_true(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+		g_signal_connect(request, "accepted",
+		                 G_CALLBACK(test_purple_authorization_request_accepted_counter_cb),
+		                 &counter);
+		g_signal_connect(request, "denied",
+		                 G_CALLBACK(test_purple_authorization_request_denied_counter_cb),
+		                 &counter);
+
+		/* Accept the request and verify that the callback was called. */
+		purple_authorization_request_accept(request);
+		g_assert_cmpint(counter, ==, 1);
+
+		/* Deny the request to trigger the critical. */
+		purple_authorization_request_deny(request, NULL);
+		g_assert_cmpint(counter, ==, 1);
+
+		/* Cleanup. */
+		g_clear_object(&account);
+		g_clear_object(&request);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-CRITICAL*request->handled*failed*");
+}
+
+static void
+test_purple_authorization_request_deny(void) {
+	if(g_test_subprocess()) {
+		PurpleAccount *account = NULL;
+		PurpleAuthorizationRequest *request = NULL;
+		gint counter = 0;
+
+		account = purple_account_new("test", "test");
+		request = purple_authorization_request_new(account, "username");
+
+		g_assert_true(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+		g_signal_connect(request, "denied",
+		                 G_CALLBACK(test_purple_authorization_request_denied_counter_cb),
+		                 &counter);
+
+		/* Deny the request and verify that the callback was called. */
+		purple_authorization_request_deny(request, NULL);
+		g_assert_cmpint(counter, ==, 1);
+
+		/* Deny the request again to trigger the critical. */
+		purple_authorization_request_deny(request, NULL);
+		g_assert_cmpint(counter, ==, 1);
+
+		/* Cleanup. */
+		g_clear_object(&account);
+		g_clear_object(&request);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-CRITICAL*request->handled*failed*");
+}
+
+static void
+test_purple_authorization_request_deny_accept(void) {
+	if(g_test_subprocess()) {
+		PurpleAccount *account = NULL;
+		PurpleAuthorizationRequest *request = NULL;
+		gint counter = 0;
+
+		account = purple_account_new("test", "test");
+		request = purple_authorization_request_new(account, "username");
+
+		g_assert_true(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+		g_signal_connect(request, "denied",
+		                 G_CALLBACK(test_purple_authorization_request_denied_counter_cb),
+		                 &counter);
+		g_signal_connect(request, "accepted",
+		                 G_CALLBACK(test_purple_authorization_request_accepted_counter_cb),
+		                 &counter);
+
+		/* Deny the request and verify that the callback was called. */
+		purple_authorization_request_deny(request, NULL);
+		g_assert_cmpint(counter, ==, 1);
+
+		/* Deny the request again to trigger the critical. */
+		purple_authorization_request_accept(request);
+		g_assert_cmpint(counter, ==, 1);
+
+		/* Cleanup. */
+		g_clear_object(&account);
+		g_clear_object(&request);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-CRITICAL*request->handled*failed*");
+}
+
+static void
+test_purple_authorization_request_deny_message_null(void) {
+	PurpleAccount *account = NULL;
+	PurpleAuthorizationRequest *request = NULL;
+
+	account = purple_account_new("test", "test");
+	request = purple_authorization_request_new(account, "username");
+
+	g_assert_true(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	g_signal_connect(request, "denied",
+	                 G_CALLBACK(test_purple_authorization_request_denied_message_cb),
+	                 NULL);
+
+	/* Deny the request the signal handler is expecting a message of NULL. */
+	purple_authorization_request_deny(request, NULL);
+
+	/* Cleanup. */
+	g_clear_object(&account);
+	g_clear_object(&request);
+}
+
+static void
+test_purple_authorization_request_deny_message_non_null(void) {
+	PurpleAccount *account = NULL;
+	PurpleAuthorizationRequest *request = NULL;
+
+	account = purple_account_new("test", "test");
+	request = purple_authorization_request_new(account, "username");
+
+	g_assert_true(PURPLE_IS_AUTHORIZATION_REQUEST(request));
+
+	g_signal_connect(request, "denied",
+	                 G_CALLBACK(test_purple_authorization_request_denied_message_cb),
+	                 "this is a message");
+
+	/* Deny the request the signal handler is expecting the above message. */
+	purple_authorization_request_deny(request, "this is a message");
+
+	/* Cleanup. */
+	g_clear_object(&account);
+	g_clear_object(&request);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	test_ui_purple_init();
+
+	g_test_add_func("/request-authorization/new",
+	                test_purple_authorization_request_new);
+	g_test_add_func("/request-authorization/properties",
+	                test_purple_authorization_request_properties);
+
+	g_test_add_func("/request-authorization/accept",
+	                test_purple_authorization_request_accept);
+	g_test_add_func("/request-authorization/accept-deny",
+	                test_purple_authorization_request_accept_deny);
+	g_test_add_func("/request-authorization/deny",
+	                test_purple_authorization_request_deny);
+	g_test_add_func("/request-authorization/deny-accept",
+	                test_purple_authorization_request_deny_accept);
+
+	g_test_add_func("/request-authorization/deny-message/null",
+	                test_purple_authorization_request_deny_message_null);
+	g_test_add_func("/request-authorization/deny-message/non-null",
+	                test_purple_authorization_request_deny_message_non_null);
+
+	return g_test_run();
+}
diff --git a/pidgin/gtkaccount.c b/pidgin/gtkaccount.c
--- a/pidgin/gtkaccount.c
+++ b/pidgin/gtkaccount.c
@@ -1355,214 +1355,6 @@
 	g_free(buffer);
 }
 
-struct auth_request
-{
-	PurpleAccountRequestAuthorizationCb auth_cb;
-	PurpleAccountRequestAuthorizationCb deny_cb;
-	void *data;
-	char *username;
-	char *alias;
-	PurpleAccount *account;
-	gboolean add_buddy_after_auth;
-};
-
-static void
-free_auth_request(struct auth_request *ar)
-{
-	g_free(ar->username);
-	g_free(ar->alias);
-	g_free(ar);
-}
-
-static void
-authorize_and_add_cb(struct auth_request *ar, const char *message)
-{
-	ar->auth_cb(message, ar->data);
-	if (ar->add_buddy_after_auth) {
-		purple_blist_request_add_buddy(ar->account, ar->username, NULL, ar->alias);
-	}
-}
-
-static void
-authorize_noreason_cb(struct auth_request *ar)
-{
-	authorize_and_add_cb(ar, NULL);
-}
-
-static void
-authorize_reason_cb(G_GNUC_UNUSED PidginMiniDialog *mini_dialog,
-                    G_GNUC_UNUSED GtkButton *button, gpointer user_data)
-{
-	struct auth_request *ar = user_data;
-	PurpleProtocol *protocol = purple_account_get_protocol(ar->account);
-
-	if (protocol && (purple_protocol_get_options(protocol) & OPT_PROTO_AUTHORIZATION_GRANTED_MESSAGE)) {
-		/* Duplicate information because ar is freed by closing minidialog */
-		struct auth_request *aa = g_new0(struct auth_request, 1);
-		aa->auth_cb = ar->auth_cb;
-		aa->deny_cb = ar->deny_cb;
-		aa->data = ar->data;
-		aa->account = ar->account;
-		aa->username = g_strdup(ar->username);
-		aa->alias = g_strdup(ar->alias);
-		aa->add_buddy_after_auth = ar->add_buddy_after_auth;
-		purple_request_input(ar->account, NULL, _("Authorization acceptance message:"),
-		                     NULL, _("No reason given."), TRUE, FALSE, NULL,
-		                     _("OK"), G_CALLBACK(authorize_and_add_cb),
-		                     _("Cancel"), G_CALLBACK(authorize_noreason_cb),
-		                     purple_request_cpar_from_account(ar->account),
-		                     aa);
-		/* FIXME: aa is going to leak now. */
-	} else {
-		authorize_noreason_cb(ar);
-	}
-}
-
-static void
-deny_no_add_cb(struct auth_request *ar, const char *message)
-{
-	ar->deny_cb(message, ar->data);
-}
-
-static void
-deny_noreason_cb(struct auth_request *ar)
-{
-	ar->deny_cb(NULL, ar->data);
-}
-
-static void
-deny_reason_cb(G_GNUC_UNUSED PidginMiniDialog *mini_dialog,
-               G_GNUC_UNUSED GtkButton *button, gpointer user_data)
-{
-	struct auth_request *ar = user_data;
-	PurpleProtocol *protocol = purple_account_get_protocol(ar->account);
-
-	if (protocol && (purple_protocol_get_options(protocol) & OPT_PROTO_AUTHORIZATION_DENIED_MESSAGE)) {
-		/* Duplicate information because ar is freed by closing minidialog */
-		struct auth_request *aa = g_new0(struct auth_request, 1);
-		aa->auth_cb = ar->auth_cb;
-		aa->deny_cb = ar->deny_cb;
-		aa->data = ar->data;
-		aa->add_buddy_after_auth = ar->add_buddy_after_auth;
-		purple_request_input(ar->account, NULL, _("Authorization denied message:"),
-		                     NULL, _("No reason given."), TRUE, FALSE, NULL,
-		                     _("OK"), G_CALLBACK(deny_no_add_cb),
-		                     _("Cancel"), G_CALLBACK(deny_noreason_cb),
-		                     purple_request_cpar_from_account(ar->account),
-		                     aa);
-		/* FIXME: aa is going to leak now. */
-	} else {
-		deny_noreason_cb(ar);
-	}
-}
-
-static gboolean
-get_user_info_cb(GtkWidget   *label,
-                 const gchar *uri,
-                 gpointer     data)
-{
-	struct auth_request *ar = data;
-	if (purple_strequal(uri, "viewinfo")) {
-		pidgin_retrieve_user_info(purple_account_get_connection(ar->account), ar->username);
-		return TRUE;
-	}
-	return FALSE;
-}
-
-static void
-send_im_cb(G_GNUC_UNUSED PidginMiniDialog *mini_dialog,
-           G_GNUC_UNUSED GtkButton *button,
-           gpointer data)
-{
-	struct auth_request *ar = data;
-	pidgin_dialogs_im_with_user(ar->account, ar->username);
-}
-
-static void *
-pidgin_accounts_request_authorization(PurpleAccount *account,
-                                      const char *remote_user,
-                                      const char *id,
-                                      const char *alias,
-                                      const char *message,
-                                      gboolean on_list,
-                                      PurpleAccountRequestAuthorizationCb auth_cb,
-                                      PurpleAccountRequestAuthorizationCb deny_cb,
-                                      void *user_data)
-{
-	char *buffer;
-	PurpleConnection *gc;
-	GtkWidget *alert;
-	PidginMiniDialog *dialog;
-	GdkPixbuf *protocol_icon;
-	struct auth_request *aa;
-	const char *our_name;
-	gboolean have_valid_alias;
-	char *escaped_remote_user;
-	char *escaped_alias;
-	char *escaped_our_name;
-	char *escaped_message;
-
-	gc = purple_account_get_connection(account);
-	if (message != NULL && *message != '\0')
-		escaped_message = g_markup_escape_text(message, -1);
-	else
-		escaped_message = g_strdup("");
-
-	our_name = (id != NULL) ? id :
-			(purple_connection_get_display_name(gc) != NULL) ? purple_connection_get_display_name(gc) :
-			purple_account_get_username(account);
-	escaped_our_name = g_markup_escape_text(our_name, -1);
-
-	escaped_remote_user = g_markup_escape_text(remote_user, -1);
-
-	have_valid_alias = alias && *alias;
-	escaped_alias = have_valid_alias ? g_markup_escape_text(alias, -1) : g_strdup("");
-
-	buffer = g_strdup_printf(_("<a href=\"viewinfo\">%s</a>%s%s%s wants to add you (%s) to his or her buddy list%s%s"),
-				escaped_remote_user,
-				(have_valid_alias ? " ("  : ""),
-				escaped_alias,
-				(have_valid_alias ? ")"   : ""),
-				escaped_our_name,
-				(*escaped_message ? ": " : "."),
-				escaped_message);
-
-	g_free(escaped_remote_user);
-	g_free(escaped_alias);
-	g_free(escaped_our_name);
-	g_free(escaped_message);
-
-	protocol_icon = pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL);
-
-	aa = g_new0(struct auth_request, 1);
-	aa->auth_cb = auth_cb;
-	aa->deny_cb = deny_cb;
-	aa->data = user_data;
-	aa->username = g_strdup(remote_user);
-	aa->alias = g_strdup(alias);
-	aa->account = account;
-	aa->add_buddy_after_auth = !on_list;
-
-	dialog = pidgin_mini_dialog_new_with_custom_icon(
-		_("Authorize buddy?"), NULL, protocol_icon);
-	alert = GTK_WIDGET(dialog);
-
-	pidgin_mini_dialog_enable_description_markup(dialog);
-	pidgin_mini_dialog_set_link_callback(dialog, G_CALLBACK(get_user_info_cb), aa);
-	pidgin_mini_dialog_set_description(dialog, buffer);
-	pidgin_mini_dialog_add_button(dialog, _("Authorize"), authorize_reason_cb, aa);
-	pidgin_mini_dialog_add_button(dialog, _("Deny"), deny_reason_cb, aa);
-	pidgin_mini_dialog_add_non_closing_button(dialog, _("Send Instant Message"), send_im_cb, aa);
-
-	g_signal_connect_swapped(G_OBJECT(alert), "destroy", G_CALLBACK(free_auth_request), aa);
-	g_signal_connect(G_OBJECT(alert), "destroy", G_CALLBACK(purple_account_request_close), NULL);
-	pidgin_blist_add_alert(alert);
-
-	g_free(buffer);
-
-	return alert;
-}
-
 static void
 pidgin_accounts_request_close(void *ui_handle)
 {
@@ -1572,7 +1364,6 @@
 static PurpleAccountUiOps ui_ops =
 {
 	.request_add = pidgin_accounts_request_add,
-	.request_authorize = pidgin_accounts_request_authorization,
 	.close_account_request = pidgin_accounts_request_close,
 };
 
diff --git a/pidgin/meson.build b/pidgin/meson.build
--- a/pidgin/meson.build
+++ b/pidgin/meson.build
@@ -44,6 +44,7 @@
 	'pidginkeypad.c',
 	'pidginmessage.c',
 	'pidginmooddialog.c',
+	'pidginnotificationauthorizationrequest.c',
 	'pidginnotificationconnectionerror.c',
 	'pidginnotificationlist.c',
 	'pidginplugininfo.c',
@@ -116,6 +117,7 @@
 	'pidginkeypad.h',
 	'pidginmessage.h',
 	'pidginmooddialog.h',
+	'pidginnotificationauthorizationrequest.h',
 	'pidginnotificationconnectionerror.h',
 	'pidginnotificationlist.h',
 	'pidginplugininfo.h',
diff --git a/pidgin/pidginnotificationauthorizationrequest.h b/pidgin/pidginnotificationauthorizationrequest.h
new file mode 100644
--- /dev/null
+++ b/pidgin/pidginnotificationauthorizationrequest.h
@@ -0,0 +1,80 @@
+/*
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PIDGIN_GLOBAL_HEADER_INSIDE) && !defined(PIDGIN_COMPILATION)
+# error "only <pidgin.h> may be included directly"
+#endif
+
+#ifndef PIDGIN_NOTIFICATION_AUTHORIZATION_REQUEST_H
+#define PIDGIN_NOTIFICATION_AUTHORIZATION_REQUEST_H
+
+#include <glib.h>
+
+#include <gtk/gtk.h>
+
+#include <handy.h>
+
+#include <purple.h>
+
+G_BEGIN_DECLS
+
+/**
+ * PidginNotificationAuthorizationRequest:
+ *
+ * #PidginNotificationAuthorizationRequest is a widget that displays
+ * notifications from [class@Purple.NotificationManager] for authorization
+ * requests.
+ *
+ * Since: 3.0.0
+ */
+
+#define PIDGIN_TYPE_NOTIFICATION_AUTHORIZATION_REQUEST (pidgin_notification_authorization_request_get_type())
+G_DECLARE_FINAL_TYPE(PidginNotificationAuthorizationRequest, pidgin_notification_authorization_request,
+                     PIDGIN, NOTIFICATION_AUTHORIZATION_REQUEST, HdyActionRow)
+
+/**
+ * pidgin_notification_authorization_request_new:
+ * @notification: A [class@Purple.Notification] to display.
+ *
+ * Creates a new #PidginNotificationAuthorizationRequest instance that will
+ * display @notification.
+ *
+ * Returns: (transfer full): The new #PidginNotificationAuthorizationRequest
+ *          instance.
+ */
+GtkWidget *pidgin_notification_authorization_request_new(PurpleNotification *notification);
+
+/**
+ * pidgin_notification_authorization_request_get_notification:
+ * @request: The instance.
+ *
+ * Gets the [class@Purple.Notification] that @request is displaying.
+ *
+ * Returns: (transfer none): The notification.
+ *
+ * Since: 3.0.0
+ */
+PurpleNotification *pidgin_notification_authorization_request_get_notification(PidginNotificationAuthorizationRequest *request);
+
+G_END_DECLS
+
+#endif /* PIDGIN_NOTIFICATION_AUTHORIZATION_REQUEST_H */
diff --git a/pidgin/pidginnotificationauthorizationrequest.c b/pidgin/pidginnotificationauthorizationrequest.c
new file mode 100644
--- /dev/null
+++ b/pidgin/pidginnotificationauthorizationrequest.c
@@ -0,0 +1,339 @@
+/*
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include <purple.h>
+
+#include "pidgin/pidginnotificationauthorizationrequest.h"
+
+#include "pidgin/gtkdialogs.h"
+
+struct _PidginNotificationAuthorizationRequest {
+	HdyActionRow parent;
+
+	PurpleNotification *notification;
+
+	GtkWidget *accept;
+	GtkWidget *deny;
+	GtkWidget *message;
+};
+
+enum {
+	PROP_0,
+	PROP_NOTIFICATION,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = { NULL, };
+
+G_DEFINE_TYPE(PidginNotificationAuthorizationRequest,
+              pidgin_notification_authorization_request, HDY_TYPE_ACTION_ROW)
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+pidgin_notification_authorization_request_update(PidginNotificationAuthorizationRequest *request) {
+	PurpleAccount *account = NULL;
+	PurpleAuthorizationRequest *purple_request = NULL;
+	const gchar *title = NULL;
+	const gchar *icon_name = NULL, *message = NULL;
+
+	g_return_if_fail(PIDGIN_IS_NOTIFICATION_AUTHORIZATION_REQUEST(request));
+
+	if(!PURPLE_IS_NOTIFICATION(request->notification)) {
+		hdy_preferences_row_set_title(HDY_PREFERENCES_ROW(request),
+		                              _("Notification missing"));
+
+		hdy_action_row_set_icon_name(HDY_ACTION_ROW(request), NULL);
+		hdy_action_row_set_subtitle(HDY_ACTION_ROW(request), NULL);
+
+		gtk_widget_hide(request->accept);
+		gtk_widget_hide(request->deny);
+		gtk_widget_hide(request->message);
+
+		return;
+	}
+
+	account = purple_notification_get_account(request->notification);
+	if(!PURPLE_IS_ACCOUNT(account)) {
+		hdy_preferences_row_set_title(HDY_PREFERENCES_ROW(request),
+		                              _("Notification is missing an account"));
+
+		hdy_action_row_set_icon_name(HDY_ACTION_ROW(request), NULL);
+		hdy_action_row_set_subtitle(HDY_ACTION_ROW(request), NULL);
+
+		gtk_widget_hide(request->accept);
+		gtk_widget_hide(request->deny);
+		gtk_widget_hide(request->message);
+
+		return;
+	}
+
+	purple_request = purple_notification_get_data(request->notification);
+
+	/* Set the icon name if one was specified. */
+	icon_name = purple_notification_get_icon_name(request->notification);
+	if(icon_name == NULL) {
+		PurpleProtocol *protocol = NULL;
+
+		protocol = purple_account_get_protocol(account);
+		icon_name = purple_protocol_get_icon_name(protocol);
+
+		if(icon_name == NULL) {
+			icon_name = "dialog-question";
+		}
+	}
+	hdy_action_row_set_icon_name(HDY_ACTION_ROW(request), icon_name);
+
+	title = purple_notification_get_title(request->notification);
+	hdy_preferences_row_set_title(HDY_PREFERENCES_ROW(request), title);
+
+	message = purple_authorization_request_get_message(purple_request);
+	hdy_action_row_set_subtitle(HDY_ACTION_ROW(request), message);
+
+	gtk_widget_show(request->accept);
+	gtk_widget_show(request->deny);
+	gtk_widget_show(request->message);
+}
+
+static void
+pidgin_notification_authorization_request_set_notification(PidginNotificationAuthorizationRequest *request,
+                                                           PurpleNotification *notification)
+{
+	if(g_set_object(&request->notification, notification)) {
+		pidgin_notification_authorization_request_update(request);
+
+		g_object_notify_by_pspec(G_OBJECT(request), properties[PROP_NOTIFICATION]);
+	}
+}
+
+static void
+pidgin_notification_authorization_request_close(PidginNotificationAuthorizationRequest *request)
+{
+	PurpleNotificationManager *manager = NULL;
+
+	purple_notification_delete(request->notification);
+
+	manager = purple_notification_manager_get_default();
+	purple_notification_manager_remove(manager, request->notification);
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+pidgin_notification_authorization_request_accept_cb(G_GNUC_UNUSED GtkButton *button,
+                                                    gpointer data)
+{
+	PidginNotificationAuthorizationRequest *pidgin_request = data;
+	PurpleAuthorizationRequest *request = NULL;
+
+	request = purple_notification_get_data(pidgin_request->notification);
+
+	purple_authorization_request_accept(request);
+
+	if(purple_authorization_request_get_add(request)) {
+		PurpleAccount *account = NULL;
+		const gchar *username = NULL, *alias = NULL;
+
+		account = purple_authorization_request_get_account(request);
+		username = purple_authorization_request_get_username(request);
+		alias = purple_authorization_request_get_alias(request);
+
+		purple_blist_request_add_buddy(account, username, NULL, alias);
+	}
+
+	pidgin_notification_authorization_request_close(pidgin_request);
+}
+
+static void
+pidgin_notification_authorization_request_deny_cb(G_GNUC_UNUSED GtkButton *button,
+                                                  gpointer data)
+{
+	PidginNotificationAuthorizationRequest *pidgin_request = data;
+	PurpleAuthorizationRequest *request = NULL;
+
+	request = purple_notification_get_data(pidgin_request->notification);
+
+	purple_authorization_request_deny(request, NULL);
+
+	pidgin_notification_authorization_request_close(pidgin_request);
+}
+
+static void
+pidgin_notification_authorization_request_message_cb(G_GNUC_UNUSED GtkButton *button,
+                                                     gpointer data)
+{
+	PidginNotificationAuthorizationRequest *pidgin_request = data;
+	PurpleAuthorizationRequest *request = NULL;
+	PurpleAccount *account = NULL;
+	const gchar *username = NULL;
+
+	request = purple_notification_get_data(pidgin_request->notification);
+
+	account = purple_authorization_request_get_account(request);
+	username = purple_authorization_request_get_username(request);
+
+	pidgin_dialogs_im_with_user(account, username);
+}
+
+static void
+pidgin_notification_authorization_request_remove_cb(G_GNUC_UNUSED GtkButton *button,
+                                                    gpointer data)
+{
+	PidginNotificationAuthorizationRequest *request = data;
+
+	pidgin_notification_authorization_request_close(request);
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+pidgin_notification_authorization_request_get_property(GObject *obj,
+                                                       guint param_id,
+                                                       GValue *value,
+                                                       GParamSpec *pspec)
+{
+	PidginNotificationAuthorizationRequest *request = NULL;
+
+	request = PIDGIN_NOTIFICATION_AUTHORIZATION_REQUEST(obj);
+
+	switch(param_id) {
+		case PROP_NOTIFICATION:
+			g_value_set_object(value,
+			                   pidgin_notification_authorization_request_get_notification(request));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+pidgin_notification_authorization_request_set_property(GObject *obj,
+                                                       guint param_id,
+                                                       const GValue *value,
+                                                       GParamSpec *pspec)
+{
+	PidginNotificationAuthorizationRequest *request = NULL;
+
+	request = PIDGIN_NOTIFICATION_AUTHORIZATION_REQUEST(obj);
+
+	switch(param_id) {
+		case PROP_NOTIFICATION:
+			pidgin_notification_authorization_request_set_notification(request,
+			                                                           g_value_get_object(value));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+pidgin_notification_authorization_request_dispose(GObject *obj) {
+	PidginNotificationAuthorizationRequest *request = NULL;
+
+	request = PIDGIN_NOTIFICATION_AUTHORIZATION_REQUEST(obj);
+
+	g_clear_object(&request->notification);
+
+	G_OBJECT_CLASS(pidgin_notification_authorization_request_parent_class)->dispose(obj);
+}
+
+static void
+pidgin_notification_authorization_request_init(PidginNotificationAuthorizationRequest *list)
+{
+	gtk_widget_init_template(GTK_WIDGET(list));
+}
+
+static void
+pidgin_notification_authorization_request_class_init(PidginNotificationAuthorizationRequestClass *klass)
+{
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+	obj_class->get_property = pidgin_notification_authorization_request_get_property;
+	obj_class->set_property = pidgin_notification_authorization_request_set_property;
+	obj_class->dispose = pidgin_notification_authorization_request_dispose;
+
+	/**
+	 * PidginNotificationAuthorizationRequest:info:
+	 *
+	 * The [type@Purple.AuthorizationRequestInfo] that this notification is for.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_NOTIFICATION] = g_param_spec_object(
+		"notification", "notification",
+		"The notification to display",
+		PURPLE_TYPE_NOTIFICATION,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
+	gtk_widget_class_set_template_from_resource(
+	    widget_class,
+	    "/im/pidgin/Pidgin3/Notifications/authorizationrequest.ui"
+	);
+
+	gtk_widget_class_bind_template_child(widget_class,
+	                                     PidginNotificationAuthorizationRequest,
+	                                     accept);
+	gtk_widget_class_bind_template_child(widget_class,
+	                                     PidginNotificationAuthorizationRequest,
+	                                     deny);
+	gtk_widget_class_bind_template_child(widget_class,
+	                                     PidginNotificationAuthorizationRequest,
+	                                     message);
+
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_notification_authorization_request_accept_cb);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_notification_authorization_request_deny_cb);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_notification_authorization_request_message_cb);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_notification_authorization_request_remove_cb);
+}
+
+/******************************************************************************
+ * API
+ *****************************************************************************/
+GtkWidget *
+pidgin_notification_authorization_request_new(PurpleNotification *notification) {
+	return g_object_new(
+		PIDGIN_TYPE_NOTIFICATION_AUTHORIZATION_REQUEST,
+		"notification", notification,
+		NULL);
+}
+
+PurpleNotification *
+pidgin_notification_authorization_request_get_notification(PidginNotificationAuthorizationRequest *request)
+{
+	g_return_val_if_fail(PIDGIN_IS_NOTIFICATION_AUTHORIZATION_REQUEST(request),
+	                     NULL);
+
+	return request->notification;
+}
diff --git a/pidgin/pidginnotificationconnectionerror.c b/pidgin/pidginnotificationconnectionerror.c
--- a/pidgin/pidginnotificationconnectionerror.c
+++ b/pidgin/pidginnotificationconnectionerror.c
@@ -142,6 +142,8 @@
 	PidginNotificationConnectionError *error = data;
 	PurpleNotificationManager *manager = NULL;
 
+	purple_notification_delete(error->notification);
+
 	manager = purple_notification_manager_get_default();
 	purple_notification_manager_remove(manager, error->notification);
 }
diff --git a/pidgin/pidginnotificationlist.c b/pidgin/pidginnotificationlist.c
--- a/pidgin/pidginnotificationlist.c
+++ b/pidgin/pidginnotificationlist.c
@@ -27,6 +27,7 @@
 #include "pidgin/pidginnotificationlist.h"
 
 #include "pidgin/pidginnotificationconnectionerror.h"
+#include "pidgin/pidginnotificationauthorizationrequest.h"
 
 struct _PidginNotificationList {
 	GtkBox parent;
@@ -71,6 +72,9 @@
 		case PURPLE_NOTIFICATION_TYPE_CONNECTION_ERROR:
 			widget = pidgin_notification_connection_error_new(notification);
 			break;
+		case PURPLE_NOTIFICATION_TYPE_AUTHORIZATION_REQUEST:
+			widget = pidgin_notification_authorization_request_new(notification);
+			break;
 		default:
 			widget = pidgin_notification_list_unknown_notification(notification);
 			break;
diff --git a/pidgin/resources/Notifications/authorizationrequest.ui b/pidgin/resources/Notifications/authorizationrequest.ui
new file mode 100644
--- /dev/null
+++ b/pidgin/resources/Notifications/authorizationrequest.ui
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 
+
+Pidgin - Internet Messenger
+Copyright (C) Pidgin Developers <devel@pidgin.im>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses/>.
+
+-->
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <requires lib="libhandy" version="0.0"/>
+  <!-- interface-license-type gplv2 -->
+  <!-- interface-name Pidgin -->
+  <!-- interface-description Internet Messenger -->
+  <!-- interface-copyright Pidgin Developers <devel@pidgin.im> -->
+  <template class="PidginNotificationAuthorizationRequest" parent="HdyActionRow">
+    <property name="visible">True</property>
+    <property name="can-focus">True</property>
+    <property name="activatable">False</property>
+    <property name="title-lines">3</property>
+    <property name="subtitle-lines">3</property>
+    <child>
+      <object class="GtkButton" id="accept">
+        <property name="label" translatable="yes">Accept</property>
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="receives-default">True</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <signal name="clicked" handler="pidgin_notification_authorization_request_accept_cb" object="PidginNotificationAuthorizationRequest" swapped="no"/>
+        <style>
+          <class name="suggested-action"/>
+        </style>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="deny">
+        <property name="label" translatable="yes">Deny</property>
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="receives-default">True</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <signal name="clicked" handler="pidgin_notification_authorization_request_deny_cb" object="PidginNotificationAuthorizationRequest" swapped="no"/>
+        <style>
+          <class name="destructive-action"/>
+        </style>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="message">
+        <property name="label" translatable="yes">Message</property>
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="receives-default">True</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <signal name="clicked" handler="pidgin_notification_authorization_request_message_cb" object="PidginNotificationAuthorizationRequest" swapped="no"/>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="remove">
+        <property name="visible">True</property>
+        <property name="can-focus">True</property>
+        <property name="receives-default">True</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="relief">none</property>
+        <signal name="clicked" handler="pidgin_notification_authorization_request_remove_cb" object="PidginNotificationAuthorizationRequest" swapped="no"/>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="can-focus">False</property>
+            <property name="icon-name">edit-delete-symbolic</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/pidgin/resources/pidgin.gresource.xml b/pidgin/resources/pidgin.gresource.xml
--- a/pidgin/resources/pidgin.gresource.xml
+++ b/pidgin/resources/pidgin.gresource.xml
@@ -24,6 +24,7 @@
     <file compressed="true">Keypad/keypad.ui</file>
     <file compressed="true">Log/log-viewer.ui</file>
     <file compressed="true">Media/window.ui</file>
+    <file compressed="true">Notifications/authorizationrequest.ui</file>
     <file compressed="true">Notifications/connectionerror.ui</file>
     <file compressed="true">Notifications/list.ui</file>
     <file compressed="true">Plugins/dialog.ui</file>
