mirror of
https://github.com/whyour/qinglong.git
synced 2025-07-27 14:46:06 +08:00
Merge remote-tracking branch 'origin/master' into dev
This commit is contained in:
commit
e5e8c45e8b
10
.github/workflows/docker.yml
vendored
10
.github/workflows/docker.yml
vendored
|
@ -30,9 +30,11 @@ jobs:
|
|||
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
|
||||
|
||||
- name: Docker buildx image and push on master branch
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
docker buildx build --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/
|
||||
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/
|
||||
|
||||
- name: Replace tag without `v`
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
@ -44,7 +46,9 @@ jobs:
|
|||
result-encoding: string
|
||||
|
||||
- name: Docker buildx image and push on release
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker buildx build --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:${{steps.version.outputs.result}} docker/
|
||||
docker buildx build --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/
|
||||
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:${{steps.version.outputs.result}} docker/
|
||||
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v7,linux/arm64 --tag whyour/qinglong:latest docker/
|
|
@ -8,7 +8,7 @@ export default defineConfig({
|
|||
type: 'none',
|
||||
},
|
||||
fastRefresh: {},
|
||||
favicon: 'https://image.whyour.cn/others/g5.ico',
|
||||
favicon: 'https://qinglong.whyour.cn/g5.ico',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:5678/',
|
||||
|
|
22
LICENSE
Normal file
22
LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
MIT LICENSE
|
||||
|
||||
Copyright (c) 2021-present whyour
|
||||
|
||||
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.
|
48
README.md
48
README.md
|
@ -1,16 +1,51 @@
|
|||
## 青龙(WIP)
|
||||
<p align="center">
|
||||
<a href="https://github.com/whyour/qinglong">
|
||||
<img width="150" src="https://qinglong.whyour.cn/qinglong.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">青龙(WIP)</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
python和javaScript的定时任务管理面板
|
||||
|
||||
[![donate][donate-image]][donate-url] [![build status][build-status-image]][build-status-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker version][docker-version-image]][docker-version-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
|
||||
|
||||
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=for-the-badge
|
||||
[donate-url]: https://qinglong.whyour.cn/nice.png
|
||||
[build-status-image]: https://img.shields.io/docker/cloud/build/whyour/qinglong?style=for-the-badge
|
||||
[build-status-url]: https://img.shields.io/docker/cloud/build/whyour/qinglong
|
||||
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=for-the-badge
|
||||
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=for-the-badge
|
||||
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
|
||||
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=for-the-badge
|
||||
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=for-the-badge
|
||||
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
|
||||
|
||||
</div>
|
||||
|
||||
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
|
||||
|
||||
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
|
||||
|
||||
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神[1]。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼[2],与白虎监兵神君一起,是道教的护卫天神。
|
||||
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
|
||||
|
||||
## 多谢
|
||||
|
||||
[https://github.com/nevinee/jd_shell](https://github.com/nevinee/jd_shell)
|
||||
* [https://github.com/nevinee/jd_shell](https://github.com/nevinee/jd_shell)
|
||||
|
||||
[https://github.com/alseambusher/crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
* [https://github.com/alseambusher/crontab-ui](https://github.com/alseambusher/crontab-ui)
|
||||
|
||||
* [Ant Design](https://ant.design)
|
||||
|
||||
* [Ant Design Pro](https://pro.ant.design/)
|
||||
|
||||
* [Umijs3.0](https://umijs.org)
|
||||
|
||||
* [darkreader](https://github.com/darkreader/darkreader)
|
||||
|
||||
## 免责声明
|
||||
|
||||
|
@ -27,8 +62,3 @@
|
|||
6. 如果任何单位或个人认为此仓储脚本可能涉嫌侵犯其权利,应及时通知并提供身份证明,所有权证明,我们将在收到认证文件确认后删除此仓储脚本。
|
||||
|
||||
7. 所有直接或间接使用、查看此仓储脚本的人均应该仔细阅读此声明。本人保留随时更改或补充此声明的权利。一旦您使用或复制了此仓储脚本,即视为您已接受此免责声明。
|
||||
|
||||
|
||||
### one more thing
|
||||
|
||||

|
||||
|
|
|
@ -16,23 +16,17 @@ const initData = [
|
|||
status: CrontabStatus.idle,
|
||||
},
|
||||
{
|
||||
name: '自定义仓库',
|
||||
command: `sleep ${randomSchedule(
|
||||
60,
|
||||
1,
|
||||
)} && diy whyour hundun "quanx/jx|quanx/jd" tokens >> $QL_DIR/log/diy_pull.log 2>&1`,
|
||||
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
|
||||
24,
|
||||
6,
|
||||
).toString()} * * *`,
|
||||
status: CrontabStatus.idle,
|
||||
name: 'build面板',
|
||||
command: 'rebuild >> ${QL_DIR}/log/rebuild.log 2>&1',
|
||||
schedule: '30 7 */7 * *',
|
||||
status: CrontabStatus.disabled,
|
||||
},
|
||||
{
|
||||
name: '自定义仓库',
|
||||
command: `sleep ${randomSchedule(
|
||||
60,
|
||||
1,
|
||||
)} && diy monk-coder dust "i-chenzhe|normal" >> $QL_DIR/log/diy_pull.log 2>&1`,
|
||||
)} && diy https://ghproxy.com/https://github.com/whyour/hundun.git "quanx/jx|quanx/jd" tokens >> $QL_DIR/log/diy_pull.log 2>&1`,
|
||||
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
|
||||
24,
|
||||
6,
|
||||
|
@ -45,12 +39,6 @@ const initData = [
|
|||
schedule: '48 5 * * *',
|
||||
status: CrontabStatus.idle,
|
||||
},
|
||||
{
|
||||
name: 'build面板',
|
||||
command: 'rebuild >> ${QL_DIR}/log/rebuild.log 2>&1',
|
||||
schedule: '30 7 */7 * *',
|
||||
status: CrontabStatus.disabled,
|
||||
},
|
||||
{
|
||||
name: '删除日志',
|
||||
command: 'rm_log >/dev/null 2>&1',
|
||||
|
|
|
@ -81,10 +81,11 @@ export default class CookieService {
|
|||
|
||||
public async refreshCookie(_id: string) {
|
||||
const current = await this.get(_id);
|
||||
const { status } = await this.getJdInfo(current.value);
|
||||
const { status, nickname } = await this.getJdInfo(current.value);
|
||||
return {
|
||||
...current,
|
||||
status,
|
||||
nickname,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -121,14 +122,15 @@ export default class CookieService {
|
|||
}
|
||||
|
||||
public async create(payload: string[]): Promise<Cookie[]> {
|
||||
const cookies = await this.cookies('', { postion: 1 });
|
||||
const cookies = await this.cookies('');
|
||||
let position = initCookiePosition;
|
||||
if (cookies && cookies.length > 0) {
|
||||
position = cookies[0].position / 2;
|
||||
position = cookies[cookies.length - 1].position;
|
||||
}
|
||||
const tabs = payload.map((x) => {
|
||||
const cookie = new Cookie({ value: x, position });
|
||||
position = position / 2;
|
||||
cookie.position = position;
|
||||
return cookie;
|
||||
});
|
||||
const docs = await this.insert(tabs);
|
||||
|
|
|
@ -48,7 +48,7 @@ export default class CronService {
|
|||
const doc = await this.get(_id);
|
||||
const tab = new Crontab({ ...doc, ...other });
|
||||
tab.saved = false;
|
||||
const newDoc = await this.update(tab);
|
||||
const newDoc = await this.updateDb(tab);
|
||||
await this.set_crontab();
|
||||
return newDoc;
|
||||
}
|
||||
|
|
|
@ -15,9 +15,7 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
|||
coreutils \
|
||||
moreutils \
|
||||
git \
|
||||
wget \
|
||||
curl \
|
||||
nano \
|
||||
tzdata \
|
||||
perl \
|
||||
openssl \
|
||||
|
|
|
@ -7,6 +7,9 @@ echo
|
|||
|
||||
echo -e "======================2. 检测配置文件========================\n"
|
||||
[ ! -d ${QL_DIR}/config ] && mkdir -p ${QL_DIR}/config
|
||||
[ ! -d ${QL_DIR}/log ] && mkdir -p ${QL_DIR}/log
|
||||
[ ! -d ${QL_DIR}/db ] && mkdir -p ${QL_DIR}/db
|
||||
[ ! -d ${QL_DIR}/manual_log ] && mkdir -p ${QL_DIR}/manual_log
|
||||
|
||||
if [ ! -s ${QL_DIR}/config/crontab.list ]
|
||||
then
|
||||
|
@ -52,6 +55,9 @@ pm2 start ${QL_DIR}/build/app.js -n panel
|
|||
echo -e "控制面板启动成功...\n"
|
||||
|
||||
echo -e "\n容器启动成功...\n"
|
||||
echo -e "\n请先访问5700端口,登录面板成功之后先手动执行一次git_pull命令...\n"
|
||||
echo -e "\n如果需要启动挂机程序手动执行docker exec -it qinglong js hangup...\n"
|
||||
echo -e "\n或者去cron管理搜索hangup手动执行挂机任务...\n"
|
||||
|
||||
crond -f
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ echo -e "重新build...\n"
|
|||
yarn install --network-timeout 1000000000 || yarn install --registry=https://registry.npm.taobao.org --network-timeout 1000000000
|
||||
yarn build
|
||||
yarn build-back
|
||||
yarn cache clean
|
||||
echo -e "重新build完成...\n"
|
||||
|
||||
echo -e "重启服务...\n"
|
||||
|
|
|
@ -3,9 +3,6 @@ import { request } from '@/utils/http';
|
|||
import config from '@/utils/config';
|
||||
|
||||
export function render(oldRender: any) {
|
||||
if (history.location.pathname === '/login') {
|
||||
oldRender();
|
||||
}
|
||||
request
|
||||
.get(`${config.apiPrefix}user`)
|
||||
.then((data) => {
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 64 KiB |
|
@ -8,7 +8,6 @@ import {
|
|||
RadiusSettingOutlined,
|
||||
ControlOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import logo from '@/assets/logo.png';
|
||||
|
||||
export default {
|
||||
route: {
|
||||
|
@ -70,5 +69,5 @@ export default {
|
|||
fixSiderbar: true,
|
||||
contentWidth: 'Fixed',
|
||||
splitMenus: false,
|
||||
logo: logo,
|
||||
logo: 'https://qinglong.whyour.cn/qinglong.png',
|
||||
} as any;
|
||||
|
|
|
@ -23,6 +23,12 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.cookie-wrapper {
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-pro-grid-content.wide {
|
||||
.ant-pro-page-container-children-content {
|
||||
|
|
|
@ -113,8 +113,18 @@ const Config = () => {
|
|||
render: (text: string, record: any) => {
|
||||
const match = record.value.match(/pt_pin=([^; ]+)(?=;?)/);
|
||||
const val = (match && match[1]) || '未匹配用户名';
|
||||
return <span style={{ cursor: 'text' }}>{decodeUrl(val)}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
align: 'center' as const,
|
||||
width: '15%',
|
||||
render: (text: string, record: any, index: number) => {
|
||||
return (
|
||||
<span style={{ cursor: 'text' }}>{decodeURIComponent(val)}</span>
|
||||
<span style={{ cursor: 'text' }}>{record.nickname || '-'} </span>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -212,6 +222,36 @@ const Config = () => {
|
|||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const decodeUrl = (val: string) => {
|
||||
try {
|
||||
const newUrl = decodeURIComponent(val);
|
||||
return newUrl;
|
||||
} catch (error) {
|
||||
return val;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value && loading) {
|
||||
asyncUpdateStatus();
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const asyncUpdateStatus = async () => {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const cookie = value[i];
|
||||
if (cookie.status === Status.已禁用) {
|
||||
continue;
|
||||
}
|
||||
await sleep(1000);
|
||||
location.pathname === '/cookie' && refreshStatus(cookie, i);
|
||||
}
|
||||
};
|
||||
|
||||
const sleep = (time: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, time));
|
||||
};
|
||||
|
||||
const refreshStatus = (record: any, index: number) => {
|
||||
request
|
||||
.get(`${config.apiPrefix}cookies/${record._id}/refresh`)
|
||||
|
@ -322,24 +362,28 @@ const Config = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleCancel = (cookie: any) => {
|
||||
const handleCancel = (cookies: any[]) => {
|
||||
setIsModalVisible(false);
|
||||
if (cookie) {
|
||||
handleCookies(cookie);
|
||||
if (cookies && cookies.length > 0) {
|
||||
handleCookies(cookies);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCookies = (cookie: any) => {
|
||||
const index = value.findIndex((x) => x._id === cookie._id);
|
||||
const result = [...value];
|
||||
if (index === -1) {
|
||||
result.push(...cookie);
|
||||
} else {
|
||||
result.splice(index, 1, {
|
||||
...cookie,
|
||||
});
|
||||
const handleCookies = (cookies: any[]) => {
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i];
|
||||
const index = value.findIndex((x) => x._id === cookie._id);
|
||||
const result = [...value];
|
||||
if (index === -1) {
|
||||
result.push(cookie);
|
||||
} else {
|
||||
result.splice(index, 1, {
|
||||
...cookie,
|
||||
});
|
||||
}
|
||||
setValue(result);
|
||||
refreshStatus(cookie, index);
|
||||
}
|
||||
setValue(result);
|
||||
};
|
||||
|
||||
const components = {
|
||||
|
@ -388,7 +432,7 @@ const Config = () => {
|
|||
|
||||
return (
|
||||
<PageContainer
|
||||
className="code-mirror-wrapper"
|
||||
className="cookie-wrapper"
|
||||
title="Cookie管理"
|
||||
loading={loading}
|
||||
extra={[
|
||||
|
|
|
@ -13,6 +13,7 @@ const CookieModal = ({
|
|||
handleCancel: (needUpdate?: boolean) => void;
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
const cookies = values.value
|
||||
|
@ -29,6 +30,7 @@ const CookieModal = ({
|
|||
if (flag) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const method = cookie ? 'put' : 'post';
|
||||
const payload = cookie ? { value: cookies[0], _id: cookie._id } : cookies;
|
||||
const { code, data } = await request[method](`${config.apiPrefix}cookies`, {
|
||||
|
@ -43,6 +45,7 @@ const CookieModal = ({
|
|||
message: data,
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
};
|
||||
|
||||
|
@ -66,6 +69,7 @@ const CookieModal = ({
|
|||
});
|
||||
}}
|
||||
onCancel={() => handleCancel()}
|
||||
confirmLoading={loading}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
|
|
|
@ -14,8 +14,10 @@ const CronModal = ({
|
|||
handleCancel: (needUpdate?: boolean) => void;
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
setLoading(true);
|
||||
const method = cron ? 'put' : 'post';
|
||||
const payload = { ...values };
|
||||
if (cron) {
|
||||
|
@ -33,6 +35,7 @@ const CronModal = ({
|
|||
message: data,
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
handleCancel(data);
|
||||
};
|
||||
|
||||
|
@ -60,6 +63,7 @@ const CronModal = ({
|
|||
});
|
||||
}}
|
||||
onCancel={() => handleCancel()}
|
||||
confirmLoading={loading}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" name="form_in_modal" preserve={false}>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
}
|
||||
|
||||
.logo {
|
||||
width: 40px;
|
||||
width: 30px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import config from '@/utils/config';
|
|||
import { history, Link } from 'umi';
|
||||
import styles from './index.less';
|
||||
import { request } from '@/utils/http';
|
||||
import logo from '@/assets/logo.png';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
|
@ -48,7 +47,11 @@ const Login = () => {
|
|||
<div className={styles.content}>
|
||||
<div className={styles.top}>
|
||||
<div className={styles.header}>
|
||||
<img alt="logo" className={styles.logo} src={logo} />
|
||||
<img
|
||||
alt="logo"
|
||||
className={styles.logo}
|
||||
src="https://qinglong.whyour.cn/qinglong.png"
|
||||
/>
|
||||
<span className={styles.title}>{config.siteName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export default {
|
||||
siteName: '脚本控制面板',
|
||||
siteName: '青龙控制面板',
|
||||
apiPrefix: '/api/',
|
||||
authKey: 'token',
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user