Commited backup

This commit is contained in:
2025-07-10 21:02:34 +03:00
parent 952c1001e3
commit da0b80823e
1310 changed files with 254133 additions and 41 deletions

145
N3rcy/modules/GameInfo.py Normal file
View File

@@ -0,0 +1,145 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
import requests
from hikkatl.tl.types import Message
from .. import loader, utils
@loader.tds
class GameInfo(loader.Module):
"""Module for fetching game information from RAWG"""
strings = {
"name": "GameInfo",
"game_not_found": "<b>❌ Game not found</b>",
"fetching": "<b>🌐 Fetching game information...</b>",
"no_api": "<b>❌ Please insert your api key in config</b> (<code>.cfg GameInfo</code>)",
"error_fetching": "<b>❌ Error fetching game information</b>",
"game": "<b><emoji document_id=5467583879948803288>🎮</emoji>Name: </b>%s",
"release": (
"<b><emoji document_id=5431897022456145283>📆</emoji>Data released: </b>%s"
),
"rawg_rating": (
"<b><emoji document_id=5435957248314579621>⭐️</emoji>Rating: </b>%s"
),
"platforms": (
"<b><emoji document_id=5386764531152198851>🏴‍☠️</emoji>Platforms: </b>%s"
),
"genres": "<b><emoji document_id=5188705588925702510>🎶</emoji>Genres: </b>%s",
"screenshots": (
"<b><emoji document_id=5818849313555483639>📸</emoji>Screenshots: </b>%s"
),
}
strings_ru = {
"game_not_found": "<b>❌ Игра не найдена</b>",
"fetching": "<b>🌐 Получение информации об игре...</b>",
"no_api": "<b>❌ Пожалуйста укажите api-ключ в конфиге (<code>.cfg GameInfo</code>)",
"error_fetching": "<b>❌ Ошибка при получении информации об игре</b>",
"game": "<b><emoji document_id=5467583879948803288>🎮</emoji>Название: </b>%s",
"release": (
"<b><emoji document_id=5431897022456145283>📆</emoji>Дата релиза: </b>%s"
),
"rawg_rating": (
"<b><emoji document_id=5435957248314579621>⭐️</emoji>Рейтинг: </b>%s"
),
"platforms": (
"<b><emoji document_id=5386764531152198851>🏴‍☠️</emoji>Платформы: </b>%s"
),
"genres": "<b><emoji document_id=5188705588925702510>🎶</emoji>Жанры: </b>%s",
"screenshots": (
"<b><emoji document_id=5818849313555483639>📸</emoji>Скриншоты: </b>%s"
),
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"api_key",
None,
lambda: "Your API token from https://rawg.io/apidocs (If you are from Russia use VPN)",
validator=loader.validators.Hidden(),
)
)
@loader.command(ru_doc="Получить информацию об игре <название игры>")
async def gameinfo(self, message: Message):
"""Fetch game information from RAWG"""
if self.config['api_key'] == None:
await utils.answer(message, self.strings('no_api'))
return
if not (game_name := utils.get_args_raw(message)):
await utils.answer(message, self.strings("game_not_found"))
return
await utils.answer(message, self.strings("fetching"))
try:
url = f"https://api.rawg.io/api/games?key={self.config['api_key']}&search={game_name}"
response = await utils.run_sync(requests.get, url)
if response.status_code == 404:
await utils.answer(message, self.strings("game_not_found"))
return
response.raise_for_status()
data = response.json()["results"][0]
game_name = data.get("name", "N/A")
released_date = data.get("released", "N/A")
rating = data.get("rating", "N/A")
platforms_str = (
", ".join(
platform["platform"]["name"]
for platform in data.get("platforms", [])
)
or "N/A"
)
genres_str = (
", ".join(genre["name"] for genre in data.get("genres", [])) or "N/A"
)
response = await utils.run_sync(
requests.get,
f"https://api.rawg.io/api/games/{data['id']}/screenshots?key={self.config['api_key']}",
)
screenshots = []
if response.status_code == 200:
screenshots_data = response.json()["results"][:3]
for screenshot in screenshots_data:
screenshots.append(screenshot["image"])
screenshots_str = ", ".join(screenshots) if screenshots else "N/A"
game_info_message = (
self.strings("game") % game_name
+ "\n"
+ self.strings("release") % released_date
+ "\n"
+ self.strings("rawg_rating") % rating
+ "\n"
+ self.strings("platforms") % platforms_str
+ "\n"
+ self.strings("genres") % genres_str
+ "\n"
+ self.strings("screenshots") % screenshots_str
)
await utils.answer(message, game_info_message)
except Exception:
await utils.answer(message, self.strings("error_fetching"))

350
N3rcy/modules/LICENSE.md Normal file
View File

@@ -0,0 +1,350 @@
Creative Commons Attribution-NonCommercial-NoDerivatives 4.0
International
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright and
certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are intended for use
by those authorized to give the public permission to use material in
ways otherwise restricted by copyright and certain other rights. Our
licenses are irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it. Licensors
should also secure all rights necessary before applying our licenses so
that the public can reuse the material as expected. Licensors should
clearly mark any material not subject to the license. This includes
other CC-licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors :
wiki.creativecommons.org/Considerations\_for\_licensors
Considerations for the public: By using one of our public licenses, a
licensor grants the public permission to use the licensed material under
specified terms and conditions. If the licensor's permission is not
necessary for any reasonfor example, because of any applicable
exception or limitation to copyrightthen that use is not regulated by
the license. Our licenses grant only permissions under copyright and
certain other rights that a licensor has authority to grant. Use of the
licensed material may still be restricted for other reasons, including
because others have copyright or other rights in the material. A
licensor may make special requests, such as asking that all changes be
marked or described. Although not required by our licenses, you are
encouraged to respect those requests where reasonable. More
considerations for the public :
wiki.creativecommons.org/Considerations\_for\_licensees
Creative Commons Attribution-NonCommercial-NoDerivatives 4.0
International Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-NoDerivatives 4.0 International Public License
("Public License"). To the extent this Public License may be interpreted
as a contract, You are granted the Licensed Rights in consideration of
Your acceptance of these terms and conditions, and the Licensor grants
You such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and conditions.
- Section 1 Definitions.
- a. Adapted Material means material subject to Copyright and
Similar Rights that is derived from or based upon the Licensed
Material and in which the Licensed Material is translated,
altered, arranged, transformed, or otherwise modified in a
manner requiring permission under the Copyright and Similar
Rights held by the Licensor. For purposes of this Public
License, where the Licensed Material is a musical work,
performance, or sound recording, Adapted Material is always
produced where the Licensed Material is synched in timed
relation with a moving image.
- b. Copyright and Similar Rights means copyright and/or similar
rights closely related to copyright including, without
limitation, performance, broadcast, sound recording, and Sui
Generis Database Rights, without regard to how the rights are
labeled or categorized. For purposes of this Public License, the
rights specified in Section 2(b)(1)-(2) are not Copyright and
Similar Rights.
- c. Effective Technological Measures means those measures that,
in the absence of proper authority, may not be circumvented
under laws fulfilling obligations under Article 11 of the WIPO
Copyright Treaty adopted on December 20, 1996, and/or similar
international agreements.
- d. Exceptions and Limitations means fair use, fair dealing,
and/or any other exception or limitation to Copyright and
Similar Rights that applies to Your use of the Licensed
Material.
- e. Licensed Material means the artistic or literary work,
database, or other material to which the Licensor applied this
Public License.
- f. Licensed Rights means the rights granted to You subject to
the terms and conditions of this Public License, which are
limited to all Copyright and Similar Rights that apply to Your
use of the Licensed Material and that the Licensor has authority
to license.
- g. Licensor means the individual(s) or entity(ies) granting
rights under this Public License.
- h. NonCommercial means not primarily intended for or directed
towards commercial advantage or monetary compensation. For
purposes of this Public License, the exchange of the Licensed
Material for other material subject to Copyright and Similar
Rights by digital file-sharing or similar means is NonCommercial
provided there is no payment of monetary compensation in
connection with the exchange.
- i. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance,
distribution, dissemination, communication, or importation, and
to make material available to the public including in ways that
members of the public may access the material from a place and
at a time individually chosen by them.
- j. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and
of the Council of 11 March 1996 on the legal protection of
databases, as amended and/or succeeded, as well as other
essentially equivalent rights anywhere in the world.
- k. You means the individual or entity exercising the Licensed
Rights under this Public License. Your has a corresponding
meaning.
- Section 2 Scope.
- a. License grant.
- 1. Subject to the terms and conditions of this Public
License, the Licensor hereby grants You a worldwide,
royalty-free, non-sublicensable, non-exclusive, irrevocable
license to exercise the Licensed Rights in the Licensed
Material to:
- A. reproduce and Share the Licensed Material, in whole
or in part, for NonCommercial purposes only; and
- B. produce and reproduce, but not Share, Adapted
Material for NonCommercial purposes only.
- 2. Exceptions and Limitations. For the avoidance of doubt,
where Exceptions and Limitations apply to Your use, this
Public License does not apply, and You do not need to comply
with its terms and conditions.
- 3. Term. The term of this Public License is specified in
Section 6(a).
- 4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter
created, and to make technical modifications necessary to do
so. The Licensor waives and/or agrees not to assert any
right or authority to forbid You from making technical
modifications necessary to exercise the Licensed Rights,
including technical modifications necessary to circumvent
Effective Technological Measures. For purposes of this
Public License, simply making modifications authorized by
this Section 2(a)(4) never produces Adapted Material.
- 5. Downstream recipients.
- A. Offer from the Licensor Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
- B. No downstream restrictions. You may not offer or
impose any additional or different terms or conditions
on, or apply any Effective Technological Measures to,
the Licensed Material if doing so restricts exercise of
the Licensed Rights by any recipient of the Licensed
Material.
- 6. No endorsement. Nothing in this Public License
constitutes or may be construed as permission to assert or
imply that You are, or that Your use of the Licensed
Material is, connected with, or sponsored, endorsed, or
granted official status by, the Licensor or others
designated to receive attribution as provided in Section
3(a)(1)(A)(i).
- b. Other rights.
- 1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however,
to the extent possible, the Licensor waives and/or agrees
not to assert any such rights held by the Licensor to the
limited extent necessary to allow You to exercise the
Licensed Rights, but not otherwise.
- 2. Patent and trademark rights are not licensed under this
Public License.
- 3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
- Section 3 License Conditions.
Your exercise of the Licensed Rights is expressly made subject to
the following conditions.
- a. Attribution.
- 1. If You Share the Licensed Material, You must:
- A. retain the following if it is supplied by the
Licensor with the Licensed Material:
- i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if designated);
- ii. a copyright notice;
- iii. a notice that refers to this Public License;
- iv. a notice that refers to the disclaimer of
warranties;
- v. a URI or hyperlink to the Licensed Material to
the extent reasonably practicable;
- B. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
- C. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
For the avoidance of doubt, You do not have permission under
this Public License to Share Adapted Material.
- 2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may
be reasonable to satisfy the conditions by providing a URI
or hyperlink to a resource that includes the required
information.
- 3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
- Section 4 Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
- a. for the avoidance of doubt, Section 2(a)(1) grants You the
right to extract, reuse, reproduce, and Share all or a
substantial portion of the contents of the database for
NonCommercial purposes only and provided You do not Share
Adapted Material;
- b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material;
and
- c. You must comply with the conditions in Section 3(a) if You
Share all or a substantial portion of the contents of the
database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the
Licensed Rights include other Copyright and Similar Rights.
- Section 5 Disclaimer of Warranties and Limitation of Liability.
- a. Unless otherwise separately undertaken by the Licensor, to
the extent possible, the Licensor offers the Licensed Material
as-is and as-available, and makes no representations or
warranties of any kind concerning the Licensed Material, whether
express, implied, statutory, or other. This includes, without
limitation, warranties of title, merchantability, fitness for a
particular purpose, non-infringement, absence of latent or other
defects, accuracy, or the presence or absence of errors, whether
or not known or discoverable. Where disclaimers of warranties
are not allowed in full or in part, this disclaimer may not
apply to You.
- b. To the extent possible, in no event will the Licensor be
liable to You on any legal theory (including, without
limitation, negligence) or otherwise for any direct, special,
indirect, incidental, consequential, punitive, exemplary, or
other losses, costs, expenses, or damages arising out of this
Public License or use of the Licensed Material, even if the
Licensor has been advised of the possibility of such losses,
costs, expenses, or damages. Where a limitation of liability is
not allowed in full or in part, this limitation may not apply to
You.
- c. The disclaimer of warranties and limitation of liability
provided above shall be interpreted in a manner that, to the
extent possible, most closely approximates an absolute
disclaimer and waiver of all liability.
- Section 6 Term and Termination.
- a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply
with this Public License, then Your rights under this Public
License terminate automatically.
- b. Where Your right to use the Licensed Material has terminated
under Section 6(a), it reinstates:
- 1. automatically as of the date the violation is cured,
provided it is cured within 30 days of Your discovery of the
violation; or
- 2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect
any right the Licensor may have to seek remedies for Your
violations of this Public License.
- c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing
so will not terminate this Public License.
- d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
- Section 7 Other Terms and Conditions.
- a. The Licensor shall not be bound by any additional or
different terms or conditions communicated by You unless
expressly agreed.
- b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
- Section 8 Interpretation.
- a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could
lawfully be made without permission under this Public License.
- b. To the extent possible, if any provision of this Public
License is deemed unenforceable, it shall be automatically
reformed to the minimum extent necessary to make it enforceable.
If the provision cannot be reformed, it shall be severed from
this Public License without affecting the enforceability of the
remaining terms and conditions.
- c. No term or condition of this Public License will be waived
and no failure to comply consented to unless expressly agreed to
by the Licensor.
- d. Nothing in this Public License constitutes or may be
interpreted as a limitation upon, or waiver of, any privileges
and immunities that apply to the Licensor or You, including from
the legal processes of any jurisdiction or authority.
Creative Commons is not a party to its public licenses. Notwithstanding,
Creative Commons may elect to apply one of its public licenses to
material it publishes and in those instances will be considered the
"Licensor." The text of the Creative Commons public licenses is
dedicated to the public domain under the CC0 Public Domain Dedication.
Except for the limited purpose of indicating that material is shared
under a Creative Commons public license or as otherwise permitted by the
Creative Commons policies published at creativecommons.org/policies,
Creative Commons does not authorize the use of the trademark "Creative
Commons" or any other trademark or logo of Creative Commons without its
prior written consent including, without limitation, in connection with
any unauthorized modifications to any of its public licenses or any
other arrangements, understandings, or agreements concerning use of
licensed material. For the avoidance of doubt, this paragraph does not
form part of the public licenses.
Creative Commons may be contacted at creativecommons.org.

22
N3rcy/modules/README.md Normal file
View File

@@ -0,0 +1,22 @@
## 🌘Hikka Userbot Modules
This repository contains a collection of modules for the Hikka Userbot. To install a module, simply copy the raw file link.
### 👤About Me
You can contact me on Telegram. Feel free to reach out with any questions or inquiries.
### 💾How install
You can install my modules using raw links. For example:
```
.dlm https://raw.githubusercontent.com/N3rcy/modules/main/{module name}
```
And you can use my repository, just add my repo:
```
.addrepo https://raw.githubusercontent.com/N3rcy/modules/main
```
After this you will can install mod simple:
```
.dlm {module name}
```

118
N3rcy/modules/clown.py Normal file
View File

@@ -0,0 +1,118 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
# scope: ffmpeg
import os
import shlex
import subprocess
import tempfile
import requests
from hikkatl.tl.types import Message
from .. import loader, utils
@loader.tds
class ClownModule(loader.Module):
"""Модуль для клоунизации 'pov - <username>'"""
strings = {
"name": "ClownMod",
"video_not_found": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Error inside module."
" (video_not_found)</b>"
),
"processing": (
"<b><emoji document_id=5334643333488713810>🌐</emoji>Processing"
" video...</b>"
),
"sending": (
"<b><emoji document_id=5371074117971745503>🤡</emoji>Sending video...</b>"
),
"error_downloading": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Error inside module."
" (error_downloading)</b>"
),
"error_sending": (
"<b><emoji document_id=5980953710157632545>❌</emoji>There was an error"
" uploading the video</b>"
),
}
strings_ru = {
"video_not_found": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Ошибка внутри"
" модуля.</b>"
),
"processing": (
"<b><emoji document_id=5334643333488713810>🌐</emoji>Обработка видео...</b>"
),
"sending": (
"<b><emoji document_id=5371074117971745503>🤡</emoji>Отправка видео...</b>"
),
"error_downloading": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Ошибка при загрузке"
" видео.</b>"
),
"error_sending": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Ошибка при отправке"
" видео.</b>"
),
}
@loader.command(ru_doc="Сделать клавном <ник> или реплай")
async def clown(self, message: Message):
"""Добавляет текст поверх видео"""
if not (
username := "pov - "
+ (
await self._get_username(reply_message.sender_id)
if (reply_message := await message.get_reply_message())
else utils.get_args_raw(message)
)
):
await utils.answer(message, self.strings("video_not_found"))
return
video_url = "https://github.com/N3rcy/modules/raw/refs/heads/main/clown_asset.mp4"
with tempfile.TemporaryDirectory() as temp_dir:
video_path = os.path.join(temp_dir, "clown_video.mp4")
output_path = os.path.join(temp_dir, "clown_output.mp4")
await utils.answer(message, self.strings("processing"))
try:
response = await utils.run_sync(requests.get, video_url)
response.raise_for_status()
with open(video_path, "wb") as f:
f.write(response.content)
except Exception:
await utils.answer(message, self.strings("error_downloading"))
return
command = f"ffmpeg -i {video_path} -vf \"drawtext=text='{username}':fontsize=50:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/4-150\" -c:a copy {output_path}"
os.system(command)
await utils.answer(message, self.strings("sending"))
try:
await utils.answer_file(
message,
output_path,
video_note=True,
)
except Exception:
await utils.answer(message, self.strings("error_sending"))
async def _get_username(self, user_id: int) -> str:
user = await self.client.get_entity(user_id, exp=0)
return user.username or user.first_name

Binary file not shown.

65
N3rcy/modules/emoji.py Normal file
View File

@@ -0,0 +1,65 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
# requires: bs4
import requests
from bs4 import BeautifulSoup
from hikkatl.tl.types import Message
from .. import loader, utils
@loader.tds
class EmojiInfo(loader.Module):
"""Module for retrieving information about emojis from emojipedia.org"""
strings = {
"name": "EmojiInfo",
"emoji_not_found": "Emoji not found.",
"error": "Error occurred while retrieving emoji information. Error: ",
}
strings_ru = {
"emoji_not_found": "Эмодзи не найдено.",
"error": "Произошла ошибка при получении информации об эмодзи. Ошибка: ",
}
@loader.command(ru_doc="Получить информацию об эмодзи")
async def emoji(self, message: Message):
"""Retrieve information about an emoji"""
if not (emoji := utils.get_args_raw(message)):
await utils.answer(message, self.strings["emoji_not_found"])
return
try:
url = f"https://emojipedia.org/{emoji}/"
response = await utils.run_sync(requests.get, url)
response.raise_for_status()
soup = BeautifulSoup(response.content, "html.parser")
emoji_name = soup.find('title').text
emoji_description = soup.find('div', {'class': 'HtmlContent_html-content-container__Ow2Bk'}).text
emoji_codepoints = ' '.join(['U+{:X}'.format(ord(char)) for char in emoji])
await utils.answer(
message,
(
f"<b>Name: {utils.escape_html(emoji_name)}</b>\n<b>Description:</b>"
f" {utils.escape_html(emoji_description)}\n<b>Codepoints:</b>"
f" {utils.escape_html(emoji_codepoints)}"
),
)
except Exception as e:
await utils.answer(
message, self.strings("error") + utils.escape_html(str(e))
)

12
N3rcy/modules/full.txt Normal file
View File

@@ -0,0 +1,12 @@
watch
twitch
clown
GameInfo
news
progmusic
whisper
github
jikan
ocr
emoji
top

136
N3rcy/modules/github.py Normal file
View File

@@ -0,0 +1,136 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
import requests
from hikkatl.tl.types import Message
from .. import loader, utils
@loader.tds
class GitHubMod(loader.Module):
"""Module for fetching GitHub profile or repository information"""
strings = {
"name": "GitHubMod",
"profile_info": "<b>GitHub Profile Info: </b>",
"repo_info": "<b>GitHub Repository Info: </b>",
"invalid_link": (
"<b><emoji document_id=5978859389614821335>❌</emoji>Invalid GitHub link."
" The correct link should start with https://github.com...</b>"
),
"user_not_found": (
"<b><emoji document_id=5978859389614821335>❌</emoji>User not found.</b>"
),
"repo_not_found": (
"<b><emoji document_id=5978859389614821335>❌</emoji>Repository not"
" found.</b>"
),
}
@loader.command(en_doc="<profile / url> - Fetch information about GitHub profile")
async def gitprof(self, message: Message):
"""<profile / url> - Fetch information about GitHub profile"""
if not (link := utils.get_args_raw(message)):
await utils.answer(message, self.strings["invalid_link"])
return
if link.startswith("https://github.com/"):
username = link.split("/")[3]
try:
response = await utils.run_sync(
requests.get, f"https://api.github.com/users/{username}"
)
response.raise_for_status()
user_data = response.json()
info_text = (
f"{self.strings['profile_info']}\n\n<b><emoji"
" document_id=5224371968014299199>📦</emoji>Link:</b>"
f" {link}\n<b><emoji"
" document_id=5222465715499446573>🌐</emoji>Username:</b>"
f" {user_data.get('login', 'N/A')}\n<b><emoji"
" document_id=5222465715499446573>🌐</emoji>Name:</b>"
f" {user_data.get('name', 'N/A')}\n<b><emoji"
" document_id=5222030772751314651>🖌</emoji>Bio:</b>"
f" {user_data.get('bio', 'N/A')}\n<b><emoji"
" document_id=5221924764368515209>🪧</emoji>Location:</b>"
f" {user_data.get('location', 'N/A')}\n<b><emoji"
" document_id=5222473609649337576>🔥</emoji>Followers:</b>"
f" {user_data.get('followers', 'N/A')}\n<b><emoji"
" document_id=5221962650275034448>❤️</emoji>Following:</b>"
f" {user_data.get('following', 'N/A')}\n<b><emoji"
" document_id=5222341131383091841>📗</emoji>Public Repositories:</b>"
f" {user_data.get('public_repos', 'N/A')}\n"
)
if avatar_url := user_data.get("avatar_url"):
await utils.answer_file(
message,
avatar_url,
info_text,
link_preview=False,
)
else:
await utils.answer(message, info_text)
except Exception:
await utils.answer(message, self.strings["user_not_found"])
@loader.command(ru_doc="Fetch information about GitHub repository")
async def gitrepo(self, message: Message):
"""Fetch information about GitHub repository"""
if not (link := utils.get_args_raw(message)):
await utils.answer(message, self.strings["invalid_link"])
return
if link.startswith("https://github.com/"):
parts = link.split("/")
if len(parts) >= 5:
username = parts[3]
repo_name = parts[4]
elif len(link.split("/")) == 2:
username, repo_name = link.split("/")
try:
response = await utils.run_sync(
requests.get, f"https://api.github.com/repos/{username}/{repo_name}"
)
response.raise_for_status()
repo_data = response.json()
info_text = (
f"{self.strings['repo_info']}\n\n<b><emoji"
" document_id=5224371968014299199>📦</emoji>Link:</b>"
f" {link}\n<b><emoji"
" document_id=5222341131383091841>📗</emoji>Repository:</b>"
f" {repo_data.get('name', 'N/A')}\n<b><emoji"
" document_id=5222030772751314651>🖌</emoji>Description:</b>"
f" {repo_data.get('description', 'N/A')}\n<b><emoji"
" document_id=5222465715499446573>🌐</emoji>Language:</b>"
f" {repo_data.get('language', 'N/A')}\n<b><emoji"
" document_id=5222473609649337576>🔥</emoji>Stars:</b>"
f" {repo_data.get('stargazers_count', 'N/A')}\n<b><emoji"
" document_id=5222331261548246439>↕️</emoji>Forks:</b>"
f" {repo_data.get('forks_count', 'N/A')}\n<b><emoji"
" document_id=5334704798765686555>👀</emoji>Watchers:</b>"
f" {repo_data.get('watchers_count', 'N/A')}\n"
)
if avatar_url := repo_data.get("avatar_url"):
await utils.answer_file(
message,
avatar_url,
info_text,
link_preview=False,
)
else:
await utils.answer(message, info_text)
except Exception:
await utils.answer(message, self.strings["repo_not_found"])

454
N3rcy/modules/jikan.py Normal file
View File

@@ -0,0 +1,454 @@
__version__ = (1, 1, 0)
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
import requests
from deep_translator import GoogleTranslator
from hikkatl.tl.types import Message
from .. import loader, utils
languages = ["ru", "en", "ja"]
@loader.tds
class JikanModule(loader.Module):
"""Module for working with Jikan API"""
strings = {
"name": "JikanModule",
"anime_not_found": "<b>No anime found.</b>",
"character_not_found": "<b>No character found.</b>",
"manga_not_found": "<b>No manga found.</b>",
"expression_missing": "<b>Please specify a search query.</b>",
"result": "<b>Result:</b>\n{result}",
"error": "<b>Error:</b> {error}",
}
strings_ru = {
"anime_not_found": "<b>Аниме не найдено.</b>",
"character_not_found": "<b>Персонаж не найден.</b>",
"manga_not_found": "<b>Манга не найдена.</b>",
"expression_missing": "<b>Пожалуйста, укажите поисковой запрос.</b>",
"result": "<b>Результат:</b>\n{result}",
"error": "<b>Ошибка:</b> {error}",
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"language",
"en",
lambda: "Language of output",
validator=loader.validators.Choice(languages),
),
)
@loader.command(
ru_doc="Поиск аниме по названию",
en_doc="Search for anime by title",
)
async def sanime(self, message: Message):
"""Search for anime by title"""
query = utils.get_args_raw(message)
if not query:
await utils.answer(message, self.strings["expression_missing"])
return
translator = GoogleTranslator(source="auto", target="en")
tquery = translator.translate(query)
response = await utils.run_sync(
requests.get,
"https://api.jikan.moe/v4/anime",
params={"q": tquery},
)
data = response.json()
if "data" not in data or not data["data"]:
await utils.answer(message, self.strings["anime_not_found"])
return
anime = data["data"][0]
title = anime.get("title")
title_english = anime.get("title_english")
title_japanese = anime.get("title_japanese")
type_ = anime.get("type")
episodes = anime.get("episodes")
status = anime.get("status")
start_date = anime.get("aired").get("from") if "aired" in anime else None
end_date = anime.get("aired").get("to") if "aired" in anime else None
duration = anime.get("duration")
rating = anime.get("rating")
score = anime.get("score")
synopsis = anime.get("synopsis")
sfw = anime.get("sfw") is None
result = f"<b>Title:</b> {title}\n"
if title_english:
result += f"<b>English Title:</b> {title_english}\n"
if title_japanese:
result += f"<b>Japanese Title:</b> {title_japanese}\n"
if type_:
result += f"<b>Type:</b> {type_}\n"
if episodes:
result += f"<b>Episodes:</b> {episodes}\n"
if status:
result += f"<b>Status:</b> {status}\n"
if start_date:
start_date = start_date.split("T")[0]
result += f"<b>Start Date:</b> {start_date}\n"
if end_date:
end_date = end_date.split("T")[0]
result += f"<b>End Date:</b> {end_date}\n"
if duration:
result += f"<b>Duration:</b> {duration}\n"
if rating:
result += f"<b>Rating:</b> {rating}\n"
if score:
result += f"<b>Score:</b> {score}\n"
if sfw:
result += f"<b>SFW:</b> Yes\n"
else:
result += f"<b>SFW:</b> No\n"
if synopsis:
result += f"<b>Synopsis:</b> {synopsis}"
if self.config["language"] == "en":
await utils.answer(message, self.strings["result"].format(result=result))
else:
translator = GoogleTranslator(source="auto", target=self.config["language"])
translation = translator.translate(
self.strings["result"].format(result=result)
)
await utils.answer(message, translation)
@loader.command(ru_doc="Поиск манги по названию", en_doc="Search manga by title")
async def smanga(self, m: Message):
"""Search manga by title"""
query = utils.get_args_raw(m)
if not query:
await utils.answer(m, self.strings["expression_missing"])
return
translator = GoogleTranslator(source="auto", target="en")
response = await utils.run_sync(
requests.get,
"https://api.jikan.moe/v4/manga",
params={"q": query},
)
data = response.json()
if "data" not in data or not data["data"]:
await utils.answer(m, self.strings["manga_not_found"])
return
manga = data["data"][0]
title = manga.get("title")
title_english = manga.get("title_english")
title_japanese = manga.get("title_japanese")
type_ = manga.get("type")
chapters = manga.get("chapters")
volumes = manga.get("volumes")
status = manga.get("status")
start_date = (
manga.get("published").get("from") if "published" in manga else None
)
end_date = manga.get("published").get("to") if "published" in manga else None
score = manga.get("score")
synopsis = manga.get("synopsis")
sfw = manga.get("explicit_genres") is None
result = f"<b>Title:</b> {title}\n"
if title_english:
result += f"<b>English Title:</b> {title_english}\n"
if title_japanese:
result += f"<b>Japanese Title:</b> {title_japanese}\n"
if type_:
result += f"<b>Type:</b> {type_}\n"
if chapters:
result += f"<b>Chapters:</b> {chapters}\n"
if volumes:
result += f"<b>Volumes:</b> {volumes}\n"
if status:
result += f"<b>Status:</b> {status}\n"
if start_date:
start_date = start_date.split("T")[0]
result += f"<b>Start Date:</b> {start_date}\n"
if end_date:
end_date = end_date.split("T")[0]
result += f"<b>End Date:</b> {end_date}\n"
if score:
result += f"<b>Score:</b> {score}\n"
if sfw:
result += f"<b>SFW:</b> Yes\n"
else:
result += f"<b>SFW:</b> No\n"
if synopsis:
result += f"<b>Synopsis:</b> {synopsis}"
if self.config["language"] == "en":
await utils.answer(m, self.strings["result"].format(result=result))
return
translator = GoogleTranslator(source="auto", target=self.config["language"])
translation = translator.translate(self.strings["result"].format(result=result))
await utils.answer(m, translation)
@loader.command(
ru_doc="Поиск персонажа по имени",
en_doc="Search character by name",
)
async def scharacter(self, message: Message):
"""Search character by name"""
query = utils.get_args_raw(message)
if not query:
await utils.answer(message, self.strings["expression_missing"])
return
translator = GoogleTranslator(source="auto", target="en")
response = await utils.run_sync(
requests.get,
"https://api.jikan.moe/v4/characters",
params={"q": query},
)
data = response.json()
if "data" not in data or not data["data"]:
await utils.answer(message, self.strings["character_not_found"])
return
character = data["data"][0]
name = character.get("name")
name_kanji = character.get("name_kanji")
nicknames = character.get("nicknames")
favorites = character.get("favorites")
about = character.get("about")
result = f"<b>Name:</b> {name}\n"
if name_kanji:
result += f"<b>Kanji Name:</b> {name_kanji}\n"
if nicknames:
result += f"<b>Nicknames:</b> {', '.join(nicknames)}\n"
if favorites is not None:
result += f"<b>Favorites:</b> {favorites}\n"
if about:
result += f"<b>About:</b> {about}"
if self.config["language"] == "en":
await utils.answer(message, self.strings["result"].format(result=result))
return
translator = GoogleTranslator(source="auto", target=self.config["language"])
translation = translator.translate(self.strings["result"].format(result=result))
await utils.answer(message, translation)
@loader.command(
ru_doc="Получить рекомендации аниме",
en_doc="Get anime recommendations",
)
async def rсanime(self, message: Message):
"""Get anime recommendations"""
response = await utils.run_sync(
requests.get,
"https://api.jikan.moe/v4/recommendations/anime",
)
data = response.json()
if "data" not in data or not data["data"]:
await utils.answer(
message,
self.strings["error"].format(error="No recommendations found."),
)
return
recommendations = data["data"][:3]
result = ""
for recommendation in recommendations:
title = recommendation["entry"][0].get("title")
content = recommendation.get("content")
user = recommendation.get("user")
username = user.get("username") if user else None
result += f"<b>Title:</b> {title}\n" if title else ""
result += f"<b>Content:</b> {content}\n" if content else ""
result += f"<b>User:</b> {username}\n" if username else ""
result += "\n"
if self.config["language"] == "en":
await utils.answer(message, self.strings["result"].format(result=result))
return
translator = GoogleTranslator(source="auto", target=self.config["language"])
translation = translator.translate(self.strings["result"].format(result=result))
await utils.answer(message, translation)
@loader.command(
ru_doc="Получить рекомендации манги",
en_doc="Get manga recommendations",
)
async def rсmanga(self, message: Message):
"""Get manga recommendations"""
response = await utils.run_sync(
requests.get,
"https://api.jikan.moe/v4/recommendations/manga",
)
data = response.json()
if "data" not in data or not data["data"]:
await utils.answer(
message,
self.strings["error"].format(error="No recommendations found."),
)
return
recommendations = data["data"][:3]
result = ""
for recommendation in recommendations:
title = recommendation["entry"][0].get("title")
content = recommendation.get("content")
user = recommendation.get("user")
username = user.get("username") if user else None
result += f"<b>Title:</b> {title}\n" if title else ""
result += f"<b>Content:</b> {content}\n" if content else ""
result += f"<b>User:</b> {username}\n" if username else ""
result += "\n"
if self.config["language"] == "en":
await utils.answer(message, self.strings["result"].format(result=result))
return
translator = GoogleTranslator(source="auto", target=self.config["language"])
translation = translator.translate(self.strings["result"].format(result=result))
await utils.answer(message, translation)
@loader.command(ru_doc="Случайное аниме", en_doc="Random anime")
async def ranime(self, message: Message):
"""Random anime"""
response = await utils.run_sync(
requests.get,
"https://api.jikan.moe/v4/random/anime",
)
data = response.json()
if not data.get("data"):
await utils.answer(message, self.strings["anime_not_found"])
return
anime = data["data"]
title = anime.get("title")
title_english = anime.get("title_english")
title_japanese = anime.get("title_japanese")
type_ = anime.get("type")
episodes = anime.get("episodes")
status = anime.get("status")
airing = anime.get("airing")
duration = anime.get("duration")
rating = anime.get("rating")
score = anime.get("score")
synopsis = anime.get("synopsis")
result = f"<b>Title:</b> {title}\n"
if title_english:
result += f"<b>English Title:</b> {title_english}\n"
if title_japanese:
result += f"<b>Japanese Title:</b> {title_japanese}\n"
if type_:
result += f"<b>Type:</b> {type_}\n"
if episodes:
result += f"<b>Episodes:</b> {episodes}\n"
if status:
result += f"<b>Status:</b> {status}\n"
if airing is not None:
result += f"<b>Airing:</b> {airing}\n"
if duration:
result += f"<b>Duration:</b> {duration}\n"
if rating:
result += f"<b>Rating:</b> {rating}\n"
if score:
result += f"<b>Score:</b> {score}\n"
if synopsis:
result += f"<b>Synopsis:</b> {synopsis}"
if self.config["language"] == "en":
await utils.answer(message, self.strings["result"].format(result=result))
return
translator = GoogleTranslator(source="auto", target=self.config["language"])
translation = translator.translate(self.strings["result"].format(result=result))
await utils.answer(message, translation)
@loader.command(ru_doc="Случайная манга", en_doc="Random manga")
async def rmanga(self, message: Message):
"""Random manga"""
response = await utils.run_sync(
requests.get,
"https://api.jikan.moe/v4/random/manga",
)
data = response.json()
if "data" not in data or not data["data"]:
await utils.answer(message, self.strings["manga_not_found"])
return
anime = data["data"]
title = anime.get("title")
title_english = anime.get("title_english")
title_japanese = anime.get("title_japanese")
type_ = anime.get("type")
episodes = anime.get("episodes")
status = anime.get("status")
airing = anime.get("airing")
duration = anime.get("duration")
rating = anime.get("rating")
score = anime.get("score")
synopsis = anime.get("synopsis")
result = f"<b>Title:</b> {title}\n"
if title_english:
result += f"<b>English Title:</b> {title_english}\n"
if title_japanese:
result += f"<b>Japanese Title:</b> {title_japanese}\n"
if type_:
result += f"<b>Type:</b> {type_}\n"
if episodes:
result += f"<b>Episodes:</b> {episodes}\n"
if status:
result += f"<b>Status:</b> {status}\n"
if airing is not None:
result += f"<b>Airing:</b> {airing}\n"
if duration:
result += f"<b>Duration:</b> {duration}\n"
if rating:
result += f"<b>Rating:</b> {rating}\n"
if score:
result += f"<b>Score:</b> {score}\n"
if synopsis:
result += f"<b>Synopsis:</b> {synopsis}"
if self.config["language"] == "en":
await utils.answer(message, self.strings["result"].format(result=result))
return
translator = GoogleTranslator(source="auto", target=self.config["language"])
translation = translator.translate(self.strings["result"].format(result=result))
await utils.answer(message, translation)

96
N3rcy/modules/news.py Normal file
View File

@@ -0,0 +1,96 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# reqires: feedparser
# scope: hikka_min 1.6.2
import feedparser
from hikkatl.tl.types import Message
from .. import loader, utils
NEWS_SOURCES = {
"Playground": "https://www.playground.ru/rss/news.xml",
"BBC": "https://feeds.bbci.co.uk/news/world/rss.xml",
"CNN": "http://rss.cnn.com/rss/edition.rss",
"The Guardian": "https://www.theguardian.com/world/rss",
"Le Monde": "https://www.lemonde.fr/rss/une.xml",
"RIA": "https://ria.ru/export/rss2/archive/index.xml",
"Lenta": "https://lenta.ru/rss/news",
"RBC": "https://rssexport.rbc.ru/rbcnews/news/30/full.rss",
}
@loader.tds
class NewsMod(loader.Module):
"""Module for displaying news from various sources"""
strings = {"name": "NewsMod"}
@loader.command(ru_doc="Получить последние новости с Playground")
async def playground(self, m: Message):
"""Get the latest news from Playground"""
await self._get_news_from_source(m, "Playground")
@loader.command(ru_doc="Получить последние новости с BBC")
async def bbc(self, m: Message):
"""Get the latest news from BBC"""
await self._get_news_from_source(m, "BBC")
@loader.command(ru_doc="Получить последние новости с CNN")
async def cnn(self, m: Message):
"""Get the latest news from CNN"""
await self._get_news_from_source(m, "CNN")
@loader.command(ru_doc="Получить последние новости с The Guardian")
async def guardian(self, m: Message):
"""Get the latest news from The Guardian"""
await self._get_news_from_source(m, "The Guardian")
@loader.command(ru_doc="Получить последние новости с Le Monde")
async def lemonde(self, m: Message):
"""Get the latest news from Le Monde"""
await self._get_news_from_source(m, "Le Monde")
@loader.command(ru_doc="Получить последние новости с Риа новости")
async def ria(self, m: Message):
"""Get the latest news from RIA"""
await self._get_news_from_source(m, "RIA")
@loader.command(ru_doc="Получить последние новости с Рбк новости")
async def rbc(self, m: Message):
"""Get the latest news from rbc"""
await self._get_news_from_source(m, "RBC")
@loader.command(ru_doc="Получить последние новости с Lenta")
async def lenta(self, m: Message):
"""Get the latest news from lenta"""
await self._get_news_from_source(m, "Lenta")
async def _get_news_from_source(self, message: Message, source_name: str):
"""Helper method to get news from a specific source"""
if source_name not in NEWS_SOURCES:
await utils.answer(
message, f"Invalid news source: {utils.escape_html(source_name)}"
)
return
feed_url = NEWS_SOURCES[source_name]
feed = feedparser.parse(feed_url)
await utils.answer(
message,
"<b><emoji document_id=5433982607035474385>📰</emoji>Latest 15 news from"
f" {source_name}</b>:\n\n"
+ "\n\n".join(
f"{i+1}: <a href='{entry.link}'>{utils.escape_html(entry.title)}</a>"
for i, entry in enumerate(feed.entries[:15])
),
)

135
N3rcy/modules/ocr.py Normal file
View File

@@ -0,0 +1,135 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
import json
import os
import requests
from hikkatl.tl.types import Message, MessageMediaPhoto
from .. import loader, utils
@loader.tds
class OCRMod(loader.Module):
"""Module for Optical Character Recognition"""
strings = {
"name": "OCRMod",
"file_not_found": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Not found to"
" recognize, please reply.</b>"
),
"error": (
f"<b><emoji document_id=5980953710157632545>❌</emoji>An error occurred"
f" while processing the image. Error: </b>"
),
"text_result": (
"<b><emoji document_id=6041850934756119589>🫠</emoji>Recognized:</b>\n"
),
"recognition": (
"<b><emoji document_id=5307937750828194743>🫥</emoji>Recognition...</b>"
),
"no_api": "<b><emoji document_id=5980953710157632545>❌</emoji> Please insert api-key in config</b> (<code>.cfg ocrmod</code>)",
"config_key": "Get key here: https://ocr.space/ocrapi/freekey",
"language": ("🌐 Recognition language, available can be viewed here:"
"https://ocr.space/OCRAPI#:~:text=faster%20upload%20speeds.-,language,-%5BOptional%5D%0AArabic"),
}
strings_ru = {
"file_not_found": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Не найдено, что"
" распознавать, ответь реплаем.</b>"
),
"error": (
f"<b><emoji document_id=5980953710157632545>❌</emoji>Произошла ошибка при"
f" обработке изображения. Ошибка: </b>"
),
"text_result": (
"<b><emoji document_id=6041850934756119589>🫠</emoji>Распознано:</b>\n"
),
"recognition": (
"<b><emoji document_id=5307937750828194743>🫥</emoji>Распознаю...</b>"
),
"no_api": "<b><emoji document_id=5980953710157632545>❌</emoji> Пожалуйста, вставьте api-key в конфиг</b> (<code>.cfg ocrmod</code>)",
"config_key": "Получить ключ можно здесь: https://ocr.space/ocrapi/freekey",
"language": ("🌐 Язык распознавания, доступные можно посмотреть здесь:"
"https://ocr.space/OCRAPI#:~:text=faster%20upload%20speeds.-,language,-%5BOptional%5D%0AArabic"),
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"api_key",
None,
lambda: self.strings['config_key'],
validator=loader.validators.Hidden(),
),
loader.ConfigValue(
"language",
"eng",
lambda: self.strings['language'],
),
)
async def ocr_space_file(self, filename, overlay=False):
"""OCR.space API request with local file."""
api_key = self.config["api_key"]
language = self.config["language"]
payload = {
"isOverlayRequired": overlay,
"apikey": api_key,
"language": language,
}
with open(filename, "rb") as f:
r = await utils.run_sync(
requests.post,
"https://api.ocr.space/parse/image",
files={filename: f},
data=payload,
)
return r.content.decode()
@loader.command(
ru_doc="Распознать текст на фото из реплая",
en_doc="Recognize text from an image in reply",
)
async def ocr(self, message: Message):
"""Recognize text from an image in reply"""
if not (reply_msg := await message.get_reply_message()) or not isinstance(
reply_msg.media, MessageMediaPhoto
):
await utils.answer(message, self.strings("file_not_found"))
return
if self.config['api_key'] == None:
await utils.answer(message, self.strings['no_api'])
return
try:
await utils.answer(message, self.strings("recognition"))
filename = await reply_msg.download_media(file="image.png")
result = await self.ocr_space_file(filename)
os.remove(filename)
parsed_result = json.loads(result)
parsed_text = parsed_result["ParsedResults"][0]["ParsedText"]
await utils.answer(
message,
f"{self.strings('text_result')}\n{utils.escape_html(parsed_text)}",
)
except Exception as e:
await utils.answer(
message, self.strings("error") + utils.escape_html(str(e))
)

120
N3rcy/modules/progmusic.py Normal file
View File

@@ -0,0 +1,120 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
import os
import random
import aiohttp
import feedparser
from hikkatl.tl.types import Message
from .. import loader, utils
from ..inline.types import InlineCall
@loader.tds
class MusicModule(loader.Module):
"""Module for music for programming from https://musicforprogramming.net/"""
strings = {
"name": "ProgMusic",
"fetching": "<b>Fetching music...</b>",
"downloading": (
"<b>Downloading... Please wait, usually it takes in 3-7 minutes ❤️</b>"
),
"download_failed": "<b>Failed to download music.</b>",
"successful": "<b>Enjoy the music!</b>",
}
@loader.command(en_doc="send random chill music")
async def prmusic(self, message: Message):
"""Send music for programming"""
await message.edit(self.strings["fetching"])
rss_url = "https://musicforprogramming.net/rss.xml"
feed = feedparser.parse(rss_url)
if not feed.entries:
await message.edit("Failed to fetch music.")
return
random_entry = random.choice(feed.entries)
title = random_entry.title
description = random_entry.description
link = random_entry.link
download_url = random_entry.enclosures[0].href
if not download_url:
await utils.answer(message, "Failed to fetch music.")
return
await self.inline.form(
text=f"<b>{title}</b>\n\n<b>Description:</b> {description}",
message=message,
reply_markup=[
[{"text": "Link 🔗", "url": link}],
[
{
"text": "Download ⬇️",
"callback": lambda c, u=download_url, l=link, t=title: self.download_and_send_music(
c, message.chat_id, u, t, l
),
}
],
[{"text": "Close ❌", "callback": self.close}],
],
silent=True,
)
async def close(self, call):
"""Close the inline form"""
await call.delete()
async def download_and_send_music(
self,
call: InlineCall,
chat_id: int,
download_url: str,
title: str,
link: str,
):
await call.edit(
text=f"<b>Title: {title}</b>\n<b>Link: {link}</b>\n\n"
+ self.strings("downloading")
)
async with aiohttp.ClientSession() as session:
async with session.get(download_url) as response:
if response.status != 200:
return
file = await response.read()
extension = download_url.split(".")[-1]
temp_file_path = os.path.join(os.getcwd(), f"chill.{extension}")
with open(temp_file_path, "wb") as temp_file:
temp_file.write(file)
await self.client.send_file(
chat_id,
temp_file_path,
voice=True,
only_document=False,
caption=self.strings("successful")
+ f"\n\n<b>Title: {utils.escape_html(title)}</b>\n<b>Link:"
f" {utils.escape_html(link)}</b>",
)
os.remove(temp_file_path)
await call.delete()

218
N3rcy/modules/top.py Normal file
View File

@@ -0,0 +1,218 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
# requires: matplotlib
from hikkatl.types import Message, PeerUser, PeerChat, PeerChannel
from .. import loader, utils
from collections import defaultdict
import matplotlib.pyplot as plt
import io
import asyncio
import warnings
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
from telethon.tl.functions.messages import SearchRequest, GetHistoryRequest
from telethon.tl.types import InputMessagesFilterEmpty
plt.style.use('dark_background')
@loader.tds
class Top(loader.Module):
"""Module for viewing the top list in chat"""
strings = {
"name": "Top",
"top": "Top users by message count",
"topchat": "<emoji document_id=5323538339062628165>💬</emoji><b>Top users in</b>",
"msgcount": "Message count",
"loading": "<emoji document_id=5780543148782522693>🕒</emoji><b>Message counting has started, please wait, it may take a long time if there are a lot of messages in the chat</b>",
"private_chat": "<emoji document_id=5323538339062628165>💬</emoji><b>Message count in private chat with</b>"
}
strings_ru = {
"top": "Топ пользователей по количеству сообщений",
"topchat": "<emoji document_id=5323538339062628165>💬</emoji><b>Топ пользователей в</b>",
"msgcount": "Количество сообщений",
"loading": "<emoji document_id=5780543148782522693>🕒</emoji><b>Подсчет сообщений начался, пожалуйста подождите, это может занять много времени если в чате много сообщений</b>",
"private_chat": "<emoji document_id=5323538339062628165>💬</emoji><b>Количество сообщений в личном чате с</b>"
}
@loader.command(ru_doc="Посмотреть топ в чате")
async def top(self, m: Message):
"""View top in the chat"""
await utils.answer(m, self.strings['loading'])
client = self.client
if isinstance(m.peer_id, PeerUser):
chat_type = 'private'
chat_id = m.peer_id.user_id
elif isinstance(m.peer_id, PeerChat) or isinstance(m.peer_id, PeerChannel):
chat_type = 'chat'
chat_id = m.chat.id
else:
await utils.answer(m, "Unsupported chat type.")
return
if chat_type == 'chat':
users = await client.get_participants(chat_id)
users_dict = {user.id: (user.username or user.first_name) for user in users}
message_count = defaultdict(int)
for user_id in users_dict:
result = await client(SearchRequest(
peer=chat_id,
q='',
filter=InputMessagesFilterEmpty(),
from_id=user_id,
limit=0,
min_date=None,
max_date=None,
offset_id=0,
add_offset=0,
max_id=0,
min_id=0,
hash=0
))
message_count[user_id] = result.count
sorted_message_count = sorted(message_count.items(), key=lambda item: item[1], reverse=True)
top_users = sorted_message_count[:20]
usernames = [users_dict[user_id] or "Unknown" for user_id, _ in top_users]
counts = [count for _, count in top_users]
fig, ax = plt.subplots(figsize=(10, 5))
colors = self._generate_gradient('#8A2BE2', '#4B0082', len(usernames))
bars = ax.barh(usernames, counts, color=colors, edgecolor='black', linewidth=0.5)
for bar in bars:
bar.set_alpha(0.8)
bar.set_hatch('///')
ax.set_xlabel(self.strings['msgcount'], fontsize=12, color='white')
ax.set_title(self.strings['top'], fontsize=14, color='white', pad=20)
ax.invert_yaxis()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#8A2BE2')
ax.spines['bottom'].set_color('#8A2BE2')
ax.grid(True, linestyle='--', alpha=0.6, color='gray')
for i, (bar, username) in enumerate(zip(bars, usernames)):
if i < 3:
bar.set_color('#FFD700')
ax.text(bar.get_width() + 5, bar.get_y() + bar.get_height() / 2,
f'#{i+1}', va='center', ha='left', color='#FFD700', fontsize=12)
buf = io.BytesIO()
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
buf.seek(0)
caption = f"{self.strings['topchat']} <b>{m.chat.title}:</b>\n"
caption += "\n".join([f"{i+1}. {user} - {count}" for i, (user, count) in enumerate(zip(usernames, counts))])
await utils.answer_file(m, buf, caption, force_document=False)
else:
me = await client.get_me()
target = await client.get_entity(chat_id)
my_count, their_count = await asyncio.gather(
self._get_message_count_fast(client, chat_id, me.id),
self._get_message_count_fast(client, chat_id, target.id)
)
message_counts = [(me.first_name, my_count), (target.first_name, their_count)]
sorted_message_counts = sorted(message_counts, key=lambda item: item[1], reverse=True)
usernames = [user for user, _ in sorted_message_counts]
counts = [count for _, count in sorted_message_counts]
fig, ax = plt.subplots(figsize=(10, 5))
colors = self._generate_gradient('#8A2BE2', '#4B0082', len(usernames))
bars = ax.barh(usernames, counts, color=colors, edgecolor='black', linewidth=0.5)
for bar in bars:
bar.set_alpha(0.8)
bar.set_hatch('///')
ax.set_xlabel(self.strings['msgcount'], fontsize=12, color='white')
ax.set_title(self.strings['top'], fontsize=14, color='white', pad=20)
ax.invert_yaxis()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#8A2BE2')
ax.spines['bottom'].set_color('#8A2BE2')
ax.grid(True, linestyle='--', alpha=0.6, color='gray')
for i, (bar, username) in enumerate(zip(bars, usernames)):
if i < 3:
bar.set_color('#FFD700')
ax.text(bar.get_width() + 5, bar.get_y() + bar.get_height() / 2,
f'#{i+1}', va='center', ha='left', color='#FFD700', fontsize=12)
buf = io.BytesIO()
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
buf.seek(0)
caption = f"{self.strings['private_chat']} <b>{target.first_name}:</b>\n"
caption += "\n".join([f'"{user}" - {count}' for user, count in zip(usernames, counts)])
await utils.answer_file(m, buf, caption, force_document=False)
async def _get_message_count_fast(self, client, chat_id, user_id):
"""Получает количество сообщений от конкретного пользователя с использованием GetHistoryRequest"""
total_count = 0
offset_id = 0
limit = 100
while True:
history = await client(GetHistoryRequest(
peer=chat_id,
offset_id=offset_id,
offset_date=None,
add_offset=0,
limit=limit,
max_id=0,
min_id=0,
hash=0
))
if not history.messages:
break
for message in history.messages:
if message.sender_id == user_id:
total_count += 1
offset_id = history.messages[-1].id
if len(history.messages) < limit:
break
return total_count
def _generate_gradient(self, start_color, end_color, n):
"""Генерация градиента между двумя цветами"""
cmap = LinearSegmentedColormap.from_list('custom_gradient', [start_color, end_color], N=n)
return [cmap(i) for i in np.linspace(0, 1, n)]

121
N3rcy/modules/twitch.py Normal file
View File

@@ -0,0 +1,121 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
# reqires: youtube_dl
import youtube_dl
from hikkatl.tl.types import Message
from .. import loader, utils
@loader.tds
class TwitchModule(loader.Module):
"""Module for downloading Twitch clips and videos"""
strings = {
"name": "Twitchdl",
"clip_download_started": (
"<emoji document_id=5307773751796964107>⏳</emoji><b>Clip download started,"
" please wait.</b>"
),
"clip_download_completed": (
"<b><emoji document_id=5215480011322042129>➡️</emoji>Clip download"
" completed. Sending...</b>"
),
"clip_download_failed": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Clip download failed."
" Ensure the correctness of the link.</b>"
),
"video_download_started": (
"<b><emoji document_id=5307773751796964107>⏳</emoji>Video download"
" started, please wait.</b>"
),
"video_download_completed": (
"<b><emoji document_id=5215480011322042129>➡️</emoji>Video download"
" completed. Sending...</b>"
),
"video_download_failed": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Video download failed."
" Ensure the correctness of the link.</b>"
),
}
strings_ru = {
"clip_download_started": (
"<emoji document_id=5307773751796964107>⏳</emoji><b>Скачивание клипа"
" началось, подождите.</b>"
),
"clip_download_completed": (
"<b><emoji document_id=5215480011322042129>➡️</emoji>Скачивание клипа"
" завершено. Отправляю...</b>"
),
"clip_download_failed": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Ошибка скачивания"
" клипа. Убедитесь в ссылке.</b>"
),
"video_download_started": (
"<b><emoji document_id=5307773751796964107>⏳</emoji>Скачивание видео"
" началось, подождите.</b>"
),
"video_download_completed": (
"<b><emoji document_id=5215480011322042129>➡️</emoji>Скачивание видео"
" завершено. Отправляю...</b>"
),
"video_download_failed": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Ошибка скачивания"
" видео. Убедитесь в ссылке.</b>"
),
}
@loader.command(ru_doc="Скачать клип с Twitch")
async def twitch(self, message: Message):
"""Download a clip from Twitch"""
if not (clip_url := utils.get_args_raw(message)):
await utils.answer(message, self.strings("clip_download_failed"))
return
try:
await utils.answer(message, self.strings("clip_download_started"))
with youtube_dl.YoutubeDL(
{
"outtmpl": "clip.mp4",
"format": "bestvideo[height<=720]+bestaudio/best[height<=720]",
}
) as ydl:
ydl.download([clip_url])
await utils.answer_file(message, "clip.mp4")
except Exception:
await utils.answer(message, self.strings("clip_download_failed"))
@loader.command(ru_doc="Скачать видео с Twitch")
async def twitchvideo(self, message: Message):
"""Download a video from Twitch"""
if not (video_url := utils.get_args_raw(message)):
await utils.answer(message, self.strings("video_download_failed"))
return
try:
await utils.answer(message, self.strings("video_download_started"))
with youtube_dl.YoutubeDL(
{
"outtmpl": "video.mp4",
"format": "bestvideo[height<=720]+bestaudio/best[height<=720]",
}
) as ydl:
ydl.download([video_url])
await utils.answer_file(message, "video.mp4")
except Exception as e:
await utils.answer(message, self.strings("video_download_failed"))

117
N3rcy/modules/watch.py Normal file
View File

@@ -0,0 +1,117 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
from hikkatl.tl.types import Message
from .. import loader, utils
@loader.tds
class WatcherModule(loader.Module):
"""Module for watching and responding to specific words"""
strings = {
"name": "WatcherModule",
"watch_added": (
"<emoji document_id=5210956306952758910>👀</emoji>Watch added: {word}"
),
"watch_removed": (
"<b><emoji document_id=5980930633298350051>✅</emoji>Watch removed:</b>"
" {word}"
),
"watch_list": (
"<b><emoji document_id=5818865088970362886>❕</emoji>List of watched"
" words:</b>\n\n{watch_list}"
),
}
strings_ru = {
"watch_added": (
"<b><emoji document_id=5210956306952758910>👀</emoji>Отслеживание"
" добавлено:</b> {word}"
),
"watch_removed": (
"<b><emoji document_id=5980930633298350051>✅</emoji>Отслеживание"
" удалено:</b> {word}"
),
"watch_list": (
"<b><emoji document_id=5818865088970362886>❕</emoji>Список отслеживаемых"
" слов:</b>\n\n{watch_list}"
),
}
def __init__(self):
self._watches = {}
async def client_ready(self):
self._load_watches()
async def watcher(self, message: Message):
"""Watcher method to track and respond to specific words"""
if getattr(message, "text", None):
text = message.text.lower()
for word, response in self._watches.items():
if word.lower() == text:
await message.reply(response)
@loader.command(ru_doc="Добавить отслеживание слова")
async def addwatch(self, message: Message):
"""Add a word to be watched"""
if len(args := utils.get_args_split_by(message, "$")) < 2:
await utils.answer(
message,
(
"<emoji document_id=5978859389614821335>❌</emoji> Не указан один"
' из аргументов! Пример правильной команды: `.addwatch "первое'
' слово"$"ответ на него"`'
),
)
return
word = args[0].strip()
response = args[1].strip()
self._watches[word] = response
self._save_watches()
await utils.answer(message, self.strings("watch_added").format(word=word))
@loader.command(ru_doc="Удалить отслеживание слова")
async def rmwatch(self, m: Message):
"""Remove a word from being watched"""
if (word := utils.get_args_raw(m)) not in self._watches:
await utils.answer(m, f"Watch not found for word: {word}")
return
del self._watches[word]
self._save_watches()
await utils.answer(m, self.strings("watch_removed").format(word=word))
@loader.command(ru_doc="Показать список отслеживаемых слов")
async def listwatches(self, message: Message):
"""Show the list of watched words"""
if not self._watches:
await utils.answer(message, "No watches found")
return
await utils.answer(
message,
self.strings("watch_list").format(
watch_list="\n".join(
f"{word}: {response}" for word, response in self._watches.items()
)
),
)
def _save_watches(self):
self.set("watches", self._watches)
def _load_watches(self):
self._watches = self.get("watches", {})

544
N3rcy/modules/whisper.py Normal file
View File

@@ -0,0 +1,544 @@
# ╔╗╔┌─┐┬─┐┌─┐┬ ┬
# ║║║├┤ ├┬┘│ └┬┘
# ╝╚╝└─┘┴└─└─┘ ┴
# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified.
# https://creativecommons.org/licenses/by-nc-nd/4.0/
# You CANNOT edit this file without direct permission from the author.
# You can redistribute this file without any changes.
# meta developer: @nercymods
# scope: hikka_min 1.6.2
# requires: pydub openai==1.3.8 ffmpeg
import os
import openai
from hikkatl.tl.types import Message
from pydub import AudioSegment
import requests
import base64
from .. import loader, utils
@loader.tds
class WhisperMod(loader.Module):
"""Module for speech recognition"""
strings = {
"name": "WhisperMod",
"audio_not_found": (
"<b><emoji document_id=5818678700274617758>👮‍♀️</emoji>Not found to"
" recognize.</b>"
),
"recognized": (
"<b><emoji"
" document_id=5821302890932736039>🗣</emoji>Recognized:</b>\n{transcription}"
),
"error": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Error occurred during"
" transcription.</b>"
),
"recognition": (
"<b><emoji document_id=5307937750828194743>🫥</emoji>Recognition...</b>"
),
"downloading": "<b><emoji document_id=5310189005181036109>🐍</emoji>Downloading, wait</b>",
"autowhisper_enabled": (
"<b><emoji document_id=5307937750828194743>🫥</emoji>Auto-whisper enabled"
" in this chat.</b>"
),
"autowhisper_disabled": (
"<b><emoji document_id=5307937750828194743>🫥</emoji>Auto-whisper disabled"
" in this chat.</b>"
),
"no_api": "<b><emoji document_id=5980953710157632545>❌</emoji> Insert openai api-key in config</b> (<code>.cfg whispermod</code>)",
"invalid_key": "<b><emoji document_id=5980953710157632545>❌</emoji> Invalid openai api-key</b>",
"hf_instructions": (
"<emoji document_id=5238154170174820439>👩‍🎓</emoji> <b>How to get hugging face api token:</b>\n"
"<b>&gt; Open Hugging Face and sign in.</b> <emoji document_id=4904848288345228262>👤</emoji> <b>\n"
"&gt; Go to Settings → Access Tokens: </b><a href=\"https://huggingface.co/settings/tokens\"><b>https://huggingface.co/settings/tokens</b></a><b>.</b> <emoji document_id=5222142557865128918>⚙️</emoji> <b>\n"
"&gt; Click New Token.</b> <emoji document_id=5431757929940273672></emoji> <b>\n"
"&gt; Select permission: \"make calls to the serverless Inference API\".</b> <emoji document_id=5253952855185829086>⚙️</emoji> <b>\n"
"&gt; Click Create Token.</b> <emoji document_id=5253652327734192243></emoji> <b>\n"
"&gt; Copy the token and paste it into the config.</b> <emoji document_id=4916036072560919511>✅</emoji>"
),
"hf_token_missing": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Missing hugging face api token</b>"
" (<code>.cfg whispermod</code>)"
)
}
strings_ru = {
"audio_not_found": (
"<b><emoji document_id=5818678700274617758>👮‍♀️</emoji>Не найдено, что"
" распознавать.</b>"
),
"recognized": (
"<b><emoji"
" document_id=5821302890932736039>🗣</emoji>Распознано:</b>\n{transcription}"
),
"error": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Ошибка при"
" транскрипции.</b>"
),
"recognition": (
"<b><emoji document_id=5307937750828194743>🫥</emoji>Распознавание...</b>"
),
"downloading": (
"<b><emoji document_id=5310189005181036109>🐍</emoji>Скачивание,"
" подождите...</b>"
),
"autowhisper_enabled": (
"<b><emoji document_id=5307937750828194743>🫥</emoji>Автораспознавание"
" включено в этом чате.</b>"
),
"autowhisper_disabled": (
"<b><emoji document_id=5307937750828194743>🫥</emoji>Автораспознавание"
" отключено в этом чате.</b>"
),
"no_api": (
"<b><emoji document_id=5980953710157632545>❌</emoji> Укажите api-ключ в конфиге</b>"
" (<code>.cfg whispermod</code>)"
),
"invalid_key": (
"<b><emoji document_id=5980953710157632545>❌</emoji> Неверный api-ключ</b>"
),
"hf_instructions": (
"<emoji document_id=5238154170174820439>👩‍🎓</emoji> <b>Как получить api-токен hugging face:</b>\n"
"<b>&gt; Откройте Hugging Face и войдите в аккаунт. </b><emoji document_id=4904848288345228262>👤</emoji><b>\n"
"&gt; Перейдите в Settings → Access Tokens: </b><a href=\"https://huggingface.co/settings/tokens\"><b>https://huggingface.co/settings/tokens</b></a><b>. </b><emoji document_id=5222142557865128918>⚙️</emoji><b>\n"
"&gt; Нажмите New Token. </b><emoji document_id=5431757929940273672></emoji><b>\n"
"&gt; Выберите разрешение: \"make calls to the serverless Inference API\". </b><emoji document_id=5253952855185829086>⚙️</emoji><b>\n"
"&gt; Нажмите Create Token. </b><emoji document_id=5253652327734192243></emoji><b>\n"
"&gt; Скопируйте токен и вставьте его в конфиг. </b><emoji document_id=4916036072560919511>✅</emoji>"
),
"hf_token_missing": (
"<b><emoji document_id=5980953710157632545>❌</emoji>Отсутствует api-токен hugging face</b>"
" (<code>.cfg whispermod</code>)"
)
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"api_key",
None,
lambda: "Api key for Whisper (https://platform.openai.com/account/api-keys)",
validator=loader.validators.Hidden(),
),
loader.ConfigValue(
"temperature",
"0.2",
lambda: (
"The sampling temperature, between 0 and 1. Higher values like 0.8"
" will make the output more random, while lower values like 0.2"
" will make it more focused and deterministic. If set to 0, the"
" model will use log probability to automatically increase the"
" temperature until certain thresholds are hit."
),
validator=loader.validators.String(),
),
loader.ConfigValue(
"prompt",
None,
lambda: (
"An optional text to guide the model's style or continue a previous"
" audio segment. The prompt should match the audio language."
),
validator=loader.validators.String(),
),
loader.ConfigValue(
"hf_api_key",
None,
lambda: "Api key for hugging face (look .hfguide)",
validator=loader.validators.Hidden(),
),
loader.ConfigValue(
"auto_voice",
True,
lambda: "Enable auto-recognition for voice messages",
validator=loader.validators.Boolean()
),
loader.ConfigValue(
"auto_video",
True,
lambda: "Enable auto-recognition for video messages",
validator=loader.validators.Boolean()
),
)
@loader.command(ru_doc="распознать речь из голосового/видео сообщения в реплае, используя openai api")
async def whisper(self, message: Message):
"""Transcribe speech from a voice/video message in reply using openai api"""
if self.config["api_key"] is None:
await utils.answer(message, self.strings["no_api"])
return
rep = await message.get_reply_message()
down = await utils.answer(message, self.strings["downloading"])
file = await rep.download_media()
file_extension = os.path.splitext(file)[1].lower()
if file_extension in [".oga", ".ogg"]:
await self.client.edit_message(
message.chat_id, down.id, self.strings["recognition"]
)
input_file = file
audio = AudioSegment.from_file(input_file, format="ogg")
audio.export("output_file.mp3", format="mp3")
audio_file = open("output_file.mp3", "rb")
client = openai.AsyncOpenAI(api_key=self.config["api_key"])
try:
response = await client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
prompt=self.config["prompt"],
temperature=self.config["temperature"],
)
except openai.AuthenticationError:
await utils.answer(message, self.strings["invalid_key"])
return
except Exception as e:
await utils.answer(
message,
f"<b><emoji document_id=5980953710157632545>❌</emoji>Error: {e}</b>",
)
return
transcription = response.text
await self.client.edit_message(
message.chat_id,
down.id,
self.strings["recognized"].format(transcription=transcription),
)
os.remove(file)
os.remove("output_file.mp3")
elif file_extension in [".mp3", "m4a", ".wav", ".mpeg", ".mp4"]:
await self.client.edit_message(
message.chat_id, down.id, self.strings["recognition"]
)
input_file = file
audio_file = open(input_file, "rb")
client = openai.AsyncOpenAI(api_key=self.config["api_key"])
try:
response = await client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
prompt=self.config["prompt"],
temperature=self.config["temperature"],
)
except openai.AuthenticationError:
await utils.answer(message, self.strings["invalid_key"])
return
except Exception as e:
await utils.answer(
message,
f"<b><emoji document_id=5980953710157632545>❌</emoji>Error: {e}</b>",
)
return
transcription = response.text
await self.client.edit_message(
message.chat_id,
down.id,
self.strings["recognized"].format(transcription=transcription),
)
os.remove(file)
else:
await utils.answer(message, self.strings["audio_not_found"])
@loader.command(
ru_doc=(
"включить/выключить автораспознавание голосовых и видео сообщений в чате"
" где введена команда"
)
)
async def autowhspr(self, message: Message):
"""Enable/disable auto-speech recognition for voice and video messages"""
chat_id = str(message.chat_id)
current_state = self.get("autowhspr", {})
enabled = current_state.get(chat_id, False)
if enabled:
current_state.pop(chat_id, None)
status_message = self.strings["autowhisper_disabled"]
else:
current_state[chat_id] = True
status_message = self.strings["autowhisper_enabled"]
self.set("autowhspr", current_state)
await utils.answer(message, status_message)
@loader.watcher(only_media=True)
async def autowhisper_watcher(self, message: Message):
"""Watcher to automatically transcribe voice and video messages when auto-speech recognition is enabled"""
chat_id = str(message.chat_id)
current_state = self.get("autowhspr", {})
if current_state.get(chat_id, False):
if (message.voice and self.config["auto_voice"]) or (message.video and self.config["auto_video"]):
if not message.gif and not message.sticker and not message.photo:
rep = message
await self.whisperwatch(rep)
async def whisperwatch(self, message: Message):
"""Transcribe speech from a voice/video message in reply"""
rep = message
down = await self.client.send_message(
message.chat.id, message=self.strings["downloading"], reply_to=rep.id
)
file = await rep.download_media()
file_extension = os.path.splitext(file)[1].lower()
if file_extension in [".oga", ".ogg"]:
await self.client.edit_message(
message.chat_id, down.id, self.strings["recognition"]
)
input_file = file
audio = AudioSegment.from_file(input_file, format="ogg")
audio.export("output_file.mp3", format="mp3")
audio_file = open("output_file.mp3", "rb")
client = openai.AsyncOpenAI(api_key=self.config["api_key"])
try:
response = await client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
prompt=self.config["prompt"],
temperature=self.config["temperature"],
)
except openai.AuthenticationError:
await utils.answer(message, self.strings["invalid_key"])
return
except Exception as e:
await utils.answer(
message,
f"<b><emoji document_id=5980953710157632545>❌</emoji>Error: {e}</b>",
)
return
transcription = response.text
await self.client.edit_message(
message.chat_id,
down.id,
self.strings["recognized"].format(transcription=transcription),
)
os.remove(file)
os.remove("output_file.mp3")
elif file_extension in [".mp3", "m4a", ".wav", ".mpeg", ".mp4"]:
await self.client.edit_message(
message.chat_id, down.id, self.strings["recognition"]
)
input_file = file
audio_file = open(input_file, "rb")
client = openai.AsyncOpenAI(api_key=self.config["api_key"])
try:
response = await client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
prompt=self.config["prompt"],
temperature=self.config["temperature"],
)
except openai.AuthenticationError:
await utils.answer(message, self.strings["invalid_key"])
return
except Exception as e:
await utils.answer(
message,
f"<b><emoji document_id=5980953710157632545>❌</emoji>Error: {e}</b>",
)
return
transcription = response.text
await self.client.edit_message(
message.chat_id,
down.id,
self.strings["recognized"].format(transcription=transcription),
)
os.remove(file)
else:
return
@loader.command(ru_doc="распознать речь из голосового/видео сообщения в реплае, используя hugging face api")
async def hfwhisper(self, m: Message):
"""Transcribe speech from a voice/video message in reply using hugging face api"""
if self.config["hf_api_key"] is None:
await utils.answer(m, self.strings["hf_token_missing"])
return
rep = await m.get_reply_message()
await utils.answer(m, self.strings["downloading"])
file = await rep.download_media()
file_extension = os.path.splitext(file)[1].lower()
if file_extension in ['.ogg', '.oga']:
try:
await utils.answer(m, self.strings["recognition"])
with open(file, "rb") as f:
audio_bytes = f.read()
audio_b64 = base64.b64encode(audio_bytes).decode('utf-8')
payload = {
"inputs": audio_b64,
}
response = await utils.run_sync(
requests.post,
url = "https://router.huggingface.co/hf-inference/models/openai/whisper-large-v3-turbo",
headers = {"Authorization": f"Bearer {self.config['hf_api_key']}",
"x-use-cache": "false",
"x-wait-for-model": "true",
"Content-Type": "application/json"
},
json = payload,
)
output = response.json()
os.remove(file)
return await utils.answer(m, self.strings["recognized"].format(transcription=output['text']))
except Exception as e:
import logging
logging.getLogger().error(e)
return await utils.answer(m, self.strings["error"])
elif file_extension in [".mp3", "m4a", ".wav", ".mpeg", ".mp4"]:
try:
await utils.answer(m, self.strings["recognition"])
audio = AudioSegment.from_file(file, format=file_extension.replace('.', ''))
audio.export("output_file.mp3", format="mp3")
with open("output_file.mp3", "rb") as f:
audio_bytes = f.read()
audio_b64 = base64.b64encode(audio_bytes).decode('utf-8')
payload = {
"inputs": audio_b64,
"language": "ru",
"attention_mask": [1] * len(audio_bytes)
}
response = await utils.run_sync(
requests.post,
url = "https://router.huggingface.co/hf-inference/models/openai/whisper-large-v3-turbo",
headers = {"Authorization": f"Bearer {self.config['hf_api_key']}",
"x-use-cache": "false",
"x-wait-for-model": "true",
"Content-Type": "application/json"
},
json = payload,
)
output = response.json()
os.remove("output_file.mp3")
os.remove(file)
return await utils.answer(m, self.strings["recognized"].format(transcription=output['text']))
except Exception as e:
import logging
logging.getLogger().error(e)
return await utils.answer(m, self.strings["error"])
@loader.command(
ru_doc=(
"включить/выключить автораспознавание через Hugging Face API в текущем чате"
)
)
async def hfautowhspr(self, message: Message):
"""Enable/disable auto-speech recognition using Hugging Face API"""
chat_id = str(message.chat_id)
current_state = self.get("hfautowhspr", {})
enabled = current_state.get(chat_id, False)
if enabled:
current_state.pop(chat_id, None)
status_message = self.strings["autowhisper_disabled"]
else:
current_state[chat_id] = True
status_message = self.strings["autowhisper_enabled"]
self.set("hfautowhspr", current_state)
await utils.answer(message, status_message)
@loader.watcher(only_media=True)
async def hfautowhisper_watcher(self, message: Message):
"""Watcher for Hugging Face auto-transcription"""
chat_id = str(message.chat_id)
current_state = self.get("hfautowhspr", {})
if current_state.get(chat_id, False):
if (message.voice and self.config["auto_voice"]) or (message.video and self.config["auto_video"]):
if not message.gif and not message.sticker and not message.photo:
rep = message
await self.hfwhisperwatch(rep)
async def hfwhisperwatch(self, message: Message):
"""Auto-transcribe using Hugging Face API"""
if self.config["hf_api_key"] is None:
return
rep = message
down = await self.client.send_message(
message.chat.id, self.strings["downloading"], reply_to=rep.id
)
file = await rep.download_media()
file_extension = os.path.splitext(file)[1].lower()
try:
await self.client.edit_message(
message.chat_id, down.id, self.strings["recognition"]
)
if file_extension in ['.ogg', '.oga']:
with open(file, "rb") as f:
audio_bytes = f.read()
else:
audio = AudioSegment.from_file(file, format=file_extension.replace('.', ''))
audio.export("temp_audio.mp3", format="mp3")
with open("temp_audio.mp3", "rb") as f:
audio_bytes = f.read()
os.remove("temp_audio.mp3")
audio_b64 = base64.b64encode(audio_bytes).decode('utf-8')
response = await utils.run_sync(
requests.post,
url = "https://router.huggingface.co/hf-inference/models/openai/whisper-large-v3-turbo",
headers={
"Authorization": f"Bearer {self.config['hf_api_key']}",
"x-use-cache": "false",
"x-wait-for-model": "true",
"Content-Type": "application/json"
},
json={"inputs": audio_b64},
)
if response.status_code != 200:
raise Exception(f"API Error: {response.text}")
output = response.json()
text = output.get('text', '')
await self.client.edit_message(
message.chat_id,
down.id,
self.strings["recognized"].format(transcription=text),
)
except Exception as e:
await self.client.edit_message(
message.chat_id,
down.id,
f"<b>❌ Error: {str(e)}</b>"
)
finally:
if os.path.exists(file):
os.remove(file)
if os.path.exists("temp_audio.mp3"):
os.remove("temp_audio.mp3")
@loader.command(ru_doc="гайд как получить hugging face токен", en_doc="guide how to get hugging face token")
async def hfguide(self, m: Message):
await utils.answer(m, self.strings['hf_instructions'])