@@ -1,6 +1,10 @@ | |||||
MOONTALK | 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. |
@@ -0,0 +1,4 @@ | |||||
Bots. | |||||
There are exactly two currently, both housed within the moonchat directory. | |||||
See 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] |
@@ -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 |
@@ -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)) |
@@ -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()) |
@@ -31,7 +31,7 @@ | |||||
int g_sockfd; | int g_sockfd; | ||||
#define g_y LINES | |||||
#define g_y LINES | |||||
#define g_x COLS | #define g_x COLS | ||||
#define HELP \ | #define HELP \ | ||||
@@ -115,7 +115,7 @@ void clearline(WINDOW * w, int y) { | |||||
void sanitize(char * buf, size_t rem) { | void sanitize(char * buf, size_t rem) { | ||||
char * base = buf; | char * base = buf; | ||||
buf += rem; | buf += rem; | ||||
while (buf - base) { | |||||
while (*buf && buf - base) { | |||||
if (*buf < ' ' || *buf > '~') { | if (*buf < ' ' || *buf > '~') { | ||||
if (*buf != '\n') | if (*buf != '\n') | ||||
{ *buf = '!'; } | { *buf = '!'; } | ||||
@@ -163,6 +163,16 @@ int main (int argc, char ** argv) { | |||||
nodelay(stdscr, TRUE); | nodelay(stdscr, TRUE); | ||||
ESCDELAY = 0; | ESCDELAY = 0; | ||||
curs_set(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(); | clear(); | ||||
#define WINCOUNT 3 | #define WINCOUNT 3 | ||||
@@ -238,9 +248,11 @@ hardrefresh: | |||||
} | } | ||||
else if ((ch > 31 && ch < 127)) { | else if ((ch > 31 && ch < 127)) { | ||||
if (sendlen + 1 < SENDMAX) | 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') { | else if (ch == '\n') { | ||||
if (sendlen == sendminlen) | if (sendlen == sendminlen) | ||||
@@ -254,63 +266,73 @@ hardrefresh: | |||||
} else { | } else { | ||||
mvwprintw(input, 1, 0, "message failed: %s", strerror(errno)); | mvwprintw(input, 1, 0, "message failed: %s", strerror(errno)); | ||||
} | } | ||||
/* mvwaddch(0, sendminlen, ' '); */ | |||||
/* mvwchgat(input, 2, 0, 1, A_STANDOUT, 0, NULL); */ | |||||
bodyrefresh = inputrefresh = 1; | bodyrefresh = inputrefresh = 1; | ||||
clearline(input, 2); | |||||
edit = sendlen = sendminlen; | edit = sendlen = sendminlen; | ||||
} | } | ||||
else if (ch == BACKSPACE || ch == C_H) { | else if (ch == BACKSPACE || ch == C_H) { | ||||
inputrefresh = 1; | 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) { | else if (ch == KEY_LEFT) { | ||||
/* if (edit > sendminlen) { --edit; } */ | |||||
if (edit > sendminlen) { --edit; } | |||||
} | } | ||||
else if (ch == KEY_RIGHT) { | else if (ch == KEY_RIGHT) { | ||||
/* if (edit - 1 < sendlen) { ++edit; } */ | |||||
if (edit < sendlen) { ++edit; } | |||||
} | } | ||||
else if (ch == KEY_DOWN) { | else if (ch == KEY_DOWN) { | ||||
mvwprintw(input, 1, 150, "scroll down %ld", offlen); | 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; } | if (*off == '\n') { ++off; } | ||||
wclear(body); | wclear(body); | ||||
bodyrefresh = 1; | bodyrefresh = 1; | ||||
} | } | ||||
else if (ch == KEY_UP) { | else if (ch == KEY_UP) { | ||||
mvwprintw(input, 1, 150, "scroll up %ld", offlen); | 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; | bodyrefresh = 1; | ||||
} | } | ||||
else if (ch == C_W) { | 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; | inputrefresh = 1; | ||||
clearline(input, 2); | |||||
} | } | ||||
} | } | ||||
/* update and rendering */ | /* update and rendering */ | ||||
if (ct % frame == 0 || inputrefresh || bodyrefresh) { | |||||
UPDATE_TIME(); | |||||
/* wclear(input); */ | |||||
if (inputrefresh) { | |||||
clearline(input, 2); | |||||
mvwaddnstr(input, 2, 0, sendbuf, sendlen); | 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); | ret = recv(sockfd, recvbuf + recvlen, RECVMAX - recvlen, MSG_DONTWAIT); | ||||
if (errno != EAGAIN) | |||||
if (errno && errno != EAGAIN) | |||||
{ mvwaddstr(input, 1, 0, strerror(errno)); } | { mvwaddstr(input, 1, 0, strerror(errno)); } | ||||
if (bodyrefresh) { | if (bodyrefresh) { | ||||
bodyrefresh = 0; | bodyrefresh = 0; | ||||
if (!(ret > -1)) | |||||
if (!(ret > 0)) | |||||
goto _bodyrefresh; | goto _bodyrefresh; | ||||
} | } | ||||
if (ret > -1) { | |||||
if (ret > 0) { | |||||
sanitize(recvbuf + recvlen, ret); | sanitize(recvbuf + recvlen, ret); | ||||
if (ret + recvlen < RECVMAX) | if (ret + recvlen < RECVMAX) | ||||
{ | { | ||||
@@ -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. |