#include "unrealircd.h" /* this should go into include/numeric.h (was there at one point of time) */ #define RPL_WHOISKEYVALUE 760 #define RPL_KEYVALUE 761 #define RPL_KEYNOTSET 766 #define RPL_METADATASUBOK 770 #define RPL_METADATAUNSUBOK 771 #define RPL_METADATASUBS 772 #define RPL_METADATASYNCLATER 774 #define STR_RPL_WHOISKEYVALUE /* 760 */ "%s %s %s :%s" #define STR_RPL_KEYVALUE /* 761 */ "%s %s %s :%s" #define STR_RPL_KEYNOTSET /* 766 */ "%s %s :key not set" /* FIXME sending FAIL instead */ #define STR_RPL_METADATASUBOK /* 770 */ ":%s" #define STR_RPL_METADATAUNSUBOK /* 771 */ ":%s" #define STR_RPL_METADATASUBS /* 772 */ ":%s" #define STR_RPL_METADATASYNCLATER /* 774 */ "%s %s" /* sendnumeric() which allows message tags (BATCH in our case) */ #define sendnumeric_mtags(to, mtags, numeric, ...) sendnumericfmt_tags(to, mtags, numeric, STR_ ## numeric, ##__VA_ARGS__) void vsendto_one(Client *to, MessageTag *mtags, const char *pattern, va_list vl); /* no prototype from send.c */ void sendnumericfmt_tags (Client *to, MessageTag *mtags, int numeric, FORMAT_STRING(const char *pattern), ...) { va_list vl; char realpattern[512]; snprintf(realpattern, sizeof(realpattern), ":%s %.3d %s %s", me.name, numeric, to->name[0] ? to->name : "*", pattern); va_start(vl, pattern); vsendto_one(to, mtags, realpattern, vl); va_end(vl); } /* actual METADATA code */ #define STR_HELPER(x) #x #define STR(x) STR_HELPER(x) /* get or set for perms */ #define MODE_SET 0 #define MODE_GET 1 #define WATCH_EVENT_METADATA 3000 /* core uses 0..8, we hope no other module will try 3000 */ #define MYCONF "metadata" #define CHECKPARAMSCNT_OR_DIE(count, return) \ { \ if (parc < count+1 || BadPtr(parv[count])) \ { \ sendnumeric(client, ERR_NEEDMOREPARAMS, "METADATA"); \ return; \ } \ } /* target "*" is always the user issuing the command */ #define PROCESS_TARGET_OR_DIE(target, user, channel, return) \ { \ char *channame; \ channame = strchr(target, '#'); \ if (channame) \ { \ channel = find_channel(channame); \ if (!channel) \ { \ sendnumeric(client, ERR_NOSUCHNICK, channame); \ return; \ } \ } else \ { \ if (strcmp(target, "*")) \ { \ user = hash_find_nickatserver(target, NULL); \ if (!user) \ { \ sendnumeric(client, ERR_NOSUCHNICK, target); \ return; \ } \ } else \ { \ user = client; \ } \ } \ } #define FOR_EACH_KEY(keyindex, parc, parv) while(keyindex++, key = parv[keyindex], (!BadPtr(key) && keyindex < parc)) #define IsSendable(x) (DBufLength(&x->local->sendQ) < 2048) #define CHECKREGISTERED_OR_DIE(client, return) \ { \ if (!IsUser(client)) \ { \ sendnumeric(client, ERR_NOTREGISTERED); \ return; \ } \ } #define USER_METADATA(client) moddata_client(client, metadataUser).ptr #define CHANNEL_METADATA(channel) moddata_channel(channel, metadataChannel).ptr #define MAKE_BATCH(client, batch, mtags) do { \ if (HasCapability(client, "batch")) { \ generate_batch_id(batch); \ sendto_one(client, NULL, ":%s BATCH +%s metadata", me.name, batch); \ mtags = safe_alloc(sizeof(MessageTag)); \ mtags->name = strdup("batch"); \ mtags->value = strdup(batch); \ } \ } while(0) #define FINISH_BATCH(client, batch, mtags) do { \ if (*batch) \ sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch); \ if (mtags) \ free_message_tags(mtags); \ } while(0) struct metadata { char *name; char *value; struct metadata *next; }; struct metadata_subscriptions { char *name; struct metadata_subscriptions *next; }; struct metadata_moddata_user { struct metadata *metadata; struct metadata_subscriptions *subs; struct metadata_unsynced *us; }; struct metadata_unsynced { /* we're listing users (UIDs) or channels that should be synced but were not */ char *id; char *key; struct metadata_unsynced *next; }; CMD_FUNC(cmd_metadata); CMD_FUNC(cmd_metadata_remote); CMD_FUNC(cmd_metadata_local); EVENT(metadata_queue_evt); const char *metadata_cap_param(Client *client); char *metadata_isupport_param(void); int metadata_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); int metadata_configposttest(int *errs); int metadata_configrun(ConfigFile *cf, ConfigEntry *ce, int type); int metadata_server_sync(Client *client); int metadata_join(Client *client, Channel *channel, MessageTag *mtags); int metadata_user_registered(Client *client); void metadata_user_free(ModData *md); void metadata_channel_free(ModData *md); void metadata_free(struct metadata *metadata); void metadata_free_subs(struct metadata_subscriptions *subs); int metadata_is_subscribed(Client *user, const char *key); const char *metadata_get_user_key_value(Client *user, const char *key); const char *metadata_get_channel_key_value(Channel *channel, const char *key); void user_metadata_changed(Client *user, const char *key, const char *value, Client *changer); void channel_metadata_changed(Channel *channel, const char *key, const char *value, Client *changer); void metadata_free_list(struct metadata *metadata, const char *whose, Client *client); struct metadata_moddata_user *metadata_prepare_user_moddata(Client *user); void metadata_set_channel(Channel *channel, const char *key, const char *value, Client *client); void metadata_set_user(Client *user, const char *key, const char *value, Client *client); void metadata_send_channel(Channel *channel, const char *key, Client *client, MessageTag *mtags); void metadata_send_user(Client *user, const char *key, Client *client, MessageTag *mtags); int metadata_subscribe(const char *key, Client *client, int remove); void metadata_clear_channel(Channel *channel, Client *client); void metadata_clear_user(Client *user, Client *client); void metadata_send_subscribtions(Client *client); void metadata_send_all_for_channel(Channel *channel, Client *client); void metadata_send_all_for_user(Client *user, Client *client); void metadata_send_pending(Client *client); void metadata_sync_user(Client *client, Client *target, MessageTag *mtags, int create_batch); void metadata_sync_channel(Client *client, Channel *channel); int metadata_key_valid(const char *key); int metadata_check_perms(Client *user, Channel *channel, Client *client, const char *key, int mode); void metadata_send_change(Client *client, MessageTag *mtags, const char *who, const char *key, const char *value, Client *changer); int metadata_notify_or_queue(Client *client, MessageTag *mtags, const char *who, const char *key, const char *value, Client *changer); int metadata_monitor_connect(Client *client); int metadata_monitor_notification(Client *client, Watch *watch, Link *lp, int event); ModDataInfo *metadataUser; ModDataInfo *metadataChannel; long CAP_METADATA = 0L; long CAP_METADATA_NOTIFY = 0L; struct metadata_settings_s { int max_user_metadata; int max_channel_metadata; int max_subscriptions; int max_value_bytes; } metadata_settings; ModuleHeader MOD_HEADER = { "third/metadata-2", "6.0", "draft/metadata-2 and draft/metadata-notify-2 cap", "k4be", "unrealircd-6" }; /* metadata { max-user-metadata 10; max-channel-metadata 10; max-subscriptions 10; max-value-bytes 300; }; */ int metadata_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) { ConfigEntry *cep; int errors = 0; int i; if (type != CONFIG_MAIN) return 0; if (!ce || !ce->name) return 0; if (strcmp(ce->name, MYCONF)) return 0; for (cep = ce->items; cep; cep = cep->next) { if (!cep->name) { config_error("%s:%i: blank %s item", cep->file->filename, cep->line_number, MYCONF); errors++; continue; } if (!cep->value || !strlen(cep->value)) { config_error("%s:%i: %s::%s must be non-empty", cep->file->filename, cep->line_number, MYCONF, cep->name); errors++; continue; } if (!strcmp(cep->name, "max-user-metadata")) { for (i = 0; cep->value[i]; i++) { if (!isdigit(cep->value[i])) { config_error("%s:%i: %s::%s must be an integer between 1 and 100", cep->file->filename, cep->line_number, MYCONF, cep->name); errors++; break; } } if (!errors && (atoi(cep->value) < 1 || atoi(cep->value) > 100)) { config_error("%s:%i: %s::%s must be an integer between 1 and 100", cep->file->filename, cep->line_number, MYCONF, cep->name); errors++; } continue; } if (!strcmp(cep->name, "max-channel-metadata")) { for (i = 0; cep->value[i]; i++) { if (!isdigit(cep->value[i])) { config_error("%s:%i: %s::%s must be an integer between 0 and 100", cep->file->filename, cep->line_number, MYCONF, cep->name); errors++; break; } } if (!errors && (atoi(cep->value) < 0 || atoi(cep->value) > 100)) { config_error("%s:%i: %s::%s must be an integer between 0 and 100", cep->file->filename, cep->line_number, MYCONF, cep->name); errors++; } continue; } if (!strcmp(cep->name, "max-subscriptions")) { for (i = 0; cep->value[i]; i++) { if (!isdigit(cep->value[i])) { config_error("%s:%i: %s::%s must be an integer between 1 and 100", cep->file->filename, cep->line_number, MYCONF, cep->name); errors++; break; } } if (!errors && (atoi(cep->value) < 0 || atoi(cep->value) > 100)) { config_error("%s:%i: %s::%s must be an integer between 1 and 100", cep->file->filename, cep->line_number, MYCONF, cep->name); errors++; } continue; } if (!strcmp(cep->name, "max-value-bytes")) { for (i = 0; cep->value[i]; i++) { if (!isdigit(cep->value[i])) { config_error("%s:%i: %s::%s must be an integer between 1 and 400", cep->file->filename, cep->line_number, MYCONF, cep->name); errors++; break; } } if (!errors && (atoi(cep->value) < 0 || atoi(cep->value) > 400)) { config_error("%s:%i: %s::%s must be an integer between 1 and 400", cep->file->filename, cep->line_number, MYCONF, cep->name); errors++; } continue; } config_warn("%s:%i: unknown item %s::%s", cep->file->filename, cep->line_number, MYCONF, cep->name); } *errs = errors; return errors ? -1 : 1; } int metadata_configposttest(int *errs) { /* null the settings to avoid keeping old value if none is set in config */ metadata_settings.max_user_metadata = 0; metadata_settings.max_channel_metadata = 0; metadata_settings.max_subscriptions = 0; metadata_settings.max_value_bytes = 0; return 1; } int metadata_configrun(ConfigFile *cf, ConfigEntry *ce, int type) { ConfigEntry *cep; if (type != CONFIG_MAIN) return 0; if (!ce || !ce->name) return 0; if (strcmp(ce->name, MYCONF)) return 0; for (cep = ce->items; cep; cep = cep->next) { if (!cep->name) continue; if (!strcmp(cep->name, "max-user-metadata")) { metadata_settings.max_user_metadata = atoi(cep->value); continue; } if (!strcmp(cep->name, "max-channel-metadata")) { metadata_settings.max_channel_metadata = atoi(cep->value); continue; } if (!strcmp(cep->name, "max-subscriptions")) { metadata_settings.max_subscriptions = atoi(cep->value); continue; } if (!strcmp(cep->name, "max-value-bytes")) { metadata_settings.max_value_bytes = atoi(cep->value); continue; } } return 1; } MOD_TEST(){ MARK_AS_OFFICIAL_MODULE(modinfo); HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, metadata_configtest); HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, metadata_configposttest); return MOD_SUCCESS; } MOD_INIT() { ClientCapabilityInfo cap; ClientCapability *c; ModDataInfo mreq; MARK_AS_OFFICIAL_MODULE(modinfo); MARK_AS_GLOBAL_MODULE(modinfo); memset(&cap, 0, sizeof(cap)); cap.name = "draft/metadata-2"; cap.parameter = metadata_cap_param; c = ClientCapabilityAdd(modinfo->handle, &cap, &CAP_METADATA); memset(&cap, 0, sizeof(cap)); cap.name = "draft/metadata-notify-2"; /* for old client compatibility */ c = ClientCapabilityAdd(modinfo->handle, &cap, &CAP_METADATA_NOTIFY); CommandAdd(modinfo->handle, "METADATA", cmd_metadata, MAXPARA, CMD_USER|CMD_SERVER|CMD_UNREGISTERED); memset(&mreq, 0 , sizeof(mreq)); mreq.type = MODDATATYPE_CLIENT; mreq.name = "metadata_user", mreq.free = metadata_user_free; metadataUser = ModDataAdd(modinfo->handle, mreq); if (!metadataUser) { config_error("[%s] Failed to request metadata_user moddata: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle)); return MOD_FAILED; } memset(&mreq, 0 , sizeof(mreq)); mreq.type = MODDATATYPE_CHANNEL; mreq.name = "metadata_channel", mreq.free = metadata_channel_free; metadataChannel = ModDataAdd(modinfo->handle, mreq); if (!metadataChannel) { config_error("[%s] Failed to request metadata_channel moddata: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle)); return MOD_FAILED; } HookAdd(modinfo->handle, HOOKTYPE_SERVER_SYNC, 0, metadata_server_sync); HookAdd(modinfo->handle, HOOKTYPE_LOCAL_JOIN, -2, metadata_join); HookAdd(modinfo->handle, HOOKTYPE_REMOTE_JOIN, -2, metadata_join); HookAdd(modinfo->handle, HOOKTYPE_LOCAL_CONNECT, 0, metadata_user_registered); HookAdd(modinfo->handle, HOOKTYPE_LOCAL_CONNECT, 0, metadata_monitor_connect); HookAdd(modinfo->handle, HOOKTYPE_REMOTE_CONNECT, 0, metadata_monitor_connect); HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, metadata_configrun); return MOD_SUCCESS; } MOD_LOAD() { /* setting default values if not configured */ if (metadata_settings.max_user_metadata == 0) metadata_settings.max_user_metadata = 10; if (metadata_settings.max_channel_metadata == 0) metadata_settings.max_channel_metadata = 10; if (metadata_settings.max_subscriptions == 0) metadata_settings.max_subscriptions = 10; if (metadata_settings.max_value_bytes == 0) metadata_settings.max_value_bytes = 300; EventAdd(modinfo->handle, "metadata_queue", metadata_queue_evt, NULL, 1500, 0); ISupportAdd(modinfo->handle, "METADATA", metadata_isupport_param()); return MOD_SUCCESS; } MOD_UNLOAD() { return MOD_SUCCESS; } const char *metadata_cap_param(Client *client) { static char buf[80]; ircsnprintf(buf, sizeof(buf), "before-connect,max-subs=%d,max-keys=%d,max-value-bytes=%d", metadata_settings.max_subscriptions, metadata_settings.max_user_metadata, metadata_settings.max_value_bytes); return buf; } char *metadata_isupport_param(void) { static char buf[20]; ircsnprintf(buf, sizeof(buf), "%d", metadata_settings.max_user_metadata); return buf; } void metadata_free(struct metadata *metadata) { safe_free(metadata->name); safe_free(metadata->value); safe_free(metadata); } void metadata_free_subs(struct metadata_subscriptions *subs) { safe_free(subs->name); safe_free(subs); } int metadata_is_subscribed(Client *user, const char *key) { struct metadata_moddata_user *moddata = USER_METADATA(user); if (!moddata) return 0; struct metadata_subscriptions *subs; for (subs = moddata->subs; subs; subs = subs->next) { if (!strcasecmp(subs->name, key)) return 1; } return 0; } const char *metadata_get_user_key_value(Client *user, const char *key) { struct metadata_moddata_user *moddata = USER_METADATA(user); struct metadata *metadata = NULL; if (!moddata) return NULL; for (metadata = moddata->metadata; metadata; metadata = metadata->next) { if (!strcasecmp(key, metadata->name)) return metadata->value; } return NULL; } const char *metadata_get_channel_key_value(Channel *channel, const char *key) { struct metadata *metadata; for (metadata = CHANNEL_METADATA(channel); metadata; metadata = metadata->next) { if (!strcasecmp(key, metadata->name)) return metadata->value; } return NULL; } /* returns 1 if something remains to sync */ int metadata_notify_or_queue(Client *client, MessageTag *mtags, const char *who, const char *key, const char *value, Client *changer) { int trylater = 0; if (!who) { unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", changer, "metadata_notify_or_queue called with null who!"); return 0; } if (!key) { unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", changer, "metadata_notify_or_queue called with null key!"); return 0; } if (!client) { unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", changer, "metadata_notify_or_queue called with null client!"); return 0; } struct metadata_moddata_user *moddata = USER_METADATA(client); if (!moddata) moddata = metadata_prepare_user_moddata(client); struct metadata_unsynced **us = &moddata->us; if (IsSendable(client)) { metadata_send_change(client, mtags, who, key, value, changer); } else { /* store for the SYNC */ Client *who_client; const char *uid_or_channel; if (*who == '#') { uid_or_channel = who; } else { who_client = find_client(who, NULL); /* FIXME the caller should already have this figured out */ if (!who_client) { unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", changer, "metadata_notify_or_queue called with nonexistent client!"); return 0; /* shouldn't happen */ } uid_or_channel = who_client->id; } trylater = 1; while (*us) us = &(*us)->next; /* find last list element */ *us = safe_alloc(sizeof(struct metadata_unsynced)); (*us)->id = strdup(uid_or_channel); (*us)->key = strdup(key); (*us)->next = NULL; } return trylater; } void metadata_send_change(Client *client, MessageTag *mtags, const char *who, const char *key, const char *value, Client *changer) { char *sender = NULL; if (!key) { unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", changer, "metadata_send_change called with null key!"); return; } if (!who) { unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", changer, "metadata_send_change called with null who!"); return; } if (!client) { unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", changer, "metadata_send_change called with null client!"); return; } if (changer) { if (IsServer(client)) sender = changer->id; else sender = changer->name; } if (!sender) sender = me.name; if (changer && IsUser(changer) && MyUser(client)) { if (!value) sendto_one(client, mtags, ":%s!%s@%s METADATA %s %s %s", sender, changer->user->username, GetHost(changer), who, key, "*"); else sendto_one(client, mtags, ":%s!%s@%s METADATA %s %s %s :%s", sender, changer->user->username, GetHost(changer), who, key, "*", value); } else { /* sending S2S (sender is id) or receiving S2S (sender is servername) */ if (!value) sendto_one(client, mtags, ":%s METADATA %s %s %s", sender, who, key, "*"); else sendto_one(client, mtags, ":%s METADATA %s %s %s :%s", sender, who, key, "*", value); } } /* used for broadcasting changes to subscribed users and linked servers */ void user_metadata_changed(Client *user, const char *key, const char *value, Client *changer){ Client *acptr; if (!user || !key) return; /* sanity check */ list_for_each_entry(acptr, &lclient_list, lclient_node) { /* notifications for local subscribers */ if(IsUser(acptr) && IsUser(user) && metadata_is_subscribed(acptr, key) && has_common_channels(user, acptr)) metadata_notify_or_queue(acptr, NULL, user->name, key, value, changer); } list_for_each_entry(acptr, &server_list, special_node) { /* notifications for linked servers, TODO change to sendto_server */ if (acptr == &me) continue; metadata_send_change(acptr, NULL, user->name, key, value, changer); } /* notifications for MONITOR */ watch_check(user, WATCH_EVENT_METADATA, metadata_monitor_notification); /* for now sending info for all entries even if only one has changed */ } void channel_metadata_changed(Channel *channel, const char *key, const char *value, Client *changer) { Client *acptr; if (!channel || !key) return; /* sanity check */ list_for_each_entry(acptr, &lclient_list, lclient_node) { /* notifications for local subscribers */ if (metadata_is_subscribed(acptr, key) && IsMember(acptr, channel)) metadata_send_change(acptr, NULL, channel->name, key, value, changer); } list_for_each_entry(acptr, &server_list, special_node) { /* notifications for linked servers, TODO change to sendto_server */ if(acptr == &me) continue; metadata_send_change(acptr, NULL, channel->name, key, value, changer); } } void metadata_free_list(struct metadata *metadata, const char *whose, Client *client) { struct metadata *prev_metadata = metadata; char *name; while(metadata) { name = metadata->name; safe_free(metadata->value); metadata = metadata->next; safe_free(prev_metadata); prev_metadata = metadata; if(client && whose && *whose) { /* send out the data being removed, unless we're unloading the module */ sendnumeric(client, RPL_KEYVALUE, whose, name, "*", ""); if(*whose == '#') channel_metadata_changed(find_channel(whose), name, NULL, client); else user_metadata_changed(hash_find_nickatserver(whose, NULL), name, NULL, client); } safe_free(name); } } void metadata_channel_free(ModData *md) { if (!md->ptr) return; /* was not set */ struct metadata *metadata = md->ptr; metadata_free_list(metadata, NULL, NULL); } void metadata_user_free(ModData *md) { struct metadata_moddata_user *moddata = md->ptr; if (!moddata) return; /* was not set */ struct metadata_subscriptions *sub = moddata->subs; struct metadata_subscriptions *prev_sub = sub; struct metadata_unsynced *us = moddata->us; struct metadata_unsynced *prev_us; while (sub) { safe_free(sub->name); sub = sub->next; safe_free(prev_sub); prev_sub = sub; } struct metadata *metadata = moddata->metadata; metadata_free_list(metadata, NULL, NULL); while (us) { safe_free(us->id); safe_free(us->key); prev_us = us; us = us->next; safe_free(prev_us); } safe_free(moddata); } struct metadata_moddata_user *metadata_prepare_user_moddata(Client *user) { USER_METADATA(user) = safe_alloc(sizeof(struct metadata_moddata_user)); struct metadata_moddata_user *ptr = USER_METADATA(user); ptr->metadata = NULL; ptr->subs = NULL; return ptr; } void metadata_set_user(Client *user, const char *key, const char *value, Client *client) { int changed = 0; Client *target; char *target_name; int removed = 0; int set = 0; int count = 0; char *utf8_val = NULL; if (user) { target = user; target_name = user->name; } else { target = client; target_name = "*"; } struct metadata_moddata_user *moddata = USER_METADATA(target); if (!moddata) /* first call for this user */ moddata = metadata_prepare_user_moddata(target); struct metadata **metadata = &moddata->metadata; struct metadata *prev; if (value && strlen(value) > metadata_settings.max_value_bytes) { if (client) sendto_one(client, NULL, ":%s FAIL METADATA VALUE_INVALID %s :metadata value too long", me.name, target_name); return; } if (BadPtr(value) || strlen(value) == 0) { /* unset */ value = NULL; /* just to make sure */ removed = 0; while (*metadata) { if (!strcasecmp(key, (*metadata)->name)) break; metadata = &(*metadata)->next; } if (*metadata) { prev = *metadata; *metadata = prev->next; metadata_free(prev); removed = 1; changed = 1; } if (!removed) { if (client) sendto_one(client, NULL, ":%s FAIL METADATA KEY_NOT_SET %s %s :key not set", me.name, target_name, key); /* not set so can't remove */ return; } } else { /* set */ utf8_val = safe_alloc(metadata_settings.max_value_bytes + 1); unrl_utf8_make_valid(value, utf8_val, metadata_settings.max_value_bytes, 1); while (*metadata) { if (!strcasecmp(key, (*metadata)->name)) { set = 1; if (strcmp(value, (*metadata)->value)) { safe_free((*metadata)->value); (*metadata)->value = utf8_val; changed = 1; } } metadata = &(*metadata)->next; count++; } if (!set) { if (!client || count < metadata_settings.max_user_metadata) { /* add new entry for user */ *metadata = safe_alloc(sizeof(struct metadata)); (*metadata)->next = NULL; (*metadata)->name = strdup(key); (*metadata)->value = utf8_val; changed = 1; } else { /* no more allowed */ if (client) sendto_one(client, NULL, ":%s FAIL METADATA LIMIT_REACHED %s :metadata limit reached", me.name, target_name); } } if (!changed) { safe_free(utf8_val); return; } } if (!IsServer(client) && MyConnect(client)) { if (BadPtr(value)) sendnumeric(client, RPL_KEYNOTSET, (*target_name)?target_name:"*", key); /* ok but empty */ else sendnumeric(client, RPL_KEYVALUE, (*target_name)?target_name:"*", key, "*", value?value:""); /* all OK */ } if (changed && (client == &me || IsUser(client) || IsServer(client))) user_metadata_changed(target, key, value, client); } void metadata_set_channel(Channel *channel, const char *key, const char *value, Client *client) { int changed = 0; int set = 0; int count = 0; char *utf8_val = NULL; struct metadata **metadata = (struct metadata **)&CHANNEL_METADATA(channel); struct metadata *prev; if (value && strlen(value) > metadata_settings.max_value_bytes) { if (client) sendto_one(client, NULL, ":%s FAIL METADATA VALUE_INVALID %s :metadata value too long", me.name, channel->name); return; } if(BadPtr(value) || strlen(value) == 0) { /* unset */ value = NULL; /* just to make sure */ int removed = 0; while (*metadata) { if (!strcasecmp(key, (*metadata)->name)) break; metadata = &(*metadata)->next; } if (*metadata) { prev = *metadata; *metadata = prev->next; metadata_free(prev); removed = 1; changed = 1; } if (!removed) { if (client) sendto_one(client, NULL, ":%s FAIL METADATA KEY_NOT_SET %s %s :key not set", me.name, channel->name, key); /* not set so can't remove */ return; } } else { /* set */ utf8_val = safe_alloc(metadata_settings.max_value_bytes + 1); unrl_utf8_make_valid(value, utf8_val, metadata_settings.max_value_bytes, 1); while (*metadata) { if (!strcasecmp(key, (*metadata)->name)) { set = 1; if (strcmp(value, (*metadata)->value)) { safe_free((*metadata)->value); (*metadata)->value = utf8_val; changed = 1; } } metadata = &(*metadata)->next; count++; } if (!set) { if (!client || count < metadata_settings.max_channel_metadata) { /* add new entry for user */ *metadata = safe_alloc(sizeof(struct metadata)); (*metadata)->next = NULL; (*metadata)->name = strdup(key); (*metadata)->value = utf8_val; changed = 1; } else { /* no more allowed */ if (client) sendto_one(client, NULL, ":%s FAIL METADATA LIMIT_REACHED %s :metadata limit reached", me.name, channel->name); } } if (!changed) { safe_free(utf8_val); return; } } if (IsUser(client) && MyUser(client)) { if (BadPtr(value)) sendnumeric(client, RPL_KEYNOTSET, channel->name, key); /* ok but empty */ else sendnumeric(client, RPL_KEYVALUE, channel->name, key, "*", value?value:""); /* all OK */ } if (changed && (IsUser(client) || IsServer(client))) channel_metadata_changed(channel, key, value, client); } int metadata_subscribe(const char *key, Client *client, int remove) { struct metadata_moddata_user *moddata = USER_METADATA(client); struct metadata_subscriptions **subs; struct metadata_subscriptions *prev_subs; int found = 0; int count = 0; int trylater = 0; const char *value; unsigned int hashnum; Channel *channel; Client *acptr; char batch[BATCHLEN+1] = ""; MessageTag *mtags = NULL; if (!client) return 0; if (!moddata) /* first call for this user */ moddata = metadata_prepare_user_moddata(client); subs = &moddata->subs; while (*subs) { count++; if (!strcasecmp(key, (*subs)->name)) { found = 1; if (remove) { prev_subs = *subs; *subs = prev_subs->next; metadata_free_subs(prev_subs); } break; } subs = &(*subs)->next; } if (!remove && !found) { if (count < metadata_settings.max_subscriptions) { *subs = safe_alloc(sizeof(struct metadata_subscriptions)); (*subs)->next = NULL; (*subs)->name = strdup(key); } else { /* no more allowed */ sendto_one(client, NULL, ":%s FAIL METADATA TOO_MANY_SUBS %s :too many subscriptions", me.name, key); return 0; } } if (!remove) { MAKE_BATCH(client, batch, mtags); sendnumeric_mtags(client, mtags, RPL_METADATASUBOK, key); if(!IsUser(client)) return 0; /* unregistered user is not getting any keys yet */ /* we have to send out all subscribed data now */ trylater = 0; list_for_each_entry(acptr, &client_list, client_node) { value = NULL; if (IsUser(client) && IsUser(acptr) && has_common_channels(acptr, client)) value = metadata_get_user_key_value(acptr, key); if (value) trylater |= metadata_notify_or_queue(client, mtags, acptr->name, key, value, NULL); } for (hashnum = 0; hashnum < CHAN_HASH_TABLE_SIZE; hashnum++) { for (channel = hash_get_chan_bucket(hashnum); channel; channel = channel->hnextch) { if (IsMember(client, channel)) { value = metadata_get_channel_key_value(channel, key); if (value) trylater |= metadata_notify_or_queue(client, mtags, channel->name, key, value, NULL); } } } FINISH_BATCH(client, batch, mtags); if (trylater) return 1; } else { sendnumeric(client, RPL_METADATAUNSUBOK, key); } return 0; } void metadata_send_channel(Channel *channel, const char *key, Client *client, MessageTag *mtags) { struct metadata *metadata; int found = 0; char batch[BATCHLEN+1] = ""; int parent_mtags = !!mtags; if (!parent_mtags) MAKE_BATCH(client, batch, mtags); for (metadata = CHANNEL_METADATA(channel); metadata; metadata = metadata->next) { if (!strcasecmp(key, metadata->name)) { found = 1; sendnumeric_mtags(client, mtags, RPL_KEYVALUE, channel->name, key, "*", metadata->value); break; } } if (!found) sendnumeric_mtags(client, mtags, RPL_KEYNOTSET, channel->name, key); if (!parent_mtags) FINISH_BATCH(client, batch, mtags); } void metadata_send_user(Client *user, const char *key, Client *client, MessageTag *mtags) { if (!user) user = client; struct metadata_moddata_user *moddata = USER_METADATA(user); struct metadata *metadata = NULL; char batch[BATCHLEN+1] = ""; int parent_mtags = !!mtags; if (!parent_mtags) MAKE_BATCH(client, batch, mtags); if (moddata) metadata = moddata->metadata; int found = 0; for ( ; metadata; metadata = metadata->next) { if (!strcasecmp(key, metadata->name)) { found = 1; sendnumeric_mtags(client, mtags, RPL_KEYVALUE, user->name, key, "*", metadata->value); break; } } if (!found) sendnumeric_mtags(client, mtags, RPL_KEYNOTSET, user->name, key); if (!parent_mtags) FINISH_BATCH(client, batch, mtags); } void metadata_clear_channel(Channel *channel, Client *client) { struct metadata *metadata = CHANNEL_METADATA(channel); metadata_free_list(metadata, channel->name, client); CHANNEL_METADATA(channel) = NULL; } void metadata_clear_user(Client *user, Client *client) { if (!user) user = client; struct metadata_moddata_user *moddata = USER_METADATA(user); struct metadata *metadata = NULL; if (!moddata) return; /* nothing to delete */ metadata = moddata->metadata; metadata_free_list(metadata, user->name, client); moddata->metadata = NULL; } void metadata_send_subscribtions(Client *client) { struct metadata_subscriptions *subs; struct metadata_moddata_user *moddata = USER_METADATA(client); char batch[BATCHLEN+1] = ""; MessageTag *mtags = NULL; MAKE_BATCH(client, batch, mtags); if (moddata) { for (subs = moddata->subs; subs; subs = subs->next) sendnumeric_mtags(client, mtags, RPL_METADATASUBS, subs->name); } FINISH_BATCH(client, batch, mtags); } void metadata_send_all_for_channel(Channel *channel, Client *client) { struct metadata *metadata; char batch[BATCHLEN+1] = ""; MessageTag *mtags = NULL; MAKE_BATCH(client, batch, mtags); for (metadata = CHANNEL_METADATA(channel); metadata; metadata = metadata->next) sendnumeric_mtags(client, mtags, RPL_KEYVALUE, channel->name, metadata->name, "*", metadata->value); FINISH_BATCH(client, batch, mtags); } void metadata_send_all_for_user(Client *user, Client *client) { struct metadata *metadata; char batch[BATCHLEN+1] = ""; MessageTag *mtags = NULL; if (!user) user = client; struct metadata_moddata_user *moddata = USER_METADATA(user); MAKE_BATCH(client, batch, mtags); if (moddata) { for (metadata = moddata->metadata; metadata; metadata = metadata->next) sendnumeric_mtags(client, mtags, RPL_KEYVALUE, user->name, metadata->name, "*", metadata->value); } FINISH_BATCH(client, batch, mtags); } int metadata_key_valid(const char *key) { for( ; *key; key++) { if(*key >= 'a' && *key <= 'z') continue; if(*key >= 'A' && *key <= 'Z') continue; if(*key >= '0' && *key <= '9') continue; if(*key == '_' || *key == '.' || *key == ':' || *key == '-') continue; return 0; } return 1; } int metadata_check_perms(Client *user, Channel *channel, Client *client, const char *key, int mode) { /* either user or channel should be NULL */ if (!IsUser(client) && channel) /* ignore channel metadata requests for unregistered users */ return 0; if ((user == client) || (!user && !channel)) /* specified target is "*" or own nick */ return 1; if (IsOper(client) && mode == MODE_GET) return 1; /* allow ircops to view everything */ if (channel) { /* The only requirement for GET is to be in the channel */ if ((mode == MODE_GET) && IsMember(client, channel)) return 1; /* Otherwise, +hoaq */ if (check_channel_access(client, channel, "hoaq")) return 1; } else if (user) { if (mode == MODE_SET) { if (user == client) return 1; } else if (mode == MODE_GET) { if(has_common_channels(user, client)) return 1; } } if (key) sendto_one(client, NULL, ":%s FAIL METADATA KEY_NO_PERMISSION %s %s :permission denied", me.name, user?user->name:channel->name, key); return 0; } /* METADATA [ ... []] */ CMD_FUNC(cmd_metadata_local) { Channel *channel = NULL; Client *user = NULL; const char *target; const char *cmd; const char *key; const char *value = NULL; int keyindex = 3-1; char *channame; MessageTag *batch_mtags = NULL; char batch[BATCHLEN+1] = ""; CHECKPARAMSCNT_OR_DIE(2, return); target = parv[1]; cmd = parv[2]; if (!strcasecmp(cmd, "GET")) { CHECKREGISTERED_OR_DIE(client, return); CHECKPARAMSCNT_OR_DIE(3, return); PROCESS_TARGET_OR_DIE(target, user, channel, return); MAKE_BATCH(client, batch, batch_mtags); FOR_EACH_KEY(keyindex, parc, parv) { if (metadata_check_perms(user, channel, client, key, MODE_GET)) { if (!metadata_key_valid(key)) { sendto_one(client, batch_mtags, ":%s FAIL METADATA KEY_INVALID %s :invalid key", me.name, key); continue; } if (channel) metadata_send_channel(channel, key, client, batch_mtags); else metadata_send_user(user, key, client, batch_mtags); } } FINISH_BATCH(client, batch, batch_mtags); } else if (!strcasecmp(cmd, "LIST")) { /* we're just not sending anything if there are no permissions */ CHECKREGISTERED_OR_DIE(client, return); PROCESS_TARGET_OR_DIE(target, user, channel, return); if (metadata_check_perms(user, channel, client, NULL, MODE_GET)) { if (channel) metadata_send_all_for_channel(channel, client); else metadata_send_all_for_user(user, client); } } else if (!strcasecmp(cmd, "SET")) { CHECKPARAMSCNT_OR_DIE(3, return); PROCESS_TARGET_OR_DIE(target, user, channel, return); key = parv[3]; if (!metadata_check_perms(user, channel, client, key, MODE_SET)) return; if (parc > 3 && !BadPtr(parv[4])) value = parv[4]; if (!metadata_key_valid(key)) { sendto_one(client, NULL, ":%s FAIL METADATA KEY_INVALID %s :invalid key", me.name, key); return; } if (channel) metadata_set_channel(channel, key, value, client); else metadata_set_user(user, key, value, client); } else if (!strcasecmp(cmd, "CLEAR")) { CHECKREGISTERED_OR_DIE(client, return); PROCESS_TARGET_OR_DIE(target, user, channel, return); if (metadata_check_perms(user, channel, client, "*", MODE_SET)) { if (channel) metadata_clear_channel(channel, client); else metadata_clear_user(user, client); } } else if (!strcasecmp(cmd, "SUB")) { PROCESS_TARGET_OR_DIE(target, user, channel, return); CHECKPARAMSCNT_OR_DIE(3, return); FOR_EACH_KEY(keyindex, parc, parv) { if(metadata_key_valid(key)) { metadata_subscribe(key, client, 0); } else { sendto_one(client, NULL, ":%s FAIL METADATA KEY_INVALID %s :invalid key", me.name, key); continue; } } } else if (!strcasecmp(cmd, "UNSUB")) { CHECKREGISTERED_OR_DIE(client, return); CHECKPARAMSCNT_OR_DIE(3, return); int subok = 0; FOR_EACH_KEY(keyindex, parc, parv) { if(metadata_key_valid(key)) { metadata_subscribe(key, client, 1); } else { sendto_one(client, NULL, ":%s FAIL METADATA KEY_INVALID %s :invalid key", me.name, key); continue; } } } else if (!strcasecmp(cmd, "SUBS")) { CHECKREGISTERED_OR_DIE(client, return); metadata_send_subscribtions(client); } else if (!strcasecmp(cmd, "SYNC")) { /* the client requested re-sending of all subbed metadata */ CHECKREGISTERED_OR_DIE(client, return); PROCESS_TARGET_OR_DIE(target, user, channel, return); if (channel) metadata_sync_channel(client, channel); else metadata_sync_user(client, user, NULL, 1); } else { sendto_one(client, NULL, ":%s FAIL METADATA SUBCOMMAND_INVALID %s :invalid subcommand", me.name, cmd); } } /* format of S2S is same as the event: ":origin METADATA *[ :]" */ CMD_FUNC(cmd_metadata_remote) { /* handling data from linked server */ Channel *channel = NULL; Client *user = NULL; const char *target; const char *key; const char *value; const char *channame; if (parc < 5 || BadPtr(parv[4])) { if (parc == 4 && !BadPtr(parv[3])) { value = NULL; } else { unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", client, "METADATA S2S: not enough args from $sender", log_data_string("sender", client->name)); return; } } else { value = parv[4]; } target = parv[1]; key = parv[2]; channame = strchr(target, '#'); if (!*target || !strcmp(target, "*") || !metadata_key_valid(key)) { unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", client, "METADATA S2S: bad metadata target $target or key $key from $sender", log_data_string("target", target), log_data_string("key", key), log_data_string("sender", client->name)); return; } PROCESS_TARGET_OR_DIE(target, user, channel, return); if(channel) { metadata_set_channel(channel, key, value, client); } else { metadata_set_user(user, key, value, client); } } CMD_FUNC(cmd_metadata) { if (client != &me && MyConnect(client) && !IsServer(client)) cmd_metadata_local(client, recv_mtags, parc, parv); else cmd_metadata_remote(client, recv_mtags, parc, parv); } int metadata_server_sync(Client *client) { /* we send all our data to the server that was just linked */ Client *acptr; struct metadata_moddata_user *moddata; struct metadata *metadata; unsigned int hashnum; Channel *channel; list_for_each_entry(acptr, &client_list, client_node) { /* send out users (all on our side of the link) */ moddata = USER_METADATA(acptr); if(!moddata) continue; for (metadata = moddata->metadata; metadata; metadata = metadata->next) metadata_send_change(client, NULL, acptr->name, metadata->name, metadata->value, &me); } for (hashnum = 0; hashnum < CHAN_HASH_TABLE_SIZE; hashnum++) { /* send out channels */ for(channel = hash_get_chan_bucket(hashnum); channel; channel = channel->hnextch) { for(metadata = CHANNEL_METADATA(channel); metadata; metadata = metadata->next) metadata_send_change(client, NULL, channel->name, metadata->name, metadata->value, &me); } } return 0; } int metadata_join(Client *client, Channel *channel, MessageTag *join_mtags) { Client *acptr; Member *cm; const char *value; struct metadata_unsynced *prev_us; struct metadata_unsynced *us; Membership *lp; struct metadata_subscriptions *subs; struct metadata *metadata; char batch[BATCHLEN+1] = ""; MessageTag *batch_mtags = NULL; struct metadata_moddata_user *moddata = USER_METADATA(client); if(!moddata) return 0; /* the user is both not subscribed to anything and has no own data */ for (metadata = moddata->metadata; metadata; metadata = metadata->next) { /* if joining user has metadata, let's notify all subscribers */ list_for_each_entry(acptr, &lclient_list, lclient_node) { if(IsMember(acptr, channel) && metadata_is_subscribed(acptr, metadata->name)) metadata_notify_or_queue(acptr, NULL, client->name, metadata->name, metadata->value, NULL); } } MAKE_BATCH(client, batch, batch_mtags); for (subs = moddata->subs; subs; subs = subs->next) { value = metadata_get_channel_key_value(channel, subs->name); /* notify joining user about channel metadata */ if(value) metadata_notify_or_queue(client, batch_mtags, channel->name, subs->name, value, NULL); for (cm = channel->members; cm; cm = cm->next) { /* notify joining user about other channel members' metadata */ acptr = cm->client; if (acptr == client) continue; /* ignore own data */ if (has_common_channels(acptr, client)) continue; /* already seen elsewhere */ value = metadata_get_user_key_value(acptr, subs->name); if (value) metadata_notify_or_queue(client, batch_mtags, acptr->name, subs->name, value, NULL); } } FINISH_BATCH(client, batch, batch_mtags); return 0; } void metadata_send_pending(Client *client) { Client *acptr = NULL; Channel *channel = NULL; int do_send = 0; char *who; char batch[BATCHLEN+1] = ""; MessageTag *mtags = NULL; struct metadata_moddata_user *my_moddata = USER_METADATA(client); if (!my_moddata) return; /* nothing queued */ struct metadata_unsynced *us = my_moddata->us; struct metadata_unsynced *prev_us; if (us) MAKE_BATCH(client, batch, mtags); while (us) { if (!IsSendable(client)) break; if (*us->id == '#') { channel = find_channel(us->id); if (channel && IsMember(client, channel)) { do_send = 1; who = us->id; } } else { acptr = find_client(us->id, NULL); if (acptr && has_common_channels(acptr, client)) { /* if not, the user has vanished since or one of us parted the channel */ do_send = 1; who = acptr->name; } } if (do_send) { struct metadata_moddata_user *moddata; if (acptr) moddata = USER_METADATA(acptr); else moddata = CHANNEL_METADATA(channel); if (moddata) { struct metadata *metadata = moddata->metadata; while (metadata) { if (!strcasecmp(us->key, metadata->name)) { /* has it */ const char *value = metadata_get_user_key_value(acptr, us->key); if(value) metadata_send_change(client, mtags, who, us->key, value, NULL); } metadata = metadata->next; } } } /* now remove the processed entry */ prev_us = us; us = us->next; safe_free(prev_us->id); safe_free(prev_us); my_moddata->us = us; /* we're always removing the first list item */ } FINISH_BATCH(client, batch, mtags); } int metadata_user_registered(Client *client) { /* if we have any metadata set at this point, let's broadcast it to other servers and users */ struct metadata *metadata; struct metadata_moddata_user *moddata = USER_METADATA(client); if(!moddata) return HOOK_CONTINUE; for (metadata = moddata->metadata; metadata; metadata = metadata->next) user_metadata_changed(client, metadata->name, metadata->value, client); return HOOK_CONTINUE; } void metadata_sync_user(Client *client, Client *target, MessageTag *mtags, int create_batch) { char batch[BATCHLEN+1] = ""; struct metadata_subscriptions *subs; struct metadata *metadata; int parent_mtags = 0; Client *acptr; if (mtags) parent_mtags = 1; struct metadata_moddata_user *moddata = USER_METADATA(target); if (!parent_mtags && create_batch) MAKE_BATCH(client, batch, mtags); if (moddata) { /* the user is either subscribed to something (this is not interesting to us) or has some own data */ for (metadata = moddata->metadata; metadata; metadata = metadata->next) { if(metadata_is_subscribed(client, metadata->name)) metadata_notify_or_queue(client, mtags, target->name, metadata->name, metadata->value, NULL); } } if (!parent_mtags) FINISH_BATCH(client, batch, mtags); } void metadata_sync_channel(Client *client, Channel *channel) { MessageTag *mtags = NULL; char batch[BATCHLEN+1] = ""; Member *cm; struct metadata_subscriptions *subs; const char *value; struct metadata_moddata_user *moddata = USER_METADATA(client); MAKE_BATCH(client, batch, mtags); if (moddata) { for (subs = moddata->subs; subs; subs = subs->next) { value = metadata_get_channel_key_value(channel, subs->name); /* channel metadata notification */ if(value) metadata_notify_or_queue(client, mtags, channel->name, subs->name, value, NULL); } for (cm = channel->members; cm; cm = cm->next) /* notify about all channel members' metadata (including the query source) */ metadata_sync_user(client, cm->client, mtags, 0); } FINISH_BATCH(client, batch, mtags); } EVENT(metadata_queue_evt) { /* let's check every 1.5 seconds whether we have something to send */ Client *acptr; list_for_each_entry(acptr, &lclient_list, lclient_node) { /* notifications for local subscribers */ if (!IsUser(acptr)) continue; metadata_send_pending(acptr); } } int metadata_monitor_connect(Client *client) { watch_check(client, WATCH_EVENT_ONLINE, metadata_monitor_notification); return 0; } int metadata_monitor_notification(Client *client, Watch *watch, Link *lp, int event) { if (!(lp->flags & WATCH_FLAG_TYPE_MONITOR)) return 0; if (!HasCapabilityFast(lp->value.client, CAP_METADATA_NOTIFY)) return 0; switch (event) { case WATCH_EVENT_ONLINE: case WATCH_EVENT_METADATA: metadata_sync_user(lp->value.client, client, NULL, 0); break; default: break; /* may be handled by other modules */ } return 0; }