mirror of
https://github.com/Alexander-D-Karpov/concord.git
synced 2026-03-16 22:04:15 +03:00
Compare commits
25 Commits
v20251201-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 59c93d5d47 | |||
| 0adad329dc | |||
| 24e8340b88 | |||
| 029448292e | |||
| dfa99fad23 | |||
| d055a75fd3 | |||
| 512b5ec57e | |||
| 368422e21c | |||
| 1bda3a8a81 | |||
| 2a05884836 | |||
| f382823177 | |||
| 966d6daa0e | |||
| e27536d700 | |||
| 060cac710c | |||
| 78fcafdda6 | |||
| 2bb7860cc4 | |||
| 162bc58b3a | |||
| 97f3ed15fb | |||
| ce5e66d446 | |||
| e2c5c1a422 | |||
| 759486ecc7 | |||
| c2b590b21e | |||
| 0e062b2155 | |||
| e967eae271 | |||
| 9b2d0172c5 |
76
Makefile
76
Makefile
|
|
@ -1,4 +1,17 @@
|
||||||
.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:
|
proto:
|
||||||
@sh scripts/gen_proto.sh
|
@sh scripts/gen_proto.sh
|
||||||
|
|
@ -12,6 +25,9 @@ build:
|
||||||
clean:
|
clean:
|
||||||
@rm -rf bin/ api/gen/
|
@rm -rf bin/ api/gen/
|
||||||
|
|
||||||
|
clean-proto-deps:
|
||||||
|
@rm -rf api/proto-deps/
|
||||||
|
|
||||||
test-cleanup:
|
test-cleanup:
|
||||||
@echo "Resetting test database..."
|
@echo "Resetting test database..."
|
||||||
@PGPASSWORD=postgres psql -h localhost -U postgres -d postgres -c "DROP DATABASE IF EXISTS concord_test;" 2>/dev/null || true
|
@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:
|
run-voice:
|
||||||
@go run ./cmd/concord-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:
|
lint:
|
||||||
@golangci-lint run ./...
|
@golangci-lint run ./...
|
||||||
|
|
||||||
|
|
@ -52,5 +64,57 @@ install-tools:
|
||||||
@go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@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-grpc-gateway@latest
|
||||||
@go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@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
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ package concord.admin.v1;
|
||||||
|
|
||||||
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/admin/v1;adminv1";
|
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/admin/v1;adminv1";
|
||||||
|
|
||||||
|
import "google/api/annotations.proto";
|
||||||
|
|
||||||
message KickRequest {
|
message KickRequest {
|
||||||
string room_id = 1;
|
string room_id = 1;
|
||||||
string user_id = 2;
|
string user_id = 2;
|
||||||
|
|
@ -24,7 +26,22 @@ message MuteRequest {
|
||||||
message EmptyResponse {}
|
message EmptyResponse {}
|
||||||
|
|
||||||
service AdminService {
|
service AdminService {
|
||||||
rpc Kick(KickRequest) returns (EmptyResponse);
|
rpc Kick(KickRequest) returns (EmptyResponse) {
|
||||||
rpc Ban(BanRequest) returns (EmptyResponse);
|
option (google.api.http) = {
|
||||||
rpc Mute(MuteRequest) returns (EmptyResponse);
|
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: "*"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ package concord.auth.v1;
|
||||||
|
|
||||||
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/auth/v1;authv1";
|
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/auth/v1;authv1";
|
||||||
|
|
||||||
|
import "google/api/annotations.proto";
|
||||||
|
|
||||||
message LoginPasswordRequest {
|
message LoginPasswordRequest {
|
||||||
string handle = 1;
|
string handle = 1;
|
||||||
string password = 2;
|
string password = 2;
|
||||||
|
|
@ -49,10 +51,40 @@ message RegisterRequest {
|
||||||
message EmptyResponse {}
|
message EmptyResponse {}
|
||||||
|
|
||||||
service AuthService {
|
service AuthService {
|
||||||
rpc Register(RegisterRequest) returns (Token);
|
rpc Register(RegisterRequest) returns (Token) {
|
||||||
rpc LoginPassword(LoginPasswordRequest) returns (Token);
|
option (google.api.http) = {
|
||||||
rpc LoginOAuth(LoginOAuthRequest) returns (Token);
|
post: "/v1/auth/register"
|
||||||
rpc OAuthBegin(OAuthBeginRequest) returns (OAuthBeginResponse);
|
body: "*"
|
||||||
rpc Refresh(RefreshRequest) returns (Token);
|
};
|
||||||
rpc Logout(LogoutRequest) returns (EmptyResponse);
|
}
|
||||||
|
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: "*"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,3 +12,9 @@ plugins:
|
||||||
out: gen/go
|
out: gen/go
|
||||||
opt:
|
opt:
|
||||||
- paths=source_relative
|
- paths=source_relative
|
||||||
|
- plugin: buf.build/grpc-ecosystem/openapiv2
|
||||||
|
out: gen/openapiv2
|
||||||
|
opt:
|
||||||
|
- allow_merge=true
|
||||||
|
- merge_file_name=concord
|
||||||
|
- openapi_naming_strategy=fqn
|
||||||
|
|
@ -2,6 +2,7 @@ version: v1
|
||||||
name: buf.build/alexander-d-karpov/concord
|
name: buf.build/alexander-d-karpov/concord
|
||||||
deps:
|
deps:
|
||||||
- buf.build/googleapis/googleapis
|
- buf.build/googleapis/googleapis
|
||||||
|
- buf.build/grpc-ecosystem/grpc-gateway
|
||||||
breaking:
|
breaking:
|
||||||
use:
|
use:
|
||||||
- FILE
|
- FILE
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ package concord.call.v1;
|
||||||
|
|
||||||
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/call/v1;callv1";
|
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";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
message JoinVoiceRequest {
|
message JoinVoiceRequest {
|
||||||
string room_id = 1;
|
string room_id = 1;
|
||||||
bool audio_only = 2;
|
bool audio_only = 2;
|
||||||
|
string preferred_region = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UdpEndpoint {
|
message UdpEndpoint {
|
||||||
|
|
@ -35,6 +37,7 @@ message JoinVoiceResponse {
|
||||||
CodecHint codec = 4;
|
CodecHint codec = 4;
|
||||||
CryptoSuite crypto = 5;
|
CryptoSuite crypto = 5;
|
||||||
repeated Participant participants = 6;
|
repeated Participant participants = 6;
|
||||||
|
uint32 screen_ssrc = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Participant {
|
message Participant {
|
||||||
|
|
@ -42,6 +45,9 @@ message Participant {
|
||||||
uint32 ssrc = 2;
|
uint32 ssrc = 2;
|
||||||
bool muted = 3;
|
bool muted = 3;
|
||||||
bool video_enabled = 4;
|
bool video_enabled = 4;
|
||||||
|
bool screen_sharing = 5;
|
||||||
|
uint32 video_ssrc = 6;
|
||||||
|
uint32 screen_ssrc = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LeaveVoiceRequest {
|
message LeaveVoiceRequest {
|
||||||
|
|
@ -52,12 +58,12 @@ message SetMediaPrefsRequest {
|
||||||
string room_id = 1;
|
string room_id = 1;
|
||||||
bool audio_only = 2;
|
bool audio_only = 2;
|
||||||
bool video_enabled = 3;
|
bool video_enabled = 3;
|
||||||
bool muted = 4;
|
bool screen_sharing = 4;
|
||||||
|
bool muted = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EmptyResponse {}
|
message EmptyResponse {}
|
||||||
|
|
||||||
|
|
||||||
message GetVoiceStatusRequest {
|
message GetVoiceStatusRequest {
|
||||||
string room_id = 1;
|
string room_id = 1;
|
||||||
}
|
}
|
||||||
|
|
@ -66,8 +72,9 @@ message VoiceParticipant {
|
||||||
string user_id = 1;
|
string user_id = 1;
|
||||||
bool muted = 2;
|
bool muted = 2;
|
||||||
bool video_enabled = 3;
|
bool video_enabled = 3;
|
||||||
bool speaking = 4;
|
bool screen_sharing = 4;
|
||||||
google.protobuf.Timestamp joined_at = 5;
|
bool speaking = 5;
|
||||||
|
google.protobuf.Timestamp joined_at = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetVoiceStatusResponse {
|
message GetVoiceStatusResponse {
|
||||||
|
|
@ -76,8 +83,26 @@ message GetVoiceStatusResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
service CallService {
|
service CallService {
|
||||||
rpc JoinVoice(JoinVoiceRequest) returns (JoinVoiceResponse);
|
rpc JoinVoice(JoinVoiceRequest) returns (JoinVoiceResponse) {
|
||||||
rpc LeaveVoice(LeaveVoiceRequest) returns (EmptyResponse);
|
option (google.api.http) = {
|
||||||
rpc SetMediaPrefs(SetMediaPrefsRequest) returns (EmptyResponse);
|
post: "/v1/rooms/{room_id}/voice/join"
|
||||||
rpc GetVoiceStatus(GetVoiceStatusRequest) returns (GetVoiceStatusResponse);
|
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
|
|
@ -21,6 +21,17 @@ message User {
|
||||||
google.protobuf.Timestamp created_at = 5;
|
google.protobuf.Timestamp created_at = 5;
|
||||||
string status = 6;
|
string status = 6;
|
||||||
string bio = 7;
|
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 {
|
message Room {
|
||||||
|
|
@ -41,6 +52,7 @@ message Member {
|
||||||
google.protobuf.Timestamp joined_at = 4;
|
google.protobuf.Timestamp joined_at = 4;
|
||||||
string nickname = 5;
|
string nickname = 5;
|
||||||
string status = 6;
|
string status = 6;
|
||||||
|
int64 last_read_message_id = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message VoiceServer {
|
message VoiceServer {
|
||||||
|
|
|
||||||
245
api/proto/dm/v1/dm.proto
Normal file
245
api/proto/dm/v1/dm.proto
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -4,6 +4,7 @@ package concord.friends.v1;
|
||||||
|
|
||||||
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/friends/v1;friendsv1";
|
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/friends/v1;friendsv1";
|
||||||
|
|
||||||
|
import "google/api/annotations.proto";
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
enum FriendRequestStatus {
|
enum FriendRequestStatus {
|
||||||
|
|
@ -20,7 +21,6 @@ message FriendRequest {
|
||||||
FriendRequestStatus status = 4;
|
FriendRequestStatus status = 4;
|
||||||
google.protobuf.Timestamp created_at = 5;
|
google.protobuf.Timestamp created_at = 5;
|
||||||
google.protobuf.Timestamp updated_at = 6;
|
google.protobuf.Timestamp updated_at = 6;
|
||||||
|
|
||||||
string from_handle = 7;
|
string from_handle = 7;
|
||||||
string from_display_name = 8;
|
string from_display_name = 8;
|
||||||
string from_avatar_url = 9;
|
string from_avatar_url = 9;
|
||||||
|
|
@ -92,16 +92,55 @@ message ListBlockedUsersResponse {
|
||||||
message EmptyResponse {}
|
message EmptyResponse {}
|
||||||
|
|
||||||
service FriendsService {
|
service FriendsService {
|
||||||
rpc SendFriendRequest(SendFriendRequestRequest) returns (SendFriendRequestResponse);
|
rpc SendFriendRequest(SendFriendRequestRequest) returns (SendFriendRequestResponse) {
|
||||||
rpc AcceptFriendRequest(AcceptFriendRequestRequest) returns (EmptyResponse);
|
option (google.api.http) = {
|
||||||
rpc RejectFriendRequest(RejectFriendRequestRequest) returns (EmptyResponse);
|
post: "/v1/friends/requests"
|
||||||
rpc CancelFriendRequest(CancelFriendRequestRequest) returns (EmptyResponse);
|
body: "*"
|
||||||
rpc RemoveFriend(RemoveFriendRequest) returns (EmptyResponse);
|
};
|
||||||
|
}
|
||||||
rpc ListFriends(ListFriendsRequest) returns (ListFriendsResponse);
|
rpc AcceptFriendRequest(AcceptFriendRequestRequest) returns (EmptyResponse) {
|
||||||
rpc ListPendingRequests(ListPendingRequestsRequest) returns (ListPendingRequestsResponse);
|
option (google.api.http) = {
|
||||||
|
post: "/v1/friends/requests/{request_id}/accept"
|
||||||
rpc BlockUser(BlockUserRequest) returns (EmptyResponse);
|
};
|
||||||
rpc UnblockUser(UnblockUserRequest) returns (EmptyResponse);
|
}
|
||||||
rpc ListBlockedUsers(ListBlockedUsersRequest) returns (ListBlockedUsersResponse);
|
rpc RejectFriendRequest(RejectFriendRequestRequest) returns (EmptyResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v1/friends/requests/{request_id}/reject"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc CancelFriendRequest(CancelFriendRequestRequest) returns (EmptyResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
delete: "/v1/friends/requests/{request_id}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc RemoveFriend(RemoveFriendRequest) returns (EmptyResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
delete: "/v1/friends/{user_id}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc ListFriends(ListFriendsRequest) returns (ListFriendsResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/v1/friends"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc ListPendingRequests(ListPendingRequestsRequest) returns (ListPendingRequestsResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/v1/friends/requests"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc BlockUser(BlockUserRequest) returns (EmptyResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v1/users/{user_id}/block"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc UnblockUser(UnblockUserRequest) returns (EmptyResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
delete: "/v1/users/{user_id}/block"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc ListBlockedUsers(ListBlockedUsersRequest) returns (ListBlockedUsersResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/v1/users/blocked"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,8 +4,9 @@ package concord.membership.v1;
|
||||||
|
|
||||||
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/membership/v1;membershipv1";
|
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/membership/v1;membershipv1";
|
||||||
|
|
||||||
import "common/v1/types.proto";
|
import "google/api/annotations.proto";
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "common/v1/types.proto";
|
||||||
|
|
||||||
message InviteRequest {
|
message InviteRequest {
|
||||||
string room_id = 1;
|
string room_id = 1;
|
||||||
|
|
@ -54,7 +55,6 @@ message RoomInvite {
|
||||||
RoomInviteStatus status = 6;
|
RoomInviteStatus status = 6;
|
||||||
google.protobuf.Timestamp created_at = 7;
|
google.protobuf.Timestamp created_at = 7;
|
||||||
google.protobuf.Timestamp updated_at = 8;
|
google.protobuf.Timestamp updated_at = 8;
|
||||||
// Populated user info
|
|
||||||
string invited_user_handle = 9;
|
string invited_user_handle = 9;
|
||||||
string invited_user_display_name = 10;
|
string invited_user_display_name = 10;
|
||||||
string invited_user_avatar_url = 11;
|
string invited_user_avatar_url = 11;
|
||||||
|
|
@ -83,13 +83,52 @@ message ListRoomInvitesResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
service MembershipService {
|
service MembershipService {
|
||||||
rpc Invite(InviteRequest) returns (RoomInvite);
|
rpc Invite(InviteRequest) returns (RoomInvite) {
|
||||||
rpc AcceptRoomInvite(AcceptRoomInviteRequest) returns (concord.common.v1.Member);
|
option (google.api.http) = {
|
||||||
rpc RejectRoomInvite(RejectRoomInviteRequest) returns (EmptyResponse);
|
post: "/v1/rooms/{room_id}/invites"
|
||||||
rpc CancelRoomInvite(CancelRoomInviteRequest) returns (EmptyResponse);
|
body: "*"
|
||||||
rpc ListRoomInvites(ListRoomInvitesRequest) returns (ListRoomInvitesResponse);
|
};
|
||||||
rpc Remove(RemoveRequest) returns (EmptyResponse);
|
}
|
||||||
rpc SetRole(SetRoleRequest) returns (concord.common.v1.Member);
|
rpc AcceptRoomInvite(AcceptRoomInviteRequest) returns (concord.common.v1.Member) {
|
||||||
rpc SetNickname(SetNicknameRequest) returns (concord.common.v1.Member);
|
option (google.api.http) = {
|
||||||
rpc ListMembers(ListMembersRequest) returns (ListMembersResponse);
|
post: "/v1/invites/{invite_id}/accept"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc RejectRoomInvite(RejectRoomInviteRequest) returns (EmptyResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v1/invites/{invite_id}/reject"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc CancelRoomInvite(CancelRoomInviteRequest) returns (EmptyResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
delete: "/v1/invites/{invite_id}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc ListRoomInvites(ListRoomInvitesRequest) returns (ListRoomInvitesResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/v1/invites"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc Remove(RemoveRequest) returns (EmptyResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
delete: "/v1/rooms/{room_id}/members/{user_id}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc SetRole(SetRoleRequest) returns (concord.common.v1.Member) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
put: "/v1/rooms/{room_id}/members/{user_id}/role"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc SetNickname(SetNicknameRequest) returns (concord.common.v1.Member) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
put: "/v1/rooms/{room_id}/nickname"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc ListMembers(ListMembersRequest) returns (ListMembersResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/v1/rooms/{room_id}/members"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,8 +4,9 @@ package concord.registry.v1;
|
||||||
|
|
||||||
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/registry/v1;registryv1";
|
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 "google/protobuf/timestamp.proto";
|
||||||
|
import "common/v1/types.proto";
|
||||||
|
|
||||||
message RegisterServerRequest {
|
message RegisterServerRequest {
|
||||||
concord.common.v1.VoiceServer server = 1;
|
concord.common.v1.VoiceServer server = 1;
|
||||||
|
|
@ -37,7 +38,21 @@ message ListServersResponse {
|
||||||
message EmptyResponse {}
|
message EmptyResponse {}
|
||||||
|
|
||||||
service RegistryService {
|
service RegistryService {
|
||||||
rpc RegisterServer(RegisterServerRequest) returns (RegisterServerResponse);
|
rpc RegisterServer(RegisterServerRequest) returns (RegisterServerResponse) {
|
||||||
rpc Heartbeat(HeartbeatRequest) returns (EmptyResponse);
|
option (google.api.http) = {
|
||||||
rpc ListServers(ListServersRequest) returns (ListServersResponse);
|
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"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,8 +4,9 @@ package concord.rooms.v1;
|
||||||
|
|
||||||
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/rooms/v1;roomsv1";
|
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/rooms/v1;roomsv1";
|
||||||
|
|
||||||
import "common/v1/types.proto";
|
import "google/api/annotations.proto";
|
||||||
import "google/protobuf/wrappers.proto";
|
import "google/protobuf/wrappers.proto";
|
||||||
|
import "common/v1/types.proto";
|
||||||
|
|
||||||
message CreateRoomRequest {
|
message CreateRoomRequest {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
|
|
@ -21,10 +22,9 @@ message GetRoomRequest {
|
||||||
|
|
||||||
message UpdateRoomRequest {
|
message UpdateRoomRequest {
|
||||||
string room_id = 1;
|
string room_id = 1;
|
||||||
|
google.protobuf.StringValue name = 2;
|
||||||
google.protobuf.StringValue name = 2; // optional
|
google.protobuf.StringValue description = 3;
|
||||||
google.protobuf.StringValue description = 3; // optional
|
google.protobuf.BoolValue is_private = 4;
|
||||||
google.protobuf.BoolValue is_private = 4; // optional
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListRoomsForUserRequest {}
|
message ListRoomsForUserRequest {}
|
||||||
|
|
@ -45,10 +45,37 @@ message DeleteRoomRequest {
|
||||||
message EmptyResponse {}
|
message EmptyResponse {}
|
||||||
|
|
||||||
service RoomsService {
|
service RoomsService {
|
||||||
rpc CreateRoom(CreateRoomRequest) returns (concord.common.v1.Room);
|
rpc CreateRoom(CreateRoomRequest) returns (concord.common.v1.Room) {
|
||||||
rpc GetRoom(GetRoomRequest) returns (concord.common.v1.Room);
|
option (google.api.http) = {
|
||||||
rpc UpdateRoom(UpdateRoomRequest) returns (concord.common.v1.Room);
|
post: "/v1/rooms"
|
||||||
rpc ListRoomsForUser(ListRoomsForUserRequest) returns (ListRoomsForUserResponse);
|
body: "*"
|
||||||
rpc AttachVoiceServer(AttachVoiceServerRequest) returns (concord.common.v1.Room);
|
};
|
||||||
rpc DeleteRoom(DeleteRoomRequest) returns (EmptyResponse);
|
}
|
||||||
|
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
28
api/proto/unfurl/v1/unfurl.proto
Normal file
28
api/proto/unfurl/v1/unfurl.proto
Normal 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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ package concord.users.v1;
|
||||||
|
|
||||||
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/users/v1;usersv1";
|
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";
|
import "common/v1/types.proto";
|
||||||
|
|
||||||
message GetSelfRequest {}
|
message GetSelfRequest {}
|
||||||
|
|
@ -46,12 +47,84 @@ message ListUsersByIDsResponse {
|
||||||
repeated concord.common.v1.User users = 1;
|
repeated concord.common.v1.User users = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
service UsersService {
|
message UploadAvatarRequest {
|
||||||
rpc GetSelf(GetSelfRequest) returns (concord.common.v1.User);
|
bytes image_data = 1;
|
||||||
rpc GetUser(GetUserRequest) returns (concord.common.v1.User);
|
string filename = 2;
|
||||||
rpc GetUserByHandle(GetUserByHandleRequest) returns (concord.common.v1.User);
|
}
|
||||||
rpc UpdateProfile(UpdateProfileRequest) returns (concord.common.v1.User);
|
|
||||||
rpc UpdateStatus(UpdateStatusRequest) returns (concord.common.v1.User);
|
message UploadAvatarResponse {
|
||||||
rpc SearchUsers(SearchUsersRequest) returns (SearchUsersResponse);
|
string avatar_url = 1;
|
||||||
rpc ListUsersByIDs(ListUsersByIDsRequest) returns (ListUsersByIDsResponse);
|
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
File diff suppressed because it is too large
Load Diff
|
|
@ -1,17 +1,17 @@
|
||||||
|
# Build stage
|
||||||
FROM golang:1.25-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
ENV GOTOOLCHAIN=auto
|
RUN apk add --no-cache git make protobuf
|
||||||
RUN apk add --no-cache bash git make protobuf-dev
|
|
||||||
|
|
||||||
ENV PATH="/go/bin:${PATH}"
|
ENV PATH="/go/bin:${PATH}"
|
||||||
|
|
||||||
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
|
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 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-grpc-gateway@latest \
|
||||||
&& go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
|
&& go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
|
|
@ -19,18 +19,15 @@ COPY . .
|
||||||
|
|
||||||
RUN make proto
|
RUN make proto
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o concord-api ./cmd/concord-api
|
RUN go build -o /bin/concord-api ./cmd/concord-api
|
||||||
|
|
||||||
|
# Final stage
|
||||||
FROM alpine:latest
|
FROM alpine:3.19
|
||||||
RUN apk --no-cache add ca-certificates tzdata
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /build/concord-api .
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
RUN addgroup -g 1000 concord \
|
COPY --from=builder /bin/concord-api /app/concord-api
|
||||||
&& adduser -D -u 1000 -G concord concord \
|
COPY --from=builder /app/api/gen/openapiv2/ /app/api/gen/openapiv2/
|
||||||
&& chown -R concord:concord /app
|
|
||||||
USER concord
|
|
||||||
|
|
||||||
EXPOSE 9090
|
EXPOSE 8080 9000 9100 8081
|
||||||
ENTRYPOINT ["./concord-api"]
|
CMD ["/app/concord-api"]
|
||||||
|
|
@ -1,34 +1,31 @@
|
||||||
|
# Build stage
|
||||||
FROM golang:1.25-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
ENV GOTOOLCHAIN=auto
|
RUN apk add --no-cache git make protobuf
|
||||||
RUN apk add --no-cache bash git make protobuf-dev
|
|
||||||
ENV PATH="/go/bin:${PATH}"
|
ENV PATH="/go/bin:${PATH}"
|
||||||
|
|
||||||
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
|
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 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-grpc-gateway@latest \
|
||||||
&& go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
|
&& go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN make proto
|
RUN make proto
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o concord-voice ./cmd/concord-voice
|
|
||||||
|
|
||||||
|
RUN go build -o /bin/concord-voice ./cmd/concord-voice
|
||||||
|
|
||||||
FROM alpine:latest
|
# Final stage
|
||||||
RUN apk --no-cache add ca-certificates
|
FROM alpine:3.19
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /build/concord-voice .
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
RUN addgroup -g 1000 concord \
|
COPY --from=builder /bin/concord-voice /app/concord-voice
|
||||||
&& adduser -D -u 1000 -G concord concord \
|
|
||||||
&& chown -R concord:concord /app
|
|
||||||
USER concord
|
|
||||||
|
|
||||||
EXPOSE 50000-52000/udp
|
EXPOSE 50000-50049/udp 9001 9101 8082
|
||||||
EXPOSE 9091
|
CMD ["/app/concord-voice"]
|
||||||
ENTRYPOINT ["./concord-voice"]
|
|
||||||
94
deploy/concord.akarpov.ru.conf
Normal file
94
deploy/concord.akarpov.ru.conf
Normal 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
7
go.mod
|
|
@ -13,8 +13,10 @@ require (
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.43.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/oauth2 v0.32.0
|
||||||
golang.org/x/time v0.14.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/grpc v1.76.0
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.10
|
||||||
)
|
)
|
||||||
|
|
@ -35,10 +37,9 @@ require (
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
golang.org/x/net v0.45.0 // 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/sys v0.37.0 // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
10
go.sum
10
go.sum
|
|
@ -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=
|
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 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
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 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
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 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
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 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
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 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
|
|
||||||
60
go.work.sum
Normal file
60
go.work.sum
Normal 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=
|
||||||
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
File diff suppressed because it is too large
Load Diff
|
|
@ -63,12 +63,14 @@ type VoiceConfig struct {
|
||||||
UDPHost string
|
UDPHost string
|
||||||
UDPPortStart int
|
UDPPortStart int
|
||||||
UDPPortEnd int
|
UDPPortEnd int
|
||||||
|
UDPPortCount int
|
||||||
ControlPort int
|
ControlPort int
|
||||||
ServerID string
|
ServerID string
|
||||||
Region string
|
Region string
|
||||||
Secret string
|
Secret string
|
||||||
RegistryURL string
|
RegistryURL string
|
||||||
PublicHost string
|
PublicHost string
|
||||||
|
StatusPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoggingConfig struct {
|
type LoggingConfig struct {
|
||||||
|
|
@ -96,6 +98,7 @@ type RateLimitConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
RequestsPerMinute int
|
RequestsPerMinute int
|
||||||
Burst int
|
Burst int
|
||||||
|
BypassToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailConfig struct {
|
type EmailConfig struct {
|
||||||
|
|
@ -142,12 +145,14 @@ func Load() (*Config, error) {
|
||||||
UDPHost: getEnv("VOICE_UDP_HOST", "0.0.0.0"),
|
UDPHost: getEnv("VOICE_UDP_HOST", "0.0.0.0"),
|
||||||
UDPPortStart: getEnvInt("VOICE_UDP_PORT_START", 50000),
|
UDPPortStart: getEnvInt("VOICE_UDP_PORT_START", 50000),
|
||||||
UDPPortEnd: getEnvInt("VOICE_UDP_PORT_END", 52000),
|
UDPPortEnd: getEnvInt("VOICE_UDP_PORT_END", 52000),
|
||||||
|
UDPPortCount: getEnvInt("VOICE_UDP_PORT_COUNT", 50),
|
||||||
ControlPort: getEnvInt("VOICE_CONTROL_PORT", 9091),
|
ControlPort: getEnvInt("VOICE_CONTROL_PORT", 9091),
|
||||||
ServerID: getEnv("VOICE_SERVER_ID", ""),
|
ServerID: getEnv("VOICE_SERVER_ID", ""),
|
||||||
Region: getEnv("VOICE_REGION", "ru-west"),
|
Region: getEnv("VOICE_REGION", "ru-west"),
|
||||||
Secret: getEnv("VOICE_SECRET", "change-me-voice-server-secret"),
|
Secret: getEnv("VOICE_SECRET", "change-me-voice-server-secret"),
|
||||||
RegistryURL: getEnv("REGISTRY_URL", "localhost:9090"),
|
RegistryURL: getEnv("REGISTRY_URL", "localhost:9090"),
|
||||||
PublicHost: getEnv("VOICE_PUBLIC_HOST", "localhost"),
|
PublicHost: getEnv("VOICE_PUBLIC_HOST", "localhost"),
|
||||||
|
StatusPort: getEnvInt("VOICE_STATUS_PORT", 9092),
|
||||||
},
|
},
|
||||||
Logging: LoggingConfig{
|
Logging: LoggingConfig{
|
||||||
Level: getEnv("LOG_LEVEL", "info"),
|
Level: getEnv("LOG_LEVEL", "info"),
|
||||||
|
|
@ -167,10 +172,11 @@ func Load() (*Config, error) {
|
||||||
Enabled: getEnvBool("RATE_LIMIT_ENABLED", true),
|
Enabled: getEnvBool("RATE_LIMIT_ENABLED", true),
|
||||||
RequestsPerMinute: getEnvInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 60),
|
RequestsPerMinute: getEnvInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 60),
|
||||||
Burst: getEnvInt("RATE_LIMIT_BURST", 10),
|
Burst: getEnvInt("RATE_LIMIT_BURST", 10),
|
||||||
|
BypassToken: getEnv("RATE_LIMIT_BYPASS_TOKEN", ""),
|
||||||
},
|
},
|
||||||
Storage: StorageConfig{
|
Storage: StorageConfig{
|
||||||
Path: getEnv("STORAGE_PATH", "./uploads"),
|
Path: getEnv("STORAGE_PATH", "./uploads"),
|
||||||
URL: getEnv("STORAGE_URL", "http://localhost:8080/files"),
|
URL: getEnv("STORAGE_URL", "/files"),
|
||||||
},
|
},
|
||||||
Email: EmailConfig{
|
Email: EmailConfig{
|
||||||
SMTPHost: getEnv("EMAIL_SMTP_HOST", "smtp.example.com"),
|
SMTPHost: getEnv("EMAIL_SMTP_HOST", "smtp.example.com"),
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -22,22 +23,23 @@ type Advertised struct {
|
||||||
func ComputeAdvertised(ctx context.Context, userConfiguredHost, udpBindHost string, port int) Advertised {
|
func ComputeAdvertised(ctx context.Context, userConfiguredHost, udpBindHost string, port int) Advertised {
|
||||||
adv := Advertised{Port: port}
|
adv := Advertised{Port: port}
|
||||||
|
|
||||||
// 0) If user configured a public host (config wins)
|
|
||||||
if h := strings.TrimSpace(userConfiguredHost); h != "" {
|
if h := strings.TrimSpace(userConfiguredHost); h != "" {
|
||||||
adv.PublicHost = trimScheme(h)
|
h = trimScheme(h)
|
||||||
|
h = stripPort(h)
|
||||||
|
adv.PublicHost = h
|
||||||
adv.Source = "config"
|
adv.Source = "config"
|
||||||
} else if env := strings.TrimSpace(os.Getenv("CONCORD_PUBLIC_HOST")); env != "" {
|
} 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"
|
adv.Source = "env"
|
||||||
} else {
|
} else {
|
||||||
// 1) Try to detect public IP via HTTP (short timeouts)
|
|
||||||
if ip, err := detectPublicIP(ctx); err == nil && ip != "" {
|
if ip, err := detectPublicIP(ctx); err == nil && ip != "" {
|
||||||
adv.PublicHost = ip
|
adv.PublicHost = ip
|
||||||
adv.Source = "http"
|
adv.Source = "http"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Determine a LAN IP fallback
|
|
||||||
if lan, err := detectLANIPPreferOutbound(); err == nil && lan != "" {
|
if lan, err := detectLANIPPreferOutbound(); err == nil && lan != "" {
|
||||||
adv.LANHost = lan
|
adv.LANHost = lan
|
||||||
} else if lan, err := firstPrivateIPv4(); err == nil && 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.")
|
adv.Notes = append(adv.Notes, "Could not find a LAN IP; falling back to 127.0.0.1.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Notes / hints
|
|
||||||
if adv.PublicHost == "" {
|
if adv.PublicHost == "" {
|
||||||
adv.Source = "lan"
|
adv.Source = "lan"
|
||||||
adv.Notes = append(adv.Notes,
|
adv.Notes = append(adv.Notes,
|
||||||
|
|
@ -65,9 +66,21 @@ func trimScheme(h string) string {
|
||||||
h = strings.TrimSpace(h)
|
h = strings.TrimSpace(h)
|
||||||
h = strings.TrimPrefix(h, "https://")
|
h = strings.TrimPrefix(h, "https://")
|
||||||
h = strings.TrimPrefix(h, "http://")
|
h = strings.TrimPrefix(h, "http://")
|
||||||
|
h = strings.TrimPrefix(h, "udp://")
|
||||||
|
h = strings.TrimPrefix(h, "tcp://")
|
||||||
return strings.TrimSuffix(h, "/")
|
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 {
|
func isAllInterfaces(h string) bool {
|
||||||
h = strings.TrimSpace(strings.ToLower(h))
|
h = strings.TrimSpace(strings.ToLower(h))
|
||||||
return h == "" || h == "0.0.0.0" || h == "::" || h == "[::]" || h == "localhost"
|
return h == "" || h == "0.0.0.0" || h == "::" || h == "[::]" || h == "localhost"
|
||||||
|
|
|
||||||
444
internal/dm/handler.go
Normal file
444
internal/dm/handler.go
Normal file
File diff suppressed because it is too large
Load Diff
870
internal/dm/repository.go
Normal file
870
internal/dm/repository.go
Normal file
File diff suppressed because it is too large
Load Diff
849
internal/dm/service.go
Normal file
849
internal/dm/service.go
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -144,14 +144,12 @@ func (h *Hub) autoSubscribeUserRooms(ctx context.Context, client *Client, userID
|
||||||
b, _ := json.Marshal(v)
|
b, _ := json.Marshal(v)
|
||||||
_ = json.Unmarshal(b, &roomIDs)
|
_ = json.Unmarshal(b, &roomIDs)
|
||||||
}
|
}
|
||||||
h.logger.Info("loaded room IDs from cache", zap.String("user_id", userID.String()), zap.Int("count", len(roomIDs)))
|
|
||||||
} else {
|
} else {
|
||||||
h.logger.Warn("GetOrLoad rooms failed; falling back to DB", zap.Error(err))
|
h.logger.Warn("GetOrLoad rooms failed; falling back to DB", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if roomIDs == nil {
|
if roomIDs == nil {
|
||||||
h.logger.Info("cache disabled or failed, querying DB directly", zap.String("user_id", userID.String()))
|
|
||||||
rows, err := h.pool.Query(ctx, `SELECT room_id FROM memberships WHERE user_id = $1`, userID)
|
rows, err := h.pool.Query(ctx, `SELECT room_id FROM memberships WHERE user_id = $1`, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("failed to query user rooms", zap.String("user_id", userID.String()), zap.Error(err))
|
h.logger.Error("failed to query user rooms", zap.String("user_id", userID.String()), zap.Error(err))
|
||||||
|
|
@ -161,16 +159,30 @@ func (h *Hub) autoSubscribeUserRooms(ctx context.Context, client *Client, userID
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var rid uuid.UUID
|
var rid uuid.UUID
|
||||||
if err := rows.Scan(&rid); err != nil {
|
if err := rows.Scan(&rid); err != nil {
|
||||||
h.logger.Warn("failed to scan room_id", zap.Error(err))
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
roomIDs = append(roomIDs, rid.String())
|
roomIDs = append(roomIDs, rid.String())
|
||||||
}
|
}
|
||||||
h.logger.Info("loaded room IDs from DB (fallback)", zap.String("user_id", userID.String()), zap.Int("count", len(roomIDs)))
|
}
|
||||||
|
|
||||||
|
// Also subscribe to DM channels
|
||||||
|
dmRows, err := h.pool.Query(ctx,
|
||||||
|
`SELECT id FROM dm_channels WHERE user1_id = $1 OR user2_id = $1`, userID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("failed to query DM channels", zap.String("user_id", userID.String()), zap.Error(err))
|
||||||
|
} else {
|
||||||
|
defer dmRows.Close()
|
||||||
|
for dmRows.Next() {
|
||||||
|
var cid uuid.UUID
|
||||||
|
if err := dmRows.Scan(&cid); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
roomIDs = append(roomIDs, cid.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(roomIDs) == 0 {
|
if len(roomIDs) == 0 {
|
||||||
h.logger.Info("no rooms found for user", zap.String("user_id", userID.String()))
|
h.logger.Info("no rooms/channels found for user", zap.String("user_id", userID.String()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,21 +201,12 @@ func (h *Hub) autoSubscribeUserRooms(ctx context.Context, client *Client, userID
|
||||||
|
|
||||||
if h.Subscribe(client.UserID, rid) {
|
if h.Subscribe(client.UserID, rid) {
|
||||||
successCount++
|
successCount++
|
||||||
h.logger.Debug("auto-subscribed to room",
|
|
||||||
zap.String("user_id", client.UserID),
|
|
||||||
zap.String("room_id", rid),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
h.logger.Warn("failed to auto-subscribe to room",
|
|
||||||
zap.String("user_id", client.UserID),
|
|
||||||
zap.String("room_id", rid),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h.logger.Info("auto-subscribe completed",
|
h.logger.Info("auto-subscribe completed",
|
||||||
zap.String("user_id", client.UserID),
|
zap.String("user_id", client.UserID),
|
||||||
zap.Int("total_rooms", len(roomIDs)),
|
zap.Int("total", len(roomIDs)),
|
||||||
zap.Int("successful", successCount),
|
zap.Int("successful", successCount),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +216,7 @@ func (h *Hub) BroadcastToRoom(roomID string, event *streamv1.ServerEvent) {
|
||||||
users, exists := h.rooms[roomID]
|
users, exists := h.rooms[roomID]
|
||||||
if !exists {
|
if !exists {
|
||||||
h.mu.RUnlock()
|
h.mu.RUnlock()
|
||||||
h.logger.Error("attempted to broadcast to room with no subscribers",
|
h.logger.Debug("no subscribers in room, skipping broadcast",
|
||||||
zap.String("room_id", roomID),
|
zap.String("room_id", roomID),
|
||||||
zap.String("event_id", event.GetEventId()),
|
zap.String("event_id", event.GetEventId()),
|
||||||
zap.String("event_type", fmt.Sprintf("%T", event.Payload)),
|
zap.String("event_type", fmt.Sprintf("%T", event.Payload)),
|
||||||
|
|
@ -228,7 +231,7 @@ func (h *Hub) BroadcastToRoom(roomID string, event *streamv1.ServerEvent) {
|
||||||
h.mu.RUnlock()
|
h.mu.RUnlock()
|
||||||
|
|
||||||
if len(userIDs) == 0 {
|
if len(userIDs) == 0 {
|
||||||
h.logger.Error("room has zero subscribers",
|
h.logger.Debug("room has zero subscribers, skipping broadcast",
|
||||||
zap.String("room_id", roomID),
|
zap.String("room_id", roomID),
|
||||||
zap.String("event_id", event.GetEventId()),
|
zap.String("event_id", event.GetEventId()),
|
||||||
zap.String("event_type", fmt.Sprintf("%T", event.Payload)),
|
zap.String("event_type", fmt.Sprintf("%T", event.Payload)),
|
||||||
|
|
@ -522,3 +525,10 @@ func (h *Hub) Shutdown(ctx context.Context) error {
|
||||||
h.logger.Info("all clients force-disconnected")
|
h.logger.Info("all clients force-disconnected")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Hub) RoomHasSubscribers(roomID string) bool {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
subs, ok := h.rooms[roomID]
|
||||||
|
return ok && len(subs) > 0
|
||||||
|
}
|
||||||
|
|
|
||||||
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
6
internal/infra/cache/aside.go
vendored
6
internal/infra/cache/aside.go
vendored
|
|
@ -2,6 +2,7 @@ package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -13,14 +14,15 @@ func NewAsidePattern(cache *Cache) *AsidePattern {
|
||||||
return &AsidePattern{cache: cache}
|
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{}
|
var result interface{}
|
||||||
err := a.cache.Get(ctx, key, &result)
|
err := a.cache.Get(ctx, key, &result)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != ErrCacheMiss {
|
if !errors.Is(err, ErrCacheMiss) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
14
internal/infra/cache/cache.go
vendored
14
internal/infra/cache/cache.go
vendored
|
|
@ -118,3 +118,17 @@ func (c *Cache) DeletePattern(ctx context.Context, pattern string) error {
|
||||||
_, err := pipe.Exec(ctx)
|
_, err := pipe.Exec(ctx)
|
||||||
return err
|
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()
|
||||||
|
}
|
||||||
|
|
|
||||||
51
internal/infra/migrations/007_dm_channels.sql
Normal file
51
internal/infra/migrations/007_dm_channels.sql
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
-- DM channels table
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_channels (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user1_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
user2_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
CONSTRAINT dm_channels_user_order CHECK (user1_id < user2_id),
|
||||||
|
CONSTRAINT dm_channels_unique_pair UNIQUE (user1_id, user2_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_channels_user1 ON dm_channels(user1_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_channels_user2 ON dm_channels(user2_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_channels_updated ON dm_channels(updated_at DESC);
|
||||||
|
|
||||||
|
-- DM participants table (Required by backend logic for membership checks)
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_participants (
|
||||||
|
channel_id UUID NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (channel_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_participants_user ON dm_participants(user_id);
|
||||||
|
|
||||||
|
-- Trigger to auto-populate participants from dm_channels (backward compatibility)
|
||||||
|
CREATE OR REPLACE FUNCTION sync_dm_participants() RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dm_participants (channel_id, user_id) VALUES (NEW.id, NEW.user1_id) ON CONFLICT DO NOTHING;
|
||||||
|
INSERT INTO dm_participants (channel_id, user_id) VALUES (NEW.id, NEW.user2_id) ON CONFLICT DO NOTHING;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_sync_dm_participants ON dm_channels;
|
||||||
|
CREATE TRIGGER trg_sync_dm_participants
|
||||||
|
AFTER INSERT ON dm_channels
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION sync_dm_participants();
|
||||||
|
|
||||||
|
-- Backfill existing channels if any
|
||||||
|
INSERT INTO dm_participants (channel_id, user_id)
|
||||||
|
SELECT id, user1_id FROM dm_channels
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO dm_participants (channel_id, user_id)
|
||||||
|
SELECT id, user2_id FROM dm_channels
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- DM messages use the same messages table with dm_channel_id instead of room_id
|
||||||
|
ALTER TABLE messages ADD COLUMN IF NOT EXISTS dm_channel_id UUID REFERENCES dm_channels(id) ON DELETE CASCADE;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_dm_channel ON messages(dm_channel_id) WHERE dm_channel_id IS NOT NULL;
|
||||||
72
internal/infra/migrations/008_dm_messages.sql
Normal file
72
internal/infra/migrations/008_dm_messages.sql
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_messages (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
channel_id UUID NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE,
|
||||||
|
author_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
content TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
edited_at TIMESTAMPTZ,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
reply_to_id BIGINT REFERENCES dm_messages(id) ON DELETE SET NULL,
|
||||||
|
reply_count INTEGER DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_messages_channel ON dm_messages(channel_id, id DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_messages_reply_to ON dm_messages(reply_to_id) WHERE reply_to_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_message_attachments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
message_id BIGINT NOT NULL REFERENCES dm_messages(id) ON DELETE CASCADE,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
content_type TEXT NOT NULL,
|
||||||
|
size BIGINT NOT NULL DEFAULT 0,
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_message_attachments_message ON dm_message_attachments(message_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_message_reactions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
message_id BIGINT NOT NULL REFERENCES dm_messages(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (message_id, user_id, emoji)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_message_reactions_message ON dm_message_reactions(message_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_message_mentions (
|
||||||
|
message_id BIGINT NOT NULL REFERENCES dm_messages(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (message_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_message_mentions_user ON dm_message_mentions(user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_pinned_messages (
|
||||||
|
channel_id UUID NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE,
|
||||||
|
message_id BIGINT NOT NULL REFERENCES dm_messages(id) ON DELETE CASCADE,
|
||||||
|
pinned_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
pinned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (channel_id, message_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_pinned_messages_channel ON dm_pinned_messages(channel_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_messages_content_search ON dm_messages USING gin(to_tsvector('english', content));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_calls (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
channel_id UUID NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE,
|
||||||
|
started_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
ended_at TIMESTAMPTZ,
|
||||||
|
voice_server_id UUID REFERENCES voice_servers(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_calls_channel ON dm_calls(channel_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_calls_active ON dm_calls(channel_id) WHERE ended_at IS NULL;
|
||||||
56
internal/infra/migrations/009_read_tracking.sql
Normal file
56
internal/infra/migrations/009_read_tracking.sql
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
-- Read tracking for rooms
|
||||||
|
CREATE TABLE IF NOT EXISTS room_read_status (
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
room_id UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
|
||||||
|
last_read_message_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (user_id, room_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_read_status_user ON room_read_status(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_read_status_room ON room_read_status(room_id);
|
||||||
|
|
||||||
|
-- Read tracking for DMs
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_read_status (
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
channel_id UUID NOT NULL REFERENCES dm_channels(id) ON DELETE CASCADE,
|
||||||
|
last_read_message_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (user_id, channel_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_read_status_user ON dm_read_status(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_read_status_channel ON dm_read_status(channel_id);
|
||||||
|
|
||||||
|
-- Typing indicators
|
||||||
|
CREATE TABLE IF NOT EXISTS typing_indicators (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
room_id UUID REFERENCES rooms(id) ON DELETE CASCADE,
|
||||||
|
channel_id UUID REFERENCES dm_channels(id) ON DELETE CASCADE,
|
||||||
|
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
CHECK (room_id IS NOT NULL OR channel_id IS NOT NULL),
|
||||||
|
CHECK (NOT (room_id IS NOT NULL AND channel_id IS NOT NULL))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Unique constraint for room typing (one entry per user per room)
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_typing_room_unique
|
||||||
|
ON typing_indicators(user_id, room_id)
|
||||||
|
WHERE room_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Unique constraint for DM typing (one entry per user per channel)
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_typing_channel_unique
|
||||||
|
ON typing_indicators(user_id, channel_id)
|
||||||
|
WHERE channel_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_typing_room ON typing_indicators(room_id) WHERE room_id IS NOT NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_typing_channel ON typing_indicators(channel_id) WHERE channel_id IS NOT NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_typing_expires ON typing_indicators(expires_at);
|
||||||
|
|
||||||
|
-- Cleanup function for expired typing indicators
|
||||||
|
CREATE OR REPLACE FUNCTION cleanup_expired_typing() RETURNS void AS $$
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM typing_indicators WHERE expires_at < NOW();
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
37
internal/infra/migrations/010_query_optimizations.sql
Normal file
37
internal/infra/migrations/010_query_optimizations.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_messages_channel_created
|
||||||
|
ON dm_messages(channel_id, created_at DESC)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_message_attachments_message_created
|
||||||
|
ON dm_message_attachments(message_id, created_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_message_reactions_message_created
|
||||||
|
ON dm_message_reactions(message_id, created_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_room_created
|
||||||
|
ON messages(room_id, created_at DESC)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_memberships_user_room
|
||||||
|
ON memberships(user_id, room_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friendships_combined
|
||||||
|
ON friendships(user_id1, user_id2);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_friend_requests_to_user_status
|
||||||
|
ON friend_requests(to_user_id, status)
|
||||||
|
WHERE status = 'pending';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_invites_invited_pending
|
||||||
|
ON room_invites(invited_user_id, status)
|
||||||
|
WHERE status = 'pending';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_room_read_status_user_room
|
||||||
|
ON room_read_status(user_id, room_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_read_status_user_channel
|
||||||
|
ON dm_read_status(user_id, channel_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_voice_servers_region_status_load
|
||||||
|
ON voice_servers(region, status, load_score)
|
||||||
|
WHERE status = 'online';
|
||||||
13
internal/infra/migrations/011_avatar_history.sql
Normal file
13
internal/infra/migrations/011_avatar_history.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_thumbnail_url TEXT DEFAULT '';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_avatars (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
full_url TEXT NOT NULL,
|
||||||
|
thumbnail_url TEXT NOT NULL,
|
||||||
|
original_filename TEXT DEFAULT '',
|
||||||
|
size_bytes BIGINT DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_avatars_user_created ON user_avatars(user_id, created_at DESC);
|
||||||
13
internal/infra/migrations/012_fts_improvements.sql
Normal file
13
internal/infra/migrations/012_fts_improvements.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
DROP INDEX IF EXISTS idx_messages_content_search;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_fts
|
||||||
|
ON messages USING gin(to_tsvector('simple', content))
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_dm_messages_content_search;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_messages_fts
|
||||||
|
ON dm_messages USING gin(to_tsvector('simple', content))
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_handle_lower ON users(lower(handle));
|
||||||
|
|
@ -200,12 +200,13 @@ func toProtoMember(m *rooms.Member) *commonv1.Member {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &commonv1.Member{
|
return &commonv1.Member{
|
||||||
RoomId: m.RoomID.String(),
|
RoomId: m.RoomID.String(),
|
||||||
UserId: m.UserID.String(),
|
UserId: m.UserID.String(),
|
||||||
Role: stringToRole(m.Role),
|
Role: stringToRole(m.Role),
|
||||||
Nickname: nickname,
|
Nickname: nickname,
|
||||||
Status: m.Status,
|
Status: m.Status,
|
||||||
JoinedAt: timestamppb.New(m.JoinedAt),
|
JoinedAt: timestamppb.New(m.JoinedAt),
|
||||||
|
LastReadMessageId: m.LastReadMessageID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user