Skip to content

Commit

Permalink
spec(skeb/role): Skeb募集中のクリエイターに自動でロールが付与されるように・バッジから募集状態の確認ができるように
Browse files Browse the repository at this point in the history
  • Loading branch information
u1-liquid committed Apr 1, 2024
1 parent 92bdc2e commit aa4ce25
Show file tree
Hide file tree
Showing 29 changed files with 323 additions and 44 deletions.
2 changes: 2 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1677,6 +1677,8 @@ _role:
iconUrl: "Icon URL"
asBadge: "Show as badge"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
badgeBehavior: "Badge behavior"
descriptionOfBadgeBehavior: "Set the behavior of the badge icon."
isExplorable: "Make role explorable"
descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled."
displayOrder: "Position"
Expand Down
8 changes: 8 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6558,6 +6558,14 @@ export interface Locale extends ILocale {
* オンにすると、ユーザー名の横にロールのアイコンが表示されます。
*/
"descriptionOfAsBadge": string;
/**
* バッジの挙動
*/
"badgeBehavior": string;
/**
* バッジの挙動を設定します。
*/
"descriptionOfBadgeBehavior": 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 @@ -1695,6 +1695,8 @@ _role:
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
badgeBehavior: "バッジの挙動"
descriptionOfBadgeBehavior: "バッジの挙動を設定します。"
isExplorable: "ユーザーを見つけやすくする"
descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。"
displayOrder: "表示順"
Expand Down
2 changes: 2 additions & 0 deletions locales/ko-KR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,8 @@ _role:
iconUrl: "아이콘 URL"
asBadge: "뱃지로 표시"
descriptionOfAsBadge: "활성화하면 유저명 옆에 역할의 아이콘이 표시됩니다."
badgeBehavior: "뱃지 동작"
descriptionOfBadgeBehavior: "뱃지의 동작 방식을 설정합니다."
isExplorable: "역할 타임라인 공개"
descriptionOfIsExplorable: "활성화하면 역할 타임라인을 공개합니다. 비활성화 시 타임라인이 공개되지 않습니다."
displayOrder: "표시 순서"
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/migration/1711946753142-role-badge-behavior.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class RoleBadgeBehavior1711946753142 {
name = 'RoleBadgeBehavior1711946753142'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" ADD "badgeBehavior" character varying(256)`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "badgeBehavior"`);
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type Source = {
headers: { [x: string]: string };
parameters: { [x: string]: string };
userIdParameterName: string;
roleId: string;
}

proxy?: string;
Expand Down Expand Up @@ -154,6 +155,7 @@ export type Config = {
headers: { [x: string]: string };
parameters: { [x: string]: string };
userIdParameterName: string;
roleId: string;
} | undefined;
proxy: string | undefined;
proxySmtp: string | undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/RoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
isModerator: values.isModerator,
isExplorable: values.isExplorable,
asBadge: values.asBadge,
badgeBehavior: values.badgeBehavior,
canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder,
policies: values.policies,
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/entities/RoleEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class RoleEntityService {
isModerator: role.isModerator,
isExplorable: role.isExplorable,
asBadge: role.asBadge,
badgeBehavior: role.badgeBehavior,
canEditMembersByModerator: role.canEditMembersByModerator,
displayOrder: role.displayOrder,
policies: policies,
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ export class UserEntityService implements OnModuleInit {
name: r.name,
iconUrl: r.iconUrl,
displayOrder: r.displayOrder,
behavior: r.badgeBehavior ?? undefined,
}))) : undefined,

...(isDetailed ? {
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/models/Role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ export class MiRole {
})
public asBadge: boolean;

@Column('varchar', {
length: 256, nullable: true,
})
public badgeBehavior: string | null;

@Column('boolean', {
default: false,
})
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/json-schema/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,10 @@ export const packedRoleSchema = {
optional: false, nullable: false,
example: false,
},
badgeBehavior: {
type: 'string',
optional: false, nullable: true,
},
canEditMembersByModerator: {
type: 'boolean',
optional: false, nullable: false,
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/json-schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ export const packedUserLiteSchema = {
type: 'number',
nullable: false, optional: false,
},
behavior: {
type: 'string',
nullable: false, optional: true,
}
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
asBadge: { type: 'boolean' },
badgeBehavior: { type: 'string', nullable: true },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
policies: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' },
isExplorable: { type: 'boolean' },
asBadge: { type: 'boolean' },
badgeBehavior: { type: 'string', nullable: true },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
policies: {
Expand Down Expand Up @@ -92,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isAdministrator: ps.isAdministrator,
isExplorable: ps.isExplorable,
asBadge: ps.asBadge,
badgeBehavior: ps.badgeBehavior,
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
policies: ps.policies,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RoleService } from '@/core/RoleService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { Config } from '@/config.js';
Expand Down Expand Up @@ -88,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config)
private config: Config,

private roleService: RoleService,
private loggerService: LoggerService,
private httpRequestService: HttpRequestService,
) {
Expand Down Expand Up @@ -128,13 +130,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
error?: unknown,
};

if (res.status > 399 || (json.error ?? json.ban_reason)) {
const hasSkebRole = await this.roleService.getUserRoles(ps.userId).then(roles => roles.some(r => r.id === this.config.skebStatus?.roleId));

if (res.status > 299 || (json.error ?? json.ban_reason)) {
logger.error('Skeb status response error', { url: url.href, userId: ps.userId, status: res.status, statusText: res.statusText, error: json.error ?? json.ban_reason });
if (res.status === 404 && hasSkebRole) await this.roleService.unassign(ps.userId, this.config.skebStatus.roleId);
throw new ApiError(meta.errors.noSuchUser);
}

logger.info('Skeb status response', { url: url.href, userId: ps.userId, status: res.status, statusText: res.statusText, skebStatus: json });

if (json.is_acceptable) {
if (!hasSkebRole) await this.roleService.assign(ps.userId, this.config.skebStatus.roleId);
} else if (hasSkebRole) {
await this.roleService.unassign(ps.userId, this.config.skebStatus.roleId);
}

return {
screenName: json.screen_name,
isCreator: json.is_creator,
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/test/e2e/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ describe('Note', () => {
isPublic: false,
isExplorable: false,
asBadge: false,
badgeBehavior: null,
canEditMembersByModerator: false,
policies: {
alwaysMarkNsfw: {
Expand Down Expand Up @@ -780,6 +781,7 @@ describe('Note', () => {
isPublic: false,
isExplorable: false,
asBadge: false,
badgeBehavior: null,
canEditMembersByModerator: false,
policies: {
mentionLimit: {
Expand Down Expand Up @@ -834,6 +836,7 @@ describe('Note', () => {
isPublic: false,
isExplorable: false,
asBadge: false,
badgeBehavior: null,
canEditMembersByModerator: false,
policies: {
mentionLimit: {
Expand Down Expand Up @@ -890,6 +893,7 @@ describe('Note', () => {
isPublic: false,
isExplorable: false,
asBadge: false,
badgeBehavior: null,
canEditMembersByModerator: false,
policies: {
mentionLimit: {
Expand Down
19 changes: 14 additions & 5 deletions packages/backend/test/e2e/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,11 +651,20 @@ describe('ユーザー', () => {
});
test('を取得することができ、バッヂロールがセットされていること', async () => {
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRoleBadge.id }, user: alice });
assert.deepStrictEqual(response.badgeRoles, [{
name: roleBadge.name,
iconUrl: roleBadge.iconUrl,
displayOrder: roleBadge.displayOrder,
}]);
if (roleBadge.badgeBehavior) {
assert.deepStrictEqual(response.badgeRoles, [{
name: roleBadge.name,
iconUrl: roleBadge.iconUrl,
displayOrder: roleBadge.displayOrder,
behavior: roleBadge.badgeBehavior,
}]);
} else {
assert.deepStrictEqual(response.badgeRoles, [{
name: roleBadge.name,
iconUrl: roleBadge.iconUrl,
displayOrder: roleBadge.displayOrder,
}]);
}
assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない
});
test('をID指定のリスト形式で取得することができる(空)', async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/backend/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export const channel = async (user: UserToken, channel: Partial<misskey.entities
export const role = async (user: UserToken, role: Partial<misskey.entities.Role> = {}, policies: any = {}): Promise<misskey.entities.Role> => {
const res = await api('admin/roles/create', {
asBadge: false,
badgeBehavior: null,
canEditMembersByModerator: false,
color: null,
condFormula: {
Expand Down
24 changes: 12 additions & 12 deletions packages/frontend/src/components/MkModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ const align = () => {
let left;
let top;

const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
const y = srcRect.top + (fixed.value ? 0 : window.scrollY);

if (props.anchor.x === 'center') {
left = x + (props.src.offsetWidth / 2) - (width / 2);
Expand Down Expand Up @@ -220,24 +220,24 @@ const align = () => {
}
} else {
// 画面から横にはみ出る場合
if (left + width - window.pageXOffset > (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset - 1;
if (left + width - window.scrollX > (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.scrollX - 1;
}

const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - (top - window.pageYOffset);
const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - (top - window.scrollY);
const upperSpace = (srcRect.top - MARGIN);

// 画面から縦にはみ出る場合
if (top + height - window.pageYOffset > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (top + height - window.scrollY > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) {
maxHeight.value = underSpace;
} else {
maxHeight.value = upperSpace;
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
top = window.scrollY + ((upperSpace + MARGIN) - height);
}
} else {
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1;
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.scrollY - 1;
}
} else {
maxHeight.value = underSpace;
Expand All @@ -255,15 +255,15 @@ const align = () => {
let transformOriginX = 'center';
let transformOriginY = 'center';

if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)) {
if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.scrollY)) {
transformOriginY = 'top';
} else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) {
} else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.scrollY)) {
transformOriginY = 'bottom';
}

if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)) {
if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.scrollX)) {
transformOriginX = 'left';
} else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.pageXOffset)) {
} else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.scrollX)) {
transformOriginX = 'right';
}

Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/src/components/MkNoteHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
<div :class="$style.username"><MkAcct :user="note.user"/></div>
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
<MkRoleBadgeIcon v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :userId="note.user.id" :role="role" :class="$style.badgeRole"/>
</div>
<div :class="$style.info">
<div v-if="mock">
Expand All @@ -40,6 +40,7 @@ import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import { notePage } from '@/filters/note.js';
import { userPage } from '@/filters/user.js';
import MkRoleBadgeIcon from '@/components/MkRoleBadgeIcon.vue';

defineProps<{
note: Misskey.entities.Note;
Expand Down
57 changes: 57 additions & 0 deletions packages/frontend/src/components/MkRoleBadgeIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<img ref="el" :src="role.iconUrl!" @click="onClick(role)"/>
</template>

<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { instance } from '@/instance.js';
import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
const props = defineProps<{
userId: string,
role: { name: string, iconUrl: string | null, displayOrder: number, behavior?: string }
}>();
const el = ref<HTMLElement | { $el: HTMLElement }>();
const userSkebStatus = ref<Misskey.Endpoints['users/get-skeb-status']['res'] | null>(null);
async function fetchSkebStatus() {
if (!instance.enableSkebStatus || props.role.behavior !== 'skeb') {
userSkebStatus.value = null;
return;
}
userSkebStatus.value = await misskeyApiGet('users/get-skeb-status', { userId: props.userId });
}
if (props.role.behavior === 'skeb') {
useTooltip(el, async (showing) => {
if (userSkebStatus.value == null) {
await fetchSkebStatus();
}
if (userSkebStatus.value === null) return;
os.popup(defineAsyncComponent(() => import('@/components/MkSkebStatusPopup.vue')), {
showing,
skebStatus: userSkebStatus.value,
source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
}, {}, 'closed');
});
}
async function onClick(role: { name: string, iconUrl: string | null, displayOrder: number, behavior?: string }) {
if (role.behavior === 'skeb') {
if (userSkebStatus.value == null) {
await fetchSkebStatus();
}
if (userSkebStatus.value != null) {
window.open(`https://skeb.jp/@${userSkebStatus.value.screenName}`, '_blank', 'noopener');
}
}
}
</script>
Loading

0 comments on commit aa4ce25

Please sign in to comment.