Newer
Older
/* Synchronet user data-related routines (exported) */
/****************************************************************************
* @format.tab-size 4 (Plain Text/Source Code File Header) *
* @format.use-tabs true (see http://www.synchro.net/ptsc_hdr.html) *
* *
* Copyright Rob Swindell - http://www.synchro.net/copyright.html *
* *
* 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 2 *
* of the License, or (at your option) any later version. *
* See the GNU General Public License for more details: gpl.txt or *
* http://www.fsf.org/copyleft/gpl.html *
* *
* For Synchronet coding style and modification guidelines, see *
* http://www.synchro.net/source.html *
* *
* Note: If this box doesn't appear square, then you need to fix your tabs. *
****************************************************************************/
#include "findstr.h"
#include "userdat.h"
#include "filedat.h"
#include "ars_defs.h"
#include "text.h"
#include "nopen.h"
#include "datewrap.h"
#include "date_str.h"
#include "smblib.h"
#include "getstats.h"
#include "msgdate.h"
#include "scfglib.h"
#include "dat_rec.h"
#define VALID_CFG(cfg) (cfg!=NULL && cfg->size==sizeof(scfg_t))
#define VALID_USER_NUMBER(n) ((n) >= 1)
#define VALID_USER_FIELD(n) ((n) >= 0 && (n) < USER_FIELD_COUNT)
#define USER_FIELD_SEPARATOR '\t'
static const char user_field_separator[2] = { USER_FIELD_SEPARATOR, '\0' };
#define LOOP_USERDAT 200
char* userdat_filename(scfg_t* cfg, char* path, size_t size)
{
safe_snprintf(path, size, "%suser/" USER_DATA_FILENAME, cfg->data_dir);
return path;
}
char* msgptrs_filename(scfg_t* cfg, unsigned user_number, char* path, size_t size)
{
safe_snprintf(path, size, "%suser/%4.4u.subs", cfg->data_dir, user_number);
return path;
}
/****************************************************************************/
/****************************************************************************/
void split_userdat(char *userdat, char* field[])
{
char* p = userdat;
for(size_t i = 0; i < USER_FIELD_COUNT; i++) {
field[i] = p;
FIND_CHAR(p, USER_FIELD_SEPARATOR);
if(*p != '\0')
*(p++) = '\0';
}
}
/****************************************************************************/
/* Looks for a perfect match among all usernames (not deleted users) */
/* Makes dots and underscores synonymous with spaces for comparisons */
/* Returns the number of the perfect matched username or 0 if no match */
/****************************************************************************/
uint matchuser(scfg_t* cfg, const char *name, bool sysop_alias)
int file,c;
char dat[LEN_ALIAS+2];
char str[256];
FILE* stream;
if(!VALID_CFG(cfg) || name==NULL || *name == '\0')
if(sysop_alias &&
(!stricmp(name,"SYSOP") || !stricmp(name,"POSTMASTER") || !stricmp(name,cfg->sys_id)))
SAFEPRINTF(str,"%suser/name.dat",cfg->data_dir);
if((stream=fnopen(&file,str,O_RDONLY))==NULL)
length = filelength(file);
if(length < sizeof(dat)) {
fclose(stream);
return 0;
}
for(l = 0; l < length; l += sizeof(dat)) {
if(fread(dat,sizeof(dat),1,stream) != 1)
break;
if(dat[c]==ETX) break;
dat[c]=0;
if(c < 1) // Deleted user
continue;
if(matchusername(cfg, dat, name))
return (uint)((l/(LEN_ALIAS+2))+1);
/****************************************************************************/
/* Return true if the user 'name' (or alias) matches with 'comp' */
/* ... ignoring non-alpha-numeric chars in either string */
/* and terminating the comparison string at an '@' */
/****************************************************************************/
bool matchusername(scfg_t* cfg, const char* name, const char* comp)
{
(void)cfg; // in case we want this matching logic to be configurable in the future
const char* np = name;
const char* cp = comp;
while(*np != '\0' && *cp != '\0' && *cp != '@') {
if(!IS_ALPHANUMERIC(*np)) {
if(!IS_ALPHANUMERIC(*cp)) {
cp++;
continue;
}
if(toupper(*np) != toupper(*cp))
break;
np++, cp++;
}
while(*np != '\0' && !IS_ALPHANUMERIC(*np))
np++;
while(*cp != '\0' && !IS_ALPHANUMERIC(*cp) && *cp != '@' )
cp++;
return *np == '\0' && (*cp == '\0' || *cp == '@');
}
/****************************************************************************/
// Given a login-ID (user number, alias, or real name), return the user
// number or 0 on failure
/****************************************************************************/
uint find_login_id(scfg_t* cfg, const char* user_id)
{
uint usernum;
if(cfg == NULL || user_id == NULL)
return 0;
if((cfg->sys_login & LOGIN_USERNUM) && IS_DIGIT(user_id[0])) {
char* end = NULL;
usernum = (uint)strtoul(user_id, &end, 10);
if(end == NULL || *end != '\0' || usernum > (uint)lastuser(cfg))
usernum = 0;
return usernum;
}
usernum = matchuser(cfg, user_id, /* sysop_alias: */false);
if(usernum < 1 && check_realname(cfg, user_id) && (cfg->sys_login & LOGIN_REALNAME))
usernum = finduserstr(cfg, 0, USER_NAME, user_id
,/* del: */false, /* next: */false
,/* Progress_cb: */NULL, /* cbdata: */NULL);
return usernum;
}
/****************************************************************************/
int total_users(scfg_t* cfg)
{
int total_users=0;
int file;
if(!VALID_CFG(cfg))
return(0);
if((file=openuserdat(cfg, /* for_modify: */false)) < 0)
for(int usernumber = 1; success; usernumber++) {
char userdat[USER_RECORD_LEN + 1];
if(readuserdat(cfg, usernumber, userdat, sizeof(userdat), file, /* leave_locked: */false) == 0) {
char* field[USER_FIELD_COUNT];
split_userdat(userdat, field);
if(!(ahtou32(field[USER_MISC]) & (DELETED|INACTIVE)))
total_users++;
}
close(file);
return total_users;
}
/****************************************************************************/
/* Returns the number of the last user in user.tab (deleted ones too) */
/****************************************************************************/
int lastuser(scfg_t* cfg)
char path[MAX_PATH + 1];
off_t length;
if(!VALID_CFG(cfg))
return(0);
if((length = flength(userdat_filename(cfg, path, sizeof(path)))) > 0)
return (int)(length / USER_RECORD_LINE_LEN);
/****************************************************************************/
/* Deletes (completely removes) last user record in userbase */
/****************************************************************************/
if((file=openuserdat(cfg, /* for_modify: */true)) < 0)
return(false);
length = filelength(file);
if(length < USER_RECORD_LINE_LEN) {
int result = chsize(file, (long)length - USER_RECORD_LINE_LEN);
/****************************************************************************/
/* Opens the user database returning the file descriptor or -1 on error */
/****************************************************************************/
int openuserdat(scfg_t* cfg, bool for_modify)
return nopen(userdat_filename(cfg, path, sizeof(path)), for_modify ? (O_RDWR|O_CREAT|O_DENYNONE) : (O_RDONLY|O_DENYNONE));
int closeuserdat(int file)
if(file < 0)
return USER_INVALID_ARG;
return close(file);
}
off_t userdatoffset(unsigned user_number)
{
return (user_number - 1) * USER_RECORD_LINE_LEN;
bool seekuserdat(int file, unsigned user_number)
{
return lseek(file, userdatoffset(user_number), SEEK_SET) == userdatoffset(user_number);
}
bool lockuserdat(int file, unsigned user_number)
{
if(!VALID_USER_NUMBER(user_number))
off_t offset = userdatoffset(user_number);
unsigned attempt=0;
while(attempt < LOOP_USERDAT && lock(file, offset, USER_RECORD_LINE_LEN) == -1) {
attempt++;
FILE_RETRY_DELAY(attempt);
}
return attempt < LOOP_USERDAT;
bool unlockuserdat(int file, unsigned user_number)
{
if(!VALID_USER_NUMBER(user_number))
return unlock(file, userdatoffset(user_number), USER_RECORD_LINE_LEN) == 0;
/****************************************************************************/
/* Locks and reads a single user record from an open userbase file into a */
/* buffer of USER_RECORD_LINE_LEN in size. */
/* Returns 0 on success. */
/****************************************************************************/
int readuserdat(scfg_t* cfg, unsigned user_number, char* userdat, size_t size, int infile, bool leave_locked)
if(!VALID_CFG(cfg) || !VALID_USER_NUMBER(user_number))
return USER_INVALID_ARG;
memset(userdat, 0, size);
if(infile >= 0)
if((file = openuserdat(cfg, /* for_modify: */false)) < 0)
if(user_number > (unsigned)(filelength(file) / USER_RECORD_LINE_LEN)) {
return USER_INVALID_NUM; /* no such user record */
}
if(!seekuserdat(file, user_number)) {
if(file != infile)
close(file);
return USER_SEEK_ERROR;

rswindell
committed
}
if(!lockuserdat(file, user_number)) {
return USER_LOCK_ERROR;
if(read(file, userdat, size - 1) != size - 1) {
unlockuserdat(file, user_number);
return USER_READ_ERROR;
if(!leave_locked)
unlockuserdat(file, user_number);
// Assumes file already positioned at beginning of user record
bool writeuserfields(scfg_t* cfg, char* field[], int file)
{
char userdat[USER_RECORD_LINE_LEN + 1] = "";
for(size_t i = 0; i < USER_FIELD_COUNT; i++) {
SAFECAT(userdat, field[i]);
SAFECAT(userdat, user_field_separator);
}
size_t len = strlen(userdat);
memset(userdat + len, USER_FIELD_SEPARATOR, USER_RECORD_LEN - len);
userdat[USER_RECORD_LINE_LEN - 1] = '\n';
if(write(file, userdat, USER_RECORD_LINE_LEN) != USER_RECORD_LINE_LEN)
}
static time32_t parse_usertime(const char* str)
{
time_t result = xpDateTime_to_time(isoDateTimeStr_parse(str));
if(result == INVALID_TIME)
result = 0;
return (time32_t)result;
}
/****************************************************************************/
/* Fills the structure 'user' with info for user.number from userdat */
/* (a buffer representing a single user 'record' from the userbase file */
/****************************************************************************/
int parseuserdat(scfg_t* cfg, char *userdat, user_t *user, char* field[])
{
unsigned user_number;
if(user==NULL)
return USER_INVALID_ARG;
user_number=user->number;
memset(user,0,sizeof(user_t));
if(!VALID_CFG(cfg) || !VALID_USER_NUMBER(user_number))
return USER_INVALID_ARG;
/* The user number needs to be set here
before calling chk_ar() below for user-number comparisons in AR strings to function correctly */
user->number=user_number; /* Signal of success */
char* fbuf[USER_FIELD_COUNT];
if(field == NULL)
field = fbuf;
SAFECOPY(user->alias, field[USER_ALIAS]);
SAFECOPY(user->name, field[USER_NAME]);
SAFECOPY(user->handle, field[USER_HANDLE]);

Rob Swindell
committed
SAFECOPY(user->lang, field[USER_LANG]);
SAFECOPY(user->note, field[USER_NOTE]);
SAFECOPY(user->ipaddr, field[USER_IPADDR]);
SAFECOPY(user->comp, field[USER_HOST]);
SAFECOPY(user->netmail, field[USER_NETMAIL]);
SAFECOPY(user->address, field[USER_ADDRESS]);
SAFECOPY(user->location, field[USER_LOCATION]);
SAFECOPY(user->zipcode, field[USER_ZIPCODE]);
SAFECOPY(user->phone, field[USER_PHONE]);
SAFECOPY(user->birth, field[USER_BIRTH]);
SAFECOPY(user->comment, field[USER_COMMENT]);
SAFECOPY(user->modem, field[USER_CONNECTION]);
user->misc = (uint32_t)strtoul(field[USER_MISC], NULL, 16);
user->qwk = (uint32_t)strtoul(field[USER_QWK], NULL, 16);
user->chat = (uint32_t)strtoul(field[USER_CHAT], NULL, 16);
user->mail = (uint32_t)strtoul(field[USER_MAIL], NULL, 16);
user->rows = strtoul(field[USER_ROWS], NULL, 0);
user->cols = strtoul(field[USER_COLS], NULL, 0);
user->xedit = getxeditnum(cfg, field[USER_XEDIT]);
user->shell = getshellnum(cfg, field[USER_SHELL]);
SAFECOPY(user->tmpext, field[USER_TMPEXT]);
user->prot = *field[USER_PROT];
SAFECOPY(user->cursub, field[USER_CURSUB]);
SAFECOPY(user->curdir, field[USER_CURDIR]);
SAFECOPY(user->curxtrn, field[USER_CURXTRN]);
user->logontime = parse_usertime(field[USER_LOGONTIME]);
user->ns_time = parse_usertime(field[USER_NS_TIME]);
user->laston = parse_usertime(field[USER_LASTON]);
user->firston = parse_usertime(field[USER_FIRSTON]);
user->logons = (ushort)strtoul(field[USER_LOGONS], NULL, 0);
user->ltoday = (ushort)strtoul(field[USER_LTODAY], NULL, 0);
user->timeon = (ushort)strtoul(field[USER_TIMEON], NULL, 0);
user->ttoday = (ushort)strtoul(field[USER_TTODAY], NULL, 0);
user->tlast = (ushort)strtoul(field[USER_TLAST], NULL, 0);
user->posts = (ushort)strtoul(field[USER_POSTS], NULL, 0);
user->emails = (ushort)strtoul(field[USER_EMAILS], NULL, 0);
user->fbacks = (ushort)strtoul(field[USER_FBACKS], NULL, 0);
user->etoday = (ushort)strtoul(field[USER_ETODAY], NULL, 0);
user->ptoday = (ushort)strtoul(field[USER_PTODAY], NULL, 0);
user->ulb = strtoull(field[USER_ULB], NULL, 0);
user->uls = (ushort)strtoul(field[USER_ULS], NULL, 0);
user->dlb = strtoull(field[USER_DLB], NULL, 0);
user->dls = (ushort)strtoul(field[USER_DLS], NULL, 0);
user->dlcps = (uint32_t)strtoul(field[USER_DLCPS], NULL, 0);
user->leech = (uchar)strtoul(field[USER_LEECH], NULL, 0);
SAFECOPY(user->pass, field[USER_PASS]);
user->pwmod = parse_usertime(field[USER_PWMOD]);
user->flags1 = aftou32(field[USER_FLAGS1]);
user->flags2 = aftou32(field[USER_FLAGS2]);
user->flags3 = aftou32(field[USER_FLAGS3]);
user->flags4 = aftou32(field[USER_FLAGS4]);
user->exempt = aftou32(field[USER_EXEMPT]);
user->rest = aftou32(field[USER_REST]);
user->cdt = strtoull(field[USER_CDT], NULL, 0);
user->freecdt = strtoull(field[USER_FREECDT], NULL, 0);
user->min = strtoul(field[USER_MIN], NULL, 0);
user->textra = (ushort)strtoul(field[USER_TEXTRA], NULL, 0);
user->expire = parse_usertime(field[USER_EXPIRE]);
/* Reset daily stats if not already logged on today */
if(user->ltoday || user->etoday || user->ptoday || user->ttoday) {
time_t now;
struct tm now_tm;
struct tm logon_tm;
now=time(NULL);
if(localtime_r(&now, &now_tm)!=NULL
&& localtime32(&user->logontime, &logon_tm)!=NULL) {
if(now_tm.tm_year!=logon_tm.tm_year
|| now_tm.tm_mon!=logon_tm.tm_mon
|| now_tm.tm_mday!=logon_tm.tm_mday)
resetdailyuserdat(cfg,user,/* write: */false);
}
/****************************************************************************/
/* Fills the structure 'user' with info for user.number from userbase file */
/****************************************************************************/
int getuserdat(scfg_t* cfg, user_t *user)
char userdat[USER_RECORD_LINE_LEN + 1];
if(!VALID_CFG(cfg) || user==NULL || !VALID_USER_NUMBER(user->number))
return USER_INVALID_ARG;
if((file = openuserdat(cfg, /* for_modify: */false)) < 0) {
user->number = 0;
}
if((retval = readuserdat(cfg, user->number, userdat, sizeof(userdat), file, /* leave_locked: */false)) != 0) {
user->number = 0;
retval = parseuserdat(cfg, userdat, user, NULL);
close(file);
return retval;
}
/* Fast getuserdat() (leaves userbase file open) */
int fgetuserdat(scfg_t* cfg, user_t *user, int file)
{
int retval;
char userdat[USER_RECORD_LEN + 1];
if(!VALID_CFG(cfg) || user==NULL || !VALID_USER_NUMBER(user->number))
return USER_INVALID_ARG;
if((retval = readuserdat(cfg, user->number, userdat, sizeof(userdat), file, /* leave_locked: */false)) != 0) {
user->number = 0;
return parseuserdat(cfg, userdat, user, NULL);
/****************************************************************************/
/****************************************************************************/
static void dirtyuserdat(scfg_t* cfg, uint usernumber)
{
int i,file = -1;
node_t node;
for(i=1;i<=cfg->sys_nodes;i++) { /* instant user data update */
// if(i==cfg->node_num)
// continue;
if(getnodedat(cfg, i,&node, /* lockit: */false, &file) != 0)
if(node.useron==usernumber && (node.status==NODE_INUSE
|| node.status==NODE_QUIET)) {
if(getnodedat(cfg, i,&node, /* lockit: */true, &file) == 0) {
node.misc|=NODE_UDAT;
putnodedat(cfg, i,&node, /* closeit: */false, file);
}
}
CLOSE_OPEN_FILE(file);
}
/****************************************************************************/
/****************************************************************************/
int is_user_online(scfg_t* cfg, uint usernumber)
{
int i;
int file = -1;
node_t node;
for(i=1; i<=cfg->sys_nodes; i++) {
getnodedat(cfg, i, &node, /* lockit: */false, &file);
if((node.status==NODE_INUSE || node.status==NODE_QUIET
|| node.status==NODE_LOGON) && node.useron==usernumber)
}
CLOSE_OPEN_FILE(file);
return 0;
}
// Returns empty string if 't' is zero (Unix epoch)
static char* format_datetime(time_t t, char* str, size_t size)
if(t == 0)
*str = '\0';
else
gmtime_to_isoDateTimeStr(t, str, size);
return str;
}
/****************************************************************************/
/****************************************************************************/
bool format_userdat(scfg_t* cfg, user_t* user, char userdat[])
if(!VALID_CFG(cfg) || !VALID_USER_NUMBER(user->number))
char flags1[LEN_FLAGSTR + 1];
char flags2[LEN_FLAGSTR + 1];
char flags3[LEN_FLAGSTR + 1];
char flags4[LEN_FLAGSTR + 1];
char exemptions[LEN_FLAGSTR + 1];
char restrictions[LEN_FLAGSTR + 1];
char logontime[64];
char ns_time[64];
char firston[64];
char laston[64];
char pwmod[64];
char expire[64];
u32toaf(user->flags1, flags1);
u32toaf(user->flags2, flags2);
u32toaf(user->flags3, flags3);
u32toaf(user->flags4, flags4);
u32toaf(user->exempt, exemptions);
u32toaf(user->rest, restrictions);
format_datetime(user->logontime, logontime, sizeof(logontime));
format_datetime(user->ns_time, ns_time, sizeof(ns_time));
format_datetime(user->firston, firston, sizeof(firston));
format_datetime(user->laston, laston, sizeof(laston));
format_datetime(user->pwmod, pwmod, sizeof(pwmod));
format_datetime(user->expire, expire, sizeof(expire));

Rob Swindell
committed
// NOTE: order must match enum user_field definition (in userfields.h)
int len = snprintf(userdat, USER_RECORD_LEN,
"%u\t" // USER_ID
"%s\t" // USER_ALIAS
"%s\t" // USER_NAME
"%s\t" // USER_HANDLE
"%s\t" // USER_NOTE
"%s\t" // USER_IPADDR
"%s\t" // USER_HOST
"%s\t" // USER_NETMAIL
"%s\t" // USER_ADDRESS
"%s\t" // USER_LOCATION
"%s\t" // USER_ZIPCODE
"%s\t" // USER_PHONE
"%s\t" // USER_BIRTH
"%c\t" // USER_GENDER
"%s\t" // USER_COMMENT
"%s\t" // USER_CONNECTION
"%x\t" // USER_MISC
"%x\t" // USER_QWK
"%x\t" // USER_CHAT
"%u\t" // USER_ROWS
"%u\t" // USER_COLS
"%s\t" // USER_XEDIT
"%s\t" // USER_SHELL
"%s\t" // USER_TMPEXT
"%c\t" // USER_PROT
"%s\t" // USER_CURSUB
"%s\t" // USER_CURDIR
"%s\t" // USER_CURXTRN
"%s\t" // USER_LOGONTIME
"%s\t" // USER_NS_TIME
"%s\t" // USER_LASTON
"%s\t" // USER_FIRSTON
"%u\t" // USER_LOGONS
"%u\t" // USER_LTODAY
"%u\t" // USER_TIMEON
"%u\t" // USER_TTODAY
"%u\t" // USER_TLAST
"%u\t" // USER_POSTS
"%u\t" // USER_EMAILS
"%u\t" // USER_FBACKS
"%u\t" // USER_ETODAY
"%u\t" // USER_PTODAY
"%" PRIu64 "\t" // USER_ULB
"%u\t" // USER_ULS
"%" PRIu64 "\t" // USER_DLB
"%u\t" // USER_DLS
"%s\t" // USER_PWMOD
"%u\t" // USER_LEVEL
"%s\t" // USER_FLAGS1
"%s\t" // USER_FLAGS2
"%s\t" // USER_FLAGS3
"%s\t" // USER_FLAGS4
"%s\t" // USER_EXEMPT
"%s\t" // USER_REST
"%" PRIu64 "\t" // USER_CDT
"%" PRIu64 "\t" // USER_FREECDT
"%" PRIu32 "\t" // USER_MIN
"%u\t" // USER_TEXTRA
"%s\t" // USER_EXPIRE
"%x\t" // USER_MAIL

Rob Swindell
committed
"%s\t" // USER_LANG
,user->number
,user->alias
,user->name
,user->handle
,user->note
,user->ipaddr
,user->comp
,user->netmail
,user->address
,user->location
,user->zipcode
,user->phone
,user->birth

Rob Swindell
committed
,user->sex ? user->sex : '?'
,user->comment
,user->modem
,user->misc
,user->qwk
,user->chat
,user->rows
,user->cols
,user->xedit && user->xedit <= cfg->total_xedits ? cfg->xedit[user->xedit - 1]->code : ""
,user->shell < cfg->total_shells ? cfg->shell[user->shell]->code : ""

Rob Swindell
committed
,user->prot ? user->prot : ' '
,user->cursub
,user->curdir
,user->curxtrn
,logontime
,ns_time
,laston
,firston
,(uint)user->ltoday
,(uint)user->timeon
,(uint)user->ttoday
,(uint)user->tlast
,(uint)user->posts
,(uint)user->emails
,(uint)user->fbacks
,(uint)user->etoday
,(uint)user->ptoday
,(uint)user->uls
,(uint)user->dls
,(uint)user->level
,flags1
,flags2
,flags3
,flags4
,exemptions
,restrictions
,user->cdt
,user->freecdt
,user->mail

Rob Swindell
committed
,user->lang
if(len > USER_RECORD_LEN || len < 0) // truncated?
memset(userdat + len, USER_FIELD_SEPARATOR, USER_RECORD_LEN - len);
userdat[USER_RECORD_LINE_LEN - 1] = '\n';
}
/****************************************************************************/
/* Writes into user.number's slot in userbase data in structure 'user' */
/* Called from functions newuser, useredit and main */
/****************************************************************************/
int putuserdat(scfg_t* cfg, user_t* user)
{
int file;
char userdat[USER_RECORD_LINE_LEN];
return USER_INVALID_ARG;
if(!VALID_CFG(cfg) || !VALID_USER_NUMBER(user->number))
return USER_INVALID_ARG;
if(!format_userdat(cfg, user, userdat))
return USER_FORMAT_ERROR;
if((file=openuserdat(cfg, /* for_modify: */true)) < 0)
return USER_OPEN_ERROR;
if(filelength(file)<((off_t)user->number - 1) * USER_RECORD_LINE_LEN) {
close(file);
return USER_INVALID_NUM;
if(!seekuserdat(file, user->number)) {
close(file);
return USER_SEEK_ERROR;
if(!lockuserdat(file, user->number)) {
return USER_LOCK_ERROR;
if(write(file,userdat,sizeof(userdat)) != sizeof(userdat)) {
unlockuserdat(file, user->number);
return USER_WRITE_ERROR;
unlockuserdat(file, user->number);
dirtyuserdat(cfg,user->number);
}
/****************************************************************************/
/* Returns the username in 'str' that corresponds to the 'usernumber' */
/* Called from functions everywhere */
/****************************************************************************/
char* username(scfg_t* cfg, int usernumber, char *name)
char str[256];
int c;
int file;
if(name==NULL)
if(!VALID_CFG(cfg) || !VALID_USER_NUMBER(usernumber)) {
name[0]=0;
SAFEPRINTF(str,"%suser/name.dat",cfg->data_dir);
name[0]=0;
if((file=nopen(str,O_RDONLY))==-1) {
name[0]=0;
if(filelength(file)<(long)((long)usernumber*(LEN_ALIAS+2))) {
close(file);
name[0]=0;
(void)lseek(file,(long)((long)(usernumber-1)*(LEN_ALIAS+2)),SEEK_SET);
if(read(file,name,LEN_ALIAS) != LEN_ALIAS)
memset(name, ETX, LEN_ALIAS);
close(file);
for(c=0;c<LEN_ALIAS;c++)
if(name[c]==ETX) break;
name[c]=0;
strcpy(name,"DELETED USER");
return(name);
/****************************************************************************/
/* Puts 'name' into slot 'number' in user/name.dat */
/****************************************************************************/
int putusername(scfg_t* cfg, int number, const char *name)
{
char str[256];
int file;
off_t length;
off_t total_users;
if(!VALID_CFG(cfg) || name==NULL || !VALID_USER_NUMBER(number))
return USER_INVALID_ARG;
SAFEPRINTF(str,"%suser/name.dat", cfg->data_dir);
if((file=nopen(str,O_RDWR|O_CREAT))==-1)
return USER_OPEN_ERROR;
/* Truncate corrupted name.dat */
total_users=lastuser(cfg);
if(length/(LEN_ALIAS+2) > total_users) {
if(chsize(file,(long)(total_users*(LEN_ALIAS+2))) != 0) {
close(file);
return USER_TRUNC_ERROR;
if(length && length%(LEN_ALIAS+2)) {
close(file);
return USER_SIZE_ERROR;
}
if(length<(((long)number-1)*(LEN_ALIAS+2))) {
SAFEPRINTF2(str,"%*s\r\n",LEN_ALIAS,"");
memset(str,ETX,LEN_ALIAS);
(void)lseek(file,0L,SEEK_END);
while((length = filelength(file)) >= 0 && length < ((long)number*(LEN_ALIAS+2))) // Shouldn't this be (number-1)?
if(write(file,str,(LEN_ALIAS+2)) != LEN_ALIAS+2)
break;
(void)lseek(file,(long)(((long)number-1)*(LEN_ALIAS+2)),SEEK_SET);
putrec(str,0,LEN_ALIAS,name);
putrec(str,LEN_ALIAS,2,"\r\n");
close(file);
return USER_WRITE_ERROR;
return USER_SUCCESS;
#define DECVAL(ch, mul) (DEC_CHAR_TO_INT(ch) * (mul))
int getbirthyear(const char* birth)
return DECVAL(birth[0], 1000)
+ DECVAL(birth[1], 100)
+ DECVAL(birth[2], 10)
+ DECVAL(birth[3], 1);
// DD/MM/YY or MM/DD/YY format
time_t now = time(NULL);
struct tm tm;
if(localtime_r(&now, &tm) == NULL)
return 0;
tm.tm_year += 1900;
int year = 1900 + DECVAL(birth[6], 10) + DECVAL(birth[7], 1);
if(tm.tm_year - year > 105)
year += 100;
return year;
}
int getbirthmonth(scfg_t* cfg, const char* birth)
return DECVAL(birth[4], 10) + DECVAL(birth[5], 1);
if(cfg->sys_misc & SM_EURODATE) { // DD/MM/YY format
return DECVAL(birth[3], 10) + DECVAL(birth[4], 1);
} else { // MM/DD/YY format
return DECVAL(birth[0], 10) + DECVAL(birth[1], 1);
}
}
int getbirthday(scfg_t* cfg, const char* birth)
return DECVAL(birth[6], 10) + DECVAL(birth[7], 1);
if(cfg->sys_misc & SM_EURODATE) { // DD/MM/YY format
return DECVAL(birth[0], 10) + DECVAL(birth[1], 1);
} else { // MM/DD/YY format
return DECVAL(birth[3], 10) + DECVAL(birth[4], 1);
}
}
// Always returns string in MM/DD/YY format
char* getbirthmmddyy(scfg_t* cfg, char sep, const char* birth, char* buf, size_t max)
safe_snprintf(buf, max, "%02u%c%02u%c%02u"
, getbirthmonth(cfg, birth)
, getbirthday(cfg, birth)
, getbirthyear(birth) % 100);
return buf;
}
// Always returns string in DD/MM/YY format
char* getbirthddmmyy(scfg_t* cfg, char sep, const char* birth, char* buf, size_t max)
safe_snprintf(buf, max, "%02u%c%02u%c%02u"
, getbirthday(cfg, birth)
, getbirthmonth(cfg, birth)
, getbirthyear(birth) % 100);
return buf;
}
// Always returns string in YY/MM/DD format
char* getbirthyymmdd(scfg_t* cfg, char sep, const char* birth, char* buf, size_t max)
safe_snprintf(buf, max, "%02u%c%02u%c%02u"
, getbirthyear(birth) % 100
, getbirthmonth(cfg, birth)
, getbirthday(cfg, birth));
return buf;
}
char* getbirthdstr(scfg_t* cfg, const char* birth, char* buf, size_t max)
{
if(cfg->sys_date_fmt == YYMMDD)
getbirthyymmdd(cfg, cfg->sys_date_sep, birth, buf, max);
else if(cfg->sys_date_fmt == DDMMYY)
getbirthddmmyy(cfg, cfg->sys_date_sep, birth, buf, max);
getbirthmmddyy(cfg, cfg->sys_date_sep, birth, buf, max);
return buf;
}
/****************************************************************************/
/* Returns the age derived from the string 'birth' in the format CCYYMMDD */
/* or legacy: MM/DD/YY or DD/MM/YY */
/* Returns 0 on invalid 'birth' date */
/****************************************************************************/
int getage(scfg_t* cfg, const char *birth)
struct tm tm;
if(!VALID_CFG(cfg) || birth==NULL)
return(0);
if(!atoi(birth) || !atoi(birth+3)) /* Invalid */
return(0);
now=time(NULL);
if(localtime_r(&now,&tm)==NULL)
tm.tm_mon++; /* convert to 1 based */
int year = getbirthyear(birth);
int age = (1900 + tm.tm_year) - year;
int mon = getbirthmonth(cfg, birth);
int day = getbirthday(cfg, birth);
if(mon < 1 || mon > 12 || day < 1 || day > 31)
return 0;
if(mon > tm.tm_mon || (mon == tm.tm_mon && day > tm.tm_mday))
age--;
return age;
/****************************************************************************/
/* Converts from MM/DD/YYYYY, DD/MM/YYYY, or YYYY/MM/DD to YYYYMMDD */
/****************************************************************************/
char* parse_birthdate(scfg_t* cfg, const char* birthdate, char* out, size_t maxlen)
{
if (cfg->sys_date_fmt == YYMMDD)
safe_snprintf(out, maxlen, "%.4s%.2s%.2s", birthdate, birthdate + 5, birthdate + 8);
else if (cfg->sys_date_fmt == DDMMYY)
safe_snprintf(out, maxlen, "%.4s%.2s%.2s", birthdate + 6, birthdate + 3, birthdate);
else
safe_snprintf(out, maxlen, "%.4s%.2s%.2s", birthdate + 6, birthdate, birthdate + 3);
return out;
}
/****************************************************************************/
/* Converts from user birth date to MM/DD/YYYYY, DD/MM/YYYY, or YYYY/MM/DD */
/****************************************************************************/
char* format_birthdate(scfg_t* cfg, const char* birthdate, char* out, size_t maxlen)
{
if(maxlen < 1)
return NULL;
*out = '\0';
if(*birthdate) {
if (cfg->sys_date_fmt == YYMMDD)
safe_snprintf(out, maxlen, "%04u%c%02u%c%02u"
, getbirthyear(birthdate)
, cfg->sys_date_sep
, getbirthmonth(cfg, birthdate)
, cfg->sys_date_sep
, getbirthday(cfg, birthdate));
else if (cfg->sys_date_fmt == DDMMYY)
safe_snprintf(out, maxlen, "%02u%c%02u%c%04u"
, getbirthday(cfg, birthdate)
, cfg->sys_date_sep
, getbirthmonth(cfg, birthdate)
, cfg->sys_date_sep
, getbirthyear(birthdate));
safe_snprintf(out, maxlen, "%02u%c%02u%c%04u"
, getbirthmonth(cfg, birthdate)
, cfg->sys_date_sep
, getbirthday(cfg, birthdate)
, cfg->sys_date_sep
, getbirthyear(birthdate));
}
return out;
}
/****************************************************************************/
/****************************************************************************/
char* birthdate_format(scfg_t* cfg, char* buf, size_t size)
switch (cfg->sys_date_fmt) {
case MMDDYY: snprintf(buf, size, "MM%cDD%cYYYY", cfg->sys_date_sep, cfg->sys_date_sep); return buf;
case DDMMYY: snprintf(buf, size, "DD%cMM%cYYYY", cfg->sys_date_sep, cfg->sys_date_sep); return buf;
case YYMMDD: snprintf(buf, size, "YYYY%cMM%cDD", cfg->sys_date_sep, cfg->sys_date_sep); return buf;
}
return "??????????";
/****************************************************************************/
/****************************************************************************/
char* birthdate_template(scfg_t* cfg, char* buf, size_t size)
{
if (cfg->sys_date_fmt == YYMMDD)
snprintf(buf, size, "nnnn%cnn%cnn", cfg->sys_date_sep, cfg->sys_date_sep);
else
snprintf(buf, size, "nn%cnn%cnnnn", cfg->sys_date_sep, cfg->sys_date_sep);
return buf;
}
/****************************************************************************/
/****************************************************************************/
int opennodedat(scfg_t* cfg)
{
char fname[MAX_PATH+1];
if(!VALID_CFG(cfg))
return -1;
SAFEPRINTF(fname, "%snode.dab", cfg->ctrl_dir);
return nopen(fname, O_RDWR | O_CREAT | O_DENYNONE);
}
off_t nodedatoffset(unsigned node_number)
{
return (node_number - 1) * sizeof(node_t);
}
bool seeknodedat(int file, unsigned node_number)
{
return lseek(file, nodedatoffset(node_number), SEEK_SET) == nodedatoffset(node_number);
}
/****************************************************************************/
/****************************************************************************/
int opennodeext(scfg_t* cfg)
{
char fname[MAX_PATH+1];
if(!VALID_CFG(cfg))
return -1;
SAFEPRINTF(fname, "%snode.exb", cfg->ctrl_dir);
return nopen(fname, O_RDWR|O_DENYNONE);
}
/****************************************************************************/

Rob Swindell
committed
/* Reads the data for 1-based node number 'number' into the node structure */

rswindell
committed
/* from node.dab */
/****************************************************************************/
int getnodedat(scfg_t* cfg, uint number, node_t *node, bool lockit, int* fdp)
int rd;
|| node==NULL || number<1 || number>cfg->sys_nodes)
return USER_INVALID_ARG;
memset(node,0,sizeof(node_t));
if(fdp != NULL && *fdp > 0)
file = *fdp;
else {
if((file = opennodedat(cfg)) == -1)
return USER_OPEN_ERROR;
int result = USER_SIZE_ERROR;
if(filelength(file)>=(long)(number*sizeof(node_t))) {
for(count=0;count<LOOP_NODEDAB;count++) {
if(count > 0)
FILE_RETRY_DELAY(count + 1);
if(!seeknodedat(file, number)) {
result = USER_SEEK_ERROR;
continue;
}
if(lockit
&& lock(file, nodedatoffset(number), sizeof(node_t))!=0) {
result = USER_LOCK_ERROR;
rd=read(file,node,sizeof(node_t));
if(rd!=sizeof(node_t)) {
result = USER_READ_ERROR;
unlock(file, nodedatoffset(number), sizeof(node_t));
} else {
result = USER_SUCCESS;
break;
}
if(fdp==NULL || result != USER_SUCCESS)
CLOSE_OPEN_FILE(file);
if(fdp!=NULL)
*fdp=file;
}
/****************************************************************************/

rswindell
committed
/* Write the data from the structure 'node' into node.dab */

Rob Swindell
committed
/* number is the 1-based node number */
/****************************************************************************/
int putnodedat(scfg_t* cfg, uint number, node_t* node, bool closeit, int file)
int result = -1;
if(file<0)
return USER_INVALID_ARG;
|| node==NULL || number<1 || number>cfg->sys_nodes) {
if(closeit)
close(file);
return USER_INVALID_ARG;
}
for(attempts=0;attempts<LOOP_USERDAT;attempts++) {
if(!seeknodedat(file, number))
result = USER_SEEK_ERROR;
else if(write(file,node,sizeof(node_t)) != sizeof(node_t))
result = USER_WRITE_ERROR;
else {
result = USER_SUCCESS;
FILE_RETRY_DELAY(attempts + 1);
unlock(file, nodedatoffset(number), sizeof(node_t));
if(closeit)
close(file);
return result;
/****************************************************************************/
/****************************************************************************/
bool set_node_status(scfg_t* cfg, int nodenum, enum node_status status)
{
node_t node;
if(getnodedat(cfg, nodenum, &node, /* lockit: */true, &file) != USER_SUCCESS)
node.status = status;
return putnodedat(cfg, nodenum, &node, /* closeit: */true, file) == USER_SUCCESS;
}
/****************************************************************************/
/****************************************************************************/
bool set_node_misc(scfg_t* cfg, int nodenum, uint misc)
{
node_t node;
if(getnodedat(cfg, nodenum, &node, /* lockit: */true, &file) != USER_SUCCESS)
node.misc = misc;
return putnodedat(cfg, nodenum, &node, /* closeit: */true, file) == USER_SUCCESS;
}
/****************************************************************************/
/****************************************************************************/
bool set_node_lock(scfg_t* cfg, int nodenum, bool set)
{
node_t node;
if(getnodedat(cfg, nodenum, &node, /* lockit: */true, &file) != USER_SUCCESS)
if(set)
node.misc |= NODE_LOCK;
else
node.misc &= ~NODE_LOCK;
return putnodedat(cfg, nodenum, &node, /* closeit: */true, file) == USER_SUCCESS;
}
/****************************************************************************/
/****************************************************************************/
bool set_node_interrupt(scfg_t* cfg, int nodenum, bool set)
{
node_t node;
if(getnodedat(cfg, nodenum, &node, /* lockit: */true, &file) != USER_SUCCESS)
if(set)
node.misc |= NODE_INTR;
else
node.misc &= ~NODE_INTR;
return putnodedat(cfg, nodenum, &node, /* closeit: */true, file) == USER_SUCCESS;
}
/****************************************************************************/
/****************************************************************************/
bool set_node_down(scfg_t* cfg, int nodenum, bool set)
{
node_t node;
if(getnodedat(cfg, nodenum, &node, /* lockit: */true, &file) != USER_SUCCESS)
if(set)
node.misc |= NODE_DOWN;
else
node.misc &= ~NODE_DOWN;
return putnodedat(cfg, nodenum, &node, /* closeit: */true, file) == USER_SUCCESS;
}
/****************************************************************************/
/****************************************************************************/
bool set_node_rerun(scfg_t* cfg, int nodenum, bool set)
{
node_t node;
if(getnodedat(cfg, nodenum, &node, /* lockit: */true, &file) != USER_SUCCESS)
if(set)
node.misc |= NODE_RRUN;
else
node.misc &= ~NODE_RRUN;
return putnodedat(cfg, nodenum, &node, /* closeit: */true, file) == USER_SUCCESS;
}
/****************************************************************************/
/****************************************************************************/
bool set_node_errors(scfg_t* cfg, int nodenum, uint errors)
{
node_t node;
if(getnodedat(cfg, nodenum, &node, /* lockit: */true, &file) != USER_SUCCESS)
node.errors = errors;
return putnodedat(cfg, nodenum, &node, /* closeit: */true, file) == USER_SUCCESS;
}
/****************************************************************************/
/* Packs the password 'pass' into 5bit ASCII inside node_t. 32bits in */
/* node.extaux, and the other 8bits in the upper byte of node.aux */
/****************************************************************************/
void packchatpass(char *pass, node_t *node)
{
char bits;
int i,j;
if(pass==NULL || node==NULL)
return;
node->aux&=~0xff00; /* clear the password */
node->extaux=0L;
if((j=strlen(pass))==0) /* there isn't a password */
return;
node->aux|=(int)((pass[0]-64)<<8); /* 1st char goes in low 5bits of aux */
if(j==1) /* password is only one char, we're done */
return;
node->aux|=(int)((pass[1]-64)<<13); /* low 3bits of 2nd char go in aux */
node->extaux|=(long)((pass[1]-64)>>3); /* high 2bits of 2nd char go extaux */
bits=2;
for(i=2;i<j;i++) { /* now process the 3rd char through the last */
node->extaux|=(long)((long)(pass[i]-64)<<bits);
}
}
/****************************************************************************/
/* Unpacks the password 'pass' from the 5bit ASCII inside node_t. 32bits in */
/* node.extaux, and the other 8bits in the upper byte of node.aux */
/****************************************************************************/
char* unpackchatpass(char *pass, node_t* node)
{
char bits;
int i;
if(pass==NULL || node==NULL)
return(NULL);
pass[0]=(node->aux&0x1f00)>>8;
pass[1]=(char)(((node->aux&0xe000)>>13)|((node->extaux&0x3)<<3));
bits=2;
for(i=2;i<8;i++) {
pass[i]=(char)((node->extaux>>bits)&0x1f);
}
pass[8]=0;
for(i=0;i<8;i++)
if(pass[i])
pass[i]+=64;
return(pass);
}
static char* node_connection_desc(ushort conn, char* str)
{
switch(conn) {
case NODE_CONNECTION_LOCAL:
strcpy(str,"Locally");
break;
case NODE_CONNECTION_TELNET:
strcpy(str,"via telnet");
break;
case NODE_CONNECTION_RLOGIN:
strcpy(str,"via rlogin");
break;
case NODE_CONNECTION_SSH:
strcpy(str,"via ssh");
break;

Rob Swindell
committed
case NODE_CONNECTION_SFTP:
strcpy(str,"via sftp");
break;
case NODE_CONNECTION_RAW:
strcpy(str,"via raw");
break;
default:
sprintf(str,"at %ubps",conn);
break;
}
return str;
}
char* getnodeext(scfg_t* cfg, int num, char* buf)
{
int f;
if(!VALID_CFG(cfg) || num < 1)
return "";
if((f = opennodeext(cfg)) < 0)
(void)lseek(f, (num-1) * 128, SEEK_SET);
if(read(f, buf, 128) != 128)
memset(buf, 0, 128);
close(f);
buf[127] = 0;
return buf;
}
char* node_vstatus(scfg_t* cfg, node_t* node, char* str, size_t size)
{
char tmp[128];
switch(node->status) {
case NODE_WFC:
return cfg->text != NULL ? cfg->text[NodeStatusWaitingForCall] : "Waiting for connection";
case NODE_OFFLINE:
return cfg->text != NULL ? cfg->text[NodeStatusOffline] : "Offline";
case NODE_NETTING: /* Obsolete */
return "Networking";
case NODE_LOGON:
return cfg->text != NULL ? cfg->text[NodeStatusLogon] : "At login prompt";
case NODE_LOGOUT:
snprintf(str, size, cfg->text != NULL ? cfg->text[NodeStatusLogout] : "Logging out %s", username(cfg,node->useron,tmp));
return str;
case NODE_EVENT_WAITING:
return cfg->text != NULL ? cfg->text[NodeStatusEventWaiting] : "Waiting for all nodes to become inactive";
case NODE_EVENT_LIMBO:
snprintf(str, size, cfg->text != NULL ? cfg->text[NodeStatusEventLimbo] : "Waiting for node %d to finish external event"
,node->aux);
break;
case NODE_EVENT_RUNNING:
return cfg->text != NULL ? cfg->text[NodeStatusEventRunning] : "Running external event";
case NODE_NEWUSER:
return cfg->text != NULL ? cfg->text[NodeStatusNewUser] : "New user applying for access";
case NODE_QUIET:
return "In use (quietly)";
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
case NODE_INUSE:
return "In use";
default:
snprintf(str, size, "Unknown status %u", node->status);
break;
}
return str;
}
char* node_activity(scfg_t* cfg, node_t* node, char* str, size_t size, int num)
{
int xtrnnum;
int gurunum;
user_t user = {0};
if(node->misc & NODE_EXT) {
getnodeext(cfg, num, str); // note assuming sizeof str is >= 128
return str;
}
switch(node->action) {
case NODE_MAIN:
return cfg->text != NULL ? cfg->text[NodeActivityMainMenu] : "at main menu";
case NODE_RMSG:
return cfg->text != NULL ? cfg->text[NodeActivityReadingMsgs] : "reading messages";
case NODE_RMAL:
return cfg->text != NULL ? cfg->text[NodeActivityReadingMail] : "reading mail";
case NODE_RSML:
return cfg->text != NULL ? cfg->text[NodeActivityReadingSentMail]: "reading sent mail";
case NODE_RTXT:
return cfg->text != NULL ? cfg->text[NodeActivityReadingTextFiles] : "reading text files";
case NODE_PMSG:
return cfg->text != NULL ? cfg->text[NodeActivityPostingMsg] : "posting message";
case NODE_SMAL:
return cfg->text != NULL ? cfg->text[NodeActivitySendingMail] : "sending mail";
case NODE_AMSG:
return cfg->text != NULL ? cfg->text[NodeActivityAutoMsg] : "posting auto-message";
case NODE_XTRN:
if(node->aux == 0)
return cfg->text != NULL ? cfg->text[NodeActivityXtrnMenu] : "at external program menu";
user.number = node->useron;
getuserdat(cfg, &user);
xtrnnum = getxtrnnum(cfg, user.curxtrn);
if(is_valid_xtrnnum(cfg, xtrnnum))
snprintf(str, size, "%s %s"
,cfg->text != NULL ? cfg->text[NodeActivityRunningXtrn] : "running"
,cfg->xtrn[xtrnnum]->name);
else if(*user.curxtrn != '\0')
snprintf(str, size, "%s external program %s"
,cfg->text != NULL ? cfg->text[NodeActivityRunningXtrn] : "running"
,user.curxtrn);
else
snprintf(str, size, "%s external program #%d"
,cfg->text != NULL ? cfg->text[NodeActivityRunningXtrn] : "running"
,node->aux);
break;
case NODE_DFLT:
return cfg->text != NULL ? cfg->text[NodeActivitySettings] : "changing defaults";
case NODE_XFER:
return cfg->text != NULL ? cfg->text[NodeActivityFileMenu] : "at transfer menu";
case NODE_RFSD:
snprintf(str, size,cfg->text != NULL ? cfg->text[NodeActivityRetrievingFile] : "retrieving from device #%d", node->aux);
break;
case NODE_DLNG:
return cfg->text != NULL ? cfg->text[NodeActivityDownloadingFile] : "downloading";
case NODE_ULNG:
return cfg->text != NULL ? cfg->text[NodeActivityUploadingFile] : "uploading";
case NODE_BXFR:
return cfg->text != NULL ? cfg->text[NodeActivityBiXferFile] : "transferring bidirectional";
case NODE_LFIL:
return cfg->text != NULL ? cfg->text[NodeActivityListingFiles] : "listing files";
case NODE_LOGN:
return cfg->text != NULL ? cfg->text[NodeActivityLoggingOn] : "logging on";
case NODE_LCHT:
snprintf(str, size, cfg->text != NULL ? cfg->text[NodeActivityLocalChat] : "chatting with %s", cfg->sys_op);
break;
case NODE_MCHT:
if(node->aux != 0)
snprintf(str, size
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
,cfg->text != NULL ? cfg->text[NodeActivityChatChannel] : "in multinode chat channel %d"
,node->aux & 0xff);
else
Loading
Loading full blame...