@openora/core 0.4.1-canary.77 → 0.4.1-canary.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/admin-console/plugin.d.ts +2 -0
- package/dist/admin-console/plugin.d.ts.map +1 -1
- package/dist/analytics/plugin.d.ts +2 -0
- package/dist/analytics/plugin.d.ts.map +1 -1
- package/dist/audit/__tests__/audit.service.int.test.js +44 -0
- package/dist/audit/__tests__/audit.service.int.test.js.map +1 -1
- package/dist/audit/plugin.d.ts +2 -0
- package/dist/audit/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.js +12 -0
- package/dist/audit/plugin.js.map +1 -1
- package/dist/casino/gaming/plugin.d.ts +2 -0
- package/dist/casino/gaming/plugin.d.ts.map +1 -1
- package/dist/casino/lobby/plugin.d.ts +2 -0
- package/dist/casino/lobby/plugin.d.ts.map +1 -1
- package/dist/cms/plugin.d.ts +2 -0
- package/dist/cms/plugin.d.ts.map +1 -1
- package/dist/compliance/plugin.d.ts +2 -0
- package/dist/compliance/plugin.d.ts.map +1 -1
- package/dist/contracts/adapters/index.d.ts +4 -0
- package/dist/contracts/adapters/index.d.ts.map +1 -1
- package/dist/contracts/adapters/index.js +2 -0
- package/dist/contracts/adapters/index.js.map +1 -1
- package/dist/contracts/adapters/player-activity.d.ts +6 -0
- package/dist/contracts/adapters/player-activity.d.ts.map +1 -0
- package/dist/contracts/adapters/player-activity.js +3 -0
- package/dist/contracts/adapters/player-activity.js.map +1 -0
- package/dist/contracts/adapters/social-commands.d.ts +12 -0
- package/dist/contracts/adapters/social-commands.d.ts.map +1 -0
- package/dist/contracts/adapters/social-commands.js +7 -0
- package/dist/contracts/adapters/social-commands.js.map +1 -0
- package/dist/contracts/schemas/events.d.ts +12 -0
- package/dist/contracts/schemas/events.d.ts.map +1 -1
- package/dist/contracts/schemas/events.js +7 -0
- package/dist/contracts/schemas/events.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat.service.int.test.js +123 -2
- package/dist/engagement/chat/__tests__/chat.service.int.test.js.map +1 -1
- package/dist/engagement/chat/contract/index.d.ts +4 -0
- package/dist/engagement/chat/contract/index.d.ts.map +1 -1
- package/dist/engagement/chat/contract/index.js +2 -0
- package/dist/engagement/chat/contract/index.js.map +1 -1
- package/dist/engagement/chat/plugin.d.ts +2 -0
- package/dist/engagement/chat/plugin.d.ts.map +1 -1
- package/dist/engagement/chat/plugin.js +2 -2
- package/dist/engagement/chat/plugin.js.map +1 -1
- package/dist/engagement/chat/router/index.d.ts +2 -0
- package/dist/engagement/chat/router/index.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat.service.d.ts +6 -2
- package/dist/engagement/chat/service/chat.service.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat.service.js +26 -13
- package/dist/engagement/chat/service/chat.service.js.map +1 -1
- package/dist/engagement/chat-commands/plugin.d.ts +2 -0
- package/dist/engagement/chat-commands/plugin.d.ts.map +1 -1
- package/dist/engagement/notifications/plugin.d.ts +2 -0
- package/dist/engagement/notifications/plugin.d.ts.map +1 -1
- package/dist/engagement/social/__tests__/social.plugin.int.test.d.ts +2 -0
- package/dist/engagement/social/__tests__/social.plugin.int.test.d.ts.map +1 -0
- package/dist/engagement/social/__tests__/social.plugin.int.test.js +106 -0
- package/dist/engagement/social/__tests__/social.plugin.int.test.js.map +1 -0
- package/dist/engagement/social/__tests__/social.router.int.test.js +44 -2
- package/dist/engagement/social/__tests__/social.router.int.test.js.map +1 -1
- package/dist/engagement/social/__tests__/social.service.int.test.js +252 -6
- package/dist/engagement/social/__tests__/social.service.int.test.js.map +1 -1
- package/dist/engagement/social/contract/index.d.ts +40 -0
- package/dist/engagement/social/contract/index.d.ts.map +1 -1
- package/dist/engagement/social/contract/index.js +20 -0
- package/dist/engagement/social/contract/index.js.map +1 -1
- package/dist/engagement/social/drizzle/migrations/0003_fuzzy_shadow_king.sql +4 -0
- package/dist/engagement/social/drizzle/migrations/meta/0003_snapshot.json +145 -0
- package/dist/engagement/social/drizzle/migrations/meta/_journal.json +7 -0
- package/dist/engagement/social/plugin.d.ts +52 -50
- package/dist/engagement/social/plugin.d.ts.map +1 -1
- package/dist/engagement/social/plugin.js +8 -7
- package/dist/engagement/social/plugin.js.map +1 -1
- package/dist/engagement/social/router/index.d.ts +24 -0
- package/dist/engagement/social/router/index.d.ts.map +1 -1
- package/dist/engagement/social/router/index.js +12 -1
- package/dist/engagement/social/router/index.js.map +1 -1
- package/dist/engagement/social/schema/index.d.ts +17 -0
- package/dist/engagement/social/schema/index.d.ts.map +1 -1
- package/dist/engagement/social/schema/index.js +5 -17
- package/dist/engagement/social/schema/index.js.map +1 -1
- package/dist/engagement/social/service/social.service.d.ts +17 -3
- package/dist/engagement/social/service/social.service.d.ts.map +1 -1
- package/dist/engagement/social/service/social.service.js +103 -12
- package/dist/engagement/social/service/social.service.js.map +1 -1
- package/dist/engagement/social-transfers/plugin.d.ts +2 -0
- package/dist/engagement/social-transfers/plugin.d.ts.map +1 -1
- package/dist/iam/plugin.d.ts +2 -0
- package/dist/iam/plugin.d.ts.map +1 -1
- package/dist/pam/identity/plugin.d.ts +2 -0
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/player-management/__tests__/player.service.int.test.js +29 -0
- package/dist/pam/player-management/__tests__/player.service.int.test.js.map +1 -1
- package/dist/pam/player-management/plugin.d.ts +2 -0
- package/dist/pam/player-management/plugin.d.ts.map +1 -1
- package/dist/pam/player-management/plugin.js +6 -4
- package/dist/pam/player-management/plugin.js.map +1 -1
- package/dist/pam/player-management/service/player.service.d.ts +3 -2
- package/dist/pam/player-management/service/player.service.d.ts.map +1 -1
- package/dist/pam/player-management/service/player.service.js +7 -1
- package/dist/pam/player-management/service/player.service.js.map +1 -1
- package/dist/pam/player-note/plugin.d.ts +2 -0
- package/dist/pam/player-note/plugin.d.ts.map +1 -1
- package/dist/pam/profile/plugin.d.ts +2 -0
- package/dist/pam/profile/plugin.d.ts.map +1 -1
- package/dist/pam/tag/plugin.d.ts +2 -0
- package/dist/pam/tag/plugin.d.ts.map +1 -1
- package/dist/server/runtime/core-token-catalog.d.ts +2 -0
- package/dist/server/runtime/core-token-catalog.d.ts.map +1 -1
- package/dist/server/runtime/core-token-catalog.js +3 -1
- package/dist/server/runtime/core-token-catalog.js.map +1 -1
- package/dist/server/runtime/create-app.d.ts.map +1 -1
- package/dist/server/runtime/create-app.js +7 -1
- package/dist/server/runtime/create-app.js.map +1 -1
- package/dist/wallet/plugin.d.ts +2 -0
- package/dist/wallet/plugin.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
2
|
readonly id: "social";
|
|
3
|
-
readonly dependsOn: readonly ["chat"];
|
|
3
|
+
readonly dependsOn: readonly ["chat", "identity"];
|
|
4
4
|
readonly register: (ctx: import("@openora/core/server").PluginContext<{
|
|
5
|
-
ADMIN_GAME_REPORTING: import("
|
|
6
|
-
ADMIN_GUARD: import("
|
|
7
|
-
ADMIN_PERMISSION_RESOLVER: import("
|
|
8
|
-
ADMIN_PLAYER_ACTIVITY: import("
|
|
9
|
-
ADMIN_USER_DIRECTORY: import("
|
|
10
|
-
ADMIN_WALLET_REPORTING: import("
|
|
11
|
-
AUDIT_WRITER: import("
|
|
12
|
-
AUTH_SESSION: import("
|
|
13
|
-
CACHE: import("
|
|
14
|
-
CHAT_BLOCK_WRITER: import("
|
|
15
|
-
CHAT_MODERATION: import("
|
|
16
|
-
CHAT_REALTIME_CLIENT_AUTHORIZER: import("
|
|
17
|
-
CHAT_REALTIME_TRANSPORT: import("
|
|
18
|
-
CHAT_ROOM_ACCESS: import("
|
|
19
|
-
CHAT_SYSTEM_WRITER: import("
|
|
20
|
-
DRIZZLE: import("
|
|
21
|
-
EMAIL_TEMPLATE_RENDERER: import("
|
|
22
|
-
ERROR_TRACKING: import("
|
|
23
|
-
EVENT_BUS: import("
|
|
24
|
-
GAME_ADAPTER: import("
|
|
25
|
-
GEO_IP_ADAPTER: import("
|
|
26
|
-
GIFT_COMMANDS: import("
|
|
27
|
-
IDENTITY_OPTIONS: import("
|
|
28
|
-
IDENTITY_READER: import("
|
|
29
|
-
IGAMING_CONFIG: import("
|
|
5
|
+
ADMIN_GAME_REPORTING: import("@openora/core/contracts").Token<import("@openora/core/contracts").AdminGameReporting>;
|
|
6
|
+
ADMIN_GUARD: import("@openora/core/contracts").Token<import("@openora/core/server").AdminGuard>;
|
|
7
|
+
ADMIN_PERMISSION_RESOLVER: import("@openora/core/contracts").Token<import("@openora/core/contracts").AdminPermissionResolver>;
|
|
8
|
+
ADMIN_PLAYER_ACTIVITY: import("@openora/core/contracts").Token<import("@openora/core/contracts").AdminPlayerActivity>;
|
|
9
|
+
ADMIN_USER_DIRECTORY: import("@openora/core/contracts").Token<import("@openora/core/contracts").AdminUserDirectory>;
|
|
10
|
+
ADMIN_WALLET_REPORTING: import("@openora/core/contracts").Token<import("@openora/core/contracts").AdminWalletReporting>;
|
|
11
|
+
AUDIT_WRITER: import("@openora/core/contracts").SealedToken<import("@openora/core/contracts").AuditWritePort>;
|
|
12
|
+
AUTH_SESSION: import("@openora/core/contracts").Token<import("@openora/core/server").SessionResolver>;
|
|
13
|
+
CACHE: import("@openora/core/contracts").Token<import("@openora/core/contracts").CacheAdapter>;
|
|
14
|
+
CHAT_BLOCK_WRITER: import("@openora/core/contracts").Token<import("@openora/core/contracts").ChatBlockWriter>;
|
|
15
|
+
CHAT_MODERATION: import("@openora/core/contracts").Token<import("@openora/core/contracts").ChatModeration, string>;
|
|
16
|
+
CHAT_REALTIME_CLIENT_AUTHORIZER: import("@openora/core/contracts").Token<import("@openora/core/contracts").RealtimeClientAuthorizer>;
|
|
17
|
+
CHAT_REALTIME_TRANSPORT: import("@openora/core/contracts").Token<import("@openora/core/contracts").RealtimeTransport>;
|
|
18
|
+
CHAT_ROOM_ACCESS: import("@openora/core/contracts").Token<import("@openora/core/contracts").ChatRoomAccess>;
|
|
19
|
+
CHAT_SYSTEM_WRITER: import("@openora/core/contracts").Token<import("@openora/core/contracts").ChatSystemWriter, string>;
|
|
20
|
+
DRIZZLE: import("@openora/core/contracts").Token<import("@openora/core/server").DrizzleService>;
|
|
21
|
+
EMAIL_TEMPLATE_RENDERER: import("@openora/core/contracts").Token<import("@openora/core/contracts").EmailTemplateRenderer>;
|
|
22
|
+
ERROR_TRACKING: import("@openora/core/contracts").Token<import("@openora/core/contracts").ErrorTrackingAdapter>;
|
|
23
|
+
EVENT_BUS: import("@openora/core/contracts").Token<import("@openora/core/server").EventBus>;
|
|
24
|
+
GAME_ADAPTER: import("@openora/core/contracts").Token<import("@openora/core/contracts").GameAdapter>;
|
|
25
|
+
GEO_IP_ADAPTER: import("@openora/core/contracts").Token<import("@openora/core/contracts").GeoIpAdapter>;
|
|
26
|
+
GIFT_COMMANDS: import("@openora/core/contracts").Token<import("@openora/core/contracts").GiftCommands>;
|
|
27
|
+
IDENTITY_OPTIONS: import("@openora/core/contracts").Token<import("@openora/core/contracts").IdentityServiceOptions>;
|
|
28
|
+
IDENTITY_READER: import("@openora/core/contracts").Token<import("@openora/core/contracts").IdentityReader, string>;
|
|
29
|
+
IGAMING_CONFIG: import("@openora/core/contracts").Token<{
|
|
30
30
|
branding: {
|
|
31
31
|
name: string;
|
|
32
32
|
themePreset?: string | undefined;
|
|
@@ -51,17 +51,17 @@ declare const _default: {
|
|
|
51
51
|
};
|
|
52
52
|
enabledModules?: string[] | undefined;
|
|
53
53
|
}, string>;
|
|
54
|
-
JOB_QUEUE: import("
|
|
55
|
-
KYC_ADAPTER: import("
|
|
56
|
-
KYC_STATUS_WRITER: import("
|
|
57
|
-
KYC_WEBHOOK_VERIFIER: import("
|
|
58
|
-
LOGIN_ENFORCEMENT: import("
|
|
59
|
-
MESSAGE_BROKER: import("
|
|
60
|
-
NOTIFICATION_DELIVERY_ADAPTER: import("
|
|
61
|
-
OUTBOX: import("
|
|
62
|
-
PAYMENT_ADAPTER: import("
|
|
63
|
-
PAYMENT_WEBHOOK_VERIFIER: import("
|
|
64
|
-
PLATFORM_CONFIG: import("
|
|
54
|
+
JOB_QUEUE: import("@openora/core/contracts").Token<import("@openora/core/contracts").JobQueueAdapter>;
|
|
55
|
+
KYC_ADAPTER: import("@openora/core/contracts").Token<import("@openora/core/contracts").KycAdapter>;
|
|
56
|
+
KYC_STATUS_WRITER: import("@openora/core/contracts").Token<import("@openora/core/contracts").KycStatusWriter>;
|
|
57
|
+
KYC_WEBHOOK_VERIFIER: import("@openora/core/contracts").Token<import("@openora/core/contracts").KycWebhookVerifier>;
|
|
58
|
+
LOGIN_ENFORCEMENT: import("@openora/core/contracts").Token<import("@openora/core/contracts").LoginEnforcementPort>;
|
|
59
|
+
MESSAGE_BROKER: import("@openora/core/contracts").Token<import("@openora/core/contracts").MessageBrokerAdapter>;
|
|
60
|
+
NOTIFICATION_DELIVERY_ADAPTER: import("@openora/core/contracts").Token<import("@openora/core/contracts").NotificationDeliveryAdapter>;
|
|
61
|
+
OUTBOX: import("@openora/core/contracts").Token<import("@openora/core/contracts").OutboxWriter>;
|
|
62
|
+
PAYMENT_ADAPTER: import("@openora/core/contracts").Token<import("@openora/core/contracts").PaymentAdapter>;
|
|
63
|
+
PAYMENT_WEBHOOK_VERIFIER: import("@openora/core/contracts").Token<import("@openora/core/contracts").PaymentWebhookVerifier>;
|
|
64
|
+
PLATFORM_CONFIG: import("@openora/core/contracts").Token<{
|
|
65
65
|
features: Record<string, boolean>;
|
|
66
66
|
brands: {
|
|
67
67
|
id: string;
|
|
@@ -93,19 +93,21 @@ declare const _default: {
|
|
|
93
93
|
} | undefined;
|
|
94
94
|
supportedLanguages?: string[] | undefined;
|
|
95
95
|
}, string>;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
96
|
+
PLAYER_ACTIVITY_TRACKER: import("@openora/core/contracts").Token<import("@openora/core/contracts").PlayerActivityTracker>;
|
|
97
|
+
PLAYER_TAGS: import("@openora/core/contracts").Token<import("@openora/core/contracts").PlayerTags>;
|
|
98
|
+
PLAY_ELIGIBILITY: import("@openora/core/contracts").Token<import("@openora/core/contracts").PlayEligibilityPort>;
|
|
99
|
+
RAIN_COMMANDS: import("@openora/core/contracts").Token<import("@openora/core/contracts").RainCommands>;
|
|
100
|
+
RATE_LIMITER: import("@openora/core/contracts").Token<import("@openora/core/contracts").RateLimiterAdapter<`register:${string}` | `login:${string}` | `enable2fa:${string}` | `verify2fa:${string}` | `disable2fa:${string}` | `pwreset-req:${string}` | `pwreset-verify:${string}` | `pwreset:${string}` | `change-password:${string}` | `email-verify:${string}` | `verify-email:${string}` | `wallet-mutation:${string}` | `chat-room-join:${string}` | `chat-send:${string}` | `report-access-denied:${string}`>>;
|
|
101
|
+
REALTIME_CLIENT_AUTHORIZER: import("@openora/core/contracts").Token<import("@openora/core/contracts").RealtimeClientAuthorizer>;
|
|
102
|
+
REALTIME_TRANSPORT: import("@openora/core/contracts").Token<import("@openora/core/contracts").RealtimeTransport>;
|
|
103
|
+
RNG_ADAPTER: import("@openora/core/contracts").Token<import("@openora/core/contracts").RngAdapter>;
|
|
104
|
+
SEND_EMAIL: import("@openora/core/contracts").Token<import("@openora/core/contracts").SendEmailPort>;
|
|
105
|
+
SESSION_COMMANDS: import("@openora/core/contracts").Token<import("@openora/core/contracts").SessionCommands>;
|
|
106
|
+
SMS_ADAPTER: import("@openora/core/contracts").Token<import("@openora/core/contracts").SmsAdapter>;
|
|
107
|
+
SOCIAL_COMMANDS: import("@openora/core/contracts").Token<import("@openora/core/contracts").SocialCommands>;
|
|
108
|
+
TAG_EVALUATION_COMMANDS: import("@openora/core/contracts").Token<import("@openora/core/contracts").TagEvaluationCommands>;
|
|
109
|
+
WALLET_COMMANDS: import("@openora/core/contracts").Token<import("@openora/core/contracts").WalletCommands>;
|
|
110
|
+
WALLET_READER: import("@openora/core/contracts").Token<import("@openora/core/contracts").WalletReader, string>;
|
|
109
111
|
}>) => void;
|
|
110
112
|
};
|
|
111
113
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/engagement/social/plugin.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/engagement/social/plugin.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAMA,wBAe8C"}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { IDENTITY_READER, SOCIAL_COMMANDS } from '@openora/core/contracts';
|
|
1
2
|
import { EVENT_BUS, DRIZZLE } from '@openora/core/server';
|
|
2
3
|
import { SocialService } from './service/social.service.js';
|
|
3
4
|
import { createSocialRouter } from './router/index.js';
|
|
4
|
-
// DI wiring only - no business logic here. sendFriendRequest/getRelationships read
|
|
5
|
-
// the `player` table via the read-only @openora/core/pam/schema/profile subpath
|
|
6
|
-
// (ADR-0020), not a DI token/command port, so no dependsOn is needed to pin load
|
|
7
|
-
// order - the imported pgTable is a plain object available regardless of plugin
|
|
8
|
-
// registration order.
|
|
9
5
|
export default {
|
|
10
6
|
id: 'social',
|
|
11
|
-
dependsOn: ['chat'],
|
|
7
|
+
dependsOn: ['chat', 'identity'],
|
|
12
8
|
register(ctx) {
|
|
13
|
-
|
|
9
|
+
let svc = null;
|
|
10
|
+
const socialService = (c) => (svc ??= new SocialService(c.get(DRIZZLE), c.get(EVENT_BUS), c.get(IDENTITY_READER)));
|
|
11
|
+
ctx.provide(SOCIAL_COMMANDS, (c) => ({
|
|
12
|
+
dissolveFriendshipOnBlock: (tx, blockerId, blockedId) => socialService(c).dissolveFriendshipOnBlock(tx, blockerId, blockedId),
|
|
13
|
+
}));
|
|
14
|
+
ctx.routers.add('social', (c) => createSocialRouter(socialService(c)));
|
|
14
15
|
},
|
|
15
16
|
};
|
|
16
17
|
//# sourceMappingURL=plugin.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/engagement/social/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/engagement/social/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,eAAe;IACb,EAAE,EAAE,QAAQ;IACZ,SAAS,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,GAAG;QACV,IAAI,GAAG,GAAyB,IAAI,CAAC;QACrC,MAAM,aAAa,GAAG,CAAC,CAAmC,EAAE,EAAE,CAC5D,CAAC,GAAG,KAAK,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAExF,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACnC,yBAAyB,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,CACtD,aAAa,CAAC,CAAC,CAAC,CAAC,yBAAyB,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,CAAC;SACvE,CAAC,CAAC,CAAC;QAEJ,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,CAAC;CAC0C,CAAC"}
|
|
@@ -27,5 +27,29 @@ export declare function createSocialRouter(social: SocialService): {
|
|
|
27
27
|
friendshipId: import("zod").ZodNullable<import("zod").ZodUUID>;
|
|
28
28
|
canSendRequest: import("zod").ZodBoolean;
|
|
29
29
|
}, import("zod/v4/core").$strip>>, import("@orpc/server").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
|
|
30
|
+
listFriends: import("@orpc/server").Procedure<import("@orpc/server").MergedInitialContext<OssContext & Record<never, never>, OssContext & Record<never, never>, OssContext>, OssContext, import("zod").ZodObject<{
|
|
31
|
+
page: import("zod").ZodDefault<import("zod").ZodCoercedNumber<unknown>>;
|
|
32
|
+
limit: import("zod").ZodDefault<import("zod").ZodCoercedNumber<unknown>>;
|
|
33
|
+
}, import("zod/v4/core").$strip>, import("zod").ZodObject<{
|
|
34
|
+
items: import("zod").ZodArray<import("zod").ZodObject<{
|
|
35
|
+
userId: import("zod").ZodUUID;
|
|
36
|
+
friendshipId: import("zod").ZodUUID;
|
|
37
|
+
displayName: import("zod").ZodString;
|
|
38
|
+
status: import("zod").ZodEnum<{
|
|
39
|
+
online: "online";
|
|
40
|
+
offline: "offline";
|
|
41
|
+
}>;
|
|
42
|
+
lastSeenAt: import("zod").ZodNullable<import("zod").ZodISODateTime>;
|
|
43
|
+
isIgnored: import("zod").ZodBoolean;
|
|
44
|
+
}, import("zod/v4/core").$strip>>;
|
|
45
|
+
total: import("zod").ZodNumber;
|
|
46
|
+
page: import("zod").ZodNumber;
|
|
47
|
+
limit: import("zod").ZodNumber;
|
|
48
|
+
}, import("zod/v4/core").$strip>, import("@orpc/server").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
|
|
49
|
+
removeFriend: import("@orpc/server").Procedure<import("@orpc/server").MergedInitialContext<OssContext & Record<never, never>, OssContext & Record<never, never>, OssContext>, OssContext, import("zod").ZodObject<{
|
|
50
|
+
targetUserId: import("zod").ZodUUID;
|
|
51
|
+
}, import("zod/v4/core").$strip>, import("zod").ZodObject<{
|
|
52
|
+
success: import("zod").ZodLiteral<true>;
|
|
53
|
+
}, import("zod/v4/core").$strip>, import("@orpc/server").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
|
|
30
54
|
};
|
|
31
55
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/engagement/social/router/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAE7E,OAAO,EACL,aAAa,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/engagement/social/router/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAE7E,OAAO,EACL,aAAa,EASd,MAAM,8BAA8B,CAAC;AAKtC,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwCvD"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { implement } from '@orpc/server';
|
|
2
2
|
import { getUserId, mapErrors } from '@openora/core/server';
|
|
3
3
|
import { socialContract } from '../contract/index.js';
|
|
4
|
-
import { SelfFriendRequestError, FriendRequestTargetNotFoundError, FriendRequestUnavailableError, AlreadyFriendsError, RequestAlreadyPendingError, FriendRequestRefusedError, BlockedBySelfError, } from '../service/social.service.js';
|
|
4
|
+
import { SelfFriendRequestError, FriendRequestTargetNotFoundError, FriendRequestUnavailableError, AlreadyFriendsError, RequestAlreadyPendingError, FriendRequestRefusedError, BlockedBySelfError, FriendshipNotFoundError, } from '../service/social.service.js';
|
|
5
5
|
// oRPC router factory for Social. plugin.ts builds the service from the
|
|
6
6
|
// container and passes it here; each procedure delegates to the service. Keep this
|
|
7
7
|
// thin: resolve the caller, call the service, map domain errors - no business rules.
|
|
@@ -26,6 +26,17 @@ export function createSocialRouter(social) {
|
|
|
26
26
|
const callerId = getUserId(context);
|
|
27
27
|
return social.getRelationships(callerId, input.userIds);
|
|
28
28
|
}),
|
|
29
|
+
listFriends: os.listFriends.handler(({ input, context }) => {
|
|
30
|
+
const callerId = getUserId(context);
|
|
31
|
+
return social.listFriends(callerId, { page: input.page, limit: input.limit });
|
|
32
|
+
}),
|
|
33
|
+
removeFriend: os.removeFriend.handler(({ input, context }) => {
|
|
34
|
+
const callerId = getUserId(context);
|
|
35
|
+
return mapErrors({ NOT_FOUND: [FriendshipNotFoundError] }, async () => {
|
|
36
|
+
await social.removeFriend(callerId, input.targetUserId);
|
|
37
|
+
return { success: true };
|
|
38
|
+
});
|
|
39
|
+
}),
|
|
29
40
|
});
|
|
30
41
|
}
|
|
31
42
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/engagement/social/router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAmB,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,sBAAsB,EACtB,gCAAgC,EAChC,6BAA6B,EAC7B,mBAAmB,EACnB,0BAA0B,EAC1B,yBAAyB,EACzB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/engagement/social/router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAmB,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAEL,sBAAsB,EACtB,gCAAgC,EAChC,6BAA6B,EAC7B,mBAAmB,EACnB,0BAA0B,EAC1B,yBAAyB,EACzB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AAEtC,wEAAwE;AACxE,mFAAmF;AACnF,qFAAqF;AACrF,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,MAAM,EAAE,GAAG,SAAS,CAAC,cAAc,CAAC,CAAC,QAAQ,EAAc,CAAC;IAE5D,OAAO,EAAE,CAAC,MAAM,CAAC;QACf,iBAAiB,EAAE,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;YACrE,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;YACpC,OAAO,SAAS,CACd;gBACE,WAAW,EAAE,CAAC,sBAAsB,CAAC;gBACrC,SAAS,EAAE,CAAC,gCAAgC,CAAC;gBAC7C,QAAQ,EAAE;oBACR,mBAAmB;oBACnB,0BAA0B;oBAC1B,yBAAyB;oBACzB,6BAA6B;oBAC7B,kBAAkB;iBACnB;aACF,EACD,GAAG,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC,CAC7D,CAAC;QACJ,CAAC,CAAC;QAEF,gBAAgB,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;YACnE,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;YACpC,OAAO,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC1D,CAAC,CAAC;QAEF,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;YACzD,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;YACpC,OAAO,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;QAChF,CAAC,CAAC;QAEF,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;YAC3D,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;YACpC,OAAO,SAAS,CAAC,EAAE,SAAS,EAAE,CAAC,uBAAuB,CAAC,EAAE,EAAE,KAAK,IAAI,EAAE;gBACpE,MAAM,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;gBACxD,OAAO,EAAE,OAAO,EAAE,IAAa,EAAE,CAAC;YACpC,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;KACH,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -121,6 +121,23 @@ export declare const friendship: import("drizzle-orm/pg-core").PgTableWithColumn
|
|
|
121
121
|
identity: undefined;
|
|
122
122
|
generated: undefined;
|
|
123
123
|
}, {}, {}>;
|
|
124
|
+
removedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
125
|
+
name: "removedAt";
|
|
126
|
+
tableName: "friendship";
|
|
127
|
+
dataType: "date";
|
|
128
|
+
columnType: "PgTimestamp";
|
|
129
|
+
data: Date;
|
|
130
|
+
driverParam: string;
|
|
131
|
+
notNull: false;
|
|
132
|
+
hasDefault: false;
|
|
133
|
+
isPrimaryKey: false;
|
|
134
|
+
isAutoincrement: false;
|
|
135
|
+
hasRuntimeDefault: false;
|
|
136
|
+
enumValues: undefined;
|
|
137
|
+
baseColumn: never;
|
|
138
|
+
identity: undefined;
|
|
139
|
+
generated: undefined;
|
|
140
|
+
}, {}, {}>;
|
|
124
141
|
};
|
|
125
142
|
dialect: "pg";
|
|
126
143
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/engagement/social/schema/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/engagement/social/schema/index.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCtB,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,YAAY,CAAC"}
|
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
import { sql } from 'drizzle-orm';
|
|
2
2
|
import { pgTable, uuid, timestamp, uniqueIndex, index, check } from 'drizzle-orm/pg-core';
|
|
3
|
-
// Drizzle tables owned by the Social module.
|
|
4
|
-
// Rules:
|
|
5
|
-
// - Column names come from the key via the snake_case casing config - never pass
|
|
6
|
-
// an explicit string (lint: drizzle-snake-case).
|
|
7
|
-
// - Timestamps are always `withTimezone` (lint: no-naive-timestamp).
|
|
8
|
-
// - Do NOT add FK references to tables owned by another domain - reference by id.
|
|
9
|
-
// One row per friendship, directional at creation (requester -> addressee) but the
|
|
10
|
-
// PAIR is what must stay unique regardless of direction - the canonical-pair unique
|
|
11
|
-
// index below is what makes duplicate-request and the mutual/simultaneous-request
|
|
12
|
-
// race condition (see social.service.ts sendFriendRequest) safe at the DB level: an
|
|
13
|
-
// INSERT is attempted directly (no pre-SELECT), and a unique-violation on the pair
|
|
14
|
-
// key is caught and resolved. A request is pending while both decision timestamps
|
|
15
|
-
// are null, accepted when acceptedAt is set, and refused when refusedAt is set.
|
|
16
3
|
export const friendship = pgTable('friendship', {
|
|
17
4
|
id: uuid().primaryKey().defaultRandom(),
|
|
18
5
|
requesterId: uuid().notNull(), // bare id - cross-module (user from pam), no FK
|
|
@@ -24,13 +11,14 @@ export const friendship = pgTable('friendship', {
|
|
|
24
11
|
.$onUpdateFn(() => new Date()),
|
|
25
12
|
acceptedAt: timestamp({ withTimezone: true }),
|
|
26
13
|
refusedAt: timestamp({ withTimezone: true }),
|
|
14
|
+
removedAt: timestamp({ withTimezone: true }),
|
|
27
15
|
}, (t) => [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
uniqueIndex('friendship_pair_key').on(sql `LEAST(${t.requesterId}, ${t.addresseeId})`, sql `GREATEST(${t.requesterId}, ${t.addresseeId})`),
|
|
16
|
+
uniqueIndex('friendship_pair_key')
|
|
17
|
+
.on(sql `LEAST(${t.requesterId}, ${t.addresseeId})`, sql `GREATEST(${t.requesterId}, ${t.addresseeId})`)
|
|
18
|
+
.where(sql `${t.removedAt} IS NULL`),
|
|
32
19
|
index('friendship_addressee_idx').on(t.addresseeId),
|
|
33
20
|
index('friendship_requester_idx').on(t.requesterId),
|
|
34
21
|
check('friendship_one_decision_key', sql `NOT (${t.acceptedAt} IS NOT NULL AND ${t.refusedAt} IS NOT NULL)`),
|
|
22
|
+
check('friendship_removed_requires_accepted_key', sql `NOT (${t.removedAt} IS NOT NULL AND ${t.acceptedAt} IS NULL)`),
|
|
35
23
|
]);
|
|
36
24
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/engagement/social/schema/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAE1F,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/engagement/social/schema/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAE1F,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAC/B,YAAY,EACZ;IACE,EAAE,EAAE,IAAI,EAAE,CAAC,UAAU,EAAE,CAAC,aAAa,EAAE;IACvC,WAAW,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,gDAAgD;IAC/E,WAAW,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,gDAAgD;IAC/E,SAAS,EAAE,SAAS,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE;IACnE,SAAS,EAAE,SAAS,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;SACzC,OAAO,EAAE;SACT,UAAU,EAAE;SACZ,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;IAChC,UAAU,EAAE,SAAS,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IAC7C,SAAS,EAAE,SAAS,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IAC5C,SAAS,EAAE,SAAS,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;CAC7C,EACD,CAAC,CAAC,EAAE,EAAE,CAAC;IACL,WAAW,CAAC,qBAAqB,CAAC;SAC/B,EAAE,CACD,GAAG,CAAA,SAAS,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,WAAW,GAAG,EAC9C,GAAG,CAAA,YAAY,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,WAAW,GAAG,CAClD;SACA,KAAK,CAAC,GAAG,CAAA,GAAG,CAAC,CAAC,SAAS,UAAU,CAAC;IACrC,KAAK,CAAC,0BAA0B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;IACnD,KAAK,CAAC,0BAA0B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;IACnD,KAAK,CACH,6BAA6B,EAC7B,GAAG,CAAA,QAAQ,CAAC,CAAC,UAAU,oBAAoB,CAAC,CAAC,SAAS,eAAe,CACtE;IACD,KAAK,CACH,0CAA0C,EAC1C,GAAG,CAAA,QAAQ,CAAC,CAAC,SAAS,oBAAoB,CAAC,CAAC,UAAU,WAAW,CAClE;CACF,CACF,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DrizzleService, type EventBus } from '@openora/core/server';
|
|
2
|
-
import { type Uuid } from '@openora/core/contracts';
|
|
3
|
-
import { type Friendship, type Relationship } from '../contract/index.js';
|
|
2
|
+
import { type FriendshipDissolvedPayload, type IdentityReader, type Uuid } from '@openora/core/contracts';
|
|
3
|
+
import { type Friendship, type FriendListEntry, type Relationship } from '../contract/index.js';
|
|
4
4
|
export declare const SelfFriendRequestError: new () => Error;
|
|
5
5
|
export declare const FriendRequestTargetNotFoundError: new (id: string) => Error;
|
|
6
6
|
export declare const FriendRequestUnavailableError: new () => Error;
|
|
@@ -8,10 +8,12 @@ export declare const BlockedBySelfError: new () => Error;
|
|
|
8
8
|
export declare const AlreadyFriendsError: new () => Error;
|
|
9
9
|
export declare const RequestAlreadyPendingError: new () => Error;
|
|
10
10
|
export declare const FriendRequestRefusedError: new () => Error;
|
|
11
|
+
export declare const FriendshipNotFoundError: new (id: string) => Error;
|
|
11
12
|
export declare class SocialService {
|
|
12
13
|
private readonly drizzle;
|
|
13
14
|
private readonly events;
|
|
14
|
-
|
|
15
|
+
private readonly identityReader;
|
|
16
|
+
constructor(drizzle: DrizzleService, events: EventBus, identityReader: IdentityReader);
|
|
15
17
|
/**
|
|
16
18
|
* Sends a friend request from `callerId` to `targetUserId`. The insert is
|
|
17
19
|
* attempted directly against the canonical-pair unique index (no pre-SELECT,
|
|
@@ -35,5 +37,17 @@ export declare class SocialService {
|
|
|
35
37
|
* ids) rather than N+1 per target.
|
|
36
38
|
*/
|
|
37
39
|
getRelationships(callerId: Uuid, targetUserIds: readonly Uuid[]): Promise<Relationship[]>;
|
|
40
|
+
private dissolveFriendship;
|
|
41
|
+
removeFriend(callerId: Uuid, targetUserId: Uuid): Promise<void>;
|
|
42
|
+
dissolveFriendshipOnBlock(tx: unknown, blockerId: Uuid, blockedId: Uuid): Promise<FriendshipDissolvedPayload | null>;
|
|
43
|
+
listFriends(callerId: Uuid, { page, limit }: {
|
|
44
|
+
page: number;
|
|
45
|
+
limit: number;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
items: FriendListEntry[];
|
|
48
|
+
total: number;
|
|
49
|
+
page: number;
|
|
50
|
+
limit: number;
|
|
51
|
+
}>;
|
|
38
52
|
}
|
|
39
53
|
//# sourceMappingURL=social.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"social.service.d.ts","sourceRoot":"","sources":["../../../../src/engagement/social/service/social.service.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"social.service.d.ts","sourceRoot":"","sources":["../../../../src/engagement/social/service/social.service.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,QAAQ,EAOd,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,cAAc,EACnB,KAAK,IAAI,EACV,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAShG,eAAO,MAAM,sBAAsB,iBAGlC,CAAC;AAKF,eAAO,MAAM,gCAAgC,2BAA2C,CAAC;AAOzF,eAAO,MAAM,6BAA6B,iBAGzC,CAAC;AAGF,eAAO,MAAM,kBAAkB,iBAG9B,CAAC;AACF,eAAO,MAAM,mBAAmB,iBAG/B,CAAC;AACF,eAAO,MAAM,0BAA0B,iBAGtC,CAAC;AACF,eAAO,MAAM,yBAAyB,iBAGrC,CAAC;AAEF,eAAO,MAAM,uBAAuB,2BAAkC,CAAC;AAkCvE,qBAAa,aAAa;IAEtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc;gBAFd,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,QAAQ,EAChB,cAAc,EAAE,cAAc;IAGjD;;;;;;;;OAQG;IACG,iBAAiB,CAAC,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC;IAwEhF;;;;;OAKG;YACW,mBAAmB;IAuCjC;;;;OAIG;IACG,gBAAgB,CAAC,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,SAAS,IAAI,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;YA2GjF,kBAAkB;IAgC1B,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAc/D,yBAAyB,CAC7B,EAAE,EAAE,OAAO,EACX,SAAS,EAAE,IAAI,EACf,SAAS,EAAE,IAAI,GACd,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAAC;IAWvC,WAAW,CACf,QAAQ,EAAE,IAAI,EACd,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAC/C,OAAO,CAAC;QAAE,KAAK,EAAE,eAAe,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAwFrF"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { and, eq, inArray, isNull, or } from 'drizzle-orm';
|
|
2
|
-
import { createDomainError, makeConflictError, makeNotFoundError, serializeRow, } from '@openora/core/server';
|
|
3
|
-
import { chatUserBlock } from '@openora/core/engagement/schema/chat';
|
|
1
|
+
import { and, count, desc, eq, inArray, isNotNull, isNull, or } from 'drizzle-orm';
|
|
2
|
+
import { createDomainError, createLogger, makeConflictError, makeNotFoundError, pageToOffset, serializeRow, } from '@openora/core/server';
|
|
3
|
+
import { chatUserBlock, chatUserIgnore } from '@openora/core/engagement/schema/chat';
|
|
4
4
|
import { player } from '@openora/core/pam/schema/profile';
|
|
5
5
|
import { friendship } from '../schema/index.js';
|
|
6
|
+
const logger = createLogger('social');
|
|
7
|
+
const ONLINE_STATUS_WINDOW_MS = 3 * 60 * 1000;
|
|
6
8
|
// The caller targeted themselves (SendFriendRequestInputSchema.targetUserId === callerId).
|
|
7
9
|
export const SelfFriendRequestError = createDomainError('SelfFriendRequestError', () => 'You cannot send a friend request to yourself');
|
|
8
10
|
// A genuinely nonexistent target only - never thrown for a target that exists but
|
|
@@ -20,12 +22,7 @@ export const BlockedBySelfError = makeConflictError('BLOCKED_BY_SELF', 'You have
|
|
|
20
22
|
export const AlreadyFriendsError = makeConflictError('ALREADY_FRIENDS', 'You are already friends with this player');
|
|
21
23
|
export const RequestAlreadyPendingError = makeConflictError('REQUEST_ALREADY_PENDING', 'A friend request to this player is already pending');
|
|
22
24
|
export const FriendRequestRefusedError = makeConflictError('FRIEND_REQUEST_REFUSED', 'This friend request was refused');
|
|
23
|
-
|
|
24
|
-
// (friendship_pair_key), so any 23505 on an insert into it is unambiguous - no
|
|
25
|
-
// constraint-name check needed. drizzle-orm wraps the driver error in a
|
|
26
|
-
// DrizzleQueryError, so the pg error (and its `.code`) is on `.cause`, not the
|
|
27
|
-
// thrown error itself - checked against a real Postgres unique-index violation
|
|
28
|
-
// in social.service.int.test.ts (a mocked query builder can't catch this).
|
|
25
|
+
export const FriendshipNotFoundError = makeNotFoundError('Friendship');
|
|
29
26
|
function pgErrorCode(e) {
|
|
30
27
|
if (typeof e !== 'object' || e === null) {
|
|
31
28
|
return undefined;
|
|
@@ -54,9 +51,11 @@ function pairCondition(userA, userB) {
|
|
|
54
51
|
export class SocialService {
|
|
55
52
|
drizzle;
|
|
56
53
|
events;
|
|
57
|
-
|
|
54
|
+
identityReader;
|
|
55
|
+
constructor(drizzle, events, identityReader) {
|
|
58
56
|
this.drizzle = drizzle;
|
|
59
57
|
this.events = events;
|
|
58
|
+
this.identityReader = identityReader;
|
|
60
59
|
}
|
|
61
60
|
/**
|
|
62
61
|
* Sends a friend request from `callerId` to `targetUserId`. The insert is
|
|
@@ -144,7 +143,7 @@ export class SocialService {
|
|
|
144
143
|
const rows = await tx
|
|
145
144
|
.select()
|
|
146
145
|
.from(friendship)
|
|
147
|
-
.where(pairCondition(callerId, targetUserId))
|
|
146
|
+
.where(and(pairCondition(callerId, targetUserId), isNull(friendship.removedAt)))
|
|
148
147
|
.for('update')
|
|
149
148
|
.limit(1);
|
|
150
149
|
const existing = rows[0];
|
|
@@ -191,7 +190,7 @@ export class SocialService {
|
|
|
191
190
|
const friendships = await this.drizzle.db
|
|
192
191
|
.select()
|
|
193
192
|
.from(friendship)
|
|
194
|
-
.where(or(and(eq(friendship.requesterId, callerId), inArray(friendship.addresseeId, uniqueTargetIds)), and(inArray(friendship.requesterId, uniqueTargetIds), eq(friendship.addresseeId, callerId))));
|
|
193
|
+
.where(and(or(and(eq(friendship.requesterId, callerId), inArray(friendship.addresseeId, uniqueTargetIds)), and(inArray(friendship.requesterId, uniqueTargetIds), eq(friendship.addresseeId, callerId))), isNull(friendship.removedAt)));
|
|
195
194
|
const friendshipByTargetId = new Map();
|
|
196
195
|
for (const row of friendships) {
|
|
197
196
|
const targetId = row.requesterId === callerId ? row.addresseeId : row.requesterId;
|
|
@@ -244,6 +243,98 @@ export class SocialService {
|
|
|
244
243
|
return { userId, status: 'none', friendshipId: null, canSendRequest: true };
|
|
245
244
|
});
|
|
246
245
|
}
|
|
246
|
+
async dissolveFriendship(tx, userA, userB, actorId, reason) {
|
|
247
|
+
const rows = await tx
|
|
248
|
+
.update(friendship)
|
|
249
|
+
.set({ removedAt: new Date() })
|
|
250
|
+
.where(and(pairCondition(userA, userB), isNull(friendship.removedAt), isNotNull(friendship.acceptedAt)))
|
|
251
|
+
.returning();
|
|
252
|
+
const removed = rows[0];
|
|
253
|
+
if (!removed) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
friendshipId: removed.id,
|
|
258
|
+
actorId,
|
|
259
|
+
actorPlayerId: await this.identityReader.getPlayerIdByUserIdSafe(actorId),
|
|
260
|
+
otherUserId: removed.requesterId === actorId ? removed.addresseeId : removed.requesterId,
|
|
261
|
+
reason,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async removeFriend(callerId, targetUserId) {
|
|
265
|
+
const dissolved = await this.dissolveFriendship(this.drizzle.db, callerId, targetUserId, callerId, 'removed_by_player');
|
|
266
|
+
if (!dissolved) {
|
|
267
|
+
throw new FriendshipNotFoundError(targetUserId);
|
|
268
|
+
}
|
|
269
|
+
this.events.emit('social.friendship.removed', dissolved);
|
|
270
|
+
}
|
|
271
|
+
async dissolveFriendshipOnBlock(tx, blockerId, blockedId) {
|
|
272
|
+
const dissolved = await this.dissolveFriendship(tx, blockerId, blockedId, blockerId, 'blocked');
|
|
273
|
+
return dissolved ?? null;
|
|
274
|
+
}
|
|
275
|
+
async listFriends(callerId, { page, limit }) {
|
|
276
|
+
const where = and(or(eq(friendship.requesterId, callerId), eq(friendship.addresseeId, callerId)), isNotNull(friendship.acceptedAt), isNull(friendship.removedAt));
|
|
277
|
+
const [rows, countRows] = await Promise.all([
|
|
278
|
+
this.drizzle.db
|
|
279
|
+
.select({
|
|
280
|
+
id: friendship.id,
|
|
281
|
+
requesterId: friendship.requesterId,
|
|
282
|
+
addresseeId: friendship.addresseeId,
|
|
283
|
+
createdAt: friendship.createdAt,
|
|
284
|
+
})
|
|
285
|
+
.from(friendship)
|
|
286
|
+
.where(where)
|
|
287
|
+
.orderBy(desc(friendship.createdAt))
|
|
288
|
+
.limit(limit)
|
|
289
|
+
.offset(pageToOffset(page, limit)),
|
|
290
|
+
this.drizzle.db.select({ n: count() }).from(friendship).where(where),
|
|
291
|
+
]);
|
|
292
|
+
const otherUserIds = rows.map((row) => row.requesterId === callerId ? row.addresseeId : row.requesterId);
|
|
293
|
+
const [players, ignores] = otherUserIds.length > 0
|
|
294
|
+
? await Promise.all([
|
|
295
|
+
this.drizzle.db
|
|
296
|
+
.select({
|
|
297
|
+
userId: player.userId,
|
|
298
|
+
displayName: player.displayName,
|
|
299
|
+
lastSeenAt: player.lastSeenAt,
|
|
300
|
+
status: player.status,
|
|
301
|
+
})
|
|
302
|
+
.from(player)
|
|
303
|
+
.where(inArray(player.userId, otherUserIds)),
|
|
304
|
+
this.drizzle.db
|
|
305
|
+
.select({ ignoredId: chatUserIgnore.ignoredId })
|
|
306
|
+
.from(chatUserIgnore)
|
|
307
|
+
.where(and(eq(chatUserIgnore.ignorerId, callerId), isNull(chatUserIgnore.removedAt), inArray(chatUserIgnore.ignoredId, otherUserIds))),
|
|
308
|
+
])
|
|
309
|
+
: [[], []];
|
|
310
|
+
const playerByUserId = new Map(players.map((p) => [p.userId, p]));
|
|
311
|
+
const ignoredIds = new Set(ignores.map((i) => i.ignoredId));
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
const items = rows.flatMap((row) => {
|
|
314
|
+
const userId = row.requesterId === callerId ? row.addresseeId : row.requesterId;
|
|
315
|
+
const targetPlayer = playerByUserId.get(userId);
|
|
316
|
+
if (!targetPlayer) {
|
|
317
|
+
logger.error({ friendshipId: row.id, userId }, 'listFriends: player row missing for friend');
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
if (targetPlayer.status === 'suspended' || targetPlayer.status === 'closed') {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
const isOnline = targetPlayer.lastSeenAt !== null &&
|
|
324
|
+
now - targetPlayer.lastSeenAt.getTime() <= ONLINE_STATUS_WINDOW_MS;
|
|
325
|
+
return [
|
|
326
|
+
serializeRow({
|
|
327
|
+
userId,
|
|
328
|
+
friendshipId: row.id,
|
|
329
|
+
displayName: targetPlayer.displayName,
|
|
330
|
+
status: isOnline ? 'online' : 'offline',
|
|
331
|
+
lastSeenAt: targetPlayer.lastSeenAt,
|
|
332
|
+
isIgnored: ignoredIds.has(userId),
|
|
333
|
+
}, { dateFields: ['lastSeenAt'] }),
|
|
334
|
+
];
|
|
335
|
+
});
|
|
336
|
+
return { items, total: Number(countRows[0]?.n ?? 0), page, limit };
|
|
337
|
+
}
|
|
247
338
|
}
|
|
248
339
|
function pairBlockCondition(callerId, targetUserId) {
|
|
249
340
|
return or(and(eq(chatUserBlock.blockerId, targetUserId), eq(chatUserBlock.blockedId, callerId)), and(eq(chatUserBlock.blockerId, callerId), eq(chatUserBlock.blockedId, targetUserId)));
|