Compare commits

...

31 Commits

Author SHA1 Message Date
59c93d5d47 welcome update 2026-03-13 15:35:24 +03:00
0adad329dc major voice updates, added voice stresser, minor updates 2026-03-13 15:08:44 +03:00
24e8340b88 add presence management and status preference handling for users 2026-03-12 12:54:14 +03:00
029448292e room invites updates 2026-03-11 18:25:27 +03:00
dfa99fad23 room invites updates 2026-03-11 18:11:01 +03:00
d055a75fd3 update friend request handling and room invite events 2026-03-11 17:34:33 +03:00
512b5ec57e add link previews, search improvements 2026-03-10 01:19:39 +03:00
368422e21c add support for message attachments in SendDMRequest and update handling in service 2026-03-05 11:21:55 +03:00
1bda3a8a81 refactor retransmit buffer handling to support multiple SSRCs 2026-02-26 21:29:41 +03:00
2a05884836 refactor UDP voice handling 2026-02-26 14:27:54 +03:00
f382823177 add support for quality report handling in server and pool 2026-02-26 00:52:02 +03:00
966d6daa0e add support for message attachments in SendMessageRequest and update handling in service 2026-02-21 12:58:11 +03:00
e27536d700 update storage configuration 2026-02-21 12:08:23 +03:00
060cac710c refactor server configuration and update Dockerfile port exposure 2026-02-20 21:32:10 +03:00
78fcafdda6 updated compose 2026-02-20 21:19:43 +03:00
2bb7860cc4 add version endpoint and improve version handling in logs and health checks 2026-02-20 20:33:39 +03:00
162bc58b3a updated registar 2026-02-20 19:14:49 +03:00
97f3ed15fb update docker-compose and Makefile for local-only port exposure and project naming 2026-02-20 18:56:28 +03:00
ce5e66d446 add protobuf well-known types to gen_proto.sh and update include paths 2026-02-20 18:50:20 +03:00
e2c5c1a422 update Dockerfiles to streamline build process and install necessary dependencies 2026-02-20 18:45:13 +03:00
759486ecc7 refactor Makefile to streamline Docker commands and add new stack management targets 2026-02-20 18:34:58 +03:00
c2b590b21e update docker-compose and add Nginx configuration for SSL and proxying 2026-02-20 18:23:28 +03:00
0e062b2155 add avatar upload and management features; include avatar history and thumbnail support 2026-02-20 16:05:29 +03:00
e967eae271 update user bio and room properties handling; add DM message and typing indicators support 2026-02-17 19:00:09 +03:00
9b2d0172c5 major voice and api updates 2025-12-24 02:29:58 +03:00
81634d43b4 update gitingore 2025-12-01 20:55:09 +03:00
033b58d285 add user status and room invite features 2025-12-01 20:52:55 +03:00
aba6b0c03b implement friends system with friend requests and blocking features 2025-11-10 00:17:58 +03:00
daee1d12c3 update ci 2025-10-30 21:21:01 +03:00
e1749aae8e update ci 2025-10-30 19:48:22 +03:00
8cc297a6be update ci 2025-10-30 19:36:56 +03:00
113 changed files with 16072 additions and 2292 deletions

View File

@ -78,3 +78,12 @@ HEALTH_PATH=/health
# Voice health port
VOICE_HEALTH_PORT=8082
# Storage Configuration
STORAGE_PATH=./uploads
STORAGE_URL=http://localhost:8080/files
# Rate Limiting (updated)
RATE_LIMIT_ENABLED=true
RATE_LIMIT_REQUESTS_PER_MINUTE=120
RATE_LIMIT_BURST=20

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
uploads
# Binaries
/bin/
*.exe

View File

@ -1,7 +1,20 @@
.PHONY: proto build clean test test-unit test-integration run-api run-voice migrate docker-build lint deps all test-cleanup
.PHONY: proto build clean clean-proto-deps test test-unit test-integration test-coverage test-cleanup \
run-api run-voice lint deps install-tools all \
up down restart rebuild ps logs logs-api logs-voice logs-db logs-redis \
update pull images
ifneq (,$(wildcard ./.env))
include .env
export
endif
COMPOSE_FILE := deploy/docker-compose.yml
ENV_FILE := .env
PROJECT := concord
DC := docker compose -p $(PROJECT) --env-file $(ENV_FILE) -f $(COMPOSE_FILE)
proto:
@bash scripts/gen_proto.sh
@sh scripts/gen_proto.sh
build:
@echo "Building concord-api..."
@ -12,6 +25,9 @@ build:
clean:
@rm -rf bin/ api/gen/
clean-proto-deps:
@rm -rf api/proto-deps/
test-cleanup:
@echo "Resetting test database..."
@PGPASSWORD=postgres psql -h localhost -U postgres -d postgres -c "DROP DATABASE IF EXISTS concord_test;" 2>/dev/null || true
@ -36,10 +52,6 @@ run-api:
run-voice:
@go run ./cmd/concord-voice
docker-build:
@docker build -f deploy/Dockerfile.api -t concord-api:latest .
@docker build -f deploy/Dockerfile.voice -t concord-voice:latest .
lint:
@golangci-lint run ./...
@ -52,5 +64,57 @@ install-tools:
@go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
@go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
@go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
@go install github.com/bufbuild/buf/cmd/buf@v1.28.1
all: proto build
all: deps proto build
up:
@echo "Starting docker stack..."
@$(DC) up -d
@$(DC) ps
down:
@echo "Stopping docker stack..."
@$(DC) down
restart:
@echo "Restarting docker stack..."
@$(DC) restart
@$(DC) ps
rebuild:
@echo "Rebuilding and restarting docker stack..."
@$(DC) up -d --build --force-recreate
@$(DC) ps
ps:
@$(DC) ps
logs:
@$(DC) logs -f --tail=200
logs-api:
@$(DC) logs -f --tail=200 api
logs-voice:
@$(DC) logs -f --tail=200 voice
logs-db:
@$(DC) logs -f --tail=200 postgres
logs-redis:
@$(DC) logs -f --tail=200 redis
pull:
@echo "Pulling images..."
@$(DC) pull
images:
@$(DC) build
update:
@echo "Updating from git and redeploying..."
@git pull
@$(DC) up -d --build --force-recreate
@$(DC) ps

218
README.md

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,8 @@ package concord.admin.v1;
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/admin/v1;adminv1";
import "google/api/annotations.proto";
message KickRequest {
string room_id = 1;
string user_id = 2;
@ -24,7 +26,22 @@ message MuteRequest {
message EmptyResponse {}
service AdminService {
rpc Kick(KickRequest) returns (EmptyResponse);
rpc Ban(BanRequest) returns (EmptyResponse);
rpc Mute(MuteRequest) returns (EmptyResponse);
rpc Kick(KickRequest) returns (EmptyResponse) {
option (google.api.http) = {
post: "/v1/rooms/{room_id}/kick"
body: "*"
};
}
rpc Ban(BanRequest) returns (EmptyResponse) {
option (google.api.http) = {
post: "/v1/rooms/{room_id}/ban"
body: "*"
};
}
rpc Mute(MuteRequest) returns (EmptyResponse) {
option (google.api.http) = {
post: "/v1/rooms/{room_id}/mute"
body: "*"
};
}
}

View File

@ -4,6 +4,8 @@ package concord.auth.v1;
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/auth/v1;authv1";
import "google/api/annotations.proto";
message LoginPasswordRequest {
string handle = 1;
string password = 2;
@ -49,10 +51,40 @@ message RegisterRequest {
message EmptyResponse {}
service AuthService {
rpc Register(RegisterRequest) returns (Token);
rpc LoginPassword(LoginPasswordRequest) returns (Token);
rpc LoginOAuth(LoginOAuthRequest) returns (Token);
rpc OAuthBegin(OAuthBeginRequest) returns (OAuthBeginResponse);
rpc Refresh(RefreshRequest) returns (Token);
rpc Logout(LogoutRequest) returns (EmptyResponse);
rpc Register(RegisterRequest) returns (Token) {
option (google.api.http) = {
post: "/v1/auth/register"
body: "*"
};
}
rpc LoginPassword(LoginPasswordRequest) returns (Token) {
option (google.api.http) = {
post: "/v1/auth/login"
body: "*"
};
}
rpc LoginOAuth(LoginOAuthRequest) returns (Token) {
option (google.api.http) = {
post: "/v1/auth/oauth"
body: "*"
};
}
rpc OAuthBegin(OAuthBeginRequest) returns (OAuthBeginResponse) {
option (google.api.http) = {
post: "/v1/auth/oauth/begin"
body: "*"
};
}
rpc Refresh(RefreshRequest) returns (Token) {
option (google.api.http) = {
post: "/v1/auth/refresh"
body: "*"
};
}
rpc Logout(LogoutRequest) returns (EmptyResponse) {
option (google.api.http) = {
post: "/v1/auth/logout"
body: "*"
};
}
}

View File

@ -12,3 +12,9 @@ plugins:
out: gen/go
opt:
- paths=source_relative
- plugin: buf.build/grpc-ecosystem/openapiv2
out: gen/openapiv2
opt:
- allow_merge=true
- merge_file_name=concord
- openapi_naming_strategy=fqn

View File

@ -2,6 +2,7 @@ version: v1
name: buf.build/alexander-d-karpov/concord
deps:
- buf.build/googleapis/googleapis
- buf.build/grpc-ecosystem/grpc-gateway
breaking:
use:
- FILE

View File

@ -4,9 +4,13 @@ package concord.call.v1;
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/call/v1;callv1";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
message JoinVoiceRequest {
string room_id = 1;
bool audio_only = 2;
string preferred_region = 3;
}
message UdpEndpoint {
@ -33,6 +37,7 @@ message JoinVoiceResponse {
CodecHint codec = 4;
CryptoSuite crypto = 5;
repeated Participant participants = 6;
uint32 screen_ssrc = 7;
}
message Participant {
@ -40,6 +45,9 @@ message Participant {
uint32 ssrc = 2;
bool muted = 3;
bool video_enabled = 4;
bool screen_sharing = 5;
uint32 video_ssrc = 6;
uint32 screen_ssrc = 7;
}
message LeaveVoiceRequest {
@ -50,13 +58,51 @@ message SetMediaPrefsRequest {
string room_id = 1;
bool audio_only = 2;
bool video_enabled = 3;
bool muted = 4;
bool screen_sharing = 4;
bool muted = 5;
}
message EmptyResponse {}
service CallService {
rpc JoinVoice(JoinVoiceRequest) returns (JoinVoiceResponse);
rpc LeaveVoice(LeaveVoiceRequest) returns (EmptyResponse);
rpc SetMediaPrefs(SetMediaPrefsRequest) returns (EmptyResponse);
message GetVoiceStatusRequest {
string room_id = 1;
}
message VoiceParticipant {
string user_id = 1;
bool muted = 2;
bool video_enabled = 3;
bool screen_sharing = 4;
bool speaking = 5;
google.protobuf.Timestamp joined_at = 6;
}
message GetVoiceStatusResponse {
repeated VoiceParticipant participants = 1;
int32 total_participants = 2;
}
service CallService {
rpc JoinVoice(JoinVoiceRequest) returns (JoinVoiceResponse) {
option (google.api.http) = {
post: "/v1/rooms/{room_id}/voice/join"
body: "*"
};
}
rpc LeaveVoice(LeaveVoiceRequest) returns (EmptyResponse) {
option (google.api.http) = {
post: "/v1/rooms/{room_id}/voice/leave"
};
}
rpc SetMediaPrefs(SetMediaPrefsRequest) returns (EmptyResponse) {
option (google.api.http) = {
put: "/v1/rooms/{room_id}/voice/prefs"
body: "*"
};
}
rpc GetVoiceStatus(GetVoiceStatusRequest) returns (GetVoiceStatusResponse) {
option (google.api.http) = {
get: "/v1/rooms/{room_id}/voice"
};
}
}

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,17 @@ message User {
google.protobuf.Timestamp created_at = 5;
string status = 6;
string bio = 7;
string avatar_thumbnail_url = 8;
string status_preference = 9;
}
message AvatarEntry {
string id = 1;
string user_id = 2;
string full_url = 3;
string thumbnail_url = 4;
string original_filename = 5;
google.protobuf.Timestamp created_at = 6;
}
message Room {
@ -40,6 +51,8 @@ message Member {
Role role = 3;
google.protobuf.Timestamp joined_at = 4;
string nickname = 5;
string status = 6;
int64 last_read_message_id = 7;
}
message VoiceServer {

245
api/proto/dm/v1/dm.proto Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,9 @@ package concord.registry.v1;
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/registry/v1;registryv1";
import "common/v1/types.proto";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "common/v1/types.proto";
message RegisterServerRequest {
concord.common.v1.VoiceServer server = 1;
@ -37,7 +38,21 @@ message ListServersResponse {
message EmptyResponse {}
service RegistryService {
rpc RegisterServer(RegisterServerRequest) returns (RegisterServerResponse);
rpc Heartbeat(HeartbeatRequest) returns (EmptyResponse);
rpc ListServers(ListServersRequest) returns (ListServersResponse);
rpc RegisterServer(RegisterServerRequest) returns (RegisterServerResponse) {
option (google.api.http) = {
post: "/v1/registry/servers"
body: "*"
};
}
rpc Heartbeat(HeartbeatRequest) returns (EmptyResponse) {
option (google.api.http) = {
post: "/v1/registry/heartbeat"
body: "*"
};
}
rpc ListServers(ListServersRequest) returns (ListServersResponse) {
option (google.api.http) = {
get: "/v1/registry/servers"
};
}
}

View File

@ -4,6 +4,8 @@ package concord.rooms.v1;
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/rooms/v1;roomsv1";
import "google/api/annotations.proto";
import "google/protobuf/wrappers.proto";
import "common/v1/types.proto";
message CreateRoomRequest {
@ -20,9 +22,9 @@ message GetRoomRequest {
message UpdateRoomRequest {
string room_id = 1;
string name = 2;
string description = 3;
bool is_private = 4;
google.protobuf.StringValue name = 2;
google.protobuf.StringValue description = 3;
google.protobuf.BoolValue is_private = 4;
}
message ListRoomsForUserRequest {}
@ -43,10 +45,37 @@ message DeleteRoomRequest {
message EmptyResponse {}
service RoomsService {
rpc CreateRoom(CreateRoomRequest) returns (concord.common.v1.Room);
rpc GetRoom(GetRoomRequest) returns (concord.common.v1.Room);
rpc UpdateRoom(UpdateRoomRequest) returns (concord.common.v1.Room);
rpc ListRoomsForUser(ListRoomsForUserRequest) returns (ListRoomsForUserResponse);
rpc AttachVoiceServer(AttachVoiceServerRequest) returns (concord.common.v1.Room);
rpc DeleteRoom(DeleteRoomRequest) returns (EmptyResponse);
rpc CreateRoom(CreateRoomRequest) returns (concord.common.v1.Room) {
option (google.api.http) = {
post: "/v1/rooms"
body: "*"
};
}
rpc GetRoom(GetRoomRequest) returns (concord.common.v1.Room) {
option (google.api.http) = {
get: "/v1/rooms/{room_id}"
};
}
rpc UpdateRoom(UpdateRoomRequest) returns (concord.common.v1.Room) {
option (google.api.http) = {
patch: "/v1/rooms/{room_id}"
body: "*"
};
}
rpc ListRoomsForUser(ListRoomsForUserRequest) returns (ListRoomsForUserResponse) {
option (google.api.http) = {
get: "/v1/rooms"
};
}
rpc AttachVoiceServer(AttachVoiceServerRequest) returns (concord.common.v1.Room) {
option (google.api.http) = {
post: "/v1/rooms/{room_id}/voice-server"
body: "*"
};
}
rpc DeleteRoom(DeleteRoomRequest) returns (EmptyResponse) {
option (google.api.http) = {
delete: "/v1/rooms/{room_id}"
};
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
syntax = "proto3";
package concord.unfurl.v1;
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/unfurl/v1;unfurlv1";
import "google/api/annotations.proto";
message UnfurlRequest {
string url = 1;
}
message UnfurlResponse {
string url = 1;
string title = 2;
string description = 3;
string image = 4;
string site_name = 5;
string favicon = 6;
}
service UnfurlService {
rpc Unfurl(UnfurlRequest) returns (UnfurlResponse) {
option (google.api.http) = {
get: "/v1/unfurl"
};
}
}

View File

@ -4,6 +4,7 @@ package concord.users.v1;
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/users/v1;usersv1";
import "google/api/annotations.proto";
import "common/v1/types.proto";
message GetSelfRequest {}
@ -46,12 +47,84 @@ message ListUsersByIDsResponse {
repeated concord.common.v1.User users = 1;
}
service UsersService {
rpc GetSelf(GetSelfRequest) returns (concord.common.v1.User);
rpc GetUser(GetUserRequest) returns (concord.common.v1.User);
rpc GetUserByHandle(GetUserByHandleRequest) returns (concord.common.v1.User);
rpc UpdateProfile(UpdateProfileRequest) returns (concord.common.v1.User);
rpc UpdateStatus(UpdateStatusRequest) returns (concord.common.v1.User);
rpc SearchUsers(SearchUsersRequest) returns (SearchUsersResponse);
rpc ListUsersByIDs(ListUsersByIDsRequest) returns (ListUsersByIDsResponse);
message UploadAvatarRequest {
bytes image_data = 1;
string filename = 2;
}
message UploadAvatarResponse {
string avatar_url = 1;
string thumbnail_url = 2;
concord.common.v1.AvatarEntry avatar = 3;
}
message DeleteAvatarRequest {
string avatar_id = 1;
}
message DeleteAvatarResponse {}
message GetAvatarHistoryRequest {
string user_id = 1;
}
message GetAvatarHistoryResponse {
repeated concord.common.v1.AvatarEntry avatars = 1;
}
service UsersService {
rpc GetSelf(GetSelfRequest) returns (concord.common.v1.User) {
option (google.api.http) = {
get: "/v1/users/me"
};
}
rpc GetUser(GetUserRequest) returns (concord.common.v1.User) {
option (google.api.http) = {
get: "/v1/users/{user_id}"
};
}
rpc GetUserByHandle(GetUserByHandleRequest) returns (concord.common.v1.User) {
option (google.api.http) = {
get: "/v1/users/handle/{handle}"
};
}
rpc UpdateProfile(UpdateProfileRequest) returns (concord.common.v1.User) {
option (google.api.http) = {
patch: "/v1/users/me"
body: "*"
};
}
rpc UpdateStatus(UpdateStatusRequest) returns (concord.common.v1.User) {
option (google.api.http) = {
put: "/v1/users/me/status"
body: "*"
};
}
rpc SearchUsers(SearchUsersRequest) returns (SearchUsersResponse) {
option (google.api.http) = {
get: "/v1/users/search"
};
}
rpc ListUsersByIDs(ListUsersByIDsRequest) returns (ListUsersByIDsResponse) {
option (google.api.http) = {
post: "/v1/users/batch"
body: "*"
};
}
rpc UploadAvatar(UploadAvatarRequest) returns (UploadAvatarResponse) {
option (google.api.http) = {
post: "/v1/users/me/avatar"
body: "*"
};
}
rpc DeleteAvatar(DeleteAvatarRequest) returns (DeleteAvatarResponse) {
option (google.api.http) = {
delete: "/v1/users/me/avatar/{avatar_id}"
};
}
rpc GetAvatarHistory(GetAvatarHistoryRequest) returns (GetAvatarHistoryResponse) {
option (google.api.http) = {
get: "/v1/users/{user_id}/avatars"
};
}
}

File diff suppressed because it is too large Load Diff

134
cmd/concord-cli/main.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,16 @@
FROM golang:1.23-alpine AS builder
# Build stage
FROM golang:1.25-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git make protobuf-dev
RUN apk add --no-cache git make protobuf
WORKDIR /build
ENV PATH="/go/bin:${PATH}"
RUN CGO_ENABLED=0 go install github.com/bufbuild/buf/cmd/buf@v1.28.1 \
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest \
&& go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest \
&& go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
COPY go.mod go.sum ./
RUN go mod download
@ -10,22 +18,16 @@ RUN go mod download
COPY . .
RUN make proto
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o concord-api ./cmd/concord-api
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
RUN go build -o /bin/concord-api ./cmd/concord-api
# Final stage
FROM alpine:3.19
WORKDIR /app
RUN apk add --no-cache ca-certificates
COPY --from=builder /build/concord-api .
COPY --from=builder /bin/concord-api /app/concord-api
COPY --from=builder /app/api/gen/openapiv2/ /app/api/gen/openapiv2/
RUN addgroup -g 1000 concord && \
adduser -D -u 1000 -G concord concord && \
chown -R concord:concord /app
USER concord
EXPOSE 9090
ENTRYPOINT ["./concord-api"]
EXPOSE 8080 9000 9100 8081
CMD ["/app/concord-api"]

View File

@ -1,8 +1,15 @@
FROM golang:1.23-alpine AS builder
# Build stage
FROM golang:1.25-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git make protobuf-dev
RUN apk add --no-cache git make protobuf
ENV PATH="/go/bin:${PATH}"
WORKDIR /build
RUN CGO_ENABLED=0 go install github.com/bufbuild/buf/cmd/buf@v1.28.1 \
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest \
&& go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest \
&& go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
COPY go.mod go.sum ./
RUN go mod download
@ -10,23 +17,15 @@ RUN go mod download
COPY . .
RUN make proto
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o concord-voice ./cmd/concord-voice
FROM alpine:latest
RUN apk --no-cache add ca-certificates
RUN go build -o /bin/concord-voice ./cmd/concord-voice
# Final stage
FROM alpine:3.19
WORKDIR /app
RUN apk add --no-cache ca-certificates
COPY --from=builder /build/concord-voice .
COPY --from=builder /bin/concord-voice /app/concord-voice
RUN addgroup -g 1000 concord && \
adduser -D -u 1000 -G concord concord && \
chown -R concord:concord /app
USER concord
EXPOSE 50000-52000/udp
EXPOSE 9091
ENTRYPOINT ["./concord-voice"]
EXPOSE 50000-50049/udp 9001 9101 8082
CMD ["/app/concord-voice"]

View File

@ -0,0 +1,94 @@
server {
listen 80;
server_name concord.akarpov.ru;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name concord.akarpov.ru;
ssl_certificate /etc/letsencrypt/live/concord.akarpov.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/concord.akarpov.ru/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
client_max_body_size 100m;
location ^~ /files/avatars/ {
alias /var/www/media/concord/avatars/;
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
location ^~ /files/ {
alias /var/www/media/concord/;
expires 7d;
add_header Cache-Control "public";
access_log off;
}
location ^~ /concord. {
grpc_pass grpc://127.0.0.1:19000;
grpc_read_timeout 1h;
grpc_send_timeout 1h;
grpc_set_header Host $host;
grpc_set_header X-Real-IP $remote_addr;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
grpc_set_header X-Forwarded-Proto https;
}
location ^~ /grpc. {
grpc_pass grpc://127.0.0.1:19000;
grpc_read_timeout 1h;
grpc_send_timeout 1h;
grpc_set_header Host $host;
grpc_set_header X-Real-IP $remote_addr;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
grpc_set_header X-Forwarded-Proto https;
}
location / {
proxy_pass http://127.0.0.1:18080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location ^~ /docs {
proxy_pass http://127.0.0.1:18080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location = /version {
proxy_pass http://127.0.0.1:18080;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location /metrics { return 404; }
location /health { return 404; }
}

File diff suppressed because it is too large Load Diff

7
go.mod
View File

@ -13,8 +13,10 @@ require (
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.43.0
golang.org/x/image v0.36.0
golang.org/x/oauth2 v0.32.0
golang.org/x/time v0.14.0
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
)
@ -35,10 +37,9 @@ require (
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
go.sum
View File

@ -86,16 +86,18 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=

60
go.work.sum Normal file
View File

@ -0,0 +1,60 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=

View File

@ -50,6 +50,8 @@ func (s *Service) KickUser(ctx context.Context, adminUserID, roomID, targetUserI
return err
}
s.hub.NotifyRoomLeave(targetUserID, roomID)
s.hub.BroadcastToRoom(roomID, &streamv1.ServerEvent{
EventId: uuid.New().String(),
CreatedAt: timestamppb.Now(),
@ -114,6 +116,8 @@ func (s *Service) BanUser(ctx context.Context, adminUserID, roomID, targetUserID
s.logger.Warn("failed to remove member during ban", zap.Error(err))
}
s.hub.NotifyRoomLeave(targetUserID, roomID)
s.hub.BroadcastToRoom(roomID, &streamv1.ServerEvent{
EventId: uuid.New().String(),
CreatedAt: timestamppb.Now(),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,8 @@ type Config struct {
Logging LoggingConfig
Redis RedisConfig
RateLimit RateLimitConfig
Storage StorageConfig
Email EmailConfig
}
type ServerConfig struct {
@ -23,6 +25,8 @@ type ServerConfig struct {
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
TLSCertFile string
TLSKeyFile string
}
type DatabaseConfig struct {
@ -59,12 +63,14 @@ type VoiceConfig struct {
UDPHost string
UDPPortStart int
UDPPortEnd int
UDPPortCount int
ControlPort int
ServerID string
Region string
Secret string
RegistryURL string
PublicHost string
StatusPort int
}
type LoggingConfig struct {
@ -83,10 +89,25 @@ type RedisConfig struct {
Enabled bool
}
type StorageConfig struct {
Path string
URL string
}
type RateLimitConfig struct {
Enabled bool
RequestsPerMinute int
Burst int
BypassToken string
}
type EmailConfig struct {
SMTPHost string
SMTPPort int
Username string
Password string
FromAddress string
FromName string
}
func Load() (*Config, error) {
@ -98,6 +119,8 @@ func Load() (*Config, error) {
ReadTimeout: getEnvDuration("READ_TIMEOUT", 10*time.Second),
WriteTimeout: getEnvDuration("WRITE_TIMEOUT", 10*time.Second),
IdleTimeout: getEnvDuration("IDLE_TIMEOUT", 120*time.Second),
TLSCertFile: getEnv("TLS_CERT_FILE", ""),
TLSKeyFile: getEnv("TLS_KEY_FILE", ""),
},
Database: DatabaseConfig{
Host: getEnv("DB_HOST", "localhost"),
@ -122,12 +145,14 @@ func Load() (*Config, error) {
UDPHost: getEnv("VOICE_UDP_HOST", "0.0.0.0"),
UDPPortStart: getEnvInt("VOICE_UDP_PORT_START", 50000),
UDPPortEnd: getEnvInt("VOICE_UDP_PORT_END", 52000),
UDPPortCount: getEnvInt("VOICE_UDP_PORT_COUNT", 50),
ControlPort: getEnvInt("VOICE_CONTROL_PORT", 9091),
ServerID: getEnv("VOICE_SERVER_ID", ""),
Region: getEnv("VOICE_REGION", "ru-west"),
Secret: getEnv("VOICE_SECRET", "change-me-voice-server-secret"),
RegistryURL: getEnv("REGISTRY_URL", "localhost:9090"),
PublicHost: getEnv("VOICE_PUBLIC_HOST", "localhost"),
StatusPort: getEnvInt("VOICE_STATUS_PORT", 9092),
},
Logging: LoggingConfig{
Level: getEnv("LOG_LEVEL", "info"),
@ -147,6 +172,17 @@ func Load() (*Config, error) {
Enabled: getEnvBool("RATE_LIMIT_ENABLED", true),
RequestsPerMinute: getEnvInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 60),
Burst: getEnvInt("RATE_LIMIT_BURST", 10),
BypassToken: getEnv("RATE_LIMIT_BYPASS_TOKEN", ""),
},
Storage: StorageConfig{
Path: getEnv("STORAGE_PATH", "./uploads"),
URL: getEnv("STORAGE_URL", "/files"),
},
Email: EmailConfig{
SMTPHost: getEnv("EMAIL_SMTP_HOST", "smtp.example.com"),
SMTPPort: getEnvInt("EMAIL_SMTP_PORT", 587),
Username: getEnv("EMAIL_USERNAME", ""),
Password: getEnv("EMAIL_PASSWORD", ""),
},
}
return cfg, nil

View File

@ -107,5 +107,29 @@ func ToGRPCError(err error) error {
return appErr.GRPCStatus().Err()
}
if st, ok := status.FromError(err); ok {
return st.Err()
}
return status.Error(codes.Internal, err.Error())
}
func IsNotFound(err error) bool {
if err == nil {
return false
}
var appErr *AppError
if errors.As(err, &appErr) {
if appErr.Code == codes.NotFound {
return true
}
return errors.Is(appErr.Err, ErrNotFound)
}
if st, ok := status.FromError(err); ok {
return st.Code() == codes.NotFound
}
return errors.Is(err, ErrNotFound)
}

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import (
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
)
@ -22,22 +23,23 @@ type Advertised struct {
func ComputeAdvertised(ctx context.Context, userConfiguredHost, udpBindHost string, port int) Advertised {
adv := Advertised{Port: port}
// 0) If user configured a public host (config wins)
if h := strings.TrimSpace(userConfiguredHost); h != "" {
adv.PublicHost = trimScheme(h)
h = trimScheme(h)
h = stripPort(h)
adv.PublicHost = h
adv.Source = "config"
} else if env := strings.TrimSpace(os.Getenv("CONCORD_PUBLIC_HOST")); env != "" {
adv.PublicHost = trimScheme(env)
h := trimScheme(env)
h = stripPort(h)
adv.PublicHost = h
adv.Source = "env"
} else {
// 1) Try to detect public IP via HTTP (short timeouts)
if ip, err := detectPublicIP(ctx); err == nil && ip != "" {
adv.PublicHost = ip
adv.Source = "http"
}
}
// 2) Determine a LAN IP fallback
if lan, err := detectLANIPPreferOutbound(); err == nil && lan != "" {
adv.LANHost = lan
} else if lan, err := firstPrivateIPv4(); err == nil && lan != "" {
@ -47,7 +49,6 @@ func ComputeAdvertised(ctx context.Context, userConfiguredHost, udpBindHost stri
adv.Notes = append(adv.Notes, "Could not find a LAN IP; falling back to 127.0.0.1.")
}
// 3) Notes / hints
if adv.PublicHost == "" {
adv.Source = "lan"
adv.Notes = append(adv.Notes,
@ -65,9 +66,21 @@ func trimScheme(h string) string {
h = strings.TrimSpace(h)
h = strings.TrimPrefix(h, "https://")
h = strings.TrimPrefix(h, "http://")
h = strings.TrimPrefix(h, "udp://")
h = strings.TrimPrefix(h, "tcp://")
return strings.TrimSuffix(h, "/")
}
func stripPort(hostWithPort string) string {
if idx := strings.LastIndex(hostWithPort, ":"); idx != -1 {
potentialPort := hostWithPort[idx+1:]
if _, err := strconv.Atoi(potentialPort); err == nil {
return hostWithPort[:idx]
}
}
return hostWithPort
}
func isAllInterfaces(h string) bool {
h = strings.TrimSpace(strings.ToLower(h))
return h == "" || h == "0.0.0.0" || h == "::" || h == "[::]" || h == "localhost"

444
internal/dm/handler.go Normal file

File diff suppressed because it is too large Load Diff

870
internal/dm/repository.go Normal file

File diff suppressed because it is too large Load Diff

849
internal/dm/service.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

199
internal/friends/handler.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

577
internal/friends/service.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ package cache
import (
"context"
"errors"
"time"
)
@ -13,14 +14,15 @@ func NewAsidePattern(cache *Cache) *AsidePattern {
return &AsidePattern{cache: cache}
}
func (a *AsidePattern) GetOrLoad(ctx context.Context, key string, ttl time.Duration, loader func() (interface{}, error)) (interface{}, error) {
func (a *AsidePattern) GetOrLoad(ctx context.Context, key string, ttl time.Duration,
loader func() (interface{}, error)) (interface{}, error) {
var result interface{}
err := a.cache.Get(ctx, key, &result)
if err == nil {
return result, nil
}
if err != ErrCacheMiss {
if !errors.Is(err, ErrCacheMiss) {
return nil, err
}

View File

@ -35,6 +35,10 @@ func New(host string, port int, password string, db int) (*Cache, error) {
return &Cache{client: client}, nil
}
func (c *Cache) Client() *redis.Client {
return c.client
}
func (c *Cache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
@ -98,3 +102,33 @@ func (c *Cache) FlushAll(ctx context.Context) error {
}
var ErrCacheMiss = fmt.Errorf("cache miss")
func (c *Cache) DeletePattern(ctx context.Context, pattern string) error {
iter := c.client.Scan(ctx, 0, pattern, 0).Iterator()
pipe := c.client.Pipeline()
for iter.Next(ctx) {
pipe.Del(ctx, iter.Val())
}
if err := iter.Err(); err != nil {
return err
}
_, err := pipe.Exec(ctx)
return err
}
func (c *Cache) HSet(ctx context.Context, key string, values map[string]string, ttl time.Duration) error {
pipe := c.client.Pipeline()
for k, v := range values {
pipe.HSet(ctx, key, k, v)
}
pipe.Expire(ctx, key, ttl)
_, err := pipe.Exec(ctx)
return err
}
func (c *Cache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
return c.client.HGetAll(ctx, key).Result()
}

View File

@ -0,0 +1,43 @@
CREATE TABLE IF NOT EXISTS friend_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
to_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (from_user_id, to_user_id),
CHECK (from_user_id != to_user_id),
CHECK (status IN ('pending', 'accepted', 'rejected'))
);
CREATE INDEX IF NOT EXISTS idx_friend_requests_from ON friend_requests(from_user_id);
CREATE INDEX IF NOT EXISTS idx_friend_requests_to ON friend_requests(to_user_id);
CREATE INDEX IF NOT EXISTS idx_friend_requests_status ON friend_requests(status);
CREATE TABLE IF NOT EXISTS friendships (
user_id1 UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user_id2 UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (user_id1, user_id2),
CHECK (user_id1 < user_id2)
);
CREATE INDEX IF NOT EXISTS idx_friendships_user1 ON friendships(user_id1);
CREATE INDEX IF NOT EXISTS idx_friendships_user2 ON friendships(user_id2);
CREATE TABLE IF NOT EXISTS blocked_users (
blocker_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
blocked_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (blocker_id, blocked_id),
CHECK (blocker_id != blocked_id)
);
CREATE INDEX IF NOT EXISTS idx_blocked_users_blocker ON blocked_users(blocker_id);
CREATE INDEX IF NOT EXISTS idx_blocked_users_blocked ON blocked_users(blocked_id);
CREATE INDEX IF NOT EXISTS idx_friend_requests_to_status ON friend_requests(to_user_id, status);
CREATE INDEX IF NOT EXISTS idx_friend_requests_from_status ON friend_requests(from_user_id, status);
CREATE INDEX IF NOT EXISTS idx_friendships_lookup ON friendships(user_id1, user_id2);
CREATE INDEX IF NOT EXISTS idx_blocked_users_lookup ON blocked_users(blocker_id, blocked_id);

View File

@ -0,0 +1,11 @@
CREATE INDEX IF NOT EXISTS idx_messages_content_search
ON messages USING gin(to_tsvector('english', content));
CREATE INDEX IF NOT EXISTS idx_users_search
ON users USING gin(to_tsvector('english', handle || ' ' || display_name));
CREATE OR REPLACE FUNCTION cleanup_expired_bans() RETURNS void AS $$
BEGIN
DELETE FROM room_bans WHERE expires_at IS NOT NULL AND expires_at < NOW();
END;
$$ LANGUAGE plpgsql;

Some files were not shown because too many files have changed in this diff Show More