blob: a0e0768f7ea37f6bd905d0f84c490f4b12be70e7 [file] [log] [blame]
// channeld.c
//
// $Id: channeld.c 9138 2015-02-03 21:46:56Z Zesstra $
//
#pragma strong_types
#pragma no_shadow // keine Shadowing...
#pragma no_clone
#pragma no_inherit
#pragma save_types
#include <sys_debug.h>
#include <lpctypes.h>
#include <wizlevels.h>
#include <properties.h>
#include <config.h>
#include <language.h>
#define NEED_PROTOTYPES
#include "channel.h"
#define CMNAME "<MasteR>"
#define CHANNEL_SAVE "/p/daemon/save/channeld"
#define MEMORY "/secure/memory"
#define TIMEOUT (time() - 60)
#define CMDS ({C_FIND, C_LIST, C_JOIN, C_LEAVE, C_SEND, C_NEW})
/* list of channels and their corresponding data (members, etc.)
channels = ([string channelname : ({ ({object* members}),
closure access_rights,
string channel_info,
string|object master_object,
string channelname }) ]) */
private nosave mapping channels;
//private nosave mapping lowerch; // unused
/* channel history
mapping channelH = ([ string channelname : ({ string channelname,
string sender,
string msg,
int msg_type }) ]) */
private nosave mapping channelH;
/* list of global channelmaster stats
mapping stats: ([ "time" : int object_time(),
"boot" : string load_name(previous_object()),
"new" : int total_channels_created,
"disposed" : int total_channels_removed ]) */
private nosave mapping stats;
/* channel cache
mapping channelC = ([ string channelname : ({ string I_NAME,
string I_INFO,
int time() }) ]) */
private mapping channelC;
/* list of players' banned commands, if any
mapping channelB = ([ string playername : ({ string* banned_commands })])*/
private mapping channelB;
/* timeout cache for player commands (timeout = 60 s, see above)
ensures that the same command is only executed once per minute, max
mapping Tcmd = ([ "lag": int timestamp,
"uptime": int timestamp,
"statistik": int timestamp]) */
private mapping Tcmd = ([]);
/* Flag to indicate that data changes have occurred and that we need saving
in the next save_object() run.
set to 0 or 1 */
private int save_me_soon;
// BEGIN OF THE CHANNEL MASTER ADMINISTRATIVE PART
#define RECV 0
#define SEND 1
#define FLAG 2
// Channel flags
// Levelbeschraenkungen gegen Magierlevel (query_wiz_level) pruefen, nicht
// P_LEVEL.
#define F_WIZARD 1
// Keine Gaeste. ;-)
#define F_NOGUEST 2
private nosave mapping admin = m_allocate(0, 3);
int check(string ch, object pl, string cmd)
{
int level;
if ((admin[ch, FLAG] & F_NOGUEST) && pl->QueryGuest())
return 0;
if ((admin[ch, FLAG] & F_WIZARD) && query_wiz_level(pl) < SEER_LVL)
return 0;
level = (admin[ch, FLAG] & F_WIZARD
? query_wiz_level(pl)
: pl->QueryProp(P_LEVEL));
switch (cmd)
{
case C_FIND:
case C_LIST:
case C_JOIN:
if (admin[ch, RECV] == -1)
return 0;
if (admin[ch, RECV] <= level)
return 1;
break;
case C_SEND:
if (admin[ch, SEND] == -1)
return 0;
if (admin[ch, SEND] <= level)
return 1;
break;
case C_LEAVE:
return 1;
default:
break;
}
return (0);
}
private int CountUser(mapping l)
{
mapping n = ([]);
walk_mapping(l, function void (string chan_name, mixed * chan_data)
{
n += mkmapping(chan_data[I_MEMBER]);
});
return sizeof(n);
}
private void banned(string n, string* cmds, string res)
{
res += sprintf("%s [%s], ", capitalize(n), implode(cmds, ","));
}
void ChannelMessage(mixed msg)
{
string ret, mesg;
mixed lag;
int max, rekord;
string tmp;
if (msg[1] == this_object() || !stringp(msg[2]) ||
msg[0] != CMNAME || previous_object() != this_object())
return;
mesg = lower_case(msg[2]);
if (!strstr("hilfe", mesg) && sizeof(mesg) <= 5)
{
ret = "Folgende Kommandos gibt es: hilfe, lag, up[time], statistik, bann";
}
else if (!strstr("lag", mesg) && sizeof(mesg) <= 3)
{
// TODO: invert logic to make this better understandable, e.g.
// if ( CMD_AVAILABLE("lag") ) and rearrange code block
if (Tcmd["lag"] > TIMEOUT)
return;
Tcmd["lag"] = time();
lag = "/p/daemon/lag-o-daemon"->read_ext_lag_data();
ret = sprintf("Lag: %.1f%%/60, %.1f%%/15, %.1f%%/5, %.1f%%/1, "
"%.1f%%/20s, %.1f%%/2s",
lag[5], lag[4], lag[3], lag[2], lag[1], lag[0]);
call_out(#'send, 2, CMNAME, this_object(), ret);
ret = query_load_average();
}
// TODO: move this logic to a function with a self-explanatory name
else if (!strstr("uptime", mesg) && sizeof(mesg) <= 6)
{
if (Tcmd["uptime"] > TIMEOUT)
return;
Tcmd["uptime"] = time();
if (file_size("/etc/maxusers") <= 0)
{
ret = "Diese Information liegt nicht vor.";
}
else
{
sscanf(read_file("/etc/maxusers"), "%d %s", max, tmp);
sscanf(read_file("/etc/maxusers.ever"), "%d %s", rekord, tmp);
ret = sprintf("Das MUD laeuft jetzt %s. Es sind momentan %d Spieler "
"eingeloggt; das Maximum lag heute bei %d und der Rekord "
"bisher ist %d.", uptime(), sizeof(users()), max, rekord);
}
}
else if (!strstr("statistik", mesg) && sizeof(mesg) <= 9)
{
if (Tcmd["statistik"] > TIMEOUT)
return;
Tcmd["statistik"] = time();
ret = sprintf(
"Im Moment sind insgesamt %d Ebenen mit %d Teilnehmern aktiv.\n"
"Der %s wurde das letzte mal am %s von %s neu gestartet.\n"
"Seitdem wurden %d Ebenen neu erzeugt und %d zerstoert.\n",
sizeof(channels), CountUser(channels), CMNAME,
dtime(stats["time"]), stats["boot"], stats["new"], stats["dispose"]);
}
else if (!strstr(mesg, "bann"))
{
string pl, cmd;
if (mesg == "bann")
{
if (sizeof(channelB))
{
ret = "";
walk_mapping(channelB, #'banned, &ret);
ret = "Fuer folgende Spieler besteht ein Bann: " + ret;
}
else
{
ret = "Zur Zeit ist kein Bann aktiv.";
}
}
else
{
if (sscanf(mesg, "bann %s %s", pl, cmd) == 2 && IS_DEPUTY(msg[1]))
{
pl = lower_case(pl);
cmd = lower_case(cmd);
if (member(CMDS, cmd) != -1)
{
if (!pointerp(channelB[pl]))
channelB[pl] = ({});
if (member(channelB[pl], cmd) != -1)
channelB[pl] -= ({ cmd });
else
channelB[pl] += ({ cmd });
ret = "Fuer '" + capitalize(pl) + "' besteht "
+ (sizeof(channelB[pl]) ?
"folgender Bann: " + implode(channelB[pl], ", ") :
"kein Bann mehr.");
if (!sizeof(channelB[pl]))
channelB = m_copy_delete(channelB, pl);
save_object(CHANNEL_SAVE);
}
else
{
ret = "Das Kommando '" + cmd + "' ist unbekannt. "
"Erlaubte Kommandos: "+ implode(CMDS, ", ");
}
}
else
{
if (!IS_ARCH(msg[1]))
return;
else
ret = "Syntax: bann <name> <kommando>";
}
}
}
else if (mesg == "lust")
{
mixed t, up;
if (Tcmd["lag"] > TIMEOUT ||
Tcmd["statistik"] > TIMEOUT ||
Tcmd["uptime"] > TIMEOUT)
return;
Tcmd["lag"] = time();
Tcmd["statistik"] = time();
Tcmd["uptime"] = time();
lag = "/p/daemon/lag-o-daemon"->read_lag_data();
sscanf(read_file("/etc/maxusers"), "%d %s", max, tmp);
sscanf(read_file("/etc/maxusers.ever"), "%d %s", rekord, tmp);
t = time() - last_reboot_time();
up = "";
if (t >= 86400)
up += sprintf("%dT", t / 86400);
if (t >= 3600)
up += sprintf("%dh", (t = t % 86400) / 3600);
if (t > 60)
up += sprintf("%dm", (t = t % 3600) / 60);
up += sprintf("%ds", t % 60);
ret = sprintf("%.1f%%/15 %.1f%%/1 %s %d:%d:%d E:%d T:%d",
lag[1], lag[2], up, sizeof(users()), max, rekord,
sizeof(channels), CountUser(channels));
}
else
{
return;
}
call_out(#'send, 2, CMNAME, this_object(), ret);
}
// setup() -- set up a channel and register it
// arguments are stored in the following order:
// ({ channel name,
// receive level, send level,
// flags,
// description,
// master obj
// })
private void setup(mixed c)
{
closure cl;
object m;
string d;
d = "- Keine Beschreibung -";
m = this_object();
if (sizeof(c) && sizeof(c[0]) > 1 && c[0][0] == '\\')
c[0] = c[0][1..];
switch (sizeof(c))
{
case 6:
if (!stringp(c[5]) || !sizeof(c[5]) ||
(catch(m = load_object(c[5]); publish) ||
!objectp(m)))
m = this_object();
case 5:
d = stringp(c[4]) || closurep(c[4]) ? c[4] : d;
case 4:
admin[c[0], FLAG] = to_int(c[3]);
case 3:
admin[c[0], SEND] = to_int(c[2]);
case 2:
admin[c[0], RECV] = to_int(c[1]);
break;
case 0:
default:
return;
}
switch (new(c[0], m, d))
{
case E_ACCESS_DENIED:
log_file("CHANNEL", sprintf("[%s] %s: %O: error, access denied\n",
dtime(time()), c[0], m));
break;
default:
break;
}
return;
}
void initialize()
{
mixed tmp;
#if !defined(__TESTMUD__) && MUDNAME=="MorgenGrauen"
tmp = read_file(object_name(this_object()) + ".init");
#else
tmp = read_file(object_name(this_object()) + ".init.testmud");
#endif
if (!stringp(tmp))
return;
tmp = regexp(old_explode(tmp, "\n"), "^[^#]");
tmp = map(tmp, #'regexplode, "[^:][^:]*$|[ \\t]*:[ \\t]*");
tmp = map(tmp, #'regexp, "^[^: \\t]");
map(tmp, #'setup);
}
// BEGIN OF THE CHANNEL MASTER IMPLEMENTATION
protected void create()
{
seteuid(getuid());
restore_object(CHANNEL_SAVE);
if (!channelC)
channelC = ([]);
if (!channelB)
channelB = ([]);
channels = ([]);
/* Die Channel-History wird nicht nur lokal sondern auch noch im Memory
gespeichert, dadurch bleibt sie auch ueber ein Reload erhalten.
Der folgende Code versucht, den Zeiger aus dem Memory zu holen. Falls
das nicht moeglich ist, wird ein neuer erzeugt und gegebenenfalls im
Memory abgelegt. */
// Hab ich die noetigen Rechte im Memory?
if (call_other(MEMORY, "HaveRights"))
{
// Objektpointer laden
channelH = (mixed) call_other(MEMORY, "Load", "History");
// Wenns nich geklappt hat, hat der Memory noch keinen Zeiger, dann
if (!mappingp(channelH)) {
// Zeiger erzeugen
channelH = ([]);
// und in den Memory schreiben
call_other(MEMORY, "Save", "History", channelH);
}
}
else
{
// Keine Rechte im Memory, dann wird mit einem lokalen Zeiger gearbeitet.
channelH = ([]);
}
stats = (["time": time(),
"boot": capitalize(getuid(previous_object()) || "<Unbekannt>")]);
new (CMNAME, this_object(), "Zentrale Informationen zu den Ebenen");
initialize();
map_objects(efun::users(), "RegisterChannels");
this_object()->send(CMNAME, this_object(),
sprintf("%d Ebenen mit %d Teilnehmern initialisiert.",
sizeof(channels),
CountUser(channels)));
}
// reset() and cache_to() - Cache Timeout, remove timed out cached channels
// SEE: new, send
private int cache_to(string key, mapping m, int t)
{
if (!pointerp(m[key]) || m[key][2] + 43200 > t)
return 1;
return (0);
}
varargs void reset(int nonstd)
{
channelC = filter_indices(channelC, #'cache_to, channelC, time());
if (save_me_soon)
{
save_me_soon = 0;
save_object(CHANNEL_SAVE);
}
}
// name() - define the name of this object.
string name()
{
return CMNAME;
}
string Name()
{
return CMNAME;
}
// access() - check access by looking for the right argument types and
// calling access closures respectively
// SEE: new, join, leave, send, list, users
// Note: <pl> is usually an object, only the master supplies a string during
// runtime error handling.
varargs private int access(mixed ch, mixed pl, string cmd, string txt)
{
mixed co, m;
if (!stringp(ch) || !sizeof(ch = lower_case(ch)) || !channels[ch])
return 0;
if (!channels[ch][I_ACCESS] || !previous_object(1) || !extern_call() ||
previous_object(1) == this_object() ||
(stringp(channels[ch][I_MASTER]) &&
previous_object(1) == find_object(channels[ch][I_MASTER])) ||
getuid(previous_object(1)) == ROOTID)
return 2;
if (!objectp(pl) ||
((previous_object(1) != pl) && (previous_object(1) != this_object())))
return 0;
if (pointerp(channelB[getuid(pl)]) &&
member(channelB[getuid(pl)], cmd) != -1)
return 0;
if (stringp(channels[ch][I_MASTER]) &&
(!(m = find_object(channels[ch][I_MASTER])) ||
(!to_object(channels[ch][I_ACCESS]) ||
get_type_info(channels[ch][I_ACCESS])[1])))
{
string err;
if (!objectp(m))
err = catch(load_object(channels[ch][I_MASTER]); publish);
if (!err &&
((!to_object(channels[ch][I_ACCESS]) ||
get_type_info(channels[ch][I_ACCESS])[1]) &&
!closurep(channels[ch][I_ACCESS] =
symbol_function("check", find_object(channels[ch][I_MASTER])))))
{
log_file("CHANNEL", sprintf("[%s] %O -> %O\n",
dtime(time()), channels[ch][I_MASTER],
err));
channels = m_copy_delete(channels, ch);
return 0;
}
this_object()->join(ch, find_object(channels[ch][I_MASTER]));
}
if (closurep(channels[ch][I_ACCESS]))
return funcall(channels[ch][I_ACCESS],
channels[ch][I_NAME], pl, cmd, &txt);
}
// new() - create a new channel
// a channel with name 'ch' is created, the player is the master
// info may contain a string which describes the channel or a closure
// to display up-to-date information, check may contain a closure
// called when a join/leave/send/list/users message is received
// SEE: access
#define IGNORE "^/xx"
varargs int new(string ch, object pl, mixed info)
{
mixed pls;
if (!objectp(pl) || !stringp(ch) || !sizeof(ch) ||
channels[lower_case(ch)] || (pl == this_object() && extern_call()) ||
sizeof(channels) >= MAX_CHANNELS ||
sizeof(regexp(({ object_name(pl) }), IGNORE)) ||
(pointerp(channelB[getuid(pl)]) &&
member(channelB[getuid(pl)], C_NEW) != -1))
return E_ACCESS_DENIED;
if (!info)
{
if (channelC[lower_case(ch)])
{
ch = channelC[lower_case(ch)][0];
info = channelC[lower_case(ch)][1];
}
else
{
return E_ACCESS_DENIED;
}
}
else
{
channelC[lower_case(ch)] = ({ ch, info, time() });
}
pls = ({ pl });
channels[lower_case(ch)] = ({
pls,
symbol_function("check", pl) || #'check, info,
(!living(pl) && !clonep(pl) && pl != this_object()
? object_name(pl)
: pl),
ch});
// ChannelH fuer einen Kanal nur dann initialisieren, wenn es sie noch nich gibt.
if (!pointerp(channelH[lower_case(ch)]))
channelH[lower_case(ch)] = ({});
if (pl != this_object())
log_file("CHANNEL.new", sprintf("[%s] %O: %O %O\n",
dtime(time()), ch, pl, info));
if (!pl->QueryProp(P_INVIS))
this_object()->send(CMNAME, pl,
"laesst die Ebene '" + ch + "' entstehen.", MSG_EMOTE);
stats["new"]++;
save_me_soon = 1;
return (0);
}
// join() - join a channel
// this function checks whether the player 'pl' is allowed to join
// the channel 'ch' and add if successful, one cannot join a channel
// twice
// SEE: leave, access
int join(string ch, object pl)
{
if (!funcall(#'access,&ch, pl, C_JOIN))
return E_ACCESS_DENIED;
if (member(channels[ch][I_MEMBER], pl) != -1)
return E_ALREADY_JOINED;
channels[ch][I_MEMBER] += ({ pl });
return (0);
}
// leave() - leave a channel
// the access check in this function is just there for completeness
// one should always be allowed to leave a channel.
// if there are no players left on the channel it will vanish, unless
// its master is this object.
// SEE: join, access
int leave(string ch, object pl)
{
int pos;
if (!funcall(#'access,&ch, pl, C_LEAVE))
return E_ACCESS_DENIED;
channels[ch][I_MEMBER] -= ({0}); // kaputte Objekte erstmal raus
if ((pos = member(channels[ch][I_MEMBER], pl)) == -1)
return E_NOT_MEMBER;
if (pl == channels[ch][I_MASTER] && sizeof(channels[ch][I_MEMBER]) > 1)
{
channels[ch][I_MASTER] = channels[ch][I_MEMBER][1];
if (!pl->QueryProp(P_INVIS))
this_object()->send(ch, pl, "uebergibt die Ebene an " +
channels[ch][I_MASTER]->name(WEN) + ".", MSG_EMOTE);
}
channels[ch][I_MEMBER][pos..pos] = ({ });
if (!sizeof(channels[ch][I_MEMBER]) && !stringp(channels[ch][I_MASTER]))
{
// delete the channel that has no members
if (!pl->QueryProp(P_INVIS))
this_object()->send(CMNAME, pl,
"verlaesst als "+
(pl->QueryProp(P_GENDER) == 1 ? "Letzter" : "Letzte")+
" die Ebene '"+channels[ch][I_NAME]+"', worauf diese sich in "
"einem Blitz oktarinen Lichts aufloest.", MSG_EMOTE);
channelC[lower_case(ch)] =
({ channels[ch][I_NAME], channels[ch][I_INFO], time() });
m_delete(channels, lower_case(ch));
// Wird ein Channel entfernt, wird auch seine History geloescht
// TODO: this foils the attempts at creating a persistent channel
// history via /secure/memory.c
channelH = m_copy_delete(channelH, lower_case(ch));
stats["dispose"]++;
save_me_soon = 1;
}
return (0);
}
// send() - send a message to all recipients of the specified channel 'ch'
// checks if 'pl' is allowed to send a message and sends if success-
// ful a message with type 'type'
// 'pl' must be an object, the message is attributed to it. e.g.
// ignore checks use it. It can be != previous_object()
// SEE: access, ch.h
varargs int send(string ch, object pl, string msg, int type)
{
int a;
if (!(a = funcall(#'access, &ch, pl, C_SEND, &msg)))
return E_ACCESS_DENIED;
if (a < 2 && member(channels[ch][I_MEMBER], pl) == -1)
return E_NOT_MEMBER;
if (!msg || !stringp(msg) || !sizeof(msg))
return E_EMPTY_MESSAGE;
map_objects(channels[ch][I_MEMBER], "ChannelMessage",
({ channels[ch][I_NAME], pl, msg, type }));
if (sizeof(channelH[ch]) > MAX_HIST_SIZE)
channelH[ch] = channelH[ch][1..];
channelH[ch] +=
({ ({ channels[ch][I_NAME],
(stringp(pl)
? pl
: (pl->QueryProp(P_INVIS)
? "/(" + capitalize(getuid(pl)) + ")$"
: "")
+ (pl->Name(WER, 2) || "<Unbekannt>")),
msg + " <" + strftime("%a, %H:%M:%S") + ">\n",
type }) });
return (0);
}
private void clean(string n, mixed a)
{
a[0] -= ({ 0 });
}
// list() - list all channels, that are at least receivable by 'pl'
// returns a mapping,
// SEE: access, channels
mixed list(object pl)
{
mapping chs = filter_indices(channels, #'access, pl, C_LIST);
walk_mapping(chs, #'clean);
if (!sizeof(chs))
return E_ACCESS_DENIED;
return deep_copy(chs);
}
// find() - find a channel by its name (may be partial)
// returns an array for multiple results and 0 for no matching name
// SEE: access
mixed find(string ch, object pl)
{
mixed chs, s;
if (stringp(ch))
ch = lower_case(ch);
if (!sizeof(regexp(({ch}), "^[<>a-z0-9#-]*$")))
return 0; // RUM
if (!sizeof(chs = regexp(m_indices(channels), "^" + ch + "$")))
chs = regexp(m_indices(channels), "^" + ch);
if ((s = sizeof(chs)) > 1)
{
if (sizeof(chs = filter(chs, #'access, pl, C_FIND)) == 1)
return channels[chs[0]][I_NAME];
else
return chs;
}
return ((s && funcall(#'access,chs[0], pl, C_FIND))
? channels[chs[0]][I_NAME]
: 0);
}
// history() - get the history of a channel
// SEE: access
mixed history(string ch, object pl)
{
if (!funcall(#'access, &ch, pl, C_JOIN))
return E_ACCESS_DENIED;
return deep_copy(channelH[ch]);
}
// remove - remove a channel (wird aus der Shell aufgerufen)
// SEE: new
mixed remove(string ch, object pl)
{
mixed members;
if (previous_object() != this_object())
{
if (!stringp(ch) ||
pl != this_player() || this_player() != this_interactive() ||
this_interactive() != previous_object() ||
!IS_ARCH(this_interactive()))
return E_ACCESS_DENIED;
}
if (channels[lower_case(ch)])
{
channels[lower_case(ch)][I_MEMBER] =
filter_objects(channels[lower_case(ch)][I_MEMBER],
"QueryProp", P_CHANNELS);
map(channels[lower_case(ch)][I_MEMBER],
function mixed(object listener)
{
string* chans = listener->QueryProp(P_CHANNELS);
chans -= ({lower_case(ch)});
({string*})listener->SetProp(P_CHANNELS, chans);
});
channels = m_copy_delete(channels, lower_case(ch));
// Wird ein Channel entfernt, wird auch seine History geloescht
if (pointerp(channelH[lower_case(ch)]))
channelH = m_copy_delete(channelH, lower_case(ch));
stats["dispose"]++;
}
if (!channelC[lower_case(ch)])
return E_ACCESS_DENIED;
channelC = m_copy_delete(channelC, lower_case(ch));
save_me_soon = 1;
return (0);
}
// Wird aus der Shell aufgerufen
mixed clear_history(string ch)
{
// Sicherheitsabfragen
if (previous_object() != this_object())
{
if (!stringp(ch) ||
this_player() != this_interactive() ||
this_interactive() != previous_object() ||
!IS_ARCH(this_interactive()))
return E_ACCESS_DENIED;
}
// History des Channels loeschen
if (pointerp(channelH[lower_case(ch)]))
channelH[lower_case(ch)] = ({});
return 0;
}