tambah html log 18/12/2024
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
exception=unpack, strip: jdk.internal
|
||||
writer=console
|
||||
writer.exception=drop cause
|
||||
writer.format={date:dd-MM-yyyy HH:mm:ss} {class}.{method}() : {message}
|
||||
writerConsole=console
|
||||
writerConsole.level=info
|
||||
writerConsole.format={date:dd-MM-yyyy HH:mm:ss} {class}.{method}() :\n{message}
|
||||
|
||||
writerFile=rolling file
|
||||
writerFile.file=logs/{date:dd-MM-yyyy}.{count}.log
|
||||
writerFile.level=info
|
||||
writerFile.charset=UTF-8
|
||||
writerFile.append=true
|
||||
writerFile.policies=daily
|
||||
writerFile.format={date:dd-MM-yyyy HH:mm:ss}\t{class}.{method}() :\t{message}
|
||||
@@ -167,4 +167,60 @@ function loginload(){
|
||||
if (socket && socket.connected) {
|
||||
socket.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function logload(){
|
||||
console.log("Log loaded");
|
||||
if (socket && socket.connected) {
|
||||
socket.disconnect();
|
||||
}
|
||||
getLogFiles();
|
||||
$('#logfiles').change(function(){
|
||||
let logname = $('#logfiles').val();
|
||||
if (logname && logname.length>0){
|
||||
getLogData(logname);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getLogFiles(){
|
||||
fetch('/logfiles', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then((response)=>{
|
||||
response.json().then((data)=>{
|
||||
let logList = $('#logfiles');
|
||||
logList.empty();
|
||||
logList.append('<option value="">Select Logfile</option>');
|
||||
data.forEach((log)=>{
|
||||
logList.append('<option value="'+log+'">'+log+'</option>');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLogData(logname){
|
||||
fetch('/logdata', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: logname
|
||||
}).then((response)=>{
|
||||
response.json().then((data)=>{
|
||||
let tablebody = $('#tablebody');
|
||||
tablebody.empty();
|
||||
data.forEach((log)=>{
|
||||
let str = '<tr>';
|
||||
str += '<td>'+log.Date+'</td>';
|
||||
str += '<td>'+log.Time+'</td>';
|
||||
str += '<td>'+log.Method+'</td>';
|
||||
str += '<td>'+log.Message+'</td>';
|
||||
str += '</tr>';
|
||||
tablebody.append(str);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item"><a class="nav-link active" href="#">Overview</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="setting.html">Setting</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="log.html">Log</a></li>
|
||||
</ul>
|
||||
<div class="d-md-none my-2"><button class="btn btn-light me-2" type="button">Button</button><button class="btn btn-primary" type="button">Button</button></div>
|
||||
</div>
|
||||
@@ -85,8 +86,8 @@
|
||||
<div class="col"><input type="text" id="dialNumber" name="dialNumber"></div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col"><button class="btn btn-primary btn-lg" id="dialButton" type="button" onclick="dialClick()">Dial</button></div>
|
||||
<div class="col"><button class="btn btn-primary btn-lg" id="hangupButton" type="button" onclick="hangupClick()">Hangup</button></div>
|
||||
<div class="col"><button class="btn btn-primary btn-lg" id="dialButton" type="button">Dial</button></div>
|
||||
<div class="col"><button class="btn btn-primary btn-lg" id="hangupButton" type="button">Hangup</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
87
WebContentt/public/log.html
Normal file
87
WebContentt/public/log.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-bs-theme="light" lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>SIPIntercom</title>
|
||||
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="assets/css/Font%20Awesome%205%20Brands.css">
|
||||
<link rel="stylesheet" href="assets/css/Font%20Awesome%205%20Duotone.css">
|
||||
<link rel="stylesheet" href="assets/css/Font%20Awesome%205%20Pro.css">
|
||||
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Brands.css">
|
||||
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Duotone.css">
|
||||
<link rel="stylesheet" href="assets/css/Font%20Awesome%206%20Pro.css">
|
||||
<link rel="stylesheet" href="assets/css/FontAwesome.css">
|
||||
<link rel="stylesheet" href="assets/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/css/Navbar-Centered-Brand-icons.css">
|
||||
</head>
|
||||
|
||||
<body onload="logload()">
|
||||
<nav class="navbar navbar-expand-md bg-body py-3">
|
||||
<div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><span class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-bezier">
|
||||
<path fill-rule="evenodd" d="M0 10.5A1.5 1.5 0 0 1 1.5 9h1A1.5 1.5 0 0 1 4 10.5v1A1.5 1.5 0 0 1 2.5 13h-1A1.5 1.5 0 0 1 0 11.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm10.5.5A1.5 1.5 0 0 1 13.5 9h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM6 4.5A1.5 1.5 0 0 1 7.5 3h1A1.5 1.5 0 0 1 10 4.5v1A1.5 1.5 0 0 1 8.5 7h-1A1.5 1.5 0 0 1 6 5.5zM7.5 4a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"></path>
|
||||
<path d="M6 4.5H1.866a1 1 0 1 0 0 1h2.668A6.517 6.517 0 0 0 1.814 9H2.5c.123 0 .244.015.358.043a5.517 5.517 0 0 1 3.185-3.185A1.503 1.503 0 0 1 6 5.5zm3.957 1.358A1.5 1.5 0 0 0 10 5.5v-1h4.134a1 1 0 1 1 0 1h-2.668a6.517 6.517 0 0 1 2.72 3.5H13.5c-.123 0-.243.015-.358.043a5.517 5.517 0 0 0-3.185-3.185z"></path>
|
||||
</svg></span><span>SIP Intercom</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-4"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
|
||||
<div class="collapse navbar-collapse flex-grow-0 order-md-first" id="navcol-4">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="index.html">Overview</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="setting.html">Setting</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="#">Log</a></li>
|
||||
</ul>
|
||||
<div class="d-md-none my-2"><button class="btn btn-light me-2" type="button">Button</button><button class="btn btn-primary" type="button">Button</button></div>
|
||||
</div>
|
||||
<div class="d-none d-md-block">
|
||||
<form method="GET" action="/logout"><button class="btn btn-light me-2" type="submit">Log Off</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<p>Log File</p>
|
||||
</div>
|
||||
<div class="col-lg-6"><select id="logfiles">
|
||||
<optgroup label="This is a group">
|
||||
<option value="12" selected="">This is item 1</option>
|
||||
<option value="13">This is item 2</option>
|
||||
<option value="14">This is item 3</option>
|
||||
</optgroup>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="table-responsive">
|
||||
<table class="table" id="logtable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Method</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tablebody">
|
||||
<tr>
|
||||
<td>Cell 1</td>
|
||||
<td>Cell 2</td>
|
||||
<td>Cell 3</td>
|
||||
<td>Cell 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cell 3</td>
|
||||
<td>Cell 4</td>
|
||||
<td>Cell 3</td>
|
||||
<td>Cell 4</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||
<script src="assets/js/jquery-3.7.1.js"></script>
|
||||
<script src="assets/js/socket.io.js"></script>
|
||||
<script src="assets/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -27,6 +27,7 @@
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="index.html">Overview</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="#">Setting</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="log.html">Log</a></li>
|
||||
</ul>
|
||||
<div class="d-md-none my-2"><button class="btn btn-light me-2" type="button">Button</button><button class="btn btn-primary" type="button">Button</button></div>
|
||||
</div>
|
||||
@@ -49,7 +50,7 @@
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col"><button class="btn btn-primary btn-lg" type="button" onclick="sendLoginData()">Save <svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 0 512 512" width="1em" height="1em" fill="currentColor">
|
||||
<div class="col"><button class="btn btn-primary btn-lg" type="submit">Save <svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 0 512 512" width="1em" height="1em" fill="currentColor">
|
||||
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M48 96V416c0 8.8 7.2 16 16 16H384c8.8 0 16-7.2 16-16V170.5c0-4.2-1.7-8.3-4.7-11.3l33.9-33.9c12 12 18.7 28.3 18.7 45.3V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96C0 60.7 28.7 32 64 32H309.5c17 0 33.3 6.7 45.3 18.7l74.5 74.5-33.9 33.9L320.8 84.7c-.3-.3-.5-.5-.8-.8V184c0 13.3-10.7 24-24 24H104c-13.3 0-24-10.7-24-24V80H64c-8.8 0-16 7.2-16 16zm80-16v80H272V80H128zm32 240a64 64 0 1 1 128 0 64 64 0 1 1 -128 0z"></path>
|
||||
</svg></button></div>
|
||||
@@ -76,7 +77,7 @@
|
||||
<div class="col"><input class="form-control" type="text" id="sipPassword" name="sipPassword" placeholder="SIP Password"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col"><button class="btn btn-primary btn-lg" type="button" onclick="sendSipData()">Save <svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 0 512 512" width="1em" height="1em" fill="currentColor">
|
||||
<div class="col"><button class="btn btn-primary btn-lg" type="submit">Save <svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 0 512 512" width="1em" height="1em" fill="currentColor">
|
||||
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M48 96V416c0 8.8 7.2 16 16 16H384c8.8 0 16-7.2 16-16V170.5c0-4.2-1.7-8.3-4.7-11.3l33.9-33.9c12 12 18.7 28.3 18.7 45.3V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96C0 60.7 28.7 32 64 32H309.5c17 0 33.3 6.7 45.3 18.7l74.5 74.5-33.9 33.9L320.8 84.7c-.3-.3-.5-.5-.8-.8V184c0 13.3-10.7 24-24 24H104c-13.3 0-24-10.7-24-24V80H64c-8.8 0-16 7.2-16 16zm80-16v80H272V80H128zm32 240a64 64 0 1 1 128 0 64 64 0 1 1 -128 0z"></path>
|
||||
</svg></button></div>
|
||||
|
||||
64
src/Log/Log.java
Normal file
64
src/Log/Log.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package Log;
|
||||
|
||||
import org.tinylog.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Log {
|
||||
private Path logpath;
|
||||
final String regex = "(\\d{2}-\\d{2}-\\d{4}) (\\d{2}:\\d{2}:\\d{2})\\s+(\\S+)\\s:\\s+(.*)";
|
||||
final Pattern pattern = Pattern.compile(regex, Pattern.UNIX_LINES);
|
||||
|
||||
public Log(){
|
||||
String currentDirectory = System.getProperty("user.dir");
|
||||
logpath = Paths.get(currentDirectory,"logs");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the log files in the logs directory
|
||||
* @return List of log files
|
||||
*/
|
||||
public List<String> getLogFiles(){
|
||||
List<String> logFiles = new ArrayList<>();
|
||||
File[] files = logpath.toFile().listFiles();
|
||||
for(File file : files){
|
||||
if(file.isFile()){
|
||||
String shortName = file.getName();
|
||||
if (shortName.endsWith(".log")){
|
||||
if (file.length()>0){
|
||||
logFiles.add(shortName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return logFiles;
|
||||
}
|
||||
|
||||
public List<LogData> ReadLog(String logFile){
|
||||
List<LogData> logData = new ArrayList<>();
|
||||
try{
|
||||
Path ff = logpath.resolve(logFile);
|
||||
byte[] bytes = Files.readAllBytes(ff);
|
||||
String content = new String(bytes);
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
while(matcher.find()){
|
||||
LogData log = new LogData();
|
||||
log.Date = matcher.group(1);
|
||||
log.Time = matcher.group(2);
|
||||
log.Method = matcher.group(3);
|
||||
log.Message = matcher.group(4);
|
||||
logData.add(log);
|
||||
}
|
||||
} catch (Exception e){
|
||||
Logger.error("Failed reading log {}, Message: {}", logFile, e.getMessage());
|
||||
}
|
||||
return logData;
|
||||
}
|
||||
}
|
||||
24
src/Log/LogData.java
Normal file
24
src/Log/LogData.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package Log;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
public class LogData {
|
||||
public String Date;
|
||||
public String Time;
|
||||
public String Method;
|
||||
public String Message;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return Date + " " + Time + " " + Method + " " + Message;
|
||||
}
|
||||
|
||||
public static JsonObject toJson(LogData log){
|
||||
JsonObject jo = new JsonObject();
|
||||
jo.addProperty("Date", log.Date);
|
||||
jo.addProperty("Time", log.Time);
|
||||
jo.addProperty("Method", log.Method);
|
||||
jo.addProperty("Message", log.Message);
|
||||
return jo;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import java.util.*;
|
||||
|
||||
import static code.common.*;
|
||||
|
||||
|
||||
public class Main {
|
||||
private static jSIPClient client;
|
||||
private static WebServer webserver;
|
||||
@@ -47,11 +48,10 @@ public class Main {
|
||||
public static Properties config;
|
||||
|
||||
private static Thread sipThread;
|
||||
|
||||
public static void main(String[] args) {
|
||||
common.ExtractProperties(currentDir,"config.properties", false);
|
||||
config = common.LoadProperties(currentDir,"config.properties");
|
||||
common.ExtractProperties(currentDir,"tinylog.properties", false);
|
||||
//common.ExtractProperties(currentDir,"tinylog.properties", false);
|
||||
|
||||
// Timer Section
|
||||
timer = new Timer();
|
||||
@@ -68,6 +68,7 @@ public class Main {
|
||||
// Start System monitoring
|
||||
init_system_monitoring();
|
||||
|
||||
|
||||
// Shutdown Hook
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
Logger.info("Shutting down SIPIntercom");
|
||||
@@ -404,7 +405,7 @@ public class Main {
|
||||
*/
|
||||
private static void pickupCall() {
|
||||
if (incomingRequest!=null || incomingResponse!=null){
|
||||
Logger.info("Pickup incoming call from {}",incomingRequest.From);
|
||||
Logger.info("Pickup incoming call from {}",incomingRequest!=null ? incomingRequest.From:"Unknown");
|
||||
client.AcceptIncomingCall(incomingRequest);
|
||||
callLight_OnCall();
|
||||
Buzzer_OnCall();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package Webpage;
|
||||
|
||||
import Log.Log;
|
||||
import Log.LogData;
|
||||
import com.google.gson.JsonArray;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.HttpCode;
|
||||
import io.javalin.http.staticfiles.Location;
|
||||
@@ -7,6 +10,7 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.tinylog.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Consumer;
|
||||
@@ -21,12 +25,15 @@ public class WebServer {
|
||||
private @Setter Consumer<LoginSetting> onLoginSettingChanged;
|
||||
private @Setter Consumer<SipSetting> onSipSettingChanged;
|
||||
private @Getter int ProcessID;
|
||||
private final Log logworker;
|
||||
|
||||
public WebServer(Properties prop) {
|
||||
listenport = GetProperties_IntValue(prop,"WebListenPort", 8080);
|
||||
webusername = GetProperties_StringValue(prop,"WebUsername", "admin");
|
||||
webpassword = GetProperties_StringValue(prop,"WebPassword", "admin");
|
||||
|
||||
logworker = new Log();
|
||||
|
||||
int PID = GetPID(listenport);
|
||||
if (PID!=0) killPID(PID);
|
||||
|
||||
@@ -49,6 +56,11 @@ public class WebServer {
|
||||
ctx.redirect("/login.html");
|
||||
}
|
||||
});
|
||||
app.before("/log.html", ctx -> {
|
||||
if (!Objects.equals(ctx.sessionAttribute("username"), webusername)){
|
||||
ctx.redirect("/login.html");
|
||||
}
|
||||
});
|
||||
app.get("/logout", ctx -> {
|
||||
ctx.sessionAttribute("username", null);
|
||||
ctx.redirect("/login.html");
|
||||
@@ -133,6 +145,27 @@ public class WebServer {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/logfiles", ctx -> {
|
||||
List<String> logfiles = logworker.getLogFiles();
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
for (String logfile : logfiles) {
|
||||
jsonArray.add(logfile);
|
||||
}
|
||||
ctx.result(jsonArray.toString());
|
||||
});
|
||||
|
||||
app.post("/logdata", ctx -> {
|
||||
String logfile = ctx.body();
|
||||
if (ValidString(logfile)){
|
||||
List<LogData> logdata = logworker.ReadLog(logfile);
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
for (LogData log : logdata) {
|
||||
jsonArray.add(LogData.toJson(log));
|
||||
}
|
||||
ctx.result(jsonArray.toString());
|
||||
} else ctx.result("Invalid Log File").status(HttpCode.BAD_REQUEST);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,7 @@ public class common {
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
if (matcher.find()) {
|
||||
PID = Integer.parseInt(matcher.group(1));
|
||||
System.out.println("PID: "+PID);
|
||||
//System.out.println("PID: "+PID);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ public class common {
|
||||
try{
|
||||
File dest = new File(directory,filename);
|
||||
if (dest.isFile() && !overwrite){
|
||||
Logger.info("Properties file already exists: {}",filename);
|
||||
//Logger.info("Properties file already exists: {}",filename);
|
||||
return;
|
||||
}
|
||||
// delete existing file
|
||||
@@ -156,8 +156,8 @@ public class common {
|
||||
InputStream is = new FileInputStream(file);
|
||||
prop.load(is);
|
||||
is.close();
|
||||
Logger.info("Loaded properties file: {}",filename);
|
||||
} else Logger.info("Properties file not found: {}",filename);
|
||||
//Logger.info("Loaded properties file: {}",filename);
|
||||
} else Logger.error("Properties file not found: {}",filename);
|
||||
|
||||
} catch (Exception e) {
|
||||
Logger.error("Failed to load properties file: {}",filename);
|
||||
|
||||
Reference in New Issue
Block a user