From e3b931ef79b288a514c7dc7e8883fc726225f325 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 11:03:05 +0200 Subject: [PATCH 01/14] Copy metadata.c to metadata2.c --- files/metadata2.c | 1385 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1385 insertions(+) create mode 100644 files/metadata2.c diff --git a/files/metadata2.c b/files/metadata2.c new file mode 100644 index 0000000..9d71cab --- /dev/null +++ b/files/metadata2.c @@ -0,0 +1,1385 @@ +/* Copyright (C) All Rights Reserved +** Written by k4be +** Website: https://github.com/pirc-pl/unrealircd-modules/ +** License: GPLv3 https://www.gnu.org/licenses/gpl-3.0.html +*/ + +/*** <<>> +module +{ + documentation "https://github.com/pirc-pl/unrealircd-modules/blob/master/README.md#metadata"; + troubleshooting "In case of problems, contact k4be on irc.pirc.pl."; + min-unrealircd-version "6.*"; + post-install-text { + "The module is installed. Now all you need to do is add a loadmodule line:"; + "loadmodule \"third/metadata\";"; + "And /REHASH the IRCd."; + "The module may be additionaly configured to change the defaults."; + "See documentation for help."; + "Please note that the implemented feature is still \"Work In Progress\"."; + } +} +*** <<>> +*/ + +#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_METADATAEND 762 +#define ERR_METADATALIMIT 764 +#define ERR_TARGETINVALID 765 +#define ERR_NOMATCHINGKEY 766 +#define ERR_KEYINVALID 767 +#define ERR_KEYNOTSET 768 +#define ERR_KEYNOPERMISSION 769 +#define RPL_METADATASUBOK 770 +#define RPL_METADATAUNSUBOK 771 +#define RPL_METADATASUBS 772 +#define ERR_METADATATOOMANYSUBS 773 +#define ERR_METADATASYNCLATER 774 +#define ERR_METADATARATELIMIT 775 +#define ERR_METADATAINVALIDSUBCOMMAND 776 + +#define STR_RPL_WHOISKEYVALUE /* 760 */ "%s %s %s :%s" +#define STR_RPL_KEYVALUE /* 761 */ "%s %s %s :%s" +#define STR_RPL_METADATAEND /* 762 */ ":end of metadata" +#define STR_ERR_METADATALIMIT /* 764 */ "%s :metadata limit reached" +#define STR_ERR_TARGETINVALID /* 765 */ "%s :invalid metadata target" +#define STR_ERR_NOMATCHINGKEY /* 766 */ "%s %s :no matching key" +#define STR_ERR_KEYINVALID /* 767 */ ":%s" +#define STR_ERR_KEYNOTSET /* 768 */ "%s %s :key not set" +#define STR_ERR_KEYNOPERMISSION /* 769 */ "%s %s :permission denied" +#define STR_RPL_METADATASUBOK /* 770 */ ":%s" +#define STR_RPL_METADATAUNSUBOK /* 771 */ ":%s" +#define STR_RPL_METADATASUBS /* 772 */ ":%s" +#define STR_ERR_METADATATOOMANYSUBS /* 773 */ "%s" +#define STR_ERR_METADATASYNCLATER /* 774 */ "%s %s" +#define STR_ERR_METADATARATELIMIT /* 775 */ "%s %s %s :%s" +#define STR_ERR_METADATAINVALIDSUBCOMMAND /* 776 */ "%s :invalid metadata subcommand" + +/* 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 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 + +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 (nicknames) that should be synced but were not */ + char *name; + 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); +void metadata_send_user(Client *user, const char *key, Client *client); +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_sync(Client *client); +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, const char *who, const char *key, const char *value, Client *changer); +int metadata_notify_or_queue(Client *client, const char *who, const char *key, const char *value, Client *changer); + +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; +} metadata_settings; + +ModuleHeader MOD_HEADER = { + "third/metadata", + "6.0", + "draft/metadata and draft/metadata-notify-2 cap", + "k4be", + "unrealircd-6" +}; + +/* +metadata { + max-user-metadata 10; + max-channel-metadata 10; + max-subscriptions 10; +}; +*/ + +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; + } + + 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; + 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; + } + } + 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"; + cap.parameter = metadata_cap_param; + c = ClientCapabilityAdd(modinfo->handle, &cap, &CAP_METADATA); + + memset(&cap, 0, sizeof(cap)); + cap.name = "draft/metadata-notify-2"; /* for irccloud 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_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; + + 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[20]; + ircsnprintf(buf, sizeof(buf), "maxsub=%d", metadata_settings.max_subscriptions); + 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, 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, who, key, value, changer); + } else + { /* store for the SYNC */ + trylater = 1; + while (*us) + us = &(*us)->next; /* find last list element */ + *us = safe_alloc(sizeof(struct metadata_unsynced)); + (*us)->name = strdup(who); + (*us)->key = strdup(key); + (*us)->next = NULL; + } + return trylater; +} + +void metadata_send_change(Client *client, 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, NULL, ":%s!%s@%s METADATA %s %s %s", sender, changer->user->username, GetHost(changer), who, key, "*"); + else + sendto_one(client, NULL, ":%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, NULL, ":%s METADATA %s %s %s", sender, who, key, "*"); + else + sendto_one(client, NULL, ":%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, 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, user->name, key, value, changer); + } +} + +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, 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, 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->name); + 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; + + 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 (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) sendnumeric(client, ERR_KEYNOTSET, target_name, key); // not set so can't remove + return; + } + } else + { /* set */ + while (*metadata) + { + if (!strcasecmp(key, (*metadata)->name)) + { + set = 1; + if (strcmp(value, (*metadata)->value)) + { + safe_free((*metadata)->value); + (*metadata)->value = strdup(value); + 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 = strdup(value); + changed = 1; + } else + { /* no more allowed */ + if (client) + sendnumeric(client, ERR_METADATALIMIT, target_name); + return; + } + } + } + if (!IsServer(client) && MyConnect(client)) + sendnumeric(client, RPL_KEYVALUE, 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; + struct metadata **metadata = (struct metadata **)&CHANNEL_METADATA(channel); + struct metadata *prev; + + 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) + sendnumeric(client, ERR_KEYNOTSET, channel->name, key); /* not set so can't remove */ + return; + } + } else { /* set */ + while (*metadata) + { + if (!strcasecmp(key, (*metadata)->name)) + { + set = 1; + if (strcmp(value, (*metadata)->value)) + { + safe_free((*metadata)->value); + (*metadata)->value = strdup(value); + 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 = strdup(value); + changed = 1; + } else + { /* no more allowed */ + if (client) + sendnumeric(client, ERR_METADATALIMIT, channel->name); + return; + } + } + } + if (IsUser(client) && MyUser(client)) + 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; + 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 */ + sendnumeric(client, ERR_METADATATOOMANYSUBS, key); + return 0; + } + } + if (!remove) + { + sendnumeric(client, 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, 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, channel->name, key, value, NULL); + } + } + } + if (trylater) + return 1; + } else + { + sendnumeric(client, RPL_METADATAUNSUBOK, key); + } + return 0; +} + +void metadata_send_channel(Channel *channel, const char *key, Client *client) +{ + struct metadata *metadata; + int found = 0; + for (metadata = CHANNEL_METADATA(channel); metadata; metadata = metadata->next) + { + if (!strcasecmp(key, metadata->name)) + { + found = 1; + sendnumeric(client, RPL_KEYVALUE, channel->name, key, "*", metadata->value); + break; + } + } + if (!found) + sendnumeric(client, ERR_NOMATCHINGKEY, channel->name, key); +} + +void metadata_send_user(Client *user, const char *key, Client *client) +{ + if (!user) + user = client; + struct metadata_moddata_user *moddata = USER_METADATA(user); + struct metadata *metadata = NULL; + if (moddata) + metadata = moddata->metadata; + int found = 0; + for ( ; metadata; metadata = metadata->next) + { + if (!strcasecmp(key, metadata->name)) + { + found = 1; + sendnumeric(client, RPL_KEYVALUE, user->name, key, "*", metadata->value); + break; + } + } + if (!found) + sendnumeric(client, ERR_NOMATCHINGKEY, user->name, key); +} + +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); + if (!moddata) + return; + for (subs = moddata->subs; subs; subs = subs->next) + sendnumeric(client, RPL_METADATASUBS, subs->name); +} + +void metadata_send_all_for_channel(Channel *channel, Client *client) +{ + struct metadata *metadata; + for (metadata = CHANNEL_METADATA(channel); metadata; metadata = metadata->next) + sendnumeric(client, RPL_KEYVALUE, channel->name, metadata->name, "*", metadata->value); +} + +void metadata_send_all_for_user(Client *user, Client *client) +{ + struct metadata *metadata; + if (!user) + user = client; + struct metadata_moddata_user *moddata = USER_METADATA(user); + if (!moddata) + return; + for (metadata = moddata->metadata; metadata; metadata = metadata->next) + sendnumeric(client, RPL_KEYVALUE, user->name, metadata->name, "*", metadata->value); +} + +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) + sendnumeric(client, ERR_KEYNOPERMISSION, 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; + + 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); + FOR_EACH_KEY(keyindex, parc, parv) + { + if (metadata_check_perms(user, channel, client, key, MODE_GET)) + { + if (!metadata_key_valid(key)) + { + sendnumeric(client, ERR_KEYINVALID, key); + continue; + } + if (channel) + metadata_send_channel(channel, key, client); + else + metadata_send_user(user, key, client); + } + } + } 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); + } + sendnumeric(client, RPL_METADATAEND); + } 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)) + { + sendnumeric(client, ERR_KEYINVALID, 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); + } + sendnumeric(client, RPL_METADATAEND); + } 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 + { + sendnumeric(client, ERR_KEYINVALID, key); + continue; + } + } + sendnumeric(client, RPL_METADATAEND); + } 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 + { + sendnumeric(client, ERR_KEYINVALID, key); + continue; + } + } + sendnumeric(client, RPL_METADATAEND); + } else if (!strcasecmp(cmd, "SUBS")) + { + CHECKREGISTERED_OR_DIE(client, return); + metadata_send_subscribtions(client); + sendnumeric(client, RPL_METADATAEND); + } else if (!strcasecmp(cmd, "SYNC")) + { /* the SYNC command is ignored, as we're using events to send out the queue - only validate the params */ + CHECKREGISTERED_OR_DIE(client, return); + PROCESS_TARGET_OR_DIE(target, user, channel, return); + } else + { + sendnumeric(client, ERR_METADATAINVALIDSUBCOMMAND, 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, 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, channel->name, metadata->name, metadata->value, &me); + } + } + return 0; +} + +int metadata_join(Client *client, Channel *channel, MessageTag *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; + + 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, client->name, metadata->name, metadata->value, NULL); + } + } + 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, channel->name, subs->name, value, NULL); + for (cm = channel->members; cm; cm = cm->next) + { /* notify joining user about other channel members' metadata, TODO check if we already see this user elsewhere */ + acptr = cm->client; + if (acptr == client) + continue; /* ignore own data */ + value = metadata_get_user_key_value(acptr, subs->name); + if (value) + metadata_notify_or_queue(client, acptr->name, subs->name, value, NULL); + } + } + return 0; +} + +void metadata_sync(Client *client) +{ + Client *acptr; + Channel *channel = 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; + + while (us) + { + if (!IsSendable(client)) + break; + acptr = hash_find_nickatserver(us->name, NULL); + if (acptr && has_common_channels(acptr, client)) + { /* if not, the user has vanished since or one of us parted the channel */ + struct metadata_moddata_user *moddata = USER_METADATA(acptr); + 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, us->name, us->key, value, NULL); + } + metadata = metadata->next; + } + } + } + /* now remove the processed entry */ + prev_us = us; + us = us->next; + safe_free(prev_us->name); + safe_free(prev_us); + my_moddata->us = us; /* we're always removing the first list item */ + } +} + +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; +} + +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_sync(acptr); + } +} + From 6b901ddcdae49f014d2c1dd873fddccef2f33bdc Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 11:03:20 +0200 Subject: [PATCH 02/14] metadata2: Update metadata (hah) and cap name --- files/metadata2.c | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/files/metadata2.c b/files/metadata2.c index 9d71cab..605a3ea 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -1,18 +1,17 @@ -/* Copyright (C) All Rights Reserved -** Written by k4be -** Website: https://github.com/pirc-pl/unrealircd-modules/ +/* Copyright (C) 2020-2021 k4be +** Copyright (C) 2023 Valentin Lorentz ** License: GPLv3 https://www.gnu.org/licenses/gpl-3.0.html */ /*** <<>> module { - documentation "https://github.com/pirc-pl/unrealircd-modules/blob/master/README.md#metadata"; - troubleshooting "In case of problems, contact k4be on irc.pirc.pl."; + documentation "Implements the draft IRCv3 metadata-2 specification https://github.com/ircv3/ircv3-specifications/pull/501" + troubleshooting "In case of problems, contact val on irc.unrealircd.org."; min-unrealircd-version "6.*"; post-install-text { "The module is installed. Now all you need to do is add a loadmodule line:"; - "loadmodule \"third/metadata\";"; + "loadmodule \"third/metadata2\";"; "And /REHASH the IRCd."; "The module may be additionaly configured to change the defaults."; "See documentation for help."; @@ -69,7 +68,7 @@ module #define MODE_SET 0 #define MODE_GET 1 -#define MYCONF "metadata" +#define MYCONF "metadata2" #define CHECKPARAMSCNT_OR_DIE(count, return) \ { \ @@ -198,10 +197,10 @@ struct metadata_settings_s { } metadata_settings; ModuleHeader MOD_HEADER = { - "third/metadata", + "third/metadata2", "6.0", - "draft/metadata and draft/metadata-notify-2 cap", - "k4be", + "draft/metadata2 and draft/metadata-notify-2 cap", + "k4be & val", "unrealircd-6" }; @@ -369,7 +368,7 @@ MOD_INIT() { MARK_AS_GLOBAL_MODULE(modinfo); memset(&cap, 0, sizeof(cap)); - cap.name = "draft/metadata"; + cap.name = "draft/metadata-2"; cap.parameter = metadata_cap_param; c = ClientCapabilityAdd(modinfo->handle, &cap, &CAP_METADATA); @@ -501,17 +500,17 @@ int metadata_notify_or_queue(Client *client, const char *who, const char *key, c int trylater = 0; if (!who) { - unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", changer, "metadata_notify_or_queue called with null who!"); + unreal_log(ULOG_DEBUG, "metadata2", "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!"); + unreal_log(ULOG_DEBUG, "metadata2", "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!"); + unreal_log(ULOG_DEBUG, "metadata2", "METADATA_DEBUG", changer, "metadata_notify_or_queue called with null client!"); return 0; } @@ -541,17 +540,17 @@ void metadata_send_change(Client *client, const char *who, const char *key, cons char *sender = NULL; if (!key) { - unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", changer, "metadata_send_change called with null key!"); + unreal_log(ULOG_DEBUG, "metadata2", "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!"); + unreal_log(ULOG_DEBUG, "metadata2", "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!"); + unreal_log(ULOG_DEBUG, "metadata2", "METADATA_DEBUG", changer, "metadata_send_change called with null client!"); return; } if (changer) @@ -1211,7 +1210,7 @@ CMD_FUNC(cmd_metadata_remote) value = NULL; } else { - unreal_log(ULOG_DEBUG, "metadata", "METADATA_DEBUG", client, "METADATA S2S: not enough args from $sender", + unreal_log(ULOG_DEBUG, "metadata2", "METADATA_DEBUG", client, "METADATA S2S: not enough args from $sender", log_data_string("sender", client->name)); return; } @@ -1226,7 +1225,7 @@ CMD_FUNC(cmd_metadata_remote) 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", + unreal_log(ULOG_DEBUG, "metadata2", "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)); From 37cb5fb6c52b1b5b72df8bdabf169bd6189e902b Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 11:42:21 +0200 Subject: [PATCH 03/14] switch to stdreplies --- files/metadata2.c | 82 +++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/files/metadata2.c b/files/metadata2.c index 605a3ea..f909330 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -25,44 +25,34 @@ module /* this should go into include/numeric.h (was there at one point of time) */ -#define RPL_WHOISKEYVALUE 760 -#define RPL_KEYVALUE 761 -#define RPL_METADATAEND 762 -#define ERR_METADATALIMIT 764 -#define ERR_TARGETINVALID 765 -#define ERR_NOMATCHINGKEY 766 -#define ERR_KEYINVALID 767 -#define ERR_KEYNOTSET 768 -#define ERR_KEYNOPERMISSION 769 -#define RPL_METADATASUBOK 770 -#define RPL_METADATAUNSUBOK 771 -#define RPL_METADATASUBS 772 -#define ERR_METADATATOOMANYSUBS 773 -#define ERR_METADATASYNCLATER 774 -#define ERR_METADATARATELIMIT 775 -#define ERR_METADATAINVALIDSUBCOMMAND 776 +#define RPL_WHOISKEYVALUE 760 +#define RPL_KEYVALUE 761 +#define RPL_METADATAEND 762 +#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_METADATAEND /* 762 */ ":end of metadata" -#define STR_ERR_METADATALIMIT /* 764 */ "%s :metadata limit reached" -#define STR_ERR_TARGETINVALID /* 765 */ "%s :invalid metadata target" -#define STR_ERR_NOMATCHINGKEY /* 766 */ "%s %s :no matching key" -#define STR_ERR_KEYINVALID /* 767 */ ":%s" -#define STR_ERR_KEYNOTSET /* 768 */ "%s %s :key not set" -#define STR_ERR_KEYNOPERMISSION /* 769 */ "%s %s :permission denied" +#define STR_RPL_KEYVALUE /* 761 */ "%s %s %s :%s" +#define STR_RPL_METADATAEND /* 762 */ ":end of metadata" +#define STR_RPL_KEYNOTSET /* 766 */ "%s %s :no matching key" #define STR_RPL_METADATASUBOK /* 770 */ ":%s" #define STR_RPL_METADATAUNSUBOK /* 771 */ ":%s" #define STR_RPL_METADATASUBS /* 772 */ ":%s" -#define STR_ERR_METADATATOOMANYSUBS /* 773 */ "%s" -#define STR_ERR_METADATASYNCLATER /* 774 */ "%s %s" -#define STR_ERR_METADATARATELIMIT /* 775 */ "%s %s %s :%s" -#define STR_ERR_METADATAINVALIDSUBCOMMAND /* 776 */ "%s :invalid metadata subcommand" +#define STR_RPL_METADATASYNCLATER /* 774 */ "%s %s" -/* actual METADATA code */ +#define STR_FAIL_INVALID_TARGET ":%s FAIL METADATA INVALID_TARGET %s :invalid metadata target" +#define STR_FAIL_INVALID_KEY ":%s FAIL METADATA INVALID_KEY %s :invalid key" +#define STR_FAIL_INVALID_SUBCOMMAND ":%s FAIL METADATA INVALID_SUBCOMMAND %s :invalid metadata subcommand" +#define STR_FAIL_KEY_NO_PERMISSION ":%s FAIL METADATA KEY_NO_PERMISSION %s %s :permission denied" +#define STR_FAIL_KEY_NOT_SET ":%s FAIL METADATA KEY_NOT_SET %s :key not set" +#define STR_FAIL_LIMIT_REACHED ":%s FAIL METADATA LIMIT_REACHED %s :metadata limit reached" +#define STR_FAIL_RATE_LIMITED ":%s FAIL METADATA RATE_LIMITED %s :rate limited" +#define STR_FAIL_TOO_MANY_SUBS ":%s FAIL METADATA TOO_MANY_SUBS %s :too many subscriptions" -#define STR_HELPER(x) #x -#define STR(x) STR_HELPER(x) +/* actual METADATA code */ /* get or set for perms */ #define MODE_SET 0 @@ -90,7 +80,7 @@ module channel = find_channel(channame); \ if (!channel) \ { \ - sendnumeric(client, ERR_NOSUCHNICK, channame); \ + sendto_one(client, NULL, STR_FAIL_INVALID_TARGET, me.name, channame); \ return; \ } \ } else \ @@ -100,7 +90,7 @@ module user = hash_find_nickatserver(target, NULL); \ if (!user) \ { \ - sendnumeric(client, ERR_NOSUCHNICK, target); \ + sendto_one(client, NULL, STR_FAIL_INVALID_TARGET, me.name, target); \ return; \ } \ } else \ @@ -728,7 +718,7 @@ void metadata_set_user(Client *user, const char *key, const char *value, Client } if (!removed) { - if(client) sendnumeric(client, ERR_KEYNOTSET, target_name, key); // not set so can't remove + if(client) sendnumeric(client, RPL_KEYNOTSET, target_name, key); // not set so can't remove return; } } else @@ -760,7 +750,7 @@ void metadata_set_user(Client *user, const char *key, const char *value, Client } else { /* no more allowed */ if (client) - sendnumeric(client, ERR_METADATALIMIT, target_name); + sendto_one(client, NULL, STR_FAIL_LIMIT_REACHED, me.name, target_name); return; } } @@ -800,7 +790,7 @@ void metadata_set_channel(Channel *channel, const char *key, const char *value, if (!removed) { if (client) - sendnumeric(client, ERR_KEYNOTSET, channel->name, key); /* not set so can't remove */ + sendto_one(client, NULL, STR_FAIL_KEY_NOT_SET, me.name, key); return; } } else { /* set */ @@ -831,7 +821,7 @@ void metadata_set_channel(Channel *channel, const char *key, const char *value, } else { /* no more allowed */ if (client) - sendnumeric(client, ERR_METADATALIMIT, channel->name); + sendto_one(client, NULL, STR_FAIL_LIMIT_REACHED, me.name, channel->name); return; } } @@ -885,7 +875,7 @@ int metadata_subscribe(const char *key, Client *client, int remove) (*subs)->name = strdup(key); } else { /* no more allowed */ - sendnumeric(client, ERR_METADATATOOMANYSUBS, key); + sendto_one(client, NULL, STR_FAIL_TOO_MANY_SUBS, me.name, key); return 0; } } @@ -939,7 +929,7 @@ void metadata_send_channel(Channel *channel, const char *key, Client *client) } } if (!found) - sendnumeric(client, ERR_NOMATCHINGKEY, channel->name, key); + sendnumeric(client, RPL_KEYNOTSET, channel->name, key); } void metadata_send_user(Client *user, const char *key, Client *client) @@ -961,7 +951,7 @@ void metadata_send_user(Client *user, const char *key, Client *client) } } if (!found) - sendnumeric(client, ERR_NOMATCHINGKEY, user->name, key); + sendnumeric(client, RPL_KEYNOTSET, user->name, key); } void metadata_clear_channel(Channel *channel, Client *client) @@ -1060,7 +1050,7 @@ int metadata_check_perms(Client *user, Channel *channel, Client *client, const c } if (key) - sendnumeric(client, ERR_KEYNOPERMISSION, user?user->name:channel->name, key); + sendto_one(client, NULL, STR_FAIL_KEY_NO_PERMISSION, me.name, user?user->name:channel->name, key); return 0; } @@ -1092,7 +1082,7 @@ CMD_FUNC(cmd_metadata_local) { if (!metadata_key_valid(key)) { - sendnumeric(client, ERR_KEYINVALID, key); + sendto_one(client, NULL, STR_FAIL_INVALID_KEY, me.name, key); continue; } if (channel) @@ -1125,7 +1115,7 @@ CMD_FUNC(cmd_metadata_local) if (!metadata_key_valid(key)) { - sendnumeric(client, ERR_KEYINVALID, key); + sendto_one(client, NULL, STR_FAIL_INVALID_KEY, me.name, key); return; } @@ -1156,7 +1146,7 @@ CMD_FUNC(cmd_metadata_local) metadata_subscribe(key, client, 0); } else { - sendnumeric(client, ERR_KEYINVALID, key); + sendto_one(client, NULL, STR_FAIL_INVALID_KEY, me.name, key); continue; } } @@ -1173,7 +1163,7 @@ CMD_FUNC(cmd_metadata_local) metadata_subscribe(key, client, 1); } else { - sendnumeric(client, ERR_KEYINVALID, key); + sendto_one(client, NULL, STR_FAIL_INVALID_KEY, me.name, key); continue; } } @@ -1189,7 +1179,7 @@ CMD_FUNC(cmd_metadata_local) PROCESS_TARGET_OR_DIE(target, user, channel, return); } else { - sendnumeric(client, ERR_METADATAINVALIDSUBCOMMAND, cmd); + sendto_one(client, NULL, STR_FAIL_INVALID_SUBCOMMAND, me.name, cmd); } } From fba338eb45e0eb07d0ecfb9d501ecacb402e9a96 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 15:42:43 +0200 Subject: [PATCH 04/14] Use 'metadata' batch instead of RPL_METADATAEND --- files/metadata2.c | 86 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/files/metadata2.c b/files/metadata2.c index f909330..393bcf5 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -27,7 +27,6 @@ module #define RPL_WHOISKEYVALUE 760 #define RPL_KEYVALUE 761 -#define RPL_METADATAEND 762 #define RPL_KEYNOTSET 766 #define RPL_METADATASUBOK 770 #define RPL_METADATAUNSUBOK 771 @@ -36,7 +35,6 @@ module #define STR_RPL_WHOISKEYVALUE /* 760 */ "%s %s %s :%s" #define STR_RPL_KEYVALUE /* 761 */ "%s %s %s :%s" -#define STR_RPL_METADATAEND /* 762 */ ":end of metadata" #define STR_RPL_KEYNOTSET /* 766 */ "%s %s :no matching key" #define STR_RPL_METADATASUBOK /* 770 */ ":%s" #define STR_RPL_METADATAUNSUBOK /* 771 */ ":%s" @@ -113,6 +111,36 @@ module #define USER_METADATA(client) moddata_client(client, metadataUser).ptr #define CHANNEL_METADATA(channel) moddata_channel(channel, metadataChannel).ptr + +#if defined(__GNUC__) +#define PUSH_IGNORE_ADDRESS \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Waddress\"") +#define POP_IGNORE_ADDRESS \ + _Pragma("GCC diagnostic pop") +#else +#define PUSH_IGNORE_ADDRESS +#define POP_IGNORE_ADDRESS +#endif + +#define batched(func, client, batch_id, ...) \ +{ \ + MessageTag *mtags = NULL; \ + MessageTag *m; \ + PUSH_IGNORE_ADDRESS /* -Waddress warns when batch_id is an array */ \ + if (!BadPtr(batch_id)) \ + POP_IGNORE_ADDRESS \ + { \ + mtags = safe_alloc(sizeof(MessageTag)); \ + mtags->name = strdup("batch"); \ + mtags->value = strdup(batchid); \ + } \ + func(client, mtags, ##__VA_ARGS__); \ + if (mtags) \ + free_message_tags(mtags); \ +} + + struct metadata { char *name; char *value; @@ -161,8 +189,8 @@ void metadata_free_list(struct metadata *metadata, const char *whose, Client *cl 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); -void metadata_send_user(Client *user, const char *key, Client *client); +void metadata_send_channel(Channel *channel, const char *key, Client *client, const char *batchid); +void metadata_send_user(Client *user, const char *key, Client *client, const char *batchid); 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); @@ -915,7 +943,7 @@ int metadata_subscribe(const char *key, Client *client, int remove) return 0; } -void metadata_send_channel(Channel *channel, const char *key, Client *client) +void metadata_send_channel(Channel *channel, const char *key, Client *client, const char *batchid) { struct metadata *metadata; int found = 0; @@ -924,15 +952,15 @@ void metadata_send_channel(Channel *channel, const char *key, Client *client) if (!strcasecmp(key, metadata->name)) { found = 1; - sendnumeric(client, RPL_KEYVALUE, channel->name, key, "*", metadata->value); + batched(sendtaggednumeric, client, batchid, RPL_KEYVALUE, channel->name, key, "*", metadata->value); break; } } if (!found) - sendnumeric(client, RPL_KEYNOTSET, channel->name, key); + batched(sendtaggednumeric, client, batchid, RPL_KEYNOTSET, channel->name, key); } -void metadata_send_user(Client *user, const char *key, Client *client) +void metadata_send_user(Client *user, const char *key, Client *client, const char *batchid) { if (!user) user = client; @@ -946,12 +974,12 @@ void metadata_send_user(Client *user, const char *key, Client *client) if (!strcasecmp(key, metadata->name)) { found = 1; - sendnumeric(client, RPL_KEYVALUE, user->name, key, "*", metadata->value); + batched(sendtaggednumeric, client, batchid, RPL_KEYVALUE, user->name, key, "*", metadata->value); break; } } if (!found) - sendnumeric(client, RPL_KEYNOTSET, user->name, key); + batched(sendtaggednumeric, client, batchid, RPL_KEYNOTSET, user->name, key); } void metadata_clear_channel(Channel *channel, Client *client) @@ -987,20 +1015,33 @@ void metadata_send_subscribtions(Client *client) void metadata_send_all_for_channel(Channel *channel, Client *client) { struct metadata *metadata; + char batchid[BATCHLEN+1]; + + generate_batch_id(batchid); + + sendto_one(client, NULL, ":%s BATCH +%s metadata", me.name, batchid); for (metadata = CHANNEL_METADATA(channel); metadata; metadata = metadata->next) - sendnumeric(client, RPL_KEYVALUE, channel->name, metadata->name, "*", metadata->value); + batched(sendtaggednumeric, client, batchid, RPL_KEYVALUE, channel->name, metadata->name, "*", metadata->value); + sendto_one(client, NULL, ":%s BATCH -%s", me.name, batchid); } void metadata_send_all_for_user(Client *user, Client *client) { struct metadata *metadata; + char batchid[BATCHLEN+1]; + + generate_batch_id(batchid); + if (!user) user = client; struct metadata_moddata_user *moddata = USER_METADATA(user); - if (!moddata) - return; - for (metadata = moddata->metadata; metadata; metadata = metadata->next) - sendnumeric(client, RPL_KEYVALUE, user->name, metadata->name, "*", metadata->value); + + sendto_one(client, NULL, ":%s BATCH +%s metadata", me.name, batchid); + if (moddata) { + for (metadata = moddata->metadata; metadata; metadata = metadata->next) + batched(sendtaggednumeric, client, batchid, RPL_KEYVALUE, user->name, metadata->name, "*", metadata->value); + } + sendto_one(client, NULL, ":%s BATCH -%s", me.name, batchid); } int metadata_key_valid(const char *key) @@ -1065,6 +1106,7 @@ CMD_FUNC(cmd_metadata_local) const char *value = NULL; int keyindex = 3-1; char *channame; + char batchid[BATCHLEN-1]; CHECKPARAMSCNT_OR_DIE(2, return); @@ -1076,21 +1118,24 @@ CMD_FUNC(cmd_metadata_local) CHECKREGISTERED_OR_DIE(client, return); CHECKPARAMSCNT_OR_DIE(3, return); PROCESS_TARGET_OR_DIE(target, user, channel, return); + generate_batch_id(batchid); + sendto_one(client, NULL, ":%s BATCH +%s metadata", me.name, batchid); FOR_EACH_KEY(keyindex, parc, parv) { if (metadata_check_perms(user, channel, client, key, MODE_GET)) { if (!metadata_key_valid(key)) { - sendto_one(client, NULL, STR_FAIL_INVALID_KEY, me.name, key); + batched(sendto_one, client, batchid, STR_FAIL_INVALID_KEY, me.name, key); continue; } if (channel) - metadata_send_channel(channel, key, client); + metadata_send_channel(channel, key, client, batchid); else - metadata_send_user(user, key, client); + metadata_send_user(user, key, client, batchid); } } + sendto_one(client, NULL, ":%s BATCH -%s", me.name, batchid); } else if (!strcasecmp(cmd, "LIST")) { /* we're just not sending anything if there are no permissions */ CHECKREGISTERED_OR_DIE(client, return); @@ -1102,7 +1147,6 @@ CMD_FUNC(cmd_metadata_local) else metadata_send_all_for_user(user, client); } - sendnumeric(client, RPL_METADATAEND); } else if (!strcasecmp(cmd, "SET")) { CHECKPARAMSCNT_OR_DIE(3, return); @@ -1134,7 +1178,6 @@ CMD_FUNC(cmd_metadata_local) else metadata_clear_user(user, client); } - sendnumeric(client, RPL_METADATAEND); } else if (!strcasecmp(cmd, "SUB")) { PROCESS_TARGET_OR_DIE(target, user, channel, return); @@ -1150,7 +1193,6 @@ CMD_FUNC(cmd_metadata_local) continue; } } - sendnumeric(client, RPL_METADATAEND); } else if (!strcasecmp(cmd, "UNSUB")) { CHECKREGISTERED_OR_DIE(client, return); @@ -1167,12 +1209,10 @@ CMD_FUNC(cmd_metadata_local) continue; } } - sendnumeric(client, RPL_METADATAEND); } else if (!strcasecmp(cmd, "SUBS")) { CHECKREGISTERED_OR_DIE(client, return); metadata_send_subscribtions(client); - sendnumeric(client, RPL_METADATAEND); } else if (!strcasecmp(cmd, "SYNC")) { /* the SYNC command is ignored, as we're using events to send out the queue - only validate the params */ CHECKREGISTERED_OR_DIE(client, return); From a1d68bb88862212e6349d23fa8fae81853620ed9 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 17:40:54 +0200 Subject: [PATCH 05/14] Validate values --- files/metadata2.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/files/metadata2.c b/files/metadata2.c index 393bcf5..1003ddc 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -49,6 +49,8 @@ module #define STR_FAIL_LIMIT_REACHED ":%s FAIL METADATA LIMIT_REACHED %s :metadata limit reached" #define STR_FAIL_RATE_LIMITED ":%s FAIL METADATA RATE_LIMITED %s :rate limited" #define STR_FAIL_TOO_MANY_SUBS ":%s FAIL METADATA TOO_MANY_SUBS %s :too many subscriptions" +#define STR_FAIL_INVALID_VALUE_UTF8 ":%s FAIL METADATA INVALID_VALUE :value contains invalid UTF8" +#define STR_FAIL_INVALID_VALUE_SIZE ":%s FAIL METADATA INVALID_VALUE :value is too long" /* actual METADATA code */ @@ -56,6 +58,10 @@ module #define MODE_SET 0 #define MODE_GET 1 +/* TODO: pick a less arbitrary value; to allow it to be as large as possible without + * overflowing IRC line length */ +#define MAX_VALUE_BYTES 300 + #define MYCONF "metadata2" #define CHECKPARAMSCNT_OR_DIE(count, return) \ @@ -449,7 +455,7 @@ MOD_UNLOAD() { const char *metadata_cap_param(Client *client) { static char buf[20]; - ircsnprintf(buf, sizeof(buf), "maxsub=%d", metadata_settings.max_subscriptions); + ircsnprintf(buf, sizeof(buf), "max-sub=%d,max-value-bytes=%d", metadata_settings.max_subscriptions, MAX_VALUE_BYTES); return buf; } @@ -1163,6 +1169,15 @@ CMD_FUNC(cmd_metadata_local) return; } + if (!unrl_utf8_validate(value, NULL)) { + sendto_one(client, NULL, STR_FAIL_INVALID_VALUE_UTF8, me.name); + return; + } + if (strlen(value) > MAX_VALUE_BYTES) { + sendto_one(client, NULL, STR_FAIL_INVALID_VALUE_SIZE, me.name); + return; + } + if (channel) metadata_set_channel(channel, key, value, client); else From 6e10fa76eb895a6d9adf020f012468d91c91860a Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 18:55:43 +0200 Subject: [PATCH 06/14] fix fail code names to be consistent with the fixed spec --- files/metadata2.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/files/metadata2.c b/files/metadata2.c index 1003ddc..36cd6bc 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -42,15 +42,15 @@ module #define STR_RPL_METADATASYNCLATER /* 774 */ "%s %s" #define STR_FAIL_INVALID_TARGET ":%s FAIL METADATA INVALID_TARGET %s :invalid metadata target" -#define STR_FAIL_INVALID_KEY ":%s FAIL METADATA INVALID_KEY %s :invalid key" -#define STR_FAIL_INVALID_SUBCOMMAND ":%s FAIL METADATA INVALID_SUBCOMMAND %s :invalid metadata subcommand" +#define STR_FAIL_KEY_INVALID ":%s FAIL METADATA KEY_INVALID %s :invalid key" +#define STR_FAIL_SUBCOMMAND_INVALID ":%s FAIL METADATA SUBCOMMAND_INVALID %s :invalid metadata subcommand" #define STR_FAIL_KEY_NO_PERMISSION ":%s FAIL METADATA KEY_NO_PERMISSION %s %s :permission denied" #define STR_FAIL_KEY_NOT_SET ":%s FAIL METADATA KEY_NOT_SET %s :key not set" #define STR_FAIL_LIMIT_REACHED ":%s FAIL METADATA LIMIT_REACHED %s :metadata limit reached" #define STR_FAIL_RATE_LIMITED ":%s FAIL METADATA RATE_LIMITED %s :rate limited" #define STR_FAIL_TOO_MANY_SUBS ":%s FAIL METADATA TOO_MANY_SUBS %s :too many subscriptions" -#define STR_FAIL_INVALID_VALUE_UTF8 ":%s FAIL METADATA INVALID_VALUE :value contains invalid UTF8" -#define STR_FAIL_INVALID_VALUE_SIZE ":%s FAIL METADATA INVALID_VALUE :value is too long" +#define STR_FAIL_VALUE_INVALID_UTF8 ":%s FAIL METADATA VALUE_INVALID :value contains invalid UTF8" +#define STR_FAIL_VALUE_INVALID_SIZE ":%s FAIL METADATA VALUE_INVALID :value is too long" /* actual METADATA code */ @@ -1132,7 +1132,7 @@ CMD_FUNC(cmd_metadata_local) { if (!metadata_key_valid(key)) { - batched(sendto_one, client, batchid, STR_FAIL_INVALID_KEY, me.name, key); + batched(sendto_one, client, batchid, STR_FAIL_KEY_INVALID, me.name, key); continue; } if (channel) @@ -1165,16 +1165,16 @@ CMD_FUNC(cmd_metadata_local) if (!metadata_key_valid(key)) { - sendto_one(client, NULL, STR_FAIL_INVALID_KEY, me.name, key); + sendto_one(client, NULL, STR_FAIL_KEY_INVALID, me.name, key); return; } if (!unrl_utf8_validate(value, NULL)) { - sendto_one(client, NULL, STR_FAIL_INVALID_VALUE_UTF8, me.name); + sendto_one(client, NULL, STR_FAIL_VALUE_INVALID_UTF8, me.name); return; } if (strlen(value) > MAX_VALUE_BYTES) { - sendto_one(client, NULL, STR_FAIL_INVALID_VALUE_SIZE, me.name); + sendto_one(client, NULL, STR_FAIL_VALUE_INVALID_SIZE, me.name); return; } @@ -1204,7 +1204,7 @@ CMD_FUNC(cmd_metadata_local) metadata_subscribe(key, client, 0); } else { - sendto_one(client, NULL, STR_FAIL_INVALID_KEY, me.name, key); + sendto_one(client, NULL, STR_FAIL_KEY_INVALID, me.name, key); continue; } } @@ -1220,7 +1220,7 @@ CMD_FUNC(cmd_metadata_local) metadata_subscribe(key, client, 1); } else { - sendto_one(client, NULL, STR_FAIL_INVALID_KEY, me.name, key); + sendto_one(client, NULL, STR_FAIL_KEY_INVALID, me.name, key); continue; } } @@ -1234,7 +1234,7 @@ CMD_FUNC(cmd_metadata_local) PROCESS_TARGET_OR_DIE(target, user, channel, return); } else { - sendto_one(client, NULL, STR_FAIL_INVALID_SUBCOMMAND, me.name, cmd); + sendto_one(client, NULL, STR_FAIL_SUBCOMMAND_INVALID, me.name, cmd); } } From a203965ee8b5b5edca6620690e42d7929bb56fca Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 20:39:50 +0200 Subject: [PATCH 07/14] Add missing params to RATE_LIMITED template --- files/metadata2.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/metadata2.c b/files/metadata2.c index 36cd6bc..3a10320 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -47,7 +47,7 @@ module #define STR_FAIL_KEY_NO_PERMISSION ":%s FAIL METADATA KEY_NO_PERMISSION %s %s :permission denied" #define STR_FAIL_KEY_NOT_SET ":%s FAIL METADATA KEY_NOT_SET %s :key not set" #define STR_FAIL_LIMIT_REACHED ":%s FAIL METADATA LIMIT_REACHED %s :metadata limit reached" -#define STR_FAIL_RATE_LIMITED ":%s FAIL METADATA RATE_LIMITED %s :rate limited" +#define STR_FAIL_RATE_LIMITED ":%s FAIL METADATA RATE_LIMITED %s %s %s :rate limited" #define STR_FAIL_TOO_MANY_SUBS ":%s FAIL METADATA TOO_MANY_SUBS %s :too many subscriptions" #define STR_FAIL_VALUE_INVALID_UTF8 ":%s FAIL METADATA VALUE_INVALID :value contains invalid UTF8" #define STR_FAIL_VALUE_INVALID_SIZE ":%s FAIL METADATA VALUE_INVALID :value is too long" From 7309f6cd0b983c8015f04c52b05fff931423050e Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 20:39:58 +0200 Subject: [PATCH 08/14] Allow '/' in keys --- files/metadata2.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/metadata2.c b/files/metadata2.c index 3a10320..2edb7c1 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -1060,7 +1060,7 @@ int metadata_key_valid(const char *key) continue; if(*key >= '0' && *key <= '9') continue; - if(*key == '_' || *key == '.' || *key == ':' || *key == '-') + if(*key == '_' || *key == '.' || *key == '/' || *key == ':' || *key == '-') continue; return 0; } From 22ff1dd56a4c9b3294d6c3512b20ec3abd1b9c9d Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 15 Apr 2023 22:45:23 +0200 Subject: [PATCH 09/14] Use RPL_KEYNOTSET instead of RPL_KEYVALUE with empty trailing --- files/metadata2.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/files/metadata2.c b/files/metadata2.c index 2edb7c1..19db2e6 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -789,8 +789,13 @@ void metadata_set_user(Client *user, const char *key, const char *value, Client } } } - if (!IsServer(client) && MyConnect(client)) - sendnumeric(client, RPL_KEYVALUE, target_name, key, "*", value?value:""); /* all OK */ + if (!IsServer(client) && MyConnect(client)) { + /* all OK */ + if (value) + sendnumeric(client, RPL_KEYVALUE, target_name, key, "*", value); + else + sendnumeric(client, RPL_KEYNOTSET, target_name, key); + } if (changed && (client == &me || IsUser(client) || IsServer(client))) user_metadata_changed(target, key, value, client); } @@ -860,8 +865,13 @@ void metadata_set_channel(Channel *channel, const char *key, const char *value, } } } - if (IsUser(client) && MyUser(client)) - sendnumeric(client, RPL_KEYVALUE, channel->name, key, "*", value?value:""); /* all OK */ + if (IsUser(client) && MyUser(client)) { + /* all OK */ + if (value) + sendnumeric(client, RPL_KEYVALUE, channel->name, key, "*", value); + else + sendnumeric(client, RPL_KEYNOTSET, channel->name, key); + } if (changed && (IsUser(client) || IsServer(client))) channel_metadata_changed(channel, key, value, client); } @@ -1169,11 +1179,11 @@ CMD_FUNC(cmd_metadata_local) return; } - if (!unrl_utf8_validate(value, NULL)) { + if (value && !unrl_utf8_validate(value, NULL)) { sendto_one(client, NULL, STR_FAIL_VALUE_INVALID_UTF8, me.name); return; } - if (strlen(value) > MAX_VALUE_BYTES) { + if (value && strlen(value) > MAX_VALUE_BYTES) { sendto_one(client, NULL, STR_FAIL_VALUE_INVALID_SIZE, me.name); return; } From 74a948630d0e8d7454634426ece2b4982e743b4c Mon Sep 17 00:00:00 2001 From: k4be Date: Sun, 16 Apr 2023 09:54:08 +0200 Subject: [PATCH 10/14] fixes JOIN tag breakage and memory leak --- files/metadata2.c | 57 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/files/metadata2.c b/files/metadata2.c index 19db2e6..ff865e6 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020-2021 k4be +/* Copyright (C) 2020-2021, 2023 k4be ** Copyright (C) 2023 Valentin Lorentz ** License: GPLv3 https://www.gnu.org/licenses/gpl-3.0.html */ @@ -164,8 +164,8 @@ struct metadata_moddata_user { struct metadata_unsynced *us; }; -struct metadata_unsynced { /* we're listing users (nicknames) that should be synced but were not */ - char *name; +struct metadata_unsynced { /* we're listing users (UIDs) that should be synced but were not */ + char *id; char *key; struct metadata_unsynced *next; }; @@ -548,11 +548,24 @@ int metadata_notify_or_queue(Client *client, const char *who, const char *key, c metadata_send_change(client, 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)->name = strdup(who); + (*us)->id = strdup(uid_or_channel); (*us)->key = strdup(key); (*us)->next = NULL; } @@ -690,7 +703,7 @@ void metadata_user_free(ModData *md) metadata_free_list(metadata, NULL, NULL); while (us) { - safe_free(us->name); + safe_free(us->id); safe_free(us->key); prev_us = us; us = us->next; @@ -1375,23 +1388,41 @@ int metadata_join(Client *client, Channel *channel, MessageTag *mtags) void metadata_sync(Client *client) { - Client *acptr; + Client *acptr = NULL; Channel *channel = NULL; + int do_send = 0; + char *who; 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; - + while (us) { if (!IsSendable(client)) break; - acptr = hash_find_nickatserver(us->name, NULL); - if (acptr && has_common_channels(acptr, client)) - { /* if not, the user has vanished since or one of us parted the channel */ - struct metadata_moddata_user *moddata = USER_METADATA(acptr); + 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; @@ -1401,7 +1432,7 @@ void metadata_sync(Client *client) { /* has it */ const char *value = metadata_get_user_key_value(acptr, us->key); if(value) - metadata_send_change(client, us->name, us->key, value, NULL); + metadata_send_change(client, us->id, us->key, value, NULL); } metadata = metadata->next; } @@ -1410,7 +1441,7 @@ void metadata_sync(Client *client) /* now remove the processed entry */ prev_us = us; us = us->next; - safe_free(prev_us->name); + safe_free(prev_us->id); safe_free(prev_us); my_moddata->us = us; /* we're always removing the first list item */ } From 1cdd9b9aaebaa2305bd99c0ac878b3cf52d583ec Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 16 Apr 2023 10:06:25 +0200 Subject: [PATCH 11/14] add another BATCH for delayed notifications Based on changes by k4be --- files/metadata2.c | 50 +++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/files/metadata2.c b/files/metadata2.c index ff865e6..66ecb3c 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -134,9 +134,8 @@ module MessageTag *mtags = NULL; \ MessageTag *m; \ PUSH_IGNORE_ADDRESS /* -Waddress warns when batch_id is an array */ \ - if (!BadPtr(batch_id)) \ + if (!BadPtr(batch_id)) { \ POP_IGNORE_ADDRESS \ - { \ mtags = safe_alloc(sizeof(MessageTag)); \ mtags->name = strdup("batch"); \ mtags->value = strdup(batchid); \ @@ -206,7 +205,7 @@ void metadata_send_all_for_user(Client *user, Client *client); void metadata_sync(Client *client); 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, const char *who, const char *key, const char *value, Client *changer); +void metadata_send_change(Client *client, const char *batchid, const char *who, const char *key, const char *value, Client *changer); int metadata_notify_or_queue(Client *client, const char *who, const char *key, const char *value, Client *changer); ModDataInfo *metadataUser; @@ -545,7 +544,7 @@ int metadata_notify_or_queue(Client *client, const char *who, const char *key, c if (IsSendable(client)) { - metadata_send_change(client, who, key, value, changer); + metadata_send_change(client, NULL, who, key, value, changer); } else { /* store for the SYNC */ Client *who_client; @@ -572,7 +571,7 @@ int metadata_notify_or_queue(Client *client, const char *who, const char *key, c return trylater; } -void metadata_send_change(Client *client, const char *who, const char *key, const char *value, Client *changer) +void metadata_send_change(Client *client, const char *batchid, const char *who, const char *key, const char *value, Client *changer) { char *sender = NULL; if (!key) @@ -601,16 +600,20 @@ void metadata_send_change(Client *client, const char *who, const char *key, cons sender = me.name; if (changer && IsUser(changer) && MyUser(client)) { - if (!value) - sendto_one(client, NULL, ":%s!%s@%s METADATA %s %s %s", sender, changer->user->username, GetHost(changer), who, key, "*"); - else - sendto_one(client, NULL, ":%s!%s@%s METADATA %s %s %s :%s", sender, changer->user->username, GetHost(changer), who, key, "*", value); + if (!value) { + batched(sendto_one, client, batchid, ":%s!%s@%s METADATA %s %s %s", sender, changer->user->username, GetHost(changer), who, key, "*"); + } + else { + batched(sendto_one, client, batchid, ":%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, NULL, ":%s METADATA %s %s %s", sender, who, key, "*"); - else - sendto_one(client, NULL, ":%s METADATA %s %s %s :%s", sender, who, key, "*", value); + if (!value) { + batched(sendto_one, client, batchid, ":%s METADATA %s %s %s", sender, who, key, "*"); + } + else { + batched(sendto_one, client, batchid, ":%s METADATA %s %s %s :%s", sender, who, key, "*", value); + } } } @@ -629,7 +632,7 @@ void user_metadata_changed(Client *user, const char *key, const char *value, Cli { /* notifications for linked servers, TODO change to sendto_server */ if (acptr == &me) continue; - metadata_send_change(acptr, user->name, key, value, changer); + metadata_send_change(acptr, NULL, user->name, key, value, changer); } } @@ -641,14 +644,14 @@ void channel_metadata_changed(Channel *channel, const char *key, const char *val 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, channel->name, key, value, changer); + 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, channel->name, key, value, changer); + metadata_send_change(acptr, NULL, channel->name, key, value, changer); } } @@ -1332,7 +1335,7 @@ int metadata_server_sync(Client *client) if(!moddata) continue; for (metadata = moddata->metadata; metadata; metadata = metadata->next) - metadata_send_change(client, acptr->name, metadata->name, metadata->value, &me); + metadata_send_change(client, NULL, acptr->name, metadata->name, metadata->value, &me); } for (hashnum = 0; hashnum < CHAN_HASH_TABLE_SIZE; hashnum++) @@ -1340,7 +1343,7 @@ int metadata_server_sync(Client *client) for(channel = hash_get_chan_bucket(hashnum); channel; channel = channel->hnextch) { for(metadata = CHANNEL_METADATA(channel); metadata; metadata = metadata->next) - metadata_send_change(client, channel->name, metadata->name, metadata->value, &me); + metadata_send_change(client, NULL, channel->name, metadata->name, metadata->value, &me); } } return 0; @@ -1392,6 +1395,7 @@ void metadata_sync(Client *client) Channel *channel = NULL; int do_send = 0; char *who; + char batchid[BATCHLEN+1] = ""; struct metadata_moddata_user *my_moddata = USER_METADATA(client); if(!my_moddata) @@ -1399,6 +1403,11 @@ void metadata_sync(Client *client) struct metadata_unsynced *us = my_moddata->us; struct metadata_unsynced *prev_us; + if (us && HasCapability(client, "batch")) { + generate_batch_id(batchid); + sendto_one(client, NULL, ":%s BATCH +%s metadata", me.name, batchid); + } + while (us) { if (!IsSendable(client)) @@ -1432,7 +1441,7 @@ void metadata_sync(Client *client) { /* has it */ const char *value = metadata_get_user_key_value(acptr, us->key); if(value) - metadata_send_change(client, us->id, us->key, value, NULL); + metadata_send_change(client, batchid, us->id, us->key, value, NULL); } metadata = metadata->next; } @@ -1445,6 +1454,9 @@ void metadata_sync(Client *client) safe_free(prev_us); my_moddata->us = us; /* we're always removing the first list item */ } + if (*batchid) + sendto_one(client, NULL, ":%s BATCH -%s", me.name, batchid); + } int metadata_user_registered(Client *client) From 598dc851d74ef29a74bda3ddfb37b566508f7914 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 15 Jun 2025 10:59:58 +0200 Subject: [PATCH 12/14] Fix build on current Unreal --- files/metadata2.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/metadata2.c b/files/metadata2.c index 66ecb3c..28855be 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -1316,9 +1316,9 @@ CMD_FUNC(cmd_metadata_remote) CMD_FUNC(cmd_metadata) { if (client != &me && MyConnect(client) && !IsServer(client)) - cmd_metadata_local(client, recv_mtags, parc, parv); + cmd_metadata_local(clictx, client, recv_mtags, parc, parv); else - cmd_metadata_remote(client, recv_mtags, parc, parv); + cmd_metadata_remote(clictx, client, recv_mtags, parc, parv); } int metadata_server_sync(Client *client) From b58ef4e0fbe1a8c8ac7abf6b4192079b6ae3ec85 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 15 Jun 2025 11:38:28 +0200 Subject: [PATCH 13/14] Fix build on Unreal < 6.2 --- files/metadata2.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/files/metadata2.c b/files/metadata2.c index 28855be..3909820 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -1315,10 +1315,17 @@ CMD_FUNC(cmd_metadata_remote) CMD_FUNC(cmd_metadata) { +#if (UNREAL_VERSION_GENERATION == 6 && UNREAL_VERSION_MAJOR >= 2) if (client != &me && MyConnect(client) && !IsServer(client)) cmd_metadata_local(clictx, client, recv_mtags, parc, parv); else cmd_metadata_remote(clictx, client, recv_mtags, parc, parv); +#else + if (client != &me && MyConnect(client) && !IsServer(client)) + cmd_metadata_local(client, recv_mtags, parc, parv); + else + cmd_metadata_remote(client, recv_mtags, parc, parv); +#endif } int metadata_server_sync(Client *client) From 9973c8fb0b2440571effb5aa82f9a7158e5e18c1 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 15 Jun 2025 22:50:30 +0200 Subject: [PATCH 14/14] Mention that third/metadata2 is incompatible with third/metadata --- files/metadata2.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/metadata2.c b/files/metadata2.c index 3909820..9872926 100644 --- a/files/metadata2.c +++ b/files/metadata2.c @@ -222,7 +222,7 @@ struct metadata_settings_s { ModuleHeader MOD_HEADER = { "third/metadata2", "6.0", - "draft/metadata2 and draft/metadata-notify-2 cap", + "draft/metadata2 and metadata-notify cap. Should not be loaded at the same time as third/metadata", "k4be & val", "unrealircd-6" };