mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-30 11:55:49 +00:00
Compare commits
1201 Commits
2026.7.0-beta.4
...
room
| Author | SHA1 | Date | |
|---|---|---|---|
| c35a6c01ff | |||
| f75c12fbd3 | |||
| 755a630468 | |||
| c2bb96e180 | |||
| 64d721d24a | |||
| a716d5511a | |||
| cb62a1d4fe | |||
| 6ec6a5d523 | |||
| 3bbe40baec | |||
| 2d7e79f924 | |||
| b3c981d14a | |||
| bcc3bc4fd7 | |||
| 1b878998a0 | |||
| dc773a2709 | |||
| 9feab0b673 | |||
| 6f579b6854 | |||
| c1d60fa5f5 | |||
| d93aede642 | |||
| d600525bb2 | |||
| 6dac6f7928 | |||
| 318af2e1ff | |||
| a44e419d5a | |||
| 3479b957e4 | |||
| 4682180c8b | |||
| 2509a28813 | |||
| a12ee2e5d7 | |||
| 6dffa2f59a | |||
| 86762db14e | |||
| f53b50ee54 | |||
| 58cb722bdf | |||
| ee2d16b237 | |||
| 319ddc674c | |||
| 703aa2360b | |||
| cb58ff32a4 | |||
| 9d4d04b6d2 | |||
| b9ca53785c | |||
| 83f3daccde | |||
| f6979333d4 | |||
| e897255e71 | |||
| daf2a85539 | |||
| 1a59ec20e3 | |||
| 3e31e59bcd | |||
| 982d4905c0 | |||
| 7ecce0901c | |||
| c70fb20b9e | |||
| cde2fe24c4 | |||
| e8951b7aa1 | |||
| d2973ecdca | |||
| cf7a6b6efc | |||
| 7c29a18d61 | |||
| 16ca5a72a6 | |||
| db8ee16e03 | |||
| 521f6006fe | |||
| 19fa9a24ab | |||
| f68da80ebd | |||
| fcb4ced33d | |||
| 85e810533b | |||
| 661838c89e | |||
| 80d14231c5 | |||
| 28153f7b5f | |||
| 6d81c81ab2 | |||
| 6fac907867 | |||
| 887f219119 | |||
| b1a1717fdc | |||
| 1903607659 | |||
| 7d750668c0 | |||
| db6708bfbd | |||
| bb06278e97 | |||
| 3fd8becdc5 | |||
| de94c15c8f | |||
| 68719db32f | |||
| 1e4539a168 | |||
| ef6c5dfc95 | |||
| 4fc9e38e0d | |||
| 60f60b6bac | |||
| a7a7b8bc69 | |||
| 253dd052f8 | |||
| c1eb67a4ce | |||
| a26d78d573 | |||
| 9e33507f01 | |||
| bde03b8259 | |||
| 5690d28d4e | |||
| 64a4283c35 | |||
| 9de48030fc | |||
| 1dbf9f9f85 | |||
| 37c3145859 | |||
| 600dae3d68 | |||
| ab9d6947ae | |||
| 56da1e5084 | |||
| 63d314750f | |||
| 1e10b67476 | |||
| cf2139d515 | |||
| 3d2941bd8f | |||
| c06eef6b58 | |||
| fa7a8b1282 | |||
| 319826fe76 | |||
| 54cfa4a13e | |||
| e507d41640 | |||
| d9cac0fa56 | |||
| 2e51cde16e | |||
| 98ac383b44 | |||
| e1290584f2 | |||
| 923d95d078 | |||
| 47a5f0cdd9 | |||
| 961d611cd4 | |||
| 2bc10ccc34 | |||
| 38dae938c0 | |||
| 1f2b635920 | |||
| 39c6baf6f2 | |||
| 79541bcbe3 | |||
| ba66b5d609 | |||
| a2c96c29c7 | |||
| 60db0a6bbd | |||
| 07f17820b3 | |||
| 66cd678cd3 | |||
| 1f604db203 | |||
| 06def2995b | |||
| c9107e5d2b | |||
| cbac208262 | |||
| b7c7323b97 | |||
| 602d034e94 | |||
| 22f78ab175 | |||
| d59afb5a29 | |||
| cc8e6a2e92 | |||
| 004ef368c2 | |||
| ac06c1700a | |||
| 2d0fb674d5 | |||
| 61b8b0aac3 | |||
| bd6e443cec | |||
| 897199a807 | |||
| 1f64579516 | |||
| 84203630a1 | |||
| 561e0c3813 | |||
| 169d655717 | |||
| ade652dd3e | |||
| 138353c84d | |||
| 4b30bfeb3c | |||
| aae2039df2 | |||
| 850e156687 | |||
| 844196cfea | |||
| 1925840f3f | |||
| d5df006463 | |||
| 2593652510 | |||
| 2e22b34c40 | |||
| 8d3c3284b5 | |||
| 7d7a125299 | |||
| aac51a171c | |||
| 908e0022e9 | |||
| b56a2a8bf7 | |||
| 61de0fb279 | |||
| b43ff3d820 | |||
| 23b2f4c7a0 | |||
| 827895b548 | |||
| 8b0dd1ea68 | |||
| ec3517ba58 | |||
| c24515044e | |||
| f55adc19d3 | |||
| 32af098cd9 | |||
| cfea88dce3 | |||
| 3a97e26f80 | |||
| 7bcb1b1732 | |||
| 8684cf14cc | |||
| 686a847697 | |||
| 0294ef74e4 | |||
| 42af31fbbd | |||
| b9ea42531a | |||
| 39c4618a35 | |||
| b025ce0ae6 | |||
| 07e660df01 | |||
| e07eed4a4b | |||
| 19ad95cfa6 | |||
| 8f5f595073 | |||
| 6a536d5c92 | |||
| c1e6e6a871 | |||
| 039bce590f | |||
| 24dac68475 | |||
| 8217a3a42c | |||
| dbcb856638 | |||
| dbbb197816 | |||
| 40263b0af5 | |||
| b978df8990 | |||
| 3a0da02b50 | |||
| af7d1a763a | |||
| a7e16c09dd | |||
| 351872f47b | |||
| e3c6c3025a | |||
| 273bc59cdb | |||
| 7a3e27d653 | |||
| 6a8aacfb95 | |||
| 99659a89b4 | |||
| 4591b940c3 | |||
| f01e9aa2ac | |||
| bba1605801 | |||
| 6d6dbb2584 | |||
| 51a08bbf34 | |||
| 7fa8bcc9fd | |||
| 9d5818f7b5 | |||
| bb028fda00 | |||
| 56ae76e994 | |||
| 69bc12201e | |||
| 313e730fbb | |||
| c14986b43d | |||
| 69c75fc464 | |||
| 1b13c945ff | |||
| 6e763d4daa | |||
| d1e5b04a10 | |||
| 7fec78b70e | |||
| c962cd33b3 | |||
| 814bf357ca | |||
| 222d914544 | |||
| b81b9e1da7 | |||
| 52cb23da63 | |||
| 6b4c2c9141 | |||
| 7af9069962 | |||
| 8f9be8c735 | |||
| cdbf5c843f | |||
| 27fff648b6 | |||
| 5cdc5f53b4 | |||
| c67e231ff5 | |||
| f564b7a29b | |||
| 1eea31aacd | |||
| 5da458ca54 | |||
| c343ccbdff | |||
| f8a981a1fc | |||
| 72df3d29ce | |||
| 75454b8075 | |||
| 559846fd7c | |||
| 8c1fa69800 | |||
| 810d437a2c | |||
| 3fca2cb664 | |||
| e95552b264 | |||
| 781f6135fd | |||
| e90830c738 | |||
| ab32ab6746 | |||
| 86c4f3072f | |||
| 8bb8b2fc97 | |||
| fad53328be | |||
| 0a6bfce548 | |||
| 5998b01ffc | |||
| 9a91170839 | |||
| a0aa64cd9f | |||
| 95864071a0 | |||
| ff9d997cec | |||
| c6d0855a2e | |||
| 624b858758 | |||
| d722b8e9d7 | |||
| fd02ea906f | |||
| aed9a4a1b4 | |||
| ace51f7605 | |||
| c10e1c4b31 | |||
| 4c80e92522 | |||
| 71c3f921cc | |||
| 6bc9eb9d84 | |||
| fbf3c02392 | |||
| 0c8b1055d4 | |||
| 0e7b517554 | |||
| 5cefdd224b | |||
| 1a5a4c834f | |||
| 2df145c458 | |||
| 43958fc70d | |||
| 8f94b04edc | |||
| c7d07d0364 | |||
| 5d974e8242 | |||
| 807ade4c37 | |||
| 2efd52f400 | |||
| f0375aa420 | |||
| 04c2703ace | |||
| 65022ed342 | |||
| 0d025d99f2 | |||
| e68847e563 | |||
| 409e9f744c | |||
| 9f81a88466 | |||
| 564d2446cb | |||
| cb605a3978 | |||
| 4f89010aca | |||
| 8933ac119a | |||
| 982897f1b5 | |||
| dd69662603 | |||
| 9485b3fba9 | |||
| 245f673a78 | |||
| 71bb388b8b | |||
| 39f2614f9a | |||
| 46efad5b12 | |||
| fda845cb63 | |||
| cccc66bbbf | |||
| 32822d6d07 | |||
| 7bca3ca179 | |||
| 6961f277b5 | |||
| eb33c8a93f | |||
| 43232d5baa | |||
| b8f67a3e69 | |||
| 748b571856 | |||
| f0bf3cda75 | |||
| bf28b303d7 | |||
| 1a064ea430 | |||
| 68431f62f1 | |||
| 2c8abf8239 | |||
| 822b567742 | |||
| a89475e820 | |||
| 2d8c6ca585 | |||
| fc6a840132 | |||
| 9a44c69177 | |||
| 0be946a10b | |||
| a201f72c8a | |||
| 83a4281a30 | |||
| 7546bf7dc2 | |||
| 2942234778 | |||
| 540e20864c | |||
| 6ee6894184 | |||
| 2a880164f8 | |||
| c002752d65 | |||
| 05e1a43a1c | |||
| ec79d6bee7 | |||
| a3b6dc0899 | |||
| 2fffa94eb2 | |||
| 0fade25574 | |||
| aa4fa9c54e | |||
| fdba915288 | |||
| 773077470e | |||
| 3eafdbe57a | |||
| 8138fd2c34 | |||
| ce72d10491 | |||
| f8851c0f2e | |||
| d3a7dc0ea5 | |||
| 32bdb77e97 | |||
| 8ac3c77ae1 | |||
| 8611d55ffc | |||
| 14b648faa6 | |||
| c6a755c0a7 | |||
| 1154bb5370 | |||
| 1701e993cf | |||
| 99f998cf60 | |||
| 05a8dcf897 | |||
| 90ea7902eb | |||
| 644c16aee6 | |||
| ce5857b265 | |||
| fbae160442 | |||
| 2daad2223d | |||
| 6cbae336d0 | |||
| 47f9f38a40 | |||
| 73d6764a0b | |||
| c85ee681bb | |||
| 61d5a5620a | |||
| 2873768cd2 | |||
| 4c957ecd9b | |||
| 8c61fc2be0 | |||
| 57057f56f5 | |||
| 985dd72a7f | |||
| dfbe765baa | |||
| 555fa80709 | |||
| 7546e7c800 | |||
| ce36de7e4f | |||
| 1cb96731e6 | |||
| 4bf0ba3716 | |||
| d2e48b707b | |||
| 8e70550c84 | |||
| 199c0f533b | |||
| ad44cc3446 | |||
| 8f73c22df3 | |||
| 20eb342eff | |||
| 5d17b557c1 | |||
| 43cb18b3e9 | |||
| 75dfcf48b6 | |||
| 9e9ae54c26 | |||
| e78694f11b | |||
| 5397beb965 | |||
| 92bc50cb90 | |||
| 45d8b656cb | |||
| a5e0c594b2 | |||
| 646b0ca041 | |||
| de27cc92dc | |||
| 09d5502eb6 | |||
| dc6ac2529f | |||
| a766609963 | |||
| e1aa62d2b7 | |||
| c62d92d88f | |||
| 1a456998bc | |||
| 14fe51c0a4 | |||
| f203ebe4d5 | |||
| de979d3a98 | |||
| 7c2e3adf25 | |||
| e24cf73a83 | |||
| d3eb3867bd | |||
| fcfd3331dd | |||
| 5aa1f0d562 | |||
| 491510a11e | |||
| e0382cdf86 | |||
| 9ae6a9f426 | |||
| a8ca0cf9ed | |||
| 45fc8994f0 | |||
| 85ae7c7efc | |||
| 2867f03f50 | |||
| 1cd75fe5a8 | |||
| 7f2286f1fd | |||
| 840e2c3744 | |||
| af1dbd3a71 | |||
| 82cdaeb56e | |||
| ea732458ab | |||
| b6e92cdef9 | |||
| 11b699a180 | |||
| e90a53a851 | |||
| 1469f890dd | |||
| 6c82e0fef8 | |||
| 6ee52259c4 | |||
| 414b522d0a | |||
| a95f3c9467 | |||
| ecb2ae379e | |||
| 2e34e6f454 | |||
| bee415625a | |||
| d0d3aef76c | |||
| 93a46ae2d6 | |||
| 8b5d006248 | |||
| bcc965a19c | |||
| 2c5ce5bfae | |||
| 170980a0a6 | |||
| 216fb56a4b | |||
| 8defcd9f9f | |||
| d38f04c97f | |||
| ecedd71192 | |||
| b582895ac6 | |||
| 267dc5fb64 | |||
| ab02a99a0e | |||
| 03481322a9 | |||
| fb130466ae | |||
| 43f20e44c3 | |||
| 2966d2a862 | |||
| 8f23991723 | |||
| bfdda83a6d | |||
| ddce5ff526 | |||
| 8dd62e57e2 | |||
| 0b17663eeb | |||
| 41c5e7242d | |||
| ef8f593de8 | |||
| 410cc8ac50 | |||
| a905eeef03 | |||
| ebb74eff92 | |||
| b69efeeb79 | |||
| 1a57b8177b | |||
| 0e300a9795 | |||
| 13d35f8124 | |||
| b53e4f6742 | |||
| 9d102c2a70 | |||
| 84f389aebd | |||
| 8072dbef8b | |||
| 8195f5257f | |||
| b8f8ac031c | |||
| 57f1adb402 | |||
| 48ce2e09ab | |||
| dca5450340 | |||
| 202c9e8f25 | |||
| 8e1e69e60e | |||
| f3d0edf546 | |||
| 08dfd23c19 | |||
| b231357ae8 | |||
| 102cf03213 | |||
| d1da15128e | |||
| 0d96f418ab | |||
| b879f2f1e3 | |||
| fb77eb4349 | |||
| 79ad7e274c | |||
| 4890673013 | |||
| 1f204f572f | |||
| 9976588025 | |||
| 414a28fb19 | |||
| 3605ffdafc | |||
| 179c9fc70a | |||
| f3a7f10319 | |||
| 17333fd7e5 | |||
| 14f4d2c228 | |||
| 18b4210eef | |||
| 54c2c4dd53 | |||
| 42da479026 | |||
| adc487ef78 | |||
| b08b3a2500 | |||
| 4324b6def2 | |||
| 21fe0f5e67 | |||
| 3ede04c563 | |||
| b750d69065 | |||
| 0c4b36e2d1 | |||
| 5cb9474494 | |||
| 3be075d281 | |||
| d7c94fbf86 | |||
| c6d7aa7be8 | |||
| d56c6dfe57 | |||
| f9be5d8c47 | |||
| ac023668a7 | |||
| 59c9b86842 | |||
| c4adcde114 | |||
| db1b5e9ce9 | |||
| a1cb4b8304 | |||
| fd04c5f2fc | |||
| 7be7465703 | |||
| 626ae675bc | |||
| 2a1cd5c197 | |||
| a9dd5fd5bf | |||
| beb5d1dec5 | |||
| d451ce8c36 | |||
| f0f78a11cb | |||
| 69dd2675fa | |||
| eb7691e3ba | |||
| db90e4ebc0 | |||
| 29491997ea | |||
| 015e6d1c81 | |||
| 49ee15dd9a | |||
| e6b5758d54 | |||
| 210368d597 | |||
| 203f29afb9 | |||
| fa0eac34c2 | |||
| d4fa5cf7ca | |||
| 479e9af17e | |||
| f03af71dc0 | |||
| 6d94f00ecf | |||
| 9e848f3135 | |||
| b16b158372 | |||
| 39525c66c2 | |||
| d3dc9bc86c | |||
| 444d862eac | |||
| 74e9851511 | |||
| a8586fe224 | |||
| d132fdfc04 | |||
| 3ba902c2b6 | |||
| 50f7c74259 | |||
| 9b7c908c68 | |||
| e7290c0486 | |||
| db22eddd1e | |||
| a8cea0622d | |||
| 5bf1b5569f | |||
| d1cb2c5bc7 | |||
| a49697042a | |||
| 4656d93358 | |||
| 2e5a02a85a | |||
| 8421ec75da | |||
| dcd2160294 | |||
| 3833469955 | |||
| 0adfbc8d51 | |||
| a931079896 | |||
| 18c08f52f1 | |||
| 8975449538 | |||
| 4e149a642d | |||
| 09d133242d | |||
| a8db20259b | |||
| d1eda166de | |||
| 7df4b729e9 | |||
| 3ed6148f6a | |||
| ebe5739ce3 | |||
| 9bc404a8f5 | |||
| 90ff3d79d1 | |||
| 6fb49ab88d | |||
| 722d09b1ae | |||
| 6d6ae6728c | |||
| ce2e74f3ca | |||
| b34e957c25 | |||
| 157b4673fd | |||
| ac1a19e95c | |||
| 5639324077 | |||
| a471fe16fa | |||
| 6cf90fd714 | |||
| c7c785ad2a | |||
| bde64b5b1f | |||
| 30f7727e33 | |||
| 5d36da17fe | |||
| 7bcda08339 | |||
| a15575078f | |||
| 56d813a184 | |||
| 8f5c09daa1 | |||
| c2d5a33400 | |||
| 6f3f4e7ef1 | |||
| 49f21d7423 | |||
| 92a6086e21 | |||
| 9d1c2d52d2 | |||
| f97a6c6d55 | |||
| 0f69a284c6 | |||
| 1427d887dd | |||
| a4c9aff8a9 | |||
| 5db8ccec74 | |||
| 70ebc0d32c | |||
| d885627350 | |||
| 4a77db7866 | |||
| e50d4fa8ab | |||
| 27578f2688 | |||
| fc97ba41af | |||
| a3610ae6c4 | |||
| fc615daad3 | |||
| 0321edb1ac | |||
| 8593737886 | |||
| 35ad1d758e | |||
| 110e5daa6f | |||
| 5de191f01a | |||
| 80c2b1fa65 | |||
| c8441da835 | |||
| fa2b1d6096 | |||
| 18a4da4ad7 | |||
| 52e9395fab | |||
| d05d7938a4 | |||
| 7846e8efb8 | |||
| e09f832fad | |||
| 515f6d9790 | |||
| 657159da45 | |||
| e88188cd6d | |||
| dcb834ed41 | |||
| 5a7960d0a9 | |||
| 11e55d8fe8 | |||
| 836de1bb28 | |||
| 58e617af6d | |||
| f44d566933 | |||
| 72fbc4bc9c | |||
| 746c16aecc | |||
| 7bef2cd8e0 | |||
| aed73eb074 | |||
| a756ca6ffb | |||
| f19040888a | |||
| 634cdf5e1e | |||
| c09d445215 | |||
| 1d71c0c6dd | |||
| 130a43f39a | |||
| 9c25c44a8a | |||
| 76ce6c84c0 | |||
| 24caff71e1 | |||
| 036b8ea320 | |||
| f98394fc60 | |||
| f52ac6351d | |||
| eb1357026d | |||
| 1b5be37f9b | |||
| 42c659c580 | |||
| ec32cad19f | |||
| 0910c47612 | |||
| 88e7303779 | |||
| 3a5532211b | |||
| 5b945278f9 | |||
| 0db2e5a42f | |||
| 8e1c5673b8 | |||
| a763c396bd | |||
| f5dae1d4c8 | |||
| 17697ba6ec | |||
| a77987ab28 | |||
| 47d9e92776 | |||
| af928ffe93 | |||
| 4adca586ed | |||
| e703705d60 | |||
| be434949a4 | |||
| bbff43e9e6 | |||
| 5c28ee0536 | |||
| 7e41d17c6a | |||
| 1cd6d01fdd | |||
| 3263f4bcc0 | |||
| 58feedb53d | |||
| e62c85a971 | |||
| 585d727297 | |||
| abd6c85b41 | |||
| 9551a3d01a | |||
| d281a81200 | |||
| 82741c2d61 | |||
| b00880c21f | |||
| df5d5d23cc | |||
| 35f6cac9f6 | |||
| 7ddcbf5e94 | |||
| 634bae3c49 | |||
| 0b50aa9d13 | |||
| e594ad9c6f | |||
| d105c707ec | |||
| 93f24c5b8f | |||
| 7c170a21e5 | |||
| 1de4440dbd | |||
| e6ce36178c | |||
| 8a6e925297 | |||
| eeda7e7002 | |||
| abfa67965e | |||
| 2c6560cc71 | |||
| 4984146f6e | |||
| c36dfc6643 | |||
| 9184f0d7b9 | |||
| 4c659c3129 | |||
| 10926e5525 | |||
| 8d3a5a6503 | |||
| 924c517bb5 | |||
| 54c339d89c | |||
| ce98d4244b | |||
| 4e4b56699b | |||
| fab7667b0a | |||
| 942e32f4de | |||
| 3897b044fe | |||
| c47a0c33cf | |||
| 2b849685a1 | |||
| bad29c7604 | |||
| b7b24a2140 | |||
| 20d9a03bb3 | |||
| 09d58a3ecf | |||
| 94e8050455 | |||
| 2f039ce2e9 | |||
| 61fd35bc97 | |||
| 2d36ccf1b2 | |||
| 174221fdc4 | |||
| 08a0f03a45 | |||
| de795a48e8 | |||
| af8a0bdf12 | |||
| b734ab3419 | |||
| 595e66c423 | |||
| 7b0e839661 | |||
| 6f79420fdc | |||
| b41bad4188 | |||
| 2f34f1c6f1 | |||
| 7b80da7737 | |||
| e65e3b4569 | |||
| 0ef489513a | |||
| a92cae8e09 | |||
| 2619509d69 | |||
| da8945dc23 | |||
| 64995bebc5 | |||
| c2dde53c1c | |||
| efaf7bdd95 | |||
| 3dcbfc0168 | |||
| e96e88b1ce | |||
| 493a2eb50c | |||
| fc4d769e1e | |||
| 0f0bc9b54f | |||
| ead90471c4 | |||
| 461e083454 | |||
| 79fe0fbd05 | |||
| db9d0090b7 | |||
| 1f81960640 | |||
| 5d389732d9 | |||
| b300f9620c | |||
| ddafd9e517 | |||
| 2b9f593e03 | |||
| 5f3f3d715a | |||
| de62fa3b59 | |||
| 32cba5b979 | |||
| ab90824b9c | |||
| ebec026508 | |||
| 8ddb2cbe75 | |||
| 97439d7718 | |||
| 60a2cd9306 | |||
| 62be4a258e | |||
| ae4a174de5 | |||
| 1e15503000 | |||
| 61ac82d297 | |||
| f18b3467d9 | |||
| dec440b6cc | |||
| 26ac4f7732 | |||
| b7b3c07a96 | |||
| 2040827615 | |||
| 2b456fec47 | |||
| b21ad59db3 | |||
| f04799a4f5 | |||
| 28dec6b0a3 | |||
| 714bff0835 | |||
| 772608ae99 | |||
| fe3b3704ba | |||
| d6caef7ee7 | |||
| 83a15f74ef | |||
| 27addb49cf | |||
| 6aa741f8d4 | |||
| e224bdf5e4 | |||
| 9fe2044f53 | |||
| b1aef2d308 | |||
| 79a063f692 | |||
| c3c36b06c2 | |||
| 358f0c0a6f | |||
| 6041db87e8 | |||
| ff4af812b5 | |||
| c2428ca3cc | |||
| e402057d3b | |||
| 3811de2283 | |||
| 421d466921 | |||
| a211d0df51 | |||
| c0690c9b80 | |||
| 8b9164a8c3 | |||
| 5df01bae9a | |||
| f8e093466d | |||
| 664ca528fe | |||
| aaab1e7260 | |||
| a85f05ca29 | |||
| 3253d30073 | |||
| 02d365e27b | |||
| a4be9b2078 | |||
| 47cd092380 | |||
| d1555d5423 | |||
| acd9b94b49 | |||
| 4b135bccd8 | |||
| 109fdd2ff3 | |||
| aa5275137e | |||
| 669286c1d8 | |||
| 623b4f087f | |||
| 02c6e1b876 | |||
| 6cca5706f7 | |||
| ebdf627b19 | |||
| ae463cde5e | |||
| 6350890e9f | |||
| 77fc803612 | |||
| e87752a07c | |||
| a7c8a3d6d1 | |||
| 2175a3a18f | |||
| 3d48146b92 | |||
| 4f9aded205 | |||
| e634fb1456 | |||
| 2b0d0d4533 | |||
| 4ac9da7f1f | |||
| 08e0ecf99b | |||
| 007a2481ef | |||
| 61eea5799b | |||
| 1fe9117944 | |||
| 4a16a71fa2 | |||
| 85701fcb6d | |||
| cd918817d9 | |||
| 24c0504cb0 | |||
| 750a48df62 | |||
| ead79ab275 | |||
| 124079f80a | |||
| ac2c6b93ce | |||
| b0d4ab371b | |||
| a3c3f7ba91 | |||
| 12e1b86b53 | |||
| 7fd9ac1cc8 | |||
| da8a7abcde | |||
| 6275196101 | |||
| 7eb8723082 | |||
| d730c26fcc | |||
| 56c2e1c989 | |||
| 77acce78dd | |||
| db5f64b097 | |||
| ddfd9f46f3 | |||
| 4254268f8d | |||
| 5104bafe95 | |||
| c426f95bee | |||
| c9ae842258 | |||
| 6a7e05d00a | |||
| af5bdb4296 | |||
| cf46a4af1e | |||
| 83daf43f49 | |||
| 3db0b8a1fd | |||
| cf9349f29c | |||
| 7e0b5ff8be | |||
| 28030ea3fa | |||
| 357aeff407 | |||
| 2f80442b99 | |||
| 3bad686c71 | |||
| 240c055b45 | |||
| 02eb8cfe1c | |||
| 87e2a046b7 | |||
| 4ce42a02c1 | |||
| f61b2504cc | |||
| 1b119c49a1 | |||
| 0ae3eb0721 | |||
| 205d2c3343 | |||
| b6e269d140 | |||
| b9335bc314 | |||
| ae92f75345 | |||
| 2ff307fe42 | |||
| 60bdd30681 | |||
| c877210828 | |||
| e85d1c6139 | |||
| 2094e82a30 | |||
| f86c5fb3b2 | |||
| 6b95d07930 | |||
| 886f64055d | |||
| 4740c76128 | |||
| baad4ae929 | |||
| e989c4b1a5 | |||
| 8f133d3fed | |||
| f26ebef565 | |||
| 7f46bd4928 | |||
| abe22da9ed | |||
| ebc9a60237 | |||
| 272c267ea0 | |||
| 21e6ac6678 | |||
| 2685e254f1 | |||
| 7d5c2052eb | |||
| e8d43032cc | |||
| 4ad5234325 | |||
| 6582087b2f | |||
| dc689a8c22 | |||
| fa8bdf55be | |||
| 87828dc0ad | |||
| 9fe161ec7c | |||
| 938dc5ce40 | |||
| 5049857e81 | |||
| 58cf1414fc | |||
| 6563eb9b8f | |||
| 69ac19d018 | |||
| eeae06014a | |||
| d808d20f8e | |||
| d775fa6360 | |||
| 5f4914e6dc | |||
| 8427685f7b | |||
| 7dd05a3d60 | |||
| 55747bdb99 | |||
| 909c0ae156 | |||
| 4c0b78a1ac | |||
| 62a9795685 | |||
| 04ff23c44f | |||
| ed119dfeb8 | |||
| c341ad21db | |||
| 11119ab046 | |||
| dc59fd4edb | |||
| 887f548985 | |||
| a627b58e85 | |||
| 57fbebaea5 | |||
| 6386ebe18e | |||
| 60bf7c6b1f | |||
| 6fe0e97ac6 | |||
| bf72fb0d9a | |||
| 85af12d35a | |||
| 7970fed6e1 | |||
| 03c0b48e1e | |||
| 45c851bb5b | |||
| 9b3424c5d3 | |||
| 9d9b47daca | |||
| 1d8f03e199 | |||
| 5e1c0d1064 | |||
| a6b7150371 | |||
| 7701bca55b | |||
| 547da71743 | |||
| 25ed41ba64 | |||
| 5e4d128f68 | |||
| 8ec7da0cf4 | |||
| 970efb3440 | |||
| 58acbac7ef | |||
| 5736b43149 | |||
| 682c5417cb | |||
| 127600a5d4 | |||
| 61386f699e | |||
| 3283ec410a | |||
| 6fd8c8c908 | |||
| 5b6fa78748 | |||
| 64fd4b7c0a | |||
| 3356cf36d3 | |||
| e2e4e83e6f | |||
| 8030e4f3fa | |||
| a00d80e30c | |||
| 82373f24d9 | |||
| fb0c089a16 | |||
| c04c6502be | |||
| 6b4310c91d | |||
| 662de635aa | |||
| af8d14a139 | |||
| afd731797e | |||
| d51d1191c5 | |||
| f4a060b9a8 | |||
| f6677aa02c | |||
| 0d966d8ded | |||
| a54a8a10ad | |||
| 602c8d8be1 | |||
| bdbbe83421 | |||
| 50575a272c | |||
| 3b80a96412 | |||
| fa979f1fed | |||
| 38bf5fc0bf | |||
| 78911b24de | |||
| 83b5305671 | |||
| 07d57b0edd | |||
| bd0e6f3268 | |||
| 0c808aa23d | |||
| 68eebb9d76 | |||
| b318cff137 | |||
| 614e8a7254 | |||
| 93f62f6054 | |||
| ca49618fdb | |||
| fdd360f490 | |||
| df5df25c80 | |||
| c454b8922b | |||
| ba4d495b42 | |||
| 4ed1d56f03 | |||
| 49823f9ec3 | |||
| 85dea8b49d | |||
| f2be8a2169 | |||
| 7121cd1ea9 | |||
| e79532e50a | |||
| 17f97bab7b | |||
| f13ada97a7 | |||
| 0db754bdd6 | |||
| 74dfbb7a74 | |||
| 2a583509ab | |||
| 4813ea5afc | |||
| 65b6821d4f | |||
| 8a165321b7 | |||
| b89fc36cd0 | |||
| f5ebbbca50 | |||
| 81414a18d3 | |||
| 6bf4feaef5 | |||
| 4efe4fb519 | |||
| 3873eb0cd7 | |||
| df092dd120 | |||
| 654c2c5b05 | |||
| f367bc37f8 | |||
| e8d8242c09 | |||
| 2ecaccedab | |||
| 4a7614f903 | |||
| d3da77c307 | |||
| 1c610ce825 | |||
| d5a5b04468 | |||
| b91409f5c6 | |||
| 74c67b843e | |||
| 913e35442c | |||
| 7a939dc5b3 | |||
| 71179b8b24 | |||
| c5a9c08814 | |||
| ea8df304c9 | |||
| a01bbf828d | |||
| d2807e974c | |||
| d6f41f9c51 | |||
| b2f2e9e75d | |||
| ee55a0a6cc | |||
| df184c8fdf | |||
| 4dd3bfc208 | |||
| c677dd6566 | |||
| de53f475c5 | |||
| b2eed6b82a | |||
| 6eefe6899c | |||
| 8619d0156e | |||
| 9bc0d7b361 | |||
| 9ffd8d777e | |||
| c558f93a0e | |||
| 5458ee016d | |||
| 5886260e0b | |||
| 42c7a483a4 | |||
| 7f5858a66f | |||
| 4965429069 | |||
| 58ec8af8cd | |||
| 1af8584aca | |||
| 564098a631 | |||
| 6fceb31c44 | |||
| 2a1a27f8c7 | |||
| 0e5c8496ce | |||
| 7ff95b8f8a | |||
| 295c91c245 | |||
| 7322697707 | |||
| ed4e3a51fd | |||
| bdc34305be | |||
| 354504b534 | |||
| f311105b54 | |||
| 2984b0000b | |||
| 98aadf8dcc | |||
| 1d3ddd279b | |||
| 349ee141bb | |||
| 9070159db7 | |||
| 0a4b81b0cc | |||
| 2de50f893f | |||
| 0157dd6b41 | |||
| 208c300460 | |||
| 46791a3bf2 | |||
| 16b54e9615 | |||
| 78b689f41c | |||
| 7b04d5d434 | |||
| c22345e3e0 | |||
| a90c179998 | |||
| 8eebeab692 | |||
| 66e0eeedfb | |||
| 3874f7abe9 | |||
| 98f74b0c7a | |||
| dbffa5520c | |||
| e336cbad62 | |||
| 2fab946b7a | |||
| 239df4694c | |||
| 1c9a324f3a | |||
| dbdb7ec324 | |||
| d43b3be6f0 | |||
| 34b46baaea | |||
| dc8dda3aac | |||
| 1b2717e256 | |||
| 9d723aaaa6 | |||
| 78bfcd71af | |||
| 5175a1e193 | |||
| 2438447ad3 | |||
| 01385575fd | |||
| 3f0c4b0577 | |||
| 36438e85d1 | |||
| 545009078a | |||
| 5910ec68e3 | |||
| 3e7166bd2c | |||
| dbfd1a751c | |||
| dda26f7f48 | |||
| 54b30d1138 | |||
| d8dc66781f | |||
| 4d37ada54d | |||
| a8456a45ab | |||
| 08667b4d35 | |||
| 03f20814c9 | |||
| 2672ae4463 | |||
| de4c1b3b66 | |||
| ec82773ff7 | |||
| cdb8d86fbf | |||
| 20dc48f221 | |||
| 0729e209c5 | |||
| c5eaf0f7af | |||
| dcae3ccaaa | |||
| fcc36759f7 | |||
| 689c24c776 | |||
| 055121d698 | |||
| 402dd538bf | |||
| 8bdf773a2b | |||
| 460f79d5cf | |||
| 998f85b260 | |||
| a0356d8d4d | |||
| d68655f5c2 | |||
| bba7076eca | |||
| aae03a914d | |||
| cdc9b47b78 | |||
| 41d40f53cf | |||
| 17a3bdb5eb | |||
| dadc5295fa | |||
| 679c75006a | |||
| cd9612e664 | |||
| d01b3036d6 | |||
| 376bb328df | |||
| 6a08231591 | |||
| 411c4ef3ae | |||
| 6f32e09db5 | |||
| 86f6498ddd | |||
| 5619cbb0da | |||
| 9475e6151f | |||
| af560802b3 | |||
| 3375220aee | |||
| ce7af6a308 | |||
| d446e00964 | |||
| 90fa65c96e | |||
| 3717962757 | |||
| 8bd2003a38 | |||
| 07909ab228 | |||
| 503a02ac42 | |||
| 8a0ba3a18a | |||
| 7b7767942f | |||
| 8c28c7c253 | |||
| 0d8a6e8136 | |||
| 6a4a09c8cf | |||
| 06adb3e045 | |||
| c12f330432 | |||
| a45611171a | |||
| f58de15d45 | |||
| aa6c9be133 | |||
| 2841f67166 | |||
| b1bb07542a | |||
| eb0544e083 | |||
| d490891acc | |||
| 4da92509cb | |||
| f85223c064 | |||
| 0397fccdb3 | |||
| c93758b554 | |||
| be67e75ef9 | |||
| d8d4b230b0 | |||
| 0996c2d9b2 | |||
| 0bcc5a3695 | |||
| e70743bf40 | |||
| 9ecb3d6a5a | |||
| 9f0fbb8531 | |||
| 52a1b30503 | |||
| 1dec481a7e | |||
| ad48f43524 | |||
| 7efa04d561 | |||
| bf7f771760 | |||
| 3acf6db835 | |||
| aafcffd1ad | |||
| 82f68f1e93 | |||
| 3a02ae8b28 | |||
| 3022313fac | |||
| 22d5c27ca7 | |||
| 8665923337 | |||
| efd101d0a0 | |||
| 36f17a156f | |||
| 2e84d2864c | |||
| 909f78b33c | |||
| cb6c790d6c | |||
| b790608f52 | |||
| 13e3bdc90b | |||
| 2a1a03ef9d | |||
| fb25331661 | |||
| 51d2b0d6a5 | |||
| de1a3e3765 | |||
| 68d28eb4ac | |||
| ad150f4718 | |||
| 91e3249b23 | |||
| 24a7131b0b | |||
| a0e318b43f | |||
| e41e700f2d | |||
| 6c64e75412 | |||
| 0c5c0ce67e | |||
| 6ac091096b | |||
| b05010bdc4 | |||
| d033704f12 | |||
| 367119a5a2 | |||
| d15c971125 | |||
| 4d532199b4 | |||
| cd15906c29 | |||
| 4ee5c73bca | |||
| cb1d9c38df | |||
| bce3411cef | |||
| a721a94902 | |||
| 3173290abb | |||
| 491b40ed80 | |||
| ab1362264a | |||
| 09993a8ac8 | |||
| 8619367ac0 | |||
| 148c853000 | |||
| 14efbe6584 | |||
| 9f054bb97b | |||
| 6f07445185 | |||
| 80b5c6cd35 | |||
| 7ab225662c | |||
| 85dc9c738b | |||
| a09bf963f4 | |||
| 242fd56aec | |||
| a42fdee480 |
@@ -173,8 +173,31 @@ id: 'aidx'
|
||||
|
||||
#sentryForBackend:
|
||||
# enableNodeProfiling: true
|
||||
# # Specify Sentry integration names to disable individual auto-instrumentation.
|
||||
# # The names are integration .name values, not factory function names.
|
||||
# # To check enabled names, set `options.debug: true` and see
|
||||
# # "Integration installed: <name>" in the Sentry logs.
|
||||
# #
|
||||
# # As of 2026-07-07 / @sentry/node 10.62.0, useful names for Misskey include:
|
||||
# # Postgres ... DB queries (when pg is externalized)
|
||||
# # Redis ... ioredis commands
|
||||
# # Fastify ... inbound HTTP routes
|
||||
# # Http ... inbound/outbound HTTP; disabling this can also affect request isolation
|
||||
# # NodeFetch ... fetch/undici requests
|
||||
# disabledIntegrations: ['Postgres']
|
||||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
# # By default, Misskey prevents Sentry trace headers (`sentry-trace` and
|
||||
# # `baggage`) from being sent to remote ActivityPub/Webhook/etc. hosts.
|
||||
# # To intentionally propagate distributed traces to trusted internal services,
|
||||
# # list only those internal URL patterns here.
|
||||
# # Avoid broad patterns that match remote servers unless you intentionally
|
||||
# # want to restore Sentry's legacy behavior of propagating traces to all
|
||||
# # outbound HTTP requests.
|
||||
# #tracePropagationTargets: []
|
||||
# # Internal allowlist example:
|
||||
# #tracePropagationTargets:
|
||||
# # - 'internal-service.example'
|
||||
|
||||
#sentryForFrontend:
|
||||
# vueIntegration:
|
||||
|
||||
@@ -329,63 +329,6 @@ id: 'aidx'
|
||||
# #tracePropagationTargets:
|
||||
# # - 'internal-service.example'
|
||||
|
||||
# ┌─────────┐
|
||||
#───┘ Tracing └────────────────────────────────────────────────
|
||||
# OpenTelemetry distributed tracing (Traces only, opt-in).
|
||||
# Existing API and Queue spans are exported to an OTLP http/protobuf endpoint.
|
||||
#
|
||||
# Standard OTEL_* environment variables are honored for values omitted below:
|
||||
# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||
# OTEL_EXPORTER_OTLP_HEADERS, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, etc.
|
||||
#
|
||||
# When this is enabled together with sentryForBackend, Misskey shares Sentry's
|
||||
# TracerProvider and adds an OTLP SpanProcessor to it. In that mode:
|
||||
# - sampleRate below is ignored; sampling follows sentryForBackend.options
|
||||
# tracesSampleRate / tracesSampler.
|
||||
# - resourceAttributes below is ignored; set OTEL_SERVICE_NAME and
|
||||
# OTEL_RESOURCE_ATTRIBUTES in the environment if you need to override them.
|
||||
# - spans produced by Sentry integrations are exported to both Sentry and OTLP.
|
||||
# - sentry-trace / baggage propagation to outbound remote requests is disabled
|
||||
# by default while OTel is enabled. Set propagateTraceToRemote: true only if
|
||||
# you intentionally want Sentry trace propagation for your deployment.
|
||||
|
||||
#otelForBackend:
|
||||
# endpoint: 'http://localhost:4318/v1/traces'
|
||||
# #headers:
|
||||
# # authorization: 'Bearer xxxxx'
|
||||
# #sampleRate: 1.0
|
||||
# # Record PostgreSQL query spans. Disabled by default to avoid instrumentation
|
||||
# # overhead when database-level diagnostics are not needed.
|
||||
# #capturePgSpans: false
|
||||
# # Include raw SQL text in PostgreSQL spans. Requires capturePgSpans and is
|
||||
# # disabled by default because non-parameterized queries can expose values to
|
||||
# # the OTLP Collector.
|
||||
# #capturePgStatement: false
|
||||
# # Include PostgreSQL connection-pool spans with an existing parent span.
|
||||
# # Requires capturePgSpans and is disabled by default to avoid noisy spans
|
||||
# # from connection churn.
|
||||
# #capturePgConnectionSpans: false
|
||||
# # Record Redis command spans. Disabled by default because subscribing to the
|
||||
# # ioredis diagnostics channel adds work to every Redis command, including
|
||||
# # commands without a parent span. Enable for development diagnostics or when
|
||||
# # the added overhead is acceptable in production.
|
||||
# #captureRedisCommandSpans: false
|
||||
# # Include Redis startup/reconnect spans. Disabled by default to avoid noisy
|
||||
# # connection churn; these spans are recorded even without an HTTP or job
|
||||
# # parent span.
|
||||
# #captureRedisConnectionSpans: false
|
||||
# # Record Redis commands without an HTTP or job parent span. Disabled by
|
||||
# # default because queue polling and background work can produce many root
|
||||
# # traces; only applies when captureRedisCommandSpans is enabled.
|
||||
# #captureRedisRootSpans: false
|
||||
# #resourceAttributes:
|
||||
# # deployment.environment: 'production'
|
||||
# #propagateTraceToRemote: false
|
||||
# # Queue worker spans are independent traces linked to their enqueuer by default.
|
||||
# # Set to 'parent' to make them child spans instead. This can create very large
|
||||
# # traces for high-fan-out queues such as federation delivery.
|
||||
# #jobTraceContextMode: 'link'
|
||||
|
||||
#sentryForFrontend:
|
||||
# vueIntegration:
|
||||
# tracingOptions:
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -35,13 +35,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
path: base
|
||||
submodules: true
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.number }}/merge
|
||||
path: head
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
with:
|
||||
package_json_file: head/package.json
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: 'head/.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -12,13 +12,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: pnpm
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
name: Check Misskey JS autogen (comment)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
types: [completed]
|
||||
workflows:
|
||||
- Check Misskey JS autogen # check-misskey-js-autogen.yml
|
||||
|
||||
jobs:
|
||||
comment-misskey-js-autogen:
|
||||
runs-on: ubuntu-latest
|
||||
# No code is cloned or executed here, so it is safe to hold pull-requests: write
|
||||
if: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
# The result is missing when the run failed before the check job (e.g. the PR does not build), so do not fail here
|
||||
- name: Download result
|
||||
id: download-result
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: misskey-js-autogen-result
|
||||
path: result
|
||||
github-token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
# The artifact comes from a run that built fork code, so validate it before using
|
||||
- name: Load result
|
||||
id: load-result
|
||||
if: steps.download-result.outcome == 'success'
|
||||
run: |
|
||||
pr_number="$(cat result/pr-number)"
|
||||
changes="$(cat result/changes)"
|
||||
|
||||
case "$pr_number" in
|
||||
''|*[!0-9]*) echo "invalid pr number"; exit 1;;
|
||||
esac
|
||||
case "$changes" in
|
||||
true|false) ;;
|
||||
*) echo "invalid result"; exit 1;;
|
||||
esac
|
||||
|
||||
echo "pr-number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||
echo "changes=$changes" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: send message
|
||||
if: steps.load-result.outputs.changes == 'true'
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
pr-number: ${{ steps.load-result.outputs.pr-number }}
|
||||
comment-tag: check-misskey-js-autogen
|
||||
message: |-
|
||||
Thank you for sending us a great Pull Request! 👍
|
||||
Please regenerate misskey-js type definitions! 🙏
|
||||
|
||||
example:
|
||||
```sh
|
||||
pnpm run build-misskey-js-with-types
|
||||
```
|
||||
|
||||
- name: send message
|
||||
if: steps.load-result.outputs.changes == 'false'
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
pr-number: ${{ steps.load-result.outputs.pr-number }}
|
||||
comment-tag: check-misskey-js-autogen
|
||||
mode: delete
|
||||
message: "Thank you!"
|
||||
create_if_not_exists: false
|
||||
@@ -1,7 +1,8 @@
|
||||
# this name is used in check-misskey-js-autogen.comment.yml so be careful when change name
|
||||
name: Check Misskey JS autogen
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
@@ -10,7 +11,6 @@ on:
|
||||
- packages/backend/**
|
||||
|
||||
jobs:
|
||||
# pull_request_target safety: permissions: read-all, and there are no secrets used in this job
|
||||
generate-misskey-js:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
persist-credentials: false
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
- name: setup node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: pnpm
|
||||
@@ -58,7 +58,6 @@ jobs:
|
||||
name: generated-misskey-js
|
||||
path: packages/misskey-js/generator/built/autogen
|
||||
|
||||
# pull_request_target safety: permissions: read-all, and no user codes are executed
|
||||
get-actual-misskey-js:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -66,7 +65,7 @@ jobs:
|
||||
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
persist-credentials: false
|
||||
@@ -78,12 +77,11 @@ jobs:
|
||||
name: actual-misskey-js
|
||||
path: packages/misskey-js/src/autogen
|
||||
|
||||
# pull_request_target safety: nothing is cloned from repository
|
||||
comment-misskey-js-autogen:
|
||||
check-misskey-js-autogen:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [generate-misskey-js, get-actual-misskey-js]
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: download generated-misskey-js
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -111,28 +109,21 @@ jobs:
|
||||
- name: Print full diff
|
||||
run: cat ./misskey-js.diff
|
||||
|
||||
- name: send message
|
||||
if: steps.check-changes.outputs.changes == 'true'
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
comment-tag: check-misskey-js-autogen
|
||||
message: |-
|
||||
Thank you for sending us a great Pull Request! 👍
|
||||
Please regenerate misskey-js type definitions! 🙏
|
||||
# A fork's pull_request token cannot comment, so hand the result to check-misskey-js-autogen.comment.yml
|
||||
- name: Save result
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
CHANGES: ${{ steps.check-changes.outputs.changes }}
|
||||
run: |
|
||||
mkdir -p result
|
||||
echo "$PR_NUMBER" > result/pr-number
|
||||
echo "$CHANGES" > result/changes
|
||||
|
||||
example:
|
||||
```sh
|
||||
pnpm run build-misskey-js-with-types
|
||||
```
|
||||
|
||||
- name: send message
|
||||
if: steps.check-changes.outputs.changes == 'false'
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
- name: Upload result
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
comment-tag: check-misskey-js-autogen
|
||||
mode: delete
|
||||
message: "Thank you!"
|
||||
create_if_not_exists: false
|
||||
name: misskey-js-autogen-result
|
||||
path: result
|
||||
|
||||
- name: Make failure if changes are detected
|
||||
if: steps.check-changes.outputs.changes == 'true'
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Check version
|
||||
run: |
|
||||
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Check
|
||||
run: |
|
||||
counter=0
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
check_copyright_year:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
- run: |
|
||||
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
|
||||
echo "Please change copyright year!"
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Log in to Docker Hub
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Docker meta
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
DOCKLE_VERSION: 0.4.15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
|
||||
run: |
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
package_json_file: head/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: head/.node-version
|
||||
cache: pnpm
|
||||
|
||||
@@ -25,14 +25,14 @@ jobs:
|
||||
ref: refs/pull/${{ github.event.number }}/merge
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
ref: ${{ matrix.ref }}
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -11,6 +11,6 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
- uses: actions/labeler@v7
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
@@ -16,6 +16,8 @@ on:
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/misskey-world/**
|
||||
- packages/frontend-misskey-world-engine/**
|
||||
- packages/shared/eslint.config.js
|
||||
- scripts/check-dts*.mjs
|
||||
- .github/workflows/lint.yml
|
||||
@@ -32,6 +34,8 @@ on:
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/misskey-world/**
|
||||
- packages/frontend-misskey-world-engine/**
|
||||
- packages/shared/eslint.config.js
|
||||
- scripts/check-dts*.mjs
|
||||
- .github/workflows/lint.yml
|
||||
@@ -40,13 +44,13 @@ jobs:
|
||||
pnpm_install:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -69,23 +73,25 @@ jobs:
|
||||
- misskey-js
|
||||
- misskey-bubble-game
|
||||
- misskey-reversi
|
||||
- misskey-world
|
||||
- frontend-misskey-world-engine
|
||||
env:
|
||||
eslint-cache-version: v1
|
||||
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Restore eslint cache
|
||||
uses: actions/cache@v5.0.5
|
||||
uses: actions/cache@v6.1.0
|
||||
with:
|
||||
path: ${{ env.eslint-cache-path }}
|
||||
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
@@ -104,13 +110,13 @@ jobs:
|
||||
- sw
|
||||
- misskey-js
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -123,13 +129,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ".node-version"
|
||||
cache: "pnpm"
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -36,12 +36,12 @@ jobs:
|
||||
- diagnostics-frontend
|
||||
- changelog-checker
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
edit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||
- name: Get PR
|
||||
run: |
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
outputs:
|
||||
pr_number: ${{ steps.get_pr.outputs.pr_number }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||
- name: Get PRs
|
||||
run: |
|
||||
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
NODE_OPTIONS: "--max_old_space_size=7168"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
if: github.event_name != 'pull_request_target'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
if: github.event_name == 'pull_request_target'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
ports:
|
||||
- 56312:6379
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.48.1
|
||||
image: getmeili/meilisearch:v1.49.0
|
||||
ports:
|
||||
- 57712:7700
|
||||
env:
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
MEILI_ENV: development
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt install -y ffmpeg
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter backend test-and-coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
@@ -103,13 +103,13 @@ jobs:
|
||||
- 56312:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter backend test-and-coverage:e2e
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
id: current-date
|
||||
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- .node-version
|
||||
- .github/min.node-version
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt install -y ffmpeg
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -28,13 +28,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter frontend test-and-coverage
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/frontend/coverage/coverage-final.json
|
||||
@@ -71,13 +71,13 @@ jobs:
|
||||
- 56312:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -22,13 +22,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
CI: true
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/misskey-js/coverage/coverage-final.json
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -17,13 +17,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
+11
-13
@@ -12,16 +12,19 @@
|
||||
- Node.js のセキュリティアップデートに伴い、最低動作バージョンを 22.22.2 / 24.17.0 / 26.4.0 に引き上げました。
|
||||
- Docker Image は Node.js 26.4.0-trixie に更新されています。
|
||||
- バックエンドで画像処理に用いているライブラリ sharp のシステム要件の変更により、**SSE4.2 命令セットをサポートしていない x86_64 CPU では Misskey が正しく動作しなくなります**。仮想マシンに Misskey をデプロイしている場合や、古いハードウェアをお使いの場合は、アップデート前にお使いの環境をご確認ください。なお、ARM64 など x86_64 ではない環境においてはこの変更による影響はありません。
|
||||
- YAMLパーサーをアップデートし、より厳格なチェックが行われるようになりました。起動時にconfigの読み取りで構文エラーが発生する可能性があります。\
|
||||
例えば `allowPrivateNetworks` を 2026.6.0 までの `example.yml` の構文を元に記述している場合は、配列の閉じ括弧のインデントを上げるか、リスト表示に書き換える必要があります。詳しくは https://github.com/misskey-dev/misskey/pull/17701 をご覧ください。
|
||||
|
||||
### General
|
||||
- Feat: コントロールパネルから二要素認証を解除できるように
|
||||
- Feat: 条件に一致したURLプレビューのサムネイルを隠すことができるように
|
||||
(Based on https://github.com/MisskeyIO/misskey/pull/214)
|
||||
- Enhance: 依存関係の更新
|
||||
|
||||
### Client
|
||||
- 2025.4.0 以前の設定情報の移行処理が削除されました
|
||||
- 2025.4.0 から直接 2026.6.0 以上にアップデートする場合は設定が移行されませんので注意してください。移行したい場合は一度 2026.5.1 を経由してください。
|
||||
- Enhance: 画像ビューワーを独自実装に変更・動画プレイヤーを統合
|
||||
- Enhance: 画像ビューワーを刷新・動画プレイヤーを統合
|
||||
- 操作性の改善
|
||||
- ビューワーとMisskeyの各種機能との統合を強化
|
||||
- パフォーマンスの向上
|
||||
@@ -29,8 +32,9 @@
|
||||
- Fix: 幅が狭い画面で動画の再生が困難な問題を修正
|
||||
- Fix: 一部の画像のみセンシティブなとき、ビューワー内で画像を切り替えるとセンシティブな画像がそのまま表示される問題を修正
|
||||
- Fix: 一部の画像をビューワーで読み込んだ際に正しく表示されない問題を修正
|
||||
- Enhance: タイムラインの読み込みパフォーマンスを改善
|
||||
- Enhance: ファイルアップロード前にプレビューできるように
|
||||
- Enhance: タブがバックグラウンドの間は必要ない定期更新処理を停止するように
|
||||
- Enhance: 翻訳の更新
|
||||
- Fix: 「画像を新しいタブで開く」が機能しなくなっていた問題を修正
|
||||
- Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正
|
||||
- Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正
|
||||
@@ -44,21 +48,12 @@
|
||||
- Fix: ノートの詳細表示で削除された引用元が表示されない問題を修正
|
||||
|
||||
### Server
|
||||
- Feat: OpenTelemetryサポート
|
||||
- 詳細な設定はconfigファイルを参照してください。
|
||||
- Sentryとの併用も可能です。Sentry併用時は、PostgreSQL Query と Redis command は Sentry で計装されます。
|
||||
- 以下の自動計装をサポートしています。(計装対象にする項目は設定可能)
|
||||
- PostgreSQL query
|
||||
- Redis command
|
||||
- 全ての受信HTTPリクエスト
|
||||
- 全ての送信HTTPリクエスト
|
||||
- ジョブキュー(エンキュー元のトレースを含む)
|
||||
- Feat: ログ基盤の刷新
|
||||
- API内部エラーのログに構造化属性と正規化したエラー情報を付与し、認証情報を自動的に秘匿するように(従来形式の表示は維持)
|
||||
- ログ全体の既定出力レベルとドメインごとの出力レベルを設定できるように
|
||||
- バックエンドのログを1行JSON形式で出力できるように
|
||||
- OpenTelemetryのTrace ContextをJSON形式のログへ関連付けられるように
|
||||
- HTTPのAccess logをstatus class単位で出力できるように(開発時のリクエスト・レスポンス本文、OpenTelemetryのTrace Contextにも対応)
|
||||
- SentryのTrace ContextをJSON形式のログへ関連付けられるように
|
||||
- HTTPのAccess logをstatus class単位で出力できるように(開発時のリクエスト・レスポンス本文、SentryのTrace Contextにも対応)
|
||||
- Enhance: Sentry バックエンドの自動計装を `sentryForBackend.disabledIntegrations` で個別に無効化できるように
|
||||
- Enhance: センシティブメディアの判定を外部サービス ([sensitive-detector](https://github.com/misskey-dev/sensitive-detector)) に分離し、`nsfwjs` / `@tensorflow/tfjs(-node)` の同梱と NSFW 判定モデルを廃止 (#16804)
|
||||
- Enhance: Node.js 22.22.2以降、24.17.0以降、26.4.0以降をサポートするように
|
||||
@@ -70,6 +65,9 @@
|
||||
- Fix: Sentry 使用環境下にて、外部送信リクエストへ `sentry-trace` / `baggage` ヘッダーが既定で付与されないように
|
||||
- Fix: フォロワー限定投稿へのリプライをホーム投稿に出来る問題を修正
|
||||
- Fix: ファイルをアップロードするAPIにて、処理終了後に一時ファイルが削除されないことがある問題を修正
|
||||
- Fix: 初期設定で作成したアカウント以外でアカウント作成APIが使用できない問題を修正
|
||||
- Fix: フォロー中のチャンネルを再度フォローした際にALREADY_FOLLOWINGエラーを返すように
|
||||
- Fix: セキュリティに関する修正
|
||||
|
||||
## 2026.6.0
|
||||
|
||||
|
||||
+10
@@ -30,6 +30,8 @@ COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
|
||||
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -61,6 +63,8 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
|
||||
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -99,12 +103,18 @@ COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/nod
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-reversi/node_modules ./packages/misskey-reversi/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/node_modules ./packages/misskey-world/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/node_modules ./packages/frontend-misskey-world-engine/node_modules
|
||||
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/built ./built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/built ./packages/misskey-js/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/built ./packages/misskey-world/built
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/built ./packages/frontend-misskey-world-engine/built
|
||||
|
||||
COPY --chown=misskey:misskey . ./
|
||||
|
||||
ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "Sortida"
|
||||
script: "Script"
|
||||
disablePagesScript: "Desactivar AiScript a les pàgines "
|
||||
updateRemoteUser: "Actualitzar la informació de l'usuari remot"
|
||||
unsetMfa: "Desactiva l'autenticació de dos factors"
|
||||
unsetMfaConfirm: "Voldries desactivar l'autenticació de dos factors?"
|
||||
unsetUserAvatar: "Desactiva l'avatar "
|
||||
unsetUserAvatarConfirm: "Segur que vols desactivar l'avatar?"
|
||||
unsetUserBanner: "Desactiva el bàner "
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "Afegeix al calaix d'emojis"
|
||||
emojiPaletteAlreadyAddedConfirm: "Aquest emoji ja està inclòs en aquest calaix d'emojis. Vols afegir-lo de nou?"
|
||||
append: "Afegeix al final"
|
||||
prepend: "Afegeix al principi"
|
||||
urlPreviewSensitiveList: "Llista d'URLs per restringir la visualització de miniatures"
|
||||
urlPreviewSensitiveListDescription: "Si separeu els termes amb un espai, s'interpretarà com una condició 'AND'; si els separeu amb un salt de línia, s'interpretarà com una condició 'OR'. Si envolupeu els termes amb barres obliqües, s'interpretarà com una expressió regular. Si es troba una coincidència, la miniatura ja no es mostrarà."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Títol de l'arxiu"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "Els resultats de la detecció interna seran desats, inclòs si aquesta opció es troba desactivada."
|
||||
analyzeVideos: "Activar anàlisis de vídeos "
|
||||
analyzeVideosDescription: "Analitzar els vídeos a més de les imatges. Això incrementarà lleugerament la càrrega del servidor."
|
||||
externalServiceInfo: "La detecció de mitjans sensibles s'ha delegat a un servei extern (sensitive-detector). Per utilitzar aquesta funció, cal configurar un servei sidecar separat i establir els detalls de connexió que es detallen a continuació. Si no es configuren els detalls de connexió, no es durà a terme cap detecció (el contingut es tractarà com a no sensible)."
|
||||
apiUrl: "URL per connectar-se al servei de verificació"
|
||||
apiUrlDescription: "L'URL base del servei de detector de sensibilitat (per exemple, http://localhost:3009). Si us connecteu a un servei en una xarxa privada, afegiu la xarxa de destinació a la configuració `allowedPrivateNetworks` del fitxer de configuració. Si utilitzeu un proxy, configureu també `proxyBypassHosts`. Si es deixa en blanc, no es realitzaran comprovacions de sensibilitat."
|
||||
apiKey: "Clau de l'API"
|
||||
apiKeyDescription: "Introduïu això si l'autenticació (token Bearer) està configurada al costat del servei d'autenticació. Si no està configurada, deixeu aquest camp en blanc."
|
||||
timeout: "Temps d'espera (mil·lisegons)"
|
||||
timeoutDescription: "1 Aquesta és la durada del temps mort per sol·licitud de validació."
|
||||
maxImagesPerRequest: "1 Nombre màxim d'imatges per sol·licitud"
|
||||
maxImagesPerRequestDescription: "1 Quan s'analitzen múltiples fotogrames, com en un vídeo, aquest és el nombre màxim d'imatges que es poden enviar en una única petició. Qualsevol imatge que superi aquest límit es dividirà en parts i s'enviarà seqüencialment. Assegureu-vos que aquest ajust no superi el valor de `maxParts` al costat del `sensitive-detector` (per defecte: 10). Si es supera aquest límit, totes les imatges d'aquest lot es consideraran no sensibles."
|
||||
_emailUnavailable:
|
||||
used: "Aquest correu electrònic ja s'està fent servir"
|
||||
format: "El format del correu electrònic és invàlid "
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "Veure registre de moderació "
|
||||
"read:admin:show-user": "Veure informació privada de l'usuari "
|
||||
"write:admin:suspend-user": "Suspendre usuari"
|
||||
"write:admin:unset-mfa": "Desactiva l'autenticació de dos factors per a un usuari"
|
||||
"write:admin:unset-user-avatar": "Esborrar avatar d'usuari "
|
||||
"write:admin:unset-user-banner": "Esborrar bàner de l'usuari "
|
||||
"write:admin:unsuspend-user": "Treure la suspensió d'un usuari"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "Decoració de l'avatar creada"
|
||||
updateAvatarDecoration: "S'ha actualitzat la decoració de l'avatar "
|
||||
deleteAvatarDecoration: "S'ha esborrat la decoració de l'avatar "
|
||||
unsetMfa: "Desactiva l'autenticació de dos factors per a l'usuari"
|
||||
unsetUserAvatar: "Esborrar l'avatar d'aquest usuari"
|
||||
unsetUserBanner: "Esborrar el bàner d'aquest usuari"
|
||||
createSystemWebhook: "Crear un SystemWebhook"
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "Output"
|
||||
script: "Script"
|
||||
disablePagesScript: "Disable AiScript on Pages"
|
||||
updateRemoteUser: "Update remote user information"
|
||||
unsetMfa: "Reset two-factor authentication"
|
||||
unsetMfaConfirm: "Are you sure you want to reset two-factor authentication?"
|
||||
unsetUserAvatar: "Unset avatar"
|
||||
unsetUserAvatarConfirm: "Are you sure you want to unset the avatar?"
|
||||
unsetUserBanner: "Unset banner"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "Add to emoji palette"
|
||||
emojiPaletteAlreadyAddedConfirm: "This emoji is already included in this emoji palette. Do you want to add it again?"
|
||||
append: "Append to end"
|
||||
prepend: "Append to beginning"
|
||||
urlPreviewSensitiveList: "URL to restrict thumbnail display"
|
||||
urlPreviewSensitiveListDescription: "Use spaces to specify AND conditions, and line breaks to specify OR conditions. Enclose text in slashes to use regular expressions. If a match is found, the thumbnail will be hidden."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "File caption"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "The results of the internal detection will be retained even if this option is turned off."
|
||||
analyzeVideos: "Enable analysis of videos"
|
||||
analyzeVideosDescription: "Analyzes videos in addition to images. This will slightly increase the load on the server."
|
||||
externalServiceInfo: "The detection of sensitive media has been offloaded to an external service (sensitive-detector). To use this feature, you must set up a separate service and configure the connection details provided below. If no connection details are configured, no detection will be performed (it will be treated as non-sensitive)."
|
||||
apiUrl: "Detection service endpoint URL"
|
||||
apiUrlDescription: "The base URL for the sensitive-detector service (e.g., http://localhost:3009). If you are connecting to a service on a private network, please allow the target network in the allowedPrivateNetworks setting in the configuration file. If you are using a proxy, please also configure proxyBypassHosts. If left blank, sensitive media detection will not be performed."
|
||||
apiKey: "API key"
|
||||
apiKeyDescription: "Enter this if authentication (Bearer token) is configured on the detector service. If it is not configured, please leave it blank."
|
||||
timeout: "Timeout (Milliseconds)"
|
||||
timeoutDescription: "Timeout duration for each judgment request."
|
||||
maxImagesPerRequest: "Max images per request"
|
||||
maxImagesPerRequestDescription: "Maximum number of images that can be sent in a single request when processing multi-frame at once, such as videos. Any images exceeding this limit will be split and sent sequentially. Please ensure this is set to not exceed the maxParts setting (default: 10) on the detector service. If it exceeds this limit, all items in that chunk will be treated as non-sensitive."
|
||||
_emailUnavailable:
|
||||
used: "This email address is already being used"
|
||||
format: "The format of this email address is invalid"
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "View moderation log"
|
||||
"read:admin:show-user": "View private user info"
|
||||
"write:admin:suspend-user": "Suspend user"
|
||||
"write:admin:unset-mfa": "Reset two-factor authentication for the user"
|
||||
"write:admin:unset-user-avatar": "Remove user avatar"
|
||||
"write:admin:unset-user-banner": "Remove user banner"
|
||||
"write:admin:unsuspend-user": "Unsuspend user"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "Avatar decoration created"
|
||||
updateAvatarDecoration: "Avatar decoration updated"
|
||||
deleteAvatarDecoration: "Avatar decoration deleted"
|
||||
unsetMfa: "Reset two-factor authentication for the user"
|
||||
unsetUserAvatar: "User avatar unset"
|
||||
unsetUserBanner: "User banner unset"
|
||||
createSystemWebhook: "System Webhook created"
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "Salida"
|
||||
script: "Script"
|
||||
disablePagesScript: "Deshabilitar AiScript en Páginas"
|
||||
updateRemoteUser: "Actualizar información de usuario remoto"
|
||||
unsetMfa: "Desactivar la autenticación de dos factores"
|
||||
unsetMfaConfirm: "¿Desea desactivar la autenticación de dos factores?"
|
||||
unsetUserAvatar: "Quitar avatar"
|
||||
unsetUserAvatarConfirm: "¿Confirmas que quieres quitar tu avatar?"
|
||||
unsetUserBanner: "Quitar banner"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "Añadir a la paleta de emojis"
|
||||
emojiPaletteAlreadyAddedConfirm: "Este emoji ya está incluido en esta paleta de emojis. ¿Quieres volver a añadirlo?"
|
||||
append: "Añadir al final"
|
||||
prepend: "Añadir al principio"
|
||||
urlPreviewSensitiveList: "URL para restringir la visualización de miniaturas"
|
||||
urlPreviewSensitiveListDescription: "Si se separan con un espacio, se interpretará como una condición «AND»; si se separan con un salto de línea, se interpretará como una condición «OR». Si se escriben entre barras, se interpretarán como expresiones regulares. Si se encuentra una coincidencia, no se mostrará la miniatura."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Título del archivo"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "Los resultados de la detección interna pueden ser retenidos incluso si la opción está desactivada."
|
||||
analyzeVideos: "Habilitar el análisis de videos"
|
||||
analyzeVideosDescription: "Analizar videos en adición a las imágenes. Esto puede incrementar ligeramente la carga del servidor."
|
||||
externalServiceInfo: "La detección de contenidos sensibles se ha externalizado a un servicio externo (sensitive-detector). Para utilizar esta función, es necesario configurar por separado un servicio «sidecar» y establecer los datos de conexión que se indican a continuación. Si no se configuran los datos de conexión, no se realizará la detección (se tratará como contenido no sensible)."
|
||||
apiUrl: "URL de conexión al servicio de verificación"
|
||||
apiUrlDescription: "URL base del servicio «sensitive-detector» (por ejemplo: http://localhost:3009). Si te conectas a un servicio situado en una red privada, debes permitir la red de destino en el parámetro «allowedPrivateNetworks» del archivo de configuración. Si utilizas un proxy, configura también el parámetro «proxyBypassHosts». Si se deja en blanco, no se realizará la evaluación de datos sensibles."
|
||||
apiKey: "Clave API"
|
||||
apiKeyDescription: "Introduce este dato si se ha configurado la autenticación (token Bearer) en el servicio de validación. Si no se ha configurado, déjalo en blanco."
|
||||
timeout: "Tiempo de espera (milisegundos)"
|
||||
timeoutDescription: "Duración del tiempo de espera para cada solicitud de resolución."
|
||||
maxImagesPerRequest: "Número máximo de imágenes por solicitud"
|
||||
maxImagesPerRequestDescription: "Es el número máximo de imágenes que se pueden enviar en una sola solicitud al analizar varios fotogramas, como en un vídeo. Las imágenes que superen este límite se dividirán y se enviarán de forma secuencial. Configúralo de manera que no se supere el valor de «maxParts» de sensitive-detector (por defecto: 10). Si se supera este límite, todas las imágenes de ese fragmento se considerarán no sensibles."
|
||||
_emailUnavailable:
|
||||
used: "Ya fue usado"
|
||||
format: "Formato no válido."
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "Ver log de moderación"
|
||||
"read:admin:show-user": "Ver información privada de usuario"
|
||||
"write:admin:suspend-user": "Suspender cuentas de usuario"
|
||||
"write:admin:unset-mfa": "Desactivar la autenticación de dos factores del usuario"
|
||||
"write:admin:unset-user-avatar": "Quitar avatares de usuario"
|
||||
"write:admin:unset-user-banner": "Quitar banner de usuarios"
|
||||
"write:admin:unsuspend-user": "Quitar suspensión de cuentas de usuario"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "Decoración de avatar creada"
|
||||
updateAvatarDecoration: "Decoración de avatar actualizada"
|
||||
deleteAvatarDecoration: "Decoración de avatar eliminada"
|
||||
unsetMfa: "Desactivar la autenticación de dos factores del usuario"
|
||||
unsetUserAvatar: "Quitar decoración de avatar de este usuario"
|
||||
unsetUserBanner: "Quitar banner de este usuario"
|
||||
createSystemWebhook: "Crear un SystemWebhook"
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "Output"
|
||||
script: "Script"
|
||||
disablePagesScript: "Disabilitare AiScript nelle pagine"
|
||||
updateRemoteUser: "Aggiorna dati dal profilo remoto"
|
||||
unsetMfa: "Rimuovere l'autenticazione a due fattori (2FA/MFA)"
|
||||
unsetMfaConfirm: "Vuoi davvero rimuovere l'autenticazione a due fattori?"
|
||||
unsetUserAvatar: "Rimozione foto profilo"
|
||||
unsetUserAvatarConfirm: "Vuoi davvero rimuovere la foto profilo?"
|
||||
unsetUserBanner: "Rimuovi intestazione profilo"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "Aggiungi alla tavolozza emoji"
|
||||
emojiPaletteAlreadyAddedConfirm: "Questa emoji è già inclusa in nella tavolozza. Vuoi davvero aggiungerla?"
|
||||
append: "Accodare"
|
||||
prepend: "Anteporre"
|
||||
urlPreviewSensitiveList: "URL da impedire alla vista delle anteprime"
|
||||
urlPreviewSensitiveListDescription: "Separando con uno spazio si indica E, separando con una linea si indica O. Circondando con barre / si indica una Espressione Regolare.\nLe URL che coincidono con le indicazioni non verranno visualizzate."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Didascalia dell'immagine"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "Anche se questa impostazione è disattivata, il risultato della decisione viene conservato internamente."
|
||||
analyzeVideos: "Abilitazione dell'analisi video."
|
||||
analyzeVideosDescription: "Assicuratevi che vengano analizzati anche i video oltre alle immagini fisse. Il carico del server aumenterà leggermente."
|
||||
externalServiceInfo: "Abbiamo spostato esternamente il riconoscimento di media espliciti (sensitive-detector). Per usufruirne devi impostare un servizio separato e indicare di seguito la destinazione. Se non è impostata, non viene emesso alcun giudizio (contenuto NON esplicito)."
|
||||
apiUrl: "URL di connessione a Sensitive-Detector"
|
||||
apiUrlDescription: "L'URL di base del servizio (ad esempio http://localhost:3009). Collegandosi a una rete privata, autorizzare la rete nel parametro allowedPrivateNetworks nel file di configurazione.\nCollegandosi con un proxy, è anche necessario impostare proxyBypassHosts. Nel caso il campo sia vuoto, non vengono emessi giudizi di sensibilità."
|
||||
apiKey: "Chiave API"
|
||||
apiKeyDescription: "Indicare il token di autenticazione (Bearer token), soltanto se occorre, altrimenti lasciare vuoto."
|
||||
timeout: "Timeout (ms)"
|
||||
timeoutDescription: "Tempo limite per ogni richiesta"
|
||||
maxImagesPerRequest: "Numero massimo di media per ogni richiesta"
|
||||
maxImagesPerRequestDescription: "In caso ci siano più fotogrammi, come nei video, questo è il numero massimo di immagini da inviare in una richiesta. Oltre questo numero, il totale sarà diviso e inviato in sequenza.\nImpostare in modo che non superi il valore maxParts (default: 10) in Sensitive-Detector. Se viene superato, il media sarà considerato non esplicito."
|
||||
_emailUnavailable:
|
||||
used: "Email già in uso"
|
||||
format: "Formato email non valido"
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "Vedere lo storico di moderazione"
|
||||
"read:admin:show-user": "Vedere le informazioni private dei profili"
|
||||
"write:admin:suspend-user": "Sospendere i profili"
|
||||
"write:admin:unset-mfa": "Può rimuovere l'autenticazione a due fattori (2FA/MFA)"
|
||||
"write:admin:unset-user-avatar": "Rimuovere la foto profilo dai profili"
|
||||
"write:admin:unset-user-banner": "Rimuovere l'immagine testata dai profili"
|
||||
"write:admin:unsuspend-user": "Rimuovere la sospensione ai profili"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "Crea una decorazione della foto profilo"
|
||||
updateAvatarDecoration: "Modifica una decorazione della foto profilo"
|
||||
deleteAvatarDecoration: "Elimina una decorazione della foto profilo"
|
||||
unsetMfa: "Rimossa l'autenticazione a due fattori (2FA7MFA)"
|
||||
unsetUserAvatar: "Toglie una foto profilo"
|
||||
unsetUserBanner: "Toglie una immagine di intestazione profilo"
|
||||
createSystemWebhook: "Aggiunge un System Webhook"
|
||||
|
||||
@@ -1414,6 +1414,8 @@ viewRenotedChannel: "リノート先のチャンネルを見る"
|
||||
previewingTheme: "テーマのプレビュー中"
|
||||
previewingThemeRestore: "元に戻す"
|
||||
accessToken: "アクセストークン"
|
||||
choose: "選択"
|
||||
rotate: "回転"
|
||||
chooseEmojiPalette: "絵文字パレットを選択"
|
||||
addToEmojiPalette: "絵文字パレットに追加"
|
||||
emojiPaletteAlreadyAddedConfirm: "この絵文字はすでにこの絵文字パレットに含まれています。追加しなおしますか?"
|
||||
@@ -3580,3 +3582,476 @@ _qr:
|
||||
scanFile: "端末の画像をスキャン"
|
||||
raw: "テキスト"
|
||||
mfm: "MFM"
|
||||
|
||||
worldAvatar: "Worldアバター"
|
||||
worldAvatar_description: "MisskeyWorld / MisskeyRoomsで使用可能な3Dアバターを作成できます。"
|
||||
|
||||
_miWorld:
|
||||
separateRenderingThread: "描画を別スレッドに分離"
|
||||
separateRenderingThread_description: "有効にするとパフォーマンスが向上します。不安定になる場合は無効すると改善する可能性があります。"
|
||||
graphicsQuality: "グラフィックの品質"
|
||||
graphicsSettings: "グラフィック設定"
|
||||
frameRateLimitation: "フレームレート制限"
|
||||
higherValuePerformanceNote: "高くすると体験が向上しますが、消費電力が増加するなどパフォーマンスに影響を与えます。"
|
||||
resolution: "解像度"
|
||||
fov: "視野角"
|
||||
failedToInitialize: "初期化に失敗しました"
|
||||
crushed_description: "描画が継続できなくなりました。デバイスのリソース不足の可能性が考えられます。"
|
||||
antialiasing: "アンチエイリアス"
|
||||
avatar: "アバター"
|
||||
advancedCustomize: "高度なアレンジ"
|
||||
attachAccessory: "アクセサリーをつける"
|
||||
takeScreenShot: "スクリーンショット"
|
||||
onlineMenu: "オンラインメニュー"
|
||||
connectToOnline: "オンラインに接続"
|
||||
disconnectToOnline: "オンラインから切断"
|
||||
character: "キャラクター"
|
||||
sit: "座る"
|
||||
lyingDown: "寝そべる"
|
||||
standUp: "立ち上がる"
|
||||
showUsernameOnAvatar: "アバターにユーザー名を表示"
|
||||
show2dAvatarOnAvatar: "アバターにユーザーアイコンを表示"
|
||||
|
||||
_avatars:
|
||||
_default:
|
||||
body: "ボディ"
|
||||
eyes: "目"
|
||||
mouth: "口"
|
||||
|
||||
_avatarAccessories:
|
||||
mug: "マグカップ"
|
||||
_mug:
|
||||
bodyMat: "コップの素材"
|
||||
liquidMat: "液体の素材"
|
||||
mikan: "みかん"
|
||||
bolt: "ボルト"
|
||||
_bolt:
|
||||
mat: "素材"
|
||||
|
||||
_miRoom:
|
||||
snapToGrid: "グリッドにスナップ"
|
||||
gridScale: "グリッドサイズ"
|
||||
thereAreUnsavedChanges: "未保存の変更があります"
|
||||
revertAllChangesConfirmation: "全ての変更を取り消し、部屋を最後に保存した状態まで戻しますか?"
|
||||
yourDeviceNotSupported_title: "MisskeyRoomを起動できません"
|
||||
yourDeviceNotSupported_description: "お使いのデバイスがMisskeyRoomをサポートしていないか、デバイスのリソース不足などにより一時的に利用できなくなっています。\nMisskeyRoomを動作させるには、WebGPUをサポートするデバイス・ブラウザが必要です。"
|
||||
imageFit: "画像のはめ込み"
|
||||
imageFit_cover: "覆う"
|
||||
imageFit_contain: "収める"
|
||||
imageFit_stretch: "伸縮"
|
||||
material_metallic: "光沢"
|
||||
material_roughness: "粗さ"
|
||||
light_brightness: "明るさ"
|
||||
advancedCustomize: "高度なアレンジ"
|
||||
enterEditMode: "エディットモードを始める"
|
||||
exitEditMode: "エディットモードを終了"
|
||||
installFurniture: "家具を設置"
|
||||
roomCustomize: "部屋のカスタマイズ"
|
||||
changeRoomName: "ルーム名を編集"
|
||||
roomInfo: "ルーム情報"
|
||||
duplicate: "複製"
|
||||
grab: "掴む"
|
||||
furnitureCustomize: "家具のアレンジ"
|
||||
uninstallFurniture: "しまう"
|
||||
furnituresCount: "家具の数"
|
||||
attachedFilesCount: "添付ファイル数"
|
||||
_furniturePlacement:
|
||||
top: "上面設置"
|
||||
bottom: "下面設置"
|
||||
side: "側面設置"
|
||||
|
||||
_furnitures:
|
||||
haniwa: "はにわ"
|
||||
_haniwa:
|
||||
bodyMat: "本体の素材"
|
||||
insideColor: "中身の色"
|
||||
woodRingFloorLamp: "リングシェードフロアランプ"
|
||||
_woodRingFloorLamp:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
a4Case: "A4ケース"
|
||||
_a4Case:
|
||||
mat: "素材"
|
||||
aircon: "エアコン"
|
||||
allInOnePc: "一体型PC"
|
||||
_allInOnePc:
|
||||
bezelMat: "ベゼルの素材"
|
||||
bodyMat: "本体の素材"
|
||||
image: "画面の画像"
|
||||
image_desktop: "デスクトップ"
|
||||
screenBrightness: "画面の明るさ"
|
||||
aquarium: "水槽"
|
||||
aromaReedDiffuser: "アロマリードディフューザー"
|
||||
_aromaReedDiffuser:
|
||||
bottleMat: "ボトルの素材"
|
||||
oilMat: "オイルの素材"
|
||||
banknote: "紙幣"
|
||||
beamLamp: "ビームランプ"
|
||||
bed: "ベッド"
|
||||
_bed:
|
||||
frameMat: "フレームの素材"
|
||||
blind: "ブラインド"
|
||||
_blind:
|
||||
angle: "羽根の回転角度"
|
||||
blades: "羽根の枚数"
|
||||
open: "開閉状態"
|
||||
book: "本"
|
||||
_book:
|
||||
height: "高さ"
|
||||
thickness: "厚み"
|
||||
variation: "バリエーション"
|
||||
width: "幅"
|
||||
books: "本の束"
|
||||
_books:
|
||||
variation: "バリエーション"
|
||||
boxWallShelf: "ボックス型ウォールシェルフ"
|
||||
_boxWallShelf:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
withBack: "背板"
|
||||
cactusS: "サボテン S"
|
||||
_cactusS:
|
||||
potMat: "鉢の素材"
|
||||
cardboardBox: "段ボール箱"
|
||||
_cardboardBox:
|
||||
variation: "種類"
|
||||
variation_aizon: "Aizon"
|
||||
variation_default: "デフォルト"
|
||||
variation_mikan: "みかん"
|
||||
ceilingFanLight: "シーリングファンライト"
|
||||
_ceilingFanLight:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
ceilingFan: "シーリングファン"
|
||||
_ceilingFan:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
chair: "椅子"
|
||||
_chair:
|
||||
primaryMat: "メインの素材"
|
||||
secondaryMat: "サブの素材"
|
||||
frameMat: "フレームの素材"
|
||||
clippedPicture: "留められた写真"
|
||||
_clippedPicture:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
coffeeCup: "コーヒーカップ"
|
||||
colorBox: "カラーボックス"
|
||||
_colorBox:
|
||||
mat: "素材"
|
||||
cuboid: "直方体"
|
||||
_cuboid:
|
||||
mat: "素材"
|
||||
x: "X"
|
||||
y: "Y"
|
||||
z: "Z"
|
||||
cupNoodle: "インスタントラーメン"
|
||||
curtain: "カーテン"
|
||||
custardPudding: "プリン"
|
||||
descriptionPlate: "説明が書かれたプレート"
|
||||
desk: "デスク"
|
||||
_desk:
|
||||
boardMat: "天板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
width: "幅"
|
||||
desktopPc: "デスクトップPC"
|
||||
_desktopPc:
|
||||
bodyMat: "本体の素材"
|
||||
coverMat: "カバーの素材"
|
||||
inner1Mat: "内部素材1"
|
||||
inner2Mat: "内部素材2"
|
||||
inner3Mat: "内部素材3"
|
||||
ledColor: "LEDの色"
|
||||
djMixer: "DJミキサー"
|
||||
djPlayer: "DJプレーヤー"
|
||||
_djPlayer:
|
||||
image: "画像"
|
||||
"image:waveform": "波形"
|
||||
screenBrightness: "画面の明るさ"
|
||||
ductRailSpotLights: "スポットライト付きダクトレール"
|
||||
_ductRailSpotLights:
|
||||
angleH: "水平角度"
|
||||
angleV: "垂直角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
ductTape: "ガムテープ"
|
||||
herbarium: "ハーバリウム"
|
||||
electronicDisplayBoard: "電光掲示板"
|
||||
_electronicDisplayBoard:
|
||||
frameMat: "フレームの素材"
|
||||
ledBrightness: "LEDの明るさ"
|
||||
ledColor: "LEDの色"
|
||||
text: "テキスト"
|
||||
emptyBento: "空の弁当容器"
|
||||
energyDrink: "エナジードリンク"
|
||||
envelope: "封筒"
|
||||
facialTissue: "ティッシュ"
|
||||
glassCylinderPotPlant: "ガラスシリンダーの鉢植えと植物"
|
||||
handheldGameConsole: "携帯ゲーム機"
|
||||
_handheldGameConsole:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
hangingDuctRail: "吊り下げダクトレール"
|
||||
_hangingDuctRail:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
hangingTShirt: "吊り下げTシャツ"
|
||||
icosahedron: "正二十面体のオブジェ"
|
||||
_icosahedron:
|
||||
mat: "素材"
|
||||
ironFrameTable: "アイアンフレームテーブル"
|
||||
_ironFrameTable:
|
||||
boardMat: "天板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
issyoubin: "一升瓶"
|
||||
_issyoubin:
|
||||
variation: "種類"
|
||||
keyboard: "キーボード"
|
||||
_keyboard:
|
||||
bodyMat: "本体の素材"
|
||||
keyMat: "キーの素材"
|
||||
laptopPc: "ノートPC"
|
||||
_laptopPc:
|
||||
bezelMat: "ベゼルの素材"
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
openAngle: "開き具合"
|
||||
screenBrightness: "画面の明るさ"
|
||||
largeMousepad: "大きいマウスパッド"
|
||||
_largeMousepad:
|
||||
image: "画像"
|
||||
lavaLamp: "ラバランプ"
|
||||
_lavaLamp:
|
||||
bodyMat: "本体の素材"
|
||||
glassMat: "ガラスの素材"
|
||||
lavaColor: "オイルの色"
|
||||
lightColor: "ランプの色"
|
||||
letterCase: "レターケース"
|
||||
lowPartitionBar: "低いパーティションバー"
|
||||
_lowPartitionBar:
|
||||
bodyMat: "本体の素材"
|
||||
width: "幅"
|
||||
miObjet: "Miオブジェ"
|
||||
miPlate: "Miプレート"
|
||||
miPlateDisplayed: "飾られたMiプレート"
|
||||
milk: "牛乳"
|
||||
mixer: "ミキサー"
|
||||
monitor: "モニター"
|
||||
_monitor:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
monitorSpeaker: "モニタースピーカー"
|
||||
_monitorSpeaker:
|
||||
mat: "素材"
|
||||
monstera: "モンステラ"
|
||||
_monstera:
|
||||
potMat: "鉢の素材"
|
||||
mug: "マグカップ"
|
||||
_mug:
|
||||
bodyMat: "コップの素材"
|
||||
liquidMat: "液体の素材"
|
||||
newtonsCradle: "ニュートンクレードル"
|
||||
_newtonsCradle:
|
||||
frameMat: "フレームの素材"
|
||||
openedCardboardBox: "開いた段ボール箱"
|
||||
pachira: "パキラ"
|
||||
_pachira:
|
||||
potMat: "鉢の素材"
|
||||
petBottle: "ペットボトル"
|
||||
_petBottle:
|
||||
empty: "空"
|
||||
variation: "種類"
|
||||
variation_greenTea: "緑茶"
|
||||
variation_mineralWater: "ミネラルウォーター"
|
||||
withCap: "キャップ"
|
||||
withLabel: "ラベル"
|
||||
piano: "ピアノ"
|
||||
_piano:
|
||||
bodyMat: "本体の素材"
|
||||
pictureFrame: "シンプルな額縁"
|
||||
_pictureFrame:
|
||||
depth: "厚さ"
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの幅"
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
matHThickness: "マットの横幅"
|
||||
matVThickness: "マットの縦幅"
|
||||
width: "幅"
|
||||
withCover: "カバーあり"
|
||||
pizza: "ピザ"
|
||||
plant: "植物"
|
||||
plant2: "植物 2"
|
||||
poster: "ポスター"
|
||||
_poster:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
powerStrip: "電源タップ"
|
||||
radiometer: "ラジオメーター"
|
||||
randomBooks: "雑多な本"
|
||||
_randomBooks:
|
||||
count: "数"
|
||||
seed: "シード"
|
||||
stackVertically: "平積み"
|
||||
variation: "バリエーション"
|
||||
variation_mix: "いろいろ"
|
||||
variation_mixPlain: "いろいろ(無地)"
|
||||
recordPlayer: "レコードプレーヤー"
|
||||
rolledUpPoster: "丸めたポスター"
|
||||
roundRug: "円形のラグ"
|
||||
router: "ルーター"
|
||||
siphon: "サイフォン"
|
||||
snakeplant: "サンセベリア"
|
||||
_snakeplant:
|
||||
potMat: "鉢の素材"
|
||||
sofa: "ソファ"
|
||||
_sofa:
|
||||
bodyMat: "本体の素材"
|
||||
speaker: "スピーカー"
|
||||
_speaker:
|
||||
innerMat: "内側の素材"
|
||||
outerMat: "外側の素材"
|
||||
speakerStand: "スピーカースタンド"
|
||||
_speakerStand:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
spotLight: "スポットライト"
|
||||
_spotLight:
|
||||
angleH: "横方向の角度"
|
||||
angleV: "縦方向の角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
downlight: "ダウンライト"
|
||||
_downlight:
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
sprayer: "霧吹き"
|
||||
stanchionPole: "スタンションポール"
|
||||
_stanchionPole:
|
||||
bodyMat: "本体の素材"
|
||||
ropeMat: "ロープの素材"
|
||||
steelRack: "スチールラック"
|
||||
_steelRack:
|
||||
height: "高さ"
|
||||
numberOfShelfs: "シェルフの数"
|
||||
poleMat: "ポールの素材"
|
||||
shelfPositionOf: "シェルフの位置"
|
||||
shelfMat: "シェルフの素材"
|
||||
widthAndDepthVariation: "W x D"
|
||||
stormGlass: "ストームグラス"
|
||||
tableSalt: "食卓塩"
|
||||
tabletopCalendar: "卓上カレンダー"
|
||||
tabletopDigitalClock: "卓上デジタル時計"
|
||||
_tabletopDigitalClock:
|
||||
bodyMat: "本体の素材"
|
||||
lcdColor: "LCDの色"
|
||||
tabletopFlag: "卓上旗"
|
||||
_tabletopFlag:
|
||||
image: "画像"
|
||||
tabletopGlassPictureFrame: "卓上ガラス製フォトフレーム"
|
||||
_tabletopGlassPictureFrame:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
tabletopIronFrameStand: "卓上アイアンフレームスタンド"
|
||||
_tabletopIronFrameStand:
|
||||
boardMat: "板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
tabletopLcdButtonsController: "LCDボタン付き卓上コントローラー"
|
||||
_tabletopLcdButtonsController:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
tabletopPictureFrame: "卓上フォトフレーム"
|
||||
_tabletopPictureFrame:
|
||||
depth: "厚さ"
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの幅"
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
matHThickness: "マットの横幅"
|
||||
matVThickness: "マットの縦幅"
|
||||
width: "幅"
|
||||
tapestry: "タペストリー"
|
||||
_tapestry:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
tetrapod: "波消ブロックの置物"
|
||||
tv: "テレビ"
|
||||
_tv:
|
||||
bodyMat: "本体の素材"
|
||||
screenBrightness: "画面の明るさ"
|
||||
twistedCubeObjet: "ねじれた立方体のオブジェ"
|
||||
usedTissue: "使用済みティッシュ"
|
||||
wallCanvas: "壁掛けキャンバス"
|
||||
_wallCanvas:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
wallClock: "壁掛け時計"
|
||||
_wallClock:
|
||||
frameMat: "フレームの素材"
|
||||
faceMat: "文字盤の素材"
|
||||
handsMat: "針の素材"
|
||||
wallGlassPictureFrame: "ガラスの壁掛けフォトフレーム"
|
||||
_wallGlassPictureFrame:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
wallMirror: "壁掛けミラー"
|
||||
_wallMirror:
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの厚み"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
wallMountSpotLight: "ウォールマウントスポットライト"
|
||||
_wallMountSpotLight:
|
||||
angleH: "横方向の角度"
|
||||
angleV: "縦方向の角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
wallShelf: "ウォールシェルフ"
|
||||
_wallShelf:
|
||||
boardMat: "板の素材"
|
||||
boardStyle: "板のスタイル"
|
||||
"boardStyle:color": "単色"
|
||||
"boardStyle:wood": "木目"
|
||||
style: "スタイル"
|
||||
wireBasket: "ワイヤーバスケット"
|
||||
_wireBasket:
|
||||
bodyMat: "本体の素材"
|
||||
wireNet: "ワイヤーネット"
|
||||
_wireNet:
|
||||
bodyMat: "本体の素材"
|
||||
woodRingsPendantLight: "リングペンダントライト"
|
||||
_woodRingsPendantLight:
|
||||
bodyMat: "本体の素材"
|
||||
length: "長さ"
|
||||
light: "照明"
|
||||
shadeMat: "シェードの素材"
|
||||
woodSoundAbsorbingPanel: "木製吸音パネル"
|
||||
ironFrameShelf: "アイアンフレームシェルフ"
|
||||
_ironFrameShelf:
|
||||
boardMat: "板の素材"
|
||||
frameMat: "フレームの素材"
|
||||
height: "段数"
|
||||
width: "幅"
|
||||
kakejiku: "掛軸"
|
||||
_kakejiku:
|
||||
image: "画像"
|
||||
"image:kitsuaishinsei": "喫藍心整"
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "출력"
|
||||
script: "스크립트"
|
||||
disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음"
|
||||
updateRemoteUser: "리모트 유저 정보 갱신"
|
||||
unsetMfa: "2단계 인증 해제"
|
||||
unsetMfaConfirm: "2단계 인증을 해제하시겠습니까?"
|
||||
unsetUserAvatar: "아바타 제거"
|
||||
unsetUserAvatarConfirm: "아바타를 제거할까요?"
|
||||
unsetUserBanner: "배너 제거"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "이모지 팔레트에 추가"
|
||||
emojiPaletteAlreadyAddedConfirm: "이 이모지는 이미 이 이모지 팔레트에 포함돼있습니다. 다시 추가하시겠습니까?"
|
||||
append: "맨뒤에 추가"
|
||||
prepend: "맨앞에 추가"
|
||||
urlPreviewSensitiveList: "썸네일 표시 제한 URL"
|
||||
urlPreviewSensitiveListDescription: "공백으로 구분하면 AND 지정으로 되고, 줄내림으로 구분하면 OR 지정으로 됩니다. 슬래시로 감싸면 정규 표현으로 됩니다. 일치한 경우에 썸네일이 표시되지 않게 됩니다."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "파일 설명"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "이 설정을 해제해도 탐지 결과는 유지됩니다."
|
||||
analyzeVideos: "동영상도 같이 확인하기"
|
||||
analyzeVideosDescription: "사진 뿐만 아니라 동영상의 NSFW 여부도 탐지합니다. 서버의 부하를 약간 증가시킵니다."
|
||||
externalServiceInfo: "민감한 미디어 판정은 외부 서비스(sensitive-detector)로 분리됐습니다. 이 기능을 이용하려면 별도 사이드카 서비스를 설정하고, 아래의 접속 위치를 설정해야 합니다. 접속 위치가 설정되지 않은 경우에는 판정이 이루어지지 않습니다. (민감하지 않음 처리)"
|
||||
apiUrl: "판정 서비스의 접속 위치 URL"
|
||||
apiUrlDescription: "sensitive-detector 서비스의 베이스 URL(예시: http://localhost:3009). 프라이빗 네트워크상의 서비스에 접속하는 경우에는 설정 파일의 allowedPrivateNetworks로 접속 위치 네트워크를 허가해 주십시오. 프록시를 사용하고 있는 경우에는 proxyBypassHosts도 설정해 주십시오. 비어있으면 민감함 판정은 이루어지지 않습니다."
|
||||
apiKey: "API 키"
|
||||
apiKeyDescription: "판정 서비스 측에서 인증(Bearer 토큰)을 설정하고 있는 경우에 입력합니다. 설정하고 있지 않은 경우에는 빈칸으로 둬주십시오."
|
||||
timeout: "타임아웃 (밀리초)"
|
||||
timeoutDescription: "판정 요청 1회당 타임아웃 시간입니다."
|
||||
maxImagesPerRequest: "한 요청당 최대 이미지 수"
|
||||
maxImagesPerRequestDescription: "동영상 등 여러 프레임을 판정할 때, 한 번의 요청에 모아서 보내는 이미지의 최대 장수입니다. 이를 넘으면 분할해 순차적으로 송신됩니다. sensitive-detector 측의 maxParts 설정(기본: 10)을 남지 않도록 설정해 주십시오. 넘은 경우에는 그 청크는 전부 민감하지 않음 처리로 됩니다."
|
||||
_emailUnavailable:
|
||||
used: "이 메일 주소는 사용중입니다"
|
||||
format: "형식이 올바르지 않습니다"
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "조정 기록 보기"
|
||||
"read:admin:show-user": "유저 개인정보 보기"
|
||||
"write:admin:suspend-user": "유저 정지하기"
|
||||
"write:admin:unset-mfa": "사용자의 2단계 인증 해제"
|
||||
"write:admin:unset-user-avatar": "유저 아바타 삭제하기"
|
||||
"write:admin:unset-user-banner": "유저 배너 삭제하기"
|
||||
"write:admin:unsuspend-user": "유저 정지 해제하기"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "아바타 장식 만들기"
|
||||
updateAvatarDecoration: "아바타 장식 수정"
|
||||
deleteAvatarDecoration: "아바타 장식 삭제"
|
||||
unsetMfa: "사용자의 2단계 인증 해제"
|
||||
unsetUserAvatar: "유저 아바타 제거"
|
||||
unsetUserBanner: "유저 배너 제거"
|
||||
createSystemWebhook: "SystemWebhook을 생성"
|
||||
|
||||
+20
-5
@@ -57,7 +57,7 @@ deleteAndEditConfirm: "要删除该帖并重新编辑吗?该帖下的所有回
|
||||
addToList: "添加至列表"
|
||||
addToAntenna: "添加到天线"
|
||||
sendMessage: "发送消息"
|
||||
copyRSS: "复制RSS"
|
||||
copyRSS: "复制 RSS"
|
||||
copyUsername: "复制用户名"
|
||||
copyUserId: "复制用户 ID"
|
||||
copyNoteId: "复制帖子 ID"
|
||||
@@ -456,8 +456,8 @@ about: "关于"
|
||||
aboutMisskey: "关于 Misskey"
|
||||
administrator: "管理员"
|
||||
token: "Token (令牌)"
|
||||
2fa: "双因素认证"
|
||||
setupOf2fa: "设置双因素认证"
|
||||
2fa: "双重验证"
|
||||
setupOf2fa: "设置双重验证"
|
||||
totp: "验证器"
|
||||
totpDescription: "使用验证器输入一次性密码"
|
||||
moderator: "监察员"
|
||||
@@ -485,7 +485,7 @@ markAsReadAllNotifications: "将所有通知标为已读"
|
||||
markAsReadAllUnreadNotes: "将所有帖子标记为已读"
|
||||
markAsReadAllTalkMessages: "将所有私信标记为已读"
|
||||
help: "帮助"
|
||||
inputMessageHere: "在此键入信息"
|
||||
inputMessageHere: "在此输入信息"
|
||||
close: "关闭"
|
||||
invites: "邀请"
|
||||
members: "成员"
|
||||
@@ -619,6 +619,8 @@ output: "输出"
|
||||
script: "脚本"
|
||||
disablePagesScript: "禁用页面脚本"
|
||||
updateRemoteUser: "更新远程用户信息"
|
||||
unsetMfa: "解除双重验证"
|
||||
unsetMfaConfirm: "确认解除双重验证吗?"
|
||||
unsetUserAvatar: "清除头像"
|
||||
unsetUserAvatarConfirm: "要清除头像吗?"
|
||||
unsetUserBanner: "清除横幅"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "添加至表情符号选择器"
|
||||
emojiPaletteAlreadyAddedConfirm: "此表情符号已存在于此表情符号选择器中。要再次添加吗?"
|
||||
append: "加到最后"
|
||||
prepend: "加到最前"
|
||||
urlPreviewSensitiveList: "限制显示缩略图的 URL"
|
||||
urlPreviewSensitiveListDescription: "AND 条件用空格分隔,OR 条件用换行符分隔,正则表达式用斜线包裹。成功匹配则不再显示缩略图。"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "文件标题"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "即使关闭此配置,识别结果也会在内部保存。"
|
||||
analyzeVideos: "启用对视频的检测"
|
||||
analyzeVideosDescription: "除了静止图像之外,还对视频进行分析。服务器负载会略微增加。"
|
||||
externalServiceInfo: "检测敏感媒体已分离至外部服务 (sensitive-detector)。若要使用,需额外部署 Sidecar 服务,并设置下方的连接 URL。未设定时将不会进行检测(视为非敏感媒体)。"
|
||||
apiUrl: "检测服务的连接 URL"
|
||||
apiUrlDescription: "sensitive-detector 服务的 base URL(如:http://localhost:3009)。若是连接至部署在专用网络上的服务,请在配置文件中的 allowedPrivateNetworks 里允许目标网络。若是使用了代理,请一并设置 proxyBypassHosts。留空则不进行敏感媒体检测。"
|
||||
apiKey: "API 密钥"
|
||||
apiKeyDescription: "若服务端有设置验证(Bearer token)则填写,未设置则留空。"
|
||||
timeout: "超时(毫秒)"
|
||||
timeoutDescription: "此为单次检测请求的超时时长。"
|
||||
maxImagesPerRequest: "单次检测请求最大图像数量"
|
||||
maxImagesPerRequestDescription: "此为在检测动画等多帧图像时,单次请求中可发送的图像数量上限。超出此值时动画将被拆分并按序发送。请勿将此值设为超出 sensitive-detector 侧的 maxParts 的值(默认:10),否则对应的分块将全被视为非敏感媒体。"
|
||||
_emailUnavailable:
|
||||
used: "已经被使用过"
|
||||
format: "无效的格式"
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "查看管理日志"
|
||||
"read:admin:show-user": "查看用户的非公开信息"
|
||||
"write:admin:suspend-user": "冻结用户"
|
||||
"write:admin:unset-mfa": "解除用户的双重验证"
|
||||
"write:admin:unset-user-avatar": "删除用户头像"
|
||||
"write:admin:unset-user-banner": "删除用户横幅"
|
||||
"write:admin:unsuspend-user": "解除用户冻结"
|
||||
@@ -2586,7 +2600,7 @@ _widgetOptions:
|
||||
_jobQueue:
|
||||
sound: "播放音效"
|
||||
_rss:
|
||||
url: "RSS feed 的 URL"
|
||||
url: "RSS 订阅源网址"
|
||||
refreshIntervalSec: "更新间隔(秒)"
|
||||
maxEntries: "最大显示个数"
|
||||
_rssTicker:
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "新建头像挂件"
|
||||
updateAvatarDecoration: "更新头像挂件"
|
||||
deleteAvatarDecoration: "删除头像挂件"
|
||||
unsetMfa: "解除用户的双重验证"
|
||||
unsetUserAvatar: "清除用户头像"
|
||||
unsetUserBanner: "清除用户横幅"
|
||||
createSystemWebhook: "新建了 SystemWebhook"
|
||||
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.7.0-beta.4",
|
||||
"version": "2026.7.0-rc.0",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -20,6 +20,8 @@
|
||||
"packages/misskey-js/generator",
|
||||
"packages/misskey-reversi",
|
||||
"packages/misskey-bubble-game",
|
||||
"packages/misskey-world",
|
||||
"packages/frontend-misskey-world-engine",
|
||||
"packages-private/*"
|
||||
],
|
||||
"private": true,
|
||||
@@ -57,8 +59,8 @@
|
||||
"esbuild": "0.28.1",
|
||||
"execa": "9.6.1",
|
||||
"ignore-walk": "9.0.0",
|
||||
"js-yaml": "5.2.1",
|
||||
"tar": "7.5.20"
|
||||
"js-yaml": "5.2.2",
|
||||
"tar": "7.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.5",
|
||||
|
||||
@@ -64,7 +64,7 @@ test('throws when filtering leaves fewer than two heap snapshots per side', asyn
|
||||
})).toThrow('At least two samples per side are required');
|
||||
});
|
||||
|
||||
test('reports the difference of medians and leaves a paired-looking PSS delta uncoloured', async () => {
|
||||
test('omits a paired-looking PSS delta that stays within observed noise', async () => {
|
||||
const base = replacePssSamples(await loadFixture('base'), [290_000, 292_900, 295_800, 298_700, 301_600]);
|
||||
const head = replacePssSamples(await loadFixture('head'), [292_900, 296_300, 298_700, 301_600, 290_000]);
|
||||
|
||||
@@ -78,16 +78,10 @@ test('reports the difference of medians and leaves a paired-looking PSS delta un
|
||||
baseHeapSnapshotUrl: 'https://example.invalid/base',
|
||||
headHeapSnapshotUrl: 'https://example.invalid/head',
|
||||
});
|
||||
const row = findMetricRow(markdown, 'PSS');
|
||||
|
||||
expect(row).toContain('295.8 MB <br> ± 2.9 MB');
|
||||
expect(row).toContain('296.3 MB <br> ± 3.4 MB');
|
||||
expect(row).toContain('$\\text{+0.5 MB}$');
|
||||
expect(row).toContain('4.5 MB');
|
||||
expect(row.split('|')).toHaveLength(7);
|
||||
expect(row).not.toMatch(/within noise|increase|decrease|inconclusive/);
|
||||
expect(row).not.toContain('\\color{orange}');
|
||||
expect(findMetricRow(markdown, 'USS')).not.toContain('\\color{orange}');
|
||||
expect(markdown).not.toContain('| **PSS** |');
|
||||
expect(markdown).toContain('<small><i>Only metrics showing significant changes are displayed.</i></small>');
|
||||
expect(markdown).not.toContain('⚠️ **Warning**: Memory usage (PSS)');
|
||||
});
|
||||
|
||||
test('keeps the convergence warning without suppressing table colour or the PSS warning', async () => {
|
||||
|
||||
@@ -251,7 +251,7 @@ test('renders a directional encoded byte delta at the absolute threshold', async
|
||||
|
||||
const row = requireMetricRow(await renderReport(null, { base, head }), 'Encoded network');
|
||||
|
||||
expect(row).toContain('$\\color{orange}{\\text{+10 KB}}$');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+10 KB}}$');
|
||||
});
|
||||
|
||||
test('colours absolute and relative deltas together when the row is significant', async () => {
|
||||
@@ -268,9 +268,9 @@ test('colours absolute and relative deltas together when the row is significant'
|
||||
|
||||
const row = requireMetricRow(await renderReport(null, { base, head }), 'Encoded network');
|
||||
|
||||
expect(row).toContain('100 MB <br> ± 0 B');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+20 KB}}$<br>$\\color{orange}{\\text{+0\\\\%}}$');
|
||||
expect(row).toContain('| 0 B |');
|
||||
expect(row).toContain('100 MB <br> ± 0 B');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+20 KB}}$<br>$\\color{orange}{\\text{+0\\\\%}}$');
|
||||
expect(row).toContain('| 0 B |');
|
||||
expect(row.split('|')).toHaveLength(7);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,15 +20,15 @@ import {
|
||||
describe('formatBytes', () => {
|
||||
// 1024ではなく1000区切り。単位が2桁以上なら小数を落とす
|
||||
test.each([
|
||||
[0, '0 B'],
|
||||
[999, '999 B'],
|
||||
[1_000, '1 KB'],
|
||||
[1_500, '1.5 KB'],
|
||||
[15_000, '15 KB'],
|
||||
[1_234_567, '1.2 MB'],
|
||||
[12_345_678, '12 MB'],
|
||||
[1_500_000_000, '1.5 GB'],
|
||||
[1_500_000_000_000, '1,500 GB'],
|
||||
[0, '0 B'],
|
||||
[999, '999 B'],
|
||||
[1_000, '1 KB'],
|
||||
[1_500, '1.5 KB'],
|
||||
[15_000, '15 KB'],
|
||||
[1_234_567, '1.2 MB'],
|
||||
[12_345_678, '12 MB'],
|
||||
[1_500_000_000, '1.5 GB'],
|
||||
[1_500_000_000_000, '1,500 GB'],
|
||||
])('formats %i as %s', (input, expected) => {
|
||||
expect(formatBytes(input)).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('renderHeapSnapshotTable', () => {
|
||||
report([1_100_000, 1_200_000, 1_300_000]),
|
||||
));
|
||||
|
||||
expect(row).toContain('$\\text{+100 KB}$');
|
||||
expect(row).toContain('$\\text{+100 KB}$');
|
||||
expect(row).not.toContain('within noise');
|
||||
expect(row).not.toContain('\\color{orange}');
|
||||
});
|
||||
@@ -56,7 +56,7 @@ describe('renderHeapSnapshotTable', () => {
|
||||
report([1_200_000, 1_200_000, 1_200_000]),
|
||||
));
|
||||
|
||||
expect(row).toContain('$\\color{orange}{\\text{+200 KB}}$');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+200 KB}}$');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+20\\\\%}}$');
|
||||
expect(row).not.toContain('increase');
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe('renderHeapSnapshotTable', () => {
|
||||
report([1_050_000, 1_050_000, 1_050_000]),
|
||||
));
|
||||
|
||||
expect(row).toContain('$\\text{+50 KB}$<br>$\\text{+5\\\\%}$');
|
||||
expect(row).toContain('$\\text{+50 KB}$<br>$\\text{+5\\\\%}$');
|
||||
expect(row.split('|')[4]).not.toContain('\\color{');
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
expect(table.split('\n')).toStrictEqual([
|
||||
'| Metric | @ Base | @ Head | Δ | MAD |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
'| <strong>Metric</strong> | 100 units <br> ± 0 units | 120 units <br> ± 0 units | $\\color{orange}{\\text{+20 units}}$<br>$\\color{orange}{\\text{+20\\\\%}}$ | 0 units |',
|
||||
'| <strong>Metric</strong> | 100 units <br> ± 0 units | 120 units <br> ± 0 units | $\\color{orange}{\\text{+20~units}}$<br>$\\color{orange}{\\text{+20\\\\%}}$ | 0 units |',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
expect(table.split('\n')).toStrictEqual([
|
||||
'| Metric | @ Base | @ Head | Δ | MAD |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
'| <strong>Metric</strong> | 100 units | 120 units | $\\color{orange}{\\text{+20 units}}$ | 0 units |',
|
||||
'| <strong>Metric</strong> | 100 units | 120 units | $\\color{orange}{\\text{+20~units}}$ | 0 units |',
|
||||
'| | | | | |',
|
||||
]);
|
||||
});
|
||||
@@ -62,7 +62,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
const head = samples(109, 109, 109);
|
||||
const table = renderMetricComparisonTable(base, head, [defaultRow]);
|
||||
|
||||
expect(table).toContain('$\\text{+9 units}$<br>$\\text{+9\\\\%}$');
|
||||
expect(table).toContain('$\\text{+9~units}$<br>$\\text{+9\\\\%}$');
|
||||
expect(table).not.toContain('\\color{');
|
||||
expect(renderMetricComparisonTable(base, head, [defaultRow], {
|
||||
onlySignificantChanges: true,
|
||||
@@ -85,7 +85,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
{ onlySignificantChanges: true },
|
||||
);
|
||||
|
||||
expect(table).toContain('$\\color{orange}{\\text{+10 units}}$');
|
||||
expect(table).toContain('$\\color{orange}{\\text{+10~units}}$');
|
||||
expect(table).toContain('$\\color{orange}{\\text{+10\\\\%}}$');
|
||||
});
|
||||
|
||||
@@ -106,11 +106,11 @@ describe('renderMetricComparisonTable', () => {
|
||||
[defaultRow],
|
||||
);
|
||||
|
||||
expect(inside).toContain('$\\text{+9 units}$');
|
||||
expect(inside).toContain('$\\text{+9~units}$');
|
||||
expect(inside).not.toContain('\\color{');
|
||||
expect(boundary).toContain('$\\text{+30 units}$');
|
||||
expect(boundary).toContain('$\\text{+30~units}$');
|
||||
expect(boundary).not.toContain('\\color{');
|
||||
expect(outside).toContain('$\\color{orange}{\\text{+31 units}}$');
|
||||
expect(outside).toContain('$\\color{orange}{\\text{+31~units}}$');
|
||||
expect(outside).toContain('$\\color{orange}{\\text{+31\\\\%}}$');
|
||||
});
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
[defaultRow],
|
||||
);
|
||||
|
||||
expect(table).toContain('$\\color{green}{\\text{-20 units}}$');
|
||||
expect(table).toContain('$\\color{green}{\\text{-20~units}}$');
|
||||
expect(table).toContain('$\\color{green}{\\text{-16.7\\\\%}}$');
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
[defaultRow],
|
||||
);
|
||||
|
||||
expect(table).toContain('$\\color{orange}{\\text{+20 units}}$<br>-');
|
||||
expect(table).toContain('$\\color{orange}{\\text{+20~units}}$<br>-');
|
||||
});
|
||||
|
||||
test('propagates the minimum sample contract', () => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class WorldRoom1778744540138 {
|
||||
name = 'WorldRoom1778744540138'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "world_room" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(1024) NOT NULL, "userId" character varying(32) NOT NULL, "likedCount" integer NOT NULL DEFAULT '0', "visibility" character varying(128) NOT NULL DEFAULT 'public', "def" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_40cfacaf35b0b54bb2281c89767" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_88289375952050da4a7752a366" ON "world_room" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f803a5efb4125c5fd8a414285e" ON "world_room" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" ADD CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "world_room" DROP CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f803a5efb4125c5fd8a414285e"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_88289375952050da4a7752a366"`);
|
||||
await queryRunner.query(`DROP TABLE "world_room"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class WorldAvatar1779921322355 {
|
||||
name = 'WorldAvatar1779921322355'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "world_avatar" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "def" jsonb NOT NULL DEFAULT '{}', "active" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_e7a27262285cc2c27114871f866" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0f1d0bdfaca455cc2f13defabe" ON "world_avatar" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_4eba43c8e2540a92e99dd7f5a9" ON "world_avatar" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" ADD "accessCount" integer NOT NULL DEFAULT '0'`);
|
||||
await queryRunner.query(`ALTER TABLE "world_avatar" ADD CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "world_avatar" DROP CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a"`);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" DROP COLUMN "accessCount"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_4eba43c8e2540a92e99dd7f5a9"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_0f1d0bdfaca455cc2f13defabe"`);
|
||||
await queryRunner.query(`DROP TABLE "world_avatar"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class HashtagTableDefaults1784899839024 {
|
||||
name = 'HashtagTableDefaults1784899839024'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "mentionedUserIds" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "mentionedLocalUserIds" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "mentionedRemoteUserIds" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "attachedUserIds" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "attachedLocalUserIds" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "attachedRemoteUserIds" SET DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "attachedRemoteUserIds" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "attachedLocalUserIds" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "attachedUserIds" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "mentionedRemoteUserIds" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "mentionedLocalUserIds" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "hashtag" ALTER COLUMN "mentionedUserIds" DROP DEFAULT`);
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,7 @@
|
||||
"@fastify/cors": "11.3.0",
|
||||
"@fastify/http-proxy": "11.5.0",
|
||||
"@fastify/multipart": "10.1.0",
|
||||
"@fastify/otel": "0.20.1",
|
||||
"@fastify/static": "10.1.0",
|
||||
"@fastify/static": "10.1.2",
|
||||
"@kitajs/html": "4.2.13",
|
||||
"@misskey-dev/emoji-assets": "17.0.3",
|
||||
"@misskey-dev/emoji-data": "17.0.3",
|
||||
@@ -68,15 +67,6 @@
|
||||
"@nestjs/common": "11.1.28",
|
||||
"@nestjs/core": "11.1.28",
|
||||
"@nestjs/testing": "11.1.28",
|
||||
"@opentelemetry/api": "1.9.1",
|
||||
"@opentelemetry/core": "2.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "0.220.0",
|
||||
"@opentelemetry/instrumentation": "0.219.0",
|
||||
"@opentelemetry/instrumentation-pg": "0.72.0",
|
||||
"@opentelemetry/resources": "2.9.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.9.0",
|
||||
"@opentelemetry/sdk-trace-node": "2.9.0",
|
||||
"@opentelemetry/semantic-conventions": "1.43.0",
|
||||
"@oxc-project/runtime": "0.139.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "10.65.0",
|
||||
@@ -193,7 +183,7 @@
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"execa": "9.6.1",
|
||||
"fkill": "10.0.3",
|
||||
"js-yaml": "5.2.1",
|
||||
"js-yaml": "5.2.2",
|
||||
"pid-port": "2.1.1",
|
||||
"rolldown": "1.1.5",
|
||||
"simple-oauth2": "5.1.0",
|
||||
|
||||
@@ -87,7 +87,6 @@ export default defineConfig((args) => {
|
||||
'class-validator',
|
||||
/^@sentry\/.*/,
|
||||
/^@sentry-internal\/.*/,
|
||||
/^@opentelemetry\/.*/,
|
||||
'@nestjs/websockets/socket-module',
|
||||
'@nestjs/microservices/microservices-module',
|
||||
'@nestjs/microservices',
|
||||
|
||||
@@ -27,21 +27,6 @@ type SentryBackendConfig = {
|
||||
disabledIntegrations?: string[];
|
||||
};
|
||||
|
||||
type OtelBackendConfig = {
|
||||
endpoint?: string;
|
||||
headers?: Record<string, string>;
|
||||
sampleRate?: number;
|
||||
capturePgSpans?: boolean;
|
||||
capturePgStatement?: boolean;
|
||||
capturePgConnectionSpans?: boolean;
|
||||
captureRedisCommandSpans?: boolean;
|
||||
captureRedisConnectionSpans?: boolean;
|
||||
captureRedisRootSpans?: boolean;
|
||||
resourceAttributes?: Record<string, string>;
|
||||
propagateTraceToRemote?: boolean;
|
||||
jobTraceContextMode?: 'link' | 'parent';
|
||||
};
|
||||
|
||||
/**
|
||||
* 設定ファイルの型
|
||||
*/
|
||||
@@ -87,7 +72,6 @@ type Source = {
|
||||
scope?: 'local' | 'global' | string[];
|
||||
};
|
||||
sentryForBackend?: SentryBackendConfig;
|
||||
otelForBackend?: OtelBackendConfig;
|
||||
sentryForFrontend?: {
|
||||
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
|
||||
vueIntegration?: SentryVue.VueIntegrationOptions | null;
|
||||
@@ -233,7 +217,6 @@ export type Config = {
|
||||
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||
redisForReactions: RedisOptions & RedisOptionsSource;
|
||||
sentryForBackend: SentryBackendConfig | undefined;
|
||||
otelForBackend: OtelBackendConfig | undefined;
|
||||
sentryForFrontend: {
|
||||
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
|
||||
vueIntegration?: SentryVue.VueIntegrationOptions | null;
|
||||
@@ -339,7 +322,6 @@ export function loadConfig(): Config {
|
||||
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
||||
redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
|
||||
sentryForBackend: config.sentryForBackend,
|
||||
otelForBackend: config.otelForBackend,
|
||||
sentryForFrontend: config.sentryForFrontend,
|
||||
id: config.id,
|
||||
proxy: config.proxy,
|
||||
|
||||
@@ -13,6 +13,8 @@ import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
|
||||
@Injectable()
|
||||
export class ChannelFollowingService implements OnModuleInit {
|
||||
@@ -96,11 +98,18 @@ export class ChannelFollowingService implements OnModuleInit {
|
||||
requestUser: MiLocalUser,
|
||||
targetChannel: MiChannel,
|
||||
): Promise<void> {
|
||||
await this.channelFollowingsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
followerId: requestUser.id,
|
||||
followeeId: targetChannel.id,
|
||||
});
|
||||
try {
|
||||
await this.channelFollowingsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
followerId: requestUser.id,
|
||||
followeeId: targetChannel.id,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
throw new IdentifiableError('6e335e39-0203-4418-a936-b3f2dc987845', 'already following');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.globalEventService.publishInternalEvent('followChannel', {
|
||||
userId: requestUser.id,
|
||||
|
||||
@@ -154,6 +154,11 @@ import { ApQuestionService } from './activitypub/models/ApQuestionService.js';
|
||||
import { QueueModule } from './QueueModule.js';
|
||||
import { QueueService } from './QueueService.js';
|
||||
import { LoggerService } from './LoggerService.js';
|
||||
import { WorldRoomService } from './WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from './entities/WorldRoomEntityService.js';
|
||||
import { WorldRoomMultiplayService } from './WorldRoomMultiplayService.js';
|
||||
import { WorldAvatarService } from './WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from './entities/WorldAvatarEntityService.js';
|
||||
import { TelemetryService } from './telemetry/TelemetryService.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
@@ -231,6 +236,9 @@ const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatServic
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
const $PageService: Provider = { provide: 'PageService', useExisting: PageService };
|
||||
const $WorldRoomService: Provider = { provide: 'WorldRoomService', useExisting: WorldRoomService };
|
||||
const $WorldRoomMultiplayService: Provider = { provide: 'WorldRoomMultiplayService', useExisting: WorldRoomMultiplayService };
|
||||
const $WorldAvatarService: Provider = { provide: 'WorldAvatarService', useExisting: WorldAvatarService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
@@ -286,6 +294,8 @@ const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting
|
||||
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
||||
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
|
||||
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
|
||||
const $WorldRoomEntityService: Provider = { provide: 'WorldRoomEntityService', useExisting: WorldRoomEntityService };
|
||||
const $WorldAvatarEntityService: Provider = { provide: 'WorldAvatarEntityService', useExisting: WorldAvatarEntityService };
|
||||
|
||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
@@ -384,6 +394,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
PageService,
|
||||
WorldRoomService,
|
||||
WorldRoomMultiplayService,
|
||||
WorldAvatarService,
|
||||
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
@@ -439,6 +452,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
WorldRoomEntityService,
|
||||
WorldAvatarEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -535,6 +550,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$PageService,
|
||||
$WorldRoomService,
|
||||
$WorldRoomMultiplayService,
|
||||
$WorldAvatarService,
|
||||
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
@@ -590,6 +608,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
$WorldRoomEntityService,
|
||||
$WorldAvatarEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
@@ -686,6 +706,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
PageService,
|
||||
WorldRoomService,
|
||||
WorldRoomMultiplayService,
|
||||
WorldAvatarService,
|
||||
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
@@ -740,6 +763,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
WorldRoomEntityService,
|
||||
WorldAvatarEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -835,6 +860,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$PageService,
|
||||
$WorldRoomService,
|
||||
$WorldRoomMultiplayService,
|
||||
$WorldAvatarService,
|
||||
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
@@ -889,6 +917,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
$WorldRoomEntityService,
|
||||
$WorldAvatarEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
|
||||
@@ -173,6 +173,16 @@ export interface ChatEventTypes {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorldRoomEventTypes {
|
||||
enter: {
|
||||
user: Packed<'UserLite'>;
|
||||
avatar: Packed<'WorldAvatarLite'>['def'] | null;
|
||||
};
|
||||
left: {
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
matched: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
@@ -315,6 +325,10 @@ export type GlobalEvents = {
|
||||
name: `chatRoomStream:${MiChatRoom['id']}`;
|
||||
payload: EventTypesToEventPayload<ChatEventTypes>;
|
||||
};
|
||||
worldRoom: {
|
||||
name: `worldRoomStream:${string}`;
|
||||
payload: EventTypesToEventPayload<WorldRoomEventTypes>;
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
@@ -435,4 +449,9 @@ export class GlobalEventService {
|
||||
public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void {
|
||||
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishWorldRoomStream<K extends keyof WorldRoomEventTypes>(roomId: string, type: K, value?: WorldRoomEventTypes[K]): void {
|
||||
this.publish(`worldRoomStream:${roomId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,20 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import Logger from '../logger.js';
|
||||
|
||||
const logger = new Logger('hashtag/create');
|
||||
|
||||
type AttachedOrMentioned = 'attached' | 'mentioned';
|
||||
type UpdatingHashtagColumn = {
|
||||
totalUserIds: keyof MiHashtag & `${AttachedOrMentioned}UserIds`,
|
||||
totalUsersCount: keyof MiHashtag & `${AttachedOrMentioned}UsersCount`,
|
||||
localUserIds: keyof MiHashtag & `${AttachedOrMentioned}LocalUserIds`,
|
||||
localUsersCount: keyof MiHashtag & `${AttachedOrMentioned}LocalUsersCount`,
|
||||
remoteUserIds: keyof MiHashtag & `${AttachedOrMentioned}RemoteUserIds`,
|
||||
remoteUsersCount: keyof MiHashtag & `${AttachedOrMentioned}RemoteUsersCount`,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class HashtagService {
|
||||
constructor(
|
||||
@@ -68,126 +77,93 @@ export class HashtagService {
|
||||
// TODO: サンプリング
|
||||
this.updateHashtagsRanking(tag, user.id);
|
||||
|
||||
{
|
||||
const index = await this.hashtagsRepository.findOneBy({ name: tag });
|
||||
const column: UpdatingHashtagColumn = isUserAttached ? {
|
||||
totalUserIds: 'attachedUserIds',
|
||||
totalUsersCount: 'attachedUsersCount',
|
||||
localUserIds: 'attachedLocalUserIds',
|
||||
localUsersCount: 'attachedLocalUsersCount',
|
||||
remoteUserIds: 'attachedRemoteUserIds',
|
||||
remoteUsersCount: 'attachedRemoteUsersCount',
|
||||
} : {
|
||||
totalUserIds: 'mentionedUserIds',
|
||||
totalUsersCount: 'mentionedUsersCount',
|
||||
localUserIds: 'mentionedLocalUserIds',
|
||||
localUsersCount: 'mentionedLocalUsersCount',
|
||||
remoteUserIds: 'mentionedRemoteUserIds',
|
||||
remoteUsersCount: 'mentionedRemoteUsersCount',
|
||||
};
|
||||
|
||||
if (index == null && inc) {
|
||||
try {
|
||||
if (isUserAttached) {
|
||||
await this.hashtagsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
name: tag,
|
||||
mentionedUserIds: [],
|
||||
mentionedUsersCount: 0,
|
||||
mentionedLocalUserIds: [],
|
||||
mentionedLocalUsersCount: 0,
|
||||
mentionedRemoteUserIds: [],
|
||||
mentionedRemoteUsersCount: 0,
|
||||
attachedUserIds: [user.id],
|
||||
attachedUsersCount: 1,
|
||||
attachedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [],
|
||||
attachedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0,
|
||||
attachedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [],
|
||||
attachedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0,
|
||||
} as MiHashtag);
|
||||
} else {
|
||||
await this.hashtagsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
name: tag,
|
||||
mentionedUserIds: [user.id],
|
||||
mentionedUsersCount: 1,
|
||||
mentionedLocalUserIds: this.userEntityService.isLocalUser(user) ? [user.id] : [],
|
||||
mentionedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0,
|
||||
mentionedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [],
|
||||
mentionedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0,
|
||||
attachedUserIds: [],
|
||||
attachedUsersCount: 0,
|
||||
attachedLocalUserIds: [],
|
||||
attachedLocalUsersCount: 0,
|
||||
attachedRemoteUserIds: [],
|
||||
attachedRemoteUsersCount: 0,
|
||||
} as MiHashtag);
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
if (isDuplicateKeyValueError(err)) {
|
||||
logger.info(`Duplicate insertion detected. Falling back to update. #${tag}`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inc) {
|
||||
await this.#incrementHashTag(user, tag, column);
|
||||
} else {
|
||||
await this.#decrementHashTag(user, tag, column);
|
||||
}
|
||||
}
|
||||
|
||||
async #incrementHashTag(
|
||||
user: { id: MiUser['id']; host: MiUser['host']; },
|
||||
tag: string,
|
||||
columns: UpdatingHashtagColumn,
|
||||
) {
|
||||
const isLocal = this.userEntityService.isLocalUser(user);
|
||||
const { totalUserIds, totalUsersCount } = columns;
|
||||
const localOrRemoteUserIds = isLocal ? columns.localUserIds : columns.remoteUserIds;
|
||||
const localOrRemoteUserCount = isLocal ? columns.localUsersCount : columns.remoteUsersCount;
|
||||
|
||||
const runner = this.db.createQueryRunner('master');
|
||||
try {
|
||||
await runner.query(
|
||||
`INSERT into "hashtag"("id", "name", "${totalUserIds}", "${totalUsersCount}", "${localOrRemoteUserIds}",
|
||||
"${localOrRemoteUserCount}")
|
||||
VALUES ($3, $1, ARRAY [$2], 1, ARRAY [$2], 1)
|
||||
ON CONFLICT ("name")
|
||||
DO UPDATE SET "${totalUserIds}" = ${appendUserIdIfNotExists(totalUserIds)},
|
||||
"${totalUsersCount}" = ${incrementCountIfNotExists(totalUserIds, totalUsersCount)},
|
||||
"${localOrRemoteUserIds}" = ${appendUserIdIfNotExists(localOrRemoteUserIds)},
|
||||
"${localOrRemoteUserCount}" = ${incrementCountIfNotExists(localOrRemoteUserIds, localOrRemoteUserCount)}`,
|
||||
[tag, user.id, this.idService.gen()],
|
||||
);
|
||||
} finally {
|
||||
await runner.release();
|
||||
}
|
||||
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
const transactionalHashtagRepository = transactionalEntityManager
|
||||
.getRepository(MiHashtag);
|
||||
function appendUserIdIfNotExists(userIds: keyof MiHashtag & `${string}UserIds`): string {
|
||||
return `CASE WHEN NOT ("hashtag"."${userIds}" @> ARRAY[$2 ::varchar]) THEN array_append("hashtag"."${userIds}", $2) ELSE "hashtag"."${userIds}" END`;
|
||||
}
|
||||
|
||||
const index = await transactionalHashtagRepository
|
||||
.createQueryBuilder()
|
||||
.setLock('pessimistic_write')
|
||||
.where('name = :name', { name: tag })
|
||||
.getOne();
|
||||
function incrementCountIfNotExists(userIds: keyof MiHashtag & `${string}UserIds`, userCount: keyof MiHashtag & `${string}UsersCount`): string {
|
||||
return `CASE WHEN NOT ("hashtag"."${userIds}" @> ARRAY[$2 ::varchar]) THEN "hashtag"."${userCount}" + 1 ELSE "hashtag"."${userCount}" END`;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == null) return;
|
||||
async #decrementHashTag(
|
||||
user: { id: MiUser['id']; host: MiUser['host']; },
|
||||
tag: string,
|
||||
columns: UpdatingHashtagColumn,
|
||||
) {
|
||||
const isLocal = this.userEntityService.isLocalUser(user);
|
||||
const { totalUserIds, totalUsersCount } = columns;
|
||||
const localOrRemoteUserIds = isLocal ? columns.localUserIds : columns.remoteUserIds;
|
||||
const localOrRemoteUserCount = isLocal ? columns.localUsersCount : columns.remoteUsersCount;
|
||||
|
||||
const set = {} as any;
|
||||
const runner = this.db.createQueryRunner('master');
|
||||
try {
|
||||
await runner.query(
|
||||
`UPDATE "hashtag"
|
||||
SET "${totalUserIds}" = array_remove("${totalUserIds}", $2),
|
||||
"${totalUsersCount}" = ${decrementIfExists(totalUserIds, totalUsersCount)},
|
||||
"${localOrRemoteUserIds}" = array_remove("${localOrRemoteUserIds}", $2),
|
||||
"${localOrRemoteUserCount}" = ${decrementIfExists(localOrRemoteUserIds, localOrRemoteUserCount)}
|
||||
WHERE "name" = $1`,
|
||||
[tag, user.id],
|
||||
);
|
||||
} finally {
|
||||
await runner.release();
|
||||
}
|
||||
|
||||
if (isUserAttached) {
|
||||
if (inc) {
|
||||
// 自分が初めてこのタグを使ったなら
|
||||
if (!index.attachedUserIds.some(id => id === user.id)) {
|
||||
set.attachedUserIds = () => `array_append("attachedUserIds", '${user.id}')`;
|
||||
set.attachedUsersCount = () => '"attachedUsersCount" + 1';
|
||||
}
|
||||
// 自分が(ローカル内で)初めてこのタグを使ったなら
|
||||
if (this.userEntityService.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) {
|
||||
set.attachedLocalUserIds = () => `array_append("attachedLocalUserIds", '${user.id}')`;
|
||||
set.attachedLocalUsersCount = () => '"attachedLocalUsersCount" + 1';
|
||||
}
|
||||
// 自分が(リモートで)初めてこのタグを使ったなら
|
||||
if (this.userEntityService.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) {
|
||||
set.attachedRemoteUserIds = () => `array_append("attachedRemoteUserIds", '${user.id}')`;
|
||||
set.attachedRemoteUsersCount = () => '"attachedRemoteUsersCount" + 1';
|
||||
}
|
||||
} else {
|
||||
set.attachedUserIds = () => `array_remove("attachedUserIds", '${user.id}')`;
|
||||
set.attachedUsersCount = () => '"attachedUsersCount" - 1';
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
set.attachedLocalUserIds = () => `array_remove("attachedLocalUserIds", '${user.id}')`;
|
||||
set.attachedLocalUsersCount = () => '"attachedLocalUsersCount" - 1';
|
||||
} else {
|
||||
set.attachedRemoteUserIds = () => `array_remove("attachedRemoteUserIds", '${user.id}')`;
|
||||
set.attachedRemoteUsersCount = () => '"attachedRemoteUsersCount" - 1';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 自分が初めてこのタグを使ったなら
|
||||
if (!index.mentionedUserIds.some(id => id === user.id)) {
|
||||
set.mentionedUserIds = () => `array_append("mentionedUserIds", '${user.id}')`;
|
||||
set.mentionedUsersCount = () => '"mentionedUsersCount" + 1';
|
||||
}
|
||||
// 自分が(ローカル内で)初めてこのタグを使ったなら
|
||||
if (this.userEntityService.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) {
|
||||
set.mentionedLocalUserIds = () => `array_append("mentionedLocalUserIds", '${user.id}')`;
|
||||
set.mentionedLocalUsersCount = () => '"mentionedLocalUsersCount" + 1';
|
||||
}
|
||||
// 自分が(リモートで)初めてこのタグを使ったなら
|
||||
if (this.userEntityService.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) {
|
||||
set.mentionedRemoteUserIds = () => `array_append("mentionedRemoteUserIds", '${user.id}')`;
|
||||
set.mentionedRemoteUsersCount = () => '"mentionedRemoteUsersCount" + 1';
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(set).length > 0) {
|
||||
await transactionalHashtagRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.where('id = :id', { id: index.id })
|
||||
.set(set)
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
function decrementIfExists(userIds: keyof MiHashtag & `${string}UserIds`, userCount: keyof MiHashtag & `${string}UsersCount`): string {
|
||||
return `CASE WHEN ("${userIds}" @> ARRAY[$2]) THEN "${userCount}" - 1 ELSE "${userCount}" END`;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
@@ -9,7 +9,6 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { baseQueueOptions, QUEUE } from '@/queue/const.js';
|
||||
import { allSettled } from '@/misc/promise-tracker.js';
|
||||
import { instrumentQueue } from '@/core/telemetry/queue-instrumentation.js';
|
||||
import {
|
||||
DeliverJobData,
|
||||
EndedPollNotificationJobData,
|
||||
@@ -33,12 +32,7 @@ export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
|
||||
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
|
||||
|
||||
function createQueue<T extends object>(queueName: string, config: Config): Bull.Queue<T> {
|
||||
const queue = new Bull.Queue<T>(queueName, baseQueueOptions(config, queueName));
|
||||
// Queue のラップは、enqueue 時に OTel context をジョブデータへ埋め込むためのもの。
|
||||
// Sentry 単独ではジョブ間の context 伝播を使わないので、OTel 未設定時は元の Queue を返す。
|
||||
if (config.otelForBackend == null) return queue;
|
||||
|
||||
return instrumentQueue(queue);
|
||||
return new Bull.Queue<T>(queueName, baseQueueOptions(config, queueName));
|
||||
}
|
||||
|
||||
const $system: Provider = {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiDriveFile,
|
||||
MiWorldAvatar,
|
||||
} from '@/models/_.js';
|
||||
import type { DriveFilesRepository, WorldAvatarsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
@Injectable()
|
||||
export class WorldAvatarService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.worldAvatarsRepository)
|
||||
private worldAvatarsRepository: WorldAvatarsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
public defaultAvatar = {
|
||||
type: 'default',
|
||||
body: {
|
||||
color: [0.8, 0.8, 0.8],
|
||||
roughness: 1,
|
||||
metallic: 0,
|
||||
},
|
||||
eyes: {
|
||||
type: 'a',
|
||||
color: [0, 0, 0],
|
||||
},
|
||||
mouth: {
|
||||
type: 'a',
|
||||
color: [0, 0, 0],
|
||||
},
|
||||
accessories: [],
|
||||
} satisfies MiWorldAvatar['def'];
|
||||
|
||||
@bindThis
|
||||
public async validateDef(
|
||||
me: MiUser,
|
||||
def: MiWorldAvatar['def'],
|
||||
): Promise<boolean> {
|
||||
// TODO
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findMyAvatarById(userId: MiUser['id'], avatarId: MiWorldAvatar['id']) {
|
||||
return this.worldAvatarsRepository.findOneBy({ id: avatarId, userId: userId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findAvatarById(avatarId: MiWorldAvatar['id']) {
|
||||
return this.worldAvatarsRepository.findOne({ where: { id: avatarId }, relations: { user: true } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getMyAvatarsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiWorldAvatar['id'] | null, untilId?: MiWorldAvatar['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.worldAvatarsRepository.createQueryBuilder('avatar'), sinceId, untilId)
|
||||
.andWhere('avatar.userId = :userId', { userId });
|
||||
|
||||
const avatars = await query.take(limit).getMany();
|
||||
|
||||
return avatars;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
me: MiUser,
|
||||
body: Partial<MiWorldAvatar>,
|
||||
): Promise<MiWorldAvatar> {
|
||||
const currentAvatarsCount = await this.worldAvatarsRepository.countBy({ userId: me.id });
|
||||
|
||||
// TODO: limit by role policy
|
||||
|
||||
const avatar = await this.worldAvatarsRepository.insertOne(new MiWorldAvatar({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: body.name,
|
||||
def: body.def,
|
||||
userId: me.id,
|
||||
active: currentAvatarsCount === 0,
|
||||
}));
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
avatar: MiWorldAvatar,
|
||||
body: Partial<MiWorldAvatar>,
|
||||
): Promise<void> {
|
||||
body.updatedAt = new Date();
|
||||
const updated = await this.worldAvatarsRepository.createQueryBuilder().update()
|
||||
.set(body)
|
||||
.where('id = :id', { id: avatar.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
|
||||
if (body.active) {
|
||||
await this.worldAvatarsRepository.createQueryBuilder().update()
|
||||
.set({ active: false })
|
||||
.where('userId = :userId', { userId: avatar.userId })
|
||||
.andWhere('id != :id', { id: avatar.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(avatar: MiWorldAvatar, deleter?: MiUser): Promise<void> {
|
||||
await this.worldAvatarsRepository.delete(avatar.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveAvatarOfUser(userId: MiUser['id']) {
|
||||
return this.worldAvatarsRepository.findOneBy({ userId, active: true });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveAvatarOfUsers(userIds: MiUser['id'][]): Promise<MiWorldAvatar[]> {
|
||||
if (userIds.length === 0) return [];
|
||||
return this.worldAvatarsRepository.findBy({ userId: In(userIds), active: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiWorldRoom,
|
||||
} from '@/models/_.js';
|
||||
import type { MiWorldAvatar, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
type PlayerState = {
|
||||
position: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
sit?: string; // id
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomMultiplayService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userEntityService: UserEntityService,
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async enter(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('enter', { userId, roomId });
|
||||
|
||||
// TODO: atomicにやる
|
||||
const currentPlayers = await this.redisClient.hlen(`worldRoom:${roomId}:players`);
|
||||
if (currentPlayers < 10) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hset(`worldRoom:${roomId}:players`, userId, 1);
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
} else {
|
||||
throw new Error('Room is full.');
|
||||
}
|
||||
|
||||
// TODO: 既に入っていたらスキップ
|
||||
const avatar = await this.worldAvatarService.getActiveAvatarOfUser(userId);
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'enter', {
|
||||
user: await this.userEntityService.pack(userId),
|
||||
avatar: avatar?.def,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async heartbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
const exists = await this.redisClient.hexists(`worldRoom:${roomId}:players`, userId);
|
||||
if (exists) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
} else {
|
||||
throw new Error('Not in room.');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async left(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('left', { userId, roomId });
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:players`, userId);
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:playerStates`, userId);
|
||||
await redisPipeline.exec();
|
||||
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'left', {
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updatePlayerState(userId: MiUser['id'], roomId: MiWorldRoom['id'], state: PlayerState) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hset(`worldRoom:${roomId}:playerStates`, userId, JSON.stringify(state));
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getPlayerStates(roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
|
||||
const entries = await this.redisClient.hgetall(`worldRoom:${roomId}:playerStates`);
|
||||
return Object.fromEntries(Object.entries(entries).map(([userId, state]) => [userId, JSON.parse(state) as PlayerState]));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getPlayerStatesAndHeatbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
|
||||
// TODO: atomicにやる
|
||||
this.heartbeat(userId, roomId);
|
||||
return this.getPlayerStates(roomId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packPlayerProfile(user: Packed<'UserLite'>, avatar: Packed<'WorldAvatarLite'>['def'] | null) {
|
||||
return {
|
||||
user: {
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
avatarUrl: user.avatarUrl,
|
||||
},
|
||||
avatar: avatar ?? this.worldAvatarService.defaultAvatar,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getPlayerProfiles(roomId: MiWorldRoom['id'], userId?: MiUser['id']): Promise<Record<string, any>> {
|
||||
let playerIds = await this.redisClient.hkeys(`worldRoom:${roomId}:players`);
|
||||
playerIds = playerIds.filter(id => id !== userId);
|
||||
|
||||
const packedUsers = await this.userEntityService.packMany(playerIds);
|
||||
const avatars = await this.worldAvatarService.getActiveAvatarOfUsers(playerIds);
|
||||
|
||||
const profiles: Record<string, any> = {};
|
||||
for (const playerId of playerIds) {
|
||||
const packedUser = packedUsers.find(u => u.id === playerId);
|
||||
if (packedUser == null) continue;
|
||||
profiles[playerId] = this.packPlayerProfile(packedUser, avatars.find(a => a.userId === playerId)?.def ?? null);
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiDriveFile,
|
||||
MiWorldRoom,
|
||||
} from '@/models/_.js';
|
||||
import type { DriveFilesRepository, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
const driveFileReferencingOptions = {
|
||||
clippedPicture: ['image'],
|
||||
tapestry: ['image'],
|
||||
poster: ['image'],
|
||||
pictureFrame: ['image'],
|
||||
tabletopPictureFrame: ['image'],
|
||||
tabletopGlassPictureFrame: ['image'],
|
||||
wallCanvas: ['image'],
|
||||
wallGlassPictureFrame: ['image'],
|
||||
tabletopFlag: ['image'],
|
||||
tabletopLcdButtonsController: ['image'],
|
||||
djPlayer: ['image'],
|
||||
monitor: ['image'],
|
||||
allInOnePc: ['image'],
|
||||
laptopPc: ['image'],
|
||||
handheldGameConsole: ['image'],
|
||||
largeMousepad: ['image'],
|
||||
kakejiku: ['image'],
|
||||
} as Record<string, string[]>;
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async validateDef(
|
||||
me: MiUser,
|
||||
def: MiWorldRoom['def'],
|
||||
): Promise<boolean> {
|
||||
// TODO: スキーマ検証(関係ないプロパティを入れたり不正な値を入れたりできないように)
|
||||
// そのためにはJSON SchemaでRoomState/各objectのoptionsを定義する必要がある
|
||||
|
||||
const objectsLimit = 100; // TODO: ref role policy
|
||||
if (def.installedFurnitures.length > objectsLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachedFilesLimit = 30; // TODO: ref role policy
|
||||
|
||||
const attachedFileIds = this.collectReferencedDriveFileIds(def);
|
||||
if (attachedFileIds.size > attachedFilesLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachedFiles = attachedFileIds.size === 0 ? [] : await this.driveFilesRepository.findBy({ id: In([...attachedFileIds]), userId: me.id });
|
||||
for (const file of attachedFiles) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return false;
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
return false;
|
||||
}
|
||||
if (Math.max(file.properties.width ?? 0, file.properties.height ?? 0) > 2048) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findMyRoomById(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
return this.worldRoomsRepository.findOneBy({ id: roomId, userId: userId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findRoomById(roomId: MiWorldRoom['id']) {
|
||||
return this.worldRoomsRepository.findOne({ where: { id: roomId }, relations: { user: true } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRoomsOfUserWithPagination(userId: MiUser['id'], self: boolean, limit: number, sinceId?: MiWorldRoom['id'] | null, untilId?: MiWorldRoom['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.worldRoomsRepository.createQueryBuilder('room'), sinceId, untilId)
|
||||
.andWhere('room.userId = :userId', { userId });
|
||||
|
||||
if (!self) {
|
||||
query.andWhere('room.visibility = :visibility', { visibility: 'public' });
|
||||
}
|
||||
|
||||
const rooms = await query.take(limit).getMany();
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
me: MiUser,
|
||||
body: Partial<MiWorldRoom>,
|
||||
): Promise<MiWorldRoom> {
|
||||
const room = await this.worldRoomsRepository.insertOne(new MiWorldRoom({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
def: body.def,
|
||||
userId: me.id,
|
||||
visibility: body.visibility,
|
||||
}));
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
room: MiWorldRoom,
|
||||
body: Partial<MiWorldRoom>,
|
||||
): Promise<void> {
|
||||
body.updatedAt = new Date();
|
||||
return this.worldRoomsRepository.createQueryBuilder().update()
|
||||
.set(body)
|
||||
.where('id = :id', { id: room.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(room: MiWorldRoom, deleter?: MiUser): Promise<void> {
|
||||
await this.worldRoomsRepository.delete(room.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public collectReferencedDriveFileIds(roomState: MiWorldRoom['def']): Set<MiDriveFile['id']> {
|
||||
const fileIds = new Set<MiDriveFile['id']>();
|
||||
const installedFurnitures = roomState.installedFurnitures ?? roomState.installedObjects; // 後方互換性のため
|
||||
for (const o of installedFurnitures) {
|
||||
const def = driveFileReferencingOptions[o.type];
|
||||
if (def == null) continue;
|
||||
for (const key of def) {
|
||||
const optionValue = o.options[key];
|
||||
if (optionValue != null && optionValue.driveFileId != null && optionValue.driveFileId !== '') {
|
||||
fileIds.add(optionValue.driveFileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, MiWorldAvatar, WorldAvatarsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class WorldAvatarEntityService {
|
||||
constructor(
|
||||
@Inject(DI.worldAvatarsRepository)
|
||||
private worldAvatarsRepository: WorldAvatarsRepository,
|
||||
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiWorldAvatar['id'] | MiWorldAvatar,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldAvatarLite'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: avatar.id,
|
||||
def: avatar.def,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiWorldAvatar['id'] | MiWorldAvatar,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldAvatarDetailed'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: avatar.id,
|
||||
createdAt: this.idService.parse(avatar.id).date.toISOString(),
|
||||
updatedAt: avatar.updatedAt.toISOString(),
|
||||
name: avatar.name,
|
||||
def: avatar.def,
|
||||
active: avatar.active,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
avatars: MiWorldAvatar[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = avatars.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(avatars.map(avatar => this.packLite(avatar, me, { packedUser: _userMap.get(avatar.userId) })));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailedMany(
|
||||
avatars: MiWorldAvatar[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = avatars.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(avatars.map(avatar => this.packDetailed(avatar, me, { packedUser: _userMap.get(avatar.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, MiWorldRoom, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomEntityService {
|
||||
constructor(
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private worldRoomService: WorldRoomService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiWorldRoom['id'] | MiWorldRoom,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldRoomLite'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const room = typeof src === 'object' ? src : await this.worldRoomsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: room.id,
|
||||
createdAt: this.idService.parse(room.id).date.toISOString(),
|
||||
updatedAt: room.updatedAt.toISOString(),
|
||||
userId: room.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(room.user ?? room.userId, me),
|
||||
name: room.name,
|
||||
description: room.description,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiWorldRoom['id'] | MiWorldRoom,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldRoomDetailed'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const room = typeof src === 'object' ? src : await this.worldRoomsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const attachedFileIds = this.worldRoomService.collectReferencedDriveFileIds(room.def);
|
||||
const attachedFiles = attachedFileIds.size === 0 ? [] : await this.driveFilesRepository.findBy({ id: In([...attachedFileIds]), userId: room.userId });
|
||||
|
||||
return await awaitAll({
|
||||
id: room.id,
|
||||
createdAt: this.idService.parse(room.id).date.toISOString(),
|
||||
updatedAt: room.updatedAt.toISOString(),
|
||||
userId: room.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(room.user ?? room.userId, me),
|
||||
name: room.name,
|
||||
description: room.description,
|
||||
def: room.def,
|
||||
attachedFiles: this.driveFileEntityService.packMany(attachedFiles),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
rooms: MiWorldRoom[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = rooms.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(rooms.map(room => this.packLite(room, me, { packedUser: _userMap.get(room.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { captureMessage, shutdownTelemetry, startSpan, startSpanWithTraceContext } from './telemetry-registry.js';
|
||||
import { captureMessage, shutdownTelemetry, startSpan } from './telemetry-registry.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import type { TelemetryCaptureMessageOptions } from './adapters/TelemetryAdapter.js';
|
||||
|
||||
@@ -21,13 +21,6 @@ export class TelemetryService implements OnApplicationShutdown {
|
||||
return startSpan(name, fn);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public startSpanWithTraceContext<T>(name: string, jobData: object, fn: () => T): T {
|
||||
// jobData に enqueue 元の context があれば worker span へ復元する。
|
||||
// context の無い既存ジョブは、通常の startSpan と同じ扱いになる。
|
||||
return startSpanWithTraceContext(name, jobData, fn);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async onApplicationShutdown(_signal?: string): Promise<void> {
|
||||
await shutdownTelemetry();
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as os from 'node:os';
|
||||
import cluster from 'node:cluster';
|
||||
import { envOption } from '@/env.js';
|
||||
import { registerDiagLogger } from '@/core/telemetry/telemetry-diag.js';
|
||||
import { installHttpClientInstrumentation } from '@/core/telemetry/http-client-instrumentation.js';
|
||||
import { installDatabaseInstrumentation } from '@/core/telemetry/database-instrumentation.js';
|
||||
import { installRedisInstrumentation } from '@/core/telemetry/redis-instrumentation.js';
|
||||
import { executeSpan, getQueueTraceContextMode, injectActiveTraceContext, recordSpanError, startSpanWithQueueTraceContext } from '@/core/telemetry/queue-trace-context.js';
|
||||
import type { LogTraceContext } from '@/logging/types.js';
|
||||
import type { Span, SpanStatusCode, Tracer } from '@opentelemetry/api';
|
||||
import type { Resource, ResourceDetector } from '@opentelemetry/resources';
|
||||
import type { ParentBasedSampler, Sampler } from '@opentelemetry/sdk-trace-base';
|
||||
import type { OtelBackendRuntimeConfig, TelemetryAdapter, TelemetryCaptureMessageOptions } from './TelemetryAdapter.js';
|
||||
import type { QueueTraceContextCarrier, QueueTraceContextDeps } from '../queue-trace-context.js';
|
||||
|
||||
const DEFAULT_SHUTDOWN_TIMEOUT = 5000;
|
||||
|
||||
type OpenTelemetryAdapterDeps = {
|
||||
tracer: Pick<Tracer, 'startActiveSpan'>;
|
||||
provider: {
|
||||
shutdown(): Promise<void>;
|
||||
};
|
||||
getActiveSpan: () => Span | undefined;
|
||||
spanStatusCodeError: SpanStatusCode;
|
||||
shutdownTimeout: number;
|
||||
shutdownHttpClientInstrumentation?: () => void;
|
||||
shutdownDatabaseInstrumentation?: () => void;
|
||||
shutdownRedisInstrumentation?: () => void;
|
||||
queueTraceContext?: QueueTraceContextDeps;
|
||||
};
|
||||
|
||||
type CreateSamplerDeps = {
|
||||
ParentBasedSampler: new (config: { root: Sampler }) => ParentBasedSampler;
|
||||
TraceIdRatioBasedSampler: new (sampleRate: number) => Sampler;
|
||||
};
|
||||
|
||||
type CreateResourceDeps = {
|
||||
defaultResource: () => Resource;
|
||||
resourceFromAttributes: (attributes: Record<string, string>) => Resource;
|
||||
detectResources: (config: { detectors: ResourceDetector[] }) => Resource;
|
||||
envDetector: ResourceDetector;
|
||||
serviceNameAttribute: string;
|
||||
serviceInstanceIdAttribute: string;
|
||||
serviceVersionAttribute: string;
|
||||
serviceVersion: string;
|
||||
};
|
||||
|
||||
export class OpenTelemetryAdapter implements TelemetryAdapter {
|
||||
public constructor(
|
||||
private readonly deps: OpenTelemetryAdapterDeps,
|
||||
) {
|
||||
}
|
||||
|
||||
public static async create(config: OtelBackendRuntimeConfig): Promise<OpenTelemetryAdapter> {
|
||||
const [
|
||||
{ context, diag, DiagLogLevel, propagation, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace },
|
||||
{ W3CTraceContextPropagator },
|
||||
{ OTLPTraceExporter },
|
||||
{ defaultResource, detectResources, envDetector, resourceFromAttributes },
|
||||
{ BatchSpanProcessor, ParentBasedSampler, TraceIdRatioBasedSampler },
|
||||
{ NodeTracerProvider },
|
||||
{ ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION },
|
||||
] = await Promise.all([
|
||||
import('@opentelemetry/api'),
|
||||
import('@opentelemetry/core'),
|
||||
import('@opentelemetry/exporter-trace-otlp-proto'),
|
||||
import('@opentelemetry/resources'),
|
||||
import('@opentelemetry/sdk-trace-base'),
|
||||
import('@opentelemetry/sdk-trace-node'),
|
||||
import('@opentelemetry/semantic-conventions'),
|
||||
]);
|
||||
|
||||
// OTel SDK内部のexport失敗は既定だと見えにくいため、Misskeyのloggerへ橋渡しする。
|
||||
registerDiagLogger(diag, DiagLogLevel.WARN);
|
||||
|
||||
// endpoint/headersを未指定にしておくと、OTEL_EXPORTER_OTLP_* 環境変数の標準fallbackが効く。
|
||||
const exporter = new OTLPTraceExporter({
|
||||
...(config.endpoint != null ? { url: config.endpoint } : {}),
|
||||
...(config.headers != null ? { headers: config.headers } : {}),
|
||||
});
|
||||
const spanProcessor = new BatchSpanProcessor(exporter);
|
||||
|
||||
// SDK 2.xではSpanProcessorをprovider生成時に渡す。ここでOTel単体用のproviderを作る。
|
||||
const provider = new NodeTracerProvider({
|
||||
resource: createResource(config, {
|
||||
defaultResource,
|
||||
resourceFromAttributes,
|
||||
detectResources,
|
||||
envDetector,
|
||||
serviceNameAttribute: ATTR_SERVICE_NAME,
|
||||
serviceInstanceIdAttribute: ATTR_SERVICE_INSTANCE_ID,
|
||||
serviceVersionAttribute: ATTR_SERVICE_VERSION,
|
||||
serviceVersion: config.serviceVersion,
|
||||
}),
|
||||
...(config.sampleRate != null ? { sampler: createSampler(config.sampleRate, {
|
||||
ParentBasedSampler,
|
||||
TraceIdRatioBasedSampler,
|
||||
}) } : {}),
|
||||
spanProcessors: [spanProcessor],
|
||||
});
|
||||
|
||||
// HTTP送信には注入しないが、将来のQueue連結でpropagation APIを使える状態にする。
|
||||
provider.register({
|
||||
propagator: new W3CTraceContextPropagator(),
|
||||
});
|
||||
|
||||
// provider操作をdepsに閉じ込め、span wrapper本体をユニットテストしやすくする。
|
||||
const tracer = provider.getTracer('misskey-backend');
|
||||
|
||||
return new OpenTelemetryAdapter({
|
||||
tracer,
|
||||
provider,
|
||||
getActiveSpan: () => trace.getActiveSpan(),
|
||||
spanStatusCodeError: SpanStatusCode.ERROR,
|
||||
shutdownTimeout: DEFAULT_SHUTDOWN_TIMEOUT,
|
||||
shutdownHttpClientInstrumentation: installHttpClientInstrumentation({
|
||||
tracer,
|
||||
spanKindClient: SpanKind.CLIENT,
|
||||
spanStatusCodeError: SpanStatusCode.ERROR,
|
||||
}),
|
||||
// pg のrequire hookとioredis diagnostics channelは、Nest moduleの動的importより前に有効化する。
|
||||
shutdownDatabaseInstrumentation: await installDatabaseInstrumentation(provider, {
|
||||
capturePgSpans: config.capturePgSpans === true,
|
||||
capturePgStatement: config.capturePgStatement === true,
|
||||
capturePgConnectionSpans: config.capturePgConnectionSpans === true,
|
||||
}),
|
||||
shutdownRedisInstrumentation: installRedisInstrumentation(tracer, SpanKind.CLIENT, SpanStatusCode.ERROR, {
|
||||
captureConnectionSpans: config.captureRedisConnectionSpans === true,
|
||||
captureCommandSpans: config.captureRedisCommandSpans === true,
|
||||
requireParentSpan: config.captureRedisRootSpans !== true,
|
||||
}),
|
||||
queueTraceContext: {
|
||||
tracer,
|
||||
propagation,
|
||||
trace,
|
||||
getActiveContext: () => context.active(),
|
||||
rootContext: ROOT_CONTEXT,
|
||||
mode: getQueueTraceContextMode(config.jobTraceContextMode),
|
||||
spanStatusCodeError: SpanStatusCode.ERROR,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public captureMessage(message: string, _opts: TelemetryCaptureMessageOptions): void {
|
||||
// captureMessageは例外通知APIなので、OTelでは対象spanにエラー状態を付ける。
|
||||
// アクティブspanが無い場合(例: BullMQのjob処理が既に完了しspanが閉じた後の'failed'イベント)でも
|
||||
// 通知を握り潰さないよう、報告専用の短命spanを作ってそこに記録する。
|
||||
const span = this.deps.getActiveSpan();
|
||||
if (span != null) {
|
||||
recordSpanError(span, new Error(message), this.deps.spanStatusCodeError);
|
||||
return;
|
||||
}
|
||||
|
||||
this.deps.tracer.startActiveSpan('captureMessage', reportSpan => {
|
||||
recordSpanError(reportSpan, new Error(message), this.deps.spanStatusCodeError);
|
||||
reportSpan.end();
|
||||
});
|
||||
}
|
||||
|
||||
/** activeなSpanの識別子を、Logging基盤で扱える形式へ変換します。 */
|
||||
public getActiveTraceContext(): LogTraceContext | undefined {
|
||||
const activeSpan = this.deps.getActiveSpan();
|
||||
if (activeSpan == null) return undefined;
|
||||
|
||||
const { traceId, spanId, traceFlags } = activeSpan.spanContext();
|
||||
return { traceId, spanId, traceFlags };
|
||||
}
|
||||
|
||||
public startSpan<T>(name: string, fn: () => T): T {
|
||||
// 既存のTelemetryAdapter契約に合わせ、同期/非同期どちらでも同じspan lifetimeを保証する。
|
||||
return this.deps.tracer.startActiveSpan(name, span => executeSpan(span, fn, this.deps.spanStatusCodeError));
|
||||
}
|
||||
|
||||
public injectTraceContext(carrier: QueueTraceContextCarrier): void {
|
||||
const queueTraceContext = this.deps.queueTraceContext;
|
||||
// Queue context 用の依存は任意なので、無い場合はジョブデータを変更しない。
|
||||
if (queueTraceContext == null) return;
|
||||
injectActiveTraceContext(queueTraceContext, carrier);
|
||||
}
|
||||
|
||||
public startSpanWithTraceContext<T>(name: string, jobData: object, fn: () => T): T {
|
||||
const queueTraceContext = this.deps.queueTraceContext;
|
||||
// Queue context 用の依存が無い場合は、従来の span 作成経路と同じ動作を保つ。
|
||||
if (queueTraceContext == null) return this.startSpan(name, fn);
|
||||
|
||||
return startSpanWithQueueTraceContext(queueTraceContext, name, jobData, fn, () => this.startSpan(name, fn));
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
this.deps.shutdownHttpClientInstrumentation?.();
|
||||
this.deps.shutdownDatabaseInstrumentation?.();
|
||||
this.deps.shutdownRedisInstrumentation?.();
|
||||
// BatchSpanProcessorのflushが詰まってもプロセス終了を妨げないよう、上限時間を設ける。
|
||||
// タイムアウト側のtimerは、flushが先に終わった場合にイベントループを無駄に引き留めないようclearする。
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
await Promise.race([
|
||||
this.deps.provider.shutdown(),
|
||||
new Promise<void>(resolve => {
|
||||
timer = setTimeout(resolve, this.deps.shutdownTimeout);
|
||||
}),
|
||||
]).finally(() => {
|
||||
if (timer != null) clearTimeout(timer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createResource(config: OtelBackendRuntimeConfig, deps: CreateResourceDeps): Resource {
|
||||
// resourceを明示指定するとSDKのdefaultResource()は自動付与されなくなる(マージではなく上書き)ため、
|
||||
// telemetry.sdk.*等の標準属性を失わないよう明示的にmergeする。
|
||||
const misskeyDefaultResource = deps.resourceFromAttributes({
|
||||
[deps.serviceNameAttribute]: 'misskey-backend',
|
||||
[deps.serviceInstanceIdAttribute]: `${os.hostname()}:${process.pid}`,
|
||||
[deps.serviceVersionAttribute]: deps.serviceVersion,
|
||||
'misskey.process.role': getMisskeyProcessRole(),
|
||||
});
|
||||
|
||||
// OTel標準の OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES を尊重する。
|
||||
// mergeは右辺が優先されるため、config.resourceAttributesを最優先にする。
|
||||
return deps.defaultResource()
|
||||
.merge(misskeyDefaultResource)
|
||||
.merge(deps.detectResources({ detectors: [deps.envDetector] }))
|
||||
.merge(deps.resourceFromAttributes(config.resourceAttributes ?? {}));
|
||||
}
|
||||
|
||||
export function createSampler(sampleRate: number, deps: CreateSamplerDeps): ParentBasedSampler {
|
||||
// 設定ミスを無言でAlwaysOn/AlwaysOffに倒さず、起動時に明確に失敗させる。
|
||||
// (YAMLでクォートされた数値文字列などnumber型の保証が無い値が来てもここで弾く)
|
||||
if (typeof sampleRate !== 'number' || !Number.isFinite(sampleRate) || sampleRate < 0 || sampleRate > 1) {
|
||||
throw new Error('otelForBackend.sampleRate must be a number between 0.0 and 1.0.');
|
||||
}
|
||||
|
||||
return new deps.ParentBasedSampler({
|
||||
root: new deps.TraceIdRatioBasedSampler(sampleRate),
|
||||
});
|
||||
}
|
||||
|
||||
export function getMisskeyProcessRole(): string {
|
||||
// Trace backend上でserver/queue/workerを見分けられるよう、Misskey固有の役割をresourceに載せる。
|
||||
if (envOption.disableClustering) {
|
||||
if (envOption.onlyServer) return 'primary-server';
|
||||
if (envOption.onlyQueue) return 'primary-queue';
|
||||
return 'primary-server+queue';
|
||||
}
|
||||
|
||||
if (cluster.isPrimary) {
|
||||
if (envOption.onlyServer) return 'fork-only';
|
||||
if (envOption.onlyQueue) return 'primary-queue';
|
||||
return 'primary-server';
|
||||
}
|
||||
|
||||
// worker.tsのworkerMainに合わせる: onlyServerならserver()、それ以外はjobQueue()を実行する。
|
||||
if (envOption.onlyServer) return 'worker-server';
|
||||
return 'worker-queue';
|
||||
}
|
||||
@@ -3,18 +3,13 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import Logger from '@/logger.js';
|
||||
import { registerDiagLogger } from '@/core/telemetry/telemetry-diag.js';
|
||||
import { getQueueTraceContextMode, injectActiveTraceContext, startSpanWithQueueTraceContext } from '@/core/telemetry/queue-trace-context.js';
|
||||
import type { LogTraceContext } from '@/logging/types.js';
|
||||
import type * as SentryNode from '@sentry/node';
|
||||
import type { NodeOptions } from '@sentry/node';
|
||||
import type { OtelBackendRuntimeConfig, SentryBackendConfig, TelemetryAdapter, TelemetryCaptureMessageOptions } from './TelemetryAdapter.js';
|
||||
import type { QueueTraceContextCarrier, QueueTraceContextDeps } from '../queue-trace-context.js';
|
||||
import type { SentryBackendConfig, TelemetryAdapter, TelemetryCaptureMessageOptions } from './TelemetryAdapter.js';
|
||||
|
||||
// OpenTelemetryAdapterのDEFAULT_SHUTDOWN_TIMEOUTと揃え、Sentryのtransportが詰まってもプロセス終了を妨げないようにする。
|
||||
// Sentryのtransportが詰まってもプロセス終了を妨げないようにする。
|
||||
const DEFAULT_SHUTDOWN_TIMEOUT = 5000;
|
||||
const logger = new Logger('telemetry', 'green');
|
||||
|
||||
type SentryIntegrationsOption = NonNullable<NodeOptions['integrations']>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -73,50 +68,9 @@ export function buildSentryNodeOptions(
|
||||
};
|
||||
}
|
||||
|
||||
type BuildSentryOtlpInitOptions = {
|
||||
sentryConfig: SentryBackendConfig;
|
||||
otelConfig: OtelBackendRuntimeConfig;
|
||||
otlpProcessor: unknown;
|
||||
nodeProfilingIntegration?: () => SentryIntegration;
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
export function buildSentryOtlpInitOptions(options: BuildSentryOtlpInitOptions): SentryNodeOptions {
|
||||
// OTel併存時も、remoteへtrace headerを漏らさないデフォルトはSentry単体時と揃える。
|
||||
// propagateTraceToRemote: true か、options.tracePropagationTargets の明示指定がある場合のみ既定を上書きする。
|
||||
const { tracePropagationTargets, ...sentryOptions } = options.sentryConfig.options;
|
||||
const propagateTraceToRemote = options.otelConfig.propagateTraceToRemote === true || tracePropagationTargets != null;
|
||||
const warn = options.warn ?? ((message: string) => logger.warn(message));
|
||||
|
||||
if (options.otelConfig.sampleRate != null) {
|
||||
warn('otelForBackend.sampleRate is ignored when sentryForBackend is also configured; configure sentryForBackend.options.tracesSampleRate or tracesSampler instead.');
|
||||
}
|
||||
|
||||
if (options.otelConfig.resourceAttributes != null) {
|
||||
warn('otelForBackend.resourceAttributes is ignored when sentryForBackend is also configured; configure OTEL_RESOURCE_ATTRIBUTES instead.');
|
||||
}
|
||||
|
||||
return {
|
||||
...buildSentryNodeOptions({
|
||||
...options.sentryConfig,
|
||||
options: {
|
||||
...sentryOptions,
|
||||
...(propagateTraceToRemote ? { tracePropagationTargets } : {}),
|
||||
},
|
||||
}, options.nodeProfilingIntegration),
|
||||
|
||||
// Sentryの単一TracerProviderにOTLP processorを追加し、親欠損や二重providerを避ける。
|
||||
openTelemetrySpanProcessors: [
|
||||
...(options.sentryConfig.options.openTelemetrySpanProcessors ?? []),
|
||||
options.otlpProcessor as NonNullable<SentryNodeOptions['openTelemetrySpanProcessors']>[number],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export class SentryTelemetryAdapter implements TelemetryAdapter {
|
||||
private constructor(
|
||||
private readonly Sentry: typeof SentryNode,
|
||||
private readonly queueTraceContext?: QueueTraceContextDeps,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -129,45 +83,6 @@ export class SentryTelemetryAdapter implements TelemetryAdapter {
|
||||
return new SentryTelemetryAdapter(Sentry);
|
||||
}
|
||||
|
||||
public static async createWithOtlpExport(
|
||||
sentryConfig: SentryBackendConfig,
|
||||
otelConfig: OtelBackendRuntimeConfig,
|
||||
): Promise<SentryTelemetryAdapter> {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
const { context, diag, DiagLogLevel, propagation, ROOT_CONTEXT, SpanStatusCode, trace } = await import('@opentelemetry/api');
|
||||
const { BatchSpanProcessor } = await import('@opentelemetry/sdk-trace-base');
|
||||
const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-proto');
|
||||
|
||||
registerDiagLogger(diag, DiagLogLevel.WARN);
|
||||
|
||||
// OTLP送信だけを担うprocessorを作り、provider生成はSentry.init側に任せる。
|
||||
const otlpProcessor = new BatchSpanProcessor(new OTLPTraceExporter({
|
||||
...(otelConfig.endpoint != null ? { url: otelConfig.endpoint } : {}),
|
||||
...(otelConfig.headers != null ? { headers: otelConfig.headers } : {}),
|
||||
}));
|
||||
|
||||
// SentryとOTLPを同一providerに集約することで、どちらの宛先にも同じspan実体を流す。
|
||||
Sentry.init(buildSentryOtlpInitOptions({
|
||||
sentryConfig,
|
||||
otelConfig,
|
||||
otlpProcessor,
|
||||
nodeProfilingIntegration,
|
||||
}));
|
||||
|
||||
// Sentry が初期化した同じ OTel provider から tracer/context API を受け取り、
|
||||
// Queue を跨ぐ context 伝播も Sentry と OTLP の両方へ同一 span として出力する。
|
||||
return new SentryTelemetryAdapter(Sentry, {
|
||||
tracer: trace.getTracer('misskey-backend'),
|
||||
propagation,
|
||||
trace,
|
||||
getActiveContext: () => context.active(),
|
||||
rootContext: ROOT_CONTEXT,
|
||||
mode: getQueueTraceContextMode(otelConfig.jobTraceContextMode),
|
||||
spanStatusCodeError: SpanStatusCode.ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
public captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void {
|
||||
this.Sentry.captureMessage(message, {
|
||||
level: opts.level,
|
||||
@@ -189,19 +104,6 @@ export class SentryTelemetryAdapter implements TelemetryAdapter {
|
||||
return this.Sentry.startSpan({ name }, fn);
|
||||
}
|
||||
|
||||
public injectTraceContext(carrier: QueueTraceContextCarrier): void {
|
||||
// Sentry 単体構成では queueTraceContext を持たず、従来どおりジョブデータを変更しない。
|
||||
if (this.queueTraceContext == null) return;
|
||||
injectActiveTraceContext(this.queueTraceContext, carrier);
|
||||
}
|
||||
|
||||
public startSpanWithTraceContext<T>(name: string, jobData: object, fn: () => T): T {
|
||||
// Sentry 単体構成では Sentry 既存の span 作成経路を使う。
|
||||
if (this.queueTraceContext == null) return this.startSpan(name, fn);
|
||||
|
||||
return startSpanWithQueueTraceContext(this.queueTraceContext, name, jobData, fn, () => this.startSpan(name, fn));
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
// timeout未指定だとtransportのflushが詰まった際にプロセス終了を妨げるため、上限時間を設ける。
|
||||
await this.Sentry.close(DEFAULT_SHUTDOWN_TIMEOUT);
|
||||
|
||||
@@ -5,19 +5,14 @@
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
import type { LogTraceContext } from '@/logging/types.js';
|
||||
import type { QueueTraceContextCarrier } from '../queue-trace-context.js';
|
||||
|
||||
export type SentryBackendConfig = NonNullable<Config['sentryForBackend']>;
|
||||
export type OtelBackendConfig = NonNullable<Config['otelForBackend']>;
|
||||
export type OtelBackendRuntimeConfig = OtelBackendConfig & {
|
||||
serviceVersion: string;
|
||||
};
|
||||
|
||||
export interface TelemetryCaptureMessageOptions {
|
||||
/** 現在はエラー通知用途だけに絞る。追加する場合は各adapterでの扱いを揃えること。 */
|
||||
level: 'error';
|
||||
|
||||
/** Sentryではuser.idへ渡す。OTel adapterは現在span属性へ付与していないため、必要ならadapter側で拡張する。 */
|
||||
/** Sentryではuser.idへ渡す補助情報です。 */
|
||||
userId?: string;
|
||||
|
||||
/** queue名やendpoint名など、通知先で調査に使う補助情報。 */
|
||||
@@ -25,14 +20,14 @@ export interface TelemetryCaptureMessageOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sentry・OpenTelemetryなど、エラートラッキング/APMサービスごとの実装差異を隠蔽するための抽象。
|
||||
* エラートラッキング/APMサービスごとの実装差異を隠蔽するための抽象。
|
||||
* 新しいサービスを追加する場合はこのインターフェースを実装するアダプタをこのディレクトリに追加し、
|
||||
* telemetry-registry.tsのinitTelemetry内で登録する。
|
||||
*/
|
||||
export interface TelemetryAdapter {
|
||||
/**
|
||||
* 実行中の処理で起きたエラー相当の事象を記録する。
|
||||
* Sentryはmessage通知、OTelはactive spanまたは短命spanへの例外記録として扱う。
|
||||
* Sentryではmessage通知として扱う。
|
||||
*/
|
||||
captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void;
|
||||
|
||||
@@ -45,18 +40,6 @@ export interface TelemetryAdapter {
|
||||
*/
|
||||
startSpan<T>(name: string, fn: () => T): T;
|
||||
|
||||
/**
|
||||
* BullMQ のジョブデータへ保存する carrier に、active trace context を注入する。
|
||||
* OTel を使わない adapter は実装しない。
|
||||
*/
|
||||
injectTraceContext?(carrier: QueueTraceContextCarrier): void;
|
||||
|
||||
/**
|
||||
* ジョブに保存された enqueue 元の context を、worker span の Link または parent として復元する。
|
||||
* context を持たないジョブの互換性は adapter 側で保つ。
|
||||
*/
|
||||
startSpanWithTraceContext?<T>(name: string, jobData: object, fn: () => T): T;
|
||||
|
||||
/**
|
||||
* プロセス終了時にtelemetry backendへ残りのデータをflushする。
|
||||
* 実装側ではtransport停止に引きずられないよう、待機時間に上限を設ける。
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Span, TracerProvider } from '@opentelemetry/api';
|
||||
|
||||
type Instrumentation = {
|
||||
setTracerProvider(provider: TracerProvider): void;
|
||||
enable(): void;
|
||||
disable(): void;
|
||||
};
|
||||
|
||||
type PgInstrumentationConfig = {
|
||||
enhancedDatabaseReporting: boolean;
|
||||
requireParentSpan: boolean;
|
||||
ignoreConnectSpans: boolean;
|
||||
requestHook?(span: Span): void;
|
||||
};
|
||||
|
||||
type InstrumentationConstructor = new (config: PgInstrumentationConfig) => Instrumentation;
|
||||
|
||||
type InstrumentationDeps = {
|
||||
PgInstrumentation: InstrumentationConstructor;
|
||||
};
|
||||
|
||||
type DatabaseInstrumentationOptions = {
|
||||
capturePgStatement: boolean;
|
||||
capturePgConnectionSpans: boolean;
|
||||
};
|
||||
|
||||
type InstallDatabaseInstrumentationOptions = DatabaseInstrumentationOptions & {
|
||||
capturePgSpans: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* pg はアプリケーションが import する前に有効化しないと、require hook 型の
|
||||
* 自動計装がモジュールを patch できない。そのため、
|
||||
* telemetry provider の登録直後にこの関数を呼び出す。
|
||||
*/
|
||||
export async function installDatabaseInstrumentation(provider: TracerProvider, options: InstallDatabaseInstrumentationOptions): Promise<() => void> {
|
||||
if (!options.capturePgSpans) return () => {};
|
||||
|
||||
const { PgInstrumentation } = await import('@opentelemetry/instrumentation-pg');
|
||||
|
||||
return installInstrumentation(provider, { PgInstrumentation }, options);
|
||||
}
|
||||
|
||||
export function installInstrumentation(provider: TracerProvider, deps: InstrumentationDeps, options: DatabaseInstrumentationOptions = {
|
||||
capturePgStatement: false,
|
||||
capturePgConnectionSpans: false,
|
||||
}): () => void {
|
||||
const instrumentations = [
|
||||
new deps.PgInstrumentation({
|
||||
// SQLパラメータには投稿内容・認証情報などが含まれ得るため、常に記録しない。
|
||||
enhancedDatabaseReporting: false,
|
||||
requireParentSpan: true,
|
||||
ignoreConnectSpans: !options.capturePgConnectionSpans,
|
||||
// instrumentation-pgはSQL本文を無加工で属性へ追加する。明示opt-in時だけ残す。
|
||||
...(options.capturePgStatement ? {} : {
|
||||
requestHook: (span: Span) => {
|
||||
span.setAttribute('db.statement', '[REDACTED]');
|
||||
span.setAttribute('db.query.text', '[REDACTED]');
|
||||
},
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
try {
|
||||
for (const instrumentation of instrumentations) {
|
||||
instrumentation.setTracerProvider(provider);
|
||||
instrumentation.enable();
|
||||
}
|
||||
} catch (error) {
|
||||
for (const instrumentation of instrumentations) {
|
||||
instrumentation.disable();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const instrumentation of instrumentations) {
|
||||
instrumentation.disable();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { channel } from 'node:diagnostics_channel';
|
||||
import type { ClientRequest, IncomingMessage } from 'node:http';
|
||||
import type { Span, SpanOptions, SpanStatusCode, Tracer } from '@opentelemetry/api';
|
||||
|
||||
const HTTP_CLIENT_REQUEST_CREATED = 'http.client.request.created';
|
||||
const HTTP_CLIENT_RESPONSE_FINISH = 'http.client.response.finish';
|
||||
const HTTP_CLIENT_REQUEST_ERROR = 'http.client.request.error';
|
||||
|
||||
type HttpClientSpan = Pick<Span, 'end' | 'recordException' | 'setAttribute' | 'setStatus'>;
|
||||
|
||||
type HttpClientInstrumentationDeps = {
|
||||
tracer: Pick<Tracer, 'startSpan'>;
|
||||
spanKindClient: SpanOptions['kind'];
|
||||
spanStatusCodeError: SpanStatusCode;
|
||||
subscribe: (name: string, listener: (message: unknown) => void) => () => void;
|
||||
};
|
||||
|
||||
type RequestCreatedMessage = { request: ClientRequest };
|
||||
type ResponseFinishMessage = { request: ClientRequest; response: IncomingMessage };
|
||||
type RequestErrorMessage = { request: ClientRequest; error: Error };
|
||||
|
||||
/**
|
||||
* require フックを使わず、Node.js 組み込み HTTP クライアントの diagnostics channel を計装する。
|
||||
* telemetry 初期化前に読み込まれたモジュールも対象になる。
|
||||
*/
|
||||
export function createHttpClientInstrumentation(deps: HttpClientInstrumentationDeps): () => void {
|
||||
const spans = new WeakMap<ClientRequest, HttpClientSpan>();
|
||||
|
||||
const unsubscribeCreated = deps.subscribe(HTTP_CLIENT_REQUEST_CREATED, (message: unknown) => {
|
||||
const { request } = message as RequestCreatedMessage;
|
||||
const { url, host, port } = getRequestDetails(request);
|
||||
const method = request.method ?? 'GET';
|
||||
const span = deps.tracer.startSpan(method, {
|
||||
kind: deps.spanKindClient,
|
||||
attributes: {
|
||||
'http.request.method': method,
|
||||
'url.full': url,
|
||||
'server.address': host,
|
||||
'server.port': port,
|
||||
},
|
||||
});
|
||||
spans.set(request, span);
|
||||
});
|
||||
|
||||
const unsubscribeResponseFinish = deps.subscribe(HTTP_CLIENT_RESPONSE_FINISH, (message: unknown) => {
|
||||
const { request, response } = message as ResponseFinishMessage;
|
||||
const span = spans.get(request);
|
||||
if (span == null) return;
|
||||
|
||||
const statusCode = response.statusCode;
|
||||
if (statusCode != null) {
|
||||
span.setAttribute('http.response.status_code', statusCode);
|
||||
}
|
||||
if (response.httpVersion != null) {
|
||||
span.setAttribute('network.protocol.version', response.httpVersion);
|
||||
}
|
||||
if (statusCode != null && statusCode >= 400) {
|
||||
span.setAttribute('error.type', String(statusCode));
|
||||
span.setStatus({ code: deps.spanStatusCodeError });
|
||||
}
|
||||
span.end();
|
||||
spans.delete(request);
|
||||
});
|
||||
|
||||
const unsubscribeRequestError = deps.subscribe(HTTP_CLIENT_REQUEST_ERROR, (message: unknown) => {
|
||||
const { request, error } = message as RequestErrorMessage;
|
||||
const span = spans.get(request);
|
||||
if (span == null) return;
|
||||
|
||||
span.recordException(error);
|
||||
span.setAttribute('error.type', getErrorType(error));
|
||||
span.setStatus({ code: deps.spanStatusCodeError });
|
||||
span.end();
|
||||
spans.delete(request);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeCreated();
|
||||
unsubscribeResponseFinish();
|
||||
unsubscribeRequestError();
|
||||
};
|
||||
}
|
||||
|
||||
export function installHttpClientInstrumentation(deps: Omit<HttpClientInstrumentationDeps, 'subscribe'>): () => void {
|
||||
return createHttpClientInstrumentation({
|
||||
...deps,
|
||||
subscribe: (name, listener) => {
|
||||
const diagnosticChannel = channel(name);
|
||||
diagnosticChannel.subscribe(listener);
|
||||
return () => diagnosticChannel.unsubscribe(listener);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getRequestDetails(request: ClientRequest): { url: string; host: string; port: number } {
|
||||
const protocol = request.protocol ?? 'http:';
|
||||
const host = request.getHeader('host')?.toString() ?? request.host ?? 'localhost';
|
||||
const url = new URL(request.path || '/', `${protocol}//${host}`);
|
||||
// URL 属性には認証情報やクエリ文字列を含めない。
|
||||
url.username = '';
|
||||
url.password = '';
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
|
||||
return {
|
||||
url: url.toString(),
|
||||
host: url.hostname,
|
||||
// URL.port は既定ポートでは空文字列になるため、スキームから補う。
|
||||
port: url.port === '' ? (url.protocol === 'https:' ? 443 : 80) : Number(url.port),
|
||||
};
|
||||
}
|
||||
|
||||
function getErrorType(error: Error): string {
|
||||
// Node.js の system error code は安定した低カーディナリティの識別子になる。
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
return code ?? error.name;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { injectTraceContext } from './telemetry-registry.js';
|
||||
import { injectQueueTraceContext } from './queue-trace-context.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
/**
|
||||
* Queue の add/addBulk をラップし、全ての BullMQ enqueue 経路を一箇所で捕捉する。
|
||||
* QueueService を通さず直接 add/addBulk する呼び出し元もあるため、それぞれで注入すると漏れやすい。
|
||||
*/
|
||||
export function instrumentQueue<T extends object>(queue: Bull.Queue<T>): Bull.Queue<T> {
|
||||
// BullMQ のメソッドは Queue インスタンスを this として使うため、差し替え前に bind して保持する。
|
||||
const add = queue.add.bind(queue);
|
||||
queue.add = ((name, data, opts) => {
|
||||
// BullMQ が data を Redis 用にシリアライズする前に、enqueue 元の context を内部フィールドへ追加する。
|
||||
injectQueueTraceContext(data, injectTraceContext);
|
||||
return add(name, data, opts);
|
||||
}) as typeof queue.add;
|
||||
|
||||
// addBulk は複数ジョブを一度にシリアライズするので、各 data へ同じ context を注入する。
|
||||
const addBulk = queue.addBulk.bind(queue);
|
||||
queue.addBulk = ((jobs) => {
|
||||
for (const job of jobs) {
|
||||
injectQueueTraceContext(job.data, injectTraceContext);
|
||||
}
|
||||
return addBulk(jobs);
|
||||
}) as typeof queue.addBulk;
|
||||
|
||||
return queue;
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Context, PropagationAPI, Span, SpanContext, SpanOptions, SpanStatusCode, Tracer } from '@opentelemetry/api';
|
||||
|
||||
/*
|
||||
* enqueue (push) から worker による取得・処理 (pop) までをテレメトリ上で関連付け、
|
||||
* Queue を挟む非同期処理を一連の流れとして追跡できるようにする。
|
||||
* そのため、trace context を次の流れで伝播する:
|
||||
*
|
||||
* 1. producer 側で active context を W3C Trace Context 形式の carrier へ注入する。
|
||||
* 2. carrier をジョブデータの内部フィールドに保存し、BullMQ/Redis 経由で worker へ渡す。
|
||||
* 3. worker 側で carrier を context へ戻し、設定に応じて Link または parent に使う。
|
||||
*
|
||||
* OTel API への依存を引数にしているのは、OTel 単体構成と Sentry + OTLP 構成で
|
||||
* 同じ伝播処理を使うため。
|
||||
*/
|
||||
/** Redis 上のジョブデータにだけ保存する、OpenTelemetry propagator 用の carrier。 */
|
||||
export type QueueTraceContextCarrier = Record<string, string>;
|
||||
export type QueueTraceContextMode = 'link' | 'parent';
|
||||
|
||||
// 通常のジョブプロセッサが参照しない Misskey 内部フィールドとして、ユーザー定義の data と区別する。
|
||||
const QUEUE_TRACE_CONTEXT_KEY = '__misskeyTraceContext';
|
||||
|
||||
export type QueueTraceContextDeps = {
|
||||
tracer: Pick<Tracer, 'startActiveSpan'>;
|
||||
propagation: Pick<PropagationAPI, 'extract' | 'inject'>;
|
||||
trace: {
|
||||
getSpanContext(context: Context): SpanContext | undefined;
|
||||
};
|
||||
getActiveContext: () => Context;
|
||||
rootContext: Context;
|
||||
mode: QueueTraceContextMode;
|
||||
spanStatusCodeError: SpanStatusCode;
|
||||
};
|
||||
|
||||
export type QueueSpanContext = {
|
||||
options: SpanOptions;
|
||||
parentContext: Context;
|
||||
};
|
||||
|
||||
type QueueSpanContextDeps = Pick<QueueTraceContextDeps, 'propagation' | 'trace' | 'rootContext' | 'mode'>;
|
||||
|
||||
/**
|
||||
* enqueue 元の active context を、ジョブ本来のデータを壊さない内部フィールドとして保持する。
|
||||
* propagator が何も注入しなかった場合は、Redis に不要な空オブジェクトを残さない。
|
||||
*/
|
||||
export function injectQueueTraceContext(data: unknown, inject: (carrier: QueueTraceContextCarrier) => void): void {
|
||||
// BullMQ の型定義外から渡される値も考慮し、書き込めない値は無視する。
|
||||
if (data == null || typeof data !== 'object') return;
|
||||
|
||||
const carrier: QueueTraceContextCarrier = {};
|
||||
inject(carrier);
|
||||
|
||||
// active span が無い場合、propagator は何も注入しない。空の内部フィールドは Redis に保存しない。
|
||||
if (Object.keys(carrier).length === 0) return;
|
||||
Object.assign(data, { [QUEUE_TRACE_CONTEXT_KEY]: carrier });
|
||||
}
|
||||
|
||||
/** 現在実行中の span を propagator の標準形式で carrier へ書き出す。 */
|
||||
export function injectActiveTraceContext(deps: QueueTraceContextDeps, carrier: QueueTraceContextCarrier): void {
|
||||
deps.propagation.inject(deps.getActiveContext(), carrier);
|
||||
}
|
||||
|
||||
/**
|
||||
* ジョブに保存された carrier から、worker span の開始に必要な context と options を組み立てる。
|
||||
*
|
||||
* - parent: enqueue span の子として同じ trace を継続する。
|
||||
* - link: worker span を別の root trace にし、enqueue span への関連だけを Link に残す。
|
||||
*
|
||||
* link モードで trace を分けることで、worker 側の sampling 判定を enqueue 側から独立させられる。
|
||||
*/
|
||||
export function getQueueSpanContext(data: unknown, deps: QueueSpanContextDeps): QueueSpanContext | undefined {
|
||||
const carrier = getQueueTraceContextCarrier(data);
|
||||
if (carrier == null) return undefined;
|
||||
|
||||
// Redis から復元した carrier は別プロセス由来なので、現在の context ではなく ROOT_CONTEXT から展開する。
|
||||
const extractedContext = deps.propagation.extract(deps.rootContext, carrier);
|
||||
if (deps.mode === 'parent') {
|
||||
return {
|
||||
options: {},
|
||||
parentContext: extractedContext,
|
||||
};
|
||||
}
|
||||
|
||||
// Link が受け取るのは Context ではなく SpanContext なので、extract 後に取り出す。
|
||||
const spanContext = deps.trace.getSpanContext(extractedContext);
|
||||
return {
|
||||
options: {
|
||||
root: true,
|
||||
...(spanContext != null ? { links: [{ context: spanContext }] } : {}),
|
||||
},
|
||||
parentContext: deps.rootContext,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* context を持つジョブは Link/parent の規則で span を開始する。
|
||||
* デプロイ前に enqueue されたジョブなど、context を持たない場合は既存の adapter 固有実装へ委ねる。
|
||||
*/
|
||||
export function startSpanWithQueueTraceContext<T>(
|
||||
deps: QueueTraceContextDeps,
|
||||
name: string,
|
||||
jobData: object,
|
||||
fn: () => T,
|
||||
fallback: () => T,
|
||||
): T {
|
||||
const spanContext = getQueueSpanContext(jobData, deps);
|
||||
if (spanContext == null) return fallback();
|
||||
|
||||
return deps.tracer.startActiveSpan(name, spanContext.options, spanContext.parentContext, span => executeSpan(span, fn, deps.spanStatusCodeError));
|
||||
}
|
||||
|
||||
/**
|
||||
* 既存の TelemetryAdapter 契約に合わせ、同期値と Promise のどちらを返す処理も span で包む。
|
||||
* 成功・失敗のどちらでも処理完了まで span を開き、必ず一度だけ閉じる。
|
||||
*/
|
||||
export function executeSpan<T>(span: Span, fn: () => T, spanStatusCodeError: SpanStatusCode): T {
|
||||
try {
|
||||
const result = fn();
|
||||
if (isPromiseLike(result)) {
|
||||
// fn() の戻り値は T のまま保ちつつ、Promise の settle 時に span を閉じる。
|
||||
return result.then(
|
||||
value => {
|
||||
span.end();
|
||||
return value;
|
||||
},
|
||||
error => {
|
||||
recordSpanError(span, error, spanStatusCodeError);
|
||||
span.end();
|
||||
throw error;
|
||||
},
|
||||
) as T;
|
||||
}
|
||||
|
||||
span.end();
|
||||
return result;
|
||||
} catch (error) {
|
||||
// fn() が同期的に throw した場合も、Promise の reject と同じ形で記録して呼び出し元へ戻す。
|
||||
recordSpanError(span, error, spanStatusCodeError);
|
||||
span.end();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Error 以外の throw 値も OTel exporter が扱える例外に正規化し、span をエラー状態にする。 */
|
||||
export function recordSpanError(span: Span, error: unknown, spanStatusCodeError: SpanStatusCode): void {
|
||||
const exception = error instanceof Error ? error : new Error(String(error));
|
||||
span.recordException(exception);
|
||||
span.setStatus({
|
||||
code: spanStatusCodeError,
|
||||
message: exception.message,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 未設定時は、enqueue と worker の sampling を分離できる link を使う。
|
||||
* 設定ミスを無言でフォールバックさせないため、未知の値は起動時にエラーにする。
|
||||
*/
|
||||
export function getQueueTraceContextMode(mode: unknown): QueueTraceContextMode {
|
||||
if (mode == null || mode === 'link') return 'link';
|
||||
if (mode === 'parent') return 'parent';
|
||||
throw new Error('otelForBackend.jobTraceContextMode must be either \'link\' or \'parent\'.');
|
||||
}
|
||||
|
||||
function getQueueTraceContextCarrier(data: unknown): QueueTraceContextCarrier | undefined {
|
||||
if (data == null || typeof data !== 'object') return undefined;
|
||||
const carrier = (data as Record<string, unknown>)[QUEUE_TRACE_CONTEXT_KEY];
|
||||
if (carrier == null || typeof carrier !== 'object' || Array.isArray(carrier)) return undefined;
|
||||
|
||||
// Redis 上の旧いジョブや壊れたデータで worker を落とさないよう、propagator に渡せる string map だけを受け入れる。
|
||||
const entries = Object.entries(carrier);
|
||||
if (entries.length === 0 || entries.some(([, value]) => typeof value !== 'string')) return undefined;
|
||||
return Object.fromEntries(entries) as QueueTraceContextCarrier;
|
||||
}
|
||||
|
||||
function isPromiseLike<T>(value: T): value is T & PromiseLike<Awaited<T>> {
|
||||
// native Promise に限らず thenable も span の完了を待てるよう、instanceof ではなく then の有無で判定する。
|
||||
return value != null && typeof (value as { then?: unknown }).then === 'function';
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { tracingChannel } from 'node:diagnostics_channel';
|
||||
import { context, trace } from '@opentelemetry/api';
|
||||
import type { Span, SpanKind, SpanStatusCode, Tracer } from '@opentelemetry/api';
|
||||
|
||||
type IORedisCommandContext = {
|
||||
command: string;
|
||||
args: string[];
|
||||
database: number;
|
||||
serverAddress: string;
|
||||
serverPort: number | undefined;
|
||||
};
|
||||
|
||||
type IORedisConnectContext = {
|
||||
serverAddress: string;
|
||||
serverPort: number | undefined;
|
||||
};
|
||||
|
||||
type TracingChannelSubscribers<T extends object> = {
|
||||
start(message: T): void;
|
||||
end(message: T & { error?: unknown }): void;
|
||||
asyncStart(message: T & { error?: unknown }): void;
|
||||
asyncEnd(message: T & { error?: unknown }): void;
|
||||
error(message: T & { error: unknown }): void;
|
||||
};
|
||||
|
||||
type TracingChannel<T extends object> = {
|
||||
subscribe(subscribers: TracingChannelSubscribers<T>): void;
|
||||
unsubscribe(subscribers: TracingChannelSubscribers<T>): void;
|
||||
};
|
||||
|
||||
type RedisInstrumentationDeps = {
|
||||
tracingChannel<T extends object>(name: string): TracingChannel<T>;
|
||||
tracer: Pick<Tracer, 'startSpan'>;
|
||||
getActiveSpan(): Span | undefined;
|
||||
spanKindClient: SpanKind;
|
||||
spanStatusCodeError: SpanStatusCode;
|
||||
};
|
||||
|
||||
type RedisInstrumentationOptions = {
|
||||
captureCommandSpans?: boolean;
|
||||
/** requireParentSpan is the official ioredis instrumentation's default. */
|
||||
requireParentSpan?: boolean;
|
||||
captureConnectionSpans?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* ioredis 5.11以降が公開する native diagnostics channel を購読する。
|
||||
* バンドル後もioredis自身が発行するイベントを使うため、require hook に
|
||||
* 依存せず、ESM import 経路でもコマンド span を取得できる。
|
||||
*/
|
||||
export function installRedisInstrumentation(
|
||||
tracer: Pick<Tracer, 'startSpan'>,
|
||||
spanKindClient: SpanKind,
|
||||
spanStatusCodeError: SpanStatusCode,
|
||||
options: RedisInstrumentationOptions = {},
|
||||
): () => void {
|
||||
return createRedisInstrumentation({
|
||||
tracingChannel,
|
||||
tracer,
|
||||
getActiveSpan: () => trace.getSpan(context.active()),
|
||||
spanKindClient,
|
||||
spanStatusCodeError,
|
||||
}, options);
|
||||
}
|
||||
|
||||
export function createRedisInstrumentation(deps: RedisInstrumentationDeps, options: RedisInstrumentationOptions = {}): () => void {
|
||||
const requireParentSpan = options.requireParentSpan ?? true;
|
||||
const cleanup: Array<() => void> = [];
|
||||
if (options.captureCommandSpans === true) {
|
||||
const commandChannel = deps.tracingChannel<IORedisCommandContext>('ioredis:command');
|
||||
const commandSubscribers = createTracingChannelSubscribers(commandChannel, deps, requireParentSpan, message => ({
|
||||
name: message.command,
|
||||
attributes: {
|
||||
'db.system.name': 'redis',
|
||||
'db.namespace': message.database.toString(10),
|
||||
'db.operation.name': message.command,
|
||||
'server.address': message.serverAddress,
|
||||
...(message.serverPort != null ? { 'server.port': message.serverPort } : {}),
|
||||
},
|
||||
}));
|
||||
cleanup.push(() => commandChannel.unsubscribe(commandSubscribers));
|
||||
}
|
||||
if (options.captureConnectionSpans === true) {
|
||||
const connectChannel = deps.tracingChannel<IORedisConnectContext>('ioredis:connect');
|
||||
// Connection spans are explicitly opt-in and should include startup and reconnect attempts.
|
||||
const connectSubscribers = createTracingChannelSubscribers(connectChannel, deps, false, message => ({
|
||||
name: 'connect',
|
||||
attributes: {
|
||||
'db.system.name': 'redis',
|
||||
'db.operation.name': 'connect',
|
||||
'server.address': message.serverAddress,
|
||||
...(message.serverPort != null ? { 'server.port': message.serverPort } : {}),
|
||||
},
|
||||
}));
|
||||
cleanup.push(() => connectChannel.unsubscribe(connectSubscribers));
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const unsubscribe of cleanup) {
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createTracingChannelSubscribers<T extends object>(
|
||||
channel: TracingChannel<T>,
|
||||
deps: RedisInstrumentationDeps,
|
||||
requireParentSpan: boolean,
|
||||
getSpanOptions: (message: T) => { name: string; attributes: Record<string, string | number> },
|
||||
): TracingChannelSubscribers<T> {
|
||||
const spans = new WeakMap<object, { span: Span; recordedError: boolean }>();
|
||||
|
||||
const recordError = (state: { span: Span; recordedError: boolean }, value: unknown): void => {
|
||||
if (state.recordedError) return;
|
||||
const error = toError(value);
|
||||
state.span.recordException(error);
|
||||
state.span.setStatus({ code: deps.spanStatusCodeError, message: error.message });
|
||||
state.span.setAttribute('error.type', getErrorType(error));
|
||||
const statusCode = getRedisErrorStatusCode(error.message);
|
||||
if (statusCode != null) state.span.setAttribute('db.response.status_code', statusCode);
|
||||
state.recordedError = true;
|
||||
};
|
||||
|
||||
const finish = (message: T & { error?: unknown }): void => {
|
||||
const state = spans.get(message);
|
||||
if (state == null) return;
|
||||
|
||||
if (message.error != null) {
|
||||
recordError(state, message.error);
|
||||
}
|
||||
state.span.end();
|
||||
spans.delete(message);
|
||||
};
|
||||
|
||||
const subscribers: TracingChannelSubscribers<T> = {
|
||||
start: (message) => {
|
||||
if (requireParentSpan && deps.getActiveSpan() == null) return;
|
||||
|
||||
const options = getSpanOptions(message);
|
||||
const span = deps.tracer.startSpan(options.name, {
|
||||
kind: deps.spanKindClient,
|
||||
attributes: options.attributes,
|
||||
});
|
||||
spans.set(message, { span, recordedError: false });
|
||||
},
|
||||
// Promiseを返すコマンドでは完了前にもendイベントが来るため、同期例外だけここで閉じる。
|
||||
end: (message) => {
|
||||
if (message.error != null) finish(message);
|
||||
},
|
||||
asyncStart: () => {},
|
||||
asyncEnd: finish,
|
||||
error: (message) => {
|
||||
const state = spans.get(message);
|
||||
if (state == null) return;
|
||||
recordError(state, message.error);
|
||||
},
|
||||
};
|
||||
|
||||
channel.subscribe(subscribers);
|
||||
return subscribers;
|
||||
}
|
||||
|
||||
function toError(value: unknown): Error {
|
||||
return value instanceof Error ? value : new Error(String(value));
|
||||
}
|
||||
|
||||
function getRedisErrorStatusCode(message: string): string | undefined {
|
||||
return message.match(/^([A-Z][A-Z0-9_]*)\b/)?.[1];
|
||||
}
|
||||
|
||||
function getErrorType(error: Error): string {
|
||||
return (error as NodeJS.ErrnoException).code ?? error.name;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import Logger from '@/logger.js';
|
||||
import type { DiagAPI, DiagLogger, DiagLogLevel } from '@opentelemetry/api';
|
||||
|
||||
export function registerDiagLogger(
|
||||
diagApi: DiagAPI,
|
||||
diagLogLevelWarn: DiagLogLevel,
|
||||
): void {
|
||||
// diagはプロセスグローバルなので、通常運用で必要なWARN以上だけをMisskeyのログに流す。
|
||||
const logger = new Logger('otel', 'green');
|
||||
const diagLogger: DiagLogger = {
|
||||
error: (message, ...args) => logger.error(formatDiagMessage(message, args)),
|
||||
warn: (message, ...args) => logger.warn(formatDiagMessage(message, args)),
|
||||
info: (message, ...args) => logger.info(formatDiagMessage(message, args)),
|
||||
debug: (message, ...args) => logger.debug(formatDiagMessage(message, args)),
|
||||
verbose: (message, ...args) => logger.debug(formatDiagMessage(message, args)),
|
||||
};
|
||||
|
||||
diagApi.setLogger(diagLogger, {
|
||||
logLevel: diagLogLevelWarn,
|
||||
suppressOverrideMessage: true,
|
||||
});
|
||||
}
|
||||
|
||||
function formatDiagMessage(message: string, args: unknown[]): string {
|
||||
if (args.length === 0) return message;
|
||||
return `${message} ${args.map(arg => {
|
||||
if (arg instanceof Error) return arg.stack ?? arg.message;
|
||||
if (typeof arg === 'string') return arg;
|
||||
try {
|
||||
return JSON.stringify(arg);
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
}).join(' ')}`;
|
||||
}
|
||||
@@ -5,10 +5,8 @@
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
import { setLogTraceContextProvider } from '@/logging/logging-runtime.js';
|
||||
import { OpenTelemetryAdapter } from './adapters/OpenTelemetryAdapter.js';
|
||||
import { SentryTelemetryAdapter } from './adapters/SentryTelemetryAdapter.js';
|
||||
import type { OtelBackendRuntimeConfig, TelemetryAdapter, TelemetryCaptureMessageOptions } from './adapters/TelemetryAdapter.js';
|
||||
import type { QueueTraceContextCarrier } from './queue-trace-context.js';
|
||||
import type { TelemetryAdapter, TelemetryCaptureMessageOptions } from './adapters/TelemetryAdapter.js';
|
||||
|
||||
/**
|
||||
* NestのDIコンテナが構築される前(boot処理内)で初期化する必要があるため、
|
||||
@@ -18,24 +16,8 @@ import type { QueueTraceContextCarrier } from './queue-trace-context.js';
|
||||
const adapters: TelemetryAdapter[] = [];
|
||||
|
||||
export async function initTelemetry(config: Config): Promise<void> {
|
||||
const otelForBackend: OtelBackendRuntimeConfig | undefined = config.otelForBackend == null ? undefined : {
|
||||
...config.otelForBackend,
|
||||
serviceVersion: config.version,
|
||||
};
|
||||
|
||||
// SentryとOTelを同時に使う場合はproviderを分けず、Sentry側へOTLP processorを追加する。
|
||||
let adapter: TelemetryAdapter | undefined;
|
||||
if (config.sentryForBackend && otelForBackend) {
|
||||
adapter = await SentryTelemetryAdapter.createWithOtlpExport(config.sentryForBackend, otelForBackend);
|
||||
} else if (config.sentryForBackend) {
|
||||
// Sentry単体時は既存のSentry adapterだけを登録する。
|
||||
adapter = await SentryTelemetryAdapter.create(config.sentryForBackend);
|
||||
} else if (otelForBackend) {
|
||||
// OTel単体時だけMisskey自身でNodeTracerProviderを立てる。
|
||||
adapter = await OpenTelemetryAdapter.create(otelForBackend);
|
||||
}
|
||||
|
||||
if (adapter != null) {
|
||||
if (config.sentryForBackend) {
|
||||
const adapter = await SentryTelemetryAdapter.create(config.sentryForBackend);
|
||||
adapters.push(adapter);
|
||||
// Telemetryの初期化後に登録し、初期化前のBootstrapログは従来どおり出力する。
|
||||
setLogTraceContextProvider(() => adapter.getActiveTraceContext?.());
|
||||
@@ -63,19 +45,6 @@ export function startSpan<T>(name: string, fn: () => T): T {
|
||||
return wrapped();
|
||||
}
|
||||
|
||||
export function injectTraceContext(carrier: QueueTraceContextCarrier): void {
|
||||
// Queue の carrier は共有データなので、通知と異なり全 adapter にブロードキャストしない。
|
||||
// OTel provider は現在 1 つだけなので、同じ header を上書きしないよう最初の対応 adapter だけを使う。
|
||||
adapters.find(adapter => adapter.injectTraceContext != null)?.injectTraceContext?.(carrier);
|
||||
}
|
||||
|
||||
export function startSpanWithTraceContext<T>(name: string, jobData: object, fn: () => T): T {
|
||||
// Queue context を解釈できる adapter に span 作成を任せる。
|
||||
// 対応 adapter が無い構成では通常の startSpan へフォールバックする。
|
||||
const adapter = adapters.find(adapter => adapter.startSpanWithTraceContext != null);
|
||||
return adapter?.startSpanWithTraceContext?.(name, jobData, fn) ?? startSpan(name, fn);
|
||||
}
|
||||
|
||||
export async function shutdownTelemetry(): Promise<void> {
|
||||
// 終了時は登録済みadapterを並列にflush/shutdownする。
|
||||
await Promise.allSettled(adapters.map(adapter => adapter.shutdown()));
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* Telemetry-specific shutdown is implemented by telemetry-registry.ts.
|
||||
* Signal coordination belongs to boot/shutdown-handler.ts so telemetry and
|
||||
* logging remain independent domains.
|
||||
*/
|
||||
export { shutdownTelemetry } from './telemetry-registry.js';
|
||||
@@ -91,5 +91,7 @@ export const DI = {
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
noteDraftsRepository: Symbol('noteDraftsRepository'),
|
||||
worldRoomsRepository: Symbol('worldRoomsRepository'),
|
||||
worldAvatarsRepository: Symbol('worldAvatarsRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { logManager } from './logging/logging-runtime.js';
|
||||
import type { LogLevel, LoggerContext, LogWriteInput } from './logging/types.js';
|
||||
import type { LogEntryInput, LogLevel, LoggerContext, LogWriteInput } from './logging/types.js';
|
||||
import type { Keyword } from 'color-convert';
|
||||
|
||||
// 旧APIのdataは表示用の任意値を受け取り、Errorや配列も既存呼び出しで使用されています。
|
||||
type LegacyData = Record<string, any> | null;
|
||||
|
||||
/**
|
||||
* ロガー名の階層と従来の公開APIを提供する薄い窓口です。
|
||||
* 出力条件の判断や整形はLogManagerとLogBackendへ委譲します。
|
||||
@@ -52,6 +55,15 @@ export default class Logger {
|
||||
});
|
||||
}
|
||||
|
||||
/** level別メソッドの構造化入力にlevelとLoggerのcontextを付けて渡します。 */
|
||||
@bindThis
|
||||
private logStructured(level: LogLevel, input: LogEntryInput): void {
|
||||
this.write({
|
||||
...input,
|
||||
level,
|
||||
});
|
||||
}
|
||||
|
||||
/** 構造化ログをLoggerのcontext付きでLogManagerへ渡します。 */
|
||||
@bindThis
|
||||
public write(input: LogWriteInput): void {
|
||||
@@ -62,24 +74,35 @@ export default class Logger {
|
||||
}
|
||||
|
||||
/** 処理を継続できない状況を記録します。 */
|
||||
public error(input: LogEntryInput): void;
|
||||
public error(error: Error, data?: LegacyData, important?: boolean): void;
|
||||
public error(message: string, data?: LegacyData, important?: boolean): void;
|
||||
public error(errorOrMessage: string | Error, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public error(x: string | Error, data?: Record<string, any> | null, important = false): void {
|
||||
public error(x: LogEntryInput | string | Error, data?: LegacyData, important = false): void {
|
||||
if (x instanceof Error) {
|
||||
// エラー本体も第2引数へ残し、従来どおりスタックなどを確認できるようにします。
|
||||
data = data ?? {};
|
||||
data.e = x;
|
||||
this.log('error', x.toString(), data, important, undefined, x);
|
||||
} else if (typeof x === 'object') {
|
||||
this.log('error', `${(x as any).message ?? (x as any).name ?? x}`, data, important);
|
||||
} else if (typeof x === 'string') {
|
||||
this.log('error', x, data, important);
|
||||
} else {
|
||||
this.log('error', `${x}`, data, important);
|
||||
this.logStructured('error', x);
|
||||
}
|
||||
}
|
||||
|
||||
/** 処理は継続できるものの、改善が必要な状況を記録します。 */
|
||||
public warn(input: LogEntryInput): void;
|
||||
public warn(message: string): void;
|
||||
public warn(message: string, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public warn(message: string, data?: Record<string, any> | null, important = false): void {
|
||||
this.log('warn', message, data, important);
|
||||
public warn(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.log('warn', inputOrMessage, data, important);
|
||||
} else {
|
||||
this.logStructured('warn', inputOrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/** 処理が成功したことを、従来のDONE表示で記録します。 */
|
||||
@@ -89,14 +112,40 @@ export default class Logger {
|
||||
}
|
||||
|
||||
/** 開発者向けの調査情報を記録します。 */
|
||||
public debug(input: LogEntryInput): void;
|
||||
public debug(message: string): void;
|
||||
public debug(message: string, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public debug(message: string, data?: Record<string, any> | null, important = false): void {
|
||||
this.log('debug', message, data, important);
|
||||
public debug(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.log('debug', inputOrMessage, data, important);
|
||||
} else {
|
||||
this.logStructured('debug', inputOrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/** 通常の動作状況を記録します。 */
|
||||
public info(input: LogEntryInput): void;
|
||||
public info(message: string): void;
|
||||
public info(message: string, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public info(message: string, data?: Record<string, any> | null, important = false): void {
|
||||
this.log('info', message, data, important);
|
||||
public info(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.log('info', inputOrMessage, data, important);
|
||||
} else {
|
||||
this.logStructured('info', inputOrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/** 致命的な状況を構造化ログとして記録します。 */
|
||||
public fatal(input: LogEntryInput): void;
|
||||
public fatal(message: string): void;
|
||||
@bindThis
|
||||
public fatal(inputOrMessage: LogEntryInput | string): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.logStructured('fatal', { message: inputOrMessage });
|
||||
} else {
|
||||
this.logStructured('fatal', inputOrMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,15 +52,19 @@ export type LogTraceContext = {
|
||||
/** LogManagerが出力直前に呼び出すTrace Context取得処理です。 */
|
||||
export type LogTraceContextProvider = () => LogTraceContext | undefined;
|
||||
|
||||
/** ロガーの呼び出し側が構造化ログとして指定する入力です。 */
|
||||
export type LogWriteInput = {
|
||||
readonly level: LogLevel;
|
||||
/** level別のLoggerメソッドが受け取る構造化ログの共通入力です。 */
|
||||
export type LogEntryInput = {
|
||||
readonly message: string;
|
||||
readonly eventName?: string;
|
||||
readonly attributes?: Readonly<Record<string, unknown>>;
|
||||
readonly error?: unknown;
|
||||
};
|
||||
|
||||
/** 動的なlevelを含めてLogManagerへ渡す構造化ログの入力です。 */
|
||||
export type LogWriteInput = LogEntryInput & {
|
||||
readonly level: LogLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* ロガー名を構成する一要素です。
|
||||
* 色は見やすい形式での表示だけに使い、ログの意味には影響させません。
|
||||
|
||||
@@ -75,6 +75,8 @@ import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-i
|
||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
|
||||
import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js';
|
||||
import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js';
|
||||
import { packedWorldRoomDetailedSchema, packedWorldRoomLiteSchema } from '@/models/json-schema/world-room.js';
|
||||
import { packedWorldAvatarDetailedSchema, packedWorldAvatarLiteSchema } from '@/models/json-schema/world-avatar.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
@@ -147,6 +149,10 @@ export const refs = {
|
||||
ChatRoom: packedChatRoomSchema,
|
||||
ChatRoomInvitation: packedChatRoomInvitationSchema,
|
||||
ChatRoomMembership: packedChatRoomMembershipSchema,
|
||||
WorldRoomLite: packedWorldRoomLiteSchema,
|
||||
WorldRoomDetailed: packedWorldRoomDetailedSchema,
|
||||
WorldAvatarLite: packedWorldAvatarLiteSchema,
|
||||
WorldAvatarDetailed: packedWorldAvatarDetailedSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
||||
@@ -20,6 +20,7 @@ export class MiHashtag {
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
default: [],
|
||||
array: true,
|
||||
})
|
||||
public mentionedUserIds: MiUser['id'][];
|
||||
@@ -32,6 +33,7 @@ export class MiHashtag {
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
default: [],
|
||||
array: true,
|
||||
})
|
||||
public mentionedLocalUserIds: MiUser['id'][];
|
||||
@@ -44,6 +46,7 @@ export class MiHashtag {
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
default: [],
|
||||
array: true,
|
||||
})
|
||||
public mentionedRemoteUserIds: MiUser['id'][];
|
||||
@@ -56,6 +59,7 @@ export class MiHashtag {
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
default: [],
|
||||
array: true,
|
||||
})
|
||||
public attachedUserIds: MiUser['id'][];
|
||||
@@ -68,6 +72,7 @@ export class MiHashtag {
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
default: [],
|
||||
array: true,
|
||||
})
|
||||
public attachedLocalUserIds: MiUser['id'][];
|
||||
@@ -80,6 +85,7 @@ export class MiHashtag {
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
default: [],
|
||||
array: true,
|
||||
})
|
||||
public attachedRemoteUserIds: MiUser['id'][];
|
||||
|
||||
@@ -84,6 +84,8 @@ import {
|
||||
MiChatRoomMembership,
|
||||
MiChatRoomInvitation,
|
||||
MiChatApproval,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
} from './_.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DataSource } from 'typeorm';
|
||||
@@ -544,6 +546,18 @@ const $reversiGamesRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $worldRoomsRepository: Provider = {
|
||||
provide: DI.worldRoomsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiWorldRoom).extend(miRepository as MiRepository<MiWorldRoom>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $worldAvatarsRepository: Provider = {
|
||||
provide: DI.worldAvatarsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiWorldAvatar).extend(miRepository as MiRepository<MiWorldAvatar>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [
|
||||
@@ -623,6 +637,8 @@ const $reversiGamesRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$worldRoomsRepository,
|
||||
$worldAvatarsRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@@ -701,6 +717,8 @@ const $reversiGamesRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$worldRoomsRepository,
|
||||
$worldAvatarsRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('world_avatar')
|
||||
export class MiWorldAvatar {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public active: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
})
|
||||
public def: Record<string, any>;
|
||||
|
||||
constructor(data: Partial<MiWorldAvatar>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
export const worldRoomVisibility = ['public', 'private'] as const;
|
||||
export type WorldRoomVisibility = typeof worldRoomVisibility[number];
|
||||
|
||||
@Entity('world_room')
|
||||
export class MiWorldRoom {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public description: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public likedCount: number;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public accessCount: number;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, default: 'public',
|
||||
})
|
||||
public visibility: WorldRoomVisibility;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
})
|
||||
public def: Record<string, any>;
|
||||
|
||||
constructor(data: Partial<MiWorldRoom>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChannelMuting } from "@/models/ChannelMuting.js";
|
||||
import { MiChannelMuting } from '@/models/ChannelMuting.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiChatRoom } from '@/models/ChatRoom.js';
|
||||
@@ -84,6 +84,8 @@ import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { MiWorldRoom } from '@/models/WorldRoom.js';
|
||||
import { MiWorldAvatar } from '@/models/WorldAvatar.js';
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||
|
||||
export interface MiRepository<T extends ObjectLiteral> {
|
||||
@@ -173,6 +175,8 @@ export {
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
||||
@@ -253,3 +257,5 @@ export type ChatRoomInvitationsRepository = Repository<MiChatRoomInvitation> & M
|
||||
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||
export type WorldRoomsRepository = Repository<MiWorldRoom> & MiRepository<MiWorldRoom>;
|
||||
export type WorldAvatarsRepository = Repository<MiWorldAvatar> & MiRepository<MiWorldAvatar>;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedWorldAvatarLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedWorldAvatarDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
active: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedWorldRoomLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedWorldRoomDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
attachedFiles: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -87,6 +87,8 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
|
||||
import { MiWorldRoom } from '@/models/WorldRoom.js';
|
||||
import { MiWorldAvatar } from '@/models/WorldAvatar.js';
|
||||
|
||||
pg.types.setTypeParser(20, Number);
|
||||
|
||||
@@ -254,6 +256,8 @@ export const entities = [
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { TelemetryService } from '@/core/telemetry/TelemetryService.js';
|
||||
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
import { runQueueJobWithTraceContext } from './queue-job-runner.js';
|
||||
import { runQueueJob } from './queue-job-runner.js';
|
||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||
@@ -159,8 +159,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
}
|
||||
|
||||
// 以下の各 Worker は job.data に保存された enqueue 元の trace context を復元し、
|
||||
// ジョブの実処理全体を Link または parent の worker span で囲む。
|
||||
// 以下の各 Worker はジョブの実処理全体を worker span で囲む。
|
||||
//#region system
|
||||
{
|
||||
const processer = (job: Bull.Job) => {
|
||||
@@ -180,10 +179,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('system');
|
||||
|
||||
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: System: ' + job.name,
|
||||
job.data,
|
||||
() => processer(job) as Promise<void>,
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
@@ -235,10 +233,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('db');
|
||||
|
||||
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: DB: ' + job.name,
|
||||
job.data,
|
||||
() => processer(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
@@ -266,10 +263,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('deliver');
|
||||
|
||||
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: Deliver',
|
||||
job.data,
|
||||
() => this.deliverProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job.data.to}`, { e: renderError(err) });
|
||||
@@ -305,10 +301,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('inbox');
|
||||
|
||||
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: Inbox',
|
||||
job.data,
|
||||
() => this.inboxProcessorService.process(job),
|
||||
err => {
|
||||
const activityId = job.data.activity ? job.data.activity.id : 'none';
|
||||
@@ -345,10 +340,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('user-webhook');
|
||||
|
||||
this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: UserWebhookDeliver',
|
||||
job.data,
|
||||
() => this.userWebhookDeliverProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job.data.to}`, { e: renderError(err) });
|
||||
@@ -384,10 +378,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('system-webhook');
|
||||
|
||||
this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: SystemWebhookDeliver',
|
||||
job.data,
|
||||
() => this.systemWebhookDeliverProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job.data.to}`, { e: renderError(err) });
|
||||
@@ -432,10 +425,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('relationship');
|
||||
|
||||
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: Relationship: ' + job.name,
|
||||
job.data,
|
||||
() => processer(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
@@ -475,10 +467,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('objectStorage');
|
||||
|
||||
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: ObjectStorage: ' + job.name,
|
||||
job.data,
|
||||
() => processer(job) as Promise<void>,
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
@@ -507,10 +498,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('ended-poll-notification');
|
||||
|
||||
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: EndedPollNotification',
|
||||
job.data,
|
||||
() => this.endedPollNotificationProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
@@ -532,10 +522,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
const logger = this.logger.createSubLogger('post-scheduled-note');
|
||||
|
||||
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
return runQueueJob(
|
||||
this.telemetryService,
|
||||
'Queue: PostScheduledNote',
|
||||
job.data,
|
||||
() => this.postScheduledNoteProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
|
||||
@@ -5,17 +5,16 @@
|
||||
|
||||
import type { TelemetryService } from '@/core/telemetry/TelemetryService.js';
|
||||
|
||||
type QueueTelemetryService = Pick<TelemetryService, 'startSpanWithTraceContext'>;
|
||||
type QueueTelemetryService = Pick<TelemetryService, 'startSpan'>;
|
||||
|
||||
/** QueueのprocessorをTrace Context付きで実行し、失敗処理をSpan内で行います。 */
|
||||
export function runQueueJobWithTraceContext<T>(
|
||||
/** Queueのprocessorを実行し、失敗処理をSpan内で行います。 */
|
||||
export function runQueueJob<T>(
|
||||
telemetryService: QueueTelemetryService,
|
||||
spanName: string,
|
||||
jobData: object,
|
||||
processJob: () => T | Promise<T>,
|
||||
onError: (error: Error) => void,
|
||||
): Promise<T> {
|
||||
return telemetryService.startSpanWithTraceContext(spanName, jobData, async (): Promise<T> => {
|
||||
return telemetryService.startSpan(spanName, async (): Promise<T> => {
|
||||
try {
|
||||
return await processJob();
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EndpointsModule } from '@/server/api/EndpointsModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { ApiCallService } from './api/ApiCallService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { HealthServerService } from './HealthServerService.js';
|
||||
@@ -30,7 +31,6 @@ import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { MainChannel } from './api/stream/channels/main.js';
|
||||
import { AdminChannel } from './api/stream/channels/admin.js';
|
||||
import { AntennaChannel } from './api/stream/channels/antenna.js';
|
||||
@@ -49,6 +49,7 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
|
||||
import { ReversiChannel } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
|
||||
import { WorldRoomChannel } from './api/stream/channels/world-room.js';
|
||||
import { NoteStreamingHidingService } from './api/stream/NoteStreamingHidingService.js';
|
||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||
|
||||
@@ -99,6 +100,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||
QueueStatsChannel,
|
||||
ServerStatsChannel,
|
||||
UserListChannel,
|
||||
WorldRoomChannel,
|
||||
NoteStreamingHidingService,
|
||||
OpenApiServerService,
|
||||
OAuth2ProviderService,
|
||||
|
||||
@@ -32,7 +32,6 @@ import { HealthServerService } from './HealthServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
import { registerHttpServerInstrumentation } from './http-server-instrumentation.js';
|
||||
import { registerHttpAccessLog } from './http-access-log.js';
|
||||
|
||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
@@ -82,7 +81,6 @@ export class ServerService implements OnApplicationShutdown {
|
||||
logger: false,
|
||||
});
|
||||
this.#fastify = fastify;
|
||||
await registerHttpServerInstrumentation(fastify, this.config);
|
||||
registerHttpAccessLog(fastify);
|
||||
|
||||
// HSTS
|
||||
|
||||
@@ -447,4 +447,14 @@ export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitati
|
||||
export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js';
|
||||
export * as 'chat/history' from './endpoints/chat/history.js';
|
||||
export * as 'chat/read-all' from './endpoints/chat/read-all.js';
|
||||
export * as 'world/rooms/create' from './endpoints/world/rooms/create.js';
|
||||
export * as 'world/rooms/update' from './endpoints/world/rooms/update.js';
|
||||
export * as 'world/rooms/delete' from './endpoints/world/rooms/delete.js';
|
||||
export * as 'world/rooms/list-by-user' from './endpoints/world/rooms/list-by-user.js';
|
||||
export * as 'world/rooms/show' from './endpoints/world/rooms/show.js';
|
||||
export * as 'world/avatars/create' from './endpoints/world/avatars/create.js';
|
||||
export * as 'world/avatars/update' from './endpoints/world/avatars/update.js';
|
||||
export * as 'world/avatars/delete' from './endpoints/world/avatars/delete.js';
|
||||
export * as 'world/avatars/list' from './endpoints/world/avatars/list.js';
|
||||
export * as 'world/avatars/show' from './endpoints/world/avatars/show.js';
|
||||
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { MiMeta, UsersRepository } from '@/models/_.js';
|
||||
import { SignupService } from '@/core/SignupService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -77,6 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private signupService: SignupService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, _me, token) => {
|
||||
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
|
||||
@@ -93,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
// 初期パスワードが設定されていないのに初期パスワードが入力された場合
|
||||
throw new ApiError(meta.errors.wrongInitialPassword);
|
||||
}
|
||||
} else if ((this.serverSettings.rootUserId != null && (this.serverSettings.rootUserId !== me?.id)) || token !== null) {
|
||||
} else if (token !== null || !(await this.roleService.isAdministrator(me))) {
|
||||
// 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ApiError } from '@/server/api/error.js';
|
||||
import type { UsersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -25,10 +26,10 @@ export const meta = {
|
||||
code: 'NO_SUCH_USER',
|
||||
id: 'ccafc7fe-5074-4edd-9dc0-8ef9ef6a701d',
|
||||
},
|
||||
cannotResetPasswordOfRootUser: {
|
||||
message: 'Cannot reset password of the root user.',
|
||||
code: 'CANNOT_RESET_PASSWORD_OF_ROOT_USER',
|
||||
id: 'f28fc207-42ca-44c7-a577-44b4f0ec5999',
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: 'cda8f8ce-89a6-4f92-8055-33bbe0c1464d',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
@@ -75,8 +77,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
if (this.serverSettings.rootUserId === user.id) {
|
||||
throw new ApiError(meta.errors.cannotResetPasswordOfRootUser);
|
||||
if (await this.roleService.isAdministrator(user) && me.id !== user.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
const passwd = secureRndstr(8);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -26,6 +27,11 @@ export const meta = {
|
||||
code: 'NO_SUCH_USER',
|
||||
id: 'ccafc7fe-5074-4edd-9dc0-8ef9ef6a701d',
|
||||
},
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: 'cda8f8ce-89a6-4f92-8055-33bbe0c1464d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -46,6 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
@@ -55,6 +62,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
if (await this.roleService.isAdministrator(user) && me.id !== user.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
await this.db.transaction(async (transactionalEntityManager) => {
|
||||
// パスキーを全て削除
|
||||
await transactionalEntityManager.delete(MiUserSecurityKey, { userId: user.id });
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ChannelsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -25,6 +26,11 @@ export const meta = {
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: 'c0031718-d573-4e85-928e-10039f1fbb68',
|
||||
},
|
||||
alreadyFollowing: {
|
||||
message: 'You are already following that channel.',
|
||||
code: 'ALREADY_FOLLOWING',
|
||||
id: '7db31665-651e-40c1-8e6e-28e9ad829a2d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -52,7 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
await this.channelFollowingService.follow(me, channel);
|
||||
try {
|
||||
await this.channelFollowingService.follow(me, channel);
|
||||
} catch (e) {
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '6e335e39-0203-4418-a936-b3f2dc987845') throw new ApiError(meta.errors.alreadyFollowing);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ import Parser from 'rss-parser';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { ApiError } from '../error.js';
|
||||
|
||||
const rssParser = new Parser();
|
||||
const MAX_URL_LENGTH = 8192;
|
||||
const MAX_RESPONSE_SIZE = 1024 * 1024;
|
||||
const MAX_CONCURRENT_REQUESTS = 32;
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
@@ -17,6 +20,34 @@ export const meta = {
|
||||
allowGet: true,
|
||||
cacheSec: 60 * 3,
|
||||
|
||||
limit: {
|
||||
duration: 60 * 1000,
|
||||
max: 300,
|
||||
},
|
||||
|
||||
errors: {
|
||||
invalidUrl: {
|
||||
message: 'Invalid URL.',
|
||||
code: 'INVALID_URL',
|
||||
id: '89b7ee05-ccfc-4bdd-9b13-61172fd1e06c',
|
||||
httpStatusCode: 400,
|
||||
},
|
||||
fetchRssFailed: {
|
||||
message: 'Failed to fetch RSS.',
|
||||
code: 'FETCH_RSS_FAILED',
|
||||
id: '8db5d3d8-31d7-452f-b0cc-ca3b8925de12',
|
||||
kind: 'server',
|
||||
httpStatusCode: 422,
|
||||
},
|
||||
fetchRssUnavailable: {
|
||||
message: 'RSS fetching is temporarily unavailable.',
|
||||
code: 'FETCH_RSS_UNAVAILABLE',
|
||||
id: '91e6ff44-c63f-4725-9ad0-b7a40d7f7655',
|
||||
kind: 'server',
|
||||
httpStatusCode: 503,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -215,21 +246,84 @@ export const paramDef = {
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
private readonly inFlightRequests = new Map<string, Promise<Awaited<ReturnType<Parser['parseString']>>>>();
|
||||
private activeRequestCount = 0;
|
||||
|
||||
constructor(
|
||||
private httpRequestService: HttpRequestService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const res = await this.httpRequestService.send(ps.url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/rss+xml, */*',
|
||||
},
|
||||
timeout: 5000,
|
||||
});
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const url = this.normalizeUrl(ps.url);
|
||||
const inFlightRequest = this.inFlightRequests.get(url);
|
||||
if (inFlightRequest != null) {
|
||||
return await inFlightRequest;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
if (this.activeRequestCount >= MAX_CONCURRENT_REQUESTS) {
|
||||
throw new ApiError(meta.errors.fetchRssUnavailable);
|
||||
}
|
||||
|
||||
return rssParser.parseString(text);
|
||||
this.activeRequestCount++;
|
||||
const request = this.fetchRss(url)
|
||||
.catch(() => {
|
||||
throw new ApiError(meta.errors.fetchRssFailed);
|
||||
})
|
||||
.finally(() => {
|
||||
this.inFlightRequests.delete(url);
|
||||
this.activeRequestCount--;
|
||||
});
|
||||
this.inFlightRequests.set(url, request);
|
||||
|
||||
return await request;
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeUrl(input: string): string {
|
||||
if (input.length === 0 || input.length > MAX_URL_LENGTH) {
|
||||
throw new ApiError(meta.errors.invalidUrl);
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(input);
|
||||
} catch {
|
||||
throw new ApiError(meta.errors.invalidUrl);
|
||||
}
|
||||
|
||||
if (
|
||||
(url.protocol !== 'http:' && url.protocol !== 'https:') ||
|
||||
url.username !== '' ||
|
||||
url.password !== ''
|
||||
) {
|
||||
throw new ApiError(meta.errors.invalidUrl);
|
||||
}
|
||||
|
||||
url.hash = '';
|
||||
return url.href;
|
||||
}
|
||||
|
||||
private async fetchRss(url: string): Promise<Awaited<ReturnType<Parser['parseString']>>> {
|
||||
const res = await this.httpRequestService.send(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/rss+xml, */*',
|
||||
},
|
||||
timeout: 5000,
|
||||
size: MAX_RESPONSE_SIZE,
|
||||
});
|
||||
|
||||
const finalUrl = new URL(res.url);
|
||||
if (finalUrl.protocol !== 'http:' && finalUrl.protocol !== 'https:') {
|
||||
throw new Error('Invalid final URL protocol');
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
const rssParser = new Parser({
|
||||
xml2js: {
|
||||
async: true,
|
||||
},
|
||||
});
|
||||
|
||||
return await rssParser.parseString(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['name', 'def'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// TODO: validate avatar
|
||||
|
||||
const avatar = await this.worldAvatarService.create(me, {
|
||||
name: ps.name,
|
||||
def: ps.def,
|
||||
});
|
||||
return await this.worldAvatarEntityService.packDetailed(avatar);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4c',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
await this.worldAvatarService.delete(avatar, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldAvatar',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
||||
|
||||
const avatars = await this.worldAvatarService.getMyAvatarsWithPagination(me.id, ps.limit, sinceId, untilId);
|
||||
return this.worldAvatarEntityService.packDetailedMany(avatars, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldAvatar',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '857ae02f-8759-4d20-9adb-6e95fffe4fd8',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findAvatarById(ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
if (avatar.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
return this.worldAvatarEntityService.packDetailed(avatar, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'fcdb0f92-bda6-47f9-bd05-343e0e020933',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
// TODO: validate avatar
|
||||
|
||||
await this.worldAvatarService.update(avatar, {
|
||||
name: ps.name,
|
||||
def: ps.def,
|
||||
active: ps.active,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user