implemented avatar management for user profiles and chat rooms, including 1:1 cropping, integrated OCS and Spreed avatar APIs, added cache invalidation logic, and updated the account settings view to display user info and profile pictures.
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../model/account_data.dart';
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../errors/auth_exception.dart';
|
||||
import '../../errors/network_exception.dart';
|
||||
import '../../errors/not_found_exception.dart';
|
||||
import '../../errors/parse_exception.dart';
|
||||
import '../../errors/server_exception.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
|
||||
/// Mix of two Nextcloud surfaces:
|
||||
/// - User info comes from the OCS provisioning API
|
||||
/// (`/ocs/v2.php/cloud/users/{userId}`).
|
||||
/// - The own-avatar upload/delete uses the *core* AvatarController at
|
||||
/// `/avatar/` — the OCS provisioning route has no POST (it answers 405).
|
||||
/// This is the same controller the read path (`/avatar/{id}/{size}` in
|
||||
/// [UserAvatar]) already talks to. CSRF is bypassed because we use Basic
|
||||
/// Auth without a session cookie.
|
||||
|
||||
/// Core AvatarController endpoint for the logged-in user (POST sets, DELETE
|
||||
/// removes). Built against the bare Nextcloud base (domain + optional path),
|
||||
/// not the OCS wrapper.
|
||||
Uri _coreAvatarUri() {
|
||||
final endpoint = EndpointData().nextcloud();
|
||||
return Uri.https(endpoint.domain, '${endpoint.path}/avatar/');
|
||||
}
|
||||
|
||||
Uri _userInfoUri() =>
|
||||
NextcloudOcs.uri('cloud/users/${AccountData().getUsername()}');
|
||||
|
||||
Future<http.Response> _send(
|
||||
Future<http.Response> Function(Uri uri, Map<String, String> headers)
|
||||
perform,
|
||||
Uri uri,
|
||||
) async {
|
||||
final headers = NextcloudOcs.headers();
|
||||
|
||||
final http.Response response;
|
||||
try {
|
||||
response = await perform(uri, headers);
|
||||
} on SocketException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
||||
} on TimeoutException catch (e) {
|
||||
throw NetworkException.timeout(technicalDetails: 'Cloud $uri: $e');
|
||||
} on http.ClientException catch (e) {
|
||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
||||
}
|
||||
|
||||
final status = response.statusCode;
|
||||
if (status >= 200 && status < 300) return response;
|
||||
|
||||
final body = response.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
||||
final detail = body.isEmpty
|
||||
? 'Cloud $uri -> HTTP $status'
|
||||
: 'Cloud $uri -> HTTP $status body=$preview';
|
||||
log(detail);
|
||||
if (status == 401) throw AuthException.unauthorized(technicalDetails: detail);
|
||||
if (status == 403) throw AuthException.forbidden(technicalDetails: detail);
|
||||
if (status == 404) throw NotFoundException(technicalDetails: detail);
|
||||
throw ServerException(statusCode: status, technicalDetails: detail);
|
||||
}
|
||||
|
||||
class SetUserAvatar {
|
||||
final Uint8List bytes;
|
||||
final String filename;
|
||||
SetUserAvatar(this.bytes, {this.filename = 'avatar.jpg'});
|
||||
|
||||
Future<void> run() async {
|
||||
await _send((uri, headers) async {
|
||||
// Core AvatarController reads $_FILES['files']['error'][0] — the field
|
||||
// must be `files[]` so PHP exposes it as an array, matching the web UI.
|
||||
final req = http.MultipartRequest('POST', uri)
|
||||
..headers.addAll(headers)
|
||||
..files.add(
|
||||
http.MultipartFile.fromBytes('files[]', bytes, filename: filename),
|
||||
);
|
||||
final streamed = await req.send();
|
||||
return http.Response.fromStream(streamed);
|
||||
}, _coreAvatarUri());
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteUserAvatar {
|
||||
Future<void> run() async {
|
||||
await _send(
|
||||
(uri, headers) => http.delete(uri, headers: headers),
|
||||
_coreAvatarUri(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CloudUserInfo {
|
||||
final String userId;
|
||||
final String displayName;
|
||||
const CloudUserInfo({required this.userId, required this.displayName});
|
||||
}
|
||||
|
||||
/// Reads the current user's provisioning record. The OCS wrapper looks like:
|
||||
/// `{ "ocs": { "meta": {...}, "data": { "id": "...", "displayname": "...", ... } } }`.
|
||||
/// We only need displayname; everything else is discarded.
|
||||
class GetUserInfo {
|
||||
Future<CloudUserInfo> run() async {
|
||||
final uri = _userInfoUri();
|
||||
final response = await _send(
|
||||
(u, headers) => http.get(u, headers: headers),
|
||||
uri,
|
||||
);
|
||||
try {
|
||||
final root = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final data =
|
||||
(root['ocs'] as Map<String, dynamic>)['data']
|
||||
as Map<String, dynamic>;
|
||||
return CloudUserInfo(
|
||||
userId: data['id'] as String,
|
||||
displayName: (data['displayname'] as String?) ?? '',
|
||||
);
|
||||
} catch (e) {
|
||||
throw ParseException(
|
||||
technicalDetails: 'Cloud $uri user info parse: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user