1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
|
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { URL, domainToASCII } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import RE2 from 're2';
import psl from 'psl';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { MiMeta } from '@/models/Meta.js';
@Injectable()
export class UtilityService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
) {
}
@bindThis
public getFullApAccount(username: string, host: string | null): string {
return host ? `${username}@${this.toPuny(host)}` : `${username}@${this.toPuny(this.config.host)}`;
}
@bindThis
public isSelfHost(host: string | null): boolean {
if (host == null) return true;
return this.toPuny(this.config.host) === this.toPuny(host);
}
@bindThis
public isUriLocal(uri: string): boolean {
return this.punyHost(uri) === this.toPuny(this.config.host);
}
// メールアドレスのバリデーションを行う
// https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
@bindThis
public validateEmailFormat(email: string): boolean {
// Note: replaced MK's complicated regex with a simpler one that is more efficient and reliable.
const regexp = /^.+@.+$/;
//const regexp = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
return regexp.test(email);
}
public isBlockedHost(host: string | null): boolean;
public isBlockedHost(blockedHosts: string[], host: string | null): boolean;
@bindThis
public isBlockedHost(blockedHostsOrHost: string[] | string | null, host?: string | null): boolean {
const blockedHosts = Array.isArray(blockedHostsOrHost) ? blockedHostsOrHost : this.meta.blockedHosts;
host = Array.isArray(blockedHostsOrHost) ? host : blockedHostsOrHost;
if (host == null) return false;
return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
public isSilencedHost(host: string | null): boolean;
public isSilencedHost(silencedHosts: string[], host: string | null): boolean;
@bindThis
public isSilencedHost(silencedHostsOrHost: string[] | string | null, host?: string | null): boolean {
const silencedHosts = Array.isArray(silencedHostsOrHost) ? silencedHostsOrHost : this.meta.silencedHosts;
host = Array.isArray(silencedHostsOrHost) ? host : silencedHostsOrHost;
if (host == null) return false;
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
public isMediaSilencedHost(host: string | null): boolean;
public isMediaSilencedHost(silencedHosts: string[], host: string | null): boolean;
@bindThis
public isMediaSilencedHost(mediaSilencedHostsOrHost: string[] | string | null, host?: string | null): boolean {
const mediaSilencedHosts = Array.isArray(mediaSilencedHostsOrHost) ? mediaSilencedHostsOrHost : this.meta.mediaSilencedHosts;
host = Array.isArray(mediaSilencedHostsOrHost) ? host : mediaSilencedHostsOrHost;
if (host == null) return false;
return mediaSilencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public isAllowListedHost(host: string | null): boolean {
if (host == null) return false;
return this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public isBubbledHost(host: string | null): boolean {
if (host == null) return false;
return this.meta.bubbleInstances.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public concatNoteContentsForKeyWordCheck(content: {
cw?: string | null;
text?: string | null;
pollChoices?: string[] | null;
others?: string[] | null;
}): string {
/**
* ノートの内容を結合してキーワードチェック用の文字列を生成する
* cwとtextは内容が繋がっているかもしれないので間に何も入れずにチェックする
*/
return `${content.cw ?? ''}${content.text ?? ''}\n${(content.pollChoices ?? []).join('\n')}\n${(content.others ?? []).join('\n')}`;
}
@bindThis
public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
if (keyWords.length === 0) return false;
if (text === '') return false;
const regexpregexp = /^\/(.+)\/(.*)$/;
const matched = keyWords.some(filter => {
// represents RegExp
const regexp = filter.match(regexpregexp);
// This should never happen due to input sanitisation.
if (!regexp) {
const words = filter.split(' ');
return words.every(keyword => text.includes(keyword));
}
try {
// TODO: RE2インスタンスをキャッシュ
return new RE2(regexp[1], regexp[2]).test(text);
} catch (err) {
// This should never happen due to input sanitisation.
return false;
}
});
return matched;
}
@bindThis
public extractDbHost(uri: string): string {
const url = new URL(uri);
return this.toPuny(url.host);
}
@bindThis
public toPuny(host: string): string {
// domainToASCII will return an empty string if we give it a
// string like `name:123`, but `host` may well be in that form
// (e.g. when testing locally, you'll get `localhost:3000`); split
// the port off, and add it back later
const hostParts = host.toLowerCase().match(/^(.+?)(:.+)?$/);
if (!hostParts) return '';
const hostname = hostParts[1];
const port = hostParts[2] ?? '';
return domainToASCII(hostname) + port;
}
@bindThis
public toPunyNullable(host: string | null | undefined): string | null {
if (host == null) return null;
return this.toPuny(host);
}
@bindThis
public punyHost(url: string): string {
const urlObj = new URL(url);
const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
return host;
}
@bindThis
private specialSuffix(hostname: string): string | null {
// masto.host provides domain names for its clients, we have to
// treat it as if it were a public suffix
const mastoHost = hostname.match(/\.?([a-zA-Z0-9-]+\.masto\.host)$/i);
if (mastoHost) {
return mastoHost[1];
}
return null;
}
@bindThis
public punyHostPSLDomain(url: string): string {
const urlObj = new URL(url);
const hostname = urlObj.hostname;
const domain = this.specialSuffix(hostname) ?? psl.get(hostname) ?? hostname;
const host = `${this.toPuny(domain)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
return host;
}
@bindThis
public isFederationAllowedHost(host: string): boolean {
if (this.meta.federation === 'none') return false;
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
if (this.isBlockedHost(this.meta.blockedHosts, host)) return false;
return true;
}
@bindThis
public isFederationAllowedUri(uri: string): boolean {
const host = this.extractDbHost(uri);
return this.isFederationAllowedHost(host);
}
@bindThis
public getUrlScheme(url: string): string {
try {
// Returns in the format "https:" or an empty string
return new URL(url).protocol;
} catch {
return '';
}
}
}
|