diff --git a/CHANGELOG.md b/CHANGELOG.md index 89378f61e1..5e81b43d3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ChangeLog unreleased ------------------- ### ✨Improvements +* アンテナの受信ソースにグループを指定できるように * 時計ウィジェットを追加 ### 🐛Fixes diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1751bc9f8f..f883e72632 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -487,6 +487,7 @@ _antennaSources: homeTimeline: "フォローしているユーザーのノート" users: "指定した一人または複数のユーザーのノート" userList: "指定したリストのユーザーのノート" + userGroup: "指定したグループのユーザーのノート" _weekday: sunday: "日曜日" diff --git a/migration/1581695816408-user-group-antenna.ts b/migration/1581695816408-user-group-antenna.ts new file mode 100644 index 0000000000..15eb2fe11b --- /dev/null +++ b/migration/1581695816408-user-group-antenna.ts @@ -0,0 +1,28 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class userGroupAntenna1581695816408 implements MigrationInterface { + name = 'userGroupAntenna1581695816408' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "antenna" ADD "userGroupJoiningId" character varying(32)`, undefined); + await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined); + await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list', 'group')`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined); + await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb" FOREIGN KEY ("userGroupJoiningId") REFERENCES "user_group_joining"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying array NOT NULL DEFAULT '{}'`, undefined); + await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'users', 'list')`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined); + await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined); + await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO "antenna_src_enum"`, undefined); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "userGroupJoiningId"`, undefined); + } + +} diff --git a/src/client/pages/my-antennas/index.antenna.vue b/src/client/pages/my-antennas/index.antenna.vue index 7ee916a1c0..d0259a55c6 100644 --- a/src/client/pages/my-antennas/index.antenna.vue +++ b/src/client/pages/my-antennas/index.antenna.vue @@ -11,12 +11,17 @@ + - + + + + + {{ $t('users') }} @@ -67,6 +72,7 @@ export default Vue.extend({ name: '', src: '', userListId: null, + userGroupId: null, users: '', keywords: '', caseSensitive: false, @@ -74,6 +80,7 @@ export default Vue.extend({ withFile: false, notify: false, userLists: null, + userGroups: null, faSave, faTrash }; }, @@ -83,6 +90,13 @@ export default Vue.extend({ if (this.src === 'list' && this.userLists === null) { this.userLists = await this.$root.api('users/lists/list'); } + + if (this.src === 'group' && this.userGroups === null) { + const groups1 = await this.$root.api('users/groups/owned'); + const groups2 = await this.$root.api('users/groups/joined'); + + this.userGroups = [...groups1, ...groups2]; + } } }, @@ -90,6 +104,7 @@ export default Vue.extend({ this.name = this.antenna.name; this.src = this.antenna.src; this.userListId = this.antenna.userListId; + this.userGroupId = this.antenna.userGroupId; this.users = this.antenna.users.join('\n'); this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n'); this.caseSensitive = this.antenna.caseSensitive; @@ -105,6 +120,7 @@ export default Vue.extend({ name: this.name, src: this.src, userListId: this.userListId, + userGroupId: this.userGroupId, withReplies: this.withReplies, withFile: this.withFile, notify: this.notify, @@ -119,6 +135,7 @@ export default Vue.extend({ name: this.name, src: this.src, userListId: this.userListId, + userGroupId: this.userGroupId, withReplies: this.withReplies, withFile: this.withFile, notify: this.notify, diff --git a/src/client/pages/my-antennas/index.vue b/src/client/pages/my-antennas/index.vue index 0c70d63d5d..8ac70ac378 100644 --- a/src/client/pages/my-antennas/index.vue +++ b/src/client/pages/my-antennas/index.vue @@ -50,6 +50,7 @@ export default Vue.extend({ name: '', src: 'all', userListId: null, + userGroupId: null, users: [], keywords: [], withReplies: false, diff --git a/src/misc/check-hit-antenna.ts b/src/misc/check-hit-antenna.ts index b527d34354..c229a07ebe 100644 --- a/src/misc/check-hit-antenna.ts +++ b/src/misc/check-hit-antenna.ts @@ -1,9 +1,10 @@ import { Antenna } from '../models/entities/antenna'; import { Note } from '../models/entities/note'; import { User } from '../models/entities/user'; -import { UserListJoinings } from '../models'; +import { UserListJoinings, UserGroupJoinings } from '../models'; import parseAcct from './acct/parse'; import { getFullApAccount } from './convert-host'; +import { ensure } from '../prelude/ensure'; export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: User, followers: User['id'][]): Promise { if (note.visibility === 'specified') return false; @@ -22,6 +23,14 @@ export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: Us })).map(x => x.userId); if (!listUsers.includes(note.userId)) return false; + } else if (antenna.src === 'group') { + const joining = await UserGroupJoinings.findOne(antenna.userGroupJoiningId!).then(ensure); + + const groupUsers = (await UserGroupJoinings.find({ + userGroupId: joining.userGroupId + })).map(x => x.userId); + + if (!groupUsers.includes(note.userId)) return false; } else if (antenna.src === 'users') { const accts = antenna.users.map(x => { const { username, host } = parseAcct(x); diff --git a/src/models/entities/antenna.ts b/src/models/entities/antenna.ts index e9971c6c07..7c2027b6ec 100644 --- a/src/models/entities/antenna.ts +++ b/src/models/entities/antenna.ts @@ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ import { User } from './user'; import { id } from '../id'; import { UserList } from './user-list'; +import { UserGroupJoining } from './user-group-joining'; @Entity() export class Antenna { @@ -32,8 +33,8 @@ export class Antenna { }) public name: string; - @Column('enum', { enum: ['home', 'all', 'users', 'list'] }) - public src: 'home' | 'all' | 'users' | 'list'; + @Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] }) + public src: 'home' | 'all' | 'users' | 'list' | 'group'; @Column({ ...id(), @@ -47,6 +48,18 @@ export class Antenna { @JoinColumn() public userList: UserList | null; + @Column({ + ...id(), + nullable: true + }) + public userGroupJoiningId: UserGroupJoining['id'] | null; + + @ManyToOne(type => UserGroupJoining, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public userGroupJoining: UserGroupJoining | null; + @Column('varchar', { length: 1024, array: true, default: '{}' diff --git a/src/models/repositories/antenna.ts b/src/models/repositories/antenna.ts index c47a7ea35c..9f8aa11347 100644 --- a/src/models/repositories/antenna.ts +++ b/src/models/repositories/antenna.ts @@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm'; import { Antenna } from '../entities/antenna'; import { ensure } from '../../prelude/ensure'; import { SchemaType } from '../../misc/schema'; -import { AntennaNotes } from '..'; +import { AntennaNotes, UserGroupJoinings } from '..'; export type PackedAntenna = SchemaType; @@ -14,6 +14,7 @@ export class AntennaRepository extends Repository { const antenna = typeof src === 'object' ? src : await this.findOne(src).then(ensure); const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null; + const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOne(antenna.userGroupJoiningId) : null; return { id: antenna.id, @@ -22,6 +23,7 @@ export class AntennaRepository extends Repository { keywords: antenna.keywords, src: antenna.src, userListId: antenna.userListId, + userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null, users: antenna.users, caseSensitive: antenna.caseSensitive, notify: antenna.notify, diff --git a/src/server/api/endpoints/antennas/create.ts b/src/server/api/endpoints/antennas/create.ts index 0e00eda1a4..26915c19b3 100644 --- a/src/server/api/endpoints/antennas/create.ts +++ b/src/server/api/endpoints/antennas/create.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import define from '../../define'; import { genId } from '../../../../misc/gen-id'; -import { Antennas, UserLists } from '../../../../models'; +import { Antennas, UserLists, UserGroupJoinings } from '../../../../models'; import { ID } from '../../../../misc/cafy-id'; import { ApiError } from '../../error'; @@ -18,13 +18,17 @@ export const meta = { }, src: { - validator: $.str.or(['home', 'all', 'users', 'list']) + validator: $.str.or(['home', 'all', 'users', 'list', 'group']) }, userListId: { validator: $.nullable.optional.type(ID), }, + userGroupId: { + validator: $.nullable.optional.type(ID), + }, + keywords: { validator: $.arr($.arr($.str)) }, @@ -55,12 +59,19 @@ export const meta = { message: 'No such user list.', code: 'NO_SUCH_USER_LIST', id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f' + }, + + noSuchUserGroup: { + message: 'No such user group.', + code: 'NO_SUCH_USER_GROUP', + id: 'aa3c0b9a-8cae-47c0-92ac-202ce5906682' } } }; export default define(meta, async (ps, user) => { let userList; + let userGroupJoining; if (ps.src === 'list') { userList = await UserLists.findOne({ @@ -71,6 +82,15 @@ export default define(meta, async (ps, user) => { if (userList == null) { throw new ApiError(meta.errors.noSuchUserList); } + } else if (ps.src === 'group') { + userGroupJoining = await UserGroupJoinings.findOne({ + userGroupId: ps.userGroupId, + userId: user.id, + }); + + if (userGroupJoining == null) { + throw new ApiError(meta.errors.noSuchUserGroup); + } } const antenna = await Antennas.save({ @@ -80,6 +100,7 @@ export default define(meta, async (ps, user) => { name: ps.name, src: ps.src, userListId: userList ? userList.id : null, + userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, keywords: ps.keywords, users: ps.users, caseSensitive: ps.caseSensitive, diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts index 28875d0f08..b4e7168888 100644 --- a/src/server/api/endpoints/antennas/update.ts +++ b/src/server/api/endpoints/antennas/update.ts @@ -2,7 +2,7 @@ import $ from 'cafy'; import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; -import { Antennas, UserLists } from '../../../../models'; +import { Antennas, UserLists, UserGroupJoinings } from '../../../../models'; export const meta = { tags: ['antennas'], @@ -21,13 +21,17 @@ export const meta = { }, src: { - validator: $.str.or(['home', 'all', 'users', 'list']) + validator: $.str.or(['home', 'all', 'users', 'list', 'group']) }, userListId: { validator: $.nullable.optional.type(ID), }, + userGroupId: { + validator: $.nullable.optional.type(ID), + }, + keywords: { validator: $.arr($.arr($.str)) }, @@ -64,6 +68,12 @@ export const meta = { message: 'No such user list.', code: 'NO_SUCH_USER_LIST', id: '1c6b35c9-943e-48c2-81e4-2844989407f7' + }, + + noSuchUserGroup: { + message: 'No such user group.', + code: 'NO_SUCH_USER_GROUP', + id: '109ed789-b6eb-456e-b8a9-6059d567d385' } } }; @@ -80,6 +90,7 @@ export default define(meta, async (ps, user) => { } let userList; + let userGroupJoining; if (ps.src === 'list') { userList = await UserLists.findOne({ @@ -90,12 +101,22 @@ export default define(meta, async (ps, user) => { if (userList == null) { throw new ApiError(meta.errors.noSuchUserList); } + } else if (ps.src === 'group') { + userGroupJoining = await UserGroupJoinings.findOne({ + userGroupId: ps.userGroupId, + userId: user.id, + }); + + if (userGroupJoining == null) { + throw new ApiError(meta.errors.noSuchUserGroup); + } } await Antennas.update(antenna.id, { name: ps.name, src: ps.src, userListId: userList ? userList.id : null, + userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, keywords: ps.keywords, users: ps.users, caseSensitive: ps.caseSensitive,