Initial commit
This commit is contained in:
commit
733c02cc85
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.ignore
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"autoDocstring.docstringFormat": "sphinx"
|
||||
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Rokas Puzonas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# MineHost Interface
|
||||
|
||||
A python library for executing commands or gettings information on MineHost minecraft servers.
|
3
minehost/__init__.py
Normal file
3
minehost/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .server import MCServer
|
||||
from .account import Account
|
||||
from .session import Session, IncorrectLoginException, InvalidSessionException
|
114
minehost/account.py
Normal file
114
minehost/account.py
Normal file
@ -0,0 +1,114 @@
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from .server import MCServer
|
||||
from .session import Session
|
||||
|
||||
# The changing of the password and profile info, are intentionally not implemented.
|
||||
# It's just too much power
|
||||
|
||||
datetime_format = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
class Account:
|
||||
def __init__(self, username: str = None, password: str = None, session: Session = None):
|
||||
self.username = username
|
||||
if username is not None and password is not None:
|
||||
self.session = Session(username, password)
|
||||
elif session is not None:
|
||||
self.session = session
|
||||
else:
|
||||
raise Exception("Session or logins must be given")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Account({self.username or self._cookies.get('PHPSESSID', 'NONE')})>"
|
||||
|
||||
def getServers(self):
|
||||
control_res = self.session.get("/mano-serveriai")
|
||||
servers = []
|
||||
for server_id in re.findall("/minecraft-serverio-valdymas/(\d*)/", control_res.text):
|
||||
servers.append(MCServer(server_id, self.session))
|
||||
return servers
|
||||
|
||||
def getServer(self, i: int = 0):
|
||||
return self.getServers()[i]
|
||||
|
||||
def getBalance(self):
|
||||
balance_res = self.session.get("/balanso-pildymas")
|
||||
balance_match = re.search(r"balanse yra (\d+\.?\d*)", balance_res.text)
|
||||
return float(balance_match.group(1))
|
||||
|
||||
def getProfileInfo(self):
|
||||
profile_res = self.session.get("/profilio-nustatymai")
|
||||
soup = BeautifulSoup(profile_res.text, "lxml")
|
||||
return {
|
||||
"email": re.search("Vartotojas: ([\w\.-]+@[\w\.-]+)", profile_res.text).group(1),
|
||||
"name": soup.find("input", id="v1")["value"],
|
||||
"surname": soup.find("input", id="v2")["value"],
|
||||
"phone": soup.find("input", id="v4")["value"],
|
||||
"skype": soup.find("input", id="v5")["value"]
|
||||
}
|
||||
|
||||
def getLoginHistory(self, limit: int = 10):
|
||||
history_res = self.session.get("/istorija/prisijungimu-istorija")
|
||||
soup = BeautifulSoup(history_res.text, "lxml")
|
||||
history = []
|
||||
|
||||
for entry in soup.find("table").children:
|
||||
if type(entry) != Tag: continue
|
||||
|
||||
fields = entry.findAll("td", align="center")
|
||||
if len(fields) != 3: continue
|
||||
|
||||
history.append({
|
||||
"date": datetime.strptime(fields[1].text, datetime_format),
|
||||
"ip": fields[2].text
|
||||
})
|
||||
if len(history) == limit: break
|
||||
|
||||
return history
|
||||
|
||||
def getFinanceHistory(self, limit: int = 10):
|
||||
history_res = self.session.get("/balanso-pildymas/ataskaita")
|
||||
soup = BeautifulSoup(history_res.text, "lxml")
|
||||
history = []
|
||||
|
||||
for entry in soup.find("table").children:
|
||||
if type(entry) != Tag: continue
|
||||
|
||||
fields = entry.findAll("td")
|
||||
if len(fields) != 4: continue
|
||||
|
||||
history.append({
|
||||
"date": datetime.strptime(fields[0].text, datetime_format),
|
||||
"action": fields[1].text,
|
||||
"balance_change": float(fields[2].text[:-4]),
|
||||
"balance_remainder": float(fields[3].text[:-3]),
|
||||
})
|
||||
if len(history) == limit: break
|
||||
|
||||
return history
|
||||
|
||||
def getProfileInfoHistory(self, limit: int = 10):
|
||||
history_res = self.session.get("/profilio-nustatymai")
|
||||
soup = BeautifulSoup(history_res.text, "lxml")
|
||||
history = []
|
||||
|
||||
for entry in soup.find("table").children:
|
||||
if type(entry) != Tag: continue
|
||||
|
||||
fields = entry.findAll("td")
|
||||
if len(fields) != 6: continue
|
||||
|
||||
history.append({
|
||||
"date": datetime.strptime(fields[0].text, datetime_format),
|
||||
"name": fields[1].text,
|
||||
"surname": fields[2].text,
|
||||
"phone": fields[3].text,
|
||||
"skype": fields[4].text,
|
||||
"ip": fields[5].text
|
||||
})
|
||||
if len(history) == limit: break
|
||||
|
||||
return history
|
||||
|
224
minehost/server.py
Normal file
224
minehost/server.py
Normal file
@ -0,0 +1,224 @@
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
import re
|
||||
from ftplib import FTP
|
||||
from datetime import datetime
|
||||
import paramiko
|
||||
import time
|
||||
|
||||
datetime_format = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
class InvalidDomainException(Exception):
|
||||
pass
|
||||
|
||||
# TODO: Make the sending of commands not need to sleep inbetween, If It dosen't sleep then it dosen'y always send the commands to the server
|
||||
class CommandSender:
|
||||
def __init__(self, host, username, password, port=22):
|
||||
self.ssh = paramiko.SSHClient()
|
||||
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
self.ssh.connect(host, port=port, username="console", password=password)
|
||||
self.channel = None
|
||||
|
||||
def __enter__(self):
|
||||
if self.channel is None:
|
||||
self.open()
|
||||
return self
|
||||
|
||||
def __exit__(self, _1, _2, _3):
|
||||
if self.channel is not None:
|
||||
self.close()
|
||||
|
||||
def __del__(self):
|
||||
self.ssh.close()
|
||||
|
||||
def send(self, *args):
|
||||
if self.channel is None:
|
||||
raise Exception("Channel is not open")
|
||||
|
||||
self.channel.sendall("\n".join(args)+"\n")
|
||||
time.sleep(0.5)
|
||||
|
||||
def open(self):
|
||||
if self.channel is not None:
|
||||
raise Exception("Channel is already open")
|
||||
|
||||
self.channel = self.ssh.invoke_shell()
|
||||
self.channel.sendall("console\n")
|
||||
time.sleep(0.5)
|
||||
|
||||
def close(self):
|
||||
if self.channel is None:
|
||||
raise Exception("Channel is not open")
|
||||
|
||||
self.channel.close()
|
||||
self.channel = None
|
||||
|
||||
class MCServer:
|
||||
def __init__(self, server_id, session):
|
||||
self.id = server_id
|
||||
self.session = session
|
||||
|
||||
info_res = self.session.get(self.__url)
|
||||
soup = BeautifulSoup(info_res.text, "lxml")
|
||||
|
||||
info_rows = soup.select("table td[align=right] b")
|
||||
self.address = info_rows[6].text.split(":")[0]
|
||||
self.ip, self.port = info_rows[7].text.split(":")
|
||||
self.port = int(self.port)
|
||||
|
||||
control_res = self.session.get(f"{self.__url}/valdymas")
|
||||
soup = BeautifulSoup(control_res.text, "lxml")
|
||||
self.__key = soup.find("input", id="khd")["value"]
|
||||
|
||||
def __repr__(self):
|
||||
return f"<MinecraftServer({self.short_address})>"
|
||||
|
||||
@property
|
||||
def __url(self):
|
||||
return f"/minecraft-serverio-valdymas/{self.id}"
|
||||
|
||||
def getPassword(self):
|
||||
res = self.session.get(f"{self.__url}/failai")
|
||||
soup = BeautifulSoup(res.text, "lxml")
|
||||
return soup.select("table td span:nth-child(2)")[0].text
|
||||
|
||||
def changePassword(self):
|
||||
res = self.session.get(f"{self.__url}/failai/change-password")
|
||||
return res.text.find("class=\"alert alert-info\"") > 0
|
||||
|
||||
def getInfo(self):
|
||||
res = self.session.get(self.__url)
|
||||
soup = BeautifulSoup(res.text, "lxml")
|
||||
|
||||
version = soup.find(id="mc_versija").text
|
||||
version = version != "-" and version or None
|
||||
|
||||
ping = soup.find(id="mc_ping").text
|
||||
ping = ping != "-" and int(ping) or None
|
||||
|
||||
online_players = soup.find(id="mc_online").text
|
||||
online_players = online_players != "-" and int(online_players) or None
|
||||
|
||||
max_players = soup.find(id="mc_zaidejai").text
|
||||
max_players = max_players != "-" and int(max_players) or None
|
||||
|
||||
return {
|
||||
"state": soup.find(id="mc_status").text,
|
||||
"version": version,
|
||||
"ping": ping,
|
||||
"online_players": online_players,
|
||||
"max_players": max_players
|
||||
}
|
||||
|
||||
def getExpirationDate(self):
|
||||
res = self.session.get(f"{self.__url}/finansai")
|
||||
date_match = re.search(r"Serveris galioja iki: (\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}:\d{2})", res.text)
|
||||
return datetime.strptime(date_match.group(1), datetime_format)
|
||||
|
||||
@property
|
||||
def start_file(self):
|
||||
response = self.session.get(f"{self.__url}/paleidimo-failas")
|
||||
soup = BeautifulSoup(response.text, "lxml")
|
||||
return soup.find("input", id="startlinein").text
|
||||
|
||||
@start_file.setter
|
||||
def start_file(self, start_file: str):
|
||||
self.session.post(f"{self.__url}/mc-versijos", data={
|
||||
"startfile": str(start_file)
|
||||
})
|
||||
|
||||
@property
|
||||
def ssh_port(self):
|
||||
return self.port+1
|
||||
|
||||
@property
|
||||
def sftp_port(self):
|
||||
return self.port+1
|
||||
|
||||
@property
|
||||
def ftp_port(self):
|
||||
return self.port+3
|
||||
|
||||
@property
|
||||
def short_address(self):
|
||||
info_res = self.session.get(self.__url)
|
||||
soup = BeautifulSoup(info_res.text, "lxml")
|
||||
return soup.find(id="shortaddr").text
|
||||
|
||||
@short_address.setter
|
||||
def short_address(self, short_address: str):
|
||||
domains = (".minehost.lt", ".hotmc.eu")
|
||||
subdomain, domain_index = None, None
|
||||
|
||||
for domain in domains:
|
||||
found_index = short_address.find(domain)
|
||||
if found_index == len(short_address)-len(domain):
|
||||
domain_index = domains.index(domain)
|
||||
subdomain = short_address[:found_index]
|
||||
break
|
||||
|
||||
if domain_index is None:
|
||||
raise InvalidDomainException()
|
||||
|
||||
res = self.session.get("/query/subdomenas.php", params={
|
||||
"f": self.id,
|
||||
"s": subdomain,
|
||||
"t": domain_index
|
||||
})
|
||||
if res.content.decode("UTF-8") == "Šis subdomenas jau užimtas. Bandykite kitą":
|
||||
raise InvalidDomainException()
|
||||
|
||||
def FTP(self):
|
||||
ftp = FTP()
|
||||
ftp.connect(self.ip, self.ftp_port)
|
||||
ftp.login("serveris", self.getPassword())
|
||||
return ftp
|
||||
|
||||
def SSH(self):
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(self.ip, port=self.ssh_port, username="console", password=self.getPassword())
|
||||
return ssh
|
||||
|
||||
def CommandSender(self):
|
||||
return CommandSender(self.ip, "serveris", self.getPassword(), self.ssh_port)
|
||||
|
||||
def __isControlLocked(self):
|
||||
res = self.session.post("/query/user_loadingct.php", data={
|
||||
"k": self.__key
|
||||
})
|
||||
return res.text != ""
|
||||
|
||||
def start(self):
|
||||
if self.__isControlLocked():
|
||||
return False
|
||||
|
||||
self.session.post(f"/query/server_ct.php", data={
|
||||
"k": self.__key,
|
||||
"c": "startmc",
|
||||
"st": "mc"
|
||||
})
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
if self.__isControlLocked():
|
||||
return False
|
||||
|
||||
self.session.post(f"/query/server_ct.php", data={
|
||||
"k": self.__key,
|
||||
"c": "stopmc",
|
||||
"st": "mc"
|
||||
})
|
||||
return True
|
||||
|
||||
def kill(self):
|
||||
if self.__isControlLocked():
|
||||
return False
|
||||
|
||||
self.session.post(f"/query/server_ct.php", data={
|
||||
"k": self.__key,
|
||||
"c": "killrestartmc",
|
||||
"st": "mc"
|
||||
})
|
||||
return True
|
||||
|
42
minehost/session.py
Normal file
42
minehost/session.py
Normal file
@ -0,0 +1,42 @@
|
||||
import requests
|
||||
|
||||
# The "Cookies" functions are used, when you already have dictionary of cookies,
|
||||
# So that the function internally would not need to create one
|
||||
|
||||
class IncorrectLoginException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSessionException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Session(requests.Session):
|
||||
def __init__(self, username: str = None, password: str = None):
|
||||
super().__init__()
|
||||
if username is not None and password is not None:
|
||||
self.login(username, password)
|
||||
|
||||
def login(self, username: str, password: str):
|
||||
requests.Session.request(self, "POST", "https://minehost.lt/prisijungimas-prie-sistemos", data = {
|
||||
"login": username,
|
||||
"password": password
|
||||
})
|
||||
if not self.isValid():
|
||||
raise IncorrectLoginException()
|
||||
|
||||
def logout(self):
|
||||
self.get("/logout")
|
||||
|
||||
def request(self, method, url, *args, **kvargs):
|
||||
response = requests.Session.request(self, method, "https://minehost.lt"+url, *args, **kvargs)
|
||||
# Basic check to see if the session is still logged in. Looks for the logout button.
|
||||
if response.text.find("href=\"/logout\"") == -1 and response.text.find("src=\"/img/logo.png\"") > 0:
|
||||
raise InvalidSessionException()
|
||||
|
||||
return response
|
||||
|
||||
def isValid(self):
|
||||
res = self.get("/valdymo-pultas")
|
||||
return res.text.find("<meta http-equiv=\"refresh\" content=\"0;url=/prisijungimas-prie-sistemos\">") == -1
|
||||
|
Loading…
Reference in New Issue
Block a user