1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-07-04 01:23:13 +02:00
Files
unrealircd/src/modules/metadata.c
T
2021-08-22 21:17:19 +02:00

1376 lines
36 KiB
C

/*
* IRC - Internet Relay Chat, src/modules/metadata.c
* (C) 2021 The UnrealIRCd Team
*
* See file AUTHORS in IRC package for additional names of
* the programmers.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 1, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "unrealircd.h"
#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"
#ifdef PREFIX_AQ
#define HOP_OR_MORE (CHFL_HALFOP | CHFL_CHANOP | CHFL_CHANADMIN | CHFL_CHANOWNER)
#else
#define HOP_OR_MORE (CHFL_HALFOP | CHFL_CHANOP)
#endif
#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() 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; } }
struct metadata {
char *name;
char *value;
struct metadata *next;
};
struct subscriptions {
char *name;
struct subscriptions *next;
};
struct moddata_user {
struct metadata *metadata;
struct subscriptions *subs;
struct unsynced *us;
};
struct unsynced { /* we're listing users (nicknames) that should be synced but were not */
char *name;
char *key;
struct unsynced *next;
};
CMD_FUNC(cmd_metadata);
CMD_FUNC(cmd_metadata_remote);
CMD_FUNC(cmd_metadata_local);
EVENT(metadata_queue_evt);
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);
/* hooks */
int metadata_server_sync(Client *client);
int metadata_join(Client *client, Channel *channel, MessageTag *mtags, char *parv[]);
int metadata_user_registered(Client *client);
void metadata_user_free(ModData *md);
void metadata_channel_free(ModData *md);
void sendnumeric(Client *to, int numeric, ...);
void free_metadata(struct metadata *metadata);
void free_subs(struct subscriptions *subs);
int is_subscribed(Client *user, char *key);
char *get_user_key_value(Client *user, char *key);
char *get_channel_key_value(Channel *channel, char *key);
void user_metadata_changed(Client *user, char *key, char *value, Client *changer);
void channel_metadata_changed(Channel *channel, char *key, char *value, Client *changer);
void metadata_free_list(struct metadata *metadata, char *whose, Client *client);
struct moddata_user *prepare_user_moddata(Client *user);
void metadata_set_channel(Channel *channel, char *key, char *value, Client *client);
void metadata_set_user(Client *user, char *key, char *value, Client *client);
void metadata_send_channel(Channel *channel, char *key, Client *client);
void metadata_send_user(Client *user, char *key, Client *client);
int metadata_subscribe(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 key_valid(char *key);
int check_perms(Client *user, Channel *channel, Client *client, char *key, int mode);
void send_change(Client *client, char *who, char *key, char *value, Client *changer);
int notify_or_queue(Client *client, char *who, char *key, char *value, Client *changer);
ModDataInfo *metadataUser;
ModDataInfo *metadataChannel;
long CAP_METADATA = 0L;
long CAP_METADATA_NOTIFY = 0L;
struct settings {
int max_user_metadata;
int max_channel_metadata;
int max_subscriptions;
int enable_debug;
} metadata_settings;
ModuleHeader MOD_HEADER = {
"metadata",
"6.0",
"draft/metadata and draft/metadata-notify-2 cap",
"UnrealIRCd Team",
"unrealircd-6"
};
/*
metadata {
max-user-metadata 10;
max-channel-metadata 10;
max-subscriptions 10;
enable-debug 0;
};
*/
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, "enable-debug"))
{
if (strlen(cep->value) != 1 || (cep->value[0] != '0' && cep->value[0] != '1'))
{
config_error("%s:%i: %s::%s must be 0 or 1", cep->file->filename, cep->line_number, MYCONF, cep->name);
errors++;
break;
}
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;
}
if (!strcmp(cep->name, "enable-debug"))
{
metadata_settings.enable_debug = 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() {
ISupportAdd(modinfo->handle, "METADATA", metadata_isupport_param());
/* 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);
return MOD_SUCCESS;
}
MOD_UNLOAD() {
return MOD_SUCCESS;
}
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 free_metadata(struct metadata *metadata)
{
safe_free(metadata->name);
safe_free(metadata->value);
safe_free(metadata);
}
void free_subs(struct subscriptions *subs)
{
safe_free(subs->name);
safe_free(subs);
}
int is_subscribed(Client *user, char *key)
{
struct moddata_user *moddata = moddata_client(user, metadataUser).ptr;
if (!moddata)
return 0;
struct subscriptions *subs;
for (subs = moddata->subs; subs; subs = subs->next)
{
if (!strcasecmp(subs->name, key))
return 1;
}
return 0;
}
char *get_user_key_value(Client *user, char *key)
{
struct moddata_user *moddata = moddata_client(user, metadataUser).ptr;
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;
}
char *get_channel_key_value(Channel *channel, char *key)
{
struct metadata *metadata;
for (metadata = moddata_channel(channel, metadataChannel).ptr; metadata; metadata = metadata->next)
{
if (!strcasecmp(key, metadata->name))
return metadata->value;
}
return NULL;
}
/* returns 1 if something remains to sync */
int notify_or_queue(Client *client, char *who, char *key, char *value, Client *changer)
{
int trylater = 0;
if (!who)
{
sendto_snomask(SNO_JUNK, "notify_or_queue called with null who!");
return 0;
}
if (!key)
{
sendto_snomask(SNO_JUNK, "notify_or_queue called with null key!");
return 0;
}
if (!client)
{
sendto_snomask(SNO_JUNK, "notify_or_queue called with null client!");
return 0;
}
struct unsynced **us, *prev_us;
struct moddata_user *moddata = moddata_client(client, metadataUser).ptr;
if (!moddata)
moddata = prepare_user_moddata(client);
if (IsSendable(client))
{
send_change(client, who, key, value, changer);
} else
{ /* store for the SYNC */
trylater = 1;
prev_us = NULL;
for (us = &moddata->us, prev_us = NULL; (*us)->next; us = &(*us)->next)
prev_us = *us; /* find last list element */
*us = safe_alloc(sizeof(struct unsynced));
(*us)->name = strdup(who);
(*us)->key = strdup(key);
(*us)->next = NULL;
}
return trylater;
}
void send_change(Client *client, char *who, char *key, char *value, Client *changer)
{
char *sender = NULL;
if (!key)
{
sendto_snomask(SNO_JUNK, "send_change called with null key!");
return;
}
if (!who)
{
sendto_snomask(SNO_JUNK, "send_change called with null who!");
return;
}
if (!client)
{
sendto_snomask(SNO_JUNK, "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, char *key, 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) && is_subscribed(acptr, key) && has_common_channels(user, acptr))
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;
send_change(acptr, user->name, key, value, changer);
}
}
void channel_metadata_changed(Channel *channel, char *key, 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 (is_subscribed(acptr, key) && IsMember(acptr, channel))
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;
send_change(acptr, channel->name, key, value, changer);
}
}
void metadata_free_list(struct metadata *metadata, 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 moddata_user *moddata = md->ptr;
if (!moddata)
return; /* was not set */
struct subscriptions *sub = moddata->subs;
struct subscriptions *prev_sub = sub;
struct unsynced *us = moddata->us;
struct 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 moddata_user *prepare_user_moddata(Client *user)
{
moddata_client(user, metadataUser).ptr = safe_alloc(sizeof(struct moddata_user));
struct moddata_user *ptr = moddata_client(user, metadataUser).ptr;
ptr->metadata = NULL;
ptr->subs = NULL;
return ptr;
}
void metadata_set_user(Client *user, char *key, 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 moddata_user *moddata = moddata_client(target, metadataUser).ptr;
if (!moddata) /* first call for this user */
moddata = 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;
free_metadata(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, char *key, char *value, Client *client)
{
int changed = 0;
int set = 0;
int count = 0;
struct metadata **metadata = (struct metadata **)&moddata_channel(channel, metadataChannel).ptr;
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;
free_metadata(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(char *key, Client *client, int remove)
{
struct moddata_user *moddata = moddata_client(client, metadataUser).ptr;
struct subscriptions **subs;
struct subscriptions *prev_subs;
int found = 0;
int count = 0;
int trylater = 0;
char *value;
unsigned int hashnum;
Channel *channel;
Client *acptr;
if (!client)
return 0;
if (!moddata) /* first call for this user */
moddata = 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;
free_subs(prev_subs);
}
break;
}
}
if (!remove && !found)
{
if (count < metadata_settings.max_subscriptions)
{
*subs = safe_alloc(sizeof(struct 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 = get_user_key_value(acptr, key);
if (value)
trylater |= 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 = get_channel_key_value(channel, key);
if (value)
trylater |= 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, char *key, Client *client)
{
struct metadata *metadata;
int found = 0;
for (metadata = moddata_channel(channel, metadataChannel).ptr; 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, char *key, Client *client)
{
if (!user)
user = client;
struct moddata_user *moddata = moddata_client(user, metadataUser).ptr;
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 = moddata_channel(channel, metadataChannel).ptr;
metadata_free_list(metadata, channel->name, client);
moddata_channel(channel, metadataChannel).ptr = NULL;
}
void metadata_clear_user(Client *user, Client *client)
{
if (!user)
user = client;
struct moddata_user *moddata = moddata_client(user, metadataUser).ptr;
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 subscriptions *subs;
struct moddata_user *moddata = moddata_client(client, metadataUser).ptr;
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 = moddata_channel(channel, metadataChannel).ptr; 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 moddata_user *moddata = moddata_client(user, metadataUser).ptr;
if (!moddata)
return;
for (metadata = moddata->metadata; metadata; metadata = metadata->next)
sendnumeric(client, RPL_KEYVALUE, user->name, metadata->name, "*", metadata->value);
}
int key_valid(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 check_perms(Client *user, Channel *channel, Client *client, char *key, int mode)
{ /* either user or channel should be NULL */
Membership *lp;
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)
{
if ((lp = find_membership_link(client->user->channel, channel)) && ((lp->flags & HOP_OR_MORE) || (mode == MODE_GET)))
return 1; /* allow setting channel metadata if we're halfop or more, and getting when we're just on this channel */
} 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 <Target> <Subcommand> [<Param 1> ... [<Param n>]] */
CMD_FUNC(cmd_metadata_local)
{
Channel *channel = NULL;
Client *user = NULL;
char buf[1024] = "";
int i;
int trylater;
if (metadata_settings.enable_debug)
{
for (i=1; i<parc; i++)
{
if (!BadPtr(parv[i]))
strlcat(buf, parv[i], 1024);
strlcat(buf, " ", 1024);
}
sendto_snomask(SNO_JUNK, "Received METADATA, sender %s, params: %s", client->name, buf);
}
CHECKPARAMSCNT_OR_DIE(2, return);
char *target = parv[1];
char *cmd = parv[2];
char *key;
int keyindex = 3-1;
char *value = NULL;
char *channame;
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()
{
if (check_perms(user, channel, client, key, MODE_GET))
{
if (!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 (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 (!check_perms(user, channel, client, key, MODE_SET))
return;
if (parc > 3 && !BadPtr(parv[4]))
value = parv[4];
if (!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 (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);
trylater = 0;
FOR_EACH_KEY()
{
if(key_valid(key))
{
if(metadata_subscribe(key, client, 0))
trylater = 1;
} 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()
{
if(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 <client/channel> <key name> *[ :<key value>]" */
CMD_FUNC(cmd_metadata_remote)
{ /* handling data from linked server */
Channel *channel = NULL;
Client *user = NULL;
char *target= parv[1];
char *key;
char *value;
char *channame = strchr(target, '#');
char buf[1024] = "";
int i;
if (metadata_settings.enable_debug)
{
for (i=1; i<parc; i++)
{
if (!BadPtr(parv[i]))
strlcat(buf, parv[i], 1024);
strlcat(buf, " ", 1024);
}
sendto_snomask(SNO_JUNK, "Received remote METADATA, sender: %s, params: %s", client->name, buf);
}
if (parc < 5 || BadPtr(parv[4]))
{
if (parc == 4 && !BadPtr(parv[3]))
{
value = NULL;
} else
{
sendto_snomask(SNO_JUNK, "METADATA not enough args from %s", client->name);
return;
}
} else
{
value = parv[4];
}
target = parv[1];
key = parv[2];
channame = strchr(target, '#');
if (!*target || !strcmp(target, "*") || !key_valid(key))
{
sendto_snomask(SNO_JUNK, "Bad metadata target %s or key %s from %s", target, key, 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 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 = moddata_client(acptr, metadataUser).ptr;
if(!moddata)
continue;
for (metadata = moddata->metadata; metadata; metadata = metadata->next)
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 = moddata_channel(channel, metadataChannel).ptr; metadata; metadata = metadata->next)
send_change(client, channel->name, metadata->name, metadata->value, &me);
}
}
return 0;
}
int metadata_join(Client *client, Channel *channel, MessageTag *mtags, char *parv[])
{
Client *acptr;
Member *cm;
char *value;
struct unsynced *prev_us;
struct unsynced *us;
Membership *lp;
struct subscriptions *subs;
struct metadata *metadata;
struct moddata_user *moddata = moddata_client(client, metadataUser).ptr;
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) && is_subscribed(acptr, metadata->name))
notify_or_queue(acptr, client->name, metadata->name, metadata->value, NULL);
}
}
for (subs = moddata->subs; subs; subs = subs->next)
{
value = get_channel_key_value(channel, subs->name); /* notify joining user about channel metadata */
if(value)
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 = get_user_key_value(acptr, subs->name);
if (value)
notify_or_queue(client, acptr->name, subs->name, value, NULL);
}
}
return 0;
}
void metadata_sync(Client *client)
{
Client *acptr;
Channel *channel = NULL;
struct moddata_user *my_moddata = moddata_client(client, metadataUser).ptr;
if(!my_moddata)
return; /* nothing queued */
struct unsynced *us = my_moddata->us;
struct 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 moddata_user *moddata = moddata_client(acptr, metadataUser).ptr;
if (moddata)
{
struct metadata *metadata = moddata->metadata;
while (metadata)
{
if (!strcasecmp(us->key, metadata->name))
{ /* has it */
char *value = get_user_key_value(acptr, us->key);
if(value)
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 moddata_user *moddata = moddata_client(client, metadataUser).ptr;
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);
}
}