New stuff (and notices)
This commit is contained in:
parent
61c60dcf0a
commit
51c96a4abf
10
README
10
README
@ -1,6 +1,10 @@
|
|||||||
MOONTALK
|
MOONTALK
|
||||||
|
|
||||||
See client/README
|
See client/README for the clients
|
||||||
|
|
||||||
Licensing...
|
See bots/README for the bots
|
||||||
Everything is licensed under GPLv3 or GPLv3+ under the respective owners.
|
|
||||||
|
See server/README for the servers
|
||||||
|
|
||||||
|
Licensing... Everything unless otherwise specified is licensed
|
||||||
|
under GPLv3 or GPLv3+ under the respective owners.
|
||||||
|
4
bots/README
Normal file
4
bots/README
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Bots.
|
||||||
|
|
||||||
|
There are exactly two currently, both housed within the moonchat directory.
|
||||||
|
See moonchat/README.
|
15
bots/moonchat/README
Normal file
15
bots/moonchat/README
Normal file
@ -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]
|
98
bots/moonchat/moonchat.py
Normal file
98
bots/moonchat/moonchat.py
Normal file
@ -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
|
58
bots/moonchat/scramble-bot.py
Normal file
58
bots/moonchat/scramble-bot.py
Normal file
@ -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))
|
82
bots/moonchat/who-bot.py
Normal file
82
bots/moonchat/who-bot.py
Normal file
@ -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())
|
@ -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); */
|
memmove(sendbuf + edit + 1, sendbuf + edit, sendlen - edit);
|
||||||
mvwaddnstr(input, 2, 0, sendbuf, sendlen);
|
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 && edit - 1 >= sendminlen)
|
||||||
if (sendlen - 1 >= sendminlen)
|
{
|
||||||
{ mvwaddch(input, 2, --sendlen, ' '); --edit; }
|
memmove(sendbuf + edit - 1, sendbuf + edit, sendlen - edit);
|
||||||
mvwaddnstr(input, 2, 0, sendbuf, sendlen);
|
--sendlen; --edit;
|
||||||
wmove(input, 2, sendlen);
|
}
|
||||||
|
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; }
|
if (off - 2 - recvbuf > 0) { off -= 2; }
|
||||||
/* wclear(body); */
|
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; }
|
i = edit;
|
||||||
while (sendlen > sendminlen && isspace(sendbuf[sendlen - 1])) { --sendlen; }
|
while (i > sendminlen && isspace(sendbuf[i - 1])) { --i; }
|
||||||
while (sendlen > sendminlen && isalnum(sendbuf[sendlen - 1])) { --sendlen; }
|
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) {
|
if (inputrefresh) {
|
||||||
UPDATE_TIME();
|
clearline(input, 2);
|
||||||
/* wclear(input); */
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
9
server/README
Normal file
9
server/README
Normal file
@ -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.
|
Loading…
Reference in New Issue
Block a user