summaryrefslogtreecommitdiff
path: root/plugins/!NotAdopted/VypressChat/userlist.c
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/!NotAdopted/VypressChat/userlist.c')
-rw-r--r--plugins/!NotAdopted/VypressChat/userlist.c718
1 files changed, 718 insertions, 0 deletions
diff --git a/plugins/!NotAdopted/VypressChat/userlist.c b/plugins/!NotAdopted/VypressChat/userlist.c
new file mode 100644
index 0000000000..c55b25df0c
--- /dev/null
+++ b/plugins/!NotAdopted/VypressChat/userlist.c
@@ -0,0 +1,718 @@
+/*
+ * Miranda-IM Vypress Chat/quickChat plugins
+ * Copyright (C) Saulius Menkevicius
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * $Id: userlist.c,v 1.26 2005/03/22 19:38:13 bobas Exp $
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "contrib/hashtable.h"
+#include "contrib/strhashfunc.h"
+
+#include "miranda.h"
+#include "main.h"
+#include "user.h"
+#include "userlist.h"
+#include "contacts.h"
+#include "msgloop.h"
+#include "chatroom.h"
+#include "chanlist.h"
+#include "util.h"
+
+/* struct descriptions
+ */
+struct userlist_entry {
+ enum vqp_status vqp_status;
+ enum vqp_codepage codepage;
+ enum vqp_gender gender;
+ unsigned int swversion;
+ vqp_uuid_t uuid;
+ vqp_addr_t addr;
+
+ int alive; /* non-0, if replied to last refresh req */
+ int chat_open;
+ char * chanlist;
+};
+
+struct userlist_remove_unreplied_info_struct {
+ char ** dead_keys;
+ int dead_key_count;
+};
+
+/* forward references
+ */
+static void userlist_entry_status_change(
+ const char * user_name, struct userlist_entry * entry,
+ enum vqp_status new_user_status, enum vqp_gender new_user_gender,
+ const char * new_status_text, int add_to_log);
+
+/* static data
+ */
+static struct hashtable * s_userlistHash = NULL;
+static UINT_PTR s_userlistTimer;
+static vqp_link_t s_vqpLink;
+
+/* static routines
+ */
+#define userlist_entry_by_name(user_name) \
+ ((struct userlist_entry*)hashtable_search(s_userlistHash, (void*)(user_name)))
+
+static struct userlist_entry *
+userlist_entry_by_name_vqp(const char * vqp_user_name)
+{
+ struct userlist_entry * entry;
+ char * user_name;
+
+#ifdef VYPRESSCHAT
+ /* by default we'll try search supposing vqp_user_name is in utf encoding */
+ entry = userlist_entry_by_name(vqp_user_name);
+ if(!entry) {
+ /* next - we'll thinking the vqp_user_name is in locale encoding */
+ entry = userlist_entry_by_name(user_name = util_loc2utf(vqp_user_name));
+ free(user_name);
+ }
+#else
+ /* by default we'll try the locale encoding:
+ * qChat clients don't know a thing about utf8
+ */
+ entry = userlist_entry_by_name(user_name = util_loc2utf(vqp_user_name));
+ free(user_name);
+#endif
+
+ return entry;
+}
+
+static void
+s_userlistHash_free_entry(void * value)
+{
+ struct userlist_entry * entry = (struct userlist_entry *)value;
+
+ chanlist_free(entry->chanlist);
+ free(entry);
+}
+
+/* userlist_remove_unreplied_cb:
+ * this is used to remove 'dead' users from user list
+ * and should be invoked periodically
+ *
+ * userlist_add/userlist_status_change are used to notify
+ * that a user is alive during the refresh period
+ */
+static int
+userlist_remove_unreplied_enum_fn(
+ void * key, void * value, void * user_data)
+{
+ struct userlist_entry * entry = (struct userlist_entry*)value;
+ struct userlist_remove_unreplied_info_struct * info
+ = (struct userlist_remove_unreplied_info_struct*)user_data;
+
+ if(!entry->alive) {
+ /* add user to dead user list */
+ info->dead_keys[info->dead_key_count++] = strdup((char*)key);
+ } else {
+ /* reset the user as dead */
+ entry->alive = 0;
+ }
+
+ return 1; /* continue enumeration */
+}
+
+static void CALLBACK
+userlist_remove_unreplied_cb(
+ HWND hWnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
+{
+ struct userlist_remove_unreplied_info_struct info;
+ int i;
+ char * r_nickname;
+
+ ASSERT_RETURNIFFAIL(VALIDPTR(s_userlistHash));
+
+ /* perform enumeration to find the unreplied ones */
+ info.dead_keys = malloc(
+ sizeof(void*) * hashtable_count(s_userlistHash));
+ info.dead_key_count = 0;
+ hashtable_enumerate(
+ s_userlistHash,
+ userlist_remove_unreplied_enum_fn, (void*)&info);
+
+ /* quietly remove those dead */
+ for(i = 0; i < info.dead_key_count; i++) {
+ userlist_remove(info.dead_keys[i], 0);
+ free(info.dead_keys[i]);
+ }
+
+ /* free leftovers */
+ free(info.dead_keys);
+
+ /* send another user refresh list request to the network
+ */
+ r_nickname = util_utf2vqp(user_codepage(), user_nickname());
+ msgloop_send(vqp_msg_refresh_req(s_vqpLink, r_nickname, user_codepage()), 0);
+ free(r_nickname);
+}
+
+/* exported routines
+ */
+void userlist_init()
+{
+ s_userlistTimer = 0;
+
+ s_userlistHash = NULL;
+ userlist_flush();
+}
+
+void userlist_uninit()
+{
+ hashtable_destroy(s_userlistHash, 1);
+ s_userlistHash = NULL;
+
+ if(s_userlistTimer) {
+ KillTimer(NULL, s_userlistTimer);
+ s_userlistTimer = 0;
+ }
+}
+
+void userlist_flush()
+{
+ /* a rude way to cleanup the list */
+ if(s_userlistHash) {
+ hashtable_destroy(s_userlistHash, 1);
+ s_userlistHash = NULL;
+ }
+
+ s_userlistHash = create_hashtable(
+ 256, hashtable_strhashfunc, hashtable_strequalfunc,
+ s_userlistHash_free_entry);
+}
+
+void userlist_connected(vqp_link_t vqpLink)
+{
+ DWORD timeout = 1000 * db_byte_get(NULL, VQCHAT_PROTO,
+ "UserlistTimeout", VQCHAT_DEF_REFRESH_TIMEOUT);
+ s_vqpLink = vqpLink;
+
+ userlist_flush();
+
+ /* start userlist refresh timers */
+ ASSERT_RETURNIFFAIL(!s_userlistTimer);
+ s_userlistTimer = SetTimer(NULL, 0, timeout, userlist_remove_unreplied_cb);
+}
+
+void userlist_disconnected()
+{
+ ASSERT_RETURNIFFAIL(s_userlistTimer);
+
+ userlist_flush();
+
+ s_vqpLink = NULL;
+
+ KillTimer(NULL, s_userlistTimer);
+ s_userlistTimer = 0;
+}
+
+/* userlist_add:
+ * adds user to userlist, updates it's info if the user already exists
+ */
+void userlist_add(
+ const char * name,
+ enum vqp_status vqp_status, enum vqp_gender gender, const vqp_uuid_t * p_uuid,
+ enum vqp_codepage codepage, unsigned int swversion, vqp_addr_t addr,
+ int add_to_log)
+{
+ struct userlist_entry * entry;
+ HANDLE hContact;
+
+ ASSERT_RETURNIFFAIL(VALIDPTR(name) && VALIDPTR(s_userlistHash));
+
+ entry = userlist_entry_by_name(name);
+ if(entry) {
+ entry->alive = 1;
+
+ userlist_entry_status_change(
+ name, entry, vqp_status, gender, "", add_to_log);
+ return;
+ }
+
+ /* setup new user list entry struct */
+ entry = malloc(sizeof(struct userlist_entry));
+ entry->vqp_status = vqp_status;
+ entry->codepage = codepage;
+ entry->gender = gender;
+ entry->swversion = swversion;
+ entry->uuid = *p_uuid;
+ entry->addr = addr;
+ entry->alive = 1;
+ entry->chat_open = 0;
+ entry->chanlist = NULL;
+
+ /* insert into the hash table */
+ hashtable_insert(s_userlistHash, strdup(name), entry);
+
+ /* notify DB that the user has become visible (if he is on list)
+ * and set user ip
+ */
+ hContact = contacts_find_contact(name);
+ if(hContact) {
+ contacts_set_contact_status(hContact, user_status_by_vqp_status(vqp_status));
+ contacts_set_contact_addr(hContact, addr);
+ }
+
+ /* every user of QuickChat/VypressChat networks is a member
+ * of #Main channel */
+ userlist_user_channel_join(name, VQCHAT_MAIN_CHANNEL, add_to_log);
+}
+
+static int userlist_remove_channel_part_enum(
+ const char * channel, void * enum_data)
+{
+ const char * user = ((const char **)enum_data)[0],
+ * explicit_part = ((const char **)enum_data)[1];
+
+ ASSERT_RETURNVALIFFAIL(VALIDPTR(channel) && VALIDPTR(enum_data), 0);
+
+ userlist_user_channel_part(user, channel, (int)explicit_part);
+ return 1;
+}
+
+/* userlist_remove:
+ * removes specified user from userlist
+ * params:
+ * @explicit_part - non-0 if the user left explicitly (he notified us of that)
+ */
+void userlist_remove(const char * user_name, int explicit_remove)
+{
+ struct userlist_entry * entry;
+
+ ASSERT_RETURNIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash));
+
+ entry = userlist_entry_by_name(user_name);
+ if(entry) {
+ const char * enum_cb_data[2];
+
+ /* notify DB that the user has become offline
+ * (if the user is in db list)
+ */
+ HANDLE hContact = contacts_find_contact(user_name);
+ if(hContact)
+ contacts_set_contact_status(hContact, ID_STATUS_OFFLINE);
+
+ /* update user's chatroom:
+ * leave every channel the user is in */
+ enum_cb_data[0] = user_name;
+ enum_cb_data[1] = (const char *)explicit_remove;
+ chanlist_enum(
+ entry->chanlist,
+ userlist_remove_channel_part_enum, (void*)enum_cb_data);
+
+ /* remove user entry from the hash and free it */
+ hashtable_remove(s_userlistHash, (void*)user_name, 1);
+ }
+}
+
+static int
+userlist_enum_proxy_fn(void * key, void * value, void * user_data)
+{
+ void ** proxy_data = (void **) user_data;
+
+ return ((userlist_enum_fn)proxy_data[0])(
+ (const char*)key, (void*)proxy_data[1]);
+}
+
+void userlist_enum(userlist_enum_fn enum_fn, void * enum_fn_data)
+{
+ void * proxy_data[2];
+
+ ASSERT_RETURNIFFAIL(enum_fn);
+
+ proxy_data[0] = (void*)enum_fn;
+ proxy_data[1] = (void*)enum_fn_data;
+
+ hashtable_enumerate(s_userlistHash,
+ userlist_enum_proxy_fn, (void*)proxy_data);
+}
+
+void userlist_name_change(const char * user_name, const char * new_user_name)
+{
+ struct userlist_entry * entry;
+
+ ASSERT_RETURNIFFAIL(VALIDPTR(user_name) && VALIDPTR(new_user_name));
+ ASSERT_RETURNIFFAIL(VALIDPTR(s_userlistHash));
+ ASSERT_RETURNIFFAIL(strcmp(user_name, new_user_name));
+
+ /* bail out if we have the new user name in the userlist already */
+ if(userlist_entry_by_name(new_user_name) )
+ return;
+
+ entry = userlist_entry_by_name(user_name);
+ if(entry) {
+ HANDLE hContact;
+
+ /* remove entry from hash table and reinsert with different nickname */
+ hashtable_remove(s_userlistHash, (void*)user_name, 0);
+ hashtable_insert(s_userlistHash, strdup(new_user_name), entry);
+
+ entry->alive = 1;
+
+ /* update contact nickname and status
+ */
+ hContact = contacts_find_contact(user_name);
+ if(hContact) {
+ if(db_byte_get(hContact, VQCHAT_PROTO, "LockNick", 0)) {
+ contacts_set_contact_status(hContact, ID_STATUS_OFFLINE);
+ } else {
+ HANDLE hAnotherContact = contacts_find_contact(new_user_name);
+ if(hAnotherContact) {
+ /* there is another contact with destination name:
+ * set hContact to offline, and get hAnotherContact alive
+ */
+ contacts_set_contact_status(hContact, ID_STATUS_OFFLINE);
+
+ contacts_set_contact_status(
+ hAnotherContact,
+ user_status_by_vqp_status(entry->vqp_status)
+ );
+ contacts_set_contact_addr(hContact, entry->addr);
+ } else {
+ /* change nickname for the contact */
+ contacts_set_contact_nickname(hContact, new_user_name);
+ }
+ }
+ } else {
+ /* the specified contact does not exist,
+ * try to find the contact with `new_user_name' nickname
+ */
+ hContact = contacts_find_contact(new_user_name);
+ if(hContact) {
+ contacts_set_contact_status(
+ hContact,
+ user_status_by_vqp_status(entry->vqp_status)
+ );
+ contacts_set_contact_addr(hContact, entry->addr);
+ }
+ }
+
+ /* update chatrooms */
+ chatroom_channel_user_name_change(user_name, new_user_name, 1);
+ }
+}
+
+/* userlist_entry_status_change:
+ * updates user's status
+ */
+static void
+userlist_entry_status_change(
+ const char * user_name, struct userlist_entry * entry,
+ enum vqp_status new_user_status, enum vqp_gender new_user_gender,
+ const char * mode_text, int add_to_log)
+{
+ entry->alive = 1;
+
+ if(entry->vqp_status != new_user_status) {
+ HANDLE hContact;
+ int new_status = user_status_by_vqp_status(new_user_status);
+
+ entry->vqp_status = new_user_status;
+
+ /* notify db of user mode change */
+ hContact = contacts_find_contact(user_name);
+ if(hContact) {
+ contacts_set_contact_status(hContact, new_status);
+ contacts_set_contact_gender(hContact, new_user_gender);
+ }
+
+ /* update chatrooms */
+ chatroom_channel_user_status_change(user_name, new_status, mode_text, add_to_log);
+ }
+
+}
+
+void userlist_status_change(
+ const char * user_name,
+ enum vqp_status new_user_status, enum vqp_gender new_user_gender,
+ const char * status_text, int add_to_log)
+{
+ struct userlist_entry * entry;
+
+ ASSERT_RETURNIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash));
+
+ entry = userlist_entry_by_name(user_name);
+ if(entry) {
+ userlist_entry_status_change(
+ user_name, entry, new_user_status, new_user_gender,
+ status_text, add_to_log);
+ }
+}
+
+void userlist_user_channel_join(
+ const char * user_name, const char * channel,
+ int add_to_log)
+{
+ struct userlist_entry * entry;
+ ASSERT_RETURNIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash));
+
+ entry = userlist_entry_by_name(user_name);
+ if(entry && !chanlist_contains(entry->chanlist, channel)) {
+ /* add channel to user's channel list */
+ entry->chanlist = chanlist_add(entry->chanlist, channel);
+
+ /* notify chatrooms */
+ chatroom_channel_user_join(channel, user_name, add_to_log);
+ }
+}
+
+/* userlist_user_channel_part:
+ * this will make the user leave the specified channel
+ * parameters:
+ * @explicit_part - specifies whether the user left explicitly, or because
+ * of refresh timeout
+ */
+void userlist_user_channel_part(
+ const char * user_name, const char * channel,
+ int explicit_part)
+{
+ struct userlist_entry * entry;
+
+ ASSERT_RETURNIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash));
+ ASSERT_RETURNIFFAIL(VALIDPTR(channel));
+
+ entry = userlist_entry_by_name(user_name);
+ if(entry && chanlist_contains(entry->chanlist, channel)) {
+ /* remove channel from user's channel list */
+ entry->chanlist = chanlist_remove(entry->chanlist, channel);
+
+ /* notify chatrooms */
+ chatroom_channel_user_part(channel, user_name, explicit_part);
+ }
+}
+
+/* userlist_user_channel_text:
+ * writes text to specified channel,
+ * and registers user and joins the channel, if not already
+ */
+void userlist_user_channel_text(
+ const char * user_name, const char * channel, const char * text,
+ vqp_addr_t src_addr, int is_action_text)
+{
+ struct userlist_entry * entry;
+
+ entry = userlist_entry_by_name(user_name);
+ if(!entry) {
+ vqp_uuid_t nil_uuid;
+ vqp_uuid_create_nil(&nil_uuid);
+
+ /* add the user to our user list */
+ userlist_add(
+ user_name, VQP_STATUS_AVAILABLE, VQP_GENDER_MALE, &nil_uuid,
+ VQCHAT_VQP_COMPAT_CODEPAGE, VQCHAT_UNDEF_SWVERSION, src_addr,
+ 0);
+ }
+
+ entry = userlist_entry_by_name(user_name);
+ if(entry) {
+ /* join the channel first and
+ * write the text to the channel
+ */
+ userlist_user_channel_join(user_name, channel, 0);
+ chatroom_channel_user_text(channel, user_name, text, is_action_text);
+ }
+}
+
+static int userlist_user_chanlist_update_join_enum_fn(
+ const char * channel, void * user_name)
+{
+ userlist_user_channel_join((const char *)user_name, channel, 0);
+ return 1;
+}
+
+static int userlist_user_chanlist_update_part_enum_fn(
+ const char * channel, void * updated_chanlist)
+{
+ const char ** enum_fn_data = (const char **)updated_chanlist;
+
+ if(!chanlist_contains(enum_fn_data[0], channel))
+ userlist_user_channel_part(enum_fn_data[1], channel, 0);
+
+ return 1;
+}
+
+void userlist_user_chanlist_update(
+ const char * user_name,
+ const char * updated_chanlist)
+{
+ struct userlist_entry * entry;
+
+ ASSERT_RETURNIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash));
+ entry = userlist_entry_by_name(user_name);
+ if(entry) {
+ const char * enum_fn_data[2];
+ char * new_chanlist = chanlist_copy(updated_chanlist);
+
+ /* join new previously-missing channels */
+ chanlist_enum(
+ new_chanlist,
+ userlist_user_chanlist_update_join_enum_fn, (void*)user_name);
+
+ /* leave the channels that the user is off */
+ enum_fn_data[0] = new_chanlist;
+ enum_fn_data[1] = user_name;
+ chanlist_enum(
+ entry->chanlist,
+ userlist_user_chanlist_update_part_enum_fn, (void*)enum_fn_data);
+
+ /* free chanlists */
+ chanlist_free(new_chanlist);
+ }
+}
+
+int userlist_user_exists(const char * user_name)
+{
+ ASSERT_RETURNVALIFFAIL(
+ VALIDPTR(s_userlistHash) && VALIDPTR(user_name), 0);
+ return userlist_entry_by_name(user_name) != NULL;
+}
+
+/* userlist_user_status:
+ * returns miranda's status for user, or ID_STATUS_OFFLINE, if not found
+ */
+int userlist_user_status(const char * user_name)
+{
+ struct userlist_entry * entry;
+
+ ASSERT_RETURNVALIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash), ID_STATUS_OFFLINE);
+
+ entry = userlist_entry_by_name(user_name);
+ return entry ? user_status_by_vqp_status(entry->vqp_status): ID_STATUS_OFFLINE;
+}
+
+/* userlist_user_addr:
+ * returns user's address for user, or 0, if not found
+ */
+vqp_addr_t
+userlist_user_addr(const char * user_name)
+{
+ struct userlist_entry * entry;
+ vqp_addr_t addr;
+
+ entry = userlist_entry_by_name(user_name);
+ if(entry)
+ return entry->addr;
+
+ memset(&addr, 0, sizeof(addr));
+ return addr;
+}
+
+/* userlist_user_chanlist:
+ * returns users' chanlist
+ */
+const char *
+userlist_user_chanlist(const char * user_name)
+{
+ struct userlist_entry * entry;
+ ASSERT_RETURNVALIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash), 0);
+
+ entry = userlist_entry_by_name(user_name);
+ return entry ? entry->chanlist: NULL;
+
+}
+
+/* userlist_user_gender:
+ * returns user's gender
+ */
+enum vqp_gender userlist_user_gender(const char * user_name)
+{
+ struct userlist_entry * entry;
+ ASSERT_RETURNVALIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash), 0);
+ entry = userlist_entry_by_name(user_name);
+ return entry ? entry->gender: VQP_GENDER_MALE;
+}
+
+/* userlist_user_swversion:
+ * returns' user's software version
+ */
+unsigned int userlist_user_swversion(const char * user_name)
+{
+ struct userlist_entry * entry;
+ ASSERT_RETURNVALIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash), 0);
+ entry = userlist_entry_by_name(user_name);
+ return entry ? entry->swversion: VQCHAT_UNDEF_SWVERSION;
+}
+
+/* userlist_user_is_chat_open:
+ * returns if we have a private chat opened with the user
+ */
+int userlist_user_is_chat_open(const char * user_name)
+{
+ struct userlist_entry * entry;
+ ASSERT_RETURNVALIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash), 0);
+ entry = userlist_entry_by_name(user_name);
+ return entry ? entry->chat_open: 0;
+}
+
+/* userlist_user_set_chat_open:
+ * sets wherether the chat is open with the user
+ */
+void userlist_user_set_chat_open(const char * user_name, int set_chat_open)
+{
+ struct userlist_entry * entry;
+ ASSERT_RETURNIFFAIL(VALIDPTR(user_name) && VALIDPTR(s_userlistHash));
+
+ entry = userlist_entry_by_name(user_name);
+ if(entry)
+ entry->chat_open = set_chat_open;
+}
+
+/* userlist_user_codepage:
+ * returns codepage for specified user
+ * (and guesses it, if the user was not found)
+ */
+enum vqp_codepage
+userlist_user_codepage(const char * user_name)
+{
+ struct userlist_entry * entry;
+ ASSERT_RETURNVALIFFAIL(VALIDPTR(user_name), VQP_CODEPAGE_LOCALE);
+
+ entry = userlist_entry_by_name(user_name);
+
+ return entry ? entry->codepage: VQCHAT_VQP_COMPAT_CODEPAGE;
+}
+
+/* userlist_user_codepage_vqp:
+ * tries to get or guess codepage for specified user name
+ * (on vypresschat it tries UTF8 encoding first, on qChat - locale)
+ */
+enum vqp_codepage
+userlist_user_codepage_vqp(const char * vqp_user_name)
+{
+ struct userlist_entry * entry;
+ ASSERT_RETURNVALIFFAIL(VALIDPTR(vqp_user_name), VQCHAT_VQP_COMPAT_CODEPAGE);
+
+ entry = userlist_entry_by_name_vqp(vqp_user_name);
+ return entry ? entry->codepage: VQCHAT_VQP_COMPAT_CODEPAGE;
+}
+
+void userlist_user_set_codepage(const char * user_name, enum vqp_codepage codepage)
+{
+ struct userlist_entry * entry;
+ ASSERT_RETURNIFFAIL(VALIDPTR(user_name));
+
+ entry = userlist_entry_by_name(user_name);
+ if(entry)
+ entry->codepage = codepage;
+}
+