Skip to content

Commit 220f7f5

Browse files
committed
Merge branch 'feature/v2.5.0' into hawk/testing
# Conflicts: # app/Http/Middleware/Api/ApiDataScopeContextSettingMiddleware.php # app/Services/Users/Keychain/Repositories/UserKeychainRepository.php # public/js/groupchat_functions.js # public/js/home_functions.js # resources/js/bootstrap.ts # resources/js/components/chat/composer/contexts/ComposerContext.svelte.ts # resources/js/oldUi/OldUiBridge.svelte.ts # resources/js/snippets/ChatSidebarButton.svelte # resources/js/utils/Bootstrapper.ts # resources/views/layouts/apps_layout.blade.php # resources/views/layouts/print_template.blade.php
2 parents 3e1d8be + 440ae01 commit 220f7f5

116 files changed

Lines changed: 2920 additions & 2747 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/Http/Controllers/HomeController.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,19 @@ public function index(
3333

3434
$userData = [
3535
'convs' => $user->conversations()->with('messages')->get(),
36-
'rooms' => $user->rooms()->with('messages')->get(),
36+
'rooms' => $user->rooms()->with('messages')->get()->map(function ($room) use ($user) {
37+
$member = $room->members()->where('user_id', $user->id)->first();
38+
39+
$raw = $room->toArray();
40+
$raw['hasUnreadMessages'] = false;
41+
foreach ($room->messages as $message) {
42+
if (!$message->isReadBy($member)) {
43+
$raw['hasUnreadMessages'] = true;
44+
break;
45+
}
46+
}
47+
return $raw;
48+
})->toArray()
3749
];
3850

3951
$activeModule = $requestModule;

app/Http/Controllers/StreamController.php

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -330,26 +330,58 @@ private function handleGroupChatRequest(array $data, Request $request): void
330330
];
331331
broadcast(new RoomMessageEvent($generationStatus));
332332

333+
$data['payload']['broadcast'] = true; // Ensure broadcast is enabled for group chat requests
334+
$data['payload']['stream'] = false; // Ensure streaming is disabled for group chat requests
335+
try {
336+
$agentRequest = $this->aiService->getAgentRequestFactory()->createFromPayload($data['payload']);
337+
} catch (\Throwable $e) {
338+
$this->logger->error('Error creating agent request from payload', ['exception' => $e]);
339+
try {
340+
$model = $this->aiService->getModels()->findOneOrFail($data['payload']['model']);
341+
RoomAiWritingEndedEvent::dispatch($room, $model);
342+
} catch (\Throwable) {
343+
}
344+
abort(400, 'Invalid payload for agent request.');
345+
}
346+
333347
// Process the request
334-
$response = $this->aiService->sendRequestToAgent($data['payload']);
348+
try {
349+
$response = $this->aiService->sendRequestToAgent($agentRequest);
350+
} catch (\Throwable $e) {
351+
$this->logger->error('Error sending request to agent', ['exception' => $e]);
352+
$response = new ChatResponse(
353+
content: 'NT: Sorry, but there was an error processing your request. Please try again later.',
354+
);
355+
}
356+
357+
if (!$response instanceof ChatResponse) {
358+
$this->logger->error('Unexpected response type from agent', ['response' => $response]);
359+
try {
360+
$model = $this->aiService->getModels()->findOneOrFail($data['payload']['model']);
361+
RoomAiWritingEndedEvent::dispatch($room, $model);
362+
} catch (\Throwable) {
363+
}
364+
abort(500, 'Unexpected response type from agent.');
365+
}
335366

336367
// Record usage
337-
$this->usageAnalyzer->submitUsageRecord(
338-
$response->usage,
339-
'group',
340-
$room->id
341-
);
368+
// $this->usageAnalyzer->submitUsageRecord(
369+
// $response->usage,
370+
// 'group',
371+
// $room->id
372+
// );
342373

343374
// @todo this was $crypto = new SymmetricCrypto();
344375
// $encryptedData = $crypto->encrypt($response->content['text'],
345376
// base64_decode($data['key']));
346377
$content = $response->content;
347-
if (array_key_exists('groundingMetadata', $response->content)) {
348-
$content = json_encode([
349-
'text' => $response->content['text'],
350-
'groundingMetadata' => $response->content['groundingMetadata'],
351-
]);
352-
}
378+
// $content = $response->content;
379+
// if (array_key_exists('groundingMetadata', $response->content)) {
380+
// $content = json_encode([
381+
// 'text' => $response->content['text'],
382+
// 'groundingMetadata' => $response->content['groundingMetadata'],
383+
// ]);
384+
// }
353385
// \Log::debug($content);
354386
$encryptedData = (new SymmetricCrypto())->encrypt(json_encode($content), base64_decode($data['key']));
355387

app/Http/Middleware/Api/ApiDataScopeContextSettingMiddleware.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Http\Middleware\Api;
44

5+
use App\Services\System\Container\ServiceLocator;
56
use Closure;
67
use Illuminate\Database\Eloquent\Model;
78
use Illuminate\Http\Request;
@@ -12,20 +13,27 @@
1213
public const string NO_SCOPE_PARAM = 'no_scope';
1314

1415
public function __construct(
15-
private Server|null $jsonApiServer
16+
// Use a service locator here, because otherwise we end up getting errors about not being able to create the Server instance
17+
// This is because the server is only bound by another middleware and therefore when the stack is resolved,
18+
// the server is not yet available in the container.
19+
private ServiceLocator $serviceLocator
1620
)
1721
{
1822
}
1923

2024
public function handle(Request $request, Closure $next)
2125
{
22-
if ($this->jsonApiServer) {
23-
$this->populateContextByRequest($request);
26+
try {
27+
$apiServer = $this->serviceLocator->get(Server::class);
28+
} catch (\Throwable) {
29+
return $next($request);
2430
}
31+
32+
$this->populateContextByRequest($request, $apiServer);
2533
return $next($request);
2634
}
2735

28-
private function populateContextByRequest(Request $request): void
36+
private function populateContextByRequest(Request $request, Server $apiServer): void
2937
{
3038
$noScope = $request->query(self::NO_SCOPE_PARAM);
3139
if (!is_array($noScope)) {
@@ -34,7 +42,7 @@ private function populateContextByRequest(Request $request): void
3442

3543
$findMostSimilar = static fn(string $input, array $options): string => collect($options)->sortBy(fn(string $item) => levenshtein($input, $item))->first();
3644

37-
$schemaContainer = $this->jsonApiServer->schemas();
45+
$schemaContainer = $apiServer->schemas();
3846
foreach ($noScope as $resourceType => $scopeKey) {
3947
if (!is_string($resourceType)) {
4048
abort(400, sprintf('Invalid resource type in "%s" query parameter. Expected string keys in format no_scope[resource-type]=scopeKey', self::NO_SCOPE_PARAM));

app/Models/Invitation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Invitation extends Model
2828
*/
2929
public function room(): BelongsTo
3030
{
31-
return $this->belongsTo(Room::class);
31+
return $this->belongsTo(Room::class)->withoutGlobalScopes();
3232
}
3333

3434
/**

app/Models/Room.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use App\Services\Storage\Values\StoredFileIdentifier;
1515
use App\Services\System\Database\Eloquent\ContextualScopes\HasContextualScopesTrait;
1616
use App\Services\System\Database\Eloquent\ContextualScopes\ScopeRegistrar;
17+
use App\Services\Users\Keychain\Repositories\UserKeychainRepository;
1718
use Exception;
1819
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
1920
use Illuminate\Database\Eloquent\Model;
@@ -166,12 +167,25 @@ public function removeMember(int $userId): bool
166167
// Attempt to delete the member from the room based on user ID
167168
$member = $this->members()->where('user_id', $userId)->firstOrFail();
168169
$member->revokeMembership();
170+
// Please don't do it like this. Don't torture your model with injecting
171+
// random repositories. There should be a service, but the service itself is cumbersome.
172+
// Will be removed in a future refactor.
173+
/** @var UserKeychainRepository $repo */
174+
$repo = app(UserKeychainRepository::class);
175+
$repo->removeRoomKey($member->user, $member->room);
169176

170177
$this->triggerOrDeferMemberEvent(fn() => MemberRemovedFromRoomEvent::dispatch($member));
171178

172179
//Check if All the members have left the room.
173180
if ($this->members()->count() === 1) {
174181
$this->deleteRoom();
182+
} else if ($member->hasRole(Member::ROLE_ADMIN)) {
183+
// If the removed member was an admin, check if there are any admins left. If not, handle this as a "delete" action
184+
// @todo in the new api it should not be possible to leave a room without at least one admin!
185+
$adminsCount = $this->members()->where('role', Member::ROLE_ADMIN)->count();
186+
if ($adminsCount === 0) {
187+
$this->delete();
188+
}
175189
}
176190
return true;
177191
} catch (\Throwable $e) {

app/Models/Scopes/KnownUsersAccessScope.php

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,18 @@ class KnownUsersAccessScope implements Scope
2020
*/
2121
public function apply(EloquentBuilder $builder, Model $model): void
2222
{
23-
try {
24-
$this->runIfUserPresent(
25-
function (User $user) use ($builder) {
26-
$builder
27-
// Allow all users to see records of groupchats they are a member of
28-
->whereHas('members', function (EloquentBuilder $query) use ($user) {
29-
$query->where('user_id', $user->id);
30-
})
31-
// Allow users to see their own record, even if they are not a member of any team
32-
->orWhere('users.id', $user->id)
33-
// Allow all users to see the HAWKI user record
34-
->orWhere('users.id', 1);
35-
},
36-
callbackNoUserInCli: true
37-
);
38-
} catch (\Throwable $e) {
39-
dbge($e);
40-
}
23+
$this->runIfUserPresent(
24+
function (User $user) use ($builder) {
25+
$builder
26+
// Allow all users to see records of groupchats they are a member of
27+
->whereHas('members')
28+
// Allow users to see their own record, even if they are not a member of any team
29+
->orWhere('users.id', $user->id)
30+
// Allow all users to see the HAWKI user record
31+
32+
->orWhere('users.id', 1);
33+
},
34+
callbackNoUserInCli: true
35+
);
4136
}
4237
}

app/Services/Ai/Agent/Chat/ChatRequestFactory.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,10 @@ private function addAttachmentsToMessage(
187187
{
188188
foreach ($attachmentUuids as $uuid) {
189189
$file = $this->fileStorageService->retrieve(StoredFileIdentifier::fromCategoryAndUuid($category, $uuid));
190-
$extracts = ($file->getExtracts()?->count() ?? 0) > 0 ? $file->getExtracts() : [$file];
190+
if (!$file) {
191+
logFile($category, $uuid, 'File not found for attachment.', $message, $attachmentUuids);
192+
}
193+
$extracts = ($file?->getExtracts()?->count() ?? 0) > 0 ? $file->getExtracts() : [$file];
191194
foreach ($extracts as $extract) {
192195
if ($extract->getFileType() === FileType::IMAGE) {
193196
$message->addContent(new ImageContent(

app/Services/Chat/Room/Traits/RoomFunctions.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,6 @@ public function delete($slug)
119119
{
120120
$room = Room::where('slug', $slug)->firstOrFail();
121121

122-
if (!$room->isMember(Auth::id())) {
123-
throw new AuthorizationException();
124-
}
125-
126122
try {
127123
$room->deleteRoom();
128124
return true;

app/Services/Chat/Room/Traits/RoomMembers.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,6 @@ public function removeMember(Member $member, Room $room): bool
6969
{
7070
// Remove the member from the room
7171
$room->removeMember($member->user_id);
72-
73-
//Check if All the members have left the room.
74-
if ($room->members()->count() === 1) {
75-
$this->delete($room->slug);
76-
}
77-
7872
return true;
7973
}
8074

@@ -83,6 +77,7 @@ public function searchUser(string $query): array
8377
{
8478
// Search in the database for users matching the query and is not removed
8579
$users = User::where('isRemoved', false)
80+
->withoutGlobalScopes()
8681
->where(function ($queryBuilder) use ($query) {
8782
$queryBuilder->where('name', 'like', "%{$query}%")
8883
->orWhere('username', 'like', "%{$query}%")

app/Services/Users/Keychain/Repositories/UserKeychainRepository.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace App\Services\Users\Keychain\Repositories;
66

77

8+
use App\Models\Room;
89
use App\Models\User;
910
use App\Models\UserKeychainValue;
1011
use App\Services\System\Database\Eloquent\Repositories\AbstractRepositoryWithContextualScopes;
@@ -113,6 +114,26 @@ public function findFirstPublicKeyOfUser(User $user): ?UserKeychainValue
113114
->first();
114115
}
115116

117+
/**
118+
* Removes all room keys for a user for a given room.
119+
* This should be called when a user is removed from a room to ensure they no longer have access to the room's key.
120+
* @param User $user
121+
* @param Room $room
122+
* @return void
123+
*/
124+
public function removeRoomKey(User $user, Room $room): void
125+
{
126+
$this->getQueryWithoutContextualScopes('access')
127+
->where('user_id', $user->id)
128+
->whereIn('type', [
129+
UserKeychainValueType::ROOM->value,
130+
UserKeychainValueType::ROOM_AI->value,
131+
UserKeychainValueType::ROOM_AI_LEGACY->value
132+
])
133+
->where('key', $room->slug)
134+
->each(fn(UserKeychainValue $val) => $val->delete());
135+
}
136+
116137
/**
117138
* A basic housekeeping method that removes all room keys for rooms the user is no longer a member of.
118139
* This should be run once in a while to keep the database clean.

0 commit comments

Comments
 (0)