]> git.xolatile.top Git - public-moontalk.git/commitdiff
New stuff (and notices)
authorEmil Williams <emilwilliams@tuta.io>
Sun, 11 Feb 2024 01:02:02 +0000 (01:02 +0000)
committerEmil Williams <emilwilliams@tuta.io>
Sun, 11 Feb 2024 01:02:02 +0000 (01:02 +0000)
README
bots/README [new file with mode: 0644]
bots/moonchat/README [new file with mode: 0644]
bots/moonchat/moonchat.py [new file with mode: 0644]
bots/moonchat/scramble-bot.py [new file with mode: 0644]
bots/moonchat/who-bot.py [new file with mode: 0644]
client/moontalk-cli.c
server/README [new file with mode: 0644]

diff --git a/README b/README
index b60a903897df8273d0c2ca33a60514e5bd1a9416..bc9c1af11cc61a74197140f609d463fefcad35e2 100644 (file)
--- a/README
+++ b/README
@@ -1,6 +1,10 @@
 MOONTALK
 
-See client/README
+See client/README for the clients
 
-Licensing...
-Everything is licensed under GPLv3 or GPLv3+ under the respective owners.
+See bots/README for the bots
+
+See server/README for the servers
+
+Licensing... Everything unless otherwise specified is licensed
+under GPLv3 or GPLv3+ under the respective owners.
diff --git a/bots/README b/bots/README
new file mode 100644 (file)
index 0000000..8dab0d4
--- /dev/null
@@ -0,0 +1,4 @@
+Bots.
+
+There are exactly two currently, both housed within the moonchat directory.
+See moonchat/README.
diff --git a/bots/moonchat/README b/bots/moonchat/README
new file mode 100644 (file)
index 0000000..fbc58aa
--- /dev/null
@@ -0,0 +1,15 @@
+Some rephrased notes from the author:
+
+Use torify, obviously,
+
+-- RUNNING --
+
+scramble-bot:
+  python3 scramble-bot.py < /usr/share/dict/american-english
+
+  the !scramble command should now work.
+
+who-bot:
+  python3 who-bot.py
+
+  [who] [whoami]
diff --git a/bots/moonchat/moonchat.py b/bots/moonchat/moonchat.py
new file mode 100644 (file)
index 0000000..9caf7b9
--- /dev/null
@@ -0,0 +1,98 @@
+import asyncio
+import re
+import typing
+
+
+server_message_regex = re.compile(r"^(?P<nickname>[\w\s]+):\s*(?P<content>.*)$")
+
+
+class MoonchatMessage(typing.NamedTuple):
+    nickname: str
+    content: str
+
+
+class MessageDecodeError(ValueError):
+    pass
+
+
+class Moonchat:
+    def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, encoding: str):
+        self.reader = reader
+        self.writer = writer
+        self.encoding = encoding
+        self.closed = False
+
+    def close(self):
+        if self.closed:
+            return
+        self.closed = True
+        if not self.writer.is_closing():
+            if self.writer.can_write_eof():
+                self.writer.write_eof()
+            self.writer.close()
+
+    @staticmethod
+    async def connect(ip: str, port: int, encoding='ascii', **kwargs):
+        """Provide the hostname, port and optional arguments to open_connection."""
+        streams = await asyncio.open_connection(ip, port, **kwargs)
+        return Moonchat(*streams, encoding=encoding) if encoding else Moonchat(*streams)
+
+    def encode_message(self, message: str) -> bytes:
+        """Return encoded raw data with trailing newline if required."""
+        return (message.removesuffix('\n')+'\n').encode(self.encoding)
+
+    def decode_message(self, data: bytes) -> MoonchatMessage:
+        """Return decoded raw data without trailing newlines."""
+        unparsed = (data.decode(self.encoding)).strip()
+        regex_match = server_message_regex.match(unparsed)
+        if not regex_match:
+            raise ValueError("cannot decode malformed message: " + unparsed)
+        return MoonchatMessage(**regex_match.groupdict())
+
+    async def send_message(self, message: str) -> bool:
+        """Sends string to chat. Return whether successful."""
+        encoded_message = self.encode_message(message)
+        return await self.send_message_raw(encoded_message)
+
+    async def send_message_raw(self, message: bytes | bytearray | memoryview) -> bool:
+        """Send raw data straight to the server if you feel like it. Return True if successful."""
+        if self.closed:
+            return False
+        if self.writer.is_closing():
+            self.close()
+            return False
+        self.writer.write(message)
+        await self.writer.drain()
+        return True
+
+    async def recieve_message_raw(self) -> bytes | None:
+        """Retrieve the next line from the server, or None if there are no more messages."""
+        if self.closed:
+            return None
+        line = await self.reader.readline()
+        if b'\n' not in line: # partial reads mean we're out of data
+            self.close()
+            return None
+        return line
+
+    async def recieve_message(self) -> MoonchatMessage | None:
+        """Retrieve the next message from the server."""
+        raw_message = await self.recieve_message_raw()
+        return self.decode_message(raw_message) if raw_message else None
+
+    async def raw_messages(self):
+        """Yield raw unencoded messages until connection is closed."""
+        while not self.closed:
+            if message := await self.recieve_message_raw():
+                yield message
+
+    async def messages(self, ignore_invalid=False):
+        """Yield messages until the connection is closed"""
+        while not self.closed:
+            try:
+                message = await self.recieve_message()
+            except MessageDecodeError as err:
+                if not ignore_invalid:
+                    raise err
+            if message:
+                yield message
diff --git a/bots/moonchat/scramble-bot.py b/bots/moonchat/scramble-bot.py
new file mode 100644 (file)
index 0000000..a7469fd
--- /dev/null
@@ -0,0 +1,58 @@
+# TODO: add a fucking scoreboard or something? this is a copy of a Espernet bot.
+from moonchat import *
+import sys
+import random
+import io
+
+class Bot:
+    def __init__(self, chat: Moonchat, words: list[str]):
+        self.chat = chat
+        self.words = words
+
+    async def next_winner(self, word: str, limit: float):
+        try:
+            async with asyncio.timeout(limit):
+                async for message in self.chat.messages():
+                    if word in message.content.lower():
+                        return message
+        except TimeoutError:
+            return None
+
+    async def handle_incoming(self):
+        limit = 60
+        async for message in self.chat.messages():
+            if message.nickname == 'Server':
+                continue # ignore the server
+            if "!scramble" not in message.content:
+                continue
+            print(f"GAME REQUESTED: {message=}")
+            selected_word = random.choice(self.words)
+            scrambled_word = ''.join(random.sample(selected_word, len(selected_word)))
+            print(f"GAME START: {scrambled_word} is {selected_word}")
+            await self.chat.send_message(f"Unscramble in {limit} seconds to win! The word is: {scrambled_word}.")
+            winner = await self.next_winner(selected_word, limit)
+            print(f"GAME OVER: {winner=}")
+            if winner:
+                await self.chat.send_message(f"The word was {selected_word}. {winner.nickname} wins!")
+            else:
+                await self.chat.send_message(f"Time's up! The word was {selected_word}. No one wins.")
+
+async def main(words: list[str]):
+    chat = await Moonchat.connect("7ks473deh6ggtwqsvbqdurepv5i6iblpbkx33b6cydon3ajph73sssad.onion", 50000)
+    bot = Bot(chat, words)
+    await chat.send_message("To play scramble say: !scramble")
+    await bot.handle_incoming()
+
+
+def load_words(file: io.TextIOBase):
+    for line in file:
+        line = line.strip().lower()
+        if "'" not in line and len(line) == 5:
+            yield line
+
+
+if __name__ == "__main__":
+    import asyncio
+    words = list(load_words(sys.stdin))
+    print(f"Loaded {len(words)} words")
+    asyncio.run(main(words))
diff --git a/bots/moonchat/who-bot.py b/bots/moonchat/who-bot.py
new file mode 100644 (file)
index 0000000..93bd514
--- /dev/null
@@ -0,0 +1,82 @@
+import re
+from datetime import datetime, timedelta
+from moonchat import *
+
+class Bot:
+    def __init__(self, chat: Moonchat, command_matcher: re.Pattern):
+        self.chat = chat
+        self.command_matcher = command_matcher
+        self.commands = dict()
+        self.last_annoyed = datetime.now()
+        self.seen = dict()
+
+    async def handle_incoming(self):
+        async for message in self.chat.messages():
+            now = datetime.now()
+            if message.nickname == 'Server':
+                continue # ignore the server
+            last_seen = self.seen.get(message.nickname, None)
+            if last_seen:
+                seen_delta = now - last_seen
+                if seen_delta > timedelta(hours=2):
+                    last_seen = None
+            if not last_seen:
+                if (now - self.last_annoyed) > timedelta(minutes=10):
+                    await self.chat.send_message(f"hello {message.nickname}! i am a robot. say [help]")
+                    self.last_annoyed = now
+            self.seen[message.nickname] = datetime.now()
+            match = self.command_matcher.search(message.content)
+            if not match:
+                continue # ignore not our messages
+            command = match.groupdict().get('command', None)
+            if not command:
+                continue # ????
+            split = command.split()
+            if not len(split):
+                continue # ????????????
+            exector = split[0]
+            command_function = self.commands.get(exector, None)
+            if command_function:
+                await command_function(self, message, split)
+            else:
+                await self.chat.send_message(f"{message.nickname}: sorry that's not a valid command")
+
+async def who_command(bot: Bot, message: MoonchatMessage, args):
+    """See recent users"""
+    now = datetime.now()
+    result = "Users from last 1hour: "
+    for username, last_seen in bot.seen.items():
+        delta: timedelta = (now - last_seen)
+        if delta < timedelta(hours=1):
+            minutes, seconds = divmod(delta.seconds, 60)
+            result += f"{username}({minutes}m{seconds}s), "
+    await bot.chat.send_message(result)
+
+async def whoami(bot: Bot, message: MoonchatMessage, args):
+    """Print your nickname"""
+    await bot.chat.send_message(message.nickname)
+
+async def help(bot: Bot, message: MoonchatMessage, args):
+    command = args[1] if len(args) > 1 else None
+    command_function = bot.commands.get(command, None)
+    if command_function:
+        await bot.chat.send_message(f"{command}: {command_function.__doc__}")
+        return
+    command_list = ', '.join(bot.commands.keys())
+    await bot.chat.send_message(f"Commands available: {command_list}")
+
+matcher = re.compile(r"\[(?P<command>[\w\s]+)\]")
+
+async def main():
+    chat = await Moonchat.connect("7ks473deh6ggtwqsvbqdurepv5i6iblpbkx33b6cydon3ajph73sssad.onion", 50000)
+    bot = Bot(chat, matcher)
+    bot.commands["help"] = help
+    bot.commands['who'] = who_command
+    bot.commands['whoami'] = whoami
+    await chat.send_message("i am a robot! do [help]")
+    await bot.handle_incoming()
+
+
+if __name__ == "__main__":
+    import asyncio
+    asyncio.run(main())
index be680583283d46e753b67db5804ff7f225c46b96..048dfcf1a6e26cf7f1cb4bd445138b7b1c66dca6 100644 (file)
@@ -31,7 +31,7 @@
 
 int g_sockfd;
 
-#define        g_y LINES
+#define g_y LINES
 #define g_x COLS
 
 #define HELP \
@@ -115,7 +115,7 @@ void clearline(WINDOW * w, int y) {
 void sanitize(char * buf, size_t rem) {
        char * base = buf;
        buf += rem;
-       while (buf - base) {
+       while (*buf && buf - base) {
                if (*buf < ' ' || *buf > '~') {
                        if (*buf != '\n')
                        { *buf = '!'; }
@@ -163,6 +163,16 @@ int main (int argc, char ** argv) {
        nodelay(stdscr, TRUE);
        ESCDELAY = 0;
        curs_set(0);
+       if (has_colors() && can_change_color()) {
+               short bg, fg;
+               /* leaks memory :( */
+               start_color();
+               for (bg = 0; bg < 8; ++bg) {
+                       for (fg = 0; fg < 8; ++fg) {
+                               init_pair(16 + fg + (bg * 8), fg, bg);
+                       }
+               }
+       }
        clear();
 
        #define WINCOUNT 3
@@ -238,9 +248,11 @@ hardrefresh:
                        }
                        else if ((ch > 31 && ch < 127)) {
                                if (sendlen + 1 < SENDMAX)
-                               { sendbuf[edit++] = ch; ++sendlen; }
-                               /* mvwchgat(input, 2, sendlen - 1, 1, A_REVERSE, 0, NULL); */
-                               mvwaddnstr(input, 2, 0, sendbuf, sendlen);
+                               {
+                                       memmove(sendbuf + edit + 1, sendbuf + edit, sendlen - edit);
+                                       sendbuf[edit++] = ch; ++sendlen;
+                               }
+                               inputrefresh = 1;
                        }
                        else if (ch == '\n') {
                                if (sendlen == sendminlen)
@@ -254,63 +266,73 @@ hardrefresh:
                                } else {
                                        mvwprintw(input, 1, 0, "message failed: %s", strerror(errno));
                                }
-                               /* mvwaddch(0, sendminlen, ' '); */
-                               /* mvwchgat(input, 2, 0, 1, A_STANDOUT, 0, NULL); */
                                bodyrefresh = inputrefresh = 1;
-                               clearline(input, 2);
                                edit = sendlen = sendminlen;
                        }
                        else if (ch == BACKSPACE || ch == C_H) {
                                inputrefresh = 1;
-                               clearline(input, 2);
-                               if (sendlen - 1 >= sendminlen)
-                               { mvwaddch(input, 2, --sendlen, ' '); --edit; }
-                               mvwaddnstr(input, 2, 0, sendbuf, sendlen);
-                               wmove(input, 2, sendlen);
+                               if (sendlen - 1 >= sendminlen && edit - 1 >= sendminlen)
+                               {
+                                       memmove(sendbuf + edit - 1, sendbuf + edit, sendlen - edit);
+                                       --sendlen; --edit;
+                               }
+                               inputrefresh = 1;
                        }
                        else if (ch == KEY_LEFT) {
-                               /* if (edit > sendminlen) { --edit; } */
+                               if (edit > sendminlen) { --edit; }
                        }
                        else if (ch == KEY_RIGHT) {
-                               /* if (edit - 1 < sendlen) { ++edit; } */
+                               if (edit < sendlen) { ++edit; }
                        }
                        else if (ch == KEY_DOWN) {
                                mvwprintw(input, 1, 150, "scroll down %ld", offlen);
-                               while (off - recvbuf < RECVMAX && *off != '\n') { ++off; }
+                               while ((size_t)(off - recvbuf) < recvlen && *off != '\n') { ++off; }
                                if (*off == '\n') { ++off; }
                                wclear(body);
                                bodyrefresh = 1;
                        }
                        else if (ch == KEY_UP) {
                                mvwprintw(input, 1, 150, "scroll up   %ld", offlen);
-                               while (off - recvbuf > 0) { --off; }
-                               /* wclear(body); */
+                               if (off - 2 - recvbuf > 0) { off -= 2; }
+                               while (off - recvbuf > 0 && *off != '\n') { --off; }
+                               if (*off == '\n') { ++off; }
                                bodyrefresh = 1;
                        }
                        else if (ch == C_W) {
-                               while (sendlen > sendminlen && ispunct(sendbuf[sendlen - 1])) { --sendlen; }
-                               while (sendlen > sendminlen && isspace(sendbuf[sendlen - 1])) { --sendlen; }
-                               while (sendlen > sendminlen && isalnum(sendbuf[sendlen - 1])) { --sendlen; }
+                               i = edit;
+                               while (i > sendminlen && isspace(sendbuf[i - 1])) { --i; }
+                               while (i > sendminlen && !isspace(sendbuf[i - 1])) { --i; }
+                               if (i == edit) { continue; }
+                               mvwprintw(input, 1, 200, "diff:%ld", sendlen - edit);
+                               /* memmove(sendbuf + i, sendbuf + edit, sendlen - edit); */
+                               /* sendlen -= edit; */
+                               /* edit = i; */
+                               /* mvwprintw(input, 1, 200, "i:%ld:%ld:sendl:%3ld", */
+                               /* i - sendminlen, (sendbuf + edit) - (sendbuf + i), sendlen - sendminlen); */
                                inputrefresh = 1;
-                               clearline(input, 2);
                        }
-
                }
                /* update and rendering */
-               if (ct % frame == 0 || inputrefresh || bodyrefresh) {
-                       UPDATE_TIME();
-                       /* wclear(input); */
+               if (inputrefresh) {
+                       clearline(input, 2);
                        mvwaddnstr(input, 2, 0, sendbuf, sendlen);
+                       mvwchgat(input, 2, edit, 1, A_REVERSE, 0, NULL);
+               }
+
+               if (ct % frame == 0) {
+                       UPDATE_TIME();
+               }
 
+               if (ct % frame == 0 || bodyrefresh) {
                        ret = recv(sockfd, recvbuf + recvlen, RECVMAX - recvlen, MSG_DONTWAIT);
-                       if (errno != EAGAIN)
+                       if (errno && errno != EAGAIN)
                        { mvwaddstr(input, 1, 0, strerror(errno)); }
                        if (bodyrefresh) {
                                bodyrefresh = 0;
-                               if (!(ret > -1))
+                               if (!(ret > 0))
                                goto _bodyrefresh;
                        }
-                       if (ret > -1) {
+                       if (ret > 0) {
                                sanitize(recvbuf + recvlen, ret);
                                if (ret + recvlen < RECVMAX)
                                {
diff --git a/server/README b/server/README
new file mode 100644 (file)
index 0000000..0c368ef
--- /dev/null
@@ -0,0 +1,9 @@
+Servers.
+
+There is one real server, written in the ever brilliant Forth, is housed
+within eventloop-server-experiment/, which is the origin of all things
+Moontalk. OP, who created it, is a legend and killed a dragon using only
+Forth and Sockets.
+
+blackhole/ just eats sent messages, made for client feedback testing
+only, stolen from beej.