feat: Webhook (#8457)
* feat: introduce webhook * wip * wip * wip * Update CHANGELOG.md
This commit is contained in:
@ -73,6 +73,7 @@ import { PasswordResetRequest } from '@/models/entities/password-reset-request.j
|
||||
import { UserPending } from '@/models/entities/user-pending.js';
|
||||
|
||||
import { entities as charts } from '@/services/chart/entities.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
||||
|
||||
@ -171,6 +172,7 @@ export const entities = [
|
||||
Ad,
|
||||
PasswordResetRequest,
|
||||
UserPending,
|
||||
Webhook,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
49
packages/backend/src/misc/webhook-cache.ts
Normal file
49
packages/backend/src/misc/webhook-cache.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
import { subsdcriber } from '../db/redis.js';
|
||||
|
||||
let webhooksFetched = false;
|
||||
let webhooks: Webhook[] = [];
|
||||
|
||||
export async function getActiveWebhooks() {
|
||||
if (!webhooksFetched) {
|
||||
webhooks = await Webhooks.findBy({
|
||||
active: true,
|
||||
});
|
||||
webhooksFetched = true;
|
||||
}
|
||||
|
||||
return webhooks;
|
||||
}
|
||||
|
||||
subsdcriber.on('message', async (_, data) => {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message;
|
||||
switch (type) {
|
||||
case 'webhookCreated':
|
||||
if (body.active) {
|
||||
webhooks.push(body);
|
||||
}
|
||||
break;
|
||||
case 'webhookUpdated':
|
||||
if (body.active) {
|
||||
const i = webhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
webhooks[i] = body;
|
||||
} else {
|
||||
webhooks.push(body);
|
||||
}
|
||||
} else {
|
||||
webhooks = webhooks.filter(a => a.id !== body.id);
|
||||
}
|
||||
break;
|
||||
case 'webhookDeleted':
|
||||
webhooks = webhooks.filter(a => a.id !== body.id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
73
packages/backend/src/models/entities/webhook.ts
Normal file
73
packages/backend/src/models/entities/webhook.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { User } from './user.js';
|
||||
import { id } from '../id.js';
|
||||
|
||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
||||
|
||||
@Entity()
|
||||
export class Webhook {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the Antenna.',
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The owner ID.',
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
comment: 'The name of the Antenna.',
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, array: true, default: '{}',
|
||||
})
|
||||
public on: (typeof webhookEventTypes)[number][];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public url: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public secret: string;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public active: boolean;
|
||||
|
||||
/**
|
||||
* 直近のリクエスト送信日時
|
||||
*/
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public latestSentAt: Date | null;
|
||||
|
||||
/**
|
||||
* 直近のリクエスト送信時のHTTPステータスコード
|
||||
*/
|
||||
@Column('integer', {
|
||||
nullable: true,
|
||||
})
|
||||
public latestStatus: number | null;
|
||||
}
|
@ -64,6 +64,7 @@ import { Ad } from './entities/ad.js';
|
||||
import { PasswordResetRequest } from './entities/password-reset-request.js';
|
||||
import { UserPending } from './entities/user-pending.js';
|
||||
import { InstanceRepository } from './repositories/instance.js';
|
||||
import { Webhook } from './entities/webhook.js';
|
||||
|
||||
export const Announcements = db.getRepository(Announcement);
|
||||
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
||||
@ -125,5 +126,6 @@ export const Channels = (ChannelRepository);
|
||||
export const ChannelFollowings = db.getRepository(ChannelFollowing);
|
||||
export const ChannelNotePinings = db.getRepository(ChannelNotePining);
|
||||
export const RegistryItems = db.getRepository(RegistryItem);
|
||||
export const Webhooks = db.getRepository(Webhook);
|
||||
export const Ads = db.getRepository(Ad);
|
||||
export const PasswordResetRequests = db.getRepository(PasswordResetRequest);
|
||||
|
@ -8,13 +8,15 @@ import processInbox from './processors/inbox.js';
|
||||
import processDb from './processors/db/index.js';
|
||||
import processObjectStorage from './processors/object-storage/index.js';
|
||||
import processSystemQueue from './processors/system/index.js';
|
||||
import processWebhookDeliver from './processors/webhook-deliver.js';
|
||||
import { endedPollNotification } from './processors/ended-poll-notification.js';
|
||||
import { queueLogger } from './logger.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { getJobInfo } from './get-job-info.js';
|
||||
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue } from './queues.js';
|
||||
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js';
|
||||
import { ThinUser } from './types.js';
|
||||
import { IActivity } from '@/remote/activitypub/type.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
|
||||
function renderError(e: Error): any {
|
||||
return {
|
||||
@ -26,6 +28,7 @@ function renderError(e: Error): any {
|
||||
|
||||
const systemLogger = queueLogger.createSubLogger('system');
|
||||
const deliverLogger = queueLogger.createSubLogger('deliver');
|
||||
const webhookLogger = queueLogger.createSubLogger('webhook');
|
||||
const inboxLogger = queueLogger.createSubLogger('inbox');
|
||||
const dbLogger = queueLogger.createSubLogger('db');
|
||||
const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
|
||||
@ -70,6 +73,14 @@ objectStorageQueue
|
||||
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
webhookDeliverQueue
|
||||
.on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
|
||||
export function deliver(user: ThinUser, content: unknown, to: string | null) {
|
||||
if (content == null) return null;
|
||||
if (to == null) return null;
|
||||
@ -251,12 +262,32 @@ export function createCleanRemoteFilesJob() {
|
||||
});
|
||||
}
|
||||
|
||||
export function webhookDeliver(webhook: Webhook, content: unknown) {
|
||||
const data = {
|
||||
content,
|
||||
webhookId: webhook.id,
|
||||
to: webhook.url,
|
||||
secret: webhook.secret,
|
||||
};
|
||||
|
||||
return webhookDeliverQueue.add(data, {
|
||||
attempts: 4,
|
||||
timeout: 1 * 60 * 1000, // 1min
|
||||
backoff: {
|
||||
type: 'apBackoff',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
export default function() {
|
||||
if (envOption.onlyServer) return;
|
||||
|
||||
deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
|
||||
inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
|
||||
endedPollNotificationQueue.process(endedPollNotification);
|
||||
webhookDeliverQueue.process(64, processWebhookDeliver);
|
||||
processDb(dbQueue);
|
||||
processObjectStorage(objectStorageQueue);
|
||||
|
||||
|
56
packages/backend/src/queue/processors/webhook-deliver.ts
Normal file
56
packages/backend/src/queue/processors/webhook-deliver.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { URL } from 'node:url';
|
||||
import Bull from 'bull';
|
||||
import Logger from '@/services/logger.js';
|
||||
import { WebhookDeliverJobData } from '../types.js';
|
||||
import { getResponse, StatusError } from '@/misc/fetch.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import config from '@/config/index.js';
|
||||
|
||||
const logger = new Logger('webhook');
|
||||
|
||||
let latest: string | null = null;
|
||||
|
||||
export default async (job: Bull.Job<WebhookDeliverJobData>) => {
|
||||
try {
|
||||
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
|
||||
logger.debug(`delivering ${latest}`);
|
||||
}
|
||||
|
||||
const res = await getResponse({
|
||||
url: job.data.to,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'Misskey-Hooks',
|
||||
'X-Misskey-Host': config.host,
|
||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||
'X-Misskey-Hook-Secret': job.data.secret,
|
||||
},
|
||||
body: JSON.stringify(job.data.content),
|
||||
});
|
||||
|
||||
Webhooks.update({ id: job.data.webhookId }, {
|
||||
latestSentAt: new Date(),
|
||||
latestStatus: res.status,
|
||||
});
|
||||
|
||||
return 'Success';
|
||||
} catch (res) {
|
||||
Webhooks.update({ id: job.data.webhookId }, {
|
||||
latestSentAt: new Date(),
|
||||
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
||||
});
|
||||
|
||||
if (res instanceof StatusError) {
|
||||
// 4xx
|
||||
if (res.isClientError) {
|
||||
return `${res.statusCode} ${res.statusMessage}`;
|
||||
}
|
||||
|
||||
// 5xx etc.
|
||||
throw `${res.statusCode} ${res.statusMessage}`;
|
||||
} else {
|
||||
// DNS error, socket error, timeout ...
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import config from '@/config/index.js';
|
||||
import { initialize as initializeQueue } from './initialize.js';
|
||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData } from './types.js';
|
||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from './types.js';
|
||||
|
||||
export const systemQueue = initializeQueue<Record<string, unknown>>('system');
|
||||
export const endedPollNotificationQueue = initializeQueue<EndedPollNotificationJobData>('endedPollNotification');
|
||||
@ -8,6 +8,7 @@ export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.de
|
||||
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
|
||||
export const dbQueue = initializeQueue<DbJobData>('db');
|
||||
export const objectStorageQueue = initializeQueue<ObjectStorageJobData>('objectStorage');
|
||||
export const webhookDeliverQueue = initializeQueue<WebhookDeliverJobData>('webhookDeliver', 64);
|
||||
|
||||
export const queues = [
|
||||
systemQueue,
|
||||
@ -16,4 +17,5 @@ export const queues = [
|
||||
inboxQueue,
|
||||
dbQueue,
|
||||
objectStorageQueue,
|
||||
webhookDeliverQueue,
|
||||
];
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Webhook } from '@/models/entities/webhook';
|
||||
import { IActivity } from '@/remote/activitypub/type.js';
|
||||
import httpSignature from 'http-signature';
|
||||
|
||||
@ -46,6 +47,13 @@ export type EndedPollNotificationJobData = {
|
||||
noteId: Note['id'];
|
||||
};
|
||||
|
||||
export type WebhookDeliverJobData = {
|
||||
content: unknown;
|
||||
webhookId: Webhook['id'];
|
||||
to: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
export type ThinUser = {
|
||||
id: User['id'];
|
||||
};
|
||||
|
@ -202,6 +202,11 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
|
||||
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
|
||||
import * as ep___i_update from './endpoints/i/update.js';
|
||||
import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js';
|
||||
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
|
||||
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___messaging_history from './endpoints/messaging/history.js';
|
||||
import * as ep___messaging_messages from './endpoints/messaging/messages.js';
|
||||
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
|
||||
@ -507,6 +512,11 @@ const eps = [
|
||||
['i/update-email', ep___i_updateEmail],
|
||||
['i/update', ep___i_update],
|
||||
['i/user-group-invites', ep___i_userGroupInvites],
|
||||
['i/webhooks/create', ep___i_webhooks_create],
|
||||
['i/webhooks/list', ep___i_webhooks_list],
|
||||
['i/webhooks/show', ep___i_webhooks_show],
|
||||
['i/webhooks/update', ep___i_webhooks_update],
|
||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
||||
['messaging/history', ep___messaging_history],
|
||||
['messaging/messages', ep___messaging_messages],
|
||||
['messaging/messages/create', ep___messaging_messages_create],
|
||||
|
@ -0,0 +1,43 @@
|
||||
import define from '../../../define.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import { publishInternalEvent } from '@/services/stream.js';
|
||||
import { webhookEventTypes } from '@/models/entities/webhook.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
url: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
on: { type: 'array', items: {
|
||||
type: 'string', enum: webhookEventTypes,
|
||||
} },
|
||||
},
|
||||
required: ['name', 'url', 'secret', 'on'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const webhook = await Webhooks.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
name: ps.name,
|
||||
url: ps.url,
|
||||
secret: ps.secret,
|
||||
on: ps.on,
|
||||
}).then(x => Webhooks.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
publishInternalEvent('webhookCreated', webhook);
|
||||
|
||||
return webhook;
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import { publishInternalEvent } from '@/services/stream.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchWebhook: {
|
||||
message: 'No such webhook.',
|
||||
code: 'NO_SUCH_WEBHOOK',
|
||||
id: 'bae73e5a-5522-4965-ae19-3a8688e71d82',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
webhookId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['webhookId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const webhook = await Webhooks.findOneBy({
|
||||
id: ps.webhookId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (webhook == null) {
|
||||
throw new ApiError(meta.errors.noSuchWebhook);
|
||||
}
|
||||
|
||||
await Webhooks.delete(webhook.id);
|
||||
|
||||
publishInternalEvent('webhookDeleted', webhook);
|
||||
});
|
25
packages/backend/src/server/api/endpoints/i/webhooks/list.ts
Normal file
25
packages/backend/src/server/api/endpoints/i/webhooks/list.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import define from '../../../define.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const webhooks = await Webhooks.findBy({
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
return webhooks;
|
||||
});
|
41
packages/backend/src/server/api/endpoints/i/webhooks/show.ts
Normal file
41
packages/backend/src/server/api/endpoints/i/webhooks/show.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchWebhook: {
|
||||
message: 'No such webhook.',
|
||||
code: 'NO_SUCH_WEBHOOK',
|
||||
id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
webhookId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['webhookId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const webhook = await Webhooks.findOneBy({
|
||||
id: ps.webhookId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (webhook == null) {
|
||||
throw new ApiError(meta.errors.noSuchWebhook);
|
||||
}
|
||||
|
||||
return webhook;
|
||||
});
|
@ -0,0 +1,59 @@
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import { publishInternalEvent } from '@/services/stream.js';
|
||||
import { webhookEventTypes } from '@/models/entities/webhook.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchWebhook: {
|
||||
message: 'No such webhook.',
|
||||
code: 'NO_SUCH_WEBHOOK',
|
||||
id: 'fb0fea69-da18-45b1-828d-bd4fd1612518',
|
||||
},
|
||||
},
|
||||
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
webhookId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
url: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
on: { type: 'array', items: {
|
||||
type: 'string', enum: webhookEventTypes,
|
||||
} },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const webhook = await Webhooks.findOneBy({
|
||||
id: ps.webhookId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (webhook == null) {
|
||||
throw new ApiError(meta.errors.noSuchWebhook);
|
||||
}
|
||||
|
||||
await Webhooks.update(webhook.id, {
|
||||
name: ps.name,
|
||||
url: ps.url,
|
||||
secret: ps.secret,
|
||||
on: ps.on,
|
||||
active: ps.active,
|
||||
});
|
||||
|
||||
publishInternalEvent('webhookUpdated', webhook);
|
||||
});
|
@ -15,6 +15,7 @@ import { AbuseUserReport } from '@/models/entities/abuse-user-report.js';
|
||||
import { Signin } from '@/models/entities/signin.js';
|
||||
import { Page } from '@/models/entities/page.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Webhook } from '@/models/entities/webhook';
|
||||
|
||||
//#region Stream type-body definitions
|
||||
export interface InternalStreamTypes {
|
||||
@ -23,6 +24,9 @@ export interface InternalStreamTypes {
|
||||
userChangeModeratorState: { id: User['id']; isModerator: User['isModerator']; };
|
||||
userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; };
|
||||
remoteUserUpdated: { id: User['id']; };
|
||||
webhookCreated: Webhook;
|
||||
webhookDeleted: Webhook;
|
||||
webhookUpdated: Webhook;
|
||||
antennaCreated: Antenna;
|
||||
antennaDeleted: Antenna;
|
||||
antennaUpdated: Antenna;
|
||||
|
@ -10,6 +10,8 @@ import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLis
|
||||
import { perUserFollowingChart } from '@/services/chart/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { getActiveWebhooks } from '@/misc/webhook-cache.js';
|
||||
import { webhookDeliver } from '@/queue/index.js';
|
||||
|
||||
export default async function(blocker: User, blockee: User) {
|
||||
await Promise.all([
|
||||
@ -57,9 +59,17 @@ async function cancelRequest(follower: User, followee: User) {
|
||||
if (Users.isLocalUser(follower)) {
|
||||
Users.pack(followee, follower, {
|
||||
detail: true,
|
||||
}).then(packed => {
|
||||
}).then(async packed => {
|
||||
publishUserEvent(follower.id, 'unfollow', packed);
|
||||
publishMainStream(follower.id, 'unfollow', packed);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'unfollow',
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -102,9 +112,17 @@ async function unFollow(follower: User, followee: User) {
|
||||
if (Users.isLocalUser(follower)) {
|
||||
Users.pack(followee, follower, {
|
||||
detail: true,
|
||||
}).then(packed => {
|
||||
}).then(async packed => {
|
||||
publishUserEvent(follower.id, 'unfollow', packed);
|
||||
publishMainStream(follower.id, 'unfollow', packed);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'unfollow',
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@ import { genId } from '@/misc/gen-id.js';
|
||||
import { createNotification } from '../create-notification.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { getActiveWebhooks } from '@/misc/webhook-cache.js';
|
||||
import { webhookDeliver } from '@/queue/index.js';
|
||||
|
||||
const logger = new Logger('following/create');
|
||||
|
||||
@ -89,15 +91,33 @@ export async function insertFollowingDoc(followee: { id: User['id']; host: User[
|
||||
if (Users.isLocalUser(follower)) {
|
||||
Users.pack(followee.id, follower, {
|
||||
detail: true,
|
||||
}).then(packed => {
|
||||
}).then(async packed => {
|
||||
publishUserEvent(follower.id, 'follow', packed as Packed<"UserDetailedNotMe">);
|
||||
publishMainStream(follower.id, 'follow', packed as Packed<"UserDetailedNotMe">);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'follow',
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Publish followed event
|
||||
if (Users.isLocalUser(followee)) {
|
||||
Users.pack(follower.id, followee).then(packed => publishMainStream(followee.id, 'followed', packed));
|
||||
Users.pack(follower.id, followee).then(async packed => {
|
||||
publishMainStream(followee.id, 'followed', packed)
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'followed',
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 通知を作成
|
||||
createNotification(followee.id, 'follow', {
|
||||
|
@ -3,12 +3,13 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||
import renderUndo from '@/remote/activitypub/renderer/undo.js';
|
||||
import renderReject from '@/remote/activitypub/renderer/reject.js';
|
||||
import { deliver } from '@/queue/index.js';
|
||||
import { deliver, webhookDeliver } from '@/queue/index.js';
|
||||
import Logger from '../logger.js';
|
||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Followings, Users, Instances } from '@/models/index.js';
|
||||
import { instanceChart, perUserFollowingChart } from '@/services/chart/index.js';
|
||||
import { getActiveWebhooks } from '@/misc/webhook-cache.js';
|
||||
|
||||
const logger = new Logger('following/delete');
|
||||
|
||||
@ -31,9 +32,17 @@ export default async function(follower: { id: User['id']; host: User['host']; ur
|
||||
if (!silent && Users.isLocalUser(follower)) {
|
||||
Users.pack(followee.id, follower, {
|
||||
detail: true,
|
||||
}).then(packed => {
|
||||
}).then(async packed => {
|
||||
publishUserEvent(follower.id, 'unfollow', packed);
|
||||
publishMainStream(follower.id, 'unfollow', packed);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'unfollow',
|
||||
user: packed,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||
import renderReject from '@/remote/activitypub/renderer/reject.js';
|
||||
import { deliver } from '@/queue/index.js';
|
||||
import { deliver, webhookDeliver } from '@/queue/index.js';
|
||||
import { publishMainStream, publishUserEvent } from '@/services/stream.js';
|
||||
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { Users, FollowRequests, Followings } from '@/models/index.js';
|
||||
import { decrementFollowing } from './delete.js';
|
||||
import { getActiveWebhooks } from '@/misc/webhook-cache.js';
|
||||
|
||||
type Local = ILocalUser | {
|
||||
id: ILocalUser['id'];
|
||||
@ -111,4 +112,12 @@ async function publishUnfollow(followee: Both, follower: Local) {
|
||||
|
||||
publishUserEvent(follower.id, 'unfollow', packedFollowee);
|
||||
publishMainStream(follower.id, 'unfollow', packedFollowee);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'unfollow',
|
||||
user: packedFollowee,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -35,9 +35,11 @@ import { Channel } from '@/models/entities/channel.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
||||
import { endedPollNotificationQueue } from '@/queue/queues.js';
|
||||
import { webhookDeliver } from '@/queue/index.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { getActiveWebhooks } from '@/misc/webhook-cache.js';
|
||||
|
||||
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
||||
|
||||
@ -345,6 +347,16 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
|
||||
publishNotesStream(noteObj);
|
||||
|
||||
getActiveWebhooks().then(webhooks => {
|
||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'note',
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const nm = new NotificationManager(user, note);
|
||||
const nmRelatedPromises = [];
|
||||
|
||||
@ -365,6 +377,14 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
if (!threadMuted) {
|
||||
nm.push(data.reply.userId, 'reply');
|
||||
publishMainStream(data.reply.userId, 'reply', noteObj);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'reply',
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -384,6 +404,14 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||
// Publish event
|
||||
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
|
||||
publishMainStream(data.renote.userId, 'renote', noteObj);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'renote',
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -620,6 +648,14 @@ async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note,
|
||||
|
||||
publishMainStream(u.id, 'mention', detailPackedNote);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, {
|
||||
type: 'mention',
|
||||
note: detailPackedNote,
|
||||
});
|
||||
}
|
||||
|
||||
// Create notification
|
||||
nm.push(u.id, 'mention');
|
||||
}
|
||||
|
Reference in New Issue
Block a user