Merge branch 'master' into l10n_master
This commit is contained in:
		
						commit
						4a3d74c608
					
				
					 101 changed files with 1490 additions and 395 deletions
				
			
		| 
						 | 
					@ -50,8 +50,11 @@ remoteDriveCapacityMb: 8
 | 
				
			||||||
# If enabled:
 | 
					# If enabled:
 | 
				
			||||||
#  Server will not cache remote files (Using direct link instead).
 | 
					#  Server will not cache remote files (Using direct link instead).
 | 
				
			||||||
#  You can save your storage.
 | 
					#  You can save your storage.
 | 
				
			||||||
#  Users cannot see remote images when they turn off "Show media from a remote server" setting.
 | 
					#
 | 
				
			||||||
preventCache: false
 | 
					#  NOTE:
 | 
				
			||||||
 | 
					#  * Users cannot see remote images when they turn off "Show media from a remote server" setting.
 | 
				
			||||||
 | 
					#  * Since thumbnails are not provided, traffic increases.
 | 
				
			||||||
 | 
					preventCacheRemoteFiles: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
drive:
 | 
					drive:
 | 
				
			||||||
  storage: 'db'
 | 
					  storage: 'db'
 | 
				
			||||||
| 
						 | 
					@ -64,7 +67,7 @@ drive:
 | 
				
			||||||
  # config:
 | 
					  # config:
 | 
				
			||||||
  #   endPoint:
 | 
					  #   endPoint:
 | 
				
			||||||
  #   port:
 | 
					  #   port:
 | 
				
			||||||
  #   secure:
 | 
					  #   useSSL:
 | 
				
			||||||
  #   accessKey:
 | 
					  #   accessKey:
 | 
				
			||||||
  #   secretKey:
 | 
					  #   secretKey:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,7 +78,7 @@ drive:
 | 
				
			||||||
  # config:
 | 
					  # config:
 | 
				
			||||||
  #   endPoint: s3-us-west-2.amazonaws.com
 | 
					  #   endPoint: s3-us-west-2.amazonaws.com
 | 
				
			||||||
  #   region: us-west-2
 | 
					  #   region: us-west-2
 | 
				
			||||||
  #   secure: true
 | 
					  #   useSSL: true
 | 
				
			||||||
  #   accessKey: XXX
 | 
					  #   accessKey: XXX
 | 
				
			||||||
  #   secretKey: YYY
 | 
					  #   secretKey: YYY
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -87,7 +90,7 @@ drive:
 | 
				
			||||||
  # config:
 | 
					  # config:
 | 
				
			||||||
  #   endPoint: s3-us-west-2.amazonaws.com
 | 
					  #   endPoint: s3-us-west-2.amazonaws.com
 | 
				
			||||||
  #   region: us-west-2
 | 
					  #   region: us-west-2
 | 
				
			||||||
  #   secure: true
 | 
					  #   useSSL: true
 | 
				
			||||||
  #   accessKey: XXX
 | 
					  #   accessKey: XXX
 | 
				
			||||||
  #   secretKey: YYY
 | 
					  #   secretKey: YYY
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -123,6 +126,7 @@ drive:
 | 
				
			||||||
# google_maps_api_key: example-google-maps-api-key
 | 
					# google_maps_api_key: example-google-maps-api-key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Twitter integration
 | 
					# Twitter integration
 | 
				
			||||||
 | 
					# You need to set the oauth callback url as : https://<your-misskey-instance>/api/tw/cb
 | 
				
			||||||
# twitter:
 | 
					# twitter:
 | 
				
			||||||
#   consumer_key: example-twitter-consumer-key
 | 
					#   consumer_key: example-twitter-consumer-key
 | 
				
			||||||
#   consumer_secret: example-twitter-consumer-secret-key
 | 
					#   consumer_secret: example-twitter-consumer-secret-key
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,6 @@ addons:
 | 
				
			||||||
      - ubuntu-toolchain-r-test
 | 
					      - ubuntu-toolchain-r-test
 | 
				
			||||||
    packages:
 | 
					    packages:
 | 
				
			||||||
      - g++-4.8
 | 
					      - g++-4.8
 | 
				
			||||||
      - graphicsmagick
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
cache:
 | 
					cache:
 | 
				
			||||||
  directories:
 | 
					  directories:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										4
									
								
								.vsls.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.vsls.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						"$schema": "http://json.schemastore.org/vsls",
 | 
				
			||||||
 | 
						"gitignore": "exclude"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,15 @@ ChangeLog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This document describes breaking changes only.
 | 
					This document describes breaking changes only.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					6.0.0
 | 
				
			||||||
 | 
					-----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Migration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					オブジェクトストレージを使用している場合、設定ファイルの`drive.config.secure`を`drive.config.useSSL`にリネームしてください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you use object storage, please rename `drive.config.secure` to `drive.config.useSSL` in config.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
5.0.0
 | 
					5.0.0
 | 
				
			||||||
-----
 | 
					-----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										31
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										31
									
								
								README.md
									
										
									
									
									
								
							| 
						 | 
					@ -43,9 +43,34 @@ If you want to...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:heart: Backers & Sponsors
 | 
					:heart: Backers & Sponsors
 | 
				
			||||||
----------------------------------------------------------------
 | 
					----------------------------------------------------------------
 | 
				
			||||||
| <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D"> | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12378075/0156f769e20f412594fa6b87d85fe228/1?token-time=2145916800&token-hash=IsIJRUXszzoD6-7pDnRY8I05T9nSznc4GTaxj7C9SwU%3D"> | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D"> | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D"> |
 | 
					<table>
 | 
				
			||||||
|:-:|:-:|:-:|:-:|
 | 
					  <tr>
 | 
				
			||||||
| [Gargron](https://www.patreon.com/mastodon) | [39ff](https://www.patreon.com/user/creators?u=12378075) | [dansup](https://www.patreon.com/dansup) | [Takashi Shibuya](https://www.patreon.com/user/creators?u=12531784) |
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12378075/0156f769e20f412594fa6b87d85fe228/1?token-time=2145916800&token-hash=IsIJRUXszzoD6-7pDnRY8I05T9nSznc4GTaxj7C9SwU%3D"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12959468/c249e15aebec4424b5c0f427173671b6/1?token-time=2145916800&token-hash=lubpCEdxAkxPlpR2O6bvZ7BIh8Q4nGf-U_mE1qpjVAQ%3D"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/1?token-time=2145916800&token-hash=f03BFb4S2FUx9YEt87TnEmifb4h33OywGBW2akQVtQY%3D"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/2?token-time=2145916800&token-hash=zElv7ZcPL3viGsXbNG_KWiKrbV0vvw1gk0panx8DJoo%3D"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12731202/0995c46cdcb54153ab5f073f5869b70a/1?token-time=2145916800&token-hash=Yd60FK_SWfQO56SeiJpy1tDHOnCV4xdEywQe8gn5_Wo%3D"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c8.patreon.com/2/100/12718187"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12931605/ead494101f364dffa90efe49e36fb494/1?token-time=2145916800&token-hash=NzSFPjIlodXyv41rwK61aZWVZWfI4surJaNj8vWKvqM%3D"></td>
 | 
				
			||||||
 | 
					    <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=GgJ_NmUB6_nnRNLVGUWjV-WX91On7BOu59LKncYV9fE%3D"></td>
 | 
				
			||||||
 | 
					  </tr>
 | 
				
			||||||
 | 
					  <tr>
 | 
				
			||||||
 | 
					    <td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
 | 
				
			||||||
 | 
					    <td><a href="https://www.patreon.com/user/creators?u=12378075">39ff</a></td>
 | 
				
			||||||
 | 
					    <td><a href="https://www.patreon.com/dansup">dansup</a></td>
 | 
				
			||||||
 | 
					    <td><a href="https://www.patreon.com/user/creators?u=12531784">Takashi Shibuya</a></td>
 | 
				
			||||||
 | 
					    <td><a href="https://www.patreon.com/fujishan">fujishan</a></td>
 | 
				
			||||||
 | 
					    <td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
 | 
				
			||||||
 | 
					    <td><a href="https://www.patreon.com/user?u=5881381">Naoki Kosaka</a></td>
 | 
				
			||||||
 | 
					    <td><a href="https://www.patreon.com/user?u=12731202">negao</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=12931605">Reiju</a></td>
 | 
				
			||||||
 | 
					    <td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
 | 
				
			||||||
 | 
					  </tr>
 | 
				
			||||||
 | 
					</table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
:four_leaf_clover: Copyright
 | 
					:four_leaf_clover: Copyright
 | 
				
			||||||
----------------------------------------------------------------
 | 
					----------------------------------------------------------------
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,26 +0,0 @@
 | 
				
			||||||
FROM base/archlinux
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
MAINTAINER Aya Morisawa
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RUN rm /etc/pacman.d/mirrorlist
 | 
					 | 
				
			||||||
RUN echo 'Server = http://ftp.jaist.ac.jp/pub/Linux/ArchLinux/$repo/os/$arch'    >> /etc/pacman.d/mirrorlist
 | 
					 | 
				
			||||||
RUN echo 'Server = http://ftp.tsukuba.wide.ad.jp/Linux/archlinux/$repo/os/$arch' >> /etc/pacman.d/mirrorlist
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RUN rm /etc/localtime
 | 
					 | 
				
			||||||
RUN ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RUN pacman -Sy --noconfirm
 | 
					 | 
				
			||||||
RUN pacman -S --noconfirm pacman
 | 
					 | 
				
			||||||
RUN pacman-db-upgrade
 | 
					 | 
				
			||||||
RUN pacman -S --noconfirm archlinux-keyring
 | 
					 | 
				
			||||||
RUN pacman -Syyu --noconfirm
 | 
					 | 
				
			||||||
RUN pacman -S --noconfirm git nodejs npm mongodb redis
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
COPY misskey.sh /root/misskey.sh
 | 
					 | 
				
			||||||
RUN chmod u+x /root/misskey.sh
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
EXPOSE 80
 | 
					 | 
				
			||||||
EXPOSE 443
 | 
					 | 
				
			||||||
EXPOSE 27017
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CMD ["/root/misskey.sh"]
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,6 +0,0 @@
 | 
				
			||||||
#!/bin/sh
 | 
					 | 
				
			||||||
redis-server --daemonize yes
 | 
					 | 
				
			||||||
mongod > /dev/null &
 | 
					 | 
				
			||||||
cd /root/misskey
 | 
					 | 
				
			||||||
npm start
 | 
					 | 
				
			||||||
tail -f /dev/null
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,29 +0,0 @@
 | 
				
			||||||
Setup with Docker :whale:
 | 
					 | 
				
			||||||
================================================================
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Ensure that the working directory is the repository root directory.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
To create misskey image:
 | 
					 | 
				
			||||||
``` console
 | 
					 | 
				
			||||||
$ sudo docker build -t misskey ./docker
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
To run misskey:
 | 
					 | 
				
			||||||
``` console
 | 
					 | 
				
			||||||
$ sudo docker run --rm -i -t -p $PORT:80 -v $(pwd):/root/misskey -v $DBPATH:/data/db misskey
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
where `$PORT` is the port used to access Misskey Web from host browser
 | 
					 | 
				
			||||||
and `$DBPATH` is the path of MongoDB database on the host for data persistence.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ex:
 | 
					 | 
				
			||||||
``` console
 | 
					 | 
				
			||||||
$ sudo docker run --rm -i -t -p 80:80 -v $(pwd):/root/misskey -v /data/db:/data/db misskey
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
If you want to run misskey in production mode, add `--env NODE_ENV=production` like this:
 | 
					 | 
				
			||||||
``` console
 | 
					 | 
				
			||||||
$ sudo docker run --rm -i -t -p 80:80 -v $(pwd):/root/misskey -v /data/db:/data/db --env NODE_ENV=production misskey
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Note that `$(pwd)` is the working directory.
 | 
					 | 
				
			||||||
| 
						 | 
					@ -62,6 +62,13 @@ npm install web-push -g
 | 
				
			||||||
web-push generate-vapid-keys
 | 
					web-push generate-vapid-keys
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*(optional)* Create a twitter application
 | 
				
			||||||
 | 
					----------------------------------------------------------------
 | 
				
			||||||
 | 
					If you want to enable the twitter integration, you need to create a twitter app at [https://developer.twitter.com/en/apply/user](https://developer.twitter.com/en/apply/user).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					In the app you need to set the oauth callback url as : https://misskey-instance/api/tw/cb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
*5.* Make configuration file
 | 
					*5.* Make configuration file
 | 
				
			||||||
----------------------------------------------------------------
 | 
					----------------------------------------------------------------
 | 
				
			||||||
1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
 | 
					1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,6 +70,7 @@ common:
 | 
				
			||||||
    congrats: "おめでとう"
 | 
					    congrats: "おめでとう"
 | 
				
			||||||
    angry: "おこ"
 | 
					    angry: "おこ"
 | 
				
			||||||
    confused: "こまこまのこまり"
 | 
					    confused: "こまこまのこまり"
 | 
				
			||||||
 | 
					    rip: "RIP"
 | 
				
			||||||
    pudding: "Pudding"
 | 
					    pudding: "Pudding"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  note-placeholders:
 | 
					  note-placeholders:
 | 
				
			||||||
| 
						 | 
					@ -89,7 +90,7 @@ common:
 | 
				
			||||||
  my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
 | 
					  my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
 | 
				
			||||||
  i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
 | 
					  i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
 | 
				
			||||||
  show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示"
 | 
					  show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示"
 | 
				
			||||||
  verified-user: "認証済みのユーザー"
 | 
					  verified-user: "公式アカウント"
 | 
				
			||||||
  disable-animated-mfm: "投稿内の動きのあるテキストを無効にする"
 | 
					  disable-animated-mfm: "投稿内の動きのあるテキストを無効にする"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  reversi:
 | 
					  reversi:
 | 
				
			||||||
| 
						 | 
					@ -181,6 +182,9 @@ common/views/components/games/reversi/reversi.vue:
 | 
				
			||||||
common/views/components/games/reversi/reversi.game.vue:
 | 
					common/views/components/games/reversi/reversi.game.vue:
 | 
				
			||||||
  surrender: "投了"
 | 
					  surrender: "投了"
 | 
				
			||||||
  surrendered: "投了により"
 | 
					  surrendered: "投了により"
 | 
				
			||||||
 | 
					  is-llotheo: "石の少ない方が勝ち(ロセオ)"
 | 
				
			||||||
 | 
					  looped-map: "ループマップ"
 | 
				
			||||||
 | 
					  can-put-everywhere: "どこでも置けるモード"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
common/views/components/games/reversi/reversi.index.vue:
 | 
					common/views/components/games/reversi/reversi.index.vue:
 | 
				
			||||||
  title: "Misskey Reversi"
 | 
					  title: "Misskey Reversi"
 | 
				
			||||||
| 
						 | 
					@ -310,6 +314,7 @@ common/views/components/signin.vue:
 | 
				
			||||||
  signin: "サインイン"
 | 
					  signin: "サインイン"
 | 
				
			||||||
  or: "または"
 | 
					  or: "または"
 | 
				
			||||||
  signin-with-twitter: "Twitterでログイン"
 | 
					  signin-with-twitter: "Twitterでログイン"
 | 
				
			||||||
 | 
					  login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
common/views/components/signup.vue:
 | 
					common/views/components/signup.vue:
 | 
				
			||||||
  username: "ユーザー名"
 | 
					  username: "ユーザー名"
 | 
				
			||||||
| 
						 | 
					@ -703,9 +708,11 @@ desktop/views/components/settings.vue:
 | 
				
			||||||
  circle-icons: "円形のアイコンを使用"
 | 
					  circle-icons: "円形のアイコンを使用"
 | 
				
			||||||
  gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
 | 
					  gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
 | 
				
			||||||
  post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
 | 
					  post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
 | 
				
			||||||
 | 
					  suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
 | 
				
			||||||
  show-reply-target: "リプライ先を表示する"
 | 
					  show-reply-target: "リプライ先を表示する"
 | 
				
			||||||
  show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
 | 
					  show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
 | 
				
			||||||
  show-renoted-my-notes: "Renoteされた自分の投稿をタイムラインに表示する"
 | 
					  show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
 | 
				
			||||||
 | 
					  show-local-renotes: "ローカルの投稿のRenoteをタイムラインに表示する"
 | 
				
			||||||
  show-maps: "マップの自動展開"
 | 
					  show-maps: "マップの自動展開"
 | 
				
			||||||
  show-maps-desc: "位置情報が添付された投稿のマップを自動的に展開します。"
 | 
					  show-maps-desc: "位置情報が添付された投稿のマップを自動的に展開します。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -893,6 +900,29 @@ desktop/views/components/window.vue:
 | 
				
			||||||
  popout: "ポップアウト"
 | 
					  popout: "ポップアウト"
 | 
				
			||||||
  close: "閉じる"
 | 
					  close: "閉じる"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					desktop/views/pages/admin/admin.vue:
 | 
				
			||||||
 | 
					  dashboard: "ダッシュボード"
 | 
				
			||||||
 | 
					  drive: "ドライブ"
 | 
				
			||||||
 | 
					  users: "ユーザー"
 | 
				
			||||||
 | 
					  update: "更新"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					desktop/views/pages/admin/admin.dashboard.vue:
 | 
				
			||||||
 | 
					  dashboard: "ダッシュボード"
 | 
				
			||||||
 | 
					  all-users: "全てのユーザー"
 | 
				
			||||||
 | 
					  original-users: "このインスタンスのユーザー"
 | 
				
			||||||
 | 
					  all-notes: "全てのノート"
 | 
				
			||||||
 | 
					  original-notes: "このインスタンスのノート"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					desktop/views/pages/admin/admin.suspend-user.vue:
 | 
				
			||||||
 | 
					  suspend-user: "ユーザーの凍結"
 | 
				
			||||||
 | 
					  suspend: "凍結"
 | 
				
			||||||
 | 
					  suspended: "凍結しました"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					desktop/views/pages/admin/admin.unsuspend-user.vue:
 | 
				
			||||||
 | 
					  unsuspend-user: "ユーザーの凍結の解除"
 | 
				
			||||||
 | 
					  unsuspend: "凍結の解除"
 | 
				
			||||||
 | 
					  unsuspended: "凍結を解除しました"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
					desktop/views/pages/deck/deck.tl-column.vue:
 | 
				
			||||||
  is-media-only: "メディア投稿のみ"
 | 
					  is-media-only: "メディア投稿のみ"
 | 
				
			||||||
  is-media-view: "メディアビュー"
 | 
					  is-media-view: "メディアビュー"
 | 
				
			||||||
| 
						 | 
					@ -1266,7 +1296,8 @@ mobile/views/pages/settings.vue:
 | 
				
			||||||
  timeline: "タイムライン"
 | 
					  timeline: "タイムライン"
 | 
				
			||||||
  show-reply-target: "リプライ先を表示する"
 | 
					  show-reply-target: "リプライ先を表示する"
 | 
				
			||||||
  show-my-renotes: "自分の行ったRenoteを表示する"
 | 
					  show-my-renotes: "自分の行ったRenoteを表示する"
 | 
				
			||||||
  show-renoted-my-notes: "Renoteされた自分の投稿を表示する"
 | 
					  show-renoted-my-notes: "自分の投稿のRenoteを表示する"
 | 
				
			||||||
 | 
					  show-local-renotes: "ローカルの投稿のRenoteを表示する"
 | 
				
			||||||
  post-style: "投稿の表示スタイル"
 | 
					  post-style: "投稿の表示スタイル"
 | 
				
			||||||
  post-style-standard: "標準"
 | 
					  post-style-standard: "標準"
 | 
				
			||||||
  post-style-smart: "スマート"
 | 
					  post-style-smart: "スマート"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										33
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										33
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	"name": "misskey",
 | 
						"name": "misskey",
 | 
				
			||||||
	"author": "syuilo <i@syuilo.com>",
 | 
						"author": "syuilo <i@syuilo.com>",
 | 
				
			||||||
	"version": "5.20.1",
 | 
						"version": "6.2.0",
 | 
				
			||||||
	"clientVersion": "1.0.8105",
 | 
						"clientVersion": "1.0.8417",
 | 
				
			||||||
	"codename": "nighthike",
 | 
						"codename": "nighthike",
 | 
				
			||||||
	"main": "./built/index.js",
 | 
						"main": "./built/index.js",
 | 
				
			||||||
	"private": true,
 | 
						"private": true,
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,7 @@
 | 
				
			||||||
		"@types/dateformat": "1.0.1",
 | 
							"@types/dateformat": "1.0.1",
 | 
				
			||||||
		"@types/debug": "0.0.30",
 | 
							"@types/debug": "0.0.30",
 | 
				
			||||||
		"@types/deep-equal": "1.0.1",
 | 
							"@types/deep-equal": "1.0.1",
 | 
				
			||||||
 | 
							"@types/double-ended-queue": "2.1.0",
 | 
				
			||||||
		"@types/elasticsearch": "5.0.25",
 | 
							"@types/elasticsearch": "5.0.25",
 | 
				
			||||||
		"@types/file-type": "5.2.1",
 | 
							"@types/file-type": "5.2.1",
 | 
				
			||||||
		"@types/gulp": "3.8.36",
 | 
							"@types/gulp": "3.8.36",
 | 
				
			||||||
| 
						 | 
					@ -57,9 +58,9 @@
 | 
				
			||||||
		"@types/minio": "6.0.2",
 | 
							"@types/minio": "6.0.2",
 | 
				
			||||||
		"@types/mkdirp": "0.5.2",
 | 
							"@types/mkdirp": "0.5.2",
 | 
				
			||||||
		"@types/mocha": "5.2.3",
 | 
							"@types/mocha": "5.2.3",
 | 
				
			||||||
		"@types/mongodb": "3.1.3",
 | 
							"@types/mongodb": "3.1.4",
 | 
				
			||||||
		"@types/ms": "0.7.30",
 | 
							"@types/ms": "0.7.30",
 | 
				
			||||||
		"@types/node": "10.5.7",
 | 
							"@types/node": "10.7.1",
 | 
				
			||||||
		"@types/portscanner": "2.1.0",
 | 
							"@types/portscanner": "2.1.0",
 | 
				
			||||||
		"@types/pug": "2.0.4",
 | 
							"@types/pug": "2.0.4",
 | 
				
			||||||
		"@types/qrcode": "1.2.0",
 | 
							"@types/qrcode": "1.2.0",
 | 
				
			||||||
| 
						 | 
					@ -76,10 +77,10 @@
 | 
				
			||||||
		"@types/systeminformation": "3.23.0",
 | 
							"@types/systeminformation": "3.23.0",
 | 
				
			||||||
		"@types/tmp": "0.0.33",
 | 
							"@types/tmp": "0.0.33",
 | 
				
			||||||
		"@types/uuid": "3.4.3",
 | 
							"@types/uuid": "3.4.3",
 | 
				
			||||||
		"@types/webpack": "4.4.9",
 | 
							"@types/webpack": "4.4.10",
 | 
				
			||||||
		"@types/webpack-stream": "3.2.10",
 | 
							"@types/webpack-stream": "3.2.10",
 | 
				
			||||||
		"@types/websocket": "0.0.39",
 | 
							"@types/websocket": "0.0.39",
 | 
				
			||||||
		"@types/ws": "5.1.2",
 | 
							"@types/ws": "6.0.0",
 | 
				
			||||||
		"animejs": "2.2.0",
 | 
							"animejs": "2.2.0",
 | 
				
			||||||
		"autosize": "4.0.2",
 | 
							"autosize": "4.0.2",
 | 
				
			||||||
		"autwh": "0.1.0",
 | 
							"autwh": "0.1.0",
 | 
				
			||||||
| 
						 | 
					@ -97,6 +98,7 @@
 | 
				
			||||||
		"deepcopy": "0.6.3",
 | 
							"deepcopy": "0.6.3",
 | 
				
			||||||
		"diskusage": "0.2.4",
 | 
							"diskusage": "0.2.4",
 | 
				
			||||||
		"dompurify": "1.0.5",
 | 
							"dompurify": "1.0.5",
 | 
				
			||||||
 | 
							"double-ended-queue": "2.1.0-0",
 | 
				
			||||||
		"elasticsearch": "15.1.1",
 | 
							"elasticsearch": "15.1.1",
 | 
				
			||||||
		"element-ui": "2.4.6",
 | 
							"element-ui": "2.4.6",
 | 
				
			||||||
		"emojilib": "2.3.0",
 | 
							"emojilib": "2.3.0",
 | 
				
			||||||
| 
						 | 
					@ -145,8 +147,9 @@
 | 
				
			||||||
		"koa-slow": "2.1.0",
 | 
							"koa-slow": "2.1.0",
 | 
				
			||||||
		"koa-views": "6.1.4",
 | 
							"koa-views": "6.1.4",
 | 
				
			||||||
		"loader-utils": "1.1.0",
 | 
							"loader-utils": "1.1.0",
 | 
				
			||||||
 | 
							"lodash.assign": "4.2.0",
 | 
				
			||||||
		"mecab-async": "0.1.2",
 | 
							"mecab-async": "0.1.2",
 | 
				
			||||||
		"minio": "6.0.0",
 | 
							"minio": "7.0.0",
 | 
				
			||||||
		"mkdirp": "0.5.1",
 | 
							"mkdirp": "0.5.1",
 | 
				
			||||||
		"mocha": "5.2.0",
 | 
							"mocha": "5.2.0",
 | 
				
			||||||
		"moji": "0.5.1",
 | 
							"moji": "0.5.1",
 | 
				
			||||||
| 
						 | 
					@ -160,7 +163,7 @@
 | 
				
			||||||
		"object-assign-deep": "0.4.0",
 | 
							"object-assign-deep": "0.4.0",
 | 
				
			||||||
		"on-build-webpack": "0.1.0",
 | 
							"on-build-webpack": "0.1.0",
 | 
				
			||||||
		"os-utils": "0.0.14",
 | 
							"os-utils": "0.0.14",
 | 
				
			||||||
		"parse5": "5.0.0",
 | 
							"parse5": "5.1.0",
 | 
				
			||||||
		"portscanner": "2.2.0",
 | 
							"portscanner": "2.2.0",
 | 
				
			||||||
		"progress-bar-webpack-plugin": "1.11.0",
 | 
							"progress-bar-webpack-plugin": "1.11.0",
 | 
				
			||||||
		"promise-sequential": "1.1.1",
 | 
							"promise-sequential": "1.1.1",
 | 
				
			||||||
| 
						 | 
					@ -171,13 +174,13 @@
 | 
				
			||||||
		"recaptcha-promise": "0.1.3",
 | 
							"recaptcha-promise": "0.1.3",
 | 
				
			||||||
		"reconnecting-websocket": "3.2.2",
 | 
							"reconnecting-websocket": "3.2.2",
 | 
				
			||||||
		"redis": "2.8.0",
 | 
							"redis": "2.8.0",
 | 
				
			||||||
		"request": "2.87.0",
 | 
							"request": "2.88.0",
 | 
				
			||||||
		"request-promise-native": "1.0.5",
 | 
							"request-promise-native": "1.0.5",
 | 
				
			||||||
		"rimraf": "2.6.2",
 | 
							"rimraf": "2.6.2",
 | 
				
			||||||
		"rndstr": "1.0.0",
 | 
							"rndstr": "1.0.0",
 | 
				
			||||||
		"s-age": "1.1.2",
 | 
							"s-age": "1.1.2",
 | 
				
			||||||
		"sass-loader": "7.1.0",
 | 
							"sass-loader": "7.1.0",
 | 
				
			||||||
		"seedrandom": "2.4.3",
 | 
							"seedrandom": "2.4.4",
 | 
				
			||||||
		"sharp": "0.20.5",
 | 
							"sharp": "0.20.5",
 | 
				
			||||||
		"showdown": "1.8.6",
 | 
							"showdown": "1.8.6",
 | 
				
			||||||
		"showdown-highlightjs-extension": "0.1.2",
 | 
							"showdown-highlightjs-extension": "0.1.2",
 | 
				
			||||||
| 
						 | 
					@ -187,27 +190,27 @@
 | 
				
			||||||
		"style-loader": "0.22.1",
 | 
							"style-loader": "0.22.1",
 | 
				
			||||||
		"stylus": "0.54.5",
 | 
							"stylus": "0.54.5",
 | 
				
			||||||
		"stylus-loader": "3.0.2",
 | 
							"stylus-loader": "3.0.2",
 | 
				
			||||||
		"summaly": "2.1.2",
 | 
							"summaly": "2.1.3",
 | 
				
			||||||
		"systeminformation": "3.42.9",
 | 
							"systeminformation": "3.42.9",
 | 
				
			||||||
		"syuilo-password-strength": "0.0.1",
 | 
							"syuilo-password-strength": "0.0.1",
 | 
				
			||||||
		"textarea-caret": "3.1.0",
 | 
							"textarea-caret": "3.1.0",
 | 
				
			||||||
		"tmp": "0.0.33",
 | 
							"tmp": "0.0.33",
 | 
				
			||||||
		"ts-loader": "4.4.1",
 | 
							"ts-loader": "4.4.1",
 | 
				
			||||||
		"ts-node": "7.0.0",
 | 
							"ts-node": "7.0.1",
 | 
				
			||||||
		"tslint": "5.10.0",
 | 
							"tslint": "5.10.0",
 | 
				
			||||||
		"typescript": "2.9.2",
 | 
							"typescript": "2.9.2",
 | 
				
			||||||
		"typescript-eslint-parser": "18.0.0",
 | 
							"typescript-eslint-parser": "18.0.0",
 | 
				
			||||||
		"uglify-es": "3.3.9",
 | 
							"uglify-es": "3.3.9",
 | 
				
			||||||
		"url-loader": "1.0.1",
 | 
							"url-loader": "1.1.0",
 | 
				
			||||||
		"uuid": "3.3.2",
 | 
							"uuid": "3.3.2",
 | 
				
			||||||
		"v-animate-css": "0.0.2",
 | 
							"v-animate-css": "0.0.2",
 | 
				
			||||||
		"vue": "2.5.17",
 | 
							"vue": "2.5.17",
 | 
				
			||||||
		"vue-cropperjs": "2.2.1",
 | 
							"vue-cropperjs": "2.2.1",
 | 
				
			||||||
		"vue-js-modal": "1.3.16",
 | 
							"vue-js-modal": "1.3.17",
 | 
				
			||||||
		"vue-json-tree-view": "2.1.4",
 | 
							"vue-json-tree-view": "2.1.4",
 | 
				
			||||||
		"vue-loader": "15.3.0",
 | 
							"vue-loader": "15.3.0",
 | 
				
			||||||
		"vue-router": "3.0.1",
 | 
							"vue-router": "3.0.1",
 | 
				
			||||||
		"vue-style-loader": "4.1.1",
 | 
							"vue-style-loader": "4.1.2",
 | 
				
			||||||
		"vue-template-compiler": "2.5.17",
 | 
							"vue-template-compiler": "2.5.17",
 | 
				
			||||||
		"vuedraggable": "2.16.0",
 | 
							"vuedraggable": "2.16.0",
 | 
				
			||||||
		"vuex": "3.0.1",
 | 
							"vuex": "3.0.1",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,9 @@
 | 
				
			||||||
export default () => [
 | 
					const kaos = [
 | 
				
			||||||
	'(=^・・^=)',
 | 
						'(=^・・^=)',
 | 
				
			||||||
	'v(\'ω\')v',
 | 
						'v(\'ω\')v',
 | 
				
			||||||
	'🐡( \'-\' 🐡 )フグパンチ!!!!'
 | 
						'🐡( \'-\' 🐡 )フグパンチ!!!!',
 | 
				
			||||||
][Math.floor(Math.random() * 3)];
 | 
						'🖕(´・_・`)🖕',
 | 
				
			||||||
 | 
						'(。>﹏<。)'
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default () => kaos[Math.floor(Math.random() * kaos.length)];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,16 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
	<span class="mk-avatar" :title="user | acct" :style="style" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"></span>
 | 
						<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
 | 
				
			||||||
	<span class="mk-avatar" :title="user | acct" :style="style" v-else-if="disableLink && disablePreview" @click="onClick"></span>
 | 
							<span class="inner" :style="style"></span>
 | 
				
			||||||
	<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id"></router-link>
 | 
						</span>
 | 
				
			||||||
	<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else-if="!disableLink && disablePreview"></router-link>
 | 
						<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
 | 
				
			||||||
 | 
							<span class="inner" :style="style"></span>
 | 
				
			||||||
 | 
						</span>
 | 
				
			||||||
 | 
						<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
 | 
				
			||||||
 | 
							<span class="inner" :style="style"></span>
 | 
				
			||||||
 | 
						</router-link>
 | 
				
			||||||
 | 
						<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
 | 
				
			||||||
 | 
							<span class="inner" :style="style"></span>
 | 
				
			||||||
 | 
						</router-link>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
| 
						 | 
					@ -30,14 +38,17 @@ export default Vue.extend({
 | 
				
			||||||
		lightmode(): boolean {
 | 
							lightmode(): boolean {
 | 
				
			||||||
			return this.$store.state.device.lightmode;
 | 
								return this.$store.state.device.lightmode;
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							cat(): boolean {
 | 
				
			||||||
 | 
								return this.user.isCat && this.$store.state.settings.circleIcons;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		style(): any {
 | 
							style(): any {
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				backgroundColor: this.lightmode
 | 
									backgroundColor: this.lightmode
 | 
				
			||||||
					? `rgb(${ this.user.avatarColor.slice(0, 3).join(',') })`
 | 
										? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`
 | 
				
			||||||
					: this.user.avatarColor && this.user.avatarColor.length == 3
 | 
										: this.user.avatarColor && this.user.avatarColor.length == 3
 | 
				
			||||||
						? `rgb(${ this.user.avatarColor.join(',') })`
 | 
											? `rgb(${this.user.avatarColor.join(',')})`
 | 
				
			||||||
						: null,
 | 
											: null,
 | 
				
			||||||
				backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl })`,
 | 
									backgroundImage: this.lightmode ? null : `url(${this.user.avatarUrl})`,
 | 
				
			||||||
				borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
 | 
									borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -51,10 +62,47 @@ export default Vue.extend({
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
.mk-avatar
 | 
					
 | 
				
			||||||
 | 
					root(isDark)
 | 
				
			||||||
	display inline-block
 | 
						display inline-block
 | 
				
			||||||
	vertical-align bottom
 | 
						vertical-align bottom
 | 
				
			||||||
	background-size cover
 | 
					
 | 
				
			||||||
 | 
						&:not(.cat)
 | 
				
			||||||
 | 
							overflow hidden
 | 
				
			||||||
 | 
							border-radius 8px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.cat::before,
 | 
				
			||||||
 | 
						&.cat::after
 | 
				
			||||||
 | 
							background #df548f
 | 
				
			||||||
 | 
							border solid 4px isDark ? #e0eefd : #202224
 | 
				
			||||||
 | 
							box-sizing border-box
 | 
				
			||||||
 | 
							content ''
 | 
				
			||||||
 | 
							display inline-block
 | 
				
			||||||
 | 
							height 50%
 | 
				
			||||||
 | 
							width 50%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.cat::before
 | 
				
			||||||
 | 
							border-radius 0 75% 75%
 | 
				
			||||||
 | 
							transform rotate(37.5deg) skew(30deg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.cat::after
 | 
				
			||||||
 | 
							border-radius 75% 0 75% 75%
 | 
				
			||||||
 | 
							transform rotate(-37.5deg) skew(-30deg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.inner
 | 
				
			||||||
		background-position center center
 | 
							background-position center center
 | 
				
			||||||
 | 
							background-size cover
 | 
				
			||||||
 | 
							bottom 0
 | 
				
			||||||
 | 
							left 0
 | 
				
			||||||
 | 
							position absolute
 | 
				
			||||||
 | 
							right 0
 | 
				
			||||||
 | 
							top 0
 | 
				
			||||||
		transition border-radius 1s ease
 | 
							transition border-radius 1s ease
 | 
				
			||||||
 | 
							z-index 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mk-avatar[data-darkmode]
 | 
				
			||||||
 | 
						root(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mk-avatar:not([data-darkmode])
 | 
				
			||||||
 | 
						root(false)
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,6 +60,12 @@
 | 
				
			||||||
			<el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button>
 | 
								<el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button>
 | 
				
			||||||
		</el-button-group>
 | 
							</el-button-group>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class="info">
 | 
				
			||||||
 | 
							<p v-if="game.settings.isLlotheo">%i18n:@is-llotheo%</p>
 | 
				
			||||||
 | 
							<p v-if="game.settings.loopedBoard">%i18n:@looped-map%</p>
 | 
				
			||||||
 | 
							<p v-if="game.settings.canPutEverywhere">%i18n:@can-put-everywhere%</p>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
 | 
					<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
 | 
				
			||||||
	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
 | 
						<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
 | 
				
			||||||
	<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
 | 
						<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
 | 
				
			||||||
	<span class="is-verified" v-if="note.user.isVerified" title="%i18n:common.verified-user%">%fa:bookmark%</span>
 | 
						<span class="is-verified" v-if="note.user.isVerified" title="%i18n:common.verified-user%">%fa:star%</span>
 | 
				
			||||||
	<span class="is-admin" v-if="note.user.isAdmin">admin</span>
 | 
						<span class="is-admin" v-if="note.user.isAdmin">admin</span>
 | 
				
			||||||
	<span class="is-bot" v-if="note.user.isBot">bot</span>
 | 
						<span class="is-bot" v-if="note.user.isBot">bot</span>
 | 
				
			||||||
	<span class="is-cat" v-if="note.user.isCat">cat</span>
 | 
						<span class="is-cat" v-if="note.user.isCat">cat</span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@
 | 
				
			||||||
	<img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%">
 | 
						<img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%">
 | 
				
			||||||
	<img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%">
 | 
						<img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%">
 | 
				
			||||||
	<img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%">
 | 
						<img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%">
 | 
				
			||||||
 | 
						<img v-if="reaction == 'rip'" src="/assets/reactions/rip.png" alt="%i18n:common.reactions.rip%">
 | 
				
			||||||
	<template v-if="reaction == 'pudding'">
 | 
						<template v-if="reaction == 'pudding'">
 | 
				
			||||||
		<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="/assets/reactions/sushi.png" alt="%i18n:common.reactions.pudding%">
 | 
							<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="/assets/reactions/sushi.png" alt="%i18n:common.reactions.pudding%">
 | 
				
			||||||
		<img v-else src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%">
 | 
							<img v-else src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,9 +10,10 @@
 | 
				
			||||||
			<button @click="react('hmm')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button>
 | 
								<button @click="react('hmm')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button>
 | 
				
			||||||
			<button @click="react('surprise')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button>
 | 
								<button @click="react('surprise')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button>
 | 
				
			||||||
			<button @click="react('congrats')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button>
 | 
								<button @click="react('congrats')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button>
 | 
				
			||||||
			<button @click="react('angry')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button>
 | 
								<button @click="react('angry')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="7" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button>
 | 
				
			||||||
			<button @click="react('confused')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button>
 | 
								<button @click="react('confused')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="8" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button>
 | 
				
			||||||
			<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button>
 | 
								<button @click="react('rip')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="9" title="%i18n:common.reactions.rip%"><mk-reaction-icon reaction='rip'/></button>
 | 
				
			||||||
 | 
								<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="10" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,16 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div class="mk-reactions-viewer">
 | 
					<div class="mk-reactions-viewer">
 | 
				
			||||||
	<template v-if="reactions">
 | 
						<template v-if="reactions">
 | 
				
			||||||
		<span v-if="reactions.like"><mk-reaction-icon reaction="like"/><span>{{ reactions.like }}</span></span>
 | 
							<span :class="{notReacted}" @click="react('like')" v-if="reactions.like"><mk-reaction-icon reaction="like"/><span>{{ reactions.like }}</span></span>
 | 
				
			||||||
		<span v-if="reactions.love"><mk-reaction-icon reaction="love"/><span>{{ reactions.love }}</span></span>
 | 
							<span :class="{notReacted}" @click="react('love')" v-if="reactions.love"><mk-reaction-icon reaction="love"/><span>{{ reactions.love }}</span></span>
 | 
				
			||||||
		<span v-if="reactions.laugh"><mk-reaction-icon reaction="laugh"/><span>{{ reactions.laugh }}</span></span>
 | 
							<span :class="{notReacted}" @click="react('laugh')" v-if="reactions.laugh"><mk-reaction-icon reaction="laugh"/><span>{{ reactions.laugh }}</span></span>
 | 
				
			||||||
		<span v-if="reactions.hmm"><mk-reaction-icon reaction="hmm"/><span>{{ reactions.hmm }}</span></span>
 | 
							<span :class="{notReacted}" @click="react('hmm')" v-if="reactions.hmm"><mk-reaction-icon reaction="hmm"/><span>{{ reactions.hmm }}</span></span>
 | 
				
			||||||
		<span v-if="reactions.surprise"><mk-reaction-icon reaction="surprise"/><span>{{ reactions.surprise }}</span></span>
 | 
							<span :class="{notReacted}" @click="react('surprise')" v-if="reactions.surprise"><mk-reaction-icon reaction="surprise"/><span>{{ reactions.surprise }}</span></span>
 | 
				
			||||||
		<span v-if="reactions.congrats"><mk-reaction-icon reaction="congrats"/><span>{{ reactions.congrats }}</span></span>
 | 
							<span :class="{notReacted}" @click="react('congrats')" v-if="reactions.congrats"><mk-reaction-icon reaction="congrats"/><span>{{ reactions.congrats }}</span></span>
 | 
				
			||||||
		<span v-if="reactions.angry"><mk-reaction-icon reaction="angry"/><span>{{ reactions.angry }}</span></span>
 | 
							<span :class="{notReacted}" @click="react('angry')" v-if="reactions.angry"><mk-reaction-icon reaction="angry"/><span>{{ reactions.angry }}</span></span>
 | 
				
			||||||
		<span v-if="reactions.confused"><mk-reaction-icon reaction="confused"/><span>{{ reactions.confused }}</span></span>
 | 
							<span :class="{notReacted}" @click="react('confused')" v-if="reactions.confused"><mk-reaction-icon reaction="confused"/><span>{{ reactions.confused }}</span></span>
 | 
				
			||||||
		<span v-if="reactions.pudding"><mk-reaction-icon reaction="pudding"/><span>{{ reactions.pudding }}</span></span>
 | 
							<span :class="{notReacted}" @click="react('rip')" v-if="reactions.rip"><mk-reaction-icon reaction="rip"/><span>{{ reactions.rip }}</span></span>
 | 
				
			||||||
 | 
							<span :class="{notReacted}" @click="react('pudding')" v-if="reactions.pudding"><mk-reaction-icon reaction="pudding"/><span>{{ reactions.pudding }}</span></span>
 | 
				
			||||||
	</template>
 | 
						</template>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -21,6 +22,17 @@ export default Vue.extend({
 | 
				
			||||||
	computed: {
 | 
						computed: {
 | 
				
			||||||
		reactions(): number {
 | 
							reactions(): number {
 | 
				
			||||||
			return this.note.reactionCounts;
 | 
								return this.note.reactionCounts;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							notReacted(): boolean {
 | 
				
			||||||
 | 
								return this.note.myReaction == null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							react(reaction: string) {
 | 
				
			||||||
 | 
								(this as any).api('notes/reactions/create', {
 | 
				
			||||||
 | 
									noteId: this.note.id,
 | 
				
			||||||
 | 
									reaction: reaction
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -39,6 +51,9 @@ root(isDark)
 | 
				
			||||||
	> span
 | 
						> span
 | 
				
			||||||
		margin-right 8px
 | 
							margin-right 8px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							&.notReacted
 | 
				
			||||||
 | 
								cursor pointer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		> .mk-reaction-icon
 | 
							> .mk-reaction-icon
 | 
				
			||||||
			font-size 1.4em
 | 
								font-size 1.4em
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,13 +12,13 @@
 | 
				
			||||||
	</ui-input>
 | 
						</ui-input>
 | 
				
			||||||
	<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/>
 | 
						<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required/>
 | 
				
			||||||
	<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
 | 
						<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
 | 
				
			||||||
	<p style="margin: 8px 0;">%i18n:@or%<a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
 | 
						<p style="margin: 8px 0;" v-if="twitterIntegration">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
import { apiUrl, host } from '../../../config';
 | 
					import { apiUrl, host, twitterIntegration } from '../../../config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
	props: {
 | 
						props: {
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,8 @@ export default Vue.extend({
 | 
				
			||||||
			password: '',
 | 
								password: '',
 | 
				
			||||||
			token: '',
 | 
								token: '',
 | 
				
			||||||
			apiUrl,
 | 
								apiUrl,
 | 
				
			||||||
			host
 | 
								host,
 | 
				
			||||||
 | 
								twitterIntegration
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
| 
						 | 
					@ -59,7 +60,7 @@ export default Vue.extend({
 | 
				
			||||||
			}).then(() => {
 | 
								}).then(() => {
 | 
				
			||||||
				location.reload();
 | 
									location.reload();
 | 
				
			||||||
			}).catch(() => {
 | 
								}).catch(() => {
 | 
				
			||||||
				alert('something happened');
 | 
									alert('%i18n:@login-failed%');
 | 
				
			||||||
				this.signing = false;
 | 
									this.signing = false;
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<iframe v-if="youtubeId" type="text/html" height="250"
 | 
					<iframe v-if="player" :src="player" heigth="250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
 | 
				
			||||||
	:src="`https://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`"
 | 
					 | 
				
			||||||
	frameborder="0"/>
 | 
					 | 
				
			||||||
<iframe v-else-if="spotifyId"
 | 
					 | 
				
			||||||
	:src="`https://open.spotify.com/embed/track/${spotifyId}`"
 | 
					 | 
				
			||||||
	frameborder="0" allowtransparency="true" allow="encrypted-media" />
 | 
					 | 
				
			||||||
<iframe v-else-if="nicovideoId"
 | 
					 | 
				
			||||||
	:src="`https://embed.nicovideo.jp/watch/${nicovideoId}?oldScript=1&referer=${misskeyUrl}&from=${position || '0'}&allowProgrammaticFullScreen=1`"
 | 
					 | 
				
			||||||
	frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
 | 
					 | 
				
			||||||
<div v-else-if="tweetUrl && detail" class="twitter">
 | 
					<div v-else-if="tweetUrl && detail" class="twitter">
 | 
				
			||||||
	<blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null">
 | 
						<blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null">
 | 
				
			||||||
		<a :href="url"></a>
 | 
							<a :href="url"></a>
 | 
				
			||||||
| 
						 | 
					@ -54,10 +46,7 @@ export default Vue.extend({
 | 
				
			||||||
			thumbnail: null,
 | 
								thumbnail: null,
 | 
				
			||||||
			icon: null,
 | 
								icon: null,
 | 
				
			||||||
			sitename: null,
 | 
								sitename: null,
 | 
				
			||||||
			youtubeId: null,
 | 
								player: null,
 | 
				
			||||||
			spotifyId: null,
 | 
					 | 
				
			||||||
			nicovideoId: null,
 | 
					 | 
				
			||||||
			position: null,
 | 
					 | 
				
			||||||
			tweetUrl: null,
 | 
								tweetUrl: null,
 | 
				
			||||||
			misskeyUrl
 | 
								misskeyUrl
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
| 
						 | 
					@ -65,23 +54,7 @@ export default Vue.extend({
 | 
				
			||||||
	created() {
 | 
						created() {
 | 
				
			||||||
		const url = new URL(this.url);
 | 
							const url = new URL(this.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (url.hostname == 'www.youtube.com') {
 | 
							if (this.detail && url.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(url.pathname)) {
 | 
				
			||||||
			this.youtubeId = url.searchParams.get('v');
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		} else if (url.hostname == 'youtu.be') {
 | 
					 | 
				
			||||||
			this.youtubeId = url.pathname;
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		} else if (url.hostname == 'open.spotify.com') {
 | 
					 | 
				
			||||||
			this.spotifyId = url.pathname.split('/').reverse().filter(x => x !== '')[0];
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		} else if (['nicovideo.jp', 'www.nicovideo.jp', 'nico.ms'].includes(url.hostname)) {
 | 
					 | 
				
			||||||
			const id = url.pathname.split('/').reverse().filter(x => x !== '')[0];
 | 
					 | 
				
			||||||
			if (['sm', 'nm', 'ax', 'ca', 'cd', 'cw', 'fx', 'ig', 'na', 'om', 'sd', 'sk', 'yk', 'yo', 'za', 'zb', 'zc', 'zd', 'ze', 'nl', 'so', ...Array(10).keys()].some(x => id.startsWith(x)) {
 | 
					 | 
				
			||||||
				this.nicovideoId = id;
 | 
					 | 
				
			||||||
				this.position = url.searchParams.get('from');
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} else if (this.detail && url.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(url.pathname)) {
 | 
					 | 
				
			||||||
			this.tweetUrl = url;
 | 
								this.tweetUrl = url;
 | 
				
			||||||
			const twttr = (window as any).twttr || {};
 | 
								const twttr = (window as any).twttr || {};
 | 
				
			||||||
			const loadTweet = () => twttr.widgets.load(this.$refs.tweet);
 | 
								const loadTweet = () => twttr.widgets.load(this.$refs.tweet);
 | 
				
			||||||
| 
						 | 
					@ -104,16 +77,95 @@ export default Vue.extend({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
 | 
							fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
 | 
				
			||||||
			res.json().then(info => {
 | 
								res.json().then(info => {
 | 
				
			||||||
 | 
									if (info.url != null) {
 | 
				
			||||||
					this.title = info.title;
 | 
										this.title = info.title;
 | 
				
			||||||
					this.description = info.description;
 | 
										this.description = info.description;
 | 
				
			||||||
					this.thumbnail = info.thumbnail;
 | 
										this.thumbnail = info.thumbnail;
 | 
				
			||||||
					this.icon = info.icon;
 | 
										this.icon = info.icon;
 | 
				
			||||||
					this.sitename = info.sitename;
 | 
										this.sitename = info.sitename;
 | 
				
			||||||
 | 
					 | 
				
			||||||
					this.fetching = false;
 | 
										this.fetching = false;
 | 
				
			||||||
			});
 | 
										if ([ // THIS IS THE WHITELIST FOR THE EMBED PLAYER
 | 
				
			||||||
		});
 | 
											'afreecatv.com',
 | 
				
			||||||
	}
 | 
											'aparat.com',
 | 
				
			||||||
 | 
											'applemusic.com',
 | 
				
			||||||
 | 
											'amazon.com',
 | 
				
			||||||
 | 
											'awa.fm',
 | 
				
			||||||
 | 
											'bandcamp.com',
 | 
				
			||||||
 | 
											'bbc.co.uk',
 | 
				
			||||||
 | 
											'beatport.com',
 | 
				
			||||||
 | 
											'bilibili.com',
 | 
				
			||||||
 | 
											'boomstream.com',
 | 
				
			||||||
 | 
											'breakers.tv',
 | 
				
			||||||
 | 
											'cam4.com',
 | 
				
			||||||
 | 
											'cavelis.net',
 | 
				
			||||||
 | 
											'chaturbate.com',
 | 
				
			||||||
 | 
											'cnn.com',
 | 
				
			||||||
 | 
											'cybergame.tv',
 | 
				
			||||||
 | 
											'dailymotion.com',
 | 
				
			||||||
 | 
											'deezer.com',
 | 
				
			||||||
 | 
											'djlive.pl',
 | 
				
			||||||
 | 
											'e-onkyo.com',
 | 
				
			||||||
 | 
											'eventials.com',
 | 
				
			||||||
 | 
											'facebook.com',
 | 
				
			||||||
 | 
											'fc2.com',
 | 
				
			||||||
 | 
											'gameplank.tv',
 | 
				
			||||||
 | 
											'goodgame.ru',
 | 
				
			||||||
 | 
											'google.com',
 | 
				
			||||||
 | 
											'hardtunes.com',
 | 
				
			||||||
 | 
											'instagram.com',
 | 
				
			||||||
 | 
											'johnnylooch.com',
 | 
				
			||||||
 | 
											'kexp.org',
 | 
				
			||||||
 | 
											'lahzenegar.com',
 | 
				
			||||||
 | 
											'liveedu.tv',
 | 
				
			||||||
 | 
											'livetube.cc',
 | 
				
			||||||
 | 
											'livestream.com',
 | 
				
			||||||
 | 
											'meridix.com',
 | 
				
			||||||
 | 
											'mixcloud.com',
 | 
				
			||||||
 | 
											'mixer.com',
 | 
				
			||||||
 | 
											'mobcrush.com',
 | 
				
			||||||
 | 
											'mylive.in.th',
 | 
				
			||||||
 | 
											'myspace.com',
 | 
				
			||||||
 | 
											'netflix.com',
 | 
				
			||||||
 | 
											'newretrowave.com',
 | 
				
			||||||
 | 
											'nhk.or.jp',
 | 
				
			||||||
 | 
											'nicovideo.jp',
 | 
				
			||||||
 | 
											'nico.ms',
 | 
				
			||||||
 | 
											'noisetrade.com',
 | 
				
			||||||
 | 
											'nood.tv',
 | 
				
			||||||
 | 
											'npr.org',
 | 
				
			||||||
 | 
											'openrec.tv',
 | 
				
			||||||
 | 
											'pandora.com',
 | 
				
			||||||
 | 
											'pandora.tv',
 | 
				
			||||||
 | 
											'picarto.tv',
 | 
				
			||||||
 | 
											'pscp.tv',
 | 
				
			||||||
 | 
											'restream.io',
 | 
				
			||||||
 | 
											'reverbnation.com',
 | 
				
			||||||
 | 
											'sermonaudio.com',
 | 
				
			||||||
 | 
											'smashcast.tv',
 | 
				
			||||||
 | 
											'songkick.com',
 | 
				
			||||||
 | 
											'soundcloud.com',
 | 
				
			||||||
 | 
											'spinninrecords.com',
 | 
				
			||||||
 | 
											'spotify.com',
 | 
				
			||||||
 | 
											'stitcher.com',
 | 
				
			||||||
 | 
											'stream.me',
 | 
				
			||||||
 | 
											'switchboard.live',
 | 
				
			||||||
 | 
											'tunein.com',
 | 
				
			||||||
 | 
											'twitcasting.tv',
 | 
				
			||||||
 | 
											'twitch.tv',
 | 
				
			||||||
 | 
											'twitter.com',
 | 
				
			||||||
 | 
											'vaughnlive.tv',
 | 
				
			||||||
 | 
											'veoh.com',
 | 
				
			||||||
 | 
											'vimeo.com',
 | 
				
			||||||
 | 
											'watchpeoplecode.com',
 | 
				
			||||||
 | 
											'web.tv',
 | 
				
			||||||
 | 
											'youtube.com',
 | 
				
			||||||
 | 
											'youtu.be'
 | 
				
			||||||
 | 
										].some(x => x == url.hostname || url.hostname.endsWith(`.${x}`)))
 | 
				
			||||||
 | 
											this.player = info.player;
 | 
				
			||||||
 | 
									}	// info.url
 | 
				
			||||||
 | 
								})	// json
 | 
				
			||||||
 | 
							});	// fetch
 | 
				
			||||||
 | 
						}	// created
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,7 @@ declare const _CODENAME_: string;
 | 
				
			||||||
declare const _LICENSE_: string;
 | 
					declare const _LICENSE_: string;
 | 
				
			||||||
declare const _GOOGLE_MAPS_API_KEY_: string;
 | 
					declare const _GOOGLE_MAPS_API_KEY_: string;
 | 
				
			||||||
declare const _WELCOME_BG_URL_: string;
 | 
					declare const _WELCOME_BG_URL_: string;
 | 
				
			||||||
 | 
					declare const _TWITTER_INTEGRATION_: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const host = _HOST_;
 | 
					export const host = _HOST_;
 | 
				
			||||||
export const hostname = _HOSTNAME_;
 | 
					export const hostname = _HOSTNAME_;
 | 
				
			||||||
| 
						 | 
					@ -47,3 +48,4 @@ export const codename = _CODENAME_;
 | 
				
			||||||
export const license = _LICENSE_;
 | 
					export const license = _LICENSE_;
 | 
				
			||||||
export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_;
 | 
					export const googleMapsApiKey = _GOOGLE_MAPS_API_KEY_;
 | 
				
			||||||
export const welcomeBgUrl = _WELCOME_BG_URL_;
 | 
					export const welcomeBgUrl = _WELCOME_BG_URL_;
 | 
				
			||||||
 | 
					export const twitterIntegration = _TWITTER_INTEGRATION_;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,7 @@ import updateBanner from './api/update-banner';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import MkIndex from './views/pages/index.vue';
 | 
					import MkIndex from './views/pages/index.vue';
 | 
				
			||||||
import MkDeck from './views/pages/deck/deck.vue';
 | 
					import MkDeck from './views/pages/deck/deck.vue';
 | 
				
			||||||
 | 
					import MkAdmin from './views/pages/admin/admin.vue';
 | 
				
			||||||
import MkUser from './views/pages/user/user.vue';
 | 
					import MkUser from './views/pages/user/user.vue';
 | 
				
			||||||
import MkFavorites from './views/pages/favorites.vue';
 | 
					import MkFavorites from './views/pages/favorites.vue';
 | 
				
			||||||
import MkSelectDrive from './views/pages/selectdrive.vue';
 | 
					import MkSelectDrive from './views/pages/selectdrive.vue';
 | 
				
			||||||
| 
						 | 
					@ -55,6 +56,7 @@ init(async (launch) => {
 | 
				
			||||||
		routes: [
 | 
							routes: [
 | 
				
			||||||
			{ path: '/', name: 'index', component: MkIndex },
 | 
								{ path: '/', name: 'index', component: MkIndex },
 | 
				
			||||||
			{ path: '/deck', name: 'deck', component: MkDeck },
 | 
								{ path: '/deck', name: 'deck', component: MkDeck },
 | 
				
			||||||
 | 
								{ path: '/admin', name: 'admin', component: MkAdmin },
 | 
				
			||||||
			{ path: '/i/customize-home', component: MkHomeCustomize },
 | 
								{ path: '/i/customize-home', component: MkHomeCustomize },
 | 
				
			||||||
			{ path: '/i/favorites', component: MkFavorites },
 | 
								{ path: '/i/favorites', component: MkFavorites },
 | 
				
			||||||
			{ path: '/i/messaging/:user', component: MkMessagingRoom },
 | 
								{ path: '/i/messaging/:user', component: MkMessagingRoom },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@
 | 
				
			||||||
		<p>%i18n:@banner%</p>
 | 
							<p>%i18n:@banner%</p>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
 | 
						<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
 | 
				
			||||||
		<img :src="file.url" alt="" @load="onThumbnailLoaded"/>
 | 
							<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded"/>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<p class="name">
 | 
						<p class="name">
 | 
				
			||||||
		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
 | 
							<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
 | 
				
			||||||
| 
						 | 
					@ -99,7 +99,7 @@ export default Vue.extend({
 | 
				
			||||||
					text: '%i18n:@contextmenu.set-as-banner%',
 | 
										text: '%i18n:@contextmenu.set-as-banner%',
 | 
				
			||||||
					action: this.setAsBanner
 | 
										action: this.setAsBanner
 | 
				
			||||||
				}]
 | 
									}]
 | 
				
			||||||
			}, {
 | 
								}, /*{
 | 
				
			||||||
				type: 'nest',
 | 
									type: 'nest',
 | 
				
			||||||
				text: '%i18n:@contextmenu.open-in-app%',
 | 
									text: '%i18n:@contextmenu.open-in-app%',
 | 
				
			||||||
				menu: [{
 | 
									menu: [{
 | 
				
			||||||
| 
						 | 
					@ -107,7 +107,7 @@ export default Vue.extend({
 | 
				
			||||||
					text: '%i18n:@contextmenu.add-app%...',
 | 
										text: '%i18n:@contextmenu.add-app%...',
 | 
				
			||||||
					action: this.addApp
 | 
										action: this.addApp
 | 
				
			||||||
				}]
 | 
									}]
 | 
				
			||||||
			}], {
 | 
								}*/], {
 | 
				
			||||||
					closed: () => {
 | 
										closed: () => {
 | 
				
			||||||
						this.isContextmenuShowing = false;
 | 
											this.isContextmenuShowing = false;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,12 +67,12 @@ export default Vue.extend({
 | 
				
			||||||
				text: '%i18n:@contextmenu.rename%',
 | 
									text: '%i18n:@contextmenu.rename%',
 | 
				
			||||||
				icon: '%fa:i-cursor%',
 | 
									icon: '%fa:i-cursor%',
 | 
				
			||||||
				action: this.rename
 | 
									action: this.rename
 | 
				
			||||||
			}, null, {
 | 
								}/*, null, {
 | 
				
			||||||
				type: 'item',
 | 
									type: 'item',
 | 
				
			||||||
				text: '%i18n:common.delete%',
 | 
									text: '%i18n:common.delete%',
 | 
				
			||||||
				icon: '%fa:R trash-alt%',
 | 
									icon: '%fa:R trash-alt%',
 | 
				
			||||||
				action: this.deleteFolder
 | 
									action: this.deleteFolder
 | 
				
			||||||
			}], {
 | 
								}*/], {
 | 
				
			||||||
					closed: () => {
 | 
										closed: () => {
 | 
				
			||||||
						this.isContextmenuShowing = false;
 | 
											this.isContextmenuShowing = false;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -567,6 +567,7 @@ export default Vue.extend({
 | 
				
			||||||
			// ファイル一覧取得
 | 
								// ファイル一覧取得
 | 
				
			||||||
			(this as any).api('drive/files', {
 | 
								(this as any).api('drive/files', {
 | 
				
			||||||
				folderId: this.folder ? this.folder.id : null,
 | 
									folderId: this.folder ? this.folder.id : null,
 | 
				
			||||||
 | 
									untilId: this.files[this.files.length - 1].id,
 | 
				
			||||||
				limit: max + 1
 | 
									limit: max + 1
 | 
				
			||||||
			}).then(files => {
 | 
								}).then(files => {
 | 
				
			||||||
				if (files.length == max + 1) {
 | 
									if (files.length == max + 1) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ export default Vue.extend({
 | 
				
			||||||
		style(): any {
 | 
							style(): any {
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 | 
									'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 | 
				
			||||||
				'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url})`
 | 
									'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.thumbnailUrl})`
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,15 +55,15 @@
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<footer>
 | 
							<footer>
 | 
				
			||||||
			<mk-reactions-viewer :note="p"/>
 | 
								<mk-reactions-viewer :note="p"/>
 | 
				
			||||||
			<button @click="reply" title="">
 | 
								<button class="replyButton" @click="reply" title="">
 | 
				
			||||||
				<template v-if="p.reply">%fa:reply-all%</template>
 | 
									<template v-if="p.reply">%fa:reply-all%</template>
 | 
				
			||||||
				<template v-else>%fa:reply%</template>
 | 
									<template v-else>%fa:reply%</template>
 | 
				
			||||||
				<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 | 
									<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
			<button @click="renote" title="%i18n:@renote%">
 | 
								<button class="renoteButton" @click="renote" title="%i18n:@renote%">
 | 
				
			||||||
				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 | 
									%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
 | 
								<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
 | 
				
			||||||
				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 | 
									%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
			<button @click="menu" ref="menuButton">
 | 
								<button @click="menu" ref="menuButton">
 | 
				
			||||||
| 
						 | 
					@ -372,15 +372,24 @@ root(isDark)
 | 
				
			||||||
				cursor pointer
 | 
									cursor pointer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				&:hover
 | 
									&:hover
 | 
				
			||||||
					color isDark ? #9198af : #666
 | 
										color isDark ? #a1a8bf : #444
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&.replyButton:hover
 | 
				
			||||||
 | 
										color #0af
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&.renoteButton:hover
 | 
				
			||||||
 | 
										color #8d0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&.reactionButton:hover
 | 
				
			||||||
 | 
										color #fa0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				> .count
 | 
									> .count
 | 
				
			||||||
					display inline
 | 
										display inline
 | 
				
			||||||
					margin 0 0 0 8px
 | 
										margin 0 0 0 8px
 | 
				
			||||||
					color #999
 | 
										color #999
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				&.reacted
 | 
									&.reacted, &.reacted:hover
 | 
				
			||||||
					color $theme-color
 | 
										color #fa0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	> .replies
 | 
						> .replies
 | 
				
			||||||
		> *
 | 
							> *
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,15 +42,15 @@
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<footer>
 | 
								<footer>
 | 
				
			||||||
				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
 | 
									<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
 | 
				
			||||||
				<button @click="reply" title="%i18n:@reply%">
 | 
									<button class="replyButton" @click="reply" title="%i18n:@reply%">
 | 
				
			||||||
					<template v-if="p.reply">%fa:reply-all%</template>
 | 
										<template v-if="p.reply">%fa:reply-all%</template>
 | 
				
			||||||
					<template v-else>%fa:reply%</template>
 | 
										<template v-else>%fa:reply%</template>
 | 
				
			||||||
					<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 | 
										<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
				<button @click="renote" title="%i18n:@renote%">
 | 
									<button class="renoteButton" @click="renote" title="%i18n:@renote%">
 | 
				
			||||||
					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 | 
										%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
 | 
									<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
 | 
				
			||||||
					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 | 
										%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
				<button @click="menu" ref="menuButton">
 | 
									<button @click="menu" ref="menuButton">
 | 
				
			||||||
| 
						 | 
					@ -487,20 +487,24 @@ root(isDark)
 | 
				
			||||||
					cursor pointer
 | 
										cursor pointer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					&:hover
 | 
										&:hover
 | 
				
			||||||
						color isDark ? #9198af : #666
 | 
											color isDark ? #a1a8bf : #444
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										&.replyButton:hover
 | 
				
			||||||
 | 
											color #0af
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										&.renoteButton:hover
 | 
				
			||||||
 | 
											color #8d0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										&.reactionButton:hover
 | 
				
			||||||
 | 
											color #fa0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					> .count
 | 
										> .count
 | 
				
			||||||
						display inline
 | 
											display inline
 | 
				
			||||||
						margin 0 0 0 8px
 | 
											margin 0 0 0 8px
 | 
				
			||||||
						color #999
 | 
											color #999
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					&.reacted
 | 
										&.reacted, &.reacted:hover
 | 
				
			||||||
						color $theme-color
 | 
											color #fa0
 | 
				
			||||||
 | 
					 | 
				
			||||||
					&:last-child
 | 
					 | 
				
			||||||
						position absolute
 | 
					 | 
				
			||||||
						right 0
 | 
					 | 
				
			||||||
						margin 0
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	> .detail
 | 
						> .detail
 | 
				
			||||||
		padding-top 4px
 | 
							padding-top 4px
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -135,6 +135,12 @@ export default Vue.extend({
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (this.$store.state.settings.showLocalRenotes === false) {
 | 
				
			||||||
 | 
									if (isPureRenote && (note.renote.user.host == null)) {
 | 
				
			||||||
 | 
										return;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			//#endregion
 | 
								//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
 | 
								// 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@
 | 
				
			||||||
			<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
 | 
								<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
 | 
				
			||||||
			<a @click="addVisibleUser">%i18n:@add-visible-user%</a>
 | 
								<a @click="addVisibleUser">%i18n:@add-visible-user%</a>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div class="hashtags" v-if="recentHashtags.length > 0">
 | 
							<div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags">
 | 
				
			||||||
			<b>%i18n:@recent-tags%:</b>
 | 
								<b>%i18n:@recent-tags%:</b>
 | 
				
			||||||
			<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" title="%@click-to-tagging%">#{{ tag }}</a>
 | 
								<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" title="%@click-to-tagging%">#{{ tag }}</a>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@
 | 
				
			||||||
		<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
 | 
							<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
 | 
				
			||||||
			<x-draggable :list="files" :options="{ animation: 150 }">
 | 
								<x-draggable :list="files" :options="{ animation: 150 }">
 | 
				
			||||||
				<div v-for="file in files" :key="file.id">
 | 
									<div v-for="file in files" :key="file.id">
 | 
				
			||||||
					<div class="img" :style="{ backgroundImage: `url(${file.url})` }" :title="file.name"></div>
 | 
										<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="%i18n:@attach-cancel%" alt=""/>
 | 
										<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:@attach-cancel%" alt=""/>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</x-draggable>
 | 
								</x-draggable>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,9 +48,11 @@
 | 
				
			||||||
				<mk-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi" text="%i18n:common.i-like-sushi%"/>
 | 
									<mk-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi" text="%i18n:common.i-like-sushi%"/>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
 | 
								<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
 | 
				
			||||||
 | 
								<mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
 | 
								<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
 | 
								<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
 | 
								<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
 | 
				
			||||||
 | 
								<mk-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes" text="%i18n:@show-local-renotes%"/>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%">
 | 
								<mk-switch v-model="$store.state.settings.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%">
 | 
				
			||||||
				<span>%i18n:@show-maps-desc%</span>
 | 
									<span>%i18n:@show-maps-desc%</span>
 | 
				
			||||||
			</mk-switch>
 | 
								</mk-switch>
 | 
				
			||||||
| 
						 | 
					@ -335,6 +337,12 @@ export default Vue.extend({
 | 
				
			||||||
				value: v
 | 
									value: v
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							onChangeSuggestRecentHashtags(v) {
 | 
				
			||||||
 | 
								this.$store.dispatch('settings/set', {
 | 
				
			||||||
 | 
									key: 'suggestRecentHashtags',
 | 
				
			||||||
 | 
									value: v
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		onChangeShowReplyTarget(v) {
 | 
							onChangeShowReplyTarget(v) {
 | 
				
			||||||
			this.$store.dispatch('settings/set', {
 | 
								this.$store.dispatch('settings/set', {
 | 
				
			||||||
				key: 'showReplyTarget',
 | 
									key: 'showReplyTarget',
 | 
				
			||||||
| 
						 | 
					@ -353,6 +361,12 @@ export default Vue.extend({
 | 
				
			||||||
				value: v
 | 
									value: v
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							onChangeShowLocalRenotes(v) {
 | 
				
			||||||
 | 
								this.$store.dispatch('settings/set', {
 | 
				
			||||||
 | 
									key: 'showLocalRenotes',
 | 
				
			||||||
 | 
									value: v
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		onChangeShowMaps(v) {
 | 
							onChangeShowMaps(v) {
 | 
				
			||||||
			this.$store.dispatch('settings/set', {
 | 
								this.$store.dispatch('settings/set', {
 | 
				
			||||||
				key: 'showMaps',
 | 
									key: 'showMaps',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -100,7 +100,8 @@ export default Vue.extend({
 | 
				
			||||||
					limit: fetchLimit + 1,
 | 
										limit: fetchLimit + 1,
 | 
				
			||||||
					untilDate: this.date ? this.date.getTime() : undefined,
 | 
										untilDate: this.date ? this.date.getTime() : undefined,
 | 
				
			||||||
					includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
										includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
										includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
										includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
				}).then(notes => {
 | 
									}).then(notes => {
 | 
				
			||||||
					if (notes.length == fetchLimit + 1) {
 | 
										if (notes.length == fetchLimit + 1) {
 | 
				
			||||||
						notes.pop();
 | 
											notes.pop();
 | 
				
			||||||
| 
						 | 
					@ -122,7 +123,8 @@ export default Vue.extend({
 | 
				
			||||||
				limit: fetchLimit + 1,
 | 
									limit: fetchLimit + 1,
 | 
				
			||||||
				untilId: (this.$refs.timeline as any).tail().id,
 | 
									untilId: (this.$refs.timeline as any).tail().id,
 | 
				
			||||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
									includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
									includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
									includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			promise.then(notes => {
 | 
								promise.then(notes => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -155,10 +155,15 @@ root(isDark)
 | 
				
			||||||
				max-width 1300px
 | 
									max-width 1300px
 | 
				
			||||||
				margin 0 auto
 | 
									margin 0 auto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									> *
 | 
				
			||||||
 | 
										position absolute
 | 
				
			||||||
 | 
										height 48px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				> .center
 | 
									> .center
 | 
				
			||||||
					margin auto
 | 
										right 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					> .icon
 | 
										> .icon
 | 
				
			||||||
 | 
											margin auto
 | 
				
			||||||
						display block
 | 
											display block
 | 
				
			||||||
						width 48px
 | 
											width 48px
 | 
				
			||||||
						height 48px
 | 
											height 48px
 | 
				
			||||||
| 
						 | 
					@ -169,11 +174,12 @@ root(isDark)
 | 
				
			||||||
						opacity 0.3
 | 
											opacity 0.3
 | 
				
			||||||
						cursor pointer
 | 
											cursor pointer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				> .left
 | 
									> .left,
 | 
				
			||||||
					height 48px
 | 
									> .center
 | 
				
			||||||
 | 
										left 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				> .right
 | 
									> .right
 | 
				
			||||||
					height 48px
 | 
										right 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					> *
 | 
										> *
 | 
				
			||||||
						display inline-block
 | 
											display inline-block
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,8 @@ export default Vue.extend({
 | 
				
			||||||
					listId: this.list.id,
 | 
										listId: this.list.id,
 | 
				
			||||||
					limit: fetchLimit + 1,
 | 
										limit: fetchLimit + 1,
 | 
				
			||||||
					includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
										includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
										includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
										includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
				}).then(notes => {
 | 
									}).then(notes => {
 | 
				
			||||||
					if (notes.length == fetchLimit + 1) {
 | 
										if (notes.length == fetchLimit + 1) {
 | 
				
			||||||
						notes.pop();
 | 
											notes.pop();
 | 
				
			||||||
| 
						 | 
					@ -67,7 +68,8 @@ export default Vue.extend({
 | 
				
			||||||
				limit: fetchLimit + 1,
 | 
									limit: fetchLimit + 1,
 | 
				
			||||||
				untilId: (this.$refs.timeline as any).tail().id,
 | 
									untilId: (this.$refs.timeline as any).tail().id,
 | 
				
			||||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
									includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
									includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
									includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			promise.then(notes => {
 | 
								promise.then(notes => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,13 +10,13 @@
 | 
				
			||||||
		<div class="description">{{ u.description }}</div>
 | 
							<div class="description">{{ u.description }}</div>
 | 
				
			||||||
		<div class="status">
 | 
							<div class="status">
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				<p>%i18n:@notes%</p><a>{{ u.notesCount }}</a>
 | 
									<p>%i18n:@notes%</p><span>{{ u.notesCount }}</span>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				<p>%i18n:@following%</p><a>{{ u.followingCount }}</a>
 | 
									<p>%i18n:@following%</p><span>{{ u.followingCount }}</span>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				<p>%i18n:@followers%</p><a>{{ u.followersCount }}</a>
 | 
									<p>%i18n:@followers%</p><span>{{ u.followersCount }}</span>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<mk-follow-button v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u"/>
 | 
							<mk-follow-button v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u"/>
 | 
				
			||||||
| 
						 | 
					@ -149,7 +149,7 @@ root(isDark)
 | 
				
			||||||
				font-size 0.7em
 | 
									font-size 0.7em
 | 
				
			||||||
				color #aaa
 | 
									color #aaa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			> a
 | 
								> span
 | 
				
			||||||
				font-size 1em
 | 
									font-size 1em
 | 
				
			||||||
				color $theme-color
 | 
									color $theme-color
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/client/app/desktop/views/pages/admin/admin.dashboard.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/client/app/desktop/views/pages/admin/admin.dashboard.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,37 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
						<h1>%i18n:@dashboard%</h1>
 | 
				
			||||||
 | 
						<div v-if="stats">
 | 
				
			||||||
 | 
							<p><b>%i18n:@all-users%</b>: <span>{{ stats.usersCount | number }}</span></p>
 | 
				
			||||||
 | 
							<p><b>%i18n:@original-users%</b>: <span>{{ stats.originalUsersCount | number }}</span></p>
 | 
				
			||||||
 | 
							<p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p>
 | 
				
			||||||
 | 
							<p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								stats: null
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							(this as any).api('stats').then(stats => {
 | 
				
			||||||
 | 
								this.stats = stats;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					h1
 | 
				
			||||||
 | 
						margin 0 0 1em 0
 | 
				
			||||||
 | 
						padding 0 0 8px 0
 | 
				
			||||||
 | 
						font-size 1em
 | 
				
			||||||
 | 
						color #555
 | 
				
			||||||
 | 
						border-bottom solid 1px #eee
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,51 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
						<header>%i18n:@suspend-user%</header>
 | 
				
			||||||
 | 
						<input v-model="username" type="text" class="ui"/>
 | 
				
			||||||
 | 
						<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from "vue";
 | 
				
			||||||
 | 
					import parseAcct from "../../../../../../misc/acct/parse";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								username: null,
 | 
				
			||||||
 | 
								suspending: false
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							async suspendUser() {
 | 
				
			||||||
 | 
								this.suspending = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const user = await (this as any).os.api(
 | 
				
			||||||
 | 
									"users/show",
 | 
				
			||||||
 | 
									parseAcct(this.username)
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								await (this as any).os.api("admin/suspend-user", {
 | 
				
			||||||
 | 
									userId: user.id
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.suspending = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								(this as any).os.apis.dialog({ text: "%i18n:@suspended%" });
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					@import '~const.styl'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					header
 | 
				
			||||||
 | 
						margin 10px 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button
 | 
				
			||||||
 | 
						margin 16px 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,51 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
						<header>%i18n:@unsuspend-user%</header>
 | 
				
			||||||
 | 
						<input v-model="username" type="text" class="ui"/>
 | 
				
			||||||
 | 
						<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from "vue";
 | 
				
			||||||
 | 
					import parseAcct from "../../../../../../misc/acct/parse";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								username: null,
 | 
				
			||||||
 | 
								unsuspending: false
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							async unsuspendUser() {
 | 
				
			||||||
 | 
								this.unsuspending = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const user = await (this as any).os.api(
 | 
				
			||||||
 | 
									"users/show",
 | 
				
			||||||
 | 
									parseAcct(this.username)
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								await (this as any).os.api("admin/unsuspend-user", {
 | 
				
			||||||
 | 
									userId: user.id
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.unsuspending = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								(this as any).os.apis.dialog({ text: "%i18n:@unsuspended%" });
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					@import '~const.styl'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					header
 | 
				
			||||||
 | 
						margin 10px 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button
 | 
				
			||||||
 | 
						margin 16px 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										102
									
								
								src/client/app/desktop/views/pages/admin/admin.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/client/app/desktop/views/pages/admin/admin.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,102 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div class="mk-admin">
 | 
				
			||||||
 | 
						<nav>
 | 
				
			||||||
 | 
							<ul>
 | 
				
			||||||
 | 
								<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li>
 | 
				
			||||||
 | 
								<li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li>
 | 
				
			||||||
 | 
								<!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> -->
 | 
				
			||||||
 | 
								<!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> -->
 | 
				
			||||||
 | 
							</ul>
 | 
				
			||||||
 | 
						</nav>
 | 
				
			||||||
 | 
						<main>
 | 
				
			||||||
 | 
							<div v-if="page == 'dashboard'">
 | 
				
			||||||
 | 
								<x-dashboard/>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div v-if="page == 'users'">
 | 
				
			||||||
 | 
								<x-suspend-user/>
 | 
				
			||||||
 | 
								<x-unsuspend-user/>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div v-if="page == 'drive'"></div>
 | 
				
			||||||
 | 
							<div v-if="page == 'update'"></div>
 | 
				
			||||||
 | 
						</main>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from "vue";
 | 
				
			||||||
 | 
					import XDashboard from "./admin.dashboard.vue";
 | 
				
			||||||
 | 
					import XSuspendUser from "./admin.suspend-user.vue";
 | 
				
			||||||
 | 
					import XUnsuspendUser from "./admin.unsuspend-user.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XDashboard,
 | 
				
			||||||
 | 
							XSuspendUser,
 | 
				
			||||||
 | 
							XUnsuspendUser
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								page: 'dashboard'
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							nav(page: string) {
 | 
				
			||||||
 | 
								this.page = page;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					@import '~const.styl'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mk-admin
 | 
				
			||||||
 | 
						display flex
 | 
				
			||||||
 | 
						height 100%
 | 
				
			||||||
 | 
						margin 32px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> nav
 | 
				
			||||||
 | 
							flex 0 0 250px
 | 
				
			||||||
 | 
							width 100%
 | 
				
			||||||
 | 
							height 100%
 | 
				
			||||||
 | 
							padding 16px 0 0 0
 | 
				
			||||||
 | 
							overflow auto
 | 
				
			||||||
 | 
							border-right solid 1px #ddd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> ul
 | 
				
			||||||
 | 
								list-style none
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> li
 | 
				
			||||||
 | 
									display block
 | 
				
			||||||
 | 
									padding 10px 16px
 | 
				
			||||||
 | 
									margin 0
 | 
				
			||||||
 | 
									color #666
 | 
				
			||||||
 | 
									cursor pointer
 | 
				
			||||||
 | 
									user-select none
 | 
				
			||||||
 | 
									transition margin-left 0.2s ease
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									> [data-fa]
 | 
				
			||||||
 | 
										margin-right 4px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&:hover
 | 
				
			||||||
 | 
										color #555
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&.active
 | 
				
			||||||
 | 
										margin-left 8px
 | 
				
			||||||
 | 
										color $theme-color !important
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> main
 | 
				
			||||||
 | 
							width 100%
 | 
				
			||||||
 | 
							padding 16px 32px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					header
 | 
				
			||||||
 | 
						margin 10px 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button
 | 
				
			||||||
 | 
						margin 16px 0
 | 
				
			||||||
 | 
						position absolute
 | 
				
			||||||
 | 
						right 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -70,7 +70,8 @@ export default Vue.extend({
 | 
				
			||||||
					limit: fetchLimit + 1,
 | 
										limit: fetchLimit + 1,
 | 
				
			||||||
					mediaOnly: this.mediaOnly,
 | 
										mediaOnly: this.mediaOnly,
 | 
				
			||||||
					includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
										includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
										includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
										includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
				}).then(notes => {
 | 
									}).then(notes => {
 | 
				
			||||||
					if (notes.length == fetchLimit + 1) {
 | 
										if (notes.length == fetchLimit + 1) {
 | 
				
			||||||
						notes.pop();
 | 
											notes.pop();
 | 
				
			||||||
| 
						 | 
					@ -91,7 +92,8 @@ export default Vue.extend({
 | 
				
			||||||
				untilId: (this.$refs.timeline as any).tail().id,
 | 
									untilId: (this.$refs.timeline as any).tail().id,
 | 
				
			||||||
				mediaOnly: this.mediaOnly,
 | 
									mediaOnly: this.mediaOnly,
 | 
				
			||||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
									includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
									includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
									includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			promise.then(notes => {
 | 
								promise.then(notes => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -140,6 +140,12 @@ export default Vue.extend({
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (this.$store.state.settings.showLocalRenotes === false) {
 | 
				
			||||||
 | 
									if (isPureRenote && (note.renote.user.host == null)) {
 | 
				
			||||||
 | 
										return;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			//#endregion
 | 
								//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (this.isScrollTop()) {
 | 
								if (this.isScrollTop()) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,7 +98,8 @@ export default Vue.extend({
 | 
				
			||||||
					limit: fetchLimit + 1,
 | 
										limit: fetchLimit + 1,
 | 
				
			||||||
					mediaOnly: this.mediaOnly,
 | 
										mediaOnly: this.mediaOnly,
 | 
				
			||||||
					includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
										includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
										includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
										includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
				}).then(notes => {
 | 
									}).then(notes => {
 | 
				
			||||||
					if (notes.length == fetchLimit + 1) {
 | 
										if (notes.length == fetchLimit + 1) {
 | 
				
			||||||
						notes.pop();
 | 
											notes.pop();
 | 
				
			||||||
| 
						 | 
					@ -119,7 +120,8 @@ export default Vue.extend({
 | 
				
			||||||
				mediaOnly: this.mediaOnly,
 | 
									mediaOnly: this.mediaOnly,
 | 
				
			||||||
				untilId: (this.$refs.timeline as any).tail().id,
 | 
									untilId: (this.$refs.timeline as any).tail().id,
 | 
				
			||||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
									includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
									includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
									includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			promise.then(notes => {
 | 
								promise.then(notes => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -176,6 +176,10 @@ root(isDark)
 | 
				
			||||||
		height 120px
 | 
							height 120px
 | 
				
			||||||
		box-shadow 1px 1px 3px rgba(#000, 0.2)
 | 
							box-shadow 1px 1px 3px rgba(#000, 0.2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> &.cat::before,
 | 
				
			||||||
 | 
							> &.cat::after
 | 
				
			||||||
 | 
								border-width 8px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	> .body
 | 
						> .body
 | 
				
			||||||
		padding 16px 16px 16px 154px
 | 
							padding 16px 16px 16px 154px
 | 
				
			||||||
		color isDark ? #c5ced6 : #555
 | 
							color isDark ? #c5ced6 : #555
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ export default Vue.extend({
 | 
				
			||||||
		thumbnail(): any {
 | 
							thumbnail(): any {
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
 | 
									'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
 | 
				
			||||||
				'background-image': `url(${this.file.url})`
 | 
									'background-image': `url(${this.file.thumbnailUrl})`
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@ export default Vue.extend({
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	computed: {
 | 
						computed: {
 | 
				
			||||||
		style(): any {
 | 
							style(): any {
 | 
				
			||||||
			let url = `url(${this.image.url})`;
 | 
								let url = `url(${this.image.thumbnailUrl})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
 | 
								if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
 | 
				
			||||||
				url = null;
 | 
									url = null;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -139,6 +139,12 @@ export default Vue.extend({
 | 
				
			||||||
					return;
 | 
										return;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (this.$store.state.settings.showLocalRenotes === false) {
 | 
				
			||||||
 | 
									if (isPureRenote && (note.renote.user.host == null)) {
 | 
				
			||||||
 | 
										return;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			//#endregion
 | 
								//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
 | 
								// 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@
 | 
				
			||||||
			<div class="attaches" v-show="files.length != 0">
 | 
								<div class="attaches" v-show="files.length != 0">
 | 
				
			||||||
				<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 | 
									<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 | 
				
			||||||
					<div class="file" v-for="file in files" :key="file.id">
 | 
										<div class="file" v-for="file in files" :key="file.id">
 | 
				
			||||||
						<div class="img" :style="`background-image: url(${file.url})`" @click="detachMedia(file)"></div>
 | 
											<div class="img" :style="`background-image: url(${file.thumbnailUrl})`" @click="detachMedia(file)"></div>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</x-draggable>
 | 
									</x-draggable>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,7 @@
 | 
				
			||||||
			<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
 | 
								<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="hashtags" v-if="recentHashtags.length > 0">
 | 
						<div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags">
 | 
				
			||||||
		<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)">#{{ tag }}</a>
 | 
							<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)">#{{ tag }}</a>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,7 +59,8 @@ export default Vue.extend({
 | 
				
			||||||
					listId: this.list.id,
 | 
										listId: this.list.id,
 | 
				
			||||||
					limit: fetchLimit + 1,
 | 
										limit: fetchLimit + 1,
 | 
				
			||||||
					includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
										includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
										includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
										includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
				}).then(notes => {
 | 
									}).then(notes => {
 | 
				
			||||||
					if (notes.length == fetchLimit + 1) {
 | 
										if (notes.length == fetchLimit + 1) {
 | 
				
			||||||
						notes.pop();
 | 
											notes.pop();
 | 
				
			||||||
| 
						 | 
					@ -82,7 +83,8 @@ export default Vue.extend({
 | 
				
			||||||
				limit: fetchLimit + 1,
 | 
									limit: fetchLimit + 1,
 | 
				
			||||||
				untilId: (this.$refs.timeline as any).tail().id,
 | 
									untilId: (this.$refs.timeline as any).tail().id,
 | 
				
			||||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
									includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
									includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
									includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			promise.then(notes => {
 | 
								promise.then(notes => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,8 @@ export default Vue.extend({
 | 
				
			||||||
					limit: fetchLimit + 1,
 | 
										limit: fetchLimit + 1,
 | 
				
			||||||
					untilDate: this.date ? this.date.getTime() : undefined,
 | 
										untilDate: this.date ? this.date.getTime() : undefined,
 | 
				
			||||||
					includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
										includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
					includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
										includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
										includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
				}).then(notes => {
 | 
									}).then(notes => {
 | 
				
			||||||
					if (notes.length == fetchLimit + 1) {
 | 
										if (notes.length == fetchLimit + 1) {
 | 
				
			||||||
						notes.pop();
 | 
											notes.pop();
 | 
				
			||||||
| 
						 | 
					@ -117,7 +118,8 @@ export default Vue.extend({
 | 
				
			||||||
				limit: fetchLimit + 1,
 | 
									limit: fetchLimit + 1,
 | 
				
			||||||
				untilId: (this.$refs.timeline as any).tail().id,
 | 
									untilId: (this.$refs.timeline as any).tail().id,
 | 
				
			||||||
				includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
									includeMyRenotes: this.$store.state.settings.showMyRenotes,
 | 
				
			||||||
				includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes
 | 
									includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
 | 
				
			||||||
 | 
									includeLocalRenotes: this.$store.state.settings.showLocalRenotes
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			promise.then(notes => {
 | 
								promise.then(notes => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@
 | 
				
			||||||
					<ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch>
 | 
										<ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch>
 | 
				
			||||||
					<ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch>
 | 
										<ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch>
 | 
				
			||||||
					<ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
 | 
										<ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
 | 
				
			||||||
 | 
										<ui-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				<div>
 | 
									<div>
 | 
				
			||||||
| 
						 | 
					@ -221,6 +222,13 @@ export default Vue.extend({
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							onChangeShowLocalRenotes(v) {
 | 
				
			||||||
 | 
								this.$store.dispatch('settings/set', {
 | 
				
			||||||
 | 
									key: 'showLocalRenotes',
 | 
				
			||||||
 | 
									value: v
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		checkForUpdate() {
 | 
							checkForUpdate() {
 | 
				
			||||||
			this.checkingForUpdate = true;
 | 
								this.checkingForUpdate = true;
 | 
				
			||||||
			checkForUpdate((this as any).os, true, true).then(newer => {
 | 
								checkForUpdate((this as any).os, true, true).then(newer => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,7 +61,6 @@ export default Vue.extend({
 | 
				
			||||||
			birthday: null,
 | 
								birthday: null,
 | 
				
			||||||
			avatarId: null,
 | 
								avatarId: null,
 | 
				
			||||||
			bannerId: null,
 | 
								bannerId: null,
 | 
				
			||||||
			isBot: false,
 | 
					 | 
				
			||||||
			isCat: false,
 | 
								isCat: false,
 | 
				
			||||||
			saving: false,
 | 
								saving: false,
 | 
				
			||||||
			avatarUploading: false,
 | 
								avatarUploading: false,
 | 
				
			||||||
| 
						 | 
					@ -77,7 +76,6 @@ export default Vue.extend({
 | 
				
			||||||
		this.birthday = this.$store.state.i.profile.birthday;
 | 
							this.birthday = this.$store.state.i.profile.birthday;
 | 
				
			||||||
		this.avatarId = this.$store.state.i.avatarId;
 | 
							this.avatarId = this.$store.state.i.avatarId;
 | 
				
			||||||
		this.bannerId = this.$store.state.i.bannerId;
 | 
							this.bannerId = this.$store.state.i.bannerId;
 | 
				
			||||||
		this.isBot = this.$store.state.i.isBot;
 | 
					 | 
				
			||||||
		this.isCat = this.$store.state.i.isCat;
 | 
							this.isCat = this.$store.state.i.isCat;
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -136,7 +134,6 @@ export default Vue.extend({
 | 
				
			||||||
				birthday: this.birthday || null,
 | 
									birthday: this.birthday || null,
 | 
				
			||||||
				avatarId: this.avatarId,
 | 
									avatarId: this.avatarId,
 | 
				
			||||||
				bannerId: this.bannerId,
 | 
									bannerId: this.bannerId,
 | 
				
			||||||
				isBot: this.isBot,
 | 
					 | 
				
			||||||
				isCat: this.isCat
 | 
									isCat: this.isCat
 | 
				
			||||||
			}).then(i => {
 | 
								}).then(i => {
 | 
				
			||||||
				this.saving = false;
 | 
									this.saving = false;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,11 +11,13 @@ const defaultSettings = {
 | 
				
			||||||
	fetchOnScroll: true,
 | 
						fetchOnScroll: true,
 | 
				
			||||||
	showMaps: true,
 | 
						showMaps: true,
 | 
				
			||||||
	showPostFormOnTopOfTl: false,
 | 
						showPostFormOnTopOfTl: false,
 | 
				
			||||||
 | 
						suggestRecentHashtags: true,
 | 
				
			||||||
	circleIcons: true,
 | 
						circleIcons: true,
 | 
				
			||||||
	gradientWindowHeader: false,
 | 
						gradientWindowHeader: false,
 | 
				
			||||||
	showReplyTarget: true,
 | 
						showReplyTarget: true,
 | 
				
			||||||
	showMyRenotes: true,
 | 
						showMyRenotes: true,
 | 
				
			||||||
	showRenotedMyNotes: true,
 | 
						showRenotedMyNotes: true,
 | 
				
			||||||
 | 
						showLocalRenotes: true,
 | 
				
			||||||
	loadRemoteMedia: true,
 | 
						loadRemoteMedia: true,
 | 
				
			||||||
	disableViaMobile: false,
 | 
						disableViaMobile: false,
 | 
				
			||||||
	memo: null,
 | 
						memo: null,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								src/client/assets/reactions/rip.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/client/assets/reactions/rip.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 6.6 KiB  | 
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	"copyright": "Copyright (c) 2014-2018 syuilo",
 | 
						"copyright": "Copyright (c) 2014-2018 syuilo",
 | 
				
			||||||
	"themeColor": "#f66e4f",
 | 
						"themeColor": "#f6584f",
 | 
				
			||||||
	"themeColorForeground": "#fff"
 | 
						"themeColorForeground": "#fff"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,11 @@
 | 
				
			||||||
import * as childProcess from 'child_process';
 | 
					import * as childProcess from 'child_process';
 | 
				
			||||||
 | 
					import * as Deque from 'double-ended-queue';
 | 
				
			||||||
import Xev from 'xev';
 | 
					import Xev from 'xev';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ev = new Xev();
 | 
					const ev = new Xev();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function() {
 | 
					export default function() {
 | 
				
			||||||
	const log: any[] = [];
 | 
						const log = new Deque<any>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const p = childProcess.fork(__dirname + '/notes-stats-child.js');
 | 
						const p = childProcess.fork(__dirname + '/notes-stats-child.js');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +16,7 @@ export default function() {
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ev.on('requestNotesStatsLog', id => {
 | 
						ev.on('requestNotesStatsLog', id => {
 | 
				
			||||||
		ev.emit('notesStatsLog:' + id, log);
 | 
							ev.emit('notesStatsLog:' + id, log.toArray());
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	process.on('exit', code => {
 | 
						process.on('exit', code => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import * as os from 'os';
 | 
					import * as os from 'os';
 | 
				
			||||||
import * as sysUtils from 'systeminformation';
 | 
					import * as sysUtils from 'systeminformation';
 | 
				
			||||||
import * as diskusage from 'diskusage';
 | 
					import * as diskusage from 'diskusage';
 | 
				
			||||||
 | 
					import * as Deque from 'double-ended-queue';
 | 
				
			||||||
import Xev from 'xev';
 | 
					import Xev from 'xev';
 | 
				
			||||||
const osUtils = require('os-utils');
 | 
					const osUtils = require('os-utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,10 +13,10 @@ const interval = 1000;
 | 
				
			||||||
 * Report server stats regularly
 | 
					 * Report server stats regularly
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export default function() {
 | 
					export default function() {
 | 
				
			||||||
	const log: any[] = [];
 | 
						const log = new Deque<any>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ev.on('requestServerStatsLog', id => {
 | 
						ev.on('requestServerStatsLog', id => {
 | 
				
			||||||
		ev.emit('serverStatsLog:' + id, log);
 | 
							ev.emit('serverStatsLog:' + id, log.toArray());
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async function tick() {
 | 
						async function tick() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,9 +3,7 @@ import config from '../config';
 | 
				
			||||||
const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
 | 
					const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
 | 
				
			||||||
const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
 | 
					const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const uri = u && p
 | 
					const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
 | 
				
			||||||
	? `mongodb://${u}:${p}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
 | 
					 | 
				
			||||||
	: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * monk
 | 
					 * monk
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								src/index.ts
									
										
									
									
									
								
							
							
						
						
									
										15
									
								
								src/index.ts
									
										
									
									
									
								
							| 
						 | 
					@ -28,7 +28,7 @@ import { Config } from './config/types';
 | 
				
			||||||
const clusterLog = debug('misskey:cluster');
 | 
					const clusterLog = debug('misskey:cluster');
 | 
				
			||||||
const ev = new Xev();
 | 
					const ev = new Xev();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (process.env.NODE_ENV != 'production') {
 | 
					if (process.env.NODE_ENV != 'production' && process.env.DEBUG == null) {
 | 
				
			||||||
	debug.enable('misskey');
 | 
						debug.enable('misskey');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,7 +48,7 @@ main();
 | 
				
			||||||
 * Init process
 | 
					 * Init process
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function main() {
 | 
					function main() {
 | 
				
			||||||
	process.title = `Misskey (${ cluster.isMaster ? 'master' : 'worker' })`;
 | 
						process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (cluster.isMaster || program.disableClustering) {
 | 
						if (cluster.isMaster || program.disableClustering) {
 | 
				
			||||||
		masterMain();
 | 
							masterMain();
 | 
				
			||||||
| 
						 | 
					@ -112,7 +112,7 @@ async function workerMain() {
 | 
				
			||||||
async function init(): Promise<Config> {
 | 
					async function init(): Promise<Config> {
 | 
				
			||||||
	Logger.info('Welcome to Misskey!');
 | 
						Logger.info('Welcome to Misskey!');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	(new Logger('Deps')).info(`Node.js ${process.version}`);
 | 
						new Logger('Deps').info(`Node.js ${process.version}`);
 | 
				
			||||||
	MachineInfo.show();
 | 
						MachineInfo.show();
 | 
				
			||||||
	EnvironmentInfo.show();
 | 
						EnvironmentInfo.show();
 | 
				
			||||||
	new DependencyInfo().showAll();
 | 
						new DependencyInfo().showAll();
 | 
				
			||||||
| 
						 | 
					@ -154,11 +154,10 @@ async function init(): Promise<Config> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function checkMongoDb(config: Config) {
 | 
					function checkMongoDb(config: Config) {
 | 
				
			||||||
	const mongoDBLogger = new Logger('MongoDB');
 | 
						const mongoDBLogger = new Logger('MongoDB');
 | 
				
			||||||
	mongoDBLogger.info(`Host: ${config.mongodb.host}`);
 | 
						const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
 | 
				
			||||||
	mongoDBLogger.info(`Port: ${config.mongodb.port}`);
 | 
						const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
 | 
				
			||||||
	mongoDBLogger.info(`DB: ${config.mongodb.db}`);
 | 
						const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
 | 
				
			||||||
	if (config.mongodb.user) mongoDBLogger.info(`User: ${config.mongodb.user}`);
 | 
						mongoDBLogger.info(`Connecting to ${uri}`);
 | 
				
			||||||
	if (config.mongodb.pass) mongoDBLogger.info(`Pass: ****`);
 | 
					 | 
				
			||||||
	require('./db/mongodb');
 | 
						require('./db/mongodb');
 | 
				
			||||||
	mongoDBLogger.succ('Connectivity confirmed');
 | 
						mongoDBLogger.succ('Connectivity confirmed');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,6 @@ export default function(text: string) {
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		type: 'bold',
 | 
							type: 'bold',
 | 
				
			||||||
		content: bold,
 | 
							content: bold,
 | 
				
			||||||
		bold: bold.substr(2, bold.length - 4)
 | 
							bold: match[1]
 | 
				
			||||||
	} as TextElementBold;
 | 
						} as TextElementBold;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@ export default function(text: string) {
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		type: 'code',
 | 
							type: 'code',
 | 
				
			||||||
		content: code,
 | 
							content: code,
 | 
				
			||||||
		code: code.substr(3, code.length - 6).trim(),
 | 
							code: match[1],
 | 
				
			||||||
		html: genHtml(code.substr(3, code.length - 6).trim())
 | 
							html: genHtml(match[1].trim())
 | 
				
			||||||
	} as TextElementCode;
 | 
						} as TextElementCode;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,12 +9,12 @@ export type TextElementEmoji = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function(text: string) {
 | 
					export default function(text: string) {
 | 
				
			||||||
	const match = text.match(/^:[a-zA-Z0-9+-_]+:/);
 | 
						const match = text.match(/^:([a-zA-Z0-9+-_]+):/);
 | 
				
			||||||
	if (!match) return null;
 | 
						if (!match) return null;
 | 
				
			||||||
	const emoji = match[0];
 | 
						const emoji = match[0];
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		type: 'emoji',
 | 
							type: 'emoji',
 | 
				
			||||||
		content: emoji,
 | 
							content: emoji,
 | 
				
			||||||
		emoji: emoji.substr(1, emoji.length - 2)
 | 
							emoji: match[1]
 | 
				
			||||||
	} as TextElementEmoji;
 | 
						} as TextElementEmoji;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,11 +14,12 @@ export type TextElementInlineCode = {
 | 
				
			||||||
export default function(text: string) {
 | 
					export default function(text: string) {
 | 
				
			||||||
	const match = text.match(/^`(.+?)`/);
 | 
						const match = text.match(/^`(.+?)`/);
 | 
				
			||||||
	if (!match) return null;
 | 
						if (!match) return null;
 | 
				
			||||||
 | 
						if (match[1].includes('´')) return null;
 | 
				
			||||||
	const code = match[0];
 | 
						const code = match[0];
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		type: 'inline-code',
 | 
							type: 'inline-code',
 | 
				
			||||||
		content: code,
 | 
							content: code,
 | 
				
			||||||
		code: code.substr(1, code.length - 2).trim(),
 | 
							code: match[1],
 | 
				
			||||||
		html: genHtml(code.substr(1, code.length - 2).trim())
 | 
							html: genHtml(match[1])
 | 
				
			||||||
	} as TextElementInlineCode;
 | 
						} as TextElementInlineCode;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,6 @@ export default function(text: string) {
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		type: 'quote',
 | 
							type: 'quote',
 | 
				
			||||||
		content: quote,
 | 
							content: quote,
 | 
				
			||||||
		quote: quote.substr(1, quote.length - 2).trim(),
 | 
							quote: match[1].trim(),
 | 
				
			||||||
	} as TextElementQuote;
 | 
						} as TextElementQuote;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ export type TextElementSearch = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function(text: string) {
 | 
					export default function(text: string) {
 | 
				
			||||||
	const match = text.match(/^(.+?) (検索|Search)(\n|$)/i);
 | 
						const match = text.match(/^(.+?)( | )(検索|\[検索\]|Search|\[Search\])(\n|$)/i);
 | 
				
			||||||
	if (!match) return null;
 | 
						if (!match) return null;
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		type: 'search',
 | 
							type: 'search',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ export default function(reaction: string): string {
 | 
				
			||||||
		case 'congrats': return '🎉';
 | 
							case 'congrats': return '🎉';
 | 
				
			||||||
		case 'angry': return '💢';
 | 
							case 'angry': return '💢';
 | 
				
			||||||
		case 'confused': return '😥';
 | 
							case 'confused': return '😥';
 | 
				
			||||||
 | 
							case 'rip': return '😇';
 | 
				
			||||||
		case 'pudding': return '🍮';
 | 
							case 'pudding': return '🍮';
 | 
				
			||||||
		default: return '';
 | 
							default: return '';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ export default App;
 | 
				
			||||||
export type IApp = {
 | 
					export type IApp = {
 | 
				
			||||||
	_id: mongo.ObjectID;
 | 
						_id: mongo.ObjectID;
 | 
				
			||||||
	createdAt: Date;
 | 
						createdAt: Date;
 | 
				
			||||||
	userId: mongo.ObjectID;
 | 
						userId: mongo.ObjectID | null;
 | 
				
			||||||
	secret: string;
 | 
						secret: string;
 | 
				
			||||||
	name: string;
 | 
						name: string;
 | 
				
			||||||
	nameId: string;
 | 
						nameId: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ import DriveFileThumbnail, { deleteDriveFileThumbnail } from './drive-file-thumb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
 | 
					const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
 | 
				
			||||||
DriveFile.createIndex('md5');
 | 
					DriveFile.createIndex('md5');
 | 
				
			||||||
DriveFile.createIndex(['metadata.uri', 'metadata.userId'], { sparse: true, unique: true });
 | 
					DriveFile.createIndex('metadata.uri');
 | 
				
			||||||
export default DriveFile;
 | 
					export default DriveFile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DriveFileChunk = monkDb.get('driveFiles.chunks');
 | 
					export const DriveFileChunk = monkDb.get('driveFiles.chunks');
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,7 @@ export type IMetadata = {
 | 
				
			||||||
	comment: string;
 | 
						comment: string;
 | 
				
			||||||
	uri?: string;
 | 
						uri?: string;
 | 
				
			||||||
	url?: string;
 | 
						url?: string;
 | 
				
			||||||
 | 
						thumbnailUrl?: string;
 | 
				
			||||||
	src?: string;
 | 
						src?: string;
 | 
				
			||||||
	deletedAt?: Date;
 | 
						deletedAt?: Date;
 | 
				
			||||||
	withoutChunks?: boolean;
 | 
						withoutChunks?: boolean;
 | 
				
			||||||
| 
						 | 
					@ -164,6 +165,7 @@ export const pack = (
 | 
				
			||||||
	_target = Object.assign(_target, _file.metadata);
 | 
						_target = Object.assign(_target, _file.metadata);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_target.url = _file.metadata.url ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
 | 
						_target.url = _file.metadata.url ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
 | 
				
			||||||
 | 
						_target.thumbnailUrl = _file.metadata.thumbnailUrl ? _file.metadata.thumbnailUrl : _file.metadata.url ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}?thumbnail`;
 | 
				
			||||||
	_target.isRemote = _file.metadata.isRemote;
 | 
						_target.isRemote = _file.metadata.isRemote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (_target.properties == null) _target.properties = {};
 | 
						if (_target.properties == null) _target.properties = {};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ export const validateReaction = $.str.or([
 | 
				
			||||||
	'congrats',
 | 
						'congrats',
 | 
				
			||||||
	'angry',
 | 
						'angry',
 | 
				
			||||||
	'confused',
 | 
						'confused',
 | 
				
			||||||
 | 
						'rip',
 | 
				
			||||||
	'pudding'
 | 
						'pudding'
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -340,7 +340,7 @@ export const pack = async (
 | 
				
			||||||
	_note = await rap(_note);
 | 
						_note = await rap(_note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (_note.user.isCat && _note.text) {
 | 
						if (_note.user.isCat && _note.text) {
 | 
				
			||||||
		_note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ');
 | 
							_note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (hide) {
 | 
						if (hide) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,11 +40,13 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
 | 
				
			||||||
	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
 | 
						if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
 | 
				
			||||||
		if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
 | 
							if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
 | 
				
			||||||
			visibility = 'home';
 | 
								visibility = 'home';
 | 
				
			||||||
 | 
							} else if (note.to.includes(`${actor.uri}/followers`)) {	// TODO: person.followerと照合するべき?
 | 
				
			||||||
 | 
								visibility = 'followers';
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			visibility = 'specified';
 | 
								visibility = 'specified';
 | 
				
			||||||
			visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
 | 
								visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}	if (activity.cc.length == 0) visibility = 'followers';
 | 
						}
 | 
				
			||||||
	//#endergion
 | 
						//#endergion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await post(actor, {
 | 
						await post(actor, {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,12 +69,13 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
 | 
				
			||||||
	if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
 | 
						if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
 | 
				
			||||||
		if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
 | 
							if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
 | 
				
			||||||
			visibility = 'home';
 | 
								visibility = 'home';
 | 
				
			||||||
 | 
							} else if (note.to.includes(`${actor.uri}/followers`)) {	// TODO: person.followerと照合するべき?
 | 
				
			||||||
 | 
								visibility = 'followers';
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			visibility = 'specified';
 | 
								visibility = 'specified';
 | 
				
			||||||
			visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
 | 
								visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if (note.cc.length == 0) visibility = 'followers';
 | 
					 | 
				
			||||||
	//#endergion
 | 
						//#endergion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 添付メディア
 | 
						// 添付メディア
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,15 @@
 | 
				
			||||||
export default (object: any) => ({
 | 
					import config from '../../../config';
 | 
				
			||||||
 | 
					import { INote } from '../../../models/note';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default (object: any, note: INote) => {
 | 
				
			||||||
 | 
						const attributedTo = `${config.url}/users/${note.userId}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							id: `${config.url}/notes/${note._id}`,
 | 
				
			||||||
		type: 'Announce',
 | 
							type: 'Announce',
 | 
				
			||||||
 | 
							published: note.createdAt.toISOString(),
 | 
				
			||||||
 | 
							to: ['https://www.w3.org/ns/activitystreams#Public'],
 | 
				
			||||||
 | 
							cc: [attributedTo, `${attributedTo}/followers`],
 | 
				
			||||||
		object
 | 
							object
 | 
				
			||||||
});
 | 
						};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,5 +4,5 @@ import { IDriveFile } from '../../../models/drive-file';
 | 
				
			||||||
export default (file: IDriveFile) => ({
 | 
					export default (file: IDriveFile) => ({
 | 
				
			||||||
	type: 'Document',
 | 
						type: 'Document',
 | 
				
			||||||
	mediaType: file.contentType,
 | 
						mediaType: file.contentType,
 | 
				
			||||||
	url: `${config.drive_url}/${file._id}`
 | 
						url: file.metadata.url || `${config.drive_url}/${file._id}`
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								src/remote/activitypub/renderer/follow-user.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/remote/activitypub/renderer/follow-user.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					import config from '../../../config';
 | 
				
			||||||
 | 
					import * as mongo from 'mongodb';
 | 
				
			||||||
 | 
					import User, { isLocalUser } from '../../../models/user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Convert (local|remote)(Follower|Followee)ID to URL
 | 
				
			||||||
 | 
					 * @param id Follower|Followee ID
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default async function renderFollowUser(id: mongo.ObjectID): Promise<any> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const user = await User.findOne({
 | 
				
			||||||
 | 
							_id: id
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,6 @@ import { IDriveFile } from '../../../models/drive-file';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default (file: IDriveFile) => ({
 | 
					export default (file: IDriveFile) => ({
 | 
				
			||||||
	type: 'Image',
 | 
						type: 'Image',
 | 
				
			||||||
	url: `${config.drive_url}/${file._id}`,
 | 
						url: file.metadata.url || `${config.drive_url}/${file._id}`,
 | 
				
			||||||
	sensitive: file.metadata.isSensitive
 | 
						sensitive: file.metadata.isSensitive
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,9 +50,21 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 | 
				
			||||||
		? note.mentionedRemoteUsers.map(x => x.uri)
 | 
							? note.mentionedRemoteUsers.map(x => x.uri)
 | 
				
			||||||
		: [];
 | 
							: [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const cc = ['public', 'home', 'followers'].includes(note.visibility)
 | 
						let to: string[] = [];
 | 
				
			||||||
		? [`${attributedTo}/followers`].concat(mentions)
 | 
						let cc: string[] = [];
 | 
				
			||||||
		: [];
 | 
					
 | 
				
			||||||
 | 
						if (note.visibility == 'public') {
 | 
				
			||||||
 | 
							to = ['https://www.w3.org/ns/activitystreams#Public'];
 | 
				
			||||||
 | 
							cc = [`${attributedTo}/followers`].concat(mentions);
 | 
				
			||||||
 | 
						} else if (note.visibility == 'home') {
 | 
				
			||||||
 | 
							to = [`${attributedTo}/followers`];
 | 
				
			||||||
 | 
							cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions);
 | 
				
			||||||
 | 
						} else if (note.visibility == 'followers') {
 | 
				
			||||||
 | 
							to = [`${attributedTo}/followers`];
 | 
				
			||||||
 | 
							cc = mentions;
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							to = mentions;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const mentionedUsers = note.mentions ? await User.find({
 | 
						const mentionedUsers = note.mentions ? await User.find({
 | 
				
			||||||
		_id: {
 | 
							_id: {
 | 
				
			||||||
| 
						 | 
					@ -74,7 +86,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 | 
				
			||||||
		summary: note.cw,
 | 
							summary: note.cw,
 | 
				
			||||||
		content: toHtml(note),
 | 
							content: toHtml(note),
 | 
				
			||||||
		published: note.createdAt.toISOString(),
 | 
							published: note.createdAt.toISOString(),
 | 
				
			||||||
		to: 'https://www.w3.org/ns/activitystreams#Public',
 | 
							to,
 | 
				
			||||||
		cc,
 | 
							cc,
 | 
				
			||||||
		inReplyTo,
 | 
							inReplyTo,
 | 
				
			||||||
		attachment: (await promisedFiles).map(renderDocument),
 | 
							attachment: (await promisedFiles).map(renderDocument),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/remote/activitypub/renderer/ordered-collection-page.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/remote/activitypub/renderer/ordered-collection-page.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Render OrderedCollectionPage
 | 
				
			||||||
 | 
					 * @param id URL of self
 | 
				
			||||||
 | 
					 * @param totalItems Number of total items
 | 
				
			||||||
 | 
					 * @param orderedItems Items
 | 
				
			||||||
 | 
					 * @param partOf URL of base
 | 
				
			||||||
 | 
					 * @param prev URL of prev page (optional)
 | 
				
			||||||
 | 
					 * @param next URL of next page (optional)
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev: string, next: string) {
 | 
				
			||||||
 | 
						const page = {
 | 
				
			||||||
 | 
							id,
 | 
				
			||||||
 | 
							partOf,
 | 
				
			||||||
 | 
							type: 'OrderedCollectionPage',
 | 
				
			||||||
 | 
							totalItems,
 | 
				
			||||||
 | 
							orderedItems
 | 
				
			||||||
 | 
						} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (prev) page.prev = prev;
 | 
				
			||||||
 | 
						if (next) page.next = next;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return page;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,19 @@
 | 
				
			||||||
export default (id: string, totalItems: any, orderedItems: any) => ({
 | 
					/**
 | 
				
			||||||
 | 
					 * Render OrderedCollection
 | 
				
			||||||
 | 
					 * @param id URL of self
 | 
				
			||||||
 | 
					 * @param totalItems Total number of items
 | 
				
			||||||
 | 
					 * @param first URL of first page (optional)
 | 
				
			||||||
 | 
					 * @param last URL of last page (optional)
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default function(id: string, totalItems: any, first: string, last: string) {
 | 
				
			||||||
 | 
						const page: any = {
 | 
				
			||||||
		id,
 | 
							id,
 | 
				
			||||||
		type: 'OrderedCollection',
 | 
							type: 'OrderedCollection',
 | 
				
			||||||
		totalItems,
 | 
							totalItems,
 | 
				
			||||||
	orderedItems
 | 
						};
 | 
				
			||||||
});
 | 
					
 | 
				
			||||||
 | 
						if (first) page.first = first;
 | 
				
			||||||
 | 
						if (last) page.last = last;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return page;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,8 @@ export default async (user: ILocalUser) => {
 | 
				
			||||||
		id,
 | 
							id,
 | 
				
			||||||
		inbox: `${id}/inbox`,
 | 
							inbox: `${id}/inbox`,
 | 
				
			||||||
		outbox: `${id}/outbox`,
 | 
							outbox: `${id}/outbox`,
 | 
				
			||||||
 | 
							followers: `${id}/followers`,
 | 
				
			||||||
 | 
							following: `${id}/following`,
 | 
				
			||||||
		sharedInbox: `${config.url}/inbox`,
 | 
							sharedInbox: `${config.url}/inbox`,
 | 
				
			||||||
		url: `${config.url}/@${user.username}`,
 | 
							url: `${config.url}/@${user.username}`,
 | 
				
			||||||
		preferredUsername: user.username,
 | 
							preferredUsername: user.username,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,8 +10,9 @@ import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
 | 
				
			||||||
import renderNote from '../remote/activitypub/renderer/note';
 | 
					import renderNote from '../remote/activitypub/renderer/note';
 | 
				
			||||||
import renderKey from '../remote/activitypub/renderer/key';
 | 
					import renderKey from '../remote/activitypub/renderer/key';
 | 
				
			||||||
import renderPerson from '../remote/activitypub/renderer/person';
 | 
					import renderPerson from '../remote/activitypub/renderer/person';
 | 
				
			||||||
import renderOrderedCollection from '../remote/activitypub/renderer/ordered-collection';
 | 
					import Outbox from './activitypub/outbox';
 | 
				
			||||||
import config from '../config';
 | 
					import Followers from './activitypub/followers';
 | 
				
			||||||
 | 
					import Following from './activitypub/following';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Init router
 | 
					// Init router
 | 
				
			||||||
const router = new Router();
 | 
					const router = new Router();
 | 
				
			||||||
| 
						 | 
					@ -64,30 +65,14 @@ router.get('/notes/:note', async (ctx, next) => {
 | 
				
			||||||
	ctx.body = pack(await renderNote(note));
 | 
						ctx.body = pack(await renderNote(note));
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// outbot
 | 
					// outbox
 | 
				
			||||||
router.get('/users/:user/outbox', async ctx => {
 | 
					router.get('/users/:user/outbox', Outbox);
 | 
				
			||||||
	const userId = new mongo.ObjectID(ctx.params.user);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const user = await User.findOne({
 | 
					// followers
 | 
				
			||||||
		_id: userId,
 | 
					router.get('/users/:user/followers', Followers);
 | 
				
			||||||
		host: null
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (user === null) {
 | 
					// following
 | 
				
			||||||
		ctx.status = 404;
 | 
					router.get('/users/:user/following', Following);
 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const notes = await Note.find({ userId: user._id }, {
 | 
					 | 
				
			||||||
		limit: 10,
 | 
					 | 
				
			||||||
		sort: { _id: -1 }
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
 | 
					 | 
				
			||||||
	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ctx.body = pack(rendered);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// publickey
 | 
					// publickey
 | 
				
			||||||
router.get('/users/:user/publickey', async ctx => {
 | 
					router.get('/users/:user/publickey', async ctx => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										80
									
								
								src/server/activitypub/followers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/server/activitypub/followers.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,80 @@
 | 
				
			||||||
 | 
					import * as mongo from 'mongodb';
 | 
				
			||||||
 | 
					import * as Koa from 'koa';
 | 
				
			||||||
 | 
					import config from '../../config';
 | 
				
			||||||
 | 
					import $ from 'cafy'; import ID from '../../misc/cafy-id';
 | 
				
			||||||
 | 
					import User from '../../models/user';
 | 
				
			||||||
 | 
					import Following from '../../models/following';
 | 
				
			||||||
 | 
					import pack from '../../remote/activitypub/renderer';
 | 
				
			||||||
 | 
					import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 | 
				
			||||||
 | 
					import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
 | 
				
			||||||
 | 
					import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async (ctx: Koa.Context) => {
 | 
				
			||||||
 | 
						const userId = new mongo.ObjectID(ctx.params.user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get 'cursor' parameter
 | 
				
			||||||
 | 
						const [cursor = null, cursorErr] = $.type(ID).optional.get(ctx.request.query.cursor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get 'page' parameter
 | 
				
			||||||
 | 
						const pageErr = !$.str.optional.or(['true', 'false']).ok(ctx.request.query.page);
 | 
				
			||||||
 | 
						const page: boolean = ctx.request.query.page === 'true';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Validate parameters
 | 
				
			||||||
 | 
						if (cursorErr || pageErr) {
 | 
				
			||||||
 | 
							ctx.status = 400;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify user
 | 
				
			||||||
 | 
						const user = await User.findOne({
 | 
				
			||||||
 | 
							_id: userId,
 | 
				
			||||||
 | 
							host: null
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (user === null) {
 | 
				
			||||||
 | 
							ctx.status = 404;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const limit = 10;
 | 
				
			||||||
 | 
						const partOf = `${config.url}/users/${userId}/followers`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (page) {
 | 
				
			||||||
 | 
							// Construct query
 | 
				
			||||||
 | 
							const query = {
 | 
				
			||||||
 | 
								followeeId: user._id
 | 
				
			||||||
 | 
							} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// カーソルが指定されている場合
 | 
				
			||||||
 | 
							if (cursor) {
 | 
				
			||||||
 | 
								query._id = {
 | 
				
			||||||
 | 
									$lt: cursor
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get followers
 | 
				
			||||||
 | 
							const followings = await Following
 | 
				
			||||||
 | 
								.find(query, {
 | 
				
			||||||
 | 
									limit: limit + 1,
 | 
				
			||||||
 | 
									sort: { _id: -1 }
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 「次のページ」があるかどうか
 | 
				
			||||||
 | 
							const inStock = followings.length === limit + 1;
 | 
				
			||||||
 | 
							if (inStock) followings.pop();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const renderedFollowers = await Promise.all(followings.map(following => renderFollowUser(following.followerId)));
 | 
				
			||||||
 | 
							const rendered = renderOrderedCollectionPage(
 | 
				
			||||||
 | 
								`${partOf}?page=true${cursor ? `&cursor=${cursor}` : ''}`,
 | 
				
			||||||
 | 
								user.followersCount, renderedFollowers, partOf,
 | 
				
			||||||
 | 
								null,
 | 
				
			||||||
 | 
								inStock ? `${partOf}?page=true&cursor=${followings[followings.length - 1]._id}` : null
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ctx.body = pack(rendered);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// index page
 | 
				
			||||||
 | 
							const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`, null);
 | 
				
			||||||
 | 
							ctx.body = pack(rendered);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										80
									
								
								src/server/activitypub/following.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/server/activitypub/following.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,80 @@
 | 
				
			||||||
 | 
					import * as mongo from 'mongodb';
 | 
				
			||||||
 | 
					import * as Koa from 'koa';
 | 
				
			||||||
 | 
					import config from '../../config';
 | 
				
			||||||
 | 
					import $ from 'cafy'; import ID from '../../misc/cafy-id';
 | 
				
			||||||
 | 
					import User from '../../models/user';
 | 
				
			||||||
 | 
					import Following from '../../models/following';
 | 
				
			||||||
 | 
					import pack from '../../remote/activitypub/renderer';
 | 
				
			||||||
 | 
					import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 | 
				
			||||||
 | 
					import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
 | 
				
			||||||
 | 
					import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async (ctx: Koa.Context) => {
 | 
				
			||||||
 | 
						const userId = new mongo.ObjectID(ctx.params.user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get 'cursor' parameter
 | 
				
			||||||
 | 
						const [cursor = null, cursorErr] = $.type(ID).optional.get(ctx.request.query.cursor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get 'page' parameter
 | 
				
			||||||
 | 
						const pageErr = !$.str.optional.or(['true', 'false']).ok(ctx.request.query.page);
 | 
				
			||||||
 | 
						const page: boolean = ctx.request.query.page === 'true';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Validate parameters
 | 
				
			||||||
 | 
						if (cursorErr || pageErr) {
 | 
				
			||||||
 | 
							ctx.status = 400;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify user
 | 
				
			||||||
 | 
						const user = await User.findOne({
 | 
				
			||||||
 | 
							_id: userId,
 | 
				
			||||||
 | 
							host: null
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (user === null) {
 | 
				
			||||||
 | 
							ctx.status = 404;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const limit = 10;
 | 
				
			||||||
 | 
						const partOf = `${config.url}/users/${userId}/following`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (page) {
 | 
				
			||||||
 | 
							// Construct query
 | 
				
			||||||
 | 
							const query = {
 | 
				
			||||||
 | 
								followerId: user._id
 | 
				
			||||||
 | 
							} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// カーソルが指定されている場合
 | 
				
			||||||
 | 
							if (cursor) {
 | 
				
			||||||
 | 
								query._id = {
 | 
				
			||||||
 | 
									$lt: cursor
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get followings
 | 
				
			||||||
 | 
							const followings = await Following
 | 
				
			||||||
 | 
								.find(query, {
 | 
				
			||||||
 | 
									limit: limit + 1,
 | 
				
			||||||
 | 
									sort: { _id: -1 }
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 「次のページ」があるかどうか
 | 
				
			||||||
 | 
							const inStock = followings.length === limit + 1;
 | 
				
			||||||
 | 
							if (inStock) followings.pop();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const renderedFollowees = await Promise.all(followings.map(following => renderFollowUser(following.followeeId)));
 | 
				
			||||||
 | 
							const rendered = renderOrderedCollectionPage(
 | 
				
			||||||
 | 
								`${partOf}?page=true${cursor ? `&cursor=${cursor}` : ''}`,
 | 
				
			||||||
 | 
								user.followingCount, renderedFollowees, partOf,
 | 
				
			||||||
 | 
								null,
 | 
				
			||||||
 | 
								inStock ? `${partOf}?page=true&cursor=${followings[followings.length - 1]._id}` : null
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ctx.body = pack(rendered);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// index page
 | 
				
			||||||
 | 
							const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`, null);
 | 
				
			||||||
 | 
							ctx.body = pack(rendered);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										103
									
								
								src/server/activitypub/outbox.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/server/activitypub/outbox.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,103 @@
 | 
				
			||||||
 | 
					import * as mongo from 'mongodb';
 | 
				
			||||||
 | 
					import * as Koa from 'koa';
 | 
				
			||||||
 | 
					import config from '../../config';
 | 
				
			||||||
 | 
					import $ from 'cafy'; import ID from '../../misc/cafy-id';
 | 
				
			||||||
 | 
					import User from '../../models/user';
 | 
				
			||||||
 | 
					import pack from '../../remote/activitypub/renderer';
 | 
				
			||||||
 | 
					import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
 | 
				
			||||||
 | 
					import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Note from '../../models/note';
 | 
				
			||||||
 | 
					import renderNote from '../../remote/activitypub/renderer/note';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async (ctx: Koa.Context) => {
 | 
				
			||||||
 | 
						const userId = new mongo.ObjectID(ctx.params.user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get 'sinceId' parameter
 | 
				
			||||||
 | 
						const [sinceId, sinceIdErr] = $.type(ID).optional.get(ctx.request.query.since_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get 'untilId' parameter
 | 
				
			||||||
 | 
						const [untilId, untilIdErr] = $.type(ID).optional.get(ctx.request.query.until_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get 'page' parameter
 | 
				
			||||||
 | 
						const pageErr = !$.str.optional.or(['true', 'false']).ok(ctx.request.query.page);
 | 
				
			||||||
 | 
						const page: boolean = ctx.request.query.page === 'true';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Validate parameters
 | 
				
			||||||
 | 
						if (sinceIdErr || untilIdErr || pageErr || [sinceId, untilId].filter(x => x != null).length > 1) {
 | 
				
			||||||
 | 
							ctx.status = 400;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify user
 | 
				
			||||||
 | 
						const user = await User.findOne({
 | 
				
			||||||
 | 
							_id: userId,
 | 
				
			||||||
 | 
							host: null
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (user === null) {
 | 
				
			||||||
 | 
							ctx.status = 404;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const limit = 20;
 | 
				
			||||||
 | 
						const partOf = `${config.url}/users/${userId}/outbox`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (page) {
 | 
				
			||||||
 | 
							//#region Construct query
 | 
				
			||||||
 | 
							const sort = {
 | 
				
			||||||
 | 
								_id: -1
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const query = {
 | 
				
			||||||
 | 
								userId: user._id,
 | 
				
			||||||
 | 
								$and: [{
 | 
				
			||||||
 | 
									$or: [ { visibility: 'public' }, { visibility: 'home' } ]
 | 
				
			||||||
 | 
								}, { // exclude renote, but include quote
 | 
				
			||||||
 | 
									$or: [{
 | 
				
			||||||
 | 
										text: { $ne: null }
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										mediaIds: { $ne: [] }
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								}]
 | 
				
			||||||
 | 
							} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (sinceId) {
 | 
				
			||||||
 | 
								sort._id = 1;
 | 
				
			||||||
 | 
								query._id = {
 | 
				
			||||||
 | 
									$gt: sinceId
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							} else if (untilId) {
 | 
				
			||||||
 | 
								query._id = {
 | 
				
			||||||
 | 
									$lt: untilId
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Issue query
 | 
				
			||||||
 | 
							const notes = await Note
 | 
				
			||||||
 | 
								.find(query, {
 | 
				
			||||||
 | 
									limit: limit,
 | 
				
			||||||
 | 
									sort: sort
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (sinceId) notes.reverse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
 | 
				
			||||||
 | 
							const rendered = renderOrderedCollectionPage(
 | 
				
			||||||
 | 
								`${partOf}?page=true${sinceId ? `&since_id=${sinceId}` : ''}${untilId ? `&until_id=${untilId}` : ''}`,
 | 
				
			||||||
 | 
								user.notesCount, renderedNotes, partOf,
 | 
				
			||||||
 | 
								notes.length > 0 ? `${partOf}?page=true&since_id=${notes[0]._id}` : null,
 | 
				
			||||||
 | 
								notes.length > 0 ? `${partOf}?page=true&until_id=${notes[notes.length - 1]._id}` : null
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ctx.body = pack(rendered);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// index page
 | 
				
			||||||
 | 
							const rendered = renderOrderedCollection(partOf, user.notesCount,
 | 
				
			||||||
 | 
								`${partOf}?page=true`,
 | 
				
			||||||
 | 
								`${partOf}?page=true&since_id=000000000000000000000000`
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							ctx.body = pack(rendered);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import { performance } from 'perf_hooks';
 | 
					import { performance } from 'perf_hooks';
 | 
				
			||||||
import limitter from './limitter';
 | 
					import limitter from './limitter';
 | 
				
			||||||
import { IUser } from '../../models/user';
 | 
					import { IUser, isLocalUser } from '../../models/user';
 | 
				
			||||||
import { IApp } from '../../models/app';
 | 
					import { IApp } from '../../models/app';
 | 
				
			||||||
import endpoints from './endpoints';
 | 
					import endpoints from './endpoints';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,10 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any)
 | 
				
			||||||
		return rej('YOUR_ACCOUNT_HAS_BEEN_SUSPENDED');
 | 
							return rej('YOUR_ACCOUNT_HAS_BEEN_SUSPENDED');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (ep.meta.requireAdmin && !(isLocalUser(user) && user.isAdmin)) {
 | 
				
			||||||
 | 
							return rej('YOU_ARE_NOT_ADMIN');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (app && ep.meta.kind) {
 | 
						if (app && ep.meta.kind) {
 | 
				
			||||||
		if (!app.permission.some(p => p === ep.meta.kind)) {
 | 
							if (!app.permission.some(p => p === ep.meta.kind)) {
 | 
				
			||||||
			return rej('PERMISSION_DENIED');
 | 
								return rej('PERMISSION_DENIED');
 | 
				
			||||||
| 
						 | 
					@ -53,7 +57,7 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any)
 | 
				
			||||||
		const time = after - before;
 | 
							const time = after - before;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (time > 1000) {
 | 
							if (time > 1000) {
 | 
				
			||||||
			console.warn(`SLOW API CALL DETECTED: ${ep.name} (${ time }ms)`);
 | 
								console.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} catch (e) {
 | 
						} catch (e) {
 | 
				
			||||||
		rej(e);
 | 
							rej(e);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,11 @@ export interface IEndpointMeta {
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	requireCredential?: boolean;
 | 
						requireCredential?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 管理者のみ使えるエンドポイントか否か
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						requireAdmin?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * エンドポイントのリミテーションに関するやつ
 | 
						 * エンドポイントのリミテーションに関するやつ
 | 
				
			||||||
	 * 省略した場合はリミテーションは無いものとして解釈されます。
 | 
						 * 省略した場合はリミテーションは無いものとして解釈されます。
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/server/api/endpoints/admin/suspend-user.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/server/api/endpoints/admin/suspend-user.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import ID from '../../../../misc/cafy-id';
 | 
				
			||||||
 | 
					import getParams from '../../get-params';
 | 
				
			||||||
 | 
					import User from '../../../../models/user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
					  desc: {
 | 
				
			||||||
 | 
					    ja: '指定したユーザーを凍結します。',
 | 
				
			||||||
 | 
					    en: 'Suspend a user.'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  requireCredential: true,
 | 
				
			||||||
 | 
					  requireAdmin: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  params: {
 | 
				
			||||||
 | 
					    userId: $.type(ID).note({
 | 
				
			||||||
 | 
					      desc: {
 | 
				
			||||||
 | 
					        ja: '対象のユーザーID',
 | 
				
			||||||
 | 
					        en: 'The user ID which you want to suspend'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default (params: any) => new Promise(async (res, rej) => {
 | 
				
			||||||
 | 
					  const [ps, psErr] = getParams(meta, params);
 | 
				
			||||||
 | 
					  if (psErr) return rej(psErr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const user = await User.findOne({
 | 
				
			||||||
 | 
					    _id: ps.userId
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (user == null) {
 | 
				
			||||||
 | 
					    return rej('user not found');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await User.findOneAndUpdate({
 | 
				
			||||||
 | 
					    _id: user._id
 | 
				
			||||||
 | 
					  }, {
 | 
				
			||||||
 | 
					      $set: {
 | 
				
			||||||
 | 
					        isSuspended: true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  res();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/server/api/endpoints/admin/unsuspend-user.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/server/api/endpoints/admin/unsuspend-user.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import ID from '../../../../misc/cafy-id';
 | 
				
			||||||
 | 
					import getParams from '../../get-params';
 | 
				
			||||||
 | 
					import User from '../../../../models/user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						desc: {
 | 
				
			||||||
 | 
							ja: '指定したユーザーの凍結を解除します。',
 | 
				
			||||||
 | 
							en: 'Unsuspend a user.'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true,
 | 
				
			||||||
 | 
						requireAdmin: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							userId: $.type(ID).note({
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: '対象のユーザーID',
 | 
				
			||||||
 | 
									en: 'The user ID which you want to unsuspend'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default (params: any) => new Promise(async (res, rej) => {
 | 
				
			||||||
 | 
						const [ps, psErr] = getParams(meta, params);
 | 
				
			||||||
 | 
						if (psErr) return rej(psErr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const user = await User.findOne({
 | 
				
			||||||
 | 
							_id: ps.userId
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (user == null) {
 | 
				
			||||||
 | 
							return rej('user not found');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await User.findOneAndUpdate({
 | 
				
			||||||
 | 
							_id: user._id
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
								$set: {
 | 
				
			||||||
 | 
									isSuspended: false
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import App, { isValidNameId, pack } from '../../../../models/app';
 | 
				
			||||||
import { ILocalUser } from '../../../../models/user';
 | 
					import { ILocalUser } from '../../../../models/user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const meta = {
 | 
					export const meta = {
 | 
				
			||||||
	requireCredential: true
 | 
						requireCredential: false
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,7 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 | 
				
			||||||
	// Create account
 | 
						// Create account
 | 
				
			||||||
	const app = await App.insert({
 | 
						const app = await App.insert({
 | 
				
			||||||
		createdAt: new Date(),
 | 
							createdAt: new Date(),
 | 
				
			||||||
		userId: user._id,
 | 
							userId: user && user._id,
 | 
				
			||||||
		name: name,
 | 
							name: name,
 | 
				
			||||||
		nameId: nameId,
 | 
							nameId: nameId,
 | 
				
			||||||
		nameIdLower: nameId.toLowerCase(),
 | 
							nameIdLower: nameId.toLowerCase(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,6 +40,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Serialize
 | 
						// Serialize
 | 
				
			||||||
	res(await Promise.all(history.map(async h =>
 | 
						res(await Promise.all(history.map(h => pack(h.messageId, user))));
 | 
				
			||||||
		await pack(h.messageId, user))));
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										43
									
								
								src/server/api/endpoints/messaging/messages/read.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/server/api/endpoints/messaging/messages/read.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
 | 
				
			||||||
 | 
					import Message from '../../../../../models/messaging-message';
 | 
				
			||||||
 | 
					import { ILocalUser } from '../../../../../models/user';
 | 
				
			||||||
 | 
					import read from '../../../common/read-messaging-message';
 | 
				
			||||||
 | 
					import getParams from '../../../get-params';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						desc: {
 | 
				
			||||||
 | 
							ja: '指定した自分宛てのメッセージを既読にします。',
 | 
				
			||||||
 | 
							en: 'Mark as read a message of messaging.'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						kind: 'messaging-write',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							messageId: $.type(ID).note({
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: '既読にするメッセージのID',
 | 
				
			||||||
 | 
									en: 'The ID of a message that you want to mark as read'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
 | 
				
			||||||
 | 
						const [ps, psErr] = getParams(meta, params);
 | 
				
			||||||
 | 
						if (psErr) throw psErr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const message = await Message.findOne({
 | 
				
			||||||
 | 
							_id: ps.messageId,
 | 
				
			||||||
 | 
							recipientId: user._id
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (message == null) {
 | 
				
			||||||
 | 
							return rej('message not found');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						read(user._id, message.userId, message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -59,6 +59,13 @@ export const meta = {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							includeLocalRenotes: $.bool.optional.note({
 | 
				
			||||||
 | 
								default: true,
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: 'Renoteされたローカルの投稿を含めるかどうか'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		mediaOnly: $.bool.optional.note({
 | 
							mediaOnly: $.bool.optional.note({
 | 
				
			||||||
			desc: {
 | 
								desc: {
 | 
				
			||||||
				ja: 'true にすると、メディアが添付された投稿だけ取得します'
 | 
									ja: 'true にすると、メディアが添付された投稿だけ取得します'
 | 
				
			||||||
| 
						 | 
					@ -180,6 +187,22 @@ export default async (params: any, user: ILocalUser) => {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (ps.includeLocalRenotes === false) {
 | 
				
			||||||
 | 
							query.$and.push({
 | 
				
			||||||
 | 
								$or: [{
 | 
				
			||||||
 | 
									'_renote.user.host': { $ne: null }
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									renoteId: null
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									text: { $ne: null }
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									mediaIds: { $ne: [] }
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									poll: { $ne: null }
 | 
				
			||||||
 | 
								}]
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (ps.mediaOnly) {
 | 
						if (ps.mediaOnly) {
 | 
				
			||||||
		query.$and.push({
 | 
							query.$and.push({
 | 
				
			||||||
			mediaIds: { $exists: true, $ne: [] }
 | 
								mediaIds: { $exists: true, $ne: [] }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,8 @@ import getParams from '../../get-params';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const meta = {
 | 
					export const meta = {
 | 
				
			||||||
	desc: {
 | 
						desc: {
 | 
				
			||||||
		ja: 'タイムラインを取得します。'
 | 
							ja: 'タイムラインを取得します。',
 | 
				
			||||||
 | 
							en: 'Get timeline of myself.'
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	requireCredential: true,
 | 
						requireCredential: true,
 | 
				
			||||||
| 
						 | 
					@ -59,6 +60,13 @@ export const meta = {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							includeLocalRenotes: $.bool.optional.note({
 | 
				
			||||||
 | 
								default: true,
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: 'Renoteされたローカルの投稿を含めるかどうか'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		mediaOnly: $.bool.optional.note({
 | 
							mediaOnly: $.bool.optional.note({
 | 
				
			||||||
			desc: {
 | 
								desc: {
 | 
				
			||||||
				ja: 'true にすると、メディアが添付された投稿だけ取得します'
 | 
									ja: 'true にすると、メディアが添付された投稿だけ取得します'
 | 
				
			||||||
| 
						 | 
					@ -67,9 +75,6 @@ export const meta = {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Get timeline of myself
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export default async (params: any, user: ILocalUser) => {
 | 
					export default async (params: any, user: ILocalUser) => {
 | 
				
			||||||
	const [ps, psErr] = getParams(meta, params);
 | 
						const [ps, psErr] = getParams(meta, params);
 | 
				
			||||||
	if (psErr) throw psErr;
 | 
						if (psErr) throw psErr;
 | 
				
			||||||
| 
						 | 
					@ -172,6 +177,22 @@ export default async (params: any, user: ILocalUser) => {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (ps.includeLocalRenotes === false) {
 | 
				
			||||||
 | 
							query.$and.push({
 | 
				
			||||||
 | 
								$or: [{
 | 
				
			||||||
 | 
									'_renote.user.host': { $ne: null }
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									renoteId: null
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									text: { $ne: null }
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									mediaIds: { $ne: [] }
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									poll: { $ne: null }
 | 
				
			||||||
 | 
								}]
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (ps.mediaOnly) {
 | 
						if (ps.mediaOnly) {
 | 
				
			||||||
		query.$and.push({
 | 
							query.$and.push({
 | 
				
			||||||
			mediaIds: { $exists: true, $ne: [] }
 | 
								mediaIds: { $exists: true, $ne: [] }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import Mute from '../../../../models/mute';
 | 
				
			||||||
import { pack } from '../../../../models/note';
 | 
					import { pack } from '../../../../models/note';
 | 
				
			||||||
import UserList from '../../../../models/user-list';
 | 
					import UserList from '../../../../models/user-list';
 | 
				
			||||||
import { ILocalUser } from '../../../../models/user';
 | 
					import { ILocalUser } from '../../../../models/user';
 | 
				
			||||||
 | 
					import getParams from '../../get-params';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const meta = {
 | 
					export const meta = {
 | 
				
			||||||
	desc: {
 | 
						desc: {
 | 
				
			||||||
| 
						 | 
					@ -11,56 +12,84 @@ export const meta = {
 | 
				
			||||||
		en: 'Get timeline of a user list.'
 | 
							en: 'Get timeline of a user list.'
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	requireCredential: true
 | 
						requireCredential: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							listId: $.type(ID).note({
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: 'リストのID'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							limit: $.num.optional.range(1, 100).note({
 | 
				
			||||||
 | 
								default: 10,
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: '最大数'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							sinceId: $.type(ID).optional.note({
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: '指定すると、この投稿を基点としてより新しい投稿を取得します'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							untilId: $.type(ID).optional.note({
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: '指定すると、この投稿を基点としてより古い投稿を取得します'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							sinceDate: $.num.optional.note({
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							untilDate: $.num.optional.note({
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							includeMyRenotes: $.bool.optional.note({
 | 
				
			||||||
 | 
								default: true,
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: '自分の行ったRenoteを含めるかどうか'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							includeRenotedMyNotes: $.bool.optional.note({
 | 
				
			||||||
 | 
								default: true,
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: 'Renoteされた自分の投稿を含めるかどうか'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							includeLocalRenotes: $.bool.optional.note({
 | 
				
			||||||
 | 
								default: true,
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: 'Renoteされたローカルの投稿を含めるかどうか'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							mediaOnly: $.bool.optional.note({
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									ja: 'true にすると、メディアが添付された投稿だけ取得します'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default async (params: any, user: ILocalUser) => {
 | 
					export default async (params: any, user: ILocalUser) => {
 | 
				
			||||||
	// Get 'limit' parameter
 | 
						const [ps, psErr] = getParams(meta, params);
 | 
				
			||||||
	const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
 | 
						if (psErr) throw psErr;
 | 
				
			||||||
	if (limitErr) throw 'invalid limit param';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get 'sinceId' parameter
 | 
					 | 
				
			||||||
	const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
 | 
					 | 
				
			||||||
	if (sinceIdErr) throw 'invalid sinceId param';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get 'untilId' parameter
 | 
					 | 
				
			||||||
	const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
 | 
					 | 
				
			||||||
	if (untilIdErr) throw 'invalid untilId param';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get 'sinceDate' parameter
 | 
					 | 
				
			||||||
	const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate);
 | 
					 | 
				
			||||||
	if (sinceDateErr) throw 'invalid sinceDate param';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get 'untilDate' parameter
 | 
					 | 
				
			||||||
	const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate);
 | 
					 | 
				
			||||||
	if (untilDateErr) throw 'invalid untilDate param';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if only one of sinceId, untilId, sinceDate, untilDate specified
 | 
					 | 
				
			||||||
	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
 | 
					 | 
				
			||||||
		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get 'includeMyRenotes' parameter
 | 
					 | 
				
			||||||
	const [includeMyRenotes = true, includeMyRenotesErr] = $.bool.optional.get(params.includeMyRenotes);
 | 
					 | 
				
			||||||
	if (includeMyRenotesErr) throw 'invalid includeMyRenotes param';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get 'includeRenotedMyNotes' parameter
 | 
					 | 
				
			||||||
	const [includeRenotedMyNotes = true, includeRenotedMyNotesErr] = $.bool.optional.get(params.includeRenotedMyNotes);
 | 
					 | 
				
			||||||
	if (includeRenotedMyNotesErr) throw 'invalid includeRenotedMyNotes param';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get 'mediaOnly' parameter
 | 
					 | 
				
			||||||
	const [mediaOnly, mediaOnlyErr] = $.bool.optional.get(params.mediaOnly);
 | 
					 | 
				
			||||||
	if (mediaOnlyErr) throw 'invalid mediaOnly param';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get 'listId' parameter
 | 
					 | 
				
			||||||
	const [listId, listIdErr] = $.type(ID).get(params.listId);
 | 
					 | 
				
			||||||
	if (listIdErr) throw 'invalid listId param';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const [list, mutedUserIds] = await Promise.all([
 | 
						const [list, mutedUserIds] = await Promise.all([
 | 
				
			||||||
		// リストを取得
 | 
							// リストを取得
 | 
				
			||||||
		// Fetch the list
 | 
							// Fetch the list
 | 
				
			||||||
		UserList.findOne({
 | 
							UserList.findOne({
 | 
				
			||||||
			_id: listId,
 | 
								_id: ps.listId,
 | 
				
			||||||
			userId: user._id
 | 
								userId: user._id
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -122,7 +151,7 @@ export default async (params: any, user: ILocalUser) => {
 | 
				
			||||||
	// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
 | 
						// つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。
 | 
				
			||||||
	// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
 | 
						// for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (includeMyRenotes === false) {
 | 
						if (ps.includeMyRenotes === false) {
 | 
				
			||||||
		query.$and.push({
 | 
							query.$and.push({
 | 
				
			||||||
			$or: [{
 | 
								$or: [{
 | 
				
			||||||
				userId: { $ne: user._id }
 | 
									userId: { $ne: user._id }
 | 
				
			||||||
| 
						 | 
					@ -138,7 +167,7 @@ export default async (params: any, user: ILocalUser) => {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (includeRenotedMyNotes === false) {
 | 
						if (ps.includeRenotedMyNotes === false) {
 | 
				
			||||||
		query.$and.push({
 | 
							query.$and.push({
 | 
				
			||||||
			$or: [{
 | 
								$or: [{
 | 
				
			||||||
				'_renote.userId': { $ne: user._id }
 | 
									'_renote.userId': { $ne: user._id }
 | 
				
			||||||
| 
						 | 
					@ -154,29 +183,45 @@ export default async (params: any, user: ILocalUser) => {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (mediaOnly) {
 | 
						if (ps.includeLocalRenotes === false) {
 | 
				
			||||||
 | 
							query.$and.push({
 | 
				
			||||||
 | 
								$or: [{
 | 
				
			||||||
 | 
									'_renote.user.host': { $ne: null }
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									renoteId: null
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									text: { $ne: null }
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									mediaIds: { $ne: [] }
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									poll: { $ne: null }
 | 
				
			||||||
 | 
								}]
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (ps.mediaOnly) {
 | 
				
			||||||
		query.$and.push({
 | 
							query.$and.push({
 | 
				
			||||||
			mediaIds: { $exists: true, $ne: [] }
 | 
								mediaIds: { $exists: true, $ne: [] }
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (sinceId) {
 | 
						if (ps.sinceId) {
 | 
				
			||||||
		sort._id = 1;
 | 
							sort._id = 1;
 | 
				
			||||||
		query._id = {
 | 
							query._id = {
 | 
				
			||||||
			$gt: sinceId
 | 
								$gt: ps.sinceId
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	} else if (untilId) {
 | 
						} else if (ps.untilId) {
 | 
				
			||||||
		query._id = {
 | 
							query._id = {
 | 
				
			||||||
			$lt: untilId
 | 
								$lt: ps.untilId
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	} else if (sinceDate) {
 | 
						} else if (ps.sinceDate) {
 | 
				
			||||||
		sort._id = 1;
 | 
							sort._id = 1;
 | 
				
			||||||
		query.createdAt = {
 | 
							query.createdAt = {
 | 
				
			||||||
			$gt: new Date(sinceDate)
 | 
								$gt: new Date(ps.sinceDate)
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	} else if (untilDate) {
 | 
						} else if (ps.untilDate) {
 | 
				
			||||||
		query.createdAt = {
 | 
							query.createdAt = {
 | 
				
			||||||
			$lt: new Date(untilDate)
 | 
								$lt: new Date(ps.untilDate)
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	//#endregion
 | 
						//#endregion
 | 
				
			||||||
| 
						 | 
					@ -184,7 +229,7 @@ export default async (params: any, user: ILocalUser) => {
 | 
				
			||||||
	// Issue query
 | 
						// Issue query
 | 
				
			||||||
	const timeline = await Note
 | 
						const timeline = await Note
 | 
				
			||||||
		.find(query, {
 | 
							.find(query, {
 | 
				
			||||||
			limit: limit,
 | 
								limit: ps.limit,
 | 
				
			||||||
			sort: sort
 | 
								sort: sort
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,17 +16,13 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
 | 
				
			||||||
	if (usernameErr) return rej('invalid username param');
 | 
						if (usernameErr) return rej('invalid username param');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (userId === undefined && username === undefined) {
 | 
						if (userId === undefined && username === undefined) {
 | 
				
			||||||
		return rej('userId or pair of username and host is required');
 | 
							return rej('userId or username is required');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get 'host' parameter
 | 
						// Get 'host' parameter
 | 
				
			||||||
	const [host, hostErr] = $.str.optional.get(params.host);
 | 
						const [host, hostErr] = $.str.optional.get(params.host);
 | 
				
			||||||
	if (hostErr) return rej('invalid host param');
 | 
						if (hostErr) return rej('invalid host param');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (userId === undefined && host === undefined) {
 | 
					 | 
				
			||||||
		return rej('userId or pair of username and host is required');
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get 'includeReplies' parameter
 | 
						// Get 'includeReplies' parameter
 | 
				
			||||||
	const [includeReplies = true, includeRepliesErr] = $.bool.optional.get(params.includeReplies);
 | 
						const [includeReplies = true, includeRepliesErr] = $.bool.optional.get(params.includeReplies);
 | 
				
			||||||
	if (includeRepliesErr) return rej('invalid includeReplies param');
 | 
						if (includeRepliesErr) return rej('invalid includeReplies param');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -92,7 +92,7 @@ export default async (ctx: Koa.Context) => {
 | 
				
			||||||
			weight: null
 | 
								weight: null
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		settings: {
 | 
							settings: {
 | 
				
			||||||
			autoWatch: true
 | 
								autoWatch: false
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,3 @@
 | 
				
			||||||
import * as fs from 'fs';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import * as Koa from 'koa';
 | 
					import * as Koa from 'koa';
 | 
				
			||||||
import * as send from 'koa-send';
 | 
					import * as send from 'koa-send';
 | 
				
			||||||
import * as mongodb from 'mongodb';
 | 
					import * as mongodb from 'mongodb';
 | 
				
			||||||
| 
						 | 
					@ -51,16 +49,10 @@ export default async function(ctx: Koa.Context) {
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if ('thumbnail' in ctx.query) {
 | 
						if ('thumbnail' in ctx.query) {
 | 
				
			||||||
		// 画像以外
 | 
							const thumb = await DriveFileThumbnail.findOne({
 | 
				
			||||||
		if (!file.contentType.startsWith('image/')) {
 | 
								'metadata.originalId': fileId
 | 
				
			||||||
			const readable = fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
 | 
							});
 | 
				
			||||||
			ctx.set('Content-Type', 'image/png');
 | 
					
 | 
				
			||||||
			ctx.body = readable;
 | 
					 | 
				
			||||||
		} else if (file.contentType == 'image/gif') {
 | 
					 | 
				
			||||||
			// GIF
 | 
					 | 
				
			||||||
			await sendRaw();
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			const thumb = await DriveFileThumbnail.findOne({ 'metadata.originalId': fileId });
 | 
					 | 
				
			||||||
		if (thumb != null) {
 | 
							if (thumb != null) {
 | 
				
			||||||
			ctx.set('Content-Type', 'image/jpeg');
 | 
								ctx.set('Content-Type', 'image/jpeg');
 | 
				
			||||||
			const bucket = await getDriveFileThumbnailBucket();
 | 
								const bucket = await getDriveFileThumbnailBucket();
 | 
				
			||||||
| 
						 | 
					@ -68,7 +60,6 @@ export default async function(ctx: Koa.Context) {
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			await sendRaw();
 | 
								await sendRaw();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		if ('download' in ctx.query) {
 | 
							if ('download' in ctx.query) {
 | 
				
			||||||
			ctx.set('Content-Disposition', 'attachment');
 | 
								ctx.set('Content-Disposition', 'attachment');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,9 @@ module.exports = async (ctx: Koa.Context) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ctx.body = summary;
 | 
							ctx.body = summary;
 | 
				
			||||||
	} catch (e) {
 | 
						} catch (e) {
 | 
				
			||||||
		ctx.status = 500;
 | 
							ctx.status = 200;
 | 
				
			||||||
 | 
							ctx.set('Cache-Control', 'max-age=86400, immutable');
 | 
				
			||||||
 | 
							ctx.body = '{}';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
import { Buffer } from 'buffer';
 | 
					import { Buffer } from 'buffer';
 | 
				
			||||||
import * as fs from 'fs';
 | 
					import * as fs from 'fs';
 | 
				
			||||||
import * as stream from 'stream';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import * as mongodb from 'mongodb';
 | 
					import * as mongodb from 'mongodb';
 | 
				
			||||||
import * as crypto from 'crypto';
 | 
					import * as crypto from 'crypto';
 | 
				
			||||||
| 
						 | 
					@ -17,30 +16,52 @@ import { publishUserStream, publishDriveStream } from '../../stream';
 | 
				
			||||||
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
 | 
					import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
 | 
				
			||||||
import delFile from './delete-file';
 | 
					import delFile from './delete-file';
 | 
				
			||||||
import config from '../../config';
 | 
					import config from '../../config';
 | 
				
			||||||
 | 
					import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = debug('misskey:drive:add-file');
 | 
					const log = debug('misskey:drive:add-file');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function save(readable: stream.Readable, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> {
 | 
					async function save(path: string, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> {
 | 
				
			||||||
 | 
						let thumbnail: Buffer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (['image/jpeg', 'image/png', 'image/webp'].includes(type)) {
 | 
				
			||||||
 | 
							thumbnail = await sharp(path)
 | 
				
			||||||
 | 
								.resize(300)
 | 
				
			||||||
 | 
								.jpeg({
 | 
				
			||||||
 | 
									quality: 50,
 | 
				
			||||||
 | 
									progressive: true
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.toBuffer();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (config.drive && config.drive.storage == 'minio') {
 | 
						if (config.drive && config.drive.storage == 'minio') {
 | 
				
			||||||
		const minio = new Minio.Client(config.drive.config);
 | 
							const minio = new Minio.Client(config.drive.config);
 | 
				
			||||||
		const id = uuid.v4();
 | 
							const key = `${config.drive.prefix}/${uuid.v4()}/${name}`;
 | 
				
			||||||
		const obj = `${config.drive.prefix}/${id}`;
 | 
							const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}/${name}.thumbnail.jpg`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const baseUrl = config.drive.baseUrl
 | 
							const baseUrl = config.drive.baseUrl
 | 
				
			||||||
			|| `${ config.drive.config.secure ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? ':' + config.drive.config.port : '' }/${ config.drive.bucket }`;
 | 
								|| `${ config.drive.config.secure ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? ':' + config.drive.config.port : '' }/${ config.drive.bucket }`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await minio.putObject(config.drive.bucket, obj, readable, size, {
 | 
							await minio.putObject(config.drive.bucket, key, fs.createReadStream(path), size, {
 | 
				
			||||||
			'Content-Type': type,
 | 
								'Content-Type': type,
 | 
				
			||||||
			'Cache-Control': 'max-age=31536000, immutable'
 | 
								'Cache-Control': 'max-age=31536000, immutable'
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (thumbnail) {
 | 
				
			||||||
 | 
								await minio.putObject(config.drive.bucket, thumbnailKey, thumbnail, size, {
 | 
				
			||||||
 | 
									'Content-Type': 'image/jpeg',
 | 
				
			||||||
 | 
									'Cache-Control': 'max-age=31536000, immutable'
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Object.assign(metadata, {
 | 
							Object.assign(metadata, {
 | 
				
			||||||
			withoutChunks: true,
 | 
								withoutChunks: true,
 | 
				
			||||||
			storage: 'minio',
 | 
								storage: 'minio',
 | 
				
			||||||
			storageProps: {
 | 
								storageProps: {
 | 
				
			||||||
				id: id
 | 
									key: key,
 | 
				
			||||||
 | 
									thumbnailKey: thumbnailKey
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			url: `${ baseUrl }/${ obj }`
 | 
								url: `${ baseUrl }/${ key }`,
 | 
				
			||||||
 | 
								thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKey }` : null
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const file = await DriveFile.insert({
 | 
							const file = await DriveFile.insert({
 | 
				
			||||||
| 
						 | 
					@ -57,12 +78,36 @@ async function save(readable: stream.Readable, name: string, type: string, hash:
 | 
				
			||||||
		// Get MongoDB GridFS bucket
 | 
							// Get MongoDB GridFS bucket
 | 
				
			||||||
		const bucket = await getDriveFileBucket();
 | 
							const bucket = await getDriveFileBucket();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return new Promise<IDriveFile>((resolve, reject) => {
 | 
							const file = await new Promise<IDriveFile>((resolve, reject) => {
 | 
				
			||||||
			const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
 | 
								const writeStream = bucket.openUploadStream(name, {
 | 
				
			||||||
 | 
									contentType: type,
 | 
				
			||||||
 | 
									metadata
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			writeStream.once('finish', resolve);
 | 
								writeStream.once('finish', resolve);
 | 
				
			||||||
			writeStream.on('error', reject);
 | 
								writeStream.on('error', reject);
 | 
				
			||||||
			readable.pipe(writeStream);
 | 
					
 | 
				
			||||||
 | 
								fs.createReadStream(path).pipe(writeStream);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (thumbnail) {
 | 
				
			||||||
 | 
								const thumbnailBucket = await getDriveFileThumbnailBucket();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								await new Promise<IDriveFile>((resolve, reject) => {
 | 
				
			||||||
 | 
									const writeStream = thumbnailBucket.openUploadStream(name, {
 | 
				
			||||||
 | 
										contentType: 'image/jpeg',
 | 
				
			||||||
 | 
										metadata: {
 | 
				
			||||||
 | 
											originalId: file._id
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									writeStream.once('finish', resolve);
 | 
				
			||||||
 | 
									writeStream.on('error', reject);
 | 
				
			||||||
 | 
									writeStream.end(thumbnail);
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return file;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -321,7 +366,7 @@ export default async function(
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		driveFile = await (save(fs.createReadStream(path), detectedName, mime, hash, size, metadata));
 | 
							driveFile = await (save(path, detectedName, mime, hash, size, metadata));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log(`drive file has been created ${driveFile._id}`);
 | 
						log(`drive file has been created ${driveFile._id}`);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,8 +6,18 @@ import config from '../../config';
 | 
				
			||||||
export default async function(file: IDriveFile, isExpired = false) {
 | 
					export default async function(file: IDriveFile, isExpired = false) {
 | 
				
			||||||
	if (file.metadata.storage == 'minio') {
 | 
						if (file.metadata.storage == 'minio') {
 | 
				
			||||||
		const minio = new Minio.Client(config.drive.config);
 | 
							const minio = new Minio.Client(config.drive.config);
 | 
				
			||||||
		const obj = `${config.drive.prefix}/${file.metadata.storageProps.id}`;
 | 
					
 | 
				
			||||||
 | 
							// 後方互換性のため、file.metadata.storageProps.key があるかどうかチェックしています。
 | 
				
			||||||
 | 
							// 将来的には const obj = file.metadata.storageProps.key; とします。
 | 
				
			||||||
 | 
							const obj = file.metadata.storageProps.key ? file.metadata.storageProps.key : `${config.drive.prefix}/${file.metadata.storageProps.id}`;
 | 
				
			||||||
		await minio.removeObject(config.drive.bucket, obj);
 | 
							await minio.removeObject(config.drive.bucket, obj);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (file.metadata.thumbnailUrl) {
 | 
				
			||||||
 | 
								// 後方互換性のため、file.metadata.storageProps.thumbnailKey があるかどうかチェックしています。
 | 
				
			||||||
 | 
								// 将来的には const thumbnailObj = file.metadata.storageProps.thumbnailKey; とします。
 | 
				
			||||||
 | 
								const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`;
 | 
				
			||||||
 | 
								await minio.removeObject(config.drive.bucket, thumbnailObj);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// チャンクをすべて削除
 | 
						// チャンクをすべて削除
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,6 +95,8 @@ type Option = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default async (user: IUser, data: Option, silent = false) => new Promise<INote>(async (res, rej) => {
 | 
					export default async (user: IUser, data: Option, silent = false) => new Promise<INote>(async (res, rej) => {
 | 
				
			||||||
 | 
						const isFirstNote = user.notesCount === 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (data.createdAt == null) data.createdAt = new Date();
 | 
						if (data.createdAt == null) data.createdAt = new Date();
 | 
				
			||||||
	if (data.visibility == null) data.visibility = 'public';
 | 
						if (data.visibility == null) data.visibility = 'public';
 | 
				
			||||||
	if (data.viaMobile == null) data.viaMobile = false;
 | 
						if (data.viaMobile == null) data.viaMobile = false;
 | 
				
			||||||
| 
						 | 
					@ -164,6 +166,10 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 | 
				
			||||||
	// Pack the note
 | 
						// Pack the note
 | 
				
			||||||
	const noteObj = await pack(note);
 | 
						const noteObj = await pack(note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (isFirstNote) {
 | 
				
			||||||
 | 
							noteObj.isFirstNote = true;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const nm = new NotificationManager(user, note);
 | 
						const nm = new NotificationManager(user, note);
 | 
				
			||||||
	const nmRelatedPromises = [];
 | 
						const nmRelatedPromises = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -188,6 +194,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 | 
				
			||||||
		// 通知
 | 
							// 通知
 | 
				
			||||||
		if (isLocalUser(data.reply._user)) {
 | 
							if (isLocalUser(data.reply._user)) {
 | 
				
			||||||
			nm.push(data.reply.userId, 'reply');
 | 
								nm.push(data.reply.userId, 'reply');
 | 
				
			||||||
 | 
								publishUserStream(data.reply.userId, 'reply', noteObj);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -209,7 +216,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Publish event
 | 
							// Publish event
 | 
				
			||||||
		if (!user._id.equals(data.renote.userId)) {
 | 
							if (!user._id.equals(data.renote.userId) && isLocalUser(data.renote._user)) {
 | 
				
			||||||
			publishUserStream(data.renote.userId, 'renote', noteObj);
 | 
								publishUserStream(data.renote.userId, 'renote', noteObj);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -228,7 +235,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function renderActivity(data: Option, note: INote) {
 | 
					async function renderActivity(data: Option, note: INote) {
 | 
				
			||||||
	const content = data.renote && data.text == null
 | 
						const content = data.renote && data.text == null
 | 
				
			||||||
		? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
 | 
							? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote), note)
 | 
				
			||||||
		: renderCreate(await renderNote(note));
 | 
							: renderCreate(await renderNote(note));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return packAp(content);
 | 
						return packAp(content);
 | 
				
			||||||
| 
						 | 
					@ -321,8 +328,18 @@ async function insertNote(user: IUser, data: Option, tokens: ReturnType<typeof p
 | 
				
			||||||
			: [],
 | 
								: [],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 以下非正規化データ
 | 
							// 以下非正規化データ
 | 
				
			||||||
		_reply: data.reply ? { userId: data.reply.userId } : null,
 | 
							_reply: data.reply ? {
 | 
				
			||||||
		_renote: data.renote ? { userId: data.renote.userId } : null,
 | 
								userId: data.reply.userId,
 | 
				
			||||||
 | 
								user: {
 | 
				
			||||||
 | 
									host: data.reply._user.host
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} : null,
 | 
				
			||||||
 | 
							_renote: data.renote ? {
 | 
				
			||||||
 | 
								userId: data.renote.userId,
 | 
				
			||||||
 | 
								user: {
 | 
				
			||||||
 | 
									host: data.renote._user.host
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} : null,
 | 
				
			||||||
		_user: {
 | 
							_user: {
 | 
				
			||||||
			host: user.host,
 | 
								host: user.host,
 | 
				
			||||||
			inbox: isRemoteUser(user) ? user.inbox : undefined
 | 
								inbox: isRemoteUser(user) ? user.inbox : undefined
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue