diff --git a/lib/database.dart b/lib/database.dart index 1fa1859..5bd08bb 100644 --- a/lib/database.dart +++ b/lib/database.dart @@ -226,7 +226,7 @@ Future actorProfileToDatabase(bsky.ActorProfile actor) async { if (existing == null) { // Doesn't exist, create a new record. final id = hashBlueskyToId(actor.did); - final info = ProfileInfo.fromActorProfile(actor); + final info = await ProfileInfo.fromActorProfile(actor); return db.userRecord.upsert( where: UserRecordWhereUniqueInput(did: actor.did), diff --git a/lib/models/mastodon/mastodon_account.dart b/lib/models/mastodon/mastodon_account.dart index 08b97d2..0f8ec24 100644 --- a/lib/models/mastodon/mastodon_account.dart +++ b/lib/models/mastodon/mastodon_account.dart @@ -1,7 +1,8 @@ -import 'package:bluesky/bluesky.dart'; -import 'package:isar/isar.dart'; +import 'package:bluesky/bluesky.dart' as bsky; +import 'package:bluesky_text/bluesky_text.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:sky_bridge/database.dart'; +import 'package:sky_bridge/facets.dart'; import 'package:sky_bridge/models/mastodon/mastodon_post.dart'; import 'package:sky_bridge/util.dart'; @@ -42,9 +43,9 @@ class MastodonAccount { /// Converts the [MastodonAccount] to JSON. Map toJson() => _$MastodonAccountToJson(this); - /// Creates a [MastodonAccount] from an [ActorProfile]. + /// Creates a [MastodonAccount] from an [bsky.ActorProfile]. static Future fromActorProfile( - ActorProfile profile, + bsky.ActorProfile profile, ) async { // Assign/get a user ID from the database. final user = await actorProfileToDatabase(profile); @@ -71,7 +72,7 @@ class MastodonAccount { locked: false, bot: false, createdAt: profile.indexedAt ?? DateTime.now().toUtc(), - note: convertTextToLinks(profile.description), + note: await processProfileDescription(profile.description ?? ''), url: 'https://bsky.social/${profile.handle}', avatar: profile.avatar ?? avatarFallback, avatarStatic: profile.avatar ?? avatarFallback, @@ -86,8 +87,8 @@ class MastodonAccount { ); } - /// Creates a [MastodonAccount] from an [Actor]. - static Future fromActor(Actor profile) async { + /// Creates a [MastodonAccount] from an [bsky.Actor]. + static Future fromActor(bsky.Actor profile) async { // Assign/get a user ID from the database. final user = await actorToDatabase(profile); @@ -113,7 +114,7 @@ class MastodonAccount { locked: false, bot: false, createdAt: profile.indexedAt ?? DateTime.now().toUtc(), - note: convertTextToLinks(user.description), + note: await processProfileDescription(profile.description ?? user.description), url: 'https://bsky.social/${profile.handle}', avatar: profile.avatar ?? avatarFallback, avatarStatic: profile.avatar ?? avatarFallback, @@ -213,7 +214,6 @@ class MastodonAccount { /// Bluesky posts don't contain profile information, so we have to /// fetch it separately. This class is used to pass around that information /// with type safety. -@embedded class ProfileInfo { /// Creates a new [ProfileInfo] instance. ProfileInfo({ @@ -224,14 +224,14 @@ class ProfileInfo { this.description = '', }); - /// Creates a new [ProfileInfo] instance from an [ActorProfile]. - factory ProfileInfo.fromActorProfile(ActorProfile profile) { + /// Creates a new [ProfileInfo] instance from an [bsky.ActorProfile]. + static FuturefromActorProfile(bsky.ActorProfile profile) async { return ProfileInfo( banner: profile.banner ?? '', followersCount: profile.followersCount, followsCount: profile.followsCount, postsCount: profile.postsCount, - description: convertTextToLinks(profile.description ?? ''), + description: await processProfileDescription(profile.description ?? ''), ); } @@ -359,3 +359,13 @@ class AccountRole { /// Whether the role is publicly visible as a badge on user profiles. final bool highlighted; } + +/// Processes handles and links in a profile description. +Future processProfileDescription(String description) async { + final text = BlueskyText(description); + final textFacets = await text.entities.toFacets(); + final facets = textFacets.map(bsky.Facet.fromJson).toList(); + final processedBio = await processFacets(facets, description); + + return processedBio.htmlText; +} diff --git a/lib/models/mastodon/mastodon_account.g.dart b/lib/models/mastodon/mastodon_account.g.dart index 3aee0ab..b9b04c7 100644 --- a/lib/models/mastodon/mastodon_account.g.dart +++ b/lib/models/mastodon/mastodon_account.g.dart @@ -2,555 +2,6 @@ part of 'mastodon_account.dart'; -// ************************************************************************** -// IsarEmbeddedGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -const ProfileInfoSchema = Schema( - name: r'ProfileInfo', - id: 5701495882167099439, - properties: { - r'banner': PropertySchema( - id: 0, - name: r'banner', - type: IsarType.string, - ), - r'description': PropertySchema( - id: 1, - name: r'description', - type: IsarType.string, - ), - r'followersCount': PropertySchema( - id: 2, - name: r'followersCount', - type: IsarType.long, - ), - r'followsCount': PropertySchema( - id: 3, - name: r'followsCount', - type: IsarType.long, - ), - r'postsCount': PropertySchema( - id: 4, - name: r'postsCount', - type: IsarType.long, - ) - }, - estimateSize: _profileInfoEstimateSize, - serialize: _profileInfoSerialize, - deserialize: _profileInfoDeserialize, - deserializeProp: _profileInfoDeserializeProp, -); - -int _profileInfoEstimateSize( - ProfileInfo object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.banner.length * 3; - bytesCount += 3 + object.description.length * 3; - return bytesCount; -} - -void _profileInfoSerialize( - ProfileInfo object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeString(offsets[0], object.banner); - writer.writeString(offsets[1], object.description); - writer.writeLong(offsets[2], object.followersCount); - writer.writeLong(offsets[3], object.followsCount); - writer.writeLong(offsets[4], object.postsCount); -} - -ProfileInfo _profileInfoDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = ProfileInfo( - banner: reader.readStringOrNull(offsets[0]) ?? '', - description: reader.readStringOrNull(offsets[1]) ?? '', - followersCount: reader.readLongOrNull(offsets[2]) ?? 0, - followsCount: reader.readLongOrNull(offsets[3]) ?? 0, - postsCount: reader.readLongOrNull(offsets[4]) ?? 0, - ); - return object; -} - -P _profileInfoDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readStringOrNull(offset) ?? '') as P; - case 1: - return (reader.readStringOrNull(offset) ?? '') as P; - case 2: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 3: - return (reader.readLongOrNull(offset) ?? 0) as P; - case 4: - return (reader.readLongOrNull(offset) ?? 0) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -extension ProfileInfoQueryFilter - on QueryBuilder { - QueryBuilder bannerEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'banner', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - bannerGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'banner', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder bannerLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'banner', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder bannerBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'banner', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - bannerStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'banner', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder bannerEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'banner', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder bannerContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'banner', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder bannerMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'banner', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - bannerIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'banner', - value: '', - )); - }); - } - - QueryBuilder - bannerIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'banner', - value: '', - )); - }); - } - - QueryBuilder - descriptionEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'description', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - descriptionGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - descriptionLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'description', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - descriptionBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'description', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - descriptionStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - descriptionEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'description', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - descriptionContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'description', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - descriptionMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'description', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - descriptionIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'description', - value: '', - )); - }); - } - - QueryBuilder - descriptionIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'description', - value: '', - )); - }); - } - - QueryBuilder - followersCountEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'followersCount', - value: value, - )); - }); - } - - QueryBuilder - followersCountGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'followersCount', - value: value, - )); - }); - } - - QueryBuilder - followersCountLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'followersCount', - value: value, - )); - }); - } - - QueryBuilder - followersCountBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'followersCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - followsCountEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'followsCount', - value: value, - )); - }); - } - - QueryBuilder - followsCountGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'followsCount', - value: value, - )); - }); - } - - QueryBuilder - followsCountLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'followsCount', - value: value, - )); - }); - } - - QueryBuilder - followsCountBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'followsCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - postsCountEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'postsCount', - value: value, - )); - }); - } - - QueryBuilder - postsCountGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'postsCount', - value: value, - )); - }); - } - - QueryBuilder - postsCountLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'postsCount', - value: value, - )); - }); - } - - QueryBuilder - postsCountBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'postsCount', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } -} - -extension ProfileInfoQueryObject - on QueryBuilder {} - // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** diff --git a/lib/util.dart b/lib/util.dart index 886b60d..ea2cf95 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -254,22 +254,6 @@ String sanitizeText(String text) { return sanitizedText ?? ''; } -/// Converts all links in a string to HTML links. -String convertTextToLinks(String? text) { - if (text == null) return ''; - // TODO(videah): This regex is not perfect, would like to cover more cases. - final linkRegex = RegExp(r'(?$url'; - }); -} - - /// [Uri.toString] lowercases the host, which breaks the URI /// for some clients. This is just a simple function that preserves the casing. String stringifyModifiedUri(Uri uri, String originalUri) {