unrealircd

- supernets unrealircd source & configuration
git clone git://git.acid.vegas/unrealircd.git
Log | Files | Refs | Archive | README | LICENSE

json.c (19047B)

      1 /************************************************************************
      2  * UnralIRCd JSON functions, src/json.c
      3  * (C) 2021-.. Bram Matthys (Syzop) and the UnrealIRCd Team
      4  * License: GPLv2 or later
      5  */
      6 
      7 #include "unrealircd.h"
      8 
      9 /** @file
     10  * @brief JSON functions - used for logging and RPC.
     11  */
     12 
     13 /** Are we currently in the logging code? */
     14 int log_json_filter = 0;
     15 
     16 /** Calculate expansion of a JSON string thanks to double escaping.
     17  * orig => JSON => IRC
     18  *    " => \"   => \\"
     19  *    \ => \\   => \\\\
     20  */
     21 int json_dump_string_length(const char *s)
     22 {
     23 	int len = 0;
     24 	for (; *s; s++)
     25 	{
     26 		if (*s == '\\')
     27 			len += 4;
     28 		else if (*s == '"')
     29 			len += 3;
     30 		else
     31 			len++;
     32 	}
     33 	return len;
     34 }
     35 
     36 /** Convert a regular string value to a JSON string.
     37  * In UnrealIRCd, this must be used instead of json_string()
     38  * as we may use non-UTF8 sequences. Also, this takes care
     39  * of using json_null() if the string was NULL, which is
     40  * usually what we want as well.
     41  * @param s	Input string
     42  * @returns a json string value or json null value.
     43  */
     44 json_t *json_string_unreal(const char *s)
     45 {
     46 	char buf1[512], buf2[512];
     47 	char *verified_s;
     48 	const char *stripped;
     49 
     50 	if (s == NULL)
     51 		return json_null();
     52 
     53 	if (log_json_filter)
     54 	{
     55 		stripped = StripControlCodesEx(s, buf1, sizeof(buf1), UNRL_STRIP_LOW_ASCII|UNRL_STRIP_KEEP_LF);
     56 		verified_s = unrl_utf8_make_valid(buf1, buf2, sizeof(buf2), 0);
     57 	} else {
     58 		verified_s = unrl_utf8_make_valid(s, buf2, sizeof(buf2), 0);
     59 	}
     60 
     61 	return json_string(verified_s);
     62 }
     63 
     64 const char *json_object_get_string(json_t *j, const char *name)
     65 {
     66 	json_t *v = json_object_get(j, name);
     67 	return v ? json_string_value(v) : NULL;
     68 }
     69 
     70 /** Get integer value of a JSON object.
     71  * @param j		The JSON object that should contain a 'name' item
     72  * @param name		The item to search for
     73  * @param default_value	The value to return when the JSON object 'name' is not found
     74  *			or not an integer.
     75  * @returns The integer value, or default_value if the object does not exist or is not an integer.
     76  */
     77 int json_object_get_integer(json_t *j, const char *name, int default_value)
     78 {
     79 	json_t *v = json_object_get(j, name);
     80 	if (!v || !json_is_integer(v))
     81 		return default_value;
     82 	return json_integer_value(v);
     83 }
     84 
     85 int json_object_get_boolean(json_t *j, const char *name, int default_value)
     86 {
     87 	json_t *v = json_object_get(j, name);
     88 	if (!v)
     89 		return default_value;
     90 	if (json_is_true(v))
     91 		return 1;
     92 	return 0;
     93 }
     94 
     95 #define json_string __BAD___DO__NOT__USE__JSON__STRING__PLZ
     96 
     97 const char *json_get_value(json_t *t)
     98 {
     99 	static char buf[32];
    100 
    101 	if (json_is_string(t))
    102 		return json_string_value(t);
    103 
    104 	if (json_is_integer(t))
    105 	{
    106 		snprintf(buf, sizeof(buf), "%lld", (long long)json_integer_value(t));
    107 		return buf;
    108 	}
    109 
    110 	if (json_is_boolean(t))
    111 	{
    112 		if (json_is_true(t))
    113 			return "true";
    114 		return "false";
    115 	}
    116 
    117 	if (json_is_array(t))
    118 		return "<array>";
    119 
    120 	return NULL;
    121 }
    122 
    123 json_t *json_timestamp(time_t v)
    124 {
    125 	const char *ts = timestamp_iso8601(v);
    126 	if (ts)
    127 		return json_string_unreal(ts);
    128 	return json_null();
    129 }
    130 
    131 const char *timestamp_iso8601_now(void)
    132 {
    133 	struct timeval t;
    134 	struct tm *tm;
    135 	time_t sec;
    136 	static char buf[64];
    137 
    138 	gettimeofday(&t, NULL);
    139 	sec = t.tv_sec;
    140 	tm = gmtime(&sec);
    141 
    142 	snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
    143 		tm->tm_year + 1900,
    144 		tm->tm_mon + 1,
    145 		tm->tm_mday,
    146 		tm->tm_hour,
    147 		tm->tm_min,
    148 		tm->tm_sec,
    149 		(int)(t.tv_usec / 1000));
    150 
    151 	return buf;
    152 }
    153 
    154 const char *timestamp_iso8601(time_t v)
    155 {
    156 	struct tm *tm;
    157 	static char buf[64];
    158 
    159 	if (v == 0)
    160 		return NULL;
    161 
    162 	tm = gmtime(&v);
    163 
    164 	if (tm == NULL)
    165 		return NULL;
    166 
    167 	snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
    168 		tm->tm_year + 1900,
    169 		tm->tm_mon + 1,
    170 		tm->tm_mday,
    171 		tm->tm_hour,
    172 		tm->tm_min,
    173 		tm->tm_sec,
    174 		0);
    175 
    176 	return buf;
    177 }
    178 
    179 void json_expand_client_security_groups(json_t *parent, Client *client)
    180 {
    181 	SecurityGroup *s;
    182 	json_t *child = json_array();
    183 	json_object_set_new(parent, "security-groups", child);
    184 
    185 	/* We put known-users or unknown-users at the beginning.
    186 	 * The latter is special and doesn't actually exist
    187 	 * in the linked list, hence the special code here,
    188 	 * and again later in the for loop to skip it.
    189 	 */
    190 	if (user_allowed_by_security_group_name(client, "known-users"))
    191 		json_array_append_new(child, json_string_unreal("known-users"));
    192 	else
    193 		json_array_append_new(child, json_string_unreal("unknown-users"));
    194 
    195 	for (s = securitygroups; s; s = s->next)
    196 		if (strcmp(s->name, "known-users") && user_allowed_by_security_group(client, s))
    197 			json_array_append_new(child, json_string_unreal(s->name));
    198 }
    199 
    200 /* detail=0:	only name, id
    201  * detail=1:	only name, id, hostname, ip, details, geoip
    202  * detail=2:	everything, except 'channels'
    203  * detail=3:	everything, with 'channels' being a max 384 character string (meant for JSON logging only)
    204  * detail=4:	everything, with 'channels' object (full).
    205  */
    206 void json_expand_client(json_t *j, const char *key, Client *client, int detail)
    207 {
    208 	char buf[BUFSIZE+1];
    209 	json_t *child;
    210 	json_t *user = NULL;
    211 	time_t ts;
    212 
    213 	if (key)
    214 	{
    215 		child = json_object();
    216 		json_object_set_new(j, key, child);
    217 	} else {
    218 		child = j;
    219 	}
    220 
    221 	/* First the information that is available for ALL client types: */
    222 	json_object_set_new(child, "name", json_string_unreal(client->name));
    223 	json_object_set_new(child, "id", json_string_unreal(client->id));
    224 
    225 	if (detail == 0)
    226 		return;
    227 
    228 	/* hostname is available for all, it just depends a bit on whether it is DNS or IP */
    229 	if (client->user && *client->user->realhost)
    230 		json_object_set_new(child, "hostname", json_string_unreal(client->user->realhost));
    231 	else if (client->local && *client->local->sockhost)
    232 		json_object_set_new(child, "hostname", json_string_unreal(client->local->sockhost));
    233 	else
    234 		json_object_set_new(child, "hostname", json_string_unreal(GetIP(client)));
    235 
    236 	/* same for ip, is there for all (well, some services pseudo-users may not have one) */
    237 	json_object_set_new(child, "ip", json_string_unreal(client->ip));
    238 	/* client.details is always available: it is nick!user@host, nick@host, server@host
    239 	 * server@ip, or just server.
    240 	 */
    241 	if (client->user)
    242 	{
    243 		snprintf(buf, sizeof(buf), "%s!%s@%s", client->name, client->user->username, client->user->realhost);
    244 		json_object_set_new(child, "details", json_string_unreal(buf));
    245 	} else if (client->ip) {
    246 		if (*client->name)
    247 			snprintf(buf, sizeof(buf), "%s@%s", client->name, client->ip);
    248 		else
    249 			snprintf(buf, sizeof(buf), "[%s]", client->ip);
    250 		json_object_set_new(child, "details", json_string_unreal(buf));
    251 	} else {
    252 		json_object_set_new(child, "details", json_string_unreal(client->name));
    253 	}
    254 
    255 	if (detail < 2)
    256 	{
    257 		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT, client, detail, child);
    258 		return;
    259 	}
    260 
    261 	if (client->local && client->local->listener)
    262 		json_object_set_new(child, "server_port", json_integer(client->local->listener->port));
    263 	if (client->local && client->local->port)
    264 		json_object_set_new(child, "client_port", json_integer(client->local->port));
    265 	if ((ts = get_creationtime(client)))
    266 		json_object_set_new(child, "connected_since", json_timestamp(ts));
    267 	if (client->local && client->local->idle_since)
    268 		json_object_set_new(child, "idle_since", json_timestamp(client->local->idle_since));
    269 
    270 	if (client->user)
    271 	{
    272 		char buf[512];
    273 		const char *str;
    274 		/* client.user */
    275 		user = json_object();
    276 		json_object_set_new(child, "user", user);
    277 
    278 		json_object_set_new(user, "username", json_string_unreal(client->user->username));
    279 		if (!BadPtr(client->info))
    280 			json_object_set_new(user, "realname", json_string_unreal(client->info));
    281 		if (has_user_mode(client, 'x') && client->user->virthost && strcmp(client->user->virthost, client->user->realhost))
    282 			json_object_set_new(user, "vhost", json_string_unreal(client->user->virthost));
    283 		if (*client->user->cloakedhost)
    284 			json_object_set_new(user, "cloakedhost", json_string_unreal(client->user->cloakedhost));
    285 		if (client->uplink)
    286 			json_object_set_new(user, "servername", json_string_unreal(client->uplink->name));
    287 		if (IsLoggedIn(client))
    288 			json_object_set_new(user, "account", json_string_unreal(client->user->account));
    289 		json_object_set_new(user, "reputation", json_integer(GetReputation(client)));
    290 		json_expand_client_security_groups(user, client);
    291 
    292 		/* user modes and snomasks */
    293 		get_usermode_string_r(client, buf, sizeof(buf));
    294 		json_object_set_new(user, "modes", json_string_unreal(buf+1));
    295 		if (client->user->snomask)
    296 			json_object_set_new(user, "snomasks", json_string_unreal(client->user->snomask));
    297 
    298 		/* if oper then we can possibly expand a bit more */
    299 		str = get_operlogin(client);
    300 		if (str)
    301 			json_object_set_new(user, "operlogin", json_string_unreal(str));
    302 		str = get_operclass(client);
    303 		if (str)
    304 			json_object_set_new(user, "operclass", json_string_unreal(str));
    305 		/* For detail>2 we will include the channels.
    306 		 * Even if the user is on 0 channels we include "channels":[]
    307 		 * so it is clear that the user is on 0 channels and it is
    308 		 * not because of low detail level that channels are skipped.
    309 		 */
    310 		if (detail > 2)
    311 		{
    312 			Membership *m;
    313 			int cnt = 0;
    314 			int len = 0;
    315 			json_t *channels = json_array();
    316 			json_object_set_new(user, "channels", channels);
    317 
    318 			if (detail == 3)
    319 			{
    320 				/* Short format, mainly for JSON logging */
    321 				for (m = client->user->channel; m; m = m->next)
    322 				{
    323 					len += json_dump_string_length(m->channel->name);
    324 					if (len > 384)
    325 					{
    326 						/* Truncated */
    327 						json_array_append_new(channels, json_string_unreal("..."));
    328 						break;
    329 					}
    330 					json_array_append_new(channels, json_string_unreal(m->channel->name));
    331 				}
    332 			} else {
    333 				/* Long format for JSON-RPC */
    334 				for (m = client->user->channel; m; m = m->next)
    335 				{
    336 					json_t *e = json_object();
    337 					json_object_set_new(e, "name", json_string_unreal(m->channel->name));
    338 					if (*m->member_modes)
    339 						json_object_set_new(e, "level", json_string_unreal(m->member_modes));
    340 					json_array_append_new(channels, e);
    341 				}
    342 			}
    343 		}
    344 		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_USER, client, detail, child, user);
    345 	} else
    346 	if (IsMe(client))
    347 	{
    348 		json_t *server = json_object();
    349 		json_t *features;
    350 
    351 		/* client.server */
    352 		json_object_set_new(child, "server", server);
    353 
    354 		if (!BadPtr(client->info))
    355 			json_object_set_new(server, "info", json_string_unreal(client->info));
    356 		json_object_set_new(server, "num_users", json_integer(client->server->users));
    357 		json_object_set_new(server, "boot_time", json_timestamp(client->server->boottime));
    358 
    359 		/* client.server.features */
    360 		features = json_object();
    361 		json_object_set_new(server, "features", features);
    362 		if (!BadPtr(client->server->features.software))
    363 		{
    364 			char buf[256];
    365 			snprintf(buf, sizeof(buf), "UnrealIRCd-%s", buildid);
    366 			json_object_set_new(features, "software", json_string_unreal(buf));
    367 		}
    368 		json_object_set_new(features, "protocol", json_integer(UnrealProtocol));
    369 		if (!BadPtr(client->server->features.usermodes))
    370 			json_object_set_new(features, "usermodes", json_string_unreal(umodestring));
    371 
    372 		/* client.server.features.chanmodes (array) */
    373 		{
    374 			int i;
    375 			char buf[512];
    376 			json_t *chanmodes = json_array();
    377 			json_object_set_new(features, "chanmodes", chanmodes);
    378 			/* first one is special - wait.. is this still the case? lol. */
    379 			snprintf(buf, sizeof(buf), "%s%s", CHPAR1, EXPAR1);
    380 			json_array_append_new(chanmodes, json_string_unreal(buf));
    381 			for (i=1; i < 4; i++)
    382 				json_array_append_new(chanmodes, json_string_unreal(extchmstr[i]));
    383 		}
    384 		if (!BadPtr(client->server->features.nickchars))
    385 			json_object_set_new(features, "nick_character_sets", json_string_unreal(charsys_get_current_languages()));
    386 		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_SERVER, client, detail, child, server);
    387 	} else
    388 	if (IsServer(client) && client->server)
    389 	{
    390 		/* client.server */
    391 
    392 		/* Whenever a server is expanded, which is rare,
    393 		 * we should probably expand as much as info as possible:
    394 		 */
    395 		json_t *server = json_object();
    396 		json_t *features;
    397 
    398 		/* client.server */
    399 		json_object_set_new(child, "server", server);
    400 		if (!BadPtr(client->info))
    401 			json_object_set_new(server, "info", json_string_unreal(client->info));
    402 		if (client->uplink)
    403 			json_object_set_new(server, "uplink", json_string_unreal(client->uplink->name));
    404 		json_object_set_new(server, "num_users", json_integer(client->server->users));
    405 		json_object_set_new(server, "boot_time", json_timestamp(client->server->boottime));
    406 		json_object_set_new(server, "synced", json_boolean(client->server->flags.synced));
    407 		json_object_set_new(server, "ulined", json_boolean(IsULine(client)));
    408 
    409 		/* client.server.features */
    410 		features = json_object();
    411 		json_object_set_new(server, "features", features);
    412 		if (!BadPtr(client->server->features.software))
    413 			json_object_set_new(features, "software", json_string_unreal(client->server->features.software));
    414 		json_object_set_new(features, "protocol", json_integer(client->server->features.protocol));
    415 		if (!BadPtr(client->server->features.usermodes))
    416 			json_object_set_new(features, "usermodes", json_string_unreal(client->server->features.usermodes));
    417 		if (!BadPtr(client->server->features.chanmodes[0]))
    418 		{
    419 			/* client.server.features.chanmodes (array) */
    420 			int i;
    421 			json_t *chanmodes = json_array();
    422 			json_object_set_new(features, "chanmodes", chanmodes);
    423 			for (i=0; i < 4; i++)
    424 				json_array_append_new(chanmodes, json_string_unreal(client->server->features.chanmodes[i]));
    425 		}
    426 		if (!BadPtr(client->server->features.nickchars))
    427 			json_object_set_new(features, "nick_character_sets", json_string_unreal(client->server->features.nickchars));
    428 		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_SERVER, client, detail, child, server);
    429 	}
    430 	RunHook(HOOKTYPE_JSON_EXPAND_CLIENT, client, detail, child);
    431 }
    432 
    433 void json_expand_channel_ban(json_t *child, const char *banlist_name, Ban *banlist)
    434 {
    435 	Ban *ban;
    436 	json_t *list, *e;
    437 
    438 	list = json_array();
    439 	json_object_set_new(child, banlist_name, list);
    440 	for (ban = banlist; ban; ban = ban->next)
    441 	{
    442 		e = json_object();
    443 		json_array_append_new(list, e);
    444 		json_object_set_new(e, "name", json_string_unreal(ban->banstr));
    445 		json_object_set_new(e, "set_by", json_string_unreal(ban->who));
    446 		json_object_set_new(e, "set_at", json_timestamp(ban->when));
    447 	}
    448 }
    449 
    450 
    451 /* detail=1 adds bans, ban_exemptions and invite_exceptions
    452  * detail=2 adds members
    453  * detail=3+ makes the members more detailed
    454  */
    455 void json_expand_channel(json_t *j, const char *key, Channel *channel, int detail)
    456 {
    457 	char mode1[512], mode2[512], modes[512];
    458 	json_t *child;
    459 
    460 	if (key)
    461 	{
    462 		child = json_object();
    463 		json_object_set_new(j, key, child);
    464 	} else {
    465 		child = j;
    466 	}
    467 
    468 	json_object_set_new(child, "name", json_string_unreal(channel->name));
    469 	if (detail == 0)
    470 		return;
    471 
    472 	json_object_set_new(child, "creation_time", json_timestamp(channel->creationtime));
    473 	json_object_set_new(child, "num_users", json_integer(channel->users));
    474 	if (channel->topic)
    475 	{
    476 		json_object_set_new(child, "topic", json_string_unreal(channel->topic));
    477 		json_object_set_new(child, "topic_set_by", json_string_unreal(channel->topic_nick));
    478 		json_object_set_new(child, "topic_set_at", json_timestamp(channel->topic_time));
    479 	}
    480 
    481 	/* Add "mode" too */
    482 	channel_modes(NULL, mode1, mode2, sizeof(mode1), sizeof(mode2), channel, 0);
    483 	if (*mode2)
    484 	{
    485 		snprintf(modes, sizeof(modes), "%s %s", mode1+1, mode2);
    486 		json_object_set_new(child, "modes", json_string_unreal(modes));
    487 	} else {
    488 		json_object_set_new(child, "modes", json_string_unreal(mode1+1));
    489 	}
    490 
    491 	if (detail > 1)
    492 	{
    493 		json_expand_channel_ban(child, "bans", channel->banlist);
    494 		json_expand_channel_ban(child, "ban_exemptions", channel->exlist);
    495 		json_expand_channel_ban(child, "invite_exceptions", channel->invexlist);
    496 	}
    497 
    498 	if (detail >= 3)
    499 	{
    500 		Member *u;
    501 		json_t *list = json_array();
    502 		json_object_set_new(child, "members", list);
    503 
    504 		for (u = channel->members; u; u = u->next)
    505 		{
    506 			json_t *e = json_object();
    507 			if (*u->member_modes)
    508 				json_object_set_new(e, "level", json_string_unreal(u->member_modes));
    509 			json_expand_client(e, NULL, u->client, detail-3);
    510 			json_array_append_new(list, e);
    511 		}
    512 	}
    513 
    514 	// Possibly later: If detail is set to 1 then expand more...
    515 	RunHook(HOOKTYPE_JSON_EXPAND_CHANNEL, channel, detail, child);
    516 }
    517 
    518 void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail)
    519 {
    520 	char buf[BUFSIZE];
    521 	json_t *j;
    522 
    523 	if (key)
    524 	{
    525 		j = json_object();
    526 		json_object_set_new(root, key, j);
    527 	} else {
    528 		j = root;
    529 	}
    530 
    531 	json_object_set_new(j, "type", json_string_unreal(tkl_type_config_string(tkl))); // Eg 'kline'
    532 	json_object_set_new(j, "type_string", json_string_unreal(tkl_type_string(tkl))); // Eg 'Soft K-Line'
    533 	json_object_set_new(j, "set_by", json_string_unreal(tkl->set_by));
    534 	json_object_set_new(j, "set_at", json_timestamp(tkl->set_at));
    535 	json_object_set_new(j, "expire_at", json_timestamp(tkl->expire_at));
    536 	*buf = '\0';
    537 	short_date(tkl->set_at, buf);
    538 	strlcat(buf, " GMT", sizeof(buf));
    539 	json_object_set_new(j, "set_at_string", json_string_unreal(buf));
    540 	if (tkl->expire_at <= 0)
    541 	{
    542 		json_object_set_new(j, "expire_at_string", json_string_unreal("Never"));
    543 		json_object_set_new(j, "duration_string", json_string_unreal("permanent"));
    544 	} else {
    545 		*buf = '\0';
    546 		short_date(tkl->expire_at, buf);
    547 		strlcat(buf, " GMT", sizeof(buf));
    548 		json_object_set_new(j, "expire_at_string", json_string_unreal(buf));
    549 		json_object_set_new(j, "duration_string", json_string_unreal(pretty_time_val_r(buf, sizeof(buf), tkl->expire_at - tkl->set_at)));
    550 	}
    551 	json_object_set_new(j, "set_at_delta", json_integer(TStime() - tkl->set_at));
    552 	if (tkl->flags & TKL_FLAG_CONFIG)
    553 		json_object_set_new(j, "set_in_config", json_boolean(1));
    554 	if (TKLIsServerBan(tkl))
    555 	{
    556 		json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0)));
    557 		json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.serverban->reason));
    558 	} else
    559 	if (TKLIsNameBan(tkl))
    560 	{
    561 		json_object_set_new(j, "name", json_string_unreal(tkl->ptr.nameban->name));
    562 		json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.nameban->reason));
    563 	} else
    564 	if (TKLIsBanException(tkl))
    565 	{
    566 		json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0)));
    567 		json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.banexception->reason));
    568 		json_object_set_new(j, "exception_types", json_string_unreal(tkl->ptr.banexception->bantypes));
    569 	} else
    570 	if (TKLIsSpamfilter(tkl))
    571 	{
    572 		json_object_set_new(j, "name", json_string_unreal(tkl->ptr.spamfilter->match->str));
    573 		json_object_set_new(j, "match_type", json_string_unreal(unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type)));
    574 		json_object_set_new(j, "ban_action", json_string_unreal(banact_valtostring(tkl->ptr.spamfilter->action)));
    575 		json_object_set_new(j, "ban_duration", json_integer(tkl->ptr.spamfilter->tkl_duration));
    576 		json_object_set_new(j, "ban_duration_string", json_string_unreal(pretty_time_val_r(buf, sizeof(buf), tkl->ptr.spamfilter->tkl_duration)));
    577 		json_object_set_new(j, "spamfilter_targets", json_string_unreal(spamfilter_target_inttostring(tkl->ptr.spamfilter->target)));
    578 		json_object_set_new(j, "reason", json_string_unreal(unreal_decodespace(tkl->ptr.spamfilter->tkl_reason)));
    579 	}
    580 }