diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 848631823c13eae2590b05c425d18958376d9374..6926ed918f3b08cf880fcb7ae558cafc47cf8caf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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). diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 370c4a8db3aec6541340379a797d12bf07094c6f..631027cb7a94b953c85058efb796b7e4db408a09 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -430,6 +430,8 @@ inUse: "مستخدم" info: "عن" user: "المستخدمون" administration: "إدارة " +expiration: "ينتهي استطلاع الرأي ÙÙŠ" +middle: "متوسط" _email: _follow: title: "يتابعك" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 4ed33433781aa40bc52d1bd8a13b2c7e9c8240df..fe1f499eacbc00d54fa3e8690004be9d9e53d3bc 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -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" diff --git a/locales/en-US.yml b/locales/en-US.yml index 57ce0c2ac94c1ffe10f2c1393ea1ebc594457d1d..15bf985f3e22ca791f64c480b722a295bbffa727 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -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" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 9ad9d466612d9216de183fbb9f388a0703785946..fa645b3ecf2287ddb5ac34d2009676ab591fd22e 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -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: diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 35a97c14563520cd0ca31492046396ecca3a097d..da2932fae3b39144dc03198b0947b97a22ab14cf 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -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" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 1a73f40a34ab25927ac92faf924740114b54eb75..db236bad9efc3973d1839f757661d3c0fbb9036f 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -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" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 041bdfb11ddfcb7ede5441ab126411887ab15ce7..0f786a6b147d48b5255183bba91f95e7be9d9993 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -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: "自分ã®æŠ•ç¨¿" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 19ee30a4c9a554a7a986862e5abc26ada545e33e..d30a6cc8c85765911ac856cadecaa5a421a60ccb 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -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: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 024faa45a377ac7e3009411db971281293e88455..c8dba9b15c1cfb4b3b69e7df7ae4123226b57272 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -747,6 +747,8 @@ gallery: "갤러리" recentPosts: "최근 í¬ìŠ¤íŠ¸" popularPosts: "ì¸ê¸° í¬ìŠ¤íŠ¸" shareWithNote: "노트로 ê³µìœ " +expiration: "투표 기한" +middle: "보통" _gallery: my: "ë‚´ 갤러리" liked: "좋아요 í•œ 갤러리" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 3b4b53bce4e84845653e26d14ef38f0d3d7aa3d3..4a1a2d8686da7d9a6d0a252bcb99c24288df4baf 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -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: diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 40372658e0433fb7a97eec609365c558cf06f01e..6e906eb55a8a2fc03a7ef7a8370cd5096c329c4a 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -747,6 +747,8 @@ gallery: "ГалереÑ" recentPosts: "Ðедавние публикации" popularPosts: "ПопулÑрные публикации" shareWithNote: "ПоделитьÑÑ Ð·Ð°Ð¼ÐµÑ‚ÐºÐ¾Ð¹" +expiration: "ÐžÐ¿Ñ€Ð¾Ñ Ð´Ð»Ð¸Ñ‚ÑÑ" +middle: "Средне" _gallery: my: "ЛичнаÑ" liked: "ПонравившееÑÑ" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index c3967d74cbcba7702568b85780b5cbe56720b474..2dfd66733047b18cff926571b7beee104969c5c1 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -689,6 +689,8 @@ goBack: "Ðазад" info: "ІнформаціÑ" user: "КориÑтувачі" administration: "УправліннÑ" +expiration: "ÐžÐ¿Ð¸Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð°ÐºÑ–Ð½Ñ‡ÑƒÑ”Ñ‚ÑŒÑÑ" +middle: "Середній" _gallery: unlike: "Ðе вподобати" _email: diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index b53be8d11366ef16e6ccd7193301ccf1996ecf1f..4d1780c687f56c41f9c1b0ac7cbcf85a957fa93e 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -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: "çƒé—¨" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 62b48829b923843e1c080549a9c87ad282a42e89..23e47f25ac4c7affa5ffbf9f41365a9f4836e14d 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -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: "追隨請求已接å—" diff --git a/migration/1619942102890-password-reset.ts b/migration/1619942102890-password-reset.ts new file mode 100644 index 0000000000000000000000000000000000000000..66854cb0255b71924edad49ef75bcac35d5bf3dc --- /dev/null +++ b/migration/1619942102890-password-reset.ts @@ -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"`); + } + +} diff --git a/migration/1620019354680-ad.ts b/migration/1620019354680-ad.ts new file mode 100644 index 0000000000000000000000000000000000000000..27fb99f181d7450b7a0dcd636508171a4578f32b --- /dev/null +++ b/migration/1620019354680-ad.ts @@ -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"`); + } + +} diff --git a/package.json b/package.json index 25ebacaa7c4c5019b38ee9734387086f37ffc8fb..2a4c9d9013923f7cc26412f2c9d7f99900d0ecfe 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue index 2a861adb0910554bdf0743e357f5054f0471fd26..d458a0eeb8303a2384323edb8068e0a9702a1e73 100644 --- a/src/client/components/date-separated-list.vue +++ b/src/client/components/date-separated-list.vue @@ -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; + } } })); }, diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue index 9bec319af205cabc83a1992e9eb9f105f2a94de2..06653324d75b690d937e44dae53fea1d0965df9a 100644 --- a/src/client/components/emoji-picker.vue +++ b/src/client/components/emoji-picker.vue @@ -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, diff --git a/src/client/components/forgot-password.vue b/src/client/components/forgot-password.vue new file mode 100644 index 0000000000000000000000000000000000000000..1f530d7ca244e6b76c98931dd9131dce7f52247b --- /dev/null +++ b/src/client/components/forgot-password.vue @@ -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> diff --git a/src/client/components/global/ad.vue b/src/client/components/global/ad.vue new file mode 100644 index 0000000000000000000000000000000000000000..00592e4ca24e546379a2e959fa4e65bd5b265d44 --- /dev/null +++ b/src/client/components/global/ad.vue @@ -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> diff --git a/src/client/components/global/url.vue b/src/client/components/global/url.vue index e633a57bd85b657eecc1ef96c4dcd31e42d200b2..218729882da19153d5713b5e615d39d6aae002ac 100644 --- a/src/client/components/global/url.vue +++ b/src/client/components/global/url.vue @@ -113,8 +113,6 @@ export default defineComponent({ > .icon { padding-left: 2px; font-size: .9em; - font-weight: 400; - font-style: normal; } > .self { diff --git a/src/client/components/index.ts b/src/client/components/index.ts index 0630ed3d8c36138689998688c70dbdae1606afcc..8b914c5eec38536fee28a57f5092cc6b4bf264d6 100644 --- a/src/client/components/index.ts +++ b/src/client/components/index.ts @@ -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); } diff --git a/src/client/components/note-preview.vue b/src/client/components/note-preview.vue index fcae4c4368645302c76cc5a5dcae5b1191d62f4b..4248c2bb1d1f48e074c84864233057d8c5f5884e 100644 --- a/src/client/components/note-preview.vue +++ b/src/client/components/note-preview.vue @@ -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; diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 675748d540b058eaffe057076337f18ad94ed128..e90102921ac456a349fe32bf2005c8910ad7c71d 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -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> diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue index 2c883e0c32c0cf07c77984b57e6161ec3b6ad946..f8249ffcd64990a7b2c450d1bda828d1899c00f9 100755 --- a/src/client/components/signin.vue +++ b/src/client/components/signin.vue @@ -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'); } } }); diff --git a/src/client/pages/gallery/post.vue b/src/client/pages/gallery/post.vue index 703506a78d18f424848154d3543f50925936c1fb..50f81376ec301c471f5c02105f983644e014d2a2 100644 --- a/src/client/pages/gallery/post.vue +++ b/src/client/pages/gallery/post.vue @@ -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}"> diff --git a/src/client/pages/instance/ads.vue b/src/client/pages/instance/ads.vue new file mode 100644 index 0000000000000000000000000000000000000000..4297e56c37c2f827b2804b2318acebd7b2020047 --- /dev/null +++ b/src/client/pages/instance/ads.vue @@ -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> diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index 5972a02de0c3c7016dd2532a9ef04a7e495d76b8..974c4345bbf6b1f54cd59b90d15e7a193fd8cff4 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -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')); diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue index 6ee3ee8d2627ecf5d59e9577c246b2b72f53228c..4e237c21863f5e8ec44514b4b4ba541d600c92e5 100644 --- a/src/client/pages/page.vue +++ b/src/client/pages/page.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}"> diff --git a/src/client/pages/reset-password.vue b/src/client/pages/reset-password.vue new file mode 100644 index 0000000000000000000000000000000000000000..c33138213288aef3234a58572b4e12f8a9c59397 --- /dev/null +++ b/src/client/pages/reset-password.vue @@ -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> diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue index 049e912898e31e1cd2418e6ff431566aefe81d3d..3fd10fc44fa92985aeaefe0c74bdd81215289d5e 100644 --- a/src/client/pages/settings/index.vue +++ b/src/client/pages/settings/index.vue @@ -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(); }, diff --git a/src/client/pages/welcome.setup.vue b/src/client/pages/welcome.setup.vue index 2a71e2311ca0e6dc59ba5c594a477cff3023aa86..79464b814a107f0d16639d42c5f2dd4ab95c6202 100644 --- a/src/client/pages/welcome.setup.vue +++ b/src/client/pages/welcome.setup.vue @@ -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; diff --git a/src/client/router.ts b/src/client/router.ts index 8dcc1d1eb4ef5ab44cc2b65da8786e3d1236aff9..4c3aa765e69c0bf4c9249c3de07cd8660a330af1 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -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') }, diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts index 2e49f1a64cd5c1324c9db91e72af6f38bdd33a68..bcb0d7f2b04d0861c038778a74d9c52ffbab8b12 100644 --- a/src/client/scripts/paging.ts +++ b/src/client/scripts/paging.ts @@ -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(); diff --git a/src/client/style.scss b/src/client/style.scss index aa00303a15555031e09e9c53d629f7233e04dd7f..39bf6ef2d59481f40693bdba0d6d29ef1f7d9b15 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -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); diff --git a/src/client/ui/chat/date-separated-list.vue b/src/client/ui/chat/date-separated-list.vue index b073a38eb11ba8bc6581dc12d264932468fdc08a..bc7fc91d383df782201e2ffa679bdde7ec921ee4 100644 --- a/src/client/ui/chat/date-separated-list.vue +++ b/src/client/ui/chat/date-separated-list.vue @@ -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', diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue index bf55cc2b3f50ecdc8901ccb3740549b5f0f47fb1..c28436ed5cbb4c2b3b27ab2c24d801ac3ac3dd12 100644 --- a/src/client/ui/chat/index.vue +++ b/src/client/ui/chat/index.vue @@ -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([{ diff --git a/src/client/ui/deck/main-column.vue b/src/client/ui/deck/main-column.vue index 7b5b50fedc0d723b9685187e50a90d8bf336943c..0b61ff6e3ac83926c9991957bf10ebbac10b99d4 100644 --- a/src/client/ui/deck/main-column.vue +++ b/src/client/ui/deck/main-column.vue @@ -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([{ diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue index 64fdef2947c0bb139b2a669c71790c84d5c6b358..3c87bf7ab48033e8761f3e07ab99e276314e7f7f 100644 --- a/src/client/ui/default.vue +++ b/src/client/ui/default.vue @@ -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([{ diff --git a/src/client/ui/default.widgets.vue b/src/client/ui/default.widgets.vue index cabd83937e837d21bba783ee3e69a4b4a99c3bcd..0dd073409b8c44e9d3d9efdfb73e2d046529b8bd 100644 --- a/src/client/ui/default.widgets.vue +++ b/src/client/ui/default.widgets.vue @@ -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> diff --git a/src/client/ui/universal.vue b/src/client/ui/universal.vue index ad3c616b8e3d271fb95af22ea4bb9e57d673d788..fb67ea89855f2b195989c2dbd9f7e389b0eafbf0 100644 --- a/src/client/ui/universal.vue +++ b/src/client/ui/universal.vue @@ -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([{ diff --git a/src/db/postgre.ts b/src/db/postgre.ts index c8b0121719ea74abc0403466666e7d9e552f524a..3ad81203f20f7eab0458e5e2b2df4a21ceaf83d3 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -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 ]; diff --git a/src/docs/en-US/theme.md b/src/docs/en-US/theme.md index 35c0197966369fe009fa43129e279d235f5ec64a..674566b4e4574db8299771f82b58664fd6a3dc38 100644 --- a/src/docs/en-US/theme.md +++ b/src/docs/en-US/theme.md @@ -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 diff --git a/src/docs/es-ES/aiscript.md b/src/docs/es-ES/aiscript.md index 9dc4f1c9753cee466413aef773eda81b4ac8dfa2..3d936e906501f0aaadded496791249ca3f1d0e14 100644 --- a/src/docs/es-ES/aiscript.md +++ b/src/docs/es-ES/aiscript.md @@ -1,4 +1,4 @@ # AiScript ## funciones -デフォルトã§å€¤æ¸¡ã—ã§ã™ã€‚ +Pasando valores por defecto diff --git a/src/docs/fr-FR/create-plugin.md b/src/docs/fr-FR/create-plugin.md index c0d5fa9c57c7b148714e9dafe07f2d33a00e6ca9..0d6e8a8ad7f0f624c2d6e00708230e780ec271ba 100644 --- a/src/docs/fr-FR/create-plugin.md +++ b/src/docs/fr-FR/create-plugin.md @@ -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. diff --git a/src/docs/fr-FR/reversi-bot.md b/src/docs/fr-FR/reversi-bot.md index 2da95be1dc145284ab899df60f38ddf7ee4d369a..c75cb1784885f5f1994eaff27bf50d1d0d07089b 100644 --- a/src/docs/fr-FR/reversi-bot.md +++ b/src/docs/fr-FR/reversi-bot.md @@ -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>. diff --git a/src/docs/fr-FR/stream.md b/src/docs/fr-FR/stream.md index cd0a2f81f15b26210230ca25667823d42c939f32..6e8d7b78267b23c4ffad7984d7ca27eb3dbecffe 100644 --- a/src/docs/fr-FR/stream.md +++ b/src/docs/fr-FR/stream.md @@ -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. diff --git a/src/docs/fr-FR/theme.md b/src/docs/fr-FR/theme.md index 4d473e455a45e13e21566257660c4d5a738de5ad..1ea74ae8acf10f509e213f61a54b0e1ba6216526 100644 --- a/src/docs/fr-FR/theme.md +++ b/src/docs/fr-FR/theme.md @@ -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}` diff --git a/src/docs/it-IT/deck.md b/src/docs/it-IT/deck.md index 6037e4291a00638ae23d6c83706211210f4bea91..cea1c59dc9cdf57844a9a8c74561c4143b88dad9 100644 --- a/src/docs/it-IT/deck.md +++ b/src/docs/it-IT/deck.md @@ -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 カラムã¯ã€ãƒ‰ãƒ©ãƒƒã‚°ã‚¢ãƒ³ãƒ‰ãƒ‰ãƒãƒƒãƒ—ã§ä»–ã®ã‚«ãƒ©ãƒ ã¨ä½ç½®ã‚’入れ替ãˆã‚‹ã“ã¨ãŒå‡ºæ¥ã‚‹ã»ã‹ã€ã‚«ãƒ©ãƒ メニュー(カラムã®ãƒ˜ãƒƒãƒ€ãƒ¼å³ã‚¯ãƒªãƒƒã‚¯)ã‹ã‚‰ä½ç½®ã‚’移動ã•ã›ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚ diff --git a/src/docs/it-IT/theme.md b/src/docs/it-IT/theme.md index 506d979d9a9357bd62f119074ffc7b471ead1555..fd1fd6e33a2e5d2c9b58ef0f66a13059117af1c4 100644 --- a/src/docs/it-IT/theme.md +++ b/src/docs/it-IT/theme.md @@ -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 diff --git a/src/models/entities/ad.ts b/src/models/entities/ad.ts new file mode 100644 index 0000000000000000000000000000000000000000..3279de29eae2d4020ad85752dbf9692194ed5536 --- /dev/null +++ b/src/models/entities/ad.ts @@ -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; + } + } +} diff --git a/src/models/entities/password-reset-request.ts b/src/models/entities/password-reset-request.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d41d38a9372b1489442d3612fc12d30091f0865 --- /dev/null +++ b/src/models/entities/password-reset-request.ts @@ -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; +} diff --git a/src/models/index.ts b/src/models/index.ts index 9d08e4985867a823d469eee43bdf479fccf74ef1..9f8bd104e942a9814edb447c128c59fba9299f8b 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -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); diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index cdf4841918389d750057f4793253c3cd91a8d4af..7b1df730247098b4aa35a28207b0adeb92dea0a6 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -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, diff --git a/src/server/api/endpoints/admin/ad/create.ts b/src/server/api/endpoints/admin/ad/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..7777e95e6e3916a5fcb197a14ccd121c296abf6b --- /dev/null +++ b/src/server/api/endpoints/admin/ad/create.ts @@ -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, + }); +}); diff --git a/src/server/api/endpoints/admin/ad/delete.ts b/src/server/api/endpoints/admin/ad/delete.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a5f92193eb04a5f24bdc01ce63327b430e539d0 --- /dev/null +++ b/src/server/api/endpoints/admin/ad/delete.ts @@ -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); +}); diff --git a/src/server/api/endpoints/admin/ad/list.ts b/src/server/api/endpoints/admin/ad/list.ts new file mode 100644 index 0000000000000000000000000000000000000000..a323f2a9eddbfcffc5c7076a14eeb04e272010b4 --- /dev/null +++ b/src/server/api/endpoints/admin/ad/list.ts @@ -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; +}); diff --git a/src/server/api/endpoints/admin/ad/update.ts b/src/server/api/endpoints/admin/ad/update.ts new file mode 100644 index 0000000000000000000000000000000000000000..694af983947685497489f5d9043d92895d7c5542 --- /dev/null +++ b/src/server/api/endpoints/admin/ad/update.ts @@ -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), + }); +}); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 3760c8b37bf26c4d695bd44651d2bad7ffa26570..5b7292ef16511906697853203eb8181417e8dab0 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -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, diff --git a/src/server/api/endpoints/request-reset-password.ts b/src/server/api/endpoints/request-reset-password.ts new file mode 100644 index 0000000000000000000000000000000000000000..c880df75275cb5b69ee8489c6791b10b138a1c45 --- /dev/null +++ b/src/server/api/endpoints/request-reset-password.ts @@ -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}`); +}); diff --git a/src/server/api/endpoints/reset-password.ts b/src/server/api/endpoints/reset-password.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f79bdbd006a2dec43a3b3fad535251d76d0bf40 --- /dev/null +++ b/src/server/api/endpoints/reset-password.ts @@ -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); +}); diff --git a/src/server/well-known.ts b/src/server/well-known.ts index b1b6b2a771abed52ab43b0d0dcabf9ffbbae4bc4..57b6aba9a048bba679fa311448bcc6a8ff49ff20 100644 --- a/src/server/well-known.ts +++ b/src/server/well-known.ts @@ -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,