cleanup: trim trailing whitespace (#11136)
* cleanup: trim trailing whitespace * update(`.editorconfig`) --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
4c879b3a33
commit
d84796588c
161 changed files with 613 additions and 607 deletions
|
@ -2,7 +2,7 @@ version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
```
|
```
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
@ -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');
|
||||||
|
|
|
@ -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>
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 :
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class NoteDeleteService {
|
||||||
private perUserNotesChart: PerUserNotesChart,
|
private perUserNotesChart: PerUserNotesChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 投稿を削除します。
|
* 投稿を削除します。
|
||||||
* @param user 投稿者
|
* @param user 投稿者
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}`,
|
||||||
'+',
|
'+',
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class WebhookService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
this.webhooksFetched = true;
|
this.webhooksFetched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.webhooks;
|
return this.webhooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -90,7 +90,7 @@ export class ApDeliverManagerService {
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
this.queueService,
|
this.queueService,
|
||||||
|
|
||||||
actor,
|
actor,
|
||||||
activity,
|
activity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class ApImageService {
|
||||||
) {
|
) {
|
||||||
this.logger = this.apLoggerService.logger;
|
this.logger = this.apLoggerService.logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imageを作成します。
|
* Imageを作成します。
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 [];
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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'>>;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>>;
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}`))
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -113,7 +113,7 @@ export class ImportCustomEmojisProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
this.logger.succ('Imported');
|
this.logger.succ('Imported');
|
||||||
});
|
});
|
||||||
unzipStream.pipe(extractor);
|
unzipStream.pipe(extractor);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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));
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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++) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`,
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'],
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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}`)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue