diff options
-rwxr-xr-x | bouncer/build.sh | 2 | ||||
-rwxr-xr-x | bouncer/build_static.sh | 2 | ||||
-rw-r--r-- | bouncer/core.h | 4 | ||||
-rw-r--r-- | bouncer/richtext.cpp | 176 | ||||
-rw-r--r-- | bouncer/richtext.h | 92 | ||||
-rw-r--r-- | bouncer/window.cpp | 114 | ||||
-rw-r--r-- | python_client.py | 109 |
7 files changed, 429 insertions, 70 deletions
diff --git a/bouncer/build.sh b/bouncer/build.sh index 092cca1..be4826d 100755 --- a/bouncer/build.sh +++ b/bouncer/build.sh @@ -2,7 +2,7 @@ mkdir -p binary NETCODE="socketcommon.cpp client.cpp mobileclient.cpp server.cpp ircserver.cpp netcore.cpp" -SOURCES="$NETCODE main.cpp window.cpp dns.cpp ini.cpp" +SOURCES="$NETCODE main.cpp window.cpp dns.cpp ini.cpp richtext.cpp" FLAGS="-std=c++11 -DUSE_GNUTLS -lgnutls -pthread -g" g++ -o binary/nb4 $FLAGS $SOURCES diff --git a/bouncer/build_static.sh b/bouncer/build_static.sh index e75d469..5807c1f 100755 --- a/bouncer/build_static.sh +++ b/bouncer/build_static.sh @@ -2,7 +2,7 @@ mkdir -p binary NETCODE="socketcommon.cpp client.cpp mobileclient.cpp server.cpp ircserver.cpp netcore.cpp" -SOURCES="$NETCODE main.cpp window.cpp dns.cpp ini.cpp" +SOURCES="$NETCODE main.cpp window.cpp dns.cpp ini.cpp richtext.cpp" FLAGS="-static -static-libgcc -static-libstdc++ -std=c++11 -pthread" g++ -o binary/nb4_static $FLAGS $SOURCES diff --git a/bouncer/core.h b/bouncer/core.h index 1a13a1d..28b99f2 100644 --- a/bouncer/core.h +++ b/bouncer/core.h @@ -66,7 +66,7 @@ public: virtual void handleUserInput(const char *str) { } virtual void handleUserClosed(); - void pushMessage(const char *str); + void pushMessage(const char *str, int priority = 0); void notifyWindowRename(); }; @@ -109,6 +109,8 @@ public: void handleTopic(const UserRef &user, const char *message); void handleTopicInfo(const char *user, int timestamp); + void outputUserMessage(const char *nick, const char *message, bool isAction); + char getEffectivePrefixChar(const char *nick) const; void disconnected(); diff --git a/bouncer/richtext.cpp b/bouncer/richtext.cpp new file mode 100644 index 0000000..22c6330 --- /dev/null +++ b/bouncer/richtext.cpp @@ -0,0 +1,176 @@ +#include "richtext.h" + +static int matchIRCColourString(const char *str, int pos, int *result) { + // First, try to match a single digit. + if (str[pos] >= '0' && str[pos] <= '9') { + int first = str[pos] - '0'; + if (str[pos + 1] >= '0' && str[pos + 1] <= '9') { + int second = str[pos + 1] - '0'; + + int attempt = (first * 10) + second; + if (attempt < 0 || attempt > 15) { + // Invalid number, give up here + return pos; + } else { + // Valid number! + *result = attempt; + return pos + 2; + } + } else { + // Second character was not a digit, so + // let's take the colour we *did* parse as is + *result = first; + return pos + 1; + } + } else { + // First character was not a digit, so pass entirely. + return pos; + } +} + +static int matchIRCColourPair(const char *str, int pos, int *whichFG, int *whichBG) { + // str[pos] is the first character following the + // \x03 colour code. + + *whichFG = -1; + *whichBG = -1; + + pos = matchIRCColourString(str, pos, whichFG); + if (*whichFG != -1) { + if (str[pos] == ',') { + pos = matchIRCColourString(str, pos + 1, whichBG); + } + } + + return pos; +} + + +void RichTextBuilder::appendIRC(const char *str) { + // We're up for fun here! + const int colLayer = COL_LEVEL_IRC; + + bool b = false, i = false, u = false; + int activeFG = COL_DEFAULT_FG, activeBG = COL_DEFAULT_BG; + + int pos = 0; + int len = strlen(str); + + while (pos < len) { + char ch = str[pos]; + + if (ch < 0 || ch == 7 || ch == 10 || ch >= 0x20) { + // Pass this character through unfiltered. + writeU8(ch); + pos++; + continue; + } + + // IRC control code, but what? + if (ch == 2) { + b = !b; + if (b) + bold(); + else + endBold(); + } else if (ch == 0x1D) { + i = !i; + if (i) + italic(); + else + endItalic(); + } else if (ch == 0x1F) { + u = !u; + if (u) + underline(); + else + endUnderline(); + } else if (ch == 0xF) { + // Reset all formatting + if (b) { + endBold(); + b = false; + } + if (i) { + endItalic(); + i = false; + } + if (u) { + endUnderline(); + u = false; + } + if (activeFG != COL_DEFAULT_FG) { + endForeground(colLayer); + activeFG = COL_DEFAULT_FG; + } + if (activeBG != COL_DEFAULT_BG) { + endBackground(colLayer); + activeBG = COL_DEFAULT_BG; + } + } else if (ch == 0x16) { + // Reverse + int swap = activeBG; + activeBG = activeFG; + activeFG = swap; + + if (activeFG == COL_DEFAULT_FG) + endForeground(colLayer); + else + foreground(colLayer, activeFG); + + if (activeBG == COL_DEFAULT_BG) + endBackground(colLayer); + else + background(colLayer, activeBG); + + } else if (ch == 3) { + // Colours! + + int whichFG = -1, whichBG = -1; + + pos = matchIRCColourPair(str, pos + 1, &whichFG, &whichBG); + + if (whichFG == -1) { + if (activeFG != COL_DEFAULT_FG) { + activeFG = COL_DEFAULT_FG; + endForeground(colLayer); + } + } else { + activeFG = whichFG; + foreground(colLayer, whichFG); + } + + if (whichBG == -1) { + if (whichFG == -1 && activeBG != COL_DEFAULT_BG) { + activeBG = COL_DEFAULT_BG; + endBackground(colLayer); + } + } else { + activeBG = whichBG; + background(colLayer, whichBG); + } + + continue; + } + pos++; + } + + // Clean up any leftover state. + if (b) + endBold(); + if (i) + endItalic(); + if (u) + endUnderline(); + if (activeFG != COL_DEFAULT_FG) + endForeground(colLayer); + if (activeBG != COL_DEFAULT_BG) + endBackground(colLayer); +} + +const char *RichTextBuilder::c_str() { + if (size() == 0 || data()[size() - 1] != 0) + writeU8(0); + + return data(); +} diff --git a/bouncer/richtext.h b/bouncer/richtext.h new file mode 100644 index 0000000..608a1d6 --- /dev/null +++ b/bouncer/richtext.h @@ -0,0 +1,92 @@ +#include "buffer.h" + +enum ColourPresets { + COL_IRC_WHITE = 0, + COL_IRC_BLACK = 1, + COL_IRC_BLUE = 2, + COL_IRC_GREEN = 3, + COL_IRC_RED = 4, + COL_IRC_BROWN = 5, + COL_IRC_PURPLE = 6, + COL_IRC_ORANGE = 7, + COL_IRC_YELLOW = 8, + COL_IRC_LIME = 9, + COL_IRC_TEAL = 10, + COL_IRC_CYAN = 11, + COL_IRC_LIGHT_BLUE = 12, + COL_IRC_PINK = 13, + COL_IRC_GREY = 14, + COL_IRC_LIGHT_GREY = 15, + + COL_DEFAULT_FG = 16, + COL_DEFAULT_BG = 17, + + COL_ACTION = 18, + COL_JOIN = 19, + COL_PART = 20, + COL_QUIT = 21, + COL_KICK = 22, + COL_CHANNEL_NOTICE = 23, +}; + +enum ColourLevels { + COL_LEVEL_BASE = 0, + COL_LEVEL_IRC = 1, + COL_LEVEL_NICK = 2, +}; + +class RichTextBuilder : public Buffer { +public: + void bold() { writeU8(1); } + void endBold() { writeU8(2); } + + void italic() { writeU8(3); } + void endItalic() { writeU8(4); } + + void underline() { writeU8(5); } + void endUnderline() { writeU8(6); } + + void colour(bool background, int layer, int r, int g, int b) { + writeU8(0x10 + (background ? 4 : 0) + layer); + writeU8((r==0)?2:(r&254)); + writeU8((g==0)?1:g); + writeU8((b==0)?1:b); + } + void colour(bool background, int layer, int col) { + writeU8(0x10 + (background ? 4 : 0) + layer); + writeU8((col << 1) | 1); + } + + void endColour(bool background, int layer) { + writeU8(0x18 + (background ? 4 : 0) + layer); + } + + void foreground(int layer, int r, int g, int b) { + colour(false, layer, r, g, b); + } + void foreground(int layer, int col) { + colour(false, layer, col); + } + void endForeground(int layer) { + endColour(false, layer); + } + + void background(int layer, int r, int g, int b) { + colour(true, layer, r, g, b); + } + void background(int layer, int col) { + colour(true, layer, col); + } + void endBackground(int layer) { + endColour(true, layer); + } + + void append(const char *str) { + Buffer::append(str, strlen(str)); + } + + void appendIRC(const char *str); + + const char *c_str(); +}; + diff --git a/bouncer/window.cpp b/bouncer/window.cpp index 88e8247..6085304 100644 --- a/bouncer/window.cpp +++ b/bouncer/window.cpp @@ -1,4 +1,5 @@ #include "core.h" +#include "richtext.h" Window::Window(NetCore *_core) { core = _core; @@ -29,7 +30,7 @@ void Window::notifyWindowRename() { Packet::B2C_WINDOW_RENAME, packet); } -void Window::pushMessage(const char *str) { +void Window::pushMessage(const char *str, int priority) { messages.push_back(str); bool createdPacket = false; @@ -39,6 +40,7 @@ void Window::pushMessage(const char *str) { if (core->clients[i]->isAuthed()) { if (!createdPacket) { packet.writeU32(id); + packet.writeU8(priority); packet.writeStr(str); createdPacket = true; } @@ -198,18 +200,7 @@ void Channel::handleUserInput(const char *str) { if (str[0] == '/') { if (strncmp(str, "/me ", 4) == 0) { - // The duplication of code between here and - // handlePrivmsg is ugly. TODO: fixme. - char prefix[2]; - prefix[0] = getEffectivePrefixChar(server->currentNick); - prefix[1] = 0; - - snprintf(msgBuf, sizeof(msgBuf), - "* %s%s %s", - prefix, - server->currentNick, - &str[4]); - pushMessage(msgBuf); + outputUserMessage(server->currentNick, &str[4], /*isAction=*/true); snprintf(msgBuf, sizeof(msgBuf), "PRIVMSG %s :\x01" "ACTION %s\x01", @@ -218,18 +209,7 @@ void Channel::handleUserInput(const char *str) { server->sendLine(msgBuf); } } else { - // Aaaand this is also pretty ugly ><;; - // TODO: fixme. - char prefix[2]; - prefix[0] = getEffectivePrefixChar(server->currentNick); - prefix[1] = 0; - - snprintf(msgBuf, sizeof(msgBuf), - "<%s%s> %s", - prefix, - server->currentNick, - str); - pushMessage(msgBuf); + outputUserMessage(server->currentNick, str, /*isAction=*/false); snprintf(msgBuf, sizeof(msgBuf), "PRIVMSG %s :%s", @@ -573,61 +553,61 @@ void Channel::handleMode(const UserRef &user, const char *str) { } void Channel::handlePrivmsg(const UserRef &user, const char *str) { - char prefix[2]; - prefix[0] = getEffectivePrefixChar(user.nick.c_str()); - prefix[1] = 0; + outputUserMessage(user.nick.c_str(), str, /*isAction=*/false); +} - char buf[15000]; - snprintf(buf, 15000, - "<%s%s> %s", - prefix, - user.nick.c_str(), - str); +void Channel::outputUserMessage(const char *nick, const char *str, bool isAction) { + RichTextBuilder rt; - pushMessage(buf); + if (isAction) { + rt.foreground(COL_LEVEL_BASE, COL_ACTION); + rt.append("* "); + } else { + rt.writeS8('<'); + } + + char prefix = getEffectivePrefixChar(nick); + if (prefix != 0) + rt.writeS8(prefix); + + rt.append(nick); + rt.append(isAction ? " " : "> "); + + rt.appendIRC(str); + + pushMessage(rt.c_str(), 2); } void Channel::handleCtcp(const UserRef &user, const char *type, const char *params) { char buf[15000]; if (strcmp(type, "ACTION") == 0) { - char prefix[2]; - prefix[0] = getEffectivePrefixChar(user.nick.c_str()); - prefix[1] = 0; - - snprintf(buf, sizeof(buf), - "* %s%s %s", - prefix, - user.nick.c_str(), - params); - + outputUserMessage(user.nick.c_str(), params, /*isAction=*/true); } else { snprintf(buf, sizeof(buf), "CTCP from %s : %s %s", user.nick.c_str(), type, params); + pushMessage(buf, 2); } - - pushMessage(buf); } void Channel::handleTopic(const UserRef &user, const char *message) { - char buf[1024]; + RichTextBuilder rt; + rt.foreground(COL_LEVEL_BASE, COL_CHANNEL_NOTICE); if (user.isValid) { - snprintf(buf, sizeof(buf), - "%s changed the topic to: %s", - user.nick.c_str(), - message); + rt.append(user.nick.c_str()); + rt.append(" changed the topic to: "); + rt.appendIRC(message); } else { - snprintf(buf, sizeof(buf), - "Topic: %s", - message); + rt.append("Topic: "); + rt.appendIRC(message); } - pushMessage(buf); + pushMessage(rt.c_str()); topic = message; @@ -639,12 +619,18 @@ void Channel::handleTopic(const UserRef &user, const char *message) { } void Channel::handleTopicInfo(const char *user, int timestamp) { - char buf[1024]; - snprintf(buf, sizeof(buf), - "Topic set by %s at %d", - user, - timestamp); - pushMessage(buf); + char intConv[50]; + snprintf(intConv, sizeof(intConv), "%d", timestamp); + + RichTextBuilder rt; + rt.foreground(COL_LEVEL_BASE, COL_CHANNEL_NOTICE); + + rt.append("Topic set by "); + rt.append(user); + rt.append(" at "); + rt.append(intConv); + + pushMessage(rt.c_str()); } @@ -764,7 +750,7 @@ void Query::handlePrivmsg(const char *str) { partner.c_str(), str); - pushMessage(buf); + pushMessage(buf, 2); } void Query::handleCtcp(const char *type, const char *params) { @@ -784,7 +770,7 @@ void Query::handleCtcp(const char *type, const char *params) { params); } - pushMessage(buf); + pushMessage(buf, 2); } void Query::renamePartner(const char *_partner) { diff --git a/python_client.py b/python_client.py index af0eecb..2f509bd 100644 --- a/python_client.py +++ b/python_client.py @@ -5,6 +5,33 @@ import sys, socket, ssl, threading, struct from PyQt5 import QtCore, QtGui, QtWidgets +PRESET_COLOURS = [ + QtGui.QColor(255,255,255), # COL_IRC_WHITE = 0, + QtGui.QColor( 0, 0, 0), # COL_IRC_BLACK = 1, + QtGui.QColor( 0, 0,128), # COL_IRC_BLUE = 2, + QtGui.QColor( 0,128, 0), # COL_IRC_GREEN = 3, + QtGui.QColor(255, 0, 0), # COL_IRC_RED = 4, + QtGui.QColor(128, 0, 0), # COL_IRC_BROWN = 5, + QtGui.QColor(128, 0,128), # COL_IRC_PURPLE = 6, + QtGui.QColor(128,128, 0), # COL_IRC_ORANGE = 7, + QtGui.QColor(255,255, 0), # COL_IRC_YELLOW = 8, + QtGui.QColor( 0,255, 0), # COL_IRC_LIME = 9, + QtGui.QColor( 0,128,128), # COL_IRC_TEAL = 10, + QtGui.QColor( 0,255,255), # COL_IRC_CYAN = 11, + QtGui.QColor( 0, 0,255), # COL_IRC_LIGHT_BLUE = 12, + QtGui.QColor(255, 0,255), # COL_IRC_PINK = 13, + QtGui.QColor(128,128,128), # COL_IRC_GREY = 14, + QtGui.QColor(192,192,192), # COL_IRC_LIGHT_GREY = 15, + QtGui.QColor( 0, 0, 0), # COL_DEFAULT_FG = 16, + QtGui.QColor(255,255,255), # COL_DEFAULT_BG = 17, + QtGui.QColor(102, 0,204), # COL_ACTION = 18, + QtGui.QColor( 0,153, 0), # COL_JOIN = 19, + QtGui.QColor(102, 0, 0), # COL_PART = 20, + QtGui.QColor(102, 0, 0), # COL_QUIT = 21, + QtGui.QColor(102, 0, 0), # COL_KICK = 22, + QtGui.QColor( 51,102,153), # COL_CHANNEL_NOTICE = 23, +] + protocolVer = 1 sock = None authed = False @@ -139,6 +166,9 @@ class WindowTab(QtWidgets.QWidget): self.output = QtWidgets.QTextEdit(self) self.output.setReadOnly(True) + self.output.setTextColor(PRESET_COLOURS[16]) + self.output.setTextBackgroundColor(PRESET_COLOURS[17]) + self.input = QtWidgets.QLineEdit(self) self.input.returnPressed.connect(self.handleLineEntered) @@ -160,7 +190,80 @@ class WindowTab(QtWidgets.QWidget): isAtEnd = cursor.atEnd() cursor.movePosition(QtGui.QTextCursor.End) cursor.clearSelection() - cursor.insertText(msg) + + fmt = QtGui.QTextCharFormat() + + foreground = [None,None,None,None] + background = [None,None,None,None] + + pos = 0 + build = '' + l = len(msg) + while pos < l: + char = msg[pos] + c = ord(char) + if c == 7 or c == 10 or c >= 0x20: + build += char + else: + cursor.insertText(build, fmt) + build = '' + + if c == 1: + fmt.setFontWeight(QtGui.QFont.Bold) + elif c == 2: + fmt.setFontWeight(QtGui.QFont.Normal) + elif c == 3: + fmt.setFontItalic(True) + elif c == 4: + fmt.setFontItalic(False) + elif c == 5: + fmt.setFontUnderline(True) + elif c == 6: + fmt.setFontUnderline(False) + elif (c >= 0x10 and c <= 0x1F): + layer = c & 3 + isBG = (True if ((c & 4) == 4) else False) + isEnd = (True if ((c & 8) == 8) else False) + + col = None + + if not isEnd and (pos + 1) < l: + bit1 = ord(msg[pos + 1]) + col = None + if (bit1 & 1) == 1: + col = PRESET_COLOURS[bit1 >> 1] + pos += 1 + elif (pos + 3) < l: + bit2 = ord(msg[pos + 2]) + bit3 = ord(msg[pos + 3]) + pos += 3 + col = QtGui.QColor(bit1,bit2,bit3) + + if isBG: + array = background + else: + array = foreground + + array[layer] = col + maxcol = None + for check in array: + if check is not None: + maxcol = check + + if isBG: + if maxcol: + fmt.setBackground(QtGui.QBrush(maxcol)) + else: + fmt.clearBackground() + else: + if maxcol: + fmt.setForeground(QtGui.QBrush(maxcol)) + else: + fmt.clearForeground() + + + pos += 1 + cursor.insertText(build, fmt) cursor.insertText('\n') if isAtEnd: @@ -392,8 +495,8 @@ class MainWindow(QtWidgets.QMainWindow): elif ptype == 0x102: # WINDOW MESSAGES - wndID, msglen = struct.unpack_from('<II', pdata, 0) - msg = pdata[8:8+msglen].decode('utf-8', 'replace') + wndID, priority, msglen = struct.unpack_from('<IbI', pdata, 0) + msg = pdata[9:9+msglen].decode('utf-8', 'replace') self.tabLookup[wndID].pushMessage(msg) elif ptype == 0x103: # WINDOW RENAME |