Skip to content

Commit

Permalink
Merge pull request #9 from anatawa12/admin-webhook
Browse files Browse the repository at this point in the history
Admin webhook
  • Loading branch information
anatawa12 authored Jul 27, 2023
2 parents b4755e4 + 490e17e commit 90d53ba
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- リストTLで、ユーザーが追加・削除されてもTLを初期化しないように
- URL取得変数を関数に変更 CURRENT_URL -> Mk:url()
- プレビューの表示状態を記憶するように
- 管理者専用の他人を見るwebhookが増えました
- Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
Expand Down
2 changes: 2 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2178,6 +2178,8 @@ export interface Locale {
"renote": string;
"reaction": string;
"mention": string;
"usersLabel": string;
"usersCaption": string;
};
};
}
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2093,3 +2093,5 @@ _webhookSettings:
renote: "Renoteされたとき"
reaction: "リアクションがあったとき"
mention: "メンションされたとき"
usersLabel: "以下のユーザがnoteしたとき"
usersCaption: "このサーバーのユーザの@に挟まれた部分を改行で区切って指定します"
3 changes: 2 additions & 1 deletion packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,8 @@ export class NoteCreateService implements OnApplicationShutdown {
this.roleService.addNoteToRoleTimeline(noteObj);

this.webhookService.getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
const userNoteEvent = `note@${user.username}` as const;
webhooks = webhooks.filter(x => (x.userId === user.id && x.on.includes('note')) || x.on.includes(userNoteEvent));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'note', {
note: noteObj,
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/core/QueueService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
import type { Webhook, WebhookEventType } from '@/models/entities/Webhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
Expand Down Expand Up @@ -407,7 +407,7 @@ export class QueueService {
}

@bindThis
public webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) {
public webhookDeliver(webhook: Webhook, type: WebhookEventType, content: unknown) {
const data = {
type,
content,
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/models/entities/Webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { id } from '../id.js';
import { User } from './User.js';

export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
export type WebhookEventType = (typeof webhookEventTypes)[number] | `note@${string}`;

@Entity()
export class Webhook {
Expand Down Expand Up @@ -37,7 +38,7 @@ export class Webhook {
@Column('varchar', {
length: 128, array: true, default: '{}',
})
public on: (typeof webhookEventTypes)[number][];
public on: WebhookEventType[];

@Column('varchar', {
length: 1024,
Expand Down
20 changes: 17 additions & 3 deletions packages/backend/src/server/api/endpoints/i/webhooks/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import type { WebhooksRepository } from '@/models/index.js';
import { webhookEventTypes } from '@/models/entities/Webhook.js';
import { webhookEventTypes, WebhookEventType } from '@/models/entities/Webhook.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
Expand All @@ -21,6 +21,11 @@ export const meta = {
code: 'TOO_MANY_WEBHOOKS',
id: '87a9bb19-111e-4e37-81d3-a3e7426453b0',
},
adminWebhookDenied: {
message: 'You cannot create webhook for other users.',
code: 'ADMIN_WEBHOOK_DENIED',
id: '0d3321b1-6f66-41aa-9fbe-233c60ce19b0',
},
},
} as const;

Expand All @@ -31,7 +36,10 @@ export const paramDef = {
url: { type: 'string', minLength: 1, maxLength: 1024 },
secret: { type: 'string', minLength: 1, maxLength: 1024 },
on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes,
oneOf: [
{ type: 'string', enum: webhookEventTypes },
{ type: 'string', pattern: '^note@[a-zA-Z0-9]{1,20}$' },
],
} },
},
required: ['name', 'url', 'secret', 'on'],
Expand All @@ -58,14 +66,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.tooManyWebhooks);
}

if (ps.on.some(x => !(webhookEventTypes as readonly string[]).includes(x))) {
if (!await this.roleService.isAdministrator(me)) {
throw new ApiError(meta.errors.adminWebhookDenied);
}
}

const webhook = await this.webhooksRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: me.id,
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on,
on: ps.on as WebhookEventType[],
}).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0]));

this.globalEventService.publishInternalEvent('webhookCreated', webhook);
Expand Down
22 changes: 19 additions & 3 deletions packages/backend/src/server/api/endpoints/i/webhooks/update.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { WebhooksRepository } from '@/models/index.js';
import { webhookEventTypes } from '@/models/entities/Webhook.js';
import { webhookEventTypes, WebhookEventType } from '@/models/entities/Webhook.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';

export const meta = {
Expand All @@ -19,6 +20,11 @@ export const meta = {
code: 'NO_SUCH_WEBHOOK',
id: 'fb0fea69-da18-45b1-828d-bd4fd1612518',
},
adminWebhookDenied: {
message: 'You cannot create webhook for other users.',
code: 'UPDATE_ADMIN_WEBHOOK_DENIED',
id: 'eb43c0c4-24a3-487d-b139-f3e4e58f87a4',
},
},

} as const;
Expand All @@ -31,7 +37,10 @@ export const paramDef = {
url: { type: 'string', minLength: 1, maxLength: 1024 },
secret: { type: 'string', minLength: 1, maxLength: 1024 },
on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes,
oneOf: [
{ type: 'string', enum: webhookEventTypes },
{ type: 'string', pattern: '^note@[a-zA-Z0-9]{1,20}$' },
],
} },
active: { type: 'boolean' },
},
Expand All @@ -48,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private webhooksRepository: WebhooksRepository,

private globalEventService: GlobalEventService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const webhook = await this.webhooksRepository.findOneBy({
Expand All @@ -59,11 +69,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchWebhook);
}

if (ps.on.some(x => !(webhookEventTypes as readonly string[]).includes(x))) {
if (!await this.roleService.isAdministrator(me)) {
throw new ApiError(meta.errors.adminWebhookDenied);
}
}

await this.webhooksRepository.update(webhook.id, {
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on,
on: ps.on as WebhookEventType[],
active: ps.active,
});

Expand Down
19 changes: 18 additions & 1 deletion packages/frontend/src/pages/settings/webhook.edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>

<MkTextarea v-if="$i?.isAdmin" v-model="users">
<template #label>{{ i18n.ts._webhookSettings._events.usersLabel }}</template>
<template #caption>{{ i18n.ts._webhookSettings._events.usersCaption }}</template>
</MkTextarea>
</div>
</FormSection>
Expand All @@ -46,6 +51,8 @@ import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { useRouter } from '@/router';
import { $i } from '@/account';
import MkTextarea from '@/components/MkTextarea.vue';
const router = useRouter();
Expand All @@ -69,16 +76,18 @@ let event_reply = $ref(webhook.on.includes('reply'));
let event_renote = $ref(webhook.on.includes('renote'));
let event_reaction = $ref(webhook.on.includes('reaction'));
let event_mention = $ref(webhook.on.includes('mention'));
let users = $ref((webhook.on as string[]).filter(x => x.startsWith('note@')).map(x => x.substring('note@'.length)).join('\n'));
async function save(): Promise<void> {
const events = [];
const events: string[] = [];
if (event_follow) events.push('follow');
if (event_followed) events.push('followed');
if (event_note) events.push('note');
if (event_reply) events.push('reply');
if (event_renote) events.push('renote');
if (event_reaction) events.push('reaction');
if (event_mention) events.push('mention');
if (users !== '') events.push(...users.split('\n').filter(x => x).map(x => `note@${x}`));
os.apiWithDialog('i/webhooks/update', {
name,
Expand Down Expand Up @@ -112,3 +121,11 @@ definePageMetadata({
icon: 'ti ti-webhook',
});
</script>
<style lang="scss" module>
.userItem {
display: flex;
}
</style>
11 changes: 10 additions & 1 deletion packages/frontend/src/pages/settings/webhook.new.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>

<MkTextarea v-if="$i?.isAdmin" v-model="users">
<template #label>{{ i18n.ts._webhookSettings._events.usersLabel }}</template>
<template #caption>{{ i18n.ts._webhookSettings._events.usersCaption }}</template>
</MkTextarea>
</div>
</FormSection>
Expand All @@ -42,6 +47,8 @@ import MkButton from '@/components/MkButton.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { $i } from '@/account';
import MkTextarea from '@/components/MkTextarea.vue';
let name = $ref('');
let url = $ref('');
Expand All @@ -54,16 +61,18 @@ let event_reply = $ref(true);
let event_renote = $ref(true);
let event_reaction = $ref(true);
let event_mention = $ref(true);
let users = $ref('');
async function create(): Promise<void> {
const events = [];
const events: string[] = [];
if (event_follow) events.push('follow');
if (event_followed) events.push('followed');
if (event_note) events.push('note');
if (event_reply) events.push('reply');
if (event_renote) events.push('renote');
if (event_reaction) events.push('reaction');
if (event_mention) events.push('mention');
if (users !== '') events.push(...users.split('\n').filter(x => x).map(x => `note@${x}`));
os.apiWithDialog('i/webhooks/create', {
name,
Expand Down

0 comments on commit 90d53ba

Please sign in to comment.