Portmanteau bot for Mastodon
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

246 lines
6.7 KiB

3 years ago
import os
import sys
import time
from datetime import datetime, timedelta, timezone
3 years ago
import toml
import random
import re
import sched
3 years ago
import math
import string
3 years ago
from mastodon import Mastodon, MastodonNotFoundError
from fedbot.bot import Bot, BotClient
3 years ago
TEST = "test" in sys.argv[1:]
USER_PCT = 35
MIN_MAIN_LEN = 3
MAX_PORT_LEN = 14
MAIN_DICT_PATH = "dicts/main.csv"
USER_DICT_PATH = "dicts/user.dict"
USED_DICT_PATH = "dicts/used.dict"
3 years ago
def next_dt():
dt = datetime.now(timezone.utc)
dt -= timedelta(hours = -1,
minutes = dt.minute,
3 years ago
seconds = dt.second,
microseconds = dt.microsecond)
return dt
config_path = os.path.join(os.path.dirname(sys.argv[0]), "config.toml")
loaded_config = {
"name": "portmanteaubot",
**toml.load(config_path)}
AFFIXES = []
3 years ago
def is_affixed(word):
return any(re.search(suf, word) for suf in AFFIXES)
3 years ago
3 years ago
def overlap_words(left_word, right_word):
if left_word == right_word:
return set()
3 years ago
min_shift = 2
offset = min_shift
attempts = set()
while offset + 2 <= len(left_word[0]):
if right_word[0].lower().startswith(left_word[0].lower()[offset : offset + 2]):
word_str = left_word[0][:offset] + right_word[0]
if len(word_str) >= 6 and not is_affixed(word_str):
attempts.add(word_str)
3 years ago
offset += 1
offset = len(right_word[0]) - 2
while offset >= 2:
if left_word[0].lower().endswith(right_word[0].lower()[offset : offset + 2]):
word_str = left_word[0] + right_word[0][offset + 2:]
if len(word_str) >= 6 and not is_affixed(word_str):
attempts.add(word_str)
3 years ago
offset -= 1
return attempts
3 years ago
def word_weight(index, length, power):
3 years ago
a = pow((index + 1) / length, 2)
return int(350000 * a)
3 years ago
def weights_for(words, power):
return [word_weight(i, len(words), power = power) for i in range(0, len(words))]
def pick_one_word(words):
3 years ago
if len(words) == 0:
return None
return random.choices(list(words.items()), weights = (v[0] for v in words.values()))[0]
3 years ago
def word_diff(a, b):
seq = difflib.SequenceMatcher(None, a, b)
return seq.ratio()
3 years ago
class WordMaker:
def __init__(self):
print("Loading dictionaries")
3 years ago
illegal = set(ch for ch in (string.ascii_uppercase + string.punctuation + string.digits + string.whitespace))
with open (MAIN_DICT_PATH, "r") as f:
self.main_words = {
sl[0] :
(int(sl[1]), tuple(sl[2].split("="))) for sl in
(tuple(l.split(",")) for l in f.read().splitlines()) if
len(sl[0]) >= MIN_MAIN_LEN and
not any(c in illegal for c in sl[0])}
with open(USER_DICT_PATH, "r") as f:
self.user_words = {l : (1, None) for l in f.read().splitlines()}
if os.path.exists(USED_DICT_PATH):
with open("dicts/used.dict", "r") as f:
used_words = {l : (1, None) for l in f.read().splitlines()}
else:
used_words = dict()
self.all_words = {k.lower() : v for k, v in {**self.main_words, **self.user_words, **used_words}.items()}
def extend_word(self, prev_word):
user_dict = random.randint(0, 100) < USER_PCT
if user_dict:
next_dict = self.user_words
3 years ago
else:
next_dict = self.main_words
new_words = dict()
for w in next_dict.items():
new_words.update(dict.fromkeys(overlap_words(prev_word, w)))
3 years ago
if len(new_words) == 0:
return None
max_len = max(len(w) for w in new_words)
for w in new_words:
new_words[w] = (math.pow(max_len + 1 - len(w), 3), None)
3 years ago
while len(new_words) > 0:
new_word = pick_one_word(new_words)
del new_words[new_word[0]]
3 years ago
if new_word[0].lower() not in self.all_words:
3 years ago
return new_word
3 years ago
return None
def get_portmanteau(self, target_times = 1):
user_dict = random.randint(0, 100) < USER_PCT
if user_dict:
words = self.user_words
3 years ago
else:
words = self.main_words
3 years ago
while True:
while True:
word = pick_one_word(words)
3 years ago
times = target_times
while times > 0:
ext_word = self.extend_word(word)
3 years ago
if ext_word is None:
3 years ago
break
3 years ago
word = ext_word
times -= 1
if times == 0:
3 years ago
break
if len(word[0]) < MAX_PORT_LEN:
3 years ago
break
word_str = word[0].lower()
3 years ago
print(word_str)
3 years ago
return word_str
3 years ago
def get_portmanteaus(self, count = 10):
words = set()
used_words = dict()
while count > 0:
word_str = self.get_portmanteau()
if word_str not in words:
words.add(word_str)
used_words[word_str] = (1, None)
count -= 1
self.all_words.update(used_words)
if not TEST:
with open("dicts/used.dict", "a") as f:
f.write("\n".join(used_words.keys()) + "\n")
return words
3 years ago
class PortBotClient(BotClient):
def __init__(self, bot, config):
config = {
"app_name": "PortmanteuBot",
"rate_limit": 3,
"retry_rate": 60,
"poll_interval": 15,
**config}
super().__init__(bot, config)
self.my_id = None
def on_start(self):
self.log("Starting")
self.my_id = self.api.me()["id"]
pass
def on_poll(self):
pass
def on_status(self, status):
if status["account"]["id"] != self.my_id:
return
def post():
for client_name, client in bot.clients.items():
3 years ago
words = wm.get_portmanteaus(3)
print()
3 years ago
if random.randint(0, 100) <= 100:
visibility = "public"
else:
visibility = "unlisted"
3 years ago
dt = next_dt()
if not TEST:
client.api.status_post("\n".join(words), visibility = visibility)
3 years ago
print("Scheduling at", dt)
if TEST:
scheduler.enter(2, 1, post)
else:
scheduler.enterabs(dt.timestamp(), 1, post)
3 years ago
wm = WordMaker()
scheduler = sched.scheduler(timefunc = time.time, delayfunc = time.sleep)
bot = Bot(PortBotClient, loaded_config)
del loaded_config
bot.start()
print("Running")
dt = next_dt()
if TEST:
scheduler.enter(2, 1, post)
else:
3 years ago
print("Scheduling at", dt)
scheduler.enterabs(dt.timestamp(), 1, post)
3 years ago
scheduler.run()