Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Unreleased

### General
-
- Feat: アンテナを公開して他のユーザーが購読・お気に入りできるように (#11132)

### Client
-
Expand Down
8 changes: 8 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2540,6 +2540,8 @@ _permissions:
"read:invite-codes": "招待コードを取得する"
"write:clip-favorite": "クリップのいいねを操作する"
"read:clip-favorite": "クリップのいいねを見る"
"write:antenna-favorite": "アンテナのお気に入りを操作する"
"read:antenna-favorite": "アンテナのお気に入りを見る"
"read:federation": "連合に関する情報を取得する"
"write:report-abuse": "違反を報告する"
"write:chat": "ダイレクトメッセージを操作する"
Expand Down Expand Up @@ -2567,6 +2569,12 @@ _antennaSources:
userList: "指定したリストのユーザーのノート"
userBlacklist: "指定した一人または複数のユーザーを除いた全てのノート"

_antenna:
public: "公開する"
publicDescription: "公開すると、他のユーザーがこのアンテナをタイムラインとして購読・お気に入りできます。あなたから見えるノートのうち、購読ユーザーから見えるものだけが配信されます。"
publicConditionsExposed: "アンテナのキーワードや指定したユーザー等の条件も、他のユーザーから閲覧可能になります。"
favoritedPublicAntennas: "お気に入りの公開アンテナ"

_weekday:
sunday: "日曜日"
monday: "月曜日"
Expand Down
41 changes: 41 additions & 0 deletions packages/backend/migration/1779105539395-antenna-share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* @class
* @implements {MigrationInterface}
*/
export class AntennaShare1779105539395 {
name = 'AntennaShare1779105539395'

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "antenna_favorite" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "antennaId" character varying(32) NOT NULL, CONSTRAINT "PK_fefb22a55e21904d2ff5b26eb23" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_ce5de8b5c18d8d9b77132e1be5" ON "antenna_favorite" ("userId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_82b4a53f04dd7b3fcb217b1436" ON "antenna_favorite" ("userId", "antennaId") `);
await queryRunner.query(`ALTER TABLE "antenna" ADD "isPublic" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "antenna_favorite" ADD CONSTRAINT "FK_ce5de8b5c18d8d9b77132e1be5f" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "antenna_favorite" ADD CONSTRAINT "FK_347623935a4b0999e05a93f5175" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna_favorite" DROP CONSTRAINT "FK_347623935a4b0999e05a93f5175"`);
await queryRunner.query(`ALTER TABLE "antenna_favorite" DROP CONSTRAINT "FK_ce5de8b5c18d8d9b77132e1be5f"`);
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "isPublic"`);
await queryRunner.query(`DROP INDEX "public"."IDX_82b4a53f04dd7b3fcb217b1436"`);
await queryRunner.query(`DROP INDEX "public"."IDX_ce5de8b5c18d8d9b77132e1be5"`);
await queryRunner.query(`DROP TABLE "antenna_favorite"`);
}
}
33 changes: 30 additions & 3 deletions packages/backend/src/core/entities/AntennaEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,40 @@

import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { AntennasRepository } from '@/models/_.js';
import type { AntennaFavoritesRepository, AntennasRepository, MiUser } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js';
import type { MiAntenna } from '@/models/Antenna.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { UserEntityService } from './UserEntityService.js';

@Injectable()
export class AntennaEntityService {
constructor(
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,

@Inject(DI.antennaFavoritesRepository)
private antennaFavoritesRepository: AntennaFavoritesRepository,

private userEntityService: UserEntityService,
private idService: IdService,
) {
}

@bindThis
public async pack(
src: MiAntenna['id'] | MiAntenna,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>,
},
): Promise<Packed<'Antenna'>> {
const meId = me ? me.id : null;
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });

return {
return await awaitAll({
id: antenna.id,
createdAt: this.idService.parse(antenna.id).date.toISOString(),
name: antenna.name,
Expand All @@ -45,6 +56,22 @@ export class AntennaEntityService {
isActive: antenna.isActive,
hasUnreadNote: false, // TODO
notify: false, // 後方互換性のため
};
isPublic: antenna.isPublic,
userId: antenna.userId,
user: hint?.packedUser ?? this.userEntityService.pack(antenna.user ?? antenna.userId),
favoritedCount: await this.antennaFavoritesRepository.countBy({ antennaId: antenna.id }),
isFavorited: meId ? await this.antennaFavoritesRepository.exists({ where: { antennaId: antenna.id, userId: meId } }) : undefined,
});
}

@bindThis
public async packMany(
antennas: MiAntenna[],
me?: { id: MiUser['id'] } | null | undefined,
) {
const _users = antennas.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(antennas.map(antenna => this.pack(antenna, me, { packedUser: _userMap.get(antenna.userId) })));
}
}
1 change: 1 addition & 0 deletions packages/backend/src/di-symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const DI = {
clipNotesRepository: Symbol('clipNotesRepository'),
clipFavoritesRepository: Symbol('clipFavoritesRepository'),
antennasRepository: Symbol('antennasRepository'),
antennaFavoritesRepository: Symbol('antennaFavoritesRepository'),
promoNotesRepository: Symbol('promoNotesRepository'),
promoReadsRepository: Symbol('promoReadsRepository'),
relaysRepository: Symbol('relaysRepository'),
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/models/Antenna.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export class MiAntenna {
default: false,
})
public excludeNotesInSensitiveChannel: boolean;

@Column('boolean', {
default: false,
})
public isPublic: boolean;
}
// Note for future developers: When you added a new column,
// You should update ExportAntennaProcessorService and ImportAntennaProcessorService
Expand Down
35 changes: 35 additions & 0 deletions packages/backend/src/models/AntennaFavorite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiAntenna } from './Antenna.js';

@Entity('antenna_favorite')
@Index(['userId', 'antennaId'], { unique: true })
export class MiAntennaFavorite {
@PrimaryColumn(id())
public id: string;

@Index()
@Column(id())
public userId: MiUser['id'];

@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;

@Column(id())
public antennaId: MiAntenna['id'];

@ManyToOne(() => MiAntenna, {
onDelete: 'CASCADE',
})
@JoinColumn()
public antenna: MiAntenna | null;
}
9 changes: 9 additions & 0 deletions packages/backend/src/models/RepositoryModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
MiAnnouncement,
MiAnnouncementRead,
MiAntenna,
MiAntennaFavorite,
MiApp,
MiAuthSession,
MiAvatarDecoration,
Expand Down Expand Up @@ -394,6 +395,12 @@ const $antennasRepository: Provider = {
inject: [DI.db],
};

const $antennaFavoritesRepository: Provider = {
provide: DI.antennaFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiAntennaFavorite).extend(miRepository as MiRepository<MiAntennaFavorite>),
inject: [DI.db],
};

const $promoNotesRepository: Provider = {
provide: DI.promoNotesRepository,
useFactory: (db: DataSource) => db.getRepository(MiPromoNote).extend(miRepository as MiRepository<MiPromoNote>),
Expand Down Expand Up @@ -598,6 +605,7 @@ const $reversiGamesRepository: Provider = {
$clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository,
$antennaFavoritesRepository,
$promoNotesRepository,
$promoReadsRepository,
$relaysRepository,
Expand Down Expand Up @@ -676,6 +684,7 @@ const $reversiGamesRepository: Provider = {
$clipNotesRepository,
$clipFavoritesRepository,
$antennasRepository,
$antennaFavoritesRepository,
$promoNotesRepository,
$promoReadsRepository,
$relaysRepository,
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/models/_.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { MiAd } from '@/models/Ad.js';
import { MiAnnouncement } from '@/models/Announcement.js';
import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
import { MiAntenna } from '@/models/Antenna.js';
import { MiAntennaFavorite } from '@/models/AntennaFavorite.js';
import { MiApp } from '@/models/App.js';
import { MiAuthSession } from '@/models/AuthSession.js';
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
Expand Down Expand Up @@ -104,6 +105,7 @@ export {
MiAnnouncement,
MiAnnouncementRead,
MiAntenna,
MiAntennaFavorite,
MiApp,
MiAvatarDecoration,
MiAuthSession,
Expand Down Expand Up @@ -184,6 +186,7 @@ export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
export type AnnouncementReadsRepository = Repository<MiAnnouncementRead> & MiRepository<MiAnnouncementRead>;
export type AntennasRepository = Repository<MiAntenna> & MiRepository<MiAntenna>;
export type AntennaFavoritesRepository = Repository<MiAntennaFavorite> & MiRepository<MiAntennaFavorite>;
export type AppsRepository = Repository<MiApp> & MiRepository<MiApp>;
export type AvatarDecorationsRepository = Repository<MiAvatarDecoration> & MiRepository<MiAvatarDecoration>;
export type AuthSessionsRepository = Repository<MiAuthSession> & MiRepository<MiAuthSession>;
Expand Down
23 changes: 23 additions & 0 deletions packages/backend/src/models/json-schema/antenna.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,28 @@ export const packedAntennaSchema = {
optional: false, nullable: false,
default: false,
},
isPublic: {
type: 'boolean',
optional: false, nullable: false,
default: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
favoritedCount: {
type: 'number',
optional: false, nullable: false,
},
isFavorited: {
type: 'boolean',
optional: true, nullable: false,
},
},
} as const;
2 changes: 2 additions & 0 deletions packages/backend/src/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { MiAd } from '@/models/Ad.js';
import { MiAnnouncement } from '@/models/Announcement.js';
import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
import { MiAntenna } from '@/models/Antenna.js';
import { MiAntennaFavorite } from '@/models/AntennaFavorite.js';
import { MiApp } from '@/models/App.js';
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
import { MiAuthSession } from '@/models/AuthSession.js';
Expand Down Expand Up @@ -227,6 +228,7 @@ export const entities = [
MiClipNote,
MiClipFavorite,
MiAntenna,
MiAntennaFavorite,
MiPromoNote,
MiPromoRead,
MiRelay,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export class ExportAntennasProcessorService {
withReplies: antenna.withReplies,
withFile: antenna.withFile,
excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel,
isPublic: antenna.isPublic,
} satisfies Required<ExportedAntenna>));
if (antennas.length - 1 !== index) {
write(', ');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const exportedAntennaSchema = {
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
excludeNotesInSensitiveChannel: { type: 'boolean' },
isPublic: { type: 'boolean' },
},
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
} as const satisfies Schema;
Expand Down Expand Up @@ -98,6 +99,7 @@ export class ImportAntennasProcessorService {
withReplies: antenna.withReplies,
withFile: antenna.withFile,
excludeNotesInSensitiveChannel: antenna.excludeNotesInSensitiveChannel,
isPublic: antenna.isPublic,
});
this.logger.succ('Antenna created: ' + result.id);
this.globalEventService.publishInternalEvent('antennaCreated', result);
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/server/api/endpoint-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,12 @@ export * as 'announcements' from './endpoints/announcements.js';
export * as 'announcements/show' from './endpoints/announcements/show.js';
export * as 'antennas/create' from './endpoints/antennas/create.js';
export * as 'antennas/delete' from './endpoints/antennas/delete.js';
export * as 'antennas/favorite' from './endpoints/antennas/favorite.js';
export * as 'antennas/list' from './endpoints/antennas/list.js';
export * as 'antennas/my-favorites' from './endpoints/antennas/my-favorites.js';
export * as 'antennas/notes' from './endpoints/antennas/notes.js';
export * as 'antennas/show' from './endpoints/antennas/show.js';
export * as 'antennas/unfavorite' from './endpoints/antennas/unfavorite.js';
export * as 'antennas/update' from './endpoints/antennas/update.js';
export * as 'ap/get' from './endpoints/ap/get.js';
export * as 'ap/show' from './endpoints/ap/show.js';
Expand Down Expand Up @@ -387,6 +390,7 @@ export * as 'test' from './endpoints/test.js';
export * as 'username/available' from './endpoints/username/available.js';
export * as 'users' from './endpoints/users.js';
export * as 'users/achievements' from './endpoints/users/achievements.js';
export * as 'users/antennas' from './endpoints/users/antennas.js';
export * as 'users/clips' from './endpoints/users/clips.js';
export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
export * as 'users/flashs' from './endpoints/users/flashs.js';
Expand Down
4 changes: 3 additions & 1 deletion packages/backend/src/server/api/endpoints/antennas/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const paramDef = {
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
excludeNotesInSensitiveChannel: { type: 'boolean' },
isPublic: { type: 'boolean' },
},
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
} as const;
Expand Down Expand Up @@ -135,11 +136,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withReplies: ps.withReplies,
withFile: ps.withFile,
excludeNotesInSensitiveChannel: ps.excludeNotesInSensitiveChannel,
isPublic: ps.isPublic ?? false,
});

this.globalEventService.publishInternalEvent('antennaCreated', antenna);

return await this.antennaEntityService.pack(antenna);
return await this.antennaEntityService.pack(antenna, me);
});
}
}
Loading
Loading