diff --git a/.config/example.yml b/.config/example.yml new file mode 100644 index 0000000000..0e167ccb77 --- /dev/null +++ b/.config/example.yml @@ -0,0 +1,57 @@ +# サーバーのメンテナ情報 +maintainer: + # メンテナの名前 + name: + + # メンテナの連絡先(URLかmailto形式のURL) + url: + +# (Misskeyを動かす)URL +url: + +# 待受ポート +port: + +# TLSの設定(利用しない場合は省略可能) +https: + # 証明書のパス... + key: + cert: + +# MongoDBの設定 +mongodb: + host: localhost + port: 27017 + db: misskey + user: + pass: + +# Redisの設定 +redis: + host: localhost + port: 6379 + pass: + +# reCAPTCHAの設定 +recaptcha: + site_key: + secret_key: + +# ServiceWrokerの設定 +sw: + # VAPIDの公開鍵 + public_key: + + # VAPIDの秘密鍵 + private_key: + +# Google Maps API +google_maps_api_key: + +# Twitterインテグレーションの設定(利用しない場合は省略可能) +twitter: + # インテグレーション用アプリのコンシューマーキー + consumer_key: + + # インテグレーション用アプリのコンシューマーシークレット + consumer_secret: diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..7a74d6ef9b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,26 @@ +{ + "parserOptions": { + "parser": "typescript-eslint-parser" + }, + "extends": [ + "eslint:recommended", + "plugin:vue/recommended" + ], + "rules": { + "vue/require-v-for-key": false, + "vue/max-attributes-per-line": false, + "vue/html-indent": false, + "vue/html-self-closing": false, + "vue/no-unused-vars": false, + "vue/attributes-order": false, + "vue/require-prop-types": false, + "no-console": 0, + "no-unused-vars": 0, + "no-empty": 0 + }, + "globals": { + "ENV": true, + "VERSION": true, + "API": true + } +} diff --git a/.gitattributes b/.gitattributes index c6c5947baf..952d6cd0e9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,3 @@ *.svg -diff -text *.psd -diff -text *.ai -diff -text - -*.tag linguist-language=HTML diff --git a/.gitignore b/.gitignore index 42b1bde94f..be8689e2ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ -/.config +/.config/* +!/.config/example.yml /.vscode /node_modules +/build /built -/uploads +/data npm-debug.log *.pem run.bat api-docs.json package-lock.json +version.json diff --git a/.travis.yml b/.travis.yml index 91e1244432..d2552bb460 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,13 @@ # travis file # https://docs.travis-ci.com/user/customizing-the-build +notifications: + email: false + language: node_js node_js: - - 7.10.0 + - 9.8.0 env: - CXX=g++-4.8 NODE_ENV=production @@ -33,14 +36,8 @@ before_script: # --only=dev オプションを付けてそれらもインストールされるようにする: - npm install --only=dev - # 設定ファイルを設定 - - mkdir ./.config + # 設定ファイルを配置 - cp ./.travis/default.yml ./.config - cp ./.travis/test.yml ./.config - - npm run build - -after_success: - # リリース - - chmod u+x ./.travis/release.sh - - if [ $TRAVIS_BRANCH = "master" ] && [ $TRAVIS_PULL_REQUEST = "false" ]; then ./.travis/release.sh; else echo "Skipping releasing task"; fi + - travis_wait npm run build diff --git a/.travis/.gitignore-release b/.travis/.gitignore-release deleted file mode 100644 index ad1d3724fc..0000000000 --- a/.travis/.gitignore-release +++ /dev/null @@ -1,8 +0,0 @@ -# Realizing whitelist by excluding everything and specifying exceptions. - -/* - -!/built -!/tools -!/elasticsearch -!/package.json diff --git a/.travis/default.yml b/.travis/default.yml index 1875748d68..471a2a7c46 100644 --- a/.travis/default.yml +++ b/.travis/default.yml @@ -22,5 +22,5 @@ elasticsearch: port: 9200 pass: '' recaptcha: - siteKey: hima - secretKey: saku + site_key: hima + secret_key: saku diff --git a/.travis/release.sh b/.travis/release.sh deleted file mode 100644 index 5def2ab03a..0000000000 --- a/.travis/release.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -echo "Starting releasing task" -openssl aes-256-cbc -K $encrypted_ceda82069128_key -iv $encrypted_ceda82069128_iv -in ./.travis/travis_rsa.enc -out travis_rsa -d -cp travis_rsa ~/.ssh/id_rsa -chmod 600 ~/.ssh/id_rsa -echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config -git checkout -b release -cp -f ./.travis/.gitignore-release .gitignore -node ./.travis/shapeup.js -git add --all -git rm --cached `git ls-files --full-name -i --exclude-standard` -git config --global user.email "AyaMorisawa4869@gmail.com" -git config --global user.name "Aya Morisawa" -git commit -m "Release build for $TRAVIS_COMMIT" -git push -f git@github.com:syuilo/misskey release -echo "Finished releasing task" diff --git a/.travis/shapeup.js b/.travis/shapeup.js deleted file mode 100644 index 9a5d85a188..0000000000 --- a/.travis/shapeup.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict' - -const fs = require('fs') -const filename = process.argv[2] || 'package.json' - -fs.readFile(filename, (err, data) => { - if (err) process.exit(2) - const object = JSON.parse(data) - delete object.devDependencies - fs.writeFile(filename, JSON.stringify(object, null, '\t') + '\n', err => { - if (err) process.exit(3) - }) -}) diff --git a/.travis/test.yml b/.travis/test.yml index f311310c7c..6a115d6ab8 100644 --- a/.travis/test.yml +++ b/.travis/test.yml @@ -22,5 +22,5 @@ elasticsearch: port: 9200 pass: '' recaptcha: - siteKey: hima - secretKey: saku + site_key: hima + secret_key: saku diff --git a/.travis/travis_rsa.enc b/.travis/travis_rsa.enc deleted file mode 100644 index ec45f8a6bb..0000000000 Binary files a/.travis/travis_rsa.enc and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 72a584ddb0..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,23 +0,0 @@ -ChangeLog -========= -主に notable な changes を書いていきます - -2380 ----- -アプリケーションが作れない問題を修正 - -2367 ----- -Statsのユーザー数グラフに「アカウントが作成された**回数**」(その日時点での「アカウント数」**ではなく**)グラフも併記するようにした - -2364 ----- -デザインの微調整 - -2361 ----- -Statsを実装するなど - -2357 ----- -Statusを実装するなど diff --git a/DONORS.md b/DONORS.md new file mode 100644 index 0000000000..6b56b13e0b --- /dev/null +++ b/DONORS.md @@ -0,0 +1,24 @@ +DONORS +====== +The list of people who have sent donation for Misskey. + +(no particular order) + +* らふぁ +* 俺様 +* なぎうり +* スルメ https://surume.tk/ +* 藍 +* 音船 https://otofune.me/ +* aqz https://misskey.xyz/aqz +* kotodu "虚無創作中" + +:heart: Thanks for donating, guys! + +--- + +If your name is missing, please contact us! + +If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. + +[syuilo-link]: https://syuilo.com diff --git a/LICENSE b/LICENSE index e3733b3961..dba13ed2dd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -The MIT License (MIT) + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Copyright (c) 2014-2017 syuilo + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 9d2d38149c..46288e0c4a 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,75 @@ -![Misskey](./assets/title.png) + + +[![Misskey](/assets/title.png)](https://misskey.xyz/) ================================================================ [![][travis-badge]][travis-link] [![][dependencies-badge]][dependencies-link] [![][himawari-badge]][himasaku] [![][sakurako-badge]][himasaku] -[![][mit-badge]][mit] +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) -[Misskey](https://misskey.xyz) is a completely open source, +> Lead Maintainer: [syuilo][syuilo-link] + +**[Misskey](https://misskey.xyz)** is a completely open source, ultimately sophisticated new type of mini-blog based SNS. -![ss](./assets/ss.jpg) - -Key features +:sparkles: Features ---------------------------------------------------------------- * Automatically updated timeline * Private messages -* Free 1GB storage for each all users -* Mobile device support (smartphone, tablet, etc) +* Two-Factor Authentication support +* ServiceWorker support * Web API for third-party applications -* No ads +* ActivityPub compatible and more! You can touch with your own eyes at https://misskey.xyz/. -Setup and Installation +:package: Setup ---------------------------------------------------------------- -Please see [Setup and installation guide](./docs/setup.en.md). +If you want to run your own instance of Misskey, +please see [Setup and installation guide](./docs/setup.en.md). -Contribution +:yen: Donation ---------------------------------------------------------------- -Please see [Contribution guide](./CONTRIBUTING.md). +If you want to donate to Misskey, please see [this](./docs/donate.ja.md). -Sponsors & Backers ----------------------------------------------------------------- -Misskey have no 100+ GitHub stars currently. However, donation are always welcome! -If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link]. +[List of all donors](./DONORS.md) -Collaborators +:mortar_board: Notable contributors ---------------------------------------------------------------- -| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | -|------------------------|-----------------------------------|---------------------------------| -| [syuilo][syuilo-link] | [Aya Morisawa][ayamorisawa-link] | [otofune][otofune-link] | +| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] | ![akihikodaki][akihikodaki-icon] | ![rinsuki][rinsuki-icon] | +|:-:|:-:|:-:|:-:|:-:| +| [syuilo][syuilo-link]
Owner | [Aya Morisawa][ayamorisawa-link]
Collaborator | [otofune][otofune-link]
Collaborator | [akihikodaki][akihikodaki-link] | [rinsuki][rinsuki-link] | [List of all contributors](https://github.com/syuilo/misskey/graphs/contributors) -Copyright +:four_leaf_clover: Copyright ---------------------------------------------------------------- -Misskey is an open-source software licensed under [The MIT License](LICENSE). +> Copyright (c) 2014-2018 syuilo -[mit]: http://opensource.org/licenses/MIT -[mit-badge]: https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square +Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE). + +[![][agpl-3.0-badge]][AGPL-3.0] + +[agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html +[agpl-3.0-badge]: https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square [travis-link]: https://travis-ci.org/syuilo/misskey [travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square -[dependencies-link]: https://gemnasium.com/syuilo/misskey -[dependencies-badge]: https://img.shields.io/gemnasium/syuilo/misskey.svg?style=flat-square +[dependencies-link]: https://david-dm.org/syuilo/misskey +[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square [himasaku]: https://himasaku.net [himawari-badge]: https://img.shields.io/badge/%E5%8F%A4%E8%B0%B7-%E5%90%91%E6%97%A5%E8%91%B5-1684c5.svg?style=flat-square [sakurako-badge]: https://img.shields.io/badge/%E5%A4%A7%E5%AE%A4-%E6%AB%BB%E5%AD%90-efb02a.svg?style=flat-square - + [syuilo-link]: https://syuilo.com [syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 [ayamorisawa-link]: https://github.com/ayamorisawa [ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70 [otofune-link]: https://github.com/otofune [otofune-icon]: https://avatars0.githubusercontent.com/u/15062473?v=3&s=70 +[akihikodaki-link]: https://github.com/akihikodaki +[akihikodaki-icon]: https://avatars2.githubusercontent.com/u/17036990?s=70&v=4 +[rinsuki-link]: https://github.com/rinsuki +[rinsuki-icon]: https://avatars0.githubusercontent.com/u/6533808?s=70&v=4 diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index d26cbc27e8..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,31 +0,0 @@ -# appveyor file -# http://www.appveyor.com/docs/appveyor-yml - -environment: - matrix: - - nodejs_version: 7.10.0 - -build: off - -install: - # Update Node.js - # 標準で入っている Node.js を更新します (2014/11/13 時点では、v0.10.32 が標準) - - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) - - node --version - - # Update NPM - - npm install -g npm - - npm --version - - # Update node-gyp - # 必須! node-gyp のバージョンを上げないと、ネイティブモジュールのコンパイルに失敗します - - npm install -g node-gyp - - - npm install - -init: - # git clone の際の改行を変換しないようにします - - git config --global core.autocrlf false - -test_script: - - npm run build diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png index 623cf6bb9a..b3c4be42af 100644 Binary files a/assets/apple-touch-icon.png and b/assets/apple-touch-icon.png differ diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000000..d63c68b016 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/favicon/128.png b/assets/favicon/128.png index 16e8dfb5b4..1ccaaeee1c 100644 Binary files a/assets/favicon/128.png and b/assets/favicon/128.png differ diff --git a/assets/favicon/128.svg b/assets/favicon/128.svg new file mode 100644 index 0000000000..34888557b9 Binary files /dev/null and b/assets/favicon/128.svg differ diff --git a/assets/favicon/16.png b/assets/favicon/16.png index 7e36d1cda0..a1d3e1be72 100644 Binary files a/assets/favicon/16.png and b/assets/favicon/16.png differ diff --git a/assets/favicon/16.svg b/assets/favicon/16.svg new file mode 100644 index 0000000000..03aa8bc6bd Binary files /dev/null and b/assets/favicon/16.svg differ diff --git a/assets/favicon/256.png b/assets/favicon/256.png index 623cf6bb9a..b3c4be42af 100644 Binary files a/assets/favicon/256.png and b/assets/favicon/256.png differ diff --git a/assets/favicon/256.svg b/assets/favicon/256.svg new file mode 100644 index 0000000000..5ecee9e0be Binary files /dev/null and b/assets/favicon/256.svg differ diff --git a/assets/favicon/32.png b/assets/favicon/32.png index f13ebb1473..f0466cce91 100644 Binary files a/assets/favicon/32.png and b/assets/favicon/32.png differ diff --git a/assets/favicon/32.svg b/assets/favicon/32.svg new file mode 100644 index 0000000000..4dfcc68606 Binary files /dev/null and b/assets/favicon/32.svg differ diff --git a/assets/favicon/64.png b/assets/favicon/64.png index 72751fe774..9710052ae7 100644 Binary files a/assets/favicon/64.png and b/assets/favicon/64.png differ diff --git a/assets/favicon/64.svg b/assets/favicon/64.svg new file mode 100644 index 0000000000..e2378791a7 Binary files /dev/null and b/assets/favicon/64.svg differ diff --git a/assets/icon.ai b/assets/icon.ai deleted file mode 100644 index c2d5219c73..0000000000 Binary files a/assets/icon.ai and /dev/null differ diff --git a/assets/mi.svg b/assets/mi.svg new file mode 100644 index 0000000000..d4f7cf7e9e Binary files /dev/null and b/assets/mi.svg differ diff --git a/assets/ss.jpg b/assets/ss.jpg deleted file mode 100644 index a337b3fa7c..0000000000 Binary files a/assets/ss.jpg and /dev/null differ diff --git a/assets/title.png b/assets/title.png index 05658c8779..cacbb248d3 100644 Binary files a/assets/title.png and b/assets/title.png differ diff --git a/assets/title.svg b/assets/title.svg index ad8290fe98..95ad11c399 100644 Binary files a/assets/title.svg and b/assets/title.svg differ diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 0000000000..0349526d52 --- /dev/null +++ b/binding.gyp @@ -0,0 +1,9 @@ +{ + 'targets': [ + { + 'target_name': 'crypto_key', + 'sources': ['src/crypto_key.cc'], + 'include_dirs': [' -p +``` + +For details, plese see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/). + +Restore +------- + +``` shell +$ mongorestore --archive=db-backup +``` + +For details, please see [mongorestore docs](https://docs.mongodb.com/manual/reference/program/mongorestore/). diff --git a/docs/donate.ja.md b/docs/donate.ja.md new file mode 100644 index 0000000000..b19d7bc370 --- /dev/null +++ b/docs/donate.ja.md @@ -0,0 +1,26 @@ +# Misskeyにカンパする方法 +Misskeyのサポートにご興味をお持ちいただきありがとうございます! +Misskeyにカンパをしていただくと、貴方のお名前と好きなURLなどをMisskeyのリポジトリに刻む権利がもらえます。 + +Misskeyにカンパして開発・運営をサポートするには、次のいくつかの方法があります: + +## ConoHaカードを購入する +(本家)Misskeyは、ConoHaというVPSサービスを利用しています。ConoHaカードを購入して、 +カードに記載されているクーポンコードを syuilotan@yahoo.co.jp までお送りいただければ、 +そのクーポンをチャージしてサーバーの運営費に充てることができます。 + +ConoHaカードについてはこちらをご覧ください: https://www.conoha.jp/conohacard/ + +Amazonでも買えます: https://www.amazon.co.jp/dp/B01N9E3416 + +## Amazonギフトカード +これは間接的な方法です。 + +## 銀行振込 +syuilotan@yahoo.co.jp までお問い合わせください。 + +## 手渡し +オフ会を行ったときなどに行使できる方法です。 + +## その他 +なにかいいアイデアがあればお教えください。 diff --git a/docs/setup.en.md b/docs/setup.en.md index 3e48935346..88e20f6bc4 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -1,41 +1,28 @@ Misskey Setup and Installation Guide ================================================================ -We thank you for your interest in setup your Misskey server! +We thank you for your interest in setting up your Misskey server! This guide describes how to install and setup Misskey. [Japanese version also available - 日本語版もあります](./setup.ja.md) ---------------------------------------------------------------- -If you can use Docker, please see [Setup with Docker](./docker.en.md). - -*1.* Domains ----------------------------------------------------------------- -Misskey requires two domains called the primary domain and the secondary domain. - -* The primary domain is used to provide main service of Misskey. -* The secondary domain is used to avoid vulnerabilities such as XSS. - -**Ensure that the secondary domain is not a subdomain of the primary domain.** - -### Subdomains -Note that Misskey uses following subdomains: - -* **api**.*{primary domain}* -* **auth**.*{primary domain}* -* **about**.*{primary domain}* -* **stats**.*{primary domain}* -* **status**.*{primary domain}* -* **dev**.*{primary domain}* -* **file**.*{secondary domain}* - -*2.* reCAPTCHA tokens +*1.* reCAPTCHA tokens ---------------------------------------------------------------- Misskey requires reCAPTCHA tokens. Please visit https://www.google.com/recaptcha/intro/ and generate keys. -*3.* Install dependencies +*(optional)* Generating VAPID keys +---------------------------------------------------------------- +If you want to enable ServiceWroker, you need to generate VAPID keys: + +``` shell +npm install web-push -g +web-push generate-vapid-keys +``` + +*2.* Install dependencies ---------------------------------------------------------------- Please install and setup these softwares: @@ -43,52 +30,39 @@ Please install and setup these softwares: * *Node.js* and *npm* * **[MongoDB](https://www.mongodb.com/)** * **[Redis](https://redis.io/)** -* **[GraphicsMagick](http://www.graphicsmagick.org/)** +* **[ImageMagick](http://www.imagemagick.org/script/index.php)** ##### Optional * [Elasticsearch](https://www.elastic.co/) - used to provide searching feature instead of MongoDB -*4.* Install Misskey +*3.* Install Misskey ---------------------------------------------------------------- -There is **two ways** to install Misskey: - -### WAY 1) Using built code (recommended) -We have official release of Misskey. -The built code is automatically pushed to https://github.com/syuilo/misskey/tree/release after the CI test succeeds. - -1. `git clone -b release git://github.com/syuilo/misskey.git` -2. `cd misskey` -3. `npm install` - -#### Update -1. `git fetch` -2. `git reset --hard origin/release` -3. `npm install` - -### WAY 2) Using source code -If you want to build Misskey manually, you can do it via the -`build` command after download the source code of Misskey and install dependencies: - 1. `git clone -b master git://github.com/syuilo/misskey.git` 2. `cd misskey` 3. `npm install` -4. `npm run build` -#### Update -1. `git pull origin master` -2. `npm install` -3. `npm run build` +*4.* Prepare configuration +---------------------------------------------------------------- +1. Copy `example.yml` of `.config` directory +2. Rename it to `default.yml` +3. Edit it -*5.* That is it. +--- + +Or you can generate config file via `npm run config` command. + +*5.* Build Misskey +---------------------------------------------------------------- +1. `npm run build` + +*6.* That is it. ---------------------------------------------------------------- Well done! Now, you have an environment that run to Misskey. ### Launch Just `sudo npm start`. GLHF! -### Testing -Run `npm test` after building - -### Debugging :bug: -#### Show debug messages -Misskey uses [debug](https://github.com/visionmedia/debug) and the namespace is `misskey:*`. +### Way to Update to latest version of your Misskey +1. `git reset --hard && git pull origin master` +2. `npm install` +3. `npm run build` diff --git a/docs/setup.ja.md b/docs/setup.ja.md index 4f48a08088..a46c38cb21 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -8,35 +8,21 @@ Misskeyサーバーの構築にご関心をお寄せいただきありがとう ---------------------------------------------------------------- -Dockerを利用してMisskeyを構築することもできます: [Setup with Docker](./docker.en.md)。 -その場合、*3番目*以降の手順はスキップできます。 - -*1.* ドメインの用意 ----------------------------------------------------------------- -Misskeyはプライマリ ドメインとセカンダリ ドメインを必要とします。 - -* プライマリ ドメインはMisskeyの主要な部分を提供するために使われます。 -* セカンダリ ドメインはXSSといった脆弱性の対策に使われます。 - -**セカンダリ ドメインがプライマリ ドメインのサブドメインであってはなりません。** - -### サブドメイン -Misskeyは以下のサブドメインを使います: - -* **api**.*{primary domain}* -* **auth**.*{primary domain}* -* **about**.*{primary domain}* -* **stats**.*{primary domain}* -* **status**.*{primary domain}* -* **dev**.*{primary domain}* -* **file**.*{secondary domain}* - -*2.* reCAPTCHAトークンの用意 +*1.* reCAPTCHAトークンの用意 ---------------------------------------------------------------- MisskeyはreCAPTCHAトークンを必要とします。 https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生成してください。 -*3.* 依存関係をインストールする +*(オプション)* VAPIDキーペアの生成 +---------------------------------------------------------------- +ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります: + +``` shell +npm install web-push -g +web-push generate-vapid-keys +``` + +*2.* 依存関係をインストールする ---------------------------------------------------------------- これらのソフトウェアをインストール・設定してください: @@ -44,54 +30,40 @@ https://www.google.com/recaptcha/intro/ にアクセスしてトークンを生 * *Node.js* と *npm* * **[MongoDB](https://www.mongodb.com/)** * **[Redis](https://redis.io/)** -* **[GraphicsMagick](http://www.graphicsmagick.org/)** +* **[ImageMagick](http://www.imagemagick.org/script/index.php)** ##### オプション * [Elasticsearch](https://www.elastic.co/) - 検索機能を向上させるために用います。 -*4.* Misskeyのインストール +*3.* Misskeyのインストール ---------------------------------------------------------------- -Misskeyをインストールするには**2つの方法**があります: - -### 方法 1) ビルドされたコードを利用する (推奨) -Misskeyには公式のリリースがあります。 -ビルドされたコードはCIテストに合格した後、自動で https://github.com/syuilo/misskey/tree/release にpushされています。 - -1. `git clone -b release git://github.com/syuilo/misskey.git` -2. `cd misskey` -3. `npm install` - -#### アップデートするには: -1. `git fetch` -2. `git reset --hard origin/release` -3. `npm install` - -### 方法 2) ソースコードを利用する -> 注: この方法では正しくビルド・動作できることは保証されません。 - -Misskeyを手動でビルドしたい場合は、Misskeyのソースコードと依存関係をインストールした後、 -`build`コマンドを用いることができます: - 1. `git clone -b master git://github.com/syuilo/misskey.git` 2. `cd misskey` 3. `npm install` -4. `npm run build` -#### アップデートするには: -1. `git pull origin master` -2. `npm install` -3. `npm run build` +*4.* 設定ファイルを用意する +---------------------------------------------------------------- +1. `.config`ディレクトリ内の`example.yml`をコピー +2. `default.yml`にリネーム +3. 編集する -*5.* 以上です! +--- + +または、`npm run config`コマンドを利用して、ガイドに従って情報を +入力して設定ファイルを生成することもできます。 + +*5.* Misskeyのビルド +---------------------------------------------------------------- +1. `npm run build` + +*6.* 以上です! ---------------------------------------------------------------- お疲れ様でした。これでMisskeyを動かす準備は整いました。 ### 起動 `sudo npm start`するだけです。GLHF! -### テスト -(ビルドされている状態で)`npm test` - -### デバッグ :bug: -#### デバッグメッセージを表示するようにする -Misskeyは[debug](https://github.com/visionmedia/debug)モジュールを利用しており、ネームスペースは`misskey:*`となっています。 +### Misskeyを最新バージョンにアップデートする方法: +1. `git reset --hard && git pull origin master` +2. `npm install` +3. `npm run build` diff --git a/gulpfile.ts b/gulpfile.ts index 4ee5fbce0e..fe3b040237 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -3,28 +3,32 @@ */ import * as childProcess from 'child_process'; +import * as fs from 'fs'; import * as Path from 'path'; import * as gulp from 'gulp'; import * as gutil from 'gulp-util'; import * as ts from 'gulp-typescript'; +const sourcemaps = require('gulp-sourcemaps'); import tslint from 'gulp-tslint'; -import * as es from 'event-stream'; import cssnano = require('gulp-cssnano'); import * as uglifyComposer from 'gulp-uglify/composer'; import pug = require('gulp-pug'); import * as rimraf from 'rimraf'; -import * as chalk from 'chalk'; +import chalk from 'chalk'; import imagemin = require('gulp-imagemin'); import * as rename from 'gulp-rename'; import * as mocha from 'gulp-mocha'; import * as replace from 'gulp-replace'; import * as htmlmin from 'gulp-htmlmin'; const uglifyes = require('uglify-es'); + +import { fa } from './src/build/fa'; import version from './src/version'; +import config from './src/config'; const uglify = uglifyComposer(uglifyes, console); -const env = process.env.NODE_ENV; +const env = process.env.NODE_ENV || 'development'; const isProduction = env === 'production'; const isDebug = !isProduction; @@ -35,40 +39,34 @@ if (isDebug) { const constants = require('./src/const.json'); +require('./src/client/docs/gulpfile.ts'); + gulp.task('build', [ - 'build:js', 'build:ts', 'build:copy', - 'build:client' + 'build:client', + 'doc' ]); gulp.task('rebuild', ['clean', 'build']); -gulp.task('build:js', () => - gulp.src(['./src/**/*.js', '!./src/web/**/*.js']) - .pipe(gulp.dest('./built/')) -); - gulp.task('build:ts', () => { - const tsProject = ts.createProject('./src/tsconfig.json'); + const tsProject = ts.createProject('./tsconfig.json'); return tsProject .src() + .pipe(sourcemaps.init()) .pipe(tsProject()) + .pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: '../built' })) .pipe(gulp.dest('./built/')); }); gulp.task('build:copy', () => - es.merge( - gulp.src([ - './src/**/assets/**/*', - '!./src/web/app/**/assets/**/*' - ]).pipe(gulp.dest('./built/')) as any, - gulp.src([ - './src/web/about/**/*', - '!./src/web/about/**/*.pug' - ]).pipe(gulp.dest('./built/web/about/')) as any - ) + gulp.src([ + './build/Release/crypto_key.node', + './src/**/assets/**/*', + '!./src/client/app/**/assets/**/*' + ]).pipe(gulp.dest('./built/')) ); gulp.task('test', ['lint', 'mocha']); @@ -81,10 +79,20 @@ gulp.task('lint', () => .pipe(tslint.report()) ); +gulp.task('format', () => +gulp.src('./src/**/*.ts') + .pipe(tslint({ + formatter: 'verbose', + fix: true + })) + .pipe(tslint.report()) +); + gulp.task('mocha', () => gulp.src([]) .pipe(mocha({ - //compilers: 'ts:ts-node/register' + exit: true, + compilers: 'ts:ts-node/register' } as any)) ); @@ -100,55 +108,43 @@ gulp.task('default', ['build']); gulp.task('build:client', [ 'build:ts', - 'build:js', - 'webpack', 'build:client:script', 'build:client:pug', 'copy:client' ]); -gulp.task('webpack', done => { - const webpack = childProcess.spawn( - Path.join('.', 'node_modules', '.bin', 'webpack'), - ['--config', './webpack/webpack.config.ts'], { - shell: true, - stdio: 'inherit' - }); - - webpack.on('exit', done); -}); - gulp.task('build:client:script', () => - gulp.src(['./src/web/app/boot.js', './src/web/app/safe.js']) + gulp.src(['./src/client/app/boot.js', './src/client/app/safe.js']) .pipe(replace('VERSION', JSON.stringify(version))) + .pipe(replace('API', JSON.stringify(config.api_url))) + .pipe(replace('ENV', JSON.stringify(env))) .pipe(isProduction ? uglify({ toplevel: true - }) : gutil.noop()) - .pipe(gulp.dest('./built/web/assets/')) as any + } as any) : gutil.noop()) + .pipe(gulp.dest('./built/client/assets/')) as any ); gulp.task('build:client:styles', () => - gulp.src('./src/web/app/init.css') + gulp.src('./src/client/app/init.css') .pipe(isProduction ? (cssnano as any)() : gutil.noop()) - .pipe(gulp.dest('./built/web/assets/')) + .pipe(gulp.dest('./built/client/assets/')) ); gulp.task('copy:client', [ - 'build:client:script', - 'webpack' + 'build:client:script' ], () => gulp.src([ './assets/**/*', - './src/web/assets/**/*', - './src/web/app/*/assets/**/*' + './src/client/assets/**/*', + './src/client/app/*/assets/**/*' ]) .pipe(isProduction ? (imagemin as any)() : gutil.noop()) .pipe(rename(path => { path.dirname = path.dirname.replace('assets', '.'); })) - .pipe(gulp.dest('./built/web/assets/')) + .pipe(gulp.dest('./built/client/assets/')) ); gulp.task('build:client:pug', [ @@ -156,10 +152,13 @@ gulp.task('build:client:pug', [ 'build:client:script', 'build:client:styles' ], () => - gulp.src('./src/web/app/base.pug') + gulp.src('./src/client/app/base.pug') .pipe(pug({ locals: { - themeColor: constants.themeColor + themeColor: constants.themeColor, + facss: fa.dom.css(), + //hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8') + hljscss: fs.readFileSync('./src/client/assets/code-highlight.css', 'utf8') } })) .pipe(htmlmin({ @@ -189,7 +188,10 @@ gulp.task('build:client:pug', [ // 属性の値がデフォルトと同じなら省略する e.g. // to // - removeRedundantAttributes: true + removeRedundantAttributes: true, + + // CSSも圧縮する + minifyCSS: true })) - .pipe(gulp.dest('./built/web/app/')) + .pipe(gulp.dest('./built/client/app/')) ); diff --git a/locales/en.yml b/locales/en.yml index 55a588f99f..900571124f 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -1,4 +1,6 @@ common: + misskey: "Note everything and share it others using Misskey." + time: unknown: "unknown" future: "future" @@ -11,6 +13,15 @@ common: months_ago: "{}month(s) ago" years_ago: "{}year(s) ago" + weekday-short: + sunday: "S" + monday: "M" + tuesday: "T" + wednesday: "W" + thursday: "T" + friday: "F" + satruday: "S" + reactions: like: "Like" love: "Love" @@ -22,14 +33,32 @@ common: confused: "Confused" pudding: "Pudding" + note_categories: + music: "Music" + game: "Video Game" + anime: "Anime" + it: "IT" + gadgets: "Gadgets" + photography: "Photography" + input-message-here: "Enter message here" send: "Send" delete: "Delete" loading: "Loading" ok: "OK" update-available: "New version of Misskey is now available({newer}, current is {current}). Reload page to apply update." + my-token-regenerated: "Your token is just regenerated, so you will signout." tags: + mk-nav-links: + about: "About" + stats: "Stats" + status: "Status" + wiki: "Wiki" + donors: "Donors" + repository: "Repository" + develop: "Developers" + mk-messaging-form: attach-from-local: "Attach file from your pc" attach-from-drive: "Attach file from the drive" @@ -55,8 +84,27 @@ common: mk-error: title: "Unable to connect to the server" - description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。" + description: "There is a problem with Internet connection, or the server may be down or maintaining. Please {try again} later." thanks: "Thank you for using Misskey." + troubleshoot: "Troubleshoot" + + troubleshooter: + title: "TroubleShooting" + network: "Network connection" + checking-network: "Checking network connection" + internet: "Internet connection" + checking-internet: "Checking internet connection" + server: "Server connection" + checking-server: "Checking server connection" + finding: "Finding a problem" + no-network: "There is no Network connection" + no-network-desc: "Please make sure you are connected to the Network." + no-internet: "There is no Internet connection" + no-internet-desc: "Please make sure you are connected to the Internet." + no-server: "Unable to connect to the server" + no-server-desc: "The network connection of your PC is normal, but you could not connect to Misskey's server. There is a possibility that the server is down or maintaining, please try to access it again after a while." + success: "Successfully connect to the Misskey's server" + success-desc: "It seems to be able to connect normally. Please reload the page." mk-forkit: open-github-link: "View source on Github" @@ -76,12 +124,20 @@ common: show-result: "Show result" voted: "Voted" + mk-note-menu: + pin: "Pin" + pinned: "Pinned" + select: "Select category" + categorize: "Accept" + categorized: "Category reported. Thank you!" + mk-reaction-picker: choose-reaction: "Pick your reaction" mk-signin: username: "Username" password: "Password" + token: "Token" signing-in: "Signing in..." signin: "Sign in" @@ -127,8 +183,46 @@ common: mk-uploader: waiting: "Waiting" +docs: + edit-this-page-on-github: "Caught a mistake or want to contribute to the documentation? " + edit-this-page-on-github-link: "Edit this page on Github!" + + api: + entities: + properties: "Properties" + endpoints: + params: "Parameters" + res: "Response" + props: + name: "Name" + type: "Type" + optional: "Optional" + description: "Description" + yes: "Yes" + no: "No" + +ch: + tags: + mk-index: + new: "Create new channel" + channel-title: "Channel title" + + mk-channel-form: + textarea: "Write here" + upload: "Upload" + drive: "Drive" + note: "Do" + posting: "Doing" + desktop: tags: + mk-api-info: + intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。" + caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。" + regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。" + regenerate-token: "Regenerate the token" + enter-password: "Please enter the password" + mk-drive-browser-base-contextmenu: create-folder: "Create a folder" upload: "Upload a file" @@ -172,7 +266,6 @@ desktop: mk-drive-browser-file: avatar: "Avatar" banner: "Banner" - wallpaper: "Wallpaper" mk-drive-browser-folder-contextmenu: move-to-this-folder: "Move to this folder" @@ -189,9 +282,16 @@ desktop: mk-drive-browser-nav-folder: drive: "Drive" + mk-selectdrive-page: + title: "Choose a file(s)" + ok: "OK" + cancel: "Cancel" + upload: "Upload a file(s) from you PC" + mk-ui-header-nav: home: "Home" messaging: "Messages" + ch: "Channels" info: "News" mk-ui-header-search: @@ -204,19 +304,64 @@ desktop: settings: "Settings" signout: "Sign out" + mk-ui-header-note-button: + note: "Compose new Post" + + mk-ui-header-notifications: + title: "Notifications" + + mk-profile-setting: + avatar: "Avatar" + choice-avatar: "Choice an image" + name: "Name" + location: "Location" + description: "Description" + birthday: "Birthday" + save: "Update profile" + + mk-password-setting: + reset: "Change your password" + enter-current-password: "Enter the current password" + enter-new-password: "Enter the new password" + enter-new-password-again: "Enter the new password again" + not-match: "New password not matched" + changed: "Password updated successfully" + + mk-2fa-setting: + intro: "If you set up 2-step verification, you will need not only a password at sign-in but also a pre-registered physical device (such as your smartphone), which will improve security. " + detail: "See details..." + url: "https://www.google.com/landing/2step/" + caution: "As a caveat, security improves, but you can not sign in to Misskey if you lose a registered device, etc." + register: "Register a device" + already-registered: "The setting has already been completed." + unregister: "Disable" + unregistered: "Two-step authentication has been disabled." + enter-password: "Enter the password" + authenticator: "First, you need install Google Authenticator to your device:" + howtoinstall: "How to install" + scan: "Next, please scan displayed QR code:" + done: "Please enter the token displaying in your device:" + submit: "Submit" + success: "Setup completed successfully!" + failed: "Failed to setup. please ensure that the token is correct." + info: "From the next sign in, enter the token that is displayed on the device in addition to the password." + + mk-mute-setting: + no-users: "No muted users" + mk-post-form: - post-placeholder: "What's happening?" - reply-placeholder: "Reply to this post..." - quote-placeholder: "Quote this post..." - post: "Post" + note-placeholder: "What's happening?" + reply-placeholder: "Reply to this note..." + quote-placeholder: "Quote this note..." + note: "Post" reply: "Reply" - repost: "Repost" + renote: "Renote" posted: "Posted!" replied: "Replied!" reposted: "Reposted!" - post-failed: "Failed to post" + note-failed: "Failed to note" reply-failed: "Failed to reply" - repost-failed: "Failed to repost" + renote-failed: "Failed to renote" posting: "Posting" attach-media-from-local: "Attach media from your pc" attach-media-from-drive: "Attach media from the drive" @@ -226,15 +371,29 @@ desktop: text-remain: "{} chars remaining" mk-post-form-window: - post: "New post" + note: "New note" reply: "Reply" attaches: "{} media attached" uploading-media: "Uploading {} media" - mk-timeline-post: + mk-note-page: + prev: "Previous note" + next: "Next note" + + mk-settings: + profile: "Profile" + mute: "Mute" + drive: "Drive" + security: "Security" + password: "Password" + 2fa: "Two-factor authentication" + other: "Other" + license: "License" + + mk-timeline-note: reposted-by: "Reposted by {}" reply: "Reply" - repost: "Repost" + renote: "Renote" add-reaction: "Add your reaction" detail: "Show detail" @@ -249,7 +408,7 @@ desktop: title: "Server info" toggle: "Toggle views" - mk-activity-home-widget: + mk-activity-widget: title: "Activity" toggle: "Toggle views" @@ -276,24 +435,79 @@ desktop: title: "Donation" text: "To manage Misskey we spend money for our domain server etc.. There's no incomes for us so we need your tip. If you're interested contact {}. Thank you for your contribution!" - mk-repost-form: + mk-channel-home-widget: + title: "Channel" + settings: "Widget settings" + get-started: "Please click the cog in the upper right to specify the channel to receive" + + mk-calendar-widget: + title: "{1} / {2}" + prev: "Previous month" + next: "Next month" + go: "Click to travel" + + mk-post-form-home-widget: + title: "Post" + note: "Post" + placeholder: "What's happening?" + + mk-access-log-home-widget: + title: "Access log" + + mk-messaging-home-widget: + title: "Messaging" + + mk-broadcast-home-widget: + fetching: "Fetching" + no-broadcasts: "No broadcasts" + have-a-nice-day: "Have a nice day!" + next: "Next" + + mk-renote-form: quote: "Quote..." cancel: "Cancel" - repost: "Repost" + renote: "Renote" reposting: "Reposting..." success: "Reposted!" - failure: "Failed to Repost" + failure: "Failed to Renote" - mk-repost-form-window: - title: "Are you sure you want to repost this post?" + mk-renote-form-window: + title: "Are you sure you want to renote this note?" + + mk-user: + last-used-at: "Last used at" + + follows-you: "Follows you" + mute: "Mute" + muted: "Muting" + unmute: "Unmute" + + photos: + title: "Photos" + loading: "Loading" + no-photos: "No photos" + + frequently-replied-users: + title: "Frequently replied" + loading: "Loading" + no-users: "No users" + + followers-you-know: + title: "Followers you know" + loading: "Loading" + no-users: "No users" mobile: tags: + mk-selectdrive-page: + select-file: "Select file(s)" + mk-drive-file-viewer: download: "Download" rename: "Rename" move: "Move" - hash: "Hash" + hash: "Hash (md5)" + exif: "EXIF" mk-entrance-signin: signup: "Sign up" @@ -325,19 +539,45 @@ mobile: mk-notifications-page: notifications: "Notifications" + read-all: "Are you sure you want to mark all unread notifications as read?" - mk-post-page: - submit: "Post" + mk-note-page: + title: "Post" + prev: "Previous note" + next: "Next note" mk-search-page: search: "Search" + mk-settings: + signed-in-as: "Signed in as {}" + mk-settings-page: profile: "Profile" applications: "Applications" twitter-integration: "Twitter integration" signin-history: "Sign in history" + link: "MisskeyLink" settings: "Settings" + signout: "Sign out" + + mk-profile-setting-page: + title: "Profile Settings" + + mk-profile-setting: + will-be-published: "These profiles will be published." + name: "Name" + location: "Location" + description: "Description" + birthday: "Birthday" + avatar: "Avatar" + banner: "Banner" + avatar-saved: "Avatar updated successfully" + banner-saved: "Banner updated successfully" + set-avatar: "Choose an avatar" + set-banner: "Choose a banner" + save: "Save" + saved: "Profile updated successfully" mk-user-followers-page: followers-of: "Followers of {}" @@ -366,40 +606,40 @@ mobile: unfollow: "Unfollow" mk-home-timeline: - empty-timeline: "There is no posts" + empty-timeline: "There is no notes" mk-notifications: more: "More" empty: "No notifications" - mk-post-detail: + mk-note-detail: reply: "Reply" reaction: "Reaction" mk-post-form: submit: "Post" - reply-placeholder: "Reply to this post..." - post-placeholder: "What's happening?" - attach-media-from-local: "Attach media from your device" + reply-placeholder: "Reply to this note..." + note-placeholder: "What's happening?" - mk-search-posts: - empty: "There is no post related to the 「{}」" + mk-search-notes: + empty: "There is no note related to the 「{}」" - mk-sub-post-content: + mk-sub-note-content: media-count: "{} media" poll: "Poll" - mk-timeline-post: + mk-timeline-note: reposted-by: "Reposted by {}" mk-timeline: - empty: "No posts" + empty: "No notes" load-more: "More" mk-ui-nav: home: "Home" notifications: "Notifications" messaging: "Messages" + ch: "Channels" drive: "Drive" settings: "Settings" about: "About Misskey" @@ -412,23 +652,58 @@ mobile: no-users: "No following." mk-user-timeline: - no-posts: "This user seems never post" - no-posts-with-media: "There is no posts with media" + no-notes: "This user seems never note" + no-notes-with-media: "There is no notes with media" + load-more: "More" mk-user: - is-followed: "Followed you" + follows-you: "Follows you" following: "Following" followers: "Followers" - posts: "Timeline" + notes: "Posts" + overview: "Overview" + timeline: "Timeline" media: "Media" + mk-user-overview: + recent-notes: "Recent notes" + images: "Images" + activity: "Activity" + keywords: "Keywords" + domains: "Domains" + frequently-replied-users: "Frequently talking users" + followers-you-know: "Followers you know" + last-used-at: "Last used at" + + mk-user-overview-notes: + loading: "Loading" + no-notes: "No notes" + + mk-user-overview-photos: + loading: "Loading" + no-photos: "No photos" + + mk-user-overview-keywords: + no-keywords: "No keywords" + + mk-user-overview-domains: + no-domains: "No domains" + + mk-user-overview-frequently-replied-users: + loading: "Loading" + no-users: "No users" + + mk-user-overview-followers-you-know: + loading: "Loading" + no-users: "No users" + mk-users-list: all: "All" known: "You know" load-more: "More" stats: - posts-count: "Number of all posts" + notes-count: "Number of all notes" users-count: "Number of all users" status: diff --git a/webpack/langs.ts b/locales/index.ts similarity index 79% rename from webpack/langs.ts rename to locales/index.ts index 409b25504a..ced3b4cb32 100644 --- a/webpack/langs.ts +++ b/locales/index.ts @@ -10,12 +10,12 @@ const loadLang = lang => yaml.safeLoad( const native = loadLang('ja'); -const langs = Object.entries({ - 'en': loadLang('en'), +const langs = { + //'en': loadLang('en'), 'ja': native -}); +}; -langs.map(([, locale]) => { +Object.entries(langs).map(([, locale]) => { // Extend native language (Japanese) locale = Object.assign({}, native, locale); }); diff --git a/locales/ja.yml b/locales/ja.yml index e5b2beaed1..4d4c853625 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -1,4 +1,6 @@ common: + misskey: "Misskeyに何でも投稿して皆と共有しましょう。" + time: unknown: "なぞのじかん" future: "未来" @@ -11,9 +13,18 @@ common: months_ago: "{}ヶ月前" years_ago: "{}年前" + weekday-short: + sunday: "日" + monday: "月" + tuesday: "火" + wednesday: "水" + thursday: "木" + friday: "金" + satruday: "土" + reactions: like: "いいね" - love: "ハート" + love: "しゅき" laugh: "笑" hmm: "ふぅ~む" surprise: "わお" @@ -22,14 +33,32 @@ common: confused: "こまこまのこまり" pudding: "Pudding" + note_categories: + music: "音楽" + game: "ゲーム" + anime: "アニメ" + it: "IT" + gadgets: "ガジェット" + photography: "写真" + input-message-here: "ここにメッセージを入力" send: "送信" delete: "削除" loading: "読み込み中" ok: "わかった" update-available: "Misskeyの新しいバージョンがあります({newer}。現在{current}を利用中)。ページを再度読み込みすると更新が適用されます。" + my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" tags: + mk-nav-links: + about: "Misskeyについて" + stats: "統計" + status: "ステータス" + wiki: "Wiki" + donors: "ドナー" + repository: "リポジトリ" + develop: "開発者" + mk-messaging-form: attach-from-local: "PCからファイルを添付する" attach-from-drive: "ドライブからファイルを添付する" @@ -55,8 +84,27 @@ common: mk-error: title: "サーバーに接続できません" - description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。" + description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。" thanks: "いつもMisskeyをご利用いただきありがとうございます。" + troubleshoot: "トラブルシュート" + + troubleshooter: + title: "トラブルシューティング" + network: "ネットワーク接続" + checking-network: "ネットワーク接続を確認中" + internet: "インターネット接続" + checking-internet: "インターネット接続を確認中" + server: "サーバー接続" + checking-server: "サーバー接続を確認中" + finding: "問題を調べています" + no-network: "ネットワークに接続されていません" + no-network-desc: "お使いのPCのネットワーク接続が正常か確認してください。" + no-internet: "インターネットに接続されていません" + no-internet-desc: "ネットワークには接続されていますが、インターネットには接続されていないようです。お使いのPCのインターネット接続が正常か確認してください。" + no-server: "Misskeyのサーバーに接続できません" + no-server-desc: "お使いのPCのインターネット接続は正常ですが、Misskeyのサーバーには接続できませんでした。サーバーがダウンまたはメンテナンスしている可能性があるので、しばらくしてから再度御アクセスください。" + success: "Misskeyのサーバーに接続できました" + success-desc: "正常に接続できるようです。ページを再度読み込みしてください。" mk-forkit: open-github-link: "View source on Github" @@ -76,12 +124,20 @@ common: show-result: "結果を見る" voted: "投票済み" + mk-note-menu: + pin: "ピン留め" + pinned: "ピン留めしました" + select: "カテゴリを選択" + categorize: "決定" + categorized: "カテゴリを報告しました。これによりMisskeyが賢くなり、投稿の自動カテゴライズに役立てられます。ご協力ありがとうございました。" + mk-reaction-picker: choose-reaction: "リアクションを選択" mk-signin: username: "ユーザー名" password: "パスワード" + token: "トークン" signing-in: "やってます..." signin: "サインイン" @@ -91,8 +147,8 @@ common: available: "利用できます" unavailable: "既に利用されています" error: "通信エラー" - invalid-format: "a~z、A~Z、0~9、-(ハイフン)が使えます" - too-short: "3文字以上でお願いします!" + invalid-format: "a~z、A~Z、0~9、_が使えます" + too-short: "1文字以上でお願いします!" too-long: "20文字以内でお願いします" password: "パスワード" password-placeholder: "8文字以上を推奨します" @@ -127,8 +183,46 @@ common: mk-uploader: waiting: "待機中" +docs: + edit-this-page-on-github: "間違いや改善点を見つけましたか?" + edit-this-page-on-github-link: "このページをGitHubで編集" + + api: + entities: + properties: "プロパティ" + endpoints: + params: "パラメータ" + res: "レスポンス" + props: + name: "名前" + type: "型" + optional: "オプション" + description: "説明" + yes: "はい" + no: "いいえ" + +ch: + tags: + mk-index: + new: "チャンネルを作成" + channel-title: "チャンネルのタイトル" + + mk-channel-form: + textarea: "書いて" + upload: "アップロード" + drive: "ドライブ" + note: "やる" + posting: "やってます" + desktop: tags: + mk-api-info: + intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。" + caution: "アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。" + regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できます。" + regenerate-token: "トークンを再生成" + enter-password: "パスワードを入力してください" + mk-drive-browser-base-contextmenu: create-folder: "フォルダーを作成" upload: "ファイルをアップロード" @@ -172,7 +266,6 @@ desktop: mk-drive-browser-file: avatar: "アバター" banner: "バナー" - wallpaper: "壁紙" mk-drive-browser-folder-contextmenu: move-to-this-folder: "このフォルダへ移動" @@ -189,9 +282,16 @@ desktop: mk-drive-browser-nav-folder: drive: "ドライブ" + mk-selectdrive-page: + title: "ファイルを選択してください" + ok: "決定" + cancel: "キャンセル" + upload: "PCからドライブにファイルをアップロード" + mk-ui-header-nav: home: "ホーム" messaging: "メッセージ" + ch: "チャンネル" info: "お知らせ" mk-ui-header-search: @@ -204,19 +304,64 @@ desktop: settings: "設定" signout: "サインアウト" + mk-ui-header-note-button: + note: "新規投稿" + + mk-ui-header-notifications: + title: "通知" + + mk-profile-setting: + avatar: "アバター" + choice-avatar: "画像を選択" + name: "名前" + location: "場所" + description: "自己紹介" + birthday: "誕生日" + save: "保存" + + mk-password-setting: + reset: "パスワードを変更する" + enter-current-password: "現在のパスワードを入力してください" + enter-new-password: "新しいパスワードを入力してください" + enter-new-password-again: "もう一度新しいパスワードを入力してください" + not-match: "新しいパスワードが一致しません" + changed: "パスワードを変更しました" + + mk-2fa-setting: + intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。" + detail: "詳細..." + url: "https://www.google.co.jp/intl/ja/landing/2step/" + caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできなくなりますのでご注意ください。" + register: "デバイスを登録する" + already-registered: "既に設定は完了しています。" + unregister: "設定を解除" + unregistered: "二段階認証が無効になりました。" + enter-password: "パスワードを入力してください" + authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:" + howtoinstall: "インストール方法はこちら" + scan: "次に、表示されているQRコードをスキャンします:" + done: "お使いのデバイスに表示されているトークンを入力して完了します:" + submit: "完了" + success: "設定が完了しました!" + failed: "設定に失敗しました。トークンに誤りがないかご確認ください。" + info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されているトークンを入力します。" + + mk-mute-setting: + no-users: "ミュートしているユーザーはいません" + mk-post-form: - post-placeholder: "いまどうしてる?" + note-placeholder: "いまどうしてる?" reply-placeholder: "この投稿への返信..." quote-placeholder: "この投稿を引用..." - post: "投稿" + note: "投稿" reply: "返信" - repost: "Repost" + renote: "Renote" posted: "投稿しました!" replied: "返信しました!" - reposted: "Repostしました!" - post-failed: "投稿に失敗しました" + reposted: "Renoteしました!" + note-failed: "投稿に失敗しました" reply-failed: "返信に失敗しました" - repost-failed: "Repostに失敗しました" + renote-failed: "Renoteに失敗しました" posting: "投稿中" attach-media-from-local: "PCからメディアを添付" attach-media-from-drive: "ドライブからメディアを添付" @@ -226,15 +371,29 @@ desktop: text-remain: "のこり{}文字" mk-post-form-window: - post: "新規投稿" + note: "新規投稿" reply: "返信" attaches: "添付: {}メディア" uploading-media: "{}個のメディアをアップロード中" - mk-timeline-post: - reposted-by: "{}がRepost" + mk-note-page: + prev: "前の投稿" + next: "次の投稿" + + mk-settings: + profile: "プロフィール" + mute: "ミュート" + drive: "ドライブ" + security: "セキュリティ" + password: "パスワード" + 2fa: "二段階認証" + other: "その他" + license: "ライセンス" + + mk-timeline-note: + reposted-by: "{}がRenote" reply: "返信" - repost: "Repost" + renote: "Renote" add-reaction: "リアクション" detail: "詳細" @@ -249,7 +408,7 @@ desktop: title: "サーバー情報" toggle: "表示を切り替え" - mk-activity-home-widget: + mk-activity-widget: title: "アクティビティ" toggle: "表示を切り替え" @@ -276,24 +435,79 @@ desktop: title: "寄付のお願い" text: "Misskeyの運営にはドメイン、サーバー等のコストが掛かります。Misskeyは広告を掲載したりしないため、収入を皆様からの寄付に頼っています。もしご興味があれば、{}までご連絡ください。ご協力ありがとうございます。" - mk-repost-form: + mk-channel-home-widget: + title: "チャンネル" + settings: "ウィジェットの設定" + get-started: "右上の歯車をクリックして受信するチャンネルを指定してください" + + mk-calendar-widget: + title: "{1}年 {2}月" + prev: "前の月" + next: "次の月" + go: "クリックして時間遡行" + + mk-post-form-home-widget: + title: "投稿" + note: "投稿" + placeholder: "いまどうしてる?" + + mk-access-log-home-widget: + title: "アクセスログ" + + mk-messaging-home-widget: + title: "メッセージ" + + mk-broadcast-home-widget: + fetching: "確認中" + no-broadcasts: "お知らせはありません" + have-a-nice-day: "良い一日を!" + next: "次" + + mk-renote-form: quote: "引用する..." cancel: "キャンセル" - repost: "Repost" + renote: "Renote" reposting: "しています..." - success: "Repostしました!" - failure: "Repostに失敗しました" + success: "Renoteしました!" + failure: "Renoteに失敗しました" - mk-repost-form-window: - title: "この投稿をRepostしますか?" + mk-renote-form-window: + title: "この投稿をRenoteしますか?" + + mk-user: + last-used-at: "最終アクセス" + + follows-you: "フォローされています" + mute: "ミュートする" + muted: "ミュートしています" + unmute: "ミュート解除" + + photos: + title: "フォト" + loading: "読み込み中" + no-photos: "写真はありません" + + frequently-replied-users: + title: "よく話すユーザー" + loading: "読み込み中" + no-users: "よく話すユーザーはいません" + + followers-you-know: + title: "知り合いのフォロワー" + loading: "読み込み中" + no-users: "知り合いのフォロワーはいません" mobile: tags: + mk-selectdrive-page: + select-file: "ファイルを選択" + mk-drive-file-viewer: download: "ダウンロード" rename: "名前を変更" move: "移動" - hash: "ハッシュ" + hash: "ハッシュ (md5)" + exif: "EXIF" mk-entrance-signin: signup: "新規登録" @@ -325,19 +539,45 @@ mobile: mk-notifications-page: notifications: "通知" + read-all: "すべての通知を既読にしますか?" - mk-post-page: - submit: "投稿" + mk-note-page: + title: "投稿" + prev: "前の投稿" + next: "次の投稿" mk-search-page: search: "検索" + mk-settings: + signed-in-as: "{}としてサインイン中" + mk-settings-page: profile: "プロフィール" applications: "アプリケーション" twitter-integration: "Twitter連携" - signin-history: "ログイン履歴" + signin-history: "サインイン履歴" + link: "Misskeyリンク" settings: "設定" + signout: "サインアウト" + + mk-profile-setting-page: + title: "プロフィール設定" + + mk-profile-setting: + will-be-published: "これらのプロフィールは公開されます。" + name: "名前" + location: "場所" + description: "自己紹介" + birthday: "誕生日" + avatar: "アバター" + banner: "バナー" + avatar-saved: "アバターを保存しました" + banner-saved: "バナーを保存しました" + set-avatar: "アバターを選択する" + set-banner: "バナーを選択する" + save: "保存" + saved: "プロフィールを保存しました" mk-user-followers-page: followers-of: "{}のフォロワー" @@ -372,25 +612,24 @@ mobile: more: "もっと見る" empty: "ありません!" - mk-post-detail: + mk-note-detail: reply: "返信" reaction: "リアクション" mk-post-form: submit: "投稿" reply-placeholder: "この投稿への返信..." - post-placeholder: "いまどうしてる?" - attach-media-from-local: "デバイスからメディアを添付" + note-placeholder: "いまどうしてる?" - mk-search-posts: + mk-search-notes: empty: "「{}」に関する投稿は見つかりませんでした。" - mk-sub-post-content: + mk-sub-note-content: media-count: "{}個のメディア" poll: "投票" - mk-timeline-post: - reposted-by: "{}がRepost" + mk-timeline-note: + reposted-by: "{}がRenote" mk-timeline: empty: "表示するものがありません" @@ -400,6 +639,7 @@ mobile: home: "ホーム" notifications: "通知" messaging: "メッセージ" + ch: "チャンネル" search: "検索" drive: "ドライブ" settings: "設定" @@ -412,24 +652,58 @@ mobile: no-users: "フォロー中のユーザーはいないようです。" mk-user-timeline: - no-posts: "このユーザーはまだ投稿していないようです。" - no-posts-with-media: "メディア付き投稿はありません。" + no-notes: "このユーザーはまだ投稿していないようです。" + no-notes-with-media: "メディア付き投稿はありません。" + load-more: "もっとみる" mk-user: - is-followed: "フォローされています" + follows-you: "フォローされています" following: "フォロー" followers: "フォロワー" - posts: "タイムライン" - posts-count: "ポスト" + notes: "投稿" + overview: "概要" + timeline: "タイムライン" media: "メディア" + mk-user-overview: + recent-notes: "最近の投稿" + images: "画像" + activity: "アクティビティ" + keywords: "キーワード" + domains: "頻出ドメイン" + frequently-replied-users: "よく会話するユーザー" + followers-you-know: "知り合いのフォロワー" + last-used-at: "最終ログイン" + + mk-user-overview-notes: + loading: "読み込み中" + no-notes: "投稿はありません" + + mk-user-overview-photos: + loading: "読み込み中" + no-photos: "写真はありません" + + mk-user-overview-keywords: + no-keywords: "キーワードはありません(十分な数の投稿をしていない可能性があります)" + + mk-user-overview-domains: + no-domains: "よく表れるドメインは検出されませんでした" + + mk-user-overview-frequently-replied-users: + loading: "読み込み中" + no-users: "よく会話するユーザーはいません" + + mk-user-overview-followers-you-know: + loading: "読み込み中" + no-users: "知り合いのユーザーはいません" + mk-users-list: all: "すべて" known: "知り合い" load-more: "もっと" stats: - posts-count: "投稿の数" + notes-count: "投稿の数" users-count: "アカウントの数" status: diff --git a/package.json b/package.json index 875ffc3c66..1e0e50551e 100644 --- a/package.json +++ b/package.json @@ -1,155 +1,219 @@ { - "name": "misskey", - "author": "syuilo ", - "version": "0.0.2380", - "license": "MIT", - "description": "A miniblog-based SNS", - "bugs": "https://github.com/syuilo/misskey/issues", - "repository": "https://github.com/syuilo/misskey.git", - "main": "./built/index.js", - "private": true, - "scripts": { - "config": "node ./tools/init.js", - "start": "node ./built", - "debug": "DEBUG=misskey:* node ./built", - "swagger": "node ./swagger.js", - "build": "gulp build", - "rebuild": "gulp rebuild", - "clean": "gulp clean", - "cleanall": "gulp cleanall", - "lint": "gulp lint", - "test": "gulp test" - }, - "devDependencies": { - "@types/bcryptjs": "2.4.0", - "@types/body-parser": "1.16.4", - "@types/chai": "4.0.3", - "@types/chai-http": "3.0.2", - "@types/chalk": "0.4.31", - "@types/compression": "0.0.33", - "@types/cors": "2.8.1", - "@types/debug": "0.0.30", - "@types/deep-equal": "1.0.0", - "@types/elasticsearch": "5.0.14", - "@types/event-stream": "3.3.31", - "@types/express": "4.0.36", - "@types/gm": "1.17.32", - "@types/gulp": "4.0.3", - "@types/gulp-htmlmin": "1.3.30", - "@types/gulp-mocha": "0.0.30", - "@types/gulp-rename": "0.0.32", - "@types/gulp-replace": "0.0.30", - "@types/gulp-tslint": "3.6.31", - "@types/gulp-typescript": "2.13.0", - "@types/gulp-uglify": "0.0.30", - "@types/gulp-util": "3.0.31", - "@types/inquirer": "0.0.34", - "@types/is-root": "1.0.0", - "@types/is-url": "1.2.28", - "@types/js-yaml": "3.9.0", - "@types/mocha": "2.2.41", - "@types/mongodb": "2.2.9", - "@types/monk": "1.0.5", - "@types/morgan": "1.7.32", - "@types/ms": "0.7.29", - "@types/multer": "1.3.2", - "@types/node": "8.0.20", - "@types/ratelimiter": "2.1.28", - "@types/redis": "2.6.0", - "@types/request": "2.0.0", - "@types/rimraf": "2.0.0", - "@types/riot": "3.6.0", - "@types/serve-favicon": "2.2.28", - "@types/uuid": "3.4.0", - "@types/webpack": "3.0.8", - "@types/webpack-stream": "3.2.7", - "@types/websocket": "0.0.34", - "chai": "4.1.1", - "chai-http": "3.0.0", - "css-loader": "0.28.4", - "event-stream": "3.3.4", - "gulp": "3.9.1", - "gulp-cssnano": "2.1.2", - "gulp-imagemin": "3.3.0", - "gulp-htmlmin": "3.0.0", - "gulp-mocha": "4.3.1", - "gulp-pug": "3.3.0", - "gulp-rename": "1.2.2", - "gulp-replace": "0.6.1", - "gulp-tslint": "8.1.2", - "gulp-typescript": "3.2.1", - "gulp-uglify": "3.0.0", - "gulp-util": "3.0.8", - "mocha": "3.5.0", - "riot-tag-loader": "1.0.0", - "string-replace-webpack-plugin": "0.1.3", - "style-loader": "0.18.2", - "stylus": "0.54.5", - "stylus-loader": "3.0.1", - "swagger-jsdoc": "1.9.7", - "tslint": "5.6.0", - "uglify-es": "3.0.27", - "uglify-es-webpack-plugin": "0.10.0", - "uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony", - "webpack": "3.5.4" - }, - "dependencies": { - "accesses": "2.5.0", - "animejs": "2.0.2", - "autwh": "0.0.1", - "bcryptjs": "2.4.3", - "body-parser": "1.17.2", - "cafy": "2.4.0", - "chalk": "2.1.0", - "compression": "1.7.0", - "cors": "2.8.4", - "cropperjs": "1.0.0-rc.3", - "crypto": "1.0.1", - "debug": "3.0.0", - "deep-equal": "1.0.1", - "deepcopy": "0.6.3", - "diskusage": "^0.2.2", - "download": "6.2.5", - "elasticsearch": "13.3.1", - "escape-regexp": "0.0.1", - "express": "4.15.4", - "file-type": "5.2.0", - "fuckadblock": "3.2.1", - "gm": "1.23.0", - "inquirer": "3.2.1", - "is-root": "1.0.0", - "is-url": "1.2.2", - "js-yaml": "3.9.1", - "mongodb": "2.2.31", - "monk": "6.0.3", - "morgan": "1.8.2", - "ms": "2.0.0", - "multer": "1.3.0", - "nprogress": "0.2.0", - "os-utils": "0.0.14", - "page": "1.7.1", - "pictograph": "2.0.4", - "prominence": "0.2.0", - "pug": "2.0.0-rc.3", - "ratelimiter": "3.0.3", - "recaptcha-promise": "0.1.3", - "reconnecting-websocket": "3.1.1", - "redis": "2.8.0", - "request": "2.81.0", - "rimraf": "2.6.1", - "riot": "3.6.1", - "rndstr": "1.0.0", - "s-age": "1.1.0", - "serve-favicon": "2.4.3", - "summaly": "2.0.3", - "syuilo-password-strength": "0.0.1", - "tcp-port-used": "0.1.2", - "textarea-caret": "3.0.2", - "ts-node": "3.3.0", - "typescript": "2.4.2", - "uuid": "3.1.0", - "vhost": "3.0.2", - "websocket": "1.0.24", - "xev": "2.0.0" - } + "name": "misskey", + "author": "syuilo ", + "version": "0.0.4771", + "codename": "nighthike", + "license": "MIT", + "description": "A miniblog-based SNS", + "bugs": "https://github.com/syuilo/misskey/issues", + "repository": "https://github.com/syuilo/misskey.git", + "main": "./built/index.js", + "private": true, + "scripts": { + "config": "node ./tools/init.js", + "start": "node ./built", + "debug": "DEBUG=misskey:* node ./built", + "swagger": "node ./swagger.js", + "build": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js && gulp build", + "webpack": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js", + "watch": "node --max_old_space_size=16384 ./node_modules/webpack/bin/webpack.js --watch", + "gulp": "gulp build", + "rebuild": "gulp rebuild", + "clean": "gulp clean", + "cleanall": "gulp cleanall", + "lint": "gulp lint", + "test": "gulp test", + "format": "gulp format" + }, + "dependencies": { + "@fortawesome/fontawesome": "1.0.1", + "@fortawesome/fontawesome-free-brands": "5.0.2", + "@fortawesome/fontawesome-free-regular": "5.0.2", + "@fortawesome/fontawesome-free-solid": "5.0.2", + "@prezzemolo/rap": "0.1.2", + "@prezzemolo/zip": "0.0.3", + "@types/bcryptjs": "2.4.1", + "@types/body-parser": "1.16.8", + "@types/chai": "4.1.2", + "@types/chai-http": "3.0.4", + "@types/compression": "0.0.36", + "@types/cookie": "0.3.1", + "@types/cors": "2.8.3", + "@types/debug": "0.0.30", + "@types/deep-equal": "1.0.1", + "@types/elasticsearch": "5.0.22", + "@types/eventemitter3": "2.0.2", + "@types/express": "4.11.1", + "@types/gm": "1.17.33", + "@types/gulp": "3.8.36", + "@types/gulp-htmlmin": "1.3.32", + "@types/gulp-mocha": "0.0.32", + "@types/gulp-rename": "0.0.33", + "@types/gulp-replace": "0.0.31", + "@types/gulp-uglify": "3.0.5", + "@types/gulp-util": "3.0.34", + "@types/inquirer": "0.0.41", + "@types/is-root": "1.0.0", + "@types/is-url": "1.2.28", + "@types/js-yaml": "3.11.1", + "@types/kue": "^0.11.8", + "@types/license-checker": "15.0.0", + "@types/mkdirp": "0.5.2", + "@types/mocha": "5.0.0", + "@types/mongodb": "3.0.12", + "@types/monk": "6.0.0", + "@types/morgan": "1.7.35", + "@types/ms": "0.7.30", + "@types/multer": "1.3.6", + "@types/node": "9.6.2", + "@types/nopt": "3.0.29", + "@types/proxy-addr": "2.0.0", + "@types/pug": "2.0.4", + "@types/qrcode": "0.8.1", + "@types/ratelimiter": "2.1.28", + "@types/redis": "2.8.6", + "@types/request": "2.47.0", + "@types/request-promise-native": "1.0.14", + "@types/rimraf": "2.0.2", + "@types/seedrandom": "2.4.27", + "@types/serve-favicon": "2.2.30", + "@types/speakeasy": "2.0.2", + "@types/tmp": "0.0.33", + "@types/uuid": "3.4.3", + "@types/webpack": "4.1.3", + "@types/webpack-stream": "3.2.10", + "@types/websocket": "0.0.38", + "@types/ws": "4.0.2", + "accesses": "2.5.0", + "animejs": "2.2.0", + "autosize": "4.0.1", + "autwh": "0.1.0", + "bcryptjs": "2.4.3", + "body-parser": "1.18.2", + "bootstrap-vue": "2.0.0-rc.6", + "cafy": "3.2.1", + "chai": "4.1.2", + "chai-http": "4.0.0", + "chalk": "2.3.2", + "compression": "1.7.2", + "cookie": "0.3.1", + "cors": "2.8.4", + "crc-32": "1.2.0", + "css-loader": "0.28.11", + "debug": "3.1.0", + "deep-equal": "1.0.1", + "deepcopy": "0.6.3", + "diskusage": "0.2.4", + "dompurify": "^1.0.3", + "elasticsearch": "14.2.2", + "element-ui": "2.3.3", + "emojilib": "2.2.12", + "escape-regexp": "0.0.1", + "eslint": "4.19.1", + "eslint-plugin-vue": "4.4.0", + "eventemitter3": "3.0.1", + "exif-js": "2.3.0", + "express": "4.16.3", + "file-loader": "1.1.11", + "file-type": "7.6.0", + "fuckadblock": "3.2.1", + "gm": "1.23.1", + "gulp": "3.9.1", + "gulp-cssnano": "2.1.3", + "gulp-htmlmin": "4.0.0", + "gulp-imagemin": "4.1.0", + "gulp-mocha": "5.0.0", + "gulp-pug": "4.0.0", + "gulp-rename": "1.2.2", + "gulp-replace": "0.6.1", + "gulp-sourcemaps": "2.6.4", + "gulp-stylus": "2.7.0", + "gulp-tslint": "8.1.3", + "gulp-typescript": "4.0.2", + "gulp-uglify": "3.0.0", + "gulp-util": "3.0.8", + "hard-source-webpack-plugin": "0.6.4", + "highlight.js": "9.12.0", + "html-minifier": "3.5.14", + "http-signature": "^1.2.0", + "inquirer": "5.2.0", + "is-root": "2.0.0", + "is-url": "1.2.4", + "js-yaml": "3.11.0", + "jsdom": "11.7.0", + "kue": "0.11.6", + "license-checker": "18.0.0", + "loader-utils": "1.1.0", + "mecab-async": "0.1.2", + "mkdirp": "0.5.1", + "mocha": "5.0.5", + "moji": "0.5.1", + "mongodb": "3.0.6", + "monk": "6.0.5", + "morgan": "1.9.0", + "ms": "2.1.1", + "multer": "1.3.0", + "nan": "2.10.0", + "node-sass": "4.8.3", + "node-sass-json-importer": "3.1.6", + "nopt": "4.0.1", + "nprogress": "0.2.0", + "object-assign-deep": "0.4.0", + "on-build-webpack": "0.1.0", + "os-utils": "0.0.14", + "progress-bar-webpack-plugin": "1.11.0", + "prominence": "0.2.0", + "proxy-addr": "2.0.3", + "pug": "2.0.3", + "punycode": "2.1.0", + "qrcode": "1.2.0", + "ratelimiter": "3.0.3", + "recaptcha-promise": "0.1.3", + "reconnecting-websocket": "3.2.2", + "redis": "2.8.0", + "request": "2.85.0", + "request-promise-native": "1.0.5", + "rimraf": "2.6.2", + "rndstr": "1.0.0", + "s-age": "1.1.2", + "sass-loader": "6.0.7", + "seedrandom": "2.4.3", + "serve-favicon": "2.5.0", + "speakeasy": "2.0.0", + "style-loader": "0.20.3", + "stylus": "0.54.5", + "stylus-loader": "3.0.2", + "summaly": "2.0.3", + "swagger-jsdoc": "1.9.7", + "syuilo-password-strength": "0.0.1", + "tcp-port-used": "0.1.2", + "textarea-caret": "3.1.0", + "tmp": "0.0.33", + "ts-loader": "4.1.0", + "ts-node": "5.0.1", + "tslint": "5.9.1", + "typescript": "2.8.1", + "typescript-eslint-parser": "14.0.0", + "uglify-es": "3.3.9", + "url-loader": "1.0.1", + "uuid": "3.2.1", + "v-animate-css": "0.0.2", + "vhost": "3.0.2", + "vue": "2.5.16", + "vue-cropperjs": "2.2.0", + "vue-js-modal": "1.3.12", + "vue-json-tree-view": "2.1.3", + "vue-loader": "14.2.2", + "vue-router": "3.0.1", + "vue-template-compiler": "2.5.16", + "vuedraggable": "2.16.0", + "web-push": "3.3.0", + "webfinger.js": "2.6.6", + "webpack": "4.5.0", + "webpack-cli": "2.0.14", + "webpack-replace-loader": "1.3.0", + "websocket": "1.0.25", + "ws": "5.1.1", + "xev": "2.0.0" + } } diff --git a/src/acct/parse.ts b/src/acct/parse.ts new file mode 100644 index 0000000000..ef1f55405d --- /dev/null +++ b/src/acct/parse.ts @@ -0,0 +1,4 @@ +export default acct => { + const splitted = acct.split('@', 2); + return { username: splitted[0], host: splitted[1] || null }; +}; diff --git a/src/acct/render.ts b/src/acct/render.ts new file mode 100644 index 0000000000..9afb03d88b --- /dev/null +++ b/src/acct/render.ts @@ -0,0 +1,3 @@ +export default user => { + return user.host === null ? user.username : `${user.username}@${user.host}`; +}; diff --git a/src/api/api-handler.ts b/src/api/api-handler.ts deleted file mode 100644 index fb603a0e2a..0000000000 --- a/src/api/api-handler.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as express from 'express'; - -import { Endpoint } from './endpoints'; -import authenticate from './authenticate'; -import { IAuthContext } from './authenticate'; -import _reply from './reply'; -import limitter from './limitter'; - -export default async (endpoint: Endpoint, req: express.Request, res: express.Response) => { - const reply = _reply.bind(null, res); - let ctx: IAuthContext; - - // Authentication - try { - ctx = await authenticate(req); - } catch (e) { - return reply(403, 'AUTHENTICATION_FAILED'); - } - - if (endpoint.secure && !ctx.isSecure) { - return reply(403, 'ACCESS_DENIED'); - } - - if (endpoint.withCredential && ctx.user == null) { - return reply(401, 'PLZ_SIGNIN'); - } - - if (ctx.app && endpoint.kind) { - if (!ctx.app.permission.some(p => p === endpoint.kind)) { - return reply(403, 'ACCESS_DENIED'); - } - } - - if (endpoint.withCredential && endpoint.limit) { - try { - await limitter(endpoint, ctx); // Rate limit - } catch (e) { - // drop request if limit exceeded - return reply(429); - } - } - - let exec = require(`${__dirname}/endpoints/${endpoint.name}`); - - if (endpoint.withFile) { - exec = exec.bind(null, req.file); - } - - // API invoking - try { - const res = await exec(req.body, ctx.user, ctx.app, ctx.isSecure); - reply(res); - } catch (e) { - reply(400, e); - } -}; diff --git a/src/api/authenticate.ts b/src/api/authenticate.ts deleted file mode 100644 index d4cc3fc41f..0000000000 --- a/src/api/authenticate.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as express from 'express'; -import App from './models/app'; -import User from './models/user'; -import AccessToken from './models/access-token'; -import isNativeToken from './common/is-native-token'; - -export interface IAuthContext { - /** - * App which requested - */ - app: any; - - /** - * Authenticated user - */ - user: any; - - /** - * Weather if the request is via the User-Native Token or not - */ - isSecure: boolean; -} - -export default (req: express.Request) => new Promise(async (resolve, reject) => { - const token = req.body['i'] as string; - - if (token == null) { - return resolve({ app: null, user: null, isSecure: false }); - } - - if (isNativeToken(token)) { - const user = await User - .findOne({ token: token }); - - if (user === null) { - return reject('user not found'); - } - - return resolve({ - app: null, - user: user, - isSecure: true - }); - } else { - const accessToken = await AccessToken.findOne({ - hash: token.toLowerCase() - }); - - if (accessToken === null) { - return reject('invalid signature'); - } - - const app = await App - .findOne({ _id: accessToken.app_id }); - - const user = await User - .findOne({ _id: accessToken.user_id }); - - return resolve({ app: app, user: user, isSecure: false }); - } -}); diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts deleted file mode 100644 index 714eeb520d..0000000000 --- a/src/api/common/add-file-to-drive.ts +++ /dev/null @@ -1,172 +0,0 @@ -import * as mongodb from 'mongodb'; -import * as crypto from 'crypto'; -import * as gm from 'gm'; -import * as debug from 'debug'; -import fileType = require('file-type'); -import prominence = require('prominence'); -import DriveFile from '../models/drive-file'; -import DriveFolder from '../models/drive-folder'; -import serialize from '../serializers/drive-file'; -import event from '../event'; -import config from '../../conf'; - -const log = debug('misskey:register-drive-file'); - -/** - * Add file to drive - * - * @param user User who wish to add file - * @param fileName File name - * @param data Contents - * @param comment Comment - * @param type File type - * @param folderId Folder ID - * @param force If set to true, forcibly upload the file even if there is a file with the same hash. - * @return Object that represents added file - */ -export default ( - user: any, - data: Buffer, - name: string = null, - comment: string = null, - folderId: mongodb.ObjectID = null, - force: boolean = false -) => new Promise(async (resolve, reject) => { - log(`registering ${name} (user: ${user.username})`); - - // File size - const size = data.byteLength; - - log(`size is ${size}`); - - // File type - let mime = 'application/octet-stream'; - const type = fileType(data); - if (type !== null) { - mime = type.mime; - - if (name === null) { - name = `untitled.${type.ext}`; - } - } else { - if (name === null) { - name = 'untitled'; - } - } - - log(`type is ${mime}`); - - // Generate hash - const hash = crypto - .createHash('sha256') - .update(data) - .digest('hex') as string; - - log(`hash is ${hash}`); - - if (!force) { - // Check if there is a file with the same hash - const much = await DriveFile.findOne({ - user_id: user._id, - hash: hash - }); - - if (much !== null) { - log('file with same hash is found'); - return resolve(much); - } else { - log('file with same hash is not found'); - } - } - - // Calculate drive usage - const usage = ((await DriveFile - .aggregate([ - { $match: { user_id: user._id } }, - { $project: { - datasize: true - }}, - { $group: { - _id: null, - usage: { $sum: '$datasize' } - }} - ]))[0] || { - usage: 0 - }).usage; - - log(`drive usage is ${usage}`); - - // If usage limit exceeded - if (usage + size > user.drive_capacity) { - return reject('no-free-space'); - } - - // If the folder is specified - let folder: any = null; - if (folderId !== null) { - folder = await DriveFolder - .findOne({ - _id: folderId, - user_id: user._id - }); - - if (folder === null) { - return reject('folder-not-found'); - } - } - - let properties: any = null; - - // If the file is an image - if (/^image\/.*$/.test(mime)) { - // Calculate width and height to save in property - const g = gm(data, name); - const size = await prominence(g).size(); - properties = { - width: size.width, - height: size.height - }; - - log('image width and height is calculated'); - } - - // Create DriveFile document - const file = await DriveFile.insert({ - created_at: new Date(), - user_id: user._id, - folder_id: folder !== null ? folder._id : null, - data: data, - datasize: size, - type: mime, - name: name, - comment: comment, - hash: hash, - properties: properties - }); - - delete file.data; - - log(`drive file has been created ${file._id}`); - - resolve(file); - - // Serialize - const fileObj = await serialize(file); - - // Publish drive_file_created event - event(user._id, 'drive_file_created', fileObj); - - // Register to search database - if (config.elasticsearch.enable) { - const es = require('../../db/elasticsearch'); - es.index({ - index: 'misskey', - type: 'drive_file', - id: file._id.toString(), - body: { - name: file.name, - user_id: user._id.toString() - } - }); - } -}); diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts deleted file mode 100644 index e7ec37d4e4..0000000000 --- a/src/api/common/notify.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as mongo from 'mongodb'; -import Notification from '../models/notification'; -import event from '../event'; -import serialize from '../serializers/notification'; - -export default ( - notifiee: mongo.ObjectID, - notifier: mongo.ObjectID, - type: string, - content?: any -) => new Promise(async (resolve, reject) => { - if (notifiee.equals(notifier)) { - return resolve(); - } - - // Create notification - const notification = await Notification.insert(Object.assign({ - created_at: new Date(), - notifiee_id: notifiee, - notifier_id: notifier, - type: type, - is_read: false - }, content)); - - resolve(notification); - - // Publish notification event - event(notifiee, 'notification', - await serialize(notification)); -}); diff --git a/src/api/common/text/elements/mention.ts b/src/api/common/text/elements/mention.ts deleted file mode 100644 index e0fac4dd76..0000000000 --- a/src/api/common/text/elements/mention.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Mention - */ - -module.exports = text => { - const match = text.match(/^@[a-zA-Z0-9\-]+/); - if (!match) return null; - const mention = match[0]; - return { - type: 'mention', - content: mention, - username: mention.substr(1) - }; -}; diff --git a/src/api/common/watch-post.ts b/src/api/common/watch-post.ts deleted file mode 100644 index 1a50f0edaa..0000000000 --- a/src/api/common/watch-post.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as mongodb from 'mongodb'; -import Watching from '../models/post-watching'; - -export default async (me: mongodb.ObjectID, post: object) => { - // 自分の投稿はwatchできない - if (me.equals((post as any).user_id)) { - return; - } - - // if watching now - const exist = await Watching.findOne({ - post_id: (post as any)._id, - user_id: me, - deleted_at: { $exists: false } - }); - - if (exist !== null) { - return; - } - - await Watching.insert({ - created_at: new Date(), - post_id: (post as any)._id, - user_id: me - }); -}; diff --git a/src/api/endpoints/aggregation/posts/reaction.ts b/src/api/endpoints/aggregation/posts/reaction.ts deleted file mode 100644 index eb99b9d088..0000000000 --- a/src/api/endpoints/aggregation/posts/reaction.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../models/post'; -import Reaction from '../../../models/post-reaction'; - -/** - * Aggregate reaction of a post - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const datas = await Reaction - .aggregate([ - { $match: { post_id: post._id } }, - { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } - } - }}, - { $group: { - _id: '$date', - count: { $sum: 1 } - }} - ]); - - datas.forEach(data => { - data.date = data._id; - delete data._id; - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter(d => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: 0 - }); - } - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/posts/reactions.ts b/src/api/endpoints/aggregation/posts/reactions.ts deleted file mode 100644 index 2cd4588ae1..0000000000 --- a/src/api/endpoints/aggregation/posts/reactions.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../models/post'; -import Reaction from '../../../models/post-reaction'; - -/** - * Aggregate reactions of a post - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); - - const reactions = await Reaction - .find({ - post_id: post._id, - $or: [ - { deleted_at: { $exists: false } }, - { deleted_at: { $gt: startTime } } - ] - }, { - _id: false, - post_id: false - }, { - sort: { created_at: -1 } - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - let day = new Date(new Date().setDate(new Date().getDate() - i)); - day = new Date(day.setMilliseconds(999)); - day = new Date(day.setSeconds(59)); - day = new Date(day.setMinutes(59)); - day = new Date(day.setHours(23)); - // day = day.getTime(); - - const count = reactions.filter(r => - r.created_at < day && (r.deleted_at == null || r.deleted_at > day) - ).length; - - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: count - }); - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/posts/reply.ts b/src/api/endpoints/aggregation/posts/reply.ts deleted file mode 100644 index 02a60c8969..0000000000 --- a/src/api/endpoints/aggregation/posts/reply.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../models/post'; - -/** - * Aggregate reply of a post - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const datas = await Post - .aggregate([ - { $match: { reply_to: post._id } }, - { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } - } - }}, - { $group: { - _id: '$date', - count: { $sum: 1 } - }} - ]); - - datas.forEach(data => { - data.date = data._id; - delete data._id; - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter(d => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: 0 - }); - } - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/posts/repost.ts b/src/api/endpoints/aggregation/posts/repost.ts deleted file mode 100644 index 217159caa7..0000000000 --- a/src/api/endpoints/aggregation/posts/repost.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../../models/post'; - -/** - * Aggregate repost of a post - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - const datas = await Post - .aggregate([ - { $match: { repost_id: post._id } }, - { $project: { - created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$created_at' }, - month: { $month: '$created_at' }, - day: { $dayOfMonth: '$created_at' } - } - }}, - { $group: { - _id: '$date', - count: { $sum: 1 } - }} - ]); - - datas.forEach(data => { - data.date = data._id; - delete data._id; - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter(d => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: 0 - }); - } - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/users/followers.ts b/src/api/endpoints/aggregation/users/followers.ts deleted file mode 100644 index 3022b2b002..0000000000 --- a/src/api/endpoints/aggregation/users/followers.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../../models/user'; -import Following from '../../../models/following'; - -/** - * Aggregate followers of a user - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Lookup user - const user = await User.findOne({ - _id: userId - }, { - fields: { - _id: true - } - }); - - if (user === null) { - return rej('user not found'); - } - - const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); - - const following = await Following - .find({ - followee_id: user._id, - $or: [ - { deleted_at: { $exists: false } }, - { deleted_at: { $gt: startTime } } - ] - }, { - _id: false, - follower_id: false, - followee_id: false - }, { - sort: { created_at: -1 } - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - let day = new Date(new Date().setDate(new Date().getDate() - i)); - day = new Date(day.setMilliseconds(999)); - day = new Date(day.setSeconds(59)); - day = new Date(day.setMinutes(59)); - day = new Date(day.setHours(23)); - // day = day.getTime(); - - const count = following.filter(f => - f.created_at < day && (f.deleted_at == null || f.deleted_at > day) - ).length; - - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: count - }); - } - - res(graph); -}); diff --git a/src/api/endpoints/aggregation/users/following.ts b/src/api/endpoints/aggregation/users/following.ts deleted file mode 100644 index 92da7e6921..0000000000 --- a/src/api/endpoints/aggregation/users/following.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../../models/user'; -import Following from '../../../models/following'; - -/** - * Aggregate following of a user - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Lookup user - const user = await User.findOne({ - _id: userId - }, { - fields: { - _id: true - } - }); - - if (user === null) { - return rej('user not found'); - } - - const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1)); - - const following = await Following - .find({ - follower_id: user._id, - $or: [ - { deleted_at: { $exists: false } }, - { deleted_at: { $gt: startTime } } - ] - }, { - _id: false, - follower_id: false, - followee_id: false - }, { - sort: { created_at: -1 } - }); - - const graph = []; - - for (let i = 0; i < 30; i++) { - let day = new Date(new Date().setDate(new Date().getDate() - i)); - day = new Date(day.setMilliseconds(999)); - day = new Date(day.setSeconds(59)); - day = new Date(day.setMinutes(59)); - day = new Date(day.setHours(23)); - - const count = following.filter(f => - f.created_at < day && (f.deleted_at == null || f.deleted_at > day) - ).length; - - graph.push({ - date: { - year: day.getFullYear(), - month: day.getMonth() + 1, // In JavaScript, month is zero-based. - day: day.getDate() - }, - count: count - }); - } - - res(graph); -}); diff --git a/src/api/endpoints/app/show.ts b/src/api/endpoints/app/show.ts deleted file mode 100644 index 054aab8596..0000000000 --- a/src/api/endpoints/app/show.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import App from '../../models/app'; -import serialize from '../../serializers/app'; - -/** - * @swagger - * /app/show: - * post: - * summary: Show an application's information - * description: Require app_id or name_id - * parameters: - * - - * name: app_id - * description: Application ID - * in: formData - * type: string - * - - * name: name_id - * description: Application unique name - * in: formData - * type: string - * - * responses: - * 200: - * description: Success - * schema: - * $ref: "#/definitions/Application" - * - * default: - * description: Failed - * schema: - * $ref: "#/definitions/Error" - */ - -/** - * Show an app - * - * @param {any} params - * @param {any} user - * @param {any} _ - * @param {any} isSecure - * @return {Promise} - */ -module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => { - // Get 'app_id' parameter - const [appId, appIdErr] = $(params.app_id).optional.id().$; - if (appIdErr) return rej('invalid app_id param'); - - // Get 'name_id' parameter - const [nameId, nameIdErr] = $(params.name_id).optional.string().$; - if (nameIdErr) return rej('invalid name_id param'); - - if (appId === undefined && nameId === undefined) { - return rej('app_id or name_id is required'); - } - - // Lookup app - const app = appId !== undefined - ? await App.findOne({ _id: appId }) - : await App.findOne({ name_id_lower: nameId.toLowerCase() }); - - if (app === null) { - return rej('app not found'); - } - - // Send response - res(await serialize(app, user, { - includeSecret: isSecure && app.user_id.equals(user._id) - })); -}); diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts deleted file mode 100644 index a68ae34817..0000000000 --- a/src/api/endpoints/drive/files.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import DriveFile from '../../models/drive-file'; -import serialize from '../../serializers/drive-file'; - -/** - * Get drive files - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - // Construct query - const sort = { - _id: -1 - }; - const query = { - user_id: user._id, - folder_id: folderId - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const files = await DriveFile - .find(query, { - fields: { - data: false - }, - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(files.map(async file => - await serialize(file)))); -}); diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts deleted file mode 100644 index 43dca7762a..0000000000 --- a/src/api/endpoints/drive/files/create.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Module dependencies - */ -import * as fs from 'fs'; -import $ from 'cafy'; -import { validateFileName } from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; -import create from '../../../common/add-file-to-drive'; - -/** - * Create a file - * - * @param {any} file - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (file, params, user) => new Promise(async (res, rej) => { - if (file == null) { - return rej('file is required'); - } - - const buffer = fs.readFileSync(file.path); - fs.unlink(file.path, (err) => { if (err) console.log(err); }); - - // Get 'name' parameter - let name = file.originalname; - if (name !== undefined && name !== null) { - name = name.trim(); - if (name.length === 0) { - name = null; - } else if (name === 'blob') { - name = null; - } else if (!validateFileName(name)) { - return rej('invalid name'); - } - } else { - name = null; - } - - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - // Create file - const driveFile = await create(user, buffer, name, null, folderId); - - // Serialize - const fileObj = await serialize(driveFile); - - // Response - res(fileObj); -}); diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts deleted file mode 100644 index 8dbc297e4f..0000000000 --- a/src/api/endpoints/drive/files/show.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import DriveFile from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; - -/** - * Show a file - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'file_id' parameter - const [fileId, fileIdErr] = $(params.file_id).id().$; - if (fileIdErr) return rej('invalid file_id param'); - - // Fetch file - const file = await DriveFile - .findOne({ - _id: fileId, - user_id: user._id - }, { - fields: { - data: false - } - }); - - if (file === null) { - return rej('file-not-found'); - } - - // Serialize - res(await serialize(file, { - detail: true - })); -}); diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts deleted file mode 100644 index 1cfbdd8f0b..0000000000 --- a/src/api/endpoints/drive/files/update.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import DriveFolder from '../../../models/drive-folder'; -import DriveFile from '../../../models/drive-file'; -import { validateFileName } from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; -import event from '../../../event'; - -/** - * Update a file - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'file_id' parameter - const [fileId, fileIdErr] = $(params.file_id).id().$; - if (fileIdErr) return rej('invalid file_id param'); - - // Fetch file - const file = await DriveFile - .findOne({ - _id: fileId, - user_id: user._id - }, { - fields: { - data: false - } - }); - - if (file === null) { - return rej('file-not-found'); - } - - // Get 'name' parameter - const [name, nameErr] = $(params.name).optional.string().pipe(validateFileName).$; - if (nameErr) return rej('invalid name param'); - if (name) file.name = name; - - // Get 'folder_id' parameter - const [folderId, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - if (folderId !== undefined) { - if (folderId === null) { - file.folder_id = null; - } else { - // Fetch folder - const folder = await DriveFolder - .findOne({ - _id: folderId, - user_id: user._id - }); - - if (folder === null) { - return rej('folder-not-found'); - } - - file.folder_id = folder._id; - } - } - - DriveFile.update(file._id, { - $set: { - name: file.name, - folder_id: file.folder_id - } - }); - - // Serialize - const fileObj = await serialize(file); - - // Response - res(fileObj); - - // Publish drive_file_updated event - event(user._id, 'drive_file_updated', fileObj); -}); diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts deleted file mode 100644 index 46cfffb69c..0000000000 --- a/src/api/endpoints/drive/files/upload_from_url.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Module dependencies - */ -import * as URL from 'url'; -const download = require('download'); -import $ from 'cafy'; -import { validateFileName } from '../../../models/drive-file'; -import serialize from '../../../serializers/drive-file'; -import create from '../../../common/add-file-to-drive'; - -/** - * Create a file from a URL - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'url' parameter - // TODO: Validate this url - const [url, urlErr] = $(params.url).string().$; - if (urlErr) return rej('invalid url param'); - - let name = URL.parse(url).pathname.split('/').pop(); - if (!validateFileName(name)) { - name = null; - } - - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - // Download file - const data = await download(url); - - // Create file - const driveFile = await create(user, data, name, null, folderId); - - // Serialize - const fileObj = await serialize(driveFile); - - // Response - res(fileObj); -}); diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts deleted file mode 100644 index d49ef0af03..0000000000 --- a/src/api/endpoints/drive/folders.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import DriveFolder from '../../models/drive-folder'; -import serialize from '../../serializers/drive-folder'; - -/** - * Get drive folders - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Get 'folder_id' parameter - const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$; - if (folderIdErr) return rej('invalid folder_id param'); - - // Construct query - const sort = { - _id: -1 - }; - const query = { - user_id: user._id, - parent_id: folderId - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const folders = await DriveFolder - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(folders.map(async folder => - await serialize(folder)))); -}); diff --git a/src/api/endpoints/following/create.ts b/src/api/endpoints/following/create.ts deleted file mode 100644 index b4a2217b16..0000000000 --- a/src/api/endpoints/following/create.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../models/user'; -import Following from '../../models/following'; -import notify from '../../common/notify'; -import event from '../../event'; -import serializeUser from '../../serializers/user'; - -/** - * Follow a user - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - const follower = user; - - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); - - // 自分自身 - if (user._id.equals(userId)) { - return rej('followee is yourself'); - } - - // Get followee - const followee = await User.findOne({ - _id: userId - }, { - fields: { - data: false, - profile: false - } - }); - - if (followee === null) { - return rej('user not found'); - } - - // Check if already following - const exist = await Following.findOne({ - follower_id: follower._id, - followee_id: followee._id, - deleted_at: { $exists: false } - }); - - if (exist !== null) { - return rej('already following'); - } - - // Create following - await Following.insert({ - created_at: new Date(), - follower_id: follower._id, - followee_id: followee._id - }); - - // Send response - res(); - - // Increment following count - User.update(follower._id, { - $inc: { - following_count: 1 - } - }); - - // Increment followers count - User.update({ _id: followee._id }, { - $inc: { - followers_count: 1 - } - }); - - // Publish follow event - event(follower._id, 'follow', await serializeUser(followee, follower)); - event(followee._id, 'followed', await serializeUser(follower, followee)); - - // Notify - notify(followee._id, follower._id, 'follow'); -}); diff --git a/src/api/endpoints/following/delete.ts b/src/api/endpoints/following/delete.ts deleted file mode 100644 index aa1639ef6c..0000000000 --- a/src/api/endpoints/following/delete.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../models/user'; -import Following from '../../models/following'; -import event from '../../event'; -import serializeUser from '../../serializers/user'; - -/** - * Unfollow a user - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - const follower = user; - - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Check if the followee is yourself - if (user._id.equals(userId)) { - return rej('followee is yourself'); - } - - // Get followee - const followee = await User.findOne({ - _id: userId - }, { - fields: { - data: false, - profile: false - } - }); - - if (followee === null) { - return rej('user not found'); - } - - // Check not following - const exist = await Following.findOne({ - follower_id: follower._id, - followee_id: followee._id, - deleted_at: { $exists: false } - }); - - if (exist === null) { - return rej('already not following'); - } - - // Delete following - await Following.update({ - _id: exist._id - }, { - $set: { - deleted_at: new Date() - } - }); - - // Send response - res(); - - // Decrement following count - User.update({ _id: follower._id }, { - $inc: { - following_count: -1 - } - }); - - // Decrement followers count - User.update({ _id: followee._id }, { - $inc: { - followers_count: -1 - } - }); - - // Publish follow event - event(follower._id, 'unfollow', await serializeUser(followee, follower)); -}); diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts deleted file mode 100644 index ae75f11d54..0000000000 --- a/src/api/endpoints/i.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Module dependencies - */ -import User from '../models/user'; -import serialize from '../serializers/user'; - -/** - * Show myself - * - * @param {any} params - * @param {any} user - * @param {any} app - * @param {Boolean} isSecure - * @return {Promise} - */ -module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => { - // Serialize - res(await serialize(user, user, { - detail: true, - includeSecrets: isSecure - })); - - // Update lastUsedAt - User.update({ _id: user._id }, { - $set: { - last_used_at: new Date() - } - }); -}); diff --git a/src/api/endpoints/i/appdata/get.ts b/src/api/endpoints/i/appdata/get.ts deleted file mode 100644 index a1a57fa13a..0000000000 --- a/src/api/endpoints/i/appdata/get.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Appdata from '../../../models/appdata'; - -/** - * Get app data - * - * @param {any} params - * @param {any} user - * @param {any} app - * @param {Boolean} isSecure - * @return {Promise} - */ -module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { - // Get 'key' parameter - const [key = null, keyError] = $(params.key).optional.nullable.string().match(/[a-z_]+/).$; - if (keyError) return rej('invalid key param'); - - if (isSecure) { - if (!user.data) { - return res(); - } - if (key !== null) { - const data = {}; - data[key] = user.data[key]; - res(data); - } else { - res(user.data); - } - } else { - const select = {}; - if (key !== null) { - select[`data.${key}`] = true; - } - const appdata = await Appdata.findOne({ - app_id: app._id, - user_id: user._id - }, { - fields: select - }); - - if (appdata) { - res(appdata.data); - } else { - res(); - } - } -}); diff --git a/src/api/endpoints/i/appdata/set.ts b/src/api/endpoints/i/appdata/set.ts deleted file mode 100644 index 24f192de6b..0000000000 --- a/src/api/endpoints/i/appdata/set.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Appdata from '../../../models/appdata'; -import User from '../../../models/user'; -import serialize from '../../../serializers/user'; -import event from '../../../event'; - -/** - * Set app data - * - * @param {any} params - * @param {any} user - * @param {any} app - * @param {Boolean} isSecure - * @return {Promise} - */ -module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) => { - // Get 'data' parameter - const [data, dataError] = $(params.data).optional.object() - .pipe(obj => { - const hasInvalidData = Object.entries(obj).some(([k, v]) => - $(k).string().match(/^[a-z_]+$/).isNg() && $(v).string().isNg()); - return !hasInvalidData; - }).$; - if (dataError) return rej('invalid data param'); - - // Get 'key' parameter - const [key, keyError] = $(params.key).optional.string().match(/[a-z_]+/).$; - if (keyError) return rej('invalid key param'); - - // Get 'value' parameter - const [value, valueError] = $(params.value).optional.string().$; - if (valueError) return rej('invalid value param'); - - const set = {}; - if (data) { - Object.entries(data).forEach(([k, v]) => { - set[`data.${k}`] = v; - }); - } else { - set[`data.${key}`] = value; - } - - if (isSecure) { - const _user = await User.findOneAndUpdate(user._id, { - $set: set - }); - - res(204); - - // Publish i updated event - event(user._id, 'i_updated', await serialize(_user, user, { - detail: true, - includeSecrets: true - })); - } else { - await Appdata.update({ - app_id: app._id, - user_id: user._id - }, Object.assign({ - app_id: app._id, - user_id: user._id - }, { - $set: set - }), { - upsert: true - }); - - res(204); - } -}); diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts deleted file mode 100644 index 111a4b1909..0000000000 --- a/src/api/endpoints/i/update.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../models/user'; -import { isValidName, isValidDescription, isValidLocation, isValidBirthday } from '../../models/user'; -import serialize from '../../serializers/user'; -import event from '../../event'; -import config from '../../../conf'; - -/** - * Update myself - * - * @param {any} params - * @param {any} user - * @param {any} _ - * @param {boolean} isSecure - * @return {Promise} - */ -module.exports = async (params, user, _, isSecure) => new Promise(async (res, rej) => { - // Get 'name' parameter - const [name, nameErr] = $(params.name).optional.string().pipe(isValidName).$; - if (nameErr) return rej('invalid name param'); - if (name) user.name = name; - - // Get 'description' parameter - const [description, descriptionErr] = $(params.description).optional.nullable.string().pipe(isValidDescription).$; - if (descriptionErr) return rej('invalid description param'); - if (description !== undefined) user.description = description; - - // Get 'location' parameter - const [location, locationErr] = $(params.location).optional.nullable.string().pipe(isValidLocation).$; - if (locationErr) return rej('invalid location param'); - if (location !== undefined) user.profile.location = location; - - // Get 'birthday' parameter - const [birthday, birthdayErr] = $(params.birthday).optional.nullable.string().pipe(isValidBirthday).$; - if (birthdayErr) return rej('invalid birthday param'); - if (birthday !== undefined) user.profile.birthday = birthday; - - // Get 'avatar_id' parameter - const [avatarId, avatarIdErr] = $(params.avatar_id).optional.id().$; - if (avatarIdErr) return rej('invalid avatar_id param'); - if (avatarId) user.avatar_id = avatarId; - - // Get 'banner_id' parameter - const [bannerId, bannerIdErr] = $(params.banner_id).optional.id().$; - if (bannerIdErr) return rej('invalid banner_id param'); - if (bannerId) user.banner_id = bannerId; - - await User.update(user._id, { - $set: { - name: user.name, - description: user.description, - avatar_id: user.avatar_id, - banner_id: user.banner_id, - profile: user.profile - } - }); - - // Serialize - const iObj = await serialize(user, user, { - detail: true, - includeSecrets: isSecure - }); - - // Send response - res(iObj); - - // Publish i updated event - event(user._id, 'i_updated', iObj); - - // Update search index - if (config.elasticsearch.enable) { - const es = require('../../../db/elasticsearch'); - - es.index({ - index: 'misskey', - type: 'user', - id: user._id.toString(), - body: { - name: user.name, - bio: user.bio - } - }); - } -}); diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts deleted file mode 100644 index 8af55d850c..0000000000 --- a/src/api/endpoints/messaging/messages/create.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Message from '../../../models/messaging-message'; -import { isValidText } from '../../../models/messaging-message'; -import History from '../../../models/messaging-history'; -import User from '../../../models/user'; -import DriveFile from '../../../models/drive-file'; -import serialize from '../../../serializers/messaging-message'; -import publishUserStream from '../../../event'; -import { publishMessagingStream } from '../../../event'; -import config from '../../../../conf'; - -/** - * Create a message - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [recipientId, recipientIdErr] = $(params.user_id).id().$; - if (recipientIdErr) return rej('invalid user_id param'); - - // Myself - if (recipientId.equals(user._id)) { - return rej('cannot send message to myself'); - } - - // Fetch recipient - const recipient = await User.findOne({ - _id: recipientId - }, { - fields: { - _id: true - } - }); - - if (recipient === null) { - return rej('user not found'); - } - - // Get 'text' parameter - const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; - if (textErr) return rej('invalid text'); - - // Get 'file_id' parameter - const [fileId, fileIdErr] = $(params.file_id).optional.id().$; - if (fileIdErr) return rej('invalid file_id param'); - - let file = null; - if (fileId !== undefined) { - file = await DriveFile.findOne({ - _id: fileId, - user_id: user._id - }, { - data: false - }); - - if (file === null) { - return rej('file not found'); - } - } - - // テキストが無いかつ添付ファイルも無かったらエラー - if (text === undefined && file === null) { - return rej('text or file is required'); - } - - // メッセージを作成 - const message = await Message.insert({ - created_at: new Date(), - file_id: file ? file._id : undefined, - recipient_id: recipient._id, - text: text ? text : undefined, - user_id: user._id, - is_read: false - }); - - // Serialize - const messageObj = await serialize(message); - - // Reponse - res(messageObj); - - // 自分のストリーム - publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj); - publishUserStream(message.user_id, 'messaging_message', messageObj); - - // 相手のストリーム - publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj); - publishUserStream(message.recipient_id, 'messaging_message', messageObj); - - // 3秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する - setTimeout(async () => { - const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true }); - if (!freshMessage.is_read) { - publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj); - } - }, 3000); - - // Register to search database - if (message.text && config.elasticsearch.enable) { - const es = require('../../../db/elasticsearch'); - - es.index({ - index: 'misskey', - type: 'messaging_message', - id: message._id.toString(), - body: { - text: message.text - } - }); - } - - // 履歴作成(自分) - History.update({ - user_id: user._id, - partner: recipient._id - }, { - updated_at: new Date(), - user_id: user._id, - partner: recipient._id, - message: message._id - }, { - upsert: true - }); - - // 履歴作成(相手) - History.update({ - user_id: recipient._id, - partner: user._id - }, { - updated_at: new Date(), - user_id: recipient._id, - partner: user._id, - message: message._id - }, { - upsert: true - }); -}); diff --git a/src/api/endpoints/messaging/unread.ts b/src/api/endpoints/messaging/unread.ts deleted file mode 100644 index 40bc83fe1c..0000000000 --- a/src/api/endpoints/messaging/unread.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Module dependencies - */ -import Message from '../../models/messaging-message'; - -/** - * Get count of unread messages - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - const count = await Message - .count({ - recipient_id: user._id, - is_read: false - }); - - res({ - count: count - }); -}); diff --git a/src/api/endpoints/notifications/mark_as_read.ts b/src/api/endpoints/notifications/mark_as_read.ts deleted file mode 100644 index 5cce33e850..0000000000 --- a/src/api/endpoints/notifications/mark_as_read.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Notification from '../../models/notification'; -import serialize from '../../serializers/notification'; -import event from '../../event'; - -/** - * Mark as read a notification - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - const [notificationId, notificationIdErr] = $(params.notification_id).id().$; - if (notificationIdErr) return rej('invalid notification_id param'); - - // Get notification - const notification = await Notification - .findOne({ - _id: notificationId, - i: user._id - }); - - if (notification === null) { - return rej('notification-not-found'); - } - - // Update - notification.is_read = true; - Notification.update({ _id: notification._id }, { - $set: { - is_read: true - } - }); - - // Response - res(); - - // Serialize - const notificationObj = await serialize(notification); - - // Publish read_notification event - event(user._id, 'read_notification', notificationObj); -}); diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts deleted file mode 100644 index 23b9bd0b66..0000000000 --- a/src/api/endpoints/posts.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../models/post'; -import serialize from '../serializers/post'; - -/** - * Lists all posts - * - * @param {any} params - * @return {Promise} - */ -module.exports = (params) => new Promise(async (res, rej) => { - // Get 'reply' parameter - const [reply, replyErr] = $(params.reply).optional.boolean().$; - if (replyErr) return rej('invalid reply param'); - - // Get 'repost' parameter - const [repost, repostErr] = $(params.repost).optional.boolean().$; - if (repostErr) return rej('invalid repost param'); - - // Get 'media' parameter - const [media, mediaErr] = $(params.media).optional.boolean().$; - if (mediaErr) return rej('invalid media param'); - - // Get 'poll' parameter - const [poll, pollErr] = $(params.poll).optional.boolean().$; - if (pollErr) return rej('invalid poll param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Construct query - const sort = { - _id: -1 - }; - const query = {} as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - if (reply != undefined) { - query.reply_to_id = reply ? { $exists: true, $ne: null } : null; - } - - if (repost != undefined) { - query.repost_id = repost ? { $exists: true, $ne: null } : null; - } - - if (media != undefined) { - query.media_ids = media ? { $exists: true, $ne: null } : null; - } - - if (poll != undefined) { - query.poll = poll ? { $exists: true, $ne: null } : null; - } - - // Issue query - const posts = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(posts.map(async post => await serialize(post)))); -}); diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts deleted file mode 100644 index eb979402c4..0000000000 --- a/src/api/endpoints/posts/create.ts +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import deepEqual = require('deep-equal'); -import parse from '../../common/text'; -import Post from '../../models/post'; -import { isValidText } from '../../models/post'; -import User from '../../models/user'; -import Following from '../../models/following'; -import DriveFile from '../../models/drive-file'; -import Watching from '../../models/post-watching'; -import serialize from '../../serializers/post'; -import notify from '../../common/notify'; -import watch from '../../common/watch-post'; -import event from '../../event'; -import config from '../../../conf'; - -/** - * Create a post - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - // Get 'text' parameter - const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; - if (textErr) return rej('invalid text'); - - // Get 'media_ids' parameter - const [mediaIds, mediaIdsErr] = $(params.media_ids).optional.array('id').unique().range(1, 4).$; - if (mediaIdsErr) return rej('invalid media_ids'); - - let files = []; - if (mediaIds !== undefined) { - // Fetch files - // forEach だと途中でエラーなどがあっても return できないので - // 敢えて for を使っています。 - for (const mediaId of mediaIds) { - // Fetch file - // SELECT _id - const entity = await DriveFile.findOne({ - _id: mediaId, - user_id: user._id - }, { - _id: true - }); - - if (entity === null) { - return rej('file not found'); - } else { - files.push(entity); - } - } - } else { - files = null; - } - - // Get 'repost_id' parameter - const [repostId, repostIdErr] = $(params.repost_id).optional.id().$; - if (repostIdErr) return rej('invalid repost_id'); - - let repost = null; - if (repostId !== undefined) { - // Fetch repost to post - repost = await Post.findOne({ - _id: repostId - }); - - if (repost == null) { - return rej('repostee is not found'); - } else if (repost.repost_id && !repost.text && !repost.media_ids) { - return rej('cannot repost to repost'); - } - - // Fetch recently post - const latestPost = await Post.findOne({ - user_id: user._id - }, { - sort: { - _id: -1 - } - }); - - // 直近と同じRepost対象かつ引用じゃなかったらエラー - if (latestPost && - latestPost.repost_id && - latestPost.repost_id.equals(repost._id) && - text === undefined && files === null) { - return rej('cannot repost same post that already reposted in your latest post'); - } - - // 直近がRepost対象かつ引用じゃなかったらエラー - if (latestPost && - latestPost._id.equals(repost._id) && - text === undefined && files === null) { - return rej('cannot repost your latest post'); - } - } - - // Get 'in_reply_to_post_id' parameter - const [inReplyToPostId, inReplyToPostIdErr] = $(params.reply_to_id).optional.id().$; - if (inReplyToPostIdErr) return rej('invalid in_reply_to_post_id'); - - let inReplyToPost = null; - if (inReplyToPostId !== undefined) { - // Fetch reply - inReplyToPost = await Post.findOne({ - _id: inReplyToPostId - }); - - if (inReplyToPost === null) { - return rej('in reply to post is not found'); - } - - // 返信対象が引用でないRepostだったらエラー - if (inReplyToPost.repost_id && !inReplyToPost.text && !inReplyToPost.media_ids) { - return rej('cannot reply to repost'); - } - } - - // Get 'poll' parameter - const [poll, pollErr] = $(params.poll).optional.strict.object() - .have('choices', $().array('string') - .unique() - .range(2, 10) - .each(c => c.length > 0 && c.length < 50)) - .$; - if (pollErr) return rej('invalid poll'); - - if (poll) { - (poll as any).choices = (poll as any).choices.map((choice, i) => ({ - id: i, // IDを付与 - text: choice.trim(), - votes: 0 - })); - } - - // テキストが無いかつ添付ファイルが無いかつRepostも無いかつ投票も無かったらエラー - if (text === undefined && files === null && repost === null && poll === undefined) { - return rej('text, media_ids, repost_id or poll is required'); - } - - // 直近の投稿と重複してたらエラー - // TODO: 直近の投稿が一日前くらいなら重複とは見なさない - if (user.latest_post) { - if (deepEqual({ - text: user.latest_post.text, - reply: user.latest_post.reply_to_id ? user.latest_post.reply_to_id.toString() : null, - repost: user.latest_post.repost_id ? user.latest_post.repost_id.toString() : null, - media_ids: (user.latest_post.media_ids || []).map(id => id.toString()) - }, { - text: text, - reply: inReplyToPost ? inReplyToPost._id.toString() : null, - repost: repost ? repost._id.toString() : null, - media_ids: (files || []).map(file => file._id.toString()) - })) { - return rej('duplicate'); - } - } - - // 投稿を作成 - const post = await Post.insert({ - created_at: new Date(), - media_ids: files ? files.map(file => file._id) : undefined, - reply_to_id: inReplyToPost ? inReplyToPost._id : undefined, - repost_id: repost ? repost._id : undefined, - poll: poll, - text: text, - user_id: user._id, - app_id: app ? app._id : null - }); - - // Serialize - const postObj = await serialize(post); - - // Reponse - res(postObj); - - // ----------------------------------------------------------- - // Post processes - - User.update({ _id: user._id }, { - $set: { - latest_post: post - } - }); - - const mentions = []; - - function addMention(mentionee, type) { - // Reject if already added - if (mentions.some(x => x.equals(mentionee))) return; - - // Add mention - mentions.push(mentionee); - - // Publish event - if (!user._id.equals(mentionee)) { - event(mentionee, type, postObj); - } - } - - // Publish event to myself's stream - event(user._id, 'post', postObj); - - // Fetch all followers - const followers = await Following - .find({ - followee_id: user._id, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - follower_id: true, - _id: false - }); - - // Publish event to followers stream - followers.forEach(following => - event(following.follower_id, 'post', postObj)); - - // Increment my posts count - User.update({ _id: user._id }, { - $inc: { - posts_count: 1 - } - }); - - // If has in reply to post - if (inReplyToPost) { - // Increment replies count - Post.update({ _id: inReplyToPost._id }, { - $inc: { - replies_count: 1 - } - }); - - // 自分自身へのリプライでない限りは通知を作成 - notify(inReplyToPost.user_id, user._id, 'reply', { - post_id: post._id - }); - - // Fetch watchers - Watching - .find({ - post_id: inReplyToPost._id, - user_id: { $ne: user._id }, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - fields: { - user_id: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.user_id, user._id, 'reply', { - post_id: post._id - }); - }); - }); - - // この投稿をWatchする - // TODO: ユーザーが「返信したときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, inReplyToPost); - - // Add mention - addMention(inReplyToPost.user_id, 'reply'); - } - - // If it is repost - if (repost) { - // Notify - const type = text ? 'quote' : 'repost'; - notify(repost.user_id, user._id, type, { - post_id: post._id - }); - - // Fetch watchers - Watching - .find({ - post_id: repost._id, - user_id: { $ne: user._id }, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - fields: { - user_id: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.user_id, user._id, type, { - post_id: post._id - }); - }); - }); - - // この投稿をWatchする - // TODO: ユーザーが「Repostしたときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, repost); - - // If it is quote repost - if (text) { - // Add mention - addMention(repost.user_id, 'quote'); - } else { - // Publish event - if (!user._id.equals(repost.user_id)) { - event(repost.user_id, 'repost', postObj); - } - } - - // 今までで同じ投稿をRepostしているか - const existRepost = await Post.findOne({ - user_id: user._id, - repost_id: repost._id, - _id: { - $ne: post._id - } - }); - - if (!existRepost) { - // Update repostee status - Post.update({ _id: repost._id }, { - $inc: { - repost_count: 1 - } - }); - } - } - - // If has text content - if (text) { - // Analyze - const tokens = parse(text); - /* - // Extract a hashtags - const hashtags = tokens - .filter(t => t.type == 'hashtag') - .map(t => t.hashtag) - // Drop dupulicates - .filter((v, i, s) => s.indexOf(v) == i); - - // ハッシュタグをデータベースに登録 - registerHashtags(user, hashtags); - */ - // Extract an '@' mentions - const atMentions = tokens - .filter(t => t.type == 'mention') - .map(m => m.username) - // Drop dupulicates - .filter((v, i, s) => s.indexOf(v) == i); - - // Resolve all mentions - await Promise.all(atMentions.map(async (mention) => { - // Fetch mentioned user - // SELECT _id - const mentionee = await User - .findOne({ - username_lower: mention.toLowerCase() - }, { _id: true }); - - // When mentioned user not found - if (mentionee == null) return; - - // 既に言及されたユーザーに対する返信や引用repostの場合も無視 - if (inReplyToPost && inReplyToPost.user_id.equals(mentionee._id)) return; - if (repost && repost.user_id.equals(mentionee._id)) return; - - // Add mention - addMention(mentionee._id, 'mention'); - - // Create notification - notify(mentionee._id, user._id, 'mention', { - post_id: post._id - }); - - return; - })); - } - - // Register to search database - if (text && config.elasticsearch.enable) { - const es = require('../../../db/elasticsearch'); - - es.index({ - index: 'misskey', - type: 'post', - id: post._id.toString(), - body: { - text: post.text - } - }); - } - - // Append mentions data - if (mentions.length > 0) { - Post.update({ _id: post._id }, { - $set: { - mentions: mentions - } - }); - } -}); diff --git a/src/api/endpoints/posts/favorites/create.ts b/src/api/endpoints/posts/favorites/create.ts deleted file mode 100644 index f9dee271b5..0000000000 --- a/src/api/endpoints/posts/favorites/create.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Favorite from '../../../models/favorite'; -import Post from '../../../models/post'; - -/** - * Favorite a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get favoritee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // if already favorited - const exist = await Favorite.findOne({ - post_id: post._id, - user_id: user._id - }); - - if (exist !== null) { - return rej('already favorited'); - } - - // Create favorite - await Favorite.insert({ - created_at: new Date(), - post_id: post._id, - user_id: user._id - }); - - // Send response - res(); -}); diff --git a/src/api/endpoints/posts/polls/vote.ts b/src/api/endpoints/posts/polls/vote.ts deleted file mode 100644 index 5a4fd1c268..0000000000 --- a/src/api/endpoints/posts/polls/vote.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Vote from '../../../models/poll-vote'; -import Post from '../../../models/post'; -import Watching from '../../../models/post-watching'; -import notify from '../../../common/notify'; -import watch from '../../../common/watch-post'; -import { publishPostStream } from '../../../event'; - -/** - * Vote poll of a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get votee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - if (post.poll == null) { - return rej('poll not found'); - } - - // Get 'choice' parameter - const [choice, choiceError] = - $(params.choice).number() - .pipe(c => post.poll.choices.some(x => x.id == c)) - .$; - if (choiceError) return rej('invalid choice param'); - - // if already voted - const exist = await Vote.findOne({ - post_id: post._id, - user_id: user._id - }); - - if (exist !== null) { - return rej('already voted'); - } - - // Create vote - await Vote.insert({ - created_at: new Date(), - post_id: post._id, - user_id: user._id, - choice: choice - }); - - // Send response - res(); - - const inc = {}; - inc[`poll.choices.${findWithAttr(post.poll.choices, 'id', choice)}.votes`] = 1; - - // Increment votes count - await Post.update({ _id: post._id }, { - $inc: inc - }); - - publishPostStream(post._id, 'poll_voted'); - - // Notify - notify(post.user_id, user._id, 'poll_vote', { - post_id: post._id, - choice: choice - }); - - // Fetch watchers - Watching - .find({ - post_id: post._id, - user_id: { $ne: user._id }, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - fields: { - user_id: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.user_id, user._id, 'poll_vote', { - post_id: post._id, - choice: choice - }); - }); - }); - - // この投稿をWatchする - // TODO: ユーザーが「投票したときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, post); -}); - -function findWithAttr(array, attr, value) { - for (let i = 0; i < array.length; i += 1) { - if (array[i][attr] === value) { - return i; - } - } - return -1; -} diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts deleted file mode 100644 index eecb928123..0000000000 --- a/src/api/endpoints/posts/reactions/create.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Reaction from '../../../models/post-reaction'; -import Post from '../../../models/post'; -import Watching from '../../../models/post-watching'; -import notify from '../../../common/notify'; -import watch from '../../../common/watch-post'; -import { publishPostStream } from '../../../event'; - -/** - * React to a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get 'reaction' parameter - const [reaction, reactionErr] = $(params.reaction).string().or([ - 'like', - 'love', - 'laugh', - 'hmm', - 'surprise', - 'congrats', - 'angry', - 'confused', - 'pudding' - ]).$; - if (reactionErr) return rej('invalid reaction param'); - - // Fetch reactee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Myself - if (post.user_id.equals(user._id)) { - return rej('cannot react to my post'); - } - - // if already reacted - const exist = await Reaction.findOne({ - post_id: post._id, - user_id: user._id, - deleted_at: { $exists: false } - }); - - if (exist !== null) { - return rej('already reacted'); - } - - // Create reaction - await Reaction.insert({ - created_at: new Date(), - post_id: post._id, - user_id: user._id, - reaction: reaction - }); - - // Send response - res(); - - const inc = {}; - inc[`reaction_counts.${reaction}`] = 1; - - // Increment reactions count - await Post.update({ _id: post._id }, { - $inc: inc - }); - - publishPostStream(post._id, 'reacted'); - - // Notify - notify(post.user_id, user._id, 'reaction', { - post_id: post._id, - reaction: reaction - }); - - // Fetch watchers - Watching - .find({ - post_id: post._id, - user_id: { $ne: user._id }, - // 削除されたドキュメントは除く - deleted_at: { $exists: false } - }, { - fields: { - user_id: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.user_id, user._id, 'reaction', { - post_id: post._id, - reaction: reaction - }); - }); - }); - - // この投稿をWatchする - // TODO: ユーザーが「リアクションしたときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, post); -}); diff --git a/src/api/endpoints/posts/reactions/delete.ts b/src/api/endpoints/posts/reactions/delete.ts deleted file mode 100644 index 922c57ab18..0000000000 --- a/src/api/endpoints/posts/reactions/delete.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Reaction from '../../../models/post-reaction'; -import Post from '../../../models/post'; -// import event from '../../../event'; - -/** - * Unreact to a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Fetch unreactee - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // if already unreacted - const exist = await Reaction.findOne({ - post_id: post._id, - user_id: user._id, - deleted_at: { $exists: false } - }); - - if (exist === null) { - return rej('never reacted'); - } - - // Delete reaction - await Reaction.update({ - _id: exist._id - }, { - $set: { - deleted_at: new Date() - } - }); - - // Send response - res(); - - const dec = {}; - dec[`reaction_counts.${exist.reaction}`] = -1; - - // Decrement reactions count - Post.update({ _id: post._id }, { - $inc: dec - }); -}); diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts deleted file mode 100644 index b701ff7574..0000000000 --- a/src/api/endpoints/posts/reposts.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; - -/** - * Show a reposts of a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Lookup post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Construct query - const sort = { - _id: -1 - }; - const query = { - repost_id: post._id - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const reposts = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(reposts.map(async post => - await serialize(post, user)))); -}); diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts deleted file mode 100644 index b434f64342..0000000000 --- a/src/api/endpoints/posts/search.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import $ from 'cafy'; -const escapeRegexp = require('escape-regexp'); -import Post from '../../models/post'; -import serialize from '../../serializers/post'; -import config from '../../../conf'; - -/** - * Search a post - * - * @param {any} params - * @param {any} me - * @return {Promise} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'query' parameter - const [query, queryError] = $(params.query).string().pipe(x => x != '').$; - if (queryError) return rej('invalid query param'); - - // Get 'offset' parameter - const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$; - if (offsetErr) return rej('invalid offset param'); - - // Get 'max' parameter - const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$; - if (maxErr) return rej('invalid max param'); - - // If Elasticsearch is available, search by $ - // If not, search by MongoDB - (config.elasticsearch.enable ? byElasticsearch : byNative) - (res, rej, me, query, offset, max); -}); - -// Search by MongoDB -async function byNative(res, rej, me, query, offset, max) { - const escapedQuery = escapeRegexp(query); - - // Search posts - const posts = await Post - .find({ - text: new RegExp(escapedQuery) - }, { - sort: { - _id: -1 - }, - limit: max, - skip: offset - }); - - // Serialize - res(await Promise.all(posts.map(async post => - await serialize(post, me)))); -} - -// Search by Elasticsearch -async function byElasticsearch(res, rej, me, query, offset, max) { - const es = require('../../db/elasticsearch'); - - es.search({ - index: 'misskey', - type: 'post', - body: { - size: max, - from: offset, - query: { - simple_query_string: { - fields: ['text'], - query: query, - default_operator: 'and' - } - }, - sort: [ - { _doc: 'desc' } - ], - highlight: { - pre_tags: [''], - post_tags: [''], - encoder: 'html', - fields: { - text: {} - } - } - } - }, async (error, response) => { - if (error) { - console.error(error); - return res(500); - } - - if (response.hits.total === 0) { - return res([]); - } - - const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id)); - - // Fetch found posts - const posts = await Post - .find({ - _id: { - $in: hits - } - }, { - sort: { - _id: -1 - } - }); - - posts.map(post => { - post._highlight = response.hits.hits.filter(hit => post._id.equals(hit._id))[0].highlight.text[0]; - }); - - // Serialize - res(await Promise.all(posts.map(async post => - await serialize(post, me)))); - }); -} diff --git a/src/api/endpoints/posts/show.ts b/src/api/endpoints/posts/show.ts deleted file mode 100644 index 5bfe4f6605..0000000000 --- a/src/api/endpoints/posts/show.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../models/post'; -import serialize from '../../serializers/post'; - -/** - * Show a post - * - * @param {any} params - * @param {any} user - * @return {Promise} - */ -module.exports = (params, user) => new Promise(async (res, rej) => { - // Get 'post_id' parameter - const [postId, postIdErr] = $(params.post_id).id().$; - if (postIdErr) return rej('invalid post_id param'); - - // Get post - const post = await Post.findOne({ - _id: postId - }); - - if (post === null) { - return rej('post not found'); - } - - // Serialize - res(await serialize(post, user, { - detail: true - })); -}); diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts deleted file mode 100644 index 314e992344..0000000000 --- a/src/api/endpoints/posts/timeline.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../models/post'; -import getFriends from '../../common/get-friends'; -import serialize from '../../serializers/post'; - -/** - * Get timeline of myself - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise} - */ -module.exports = (params, user, app) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // ID list of the user $self and other users who the user follows - const followingIds = await getFriends(user._id); - - // Construct query - const sort = { - _id: -1 - }; - const query = { - user_id: { - $in: followingIds - } - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const timeline = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(timeline.map(async post => - await serialize(post, user) - ))); -}); diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts deleted file mode 100644 index 134f262fb1..0000000000 --- a/src/api/endpoints/users.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../models/user'; -import serialize from '../serializers/user'; - -/** - * Lists all users - * - * @param {any} params - * @param {any} me - * @return {Promise} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - // Construct query - const sort = { - _id: -1 - }; - const query = {} as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - // Issue query - const users = await User - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(users.map(async user => - await serialize(user, me)))); -}); diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts deleted file mode 100644 index e37b660773..0000000000 --- a/src/api/endpoints/users/posts.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import Post from '../../models/post'; -import User from '../../models/user'; -import serialize from '../../serializers/post'; - -/** - * Get posts of a user - * - * @param {any} params - * @param {any} me - * @return {Promise} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).optional.id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Get 'username' parameter - const [username, usernameErr] = $(params.username).optional.string().$; - if (usernameErr) return rej('invalid username param'); - - if (userId === undefined && username === undefined) { - return rej('user_id or username is required'); - } - - // Get 'include_replies' parameter - const [includeReplies = true, includeRepliesErr] = $(params.include_replies).optional.boolean().$; - if (includeRepliesErr) return rej('invalid include_replies param'); - - // Get 'with_media' parameter - const [withMedia = false, withMediaErr] = $(params.with_media).optional.boolean().$; - if (withMediaErr) return rej('invalid with_media param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; - if (limitErr) return rej('invalid limit param'); - - // Get 'since_id' parameter - const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$; - if (sinceIdErr) return rej('invalid since_id param'); - - // Get 'max_id' parameter - const [maxId, maxIdErr] = $(params.max_id).optional.id().$; - if (maxIdErr) return rej('invalid max_id param'); - - // Check if both of since_id and max_id is specified - if (sinceId && maxId) { - return rej('cannot set since_id and max_id'); - } - - const q = userId !== undefined - ? { _id: userId } - : { username_lower: username.toLowerCase() } ; - - // Lookup user - const user = await User.findOne(q, { - fields: { - _id: true - } - }); - - if (user === null) { - return rej('user not found'); - } - - // Construct query - const sort = { - _id: -1 - }; - const query = { - user_id: user._id - } as any; - if (sinceId) { - sort._id = 1; - query._id = { - $gt: sinceId - }; - } else if (maxId) { - query._id = { - $lt: maxId - }; - } - - if (!includeReplies) { - query.reply_to_id = null; - } - - if (withMedia) { - query.media_ids = { - $exists: true, - $ne: null - }; - } - - // Issue query - const posts = await Post - .find(query, { - limit: limit, - sort: sort - }); - - // Serialize - res(await Promise.all(posts.map(async (post) => - await serialize(post, me) - ))); -}); diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts deleted file mode 100644 index 8e74b0fe3f..0000000000 --- a/src/api/endpoints/users/show.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Module dependencies - */ -import $ from 'cafy'; -import User from '../../models/user'; -import serialize from '../../serializers/user'; - -/** - * Show a user - * - * @param {any} params - * @param {any} me - * @return {Promise} - */ -module.exports = (params, me) => new Promise(async (res, rej) => { - // Get 'user_id' parameter - const [userId, userIdErr] = $(params.user_id).optional.id().$; - if (userIdErr) return rej('invalid user_id param'); - - // Get 'username' parameter - const [username, usernameErr] = $(params.username).optional.string().$; - if (usernameErr) return rej('invalid username param'); - - if (userId === undefined && username === undefined) { - return rej('user_id or username is required'); - } - - const q = userId !== undefined - ? { _id: userId } - : { username_lower: username.toLowerCase() }; - - // Lookup user - const user = await User.findOne(q, { - fields: { - data: false - } - }); - - if (user === null) { - return rej('user not found'); - } - - // Send response - res(await serialize(user, me, { - detail: true - })); -}); diff --git a/src/api/event.ts b/src/api/event.ts deleted file mode 100644 index 9613a9f7cc..0000000000 --- a/src/api/event.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as mongo from 'mongodb'; -import * as redis from 'redis'; -import config from '../conf'; - -type ID = string | mongo.ObjectID; - -class MisskeyEvent { - private redisClient: redis.RedisClient; - - constructor() { - // Connect to Redis - this.redisClient = redis.createClient( - config.redis.port, config.redis.host); - } - - public publishUserStream(userId: ID, type: string, value?: any): void { - this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishPostStream(postId: ID, type: string, value?: any): void { - this.publish(`post-stream:${postId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void { - this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); - } - - private publish(channel: string, type: string, value?: any): void { - const message = value == null ? - { type: type } : - { type: type, body: value }; - - this.redisClient.publish(`misskey:${channel}`, JSON.stringify(message)); - } -} - -const ev = new MisskeyEvent(); - -export default ev.publishUserStream.bind(ev); - -export const publishPostStream = ev.publishPostStream.bind(ev); - -export const publishMessagingStream = ev.publishMessagingStream.bind(ev); diff --git a/src/api/models/access-token.ts b/src/api/models/access-token.ts deleted file mode 100644 index 2a8a512ddc..0000000000 --- a/src/api/models/access-token.ts +++ /dev/null @@ -1,8 +0,0 @@ -import db from '../../db/mongodb'; - -const collection = db.get('access_tokens'); - -(collection as any).index('token'); // fuck type definition -(collection as any).index('hash'); // fuck type definition - -export default collection as any; // fuck type definition diff --git a/src/api/models/app.ts b/src/api/models/app.ts deleted file mode 100644 index bf5dc80c2c..0000000000 --- a/src/api/models/app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import db from '../../db/mongodb'; - -const collection = db.get('apps'); - -(collection as any).index('name_id'); // fuck type definition -(collection as any).index('name_id_lower'); // fuck type definition -(collection as any).index('secret'); // fuck type definition - -export default collection as any; // fuck type definition - -export function isValidNameId(nameId: string): boolean { - return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId); -} diff --git a/src/api/models/appdata.ts b/src/api/models/appdata.ts deleted file mode 100644 index 3e68354fa4..0000000000 --- a/src/api/models/appdata.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('appdata') as any; // fuck type definition diff --git a/src/api/models/auth-session.ts b/src/api/models/auth-session.ts deleted file mode 100644 index b264a133e9..0000000000 --- a/src/api/models/auth-session.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('auth_sessions') as any; // fuck type definition diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts deleted file mode 100644 index 4c7204b1f4..0000000000 --- a/src/api/models/drive-file.ts +++ /dev/null @@ -1,17 +0,0 @@ -import db from '../../db/mongodb'; - -const collection = db.get('drive_files'); - -(collection as any).index('hash'); // fuck type definition - -export default collection as any; // fuck type definition - -export function validateFileName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) && - (name.indexOf('\\') === -1) && - (name.indexOf('/') === -1) && - (name.indexOf('..') === -1) - ); -} diff --git a/src/api/models/drive-folder.ts b/src/api/models/drive-folder.ts deleted file mode 100644 index f81ffe855d..0000000000 --- a/src/api/models/drive-folder.ts +++ /dev/null @@ -1,10 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('drive_folders') as any; // fuck type definition - -export function isValidFolderName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) - ); -} diff --git a/src/api/models/drive-tag.ts b/src/api/models/drive-tag.ts deleted file mode 100644 index 991c935e81..0000000000 --- a/src/api/models/drive-tag.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('drive_tags') as any; // fuck type definition diff --git a/src/api/models/favorite.ts b/src/api/models/favorite.ts deleted file mode 100644 index e01d9e343c..0000000000 --- a/src/api/models/favorite.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('favorites') as any; // fuck type definition diff --git a/src/api/models/following.ts b/src/api/models/following.ts deleted file mode 100644 index cb3db9b539..0000000000 --- a/src/api/models/following.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('following') as any; // fuck type definition diff --git a/src/api/models/messaging-history.ts b/src/api/models/messaging-history.ts deleted file mode 100644 index c06987e451..0000000000 --- a/src/api/models/messaging-history.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('messaging_histories') as any; // fuck type definition diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts deleted file mode 100644 index 18afa57e44..0000000000 --- a/src/api/models/messaging-message.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../../db/mongodb'; - -export default db.get('messaging_messages') as any; // fuck type definition - -export interface IMessagingMessage { - _id: mongo.ObjectID; -} - -export function isValidText(text: string): boolean { - return text.length <= 1000 && text.trim() != ''; -} diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts deleted file mode 100644 index 1c1f429a0d..0000000000 --- a/src/api/models/notification.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('notifications') as any; // fuck type definition diff --git a/src/api/models/poll-vote.ts b/src/api/models/poll-vote.ts deleted file mode 100644 index af77a2643e..0000000000 --- a/src/api/models/poll-vote.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('poll_votes') as any; // fuck type definition diff --git a/src/api/models/post-reaction.ts b/src/api/models/post-reaction.ts deleted file mode 100644 index 282ae5bd21..0000000000 --- a/src/api/models/post-reaction.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('post_reactions') as any; // fuck type definition diff --git a/src/api/models/post-watching.ts b/src/api/models/post-watching.ts deleted file mode 100644 index 41d37e2703..0000000000 --- a/src/api/models/post-watching.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('post_watching') as any; // fuck type definition diff --git a/src/api/models/post.ts b/src/api/models/post.ts deleted file mode 100644 index baab63f991..0000000000 --- a/src/api/models/post.ts +++ /dev/null @@ -1,7 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('posts') as any; // fuck type definition - -export function isValidText(text: string): boolean { - return text.length <= 1000 && text.trim() != ''; -} diff --git a/src/api/models/signin.ts b/src/api/models/signin.ts deleted file mode 100644 index 385a348f2e..0000000000 --- a/src/api/models/signin.ts +++ /dev/null @@ -1,3 +0,0 @@ -import db from '../../db/mongodb'; - -export default db.get('signin') as any; // fuck type definition diff --git a/src/api/models/user.ts b/src/api/models/user.ts deleted file mode 100644 index cd16459891..0000000000 --- a/src/api/models/user.ts +++ /dev/null @@ -1,36 +0,0 @@ -import db from '../../db/mongodb'; - -const collection = db.get('users'); - -(collection as any).index('username'); // fuck type definition -(collection as any).index('token'); // fuck type definition - -export default collection as any; // fuck type definition - -export function validateUsername(username: string): boolean { - return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username); -} - -export function validatePassword(password: string): boolean { - return typeof password == 'string' && password != ''; -} - -export function isValidName(name: string): boolean { - return typeof name == 'string' && name.length < 30 && name.trim() != ''; -} - -export function isValidDescription(description: string): boolean { - return typeof description == 'string' && description.length < 500 && description.trim() != ''; -} - -export function isValidLocation(location: string): boolean { - return typeof location == 'string' && location.length < 50 && location.trim() != ''; -} - -export function isValidBirthday(birthday: string): boolean { - return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday); -} - -export interface IUser { - name: string; -} diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts deleted file mode 100644 index afa83e50c3..0000000000 --- a/src/api/private/signin.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as express from 'express'; -import * as bcrypt from 'bcryptjs'; -import User from '../models/user'; -import Signin from '../models/signin'; -import serialize from '../serializers/signin'; -import event from '../event'; -import config from '../../conf'; - -export default async (req: express.Request, res: express.Response) => { - res.header('Access-Control-Allow-Credentials', 'true'); - - const username = req.body['username']; - const password = req.body['password']; - - if (typeof username != 'string') { - res.sendStatus(400); - return; - } - - if (typeof password != 'string') { - res.sendStatus(400); - return; - } - - // Fetch user - const user = await User.findOne({ - username_lower: username.toLowerCase() - }, { - fields: { - data: false, - profile: false - } - }); - - if (user === null) { - res.status(404).send({ - error: 'user not found' - }); - return; - } - - // Compare password - const same = bcrypt.compareSync(password, user.password); - - if (same) { - const expires = 1000 * 60 * 60 * 24 * 365; // One Year - res.cookie('i', user.token, { - path: '/', - domain: `.${config.host}`, - secure: config.url.substr(0, 5) === 'https', - httpOnly: false, - expires: new Date(Date.now() + expires), - maxAge: expires - }); - - res.sendStatus(204); - } else { - res.status(400).send({ - error: 'incorrect password' - }); - } - - // Append signin history - const record = await Signin.insert({ - created_at: new Date(), - user_id: user._id, - ip: req.ip, - headers: req.headers, - success: same - }); - - // Publish signin event - event(user._id, 'signin', await serialize(record)); -}; diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts deleted file mode 100644 index 2375c22845..0000000000 --- a/src/api/private/signup.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as express from 'express'; -import * as bcrypt from 'bcryptjs'; -import rndstr from 'rndstr'; -import recaptcha = require('recaptcha-promise'); -import User from '../models/user'; -import { validateUsername, validatePassword } from '../models/user'; -import serialize from '../serializers/user'; -import config from '../../conf'; - -recaptcha.init({ - secret_key: config.recaptcha.secretKey -}); - -export default async (req: express.Request, res: express.Response) => { - // Verify recaptcha - // ただしテスト時はこの機構は障害となるため無効にする - if (process.env.NODE_ENV !== 'test') { - const success = await recaptcha(req.body['g-recaptcha-response']); - - if (!success) { - res.status(400).send('recaptcha-failed'); - return; - } - } - - const username = req.body['username']; - const password = req.body['password']; - const name = '名無し'; - - // Validate username - if (!validateUsername(username)) { - res.sendStatus(400); - return; - } - - // Validate password - if (!validatePassword(password)) { - res.sendStatus(400); - return; - } - - // Fetch exist user that same username - const usernameExist = await User - .count({ - username_lower: username.toLowerCase() - }, { - limit: 1 - }); - - // Check username already used - if (usernameExist !== 0) { - res.sendStatus(400); - return; - } - - // Generate hash of password - const salt = bcrypt.genSaltSync(8); - const hash = bcrypt.hashSync(password, salt); - - // Generate secret - const secret = `!${rndstr('a-zA-Z0-9', 32)}`; - - // Create account - const account = await User.insert({ - token: secret, - avatar_id: null, - banner_id: null, - created_at: new Date(), - description: null, - email: null, - followers_count: 0, - following_count: 0, - links: null, - name: name, - password: hash, - posts_count: 0, - likes_count: 0, - liked_count: 0, - drive_capacity: 1073741824, // 1GB - username: username, - username_lower: username.toLowerCase(), - profile: { - bio: null, - birthday: null, - blood: null, - gender: null, - handedness: null, - height: null, - location: null, - weight: null - } - }); - - // Response - res.send(await serialize(account)); - - // Create search index - if (config.elasticsearch.enable) { - const es = require('../../db/elasticsearch'); - es.index({ - index: 'misskey', - type: 'user', - id: account._id.toString(), - body: { - username: username - } - }); - } -}; diff --git a/src/api/reply.ts b/src/api/reply.ts deleted file mode 100644 index e47fc85b9b..0000000000 --- a/src/api/reply.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as express from 'express'; - -export default (res: express.Response, x?: any, y?: any) => { - if (x === undefined) { - res.sendStatus(204); - } else if (typeof x === 'number') { - res.status(x).send({ - error: x === 500 ? 'INTERNAL_ERROR' : y - }); - } else { - res.send(x); - } -}; diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts deleted file mode 100644 index b4e2ab064a..0000000000 --- a/src/api/serializers/drive-file.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import DriveFile from '../models/drive-file'; -import serializeDriveFolder from './drive-folder'; -import serializeDriveTag from './drive-tag'; -import deepcopy = require('deepcopy'); -import config from '../../conf'; - -/** - * Serialize a drive file - * - * @param {any} file - * @param {any} options? - * @return {Promise} - */ -export default ( - file: any, - options?: { - detail: boolean - } -) => new Promise(async (resolve, reject) => { - const opts = Object.assign({ - detail: false - }, options); - - let _file: any; - - // Populate the file if 'file' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(file)) { - _file = await DriveFile.findOne({ - _id: file - }, { - fields: { - data: false - } - }); - } else if (typeof file === 'string') { - _file = await DriveFile.findOne({ - _id: new mongo.ObjectID(file) - }, { - fields: { - data: false - } - }); - } else { - _file = deepcopy(file); - } - - // Rename _id to id - _file.id = _file._id; - delete _file._id; - - delete _file.data; - - _file.url = `${config.drive_url}/${_file.id}/${encodeURIComponent(_file.name)}`; - - if (opts.detail && _file.folder_id) { - // Populate folder - _file.folder = await serializeDriveFolder(_file.folder_id, { - detail: true - }); - } - - if (opts.detail && _file.tags) { - // Populate tags - _file.tags = await _file.tags.map(async (tag: any) => - await serializeDriveTag(tag) - ); - } - - resolve(_file); -}); diff --git a/src/api/serializers/drive-tag.ts b/src/api/serializers/drive-tag.ts deleted file mode 100644 index 2f152381bd..0000000000 --- a/src/api/serializers/drive-tag.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import DriveTag from '../models/drive-tag'; -import deepcopy = require('deepcopy'); - -/** - * Serialize a drive tag - * - * @param {any} tag - * @return {Promise} - */ -const self = ( - tag: any -) => new Promise(async (resolve, reject) => { - let _tag: any; - - // Populate the tag if 'tag' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(tag)) { - _tag = await DriveTag.findOne({ _id: tag }); - } else if (typeof tag === 'string') { - _tag = await DriveTag.findOne({ _id: new mongo.ObjectID(tag) }); - } else { - _tag = deepcopy(tag); - } - - // Rename _id to id - _tag.id = _tag._id; - delete _tag._id; - - resolve(_tag); -}); - -export default self; diff --git a/src/api/serializers/messaging-message.ts b/src/api/serializers/messaging-message.ts deleted file mode 100644 index 4ab95e42a3..0000000000 --- a/src/api/serializers/messaging-message.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import Message from '../models/messaging-message'; -import serializeUser from './user'; -import serializeDriveFile from './drive-file'; -import parse from '../common/text'; - -/** - * Serialize a message - * - * @param {any} message - * @param {any} me? - * @param {any} options? - * @return {Promise} - */ -export default ( - message: any, - me?: any, - options?: { - populateRecipient: boolean - } -) => new Promise(async (resolve, reject) => { - const opts = options || { - populateRecipient: true - }; - - let _message: any; - - // Populate the message if 'message' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(message)) { - _message = await Message.findOne({ - _id: message - }); - } else if (typeof message === 'string') { - _message = await Message.findOne({ - _id: new mongo.ObjectID(message) - }); - } else { - _message = deepcopy(message); - } - - // Rename _id to id - _message.id = _message._id; - delete _message._id; - - // Parse text - if (_message.text) { - _message.ast = parse(_message.text); - } - - // Populate user - _message.user = await serializeUser(_message.user_id, me); - - if (_message.file) { - // Populate file - _message.file = await serializeDriveFile(_message.file_id); - } - - if (opts.populateRecipient) { - // Populate recipient - _message.recipient = await serializeUser(_message.recipient_id, me); - } - - resolve(_message); -}); diff --git a/src/api/serializers/notification.ts b/src/api/serializers/notification.ts deleted file mode 100644 index ac919dc8b0..0000000000 --- a/src/api/serializers/notification.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import Notification from '../models/notification'; -import serializeUser from './user'; -import serializePost from './post'; -import deepcopy = require('deepcopy'); - -/** - * Serialize a notification - * - * @param {any} notification - * @return {Promise} - */ -export default (notification: any) => new Promise(async (resolve, reject) => { - let _notification: any; - - // Populate the notification if 'notification' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(notification)) { - _notification = await Notification.findOne({ - _id: notification - }); - } else if (typeof notification === 'string') { - _notification = await Notification.findOne({ - _id: new mongo.ObjectID(notification) - }); - } else { - _notification = deepcopy(notification); - } - - // Rename _id to id - _notification.id = _notification._id; - delete _notification._id; - - // Rename notifier_id to user_id - _notification.user_id = _notification.notifier_id; - delete _notification.notifier_id; - - const me = _notification.notifiee_id; - delete _notification.notifiee_id; - - // Populate notifier - _notification.user = await serializeUser(_notification.user_id, me); - - switch (_notification.type) { - case 'follow': - // nope - break; - case 'mention': - case 'reply': - case 'repost': - case 'quote': - case 'reaction': - case 'poll_vote': - // Populate post - _notification.post = await serializePost(_notification.post_id, me); - break; - default: - console.error(`Unknown type: ${_notification.type}`); - break; - } - - resolve(_notification); -}); diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts deleted file mode 100644 index 3c96884dd1..0000000000 --- a/src/api/serializers/post.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import Post from '../models/post'; -import Reaction from '../models/post-reaction'; -import Vote from '../models/poll-vote'; -import serializeApp from './app'; -import serializeUser from './user'; -import serializeDriveFile from './drive-file'; -import parse from '../common/text'; - -/** - * Serialize a post - * - * @param {any} post - * @param {any} me? - * @param {any} options? - * @return {Promise} - */ -const self = ( - post: any, - me?: any, - options?: { - detail: boolean - } -) => new Promise(async (resolve, reject) => { - const opts = options || { - detail: true, - }; - - let _post: any; - - // Populate the post if 'post' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(post)) { - _post = await Post.findOne({ - _id: post - }); - } else if (typeof post === 'string') { - _post = await Post.findOne({ - _id: new mongo.ObjectID(post) - }); - } else { - _post = deepcopy(post); - } - - const id = _post._id; - - // Rename _id to id - _post.id = _post._id; - delete _post._id; - - delete _post.mentions; - - // Parse text - if (_post.text) { - _post.ast = parse(_post.text); - } - - // Populate user - _post.user = await serializeUser(_post.user_id, me); - - // Populate app - if (_post.app_id) { - _post.app = await serializeApp(_post.app_id); - } - - if (_post.media_ids) { - // Populate media - _post.media = await Promise.all(_post.media_ids.map(async fileId => - await serializeDriveFile(fileId) - )); - } - - if (_post.reply_to_id && opts.detail) { - // Populate reply to post - _post.reply_to = await self(_post.reply_to_id, me, { - detail: false - }); - } - - if (_post.repost_id && opts.detail) { - // Populate repost - _post.repost = await self(_post.repost_id, me, { - detail: _post.text == null - }); - } - - // Poll - if (me && _post.poll && opts.detail) { - const vote = await Vote - .findOne({ - user_id: me._id, - post_id: id - }); - - if (vote != null) { - _post.poll.choices.filter(c => c.id == vote.choice)[0].is_voted = true; - } - } - - // Fetch my reaction - if (me && opts.detail) { - const reaction = await Reaction - .findOne({ - user_id: me._id, - post_id: id, - deleted_at: { $exists: false } - }); - - if (reaction) { - _post.my_reaction = reaction.reaction; - } - } - - resolve(_post); -}); - -export default self; diff --git a/src/api/serializers/signin.ts b/src/api/serializers/signin.ts deleted file mode 100644 index 4068067678..0000000000 --- a/src/api/serializers/signin.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Module dependencies - */ -import deepcopy = require('deepcopy'); - -/** - * Serialize a signin record - * - * @param {any} record - * @return {Promise} - */ -export default ( - record: any -) => new Promise(async (resolve, reject) => { - - const _record = deepcopy(record); - - // Rename _id to id - _record.id = _record._id; - delete _record._id; - - resolve(_record); -}); diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts deleted file mode 100644 index bdbc749589..0000000000 --- a/src/api/serializers/user.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Module dependencies - */ -import * as mongo from 'mongodb'; -import deepcopy = require('deepcopy'); -import User from '../models/user'; -import Following from '../models/following'; -import getFriends from '../common/get-friends'; -import config from '../../conf'; - -/** - * Serialize a user - * - * @param {any} user - * @param {any} me? - * @param {any} options? - * @return {Promise} - */ -export default ( - user: any, - me?: any, - options?: { - detail?: boolean, - includeSecrets?: boolean - } -) => new Promise(async (resolve, reject) => { - - const opts = Object.assign({ - detail: false, - includeSecrets: false - }, options); - - let _user: any; - - const fields = opts.detail ? { - data: false - } : { - data: false, - profile: false - }; - - // Populate the user if 'user' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(user)) { - _user = await User.findOne({ - _id: user - }, { fields }); - } else if (typeof user === 'string') { - _user = await User.findOne({ - _id: new mongo.ObjectID(user) - }, { fields }); - } else { - _user = deepcopy(user); - } - - // Me - if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) { - if (typeof me === 'string') { - me = new mongo.ObjectID(me); - } else { - me = me._id; - } - } - - // Rename _id to id - _user.id = _user._id; - delete _user._id; - - // Remove needless properties - delete _user.lates_post; - - // Remove private properties - delete _user.password; - delete _user.token; - delete _user.username_lower; - if (_user.twitter) { - delete _user.twitter.access_token; - delete _user.twitter.access_token_secret; - } - - // Visible via only the official client - if (!opts.includeSecrets) { - delete _user.data; - delete _user.email; - } - - _user.avatar_url = _user.avatar_id != null - ? `${config.drive_url}/${_user.avatar_id}` - : `${config.drive_url}/default-avatar.jpg`; - - _user.banner_url = _user.banner_id != null - ? `${config.drive_url}/${_user.banner_id}` - : null; - - if (!me || !me.equals(_user.id) || !opts.detail) { - delete _user.avatar_id; - delete _user.banner_id; - - delete _user.drive_capacity; - } - - if (me && !me.equals(_user.id)) { - // If the user is following - const follow = await Following.findOne({ - follower_id: me, - followee_id: _user.id, - deleted_at: { $exists: false } - }); - _user.is_following = follow !== null; - - // If the user is followed - const follow2 = await Following.findOne({ - follower_id: _user.id, - followee_id: me, - deleted_at: { $exists: false } - }); - _user.is_followed = follow2 !== null; - } - - if (me && !me.equals(_user.id) && opts.detail) { - const myFollowingIds = await getFriends(me); - - // Get following you know count - const followingYouKnowCount = await Following.count({ - followee_id: { $in: myFollowingIds }, - follower_id: _user.id, - deleted_at: { $exists: false } - }); - _user.following_you_know_count = followingYouKnowCount; - - // Get followers you know count - const followersYouKnowCount = await Following.count({ - followee_id: _user.id, - follower_id: { $in: myFollowingIds }, - deleted_at: { $exists: false } - }); - _user.followers_you_know_count = followersYouKnowCount; - } - - resolve(_user); -}); -/* -function img(url) { - return { - thumbnail: { - large: `${url}`, - medium: '', - small: '' - } - }; -} -*/ diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts deleted file mode 100644 index 9fb274aacb..0000000000 --- a/src/api/service/twitter.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as express from 'express'; -// import * as Twitter from 'twitter'; -// const Twitter = require('twitter'); -import autwh from 'autwh'; -import redis from '../../db/redis'; -import User from '../models/user'; -import serialize from '../serializers/user'; -import event from '../event'; -import config from '../../conf'; - -module.exports = (app: express.Application) => { - app.get('/disconnect/twitter', async (req, res): Promise => { - if (res.locals.user == null) return res.send('plz signin'); - const user = await User.findOneAndUpdate({ - token: res.locals.user - }, { - $set: { - twitter: null - } - }); - - res.send(`Twitterの連携を解除しました :v:`); - - // Publish i updated event - event(user._id, 'i_updated', await serialize(user, user, { - detail: true, - includeSecrets: true - })); - }); - - if (config.twitter == null) { - app.get('/connect/twitter', (req, res) => { - res.send('現在Twitterへ接続できません'); - }); - return; - } - - const twAuth = autwh({ - consumerKey: config.twitter.consumer_key, - consumerSecret: config.twitter.consumer_secret, - callbackUrl: `${config.api_url}/tw/cb` - }); - - app.get('/connect/twitter', async (req, res): Promise => { - if (res.locals.user == null) return res.send('plz signin'); - const ctx = await twAuth.begin(); - redis.set(res.locals.user, JSON.stringify(ctx)); - res.redirect(ctx.url); - }); - - app.get('/tw/cb', (req, res): any => { - if (res.locals.user == null) return res.send('plz signin'); - redis.get(res.locals.user, async (_, ctx) => { - const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); - - const user = await User.findOneAndUpdate({ - token: res.locals.user - }, { - $set: { - twitter: { - access_token: result.accessToken, - access_token_secret: result.accessTokenSecret, - user_id: result.userId, - screen_name: result.screenName - } - } - }); - - res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); - - // Publish i updated event - event(user._id, 'i_updated', await serialize(user, user, { - detail: true, - includeSecrets: true - })); - }); - }); -}; diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts deleted file mode 100644 index 2ab8d3025b..0000000000 --- a/src/api/stream/home.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as websocket from 'websocket'; -import * as redis from 'redis'; -import * as debug from 'debug'; - -import serializePost from '../serializers/post'; - -const log = debug('misskey'); - -export default function homeStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { - // Subscribe Home stream channel - subscriber.subscribe(`misskey:user-stream:${user._id}`); - - subscriber.on('message', async (channel, data) => { - switch (channel.split(':')[1]) { - case 'user-stream': - connection.send(data); - break; - case 'post-stream': - const postId = channel.split(':')[2]; - log(`RECEIVED: ${postId} ${data} by @${user.username}`); - const post = await serializePost(postId, user, { - detail: true - }); - connection.send(JSON.stringify({ - type: 'post-updated', - body: { - post: post - } - })); - break; - } - }); - - connection.on('message', data => { - const msg = JSON.parse(data.utf8Data); - - switch (msg.type) { - case 'capture': - if (!msg.id) return; - const postId = msg.id; - log(`CAPTURE: ${postId} by @${user.username}`); - subscriber.subscribe(`misskey:post-stream:${postId}`); - break; - } - }); -} diff --git a/src/api/streaming.ts b/src/api/streaming.ts deleted file mode 100644 index c71132100c..0000000000 --- a/src/api/streaming.ts +++ /dev/null @@ -1,95 +0,0 @@ -import * as http from 'http'; -import * as websocket from 'websocket'; -import * as redis from 'redis'; -import config from '../conf'; -import User from './models/user'; -import AccessToken from './models/access-token'; -import isNativeToken from './common/is-native-token'; - -import homeStream from './stream/home'; -import messagingStream from './stream/messaging'; -import serverStream from './stream/server'; - -module.exports = (server: http.Server) => { - /** - * Init websocket server - */ - const ws = new websocket.server({ - httpServer: server - }); - - ws.on('request', async (request) => { - const connection = request.accept(); - - if (request.resourceURL.pathname === '/server') { - serverStream(request, connection); - return; - } - - const user = await authenticate(connection, request.resourceURL.query.i); - - if (user == null) { - connection.send('authentication-failed'); - connection.close(); - return; - } - - // Connect to Redis - const subscriber = redis.createClient( - config.redis.port, config.redis.host); - - connection.on('close', () => { - subscriber.unsubscribe(); - subscriber.quit(); - }); - - const channel = - request.resourceURL.pathname === '/' ? homeStream : - request.resourceURL.pathname === '/messaging' ? messagingStream : - null; - - if (channel !== null) { - channel(request, connection, subscriber, user); - } else { - connection.close(); - } - }); -}; - -function authenticate(connection: websocket.connection, token: string): Promise { - if (token == null) { - return Promise.resolve(null); - } - - return new Promise(async (resolve, reject) => { - if (isNativeToken(token)) { - // Fetch user - // SELECT _id - const user = await User - .findOne({ - token: token - }); - - resolve(user); - } else { - const accessToken = await AccessToken.findOne({ - hash: token - }); - - if (accessToken == null) { - return reject('invalid signature'); - } - - // Fetch user - // SELECT _id - const user = await User - .findOne({ _id: accessToken.user_id }, { - fields: { - _id: true - } - }); - - resolve(user); - } - }); -} diff --git a/src/build/fa.ts b/src/build/fa.ts new file mode 100644 index 0000000000..0c21be9504 --- /dev/null +++ b/src/build/fa.ts @@ -0,0 +1,57 @@ +/** + * Replace fontawesome symbols + */ + +import * as fontawesome from '@fortawesome/fontawesome'; +import * as regular from '@fortawesome/fontawesome-free-regular'; +import * as solid from '@fortawesome/fontawesome-free-solid'; +import * as brands from '@fortawesome/fontawesome-free-brands'; + +// Add icons +fontawesome.library.add(regular); +fontawesome.library.add(solid); +fontawesome.library.add(brands); + +export const pattern = /%fa:(.+?)%/g; + +export const replacement = (_, key) => { + const args = key.split(' '); + let prefix = 'fas'; + const classes = []; + let transform = ''; + let name; + + args.forEach(arg => { + if (arg == 'R' || arg == 'S' || arg == 'B') { + prefix = + arg == 'R' ? 'far' : + arg == 'S' ? 'fas' : + arg == 'B' ? 'fab' : + ''; + } else if (arg[0] == '.') { + classes.push('fa-' + arg.substr(1)); + } else if (arg[0] == '-') { + transform = arg.substr(1).split('|').join(' '); + } else { + name = arg; + } + }); + + const icon = fontawesome.icon({ prefix, iconName: name }, { + classes: classes + }); + + if (icon) { + icon.transform = fontawesome.parse.transform(transform); + return `${icon.html[0]}`; + } else { + console.warn(`'${name}' not found in fa`); + return ''; + } +}; + +export default (src: string) => { + return src.replace(pattern, replacement); +}; + +export const fa = fontawesome; diff --git a/src/build/i18n.ts b/src/build/i18n.ts new file mode 100644 index 0000000000..b9b7403214 --- /dev/null +++ b/src/build/i18n.ts @@ -0,0 +1,57 @@ +/** + * Replace i18n texts + */ + +import locale from '../../locales'; + +export default class Replacer { + private lang: string; + + public pattern = /"%i18n:(.+?)%"|'%i18n:(.+?)%'|%i18n:(.+?)%/g; + + constructor(lang: string) { + this.lang = lang; + + this.get = this.get.bind(this); + this.replacement = this.replacement.bind(this); + } + + private get(key: string) { + const texts = locale[this.lang]; + + if (texts == null) { + console.warn(`lang '${this.lang}' is not supported`); + return key; // Fallback + } + + let text = texts; + + // Check the key existance + const error = key.split('.').some(k => { + if (text.hasOwnProperty(k)) { + text = text[k]; + return false; + } else { + return true; + } + }); + + if (error) { + console.warn(`key '${key}' not found in '${this.lang}'`); + return key; // Fallback + } else { + return text; + } + } + + public replacement(match, a, b, c) { + const key = a || b || c; + if (match[0] == '"') { + return '"' + this.get(key).replace(/"/g, '\\"') + '"'; + } else if (match[0] == "'") { + return '\'' + this.get(key).replace(/'/g, '\\\'') + '\''; + } else { + return this.get(key); + } + } +} diff --git a/src/build/license.ts b/src/build/license.ts new file mode 100644 index 0000000000..d36af665cd --- /dev/null +++ b/src/build/license.ts @@ -0,0 +1,13 @@ +import * as fs from 'fs'; + +const license = fs.readFileSync(__dirname + '/../../LICENSE', 'utf-8'); + +const licenseHtml = license + .replace(/\r\n/g, '\n') + .replace(/(.)\n(.)/g, '$1 $2') + .replace(/(^|\n)(.*?)($|\n)/g, '

$2

'); + +export { + license, + licenseHtml +}; diff --git a/src/client/app/animation.styl b/src/client/app/animation.styl new file mode 100644 index 0000000000..8f121b313b --- /dev/null +++ b/src/client/app/animation.styl @@ -0,0 +1,12 @@ +.zoom-in-top-enter-active, +.zoom-in-top-leave-active { + opacity: 1; + transform: scaleY(1); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); + transform-origin: center top; +} +.zoom-in-top-enter, +.zoom-in-top-leave-active { + opacity: 0; + transform: scaleY(0); +} diff --git a/src/web/app/base.styl b/src/client/app/app.styl similarity index 70% rename from src/web/app/base.styl rename to src/client/app/app.styl index 81c039f0a3..431b9daa65 100644 --- a/src/web/app/base.styl +++ b/src/client/app/app.styl @@ -1,34 +1,14 @@ -json('../../const.json') - -@charset 'utf-8' - -$theme-color = themeColor -$theme-color-foreground = themeColorForeground - -@import './reset' - -/* - ::selection - background $theme-color - color #fff -*/ - -* - tap-highlight-color rgba($theme-color, 0.7) - -webkit-tap-highlight-color rgba($theme-color, 0.7) - -html, body - margin 0 - padding 0 - scroll-behavior smooth - text-size-adjust 100% - font-family sans-serif +@import "../style" +@import "../animation" html &.progress &, * cursor progress !important +body + overflow-wrap break-word + #error padding 32px color #fff @@ -92,17 +72,6 @@ html 100% transform rotate(360deg) -a - text-decoration none - color $theme-color - cursor pointer - - &:hover - text-decoration underline - - * - cursor pointer - code font-family Consolas, 'Courier New', Courier, Monaco, monospace @@ -155,12 +124,5 @@ pre overflow auto tab-size 2 -mk-locker - display block - position fixed - top 0 - left 0 - z-index 65536 - width 100% - height 100% - cursor wait +[data-fa] + display inline-block diff --git a/src/client/app/app.vue b/src/client/app/app.vue new file mode 100644 index 0000000000..7a46e7dea0 --- /dev/null +++ b/src/client/app/app.vue @@ -0,0 +1,3 @@ + diff --git a/src/web/app/auth/assets/logo.svg b/src/client/app/auth/assets/logo.svg similarity index 100% rename from src/web/app/auth/assets/logo.svg rename to src/client/app/auth/assets/logo.svg diff --git a/src/client/app/auth/script.ts b/src/client/app/auth/script.ts new file mode 100644 index 0000000000..31c758ebc2 --- /dev/null +++ b/src/client/app/auth/script.ts @@ -0,0 +1,25 @@ +/** + * Authorize Form + */ + +// Style +import './style.styl'; + +import init from '../init'; + +import Index from './views/index.vue'; + +/** + * init + */ +init(async (launch) => { + document.title = 'Misskey | アプリの連携'; + + // Launch the app + const [app] = launch(); + + // Routing + app.$router.addRoutes([ + { path: '/:token', component: Index }, + ]); +}); diff --git a/src/web/app/auth/style.styl b/src/client/app/auth/style.styl similarity index 79% rename from src/web/app/auth/style.styl rename to src/client/app/auth/style.styl index 046a5ff6ee..bd25e1b572 100644 --- a/src/web/app/auth/style.styl +++ b/src/client/app/auth/style.styl @@ -1,4 +1,5 @@ -@import "../base" +@import "../app" +@import "../reset" html background #eee diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue new file mode 100644 index 0000000000..b323907eb0 --- /dev/null +++ b/src/client/app/auth/views/form.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue new file mode 100644 index 0000000000..e1e1b265e1 --- /dev/null +++ b/src/client/app/auth/views/index.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/src/web/app/base.pug b/src/client/app/base.pug similarity index 56% rename from src/web/app/base.pug rename to src/client/app/base.pug index b1ca80deb9..32a95a6c99 100644 --- a/src/web/app/base.pug +++ b/src/client/app/base.pug @@ -9,24 +9,29 @@ html meta(name='application-name' content='Misskey') meta(name='theme-color' content=themeColor) meta(name='referrer' content='origin') + link(rel='manifest' href='/manifest.json') title Misskey style - include ./../../../built/web/assets/init.css + include ./../../../built/client/assets/init.css script - include ./../../../built/web/assets/boot.js + include ./../../../built/client/assets/boot.js script - include ./../../../built/web/assets/safe.js + include ./../../../built/client/assets/safe.js - script(src='https://use.fontawesome.com/db921426cb.js' async) + //- FontAwesome style + style #{facss} + + //- highlight.js style + style #{hljscss} body noscript: p | JavaScriptを有効にしてください br - | Please turn on JavaScript + | Please turn on your JavaScript div#ini: p span . span . diff --git a/src/web/app/boot.js b/src/client/app/boot.js similarity index 51% rename from src/web/app/boot.js rename to src/client/app/boot.js index ac6c18d649..0846e4bd55 100644 --- a/src/web/app/boot.js +++ b/src/client/app/boot.js @@ -21,18 +21,20 @@ // Get the current url information const url = new URL(location.href); - // Extarct the (sub) domain part of the current url - // - // e.g. - // misskey.alice => misskey - // misskey.strawberry.pasta => misskey - // dev.misskey.arisu.tachibana => dev - let app = url.host.split('.')[0]; + //#region Detect app name + let app = null; + + if (url.pathname == '/docs') app = 'docs'; + if (url.pathname == '/dev') app = 'dev'; + if (url.pathname == '/auth') app = 'auth'; + //#endregion // Detect the user language // Note: The default language is English let lang = navigator.language.split('-')[0]; if (!/^(en|ja)$/.test(lang)) lang = 'en'; + if (localStorage.getItem('lang')) lang = localStorage.getItem('lang'); + if (ENV != 'production') lang = 'ja'; // Detect the user agent const ua = navigator.userAgent.toLowerCase(); @@ -55,16 +57,64 @@ } // Switch desktop or mobile version - if (app == 'misskey') { + if (app == null) { app = isMobile ? 'mobile' : 'desktop'; } + // Script version + const ver = localStorage.getItem('v') || VERSION; + + // Whether in debug mode + const isDebug = localStorage.getItem('debug') == 'true'; + + // Whether use raw version script + const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug) + || ENV != 'production'; + // Load an app script // Note: 'async' make it possible to load the script asyncly. // 'defer' make it possible to run the script when the dom loaded. const script = document.createElement('script'); - script.setAttribute('src', `/assets/${app}.${VERSION}.${lang}.js`); + script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js`); script.setAttribute('async', 'true'); script.setAttribute('defer', 'true'); head.appendChild(script); + + // 1秒経ってもスクリプトがロードされない場合はバージョンが古くて + // 404になっているせいかもしれないので、バージョンを確認して古ければ更新する + // + // 読み込まれたスクリプトからこのタイマーを解除できるように、 + // グローバルにタイマーIDを代入しておく + window.mkBootTimer = window.setTimeout(async () => { + // Fetch meta + const res = await fetch(API + '/meta', { + method: 'POST', + cache: 'no-cache' + }); + + // Parse + const meta = await res.json(); + + // Compare versions + if (meta.version != ver) { + alert( + 'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' + + '\n\n' + + 'New version of Misskey available. The page will be reloaded.'); + + // Clear cache (serive worker) + try { + navigator.serviceWorker.controller.postMessage('clear'); + + navigator.serviceWorker.getRegistrations().then(registrations => { + registrations.forEach(registration => registration.unregister()); + }); + } catch (e) { + console.error(e); + } + + // Force reload + location.reload(true); + } + }, 1000); } diff --git a/src/web/app/dev/script.js b/src/client/app/ch/script.ts similarity index 54% rename from src/web/app/dev/script.js rename to src/client/app/ch/script.ts index 39d7fc891e..4c6b6dfd1b 100644 --- a/src/web/app/dev/script.js +++ b/src/client/app/ch/script.ts @@ -1,5 +1,5 @@ /** - * Developer Center + * Channels */ // Style @@ -7,12 +7,9 @@ import './style.styl'; require('./tags'); import init from '../init'; -import route from './router'; /** * init */ -init(me => { - // Start routing - route(me); +init(() => { }); diff --git a/src/client/app/ch/style.styl b/src/client/app/ch/style.styl new file mode 100644 index 0000000000..21ca648cbe --- /dev/null +++ b/src/client/app/ch/style.styl @@ -0,0 +1,10 @@ +@import "../app" + +html + padding 8px + background #efefef + +#wait + top auto + bottom 15px + left 15px diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag new file mode 100644 index 0000000000..c0561c9b92 --- /dev/null +++ b/src/client/app/ch/tags/channel.tag @@ -0,0 +1,409 @@ + + +
+
+

{ channel.title }

+ +
+

このチャンネルをウォッチしています ウォッチ解除

+

このチャンネルをウォッチする

+
+ + + +
+

読み込み中

+
+

まだ投稿がありません

+ +
+
+
+ +
+

参加するにはログインまたは新規登録してください

+
+
+
+ Misskey ver { _VERSION_ } (葵 aoi) +
+
+ + +
+ + +
+ { note.index }: + { getUserName(note.user) } + + + ID:{ acct } +
+
+ >>{ note.reply.index } + { note.text } +
+ +
+
+ + +
+ + +

>>{ reply.index } ({ getUserName(reply.user) }): [x]

+ +
+ + + +
+ +
    +
  1. { name }
  2. +
+ + + +
+ + + + + + + + + + diff --git a/src/client/app/ch/tags/header.tag b/src/client/app/ch/tags/header.tag new file mode 100644 index 0000000000..901123d63b --- /dev/null +++ b/src/client/app/ch/tags/header.tag @@ -0,0 +1,20 @@ + +
+ Index | Misskey +
+ + + +
diff --git a/src/client/app/ch/tags/index.tag b/src/client/app/ch/tags/index.tag new file mode 100644 index 0000000000..88df2ec45d --- /dev/null +++ b/src/client/app/ch/tags/index.tag @@ -0,0 +1,37 @@ + + +
+ +
+ + + +
diff --git a/src/client/app/ch/tags/index.ts b/src/client/app/ch/tags/index.ts new file mode 100644 index 0000000000..12ffdaeb84 --- /dev/null +++ b/src/client/app/ch/tags/index.ts @@ -0,0 +1,3 @@ +require('./index.tag'); +require('./channel.tag'); +require('./header.tag'); diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts new file mode 100644 index 0000000000..7b98c0903f --- /dev/null +++ b/src/client/app/common/define-widget.ts @@ -0,0 +1,79 @@ +import Vue from 'vue'; + +export default function(data: { + name: string; + props?: () => T; +}) { + return Vue.extend({ + props: { + widget: { + type: Object + }, + isMobile: { + type: Boolean, + default: false + }, + isCustomizeMode: { + type: Boolean, + default: false + } + }, + computed: { + id(): string { + return this.widget.id; + } + }, + data() { + return { + props: data.props ? data.props() : {} as T, + bakedOldProps: null, + preventSave: false + }; + }, + created() { + if (this.props) { + Object.keys(this.props).forEach(prop => { + if (this.widget.data.hasOwnProperty(prop)) { + this.props[prop] = this.widget.data[prop]; + } + }); + } + + this.bakeProps(); + + this.$watch('props', newProps => { + if (this.preventSave) { + this.preventSave = false; + this.bakeProps(); + return; + } + if (this.bakedOldProps == JSON.stringify(newProps)) return; + + this.bakeProps(); + + if (this.isMobile) { + (this as any).api('i/update_mobile_home', { + id: this.id, + data: newProps + }).then(() => { + (this as any).os.i.clientSettings.mobileHome.find(w => w.id == this.id).data = newProps; + }); + } else { + (this as any).api('i/update_home', { + id: this.id, + data: newProps + }).then(() => { + (this as any).os.i.clientSettings.home.find(w => w.id == this.id).data = newProps; + }); + } + }, { + deep: true + }); + }, + methods: { + bakeProps() { + this.bakedOldProps = JSON.stringify(this.props); + } + } + }); +} diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts new file mode 100644 index 0000000000..5e0c7d2f3b --- /dev/null +++ b/src/client/app/common/mios.ts @@ -0,0 +1,588 @@ +import Vue from 'vue'; +import { EventEmitter } from 'eventemitter3'; +import * as merge from 'object-assign-deep'; +import * as uuid from 'uuid'; + +import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config'; +import Progress from './scripts/loading'; +import Connection from './scripts/streaming/stream'; +import { HomeStreamManager } from './scripts/streaming/home'; +import { DriveStreamManager } from './scripts/streaming/drive'; +import { ServerStreamManager } from './scripts/streaming/server'; +import { RequestsStreamManager } from './scripts/streaming/requests'; +import { MessagingIndexStreamManager } from './scripts/streaming/messaging-index'; +import { OthelloStreamManager } from './scripts/streaming/othello'; + +import Err from '../common/views/components/connect-failed.vue'; + +//#region api requests +let spinner = null; +let pending = 0; +//#endregion + +export type API = { + chooseDriveFile: (opts: { + title?: string; + currentFolder?: any; + multiple?: boolean; + }) => Promise; + + chooseDriveFolder: (opts: { + title?: string; + currentFolder?: any; + }) => Promise; + + dialog: (opts: { + title: string; + text: string; + actions?: Array<{ + text: string; + id?: string; + }>; + }) => Promise; + + input: (opts: { + title: string; + placeholder?: string; + default?: string; + }) => Promise; + + post: (opts?: { + reply?: any; + renote?: any; + }) => void; + + notify: (message: string) => void; +}; + +/** + * Misskey Operating System + */ +export default class MiOS extends EventEmitter { + /** + * Misskeyの /meta で取得できるメタ情報 + */ + private meta: { + data: { [x: string]: any }; + chachedAt: Date; + }; + + private isMetaFetching = false; + + public app: Vue; + + public new(vm, props) { + const w = new vm({ + parent: this.app, + propsData: props + }).$mount(); + document.body.appendChild(w.$el); + } + + /** + * A signing user + */ + public i: { [x: string]: any }; + + /** + * Whether signed in + */ + public get isSignedIn() { + return this.i != null; + } + + /** + * Whether is debug mode + */ + public get debug() { + return localStorage.getItem('debug') == 'true'; + } + + /** + * Whether enable sounds + */ + public get isEnableSounds() { + return localStorage.getItem('enableSounds') == 'true'; + } + + public apis: API; + + /** + * A connection manager of home stream + */ + public stream: HomeStreamManager; + + /** + * Connection managers + */ + public streams: { + driveStream: DriveStreamManager; + serverStream: ServerStreamManager; + requestsStream: RequestsStreamManager; + messagingIndexStream: MessagingIndexStreamManager; + othelloStream: OthelloStreamManager; + } = { + driveStream: null, + serverStream: null, + requestsStream: null, + messagingIndexStream: null, + othelloStream: null + }; + + /** + * A registration of service worker + */ + private swRegistration: ServiceWorkerRegistration = null; + + /** + * Whether should register ServiceWorker + */ + private shouldRegisterSw: boolean; + + /** + * ウィンドウシステム + */ + public windows = new WindowSystem(); + + /** + * MiOSインスタンスを作成します + * @param shouldRegisterSw ServiceWorkerを登録するかどうか + */ + constructor(shouldRegisterSw = false) { + super(); + + this.shouldRegisterSw = shouldRegisterSw; + + //#region BIND + this.log = this.log.bind(this); + this.logInfo = this.logInfo.bind(this); + this.logWarn = this.logWarn.bind(this); + this.logError = this.logError.bind(this); + this.init = this.init.bind(this); + this.api = this.api.bind(this); + this.getMeta = this.getMeta.bind(this); + this.registerSw = this.registerSw.bind(this); + //#endregion + + if (this.debug) { + (window as any).os = this; + } + } + + private googleMapsIniting = false; + + public getGoogleMaps() { + return new Promise((res, rej) => { + if ((window as any).google && (window as any).google.maps) { + res((window as any).google.maps); + } else { + this.once('init-google-maps', () => { + res((window as any).google.maps); + }); + + //#region load google maps api + if (!this.googleMapsIniting) { + this.googleMapsIniting = true; + (window as any).initGoogleMaps = () => { + this.emit('init-google-maps'); + }; + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`); + script.setAttribute('async', 'true'); + script.setAttribute('defer', 'true'); + head.appendChild(script); + } + //#endregion + } + }); + } + + public log(...args) { + if (!this.debug) return; + console.log.apply(null, args); + } + + public logInfo(...args) { + if (!this.debug) return; + console.info.apply(null, args); + } + + public logWarn(...args) { + if (!this.debug) return; + console.warn.apply(null, args); + } + + public logError(...args) { + if (!this.debug) return; + console.error.apply(null, args); + } + + public signout() { + localStorage.removeItem('me'); + document.cookie = `i=; domain=${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; + location.href = '/'; + } + + /** + * Initialize MiOS (boot) + * @param callback A function that call when initialized + */ + public async init(callback) { + //#region Init stream managers + this.streams.serverStream = new ServerStreamManager(this); + this.streams.requestsStream = new RequestsStreamManager(this); + + this.once('signedin', () => { + // Init home stream manager + this.stream = new HomeStreamManager(this, this.i); + + // Init other stream manager + this.streams.driveStream = new DriveStreamManager(this, this.i); + this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.i); + this.streams.othelloStream = new OthelloStreamManager(this, this.i); + }); + //#endregion + + // ユーザーをフェッチしてコールバックする + const fetchme = (token, cb) => { + let me = null; + + // Return when not signed in + if (token == null) { + return done(); + } + + // Fetch user + fetch(`${apiUrl}/i`, { + method: 'POST', + body: JSON.stringify({ + i: token + }) + }) + // When success + .then(res => { + // When failed to authenticate user + if (res.status !== 200) { + return this.signout(); + } + + // Parse response + res.json().then(i => { + me = i; + me.token = token; + done(); + }); + }) + // When failure + .catch(() => { + // Render the error screen + document.body.innerHTML = '
'; + new Vue({ + render: createEl => createEl(Err) + }).$mount('#err'); + + Progress.done(); + }); + + function done() { + if (cb) cb(me); + } + }; + + // フェッチが完了したとき + const fetched = me => { + if (me) { + // デフォルトの設定をマージ + me.clientSettings = Object.assign({ + fetchOnScroll: true, + showMaps: true, + showPostFormOnTopOfTl: false, + gradientWindowHeader: false + }, me.clientSettings); + + // ローカルストレージにキャッシュ + localStorage.setItem('me', JSON.stringify(me)); + } + + this.i = me; + + this.emit('signedin'); + + // Finish init + callback(); + + //#region Note + + // Init service worker + if (this.shouldRegisterSw) this.registerSw(); + + //#endregion + }; + + // Get cached account data + const cachedMe = JSON.parse(localStorage.getItem('me')); + + // キャッシュがあったとき + if (cachedMe) { + if (cachedMe.token == null) { + this.signout(); + return; + } + + // とりあえずキャッシュされたデータでお茶を濁して(?)おいて、 + fetched(cachedMe); + + // 後から新鮮なデータをフェッチ + fetchme(cachedMe.token, freshData => { + merge(cachedMe, freshData); + }); + } else { + // Get token from cookie + const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1]; + + fetchme(i, fetched); + } + } + + /** + * Register service worker + */ + private registerSw() { + // Check whether service worker and push manager supported + const isSwSupported = + ('serviceWorker' in navigator) && ('PushManager' in window); + + // Reject when browser not service worker supported + if (!isSwSupported) return; + + // Reject when not signed in to Misskey + if (!this.isSignedIn) return; + + // When service worker activated + navigator.serviceWorker.ready.then(registration => { + this.log('[sw] ready: ', registration); + + this.swRegistration = registration; + + // Options of pushManager.subscribe + // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters + const opts = { + // A boolean indicating that the returned push subscription + // will only be used for messages whose effect is made visible to the user. + userVisibleOnly: true, + + // A public key your push server will use to send + // messages to client apps via a push server. + applicationServerKey: urlBase64ToUint8Array(swPublickey) + }; + + // Subscribe push notification + this.swRegistration.pushManager.subscribe(opts).then(subscription => { + this.log('[sw] Subscribe OK:', subscription); + + function encode(buffer: ArrayBuffer) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); + } + + // Register + this.api('sw/register', { + endpoint: subscription.endpoint, + auth: encode(subscription.getKey('auth')), + publickey: encode(subscription.getKey('p256dh')) + }); + }) + // When subscribe failed + .catch(async (err: Error) => { + this.logError('[sw] Subscribe Error:', err); + + // 通知が許可されていなかったとき + if (err.name == 'NotAllowedError') { + this.logError('[sw] Subscribe failed due to notification not allowed'); + return; + } + + // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが + // 既に存在していることが原因でエラーになった可能性があるので、 + // そのサブスクリプションを解除しておく + const subscription = await this.swRegistration.pushManager.getSubscription(); + if (subscription) subscription.unsubscribe(); + }); + }); + + // Whether use raw version script + const raw = (localStorage.getItem('useRawScript') == 'true' && this.debug) + || process.env.NODE_ENV != 'production'; + + // The path of service worker script + const sw = `/sw.${version}.${lang}.${raw ? 'raw' : 'min'}.js`; + + // Register service worker + navigator.serviceWorker.register(sw).then(registration => { + // 登録成功 + this.logInfo('[sw] Registration successful with scope: ', registration.scope); + }).catch(err => { + // 登録失敗 :( + this.logError('[sw] Registration failed: ', err); + }); + } + + public requests = []; + + /** + * Misskey APIにリクエストします + * @param endpoint エンドポイント名 + * @param data パラメータ + */ + public api(endpoint: string, data: { [x: string]: any } = {}): Promise<{ [x: string]: any }> { + if (++pending === 1) { + spinner = document.createElement('div'); + spinner.setAttribute('id', 'wait'); + document.body.appendChild(spinner); + } + + // Append a credential + if (this.isSignedIn) (data as any).i = this.i.token; + + const viaStream = localStorage.getItem('enableExperimental') == 'true'; + + return new Promise((resolve, reject) => { + if (viaStream) { + const stream = this.stream.borrow(); + const id = Math.random().toString(); + + stream.once(`api-res:${id}`, res => { + if (res.res) { + resolve(res.res); + } else { + reject(res.e); + } + }); + + stream.send({ + type: 'api', + id, + endpoint, + data + }); + } else { + const req = { + id: uuid(), + date: new Date(), + name: endpoint, + data, + res: null, + status: null + }; + + if (this.debug) { + this.requests.push(req); + } + + // Send request + fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + credentials: endpoint === 'signin' ? 'include' : 'omit', + cache: 'no-cache' + }).then(async (res) => { + if (--pending === 0) spinner.parentNode.removeChild(spinner); + + const body = res.status === 204 ? null : await res.json(); + + if (this.debug) { + req.status = res.status; + req.res = body; + } + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + /*}*/ + }); + } + + /** + * Misskeyのメタ情報を取得します + * @param force キャッシュを無視するか否か + */ + public getMeta(force = false) { + return new Promise<{ [x: string]: any }>(async (res, rej) => { + if (this.isMetaFetching) { + this.once('_meta_fetched_', () => { + res(this.meta.data); + }); + return; + } + + const expire = 1000 * 60; // 1min + + // forceが有効, meta情報を保持していない or 期限切れ + if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) { + this.isMetaFetching = true; + const meta = await this.api('meta'); + this.meta = { + data: meta, + chachedAt: new Date() + }; + this.isMetaFetching = false; + this.emit('_meta_fetched_'); + res(meta); + } else { + res(this.meta.data); + } + }); + } + + public connections: Connection[] = []; + + public registerStreamConnection(connection: Connection) { + this.connections.push(connection); + } + + public unregisterStreamConnection(connection: Connection) { + this.connections = this.connections.filter(c => c != connection); + } +} + +class WindowSystem extends EventEmitter { + public windows = new Set(); + + public add(window) { + this.windows.add(window); + this.emit('added', window); + } + + public remove(window) { + this.windows.delete(window); + this.emit('removed', window); + } + + public getAll() { + return this.windows; + } +} + +/** + * Convert the URL safe base64 string to a Uint8Array + * @param base64String base64 string + */ +function urlBase64ToUint8Array(base64String: string): Uint8Array { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts new file mode 100644 index 0000000000..81c1eb9812 --- /dev/null +++ b/src/client/app/common/scripts/check-for-update.ts @@ -0,0 +1,33 @@ +import MiOS from '../mios'; +import { version as current } from '../../config'; + +export default async function(mios: MiOS, force = false, silent = false) { + const meta = await mios.getMeta(force); + const newer = meta.version; + + if (newer != current) { + localStorage.setItem('should-refresh', 'true'); + localStorage.setItem('v', newer); + + // Clear cache (serive worker) + try { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage('clear'); + } + + navigator.serviceWorker.getRegistrations().then(registrations => { + registrations.forEach(registration => registration.unregister()); + }); + } catch (e) { + console.error(e); + } + + if (!silent) { + alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current)); + } + + return newer; + } else { + return null; + } +} diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts new file mode 100644 index 0000000000..c19b1c5ad0 --- /dev/null +++ b/src/client/app/common/scripts/compose-notification.ts @@ -0,0 +1,68 @@ +import getNoteSummary from '../../../../renderers/get-note-summary'; +import getReactionEmoji from '../../../../renderers/get-reaction-emoji'; +import getUserName from '../../../../renderers/get-user-name'; + +type Notification = { + title: string; + body: string; + icon: string; + onclick?: any; +}; + +// TODO: i18n + +export default function(type, data): Notification { + switch (type) { + case 'drive_file_created': + return { + title: 'ファイルがアップロードされました', + body: data.name, + icon: data.url + '?thumbnail&size=64' + }; + + case 'mention': + return { + title: `${getUserName(data.user)}さんから:`, + body: getNoteSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'reply': + return { + title: `${getUserName(data.user)}さんから返信:`, + body: getNoteSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'quote': + return { + title: `${getUserName(data.user)}さんが引用:`, + body: getNoteSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'reaction': + return { + title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, + body: getNoteSummary(data.note), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'unread_messaging_message': + return { + title: `${getUserName(data.user)}さんからメッセージ:`, + body: data.text, // TODO: getMessagingMessageSummary(data), + icon: data.user.avatarUrl + '?thumbnail&size=64' + }; + + case 'othello_invited': + return { + title: '対局への招待があります', + body: `${getUserName(data.parent)}さんから`, + icon: data.parent.avatarUrl + '?thumbnail&size=64' + }; + + default: + return null; + } +} diff --git a/src/web/app/common/scripts/contains.js b/src/client/app/common/scripts/contains.ts similarity index 100% rename from src/web/app/common/scripts/contains.js rename to src/client/app/common/scripts/contains.ts diff --git a/src/web/app/common/scripts/copy-to-clipboard.js b/src/client/app/common/scripts/copy-to-clipboard.ts similarity index 100% rename from src/web/app/common/scripts/copy-to-clipboard.js rename to src/client/app/common/scripts/copy-to-clipboard.ts diff --git a/src/web/app/common/scripts/date-stringify.js b/src/client/app/common/scripts/date-stringify.ts similarity index 100% rename from src/web/app/common/scripts/date-stringify.js rename to src/client/app/common/scripts/date-stringify.ts diff --git a/src/client/app/common/scripts/fuck-ad-block.ts b/src/client/app/common/scripts/fuck-ad-block.ts new file mode 100644 index 0000000000..9bcf7deeff --- /dev/null +++ b/src/client/app/common/scripts/fuck-ad-block.ts @@ -0,0 +1,21 @@ +require('fuckadblock'); + +declare const fuckAdBlock: any; + +export default (os) => { + function adBlockDetected() { + os.apis.dialog({ + title: '%fa:exclamation-triangle%広告ブロッカーを無効にしてください', + text: 'Misskeyは広告を掲載していませんが、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。', + actins: [{ + text: 'OK' + }] + }); + } + + if (fuckAdBlock === undefined) { + adBlockDetected(); + } else { + fuckAdBlock.onDetected(adBlockDetected); + } +}; diff --git a/src/web/app/common/scripts/gcd.js b/src/client/app/common/scripts/gcd.ts similarity index 100% rename from src/web/app/common/scripts/gcd.js rename to src/client/app/common/scripts/gcd.ts diff --git a/src/web/app/common/scripts/get-kao.js b/src/client/app/common/scripts/get-kao.ts similarity index 65% rename from src/web/app/common/scripts/get-kao.js rename to src/client/app/common/scripts/get-kao.ts index 0b77ee285a..2168c5be88 100644 --- a/src/web/app/common/scripts/get-kao.js +++ b/src/client/app/common/scripts/get-kao.ts @@ -1,5 +1,5 @@ export default () => [ '(=^・・^=)', 'v(‘ω’)v', - '🐡( '-' 🐡 )フグパンチ!!!!' + '🐡( \'-\' 🐡 )フグパンチ!!!!' ][Math.floor(Math.random() * 3)]; diff --git a/src/client/app/common/scripts/get-median.ts b/src/client/app/common/scripts/get-median.ts new file mode 100644 index 0000000000..91a415d5b2 --- /dev/null +++ b/src/client/app/common/scripts/get-median.ts @@ -0,0 +1,11 @@ +/** + * 中央値を求めます + * @param samples サンプル + */ +export default function(samples) { + if (!samples.length) return 0; + const numbers = samples.slice(0).sort((a, b) => a - b); + const middle = Math.floor(numbers.length / 2); + const isEven = numbers.length % 2 === 0; + return isEven ? (numbers[middle] + numbers[middle - 1]) / 2 : numbers[middle]; +} diff --git a/src/web/app/common/scripts/loading.js b/src/client/app/common/scripts/loading.ts similarity index 100% rename from src/web/app/common/scripts/loading.js rename to src/client/app/common/scripts/loading.ts diff --git a/src/client/app/common/scripts/parse-search-query.ts b/src/client/app/common/scripts/parse-search-query.ts new file mode 100644 index 0000000000..5f6ae3320a --- /dev/null +++ b/src/client/app/common/scripts/parse-search-query.ts @@ -0,0 +1,53 @@ +export default function(qs: string) { + const q = { + text: '' + }; + + qs.split(' ').forEach(x => { + if (/^([a-z_]+?):(.+?)$/.test(x)) { + const [key, value] = x.split(':'); + switch (key) { + case 'user': + q['includeUserUsernames'] = value.split(','); + break; + case 'exclude_user': + q['excludeUserUsernames'] = value.split(','); + break; + case 'follow': + q['following'] = value == 'null' ? null : value == 'true'; + break; + case 'reply': + q['reply'] = value == 'null' ? null : value == 'true'; + break; + case 'renote': + q['renote'] = value == 'null' ? null : value == 'true'; + break; + case 'media': + q['media'] = value == 'null' ? null : value == 'true'; + break; + case 'poll': + q['poll'] = value == 'null' ? null : value == 'true'; + break; + case 'until': + case 'since': + // YYYY-MM-DD + if (/^[0-9]+\-[0-9]+\-[0-9]+$/) { + const [yyyy, mm, dd] = value.split('-'); + q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime(); + } + break; + default: + q[key] = value; + break; + } + } else { + q.text += x + ' '; + } + }); + + if (q.text) { + q.text = q.text.trim(); + } + + return q; +} diff --git a/src/client/app/common/scripts/streaming/channel.ts b/src/client/app/common/scripts/streaming/channel.ts new file mode 100644 index 0000000000..cab5f4edb4 --- /dev/null +++ b/src/client/app/common/scripts/streaming/channel.ts @@ -0,0 +1,13 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +/** + * Channel stream connection + */ +export default class Connection extends Stream { + constructor(os: MiOS, channelId) { + super(os, 'channel', { + channel: channelId + }); + } +} diff --git a/src/client/app/common/scripts/streaming/drive.ts b/src/client/app/common/scripts/streaming/drive.ts new file mode 100644 index 0000000000..7ff85b5946 --- /dev/null +++ b/src/client/app/common/scripts/streaming/drive.ts @@ -0,0 +1,34 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Drive stream connection + */ +export class DriveStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'drive', { + i: me.token + }); + } +} + +export class DriveStreamManager extends StreamManager { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new DriveStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts new file mode 100644 index 0000000000..e085801e15 --- /dev/null +++ b/src/client/app/common/scripts/streaming/home.ts @@ -0,0 +1,57 @@ +import * as merge from 'object-assign-deep'; + +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Home stream connection + */ +export class HomeStream extends Stream { + constructor(os: MiOS, me) { + super(os, '', { + i: me.token + }); + + // 最終利用日時を更新するため定期的にaliveメッセージを送信 + setInterval(() => { + this.send({ type: 'alive' }); + me.lastUsedAt = new Date(); + }, 1000 * 60); + + // 自分の情報が更新されたとき + this.on('i_updated', i => { + if (os.debug) { + console.log('I updated:', i); + } + merge(me, i); + }); + + // トークンが再生成されたとき + // このままではAPIが利用できないので強制的にサインアウトさせる + this.on('my_token_regenerated', () => { + alert('%i18n:common.my-token-regenerated%'); + os.signout(); + }); + } +} + +export class HomeStreamManager extends StreamManager { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new HomeStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/messaging-index.ts b/src/client/app/common/scripts/streaming/messaging-index.ts new file mode 100644 index 0000000000..84e2174ec4 --- /dev/null +++ b/src/client/app/common/scripts/streaming/messaging-index.ts @@ -0,0 +1,34 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Messaging index stream connection + */ +export class MessagingIndexStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'messaging-index', { + i: me.token + }); + } +} + +export class MessagingIndexStreamManager extends StreamManager { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new MessagingIndexStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/messaging.ts b/src/client/app/common/scripts/streaming/messaging.ts new file mode 100644 index 0000000000..c1b5875cfb --- /dev/null +++ b/src/client/app/common/scripts/streaming/messaging.ts @@ -0,0 +1,20 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +/** + * Messaging stream connection + */ +export class MessagingStream extends Stream { + constructor(os: MiOS, me, otherparty) { + super(os, 'messaging', { + i: me.token, + otherparty + }); + + (this as any).on('_connected_', () => { + this.send({ + i: me.token + }); + }); + } +} diff --git a/src/client/app/common/scripts/streaming/othello-game.ts b/src/client/app/common/scripts/streaming/othello-game.ts new file mode 100644 index 0000000000..b85af8f72b --- /dev/null +++ b/src/client/app/common/scripts/streaming/othello-game.ts @@ -0,0 +1,11 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +export class OthelloGameStream extends Stream { + constructor(os: MiOS, me, game) { + super(os, 'othello-game', { + i: me ? me.token : null, + game: game.id + }); + } +} diff --git a/src/client/app/common/scripts/streaming/othello.ts b/src/client/app/common/scripts/streaming/othello.ts new file mode 100644 index 0000000000..f5d47431cd --- /dev/null +++ b/src/client/app/common/scripts/streaming/othello.ts @@ -0,0 +1,31 @@ +import StreamManager from './stream-manager'; +import Stream from './stream'; +import MiOS from '../../mios'; + +export class OthelloStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'othello', { + i: me.token + }); + } +} + +export class OthelloStreamManager extends StreamManager { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new OthelloStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/requests.ts b/src/client/app/common/scripts/streaming/requests.ts new file mode 100644 index 0000000000..5bec30143f --- /dev/null +++ b/src/client/app/common/scripts/streaming/requests.ts @@ -0,0 +1,30 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Requests stream connection + */ +export class RequestsStream extends Stream { + constructor(os: MiOS) { + super(os, 'requests'); + } +} + +export class RequestsStreamManager extends StreamManager { + private os: MiOS; + + constructor(os: MiOS) { + super(); + + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new RequestsStream(this.os); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/server.ts b/src/client/app/common/scripts/streaming/server.ts new file mode 100644 index 0000000000..3d35ef4d9d --- /dev/null +++ b/src/client/app/common/scripts/streaming/server.ts @@ -0,0 +1,30 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../mios'; + +/** + * Server stream connection + */ +export class ServerStream extends Stream { + constructor(os: MiOS) { + super(os, 'server'); + } +} + +export class ServerStreamManager extends StreamManager { + private os: MiOS; + + constructor(os: MiOS) { + super(); + + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new ServerStream(this.os); + } + + return this.connection; + } +} diff --git a/src/client/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts new file mode 100644 index 0000000000..568b8b0372 --- /dev/null +++ b/src/client/app/common/scripts/streaming/stream-manager.ts @@ -0,0 +1,108 @@ +import { EventEmitter } from 'eventemitter3'; +import * as uuid from 'uuid'; +import Connection from './stream'; + +/** + * ストリーム接続を管理するクラス + * 複数の場所から同じストリームを利用する際、接続をまとめたりする + */ +export default abstract class StreamManager extends EventEmitter { + private _connection: T = null; + + private disposeTimerId: any; + + /** + * コネクションを必要としているユーザー + */ + private users = []; + + protected set connection(connection: T) { + this._connection = connection; + + if (this._connection == null) { + this.emit('disconnected'); + } else { + this.emit('connected', this._connection); + + this._connection.on('_connected_', () => { + this.emit('_connected_'); + }); + + this._connection.on('_disconnected_', () => { + this.emit('_disconnected_'); + }); + + this._connection.user = 'Managed'; + } + } + + protected get connection() { + return this._connection; + } + + /** + * コネクションを持っているか否か + */ + public get hasConnection() { + return this._connection != null; + } + + public get state(): string { + if (!this.hasConnection) return 'no-connection'; + return this._connection.state; + } + + /** + * コネクションを要求します + */ + public abstract getConnection(): T; + + /** + * 現在接続しているコネクションを取得します + */ + public borrow() { + return this._connection; + } + + /** + * コネクションを要求するためのユーザーIDを発行します + */ + public use() { + // タイマー解除 + if (this.disposeTimerId) { + clearTimeout(this.disposeTimerId); + this.disposeTimerId = null; + } + + // ユーザーID生成 + const userId = uuid(); + + this.users.push(userId); + + this._connection.user = `Managed (${ this.users.length })`; + + return userId; + } + + /** + * コネクションを利用し終わってもう必要ないことを通知します + * @param userId use で発行したユーザーID + */ + public dispose(userId) { + this.users = this.users.filter(id => id != userId); + + this._connection.user = `Managed (${ this.users.length })`; + + // 誰もコネクションの利用者がいなくなったら + if (this.users.length == 0) { + // また直ぐに再利用される可能性があるので、一定時間待ち、 + // 新たな利用者が現れなければコネクションを切断する + this.disposeTimerId = setTimeout(() => { + this.disposeTimerId = null; + + this.connection.close(); + this.connection = null; + }, 3000); + } + } +} diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts new file mode 100644 index 0000000000..3912186ad3 --- /dev/null +++ b/src/client/app/common/scripts/streaming/stream.ts @@ -0,0 +1,137 @@ +import { EventEmitter } from 'eventemitter3'; +import * as uuid from 'uuid'; +import * as ReconnectingWebsocket from 'reconnecting-websocket'; +import { wsUrl } from '../../../config'; +import MiOS from '../../mios'; + +/** + * Misskey stream connection + */ +export default class Connection extends EventEmitter { + public state: string; + private buffer: any[]; + public socket: ReconnectingWebsocket; + public name: string; + public connectedAt: Date; + public user: string = null; + public in: number = 0; + public out: number = 0; + public inout: Array<{ + type: 'in' | 'out', + at: Date, + data: string + }> = []; + public id: string; + public isSuspended = false; + private os: MiOS; + + constructor(os: MiOS, endpoint, params?) { + super(); + + //#region BIND + this.onOpen = this.onOpen.bind(this); + this.onClose = this.onClose.bind(this); + this.onMessage = this.onMessage.bind(this); + this.send = this.send.bind(this); + this.close = this.close.bind(this); + //#endregion + + this.id = uuid(); + this.os = os; + this.name = endpoint; + this.state = 'initializing'; + this.buffer = []; + + const query = params + ? Object.keys(params) + .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) + .join('&') + : null; + + this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`); + this.socket.addEventListener('open', this.onOpen); + this.socket.addEventListener('close', this.onClose); + this.socket.addEventListener('message', this.onMessage); + + // Register this connection for debugging + this.os.registerStreamConnection(this); + } + + /** + * Callback of when open connection + */ + private onOpen() { + this.state = 'connected'; + this.emit('_connected_'); + + this.connectedAt = new Date(); + + // バッファーを処理 + const _buffer = [].concat(this.buffer); // Shallow copy + this.buffer = []; // Clear buffer + _buffer.forEach(data => { + this.send(data); // Resend each buffered messages + + if (this.os.debug) { + this.out++; + this.inout.push({ type: 'out', at: new Date(), data }); + } + }); + } + + /** + * Callback of when close connection + */ + private onClose() { + this.state = 'reconnecting'; + this.emit('_disconnected_'); + } + + /** + * Callback of when received a message from connection + */ + private onMessage(message) { + if (this.isSuspended) return; + + if (this.os.debug) { + this.in++; + this.inout.push({ type: 'in', at: new Date(), data: message.data }); + } + + try { + const msg = JSON.parse(message.data); + if (msg.type) this.emit(msg.type, msg.body); + } catch (e) { + // noop + } + } + + /** + * Send a message to connection + */ + public send(data) { + if (this.isSuspended) return; + + // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する + if (this.state != 'connected') { + this.buffer.push(data); + return; + } + + if (this.os.debug) { + this.out++; + this.inout.push({ type: 'out', at: new Date(), data }); + } + + this.socket.send(JSON.stringify(data)); + } + + /** + * Close this connection + */ + public close() { + this.os.unregisterStreamConnection(this); + this.socket.removeEventListener('open', this.onOpen); + this.socket.removeEventListener('message', this.onMessage); + } +} diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue new file mode 100644 index 0000000000..5c8f61a2a2 --- /dev/null +++ b/src/client/app/common/views/components/autocomplete.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue new file mode 100644 index 0000000000..cadbd36ba4 --- /dev/null +++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue new file mode 100644 index 0000000000..185250dbd8 --- /dev/null +++ b/src/client/app/common/views/components/connect-failed.vue @@ -0,0 +1,106 @@ + + + + + + diff --git a/src/client/app/common/views/components/ellipsis.vue b/src/client/app/common/views/components/ellipsis.vue new file mode 100644 index 0000000000..07349902de --- /dev/null +++ b/src/client/app/common/views/components/ellipsis.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/client/app/common/views/components/file-type-icon.vue b/src/client/app/common/views/components/file-type-icon.vue new file mode 100644 index 0000000000..b7e868d1f7 --- /dev/null +++ b/src/client/app/common/views/components/file-type-icon.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue new file mode 100644 index 0000000000..6f334b965a --- /dev/null +++ b/src/client/app/common/views/components/forkit.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts new file mode 100644 index 0000000000..6bfe43a800 --- /dev/null +++ b/src/client/app/common/views/components/index.ts @@ -0,0 +1,51 @@ +import Vue from 'vue'; + +import signin from './signin.vue'; +import signup from './signup.vue'; +import forkit from './forkit.vue'; +import nav from './nav.vue'; +import noteHtml from './note-html'; +import poll from './poll.vue'; +import pollEditor from './poll-editor.vue'; +import reactionIcon from './reaction-icon.vue'; +import reactionsViewer from './reactions-viewer.vue'; +import time from './time.vue'; +import timer from './timer.vue'; +import mediaList from './media-list.vue'; +import uploader from './uploader.vue'; +import specialMessage from './special-message.vue'; +import streamIndicator from './stream-indicator.vue'; +import ellipsis from './ellipsis.vue'; +import messaging from './messaging.vue'; +import messagingRoom from './messaging-room.vue'; +import urlPreview from './url-preview.vue'; +import twitterSetting from './twitter-setting.vue'; +import fileTypeIcon from './file-type-icon.vue'; +import Switch from './switch.vue'; +import Othello from './othello.vue'; +import welcomeTimeline from './welcome-timeline.vue'; + +Vue.component('mk-signin', signin); +Vue.component('mk-signup', signup); +Vue.component('mk-forkit', forkit); +Vue.component('mk-nav', nav); +Vue.component('mk-note-html', noteHtml); +Vue.component('mk-poll', poll); +Vue.component('mk-poll-editor', pollEditor); +Vue.component('mk-reaction-icon', reactionIcon); +Vue.component('mk-reactions-viewer', reactionsViewer); +Vue.component('mk-time', time); +Vue.component('mk-timer', timer); +Vue.component('mk-media-list', mediaList); +Vue.component('mk-uploader', uploader); +Vue.component('mk-special-message', specialMessage); +Vue.component('mk-stream-indicator', streamIndicator); +Vue.component('mk-ellipsis', ellipsis); +Vue.component('mk-messaging', messaging); +Vue.component('mk-messaging-room', messagingRoom); +Vue.component('mk-url-preview', urlPreview); +Vue.component('mk-twitter-setting', twitterSetting); +Vue.component('mk-file-type-icon', fileTypeIcon); +Vue.component('mk-switch', Switch); +Vue.component('mk-othello', Othello); +Vue.component('mk-welcome-timeline', welcomeTimeline); diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue new file mode 100644 index 0000000000..64172ad0b4 --- /dev/null +++ b/src/client/app/common/views/components/media-list.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue new file mode 100644 index 0000000000..704f2016d8 --- /dev/null +++ b/src/client/app/common/views/components/messaging-room.form.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue new file mode 100644 index 0000000000..60e5258b63 --- /dev/null +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue new file mode 100644 index 0000000000..d30c64d74a --- /dev/null +++ b/src/client/app/common/views/components/messaging-room.vue @@ -0,0 +1,377 @@ + + + + + diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue new file mode 100644 index 0000000000..e6c32f80d8 --- /dev/null +++ b/src/client/app/common/views/components/messaging.vue @@ -0,0 +1,463 @@ + + + + + diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue new file mode 100644 index 0000000000..8ce75d3529 --- /dev/null +++ b/src/client/app/common/views/components/nav.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/client/app/common/views/components/note-html.ts b/src/client/app/common/views/components/note-html.ts new file mode 100644 index 0000000000..24e750a671 --- /dev/null +++ b/src/client/app/common/views/components/note-html.ts @@ -0,0 +1,157 @@ +import Vue from 'vue'; +import * as emojilib from 'emojilib'; +import parse from '../../../../../text/parse'; +import getAcct from '../../../../../acct/render'; +import { url } from '../../../config'; +import MkUrl from './url.vue'; + +const flatten = list => list.reduce( + (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] +); + +export default Vue.component('mk-note-html', { + props: { + text: { + type: String, + required: true + }, + ast: { + type: [], + required: false + }, + shouldBreak: { + type: Boolean, + default: true + }, + i: { + type: Object, + default: null + } + }, + + render(createElement) { + let ast; + + if (this.ast == null) { + // Parse text to ast + ast = parse(this.text); + } else { + ast = this.ast; + } + + // Parse ast to DOM + const els = flatten(ast.map(token => { + switch (token.type) { + case 'text': + const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); + + if (this.shouldBreak) { + const x = text.split('\n') + .map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]); + x[x.length - 1].pop(); + return x; + } else { + return createElement('span', text.replace(/\n/g, ' ')); + } + + case 'bold': + return createElement('strong', token.bold); + + case 'url': + return createElement(MkUrl, { + props: { + url: token.content, + target: '_blank' + } + }); + + case 'link': + return createElement('a', { + attrs: { + class: 'link', + href: token.url, + target: '_blank', + title: token.url + } + }, token.title); + + case 'mention': + return (createElement as any)('a', { + attrs: { + href: `${url}/@${getAcct(token)}`, + target: '_blank', + dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token) + }, + directives: [{ + name: 'user-preview', + value: token.content + }] + }, token.content); + + case 'hashtag': + return createElement('a', { + attrs: { + href: `${url}/search?q=${token.content}`, + target: '_blank' + } + }, token.content); + + case 'code': + return createElement('pre', [ + createElement('code', { + domProps: { + innerHTML: token.html + } + }) + ]); + + case 'inline-code': + return createElement('code', { + domProps: { + innerHTML: token.html + } + }); + + case 'quote': + const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n'); + + if (this.shouldBreak) { + const x = text2.split('\n') + .map(t => [createElement('span', t), createElement('br')]); + x[x.length - 1].pop(); + return createElement('div', { + attrs: { + class: 'quote' + } + }, x); + } else { + return createElement('span', { + attrs: { + class: 'quote' + } + }, text2.replace(/\n/g, ' ')); + } + + case 'emoji': + const emoji = emojilib.lib[token.emoji]; + return createElement('span', emoji ? emoji.char : token.content); + + default: + console.log('unknown ast type:', token.type); + } + })); + + const _els = []; + els.forEach((el, i) => { + if (el.tag == 'br') { + if (els[i - 1].tag != 'div') { + _els.push(el); + } + } else { + _els.push(el); + } + }); + + return createElement('span', _els); + } +}); diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue new file mode 100644 index 0000000000..d053748728 --- /dev/null +++ b/src/client/app/common/views/components/note-menu.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue new file mode 100644 index 0000000000..b9d946de96 --- /dev/null +++ b/src/client/app/common/views/components/othello.game.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/src/client/app/common/views/components/othello.gameroom.vue b/src/client/app/common/views/components/othello.gameroom.vue new file mode 100644 index 0000000000..dba9ccd16d --- /dev/null +++ b/src/client/app/common/views/components/othello.gameroom.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/client/app/common/views/components/othello.room.vue b/src/client/app/common/views/components/othello.room.vue new file mode 100644 index 0000000000..86368b3cc3 --- /dev/null +++ b/src/client/app/common/views/components/othello.room.vue @@ -0,0 +1,297 @@ + + + + + + + + + diff --git a/src/client/app/common/views/components/othello.vue b/src/client/app/common/views/components/othello.vue new file mode 100644 index 0000000000..8f7d9dfd6a --- /dev/null +++ b/src/client/app/common/views/components/othello.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue new file mode 100644 index 0000000000..47d901d7b1 --- /dev/null +++ b/src/client/app/common/views/components/poll-editor.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue new file mode 100644 index 0000000000..eb29aa8837 --- /dev/null +++ b/src/client/app/common/views/components/poll.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue new file mode 100644 index 0000000000..7d24f4f9e9 --- /dev/null +++ b/src/client/app/common/views/components/reaction-icon.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue new file mode 100644 index 0000000000..fa1998dca9 --- /dev/null +++ b/src/client/app/common/views/components/reaction-picker.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue new file mode 100644 index 0000000000..1afcf525d2 --- /dev/null +++ b/src/client/app/common/views/components/reactions-viewer.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue new file mode 100644 index 0000000000..da7472b8c7 --- /dev/null +++ b/src/client/app/common/views/components/signin.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue new file mode 100644 index 0000000000..30fe7b7ad0 --- /dev/null +++ b/src/client/app/common/views/components/signup.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/src/client/app/common/views/components/special-message.vue b/src/client/app/common/views/components/special-message.vue new file mode 100644 index 0000000000..2fd4d6515e --- /dev/null +++ b/src/client/app/common/views/components/special-message.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue new file mode 100644 index 0000000000..1f18fa76ed --- /dev/null +++ b/src/client/app/common/views/components/stream-indicator.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/client/app/common/views/components/switch.vue b/src/client/app/common/views/components/switch.vue new file mode 100644 index 0000000000..19a4adc3de --- /dev/null +++ b/src/client/app/common/views/components/switch.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/src/client/app/common/views/components/time.vue b/src/client/app/common/views/components/time.vue new file mode 100644 index 0000000000..6e0d2b0dcb --- /dev/null +++ b/src/client/app/common/views/components/time.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/client/app/common/views/components/timer.vue b/src/client/app/common/views/components/timer.vue new file mode 100644 index 0000000000..a3c4f01b77 --- /dev/null +++ b/src/client/app/common/views/components/timer.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue new file mode 100644 index 0000000000..00669cd833 --- /dev/null +++ b/src/client/app/common/views/components/twitter-setting.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue new file mode 100644 index 0000000000..ccad50dc37 --- /dev/null +++ b/src/client/app/common/views/components/uploader.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue new file mode 100644 index 0000000000..e91e510550 --- /dev/null +++ b/src/client/app/common/views/components/url-preview.vue @@ -0,0 +1,142 @@ +