Merge branch 'develop' into better-8176

This commit is contained in:
tamaina 2022-02-05 02:18:19 +09:00
commit e87e97fce4
102 changed files with 1475 additions and 1295 deletions

View file

@ -16,11 +16,11 @@ First, in order to avoid duplicate Issues, please search to see if the problem y
<!-- Tell us what the bug is -->
## 🙂 Expected Behavior
## 🥰 Expected Behavior
<!--- Tell us what should happen -->
## ☹️ Actual Behavior
## 🤬 Actual Behavior
<!--- Tell us what happens instead of the expected behavior -->
@ -33,3 +33,7 @@ First, in order to avoid duplicate Issues, please search to see if the problem y
## 📌 Environment
<!-- Tell us where on the platform it happens -->
Misskey version:
Your OS:
Your browser:

View file

@ -7,18 +7,26 @@
-->
## 12.x.x (unreleased)
### Improvements
## 12.103.1 (2022/02/02)
### Bugfixes
- 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正
- 「クリップ」ページが開かない問題を修正
- トレンドウィジェットが動作しないのを修正
- フェデレーションウィジェットが動作しないのを修正
- リアクション設定で絵文字ピッカーが開かないのを修正
- DMページでメンションが含まれる問題を修正
- 投稿フォームのハッシュタグ保持フィールドが動作しない問題を修正
- クライアント: ツールチップの表示位置が正しくない問題を修正
## 12.103.0 (2022/02/02)
### Improvements
- クライアント: 連合インスタンスページからインスタンス情報再取得を行えるように
### Bugfixes
- クライアント: 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正
- クライアント: 「クリップ」ページが開かない問題を修正
- クライアント: トレンドウィジェットが動作しないのを修正
- クライアント: フェデレーションウィジェットが動作しないのを修正
- クライアント: リアクション設定で絵文字ピッカーが開かないのを修正
- クライアント: DMページでメンションが含まれる問題を修正
- クライアント: 投稿フォームのハッシュタグ保持フィールドが動作しない問題を修正
- クライアント: サイドビューが動かないのを修正
- クライアント: ensure that specified users does not get duplicates
- Add `img-src` and `media-src` directives to `Content-Security-Policy` for
files and media proxy

View file

@ -1 +1,510 @@
---
_lang_: "বাংলা"
headlineMisskey: "নোট ব্যাবহার করে সংযুক্ত নেটওয়ার্ক"
introMisskey: "স্বাগতম! মিসকি একটি ওপেন সোর্স, ডিসেন্ট্রালাইজড মাইক্রোব্লগিং পরিষেবা। \n\"নোট\" তৈরির মাধ্যমে যা ঘটছে তা সবার সাথে শেয়ার করুন 📡\n\"রিঅ্যাকশন\" গুলির মাধ্যমে যেকোনো নোট সম্পর্কে আপনার অনুভূতি ব্যাক্ত করতে পারেন 👍\nএকটি নতুন দুনিয়া ঘুরে দেখুন 🚀\n"
monthAndDay: "{day}/{month}"
search: "খুঁজুন"
notifications: "বিজ্ঞপ্তি"
username: "ব্যবহারকারীর নাম"
password: "পাসওয়ার্ড"
forgotPassword: "পাসওয়ার্ড ভুলে গেছেন"
fetchingAsApObject: "ফেডিভার্স থেকে খবর আনা হচ্ছে..."
ok: "ঠিক"
gotIt: "বুঝেছি"
cancel: "বাতিল"
enterUsername: "ইউজারনেম লিখুন"
renotedBy: "{user} রিনোট করেছেন"
noNotes: "কোন নোট নেই"
noNotifications: "কোনো বিজ্ঞপ্তি নেই"
instance: "ইন্সট্যান্স"
settings: "সেটিংস"
basicSettings: "সাধারণ সেটিংস"
otherSettings: "অন্যান্য সেটিংস"
openInWindow: "নতুন উইন্ডোতে খুলা"
profile: "প্রোফাইল"
timeline: "টাইমলাইন"
noAccountDescription: "এই ব্যাবহারকারীর কোন বায়ো নেই"
login: "প্রবেশ করুন"
loggingIn: "প্রবেশ করা হচ্ছে..."
logout: "লগআউট"
signup: "নিবন্ধন করুন"
uploading: "আপলোড হচ্ছ …"
save: "সংরক্ষণ"
users: "ব্যবহারকারীগণ"
addUser: "ব্যবহারকারী যোগ করুন"
favorite: "পছন্দ"
favorites: "পছন্দগুলি"
unfavorite: "পছন্দ না"
favorited: "পছন্দ করা হয়েছে"
alreadyFavorited: "ইতিমধ্যে পছন্দ করা হয়েছে"
cantFavorite: "পছন্দ করা যায়নি"
pin: "পিন করা"
unpin: "পিন সরান"
copyContent: "বিষয়বস্তু কপি করুন"
copyLink: "লিঙ্ক কপি করুন"
delete: "মুছুন"
deleteAndEdit: "মুছুন এবং সম্পাদনা করুন"
deleteAndEditConfirm: "আপনি কি এই নোটটি মুছে এটি সম্পাদনা করার বিষয়ে নিশ্চিত? আপনি এটির সমস্ত রিঅ্যাকশন, রিনোট এবং জবাব হারাবেন।"
addToList: "লিস্ট এ যোগ করুন"
sendMessage: "একটি বার্তা পাঠান"
copyUsername: "ব্যবহারকারীর নাম কপি করুন"
searchUser: "ব্যবহারকারী খুঁজুন..."
reply: "জবাব"
loadMore: "আরও দেখুন"
showMore: "আরও দেখুন"
youGotNewFollower: "আপনাকে অনুসরণ করছে"
receiveFollowRequest: "অনুসরণ করার জন্য অনুরোধ পাওয়া গেছে"
followRequestAccepted: "অনুসরণ করার অনুরোধ গৃহীত হয়েছে"
mention: "উল্লেখ"
mentions: "উল্লেখসমূহ"
directNotes: "ডাইরেক্ট নোটগুলি"
importAndExport: "আমদানি এবং রপ্তানি"
import: "আমদানি করুণ"
export: "রপ্তানি"
files: "ফাইলগুলি"
download: "ডাউনলোড"
driveFileDeleteConfirm: "আপনি কি নিশ্চিত যে আপনি \"{name}\" ডিলিট করতে চান? যে সকল নোটের সাথে এই ফাইলটি সংযুক্ত সেগুলোও ডিলিট করা হবে।"
unfollowConfirm: "{name} কে আনফলোও করার ব্যাপারে নিশ্চিত?"
exportRequested: "আপনার তথ্যসমূহ রপ্তানির জন্য অনুরোধ করেছেন। এতে কিছু সময় লাগতে পারে। রপ্তানি সম্পন্ন হলে তা আপনার ড্রাইভে সংরক্ষিত হবে।"
importRequested: "আপনার তথ্যসমূহ আমদানির জন্য অনুরোধ করেছেন। এতে কিছু সময় লাগতে পারে। "
lists: "লিস্ট"
noLists: "কোন লিস্ট নেই"
note: "নোট"
notes: "নোটগুলি"
following: "অনুসরণ করা হচ্ছে"
followers: "অনুসরণকারী"
followsYou: "আপনাকে অনুসরণ করে"
createList: "লিস্ট তৈরি করুন"
manageLists: "লিস্ট ব্যাবস্থাপনা"
error: "সমস্যা"
somethingHappened: "একটি ত্রুটি হয়েছে"
retry: "আবার চেষ্টা করুন"
pageLoadError: "পেজ লোড করা যায়নি"
pageLoadErrorDescription: "এটি সাধারনত নেটওয়ার্কের সমস্যার বা ব্রাউজার ক্যাশের কারণে ঘটে থাকে। ব্রাউজার এর ক্যাশ পরিষ্কার করুন এবং একটু পর আবার চেষ্টা করুন। "
serverIsDead: "এই সার্ভার বর্তমানে সাড়া দিচ্ছে না। একটু পরে আবার চেষ্টা করুন।"
youShouldUpgradeClient: "এই পেজ দেখার জন্য আপনার ব্রাউজার রিফ্রেশ করে ক্লায়েন্ট আপডেট করুন। "
enterListName: "লিস্টের নাম লিখুন"
privacy: "গোপনীয়তা"
makeFollowManuallyApprove: "অনুসরণ করার অনুরোধগুলি গৃহীত হওয়ার জন্য আপনার অনুমতি লাগবে"
defaultNoteVisibility: "ডিফল্ট দৃশ্যমান্যতা"
follow: "অনুসরণ"
followRequest: "অনুসরণ করার অনুরোধ"
followRequests: "অনুসরণ করার অনুরোধসমূহ"
unfollow: "অনুসরণ বাতিল"
followRequestPending: "অনুসরণ করার অনুরোধ বিচারাধীন"
enterEmoji: "ইমোজি প্রবেশ করান"
renote: "রিনোট"
unrenote: "রিনোট সরান "
renoted: "রিনোট করা হয়েছে"
cantRenote: "এই নোটটি রিনোট করা যাবে না।"
cantReRenote: "রিনোটকে রিনোট করা যাবে না।"
quote: "উদ্ধৃতি"
pinnedNote: "পিন করা নোট"
pinned: "পিন করা"
you: "আপনি"
clickToShow: "দেখার জন্য ক্লিক করুন"
sensitive: "সংবেদনশীল বিষয়বস্তু"
add: "যুক্ত করুন"
reaction: "প্রতিক্রিয়া"
reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে"
reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।"
rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন"
attachCancel: "অ্যাটাচমেন্ট সরান "
markAsSensitive: "সংবেদনশীল হিসাবে চিহ্নিত করুন"
unmarkAsSensitive: "সংবেদনশীল চিহ্ন সরান"
enterFileName: "ফাইলের নাম লিখুন"
mute: "মিউট"
unmute: "আনমিউট"
block: "ব্লক"
unblock: "ব্লক সরান"
suspend: "স্থগিত করা"
unsuspend: "অস্থগিত করা"
blockConfirm: "ব্লক করতে চান?"
unblockConfirm: "ব্লক সরাতে চান?"
suspendConfirm: "স্থগিত করতে চান?"
unsuspendConfirm: "অস্থগিত করতে চান?"
selectList: "লিস্ট নির্বাচন করুন"
selectAntenna: "অ্যান্টেনা নির্বাচন করুন"
selectWidget: "উইজেট নির্বাচন করুন"
editWidgets: "উইজেট সম্পাদনা করুন"
editWidgetsExit: "সম্পাদনা শেষ করুন"
customEmojis: "স্বনির্ধারিত ইমোজিগুলি"
emoji: "ইমোজি"
emojis: "ইমোজিগুলি"
emojiName: "ইমোজির নাম"
emojiUrl: "ইমোজির URL"
addEmoji: "ইমোজি যুক্ত করুন"
settingGuide: "সুপারিশকৃত সেটিংস"
cacheRemoteFiles: "রিমোট ফাইলসমুহ ক্যাশ করুন"
cacheRemoteFilesDescription: "যখন এই অপশনটি বন্ধ থাকে তখন রিমোট ফাইল সমূহ সরাসরি রিমোট ইন্সট্যান্স থেকে লোড করা হয়। এই অপশনটি বন্ধ করলে স্টোরেজ এর ব্যাবহার কমবে তবে থাম্বনেইল তৈরি না করার কারণে নেটওয়ার্ক ব্যান্ডউইথ বেশী লাগবে। "
flagAsBot: "বট হিসাবে চিহ্নিত করুন"
flagAsBotDescription: "এই অ্যাকাউন্টটি যদি একটি প্রোগ্রাম দ্বারা পরিচালিত হয়, তাহলে এই অপশনটি চালু করুন। ইন্টারঅ্যাকশান চেইনিং রোধ করতে, মিস্কির সিস্টেম পরিচালনাকে বট-বান্ধব করতে এবং অন্যান্য ডেভেলপারদের সাহায্য করতে আপনার বট এ এই অপশনটি চালু করুন৷"
flagAsCat: "বিড়াল হিসাবে চিহ্নিত করুন"
flagAsCatDescription: "অ্যাকাউন্টটিকে বিড়াল হিসাবে চিহ্নিত করার জন্য অপশনটি চালু করুন।"
autoAcceptFollowed: "আপনি যেসব অ্যাকাউন্ট অনুসরণ করেন, স্বয়ংক্রিয়ভাবে তাদের অনুসরণের অনুরধ স্বীকার করুন"
addAccount: "অ্যাকাউন্ট যোগ করুন"
loginFailed: "প্রবেশ করা যায়নি"
showOnRemote: "রিমোট সার্ভারে দেখুন"
general: "সাধারণ"
wallpaper: "ওয়ালপেপার"
setWallpaper: "ওয়ালপেপার সেট করুন"
removeWallpaper: "ওয়ালপেপার সরান"
searchWith: "খুঁজুন: {q}"
youHaveNoLists: "আপনার কোন লিস্ট নেই"
followConfirm: "{name} কে ফলোও করার ব্যাপারে নিশ্চিত?"
proxyAccount: "প্রক্সি অ্যাকাউন্ট"
proxyAccountDescription: "একটি প্রক্সি অ্যাকাউন্ট এমন একটি অ্যাকাউন্ট যা নির্দিষ্ট শর্তে ব্যবহারকারীদের জন্য রিমোট অনুসরণকারী হিসাবে কাজ করে। উদাহরণস্বরূপ, যখন একজন ব্যবহারকারী একটি রিমোট ব্যবহারকারীকে তালিকাভুক্ত করে, তখন ক্রিয়াকলাপের দৃষ্টান্তে বিতরণ করা হবে না যদি না কেউ তালিকাভুক্ত ব্যবহারকারীকে অনুসরণ করে, তাই প্রক্সি অ্যাকাউন্ট দ্বারা তাকে অনুসরণ করা হবে।"
host: "হোস্ট"
selectUser: "ব্যবহারকারী নির্বাচন করুন"
recipient: "প্রতি"
annotation: "মন্তব্য"
federation: "ফেডিভার্স"
instances: "ইন্সট্যান্স"
registeredAt: "যোগ দিয়েছেন"
latestRequestSentAt: "শেষ রিকুয়েস্ট পাঠানো হয়েছে"
latestRequestReceivedAt: "শেষ রিকুয়েস্ট গৃহীত হয়েছে"
latestStatus: "সর্বশেষ অবস্থা"
storageUsage: "স্টোরেজের ব্যাবহার"
charts: "চার্ট"
perHour: "ঘন্টা প্রতি"
perDay: "দৈনিক"
stopActivityDelivery: "অ্যাক্টিভিটি পাঠানো বন্ধ করুন"
blockThisInstance: "ইন্সট্যান্স ব্লক করুন"
operations: "ক্রিয়াকলাপ"
software: "সফটওয়্যার"
version: "সংস্করণ"
metadata: "মেটাডাটা"
withNFiles: "{n} টি ফাইল"
monitor: "মনিটর"
jobQueue: "জব কিউ"
cpuAndMemory: "সিপিউ এবং মেমরি"
network: "নেটওয়ার্ক"
disk: "ডিস্ক"
instanceInfo: "ইন্সট্যান্সের তথ্য"
statistics: "পরিসংখ্যান"
clearQueue: "কিউ পরিষ্কার করুন"
clearQueueConfirmTitle: "আপনি কি কিউ পরিষ্কার করার ব্যাপারে নিশ্চিত?"
clearQueueConfirmText: "বিতরণ না করা নোট আর বিতরণ করা হবে না। সাধারণত আপনার এটি করার দরকার নেই।"
clearCachedFiles: "ক্যাশ পরিষ্কার করুন"
clearCachedFilesConfirm: "আপনি কি ক্যাশ পরিষ্কার করার ব্যাপারে নিশ্চিত?"
blockedInstances: "ব্লককৃত ইন্সট্যান্সসমুহ"
blockedInstancesDescription: "আপনি যে ইন্সট্যান্সগুলি ব্লক করতে চান তার হোস্টনেমগুলি প্রত্যেকটি আলাদা লাইনে লিখুন। ব্লককৃত ইন্সট্যান্সগুলি এই ইন্সট্যান্সের সাথে যোগাযোগ করতে পারবেনা৷"
muteAndBlock: "মিউট এবং ব্লকগুলি"
mutedUsers: "নিঃশব্দকৃত ব্যবহারকারী"
blockedUsers: "যাদের ব্লক করা হয়েছে"
noUsers: "কোন ব্যাবহারকারী নেই"
editProfile: "প্রোফাইল সম্পাদনা করুন"
noteDeleteConfirm: "আপনি কি নোট ডিলিট করার ব্যাপারে নিশ্চিত?"
pinLimitExceeded: "আপনি আর কোন নোট পিন করতে পারবেন না"
intro: "Misskey এর ইন্সটলেশন সম্পন্ন হয়েছে!দয়া করে অ্যাডমিন ইউজার তৈরি করুন।"
done: "সম্পন্ন"
processing: "প্রক্রিয়াধীন..."
preview: "পূর্বরূপ দেখুন"
default: "পূর্বনির্ধারিত"
noCustomEmojis: "কোন ইমোজি নাই"
noJobs: "কোন জব নাই"
federating: "ফেডারেট করা হচ্ছে"
blocked: "ব্লক করা হয়েছে"
suspended: "স্থগিত করা হয়েছে"
all: "সবগুলো"
subscribing: "সদস্যতা নেয়া হচ্ছে"
publishing: "প্রকাশ করা হচ্ছে"
notResponding: "সাড়া নেই"
instanceFollowing: "ইন্সট্যান্স অনুসরণ করা হচ্ছে"
instanceFollowers: "ইন্সট্যান্স অনুসরণকারী"
instanceUsers: "ইন্সট্যান্স ব্যাবহারকারী"
changePassword: "পাসওয়ার্ড পরিবর্তন করুন"
security: "নিরাপত্তা"
retypedNotMatch: "ইনপুট মেলে না।"
currentPassword: "বর্তমান পাসওয়ার্ড"
newPassword: "নতুন পাসওয়ার্ড"
newPasswordRetype: "নতুন পাসওয়ার্ড (পুনরায় লিখুন)"
attachFile: "ফাইল সংযুক্ত করুন"
more: "আরও!"
featured: "হাইলাইট"
usernameOrUserId: "ব্যাবহারকারীর নাম বা ব্যাবহারকারী ID"
noSuchUser: "কোন ব্যবহারকারী খুঁজে পাওয়া যায়নি"
lookup: "খুঁজে দেখো"
announcements: "ঘোষণা"
imageUrl: "চিত্রের URL"
remove: "মুছুন"
removed: "সরানো হয়েছে"
removeAreYouSure: "আপনি কি \"{x}\" সরানোর ব্যাপারে নিশ্চিত?"
deleteAreYouSure: "আপনি কি \"{x}\" সরানোর ব্যাপারে নিশ্চিত?"
resetAreYouSure: "রিসেট করার ব্যাপারে নিশ্চিত?"
saved: "সংরক্ষিত হয়েছে"
messaging: "চ্যাট"
upload: "আপলোড"
keepOriginalUploading: "আসল ছবি রাখুন"
keepOriginalUploadingDescription: "ছবিটি আপলোড করার সময় আসল সংস্করণটি রাখুন। অপশনটি বন্ধ থাকলে, আপলোডের সময় ওয়েব প্রকাশনার জন্য ছবি ব্রাউজারে তৈরি করা হবে।"
fromDrive: "ড্রাইভ হতে"
fromUrl: "URL হতে"
uploadFromUrl: "URL হতে আপলোড"
uploadFromUrlDescription: "যে ফাইলটি আপলোড করতে চান, সেটির URL"
uploadFromUrlRequested: "আপলোড অনুরোধ করা হয়েছে"
uploadFromUrlMayTakeTime: "URL হতে আপলোড হতে কিছু সময় লাগতে পারে।"
explore: "ঘুরে দেখুন"
messageRead: "পড়া"
noMoreHistory: "আর কোন ইতিহাস নেই"
startMessaging: "চ্যাট শুরু করুন"
nUsersRead: "{n} জন পড়েছেন"
agreeTo: "{0} এর প্রতি আমি সম্মত"
tos: "পরিষেবার শর্তাদি"
start: "শুরু করুন"
home: "মূল পাতা"
remoteUserCaution: "এই ব্যাবহারকারী রিমোট ইন্সট্যান্সের, নিম্নক্ত তথ্য অসম্পূর্ণ হতে পারে।"
activity: "কার্যকলাপ"
images: "ছবি"
birthday: "জন্মদিন"
yearsOld: "{age} বছর"
registeredDate: "যোগদানের তারিখ"
location: "অবস্থান"
theme: "থিম"
themeForLightMode: "লাইট মোডের থিম"
themeForDarkMode: "ডার্ক মোডের থিম"
light: "আলোকিত"
dark: "অন্ধকার"
lightThemes: "আলোকিত থিম"
darkThemes: "অন্ধকার থিম"
syncDeviceDarkMode: "ডিভাইসের সেটিং অনুযায়ী ডার্ক মোড সেট করুন"
drive: "ড্রাইভ"
fileName: "ফাইলের নাম"
selectFile: "ফাইল নির্বাচন করুন"
selectFiles: "ফাইল নির্বাচন করুন"
selectFolder: "ফোল্ডার নির্বাচন করুন"
selectFolders: "ফোল্ডার নির্বাচন করুন"
renameFile: "ফাইল পুনঃনামকরন"
folderName: "ফোল্ডারের নাম"
createFolder: "ফোল্ডার তৈরি করুন"
renameFolder: "ফোল্ডার পুনঃনামকরন"
deleteFolder: "ফোল্ডার মুছুন"
addFile: "ফাইল যোগ করুন"
emptyDrive: "আপনার ড্রাইভ খালি"
emptyFolder: "এই ফোল্ডার খালি"
unableToDelete: "মুছে ফেলা যায়নি"
inputNewFileName: "ফাইলের নতুন নাম লিখুন"
inputNewDescription: "নতুন ক্যাপশন লিখুন"
inputNewFolderName: "ফোল্ডারের নতুন নাম লিখুন"
circularReferenceFolder: "গন্তব্য ফোল্ডারটি আপনি যে ফোল্ডারটি সরাতে চান তার একটি সাবফোল্ডার।"
hasChildFilesOrFolders: "এই ফোল্ডারটি খালি না হওয়ায় ডিলিট করা যায়নি।"
copyUrl: "URL কপি করুন"
rename: "পুনঃনামকরণ"
avatar: "প্রোফাইল ছবি"
banner: "ব্যানার"
nsfw: "সংবেদনশীল বিষয়বস্তু"
whenServerDisconnected: "সার্ভারের সাথে সংযোগ বিচ্ছিন্ন হয়ে গেলে"
disconnectedFromServer: "সার্ভার থেকে সংযোগ বিচ্ছিন্ন হয়েছে"
reload: "আবার লোড করুন"
doNothing: "কিছু করবেন না"
reloadConfirm: "আপনি কি রিলোড করতে চান?"
watch: "দেখুন"
unwatch: "দেখা বন্ধ করুন "
accept: "অনুমোদন"
reject: "প্রত্যাখ্যান"
normal: "স্বাভাবিক"
instanceName: "ইন্সট্যান্সের নাম"
instanceDescription: "ইন্সট্যান্সের বর্ণনা"
maintainerName: "মেইনটেইনার"
maintainerEmail: "মেইনটেইনারের ইমেইল"
tosUrl: "ব্যবহারের শর্তাবলীর URL"
thisYear: "বছর"
thisMonth: "মাস"
today: "আজ"
dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "পৃষ্ঠা"
integration: "ইন্টিগ্রেশন"
connectService: "সংযুক্ত করুন"
disconnectService: "সংযোগ বিচ্ছিন্ন করুন"
enableLocalTimeline: "স্থানীয় টাইমলাইন চালু করুন"
enableGlobalTimeline: "গ্লোবাল টাইমলাইন চালু করুন"
disablingTimelinesInfo: "আপনি এই টাইমলাইনগুলি বন্ধ করলেও প্রশাসক এবং মডারেটররা এই টাইমলাইনগুলি ব্যাবহার করতে পারবে"
registration: "নিবন্ধন"
enableRegistration: "নতুন ব্যাবহারকারী নিবন্ধন চালু করুন"
invite: "আমন্ত্রণ"
proxyRemoteFiles: "রিমোট ফাইলসমুহ প্রক্সি করুন"
proxyRemoteFilesDescription: "যখন এই সেটিংটি চালু থাকে, তখন অসংরক্ষিত বা অতিরিক্ত ক্ষমতার কারণে দূরবর্তী ফাইলগুলিকে স্থানীয়ভাবে প্রক্সি করা হবে এবং থাম্বনেলগুলিও তৈরি করা হবে৷ সার্ভার স্টোরেজ ব্যাবহার করে না,"
driveCapacityPerLocalAccount: "প্রত্যেক স্থানীয় ব্যাবহারকারীর জন্য ড্রাইভের জায়গা"
driveCapacityPerRemoteAccount: "প্রত্যেক রিমোট ব্যাবহারকারীর জন্য ড্রাইভের জায়গা"
inMb: "মেগাবাইটে লিখুন"
iconUrl: "আইকনের URL (ফ্যাভিকন, ইত্যাদি)"
bannerUrl: "ব্যানার ছবির URL"
backgroundImageUrl: "পটভূমির চিত্রের URL"
basicInfo: "আপনার ব্যক্তিগত তথ্য"
pinnedUsers: "পিন করা ব্যাবহারকারীগণ"
pinnedUsersDescription: "আপনি যেসব ব্যবহারকারীদের \"ঘুরে দেখুন\" পৃষ্ঠায় পিন করতে চান তাদের বর্ণনা করুন, প্রত্যেকের বর্ণনা আলাদা লাইনে লিখুন"
pinnedPages: "পিন করা পৃষ্ঠাসুমহ"
pinnedPagesDescription: "আপনি যেসকল পৃষ্ঠাসমূহকে \"ঘুরে দেখুন\" পৃষ্ঠায় পিন করতে চান তাদের বর্ণনা করুন, প্রত্যেকের বর্ণনা আলাদা লাইনে লিখুন"
pinnedClipId: "পিনকৃত ক্লিপের ID"
pinnedNotes: "পিন করা নোট"
hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptcha চালু করুন"
hcaptchaSiteKey: "সাইট কী"
hcaptchaSecretKey: "সিক্রেট কী"
recaptcha: "reCAPTCHA"
enableRecaptcha: "reCAPTCHA চালু করুন"
recaptchaSiteKey: "সাইট কী"
antennas: "অ্যান্টেনা"
manageAntennas: "অ্যান্টেনা ব্যবস্থাপনা"
name: "নাম"
antennaSource: "অ্যান্টেনার উৎস"
antennaKeywords: "যেসব কীওয়ার্ড দেখা হবে"
antennaExcludeKeywords: "যেসব কীওয়ার্ড দেখা হবে না"
antennaKeywordsDescription: "স্পেস দিয়ে আলাদা করলে AND শর্ত তৈরি হবে এবং আলাদা লাইনে লিখলে OR শর্ত তৈরি হবে।"
notifyAntenna: "নতুন নোট সম্পর্কে অবহিত করুন"
withFileAntenna: "শুধুমাত্র ফাইলযুক্ত নোট"
enableServiceworker: "ServiceWorker চালু করুন"
antennaUsersDescription: "প্রত্যেক লাইনে একজন ব্যবহারকারীর নাম লিখুন"
caseSensitive: "ছোট হাতের এবং বড় হাতের অক্ষর নির্দিষ্ট করুন"
withReplies: "জবাবসমুহ যুক্ত করুন"
connectedTo: "আপনি নিম্নলিখিত অ্যাকাউন্টের সাথে সংযুক্ত"
notesAndReplies: "নোটসমূহ এবং জবাবগুলি"
withFiles: "ফাইলগুলি যুক্ত করুন"
silence: "নীরব"
silenceConfirm: "আপনি কি এই ব্যাবহারকারীকের নীরব করতে চান?"
unsilence: "সরব"
unsilenceConfirm: "আপনি কি এই ব্যাবহারকারীকের সরব করতে চান?"
popularUsers: "জনপ্রিয় ব্যবহারকারীগন"
recentlyUpdatedUsers: "সম্প্রতি পোস্ট করা ব্যবহারকারীগন"
recentlyRegisteredUsers: "নতুন যোগ দেওয়া ব্যবহারকারীগন"
recentlyDiscoveredUsers: "নতুন খুঁজে পাওয়া ব্যবহারকারীগন"
exploreUsersCount: "{count} জন ব্যাবহারকারী"
exploreFediverse: "Fediverse ঘুরে দেখুন"
popularTags: "জনপ্রিয় ট্যাগগুলি"
userList: "লিস্ট"
about: "আপনার সম্পর্কে"
aboutMisskey: "Misskey সম্পর্কে"
administrator: "প্রশাসক"
token: "টোকেন"
twoStepAuthentication: "২-ধাপ প্রমাণীকরণ"
moderator: "মডারেটর"
nUsersMentioned: "{n} জনকে উল্লেখ করা হয়েছে"
securityKey: "সিকিউরিটি কী"
securityKeyName: "কী'র নাম"
registerSecurityKey: "সিকিউরিটি কী নিবন্ধন করুন"
lastUsed: "শেষ ব্যাবহার করা হয়েছে"
unregister: "নিবন্ধনমুক্ত হন"
passwordLessLogin: "পাসওয়ার্ড-বিহীন লগইন সেট আপ করুন"
resetPassword: "পাসওয়ার্ড রিসেট করুন"
newPasswordIs: "নতুন পাসওয়ার্ড হচ্ছে \"{password}\""
reduceUiAnimation: "UI অ্যানিমেশন কমান"
share: "শেয়ার"
notFound: "পাওয়া যায়নি"
notFoundDescription: "এই URL-এর সাথে সম্পর্কিত কোনো পৃষ্ঠা নেই।"
uploadFolder: "আপলোডের জন্য ডিফল্ট ফোল্ডার"
cacheClear: "ক্যাশ পরিষ্কার করুন"
markAsReadAllNotifications: "সমস্ত বিজ্ঞপ্তিগুলি পঠিত হিসাবে চিহ্নিত করুন"
markAsReadAllUnreadNotes: "সমস্ত নোটগুলি পঠিত হিসাবে চিহ্নিত করুন"
invites: "আমন্ত্রণ"
invitations: "আমন্ত্রণ"
useOsNativeEmojis: "অপারেটিং সিস্টেমের নেটিভ ইমোজি ব্যবহার করুন"
disableDrawer: "ড্রয়ার মেনু প্রদর্শন করবেন না"
youHaveNoGroups: "আপনার কোন গ্রুপ নেই "
joinOrCreateGroup: "একটি বিদ্যমান গ্রুপের আমন্ত্রণ পান বা একটি নতুন গ্রুপ তৈরি করুন৷"
noHistory: "কোনো ইতিহাস নেই"
signinHistory: "প্রবেশ করার ইতিহাস"
disableAnimatedMfm: "অ্যানিমেটেড MFM অক্ষম করুন"
doing: "প্রক্রিয়া করছে..."
category: "বিভাগ"
tags: "ট‍্যাগসমূহ"
docSource: "ডকুমেন্টের উৎস"
createAccount: "অ্যাকাউন্ট তৈরি করুন"
existingAccount: "বিদ্যমান অ্যাকাউন্ট"
regenerate: "আবারও তৈরি করুন"
fontSize: "ফন্টের আকার"
noFollowRequests: "আপনার কোন ফলোও রিকুয়েস্ট নেই"
openImageInNewTab: "ছবি নতুন ট্যাবে খুলুন"
dashboard: "ড্যাশবোর্ড"
local: "স্থানীয়"
remote: "রিমোট"
total: "মোট"
weekOverWeekChanges: "গত সপ্তাহে"
dayOverDayChanges: "গতকাল"
appearance: "অবয়ব"
clientSettings: "ক্লায়েন্ট সেটিংস"
accountSettings: "অ্যাকাউন্ট সেটিংস"
promotion: "প্রমোশন"
promote: "প্রচার করুন"
numberOfDays: "দিনের সংখ্যা"
hideThisNote: "নোটটি লুকান"
smtpHost: "হোস্ট"
smtpUser: "ব্যবহারকারীর নাম"
smtpPass: "পাসওয়ার্ড"
clearCache: "ক্যাশ পরিষ্কার করুন"
info: "আপনার সম্পর্কে"
user: "ব্যবহারকারীগণ"
controlPanel: "নিয়ন্ত্রন কেন্দ্র"
_email:
_follow:
title: "আপনাকে অনুসরণ করছে"
_mfm:
mention: "উল্লেখ"
quote: "উদ্ধৃতি"
emoji: "স্বনির্ধারিত ইমোজিগুলি"
search: "খুঁজুন"
_theme:
keys:
mention: "উল্লেখ"
renote: "রিনোট"
_sfx:
note: "নোটগুলি"
notification: "বিজ্ঞপ্তি"
chat: "চ্যাট"
_widgets:
notifications: "বিজ্ঞপ্তি"
timeline: "টাইমলাইন"
activity: "কার্যকলাপ"
federation: "ফেডিভার্স"
jobQueue: "জব কিউ"
_cw:
show: "আরও দেখুন"
_visibility:
home: "মূল পাতা"
followers: "অনুসরণকারী"
_profile:
name: "নাম"
username: "ব্যবহারকারীর নাম"
_exportOrImport:
followingList: "অনুসরণ করা হচ্ছে"
muteList: "মিউট"
blockingList: "ব্লক"
userLists: "লিস্ট"
_timelines:
home: "মূল পাতা"
_pages:
blocks:
image: "ছবি"
script:
categories:
list: "লিস্ট"
blocks:
_join:
arg1: "লিস্ট"
_randomPick:
arg1: "লিস্ট"
_dailyRandomPick:
arg1: "লিস্ট"
_seedRandomPick:
arg2: "লিস্ট"
_pick:
arg1: "লিস্ট"
_listLen:
arg1: "লিস্ট"
types:
array: "লিস্ট"
_notification:
youWereFollowed: "আপনাকে অনুসরণ করছে"
_types:
follow: "অনুসরণ করা হচ্ছে"
mention: "উল্লেখ"
renote: "রিনোট"
quote: "উদ্ধৃতি"
reaction: "প্রতিক্রিয়া"
_deck:
_columns:
notifications: "বিজ্ঞপ্তি"
tl: "টাইমলাইন"
antenna: "অ্যান্টেনা"
list: "লিস্ট"
mentions: "উল্লেখসমূহ"

View file

@ -235,6 +235,8 @@ resetAreYouSure: "Wirklich zurücksetzen?"
saved: "Gespeichert"
messaging: "Chat"
upload: "Hochladen"
keepOriginalUploading: "Originalbild speichern"
keepOriginalUploadingDescription: "Speichert das Originalbild so, wie es ist. Ist dies deaktiviert, wird eine Version zum Anzeigen im Internet generiert."
fromDrive: "Aus Drive"
fromUrl: "Von einer URL"
uploadFromUrl: "Von einer URL hochladen"

View file

@ -235,6 +235,8 @@ resetAreYouSure: "Really reset?"
saved: "Saved"
messaging: "Chat"
upload: "Upload"
keepOriginalUploading: "Keep original image"
keepOriginalUploadingDescription: "Saves the originally uploaded image as-is. If turned off, a version to display on the web will be generated on upload."
fromDrive: "From Drive"
fromUrl: "From URL"
uploadFromUrl: "Upload from a URL"

View file

@ -235,6 +235,7 @@ resetAreYouSure: "Voulez-vous réinitialiser ?"
saved: "Enregistré"
messaging: "Discuter"
upload: "Téléverser"
keepOriginalUploading: "Garder limage dorigine"
fromDrive: "Depuis le Drive"
fromUrl: "Depuis une URL"
uploadFromUrl: "Téléverser via une URL"
@ -743,6 +744,7 @@ notRecommended: "Déconseillé"
botProtection: "Protection contre les bots"
instanceBlocking: "Instances bloquées"
selectAccount: "Sélectionner un compte"
switchAccount: "Changer de compte"
enabled: "Activé"
disabled: "Désactivé"
quickAction: "Actions rapides"
@ -803,6 +805,7 @@ makeReactionsPublic: "Rendre les réactions publiques"
makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique."
classic: "Classique"
muteThread: "Mettre ce thread en sourdine"
unmuteThread: "Ne plus masquer le fil"
ffVisibility: "Visibilité des abonnés/abonnements"
ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent."
continueThread: "Afficher la suite du fil"
@ -1241,6 +1244,7 @@ _exportOrImport:
muteList: "Comptes masqués"
blockingList: "Comptes bloqués"
userLists: "Listes"
excludeMutingUsers: "Exclure les utilisateur·rice·s mis en sourdine"
excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs"
_charts:
federationInstancesIncDec: "Variation du nombre d'instances fédérées"

View file

@ -106,6 +106,7 @@ clickToShow: "클릭하여 보기"
sensitive: "열람주의"
add: "추가"
reaction: "리액션"
reactionSetting: "선택기에 표시할 리액션"
reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
rememberNoteVisibility: "공개 범위를 기억하기"
attachCancel: "첨부 취소"
@ -234,6 +235,8 @@ resetAreYouSure: "초기화 하시겠습니까?"
saved: "저장하였습니다"
messaging: "대화"
upload: "업로드"
keepOriginalUploading: "원본 이미지를 유지"
keepOriginalUploadingDescription: "이미지를 업로드할 때에 원본을 그대로 유지합니다. 비활성화하면 업로드할 때 브라우저에서 웹 공개용 이미지를 생성합니다."
fromDrive: "드라이브에서"
fromUrl: "URL로부터"
uploadFromUrl: "URL 업로드"
@ -446,6 +449,7 @@ uiLanguage: "UI 표시 언어"
groupInvited: "그룹에 초대되었습니다"
aboutX: "{x}에 대하여"
useOsNativeEmojis: "OS 기본 이모지를 사용"
disableDrawer: "드로어 메뉴를 사용하지 않기"
youHaveNoGroups: "그룹이 없습니다"
joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들어 보세요."
noHistory: "기록이 없습니다"
@ -617,8 +621,11 @@ reportAbuse: "신고"
reportAbuseOf: "{name}을 신고하기"
fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요."
abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
reporter: "신고자"
reporteeOrigin: "피신고자"
reporterOrigin: "신고자"
forwardReport: "리모트 인스턴스에도 신고 내용 보내기"
forwardReportIsAnonymous: "리모트 인스턴스에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다."
send: "전송"
abuseMarkAsResolved: "해결됨으로 표시"
openInNewTab: "새 탭에서 열기"
@ -680,6 +687,7 @@ center: "가운데"
wide: "넓게"
narrow: "좁게"
reloadToApplySetting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?"
needReloadToApply: "변경 사항은 새로고침하면 적용됩니다."
showTitlebar: "타이틀 바를 표시하기"
clearCache: "캐시 비우기"
onlineUsersCount: "{n}명이 접속 중"
@ -740,6 +748,7 @@ notRecommended: "추천하지 않음"
botProtection: "Bot 방어"
instanceBlocking: "인스턴스 차단"
selectAccount: "계정 선택"
switchAccount: "계정 바꾸기"
enabled: "활성화"
disabled: "비활성화"
quickAction: "빠른 동작"
@ -808,6 +817,11 @@ deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다.
incorrectPassword: "비밀번호가 올바르지 않습니다."
voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
hide: "숨기기"
leaveGroup: "그룹 나가기"
leaveGroupConfirm: "\"{name}\"에서 나갈까요?"
useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시"
welcomeBackWithName: "환영합니다, {name}님"
clickToFinishEmailVerification: "[{ok}]를 눌러 이메일 인증을 완료하세요."
_emailUnavailable:
used: "이 메일 주소는 사용중입니다"
format: "형식이 올바르지 않습니다"

View file

@ -235,6 +235,8 @@ resetAreYouSure: "恢复默认设置?"
saved: "已保存"
messaging: "聊天"
upload: "本地上传"
keepOriginalUploading: "保留原图"
keepOriginalUploadingDescription: "上传图片时保留原始图片。关闭时浏览器会在上传时生成一张用于web发布的图片。"
fromDrive: "从网盘中"
fromUrl: "从 URL"
uploadFromUrl: "从网址上传"
@ -619,8 +621,11 @@ reportAbuse: "举报"
reportAbuseOf: "举报{name}"
fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子请同时填写URL地址。"
abuseReported: "内容已发送。感谢您的报告。"
reporter: "报告者"
reporteeOrigin: "举报来源"
reporterOrigin: "举报者来源"
forwardReport: "将报告转发给远程实例"
forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。"
send: "发送"
abuseMarkAsResolved: "处理完毕"
openInNewTab: "在新标签页中打开"

View file

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "12.102.1",
"version": "12.103.1",
"codename": "indigo",
"repository": {
"type": "git",
@ -46,7 +46,7 @@
"@types/fluent-ffmpeg": "2.1.20",
"@typescript-eslint/parser": "5.10.0",
"cross-env": "7.0.3",
"cypress": "9.3.1",
"cypress": "9.4.1",
"start-server-and-test": "1.14.0",
"typescript": "4.5.5"
}

View file

@ -19,12 +19,11 @@
"@koa/cors": "3.1.0",
"@koa/multer": "3.0.0",
"@koa/router": "9.0.1",
"@sinonjs/fake-timers": "7.1.2",
"@sinonjs/fake-timers": "9.1.0",
"@syuilo/aiscript": "0.11.1",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.7",
"@types/cbor": "6.0.0",
"@types/dateformat": "3.0.1",
"@types/escape-regexp": "0.0.1",
"@types/glob": "7.2.0",
"@types/is-url": "1.2.30",
@ -43,7 +42,7 @@
"@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11",
"@types/mocha": "8.2.3",
"@types/node": "17.0.10",
"@types/node": "17.0.14",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.4",
"@types/oauth": "0.9.1",
@ -58,41 +57,39 @@
"@types/rename": "1.0.4",
"@types/request-stats": "3.0.0",
"@types/sanitize-html": "2.6.2",
"@types/seedrandom": "2.4.28",
"@types/seedrandom": "3.0.1",
"@types/sharp": "0.29.5",
"@types/sinonjs__fake-timers": "6.0.4",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/speakeasy": "2.0.7",
"@types/throttle-debounce": "2.1.0",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/uuid": "8.3.4",
"@types/web-push": "3.3.2",
"@types/webpack": "5.28.0",
"@types/webpack-stream": "3.2.12",
"@types/websocket": "1.0.4",
"@types/websocket": "1.0.5",
"@types/ws": "8.2.2",
"@typescript-eslint/eslint-plugin": "5.10.0",
"@typescript-eslint/parser": "5.10.0",
"@typescript-eslint/eslint-plugin": "5.10.2",
"@typescript-eslint/parser": "5.10.2",
"abort-controller": "3.0.0",
"archiver": "5.3.0",
"autobind-decorator": "2.4.0",
"autwh": "0.1.0",
"aws-sdk": "2.1061.0",
"aws-sdk": "2.1067.0",
"bcryptjs": "2.4.3",
"blurhash": "1.1.4",
"broadcast-channel": "4.9.0",
"bull": "4.2.1",
"bull": "4.5.0",
"cacheable-lookup": "6.0.4",
"cafy": "15.2.1",
"cbor": "8.1.0",
"chalk": "4.1.2",
"cli-highlight": "2.1.11",
"content-disposition": "0.5.4",
"crc-32": "1.2.0",
"dateformat": "4.5.1",
"crc-32": "1.2.1",
"date-fns": "2.28.0",
"deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1",
"eslint": "8.7.0",
"eslint": "8.8.0",
"eslint-plugin-import": "2.25.4",
"eventemitter3": "4.0.7",
"feed": "4.2.2",
@ -105,7 +102,7 @@
"ip-cidr": "3.0.4",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "16.7.0",
"jsdom": "19.0.0",
"json5": "2.2.0",
"json5-loader": "4.0.1",
"jsonld": "5.2.0",
@ -134,7 +131,7 @@
"pg": "8.7.1",
"portscanner": "2.2.0",
"private-ip": "2.3.3",
"probe-image-size": "7.2.2",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
"punycode": "2.1.1",
@ -153,14 +150,14 @@
"s-age": "1.1.2",
"sanitize-html": "2.6.1",
"seedrandom": "3.0.5",
"sharp": "0.29.3",
"sharp": "0.30.0",
"speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"style-loader": "3.3.1",
"summaly": "2.5.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.9.9",
"systeminformation": "5.11.0",
"throttle-debounce": "3.0.1",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",

View file

@ -5,9 +5,7 @@ import { URL } from 'url';
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
export function fromHtml(html: string, hashtagNames?: string[]): string | null {
if (html == null) return null;
export function fromHtml(html: string, hashtagNames?: string[]): string {
const dom = parse5.parseFragment(html);
let text = '';

View file

@ -24,14 +24,14 @@ const SHUTDOWN_TIMEOUT = 15000;
* down the process.
* @type {BeforeShutdownListener[]}
*/
const shutdownListeners = [];
const shutdownListeners: ((signalOrEvent: string) => void)[] = [];
/**
* Listen for signals and execute given `fn` function once.
* @param {string[]} signals System signals to listen to.
* @param {function(string)} fn Function to execute on shutdown.
*/
const processOnce = (signals, fn) => {
const processOnce = (signals: string[], fn: (signalOrEvent: string) => void) => {
for (const sig of signals) {
process.once(sig, fn);
}
@ -41,7 +41,7 @@ const processOnce = (signals, fn) => {
* Sets a forced shutdown mechanism that will exit the process after `timeout` milliseconds.
* @param {number} timeout Time to wait before forcing shutdown (milliseconds)
*/
const forceExitAfter = timeout => () => {
const forceExitAfter = (timeout: number) => () => {
setTimeout(() => {
// Force shutdown after timeout
console.warn(`Could not close resources gracefully after ${timeout}ms: forcing shutdown`);
@ -55,7 +55,7 @@ const forceExitAfter = timeout => () => {
* be logged out as a warning, but won't prevent other callbacks from executing.
* @param {string} signalOrEvent The exit signal or event name received on the process.
*/
async function shutdownHandler(signalOrEvent) {
async function shutdownHandler(signalOrEvent: string) {
if (process.env.NODE_ENV === 'test') return process.exit(0);
console.warn(`Shutting down: received [${signalOrEvent}] signal`);
@ -64,7 +64,9 @@ async function shutdownHandler(signalOrEvent) {
try {
await listener(signalOrEvent);
} catch (err) {
console.warn(`A shutdown handler failed before completing with: ${err.message || err}`);
if (err instanceof Error) {
console.warn(`A shutdown handler failed before completing with: ${err.message || err}`);
}
}
}
@ -78,7 +80,7 @@ async function shutdownHandler(signalOrEvent) {
* @param {BeforeShutdownListener} listener The shutdown listener to register.
* @returns {BeforeShutdownListener} Echoes back the supplied `listener`.
*/
export function beforeShutdown(listener) {
export function beforeShutdown(listener: () => void) {
shutdownListeners.push(listener);
return listener;
}

View file

@ -38,7 +38,9 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
https: httpsAgent,
},
http2: false, // default
retry: 0,
retry: {
limit: 0,
},
}).on('response', (res: Got.Response) => {
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) {
if (isPrivateIp(res.ip)) {
@ -75,7 +77,7 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
logger.succ(`Download finished: ${chalk.cyan(url)}`);
}
function isPrivateIp(ip: string) {
function isPrivateIp(ip: string): boolean {
for (const net of config.allowedPrivateNetworks || []) {
const cidr = new IPCIDR(net);
if (cidr.contains(ip)) {

View file

@ -39,7 +39,7 @@ const sideN = Math.floor(n / 2);
*/
export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
const rand = gen.create(seed);
const canvas = p.make(size, size);
const canvas = p.make(size, size, undefined);
const ctx = canvas.getContext('2d');
ctx.fillStyle = bg;

View file

@ -1,3 +1,3 @@
export function isDuplicateKeyValueError(e: Error): boolean {
return e.message.startsWith('duplicate key value');
export function isDuplicateKeyValueError(e: unknown | Error): boolean {
return (e as any).message && (e as Error).message.startsWith('duplicate key value');
}

View file

@ -12,7 +12,7 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
return await awaitAll({
id: report.id,
createdAt: report.createdAt,
createdAt: report.createdAt.toISOString(),
comment: report.comment,
resolved: report.resolved,
reporterId: report.reporterId,

View file

@ -12,7 +12,7 @@ export class ModerationLogRepository extends Repository<ModerationLog> {
return await awaitAll({
id: log.id,
createdAt: log.createdAt,
createdAt: log.createdAt.toISOString(),
type: log.type,
info: log.info,
userId: log.userId,

View file

@ -13,7 +13,7 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> {
return {
id: favorite.id,
createdAt: favorite.createdAt,
createdAt: favorite.createdAt.toISOString(),
noteId: favorite.noteId,
note: await Notes.pack(favorite.note || favorite.noteId, me),
};

View file

@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import { addFile } from '@/services/drive/add-file';
import * as dateFormat from 'dateformat';
import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host';
import { Users, Blockings } from '@/models/index';
import { MoreThan } from 'typeorm';
@ -85,7 +85,7 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);

View file

@ -7,7 +7,7 @@ const mime = require('mime-types');
const archiver = require('archiver');
import { queueLogger } from '../../logger';
import { addFile } from '@/services/drive/add-file';
import * as dateFormat from 'dateformat';
import { format as dateFormat } from 'date-fns';
import { Users, Emojis } from '@/models/index';
import { } from '@/queue/types';
import { downloadUrl } from '@/misc/download-url';
@ -110,7 +110,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
archiveStream.on('close', async () => {
logger.succ(`Exported to: ${archivePath}`);
const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.zip';
const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip';
const driveFile = await addFile({ user, path: archivePath, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);

View file

@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import { addFile } from '@/services/drive/add-file';
import * as dateFormat from 'dateformat';
import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host';
import { Users, Followings, Mutings } from '@/models/index';
import { In, MoreThan, Not } from 'typeorm';
@ -86,7 +86,7 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);

View file

@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import { addFile } from '@/services/drive/add-file';
import * as dateFormat from 'dateformat';
import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host';
import { Users, Mutings } from '@/models/index';
import { MoreThan } from 'typeorm';
@ -85,7 +85,7 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);

View file

@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import { addFile } from '@/services/drive/add-file';
import * as dateFormat from 'dateformat';
import { format as dateFormat } from 'date-fns';
import { Users, Notes, Polls } from '@/models/index';
import { MoreThan } from 'typeorm';
import { Note } from '@/models/entities/note';
@ -94,7 +94,7 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);

View file

@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import { addFile } from '@/services/drive/add-file';
import * as dateFormat from 'dateformat';
import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host';
import { Users, UserLists, UserListJoinings } from '@/models/index';
import { In } from 'typeorm';
@ -62,7 +62,7 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any):
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);

View file

@ -41,7 +41,9 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
fs.writeFileSync(destPath, '', 'binary');
await downloadUrl(file.url, destPath);
} catch (e) { // TODO: 何度か再試行
logger.error(e);
if (e instanceof Error || typeof e === 'string') {
logger.error(e);
}
throw e;
}

View file

@ -51,7 +51,6 @@ export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done:
createdAt: new Date(),
userId: user.id,
name: listName,
userIds: [],
}).then(x => UserLists.findOneOrFail(x.identifiers[0]));
}
@ -67,9 +66,9 @@ export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done:
target = await resolveUser(username, host);
}
if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue;
if (await UserListJoinings.findOne({ userListId: list!.id, userId: target.id }) != null) continue;
pushUserToUserList(target, list);
pushUserToUserList(target, list!);
} catch (e) {
logger.warn(`Error in line:${linenum} ${e}`);
}

View file

@ -54,10 +54,12 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
} catch (e) {
// 対象が4xxならスキップ
if (e instanceof StatusError && e.isClientError) {
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
if (e instanceof StatusError) {
if (e.isClientError) {
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
}
throw `Error in actor ${activity.actor} - ${e.statusCode || e}`;
}
throw `Error in actor ${activity.actor} - ${e.statusCode || e}`;
}
}

View file

@ -42,11 +42,14 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
renote = await resolveNote(targetUri);
} catch (e) {
// 対象が4xxならスキップ
if (e instanceof StatusError && e.isClientError) {
logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
return;
if (e instanceof StatusError) {
if (e.isClientError) {
logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
return;
}
logger.warn(`Error in announce target ${targetUri} - ${e.statusCode || e}`);
}
logger.warn(`Error in announce target ${targetUri} - ${e.statusCode || e}`);
throw e;
}

View file

@ -10,7 +10,7 @@ export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => {
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
const uris = getApIds(activity.object);
const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop());
const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!);
const users = await Users.find({
id: In(userIds),
});

View file

@ -25,8 +25,10 @@ export async function performActivity(actor: IRemoteUser, activity: IObject) {
const act = await resolver.resolve(item);
try {
await performOneActivity(actor, act);
} catch (e) {
apLogger.error(e);
} catch (err) {
if (err instanceof Error || typeof err === 'string') {
apLogger.error(err);
}
}
}
} else {

View file

@ -24,7 +24,7 @@ export class LdSignature {
} as {
type: string;
creator: string;
domain: string;
domain?: string;
nonce: string;
created: string;
};
@ -114,7 +114,7 @@ export class LdSignature {
Accept: 'application/ld+json, application/json',
},
timeout: this.loderTimeout,
agent: u => u.protocol == 'http:' ? httpAgent : httpsAgent,
agent: u => u.protocol === 'http:' ? httpAgent : httpsAgent,
}).then(res => {
if (!res.ok) {
throw `${res.status} ${res.statusText}`;

View file

@ -11,7 +11,7 @@ import { In } from 'typeorm';
import { Emoji } from '@/models/entities/emoji';
import { Poll } from '@/models/entities/poll';
export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<any> {
export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<Record<string, unknown>> {
const getPromisedFiles = async (ids: string[]) => {
if (!ids || ids.length === 0) return [];
const items = await DriveFiles.find({ id: In(ids) });

View file

@ -6,7 +6,14 @@
* @param last URL of last page (optional)
* @param orderedItems attached objects (optional)
*/
export default function(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: Record<string, unknown>) {
export default function(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: Record<string, unknown>[]): {
id: string | null;
type: 'OrderedCollection';
totalItems: any;
first?: string;
last?: string;
orderedItems?: Record<string, unknown>[];
} {
const page: any = {
id,
type: 'OrderedCollection',

View file

@ -32,7 +32,7 @@ export default async (ctx: Router.RouterContext) => {
const rendered = renderOrderedCollection(
`${config.url}/users/${userId}/collections/featured`,
renderedNotes.length, undefined, undefined, renderedNotes
renderedNotes.length, undefined, undefined, renderedNotes,
);
ctx.body = renderActivity(rendered);

View file

@ -32,7 +32,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
// Authentication
authenticate(body['i']).then(([user, app]) => {
// API invoking
call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => {
call(endpoint.name, user, app, body, ctx).then((res: any) => {
reply(res);
}).catch((e: ApiError) => {
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);

View file

@ -1,3 +1,4 @@
import * as Koa from 'koa';
import { performance } from 'perf_hooks';
import { limiter } from './limiter';
import { User } from '@/models/entities/user';
@ -12,7 +13,7 @@ const accessDenied = {
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e',
};
export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => {
export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
const isSecure = user != null && token == null;
const ep = endpoints.find(e => e.name === endpoint);
@ -76,9 +77,20 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
});
}
// Cast non JSON input
if (ep.meta.requireFile && ep.meta.params) {
const body = (ctx!.request as any).body;
for (const k of Object.keys(ep.meta.params)) {
const param = ep.meta.params[k];
if (['Boolean', 'Number'].includes(param.validator.name) && typeof body[k] === 'string') {
body[k] = JSON.parse(body[k]);
}
}
}
// API invoking
const before = performance.now();
return await ep.exec(data, user, token, file).catch((e: Error) => {
return await ep.exec(data, user, token, ctx?.file).catch((e: Error) => {
if (e instanceof ApiError) {
throw e;
} else {

View file

@ -36,9 +36,9 @@ export default define(meta, async (ps, me) => {
if (ps.forward && report.targetUserHost != null) {
const actor = await getInstanceActor();
const targetUser = await Users.findOne(report.targetUserId);
const targetUser = await Users.findOneOrFail(report.targetUserId);
deliver(actor, renderActivity(renderFlag(actor, [targetUser.uri], report.comment)), targetUser.inbox);
deliver(actor, renderActivity(renderFlag(actor, [targetUser.uri!], report.comment)), targetUser.inbox);
}
await AbuseUserReports.update(report.id, {

View file

@ -18,144 +18,6 @@ export const meta = {
res: {
type: 'object',
nullable: false, optional: false,
properties: {
id: {
type: 'string',
nullable: false, optional: false,
format: 'id',
},
createdAt: {
type: 'string',
nullable: false, optional: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
nullable: true, optional: false,
format: 'date-time',
},
lastFetchedAt: {
type: 'string',
nullable: true, optional: false,
},
username: {
type: 'string',
nullable: false, optional: false,
},
name: {
type: 'string',
nullable: true, optional: false,
},
folowersCount: {
type: 'number',
nullable: false, optional: true,
},
followingCount: {
type: 'number',
nullable: false, optional: false,
},
notesCount: {
type: 'number',
nullable: false, optional: false,
},
avatarId: {
type: 'string',
nullable: true, optional: false,
},
bannerId: {
type: 'string',
nullable: true, optional: false,
},
tags: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
},
},
avatarUrl: {
type: 'string',
nullable: true, optional: false,
format: 'url',
},
bannerUrl: {
type: 'string',
nullable: true, optional: false,
format: 'url',
},
avatarBlurhash: {
type: 'any',
nullable: true, optional: false,
default: null,
},
bannerBlurhash: {
type: 'any',
nullable: true, optional: false,
default: null,
},
isSuspended: {
type: 'boolean',
nullable: false, optional: false,
},
isSilenced: {
type: 'boolean',
nullable: false, optional: false,
},
isLocked: {
type: 'boolean',
nullable: false, optional: false,
},
isBot: {
type: 'boolean',
nullable: false, optional: false,
},
isCat: {
type: 'boolean',
nullable: false, optional: false,
},
isAdmin: {
type: 'boolean',
nullable: false, optional: false,
},
isModerator: {
type: 'boolean',
nullable: false, optional: false,
},
emojis: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
},
},
host: {
type: 'string',
nullable: true, optional: false,
},
inbox: {
type: 'string',
nullable: true, optional: false,
},
sharedInbox: {
type: 'string',
nullable: true, optional: false,
},
featured: {
type: 'string',
nullable: true, optional: false,
},
uri: {
type: 'string',
nullable: true, optional: false,
},
token: {
type: 'string',
nullable: true, optional: false,
default: '<MASKED>',
},
},
},
} as const;

View file

@ -89,5 +89,9 @@ export default define(meta, async (ps, user) => {
}
}
return ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements;
return (ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements).map((a) => ({
...a,
createdAt: a.createdAt.toISOString(),
updatedAt: a.updatedAt?.toISOString() ?? null,
}));
});

View file

@ -39,15 +39,13 @@ export const meta = {
},
isSensitive: {
validator: $.optional.either($.bool, $.str),
validator: $.optional.bool,
default: false,
transform: (v: any): boolean => v === true || v === 'true',
},
force: {
validator: $.optional.either($.bool, $.str),
validator: $.optional.bool,
default: false,
transform: (v: any): boolean => v === true || v === 'true',
},
},
@ -88,7 +86,9 @@ export default define(meta, async (ps, user, _, file, cleanup) => {
const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive });
return await DriveFiles.pack(driveFile, { self: true });
} catch (e) {
apiLogger.error(e);
if (e instanceof Error || typeof e === 'string') {
apiLogger.error(e);
}
throw new ApiError();
} finally {
cleanup!();

View file

@ -6,6 +6,7 @@ import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
import { Followings, Users } from '@/models/index';
import { IdentifiableError } from '@/misc/identifiable-error';
export const meta = {
tags: ['following', 'users'],
@ -92,8 +93,10 @@ export default define(meta, async (ps, user) => {
try {
await create(follower, followee);
} catch (e) {
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking);
if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked);
if (e instanceof IdentifiableError) {
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking);
if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked);
}
throw e;
}

View file

@ -5,6 +5,7 @@ import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';
import { Users } from '@/models/index';
import { IdentifiableError } from '@/misc/identifiable-error';
export const meta = {
tags: ['following', 'account'],
@ -51,7 +52,9 @@ export default define(meta, async (ps, user) => {
try {
await cancelFollowRequest(followee, user);
} catch (e) {
if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound);
if (e instanceof IdentifiableError) {
if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound);
}
throw e;
}

View file

@ -114,4 +114,6 @@ export default define(meta, async (ps, me) => {
return await Users.packMany(users, me, { detail: !!ps.detail });
}
return [];
});

View file

@ -11,18 +11,18 @@ import { fetchMeta } from '@/misc/fetch-meta';
import { Users, UserProfiles } from '@/models/index';
import { ILocalUser } from '@/models/entities/user';
function getUserToken(ctx: Koa.Context) {
function getUserToken(ctx: Koa.BaseContext): string | null {
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
}
function compareOrigin(ctx: Koa.Context) {
function normalizeUrl(url: string) {
function compareOrigin(ctx: Koa.BaseContext): boolean {
function normalizeUrl(url?: string): string {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
}
const referer = ctx.headers['referer'];
return (normalizeUrl(referer) == normalizeUrl(config.url));
return (normalizeUrl(referer) === normalizeUrl(config.url));
}
// Init router

View file

@ -11,18 +11,18 @@ import { fetchMeta } from '@/misc/fetch-meta';
import { Users, UserProfiles } from '@/models/index';
import { ILocalUser } from '@/models/entities/user';
function getUserToken(ctx: Koa.Context) {
function getUserToken(ctx: Koa.BaseContext): string | null {
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
}
function compareOrigin(ctx: Koa.Context) {
function normalizeUrl(url: string) {
function compareOrigin(ctx: Koa.BaseContext): boolean {
function normalizeUrl(url?: string): string {
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
}
const referer = ctx.headers['referer'];
return (normalizeUrl(referer) == normalizeUrl(config.url));
return (normalizeUrl(referer) === normalizeUrl(config.url));
}
// Init router

View file

@ -10,18 +10,18 @@ import { fetchMeta } from '@/misc/fetch-meta';
import { Users, UserProfiles } from '@/models/index';
import { ILocalUser } from '@/models/entities/user';
function getUserToken(ctx: Koa.Context) {
function getUserToken(ctx: Koa.BaseContext): string | null {
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
}
function compareOrigin(ctx: Koa.Context) {
function normalizeUrl(url: string) {
return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
function compareOrigin(ctx: Koa.BaseContext): boolean {
function normalizeUrl(url?: string): string {
return url == null ? '' : url.endsWith('/') ? url.substr(0, url.length - 1) : url;
}
const referer = ctx.headers['referer'];
return (normalizeUrl(referer) == normalizeUrl(config.url));
return (normalizeUrl(referer) === normalizeUrl(config.url));
}
// Init router

View file

@ -105,7 +105,10 @@ export interface NoteStreamTypes {
};
reacted: {
reaction: string;
emoji?: Emoji;
emoji?: {
name: string;
url: string;
} | null;
userId: User['id'];
};
unreacted: {

View file

@ -59,7 +59,7 @@ module.exports = (server: http.Server) => {
});
connection.on('message', async (data) => {
if (data.utf8Data === 'ping') {
if (data.type === 'utf8' && data.utf8Data === 'ping') {
connection.send('pong');
}
});

View file

@ -11,6 +11,11 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const';
export async function proxyMedia(ctx: Koa.Context) {
const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
if (typeof url !== 'string') {
ctx.status = 400;
return;
}
// Create temp file
const [path, cleanup] = await createTemp();

View file

@ -9,22 +9,34 @@ import { getJson } from '@/misc/fetch';
const logger = new Logger('url-preview');
module.exports = async (ctx: Koa.Context) => {
const url = ctx.query.url;
if (typeof url !== 'string') {
ctx.status = 400;
return;
}
const lang = ctx.query.lang;
if (Array.isArray(lang)) {
ctx.status = 400;
return;
}
const meta = await fetchMeta();
logger.info(meta.summalyProxy
? `(Proxy) Getting preview of ${ctx.query.url}@${ctx.query.lang} ...`
: `Getting preview of ${ctx.query.url}@${ctx.query.lang} ...`);
? `(Proxy) Getting preview of ${url}@${lang} ...`
: `Getting preview of ${url}@${lang} ...`);
try {
const summary = meta.summalyProxy ? await getJson(`${meta.summalyProxy}?${query({
url: ctx.query.url,
lang: ctx.query.lang || 'ja-JP',
})}`) : await summaly(ctx.query.url, {
url: url,
lang: lang ?? 'ja-JP',
})}`) : await summaly(url, {
followRedirects: false,
lang: ctx.query.lang || 'ja-JP',
lang: lang ?? 'ja-JP',
});
logger.succ(`Got preview of ${ctx.query.url}: ${summary.title}`);
logger.succ(`Got preview of ${url}: ${summary.title}`);
summary.icon = wrap(summary.icon);
summary.thumbnail = wrap(summary.thumbnail);
@ -33,8 +45,8 @@ module.exports = async (ctx: Koa.Context) => {
ctx.set('Cache-Control', 'max-age=604800, immutable');
ctx.body = summary;
} catch (e) {
logger.warn(`Failed to get preview of ${ctx.query.url}: ${e}`);
} catch (err) {
logger.warn(`Failed to get preview of ${url}: ${err}`);
ctx.status = 200;
ctx.set('Cache-Control', 'max-age=86400, immutable');
ctx.body = '{}';

View file

@ -21,6 +21,7 @@ html
meta(name='referrer' content='origin')
meta(name='theme-color' content='#86b300')
meta(name='theme-color-orig' content='#86b300')
meta(property='twitter:card' content='summary')
meta(property='og:site_name' content= instanceName || 'Misskey')
meta(name='viewport' content='width=device-width, initial-scale=1')
link(rel='icon' href= icon || '/favicon.ico')
@ -42,7 +43,9 @@ html
block meta
block og
meta(property='og:image' content=img)
meta(property='og:title' content= title || 'Misskey')
meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
meta(property='og:image' content= img)
style
include ../style.css

View file

@ -16,6 +16,3 @@ block og
meta(property='og:description' content= channel.description)
meta(property='og:url' content= url)
meta(property='og:image' content= channel.bannerUrl)
block meta
meta(name='twitter:card' content='summary')

View file

@ -26,8 +26,6 @@ block meta
meta(name='misskey:user-id' content=user.id)
meta(name='misskey:clip-id' content=clip.id)
meta(name='twitter:card' content='summary')
// todo
if user.twitter
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)

View file

@ -25,8 +25,6 @@ block meta
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)
meta(name='twitter:card' content='summary')
// todo
if user.twitter
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)

View file

@ -26,9 +26,7 @@ block meta
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)
meta(name='misskey:note-id' content=note.id)
meta(name='twitter:card' content='summary')
// todo
if user.twitter
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)

View file

@ -26,8 +26,6 @@ block meta
meta(name='misskey:user-id' content=user.id)
meta(name='misskey:page-id' content=page.id)
meta(name='twitter:card' content='summary')
// todo
if user.twitter
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)

View file

@ -25,8 +25,6 @@ block meta
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)
meta(name='twitter:card' content='summary')
if profile.twitter
meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)

View file

@ -160,8 +160,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
webpublic: null,
thumbnail,
};
} catch (e) {
logger.warn(`GenerateVideoThumbnail failed: ${e}`);
} catch (err) {
logger.warn(`GenerateVideoThumbnail failed: ${err}`);
return {
webpublic: null,
thumbnail: null,
@ -199,8 +199,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
metadata.width && metadata.width <= 2048 &&
metadata.height && metadata.height <= 2048
);
} catch (e) {
logger.warn(`sharp failed: ${e}`);
} catch (err) {
logger.warn(`sharp failed: ${err}`);
return {
webpublic: null,
thumbnail: null,
@ -221,8 +221,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
} else {
logger.debug(`web image not created (not an required image)`);
}
} catch (e) {
logger.warn(`web image not created (an error occured)`, e);
} catch (err) {
logger.warn(`web image not created (an error occured)`, err as Error);
}
} else {
if (satisfyWebpublic) logger.info(`web image not created (original satisfies webpublic)`);
@ -239,8 +239,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
} else {
logger.debug(`thumbnail not created (not an required file)`);
}
} catch (e) {
logger.warn(`thumbnail not created (an error occured)`, e);
} catch (err) {
logger.warn(`thumbnail not created (an error occured)`, err as Error);
}
// #endregion thumbnail
@ -456,9 +456,9 @@ export async function addFile({
file.storedInternal = false;
file = await DriveFiles.insert(file).then(x => DriveFiles.findOneOrFail(x.identifiers[0]));
} catch (e) {
} catch (err) {
// duplicate key error (when already registered)
if (isDuplicateKeyValueError(e)) {
if (isDuplicateKeyValueError(err)) {
logger.info(`already registered ${file.uri}`);
file = await DriveFiles.findOne({
@ -466,8 +466,8 @@ export async function addFile({
userId: user ? user.id : null,
}) as DriveFile;
} else {
logger.error(e);
throw e;
logger.error(err as Error);
throw err;
}
}
} else {

View file

@ -1,6 +1,6 @@
import * as cluster from 'cluster';
import * as chalk from 'chalk';
import * as dateformat from 'dateformat';
import { format as dateFormat } from 'date-fns';
import { envOption } from '../env';
import config from '@/config/index';
@ -57,7 +57,7 @@ export default class Logger {
return;
}
const time = dateformat(new Date(), 'HH:MM:ss');
const time = dateFormat(new Date(), 'HH:mm:ss');
const worker = cluster.isPrimary ? '*' : cluster.worker.id;
const l =
level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
@ -116,7 +116,7 @@ export default class Logger {
}
public debug(message: string, data?: Record<string, any> | null, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報)
if (process.env.NODE_ENV != 'production' || envOption.verbose) {
if (process.env.NODE_ENV !== 'production' || envOption.verbose) {
this.log('debug', message, data, important);
}
}

View file

@ -59,7 +59,7 @@ class NotificationManager {
if (exist) {
// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
if (reason != 'mention') {
if (reason !== 'mention') {
exist.reason = reason;
}
} else {
@ -201,7 +201,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
mentionedUsers.push(await Users.findOneOrFail(data.reply.userId));
}
if (data.visibility == 'specified') {
if (data.visibility === 'specified') {
if (data.visibleUsers == null) throw new Error('invalid param');
for (const u of data.visibleUsers) {
@ -301,7 +301,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
if (Users.isRemoteUser(user)) activeUsersChart.update(user);
// 未読通知を作成
if (data.visibility == 'specified') {
if (data.visibility === 'specified') {
if (data.visibleUsers == null) throw new Error('invalid param');
for (const u of data.visibleUsers) {
@ -439,7 +439,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
async function renderNoteOrRenoteActivity(data: Option, note: Note) {
if (data.localOnly) return null;
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)
? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note)
: renderCreate(await renderNote(note, false), note);
@ -478,7 +478,7 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
userId: user.id,
localOnly: data.localOnly!,
visibility: data.visibility as any,
visibleUserIds: data.visibility == 'specified'
visibleUserIds: data.visibility === 'specified'
? data.visibleUsers
? data.visibleUsers.map(u => u.id)
: []
@ -502,7 +502,7 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
insert.mentions = mentionedUsers.map(u => u.id);
const profiles = await UserProfiles.find({ userId: In(insert.mentions) });
insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => {
const profile = profiles.find(p => p.userId == u.id);
const profile = profiles.find(p => p.userId === u.id);
const url = profile != null ? profile.url : null;
return {
uri: u.uri,

View file

@ -39,7 +39,7 @@ export default async function(user: User, note: Note, quiet = false) {
let renote: Note | undefined;
// if deletd note is renote
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) {
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
renote = await Notes.findOne({
id: note.renoteId,
});

View file

@ -76,7 +76,7 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
// カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = decodeReaction(reaction);
let emoji = await Emojis.findOne({
const emoji = await Emojis.findOne({
where: {
name: decodedReaction.name,
host: decodedReaction.host,

View file

@ -52,7 +52,7 @@ export default async function(
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
if (await checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) {
if (await checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) {
readAntennaNotes.push(note);
}
}

View file

@ -114,9 +114,9 @@ export async function sendEmail(to: string, subject: string, html: string, text:
</html>`,
});
logger.info('Message sent: %s', info.messageId);
} catch (e) {
logger.error(e);
throw e;
logger.info(`Message sent: ${info.messageId}`);
} catch (err) {
logger.error(err as Error);
throw err;
}
}

View file

@ -30,7 +30,7 @@
"outDir": "./built",
"typeRoots": [
"./node_modules/@types",
"./@types"
"./src/@types"
],
"lib": [
"esnext"

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,7 @@ module.exports = {
// data の禁止理由: 抽象的すぎるため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
"id-denylist": ["error", "window", "data", "e"],
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
"vue/attributes-order": ["error", {
"alphabetical": false
}],

View file

@ -12,7 +12,6 @@
"dependencies": {
"@discordapp/twemoji": "13.1.0",
"@syuilo/aiscript": "0.11.1",
"@types/dateformat": "3.0.1",
"@types/escape-regexp": "0.0.1",
"@types/glob": "7.2.0",
"@types/gulp": "4.0.9",

View file

@ -0,0 +1,51 @@
<template>
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')">
<div v-if="title" class="qpcyisrl">
<div class="title">{{ title }}</div>
<div v-for="x in series" class="series">
<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span>
<span>{{ x.text }}</span>
</div>
</div>
</MkTooltip>
</template>
<script lang="ts" setup>
import { } from 'vue';
import MkTooltip from './ui/tooltip.vue';
const props = defineProps<{
showing: boolean;
x: number;
y: number;
title: string;
series: {
backgroundColor: string;
borderColor: string;
text: string;
}[];
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
</script>
<style lang="scss" scoped>
.qpcyisrl {
> .title {
margin-bottom: 4px;
}
> .series {
> .color {
display: inline-block;
width: 8px;
height: 8px;
border-width: 1px;
border-style: solid;
margin-right: 8px;
}
}
}
</style>

View file

@ -8,7 +8,7 @@
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue';
import {
Chart,
ArcElement,
@ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale';
import zoomPlugin from 'chartjs-plugin-zoom';
import * as os from '@/os';
import { defaultStore } from '@/store';
import MkChartTooltip from '@/components/chart-tooltip.vue';
Chart.register(
ArcElement,
@ -94,6 +95,11 @@ export default defineComponent({
required: false,
default: false
},
bar: {
type: Boolean,
required: false,
default: false
},
aspectRatio: {
type: Number,
required: false,
@ -137,6 +143,43 @@ export default defineComponent({
}));
};
const tooltipShowing = ref(false);
const tooltipX = ref(0);
const tooltipY = ref(0);
const tooltipTitle = ref(null);
const tooltipSeries = ref(null);
let disposeTooltipComponent;
os.popup(MkChartTooltip, {
showing: tooltipShowing,
x: tooltipX,
y: tooltipY,
title: tooltipTitle,
series: tooltipSeries,
}, {}).then(({ dispose }) => {
disposeTooltipComponent = dispose;
});
function externalTooltipHandler(context) {
if (context.tooltip.opacity === 0) {
tooltipShowing.value = false;
return;
}
tooltipTitle.value = context.tooltip.title[0];
tooltipSeries.value = context.tooltip.body.map((b, i) => ({
backgroundColor: context.tooltip.labelColors[i].backgroundColor,
borderColor: context.tooltip.labelColors[i].borderColor,
text: b.lines[0],
}));
const rect = context.chart.canvas.getBoundingClientRect();
tooltipShowing.value = true;
tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
}
const render = () => {
if (chartInstance) {
chartInstance.destroy();
@ -149,7 +192,7 @@ export default defineComponent({
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
chartInstance = new Chart(chartEl.value, {
type: 'line',
type: props.bar ? 'bar' : 'line',
data: {
labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
datasets: data.series.map((x, i) => ({
@ -157,12 +200,13 @@ export default defineComponent({
label: x.name,
data: x.data.slice().reverse(),
pointRadius: 0,
tension: 0,
borderWidth: 2,
borderColor: x.color ? x.color : getColor(i),
borderDash: x.borderDash || [],
borderJoinStyle: 'round',
backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
barPercentage: 0.9,
categoryPercentage: 0.9,
fill: x.type === 'area',
hidden: !!x.hidden,
})),
@ -180,6 +224,7 @@ export default defineComponent({
scales: {
x: {
type: 'time',
stacked: props.stacked,
time: {
stepSize: 1,
unit: props.span === 'day' ? 'month' : 'day',
@ -212,7 +257,15 @@ export default defineComponent({
},
interaction: {
intersect: false,
mode: 'index',
},
elements: {
point: {
hoverRadius: 5,
hoverBorderWidth: 2,
},
},
animation: false,
plugins: {
legend: {
display: props.detailed,
@ -222,10 +275,12 @@ export default defineComponent({
},
},
tooltip: {
enabled: false,
mode: 'index',
animation: {
duration: 0,
},
external: externalTooltipHandler,
},
zoom: {
pan: {
@ -640,6 +695,21 @@ export default defineComponent({
};
};
const fetchPerUserDriveChart = async (): Promise<typeof data> => {
const raw = await os.api('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span });
return {
series: [{
name: 'Inc',
type: 'area',
data: format(raw.incSize),
}, {
name: 'Dec',
type: 'area',
data: format(raw.decSize),
}],
};
};
const fetchAndRender = async () => {
const fetchData = () => {
switch (props.src) {
@ -670,6 +740,7 @@ export default defineComponent({
case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);
case 'per-user-notes': return fetchPerUserNotesChart();
case 'per-user-drive': return fetchPerUserDriveChart();
}
};
fetching.value = true;
@ -684,6 +755,10 @@ export default defineComponent({
fetchAndRender();
});
onUnmounted(() => {
if (disposeTooltipComponent) disposeTooltipComponent();
});
return {
chartEl,
fetching,

View file

@ -117,7 +117,7 @@ export default defineComponent({
text: computed(() => {
return props.textConverter(finalValue.value);
}),
source: thumbEl,
targetElement: thumbEl,
}, {}, 'closed');
const style = document.createElement('style');

View file

@ -23,8 +23,9 @@ const props = withDefaults(defineProps<{
behavior: null,
});
const navHook = inject('navHook', null);
const sideViewHook = inject('sideViewHook', null);
type Navigate = (path: string, record?: boolean) => void;
const navHook = inject<null | Navigate>('navHook', null);
const sideViewHook = inject<null | Navigate>('sideViewHook', null);
const active = $computed(() => {
if (props.activeClass == null) return false;

View file

@ -153,7 +153,7 @@ export default defineComponent({
showing,
reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
emojis: props.notification.note.emojis,
source: reactionRef.value.$el,
targetElement: reactionRef.value.$el,
}, {}, 'closed');
});

View file

@ -136,7 +136,10 @@ let showPreview = $ref(false);
let cw = $ref<string | null>(null);
let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]);
let visibleUsers = $ref(props.initialVisibleUsers ?? []);
let visibleUsers = $ref([]);
if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(pushVisibleUser);
}
let autocomplete = $ref(null);
let draghover = $ref(false);
let quoteId = $ref(null);
@ -263,12 +266,12 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
os.api('users/show', {
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId)
}).then(users => {
visibleUsers.push(...users);
users.forEach(pushVisibleUser);
});
if (props.reply.userId !== $i.id) {
os.api('users/show', { userId: props.reply.userId }).then(user => {
visibleUsers.push(user);
pushVisibleUser(user);
});
}
}
@ -276,7 +279,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
if (props.specified) {
visibility = 'specified';
visibleUsers.push(props.specified);
pushVisibleUser(props.specified);
}
// keep cw when reply
@ -398,9 +401,15 @@ function setVisibility() {
}, 'closed');
}
function pushVisibleUser(user) {
if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
visibleUsers.push(user);
}
}
function addVisibleUser() {
os.selectUser().then(user => {
visibleUsers.push(user);
pushVisibleUser(user);
});
}

View file

@ -1,5 +1,5 @@
<template>
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
<MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="beeadbfb">
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
<div class="name">{{ reaction.replace('@.', '') }}</div>
@ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue';
const props = defineProps<{
reaction: string;
emojis: any[]; // TODO
source: any; // TODO
targetElement: HTMLElement;
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
</script>

View file

@ -1,5 +1,5 @@
<template>
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
<MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="bqxuuuey">
<div class="reaction">
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
@ -26,11 +26,11 @@ const props = defineProps<{
users: any[]; // TODO
count: number;
emojis: any[]; // TODO
source: any; // TODO
targetElement: HTMLElement;
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
</script>

View file

@ -101,7 +101,7 @@ export default defineComponent({
emojis: props.note.emojis,
users,
count: props.count,
source: buttonRef.value
targetElement: buttonRef.value,
}, {}, 'closed');
});

View file

@ -52,7 +52,7 @@ export default defineComponent({
showing,
users,
count: props.count,
source: buttonRef.value
targetElement: buttonRef.value
}, {}, 'closed');
});

View file

@ -1,5 +1,5 @@
<template>
<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')">
<MkTooltip ref="tooltip" :target-element="targetElement" :max-width="250" @closed="emit('closed')">
<div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user">
<MkAvatar class="avatar" :user="u"/>
@ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue';
const props = defineProps<{
users: any[]; // TODO
count: number;
source: any; // TODO
targetElement: HTMLElement;
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
</script>

View file

@ -12,9 +12,11 @@ import * as os from '@/os';
const props = withDefaults(defineProps<{
showing: boolean;
source: HTMLElement;
targetElement?: HTMLElement;
x?: number;
y?: number;
text?: string;
maxWidth?; number;
maxWidth?: number;
}>(), {
maxWidth: 250,
});
@ -29,13 +31,25 @@ const zIndex = os.claimZIndex('high');
const setPosition = () => {
if (el.value == null) return;
const rect = props.source.getBoundingClientRect();
const contentWidth = el.value.offsetWidth;
const contentHeight = el.value.offsetHeight;
let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2);
let top = rect.top + window.pageYOffset - contentHeight;
let left: number;
let top: number;
let rect: DOMRect;
if (props.targetElement) {
rect = props.targetElement.getBoundingClientRect();
left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
top = rect.top + window.pageYOffset - contentHeight;
el.value.style.transformOrigin = 'center bottom';
} else {
left = props.x;
top = props.y - contentHeight;
}
left -= (el.value.offsetWidth / 2);
@ -43,9 +57,14 @@ const setPosition = () => {
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
}
//
if (top - window.pageYOffset < 0) {
top = rect.top + window.pageYOffset + props.source.offsetHeight;
el.value.style.transformOrigin = 'center top';
if (props.targetElement) {
top = rect.top + window.pageYOffset + props.targetElement.offsetHeight;
el.value.style.transformOrigin = 'center top';
} else {
top = props.y;
}
}
el.value.style.left = left + 'px';
@ -54,11 +73,6 @@ const setPosition = () => {
onMounted(() => {
nextTick(() => {
if (props.source == null) {
emit('closed');
return;
}
setPosition();
let loopHandler;
@ -101,6 +115,6 @@ onMounted(() => {
border-radius: 4px;
border: solid 0.5px var(--divider);
pointer-events: none;
transform-origin: center bottom;
transform-origin: center center;
}
</style>

View file

@ -48,7 +48,7 @@ export default {
popup(import('@/components/ui/tooltip.vue'), {
showing,
text: self.text,
source: el
targetElement: el,
}, {}, 'closed');
self._close = () => {
@ -56,8 +56,8 @@ export default {
};
};
el.addEventListener('selectstart', e => {
e.preventDefault();
el.addEventListener('selectstart', ev => {
ev.preventDefault();
});
el.addEventListener(start, () => {

View file

@ -14,7 +14,7 @@ if (localStorage.getItem('accounts') != null) {
//#endregion
import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue';
import * as compareVersions from 'compare-versions';
import compareVersions from 'compare-versions';
import widgets from '@/widgets';
import directives from '@/directives';

View file

@ -115,7 +115,7 @@ const pagination = {
offsetMode: true,
params: computed(() => ({
sort: sort,
host: host != '' ? host : null,
host: host !== '' ? host : null,
...(
state === 'federating' ? { federating: true } :
state === 'subscribing' ? { subscribing: true } :
@ -157,11 +157,10 @@ defineExpose({
> .instance {
padding: 16px;
border: solid 1px var(--divider);
border-radius: 6px;
background: var(--panel);
border-radius: 8px;
&:hover {
border: solid 1px var(--accent);
text-decoration: none;
}

View file

@ -29,6 +29,7 @@
<template #label>Moderation</template>
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
<MkButton @click="refreshMetadata">Refresh metadata</MkButton>
</FormSection>
<FormSection>
@ -111,6 +112,7 @@ import MkChart from '@/components/chart.vue';
import MkObjectView from '@/components/object-view.vue';
import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/link.vue';
import MkButton from '@/components/ui/button.vue';
import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue';
import MkSelect from '@/components/form/select.vue';
@ -155,6 +157,15 @@ async function toggleSuspend(v) {
});
}
function refreshMetadata() {
os.api('admin/federation/refresh-remote-instance-metadata', {
host: instance.host,
});
os.alert({
text: 'Refresh requested',
});
}
fetch();
defineExpose({

View file

@ -19,7 +19,7 @@
<FormSection>
<template #label>{{ $ts.statistics }}</template>
<div ref="chart"></div>
<MkChart src="per-user-drive" :args="{ user: $i }" span="day" :limit="7 * 5" :bar="true" :stacked="true" :detailed="false" :aspect-ratio="6"/>
</FormSection>
<FormSection>
@ -45,8 +45,7 @@ import * as os from '@/os';
import bytes from '@/filters/bytes';
import * as symbols from '@/symbols';
import { defaultStore } from '@/store';
// TODO: render chart
import MkChart from '@/components/chart.vue';
export default defineComponent({
components: {
@ -55,6 +54,7 @@ export default defineComponent({
FormSection,
MkKeyValue,
FormSplit,
MkChart,
},
emits: ['info'],

View file

@ -46,8 +46,10 @@ const keymap = {
const tlComponent = $ref<InstanceType<typeof XTimeline>>();
const rootEl = $ref<HTMLElement>();
let src = $ref<'home' | 'local' | 'social' | 'global'>(defaultStore.state.tl.src);
let queue = $ref(0);
const src = $computed(() => defaultStore.reactiveState.tl.value.src);
watch ($$(src), () => queue = 0);
function queueUpdated(q: number): void {
queue = q;
@ -60,7 +62,7 @@ function top(): void {
async function chooseList(ev: MouseEvent): Promise<void> {
const lists = await os.api('users/lists/list');
const items = lists.map(list => ({
type: 'link',
type: 'link' as const,
text: list.name,
to: `/timeline/list/${list.id}`,
}));
@ -70,7 +72,7 @@ async function chooseList(ev: MouseEvent): Promise<void> {
async function chooseAntenna(ev: MouseEvent): Promise<void> {
const antennas = await os.api('antennas/list');
const items = antennas.map(antenna => ({
type: 'link',
type: 'link' as const,
text: antenna.name,
indicate: antenna.hasUnreadNote,
to: `/timeline/antenna/${antenna.id}`,
@ -81,7 +83,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
async function chooseChannel(ev: MouseEvent): Promise<void> {
const channels = await os.api('channels/followed');
const items = channels.map(channel => ({
type: 'link',
type: 'link' as const,
text: channel.name,
indicate: channel.hasUnreadNote,
to: `/channels/${channel.id}`,
@ -89,9 +91,10 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
function saveSrc(): void {
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global'): void {
defaultStore.set('tl', {
src: src,
...defaultStore.state.tl,
src: newSrc,
});
}
@ -135,25 +138,25 @@ defineExpose({
title: i18n.ts._timelines.home,
icon: 'fas fa-home',
iconOnly: true,
onClick: () => { src = 'home'; saveSrc(); },
onClick: () => { saveSrc('home'); },
}, ...(isLocalTimelineAvailable ? [{
active: src === 'local',
title: i18n.ts._timelines.local,
icon: 'fas fa-comments',
iconOnly: true,
onClick: () => { src = 'local'; saveSrc(); },
onClick: () => { saveSrc('local'); },
}, {
active: src === 'social',
title: i18n.ts._timelines.social,
icon: 'fas fa-share-alt',
iconOnly: true,
onClick: () => { src = 'social'; saveSrc(); },
onClick: () => { saveSrc('social'); },
}] : []), ...(isGlobalTimelineAvailable ? [{
active: src === 'global',
title: i18n.ts._timelines.global,
icon: 'fas fa-globe',
iconOnly: true,
onClick: () => { src = 'global'; saveSrc(); },
onClick: () => { saveSrc('global'); },
}] : [])],
})),
});

View file

@ -3,7 +3,7 @@
<template #header><i class="fas fa-chart-bar" style="margin-right: 0.5em;"></i>{{ $ts.activity }}</template>
<div style="padding: 8px;">
<MkChart src="per-user-notes" :args="{ user, withoutAll: true }" span="day" :limit="limit" :stacked="true" :detailed="false" :aspect-ratio="6"/>
<MkChart src="per-user-notes" :args="{ user, withoutAll: true }" span="day" :limit="limit" :bar="true" :stacked="true" :detailed="false" :aspect-ratio="5"/>
</div>
</MkContainer>
</template>

View file

@ -4,7 +4,7 @@
<header class="header" @contextmenu.prevent.stop="onContextmenu">
<button v-if="history.length > 0" class="_button" @click="back()"><i class="fas fa-chevron-left"></i></button>
<button v-else class="_button" style="pointer-events: none;"><!-- マージンのバランスを取るためのダミー --></button>
<span class="title">{{ pageInfo.title }}</span>
<span class="title" v-text="pageInfo?.title" />
<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
</header>
<MkHeader class="pageHeader" :info="pageInfo"/>
@ -13,99 +13,89 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { provide } from 'vue';
import * as os from '@/os';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { resolve } from '@/router';
import { url } from '@/config';
import { resolve, router } from '@/router';
import { url as root } from '@/config';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
provide() {
return {
navHook: (path) => {
this.navigate(path);
}
};
},
provide('navHook', navigate);
data() {
return {
path: null,
component: null,
props: {},
pageInfo: null,
history: [],
};
},
let path: string | null = $ref(null);
let component: ReturnType<typeof resolve>['component'] | null = $ref(null);
let props: any | null = $ref(null);
let pageInfo: any | null = $ref(null);
let history: string[] = $ref([]);
computed: {
url(): string {
return url + this.path;
}
},
let url = $computed(() => `${root}${path}`);
methods: {
changePage(page) {
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
this.pageInfo = page[symbols.PAGE_INFO];
}
},
navigate(path, record = true) {
if (record && this.path) this.history.push(this.path);
this.path = path;
const { component, props } = resolve(path);
this.component = component;
this.props = props;
},
back() {
this.navigate(this.history.pop(), false);
},
close() {
this.path = null;
this.component = null;
this.props = {};
},
onContextmenu(ev: MouseEvent) {
os.contextMenu([{
type: 'label',
text: this.path,
}, {
icon: 'fas fa-expand-alt',
text: this.$ts.showInPage,
action: () => {
this.$router.push(this.path);
this.close();
}
}, {
icon: 'fas fa-window-maximize',
text: this.$ts.openInWindow,
action: () => {
os.pageWindow(this.path);
this.close();
}
}, null, {
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.close();
}
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
}
}], ev);
}
function changePage(page) {
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
pageInfo = page[symbols.PAGE_INFO];
}
}
function navigate(_path: string, record = true) {
if (record && path) history.push($$(path).value);
path = _path;
const resolved = resolve(path);
component = resolved.component;
props = resolved.props;
}
function back() {
const prev = history.pop();
if (prev) navigate(prev, false);
}
function close() {
path = null;
component = null;
props = {};
}
function onContextmenu(ev: MouseEvent) {
os.contextMenu([{
type: 'label',
text: path || '',
}, {
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: () => {
if (path) router.push(path);
close();
}
}, {
icon: 'fas fa-window-maximize',
text: i18n.ts.openInWindow,
action: () => {
if (path) os.pageWindow(path);
close();
}
}, null, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => {
window.open(url, '_blank');
close();
}
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(url);
}
}], ev);
}
defineExpose({
navigate,
back,
close,
});
</script>

View file

@ -20,7 +20,7 @@
</main>
</div>
<XSideView v-if="isDesktop" ref="side" class="side"/>
<XSideView v-if="isDesktop" ref="sideEl" class="side"/>
<div v-if="isDesktop" ref="widgetsEl" class="widgets">
<XWidgets @mounted="attachSticky"/>
@ -31,9 +31,9 @@
<div v-if="isMobile" class="buttons">
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
<button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button>
</div>
<transition :name="$store.state.animation ? 'menuDrawer-back' : ''">
@ -64,155 +64,133 @@
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue';
import { instanceName } from '@/config';
import { StickySidebar } from '@/scripts/sticky-sidebar';
import XSidebar from '@/ui/_common_/sidebar.vue';
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
import XCommon from './_common_/common.vue';
import XSideView from './classic.side.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { defaultStore } from '@/store';
import * as EventEmitter from 'eventemitter3';
import { menuDef } from '@/menu';
import { useRoute } from 'vue-router';
import { i18n } from '@/i18n';
import { $i } from '@/account';
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/sidebar.vue'));
const DESKTOP_THRESHOLD = 1100;
const MOBILE_THRESHOLD = 500;
export default defineComponent({
components: {
XCommon,
XSidebar,
XDrawerMenu,
XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')),
XSideView, // NOTE: dynamic importAsyncComponentWrapperref
},
setup() {
const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD);
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= MOBILE_THRESHOLD;
});
const pageInfo = ref();
const widgetsEl = ref<HTMLElement>();
const widgetsShowing = ref(false);
const sideViewController = new EventEmitter();
provide('sideViewHook', isDesktop.value ? (url) => {
sideViewController.emit('navigate', url);
} : null);
const menuIndicated = computed(() => {
for (const def in menuDef) {
if (def === 'notifications') continue; //
if (menuDef[def].indicated) return true;
}
return false;
});
const drawerMenuShowing = ref(false);
const route = useRoute();
watch(route, () => {
drawerMenuShowing.value = false;
});
document.documentElement.style.overflowY = 'scroll';
if (defaultStore.state.widgets.length === 0) {
defaultStore.set('widgets', [{
name: 'calendar',
id: 'a', place: 'right', data: {}
}, {
name: 'notifications',
id: 'b', place: 'right', data: {}
}, {
name: 'trends',
id: 'c', place: 'right', data: {}
}]);
}
onMounted(() => {
if (!isDesktop.value) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
}, { passive: true });
}
});
const changePage = (page) => {
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
pageInfo.value = page[symbols.PAGE_INFO];
document.title = `${pageInfo.value.title} | ${instanceName}`;
}
};
const onContextmenu = (ev) => {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = route.path;
os.contextMenu([{
type: 'label',
text: path,
}, {
icon: 'fas fa-columns',
text: i18n.ts.openInSideView,
action: () => {
this.$refs.side.navigate(path);
}
}, {
icon: 'fas fa-window-maximize',
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(path);
}
}], ev);
};
const attachSticky = (el) => {
const sticky = new StickySidebar(widgetsEl.value);
window.addEventListener('scroll', () => {
sticky.calc(window.scrollY);
}, { passive: true });
};
return {
pageInfo,
isDesktop,
isMobile,
widgetsEl,
widgetsShowing,
drawerMenuShowing,
menuIndicated,
wallpaper: localStorage.getItem('wallpaper') != null,
changePage,
top: () => {
window.scroll({ top: 0, behavior: 'smooth' });
},
onTransition: () => {
if (window._scroll) window._scroll();
},
post: os.post,
onContextmenu,
attachSticky,
};
},
const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD);
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= MOBILE_THRESHOLD;
});
const pageInfo = ref();
const widgetsEl = $ref<HTMLElement>();
const widgetsShowing = ref(false);
let sideEl = $ref<InstanceType<typeof XSideView>>();
provide('sideViewHook', isDesktop.value ? (url) => {
sideEl.navigate(url);
} : null);
const menuIndicated = computed(() => {
for (const def in menuDef) {
if (def === 'notifications') continue; //
if (menuDef[def].indicated) return true;
}
return false;
});
const drawerMenuShowing = ref(false);
const route = useRoute();
watch(route, () => {
drawerMenuShowing.value = false;
});
document.documentElement.style.overflowY = 'scroll';
if (defaultStore.state.widgets.length === 0) {
defaultStore.set('widgets', [{
name: 'calendar',
id: 'a', place: 'right', data: {}
}, {
name: 'notifications',
id: 'b', place: 'right', data: {}
}, {
name: 'trends',
id: 'c', place: 'right', data: {}
}]);
}
onMounted(() => {
if (!isDesktop.value) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
}, { passive: true });
}
});
const changePage = (page) => {
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
pageInfo.value = page[symbols.PAGE_INFO];
document.title = `${pageInfo.value.title} | ${instanceName}`;
}
};
const onContextmenu = (ev) => {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection()?.toString() !== '') return;
const path = route.path;
os.contextMenu([{
type: 'label',
text: path,
}, {
icon: 'fas fa-columns',
text: i18n.ts.openInSideView,
action: () => {
sideEl.navigate(path);
}
}, {
icon: 'fas fa-window-maximize',
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(path);
}
}], ev);
};
const attachSticky = (el) => {
const sticky = new StickySidebar(widgetsEl);
window.addEventListener('scroll', () => {
sticky.calc(window.scrollY);
}, { passive: true });
};
function top() {
window.scroll({ top: 0, behavior: 'smooth' });
}
function onTransition() {
if (window._scroll) window._scroll();
}
const wallpaper = localStorage.getItem('wallpaper') != null;
</script>
<style lang="scss" scoped>

View file

@ -58,7 +58,7 @@ const fetch = async () => {
sort: '+lastCommunicatedAt',
limit: 5
});
const fetchedCharts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
instances.value = fetchedInstances;
charts.value = fetchedCharts;
fetching.value = false;

View file

@ -266,11 +266,6 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/dateformat@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc"
integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==
"@types/escape-regexp@0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.1.tgz#f1a977ccdf2ef059e9862bd3af5e92cbbe723e0e"

Some files were not shown because too many files have changed in this diff Show more