# HG changeset patch # User maiku@pidgin.im # Date 1250901385 0 # Node ID 33e8d40ae6ec9105c8a4217b100b8a47c5ce58ed # Parent 3a6b755da6692731caaf9e5558b1368db1f9cfc0 Add the vvconfig plugin. It allows you to choose different microphones, webcams, and output elements from the defaults. diff -r 3a6b755da669 -r 33e8d40ae6ec pidgin/plugins/Makefile.am --- a/pidgin/plugins/Makefile.am Fri Aug 21 20:49:06 2009 +0000 +++ b/pidgin/plugins/Makefile.am Sat Aug 22 00:36:25 2009 +0000 @@ -47,6 +47,7 @@ themeedit_la_LDFLAGS = -module -avoid-version timestamp_la_LDFLAGS = -module -avoid-version timestamp_format_la_LDFLAGS = -module -avoid-version +vvconfig_la_LDFLAGS = -module -avoid-version xmppconsole_la_LDFLAGS = -module -avoid-version if PLUGINS @@ -66,6 +67,7 @@ themeedit.la \ timestamp.la \ timestamp_format.la \ + vvconfig.la \ xmppconsole.la noinst_LTLIBRARIES = \ @@ -88,6 +90,7 @@ themeedit_la_SOURCES = themeedit.c themeedit-icon.c themeedit-icon.h timestamp_la_SOURCES = timestamp.c timestamp_format_la_SOURCES = timestamp_format.c +vvconfig_SOURCES = vvconfig.c xmppconsole_la_SOURCES = xmppconsole.c convcolors_la_LIBADD = $(GTK_LIBS) @@ -106,6 +109,7 @@ themeedit_la_LIBADD = $(GTK_LIBS) timestamp_la_LIBADD = $(GTK_LIBS) timestamp_format_la_LIBADD = $(GTK_LIBS) +vvconfig_la_LIBADD = $(GTK_LIBS) xmppconsole_la_LIBADD = $(GTK_LIBS) endif # PLUGINS @@ -129,6 +133,7 @@ -I$(top_srcdir)/pidgin \ $(DEBUG_CFLAGS) \ $(GTK_CFLAGS) \ + $(GSTREAMER_CFLAGS) \ $(PLUGIN_CFLAGS) # diff -r 3a6b755da669 -r 33e8d40ae6ec pidgin/plugins/Makefile.mingw --- a/pidgin/plugins/Makefile.mingw Fri Aug 21 20:49:06 2009 +0000 +++ b/pidgin/plugins/Makefile.mingw Sat Aug 22 00:36:25 2009 +0000 @@ -91,6 +91,7 @@ spellchk.dll \ timestamp_format.dll \ timestamp.dll \ + vvconfig.dll \ xmppconsole.dll ## diff -r 3a6b755da669 -r 33e8d40ae6ec pidgin/plugins/vvconfig.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pidgin/plugins/vvconfig.c Sat Aug 22 00:36:25 2009 +0000 @@ -0,0 +1,566 @@ +/* + * Configures microphones and webcams for voice and video + * Copyright (C) 2009 Mike Ruprecht + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA. + */ +#include "internal.h" + +#include "debug.h" +#include "mediamanager.h" +#include "media-gst.h" +#include "version.h" +#include "gtkplugin.h" +#include "gtkutils.h" +#include "gtkprefs.h" + +#include + +static PurpleMediaElementInfo *old_video_src = NULL, *old_video_sink = NULL, + *old_audio_src = NULL, *old_audio_sink = NULL; + +static const gchar *AUDIO_SRC_PLUGINS[] = { + "alsasrc", "ALSA", + /* "esdmon", "ESD", ? */ + "osssrc", "OSS", + "pulsesrc", "PulseAudio", + /* "audiotestsrc wave=silence", "Silence", */ + "audiotestsrc", "Test Sound", + NULL +}; + +static const gchar *AUDIO_SINK_PLUGINS[] = { + "alsasink", "ALSA", + "artsdsink", "aRts", + "esdsink", "ESD", + "osssink", "OSS", + "pulsesink", "PulseAudio", + NULL +}; + +static const gchar *VIDEO_SRC_PLUGINS[] = { + "videotestsrc", "Test Input", + "dshowvideosrc","DirectDraw", + "ksvideosrc", "KS Video", + "qcamsrc", "Quickcam", + "v4lsrc", "Video4Linux", + "v4l2src", "Video4Linux2", + "v4lmjpegsrc", "Video4Linux MJPEG", + NULL +}; + +static const gchar *VIDEO_SINK_PLUGINS[] = { + /* "aasink", "AALib", Didn't work for me */ + "directdrawsink","DirectDraw", + "glimagesink", "OpenGL", + "ximagesink", "X Window System", + "xvimagesink", "X Window System (Xv)", + NULL +}; + +static GList * +get_element_devices(const gchar *element_name) +{ + GList *ret = NULL; + GstElement *element; + GObjectClass *klass; + GstPropertyProbe *probe; + const GParamSpec *pspec; + + if (!strcmp(element_name, "")) { + ret = g_list_prepend(ret, NULL); + ret = g_list_prepend(ret, (gpointer)_("Default")); + ret = g_list_prepend(ret, ""); + return ret; + } + + ret = g_list_prepend(ret, (gpointer)_("Default")); + ret = g_list_prepend(ret, ""); + + if (*element_name == '\0') { + ret = g_list_prepend(ret, NULL); + ret = g_list_reverse(ret); + return ret; + } + + element = gst_element_factory_make(element_name, "test"); + klass = G_OBJECT_GET_CLASS (element); + if (!g_object_class_find_property(klass, "device") || + !GST_IS_PROPERTY_PROBE(element) || + !(probe = GST_PROPERTY_PROBE(element)) || + !(pspec = gst_property_probe_get_property(probe, "device"))) { + purple_debug_info("vvconfig", "'%s' - no device\n", element_name); + } else { + gint n; + GValueArray *array; + + /* Set autoprobe[-fps] to FALSE to avoid delays when probing. */ + if (g_object_class_find_property (klass, "autoprobe")) { + g_object_set (G_OBJECT (element), "autoprobe", FALSE, NULL); + if (g_object_class_find_property (klass, "autoprobe-fps")) { + g_object_set (G_OBJECT (element), "autoprobe-fps", FALSE, NULL); + } + } + + array = gst_property_probe_probe_and_get_values (probe, pspec); + if (array == NULL) { + purple_debug_info("vvconfig", "'%s' has no devices\n", element_name); + ret = g_list_prepend(ret, NULL); + ret = g_list_reverse(ret); + return ret; + } + + for (n=0; n < array->n_values; ++n) { + GValue *device; + const gchar *name; + const gchar *device_name; + + device = g_value_array_get_nth(array, n); + g_object_set_property(G_OBJECT(element), "device", device); + if (gst_element_set_state(element, GST_STATE_READY) + != GST_STATE_CHANGE_SUCCESS) { + purple_debug_warning("vvconfig", + "Error changing state of %s\n", + element_name); + continue; + } + + g_object_get(G_OBJECT(element), "device-name", &name, NULL); + device_name = g_value_get_string(device); + if (name == NULL) + name = _("Unknown"); + purple_debug_info("vvconfig", "Found device %s : %s for %s\n", + device_name, name, element_name); + ret = g_list_prepend(ret, (gpointer)name); + ret = g_list_prepend(ret, (gpointer)device_name); + gst_element_set_state(element, GST_STATE_NULL); + } + } + gst_object_unref(element); + + ret = g_list_prepend(ret, NULL); + ret = g_list_reverse(ret); + + return ret; +} + +static GList * +get_element_plugins(const gchar **plugins) +{ + GList *ret = NULL; + + ret = g_list_prepend(ret, "Default"); + ret = g_list_prepend(ret, ""); + for (; plugins[0] && plugins[1]; plugins += 2) { + if (gst_default_registry_check_feature_version( + plugins[0], 0, 0, 0)) { + ret = g_list_prepend(ret, (gpointer)plugins[1]); + ret = g_list_prepend(ret, (gpointer)plugins[0]); + } + } + ret = g_list_prepend(ret, NULL); + ret = g_list_reverse(ret); + return ret; +} + +static void +device_changed_cb(const gchar *name, PurplePrefType type, + gconstpointer value, gpointer data) +{ + GtkSizeGroup *sg = data; + GtkWidget *parent, *widget; + GSList *widgets; + GList *devices; + GValue gvalue; + gint position; + gchar *label, *pref; + + widgets = gtk_size_group_get_widgets(GTK_SIZE_GROUP(sg)); + for (; widgets; widgets = g_slist_next(widgets)) { + const gchar *widget_name = + gtk_widget_get_name(GTK_WIDGET(widgets->data)); + if (!strcmp(widget_name, name)) { + gchar *temp_str; + gchar delimiters[3] = {0, 0, 0}; + const gchar *text; + gint keyval, pos; + + widget = widgets->data; + /* Get label with _ from the GtkLabel */ + text = gtk_label_get_text(GTK_LABEL(widget)); + keyval = gtk_label_get_mnemonic_keyval(GTK_LABEL(widget)); + delimiters[0] = g_ascii_tolower(keyval); + delimiters[1] = g_ascii_toupper(keyval); + pos = strcspn(text, delimiters); + if (pos != -1) { + temp_str = g_strndup(text, pos); + label = g_strconcat(temp_str, "_", + text + pos, NULL); + g_free(temp_str); + } else { + label = g_strdup(text); + } + break; + } + } + + if (widgets == NULL) + return; + + parent = gtk_widget_get_parent(widget); + widget = parent; + parent = gtk_widget_get_parent(GTK_WIDGET(widget)); + gvalue.g_type = 0; + g_value_init(&gvalue, G_TYPE_INT); + gtk_container_child_get_property(GTK_CONTAINER(parent), + GTK_WIDGET(widget), "position", &gvalue); + position = g_value_get_int(&gvalue); + g_value_unset(&gvalue); + gtk_widget_destroy(widget); + + pref = g_strdup(name); + strcpy(pref + strlen(pref) - strlen("plugin"), "device"); + devices = get_element_devices(value); + if (g_list_find(devices, purple_prefs_get_string(pref)) == NULL) + purple_prefs_set_string(pref, g_list_next(devices)->data); + widget = pidgin_prefs_dropdown_from_list(parent, + label, PURPLE_PREF_STRING, + pref, devices); + g_list_free(devices); + g_signal_connect_swapped(widget, "destroy", + G_CALLBACK(g_free), pref); + g_free(label); + gtk_misc_set_alignment(GTK_MISC(widget), 0, 0.5); + gtk_widget_set_name(widget, name); + gtk_size_group_add_widget(sg, widget); + gtk_box_reorder_child(GTK_BOX(parent), + gtk_widget_get_parent(GTK_WIDGET(widget)), position); +} + +static void +get_plugin_frame(GtkWidget *parent, GtkSizeGroup *sg, + const gchar *name, const gchar *plugin_label, + const gchar **plugin_strs, const gchar *plugin_pref, + const gchar *device_label, const gchar *device_pref) +{ + GtkWidget *vbox, *widget; + GList *plugins, *devices; + + vbox = pidgin_make_frame(parent, name); + + /* Setup plugin preference */ + plugins = get_element_plugins(plugin_strs); + widget = pidgin_prefs_dropdown_from_list(vbox, plugin_label, + PURPLE_PREF_STRING, plugin_pref, plugins); + g_list_free(plugins); + gtk_size_group_add_widget(sg, widget); + gtk_misc_set_alignment(GTK_MISC(widget), 0, 0.5); + + /* Setup device preference */ + devices = get_element_devices(purple_prefs_get_string(plugin_pref)); + if (g_list_find(devices, purple_prefs_get_string(device_pref)) == NULL) + purple_prefs_set_string(device_pref, g_list_next(devices)->data); + widget = pidgin_prefs_dropdown_from_list(vbox, device_label, + PURPLE_PREF_STRING, device_pref, devices); + g_list_free(devices); + gtk_widget_set_name(widget, plugin_pref); + gtk_size_group_add_widget(sg, widget); + gtk_misc_set_alignment(GTK_MISC(widget), 0, 0.5); + + purple_prefs_connect_callback(vbox, plugin_pref, + device_changed_cb, sg); + g_signal_connect_swapped(vbox, "destroy", + G_CALLBACK(purple_prefs_disconnect_by_handle), vbox); +} + +static GtkWidget * +get_plugin_config_frame(PurplePlugin *plugin) { + GtkWidget *notebook, *vbox_audio, *vbox_video; + GtkSizeGroup *sg; + + notebook = gtk_notebook_new(); + gtk_container_set_border_width(GTK_CONTAINER(notebook), + PIDGIN_HIG_BORDER); + gtk_widget_show(notebook); + + vbox_audio = gtk_vbox_new(FALSE, PIDGIN_HIG_CAT_SPACE); + vbox_video = gtk_vbox_new(FALSE, PIDGIN_HIG_CAT_SPACE); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), + vbox_audio, gtk_label_new(_("Audio"))); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), + vbox_video, gtk_label_new(_("Video"))); + gtk_container_set_border_width(GTK_CONTAINER (vbox_audio), + PIDGIN_HIG_BORDER); + gtk_container_set_border_width(GTK_CONTAINER (vbox_video), + PIDGIN_HIG_BORDER); + + gtk_widget_show(vbox_audio); + gtk_widget_show(vbox_video); + + sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + get_plugin_frame(vbox_audio, sg, _("Output"), _("_Plugin"), AUDIO_SINK_PLUGINS, + "/plugins/core/vvconfig/audio/sink/plugin", _("_Device"), + "/plugins/core/vvconfig/audio/sink/device"); + get_plugin_frame(vbox_audio, sg, _("Input"), _("P_lugin"), AUDIO_SRC_PLUGINS, + "/plugins/core/vvconfig/audio/src/plugin", _("D_evice"), + "/plugins/core/vvconfig/audio/src/device"); + + get_plugin_frame(vbox_video, sg, _("Output"), _("_Plugin"), VIDEO_SINK_PLUGINS, + "/plugins/gtk/vvconfig/video/sink/plugin", _("_Device"), + "/plugins/gtk/vvconfig/video/sink/device"); + get_plugin_frame(vbox_video, sg, _("Input"), _("P_lugin"), VIDEO_SRC_PLUGINS, + "/plugins/core/vvconfig/video/src/plugin", _("D_evice"), + "/plugins/core/vvconfig/video/src/device"); + + return notebook; +} + +static GstElement * +create_video_src(PurpleMedia *media, + const gchar *session_id, const gchar *participant) +{ + const gchar *plugin = purple_prefs_get_string( + "/plugins/core/vvconfig/video/src/plugin"); + const gchar *device = purple_prefs_get_string( + "/plugins/core/vvconfig/video/src/device"); + GstElement *ret; + + if (plugin[0] == '\0') + return purple_media_element_info_call_create(old_video_src, + media, session_id, participant); + + ret = gst_element_factory_make(plugin, "vvconfig-videosrc"); + if (device[0] != '\0') + g_object_set(G_OBJECT(ret), "device", device, NULL); + if (!strcmp(plugin, "videotestsrc")) + g_object_set(G_OBJECT(ret), "is-live", 1, NULL); + return ret; +} + +static GstElement * +create_video_sink(PurpleMedia *media, + const gchar *session_id, const gchar *participant) +{ + const gchar *plugin = purple_prefs_get_string( + "/plugins/gtk/vvconfig/video/sink/plugin"); + const gchar *device = purple_prefs_get_string( + "/plugins/gtk/vvconfig/video/sink/device"); + GstElement *ret; + + if (plugin[0] == '\0') + return purple_media_element_info_call_create(old_video_sink, + media, session_id, participant); + + ret = gst_element_factory_make(plugin, NULL); + if (device[0] != '\0') + g_object_set(G_OBJECT(ret), "device", device, NULL); + return ret; +} + +static GstElement * +create_audio_src(PurpleMedia *media, + const gchar *session_id, const gchar *participant) +{ + const gchar *plugin = purple_prefs_get_string( + "/plugins/core/vvconfig/audio/src/plugin"); + const gchar *device = purple_prefs_get_string( + "/plugins/core/vvconfig/audio/src/device"); + GstElement *ret; + + if (plugin[0] == '\0') + return purple_media_element_info_call_create(old_audio_src, + media, session_id, participant); + + ret = gst_element_factory_make(plugin, NULL); + if (device[0] != '\0') + g_object_set(G_OBJECT(ret), "device", device, NULL); + return ret; +} + +static GstElement * +create_audio_sink(PurpleMedia *media, + const gchar *session_id, const gchar *participant) +{ + const gchar *plugin = purple_prefs_get_string( + "/plugins/core/vvconfig/audio/sink/plugin"); + const gchar *device = purple_prefs_get_string( + "/plugins/core/vvconfig/audio/sink/device"); + GstElement *ret; + + if (plugin[0] == '\0') + return purple_media_element_info_call_create(old_audio_sink, + media, session_id, participant); + + ret = gst_element_factory_make(plugin, NULL); + if (device[0] != '\0') + g_object_set(G_OBJECT(ret), "device", device, NULL); + return ret; +} + +static void +set_element_info_cond(PurpleMediaElementInfo *old_info, + PurpleMediaElementInfo *new_info, const gchar *id) +{ + gchar *element_id = purple_media_element_info_get_id(old_info); + if (!strcmp(element_id, id)) + purple_media_manager_set_active_element( + purple_media_manager_get(), new_info); + g_free(element_id); +} + +static gboolean +plugin_load(PurplePlugin *plugin) +{ + PurpleMediaManager *manager; + PurpleMediaElementInfo *video_src, *video_sink, + *audio_src, *audio_sink; + + /* Disable the plugin if the UI doesn't support VV */ + if (purple_media_manager_get_ui_caps(purple_media_manager_get()) == + PURPLE_MEDIA_CAPS_NONE) + return FALSE; + + purple_prefs_add_none("/plugins/core/vvconfig"); + purple_prefs_add_none("/plugins/core/vvconfig/audio"); + purple_prefs_add_none("/plugins/core/vvconfig/audio/src"); + purple_prefs_add_string("/plugins/core/vvconfig/audio/src/plugin", ""); + purple_prefs_add_string("/plugins/core/vvconfig/audio/src/device", ""); + purple_prefs_add_none("/plugins/core/vvconfig/audio/sink"); + purple_prefs_add_string("/plugins/core/vvconfig/audio/sink/plugin", ""); + purple_prefs_add_string("/plugins/core/vvconfig/audio/sink/device", ""); + purple_prefs_add_none("/plugins/core/vvconfig/video"); + purple_prefs_add_none("/plugins/core/vvconfig/video/src"); + purple_prefs_add_string("/plugins/core/vvconfig/video/src/plugin", ""); + purple_prefs_add_string("/plugins/core/vvconfig/video/src/device", ""); + purple_prefs_add_none("/plugins/gtk/vvconfig"); + purple_prefs_add_none("/plugins/gtk/vvconfig/video"); + purple_prefs_add_none("/plugins/gtk/vvconfig/video/sink"); + purple_prefs_add_string("/plugins/gtk/vvconfig/video/sink/plugin", ""); + purple_prefs_add_string("/plugins/gtk/vvconfig/video/sink/device", ""); + + video_src = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, + "id", "vvconfig-videosrc", + "name", "VV Conf Plugin Video Source", + "type", PURPLE_MEDIA_ELEMENT_VIDEO + | PURPLE_MEDIA_ELEMENT_SRC + | PURPLE_MEDIA_ELEMENT_ONE_SRC + | PURPLE_MEDIA_ELEMENT_UNIQUE, + "create-cb", create_video_src, NULL); + video_sink = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, + "id", "vvconfig-videosink", + "name", "VV Conf Plugin Video Sink", + "type", PURPLE_MEDIA_ELEMENT_VIDEO + | PURPLE_MEDIA_ELEMENT_SINK + | PURPLE_MEDIA_ELEMENT_ONE_SINK, + "create-cb", create_video_sink, NULL); + audio_src = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, + "id", "vvconfig-audiosrc", + "name", "VV Conf Plugin Audio Source", + "type", PURPLE_MEDIA_ELEMENT_AUDIO + | PURPLE_MEDIA_ELEMENT_SRC + | PURPLE_MEDIA_ELEMENT_ONE_SRC + | PURPLE_MEDIA_ELEMENT_UNIQUE, + "create-cb", create_audio_src, NULL); + audio_sink = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, + "id", "vvconfig-audiosink", + "name", "VV Conf Plugin Audio Sink", + "type", PURPLE_MEDIA_ELEMENT_AUDIO + | PURPLE_MEDIA_ELEMENT_SINK + | PURPLE_MEDIA_ELEMENT_ONE_SINK, + "create-cb", create_audio_sink, NULL); + + purple_debug_info("gtkmedia", "Registering media element types\n"); + manager = purple_media_manager_get(); + + old_video_src = purple_media_manager_get_active_element(manager, + PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC); + old_video_sink = purple_media_manager_get_active_element(manager, + PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SINK); + old_audio_src = purple_media_manager_get_active_element(manager, + PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SRC); + old_audio_sink = purple_media_manager_get_active_element(manager, + PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SINK); + + set_element_info_cond(old_video_src, video_src, "pidgindefaultvideosrc"); + set_element_info_cond(old_video_sink, video_sink, "pidgindefaultvideosink"); + set_element_info_cond(old_audio_src, audio_src, "pidgindefaultaudiosrc"); + set_element_info_cond(old_audio_sink, audio_sink, "pidgindefaultaudiosink"); + + return TRUE; +} + +static gboolean +plugin_unload(PurplePlugin *plugin) +{ + PurpleMediaManager *manager = purple_media_manager_get(); + purple_media_manager_set_active_element(manager, old_video_src); + purple_media_manager_set_active_element(manager, old_video_sink); + purple_media_manager_set_active_element(manager, old_audio_src); + purple_media_manager_set_active_element(manager, old_audio_sink); + return TRUE; +} + +static PidginPluginUiInfo ui_info = { + get_plugin_config_frame, + 0, /* page_num (Reserved) */ + /* Padding */ + NULL, + NULL, + NULL, + NULL +}; + +static PurplePluginInfo info = +{ + PURPLE_PLUGIN_MAGIC, /**< magic */ + PURPLE_MAJOR_VERSION, /**< major version */ + PURPLE_MINOR_VERSION, /**< minor version */ + PURPLE_PLUGIN_STANDARD, /**< type */ + PIDGIN_PLUGIN_TYPE, /**< ui_requirement */ + 0, /**< flags */ + NULL, /**< dependencies */ + PURPLE_PRIORITY_DEFAULT, /**< priority */ + + "gtk-maiku-vvconfig", /**< id */ + N_("Voice/Video Settings"), /**< name */ + DISPLAY_VERSION, /**< version */ + N_("Configure your microphone and webcam."), /**< summary */ + N_("Configure microphone and webcam " + "settings for voice/video calls."), /**< description */ + "Mike Ruprecht ", /**< author */ + PURPLE_WEBSITE, /**< homepage */ + + plugin_load, /**< load */ + plugin_unload, /**< unload */ + NULL, /**< destroy */ + + &ui_info, /**< ui_info */ + NULL, /**< extra_info */ + NULL, /**< prefs_info */ + NULL, /**< actions */ + + /* padding */ + NULL, + NULL, + NULL, + NULL +}; + +static void +init_plugin(PurplePlugin *plugin) { +} + +PURPLE_INIT_PLUGIN(vvconfig, init_plugin, info)