update documentation and add docs from config
This commit is contained in:
24
docs/API/api.md
Normal file
24
docs/API/api.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# pyasic
|
||||||
|
## Miner APIs
|
||||||
|
Each miner has a unique API that is used to communicate with it.
|
||||||
|
Each of these API types has commands that differ between them, and some commands have data that others do not.
|
||||||
|
Each miner that is a subclass of `BaseMiner` should have an API linked to it as `Miner.api`.
|
||||||
|
|
||||||
|
All API implementations inherit from [`BaseMinerAPI`][pyasic.API.BaseMinerAPI], which implements the basic communications protocols.
|
||||||
|
|
||||||
|
BaseMinerAPI should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
|
||||||
|
Use these instead -
|
||||||
|
|
||||||
|
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]
|
||||||
|
#### [BOSMiner API][pyasic.API.bosminer.BOSMinerAPI]
|
||||||
|
#### [BTMiner API][pyasic.API.btminer.BTMinerAPI]
|
||||||
|
#### [CGMiner API][pyasic.API.cgminer.CGMinerAPI]
|
||||||
|
#### [Unknown API][pyasic.API.unknown.UnknownAPI]
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## BaseMinerAPI
|
||||||
|
::: pyasic.API.BaseMinerAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
heading_level: 4
|
||||||
7
docs/API/bmminer.md
Normal file
7
docs/API/bmminer.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# pyasic
|
||||||
|
## BMMinerAPI
|
||||||
|
::: pyasic.API.bmminer.BMMinerAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
7
docs/API/bosminer.md
Normal file
7
docs/API/bosminer.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# pyasic
|
||||||
|
## BOSMinerAPI
|
||||||
|
::: pyasic.API.bosminer.BOSMinerAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
7
docs/API/btminer.md
Normal file
7
docs/API/btminer.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# pyasic
|
||||||
|
## BTMinerAPI
|
||||||
|
::: pyasic.API.btminer.BTMinerAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
7
docs/API/cgminer.md
Normal file
7
docs/API/cgminer.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# pyasic
|
||||||
|
## CGMinerAPI
|
||||||
|
::: pyasic.API.cgminer.CGMinerAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
7
docs/API/unknown.md
Normal file
7
docs/API/unknown.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# pyasic
|
||||||
|
## UnknownAPI
|
||||||
|
::: pyasic.API.unknown.UnknownAPI
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
42
docs/api.md
42
docs/api.md
@@ -1,42 +0,0 @@
|
|||||||
# pyasic
|
|
||||||
## Miner APIs
|
|
||||||
Each miner has a unique API that is used to communicate with it.
|
|
||||||
Each of these API types has commands that differ between them, and some commands have data that others do not.
|
|
||||||
Each miner that is a subclass of `BaseMiner` should have an API linked to it as `Miner.api`.
|
|
||||||
|
|
||||||
All API implementations inherit from `BaseMinerAPI`, which implements the basic communications protocols.
|
|
||||||
|
|
||||||
## BMMinerAPI
|
|
||||||
::: pyasic.API.bmminer.BMMinerAPI
|
|
||||||
handler: python
|
|
||||||
options:
|
|
||||||
show_root_heading: false
|
|
||||||
heading_level: 4
|
|
||||||
|
|
||||||
## BOSMinerAPI
|
|
||||||
::: pyasic.API.bosminer.BOSMinerAPI
|
|
||||||
handler: python
|
|
||||||
options:
|
|
||||||
show_root_heading: false
|
|
||||||
heading_level: 4
|
|
||||||
|
|
||||||
## BTMinerAPI
|
|
||||||
::: pyasic.API.btminer.BTMinerAPI
|
|
||||||
handler: python
|
|
||||||
options:
|
|
||||||
show_root_heading: false
|
|
||||||
heading_level: 4
|
|
||||||
|
|
||||||
## CGMinerAPI
|
|
||||||
::: pyasic.API.cgminer.CGMinerAPI
|
|
||||||
handler: python
|
|
||||||
options:
|
|
||||||
show_root_heading: false
|
|
||||||
heading_level: 4
|
|
||||||
|
|
||||||
## UnknownAPI
|
|
||||||
::: pyasic.API.unknown.UnknownAPI
|
|
||||||
handler: python
|
|
||||||
options:
|
|
||||||
show_root_heading: false
|
|
||||||
heading_level: 4
|
|
||||||
24
docs/config/miner_config.md
Normal file
24
docs/config/miner_config.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# pyasic
|
||||||
|
## Miner Config
|
||||||
|
|
||||||
|
::: pyasic.config.MinerConfig
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## Pool Groups
|
||||||
|
|
||||||
|
::: pyasic.config._PoolGroup
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
|
|
||||||
|
## Pools
|
||||||
|
|
||||||
|
::: pyasic.config._Pool
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
heading_level: 4
|
||||||
20
mkdocs.yml
20
mkdocs.yml
@@ -2,12 +2,22 @@ site_name: pyasic
|
|||||||
repo_url: https://github.com/UpstreamData/pyasic
|
repo_url: https://github.com/UpstreamData/pyasic
|
||||||
nav:
|
nav:
|
||||||
- Introduction: "index.md"
|
- Introduction: "index.md"
|
||||||
- Usage:
|
- Miners:
|
||||||
- Miner Factory: "miner_factory.md"
|
- Miner Factory: "miners/miner_factory.md"
|
||||||
- Miner Network: "miner_network.md"
|
- Network:
|
||||||
- Miner Data: "miner_data.md"
|
- Miner Network: "network/miner_network.md"
|
||||||
|
- Data:
|
||||||
|
- Miner Data: "data/miner_data.md"
|
||||||
|
- Config:
|
||||||
|
- Miner Config: "config/miner_config.md"
|
||||||
- Advanced:
|
- Advanced:
|
||||||
- API: "api.md"
|
- Miner APIs:
|
||||||
|
- Base: "API/api.md"
|
||||||
|
- BMMiner: "API/bmminer.md"
|
||||||
|
- BOSMiner: "API/bosminer.md"
|
||||||
|
- BTMiner: "API/btminer.md"
|
||||||
|
- CGMiner: "API/cgminer.md"
|
||||||
|
- Unknown: "API/unknown.md"
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- mkdocstrings
|
- mkdocstrings
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import json
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import warnings
|
import warnings
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
class APIError(Exception):
|
class APIError(Exception):
|
||||||
@@ -98,8 +99,8 @@ If you are sure you want to use this command please use API.send_command("{item}
|
|||||||
|
|
||||||
async def send_command(
|
async def send_command(
|
||||||
self,
|
self,
|
||||||
command: str or bytes,
|
command: Union[str, bytes],
|
||||||
parameters: str or int or bool = None,
|
parameters: Union[str, int, bool] = None,
|
||||||
ignore_errors: bool = False,
|
ignore_errors: bool = False,
|
||||||
x19_command: bool = False,
|
x19_command: bool = False,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from typing import List, Literal
|
from typing import Literal, List
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
@@ -13,9 +13,10 @@ import time
|
|||||||
class _Pool:
|
class _Pool:
|
||||||
"""A dataclass for pool information.
|
"""A dataclass for pool information.
|
||||||
|
|
||||||
:param url: URL of the pool.
|
Attributes:
|
||||||
:param username: Username on the pool.
|
url: URL of the pool.
|
||||||
:param password: Worker password on the pool.
|
username: Username on the pool.
|
||||||
|
password: Worker password on the pool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url: str = ""
|
url: str = ""
|
||||||
@@ -25,7 +26,8 @@ class _Pool:
|
|||||||
def from_dict(self, data: dict):
|
def from_dict(self, data: dict):
|
||||||
"""Convert raw pool data as a dict to usable data and save it to this class.
|
"""Convert raw pool data as a dict to usable data and save it to this class.
|
||||||
|
|
||||||
:param data: The raw config data to convert.
|
Parameters:
|
||||||
|
data: The raw config data to convert.
|
||||||
"""
|
"""
|
||||||
for key in data.keys():
|
for key in data.keys():
|
||||||
if key == "url":
|
if key == "url":
|
||||||
@@ -36,10 +38,11 @@ class _Pool:
|
|||||||
self.password = data[key]
|
self.password = data[key]
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def as_x19(self, user_suffix: str = None):
|
def as_x19(self, user_suffix: str = None) -> dict:
|
||||||
"""Convert the data in this class to a dict usable by an X19 device.
|
"""Convert the data in this class to a dict usable by an X19 device.
|
||||||
|
|
||||||
:param user_suffix: The suffix to append to username.
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
username = self.username
|
username = self.username
|
||||||
if user_suffix:
|
if user_suffix:
|
||||||
@@ -48,7 +51,12 @@ class _Pool:
|
|||||||
pool = {"url": self.url, "user": username, "pass": self.password}
|
pool = {"url": self.url, "user": username, "pass": self.password}
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
def as_avalon(self, user_suffix: str = None):
|
def as_avalon(self, user_suffix: str = None) -> str:
|
||||||
|
"""Convert the data in this class to a string usable by an Avalonminer device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
username = self.username
|
username = self.username
|
||||||
if user_suffix:
|
if user_suffix:
|
||||||
username = f"{username}{user_suffix}"
|
username = f"{username}{user_suffix}"
|
||||||
@@ -56,10 +64,11 @@ class _Pool:
|
|||||||
pool = ",".join([self.url, username, self.password])
|
pool = ",".join([self.url, username, self.password])
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
def as_bos(self, user_suffix: str = None):
|
def as_bos(self, user_suffix: str = None) -> dict:
|
||||||
"""Convert the data in this class to a dict usable by an BOSMiner device.
|
"""Convert the data in this class to a dict usable by an BOSMiner device.
|
||||||
|
|
||||||
:param user_suffix: The suffix to append to username.
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
username = self.username
|
username = self.username
|
||||||
if user_suffix:
|
if user_suffix:
|
||||||
@@ -73,9 +82,10 @@ class _Pool:
|
|||||||
class _PoolGroup:
|
class _PoolGroup:
|
||||||
"""A dataclass for pool group information.
|
"""A dataclass for pool group information.
|
||||||
|
|
||||||
:param quota: The group quota.
|
Attributes:
|
||||||
:param group_name: The name of the pool group.
|
quota: The group quota.
|
||||||
:param pools: A list of pools in this group.
|
group_name: The name of the pool group.
|
||||||
|
pools: A list of pools in this group.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
quota: int = 1
|
quota: int = 1
|
||||||
@@ -91,7 +101,8 @@ class _PoolGroup:
|
|||||||
def from_dict(self, data: dict):
|
def from_dict(self, data: dict):
|
||||||
"""Convert raw pool group data as a dict to usable data and save it to this class.
|
"""Convert raw pool group data as a dict to usable data and save it to this class.
|
||||||
|
|
||||||
:param data: The raw config data to convert.
|
Parameters:
|
||||||
|
data: The raw config data to convert.
|
||||||
"""
|
"""
|
||||||
pools = []
|
pools = []
|
||||||
for key in data.keys():
|
for key in data.keys():
|
||||||
@@ -105,24 +116,31 @@ class _PoolGroup:
|
|||||||
self.pools = pools
|
self.pools = pools
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def as_x19(self, user_suffix: str = None):
|
def as_x19(self, user_suffix: str = None) -> List[dict]:
|
||||||
"""Convert the data in this class to a dict usable by an X19 device.
|
"""Convert the data in this class to a list usable by an X19 device.
|
||||||
|
|
||||||
:param user_suffix: The suffix to append to username.
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
pools = []
|
pools = []
|
||||||
for pool in self.pools[:3]:
|
for pool in self.pools[:3]:
|
||||||
pools.append(pool.as_x19(user_suffix=user_suffix))
|
pools.append(pool.as_x19(user_suffix=user_suffix))
|
||||||
return pools
|
return pools
|
||||||
|
|
||||||
def as_avalon(self, user_suffix: str = None):
|
def as_avalon(self, user_suffix: str = None) -> str:
|
||||||
|
"""Convert the data in this class to a dict usable by an Avalonminer device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
pool = self.pools[0].as_avalon(user_suffix=user_suffix)
|
pool = self.pools[0].as_avalon(user_suffix=user_suffix)
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
def as_bos(self, user_suffix: str = None):
|
def as_bos(self, user_suffix: str = None) -> dict:
|
||||||
"""Convert the data in this class to a dict usable by an BOSMiner device.
|
"""Convert the data in this class to a dict usable by an BOSMiner device.
|
||||||
|
|
||||||
:param user_suffix: The suffix to append to username.
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
group = {
|
group = {
|
||||||
"name": self.group_name,
|
"name": self.group_name,
|
||||||
@@ -136,21 +154,22 @@ class _PoolGroup:
|
|||||||
class MinerConfig:
|
class MinerConfig:
|
||||||
"""A dataclass for miner configuration information.
|
"""A dataclass for miner configuration information.
|
||||||
|
|
||||||
:param pool_groups: A list of pool groups in this config.
|
Attributes:
|
||||||
:param temp_mode: The temperature control mode.
|
pool_groups: A list of pool groups in this config.
|
||||||
:param temp_target: The target temp.
|
temp_mode: The temperature control mode.
|
||||||
:param temp_hot: The hot temp (100% fans).
|
temp_target: The target temp.
|
||||||
:param temp_dangerous: The dangerous temp (shutdown).
|
temp_hot: The hot temp (100% fans).
|
||||||
:param minimum_fans: The minimum numbers of fans to run the miner.
|
temp_dangerous: The dangerous temp (shutdown).
|
||||||
:param fan_speed: Manual fan speed to run the fan at (only if temp_mode == "manual").
|
minimum_fans: The minimum numbers of fans to run the miner.
|
||||||
:param asicboost: Whether or not to enable asicboost.
|
fan_speed: Manual fan speed to run the fan at (only if temp_mode == "manual").
|
||||||
:param autotuning_enabled: Whether or not to enable autotuning.
|
asicboost: Whether or not to enable asicboost.
|
||||||
:param autotuning_wattage: The wattage to use when autotuning.
|
autotuning_enabled: Whether or not to enable autotuning.
|
||||||
:param dps_enabled: Whether or not to enable dynamic power scaling.
|
autotuning_wattage: The wattage to use when autotuning.
|
||||||
:param dps_power_step: The amount of power to reduce autotuning by when the miner reaches dangerous temp.
|
dps_enabled: Whether or not to enable dynamic power scaling.
|
||||||
:param dps_min_power: The minimum power to reduce autotuning to.
|
dps_power_step: The amount of power to reduce autotuning by when the miner reaches dangerous temp.
|
||||||
:param dps_shutdown_enabled: Whether or not to shutdown the miner when `dps_min_power` is reached.
|
dps_min_power: The minimum power to reduce autotuning to.
|
||||||
:param dps_shutdown_duration: The amount of time to shutdown for (in hours).
|
dps_shutdown_enabled: Whether or not to shutdown the miner when `dps_min_power` is reached.
|
||||||
|
dps_shutdown_duration: The amount of time to shutdown for (in hours).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pool_groups: List[_PoolGroup] = None
|
pool_groups: List[_PoolGroup] = None
|
||||||
@@ -174,27 +193,28 @@ class MinerConfig:
|
|||||||
dps_shutdown_enabled: bool = None
|
dps_shutdown_enabled: bool = None
|
||||||
dps_shutdown_duration: float = None
|
dps_shutdown_duration: float = None
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self) -> dict:
|
||||||
"""Convert the data in this class to a dict."""
|
"""Convert the data in this class to a dict."""
|
||||||
|
|
||||||
data_dict = asdict(self)
|
data_dict = asdict(self)
|
||||||
for key in asdict(self).keys():
|
for key in asdict(self).keys():
|
||||||
if data_dict[key] is None:
|
if data_dict[key] is None:
|
||||||
del data_dict[key]
|
del data_dict[key]
|
||||||
return data_dict
|
return data_dict
|
||||||
|
|
||||||
def as_toml(self):
|
def as_toml(self) -> str:
|
||||||
"""Convert the data in this class to toml."""
|
"""Convert the data in this class to toml."""
|
||||||
return toml.dumps(self.as_dict())
|
return toml.dumps(self.as_dict())
|
||||||
|
|
||||||
def as_yaml(self):
|
def as_yaml(self) -> str:
|
||||||
"""Convert the data in this class to yaml."""
|
"""Convert the data in this class to yaml."""
|
||||||
return yaml.dump(self.as_dict(), sort_keys=False)
|
return yaml.dump(self.as_dict(), sort_keys=False)
|
||||||
|
|
||||||
def from_raw(self, data: dict):
|
def from_raw(self, data: dict):
|
||||||
"""Convert raw config data as a dict to usable data and save it to this class.
|
"""Convert raw config data as a dict to usable data and save it to this class.
|
||||||
|
This should be able to handle any raw config file from any miner supported by pyasic.
|
||||||
|
|
||||||
:param data: The raw config data to convert.
|
Parameters:
|
||||||
|
data: The raw config data to convert.
|
||||||
"""
|
"""
|
||||||
pool_groups = []
|
pool_groups = []
|
||||||
for key in data.keys():
|
for key in data.keys():
|
||||||
@@ -256,7 +276,8 @@ class MinerConfig:
|
|||||||
def from_dict(self, data: dict):
|
def from_dict(self, data: dict):
|
||||||
"""Convert an output dict of this class back into usable data and save it to this class.
|
"""Convert an output dict of this class back into usable data and save it to this class.
|
||||||
|
|
||||||
:param data: The raw config data to convert.
|
Parameters:
|
||||||
|
data: The dict config data to convert.
|
||||||
"""
|
"""
|
||||||
pool_groups = []
|
pool_groups = []
|
||||||
for group in data["pool_groups"]:
|
for group in data["pool_groups"]:
|
||||||
@@ -270,21 +291,24 @@ class MinerConfig:
|
|||||||
def from_toml(self, data: str):
|
def from_toml(self, data: str):
|
||||||
"""Convert output toml of this class back into usable data and save it to this class.
|
"""Convert output toml of this class back into usable data and save it to this class.
|
||||||
|
|
||||||
:param data: The raw config data to convert.
|
Parameters:
|
||||||
|
data: The toml config data to convert.
|
||||||
"""
|
"""
|
||||||
return self.from_dict(toml.loads(data))
|
return self.from_dict(toml.loads(data))
|
||||||
|
|
||||||
def from_yaml(self, data: str):
|
def from_yaml(self, data: str):
|
||||||
"""Convert output yaml of this class back into usable data and save it to this class.
|
"""Convert output yaml of this class back into usable data and save it to this class.
|
||||||
|
|
||||||
:param data: The raw config data to convert.
|
Parameters:
|
||||||
|
data: The yaml config data to convert.
|
||||||
"""
|
"""
|
||||||
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
|
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
|
||||||
|
|
||||||
def as_x19(self, user_suffix: str = None) -> str:
|
def as_x19(self, user_suffix: str = None) -> str:
|
||||||
"""Convert the data in this class to a config usable by an X19 device.
|
"""Convert the data in this class to a config usable by an X19 device.
|
||||||
|
|
||||||
:param user_suffix: The suffix to append to username.
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
cfg = {
|
cfg = {
|
||||||
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
|
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
|
||||||
@@ -301,14 +325,20 @@ class MinerConfig:
|
|||||||
return json.dumps(cfg)
|
return json.dumps(cfg)
|
||||||
|
|
||||||
def as_avalon(self, user_suffix: str = None) -> str:
|
def as_avalon(self, user_suffix: str = None) -> str:
|
||||||
cfg = self.pool_groups[0].as_avalon()
|
"""Convert the data in this class to a config usable by an Avalonminer device.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
|
"""
|
||||||
|
cfg = self.pool_groups[0].as_avalon(user_suffix=user_suffix)
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
def as_bos(self, model: str = "S9", user_suffix: str = None) -> str:
|
def as_bos(self, model: str = "S9", user_suffix: str = None) -> str:
|
||||||
"""Convert the data in this class to a config usable by an BOSMiner device.
|
"""Convert the data in this class to a config usable by an BOSMiner device.
|
||||||
|
|
||||||
:param model: The model of the miner to be used in the format portion of the config.
|
Parameters:
|
||||||
:param user_suffix: The suffix to append to username.
|
model: The model of the miner to be used in the format portion of the config.
|
||||||
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
cfg = {
|
cfg = {
|
||||||
"format": {
|
"format": {
|
||||||
|
|||||||
Reference in New Issue
Block a user