feat: / команды

This commit is contained in:
Jganenokk
2026-06-21 22:35:58 +07:00
parent 78cc4fdae6
commit fd308327f5
11 changed files with 592 additions and 77 deletions
+5 -2
View File
@@ -649,16 +649,19 @@ class MessagesModule {
int chatId, int chatId,
String messageId, { String messageId, {
required String text, required String text,
List<Map<String, dynamic>> elements = const [],
bool sendAttachments = false,
}) async { }) async {
final id = int.tryParse(messageId); final id = int.tryParse(messageId);
if (id == null) return false; if (id == null) return false;
final payload = { final payload = <String, dynamic>{
'messageId': id, 'messageId': id,
'chatId': chatId, 'chatId': chatId,
'elements': <dynamic>[], 'elements': elements,
'text': text, 'text': text,
}; };
if (sendAttachments) payload['attachments'] = const <dynamic>[];
final response = await _api.sendRequest(Opcode.msgEdit, payload); final response = await _api.sendRequest(Opcode.msgEdit, payload);
return response.isOk; return response.isOk;
+20
View File
@@ -0,0 +1,20 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppCommands {
static const prefKey = 'dev_commands';
static const bool defaultValue = false;
static final ValueNotifier<bool> current = ValueNotifier(defaultValue);
static Future<bool> load() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(prefKey) ?? defaultValue;
}
static Future<void> save(bool value) async {
current.value = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(prefKey, value);
}
}
@@ -0,0 +1,21 @@
import 'crush_command.dart';
import 'info_command.dart';
import 'slash_command.dart';
const List<SlashCommand> kSlashCommands = [
SlashCommand('/test', '12345 test отображение'),
SlashCommand('/info', 'сводка данных о человеке', run: runInfo),
SlashCommand(
'/crush',
'Тест устойчивости веб клиента макса',
run: runCrush,
hidden: true,
),
];
SlashCommand? findSlashCommand(String text) {
for (final c in kSlashCommands) {
if (c.name == text) return c;
}
return null;
}
+118
View File
@@ -0,0 +1,118 @@
import 'dart:async';
import 'slash_command.dart';
const int _cycles = 40;
const Duration _delay = Duration(milliseconds: 400);
const String _text =
'burmaldaburmaldaburmaldaburmaldaburmaldaburmaldaburmaldaburmalda';
const List<Map<String, dynamic>> _elements = [
{'type': 'STRONG', 'from': 0, 'length': 1},
{'type': 'EMPHASIZED', 'from': 1, 'length': 1},
{'type': 'UNDERLINE', 'from': 2, 'length': 1},
{'type': 'STRIKETHROUGH', 'from': 3, 'length': 1},
{'type': 'STRONG', 'from': 4, 'length': 2},
{'type': 'STRONG', 'from': 6, 'length': 1},
{'type': 'EMPHASIZED', 'from': 6, 'length': 1},
{'type': 'STRONG', 'from': 7, 'length': 1},
{'type': 'EMPHASIZED', 'from': 7, 'length': 1},
{'type': 'STRIKETHROUGH', 'from': 7, 'length': 1},
{'type': 'STRONG', 'from': 8, 'length': 1},
{'type': 'EMPHASIZED', 'from': 8, 'length': 1},
{'type': 'UNDERLINE', 'from': 8, 'length': 1},
{'type': 'STRIKETHROUGH', 'from': 8, 'length': 1},
{'type': 'STRONG', 'from': 9, 'length': 1},
{'type': 'EMPHASIZED', 'from': 9, 'length': 1},
{'type': 'STRIKETHROUGH', 'from': 9, 'length': 1},
{'type': 'STRIKETHROUGH', 'from': 13, 'length': 3},
{'type': 'STRONG', 'from': 16, 'length': 2},
{'type': 'STRIKETHROUGH', 'from': 16, 'length': 2},
{'type': 'STRONG', 'from': 18, 'length': 5},
{'type': 'EMPHASIZED', 'from': 18, 'length': 5},
{'type': 'STRIKETHROUGH', 'from': 18, 'length': 5},
{'type': 'STRONG', 'from': 23, 'length': 2},
{'type': 'EMPHASIZED', 'from': 23, 'length': 2},
{'type': 'UNDERLINE', 'from': 23, 'length': 2},
{'type': 'STRIKETHROUGH', 'from': 23, 'length': 2},
{'type': 'STRONG', 'from': 25, 'length': 1},
{'type': 'EMPHASIZED', 'from': 25, 'length': 1},
{'type': 'UNDERLINE', 'from': 25, 'length': 1},
{'type': 'STRONG', 'from': 26, 'length': 2},
{'type': 'UNDERLINE', 'from': 26, 'length': 2},
{'type': 'STRONG', 'from': 28, 'length': 3},
{'type': 'EMPHASIZED', 'from': 28, 'length': 3},
{'type': 'UNDERLINE', 'from': 28, 'length': 3},
{'type': 'EMPHASIZED', 'from': 31, 'length': 2},
{'type': 'EMPHASIZED', 'from': 33, 'length': 2},
{'type': 'UNDERLINE', 'from': 33, 'length': 2},
{'type': 'STRONG', 'from': 35, 'length': 6},
{'type': 'EMPHASIZED', 'from': 35, 'length': 6},
{'type': 'UNDERLINE', 'from': 35, 'length': 6},
{'type': 'STRONG', 'from': 41, 'length': 2},
{'type': 'UNDERLINE', 'from': 41, 'length': 2},
{'type': 'STRONG', 'from': 43, 'length': 8},
{'type': 'UNDERLINE', 'from': 43, 'length': 8},
{'type': 'STRIKETHROUGH', 'from': 43, 'length': 8},
{'type': 'UNDERLINE', 'from': 51, 'length': 7},
{'type': 'STRIKETHROUGH', 'from': 51, 'length': 7},
{'type': 'STRIKETHROUGH', 'from': 58, 'length': 5},
{'type': 'MONOSPACED', 'from': 0, 'length': 4},
{'type': 'MONOSPACED', 'from': 9, 'length': 4},
{'type': 'MONOSPACED', 'from': 16, 'length': 7},
{'type': 'MONOSPACED', 'from': 25, 'length': 6},
{'type': 'MONOSPACED', 'from': 33, 'length': 8},
{'type': 'MONOSPACED', 'from': 43, 'length': 8},
{'type': 'MONOSPACED', 'from': 51, 'length': 7},
{'type': 'MONOSPACED', 'from': 58, 'length': 5},
{
'type': 'LINK',
'from': 0,
'length': 8,
'attributes': {'url': 'https://vk.com'},
},
{
'type': 'LINK',
'from': 24,
'length': 16,
'attributes': {'url': 'https://max.ru'},
},
{
'type': 'LINK',
'from': 48,
'length': 16,
'attributes': {'url': 'https://web.max.ru'},
},
];
Future<void> runCrush(CommandContext ctx) async {
if (!ctx.isOnline()) {
ctx.notify('Нет соединения');
return;
}
ctx.notify('Crush: запуск ($_cycles циклов)');
for (var i = 0; i < _cycles; i++) {
if (!ctx.isActive()) return;
unawaited(_cycle(ctx));
await Future.delayed(_delay);
}
if (ctx.isActive()) ctx.notify('Crush: завершено');
}
Future<void> _cycle(CommandContext ctx) async {
try {
final id = await ctx.messages.sendMessage(ctx.accountId, ctx.chatId, _text);
if (id.isEmpty) return;
await Future.wait([
ctx.messages.editMessage(
ctx.chatId,
id,
text: _text,
elements: _elements,
sendAttachments: true,
),
ctx.messages.deleteMessages(ctx.chatId, [id], forEveryone: true),
]);
} catch (_) {}
}
+56
View File
@@ -0,0 +1,56 @@
import '../../core/cache/info_cache.dart';
import '../../core/utils/format.dart';
import 'slash_command.dart';
Future<void> runInfo(CommandContext ctx) async {
final targetId = ctx.otherUserId;
if (targetId == null) {
ctx.notify('Команда доступна только в диалоге');
return;
}
if (!ctx.isOnline()) {
ctx.notify('Нет соединения');
return;
}
final messageId = await ctx.postMessage('сбор данных...');
if (messageId.isEmpty) return;
final contact = await ContactInfoFetch.get(targetId, forceRefresh: true);
if (!ctx.isActive()) return;
await ctx.updateMessage(
messageId,
contact == null ? 'Данные не получены' : _summary(contact, targetId),
);
}
String _summary(Map<String, dynamic> c, int targetId) {
final flags = (c['options'] as List?)?.whereType<String>().toList() ?? const [];
final region = (c['country'] as String?)?.trim();
return 'Никнейм: ${_nick(c)}\n'
'Дата регистрации: ${_date(c['registrationTime'])}\n'
'Дата последнего изменения профиля: ${_date(c['updateTime'])}\n'
'id: ${c['id'] ?? targetId}\n'
'Регион: ${region == null || region.isEmpty ? '' : region}\n'
'Флаги: ${flags.isEmpty ? '' : flags.join(', ')}\n'
'ip: not fetched';
}
String _nick(Map<String, dynamic> c) {
final names = c['names'];
if (names is List && names.isNotEmpty && names.first is Map) {
final n = names.first as Map;
final name = (n['name'] as String?) ??
'${n['firstName'] ?? ''} ${n['lastName'] ?? ''}'.trim();
if (name.isNotEmpty) return name;
}
return '';
}
String _date(dynamic ms) {
if (ms is! int || ms <= 0) return '';
return formatDateTimeWords(DateTime.fromMillisecondsSinceEpoch(ms));
}
+41
View File
@@ -0,0 +1,41 @@
import '../../backend/modules/messages.dart';
class CommandContext {
final int accountId;
final int chatId;
final int? otherUserId;
final MessagesModule messages;
final bool Function() isOnline;
final bool Function() isActive;
final void Function(String message) notify;
final Future<String> Function(String text) postMessage;
final Future<void> Function(String id, String text) updateMessage;
const CommandContext({
required this.accountId,
required this.chatId,
required this.otherUserId,
required this.messages,
required this.isOnline,
required this.isActive,
required this.notify,
required this.postMessage,
required this.updateMessage,
});
}
typedef CommandRunner = Future<void> Function(CommandContext ctx);
class SlashCommand {
final String name;
final String description;
final CommandRunner? run;
final bool hidden;
const SlashCommand(
this.name,
this.description, {
this.run,
this.hidden = false,
});
}
+185 -3
View File
@@ -34,10 +34,14 @@ import '../../../core/config/app_cache_extent.dart';
import '../../../core/config/app_message_actions_style.dart'; import '../../../core/config/app_message_actions_style.dart';
import '../../../core/config/app_swipe_back_desktop.dart'; import '../../../core/config/app_swipe_back_desktop.dart';
import '../../../core/config/app_pranks.dart'; import '../../../core/config/app_pranks.dart';
import '../../../core/config/app_commands.dart';
import '../../../core/config/app_visual_style.dart'; import '../../../core/config/app_visual_style.dart';
import '../../../core/config/komet_settings.dart'; import '../../../core/config/komet_settings.dart';
import '../../../models/attachment.dart'; import '../../../models/attachment.dart';
import '../../commands/command_registry.dart';
import '../../commands/slash_command.dart';
import '../../widgets/glossy_pill.dart'; import '../../widgets/glossy_pill.dart';
import '../../widgets/command_suggestions_panel.dart';
import '../../widgets/online_dot.dart'; import '../../widgets/online_dot.dart';
import '../../widgets/connection_status.dart'; import '../../widgets/connection_status.dart';
import '../../widgets/message_bubble.dart'; import '../../widgets/message_bubble.dart';
@@ -158,6 +162,8 @@ class _ChatScreenState extends State<ChatScreen>
final ValueNotifier<int> _otherReadTime = ValueNotifier(0); final ValueNotifier<int> _otherReadTime = ValueNotifier(0);
int _tempIdCounter = 0; int _tempIdCounter = 0;
late final AnimationController _attachAnim; late final AnimationController _attachAnim;
late final AnimationController _commandAnim;
bool _commandPanelVisible = false;
String _nextTempId() => String _nextTempId() =>
'temp_${++_tempIdCounter}_${DateTime.now().microsecondsSinceEpoch}'; 'temp_${++_tempIdCounter}_${DateTime.now().microsecondsSinceEpoch}';
@@ -201,6 +207,11 @@ class _ChatScreenState extends State<ChatScreen>
reverseDuration: const Duration(milliseconds: 240), reverseDuration: const Duration(milliseconds: 240),
); );
_showAttachmentPanel.addListener(_onAttachPanelToggle); _showAttachmentPanel.addListener(_onAttachPanelToggle);
_commandAnim = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
AppCommands.current.addListener(_updateCommandPanel);
_pushSub = api.pushStream _pushSub = api.pushStream
.where( .where(
(p) => (p) =>
@@ -581,6 +592,8 @@ class _ChatScreenState extends State<ChatScreen>
_finishPrankReveal(); _finishPrankReveal();
_uploadStatus.dispose(); _uploadStatus.dispose();
_attachAnim.dispose(); _attachAnim.dispose();
AppCommands.current.removeListener(_updateCommandPanel);
_commandAnim.dispose();
_messageController.dispose(); _messageController.dispose();
_messageFocusNode.dispose(); _messageFocusNode.dispose();
_scrollController.dispose(); _scrollController.dispose();
@@ -594,6 +607,28 @@ class _ChatScreenState extends State<ChatScreen>
if (newHasText != _hasText.value) { if (newHasText != _hasText.value) {
_hasText.value = newHasText; _hasText.value = newHasText;
} }
_updateCommandPanel();
}
void _updateCommandPanel() {
final show =
AppCommands.current.value && _messageController.text.startsWith('/');
if (show == _commandPanelVisible) return;
_commandPanelVisible = show;
if (show) {
_commandAnim.forward();
} else {
_commandAnim.reverse();
}
}
void _onCommandSelected(SlashCommand c) {
final text = '${c.name} ';
_messageController.value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
_messageFocusNode.requestFocus();
} }
void _restoreDraft() { void _restoreDraft() {
@@ -620,6 +655,26 @@ class _ChatScreenState extends State<ChatScreen>
} }
} }
Widget _buildCommandPanel() {
return AnimatedBuilder(
animation: _commandAnim,
builder: (context, _) {
final t = _commandAnim.value;
if (t == 0) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
child: IgnorePointer(
ignoring: t < 1,
child: Opacity(
opacity: t,
child: CommandSuggestionsPanel(onSelected: _onCommandSelected),
),
),
);
},
);
}
int _computeOtherReadTime() { int _computeOtherReadTime() {
final c = chat; final c = chat;
if (c == null) return 0; if (c == null) return 0;
@@ -1442,6 +1497,16 @@ class _ChatScreenState extends State<ChatScreen>
final text = _messageController.text.trim(); final text = _messageController.text.trim();
if (text.isEmpty || _myId == 0) return; if (text.isEmpty || _myId == 0) return;
if (AppCommands.current.value) {
final command = findSlashCommand(text);
if (command?.run != null) {
_messageController.clear();
_hasText.value = false;
unawaited(command!.run!(_commandContext()));
return;
}
}
final tempId = _nextTempId(); final tempId = _nextTempId();
final now = DateTime.now().millisecondsSinceEpoch; final now = DateTime.now().millisecondsSinceEpoch;
final online = api.state == SessionState.online; final online = api.state == SessionState.online;
@@ -1550,6 +1615,111 @@ class _ChatScreenState extends State<ChatScreen>
} }
} }
int? _resolveOtherId() {
if (widget.chatType != 'DIALOG' || _myId == 0) return null;
final id = widget.chatId ^ _myId;
return id > 0 ? id : null;
}
CachedMessage _replaceMessage(
int index, {
String? id,
String? text,
String? status,
}) {
final old = _messages[index];
final updated = CachedMessage(
id: id ?? old.id,
accountId: old.accountId,
chatId: old.chatId,
senderId: old.senderId,
text: text ?? old.text,
time: old.time,
status: status ?? old.status,
payload: old.payload,
attachments: old.attachments,
isControl: old.isControl,
);
_messages[index] = updated;
_bumpMessages();
return updated;
}
Future<String> _postCommandMessage(String text) async {
if (!mounted || _myId == 0) return '';
final tempId = _nextTempId();
final now = DateTime.now().millisecondsSinceEpoch;
final online = api.state == SessionState.online;
final composed = CachedMessage(
id: tempId,
accountId: _myId,
chatId: widget.chatId,
senderId: _myId,
text: text,
time: now,
status: online ? 'sending' : 'pending',
);
_messages.add(composed);
_bumpMessages();
_scrollToBottom();
unawaited(_persistOutgoing(composed));
unawaited(ChatsModule.applyOutgoing(
_myId,
widget.chatId,
messageId: tempId,
time: now,
text: text,
status: composed.status ?? 'sending',
));
if (!online) return tempId;
try {
final actualId = await messagesModule.sendMessage(_myId, widget.chatId, text);
final realId = actualId.isNotEmpty ? actualId : tempId;
final i = _messages.indexWhere((m) => m.id == tempId);
if (i != -1) {
final sent = _replaceMessage(i, id: realId, status: 'sent');
unawaited(_persistOutgoing(sent, removeId: tempId));
unawaited(ChatsModule.applyOutgoing(
_myId,
widget.chatId,
messageId: realId,
time: now,
text: text,
status: 'sent',
));
}
return realId;
} catch (_) {
return tempId;
}
}
Future<void> _updateCommandMessage(String id, String text) async {
if (id.isEmpty) return;
final i = _messages.indexWhere((m) => m.id == id);
if (i != -1) {
final edited = _replaceMessage(i, text: text, status: 'EDITED');
unawaited(_persistOutgoing(edited));
}
if (!id.startsWith('temp_')) {
await messagesModule.editMessage(widget.chatId, id, text: text);
}
}
CommandContext _commandContext() => CommandContext(
accountId: _myId,
chatId: widget.chatId,
otherUserId: _resolveOtherId(),
messages: messagesModule,
isOnline: () => api.state == SessionState.online,
isActive: () => mounted,
notify: (message) {
if (mounted) showCustomNotification(context, message);
},
postMessage: _postCommandMessage,
updateMessage: _updateCommandMessage,
);
Future<void> _scheduleMessage() async { Future<void> _scheduleMessage() async {
final text = _messageController.text.trim(); final text = _messageController.text.trim();
if (text.isEmpty || _myId == 0) return; if (text.isEmpty || _myId == 0) return;
@@ -2066,9 +2236,21 @@ class _ChatScreenState extends State<ChatScreen>
body: Column( body: Column(
children: [ children: [
Expanded( Expanded(
child: _isLoading && _messages.isEmpty child: Stack(
? _buildShimmerLoading() children: [
: _buildMessagesList(), Positioned.fill(
child: _isLoading && _messages.isEmpty
? _buildShimmerLoading()
: _buildMessagesList(),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildCommandPanel(),
),
],
),
), ),
AnimatedBuilder( AnimatedBuilder(
animation: _attachAnim, animation: _attachAnim,
@@ -6,6 +6,7 @@ import '../../../backend/modules/chats.dart';
import '../../../core/config/app_swipe_back_desktop.dart'; import '../../../core/config/app_swipe_back_desktop.dart';
import '../../../core/config/app_pranks.dart'; import '../../../core/config/app_pranks.dart';
import '../../../core/config/app_stories.dart'; import '../../../core/config/app_stories.dart';
import '../../../core/config/app_commands.dart';
import '../../../core/config/app_link_preview.dart'; import '../../../core/config/app_link_preview.dart';
import '../../../core/config/app_digital_id_mode.dart'; import '../../../core/config/app_digital_id_mode.dart';
import '../../../core/config/app_media_cache.dart'; import '../../../core/config/app_media_cache.dart';
@@ -786,6 +787,65 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
), ),
), ),
), ),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: ValueListenableBuilder<bool>(
valueListenable: AppCommands.current,
builder: (context, commandsOn, _) {
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
depth: 6,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.terminal,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Команды',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Панель команд по вводу «/» в строке сообщения',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Switch(
value: commandsOn,
onChanged: (v) {
AppCommands.save(v);
},
),
],
),
);
},
),
),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import '../commands/command_registry.dart';
import '../commands/slash_command.dart';
class CommandSuggestionsPanel extends StatelessWidget {
final List<SlashCommand> commands;
final double maxHeight;
final ValueChanged<SlashCommand>? onSelected;
const CommandSuggestionsPanel({
super.key,
this.commands = kSlashCommands,
this.maxHeight = 220,
this.onSelected,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final visible = commands.where((c) => !c.hidden).toList(growable: false);
return Material(
type: MaterialType.transparency,
child: Container(
constraints: BoxConstraints(maxHeight: maxHeight),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.3)),
),
clipBehavior: Clip.antiAlias,
child: ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 6),
itemCount: visible.length,
separatorBuilder: (_, _) => Divider(
height: 1,
thickness: 1,
indent: 14,
endIndent: 14,
color: cs.outlineVariant.withValues(alpha: 0.18),
),
itemBuilder: (context, i) {
final c = visible[i];
return InkWell(
onTap: onSelected == null ? null : () => onSelected!(c),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 84,
child: Text(
c.name,
style: TextStyle(
color: cs.primary,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
c.description,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
),
),
),
],
),
),
);
},
),
),
);
}
}
+3
View File
@@ -25,6 +25,7 @@ import 'core/config/app_message_actions_style.dart';
import 'core/config/app_swipe_back_desktop.dart'; import 'core/config/app_swipe_back_desktop.dart';
import 'core/config/app_pranks.dart'; import 'core/config/app_pranks.dart';
import 'core/config/app_stories.dart'; import 'core/config/app_stories.dart';
import 'core/config/app_commands.dart';
import 'core/config/app_link_preview.dart'; import 'core/config/app_link_preview.dart';
import 'core/config/app_media_cache.dart'; import 'core/config/app_media_cache.dart';
import 'core/config/app_pill_gradient.dart'; import 'core/config/app_pill_gradient.dart';
@@ -112,6 +113,7 @@ void main() async {
final swipeBackFuture = AppSwipeBackDesktop.load(); final swipeBackFuture = AppSwipeBackDesktop.load();
final pranksFuture = AppPranks.load(); final pranksFuture = AppPranks.load();
final storiesFuture = AppStories.load(); final storiesFuture = AppStories.load();
final commandsFuture = AppCommands.load();
final linkPreviewFuture = AppLinkPreview.load(); final linkPreviewFuture = AppLinkPreview.load();
final cacheLimitFuture = AppMediaCacheLimit.load(); final cacheLimitFuture = AppMediaCacheLimit.load();
final digitalIdNativeFuture = AppDigitalIdNative.load(); final digitalIdNativeFuture = AppDigitalIdNative.load();
@@ -150,6 +152,7 @@ void main() async {
AppSwipeBackDesktop.current.value = await swipeBackFuture; AppSwipeBackDesktop.current.value = await swipeBackFuture;
AppPranks.current.value = await pranksFuture; AppPranks.current.value = await pranksFuture;
AppStories.current.value = await storiesFuture; AppStories.current.value = await storiesFuture;
AppCommands.current.value = await commandsFuture;
AppLinkPreview.current.value = await linkPreviewFuture; AppLinkPreview.current.value = await linkPreviewFuture;
AppMediaCacheLimit.current.value = await cacheLimitFuture; AppMediaCacheLimit.current.value = await cacheLimitFuture;
AppDigitalIdNative.current.value = await digitalIdNativeFuture; AppDigitalIdNative.current.value = await digitalIdNativeFuture;
-72
View File
@@ -1,72 +0,0 @@
"""mitmproxy addon: dump okcdn call signaling (ws2 WebSocket + HTTP) to a log."""
import json
import time
from mitmproxy import http, ctx
LOG = r"C:\Users\klockky\Komet\docs\ws2_capture.log"
HOSTS = ("okcdn.ru", "videowebrtc")
def _interesting(host: str) -> bool:
return any(h in host for h in HOSTS)
def _w(line: str) -> None:
with open(LOG, "a", encoding="utf-8") as f:
f.write(line + "\n")
def _fmt(content: bytes) -> str:
try:
text = content.decode("utf-8")
try:
return json.dumps(json.loads(text), ensure_ascii=False, indent=2)
except Exception:
return text
except Exception:
return "HEX " + content.hex()
def websocket_start(flow: http.HTTPFlow) -> None:
if not _interesting(flow.request.pretty_host):
return
_w("=" * 70)
_w(f"# WS OPEN {flow.request.pretty_host} {flow.request.path}")
_w(f" headers: {dict(flow.request.headers)}")
_w("=" * 70)
def websocket_message(flow: http.HTTPFlow) -> None:
if not _interesting(flow.request.pretty_host):
return
msg = flow.websocket.messages[-1]
arrow = "TX (client->server)" if msg.from_client else "RX (server->client)"
ts = time.strftime("%H:%M:%S")
_w(f"\n--- {arrow} {ts} {len(msg.content)} B host={flow.request.pretty_host} ---")
_w(_fmt(msg.content))
def websocket_end(flow: http.HTTPFlow) -> None:
if not _interesting(flow.request.pretty_host):
return
_w(f"\n# WS CLOSE {flow.request.pretty_host}\n")
def response(flow: http.HTTPFlow) -> None:
host = flow.request.pretty_host
if not _interesting(host):
return
if flow.websocket is not None:
return
_w("\n" + "#" * 70)
_w(f"# HTTP {flow.request.method} {host}{flow.request.path} -> {flow.response.status_code}")
if flow.request.content:
_w(" REQ: " + _fmt(flow.request.content)[:2000])
if flow.response.content:
_w(" RES: " + _fmt(flow.response.content)[:2000])
def load(loader) -> None:
_w(f"\n\n########## capture session start {time.strftime('%Y-%m-%d %H:%M:%S')} ##########")
ctx.log.info("ws2_dump addon loaded")