2024-05-29 02:59:49 +02:00
import { idValidator } from "@/api" ;
2025-02-11 18:22:39 +01:00
import { getBestContentType } from "@/content_types" ;
2024-06-13 07:38:26 +02:00
import { randomString } from "@/math" ;
2024-07-26 18:51:39 +02:00
import { sentry } from "@/sentry" ;
import { getLogger } from "@logtape/logtape" ;
2025-03-22 02:34:03 +01:00
import type {
Account ,
Mention as MentionSchema ,
Source ,
2025-03-22 18:04:47 +01:00
} from "@versia/client/schemas" ;
import type { RolePermission } from "@versia/client/schemas" ;
2024-07-26 18:51:39 +02:00
import {
EntityValidator ,
FederationRequester ,
type HttpVerb ,
SignatureConstructor ,
2024-08-26 19:06:49 +02:00
} from "@versia/federation" ;
2024-08-27 15:50:14 +02:00
import type {
Collection ,
Unfollow ,
2024-11-25 20:50:55 +01:00
FollowAccept as VersiaFollowAccept ,
FollowReject as VersiaFollowReject ,
2024-08-27 15:50:14 +02:00
User as VersiaUser ,
} from "@versia/federation/types" ;
2025-01-28 19:07:55 +01:00
import { Media , Notification , PushSubscription , db } from "@versia/kit/db" ;
2024-11-01 21:05:54 +01:00
import {
EmojiToUser ,
Likes ,
NoteToMentions ,
Notes ,
Notifications ,
UserToPinnedNotes ,
Users ,
} from "@versia/kit/tables" ;
2025-03-30 22:10:33 +02:00
import { randomUUIDv7 } from "bun" ;
2025-03-30 23:06:34 +02:00
import { password as bunPassword } from "bun" ;
2024-07-26 18:51:39 +02:00
import chalk from "chalk" ;
2024-05-06 09:16:33 +02:00
import {
2024-06-13 04:26:43 +02:00
type InferInsertModel ,
2024-11-04 14:58:17 +01:00
type InferSelectModel ,
2024-05-06 09:16:33 +02:00
type SQL ,
and ,
countDistinct ,
desc ,
eq ,
gte ,
inArray ,
2024-06-06 07:12:23 +02:00
isNotNull ,
2024-05-06 09:16:33 +02:00
isNull ,
2024-06-06 06:58:28 +02:00
sql ,
2024-05-06 09:16:33 +02:00
} from "drizzle-orm" ;
2024-04-25 05:40:27 +02:00
import { htmlToText } from "html-to-text" ;
2025-03-29 03:30:06 +01:00
import type { z } from "zod" ;
2024-11-25 20:50:55 +01:00
import { findManyUsers } from "~/classes/functions/user" ;
2024-06-29 11:40:44 +02:00
import { searchManager } from "~/classes/search/search-manager" ;
2025-02-15 02:47:29 +01:00
import { config } from "~/config.ts" ;
2024-08-26 19:06:49 +02:00
import type { KnownEntity } from "~/types/api.ts" ;
2025-03-30 23:44:50 +02:00
import { ProxiableUrl } from "../media/url.ts" ;
2024-11-25 21:54:31 +01:00
import { DeliveryJobType , deliveryQueue } from "../queues/delivery.ts" ;
2025-01-02 02:45:40 +01:00
import { PushJobType , pushQueue } from "../queues/push.ts" ;
2024-10-04 15:22:48 +02:00
import { BaseInterface } from "./base.ts" ;
2024-11-04 15:20:53 +01:00
import { Emoji } from "./emoji.ts" ;
import { Instance } from "./instance.ts" ;
import { Like } from "./like.ts" ;
import type { Note } from "./note.ts" ;
import { Relationship } from "./relationship.ts" ;
import { Role } from "./role.ts" ;
2024-04-25 05:40:27 +02:00
2024-11-04 14:58:17 +01:00
type UserWithInstance = InferSelectModel < typeof Users > & {
instance : typeof Instance . $type | null ;
} ;
type UserWithRelations = UserWithInstance & {
emojis : ( typeof Emoji . $type ) [ ] ;
2025-01-28 19:07:55 +01:00
avatar : typeof Media . $type | null ;
header : typeof Media . $type | null ;
2024-11-04 14:58:17 +01:00
followerCount : number ;
followingCount : number ;
statusCount : number ;
roles : ( typeof Role . $type ) [ ] ;
} ;
2024-04-25 05:40:27 +02:00
/ * *
* Gives helpers to fetch users from database in a nice format
* /
2024-06-13 02:45:07 +02:00
export class User extends BaseInterface < typeof Users , UserWithRelations > {
2024-11-04 14:58:17 +01:00
public static $type : UserWithRelations ;
2025-01-28 19:07:55 +01:00
public avatar : Media | null ;
public header : Media | null ;
public constructor ( data : UserWithRelations ) {
super ( data ) ;
this . avatar = data . avatar ? new Media ( data . avatar ) : null ;
this . header = data . header ? new Media ( data . header ) : null ;
}
2024-11-01 21:20:12 +01:00
public async reload ( ) : Promise < void > {
2024-06-13 02:45:07 +02:00
const reloaded = await User . fromId ( this . data . id ) ;
if ( ! reloaded ) {
throw new Error ( "Failed to reload user" ) ;
}
this . data = reloaded . data ;
2025-03-23 03:34:17 +01:00
this . avatar = reloaded . avatar ;
this . header = reloaded . header ;
2024-06-13 02:45:07 +02:00
}
2024-04-25 05:40:27 +02:00
2024-11-01 21:20:12 +01:00
public static async fromId ( id : string | null ) : Promise < User | null > {
2024-06-13 04:26:43 +02:00
if ( ! id ) {
return null ;
}
2024-04-25 05:40:27 +02:00
return await User . fromSql ( eq ( Users . id , id ) ) ;
}
2024-11-01 21:20:12 +01:00
public static async fromIds ( ids : string [ ] ) : Promise < User [ ] > {
2024-04-25 05:40:27 +02:00
return await User . manyFromSql ( inArray ( Users . id , ids ) ) ;
}
2024-11-01 21:20:12 +01:00
public static async fromSql (
2024-04-25 05:40:27 +02:00
sql : SQL < unknown > | undefined ,
orderBy : SQL < unknown > | undefined = desc ( Users . id ) ,
2024-11-02 00:43:33 +01:00
) : Promise < User | null > {
2024-06-13 08:34:17 +02:00
const found = await findManyUsers ( {
2024-04-25 05:40:27 +02:00
where : sql ,
orderBy ,
} ) ;
2024-06-13 08:34:17 +02:00
if ( ! found [ 0 ] ) {
2024-06-13 04:26:43 +02:00
return null ;
}
2024-06-13 08:34:17 +02:00
return new User ( found [ 0 ] ) ;
2024-04-25 05:40:27 +02:00
}
2024-11-01 21:20:12 +01:00
public static async manyFromSql (
2024-04-25 05:40:27 +02:00
sql : SQL < unknown > | undefined ,
orderBy : SQL < unknown > | undefined = desc ( Users . id ) ,
limit? : number ,
offset? : number ,
extra? : Parameters < typeof db.query.Users.findMany > [ 0 ] ,
2024-11-02 00:43:33 +01:00
) : Promise < User [ ] > {
2024-04-25 05:40:27 +02:00
const found = await findManyUsers ( {
where : sql ,
orderBy ,
limit ,
offset ,
with : extra ? . with ,
} ) ;
return found . map ( ( s ) = > new User ( s ) ) ;
}
2024-11-02 00:43:33 +01:00
public get id ( ) : string {
2024-06-13 02:45:07 +02:00
return this . data . id ;
2024-04-25 05:40:27 +02:00
}
2024-11-02 00:43:33 +01:00
public isLocal ( ) : boolean {
2024-06-13 02:45:07 +02:00
return this . data . instanceId === null ;
2024-04-25 05:40:27 +02:00
}
2024-11-02 00:43:33 +01:00
public isRemote ( ) : boolean {
2024-04-25 05:40:27 +02:00
return ! this . isLocal ( ) ;
}
2025-02-01 16:32:18 +01:00
public getUri ( ) : URL {
return this . data . uri
? new URL ( this . data . uri )
: new URL ( ` /users/ ${ this . data . id } ` , config . http . base_url ) ;
2024-04-25 05:40:27 +02:00
}
2025-02-01 16:32:18 +01:00
public static getUri ( id : string , uri : URL | null ) : URL {
return uri ? uri : new URL ( ` /users/ ${ id } ` , config . http . base_url ) ;
2024-04-25 05:40:27 +02:00
}
2025-03-22 18:04:47 +01:00
public hasPermission ( permission : RolePermission ) : boolean {
2024-06-08 06:57:29 +02:00
return this . getAllPermissions ( ) . includes ( permission ) ;
}
2025-03-22 18:04:47 +01:00
public getAllPermissions ( ) : RolePermission [ ] {
2025-03-30 23:44:50 +02:00
return Array . from (
new Set ( [
. . . this . data . roles . flatMap ( ( role ) = > role . permissions ) ,
2024-06-08 06:57:29 +02:00
// Add default permissions
2025-03-30 23:44:50 +02:00
. . . config . permissions . default ,
2024-06-08 06:57:29 +02:00
// If admin, add admin permissions
2025-03-30 23:44:50 +02:00
. . . ( this . data . isAdmin ? config . permissions . admin : [ ] ) ,
] ) ,
2024-06-08 06:57:29 +02:00
) ;
}
2024-07-27 20:46:19 +02:00
public async followRequest (
otherUser : User ,
options ? : {
reblogs? : boolean ;
notify? : boolean ;
languages? : string [ ] ;
} ,
) : Promise < Relationship > {
const foundRelationship = await Relationship . fromOwnerAndSubject (
this ,
otherUser ,
) ;
await foundRelationship . update ( {
following : otherUser.isRemote ( ) ? false : ! otherUser . data . isLocked ,
requested : otherUser.isRemote ( ) ? true : otherUser . data . isLocked ,
showingReblogs : options?.reblogs ,
notifying : options?.notify ,
languages : options?.languages ,
} ) ;
if ( otherUser . isRemote ( ) ) {
2024-11-25 20:50:55 +01:00
await deliveryQueue . add ( DeliveryJobType . FederateEntity , {
entity : {
type : "Follow" ,
id : crypto.randomUUID ( ) ,
2025-02-01 16:32:18 +01:00
author : this.getUri ( ) . toString ( ) ,
followee : otherUser.getUri ( ) . toString ( ) ,
2024-11-25 20:50:55 +01:00
created_at : new Date ( ) . toISOString ( ) ,
} ,
recipientId : otherUser.id ,
senderId : this.id ,
} ) ;
2024-07-27 20:46:19 +02:00
} else {
2024-12-09 15:01:19 +01:00
await otherUser . notify (
2024-12-09 13:50:46 +01:00
otherUser . data . isLocked ? "follow_request" : "follow" ,
this ,
) ;
2024-07-27 20:46:19 +02:00
}
return foundRelationship ;
}
2024-11-02 00:43:33 +01:00
public async unfollow (
followee : User ,
relationship : Relationship ,
2024-12-30 18:00:23 +01:00
) : Promise < void > {
2024-08-02 17:28:50 +02:00
if ( followee . isRemote ( ) ) {
2024-11-25 20:50:55 +01:00
await deliveryQueue . add ( DeliveryJobType . FederateEntity , {
entity : this.unfollowToVersia ( followee ) ,
recipientId : followee.id ,
senderId : this.id ,
} ) ;
2024-08-02 17:28:50 +02:00
}
await relationship . update ( {
following : false ,
} ) ;
}
2024-08-27 15:50:14 +02:00
private unfollowToVersia ( followee : User ) : Unfollow {
const id = crypto . randomUUID ( ) ;
return {
type : "Unfollow" ,
id ,
2025-02-01 16:32:18 +01:00
author : this.getUri ( ) . toString ( ) ,
2024-08-27 15:50:14 +02:00
created_at : new Date ( ) . toISOString ( ) ,
2025-02-01 16:32:18 +01:00
followee : followee.getUri ( ) . toString ( ) ,
2024-08-27 15:50:14 +02:00
} ;
}
2024-11-01 20:42:32 +01:00
public async sendFollowAccept ( follower : User ) : Promise < void > {
2024-11-25 20:50:55 +01:00
if ( ! follower . isRemote ( ) ) {
throw new Error ( "Follower must be a remote user" ) ;
}
if ( this . isRemote ( ) ) {
throw new Error ( "Followee must be a local user" ) ;
}
const entity : VersiaFollowAccept = {
type : "FollowAccept" ,
id : crypto.randomUUID ( ) ,
2025-02-01 16:32:18 +01:00
author : this.getUri ( ) . toString ( ) ,
2024-11-25 20:50:55 +01:00
created_at : new Date ( ) . toISOString ( ) ,
2025-02-01 16:32:18 +01:00
follower : follower.getUri ( ) . toString ( ) ,
2024-11-25 20:50:55 +01:00
} ;
await deliveryQueue . add ( DeliveryJobType . FederateEntity , {
entity ,
recipientId : follower.id ,
senderId : this.id ,
} ) ;
2024-11-01 20:42:32 +01:00
}
public async sendFollowReject ( follower : User ) : Promise < void > {
2024-11-25 20:50:55 +01:00
if ( ! follower . isRemote ( ) ) {
throw new Error ( "Follower must be a remote user" ) ;
}
if ( this . isRemote ( ) ) {
throw new Error ( "Followee must be a local user" ) ;
}
const entity : VersiaFollowReject = {
type : "FollowReject" ,
id : crypto.randomUUID ( ) ,
2025-02-01 16:32:18 +01:00
author : this.getUri ( ) . toString ( ) ,
2024-11-25 20:50:55 +01:00
created_at : new Date ( ) . toISOString ( ) ,
2025-02-01 16:32:18 +01:00
follower : follower.getUri ( ) . toString ( ) ,
2024-11-25 20:50:55 +01:00
} ;
await deliveryQueue . add ( DeliveryJobType . FederateEntity , {
entity ,
recipientId : follower.id ,
senderId : this.id ,
} ) ;
2024-11-01 20:42:32 +01:00
}
2024-12-09 15:01:19 +01:00
/ * *
* Perform a WebFinger lookup to find a user ' s URI
* @param manager
* @param username
* @param hostname
* @returns URI , or null if not found
* /
2025-02-01 16:32:18 +01:00
public static async webFinger (
2024-07-17 15:37:36 +02:00
manager : FederationRequester ,
username : string ,
2024-07-26 18:07:11 +02:00
hostname : string ,
2025-02-01 16:32:18 +01:00
) : Promise < URL | null > {
2024-12-09 15:01:19 +01:00
try {
2025-02-01 16:32:18 +01:00
return new URL ( await manager . webFinger ( username , hostname ) ) ;
2024-12-09 15:01:19 +01:00
} catch {
try {
2025-02-01 16:32:18 +01:00
return new URL (
await manager . webFinger (
username ,
hostname ,
"application/activity+json" ,
) ,
2024-12-09 15:01:19 +01:00
) ;
} catch {
return Promise . resolve ( null ) ;
}
}
2024-07-17 15:37:36 +02:00
}
2024-11-01 21:20:12 +01:00
public static getCount ( ) : Promise < number > {
2024-10-11 15:46:05 +02:00
return db . $count ( Users , isNull ( Users . instanceId ) ) ;
2024-05-06 09:16:33 +02:00
}
2024-11-02 00:43:33 +01:00
public static async getActiveInPeriod (
milliseconds : number ,
) : Promise < number > {
2024-05-06 09:16:33 +02:00
return (
await db
. select ( {
count : countDistinct ( Users ) ,
} )
. from ( Users )
. leftJoin ( Notes , eq ( Users . id , Notes . authorId ) )
. where (
and (
isNull ( Users . instanceId ) ,
gte (
Notes . createdAt ,
new Date ( Date . now ( ) - milliseconds ) . toISOString ( ) ,
) ,
) ,
)
) [ 0 ] . count ;
}
2024-11-01 21:20:12 +01:00
public async delete ( ids? : string [ ] ) : Promise < void > {
2024-06-13 02:45:07 +02:00
if ( Array . isArray ( ids ) ) {
await db . delete ( Users ) . where ( inArray ( Users . id , ids ) ) ;
} else {
await db . delete ( Users ) . where ( eq ( Users . id , this . id ) ) ;
}
2024-05-07 09:41:02 +02:00
}
2024-11-02 00:43:33 +01:00
public async resetPassword ( ) : Promise < string > {
2024-06-13 07:38:26 +02:00
const resetToken = randomString ( 32 , "hex" ) ;
2024-05-17 10:27:41 +02:00
await this . update ( {
passwordResetToken : resetToken ,
} ) ;
return resetToken ;
}
2024-11-02 00:43:33 +01:00
public async pin ( note : Note ) : Promise < void > {
await db . insert ( UserToPinnedNotes ) . values ( {
noteId : note.id ,
userId : this.id ,
} ) ;
2024-04-25 05:40:27 +02:00
}
2024-11-02 00:43:33 +01:00
public async unpin ( note : Note ) : Promise < void > {
await db
. delete ( UserToPinnedNotes )
. where (
and (
eq ( NoteToMentions . noteId , note . id ) ,
eq ( NoteToMentions . userId , this . id ) ,
) ,
) ;
2024-04-25 05:40:27 +02:00
}
2024-11-01 21:20:12 +01:00
public save ( ) : Promise < UserWithRelations > {
2024-06-13 02:45:07 +02:00
return this . update ( this . data ) ;
2024-06-06 06:49:06 +02:00
}
2024-04-25 05:40:27 +02:00
2024-10-11 17:03:33 +02:00
public async getLinkedOidcAccounts (
providers : {
id : string ;
name : string ;
url : string ;
2025-03-30 23:44:50 +02:00
icon? : ProxiableUrl ;
2024-10-11 17:03:33 +02:00
} [ ] ,
) : Promise <
2024-09-24 14:42:39 +02:00
{
id : string ;
name : string ;
url : string ;
icon? : string | undefined ;
server_id : string ;
} [ ]
> {
// Get all linked accounts
const accounts = await db . query . OpenIdAccounts . findMany ( {
2024-11-02 00:43:33 +01:00
where : ( User , { eq } ) : SQL | undefined = > eq ( User . userId , this . id ) ,
2024-09-24 14:42:39 +02:00
} ) ;
return accounts
. map ( ( account ) = > {
2024-10-11 17:03:33 +02:00
const issuer = providers . find (
2024-09-24 14:42:39 +02:00
( provider ) = > provider . id === account . issuerId ,
) ;
if ( ! issuer ) {
return null ;
}
return {
id : issuer.id ,
name : issuer.name ,
url : issuer.url ,
2025-03-30 23:44:50 +02:00
icon : issuer.icon?.proxied ,
2024-09-24 14:42:39 +02:00
server_id : account.serverId ,
} ;
} )
. filter ( ( x ) = > x !== null ) ;
}
2024-10-24 17:20:00 +02:00
/ * *
* Like a note .
*
* If the note is already liked , it will return the existing like . Also creates a notification for the author of the note .
* @param note The note to like
2024-10-24 19:08:28 +02:00
* @param uri The URI of the like , if it is remote
2024-10-24 17:20:00 +02:00
* @returns The like object created or the existing like
* /
2024-10-24 19:08:28 +02:00
public async like ( note : Note , uri? : string ) : Promise < Like > {
2024-10-24 17:20:00 +02:00
// Check if the user has already liked the note
const existingLike = await Like . fromSql (
and ( eq ( Likes . likerId , this . id ) , eq ( Likes . likedId , note . id ) ) ,
) ;
if ( existingLike ) {
return existingLike ;
}
const newLike = await Like . insert ( {
2025-03-30 22:10:33 +02:00
id : randomUUIDv7 ( ) ,
2024-10-24 17:20:00 +02:00
likerId : this.id ,
likedId : note.id ,
2024-10-24 19:08:28 +02:00
uri ,
2024-10-24 17:20:00 +02:00
} ) ;
2024-10-24 19:08:28 +02:00
if ( this . isLocal ( ) && note . author . isLocal ( ) ) {
2024-10-24 17:20:00 +02:00
// Notify the user that their post has been favourited
2024-12-09 15:01:19 +01:00
await note . author . notify ( "favourite" , this , note ) ;
2024-10-24 19:08:28 +02:00
} else if ( this . isLocal ( ) && note . author . isRemote ( ) ) {
2024-10-24 17:31:39 +02:00
// Federate the like
this . federateToFollowers ( newLike . toVersia ( ) ) ;
2024-10-24 17:20:00 +02:00
}
return newLike ;
}
/ * *
* Unlike a note .
*
* If the note is not liked , it will return without doing anything . Also removes any notifications for this like .
* @param note The note to unlike
* @returns
* /
public async unlike ( note : Note ) : Promise < void > {
const likeToDelete = await Like . fromSql (
and ( eq ( Likes . likerId , this . id ) , eq ( Likes . likedId , note . id ) ) ,
) ;
if ( ! likeToDelete ) {
return ;
}
await likeToDelete . delete ( ) ;
2024-10-24 19:08:28 +02:00
if ( this . isLocal ( ) && note . author . isLocal ( ) ) {
// Remove any eventual notifications for this like
2024-11-04 10:43:30 +01:00
await likeToDelete . clearRelatedNotifications ( ) ;
2024-10-24 19:08:28 +02:00
} else if ( this . isLocal ( ) && note . author . isRemote ( ) ) {
2024-10-24 17:20:00 +02:00
// User is local, federate the delete
2024-10-24 17:31:39 +02:00
this . federateToFollowers ( likeToDelete . unlikeToVersia ( this ) ) ;
2024-10-24 17:20:00 +02:00
}
}
2024-12-09 15:01:19 +01:00
public async notify (
2024-12-09 13:50:46 +01:00
type : "mention" | "follow_request" | "follow" | "favourite" | "reblog" ,
relatedUser : User ,
note? : Note ,
) : Promise < void > {
2025-01-02 02:45:40 +01:00
const notification = await Notification . insert ( {
2025-03-30 22:10:33 +02:00
id : randomUUIDv7 ( ) ,
2024-12-09 13:50:46 +01:00
accountId : relatedUser.id ,
type ,
notifiedId : this.id ,
noteId : note?.id ? ? null ,
} ) ;
2025-01-02 02:45:40 +01:00
// Also do push notifications
2025-02-15 02:47:29 +01:00
if ( config . notifications . push ) {
2025-01-02 02:45:40 +01:00
await this . notifyPush ( notification . id , type , relatedUser , note ) ;
}
}
private async notifyPush (
notificationId : string ,
type : "mention" | "follow_request" | "follow" | "favourite" | "reblog" ,
relatedUser : User ,
note? : Note ,
) : Promise < void > {
// Fetch all push subscriptions
const ps = await PushSubscription . manyFromUser ( this ) ;
pushQueue . addBulk (
ps . map ( ( p ) = > ( {
data : {
psId : p.id ,
type ,
relatedUserId : relatedUser.id ,
noteId : note?.id ,
notificationId ,
} ,
name : PushJobType.Notify ,
} ) ) ,
) ;
2024-12-09 13:50:46 +01:00
}
2024-11-04 10:43:30 +01:00
public async clearAllNotifications ( ) : Promise < void > {
await db
. update ( Notifications )
. set ( {
dismissed : true ,
} )
. where ( eq ( Notifications . notifiedId , this . id ) ) ;
}
public async clearSomeNotifications ( ids : string [ ] ) : Promise < void > {
await db
. update ( Notifications )
. set ( {
dismissed : true ,
} )
. where (
and (
inArray ( Notifications . id , ids ) ,
eq ( Notifications . notifiedId , this . id ) ,
) ,
) ;
}
2024-11-01 21:20:12 +01:00
public async updateFromRemote ( ) : Promise < User > {
2024-06-06 06:49:06 +02:00
if ( ! this . isRemote ( ) ) {
2024-06-06 06:58:28 +02:00
throw new Error (
"Cannot refetch a local user (they are not remote)" ,
) ;
2024-06-06 06:49:06 +02:00
}
2024-04-25 05:40:27 +02:00
2024-12-09 13:36:15 +01:00
const updated = await User . fetchFromRemote ( this . getUri ( ) ) ;
2024-04-25 05:40:27 +02:00
2024-12-02 15:07:05 +01:00
if ( ! updated ) {
throw new Error ( "Failed to update user from remote" ) ;
}
2024-06-13 02:45:07 +02:00
this . data = updated . data ;
2024-06-06 06:49:06 +02:00
return this ;
}
2025-02-01 16:32:18 +01:00
public static async fetchFromRemote ( uri : URL ) : Promise < User | null > {
2024-06-30 08:58:39 +02:00
const instance = await Instance . resolve ( uri ) ;
2024-12-02 15:07:05 +01:00
if ( ! instance ) {
return null ;
}
2024-08-19 15:16:01 +02:00
if ( instance . data . protocol === "versia" ) {
return await User . saveFromVersia ( uri , instance ) ;
2024-06-30 08:58:39 +02:00
}
if ( instance . data . protocol === "activitypub" ) {
2025-02-15 02:47:29 +01:00
if ( ! config . federation . bridge ) {
2024-07-16 23:30:52 +02:00
throw new Error ( "ActivityPub bridge is not enabled" ) ;
}
2024-07-16 23:29:20 +02:00
const bridgeUri = new URL (
2024-11-22 22:17:25 +01:00
` /apbridge/versia/query? ${ new URLSearchParams ( {
2025-02-01 16:32:18 +01:00
user_url : uri.toString ( ) ,
2024-07-16 23:29:20 +02:00
} ) } ` ,
config . federation . bridge . url ,
) ;
2025-02-01 16:32:18 +01:00
return await User . saveFromVersia ( bridgeUri , instance ) ;
2024-06-30 08:58:39 +02:00
}
throw new Error ( ` Unsupported protocol: ${ instance . data . protocol } ` ) ;
}
2024-08-19 15:16:01 +02:00
private static async saveFromVersia (
2025-02-01 16:32:18 +01:00
uri : URL ,
2024-06-30 08:58:39 +02:00
instance : Instance ,
2024-12-09 13:36:15 +01:00
) : Promise < User > {
2024-08-26 19:27:40 +02:00
const requester = await User . getFederationRequester ( ) ;
2024-12-09 13:36:15 +01:00
const output = await requester . get < Partial < VersiaUser > > ( uri , {
// @ts-expect-error Bun extension
2025-02-15 02:47:29 +01:00
proxy : config.http.proxy_address ,
2024-12-09 13:36:15 +01:00
} ) ;
2024-12-02 15:07:05 +01:00
const { data : json } = output ;
2024-04-25 05:40:27 +02:00
2024-06-06 06:49:06 +02:00
const validator = new EntityValidator ( ) ;
const data = await validator . User ( json ) ;
2024-04-25 05:40:27 +02:00
2024-08-19 15:16:01 +02:00
const user = await User . fromVersia ( data , instance ) ;
2024-06-30 08:58:39 +02:00
2024-12-09 13:36:15 +01:00
await searchManager . addUser ( user ) ;
2024-04-25 05:40:27 +02:00
2024-12-09 13:36:15 +01:00
return user ;
}
2024-04-25 05:40:27 +02:00
2024-12-09 13:36:15 +01:00
/ * *
* Change the emojis linked to this user in database
* @param emojis
* @returns
* /
public async updateEmojis ( emojis : Emoji [ ] ) : Promise < void > {
if ( emojis . length === 0 ) {
return ;
2024-06-13 04:26:43 +02:00
}
2024-04-25 05:40:27 +02:00
2024-12-09 13:36:15 +01:00
await db . delete ( EmojiToUser ) . where ( eq ( EmojiToUser . userId , this . id ) ) ;
await db . insert ( EmojiToUser ) . values (
emojis . map ( ( emoji ) = > ( {
emojiId : emoji.id ,
userId : this.id ,
} ) ) ,
) ;
2024-04-25 05:40:27 +02:00
}
2024-11-01 21:20:12 +01:00
public static async fromVersia (
2024-08-19 15:16:01 +02:00
user : VersiaUser ,
2024-06-30 08:58:39 +02:00
instance : Instance ,
2024-06-13 04:26:43 +02:00
) : Promise < User > {
const data = {
username : user.username ,
uri : user.uri ,
createdAt : new Date ( user . created_at ) . toISOString ( ) ,
endpoints : {
2024-08-26 19:06:49 +02:00
dislikes :
user . collections [ "pub.versia:likes/Dislikes" ] ? ? undefined ,
featured : user.collections.featured ,
likes : user.collections [ "pub.versia:likes/Likes" ] ? ? undefined ,
followers : user.collections.followers ,
following : user.collections.following ,
2024-06-13 04:26:43 +02:00
inbox : user.inbox ,
2024-08-26 19:06:49 +02:00
outbox : user.collections.outbox ,
2024-06-13 04:26:43 +02:00
} ,
fields : user.fields ? ? [ ] ,
updatedAt : new Date ( user . created_at ) . toISOString ( ) ,
instanceId : instance.id ,
displayName : user.display_name ? ? "" ,
note : getBestContentType ( user . bio ) . content ,
2024-08-26 19:06:49 +02:00
publicKey : user.public_key.key ,
2024-06-13 04:26:43 +02:00
source : {
2025-02-11 18:22:39 +01:00
language : "en" ,
2024-06-13 04:26:43 +02:00
note : "" ,
privacy : "public" ,
sensitive : false ,
fields : [ ] ,
2025-02-11 18:22:39 +01:00
} as z . infer < typeof Source > ,
2024-06-13 04:26:43 +02:00
} ;
2024-12-09 13:36:15 +01:00
const userEmojis =
user . extensions ? . [ "pub.versia:custom_emojis" ] ? . emojis ? ? [ ] ;
2024-06-13 04:26:43 +02:00
2024-12-09 13:36:15 +01:00
const emojis = await Promise . all (
userEmojis . map ( ( emoji ) = > Emoji . fromVersia ( emoji , instance ) ) ,
) ;
// Check if new user already exists
2024-06-13 04:26:43 +02:00
const foundUser = await User . fromSql ( eq ( Users . uri , user . uri ) ) ;
// If it exists, simply update it
if ( foundUser ) {
2025-01-28 19:07:55 +01:00
let avatar : Media | null = null ;
let header : Media | null = null ;
if ( user . avatar ) {
if ( foundUser . avatar ) {
avatar = new Media (
await foundUser . avatar . update ( {
content : user.avatar ,
} ) ,
) ;
} else {
avatar = await Media . insert ( {
2025-03-30 22:10:33 +02:00
id : randomUUIDv7 ( ) ,
2025-01-28 19:07:55 +01:00
content : user.avatar ,
} ) ;
}
}
if ( user . header ) {
if ( foundUser . header ) {
header = new Media (
await foundUser . header . update ( {
content : user.header ,
} ) ,
) ;
} else {
header = await Media . insert ( {
2025-03-30 22:10:33 +02:00
id : randomUUIDv7 ( ) ,
2025-01-28 19:07:55 +01:00
content : user.header ,
} ) ;
}
}
await foundUser . update ( {
. . . data ,
avatarId : avatar?.id ,
headerId : header?.id ,
} ) ;
2024-12-09 13:36:15 +01:00
await foundUser . updateEmojis ( emojis ) ;
2024-06-13 04:26:43 +02:00
return foundUser ;
}
// Else, create a new user
2025-01-28 19:07:55 +01:00
const avatar = user . avatar
? await Media . insert ( {
2025-03-30 22:10:33 +02:00
id : randomUUIDv7 ( ) ,
2025-01-28 19:07:55 +01:00
content : user.avatar ,
} )
: null ;
const header = user . header
? await Media . insert ( {
2025-03-30 22:10:33 +02:00
id : randomUUIDv7 ( ) ,
2025-01-28 19:07:55 +01:00
content : user.header ,
} )
: null ;
const newUser = await User . insert ( {
2025-03-30 22:10:33 +02:00
id : randomUUIDv7 ( ) ,
2025-01-28 19:07:55 +01:00
. . . data ,
avatarId : avatar?.id ,
headerId : header?.id ,
} ) ;
2024-12-09 13:36:15 +01:00
await newUser . updateEmojis ( emojis ) ;
return newUser ;
2024-06-13 04:26:43 +02:00
}
public static async insert (
data : InferInsertModel < typeof Users > ,
) : Promise < User > {
const inserted = ( await db . insert ( Users ) . values ( data ) . returning ( ) ) [ 0 ] ;
const user = await User . fromId ( inserted . id ) ;
if ( ! user ) {
throw new Error ( "Failed to insert user" ) ;
}
return user ;
}
2025-02-01 16:32:18 +01:00
public static async resolve ( uri : URL ) : Promise < User | null > {
2024-11-24 23:13:29 +01:00
getLogger ( [ "federation" , "resolvers" ] )
. debug ` Resolving user ${ chalk . gray ( uri ) } ` ;
2024-06-06 06:49:06 +02:00
// Check if user not already in database
2025-02-01 16:32:18 +01:00
const foundUser = await User . fromSql ( eq ( Users . uri , uri . toString ( ) ) ) ;
2024-06-06 06:49:06 +02:00
2024-06-13 04:26:43 +02:00
if ( foundUser ) {
return foundUser ;
}
2024-06-06 06:49:06 +02:00
// Check if URI is of a local user
2025-02-01 16:32:18 +01:00
if ( uri . origin === config . http . base_url . origin ) {
const uuid = uri . href . match ( idValidator ) ;
2024-06-06 06:49:06 +02:00
2024-06-13 04:26:43 +02:00
if ( ! uuid ? . [ 0 ] ) {
2024-06-06 06:49:06 +02:00
throw new Error (
` URI ${ uri } is of a local user, but it could not be parsed ` ,
) ;
}
return await User . fromId ( uuid [ 0 ] ) ;
}
2024-11-24 23:13:29 +01:00
getLogger ( [ "federation" , "resolvers" ] )
2024-11-24 22:01:14 +01:00
. debug ` User not found in database, fetching from remote ` ;
2024-12-09 13:36:15 +01:00
return await User . fetchFromRemote ( uri ) ;
2024-06-06 06:49:06 +02:00
}
2024-04-25 05:40:27 +02:00
/ * *
* Get the user ' s avatar in raw URL format
* @returns The raw URL for the user ' s avatar
* /
2025-03-30 23:44:50 +02:00
public getAvatarUrl ( ) : ProxiableUrl {
2025-01-28 19:07:55 +01:00
if ( ! this . avatar ) {
2024-04-25 05:40:27 +02:00
return (
config . defaults . avatar ||
2025-03-30 23:44:50 +02:00
new ProxiableUrl (
2025-02-01 16:32:18 +01:00
` https://api.dicebear.com/8.x/ ${ config . defaults . placeholder_style } /svg?seed= ${ this . data . username } ` ,
)
2024-04-25 05:40:27 +02:00
) ;
2024-06-13 04:26:43 +02:00
}
2025-01-28 19:07:55 +01:00
return this . avatar ? . getUrl ( ) ;
2024-04-25 05:40:27 +02:00
}
2024-11-02 00:43:33 +01:00
public static async generateKeys ( ) : Promise < {
private_key : string ;
public_key : string ;
} > {
2024-04-25 05:48:39 +02:00
const keys = await crypto . subtle . generateKey ( "Ed25519" , true , [
"sign" ,
"verify" ,
] ) ;
const privateKey = Buffer . from (
await crypto . subtle . exportKey ( "pkcs8" , keys . privateKey ) ,
) . toString ( "base64" ) ;
const publicKey = Buffer . from (
await crypto . subtle . exportKey ( "spki" , keys . publicKey ) ,
) . toString ( "base64" ) ;
// Add header, footer and newlines later on
// These keys are base64 encrypted
return {
private_key : privateKey ,
public_key : publicKey ,
} ;
}
2024-11-01 21:20:12 +01:00
public static async fromDataLocal ( data : {
2024-04-25 05:48:39 +02:00
username : string ;
display_name? : string ;
2024-05-08 02:10:14 +02:00
password : string | undefined ;
email : string | undefined ;
2024-04-25 05:48:39 +02:00
bio? : string ;
2025-01-28 19:07:55 +01:00
avatar? : Media ;
header? : Media ;
2024-04-25 05:48:39 +02:00
admin? : boolean ;
skipPasswordHash? : boolean ;
2024-06-14 11:05:04 +02:00
} ) : Promise < User > {
2024-04-25 05:48:39 +02:00
const keys = await User . generateKeys ( ) ;
const newUser = (
await db
. insert ( Users )
. values ( {
2025-03-30 22:10:33 +02:00
id : randomUUIDv7 ( ) ,
2024-04-25 05:48:39 +02:00
username : data.username ,
displayName : data.display_name ? ? data . username ,
2024-05-08 02:10:14 +02:00
password :
data . skipPasswordHash || ! data . password
? data . password
2025-03-30 23:06:34 +02:00
: await bunPassword . hash ( data . password ) ,
2024-04-25 05:48:39 +02:00
email : data.email ,
note : data.bio ? ? "" ,
2025-01-28 19:07:55 +01:00
avatarId : data.avatar?.id ,
headerId : data.header?.id ,
2024-04-25 05:48:39 +02:00
isAdmin : data.admin ? ? false ,
publicKey : keys.public_key ,
2024-04-25 06:37:55 +02:00
fields : [ ] ,
2024-04-25 05:48:39 +02:00
privateKey : keys.private_key ,
updatedAt : new Date ( ) . toISOString ( ) ,
source : {
2025-02-11 18:22:39 +01:00
language : "en" ,
2024-04-25 05:48:39 +02:00
note : "" ,
privacy : "public" ,
sensitive : false ,
fields : [ ] ,
2025-02-11 18:22:39 +01:00
} as z . infer < typeof Source > ,
2024-04-25 05:48:39 +02:00
} )
. returning ( )
) [ 0 ] ;
const finalUser = await User . fromId ( newUser . id ) ;
2024-06-13 04:26:43 +02:00
if ( ! finalUser ) {
2024-06-14 11:05:04 +02:00
throw new Error ( "Failed to create user" ) ;
2024-06-13 04:26:43 +02:00
}
2024-04-25 05:48:39 +02:00
2024-06-29 11:40:44 +02:00
// Add to search index
await searchManager . addUser ( finalUser ) ;
2024-04-25 05:48:39 +02:00
return finalUser ;
}
2024-04-25 05:40:27 +02:00
/ * *
* Get the user ' s header in raw URL format
* @returns The raw URL for the user ' s header
* /
2025-03-30 23:44:50 +02:00
public getHeaderUrl ( ) : ProxiableUrl | null {
2025-01-28 19:07:55 +01:00
if ( ! this . header ) {
2025-02-01 16:32:18 +01:00
return config . defaults . header ? ? null ;
2024-06-13 04:26:43 +02:00
}
2025-03-30 23:44:50 +02:00
2025-01-28 19:07:55 +01:00
return this . header . getUrl ( ) ;
2024-04-25 05:40:27 +02:00
}
2024-11-02 00:43:33 +01:00
public getAcct ( ) : string {
2024-04-25 05:40:27 +02:00
return this . isLocal ( )
2024-06-13 02:45:07 +02:00
? this . data . username
: ` ${ this . data . username } @ ${ this . data . instance ? . baseUrl } ` ;
2024-04-25 05:40:27 +02:00
}
2024-11-01 21:20:12 +01:00
public static getAcct (
isLocal : boolean ,
username : string ,
baseUrl? : string ,
2024-11-02 00:43:33 +01:00
) : string {
2024-04-25 05:40:27 +02:00
return isLocal ? username : ` ${ username } @ ${ baseUrl } ` ;
}
2024-11-01 21:20:12 +01:00
public async update (
2024-06-13 02:45:07 +02:00
newUser : Partial < UserWithRelations > ,
) : Promise < UserWithRelations > {
await db . update ( Users ) . set ( newUser ) . where ( eq ( Users . id , this . id ) ) ;
2024-05-12 03:27:28 +02:00
2024-06-13 02:45:07 +02:00
const updated = await User . fromId ( this . data . id ) ;
2024-05-12 03:27:28 +02:00
2024-06-13 02:45:07 +02:00
if ( ! updated ) {
throw new Error ( "Failed to update user" ) ;
}
2024-05-12 03:27:28 +02:00
2024-06-06 06:58:28 +02:00
// If something important is updated, federate it
if (
2024-06-30 10:55:50 +02:00
this . isLocal ( ) &&
( newUser . username ||
newUser . displayName ||
newUser . note ||
newUser . avatar ||
newUser . header ||
newUser . fields ||
newUser . publicKey ||
newUser . isAdmin ||
newUser . isBot ||
newUser . isLocked ||
newUser . endpoints ||
2025-03-30 23:44:50 +02:00
newUser . isDiscoverable ||
newUser . isIndexable )
2024-06-06 06:58:28 +02:00
) {
2024-08-19 15:16:01 +02:00
await this . federateToFollowers ( this . toVersia ( ) ) ;
2024-06-06 06:58:28 +02:00
}
2024-06-13 02:45:07 +02:00
return updated . data ;
2024-05-12 03:27:28 +02:00
}
2024-06-06 07:25:49 +02:00
2024-07-26 18:51:39 +02:00
/ * *
2024-08-19 15:16:01 +02:00
* Signs a Versia entity with that user ' s private key
2024-07-26 18:51:39 +02:00
*
* @param entity Entity to sign
* @param signatureUrl URL to embed in signature ( must be the same URI of queries made with this signature )
* @param signatureMethod HTTP method to embed in signature ( default : POST )
* @returns The signed string and headers to send with the request
* /
2024-11-01 21:20:12 +01:00
public async sign (
2024-08-26 19:40:15 +02:00
entity : KnownEntity | Collection ,
2025-02-01 16:32:18 +01:00
signatureUrl : URL ,
2024-07-26 18:51:39 +02:00
signatureMethod : HttpVerb = "POST" ,
) : Promise < {
headers : Headers ;
signedString : string ;
} > {
const signatureConstructor = await SignatureConstructor . fromStringKey (
this . data . privateKey ? ? "" ,
this . getUri ( ) ,
) ;
const output = await signatureConstructor . sign (
signatureMethod ,
2025-02-01 16:32:18 +01:00
signatureUrl ,
2024-07-26 18:51:39 +02:00
JSON . stringify ( entity ) ,
) ;
2025-02-15 02:47:29 +01:00
if ( config . debug ? . federation ) {
2024-07-26 18:51:39 +02:00
const logger = getLogger ( "federation" ) ;
// Log public key
logger . debug ` Sender public key: ${ this . data . publicKey } ` ;
// Log signed string
logger . debug ` Signed string: \ n ${ output . signedString } ` ;
}
return output ;
}
2024-08-26 19:27:40 +02:00
/ * *
* Helper to get the appropriate Versia SDK requester with the instance ' s private key
*
* @returns The requester
* /
2025-02-15 02:47:29 +01:00
public static getFederationRequester ( ) : FederationRequester {
const signatureConstructor = new SignatureConstructor (
2024-08-26 19:27:40 +02:00
config . instance . keys . private ,
config . http . base_url ,
) ;
return new FederationRequester ( signatureConstructor ) ;
}
2024-07-26 18:51:39 +02:00
/ * *
2024-08-19 15:16:01 +02:00
* Helper to get the appropriate Versia SDK requester with this user ' s private key
2024-07-26 18:51:39 +02:00
*
* @returns The requester
* /
2024-11-01 21:20:12 +01:00
public async getFederationRequester ( ) : Promise < FederationRequester > {
2024-07-26 18:51:39 +02:00
const signatureConstructor = await SignatureConstructor . fromStringKey (
this . data . privateKey ? ? "" ,
this . getUri ( ) ,
) ;
return new FederationRequester ( signatureConstructor ) ;
}
/ * *
2025-03-30 23:44:50 +02:00
* Get all remote followers of the user
* @returns The remote followers
2024-07-26 18:51:39 +02:00
* /
2025-03-30 23:44:50 +02:00
private getRemoteFollowers ( ) : Promise < User [ ] > {
return User . manyFromSql (
2024-06-06 07:25:49 +02:00
and (
sql ` EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${ this . id } AND "Relationships"."ownerId" = ${ Users . id } AND "Relationships"."following" = true) ` ,
isNotNull ( Users . instanceId ) ,
) ,
) ;
2025-03-30 23:44:50 +02:00
}
/ * *
* Federates an entity to all followers of the user
*
* @param entity Entity to federate
* /
public async federateToFollowers ( entity : KnownEntity ) : Promise < void > {
// Get followers
const followers = await this . getRemoteFollowers ( ) ;
2024-06-06 07:25:49 +02:00
2024-11-25 20:50:55 +01:00
await deliveryQueue . addBulk (
followers . map ( ( follower ) = > ( {
name : DeliveryJobType.FederateEntity ,
data : {
entity ,
recipientId : follower.id ,
senderId : this.id ,
} ,
} ) ) ,
) ;
2024-07-26 18:51:39 +02:00
}
/ * *
* Federates an entity to any user .
*
* @param entity Entity to federate
* @param user User to federate to
* @returns Whether the federation was successful
* /
2024-11-01 21:20:12 +01:00
public async federateToUser (
2024-08-26 19:06:49 +02:00
entity : KnownEntity ,
user : User ,
) : Promise < { ok : boolean } > {
2024-11-25 20:50:55 +01:00
const inbox = user . data . instance ? . inbox || user . data . endpoints ? . inbox ;
if ( ! inbox ) {
throw new Error (
` User ${ chalk . gray ( user . getUri ( ) ) } does not have an inbox endpoint ` ,
) ;
}
2025-02-01 16:32:18 +01:00
const { headers } = await this . sign ( entity , new URL ( inbox ) ) ;
2024-07-26 18:51:39 +02:00
try {
2024-11-25 20:50:55 +01:00
await new FederationRequester ( ) . post ( inbox , entity , {
// @ts-expect-error Bun extension
2025-02-15 02:47:29 +01:00
proxy : config.http.proxy_address ,
2024-11-25 20:50:55 +01:00
headers : {
. . . headers . toJSON ( ) ,
"Content-Type" : "application/json; charset=utf-8" ,
2024-07-26 18:51:39 +02:00
} ,
2024-11-25 20:50:55 +01:00
} ) ;
2024-07-26 18:51:39 +02:00
} catch ( e ) {
2024-11-25 11:29:48 +01:00
getLogger ( [ "federation" , "delivery" ] )
2024-07-26 18:51:39 +02:00
. error ` Federating ${ chalk . gray ( entity . type ) } to ${ user . getUri ( ) } ${ chalk . bold . red ( "failed" ) } ` ;
2024-11-25 11:29:48 +01:00
getLogger ( [ "federation" , "delivery" ] ) . error ` ${ e } ` ;
2024-07-26 18:51:39 +02:00
sentry ? . captureException ( e ) ;
2024-06-06 07:25:49 +02:00
2024-07-26 18:51:39 +02:00
return { ok : false } ;
2024-06-06 07:25:49 +02:00
}
2024-07-26 18:51:39 +02:00
return { ok : true } ;
2024-06-06 07:25:49 +02:00
}
2024-05-12 03:27:28 +02:00
2025-02-05 21:49:39 +01:00
public toApi ( isOwnAccount = false ) : z . infer < typeof Account > {
2024-06-13 02:45:07 +02:00
const user = this . data ;
2025-02-11 18:22:39 +01:00
2024-04-25 05:40:27 +02:00
return {
id : user.id ,
username : user.username ,
display_name : user.displayName ,
note : user.note ,
2025-02-01 16:32:18 +01:00
uri : this.getUri ( ) . toString ( ) ,
2024-04-25 05:40:27 +02:00
url :
user . uri ||
new URL ( ` /@ ${ user . username } ` , config . http . base_url ) . toString ( ) ,
2025-03-30 23:44:50 +02:00
avatar : this.getAvatarUrl ( ) . proxied ,
header : this.getHeaderUrl ( ) ? . proxied ? ? "" ,
2024-04-25 05:40:27 +02:00
locked : user.isLocked ,
created_at : new Date ( user . createdAt ) . toISOString ( ) ,
2025-03-30 20:32:42 +02:00
followers_count :
user . isHidingCollections && ! isOwnAccount
? 0
: user . followerCount ,
following_count :
user . isHidingCollections && ! isOwnAccount
? 0
: user . followingCount ,
2024-04-25 05:40:27 +02:00
statuses_count : user.statusCount ,
2024-06-13 06:52:01 +02:00
emojis : user.emojis.map ( ( emoji ) = > new Emoji ( emoji ) . toApi ( ) ) ,
2024-04-25 06:37:55 +02:00
fields : user.fields.map ( ( field ) = > ( {
name : htmlToText ( getBestContentType ( field . key ) . content ) ,
value : getBestContentType ( field . value ) . content ,
2025-02-05 21:49:39 +01:00
verified_at : null ,
2024-04-25 06:37:55 +02:00
} ) ) ,
2024-04-25 05:40:27 +02:00
bot : user.isBot ,
source : isOwnAccount ? user.source : undefined ,
// TODO: Add static avatar and header
2025-03-30 23:44:50 +02:00
avatar_static : this.getAvatarUrl ( ) . proxied ,
header_static : this.getHeaderUrl ( ) ? . proxied ? ? "" ,
2024-04-25 05:40:27 +02:00
acct : this.getAcct ( ) ,
// TODO: Add these fields
limited : false ,
moved : null ,
2025-03-30 20:32:42 +02:00
noindex : ! user . isIndexable ,
2024-04-25 05:40:27 +02:00
suspended : false ,
2025-03-23 03:34:17 +01:00
discoverable : user.isDiscoverable ,
2025-02-05 21:49:39 +01:00
mute_expires_at : null ,
2024-06-12 02:29:59 +02:00
roles : user.roles
2024-06-13 03:03:57 +02:00
. map ( ( role ) = > new Role ( role ) )
2025-03-30 23:44:50 +02:00
. concat ( Role . defaultRole )
. concat ( user . isAdmin ? Role . adminRole : [ ] )
2024-06-13 04:26:43 +02:00
. map ( ( r ) = > r . toApi ( ) ) ,
2024-04-25 05:40:27 +02:00
group : false ,
2025-02-11 18:22:39 +01:00
// TODO
last_status_at : null ,
2024-04-25 05:40:27 +02:00
} ;
}
2024-11-01 21:20:12 +01:00
public toVersia ( ) : VersiaUser {
2024-04-25 05:40:27 +02:00
if ( this . isRemote ( ) ) {
2024-08-19 15:16:01 +02:00
throw new Error ( "Cannot convert remote user to Versia format" ) ;
2024-04-25 05:40:27 +02:00
}
2024-06-13 02:45:07 +02:00
const user = this . data ;
2024-04-25 05:40:27 +02:00
return {
id : user.id ,
type : "User" ,
2025-02-01 16:32:18 +01:00
uri : this.getUri ( ) . toString ( ) ,
2024-04-25 05:40:27 +02:00
bio : {
"text/html" : {
content : user.note ,
2024-08-26 19:06:49 +02:00
remote : false ,
2024-04-25 05:40:27 +02:00
} ,
"text/plain" : {
content : htmlToText ( user . note ) ,
2024-08-26 19:06:49 +02:00
remote : false ,
2024-04-25 05:40:27 +02:00
} ,
} ,
created_at : new Date ( user . createdAt ) . toISOString ( ) ,
2024-08-26 19:06:49 +02:00
collections : {
featured : new URL (
` /users/ ${ user . id } /featured ` ,
config . http . base_url ,
) . toString ( ) ,
"pub.versia:likes/Likes" : new URL (
` /users/ ${ user . id } /likes ` ,
config . http . base_url ,
) . toString ( ) ,
"pub.versia:likes/Dislikes" : new URL (
` /users/ ${ user . id } /dislikes ` ,
config . http . base_url ,
) . toString ( ) ,
followers : new URL (
` /users/ ${ user . id } /followers ` ,
config . http . base_url ,
) . toString ( ) ,
following : new URL (
` /users/ ${ user . id } /following ` ,
config . http . base_url ,
) . toString ( ) ,
outbox : new URL (
` /users/ ${ user . id } /outbox ` ,
config . http . base_url ,
) . toString ( ) ,
} ,
2024-04-25 05:40:27 +02:00
inbox : new URL (
` /users/ ${ user . id } /inbox ` ,
config . http . base_url ,
) . toString ( ) ,
2025-03-30 20:32:42 +02:00
indexable : this.data.isIndexable ,
2024-04-25 05:40:27 +02:00
username : user.username ,
2024-08-26 19:27:40 +02:00
manually_approves_followers : this.data.isLocked ,
2025-02-11 18:22:39 +01:00
avatar : this.avatar?.toVersia ( ) ,
header : this.header?.toVersia ( ) ,
2024-04-25 05:40:27 +02:00
display_name : user.displayName ,
2024-05-17 19:39:59 +02:00
fields : user.fields ,
2024-04-25 05:40:27 +02:00
public_key : {
actor : new URL (
` /users/ ${ user . id } ` ,
config . http . base_url ,
) . toString ( ) ,
2024-08-26 19:06:49 +02:00
key : user.publicKey ,
algorithm : "ed25519" ,
2024-04-25 05:40:27 +02:00
} ,
extensions : {
2024-08-26 19:27:40 +02:00
"pub.versia:custom_emojis" : {
2024-06-13 06:52:01 +02:00
emojis : user.emojis.map ( ( emoji ) = >
2024-08-19 15:16:01 +02:00
new Emoji ( emoji ) . toVersia ( ) ,
2024-06-13 06:52:01 +02:00
) ,
2024-04-25 05:40:27 +02:00
} ,
} ,
} ;
}
2025-02-12 23:33:07 +01:00
public toMention ( ) : z . infer < typeof MentionSchema > {
2024-04-25 05:40:27 +02:00
return {
2025-02-01 16:32:18 +01:00
url : this.getUri ( ) . toString ( ) ,
2024-06-13 02:45:07 +02:00
username : this.data.username ,
2024-04-25 05:40:27 +02:00
acct : this.getAcct ( ) ,
id : this.id ,
} ;
}
}