summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-04-02 15:28:49 +0900
committerGitHub <noreply@github.com>2022-04-02 15:28:49 +0900
commit8e5f2690f29b7e6bee95e54a8bb647ff1ff4b94a (patch)
tree78740472dc48e4fec6986056f548e4ee7743cc29 /packages/backend/src
parentUpdate CHANGELOG.md (diff)
downloadsharkey-8e5f2690f29b7e6bee95e54a8bb647ff1ff4b94a.tar.gz
sharkey-8e5f2690f29b7e6bee95e54a8bb647ff1ff4b94a.tar.bz2
sharkey-8e5f2690f29b7e6bee95e54a8bb647ff1ff4b94a.zip
feat: Webhook (#8457)
* feat: introduce webhook * wip * wip * wip * Update CHANGELOG.md
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/db/postgre.ts2
-rw-r--r--packages/backend/src/misc/webhook-cache.ts49
-rw-r--r--packages/backend/src/models/entities/webhook.ts73
-rw-r--r--packages/backend/src/models/index.ts2
-rw-r--r--packages/backend/src/queue/index.ts33
-rw-r--r--packages/backend/src/queue/processors/webhook-deliver.ts56
-rw-r--r--packages/backend/src/queue/queues.ts4
-rw-r--r--packages/backend/src/queue/types.ts8
-rw-r--r--packages/backend/src/server/api/endpoints.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/create.ts43
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/delete.ts44
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/list.ts25
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/show.ts41
-rw-r--r--packages/backend/src/server/api/endpoints/i/webhooks/update.ts59
-rw-r--r--packages/backend/src/server/api/stream/types.ts4
-rw-r--r--packages/backend/src/services/blocking/create.ts22
-rw-r--r--packages/backend/src/services/following/create.ts24
-rw-r--r--packages/backend/src/services/following/delete.ts13
-rw-r--r--packages/backend/src/services/following/reject.ts11
-rw-r--r--packages/backend/src/services/note/create.ts36
20 files changed, 550 insertions, 9 deletions
diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts
index 491c1a174f..f7638a53d0 100644
--- a/packages/backend/src/db/postgre.ts
+++ b/packages/backend/src/db/postgre.ts
@@ -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,
];
diff --git a/packages/backend/src/misc/webhook-cache.ts b/packages/backend/src/misc/webhook-cache.ts
new file mode 100644
index 0000000000..4bd2333661
--- /dev/null
+++ b/packages/backend/src/misc/webhook-cache.ts
@@ -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;
+ }
+ }
+});
diff --git a/packages/backend/src/models/entities/webhook.ts b/packages/backend/src/models/entities/webhook.ts
new file mode 100644
index 0000000000..56b411f879
--- /dev/null
+++ b/packages/backend/src/models/entities/webhook.ts
@@ -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;
+}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index 54582347c7..814b37d448 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -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);
diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts
index b679a552b2..a570400b7b 100644
--- a/packages/backend/src/queue/index.ts
+++ b/packages/backend/src/queue/index.ts
@@ -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);
diff --git a/packages/backend/src/queue/processors/webhook-deliver.ts b/packages/backend/src/queue/processors/webhook-deliver.ts
new file mode 100644
index 0000000000..a4d39d86e4
--- /dev/null
+++ b/packages/backend/src/queue/processors/webhook-deliver.ts
@@ -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;
+ }
+ }
+};
diff --git a/packages/backend/src/queue/queues.ts b/packages/backend/src/queue/queues.ts
index d612dee450..f3a267790c 100644
--- a/packages/backend/src/queue/queues.ts
+++ b/packages/backend/src/queue/queues.ts
@@ -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,
];
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index 5191caea4c..8aeacf4625 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -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'];
};
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index b58ee8e8d0..e2db03f13a 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -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],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
new file mode 100644
index 0000000000..2e2fd00b8c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -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;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts
new file mode 100644
index 0000000000..2821eaa5f1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts
@@ -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);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
new file mode 100644
index 0000000000..54e4563732
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
@@ -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;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
new file mode 100644
index 0000000000..02fa1edb5e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
@@ -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;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
new file mode 100644
index 0000000000..f87b9753fb
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts
@@ -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);
+});
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
index bea863eb7c..3b0a75d793 100644
--- a/packages/backend/src/server/api/stream/types.ts
+++ b/packages/backend/src/server/api/stream/types.ts
@@ -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;
diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts
index 86c7d7967b..5c67190079 100644
--- a/packages/backend/src/services/blocking/create.ts
+++ b/packages/backend/src/services/blocking/create.ts
@@ -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,
+ });
+ }
});
}
diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts
index 0daf30ddad..d243317d97 100644
--- a/packages/backend/src/services/following/create.ts
+++ b/packages/backend/src/services/following/create.ts
@@ -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', {
diff --git a/packages/backend/src/services/following/delete.ts b/packages/backend/src/services/following/delete.ts
index 35fd664b55..85e40f1365 100644
--- a/packages/backend/src/services/following/delete.ts
+++ b/packages/backend/src/services/following/delete.ts
@@ -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,
+ });
+ }
});
}
diff --git a/packages/backend/src/services/following/reject.ts b/packages/backend/src/services/following/reject.ts
index 2d1db3c342..e1744e05be 100644
--- a/packages/backend/src/services/following/reject.ts
+++ b/packages/backend/src/services/following/reject.ts
@@ -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,
+ });
+ }
}
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
index 2ed194b7e9..6f373aaf45 100644
--- a/packages/backend/src/services/note/create.ts
+++ b/packages/backend/src/services/note/create.ts
@@ -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');
}