From 0559bff00a6be2054194632c3543bf62af1fb56f Mon Sep 17 00:00:00 2001
From: Luke Bratch <luke@bratch.co.uk>
Date: Sat, 11 May 2019 19:03:57 +0100
Subject: Add the ability to replay messages from a replay log file.  (No
 replay log file writing yet.)

---
 Makefile        |   6 +-
 blabouncer.c    |  27 ++++++-
 blabouncer.conf |   3 +
 config.c        |  41 +++++++++-
 config.h        |   2 +
 replay.c        | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 replay.h        |  21 ++++++
 7 files changed, 325 insertions(+), 6 deletions(-)
 create mode 100644 replay.c
 create mode 100644 replay.h

diff --git a/Makefile b/Makefile
index 3f8d034..5bf17a3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,8 @@
 CC=gcc
-DEPS = functions.h sockets.h config.h
+DEPS = functions.h sockets.h config.h replay.h
 
 %.o: %.c $(DEPS)
 	$(CC) -Wall -Wextra -c -o $@ $<
 
-blabouncer: blabouncer.o functions.o sockets.o config.o
-	$(CC) -Wall -Wextra -o blabouncer blabouncer.o functions.o sockets.o config.o
+blabouncer: blabouncer.o functions.o sockets.o config.o replay.o
+	$(CC) -Wall -Wextra -o blabouncer blabouncer.o functions.o sockets.o config.o replay.o
diff --git a/blabouncer.c b/blabouncer.c
index ba9dca0..2e1f414 100644
--- a/blabouncer.c
+++ b/blabouncer.c
@@ -8,7 +8,9 @@
 // - Add blabouncer MOTD (375, 372, 376)
 // - "01:53:47 -!- ServerMode/#test [b] by irc.tghost.co.uk" on existing clients when new client connects
 // - Keep track of changing user nicks/modes
-// - Relay log can just be "log/resend everything that ever hit sendto[all]client[s]()"
+// - Relay log can just be "log/resend everything that ever hit sendto[all]client[s]()" (or maybe just PRIVMSGs?  + NOTICEs and friends?)
+// - Implement TLS
+// - Implement password/login
 //
 // Example WHOIS reply:
 // BOUNCER-SERVER RECEIVED: :irc.tghost.co.uk 307 blabounce l_bratch :is identified for this nick
@@ -32,6 +34,7 @@
 #include "functions.h"
 #include "sockets.h"
 #include "config.h"
+#include "replay.h"
 
 #define SOURCE_SERVER 0
 #define SOURCE_CLIENT 1
@@ -645,6 +648,28 @@ int processircmessage(int *serversockfd, int *clientsockfd, char *str, int sourc
           sendtoclient(sourcefd, outgoingmsg);
         }
 
+        // Send the client however many relay lines have been requested
+        int relaysecs = confrelayseconds();
+
+        // Figure out how many lines to relay
+        int numlines = relaylines(relaysecs);
+        printf("Replay log lines: '%d'.\n", numlines);
+
+        if (numlines < 0) {
+          printf("Error getting number of replay lines.\n");
+          exit(1);
+        }
+
+        // Relay those lines!
+        for (int i = 0; i < numlines; i++) {
+          if (!readrelayline(relaysecs, i, outgoingmsg)) {
+            printf("Error requesting relay line.\n");
+            exit(1);
+          }
+          printf("Sending relay line: '%s'.\n", outgoingmsg);
+          sendtoclient(sourcefd, outgoingmsg);
+        }
+
         return 1;
       }
 
diff --git a/blabouncer.conf b/blabouncer.conf
index 4f569dd..5b68918 100644
--- a/blabouncer.conf
+++ b/blabouncer.conf
@@ -7,3 +7,6 @@
 nick = "blabounce"
 username = "blabounce"
 realname = "Mr Bla Bouncer"
+
+# How many seconds of relay log should be sent to connecting clients
+relayseconds = "7200"
diff --git a/config.c b/config.c
index ca6b158..7fec4de 100644
--- a/config.c
+++ b/config.c
@@ -1,5 +1,9 @@
 #include "config.h"
 
+// TODO - Multiple functions here (at least readnames() and relayseconds() have the file opening code, rewrite.
+
+// TODO - Can isconf() and getconf() just be merged into one function?
+
 // Check to see if a line is the configuration option specified by
 // checking to see if str starts with conf followed by a space, a tab,
 // or an equals sign.
@@ -69,9 +73,10 @@ int readnames(char *nick, char *username, char *realname) {
   fp = fopen(filename, "r");
 
   if (fp == NULL) {
-    printf("error: could not open configuration file '%s.'", filename);
-    return 1;
+    printf("error: could not open configuration file '%s'.\n", filename);
+    exit(1);
   }
+
   while (fgets(str, MAXCHAR, fp) != NULL) {
     long int len;
     if ((len = isconf(str, "nick"))) {
@@ -101,3 +106,35 @@ int readnames(char *nick, char *username, char *realname) {
   fclose(fp);
   return 1;
 }
+
+// Return the relayseconds configuration option
+// (How many seconds of relay should be sent to connecting clients)
+int confrelayseconds() {
+  FILE *fp;
+  char str[MAXCHAR];
+  char* filename = "blabouncer.conf";
+
+  char secondsstr[MAXCHAR];
+  int seconds;
+
+  fp = fopen(filename, "r");
+
+  if (fp == NULL) {
+    printf("error: could not open configuration file '%s'.\n", filename);
+    exit(1);
+  }
+
+  while (fgets(str, MAXCHAR, fp) != NULL) {
+    long int len;
+    if ((len = isconf(str, "relayseconds"))) {
+      getconf(str, len);
+      strncpy(secondsstr, str, strlen(str));
+      secondsstr[strlen(str)] = '\0';
+      printf("secondsstr is: '%s', length '%ld'.\n", secondsstr, strlen(secondsstr));
+      seconds = strtol(secondsstr, NULL, 10); // Convert resulting string to an integer, base 10
+      printf("seconds is: '%d'.\n", seconds);
+    }
+  }
+
+  return seconds;
+}
diff --git a/config.h b/config.h
index 5307178..57defb6 100644
--- a/config.h
+++ b/config.h
@@ -9,4 +9,6 @@
 
 int readnames(char *nick, char *username, char *realname);
 
+int confrelayseconds();
+
 #endif
diff --git a/replay.c b/replay.c
new file mode 100644
index 0000000..8c37d4c
--- /dev/null
+++ b/replay.c
@@ -0,0 +1,231 @@
+#include "replay.h"
+
+// Return the unixtime timestamp at the start of a line,
+// or -1 if a timestamp couldn't be extracted
+int gettimestamp(char *str) {
+  int timestamp;
+  char timestr[TIMELEN];
+  int count = 0;
+
+  // Make sure we're starting with a digit
+  if (!isdigit(str[0])) {
+    return -1;
+  }
+
+  // Extract each digit until we encounter a non-digit
+  for (int i = 0; i < TIMELEN; i++) {
+    if (isdigit(str[i])) {
+      timestr[count] = str[i];
+      count++;
+    }
+  }
+
+  timestr[count] = '\0'; // Null terminate
+
+  timestamp = strtol(timestr, NULL, 10); // Convert resulting string to an integer, base 10
+
+  return timestamp;
+}
+
+// Set 'str' to a string with the leading unixtime timestamp removed.
+// Returns 1 on success, 0 on failure
+int striptimestamp(char *str) {
+  char line[MAXCHAR];
+  int count = 0;
+
+  // Make sure we're starting with a digit
+  if (!isdigit(str[0])) {
+    return 0;
+  }
+
+  // Skip over each digit until we encounter a non-digit, record the position
+  for (int i = 0; i < TIMELEN; i++) {
+    if (isdigit(str[i])) {
+      count++;
+    }
+  }
+
+  // Skip over the space
+  count++;
+
+  int count2 = 0;
+
+  // Copy from the end of the digits (and the space) until the end of the string
+  for (size_t i = count; i < strlen(str); i++) {
+    line[count2] = str[i];
+    count2++;
+  }
+
+  strncpy(str, line, count2);
+  str[count2] = '\0';
+
+  return 1;
+}
+
+// Take a string like:
+// 1557592901 :foo!bar@baz PRIVMSG foo :hello world
+// And convert it to:
+// :foo!bar@baz PRIVMSG foo :[17:41:41] hello world
+// Only inserts the formatted time for PRIVMSGs at the moment (and maybe only needs to!).
+void formattime(char *str) {
+  // Extract the timestamp for conversion into [HH:MM:SS]
+  char timestr[TIMELEN];
+  sprintf(timestr, "%d", gettimestamp(str)); // Convert int time to string time
+  struct tm tm;
+  strptime(timestr, "%s", &tm);
+  char timestampf[TIMELEN]; // Formatted timestamp
+  // Convert into [HH:MM:SS]
+  strftime(timestampf, TIMELEN, "[%H:%M:%S]", &tm);
+  printf("Formatted time string: '%s'.\n", timestampf);
+
+  // Strip the original unixtimestamp
+  striptimestamp(str);
+  printf("Now got '%s' and '%s'.\n", timestampf, str);
+
+  // Take note of the length
+  int len = strlen(str);
+
+  // Find the start of the message if it's a PRIVMSG
+  char *ret;
+  int pos1;
+  if ((ret = strstr(str, "PRIVMSG")) != NULL) {
+    // Position within str of "PRIVMSG"
+    pos1 = ret - str;
+    printf("Found 'PRIVMSG' at position '%d' of '%s'.\n", pos1, str);
+  } else {
+    // If it's not a PRIVMSG, stop here
+    return;
+  }
+
+  char *ret2;
+  int pos2;
+  // Find the start of the actual message within the PRIVMSG
+  if ((ret2 = strstr(ret, ":")) != NULL) {
+    // Position within ret of ":"
+    pos2 = ret2 - ret;
+    printf("Found ':' at position '%d' of '%s'.\n", pos2, ret);
+  } else {
+    // Didn't find the real message, weird.
+    return;
+  }
+
+  // Position of start of PRIVMSG colon in original string
+  int realpos = pos1 + pos2 + 1;
+
+  // Build the new formatted string
+  char newline[MAXCHAR];
+
+  // First bit (:foo!bar@baz PRIVMSG foo :)
+  for (int i = 0; i < realpos; i++) {
+    newline[i] = str[i];
+  }
+
+  // Second bit (:foo!bar@baz PRIVMSG foo :[HH:MM:SS])
+  int j = 0;
+  for (int i = realpos; i < TIMELEN + realpos - 1; i++) { // -1 to avoid the null char from timestampf
+    newline[i] = timestampf[j];
+    j++;
+  }
+
+  // Insert a space after the formatted timestamp
+  newline[TIMELEN + realpos - 1] = ' ';
+
+  // Final bit (the original PRIVMSG contents)
+  for (int i = TIMELEN + realpos; i < len + TIMELEN; i++) {
+    newline[i] = str[i - TIMELEN];
+  }
+
+  // Copy the whole thing back to str and null terminate
+  strncpy(str, newline, len + TIMELEN);
+  str[len + TIMELEN] = '\0';
+
+  printf("Ended up with relay string of: '%s'.\n", str);
+}
+
+// Return the number of lines in the replay log since 'seconds' seconds ago, or -1 if there a problem.
+int relaylines(int seconds) {
+  FILE *fp;
+  char str[MAXCHAR];
+  char* filename = "replay.log";
+
+  int numlines = 0;
+
+  fp = fopen(filename, "r");
+
+  if (fp == NULL) {
+    printf("error: could not open replay log '%s'.\n", filename);
+    exit(1);
+  }
+
+  // Get the current time for comparison later
+  int timenow = (int)time(NULL);
+
+  while (fgets(str, MAXCHAR, fp) != NULL) {
+    // Read the timestamp from each line
+    int timestamp = gettimestamp(str);
+    if (timestamp < 1) {
+      printf("Error reading timestamp from replay log file.\n");
+      return -1;
+    }
+    
+    printf("Timestamp is: '%d'.\n", timestamp);
+
+    // If the line is within range of the requested time, count it
+    if (timestamp > timenow - seconds) {
+      numlines++;
+    }
+  }
+
+  fclose(fp);
+  return numlines;
+}
+
+// Set 'str' to the line in the log with a timestamp of greater than 'seconds'
+// seconds ago, plus however many lines 'linenum' is set to.
+// Also modify the line to include a timestamp in the form "[HH:MM:SS]".
+// Returns 1 on success, or 0 on failure.
+// TODO - This is horribly inefficient since it re-reads the entire file each call, rewrite this!
+int readrelayline(int seconds, int linenum, char *str) {
+  FILE *fp;
+  char line[MAXCHAR];
+  char* filename = "replay.log";
+
+  int count = 0;
+
+  fp = fopen(filename, "r");
+
+  if (fp == NULL) {
+    printf("error: could not open replay log '%s'.\n", filename);
+    return 1;
+  }
+
+  // Get the current time for comparison later
+  int timenow = (int)time(NULL);
+
+  while (fgets(line, MAXCHAR, fp) != NULL) {
+    printf("Read: '%s'.\n", line);
+    // Read the timestamp from each line
+    int timestamp = gettimestamp(line);
+    if (timestamp < 1) {
+      printf("Error reading timestamp from replay log file.\n");
+      exit(1);
+    }
+
+    // If the line is within range of the requested time...
+    if (timestamp > timenow - seconds) {
+      // ...and it is the current requested line then return it
+      if (count == linenum) {
+        // Insert our formatted [HH:MM:SS] timestamp into the message
+        formattime(line);
+
+        strncpy(str, line, strlen(line));
+        str[strlen(line)] = '\0';
+        return 1;
+      }
+      count++;
+    }
+  }
+
+  // If we got here something went wrong
+  return 0;
+}
diff --git a/replay.h b/replay.h
new file mode 100644
index 0000000..e0b5822
--- /dev/null
+++ b/replay.h
@@ -0,0 +1,21 @@
+#ifndef REPLAY_H_INCLUDED
+#define REPLAY_H_INCLUDED
+
+#define _XOPEN_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <ctype.h>
+
+#define MAXCHAR 1000
+#define TIMELEN 11 // 32-bit unixtime is up to 10 characters (+1 for null char) // TODO - Make this Year 2038 proof
+#define TIMELENF 11 // [HH:MM:SS] = 10 characters + 1 for null char
+
+int relaylines(int seconds);
+
+int readrelayline(int seconds, int linenum, char *str);
+
+int writerelayline(char *str);
+
+#endif
-- 
cgit v1.2.3