Compare commits
8 commits
6501c542b2
...
ac1ef641f5
Author | SHA1 | Date | |
---|---|---|---|
ac1ef641f5 | |||
1af0687423 | |||
09ff7f0c7d | |||
f285281b5a | |||
624157f03e | |||
e366116ac1 | |||
2b5a35147a | |||
1098b3a038 |
17 changed files with 69 additions and 63 deletions
|
@ -494,6 +494,8 @@ output: "Output"
|
|||
updateRemoteUser: "Update remote user information"
|
||||
deleteAllFiles: "Delete all files"
|
||||
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
||||
deleteAllNotes: "Delete all notes"
|
||||
deleteAllNotesConfirm: "Are you sure that you want to delete all visible notes of this clip?"
|
||||
removeAllFollowing: "Unfollow all followed users"
|
||||
removeAllFollowingDescription: "Executing this unfollows all accounts from {host}.\
|
||||
\ Please run this if the instance e.g. no longer exists."
|
||||
|
|
|
@ -82,7 +82,7 @@ export class Cache<T> {
|
|||
// Items may have been removed in the meantime or this may be
|
||||
// the initial call for the first key inserted into the cache.
|
||||
const [expiredKey, expiredValue] = this.cache.entries().next().value;
|
||||
if (expiredValue.date + this.lifetime >= Date.now()) {
|
||||
if (expiredValue.date + this.lifetime <= Date.now()) {
|
||||
// This item is due for expiration, so remove it.
|
||||
this.cache.delete(expiredKey);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Instances } from '@/models/index.js';
|
|||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||
import { toPuny } from '@/misc/convert-host.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||
import { shouldSkipInstance } from '@/misc/skipped-instances.js';
|
||||
import { DeliverJobData } from '@/queue/types.js';
|
||||
|
||||
|
@ -18,13 +19,15 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
|||
|
||||
if (await shouldSkipInstance(puny)) return 'skip';
|
||||
|
||||
const keypair = await getUserKeypair(job.data.user.id);
|
||||
|
||||
try {
|
||||
if (Array.isArray(job.data.content)) {
|
||||
await Promise.all(
|
||||
job.data.content.map(x => request(job.data.user, job.data.to, x))
|
||||
job.data.content.map(x => request(job.data.user, job.data.to, x, keypair))
|
||||
);
|
||||
} else {
|
||||
await request(job.data.user, job.data.to, job.data.content);
|
||||
await request(job.data.user, job.data.to, job.data.content, keypair);
|
||||
}
|
||||
|
||||
// Update stats
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { toArray } from '@/prelude/array.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||
import { apLogger } from '../logger.js';
|
||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, isMove, getApId } from '../type.js';
|
||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isFlag, isMove, getApId } from '../type.js';
|
||||
import create from './create/index.js';
|
||||
import performDeleteActivity from './delete/index.js';
|
||||
import performUpdateActivity from './update/index.js';
|
||||
|
@ -22,23 +21,6 @@ import flag from './flag/index.js';
|
|||
import { move } from './move/index.js';
|
||||
|
||||
export async function performActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||
const act = await resolver.resolve(item);
|
||||
try {
|
||||
await performOneActivity(actor, act, resolver);
|
||||
} catch (err) {
|
||||
if (err instanceof Error || typeof err === 'string') {
|
||||
apLogger.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await performOneActivity(actor, activity, resolver);
|
||||
}
|
||||
}
|
||||
|
||||
async function performOneActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
||||
if (actor.isSuspended) return;
|
||||
|
||||
if (typeof activity.id !== 'undefined') {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { URL } from 'node:url';
|
|||
import config from '@/config/index.js';
|
||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
||||
import { getResponse } from '@/misc/fetch.js';
|
||||
import { createSignedPost, createSignedGet } from './ap-request.js';
|
||||
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
|
||||
|
@ -14,11 +15,9 @@ import { apRequestChart, federationChart, instanceChart } from '@/services/chart
|
|||
* @param url The URL of the inbox.
|
||||
* @param object The Activity or other object to be posted to the inbox.
|
||||
*/
|
||||
export async function request(user: { id: User['id'] }, url: string, object: any): Promise<void> {
|
||||
export async function request(user: { id: User['id'] }, url: string, object: any, keypair: UserKeypair): Promise<void> {
|
||||
const body = JSON.stringify(object);
|
||||
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
const req = createSignedPost({
|
||||
key: {
|
||||
privateKeyPem: keypair.privateKey,
|
||||
|
|
|
@ -55,6 +55,18 @@ function isActivityPubReq(ctx: Router.RouterContext): boolean {
|
|||
return typeof accepted === 'string' && !accepted.match(/html/);
|
||||
}
|
||||
|
||||
export function denyActivityPub() {
|
||||
return async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
|
||||
// Clients are required to set the `Accept` header to an Activitypub content type.
|
||||
// If such a content type negotiation header is received on an unexpected route,
|
||||
// something seems to be fishy and we should not respond with content. A user
|
||||
// might have e.g. uploaded a malicious file that looks like an activity.
|
||||
ctx.status = 406;
|
||||
};
|
||||
}
|
||||
|
||||
export function setResponseType(ctx: Router.RouterContext): void {
|
||||
const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON);
|
||||
if (accept === LD_JSON) {
|
||||
|
|
|
@ -10,7 +10,6 @@ import { toArray } from '@/prelude/array.js';
|
|||
import { renderReadActivity } from '@/remote/activitypub/renderer/read.js';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import { deliver } from '@/queue/index.js';
|
||||
import orderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
|
||||
|
||||
/**
|
||||
* Mark messages as read
|
||||
|
@ -133,18 +132,3 @@ export async function readGroupMessagingMessage(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) {
|
||||
const contents = toArray(messages)
|
||||
.filter(x => x.uri)
|
||||
.map(x => renderReadActivity(user, x));
|
||||
|
||||
if (contents.length > 1) {
|
||||
const collection = orderedCollection(null, contents.length, undefined, undefined, contents);
|
||||
deliver(user, renderActivity(collection), recipient.inbox);
|
||||
} else {
|
||||
for (const content of contents) {
|
||||
deliver(user, renderActivity(content), recipient.inbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
});
|
||||
|
||||
/***
|
||||
* URIからUserかNoteを解決する
|
||||
* Resolve a User or Note from a given URI
|
||||
*/
|
||||
async function fetchAny(uri: string, me: ILocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||
// Stop if the host is blocked.
|
||||
|
@ -92,6 +92,7 @@ async function fetchAny(uri: string, me: ILocalUser | null | undefined): Promise
|
|||
return null;
|
||||
}
|
||||
|
||||
// first try to fetch the object from the database
|
||||
const dbResolver = new DbResolver();
|
||||
|
||||
let local = await mergePack(me, ...await Promise.all([
|
||||
|
@ -100,13 +101,15 @@ async function fetchAny(uri: string, me: ILocalUser | null | undefined): Promise
|
|||
]));
|
||||
if (local != null) return local;
|
||||
|
||||
// fetch object from remote
|
||||
// getting the object from the database failed, fetch from remote
|
||||
const resolver = new Resolver();
|
||||
// allow redirect
|
||||
const object = await resolver.resolve(uri, true) as any;
|
||||
|
||||
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
||||
// これはDBに存在する可能性があるため再度DB検索
|
||||
// If a URI other than the canonical id such as `/@user` is specified,
|
||||
// the canonical URI is determined here for the first time.
|
||||
//
|
||||
// DB search again, since this may exist in the DB
|
||||
if (uri !== object.id) {
|
||||
local = await mergePack(me, ...await Promise.all([
|
||||
dbResolver.getUserFromApId(object.id),
|
||||
|
|
|
@ -35,6 +35,14 @@ export const paramDef = {
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
/*
|
||||
Note: It should not be allowed for the actual file contents to be updated.
|
||||
Not allowing the user to change the contents after the public URL has been determined
|
||||
is relevant because it is a defense mechanism against AcitivtyPub content "impersonation".
|
||||
|
||||
If the URL is known, an integrity check could be defeated which checks that the `id`
|
||||
indicated in an ActivityPub object is actually retrievable at that given `id`.
|
||||
*/
|
||||
const file = await DriveFiles.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (file == null) throw new ApiError('NO_SUCH_FILE');
|
||||
|
|
|
@ -4,7 +4,7 @@ import define from '@/server/api/define.js';
|
|||
import { ApiError } from '@/server/api/error.js';
|
||||
import { getUser } from '@/server/api/common/getters.js';
|
||||
import { makePaginationQuery } from '@/server/api/common/make-pagination-query.js';
|
||||
import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '@/server/api/common/read-messaging-message.js';
|
||||
import { readUserMessagingMessage, readGroupMessagingMessage } from '@/server/api/common/read-messaging-message.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['messaging'],
|
||||
|
@ -75,11 +75,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
// Mark all as read
|
||||
if (ps.markAsRead) {
|
||||
readUserMessagingMessage(user.id, recipient.id, messages.filter(m => m.recipientId === user.id).map(x => x.id));
|
||||
|
||||
// リモートユーザーとのメッセージだったら既読配信
|
||||
if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) {
|
||||
deliverReadActivity(user, recipient, messages);
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
|
||||
|
|
|
@ -10,6 +10,7 @@ import cors from '@koa/cors';
|
|||
|
||||
import { Instances, AccessTokens, Users } from '@/models/index.js';
|
||||
import config from '@/config/index.js';
|
||||
import { denyActivityPub } from '@/server/activitypub.js';
|
||||
import { endpoints } from './endpoints.js';
|
||||
import { handler } from './api-handler.js';
|
||||
import signup from './private/signup.js';
|
||||
|
@ -24,6 +25,7 @@ const app = new Koa();
|
|||
app.use(cors({
|
||||
origin: '*',
|
||||
}));
|
||||
app.use(denyActivityPub());
|
||||
|
||||
// No caching
|
||||
app.use(async (ctx, next) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index.js';
|
||||
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { UserGroup } from '@/models/entities/user-group.js';
|
||||
import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '@/server/api/common/read-messaging-message.js';
|
||||
import { readUserMessagingMessage, readGroupMessagingMessage } from '@/server/api/common/read-messaging-message.js';
|
||||
import Channel from '@/server/api/stream/channel.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
|
||||
|
@ -69,13 +69,6 @@ export default class extends Channel {
|
|||
case 'read':
|
||||
if (this.otherpartyId) {
|
||||
readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]);
|
||||
|
||||
// リモートユーザーからのメッセージだったら既読配信
|
||||
if (Users.isLocalUser(this.user!) && Users.isRemoteUser(this.otherparty!)) {
|
||||
MessagingMessages.findOneBy({ id: body.id }).then(message => {
|
||||
if (message) deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message);
|
||||
});
|
||||
}
|
||||
} else if (this.groupId) {
|
||||
readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { dirname } from 'node:path';
|
|||
import Koa from 'koa';
|
||||
import cors from '@koa/cors';
|
||||
import Router from '@koa/router';
|
||||
import { denyActivityPub } from '@/server/activitypub.js';
|
||||
import { sendDriveFile } from './send-drive-file.js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
|
@ -16,6 +17,7 @@ const _dirname = dirname(_filename);
|
|||
// Init app
|
||||
const app = new Koa();
|
||||
app.use(cors());
|
||||
app.use(denyActivityPub());
|
||||
app.use(async (ctx, next) => {
|
||||
ctx.set('Content-Security-Policy', "default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'");
|
||||
await next();
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
import Koa from 'koa';
|
||||
import cors from '@koa/cors';
|
||||
import Router from '@koa/router';
|
||||
import { denyActivityPub } from '@/server/activitypub.js';
|
||||
import { proxyMedia } from './proxy-media.js';
|
||||
|
||||
// Init app
|
||||
const app = new Koa();
|
||||
app.use(cors());
|
||||
app.use(denyActivityPub());
|
||||
app.use(async (ctx, next) => {
|
||||
ctx.set('Content-Security-Policy', "default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'");
|
||||
await next();
|
||||
|
|
|
@ -11,7 +11,7 @@ const cache = new Cache<Instance>(
|
|||
if (host == null) return undefined;
|
||||
const res = await Instances.findOneBy({ host });
|
||||
return res ?? undefined;
|
||||
} ,
|
||||
},
|
||||
);
|
||||
|
||||
export async function registerOrFetchInstanceDoc(idnHost: string): Promise<Instance> {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IsNull } from 'typeorm';
|
||||
import { ILocalUser, User } from '@/models/entities/user.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import { Users, UserPublickeys } from '@/models/index.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { subscriber } from '@/db/redis.js';
|
||||
import { MINUTE } from '@/const.js';
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<XNotes :pagination="pagination" :detail="true"/>
|
||||
<XNotes ref="tlComponent" :pagination="pagination" :detail="true"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
@ -39,6 +39,7 @@ const pagination = {
|
|||
clipId: props.clipId,
|
||||
})),
|
||||
};
|
||||
const tlComponent = $ref();
|
||||
|
||||
const isOwned: boolean | null = $computed<boolean | null>(() => $i && clip && ($i.id === clip.userId));
|
||||
|
||||
|
@ -52,7 +53,25 @@ watch(() => props.clipId, async () => {
|
|||
|
||||
provide('currentClipPage', $$(clip));
|
||||
|
||||
const headerActions = $computed(() => clip && isOwned ? [{
|
||||
const headerActions = $computed(() => clip && isOwned ? [
|
||||
...($i.isAdmin ? [{
|
||||
icon: 'fas fa-dumpster',
|
||||
text: i18n.ts.deleteAllNotes,
|
||||
handler: async (): Promise<void> => {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteAllNotesConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('notes/delete-many', {
|
||||
noteIds: tlComponent.pagingComponent?.items.map(item => item.id)
|
||||
});
|
||||
|
||||
tlComponent.pagingComponent?.reload();
|
||||
},
|
||||
}] : []),
|
||||
{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
handler: async (): Promise<void> => {
|
||||
|
|
Loading…
Add table
Reference in a new issue