2024-06-20 01:21:02 +02:00
import type {
Follow ,
FollowAccept ,
FollowReject ,
} from "@lysand-org/federation/types" ;
2024-04-25 05:40:27 +02:00
import { config } from "config-manager" ;
2024-05-15 02:35:13 +02:00
import { type InferSelectModel , and , eq , sql } from "drizzle-orm" ;
2024-05-29 02:59:49 +02:00
import { db } from "~/drizzle/db" ;
2024-04-11 13:39:07 +02:00
import {
2024-04-17 08:36:01 +02:00
Applications ,
2024-06-30 10:24:10 +02:00
type Instances ,
2024-04-17 08:36:01 +02:00
Notifications ,
Relationships ,
2024-06-08 06:57:29 +02:00
type Roles ,
2024-04-17 08:36:01 +02:00
Tokens ,
2024-06-30 10:24:10 +02:00
type Users ,
2024-05-29 02:59:49 +02:00
} from "~/drizzle/schema" ;
2024-07-26 18:51:39 +02:00
import type { EmojiWithInstance } from "~/packages/database-interface/emoji" ;
2024-05-29 02:59:49 +02:00
import { User } from "~/packages/database-interface/user" ;
2024-06-13 04:26:43 +02:00
import type { Application } from "./application" ;
2024-06-12 01:42:36 +02:00
import {
type Relationship ,
checkForBidirectionalRelationships ,
createNewRelationship ,
2024-06-13 04:26:43 +02:00
} from "./relationship" ;
import type { Token } from "./token" ;
2024-04-11 13:39:07 +02:00
2024-04-25 05:40:27 +02:00
export type UserType = InferSelectModel < typeof Users > ;
2024-04-17 06:09:21 +02:00
2024-04-25 05:40:27 +02:00
export type UserWithInstance = UserType & {
2024-04-17 08:36:01 +02:00
instance : InferSelectModel < typeof Instances > | null ;
2024-04-11 13:39:07 +02:00
} ;
2024-03-11 03:04:14 +01:00
2024-04-25 05:40:27 +02:00
export type UserWithRelations = UserType & {
2024-04-17 08:36:01 +02:00
instance : InferSelectModel < typeof Instances > | null ;
2024-04-11 13:39:07 +02:00
emojis : EmojiWithInstance [ ] ;
followerCount : number ;
followingCount : number ;
statusCount : number ;
2024-06-08 06:57:29 +02:00
roles : InferSelectModel < typeof Roles > [ ] ;
2024-04-11 13:39:07 +02:00
} ;
2023-10-23 07:39:42 +02:00
2024-04-11 13:39:07 +02:00
export const userRelations : {
instance : true ;
emojis : {
with : {
emoji : {
with : {
instance : true ;
} ;
} ;
} ;
} ;
2024-06-08 06:57:29 +02:00
roles : {
with : {
role : true ;
} ;
} ;
2024-04-11 13:39:07 +02:00
} = {
instance : true ,
emojis : {
with : {
emoji : {
with : {
instance : true ,
} ,
} ,
} ,
} ,
2024-06-08 06:57:29 +02:00
roles : {
with : {
role : true ,
} ,
} ,
2024-04-11 13:39:07 +02:00
} ;
export const userExtras = {
followerCount :
2024-04-17 08:36:01 +02:00
sql ` (SELECT COUNT(*) FROM "Relationships" "relationships" WHERE ("relationships"."ownerId" = "Users".id AND "relationships"."following" = true)) ` . as (
2024-04-11 13:39:07 +02:00
"follower_count" ,
) ,
followingCount :
2024-04-17 08:36:01 +02:00
sql ` (SELECT COUNT(*) FROM "Relationships" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = "Users".id AND "relationshipSubjects"."following" = true)) ` . as (
2024-04-11 13:39:07 +02:00
"following_count" ,
) ,
statusCount :
2024-04-17 08:36:01 +02:00
sql ` (SELECT COUNT(*) FROM "Notes" WHERE "Notes"."authorId" = "Users".id) ` . as (
2024-04-11 13:39:07 +02:00
"status_count" ,
) ,
} ;
2023-11-04 04:34:31 +01:00
2024-04-11 13:39:07 +02:00
export const userExtrasTemplate = ( name : string ) = > ( {
// @ts-ignore
followerCount : sql ( [
2024-04-17 08:36:01 +02:00
` (SELECT COUNT(*) FROM "Relationships" "relationships" WHERE ("relationships"."ownerId" = " ${ name } ".id AND "relationships"."following" = true)) ` ,
2024-04-11 13:39:07 +02:00
] ) . as ( "follower_count" ) ,
// @ts-ignore
followingCount : sql ( [
2024-04-17 08:36:01 +02:00
` (SELECT COUNT(*) FROM "Relationships" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = " ${ name } ".id AND "relationshipSubjects"."following" = true)) ` ,
2024-04-11 13:39:07 +02:00
] ) . as ( "following_count" ) ,
// @ts-ignore
statusCount : sql ( [
2024-04-17 08:36:01 +02:00
` (SELECT COUNT(*) FROM "Notes" WHERE "Notes"."authorId" = " ${ name } ".id) ` ,
2024-04-11 13:39:07 +02:00
] ) . as ( "status_count" ) ,
2023-11-27 01:56:16 +01:00
} ) ;
2024-04-11 13:39:07 +02:00
export interface AuthData {
2024-04-25 05:40:27 +02:00
user : User | null ;
2024-04-11 13:39:07 +02:00
token : string ;
2024-04-16 04:09:16 +02:00
application : Application | null ;
2024-04-11 13:39:07 +02:00
}
2023-10-08 22:20:42 +02:00
2024-05-06 09:16:33 +02:00
export const getFromHeader = async ( value : string ) : Promise < AuthData > = > {
const token = value . split ( " " ) [ 1 ] ;
const { user , application } =
await retrieveUserAndApplicationFromToken ( token ) ;
return { user , token , application } ;
} ;
2024-04-10 07:51:00 +02:00
export const followRequestUser = async (
2024-04-09 04:12:54 +02:00
follower : User ,
followee : User ,
2024-04-09 04:26:48 +02:00
relationshipId : string ,
2024-04-09 04:12:54 +02:00
reblogs = false ,
notify = false ,
languages : string [ ] = [ ] ,
2024-06-12 00:32:38 +02:00
) : Promise < Relationship > = > {
2024-04-25 05:40:27 +02:00
const isRemote = followee . isRemote ( ) ;
2024-04-10 07:51:00 +02:00
2024-06-12 00:32:38 +02:00
await db
. update ( Relationships )
. set ( {
2024-06-13 02:45:07 +02:00
following : isRemote ? false : ! followee . data . isLocked ,
requested : isRemote ? true : followee . data . isLocked ,
2024-06-12 00:32:38 +02:00
showingReblogs : reblogs ,
notifying : notify ,
languages : languages ,
} )
. where ( eq ( Relationships . id , relationshipId ) ) ;
2024-06-12 01:42:36 +02:00
// Set requested_by on other side
await db
. update ( Relationships )
. set ( {
2024-06-13 02:45:07 +02:00
requestedBy : isRemote ? true : followee . data . isLocked ,
followedBy : isRemote ? false : followee . data . isLocked ,
2024-06-12 01:42:36 +02:00
} )
. where (
and (
eq ( Relationships . ownerId , followee . id ) ,
eq ( Relationships . subjectId , follower . id ) ,
) ,
) ;
2024-06-12 00:32:38 +02:00
const updatedRelationship = await db . query . Relationships . findFirst ( {
where : ( rel , { eq } ) = > eq ( rel . id , relationshipId ) ,
} ) ;
if ( ! updatedRelationship ) {
throw new Error ( "Failed to update relationship" ) ;
}
2024-04-10 09:54:15 +02:00
2024-04-10 07:51:00 +02:00
if ( isRemote ) {
2024-07-26 18:51:39 +02:00
const { ok } = await follower . federateToUser (
2024-04-10 07:51:00 +02:00
followRequestToLysand ( follower , followee ) ,
followee ,
) ;
2024-04-09 04:12:54 +02:00
2024-07-26 18:51:39 +02:00
if ( ! ok ) {
2024-06-12 00:32:38 +02:00
await db
. update ( Relationships )
. set ( {
following : false ,
requested : false ,
} )
. where ( eq ( Relationships . id , relationshipId ) ) ;
const result = await db . query . Relationships . findFirst ( {
where : ( rel , { eq } ) = > eq ( rel . id , relationshipId ) ,
} ) ;
if ( ! result ) {
throw new Error ( "Failed to update relationship" ) ;
}
return result ;
2024-04-10 07:51:00 +02:00
}
2024-04-09 04:12:54 +02:00
} else {
2024-04-17 08:36:01 +02:00
await db . insert ( Notifications ) . values ( {
2024-04-16 07:40:35 +02:00
accountId : follower.id ,
2024-06-13 02:45:07 +02:00
type : followee . data . isLocked ? "follow_request" : "follow" ,
2024-04-16 07:40:35 +02:00
notifiedId : followee.id ,
2024-04-11 13:39:07 +02:00
} ) ;
2024-04-09 04:12:54 +02:00
}
2024-04-11 13:39:07 +02:00
return updatedRelationship ;
2024-04-09 04:12:54 +02:00
} ;
2024-04-10 10:07:03 +02:00
export const sendFollowAccept = async ( follower : User , followee : User ) = > {
2024-07-26 18:51:39 +02:00
await follower . federateToUser (
2024-04-10 10:07:03 +02:00
followAcceptToLysand ( follower , followee ) ,
followee ,
) ;
} ;
export const sendFollowReject = async ( follower : User , followee : User ) = > {
2024-07-26 18:51:39 +02:00
await follower . federateToUser (
2024-04-10 10:07:03 +02:00
followRejectToLysand ( follower , followee ) ,
followee ,
) ;
} ;
2024-04-11 13:39:07 +02:00
export const transformOutputToUserWithRelations = (
2024-04-25 05:40:27 +02:00
user : Omit < UserType , " endpoints " > & {
2024-04-11 13:39:07 +02:00
followerCount : unknown ;
followingCount : unknown ;
statusCount : unknown ;
emojis : {
2024-04-14 03:21:38 +02:00
userId : string ;
emojiId : string ;
2024-04-11 13:39:07 +02:00
emoji? : EmojiWithInstance ;
} [ ] ;
2024-04-17 08:36:01 +02:00
instance : InferSelectModel < typeof Instances > | null ;
2024-06-08 06:57:29 +02:00
roles : {
userId : string ;
roleId : string ;
role? : InferSelectModel < typeof Roles > ;
} [ ] ;
2024-04-11 13:39:07 +02:00
endpoints : unknown ;
} ,
) : UserWithRelations = > {
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 EmojiWithInstance ,
) ,
2024-06-08 06:57:29 +02:00
roles : user.roles
. map ( ( role ) = > role . role )
. filter ( Boolean ) as InferSelectModel < typeof Roles > [ ] ,
2024-04-11 13:39:07 +02:00
} ;
} ;
export const findManyUsers = async (
2024-04-17 08:36:01 +02:00
query : Parameters < typeof db.query.Users.findMany > [ 0 ] ,
2024-04-11 13:39:07 +02:00
) : Promise < UserWithRelations [ ] > = > {
2024-04-17 08:36:01 +02:00
const output = await db . query . Users . findMany ( {
2024-04-11 13:39:07 +02:00
. . . query ,
with : {
. . . userRelations ,
. . . query ? . with ,
} ,
extras : {
. . . userExtras ,
. . . query ? . extras ,
} ,
} ) ;
return output . map ( ( user ) = > transformOutputToUserWithRelations ( user ) ) ;
} ;
2023-11-11 03:36:06 +01:00
/ * *
* Retrieves a user from a token .
* @param access_token The access token to retrieve the user from .
* @returns The user associated with the given access token .
* /
2024-04-11 13:39:07 +02:00
export const retrieveUserFromToken = async (
2024-06-13 04:26:43 +02:00
accessToken : string ,
2024-04-25 05:40:27 +02:00
) : Promise < User | null > = > {
2024-06-13 04:26:43 +02:00
if ( ! accessToken ) {
return null ;
}
2024-04-07 07:30:49 +02:00
2024-06-13 04:26:43 +02:00
const token = await retrieveToken ( accessToken ) ;
2024-04-07 07:30:49 +02:00
2024-06-13 04:26:43 +02:00
if ( ! token ? . userId ) {
return null ;
}
2024-04-11 13:39:07 +02:00
2024-04-25 05:40:27 +02:00
const user = await User . fromId ( token . userId ) ;
2024-04-07 07:30:49 +02:00
2024-04-11 13:39:07 +02:00
return user ;
2023-11-11 03:36:06 +01:00
} ;
2023-09-22 05:18:05 +02:00
2024-04-16 04:09:16 +02:00
export const retrieveUserAndApplicationFromToken = async (
2024-06-13 04:26:43 +02:00
accessToken : string ,
2024-04-16 04:09:16 +02:00
) : Promise < {
2024-04-25 05:40:27 +02:00
user : User | null ;
2024-04-16 04:09:16 +02:00
application : Application | null ;
} > = > {
2024-06-13 04:26:43 +02:00
if ( ! accessToken ) {
return { user : null , application : null } ;
}
2024-04-16 04:09:16 +02:00
const output = (
await db
. select ( {
2024-04-17 08:36:01 +02:00
token : Tokens ,
application : Applications ,
2024-04-16 04:09:16 +02:00
} )
2024-04-17 08:36:01 +02:00
. from ( Tokens )
. leftJoin ( Applications , eq ( Tokens . applicationId , Applications . id ) )
2024-06-13 04:26:43 +02:00
. where ( eq ( Tokens . accessToken , accessToken ) )
2024-04-16 04:09:16 +02:00
. limit ( 1 )
) [ 0 ] ;
2024-06-13 04:26:43 +02:00
if ( ! output ? . token . userId ) {
return { user : null , application : null } ;
}
2024-04-16 04:09:16 +02:00
2024-04-25 05:40:27 +02:00
const user = await User . fromId ( output . token . userId ) ;
2024-04-16 04:09:16 +02:00
return { user , application : output.application ? ? null } ;
} ;
export const retrieveToken = async (
2024-06-13 04:26:43 +02:00
accessToken : string ,
2024-04-16 04:09:16 +02:00
) : Promise < Token | null > = > {
2024-06-13 04:26:43 +02:00
if ( ! accessToken ) {
return null ;
}
2024-04-16 04:09:16 +02:00
return (
2024-04-17 08:36:01 +02:00
( await db . query . Tokens . findFirst ( {
2024-06-13 04:26:43 +02:00
where : ( tokens , { eq } ) = > eq ( tokens . accessToken , accessToken ) ,
2024-04-16 04:09:16 +02:00
} ) ) ? ? null
) ;
} ;
2023-11-11 03:36:06 +01:00
/ * *
* Gets the relationship to another user .
* @param other The other user to get the relationship to .
* @returns The relationship to the other user .
* /
export const getRelationshipToOtherUser = async (
2024-04-25 05:40:27 +02:00
user : User ,
2024-04-07 07:30:49 +02:00
other : User ,
2024-06-12 00:32:38 +02:00
) : Promise < Relationship > = > {
2024-06-12 01:42:36 +02:00
await checkForBidirectionalRelationships ( user , other ) ;
2024-04-17 08:36:01 +02:00
const foundRelationship = await db . query . Relationships . findFirst ( {
2024-04-11 13:39:07 +02:00
where : ( relationship , { and , eq } ) = >
and (
eq ( relationship . ownerId , user . id ) ,
eq ( relationship . subjectId , other . id ) ,
) ,
2024-04-07 07:30:49 +02:00
} ) ;
2024-04-09 04:12:54 +02:00
2024-04-11 13:39:07 +02:00
if ( ! foundRelationship ) {
2024-04-09 04:12:54 +02:00
// Create new relationship
const newRelationship = await createNewRelationship ( user , other ) ;
return newRelationship ;
}
2024-04-11 13:39:07 +02:00
return foundRelationship ;
2023-11-11 03:36:06 +01:00
} ;
2023-09-22 03:09:14 +02:00
2024-04-10 07:51:00 +02:00
export const followRequestToLysand = (
follower : User ,
followee : User ,
2024-06-20 01:21:02 +02:00
) : Follow = > {
2024-04-25 05:40:27 +02:00
if ( follower . isRemote ( ) ) {
2024-04-10 07:51:00 +02:00
throw new Error ( "Follower must be a local user" ) ;
}
2024-04-25 05:40:27 +02:00
if ( ! followee . isRemote ( ) ) {
2024-04-10 07:51:00 +02:00
throw new Error ( "Followee must be a remote user" ) ;
}
2024-06-13 02:45:07 +02:00
if ( ! followee . data . uri ) {
2024-04-10 07:51:00 +02:00
throw new Error ( "Followee must have a URI in database" ) ;
}
const id = crypto . randomUUID ( ) ;
return {
type : "Follow" ,
id : id ,
2024-04-25 05:40:27 +02:00
author : follower.getUri ( ) ,
followee : followee.getUri ( ) ,
2024-04-10 07:51:00 +02:00
created_at : new Date ( ) . toISOString ( ) ,
uri : new URL ( ` /follows/ ${ id } ` , config . http . base_url ) . toString ( ) ,
} ;
} ;
export const followAcceptToLysand = (
follower : User ,
followee : User ,
2024-06-20 01:21:02 +02:00
) : FollowAccept = > {
2024-04-25 05:40:27 +02:00
if ( ! follower . isRemote ( ) ) {
2024-04-10 09:18:41 +02:00
throw new Error ( "Follower must be a remote user" ) ;
2024-04-10 07:51:00 +02:00
}
2024-04-25 05:40:27 +02:00
if ( followee . isRemote ( ) ) {
2024-04-10 09:18:41 +02:00
throw new Error ( "Followee must be a local user" ) ;
2024-04-10 07:51:00 +02:00
}
2024-06-13 02:45:07 +02:00
if ( ! follower . data . uri ) {
2024-04-10 09:18:41 +02:00
throw new Error ( "Follower must have a URI in database" ) ;
2024-04-10 07:51:00 +02:00
}
const id = crypto . randomUUID ( ) ;
return {
type : "FollowAccept" ,
id : id ,
2024-04-25 05:40:27 +02:00
author : followee.getUri ( ) ,
2024-04-10 07:51:00 +02:00
created_at : new Date ( ) . toISOString ( ) ,
2024-04-25 05:40:27 +02:00
follower : follower.getUri ( ) ,
2024-04-10 07:51:00 +02:00
uri : new URL ( ` /follows/ ${ id } ` , config . http . base_url ) . toString ( ) ,
} ;
} ;
2024-04-10 10:07:03 +02:00
export const followRejectToLysand = (
follower : User ,
followee : User ,
2024-06-20 01:21:02 +02:00
) : FollowReject = > {
2024-04-10 10:07:03 +02:00
return {
. . . followAcceptToLysand ( follower , followee ) ,
type : "FollowReject" ,
} ;
} ;