feat(stories): публикация видео-историй + фикс TTL
This commit is contained in:
@@ -198,6 +198,12 @@ class FileUploader {
|
||||
final respBody = utf8.decode(result.body, allowMalformed: true);
|
||||
final hasError =
|
||||
respBody.contains('error_msg') || respBody.contains('error_code');
|
||||
if (result.status != 200 || hasError) {
|
||||
logger.w(
|
||||
'uploadMediaFile rejected: status=${result.status} '
|
||||
'body=${respBody.length > 500 ? respBody.substring(0, 500) : respBody}',
|
||||
);
|
||||
}
|
||||
return result.status == 200 && !hasError;
|
||||
} catch (e) {
|
||||
logger.w('uploadMediaFile: $e');
|
||||
@@ -292,6 +298,80 @@ class FileUploader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Загрузка видео для истории. В отличие от чата (чанковый `uploadVideoPath`
|
||||
/// с GET-handshake), story-эндпоинт `su.oneme.ru/uploadVideo` ждёт **один POST
|
||||
/// на весь файл** (как у оригинального клиента) и возвращает медиа-токен в теле
|
||||
/// ответа — `[{"token":"..."}]`. Именно этот токен идёт в `media.token`
|
||||
/// STORIES_SEND, а не токен из ответа VIDEO_UPLOAD.
|
||||
Future<({bool ok, String? token})> uploadVideoWithToken(
|
||||
Uri uri,
|
||||
File file, {
|
||||
void Function(int sent, int total)? onProgress,
|
||||
}) async {
|
||||
final session = api.session;
|
||||
if (session == null) return (ok: false, token: null);
|
||||
try {
|
||||
final result = await _consume(
|
||||
session.uploadFilePath(
|
||||
url: uri.toString(),
|
||||
path: file.path,
|
||||
filename: _syntheticFilename(),
|
||||
contentType: 'application/octet-stream',
|
||||
connection: 'close',
|
||||
),
|
||||
onProgress: onProgress,
|
||||
);
|
||||
if (result.error != null || result.status != 200) {
|
||||
logger.w(
|
||||
'uploadVideoWithToken: status=${result.status} error=${result.error}',
|
||||
);
|
||||
return (ok: false, token: null);
|
||||
}
|
||||
final token = _parseVideoToken(
|
||||
utf8.decode(result.body, allowMalformed: true),
|
||||
);
|
||||
return (ok: true, token: token);
|
||||
} catch (e) {
|
||||
logger.w('uploadVideoWithToken: $e');
|
||||
return (ok: false, token: null);
|
||||
}
|
||||
}
|
||||
|
||||
String? _parseVideoToken(String body) {
|
||||
try {
|
||||
final json = jsonDecode(body);
|
||||
// Сервер отвечает массивом: [{"token":"..."}]
|
||||
if (json is List) {
|
||||
for (final v in json) {
|
||||
if (v is Map) {
|
||||
final token = v['token'];
|
||||
if (token is String && token.isNotEmpty) return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (json is Map) {
|
||||
for (final key in const ['videos', 'video', 'photos']) {
|
||||
final node = json[key];
|
||||
if (node is Map) {
|
||||
for (final v in node.values) {
|
||||
if (v is Map) {
|
||||
final token = v['token'];
|
||||
if (token is String && token.isNotEmpty) return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final key in const ['token', 'videoToken', 'photoToken']) {
|
||||
final t = json[key];
|
||||
if (t is String && t.isNotEmpty) return t;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.w('parseVideoToken: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Прогоняет стрим ядра до конца, форвардит прогресс, отдаёт итог.
|
||||
Future<({int status, Uint8List body, String? error})> _consume(
|
||||
Stream<kb.UploadEvent> stream, {
|
||||
|
||||
@@ -1329,8 +1329,11 @@ class MessagesModule {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> requestPhotoUploadUrl() async {
|
||||
final response = await _api.sendRequest(Opcode.photoUpload, {'count': 1});
|
||||
Future<String?> requestPhotoUploadUrl({int? type}) async {
|
||||
final response = await _api.sendRequest(Opcode.photoUpload, {
|
||||
'type': ?type,
|
||||
'count': 1,
|
||||
});
|
||||
if (!response.isOk) return null;
|
||||
final data = response.payload;
|
||||
if (data is! Map) return null;
|
||||
@@ -1371,10 +1374,10 @@ class MessagesModule {
|
||||
);
|
||||
}
|
||||
|
||||
Future<VideoUploadInfo?> requestVideoUploadUrl() async {
|
||||
Future<VideoUploadInfo?> requestVideoUploadUrl({int type = 0}) async {
|
||||
final response = await _api.sendRequest(Opcode.videoUpload, {
|
||||
'uploaderType': 0,
|
||||
'type': 0,
|
||||
'type': type,
|
||||
'count': 1,
|
||||
});
|
||||
if (!response.isOk) return null;
|
||||
|
||||
@@ -322,13 +322,45 @@ class StoriesModule {
|
||||
}
|
||||
|
||||
/// Публикация фото-истории. [photoToken] — токен уже загруженного фото.
|
||||
/// [settings]: 1 = видно всем, 2 = только контактам. [expiration] — TTL, сек.
|
||||
/// [settings]: 1 = видно всем, 2 = только контактам. [expiration] — TTL, мс.
|
||||
/// Бросает [PacketError]/[TimeoutException] при ошибке сервера — чтобы UI
|
||||
/// показал реальную причину, а не общее «не удалось».
|
||||
Future<void> publishPhoto({
|
||||
required String photoToken,
|
||||
int settings = 1,
|
||||
int expiration = 86400,
|
||||
int expiration = 86400000,
|
||||
}) {
|
||||
return _publishMedia(
|
||||
media: {'_type': 'PHOTO', 'photoToken': photoToken},
|
||||
settings: settings,
|
||||
expiration: expiration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Публикация видео-истории. [videoToken] — токен уже загруженного видео
|
||||
/// (`VideoUploadInfo.token`), [durationMs] — длительность ролика в мс.
|
||||
Future<void> publishVideo({
|
||||
required String videoToken,
|
||||
int? durationMs,
|
||||
int settings = 1,
|
||||
int expiration = 86400000,
|
||||
}) {
|
||||
return _publishMedia(
|
||||
media: {
|
||||
'_type': 'VIDEO',
|
||||
'videoType': 2,
|
||||
'token': videoToken,
|
||||
'duration': ?durationMs,
|
||||
},
|
||||
settings: settings,
|
||||
expiration: expiration,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _publishMedia({
|
||||
required Map<String, dynamic> media,
|
||||
required int settings,
|
||||
required int expiration,
|
||||
}) async {
|
||||
if (_api.state != SessionState.online) {
|
||||
throw const PacketError('Нет соединения с сервером');
|
||||
@@ -339,7 +371,7 @@ class StoriesModule {
|
||||
{
|
||||
'cid': cid,
|
||||
'settings': settings,
|
||||
'media': {'_type': 'PHOTO', 'photoToken': photoToken},
|
||||
'media': media,
|
||||
'expiration': expiration,
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user