2025-03-22 02:34:03 +01:00
import type {
Account ,
Mention as MentionSchema ,
2025-05-01 16:27:34 +02:00
RolePermission ,
2025-03-22 02:34:03 +01:00
Source ,
2025-03-22 18:04:47 +01:00
} from "@versia/client/schemas" ;
2025-06-15 23:50:34 +02:00
import { sign } from "@versia/sdk/crypto" ;
import * as VersiaEntities from "@versia/sdk/entities" ;
import { FederationRequester } from "@versia/sdk/http" ;
import type { ImageContentFormatSchema } from "@versia/sdk/schemas" ;
import { config , ProxiableUrl } from "@versia-server/config" ;
2025-06-22 18:43:03 +02:00
import {
federationDeliveryLogger ,
federationResolversLogger ,
} from "@versia-server/logging" ;
2025-05-01 16:27:34 +02:00
import { password as bunPassword , randomUUIDv7 } from "bun" ;
2024-07-26 18:51:39 +02:00
import chalk from "chalk" ;
2024-05-06 09:16:33 +02:00
import {
and ,
countDistinct ,
desc ,
eq ,
gte ,
2025-04-10 19:15:31 +02:00
type InferInsertModel ,
type InferSelectModel ,
2024-05-06 09:16:33 +02:00
inArray ,
2024-06-06 07:12:23 +02:00
isNotNull ,
2024-05-06 09:16:33 +02:00
isNull ,
2025-04-10 19:15:31 +02:00
type SQL ,
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-07-07 03:42:35 +02:00
import type { z } from "zod/v4" ;
2025-04-10 19:15:31 +02:00
import { getBestContentType } from "@/content_types" ;
import { randomString } from "@/math" ;
2025-04-08 16:01:10 +02:00
import type { HttpVerb , KnownEntity } from "~/types/api.ts" ;
2025-07-04 06:29:43 +02:00
import { DeliveryJobType , deliveryQueue } from "../queues/delivery/queue.ts" ;
import { PushJobType , pushQueue } from "../queues/push/queue.ts" ;
import { uuid } from "../regex.ts" ;
import { db } from "../tables/db.ts" ;
import {
EmojiToUser ,
Notes ,
NoteToMentions ,
Notifications ,
Relationships ,
Users ,
UserToPinnedNotes ,
} from "../tables/schema.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" ;
2025-07-04 06:29:43 +02:00
import { Media } from "./media.ts" ;
import type { Note } from "./note.ts" ;
import { PushSubscription } from "./pushsubscription.ts" ;
2024-11-04 15:20:53 +01:00
import { Relationship } from "./relationship.ts" ;
import { Role } from "./role.ts" ;
2024-04-25 05:40:27 +02:00
2025-06-15 23:43:27 +02:00
export const userRelations = {
instance : true ,
emojis : {
with : {
emoji : {
with : {
instance : true ,
media : true ,
} ,
} ,
} ,
} ,
avatar : true ,
header : true ,
roles : {
with : {
role : true ,
} ,
} ,
} as const ;
2025-08-21 00:45:58 +02:00
// TODO: Remove this function and use what drizzle outputs directly instead of transforming it
2025-06-15 23:43:27 +02:00
export const transformOutputToUserWithRelations = (
user : Omit < InferSelectModel < typeof Users > , "endpoints" > & {
followerCount : unknown ;
followingCount : unknown ;
statusCount : unknown ;
avatar : typeof Media . $type | null ;
header : typeof Media . $type | null ;
emojis : {
userId : string ;
emojiId : string ;
emoji? : typeof Emoji . $type ;
} [ ] ;
instance : typeof Instance . $type | null ;
roles : {
userId : string ;
roleId : string ;
role? : typeof Role . $type ;
} [ ] ;
endpoints : unknown ;
} ,
) : typeof User . $type = > {
return {
. . . user ,
followerCount : Number ( user . followerCount ) ,
followingCount : Number ( user . followingCount ) ,
statusCount : Number ( user . statusCount ) ,
endpoints :
user . endpoints ? ?
( { } as Partial < {
dislikes : string ;
featured : string ;
likes : string ;
followers : string ;
following : string ;
inbox : string ;
outbox : string ;
} > ) ,
emojis : user.emojis.map (
( emoji ) = >
( emoji as unknown as Record < string , object > )
. emoji as typeof Emoji . $type ,
) ,
roles : user.roles
. map ( ( role ) = > role . role )
. filter ( Boolean ) as ( typeof Role . $type ) [ ] ,
} ;
} ;
const findManyUsers = async (
query : Parameters < typeof db.query.Users.findMany > [ 0 ] ,
) : Promise < ( typeof User . $type ) [ ] > = > {
const output = await db . query . Users . findMany ( {
. . . query ,
with : {
. . . userRelations ,
. . . query ? . with ,
} ,
} ) ;
return output . map ( ( user ) = > transformOutputToUserWithRelations ( user ) ) ;
} ;
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
}
2025-04-08 18:13:30 +02:00
public get local ( ) : boolean {
2024-06-13 02:45:07 +02:00
return this . data . instanceId === null ;
2024-04-25 05:40:27 +02:00
}
2025-04-08 18:13:30 +02:00
public get remote ( ) : boolean {
return ! this . local ;
2024-04-25 05:40:27 +02:00
}
2025-04-08 17:27:08 +02:00
public get uri ( ) : URL {
2025-02-01 16:32:18 +01:00
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 ( {
2025-04-08 18:13:30 +02:00
following : otherUser.remote ? false : ! otherUser . data . isLocked ,
requested : otherUser.remote ? true : otherUser . data . isLocked ,
2024-07-27 20:46:19 +02:00
showingReblogs : options?.reblogs ,
notifying : options?.notify ,
languages : options?.languages ,
} ) ;
2025-05-04 16:38:37 +02:00
if ( ! otherUser . data . isLocked ) {
// Update the follower count
await otherUser . recalculateFollowerCount ( ) ;
await this . recalculateFollowingCount ( ) ;
}
2025-04-08 18:13:30 +02:00
if ( otherUser . remote ) {
2024-11-25 20:50:55 +01:00
await deliveryQueue . add ( DeliveryJobType . FederateEntity , {
entity : {
type : "Follow" ,
id : crypto.randomUUID ( ) ,
2025-04-08 17:27:08 +02:00
author : this.uri.href ,
followee : otherUser.uri.href ,
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 > {
2025-04-08 18:13:30 +02:00
if ( followee . remote ) {
2024-11-25 20:50:55 +01:00
await deliveryQueue . add ( DeliveryJobType . FederateEntity , {
2025-04-08 16:01:10 +02:00
entity : this.unfollowToVersia ( followee ) . toJSON ( ) ,
2024-11-25 20:50:55 +01:00
recipientId : followee.id ,
senderId : this.id ,
} ) ;
2024-08-02 17:28:50 +02:00
}
2025-05-04 16:38:37 +02:00
await this . recalculateFollowingCount ( ) ;
await followee . recalculateFollowerCount ( ) ;
2024-08-02 17:28:50 +02:00
await relationship . update ( {
following : false ,
} ) ;
}
2025-04-08 16:01:10 +02:00
private unfollowToVersia ( followee : User ) : VersiaEntities . Unfollow {
2024-08-27 15:50:14 +02:00
const id = crypto . randomUUID ( ) ;
2025-04-08 16:01:10 +02:00
return new VersiaEntities . Unfollow ( {
2024-08-27 15:50:14 +02:00
type : "Unfollow" ,
id ,
2025-04-16 16:35:17 +02:00
author : this.uri.href ,
2024-08-27 15:50:14 +02:00
created_at : new Date ( ) . toISOString ( ) ,
2025-04-16 16:35:17 +02:00
followee : followee.uri.href ,
2025-04-08 16:01:10 +02:00
} ) ;
2024-08-27 15:50:14 +02:00
}
2025-04-08 17:27:08 +02:00
public async acceptFollowRequest ( follower : User ) : Promise < void > {
2025-04-08 18:13:30 +02:00
if ( ! follower . remote ) {
2024-11-25 20:50:55 +01:00
throw new Error ( "Follower must be a remote user" ) ;
}
2025-04-08 18:13:30 +02:00
if ( this . remote ) {
2024-11-25 20:50:55 +01:00
throw new Error ( "Followee must be a local user" ) ;
}
2025-05-04 16:38:37 +02:00
await follower . recalculateFollowerCount ( ) ;
await this . recalculateFollowingCount ( ) ;
2025-04-08 16:01:10 +02:00
const entity = new VersiaEntities . FollowAccept ( {
2024-11-25 20:50:55 +01:00
type : "FollowAccept" ,
id : crypto.randomUUID ( ) ,
2025-04-16 16:35:17 +02:00
author : this.uri.href ,
2024-11-25 20:50:55 +01:00
created_at : new Date ( ) . toISOString ( ) ,
2025-04-16 16:35:17 +02:00
follower : follower.uri.href ,
2025-04-08 16:01:10 +02:00
} ) ;
2024-11-25 20:50:55 +01:00
await deliveryQueue . add ( DeliveryJobType . FederateEntity , {
2025-04-08 16:01:10 +02:00
entity : entity.toJSON ( ) ,
2024-11-25 20:50:55 +01:00
recipientId : follower.id ,
senderId : this.id ,
} ) ;
2024-11-01 20:42:32 +01:00
}
2025-04-08 17:27:08 +02:00
public async rejectFollowRequest ( follower : User ) : Promise < void > {
2025-04-08 18:13:30 +02:00
if ( ! follower . remote ) {
2024-11-25 20:50:55 +01:00
throw new Error ( "Follower must be a remote user" ) ;
}
2025-04-08 18:13:30 +02:00
if ( this . remote ) {
2024-11-25 20:50:55 +01:00
throw new Error ( "Followee must be a local user" ) ;
}
2025-04-08 16:01:10 +02:00
const entity = new VersiaEntities . FollowReject ( {
2024-11-25 20:50:55 +01:00
type : "FollowReject" ,
id : crypto.randomUUID ( ) ,
2025-04-16 16:35:17 +02:00
author : this.uri.href ,
2024-11-25 20:50:55 +01:00
created_at : new Date ( ) . toISOString ( ) ,
2025-04-16 16:35:17 +02:00
follower : follower.uri.href ,
2025-04-08 16:01:10 +02:00
} ) ;
2024-11-25 20:50:55 +01:00
await deliveryQueue . add ( DeliveryJobType . FederateEntity , {
2025-04-08 16:01:10 +02:00
entity : entity.toJSON ( ) ,
2024-11-25 20:50:55 +01:00
recipientId : follower.id ,
senderId : this.id ,
} ) ;
2024-11-01 20:42:32 +01:00
}
2025-04-08 16:01:10 +02:00
/ * *
* Signs a Versia entity with that user ' s private key
*
* @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
* /
public async sign (
entity : KnownEntity | VersiaEntities . Collection ,
signatureUrl : URL ,
signatureMethod : HttpVerb = "POST" ,
) : Promise < {
headers : Headers ;
} > {
const privateKey = await crypto . subtle . importKey (
"pkcs8" ,
Buffer . from ( this . data . privateKey ? ? "" , "base64" ) ,
"Ed25519" ,
false ,
[ "sign" ] ,
) ;
const { headers } = await sign (
privateKey ,
2025-04-08 17:27:08 +02:00
this . uri ,
2025-04-08 16:01:10 +02:00
new Request ( signatureUrl , {
method : signatureMethod ,
body : JSON.stringify ( entity ) ,
} ) ,
) ;
return { headers } ;
}
2024-12-09 15:01:19 +01:00
/ * *
* Perform a WebFinger lookup to find a user ' s URI
* @param username
* @param hostname
* @returns URI , or null if not found
* /
2025-04-08 16:01:10 +02:00
public static webFinger (
2024-07-17 15:37:36 +02:00
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-04-09 02:15:00 +02:00
return FederationRequester . resolveWebFinger ( username , hostname ) ;
2024-12-09 15:01:19 +01:00
} catch {
try {
2025-04-09 02:15:00 +02:00
return FederationRequester . resolveWebFinger (
2025-04-08 16:01:10 +02:00
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 ;
2025-08-21 00:45:58 +02:00
url : ProxiableUrl ;
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 ;
2025-08-21 00:45:58 +02:00
url : ProxiableUrl ;
icon? : ProxiableUrl ;
2024-09-24 14:42:39 +02:00
server_id : string ;
} [ ]
> {
// Get all linked accounts
const accounts = await db . query . OpenIdAccounts . findMany ( {
2025-05-26 19:00:24 +02:00
where : ( User ) : 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-08-21 00:45:58 +02:00
icon : issuer.icon ,
2024-09-24 14:42:39 +02:00
server_id : account.serverId ,
} ;
} )
. filter ( ( x ) = > x !== null ) ;
}
2025-05-04 16:38:37 +02:00
public async recalculateFollowerCount ( ) : Promise < void > {
const followerCount = await db . $count (
Relationships ,
and (
eq ( Relationships . subjectId , this . id ) ,
eq ( Relationships . following , true ) ,
) ,
) ;
await this . update ( {
followerCount ,
} ) ;
}
public async recalculateFollowingCount ( ) : Promise < void > {
const followingCount = await db . $count (
Relationships ,
and (
eq ( Relationships . ownerId , this . id ) ,
eq ( Relationships . following , true ) ,
) ,
) ;
await this . update ( {
followingCount ,
} ) ;
}
public async recalculateStatusCount ( ) : Promise < void > {
const statusCount = await db . $count (
Notes ,
and ( eq ( Notes . authorId , this . id ) ) ,
) ;
await this . update ( {
statusCount ,
} ) ;
}
2024-12-09 15:01:19 +01:00
public async notify (
2025-05-28 17:17:03 +02:00
type :
| "mention"
| "follow_request"
| "follow"
| "favourite"
| "reblog"
| "reaction" ,
2024-12-09 13:50:46 +01:00
relatedUser : User ,
note? : Note ,
) : Promise < void > {
2025-07-04 06:29:43 +02:00
const notification = (
await db
. insert ( Notifications )
. values ( {
id : randomUUIDv7 ( ) ,
accountId : relatedUser.id ,
type ,
notifiedId : this.id ,
noteId : note?.id ? ? null ,
} )
. returning ( )
) [ 0 ] ;
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 ,
2025-05-28 17:17:03 +02:00
type :
| "mention"
| "follow_request"
| "follow"
| "favourite"
| "reblog"
| "reaction" ,
2025-01-02 02:45:40 +01:00
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-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
}
2025-04-08 17:27:08 +02:00
/ * *
* Tries to fetch a Versia user from the given URL .
*
* @param url The URL to fetch the user from
* /
public static async fromVersia ( url : URL ) : Promise < User > ;
2025-04-08 16:59:18 +02:00
/ * *
* Takes a Versia User representation , and serializes it to the database .
*
* If the user already exists , it will update it .
2025-04-08 18:13:30 +02:00
* @param versiaUser
2025-04-08 16:59:18 +02:00
* /
public static async fromVersia (
versiaUser : VersiaEntities.User ,
2025-04-08 17:27:08 +02:00
) : Promise < User > ;
public static async fromVersia (
versiaUser : VersiaEntities.User | URL ,
2025-04-08 16:59:18 +02:00
) : Promise < User > {
2025-04-08 17:27:08 +02:00
if ( versiaUser instanceof URL ) {
let uri = versiaUser ;
const instance = await Instance . resolve ( uri ) ;
if ( instance . data . protocol === "activitypub" ) {
if ( ! config . federation . bridge ) {
throw new Error ( "ActivityPub bridge is not enabled" ) ;
}
uri = new URL (
` /apbridge/versia/query? ${ new URLSearchParams ( {
user_url : uri.href ,
} ) } ` ,
config . federation . bridge . url ,
) ;
}
2025-07-04 06:29:43 +02:00
const user = await new FederationRequester (
config . instance . keys . private ,
config . http . base_url ,
) . fetchEntity ( uri , VersiaEntities . User ) ;
2025-04-08 17:27:08 +02:00
return User . fromVersia ( user ) ;
}
2025-04-08 16:59:18 +02:00
const {
username ,
inbox ,
avatar ,
header ,
display_name ,
fields ,
collections ,
created_at ,
2025-05-09 18:47:28 +02:00
manually_approves_followers ,
2025-04-08 16:59:18 +02:00
bio ,
public_key ,
uri ,
extensions ,
} = versiaUser . data ;
2025-04-16 16:35:17 +02:00
const instance = await Instance . resolve ( new URL ( versiaUser . data . uri ) ) ;
2025-04-08 16:59:18 +02:00
const existingUser = await User . fromSql (
2025-04-16 16:35:17 +02:00
eq ( Users . uri , versiaUser . data . uri ) ,
2024-12-09 13:36:15 +01:00
) ;
2025-04-08 16:59:18 +02:00
const user =
existingUser ? ?
( await User . insert ( {
username ,
id : randomUUIDv7 ( ) ,
publicKey : public_key.key ,
2025-04-16 16:35:17 +02:00
uri ,
2025-04-08 16:59:18 +02:00
instanceId : instance.id ,
} ) ) ;
// Avatars and headers are stored in a separate table, so we need to update them separately
let userAvatar : Media | null = null ;
let userHeader : Media | null = null ;
if ( avatar ) {
if ( user . avatar ) {
userAvatar = new Media (
await user . avatar . update ( {
content : avatar ,
} ) ,
) ;
} else {
userAvatar = await Media . insert ( {
id : randomUUIDv7 ( ) ,
content : avatar ,
} ) ;
2025-01-28 19:07:55 +01:00
}
2025-04-08 16:59:18 +02:00
}
2025-01-28 19:07:55 +01:00
2025-04-08 16:59:18 +02:00
if ( header ) {
if ( user . header ) {
userHeader = new Media (
await user . header . update ( {
content : header ,
} ) ,
) ;
} else {
userHeader = await Media . insert ( {
id : randomUUIDv7 ( ) ,
content : header ,
} ) ;
2025-01-28 19:07:55 +01:00
}
2024-06-13 04:26:43 +02:00
}
2025-04-08 16:59:18 +02:00
await user . update ( {
createdAt : new Date ( created_at ) . toISOString ( ) ,
endpoints : {
2025-04-16 16:35:17 +02:00
inbox ,
outbox : collections.outbox ,
followers : collections.followers ,
following : collections.following ,
featured : collections.featured ,
likes : collections [ "pub.versia:likes/Likes" ] ? ? undefined ,
dislikes : collections [ "pub.versia:likes/Dislikes" ] ? ? undefined ,
2025-04-08 16:59:18 +02:00
} ,
2025-05-09 18:47:28 +02:00
isLocked : manually_approves_followers ? ? false ,
2025-04-08 16:59:18 +02:00
avatarId : userAvatar?.id ,
headerId : userHeader?.id ,
fields : fields ? ? [ ] ,
displayName : display_name ,
note : getBestContentType ( bio ) . content ,
2025-01-28 19:07:55 +01:00
} ) ;
2024-12-09 13:36:15 +01:00
2025-04-08 16:59:18 +02:00
// Emojis are stored in a separate table, so we need to update them separately
const emojis = await Promise . all (
extensions ? . [ "pub.versia:custom_emojis" ] ? . emojis . map ( ( e ) = >
Emoji . fromVersia ( e , instance ) ,
) ? ? [ ] ,
) ;
await user . updateEmojis ( emojis ) ;
return user ;
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 > {
2025-06-22 18:43:03 +02:00
federationResolversLogger . debug ` Resolving user ${ chalk . gray ( uri ) } ` ;
2024-06-06 06:49:06 +02:00
// Check if user not already in database
2025-04-08 17:27:08 +02:00
const foundUser = await User . fromSql ( eq ( Users . uri , uri . href ) ) ;
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 ) {
2025-06-15 23:43:27 +02:00
const userUuid = uri . href . match ( uuid ) ;
2024-06-06 06:49:06 +02:00
2025-06-15 23:43:27 +02:00
if ( ! userUuid ? . [ 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 ` ,
) ;
}
2025-06-15 23:43:27 +02:00
return await User . fromId ( userUuid [ 0 ] ) ;
2024-06-06 06:49:06 +02:00
}
2025-06-22 18:43:03 +02:00
federationResolversLogger . debug ` User not found in database, fetching from remote ` ;
2024-11-24 22:01:14 +01:00
2025-04-08 17:27:08 +02:00
return User . fromVersia ( 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 ,
} ;
}
2025-04-08 16:59:18 +02:00
public static async register (
username : string ,
options? : Partial < {
email : string ;
password : string ;
avatar : Media ;
isAdmin : boolean ;
} > ,
) : Promise < User > {
2024-04-25 05:48:39 +02:00
const keys = await User . generateKeys ( ) ;
2025-04-08 16:59:18 +02:00
const user = await User . insert ( {
id : randomUUIDv7 ( ) ,
2025-04-09 02:15:00 +02:00
username ,
2025-04-08 16:59:18 +02:00
displayName : username ,
password : options?.password
? await bunPassword . hash ( options . password )
: null ,
email : options?.email ,
note : "" ,
avatarId : options?.avatar?.id ,
isAdmin : options?.isAdmin ,
publicKey : keys.public_key ,
fields : [ ] ,
privateKey : keys.private_key ,
updatedAt : new Date ( ) . toISOString ( ) ,
source : {
language : "en" ,
note : "" ,
privacy : "public" ,
sensitive : false ,
fields : [ ] ,
} as z . infer < typeof Source > ,
} ) ;
2024-04-25 05:48:39 +02:00
2025-04-08 16:59:18 +02:00
return user ;
2024-04-25 05:48:39 +02:00
}
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 {
2025-04-08 18:13:30 +02:00
return this . local
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 (
2025-04-08 18:13:30 +02:00
this . local &&
2024-06-30 10:55:50 +02:00
( 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
2025-04-08 16:01:10 +02:00
public get federationRequester ( ) : Promise < FederationRequester > {
return crypto . subtle
. importKey (
"pkcs8" ,
Buffer . from ( this . data . privateKey ? ? "" , "base64" ) ,
"Ed25519" ,
false ,
[ "sign" ] ,
)
. then ( ( k ) = > {
2025-04-08 17:27:08 +02:00
return new FederationRequester ( k , this . uri ) ;
2025-04-08 16:01:10 +02:00
} ) ;
2024-07-26 18:51:39 +02:00
}
/ * *
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
2025-05-01 03:19:38 +02:00
* @returns The followers that received the entity
2025-03-30 23:44:50 +02:00
* /
2025-05-01 03:19:38 +02:00
public async federateToFollowers ( entity : KnownEntity ) : Promise < User [ ] > {
2025-03-30 23:44:50 +02:00
// 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 : {
2025-04-08 16:01:10 +02:00
entity : entity.toJSON ( ) ,
type : entity . data . type ,
2024-11-25 20:50:55 +01:00
recipientId : follower.id ,
senderId : this.id ,
} ,
} ) ) ,
) ;
2025-05-01 03:19:38 +02:00
return followers ;
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 (
2025-04-08 17:27:08 +02:00
` User ${ chalk . gray ( user . uri ) } does not have an inbox endpoint ` ,
2024-11-25 20:50:55 +01:00
) ;
}
2024-07-26 18:51:39 +02:00
try {
2025-04-08 16:01:10 +02:00
await ( await this . federationRequester ) . postEntity (
new URL ( inbox ) ,
entity ,
) ;
2024-07-26 18:51:39 +02:00
} catch ( e ) {
2025-06-22 18:43:03 +02:00
federationDeliveryLogger . error ` Federating ${ chalk . gray (
2025-04-08 21:54:55 +02:00
entity . data . type ,
) } to $ { user . uri } $ { chalk . bold . red ( "failed" ) } ` ;
2025-06-22 18:43:03 +02:00
federationDeliveryLogger . error ` ${ 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 ,
2025-04-08 16:59:18 +02:00
display_name : user.displayName || user . username ,
2024-04-25 05:40:27 +02:00
note : user.note ,
2025-04-08 17:27:08 +02:00
uri : this.uri.href ,
2024-04-25 05:40:27 +02:00
url :
user . uri ||
2025-04-08 17:27:08 +02:00
new URL ( ` /@ ${ user . username } ` , config . http . base_url ) . href ,
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 ,
2025-04-08 16:59:18 +02:00
source : isOwnAccount ? ( user . source ? ? undefined ) : undefined ,
2024-04-25 05:40:27 +02:00
// 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
} ;
}
2025-04-08 16:01:10 +02:00
public toVersia ( ) : VersiaEntities . User {
2025-04-08 18:13:30 +02:00
if ( this . remote ) {
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
2025-04-08 16:01:10 +02:00
return new VersiaEntities . User ( {
2024-04-25 05:40:27 +02:00
id : user.id ,
type : "User" ,
2025-04-16 16:35:17 +02:00
uri : this.uri.href ,
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 ,
2025-04-16 16:35:17 +02:00
) . href ,
2024-08-26 19:06:49 +02:00
"pub.versia:likes/Likes" : new URL (
` /users/ ${ user . id } /likes ` ,
config . http . base_url ,
2025-04-16 16:35:17 +02:00
) . href ,
2024-08-26 19:06:49 +02:00
"pub.versia:likes/Dislikes" : new URL (
` /users/ ${ user . id } /dislikes ` ,
config . http . base_url ,
2025-04-16 16:35:17 +02:00
) . href ,
2024-08-26 19:06:49 +02:00
followers : new URL (
` /users/ ${ user . id } /followers ` ,
config . http . base_url ,
2025-04-16 16:35:17 +02:00
) . href ,
2024-08-26 19:06:49 +02:00
following : new URL (
` /users/ ${ user . id } /following ` ,
config . http . base_url ,
2025-04-16 16:35:17 +02:00
) . href ,
2024-08-26 19:06:49 +02:00
outbox : new URL (
` /users/ ${ user . id } /outbox ` ,
config . http . base_url ,
2025-04-16 16:35:17 +02:00
) . href ,
2024-08-26 19:06:49 +02:00
} ,
2025-04-16 16:35:17 +02:00
inbox : new URL ( ` /users/ ${ user . id } /inbox ` , config . http . base_url )
. href ,
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-04-08 16:01:10 +02:00
avatar : this.avatar?.toVersia ( ) . data as z . infer <
typeof ImageContentFormatSchema
> ,
header : this.header?.toVersia ( ) . data as z . infer <
typeof ImageContentFormatSchema
> ,
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 : {
2025-04-16 16:35:17 +02:00
actor : new URL ( ` /users/ ${ user . id } ` , config . http . base_url ) . href ,
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-04-08 16:01:10 +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-04-08 17:27:08 +02:00
url : this.uri.href ,
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 ,
} ;
}
}