Add random user generator (#1)

This PR adds scripts for generating random users, and generating random relationships between these users. This PR also refactors config file loading and writing into a class.

Reviewed-on: https://codeberg.org/vlw/misskey-microblogger/pulls/1
Co-authored-by: Victor Westerlund <victor.vesterlund@gmail.com>
Co-committed-by: Victor Westerlund <victor.vesterlund@gmail.com>
This commit is contained in:
Victor Westerlund 2024-11-14 10:02:03 +00:00 committed by Victor Westerlund
parent dcf582d9cf
commit ac5b9f3955
13 changed files with 328 additions and 47 deletions

2
.gitignore vendored
View file

@ -1,6 +1,6 @@
# Config # Config
data/users data/users
config.json .config.json
# Bootstrapping # Bootstrapping
__pycache__ __pycache__

View file

@ -1,24 +1,13 @@
{ {
"key": "API_KEY", "key": "API_KEY",
"online": { "online": {
"intervals": [ "intervals": []
{
"start": {
"from": 0.00,
"to": 0.00
},
"end": {
"from": 24.00,
"to": 24.00
}
}
]
}, },
"actions": { "actions": {
"posts": { "posts": {
"public": { "public": {
"percent": 0, "percent": 0,
"cooldown": 86400 "cooldown": 0
}, },
"specified": { "specified": {
"percent": { "percent": {
@ -27,7 +16,7 @@
"neutral": 0, "neutral": 0,
"enemies": 0 "enemies": 0
}, },
"cooldown": 86400 "cooldown": 0
} }
}, },
"replies": { "replies": {
@ -85,6 +74,12 @@
} }
} }
}, },
"note": {
"text_length": {
"flux": 0,
"average": 0
}
},
"relationships": { "relationships": {
"friends": [], "friends": [],
"enemies": [], "enemies": [],

46
generate.py Normal file
View file

@ -0,0 +1,46 @@
import sys
import json
import typing
from os import system
from src.Config import Config
from src.User.User import User
from src.Generate.GenerateUser import GenerateUser
from src.Generate.GenerateRelationships import GenerateRelationships
DEFAULT_GENERATE_USER_COUNT = 10
# Generate a user
def generate_user() -> User:
while True:
user = GenerateUser()
print(f"1. Create username: {user.username}")
# We need the human's help here since I haven't found a way to get API keys from Misskey automatically
user.set_api_key(input("2. Paste API key:"))
yield user.autorun()
def main():
config = Config()
users = []
# Generate n amount of users
for i in (range(int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_GENERATE_USER_COUNT)):
system("clear")
print(f"Generating user: {i + 1}")
users.append(next(generate_user()))
i += 1
# Create random relationships for generated users
print("Generating random user relationships")
GenerateRelationships(users).autorun()
print("Activating users")
# Add generated users to active users in config
for user in users:
config.add_active_user(user.username)
config.save_config()
if __name__ == "__main__":
main()

18
run.py
View file

@ -2,26 +2,18 @@ import typing
import random import random
import json import json
from src.Config import Config
from src.Poster import Poster from src.Poster import Poster
from src.User.User import User
from src.Misskey import Misskey
def main(): def main():
with open("config.json", "r") as f: config = Config()
config = json.load(f)
# Don't do ANYTHING this time if the roll against the global activity percentage failed # Don't do ANYTHING this time if the roll against the global activity percentage failed
if (random.randint(0, 100) >= config["global"]["activity"]): if (not config.get_global_activity_roll()):
return False return False
for username in config["active_users"]: for username in config.get_active_users():
user = User(username) Poster(username).autorun()
# Don't do anything for this user if they're not active right now
if (not user.is_online()):
continue
Poster(user.username, Misskey(config["server"]["url"], user.config["key"])).autorun()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

28
src/Config.py Normal file
View file

@ -0,0 +1,28 @@
import json
import random
import typing
from pathlib import Path
CONFIG_FILEPATH = Path.cwd() / ".config.json"
class Config():
def __init__(self):
with open(CONFIG_FILEPATH, "r") as f:
self.config = json.load(f)
# Overwrite modified config
def save_config(self) -> bool:
with open(CONFIG_FILEPATH, "w") as f:
json.dump(self.config, f, indent=4, ensure_ascii=False)
def add_active_user(self, username: str) -> None:
self.config["active_users"].append(username)
def get_global_activity_roll(self) -> bool:
return random.randint(0, 100) < self.config["global"]["activity"]
def get_misskey_server(self) -> str:
return self.config["server"]["url"]
def get_active_users(self) -> list:
return self.config["active_users"]

View file

@ -16,25 +16,25 @@ class DictionaryParser(Dictionary):
tokens = [token.lower() for token in note["text"].split(" ")] tokens = [token.lower() for token in note["text"].split(" ")]
return set(tokens) return set(tokens)
def get_tone(self) -> NoteTones: def get_tone(self) -> NoteTones | None:
words = self.get_words(Dictionaries.CONTROL)["tone"] words = self.get_words(Dictionaries.CONTROL)["tone"]
if (self.tokens & set(words[NoteTones.INFORMAL.value])): return NoteTones.INFORMAL if (self.tokens & set(words[NoteTones.INFORMAL.value])): return NoteTones.INFORMAL
elif (self.tokens & set(words[NoteTones.FORMAL.value])): return NoteTones.FORMAL elif (self.tokens & set(words[NoteTones.FORMAL.value])): return NoteTones.FORMAL
return NoteTones.UNKNOWN return None
def get_mood(self) -> NoteMoods: def get_mood(self) -> NoteMoods | None:
words = self.get_words(Dictionaries.CONTROL)["mood"] words = self.get_words(Dictionaries.CONTROL)["mood"]
if (self.tokens & set(words[NoteMoods.FUNNY.value])): return NoteMoods.FUNNY if (self.tokens & set(words[NoteMoods.FUNNY.value])): return NoteMoods.FUNNY
elif (self.tokens & set(words[NoteMoods.DECENT.value])): return NoteMoods.DECENT elif (self.tokens & set(words[NoteMoods.DECENT.value])): return NoteMoods.DECENT
elif (self.tokens & set(words[NoteMoods.ANNOYED.value])): return NoteMoods.ANNOYED elif (self.tokens & set(words[NoteMoods.ANNOYED.value])): return NoteMoods.ANNOYED
return NoteMoods.UNKNOWN return None
def get_type(self) -> NoteTypes: def get_type(self) -> NoteTypes | None:
words = self.get_words(Dictionaries.CONTROL)["type"] words = self.get_words(Dictionaries.CONTROL)["type"]
if (self.tokens & set(words[NoteTypes.QUESTION.value])): return NoteTypes.QUESTION if (self.tokens & set(words[NoteTypes.QUESTION.value])): return NoteTypes.QUESTION
elif (self.tokens & set(words[NoteTypes.EXAGGERATED.value])): return NoteTypes.EXAGGERATED elif (self.tokens & set(words[NoteTypes.EXAGGERATED.value])): return NoteTypes.EXAGGERATED
elif (self.tokens & set(words[NoteTypes.STATEMENT.value])): return NoteTypes.STATEMENT elif (self.tokens & set(words[NoteTypes.STATEMENT.value])): return NoteTypes.STATEMENT
return NoteTypes.UNKNOWN return None

View file

@ -1,8 +1,5 @@
from enum import Enum from enum import Enum
class Traits(Enum):
UNKNOWN = None
class Errors(Enum): class Errors(Enum):
WORD_LIST_TOO_LONG = 0 WORD_LIST_TOO_LONG = 0
WORD_LIST_HAS_DUPLICATES = 1 WORD_LIST_HAS_DUPLICATES = 1
@ -43,16 +40,13 @@ class ControlWords(Enum):
class NoteTones(Enum): class NoteTones(Enum):
FORMAL = "formal" FORMAL = "formal"
INFORMAL = "informal" INFORMAL = "informal"
UNKNOWN = Traits.UNKNOWN.value
class NoteMoods(Enum): class NoteMoods(Enum):
FUNNY = "funny" FUNNY = "funny"
DECENT = "decent" DECENT = "decent"
ANNOYED = "annoyed" ANNOYED = "annoyed"
UNKNOWN = Traits.UNKNOWN.value
class NoteTypes(Enum): class NoteTypes(Enum):
QUESTION = "question" QUESTION = "question"
EXAGGERATED = "exaggerated" EXAGGERATED = "exaggerated"
STATEMENT = "statement" STATEMENT = "statement"
UNKNOWN = Traits.UNKNOWN.value

View file

@ -0,0 +1,101 @@
import typing
import random
from ..Misskey import Misskey
from ..Enums import RelationshipType
from ..User.User import USER_CONFIG_DIR, User
CHANCE_FRIEND = 70
CHANCE_PARTNER = 30
MIN_RELATIONSHIPS = 3
MAX_RELATIONSHIPS = 7
RECURSE_LIMIT = 10
class GenerateRelationships():
def __init__(self, users: list = None):
self.users = users
self.partners = []
# Compute diff between a provided target list of users against list of all users
def users_diff(self, target: list) -> list:
return list(set(self.users) - set(target))
def pick_random_user_from_list(self, target: list, ignore: User) -> User | None:
available = self.users_diff(target)
available.remove(ignore)
return random.choice(available) if available else None
# Pick a random partner from available partners
def set_random_partner(self, user: User, i: int = 0) -> None:
# Pick a random user
partner = self.pick_random_user_from_list(self.partners, user)
# Give up trying to find a partner if none found or we've reached the recurse depth limit
if (not partner or i >= RECURSE_LIMIT):
return None
# Don't partner up with this user if they're already a friend or enemy
if (user.get_relationship_with_user(partner.username) != RelationshipType.NEUTRAL):
return self.set_random_partner(user, i + 1)
# Set partner's usernames in each other's configs
user.config["relationships"][RelationshipType.PARTNER.value] = partner.username
partner.config["relationships"][RelationshipType.PARTNER.value] = user.username
# Mark users as partners
self.partners.extend((user, partner))
# Set a another random user as friend or foe
def set_random_user_relationship(self, user: User, i: int = 0) -> None:
# Roll if ranomd user should be a friend or enemy
relationship = RelationshipType.FRIEND if random.randint(0, 100) < CHANCE_FRIEND else RelationshipType.ENEMY
# Get available friends (not already a friend, enemy, or self)
target = self.pick_random_user_from_list(user.get_friends() + user.get_enemies(), user)
# Give up trying to find a friend if none found or if we've reached the recurse depth limit
if (not target or i >= RECURSE_LIMIT):
return None
# Try again if target user is not neutral with user
if (user.get_relationship_with_user(target.username) != RelationshipType.NEUTRAL):
return self.set_random_user_relationship(user, i + 1)
# Set relationship on both users
user.config["relationships"][relationship.value].append(target.username)
target.config["relationships"][relationship.value].append(user.username)
return None
# Save all modified user configs and follow users with new relationships
def save_all(self) -> None:
for user in self.users:
# Save modified config
user.save_config()
# Create a Misskey instance for user
mk = Misskey(user.get_api_key())
# Place partner string in a list if set
partner = [user.get_partner()] if user.get_partner() else []
# Follow all users on Misskey we've added relationships for. It's required for the home timeline
for username in user.get_friends() + user.get_enemies() + partner:
mk.follow_user(username)
def autorun(self) -> None:
for user in self.users:
# Find a random partner for user if they don't have one already and if roll is in bounds
if (random.randint(0, 100) < CHANCE_PARTNER and not user.get_partner()):
self.set_random_partner(user)
# Set random relationships for user
for i in range(random.randint(MIN_RELATIONSHIPS, MAX_RELATIONSHIPS)):
self.set_random_user_relationship(user)
return self.save_all()

View file

@ -0,0 +1,107 @@
import math
import json
import typing
import random
from pathlib import Path
from ..User.User import USER_CONFIG_DIR, User
from ..Enums import RelationshipType, NoteTones, NoteMoods, NoteTypes
from misskey.enum import NoteVisibility
from random_username.generate import generate_username
# Use this file as a template for generated users
USER_TEMPLATE_FILE = Path.cwd() / "data" / "users_template.json"
# Minimum and maximum cooldown time for posting new notes
CONFIG_POST_MIN_COOLDOWN = 300 # 10 minutes
CONFIG_POST_MAX_COOLDOWN = 259200 # 48 hours
# Available preferred reactions
REACTIONS = [
"",
"👍",
"😆"
]
class GenerateUser():
def __init__(self):
self.username = self.gen_username()
# Load user template file
with open(USER_TEMPLATE_FILE, "r") as f:
self.config = json.load(f)
# Return a random unique username
@staticmethod
def gen_username() -> str:
username = generate_username(1)[0]
return username if not (USER_CONFIG_DIR / f"{username}.json").exists() else GenerateUser.gen_username()
# Generate a random time interval
@staticmethod
def gen_interval() -> dict:
def to_time(h: int) -> float:
return float(f"{h}.{random.randint(0, 59)}")
start_hour_from = random.randint(0, 22)
start_hour_end = start_hour_from + 1
end_hour_from = random.randint(min(start_hour_end + 1, 22), 23)
end_hour_to = min(end_hour_from + 1, 23)
return {
"start": {
"from": to_time(start_hour_from),
"to": to_time(start_hour_end)
},
"end": {
"from": to_time(end_hour_from),
"to": to_time(end_hour_to)
}
}
def set_api_key(self, key: str) -> None:
self.config["key"] = key
def save_config(self) -> bool:
USER_CONFIG_DIR.mkdir(exist_ok=True)
with open(USER_CONFIG_DIR / f"{self.username}.json", "w") as f:
json.dump(self.config, f, indent=4, ensure_ascii=False)
def autorun(self) -> User:
# Generate a random online interval
self.config["online"]["intervals"].append(self.gen_interval())
# Set a random percent for user to post new notes
self.config["actions"]["posts"]["public"]["percent"] = random.randint(0, 100)
self.config["actions"]["posts"]["public"]["cooldown"] = random.randint(CONFIG_POST_MIN_COOLDOWN, CONFIG_POST_MAX_COOLDOWN)
# Set random reply chance for each relationship
for visiblity in [NoteVisibility.PUBLIC, NoteVisibility.SPECIFIED]:
for relationship in RelationshipType:
self.config["actions"]["replies"][visiblity.value]["percent"][relationship.value] = random.randint(0, 100)
# Set random react chances for each relationship
for relationship in RelationshipType:
self.config["actions"]["reacts"]["percent"][relationship.value] = random.randint(0, 100)
# Set preferred reactions for each relationship
for relationship in RelationshipType:
self.config["actions"]["reacts"]["prefrerred_reaction"][relationship.value] = random.choice(REACTIONS)
# Set random personality tone
for x in NoteTones:
self.config["personality"]["tone"]["probability"][x.value] = random.randint(0, 100)
# Set random personality mood
for x in NoteMoods:
self.config["personality"]["mood"]["probability"][x.value] = random.randint(0, 100)
# Set random personality type
for x in NoteTypes:
self.config["personality"]["type"]["probability"][x.value] = random.randint(0, 100)
self.save_config()
return User(self.username)

0
src/Generate/__init__.py Normal file
View file

View file

@ -1,6 +1,8 @@
import typing import typing
from datetime import datetime, timedelta from datetime import datetime, timedelta
from .Config import Config
from misskey import Misskey as lib_Misskey from misskey import Misskey as lib_Misskey
from misskey.enum import NotificationsType, NoteVisibility from misskey.enum import NotificationsType, NoteVisibility
@ -12,8 +14,8 @@ TIMELINE_FETCH_LIMIT = 5
TIMELINE_THROTTLE_SECONDS = 300 TIMELINE_THROTTLE_SECONDS = 300
class Misskey(lib_Misskey): class Misskey(lib_Misskey):
def __init__(self, server: str, key: str): def __init__(self, key: str):
super().__init__(server, i=key) super().__init__(Config().get_misskey_server(), i=key)
# Reverse lists returned by Misskey.py because for some stupid reason they're ordered oldest-to-newest # Reverse lists returned by Misskey.py because for some stupid reason they're ordered oldest-to-newest
@staticmethod @staticmethod
@ -82,3 +84,9 @@ class Misskey(lib_Misskey):
note_id=note["id"], note_id=note["id"],
reaction=reaction reaction=reaction
) )
# Send a follow request to username
def follow_user(self, username: str) -> dict:
return self.following_create(
user_id=self.resolve_user_id(username)
)

View file

@ -2,17 +2,17 @@ import typing
from datetime import datetime from datetime import datetime
from .Note import Note from .Note import Note
from .Enums import Intent
from .Misskey import Misskey from .Misskey import Misskey
from .User.UserIntent import UserIntent from .User.UserIntent import UserIntent
from misskey.enum import NoteVisibility from misskey.enum import NoteVisibility
class Poster(): class Poster():
def __init__(self, username: str, mk: Misskey): def __init__(self, username: str):
self.mk = mk
self.note = Note(username)
self.user = UserIntent(username) self.user = UserIntent(username)
self.note = Note(username)
self.mk = Misskey(self.user.get_api_key())
def note_is_older_than_cooldown(self, note: dict) -> bool: def note_is_older_than_cooldown(self, note: dict) -> bool:
date_now = datetime.now() date_now = datetime.now()

View file

@ -19,6 +19,13 @@ class User():
with open(USER_CONFIG_DIR / f"{self.username}.json", "r") as f: with open(USER_CONFIG_DIR / f"{self.username}.json", "r") as f:
self.config = json.load(f) self.config = json.load(f)
# Overwrite current config to user config file
def save_config(self) -> None:
USER_CONFIG_DIR.mkdir(exist_ok=True)
with open(USER_CONFIG_DIR / f"{self.username}.json", "w") as f:
json.dump(self.config, f, indent=4, ensure_ascii=False)
# Check if the user is currently online given their time intervals # Check if the user is currently online given their time intervals
def is_online(self) -> bool: def is_online(self) -> bool:
# Find the first time interval that is within the current time # Find the first time interval that is within the current time
@ -38,6 +45,9 @@ class User():
# Current time was not in range of any configured intervals # Current time was not in range of any configured intervals
return False return False
def get_api_key(self) -> str:
return self.config["key"]
def get_post_cooldown(self, visibility: NoteVisibility = NoteVisibility.PUBLIC) -> int: def get_post_cooldown(self, visibility: NoteVisibility = NoteVisibility.PUBLIC) -> int:
return self.config["actions"][Intent.POST.value][visibility.value]["cooldown"] return self.config["actions"][Intent.POST.value][visibility.value]["cooldown"]