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.
--- /dev/null
+Bots.
+
+There are exactly two currently, both housed within the moonchat directory.
+See moonchat/README.
--- /dev/null
+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]
--- /dev/null
+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
--- /dev/null
+# 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))
--- /dev/null
+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())
int g_sockfd;
-#define g_y LINES
+#define g_y LINES
#define g_x COLS
#define HELP \
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 = '!'; }
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
}
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)
} 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)
{
--- /dev/null
+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.