mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2024-11-20 02:28:10 +01:00
feature request from ben new pr
This commit is contained in:
parent
a813a37e68
commit
569990a760
@ -1,10 +1,9 @@
|
||||
from http import HTTPStatus
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import CreateEmail, CreateEmailaddress, Emailaddresses, Emails
|
||||
from .models import CreateEmail, CreateEmailaddress, Email, Emailaddress
|
||||
from .smtp import send_mail
|
||||
|
||||
|
||||
@ -17,7 +16,7 @@ def get_test_mail(email, testemail):
|
||||
)
|
||||
|
||||
|
||||
async def create_emailaddress(data: CreateEmailaddress) -> Emailaddresses:
|
||||
async def create_emailaddress(data: CreateEmailaddress) -> Emailaddress:
|
||||
|
||||
emailaddress_id = urlsafe_short_hash()
|
||||
|
||||
@ -50,7 +49,7 @@ async def create_emailaddress(data: CreateEmailaddress) -> Emailaddresses:
|
||||
return new_emailaddress
|
||||
|
||||
|
||||
async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddresses:
|
||||
async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddress:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE smtp.emailaddress SET {q} WHERE id = ?",
|
||||
@ -65,30 +64,22 @@ async def update_emailaddress(emailaddress_id: str, **kwargs) -> Emailaddresses:
|
||||
await send_mail(row, email)
|
||||
|
||||
assert row, "Newly updated emailaddress couldn't be retrieved"
|
||||
return Emailaddresses(**row)
|
||||
return Emailaddress(**row)
|
||||
|
||||
|
||||
async def get_emailaddress(emailaddress_id: str) -> Optional[Emailaddresses]:
|
||||
async def get_emailaddress(emailaddress_id: str) -> Optional[Emailaddress]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,)
|
||||
)
|
||||
return Emailaddresses(**row) if row else None
|
||||
return Emailaddress(**row) if row else None
|
||||
|
||||
|
||||
async def get_emailaddress_by_email(email: str) -> Optional[Emailaddresses]:
|
||||
async def get_emailaddress_by_email(email: str) -> Optional[Emailaddress]:
|
||||
row = await db.fetchone("SELECT * FROM smtp.emailaddress WHERE email = ?", (email,))
|
||||
return Emailaddresses(**row) if row else None
|
||||
return Emailaddress(**row) if row else None
|
||||
|
||||
|
||||
# async def get_emailAddressByEmail(email: str) -> Optional[Emails]:
|
||||
# row = await db.fetchone(
|
||||
# "SELECT s.*, d.emailaddress as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.emailaddress = ?",
|
||||
# (email,),
|
||||
# )
|
||||
# return Subdomains(**row) if row else None
|
||||
|
||||
|
||||
async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailaddresses]:
|
||||
async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailaddress]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
@ -97,21 +88,22 @@ async def get_emailaddresses(wallet_ids: Union[str, List[str]]) -> List[Emailadd
|
||||
f"SELECT * FROM smtp.emailaddress WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
)
|
||||
|
||||
return [Emailaddresses(**row) for row in rows]
|
||||
return [Emailaddress(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_emailaddress(emailaddress_id: str) -> None:
|
||||
await db.execute("DELETE FROM smtp.emailaddress WHERE id = ?", (emailaddress_id,))
|
||||
|
||||
|
||||
## create emails
|
||||
async def create_email(payment_hash, wallet, data: CreateEmail) -> Emails:
|
||||
async def create_email(wallet: str, data: CreateEmail, payment_hash: str = "") -> Email:
|
||||
id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO smtp.email (id, wallet, emailaddress_id, subject, receiver, message, paid)
|
||||
INSERT INTO smtp.email (id, payment_hash, wallet, emailaddress_id, subject, receiver, message, paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
id,
|
||||
payment_hash,
|
||||
wallet,
|
||||
data.emailaddress_id,
|
||||
@ -122,36 +114,34 @@ async def create_email(payment_hash, wallet, data: CreateEmail) -> Emails:
|
||||
),
|
||||
)
|
||||
|
||||
new_email = await get_email(payment_hash)
|
||||
new_email = await get_email(id)
|
||||
assert new_email, "Newly created email couldn't be retrieved"
|
||||
return new_email
|
||||
|
||||
|
||||
async def set_email_paid(payment_hash: str) -> Emails:
|
||||
email = await get_email(payment_hash)
|
||||
async def set_email_paid(payment_hash: str) -> bool:
|
||||
email = await get_email_by_payment_hash(payment_hash)
|
||||
if email and email.paid == False:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE smtp.email
|
||||
SET paid = true
|
||||
WHERE id = ?
|
||||
""",
|
||||
(payment_hash,),
|
||||
f"UPDATE smtp.email SET paid = true WHERE payment_hash = {payment_hash}"
|
||||
)
|
||||
new_email = await get_email(payment_hash)
|
||||
assert new_email, "Newly paid email couldn't be retrieved"
|
||||
return new_email
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def get_email(email_id: str) -> Optional[Emails]:
|
||||
async def get_email_by_payment_hash(payment_hash: str) -> Optional[Email]:
|
||||
row = await db.fetchone(
|
||||
"SELECT s.*, d.email as emailaddress FROM smtp.email s INNER JOIN smtp.emailaddress d ON (s.emailaddress_id = d.id) WHERE s.id = ?",
|
||||
(email_id,),
|
||||
f"SELECT * FROM smtp.email WHERE payment_hash = {payment_hash}"
|
||||
)
|
||||
return Emails(**row) if row else None
|
||||
return Email(**row) if row else None
|
||||
|
||||
|
||||
async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Emails]:
|
||||
async def get_email(id: str) -> Optional[Email]:
|
||||
row = await db.fetchone(f"SELECT * FROM smtp.email WHERE id = {id}")
|
||||
return Email(**row) if row else None
|
||||
|
||||
|
||||
async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Email]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
@ -161,7 +151,7 @@ async def get_emails(wallet_ids: Union[str, List[str]]) -> List[Emails]:
|
||||
(*wallet_ids,),
|
||||
)
|
||||
|
||||
return [Emails(**row) for row in rows]
|
||||
return [Email(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_email(email_id: str) -> None:
|
||||
|
@ -33,3 +33,7 @@ async def m001_initial(db):
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m002_add_payment_hash(db):
|
||||
await db.execute(f"ALTER TABLE smtp.email ADD COLUMN payment_hash TEXT NOT NULL;")
|
||||
|
@ -15,7 +15,7 @@ class CreateEmailaddress(BaseModel):
|
||||
cost: int = Query(..., ge=0)
|
||||
|
||||
|
||||
class Emailaddresses(BaseModel):
|
||||
class Emailaddress(BaseModel):
|
||||
id: str
|
||||
wallet: str
|
||||
email: str
|
||||
@ -36,7 +36,7 @@ class CreateEmail(BaseModel):
|
||||
message: str = Query(...)
|
||||
|
||||
|
||||
class Emails(BaseModel):
|
||||
class Email(BaseModel):
|
||||
id: str
|
||||
wallet: str
|
||||
emailaddress_id: str
|
||||
|
@ -4,83 +4,107 @@ import time
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate
|
||||
from http import HTTPStatus
|
||||
from smtplib import SMTP_SSL as SMTP
|
||||
from typing import Union
|
||||
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from .models import CreateEmail, CreateEmailaddress, Email, Emailaddress
|
||||
|
||||
|
||||
async def send_mail(
|
||||
emailaddress: Union[Emailaddress, CreateEmailaddress],
|
||||
email: Union[Email, CreateEmail],
|
||||
):
|
||||
smtp_client = SmtpService(emailaddress)
|
||||
message = smtp_client.create_message(email)
|
||||
await smtp_client.send_mail(email.receiver, message)
|
||||
|
||||
|
||||
def valid_email(s):
|
||||
# https://regexr.com/2rhq7
|
||||
pat = "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
|
||||
pat = r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
|
||||
if re.match(pat, s):
|
||||
return True
|
||||
msg = f"SMTP - invalid email: {s}."
|
||||
logger.error(msg)
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
|
||||
log = f"SMTP - invalid email: {s}."
|
||||
logger.error(log)
|
||||
raise Exception(log)
|
||||
|
||||
|
||||
async def send_mail(emailaddress, email):
|
||||
valid_email(emailaddress.email)
|
||||
valid_email(email.receiver)
|
||||
class SmtpService:
|
||||
def __init__(self, emailaddress: Union[Emailaddress, CreateEmailaddress]) -> None:
|
||||
self.sender = emailaddress.email
|
||||
self.smtp_server = emailaddress.smtp_server
|
||||
self.smtp_port = emailaddress.smtp_port
|
||||
self.smtp_user = emailaddress.smtp_user
|
||||
self.smtp_password = emailaddress.smtp_password
|
||||
|
||||
ts = time.time()
|
||||
date = formatdate(ts, True)
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Date"] = date
|
||||
msg["Subject"] = email.subject
|
||||
msg["From"] = emailaddress.email
|
||||
msg["To"] = email.receiver
|
||||
|
||||
signature = "Email sent anonymiously by LNbits Sendmail extension."
|
||||
text = f"""
|
||||
{email.message}
|
||||
|
||||
{signature}
|
||||
"""
|
||||
|
||||
html = f"""
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>{email.message}<p>
|
||||
<br>
|
||||
<p>{signature}</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(html, "html")
|
||||
msg.attach(part1)
|
||||
msg.attach(part2)
|
||||
|
||||
try:
|
||||
conn = SMTP(
|
||||
host=emailaddress.smtp_server, port=emailaddress.smtp_port, timeout=10
|
||||
def render_email(self, email: Union[Email, CreateEmail]):
|
||||
signature: str = "Email sent by LNbits SMTP extension."
|
||||
text = f"{email.message}\n\n{signature}"
|
||||
html = (
|
||||
"""
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>"""
|
||||
+ email.message
|
||||
+ """</p>
|
||||
<p>"""
|
||||
+ signature
|
||||
+ """</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
logger.debug("SMTP - connected to smtp server.")
|
||||
# conn.set_debuglevel(True)
|
||||
except:
|
||||
msg = f"SMTP - error connecting to smtp server: {emailaddress.smtp_server}:{emailaddress.smtp_port}."
|
||||
logger.error(msg)
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
|
||||
try:
|
||||
conn.login(emailaddress.smtp_user, emailaddress.smtp_password)
|
||||
logger.debug("SMTP - successful login to smtp server.")
|
||||
except:
|
||||
msg = f"SMTP - error login into smtp {emailaddress.smtp_user}."
|
||||
logger.error(msg)
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
|
||||
try:
|
||||
conn.sendmail(emailaddress.email, email.receiver, msg.as_string())
|
||||
logger.debug("SMTP - successfully send email.")
|
||||
except socket.error as e:
|
||||
msg = f"SMTP - error sending email: {str(e)}."
|
||||
logger.error(msg)
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg)
|
||||
finally:
|
||||
conn.quit()
|
||||
return text, html
|
||||
|
||||
def create_message(self, email: Union[Email, CreateEmail]):
|
||||
ts = time.time()
|
||||
date = formatdate(ts, True)
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Date"] = date
|
||||
msg["Subject"] = email.subject
|
||||
msg["From"] = self.sender
|
||||
msg["To"] = email.receiver
|
||||
|
||||
text, html = self.render_email(email)
|
||||
|
||||
part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(html, "html")
|
||||
msg.attach(part1)
|
||||
msg.attach(part2)
|
||||
return msg
|
||||
|
||||
async def send_mail(self, receiver, msg: MIMEMultipart):
|
||||
|
||||
valid_email(self.sender)
|
||||
valid_email(receiver)
|
||||
|
||||
try:
|
||||
conn = SMTP(host=self.smtp_server, port=int(self.smtp_port), timeout=10)
|
||||
logger.debug("SMTP - connected to smtp server.")
|
||||
# conn.set_debuglevel(True)
|
||||
except:
|
||||
log = f"SMTP - error connecting to smtp server: {self.smtp_server}:{self.smtp_port}."
|
||||
logger.debug(log)
|
||||
raise Exception(log)
|
||||
|
||||
try:
|
||||
conn.login(self.smtp_user, self.smtp_password)
|
||||
logger.debug("SMTP - successful login to smtp server.")
|
||||
except:
|
||||
log = f"SMTP - error login into smtp {self.smtp_user}."
|
||||
logger.error(log)
|
||||
raise Exception(log)
|
||||
|
||||
try:
|
||||
conn.sendmail(self.sender, receiver, msg.as_string())
|
||||
logger.debug("SMTP - successfully send email.")
|
||||
except socket.error as e:
|
||||
log = f"SMTP - error sending email: {str(e)}."
|
||||
logger.error(log)
|
||||
raise Exception(log)
|
||||
finally:
|
||||
conn.quit()
|
||||
|
@ -5,7 +5,7 @@ from loguru import logger
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_email, get_emailaddress, set_email_paid
|
||||
from .crud import get_email_by_payment_hash, get_emailaddress, set_email_paid
|
||||
from .smtp import send_mail
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra.get("tag") != "smtp":
|
||||
return
|
||||
|
||||
email = await get_email(payment.checking_id)
|
||||
email = await get_email_by_payment_hash(payment.checking_id)
|
||||
if not email:
|
||||
logger.error("SMTP: email can not by fetched")
|
||||
return
|
||||
|
@ -57,6 +57,14 @@
|
||||
:href="props.row.displayUrl"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="email"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="showEmailDialog(props.row.id)"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
@ -154,6 +162,42 @@
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="emailDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendEmail()" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="emailDialog.data.receiver"
|
||||
type="text"
|
||||
label="Receiver"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="emailDialog.data.subject"
|
||||
type="text"
|
||||
label="Subject"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="emailDialog.data.message"
|
||||
type="textarea"
|
||||
label="Message "
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="emailDialog.data.receiver == '' || emailDialog.data.subject == '' || emailDialog.data.message == ''"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-dialog v-model="emailaddressDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormData" class="q-gutter-md">
|
||||
@ -316,10 +360,10 @@
|
||||
emailsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'emailaddress',
|
||||
name: 'emailaddress_id',
|
||||
align: 'left',
|
||||
label: 'From',
|
||||
field: 'emailaddress'
|
||||
field: 'emailaddress_id'
|
||||
},
|
||||
{
|
||||
name: 'receiver',
|
||||
@ -350,6 +394,10 @@
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
emailDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
emailaddressDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
@ -453,6 +501,33 @@
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
sendEmail: function () {
|
||||
var self = this
|
||||
var emailaddress = _.findWhere(this.emailaddresses, {
|
||||
id: self.emailDialog.data.emailaddress_id
|
||||
})
|
||||
var wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: emailaddress.wallet
|
||||
})
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/smtp/api/v1/email/' + emailaddress.id + '/send',
|
||||
wallet.adminkey,
|
||||
self.emailDialog.data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.emailDialog.show = false
|
||||
self.emailDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
showEmailDialog: function (emailaddress_id) {
|
||||
this.emailDialog.data.emailaddress_id = emailaddress_id
|
||||
this.emailDialog.show = true
|
||||
},
|
||||
updateEmailaddressDialog: function (formId) {
|
||||
var link = _.findWhere(this.emailaddresses, {id: formId})
|
||||
this.emailaddressDialog.data = _.clone(link)
|
||||
|
@ -4,7 +4,7 @@ from fastapi import Depends, HTTPException, Query
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.services import check_transaction_status, create_invoice
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
|
||||
from . import smtp_ext
|
||||
from .crud import (
|
||||
@ -19,7 +19,7 @@ from .crud import (
|
||||
update_emailaddress,
|
||||
)
|
||||
from .models import CreateEmail, CreateEmailaddress
|
||||
from .smtp import valid_email
|
||||
from .smtp import send_mail, valid_email
|
||||
|
||||
|
||||
## EMAILS
|
||||
@ -44,6 +44,7 @@ async def api_smtp_send_email(payment_hash):
|
||||
)
|
||||
|
||||
emailaddress = await get_emailaddress(email.emailaddress_id)
|
||||
assert emailaddress
|
||||
|
||||
try:
|
||||
status = await check_transaction_status(email.wallet, payment_hash)
|
||||
@ -59,11 +60,9 @@ async def api_smtp_send_email(payment_hash):
|
||||
|
||||
@smtp_ext.post("/api/v1/email/{emailaddress_id}")
|
||||
async def api_smtp_make_email(emailaddress_id, data: CreateEmail):
|
||||
|
||||
valid_email(data.receiver)
|
||||
|
||||
emailaddress = await get_emailaddress(emailaddress_id)
|
||||
# If the request is coming for the non-existant emailaddress
|
||||
if not emailaddress:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
@ -94,6 +93,26 @@ async def api_smtp_make_email(emailaddress_id, data: CreateEmail):
|
||||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
||||
|
||||
|
||||
@smtp_ext.post(
|
||||
"/api/v1/email/{emailaddress_id}/send", dependencies=[Depends(require_admin_key)]
|
||||
)
|
||||
async def api_smtp_make_email_send(emailaddress_id, data: CreateEmail):
|
||||
valid_email(data.receiver)
|
||||
emailaddress = await get_emailaddress(emailaddress_id)
|
||||
if not emailaddress:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Emailaddress address does not exist.",
|
||||
)
|
||||
email = await create_email(wallet=emailaddress.wallet, data=data)
|
||||
if not email:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Email could not be fetched."
|
||||
)
|
||||
await send_mail(emailaddress, email)
|
||||
return {"sent": True}
|
||||
|
||||
|
||||
@smtp_ext.delete("/api/v1/email/{email_id}")
|
||||
async def api_email_delete(email_id, g: WalletTypeInfo = Depends(get_key_type)):
|
||||
email = await get_email(email_id)
|
||||
|
Loading…
Reference in New Issue
Block a user