Compare commits
	
		
			54 Commits
		
	
	
		
			2024.9.0-a
			...
			10.102.4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					ea60565b0d | ||
| 
						 | 
					a26585dcc7 | ||
| 
						 | 
					008e5bd24c | ||
| 
						 | 
					ebc6564fbb | ||
| 
						 | 
					06b5078245 | ||
| 
						 | 
					68337f95ff | ||
| 
						 | 
					72e148002a | ||
| 
						 | 
					f0a03f71eb | ||
| 
						 | 
					6049473e46 | ||
| 
						 | 
					f9f2787dfc | ||
| 
						 | 
					b0a068e269 | ||
| 
						 | 
					dd5546690a | ||
| 
						 | 
					d87b6d38ab | ||
| 
						 | 
					4069bb170a | ||
| 
						 | 
					600c009549 | ||
| 
						 | 
					c953a28201 | ||
| 
						 | 
					f6541df42a | ||
| 
						 | 
					a57e9460c8 | ||
| 
						 | 
					c7bcf31105 | ||
| 
						 | 
					a397c040fe | ||
| 
						 | 
					e311d73ffc | ||
| 
						 | 
					2a3599a14d | ||
| 
						 | 
					b85dc8a658 | ||
| 
						 | 
					7513123052 | ||
| 
						 | 
					2526b86ec5 | ||
| 
						 | 
					e45aa0532c | ||
| 
						 | 
					71fc84e224 | ||
| 
						 | 
					6b726eea39 | ||
| 
						 | 
					acd4b101e1 | ||
| 
						 | 
					98b8a94f2b | ||
| 
						 | 
					7d31bd97ff | ||
| 
						 | 
					828a2a73c9 | ||
| 
						 | 
					bdc7167cf4 | ||
| 
						 | 
					45b94086ed | ||
| 
						 | 
					e62bb7cdaf | ||
| 
						 | 
					c5f65d9eeb | ||
| 
						 | 
					4557856104 | ||
| 
						 | 
					4a23c36740 | ||
| 
						 | 
					2553b20130 | ||
| 
						 | 
					6982faf668 | ||
| 
						 | 
					4db972318f | ||
| 
						 | 
					81006566a5 | ||
| 
						 | 
					6abc053a48 | ||
| 
						 | 
					03a3c56a54 | ||
| 
						 | 
					83b7010d6a | ||
| 
						 | 
					71654cbe47 | ||
| 
						 | 
					e8f96e848a | ||
| 
						 | 
					251abf21d4 | ||
| 
						 | 
					d103427932 | ||
| 
						 | 
					592cdfa910 | ||
| 
						 | 
					f2ad1a0406 | ||
| 
						 | 
					82af9320c0 | ||
| 
						 | 
					fceebf7388 | ||
| 
						 | 
					13caf37991 | 
@@ -53,40 +53,54 @@ mongodb:
 | 
			
		||||
drive:
 | 
			
		||||
  storage: 'db'
 | 
			
		||||
 | 
			
		||||
  # OR
 | 
			
		||||
# OR
 | 
			
		||||
 | 
			
		||||
  # storage: 'minio'
 | 
			
		||||
  # bucket:
 | 
			
		||||
  # prefix:
 | 
			
		||||
  # config:
 | 
			
		||||
  #   endPoint:
 | 
			
		||||
  #   port:
 | 
			
		||||
  #   useSSL:
 | 
			
		||||
  #   accessKey:
 | 
			
		||||
  #   secretKey:
 | 
			
		||||
#drive:
 | 
			
		||||
#  storage: 'minio'
 | 
			
		||||
#  bucket:
 | 
			
		||||
#  prefix:
 | 
			
		||||
#  config:
 | 
			
		||||
#    endPoint:
 | 
			
		||||
#    port:
 | 
			
		||||
#    useSSL:
 | 
			
		||||
#    accessKey:
 | 
			
		||||
#    secretKey:
 | 
			
		||||
 | 
			
		||||
  # S3 example
 | 
			
		||||
  # storage: 'minio'
 | 
			
		||||
  # bucket: bucket-name
 | 
			
		||||
  # prefix: files
 | 
			
		||||
  # config:
 | 
			
		||||
  #   endPoint: s3-us-west-2.amazonaws.com
 | 
			
		||||
  #   region: us-west-2
 | 
			
		||||
  #   useSSL: true
 | 
			
		||||
  #   accessKey: XXX
 | 
			
		||||
  #   secretKey: YYY
 | 
			
		||||
# S3/GCS example
 | 
			
		||||
#
 | 
			
		||||
# * Replace <endpoint> to
 | 
			
		||||
#     S3: see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
 | 
			
		||||
#     GCS: use 'storage.googleapis.com'
 | 
			
		||||
#
 | 
			
		||||
# * Replace <region> to
 | 
			
		||||
#     S3: see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
 | 
			
		||||
#     GCS: not needed (just delete the region line)
 | 
			
		||||
#
 | 
			
		||||
#drive:
 | 
			
		||||
#  storage: 'minio'
 | 
			
		||||
#  bucket: bucket-name
 | 
			
		||||
#  prefix: files
 | 
			
		||||
#  baseUrl: https://bucket-name.<endpoint>
 | 
			
		||||
#  config:
 | 
			
		||||
#    endPoint: <endpoint>
 | 
			
		||||
#    region: <region>
 | 
			
		||||
#    useSSL: true
 | 
			
		||||
#    accessKey: XXX
 | 
			
		||||
#    secretKey: YYY
 | 
			
		||||
 | 
			
		||||
  # S3 example (with CDN, custom domain)
 | 
			
		||||
  # storage: 'minio'
 | 
			
		||||
  # bucket: drive.example.com
 | 
			
		||||
  # prefix: files
 | 
			
		||||
  # baseUrl: https://drive.example.com
 | 
			
		||||
  # config:
 | 
			
		||||
  #   endPoint: s3-us-west-2.amazonaws.com
 | 
			
		||||
  #   region: us-west-2
 | 
			
		||||
  #   useSSL: true
 | 
			
		||||
  #   accessKey: XXX
 | 
			
		||||
  #   secretKey: YYY
 | 
			
		||||
# S3/GCS example (with CDN, custom domain)
 | 
			
		||||
#
 | 
			
		||||
#drive:
 | 
			
		||||
#  storage: 'minio'
 | 
			
		||||
#  bucket: drive.example.com
 | 
			
		||||
#  prefix: files
 | 
			
		||||
#  baseUrl: https://drive.example.com
 | 
			
		||||
#  config:
 | 
			
		||||
#    endPoint: <endpoint>
 | 
			
		||||
#    region: <region>
 | 
			
		||||
#    useSSL: true
 | 
			
		||||
#    accessKey: XXX
 | 
			
		||||
#    secretKey: YYY
 | 
			
		||||
 | 
			
		||||
# If enabled:
 | 
			
		||||
#  The first account created is automatically marked as Admin.
 | 
			
		||||
@@ -113,3 +127,6 @@ autoAdmin: true
 | 
			
		||||
 | 
			
		||||
# Clustering
 | 
			
		||||
#clusterLimit: 1
 | 
			
		||||
 | 
			
		||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
 | 
			
		||||
#outgoingAddressFamily: ipv4
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
# PATH                                          OWNERS
 | 
			
		||||
/.autogen/                                      @acid-chicken
 | 
			
		||||
/.circleci/                                     @syuilo @acid-chicken
 | 
			
		||||
/.config/                                       @syuilo @AyaMorisawa @mei23 @acid-chicken
 | 
			
		||||
/.config/                                       @syuilo @AyaMorisawa @mei23 @acid-chicken @rinsuki
 | 
			
		||||
# /.config/mongo_initdb_example.js              @khws4v1
 | 
			
		||||
/.github/                                       @syuilo @AyaMorisawa @acid-chicken
 | 
			
		||||
/.vscode/                                       @acid-chicken
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
# /docs/*.fr.md                                 @BoFFire
 | 
			
		||||
# /docs/docker.*.md                             @khws4v1
 | 
			
		||||
/locales/                                       @syuilo
 | 
			
		||||
/src/                                           @syuilo @AyaMorisawa @mei23 @acid-chicken
 | 
			
		||||
/src/                                           @syuilo @AyaMorisawa @mei23 @acid-chicken @rinsuki
 | 
			
		||||
# /src/crypto_key.cc                            @akihikodaki
 | 
			
		||||
# /src/crypto_key.d.ts                          @akihikodaki
 | 
			
		||||
/.dockerignore                                  @syuilo # @khws4v1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -19,3 +19,4 @@ api-docs.json
 | 
			
		||||
*.code-workspace
 | 
			
		||||
yarn.lock
 | 
			
		||||
.DS_Store
 | 
			
		||||
/files
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -5,6 +5,27 @@ If you encounter any problems with updating, please try the following:
 | 
			
		||||
1. `npm run clean` or `npm run cleanall`
 | 
			
		||||
2. Retry update (Don't forget `npm i`)
 | 
			
		||||
 | 
			
		||||
10.102.1
 | 
			
		||||
----------
 | 
			
		||||
* 投稿が増殖する問題を修正
 | 
			
		||||
* リモートユーザーの修復処理が自動的に実行されない問題を修正
 | 
			
		||||
 | 
			
		||||
10.101.0
 | 
			
		||||
----------
 | 
			
		||||
* WebFingerリクエストで Proxy, Keep-Alive などをサポート
 | 
			
		||||
* AP actor Service のサポートが不完全な問題を修正
 | 
			
		||||
* Punycodeなインスタンスが重複登録される問題を修正
 | 
			
		||||
* ObjectStrage利用時にドライブファイルアイコンが表示されない問題を修正
 | 
			
		||||
 | 
			
		||||
10.100.0
 | 
			
		||||
----------
 | 
			
		||||
* ユーザーリストでフォローボタンを表示するように
 | 
			
		||||
* ドライブのファイルのサムネイルを修正
 | 
			
		||||
* 投稿ウィジットでローカルのみの公開範囲で投稿できない問題を修正
 | 
			
		||||
* TLを遡った時に抜けがある時がある問題を修正
 | 
			
		||||
* ユーザータイムラインが投稿日時順ではなくなっているのを修正
 | 
			
		||||
* 10.99.0 でチャートのレンダリングがおかしい問題を修正
 | 
			
		||||
 | 
			
		||||
10.99.0
 | 
			
		||||
----------
 | 
			
		||||
* manifest.json にインスタンス名を反映させるように
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										77
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										77
									
								
								README.md
									
									
									
									
									
								
							@@ -88,12 +88,14 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 | 
			
		||||
		<td><img src="https://avatars0.githubusercontent.com/u/10798641?s=460&v=4" alt="AyaMorisawa" width="100"></td>
 | 
			
		||||
		<td><img src="https://avatars1.githubusercontent.com/u/30769358?s=460&v=4" alt="mei23" width="100"></td>
 | 
			
		||||
		<td><img src="https://avatars2.githubusercontent.com/u/20679825?s=460&v=4" alt="acid-chicken" width="100"></td>
 | 
			
		||||
		<td><img src="https://avatars2.githubusercontent.com/u/6533808?s=460&v=4" alt="rinsuki" width="100"></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
	<tr>
 | 
			
		||||
		<td align="center"><a href="https://github.com/syuilo">@syuilo</a></td>
 | 
			
		||||
		<td align="center"><a href="https://github.com/AyaMorisawa">@AyaMorisawa</a></td>
 | 
			
		||||
		<td align="center"><a href="https://github.com/mei23">@mei23</a></td>
 | 
			
		||||
		<td align="center"><a href="https://github.com/acid-chicken">@acid-chicken</a></td>
 | 
			
		||||
		<td align="center"><a href="https://github.com/rinsuki">@rinsuki</a></td>
 | 
			
		||||
	</tr>
 | 
			
		||||
</table>
 | 
			
		||||
 | 
			
		||||
@@ -101,62 +103,83 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
<!-- PATREON_START -->
 | 
			
		||||
<table><tr>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5888816/36da0f7c15954df0ab13f9abdf227f66/1?token-time=2145916800&token-hash=HGkZJ7s4bSaQVoOJ5q30mTWHTxDLiw1LuyaogKPLy24%3D" alt="Hiroshi Seki" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/1?token-time=2145916800&token-hash=WeuDzzz24cRXJogyIkU-mxARqkdyms-rcZKbO-GpGjw%3D" alt="weep" width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/12059069" alt="naga_rus" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/4?token-time=2145916800&token-hash=vZdDTTF-ahiKBjjgppS2ev4rkD8H7TTKkXXoxsucs6Y%3D" alt="Melilot" width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5888816/36da0f7c15954df0ab13f9abdf227f66/1.jpeg?token-time=2145916800&token-hash=at8QpJXJ8C0zINY_NmoMKv-MhXVoUK-YzTgaJPJzJYU%3D" alt="Hiroshi Seki" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19045173/cb91c0f345c24d4ebfd05f19906d5e26/1.png?token-time=2145916800&token-hash=o_zKBytJs_AxHwSYw_5R8eD0eSJe3RoTR3kR3Q0syN0%3D" alt="kiritan" width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/776209" alt="Denshi" width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/557245" alt="mkatze" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13099460/43cecdbaa63a40d79bf50a96b9910b9d/1.jpe?token-time=2145916800&token-hash=bqwLTk0Wo0hUJJ8J5y7ii05bLzz-_CDA7Bo0Mp4RFU0%3D" alt="ne_moni" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/4.jpe?token-time=2145916800&token-hash=zEyJqVM7u9d8Ri-65fJYSJcWF1jBH1nJ5a3taRzrTmw%3D" alt="Melilot" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon" width="100"></td>
 | 
			
		||||
</tr><tr>
 | 
			
		||||
<td><a href="https://www.patreon.com/rane_hs">Hiroshi Seki</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/weepjp">weep</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=12059069">naga_rus</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/weepjp">weepjp</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=19045173">kiritan</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=557245">mkatze</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=13099460">ne_moni</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/osapon">osapon</a></td>
 | 
			
		||||
</tr></table>
 | 
			
		||||
<table><tr>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=1FlxS9MEgmNGH_RHUVHbO5hIXB5I1z0lvA33CTvYvjA%3D" alt="gutfuckllc" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1?token-time=2145916800&token-hash=0xgcpqvFDqRcV_YIEhcPNVH7gs9sLg_BBnTJXCkN4ao%3D" alt="mydarkstar" width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1.png?token-time=2145916800&token-hash=FMV7cPKBD1TU2WTbl1jg6AcdKSvTb2BSFcDhgc-EO8w%3D" alt="gutfuckllc" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1.png?token-time=2145916800&token-hash=9nEQje_eMvUjq9a7L3uBqW-MQbS-rRMaMgd7UYVoFNM%3D" alt="mydarkstar" width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/12718187" alt="Peter G." width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/18833336" alt="itiradi" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1?token-time=2145916800&token-hash=2PsbFNw0tnubZzgSXD01R6hIgncfiElG7H7HX2Y3dyo%3D" alt="nemu" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1.jpe?token-time=2145916800&token-hash=UQRWf01TwHDV4Cls1K0YAOAjM29ssif7hLVq0ESQ0hs%3D" alt="nemu" width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3?token-time=2145916800&token-hash=9JtETp0X8gI280Ne1E8bxn6j4Lw5o2k4mJkICx97V_k%3D" alt="YUKIMOCHI" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td>
 | 
			
		||||
<td><img src="https://c8.patreon.com/2/200/17463605" alt="Sampot" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1?token-time=2145916800&token-hash=95p8VdGX45E8BitZR_eOcDlqCjumjzNLBPQJrJdeCpI%3D" alt="takimura" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17195955/be45e5e14c3e48b2bee0456c84e19df4/4?token-time=2145916800&token-hash=SbdZeN5SmsuT9stD6v0jN1z0hftg0FmRiCTxysU0Ihw%3D" alt="Damillora" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s" width="100"></td>
 | 
			
		||||
</tr><tr>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=18833336">itiradi</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=13039004">nemu</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=17463605">Sampot</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/takimura">takimura</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/damillora">Damillora</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=19356899">sheeta.s</a></td>
 | 
			
		||||
</tr></table>
 | 
			
		||||
<table><tr>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/935a10339daa4ede8e555903a0707060/1?token-time=2145916800&token-hash=3CrpqH-XtKs_NoIlSsTyVs8wCzP1WFCsG2xwps1IJq0%3D" alt="Atsuko Tominaga" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3?token-time=2145916800&token-hash=-iJszBqgYBhsM5qMdA1knf9wvprhEfESzKfR2oh7mIA%3D" alt="natalie" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=5T8XcaAf9Zyzfg3QubR06s_kJZkArVEM2dwObrBVAU4%3D" alt="Hiratake" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1?token-time=2145916800&token-hash=D6QK3fPyqiYKJfOzc-QqaSSairUrWdjld-ewp2waj6s%3D" alt="Hekovic" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=Ksk_2l3gjPDbnzMUOCSW1E-hdPJsNs2tSR4_RAakRK8%3D" alt="dansup" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=CXe9AqlZy9AsYfiWd3OBYVOzvODoN47Litz0Tu4BFpU%3D" alt="Gargron" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=xhR1n6NAAyEb-IUXLD6_dshkFa3mefU5ZZuk1L8qKTs%3D" alt="Nokotaro Takeda" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=uR-48MQ0A4j0irQSrCAQZJ-sJUSs_Fkihlg3-l59b7c%3D" alt="Takashi Shibuya" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpe?token-time=2145916800&token-hash=CPxGQhKIlEaa6WUcgbyHixyKEhakiw9RFdOhsIJBQ_o%3D" alt="takimura" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17195955/be45e5e14c3e48b2bee0456c84e19df4/4.jpe?token-time=2145916800&token-hash=UslrPVM-8TXOe8AapuNiaFYjcIJgPNcU-fKpGbfGJNI%3D" alt="Damillora" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/83884b38afc74d4cbe83c30a13b10edd/1.png?token-time=2145916800&token-hash=R5Tog8RWg0rguRoCIoir3lThokrdPvs8Utfikhc0nhY%3D" alt="Atsuko Tominaga" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1.jpe?token-time=2145916800&token-hash=EWxXhVbZYH7KB4IDT3joc8TbIg8zPO40x1r5IDn3R7c%3D" alt="Hiratake" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpe?token-time=2145916800&token-hash=qA8j97lIZNc-74AuZ0p4F3ms6sKPeKjtNt2vEuwpsyo%3D" alt="Hekovic" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/10789744/97175095d8f04c0f86225ff47cb98d40/1.jpeg?token-time=2145916800&token-hash=l4AoMR7Nj7K4yAHrkrk2hAoggPkbSPm12m1nmbe9Pb8%3D" alt="Naoki Hirayama" width="100"></td>
 | 
			
		||||
</tr><tr>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/takimura">takimura</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/damillora">Damillora</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/noellabo">noellabo</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/Corset">CG</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/spinlock">Naoki Hirayama</a></td>
 | 
			
		||||
</tr></table>
 | 
			
		||||
<table><tr>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1.jpeg?token-time=2145916800&token-hash=d8jBQLMOHD87KtXs5C9fk1o58DMF73pQ-dYH3uZJPBE%3D" alt="Gargron" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td>
 | 
			
		||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td>
 | 
			
		||||
</tr><tr>
 | 
			
		||||
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
 | 
			
		||||
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
 | 
			
		||||
</tr></table>
 | 
			
		||||
 | 
			
		||||
**Last updated:** Fri, 05 Apr 2019 09:39:06 UTC
 | 
			
		||||
**Last updated:** Mon, 03 Jun 2019 17:28:09 UTC
 | 
			
		||||
<!-- PATREON_END -->
 | 
			
		||||
 | 
			
		||||
:four_leaf_clover: Copyright
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,23 @@ This guide describes how to install and setup Misskey with Docker.
 | 
			
		||||
 | 
			
		||||
*1.* Download Misskey
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `git clone -b master git://github.com/syuilo/misskey.git` Clone Misskey repository's master branch.
 | 
			
		||||
2. `cd misskey` Move to misskey directory.
 | 
			
		||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) tag.
 | 
			
		||||
1. Clone Misskey repository's master branch.
 | 
			
		||||
 | 
			
		||||
	`git clone -b master git://github.com/syuilo/misskey.git`
 | 
			
		||||
 | 
			
		||||
2. Move to misskey directory.
 | 
			
		||||
 | 
			
		||||
	`cd misskey`
 | 
			
		||||
 | 
			
		||||
3. Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) tag.
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
*2.* Configure Misskey
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
@@ -39,7 +53,15 @@ Just `docker-compose up -d`. GLHF!
 | 
			
		||||
### How to update your Misskey server to the latest version
 | 
			
		||||
1. `git fetch`
 | 
			
		||||
2. `git stash`
 | 
			
		||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
 | 
			
		||||
3. 
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
4. `git stash pop`
 | 
			
		||||
5. `docker-compose build`
 | 
			
		||||
6. Check [ChangeLog](../CHANGELOG.md) for migration information
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,23 @@ Ce guide explique comment installer et configurer Misskey avec Docker.
 | 
			
		||||
 | 
			
		||||
*1.* Télécharger Misskey
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `git clone -b master git://github.com/syuilo/misskey.git` Clone le dépôt de Misskey sur la branche master.
 | 
			
		||||
2. `cd misskey` Naviguez dans le dossier du dépôt.
 | 
			
		||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout sur le tag de la [dernière version](https://github.com/syuilo/misskey/releases/latest).
 | 
			
		||||
1. Clone le dépôt de Misskey sur la branche master.
 | 
			
		||||
 | 
			
		||||
	`git clone -b master git://github.com/syuilo/misskey.git`
 | 
			
		||||
 | 
			
		||||
2. Naviguez dans le dossier du dépôt.
 | 
			
		||||
 | 
			
		||||
	`cd misskey`
 | 
			
		||||
 | 
			
		||||
3. Checkout sur le tag de la [dernière version](https://github.com/syuilo/misskey/releases/latest).
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
*2.* Configuration de Misskey
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
@@ -40,7 +54,15 @@ Utilisez la commande `docker-compose up -d`. GLHF!
 | 
			
		||||
### How to update your Misskey server to the latest version
 | 
			
		||||
1. `git fetch`
 | 
			
		||||
2. `git stash`
 | 
			
		||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
 | 
			
		||||
3. 
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
4. `git stash pop`
 | 
			
		||||
5. `docker-compose build`
 | 
			
		||||
6. Consultez le [ChangeLog](../CHANGELOG.md) pour avoir les éventuelles informations de migration
 | 
			
		||||
@@ -52,14 +74,28 @@ Utilisez la commande `docker-compose up -d`. GLHF!
 | 
			
		||||
### Configuration d'ElasticSearch (pour la fonction de recherche)
 | 
			
		||||
*1.* Préparation de l'environnement
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `mkdir elasticsearch && chown 1000:1000 elasticsearch` Permet de créer le dossier d'accueil de la base ElasticSearch aves les bons droits
 | 
			
		||||
2. `sysctl -w vm.max_map_count=262144` Augmente la valeur max du paramètre map_count du système (valeur minimum pour pouvoir lancer ES)
 | 
			
		||||
1. Permet de créer le dossier d'accueil de la base ElasticSearch aves les bons droits
 | 
			
		||||
 | 
			
		||||
	`mkdir elasticsearch && chown 1000:1000 elasticsearch`
 | 
			
		||||
 | 
			
		||||
2. Augmente la valeur max du paramètre map_count du système (valeur minimum pour pouvoir lancer ES)
 | 
			
		||||
 | 
			
		||||
	`sysctl -w vm.max_map_count=262144`
 | 
			
		||||
 | 
			
		||||
*2.* Après lancement du docker-compose, initialisation de la base ElasticSearch
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `docker-compose -it web /bin/sh` Connexion dans le conteneur web
 | 
			
		||||
2. `apk add curl` Ajout du paquet curl
 | 
			
		||||
3. `curl -X PUT "es:9200/misskey" -H 'Content-Type: application/json' -d'{ "settings" : { "index" : { } }}'` Création de la base ES
 | 
			
		||||
1. Connexion dans le conteneur web
 | 
			
		||||
 | 
			
		||||
	`docker-compose -it web /bin/sh`
 | 
			
		||||
 | 
			
		||||
2. Ajout du paquet curl
 | 
			
		||||
 | 
			
		||||
	`apk add curl`
 | 
			
		||||
 | 
			
		||||
3. Création de la base ES
 | 
			
		||||
 | 
			
		||||
	`curl -X PUT "es:9200/misskey" -H 'Content-Type: application/json' -d'{ "settings" : { "index" : { } }}'`
 | 
			
		||||
 | 
			
		||||
4. `exit`
 | 
			
		||||
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,23 @@ Dockerを使ったMisskey構築方法
 | 
			
		||||
 | 
			
		||||
*1.* Misskeyのダウンロード
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `git clone -b master git://github.com/syuilo/misskey.git` masterブランチからMisskeyレポジトリをクローン
 | 
			
		||||
2. `cd misskey` misskeyディレクトリに移動
 | 
			
		||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
 | 
			
		||||
1. masterブランチからMisskeyレポジトリをクローン
 | 
			
		||||
 | 
			
		||||
	`git clone -b master git://github.com/syuilo/misskey.git`
 | 
			
		||||
 | 
			
		||||
2. misskeyディレクトリに移動
 | 
			
		||||
 | 
			
		||||
	`cd misskey`
 | 
			
		||||
 | 
			
		||||
3. [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
*2.* 設定ファイルを作成する
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
@@ -39,7 +53,15 @@ Dockerを使ったMisskey構築方法
 | 
			
		||||
### Misskeyを最新バージョンにアップデートする方法:
 | 
			
		||||
1. `git fetch`
 | 
			
		||||
2. `git stash`
 | 
			
		||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
 | 
			
		||||
3. 
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
4. `git stash pop`
 | 
			
		||||
5. `docker-compose build`
 | 
			
		||||
6. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
 | 
			
		||||
 
 | 
			
		||||
@@ -41,15 +41,38 @@ As root:
 | 
			
		||||
 | 
			
		||||
*4.* Install Misskey
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `su - misskey` Connect to misskey user.
 | 
			
		||||
2. `git clone -b master git://github.com/syuilo/misskey.git` Clone the misskey repo from master branch.
 | 
			
		||||
3. `cd misskey` Navigate to misskey directory
 | 
			
		||||
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest)
 | 
			
		||||
5. `npm install` Install misskey dependencies.
 | 
			
		||||
1. Connect to misskey user.
 | 
			
		||||
 | 
			
		||||
	`su - misskey`
 | 
			
		||||
 | 
			
		||||
2. Clone the misskey repo from master branch.
 | 
			
		||||
 | 
			
		||||
	`git clone -b master git://github.com/syuilo/misskey.git`
 | 
			
		||||
 | 
			
		||||
3. Navigate to misskey directory
 | 
			
		||||
 | 
			
		||||
	`cd misskey`
 | 
			
		||||
 | 
			
		||||
4. Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest)
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
5. Install misskey dependencies.
 | 
			
		||||
 | 
			
		||||
	`npm install`
 | 
			
		||||
 | 
			
		||||
*5.* Configure Misskey
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
 | 
			
		||||
1. Copy the `.config/example.yml` and rename it to `default.yml`.
 | 
			
		||||
 | 
			
		||||
	`cp .config/example.yml .config/default.yml`
 | 
			
		||||
 | 
			
		||||
2. Edit `default.yml`
 | 
			
		||||
 | 
			
		||||
*6.* Build Misskey
 | 
			
		||||
@@ -77,37 +100,53 @@ Just `NODE_ENV=production npm start`. GLHF!
 | 
			
		||||
 | 
			
		||||
### Launch with systemd
 | 
			
		||||
 | 
			
		||||
1. Create a systemd service here: `/etc/systemd/system/misskey.service`
 | 
			
		||||
1. Create a systemd service here
 | 
			
		||||
 | 
			
		||||
	`/etc/systemd/system/misskey.service`
 | 
			
		||||
 | 
			
		||||
2. Edit it, and paste this and save:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Misskey daemon
 | 
			
		||||
	```
 | 
			
		||||
	[Unit]
 | 
			
		||||
	Description=Misskey daemon
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=simple
 | 
			
		||||
User=misskey
 | 
			
		||||
ExecStart=/usr/bin/npm start
 | 
			
		||||
WorkingDirectory=/home/misskey/misskey
 | 
			
		||||
Environment="NODE_ENV=production"
 | 
			
		||||
TimeoutSec=60
 | 
			
		||||
StandardOutput=syslog
 | 
			
		||||
StandardError=syslog
 | 
			
		||||
SyslogIdentifier=misskey
 | 
			
		||||
Restart=always
 | 
			
		||||
	[Service]
 | 
			
		||||
	Type=simple
 | 
			
		||||
	User=misskey
 | 
			
		||||
	ExecStart=/usr/bin/npm start
 | 
			
		||||
	WorkingDirectory=/home/misskey/misskey
 | 
			
		||||
	Environment="NODE_ENV=production"
 | 
			
		||||
	TimeoutSec=60
 | 
			
		||||
	StandardOutput=syslog
 | 
			
		||||
	StandardError=syslog
 | 
			
		||||
	SyslogIdentifier=misskey
 | 
			
		||||
	Restart=always
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
```
 | 
			
		||||
	[Install]
 | 
			
		||||
	WantedBy=multi-user.target
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
3. `systemctl daemon-reload ; systemctl enable misskey` Reload systemd and enable the misskey service.
 | 
			
		||||
4. `systemctl start misskey` Start the misskey service.
 | 
			
		||||
3. Reload systemd and enable the misskey service.
 | 
			
		||||
 | 
			
		||||
	`systemctl daemon-reload ; systemctl enable misskey`
 | 
			
		||||
 | 
			
		||||
4. Start the misskey service.
 | 
			
		||||
 | 
			
		||||
	`systemctl start misskey`
 | 
			
		||||
 | 
			
		||||
You can check if the service is running with `systemctl status misskey`.
 | 
			
		||||
 | 
			
		||||
### How to update your Misskey server to the latest version
 | 
			
		||||
1. `git fetch`
 | 
			
		||||
2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
 | 
			
		||||
2.  
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
3. `npm install`
 | 
			
		||||
4. `NODE_ENV=production npm run build`
 | 
			
		||||
5. Check [ChangeLog](../CHANGELOG.md) for migration information
 | 
			
		||||
 
 | 
			
		||||
@@ -41,15 +41,38 @@ En root :
 | 
			
		||||
 | 
			
		||||
*4.* Installation de Misskey
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `su - misskey` Basculez vers l'utilisateur misskey.
 | 
			
		||||
2. `git clone -b master git://github.com/syuilo/misskey.git` Clonez la branche master du dépôt misskey.
 | 
			
		||||
3. `cd misskey` Accédez au dossier misskey.
 | 
			
		||||
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout sur le tag de la [version la plus récente](https://github.com/syuilo/misskey/releases/latest)
 | 
			
		||||
5. `npm install` Installez les dépendances de misskey.
 | 
			
		||||
1. Basculez vers l'utilisateur misskey.
 | 
			
		||||
 | 
			
		||||
	`su - misskey`
 | 
			
		||||
 | 
			
		||||
2. Clonez la branche master du dépôt misskey.
 | 
			
		||||
 | 
			
		||||
	`git clone -b master git://github.com/syuilo/misskey.git`
 | 
			
		||||
 | 
			
		||||
3. Accédez au dossier misskey.
 | 
			
		||||
 | 
			
		||||
	`cd misskey`
 | 
			
		||||
 | 
			
		||||
4. Checkout sur le tag de la [version la plus récente](https://github.com/syuilo/misskey/releases/latest)
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
 
 | 
			
		||||
5. Installez les dépendances de misskey.
 | 
			
		||||
 | 
			
		||||
	`npm install`
 | 
			
		||||
 | 
			
		||||
*5.* Création du fichier de configuration
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `cp .config/example.yml .config/default.yml` Copiez le fichier `.config/example.yml` et renommez-le`default.yml`.
 | 
			
		||||
1. Copiez le fichier `.config/example.yml` et renommez-le`default.yml`.
 | 
			
		||||
 | 
			
		||||
	`cp .config/example.yml .config/default.yml`
 | 
			
		||||
 | 
			
		||||
2. Editez le fichier `default.yml`
 | 
			
		||||
 | 
			
		||||
*6.* Construction de Misskey
 | 
			
		||||
@@ -77,37 +100,53 @@ Lancez tout simplement `NODE_ENV=production npm start`. Bonne chance et amusez-v
 | 
			
		||||
 | 
			
		||||
### Démarrage avec systemd
 | 
			
		||||
 | 
			
		||||
1. Créez un service systemd sur : `/etc/systemd/system/misskey.service`
 | 
			
		||||
1. Créez un service systemd sur
 | 
			
		||||
 | 
			
		||||
	`/etc/systemd/system/misskey.service`
 | 
			
		||||
 | 
			
		||||
2. Editez-le puis copiez et coller ceci dans le fichier :
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Misskey daemon
 | 
			
		||||
	```
 | 
			
		||||
	[Unit]
 | 
			
		||||
	Description=Misskey daemon
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=simple
 | 
			
		||||
User=misskey
 | 
			
		||||
ExecStart=/usr/bin/npm start
 | 
			
		||||
WorkingDirectory=/home/misskey/misskey
 | 
			
		||||
Environment="NODE_ENV=production"
 | 
			
		||||
TimeoutSec=60
 | 
			
		||||
StandardOutput=syslog
 | 
			
		||||
StandardError=syslog
 | 
			
		||||
SyslogIdentifier=misskey
 | 
			
		||||
Restart=always
 | 
			
		||||
	[Service]
 | 
			
		||||
	Type=simple
 | 
			
		||||
	User=misskey
 | 
			
		||||
	ExecStart=/usr/bin/npm start
 | 
			
		||||
	WorkingDirectory=/home/misskey/misskey
 | 
			
		||||
	Environment="NODE_ENV=production"
 | 
			
		||||
	TimeoutSec=60
 | 
			
		||||
	StandardOutput=syslog
 | 
			
		||||
	StandardError=syslog
 | 
			
		||||
	SyslogIdentifier=misskey
 | 
			
		||||
	Restart=always
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
```
 | 
			
		||||
	[Install]
 | 
			
		||||
	WantedBy=multi-user.target
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
3. `systemctl daemon-reload ; systemctl enable misskey` Redémarre systemd et active le service misskey.
 | 
			
		||||
4. `systemctl start misskey` Démarre le service misskey.
 | 
			
		||||
3. Redémarre systemd et active le service misskey.
 | 
			
		||||
 | 
			
		||||
	`systemctl daemon-reload ; systemctl enable misskey`
 | 
			
		||||
 | 
			
		||||
4. Démarre le service misskey.
 | 
			
		||||
 | 
			
		||||
	`systemctl start misskey`
 | 
			
		||||
 | 
			
		||||
Vous pouvez vérifier si le service a démarré en utilisant la commande `systemctl status misskey`.
 | 
			
		||||
 | 
			
		||||
### Méthode de mise à jour vers la plus récente version de Misskey
 | 
			
		||||
1. `git fetch`
 | 
			
		||||
2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
 | 
			
		||||
2.  
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
3. `npm install`
 | 
			
		||||
4. `NODE_ENV=production npm run build`
 | 
			
		||||
5. Consultez [ChangeLog](../CHANGELOG.md) pour les information de migration.
 | 
			
		||||
 
 | 
			
		||||
@@ -48,15 +48,38 @@ adduser --disabled-password --disabled-login misskey
 | 
			
		||||
 | 
			
		||||
*4.* Misskeyのインストール
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `su - misskey` misskeyユーザーを使用
 | 
			
		||||
2. `git clone -b master git://github.com/syuilo/misskey.git` masterブランチからMisskeyレポジトリをクローン
 | 
			
		||||
3. `cd misskey` misskeyディレクトリに移動
 | 
			
		||||
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
 | 
			
		||||
5. `npm install` Misskeyの依存パッケージをインストール
 | 
			
		||||
1. misskeyユーザーを使用
 | 
			
		||||
 | 
			
		||||
	`su - misskey`
 | 
			
		||||
 | 
			
		||||
2. masterブランチからMisskeyレポジトリをクローン
 | 
			
		||||
 | 
			
		||||
	`git clone -b master git://github.com/syuilo/misskey.git`
 | 
			
		||||
 | 
			
		||||
3. misskeyディレクトリに移動
 | 
			
		||||
 | 
			
		||||
	`cd misskey`
 | 
			
		||||
 | 
			
		||||
4. [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
5. Misskeyの依存パッケージをインストール
 | 
			
		||||
 | 
			
		||||
	`npm install`
 | 
			
		||||
 | 
			
		||||
*5.* 設定ファイルを作成する
 | 
			
		||||
----------------------------------------------------------------
 | 
			
		||||
1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。
 | 
			
		||||
1. `.config/example.yml`をコピーし名前を`default.yml`にする。
 | 
			
		||||
 | 
			
		||||
	`cp .config/example.yml .config/default.yml`
 | 
			
		||||
 | 
			
		||||
2. `default.yml` を編集する。
 | 
			
		||||
 | 
			
		||||
*6.* Misskeyのビルド
 | 
			
		||||
@@ -82,38 +105,56 @@ Debianをお使いであれば、`build-essential`パッケージをインスト
 | 
			
		||||
`NODE_ENV=production npm start`するだけです。GLHF!
 | 
			
		||||
 | 
			
		||||
### systemdを用いた起動
 | 
			
		||||
1. systemdサービスのファイルを作成: `/etc/systemd/system/misskey.service`
 | 
			
		||||
1. systemdサービスのファイルを作成
 | 
			
		||||
 | 
			
		||||
	`/etc/systemd/system/misskey.service`
 | 
			
		||||
 | 
			
		||||
2. エディタで開き、以下のコードを貼り付けて保存:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Misskey daemon
 | 
			
		||||
	```
 | 
			
		||||
	[Unit]
 | 
			
		||||
	Description=Misskey daemon
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=simple
 | 
			
		||||
User=misskey
 | 
			
		||||
ExecStart=/usr/bin/npm start
 | 
			
		||||
WorkingDirectory=/home/misskey/misskey
 | 
			
		||||
Environment="NODE_ENV=production"
 | 
			
		||||
TimeoutSec=60
 | 
			
		||||
StandardOutput=syslog
 | 
			
		||||
StandardError=syslog
 | 
			
		||||
SyslogIdentifier=misskey
 | 
			
		||||
Restart=always
 | 
			
		||||
	[Service]
 | 
			
		||||
	Type=simple
 | 
			
		||||
	User=misskey
 | 
			
		||||
	ExecStart=/usr/bin/npm start
 | 
			
		||||
	WorkingDirectory=/home/misskey/misskey
 | 
			
		||||
	Environment="NODE_ENV=production"
 | 
			
		||||
	TimeoutSec=60
 | 
			
		||||
	StandardOutput=syslog
 | 
			
		||||
	StandardError=syslog
 | 
			
		||||
	SyslogIdentifier=misskey
 | 
			
		||||
	Restart=always
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
```
 | 
			
		||||
CentOSで1024以下のポートを使用してMisskeyを使用する場合は`ExecStart=/usr/bin/sudo /usr/bin/npm start`に変更する必要があります。
 | 
			
		||||
	[Install]
 | 
			
		||||
	WantedBy=multi-user.target
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
3. `systemctl daemon-reload ; systemctl enable misskey` systemdを再読み込みしmisskeyサービスを有効化
 | 
			
		||||
4. `systemctl start misskey` misskeyサービスの起動
 | 
			
		||||
	CentOSで1024以下のポートを使用してMisskeyを使用する場合は`ExecStart=/usr/bin/sudo /usr/bin/npm start`に変更する必要があります。
 | 
			
		||||
 | 
			
		||||
3. systemdを再読み込みしmisskeyサービスを有効化
 | 
			
		||||
 | 
			
		||||
	`systemctl daemon-reload ; systemctl enable misskey`
 | 
			
		||||
 | 
			
		||||
4. misskeyサービスの起動
 | 
			
		||||
 | 
			
		||||
	`systemctl start misskey`
 | 
			
		||||
 | 
			
		||||
`systemctl status misskey`と入力すると、サービスの状態を調べることができます。
 | 
			
		||||
 | 
			
		||||
### Misskeyを最新バージョンにアップデートする方法:
 | 
			
		||||
1. `git fetch`
 | 
			
		||||
2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
 | 
			
		||||
2.  
 | 
			
		||||
 | 
			
		||||
	```bash
 | 
			
		||||
	git tag | grep '^10\.' | sort -V --reverse | \
 | 
			
		||||
	while read tag_name; do \
 | 
			
		||||
	if ! curl -s "https://api.github.com/repos/syuilo/misskey/releases/tags/$tag_name" \
 | 
			
		||||
	| grep -qE '"(draft|prerelease)": true'; \
 | 
			
		||||
	then git checkout $tag_name; break; fi ; done
 | 
			
		||||
	```
 | 
			
		||||
 | 
			
		||||
3. `npm install`
 | 
			
		||||
4. `NODE_ENV=production npm run build`
 | 
			
		||||
5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
 | 
			
		||||
 
 | 
			
		||||
@@ -877,7 +877,6 @@ desktop/views/components/post-form.vue:
 | 
			
		||||
  posting: "Posting"
 | 
			
		||||
  attach-media-from-local: "Attach media from your device"
 | 
			
		||||
  attach-media-from-drive: "Attach media from your Drive"
 | 
			
		||||
  attach-cancel: "Cancel attachment"
 | 
			
		||||
  insert-a-kao: "v('ω')v"
 | 
			
		||||
  create-poll: "Create a poll"
 | 
			
		||||
  text-remain: "{} characters remaining"
 | 
			
		||||
@@ -970,6 +969,10 @@ common/views/components/password-settings.vue:
 | 
			
		||||
  not-match: "The new passwords do not match"
 | 
			
		||||
  changed: "Password changed"
 | 
			
		||||
  failed: "Failed to change password"
 | 
			
		||||
common/views/components/post-form-attaches.vue:
 | 
			
		||||
  attach-cancel: "Remove Attachment"
 | 
			
		||||
  mark-as-sensitive: "Mark as 'sensitive'"
 | 
			
		||||
  unmark-as-sensitive: "Unmark as 'sensitive'"
 | 
			
		||||
desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "This post is private"
 | 
			
		||||
  deleted: "This post has been deleted"
 | 
			
		||||
 
 | 
			
		||||
@@ -513,8 +513,12 @@ common/views/components/user-menu.vue:
 | 
			
		||||
  mention: "メンション"
 | 
			
		||||
  mute: "ミュート"
 | 
			
		||||
  unmute: "ミュート解除"
 | 
			
		||||
  mute-confirm: "このユーザーをミュートしますか?"
 | 
			
		||||
  unmute-confirm: "このユーザーをミュート解除しますか?"
 | 
			
		||||
  block: "ブロック"
 | 
			
		||||
  unblock: "ブロック解除"
 | 
			
		||||
  block-confirm: "このユーザーをブロックしますか?"
 | 
			
		||||
  unblock-confirm: "このユーザーをブロック解除しますか?"
 | 
			
		||||
  push-to-list: "リストに追加"
 | 
			
		||||
  select-list: "リストを選択してください"
 | 
			
		||||
  report-abuse: "スパムを報告"
 | 
			
		||||
@@ -522,8 +526,12 @@ common/views/components/user-menu.vue:
 | 
			
		||||
  report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。"
 | 
			
		||||
  silence: "サイレンス"
 | 
			
		||||
  unsilence: "サイレンス解除"
 | 
			
		||||
  silence-confirm: "このユーザーをサイレンスしますか?"
 | 
			
		||||
  unsilence-confirm: "このユーザーをサイレンス解除しますか?"
 | 
			
		||||
  suspend: "凍結"
 | 
			
		||||
  unsuspend: "凍結解除"
 | 
			
		||||
  suspend-confirm: "このユーザーを凍結しますか?"
 | 
			
		||||
  unsuspend-confirm: "このユーザーを凍結解除しますか?"
 | 
			
		||||
 | 
			
		||||
common/views/components/poll.vue:
 | 
			
		||||
  vote-to: "「{}」に投票する"
 | 
			
		||||
@@ -967,7 +975,6 @@ desktop/views/components/post-form.vue:
 | 
			
		||||
  posting: "投稿中"
 | 
			
		||||
  attach-media-from-local: "PCからメディアを添付"
 | 
			
		||||
  attach-media-from-drive: "ドライブからメディアを添付"
 | 
			
		||||
  attach-cancel: "添付取り消し"
 | 
			
		||||
  insert-a-kao: "v('ω')v"
 | 
			
		||||
  create-poll: "アンケートを作成"
 | 
			
		||||
  text-remain: "残り{}文字"
 | 
			
		||||
@@ -1073,6 +1080,11 @@ common/views/components/password-settings.vue:
 | 
			
		||||
  changed: "パスワードを変更しました"
 | 
			
		||||
  failed: "パスワード変更に失敗しました"
 | 
			
		||||
 | 
			
		||||
common/views/components/post-form-attaches.vue:
 | 
			
		||||
  attach-cancel: "添付取り消し"
 | 
			
		||||
  mark-as-sensitive: "閲覧注意に設定"
 | 
			
		||||
  unmark-as-sensitive: "閲覧注意を解除"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は非公開です"
 | 
			
		||||
  deleted: "この投稿は削除されました"
 | 
			
		||||
@@ -1326,7 +1338,9 @@ admin/views/users.vue:
 | 
			
		||||
  unsuspend-confirm: "凍結を解除しますか?"
 | 
			
		||||
  unsuspended: "凍結を解除しました"
 | 
			
		||||
  make-silence: "サイレンス"
 | 
			
		||||
  silence-confirm: "サイレンスしますか?"
 | 
			
		||||
  unmake-silence: "サイレンスの解除"
 | 
			
		||||
  unsilence-confirm: "サイレンスを解除しますか?"
 | 
			
		||||
  verify: "公式アカウントにする"
 | 
			
		||||
  verify-confirm: "公式アカウントにしますか?"
 | 
			
		||||
  verified: "公式アカウントにしました"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										130
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								package.json
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"author": "syuilo <i@syuilo.com>",
 | 
			
		||||
	"version": "10.99.0",
 | 
			
		||||
	"version": "10.102.4",
 | 
			
		||||
	"codename": "nighthike",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
@@ -22,24 +22,27 @@
 | 
			
		||||
		"test": "gulp test",
 | 
			
		||||
		"format": "gulp format"
 | 
			
		||||
	},
 | 
			
		||||
	"resolutions": {
 | 
			
		||||
		"gulp-cssnano/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1",
 | 
			
		||||
		"video-thumbnail-generator/lodash": "^4.17.11"
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@fortawesome/fontawesome-svg-core": "1.2.15",
 | 
			
		||||
		"@fortawesome/free-brands-svg-icons": "5.7.2",
 | 
			
		||||
		"@fortawesome/free-regular-svg-icons": "5.7.2",
 | 
			
		||||
		"@fortawesome/free-solid-svg-icons": "5.7.2",
 | 
			
		||||
		"@fortawesome/vue-fontawesome": "0.1.5",
 | 
			
		||||
		"@fortawesome/fontawesome-svg-core": "1.2.19",
 | 
			
		||||
		"@fortawesome/free-brands-svg-icons": "5.9.0",
 | 
			
		||||
		"@fortawesome/free-regular-svg-icons": "5.9.0",
 | 
			
		||||
		"@fortawesome/free-solid-svg-icons": "5.9.0",
 | 
			
		||||
		"@fortawesome/vue-fontawesome": "0.1.6",
 | 
			
		||||
		"@koa/cors": "2.2.3",
 | 
			
		||||
		"@prezzemolo/rap": "0.1.2",
 | 
			
		||||
		"@prezzemolo/zip": "0.0.3",
 | 
			
		||||
		"@types/bcryptjs": "2.4.2",
 | 
			
		||||
		"@types/bull": "3.5.8",
 | 
			
		||||
		"@types/bull": "3.5.14",
 | 
			
		||||
		"@types/chai-http": "3.0.5",
 | 
			
		||||
		"@types/dateformat": "3.0.0",
 | 
			
		||||
		"@types/deep-equal": "1.0.1",
 | 
			
		||||
		"@types/double-ended-queue": "2.1.0",
 | 
			
		||||
		"@types/elasticsearch": "5.0.30",
 | 
			
		||||
		"@types/file-type": "10.6.0",
 | 
			
		||||
		"@types/gulp": "4.0.5",
 | 
			
		||||
		"@types/elasticsearch": "5.0.34",
 | 
			
		||||
		"@types/gulp": "4.0.6",
 | 
			
		||||
		"@types/gulp-mocha": "0.0.32",
 | 
			
		||||
		"@types/gulp-rename": "0.0.33",
 | 
			
		||||
		"@types/gulp-replace": "0.0.31",
 | 
			
		||||
@@ -47,65 +50,65 @@
 | 
			
		||||
		"@types/gulp-util": "3.0.34",
 | 
			
		||||
		"@types/is-root": "1.0.0",
 | 
			
		||||
		"@types/is-url": "1.2.28",
 | 
			
		||||
		"@types/js-yaml": "3.12.0",
 | 
			
		||||
		"@types/js-yaml": "3.12.1",
 | 
			
		||||
		"@types/jsdom": "12.2.3",
 | 
			
		||||
		"@types/katex": "0.10.1",
 | 
			
		||||
		"@types/koa": "2.0.48",
 | 
			
		||||
		"@types/koa-bodyparser": "5.0.2",
 | 
			
		||||
		"@types/koa-compress": "2.0.8",
 | 
			
		||||
		"@types/koa-compress": "2.0.9",
 | 
			
		||||
		"@types/koa-cors": "0.0.0",
 | 
			
		||||
		"@types/koa-favicon": "2.0.19",
 | 
			
		||||
		"@types/koa-logger": "3.1.1",
 | 
			
		||||
		"@types/koa-mount": "3.0.1",
 | 
			
		||||
		"@types/koa-multer": "1.0.0",
 | 
			
		||||
		"@types/koa-router": "7.0.40",
 | 
			
		||||
		"@types/koa-send": "4.1.1",
 | 
			
		||||
		"@types/koa-send": "4.1.2",
 | 
			
		||||
		"@types/koa-views": "2.0.3",
 | 
			
		||||
		"@types/koa__cors": "2.2.3",
 | 
			
		||||
		"@types/minio": "7.0.1",
 | 
			
		||||
		"@types/minio": "7.0.2",
 | 
			
		||||
		"@types/mkdirp": "0.5.2",
 | 
			
		||||
		"@types/mocha": "5.2.5",
 | 
			
		||||
		"@types/mongodb": "3.1.20",
 | 
			
		||||
		"@types/mocha": "5.2.7",
 | 
			
		||||
		"@types/mongodb": "3.1.28",
 | 
			
		||||
		"@types/node": "11.10.4",
 | 
			
		||||
		"@types/nodemailer": "4.6.6",
 | 
			
		||||
		"@types/nprogress": "0.0.29",
 | 
			
		||||
		"@types/nodemailer": "6.2.0",
 | 
			
		||||
		"@types/nprogress": "0.2.0",
 | 
			
		||||
		"@types/oauth": "0.9.1",
 | 
			
		||||
		"@types/parse5": "5.0.0",
 | 
			
		||||
		"@types/parsimmon": "1.10.0",
 | 
			
		||||
		"@types/portscanner": "2.1.0",
 | 
			
		||||
		"@types/pug": "2.0.4",
 | 
			
		||||
		"@types/qrcode": "1.3.0",
 | 
			
		||||
		"@types/qrcode": "1.3.3",
 | 
			
		||||
		"@types/ratelimiter": "2.1.28",
 | 
			
		||||
		"@types/redis": "2.8.10",
 | 
			
		||||
		"@types/redis": "2.8.13",
 | 
			
		||||
		"@types/rename": "1.0.1",
 | 
			
		||||
		"@types/request": "2.48.1",
 | 
			
		||||
		"@types/request-promise-native": "1.0.15",
 | 
			
		||||
		"@types/request-promise-native": "1.0.16",
 | 
			
		||||
		"@types/request-stats": "3.0.0",
 | 
			
		||||
		"@types/rimraf": "2.0.2",
 | 
			
		||||
		"@types/seedrandom": "2.4.27",
 | 
			
		||||
		"@types/sharp": "0.21.2",
 | 
			
		||||
		"@types/seedrandom": "2.4.28",
 | 
			
		||||
		"@types/sharp": "0.22.2",
 | 
			
		||||
		"@types/showdown": "1.9.2",
 | 
			
		||||
		"@types/speakeasy": "2.0.4",
 | 
			
		||||
		"@types/systeminformation": "3.23.1",
 | 
			
		||||
		"@types/tinycolor2": "1.4.1",
 | 
			
		||||
		"@types/tmp": "0.0.33",
 | 
			
		||||
		"@types/tinycolor2": "1.4.2",
 | 
			
		||||
		"@types/tmp": "0.1.0",
 | 
			
		||||
		"@types/uuid": "3.4.4",
 | 
			
		||||
		"@types/web-push": "3.3.0",
 | 
			
		||||
		"@types/webpack": "4.4.24",
 | 
			
		||||
		"@types/webpack": "4.4.32",
 | 
			
		||||
		"@types/webpack-stream": "3.2.10",
 | 
			
		||||
		"@types/websocket": "0.0.40",
 | 
			
		||||
		"@types/ws": "6.0.1",
 | 
			
		||||
		"animejs": "3.0.1",
 | 
			
		||||
		"apexcharts": "3.6.5",
 | 
			
		||||
		"apexcharts": "3.8.0",
 | 
			
		||||
		"autobind-decorator": "2.4.0",
 | 
			
		||||
		"autosize": "4.0.2",
 | 
			
		||||
		"autwh": "0.1.0",
 | 
			
		||||
		"bcryptjs": "2.4.3",
 | 
			
		||||
		"bootstrap-vue": "2.0.0-rc.13",
 | 
			
		||||
		"bull": "3.7.0",
 | 
			
		||||
		"cafy": "15.1.0",
 | 
			
		||||
		"bootstrap-vue": "2.0.0-rc.22",
 | 
			
		||||
		"bull": "3.10.0",
 | 
			
		||||
		"cafy": "15.1.1",
 | 
			
		||||
		"chai": "4.2.0",
 | 
			
		||||
		"chai-http": "4.2.1",
 | 
			
		||||
		"chai-http": "4.3.0",
 | 
			
		||||
		"chalk": "2.4.2",
 | 
			
		||||
		"commander": "2.20.0",
 | 
			
		||||
		"content-disposition": "0.5.3",
 | 
			
		||||
@@ -115,18 +118,18 @@
 | 
			
		||||
		"dateformat": "3.0.3",
 | 
			
		||||
		"deep-equal": "1.0.1",
 | 
			
		||||
		"deepcopy": "0.6.3",
 | 
			
		||||
		"diskusage": "1.0.0",
 | 
			
		||||
		"diskusage": "1.1.1",
 | 
			
		||||
		"double-ended-queue": "2.1.0-0",
 | 
			
		||||
		"elasticsearch": "15.4.1",
 | 
			
		||||
		"emojilib": "2.4.0",
 | 
			
		||||
		"escape-regexp": "0.0.1",
 | 
			
		||||
		"eslint": "5.15.1",
 | 
			
		||||
		"eslint": "5.16.0",
 | 
			
		||||
		"eslint-plugin-vue": "5.2.2",
 | 
			
		||||
		"eventemitter3": "3.1.0",
 | 
			
		||||
		"eventemitter3": "3.1.2",
 | 
			
		||||
		"feed": "2.0.4",
 | 
			
		||||
		"file-type": "10.10.0",
 | 
			
		||||
		"fuckadblock": "3.2.1",
 | 
			
		||||
		"gulp": "4.0.0",
 | 
			
		||||
		"gulp": "4.0.2",
 | 
			
		||||
		"gulp-cssnano": "2.1.3",
 | 
			
		||||
		"gulp-imagemin": "5.0.3",
 | 
			
		||||
		"gulp-mocha": "6.0.0",
 | 
			
		||||
@@ -135,20 +138,20 @@
 | 
			
		||||
		"gulp-sourcemaps": "2.6.5",
 | 
			
		||||
		"gulp-stylus": "2.7.0",
 | 
			
		||||
		"gulp-tslint": "8.1.4",
 | 
			
		||||
		"gulp-typescript": "5.0.0",
 | 
			
		||||
		"gulp-typescript": "5.0.1",
 | 
			
		||||
		"gulp-uglify": "3.0.2",
 | 
			
		||||
		"gulp-util": "3.0.8",
 | 
			
		||||
		"hard-source-webpack-plugin": "0.13.1",
 | 
			
		||||
		"html-minifier": "3.5.21",
 | 
			
		||||
		"html-minifier": "4.0.0",
 | 
			
		||||
		"http-signature": "1.2.0",
 | 
			
		||||
		"insert-text-at-cursor": "0.1.2",
 | 
			
		||||
		"is-root": "2.0.0",
 | 
			
		||||
		"is-svg": "4.0.0",
 | 
			
		||||
		"js-yaml": "3.13.0",
 | 
			
		||||
		"jsdom": "14.0.0",
 | 
			
		||||
		"insert-text-at-cursor": "0.2.0",
 | 
			
		||||
		"is-root": "2.1.0",
 | 
			
		||||
		"is-svg": "4.2.0",
 | 
			
		||||
		"js-yaml": "3.13.1",
 | 
			
		||||
		"jsdom": "15.1.1",
 | 
			
		||||
		"json5": "2.1.0",
 | 
			
		||||
		"json5-loader": "1.0.1",
 | 
			
		||||
		"katex": "0.10.1",
 | 
			
		||||
		"json5-loader": "2.0.0",
 | 
			
		||||
		"katex": "0.10.2",
 | 
			
		||||
		"koa": "2.7.0",
 | 
			
		||||
		"koa-bodyparser": "4.2.1",
 | 
			
		||||
		"koa-compress": "3.0.0",
 | 
			
		||||
@@ -164,15 +167,15 @@
 | 
			
		||||
		"langmap": "0.0.16",
 | 
			
		||||
		"loader-utils": "1.2.3",
 | 
			
		||||
		"lookup-dns-cache": "2.1.0",
 | 
			
		||||
		"minio": "7.0.5",
 | 
			
		||||
		"minio": "7.0.8",
 | 
			
		||||
		"mkdirp": "0.5.1",
 | 
			
		||||
		"mocha": "5.2.0",
 | 
			
		||||
		"moji": "0.5.1",
 | 
			
		||||
		"moment": "2.24.0",
 | 
			
		||||
		"mongodb": "3.2.2",
 | 
			
		||||
		"mongodb": "3.2.7",
 | 
			
		||||
		"monk": "6.0.6",
 | 
			
		||||
		"ms": "2.1.1",
 | 
			
		||||
		"nan": "2.12.1",
 | 
			
		||||
		"ms": "2.1.2",
 | 
			
		||||
		"nan": "2.14.0",
 | 
			
		||||
		"nested-property": "0.0.7",
 | 
			
		||||
		"nodemailer": "5.1.1",
 | 
			
		||||
		"nprogress": "0.2.0",
 | 
			
		||||
@@ -203,7 +206,7 @@
 | 
			
		||||
		"rndstr": "1.0.0",
 | 
			
		||||
		"s-age": "1.1.2",
 | 
			
		||||
		"seedrandom": "2.4.4",
 | 
			
		||||
		"sharp": "0.22.0",
 | 
			
		||||
		"sharp": "0.22.1",
 | 
			
		||||
		"showdown": "1.9.0",
 | 
			
		||||
		"showdown-highlightjs-extension": "0.1.2",
 | 
			
		||||
		"speakeasy": "2.0.0",
 | 
			
		||||
@@ -212,17 +215,17 @@
 | 
			
		||||
		"stylus": "0.54.5",
 | 
			
		||||
		"stylus-loader": "3.0.2",
 | 
			
		||||
		"summaly": "2.2.0",
 | 
			
		||||
		"systeminformation": "4.0.16",
 | 
			
		||||
		"systeminformation": "4.9.0",
 | 
			
		||||
		"syuilo-password-strength": "0.0.1",
 | 
			
		||||
		"terser-webpack-plugin": "1.2.3",
 | 
			
		||||
		"terser-webpack-plugin": "1.3.0",
 | 
			
		||||
		"textarea-caret": "3.1.0",
 | 
			
		||||
		"tinycolor2": "1.4.1",
 | 
			
		||||
		"tmp": "0.0.33",
 | 
			
		||||
		"tmp": "0.1.0",
 | 
			
		||||
		"ts-loader": "5.3.3",
 | 
			
		||||
		"ts-node": "8.0.3",
 | 
			
		||||
		"tslint": "5.13.1",
 | 
			
		||||
		"tslint": "5.17.0",
 | 
			
		||||
		"tslint-sonarts": "1.9.0",
 | 
			
		||||
		"typescript": "3.3.3333",
 | 
			
		||||
		"typescript": "3.5.1",
 | 
			
		||||
		"typescript-eslint-parser": "22.0.0",
 | 
			
		||||
		"uglify-es": "3.3.9",
 | 
			
		||||
		"url-loader": "1.1.2",
 | 
			
		||||
@@ -232,27 +235,26 @@
 | 
			
		||||
		"video-thumbnail-generator": "1.1.3",
 | 
			
		||||
		"vue": "2.6.10",
 | 
			
		||||
		"vue-color": "2.7.0",
 | 
			
		||||
		"vue-content-loading": "1.5.3",
 | 
			
		||||
		"vue-content-loading": "1.6.0",
 | 
			
		||||
		"vue-cropperjs": "3.0.0",
 | 
			
		||||
		"vue-i18n": "8.10.0",
 | 
			
		||||
		"vue-js-modal": "1.3.28",
 | 
			
		||||
		"vue-i18n": "8.11.2",
 | 
			
		||||
		"vue-js-modal": "1.3.31",
 | 
			
		||||
		"vue-json-pretty": "1.6.0",
 | 
			
		||||
		"vue-loader": "15.7.0",
 | 
			
		||||
		"vue-marquee-text-component": "1.1.1",
 | 
			
		||||
		"vue-prism-component": "1.1.1",
 | 
			
		||||
		"vue-router": "3.0.2",
 | 
			
		||||
		"vue-router": "3.0.6",
 | 
			
		||||
		"vue-sequential-entrance": "1.1.3",
 | 
			
		||||
		"vue-style-loader": "4.1.2",
 | 
			
		||||
		"vue-svg-inline-loader": "1.2.15",
 | 
			
		||||
		"vue-template-compiler": "2.6.10",
 | 
			
		||||
		"vuedraggable": "2.20.0",
 | 
			
		||||
		"vuedraggable": "2.21.0",
 | 
			
		||||
		"vuewordcloud": "18.7.11",
 | 
			
		||||
		"vuex": "3.1.0",
 | 
			
		||||
		"vuex": "3.1.1",
 | 
			
		||||
		"vuex-persistedstate": "2.5.4",
 | 
			
		||||
		"web-push": "3.3.3",
 | 
			
		||||
		"webfinger.js": "2.7.0",
 | 
			
		||||
		"webpack": "4.28.4",
 | 
			
		||||
		"webpack-cli": "3.2.3",
 | 
			
		||||
		"web-push": "3.3.5",
 | 
			
		||||
		"webpack": "4.33.0",
 | 
			
		||||
		"webpack-cli": "3.3.3",
 | 
			
		||||
		"websocket": "1.0.28",
 | 
			
		||||
		"ws": "6.2.1",
 | 
			
		||||
		"xev": "2.0.1"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										65
									
								
								src/@types/webfinger.js.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										65
									
								
								src/@types/webfinger.js.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,65 +0,0 @@
 | 
			
		||||
declare module 'webfinger.js' {
 | 
			
		||||
	interface IWebFingerConstructorConfig {
 | 
			
		||||
		tls_only?: boolean;
 | 
			
		||||
		webfist_fallback?: boolean;
 | 
			
		||||
		uri_fallback?: boolean;
 | 
			
		||||
		request_timeout?: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type JRDProperties = { [type: string]: string };
 | 
			
		||||
 | 
			
		||||
	interface IJRDLink {
 | 
			
		||||
		rel: string;
 | 
			
		||||
		type?: string;
 | 
			
		||||
		href?: string;
 | 
			
		||||
		template?: string;
 | 
			
		||||
		titles?: { [lang: string]: string };
 | 
			
		||||
		properties?: JRDProperties;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface IJRD {
 | 
			
		||||
		subject?: string;
 | 
			
		||||
		expires?: Date;
 | 
			
		||||
		aliases?: string[];
 | 
			
		||||
		properties?: JRDProperties;
 | 
			
		||||
		links?: IJRDLink[];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface IIDXLinks {
 | 
			
		||||
		'avatar': IJRDLink[];
 | 
			
		||||
		'remotestorage': IJRDLink[];
 | 
			
		||||
		'blog': IJRDLink[];
 | 
			
		||||
		'vcard': IJRDLink[];
 | 
			
		||||
		'updates': IJRDLink[];
 | 
			
		||||
		'share': IJRDLink[];
 | 
			
		||||
		'profile': IJRDLink[];
 | 
			
		||||
		'webfist': IJRDLink[];
 | 
			
		||||
		'camlistore': IJRDLink[];
 | 
			
		||||
		[type: string]: IJRDLink[];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface IIDXProperties {
 | 
			
		||||
		'name': string;
 | 
			
		||||
		[type: string]: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface IIDX {
 | 
			
		||||
		links: IIDXLinks;
 | 
			
		||||
		properties: IIDXProperties;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface ILookupCallbackResult {
 | 
			
		||||
		object: IJRD;
 | 
			
		||||
		json: string;
 | 
			
		||||
		idx: IIDX;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type LookupCallback = (err: Error | string, result?: ILookupCallbackResult) => void;
 | 
			
		||||
 | 
			
		||||
	export class WebFinger {
 | 
			
		||||
		constructor(config?: IWebFingerConstructorConfig);
 | 
			
		||||
 | 
			
		||||
		public lookup(address: string, cb: LookupCallback): NodeJS.Timeout;
 | 
			
		||||
		public lookupLink(address: string, rel: string, cb: IJRDLink): void;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -38,7 +38,7 @@
 | 
			
		||||
				<div class="kidvdlkg" v-for="file in files">
 | 
			
		||||
					<div @click="file._open = !file._open">
 | 
			
		||||
						<div>
 | 
			
		||||
							<div class="thumbnail" :style="thumbnail(file)"></div>
 | 
			
		||||
							<x-file-thumbnail class="thumbnail" :file="file" fit="contain" @click="showFileMenu(file)"/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<header>
 | 
			
		||||
@@ -75,10 +75,15 @@ import Vue from 'vue';
 | 
			
		||||
import i18n from '../../i18n';
 | 
			
		||||
import { faCloud, faTerminal, faSearch } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import XFileThumbnail from '../../common/views/components/drive-file-thumbnail.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('admin/views/drive.vue'),
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		XFileThumbnail
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			file: null,
 | 
			
		||||
@@ -151,13 +156,6 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		thumbnail(file: any): any {
 | 
			
		||||
			return {
 | 
			
		||||
				'background-color': file.properties.avgColor && file.properties.avgColor.length == 3 ? `rgb(${file.properties.avgColor.join(',')})` : 'transparent',
 | 
			
		||||
				'background-image': `url(${file.thumbnailUrl})`
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async del(file: any) {
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				await this.$root.api('drive/files/delete', { fileId: file.id });
 | 
			
		||||
@@ -179,9 +177,9 @@ export default Vue.extend({
 | 
			
		||||
			this.$root.api('drive/files/update', {
 | 
			
		||||
				fileId: file.id,
 | 
			
		||||
				isSensitive: !file.isSensitive
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				file.isSensitive = !file.isSensitive;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async show() {
 | 
			
		||||
@@ -244,7 +242,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
		> div:nth-child(1)
 | 
			
		||||
			> .thumbnail
 | 
			
		||||
				display block
 | 
			
		||||
				display flex
 | 
			
		||||
				width 64px
 | 
			
		||||
				height 64px
 | 
			
		||||
				background-size cover
 | 
			
		||||
 
 | 
			
		||||
@@ -130,7 +130,7 @@
 | 
			
		||||
					<span>{{ $t('status') }}</span>
 | 
			
		||||
				</header>
 | 
			
		||||
				<div v-for="instance in instances" :style="{ opacity: instance.isNotResponding ? 0.5 : 1 }">
 | 
			
		||||
					<a @click.prevent="showInstance(instance.host)" target="_blank" :href="`https://${instance.host}`" :style="{ textDecoration: instance.isMarkedAsClosed ? 'line-through' : 'none' }">{{ instance.host }}</a>
 | 
			
		||||
					<a @click.prevent="showInstance(instance.host)" rel="nofollow noopener" target="_blank" :href="`https://${instance.host}`" :style="{ textDecoration: instance.isMarkedAsClosed ? 'line-through' : 'none' }">{{ instance.host }}</a>
 | 
			
		||||
					<span>{{ instance.notesCount | number }}</span>
 | 
			
		||||
					<span>{{ instance.usersCount | number }}</span>
 | 
			
		||||
					<span>{{ instance.followingCount | number }}</span>
 | 
			
		||||
 
 | 
			
		||||
@@ -232,6 +232,8 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async silenceUser() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t('silence-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				await this.$root.api('admin/silence-user', { userId: this.user._id });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
@@ -251,6 +253,8 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async unsilenceUser() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t('unsilence-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				await this.$root.api('admin/unsilence-user', { userId: this.user._id });
 | 
			
		||||
				this.$root.dialog({
 | 
			
		||||
 
 | 
			
		||||
@@ -183,9 +183,6 @@ export default Vue.extend({
 | 
			
		||||
	height 100%
 | 
			
		||||
 | 
			
		||||
	&.splash
 | 
			
		||||
		&, *
 | 
			
		||||
			pointer-events none !important
 | 
			
		||||
 | 
			
		||||
		> .main
 | 
			
		||||
			min-width 0
 | 
			
		||||
			width initial
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<a class="a" :href="repositoryUrl" target="_blank" title="View source on GitHub">
 | 
			
		||||
<a class="a" :href="repositoryUrl" rel="noopener" target="_blank" title="View source on GitHub">
 | 
			
		||||
	<svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="aria-hidden">
 | 
			
		||||
		<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
 | 
			
		||||
		<path class="octo-arm" d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor"></path>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<a class="zxrjzpcj" :href="url" :class="service" target="_blank">
 | 
			
		||||
<a class="zxrjzpcj" :href="url" :class="service" rel="noopener" target="_blank">
 | 
			
		||||
	<fa :icon="icon" size="lg" fixed-width /><span>{{ text }}</span>
 | 
			
		||||
</a>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
			<div class="content" v-if="!message.isDeleted">
 | 
			
		||||
				<mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
 | 
			
		||||
				<div class="file" v-if="message.file">
 | 
			
		||||
					<a :href="message.file.url" target="_blank" :title="message.file.name">
 | 
			
		||||
					<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
 | 
			
		||||
						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"
 | 
			
		||||
							:style="{ backgroundColor: message.file.properties.avgColor && message.file.properties.avgColor.length == 3 ? `rgb(${message.file.properties.avgColor.join(',')})` : 'transparent' }"/>
 | 
			
		||||
						<p v-else>{{ message.file.name }}</p>
 | 
			
		||||
 
 | 
			
		||||
@@ -270,17 +270,13 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.mk-messaging-room
 | 
			
		||||
	display flex
 | 
			
		||||
	flex 1
 | 
			
		||||
	flex-direction column
 | 
			
		||||
	height 100%
 | 
			
		||||
	background var(--messagingRoomBg)
 | 
			
		||||
 | 
			
		||||
	> .body
 | 
			
		||||
		width 100%
 | 
			
		||||
		max-width 600px
 | 
			
		||||
		margin 0 auto
 | 
			
		||||
		flex 1
 | 
			
		||||
		min-height calc(100% - 103px)
 | 
			
		||||
 | 
			
		||||
		> .init,
 | 
			
		||||
		> .empty
 | 
			
		||||
 
 | 
			
		||||
@@ -174,6 +174,7 @@ export default Vue.component('misskey-flavored-markdown', {
 | 
			
		||||
						key: Math.random(),
 | 
			
		||||
						props: {
 | 
			
		||||
							url: token.node.props.url,
 | 
			
		||||
							rel: 'nofollow noopener',
 | 
			
		||||
							target: '_blank'
 | 
			
		||||
						},
 | 
			
		||||
						attrs: {
 | 
			
		||||
@@ -187,6 +188,7 @@ export default Vue.component('misskey-flavored-markdown', {
 | 
			
		||||
						attrs: {
 | 
			
		||||
							class: 'link',
 | 
			
		||||
							href: token.node.props.url,
 | 
			
		||||
							rel: 'nofollow noopener',
 | 
			
		||||
							target: '_blank',
 | 
			
		||||
							title: token.node.props.url,
 | 
			
		||||
							style: 'color:var(--mfmLink);'
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@
 | 
			
		||||
<span class="mk-nav">
 | 
			
		||||
	<a :href="aboutUrl">{{ $t('about') }}</a>
 | 
			
		||||
	<i>・</i>
 | 
			
		||||
	<a :href="repositoryUrl">{{ $t('repository') }}</a>
 | 
			
		||||
	<a :href="repositoryUrl" rel="noopener" target="_blank">{{ $t('repository') }}</a>
 | 
			
		||||
	<i>・</i>
 | 
			
		||||
	<a :href="feedbackUrl" target="_blank">{{ $t('feedback') }}</a>
 | 
			
		||||
	<a :href="feedbackUrl" rel="noopener" target="_blank">{{ $t('feedback') }}</a>
 | 
			
		||||
	<i>・</i>
 | 
			
		||||
	<a href="/dev">{{ $t('develop') }}</a>
 | 
			
		||||
</span>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										140
									
								
								src/client/app/common/views/components/post-form-attaches.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/client/app/common/views/components/post-form-attaches.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="skeikyzd" v-show="files.length != 0">
 | 
			
		||||
	<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 | 
			
		||||
		<div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)">
 | 
			
		||||
			<x-file-thumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/>
 | 
			
		||||
			<img class="remove" @click.stop="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/>
 | 
			
		||||
			<div class="sensitive" v-if="file.isSensitive">
 | 
			
		||||
				<fa class="icon" :icon="faExclamationTriangle"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</x-draggable>
 | 
			
		||||
	<p class="remain">{{ 4 - files.length }}/4</p>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import * as XDraggable from 'vuedraggable';
 | 
			
		||||
import XMenu from '../../../common/views/components/menu.vue';
 | 
			
		||||
import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import XFileThumbnail from './drive-file-thumbnail.vue'
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('common/views/components/post-form-attaches.vue'),
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		XDraggable,
 | 
			
		||||
		XFileThumbnail
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		files: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		detachMediaFn: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			faExclamationTriangle
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		detachMedia(id) {
 | 
			
		||||
			if (this.detachMediaFn) this.detachMediaFn(id)
 | 
			
		||||
			else if (this.$parent.detachMedia) this.$parent.detachMedia(id)
 | 
			
		||||
		},
 | 
			
		||||
		toggleSensitive(file) {
 | 
			
		||||
			this.$root.api('drive/files/update', {
 | 
			
		||||
				fileId: file.id,
 | 
			
		||||
				isSensitive: !file.isSensitive
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				file.isSensitive = !file.isSensitive;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
		showFileMenu(file, ev: MouseEvent) {
 | 
			
		||||
			this.$root.new(XMenu, {
 | 
			
		||||
				items: [{
 | 
			
		||||
					type: 'item',
 | 
			
		||||
					text: file.isSensitive ? this.$t('unmark-as-sensitive') : this.$t('mark-as-sensitive'),
 | 
			
		||||
					icon: file.isSensitive ? faEyeSlash : faEye,
 | 
			
		||||
					action: () => { this.toggleSensitive(file) }
 | 
			
		||||
				}, {
 | 
			
		||||
					type: 'item',
 | 
			
		||||
					text: this.$t('attach-cancel'),
 | 
			
		||||
					icon: faTimesCircle,
 | 
			
		||||
					action: () => { this.detachMedia(file.id) }
 | 
			
		||||
				}],
 | 
			
		||||
				source: ev.currentTarget || ev.target
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.skeikyzd
 | 
			
		||||
	padding 4px
 | 
			
		||||
 | 
			
		||||
	> .files
 | 
			
		||||
		display flex
 | 
			
		||||
		flex-wrap wrap
 | 
			
		||||
 | 
			
		||||
		> div
 | 
			
		||||
			width 64px
 | 
			
		||||
			height 64px
 | 
			
		||||
			margin 4px
 | 
			
		||||
			cursor move
 | 
			
		||||
 | 
			
		||||
			&:hover > .remove
 | 
			
		||||
				display block
 | 
			
		||||
 | 
			
		||||
			> .thumbnail
 | 
			
		||||
				width 100%
 | 
			
		||||
				height 100%
 | 
			
		||||
				z-index 1
 | 
			
		||||
				color var(--text)
 | 
			
		||||
				background-color: rgba(128, 128, 128, 0.3)
 | 
			
		||||
 | 
			
		||||
			> .remove
 | 
			
		||||
				display none
 | 
			
		||||
				position absolute
 | 
			
		||||
				top -6px
 | 
			
		||||
				right -6px
 | 
			
		||||
				width 16px
 | 
			
		||||
				height 16px
 | 
			
		||||
				cursor pointer
 | 
			
		||||
				z-index 1000
 | 
			
		||||
 | 
			
		||||
			> .sensitive
 | 
			
		||||
				display flex
 | 
			
		||||
				position absolute
 | 
			
		||||
				width 64px
 | 
			
		||||
				height 64px
 | 
			
		||||
				top 0
 | 
			
		||||
				left 0
 | 
			
		||||
				z-index 2
 | 
			
		||||
				background rgba(17, 17, 17, .7)
 | 
			
		||||
				color #fff
 | 
			
		||||
 | 
			
		||||
				> .icon
 | 
			
		||||
					margin auto
 | 
			
		||||
 | 
			
		||||
	> .remain
 | 
			
		||||
		display block
 | 
			
		||||
		position absolute
 | 
			
		||||
		top 8px
 | 
			
		||||
		right 8px
 | 
			
		||||
		margin 0
 | 
			
		||||
		padding 0
 | 
			
		||||
		color var(--primaryAlpha04)
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
	</template>
 | 
			
		||||
	<div v-if="data && !$store.state.i.twoFactorEnabled">
 | 
			
		||||
		<ol>
 | 
			
		||||
			<li>{{ $t('authenticator') }}<a href="https://support.google.com/accounts/answer/1066447" target="_blank">{{ $t('howtoinstall') }}</a></li>
 | 
			
		||||
			<li>{{ $t('authenticator') }}<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank">{{ $t('howtoinstall') }}</a></li>
 | 
			
		||||
			<li>{{ $t('scan') }}<br><img :src="data.qr"></li>
 | 
			
		||||
			<li>{{ $t('done') }}<br>
 | 
			
		||||
				<ui-input v-model="token">{{ $t('token') }}</ui-input>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,21 +4,21 @@
 | 
			
		||||
 | 
			
		||||
	<section v-if="enableTwitterIntegration">
 | 
			
		||||
		<header><fa :icon="['fab', 'twitter']"/> Twitter</header>
 | 
			
		||||
		<p v-if="$store.state.i.twitter">{{ $t('connected-to') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
 | 
			
		||||
		<p v-if="$store.state.i.twitter">{{ $t('connected-to') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
 | 
			
		||||
		<ui-button v-if="$store.state.i.twitter" @click="disconnectTwitter">{{ $t('disconnect') }}</ui-button>
 | 
			
		||||
		<ui-button v-else @click="connectTwitter">{{ $t('connect') }}</ui-button>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section v-if="enableDiscordIntegration">
 | 
			
		||||
		<header><fa :icon="['fab', 'discord']"/> Discord</header>
 | 
			
		||||
		<p v-if="$store.state.i.discord">{{ $t('connected-to') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p>
 | 
			
		||||
		<p v-if="$store.state.i.discord">{{ $t('connected-to') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p>
 | 
			
		||||
		<ui-button v-if="$store.state.i.discord" @click="disconnectDiscord">{{ $t('disconnect') }}</ui-button>
 | 
			
		||||
		<ui-button v-else @click="connectDiscord">{{ $t('connect') }}</ui-button>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	<section v-if="enableGithubIntegration">
 | 
			
		||||
		<header><fa :icon="['fab', 'github']"/> GitHub</header>
 | 
			
		||||
		<p v-if="$store.state.i.github">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
 | 
			
		||||
		<p v-if="$store.state.i.github">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" rel="nofollow noopener" target="_blank">@{{ $store.state.i.github.login }}</a></p>
 | 
			
		||||
		<ui-button v-if="$store.state.i.github" @click="disconnectGithub">{{ $t('disconnect') }}</ui-button>
 | 
			
		||||
		<ui-button v-else @click="connectGithub">{{ $t('connect') }}</ui-button>
 | 
			
		||||
	</section>
 | 
			
		||||
 
 | 
			
		||||
@@ -290,12 +290,17 @@ export default Vue.extend({
 | 
			
		||||
				this.exportTarget == 'mute' ? 'i/export-mute' :
 | 
			
		||||
				this.exportTarget == 'blocking' ? 'i/export-blocking' :
 | 
			
		||||
				this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
 | 
			
		||||
				null, {});
 | 
			
		||||
 | 
			
		||||
				null, {}).then(() => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'info',
 | 
			
		||||
						text: this.$t('export-requested')
 | 
			
		||||
					});
 | 
			
		||||
				}).catch((e: any) => {
 | 
			
		||||
					this.$root.dialog({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: e.message
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		doImport() {
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
			</ui-select>
 | 
			
		||||
		</label>
 | 
			
		||||
 | 
			
		||||
		<a href="https://assets.msky.cafe/theme/list" target="_blank">{{ $t('find-more-theme') }}</a>
 | 
			
		||||
		<a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank">{{ $t('find-more-theme') }}</a>
 | 
			
		||||
 | 
			
		||||
		<details class="creator">
 | 
			
		||||
			<summary><fa icon="palette"/> {{ $t('create-a-theme') }}</summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
	</blockquote>
 | 
			
		||||
</div>
 | 
			
		||||
<div v-else class="mk-url-preview">
 | 
			
		||||
	<a :class="{ mini: narrow, compact }" :href="url" target="_blank" :title="url" v-if="!fetching">
 | 
			
		||||
	<a :class="{ mini: narrow, compact }" :href="url" rel="nofollow noopener" target="_blank" :title="url" v-if="!fetching">
 | 
			
		||||
		<div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`">
 | 
			
		||||
			<button v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$t('enable-player')"><fa :icon="['far', 'play-circle']"/></button>
 | 
			
		||||
		</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<a class="mk-url" :href="url" :target="target">
 | 
			
		||||
<a class="mk-url" :href="url" :rel="rel" :target="target">
 | 
			
		||||
	<span class="schema">{{ schema }}//</span>
 | 
			
		||||
	<span class="hostname">{{ hostname }}</span>
 | 
			
		||||
	<span class="port" v-if="port != ''">:{{ port }}</span>
 | 
			
		||||
@@ -15,7 +15,7 @@ import Vue from 'vue';
 | 
			
		||||
import { toUnicode as decodePunycode } from 'punycode';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: ['url', 'target'],
 | 
			
		||||
	props: ['url', 'rel', 'target'],
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			schema: null,
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
		<div class="no-users" v-if="inited && us.length == 0">
 | 
			
		||||
			<p>{{ $t('no-users') }}</p>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="user" v-for="user in us">
 | 
			
		||||
		<div class="user" v-for="user in us" :key="user.id">
 | 
			
		||||
			<mk-avatar class="avatar" :user="user"/>
 | 
			
		||||
			<div class="body" v-if="!iconOnly">
 | 
			
		||||
				<div class="name">
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
				<div class="description" v-if="user.description" :title="user.description">
 | 
			
		||||
					<mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :should-break="false" :plain-text="true"/>
 | 
			
		||||
				</div>
 | 
			
		||||
				<mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<button class="more" :class="{ fetching: fetchingMoreUsers }" v-if="cursor != null" @click="fetchMoreUsers()" :disabled="fetchingMoreUsers">
 | 
			
		||||
@@ -160,6 +161,12 @@ export default Vue.extend({
 | 
			
		||||
				text-overflow ellipsis
 | 
			
		||||
				opacity 0.7
 | 
			
		||||
				font-size 14px
 | 
			
		||||
				padding-right 40px
 | 
			
		||||
 | 
			
		||||
			> .follow-button
 | 
			
		||||
				position absolute
 | 
			
		||||
				top 8px
 | 
			
		||||
				right 0px
 | 
			
		||||
 | 
			
		||||
	> .more
 | 
			
		||||
		display block
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
 | 
			
		||||
import { faExclamationCircle, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
 | 
			
		||||
@@ -27,7 +26,10 @@ export default Vue.extend({
 | 
			
		||||
			icon: ['fas', 'list'],
 | 
			
		||||
			text: this.$t('push-to-list'),
 | 
			
		||||
			action: this.pushList
 | 
			
		||||
		}, null, {
 | 
			
		||||
		}] as any;
 | 
			
		||||
		
 | 
			
		||||
		if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) {
 | 
			
		||||
			menu = menu.concat([null, {
 | 
			
		||||
				icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'],
 | 
			
		||||
				text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'),
 | 
			
		||||
				action: this.toggleMute
 | 
			
		||||
@@ -39,7 +41,8 @@ export default Vue.extend({
 | 
			
		||||
				icon: faExclamationCircle,
 | 
			
		||||
				text: this.$t('report-abuse'),
 | 
			
		||||
				action: this.reportAbuse
 | 
			
		||||
		}];
 | 
			
		||||
			}]);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.$store.getters.isSignedIn && (this.$store.state.i.isAdmin || this.$store.state.i.isModerator)) {
 | 
			
		||||
			menu = menu.concat([null, {
 | 
			
		||||
@@ -89,8 +92,10 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		toggleMute() {
 | 
			
		||||
		async toggleMute() {
 | 
			
		||||
			if (this.user.isMuted) {
 | 
			
		||||
				if (!await this.getConfirmed(this.$t('unmute-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('mute/delete', {
 | 
			
		||||
					userId: this.user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
@@ -102,6 +107,8 @@ export default Vue.extend({
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				if (!await this.getConfirmed(this.$t('mute-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('mute/create', {
 | 
			
		||||
					userId: this.user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
@@ -115,8 +122,10 @@ export default Vue.extend({
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		toggleBlock() {
 | 
			
		||||
		async toggleBlock() {
 | 
			
		||||
			if (this.user.isBlocking) {
 | 
			
		||||
				if (!await this.getConfirmed(this.$t('unblock-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('blocking/delete', {
 | 
			
		||||
					userId: this.user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
@@ -128,6 +137,8 @@ export default Vue.extend({
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				if (!await this.getConfirmed(this.$t('block-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
				this.$root.api('blocking/create', {
 | 
			
		||||
					userId: this.user.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
@@ -164,7 +175,9 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		toggleSilence() {
 | 
			
		||||
		async toggleSilence() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t(this.user.isSilenced ? 'unsilence-confirm' : 'silence-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			this.$root.api(this.user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
 | 
			
		||||
				userId: this.user.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
@@ -181,7 +194,9 @@ export default Vue.extend({
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		toggleSuspend() {
 | 
			
		||||
		async toggleSuspend() {
 | 
			
		||||
			if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspend-confirm' : 'suspend-confirm'))) return;
 | 
			
		||||
 | 
			
		||||
			this.$root.api(this.user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
 | 
			
		||||
				userId: this.user.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
@@ -196,7 +211,18 @@ export default Vue.extend({
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async getConfirmed(text: string): Promise<Boolean> {
 | 
			
		||||
			const confirm = await this.$root.dialog({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				showCancelButton: true,
 | 
			
		||||
				title: 'confirm',
 | 
			
		||||
				text,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return !confirm.canceled;
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
		<div class="is-remote" v-if="note.user.host != null">
 | 
			
		||||
			<details>
 | 
			
		||||
				<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-post') }}</summary>
 | 
			
		||||
				<a :href="note.url || note.uri" target="_blank">{{ $t('@.view-on-remote') }}</a>
 | 
			
		||||
				<a :href="note.url || note.uri" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a>
 | 
			
		||||
			</details>
 | 
			
		||||
		</div>
 | 
			
		||||
		<mk-note :note="note" :detail="true" :key="note.id"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -157,6 +157,7 @@ export default Vue.extend({
 | 
			
		||||
				// オーバーフローしたら古い投稿は捨てる
 | 
			
		||||
				if (this.notes.length >= displayLimit) {
 | 
			
		||||
					this.notes = this.notes.slice(0, displayLimit);
 | 
			
		||||
					this.cursor = this.notes[this.notes.length - 1].id
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				this.queue.push(note);
 | 
			
		||||
@@ -165,6 +166,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
		append(note) {
 | 
			
		||||
			this.notes.push(note);
 | 
			
		||||
			this.cursor = this.notes[this.notes.length - 1].id
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		releaseQueue() {
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ export default Vue.extend({
 | 
			
		||||
			this.makePromise = cursor => this.$root.api('users/notes', {
 | 
			
		||||
				userId: this.user.id,
 | 
			
		||||
				limit: fetchLimit + 1,
 | 
			
		||||
				untilId: cursor ? cursor : undefined,
 | 
			
		||||
				untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365,
 | 
			
		||||
				withFiles: this.withFiles,
 | 
			
		||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
			
		||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
			
		||||
@@ -95,7 +95,7 @@ export default Vue.extend({
 | 
			
		||||
					notes.pop();
 | 
			
		||||
					return {
 | 
			
		||||
						notes: notes,
 | 
			
		||||
						cursor: notes[notes.length - 1].id
 | 
			
		||||
						cursor: new Date(notes[notes.length - 1].createdAt).getTime()
 | 
			
		||||
					};
 | 
			
		||||
				} else {
 | 
			
		||||
					return {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
		<div class="is-remote" v-if="user.host != null">
 | 
			
		||||
			<details>
 | 
			
		||||
				<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary>
 | 
			
		||||
				<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a>
 | 
			
		||||
				<a :href="user.url || user.uri" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a>
 | 
			
		||||
			</details>
 | 
			
		||||
		</div>
 | 
			
		||||
		<header :style="bannerStyle">
 | 
			
		||||
 
 | 
			
		||||
@@ -21,14 +21,7 @@
 | 
			
		||||
					<fa :icon="['far', 'laugh']"/>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="files" v-show="files.length != 0">
 | 
			
		||||
				<x-draggable :list="files" :options="{ animation: 150 }">
 | 
			
		||||
					<div v-for="file in files" :key="file.id">
 | 
			
		||||
						<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
 | 
			
		||||
						<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/>
 | 
			
		||||
					</div>
 | 
			
		||||
				</x-draggable>
 | 
			
		||||
			</div>
 | 
			
		||||
			<x-post-form-attaches class="files" :files="files" :detachMediaFn="detachMedia"/>
 | 
			
		||||
			<input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
 | 
			
		||||
			<mk-uploader ref="uploader" @uploaded="attachMedia"/>
 | 
			
		||||
			<footer>
 | 
			
		||||
@@ -45,7 +38,7 @@
 | 
			
		||||
import define from '../../../common/define-widget';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import insertTextAtCursor from 'insert-text-at-cursor';
 | 
			
		||||
import * as XDraggable from 'vuedraggable';
 | 
			
		||||
import XPostFormAttaches from '../components/post-form-attaches.vue';
 | 
			
		||||
 | 
			
		||||
export default define({
 | 
			
		||||
	name: 'post-form',
 | 
			
		||||
@@ -56,7 +49,7 @@ export default define({
 | 
			
		||||
	i18n: i18n('desktop/views/widgets/post-form.vue'),
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		XDraggable
 | 
			
		||||
		XPostFormAttaches
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
@@ -176,10 +169,22 @@ export default define({
 | 
			
		||||
		post() {
 | 
			
		||||
			this.posting = true;
 | 
			
		||||
 | 
			
		||||
			let visibility = 'public';
 | 
			
		||||
			let localOnly = false;
 | 
			
		||||
 | 
			
		||||
			const m = this.$store.state.settings.defaultNoteVisibility.match(/^local-(.+)/);
 | 
			
		||||
			if (m) {
 | 
			
		||||
				visibility = m[1];
 | 
			
		||||
				localOnly = true;
 | 
			
		||||
			} else {
 | 
			
		||||
				visibility = this.$store.state.settings.defaultNoteVisibility;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.$root.api('notes/create', {
 | 
			
		||||
				text: this.text == '' ? undefined : this.text,
 | 
			
		||||
				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
 | 
			
		||||
				visibility: this.$store.state.settings.defaultNoteVisibility
 | 
			
		||||
				visibility,
 | 
			
		||||
				localOnly,
 | 
			
		||||
			}).then(data => {
 | 
			
		||||
				this.clear();
 | 
			
		||||
			}).catch(err => {
 | 
			
		||||
@@ -237,38 +242,6 @@ export default define({
 | 
			
		||||
				& + .emoji
 | 
			
		||||
					opacity 0.7
 | 
			
		||||
 | 
			
		||||
	> .files
 | 
			
		||||
		> div
 | 
			
		||||
			padding 4px
 | 
			
		||||
 | 
			
		||||
			&:after
 | 
			
		||||
				content ""
 | 
			
		||||
				display block
 | 
			
		||||
				clear both
 | 
			
		||||
 | 
			
		||||
			> div
 | 
			
		||||
				float left
 | 
			
		||||
				border solid 4px transparent
 | 
			
		||||
				cursor move
 | 
			
		||||
 | 
			
		||||
				&:hover > .remove
 | 
			
		||||
					display block
 | 
			
		||||
 | 
			
		||||
				> .img
 | 
			
		||||
					width 64px
 | 
			
		||||
					height 64px
 | 
			
		||||
					background-size cover
 | 
			
		||||
					background-position center center
 | 
			
		||||
 | 
			
		||||
				> .remove
 | 
			
		||||
					display none
 | 
			
		||||
					position absolute
 | 
			
		||||
					top -6px
 | 
			
		||||
					right -6px
 | 
			
		||||
					width 16px
 | 
			
		||||
					height 16px
 | 
			
		||||
					cursor pointer
 | 
			
		||||
 | 
			
		||||
	> input[type=file]
 | 
			
		||||
		display none
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
		<div class="mkw-rss--body" :data-mobile="platform == 'mobile'">
 | 
			
		||||
			<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 | 
			
		||||
			<div class="feed" v-else>
 | 
			
		||||
				<a v-for="item in items" :href="item.link" target="_blank" :title="item.title">{{ item.title }}</a>
 | 
			
		||||
				<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</ui-container>
 | 
			
		||||
 
 | 
			
		||||
@@ -769,7 +769,6 @@ export default Vue.extend({
 | 
			
		||||
	> .mk-uploader
 | 
			
		||||
		height 100px
 | 
			
		||||
		padding 16px
 | 
			
		||||
		background #fff
 | 
			
		||||
 | 
			
		||||
	> input
 | 
			
		||||
		display none
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@
 | 
			
		||||
				</div>
 | 
			
		||||
				<mk-poll v-if="appearNote.poll" :note="appearNote"/>
 | 
			
		||||
				<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
 | 
			
		||||
				<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 | 
			
		||||
				<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 | 
			
		||||
				<div class="map" v-if="appearNote.geo" ref="map"></div>
 | 
			
		||||
				<div class="renote" v-if="appearNote.renote">
 | 
			
		||||
					<mk-note-preview :note="appearNote.renote"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@
 | 
			
		||||
						<mk-media-list :media-list="appearNote.files"/>
 | 
			
		||||
					</div>
 | 
			
		||||
					<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
 | 
			
		||||
					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> 位置情報</a>
 | 
			
		||||
					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> 位置情報</a>
 | 
			
		||||
					<div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
 | 
			
		||||
					<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="compact"/>
 | 
			
		||||
				</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -157,6 +157,7 @@ export default Vue.extend({
 | 
			
		||||
				// オーバーフローしたら古い投稿は捨てる
 | 
			
		||||
				if (this.notes.length >= displayLimit) {
 | 
			
		||||
					this.notes = this.notes.slice(0, displayLimit);
 | 
			
		||||
					this.cursor = this.notes[this.notes.length - 1].id
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				this.queue.push(note);
 | 
			
		||||
@@ -165,6 +166,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
		append(note) {
 | 
			
		||||
			this.notes.push(note);
 | 
			
		||||
			this.cursor = this.notes[this.notes.length - 1].id
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		releaseQueue() {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,15 +27,7 @@
 | 
			
		||||
			<button class="emoji" @click="emoji" ref="emoji">
 | 
			
		||||
				<fa :icon="['far', 'laugh']"/>
 | 
			
		||||
			</button>
 | 
			
		||||
			<div class="files" :class="{ with: poll }" v-show="files.length != 0">
 | 
			
		||||
				<x-draggable :list="files" :options="{ animation: 150 }">
 | 
			
		||||
					<div v-for="file in files" :key="file.id">
 | 
			
		||||
						<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
 | 
			
		||||
						<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/>
 | 
			
		||||
					</div>
 | 
			
		||||
				</x-draggable>
 | 
			
		||||
				<p class="remain">{{ 4 - files.length }}/4</p>
 | 
			
		||||
			</div>
 | 
			
		||||
			<x-post-form-attaches class="files" :files="files" :detachMediaFn="detachMedia"/>
 | 
			
		||||
			<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
@@ -65,7 +57,6 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import insertTextAtCursor from 'insert-text-at-cursor';
 | 
			
		||||
import * as XDraggable from 'vuedraggable';
 | 
			
		||||
import getFace from '../../../common/scripts/get-face';
 | 
			
		||||
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
 | 
			
		||||
import { parse } from '../../../../../mfm/parse';
 | 
			
		||||
@@ -74,13 +65,14 @@ import { erase, unique } from '../../../../../prelude/array';
 | 
			
		||||
import { length } from 'stringz';
 | 
			
		||||
import { toASCII } from 'punycode';
 | 
			
		||||
import extractMentions from '../../../../../misc/extract-mentions';
 | 
			
		||||
import XPostFormAttaches from '../../../common/views/components/post-form-attaches.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('desktop/views/components/post-form.vue'),
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		XDraggable,
 | 
			
		||||
		MkVisibilityChooser
 | 
			
		||||
		MkVisibilityChooser,
 | 
			
		||||
		XPostFormAttaches
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -640,17 +632,14 @@ export default Vue.extend({
 | 
			
		||||
						border solid 4px transparent
 | 
			
		||||
						cursor move
 | 
			
		||||
 | 
			
		||||
						&:hover > .remove
 | 
			
		||||
							display block
 | 
			
		||||
 | 
			
		||||
						> .img
 | 
			
		||||
							width 64px
 | 
			
		||||
							height 64px
 | 
			
		||||
							background-size cover
 | 
			
		||||
							background-position center center
 | 
			
		||||
							background-color: rgba(128, 128, 128, 0.3)
 | 
			
		||||
 | 
			
		||||
						> .remove
 | 
			
		||||
							display none
 | 
			
		||||
							position absolute
 | 
			
		||||
							top -6px
 | 
			
		||||
							right -6px
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
		<fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="is-remote" v-if="user.host != null" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
 | 
			
		||||
		<fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a>
 | 
			
		||||
		<fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="main">
 | 
			
		||||
		<x-header class="header" :user="user"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="header" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
 | 
			
		||||
	<div class="banner-container" :style="style">
 | 
			
		||||
		<div class="banner" ref="banner" :style="style" @click="onBannerClick"></div>
 | 
			
		||||
		<div class="banner" ref="banner" :style="style"></div>
 | 
			
		||||
		<div class="fade"></div>
 | 
			
		||||
		<div class="title">
 | 
			
		||||
			<p class="name">
 | 
			
		||||
@@ -105,14 +105,6 @@ export default Vue.extend({
 | 
			
		||||
			if (blur <= 10) banner.style.filter = `blur(${blur}px)`;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onBannerClick() {
 | 
			
		||||
			if (!this.$store.getters.isSignedIn || this.$store.state.i.id != this.user.id) return;
 | 
			
		||||
 | 
			
		||||
			this.$updateBanner().then(i => {
 | 
			
		||||
				this.user.bannerUrl = i.bannerUrl;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		menu() {
 | 
			
		||||
			this.$root.new(XUserMenu, {
 | 
			
		||||
				source: this.$refs.menu,
 | 
			
		||||
@@ -171,9 +163,6 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
			> .menu
 | 
			
		||||
				height 100%
 | 
			
		||||
				display block
 | 
			
		||||
				position absolute
 | 
			
		||||
				left -42px
 | 
			
		||||
				padding 0 14px
 | 
			
		||||
				color #fff
 | 
			
		||||
				text-shadow 0 0 8px #000
 | 
			
		||||
 
 | 
			
		||||
@@ -36,13 +36,13 @@ export default Vue.extend({
 | 
			
		||||
				includeReplies: this.mode == 'with-replies',
 | 
			
		||||
				includeMyRenotes: this.mode != 'my-posts',
 | 
			
		||||
				withFiles: this.mode == 'with-media',
 | 
			
		||||
				untilId: cursor ? cursor : undefined
 | 
			
		||||
				untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365
 | 
			
		||||
			}).then(notes => {
 | 
			
		||||
				if (notes.length == fetchLimit + 1) {
 | 
			
		||||
					notes.pop();
 | 
			
		||||
					return {
 | 
			
		||||
						notes: notes,
 | 
			
		||||
						cursor: notes[notes.length - 1].id
 | 
			
		||||
						cursor: new Date(notes[notes.length - 1].createdAt).getTime()
 | 
			
		||||
					};
 | 
			
		||||
				} else {
 | 
			
		||||
					return {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,9 @@
 | 
			
		||||
 | 
			
		||||
		<div class="mkw-polls--body">
 | 
			
		||||
			<div class="poll" v-if="!fetching && poll != null">
 | 
			
		||||
				<p v-if="poll.text"><router-link :to="poll | notePage">{{ poll.text }}</router-link></p>
 | 
			
		||||
				<p v-if="poll.text"><router-link :to="poll | notePage">
 | 
			
		||||
					<mfm :text="poll.text" :author="poll.user" :custom-emojis="poll.emojis"/>
 | 
			
		||||
				</router-link></p>
 | 
			
		||||
				<p v-if="!poll.text"><router-link :to="poll | notePage"><fa icon="link"/></router-link></p>
 | 
			
		||||
				<mk-poll :note="poll"/>
 | 
			
		||||
			</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@
 | 
			
		||||
</div>
 | 
			
		||||
<a class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else
 | 
			
		||||
	:href="video.url"
 | 
			
		||||
	rel="nofollow noopener"
 | 
			
		||||
	target="_blank"
 | 
			
		||||
	:style="imageStyle"
 | 
			
		||||
	:title="video.name"
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,7 @@
 | 
			
		||||
				</div>
 | 
			
		||||
				<mk-poll v-if="appearNote.poll" :note="appearNote"/>
 | 
			
		||||
				<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
 | 
			
		||||
				<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 | 
			
		||||
				<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 | 
			
		||||
				<div class="map" v-if="appearNote.geo" ref="map"></div>
 | 
			
		||||
				<div class="renote" v-if="appearNote.renote">
 | 
			
		||||
					<mk-note-preview :note="appearNote.renote"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@
 | 
			
		||||
					</div>
 | 
			
		||||
					<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
 | 
			
		||||
					<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true"/>
 | 
			
		||||
					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 | 
			
		||||
					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" rel="noopener" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 | 
			
		||||
					<div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<span class="app" v-if="appearNote.app && $store.state.settings.showVia">via <b>{{ appearNote.app.name }}</b></span>
 | 
			
		||||
 
 | 
			
		||||
@@ -151,6 +151,7 @@ export default Vue.extend({
 | 
			
		||||
				// オーバーフローしたら古い投稿は捨てる
 | 
			
		||||
				if (this.notes.length >= displayLimit) {
 | 
			
		||||
					this.notes = this.notes.slice(0, displayLimit);
 | 
			
		||||
					this.cursor = this.notes[this.notes.length - 1].id
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				this.queue.push(note);
 | 
			
		||||
@@ -159,6 +160,7 @@ export default Vue.extend({
 | 
			
		||||
 | 
			
		||||
		append(note) {
 | 
			
		||||
			this.notes.push(note);
 | 
			
		||||
			this.cursor = this.notes[this.notes.length - 1].id
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		releaseQueue() {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,13 +21,7 @@
 | 
			
		||||
			</div>
 | 
			
		||||
			<input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('annotations')" v-autocomplete="{ model: 'cw' }">
 | 
			
		||||
			<textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }"></textarea>
 | 
			
		||||
			<div class="attaches" v-show="files.length != 0">
 | 
			
		||||
				<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 | 
			
		||||
					<div class="file" v-for="file in files" :key="file.id">
 | 
			
		||||
						<div class="img" :style="`background-image: url(${file.thumbnailUrl})`" @click="detachMedia(file)"></div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</x-draggable>
 | 
			
		||||
			</div>
 | 
			
		||||
			<x-post-form-attaches class="attaches" :files="files"/>
 | 
			
		||||
			<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/>
 | 
			
		||||
			<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
 | 
			
		||||
			<footer>
 | 
			
		||||
@@ -57,7 +51,6 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../../i18n';
 | 
			
		||||
import insertTextAtCursor from 'insert-text-at-cursor';
 | 
			
		||||
import * as XDraggable from 'vuedraggable';
 | 
			
		||||
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
 | 
			
		||||
import getFace from '../../../common/scripts/get-face';
 | 
			
		||||
import { parse } from '../../../../../mfm/parse';
 | 
			
		||||
@@ -66,11 +59,12 @@ import { erase, unique } from '../../../../../prelude/array';
 | 
			
		||||
import { length } from 'stringz';
 | 
			
		||||
import { toASCII } from 'punycode';
 | 
			
		||||
import extractMentions from '../../../../../misc/extract-mentions';
 | 
			
		||||
import XPostFormAttaches from '../../../common/views/components/post-form-attaches.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n: i18n('mobile/views/components/post-form.vue'),
 | 
			
		||||
	components: {
 | 
			
		||||
		XDraggable
 | 
			
		||||
		XPostFormAttaches
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -264,8 +258,8 @@ export default Vue.extend({
 | 
			
		||||
			this.$emit('change-attached-files', this.files);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		detachMedia(file) {
 | 
			
		||||
			this.files = this.files.filter(x => x.id != file.id);
 | 
			
		||||
		detachMedia(id) {
 | 
			
		||||
			this.files = this.files.filter(x => x.id != id);
 | 
			
		||||
			this.$emit('change-attached-files', this.files);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
@@ -481,32 +475,6 @@ export default Vue.extend({
 | 
			
		||||
				min-width 100%
 | 
			
		||||
				min-height 80px
 | 
			
		||||
 | 
			
		||||
			> .attaches
 | 
			
		||||
 | 
			
		||||
				> .files
 | 
			
		||||
					display block
 | 
			
		||||
					margin 0
 | 
			
		||||
					padding 4px
 | 
			
		||||
					list-style none
 | 
			
		||||
 | 
			
		||||
					&:after
 | 
			
		||||
						content ""
 | 
			
		||||
						display block
 | 
			
		||||
						clear both
 | 
			
		||||
 | 
			
		||||
					> .file
 | 
			
		||||
						display block
 | 
			
		||||
						float left
 | 
			
		||||
						margin 0
 | 
			
		||||
						padding 0
 | 
			
		||||
						border solid 4px transparent
 | 
			
		||||
 | 
			
		||||
						> .img
 | 
			
		||||
							width 64px
 | 
			
		||||
							height 64px
 | 
			
		||||
							background-size cover
 | 
			
		||||
							background-position center center
 | 
			
		||||
 | 
			
		||||
			> .mk-uploader
 | 
			
		||||
				margin 8px 0 0 0
 | 
			
		||||
				padding 8px
 | 
			
		||||
 
 | 
			
		||||
@@ -21,13 +21,13 @@ export default Vue.extend({
 | 
			
		||||
				userId: this.user.id,
 | 
			
		||||
				limit: fetchLimit + 1,
 | 
			
		||||
				withFiles: this.withMedia,
 | 
			
		||||
				untilId: cursor ? cursor : undefined
 | 
			
		||||
				untilDate: cursor ? cursor : new Date().getTime() + 1000 * 86400 * 365
 | 
			
		||||
			}).then(notes => {
 | 
			
		||||
				if (notes.length == fetchLimit + 1) {
 | 
			
		||||
					notes.pop();
 | 
			
		||||
					return {
 | 
			
		||||
						notes: notes,
 | 
			
		||||
						cursor: notes[notes.length - 1].id
 | 
			
		||||
						cursor: new Date(notes[notes.length - 1].createdAt).getTime()
 | 
			
		||||
					};
 | 
			
		||||
				} else {
 | 
			
		||||
					return {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
	</template>
 | 
			
		||||
	<div class="wwtwuxyh" v-if="!fetching">
 | 
			
		||||
		<div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</p></div>
 | 
			
		||||
		<div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div>
 | 
			
		||||
		<div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" rel="nofollow noopener" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div>
 | 
			
		||||
		<header>
 | 
			
		||||
			<div class="banner" :style="style"></div>
 | 
			
		||||
			<div class="body">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/client/assets/thumbnail-not-available.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/client/assets/thumbnail-not-available.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 5.6 KiB  | 
@@ -42,6 +42,8 @@ export type Source = {
 | 
			
		||||
	accesslog?: string;
 | 
			
		||||
 | 
			
		||||
	clusterLimit?: number;
 | 
			
		||||
 | 
			
		||||
	outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -56,20 +56,12 @@ function cpuUsage() {
 | 
			
		||||
 | 
			
		||||
// MEMORY(excl buffer + cache) STAT
 | 
			
		||||
async function usedMem() {
 | 
			
		||||
	try {
 | 
			
		||||
	const data = await sysUtils.mem();
 | 
			
		||||
	return data.active;
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		throw error;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TOTAL MEMORY STAT
 | 
			
		||||
async function totalMem() {
 | 
			
		||||
	try {
 | 
			
		||||
	const data = await sysUtils.mem();
 | 
			
		||||
	return data.total;
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		throw error;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,4 +6,4 @@ block main
 | 
			
		||||
block footer
 | 
			
		||||
	p
 | 
			
		||||
		= i18n('docs.edit-this-page-on-github')
 | 
			
		||||
		a(href=src target="_blank")= i18n('docs.edit-this-page-on-github-link')
 | 
			
		||||
		a(href=src rel="noopener" target="_blank")= i18n('docs.edit-this-page-on-github-link')
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,7 @@ function greet() {
 | 
			
		||||
		console.log(' ' + chalk.gray(v) + ('                        |___|\n'.substr(v.length)));
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
		console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
 | 
			
		||||
		console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, @acid-chicken, and @rinsuki.');
 | 
			
		||||
		console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
 | 
			
		||||
 | 
			
		||||
		console.log('');
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,11 @@ export function extractDbHost(uri: string) {
 | 
			
		||||
	return toDbHost(url.hostname);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function extractApHost(uri: string) {
 | 
			
		||||
	const url = new URL(uri);
 | 
			
		||||
	return toApHost(url.hostname);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toDbHost(host: string) {
 | 
			
		||||
	if (host == null) return null;
 | 
			
		||||
	return toUnicode(host.toLowerCase());
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								src/misc/gen-id.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/misc/gen-id.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
import { genMeid7 } from './id/meid7';
 | 
			
		||||
 | 
			
		||||
const method = 'meid7';
 | 
			
		||||
 | 
			
		||||
export function genId(date?: Date): string {
 | 
			
		||||
	if (!date || (date > new Date())) date = new Date();
 | 
			
		||||
 | 
			
		||||
	switch (method) {
 | 
			
		||||
		case 'meid7': return genMeid7(date);
 | 
			
		||||
		default: throw new Error('unknown id generation method');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/misc/id/meid7.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/misc/id/meid7.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
const CHARS = '0123456789abcdef';
 | 
			
		||||
 | 
			
		||||
//  4bit Fixed hex value '7'
 | 
			
		||||
// 44bit UNIX Time ms in Hex
 | 
			
		||||
// 48bit Random value in Hex
 | 
			
		||||
 | 
			
		||||
function getTime(time: number) {
 | 
			
		||||
	if (time < 0) time = 0;
 | 
			
		||||
	if (time === 0) {
 | 
			
		||||
		return CHARS[0];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return time.toString(16).padStart(11, CHARS[0]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRandom() {
 | 
			
		||||
	let str = '';
 | 
			
		||||
 | 
			
		||||
	for (let i = 0; i < 12; i++) {
 | 
			
		||||
		str += CHARS[Math.floor(Math.random() * CHARS.length)];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return str;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function genMeid7(date: Date): string {
 | 
			
		||||
	return '7' + getTime(date.getTime()) + getRandom();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
export interface Maybe<T> {
 | 
			
		||||
	isJust(): this is Just<T>;
 | 
			
		||||
export interface IMaybe<T> {
 | 
			
		||||
	isJust(): this is IJust<T>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Just<T> = Maybe<T> & {
 | 
			
		||||
	get(): T
 | 
			
		||||
};
 | 
			
		||||
export interface IJust<T> extends IMaybe<T> {
 | 
			
		||||
	get(): T;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function just<T>(value: T): Just<T> {
 | 
			
		||||
export function just<T>(value: T): IJust<T> {
 | 
			
		||||
	return {
 | 
			
		||||
		isJust: () => true,
 | 
			
		||||
		get: () => value
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function nothing<T>(): Maybe<T> {
 | 
			
		||||
export function nothing<T>(): IMaybe<T> {
 | 
			
		||||
	return {
 | 
			
		||||
		isJust: () => false,
 | 
			
		||||
	};
 | 
			
		||||
 
 | 
			
		||||
@@ -16,10 +16,9 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void>
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	let deletedCount = 0;
 | 
			
		||||
	let ended = false;
 | 
			
		||||
	let cursor: any = null;
 | 
			
		||||
 | 
			
		||||
	while (!ended) {
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const files = await DriveFile.find({
 | 
			
		||||
			userId: user._id,
 | 
			
		||||
			...(cursor ? { _id: { $gt: cursor } } : {})
 | 
			
		||||
@@ -31,7 +30,6 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void>
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (files.length === 0) {
 | 
			
		||||
			ended = true;
 | 
			
		||||
			job.progress(100);
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,10 +32,9 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
	const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
 | 
			
		||||
	let exportedCount = 0;
 | 
			
		||||
	let ended = false;
 | 
			
		||||
	let cursor: any = null;
 | 
			
		||||
 | 
			
		||||
	while (!ended) {
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const blockings = await Blocking.find({
 | 
			
		||||
			blockerId: user._id,
 | 
			
		||||
			...(cursor ? { _id: { $gt: cursor } } : {})
 | 
			
		||||
@@ -47,7 +46,6 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (blockings.length === 0) {
 | 
			
		||||
			ended = true;
 | 
			
		||||
			job.progress(100);
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
@@ -81,7 +79,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName);
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile._id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
 
 | 
			
		||||
@@ -32,10 +32,9 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
	const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
 | 
			
		||||
	let exportedCount = 0;
 | 
			
		||||
	let ended = false;
 | 
			
		||||
	let cursor: any = null;
 | 
			
		||||
 | 
			
		||||
	while (!ended) {
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const followings = await Following.find({
 | 
			
		||||
			followerId: user._id,
 | 
			
		||||
			...(cursor ? { _id: { $gt: cursor } } : {})
 | 
			
		||||
@@ -47,7 +46,6 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (followings.length === 0) {
 | 
			
		||||
			ended = true;
 | 
			
		||||
			job.progress(100);
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
@@ -81,7 +79,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName);
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile._id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
 
 | 
			
		||||
@@ -32,10 +32,9 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
	const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
 | 
			
		||||
	let exportedCount = 0;
 | 
			
		||||
	let ended = false;
 | 
			
		||||
	let cursor: any = null;
 | 
			
		||||
 | 
			
		||||
	while (!ended) {
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const mutes = await Mute.find({
 | 
			
		||||
			muterId: user._id,
 | 
			
		||||
			...(cursor ? { _id: { $gt: cursor } } : {})
 | 
			
		||||
@@ -47,7 +46,6 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (mutes.length === 0) {
 | 
			
		||||
			ended = true;
 | 
			
		||||
			job.progress(100);
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
@@ -81,7 +79,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName);
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile._id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
 
 | 
			
		||||
@@ -42,10 +42,9 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	let exportedNotesCount = 0;
 | 
			
		||||
	let ended = false;
 | 
			
		||||
	let cursor: any = null;
 | 
			
		||||
 | 
			
		||||
	while (!ended) {
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const notes = await Note.find({
 | 
			
		||||
			userId: user._id,
 | 
			
		||||
			...(cursor ? { _id: { $gt: cursor } } : {})
 | 
			
		||||
@@ -57,7 +56,6 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (notes.length === 0) {
 | 
			
		||||
			ended = true;
 | 
			
		||||
			job.progress(100);
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
@@ -101,7 +99,7 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName);
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile._id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName);
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile._id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,8 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
 | 
			
		||||
		linenum++;
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			const { username, host } = parseAcct(line.trim());
 | 
			
		||||
			const acct = line.split(',')[0].trim();
 | 
			
		||||
			const { username, host } = parseAcct(acct);
 | 
			
		||||
 | 
			
		||||
			let target = isSelfHost(host) ? await User.findOne({
 | 
			
		||||
				host: null,
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@ import Logger from '../../services/logger';
 | 
			
		||||
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';
 | 
			
		||||
import Instance from '../../models/instance';
 | 
			
		||||
import instanceChart from '../../services/chart/instance';
 | 
			
		||||
import { validActor } from '../../remote/activitypub/type';
 | 
			
		||||
import { toDbHost } from '../../misc/convert-host';
 | 
			
		||||
 | 
			
		||||
const logger = new Logger('inbox');
 | 
			
		||||
 | 
			
		||||
@@ -46,7 +48,7 @@ export default async (job: Bull.Job): Promise<void> => {
 | 
			
		||||
 | 
			
		||||
		// ブロックしてたら中断
 | 
			
		||||
		// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
 | 
			
		||||
		const instance = await Instance.findOne({ host: host.toLowerCase() });
 | 
			
		||||
		const instance = await Instance.findOne({ host: toDbHost(host) });
 | 
			
		||||
		if (instance && instance.isBlocked) {
 | 
			
		||||
			logger.info(`Blocked request: ${host}`);
 | 
			
		||||
			return;
 | 
			
		||||
@@ -65,7 +67,7 @@ export default async (job: Bull.Job): Promise<void> => {
 | 
			
		||||
 | 
			
		||||
		// ブロックしてたら中断
 | 
			
		||||
		// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
 | 
			
		||||
		const instance = await Instance.findOne({ host: host.toLowerCase() });
 | 
			
		||||
		const instance = await Instance.findOne({ host: toDbHost(host) });
 | 
			
		||||
		if (instance && instance.isBlocked) {
 | 
			
		||||
			logger.warn(`Blocked request: ${host}`);
 | 
			
		||||
			return;
 | 
			
		||||
@@ -79,7 +81,7 @@ export default async (job: Bull.Job): Promise<void> => {
 | 
			
		||||
 | 
			
		||||
	// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
 | 
			
		||||
	if (activity.type === 'Update') {
 | 
			
		||||
		if (activity.object && activity.object.type === 'Person') {
 | 
			
		||||
		if (activity.object && validActor.includes(activity.object.type)) {
 | 
			
		||||
			if (user == null) {
 | 
			
		||||
				logger.warn('Update activity received, but user not registed.');
 | 
			
		||||
			} else if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,10 +20,32 @@ import { apLogger } from '../logger';
 | 
			
		||||
import { IDriveFile } from '../../../models/drive-file';
 | 
			
		||||
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
 | 
			
		||||
import Instance from '../../../models/instance';
 | 
			
		||||
import { extractDbHost } from '../../../misc/convert-host';
 | 
			
		||||
import { extractDbHost, extractApHost } from '../../../misc/convert-host';
 | 
			
		||||
 | 
			
		||||
const logger = apLogger;
 | 
			
		||||
 | 
			
		||||
export function validateNote(object: any, uri: string) {
 | 
			
		||||
	const expectHost = extractApHost(uri);
 | 
			
		||||
 | 
			
		||||
	if (object == null) {
 | 
			
		||||
		return new Error('invalid Note: object is null');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!['Note', 'Question', 'Article'].includes(object.type)) {
 | 
			
		||||
		return new Error(`invalid Note: invalied object type ${object.type}`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (object.id && extractApHost(object.id) !== expectHost) {
 | 
			
		||||
		return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${extractApHost(object.id)}`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (object.attributedTo && extractApHost(object.attributedTo) !== expectHost) {
 | 
			
		||||
		return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${extractApHost(object.attributedTo)}`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Noteをフェッチします。
 | 
			
		||||
 *
 | 
			
		||||
@@ -57,8 +79,10 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 | 
			
		||||
 | 
			
		||||
	const object: any = await resolver.resolve(value);
 | 
			
		||||
 | 
			
		||||
	if (!object || !['Note', 'Question', 'Article'].includes(object.type)) {
 | 
			
		||||
		logger.error(`invalid note: ${value}`, {
 | 
			
		||||
	const entryUri = value.id || value;
 | 
			
		||||
	const err = validateNote(object, entryUri);
 | 
			
		||||
	if (err) {
 | 
			
		||||
		logger.error(`${err.message}`, {
 | 
			
		||||
			resolver: {
 | 
			
		||||
				history: resolver.getHistory()
 | 
			
		||||
			},
 | 
			
		||||
@@ -241,7 +265,7 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
 | 
			
		||||
	// リモートサーバーからフェッチしてきて登録
 | 
			
		||||
	// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
 | 
			
		||||
	// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
 | 
			
		||||
	return await createNote(uri, resolver);
 | 
			
		||||
	return await createNote(uri, resolver, true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function extractEmojis(tags: ITag[], host_: string) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import config from '../../../config';
 | 
			
		||||
import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user';
 | 
			
		||||
import Resolver from '../resolver';
 | 
			
		||||
import { resolveImage } from './image';
 | 
			
		||||
import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
 | 
			
		||||
import { isCollectionOrOrderedCollection, isCollection, IPerson, validActor } from '../type';
 | 
			
		||||
import { IDriveFile } from '../../../models/drive-file';
 | 
			
		||||
import Meta from '../../../models/meta';
 | 
			
		||||
import { fromHtml } from '../../../mfm/fromHtml';
 | 
			
		||||
@@ -38,7 +38,7 @@ function validatePerson(x: any, uri: string) {
 | 
			
		||||
		return new Error('invalid person: object is null');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (x.type != 'Person' && x.type != 'Service') {
 | 
			
		||||
	if (!validActor.includes(x.type)) {
 | 
			
		||||
		return new Error(`invalid person: object is not a person or service '${x.type}'`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -294,13 +294,6 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
 | 
			
		||||
	await User.update({ _id: exist._id }, {
 | 
			
		||||
		$set: {
 | 
			
		||||
			lastFetchedAt: new Date(),
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (resolver == null) resolver = new Resolver();
 | 
			
		||||
 | 
			
		||||
	const object = hint || await resolver.resolve(uri) as any;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,13 @@ import { URL } from 'url';
 | 
			
		||||
import * as crypto from 'crypto';
 | 
			
		||||
import { lookup, IRunOptions } from 'lookup-dns-cache';
 | 
			
		||||
import * as promiseAny from 'promise-any';
 | 
			
		||||
import { toUnicode } from 'punycode';
 | 
			
		||||
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { ILocalUser } from '../../models/user';
 | 
			
		||||
import { publishApLogStream } from '../../services/stream';
 | 
			
		||||
import { apLogger } from './logger';
 | 
			
		||||
import Instance from '../../models/instance';
 | 
			
		||||
import { toDbHost } from '../../misc/convert-host';
 | 
			
		||||
 | 
			
		||||
export const logger = apLogger.createSubLogger('deliver');
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,7 @@ export default async (user: ILocalUser, url: string, object: any) => {
 | 
			
		||||
 | 
			
		||||
	// ブロックしてたら中断
 | 
			
		||||
	// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
 | 
			
		||||
	const instance = await Instance.findOne({ host: toUnicode(host) });
 | 
			
		||||
	const instance = await Instance.findOne({ host: toDbHost(host) });
 | 
			
		||||
	if (instance && instance.isBlocked) return;
 | 
			
		||||
 | 
			
		||||
	const data = JSON.stringify(object);
 | 
			
		||||
@@ -35,7 +35,7 @@ export default async (user: ILocalUser, url: string, object: any) => {
 | 
			
		||||
	const addr = await resolveAddr(hostname);
 | 
			
		||||
	if (!addr) return;
 | 
			
		||||
 | 
			
		||||
	const _ = new Promise((resolve, reject) => {
 | 
			
		||||
	await new  Promise((resolve, reject) => {
 | 
			
		||||
		const req = request({
 | 
			
		||||
			protocol,
 | 
			
		||||
			hostname: addr,
 | 
			
		||||
@@ -82,8 +82,6 @@ export default async (user: ILocalUser, url: string, object: any) => {
 | 
			
		||||
		req.end(data);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	await _;
 | 
			
		||||
 | 
			
		||||
	//#region Log
 | 
			
		||||
	publishApLogStream({
 | 
			
		||||
		direction: 'out',
 | 
			
		||||
@@ -98,11 +96,18 @@ export default async (user: ILocalUser, url: string, object: any) => {
 | 
			
		||||
 * Resolve host (with cached, asynchrony)
 | 
			
		||||
 */
 | 
			
		||||
async function resolveAddr(domain: string) {
 | 
			
		||||
	const af = config.outgoingAddressFamily || 'ipv4';
 | 
			
		||||
	const useV4 = af == 'ipv4' || af == 'dual';
 | 
			
		||||
	const useV6 = af == 'ipv6' || af == 'dual';
 | 
			
		||||
 | 
			
		||||
	const promises = [];
 | 
			
		||||
 | 
			
		||||
	if (!useV4 && !useV6) throw 'No usable address family available';
 | 
			
		||||
	if (useV4) promises.push(resolveAddrInner(domain, { family: 4 }));
 | 
			
		||||
	if (useV6) promises.push(resolveAddrInner(domain, { family: 6 }));
 | 
			
		||||
 | 
			
		||||
	// v4/v6で先に取得できた方を採用する
 | 
			
		||||
	return await promiseAny([
 | 
			
		||||
		resolveAddrInner(domain, { family: 4 }),
 | 
			
		||||
		resolveAddrInner(domain, { family: 6 })
 | 
			
		||||
	]);
 | 
			
		||||
	return await promiseAny(promises);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resolveAddrInner(domain: string, options: IRunOptions = {}): Promise<string> {
 | 
			
		||||
 
 | 
			
		||||
@@ -65,6 +65,8 @@ interface IQuestionChoice {
 | 
			
		||||
	_misskey_votes?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const validActor = ['Person', 'Service'];
 | 
			
		||||
 | 
			
		||||
export interface IPerson extends IObject {
 | 
			
		||||
	type: 'Person';
 | 
			
		||||
	name: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import chalk from 'chalk';
 | 
			
		||||
 | 
			
		||||
const logger = remoteLogger.createSubLogger('resolve-user');
 | 
			
		||||
 | 
			
		||||
export default async (username: string, _host: string, option?: any, resync?: boolean): Promise<IUser> => {
 | 
			
		||||
export default async (username: string, _host: string, option?: any, resync = false): Promise<IUser> => {
 | 
			
		||||
	const usernameLower = username.toLowerCase();
 | 
			
		||||
 | 
			
		||||
	if (_host == null) {
 | 
			
		||||
@@ -28,7 +28,7 @@ export default async (username: string, _host: string, option?: any, resync?: bo
 | 
			
		||||
		return await User.findOne({ usernameLower, host: null });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const user = await User.findOne({ usernameLower, host }, option);
 | 
			
		||||
	const user = await User.findOne({ usernameLower, host }, option) as IRemoteUser;
 | 
			
		||||
 | 
			
		||||
	const acctLower = `${usernameLower}@${hostAscii}`;
 | 
			
		||||
 | 
			
		||||
@@ -39,14 +39,22 @@ export default async (username: string, _host: string, option?: any, resync?: bo
 | 
			
		||||
		return await createPerson(self.href);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (resync) {
 | 
			
		||||
	// resyncオプション OR ユーザー情報が古い場合は、WebFilgerからやりなおして返す
 | 
			
		||||
	if (resync || user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
 | 
			
		||||
		// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
 | 
			
		||||
		await User.update({ _id: user._id }, {
 | 
			
		||||
			$set: {
 | 
			
		||||
				lastFetchedAt: new Date(),
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		logger.info(`try resync: ${acctLower}`);
 | 
			
		||||
		const self = await resolveSelf(acctLower);
 | 
			
		||||
 | 
			
		||||
		if ((user as IRemoteUser).uri !== self.href) {
 | 
			
		||||
		if (user.uri !== self.href) {
 | 
			
		||||
			// if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping.
 | 
			
		||||
			logger.info(`uri missmatch: ${acctLower}`);
 | 
			
		||||
			logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${(user as IRemoteUser).uri} to ${self.href}`);
 | 
			
		||||
			logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
 | 
			
		||||
 | 
			
		||||
			// validate uri
 | 
			
		||||
			const uri = new URL(self.href);
 | 
			
		||||
@@ -79,8 +87,8 @@ export default async (username: string, _host: string, option?: any, resync?: bo
 | 
			
		||||
async function resolveSelf(acctLower: string) {
 | 
			
		||||
	logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
 | 
			
		||||
	const finger = await webFinger(acctLower).catch(e => {
 | 
			
		||||
		logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${e.message} (${e.status})`);
 | 
			
		||||
		throw e;
 | 
			
		||||
		logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`);
 | 
			
		||||
		throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`);
 | 
			
		||||
	});
 | 
			
		||||
	const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
 | 
			
		||||
	if (!self) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { WebFinger } from 'webfinger.js';
 | 
			
		||||
 | 
			
		||||
const webFinger = new WebFinger({ });
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import * as request from 'request-promise-native';
 | 
			
		||||
import { URL } from 'url';
 | 
			
		||||
import { query as urlQuery } from '../prelude/url';
 | 
			
		||||
 | 
			
		||||
type ILink = {
 | 
			
		||||
	href: string;
 | 
			
		||||
@@ -12,12 +13,33 @@ type IWebFinger = {
 | 
			
		||||
	subject: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default async function resolve(query: any): Promise<IWebFinger> {
 | 
			
		||||
	return await new Promise((res, rej) => webFinger.lookup(query, (error: Error | string, result: any) => {
 | 
			
		||||
		if (error) {
 | 
			
		||||
			return rej(error);
 | 
			
		||||
export default async function(query: string): Promise<IWebFinger> {
 | 
			
		||||
	const url = genUrl(query);
 | 
			
		||||
 | 
			
		||||
	return await request({
 | 
			
		||||
		url,
 | 
			
		||||
		proxy: config.proxy,
 | 
			
		||||
		timeout: 10 * 1000,
 | 
			
		||||
		forever: true,
 | 
			
		||||
		headers: {
 | 
			
		||||
			'User-Agent': config.userAgent,
 | 
			
		||||
			Accept: 'application/jrd+json, application/json'
 | 
			
		||||
		},
 | 
			
		||||
		json: true
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function genUrl(query: string) {
 | 
			
		||||
	if (query.match(/^https?:\/\//)) {
 | 
			
		||||
		const u = new URL(query);
 | 
			
		||||
		return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		res(result.object);
 | 
			
		||||
	})) as IWebFinger;
 | 
			
		||||
	const m = query.match(/^([^@]+)@(.*)/);
 | 
			
		||||
	if (m) {
 | 
			
		||||
		const hostname = m[2];
 | 
			
		||||
		return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	throw new Error(`Invalied query (${query})`);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import Resolver from '../../../../remote/activitypub/resolver';
 | 
			
		||||
import { ApiError } from '../../error';
 | 
			
		||||
import Instance from '../../../../models/instance';
 | 
			
		||||
import { extractDbHost } from '../../../../misc/convert-host';
 | 
			
		||||
import { validActor } from '../../../../remote/activitypub/type';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['federation'],
 | 
			
		||||
@@ -85,6 +86,17 @@ async function fetchAny(uri: string) {
 | 
			
		||||
	// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
 | 
			
		||||
	// これはDBに存在する可能性があるため再度DB検索
 | 
			
		||||
	if (uri !== object.id) {
 | 
			
		||||
		if (object.id.startsWith(config.url + '/')) {
 | 
			
		||||
			const id = new mongo.ObjectID(object.id.split('/').pop());
 | 
			
		||||
			const [user, note] = await Promise.all([
 | 
			
		||||
				User.findOne({ _id: id }),
 | 
			
		||||
				Note.findOne({ _id: id })
 | 
			
		||||
			]);
 | 
			
		||||
 | 
			
		||||
			const packed = await mergePack(user, note);
 | 
			
		||||
			if (packed !== null) return packed;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const [user, note] = await Promise.all([
 | 
			
		||||
			User.findOne({ uri: object.id }),
 | 
			
		||||
			Note.findOne({ uri: object.id })
 | 
			
		||||
@@ -95,7 +107,7 @@ async function fetchAny(uri: string) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// それでもみつからなければ新規であるため登録
 | 
			
		||||
	if (object.type === 'Person') {
 | 
			
		||||
	if (validActor.includes(object.type)) {
 | 
			
		||||
		const user = await createPerson(object.id);
 | 
			
		||||
		return {
 | 
			
		||||
			type: 'User',
 | 
			
		||||
@@ -104,7 +116,7 @@ async function fetchAny(uri: string) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (['Note', 'Question', 'Article'].includes(object.type)) {
 | 
			
		||||
		const note = await createNote(object.id);
 | 
			
		||||
		const note = await createNote(object.id, null, true);
 | 
			
		||||
		return {
 | 
			
		||||
			type: 'Note',
 | 
			
		||||
			object: await packNote(note, null, { detail: true })
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import $ from 'cafy';
 | 
			
		||||
import ID, { transform, transformMany } from '../../../../misc/cafy-id';
 | 
			
		||||
import User, { pack, isRemoteUser } from '../../../../models/user';
 | 
			
		||||
import User, { pack } from '../../../../models/user';
 | 
			
		||||
import resolveRemoteUser from '../../../../remote/resolve-user';
 | 
			
		||||
import define from '../../define';
 | 
			
		||||
import { apiLogger } from '../../logger';
 | 
			
		||||
@@ -96,13 +96,6 @@ export default define(meta, async (ps, me) => {
 | 
			
		||||
			throw new ApiError(meta.errors.noSuchUser);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// ユーザー情報更新
 | 
			
		||||
		if (isRemoteUser(user)) {
 | 
			
		||||
			if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
 | 
			
		||||
				resolveRemoteUser(ps.username, ps.host, { }, true);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return await pack(user, me, {
 | 
			
		||||
			detail: true
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,8 @@ import discord from './service/discord';
 | 
			
		||||
import github from './service/github';
 | 
			
		||||
import twitter from './service/twitter';
 | 
			
		||||
import Instance from '../../models/instance';
 | 
			
		||||
import { toASCII } from 'punycode';
 | 
			
		||||
import { toApHost } from '../../misc/convert-host';
 | 
			
		||||
import { unique } from '../../prelude/array';
 | 
			
		||||
 | 
			
		||||
// Init app
 | 
			
		||||
const app = new Koa();
 | 
			
		||||
@@ -72,7 +73,7 @@ router.get('/v1/instance/peers', async ctx => {
 | 
			
		||||
			host: 1
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	const punyCodes = instances.map(instance => toASCII(instance.host));
 | 
			
		||||
	const punyCodes = unique(instances.map(instance => toApHost(instance.host)));
 | 
			
		||||
 | 
			
		||||
	ctx.body = punyCodes;
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ export default class extends Channel {
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
				case 'mention': {
 | 
			
		||||
					if (mutedUserIds.includes(body.userId)) return;
 | 
			
		||||
					if (body.isHidden) return;
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ export default async function(ctx: Koa.BaseContext) {
 | 
			
		||||
				await sendRaw();
 | 
			
		||||
			} else {
 | 
			
		||||
				ctx.status = 404;
 | 
			
		||||
				await send(ctx as any, '/dummy.png', { root: assets });
 | 
			
		||||
				await send(ctx as any, '/thumbnail-not-available.png', { root: assets });
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if ('web' in ctx.query) {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,9 @@ block meta
 | 
			
		||||
 | 
			
		||||
	meta(name='twitter:card' content='summary')
 | 
			
		||||
 | 
			
		||||
	if user.host
 | 
			
		||||
		meta(name='robots' content='noindex')
 | 
			
		||||
 | 
			
		||||
	if user.twitter
 | 
			
		||||
		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,9 @@ block meta
 | 
			
		||||
 | 
			
		||||
	meta(name='twitter:card' content='summary')
 | 
			
		||||
 | 
			
		||||
	if user.host
 | 
			
		||||
		meta(name='robots' content='noindex')
 | 
			
		||||
 | 
			
		||||
	if user.twitter
 | 
			
		||||
		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ import Instance from '../../models/instance';
 | 
			
		||||
import extractMentions from '../../misc/extract-mentions';
 | 
			
		||||
import extractEmojis from '../../misc/extract-emojis';
 | 
			
		||||
import extractHashtags from '../../misc/extract-hashtags';
 | 
			
		||||
import { genId } from '../../misc/gen-id';
 | 
			
		||||
 | 
			
		||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
 | 
			
		||||
 | 
			
		||||
@@ -434,6 +435,7 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
 | 
			
		||||
 | 
			
		||||
async function insertNote(user: IUser, data: Option, tags: string[], emojis: string[], mentionedUsers: IUser[]) {
 | 
			
		||||
	const insert: any = {
 | 
			
		||||
		_id: genId(data.createdAt),
 | 
			
		||||
		createdAt: data.createdAt,
 | 
			
		||||
		fileIds: data.files ? data.files.map(file => file._id) : [],
 | 
			
		||||
		replyId: data.reply ? data.reply._id : null,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,12 @@
 | 
			
		||||
import Instance, { IInstance } from '../models/instance';
 | 
			
		||||
import federationChart from '../services/chart/federation';
 | 
			
		||||
import { toDbHost } from '../misc/convert-host';
 | 
			
		||||
 | 
			
		||||
export async function registerOrFetchInstanceDoc(host: string): Promise<IInstance> {
 | 
			
		||||
	if (host == null) return null;
 | 
			
		||||
 | 
			
		||||
	host = toDbHost(host);
 | 
			
		||||
 | 
			
		||||
	const index = await Instance.findOne({ host });
 | 
			
		||||
 | 
			
		||||
	if (index == null) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user