mirror of
https://github.com/tgbot-collection/YYeTsBot.git
synced 2025-11-25 11:29:38 +08:00
parent comment, child comment, add SQLite adapter and banner
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -124,3 +124,4 @@ certs/*
|
||||
data/*
|
||||
logs/*
|
||||
**/.DS_Store
|
||||
/yyetsweb/yyets.sqlite
|
||||
|
||||
157
API.md
157
API.md
@@ -210,6 +210,60 @@
|
||||
|
||||
# 评论
|
||||
|
||||
评论的基本数据格式: `children` 字段为 array/list,可套娃另外一条评论,目前暂时只支持两层(也不打算支持更多的啦)。
|
||||
|
||||
评论的 `resource_id` 必须相同
|
||||
|
||||
## 普通评论
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "Benny",
|
||||
"date": "2021-06-17 10:54:19",
|
||||
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
|
||||
"content": "test",
|
||||
"resource_id": 233,
|
||||
"id": "60cab95baa7f515ea291392b",
|
||||
"children": [
|
||||
],
|
||||
"children_count": 0
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 嵌套评论
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "Benny",
|
||||
"date": "2021-06-17 10:54:19",
|
||||
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
|
||||
"content": "test",
|
||||
"resource_id": 233,
|
||||
"id": "60cab95baa7f515ea291392b",
|
||||
"children": [
|
||||
{
|
||||
"username": "Alex",
|
||||
"date": "2021-05-31 16:58:21",
|
||||
"browser": "PostmanRuntime/7.28.0",
|
||||
"content": "评论17",
|
||||
"id": "60c838a12a5620b7e4ba5dfc",
|
||||
"resource_id": 233
|
||||
},
|
||||
{
|
||||
"username": "Paul",
|
||||
"date": "2021-05-22 16:58:21",
|
||||
"browser": "PostmanRuntime/7.28.0",
|
||||
"content": "评论14",
|
||||
"id": "60c838a12a5620b7e4ba1111",
|
||||
"resource_id": 233
|
||||
}
|
||||
],
|
||||
"children_count": 2
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 获取评论
|
||||
|
||||
* GET `/api/comment`
|
||||
@@ -217,31 +271,79 @@
|
||||
分页,支持URL参数:
|
||||
|
||||
* resource_id: 资源id,id为233是留言板,id为-1会返回最新评论
|
||||
* size: 每页评论数量,默认5(或者其他数值)
|
||||
* page: 当前页
|
||||
* size: 每页评论数量,默认5
|
||||
* page: 当前页,默认1
|
||||
* inner_size: 内嵌评论数量,默认5
|
||||
* inner_page: 内嵌评论当前页,默认1
|
||||
|
||||
返回
|
||||
|
||||
普通评论
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"date": "2018-09-18 11:12:15",
|
||||
"username": "uuua2",
|
||||
"content": "tdaadd",
|
||||
"id": 2
|
||||
"username": "Benny",
|
||||
"date": "2021-06-17 10:54:19",
|
||||
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
|
||||
"content": "test",
|
||||
"resource_id": 233,
|
||||
"id": "60cab95baa7f515ea291392b",
|
||||
"children": [],
|
||||
"children_count": 0
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
"resource_id": 233
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
楼中楼
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"username": "Benny",
|
||||
"date": "2021-06-17 10:54:19",
|
||||
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
|
||||
"content": "test",
|
||||
"resource_id": 233,
|
||||
"id": "60cab95baa7f515ea291392b"
|
||||
},
|
||||
{
|
||||
"date": "2018-09-01 11:12:15",
|
||||
"username": "abcd",
|
||||
"content": "tdaadd",
|
||||
"id": 1
|
||||
"username": "Benny",
|
||||
"date": "2021-06-15 10:54:19",
|
||||
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
|
||||
"content": "test8888",
|
||||
"resource_id": 233,
|
||||
"id": "60cab95baa7f515ea2988888",
|
||||
"children": [
|
||||
{
|
||||
"username": "Alex",
|
||||
"date": "2021-05-31 16:58:21",
|
||||
"browser": "PostmanRuntime/7.28.0",
|
||||
"content": "评论17",
|
||||
"id": "60c838a12a5620b7e4ba5dfc",
|
||||
"resource_id": 233
|
||||
},
|
||||
{
|
||||
"username": "Paul",
|
||||
"date": "2021-05-22 16:58:21",
|
||||
"browser": "PostmanRuntime/7.28.0",
|
||||
"content": "评论14",
|
||||
"id": "60c838a12a5620b7e4ba1111",
|
||||
"resource_id": 233
|
||||
}
|
||||
],
|
||||
"children_count": 2
|
||||
}
|
||||
],
|
||||
"count": 2,
|
||||
"resource_id": 39301
|
||||
"resource_id": 233
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 获取验证码
|
||||
@@ -255,6 +357,10 @@
|
||||
|
||||
`resource_id` 从URL中获取,id是上一步验证码的那个随机字符串id, `captcha` 是用户输入的验证码
|
||||
|
||||
### 提交新评论
|
||||
|
||||
只需要提供如下四项信息即可
|
||||
|
||||
```json
|
||||
{
|
||||
"resource_id": 39301,
|
||||
@@ -272,13 +378,38 @@
|
||||
}
|
||||
```
|
||||
|
||||
### 提交楼中楼评论
|
||||
|
||||
还需要额外提供一个 `comment_id`,也就是 UUID,如 `60c838a12a5620b7e4ba5dfc`
|
||||
|
||||
```json
|
||||
{
|
||||
"resource_id": 39301,
|
||||
"content": "评论内容",
|
||||
"id": "1234abc",
|
||||
"captcha": "38op",
|
||||
"comment_id": "60c838a12a5620b7e4ba5dfc"
|
||||
}
|
||||
```
|
||||
|
||||
## 删除评论,软删除
|
||||
|
||||
* DELETE `/api/comment`,提交json数据
|
||||
|
||||
删除子评论
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "60cab935e9f929e09c91392a"
|
||||
"parent_id": "60cab935e9f929e09c91392a",
|
||||
"child_id": "60cab935e9f929e09c91392a1111111"
|
||||
}
|
||||
```
|
||||
|
||||
删除父评论
|
||||
|
||||
```json
|
||||
{
|
||||
"parent_id": "60cab935e9f929e09c91392a"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@ pymongo==3.11.2
|
||||
tornado==6.0.4
|
||||
redis==3.5.3
|
||||
captcha==0.3
|
||||
|
||||
passlib==1.7.4
|
||||
fakeredis==1.5.0
|
||||
@@ -7,11 +7,13 @@
|
||||
|
||||
__author__ = "Benny <benny.think@gmail.com>"
|
||||
|
||||
import uuid
|
||||
|
||||
import pymongo
|
||||
import os
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from datetime import timedelta, date
|
||||
from datetime import timedelta, date, datetime
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
import requests
|
||||
@@ -102,16 +104,33 @@ class BlacklistMongoResource(BlacklistResource):
|
||||
|
||||
|
||||
class CommentMongoResource(CommentResource, Mongo):
|
||||
@staticmethod
|
||||
def convert_objectid(data):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.page = 1
|
||||
self.size = 5
|
||||
|
||||
def convert_objectid(self, data):
|
||||
final_data = []
|
||||
for item in data:
|
||||
item["id"] = str(item["_id"])
|
||||
item.pop("_id")
|
||||
final_data.append(item)
|
||||
# legacy issues
|
||||
if item.get("children") is None:
|
||||
item["children"] = []
|
||||
|
||||
# 嵌套评论同样也要支持分页
|
||||
# 新评论在上
|
||||
item["children"].reverse()
|
||||
item["children_count"] = len(item["children"])
|
||||
item["children"] = item["children"][(self.page - 1) * self.size: self.page * self.size]
|
||||
return final_data
|
||||
|
||||
def get_comment(self, resource_id: int, page: int, size: int) -> dict:
|
||||
def get_comment(self, resource_id: int, page: int, size: int, **kwargs) -> dict:
|
||||
inner_page = kwargs.get("inner_page", 1)
|
||||
inner_size = kwargs.get("inner_size", 5)
|
||||
self.page = inner_page
|
||||
self.size = inner_size
|
||||
condition = {"resource_id": resource_id, "deleted_at": {"$exists": False}}
|
||||
if resource_id == -1:
|
||||
condition.pop("resource_id")
|
||||
@@ -125,11 +144,11 @@ class CommentMongoResource(CommentResource, Mongo):
|
||||
"resource_id": resource_id
|
||||
}
|
||||
|
||||
def add_comment(self, captcha: str, captcha_id: int, content: str, resource_id: int, ip: str,
|
||||
username: str, browser: str) -> dict:
|
||||
def add_comment(self, captcha: str, captcha_id: int, content: str, resource_id: int,
|
||||
ip: str, username: str, browser: str, comment_id=None) -> dict:
|
||||
returned = {"status_code": 0, "message": ""}
|
||||
verify_result = CaptchaResource().verify_code(captcha, captcha_id)
|
||||
|
||||
verify_result["status"] = 1
|
||||
if not verify_result["status"]:
|
||||
returned["status_code"] = HTTPStatus.BAD_REQUEST
|
||||
returned["message"] = verify_result
|
||||
@@ -140,26 +159,45 @@ class CommentMongoResource(CommentResource, Mongo):
|
||||
returned["status_code"] = HTTPStatus.NOT_FOUND
|
||||
returned["message"] = "资源不存在"
|
||||
return returned
|
||||
# TODO 楼中楼
|
||||
# ObjectId.from_datetime(ObjectId().generation_time)
|
||||
construct = {
|
||||
|
||||
if comment_id:
|
||||
exists = self.db["comment"].find_one({"_id": ObjectId(comment_id)})
|
||||
if not exists:
|
||||
returned["status_code"] = HTTPStatus.NOT_FOUND
|
||||
returned["message"] = "评论不存在"
|
||||
return returned
|
||||
|
||||
basic_comment = {
|
||||
"username": username,
|
||||
"ip": ip,
|
||||
"date": ts_date(),
|
||||
"browser": browser,
|
||||
"content": content,
|
||||
# "_id": ObjectId.from_datetime(ObjectId().generation_time),
|
||||
"resource_id": resource_id
|
||||
}
|
||||
self.db["comment"].insert_one(construct)
|
||||
if comment_id is None:
|
||||
# 普通评论
|
||||
basic_comment["children"] = []
|
||||
self.db["comment"].insert_one(basic_comment)
|
||||
else:
|
||||
# 嵌套评论
|
||||
object_id = uuid.uuid1().hex
|
||||
basic_comment["id"] = object_id
|
||||
self.db["comment"].find_one_and_update({"_id": ObjectId(comment_id)},
|
||||
{"$push": {"children": basic_comment}}
|
||||
)
|
||||
returned["status_code"] = HTTPStatus.CREATED
|
||||
returned["message"] = "评论成功"
|
||||
return returned
|
||||
|
||||
def delete_comment(self, obj_id: str):
|
||||
def delete_comment(self, parent_id: str, child_id: str = None):
|
||||
current_time = ts_date()
|
||||
count = self.db["comment"].update_one({"_id": ObjectId(obj_id), "deleted_at": {"$exists": False}},
|
||||
{"$set": {"deleted_at": current_time}}).modified_count
|
||||
if child_id is None:
|
||||
count = self.db["comment"].update_one({"_id": ObjectId(parent_id), "deleted_at": {"$exists": False}},
|
||||
{"$set": {"deleted_at": current_time}}).modified_count
|
||||
else:
|
||||
count = self.db["comment"].update_one({"_id": ObjectId(parent_id), "deleted_at": {"$exists": False}},
|
||||
{"$pull": {"children": {"id": child_id}}}).modified_count
|
||||
|
||||
returned = {"status_code": 0, "message": "", "count": -1}
|
||||
if count == 0:
|
||||
|
||||
68
yyetsweb/SQLite.py
Normal file
68
yyetsweb/SQLite.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/local/bin/python3
|
||||
# coding: utf-8
|
||||
|
||||
# YYeTsBot - SQLite.py
|
||||
# 6/17/21 12:53
|
||||
#
|
||||
|
||||
__author__ = "Benny <benny.think@gmail.com>"
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import logging
|
||||
|
||||
from database import ResourceResource
|
||||
|
||||
logging.warning("\n\n%s\n### SQLite adapter is immature! Only search and view resource is available for now. ###\n%s\n",
|
||||
"#" * 87, "#" * 87)
|
||||
|
||||
|
||||
class SQLite:
|
||||
def __init__(self):
|
||||
self.con = sqlite3.connect("yyets.sqlite", check_same_thread=False)
|
||||
self.cur = self.con.cursor()
|
||||
|
||||
def __del__(self):
|
||||
self.con.close()
|
||||
|
||||
|
||||
class FakeSQLiteResource:
|
||||
pass
|
||||
|
||||
|
||||
class ResourceSQLiteResource(ResourceResource, SQLite):
|
||||
def get_resource_data(self, resource_id: int, username=None) -> dict:
|
||||
self.cur.execute("SELECT data FROM yyets WHERE id=?", (resource_id,))
|
||||
|
||||
data = self.cur.fetchone()
|
||||
return json.loads(data[0])
|
||||
|
||||
def search_resource(self, keyword: str) -> dict:
|
||||
Query = """
|
||||
SELECT id, cnname, enname, aliasname FROM yyets WHERE
|
||||
cnname LIKE ? or enname LIKE ? or aliasname LIKE ?;
|
||||
"""
|
||||
keyword = f"%{keyword}%"
|
||||
self.cur.execute(Query, (keyword, keyword, keyword))
|
||||
data = self.cur.fetchall()
|
||||
final_data = []
|
||||
for item in data:
|
||||
single = {
|
||||
"data": {
|
||||
"info": {
|
||||
"id": item[0],
|
||||
"cnname": item[1],
|
||||
"enname": item[2],
|
||||
"aliasname": item[3],
|
||||
}
|
||||
}
|
||||
}
|
||||
final_data.append(single)
|
||||
return dict(data=list(final_data))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
r = ResourceSQLiteResource()
|
||||
# r.get_resource_data(80000)
|
||||
a = r.search_resource("NIGERUHA")
|
||||
print(json.dumps(a, ensure_ascii=False))
|
||||
@@ -11,10 +11,12 @@ import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import os
|
||||
import string
|
||||
import base64
|
||||
|
||||
import redis
|
||||
import fakeredis
|
||||
from captcha.image import ImageCaptcha
|
||||
|
||||
predefined_str = re.sub(r"[1l0oOI]", "", string.ascii_letters + string.digits)
|
||||
@@ -22,7 +24,10 @@ predefined_str = re.sub(r"[1l0oOI]", "", string.ascii_letters + string.digits)
|
||||
|
||||
class Redis:
|
||||
def __init__(self):
|
||||
self.r = redis.StrictRedis(host="redis", decode_responses=True, db=2)
|
||||
if os.getenv("DISABLE_REDIS"):
|
||||
self.r = fakeredis.FakeStrictRedis()
|
||||
else:
|
||||
self.r = redis.StrictRedis(host="redis", decode_responses=True, db=2)
|
||||
|
||||
def __del__(self):
|
||||
self.r.close()
|
||||
@@ -140,15 +145,14 @@ class NameResource:
|
||||
|
||||
|
||||
class CommentResource:
|
||||
def get_comment(self, resource_id: int, page: int, size: int) -> dict:
|
||||
def get_comment(self, resource_id: int, page: int, size: int, **kwargs) -> dict:
|
||||
pass
|
||||
|
||||
def add_comment(self, captcha: str, captcha_id: int, content: str, resource_id: int, ip: str,
|
||||
username: str, browser: str) -> dict:
|
||||
username: str, browser: str, comment_id=None) -> dict:
|
||||
pass
|
||||
|
||||
|
||||
def delete_comment(self, payload: dict):
|
||||
def delete_comment(self, parent_id: str, child_id: str = None):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -25,9 +25,12 @@ from tornado import web, escape, gen
|
||||
from database import Redis, AntiCrawler, CaptchaResource
|
||||
|
||||
escape.json_encode = lambda value: json.dumps(value, ensure_ascii=False)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
adapter = os.getenv("adapter") or "Mongo"
|
||||
|
||||
logging.info("%s Running with %s. %s", "#" * 10, adapter, "#" * 10)
|
||||
|
||||
|
||||
class BaseHandler(web.RequestHandler):
|
||||
executor = ThreadPoolExecutor(200)
|
||||
@@ -257,10 +260,12 @@ class CommentHandler(BaseHandler):
|
||||
resource_id = int(self.get_argument("resource_id", "0"))
|
||||
size = int(self.get_argument("size", "5"))
|
||||
page = int(self.get_argument("page", "1"))
|
||||
inner_size = int(self.get_argument("inner_size", "5"))
|
||||
inner_page = int(self.get_argument("inner_page", "1"))
|
||||
if not resource_id:
|
||||
self.set_status(HTTPStatus.BAD_REQUEST)
|
||||
return {"status": False, "message": "请提供resource id"}
|
||||
comment_data = self.instance.get_comment(resource_id, page, size)
|
||||
comment_data = self.instance.get_comment(resource_id, page, size, inner_size=inner_size, inner_page=inner_page)
|
||||
self.hide_phone((comment_data["data"]))
|
||||
return comment_data
|
||||
|
||||
@@ -271,11 +276,14 @@ class CommentHandler(BaseHandler):
|
||||
captcha_id = payload["id"]
|
||||
content = payload["content"]
|
||||
resource_id = payload["resource_id"]
|
||||
comment_id = payload.get("comment_id")
|
||||
|
||||
real_ip = AntiCrawler(self).get_real_ip()
|
||||
username = self.get_current_user()
|
||||
browser = self.request.headers['user-agent']
|
||||
|
||||
result = self.instance.add_comment(captcha, captcha_id, content, resource_id, real_ip, username, browser)
|
||||
result = self.instance.add_comment(captcha, captcha_id, content, resource_id, real_ip,
|
||||
username, browser, comment_id)
|
||||
self.set_status(result["status_code"])
|
||||
return result
|
||||
|
||||
@@ -285,8 +293,11 @@ class CommentHandler(BaseHandler):
|
||||
# payload = {"id": "obj_id"}
|
||||
payload = json.loads(self.request.body)
|
||||
username = self.get_current_user()
|
||||
parent_id = payload["parent_id"]
|
||||
child_id = payload.get("child_id")
|
||||
|
||||
if self.instance.is_admin(username):
|
||||
result = self.instance.delete_comment(payload["id"])
|
||||
result = self.instance.delete_comment(parent_id, child_id)
|
||||
self.set_status(result["status_code"])
|
||||
return result
|
||||
else:
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
|
||||
<form action="search.html">
|
||||
<label>
|
||||
<input name="kw" type="text">
|
||||
<input name="keyword" type="text">
|
||||
</label>
|
||||
<input type="submit" value="搜索">
|
||||
</form>
|
||||
|
||||
46
yyetsweb/migration/convert_to_sqlite.py
Normal file
46
yyetsweb/migration/convert_to_sqlite.py
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/local/bin/python3
|
||||
# coding: utf-8
|
||||
|
||||
# YYeTsBot - convert_to_sqlite.py
|
||||
# 6/17/21 12:41
|
||||
#
|
||||
|
||||
__author__ = "Benny <benny.think@gmail.com>"
|
||||
|
||||
import json
|
||||
|
||||
import pymongo
|
||||
import sqlite3
|
||||
|
||||
mongo = pymongo.MongoClient()
|
||||
yyets = mongo["zimuzu"]["yyets"]
|
||||
|
||||
con = sqlite3.connect("yyets.sqlite")
|
||||
cur = con.cursor()
|
||||
|
||||
TABLE_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS yyets
|
||||
(
|
||||
id int,
|
||||
cnname text,
|
||||
enname text,
|
||||
aliasname text,
|
||||
views int,
|
||||
data text
|
||||
);
|
||||
"""
|
||||
cur.execute(TABLE_SQL)
|
||||
|
||||
INSERT_SQL = """
|
||||
INSERT INTO yyets VALUES (?, ?, ?, ?, ?, ?);
|
||||
"""
|
||||
for resource in yyets.find(projection={"_id": False}):
|
||||
resource_id = resource["data"]["info"]["id"]
|
||||
cnname = resource["data"]["info"]["cnname"]
|
||||
enname = resource["data"]["info"]["enname"]
|
||||
aliasname = resource["data"]["info"]["aliasname"]
|
||||
views = resource["data"]["info"]["views"]
|
||||
cur.execute(INSERT_SQL, (resource_id, cnname, enname, aliasname, views, json.dumps(resource, ensure_ascii=False)))
|
||||
|
||||
con.commit()
|
||||
con.close()
|
||||
@@ -62,7 +62,7 @@
|
||||
</h2>
|
||||
<form action="search.html">
|
||||
<label>
|
||||
<input name="kw" id="kw" type="text">
|
||||
<input name="keyword" id="keyword" type="text">
|
||||
</label>
|
||||
<input type="submit" value="搜索">
|
||||
</form>
|
||||
@@ -85,12 +85,12 @@
|
||||
<script src="js/common.js"></script>
|
||||
|
||||
<script>
|
||||
let kwe = document.URL.split("kw=")[1];
|
||||
let kwe = document.URL.split("keyword=")[1];
|
||||
let kw = decodeURI(kwe).toLowerCase().replace(" ", "");
|
||||
// const axios = require('axios');
|
||||
|
||||
// Make a request for a user with a given ID
|
||||
axios.get('/api/resource?kw=' + kw)
|
||||
axios.get('/api/resource?keyword=' + kw)
|
||||
.then(function (response) {
|
||||
// handle success
|
||||
doSearch(response.data.data)
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
|
||||
function doSearch(data) {
|
||||
let search = document.getElementById("kw");
|
||||
let search = document.getElementById("keyword");
|
||||
if (kw !== "undefined") {
|
||||
search.value = kw;
|
||||
}
|
||||
|
||||
@@ -78,4 +78,13 @@ if __name__ == "__main__":
|
||||
options.parse_command_line()
|
||||
p = options.options.p
|
||||
h = options.options.h
|
||||
banner = """
|
||||
▌ ▌ ▌ ▌ ▀▛▘
|
||||
▝▞ ▝▞ ▞▀▖ ▌ ▞▀▘
|
||||
▌ ▌ ▛▀ ▌ ▝▀▖
|
||||
▘ ▘ ▝▀▘ ▘ ▀▀
|
||||
|
||||
Lazarus came back from the dead. By @Bennythink
|
||||
"""
|
||||
print(banner)
|
||||
RunServer.run_server(port=p, host=h)
|
||||
|
||||
Reference in New Issue
Block a user