Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

virtual kemomimi relay timeline #174

Merged
merged 14 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 4 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8569,6 +8569,10 @@ export interface Locale extends ILocale {
* グローバル
*/
"global": string;
/**
* Virtual Kemomimi リレー
*/
"vmimiRelay": string;
};
"_play": {
/**
Expand Down
1 change: 1 addition & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2262,6 +2262,7 @@ _timelines:
local: "ローカル"
social: "ソーシャル"
global: "グローバル"
vmimiRelay: "Virtual Kemomimi リレー"
anatawa12 marked this conversation as resolved.
Show resolved Hide resolved

_play:
new: "Playの作成"
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/core/CoreModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,14 @@ import { ApMentionService } from './activitypub/models/ApMentionService.js';
import { ApNoteService } from './activitypub/models/ApNoteService.js';
import { ApPersonService } from './activitypub/models/ApPersonService.js';
import { ApQuestionService } from './activitypub/models/ApQuestionService.js';
import { VmimiRelayTimelineService } from './VmimiRelayTimelineService.js';
import { QueueModule } from './QueueModule.js';
import { QueueService } from './QueueService.js';
import { LoggerService } from './LoggerService.js';
import type { Provider } from '@nestjs/common';

//#region 文字列ベースでのinjection用(循環参照対応のため)
const $VmimiRelayTimelineService: Provider = { provide: 'VmimiRelayTimelineService', useExisting: VmimiRelayTimelineService };
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
Expand Down Expand Up @@ -282,6 +284,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
QueueModule,
],
providers: [
VmimiRelayTimelineService,
LoggerService,
AccountMoveService,
AccountUpdateService,
Expand Down Expand Up @@ -418,6 +421,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
QueueService,

//#region 文字列ベースでのinjection用(循環参照対応のため)
$VmimiRelayTimelineService,
$LoggerService,
$AccountMoveService,
$AccountUpdateService,
Expand Down Expand Up @@ -554,6 +558,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#endregion
],
exports: [
VmimiRelayTimelineService,
QueueModule,
LoggerService,
AccountMoveService,
Expand Down Expand Up @@ -690,6 +695,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
QueueService,

//#region 文字列ベースでのinjection用(循環参照対応のため)
$VmimiRelayTimelineService,
$LoggerService,
$AccountMoveService,
$AccountUpdateService,
Expand Down
81 changes: 81 additions & 0 deletions packages/backend/src/core/VmimiRelayTimelineService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Injectable } from '@nestjs/common';
import { Brackets, SelectQueryBuilder } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';

type VmimiInstanceList = { Url: string; }[];

@Injectable()
export class VmimiRelayTimelineService {
instanceHosts: Set<string>;
instanceHostsArray: string[];
lastUpdated: number;
updatePromise: Promise<void> | null;
private logger: Logger;

constructor(
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
) {
// Initialize with
this.instanceHosts = new Set<string>([]);
this.instanceHostsArray = [];
this.lastUpdated = 0;
this.updatePromise = null;

this.logger = this.loggerService.getLogger('vmimi');

this.checkForUpdateInstanceList();
}

@bindThis
checkForUpdateInstanceList() {
// one day
const UpdateInterval = 60 * 60 * 24;

if (this.updatePromise == null && this.lastUpdated + UpdateInterval < Date.now()) {
this.updatePromise = this.updateInstanceList().then(() => {
this.updatePromise = null;
});
}
}

@bindThis
async updateInstanceList() {
this.logger.info('Updating instance list');
const instanceList = await this.httpRequestService.getJson<VmimiInstanceList>('https://relay.virtualkemomimi.net/api/servers');
anatawa12 marked this conversation as resolved.
Show resolved Hide resolved
this.instanceHostsArray = instanceList.map(i => new URL(i.Url).host);
this.instanceHosts = new Set<string>(this.instanceHostsArray);
this.lastUpdated = Date.now();
this.logger.info(`Got instance list: ${this.instanceHostsArray}`);
}

@bindThis
isRelayedInstance(host: string | null): boolean {
this.checkForUpdateInstanceList();
// assuming the current instance is joined to the i relay
if (host == null) return true;
return this.instanceHosts.has(host);
}

get hostNames (): string[] {
this.checkForUpdateInstanceList();
return this.instanceHostsArray;
}

@bindThis
generateFilterQuery(query: SelectQueryBuilder<any>) {
query.andWhere(new Brackets(qb => {
qb
.andWhere('note.userHost IS NULL')
.orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: this.hostNames });
}));
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/server/ServerModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { AntennaChannelService } from './api/stream/channels/antenna.js';
import { ChannelChannelService } from './api/stream/channels/channel.js';
import { DriveChannelService } from './api/stream/channels/drive.js';
import { GlobalTimelineChannelService } from './api/stream/channels/global-timeline.js';
import { VmimiRelayTimelineChannelService } from './api/stream/channels/vmimi-relay-timeline.js';
import { HashtagChannelService } from './api/stream/channels/hashtag.js';
import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
Expand Down Expand Up @@ -78,6 +79,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
ChannelChannelService,
DriveChannelService,
GlobalTimelineChannelService,
VmimiRelayTimelineChannelService,
HashtagChannelService,
RoleTimelineChannelService,
ReversiChannelService,
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/server/api/EndpointsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_vmimiRelayTimeline from './endpoints/notes/vmimi-relay-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
Expand Down Expand Up @@ -644,6 +645,7 @@ const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create'
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default };
const $notes_vmimiRelayTimeline: Provider = { provide: 'ep:notes/vmimi-relay-timeline', useClass: ep___notes_vmimiRelayTimeline.default };
const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default };
const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default };
const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
Expand Down Expand Up @@ -1020,6 +1022,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_favorites_delete,
$notes_featured,
$notes_globalTimeline,
$notes_vmimiRelayTimeline,
$notes_hybridTimeline,
$notes_localTimeline,
$notes_mentions,
Expand Down Expand Up @@ -1390,6 +1393,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_favorites_delete,
$notes_featured,
$notes_globalTimeline,
$notes_vmimiRelayTimeline,
$notes_hybridTimeline,
$notes_localTimeline,
$notes_mentions,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/server/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ import * as ep___notes_favorites_create from './endpoints/notes/favorites/create
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_vmimiRelayTimeline from './endpoints/notes/vmimi-relay-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
Expand Down Expand Up @@ -642,6 +643,7 @@ const eps = [
['notes/favorites/delete', ep___notes_favorites_delete],
['notes/featured', ep___notes_featured],
['notes/global-timeline', ep___notes_globalTimeline],
['notes/vmimi-relay-timeline', ep___notes_vmimiRelayTimeline],
['notes/hybrid-timeline', ep___notes_hybridTimeline],
['notes/local-timeline', ep___notes_localTimeline],
['notes/mentions', ep___notes_mentions],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
anatawa12 marked this conversation as resolved.
Show resolved Hide resolved
* SPDX-FileCopyrightText: anatawa12
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import type { NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { VmimiRelayTimelineService } from '@/core/VmimiRelayTimelineService.js';
import { ApiError } from '../../error.js';

export const meta = {
tags: ['notes'],

res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},

errors: {
gtlDisabled: {
message: 'Global timeline has been disabled.',
code: 'GTL_DISABLED',
id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b',
},
},
} as const;

export const paramDef = {
type: 'object',
properties: {
withFiles: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;

@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,

private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private vmimiRelayTimelineService: VmimiRelayTimelineService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
if (!policies.gtlAvailable) {
throw new ApiError(meta.errors.gtlDisabled);
}

//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.visibility = \'public\'')
.andWhere('note.channelId IS NULL')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');

this.vmimiRelayTimelineService.generateFilterQuery(query);

if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}

if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}

if (ps.withRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.where('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.where('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
//#endregion

const timeline = await query.limit(ps.limit).getMany();

process.nextTick(() => {
if (me) {
this.activeUsersChart.read(me);
}
});

return await this.noteEntityService.packMany(timeline, me);
});
}
}
3 changes: 3 additions & 0 deletions packages/backend/src/server/api/stream/ChannelsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { HybridTimelineChannelService } from './channels/hybrid-timeline.js';
import { LocalTimelineChannelService } from './channels/local-timeline.js';
import { HomeTimelineChannelService } from './channels/home-timeline.js';
import { GlobalTimelineChannelService } from './channels/global-timeline.js';
import { VmimiRelayTimelineChannelService } from './channels/vmimi-relay-timeline.js';
import { MainChannelService } from './channels/main.js';
import { ChannelChannelService } from './channels/channel.js';
import { AdminChannelService } from './channels/admin.js';
Expand All @@ -31,6 +32,7 @@ export class ChannelsService {
private localTimelineChannelService: LocalTimelineChannelService,
private hybridTimelineChannelService: HybridTimelineChannelService,
private globalTimelineChannelService: GlobalTimelineChannelService,
private vmimiRelayTimelineChannelService: VmimiRelayTimelineChannelService,
private userListChannelService: UserListChannelService,
private hashtagChannelService: HashtagChannelService,
private roleTimelineChannelService: RoleTimelineChannelService,
Expand All @@ -53,6 +55,7 @@ export class ChannelsService {
case 'localTimeline': return this.localTimelineChannelService;
case 'hybridTimeline': return this.hybridTimelineChannelService;
case 'globalTimeline': return this.globalTimelineChannelService;
case 'vmimiRelayTimeline': return this.vmimiRelayTimelineChannelService;
case 'userList': return this.userListChannelService;
case 'hashtag': return this.hashtagChannelService;
case 'roleTimeline': return this.roleTimelineChannelService;
Expand Down
Loading
Loading