Compare commits
20
Commits
f8577dc3dc
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59584f3928 | ||
|
|
77bcf116fb | ||
|
|
41437f0f2f | ||
|
|
1f05960ffc | ||
|
|
ae2879de41 | ||
|
|
efd3916fb4 | ||
|
|
52238f9aaf | ||
|
|
a887d76dd9 | ||
|
|
014186b65a | ||
|
|
e6763a6d4b | ||
|
|
e80cba9110 | ||
|
|
ac5c660583 | ||
|
|
853fda9814 | ||
|
|
aff454c277 | ||
|
|
df2338b6f1 | ||
|
|
ca22cc501e | ||
|
|
49bfccfbd8 | ||
|
|
73a7c8469a | ||
|
|
09e2afe329 | ||
|
|
d1f91e547b |
@@ -0,0 +1,28 @@
|
|||||||
|
name: Build
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: golang:latest
|
||||||
|
options: --network host
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y nodejs npm
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
|
||||||
|
- name: List files in the repository
|
||||||
|
run: |
|
||||||
|
ls ${{ gitea.workspace }}
|
||||||
|
- run: go mod tidy
|
||||||
|
- run: make build
|
||||||
|
- run: ls -lah ${{ gitea.workspace }}
|
||||||
|
- name: Upload Build Artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: alox.protogen
|
||||||
|
path: ./alox.protogen
|
||||||
|
- run: echo "This job's status is ${{ job.status }}."
|
||||||
@@ -1 +1,3 @@
|
|||||||
alox.protogen
|
alox.protogen
|
||||||
|
alox.protogen.exe
|
||||||
|
build/
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.PHONY: build build_win clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
GOOS=linux go build .
|
||||||
|
|
||||||
|
build_win:
|
||||||
|
GOOS=windows go build .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build/*
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -11,22 +14,69 @@ import (
|
|||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var embeddedTemplates embed.FS
|
||||||
|
|
||||||
var appConfig = koanf.New(".")
|
var appConfig = koanf.New(".")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := appConfig.Load(file.Provider("testdata/conf.yaml"), yaml.Parser()); err != nil {
|
confPath := flag.String("c", "testdata/conf.yaml", "Config File")
|
||||||
|
outFileName := flag.String("o", "", "Name of the Output File")
|
||||||
|
inputFile := flag.String("i", "testdata/prot1.json", "Name of the Input File")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if err := appConfig.Load(file.Provider(*confPath), yaml.Parser()); err != nil {
|
||||||
log.Printf("error loading config: %v", err)
|
log.Printf("error loading config: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("CONF: %v", appConfig)
|
log.Printf("CONF: %v", appConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, _ := os.ReadFile("testdata/prot1.json")
|
file, err := os.ReadFile(*inputFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
var cfg proto.Proto
|
var cfg proto.Proto
|
||||||
json.Unmarshal(file, &cfg)
|
json.Unmarshal(file, &cfg)
|
||||||
|
|
||||||
a, err := proto.CreateCStyle(&cfg)
|
// if outFileName is set use it, if is not set set it to default value
|
||||||
|
if *outFileName != "" {
|
||||||
|
cfg.FileName = *outFileName
|
||||||
|
} else {
|
||||||
|
*outFileName = "build/proto"
|
||||||
|
}
|
||||||
|
|
||||||
|
// If filename not set in json take it from cli or default
|
||||||
|
if cfg.FileName != "" {
|
||||||
|
cfg.FileName = *outFileName
|
||||||
|
}
|
||||||
|
|
||||||
|
proto.InitTemplates(embeddedTemplates, "templates/*.tmpl")
|
||||||
|
|
||||||
|
header, err := proto.CreateCStyleHeader(&cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ERROR: %v", err)
|
log.Printf("ERROR: %v", err)
|
||||||
}
|
}
|
||||||
log.Printf("TEMPLATE: \n%v", a)
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(fmt.Sprintf("%s.h", *outFileName), []byte(header), 0666)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
source, err := proto.CreateCStyleSource(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: %v", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(fmt.Sprintf("%s.c", *outFileName), []byte(source), 0666)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-4
@@ -3,6 +3,7 @@ package proto
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"text/template"
|
"text/template"
|
||||||
@@ -27,13 +28,14 @@ type ProtoMessage struct {
|
|||||||
type ProtoMessagePayload struct {
|
type ProtoMessagePayload struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
DataType string `json:"type"`
|
DataType string `json:"type"`
|
||||||
ArrayLength int `json:"array,omitempty"`
|
ArrayLength int `json:"array,omitempty"` // optional falls kein array
|
||||||
}
|
}
|
||||||
|
|
||||||
type Proto struct {
|
type Proto struct {
|
||||||
ProtocolHeader ProtocolHeader `json:"protocol"`
|
ProtocolHeader ProtocolHeader `json:"protocol"`
|
||||||
ESP_TO_PC []ProtoMessage `json:"messages_esp_to_pc"`
|
ESP_TO_PC []ProtoMessage `json:"messages_esp_to_pc"`
|
||||||
PC_TO_ESP []ProtoMessage `json:"messages_pc_to_esp"`
|
PC_TO_ESP []ProtoMessage `json:"messages_pc_to_esp"`
|
||||||
|
FileName string `json:"filename,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HexByte) UnmarshalJSON(data []byte) error {
|
func (h *HexByte) UnmarshalJSON(data []byte) error {
|
||||||
@@ -62,6 +64,7 @@ type PayloadStruct struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CStyleData struct {
|
type CStyleData struct {
|
||||||
|
FileName string
|
||||||
CenumPCTOESP Cenum
|
CenumPCTOESP Cenum
|
||||||
CenumESPTOPC Cenum
|
CenumESPTOPC Cenum
|
||||||
PayloadStructs []PayloadStruct
|
PayloadStructs []PayloadStruct
|
||||||
@@ -69,16 +72,22 @@ type CStyleData struct {
|
|||||||
MessagesESPtoPC []ProtoMessage
|
MessagesESPtoPC []ProtoMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateCStyle(proto *Proto) (string, error) {
|
var tmpl *template.Template
|
||||||
|
|
||||||
|
func InitTemplates(fsys fs.FS, pattern string) {
|
||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"snake": toSnakeCase,
|
"snake": toSnakeCase,
|
||||||
"isLast": isLast,
|
"isLast": isLast,
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := template.New("all").Funcs(funcs).ParseGlob("templates/*.tmpl")
|
Ntmpl, err := template.New("all").Funcs(funcs).ParseFS(fsys, pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to parse templates: %v", err)
|
log.Fatalf("failed to parse templates: %v", err)
|
||||||
}
|
}
|
||||||
|
tmpl = Ntmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCStyleHeader(proto *Proto) (string, error) {
|
||||||
|
|
||||||
payloads := []PayloadStruct{}
|
payloads := []PayloadStruct{}
|
||||||
|
|
||||||
@@ -99,6 +108,7 @@ func CreateCStyle(proto *Proto) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var data = CStyleData{
|
var data = CStyleData{
|
||||||
|
FileName: proto.FileName,
|
||||||
CenumPCTOESP: Cenum{
|
CenumPCTOESP: Cenum{
|
||||||
EnumName: "PC_TO_ESP_MESSAGE_IDS",
|
EnumName: "PC_TO_ESP_MESSAGE_IDS",
|
||||||
Entries: proto.PC_TO_ESP,
|
Entries: proto.PC_TO_ESP,
|
||||||
@@ -113,7 +123,51 @@ func CreateCStyle(proto *Proto) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = tmpl.ExecuteTemplate(&buf, "cpart", data)
|
err := tmpl.ExecuteTemplate(&buf, "cpartHeader", data)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error Rendering template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCStyleSource(proto *Proto) (string, error) {
|
||||||
|
|
||||||
|
payloads := []PayloadStruct{}
|
||||||
|
|
||||||
|
for _, payload := range proto.PC_TO_ESP {
|
||||||
|
tmp := PayloadStruct{
|
||||||
|
Name: payload.Name,
|
||||||
|
Entries: payload.Payload,
|
||||||
|
}
|
||||||
|
payloads = append(payloads, tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, payload := range proto.ESP_TO_PC {
|
||||||
|
tmp := PayloadStruct{
|
||||||
|
Name: payload.Name,
|
||||||
|
Entries: payload.Payload,
|
||||||
|
}
|
||||||
|
payloads = append(payloads, tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = CStyleData{
|
||||||
|
FileName: proto.FileName,
|
||||||
|
CenumPCTOESP: Cenum{
|
||||||
|
EnumName: "PC_TO_ESP_MESSAGE_IDS",
|
||||||
|
Entries: proto.PC_TO_ESP,
|
||||||
|
},
|
||||||
|
CenumESPTOPC: Cenum{
|
||||||
|
EnumName: "ESP_TO_PC_MESSAGE_IDS",
|
||||||
|
Entries: proto.ESP_TO_PC,
|
||||||
|
},
|
||||||
|
PayloadStructs: payloads,
|
||||||
|
MessagesPCtoESP: proto.PC_TO_ESP,
|
||||||
|
MessagesESPtoPC: proto.ESP_TO_PC,
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := tmpl.ExecuteTemplate(&buf, "cpartSource", data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error Rendering template: %v", err)
|
log.Printf("Error Rendering template: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ Each message is described as:
|
|||||||
- [x] Hook-Funktionen
|
- [x] Hook-Funktionen
|
||||||
- [x] Message-Dispatcher (Switch-Case)
|
- [x] Message-Dispatcher (Switch-Case)
|
||||||
- [x] Send-Funktionen
|
- [x] Send-Funktionen
|
||||||
- [ ] Generate Header and Source File
|
- [x] Generate Header and Source File
|
||||||
---
|
---
|
||||||
|
|
||||||
## To-Do
|
## To-Do
|
||||||
@@ -119,7 +119,7 @@ Each message is described as:
|
|||||||
### Tooling
|
### Tooling
|
||||||
- [ ] CLI interface (`--input`, `--output`, `--language`, ...)
|
- [ ] CLI interface (`--input`, `--output`, `--language`, ...)
|
||||||
- [ ] Logging / verbose mode
|
- [ ] Logging / verbose mode
|
||||||
- [ ] Build script / Makefile
|
- [x] Build script / Makefile
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
- [ ] Unit tests for generator
|
- [ ] Unit tests for generator
|
||||||
|
|||||||
+55
-20
@@ -3,7 +3,7 @@ typedef enum {
|
|||||||
{{- range .Entries }}
|
{{- range .Entries }}
|
||||||
{{ .Name }} = {{ printf "0x%X" .ID }},
|
{{ .Name }} = {{ printf "0x%X" .ID }},
|
||||||
{{- end }}
|
{{- end }}
|
||||||
}{{.EnumName}};
|
} {{.EnumName}};
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "payloads"}}
|
{{define "payloads"}}
|
||||||
@@ -19,26 +19,42 @@ typedef struct {
|
|||||||
{{.DataType}} {{.Name}};
|
{{.DataType}} {{.Name}};
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
}{{ .Name }}Payload;
|
} {{ .Name }}Payload;
|
||||||
{{ end }}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
{{define "union"}}
|
{{define "union"}}
|
||||||
typedef union {
|
typedef union {
|
||||||
{{- range .}}
|
{{- range .}}
|
||||||
{{.Name}} {{ .Name | snake}}
|
{{.Name}}Payload {{ .Name | snake}};
|
||||||
{{- end }}
|
{{- end }}
|
||||||
}PayloadUnion:
|
} PayloadUnion;
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "basemessage"}}
|
||||||
|
typedef struct {
|
||||||
|
uint8_t Version;
|
||||||
|
{{.CenumPCTOESP.EnumName}} MessageID;
|
||||||
|
uint8_t Length;
|
||||||
|
PayloadUnion Payload;
|
||||||
|
} PCTOESPBaseMessage;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t Version;
|
||||||
|
{{.CenumESPTOPC.EnumName}} MessageID;
|
||||||
|
uint8_t Length;
|
||||||
|
PayloadUnion Payload;
|
||||||
|
} ESPTOPCBaseMessage;
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "handler"}}
|
{{define "handler"}}
|
||||||
{{- range . }}
|
{{- range . }}
|
||||||
void (*on_{{ .Name | snake }})({{ .Name }}Payload*) = NULL;
|
void (*on_{{ .Name | snake }})({{ .Name }}Payload*);
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "dispatcher"}}
|
{{define "dispatcher"}}
|
||||||
void dispatch_message(uint8_t msg_id, void* payload) {
|
void dispatch_message(uint8_t msg_id, void *payload) {
|
||||||
switch (msg_id) {
|
switch (msg_id) {
|
||||||
{{- range . }}
|
{{- range . }}
|
||||||
case {{ .Name }}:
|
case {{ .Name }}:
|
||||||
@@ -52,19 +68,38 @@ void dispatch_message(uint8_t msg_id, void* payload) {
|
|||||||
}
|
}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "send_functions"}}
|
{{define "send_function_prototype"}}
|
||||||
|
void send_message(ESP_TO_PC_MESSAGE_IDS msgid, PayloadUnion *payload);
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "send_function_source"}}
|
||||||
|
void send_message(ESP_TO_PC_MESSAGE_IDS msgid, PayloadUnion *payload) {}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "send_functions_prototype"}}
|
||||||
{{- range .}}
|
{{- range .}}
|
||||||
{{ $argLen := len .Payload }}
|
{{- $argLen := len .Payload }}
|
||||||
void send_{{.Name | snake}}({{- range $i, $p := .Payload}}{{$p.DataType}} {{$p.Name}} {{- if not (isLast $argLen $i) }}, {{ end}} {{- end}}) {
|
void send_{{.Name | snake}}({{- range $i, $p := .Payload}}{{$p.DataType}} {{ if .ArrayLength}}*{{end}}{{$p.Name}} {{- if not (isLast $argLen $i) }}, {{ end}} {{- end}});
|
||||||
{{.Name}}Payload payload;
|
|
||||||
|
|
||||||
// Payload-Daten zuweisen
|
|
||||||
{{- range .Payload}}
|
|
||||||
payload.{{.Name}} = {{.Name}};
|
|
||||||
{{- end}}
|
|
||||||
|
|
||||||
// Nachricht senden
|
|
||||||
send_message({{.Name}}, &payload);
|
|
||||||
}
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "send_functions_source"}}
|
||||||
|
{{- range .}}
|
||||||
|
{{- $argLen := len .Payload }}
|
||||||
|
void send_{{.Name | snake}}({{- range $i, $p := .Payload}}{{$p.DataType}} {{ if .ArrayLength}}*{{end}}{{$p.Name}} {{ if not (isLast $argLen $i) }}, {{ end}} {{- end}}) {
|
||||||
|
{{.Name}}Payload payload;
|
||||||
|
|
||||||
|
// Payload-Daten zuweisen
|
||||||
|
{{- range .Payload}}
|
||||||
|
{{- if .ArrayLength }}
|
||||||
|
memcpy(payload.{{.Name}}, {{.Name}}, {{.ArrayLength}});
|
||||||
|
{{- else }}
|
||||||
|
payload.{{.Name}} = {{.Name}};
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
// Nachricht senden
|
||||||
|
send_message({{.Name}}, (PayloadUnion *)&payload);
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
{{- define "cpart" -}}
|
|
||||||
// AUTO GENERATED DO NOT EDIT!!!
|
|
||||||
|
|
||||||
#ifndef _PROTO_HEADER
|
|
||||||
#define _PROTO_HEADER
|
|
||||||
|
|
||||||
{{ block "cenum" .CenumPCTOESP }} {{ end }}
|
|
||||||
{{ block "cenum" .CenumESPTOPC }} {{ end }}
|
|
||||||
|
|
||||||
{{ block "payloads" .PayloadStructs }} {{ end }}
|
|
||||||
|
|
||||||
{{ block "union" .PayloadStructs }} {{ end }}
|
|
||||||
|
|
||||||
{{ block "handler" .MessagesPCtoESP}} {{ end }}
|
|
||||||
|
|
||||||
{{ block "dispatcher" .MessagesPCtoESP}} {{ end }}
|
|
||||||
|
|
||||||
{{ block "send_functions" .MessagesESPtoPC}} {{ end }}
|
|
||||||
#endif
|
|
||||||
{{- end -}}
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{{- define "cpartHeader" -}}
|
||||||
|
// AUTO GENERATED DO NOT EDIT!!!
|
||||||
|
|
||||||
|
#ifndef _PROTO_HEADER
|
||||||
|
#define _PROTO_HEADER
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// MessageIDs
|
||||||
|
{{- block "cenum" .CenumPCTOESP }} {{- end}}
|
||||||
|
{{- block "cenum" .CenumESPTOPC }} {{- end}}
|
||||||
|
|
||||||
|
// Payloads for single Messages
|
||||||
|
{{- block "payloads" .PayloadStructs }} {{- end}}
|
||||||
|
|
||||||
|
// Union for all the Payloads
|
||||||
|
{{- block "union" .PayloadStructs }} {{- end}}
|
||||||
|
|
||||||
|
// Base Message that can hold all Payloads
|
||||||
|
{{- block "basemessage" . }} {{- end}}
|
||||||
|
|
||||||
|
// Generic Send Function Prototype
|
||||||
|
{{- block "send_function_prototype" .MessagesESPtoPC}} {{- end}}
|
||||||
|
|
||||||
|
// Spezific Send Functions Prototype
|
||||||
|
{{- block "send_functions_prototype" .MessagesESPtoPC}} {{- end}}
|
||||||
|
|
||||||
|
// Prototypes for Message Recieve Handler to be set in user code
|
||||||
|
{{- block "handler" .MessagesPCtoESP}} {{ end }}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{{- define "cpartSource" -}}
|
||||||
|
// AUTO GENERATED DO NOT EDIT!!!
|
||||||
|
|
||||||
|
#include "{{.FileName}}.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// Message Dispatcher
|
||||||
|
{{- block "dispatcher" .MessagesPCtoESP}} {{ end }}
|
||||||
|
|
||||||
|
//Generic Send Function
|
||||||
|
{{- block "send_function_source" .MessagesESPtoPC}} {{ end }}
|
||||||
|
|
||||||
|
// Sepzific Send Functions
|
||||||
|
{{- block "send_functions_source" .MessagesESPtoPC}} {{ end }}
|
||||||
|
{{- end -}}
|
||||||
Reference in New Issue
Block a user