mirror of
https://gitlab.com/vylion/velascobot.git
synced 2025-04-19 13:36:36 +02:00
Added documenting comments accross all the files
Added minimum and maximum period values as argument flags
This commit is contained in:
parent
a13bdd51c7
commit
09cf241f18
7 changed files with 288 additions and 129 deletions
38
archivist.py
38
archivist.py
|
@ -7,8 +7,8 @@ from generator import Generator
|
||||||
class Archivist(object):
|
class Archivist(object):
|
||||||
|
|
||||||
def __init__(self, logger, chatdir=None, chatext=None, admin=0,
|
def __init__(self, logger, chatdir=None, chatext=None, admin=0,
|
||||||
period_inc=5, save_count=15, max_period=100000,
|
period_inc=5, save_count=15, min_period=1,
|
||||||
read_only=False
|
max_period=100000, read_only=False
|
||||||
):
|
):
|
||||||
if chatdir is None or len(chatdir) == 0:
|
if chatdir is None or len(chatdir) == 0:
|
||||||
chatdir = "./"
|
chatdir = "./"
|
||||||
|
@ -19,16 +19,20 @@ class Archivist(object):
|
||||||
self.chatext = chatext
|
self.chatext = chatext
|
||||||
self.period_inc = period_inc
|
self.period_inc = period_inc
|
||||||
self.save_count = save_count
|
self.save_count = save_count
|
||||||
|
self.min_period = min_period
|
||||||
self.max_period = max_period
|
self.max_period = max_period
|
||||||
self.read_only = read_only
|
self.read_only = read_only
|
||||||
|
|
||||||
|
# Formats and returns a chat folder path
|
||||||
def chat_folder(self, *formatting, **key_format):
|
def chat_folder(self, *formatting, **key_format):
|
||||||
return (self.chatdir + "/chat_{tag}").format(*formatting, **key_format)
|
return (self.chatdir + "/chat_{tag}").format(*formatting, **key_format)
|
||||||
|
|
||||||
|
# Formats and returns a chat file path
|
||||||
def chat_file(self, *formatting, **key_format):
|
def chat_file(self, *formatting, **key_format):
|
||||||
return (self.chatdir + "/chat_{tag}/{file}{ext}").format(*formatting, **key_format)
|
return (self.chatdir + "/chat_{tag}/{file}{ext}").format(*formatting, **key_format)
|
||||||
|
|
||||||
def store(self, tag, data, vocab_dumper):
|
# Stores a Reader/Generator file pair
|
||||||
|
def store(self, tag, data, vocab):
|
||||||
chat_folder = self.chat_folder(tag=tag)
|
chat_folder = self.chat_folder(tag=tag)
|
||||||
chat_card = self.chat_file(tag=tag, file="card", ext=".txt")
|
chat_card = self.chat_file(tag=tag, file="card", ext=".txt")
|
||||||
|
|
||||||
|
@ -45,17 +49,18 @@ class Archivist(object):
|
||||||
file.write(data)
|
file.write(data)
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
if vocab_dumper is not None:
|
if vocab is not None:
|
||||||
chat_record = self.chat_file(tag=tag, file="record", ext=self.chatext)
|
chat_record = self.chat_file(tag=tag, file="record", ext=self.chatext)
|
||||||
file = open(chat_record, 'w', encoding="utf-16")
|
file = open(chat_record, 'w', encoding="utf-16")
|
||||||
vocab_dumper(file)
|
file.write(vocab)
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
|
# Loads a Generator's vocabulary file dump
|
||||||
def load_vocab(self, tag):
|
def load_vocab(self, tag):
|
||||||
filepath = self.chat_file(tag=tag, file="record", ext=self.chatext)
|
filepath = self.chat_file(tag=tag, file="record", ext=self.chatext)
|
||||||
try:
|
try:
|
||||||
file = open(filepath, 'r', encoding="utf-16")
|
file = open(filepath, 'r', encoding="utf-16")
|
||||||
record = Generator.load(file)
|
record = file.read()
|
||||||
file.close()
|
file.close()
|
||||||
return record
|
return record
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -63,6 +68,7 @@ class Archivist(object):
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Loads a Generator's vocabulary file dump in the old UTF-8 encoding
|
||||||
def load_vocab_old(self, tag):
|
def load_vocab_old(self, tag):
|
||||||
filepath = self.chat_file(tag=tag, file="record", ext=self.chatext)
|
filepath = self.chat_file(tag=tag, file="record", ext=self.chatext)
|
||||||
try:
|
try:
|
||||||
|
@ -75,7 +81,8 @@ class Archivist(object):
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def load_reader(self, tag):
|
# Loads a Metadata card file dump
|
||||||
|
def load_card(self, tag):
|
||||||
filepath = self.chat_file(tag=tag, file="card", ext=".txt")
|
filepath = self.chat_file(tag=tag, file="card", ext=".txt")
|
||||||
try:
|
try:
|
||||||
reader_file = open(filepath, 'r')
|
reader_file = open(filepath, 'r')
|
||||||
|
@ -86,16 +93,21 @@ class Archivist(object):
|
||||||
self.logger.error("Metadata file {} not found.".format(filepath))
|
self.logger.error("Metadata file {} not found.".format(filepath))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Returns a Reader for a given ID with an already working vocabulary - be it
|
||||||
|
# new or loaded from file
|
||||||
def get_reader(self, tag):
|
def get_reader(self, tag):
|
||||||
reader = self.load_reader(tag)
|
card = self.load_card(tag)
|
||||||
if reader:
|
if card:
|
||||||
vocab = self.load_vocab(tag)
|
vocab_dump = self.load_vocab(tag)
|
||||||
if not vocab:
|
if vocab_dump:
|
||||||
|
vocab = Generator.loads(vocab_dump)
|
||||||
|
else:
|
||||||
vocab = Generator()
|
vocab = Generator()
|
||||||
return Reader.FromCard(reader, vocab, self.max_period, self.logger)
|
return Reader.FromCard(card, vocab, self.max_period, self.logger)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Count the stored chats
|
||||||
def chat_count(self):
|
def chat_count(self):
|
||||||
count = 0
|
count = 0
|
||||||
directory = os.fsencode(self.chatdir)
|
directory = os.fsencode(self.chatdir)
|
||||||
|
@ -105,6 +117,7 @@ class Archivist(object):
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
# Crawl through all the stored Readers
|
||||||
def readers_pass(self):
|
def readers_pass(self):
|
||||||
directory = os.fsencode(self.chatdir)
|
directory = os.fsencode(self.chatdir)
|
||||||
for subdir in os.scandir(directory):
|
for subdir in os.scandir(directory):
|
||||||
|
@ -124,6 +137,7 @@ class Archivist(object):
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# Load and immediately store every Reader
|
||||||
def update(self):
|
def update(self):
|
||||||
for reader in self.readers_pass():
|
for reader in self.readers_pass():
|
||||||
if reader.vocab is None:
|
if reader.vocab is None:
|
||||||
|
|
76
generator.py
76
generator.py
|
@ -4,12 +4,12 @@ import random
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
# This splits strings into lists of words delimited by space.
|
||||||
|
# Other whitespaces are appended space characters so they are included
|
||||||
|
# as their own Markov chain element, so as not to pollude with
|
||||||
|
# "different" words that would only differ in having a whitespace
|
||||||
|
# attached or not
|
||||||
def rewrite(text):
|
def rewrite(text):
|
||||||
# This splits strings into lists of words delimited by space.
|
|
||||||
# Other whitespaces are appended space characters so they are included
|
|
||||||
# as their own Markov chain element, so as not to pollude with
|
|
||||||
# "different" words that would only differ in having a whitespace
|
|
||||||
# attached or not
|
|
||||||
words = text.replace('\n', '\n ').split(' ')
|
words = text.replace('\n', '\n ').split(' ')
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(words):
|
while i < len(words):
|
||||||
|
@ -23,24 +23,24 @@ def rewrite(text):
|
||||||
return words
|
return words
|
||||||
|
|
||||||
|
|
||||||
|
# This gives a dictionary key from 2 words, ignoring case
|
||||||
def getkey(w1, w2):
|
def getkey(w1, w2):
|
||||||
# This gives a dictionary key from 2 words, ignoring case
|
|
||||||
key = (w1.strip().casefold(), w2.strip().casefold())
|
key = (w1.strip().casefold(), w2.strip().casefold())
|
||||||
return str(key)
|
return str(key)
|
||||||
|
|
||||||
|
|
||||||
|
# This turns a dictionary key back into 2 separate words
|
||||||
def getwords(key):
|
def getwords(key):
|
||||||
# This turns a dictionary key back into 2 separate words
|
|
||||||
words = key.strip('()').split(', ')
|
words = key.strip('()').split(', ')
|
||||||
for i in range(len(words)):
|
for i in range(len(words)):
|
||||||
words[i].strip('\'')
|
words[i].strip('\'')
|
||||||
return words
|
return words
|
||||||
|
|
||||||
|
|
||||||
|
# Generates triplets of words from the given data string. So if our string
|
||||||
|
# were "What a lovely day", we'd generate (What, a, lovely) and then
|
||||||
|
# (a, lovely, day).
|
||||||
def triplets(wordlist):
|
def triplets(wordlist):
|
||||||
# Generates triplets of words from the given data string. So if our string
|
|
||||||
# were "What a lovely day", we'd generate (What, a, lovely) and then
|
|
||||||
# (a, lovely, day).
|
|
||||||
if len(wordlist) < 3:
|
if len(wordlist) < 3:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -49,24 +49,25 @@ def triplets(wordlist):
|
||||||
|
|
||||||
|
|
||||||
class Generator(object):
|
class Generator(object):
|
||||||
|
# Marks when we want to create a Generator object from a given JSON
|
||||||
MODE_JSON = "MODE_JSON"
|
MODE_JSON = "MODE_JSON"
|
||||||
# This is to mark when we want to create a Generator object from a given JSON
|
|
||||||
|
|
||||||
|
# Marks when we want to create a Generator object from a given list of words
|
||||||
MODE_LIST = "MODE_LIST"
|
MODE_LIST = "MODE_LIST"
|
||||||
# This is to mark when we want to create a Generator object from a given list of words
|
|
||||||
|
|
||||||
|
# Marks when we want to create a Generator object from a given dictionary
|
||||||
MODE_DICT = "MODE_DICT"
|
MODE_DICT = "MODE_DICT"
|
||||||
# This is to mark when we want to create a Generator object from a given dictionary
|
|
||||||
|
|
||||||
MODE_CHAT_DATA = "MODE_CHAT_DATA"
|
# Marks when we want to create a Generator object from a whole Chat history (WIP)
|
||||||
# This is to mark when we want to create a Generator object from Chat data (WIP)
|
MODE_HIST = "MODE_HIST"
|
||||||
|
|
||||||
|
# Marks the beginning of a message
|
||||||
HEAD = "\n^MESSAGE_SEPARATOR^"
|
HEAD = "\n^MESSAGE_SEPARATOR^"
|
||||||
|
# Marks the end of a message
|
||||||
TAIL = " ^MESSAGE_SEPARATOR^"
|
TAIL = " ^MESSAGE_SEPARATOR^"
|
||||||
|
|
||||||
def __init__(self, load=None, mode=None):
|
def __init__(self, load=None, mode=None):
|
||||||
if mode is not None:
|
if mode is not None:
|
||||||
# We ain't creating a new Generator from scratch
|
|
||||||
if mode == Generator.MODE_JSON:
|
if mode == Generator.MODE_JSON:
|
||||||
self.cache = json.loads(load)
|
self.cache = json.loads(load)
|
||||||
elif mode == Generator.MODE_LIST:
|
elif mode == Generator.MODE_LIST:
|
||||||
|
@ -74,45 +75,44 @@ class Generator(object):
|
||||||
self.load_list(load)
|
self.load_list(load)
|
||||||
elif mode == Generator.MODE_DICT:
|
elif mode == Generator.MODE_DICT:
|
||||||
self.cache = load
|
self.cache = load
|
||||||
|
# TODO: Chat History mode
|
||||||
else:
|
else:
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
# The cache is where we store our words
|
|
||||||
|
|
||||||
|
# Loads a text divided into a list of lines
|
||||||
def load_list(self, many):
|
def load_list(self, many):
|
||||||
# Takes a list of strings and adds them to the cache one by one
|
|
||||||
for one in many:
|
for one in many:
|
||||||
self.add(one)
|
self.add(one)
|
||||||
|
|
||||||
|
# Dumps the cache dictionary into a JSON-formatted string
|
||||||
def dumps(self):
|
def dumps(self):
|
||||||
# Dumps the cache dictionary into a JSON-formatted string
|
|
||||||
return json.dumps(self.cache, ensure_ascii=False)
|
return json.dumps(self.cache, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Dumps the cache dictionary into a file, formatted as JSON
|
||||||
def dump(self, f):
|
def dump(self, f):
|
||||||
json.dump(self.cache, f, ensure_ascii=False, indent='')
|
json.dump(self.cache, f, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Loads the cache dictionary from a JSON-formatted string
|
||||||
def loads(dump):
|
def loads(dump):
|
||||||
# Loads the cache dictionary from a JSON-formatted string
|
|
||||||
if len(dump) == 0:
|
if len(dump) == 0:
|
||||||
# faulty dump gives default Generator
|
# faulty dump gives default Generator
|
||||||
return Generator()
|
return Generator()
|
||||||
# otherwise
|
# otherwise
|
||||||
return Generator(load=dump, mode=Generator.MODE_JSON)
|
return Generator(load=dump, mode=Generator.MODE_JSON)
|
||||||
|
|
||||||
|
# Loads the cache dictionary from a file, formatted as JSON
|
||||||
def load(f):
|
def load(f):
|
||||||
return Generator(load=json.load(f), mode=Generator.MODE_DICT)
|
return Generator(load=json.load(f), mode=Generator.MODE_DICT)
|
||||||
|
|
||||||
def add(self, text):
|
def add(self, text):
|
||||||
# This takes a string and stores it in the cache, preceding it
|
|
||||||
# with the HEAD that marks the beginning of a new message and
|
|
||||||
# following it with the TAIL that marks the end
|
|
||||||
words = [Generator.HEAD]
|
words = [Generator.HEAD]
|
||||||
text = rewrite(text + Generator.TAIL)
|
text = rewrite(text + Generator.TAIL)
|
||||||
words.extend(text)
|
words.extend(text)
|
||||||
self.database(words)
|
self.database(words)
|
||||||
|
|
||||||
|
# This takes a list of words and stores it in the cache, adding
|
||||||
|
# a special entry for the first word (the HEAD marker)
|
||||||
def database(self, words):
|
def database(self, words):
|
||||||
# This takes a list of words and stores it in the cache, adding
|
|
||||||
# a special entry for the first word (the HEAD marker)
|
|
||||||
for w1, w2, w3 in triplets(words):
|
for w1, w2, w3 in triplets(words):
|
||||||
if w1 == Generator.HEAD:
|
if w1 == Generator.HEAD:
|
||||||
if w1 in self.cache:
|
if w1 in self.cache:
|
||||||
|
@ -128,50 +128,50 @@ class Generator(object):
|
||||||
# the new end of chain
|
# the new end of chain
|
||||||
self.cache[key] = [w3]
|
self.cache[key] = [w3]
|
||||||
|
|
||||||
|
# This generates the Markov text/word chain
|
||||||
|
# silence=True disables Telegram user mentions
|
||||||
def generate(self, size=50, silence=False):
|
def generate(self, size=50, silence=False):
|
||||||
# This generates the Markov text/word chain
|
|
||||||
# silence tells if mentions should be silenced
|
|
||||||
if len(self.cache) == 0:
|
if len(self.cache) == 0:
|
||||||
# If there is nothing in the cache we cannot generate anything
|
# If there is nothing in the cache we cannot generate anything
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
# Start with a message HEAD and a random message starting word
|
||||||
w1 = random.choice(self.cache[Generator.HEAD])
|
w1 = random.choice(self.cache[Generator.HEAD])
|
||||||
w2 = random.choice(self.cache[getkey(Generator.HEAD, w1)])
|
w2 = random.choice(self.cache[getkey(Generator.HEAD, w1)])
|
||||||
# Start with a message HEAD and a random message starting word
|
|
||||||
gen_words = []
|
gen_words = []
|
||||||
|
# As long as we don't go over the max. message length (in n. of words)...
|
||||||
for i in range(size):
|
for i in range(size):
|
||||||
# As long as we don't go over the size value (max. message length)...
|
|
||||||
if silence and w1.startswith("@") and len(w1) > 1:
|
if silence and w1.startswith("@") and len(w1) > 1:
|
||||||
|
# ...append word 1, disabling any possible Telegram mention
|
||||||
gen_words.append(w1.replace("@", "(@)"))
|
gen_words.append(w1.replace("@", "(@)"))
|
||||||
# ...append the first word, silencing any possible username mention
|
|
||||||
else:
|
else:
|
||||||
|
# ..append word 1
|
||||||
gen_words.append(w1)
|
gen_words.append(w1)
|
||||||
# ..append the first word
|
|
||||||
if w2 == Generator.TAIL or not getkey(w1, w2) in self.cache:
|
if w2 == Generator.TAIL or not getkey(w1, w2) in self.cache:
|
||||||
# When there's no key from the last 2 words to follow the chain,
|
# When there's no key from the last 2 words to follow the chain,
|
||||||
# or we reached a separation between messages, stop
|
# or we reached a separation between messages, stop
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
# Get a random third word that follows the chain of words 1
|
||||||
|
# and 2, then make words 2 and 3 to be the new words 1 and 2
|
||||||
w1, w2 = w2, random.choice(self.cache[getkey(w1, w2)])
|
w1, w2 = w2, random.choice(self.cache[getkey(w1, w2)])
|
||||||
# Make the second word to be the new first word, and
|
|
||||||
# make a new random word that follows the chain to be
|
|
||||||
# the new second word
|
|
||||||
return ' '.join(gen_words)
|
return ' '.join(gen_words)
|
||||||
|
|
||||||
|
# Cross a second Generator into this one
|
||||||
def cross(self, gen):
|
def cross(self, gen):
|
||||||
# cross 2 Generators into this one
|
|
||||||
for key in gen.cache:
|
for key in gen.cache:
|
||||||
if key in self.cache:
|
if key in self.cache:
|
||||||
self.cache[key].extend(gen.cache[key])
|
self.cache[key].extend(gen.cache[key])
|
||||||
else:
|
else:
|
||||||
self.cache[key] = list(gen.cache[key])
|
self.cache[key] = list(gen.cache[key])
|
||||||
|
|
||||||
|
# Count again the number of messages
|
||||||
|
# (for whenever the count number is unreliable)
|
||||||
def new_count(self):
|
def new_count(self):
|
||||||
# Count again the number of messages if the current number is unreliable
|
|
||||||
count = 0
|
count = 0
|
||||||
for key in self.cache:
|
for key in self.cache:
|
||||||
for word in self.cache[key]:
|
for word in self.cache[key]:
|
||||||
if word == Generator.TAIL:
|
if word == Generator.TAIL:
|
||||||
|
# ...by just counting message separators
|
||||||
count += 1
|
count += 1
|
||||||
# by just counting message separators
|
|
||||||
return count
|
return count
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from collections.abc import MutableSequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
|
||||||
class MemoryList(MutableSequence):
|
class MemoryList(Sequence):
|
||||||
|
"""Special "memory list" class that:
|
||||||
|
- Whenever an item is added that was already in the list,
|
||||||
|
it gets moved to the back instead
|
||||||
|
- Whenever an item is looked for, it gets moved to the
|
||||||
|
back
|
||||||
|
- If a new item is added that goes over a given capacity
|
||||||
|
limit, the item at the front (oldest accessed item)
|
||||||
|
is removed (and returned)"""
|
||||||
|
|
||||||
def __init__(self, capacity, data=None):
|
def __init__(self, capacity, data=None):
|
||||||
"""Initialize the class"""
|
|
||||||
super(MemoryList, self).__init__()
|
super(MemoryList, self).__init__()
|
||||||
self._capacity = capacity
|
self._capacity = capacity
|
||||||
if (data is not None):
|
if (data is not None):
|
||||||
|
@ -16,37 +24,25 @@ class MemoryList(MutableSequence):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{0} {1}, capacity {2}>".format(self.__class__.__name__, self._list, self._capacity)
|
return "<{0} {1}, capacity {2}>".format(self.__class__.__name__, self._list, self._capacity)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{0}, {1}/{2}".format(self._list, len(self._list), self._capacity)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
"""List length"""
|
|
||||||
return len(self._list)
|
return len(self._list)
|
||||||
|
|
||||||
def capacity(self):
|
def capacity(self):
|
||||||
return self._capacity
|
return self._capacity
|
||||||
|
|
||||||
def __getitem__(self, ii):
|
def __getitem__(self, ii):
|
||||||
"""Get a list item"""
|
|
||||||
return self._list[ii]
|
return self._list[ii]
|
||||||
|
|
||||||
def __delitem__(self, ii):
|
|
||||||
"""Delete an item"""
|
|
||||||
del self._list[ii]
|
|
||||||
|
|
||||||
def __setitem__(self, ii, val):
|
|
||||||
self._list[ii] = val
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self._list)
|
|
||||||
|
|
||||||
def __contains__(self, val):
|
def __contains__(self, val):
|
||||||
return val in self._list
|
return val in self._list
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self._list.__iter__()
|
return self._list.__iter__()
|
||||||
|
|
||||||
def insert(self, ii, val):
|
def add(self, val):
|
||||||
self._list.insert(ii, val)
|
|
||||||
|
|
||||||
def append(self, val):
|
|
||||||
if val in self._list:
|
if val in self._list:
|
||||||
self._list.remove(val)
|
self._list.remove(val)
|
||||||
|
|
||||||
|
@ -58,8 +54,8 @@ class MemoryList(MutableSequence):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_next(self, cond):
|
def search(self, cond, *args, **kwargs):
|
||||||
val = next((v for v in self._list if cond(v)), None)
|
val = next((v for v in self._list if cond(v)), *args, **kwargs)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
self._list.remove(val)
|
self._list.remove(val)
|
||||||
self._list.append(val)
|
self._list.append(val)
|
||||||
|
|
67
metadata.py
67
metadata.py
|
@ -1,8 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# This reads a line in the format 'VARIABLE=value' and gives me the value.
|
||||||
|
# See Metadata.loadl(...) for more details
|
||||||
def parse_card_line(line):
|
def parse_card_line(line):
|
||||||
# This reads a line in the format 'VARIABLE=value' and gives me the value.
|
|
||||||
# See Metadata.loadl(...) for more details
|
|
||||||
s = line.split('=', 1)
|
s = line.split('=', 1)
|
||||||
if len(s) < 2:
|
if len(s) < 2:
|
||||||
return ""
|
return ""
|
||||||
|
@ -10,35 +10,37 @@ def parse_card_line(line):
|
||||||
return s[1]
|
return s[1]
|
||||||
|
|
||||||
|
|
||||||
|
# This is a chat's Metadata, holding different configuration values for
|
||||||
|
# Velasco and other miscellaneous information about the chat
|
||||||
class Metadata(object):
|
class Metadata(object):
|
||||||
# This is a chat's Metadata, holding different configuration values for
|
|
||||||
# Velasco and other miscellaneous information about the chat
|
|
||||||
|
|
||||||
def __init__(self, cid, ctype, title, count=0, period=None, answer=0.5, restricted=False, silenced=False):
|
def __init__(self, cid, ctype, title, count=0, period=None, answer=0.5, restricted=False, silenced=False):
|
||||||
self.id = str(cid)
|
|
||||||
# The Telegram chat's ID
|
# The Telegram chat's ID
|
||||||
self.type = ctype
|
self.id = str(cid)
|
||||||
# The type of chat
|
# The type of chat
|
||||||
self.title = title
|
self.type = ctype
|
||||||
# The title of the chat
|
# The title of the chat
|
||||||
|
self.title = title
|
||||||
if period is None:
|
if period is None:
|
||||||
if "group" in ctype:
|
if "group" in ctype:
|
||||||
period = 10
|
|
||||||
# Default period for groups and supergroups
|
# Default period for groups and supergroups
|
||||||
|
period = 10
|
||||||
else:
|
else:
|
||||||
period = 2
|
|
||||||
# Default period for private or channel chats
|
# Default period for private or channel chats
|
||||||
|
period = 2
|
||||||
|
# The number of messages read in a chat
|
||||||
self.count = count
|
self.count = count
|
||||||
# The number of messages read
|
|
||||||
self.period = period
|
|
||||||
# This chat's configured period
|
# This chat's configured period
|
||||||
self.answer = answer
|
self.period = period
|
||||||
# This chat's configured answer probability
|
# This chat's configured answer probability
|
||||||
self.restricted = restricted
|
self.answer = answer
|
||||||
# Wether some interactions are restricted to admins only
|
# Wether some interactions are restricted to admins only
|
||||||
self.silenced = silenced
|
self.restricted = restricted
|
||||||
# Wether messages should silence user mentions
|
# Wether messages should silence user mentions
|
||||||
|
self.silenced = silenced
|
||||||
|
|
||||||
|
# Sets the period for a chat
|
||||||
|
# It has to be higher than 1
|
||||||
|
# Returns the new value
|
||||||
def set_period(self, period):
|
def set_period(self, period):
|
||||||
if period < 1:
|
if period < 1:
|
||||||
raise ValueError('Tried to set period a value less than 1.')
|
raise ValueError('Tried to set period a value less than 1.')
|
||||||
|
@ -46,6 +48,9 @@ class Metadata(object):
|
||||||
self.period = period
|
self.period = period
|
||||||
return self.period
|
return self.period
|
||||||
|
|
||||||
|
# Sets the answer probability
|
||||||
|
# It's a percentage represented as a decimal between 0 and 1
|
||||||
|
# Returns the new value
|
||||||
def set_answer(self, prob):
|
def set_answer(self, prob):
|
||||||
if prob > 1:
|
if prob > 1:
|
||||||
raise ValueError('Tried to set answer probability higher than 1.')
|
raise ValueError('Tried to set answer probability higher than 1.')
|
||||||
|
@ -55,6 +60,8 @@ class Metadata(object):
|
||||||
self.answer = prob
|
self.answer = prob
|
||||||
return self.answer
|
return self.answer
|
||||||
|
|
||||||
|
# Dumps the metadata into a list of lines, then joined together in a string,
|
||||||
|
# ready to be written into a file
|
||||||
def dumps(self):
|
def dumps(self):
|
||||||
lines = ["CARD=v5"]
|
lines = ["CARD=v5"]
|
||||||
lines.append("CHAT_ID=" + self.id)
|
lines.append("CHAT_ID=" + self.id)
|
||||||
|
@ -68,10 +75,12 @@ class Metadata(object):
|
||||||
# lines.append("WORD_DICT=")
|
# lines.append("WORD_DICT=")
|
||||||
return ('\n'.join(lines)) + '\n'
|
return ('\n'.join(lines)) + '\n'
|
||||||
|
|
||||||
|
# Creates a Metadata object from a previous text dump
|
||||||
def loads(text):
|
def loads(text):
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
return Metadata.loadl(lines)
|
return Metadata.loadl(lines)
|
||||||
|
|
||||||
|
# Creates a Metadata object from a list of metadata lines
|
||||||
def loadl(lines):
|
def loadl(lines):
|
||||||
# In a perfect world, I would get both the variable name and its corresponding value
|
# In a perfect world, I would get both the variable name and its corresponding value
|
||||||
# from each side of the lines, but I know the order in which the lines are writen in
|
# from each side of the lines, but I know the order in which the lines are writen in
|
||||||
|
@ -90,6 +99,14 @@ class Metadata(object):
|
||||||
silenced=(parse_card_line(lines[8]) == 'True')
|
silenced=(parse_card_line(lines[8]) == 'True')
|
||||||
)
|
)
|
||||||
elif version == "v3":
|
elif version == "v3":
|
||||||
|
# Deprecated: this elif block will be removed in a new version
|
||||||
|
print("Warning! This Card format ({}) is deprecated. Update all".format(version),
|
||||||
|
"your files in case that there are still some left in old formats before",
|
||||||
|
"downloading the next update.")
|
||||||
|
|
||||||
|
# This is kept for retrocompatibility purposes, in case someone did a fork
|
||||||
|
# of this repo and still has some chat files that haven't been updated in
|
||||||
|
# a long while -- but I already converted all my files to v5
|
||||||
return Metadata(cid=parse_card_line(lines[1]),
|
return Metadata(cid=parse_card_line(lines[1]),
|
||||||
ctype=parse_card_line(lines[2]),
|
ctype=parse_card_line(lines[2]),
|
||||||
title=parse_card_line(lines[3]),
|
title=parse_card_line(lines[3]),
|
||||||
|
@ -99,6 +116,12 @@ class Metadata(object):
|
||||||
restricted=(parse_card_line(lines[6]) == 'True')
|
restricted=(parse_card_line(lines[6]) == 'True')
|
||||||
)
|
)
|
||||||
elif version == "v2":
|
elif version == "v2":
|
||||||
|
# Deprecated: this elif block will be removed in a new version
|
||||||
|
print("Warning! This Card format ({}) is deprecated. Update all".format(version),
|
||||||
|
"your files in case that there are still some left in old formats before",
|
||||||
|
"downloading the next update.")
|
||||||
|
|
||||||
|
# Also kept for retrocompatibility purposes
|
||||||
return Metadata(cid=parse_card_line(lines[1]),
|
return Metadata(cid=parse_card_line(lines[1]),
|
||||||
ctype=parse_card_line(lines[2]),
|
ctype=parse_card_line(lines[2]),
|
||||||
title=parse_card_line(lines[3]),
|
title=parse_card_line(lines[3]),
|
||||||
|
@ -107,6 +130,12 @@ class Metadata(object):
|
||||||
answer=float(parse_card_line(lines[5]))
|
answer=float(parse_card_line(lines[5]))
|
||||||
)
|
)
|
||||||
elif version == "dict:":
|
elif version == "dict:":
|
||||||
|
# Deprecated: this elif block will be removed in a new version
|
||||||
|
print("Warning! This Card format ('dict') is deprecated. Update all",
|
||||||
|
"your files in case that there are still some left in old formats before",
|
||||||
|
"downloading the next update.")
|
||||||
|
|
||||||
|
# Also kept for retrocompatibility purposes
|
||||||
# At some point I decided to number the versions of each dictionary format,
|
# At some point I decided to number the versions of each dictionary format,
|
||||||
# but this was not always the case. This is what you get if you try to read
|
# but this was not always the case. This is what you get if you try to read
|
||||||
# whatever there is in very old files where the version should be
|
# whatever there is in very old files where the version should be
|
||||||
|
@ -117,7 +146,13 @@ class Metadata(object):
|
||||||
period=int(lines[3])
|
period=int(lines[3])
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# This is for the oldest of files
|
# Deprecated: this elif block will be removed in a new version
|
||||||
|
print("Warning! This ancient Card format is deprecated. Update all",
|
||||||
|
"your files in case that there are still some left in old formats before",
|
||||||
|
"downloading the next update.")
|
||||||
|
|
||||||
|
# Also kept for retrocompatibility purposes
|
||||||
|
# This is for the oldest of file formats
|
||||||
return Metadata(cid=lines[0],
|
return Metadata(cid=lines[0],
|
||||||
ctype=lines[1],
|
ctype=lines[1],
|
||||||
title=lines[2],
|
title=lines[2],
|
||||||
|
|
76
reader.py
76
reader.py
|
@ -5,9 +5,9 @@ from metadata import Metadata, parse_card_line
|
||||||
from generator import Generator
|
from generator import Generator
|
||||||
|
|
||||||
|
|
||||||
|
# This gives me the chat title, or the first and maybe last
|
||||||
|
# name of the user as fallback if it's a private chat
|
||||||
def get_chat_title(chat):
|
def get_chat_title(chat):
|
||||||
# This gives me the chat title, or the first and maybe last
|
|
||||||
# name of the user as fallback if it's a private chat
|
|
||||||
if chat.title is not None:
|
if chat.title is not None:
|
||||||
return chat.title
|
return chat.title
|
||||||
elif chat.first_name is not None:
|
elif chat.first_name is not None:
|
||||||
|
@ -25,40 +25,52 @@ class Memory(object):
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|
||||||
|
|
||||||
|
# This is a chat Reader object, in charge of managing the parsing of messages
|
||||||
|
# for a specific chat, and holding said chat's metadata
|
||||||
class Reader(object):
|
class Reader(object):
|
||||||
# This is a chat Reader object, in charge of managing the parsing of messages
|
# Media tagging variables
|
||||||
# for a specific chat, and holding said chat's metadata
|
|
||||||
|
|
||||||
TAG_PREFIX = "^IS_"
|
TAG_PREFIX = "^IS_"
|
||||||
STICKER_TAG = "^IS_STICKER^"
|
STICKER_TAG = "^IS_STICKER^"
|
||||||
ANIM_TAG = "^IS_ANIMATION^"
|
ANIM_TAG = "^IS_ANIMATION^"
|
||||||
VIDEO_TAG = "^IS_VIDEO^"
|
VIDEO_TAG = "^IS_VIDEO^"
|
||||||
|
|
||||||
def __init__(self, metadata, vocab, max_period, logger):
|
def __init__(self, metadata, vocab, max_period, logger, names=[]):
|
||||||
|
# The Metadata object holding a chat's specific bot parameters
|
||||||
self.meta = metadata
|
self.meta = metadata
|
||||||
|
# The Generator object holding the vocabulary learned so far
|
||||||
self.vocab = vocab
|
self.vocab = vocab
|
||||||
|
# The maximum period allowed for this bot
|
||||||
self.max_period = max_period
|
self.max_period = max_period
|
||||||
|
# The short term memory, for recently read messages (see below)
|
||||||
self.short_term_mem = []
|
self.short_term_mem = []
|
||||||
|
# The countdown until the period ends and it's time to talk
|
||||||
self.countdown = self.meta.period
|
self.countdown = self.meta.period
|
||||||
|
# The logger object shared program-wide
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
# The bot's nicknames + username
|
||||||
|
self.names = names
|
||||||
|
|
||||||
|
# Create a new Reader from a Chat object
|
||||||
def FromChat(chat, max_period, logger):
|
def FromChat(chat, max_period, logger):
|
||||||
# Create a new Reader from a Chat object
|
|
||||||
meta = Metadata(chat.id, chat.type, get_chat_title(chat))
|
meta = Metadata(chat.id, chat.type, get_chat_title(chat))
|
||||||
vocab = Generator()
|
vocab = Generator()
|
||||||
return Reader(meta, vocab, max_period, logger)
|
return Reader(meta, vocab, max_period, logger)
|
||||||
|
|
||||||
|
# TODO: Create a new Reader from a whole Chat history
|
||||||
def FromHistory(history, vocab, max_period, logger):
|
def FromHistory(history, vocab, max_period, logger):
|
||||||
# Create a new Reader from a whole Chat history (WIP)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Create a new Reader from a meta's file dump
|
||||||
def FromCard(meta, vocab, max_period, logger):
|
def FromCard(meta, vocab, max_period, logger):
|
||||||
# Create a new Reader from a meta's file dump
|
|
||||||
metadata = Metadata.loads(meta)
|
metadata = Metadata.loads(meta)
|
||||||
return Reader(metadata, vocab, max_period, logger)
|
return Reader(metadata, vocab, max_period, logger)
|
||||||
|
|
||||||
|
# Deprecated: this method will be removed in a new version
|
||||||
def FromFile(text, max_period, logger, vocab=None):
|
def FromFile(text, max_period, logger, vocab=None):
|
||||||
# Load a Reader from a file's text string (obsolete)
|
print("Warning! This method of loading a Reader from file (Reader.FromFile(...))",
|
||||||
|
"is deprecated, and will be removed from the next update. Use FromCard instead.")
|
||||||
|
|
||||||
|
# Load a Reader from a file's text string
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
version = parse_card_line(lines[0]).strip()
|
version = parse_card_line(lines[0]).strip()
|
||||||
version = version if len(version.strip()) > 1 else lines[4]
|
version = version if len(version.strip()) > 1 else lines[4]
|
||||||
|
@ -86,27 +98,33 @@ class Reader(object):
|
||||||
r = Reader(meta, vocab, max_period, logger)
|
r = Reader(meta, vocab, max_period, logger)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
# Returns a nice lice little tuple package for the archivist to save to file.
|
||||||
|
# Also commits to long term memory any pending short term memories
|
||||||
def archive(self):
|
def archive(self):
|
||||||
# Returns a nice lice little tuple package for the archivist to save to file.
|
|
||||||
# Also commits to long term memory any pending short term memories
|
|
||||||
self.commit_memory()
|
self.commit_memory()
|
||||||
return (self.meta.id, self.meta.dumps(), self.vocab.dump)
|
return (self.meta.id, self.meta.dumps(), self.vocab.dumps())
|
||||||
|
|
||||||
|
# Checks type. Returns "True" for "group" even if it's supergroupA
|
||||||
def check_type(self, t):
|
def check_type(self, t):
|
||||||
# Checks type. Returns "True" for "group" even if it's supergroup
|
|
||||||
return t in self.meta.type
|
return t in self.meta.type
|
||||||
|
|
||||||
|
# Hard check
|
||||||
def exactly_type(self, t):
|
def exactly_type(self, t):
|
||||||
# Hard check
|
|
||||||
return t == self.meta.type
|
return t == self.meta.type
|
||||||
|
|
||||||
def set_title(self, title):
|
def set_title(self, title):
|
||||||
self.meta.title = title
|
self.meta.title = title
|
||||||
|
|
||||||
|
# Sets a new period in the Metadata
|
||||||
def set_period(self, period):
|
def set_period(self, period):
|
||||||
if period < self.countdown:
|
# The period has to be under max_period; otherwise, truncate to max_period
|
||||||
self.countdown = max(period, 1)
|
new_period = min(period, self.max_period)
|
||||||
return self.meta.set_period(min(period, self.max_period))
|
set_period = self.meta.set_period(new_period)
|
||||||
|
if new_period == set_period and new_period < self.countdown:
|
||||||
|
# If succesfully changed and the new period is less than the current
|
||||||
|
# remaining countdown, reduce the countdown to the new period
|
||||||
|
self.countdown = new_period
|
||||||
|
return new_period
|
||||||
|
|
||||||
def set_answer(self, prob):
|
def set_answer(self, prob):
|
||||||
return self.meta.set_answer(prob)
|
return self.meta.set_answer(prob)
|
||||||
|
@ -141,6 +159,8 @@ class Reader(object):
|
||||||
def toggle_silence(self):
|
def toggle_silence(self):
|
||||||
self.meta.silenced = (not self.meta.silenced)
|
self.meta.silenced = (not self.meta.silenced)
|
||||||
|
|
||||||
|
# Rolls the chance for answering in this specific chat,
|
||||||
|
# according to the answer probability
|
||||||
def is_answering(self):
|
def is_answering(self):
|
||||||
rand = random.random()
|
rand = random.random()
|
||||||
chance = self.answer()
|
chance = self.answer()
|
||||||
|
@ -150,10 +170,13 @@ class Reader(object):
|
||||||
return False
|
return False
|
||||||
return rand <= chance
|
return rand <= chance
|
||||||
|
|
||||||
|
# Adds a new message to the short term memory
|
||||||
def add_memory(self, mid, content):
|
def add_memory(self, mid, content):
|
||||||
mem = Memory(mid, content)
|
mem = Memory(mid, content)
|
||||||
self.short_term_mem.append(mem)
|
self.short_term_mem.append(mem)
|
||||||
|
|
||||||
|
# Returns a random message ID from the short memory,
|
||||||
|
# when answering to a random comment
|
||||||
def random_memory(self):
|
def random_memory(self):
|
||||||
if len(self.short_term_mem) == 0:
|
if len(self.short_term_mem) == 0:
|
||||||
return None
|
return None
|
||||||
|
@ -163,6 +186,10 @@ class Reader(object):
|
||||||
def reset_countdown(self):
|
def reset_countdown(self):
|
||||||
self.countdown = self.meta.period
|
self.countdown = self.meta.period
|
||||||
|
|
||||||
|
# Reads a message
|
||||||
|
# This process will determine which kind of message it is (Sticker, Anim,
|
||||||
|
# Video, or actual text) and pre-process it accordingly for the Generator,
|
||||||
|
# then store it in the short term memory
|
||||||
def read(self, message):
|
def read(self, message):
|
||||||
mid = str(message.message_id)
|
mid = str(message.message_id)
|
||||||
|
|
||||||
|
@ -174,16 +201,25 @@ class Reader(object):
|
||||||
self.learn_drawing(mid, Reader.ANIM_TAG, message.animation.file_id)
|
self.learn_drawing(mid, Reader.ANIM_TAG, message.animation.file_id)
|
||||||
elif message.video is not None:
|
elif message.video is not None:
|
||||||
self.learn_drawing(mid, Reader.VIDEO_TAG, message.video.file_id)
|
self.learn_drawing(mid, Reader.VIDEO_TAG, message.video.file_id)
|
||||||
|
|
||||||
self.meta.count += 1
|
self.meta.count += 1
|
||||||
|
|
||||||
|
# Stores a multimedia message in the short term memory as a text with
|
||||||
|
# TAG + the media file ID
|
||||||
def learn_drawing(self, mid, tag, drawing):
|
def learn_drawing(self, mid, tag, drawing):
|
||||||
self.learn(mid, tag + " " + drawing)
|
self.learn(mid, tag + " " + drawing)
|
||||||
|
|
||||||
|
# Stores a text message in the short term memory
|
||||||
def learn(self, mid, text):
|
def learn(self, mid, text):
|
||||||
if "velasco" in text.casefold() and len(text.split()) <= 3:
|
for name in self.names:
|
||||||
return
|
if name.casefold() in text.casefold() and len(text.split()) <= 3:
|
||||||
|
# If it's less than 3 words and one of the bot's names is in
|
||||||
|
# the message, ignore it as it's most probably just a summon
|
||||||
|
return
|
||||||
self.add_memory(mid, text)
|
self.add_memory(mid, text)
|
||||||
|
|
||||||
|
# Commits the short term memory messages into the "long term memory"
|
||||||
|
# aka the vocabulary Generator's cache
|
||||||
def commit_memory(self):
|
def commit_memory(self):
|
||||||
for mem in self.short_term_mem:
|
for mem in self.short_term_mem:
|
||||||
self.vocab.add(mem.content)
|
self.vocab.add(mem.content)
|
||||||
|
|
114
speaker.py
114
speaker.py
|
@ -8,19 +8,24 @@ from reader import Reader, get_chat_title
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError
|
||||||
|
|
||||||
|
|
||||||
|
# Auxiliar print to stderr function (alongside logger messages)
|
||||||
def eprint(*args, **kwargs):
|
def eprint(*args, **kwargs):
|
||||||
print(*args, end=' ', file=stderr, **kwargs)
|
print(*args, end=' ', file=stderr, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# Auxiliar message to send a text to a chat through a bot
|
||||||
def send(bot, cid, text, replying=None, formatting=None, logger=None, **kwargs):
|
def send(bot, cid, text, replying=None, formatting=None, logger=None, **kwargs):
|
||||||
kwargs["parse_mode"] = formatting
|
# Markdown or HTML formatting (both argument names are valid)
|
||||||
kwargs["reply_to_message_id"] = replying
|
kwargs["parse_mode"] = formatting or kwargs.get("parse_mode")
|
||||||
|
# ID of the message it's replying to (both argument names are valid)
|
||||||
|
kwargs["reply_to_message_id"] = replying or kwargs.get("reply_to_message_id")
|
||||||
|
# Reminder that dict.get(key) defaults to None if the key isn't found
|
||||||
|
|
||||||
if text.startswith(Reader.TAG_PREFIX):
|
if text.startswith(Reader.TAG_PREFIX):
|
||||||
|
# We're sending a media file ID
|
||||||
words = text.split(maxsplit=1)
|
words = text.split(maxsplit=1)
|
||||||
if logger:
|
if logger:
|
||||||
logger.info('Sending {} "{}" to {}'.format(words[0][4:-1], words[1], cid))
|
logger.info('Sending {} "{}" to {}'.format(words[0][4:-1], words[1], cid))
|
||||||
# eprint('[]')
|
|
||||||
# Logs something like 'Sending VIDEO "VIDEO_ID" to CHAT_ID'
|
# Logs something like 'Sending VIDEO "VIDEO_ID" to CHAT_ID'
|
||||||
|
|
||||||
if words[0] == Reader.STICKER_TAG:
|
if words[0] == Reader.STICKER_TAG:
|
||||||
|
@ -30,16 +35,18 @@ def send(bot, cid, text, replying=None, formatting=None, logger=None, **kwargs):
|
||||||
elif words[0] == Reader.VIDEO_TAG:
|
elif words[0] == Reader.VIDEO_TAG:
|
||||||
return bot.send_video(cid, words[1], **kwargs)
|
return bot.send_video(cid, words[1], **kwargs)
|
||||||
else:
|
else:
|
||||||
text
|
# It's text
|
||||||
if logger:
|
if logger:
|
||||||
mtype = "reply" if replying else "message"
|
mtype = "reply" if (kwargs.get("reply_to_message_id")) else "message"
|
||||||
logger.info("Sending a {} to {}: '{}'".format(mtype, cid, text))
|
logger.info("Sending a {} to {}: '{}'".format(mtype, cid, text))
|
||||||
# eprint('.')
|
# eprint('.')
|
||||||
return bot.send_message(cid, text, **kwargs)
|
return bot.send_message(cid, text, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Speaker(object):
|
class Speaker(object):
|
||||||
|
# Marks if the period is a fixed time when to send a new message
|
||||||
ModeFixed = "FIXED_MODE"
|
ModeFixed = "FIXED_MODE"
|
||||||
|
# Marks if the "periodic" messages have a weighted random chance to be sent, depending on the period
|
||||||
ModeChance = "CHANCE_MODE"
|
ModeChance = "CHANCE_MODE"
|
||||||
|
|
||||||
def __init__(self, username, archivist, logger, admin=0, nicknames=[],
|
def __init__(self, username, archivist, logger, admin=0, nicknames=[],
|
||||||
|
@ -47,35 +54,55 @@ class Speaker(object):
|
||||||
memory=20, mute_time=60, save_time=3600, bypass=False,
|
memory=20, mute_time=60, save_time=3600, bypass=False,
|
||||||
cid_whitelist=None, max_len=50
|
cid_whitelist=None, max_len=50
|
||||||
):
|
):
|
||||||
|
# List of nicknames other than the username that the bot can be called as
|
||||||
self.names = nicknames
|
self.names = nicknames
|
||||||
|
# Mute time for Telegram network errors
|
||||||
self.mute_time = mute_time
|
self.mute_time = mute_time
|
||||||
|
# Last mute timestamp
|
||||||
self.mute_timer = None
|
self.mute_timer = None
|
||||||
|
# The bot's username, "@" included
|
||||||
self.username = username
|
self.username = username
|
||||||
|
# The maximum chat period for this bot
|
||||||
self.max_period = archivist.max_period
|
self.max_period = archivist.max_period
|
||||||
|
|
||||||
|
# The Archivist functions to load and save from and to files
|
||||||
self.get_reader_file = archivist.get_reader
|
self.get_reader_file = archivist.get_reader
|
||||||
self.store_file = archivist.store
|
self.store_file = archivist.store
|
||||||
|
|
||||||
|
# Archivist function to crawl all stored Readers
|
||||||
self.readers_pass = archivist.readers_pass
|
self.readers_pass = archivist.readers_pass
|
||||||
|
|
||||||
|
# Legacy load logging emssages
|
||||||
logger.info("----")
|
logger.info("----")
|
||||||
logger.info("Finished loading.")
|
logger.info("Finished loading.")
|
||||||
logger.info("Loaded {} chats.".format(archivist.chat_count()))
|
logger.info("Loaded {} chats.".format(archivist.chat_count()))
|
||||||
logger.info("----")
|
logger.info("----")
|
||||||
|
|
||||||
|
# Wakeup flag that determines if it should send a wakeup message to stored groupchats
|
||||||
self.wakeup = wakeup
|
self.wakeup = wakeup
|
||||||
|
# The logger shared program-wide
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
# Chance of sending messages as replies
|
||||||
self.reply = reply
|
self.reply = reply
|
||||||
|
# Chance of sending 2 messages in a row
|
||||||
self.repeat = repeat
|
self.repeat = repeat
|
||||||
|
# If not empty, whitelist of chat IDs to only respond to
|
||||||
self.cid_whitelist = cid_whitelist
|
self.cid_whitelist = cid_whitelist
|
||||||
|
# Memory list/cache for the last accessed chats
|
||||||
self.memory = MemoryList(memory)
|
self.memory = MemoryList(memory)
|
||||||
|
# Minimum time to wait between memory saves (triggered at the next message from any chat)
|
||||||
self.save_time = save_time
|
self.save_time = save_time
|
||||||
|
# Last save timestamp
|
||||||
self.memory_timer = int(time.perf_counter())
|
self.memory_timer = int(time.perf_counter())
|
||||||
|
# Admin user ID
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
|
# For testing purposes
|
||||||
self.bypass = bypass
|
self.bypass = bypass
|
||||||
|
# Max word length for a message
|
||||||
self.max_len = max_len
|
self.max_len = max_len
|
||||||
|
|
||||||
|
# Sends an announcement to all chats that pass the check
|
||||||
def announce(self, bot, announcement, check=(lambda _: True)):
|
def announce(self, bot, announcement, check=(lambda _: True)):
|
||||||
# Sends an announcement to all chats that pass the check
|
|
||||||
for reader in self.readers_pass():
|
for reader in self.readers_pass():
|
||||||
try:
|
try:
|
||||||
if check(reader):
|
if check(reader):
|
||||||
|
@ -84,9 +111,9 @@ class Speaker(object):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# If wakeup flag is set, sends a wake-up message as announcement to all chats that
|
||||||
|
# are groups. Also, always sends a wakeup message to the 'bot admin'
|
||||||
def wake(self, bot, wake):
|
def wake(self, bot, wake):
|
||||||
# If wakeup flag is set, sends a wake-up message as announcement to all chats that
|
|
||||||
# are groups. Also, always sends a wakeup message to the 'bot admin'
|
|
||||||
send(bot, self.admin, wake)
|
send(bot, self.admin, wake)
|
||||||
|
|
||||||
if self.wakeup:
|
if self.wakeup:
|
||||||
|
@ -94,9 +121,13 @@ class Speaker(object):
|
||||||
return reader.check_type("group")
|
return reader.check_type("group")
|
||||||
self.announce(bot, wake, group_check)
|
self.announce(bot, wake, group_check)
|
||||||
|
|
||||||
|
# Looks up a reader in the memory list
|
||||||
def get_reader(self, cid):
|
def get_reader(self, cid):
|
||||||
return self.memory.get_next(lambda r: r.cid() == cid)
|
return self.memory.search(lambda r: r.cid() == cid, None)
|
||||||
|
|
||||||
|
# Looks up and returns a reader if it's in memory, or loads up a reader from
|
||||||
|
# file, adds it to memory, and returns it. Any other reader pushed out of
|
||||||
|
# memory is saved to file
|
||||||
def load_reader(self, chat):
|
def load_reader(self, chat):
|
||||||
cid = str(chat.id)
|
cid = str(chat.id)
|
||||||
reader = self.get_reader(cid)
|
reader = self.get_reader(cid)
|
||||||
|
@ -107,19 +138,24 @@ class Speaker(object):
|
||||||
if not reader:
|
if not reader:
|
||||||
reader = Reader.FromChat(chat, self.max_period, self.logger)
|
reader = Reader.FromChat(chat, self.max_period, self.logger)
|
||||||
|
|
||||||
old_reader = self.memory.append(reader)
|
old_reader = self.memory.add(reader)
|
||||||
if old_reader is not None:
|
if old_reader is not None:
|
||||||
old_reader.commit_memory()
|
old_reader.commit_memory()
|
||||||
self.store(old_reader)
|
self.store(old_reader)
|
||||||
|
|
||||||
return reader
|
return reader
|
||||||
|
|
||||||
|
# Returns a reader if it's in memory, or loads it up from a file and returns
|
||||||
|
# it otherwise. Does NOT add the Reader to memory
|
||||||
|
# This is useful for command prompts that do not require the Reader to be cached
|
||||||
def access_reader(self, cid):
|
def access_reader(self, cid):
|
||||||
reader = self.get_reader(cid)
|
reader = self.get_reader(cid)
|
||||||
if reader is None:
|
if reader is None:
|
||||||
return self.get_reader_file(cid)
|
return self.get_reader_file(cid)
|
||||||
return reader
|
return reader
|
||||||
|
|
||||||
|
# Returns True if the bot's username is called, or if one of the nicknames is
|
||||||
|
# mentioned and they're not another user's username
|
||||||
def mentioned(self, text):
|
def mentioned(self, text):
|
||||||
if self.username in text:
|
if self.username in text:
|
||||||
return True
|
return True
|
||||||
|
@ -128,20 +164,28 @@ class Speaker(object):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Returns True if not enough time has passed since the last mute timestamp
|
||||||
def is_mute(self):
|
def is_mute(self):
|
||||||
current_time = int(time.perf_counter())
|
current_time = int(time.perf_counter())
|
||||||
return self.mute_timer is not None and (current_time - self.mute_timer) < self.mute_time
|
return self.mute_timer is not None and (current_time - self.mute_timer) < self.mute_time
|
||||||
|
|
||||||
|
# Series of checks to determine if the bot should reply to a specific message, aside
|
||||||
|
# from the usual periodic messages
|
||||||
def should_reply(self, message, reader):
|
def should_reply(self, message, reader):
|
||||||
if self.is_mute():
|
if self.is_mute():
|
||||||
|
# Not if mute time hasn't finished
|
||||||
return False
|
return False
|
||||||
if not self.bypass and reader.is_restricted():
|
if not self.bypass and reader.is_restricted():
|
||||||
|
# If we're not in testing mode and the chat is restricted
|
||||||
user = message.chat.get_member(message.from_user.id)
|
user = message.chat.get_member(message.from_user.id)
|
||||||
if not self.user_is_admin(user):
|
if not self.user_is_admin(user):
|
||||||
# update.message.reply_text("You do not have permissions to do that.")
|
# ...And the user has no permissions, should not reply
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# otherwise (testing mode, or the chat is unrestricted, or the user has permissions)
|
||||||
replied = message.reply_to_message
|
replied = message.reply_to_message
|
||||||
text = message.text.casefold() if message.text else ""
|
text = message.text.casefold() if message.text else ""
|
||||||
|
# Only if it's a reply to a message of ours or the bot is mentioned in the message
|
||||||
return (((replied is not None) and (replied.from_user.name == self.username))
|
return (((replied is not None) and (replied.from_user.name == self.username))
|
||||||
or (self.mentioned(text)))
|
or (self.mentioned(text)))
|
||||||
|
|
||||||
|
@ -151,12 +195,14 @@ class Speaker(object):
|
||||||
else:
|
else:
|
||||||
self.store_file(*reader.archive())
|
self.store_file(*reader.archive())
|
||||||
|
|
||||||
|
# Check if enough time for saving memory has passed
|
||||||
def should_save(self):
|
def should_save(self):
|
||||||
current_time = int(time.perf_counter())
|
current_time = int(time.perf_counter())
|
||||||
elapsed = (current_time - self.memory_timer)
|
elapsed = (current_time - self.memory_timer)
|
||||||
self.logger.debug("Save check: {}".format(elapsed))
|
self.logger.debug("Save check: {}".format(elapsed))
|
||||||
return elapsed >= self.save_time
|
return elapsed >= self.save_time
|
||||||
|
|
||||||
|
# Save all Readers in memory to files if it's save time
|
||||||
def save(self):
|
def save(self):
|
||||||
if self.should_save():
|
if self.should_save():
|
||||||
self.logger.info("Saving chats in memory...")
|
self.logger.info("Saving chats in memory...")
|
||||||
|
@ -165,29 +211,38 @@ class Speaker(object):
|
||||||
self.memory_timer = time.perf_counter()
|
self.memory_timer = time.perf_counter()
|
||||||
self.logger.info("Chats saved.")
|
self.logger.info("Chats saved.")
|
||||||
|
|
||||||
|
# Reads a non-command message
|
||||||
def read(self, update, context):
|
def read(self, update, context):
|
||||||
|
# Check for save time
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
# Ignore non-message updates
|
||||||
if update.message is None:
|
if update.message is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
chat = update.message.chat
|
chat = update.message.chat
|
||||||
reader = self.load_reader(chat)
|
reader = self.load_reader(chat)
|
||||||
reader.read(update.message)
|
reader.read(update.message)
|
||||||
|
|
||||||
|
# Check if it's a "replyable" message & roll the chance to do so
|
||||||
if self.should_reply(update.message, reader) and reader.is_answering():
|
if self.should_reply(update.message, reader) and reader.is_answering():
|
||||||
self.say(context.bot, reader, replying=update.message.message_id)
|
self.say(context.bot, reader, replying=update.message.message_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Update the Reader's title if it has changed since the last message read
|
||||||
title = get_chat_title(update.message.chat)
|
title = get_chat_title(update.message.chat)
|
||||||
if title != reader.title():
|
if title != reader.title():
|
||||||
reader.set_title(title)
|
reader.set_title(title)
|
||||||
|
|
||||||
|
# Decrease the countdown for the chat, and send a message if it reached 0
|
||||||
reader.countdown -= 1
|
reader.countdown -= 1
|
||||||
if reader.countdown < 0:
|
if reader.countdown < 0:
|
||||||
reader.reset_countdown()
|
reader.reset_countdown()
|
||||||
|
# Random chance to reply to a recent message
|
||||||
rid = reader.random_memory() if random.random() <= self.reply else None
|
rid = reader.random_memory() if random.random() <= self.reply else None
|
||||||
self.say(context.bot, reader, replying=rid)
|
self.say(context.bot, reader, replying=rid)
|
||||||
|
|
||||||
|
# Handles /speak command
|
||||||
def speak(self, update, context):
|
def speak(self, update, context):
|
||||||
chat = (update.message.chat)
|
chat = (update.message.chat)
|
||||||
reader = self.load_reader(chat)
|
reader = self.load_reader(chat)
|
||||||
|
@ -200,12 +255,14 @@ class Speaker(object):
|
||||||
|
|
||||||
mid = str(update.message.message_id)
|
mid = str(update.message.message_id)
|
||||||
replied = update.message.reply_to_message
|
replied = update.message.reply_to_message
|
||||||
|
# Reply to the message that the command replies to, otherwise to the command itself
|
||||||
rid = replied.message_id if replied else mid
|
rid = replied.message_id if replied else mid
|
||||||
words = update.message.text.split()
|
words = update.message.text.split()
|
||||||
if len(words) > 1:
|
if len(words) > 1:
|
||||||
reader.read(' '.join(words[1:]))
|
reader.read(' '.join(words[1:]))
|
||||||
self.say(context.bot, reader, replying=rid)
|
self.say(context.bot, reader, replying=rid)
|
||||||
|
|
||||||
|
# Checks user permissions. Bot admin is always considered as having full permissions
|
||||||
def user_is_admin(self, member):
|
def user_is_admin(self, member):
|
||||||
self.logger.info("user {} ({}) requesting a restricted action".format(str(member.user.id), member.user.name))
|
self.logger.info("user {} ({}) requesting a restricted action".format(str(member.user.id), member.user.name))
|
||||||
# eprint('!')
|
# eprint('!')
|
||||||
|
@ -214,23 +271,30 @@ class Speaker(object):
|
||||||
or (member.status == 'administrator')
|
or (member.status == 'administrator')
|
||||||
or (member.user.id == self.admin))
|
or (member.user.id == self.admin))
|
||||||
|
|
||||||
|
# Generate speech (message)
|
||||||
def speech(self, reader):
|
def speech(self, reader):
|
||||||
return reader.generate_message(self.max_len)
|
return reader.generate_message(self.max_len)
|
||||||
|
|
||||||
|
# Say a newly generated message
|
||||||
def say(self, bot, reader, replying=None, **kwargs):
|
def say(self, bot, reader, replying=None, **kwargs):
|
||||||
cid = reader.cid()
|
cid = reader.cid()
|
||||||
if self.cid_whitelist is not None and cid not in self.cid_whitelist:
|
if self.cid_whitelist is not None and cid not in self.cid_whitelist:
|
||||||
|
# Don't, if there's a whitelist and this chat is not in it
|
||||||
return
|
return
|
||||||
if self.is_mute():
|
if self.is_mute():
|
||||||
|
# Don't, if mute time isn't over
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
send(bot, cid, self.speech(reader), replying, logger=self.logger, **kwargs)
|
send(bot, cid, self.speech(reader), replying, logger=self.logger, **kwargs)
|
||||||
if self.bypass:
|
if self.bypass:
|
||||||
|
# Testing mode, force a reasonable period (to not have the bot spam one specific chat with a low period)
|
||||||
max_period = self.max_period
|
max_period = self.max_period
|
||||||
reader.set_period(random.randint(max_period // 4, max_period))
|
reader.set_period(random.randint(max_period // 4, max_period))
|
||||||
if random.random() <= self.repeat:
|
if random.random() <= self.repeat:
|
||||||
send(bot, cid, self.speech(reader), logger=self.logger, **kwargs)
|
send(bot, cid, self.speech(reader), logger=self.logger, **kwargs)
|
||||||
|
# Consider any Network Error as a Telegram temporary ban, as I couldn't find
|
||||||
|
# out in the documentation how error 429 is handled by python-telegram-bot
|
||||||
except NetworkError as e:
|
except NetworkError as e:
|
||||||
self.logger.error("Sending a message caused network error:")
|
self.logger.error("Sending a message caused network error:")
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
|
@ -240,21 +304,25 @@ class Speaker(object):
|
||||||
self.logger.error("Sending a message caused exception:")
|
self.logger.error("Sending a message caused exception:")
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
|
|
||||||
|
# Handling /count command
|
||||||
def get_count(self, update, context):
|
def get_count(self, update, context):
|
||||||
cid = str(update.message.chat.id)
|
cid = str(update.message.chat.id)
|
||||||
reader = self.access_reader(cid)
|
reader = self.load_reader(cid)
|
||||||
|
|
||||||
num = str(reader.count()) if reader else "no"
|
num = str(reader.count()) if reader else "no"
|
||||||
update.message.reply_text("I remember {} messages.".format(num))
|
update.message.reply_text("I remember {} messages.".format(num))
|
||||||
|
|
||||||
|
# Handling /get_chats command (exclusive for bot admin)
|
||||||
def get_chats(self, update, context):
|
def get_chats(self, update, context):
|
||||||
lines = ["[{}]: {}".format(reader.cid(), reader.title()) for reader in self.readers_pass()]
|
lines = ["[{}]: {}".format(reader.cid(), reader.title()) for reader in self.readers_pass()]
|
||||||
chat_list = "\n".join(lines)
|
chat_list = "\n".join(lines)
|
||||||
update.message.reply_text("I have the following chats:\n\n" + chat_list)
|
update.message.reply_text("I have the following chats:\n\n" + chat_list)
|
||||||
|
|
||||||
|
# Handling /period command
|
||||||
|
# Print the current period or set a new one if one is given
|
||||||
def period(self, update, context):
|
def period(self, update, context):
|
||||||
chat = update.message.chat
|
chat = update.message.chat
|
||||||
reader = self.access_reader(str(chat.id))
|
reader = self.load_reader(str(chat.id))
|
||||||
|
|
||||||
words = update.message.text.split()
|
words = update.message.text.split()
|
||||||
if len(words) <= 1:
|
if len(words) <= 1:
|
||||||
|
@ -270,13 +338,14 @@ class Speaker(object):
|
||||||
period = int(words[1])
|
period = int(words[1])
|
||||||
period = reader.set_period(period)
|
period = reader.set_period(period)
|
||||||
update.message.reply_text("Period of speaking set to {}.".format(period))
|
update.message.reply_text("Period of speaking set to {}.".format(period))
|
||||||
self.store_file(*reader.archive())
|
|
||||||
except Exception:
|
except Exception:
|
||||||
update.message.reply_text("Format was confusing; period unchanged from {}.".format(reader.period()))
|
update.message.reply_text("Format was confusing; period unchanged from {}.".format(reader.period()))
|
||||||
|
|
||||||
|
# Handling /answer command
|
||||||
|
# Print the current answer probability or set a new one if one is given
|
||||||
def answer(self, update, context):
|
def answer(self, update, context):
|
||||||
chat = update.message.chat
|
chat = update.message.chat
|
||||||
reader = self.access_reader(str(chat.id))
|
reader = self.load_reader(str(chat.id))
|
||||||
|
|
||||||
words = update.message.text.split()
|
words = update.message.text.split()
|
||||||
if len(words) <= 1:
|
if len(words) <= 1:
|
||||||
|
@ -292,17 +361,18 @@ class Speaker(object):
|
||||||
answer = float(words[1])
|
answer = float(words[1])
|
||||||
answer = reader.set_answer(answer)
|
answer = reader.set_answer(answer)
|
||||||
update.message.reply_text("Answer probability set to {}.".format(answer))
|
update.message.reply_text("Answer probability set to {}.".format(answer))
|
||||||
self.store_file(*reader.archive())
|
|
||||||
except Exception:
|
except Exception:
|
||||||
update.message.reply_text("Format was confusing; answer probability unchanged from {}.".format(reader.answer()))
|
update.message.reply_text("Format was confusing; answer probability unchanged from {}.".format(reader.answer()))
|
||||||
|
|
||||||
|
# Handling /restrict command
|
||||||
|
# Toggle the restriction value if it's a group chat and the user has permissions to do so
|
||||||
def restrict(self, update, context):
|
def restrict(self, update, context):
|
||||||
if "group" not in update.message.chat.type:
|
if "group" not in update.message.chat.type:
|
||||||
update.message.reply_text("That only works in groups.")
|
update.message.reply_text("That only works in groups.")
|
||||||
return
|
return
|
||||||
chat = update.message.chat
|
chat = update.message.chat
|
||||||
user = chat.get_member(update.message.from_user.id)
|
user = chat.get_member(update.message.from_user.id)
|
||||||
reader = self.access_reader(str(chat.id))
|
reader = self.load_reader(str(chat.id))
|
||||||
|
|
||||||
if reader.is_restricted():
|
if reader.is_restricted():
|
||||||
if not self.user_is_admin(user):
|
if not self.user_is_admin(user):
|
||||||
|
@ -311,15 +381,16 @@ class Speaker(object):
|
||||||
reader.toggle_restrict()
|
reader.toggle_restrict()
|
||||||
allowed = "let only admins" if reader.is_restricted() else "let everyone"
|
allowed = "let only admins" if reader.is_restricted() else "let everyone"
|
||||||
update.message.reply_text("I will {} configure me now.".format(allowed))
|
update.message.reply_text("I will {} configure me now.".format(allowed))
|
||||||
self.store_file(*reader.archive())
|
|
||||||
|
|
||||||
|
# Handling /silence command
|
||||||
|
# Toggle the silence value if it's a group chat and the user has permissions to do so
|
||||||
def silence(self, update, context):
|
def silence(self, update, context):
|
||||||
if "group" not in update.message.chat.type:
|
if "group" not in update.message.chat.type:
|
||||||
update.message.reply_text("That only works in groups.")
|
update.message.reply_text("That only works in groups.")
|
||||||
return
|
return
|
||||||
chat = update.message.chat
|
chat = update.message.chat
|
||||||
user = chat.get_member(update.message.from_user.id)
|
user = chat.get_member(update.message.from_user.id)
|
||||||
reader = self.access_reader(str(chat.id))
|
reader = self.load_reader(str(chat.id))
|
||||||
|
|
||||||
if reader.is_restricted():
|
if reader.is_restricted():
|
||||||
if not self.user_is_admin(user):
|
if not self.user_is_admin(user):
|
||||||
|
@ -328,8 +399,8 @@ class Speaker(object):
|
||||||
reader.toggle_silence()
|
reader.toggle_silence()
|
||||||
allowed = "avoid mentioning" if reader.is_silenced() else "mention"
|
allowed = "avoid mentioning" if reader.is_silenced() else "mention"
|
||||||
update.message.reply_text("I will {} people now.".format(allowed))
|
update.message.reply_text("I will {} people now.".format(allowed))
|
||||||
self.store_file(*reader.archive())
|
|
||||||
|
|
||||||
|
# Handling /who command
|
||||||
def who(self, update, context):
|
def who(self, update, context):
|
||||||
msg = update.message
|
msg = update.message
|
||||||
usr = msg.from_user
|
usr = msg.from_user
|
||||||
|
@ -346,6 +417,7 @@ class Speaker(object):
|
||||||
|
|
||||||
msg.reply_markdown(answer)
|
msg.reply_markdown(answer)
|
||||||
|
|
||||||
|
# Handling /where command
|
||||||
def where(self, update, context):
|
def where(self, update, context):
|
||||||
msg = update.message
|
msg = update.message
|
||||||
chat = msg.chat
|
chat = msg.chat
|
||||||
|
|
|
@ -90,7 +90,11 @@ def main():
|
||||||
parser.add_argument('-m', '--mute_time', metavar='T', type=int, default=60,
|
parser.add_argument('-m', '--mute_time', metavar='T', type=int, default=60,
|
||||||
help='The time (in s) for the muting period when Telegram limits the bot. (default: 60).')
|
help='The time (in s) for the muting period when Telegram limits the bot. (default: 60).')
|
||||||
parser.add_argument('-s', '--save_time', metavar='T', type=int, default=3600,
|
parser.add_argument('-s', '--save_time', metavar='T', type=int, default=3600,
|
||||||
help='The time (in s) for periodic saves (default: 3600).')
|
help='The time (in s) for periodic saves. (default: 3600)')
|
||||||
|
parser.add_argument('-p', '--min_period', metavar='MIN_P', type=int, default=1,
|
||||||
|
help='The minimum value for a chat\'s period. (default: 1)')
|
||||||
|
parser.add_argument('-P', '--max_period', metavar='MAX_P', type=int, default=100000,
|
||||||
|
help='The maximum value for a chat\'s period. (default: 100000)')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -104,6 +108,8 @@ def main():
|
||||||
archivist = Archivist(logger,
|
archivist = Archivist(logger,
|
||||||
chatdir=args.directory,
|
chatdir=args.directory,
|
||||||
chatext=".vls",
|
chatext=".vls",
|
||||||
|
min_period=args.min_period,
|
||||||
|
max_period=args.max_period,
|
||||||
read_only=False
|
read_only=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue