Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b946386e5d | |||
| c98b329df3 | |||
| 734b66090a | |||
| 34708bd57e |
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@ -18,10 +18,10 @@
|
|||||||
"cwd": "${workspaceFolder}/Biblioteca",
|
"cwd": "${workspaceFolder}/Biblioteca",
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||||
"serverReadyAction": {
|
// "serverReadyAction": {
|
||||||
"action": "openExternally",
|
// "action": "openExternally",
|
||||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
// "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||||
},
|
// },
|
||||||
"env": {
|
"env": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
||||||
|
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
### Obtém uma lista de livros
|
### Obtém uma lista de livros
|
||||||
|
|
||||||
GET {{url}}/livros
|
GET {{url}}/livros?pagina=3
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
|
|
||||||
### Cria um novo livro
|
### Cria um novo livro
|
||||||
@ -12,29 +12,28 @@ Accept: application/json
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"isbn": "9780321741769",
|
"isbn": "aabbccddee",
|
||||||
"titulo": "The C# Programming Language",
|
"titulo": "Livro teste",
|
||||||
"autor": "Anders Hejlsberg"
|
"autor": "Autor"
|
||||||
}
|
}
|
||||||
|
|
||||||
### Edita um livro
|
### Edita um livro
|
||||||
|
|
||||||
PUT {{url}}/livros/9780321741769
|
PUT {{url}}/livros/1027
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"isbn": "9780321741769",
|
"titulo": "teste4",
|
||||||
"titulo": "The C# Programming Language",
|
|
||||||
"autor": "Anders Hejlsberg, Mads Torgensen"
|
"autor": "Anders Hejlsberg, Mads Torgensen"
|
||||||
}
|
}
|
||||||
|
|
||||||
### Obtém um livro individual
|
### Obtém um livro individual
|
||||||
|
|
||||||
GET {{url}}/livros/9780321741769
|
GET {{url}}/livros/1027
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
|
|
||||||
### Remove um livro
|
### Remove um livro
|
||||||
|
|
||||||
DELETE {{url}}/livros/9780321741769
|
DELETE {{url}}/livros/1027
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
16
Biblioteca/Models/Livro.cs
Normal file
16
Biblioteca/Models/Livro.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace Biblioteca.Models;
|
||||||
|
|
||||||
|
public class Livro
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string? Isbn { get; set; }
|
||||||
|
public string? Titulo { get; set; }
|
||||||
|
public string? Autor { get; set; }
|
||||||
|
public string? Genero { get; set; }
|
||||||
|
public string? Descricao { get; set; }
|
||||||
|
public string? Foto { get; set; }
|
||||||
|
public string? Keywords { get; set; }
|
||||||
|
public bool Ativo { get; set; }
|
||||||
|
public DateTime CriadoEm { get; set; }
|
||||||
|
public DateTime AtualizadoEm { get; set; }
|
||||||
|
}
|
||||||
@ -1,71 +1,52 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Biblioteca.Models;
|
||||||
|
using Biblioteca.Repositories;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.MapGet("/", () =>
|
|
||||||
{
|
|
||||||
string[] saudacoes = ["Olá!", "こんにちは", "Привет", "Ողջույն"];
|
|
||||||
|
|
||||||
return saudacoes[Random.Shared.Next(saudacoes.Length)];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Obtém uma lista com os livros registrados.
|
// Obtém uma lista com os livros registrados.
|
||||||
app.MapGet("/livros", () =>
|
app.MapGet("/livros", async (int pagina) =>
|
||||||
{
|
{
|
||||||
return new Livro[]
|
var repo = new LivroRepository();
|
||||||
{
|
var res = await repo.Obter(pagina: pagina);
|
||||||
new()
|
|
||||||
{
|
return res;
|
||||||
Isbn = "9780262510875",
|
|
||||||
Titulo = "Structure and Interpretation of Computer Programs",
|
|
||||||
Autor = "Gerald Jay Sussman"
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Isbn = "9780131103627",
|
|
||||||
Titulo = "C Programming Language: ANSI C Version",
|
|
||||||
Autor = "Dennis Ritchie, Brian Kerningham"
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Isbn = "9780134190440",
|
|
||||||
Titulo = "The Go Programming Language",
|
|
||||||
Autor = "Brian Kerningham"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cria um novo livro.
|
// Cria um novo livro.
|
||||||
app.MapPost("/livros", (Livro livro) =>
|
app.MapPost("/livros", async (Livro livro) =>
|
||||||
{
|
{
|
||||||
return livro;
|
var repo = new LivroRepository();
|
||||||
|
var res = await repo.Criar(livro);
|
||||||
|
|
||||||
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Edita um livro.
|
// Edita um livro.
|
||||||
app.MapPut("/livros/{isbn}", (string isbn, Livro livro) =>
|
app.MapPut("/livros/{id}", async (long id, Livro livro) =>
|
||||||
{
|
{
|
||||||
return new { editando = isbn, dados = livro };
|
var repo = new LivroRepository();
|
||||||
|
var res = await repo.Editar(id, livro);
|
||||||
|
|
||||||
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Obtém os dados de um livro individual.
|
// Obtém os dados de um livro individual.
|
||||||
app.MapGet("/livros/{isbn}", (string isbn) =>
|
app.MapGet("/livros/{id}", async (long id) =>
|
||||||
{
|
{
|
||||||
return new Livro() { Isbn = isbn };
|
var repo = new LivroRepository();
|
||||||
|
var res = await repo.Obter(id);
|
||||||
|
|
||||||
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove um livro.
|
// Remove um livro.
|
||||||
app.MapDelete("/livros/{isbn}", (string isbn) =>
|
app.MapDelete("/livros/{id}", async (long id) =>
|
||||||
{
|
{
|
||||||
return Results.NoContent();
|
var repo = new LivroRepository();
|
||||||
|
|
||||||
|
await repo.Desativar(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
public class Livro
|
|
||||||
{
|
|
||||||
public string? Isbn { get; set; }
|
|
||||||
public string? Titulo { get; set; }
|
|
||||||
public string? Autor { get; set; }
|
|
||||||
};
|
|
||||||
198
Biblioteca/Repositories/LivroRepository.cs
Normal file
198
Biblioteca/Repositories/LivroRepository.cs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
using Biblioteca.Models;
|
||||||
|
using MySqlConnector;
|
||||||
|
|
||||||
|
namespace Biblioteca.Repositories;
|
||||||
|
|
||||||
|
public class LivroRepository
|
||||||
|
{
|
||||||
|
private const string ConnString = "Server=gbrl.dev;Port=5306;User ID=sistemasdistribuidos.aluno;Password=eW03avS7M8kOUL1A9bZWW2RTIfzEI1Di;Database=sistemasdistribuidos";
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Livro>> Obter(int pagina)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
var take = 30;
|
||||||
|
var offset = take * Math.Max(pagina-1, 0);
|
||||||
|
var lista = new List<Livro>();
|
||||||
|
|
||||||
|
cmd.CommandText = "SELECT Id, Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro ORDER BY CriadoEm LIMIT @offset,@take";
|
||||||
|
cmd.Parameters.AddWithValue("offset", offset);
|
||||||
|
cmd.Parameters.AddWithValue("take", take);
|
||||||
|
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
lista.Add(new()
|
||||||
|
{
|
||||||
|
Id = reader.GetInt64(0),
|
||||||
|
Isbn = reader.GetString(1),
|
||||||
|
Titulo = reader.GetString(2),
|
||||||
|
Autor = reader.GetString(3),
|
||||||
|
Genero = reader.GetString(4),
|
||||||
|
Descricao = reader.GetString(5),
|
||||||
|
Foto = reader.GetString(6),
|
||||||
|
Keywords = reader.GetString(7),
|
||||||
|
Ativo = reader.GetBoolean(8),
|
||||||
|
CriadoEm = reader.GetDateTime(9),
|
||||||
|
AtualizadoEm = reader.GetDateTime(10),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return lista;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém um livro pelo seu ISBN.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="isbn"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Livro> Obter(long id)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
cmd.CommandText = "SELECT Id, Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm FROM Livro WHERE Id=@id";
|
||||||
|
cmd.Parameters.AddWithValue("id", id);
|
||||||
|
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
var existe = await reader.ReadAsync();
|
||||||
|
|
||||||
|
if (!existe)
|
||||||
|
{
|
||||||
|
throw new Exception($"Livro {id} não encontrado");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = reader.GetInt64(0),
|
||||||
|
Isbn = reader.GetString(1),
|
||||||
|
Titulo = reader.GetString(2),
|
||||||
|
Autor = reader.GetString(3),
|
||||||
|
Genero = reader.GetString(4),
|
||||||
|
Descricao = reader.GetString(5),
|
||||||
|
Foto = reader.GetString(6),
|
||||||
|
Keywords = reader.GetString(7),
|
||||||
|
Ativo = reader.GetBoolean(8),
|
||||||
|
CriadoEm = reader.GetDateTime(9),
|
||||||
|
AtualizadoEm = reader.GetDateTime(10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Livro> Criar(Livro dados)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
var livro = new Livro
|
||||||
|
{
|
||||||
|
Isbn = dados.Isbn?.Trim() ?? "",
|
||||||
|
Titulo = dados.Titulo?.Trim() ?? "",
|
||||||
|
Autor = dados.Autor?.Trim() ?? "",
|
||||||
|
Genero = dados.Genero?.Trim() ?? "",
|
||||||
|
Descricao = dados.Descricao?.Trim() ?? "",
|
||||||
|
Foto = dados.Foto?.Trim() ?? "",
|
||||||
|
Keywords = dados.Keywords?.Trim() ?? "",
|
||||||
|
Ativo = true,
|
||||||
|
CriadoEm = DateTime.Now,
|
||||||
|
AtualizadoEm = default,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (livro.Titulo == "")
|
||||||
|
{
|
||||||
|
throw new Exception("O título do livro é obrigatório.");
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.CommandText =
|
||||||
|
@"
|
||||||
|
INSERT INTO Livro
|
||||||
|
(Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm)
|
||||||
|
VALUES
|
||||||
|
(@isbn, @titulo, @autor, @genero, @descricao, @foto, @keywords, @ativo, @criadoem, @atualizadoem)
|
||||||
|
";
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("isbn", livro.Isbn);
|
||||||
|
cmd.Parameters.AddWithValue("titulo", livro.Titulo);
|
||||||
|
cmd.Parameters.AddWithValue("autor", livro.Autor);
|
||||||
|
cmd.Parameters.AddWithValue("genero", livro.Genero);
|
||||||
|
cmd.Parameters.AddWithValue("descricao", livro.Descricao);
|
||||||
|
cmd.Parameters.AddWithValue("foto", livro.Foto);
|
||||||
|
cmd.Parameters.AddWithValue("keywords", livro.Keywords);
|
||||||
|
cmd.Parameters.AddWithValue("ativo", livro.Ativo);
|
||||||
|
cmd.Parameters.AddWithValue("criadoem", livro.CriadoEm);
|
||||||
|
cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
using var cmd2 = conn.CreateCommand();
|
||||||
|
|
||||||
|
cmd2.CommandText = "SELECT LAST_INSERT_ID()";
|
||||||
|
|
||||||
|
using var reader = await cmd2.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
await reader.ReadAsync();
|
||||||
|
|
||||||
|
livro.Id = reader.GetInt64(0);
|
||||||
|
|
||||||
|
return livro;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Livro> Editar(long id, Livro dados)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
var livro = await Obter(id);
|
||||||
|
|
||||||
|
livro.Titulo = dados.Titulo?.Trim() ?? "";
|
||||||
|
livro.Autor = dados.Autor?.Trim() ?? "";
|
||||||
|
livro.Genero = dados.Genero?.Trim() ?? "";
|
||||||
|
livro.Descricao = dados.Descricao?.Trim() ?? "";
|
||||||
|
livro.Foto = dados.Foto?.Trim() ?? "";
|
||||||
|
livro.Keywords = dados.Keywords?.Trim() ?? "";
|
||||||
|
livro.AtualizadoEm = DateTime.Now;
|
||||||
|
|
||||||
|
cmd.CommandText =
|
||||||
|
@"
|
||||||
|
UPDATE Livro SET
|
||||||
|
Titulo=@titulo, Autor=@autor, Genero=@genero, Descricao=@descricao, Foto=@foto, Keywords=@keywords, AtualizadoEm=@atualizadoem
|
||||||
|
WHERE Id=@id
|
||||||
|
";
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("id", id);
|
||||||
|
cmd.Parameters.AddWithValue("titulo", livro.Titulo);
|
||||||
|
cmd.Parameters.AddWithValue("autor", livro.Autor);
|
||||||
|
cmd.Parameters.AddWithValue("genero", livro.Genero);
|
||||||
|
cmd.Parameters.AddWithValue("descricao", livro.Descricao);
|
||||||
|
cmd.Parameters.AddWithValue("foto", livro.Foto);
|
||||||
|
cmd.Parameters.AddWithValue("keywords", livro.Keywords);
|
||||||
|
cmd.Parameters.AddWithValue("atualizadoem", livro.AtualizadoEm);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
return livro;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Desativar(long id)
|
||||||
|
{
|
||||||
|
using var conn = new MySqlConnection(ConnString);
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
cmd.CommandText = "UPDATE Livro SET Ativo=false WHERE Id=@id";
|
||||||
|
cmd.Parameters.AddWithValue("id", id);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
71
scripts/books.py
Normal file
71
scripts/books.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#
|
||||||
|
# books.py
|
||||||
|
# Gera um arquivo .sql com INSERTs de livros obtidos do Project Gutemberg
|
||||||
|
# https://gutendex.com/
|
||||||
|
#
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
API_URL = "https://gutendex.com/books/"
|
||||||
|
BOOKS_COUNT = 1000
|
||||||
|
FETCH_DELAY = 1#s
|
||||||
|
OUTPUT = "./books.sql"
|
||||||
|
|
||||||
|
def fetch(resource):
|
||||||
|
url = urlparse(resource)
|
||||||
|
client = http.client.HTTPSConnection if url.scheme == "https" else http.client.HTTPConnection
|
||||||
|
conn = client(url.netloc)
|
||||||
|
|
||||||
|
conn.request("GET", f"{url.path}?{url.query}")
|
||||||
|
res = conn.getresponse()
|
||||||
|
|
||||||
|
if res.status < 200 or res.status > 299:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return json.loads(res.read())
|
||||||
|
|
||||||
|
def write_inserts(file, page):
|
||||||
|
data = fetch(f"{API_URL}?page={page}")
|
||||||
|
books = data["results"]
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
esc = lambda str: str.replace('"', '\\"')
|
||||||
|
|
||||||
|
for book in books:
|
||||||
|
summaries = book["summaries"]
|
||||||
|
imgs = [book["formats"][f] for f in book["formats"] if f.startswith("image/")]
|
||||||
|
|
||||||
|
isbn = ""
|
||||||
|
titulo = book["title"]
|
||||||
|
autor = ",".join([a["name"] for a in book["authors"]])
|
||||||
|
genero = ",".join(book["subjects"])
|
||||||
|
descricao = summaries[0] if len(summaries) > 0 else ""
|
||||||
|
foto = imgs[0] if len(imgs) > 0 else ""
|
||||||
|
keywords = ",".join(book["bookshelves"])
|
||||||
|
|
||||||
|
lines.append(f'("{esc(isbn)}", "{esc(titulo)}", "{esc(autor)}", "{esc(genero)}", "{esc(descricao)}", "{esc(foto)}", "{esc(keywords)}", true, NOW(), NOW())')
|
||||||
|
|
||||||
|
values = ",\n".join(lines)
|
||||||
|
insert = f"INSERT INTO Livro (Isbn, Titulo, Autor, Genero, Descricao, Foto, Keywords, Ativo, CriadoEm, AtualizadoEm) VALUES {values};\n"
|
||||||
|
|
||||||
|
file.write(insert)
|
||||||
|
|
||||||
|
return len(lines)
|
||||||
|
|
||||||
|
def collect(n, file, _page=1):
|
||||||
|
if n <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
written = write_inserts(file, _page)
|
||||||
|
|
||||||
|
time.sleep(FETCH_DELAY)
|
||||||
|
collect(n-written, file, _page+1)
|
||||||
|
|
||||||
|
def run():
|
||||||
|
with open(OUTPUT, "w") as file:
|
||||||
|
collect(BOOKS_COUNT, file)
|
||||||
|
|
||||||
|
run()
|
||||||
1024
scripts/books.sql
Normal file
1024
scripts/books.sql
Normal file
File diff suppressed because it is too large
Load Diff
17
scripts/schema.sql
Normal file
17
scripts/schema.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE Livro (
|
||||||
|
Id BIGINT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||||
|
Isbn VARCHAR(255) NOT NULL,
|
||||||
|
Titulo VARCHAR(512) NOT NULL,
|
||||||
|
Autor TEXT NOT NULL,
|
||||||
|
Genero TEXT NOT NULL,
|
||||||
|
Descricao TEXT NOT NULL,
|
||||||
|
Foto TEXT NOT NULL,
|
||||||
|
Keywords TEXT NOT NULL,
|
||||||
|
Ativo BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
CriadoEm DATETIME NOT NULL,
|
||||||
|
AtualizadoEm DATETIME NOT NULL,
|
||||||
|
|
||||||
|
INDEX (Isbn),
|
||||||
|
INDEX (CriadoEm),
|
||||||
|
FULLTEXT (Titulo, Autor, Genero, Descricao, Keywords)
|
||||||
|
);
|
||||||
Loading…
x
Reference in New Issue
Block a user