0.1 #1
15 changed files with 2394 additions and 3 deletions
1745
Cargo.lock
generated
Normal file
1745
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -4,3 +4,5 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rouille = "3.6.2"
|
||||
postgres = "0.19.10"
|
||||
|
|
8
README.md
Normal file
8
README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Fediverse-Server
|
||||
|
||||
It's a fucking thing.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Everything listed in Cargo.toml.
|
||||
- `postgresql` for your distribution.
|
90
src/API/mod.rs
Normal file
90
src/API/mod.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use postgres::{Client, NoTls};
|
||||
|
||||
pub fn Login(Username: &str, Password: &str) -> Result<Vec<String>, Box<dyn std::error::Error>>{
|
||||
let mut Client = Client::connect("host=/var/run/postgresql,localhost user=postgres password=Password dbname=ActivityPub", NoTls)?;
|
||||
|
||||
let Table = Client.query("SELECT 1
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_TYPE='BASE TABLE'
|
||||
AND TABLE_NAME='person'", &[]);
|
||||
// Check if the table doesn't exists. Or does?
|
||||
match Table {
|
||||
Ok(_) => {
|
||||
// Check if the table exists.
|
||||
if Table?.len() == 0 {
|
||||
Client.batch_execute("
|
||||
CREATE TABLE person (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
)")?;
|
||||
}
|
||||
},
|
||||
Err(_) => ()
|
||||
}
|
||||
if Client.query("SELECT username, password FROM person WHERE username = $1", &[&Username])?.len() != 0 {
|
||||
let mut Response: Vec<String> = Vec::new();
|
||||
if Client.query("SELECT username, password FROM person WHERE password = $1", &[&Password])?.len() != 0 {
|
||||
let Result = Client.query("SELECT username, password FROM person WHERE password = $1", &[&Password])?[0].clone();
|
||||
Response.push(Result.get(0));
|
||||
Response.push(Result.get(1));
|
||||
return Ok(Response);
|
||||
} else {
|
||||
return Ok(vec!["Password Not Correct.".to_string()]);
|
||||
}
|
||||
} else {
|
||||
return Ok(vec!["Username Not Found.".to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn GetAccountWithID(ID: i32) -> Result<String, Box<dyn std::error::Error>>{
|
||||
let mut Client = Client::connect("host=/var/run/postgresql,localhost user=postgres password=Password dbname=ActivityPub", NoTls)?;
|
||||
|
||||
let Table = Client.query("SELECT 1
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_TYPE='BASE TABLE'
|
||||
AND TABLE_NAME='person'", &[]);
|
||||
// Check if the table doesn't exists. Or does?
|
||||
match Table {
|
||||
Ok(_) => {
|
||||
// Check if the table exists.
|
||||
if Table?.len() == 0 {
|
||||
Client.batch_execute("
|
||||
CREATE TABLE person (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
)")?;
|
||||
}
|
||||
},
|
||||
Err(_) => ()
|
||||
}
|
||||
let Result: String = Client.query("SELECT username FROM person WHERE id = $1", &[&ID]).unwrap()[0].get(0);
|
||||
return Ok(Result.to_string());
|
||||
}
|
||||
|
||||
pub fn MakeAccount(User: String, Pass: String) -> Result<String, Box<dyn std::error::Error>>{
|
||||
let mut Client = Client::connect("host=/var/run/postgresql,localhost user=postgres password=Password dbname=ActivityPub", NoTls)?;
|
||||
|
||||
let Table = Client.query("SELECT 1
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_TYPE='BASE TABLE'
|
||||
AND TABLE_NAME='person'", &[]);
|
||||
// Check if the table doesn't exists. Or does?
|
||||
match Table {
|
||||
Ok(_) => {
|
||||
// Check if the table exists.
|
||||
if Table?.len() == 0 {
|
||||
Client.batch_execute("
|
||||
CREATE TABLE person (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
)")?;
|
||||
}
|
||||
},
|
||||
Err(_) => ()
|
||||
}
|
||||
Client.execute("INSERT INTO person (username, password) VALUES ($1, $2)", &[&User, &Pass])?;
|
||||
return Ok("Account Created!".to_string());
|
||||
}
|
103
src/ActivityPub/ActivityTypes/mod.rs
Normal file
103
src/ActivityPub/ActivityTypes/mod.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
pub struct Accept {
|
||||
|
||||
}
|
||||
|
||||
pub struct TentativeAccept {
|
||||
|
||||
}
|
||||
|
||||
pub struct Add {
|
||||
|
||||
}
|
||||
|
||||
pub struct Create {
|
||||
|
||||
}
|
||||
|
||||
pub struct Delete {
|
||||
|
||||
}
|
||||
|
||||
pub struct Ignore {
|
||||
|
||||
}
|
||||
|
||||
pub struct Join {
|
||||
|
||||
}
|
||||
|
||||
pub struct Leave {
|
||||
|
||||
}
|
||||
|
||||
pub struct Like {
|
||||
|
||||
}
|
||||
|
||||
pub struct Offer {
|
||||
|
||||
}
|
||||
|
||||
pub struct Invite {
|
||||
|
||||
}
|
||||
|
||||
pub struct Reject {
|
||||
|
||||
}
|
||||
|
||||
pub struct TentativeReject {
|
||||
|
||||
}
|
||||
|
||||
pub struct Remove {
|
||||
|
||||
}
|
||||
|
||||
pub struct Undo {
|
||||
|
||||
}
|
||||
|
||||
pub struct Update {
|
||||
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
|
||||
}
|
||||
|
||||
pub struct Listen {
|
||||
|
||||
}
|
||||
|
||||
pub struct Read {
|
||||
|
||||
}
|
||||
|
||||
pub struct Move {
|
||||
|
||||
}
|
||||
|
||||
pub struct Travel {
|
||||
|
||||
}
|
||||
|
||||
pub struct Announce {
|
||||
|
||||
}
|
||||
|
||||
pub struct Block {
|
||||
|
||||
}
|
||||
|
||||
pub struct Flag {
|
||||
|
||||
}
|
||||
|
||||
pub struct Dislike {
|
||||
|
||||
}
|
||||
|
||||
pub struct Question {
|
||||
|
||||
}
|
19
src/ActivityPub/ActorTypes/mod.rs
Normal file
19
src/ActivityPub/ActorTypes/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
pub struct Application {
|
||||
|
||||
}
|
||||
|
||||
pub struct Group {
|
||||
|
||||
}
|
||||
|
||||
pub struct Organization {
|
||||
|
||||
}
|
||||
|
||||
pub struct Person {
|
||||
|
||||
}
|
||||
|
||||
pub struct Service {
|
||||
|
||||
}
|
51
src/ActivityPub/ObjectTypes/mod.rs
Normal file
51
src/ActivityPub/ObjectTypes/mod.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
pub struct Relationship {
|
||||
|
||||
}
|
||||
|
||||
pub struct Article {
|
||||
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
|
||||
}
|
||||
|
||||
pub struct Audio {
|
||||
|
||||
}
|
||||
|
||||
pub struct Image {
|
||||
|
||||
}
|
||||
|
||||
pub struct Video {
|
||||
|
||||
}
|
||||
|
||||
pub struct Note {
|
||||
|
||||
}
|
||||
|
||||
pub struct Page {
|
||||
|
||||
}
|
||||
|
||||
pub struct Event {
|
||||
|
||||
}
|
||||
|
||||
pub struct Place {
|
||||
|
||||
}
|
||||
|
||||
pub struct Mention {
|
||||
|
||||
}
|
||||
|
||||
pub struct Profile {
|
||||
|
||||
}
|
||||
|
||||
pub struct Tombstone {
|
||||
|
||||
}
|
170
src/ActivityPub/mod.rs
Normal file
170
src/ActivityPub/mod.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
pub mod ActivityTypes;
|
||||
pub mod ActorTypes;
|
||||
pub mod ObjectTypes;
|
||||
|
||||
use crate::ActivityPub::ActorTypes::Person;
|
||||
|
||||
pub struct Object {
|
||||
r#type: String,
|
||||
id: String,
|
||||
|
||||
attachment: Option<Vec<String>>,
|
||||
attributedTo: Option<Vec<String>>,
|
||||
audience: Option<Vec<String>>,
|
||||
content: Option<String>,
|
||||
context: Option<String>,
|
||||
name: Option<String>,
|
||||
// dateTime format for string.
|
||||
endTime: Option<String>,
|
||||
generator: Option<Vec<String>>,
|
||||
icon: Option<Vec<String>>,
|
||||
image: Option<ObjectTypes::Image>,
|
||||
inReplyTo: Option<ObjectTypes::Note>,
|
||||
location: Option<Vec<String>>,
|
||||
preview: Option<String>,
|
||||
// dateTime format for string.
|
||||
published: Option<String>,
|
||||
replies: Option<Vec<ObjectTypes::Note>>,
|
||||
// dateTime format for string.
|
||||
startTime: Option<String>,
|
||||
summary: Option<String>,
|
||||
tag: Option<Vec<String>>,
|
||||
// dateTime format for string.
|
||||
updated: Option<String>,
|
||||
url: Option<Link>,
|
||||
to: Option<Vec<String>>,
|
||||
bto: Option<Vec<String>>,
|
||||
cc: Option<Vec<String>>,
|
||||
bcc: Option<Vec<String>>,
|
||||
mediaType: Option<String>,
|
||||
// dateTime format for string.
|
||||
duration: Option<String>
|
||||
}
|
||||
|
||||
impl Object {
|
||||
fn new(mut Type: String, ID: String) -> Self {
|
||||
if Type == "" {
|
||||
Type = "Object".to_string();
|
||||
}
|
||||
Self { r#type: Type.to_string(), id: ID, attachment: None, attributedTo: None, audience: None, content: None, context: None, name: None, endTime: None, generator: None, icon: None, image: None, inReplyTo: None, location: None, preview: None, published: None, replies: None, startTime: None, summary: None, tag: None, updated: None, url: None, to: None, bto: None, cc: None, bcc: None, mediaType: None, duration: None}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Link {
|
||||
r#type: String,
|
||||
|
||||
href: Option<String>,
|
||||
// Must meet HTML5 and Web Linking "Link Relation" definitions.
|
||||
rel: Option<Vec<String>>,
|
||||
mediaType: Option<String>,
|
||||
name: Option<String>,
|
||||
// Must meet Language-Tag requirements.
|
||||
hreflang: Option<String>,
|
||||
height: Option<u32>,
|
||||
width: Option<u32>,
|
||||
preview: Option<String>
|
||||
}
|
||||
|
||||
impl Link {
|
||||
fn new(mut Type: String) -> Self {
|
||||
if Type == "" {
|
||||
Type = "Link".to_string();
|
||||
}
|
||||
Self {r#type: Type.to_string(), href: None, rel: None, mediaType: None, name: None, hreflang: None, height: None, width: None, preview: None}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Activity {
|
||||
Inherit: Object,
|
||||
actor: Person,
|
||||
object: Option<Object>,
|
||||
target: Option<Object>,
|
||||
result: Option<String>,
|
||||
origin: Option<Object>,
|
||||
instrument: Option<ActorTypes::Service>
|
||||
}
|
||||
|
||||
impl Activity {
|
||||
fn new(mut Type: String, ID: String, Actor: Person) -> Self {
|
||||
if Type == "" {
|
||||
Type = "Activity".to_string();
|
||||
}
|
||||
Self { Inherit: Object::new(Type, ID), actor: Actor, object: None, target: None, result: None, origin: None, instrument: None}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IntransitiveActivity {
|
||||
// Object should NEVER be used for this class.
|
||||
Inherit: Activity
|
||||
}
|
||||
|
||||
impl IntransitiveActivity {
|
||||
fn new(mut Type: String, ID: String, Actor: Person) -> Self {
|
||||
if Type == "" {
|
||||
Type = "IntransitiveActivity".to_string();
|
||||
}
|
||||
Self {Inherit: Activity::new(Type, ID, Actor)}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Collection {
|
||||
Inherit: Object,
|
||||
|
||||
totalItems: u32,
|
||||
current: Option<String>,
|
||||
first: Option<String>,
|
||||
last: Option<String>,
|
||||
items: Option<Object>
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
fn new(mut Type: String, ID: String, TotalItems: u32) -> Self {
|
||||
if Type == "" {
|
||||
Type = "Collection".to_string();
|
||||
}
|
||||
Self { Inherit: Object::new(Type.to_string(), ID), totalItems: TotalItems, current: None, first: None, last: None, items: None}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OrderedCollection {
|
||||
Inherit: Collection
|
||||
}
|
||||
|
||||
impl OrderedCollection {
|
||||
fn new(mut Type: String, ID: String, TotalItems: u32) -> Self {
|
||||
if Type == "" {
|
||||
Type = "OrderedCollection".to_string();
|
||||
}
|
||||
Self { Inherit: Collection::new(Type.to_string(), ID, TotalItems)}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CollectionPage {
|
||||
Inherit: Collection,
|
||||
partOf: String,
|
||||
next: Option<Link>,
|
||||
prev: Option<Link>
|
||||
}
|
||||
|
||||
impl CollectionPage {
|
||||
fn new(mut Type: String, ID: String, TotalItems: u32, PartOf: String) -> Self {
|
||||
if Type == "" {
|
||||
Type = "CollectionPage".to_string();
|
||||
}
|
||||
Self { Inherit: Collection::new(Type.to_string(), ID, TotalItems), partOf: PartOf, next: None, prev: None }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OrderedCollectionPage {
|
||||
Inherit: CollectionPage,
|
||||
startIndex: Option<u32>
|
||||
}
|
||||
|
||||
impl OrderedCollectionPage {
|
||||
fn new(mut Type: String, ID: String, TotalItems: u32, PartOf: String) -> Self {
|
||||
if Type == "" {
|
||||
Type = "OrderedCollectionPage".to_string();
|
||||
}
|
||||
Self { Inherit: CollectionPage::new(Type.to_string(), ID, TotalItems, PartOf), startIndex: None }
|
||||
}
|
||||
}
|
13
src/HTTP/404.html
Normal file
13
src/HTTP/404.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="/favicon.ico" type="image/webp" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title>Ouch!</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404</h1>
|
||||
<p>Page not found.</p>
|
||||
</body>
|
||||
</html>
|
BIN
src/HTTP/favicon.ico
Normal file
BIN
src/HTTP/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 761 B |
17
src/HTTP/index.html
Normal file
17
src/HTTP/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="/favicon.ico" type="image/webp" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title>Main</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello!</h1>
|
||||
<p>Hi from Rust.</p>
|
||||
<a href="/Profile">View your profile.</a>
|
||||
|
||||
<script src="script.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
32
src/HTTP/profile.html
Normal file
32
src/HTTP/profile.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="/favicon.ico" type="image/webp" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title>Profile</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Profile!</h1>
|
||||
<p id="Account">Log in. Or don't. Up to you.</p>
|
||||
|
||||
<form action="" method="get">
|
||||
<div>
|
||||
<label>Username</label>
|
||||
<input type="text" name="Username" required />
|
||||
</div>
|
||||
<div>
|
||||
<label>Password</label>
|
||||
<input type="text" name="Password" required />
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Log on" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p id="Information"></p>
|
||||
|
||||
<script src="profile.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
28
src/HTTP/profile.js
Normal file
28
src/HTTP/profile.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
let Infomation = document.getElementById("Information");
|
||||
let Account = document.getElementById("Account");
|
||||
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('GET', document.location, true);
|
||||
req.send(null);
|
||||
req.onload = function() {
|
||||
let headers = req.getAllResponseHeaders();
|
||||
const arr = headers.trim().split(/[\r\n]+/);
|
||||
|
||||
// Create a map of header names to values
|
||||
const headerMap = new Map();
|
||||
arr.forEach((line) => {
|
||||
const parts = line.split(": ");
|
||||
headerMap.set(parts[0], parts[1]);
|
||||
});
|
||||
|
||||
if (headerMap.get("profile") != null) {
|
||||
if (headerMap.get("profile") == "Username Not Found.") {
|
||||
Infomation.innerHTML = "User not found. Please check your username and try again!";
|
||||
} else if (headerMap.get("profile") == "Password Not Correct.") {
|
||||
Infomation.innerHTML = "Password not correct. Please check your password and try again!";
|
||||
} else {
|
||||
Account.innerHTML = headerMap.get("profile");
|
||||
Infomation.innerHTML = "Welcome!";
|
||||
}
|
||||
}
|
||||
};
|
53
src/HTTP/style.css
Normal file
53
src/HTTP/style.css
Normal file
|
@ -0,0 +1,53 @@
|
|||
body {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 8ch;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 7ch;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 6ch;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 5ch;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 4ch;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 3ch;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 2ch;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
66
src/main.rs
66
src/main.rs
|
@ -1,5 +1,65 @@
|
|||
use std::net;
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(unused_braces)]
|
||||
use rouille::{post_input, try_or_400, router, Response};
|
||||
use std::fs::File;
|
||||
mod ActivityPub;
|
||||
mod API;
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
fn main(){
|
||||
|
||||
// Never leave the server. CTRL + C if you have issues.
|
||||
rouille::start_server("127.0.0.1:8080", move |Request| {
|
||||
// Router. Go to the correct pages, else hit the sack.
|
||||
router!(Request,
|
||||
// Actual Web Pages.
|
||||
(GET) ["/"] => {
|
||||
Response::from_file("text/html", File::open("src/HTTP/index.html").unwrap()).with_status_code(200)
|
||||
},
|
||||
(GET) ["/Profile"] => {
|
||||
let Username = Request.get_param("Username");
|
||||
let Password = Request.get_param("Password").unwrap();
|
||||
match Username {
|
||||
Some(x) => {
|
||||
let Account: Vec<String> = API::Login(&x, &Password).unwrap();
|
||||
Response::from_file("text/html", File::open("src/HTTP/profile.html").unwrap()).with_status_code(200).with_additional_header("profile", Account[0].clone())
|
||||
},
|
||||
None => {
|
||||
Response::from_file("text/html", File::open("src/HTTP/profile.html").unwrap()).with_status_code(200)
|
||||
}
|
||||
}
|
||||
},
|
||||
// API stuff.
|
||||
(GET) ["/API/Profile:Get"] => {
|
||||
// Content-type: application/x-www-form-urlencoded
|
||||
let Profile = Request.get_param("id").unwrap().parse::<i32>().unwrap();
|
||||
let Things = API::GetAccountWithID(Profile).unwrap();
|
||||
let Text: String = "Got Account: ".to_string() + &Things.to_string();
|
||||
Response::text(Text).with_status_code(200)
|
||||
},
|
||||
(POST) ["/API/Profile:Create"] => {
|
||||
// Content-type: application/x-www-form-urlencoded
|
||||
let Profile = try_or_400!(post_input!(Request, {
|
||||
Username: String,
|
||||
Password: String,
|
||||
}));
|
||||
let Things = API::MakeAccount(Profile.Username.to_string(), Profile.Password.to_string()).unwrap();
|
||||
let Text: String = Things.to_string();
|
||||
Response::text(Text).with_status_code(201)
|
||||
},
|
||||
// Get specific images. Because the browser said so.
|
||||
(GET) ["/favicon.ico"] => {
|
||||
Response::from_file("image/vnd.microsoft.icon", File::open("src/HTTP/favicon.ico").unwrap()).with_status_code(200)
|
||||
},
|
||||
(GET) ["/style.css"] => {
|
||||
Response::from_file("text/css", File::open("src/HTTP/style.css").unwrap()).with_status_code(200)
|
||||
},
|
||||
(GET) ["/profile.js"] => {
|
||||
Response::from_file("text/javascript", File::open("src/HTTP/profile.js").unwrap()).with_status_code(200)
|
||||
},
|
||||
// Catch-all. Fuck you.
|
||||
_ => {
|
||||
Response::from_file("text/html", File::open("src/HTTP/404.html").unwrap()).with_status_code(404)
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue