@quenty/chatproviderservice 9.67.3 → 9.67.4
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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [9.67.4](https://github.com/Quenty/NevermoreEngine/compare/@quenty/chatproviderservice@9.67.3...@quenty/chatproviderservice@9.67.4) (2026-07-29)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **chatproviderservice:** let a PlayerMock be chat tagged ([#776](https://github.com/Quenty/NevermoreEngine/issues/776)) ([7d1843b](https://github.com/Quenty/NevermoreEngine/commit/7d1843b24225a3d3e2f70c2743688b28a0e2acb4))
|
|
11
|
+
|
|
6
12
|
## [9.67.3](https://github.com/Quenty/NevermoreEngine/compare/@quenty/chatproviderservice@9.67.2...@quenty/chatproviderservice@9.67.3) (2026-07-29)
|
|
7
13
|
|
|
8
14
|
**Note:** Version bump only for package @quenty/chatproviderservice
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/chatproviderservice",
|
|
3
|
-
"version": "9.67.
|
|
3
|
+
"version": "9.67.4",
|
|
4
4
|
"description": "Provide wrapper around chat system to allow tags to be set",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"@quenty/nevermore-test-runner": "1.5.0",
|
|
44
44
|
"@quenty/permissionprovider": "14.49.0",
|
|
45
45
|
"@quenty/playerbinder": "14.46.0",
|
|
46
|
+
"@quenty/playermock": "1.6.0",
|
|
46
47
|
"@quenty/playerutils": "8.41.0",
|
|
47
48
|
"@quenty/preferredparentutils": "4.4.2",
|
|
48
49
|
"@quenty/promise": "10.23.0",
|
|
@@ -60,5 +61,5 @@
|
|
|
60
61
|
"publishConfig": {
|
|
61
62
|
"access": "public"
|
|
62
63
|
},
|
|
63
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "98488545769a2a9586da14845730dfb36d718744"
|
|
64
65
|
}
|
|
@@ -7,6 +7,7 @@ local require = require(script.Parent.loader).load(script)
|
|
|
7
7
|
|
|
8
8
|
local HttpService = game:GetService("HttpService")
|
|
9
9
|
local Players = game:GetService("Players")
|
|
10
|
+
local RunService = game:GetService("RunService")
|
|
10
11
|
local TextChatService = game:GetService("TextChatService")
|
|
11
12
|
|
|
12
13
|
local Binder = require("Binder")
|
|
@@ -49,6 +50,12 @@ function ChatProviderServiceClient.Init(self: ChatProviderServiceClient, service
|
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
function ChatProviderServiceClient.Start(self: ChatProviderServiceClient): ()
|
|
53
|
+
-- The engine rejects this assignment outside a client, and a headless test that boots a client bag
|
|
54
|
+
-- on the server would take the whole bag down with it. Nothing to render there anyway.
|
|
55
|
+
if not RunService:IsClient() then
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
TextChatService.OnIncomingMessage = function(textChatMessage): TextChatMessageProperties?
|
|
53
60
|
self.MessageIncoming:Fire(textChatMessage)
|
|
54
61
|
|
|
@@ -17,6 +17,7 @@ local LocalizedTextUtils = require("LocalizedTextUtils")
|
|
|
17
17
|
local Maid = require("Maid")
|
|
18
18
|
local Observable = require("Observable")
|
|
19
19
|
local PermissionLevel = require("PermissionLevel")
|
|
20
|
+
local PlayerMock = require("PlayerMock")
|
|
20
21
|
local PreferredParentUtils = require("PreferredParentUtils")
|
|
21
22
|
local Promise = require("Promise")
|
|
22
23
|
local Rx = require("Rx")
|
|
@@ -173,7 +174,7 @@ function ChatProviderService.PromiseAddChatTag(
|
|
|
173
174
|
player: Player,
|
|
174
175
|
chatTagData: ChatTagDataUtils.ChatTagData
|
|
175
176
|
): Promise.Promise<Instance>
|
|
176
|
-
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
177
|
+
assert((typeof(player) == "Instance" and player:IsA("Player")) or PlayerMock.isMock(player), "Bad player")
|
|
177
178
|
assert(ChatTagDataUtils.isChatTagData(chatTagData), "Bad chatTagData")
|
|
178
179
|
|
|
179
180
|
local hasChatTagBinder = self._serviceBag:GetService(HasChatTags)
|
|
@@ -189,7 +190,7 @@ end
|
|
|
189
190
|
@param player Player
|
|
190
191
|
]=]
|
|
191
192
|
function ChatProviderService.ClearChatTags(self: ChatProviderService, player: Player)
|
|
192
|
-
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
193
|
+
assert((typeof(player) == "Instance" and player:IsA("Player")) or PlayerMock.isMock(player), "Bad player")
|
|
193
194
|
|
|
194
195
|
local hasChatTagBinder = self._serviceBag:GetService(HasChatTags)
|
|
195
196
|
local hasChatTags = hasChatTagBinder:Get(player)
|
|
@@ -300,4 +301,15 @@ function ChatProviderService._promiseSpeaker(self: ChatProviderService, speakerN
|
|
|
300
301
|
end)
|
|
301
302
|
end
|
|
302
303
|
|
|
304
|
+
--[=[
|
|
305
|
+
Cleans up the service. Should be done via [ServiceBag].
|
|
306
|
+
|
|
307
|
+
Load-bearing rather than tidy: the developer and admin tags hold a subscription to every player in
|
|
308
|
+
the place, which outlived this service without it. A destroyed bag then went on resolving
|
|
309
|
+
permissions and tagging players through services it no longer owns.
|
|
310
|
+
]=]
|
|
311
|
+
function ChatProviderService.Destroy(self: ChatProviderService)
|
|
312
|
+
self._maid:DoCleaning()
|
|
313
|
+
end
|
|
314
|
+
|
|
303
315
|
return ChatProviderService
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Coverage for the server chat-tag API against a PlayerMock. No real player joins a headless Open
|
|
4
|
+
Cloud place, so a mock has to be able to reach a chat tag the same way a joined player does --
|
|
5
|
+
through the PlayerBinder-backed HasChatTags, with the test creating nothing by hand but the mock.
|
|
6
|
+
|
|
7
|
+
The built-in (dev) and (mod) tags are switched off per test: they resolve through PermissionService,
|
|
8
|
+
which is a different question than whether a mock can be tagged at all.
|
|
9
|
+
|
|
10
|
+
@class ChatProviderService.spec.lua
|
|
11
|
+
]]
|
|
12
|
+
|
|
13
|
+
local require = require(script.Parent.loader).load(script)
|
|
14
|
+
|
|
15
|
+
local ChatProviderService = require("ChatProviderService")
|
|
16
|
+
local ChatTagConstants = require("ChatTagConstants")
|
|
17
|
+
local ChatTagDataUtils = require("ChatTagDataUtils")
|
|
18
|
+
local HasChatTagsConstants = require("HasChatTagsConstants")
|
|
19
|
+
local Jest = require("Jest")
|
|
20
|
+
local PermissionProviderUtils = require("PermissionProviderUtils")
|
|
21
|
+
local PermissionService = require("PermissionService")
|
|
22
|
+
local PlayerMockService = require("PlayerMockService")
|
|
23
|
+
local ServiceBag = require("ServiceBag")
|
|
24
|
+
|
|
25
|
+
local afterEach = Jest.Globals.afterEach
|
|
26
|
+
local describe = Jest.Globals.describe
|
|
27
|
+
local expect = Jest.Globals.expect
|
|
28
|
+
local it = Jest.Globals.it
|
|
29
|
+
|
|
30
|
+
local DEMO_TAG = ChatTagDataUtils.createChatTagData({
|
|
31
|
+
TagText = "[DEMO]",
|
|
32
|
+
TagPriority = 5,
|
|
33
|
+
TagColor = Color3.fromRGB(245, 163, 27),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
local activeController: any = nil
|
|
37
|
+
|
|
38
|
+
afterEach(function()
|
|
39
|
+
if activeController then
|
|
40
|
+
local controller = activeController
|
|
41
|
+
activeController = nil
|
|
42
|
+
controller.destroy()
|
|
43
|
+
end
|
|
44
|
+
end)
|
|
45
|
+
|
|
46
|
+
local remoteNameCounter = 0
|
|
47
|
+
|
|
48
|
+
-- Polls rather than observes: the tag arrives through a binder, a permission promise and an Rx chain,
|
|
49
|
+
-- and the point of these tests is the end state rather than which hop delivered it.
|
|
50
|
+
local function waitForTagCount(player: Player, expected: number, timeout: number): number
|
|
51
|
+
local deadline = os.clock() + timeout
|
|
52
|
+
|
|
53
|
+
repeat
|
|
54
|
+
local container = player:FindFirstChild(HasChatTagsConstants.TAG_CONTAINER_NAME)
|
|
55
|
+
local count = if container then #container:GetChildren() else 0
|
|
56
|
+
if count == expected then
|
|
57
|
+
return count
|
|
58
|
+
end
|
|
59
|
+
task.wait(0.1)
|
|
60
|
+
until os.clock() > deadline
|
|
61
|
+
|
|
62
|
+
local container = player:FindFirstChild(HasChatTagsConstants.TAG_CONTAINER_NAME)
|
|
63
|
+
return if container then #container:GetChildren() else 0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
-- creatorUserId, when given, makes that user the creator so the built-in developer tag actually
|
|
67
|
+
-- applies to them. Left nil, both built-in tags are switched off: they resolve through
|
|
68
|
+
-- PermissionService, which is a different question than whether a mock can be tagged at all.
|
|
69
|
+
local function setup(creatorUserId: number?)
|
|
70
|
+
local serviceBag = ServiceBag.new()
|
|
71
|
+
local container = Instance.new("Folder")
|
|
72
|
+
container.Name = "ChatProviderServiceSpecContainer"
|
|
73
|
+
container.Parent = workspace
|
|
74
|
+
|
|
75
|
+
local chatProviderService: any = serviceBag:GetService(ChatProviderService)
|
|
76
|
+
local permissionService: any = serviceBag:GetService(PermissionService)
|
|
77
|
+
local playerMockService: any = serviceBag:GetService(PlayerMockService)
|
|
78
|
+
|
|
79
|
+
serviceBag:Init()
|
|
80
|
+
|
|
81
|
+
if creatorUserId then
|
|
82
|
+
remoteNameCounter += 1
|
|
83
|
+
permissionService:SetProviderFromConfig(PermissionProviderUtils.createSingleUserConfig({
|
|
84
|
+
userId = creatorUserId,
|
|
85
|
+
remoteFunctionName = string.format("ChatProviderServiceSpecRemote%d", remoteNameCounter),
|
|
86
|
+
}))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
serviceBag:Start()
|
|
90
|
+
|
|
91
|
+
if not creatorUserId then
|
|
92
|
+
chatProviderService:SetDeveloperTag(nil)
|
|
93
|
+
chatProviderService:SetAdminTag(nil)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
local mocks: { Player } = {}
|
|
97
|
+
local bagDestroyed = false
|
|
98
|
+
|
|
99
|
+
local function destroyBag()
|
|
100
|
+
if not bagDestroyed then
|
|
101
|
+
bagDestroyed = true
|
|
102
|
+
serviceBag:Destroy()
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
local controller
|
|
107
|
+
controller = {
|
|
108
|
+
chatProviderService = chatProviderService,
|
|
109
|
+
newMock = function(userId: number?): Player
|
|
110
|
+
local mock = playerMockService:CreatePlayer(if userId then { UserId = userId } else nil)
|
|
111
|
+
mock.Parent = container
|
|
112
|
+
table.insert(mocks, mock)
|
|
113
|
+
return mock
|
|
114
|
+
end,
|
|
115
|
+
destroyBag = destroyBag,
|
|
116
|
+
destroy = function()
|
|
117
|
+
destroyBag()
|
|
118
|
+
|
|
119
|
+
for _, mock in mocks do
|
|
120
|
+
mock:Destroy()
|
|
121
|
+
end
|
|
122
|
+
table.clear(mocks)
|
|
123
|
+
|
|
124
|
+
container:Destroy()
|
|
125
|
+
end,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
activeController = controller
|
|
129
|
+
return controller
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe("ChatProviderService.PromiseAddChatTag", function()
|
|
133
|
+
it("tags a player mock", function()
|
|
134
|
+
local controller = setup()
|
|
135
|
+
local player = controller.newMock()
|
|
136
|
+
|
|
137
|
+
local tag = controller.chatProviderService:PromiseAddChatTag(player, DEMO_TAG):Wait()
|
|
138
|
+
|
|
139
|
+
expect(tag:GetAttribute(ChatTagConstants.TAG_TEXT_ATTRIBUTE)).toBe(DEMO_TAG.TagText)
|
|
140
|
+
expect(tag:IsDescendantOf(player)).toBe(true)
|
|
141
|
+
end)
|
|
142
|
+
|
|
143
|
+
it("drops the tag when the instance is destroyed", function()
|
|
144
|
+
local controller = setup()
|
|
145
|
+
local player = controller.newMock()
|
|
146
|
+
|
|
147
|
+
local tag = controller.chatProviderService:PromiseAddChatTag(player, DEMO_TAG):Wait()
|
|
148
|
+
tag:Destroy()
|
|
149
|
+
|
|
150
|
+
expect(tag:IsDescendantOf(player)).toBe(false)
|
|
151
|
+
end)
|
|
152
|
+
end)
|
|
153
|
+
|
|
154
|
+
describe("ChatProviderService.SetDeveloperTag", function()
|
|
155
|
+
local CREATOR_USER_ID = 4242
|
|
156
|
+
|
|
157
|
+
it("tags the creator", function()
|
|
158
|
+
local controller = setup(CREATOR_USER_ID)
|
|
159
|
+
local player = controller.newMock(CREATOR_USER_ID)
|
|
160
|
+
|
|
161
|
+
expect(waitForTagCount(player, 1, 10)).toBe(1)
|
|
162
|
+
end)
|
|
163
|
+
|
|
164
|
+
it("leaves everybody else alone", function()
|
|
165
|
+
local controller = setup(CREATOR_USER_ID)
|
|
166
|
+
local player = controller.newMock(CREATOR_USER_ID + 1)
|
|
167
|
+
|
|
168
|
+
expect(waitForTagCount(player, 0, 3)).toBe(0)
|
|
169
|
+
end)
|
|
170
|
+
|
|
171
|
+
--[[
|
|
172
|
+
The tag holds a subscription to every player in the place, so without a Destroy it outlived its
|
|
173
|
+
own bag: a destroyed service went on resolving permissions and reaching for binders through
|
|
174
|
+
services it no longer owned.
|
|
175
|
+
]]
|
|
176
|
+
it("stops tagging once its bag is destroyed", function()
|
|
177
|
+
local controller = setup(CREATOR_USER_ID)
|
|
178
|
+
controller.destroyBag()
|
|
179
|
+
|
|
180
|
+
local player = controller.newMock(CREATOR_USER_ID)
|
|
181
|
+
|
|
182
|
+
expect(waitForTagCount(player, 0, 3)).toBe(0)
|
|
183
|
+
end)
|
|
184
|
+
end)
|
|
185
|
+
|
|
186
|
+
describe("ChatProviderService.ClearChatTags", function()
|
|
187
|
+
it("clears a player mock's tags", function()
|
|
188
|
+
local controller = setup()
|
|
189
|
+
local player = controller.newMock()
|
|
190
|
+
|
|
191
|
+
local tag = controller.chatProviderService:PromiseAddChatTag(player, DEMO_TAG):Wait()
|
|
192
|
+
controller.chatProviderService:ClearChatTags(player)
|
|
193
|
+
|
|
194
|
+
expect(tag.Parent).toBe(nil)
|
|
195
|
+
end)
|
|
196
|
+
end)
|