finish up translations and setup doc server stuff

This commit is contained in:
ngn
2025-01-10 00:16:06 +03:00
parent ac307de76c
commit 5fb3c03e40
30 changed files with 591 additions and 104 deletions

225
doc/.clang-format Normal file
View File

@ -0,0 +1,225 @@
---
Language: Cpp
# BasedOnStyle: LLVM
AccessModifierOffset: -2
AlignAfterOpenBracket: DontAlign
AlignArrayOfStructures: Left
AlignConsecutiveAssignments:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: true
AlignConsecutiveBitFields:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveDeclarations:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignEscapedNewlines: Right
AlignOperands: Align
AlignTrailingComments:
Kind: Always
OverEmptyLines: 0
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: MultiLine
AttributeMacros:
- __capability
BinPackArguments: false
BinPackParameters: true
BitFieldColonSpacing: Both
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterExternBlock: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakAfterAttributes: Never
BreakAfterJavaFieldAnnotations: false
BreakArrays: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: Always
BreakBeforeBraces: Attach
BreakBeforeInlineASMColon: OnlyMultiline
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 1
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '(Test)?$'
IncludeIsMainSourceRegex: ''
IndentAccessModifiers: false
IndentCaseBlocks: false
IndentCaseLabels: false
IndentExternBlock: AfterExternBlock
IndentGotoLabels: true
IndentPPDirectives: None
IndentRequiresClause: true
IndentWidth: 2
IndentWrappedFunctionNames: false
InsertBraces: false
InsertNewlineAtEOF: false
InsertTrailingCommas: None
IntegerLiteralSeparator:
Binary: 0
BinaryMinDigits: 0
Decimal: 0
DecimalMinDigits: 0
Hex: 0
HexMinDigits: 0
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: true
LambdaBodyIndentation: Signature
LineEnding: DeriveLF
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PackConstructorInitializers: BinPack
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakOpenParenthesis: 0
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyIndentedWhitespace: 0
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
PPIndentWidth: -1
QualifierAlignment: Leave
ReferenceAlignment: Pointer
ReflowComments: true
RemoveBracesLLVM: false
RemoveSemicolon: false
RequiresClausePosition: OwnLine
RequiresExpressionIndentation: OuterScope
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SortIncludes: false
SortJavaStaticImport: Before
SortUsingDeclarations: LexicographicNumeric
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceAroundPointerQualifiers: Default
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeParensOptions:
AfterControlStatements: true
AfterForeachMacros: true
AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
AfterIfMacros: true
AfterOverloadedOperator: false
AfterRequiresInClause: false
AfterRequiresInExpression: false
BeforeNonEmptyParentheses: false
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Latest
StatementAttributeLikeMacros:
- Q_EMIT
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseTab: Never
WhitespaceSensitiveMacros:
- BOOST_PP_STRINGIZE
- CF_SWIFT_NAME
- NS_SWIFT_NAME
- PP_STRINGIZE
- STRINGIZE
...

4
doc/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
compile_commands.json
.cache
*.elf
dist

35
doc/Makefile Normal file
View File

@ -0,0 +1,35 @@
# dirs
DIRS = $(shell find src/* -type d)
DISTDIR = dist
OUTDIRS = $(patsubst src/%,$(DISTDIR)/%,$(DIRS))
# sources
HSRCS = $(wildcard inc/*.h)
CSRCS = $(shell find -type f -name '*.c')
OBJS = $(patsubst ./src/%.c,./$(DISTDIR)/%.o,$(CSRCS))
# compiler flags
CFLAGS = -O3 -fstack-protector-strong -fcf-protection=full -fstack-clash-protection
LIBS = -lctorm -lcmark
INCLUDE = -I./inc
all: doc.elf
doc.elf: $(OBJS)
echo $(OBJS) $(OUTDIRS)
gcc $(CFLAGS) -o $@ $^ $(LIBS)
$(DISTDIR)/%.o: src/%.c
@mkdir -pv $(OUTDIRS)
gcc $(CFLAGS) $(INCLUDE) -c -o $@ $^ $(LIBS)
format:
clang-format -i -style=file $(CSRCS) $(HSRCS)
clean:
rm -rf $(DISTDIR)
run:
./doc.elf
.PHONY: format clean run

178
doc/docs/en/api.md Normal file
View File

@ -0,0 +1,178 @@
<!-- This is the markdown file that will be served by the index route -->
<style>
* {
font-family: monospace;
}
</style>
# [{{.api.Host}}]({{.api.String}})
This is the API for my personal website, [{{.frontend.Host}}]({{.frontend.String}}).
It stores information about the self-hosted services I provide and it also allows me
to publish news and updates about these services using an Atom feed. It's written in
Go and uses SQLite for storage. Licensed under GNU GPL version 3.
**Source code and the license is available at**: [https://github.com/ngn13/website](https://github.com/ngn13/website)
**You can report issues to**: [https://github.com/ngn13/website/issues](https://github.com/ngn13/website/issues)
The rest of this document contains documentation for all the available API endpoints.
## Version 1 Endpoints
Each version 1 endpoint, can be accessed using the /v1 route.
All the endpoints return JSON formatted data.
### Errors
If any error occurs, you will get a non-200 response. And the JSON data will have an
"error" key, which will contain information about the error that occured, in the
string format. This is the only JSON key that will be set in non-200 responses.
### Results
If no error occurs, "error" key will be set to an emtpy string (""). If the endpoint
returns any data, this will be stored using the "result" key. The "result" have a
different expected type and a format for each endpoint.
### Multilang
Some "result" formats may use a structure called "Multilang". This is a simple JSON
structure that includes one key for each supported language. The key is named after
the language it represents. Currently only supported languages are:
- English (`en`)
- Turkish (`tr`)
So each multilang structure, will have **at least** one of these keys.
Here is an example multilang structure:
```
{
"en": "Hello, world!",
"tr": "Merhaba, dünya!"
}
```
If a "result" field is using a multilang structure, it will be specified as "Multilang"
in the rest of the documentation.
### Administrator routes
The endpoints under the "/v1/admin" route, are administrator-only routes. To access
these routes you'll need to specfiy and password using the "Authorization" header.
If the password you specify, matches with the password specified using the
`API_PASSWORD` environment variable, you will be able to access the route.
### GET /v1/services
Returns a list of available services. Each service has the following JSON format:
```
{
"name": "Test Service",
"desc": {
"en": "Service used for testing the API",
"tr": "API'ı test etmek için kullanılan servis"
},
"check_time": 1735861944,
"check_res": 1,
"check_url": "http://localhost:7001",
"clear": "http://localhost:7001",
"onion": "",
"i2p": ""
}
```
Where:
- `name`: Service name (string)
- `desc`: Service description (Multilang)
- `check_time`: Last time status check time for the service, set 0 if status checking is
not supported for this service/status checking is disabled (integer, UNIX timestamp)
- `check_res`: Last service status check result (integer)
* 0 if the service is down
* 1 if the service is up
* 2 if the service is up, but slow
* 3 if the service doesn't support status checking/status checking is disabled
- `check_url`: URL used for service's status check (string, empty if none)
- `clear`: Clearnet URL for the service (string, empty string if none)
- `onion`: Onion (TOR) URL for the service (string, empty string if none)
- `i2p`: I2P URL for the service (string, empty string if none)
You can also get information about a specific service by specifying it's name using
a URL query named "name".
### GET /v1/news/:language
Returns a Atom feed of news for the given language. Supports languages that are supported
by Multilang.
### GET /v1/metrics
Returns metrics about the API usage. The metric data has the following format:
```
{
"number":8,
"since":1736294400,
"total":8
}
```
Where:
- `number`: Visitor number of the the current visitor (integer)
- `since`: Metric collection start date (integer, UNIX timestamp)
- `total`: Total number of visitors (integer)
Note that visitor number may change after a certain amount of requests by other clients,
if the client wants to preserve it's visitor number, it should save it somewhere.
### GET /v1/admin/logs
Returns a list of administrator logs. Each log has the following JSON format:
```
{
"action": "Added service \"Test Service\"",
"time": 1735861794
}
```
Where:
- `action`: Action that the administrator performed (string)
- `time`: Time when the administrator action was performed (integer, UNIX timestamp)
Client can get the logs for only a single address, by setting the URL query "addr".
### PUT /v1/admin/service/add
Creates a new service. The request body needs to contain JSON data, and it needs to
have the JSON format used to represent a service. See "/v1/services/all" route to
see this format.
Returns no data on success.
### DELETE /v1/admin/service/del
Deletes a service. The client needs to specify the name of the service to delete, by
setting the URL query "name".
Returns no data on success.
### GET /v1/admin/service/check
Forces a status check for all the services.
Returns no data on success.
### PUT /v1/admin/news/add
Creates a news post. The request body needs to contain JSON data, and it needs
to use the following JSON format:
```
{
"id": "test_news",
"title": {
"en": "Very important news",
"tr": "Çok önemli haber"
},
"author": "ngn",
"content": {
"en": "Just letting you know that I'm testing the API",
"tr": "Sadece API'ı test ettiğimi bilmenizi istedim"
}
}
```
Where:
- `id`: Unique ID for the news post (string)
- `title`: Title for the news post (Multilang)
- `author`: Author of the news post (string)
- `content`: Contents of the news post (Multilang)
Returns no data on success.
### DELETE /v1/admin/news/del
Deletes a news post. The client needs to specify the ID of the news post to delete,
by setting the URL query "id".
Returns no data on success.

18
doc/inc/config.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
typedef struct option {
const char *name;
char *value;
bool required;
} option_t;
typedef struct config {
option_t *options;
int32_t count;
} config_t;
int32_t config_load(config_t *conf);
char *config_get(config_t *conf, const char *name);

4
doc/inc/routes.h Normal file
View File

@ -0,0 +1,4 @@
#pragma once
#include <ctorm/all.h>
void GET_read(req_t *req, res_t *res);

39
doc/src/config.c Normal file
View File

@ -0,0 +1,39 @@
#include <ctorm/log.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "config.h"
option_t options[] = {
{"host", "0.0.0.0:7003", true }, // host the server should listen on
{NULL, NULL, false},
};
int32_t config_load(config_t *conf) {
bzero(conf, sizeof(*conf));
char *value = NULL;
conf->options = options;
for (option_t *opt = conf->options; opt->name != NULL; opt++) {
if ((value = getenv(opt->name)) != NULL)
opt->value = value;
if (opt->required && *opt->value == 0) {
error("please specify a value for the required config option %s", opt->name);
errno = EFAULT;
return -1;
}
conf->count++;
}
return 0;
}
char *config_get(config_t *conf, const char *name) {
for (int32_t i = 0; i < conf->count; i++)
if (strcmp(conf->options[i].name, name) == 0)
return conf->options[i].value;
return NULL;
}

25
doc/src/main.c Normal file
View File

@ -0,0 +1,25 @@
#include <ctorm/all.h>
#include <stdlib.h>
#include "routes.h"
#include "config.h"
int main() {
config_t conf;
app_config_t config;
if (config_load(&conf) < 0)
return EXIT_FAILURE;
app_config_new(&config);
config.disable_logging = true;
app_t *app = app_new(&config);
GET(app, "/read", GET_read);
if (!app_run(app, config_get(&conf, "host")))
error("failed to start the app: %s", app_geterror());
app_free(app);
return EXIT_SUCCESS;
}

118
doc/src/routes/read.c Normal file
View File

@ -0,0 +1,118 @@
#include <linux/limits.h>
#include <ctorm/all.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <cmark.h>
#include <fcntl.h>
#include "routes.h"
void GET_read(req_t *req, res_t *res) {
const char *lang = REQ_QUERY("lang");
const char *name = REQ_QUERY("name");
char fp[PATH_MAX + 1], md[NAME_MAX + 1];
struct dirent *dirent = NULL;
int32_t name_len = 0, ent_len = 0;
DIR *dir = NULL;
bzero(fp, sizeof(fp));
bzero(md, sizeof(md));
if (NULL == name)
return RES_REDIRECT("/");
name_len = strlen(name);
if (NULL == lang)
lang = "en";
if (NULL == (dir = opendir("docs"))) {
error("failed to open the docs dir: %s", strerror(errno));
RES_SENDFILE("html/internal.html");
res->code = 500;
return;
}
while ((dirent = readdir(dir)) != NULL) {
if (strncmp(dirent->d_name, lang, PATH_MAX) == 0)
break;
}
closedir(dir);
if (NULL == dirent) {
RES_SENDFILE("html/notfound.html");
return;
}
snprintf(fp, sizeof(fp), "docs/%s", lang);
if (NULL == (dir = opendir(fp))) {
error("failed to open the language dir: %s", strerror(errno));
RES_SENDFILE("html/internal.html");
res->code = 500;
return;
}
while ((dirent = readdir(dir)) != NULL) {
if ((ent_len = strlen(dirent->d_name) - 3) != name_len)
continue;
if (strncmp(dirent->d_name, name, name_len) == 0) {
memcpy(md, dirent->d_name, ent_len + 3);
break;
}
}
closedir(dir);
if (NULL == dirent) {
RES_SENDFILE("html/notfound.html");
return;
}
char *md_content = NULL, md_fp[PATH_MAX + 1];
struct stat md_st;
int md_fd = 0;
snprintf(md_fp, sizeof(fp), "%s/%s", fp, md);
if ((md_fd = open(md_fp, O_RDONLY)) < 0) {
error("failed to open %s: %s", fp, strerror(errno));
goto err_internal_close;
}
if (fstat(md_fd, &md_st) < 0) {
error("failed to fstat %s: %s", fp, strerror(errno));
goto err_internal_close;
}
if ((md_content = mmap(0, md_st.st_size, PROT_READ, MAP_PRIVATE, md_fd, 0)) == NULL) {
error("failed to mmap %s: %s", fp, strerror(errno));
goto err_internal_close;
}
char *parsed = cmark_markdown_to_html(md_content, md_st.st_size, CMARK_OPT_DEFAULT);
RES_SEND(parsed);
free(parsed);
munmap(md_content, md_st.st_size);
close(md_fd);
return;
err_internal_close:
close(md_fd);
RES_SENDFILE("html/internal.html");
return;
}