diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index f47ed88ae..a336ba12b 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -8,7 +8,6 @@ import { UserConverter } from "@/server/api/mastodon/converters/user.js"; import authenticate from "@/server/api/authenticate.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; -import config from "@/config/index.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; const relationshipModel = { @@ -254,16 +253,19 @@ export function apiAccountMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/accounts/:id/follow", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); try { - const data = await client.followAccount( - convertId(ctx.params.id, IdType.IceshrimpId), - ); - let acct = convertRelationship(data.data); - acct.following = true; - ctx.body = acct; + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? null; + + if (!user) { + ctx.status = 401; + return; + } + + const target = await UserHelpers.getUserCached(convertId(ctx.params.id, IdType.IceshrimpId)); + //FIXME: Parse form data + const result = await UserHelpers.followUser(target, user, true, false); + ctx.body = convertRelationship(result); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -275,16 +277,18 @@ export function apiAccountMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/accounts/:id/unfollow", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); try { - const data = await client.unfollowAccount( - convertId(ctx.params.id, IdType.IceshrimpId), - ); - let acct = convertRelationship(data.data); - acct.following = false; - ctx.body = acct; + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? null; + + if (!user) { + ctx.status = 401; + return; + } + + const target = await UserHelpers.getUserCached(convertId(ctx.params.id, IdType.IceshrimpId)); + const result = await UserHelpers.unfollowUser(target, user); + ctx.body = convertRelationship(result); } catch (e: any) { console.error(e); console.error(e.response.data); diff --git a/packages/backend/src/server/api/mastodon/entities/relationship.ts b/packages/backend/src/server/api/mastodon/entities/relationship.ts index 8cb18c460..bca2f4cba 100644 --- a/packages/backend/src/server/api/mastodon/entities/relationship.ts +++ b/packages/backend/src/server/api/mastodon/entities/relationship.ts @@ -3,7 +3,6 @@ namespace MastodonEntity { id: string; following: boolean; followed_by: boolean; - delivery_following?: boolean; blocking: boolean; blocked_by: boolean; muting: boolean; @@ -13,5 +12,6 @@ namespace MastodonEntity { showing_reblogs: boolean; endorsed: boolean; notifying: boolean; + note: string; }; } diff --git a/packages/backend/src/server/api/mastodon/helpers/user.ts b/packages/backend/src/server/api/mastodon/helpers/user.ts index 564a9d3a0..55b3a3ab3 100644 --- a/packages/backend/src/server/api/mastodon/helpers/user.ts +++ b/packages/backend/src/server/api/mastodon/helpers/user.ts @@ -1,6 +1,14 @@ import { Note } from "@/models/entities/note.js"; import { ILocalUser, User } from "@/models/entities/user.js"; -import { Followings, NoteFavorites, NoteReactions, Notes, UserProfiles } from "@/models/index.js"; +import { + Followings, + FollowRequests, + NoteFavorites, + NoteReactions, + Notes, + UserProfiles, + Users +} from "@/models/index.js"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js"; import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js"; @@ -11,6 +19,10 @@ import Entity from "megalodon/src/entity.js"; import AsyncLock from "async-lock"; import { getUser } from "@/server/api/common/getters.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; +import { awaitAll } from "@/prelude/await-all.js"; +import createFollowing from "@/services/following/create.js"; +import deleteFollowing from "@/services/following/delete.js"; +import cancelFollowRequest from "@/services/following/requests/cancel.js"; export type AccountCache = { locks: AsyncLock; @@ -27,6 +39,27 @@ export type LinkPaginationObject = { type RelationshipType = 'followers' | 'following'; export class UserHelpers { + public static async followUser(target: User, localUser: ILocalUser, reblogs: boolean, notify: boolean) { + //FIXME: implement reblogs & notify params + const following = await Followings.exist({where: {followerId: localUser.id, followeeId: target.id}}); + const requested = await FollowRequests.exist({where: {followerId: localUser.id, followeeId: target.id}}); + if (!following && !requested) + await createFollowing(localUser, target); + + return this.getUserRelationshipTo(target, localUser); + } + + public static async unfollowUser(target: User, localUser: ILocalUser) { + const following = await Followings.exist({where: {followerId: localUser.id, followeeId: target.id}}); + const requested = await FollowRequests.exist({where: {followerId: localUser.id, followeeId: target.id}}); + if (following) + await deleteFollowing(localUser, target); + if (requested) + await cancelFollowRequest(target, localUser); + + return this.getUserRelationshipTo(target, localUser); + } + public static async getUserStatuses(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, excludeReplies: boolean = false, excludeReblogs: boolean = false, pinned: boolean = false, tagged: string | undefined): Promise { if (limit > 40) limit = 40; @@ -172,6 +205,27 @@ export class UserHelpers { return this.getUserRelationships('following', user, localUser, maxId, sinceId, minId, limit); } + public static async getUserRelationshipTo(target: User, localUser: ILocalUser): Promise { + const relation = await Users.getRelation(localUser.id, target.id); + const response = { + id: target.id, + following: relation.isFollowing, + followed_by: relation.isFollowed, + blocking: relation.isBlocking, + blocked_by: relation.isBlocked, + muting: relation.isMuted, + muting_notifications: relation.isMuted, + requested: relation.hasPendingFollowRequestFromYou, + domain_blocking: false, //FIXME + showing_reblogs: !relation.isRenoteMuted, + endorsed: false, + notifying: false, //FIXME + note: '' //FIXME + } + + return awaitAll(response); + } + public static async getUserCached(id: string, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise { return cache.locks.acquire(id, async () => { const cacheHit = cache.users.find(p => p.id == id);