Skip to content

Commit 630c610

Browse files
authored
Merge pull request #86 from prog-time/issues-85
Issues 85
2 parents c4ac47e + c13845e commit 630c610

57 files changed

Lines changed: 919 additions & 568 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.

.cursor/rules/development-workflow.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ description: Development workflow and best practices
3131
- Access Swagger UI through [resources/views/swagger-ui.blade.php](mdc:resources/views/swagger-ui.blade.php)
3232

3333
## Logging and Monitoring
34-
- Use [app/Logging/LokiLogger.php](mdc:app/Logging/LokiLogger.php) for centralized logging
34+
- Use `Log::channel('loki')` for centralized logging via [app/Logging/LokiHandler.php](mdc:app/Logging/LokiHandler.php)
3535
- Monitor application through [config/logging.php](mdc:config/logging.php)
3636
- Use [config/sentry.php](mdc:config/sentry.php) for error tracking

app/Actions/Ai/AiAcceptMessage.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use App\DTOs\TelegramUpdateDto;
66
use App\DTOs\TGTextMessageDto;
77
use App\Jobs\SendMessage\SendTelegramMessageJob;
8-
use App\Logging\LokiLogger;
8+
use Illuminate\Support\Facades\Log;
99
use App\Models\BotUser;
1010
use phpDocumentor\Reflection\Exception;
1111

@@ -64,7 +64,7 @@ public function execute(TelegramUpdateDto $update): void
6464
'outgoing',
6565
);
6666
} catch (\Throwable $e) {
67-
(new LokiLogger())->log('ai_error', $e->getMessage());
67+
Log::channel('loki')->error($e->getMessage(), ['source' => 'ai_error']);
6868
}
6969
}
7070
}

app/Actions/Ai/AiCancelMessage.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use App\DTOs\TelegramUpdateDto;
66
use App\DTOs\TGTextMessageDto;
77
use App\Jobs\SendMessage\SendTelegramMessageJob;
8-
use App\Logging\LokiLogger;
8+
use Illuminate\Support\Facades\Log;
99
use App\Models\AiMessage;
1010
use App\Models\BotUser;
1111
use phpDocumentor\Reflection\Exception;
@@ -52,7 +52,7 @@ public function execute(TelegramUpdateDto $update): void
5252

5353
AiMessage::where('message_id', $messageData->message_id)->delete();
5454
} catch (\Throwable $e) {
55-
(new LokiLogger())->log('ai_error', $e->getMessage());
55+
Log::channel('loki')->error($e->getMessage(), ['source' => 'ai_error']);
5656
}
5757
}
5858
}

app/Actions/Ai/EditAiMessage.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use App\DTOs\TGTextMessageDto;
77
use App\Helpers\AiHelper;
88
use App\Jobs\SendMessage\SendTelegramMessageJob;
9-
use App\Logging\LokiLogger;
9+
use Illuminate\Support\Facades\Log;
1010
use App\Models\AiMessage;
1111
use App\Models\BotUser;
1212
use phpDocumentor\Reflection\Exception;
@@ -93,7 +93,7 @@ public function execute(TelegramUpdateDto $update): void
9393
'incoming',
9494
);
9595
} catch (\Throwable $e) {
96-
(new LokiLogger())->log('ai_error', $e->getMessage());
96+
Log::channel('loki')->error($e->getMessage(), ['source' => 'ai_error']);
9797
}
9898
}
9999
}

app/Actions/External/DeleteMessage.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
namespace App\Actions\External;
44

55
use App\DTOs\External\ExternalMessageDto;
6-
use App\Logging\LokiLogger;
76
use App\Models\BotUser;
7+
use Illuminate\Support\Facades\Log;
88
use App\Models\ExternalUser;
99
use App\Models\Message;
1010
use App\TelegramBot\TelegramMethods;
@@ -59,7 +59,7 @@ public static function execute(ExternalMessageDto $updateData): void
5959

6060
Message::where($whereParamsMessage)->delete();
6161
} catch (Exception $e) {
62-
(new LokiLogger())->logException($e);
62+
Log::channel('loki')->log($e->getCode() === 1 ? 'warning' : 'error', $e->getMessage(), ['file' => $e->getFile(), 'line' => $e->getLine()]);
6363
}
6464
}
6565
}

app/Actions/Telegram/ConversionMessageText.php

Lines changed: 131 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,45 +39,149 @@ public static function hasFormattingEntities(array $entities): bool
3939

4040
/**
4141
* Convert message to MarkdownV2 format.
42+
* Supports nested and overlapping entities via event-based processing.
4243
*
4344
* @param string $text
4445
* @param array $entities
4546
* @return string
4647
*/
4748
public static function conversionMarkdownFormat(string $text, array $entities): string
4849
{
49-
usort($entities, fn($a, $b) => $b['offset'] <=> $a['offset']);
50+
$entities = array_values(array_filter(
51+
$entities,
52+
fn ($e) => in_array($e['type'], self::FORMATTING_ENTITY_TYPES, true)
53+
));
5054

51-
foreach ($entities as $entity) {
52-
$offset = $entity['offset'];
53-
$length = $entity['length'];
54-
$type = $entity['type'];
55-
$part = mb_substr($text, $offset, $length);
56-
57-
switch ($type) {
58-
case 'bold':
59-
$wrapped = "**{$part}**";
60-
break;
61-
case 'italic':
62-
$wrapped = "_{$part}_";
63-
break;
64-
case 'code':
65-
$wrapped = "`{$part}`";
66-
break;
67-
case 'pre':
68-
$wrapped = "```\n{$part}\n```";
69-
break;
70-
case 'text_link':
71-
$wrapped = "[{$part}]({$entity['url']})";
72-
break;
73-
default:
74-
$wrapped = $part;
55+
if (empty($entities)) {
56+
return self::escapeMarkdownV2($text);
57+
}
58+
59+
$events = [];
60+
foreach ($entities as $idx => $entity) {
61+
$events[] = [$entity['offset'], 'open', $idx];
62+
$events[] = [$entity['offset'] + $entity['length'], 'close', $idx];
63+
}
64+
65+
usort($events, function ($a, $b) use ($entities) {
66+
if ($a[0] !== $b[0]) {
67+
return $a[0] - $b[0];
68+
}
69+
// At same position: closes before opens
70+
if ($a[1] !== $b[1]) {
71+
return $a[1] === 'close' ? -1 : 1;
72+
}
73+
// Same type at same position
74+
if ($a[1] === 'open') {
75+
// Longer (outer) entities open first
76+
return $entities[$b[2]]['length'] - $entities[$a[2]]['length'];
7577
}
78+
// Shorter (inner) entities close first
79+
return $entities[$a[2]]['length'] - $entities[$b[2]]['length'];
80+
});
7681

77-
$text = mb_substr($text, 0, $offset) . $wrapped . mb_substr($text, $offset + $length);
82+
$result = '';
83+
$lastPos = 0;
84+
$openEntities = [];
85+
86+
foreach ($events as [$pos, $type, $entityIdx]) {
87+
$entity = $entities[$entityIdx];
88+
89+
if ($pos > $lastPos) {
90+
$segment = mb_substr($text, $lastPos, $pos - $lastPos);
91+
$insideCode = !empty(array_filter($openEntities, fn ($e) => in_array($e['type'], ['code', 'pre'])));
92+
$result .= $insideCode ? self::escapeCode($segment) : self::escapeMarkdownV2($segment);
93+
$lastPos = $pos;
94+
}
95+
96+
if ($type === 'open') {
97+
$result .= self::getOpenMarker($entity);
98+
$openEntities[$entityIdx] = $entity;
99+
} else {
100+
$result .= self::getCloseMarker($entity);
101+
unset($openEntities[$entityIdx]);
102+
}
78103
}
79104

80-
return $text;
105+
if ($lastPos < mb_strlen($text)) {
106+
$result .= self::escapeMarkdownV2(mb_substr($text, $lastPos));
107+
}
108+
109+
return $result;
110+
}
111+
112+
/**
113+
* Get the opening MarkdownV2 marker for an entity.
114+
*
115+
* @param array $entity
116+
* @return string
117+
*/
118+
private static function getOpenMarker(array $entity): string
119+
{
120+
return match ($entity['type']) {
121+
'bold' => '*',
122+
'italic' => '_',
123+
'underline' => '__',
124+
'strikethrough' => '~',
125+
'spoiler' => '||',
126+
'code' => '`',
127+
'pre' => '```' . ($entity['language'] ?? '') . "\n",
128+
'text_link' => '[',
129+
'blockquote' => '>',
130+
default => '',
131+
};
132+
}
133+
134+
/**
135+
* Get the closing MarkdownV2 marker for an entity.
136+
*
137+
* @param array $entity
138+
* @return string
139+
*/
140+
private static function getCloseMarker(array $entity): string
141+
{
142+
return match ($entity['type']) {
143+
'bold' => '*',
144+
'italic' => '_',
145+
'underline' => '__',
146+
'strikethrough' => '~',
147+
'spoiler' => '||',
148+
'code' => '`',
149+
'pre' => "\n```",
150+
'text_link' => '](' . self::escapeUrl($entity['url'] ?? '') . ')',
151+
default => '',
152+
};
153+
}
154+
155+
/**
156+
* Escape special characters for MarkdownV2.
157+
*
158+
* @param string $text
159+
* @return string
160+
*/
161+
private static function escapeMarkdownV2(string $text): string
162+
{
163+
return preg_replace('/([_*\[\]()~`>#+\-=|{}.!\\\\])/', '\\\\$1', $text) ?? $text;
81164
}
82165

166+
/**
167+
* Escape characters inside code/pre blocks.
168+
*
169+
* @param string $text
170+
* @return string
171+
*/
172+
private static function escapeCode(string $text): string
173+
{
174+
return str_replace(['\\', '`'], ['\\\\', '\\`'], $text);
175+
}
176+
177+
/**
178+
* Escape characters inside URL part of text_link.
179+
*
180+
* @param string $url
181+
* @return string
182+
*/
183+
private static function escapeUrl(string $url): string
184+
{
185+
return str_replace(['\\', ')'], ['\\\\', '\\)'], $url);
186+
}
83187
}

app/Actions/Telegram/SendAiAnswerMessage.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use App\DTOs\TelegramUpdateDto;
66
use App\Jobs\SendMessage\SendAiResponseMessageJob;
7-
use App\Logging\LokiLogger;
7+
use Illuminate\Support\Facades\Log;
88
use App\Models\BotUser;
99
use Exception;
1010

@@ -34,7 +34,7 @@ public function execute(TelegramUpdateDto $update): void
3434
$update,
3535
);
3636
} catch (\Throwable $e) {
37-
(new LokiLogger())->log('ai_error', $e->getMessage());
37+
Log::channel('loki')->error($e->getMessage(), ['source' => 'ai_error']);
3838
}
3939
}
4040
}

app/DTOs/External/ExternalMessageDto.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class ExternalMessageDto extends Data
2222
* @param string $external_id
2323
* @param ?string $text
2424
* @param string|int|null $message_id
25+
* @param ?string $file_type
26+
* @param ?string $file_name
2527
*/
2628
public function __construct(
2729
public string $source,
@@ -30,6 +32,8 @@ public function __construct(
3032
public ?string $text,
3133
public ?UploadedFile $uploaded_file,
3234
public ?string $uploaded_file_path,
35+
public ?string $file_type,
36+
public ?string $file_name,
3337
) {
3438
}
3539

@@ -43,7 +47,12 @@ public static function fromRequest(Request $request): ?self
4347
try {
4448
$data = $request->all();
4549

50+
$fileType = null;
51+
$fileName = null;
4652
if (!empty($data['uploaded_file'])) {
53+
$mime = $data['uploaded_file']->getMimeType();
54+
$fileType = $mime && str_starts_with($mime, 'image/') ? 'photo' : 'document';
55+
$fileName = $data['uploaded_file']->getClientOriginalName();
4756
$uploadedFilePath = $data['uploaded_file']->store('uploads', 'public');
4857
}
4958

@@ -54,6 +63,8 @@ public static function fromRequest(Request $request): ?self
5463
text: $data['text'] ?? null,
5564
uploaded_file: $data['uploaded_file'] ?? null,
5665
uploaded_file_path: $uploadedFilePath ?? null,
66+
file_type: $fileType,
67+
file_name: $fileName,
5768
);
5869
} catch (\Throwable $e) {
5970
return null;

app/DTOs/External/ExternalMessageResponseDto.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class ExternalMessageResponseDto extends Data
1616
* @param string|null $file_id
1717
* @param string|null $file_url
1818
* @param string|null $file_type
19+
* @param string|null $file_name
1920
* @param array|null $buttons Buttons as ButtonDto array
2021
*/
2122
public function __construct(
@@ -28,6 +29,7 @@ public function __construct(
2829
public ?string $file_id,
2930
public ?string $file_url,
3031
public ?string $file_type,
32+
public ?string $file_name = null,
3133
public ?array $buttons = null,
3234
) {
3335
}
@@ -57,6 +59,7 @@ public static function fromArray(array $data): ?self
5759
file_id: $data['file_id'] ?? null,
5860
file_url: $data['file_url'] ?? null,
5961
file_type: $data['file_type'] ?? null,
62+
file_name: $data['file_name'] ?? null,
6063
buttons: $data['buttons'] ?? null,
6164
);
6265
} catch (\Throwable $e) {

app/Http/Controllers/FilesController.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace App\Http\Controllers;
44

5-
use App\Logging\LokiLogger;
65
use App\Services\File\FileService;
76
use Illuminate\Http\Response;
7+
use Illuminate\Support\Facades\Log;
88
use Symfony\Component\HttpFoundation\StreamedResponse;
99

1010
/**
@@ -33,7 +33,7 @@ public function getFileStream(string $fileId): StreamedResponse
3333
try {
3434
return $this->fileService->streamFile($fileId);
3535
} catch (\Throwable $e) {
36-
(new LokiLogger())->log('tg_request', $e->getMessage());
36+
Log::channel('loki')->info($e->getMessage(), ['source' => 'tg_request']);
3737
die();
3838
}
3939
}
@@ -50,7 +50,7 @@ public function getFileDownload(string $fileId): Response
5050
try {
5151
return $this->fileService->downloadFile($fileId);
5252
} catch (\Throwable $e) {
53-
(new LokiLogger())->log('tg_request', $e->getMessage());
53+
Log::channel('loki')->info($e->getMessage(), ['source' => 'tg_request']);
5454
die();
5555
}
5656
}

0 commit comments

Comments
 (0)