From 51c96a4abfee298f258dd5da29b55e5d63fec47f Mon Sep 17 00:00:00 2001 From: Emil Williams Date: Sun, 11 Feb 2024 01:02:02 +0000 Subject: [PATCH] New stuff (and notices) --- README | 10 +++-- bots/README | 4 ++ bots/moonchat/README | 15 +++++++ bots/moonchat/moonchat.py | 98 +++++++++++++++++++++++++++++++++++++++++++ bots/moonchat/scramble-bot.py | 58 +++++++++++++++++++++++++ bots/moonchat/who-bot.py | 82 ++++++++++++++++++++++++++++++++++++ client/moontalk-cli.c | 80 ++++++++++++++++++++++------------- server/README | 9 ++++ 8 files changed, 324 insertions(+), 32 deletions(-) create mode 100644 bots/README create mode 100644 bots/moonchat/README create mode 100644 bots/moonchat/moonchat.py create mode 100644 bots/moonchat/scramble-bot.py create mode 100644 bots/moonchat/who-bot.py create mode 100644 server/README diff --git a/README b/README index b60a903..bc9c1af 100644 --- 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 index 0000000..8dab0d4 --- /dev/null +++ b/bots/README @@ -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 index 0000000..fbc58aa --- /dev/null +++ b/bots/moonchat/README @@ -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 index 0000000..9caf7b9 --- /dev/null +++ b/bots/moonchat/moonchat.py @@ -0,0 +1,98 @@ +import asyncio +import re +import typing + + +server_message_regex = re.compile(r"^(?P[\w\s]+):\s*(?P.*)$") + + +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 index 0000000..a7469fd --- /dev/null +++ b/bots/moonchat/scramble-bot.py @@ -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 index 0000000..93bd514 --- /dev/null +++ b/bots/moonchat/who-bot.py @@ -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[\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()) diff --git a/client/moontalk-cli.c b/client/moontalk-cli.c index be68058..048dfcf 100644 --- a/client/moontalk-cli.c +++ b/client/moontalk-cli.c @@ -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 index 0000000..0c368ef --- /dev/null +++ b/server/README @@ -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.