This commit is contained in:
syuilo 2020-02-15 01:03:59 +09:00
parent 84958af4ce
commit 65c0b6c7da
10 changed files with 123 additions and 9 deletions

View File

@ -4,6 +4,7 @@ ChangeLog
unreleased unreleased
------------------- -------------------
### ✨Improvements ### ✨Improvements
* アンテナの受信ソースにグループを指定できるように
* 時計ウィジェットを追加 * 時計ウィジェットを追加
### 🐛Fixes ### 🐛Fixes

View File

@ -487,6 +487,7 @@ _antennaSources:
homeTimeline: "フォローしているユーザーのノート" homeTimeline: "フォローしているユーザーのノート"
users: "指定した一人または複数のユーザーのノート" users: "指定した一人または複数のユーザーのノート"
userList: "指定したリストのユーザーのノート" userList: "指定したリストのユーザーのノート"
userGroup: "指定したグループのユーザーのノート"
_weekday: _weekday:
sunday: "日曜日" sunday: "日曜日"

View File

@ -0,0 +1,28 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class userGroupAntenna1581695816408 implements MigrationInterface {
name = 'userGroupAntenna1581695816408'
public async up(queryRunner: QueryRunner): Promise<any> {
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<any> {
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);
}
}

View File

@ -11,12 +11,17 @@
<option value="home">{{ $t('_antennaSources.homeTimeline') }}</option> <option value="home">{{ $t('_antennaSources.homeTimeline') }}</option>
<option value="users">{{ $t('_antennaSources.users') }}</option> <option value="users">{{ $t('_antennaSources.users') }}</option>
<option value="list">{{ $t('_antennaSources.userList') }}</option> <option value="list">{{ $t('_antennaSources.userList') }}</option>
<option value="group">{{ $t('_antennaSources.userGroup') }}</option>
</mk-select> </mk-select>
<mk-select v-model="userListId" v-if="src === 'list'"> <mk-select v-model="userListId" v-if="src === 'list'">
<template #label>{{ $t('userList') }}</template> <template #label>{{ $t('userList') }}</template>
<option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option> <option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option>
</mk-select> </mk-select>
<mk-textarea v-model="users" v-if="src === 'users'"> <mk-select v-model="userGroupId" v-else-if="src === 'group'">
<template #label>{{ $t('userGroup') }}</template>
<option v-for="group in userGroups" :value="group.id" :key="group.id">{{ group.name }}</option>
</mk-select>
<mk-textarea v-model="users" v-else-if="src === 'users'">
<span>{{ $t('users') }}</span> <span>{{ $t('users') }}</span>
<template #desc>{{ $t('antennaUsersDescription') }} <button class="_textButton" @click="addUser">{{ $t('addUser') }}</button></template> <template #desc>{{ $t('antennaUsersDescription') }} <button class="_textButton" @click="addUser">{{ $t('addUser') }}</button></template>
</mk-textarea> </mk-textarea>
@ -67,6 +72,7 @@ export default Vue.extend({
name: '', name: '',
src: '', src: '',
userListId: null, userListId: null,
userGroupId: null,
users: '', users: '',
keywords: '', keywords: '',
caseSensitive: false, caseSensitive: false,
@ -74,6 +80,7 @@ export default Vue.extend({
withFile: false, withFile: false,
notify: false, notify: false,
userLists: null, userLists: null,
userGroups: null,
faSave, faTrash faSave, faTrash
}; };
}, },
@ -83,6 +90,13 @@ export default Vue.extend({
if (this.src === 'list' && this.userLists === null) { if (this.src === 'list' && this.userLists === null) {
this.userLists = await this.$root.api('users/lists/list'); 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.name = this.antenna.name;
this.src = this.antenna.src; this.src = this.antenna.src;
this.userListId = this.antenna.userListId; this.userListId = this.antenna.userListId;
this.userGroupId = this.antenna.userGroupId;
this.users = this.antenna.users.join('\n'); this.users = this.antenna.users.join('\n');
this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n'); this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n');
this.caseSensitive = this.antenna.caseSensitive; this.caseSensitive = this.antenna.caseSensitive;
@ -105,6 +120,7 @@ export default Vue.extend({
name: this.name, name: this.name,
src: this.src, src: this.src,
userListId: this.userListId, userListId: this.userListId,
userGroupId: this.userGroupId,
withReplies: this.withReplies, withReplies: this.withReplies,
withFile: this.withFile, withFile: this.withFile,
notify: this.notify, notify: this.notify,
@ -119,6 +135,7 @@ export default Vue.extend({
name: this.name, name: this.name,
src: this.src, src: this.src,
userListId: this.userListId, userListId: this.userListId,
userGroupId: this.userGroupId,
withReplies: this.withReplies, withReplies: this.withReplies,
withFile: this.withFile, withFile: this.withFile,
notify: this.notify, notify: this.notify,

View File

@ -50,6 +50,7 @@ export default Vue.extend({
name: '', name: '',
src: 'all', src: 'all',
userListId: null, userListId: null,
userGroupId: null,
users: [], users: [],
keywords: [], keywords: [],
withReplies: false, withReplies: false,

View File

@ -1,9 +1,10 @@
import { Antenna } from '../models/entities/antenna'; import { Antenna } from '../models/entities/antenna';
import { Note } from '../models/entities/note'; import { Note } from '../models/entities/note';
import { User } from '../models/entities/user'; import { User } from '../models/entities/user';
import { UserListJoinings } from '../models'; import { UserListJoinings, UserGroupJoinings } from '../models';
import parseAcct from './acct/parse'; import parseAcct from './acct/parse';
import { getFullApAccount } from './convert-host'; import { getFullApAccount } from './convert-host';
import { ensure } from '../prelude/ensure';
export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: User, followers: User['id'][]): Promise<boolean> { export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: User, followers: User['id'][]): Promise<boolean> {
if (note.visibility === 'specified') return false; if (note.visibility === 'specified') return false;
@ -22,6 +23,14 @@ export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: Us
})).map(x => x.userId); })).map(x => x.userId);
if (!listUsers.includes(note.userId)) return false; 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') { } else if (antenna.src === 'users') {
const accts = antenna.users.map(x => { const accts = antenna.users.map(x => {
const { username, host } = parseAcct(x); const { username, host } = parseAcct(x);

View File

@ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
import { User } from './user'; import { User } from './user';
import { id } from '../id'; import { id } from '../id';
import { UserList } from './user-list'; import { UserList } from './user-list';
import { UserGroupJoining } from './user-group-joining';
@Entity() @Entity()
export class Antenna { export class Antenna {
@ -32,8 +33,8 @@ export class Antenna {
}) })
public name: string; public name: string;
@Column('enum', { enum: ['home', 'all', 'users', 'list'] }) @Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] })
public src: 'home' | 'all' | 'users' | 'list'; public src: 'home' | 'all' | 'users' | 'list' | 'group';
@Column({ @Column({
...id(), ...id(),
@ -47,6 +48,18 @@ export class Antenna {
@JoinColumn() @JoinColumn()
public userList: UserList | null; 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', { @Column('varchar', {
length: 1024, array: true, length: 1024, array: true,
default: '{}' default: '{}'

View File

@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { Antenna } from '../entities/antenna'; import { Antenna } from '../entities/antenna';
import { ensure } from '../../prelude/ensure'; import { ensure } from '../../prelude/ensure';
import { SchemaType } from '../../misc/schema'; import { SchemaType } from '../../misc/schema';
import { AntennaNotes } from '..'; import { AntennaNotes, UserGroupJoinings } from '..';
export type PackedAntenna = SchemaType<typeof packedAntennaSchema>; export type PackedAntenna = SchemaType<typeof packedAntennaSchema>;
@ -14,6 +14,7 @@ export class AntennaRepository extends Repository<Antenna> {
const antenna = typeof src === 'object' ? src : await this.findOne(src).then(ensure); const antenna = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null; const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null;
const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOne(antenna.userGroupJoiningId) : null;
return { return {
id: antenna.id, id: antenna.id,
@ -22,6 +23,7 @@ export class AntennaRepository extends Repository<Antenna> {
keywords: antenna.keywords, keywords: antenna.keywords,
src: antenna.src, src: antenna.src,
userListId: antenna.userListId, userListId: antenna.userListId,
userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
users: antenna.users, users: antenna.users,
caseSensitive: antenna.caseSensitive, caseSensitive: antenna.caseSensitive,
notify: antenna.notify, notify: antenna.notify,

View File

@ -1,7 +1,7 @@
import $ from 'cafy'; import $ from 'cafy';
import define from '../../define'; import define from '../../define';
import { genId } from '../../../../misc/gen-id'; import { genId } from '../../../../misc/gen-id';
import { Antennas, UserLists } from '../../../../models'; import { Antennas, UserLists, UserGroupJoinings } from '../../../../models';
import { ID } from '../../../../misc/cafy-id'; import { ID } from '../../../../misc/cafy-id';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
@ -18,13 +18,17 @@ export const meta = {
}, },
src: { src: {
validator: $.str.or(['home', 'all', 'users', 'list']) validator: $.str.or(['home', 'all', 'users', 'list', 'group'])
}, },
userListId: { userListId: {
validator: $.nullable.optional.type(ID), validator: $.nullable.optional.type(ID),
}, },
userGroupId: {
validator: $.nullable.optional.type(ID),
},
keywords: { keywords: {
validator: $.arr($.arr($.str)) validator: $.arr($.arr($.str))
}, },
@ -55,12 +59,19 @@ export const meta = {
message: 'No such user list.', message: 'No such user list.',
code: 'NO_SUCH_USER_LIST', code: 'NO_SUCH_USER_LIST',
id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f' 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) => { export default define(meta, async (ps, user) => {
let userList; let userList;
let userGroupJoining;
if (ps.src === 'list') { if (ps.src === 'list') {
userList = await UserLists.findOne({ userList = await UserLists.findOne({
@ -71,6 +82,15 @@ export default define(meta, async (ps, user) => {
if (userList == null) { if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList); 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({ const antenna = await Antennas.save({
@ -80,6 +100,7 @@ export default define(meta, async (ps, user) => {
name: ps.name, name: ps.name,
src: ps.src, src: ps.src,
userListId: userList ? userList.id : null, userListId: userList ? userList.id : null,
userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
keywords: ps.keywords, keywords: ps.keywords,
users: ps.users, users: ps.users,
caseSensitive: ps.caseSensitive, caseSensitive: ps.caseSensitive,

View File

@ -2,7 +2,7 @@ import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id'; import { ID } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import { Antennas, UserLists } from '../../../../models'; import { Antennas, UserLists, UserGroupJoinings } from '../../../../models';
export const meta = { export const meta = {
tags: ['antennas'], tags: ['antennas'],
@ -21,13 +21,17 @@ export const meta = {
}, },
src: { src: {
validator: $.str.or(['home', 'all', 'users', 'list']) validator: $.str.or(['home', 'all', 'users', 'list', 'group'])
}, },
userListId: { userListId: {
validator: $.nullable.optional.type(ID), validator: $.nullable.optional.type(ID),
}, },
userGroupId: {
validator: $.nullable.optional.type(ID),
},
keywords: { keywords: {
validator: $.arr($.arr($.str)) validator: $.arr($.arr($.str))
}, },
@ -64,6 +68,12 @@ export const meta = {
message: 'No such user list.', message: 'No such user list.',
code: 'NO_SUCH_USER_LIST', code: 'NO_SUCH_USER_LIST',
id: '1c6b35c9-943e-48c2-81e4-2844989407f7' 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 userList;
let userGroupJoining;
if (ps.src === 'list') { if (ps.src === 'list') {
userList = await UserLists.findOne({ userList = await UserLists.findOne({
@ -90,12 +101,22 @@ export default define(meta, async (ps, user) => {
if (userList == null) { if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList); 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, { await Antennas.update(antenna.id, {
name: ps.name, name: ps.name,
src: ps.src, src: ps.src,
userListId: userList ? userList.id : null, userListId: userList ? userList.id : null,
userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
keywords: ps.keywords, keywords: ps.keywords,
users: ps.users, users: ps.users,
caseSensitive: ps.caseSensitive, caseSensitive: ps.caseSensitive,