cleanup: trim trailing whitespace (#11136)

* cleanup: trim trailing whitespace

* update(`.editorconfig`)

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
okayurisotto 2023-07-08 07:08:16 +09:00 committed by GitHub
parent 4c879b3a33
commit d84796588c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
161 changed files with 615 additions and 609 deletions

View File

@ -2,7 +2,7 @@ version: '3.8'
services: services:
app: app:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@ -6,6 +6,10 @@ indent_size = 2
charset = utf-8 charset = utf-8
insert_final_newline = true insert_final_newline = true
end_of_line = lf end_of_line = lf
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}] [*.{yml,yaml}]
indent_style = space indent_style = space

View File

@ -106,7 +106,7 @@ If your language is not listed in Crowdin, please open an issue.
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
## Development ## Development
During development, it is useful to use the During development, it is useful to use the
``` ```
pnpm dev pnpm dev
@ -150,7 +150,7 @@ Prepare DB/Redis for testing.
``` ```
docker compose -f packages/backend/test/docker-compose.yml up docker compose -f packages/backend/test/docker-compose.yml up
``` ```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
Run all test. Run all test.
``` ```

View File

@ -2,9 +2,9 @@
<a href="https://misskey-hub.net"> <a href="https://misskey-hub.net">
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/> <img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
</a> </a>
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀** **🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
--- ---
<a href="https://misskey-hub.net/instances.html"> <a href="https://misskey-hub.net/instances.html">
@ -21,7 +21,7 @@
<a href="https://www.patreon.com/syuilo"> <a href="https://www.patreon.com/syuilo">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a> <img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
--- ---
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey) [![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)

View File

@ -23,13 +23,13 @@
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<style> <style>
#g8 { #g8 {
animation-name: floating; animation-name: floating;
animation-duration: 3s; animation-duration: 3s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
} }
@keyframes floating { @keyframes floating {
0% { transform: translate(0, 0px); } 0% { transform: translate(0, 0px); }
50% { transform: translate(0, -5px); } 50% { transform: translate(0, -5px); }

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -56,7 +56,7 @@ describe('After setup instance', () => {
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click(); cy.get('[data-cy-signup-rules-continue]').click();
cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-username] input').type('alice'); cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-submit]').should('be.disabled');

View File

@ -295,7 +295,7 @@ export class AccountMoveService {
* dstユーザーのalsoKnownAsをfetchPersonしていきmovedToUrlをdstに指定するユーザーが存在するのかを調べる * dstユーザーのalsoKnownAsをfetchPersonしていきmovedToUrlをdstに指定するユーザーが存在するのかを調べる
* *
* @param dst movedToUrlを指定するユーザー * @param dst movedToUrlを指定するユーザー
* @param check * @param check
* @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか * @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
* @returns Promise<LocalUser | RemoteUser | null> * @returns Promise<LocalUser | RemoteUser | null>
*/ */

View File

@ -31,16 +31,16 @@ export class AiService {
const cpuFlags = await this.getCpuFlags(); const cpuFlags = await this.getCpuFlags();
isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required)); isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required));
} }
if (!isSupportedCpu) { if (!isSupportedCpu) {
console.error('This CPU cannot use TensorFlow.'); console.error('This CPU cannot use TensorFlow.');
return null; return null;
} }
const tf = await import('@tensorflow/tfjs-node'); const tf = await import('@tensorflow/tfjs-node');
if (this.model == null) this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); if (this.model == null) this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
const buffer = await fs.promises.readFile(path); const buffer = await fs.promises.readFile(path);
const image = await tf.node.decodeImage(buffer, 3) as any; const image = await tf.node.decodeImage(buffer, 3) as any;
try { try {

View File

@ -99,7 +99,7 @@ export class AntennaService implements OnApplicationShutdown {
'MAXLEN', '~', '200', 'MAXLEN', '~', '200',
'*', '*',
'note', note.id); 'note', note.id);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note); this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
} }
@ -112,16 +112,16 @@ export class AntennaService implements OnApplicationShutdown {
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> { public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
if (note.visibility === 'specified') return false; if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false; if (note.visibility === 'followers') return false;
if (!antenna.withReplies && note.replyId != null) return false; if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === 'home') { if (antenna.src === 'home') {
// TODO // TODO
} else if (antenna.src === 'list') { } else if (antenna.src === 'list') {
const listUsers = (await this.userListJoiningsRepository.findBy({ const listUsers = (await this.userListJoiningsRepository.findBy({
userListId: antenna.userListId!, userListId: antenna.userListId!,
})).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 === 'users') { } else if (antenna.src === 'users') {
const accts = antenna.users.map(x => { const accts = antenna.users.map(x => {
@ -130,32 +130,32 @@ export class AntennaService implements OnApplicationShutdown {
}); });
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
} }
const keywords = antenna.keywords const keywords = antenna.keywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0); .filter(xs => xs.length > 0);
if (keywords.length > 0) { if (keywords.length > 0) {
if (note.text == null && note.cw == null) return false; if (note.text == null && note.cw == null) return false;
const _text = (note.text ?? '') + '\n' + (note.cw ?? ''); const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
const matched = keywords.some(and => const matched = keywords.some(and =>
and.every(keyword => and.every(keyword =>
antenna.caseSensitive antenna.caseSensitive
? _text.includes(keyword) ? _text.includes(keyword)
: _text.toLowerCase().includes(keyword.toLowerCase()), : _text.toLowerCase().includes(keyword.toLowerCase()),
)); ));
if (!matched) return false; if (!matched) return false;
} }
const excludeKeywords = antenna.excludeKeywords const excludeKeywords = antenna.excludeKeywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0); .filter(xs => xs.length > 0);
if (excludeKeywords.length > 0) { if (excludeKeywords.length > 0) {
if (note.text == null && note.cw == null) return false; if (note.text == null && note.cw == null) return false;
@ -167,16 +167,16 @@ export class AntennaService implements OnApplicationShutdown {
? _text.includes(keyword) ? _text.includes(keyword)
: _text.toLowerCase().includes(keyword.toLowerCase()), : _text.toLowerCase().includes(keyword.toLowerCase()),
)); ));
if (matched) return false; if (matched) return false;
} }
if (antenna.withFile) { if (antenna.withFile) {
if (note.fileIds && note.fileIds.length === 0) return false; if (note.fileIds && note.fileIds.length === 0) return false;
} }
// TODO: eval expression // TODO: eval expression
return true; return true;
} }
@ -188,7 +188,7 @@ export class AntennaService implements OnApplicationShutdown {
}); });
this.antennasFetched = true; this.antennasFetched = true;
} }
return this.antennas; return this.antennas;
} }

View File

@ -20,7 +20,7 @@ export class CaptchaService {
secret, secret,
response, response,
}); });
const res = await this.httpRequestService.send(url, { const res = await this.httpRequestService.send(url, {
method: 'POST', method: 'POST',
body: params.toString(), body: params.toString(),
@ -28,14 +28,14 @@ export class CaptchaService {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
}, { throwErrorWhenResponseNotOk: false }); }, { throwErrorWhenResponseNotOk: false });
if (!res.ok) { if (!res.ok) {
throw new Error(`${res.status}`); throw new Error(`${res.status}`);
} }
return await res.json() as CaptchaResponse; return await res.json() as CaptchaResponse;
} }
@bindThis @bindThis
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> { public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
if (response == null) { if (response == null) {
@ -73,7 +73,7 @@ export class CaptchaService {
if (response == null) { if (response == null) {
throw new Error('turnstile-failed: no response provided'); throw new Error('turnstile-failed: no response provided');
} }
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
throw new Error(`turnstile-request-failed: ${err}`); throw new Error(`turnstile-request-failed: ${err}`);
}); });

View File

@ -25,27 +25,27 @@ export class CreateSystemUserService {
@bindThis @bindThis
public async createSystemUser(username: string): Promise<User> { public async createSystemUser(username: string): Promise<User> {
const password = uuid(); const password = uuid();
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt); const hash = await bcrypt.hash(password, salt);
// Generate secret // Generate secret
const secret = generateNativeUserToken(); const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair(4096); const keyPair = await genRsaKeyPair(4096);
let account!: User; let account!: User;
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(User, { const exist = await transactionalEntityManager.findOneBy(User, {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: IsNull(), host: IsNull(),
}); });
if (exist) throw new Error('the user is already exists'); if (exist) throw new Error('the user is already exists');
account = await transactionalEntityManager.insert(User, { account = await transactionalEntityManager.insert(User, {
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@ -58,25 +58,25 @@ export class CreateSystemUserService {
isExplorable: false, isExplorable: false,
isBot: true, isBot: true,
}).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0])); }).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0]));
await transactionalEntityManager.insert(UserKeypair, { await transactionalEntityManager.insert(UserKeypair, {
publicKey: keyPair.publicKey, publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey, privateKey: keyPair.privateKey,
userId: account.id, userId: account.id,
}); });
await transactionalEntityManager.insert(UserProfile, { await transactionalEntityManager.insert(UserProfile, {
userId: account.id, userId: account.id,
autoAcceptFollowed: false, autoAcceptFollowed: false,
password: hash, password: hash,
}); });
await transactionalEntityManager.insert(UsedUsername, { await transactionalEntityManager.insert(UsedUsername, {
createdAt: new Date(), createdAt: new Date(),
username: username.toLowerCase(), username: username.toLowerCase(),
}); });
}); });
return account; return account;
} }
} }

View File

@ -140,7 +140,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
this.globalEventService.publishBroadcastStream('emojiAdded', { this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated, emoji: updated,
}); });
} }
} }
@ -194,7 +194,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
} }
this.localEmojisCache.refresh(); this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', { this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids), emojis: await this.emojiEntityService.packDetailedMany(ids),
}); });
@ -215,7 +215,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
emojis: await this.emojiEntityService.packDetailedMany(ids), emojis: await this.emojiEntityService.packDetailedMany(ids),
}); });
} }
@bindThis @bindThis
public async setLicenseBulk(ids: Emoji['id'][], license: string | null) { public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
await this.emojisRepository.update({ await this.emojisRepository.update({

View File

@ -28,11 +28,11 @@ export class DeleteAccountService {
// 物理削除する前にDelete activityを送信する // 物理削除する前にDelete activityを送信する
await this.userSuspendService.doPostSuspend(user).catch(e => {}); await this.userSuspendService.doPostSuspend(user).catch(e => {});
this.queueService.createDeleteAccountJob(user, { this.queueService.createDeleteAccountJob(user, {
soft: false, soft: false,
}); });
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
isDeleted: true, isDeleted: true,
}); });

View File

@ -29,12 +29,12 @@ export class EmailService {
@bindThis @bindThis
public async sendEmail(to: string, subject: string, html: string, text: string) { public async sendEmail(to: string, subject: string, html: string, text: string) {
const meta = await this.metaService.fetch(true); const meta = await this.metaService.fetch(true);
const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
const emailSettingUrl = `${this.config.url}/settings/email`; const emailSettingUrl = `${this.config.url}/settings/email`;
const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: meta.smtpHost, host: meta.smtpHost,
port: meta.smtpPort, port: meta.smtpPort,
@ -46,7 +46,7 @@ export class EmailService {
pass: meta.smtpPass, pass: meta.smtpPass,
} : undefined, } : undefined,
} as any); } as any);
try { try {
// TODO: htmlサニタイズ // TODO: htmlサニタイズ
const info = await transporter.sendMail({ const info = await transporter.sendMail({
@ -135,7 +135,7 @@ export class EmailService {
</body> </body>
</html>`, </html>`,
}); });
this.logger.info(`Message sent: ${info.messageId}`); this.logger.info(`Message sent: ${info.messageId}`);
} catch (err) { } catch (err) {
this.logger.error(err as Error); this.logger.error(err as Error);
@ -149,12 +149,12 @@ export class EmailService {
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
}> { }> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
const exist = await this.userProfilesRepository.countBy({ const exist = await this.userProfilesRepository.countBy({
emailVerified: true, emailVerified: true,
email: emailAddress, email: emailAddress,
}); });
const validated = meta.enableActiveEmailValidation ? await validateEmail({ const validated = meta.enableActiveEmailValidation ? await validateEmail({
email: emailAddress, email: emailAddress,
validateRegex: true, validateRegex: true,
@ -163,9 +163,9 @@ export class EmailService {
validateDisposable: true, // 捨てアドかどうかチェック validateDisposable: true, // 捨てアドかどうかチェック
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
}) : { valid: true, reason: null }; }) : { valid: true, reason: null };
const available = exist === 0 && validated.valid; const available = exist === 0 && validated.valid;
return { return {
available, available,
reason: available ? null : reason: available ? null :

View File

@ -43,19 +43,19 @@ export class FederatedInstanceService implements OnApplicationShutdown {
@bindThis @bindThis
public async fetch(host: string): Promise<Instance> { public async fetch(host: string): Promise<Instance> {
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
const cached = await this.federatedInstanceCache.get(host); const cached = await this.federatedInstanceCache.get(host);
if (cached) return cached; if (cached) return cached;
const index = await this.instancesRepository.findOneBy({ host }); const index = await this.instancesRepository.findOneBy({ host });
if (index == null) { if (index == null) {
const i = await this.instancesRepository.insert({ const i = await this.instancesRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
host, host,
firstRetrievedAt: new Date(), firstRetrievedAt: new Date(),
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
this.federatedInstanceCache.set(host, i); this.federatedInstanceCache.set(host, i);
return i; return i;
} else { } else {
@ -74,7 +74,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
.then((response) => { .then((response) => {
return response.raw[0]; return response.raw[0];
}); });
this.federatedInstanceCache.set(result.host, result); this.federatedInstanceCache.set(result.host, result);
} }

View File

@ -58,6 +58,8 @@ export class FetchInstanceMetadataService {
@bindThis @bindThis
public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> { public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
try {
const host = instance.host; const host = instance.host;
// Acquire mutex to ensure no parallel runs // Acquire mutex to ensure no parallel runs
if (!await this.tryLock(host)) return; if (!await this.tryLock(host)) return;
@ -72,13 +74,13 @@ export class FetchInstanceMetadataService {
} }
this.logger.info(`Fetching metadata of ${instance.host} ...`); this.logger.info(`Fetching metadata of ${instance.host} ...`);
const [info, dom, manifest] = await Promise.all([ const [info, dom, manifest] = await Promise.all([
this.fetchNodeinfo(instance).catch(() => null), this.fetchNodeinfo(instance).catch(() => null),
this.fetchDom(instance).catch(() => null), this.fetchDom(instance).catch(() => null),
this.fetchManifest(instance).catch(() => null), this.fetchManifest(instance).catch(() => null),
]); ]);
const [favicon, icon, themeColor, name, description] = await Promise.all([ const [favicon, icon, themeColor, name, description] = await Promise.all([
this.fetchFaviconUrl(instance, dom).catch(() => null), this.fetchFaviconUrl(instance, dom).catch(() => null),
this.fetchIconUrl(instance, dom, manifest).catch(() => null), this.fetchIconUrl(instance, dom, manifest).catch(() => null),
@ -86,13 +88,13 @@ export class FetchInstanceMetadataService {
this.getSiteName(info, dom, manifest).catch(() => null), this.getSiteName(info, dom, manifest).catch(() => null),
this.getDescription(info, dom, manifest).catch(() => null), this.getDescription(info, dom, manifest).catch(() => null),
]); ]);
this.logger.succ(`Successfuly fetched metadata of ${instance.host}`); this.logger.succ(`Successfuly fetched metadata of ${instance.host}`);
const updates = { const updates = {
infoUpdatedAt: new Date(), infoUpdatedAt: new Date(),
} as Record<string, any>; } as Record<string, any>;
if (info) { if (info) {
updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?'; updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?';
updates.softwareVersion = info.software?.version; updates.softwareVersion = info.software?.version;
@ -100,15 +102,15 @@ export class FetchInstanceMetadataService {
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
} }
if (name) updates.name = name; if (name) updates.name = name;
if (description) updates.description = description; if (description) updates.description = description;
if (icon || favicon) updates.iconUrl = icon ?? favicon; if (icon || favicon) updates.iconUrl = icon ?? favicon;
if (favicon) updates.faviconUrl = favicon; if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor; if (themeColor) updates.themeColor = themeColor;
await this.federatedInstanceService.update(instance.id, updates); await this.federatedInstanceService.update(instance.id, updates);
this.logger.succ(`Successfuly updated metadata of ${instance.host}`); this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
} catch (e) { } catch (e) {
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
@ -120,7 +122,7 @@ export class FetchInstanceMetadataService {
@bindThis @bindThis
private async fetchNodeinfo(instance: Instance): Promise<NodeInfo> { private async fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
try { try {
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
.catch(err => { .catch(err => {
@ -130,33 +132,33 @@ export class FetchInstanceMetadataService {
throw err.statusCode ?? err.message; throw err.statusCode ?? err.message;
} }
}) as Record<string, unknown>; }) as Record<string, unknown>;
if (wellknown.links == null || !Array.isArray(wellknown.links)) { if (wellknown.links == null || !Array.isArray(wellknown.links)) {
throw new Error('No wellknown links'); throw new Error('No wellknown links');
} }
const links = wellknown.links as any[]; const links = wellknown.links as any[];
const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1'); const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0; const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
if (link == null) { if (link == null) {
throw new Error('No nodeinfo link provided'); throw new Error('No nodeinfo link provided');
} }
const info = await this.httpRequestService.getJson(link.href) const info = await this.httpRequestService.getJson(link.href)
.catch(err => { .catch(err => {
throw err.statusCode ?? err.message; throw err.statusCode ?? err.message;
}); });
this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
return info as NodeInfo; return info as NodeInfo;
} catch (err) { } catch (err) {
this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`); this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`);
throw err; throw err;
} }
} }
@ -164,51 +166,51 @@ export class FetchInstanceMetadataService {
@bindThis @bindThis
private async fetchDom(instance: Instance): Promise<DOMWindow['document']> { private async fetchDom(instance: Instance): Promise<DOMWindow['document']> {
this.logger.info(`Fetching HTML of ${instance.host} ...`); this.logger.info(`Fetching HTML of ${instance.host} ...`);
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
const html = await this.httpRequestService.getHtml(url); const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html); const { window } = new JSDOM(html);
const doc = window.document; const doc = window.document;
return doc; return doc;
} }
@bindThis @bindThis
private async fetchManifest(instance: Instance): Promise<Record<string, unknown> | null> { private async fetchManifest(instance: Instance): Promise<Record<string, unknown> | null> {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
const manifestUrl = url + '/manifest.json'; const manifestUrl = url + '/manifest.json';
const manifest = await this.httpRequestService.getJson(manifestUrl) as Record<string, unknown>; const manifest = await this.httpRequestService.getJson(manifestUrl) as Record<string, unknown>;
return manifest; return manifest;
} }
@bindThis @bindThis
private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> { private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
if (doc) { if (doc) {
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href; const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href;
if (href) { if (href) {
return (new URL(href, url)).href; return (new URL(href, url)).href;
} }
} }
const faviconUrl = url + '/favicon.ico'; const faviconUrl = url + '/favicon.ico';
const favicon = await this.httpRequestService.send(faviconUrl, { const favicon = await this.httpRequestService.send(faviconUrl, {
method: 'HEAD', method: 'HEAD',
}, { throwErrorWhenResponseNotOk: false }); }, { throwErrorWhenResponseNotOk: false });
if (favicon.ok) { if (favicon.ok) {
return faviconUrl; return faviconUrl;
} }
return null; return null;
} }
@ -218,38 +220,38 @@ export class FetchInstanceMetadataService {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
return (new URL(manifest.icons[0].src, url)).href; return (new URL(manifest.icons[0].src, url)).href;
} }
if (doc) { if (doc) {
const url = 'https://' + instance.host; const url = 'https://' + instance.host;
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
const links = Array.from(doc.getElementsByTagName('link')).reverse(); const links = Array.from(doc.getElementsByTagName('link')).reverse();
// https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559 // https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559
const href = const href =
[ [
links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href, links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href,
links.find(link => link.relList.contains('apple-touch-icon'))?.href, links.find(link => link.relList.contains('apple-touch-icon'))?.href,
links.find(link => link.relList.contains('icon'))?.href, links.find(link => link.relList.contains('icon'))?.href,
] ]
.find(href => href); .find(href => href);
if (href) { if (href) {
return (new URL(href, url)).href; return (new URL(href, url)).href;
} }
} }
return null; return null;
} }
@bindThis @bindThis
private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color; const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color;
if (themeColor) { if (themeColor) {
const color = new tinycolor(themeColor); const color = new tinycolor(themeColor);
if (color.isValid()) return color.toHexString(); if (color.isValid()) return color.toHexString();
} }
return null; return null;
} }
@ -262,19 +264,19 @@ export class FetchInstanceMetadataService {
return info.metadata.name; return info.metadata.name;
} }
} }
if (doc) { if (doc) {
const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content');
if (og) { if (og) {
return og; return og;
} }
} }
if (manifest) { if (manifest) {
return manifest.name ?? manifest.short_name; return manifest.name ?? manifest.short_name;
} }
return null; return null;
} }
@ -287,23 +289,23 @@ export class FetchInstanceMetadataService {
return info.metadata.description; return info.metadata.description;
} }
} }
if (doc) { if (doc) {
const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content');
if (meta) { if (meta) {
return meta; return meta;
} }
const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content');
if (og) { if (og) {
return og; return og;
} }
} }
if (manifest) { if (manifest) {
return manifest.name ?? manifest.short_name; return manifest.name ?? manifest.short_name;
} }
return null; return null;
} }
} }

View File

@ -161,20 +161,20 @@ export class FileInfoService {
private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> {
let sensitive = false; let sensitive = false;
let porn = false; let porn = false;
function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] { function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] {
let sensitive = false; let sensitive = false;
let porn = false; let porn = false;
if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true; if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true;
return [sensitive, porn]; return [sensitive, porn];
} }
if ([ if ([
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
@ -253,10 +253,10 @@ export class FileInfoService {
disposeOutDir(); disposeOutDir();
} }
} }
return [sensitive, porn]; return [sensitive, porn];
} }
private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> { private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
const watcher = new FSWatcher({ const watcher = new FSWatcher({
cwd, cwd,
@ -295,7 +295,7 @@ export class FileInfoService {
} }
} }
} }
@bindThis @bindThis
private exists(path: string): Promise<boolean> { private exists(path: string): Promise<boolean> {
return fs.promises.access(path).then(() => true, () => false); return fs.promises.access(path).then(() => true, () => false);

View File

@ -42,21 +42,21 @@ export class HttpRequestService {
errorTtl: 30, // 30secs errorTtl: 30, // 30secs
lookup: false, // nativeのdns.lookupにfallbackしない lookup: false, // nativeのdns.lookupにfallbackしない
}); });
this.http = new http.Agent({ this.http = new http.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup,
} as http.AgentOptions); } as http.AgentOptions);
this.https = new https.Agent({ this.https = new https.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup,
} as https.AgentOptions); } as https.AgentOptions);
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
this.httpAgent = config.proxy this.httpAgent = config.proxy
? new HttpProxyAgent({ ? new HttpProxyAgent({
keepAlive: true, keepAlive: true,

View File

@ -23,7 +23,7 @@ export class IdService {
@bindThis @bindThis
public genId(date?: Date): string { public genId(date?: Date): string {
if (!date || (date > new Date())) date = new Date(); if (!date || (date > new Date())) date = new Date();
switch (this.method) { switch (this.method) {
case 'aid': return genAid(date); case 'aid': return genAid(date);
case 'meid': return genMeid(date); case 'meid': return genMeid(date);

View File

@ -26,12 +26,12 @@ export class InstanceActorService {
public async getInstanceActor(): Promise<LocalUser> { public async getInstanceActor(): Promise<LocalUser> {
const cached = this.cache.get(); const cached = this.cache.get();
if (cached) return cached; if (cached) return cached;
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
host: IsNull(), host: IsNull(),
username: ACTOR_USERNAME, username: ACTOR_USERNAME,
}) as LocalUser | undefined; }) as LocalUser | undefined;
if (user) { if (user) {
this.cache.set(user); this.cache.set(user);
return user; return user;

View File

@ -56,7 +56,7 @@ export class MetaService implements OnApplicationShutdown {
@bindThis @bindThis
public async fetch(noCache = false): Promise<Meta> { public async fetch(noCache = false): Promise<Meta> {
if (!noCache && this.cache) return this.cache; if (!noCache && this.cache) return this.cache;
return await this.db.transaction(async transactionalEntityManager => { return await this.db.transaction(async transactionalEntityManager => {
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
const metas = await transactionalEntityManager.find(Meta, { const metas = await transactionalEntityManager.find(Meta, {
@ -64,9 +64,9 @@ export class MetaService implements OnApplicationShutdown {
id: 'DESC', id: 'DESC',
}, },
}); });
const meta = metas[0]; const meta = metas[0];
if (meta) { if (meta) {
this.cache = meta; this.cache = meta;
return meta; return meta;
@ -81,7 +81,7 @@ export class MetaService implements OnApplicationShutdown {
['id'], ['id'],
) )
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
this.cache = saved; this.cache = saved;
return saved; return saved;
} }

View File

@ -27,29 +27,29 @@ export class MfmService {
public fromHtml(html: string, hashtagNames?: string[]): string { public fromHtml(html: string, hashtagNames?: string[]): string {
// some AP servers like Pixelfed use br tags as well as newlines // some AP servers like Pixelfed use br tags as well as newlines
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n'); html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
const dom = parse5.parseFragment(html); const dom = parse5.parseFragment(html);
let text = ''; let text = '';
for (const n of dom.childNodes) { for (const n of dom.childNodes) {
analyze(n); analyze(n);
} }
return text.trim(); return text.trim();
function getText(node: TreeAdapter.Node): string { function getText(node: TreeAdapter.Node): string {
if (treeAdapter.isTextNode(node)) return node.value; if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return ''; if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n'; if (node.nodeName === 'br') return '\n';
if (node.childNodes) { if (node.childNodes) {
return node.childNodes.map(n => getText(n)).join(''); return node.childNodes.map(n => getText(n)).join('');
} }
return ''; return '';
} }
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
if (childNodes) { if (childNodes) {
for (const n of childNodes) { for (const n of childNodes) {
@ -57,35 +57,35 @@ export class MfmService {
} }
} }
} }
function analyze(node: TreeAdapter.Node) { function analyze(node: TreeAdapter.Node) {
if (treeAdapter.isTextNode(node)) { if (treeAdapter.isTextNode(node)) {
text += node.value; text += node.value;
return; return;
} }
// Skip comment or document type node // Skip comment or document type node
if (!treeAdapter.isElementNode(node)) return; if (!treeAdapter.isElementNode(node)) return;
switch (node.nodeName) { switch (node.nodeName) {
case 'br': { case 'br': {
text += '\n'; text += '\n';
break; break;
} }
case 'a': case 'a':
{ {
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel'); const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href'); const href = node.attrs.find(x => x.name === 'href');
// ハッシュタグ // ハッシュタグ
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
text += txt; text += txt;
// メンション // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
const part = txt.split('@'); const part = txt.split('@');
if (part.length === 2 && href) { if (part.length === 2 && href) {
//#region ホスト名部分が省略されているので復元する //#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`; const acct = `${txt}@${(new URL(href.value)).hostname}`;
@ -116,12 +116,12 @@ export class MfmService {
return `[${txt}](${href.value})`; return `[${txt}](${href.value})`;
} }
}; };
text += generateLink(); text += generateLink();
} }
break; break;
} }
case 'h1': case 'h1':
{ {
text += '【'; text += '【';
@ -129,7 +129,7 @@ export class MfmService {
text += '】\n'; text += '】\n';
break; break;
} }
case 'b': case 'b':
case 'strong': case 'strong':
{ {
@ -138,7 +138,7 @@ export class MfmService {
text += '**'; text += '**';
break; break;
} }
case 'small': case 'small':
{ {
text += '<small>'; text += '<small>';
@ -146,7 +146,7 @@ export class MfmService {
text += '</small>'; text += '</small>';
break; break;
} }
case 's': case 's':
case 'del': case 'del':
{ {
@ -155,7 +155,7 @@ export class MfmService {
text += '~~'; text += '~~';
break; break;
} }
case 'i': case 'i':
case 'em': case 'em':
{ {
@ -164,7 +164,7 @@ export class MfmService {
text += '</i>'; text += '</i>';
break; break;
} }
// block code (<pre><code>) // block code (<pre><code>)
case 'pre': { case 'pre': {
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') { if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
@ -176,7 +176,7 @@ export class MfmService {
} }
break; break;
} }
// inline code (<code>) // inline code (<code>)
case 'code': { case 'code': {
text += '`'; text += '`';
@ -184,7 +184,7 @@ export class MfmService {
text += '`'; text += '`';
break; break;
} }
case 'blockquote': { case 'blockquote': {
const t = getText(node); const t = getText(node);
if (t) { if (t) {
@ -193,7 +193,7 @@ export class MfmService {
} }
break; break;
} }
case 'p': case 'p':
case 'h2': case 'h2':
case 'h3': case 'h3':
@ -205,7 +205,7 @@ export class MfmService {
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
// other block elements // other block elements
case 'div': case 'div':
case 'header': case 'header':
@ -219,7 +219,7 @@ export class MfmService {
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
default: // includes inline elements default: // includes inline elements
{ {
appendChildren(node.childNodes); appendChildren(node.childNodes);
@ -234,48 +234,48 @@ export class MfmService {
if (nodes == null) { if (nodes == null) {
return null; return null;
} }
const { window } = new Window(); const { window } = new Window();
const doc = window.document; const doc = window.document;
function appendChildren(children: mfm.MfmNode[], targetElement: any): void { function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) { if (children) {
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child); for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
} }
} }
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = { const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
bold: (node) => { bold: (node) => {
const el = doc.createElement('b'); const el = doc.createElement('b');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
small: (node) => { small: (node) => {
const el = doc.createElement('small'); const el = doc.createElement('small');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
strike: (node) => { strike: (node) => {
const el = doc.createElement('del'); const el = doc.createElement('del');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
italic: (node) => { italic: (node) => {
const el = doc.createElement('i'); const el = doc.createElement('i');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
fn: (node) => { fn: (node) => {
const el = doc.createElement('i'); const el = doc.createElement('i');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
blockCode: (node) => { blockCode: (node) => {
const pre = doc.createElement('pre'); const pre = doc.createElement('pre');
const inner = doc.createElement('code'); const inner = doc.createElement('code');
@ -283,21 +283,21 @@ export class MfmService {
pre.appendChild(inner); pre.appendChild(inner);
return pre; return pre;
}, },
center: (node) => { center: (node) => {
const el = doc.createElement('div'); const el = doc.createElement('div');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
emojiCode: (node) => { emojiCode: (node) => {
return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
}, },
unicodeEmoji: (node) => { unicodeEmoji: (node) => {
return doc.createTextNode(node.props.emoji); return doc.createTextNode(node.props.emoji);
}, },
hashtag: (node) => { hashtag: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`); a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
@ -305,32 +305,32 @@ export class MfmService {
a.setAttribute('rel', 'tag'); a.setAttribute('rel', 'tag');
return a; return a;
}, },
inlineCode: (node) => { inlineCode: (node) => {
const el = doc.createElement('code'); const el = doc.createElement('code');
el.textContent = node.props.code; el.textContent = node.props.code;
return el; return el;
}, },
mathInline: (node) => { mathInline: (node) => {
const el = doc.createElement('code'); const el = doc.createElement('code');
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
mathBlock: (node) => { mathBlock: (node) => {
const el = doc.createElement('code'); const el = doc.createElement('code');
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
link: (node) => { link: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', node.props.url); a.setAttribute('href', node.props.url);
appendChildren(node.children, a); appendChildren(node.children, a);
return a; return a;
}, },
mention: (node) => { mention: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
@ -340,47 +340,47 @@ export class MfmService {
a.textContent = acct; a.textContent = acct;
return a; return a;
}, },
quote: (node) => { quote: (node) => {
const el = doc.createElement('blockquote'); const el = doc.createElement('blockquote');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
text: (node) => { text: (node) => {
const el = doc.createElement('span'); const el = doc.createElement('span');
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x)); const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
for (const x of intersperse<FIXME | 'br'>('br', nodes)) { for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
el.appendChild(x === 'br' ? doc.createElement('br') : x); el.appendChild(x === 'br' ? doc.createElement('br') : x);
} }
return el; return el;
}, },
url: (node) => { url: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', node.props.url); a.setAttribute('href', node.props.url);
a.textContent = node.props.url; a.textContent = node.props.url;
return a; return a;
}, },
search: (node) => { search: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`); a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
a.textContent = node.props.content; a.textContent = node.props.content;
return a; return a;
}, },
plain: (node) => { plain: (node) => {
const el = doc.createElement('span'); const el = doc.createElement('span');
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
}; };
appendChildren(nodes, doc.body); appendChildren(nodes, doc.body);
return `<p>${doc.body.innerHTML}</p>`; return `<p>${doc.body.innerHTML}</p>`;
} }
} }

View File

@ -672,7 +672,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Register to search database // Register to search database
this.index(note); this.index(note);
} }
@bindThis @bindThis
private isSensitive(note: Option, sensitiveWord: string[]): boolean { private isSensitive(note: Option, sensitiveWord: string[]): boolean {
if (sensitiveWord.length > 0) { if (sensitiveWord.length > 0) {
@ -758,7 +758,7 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis @bindThis
private index(note: Note) { private index(note: Note) {
if (note.text == null && note.cw == null) return; if (note.text == null && note.cw == null) return;
this.searchService.indexNote(note); this.searchService.indexNote(note);
} }

View File

@ -45,7 +45,7 @@ export class NoteDeleteService {
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) {} ) {}
/** /**
* 稿 * 稿
* @param user 稿 * @param user 稿

View File

@ -99,7 +99,7 @@ export class NoteReadService implements OnApplicationShutdown {
}); });
// TODO: ↓まとめてクエリしたい // TODO: ↓まとめてクエリしたい
this.noteUnreadsRepository.countBy({ this.noteUnreadsRepository.countBy({
userId: userId, userId: userId,
isMentioned: true, isMentioned: true,
@ -109,7 +109,7 @@ export class NoteReadService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
} }
}); });
this.noteUnreadsRepository.countBy({ this.noteUnreadsRepository.countBy({
userId: userId, userId: userId,
isSpecified: true, isSpecified: true,

View File

@ -46,7 +46,7 @@ export class NotificationService implements OnApplicationShutdown {
force = false, force = false,
) { ) {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`); const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
const latestNotificationIdsRes = await this.redisClient.xrevrange( const latestNotificationIdsRes = await this.redisClient.xrevrange(
`notificationTimeline:${userId}`, `notificationTimeline:${userId}`,
'+', '+',

View File

@ -39,12 +39,12 @@ export class PollService {
@bindThis @bindThis
public async vote(user: User, note: Note, choice: number) { public async vote(user: User, note: Note, choice: number) {
const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
if (poll == null) throw new Error('poll not found'); if (poll == null) throw new Error('poll not found');
// Check whether is valid choice // Check whether is valid choice
if (poll.choices[choice] == null) throw new Error('invalid choice param'); if (poll.choices[choice] == null) throw new Error('invalid choice param');
// Check blocking // Check blocking
if (note.userId !== user.id) { if (note.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
@ -52,13 +52,13 @@ export class PollService {
throw new Error('blocked'); throw new Error('blocked');
} }
} }
// if already voted // if already voted
const exist = await this.pollVotesRepository.findBy({ const exist = await this.pollVotesRepository.findBy({
noteId: note.id, noteId: note.id,
userId: user.id, userId: user.id,
}); });
if (poll.multiple) { if (poll.multiple) {
if (exist.some(x => x.choice === choice)) { if (exist.some(x => x.choice === choice)) {
throw new Error('already voted'); throw new Error('already voted');
@ -66,7 +66,7 @@ export class PollService {
} else if (exist.length !== 0) { } else if (exist.length !== 0) {
throw new Error('already voted'); throw new Error('already voted');
} }
// Create vote // Create vote
await this.pollVotesRepository.insert({ await this.pollVotesRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
@ -75,11 +75,11 @@ export class PollService {
userId: user.id, userId: user.id,
choice: choice, choice: choice,
}); });
// Increment votes count // Increment votes count
const index = choice + 1; // In SQL, array index is 1 based const index = choice + 1; // In SQL, array index is 1 based
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
this.globalEventService.publishNoteStream(note.id, 'pollVoted', { this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
choice: choice, choice: choice,
userId: user.id, userId: user.id,
@ -90,10 +90,10 @@ export class PollService {
public async deliverQuestionUpdate(noteId: Note['id']) { public async deliverQuestionUpdate(noteId: Note['id']) {
const note = await this.notesRepository.findOneBy({ id: noteId }); const note = await this.notesRepository.findOneBy({ id: noteId });
if (note == null) throw new Error('note not found'); if (note == null) throw new Error('note not found');
const user = await this.usersRepository.findOneBy({ id: note.userId }); const user = await this.usersRepository.findOneBy({ id: note.userId });
if (user == null) throw new Error('note not found'); if (user == null) throw new Error('note not found');
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
this.apDeliverManagerService.deliverToFollowers(user, content); this.apDeliverManagerService.deliverToFollowers(user, content);

View File

@ -31,7 +31,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
...body.note, ...body.note,
// textをgetNoteSummaryしたものに置き換える // textをgetNoteSummaryしたものに置き換える
text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note), text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note),
cw: undefined, cw: undefined,
reply: undefined, reply: undefined,
renote: undefined, renote: undefined,
@ -69,16 +69,16 @@ export class PushNotificationService implements OnApplicationShutdown {
@bindThis @bindThis
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) { public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
push.setVapidDetails(this.config.url, push.setVapidDetails(this.config.url,
meta.swPublicKey, meta.swPublicKey,
meta.swPrivateKey); meta.swPrivateKey);
const subscriptions = await this.subscriptionsCache.fetch(userId); const subscriptions = await this.subscriptionsCache.fetch(userId);
for (const subscription of subscriptions) { for (const subscription of subscriptions) {
if ([ if ([
'readAllNotifications', 'readAllNotifications',
@ -103,7 +103,7 @@ export class PushNotificationService implements OnApplicationShutdown {
//swLogger.info(err.statusCode); //swLogger.info(err.statusCode);
//swLogger.info(err.headers); //swLogger.info(err.headers);
//swLogger.info(err.body); //swLogger.info(err.body);
if (err.statusCode === 410) { if (err.statusCode === 410) {
this.swSubscriptionsRepository.delete({ this.swSubscriptionsRepository.delete({
userId: userId, userId: userId,

View File

@ -60,8 +60,8 @@ export class QueryService {
q.orderBy(`${q.alias}.id`, 'DESC'); q.orderBy(`${q.alias}.id`, 'DESC');
} }
return q; return q;
} }
// ここでいうBlockedは被Blockedの意 // ここでいうBlockedは被Blockedの意
@bindThis @bindThis
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void { public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
@ -109,18 +109,18 @@ export class QueryService {
q.andWhere('note.channelId IS NULL'); q.andWhere('note.channelId IS NULL');
} else { } else {
q.leftJoinAndSelect('note.channel', 'channel'); q.leftJoinAndSelect('note.channel', 'channel');
const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing') const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
.select('channelFollowing.followeeId') .select('channelFollowing.followeeId')
.where('channelFollowing.followerId = :followerId', { followerId: me.id }); .where('channelFollowing.followerId = :followerId', { followerId: me.id });
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => { qb
// チャンネルのノートではない // チャンネルのノートではない
.where('note.channelId IS NULL') .where('note.channelId IS NULL')
// または自分がフォローしているチャンネルのノート // または自分がフォローしているチャンネルのノート
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
})); }));
q.setParameters(channelFollowingQuery.getParameters()); q.setParameters(channelFollowingQuery.getParameters());
} }
} }
@ -130,9 +130,9 @@ export class QueryService {
const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted') const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
.select('muted.noteId') .select('muted.noteId')
.where('muted.userId = :userId', { userId: me.id }); .where('muted.userId = :userId', { userId: me.id });
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
q.setParameters(mutedQuery.getParameters()); q.setParameters(mutedQuery.getParameters());
} }
@ -141,13 +141,13 @@ export class QueryService {
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
.select('threadMuted.threadId') .select('threadMuted.threadId')
.where('threadMuted.userId = :userId', { userId: me.id }); .where('threadMuted.userId = :userId', { userId: me.id });
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => { qb
.where('note.threadId IS NULL') .where('note.threadId IS NULL')
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
})); }));
q.setParameters(mutedQuery.getParameters()); q.setParameters(mutedQuery.getParameters());
} }
@ -156,15 +156,15 @@ export class QueryService {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id }); .where('muting.muterId = :muterId', { muterId: me.id });
if (exclude) { if (exclude) {
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
} }
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances') .select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id }); .where('user_profile.userId = :muterId', { muterId: me.id });
// 投稿の作者をミュートしていない かつ // 投稿の作者をミュートしていない かつ
// 投稿の返信先の作者をミュートしていない かつ // 投稿の返信先の作者をミュートしていない かつ
// 投稿の引用元の作者をミュートしていない // 投稿の引用元の作者をミュートしていない
@ -191,7 +191,7 @@ export class QueryService {
.where('note.renoteUserHost IS NULL') .where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
})); }));
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
q.setParameters(mutingInstanceQuery.getParameters()); q.setParameters(mutingInstanceQuery.getParameters());
} }
@ -201,9 +201,9 @@ export class QueryService {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id }); .where('muting.muterId = :muterId', { muterId: me.id });
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
} }
@ -245,7 +245,7 @@ export class QueryService {
const followingQuery = this.followingsRepository.createQueryBuilder('following') const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId') .select('following.followeeId')
.where('following.followerId = :meId'); .where('following.followerId = :meId');
q.andWhere(new Brackets(qb => { qb q.andWhere(new Brackets(qb => { qb
// 公開投稿である // 公開投稿である
.where(new Brackets(qb => { qb .where(new Brackets(qb => { qb
@ -268,7 +268,7 @@ export class QueryService {
})); }));
})); }));
})); }));
q.setParameters({ meId: me.id }); q.setParameters({ meId: me.id });
} }
} }
@ -278,10 +278,10 @@ export class QueryService {
const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting') const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
.select('renote_muting.muteeId') .select('renote_muting.muteeId')
.where('renote_muting.muterId = :muterId', { muterId: me.id }); .where('renote_muting.muterId = :muterId', { muterId: me.id });
q.andWhere(new Brackets(qb => { q.andWhere(new Brackets(qb => {
qb qb
.where(new Brackets(qb => { .where(new Brackets(qb => {
qb.where('note.renoteId IS NOT NULL'); qb.where('note.renoteId IS NOT NULL');
qb.andWhere('note.text IS NULL'); qb.andWhere('note.text IS NULL');
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`); qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
@ -289,7 +289,7 @@ export class QueryService {
.orWhere('note.renoteId IS NULL') .orWhere('note.renoteId IS NULL')
.orWhere('note.text IS NOT NULL'); .orWhere('note.text IS NOT NULL');
})); }));
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());
} }
} }

View File

@ -39,9 +39,9 @@ export class RelayService {
host: IsNull(), host: IsNull(),
username: ACTOR_USERNAME, username: ACTOR_USERNAME,
}); });
if (user) return user as LocalUser; if (user) return user as LocalUser;
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME); const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME);
return created as LocalUser; return created as LocalUser;
} }
@ -53,12 +53,12 @@ export class RelayService {
inbox, inbox,
status: 'requesting', status: 'requesting',
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
const relayActor = await this.getRelayActor(); const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
const activity = this.apRendererService.addContext(follow); const activity = this.apRendererService.addContext(follow);
this.queueService.deliver(relayActor, activity, relay.inbox, false); this.queueService.deliver(relayActor, activity, relay.inbox, false);
return relay; return relay;
} }
@ -67,17 +67,17 @@ export class RelayService {
const relay = await this.relaysRepository.findOneBy({ const relay = await this.relaysRepository.findOneBy({
inbox, inbox,
}); });
if (relay == null) { if (relay == null) {
throw new Error('relay not found'); throw new Error('relay not found');
} }
const relayActor = await this.getRelayActor(); const relayActor = await this.getRelayActor();
const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
const undo = this.apRendererService.renderUndo(follow, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor);
const activity = this.apRendererService.addContext(undo); const activity = this.apRendererService.addContext(undo);
this.queueService.deliver(relayActor, activity, relay.inbox, false); this.queueService.deliver(relayActor, activity, relay.inbox, false);
await this.relaysRepository.delete(relay.id); await this.relaysRepository.delete(relay.id);
} }
@ -86,13 +86,13 @@ export class RelayService {
const relays = await this.relaysRepository.find(); const relays = await this.relaysRepository.find();
return relays; return relays;
} }
@bindThis @bindThis
public async relayAccepted(id: string): Promise<string> { public async relayAccepted(id: string): Promise<string> {
const result = await this.relaysRepository.update(id, { const result = await this.relaysRepository.update(id, {
status: 'accepted', status: 'accepted',
}); });
return JSON.stringify(result); return JSON.stringify(result);
} }
@ -101,24 +101,24 @@ export class RelayService {
const result = await this.relaysRepository.update(id, { const result = await this.relaysRepository.update(id, {
status: 'rejected', status: 'rejected',
}); });
return JSON.stringify(result); return JSON.stringify(result);
} }
@bindThis @bindThis
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> { public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
if (activity == null) return; if (activity == null) return;
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
status: 'accepted', status: 'accepted',
})); }));
if (relays.length === 0) return; if (relays.length === 0) return;
const copy = deepClone(activity); const copy = deepClone(activity);
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
const signed = await this.apRendererService.attachLdSignature(copy, user); const signed = await this.apRendererService.attachLdSignature(copy, user);
for (const relay of relays) { for (const relay of relays) {
this.queueService.deliver(user, signed, relay.inbox, false); this.queueService.deliver(user, signed, relay.inbox, false);
} }

View File

@ -35,7 +35,7 @@ export class RemoteUserResolveService {
@bindThis @bindThis
public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> { public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
const usernameLower = username.toLowerCase(); const usernameLower = username.toLowerCase();
if (host == null) { if (host == null) {
this.logger.info(`return local user: ${usernameLower}`); this.logger.info(`return local user: ${usernameLower}`);
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
@ -46,9 +46,9 @@ export class RemoteUserResolveService {
} }
}) as LocalUser; }) as LocalUser;
} }
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
if (this.config.host === host) { if (this.config.host === host) {
this.logger.info(`return local user: ${usernameLower}`); this.logger.info(`return local user: ${usernameLower}`);
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
@ -59,39 +59,39 @@ export class RemoteUserResolveService {
} }
}) as LocalUser; }) as LocalUser;
} }
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null; const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
const acctLower = `${usernameLower}@${host}`; const acctLower = `${usernameLower}@${host}`;
if (user == null) { if (user == null) {
const self = await this.resolveSelf(acctLower); const self = await this.resolveSelf(acctLower);
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
return await this.apPersonService.createPerson(self.href); return await this.apPersonService.createPerson(self.href);
} }
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す // ユーザー情報が古い場合は、WebFilgerからやりなおして返す
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
}); });
this.logger.info(`try resync: ${acctLower}`); this.logger.info(`try resync: ${acctLower}`);
const self = await this.resolveSelf(acctLower); const self = await this.resolveSelf(acctLower);
if (user.uri !== self.href) { if (user.uri !== self.href) {
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping. // if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
this.logger.info(`uri missmatch: ${acctLower}`); this.logger.info(`uri missmatch: ${acctLower}`);
this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
// validate uri // validate uri
const uri = new URL(self.href); const uri = new URL(self.href);
if (uri.hostname !== host) { if (uri.hostname !== host) {
throw new Error('Invalid uri'); throw new Error('Invalid uri');
} }
await this.usersRepository.update({ await this.usersRepository.update({
usernameLower, usernameLower,
host: host, host: host,
@ -101,9 +101,9 @@ export class RemoteUserResolveService {
} else { } else {
this.logger.info(`uri is fine: ${acctLower}`); this.logger.info(`uri is fine: ${acctLower}`);
} }
await this.apPersonService.updatePerson(self.href); await this.apPersonService.updatePerson(self.href);
this.logger.info(`return resynced remote user: ${acctLower}`); this.logger.info(`return resynced remote user: ${acctLower}`);
return await this.usersRepository.findOneBy({ uri: self.href }).then(u => { return await this.usersRepository.findOneBy({ uri: self.href }).then(u => {
if (u == null) { if (u == null) {
@ -113,7 +113,7 @@ export class RemoteUserResolveService {
} }
}); });
} }
this.logger.info(`return existing remote user: ${acctLower}`); this.logger.info(`return existing remote user: ${acctLower}`);
return user; return user;
} }

View File

@ -392,7 +392,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> { public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> {
const now = new Date(); const now = new Date();
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId }); const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
if (existing == null) { if (existing == null) {
throw new RoleService.NotAssignedError(); throw new RoleService.NotAssignedError();

View File

@ -50,31 +50,31 @@ export class SignupService {
}) { }) {
const { username, password, passwordHash, host } = opts; const { username, password, passwordHash, host } = opts;
let hash = passwordHash; let hash = passwordHash;
// Validate username // Validate username
if (!this.userEntityService.validateLocalUsername(username)) { if (!this.userEntityService.validateLocalUsername(username)) {
throw new Error('INVALID_USERNAME'); throw new Error('INVALID_USERNAME');
} }
if (password != null && passwordHash == null) { if (password != null && passwordHash == null) {
// Validate password // Validate password
if (!this.userEntityService.validatePassword(password)) { if (!this.userEntityService.validatePassword(password)) {
throw new Error('INVALID_PASSWORD'); throw new Error('INVALID_PASSWORD');
} }
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const salt = await bcrypt.genSalt(8);
hash = await bcrypt.hash(password, salt); hash = await bcrypt.hash(password, salt);
} }
// Generate secret // Generate secret
const secret = generateUserToken(); const secret = generateUserToken();
// Check username duplication // Check username duplication
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
throw new Error('DUPLICATED_USERNAME'); throw new Error('DUPLICATED_USERNAME');
} }
// Check deleted username duplication // Check deleted username duplication
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
throw new Error('USED_USERNAME'); throw new Error('USED_USERNAME');
@ -106,18 +106,18 @@ export class SignupService {
}, (err, publicKey, privateKey) => }, (err, publicKey, privateKey) =>
err ? rej(err) : res([publicKey, privateKey]), err ? rej(err) : res([publicKey, privateKey]),
)); ));
let account!: User; let account!: User;
// Start transaction // Start transaction
await this.db.transaction(async transactionalEntityManager => { await this.db.transaction(async transactionalEntityManager => {
const exist = await transactionalEntityManager.findOneBy(User, { const exist = await transactionalEntityManager.findOneBy(User, {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: IsNull(), host: IsNull(),
}); });
if (exist) throw new Error(' the username is already used'); if (exist) throw new Error(' the username is already used');
account = await transactionalEntityManager.save(new User({ account = await transactionalEntityManager.save(new User({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@ -127,27 +127,27 @@ export class SignupService {
token: secret, token: secret,
isRoot: isTheFirstUser, isRoot: isTheFirstUser,
})); }));
await transactionalEntityManager.save(new UserKeypair({ await transactionalEntityManager.save(new UserKeypair({
publicKey: keyPair[0], publicKey: keyPair[0],
privateKey: keyPair[1], privateKey: keyPair[1],
userId: account.id, userId: account.id,
})); }));
await transactionalEntityManager.save(new UserProfile({ await transactionalEntityManager.save(new UserProfile({
userId: account.id, userId: account.id,
autoAcceptFollowed: true, autoAcceptFollowed: true,
password: hash, password: hash,
})); }));
await transactionalEntityManager.save(new UsedUsername({ await transactionalEntityManager.save(new UsedUsername({
createdAt: new Date(), createdAt: new Date(),
username: username.toLowerCase(), username: username.toLowerCase(),
})); }));
}); });
this.usersChart.update(account, true); this.usersChart.update(account, true);
return { account, secret }; return { account, secret };
} }
} }

View File

@ -69,7 +69,7 @@ function verifyCertificateChain(certificates: string[]) {
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]); const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
if (certStruct == null) throw new Error('certStruct is null'); if (certStruct == null) throw new Error('certStruct is null');
const algorithm = certificate.getSignatureAlgorithmField(); const algorithm = certificate.getSignatureAlgorithmField();
const signatureHex = certificate.getSignatureValueHex(); const signatureHex = certificate.getSignatureValueHex();
@ -143,19 +143,19 @@ export class TwoFactorAuthenticationService {
if (clientData.type !== 'webauthn.get') { if (clientData.type !== 'webauthn.get') {
throw new Error('type is not webauthn.get'); throw new Error('type is not webauthn.get');
} }
if (this.hash(clientData.challenge).toString('hex') !== challenge) { if (this.hash(clientData.challenge).toString('hex') !== challenge) {
throw new Error('challenge mismatch'); throw new Error('challenge mismatch');
} }
if (clientData.origin !== this.config.scheme + '://' + this.config.host) { if (clientData.origin !== this.config.scheme + '://' + this.config.host) {
throw new Error('origin mismatch'); throw new Error('origin mismatch');
} }
const verificationData = Buffer.concat( const verificationData = Buffer.concat(
[authenticatorData, this.hash(clientDataJSON)], [authenticatorData, this.hash(clientDataJSON)],
32 + authenticatorData.length, 32 + authenticatorData.length,
); );
return crypto return crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
@ -168,7 +168,7 @@ export class TwoFactorAuthenticationService {
none: { none: {
verify({ publicKey }: { publicKey: Map<number, Buffer> }) { verify({ publicKey }: { publicKey: Map<number, Buffer> }) {
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@ -176,12 +176,12 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyU2F = Buffer.concat( const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
return { return {
publicKey: publicKeyU2F, publicKey: publicKeyU2F,
valid: true, valid: true,
@ -207,16 +207,16 @@ export class TwoFactorAuthenticationService {
if (attStmt.alg !== -7) { if (attStmt.alg !== -7) {
throw new Error('alg mismatch'); throw new Error('alg mismatch');
} }
const verificationData = Buffer.concat([ const verificationData = Buffer.concat([
authenticatorData, authenticatorData,
clientDataHash, clientDataHash,
]); ]);
const attCert: Buffer = attStmt.x5c[0]; const attCert: Buffer = attStmt.x5c[0];
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@ -224,23 +224,23 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyData = Buffer.concat( const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
if (!attCert.equals(publicKeyData)) { if (!attCert.equals(publicKeyData)) {
throw new Error('public key mismatch'); throw new Error('public key mismatch');
} }
const isValid = crypto const isValid = crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
.verify(PEMString(attCert), attStmt.sig); .verify(PEMString(attCert), attStmt.sig);
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON) // TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
return { return {
valid: isValid, valid: isValid,
publicKey: publicKeyData, publicKey: publicKeyData,
@ -267,43 +267,43 @@ export class TwoFactorAuthenticationService {
const verificationData = this.hash( const verificationData = this.hash(
Buffer.concat([authenticatorData, clientDataHash]), Buffer.concat([authenticatorData, clientDataHash]),
); );
const jwsParts = attStmt.response.toString('utf-8').split('.'); const jwsParts = attStmt.response.toString('utf-8').split('.');
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8')); const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
const response = JSON.parse( const response = JSON.parse(
base64URLDecode(jwsParts[1]).toString('utf-8'), base64URLDecode(jwsParts[1]).toString('utf-8'),
); );
const signature = jwsParts[2]; const signature = jwsParts[2];
if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) { if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
throw new Error('invalid nonce'); throw new Error('invalid nonce');
} }
const certificateChain = header.x5c const certificateChain = header.x5c
.map((key: any) => PEMString(key)) .map((key: any) => PEMString(key))
.concat([GSR2]); .concat([GSR2]);
if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') { if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
throw new Error('invalid common name'); throw new Error('invalid common name');
} }
if (!verifyCertificateChain(certificateChain)) { if (!verifyCertificateChain(certificateChain)) {
throw new Error('Invalid certificate chain!'); throw new Error('Invalid certificate chain!');
} }
const signatureBase = Buffer.from( const signatureBase = Buffer.from(
jwsParts[0] + '.' + jwsParts[1], jwsParts[0] + '.' + jwsParts[1],
'utf-8', 'utf-8',
); );
const valid = crypto const valid = crypto
.createVerify('sha256') .createVerify('sha256')
.update(signatureBase) .update(signatureBase)
.verify(certificateChain[0], base64URLDecode(signature)); .verify(certificateChain[0], base64URLDecode(signature));
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@ -311,7 +311,7 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyData = Buffer.concat( const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
@ -342,17 +342,17 @@ export class TwoFactorAuthenticationService {
authenticatorData, authenticatorData,
clientDataHash, clientDataHash,
]); ]);
if (attStmt.x5c) { if (attStmt.x5c) {
const attCert = attStmt.x5c[0]; const attCert = attStmt.x5c[0];
const validSignature = crypto const validSignature = crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
.verify(PEMString(attCert), attStmt.sig); .verify(PEMString(attCert), attStmt.sig);
const negTwo = publicKey.get(-2); const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@ -360,12 +360,12 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyData = Buffer.concat( const publicKeyData = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
return { return {
valid: validSignature, valid: validSignature,
publicKey: publicKeyData, publicKey: publicKeyData,
@ -375,12 +375,12 @@ export class TwoFactorAuthenticationService {
throw new Error('ECDAA-Verify is not supported'); throw new Error('ECDAA-Verify is not supported');
} else { } else {
if (attStmt.alg !== -7) throw new Error('alg mismatch'); if (attStmt.alg !== -7) throw new Error('alg mismatch');
throw new Error('self attestation is not supported'); throw new Error('self attestation is not supported');
} }
}, },
}, },
'fido-u2f': { 'fido-u2f': {
verify({ verify({
attStmt, attStmt,
@ -401,13 +401,13 @@ export class TwoFactorAuthenticationService {
if (x5c.length !== 1) { if (x5c.length !== 1) {
throw new Error('x5c length does not match expectation'); throw new Error('x5c length does not match expectation');
} }
const attCert = x5c[0]; const attCert = x5c[0];
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve // TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
const negTwo: Buffer = publicKey.get(-2); const negTwo: Buffer = publicKey.get(-2);
if (!negTwo || negTwo.length !== 32) { if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given'); throw new Error('invalid or no -2 key given');
} }
@ -415,12 +415,12 @@ export class TwoFactorAuthenticationService {
if (!negThree || negThree.length !== 32) { if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given'); throw new Error('invalid or no -3 key given');
} }
const publicKeyU2F = Buffer.concat( const publicKeyU2F = Buffer.concat(
[ECC_PRELUDE, negTwo, negThree], [ECC_PRELUDE, negTwo, negThree],
1 + 32 + 32, 1 + 32 + 32,
); );
const verificationData = Buffer.concat([ const verificationData = Buffer.concat([
NULL_BYTE, NULL_BYTE,
rpIdHash, rpIdHash,
@ -428,12 +428,12 @@ export class TwoFactorAuthenticationService {
credentialId, credentialId,
publicKeyU2F, publicKeyU2F,
]); ]);
const validSignature = crypto const validSignature = crypto
.createVerify('SHA256') .createVerify('SHA256')
.update(verificationData) .update(verificationData)
.verify(PEMString(attCert), attStmt.sig); .verify(PEMString(attCert), attStmt.sig);
return { return {
valid: validSignature, valid: validSignature,
publicKey: publicKeyU2F, publicKey: publicKeyU2F,

View File

@ -32,13 +32,13 @@ export class UserSuspendService {
@bindThis @bindThis
public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise<void> { public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信 // 知り得る全SharedInboxにDelete配信
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
const queue: string[] = []; const queue: string[] = [];
const followings = await this.followingsRepository.find({ const followings = await this.followingsRepository.find({
where: [ where: [
{ followerSharedInbox: Not(IsNull()) }, { followerSharedInbox: Not(IsNull()) },
@ -46,13 +46,13 @@ export class UserSuspendService {
], ],
select: ['followerSharedInbox', 'followeeSharedInbox'], select: ['followerSharedInbox', 'followeeSharedInbox'],
}); });
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) { for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox); if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user, content, inbox, true); this.queueService.deliver(user, content, inbox, true);
} }
@ -62,13 +62,13 @@ export class UserSuspendService {
@bindThis @bindThis
public async doPostUnsuspend(user: User): Promise<void> { public async doPostUnsuspend(user: User): Promise<void> {
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにUndo Delete配信 // 知り得る全SharedInboxにUndo Delete配信
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user)); const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
const queue: string[] = []; const queue: string[] = [];
const followings = await this.followingsRepository.find({ const followings = await this.followingsRepository.find({
where: [ where: [
{ followerSharedInbox: Not(IsNull()) }, { followerSharedInbox: Not(IsNull()) },
@ -76,13 +76,13 @@ export class UserSuspendService {
], ],
select: ['followerSharedInbox', 'followeeSharedInbox'], select: ['followerSharedInbox', 'followeeSharedInbox'],
}); });
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
for (const inbox of inboxes) { for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox); if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
} }
for (const inbox of queue) { for (const inbox of queue) {
this.queueService.deliver(user as any, content, inbox, true); this.queueService.deliver(user as any, content, inbox, true);
} }

View File

@ -21,7 +21,7 @@ export class VideoProcessingService {
@bindThis @bindThis
public async generateVideoThumbnail(source: string): Promise<IImage> { public async generateVideoThumbnail(source: string): Promise<IImage> {
const [dir, cleanup] = await createTempDir(); const [dir, cleanup] = await createTempDir();
try { try {
await new Promise((res, rej) => { await new Promise((res, rej) => {
FFmpeg({ FFmpeg({

View File

@ -31,7 +31,7 @@ export class WebhookService implements OnApplicationShutdown {
}); });
this.webhooksFetched = true; this.webhooksFetched = true;
} }
return this.webhooks; return this.webhooks;
} }

View File

@ -27,14 +27,14 @@ export class ApAudienceService {
public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> { public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
const toGroups = this.groupingAudience(getApIds(to), actor); const toGroups = this.groupingAudience(getApIds(to), actor);
const ccGroups = this.groupingAudience(getApIds(cc), actor); const ccGroups = this.groupingAudience(getApIds(cc), actor);
const others = unique(concat([toGroups.other, ccGroups.other])); const others = unique(concat([toGroups.other, ccGroups.other]));
const limit = promiseLimit<User | null>(2); const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
)).filter((x): x is User => x != null); )).filter((x): x is User => x != null);
if (toGroups.public.length > 0) { if (toGroups.public.length > 0) {
return { return {
visibility: 'public', visibility: 'public',
@ -42,7 +42,7 @@ export class ApAudienceService {
visibleUsers: [], visibleUsers: [],
}; };
} }
if (ccGroups.public.length > 0) { if (ccGroups.public.length > 0) {
return { return {
visibility: 'home', visibility: 'home',
@ -50,7 +50,7 @@ export class ApAudienceService {
visibleUsers: [], visibleUsers: [],
}; };
} }
if (toGroups.followers.length > 0) { if (toGroups.followers.length > 0) {
return { return {
visibility: 'followers', visibility: 'followers',
@ -58,14 +58,14 @@ export class ApAudienceService {
visibleUsers: [], visibleUsers: [],
}; };
} }
return { return {
visibility: 'specified', visibility: 'specified',
mentionedUsers, mentionedUsers,
visibleUsers: mentionedUsers, visibleUsers: mentionedUsers,
}; };
} }
@bindThis @bindThis
private groupingAudience(ids: string[], actor: RemoteUser) { private groupingAudience(ids: string[], actor: RemoteUser) {
const groups = { const groups = {
@ -73,7 +73,7 @@ export class ApAudienceService {
followers: [] as string[], followers: [] as string[],
other: [] as string[], other: [] as string[],
}; };
for (const id of ids) { for (const id of ids) {
if (this.isPublic(id)) { if (this.isPublic(id)) {
groups.public.push(id); groups.public.push(id);
@ -83,12 +83,12 @@ export class ApAudienceService {
groups.other.push(id); groups.other.push(id);
} }
} }
groups.other = unique(groups.other); groups.other = unique(groups.other);
return groups; return groups;
} }
@bindThis @bindThis
private isPublic(id: string) { private isPublic(id: string) {
return [ return [
@ -97,7 +97,7 @@ export class ApAudienceService {
'Public', 'Public',
].includes(id); ].includes(id);
} }
@bindThis @bindThis
private isFollowers(id: string, actor: RemoteUser) { private isFollowers(id: string, actor: RemoteUser) {
return ( return (

View File

@ -121,7 +121,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
const key = await this.userPublickeysRepository.findOneBy({ const key = await this.userPublickeysRepository.findOneBy({
keyId, keyId,
}); });
if (key == null) return null; if (key == null) return null;
return key; return key;
@ -147,7 +147,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
if (user == null) return null; if (user == null) return null;
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null); const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
return { return {
user, user,

View File

@ -90,7 +90,7 @@ export class ApDeliverManagerService {
this.followingsRepository, this.followingsRepository,
this.queueService, this.queueService,
actor, actor,
activity, activity,
); );
} }

View File

@ -21,7 +21,7 @@ export class ApMfmService {
@bindThis @bindThis
public htmlToMfm(html: string, tag?: IObject | IObject[]) { public htmlToMfm(html: string, tag?: IObject | IObject[]) {
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
return this.mfmService.fromHtml(html, hashtagNames); return this.mfmService.fromHtml(html, hashtagNames);
} }
@ -29,5 +29,5 @@ export class ApMfmService {
public getNoteHtml(note: Note) { public getNoteHtml(note: Note) {
if (!note.text) return ''; if (!note.text) return '';
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
} }
} }

View File

@ -32,7 +32,7 @@ export class ApImageService {
) { ) {
this.logger = this.apLoggerService.logger; this.logger = this.apLoggerService.logger;
} }
/** /**
* Imageを作成します * Imageを作成します
*/ */

View File

@ -29,10 +29,10 @@ export class ApMentionService {
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
)).filter((x): x is User => x != null); )).filter((x): x is User => x != null);
return mentionedUsers; return mentionedUsers;
} }
@bindThis @bindThis
public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] { public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
if (tags == null) return []; if (tags == null) return [];

View File

@ -55,7 +55,7 @@ export class ApNoteService {
// 循環参照のため / for circular dependency // 循環参照のため / for circular dependency
@Inject(forwardRef(() => ApPersonService)) @Inject(forwardRef(() => ApPersonService))
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
private utilityService: UtilityService, private utilityService: UtilityService,
private apAudienceService: ApAudienceService, private apAudienceService: ApAudienceService,
private apMentionService: ApMentionService, private apMentionService: ApMentionService,
@ -74,15 +74,15 @@ export class ApNoteService {
@bindThis @bindThis
public validateNote(object: IObject, uri: string) { public validateNote(object: IObject, uri: string) {
const expectHost = this.utilityService.extractDbHost(uri); const expectHost = this.utilityService.extractDbHost(uri);
if (object == null) { if (object == null) {
return new Error('invalid Note: object is null'); return new Error('invalid Note: object is null');
} }
if (!validPost.includes(getApType(object))) { if (!validPost.includes(getApType(object))) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`); return new Error(`invalid Note: invalid object type ${getApType(object)}`);
} }
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
} }
@ -91,10 +91,10 @@ export class ApNoteService {
if (object.attributedTo && actualHost !== expectHost) { if (object.attributedTo && actualHost !== expectHost) {
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
} }
return null; return null;
} }
/** /**
* Noteをフェッチします * Noteをフェッチします
* *
@ -104,16 +104,16 @@ export class ApNoteService {
public async fetchNote(object: string | IObject): Promise<Note | null> { public async fetchNote(object: string | IObject): Promise<Note | null> {
return await this.apDbResolverService.getNoteFromApId(object); return await this.apDbResolverService.getNoteFromApId(object);
} }
/** /**
* Noteを作成します * Noteを作成します
*/ */
@bindThis @bindThis
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> { public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
if (resolver == null) resolver = this.apResolverService.createResolver(); if (resolver == null) resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(value); const object = await resolver.resolve(value);
const entryUri = getApId(value); const entryUri = getApId(value);
const err = this.validateNote(object, entryUri); const err = this.validateNote(object, entryUri);
if (err) { if (err) {
@ -126,9 +126,9 @@ export class ApNoteService {
}); });
throw new Error('invalid note'); throw new Error('invalid note');
} }
const note = object as IPost; const note = object as IPost;
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
if (note.id && !checkHttps(note.id)) { if (note.id && !checkHttps(note.id)) {
@ -140,21 +140,21 @@ export class ApNoteService {
if (url && !checkHttps(url)) { if (url && !checkHttps(url)) {
throw new Error('unexpected shcema of note url: ' + url); throw new Error('unexpected shcema of note url: ' + url);
} }
this.logger.info(`Creating the Note: ${note.id}`); this.logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ // 投稿者をフェッチ
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser; const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser;
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
throw new Error('actor has been suspended'); throw new Error('actor has been suspended');
} }
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
let visibility = noteAudience.visibility; let visibility = noteAudience.visibility;
const visibleUsers = noteAudience.visibleUsers; const visibleUsers = noteAudience.visibleUsers;
// Audience (to, cc) が指定されてなかった場合 // Audience (to, cc) が指定されてなかった場合
if (visibility === 'specified' && visibleUsers.length === 0) { if (visibility === 'specified' && visibleUsers.length === 0) {
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
@ -162,23 +162,23 @@ export class ApNoteService {
visibility = 'public'; visibility = 'public';
} }
} }
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = await extractApHashtags(note.tag); const apHashtags = await extractApHashtags(note.tag);
// 添付ファイル // 添付ファイル
// TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない // TODO: attachmentは必ずしも配列ではない
// Noteがsensitiveなら添付もsensitiveにする // Noteがsensitiveなら添付もsensitiveにする
const limit = promiseLimit(2); const limit = promiseLimit(2);
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
const files = note.attachment const files = note.attachment
.map(attach => attach.sensitive = note.sensitive) .map(attach => attach.sensitive = note.sensitive)
? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise<DriveFile>))) ? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise<DriveFile>)))
.filter(image => image != null) .filter(image => image != null)
: []; : [];
// リプライ // リプライ
const reply: Note | null = note.inReplyTo const reply: Note | null = note.inReplyTo
? await this.resolveNote(note.inReplyTo, resolver).then(x => { ? await this.resolveNote(note.inReplyTo, resolver).then(x => {
@ -193,10 +193,10 @@ export class ApNoteService {
throw err; throw err;
}) })
: null; : null;
// 引用 // 引用
let quote: Note | undefined | null; let quote: Note | undefined | null;
if (note._misskey_quote || note.quoteUrl) { if (note._misskey_quote || note.quoteUrl) {
const tryResolveNote = async (uri: string): Promise<{ const tryResolveNote = async (uri: string): Promise<{
status: 'ok'; status: 'ok';
@ -223,10 +223,10 @@ export class ApNoteService {
}; };
} }
}; };
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
const results = await Promise.all(uris.map(uri => tryResolveNote(uri))); const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
if (!quote) { if (!quote) {
if (results.some(x => x.status === 'temperror')) { if (results.some(x => x.status === 'temperror')) {
@ -234,9 +234,9 @@ export class ApNoteService {
} }
} }
} }
const cw = note.summary === '' ? null : note.summary; const cw = note.summary === '' ? null : note.summary;
// テキストのパース // テキストのパース
let text: string | null = null; let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
@ -246,38 +246,38 @@ export class ApNoteService {
} else if (typeof note.content === 'string') { } else if (typeof note.content === 'string') {
text = this.apMfmService.htmlToMfm(note.content, note.tag); text = this.apMfmService.htmlToMfm(note.content, note.tag);
} }
// vote // vote
if (reply && reply.hasPoll) { if (reply && reply.hasPoll) {
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
const tryCreateVote = async (name: string, index: number): Promise<null> => { const tryCreateVote = async (name: string, index: number): Promise<null> => {
if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
} else if (index >= 0) { } else if (index >= 0) {
this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
await this.pollService.vote(actor, reply, index); await this.pollService.vote(actor, reply, index);
// リモートフォロワーにUpdate配信 // リモートフォロワーにUpdate配信
this.pollService.deliverQuestionUpdate(reply.id); this.pollService.deliverQuestionUpdate(reply.id);
} }
return null; return null;
}; };
if (note.name) { if (note.name) {
return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
} }
} }
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
return [] as Emoji[]; return [] as Emoji[];
}); });
const apEmojis = emojis.map(emoji => emoji.name); const apEmojis = emojis.map(emoji => emoji.name);
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
return await this.noteCreateService.create(actor, { return await this.noteCreateService.create(actor, {
createdAt: note.published ? new Date(note.published) : null, createdAt: note.published ? new Date(note.published) : null,
files, files,
@ -297,7 +297,7 @@ export class ApNoteService {
url: url, url: url,
}, silent); }, silent);
} }
/** /**
* Noteを解決します * Noteを解決します
* *
@ -308,26 +308,26 @@ export class ApNoteService {
public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> { public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('missing uri'); if (uri == null) throw new Error('missing uri');
// ブロックしてたら中断 // ブロックしてたら中断
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451); if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
const unlock = await this.appLockService.getApLock(uri); const unlock = await this.appLockService.getApLock(uri);
try { try {
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await this.fetchNote(uri); const exist = await this.fetchNote(uri);
if (exist) { if (exist) {
return exist; return exist;
} }
//#endregion //#endregion
if (uri.startsWith(this.config.url)) { if (uri.startsWith(this.config.url)) {
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
} }
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
@ -336,26 +336,26 @@ export class ApNoteService {
unlock(); unlock();
} }
} }
@bindThis @bindThis
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> { public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
if (!tags) return []; if (!tags) return [];
const eomjiTags = toArray(tags).filter(isEmoji); const eomjiTags = toArray(tags).filter(isEmoji);
const existingEmojis = await this.emojisRepository.findBy({ const existingEmojis = await this.emojisRepository.findBy({
host, host,
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))), name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
}); });
return await Promise.all(eomjiTags.map(async tag => { return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name!.replaceAll(':', ''); const name = tag.name!.replaceAll(':', '');
tag.icon = toSingle(tag.icon); tag.icon = toSingle(tag.icon);
const exists = existingEmojis.find(x => x.name === name); const exists = existingEmojis.find(x => x.name === name);
if (exists) { if (exists) {
if ((tag.updated != null && exists.updatedAt == null) if ((tag.updated != null && exists.updatedAt == null)
|| (tag.id != null && exists.uri == null) || (tag.id != null && exists.uri == null)
@ -371,18 +371,18 @@ export class ApNoteService {
publicUrl: tag.icon!.url, publicUrl: tag.icon!.url,
updatedAt: new Date(), updatedAt: new Date(),
}); });
return await this.emojisRepository.findOneBy({ return await this.emojisRepository.findOneBy({
host, host,
name, name,
}) as Emoji; }) as Emoji;
} }
return exists; return exists;
} }
this.logger.info(`register emoji host=${host}, name=${name}`); this.logger.info(`register emoji host=${host}, name=${name}`);
return await this.emojisRepository.insert({ return await this.emojisRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
host, host,

View File

@ -415,7 +415,7 @@ export class ApPersonService implements OnModuleInit {
* Personの情報を更新します * Personの情報を更新します
* Misskeyに対象のPersonが登録されていなければ無視します * Misskeyに対象のPersonが登録されていなければ無視します
* *
* *
* @param uri URI of Person * @param uri URI of Person
* @param resolver Resolver * @param resolver Resolver
* @param hint Hint of Person object (Personの場合Remote resolveをせずに更新に利用します) * @param hint Hint of Person object (Personの場合Remote resolveをせずに更新に利用します)
@ -688,7 +688,7 @@ export class ApPersonService implements OnModuleInit {
// (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする) // (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする)
dst = await this.resolvePerson(src.movedToUri); dst = await this.resolvePerson(src.movedToUri);
} }
if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; //
if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; //
if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri'; if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';

View File

@ -47,7 +47,7 @@ export class DriveFileEntityService {
private videoProcessingService: VideoProcessingService, private videoProcessingService: VideoProcessingService,
) { ) {
} }
@bindThis @bindThis
public validateFileName(name: string): boolean { public validateFileName(name: string): boolean {
return ( return (

View File

@ -24,7 +24,7 @@ export class NoteEntityService implements OnModuleInit {
private driveFileEntityService: DriveFileEntityService; private driveFileEntityService: DriveFileEntityService;
private customEmojiService: CustomEmojiService; private customEmojiService: CustomEmojiService;
private reactionService: ReactionService; private reactionService: ReactionService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@ -68,7 +68,7 @@ export class NoteEntityService implements OnModuleInit {
this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.reactionService = this.moduleRef.get('ReactionService'); this.reactionService = this.moduleRef.get('ReactionService');
} }
@bindThis @bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
@ -457,12 +457,12 @@ export class NoteEntityService implements OnModuleInit {
const query = this.notesRepository.createQueryBuilder('note') const query = this.notesRepository.createQueryBuilder('note')
.where('note.userId = :userId', { userId }) .where('note.userId = :userId', { userId })
.andWhere('note.renoteId = :renoteId', { renoteId }); .andWhere('note.renoteId = :renoteId', { renoteId });
// 指定した投稿を除く // 指定した投稿を除く
if (excludeNoteId) { if (excludeNoteId) {
query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
} }
return await query.getCount(); return await query.getCount();
} }
} }

View File

@ -17,7 +17,7 @@ export class NoteReactionEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService; private noteEntityService: NoteEntityService;
private reactionService: ReactionService; private reactionService: ReactionService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,

View File

@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
meId: User['id'], meId: User['id'],
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
options: { options: {
}, },
hint?: { hint?: {
packedNotes: Map<Note['id'], Packed<'Note'>>; packedNotes: Map<Note['id'], Packed<'Note'>>;

View File

@ -113,7 +113,7 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.pagesRepository) @Inject(DI.pagesRepository)
private pagesRepository: PagesRepository, private pagesRepository: PagesRepository,
@Inject(DI.userMemosRepository) @Inject(DI.userMemosRepository)
private userMemosRepository: UserMemoRepository, private userMemosRepository: UserMemoRepository,

View File

@ -81,7 +81,7 @@ export class QueueStatsService implements OnApplicationShutdown {
this.intervalId = setInterval(tick, interval); this.intervalId = setInterval(tick, interval);
} }
@bindThis @bindThis
public dispose(): void { public dispose(): void {
clearInterval(this.intervalId); clearInterval(this.intervalId);

View File

@ -131,7 +131,7 @@ type NullOrUndefined<p extends Schema, T> =
| T; | T;
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
// Get intersection from union // Get intersection from union
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type PartialIntersection<T> = Partial<UnionToIntersection<T>>; type PartialIntersection<T> = Partial<UnionToIntersection<T>>;

View File

@ -2,7 +2,7 @@
* 1. * 1.
* 2. undefinedの時はクエリを付けない * 2. undefinedの時はクエリを付けない
* new URLSearchParams(obj) * new URLSearchParams(obj)
*/ */
export function query(obj: Record<string, unknown>): string { export function query(obj: Record<string, unknown>): string {
const params = Object.entries(obj) const params = Object.entries(obj)
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)

View File

@ -207,7 +207,7 @@ export class UserProfile {
public mutedInstances: string[]; public mutedInstances: string[];
@Column('enum', { @Column('enum', {
enum: [ enum: [
...notificationTypes, ...notificationTypes,
// マイグレーションで削除が困難なので古いenumは残しておく // マイグレーションで削除が困難なので古いenumは残しておく
...obsoleteNotificationTypes, ...obsoleteNotificationTypes,

View File

@ -283,7 +283,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
}); });
const relationshipLogger = this.logger.createSubLogger('relationship'); const relationshipLogger = this.logger.createSubLogger('relationship');
this.relationshipQueueWorker this.relationshipQueueWorker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))

View File

@ -30,7 +30,7 @@ export class ExportAntennasProcessorService {
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListJoiningsRepository: UserListJoiningsRepository,
private driveService: DriveService, private driveService: DriveService,
private utilityService: UtilityService, private utilityService: UtilityService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,

View File

@ -17,11 +17,11 @@ const validate = new Ajv().compile({
properties: { properties: {
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, src: { type: 'string', enum: ['home', 'all', 'users', 'list'] },
userListAccts: { userListAccts: {
type: 'array', type: 'array',
items: { items: {
type: 'string', type: 'string',
}, },
nullable: true, nullable: true,
}, },
keywords: { type: 'array', items: { keywords: { type: 'array', items: {

View File

@ -113,7 +113,7 @@ export class ImportCustomEmojisProcessorService {
} }
cleanup(); cleanup();
this.logger.succ('Imported'); this.logger.succ('Imported');
}); });
unzipStream.pipe(extractor); unzipStream.pipe(extractor);

View File

@ -31,7 +31,7 @@ export class WebhookDeliverProcessorService {
public async process(job: Bull.Job<WebhookDeliverJobData>): Promise<string> { public async process(job: Bull.Job<WebhookDeliverJobData>): Promise<string> {
try { try {
this.logger.debug(`delivering ${job.data.webhookId}`); this.logger.debug(`delivering ${job.data.webhookId}`);
const res = await this.httpRequestService.send(job.data.to, { const res = await this.httpRequestService.send(job.data.to, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -50,25 +50,25 @@ export class WebhookDeliverProcessorService {
body: job.data.content, body: job.data.content,
}), }),
}); });
this.webhooksRepository.update({ id: job.data.webhookId }, { this.webhooksRepository.update({ id: job.data.webhookId }, {
latestSentAt: new Date(), latestSentAt: new Date(),
latestStatus: res.status, latestStatus: res.status,
}); });
return 'Success'; return 'Success';
} catch (res) { } catch (res) {
this.webhooksRepository.update({ id: job.data.webhookId }, { this.webhooksRepository.update({ id: job.data.webhookId }, {
latestSentAt: new Date(), latestSentAt: new Date(),
latestStatus: res instanceof StatusError ? res.statusCode : 1, latestStatus: res instanceof StatusError ? res.statusCode : 1,
}); });
if (res instanceof StatusError) { if (res instanceof StatusError) {
// 4xx // 4xx
if (res.isClientError) { if (res.isClientError) {
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
} }
// 5xx etc. // 5xx etc.
throw new Error(`${res.statusCode} ${res.statusMessage}`); throw new Error(`${res.statusCode} ${res.statusMessage}`);
} else { } else {

View File

@ -40,15 +40,15 @@ export class AuthenticateService implements OnApplicationShutdown {
if (token == null) { if (token == null) {
return [null, null]; return [null, null];
} }
if (isNativeToken(token)) { if (isNativeToken(token)) {
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
() => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>); () => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>);
if (user == null) { if (user == null) {
throw new AuthenticationError('user not found'); throw new AuthenticationError('user not found');
} }
return [user, null]; return [user, null];
} else { } else {
const accessToken = await this.accessTokensRepository.findOne({ const accessToken = await this.accessTokensRepository.findOne({
@ -58,24 +58,24 @@ export class AuthenticateService implements OnApplicationShutdown {
token: token, // miauth token: token, // miauth
}], }],
}); });
if (accessToken == null) { if (accessToken == null) {
throw new AuthenticationError('invalid signature'); throw new AuthenticationError('invalid signature');
} }
this.accessTokensRepository.update(accessToken.id, { this.accessTokensRepository.update(accessToken.id, {
lastUsedAt: new Date(), lastUsedAt: new Date(),
}); });
const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId, const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId,
() => this.usersRepository.findOneBy({ () => this.usersRepository.findOneBy({
id: accessToken.userId, id: accessToken.userId,
}) as Promise<LocalUser>); }) as Promise<LocalUser>);
if (accessToken.appId) { if (accessToken.appId) {
const app = await this.appCache.fetch(accessToken.appId, const app = await this.appCache.fetch(accessToken.appId,
() => this.appsRepository.findOneByOrFail({ id: accessToken.appId! })); () => this.appsRepository.findOneByOrFail({ id: accessToken.appId! }));
return [user, { return [user, {
id: accessToken.id, id: accessToken.id,
permission: app.permission, permission: app.permission,

View File

@ -38,14 +38,14 @@ export class RateLimiterService {
max: 1, max: 1,
db: this.redisClient, db: this.redisClient,
}); });
minIntervalLimiter.get((err, info) => { minIntervalLimiter.get((err, info) => {
if (err) { if (err) {
return reject('ERR'); return reject('ERR');
} }
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('BRIEF_REQUEST_INTERVAL'); reject('BRIEF_REQUEST_INTERVAL');
} else { } else {
@ -57,7 +57,7 @@ export class RateLimiterService {
} }
}); });
}; };
// Long term limit // Long term limit
const max = (): void => { const max = (): void => {
const limiter = new Limiter({ const limiter = new Limiter({
@ -66,14 +66,14 @@ export class RateLimiterService {
max: limitation.max! / factor, max: limitation.max! / factor,
db: this.redisClient, db: this.redisClient,
}); });
limiter.get((err, info) => { limiter.get((err, info) => {
if (err) { if (err) {
return reject('ERR'); return reject('ERR');
} }
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('RATE_LIMIT_EXCEEDED'); reject('RATE_LIMIT_EXCEEDED');
} else { } else {
@ -81,13 +81,13 @@ export class RateLimiterService {
} }
}); });
}; };
const hasShortTermLimit = typeof limitation.minInterval === 'number'; const hasShortTermLimit = typeof limitation.minInterval === 'number';
const hasLongTermLimit = const hasLongTermLimit =
typeof limitation.duration === 'number' && typeof limitation.duration === 'number' &&
typeof limitation.max === 'number'; typeof limitation.max === 'number';
if (hasShortTermLimit) { if (hasShortTermLimit) {
min(); min();
} else if (hasLongTermLimit) { } else if (hasLongTermLimit) {

View File

@ -36,7 +36,7 @@ export class SigninService {
headers: request.headers as any, headers: request.headers as any,
success: true, success: true,
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
// Publish signin event // Publish signin event
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
}); });

View File

@ -34,23 +34,23 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => { this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
let cleanup: undefined | (() => void) = undefined; let cleanup: undefined | (() => void) = undefined;
if (meta.requireFile) { if (meta.requireFile) {
cleanup = () => { cleanup = () => {
if (file) fs.unlink(file.path, () => {}); if (file) fs.unlink(file.path, () => {});
}; };
if (file == null) return Promise.reject(new ApiError({ if (file == null) return Promise.reject(new ApiError({
message: 'File required.', message: 'File required.',
code: 'FILE_REQUIRED', code: 'FILE_REQUIRED',
id: '4267801e-70d1-416a-b011-4ee502885d8b', id: '4267801e-70d1-416a-b011-4ee502885d8b',
})); }));
} }
const valid = validate(params); const valid = validate(params);
if (!valid) { if (!valid) {
if (file) cleanup!(); if (file) cleanup!();
const errors = validate.errors!; const errors = validate.errors!;
const err = new ApiError({ const err = new ApiError({
message: 'Invalid param.', message: 'Invalid param.',
@ -62,7 +62,7 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
}); });
return Promise.reject(err); return Promise.reject(err);
} }
return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers); return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
}; };
} }

View File

@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
title: ps.title, title: ps.title,
text: ps.text, text: ps.text,
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
imageUrl: ps.imageUrl || null, imageUrl: ps.imageUrl || null,
}); });
}); });
} }

View File

@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g); const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g);
if (queryarry) { if (queryarry) {
emojis = emojis.filter(emoji => emojis = emojis.filter(emoji =>
queryarry.includes(`:${emoji.name}:`) queryarry.includes(`:${emoji.name}:`)
); );
} else { } else {

View File

@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
} }
await this.customEmojiService.update(ps.id, { await this.customEmojiService.update(ps.id, {
driveFile, driveFile,
name: ps.name, name: ps.name,

View File

@ -36,7 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await queue.promote(); await queue.promote();
} }
break; break;
case 'inbox': case 'inbox':
delayedQueues = await this.queueService.inboxQueue.getDelayed(); delayedQueues = await this.queueService.inboxQueue.getDelayed();
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {

View File

@ -136,7 +136,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (Array.isArray(ps.sensitiveWords)) { if (Array.isArray(ps.sensitiveWords)) {
set.sensitiveWords = ps.sensitiveWords.filter(Boolean); set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
} }
if (ps.themeColor !== undefined) { if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor; set.themeColor = ps.themeColor;
} }

View File

@ -77,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = []; let noteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) { if (!ps.sinceId && !ps.sinceDate) {
noteIdsRes = await this.redisClient.xrevrange( noteIdsRes = await this.redisClient.xrevrange(
`channelTimeline:${channel.id}`, `channelTimeline:${channel.id}`,

View File

@ -40,7 +40,7 @@ export const meta = {
code: 'NO_SUCH_FOLDER', code: 'NO_SUCH_FOLDER',
id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73', id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73',
}, },
restrictedByRole: { restrictedByRole: {
message: 'This feature is restricted by your role.', message: 'This feature is restricted by your role.',
code: 'RESTRICTED_BY_ROLE', code: 'RESTRICTED_BY_ROLE',

View File

@ -36,7 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,

View File

@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,

View File

@ -39,7 +39,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {

View File

@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}); });
userProfile.loggedInDates = [...userProfile.loggedInDates, today]; userProfile.loggedInDates = [...userProfile.loggedInDates, today];
} }
return await this.userEntityService.pack<true, true>(userProfile.user!, userProfile.user!, { return await this.userEntityService.pack<true, true>(userProfile.user!, userProfile.user!, {
detail: true, detail: true,
includeSecrets: isSecure, includeSecrets: isSecure,

View File

@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (key.userId !== me.id) { if (key.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied); throw new ApiError(meta.errors.accessDenied);
} }
await this.userSecurityKeysRepository.update(key.id, { await this.userSecurityKeysRepository.update(key.id, {
name: ps.name, name: ps.name,
}); });

View File

@ -250,7 +250,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,

View File

@ -53,34 +53,34 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser');
if (ps.local) { if (ps.local) {
query.andWhere('note.userHost IS NULL'); query.andWhere('note.userHost IS NULL');
} }
if (ps.reply !== undefined) { if (ps.reply !== undefined) {
query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL'); query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL');
} }
if (ps.renote !== undefined) { if (ps.renote !== undefined) {
query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL');
} }
if (ps.withFiles !== undefined) { if (ps.withFiles !== undefined) {
query.andWhere(ps.withFiles ? 'note.fileIds != \'{}\'' : 'note.fileIds = \'{}\''); query.andWhere(ps.withFiles ? 'note.fileIds != \'{}\'' : 'note.fileIds = \'{}\'');
} }
if (ps.poll !== undefined) { if (ps.poll !== undefined) {
query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
} }
// TODO // TODO
//if (bot != undefined) { //if (bot != undefined) {
// query.isBot = bot; // query.isBot = bot;
//} //}
const notes = await query.take(ps.limit).getMany(); const notes = await query.take(ps.limit).getMany();
return await this.noteEntityService.packMany(notes); return await this.noteEntityService.packMany(notes);
}); });
} }

View File

@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private searchService: SearchService, private searchService: SearchService,
private roleService: RoleService, private roleService: RoleService,
@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (!policies.canSearchNotes) { if (!policies.canSearchNotes) {
throw new ApiError(meta.errors.unavailable); throw new ApiError(meta.errors.unavailable);
} }
const notes = await this.searchService.searchNote(ps.query, me, { const notes = await this.searchService.searchNote(ps.query, me, {
userId: ps.userId, userId: ps.userId,
channelId: ps.channelId, channelId: ps.channelId,

View File

@ -44,7 +44,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,

View File

@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (role == null) { if (role == null) {
throw new ApiError(meta.errors.noSuchRole); throw new ApiError(meta.errors.noSuchRole);
} }
if (!role.isExplorable) { if (!role.isExplorable) {
return []; return [];
} }
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1

View File

@ -95,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists); throw new ApiError(meta.errors.tooManyUserLists);
} }
const userList = await this.userListsRepository.insert({ const userList = await this.userListsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userListId: userList.id, userListId: userList.id,
userId: currentUser.id, userId: currentUser.id,
}); });
if (exist) { if (exist) {
throw new ApiError(meta.errors.alreadyAdded); throw new ApiError(meta.errors.alreadyAdded);
} }

View File

@ -41,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private idService: IdService, private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const userList = await this.userListsRepository.findOneBy({ const userList = await this.userListsRepository.findOneBy({
id: ps.listId, id: ps.listId,
isPublic: true, isPublic: true,
}); });

View File

@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.getMany(); .getMany();
} else { } else {
const nameQuery = this.usersRepository.createQueryBuilder('user') const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => { .where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
// Also search username if it qualifies as username // Also search username if it qualifies as username

View File

@ -52,7 +52,7 @@ export class ChannelsService {
case 'serverStats': return this.serverStatsChannelService; case 'serverStats': return this.serverStatsChannelService;
case 'queueStats': return this.queueStatsChannelService; case 'queueStats': return this.queueStatsChannelService;
case 'admin': return this.adminChannelService; case 'admin': return this.adminChannelService;
default: default:
throw new Error(`no such channel: ${name}`); throw new Error(`no such channel: ${name}`);
} }

View File

@ -49,7 +49,7 @@ class HashtagChannel extends Channel {
if (isUserRelated(note, this.userIdsWhoMeMuting)) return; if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
this.connection.cacheNote(note); this.connection.cacheNote(note);

View File

@ -26,7 +26,7 @@ class HomeTimelineChannel extends Channel {
@bindThis @bindThis
public async init(params: any) { public async init(params: any) {
this.withReplies = params.withReplies as boolean; this.withReplies = params.withReplies as boolean;
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
} }

View File

@ -12,7 +12,7 @@ class RoleTimelineChannel extends Channel {
public static shouldShare = false; public static shouldShare = false;
public static requireCredential = false; public static requireCredential = false;
private roleId: string; private roleId: string;
constructor( constructor(
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private roleservice: RoleService, private roleservice: RoleService,

View File

@ -20,7 +20,7 @@ class UserListChannel extends Channel {
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
private userListJoiningsRepository: UserListJoiningsRepository, private userListJoiningsRepository: UserListJoiningsRepository,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
id: string, id: string,
connection: Channel['connection'], connection: Channel['connection'],
) { ) {

View File

@ -38,9 +38,9 @@ export class FeedService {
link: `${this.config.url}/@${user.username}`, link: `${this.config.url}/@${user.username}`,
name: user.name ?? user.username, name: user.name ?? user.username,
}; };
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
const notes = await this.notesRepository.find({ const notes = await this.notesRepository.find({
where: { where: {
userId: user.id, userId: user.id,
@ -50,7 +50,7 @@ export class FeedService {
order: { createdAt: -1 }, order: { createdAt: -1 },
take: 20, take: 20,
}); });
const feed = new Feed({ const feed = new Feed({
id: author.link, id: author.link,
title: `${author.name} (@${user.username}@${this.config.host})`, title: `${author.name} (@${user.username}@${this.config.host})`,
@ -66,13 +66,13 @@ export class FeedService {
author, author,
copyright: user.name ?? user.username, copyright: user.name ?? user.username,
}); });
for (const note of notes) { for (const note of notes) {
const files = note.fileIds.length > 0 ? await this.driveFilesRepository.findBy({ const files = note.fileIds.length > 0 ? await this.driveFilesRepository.findBy({
id: In(note.fileIds), id: In(note.fileIds),
}) : []; }) : [];
const file = files.find(file => file.type.startsWith('image/')); const file = files.find(file => file.type.startsWith('image/'));
feed.addItem({ feed.addItem({
title: `New note by ${author.name}`, title: `New note by ${author.name}`,
link: `${this.config.url}/notes/${note.id}`, link: `${this.config.url}/notes/${note.id}`,
@ -82,7 +82,7 @@ export class FeedService {
image: file ? this.driveFileEntityService.getPublicUrl(file) ?? undefined : undefined, image: file ? this.driveFileEntityService.getPublicUrl(file) ?? undefined : undefined,
}); });
} }
return feed; return feed;
} }
} }

View File

@ -8,7 +8,7 @@ window.onload = async () => {
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
// Append a credential // Append a credential
if (i) data.i = i; if (i) data.i = i;
// Send request // Send request
window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
method: 'POST', method: 'POST',
@ -17,7 +17,7 @@ window.onload = async () => {
cache: 'no-cache' cache: 'no-cache'
}).then(async (res) => { }).then(async (res) => {
const body = res.status === 204 ? null : await res.json(); const body = res.status === 204 ? null : await res.json();
if (res.status === 200) { if (res.status === 200) {
resolve(body); resolve(body);
} else if (res.status === 204) { } else if (res.status === 204) {
@ -27,7 +27,7 @@ window.onload = async () => {
} }
}).catch(reject); }).catch(reject);
}); });
return promise; return promise;
}; };

View File

@ -8,7 +8,7 @@ window.onload = async () => {
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
// Append a credential // Append a credential
if (i) data.i = i; if (i) data.i = i;
// Send request // Send request
fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
headers: { headers: {
@ -20,7 +20,7 @@ window.onload = async () => {
cache: 'no-cache' cache: 'no-cache'
}).then(async (res) => { }).then(async (res) => {
const body = res.status === 204 ? null : await res.json(); const body = res.status === 204 ? null : await res.json();
if (res.status === 200) { if (res.status === 200) {
resolve(body); resolve(body);
} else if (res.status === 204) { } else if (res.status === 204) {
@ -30,7 +30,7 @@ window.onload = async () => {
} }
}).catch(reject); }).catch(reject);
}); });
return promise; return promise;
}; };

View File

@ -55,8 +55,8 @@ html
block meta block meta
block og block og
meta(property='og:title' content= title || 'Misskey') meta(property='og:title' content= title || 'Misskey')
meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨') meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
meta(property='og:image' content= img) meta(property='og:image' content= img)
meta(property='twitter:card' content='summary') meta(property='twitter:card' content='summary')

View File

@ -32,12 +32,12 @@ body
path(stroke="none", d="M0 0h24v24H0z", fill="none") path(stroke="none", d="M0 0h24v24H0z", fill="none")
path(d="M12 9v2m0 4v.01") path(d="M12 9v2m0 4v.01")
path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75") path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75")
h1 An error has occurred! h1 An error has occurred!
button.button-big(onclick="location.reload();") button.button-big(onclick="location.reload();")
span.button-label-big Refresh span.button-label-big Refresh
p.dont-worry Don't worry, it's (probably) not your fault. p.dont-worry Don't worry, it's (probably) not your fault.
p If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID. p If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.

View File

@ -43,7 +43,7 @@ block meta
meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)
meta(name='misskey:note-id' content=note.id) meta(name='misskey:note-id' content=note.id)
// todo // todo
if user.twitter if user.twitter
meta(name='twitter:creator' content=`@${user.twitter.screenName}`) meta(name='twitter:creator' content=`@${user.twitter.screenName}`)

View File

@ -5,7 +5,7 @@ export const getValidator = (paramDef: Schema) => {
const ajv = new Ajv({ const ajv = new Ajv({
useDefaults: true, useDefaults: true,
}); });
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
return ajv.compile(paramDef); return ajv.compile(paramDef);
} }

View File

@ -34,7 +34,7 @@ describe('DriveService', () => {
test('delete a file', async () => { test('delete a file', async () => {
s3Mock.on(DeleteObjectCommand) s3Mock.on(DeleteObjectCommand)
.resolves({} as DeleteObjectCommandOutput); .resolves({} as DeleteObjectCommandOutput);
await driveService.deleteObjectStorageFile('peace of the world'); await driveService.deleteObjectStorageFile('peace of the world');
}); });

View File

@ -94,7 +94,7 @@ describe('FileInfoService', () => {
orientation: undefined, orientation: undefined,
}); });
}); });
test('Generic APNG', async () => { test('Generic APNG', async () => {
const path = `${resources}/anime.png`; const path = `${resources}/anime.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -114,7 +114,7 @@ describe('FileInfoService', () => {
orientation: undefined, orientation: undefined,
}); });
}); });
test('Generic AGIF', async () => { test('Generic AGIF', async () => {
const path = `${resources}/anime.gif`; const path = `${resources}/anime.gif`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -134,7 +134,7 @@ describe('FileInfoService', () => {
orientation: undefined, orientation: undefined,
}); });
}); });
test('PNG with alpha', async () => { test('PNG with alpha', async () => {
const path = `${resources}/with-alpha.png`; const path = `${resources}/with-alpha.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -154,7 +154,7 @@ describe('FileInfoService', () => {
orientation: undefined, orientation: undefined,
}); });
}); });
test('Generic SVG', async () => { test('Generic SVG', async () => {
const path = `${resources}/image.svg`; const path = `${resources}/image.svg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -174,7 +174,7 @@ describe('FileInfoService', () => {
orientation: undefined, orientation: undefined,
}); });
}); });
test('SVG with XML definition', async () => { test('SVG with XML definition', async () => {
// https://github.com/misskey-dev/misskey/issues/4413 // https://github.com/misskey-dev/misskey/issues/4413
const path = `${resources}/with-xml-def.svg`; const path = `${resources}/with-xml-def.svg`;
@ -195,7 +195,7 @@ describe('FileInfoService', () => {
orientation: undefined, orientation: undefined,
}); });
}); });
test('Dimension limit', async () => { test('Dimension limit', async () => {
const path = `${resources}/25000x25000.png`; const path = `${resources}/25000x25000.png`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -215,7 +215,7 @@ describe('FileInfoService', () => {
orientation: undefined, orientation: undefined,
}); });
}); });
test('Rotate JPEG', async () => { test('Rotate JPEG', async () => {
const path = `${resources}/rotate.jpg`; const path = `${resources}/rotate.jpg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -257,7 +257,7 @@ describe('FileInfoService', () => {
}, },
}); });
}); });
test('WAV', async () => { test('WAV', async () => {
const path = `${resources}/kick_gaba7.wav`; const path = `${resources}/kick_gaba7.wav`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -277,7 +277,7 @@ describe('FileInfoService', () => {
}, },
}); });
}); });
test('AAC', async () => { test('AAC', async () => {
const path = `${resources}/kick_gaba7.aac`; const path = `${resources}/kick_gaba7.aac`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -297,7 +297,7 @@ describe('FileInfoService', () => {
}, },
}); });
}); });
test('FLAC', async () => { test('FLAC', async () => {
const path = `${resources}/kick_gaba7.flac`; const path = `${resources}/kick_gaba7.flac`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
@ -317,7 +317,7 @@ describe('FileInfoService', () => {
}, },
}); });
}); });
/* /*
* video/webmとして検出されてしまう * video/webmとして検出されてしまう
test('WEBM AUDIO', async () => { test('WEBM AUDIO', async () => {

View File

@ -61,7 +61,7 @@ describe('RelayService', () => {
await app.close(); await app.close();
}); });
test('addRelay', async () => { test('addRelay', async () => {
const result = await relayService.addRelay('https://example.com'); const result = await relayService.addRelay('https://example.com');
expect(result.inbox).toBe('https://example.com'); expect(result.inbox).toBe('https://example.com');
@ -72,7 +72,7 @@ describe('RelayService', () => {
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');
}); });
test('listRelay', async () => { test('listRelay', async () => {
const result = await relayService.listRelay(); const result = await relayService.listRelay();
expect(result.length).toBe(1); expect(result.length).toBe(1);
@ -80,7 +80,7 @@ describe('RelayService', () => {
expect(result[0].status).toBe('requesting'); expect(result[0].status).toBe('requesting');
}); });
test('removeRelay: succ', async () => { test('removeRelay: succ', async () => {
await relayService.removeRelay('https://example.com'); await relayService.removeRelay('https://example.com');
expect(queueService.deliver).toHaveBeenCalled(); expect(queueService.deliver).toHaveBeenCalled();
@ -93,7 +93,7 @@ describe('RelayService', () => {
expect(list.length).toBe(0); expect(list.length).toBe(0);
}); });
test('removeRelay: fail', async () => { test('removeRelay: fail', async () => {
await expect(relayService.removeRelay('https://x.example.com')) await expect(relayService.removeRelay('https://x.example.com'))
.rejects.toThrow('relay not found'); .rejects.toThrow('relay not found');
}); });

View File

@ -475,16 +475,16 @@ describe('Chart', () => {
await testIntersectionChart.addA('bob'); await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol'); await testIntersectionChart.addB('carol');
await testIntersectionChart.save(); await testIntersectionChart.save();
const chartHours = await testIntersectionChart.getChart('hour', 3, null); const chartHours = await testIntersectionChart.getChart('hour', 3, null);
const chartDays = await testIntersectionChart.getChart('day', 3, null); const chartDays = await testIntersectionChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
a: [2, 0, 0], a: [2, 0, 0],
b: [1, 0, 0], b: [1, 0, 0],
aAndB: [0, 0, 0], aAndB: [0, 0, 0],
}); });
assert.deepStrictEqual(chartDays, { assert.deepStrictEqual(chartDays, {
a: [2, 0, 0], a: [2, 0, 0],
b: [1, 0, 0], b: [1, 0, 0],
@ -498,16 +498,16 @@ describe('Chart', () => {
await testIntersectionChart.addB('carol'); await testIntersectionChart.addB('carol');
await testIntersectionChart.addB('alice'); await testIntersectionChart.addB('alice');
await testIntersectionChart.save(); await testIntersectionChart.save();
const chartHours = await testIntersectionChart.getChart('hour', 3, null); const chartHours = await testIntersectionChart.getChart('hour', 3, null);
const chartDays = await testIntersectionChart.getChart('day', 3, null); const chartDays = await testIntersectionChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, { assert.deepStrictEqual(chartHours, {
a: [2, 0, 0], a: [2, 0, 0],
b: [2, 0, 0], b: [2, 0, 0],
aAndB: [1, 0, 0], aAndB: [1, 0, 0],
}); });
assert.deepStrictEqual(chartDays, { assert.deepStrictEqual(chartDays, {
a: [2, 0, 0], a: [2, 0, 0],
b: [2, 0, 0], b: [2, 0, 0],

View File

@ -87,7 +87,7 @@ export async function mainBoot() {
const now = new Date(); const now = new Date();
const m = now.getMonth() + 1; const m = now.getMonth() + 1;
const d = now.getDate(); const d = now.getDate();
if ($i.birthday) { if ($i.birthday) {
const bm = parseInt($i.birthday.split('-')[1]); const bm = parseInt($i.birthday.split('-')[1]);
const bd = parseInt($i.birthday.split('-')[2]); const bd = parseInt($i.birthday.split('-')[2]);

Some files were not shown because too many files have changed in this diff Show More