2024-05-29 02:59:49 +02:00
import { idValidator } from "@/api" ;
import { getBestContentType , urlToContentFormat } from "@/content_types" ;
2024-06-13 07:38:26 +02:00
import { randomString } from "@/math" ;
2024-05-29 02:59:49 +02:00
import { addUserToMeilisearch } from "@/meilisearch" ;
import { proxyUrl } from "@/response" ;
2024-06-06 06:49:06 +02:00
import { EntityValidator } from "@lysand-org/federation" ;
2024-06-20 01:21:02 +02:00
import type { Entity , User as LysandUser } from "@lysand-org/federation/types" ;
2024-05-06 09:16:33 +02:00
import {
2024-06-13 04:26:43 +02:00
type InferInsertModel ,
type InferSelectModel ,
2024-05-06 09:16:33 +02:00
type SQL ,
and ,
count ,
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" ;
2024-06-29 05:50:56 +02:00
import { objectToInboxRequest } from "~/classes/functions/federation" ;
import { addInstanceIfNotExists } from "~/classes/functions/instance" ;
2024-04-25 05:40:27 +02:00
import {
type UserWithRelations ,
findManyUsers ,
2024-06-29 05:50:56 +02:00
} from "~/classes/functions/user" ;
2024-05-29 02:59:49 +02:00
import { db } from "~/drizzle/db" ;
2024-04-25 05:40:27 +02:00
import {
EmojiToUser ,
2024-06-13 04:26:43 +02:00
type Instances ,
2024-04-25 05:40:27 +02:00
NoteToMentions ,
2024-05-06 09:16:33 +02:00
Notes ,
2024-06-08 06:57:29 +02:00
type RolePermissions ,
2024-04-25 05:40:27 +02:00
UserToPinnedNotes ,
Users ,
2024-05-29 02:59:49 +02:00
} from "~/drizzle/schema" ;
import { type Config , config } from "~/packages/config-manager" ;
2024-06-13 04:26:43 +02:00
import type { Account as apiAccount } from "~/types/mastodon/account" ;
import type { Mention as apiMention } from "~/types/mastodon/mention" ;
2024-06-13 02:45:07 +02:00
import { BaseInterface } from "./base" ;
2024-06-13 06:52:01 +02:00
import { Emoji } from "./emoji" ;
2024-04-25 05:40:27 +02:00
import type { Note } from "./note" ;
2024-06-12 02:29:59 +02:00
import { Role } from "./role" ;
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 > {
async reload ( ) : Promise < void > {
const reloaded = await User . fromId ( this . data . id ) ;
if ( ! reloaded ) {
throw new Error ( "Failed to reload user" ) ;
}
this . data = reloaded . data ;
}
2024-04-25 05:40:27 +02:00
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 ) ) ;
}
static async fromIds ( ids : string [ ] ) : Promise < User [ ] > {
return await User . manyFromSql ( inArray ( Users . id , ids ) ) ;
}
static async fromSql (
sql : SQL < unknown > | undefined ,
orderBy : SQL < unknown > | undefined = desc ( Users . id ) ,
) {
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
}
static async manyFromSql (
sql : SQL < unknown > | undefined ,
orderBy : SQL < unknown > | undefined = desc ( Users . id ) ,
limit? : number ,
offset? : number ,
extra? : Parameters < typeof db.query.Users.findMany > [ 0 ] ,
) {
const found = await findManyUsers ( {
where : sql ,
orderBy ,
limit ,
offset ,
with : extra ? . with ,
} ) ;
return found . map ( ( s ) = > new User ( s ) ) ;
}
get id() {
2024-06-13 02:45:07 +02:00
return this . data . id ;
2024-04-25 05:40:27 +02:00
}
isLocal() {
2024-06-13 02:45:07 +02:00
return this . data . instanceId === null ;
2024-04-25 05:40:27 +02:00
}
isRemote() {
return ! this . isLocal ( ) ;
}
getUri() {
return (
2024-06-13 02:45:07 +02:00
this . data . uri ||
new URL ( ` /users/ ${ this . data . id } ` , config . http . base_url ) . toString ( )
2024-04-25 05:40:27 +02:00
) ;
}
static getUri ( id : string , uri : string | null , baseUrl : string ) {
return uri || new URL ( ` /users/ ${ id } ` , baseUrl ) . toString ( ) ;
}
2024-06-08 06:57:29 +02:00
public hasPermission ( permission : RolePermissions ) {
return this . getAllPermissions ( ) . includes ( permission ) ;
}
public getAllPermissions() {
return (
2024-06-13 02:45:07 +02:00
this . data . roles
2024-06-08 06:57:29 +02:00
. flatMap ( ( role ) = > role . permissions )
// Add default permissions
. concat ( config . permissions . default )
// If admin, add admin permissions
2024-06-13 02:45:07 +02:00
. concat ( this . data . isAdmin ? config . permissions . admin : [ ] )
2024-06-08 06:57:29 +02:00
. reduce ( ( acc , permission ) = > {
2024-06-13 04:26:43 +02:00
if ( ! acc . includes ( permission ) ) {
acc . push ( permission ) ;
}
2024-06-08 06:57:29 +02:00
return acc ;
} , [ ] as RolePermissions [ ] )
) ;
}
2024-05-06 09:16:33 +02:00
static async getCount() {
return (
await db
. select ( {
count : count ( ) ,
} )
. from ( Users )
. where ( isNull ( Users . instanceId ) )
) [ 0 ] . count ;
}
static async getActiveInPeriod ( milliseconds : number ) {
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-06-13 02:45:07 +02:00
async delete ( ids : string [ ] ) : Promise < void > ;
async delete ( ) : Promise < void > ;
async delete ( ids? : unknown ) : Promise < void > {
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-05-17 10:27:41 +02:00
async resetPassword() {
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-04-25 05:40:27 +02:00
async pin ( note : Note ) {
return (
await db
. insert ( UserToPinnedNotes )
. values ( {
noteId : note.id ,
userId : this.id ,
} )
. returning ( )
) [ 0 ] ;
}
async unpin ( note : Note ) {
return (
await db
. delete ( UserToPinnedNotes )
. where (
and (
eq ( NoteToMentions . noteId , note . id ) ,
eq ( NoteToMentions . userId , this . id ) ,
) ,
)
. returning ( )
) [ 0 ] ;
}
2024-06-13 04:26:43 +02:00
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-06-13 04:26:43 +02:00
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-06-06 06:49:06 +02:00
const updated = await User . saveFromRemote ( this . getUri ( ) ) ;
2024-04-25 05:40:27 +02:00
2024-06-13 02:45:07 +02:00
this . data = updated . data ;
2024-06-06 06:49:06 +02:00
return this ;
}
2024-06-13 04:26:43 +02:00
static async saveFromRemote ( uri : string ) : Promise < User > {
2024-04-25 05:40:27 +02:00
if ( ! URL . canParse ( uri ) ) {
throw new Error ( ` Invalid URI to parse ${ uri } ` ) ;
}
const response = await fetch ( uri , {
method : "GET" ,
headers : {
Accept : "application/json" ,
} ,
2024-06-26 05:13:40 +02:00
proxy : config.http.proxy.address ,
2024-04-25 05:40:27 +02:00
} ) ;
2024-06-20 01:21:02 +02:00
const json = ( await response . json ( ) ) as Partial < LysandUser > ;
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
// Parse emojis and add them to database
const userEmojis =
data . extensions ? . [ "org.lysand:custom_emojis" ] ? . emojis ? ? [ ] ;
const instance = await addInstanceIfNotExists ( data . uri ) ;
2024-06-13 06:52:01 +02:00
const emojis = await Promise . all (
userEmojis . map ( ( emoji ) = > Emoji . fromLysand ( emoji , instance . id ) ) ,
) ;
2024-04-25 05:40:27 +02:00
2024-06-13 04:26:43 +02:00
const user = await User . fromLysand ( data , instance ) ;
2024-04-25 05:40:27 +02:00
// Add emojis to user
if ( emojis . length > 0 ) {
2024-06-13 04:26:43 +02:00
await db . delete ( EmojiToUser ) . where ( eq ( EmojiToUser . userId , user . id ) ) ;
2024-04-25 05:40:27 +02:00
await db . insert ( EmojiToUser ) . values (
emojis . map ( ( emoji ) = > ( {
emojiId : emoji.id ,
2024-06-13 04:26:43 +02:00
userId : user.id ,
2024-04-25 05:40:27 +02:00
} ) ) ,
) ;
}
2024-06-13 04:26:43 +02:00
const finalUser = await User . fromId ( user . id ) ;
2024-04-25 05:40:27 +02:00
2024-06-13 04:26:43 +02:00
if ( ! finalUser ) {
throw new Error ( "Failed to save user from remote" ) ;
}
2024-04-25 05:40:27 +02:00
// Add to Meilisearch
await addUserToMeilisearch ( finalUser ) ;
return finalUser ;
}
2024-06-13 04:26:43 +02:00
static async fromLysand (
2024-06-20 01:21:02 +02:00
user : LysandUser ,
2024-06-13 04:26:43 +02:00
instance : InferSelectModel < typeof Instances > ,
) : Promise < User > {
const data = {
username : user.username ,
uri : user.uri ,
createdAt : new Date ( user . created_at ) . toISOString ( ) ,
endpoints : {
dislikes : user.dislikes ,
featured : user.featured ,
likes : user.likes ,
followers : user.followers ,
following : user.following ,
inbox : user.inbox ,
outbox : user.outbox ,
} ,
fields : user.fields ? ? [ ] ,
updatedAt : new Date ( user . created_at ) . toISOString ( ) ,
instanceId : instance.id ,
avatar : user.avatar
? Object . entries ( user . avatar ) [ 0 ] [ 1 ] . content
: "" ,
header : user.header
? Object . entries ( user . header ) [ 0 ] [ 1 ] . content
: "" ,
displayName : user.display_name ? ? "" ,
note : getBestContentType ( user . bio ) . content ,
publicKey : user.public_key.public_key ,
source : {
language : null ,
note : "" ,
privacy : "public" ,
sensitive : false ,
fields : [ ] ,
} ,
} ;
// Check if new user already exists
const foundUser = await User . fromSql ( eq ( Users . uri , user . uri ) ) ;
// If it exists, simply update it
if ( foundUser ) {
await foundUser . update ( data ) ;
return foundUser ;
}
// Else, create a new user
return await User . insert ( data ) ;
}
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 ;
}
2024-06-06 06:49:06 +02:00
static async resolve ( uri : string ) : Promise < User | null > {
// Check if user not already in database
const foundUser = await User . fromSql ( eq ( Users . uri , uri ) ) ;
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
if ( uri . startsWith ( config . http . base_url ) ) {
const uuid = uri . match ( idValidator ) ;
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 ] ) ;
}
return await User . saveFromRemote ( uri ) ;
}
2024-04-25 05:40:27 +02:00
/ * *
* Get the user ' s avatar in raw URL format
* @param config The config to use
* @returns The raw URL for the user ' s avatar
* /
getAvatarUrl ( config : Config ) {
2024-06-13 04:26:43 +02:00
if ( ! this . data . avatar ) {
2024-04-25 05:40:27 +02:00
return (
config . defaults . avatar ||
2024-06-13 02:45:07 +02: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
}
2024-06-13 02:45:07 +02:00
return this . data . avatar ;
2024-04-25 05:40:27 +02:00
}
2024-04-25 05:48:39 +02:00
static async generateKeys() {
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 ,
} ;
}
static async fromDataLocal ( data : {
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 ;
avatar? : string ;
header? : string ;
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 ( {
username : data.username ,
displayName : data.display_name ? ? data . username ,
2024-05-08 02:10:14 +02:00
password :
data . skipPasswordHash || ! data . password
? data . password
: await Bun . password . hash ( data . password ) ,
2024-04-25 05:48:39 +02:00
email : data.email ,
note : data.bio ? ? "" ,
2024-05-16 04:37:25 +02:00
avatar : data.avatar ? ? config . defaults . avatar ? ? "" ,
header : data.header ? ? config . defaults . avatar ? ? "" ,
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 : {
language : null ,
note : "" ,
privacy : "public" ,
sensitive : false ,
fields : [ ] ,
} ,
} )
. 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
// Add to Meilisearch
await addUserToMeilisearch ( finalUser ) ;
return finalUser ;
}
2024-04-25 05:40:27 +02:00
/ * *
* Get the user ' s header in raw URL format
* @param config The config to use
* @returns The raw URL for the user ' s header
* /
getHeaderUrl ( config : Config ) {
2024-06-13 04:26:43 +02:00
if ( ! this . data . header ) {
return config . defaults . header || "" ;
}
2024-06-13 02:45:07 +02:00
return this . data . header ;
2024-04-25 05:40:27 +02:00
}
getAcct() {
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
}
static getAcct ( isLocal : boolean , username : string , baseUrl? : string ) {
return isLocal ? username : ` ${ username } @ ${ baseUrl } ` ;
}
2024-06-13 02:45:07 +02:00
async update (
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-13 02:45:07 +02:00
newUser . username ||
newUser . displayName ||
newUser . note ||
newUser . avatar ||
newUser . header ||
newUser . fields ||
newUser . publicKey ||
newUser . isAdmin ||
newUser . isBot ||
newUser . isLocked ||
newUser . endpoints ||
newUser . isDiscoverable
2024-06-06 06:58:28 +02:00
) {
2024-06-06 07:25:49 +02:00
await this . federateToFollowers ( this . toLysand ( ) ) ;
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-06-20 01:21:02 +02:00
async federateToFollowers ( object : Entity ) {
2024-06-06 07:25:49 +02:00
// Get followers
const followers = await User . manyFromSql (
and (
sql ` EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${ this . id } AND "Relationships"."ownerId" = ${ Users . id } AND "Relationships"."following" = true) ` ,
isNotNull ( Users . instanceId ) ,
) ,
) ;
for ( const follower of followers ) {
const federationRequest = await objectToInboxRequest (
object ,
this ,
follower ,
) ;
// FIXME: Add to new queue system when it's implemented
2024-06-26 05:13:40 +02:00
fetch ( federationRequest , {
proxy : config.http.proxy.address ,
} ) ;
2024-06-06 07:25:49 +02:00
}
}
2024-05-12 03:27:28 +02:00
2024-06-13 04:26:43 +02:00
toApi ( isOwnAccount = false ) : apiAccount {
2024-06-13 02:45:07 +02:00
const user = this . data ;
2024-04-25 05:40:27 +02:00
return {
id : user.id ,
username : user.username ,
display_name : user.displayName ,
note : user.note ,
url :
user . uri ||
new URL ( ` /@ ${ user . username } ` , config . http . base_url ) . toString ( ) ,
2024-05-05 07:13:23 +02:00
avatar : proxyUrl ( this . getAvatarUrl ( config ) ) ? ? "" ,
header : proxyUrl ( this . getHeaderUrl ( config ) ) ? ? "" ,
2024-04-25 05:40:27 +02:00
locked : user.isLocked ,
created_at : new Date ( user . createdAt ) . toISOString ( ) ,
followers_count : user.followerCount ,
following_count : user.followingCount ,
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 ,
} ) ) ,
2024-04-25 05:40:27 +02:00
bot : user.isBot ,
source : isOwnAccount ? user.source : undefined ,
// TODO: Add static avatar and header
2024-05-05 07:13:23 +02:00
avatar_static : proxyUrl ( this . getAvatarUrl ( config ) ) ? ? "" ,
header_static : proxyUrl ( this . getHeaderUrl ( config ) ) ? ? "" ,
2024-04-25 05:40:27 +02:00
acct : this.getAcct ( ) ,
// TODO: Add these fields
limited : false ,
moved : null ,
noindex : false ,
suspended : false ,
discoverable : undefined ,
mute_expires_at : undefined ,
2024-06-12 02:29:59 +02:00
roles : user.roles
2024-06-13 03:03:57 +02:00
. map ( ( role ) = > new Role ( role ) )
2024-06-12 02:29:59 +02:00
. concat (
2024-06-13 03:03:57 +02:00
new Role ( {
2024-06-12 02:29:59 +02:00
id : "default" ,
name : "Default" ,
permissions : config.permissions.default ,
priority : 0 ,
description : "Default role for all users" ,
visible : false ,
icon : null ,
} ) ,
)
. concat (
user . isAdmin
? [
2024-06-13 03:03:57 +02:00
new Role ( {
2024-06-12 02:29:59 +02:00
id : "admin" ,
name : "Admin" ,
permissions : config.permissions.admin ,
priority : 2 * * 31 - 1 ,
description :
"Default role for all administrators" ,
visible : false ,
icon : null ,
} ) ,
]
: [ ] ,
)
2024-06-13 04:26:43 +02:00
. map ( ( r ) = > r . toApi ( ) ) ,
2024-04-25 05:40:27 +02:00
group : false ,
} ;
}
2024-06-20 01:21:02 +02:00
toLysand ( ) : LysandUser {
2024-04-25 05:40:27 +02:00
if ( this . isRemote ( ) ) {
throw new Error ( "Cannot convert remote user to Lysand format" ) ;
}
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" ,
uri : this.getUri ( ) ,
bio : {
"text/html" : {
content : user.note ,
} ,
"text/plain" : {
content : htmlToText ( user . note ) ,
} ,
} ,
created_at : new Date ( user . createdAt ) . toISOString ( ) ,
dislikes : new URL (
` /users/ ${ user . id } /dislikes ` ,
config . http . base_url ,
) . toString ( ) ,
featured : new URL (
` /users/ ${ user . id } /featured ` ,
config . http . base_url ,
) . toString ( ) ,
likes : new URL (
` /users/ ${ user . id } /likes ` ,
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 ( ) ,
inbox : new URL (
` /users/ ${ user . id } /inbox ` ,
config . http . base_url ,
) . toString ( ) ,
outbox : new URL (
` /users/ ${ user . id } /outbox ` ,
config . http . base_url ,
) . toString ( ) ,
indexable : false ,
username : user.username ,
avatar : urlToContentFormat ( this . getAvatarUrl ( config ) ) ? ? undefined ,
header : urlToContentFormat ( this . getHeaderUrl ( config ) ) ? ? undefined ,
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 ( ) ,
public_key : user.publicKey ,
} ,
extensions : {
"org.lysand:custom_emojis" : {
2024-06-13 06:52:01 +02:00
emojis : user.emojis.map ( ( emoji ) = >
new Emoji ( emoji ) . toLysand ( ) ,
) ,
2024-04-25 05:40:27 +02:00
} ,
} ,
} ;
}
2024-06-13 04:26:43 +02:00
toMention ( ) : apiMention {
2024-04-25 05:40:27 +02:00
return {
url : this.getUri ( ) ,
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 ,
} ;
}
}