Skip to content

Commit

Permalink
Merge pull request #174 from anatawa12/vmimi-relay-timeline
Browse files Browse the repository at this point in the history
virtual kemomimi relay timeline
  • Loading branch information
anatawa12 authored Apr 12, 2024
2 parents cfd8fbe + 16193fe commit 8e6f4f7
Show file tree
Hide file tree
Showing 22 changed files with 511 additions and 9 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

## 202x.x.x-kinel.x (unreleased)

### General
- Enhance: ぶいみみリレータイムラインを追加しました
- ぶいみみリレータイムラインは、[Virtual Kemomimiリレー]に参加しているサーバーからのノートのみが流れるタイムラインです

[Virtual Kemomimiリレー]: https://relay.virtualkemomimi.net/

### Client
- Enhance: 画像アップロード時に縮小する場合の大きさを2048x2048以下から2560x2560以下に変更しました
- 既存のファイルは更新されず、新規アップロード分にのみ適用されます
Expand Down
4 changes: 4 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8597,6 +8597,10 @@ export interface Locale extends ILocale {
* グローバル
*/
"global": string;
/**
* ぶいみみリレー
*/
"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 @@ -2269,6 +2269,7 @@ _timelines:
local: "ローカル"
social: "ソーシャル"
global: "グローバル"
vmimiRelay: "ぶいみみリレー"

_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,13 +136,15 @@ 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 { AbuseDiscordHookService } from './AbuseDiscordHookService.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 @@ -283,6 +285,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
QueueModule,
],
providers: [
VmimiRelayTimelineService,
LoggerService,
AccountMoveService,
AccountUpdateService,
Expand Down Expand Up @@ -420,6 +423,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
AbuseDiscordHookService,

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

//#region 文字列ベースでのinjection用(循環参照対応のため)
$VmimiRelayTimelineService,
$LoggerService,
$AccountMoveService,
$AccountUpdateService,
Expand Down
96 changes: 96 additions & 0 deletions packages/backend/src/core/VmimiRelayTimelineService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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; }[];

// one day
const UpdateInterval = 1000 * 60 * 60 * 24;
const RetryInterval = 1000 * 60 * 60 * 6;

@Injectable()
export class VmimiRelayTimelineService {
instanceHosts: Set<string>;
instanceHostsArray: string[];
nextUpdate: 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.nextUpdate = 0;
this.updatePromise = null;

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

this.checkForUpdateInstanceList();
}

@bindThis
checkForUpdateInstanceList() {
if (this.updatePromise == null && this.nextUpdate < Date.now()) {
this.updatePromise = this.updateInstanceList().finally(() => this.updatePromise = null);
}
}

@bindThis
async updateInstanceList() {
try {
this.logger.info('Updating instance list');
const instanceList = await this.httpRequestService.getJson<VmimiInstanceList>('https://relay.virtualkemomimi.net/api/servers');
this.instanceHostsArray = instanceList.map(i => new URL(i.Url).host);
this.instanceHosts = new Set<string>(this.instanceHostsArray);
this.nextUpdate = Date.now() + UpdateInterval;
this.logger.info(`Got instance list: ${this.instanceHostsArray}`);
} catch (e) {
this.logger.error('Failed to update instance list', e as any);
this.nextUpdate = Date.now() + RetryInterval;
setTimeout(() => this.checkForUpdateInstanceList(), RetryInterval + 5);
}
}

@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>, excludeReplies: boolean) {
const names = this.hostNames;
query.andWhere(new Brackets(qb => {
qb.where('note.userHost IS NULL');
if (names.length !== 0) {
qb.orWhere('note.userHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: names });
}
}));
if (excludeReplies) {
query.andWhere(new Brackets(qb => {
qb.where('note.replyUserHost IS NULL');
if (names.length !== 0) {
qb.orWhere('note.replyUserHost IN (:...vmimiRelayInstances)', { vmimiRelayInstances: names });
}
}));
}
}
}
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 @@ -274,6 +274,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 @@ -648,6 +649,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 @@ -1026,6 +1028,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 @@ -1398,6 +1401,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 @@ -274,6 +274,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 @@ -646,6 +647,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,118 @@
/*
* 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 },
withReplies: { type: 'boolean', default: false },
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, !ps.withReplies);

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

0 comments on commit 8e6f4f7

Please sign in to comment.