Merge branch 'notification-read-api' into swn

This commit is contained in:
tamaina
2021-12-05 12:47:24 +09:00
95 changed files with 1935 additions and 1162 deletions

View File

@@ -19,6 +19,7 @@ export type FileInfo = {
};
width?: number;
height?: number;
orientation?: number;
blurhash?: string;
warnings: string[];
};
@@ -47,6 +48,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
// image dimensions
let width: number | undefined;
let height: number | undefined;
let orientation: number | undefined;
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) {
const imageSize = await detectImageSize(path).catch(e => {
@@ -61,6 +63,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
} else if (imageSize.wUnits === 'px') {
width = imageSize.width;
height = imageSize.height;
orientation = imageSize.orientation;
// 制限を超えている画像は octet-stream にする
if (imageSize.width > 16383 || imageSize.height > 16383) {
@@ -87,6 +90,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
type,
width,
height,
orientation,
blurhash,
warnings,
};
@@ -163,6 +167,7 @@ async function detectImageSize(path: string): Promise<{
height: number;
wUnits: string;
hUnits: string;
orientation?: number;
}> {
const readable = fs.createReadStream(path);
const imageSize = await probeImageSize(readable);

View File

@@ -77,7 +77,7 @@ export class DriveFile {
default: {},
comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
})
public properties: { width?: number; height?: number; avgColor?: string };
public properties: { width?: number; height?: number; orientation?: number; avgColor?: string };
@Index()
@Column('boolean')

View File

@@ -28,6 +28,19 @@ export class DriveFileRepository extends Repository<DriveFile> {
);
}
public getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) {
const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width];
}
properties.orientation = undefined;
return properties;
}
return file.properties;
}
public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
// リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
@@ -122,7 +135,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
size: file.size,
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: file.properties,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
thumbnailUrl: this.getPublicUrl(file, true, meta),
comment: file.comment,
@@ -202,6 +215,11 @@ export const packedDriveFileSchema = {
optional: true as const, nullable: false as const,
example: 720
},
orientation: {
type: 'number' as const,
optional: true as const, nullable: false as const,
example: 8
},
avgColor: {
type: 'string' as const,
optional: true as const, nullable: false as const,

View File

@@ -189,12 +189,12 @@ export class UserRepository extends Repository<User> {
const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
null;
const followersCount = profile == null ? null :
(profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
const falsy = opts.detail ? false : undefined;

View File

@@ -1,8 +1,9 @@
import { IRemoteUser } from '@/models/entities/user';
import reject from '@/services/following/requests/reject';
import { remoteReject } from '@/services/following/reject';
import { IFollow } from '../../type';
import DbResolver from '../../db-resolver';
import { relayRejected } from '@/services/relay';
import { Users } from '@/models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
@@ -14,7 +15,7 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return `skip: follower not found`;
}
if (follower.host != null) {
if (!Users.isLocalUser(follower)) {
return `skip: follower is not a local user`;
}
@@ -24,6 +25,6 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return await relayRejected(match[1]);
}
await reject(actor, follower);
await remoteReject(actor, follower);
return `ok`;
};

View File

@@ -0,0 +1,27 @@
import unfollow from '@/services/following/delete';
import cancelRequest from '@/services/following/requests/cancel';
import {IAccept} from '../../type';
import { IRemoteUser } from '@/models/entities/user';
import { Followings } from '@/models/index';
import DbResolver from '../../db-resolver';
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.object);
if (follower == null) {
return `skip: follower not found`;
}
const following = await Followings.findOne({
followerId: follower.id,
followeeId: actor.id
});
if (following) {
await unfollow(follower, actor);
return `ok: unfollowed`;
}
return `skip: フォローされていない`;
};

View File

@@ -1,8 +1,9 @@
import { IRemoteUser } from '@/models/entities/user';
import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type';
import {IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept} from '../../type';
import unfollow from './follow';
import unblock from './block';
import undoLike from './like';
import undoAccept from './accept';
import { undoAnnounce } from './announce';
import Resolver from '../../resolver';
import { apLogger } from '../../logger';
@@ -29,6 +30,7 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
if (isBlock(object)) return await unblock(actor, object);
if (isLike(object)) return await undoLike(actor, object);
if (isAnnounce(object)) return await undoAnnounce(actor, object);
if (isAccept(object)) return await undoAccept(actor, object);
return `skip: unknown object type ${getApType(object)}`;
};

View File

@@ -0,0 +1,82 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import * as ms from 'ms';
import deleteFollowing from '@/services/following/delete';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
import { Followings, Users } from '@/models/index';
export const meta = {
tags: ['following', 'users'],
limit: {
duration: ms('1hour'),
max: 100
},
requireCredential: true as const,
kind: 'write:following',
params: {
userId: {
validator: $.type(ID),
}
},
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8'
},
followerIsYourself: {
message: 'Follower is yourself.',
code: 'FOLLOWER_IS_YOURSELF',
id: '07dc03b9-03da-422d-885b-438313707662'
},
notFollowing: {
message: 'The other use is not following you.',
code: 'NOT_FOLLOWING',
id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09'
},
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User'
}
};
export default define(meta, async (ps, user) => {
const followee = user;
// Check if the follower is yourself
if (user.id === ps.userId) {
throw new ApiError(meta.errors.followerIsYourself);
}
// Get follower
const follower = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
// Check not following
const exist = await Followings.findOne({
followerId: follower.id,
followeeId: followee.id
});
if (exist == null) {
throw new ApiError(meta.errors.notFollowing);
}
await deleteFollowing(follower, followee);
return await Users.pack(followee.id, user);
});

View File

@@ -1,6 +1,6 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import rejectFollowRequest from '@/services/following/requests/reject';
import { rejectFollowRequest } from '@/services/following/reject';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';

View File

@@ -372,12 +372,16 @@ export default async function(
const properties: {
width?: number;
height?: number;
orientation?: number;
} = {};
if (info.width) {
properties['width'] = info.width;
properties['height'] = info.height;
}
if (info.orientation != null) {
properties['orientation'] = info.orientation;
}
const profile = user ? await UserProfiles.findOne(user.id) : null;

View File

@@ -2,6 +2,7 @@ import { publishMainStream, publishUserEvent } from '@/services/stream';
import { renderActivity } from '@/remote/activitypub/renderer/index';
import renderFollow from '@/remote/activitypub/renderer/follow';
import renderUndo from '@/remote/activitypub/renderer/undo';
import renderReject from '@/remote/activitypub/renderer/reject';
import { deliver } from '@/queue/index';
import Logger from '../logger';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
@@ -40,6 +41,12 @@ export default async function(follower: { id: User['id']; host: User['host']; ur
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
if (Users.isLocalUser(followee) && Users.isRemoteUser(follower)) {
// local user has null host
const content = renderActivity(renderReject(renderFollow(follower, followee), followee));
deliver(followee, content, follower.inbox);
}
}
export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) {

View File

@@ -0,0 +1,105 @@
import { renderActivity } from '@/remote/activitypub/renderer/index';
import renderFollow from '@/remote/activitypub/renderer/follow';
import renderReject from '@/remote/activitypub/renderer/reject';
import { deliver } from '@/queue/index';
import { publishMainStream, publishUserEvent } from '@/services/stream';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
import { Users, FollowRequests, Followings } from '@/models/index';
import { decrementFollowing } from './delete';
type Local = ILocalUser | { id: User['id']; host: User['host']; uri: User['host'] };
type Remote = IRemoteUser;
type Both = Local | Remote;
/**
* API following/request/reject
*/
export async function rejectFollowRequest(user: Local, follower: Both) {
if (Users.isRemoteUser(follower)) {
deliverReject(user, follower);
}
await removeFollowRequest(user, follower);
if (Users.isLocalUser(follower)) {
publishUnfollow(user, follower);
}
}
/**
* API following/reject
*/
export async function rejectFollow(user: Local, follower: Both) {
if (Users.isRemoteUser(follower)) {
deliverReject(user, follower);
}
await removeFollow(user, follower);
if (Users.isLocalUser(follower)) {
publishUnfollow(user, follower);
}
}
/**
* AP Reject/Follow
*/
export async function remoteReject(actor: Remote, follower: Local) {
await removeFollowRequest(actor, follower);
await removeFollow(actor, follower);
publishUnfollow(actor, follower);
}
/**
* Remove follow request record
*/
async function removeFollowRequest(followee: Both, follower: Both) {
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (!request) return;
await FollowRequests.delete(request.id);
}
/**
* Remove follow record
*/
async function removeFollow(followee: Both, follower: Both) {
const following = await Followings.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (!following) return;
await Followings.delete(following.id);
decrementFollowing(follower, followee);
}
/**
* Deliver Reject to remote
*/
async function deliverReject(followee: Local, follower: Remote) {
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
const content = renderActivity(renderReject(renderFollow(follower, followee, request?.requestId || undefined), followee));
deliver(followee, content, follower.inbox);
}
/**
* Publish unfollow to local
*/
async function publishUnfollow(followee: Both, follower: Local) {
const packedFollowee = await Users.pack(followee.id, follower, {
detail: true
});
publishUserEvent(follower.id, 'unfollow', packedFollowee);
publishMainStream(follower.id, 'unfollow', packedFollowee);
}

View File

@@ -1,46 +0,0 @@
import { renderActivity } from '@/remote/activitypub/renderer/index';
import renderFollow from '@/remote/activitypub/renderer/follow';
import renderReject from '@/remote/activitypub/renderer/reject';
import { deliver } from '@/queue/index';
import { publishMainStream, publishUserEvent } from '@/services/stream';
import { User, ILocalUser } from '@/models/entities/user';
import { Users, FollowRequests, Followings } from '@/models/index';
import { decrementFollowing } from '../delete';
export default async function(followee: { id: User['id']; host: User['host']; uri: User['host'] }, follower: User) {
if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
const content = renderActivity(renderReject(renderFollow(follower, followee, request!.requestId!), followee));
deliver(followee, content, follower.inbox);
}
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (request) {
await FollowRequests.delete(request.id);
} else {
const following = await Followings.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (following) {
await Followings.delete(following.id);
decrementFollowing(follower, followee);
}
}
Users.pack(followee.id, follower, {
detail: true
}).then(packed => {
publishUserEvent(follower.id, 'unfollow', packed);
publishMainStream(follower.id, 'unfollow', packed);
});
}

View File

@@ -17,6 +17,7 @@ describe('Get file info', () => {
},
width: undefined,
height: undefined,
orientation: undefined,
});
}));
@@ -34,6 +35,7 @@ describe('Get file info', () => {
},
width: 512,
height: 512,
orientation: undefined,
});
}));
@@ -51,6 +53,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@@ -68,6 +71,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@@ -85,6 +89,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@@ -102,6 +107,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@@ -120,6 +126,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@@ -137,6 +144,25 @@ describe('Get file info', () => {
},
width: 25000,
height: 25000,
orientation: undefined,
});
}));
it('Rotate JPEG', async (async () => {
const path = `${__dirname}/resources/rotate.jpg`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 12624,
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
type: {
mime: 'image/jpeg',
ext: 'jpg'
},
width: 512,
height: 256,
orientation: 8,
});
}));
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB