From 9c0a9db195dd27410e7a5d52d884d60a1b62f233 Mon Sep 17 00:00:00 2001 From: leca Date: Mon, 12 Aug 2024 17:32:37 +0300 Subject: [PATCH] translations and remade location --- scheme.psql | 4 +- src/db.js | 109 ++++++++++++++++++++------------------- src/index.js | 118 ++++++++++++++++++++++++++----------------- src/interactions.js | 16 +++++- translations/en.json | 7 ++- translations/ru.json | 5 +- 6 files changed, 148 insertions(+), 111 deletions(-) diff --git a/scheme.psql b/scheme.psql index a41164e..58e0b3c 100644 --- a/scheme.psql +++ b/scheme.psql @@ -7,8 +7,7 @@ CREATE TABLE IF NOT EXISTS users ( interest CHAR, -- male, female or both ('m', 'f' and 'b') description VARCHAR(512) DEFAULT NULL, language VARCHAR(8) DEFAULT 'en', - country VARCHAR(64) DEFAULT NULL, - city VARCHAR(64) DEFAULT NULL, + location INTEGER DEFAULT NULL, -- link to ID, cities range DOUBLE PRECISION DEFAULT 20.0, current_action VARCHAR(16) DEFAULT NULL, currently_viewing VARCHAR(64) --link to "room_id" @@ -40,6 +39,7 @@ CREATE TABLE IF NOT EXISTS messages ( CREATE UNIQUE INDEX IF NOT EXISTS unique_messages ON messages(sender, recipient); CREATE TABLE IF NOT EXISTS cities ( + ID SERIAL, name VARCHAR(64), lat REAL, lng REAL, diff --git a/src/db.js b/src/db.js index df70cb7..ba0180a 100644 --- a/src/db.js +++ b/src/db.js @@ -48,6 +48,13 @@ const appendUserPictures = async (roomId, mxc, type) => { await db.query("INSERT INTO media (owner, type, purpose, url) VALUES ($1, $2, 'p', $3)", [roomId, type, mxc]); }; +const fullEraseUser = async (roomId) => { + await eraseUser(roomId) + await eraseUserLikes(roomId); + await eraseUserMedia(roomId); + await eraseUserMessages(roomId); +} + const eraseUser = async (roomId) => { await db.query("DELETE FROM users WHERE room_id = $1", [roomId]); }; @@ -60,16 +67,19 @@ const eraseUserMedia = async (roomId) => { await db.query("DELETE FROM media WHERE owner = $1", [roomId]); }; +const eraseUserMessages = async (roomid) => { + await db.query("DELETE FROM messages WHERE sender = $1 OR recipient = $1", [roomId]); +} + const selectProfilesForUser = async (roomId) => { - const { myrange, myinterest, mysex, myage, mycity, mycountry } = (await db.query(`SELECT range AS myrange, + const { myrange, myinterest, mysex, myage, mycity } = (await db.query(`SELECT range AS myrange, interest AS myinterest, sex AS mysex, age AS myage, - city AS mycity, - country AS mycountry + location AS mycity FROM users WHERE room_id = $1`, [roomId]) ).rows[0]; - const { lat, lng } = (await db.query("SELECT lat, lng FROM cities WHERE name = $1 AND country = $2", [mycity, mycountry])).rows[0]; + const { lat, lng } = (await db.query("SELECT lat, lng FROM cities WHERE ID = $1", [mycity])).rows[0]; //Selecting profiles other than user's and fitting their needs /* 2 * ASIN(SQRT( @@ -80,27 +90,24 @@ const selectProfilesForUser = async (roomId) => { )) * 6371 <= $5::double precision */ - // AND (range >= $5::double precision AND range != 0) + // let user = (await db.query(`SELECT - room_id, name, age, sex, description, country, city FROM users + room_id, name, age, sex, description, location, range FROM users WHERE age::numeric <@ ANY(ARRAY[numrange($1 - 2, $1 + 2)]) AND room_id != $2 AND ${myinterest !== 'b' ? "sex = $3" : "$3 = $3 AND $4 = $4 AND $5 = $5 AND $6 = $6 AND $7 = $7"} AND ${myrange !== 0 ? - `city = ANY(ARRAY(SELECT name - FROM cities - WHERE name = ANY(ARRAY(SELECT name - FROM cities - WHERE - check_distance($6::double precision, $7::double precision, lat, lng, $5::double precision) - - )) - ))` - : - `check_distance($6::double precision, $7::double precision, (SELECT lat FROM cities WHERE name = city AND cities.country = country), (SELECT lng FROM cities WHERE name = city AND cities.country = country), range) - OR range = 0` - } + `location = ANY(ARRAY(SELECT ID + FROM cities + WHERE + check_distance($6::double precision, $7::double precision, lat, lng, $5::double precision) + AND (check_distance($6::double precision, $7::double precision, lat, lng, range) OR range = 0) + ))` + : + `range = 0 OR + check_distance($6::double precision, $7::double precision, (SELECT lat FROM cities WHERE ID = location), (SELECT lng FROM cities WHERE ID = location), range)` + } AND (interest = $4 OR interest = 'b') ORDER BY RANDOM() LIMIT 1`, [myage, roomId, myinterest, mysex, myrange, lat, lng]) @@ -148,7 +155,7 @@ const checkForMutualLike = async (roomId1, roomId2) => { }; const getProfileInfo = async (roomId) => { - let user = (await db.query("SELECT mx_id, room_id, name, age, sex, description, country, city FROM users WHERE room_id = $1", [roomId])).rows[0]; + let user = (await db.query("SELECT mx_id, room_id, name, age, sex, description, location FROM users WHERE room_id = $1", [roomId])).rows[0]; if (!user) return null; let media = await getUserProfilePictures(user.room_id); user.media = media; @@ -192,45 +199,42 @@ const getUserLanguage = async (roomId) => { return (await db.query("SELECT language FROM users WHERE room_id = $1", [roomId])).rows[0].language; } -const checkCountry = async (name) => { - let res = (await db.query(`SELECT country AS name, levenshtein(country, $1) AS similarity - FROM cities - ORDER BY similarity ASC - LIMIT 3`, [name]) - ).rows; +// const checkCountry = async (name) => { +// let res = (await db.query(`SELECT country AS name, levenshtein(country, $1) AS similarity +// FROM cities +// ORDER BY similarity ASC +// LIMIT 3`, [name]) +// ).rows; - if (res[0].similarity == 0) { // 'similarity' is actually inversed. The less 'similarity', the more it similar. 0 means the same - return { - exists: true - }; - } - return { - exists: false, - variants: res - }; -} +// if (res[0].similarity == 0) { // 'similarity' is actually inversed. The less 'similarity', the more it similar. 0 means the same +// return { +// exists: true +// }; +// } +// return { +// exists: false, +// variants: res +// }; +// } -const checkCity = async (name) => { - let res = (await db.query(`SELECT name, country, levenshtein(name, $1) AS similarity +const findCity = async (name) => { + return (await db.query(`SELECT ID, name, lat, lng, country, levenshtein(name, $1) AS similarity FROM cities ORDER BY similarity ASC LIMIT 5`, [name]) ).rows; +} - if (res[0].similarity == 0) { - return { - exists: true, - country: res[0].country - } - } - return { - exists: false, - variants: res - } +const getCityNameByID = async (id) => { + return (await db.query(`SELECT name FROM cities WHERE ID = $1`, [id])).rows[0].name; +} + +const getCountryNameByID = async (id) => { + return (await db.query(`SELECT country FROM cities WHERE ID = $1`, [id])).rows[0].country; } export { - eraseUser, + fullEraseUser, appendUserPictures, setUserState, getCurrentUserAction, @@ -244,14 +248,13 @@ export { getProfileInfo, getAllLikesForUser, markLikeAsRead, - eraseUserLikes, - eraseUserMedia, uploadMediaAsMessage, insertMessageIntoDB, getUnreadMessages, markMessageAsRead, setUserLanguage, getUserLanguage, - checkCountry, - checkCity + findCity, + getCityNameByID, + getCountryNameByID }; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 4bf236e..4cc7d2b 100644 --- a/src/index.js +++ b/src/index.js @@ -20,9 +20,7 @@ import { appendUserLikes, appendUserPictures, checkForMutualLike, - eraseUser, - eraseUserLikes, - eraseUserMedia, + fullEraseUser, getAmountOfUserPictures, getCurrentUserAction, getUnreadMessages, @@ -33,8 +31,9 @@ import { uploadMediaAsMessage, setUserLanguage, getUserLanguage, - checkCountry, - checkCity + findCity, + getCityNameByID, + getCountryNameByID } from './db.js'; import { processRequest, showRandomProfileToUser, showNewLikes } from "./interactions.js"; @@ -81,42 +80,66 @@ client.on("room.message", async (roomId, event) => { if (a[1] !== "ru" && a[1] !== "en") return; await setUserLanguage(roomId, a[1]); i18n.locale = a[1] - await setUserState(roomId, "country"); - await client.sendText(roomId, i18n.t(["setup", "country"])); + await setUserState(roomId, "location"); + await client.sendText(roomId, i18n.t(["setup", "location"])); break; - case "country": - let checkResultCountry = await checkCountry(answer); - if (!checkResultCountry.exists) { - console.log(checkResultCountry.variants) - await client.sendText(roomId, i18n.t( - ["errors", "wrongcountry"], - { - first: checkResultCountry.variants[0].name, - second: checkResultCountry.variants[1].name, - third: checkResultCountry.variants[2].name - } - )); - return; + case "location": + let number = parseInt(answer) + if (!number) { + let cities = await findCity(answer); + await client.sendText(roomId, i18n.t(["setup", "choosecity"], { + number1: cities[0].id, + name1: cities[0].name, + country1: cities[0].country, + lat1: cities[0].lat, + lng1: cities[0].lng, + + number2: cities[1].id, + name2: cities[1].name, + country2: cities[1].country, + lat2: cities[1].lat, + lng2: cities[1].lng, + + number3: cities[2].id, + name3: cities[2].name, + country3: cities[2].country, + lat3: cities[2].lat, + lng3: cities[2].lng, + + number4: cities[3].id, + name4: cities[3].name, + country4: cities[3].country, + lat4: cities[3].lat, + lng4: cities[3].lng, + + number5: cities[4].id, + name5: cities[4].name, + country5: cities[4].country, + lat5: cities[4].lat, + lng5: cities[4].lng, + })) + } else { + // let cityName = await getCityNameByID(number); + // let countryName = await getCountryNameByID(number); + + await processRequest(client, roomId, current_action, number, 'range'); } - await processRequest(client, roomId, current_action, answer, 'city'); - break; - case "city": - let checkResultCity = await checkCity(answer); - if (!checkResultCity.exists) { - await client.sendText(roomId, i18n.t( - ["errors", "wrongcity"], - { - first: checkResultCity.variants[0].name, - second: checkResultCity.variants[1].name, - third: checkResultCity.variants[2].name, - fourth: checkResultCity.variants[3].name, - fifth: checkResultCity.variants[4].name, - } - )); - return; - } - await processRequest(client, roomId, current_action, answer, 'range'); + // let checkResultCity = await checkCity(answer); + // if (!checkResultCity.exists) { + // await client.sendText(roomId, i18n.t( + // ["errors", "wrongcity"], + // { + // first: checkResultCity.variants[0].name, + // second: checkResultCity.variants[1].name, + // third: checkResultCity.variants[2].name, + // fourth: checkResultCity.variants[3].name, + // fifth: checkResultCity.variants[4].name, + // } + // )); + // return; + // } + // await processRequest(client, roomId, current_action, answer, 'range'); break; case "range": answer = parseInt(answer.split(" ")[0]); @@ -140,7 +163,7 @@ client.on("room.message", async (roomId, event) => { await client.sendText(roomId, i18n.t(["errors", "tooyoung"])); await client.leaveRoom(roomId); - await eraseUser(roomId); + await fullEraseUser(roomId); return; } await processRequest(client, roomId, current_action, answer, 'sex'); @@ -204,7 +227,7 @@ client.on("room.message", async (roomId, event) => { await appendUserLikes(roomId, currently_viewing); let value = await checkForMutualLike(roomId, currently_viewing); if (value) { - await client.sendText(roomId, i18n.t(["general", "newlikes"])); + await client.sendText(currently_viewing, i18n.t(["general", "newlikes"])); await client.sendText(roomId, i18n.t(["general", "newlikes"])); @@ -238,7 +261,6 @@ client.on("room.message", async (roomId, event) => { } else { await client.sendText(roomId, i18n.t(["errors", "alreadymessaged"])); - } await setUserState(roomId, 'view_profiles'); await showRandomProfileToUser(client, roomId); @@ -306,12 +328,14 @@ client.on("room.message", async (roomId, event) => { }); client.on("room.event", async (roomId, event) => { - if (event.type === "m.room.member" && event.content?.membership === "leave") { - await eraseUser(roomId); - await eraseUserLikes(roomId); - await eraseUserMedia(roomId); - logInfo(`Bot has left a room with ID ${roomId}`) - client.leaveRoom(roomId); + try { + if (event.type === "m.room.member" && event.content?.membership === "leave") { + await fullEraseUser(roomId); + logInfo(`Bot has left a room with ID ${roomId}`) + client.leaveRoom(roomId); + } + } catch (e) { + logError(e); } }) diff --git a/src/interactions.js b/src/interactions.js index fd9f784..147f25e 100644 --- a/src/interactions.js +++ b/src/interactions.js @@ -13,7 +13,9 @@ import { getAllLikesForUser, checkForMutualLike, markLikeAsRead, - getUserLanguage + getUserLanguage, + getCountryNameByID, + getCityNameByID } from "./db.js"; import fs from 'fs'; @@ -58,11 +60,17 @@ const showRandomProfileToUser = async (client, roomId) => { i18n.locale = preferredLanguage; let chosenProfile = await selectProfilesForUser(roomId); + console.log(chosenProfile); + + if (!chosenProfile) { await client.sendText(roomId, i18n.t(["errors", "noprofiles"])); - return; } + + chosenProfile.country = await getCountryNameByID(chosenProfile.location); + chosenProfile.city = await getCityNameByID(chosenProfile.location); + let message = `${chosenProfile.country}, ${chosenProfile.city}. ${chosenProfile.name}, ${chosenProfile.sex == 'm' ? 'male' : 'female'}, ${chosenProfile.age}. @@ -96,6 +104,10 @@ const showProfileToUser = async (client, roomId, profileId) => { i18n.locale = preferredLanguage; let profileInfo = await getProfileInfo(profileId); + + profileInfo.country = await getCountryNameByID(profileInfo.location); + profileInfo.city = await getCityNameByID(profileInfo.location); + let message = `${profileInfo.country}, ${profileInfo.city}. ${profileInfo.name}, ${profileInfo.sex == 'm' ? 'male' : 'female'}, ${profileInfo.age}. diff --git a/translations/en.json b/translations/en.json index c9e46de..db7aab1 100644 --- a/translations/en.json +++ b/translations/en.json @@ -8,15 +8,15 @@ "mxid": "Go drop them a line! Their MXID is: %{mxid}", "message": "Write one message to this person or upload one picture/video to send them!", "newmessage": "You've got new message(s)! Go to a menu (🏠️) to see!", - "showmessage": " sent you this: ", + "showmessage": "%{user} sent you this:", "nonewmessages": "You have no new messages!", "messagesent": "You message was sent!", "msg": "Messages:", "setopt": "Got \"%{opt}\"" }, "setup": { - "country": "Write a name of country you are living in. Write them in English. For example, 'Russia', 'United States' or 'Ukraine'.", - "city": "Write a name of city you are living in. Write them in English. For example, 'Moscow', 'Washington', or 'Kyiv'.", + "location": "Write a name of city you are living in. Write them in English. For example, 'Moscow', 'Washington', or 'Kyiv'.", + "choosecity": "These are 5 most similar names of cities. Pick a city by writing its number. If you are unsatisfied with results, write a name of your city in other way.\n%{number1}) %{name1}, %{country1}. (%{lat1}, %{lng1})\n%{number2}) %{name2}, %{country2}. (%{lat2}, %{lng2})\n%{number3}) %{name3}, %{country3}. (%{lat3}, %{lng3})\n%{number4}) %{name4}, %{country4}. (%{lat4}, %{lng4})\n%{number5} %{name5}, %{country5}. (%{lat5}, %{lng5})", "range": "Write a radius (in kilometers) of a circle you'd like to find people in (Write 0 if you don't care or small values to find people in your city only)", "name": "Write a name of your profile. Not longer than 32 symbols", "description": "Write a description of your profile. Not longer than 512 symbols.", @@ -38,7 +38,6 @@ "notimplemented": "This feature is not yet implemented! Please, keep track of my updates at https://git.foxarmy.org/leca/heart2heart. Bother your admins to update, if released! ;)", "noprofiles": "There are no profiles left for you! I'm sorry. Please, come back later!", "alreadymessaged": "You have already messaged that person. Your message was not sent!", - "wrongcountry": "There is no such country. Perhaps, you meant %{first}, %{second} or %{third}?", "wrongcity": "There is no such city. Perhaps, you meant %{first}, %{second}, %{third}, %{fourth} or %{fifth}?" } } \ No newline at end of file diff --git a/translations/ru.json b/translations/ru.json index cff3c2c..540ba1e 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -15,8 +15,8 @@ "setopt": "Записал \"%{opt}\"" }, "setup": { - "country": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ имя страны, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΉ Π²Ρ‹ Π½Π°Ρ…ΠΎΠ΄ΠΈΡ‚Π΅ΡΡŒ. ΠŸΠΈΡΠ°Ρ‚ΡŒ Π½Π° английском языкС. НапримСр, 'Russia', 'United States' or 'Ukraine'.", - "city": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ имя Π³ΠΎΡ€ΠΎΠ΄Π°, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ Π²Ρ‹ Π½Π°Ρ…ΠΎΠ΄ΠΈΡ‚Π΅ΡΡŒ. ΠŸΠΈΡΠ°Ρ‚ΡŒ Π½Π° английском языкС. НапримСр, 'Moscow', 'Washington', or 'Kyiv'.", + "location": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Π³ΠΎΡ€ΠΎΠ΄Π°, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ Π²Ρ‹ ΠΆΠΈΠ²Ρ‘Ρ‚Π΅. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ английский язык. Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, 'Moscow', 'Washington', or 'Kyiv'.", + "choosecity": "Π’ΠΎΡ‚ 5 Π½Π°ΠΈΠ±ΠΎΠ»Π΅Π΅ ΠΏΠΎΡ…ΠΎΠΆΠΈΡ… ΠΏΠΎ названию Π³ΠΎΡ€ΠΎΠ΄ΠΎΠ². Π§Ρ‚ΠΎΠ±Ρ‹ Π²Ρ‹Π±Ρ€Π°Ρ‚ΡŒ Π³ΠΎΡ€ΠΎΠ΄, Π½Π°ΠΏΠΈΡˆΠΈΡ‚Π΅ Π΅Π³ΠΎ Π½ΠΎΠΌΠ΅Ρ€. Если Π²Ρ‹ Π½Π΅ ΡƒΠ΄ΠΎΠ²Π»Π΅Ρ‚Π²ΠΎΡ€Π΅Π½Ρ‹ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π°ΠΌΠΈ, Π½Π°ΠΏΠΈΡˆΠΈΡ‚Π΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Π³ΠΎΡ€ΠΎΠ΄Π° ΠΈΠ½Π°Ρ‡Π΅.\n%{number1}) %{name1}, %{country1}. (%{lat1}, %{lng1})\n%{number2}) %{name2}, %{country2}. (%{lat2}, %{lng2})\n%{number3}) %{name3}, %{country3}. (%{lat3}, %{lng3})\n%{number4}) %{name4}, %{country4}. (%{lat4}, %{lng4})\n%{number5} %{name5}, %{country5}. (%{lat5}, %{lng5})", "range": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ радиус (Π² ΠΊΠΈΠ»ΠΎΠΌΠ΅Ρ‚Ρ€Π²Ρ…), Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ Π²Ρ‹ Ρ…ΠΎΡ‚Π΅Π»ΠΈ Π±Ρ‹ Π½Π°ΠΉΡ‚ΠΈ людСй (ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ 0, Ссли Π²Π°ΠΌ Π½Π΅ Π²Π°ΠΆΠ΅Π½ радиус ΠΈΠ»ΠΈ малСнькиС значСния, Ссли Ρ…ΠΎΡ‚ΠΈΡ‚Π΅ Π½Π°ΠΉΡ‚ΠΈ людСй Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π² своём Π³ΠΎΡ€ΠΎΠ΄Π΅)", "name": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ имя вашСго профиля. НС большС 32 символов", "description": "ΠΠ°ΠΏΠΈΡˆΠΈΡ‚Π΅ описаниС вашСго профиля. НС большС 512 символов.", @@ -38,7 +38,6 @@ "notimplemented": "Π­Ρ‚Π° Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π΅Ρ‰Ρ‘ Π½Π΅ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Π°! ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, слСдитС Π·Π° обновлСниями Π½Π° ΠΌΠΎΡ‘ΠΌ git'Π΅: https://git.foxarmy.org/leca/heart2heart. ДоставайтС своих Π°Π΄ΠΌΠΈΠ½ΠΎΠ², Ρ‡Ρ‚ΠΎΠ±Ρ‹ обновлялись, ΠΊΠΎΠ³Π΄Π° новая вСрсия Π²Ρ‹Ρ…ΠΎΠ΄ΠΈΡ‚ ;)", "noprofiles": "НС ΠΌΠΎΠ³Ρƒ Π½Π°ΠΉΡ‚ΠΈ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ подходят вашим критСриям! Π˜Π·Π²ΠΈΠ½ΠΈΡ‚Π΅. ΠŸΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ ΠΏΠΎΠ·ΠΆΠ΅!", "alreadymessaged": "Π’Ρ‹ ΡƒΠΆΠ΅ отправляли сообщСниС этому Ρ‡Π΅Π»ΠΎΠ²Π΅ΠΊΡƒ. Π’Π°ΡˆΠ΅ сообщСниС Π½Π΅ Π±Ρ‹Π»ΠΎ ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ!", - "wrongcountry": "НС знаю Ρ‚Π°ΠΊΠΎΠΉ страны. Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, Π²Ρ‹ ΠΈΠΌΠ΅Π»ΠΈ Π² Π²ΠΈΠ΄Ρƒ %{first}, %{second} ΠΈΠ»ΠΈ %{third}?", "wrongcity": "НС знаю Ρ‚Π°ΠΊΠΎΠ³ΠΎ Π³ΠΎΡ€ΠΎΠ΄Π°. Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, Π²Ρ‹ ΠΈΠΌΠ΅Π»ΠΈ Π² Π²ΠΈΠ΄Ρƒ %{first}, %{second}, %{third}, %{fourth} ΠΈΠ»ΠΈ %{fifth}?" } } \ No newline at end of file