mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-23 21:13:09 +02:00
Merge branch 'develop'
This commit is contained in:
commit
cdef5cd1ad
64 changed files with 1319 additions and 283 deletions
|
@ -50,7 +50,7 @@ Configuration files are located in [`/.circleci`](/.circleci).
|
|||
* Your PR should include all source files (e.g. `.png`, `.blend`) of your models (for later editing).
|
||||
* Your PR must include the glTF binary files (`.glb`) of your models.
|
||||
* Add a locale key `room.furnitures.YOUR_ITEM` at [`/locales/ja-JP.yml`](/locales/ja-JP.yml).
|
||||
* Add a furniture definition at [`/src/client/app/common/scripts/room/furnitures.json5`](/src/client/app/common/scripts/room/furnitures.json5).
|
||||
* Add a furniture definition at [`src/client/scripts/room/furnitures.json5`](src/client/scripts/room/furnitures.json5).
|
||||
|
||||
If you have no experience on 3D modeling, we suggest to use the free 3DCG software [Blender](https://www.blender.org/).
|
||||
You can find information on glTF 2.0 at [glTF 2.0 — Blender Manual]( https://docs.blender.org/manual/en/dev/addons/io_scene_gltf2.html).
|
||||
|
|
|
@ -430,6 +430,8 @@ inUse: "مستخدم"
|
|||
info: "عن"
|
||||
user: "المستخدمون"
|
||||
administration: "إدارة "
|
||||
expiration: "ينتهي استطلاع الرأي في"
|
||||
middle: "متوسط"
|
||||
_email:
|
||||
_follow:
|
||||
title: "يتابعك"
|
||||
|
|
|
@ -7,6 +7,7 @@ search: "Suchen"
|
|||
notifications: "Benachrichtigungen"
|
||||
username: "Benutzername"
|
||||
password: "Passwort"
|
||||
forgotPassword: "Passwort vergessen"
|
||||
fetchingAsApObject: "Wird aus dem Fediverse angefragt..."
|
||||
ok: "OK"
|
||||
gotIt: "Verstanden!"
|
||||
|
@ -298,8 +299,8 @@ reject: "Ablehnen"
|
|||
normal: "Normal"
|
||||
instanceName: "Name der Instanz"
|
||||
instanceDescription: "Beschreibung der Instanz"
|
||||
maintainerName: "Betreiber"
|
||||
maintainerEmail: "Betreiber-Email"
|
||||
maintainerName: "Administrator"
|
||||
maintainerEmail: "Administrator-Email"
|
||||
tosUrl: "URL der Nutzungsbedingungen"
|
||||
thisYear: "Dieses Jahr"
|
||||
thisMonth: "Dieser Monat"
|
||||
|
@ -737,7 +738,7 @@ user: "Benutzer"
|
|||
administration: "Verwaltung"
|
||||
accounts: "Benutzerkonten"
|
||||
switch: "Wechseln"
|
||||
noMaintainerInformationWarning: "Betreiberinformationen sind nicht konfiguriert."
|
||||
noMaintainerInformationWarning: "Administratorinformationen sind nicht konfiguriert."
|
||||
noBotProtectionWarning: "Bot-Schutz ist nicht konfiguriert."
|
||||
configure: "Konfigurieren"
|
||||
postToGallery: "Beitrag zu Galerie hinzufügen"
|
||||
|
@ -745,6 +746,13 @@ gallery: "Galerie"
|
|||
recentPosts: "Neue Beiträge"
|
||||
popularPosts: "Beliebte Beiträge"
|
||||
shareWithNote: "Mit Notiz teilen"
|
||||
expiration: "Abstimmung endet am"
|
||||
middle: "Mittel"
|
||||
emailNotConfiguredWarning: "Keine Email-Adresse hinterlegt"
|
||||
_forgotPassword:
|
||||
enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit der du dein Passwort zurücksetzen kannst."
|
||||
ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator."
|
||||
contactAdmin: "Diese Instanz unterstützt die Verwendung von Email-Adressen nicht. Wende dich, um dein Passwort zurückzusetzen, an den Administrator."
|
||||
_gallery:
|
||||
my: "Meine Galerie"
|
||||
liked: "Beiträge, die mir gefallen"
|
||||
|
|
|
@ -7,6 +7,7 @@ search: "Search"
|
|||
notifications: "Notifications"
|
||||
username: "Username"
|
||||
password: "Password"
|
||||
forgotPassword: "Forgot password"
|
||||
fetchingAsApObject: "Fetching from Fediverse..."
|
||||
ok: "OK"
|
||||
gotIt: "Got it!"
|
||||
|
@ -730,6 +731,7 @@ active: "Active"
|
|||
offline: "Offline"
|
||||
notRecommended: "Not recommended"
|
||||
botProtection: "Bot Protection"
|
||||
instanceBlocking: "Blocked Instances"
|
||||
selectAccount: "Select account"
|
||||
enabled: "Enabled"
|
||||
disabled: "Disabled"
|
||||
|
@ -746,6 +748,13 @@ gallery: "Gallery"
|
|||
recentPosts: "Recent posts"
|
||||
popularPosts: "Popular posts"
|
||||
shareWithNote: "Share with note"
|
||||
expiration: "Poll ends on"
|
||||
middle: "Medium"
|
||||
emailNotConfiguredWarning: "Email address not set"
|
||||
_forgotPassword:
|
||||
enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it."
|
||||
ifNoEmail: "If you did not use an email during registration, please contact the administrator instead."
|
||||
contactAdmin: "This instance does not support using email addresses, please contact the administrator to reset your password instead."
|
||||
_gallery:
|
||||
my: "My Gallery"
|
||||
liked: "Liked Posts"
|
||||
|
@ -1068,7 +1077,7 @@ _auth:
|
|||
permissionAsk: "This application requires following permissions:"
|
||||
pleaseGoBack: "Please go back to the application"
|
||||
callback: "Returning back to the application"
|
||||
denied: "Access Denied"
|
||||
denied: "Access denied"
|
||||
_antennaSources:
|
||||
all: "All notes"
|
||||
homeTimeline: "Notes from following users"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
_lang_: "Español"
|
||||
headlineMisskey: "Red conectada por notas"
|
||||
introMisskey: "¡Bienvenido/a! Misskey es un servicio de microblogging descentralizado de código abierto.\nEscribe \"notas\" para compartir lo que te ocurre ahora o para contar sobre ti a todos 📡\nCon la función de \"reacciones\", puedes también añadir una reacción rápida a las notas de todos 👍\nExplora un nuevo mundo 🚀"
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Buscar"
|
||||
|
@ -137,6 +138,7 @@ flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, a
|
|||
flagAsCat: "Esta cuenta es un gato"
|
||||
flagAsCatDescription: "En caso de que declare que esta cuenta es de un gato, active esta opción."
|
||||
autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues"
|
||||
addAccount: "Agregar Cuenta"
|
||||
loginFailed: "Error al iniciar sesión."
|
||||
showOnRemote: "Ver en una instancia remota"
|
||||
general: "General"
|
||||
|
@ -435,6 +437,7 @@ signinWith: "Inicie sesión con {x}"
|
|||
signinFailed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario y contraseña correctos."
|
||||
tapSecurityKey: "Toque la clave de seguridad"
|
||||
or: "O"
|
||||
language: "Idioma"
|
||||
uiLanguage: "Idioma de visualización de la interfaz"
|
||||
groupInvited: "Invitado al grupo"
|
||||
aboutX: "Acerca de {x}"
|
||||
|
@ -449,6 +452,7 @@ category: "Categoría"
|
|||
tags: "Etiqueta"
|
||||
docSource: "Fuente de este documento"
|
||||
createAccount: "Crear cuenta"
|
||||
existingAccount: "Cuenta existente"
|
||||
regenerate: "Regenerar"
|
||||
fontSize: "Tamaño de la letra"
|
||||
noFollowRequests: "No hay solicitudes de seguimiento"
|
||||
|
@ -563,6 +567,7 @@ pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos
|
|||
notificationType: "Tipo de notificación"
|
||||
edit: "Editar"
|
||||
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
|
||||
emailServer: "Servidor de correo"
|
||||
enableEmail: "Activar el envío de correos electrónicos"
|
||||
emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
|
||||
email: "Correo"
|
||||
|
@ -642,6 +647,14 @@ driveFilesCount: "Cantidad de archivos en el drive"
|
|||
driveUsage: "Uso del drive"
|
||||
noCrawle: "Rechazar indexación del crawler"
|
||||
noCrawleDescription: "Pedir a los motores de búsqueda que no indexen tu perfil, notas, páginas, etc."
|
||||
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto"
|
||||
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración."
|
||||
notSet: "Sin especificar"
|
||||
emailVerified: "Su dirección de correo electrónico ha sido verificada."
|
||||
noteFavoritesCount: "Número de notas favoritas"
|
||||
pageLikesCount: "Número de favoritos en la página"
|
||||
pageLikedCount: "Número de favoritos de su página"
|
||||
contact: "Contacto"
|
||||
clips: "Clip"
|
||||
clearCache: "Limpiar caché"
|
||||
backgroundColor: "Fondo"
|
||||
|
@ -652,6 +665,8 @@ goBack: "Deseleccionar"
|
|||
info: "Información"
|
||||
user: "Usuarios"
|
||||
administration: "Administrar"
|
||||
expiration: "Termina el"
|
||||
middle: "Mediano"
|
||||
_gallery:
|
||||
unlike: "Quitar me gusta"
|
||||
_email:
|
||||
|
|
|
@ -604,6 +604,7 @@ setMultipleBySeparatingWithSpace: "Vous pouvez en définir plusieurs, en les sé
|
|||
fileIdOrUrl: "ID du fichier ou URL"
|
||||
chatOpenBehavior: "Comportement de la fenêtre de discussion lors de son ouverture"
|
||||
behavior: "Comportement"
|
||||
sample: "Exemple"
|
||||
abuseReports: "Signalements"
|
||||
reportAbuse: "Signalements"
|
||||
reportAbuseOf: "Signaler {name}"
|
||||
|
@ -725,6 +726,7 @@ onlineStatus: "Statut"
|
|||
hideOnlineStatus: "Se rendre invisible"
|
||||
hideOnlineStatusDescription: "Rendre votre statut invisible peut diminuer les performances de certaines fonctionnalités, telles que la Recherche."
|
||||
online: "En ligne"
|
||||
active: "Actif·ve"
|
||||
offline: "Hors ligne"
|
||||
notRecommended: "Déconseillé"
|
||||
botProtection: "Protection contre les bots"
|
||||
|
@ -745,6 +747,8 @@ gallery: "Galerie"
|
|||
recentPosts: "Les plus récentes"
|
||||
popularPosts: "Les plus consultées"
|
||||
shareWithNote: "Partager dans une note"
|
||||
expiration: "Fin du sondage"
|
||||
middle: "Moyen"
|
||||
_gallery:
|
||||
my: "Mes publications"
|
||||
liked: " Publications que j'ai aimées"
|
||||
|
@ -760,6 +764,7 @@ _plugin:
|
|||
installWarn: "N’installez que des extensions provenant de sources de confiance."
|
||||
manage: "Gestion des plugins"
|
||||
_registry:
|
||||
scope: "Portée"
|
||||
key: "Clé "
|
||||
keys: "Clé "
|
||||
domain: "Domaine"
|
||||
|
@ -793,18 +798,38 @@ _mfm:
|
|||
boldDescription: "Il est possible de mettre le texte en exergue en le mettant en gras."
|
||||
small: "Diminuer l'emphase"
|
||||
smallDescription: "Le contenu peut être affiché en petit et fin."
|
||||
center: "Centrée"
|
||||
center: "Centrer"
|
||||
centerDescription: "Le contenu peut être centré"
|
||||
inlineCode: "Code (inline)"
|
||||
inlineCodeDescription: "Coloration syntaxique des lignes de code."
|
||||
blockCode: "Bloc de code"
|
||||
blockCodeDescription: "Coloration syntaxique des lignes de code pour les blocs multi-lignes."
|
||||
inlineMath: "Formule mathématique (inline)"
|
||||
inlineMathDescription: "Afficher les formules mathématiques (KaTeX)."
|
||||
blockMath: "Formule mathématique (bloc)"
|
||||
blockMathDescription: "Afficher les formules mathématiques (KaTeX) multi-lignes dans un bloc."
|
||||
quote: "Citer"
|
||||
quoteDescription: "Affiche le contenu sous forme de citation."
|
||||
emoji: "Émojis personnalisés"
|
||||
emojiDescription: "Entourez le nom de l'émoji personnalisé de deux points pour l'afficher."
|
||||
search: "Rechercher"
|
||||
searchDescription: "Affiche une boîte de recherche avec du texte pré-saisi."
|
||||
flip: "Inverser"
|
||||
flipDescription: "Rotation verticale ou horizontale du contenu"
|
||||
jelly: "Animation (Gelée)"
|
||||
jellyDescription: "Donne une animation d'étirement."
|
||||
tada: "Animation (Tada)"
|
||||
tadaDescription: "Donne une animation qui donne une impression de \"Tada !\""
|
||||
jump: "Animation (Saut)"
|
||||
jumpDescription: "Donne une animation qui saute."
|
||||
bounce: "Animation (Rebond)"
|
||||
bounceDescription: "Donne une animation de rebondissement."
|
||||
shake: "Animation (Secousse)"
|
||||
shakeDescription: "Donne une animation tremblante."
|
||||
twitch: "Animation (Tremblement)"
|
||||
twitchDescription: "Donne une animation de tremblement intense."
|
||||
spin: "Animation (Rotation)"
|
||||
spinDescription: "Donne une animation de rotation."
|
||||
x2: "Grand"
|
||||
x2Description: "Afficher le contenu en grand."
|
||||
x3: "Très grand"
|
||||
|
@ -846,6 +871,7 @@ _reversi:
|
|||
ended: "Fin de partie"
|
||||
playing: "En cours"
|
||||
isLlotheo: "Celui ou celle qui a le moins de pièces gagne (Llotheo)"
|
||||
loopedMap: "Carte en boucle"
|
||||
canPutEverywhere: "Les pions peuvent être placés partout "
|
||||
_instanceTicker:
|
||||
none: "Cacher "
|
||||
|
@ -895,11 +921,13 @@ _theme:
|
|||
constant: "Constante"
|
||||
defaultValue: "Valeur par défaut"
|
||||
color: "Couleur"
|
||||
refConst: "Référencez une constante"
|
||||
refProp: "Appeler une propriété"
|
||||
refConst: "Appeler une constante"
|
||||
key: "Clé "
|
||||
func: "Fonction"
|
||||
funcKind: "Type de fonction"
|
||||
argument: "Argument"
|
||||
basedProp: "Nom de la propriété référencée"
|
||||
alpha: "Transparence"
|
||||
darken: "Sombre"
|
||||
lighten: "Clair"
|
||||
|
@ -925,9 +953,12 @@ _theme:
|
|||
mention: "Mentionner"
|
||||
mentionMe: "Mentions (Moi)"
|
||||
renote: "Partager"
|
||||
modalBg: "Modal d'arrière-plan"
|
||||
divider: "Séparateur"
|
||||
scrollbarHandle: "Poignée de la barre de navigation"
|
||||
scrollbarHandleHover: "Poignée de la barre de navigation (survolée)"
|
||||
dateLabelFg: "Texte de l'étiquette de la date"
|
||||
infoBg: "Arrière-plan pour les informations"
|
||||
infoFg: "Texte d'information"
|
||||
infoWarnBg: "Arrière-plan des avertissements"
|
||||
infoWarnFg: "Texte d’avertissement"
|
||||
|
@ -941,6 +972,7 @@ _theme:
|
|||
inputBorder: "Cadre de la zone de texte"
|
||||
listItemHoverBg: "Arrière-plan d'item de liste (survolé)"
|
||||
driveFolderBg: "Arrière-plan du dossier de disque"
|
||||
wallpaperOverlay: "Superposition de fond d'écran"
|
||||
badge: "Badge"
|
||||
messageBg: "Arrière plan de la discussion"
|
||||
accentDarken: "Plus sombre"
|
||||
|
@ -951,7 +983,7 @@ _sfx:
|
|||
noteMy: "Ma note"
|
||||
notification: "Notifications"
|
||||
chat: "Discuter"
|
||||
chatBg: "Discuter (De fond)"
|
||||
chatBg: "Discussion (arrière-plan)"
|
||||
antenna: "Réception de l’antenne"
|
||||
channel: "Notifications de canal"
|
||||
reversiPutBlack: "Reversi : les pions noirs ont joué"
|
||||
|
@ -996,14 +1028,14 @@ _tutorial:
|
|||
step7_2: "Si vous désirez en savoir plus sur Misskey, jetez un œil sur la section {help}."
|
||||
step7_3: "Bon courage et amusez-vous bien sur Misskey ! 🚀"
|
||||
_2fa:
|
||||
alreadyRegistered: "Cette étape à déjà été complétée"
|
||||
alreadyRegistered: "Configuration déjà achevée."
|
||||
registerDevice: "Ajouter un nouvel appareil"
|
||||
registerKey: "S’inscrire la clé"
|
||||
registerKey: "Enregistrer une clef"
|
||||
step1: "Tout d'abord, installez une application d'authentification, telle que {a} ou {b}, sur votre appareil."
|
||||
step2: "Ensuite, scannez le code QR affiché sur l’écran."
|
||||
step3: "Entrez le jeton affiché sur votre application pour compléter la configuration."
|
||||
step4: "Lorsque vous vous connectez, entrez le jeton de la même manière."
|
||||
securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion avec non seulement la clé de sécurité matérielle qui prend en charge FIDO2, mais également l'authentification par empreinte digitale ou PIN sur votre appareil."
|
||||
step4: "À partir de maintenant, ce même jeton vous sera demandé à chacune de vos connexions."
|
||||
securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion grâce à une clé de sécurité matérielle qui prend en charge FIDO2, ou bien en configurant l'authentification par empreinte digitale ou par code PIN sur votre appareil."
|
||||
_permissions:
|
||||
"read:account": "Afficher les informations du compte"
|
||||
"write:account": "Mettre à jour les informations de votre compte"
|
||||
|
@ -1015,8 +1047,8 @@ _permissions:
|
|||
"write:favorites": "Gérer les favoris"
|
||||
"read:following": "Voir les informations de vos abonnements"
|
||||
"write:following": "Abonnements/Se désabonner"
|
||||
"read:messaging": "Cherche à discuter"
|
||||
"write:messaging": "Contrôler le discuter"
|
||||
"read:messaging": "Voir vos discussions"
|
||||
"write:messaging": "Gérer les discussions"
|
||||
"read:mutes": "Voir les comptes masqués"
|
||||
"write:mutes": "Gérer les comptes masqués"
|
||||
"write:notes": "Créer / supprimer des notes"
|
||||
|
@ -1025,10 +1057,10 @@ _permissions:
|
|||
"read:reactions": "Lire les réactions"
|
||||
"write:reactions": "Gérer vos réactions"
|
||||
"write:votes": "Voter"
|
||||
"read:pages": "Afficher la page"
|
||||
"write:pages": "Mettre à jour les Pages"
|
||||
"read:page-likes": "Voir les favoris sur les Pages"
|
||||
"write:page-likes": "Mettre à jour les favoris sur les Pages"
|
||||
"read:pages": "Voir vos pages"
|
||||
"write:pages": "Gérer les pages"
|
||||
"read:page-likes": "Voir les mentions « J'aime » des pages"
|
||||
"write:page-likes": "Gérer les mentions « J'aime » sur les pages"
|
||||
"read:user-groups": "Voir les groupes d'utilisateur·rice·s"
|
||||
"write:user-groups": "Éditer les groupes des utilisateur·rice·s"
|
||||
"read:channels": "Lire les canaux"
|
||||
|
|
|
@ -628,6 +628,7 @@ driveFilesCount: "Numero di file nel Drive"
|
|||
driveUsage: "Utilizzazione del Drive"
|
||||
noCrawle: "Rifiuta l'indicizzazione dai robot."
|
||||
noCrawleDescription: "Richiedi che i motori di ricerca non indicizzino la tua pagina di profilo, le tue note, pagine, ecc."
|
||||
lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"Solo ai follower\", le tue note sono visibili da tutti, anche se hai configurato l'account per confermare manualmente le richieste di follow."
|
||||
alwaysMarkSensitive: "Segnare i media come sensibili per impostazione predefinita"
|
||||
loadRawImages: "Visualizza le intere immagini allegate invece delle miniature."
|
||||
disableShowingAnimatedImages: "Disabilita le immagini animate"
|
||||
|
@ -706,6 +707,7 @@ online: "Online"
|
|||
offline: "Offline"
|
||||
notRecommended: "Sconsigliato"
|
||||
botProtection: "Protezione contro i bot"
|
||||
instanceBlocking: "Istanze bloccate"
|
||||
selectAccount: "Scegli account"
|
||||
enabled: "Attivo"
|
||||
disabled: "Inattivo"
|
||||
|
@ -722,6 +724,8 @@ gallery: "Galleria"
|
|||
recentPosts: "Le più recenti"
|
||||
popularPosts: "Le più visualizzate"
|
||||
shareWithNote: "Condividere in nota"
|
||||
expiration: "Scadenza"
|
||||
middle: "Predefinito"
|
||||
_gallery:
|
||||
my: "Le mie pubblicazioni"
|
||||
liked: "Pubblicazioni che mi piacciono"
|
||||
|
@ -829,11 +833,15 @@ _theme:
|
|||
constant: "Costante"
|
||||
defaultValue: "Valore predefinito"
|
||||
color: "Colore"
|
||||
refConst: "Chiama costante"
|
||||
key: "Chiave"
|
||||
func: "Funzione"
|
||||
funcKind: "Tipo di funzione"
|
||||
argument: "Argomento"
|
||||
darken: "Scuro"
|
||||
lighten: "Chiaro"
|
||||
inputConstantName: "Inserisci un nome per la costante"
|
||||
deleteConstantConfirm: "Vuoi davvero eliminare la costante {const}?"
|
||||
keys:
|
||||
bg: "Sfondo"
|
||||
fg: "Testo"
|
||||
|
@ -850,8 +858,10 @@ _theme:
|
|||
link: "Link"
|
||||
hashtag: "Hashtag"
|
||||
mention: "Menzioni"
|
||||
mentionMe: "Menzioni (di me)"
|
||||
renote: "Rinota"
|
||||
divider: "Interruzione di linea"
|
||||
infoBg: "Sfondo informazioni"
|
||||
infoFg: "Testo di informazioni"
|
||||
infoWarnBg: "Sfondo degli avvisi"
|
||||
infoWarnFg: "Testo di avviso"
|
||||
|
@ -865,11 +875,13 @@ _theme:
|
|||
inputBorder: "Inquadra casella di testo"
|
||||
listItemHoverBg: "Sfondo della voce di elenco (sorvolato)"
|
||||
driveFolderBg: "Sfondo della cartella di disco"
|
||||
messageBg: "Sfondo della chat"
|
||||
_sfx:
|
||||
note: "Nota"
|
||||
noteMy: "Mia nota"
|
||||
notification: "Notifiche"
|
||||
chat: "Messaggi"
|
||||
chatBg: "Chat (sfondo)"
|
||||
antenna: "Ricezione dell'antenna"
|
||||
channel: "Notifiche di canale"
|
||||
_ago:
|
||||
|
@ -914,12 +926,18 @@ _tutorial:
|
|||
_2fa:
|
||||
registerDevice: "Aggiungi dispositivo"
|
||||
_permissions:
|
||||
"read:account": "Visualizzare le informazioni dell'account"
|
||||
"write:account": "Modificare le informazioni dell'account"
|
||||
"read:blocks": "Visualizza gli account bloccati"
|
||||
"write:blocks": "Gestisci gli account bloccati"
|
||||
"read:drive": "Aprire il Drive"
|
||||
"write:drive": "Gestire il Drive"
|
||||
"read:favorites": "Visualizza i tuoi preferiti"
|
||||
"write:favorites": "Gestisci i tuoi preferiti"
|
||||
"read:following": "Vedi le informazioni di follow"
|
||||
"write:following": "Seguiti/ Smetti di seguire"
|
||||
"read:messaging": "Visualizzare la chat"
|
||||
"write:messaging": "Gestire la chat"
|
||||
"read:mutes": "Vedi account silenziati"
|
||||
"write:mutes": "Gerisci account silenziati"
|
||||
"write:notes": "Creare / Eliminare note"
|
||||
|
@ -927,12 +945,22 @@ _permissions:
|
|||
"write:notifications": "Gerisci notifiche"
|
||||
"read:reactions": "Vedi reazioni"
|
||||
"write:reactions": "Gerisci reazioni"
|
||||
"write:votes": "Votare"
|
||||
"read:pages": "Visualizzare pagine"
|
||||
"write:pages": "Gestire pagine"
|
||||
"read:page-likes": "Visualizzare i \"Mi piace\" di pagine"
|
||||
"write:page-likes": "Gestire i \"Mi piace\" di pagine"
|
||||
"read:user-groups": "Vedi gruppi di utenti"
|
||||
"write:user-groups": "Gestisci gruppi di utenti"
|
||||
"read:channels": "Visualizza canali"
|
||||
"write:channels": "Gerisci canali"
|
||||
_auth:
|
||||
shareAccess: "Autorizzare「{name}」ad accedere al tuo account?"
|
||||
shareAccessAsk: "Vuoi davvero consentire l'accesso al tuo account a questa app'?"
|
||||
permissionAsk: "Questa app richiede le seguenti autorizzazioni:"
|
||||
pleaseGoBack: "Si prega di ritornare sulla app"
|
||||
callback: "Ritornando sulla app"
|
||||
denied: "Accesso negato"
|
||||
_antennaSources:
|
||||
all: "Tutte le note"
|
||||
homeTimeline: "Note dagli utenti che segui"
|
||||
|
@ -960,22 +988,39 @@ _widgets:
|
|||
digitalClock: "Orologio digitale"
|
||||
federation: "Federazione"
|
||||
postForm: "Finestra di pubblicazione"
|
||||
slideshow: "Diapositive"
|
||||
button: "Pulsante"
|
||||
onlineUsers: "Utenti online"
|
||||
jobQueue: "Coda di lavoro"
|
||||
serverMetric: "Statistiche server"
|
||||
aiscript: "Console AiScript"
|
||||
_cw:
|
||||
hide: "Nascondere"
|
||||
show: "Mostra di più"
|
||||
chars: "{count} caratteri"
|
||||
files: "{count} file"
|
||||
_poll:
|
||||
noOnlyOneChoice: "Sono necessarie almeno 2 risposte"
|
||||
choiceN: "Opzione {n}"
|
||||
noMore: "Hai aggiunto il numero massimo di opzioni."
|
||||
canMultipleVote: "Risposte multiple"
|
||||
canMultipleVote: "Possibilità di risposte multiple"
|
||||
expiration: "Scadenza"
|
||||
infinite: "Permanente"
|
||||
infinite: "Non scade"
|
||||
at: "Seleziona data"
|
||||
after: "Seleziona durata"
|
||||
deadlineDate: "Data di scadenza"
|
||||
deadlineTime: "h"
|
||||
voted: "Votato"
|
||||
deadlineTime: "Ora di scadenza"
|
||||
duration: "Durata"
|
||||
votesCount: "{n} voti"
|
||||
totalVotes: "Totale di {n} voti"
|
||||
vote: "Vota"
|
||||
showResult: "Visualizza risultati"
|
||||
voted: "Hai votato"
|
||||
closed: "Terminato"
|
||||
remainingDays: "Rimangono {d} giorni e {h} ore"
|
||||
remainingHours: "Rimangono {h} ore e {m} minuti"
|
||||
remainingMinutes: "Rimangono {m} minuti e {s} secondi"
|
||||
remainingSeconds: "Rimangono {s} secondi"
|
||||
_visibility:
|
||||
public: "Pubblica"
|
||||
publicDescription: "Visibile per tutti sul Fediverso"
|
||||
|
@ -1003,7 +1048,7 @@ _profile:
|
|||
username: "Nome utente"
|
||||
description: "Bio"
|
||||
youCanIncludeHashtags: "Puoi anche includere hashtag."
|
||||
metadata: "Metadati"
|
||||
metadata: "Informazioni aggiuntive"
|
||||
metadataEdit: "Modifica informazioni aggiuntive"
|
||||
metadataDescription: "Puoi pubblicare fino a quattro informazioni aggiuntive sul profilo."
|
||||
metadataLabel: "Etichetta"
|
||||
|
@ -1312,7 +1357,7 @@ _notification:
|
|||
youGotReply: "{name} ti ha risposto"
|
||||
youGotQuote: "{name} ha citato il tuo Nota e ha detto"
|
||||
youRenoted: "{name} ha rinotato"
|
||||
youGotPoll: "{name} ha volluto."
|
||||
youGotPoll: "{name} ha votato"
|
||||
youGotMessagingMessageFromUser: "{name} ti ha mandato un messaggio"
|
||||
youGotMessagingMessageFromGroup: "{name} ti ha mandato un messaggio nella chat"
|
||||
youWereFollowed: "Ha iniziato a seguirti"
|
||||
|
|
|
@ -7,6 +7,7 @@ search: "検索"
|
|||
notifications: "通知"
|
||||
username: "ユーザー名"
|
||||
password: "パスワード"
|
||||
forgotPassword: "パスワードを忘れた"
|
||||
fetchingAsApObject: "連合に照会中"
|
||||
ok: "OK"
|
||||
gotIt: "わかった"
|
||||
|
@ -747,6 +748,19 @@ gallery: "ギャラリー"
|
|||
recentPosts: "最近の投稿"
|
||||
popularPosts: "人気の投稿"
|
||||
shareWithNote: "ノートで共有"
|
||||
ads: "広告"
|
||||
expiration: "期限"
|
||||
memo: "メモ"
|
||||
priority: "優先度"
|
||||
high: "高"
|
||||
middle: "中"
|
||||
low: "低"
|
||||
emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
|
||||
|
||||
_forgotPassword:
|
||||
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
|
||||
ifNoEmail: "メールアドレスを登録していない場合は、管理者までお問い合わせください。"
|
||||
contactAdmin: "このインスタンスではメールがサポートされていないため、パスワードリセットを行う場合は管理者までお問い合わせください。"
|
||||
|
||||
_gallery:
|
||||
my: "自分の投稿"
|
||||
|
|
|
@ -552,7 +552,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
|||
testEmail: "配信テスト"
|
||||
wordMute: "ワードミュート"
|
||||
userSaysSomething: "{name}が何か言ったようやで"
|
||||
makeActive: "アクティブにしてや"
|
||||
makeActive: "使うで"
|
||||
display: "表示"
|
||||
copy: "コピー"
|
||||
metrics: "メトリクス"
|
||||
|
@ -643,6 +643,8 @@ goBack: "戻る"
|
|||
info: "情報"
|
||||
user: "ユーザー"
|
||||
administration: "管理"
|
||||
expiration: "期限"
|
||||
middle: "中"
|
||||
_gallery:
|
||||
unlike: "良くないわ"
|
||||
_email:
|
||||
|
|
|
@ -747,6 +747,8 @@ gallery: "갤러리"
|
|||
recentPosts: "최근 포스트"
|
||||
popularPosts: "인기 포스트"
|
||||
shareWithNote: "노트로 공유"
|
||||
expiration: "투표 기한"
|
||||
middle: "보통"
|
||||
_gallery:
|
||||
my: "내 갤러리"
|
||||
liked: "좋아요 한 갤러리"
|
||||
|
|
|
@ -647,6 +647,8 @@ goBack: "Wróć"
|
|||
info: "Informacje"
|
||||
user: "Użytkownicy"
|
||||
administration: "Zarządzanie"
|
||||
expiration: "Ankieta kończy się"
|
||||
middle: "Średnie"
|
||||
_gallery:
|
||||
unlike: "Cofnij polubienie"
|
||||
_email:
|
||||
|
|
|
@ -747,6 +747,8 @@ gallery: "Галерея"
|
|||
recentPosts: "Недавние публикации"
|
||||
popularPosts: "Популярные публикации"
|
||||
shareWithNote: "Поделиться заметкой"
|
||||
expiration: "Опрос длится"
|
||||
middle: "Средне"
|
||||
_gallery:
|
||||
my: "Личная"
|
||||
liked: "Понравившееся"
|
||||
|
|
|
@ -689,6 +689,8 @@ goBack: "Назад"
|
|||
info: "Інформація"
|
||||
user: "Користувачі"
|
||||
administration: "Управління"
|
||||
expiration: "Опитування закінчується"
|
||||
middle: "Середній"
|
||||
_gallery:
|
||||
unlike: "Не вподобати"
|
||||
_email:
|
||||
|
|
|
@ -747,11 +747,13 @@ gallery: "图库"
|
|||
recentPosts: "最新发布"
|
||||
popularPosts: "热门投稿"
|
||||
shareWithNote: "在帖子中分享"
|
||||
expiration: "截止时间"
|
||||
middle: "中"
|
||||
_gallery:
|
||||
my: "我的图库"
|
||||
liked: "喜欢的图片"
|
||||
like: "喜欢❤"
|
||||
unlike: "取消赞"
|
||||
like: "喜欢"
|
||||
unlike: "取消喜欢"
|
||||
_email:
|
||||
_follow:
|
||||
title: "你有新的关注者"
|
||||
|
@ -1278,7 +1280,7 @@ _pages:
|
|||
viewSource: "查看源代码"
|
||||
viewPage: "查看页面"
|
||||
like: "赞"
|
||||
unlike: "取消赞"
|
||||
unlike: "取消喜欢"
|
||||
my: "我的页面"
|
||||
liked: "喜欢的页面"
|
||||
featured: "热门"
|
||||
|
|
|
@ -44,7 +44,7 @@ copyLink: "複製連結"
|
|||
delete: "刪除"
|
||||
deleteAndEdit: "刪除並編輯"
|
||||
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有情感、轉發和回覆也將會消失。"
|
||||
addToList: "新增至清單"
|
||||
addToList: "加入至清單"
|
||||
sendMessage: "發送訊息"
|
||||
copyUsername: "複製用戶名"
|
||||
searchUser: "搜尋用戶"
|
||||
|
@ -129,7 +129,7 @@ customEmojis: "自訂表情符號"
|
|||
emoji: "表情符號"
|
||||
emojiName: "表情符號名稱"
|
||||
emojiUrl: "表情符號URL"
|
||||
addEmoji: "新增表情符號"
|
||||
addEmoji: "加入表情符號"
|
||||
settingGuide: "推薦設定"
|
||||
cacheRemoteFiles: "緩存非遠程檔案"
|
||||
cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。"
|
||||
|
@ -218,7 +218,7 @@ newPasswordRetype: "確認密碼"
|
|||
attachFile: "上傳附件"
|
||||
more: "更多!"
|
||||
featured: "精選"
|
||||
usernameOrUserId: "使用者名稱或用戶ID"
|
||||
usernameOrUserId: "使用者名稱或使用者ID"
|
||||
noSuchUser: "使用者不存在"
|
||||
lookup: "查詢"
|
||||
announcements: "公告"
|
||||
|
@ -273,7 +273,7 @@ folderName: "資料夾名稱"
|
|||
createFolder: "新增資料夾"
|
||||
renameFolder: "重新命名資料夾"
|
||||
deleteFolder: "刪除資料夾"
|
||||
addFile: "添加附件"
|
||||
addFile: "加入附件"
|
||||
emptyDrive: "雲端硬碟為空"
|
||||
emptyFolder: "資料夾為空"
|
||||
unableToDelete: "無法刪除"
|
||||
|
@ -693,6 +693,7 @@ editCode: "編輯代碼"
|
|||
apply: "套用"
|
||||
receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知"
|
||||
emailNotification: "郵件通知"
|
||||
publish: "發佈"
|
||||
inChannelSearch: "頻道内搜尋"
|
||||
useReactionPickerForContextMenu: "點擊右鍵開啟回應工具欄"
|
||||
typingUsers: "{users}輸入中..."
|
||||
|
@ -730,6 +731,8 @@ switch: "切換"
|
|||
noMaintainerInformationWarning: "尚未設定管理員信息。"
|
||||
noBotProtectionWarning: "尚未設定Bot防護。"
|
||||
configure: "設定"
|
||||
expiration: "期限"
|
||||
middle: "中"
|
||||
_gallery:
|
||||
unlike: "收回喜歡"
|
||||
_email:
|
||||
|
@ -1473,7 +1476,7 @@ _notification:
|
|||
reply: "回覆"
|
||||
renote: "轉發貼文"
|
||||
quote: "引用"
|
||||
reaction: "情感"
|
||||
reaction: "反應"
|
||||
pollVote: "統計已投票數"
|
||||
receiveFollowRequest: "已收到追隨請求"
|
||||
followRequestAccepted: "追隨請求已接受"
|
||||
|
|
20
migration/1619942102890-password-reset.ts
Normal file
20
migration/1619942102890-password-reset.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class passwordReset1619942102890 implements MigrationInterface {
|
||||
name = 'passwordReset1619942102890'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "password_reset_request" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_fcf4b02eae1403a2edaf87fd074" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0b575fa9a4cfe638a925949285" ON "password_reset_request" ("token") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac" ON "password_reset_request" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "password_reset_request" ADD CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "password_reset_request" DROP CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_0b575fa9a4cfe638a925949285"`);
|
||||
await queryRunner.query(`DROP TABLE "password_reset_request"`);
|
||||
}
|
||||
|
||||
}
|
18
migration/1620019354680-ad.ts
Normal file
18
migration/1620019354680-ad.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class ad1620019354680 implements MigrationInterface {
|
||||
name = 'ad1620019354680'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "ad" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "place" character varying(32) NOT NULL, "priority" character varying(32) NOT NULL, "url" character varying(1024) NOT NULL, "imageUrl" character varying(1024) NOT NULL, "memo" character varying(8192) NOT NULL, CONSTRAINT "PK_0193d5ef09746e88e9ea92c634d" PRIMARY KEY ("id")); COMMENT ON COLUMN "ad"."createdAt" IS 'The created date of the Ad.'; COMMENT ON COLUMN "ad"."expiresAt" IS 'The expired date of the Ad.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_1129c2ef687fc272df040bafaa" ON "ad" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_2da24ce20ad209f1d9dc032457" ON "ad" ("expiresAt") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_2da24ce20ad209f1d9dc032457"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_1129c2ef687fc272df040bafaa"`);
|
||||
await queryRunner.query(`DROP TABLE "ad"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||
"version": "12.79.3",
|
||||
"version": "12.80.0",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -32,7 +32,6 @@
|
|||
"resolutions": {
|
||||
"chokidar": "^3.3.1",
|
||||
"constantinople": "^4.0.1",
|
||||
"gulp/gulp-cli/yargs/yargs-parser": "5.0.0-security.0",
|
||||
"jsonld/rdf-canonize/node-forge": "0.10.0",
|
||||
"lodash": "^4.17.20"
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, h, TransitionGroup } from 'vue';
|
||||
import MkAd from '@client/components/global/ad.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
@ -22,6 +23,11 @@ export default defineComponent({
|
|||
required: false,
|
||||
default: false
|
||||
},
|
||||
ad: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -58,11 +64,7 @@ export default defineComponent({
|
|||
|
||||
if (
|
||||
i != this.items.length - 1 &&
|
||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
|
||||
!item._prId_ &&
|
||||
!this.items[i + 1]._prId_ &&
|
||||
!item._featuredId_ &&
|
||||
!this.items[i + 1]._featuredId_
|
||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
|
||||
) {
|
||||
const separator = h('div', {
|
||||
class: 'separator',
|
||||
|
@ -86,7 +88,15 @@ export default defineComponent({
|
|||
|
||||
return [el, separator];
|
||||
} else {
|
||||
return el;
|
||||
if (this.ad && item._shouldInsertAd_) {
|
||||
return [h(MkAd, {
|
||||
class: 'ad',
|
||||
key: item.id + ':ad',
|
||||
prefer: 'horizontal',
|
||||
}), el];
|
||||
} else {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
class="_button"
|
||||
@click="chosen(emoji, $event)"
|
||||
tabindex="0"
|
||||
:key="emoji"
|
||||
>
|
||||
<MkEmoji :emoji="emoji" :normal="true"/>
|
||||
</button>
|
||||
|
@ -104,7 +105,7 @@ export default defineComponent({
|
|||
return {
|
||||
emojilist: markRaw(emojilist),
|
||||
getStaticImageUrl,
|
||||
pinned: this.$store.state.reactions,
|
||||
pinned: this.$store.reactiveState.reactions,
|
||||
width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
|
||||
height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
|
||||
big: this.asReactionPicker ? isDeviceTouch : false,
|
||||
|
|
71
src/client/components/forgot-password.vue
Normal file
71
src/client/components/forgot-password.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<XModalWindow ref="dialog"
|
||||
:width="370"
|
||||
:height="400"
|
||||
@close="$refs.dialog.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>{{ $ts.forgotPassword }}</template>
|
||||
|
||||
<form class="_monolithic_" @submit.prevent="onSubmit" v-if="$instance.enableEmail">
|
||||
<div class="_section">
|
||||
<MkInput v-model:value="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
|
||||
<span>{{ $ts.username }}</span>
|
||||
<template #prefix>@</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model:value="email" type="email" spellcheck="false" required>
|
||||
<span>{{ $ts.emailAddress }}</span>
|
||||
<template #desc>{{ $ts._forgotPassword.enterEmail }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
|
||||
</div>
|
||||
<div class="_section">
|
||||
<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
{{ $ts._forgotPassword.contactAdmin }}
|
||||
</div>
|
||||
</XModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import XModalWindow from '@client/components/ui/modal-window.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import MkInput from '@client/components/ui/input.vue';
|
||||
import * as os from '@client/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XModalWindow,
|
||||
MkButton,
|
||||
MkInput,
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
email: '',
|
||||
processing: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onSubmit() {
|
||||
this.processing = true;
|
||||
await os.apiWithDialog('request-reset-password', {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
});
|
||||
|
||||
this.$emit('done');
|
||||
this.$refs.dialog.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
142
src/client/components/global/ad.vue
Normal file
142
src/client/components/global/ad.vue
Normal file
|
@ -0,0 +1,142 @@
|
|||
<template>
|
||||
<div class="qiivuoyo" v-if="ad">
|
||||
<div class="main" :class="ad.place" v-if="!showMenu">
|
||||
<a :href="ad.url" target="_blank">
|
||||
<img :src="ad.imageUrl">
|
||||
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="menu" v-else>
|
||||
<div class="body">
|
||||
<div>Ads by {{ host }}</div>
|
||||
<!--<MkButton>{{ $ts.stopThisAd }}</MkButton>-->
|
||||
<button class="_textButton" @click="toggleMenu">{{ $ts.close }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { instance } from '@client/instance';
|
||||
import { host } from '@client/config';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
|
||||
props: {
|
||||
prefer: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
ad: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const showMenu = ref(false);
|
||||
const toggleMenu = () => {
|
||||
showMenu.value = !showMenu.value;
|
||||
};
|
||||
|
||||
let ad = null;
|
||||
|
||||
if (props.ad) {
|
||||
ad = props.ad;
|
||||
} else {
|
||||
let ads = instance.ads.filter(ad => ad.place === props.prefer);
|
||||
|
||||
if (ads.length === 0) {
|
||||
ads = instance.ads.filter(ad => ad.place === 'square');
|
||||
}
|
||||
|
||||
const high = ads.filter(ad => ad.priority === 'high');
|
||||
const middle = ads.filter(ad => ad.priority === 'middle');
|
||||
const low = ads.filter(ad => ad.priority === 'low');
|
||||
|
||||
if (high.length > 0) {
|
||||
ad = high[Math.floor(Math.random() * high.length)];
|
||||
} else if (middle.length > 0) {
|
||||
ad = middle[Math.floor(Math.random() * middle.length)];
|
||||
} else if (low.length > 0) {
|
||||
ad = low[Math.floor(Math.random() * low.length)];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ad,
|
||||
showMenu,
|
||||
toggleMenu,
|
||||
host,
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qiivuoyo {
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
|
||||
|
||||
> .main {
|
||||
> a {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
> .menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: var(--panel);
|
||||
}
|
||||
}
|
||||
|
||||
&.square {
|
||||
> a {
|
||||
max-width: min(300px, 100%);
|
||||
max-height: min(300px, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
padding: 8px;
|
||||
|
||||
> a {
|
||||
max-width: min(600px, 100%);
|
||||
max-height: min(100px, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
> a {
|
||||
max-width: min(100px, 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .menu {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
|
||||
> .body {
|
||||
padding: 8px;
|
||||
margin: 0 auto;
|
||||
max-width: 400px;
|
||||
border: solid 1px var(--divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -113,8 +113,6 @@ export default defineComponent({
|
|||
> .icon {
|
||||
padding-left: 2px;
|
||||
font-size: .9em;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
> .self {
|
||||
|
|
|
@ -12,8 +12,10 @@ import url from './global/url.vue';
|
|||
import i18n from './global/i18n';
|
||||
import loading from './global/loading.vue';
|
||||
import error from './global/error.vue';
|
||||
import ad from './global/ad.vue';
|
||||
|
||||
export default function(app: App) {
|
||||
app.component('I18n', i18n);
|
||||
app.component('Mfm', mfm);
|
||||
app.component('MkA', a);
|
||||
app.component('MkAcct', acct);
|
||||
|
@ -25,5 +27,5 @@ export default function(app: App) {
|
|||
app.component('MkUrl', url);
|
||||
app.component('MkLoading', loading);
|
||||
app.component('MkError', error);
|
||||
app.component('I18n', i18n);
|
||||
app.component('MkAd', ad);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="yohlumlk">
|
||||
<div class="yohlumlk" v-size="{ min: [350, 500] }">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<div class="main">
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
|
@ -50,18 +50,19 @@ export default defineComponent({
|
|||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
font-size: 0.95em;
|
||||
|
||||
> .avatar {
|
||||
|
||||
@media (min-width: 350px) {
|
||||
&.min-width_350px {
|
||||
> .avatar {
|
||||
margin: 0 10px 0 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
&.min-width_500px {
|
||||
> .avatar {
|
||||
margin: 0 12px 0 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</MkButton>
|
||||
</div>
|
||||
|
||||
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap">
|
||||
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true">
|
||||
<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
|
||||
</XList>
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<MkInput v-model:value="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
|
||||
<span>{{ $ts.password }}</span>
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
<template #desc><button class="_textButton" @click="resetPassword">{{ $ts.forgotPassword }}</button></template>
|
||||
</MkInput>
|
||||
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
|
||||
</div>
|
||||
|
@ -49,8 +50,8 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import MkButton from './ui/button.vue';
|
||||
import MkInput from './ui/input.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import MkInput from '@client/components/ui/input.vue';
|
||||
import { apiUrl, host } from '@client/config';
|
||||
import { byteify, hexify } from '@client/scripts/2fa';
|
||||
import * as os from '@client/os';
|
||||
|
@ -197,6 +198,11 @@ export default defineComponent({
|
|||
this.signing = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
resetPassword() {
|
||||
os.popup(import('@client/components/forgot-password.vue'), {}, {
|
||||
}, 'closed');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkAd prefer="horizontal"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
||||
<MkPagination :pagination="otherPostsPagination" #default="{items}">
|
||||
|
|
125
src/client/pages/instance/ads.vue
Normal file
125
src/client/pages/instance/ads.vue
Normal file
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<div class="uqshojas">
|
||||
<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
|
||||
<section class="_card _gap ads" v-for="ad in ads">
|
||||
<div class="_content ad">
|
||||
<MkAd v-if="ad.url" :ad="ad"/>
|
||||
<MkInput v-model:value="ad.url" type="url">
|
||||
<span>URL</span>
|
||||
</MkInput>
|
||||
<MkInput v-model:value="ad.imageUrl">
|
||||
<span>{{ $ts.imageUrl }}</span>
|
||||
</MkInput>
|
||||
<div style="margin: 32px 0;">
|
||||
<MkRadio v-model="ad.place" value="square">square</MkRadio>
|
||||
<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
|
||||
</div>
|
||||
<div style="margin: 32px 0;">
|
||||
{{ $ts.priority }}
|
||||
<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
|
||||
<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
|
||||
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
|
||||
</div>
|
||||
<MkInput v-model:value="ad.expiresAt" type="date">
|
||||
<span>{{ $ts.expiration }}</span>
|
||||
</MkInput>
|
||||
<MkTextarea v-model:value="ad.memo">
|
||||
<span>{{ $ts.memo }}</span>
|
||||
</MkTextarea>
|
||||
<div class="buttons">
|
||||
<MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
<MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import MkInput from '@client/components/ui/input.vue';
|
||||
import MkTextarea from '@client/components/ui/textarea.vue';
|
||||
import MkRadio from '@client/components/ui/radio.vue';
|
||||
import * as os from '@client/os';
|
||||
import * as symbols from '@client/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkRadio,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.ads,
|
||||
icon: 'fas fa-audio-description'
|
||||
},
|
||||
ads: [],
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
os.api('admin/ad/list').then(ads => {
|
||||
this.ads = ads;
|
||||
});
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.ads.unshift({
|
||||
id: null,
|
||||
memo: '',
|
||||
place: 'square',
|
||||
priority: 'middle',
|
||||
url: '',
|
||||
imageUrl: null,
|
||||
expiresAt: null,
|
||||
});
|
||||
},
|
||||
|
||||
remove(ad) {
|
||||
os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('removeAreYouSure', { x: ad.url }),
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
this.ads = this.ads.filter(x => x != ad);
|
||||
os.apiWithDialog('admin/ad/delete', {
|
||||
id: ad.id
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
save(ad) {
|
||||
if (ad.id == null) {
|
||||
os.apiWithDialog('admin/ad/create', {
|
||||
...ad,
|
||||
expiresAt: new Date(ad.expiresAt).getTime()
|
||||
});
|
||||
} else {
|
||||
os.apiWithDialog('admin/ad/update', {
|
||||
...ad,
|
||||
expiresAt: new Date(ad.expiresAt).getTime()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uqshojas {
|
||||
margin: var(--margin);
|
||||
}
|
||||
</style>
|
|
@ -23,6 +23,7 @@
|
|||
<FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink>
|
||||
<FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
|
||||
<FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink>
|
||||
<FormLink :active="page === 'ads'" replace to="/instance/ads"><template #icon><i class="fas fa-audio-description"></i></template>{{ $ts.ads }}</FormLink>
|
||||
<FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
|
@ -102,6 +103,7 @@ export default defineComponent({
|
|||
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
|
||||
case 'files': return defineAsyncComponent(() => import('./files.vue'));
|
||||
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
||||
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
|
||||
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
||||
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
||||
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
<div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
|
||||
<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
|
||||
</div>
|
||||
<MkAd prefer="horizontal"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
||||
<MkPagination :pagination="otherPostsPagination" #default="{items}">
|
||||
|
|
69
src/client/pages/reset-password.vue
Normal file
69
src/client/pages/reset-password.vue
Normal file
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<FormBase v-if="token">
|
||||
<FormInput v-model:value="password" type="password">
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
<span>{{ $ts.newPassword }}</span>
|
||||
</FormInput>
|
||||
|
||||
<FormButton primary @click="save">{{ $ts.save }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormLink from '@client/components/form/link.vue';
|
||||
import FormBase from '@client/components/form/base.vue';
|
||||
import FormGroup from '@client/components/form/group.vue';
|
||||
import FormInput from '@client/components/form/input.vue';
|
||||
import FormButton from '@client/components/form/button.vue';
|
||||
import * as os from '@client/os';
|
||||
import * as symbols from '@client/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormLink,
|
||||
FormInput,
|
||||
FormButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
token: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.resetPassword,
|
||||
icon: 'fas fa-lock'
|
||||
},
|
||||
password: '',
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.token == null) {
|
||||
os.popup(import('@client/components/forgot-password.vue'), {}, {}, 'closed');
|
||||
this.$router.push('/');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async save() {
|
||||
await os.apiWithDialog('reset-password', {
|
||||
token: this.token,
|
||||
password: this.password,
|
||||
});
|
||||
this.$router.push('/');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -10,6 +10,7 @@
|
|||
</div>
|
||||
<FormLink :active="page === 'accounts'" replace to="/settings/accounts"><template #icon><i class="fas fa-users"></i></template>{{ $ts.accounts }}</FormLink>
|
||||
</FormGroup>
|
||||
<FormInfo v-if="emailNotConfigured" warn>{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></FormInfo>
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.basicSettings }}</template>
|
||||
<FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><i class="fas fa-user"></i></template>{{ $ts.profile }}</FormLink>
|
||||
|
@ -58,10 +59,13 @@ import FormLink from '@client/components/form/link.vue';
|
|||
import FormGroup from '@client/components/form/group.vue';
|
||||
import FormBase from '@client/components/form/base.vue';
|
||||
import FormButton from '@client/components/form/button.vue';
|
||||
import FormInfo from '@client/components/form/info.vue';
|
||||
import { scroll } from '@client/scripts/scroll';
|
||||
import { signout } from '@client/account';
|
||||
import { unisonReload } from '@client/scripts/unison-reload';
|
||||
import * as symbols from '@client/symbols';
|
||||
import { instance } from '@client/instance';
|
||||
import { $i } from '@client/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@ -69,6 +73,7 @@ export default defineComponent({
|
|||
FormLink,
|
||||
FormGroup,
|
||||
FormButton,
|
||||
FormInfo,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -173,6 +178,8 @@ export default defineComponent({
|
|||
}
|
||||
});
|
||||
|
||||
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
|
||||
|
||||
return {
|
||||
[symbols.PAGE_INFO]: INFO,
|
||||
page,
|
||||
|
@ -182,6 +189,7 @@ export default defineComponent({
|
|||
onInfo,
|
||||
pageProps,
|
||||
component,
|
||||
emailNotConfigured,
|
||||
logout: () => {
|
||||
signout();
|
||||
},
|
||||
|
|
|
@ -70,6 +70,8 @@ export default defineComponent({
|
|||
border-radius: var(--radius);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 500px;
|
||||
margin: 32px auto;
|
||||
|
||||
> h1 {
|
||||
margin: 0;
|
||||
|
|
|
@ -23,6 +23,7 @@ export const router = createRouter({
|
|||
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
|
||||
{ path: '/@:acct/room', props: true, component: page('room/room') },
|
||||
{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
|
||||
{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
|
||||
{ path: '/announcements', component: page('announcements') },
|
||||
{ path: '/about', component: page('about') },
|
||||
{ path: '/about-misskey', component: page('about-misskey') },
|
||||
|
|
|
@ -91,8 +91,10 @@ export default (opts) => ({
|
|||
...params,
|
||||
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
|
||||
}).then(items => {
|
||||
for (const item of items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
markRaw(item);
|
||||
if (i === 3) item._shouldInsertAd_ = true;
|
||||
}
|
||||
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
|
||||
items.pop();
|
||||
|
@ -128,8 +130,10 @@ export default (opts) => ({
|
|||
untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
|
||||
}),
|
||||
}).then(items => {
|
||||
for (const item of items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
markRaw(item);
|
||||
if (i === 10) item._shouldInsertAd_ = true;
|
||||
}
|
||||
if (items.length > SECOND_FETCH_LIMIT) {
|
||||
items.pop();
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
@media (max-width: 500px) {
|
||||
--margin: var(--marginHalf);
|
||||
}
|
||||
|
||||
//--ad: rgb(255 169 0 / 10%);
|
||||
}
|
||||
|
||||
::selection {
|
||||
|
@ -337,7 +339,7 @@ hr {
|
|||
}
|
||||
|
||||
._monolithic_ {
|
||||
._section {
|
||||
._section:not(:empty) {
|
||||
box-sizing: border-box;
|
||||
padding: var(--root-margin, 32px);
|
||||
|
||||
|
|
|
@ -42,11 +42,7 @@ export default defineComponent({
|
|||
|
||||
if (
|
||||
i != this.items.length - 1 &&
|
||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
|
||||
!item._prId_ &&
|
||||
!this.items[i + 1]._prId_ &&
|
||||
!item._featuredId_ &&
|
||||
!this.items[i + 1]._featuredId_
|
||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
|
||||
) {
|
||||
const separator = h('div', {
|
||||
class: 'separator',
|
||||
|
|
|
@ -313,7 +313,7 @@ export default defineComponent({
|
|||
}
|
||||
};
|
||||
if (isLink(e.target)) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
const path = this.$route.path;
|
||||
os.contextMenu([{
|
||||
|
|
|
@ -64,7 +64,7 @@ export default defineComponent({
|
|||
}
|
||||
};
|
||||
if (isLink(e.target)) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
const path = this.$route.path;
|
||||
os.contextMenu([{
|
||||
|
|
|
@ -165,7 +165,7 @@ export default defineComponent({
|
|||
}
|
||||
};
|
||||
if (isLink(e.target)) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
const path = this.$route.path;
|
||||
os.contextMenu([{
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="efzpzdvf">
|
||||
<XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
|
||||
<MkAd prefer="square"/>
|
||||
|
||||
<button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
|
||||
<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
|
||||
|
|
|
@ -191,7 +191,7 @@ export default defineComponent({
|
|||
}
|
||||
};
|
||||
if (isLink(e.target)) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
const path = this.$route.path;
|
||||
os.contextMenu([{
|
||||
|
|
|
@ -70,6 +70,8 @@ import { Channel } from '../models/entities/channel';
|
|||
import { ChannelFollowing } from '../models/entities/channel-following';
|
||||
import { ChannelNotePining } from '../models/entities/channel-note-pining';
|
||||
import { RegistryItem } from '../models/entities/registry-item';
|
||||
import { Ad } from '../models/entities/ad';
|
||||
import { PasswordResetRequest } from '@/models/entities/password-reset-request';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
|
||||
|
@ -169,6 +171,8 @@ export const entities = [
|
|||
ChannelFollowing,
|
||||
ChannelNotePining,
|
||||
RegistryItem,
|
||||
Ad,
|
||||
PasswordResetRequest,
|
||||
...charts as any
|
||||
];
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ Theme codes are saved as a JSON5 object of theme options. Themes are composed of
|
|||
* `props` ... The style definitions of the theme.These will be explained in the following.
|
||||
|
||||
### Theme style definitions
|
||||
Define the style of the theme within `props`. The keys will become CSS variables, and the value specifies the content. In addition, the default `props` options are inherited from the base theme. If this theme's `base` is `light`, they will be copied from [_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5), if it is `dark` they will be copied from [_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5). In other words, if there is for example no `panel` key contained in `props`, then the value of `panel` from the base theme will be used.
|
||||
Define the style of the theme within `props`. The keys will become CSS variables names, and the value specifies the content. In addition, the default `props` options are inherited from the base theme. If this theme's `base` is `light`, they will be copied from [_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5), and if it is `dark`, they will be copied from [_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5). In other words, if there is for example no `panel` key contained in `props`, then the value of `panel` from the base theme will be used.
|
||||
|
||||
#### Syntax for values
|
||||
* Hex colors
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# AiScript
|
||||
|
||||
## funciones
|
||||
デフォルトで値渡しです。
|
||||
Pasando valores por defecto
|
||||
|
|
|
@ -1,74 +1,74 @@
|
|||
# プラグインの作成
|
||||
Misskey Webクライアントのプラグイン機能を使うと、クライアントを拡張し、様々な機能を追加できます。 ここではプラグインの作成にあたってのメタデータ定義や、AiScript APIリファレンスを掲載します。
|
||||
# Création d'un plugin
|
||||
En utilisant la fonction plugin du client web Misskey, vous pouvez étendre et y ajouter de nouvelles fonctionnalités. Cette page liste la définition des métadonnées et les références de l'API AIScript pour la création des plugins.
|
||||
|
||||
## Métadonnées
|
||||
プラグインは、AiScriptのメタデータ埋め込み機能を使って、デフォルトとしてプラグインのメタデータを定義する必要があります。 メタデータは次のプロパティを含むオブジェクトです。
|
||||
Les plugins doivent définir des métadonnées de plugin par défaut via le format de métadonnées AiScript. Les métadonnées sont un objet contenant les propriétés suivantes :
|
||||
|
||||
### name
|
||||
プラグイン名
|
||||
Nom du plugin.
|
||||
|
||||
### author
|
||||
プラグイン作者
|
||||
Nom de l'auteur du plugin.
|
||||
|
||||
### version
|
||||
プラグインバージョン。数値を指定してください。
|
||||
Version du plugin.Cette valeur doit être un nombre.
|
||||
|
||||
### description
|
||||
プラグインの説明
|
||||
Description du plugin.
|
||||
|
||||
### permissions
|
||||
プラグインが要求する権限。MisskeyAPIにリクエストする際に用いられます。
|
||||
Permissions requises par le plugin.Utilisé pour les requêtes de l'API Misskey.
|
||||
|
||||
### config
|
||||
プラグインの設定情報を表すオブジェクト。 キーに設定名、値に以下のプロパティを含めます。
|
||||
Un objet représentant les paramètres du plugin. Les clés représentent les noms des paramètres et les valeurs sont l'une des propriétés ci-dessous.
|
||||
|
||||
#### type
|
||||
設定値の種類を表す文字列。以下から選択します。 string number boolean
|
||||
Une chaîne de caractères représentant le type de valeur du paramètre.Sélectionnez l'une des options suivantes : string number boolean
|
||||
|
||||
#### label
|
||||
ユーザーに表示する設定名
|
||||
Nom du paramètre affiché à l'utilisateur.
|
||||
|
||||
#### description
|
||||
設定の説明
|
||||
Description du paramètre
|
||||
|
||||
#### default
|
||||
設定のデフォルト値
|
||||
Valeur par défaut du paramètre
|
||||
|
||||
## Références API de Misskey
|
||||
AiScript標準で組み込まれているAPIは掲載しません。
|
||||
L'API intégrée directement dans la norme AiScript elle-même ne sera pas répertoriée.
|
||||
|
||||
### Mk:dialog(title text type)
|
||||
ダイアログを表示します。typeには以下の値が設定できます。 info success warn error question 省略すると info になります。
|
||||
Affiche la boîte de dialogue.type peut être défini par les valeurs suivantes. info success warn error question Si elle est omise, c'est "info" qui est utilisée.
|
||||
|
||||
### Mk:confirm(title text type)
|
||||
確認ダイアログを表示します。typeには以下の値が設定できます。 info success warn error question 省略すると question になります。 ユーザーが"OK"を選択した場合は true を、"キャンセル"を選択した場合は false が返ります。
|
||||
Affiche une boîte de dialogue de confirmation.Le type peut être défini par les valeurs suivantes. info success warn error question Si elle est omise, c'est "question" qui est utilisé par défaut. Si l'utilisateur sélectionne "OK", true est renvoyé, si l'utilisateur sélectionne "Cancel", false est renvoyé.
|
||||
|
||||
### Mk:api(endpoint params)
|
||||
Misskey APIにリクエストします。第一引数にエンドポイント名、第二引数にパラメータオブジェクトを渡します。
|
||||
Envoie une requête à l'API Misskey.Le premier paramètre spécifie le point de terminaison de l'API, le second spécifie les paramètres de la requête sous forme d'objet.
|
||||
|
||||
### Mk:save(key value)
|
||||
任意の値に任意の名前を付けて永続化します。永続化した値は、AiScriptコンテキストが終了しても残り、Mk:loadで読み取ることができます。
|
||||
Fait persister une valeur arbitraire avec un nom arbitraire.La valeur persistante reste après la fin du contexte AiScript et peut être lue par Mk:load.
|
||||
|
||||
### Mk:load(key)
|
||||
Mk:saveで永続化した指定の名前の値を読み取ります。
|
||||
Lit la valeur du nom spécifié persisté par Mk:save.
|
||||
|
||||
### Plugin:register_post_form_action(title fn)
|
||||
投稿フォームにアクションを追加します。第一引数にアクション名、第二引数にアクションが選択された際のコールバック関数を渡します。 コールバック関数には、第一引数に投稿フォームオブジェクトが渡されます。
|
||||
Ajoute une action au formulaire de soumission.Le premier argument est le nom de l'action, le second est la fonction de rappel lorsque l'action est sélectionnée. La fonction de rappel reçoit l'objet du formulaire de soumission comme premier argument.
|
||||
|
||||
### Plugin:register_note_action(title fn)
|
||||
ノートメニューに項目を追加します。第一引数に項目名、第二引数に項目が選択された際のコールバック関数を渡します。 コールバック関数には、第一引数に対象のノートオブジェクトが渡されます。
|
||||
Ajoute un élément au menu note. Le premier paramètre spécifie le nom de l'action, le second paramètre spécifie une fonction de rappel qui est exécutée lorsque cet élément est sélectionné. La fonction de rappel reçoit un objet note comme premier paramètre.
|
||||
|
||||
### Plugin:register_user_action(title fn)
|
||||
ユーザーメニューに項目を追加します。第一引数に項目名、第二引数に項目が選択された際のコールバック関数を渡します。 コールバック関数には、第一引数に対象のユーザーオブジェクトが渡されます。
|
||||
Ajoute un élément au menu de l'utilisateur.Le premier paramètre spécifie le nom de l'action, le second paramètre spécifie une fonction de rappel qui est exécutée lorsque cet élément est sélectionné. La fonction de rappel reçoit un objet utilisateur comme premier paramètre.
|
||||
|
||||
### Plugin:register_note_view_interruptor(fn)
|
||||
UIに表示されるノート情報を書き換えます。 コールバック関数には、第一引数に対象のノートオブジェクトが渡されます。 コールバック関数の返り値でノートが書き換えられます。
|
||||
Réécrit les informations de la note affichée dans l'interface utilisateur. L'objet note cible est passé comme premier argument à la fonction de rappel. La note est réécrite dans la valeur de retour de la fonction de rappel.
|
||||
|
||||
### Plugin:register_note_post_interruptor(fn)
|
||||
ノート投稿時にノート情報を書き換えます。 コールバック関数には、第一引数に対象のノートオブジェクトが渡されます。 コールバック関数の返り値でノートが書き換えられます。
|
||||
Réécrit les informations de la note lors de la publication d'une note. L'objet note cible est passé comme premier argument à la fonction de rappel. La note sera réécrite dans la valeur de retour de la fonction de rappel.
|
||||
|
||||
### Plugin:open_url(url)
|
||||
第一引数に渡されたURLをブラウザの新しいタブで開きます。
|
||||
Ouvre l'URL passée comme premier argument dans un nouvel onglet du navigateur.
|
||||
|
||||
### Plugin:config
|
||||
プラグインの設定が格納されるオブジェクト。プラグイン定義のconfigで設定したキーで値が入ります。
|
||||
Un objet dans lequel la configuration du plugin est stockée.La valeur est saisie par la clé définie dans la configuration de la définition du plugin.
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
# MisskeyリバーシBotの開発
|
||||
Misskeyのリバーシ機能に対応したBotの開発方法をここに記します。
|
||||
# Développement du bot Reversi de Misskey
|
||||
Cette page explique comment développer un bot pour la fonction Reversi de Misskey.
|
||||
|
||||
1. `games/reversi`ストリームに以下のパラメータを付けて接続する:
|
||||
* `i`: botアカウントのAPIキー
|
||||
1. Connectez-vous au flux `games/reversi` avec les paramètres suivants :
|
||||
* `i` : Clé API pour le compte du bot
|
||||
|
||||
2. 対局への招待が来たら、ストリームから`invited`イベントが流れてくる
|
||||
* イベントの中身に、`parent`という名前で対局へ誘ってきたユーザーの情報が含まれている
|
||||
2. Lorsqu'une invitation à un jeu arrive, un événement `invited` sera lancé à partir du flux.
|
||||
* Le contenu de cet événement est un attribut `parent`, qui contient des informations sur l'utilisateur qui a envoyé l'invitation.
|
||||
|
||||
3. `games/reversi/match`へ、`user_id`として`parent`の`id`が含まれたリクエストを送信する
|
||||
3. Envoie une requête à `games/reversi/match`, où la valeur du paramètre `user_id` est l'attribut `id` de l'objet `parent` obtenu précédemment.
|
||||
|
||||
4. 上手くいくとゲーム情報が返ってくるので、`games/reversi-game`ストリームへ、以下のパラメータを付けて接続する:
|
||||
* `i`: botアカウントのAPIキー
|
||||
* `game`: `game`の`id`
|
||||
4. Si la requête fonctionne, les informations sur le jeu seront renvoyées et vous pourrez vous connecter au flux `games/reversi-game` avec les paramètres suivants :
|
||||
* `i` : Clé API pour le compte du bot
|
||||
* `game`: `game` de `id`
|
||||
|
||||
5. この間、相手がゲームの設定を変更するとその都度`update-settings`イベントが流れてくるので、必要であれば何かしらの処理を行う
|
||||
5. Pendant ce temps, l'adversaire peut modifier les paramètres du jeu. Chaque fois qu'un paramètre est modifié, le flux envoie un événement `update-settings`, donc une logique pour gérer ces événements peut être nécessaire.
|
||||
|
||||
6. 設定に満足したら、`{ type: 'accept' }`メッセージをストリームに送信する
|
||||
6. Une fois que vous êtes satisfait·e des paramètres du jeu, envoyez le message `{ type : 'accept' }` au flux.
|
||||
|
||||
7. ゲームが開始すると、`started`イベントが流れてくる
|
||||
* イベントの中身にはゲーム情報が含まれている
|
||||
7. Lorsque le jeu commence, l'événement `started` sera envoyé.
|
||||
* Les informations sur l'état du jeu seront inclus dans cet événement.
|
||||
|
||||
8. 石を打つには、ストリームに`{ type: 'set', pos: <位置> }`を送信する(位置の計算方法は後述)
|
||||
8. Pour placer une pierre, envoyez `{ type : 'set', pos : <Position> ; }` au flux (voir ci-dessous pour savoir comment calculer la position).
|
||||
|
||||
9. 相手または自分が石を打つと、ストリームから`set`イベントが流れてくる
|
||||
* `color`として石の色が含まれている
|
||||
* `pos`として位置情報が含まれている
|
||||
9. Lorsque votre adversaire ou vous-même placez une pierre, un événement `set` est envoyé depuis le flux.
|
||||
* `color` contient la couleur de la pierre placée
|
||||
* `pos` contient la position de la pierre
|
||||
|
||||
## 位置の計算法
|
||||
8x8のマップを考える場合、各マスの位置(インデックスと呼びます)は次のようになっています:
|
||||
## Calculer la position
|
||||
Si nous considérons une carte 8x8, la position de chaque carré (appelée index) est la suivante :
|
||||
```
|
||||
+--+--+--+--+--+--+--+--+
|
||||
| 0| 1| 2| 3| 4| 5| 6| 7|
|
||||
|
@ -38,29 +38,29 @@ Misskeyのリバーシ機能に対応したBotの開発方法をここに記し
|
|||
...
|
||||
```
|
||||
|
||||
### X,Y座標 から インデックス に変換する
|
||||
### Trouver les index à partir des coordonnées X, Y
|
||||
```
|
||||
pos = x + (y * mapWidth)
|
||||
```
|
||||
`mapWidth`は、ゲーム情報の`map`から、次のようにして計算できます:
|
||||
`mapWidth` est une donnée de la carte prise sur la `map` comme suit :
|
||||
```
|
||||
mapWidth = map[0].length
|
||||
```
|
||||
|
||||
### インデックス から X,Y座標 に変換する
|
||||
### Trouver les coordonnées X, Y depuis l'index
|
||||
```
|
||||
x = pos % mapWidth
|
||||
y = Math.floor(pos / mapWidth)
|
||||
```
|
||||
|
||||
## マップ情報
|
||||
マップ情報は、ゲーム情報の`map`に入っています。 文字列の配列になっており、ひとつひとつの文字がマス情報を表しています。 それをもとにマップのデザインを知る事が出来ます:
|
||||
* `(スペース)` ... マス無し
|
||||
* `-` ... マス
|
||||
* `b` ... 初期配置される黒石
|
||||
* `w` ... 初期配置される白石
|
||||
## Information sur la carte
|
||||
Les données de la carte sont incluses dans `map` dans les données du jeu. Comme les données sont représentées sous la forme d'un tableau de chaînes de caractères, chaque caractère représente un champ. Sur la base de ces données, vous pouvez reconstruire l'état de la carte :
|
||||
* `(Vide)` ... Aucun champ
|
||||
* `-` ... Champ
|
||||
* `b` ... La première pierre placée est noire
|
||||
* `w` ... La première pierre placée est blanche
|
||||
|
||||
例えば、4*4の次のような単純なマップがあるとします:
|
||||
Par exemple, supposons que nous ayons la carte simple suivante de 4×4 :
|
||||
```text
|
||||
+---+---+---+---+
|
||||
| | | | |
|
||||
|
@ -73,23 +73,23 @@ y = Math.floor(pos / mapWidth)
|
|||
+---+---+---+---+
|
||||
```
|
||||
|
||||
この場合、マップデータはこのようになります:
|
||||
Dans ce cas, les données de la carte ressembleront à ceci :
|
||||
```javascript
|
||||
['----', '-wb-', '-bw-', '----']
|
||||
```
|
||||
|
||||
## ユーザーにフォームを提示して対話可能Botを作成する
|
||||
ユーザーとのコミュニケーションを行うため、ゲームの設定画面でユーザーにフォームを提示することができます。 例えば、Botの強さをユーザーが設定できるようにする、といったシナリオが考えられます。
|
||||
## Créer un Bot interactif en présentant un formulaire à l'utilisateur.
|
||||
Afin de communiquer avec l'utilisateur, un formulaire peut être présenté à l'utilisateur sur l'écran des paramètres du jeu. Par exemple, un scénario pourrait consister à permettre à l'utilisateur de définir la force du bot.
|
||||
|
||||
フォームを提示するには、`reversi-game`ストリームに次のメッセージを送信します:
|
||||
Pour présenter le formulaire, envoyez le message suivant au flux `reversi-game` :
|
||||
```javascript
|
||||
{
|
||||
type: 'init-form',
|
||||
body: [フォームコントロールの配列]
|
||||
body: [Tableau de contrôles de formulaires]
|
||||
}
|
||||
```
|
||||
|
||||
フォームコントロールの配列については今から説明します。 フォームコントロールは、次のようなオブジェクトです:
|
||||
Nous allons maintenant expliquer le tableau des contrôles de formulaires. Un contrôle de formulaire est un objet qui ressemble à ce qui suit :
|
||||
```javascript
|
||||
{
|
||||
id: 'switch1',
|
||||
|
@ -98,10 +98,10 @@ y = Math.floor(pos / mapWidth)
|
|||
value: false
|
||||
}
|
||||
```
|
||||
`id` ... コントロールのID。 `type` ... コントロールの種類。後述します。 `label` ... コントロールと一緒に表記するテキスト。 `value` ... コントロールのデフォルト値。
|
||||
`id` ... ID de l'élément de contrôle. `type` ... Le type d'élément de contrôle. Nous y reviendrons plus tard. Texte affiché à côté de l'élément de contrôle. `value` ... La valeur par défaut de l'élément de contrôle.
|
||||
|
||||
### フォームの操作を受け取る
|
||||
ユーザーがフォームを操作すると、ストリームから`update-form`イベントが流れてきます。 イベントの中身には、コントロールのIDと、ユーザーが設定した値が含まれています。 例えば、上で示したスイッチをユーザーがオンにしたとすると、次のイベントが流れてきます:
|
||||
### Gestion des interactions avec les formulaires
|
||||
Lorsqu'un utilisateur interagit avec le formulaire, un événement `update-form` est envoyé par le flux. Le contenu de l'événement contient l'ID du contrôle et la valeur définie par l'utilisateur. Par exemple, si l'utilisateur allume l'interrupteur illustré ci-dessus, l'événement suivant sera diffusé :
|
||||
```javascript
|
||||
{
|
||||
id: 'switch1',
|
||||
|
@ -109,52 +109,52 @@ y = Math.floor(pos / mapWidth)
|
|||
}
|
||||
```
|
||||
|
||||
### フォームコントロールの種類
|
||||
### Types d'éléments de contrôles de formulaires
|
||||
#### Interrupteur
|
||||
type: `switch` スイッチを表示します。何かの機能をオン/オフさせたい場合に有用です。
|
||||
type: `switch` Affiche un interrupteur.Cette fonction est utile lorsque vous souhaitez activer ou désactiver une fonction.
|
||||
|
||||
##### プロパティ
|
||||
`label` ... スイッチに表記するテキスト。
|
||||
##### Propriétés
|
||||
`label` ... Texte à marquer sur l'interrupteur.
|
||||
|
||||
#### ラジオボタン
|
||||
type: `radio` ラジオボタンを表示します。選択肢を提示するのに有用です。例えば、Botの強さを設定させるなどです。
|
||||
#### Boutons radio
|
||||
type: `radio` Affiche le bouton radio.Il est utile pour proposer des options.Par exemple, pour choisir la difficulté du bot.
|
||||
|
||||
##### プロパティ
|
||||
`items` ... ラジオボタンの選択肢。例:
|
||||
##### Propriétés
|
||||
`items` ... Les options des boutons radio. Par exemple :
|
||||
```javascript
|
||||
items: [{
|
||||
label: '弱',
|
||||
label: 'Facile',
|
||||
value: 1
|
||||
}, {
|
||||
label: '中',
|
||||
label: 'Moyen',
|
||||
value: 2
|
||||
}, {
|
||||
label: '強',
|
||||
label: 'Difficile',
|
||||
value: 3
|
||||
}]
|
||||
```
|
||||
|
||||
#### スライダー
|
||||
type: `slider` スライダーを表示します。
|
||||
#### Glissière
|
||||
type: `slider` Affiche une glissière.
|
||||
|
||||
##### プロパティ
|
||||
`min` ... スライダーの下限。 `max` ... スライダーの上限。 `step` ... 入力欄で刻むステップ値。
|
||||
##### Propriétés
|
||||
`min` ... Limite minimum de la glissière. `max` ... Limite maximum de la glissière. `step` ... Étapes entre les valeurs de la glissière.
|
||||
|
||||
#### テキストボックス
|
||||
type: `textbox` テキストボックスを表示します。ユーザーになにか入力させる一般的な用途に利用できます。
|
||||
#### Zones de texte
|
||||
type: `textbox` Affiche une zone de texte.Cette fonction peut être utilisée à des fins générales lorsque vous souhaitez que l'utilisateur tape quelque chose.
|
||||
|
||||
## ユーザーにメッセージを表示する
|
||||
設定画面でユーザーと対話する、フォーム以外のもうひとつの方法がこれです。ユーザーになにかメッセージを表示することができます。 例えば、ユーザーがBotの対応していないモードやマップを選択したとき、警告を表示するなどです。 メッセージを表示するには、次のメッセージをストリームに送信します:
|
||||
## Afficher un message à l'utilisateur
|
||||
C'est un autre moyen, autre que les formulaires, d'interagir avec les utilisateurs dans l'écran de configuration.Vous pouvez afficher un message à l'intention de l'utilisateur. Par exemple, vous pouvez afficher un avertissement lorsque l'utilisateur sélectionne un mode ou une carte qui n'est pas pris en charge par le Bot. Pour afficher un message, envoyez le message suivant au flux :
|
||||
```javascript
|
||||
{
|
||||
type: 'message',
|
||||
body: {
|
||||
text: 'メッセージ内容',
|
||||
type: 'メッセージの種類'
|
||||
text: 'contenu du message',
|
||||
type: 'Type du message'
|
||||
}
|
||||
}
|
||||
```
|
||||
メッセージの種類: `success`, `info`, `warning`, `error`。
|
||||
Type de message : `success`, `info`, `warning`, `error`.
|
||||
|
||||
## 投了する
|
||||
投了をするには、<a href="./api/endpoints/games/reversi/games/surrender">このエンドポイント</a>にリクエストします。
|
||||
## Abandonner
|
||||
Pour se rendre, faites une demande à <a href="./api/endpoints/games/reversi/games/surrender">cette terminaison</a>.
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
# API Stream
|
||||
# API streaming
|
||||
|
||||
L'API Stream permet d'implémenter l'exécution d'opérations variées et la réception de diverses informations en temps réel. Cela concerne, par exemple, l'affichage des nouvelles publications dans les fils, la réception de nouveaux messages, les nouveaux abonnements, etc.
|
||||
L'API Streaming permet d'implémenter l'exécution d'opérations variées et la réception de diverses informations en temps réel. Cela concerne, par exemple, l'affichage des nouvelles publications dans les fils, la réception de nouveaux messages, les nouveaux abonnements, etc.
|
||||
|
||||
## ストリームに接続する
|
||||
## Se connecter aux flux
|
||||
|
||||
ストリーミングAPIを利用するには、まずMisskeyサーバーに**websocket**接続する必要があります。
|
||||
Pour utiliser l'API de streaming, vous devez d'abord effectuer une connexion **websocket** au serveur Misskey.
|
||||
|
||||
以下のURLに、`i`というパラメータ名で認証情報を含めて、websocket接続してください。例:
|
||||
Veuillez vous connecter à l'URL suivante avec le nom de paramètre `i` et inclure les informations d'authentification dans la connexion websocket.Par exemple :
|
||||
```
|
||||
%WS_URL%/streaming?i=xxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
認証情報は、自分のAPIキーや、アプリケーションからストリームに接続する際はユーザーのアクセストークンのことを指します。
|
||||
Les informations d'identification sont votre clé API ou, en cas de connexion au flux depuis votre application, le jeton d'accès de l'utilisateur.
|
||||
|
||||
<div class="ui info">
|
||||
<p><i class="fas fa-info-circle"></i> 認証情報の取得については、<a href="./api">こちらのドキュメント</a>をご確認ください。</p>
|
||||
<p><i class="fas fa-info-circle"></i> Pour obtenir des informations sur l'obtention d'accréditations, veuillez consulter <a href="./api">ce document</a>.</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
認証情報は省略することもできますが、その場合非ログインでの利用ということになり、受信できる情報や可能な操作は限られます。例:
|
||||
Vous pouvez omettre les informations d'authentification, mais dans ce cas, vous utiliserez le système sans vous connecter, et les informations que vous pourrez recevoir et les opérations que vous pourrez effectuer seront limitées.Par exemple :
|
||||
|
||||
```
|
||||
%WS_URL%/streaming
|
||||
|
@ -27,15 +27,15 @@ L'API Stream permet d'implémenter l'exécution d'opérations variées et la ré
|
|||
|
||||
---
|
||||
|
||||
ストリームに接続すると、後述するAPI操作や、投稿の購読を行ったりすることができます。 しかしまだこの段階では、例えばタイムラインへの新しい投稿を受信したりすることはできません。 それを行うには、ストリーム上で、後述する**チャンネル**に接続する必要があります。
|
||||
Une fois que vous êtes connecté au flux, vous pouvez utiliser l'API comme décrit ci-dessous, ou vous abonner aux messages. Cependant, à ce stade, vous ne pouvez pas recevoir de nouveaux messages sur votre fil, par exemple. Pour ce faire, vous devez vous connecter à un **canal** sur le flux, comme décrit ci-dessous.
|
||||
|
||||
**ストリームでのやり取りはすべてJSONです。**
|
||||
**Toutes les interactions dans le flux sont JSON.**
|
||||
|
||||
## Canaux
|
||||
MisskeyのストリーミングAPIにはチャンネルという概念があります。これは、送受信する情報を分離するための仕組みです。 Misskeyのストリームに接続しただけでは、まだリアルタイムでタイムラインの投稿を受信したりはできません。 ストリーム上でチャンネルに接続することで、様々な情報を受け取ったり情報を送信したりすることができるようになります。
|
||||
L'API de streaming de Misskey possède le concept de canaux.Il s'agit d'un mécanisme permettant de séparer les informations que vous envoyez et recevez. Si vous vous connectez simplement à un flux Misskey, vous ne pourrez pas encore recevoir les messages de votre timeline en temps réel. En vous connectant aux canaux du flux, vous pourrez recevoir diverses informations et en envoyer.
|
||||
|
||||
### チャンネルに接続する
|
||||
チャンネルに接続するには、次のようなデータをJSONでストリームに送信します:
|
||||
### Se connecter à un canal
|
||||
Pour se connecter à un canal, envoyez les données suivantes au flux en JSON :
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -50,19 +50,19 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
|
|||
}
|
||||
```
|
||||
|
||||
ここで、
|
||||
* `channel`には接続したいチャンネル名を設定します。チャンネルの種類については後述します。
|
||||
* `id`にはそのチャンネルとやり取りするための任意のIDを設定します。ストリームでは様々なメッセージが流れるので、そのメッセージがどのチャンネルからのものなのか識別する必要があるからです。このIDは、UUIDや、乱数のようなもので構いません。
|
||||
* `params`はチャンネルに接続する際のパラメータです。チャンネルによって接続時に必要とされるパラメータは異なります。パラメータ不要のチャンネルに接続する際は、このプロパティは省略可能です。
|
||||
Ici,
|
||||
* Définissez `channel` au nom du canal auquel vous voulez vous connecter.Les types de canaux sont décrits ci-dessous.
|
||||
* `id` est un identifiant arbitraire pour interagir avec ce canal.En effet, le flux contient une variété de messages, et nous devons identifier de quel canal provient le message.Cet ID peut être un UUID ou une sorte de numéro aléatoire.
|
||||
* `params` sont les paramètres utilisés pour se connecter au canal.Les différents canaux nécessitent des paramètres différents pour la connexion.Lors de la connexion à un canal qui ne nécessite pas de paramètres, cette propriété peut être omise.
|
||||
|
||||
<div class="ui info">
|
||||
<p><i class="fas fa-info-circle"></i> IDはチャンネルごとではなく「チャンネルの接続ごと」です。なぜなら、同じチャンネルに異なるパラメータで複数接続するケースもあるからです。</p>
|
||||
<p><i class="fas fa-info-circle"></i> L'ID est "par connexion de canal", et non par canal. En effet, dans certains cas, plusieurs connexions sont établies sur le même canal avec des paramètres différents.</p>
|
||||
</div>
|
||||
|
||||
### チャンネルからのメッセージを受け取る
|
||||
例えばタイムラインのチャンネルなら、新しい投稿があった時にメッセージを発します。そのメッセージを受け取ることで、タイムラインに新しい投稿がされたことをリアルタイムで知ることができます。
|
||||
### Recevoir des messages du canal
|
||||
Par exemple, lorsqu'un événement est émis dans l'un des canaux du fil en raison de la publication d'un nouveau message.En recevant ce message, vous saurez en temps réel qu'une nouvelle publication a été faite sur votre fil.
|
||||
|
||||
チャンネルがメッセージを発すると、次のようなデータがJSONでストリームに流れてきます:
|
||||
Lorsqu'un canal émet un message, les données suivantes sont diffusées en JSON :
|
||||
```json
|
||||
{
|
||||
type: 'channel',
|
||||
|
@ -76,15 +76,15 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
|
|||
}
|
||||
```
|
||||
|
||||
ここで、
|
||||
* `id`には前述したそのチャンネルに接続する際に設定したIDが設定されています。これで、このメッセージがどのチャンネルからのものなのか知ることができます。
|
||||
* `type`にはメッセージの種類が設定されます。チャンネルによって、どのような種類のメッセージが流れてくるかは異なります。
|
||||
* `body`にはメッセージの内容が設定されます。チャンネルによって、どのような内容のメッセージが流れてくるかは異なります。
|
||||
Ici,
|
||||
* `id` est réglé sur l'ID que vous avez défini lors de la connexion à ce canal comme décrit ci-dessus.Cela vous permettra de savoir de quel canal provient ce message.
|
||||
* `type` est défini comme le type du message.Le type de message qui sera diffusé dépend du canal.
|
||||
* `body` est défini comme le contenu du message.En fonction du canal, le type de message qui sera diffusé dépendra du canal.
|
||||
|
||||
### チャンネルに向けてメッセージを送信する
|
||||
チャンネルによっては、メッセージを受け取るだけでなく、こちらから何かメッセージを送信し、何らかの操作を行える場合があります。
|
||||
### Envoi d'un message à un canal
|
||||
Selon le canal, il se peut que vous ne receviez pas seulement des messages, mais que vous puissiez également envoyer certains messages et effectuer certaines opérations.
|
||||
|
||||
チャンネルにメッセージを送信するには、次のようなデータをJSONでストリームに送信します:
|
||||
Pour envoyer un message à un canal, envoyez les données suivantes au flux en JSON :
|
||||
```json
|
||||
{
|
||||
type: 'channel',
|
||||
|
@ -98,13 +98,13 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
|
|||
}
|
||||
```
|
||||
|
||||
ここで、
|
||||
* `id`には前述したそのチャンネルに接続する際に設定したIDを設定します。これで、このメッセージがどのチャンネルに向けたものなのか識別させることができます。
|
||||
* `type`にはメッセージの種類を設定します。チャンネルによって、どのような種類のメッセージを受け付けるかは異なります。
|
||||
* `body`にはメッセージの内容を設定します。チャンネルによって、どのような内容のメッセージを受け付けるかは異なります。
|
||||
Ici,
|
||||
* `id` doit être réglé sur l'ID que vous avez défini lors de la connexion à ce canal comme décrit ci-dessus.Cela vous permettra d'identifier le canal auquel ce message est destiné.
|
||||
* `type` définit le type du message.Les différents canaux acceptent différents types de messages.
|
||||
* `body` est défini comme le contenu du message.Les différents canaux acceptent différents types de messages.
|
||||
|
||||
### チャンネルから切断する
|
||||
チャンネルから切断するには、次のようなデータをJSONでストリームに送信します:
|
||||
### Déconnexion d'un canal
|
||||
Pour se déconnecter d'un canal, envoyez les données suivantes au flux en JSON :
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -115,14 +115,14 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
|
|||
}
|
||||
```
|
||||
|
||||
ここで、
|
||||
* `id`には前述したそのチャンネルに接続する際に設定したIDを設定します。
|
||||
Ici,
|
||||
* `id` doit être réglé sur l'ID que vous avez défini lors de la connexion à ce canal comme décrit ci-dessus.
|
||||
|
||||
## ストリームを経由してAPIリクエストする
|
||||
## Faire une requête API via le flux
|
||||
|
||||
ストリームを経由してAPIリクエストすると、HTTPリクエストを発生させずにAPIを利用できます。そのため、コードを簡潔にできたり、パフォーマンスの向上を見込めるかもしれません。
|
||||
Si vous effectuez une requête d'API via un flux, vous pouvez utiliser l'API sans générer de requête HTTP.Cela peut rendre votre code plus concis et améliorer les performances.
|
||||
|
||||
ストリームを経由してAPIリクエストするには、次のようなデータをJSONでストリームに送信します:
|
||||
Pour effectuer une demande d'API via un flux, envoyez les données suivantes au flux en JSON :
|
||||
```json
|
||||
{
|
||||
type: 'api',
|
||||
|
@ -136,18 +136,18 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
|
|||
}
|
||||
```
|
||||
|
||||
ここで、
|
||||
* `id`には、APIのレスポンスを識別するための、APIリクエストごとの一意なIDを設定する必要があります。UUIDや、簡単な乱数のようなもので構いません。
|
||||
* `endpoint`には、あなたがリクエストしたいAPIのエンドポイントを指定します。
|
||||
* `data`には、エンドポイントのパラメータを含めます。
|
||||
Ici,
|
||||
* `id` doit être défini comme un identifiant unique pour chaque demande d'API afin d'identifier la réponse de l'API.Il peut s'agir de quelque chose comme un UUID ou un simple nombre aléatoire.
|
||||
* `endpoint` est le point de terminaison de l'API que vous voulez demander.
|
||||
* `data` contient les paramètres de la terminaison.
|
||||
|
||||
<div class="ui info">
|
||||
<p><i class="fas fa-info-circle"></i> APIのエンドポイントやパラメータについてはAPIリファレンスをご確認ください。</p>
|
||||
<p><i class="fas fa-info-circle"></i> Veuillez vous reporter à la référence de l'API pour les points de terminaison et les paramètres de l'API.</p>
|
||||
</div>
|
||||
|
||||
### レスポンスの受信
|
||||
### Réception des réponses
|
||||
|
||||
APIへリクエストすると、レスポンスがストリームから次のような形式で流れてきます。
|
||||
Lorsque vous faites une demande à l'API, la réponse viendra du flux dans le format suivant.
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -158,23 +158,23 @@ APIへリクエストすると、レスポンスがストリームから次の
|
|||
}
|
||||
```
|
||||
|
||||
ここで、
|
||||
* `xxxxxxxxxxxxxxxx`の部分には、リクエストの際に設定された`id`が含まれています。これにより、どのリクエストに対するレスポンスなのか判別することができます。
|
||||
* `body`には、レスポンスが含まれています。
|
||||
Ici,
|
||||
* La partie `xxxxxxxxxxxxxxxx` contient le `id` qui a été défini au moment de la demande.Cela vous permet de déterminer à quelle demande il répond.
|
||||
* `body` contient la réponse.
|
||||
|
||||
## 投稿のキャプチャ
|
||||
## Capture de message
|
||||
|
||||
Misskeyは投稿のキャプチャと呼ばれる仕組みを提供しています。これは、指定した投稿のイベントをストリームで受け取る機能です。
|
||||
Misskey propose un mécanisme appelé post-capture.Il s'agit de la possibilité de recevoir un flux d'événements pour un message donné.
|
||||
|
||||
例えばタイムラインを取得してユーザーに表示したとします。ここで誰かがそのタイムラインに含まれるどれかの投稿に対してリアクションしたとします。
|
||||
Par exemple, supposons une situation dans laquelle le fil est affichée pour un utilisateur.Supposons maintenant que quelqu'un réagisse à l'un des messages de ce fil.
|
||||
|
||||
しかし、クライアントからするとある投稿にリアクションが付いたことなどは知る由がないため、リアルタイムでリアクションをタイムライン上の投稿に反映して表示するといったことができません。
|
||||
Cependant, comme le client n'a aucun moyen de savoir qu'un message a reçu une réaction, il n'est pas possible de refléter la réaction en temps réel sur le message dans le fil.
|
||||
|
||||
この問題を解決するために、Misskeyは投稿のキャプチャ機構を用意しています。投稿をキャプチャすると、その投稿に関するイベントを受け取ることができるため、リアルタイムでリアクションを反映させたりすることが可能になります。
|
||||
Pour résoudre ce problème, Misskey fournit un mécanisme de post-capture.Lorsque vous capturez un message, vous recevez des événements liés à ce message, ce qui vous permet de refléter les réactions en temps réel.
|
||||
|
||||
### 投稿をキャプチャする
|
||||
### Capturer un message
|
||||
|
||||
投稿をキャプチャするには、ストリームに次のようなメッセージを送信します:
|
||||
Pour capturer un message, envoyez un message comme le suivant au flux :
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -185,12 +185,12 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
|
|||
}
|
||||
```
|
||||
|
||||
ここで、
|
||||
* `id`にキャプチャしたい投稿の`id`を設定します。
|
||||
Ici,
|
||||
* Définissez `id` comme l'`id` du message que vous voulez capturer.
|
||||
|
||||
このメッセージを送信すると、Misskeyにキャプチャを要請したことになり、以後、その投稿に関するイベントが流れてくるようになります。
|
||||
Lorsque vous envoyez ce message, vous demandez à Misskey de le saisir, et les événements liés à ce message se succéderont à partir de ce moment-là.
|
||||
|
||||
例えば投稿にリアクションが付いたとすると、次のようなメッセージが流れてきます:
|
||||
Par exemple, lorsqu'un message suscite une réaction, vous verrez apparaître un message du type suivant :
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -206,20 +206,20 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
|
|||
}
|
||||
```
|
||||
|
||||
ここで、
|
||||
* `body`内の`id`に、イベントを発生させた投稿のIDが設定されます。
|
||||
* `body`内の`type`に、イベントの種類が設定されます。
|
||||
* `body`内の`body`に、イベントの詳細が設定されます。
|
||||
Ici,
|
||||
* Le `id` dans le `body` est défini comme l'ID du post qui a déclenché l'événement.
|
||||
* Le type de l'événement est défini par `type` dans `body`.
|
||||
* L'attribut `body` dans `body` contient les informations sur l'événement.
|
||||
|
||||
#### イベントの種類
|
||||
#### Type d'événements
|
||||
|
||||
##### `reacted`
|
||||
その投稿にリアクションがされた時に発生します。
|
||||
Cela se produit lorsqu'une réaction est faite à ce message.
|
||||
|
||||
* `reaction`に、リアクションの種類が設定されます。
|
||||
* `userId`に、リアクションを行ったユーザーのIDが設定されます。
|
||||
* `reaction` est défini comme le type de réaction.
|
||||
* `userId` sera défini comme l'ID de l'utilisateur qui a fait la réaction.
|
||||
|
||||
例:
|
||||
Par exemple :
|
||||
```json
|
||||
{
|
||||
type: 'noteUpdated',
|
||||
|
@ -235,11 +235,11 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
|
|||
```
|
||||
|
||||
##### `deleted`
|
||||
その投稿が削除された時に発生します。
|
||||
Cela se produit lorsque ce message est supprimé.
|
||||
|
||||
* `deletedAt`に、削除日時が設定されます。
|
||||
* `deletedAt` est défini comme la date et l'heure de la suppression.
|
||||
|
||||
例:
|
||||
Par exemple :
|
||||
```json
|
||||
{
|
||||
type: 'noteUpdated',
|
||||
|
@ -254,12 +254,12 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
|
|||
```
|
||||
|
||||
##### `pollVoted`
|
||||
その投稿に添付されたアンケートに投票された時に発生します。
|
||||
Déclenché lors du vote sur un sondage dans ce message.
|
||||
|
||||
* `choice`に、選択肢IDが設定されます。
|
||||
* `userId`に、投票を行ったユーザーのIDが設定されます。
|
||||
* `choice` contient l'ID du choix sélectionné.
|
||||
* `userId` sera défini comme l'ID de l'utilisateur qui a voté.
|
||||
|
||||
例:
|
||||
Par exemple :
|
||||
```json
|
||||
{
|
||||
type: 'noteUpdated',
|
||||
|
@ -274,11 +274,11 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
|
|||
}
|
||||
```
|
||||
|
||||
### 投稿のキャプチャを解除する
|
||||
### Annuler la capture de publication
|
||||
|
||||
その投稿がもう画面に表示されなくなったりして、その投稿に関するイベントをもう受け取る必要がなくなったときは、キャプチャの解除を申請してください。
|
||||
Quand une publication n'est plus affichée et que vous n'avez plus besoin de recevoir les événements la concernant, vous pouvez demander l'annulation de la capture.
|
||||
|
||||
次のメッセージを送信します:
|
||||
Envoyez le message suivant :
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -289,66 +289,66 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
|
|||
}
|
||||
```
|
||||
|
||||
ここで、
|
||||
* `id`にキャプチャを解除したい投稿の`id`を設定します。
|
||||
Ici,
|
||||
* Définissez `id` comme le `id` du message que vous voulez annuler.
|
||||
|
||||
このメッセージを送信すると、以後、その投稿に関するイベントは流れてこないようになります。
|
||||
Une fois que vous aurez envoyé ce message, aucun autre événement lié au message ne sera diffusé.
|
||||
|
||||
# チャンネル一覧
|
||||
# Liste des canaux
|
||||
## `main`
|
||||
アカウントに関する基本的な情報が流れてきます。このチャンネルにパラメータはありません。
|
||||
Les informations de base relatives au compte seront transmises ici.Il n'y a pas de paramètres pour ce canal.
|
||||
|
||||
### 流れてくるイベント一覧
|
||||
### Liste des événements envoyés
|
||||
|
||||
#### `renote`
|
||||
自分の投稿がRenoteされた時に発生するイベントです。自分自身の投稿をRenoteしたときは発生しません。
|
||||
Cet événement est déclenché lorsque votre message est renoté.Cela ne se produit pas lorsque vous renotez votre propre message.
|
||||
|
||||
#### `mention`
|
||||
誰かからメンションされたときに発生するイベントです。
|
||||
Il s'agit d'un événement qui se produit lorsque quelqu'un fait vous mentionne.
|
||||
|
||||
#### `readAllNotifications`
|
||||
自分宛ての通知がすべて既読になったことを表すイベントです。このイベントを利用して、「通知があることを示すアイコン」のようなものをオフにしたりする等のケースが想定されます。
|
||||
Cet événement indique que toutes les notifications qui vous ont été adressées ont été lues.Cet événement peut être utilisé pour désactiver des choses comme "l'icône indiquant qu'il y a une notification" et d'autres cas.
|
||||
|
||||
#### `meUpdated`
|
||||
自分の情報が更新されたことを表すイベントです。
|
||||
Cet événement indique que vos informations ont été mises à jour.
|
||||
|
||||
#### `follow`
|
||||
自分が誰かをフォローしたときに発生するイベントです。
|
||||
Cet événement se produit lorsque vous suivez quelqu'un.
|
||||
|
||||
#### `unfollow`
|
||||
自分が誰かのフォローを解除したときに発生するイベントです。
|
||||
Cet événement se produit lorsque vous retirez quelqu'un de vos suivis.
|
||||
|
||||
#### `followed`
|
||||
自分が誰かにフォローされたときに発生するイベントです。
|
||||
Cet événement se produit lorsque vous êtes suivi par quelqu'un.
|
||||
|
||||
## `homeTimeline`
|
||||
ホームタイムラインの投稿情報が流れてきます。このチャンネルにパラメータはありません。
|
||||
Vous verrez ce flux d'informations s'afficher sur votre fil personnel.Il n'y a pas de paramètres pour ce canal.
|
||||
|
||||
### 流れてくるイベント一覧
|
||||
### Liste des événements envoyés
|
||||
|
||||
#### `note`
|
||||
タイムラインに新しい投稿が流れてきたときに発生するイベントです。
|
||||
Cet événement est déclenché lorsqu'un nouveau message arrive sur sur fil.
|
||||
|
||||
## `localTimeline`
|
||||
ローカルタイムラインの投稿情報が流れてきます。このチャンネルにパラメータはありません。
|
||||
Vous verrez l'information affichée sur votre fil local.Il n'y a pas de paramètres pour ce canal.
|
||||
|
||||
### 流れてくるイベント一覧
|
||||
### Liste des événements envoyés
|
||||
|
||||
#### `note`
|
||||
ローカルタイムラインに新しい投稿が流れてきたときに発生するイベントです。
|
||||
Cet événement est déclenché lorsqu'un nouveau message apparaît dans le fil local.
|
||||
|
||||
## `hybridTimeline`
|
||||
ソーシャルタイムラインの投稿情報が流れてきます。このチャンネルにパラメータはありません。
|
||||
Vous verrez l'information affichée sur le fil social.Il n'y a pas de paramètres pour ce canal.
|
||||
|
||||
### 流れてくるイベント一覧
|
||||
### Liste des événements envoyés
|
||||
|
||||
#### `note`
|
||||
ソーシャルタイムラインに新しい投稿が流れてきたときに発生するイベントです。
|
||||
Cet événement est déclenché lorsqu'un nouveau message apparaît sur votre fil social.
|
||||
|
||||
## `globalTimeline`
|
||||
グローバルタイムラインの投稿情報が流れてきます。このチャンネルにパラメータはありません。
|
||||
Vous verrez l'information s'afficher sur le fil global.Il n'y a pas de paramètres pour ce canal.
|
||||
|
||||
### 流れてくるイベント一覧
|
||||
### Liste des événements envoyés
|
||||
|
||||
#### `note`
|
||||
グローバルタイムラインに新しい投稿が流れてきたときに発生するイベントです。
|
||||
Cet événement est déclenché lorsqu'un nouveau message arrive sur le fil global.
|
||||
|
|
|
@ -43,7 +43,7 @@ Le code des thèmes est écrit sous forme d'objets JSON5. Les thèmes comprennen
|
|||
* `props` ... Définir un style de thème.Voir les explications ci-après.
|
||||
|
||||
### Définir un style de thème
|
||||
C'est dans `props` que vous définirez le style de thème. Les propriétés deviendront des variables CSS et les valeurs associées spécifieront le contenu de ces variables. Par ailleurs, les objets présents par défaut dans `props` sont hérités du thème de base. Ainsi, si le thème de `base` est clair `light` ce sera l'objet [_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5) ; et s'il est sombre `dark` ce sera l'objet [_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5). Cela signifie, par exemple, que s'il n'y pas de propriété `panel` définie dans les `props` du thème, alors ce sera la valeur `panel` du thème de base qui sera prise en compte.
|
||||
C'est dans `props` que vous définirez le style du thème. Les clés deviendront des noms de variables CSS dont le contenu sera spécifié par les valeurs associées. Par ailleurs, les objets présents par défaut dans `props` sont hérités du thème de base. Ainsi, si le thème de `base` est `clair`, ce sera le fichier [_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5) ; et s'il est `sombre`, le fichier [_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5). En bref, s'il n'y a, par exemple, pas de clé `panel` définie dans les `props` du thème, alors ce sera la valeur `panel` du thème de base qui sera prise en compte.
|
||||
|
||||
#### Syntaxe des valeurs
|
||||
* Codes de couleur Hex
|
||||
|
@ -52,11 +52,11 @@ C'est dans `props` que vous définirez le style de thème. Les propriétés devi
|
|||
* Ex. : `rgb(0, 255, 0)`
|
||||
* Couleurs avec les valeurs RVBA : `rgba(r, g, b, a)`
|
||||
* Ex. : `rgba(0, 255, 0, 0.5)`
|
||||
* Appeler les valeurs d'autres propriétés
|
||||
* Entrer `@{keyname}` pour utiliser la valeur de la propriété citée. Remplacer alors `{keyname}` par le nom de la propriété que vous souhaitez citer.
|
||||
* Appeler les valeurs d'autres clés
|
||||
* Entrer `@{keyname}` pour appeler la valeur d'une autre clé. Remplacer alors `{keyname}` par le nom de la clé que vous souhaitez appeler.
|
||||
* Ex. : `@panel`
|
||||
* Constantes (voir ci-dessous)
|
||||
* Entrer `${constantname}` pour utiliser la valeur de la constante citée.Remplacer alors `{constantname}` par la nom de la constante que vous souhaitez citer.
|
||||
* Entrer `${constantname}` vous permet d'appeler une constante. Remplacer alors `{constantname}` par le nom de la constante que vous souhaitez appeler.
|
||||
* Ex. : `$main`
|
||||
* Fonctions (voir ci-dessous)
|
||||
* `:{functionname}<{argument}<{color}`
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# Deck
|
||||
|
||||
Il deck è una delle interfacce utente disponibili.「カラム」と呼ばれるビューを複数並べて表示させることで、カスタマイズ性が高く、情報量の多いUIが構築できることが特徴です。
|
||||
Il deck è una delle interfacce utente disponibili.Ti consente di configurare varie colonne fianco a fianco, con molte possibilità di personalizzazione, per ottenere uno schermo più ricco in contenuti.
|
||||
|
||||
## Aggiungere colonne
|
||||
Puoi aggiungere una colonna facendo un clic destro nello sfondo del deck, poi scegliendo "Aggiungi colonna".
|
||||
Puoi aggiungere una colonna facendo un clic destro nello sfondo del deck, poi selezionando "Aggiungi colonna".
|
||||
|
||||
## Spostare colonne
|
||||
カラムは、ドラッグアンドドロップで他のカラムと位置を入れ替えることが出来るほか、カラムメニュー(カラムのヘッダー右クリック)から位置を移動させることもできます。
|
||||
|
|
|
@ -43,7 +43,7 @@ Il codice dei temi è scritto a forma di oggetti JSON5. I temi contengono gli og
|
|||
* `props` ... Imposta uno stile di tema. (Vedi spiegazioni sotto.)
|
||||
|
||||
### Impostare uno stile di tema
|
||||
`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。
|
||||
Puoi configurare lo stile del tema dentro le `props`. Le chiavi diventeranno nomi di variabili CSS, il cui contenuto verrà definito dai valori associati ad esse. Inoltre, gli oggetti presenti in `props` per impostazione predefinita vengono ereditati dal tema di base. Il tema di base sarà [_light.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_light.json5) per una `base` `chiara`, e [_dark.json5](https://github.com/misskey-dev/misskey/blob/develop/src/client/themes/_dark.json5) per una `base` `scura`. Cioè, se non viene definita una chiave `panel` nelle `props` del tema, si terrà conto del valore <0>panel</0> predefinito del tema usato.
|
||||
|
||||
#### Sintassi dei valori
|
||||
* Colori HEX
|
||||
|
@ -52,17 +52,17 @@ Il codice dei temi è scritto a forma di oggetti JSON5. I temi contengono gli og
|
|||
* Es.: `rgb(0, 255, 0)`
|
||||
* Colori `RGBA(r, g, b, a)`
|
||||
* Es.: `rgba(0, 255, 0, 0.5)`
|
||||
* 他のキーの値の参照
|
||||
* `@{キー名}`と書くと他のキーの値の参照になります。`{キー名}`は参照したいキーの名前に置き換えます。
|
||||
* Chiamare valori di altre chiavi
|
||||
* Inserisci `@{keyname}` per chiamare il valore di un'altra chiave. Bisogna sostituire il testo `{keyname}` col nome della chiave che vuoi chiamare.
|
||||
* Es.: `@panel`
|
||||
* Costanti (vedi sotto)
|
||||
* `${定数名}`と書くと定数の参照になります。`{定数名}`は参照したい定数の名前に置き換えます。
|
||||
* Inserisci `${constantname}` per chiamare una costante.Bisogna sostituire il testo `{constantname}` col nome della costante che vuoi chiamare.
|
||||
* Es.: `$main`
|
||||
* Funzioni (vedi sotto)
|
||||
* `:{functionname}<{argument}<{color}`
|
||||
|
||||
#### Costanti
|
||||
「CSS変数として出力はしたくないが、他のCSS変数の値として使いまわしたい」値があるときは、定数を使うと便利です。 キー名を`$`で始めると、そのキーはCSS変数として出力されません。
|
||||
Può essere vantaggioso usare una costante nei casi in cui non vuoi che un valore produca una variabile CSS, perché lo vuoi utilizzare come valore di un'altra variabile CSS. In tal caso, basta aggiungere `$` davanti al nome della chiave affinché non generi variabile CSS.
|
||||
|
||||
#### Funzioni
|
||||
wip
|
||||
|
|
53
src/models/entities/ad.ts
Normal file
53
src/models/entities/ad.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
|
||||
import { id } from '../id';
|
||||
|
||||
@Entity()
|
||||
export class Ad {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the Ad.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The expired date of the Ad.'
|
||||
})
|
||||
public expiresAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32, nullable: false
|
||||
})
|
||||
public place: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32, nullable: false
|
||||
})
|
||||
public priority: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, nullable: false
|
||||
})
|
||||
public url: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, nullable: false
|
||||
})
|
||||
public imageUrl: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 8192, nullable: false
|
||||
})
|
||||
public memo: string;
|
||||
|
||||
constructor(data: Partial<Ad>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
30
src/models/entities/password-reset-request.ts
Normal file
30
src/models/entities/password-reset-request.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { id } from '../id';
|
||||
import { User } from './user';
|
||||
|
||||
@Entity()
|
||||
export class PasswordResetRequest {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone')
|
||||
public createdAt: Date;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public token: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
}
|
|
@ -60,6 +60,8 @@ import { MutedNote } from './entities/muted-note';
|
|||
import { ChannelFollowing } from './entities/channel-following';
|
||||
import { ChannelNotePining } from './entities/channel-note-pining';
|
||||
import { RegistryItem } from './entities/registry-item';
|
||||
import { Ad } from './entities/ad';
|
||||
import { PasswordResetRequest } from './entities/password-reset-request';
|
||||
|
||||
export const Announcements = getRepository(Announcement);
|
||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||
|
@ -122,3 +124,5 @@ export const Channels = getCustomRepository(ChannelRepository);
|
|||
export const ChannelFollowings = getRepository(ChannelFollowing);
|
||||
export const ChannelNotePinings = getRepository(ChannelNotePining);
|
||||
export const RegistryItems = getRepository(RegistryItem);
|
||||
export const Ads = getRepository(Ad);
|
||||
export const PasswordResetRequests = getRepository(PasswordResetRequest);
|
||||
|
|
|
@ -200,8 +200,6 @@ export class NoteRepository extends Repository<Note> {
|
|||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||
uri: note.uri || undefined,
|
||||
url: note.url || undefined,
|
||||
_featuredId_: (note as any)._featuredId_ || undefined,
|
||||
_prId_: (note as any)._prId_ || undefined,
|
||||
|
||||
...(opts.detail ? {
|
||||
reply: note.replyId ? this.pack(note.reply || note.replyId, me, {
|
||||
|
@ -448,14 +446,7 @@ export const packedNoteSchema = {
|
|||
optional: false as const, nullable: true as const,
|
||||
description: 'The human readable url of a note. it will be null when the note is local.',
|
||||
},
|
||||
_featuredId_: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
},
|
||||
_prId_: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
},
|
||||
|
||||
myReaction: {
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
|
|
45
src/server/api/endpoints/admin/ad/create.ts
Normal file
45
src/server/api/endpoints/admin/ad/create.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { Ads } from '../../../../../models';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true as const,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
url: {
|
||||
validator: $.str.min(1)
|
||||
},
|
||||
memo: {
|
||||
validator: $.str
|
||||
},
|
||||
place: {
|
||||
validator: $.str
|
||||
},
|
||||
priority: {
|
||||
validator: $.str
|
||||
},
|
||||
expiresAt: {
|
||||
validator: $.num.int()
|
||||
},
|
||||
imageUrl: {
|
||||
validator: $.str.min(1)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
await Ads.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: new Date(ps.expiresAt),
|
||||
url: ps.url,
|
||||
imageUrl: ps.imageUrl,
|
||||
priority: ps.priority,
|
||||
place: ps.place,
|
||||
memo: ps.memo,
|
||||
});
|
||||
});
|
34
src/server/api/endpoints/admin/ad/delete.ts
Normal file
34
src/server/api/endpoints/admin/ad/delete.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import { Ads } from '../../../../../models';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true as const,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
id: {
|
||||
validator: $.type(ID)
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAd: {
|
||||
message: 'No such ad.',
|
||||
code: 'NO_SUCH_AD',
|
||||
id: 'ccac9863-3a03-416e-b899-8a64041118b1'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const ad = await Ads.findOne(ps.id);
|
||||
|
||||
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
|
||||
|
||||
await Ads.delete(ad.id);
|
||||
});
|
36
src/server/api/endpoints/admin/ad/list.ts
Normal file
36
src/server/api/endpoints/admin/ad/list.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { Ads } from '../../../../../models';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true as const,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId)
|
||||
.andWhere('ad.expiresAt > :now', { now: new Date() });
|
||||
|
||||
const ads = await query.take(ps.limit!).getMany();
|
||||
|
||||
return ads;
|
||||
});
|
59
src/server/api/endpoints/admin/ad/update.ts
Normal file
59
src/server/api/endpoints/admin/ad/update.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import { Ads } from '../../../../../models';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true as const,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
id: {
|
||||
validator: $.type(ID)
|
||||
},
|
||||
memo: {
|
||||
validator: $.str
|
||||
},
|
||||
url: {
|
||||
validator: $.str.min(1)
|
||||
},
|
||||
imageUrl: {
|
||||
validator: $.str.min(1)
|
||||
},
|
||||
place: {
|
||||
validator: $.str
|
||||
},
|
||||
priority: {
|
||||
validator: $.str
|
||||
},
|
||||
expiresAt: {
|
||||
validator: $.num.int()
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAd: {
|
||||
message: 'No such ad.',
|
||||
code: 'NO_SUCH_AD',
|
||||
id: 'b7aa1727-1354-47bc-a182-3a9c3973d300'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const ad = await Ads.findOne(ps.id);
|
||||
|
||||
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
|
||||
|
||||
await Ads.update(ad.id, {
|
||||
url: ps.url,
|
||||
place: ps.place,
|
||||
priority: ps.priority,
|
||||
memo: ps.memo,
|
||||
imageUrl: ps.imageUrl,
|
||||
expiresAt: new Date(ps.expiresAt),
|
||||
});
|
||||
});
|
|
@ -2,8 +2,9 @@ import $ from 'cafy';
|
|||
import config from '@/config';
|
||||
import define from '../define';
|
||||
import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { Emojis, Users } from '../../../models';
|
||||
import { Ads, Emojis, Users } from '../../../models';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
|
||||
import { MoreThan } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -193,6 +194,30 @@ export const meta = {
|
|||
}
|
||||
}
|
||||
},
|
||||
ads: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
place: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
url: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'url'
|
||||
},
|
||||
imageUrl: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'url'
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
requireSetup: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
|
@ -443,6 +468,12 @@ export default define(meta, async (ps, me) => {
|
|||
}
|
||||
});
|
||||
|
||||
const ads = await Ads.find({
|
||||
where: {
|
||||
expiresAt: MoreThan(new Date())
|
||||
},
|
||||
});
|
||||
|
||||
const response: any = {
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
|
@ -477,6 +508,12 @@ export default define(meta, async (ps, me) => {
|
|||
logoImageUrl: instance.logoImageUrl,
|
||||
maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
|
||||
emojis: await Emojis.packMany(emojis),
|
||||
ads: ads.map(ad => ({
|
||||
url: ad.url,
|
||||
place: ad.place,
|
||||
priority: ad.priority,
|
||||
imageUrl: ad.imageUrl,
|
||||
})),
|
||||
enableEmail: instance.enableEmail,
|
||||
|
||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||
|
|
73
src/server/api/endpoints/request-reset-password.ts
Normal file
73
src/server/api/endpoints/request-reset-password.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import $ from 'cafy';
|
||||
import { publishMainStream } from '../../../services/stream';
|
||||
import define from '../define';
|
||||
import rndstr from 'rndstr';
|
||||
import config from '@/config';
|
||||
import * as ms from 'ms';
|
||||
import { Users, UserProfiles, PasswordResetRequests } from '../../../models';
|
||||
import { sendEmail } from '../../../services/send-email';
|
||||
import { ApiError } from '../error';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false as const,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 3
|
||||
},
|
||||
|
||||
params: {
|
||||
username: {
|
||||
validator: $.str
|
||||
},
|
||||
|
||||
email: {
|
||||
validator: $.str
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
const user = await Users.findOne({
|
||||
usernameLower: ps.username.toLowerCase(),
|
||||
host: IsNull()
|
||||
});
|
||||
|
||||
// 合致するユーザーが登録されていなかったら無視
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await UserProfiles.findOneOrFail(user.id);
|
||||
|
||||
// 合致するメアドが登録されていなかったら無視
|
||||
if (profile.email !== ps.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
// メアドが認証されていなかったら無視
|
||||
if (!profile.emailVerified) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = rndstr('a-z0-9', 64);
|
||||
|
||||
await PasswordResetRequests.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: profile.userId,
|
||||
token
|
||||
});
|
||||
|
||||
const link = `${config.url}/reset-password/${token}`;
|
||||
|
||||
sendEmail(ps.email, 'Password reset requested',
|
||||
`To reset password, please click this link:<br><a href="${link}">${link}</a>`,
|
||||
`To reset password, please click this link: ${link}`);
|
||||
});
|
45
src/server/api/endpoints/reset-password.ts
Normal file
45
src/server/api/endpoints/reset-password.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import $ from 'cafy';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { publishMainStream } from '../../../services/stream';
|
||||
import define from '../define';
|
||||
import { Users, UserProfiles, PasswordResetRequests } from '../../../models';
|
||||
import { ApiError } from '../error';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
token: {
|
||||
validator: $.str
|
||||
},
|
||||
|
||||
password: {
|
||||
validator: $.str
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const req = await PasswordResetRequests.findOneOrFail({
|
||||
token: ps.token,
|
||||
});
|
||||
|
||||
// 発行してから30分以上経過していたら無効
|
||||
if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) {
|
||||
throw new Error(); // TODO
|
||||
}
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(ps.password, salt);
|
||||
|
||||
await UserProfiles.update(req.userId, {
|
||||
password: hash
|
||||
});
|
||||
|
||||
PasswordResetRequests.delete(req.id);
|
||||
});
|
|
@ -61,6 +61,11 @@ router.get('/.well-known/nodeinfo', async ctx => {
|
|||
ctx.body = { links };
|
||||
});
|
||||
|
||||
/* TODO
|
||||
router.get('/.well-known/change-password', async ctx => {
|
||||
});
|
||||
*/
|
||||
|
||||
router.get(webFingerPath, async ctx => {
|
||||
const fromId = (id: User['id']): Record<string, any> => ({
|
||||
id,
|
||||
|
|
Loading…
Reference in a new issue