Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 280 additions & 0 deletions files/datemotd.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/* GPL v3
Valware © 2025
Shows a specific message in the MOTD on a specific date
*/
/*** <<<MODULE MANAGER START>>>
module
{
documentation "https://github.com/ValwareIRC/valware-unrealircd-mods/blob/main/datemotd/README.md";
troubleshooting "In case of problems, check the documentation or e-mail me at [email protected]";
min-unrealircd-version "6.2.2";
max-unrealircd-version "6.*";
post-install-text
{
"The module is installed. Now all you need to do is add a loadmodule line:";
"loadmodule \"third/datemotd\";";
"Configure the block as described in the documentation.";
"Once you're good to go, type ./unrealircd rehash";
}
}
*** <<<MODULE MANAGER END>>>
*/

#include "unrealircd.h"

#define DATEMOTD_CONF "datemotd"

struct datemotd_item {
char *name;
char *file;
char *date; // NEW: date in format YYYY-MM-DD
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "NEW: date in format YYYY-MM-DD" is incomplete and misleading. The date field actually supports both YYYY-MM-DD format and weekday names (e.g., "Monday", "Mon"). Update the comment to reflect all supported formats.

Suggested change
char *date; // NEW: date in format YYYY-MM-DD
char *date; // Date in format YYYY-MM-DD or weekday name (e.g., "Monday", "Mon")

Copilot uses AI. Check for mistakes.
struct datemotd_item *next;
};

struct datemotd_conf {
struct datemotd_item *datemotds;
unsigned short int got_datemotd;
};

static struct datemotd_conf datemotd_conf;


void set_config_defaults(void);
void free_config(void);
int datemotd_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int datemotd_configrun(ConfigFile *cf, ConfigEntry *ce, int type);
int motd_hook(Client *client);

ModuleHeader MOD_HEADER = {
"third/datemotd",
"1.1",
"Shows a specific message in the MOTD on a specific date",
"Valware",
"unrealircd-6"
};


static int is_valid_date_string(const char *datestr)
{
if (!datestr) return 0;

// YYYY-MM-DD
if (isdigit(datestr[0])) {
int y, m, d;
if (sscanf(datestr, "%d-%d-%d", &y, &m, &d) != 3)
return 0;
if (y < 1970 || m < 1 || m > 12 || d < 1 || d > 31)
return 0;
Comment on lines +62 to +67
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date validation doesn't account for valid days per month (e.g., February 31st would pass validation). Consider adding proper date validation that checks the actual number of days in each month, including leap years.

Copilot uses AI. Check for mistakes.
return 1;
}

// Weekday names
const char *days_short[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
const char *days_long[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };

char lower_input[16];
int i;
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buffer size check i < sizeof(lower_input)-1 should use strlen(datestr) or ensure the input length doesn't exceed the buffer. If datestr is longer than 15 characters, it will be silently truncated, which could lead to unexpected behavior for invalid inputs.

Suggested change
int i;
int i;
// Reject input longer than buffer size (15 chars)
if (strlen(datestr) > sizeof(lower_input) - 1)
return 0;

Copilot uses AI. Check for mistakes.
for (i = 0; i < sizeof(lower_input)-1 && datestr[i]; i++)
lower_input[i] = tolower((unsigned char)datestr[i]);
lower_input[i] = '\0';

for (i = 0; i < 7; i++) {
char tmp[16];
snprintf(tmp, sizeof(tmp), "%s", days_short[i]);
for (int j = 0; tmp[j]; j++) tmp[j] = tolower((unsigned char)tmp[j]);
Comment on lines +82 to +84
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable j is declared in the for loop initialization, which requires C99 or later. While this is likely supported, ensure consistency with the rest of the codebase's C standard usage. The same pattern appears on line 88.

Copilot uses AI. Check for mistakes.
if (!strcmp(lower_input, tmp)) return 1;

snprintf(tmp, sizeof(tmp), "%s", days_long[i]);
for (int j = 0; tmp[j]; j++) tmp[j] = tolower((unsigned char)tmp[j]);
if (!strcmp(lower_input, tmp)) return 1;
}
Comment on lines +72 to +90
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The weekday name arrays and lowercase conversion logic are duplicated in three places: is_valid_date_string (lines 72-90), is_today (lines 112-128), and the arrays appear again in is_today. Consider extracting this into a helper function or using constants to avoid code duplication and reduce maintenance burden.

Copilot uses AI. Check for mistakes.

return 0; // invalid string
}

static int is_today(const char *datestr)
{
if (!datestr)
return 0;

time_t now = time(NULL);
struct tm *tm_now = localtime(&now);
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

localtime() returns a pointer to static internal storage that may be overwritten by subsequent calls. While this is likely safe in this specific context, it's not thread-safe. Consider using localtime_r() for thread safety if the UnrealIRCd framework supports multi-threaded operations.

Copilot uses AI. Check for mistakes.

// --- Case 1: YYYY-MM-DD format ---
if (isdigit(datestr[0])) {
char today[11];
snprintf(today, sizeof(today), "%04d-%02d-%02d",
tm_now->tm_year + 1900, tm_now->tm_mon + 1, tm_now->tm_mday);
return !strcmp(today, datestr);
}

// --- Case 2: Day of week (Mon, Monday, etc.) ---
const char *days_short[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
const char *days_long[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };

char lower_input[16];
int i;
for (i = 0; i < sizeof(lower_input)-1 && datestr[i]; i++)
lower_input[i] = tolower((unsigned char)datestr[i]);
lower_input[i] = '\0';

// get today's weekday in both short and long form (lowercase)
char today_short[8], today_long[16];
snprintf(today_short, sizeof(today_short), "%s", days_short[tm_now->tm_wday]);
snprintf(today_long, sizeof(today_long), "%s", days_long[tm_now->tm_wday]);

for (i = 0; today_short[i]; i++) today_short[i] = tolower((unsigned char)today_short[i]);
for (i = 0; today_long[i]; i++) today_long[i] = tolower((unsigned char)today_long[i]);


if (!strcmp(lower_input, today_short) || !strcmp(lower_input, today_long)){
return 1;
}
return 0;
}

void send_datemotd_file(Client *client, const char *file)
{
FILE *fp = fopen(file, "r");
if (!fp)
return;
Comment on lines +138 to +140
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no error logging when the file cannot be opened. Users won't know why their configured datemotd isn't being displayed. Consider logging a warning or error message to help with troubleshooting.

Copilot uses AI. Check for mistakes.

char line[512];
while (fgets(line, sizeof(line), fp)) {
size_t len = strlen(line);
if (len > 0 && line[len-1] == '\n') {
line[len-1] = '\0';
sendnumeric(client, RPL_MOTD, line);
}
Comment on lines +147 to +148
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file reading logic doesn't handle lines that don't end with a newline character (e.g., the last line of a file). If the last line doesn't have a trailing newline, line[len-1] won't be '\n', and the line will be sent with its content intact but won't be processed through the newline removal logic. This could lead to inconsistent behavior. Consider also sending lines that don't end with newlines.

Suggested change
sendnumeric(client, RPL_MOTD, line);
}
}
sendnumeric(client, RPL_MOTD, line);

Copilot uses AI. Check for mistakes.
}
fclose(fp);
}

void set_config_defaults(void)
{
datemotd_conf.datemotds = NULL;
}

void free_config(void)
{
struct datemotd_item *p = datemotd_conf.datemotds;
while (p) {
struct datemotd_item *next = p->next;
safe_free(p->name);
safe_free(p->file);
safe_free(p->date); // NEW
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "NEW" is a development artifact that should be removed before finalizing the code. This type of comment is useful during development but serves no purpose in the final codebase.

Suggested change
safe_free(p->date); // NEW
safe_free(p->date);

Copilot uses AI. Check for mistakes.
safe_free(p);
p = next;
}
datemotd_conf.datemotds = NULL;
}
int datemotd_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;

if (type != CONFIG_MAIN || !ce || !ce->name || strcmp(ce->name, DATEMOTD_CONF))
return 0;

for (ConfigEntry *entry = ce->items; entry; entry = entry->next) {
if (strcmp(entry->name, "item") != 0)
continue;

int has_name = 0, has_file = 0, has_date = 0;
for (ConfigEntry *item = entry->items; item; item = item->next) {

if (!item->name || !item->value)
continue;

if (strcmp(item->name, "name") == 0) has_name = 1;
else if (strcmp(item->name, "file") == 0) has_file = 1;
else if (strcmp(item->name, "date") == 0) {
if (!is_valid_date_string(item->value)) {
config_error("%s:%i: invalid date '%s' (must be YYYY-MM-DD or weekday name)",
item->file->filename, item->line_number, item->value);

errors++;
}
else
has_date = 1;
} else {
config_warn("%s:%i: unknown key '%s' in datemotd entry",
item->file->filename, item->line_number, item->name);
}
}
if (!has_name || !has_file || !has_date) {
config_error("%s:%i: each datemotd entry must have name (%i), file (%i), and date (%i)",
entry->file->filename, entry->line_number, has_name, has_file, has_date);
errors++;
}
}

*errs = errors;
return errors ? -1 : 1;
}

int datemotd_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
{
if (type != CONFIG_MAIN || !ce || !ce->name || strcmp(ce->name, DATEMOTD_CONF))
return 0;

for (ConfigEntry *entry = ce->items; entry; entry = entry->next) {
if (strcmp(entry->name, "item") != 0)
continue;

struct datemotd_item *new_item = safe_alloc(sizeof(*new_item));
memset(new_item, 0, sizeof(*new_item));
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The memset after safe_alloc is redundant. The safe_alloc function typically already zeros the allocated memory (it's a common pattern in C codebases to have allocation functions that zero memory). Verify if this is the case in the UnrealIRCd codebase and remove the redundant memset if so.

Suggested change
memset(new_item, 0, sizeof(*new_item));

Copilot uses AI. Check for mistakes.
for (ConfigEntry *item = entry->items; item; item = item->next) {

if (!item->name || !item->value)
continue;

if (strcmp(item->name, "name") == 0) safe_strdup(new_item->name, item->value);
else if (strcmp(item->name, "file") == 0) safe_strdup(new_item->file, item->value);
else if (strcmp(item->name, "date") == 0) safe_strdup(new_item->date, item->value);

}
new_item->next = datemotd_conf.datemotds;
datemotd_conf.datemotds = new_item;
}
Comment on lines +236 to +238
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The safe_strdup macro is being used with a side effect pattern. If any of the safe_strdup calls fail (return NULL or similar), the new_item will be partially initialized and still added to the list, potentially causing issues later. Consider validating that all required fields are properly set before adding the item to the list.

Suggested change
new_item->next = datemotd_conf.datemotds;
datemotd_conf.datemotds = new_item;
}
// Validate that all required fields are set
if (!new_item->name || !new_item->file || !new_item->date) {
// Free any allocated fields and the struct itself
if (new_item->name) free(new_item->name);
if (new_item->file) free(new_item->file);
if (new_item->date) free(new_item->date);
free(new_item);
config_error("datemotd: 'item' block is missing required fields or memory allocation failed");
continue;
}
new_item->next = datemotd_conf.datemotds;
datemotd_conf.datemotds = new_item;

Copilot uses AI. Check for mistakes.

return 1;
}


// NEW: hook to send MOTD only on the matching date
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "NEW: hook to send MOTD only on the matching date" is a development artifact that should be removed. This type of comment is useful during development but serves no purpose in the final codebase.

Suggested change
// NEW: hook to send MOTD only on the matching date

Copilot uses AI. Check for mistakes.
int motd_hook(Client *client)
{
struct datemotd_item *p;
for (p = datemotd_conf.datemotds; p; p = p->next) {
if (is_today(p->date)) {
send_datemotd_file(client, p->file);
}
}
return 0;
}

MOD_INIT()
{
set_config_defaults();
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, datemotd_configrun);
HookAdd(modinfo->handle, HOOKTYPE_MOTD, 0, motd_hook);
return MOD_SUCCESS;
}

MOD_TEST()
{
memset(&datemotd_conf, 0, sizeof(datemotd_conf));
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, datemotd_configtest);
return MOD_SUCCESS;
}

MOD_LOAD()
{
return MOD_SUCCESS;
}

MOD_UNLOAD()
{
free_config();
return MOD_SUCCESS;
}