patch 89bc0957a4fb31bf8066e338bc4548cd2d52c821
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Nov 2 20:20:45 CET 2024
* telegram, irc: Set topic in IRC with Telegram channel/chat description
patch 86ef89f9f9220ecde8477db2aa673f94f32656ab
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Oct 26 20:43:28 CEST 2024
* telegram: Fix op and founder detection in channels
patch 6d99fce9643fbdda1594f99331f4a4642ecc7f0f
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Oct 26 20:35:11 CEST 2024
* telegram: Fix handler for next reactions when the event is empty
patch efaf8df4b3fa314997615e2fc07a3decb3a7cfba
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Oct 26 13:01:02 CEST 2024
* exclam: Reorder handler list so commands are listed ordered in help
patch 0cfc7e59b24fb1a1b279fc593f8d04d0648e3880
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Oct 21 00:54:16 CEST 2024
* README update
patch abf1d31ddcf3cddd55844900065a3c3dd6bf9c67
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Oct 20 02:32:44 CEST 2024
* exclam: Add "-" parameter to "!react" to remove a reaction
patch 5c1687a16a1e49af7b5f70e9449c447bbf7c9df9
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Oct 19 22:56:31 CEST 2024
* telegram: On emoticon->emoji conversions for reactions,
when several emoticons can be mapped to an emoji,
keep first elements that are more probable allowed for reactions
patch 7db9d9a07af099b00a32a6ee0197c400a52240e5
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Oct 13 23:00:24 CEST 2024
* telegram: Limit text length for reactions to twice as replies
patch 42fe2f72d41ed36616e6a19b9388eec356679e38
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Oct 12 23:17:45 CEST 2024
* README update
patch 873004141f83b60da113e8967a2148d9d33008be
Author: E. Bosch <presidev@AT@gmail.com>
Date: Thu Oct 10 23:11:40 CEST 2024
* exclam: Add reaction (!react) command to send emoticon->emoji as Telegram reaction
patch 69a63d9f5f49ece35c5d552ba8b3081c8277490d
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Oct 7 00:07:54 CEST 2024
* telegram, service: Move initial help to service module
add a line for "equivalent command"
patch c1ffe716a42ea01ca345d7a756b685d7174f99c5
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Oct 6 23:59:23 CEST 2024
* telegram: Fix regression in delete reaction event
patch beacde93a685dd954f9823dc0a6fea4594b2c1e4
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Sep 28 02:53:06 CEST 2024
* telegram: Avoid duplicated reactions events in some cases
patch b2f8fe9251a26c43e16ba1aadff8d71e64a5a7e9
Author: E. Bosch <presidev@AT@gmail.com>
Date: Fri Sep 27 11:05:53 CEST 2024
* telegram: Add handler for next reactions (2nd, 3rd, etc.) that don't come from
the same events as 1st (why?!)
patch 8770a66d55d4d1c34e009fe5d0078f77c3be4d34
Author: E. Bosch <presidev@AT@gmail.com>
Date: Wed Sep 25 01:36:06 CEST 2024
* telegram: Fix in reaction handler
patch 7e550077a65e4737ca30a81e0decbcb4db0485a4
Author: E. Bosch <presidev@AT@gmail.com>
Date: Fri Sep 20 23:50:13 CEST 2024
* telegram: Don't truncate text for reactions of replies
patch f9ff84bf789b6bd5109c5953590ca41c438fe123
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Sep 15 23:50:10 CEST 2024
* telegram: Minor improvement in debug of relay methods
patch 1d527812923bdf50653bb371185bec70c2abad40
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Sep 15 01:23:24 CEST 2024
* telegram: Improve a bit reactions handler
patch b43b2bc6a4e9dcf0eaddb66ea3fd5abf7c95082b
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Sep 7 23:20:27 CEST 2024
* Fix typo in a constant
patch 6aaf9a2af5898f8b6ec1027a3ccb7e85f6893f22
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Sep 1 01:01:19 CEST 2024
* Increase virtual version to 0.2
Remove alpha status
patch a39c65dc932ee95b44b5a759cad3e413177fc5aa
Author: E. Bosch <presidev@AT@gmail.com>
Date: Fri Aug 30 21:53:13 CEST 2024
* telegram: Add a cache of "volatile" events (delete, edit, react) to be shown in history
patch 95e72ac9b26835162b8ba997c5ff99edfd5d464e
Author: E. Bosch <presidev@AT@gmail.com>
Date: Fri Aug 30 19:00:53 CEST 2024
* utils: Small optimization in pretty()
patch 799dcf8a6a7c8346af93e7f17841baf08db70e7c
Author: E. Bosch <presidev@AT@gmail.com>
Date: Fri Aug 30 01:58:06 CEST 2024
* utils: Add current_date() shortcut
patch 6c991a90a37dcc5a96906992b5c4df41e7f68991
Author: E. Bosch <presidev@AT@gmail.com>
Date: Thu Aug 29 23:06:59 CEST 2024
* Add trailing commas (and some spacing)
patch 6057cbb6c30094c80cba9f6326a5b513a9ab540c
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Aug 25 01:21:01 CEST 2024
* utils, telegram: Add pretty() function to print readable objects in debug
patch 0fd4392905932f144e14844f164d801a22b68467
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Aug 18 14:01:26 CEST 2024
* exclam: Add re-upload (!reupl) command to upload files/media as a reply to a message
patch 477b15fc239d17cccf626d042db2f323e1fa1b4b
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Aug 18 13:58:56 CEST 2024
* exclam: Check valid range for message IDs
patch 35206dbcb8c561df88e525160e5d6a50dad07481
Author: E. Bosch <presidev@AT@gmail.com>
Date: Thu Aug 15 01:30:54 CEST 2024
* Handle replies to deleted messages (maybe this case is only given from history)
patch ca113b48abe430eb11d500a5eb086edd15f23b0d
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 28 19:45:06 CEST 2024
* Update copyright year in LICENSE
patch 3ad99bad13beb07c34a9a992b026ed923f39b047
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 28 13:16:24 CEST 2024
* README update
patch c5d6314d751bbaca2ba4d24f4af0465af751ac40
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 28 00:28:22 CEST 2024
* utils: Fix when a filename has no extension
patch 5d8ba95f7bbee459c4a9c7a0d524894ae8836c83
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Apr 27 20:32:49 CEST 2024
* exclam: Add command indicator to error messages
patch b287d9843e3a4854c7ab0dc15558546ab7fd4c86
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 21 21:19:29 CEST 2024
* README update
patch d523591db91c8bf0ceb6ea65bac6358e5080f35a
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 21 20:59:04 CEST 2024
* exclam: !upl: Add support for HTTP/HTTPS URL for file upload
patch 18c87eca10fbc53fd73e8adf6b4da64dd4f24109
Author: E. Bosch <presidev@AT@gmail.com>
Date: Fri Apr 19 01:11:38 CEST 2024
* service: Disable by now the help for subcommands "archive" and "delete" from
command "dialog" as they are not really implemented yet
patch ca68ae9cfb315331dd08f8033ea2270e8a93e626
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 14 22:48:30 CEST 2024
* exclam: Add upload (!upl) command to upload files/media to chats/channels
Add "upload_dir" option to define the local directory to pick the files up,
by default "~/.cache/irgramd/upload"
patch fa015f3a5b1ea9fe2b6c068491481d57837ecdc5
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 14 02:13:34 CEST 2024
* telegram: Use directory ".cache/irgramd/media" instead of
".config/irgramd/media" by default (relative to home directory)
Added "cache_dir" option to override the default "~/.cache/irgramd"
patch 170328f9bde0160c50f56b98cbf51c8726ab4d69
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 7 19:48:33 CEST 2024
* telegram: Fix a corner case in forward handler when saved_from_peer is not present
patch 734e8c9f78627a6536e3ff52bd2db4879737830d
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 7 19:08:52 CEST 2024
* README update
patch 4bb5866f7a3278949670e153444b9b1db74344ad
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Apr 7 19:07:04 CEST 2024
* exclam: Add forward (!fwd) command to forward messages to other channels or chats
patch aa90a8fae2fb0ed855e80f146dc88cbf7069a2dd
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Dec 31 01:26:30 CET 2023
* README update
patch f7068578a6b2806e38c5c5bfc1c03b8e59a14455
Author: E. Bosch <presidev@AT@gmail.com>
Date: Wed Dec 20 01:50:56 CET 2023
* Fix logging system.
Remove logging options from tornado.log that were not working correctly in this setup
and use the new options from irgramd ("log_file" and "log_level").
Defer first logs to be included in log file opened later.
Improve option error handling.
patch 987c7436cd18763a950f5799ef5fac6e7cb127e4
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Dec 18 21:18:42 CET 2023
* telegram, utils: Replace invalid characters in filenames with number sequences instead of just removing.
This will prevent some filename collisions in corner cases.
patch 3ba3cb2a3d628290c5d421637e779869c00fba7d
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Dec 17 03:45:05 CET 2023
* telegram: Remove characters '#', '%' and '?' from filenames,
are not valid for static files in HTTP URLs.
patch 59776df1810cf643a8b7d70ab0502afde6a5c47f
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Dec 17 02:49:18 CET 2023
* telegram: Add compact message IDs to filenames of media,
this will prevent most of the possible collisions of media with the same filename
patch 50155e1ef1b508f47640d4f570da84407efac9cb
Author: E. Bosch <presidev@AT@gmail.com>
Date: Fri Dec 15 00:00:44 CET 2023
* telegram: Improve metadata shown for audio and recording/voice media,
and get filename from them
diff -rN -u old-irgramd/LICENSE new-irgramd/LICENSE
--- old-irgramd/LICENSE 2024-11-22 16:29:58.115958673 +0100
+++ new-irgramd/LICENSE 2024-11-22 16:29:58.123958660 +0100
@@ -1,7 +1,7 @@
MIT License
Copyright (c) 2019 Peter Bui <pbui@bx612.space>
-Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff -rN -u old-irgramd/README.md new-irgramd/README.md
--- old-irgramd/README.md 2024-11-22 16:29:58.115958673 +0100
+++ new-irgramd/README.md 2024-11-22 16:29:58.123958660 +0100
@@ -13,7 +13,7 @@
**irgramd was forked from [pbui/irtelegramd], was heavily modified and
currently is a project on its own**
-**irgramd is under active development in alpha state, though usable, several
+**irgramd is under active development, though usable, several
planned features are not implemented yet**
## How it works
@@ -45,12 +45,12 @@
- Channels, groups and private chats
- Users and channels mapped in IRC
- Messages (receive, send)
-- Media in messages (receive, download)
+- Media in messages (receive, download, upload)
- Replies (receive, send)
-- Forwards (receive)
+- Forwards (receive, send)
- Deletions (receive, do)
- Editions (receive, do)
-- Reactions (receive)
+- Reactions (receive, send, remove)
- Polls (receive, show)
- Actions [pin message, channel photo] (receive)
- Dialogs management
@@ -105,9 +105,9 @@
./irgramd
-In background (without logs):
+In background (with logs):
- ./irgramd --logging=none &
+ ./irgramd --log-file=irgramd.log &
## Notes
@@ -116,10 +116,16 @@
(e.g. in Linux be in the shadow group or equivalent). The dependency is
totally optional, if not used, the module pyPAM is not needed.
+## Inspired by
+
+- [telegramircd]
+- [ibotg]
+- [bitlbee]
+
## License
Copyright (c) 2019 Peter Bui <pbui@bx612.space>
-Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
Use of this source code is governed by a MIT style license that
can be found in the LICENSE file included in this project.
@@ -137,3 +143,6 @@
[aioconsole]: https://github.com/vxgmichel/aioconsole
[pyPAM]: https://packages.debian.org/bullseye/python3-pam
[BNC]: https://en.wikipedia.org/wiki/BNC_(software)
+[telegramircd]: https://github.com/prsai/telegramircd
+[ibotg]: https://github.com/prsai/ibotg
+[bitlbee]: https://www.bitlbee.org
diff -rN -u old-irgramd/emoji2emoticon.py new-irgramd/emoji2emoticon.py
--- old-irgramd/emoji2emoticon.py 2024-11-22 16:29:58.119958666 +0100
+++ new-irgramd/emoji2emoticon.py 2024-11-22 16:29:58.123958660 +0100
@@ -86,9 +86,13 @@
'\U0001f644': '"o o,"',
'\U0001f914': '":-L"',
'\U0001f92b': '":-o-m"',
- '\U0001f970': '":)e>"'
+ '\U0001f970': '":)e>"',
}
+emo_inv = { '-': None }
+for k in reversed(emo):
+ emo_inv[emo[k][1:-1]] = k
+
def replace_mult(line, emo):
for utf_emo in emo:
if utf_emo in line:
diff -rN -u old-irgramd/exclam.py new-irgramd/exclam.py
--- old-irgramd/exclam.py 2024-11-22 16:29:58.119958666 +0100
+++ new-irgramd/exclam.py 2024-11-22 16:29:58.123958660 +0100
@@ -1,22 +1,30 @@
# irgramd: IRC-Telegram gateway
# exclam.py: IRC exclamation command handlers
#
-# Copyright (c) 2023 E. Bosch <presidev@AT@gmail.com>
+# Copyright (c) 2023, 2024 E. Bosch <presidev@AT@gmail.com>
#
# Use of this source code is governed by a MIT style license that
# can be found in the LICENSE file included in this project.
-from telethon.errors.rpcerrorlist import MessageNotModifiedError, MessageAuthorRequiredError
+import os
+from telethon.tl.functions.messages import SendReactionRequest
+from telethon import types as tgty
+from telethon.errors.rpcerrorlist import MessageNotModifiedError, MessageAuthorRequiredError, ReactionInvalidError
from utils import command, HELP
+from emoji2emoticon import emo_inv
class exclam(command):
def __init__(self, telegram):
self.commands = \
{ # Command Handler Arguments Min Max Maxsplit
- '!re': (self.handle_command_re, 2, 2, 2),
- '!ed': (self.handle_command_ed, 2, 2, 2),
'!del': (self.handle_command_del, 1, 1, -1),
+ '!ed': (self.handle_command_ed, 2, 2, 2),
+ '!fwd': (self.handle_command_fwd, 2, 2, -1),
+ '!re': (self.handle_command_re, 2, 2, 2),
+ '!react': (self.handle_command_react, 2, 2, -1),
+ '!reupl': (self.handle_command_reupl, 2, 3, 3),
+ '!upl': (self.handle_command_upl, 1, 2, 2),
}
self.tg = telegram
self.irc = telegram.irc
@@ -34,7 +42,7 @@
async def check_msg(self, cid):
id = self.tg.mid.id_to_num_offset(self.tmp_telegram_id, cid)
- if id is None:
+ if id is None or id < -2147483648 or id > 2147483647:
chk_msg = None
else:
chk_msg = await self.tg.telegram_client.get_messages(entity=self.tmp_telegram_id, ids=id)
@@ -47,7 +55,7 @@
self.tmp_tg_msg = await self.tg.telegram_client.send_message(self.tmp_telegram_id, msg, reply_to=id)
reply = True
else:
- reply = ('Unknown message to reply',)
+ reply = ('!re: Unknown message to reply',)
else: # HELP.brief or HELP.desc (first line)
reply = (' !re Reply to a message',)
if help == HELP.desc: # rest of HELP.desc
@@ -55,7 +63,7 @@
(
' !re <compact_id> <message>',
'Reply with <message> to a message with <compact_id> on current',
- 'channel/chat.'
+ 'channel/chat.',
)
return reply
@@ -69,7 +77,7 @@
self.tmp_tg_msg = ed_msg
reply = True
except MessageAuthorRequiredError:
- reply = ('Not the author of the message to edit',)
+ reply = ('!ed: Not the author of the message to edit',)
else:
reply = True
else:
@@ -81,7 +89,7 @@
(
' !ed <compact_id> <new_message>',
'Edit a message with <compact_id> on current channel/chat,',
- '<new_message> replaces the current message.'
+ '<new_message> replaces the current message.',
)
return reply
@@ -91,7 +99,7 @@
if del_msg is not None:
deleted = await self.tg.telegram_client.delete_messages(self.tmp_telegram_id, del_msg)
if deleted[0].pts_count == 0:
- reply = ('Not possible to delete',)
+ reply = ('!del: Not possible to delete',)
else:
self.tmp_tg_msg = None
reply = None
@@ -106,3 +114,111 @@
'Delete a message with <compact_id> on current channel/chat'
)
return reply
+
+ async def handle_command_fwd(self, cid=None, chat=None, help=None):
+ if not help:
+ id, chk_msg = await self.check_msg(cid)
+ if chk_msg is not None:
+ async def send_fwd(tgt_ent, id):
+ from_ent = await self.tg.telegram_client.get_entity(self.tmp_telegram_id)
+ self.tmp_tg_msg = await self.tg.telegram_client.forward_messages(tgt_ent, id, from_ent)
+ return self.tmp_tg_msg
+
+ tgt = chat.lower()
+ if tgt in self.irc.iid_to_tid:
+ tgt_ent = await self.tg.telegram_client.get_entity(self.irc.iid_to_tid[tgt])
+ msg = await send_fwd(tgt_ent, id)
+ # echo fwded message
+ await self.tg.handle_telegram_message(event=None, message=msg)
+ reply = True
+ elif tgt in (u.irc_nick.lower() for u in self.irc.users.values() if u.stream):
+ tgt_ent = await self.tg.telegram_client.get_me()
+ await send_fwd(tgt_ent, id)
+ reply = True
+ else:
+ reply = ('!fwd: Unknown chat to forward',)
+ else:
+ reply = ('Unknown message to forward',)
+ else: # HELP.brief or HELP.desc (first line)
+ reply = (' !fwd Forward a message',)
+ if help == HELP.desc: # rest of HELP.desc
+ reply += \
+ (
+ ' !fwd <compact_id> <chat>',
+ 'Forward a message with <compact_id> to <chat> channel/chat.'
+ )
+ return reply
+
+ async def handle_command_upl(self, file=None, caption=None, help=None, re_id=None):
+ if not help:
+ try:
+ if file[:8] == 'https://' or file[:7] == 'http://':
+ file_path = file
+ else:
+ file_path = os.path.join(self.tg.telegram_upload_dir, file)
+ self.tmp_tg_msg = await self.tg.telegram_client.send_file(self.tmp_telegram_id, file_path, caption=caption, reply_to=re_id)
+ reply = True
+ except:
+ cmd = '!reupl' if re_id else '!upl'
+ reply = ('{}: Error uploading'.format(cmd),)
+ else: # HELP.brief or HELP.desc (first line)
+ reply = (' !upl Upload a file to current channel/chat',)
+ if help == HELP.desc: # rest of HELP.desc
+ reply += \
+ (
+ ' !upl <file name/URL> [<optional caption>]',
+ 'Upload the file referenced by <file name/URL> to current',
+ 'channel/chat, the file must be present in "upload"',
+ 'irgramd local directory or be an external HTTP/HTTPS URL.',
+ )
+ return reply
+
+ async def handle_command_reupl(self, cid=None, file=None, caption=None, help=None):
+ if not help:
+ id, chk_msg = await self.check_msg(cid)
+ if chk_msg is not None:
+ reply = await self.handle_command_upl(file, caption, re_id=id)
+ else:
+ reply = ('!reupl: Unknown message to reply',)
+ else: # HELP.brief or HELP.desc (first line)
+ reply = (' !reupl Reply to a message with an upload',)
+ if help == HELP.desc: # rest of HELP.desc
+ reply += \
+ (
+ ' !reupl <compact_id> <file name/URL> [<optional caption>]',
+ 'Reply with the upload of <file name/URL> to a message with',
+ '<compact_id> on current channel/chat. The file must be',
+ 'present in "upload" irgramd local directory or be an external',
+ 'HTTP/HTTPS URL.',
+ )
+ return reply
+
+ async def handle_command_react(self, cid=None, act=None, help=None):
+ if not help:
+ id, chk_msg = await self.check_msg(cid)
+ if chk_msg is not None:
+ if act in emo_inv:
+ utf8_emo = emo_inv[act]
+ reaction = [ tgty.ReactionEmoji(emoticon=utf8_emo) ] if utf8_emo else None
+ try:
+ update = await self.tg.telegram_client(SendReactionRequest(self.tmp_telegram_id, id, reaction=reaction))
+ except ReactionInvalidError:
+ reply = ('!react: Reaction not allowed',)
+ else:
+ self.tmp_tg_msg = update.updates[0].message
+ reply = True
+ else:
+ reply = ('!react: Unknown reaction',)
+ else:
+ reply = ('!react: Unknown message to react',)
+ else: # HELP.brief or HELP.desc (first line)
+ reply = (' !react React to a message',)
+ if help == HELP.desc: # rest of HELP.desc
+ reply += \
+ (
+ ' !react <compact_id> <emoticon reaction>|-',
+ 'React with <emoticon reaction> to a message with <compact_id>,',
+ 'irgramd will translate emoticon to closest emoji.',
+ 'Use - to remove a previous reaction.',
+ )
+ return reply
diff -rN -u old-irgramd/include.py new-irgramd/include.py
--- old-irgramd/include.py 2024-11-22 16:29:58.119958666 +0100
+++ new-irgramd/include.py 2024-11-22 16:29:58.127958653 +0100
@@ -8,6 +8,7 @@
# Constants
-VERSION = '0.1'
+VERSION = '0.2'
NICK_MAX_LENGTH = 20
-CHAN_MAX_LENGHT = 50
+CHAN_MAX_LENGTH = 50
+MAX_LINE = 400
diff -rN -u old-irgramd/irc.py new-irgramd/irc.py
--- old-irgramd/irc.py 2024-11-22 16:29:58.119958666 +0100
+++ new-irgramd/irc.py 2024-11-22 16:29:58.127958653 +0100
@@ -18,7 +18,7 @@
# Local modules
-from include import VERSION, CHAN_MAX_LENGHT, NICK_MAX_LENGTH
+from include import VERSION, CHAN_MAX_LENGTH, NICK_MAX_LENGTH, MAX_LINE
from irc_replies import irc_codes
from utils import chunks, set_replace, split_lines
from service import service
@@ -259,7 +259,7 @@
real_chan = self.get_realcaps_name(chan)
users_count = len(self.irc_channels[chan])
topic = await self.tg.get_channel_topic(chan, [None])
- await self.reply_code(user, 'RPL_LIST', (real_chan, users_count, topic))
+ await self.reply_code(user, 'RPL_LIST', (real_chan, users_count, topic[:MAX_LINE]))
await self.reply_code(user, 'RPL_LISTEND')
async def handle_irc_names(self, user, channels):
@@ -529,14 +529,10 @@
await self.reply_code(user, 'RPL_ENDOFMOTD')
async def send_isupport(self, user):
- await self.reply_code(user, 'RPL_ISUPPORT', (CHAN_MAX_LENGHT, NICK_MAX_LENGTH))
+ await self.reply_code(user, 'RPL_ISUPPORT', (CHAN_MAX_LENGTH, NICK_MAX_LENGTH))
async def send_help(self, user):
- for line in (
- 'Welcome to irgramd service',
- 'use /msg {} help'.format(self.service_user.irc_nick),
- 'to get help',
- ):
+ for line in self.service.initial_help():
await self.send_msg(self.service_user, None, line, user)
async def check_telegram_auth(self, user):
@@ -616,7 +612,7 @@
founder = list(self.irc_channels_founder[chan])[0]
else:
founder = self.service_user.irc_nick
- await self.reply_code(user, 'RPL_TOPIC', (channel, topic))
+ await self.reply_code(user, 'RPL_TOPIC', (channel, topic[:MAX_LINE]))
await self.reply_code(user, 'RPL_TOPICWHOTIME', (channel, founder, timestamp))
async def irc_namelist(self, user, channel):
diff -rN -u old-irgramd/irgramd new-irgramd/irgramd
--- old-irgramd/irgramd 2024-11-22 16:29:58.119958666 +0100
+++ new-irgramd/irgramd 2024-11-22 16:29:58.127958653 +0100
@@ -3,7 +3,7 @@
# irgramd: IRC-Telegram gateway - Main file
#
# Copyright (c) 2019 Peter Bui <pbui@bx612.space>
-# Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+# Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
#
# Use of this source code is governed by a MIT style license that
# can be found in the LICENSE file included in this project.
@@ -20,6 +20,7 @@
from irc import IRCHandler
from telegram import TelegramHandler
+from utils import parse_loglevel
# IRC Telegram Daemon
@@ -65,10 +66,20 @@
# Main Execution
if __name__ == '__main__':
- logger = logging.getLogger()
+ # Remove tornado.log options (ugly hacks but these must not be defined)
+ tornado.options.options.logging = None
+ tornado_log_options = tuple(x for x in tornado.options.options._options.keys() if x != 'help' and x != 'logging')
+ for opt in tornado_log_options:
+ del tornado.options.options._options[opt]
+ # and reuse "--logging" to document empty "--" ;)
+ tornado.options.options._options['logging'].help = 'Stop parsing options'
+ for att in ('name', 'metavar', 'group_name', 'default'):
+ setattr(tornado.options.options._options['logging'], att, '')
+ # Define irgramd options
tornado.options.define('api_hash', default=None, metavar='HASH', help='Telegram API Hash for your account (obtained from https://my.telegram.org/apps)')
tornado.options.define('api_id', type=int, default=None, metavar='ID', help='Telegram API ID for your account (obtained from https://my.telegram.org/apps)')
tornado.options.define('ask_code', default=False, help='Ask authentication code (sent by Telegram) in console instead of "code" service command in IRC')
+ tornado.options.define('cache_dir', default='~/.cache/irgramd', metavar='PATH', help='Cache directory where telegram media is saved by default')
tornado.options.define('char_in_encoding', default='utf-8', metavar='ENCODING', help='Character input encoding for IRC')
tornado.options.define('char_out_encoding', default='utf-8', metavar='ENCODING', help='Character output encoding for IRC')
tornado.options.define('config', default='irgramdrc', metavar='CONFIGFILE', help='Config file absolute or relative to `config_dir` (command line options override it)')
@@ -82,7 +93,9 @@
tornado.options.define('irc_nicks', type=str, multiple=True, metavar='nick,..', help='List of nicks allowed for IRC, if `pam` and optionally `pam_group` are set, PAM authentication will be used instead')
tornado.options.define('irc_password', default='', metavar='PASSWORD', help='Password for IRC authentication, if `pam` is set, PAM authentication will be used instead')
tornado.options.define('irc_port', type=int, default=None, metavar='PORT', help='Port to listen on for IRC. (default 6667, default with TLS 6697)')
- tornado.options.define('media_dir', default=None, metavar='PATH', help='Directory where Telegram media files are downloaded, default "media" in `config_dir`')
+ tornado.options.define('log_file', default=None, metavar='PATH', help='File where logs are appended, if not set will be stderr')
+ tornado.options.define('log_level', default='INFO', metavar='DEBUG|INFO|WARNING|ERROR|CRITICAL|NONE', help='The log level (and any higher to it) that will be logged')
+ tornado.options.define('media_dir', default=None, metavar='PATH', help='Directory where Telegram media files are downloaded, default "media" in `cache_dir`')
tornado.options.define('media_url', default=None, metavar='BASE_URL', help='Base URL for media files, should be configured in the external (to irgramd) webserver')
tornado.options.define('pam', default=False, help='Use PAM for IRC authentication, if not set you should set `irc_password`')
tornado.options.define('pam_group', default=None, metavar='GROUP', help='Unix group allowed if `pam` enabled, if empty any user is allowed')
@@ -97,27 +110,52 @@
tornado.options.define('tls', default=False, help='Use TLS/SSL encrypted connection for IRC server')
tornado.options.define('tls_cert', default=None, metavar='CERTFILE', help='IRC server certificate chain for TLS/SSL, also can contain private key if not defined with `tls_key`')
tornado.options.define('tls_key', default=None, metavar='KEYFILE', help='IRC server private key for TLS/SSL')
- # parse cmd line first time to get --config and --config_dir
- tornado.options.parse_command_line()
+ tornado.options.define('upload_dir', default=None, metavar='PATH', help='Directory where files to upload are picked up, default "upload" in `cache_dir`')
+ try:
+ # parse cmd line first time to get --config and --config_dir
+ tornado.options.parse_command_line()
+ except Exception as exc:
+ print(exc)
+ exit(1)
config_file = os.path.expanduser(tornado.options.options.config)
config_dir = os.path.expanduser(tornado.options.options.config_dir)
if not os.path.exists(config_dir):
os.makedirs(config_dir)
- logger.info('Configuration Directory: %s', config_dir)
+ defered_logs = [(logging.INFO, 'Configuration Directory: %s', config_dir)]
if not os.path.isabs(config_file):
config_file = os.path.join(config_dir, config_file)
if os.path.isfile(config_file):
- logger.info('Using configuration file: %s', config_file)
- tornado.options.parse_config_file(config_file)
+ defered_logs.append((logging.INFO, 'Using configuration file: %s', config_file))
+ try:
+ tornado.options.parse_config_file(config_file)
+ except Exception as exc:
+ print(exc)
+ exit(1)
else:
- logger.warning('Configuration file not present, using only command line options and defaults')
+ defered_logs.append((logging.WARNING, 'Configuration file not present, using only command line options and defaults'))
# parse cmd line second time to override file options
tornado.options.parse_command_line()
options = tornado.options.options.as_dict()
options['config_dir'] = config_dir
+ # configure logging
+ loglevel = parse_loglevel(options['log_level'])
+ if loglevel == False:
+ print("Option 'log_level' requires one of these values: {}".format(tornado.options.options._options['log-level'].metavar))
+ exit(1)
+ logger_formats = { 'datefmt':'%Y-%m-%d %H:%M:%S', 'format':'[%(levelname).1s %(asctime)s %(module)s:%(lineno)d] %(message)s' }
+ logger = logging.getLogger()
+ if options['log_file']:
+ logging.basicConfig(filename=options['log_file'], level=loglevel, **logger_formats)
+ else:
+ logging.basicConfig(level=loglevel, **logger_formats)
+
+ for log in defered_logs:
+ logger.log(*log)
+
+ # main loop
irc_server = IRCTelegramd(logger, options)
loop = asyncio.new_event_loop()
loop.run_until_complete(irc_server.run(options))
diff -rN -u old-irgramd/service.py new-irgramd/service.py
--- old-irgramd/service.py 2024-11-22 16:29:58.119958666 +0100
+++ new-irgramd/service.py 2024-11-22 16:29:58.127958653 +0100
@@ -25,6 +25,14 @@
self.irc = telegram.irc
self.tmp_ircnick = None
+ def initial_help(self):
+ return (
+ 'Welcome to irgramd service',
+ 'use /msg {} help'.format(self.irc.service_user.irc_nick),
+ 'or equivalent in your IRC client',
+ 'to get help',
+ )
+
async def handle_command_code(self, code=None, help=None):
if not help:
if self.ask_code:
@@ -91,8 +99,8 @@
' dialog <subcommand> [id]',
'Manage conversations (dialogs) established in Telegram, the',
'following subcommands are available:',
- ' archive <id> Archive the dialog specified by id',
- ' delete <id> Delete the dialog specified by id',
+# ' archive <id> Archive the dialog specified by id',
+# ' delete <id> Delete the dialog specified by id',
' list Show all dialogs',
)
return reply
@@ -249,7 +257,7 @@
(
' mark_read <peer>',
'Mark all messages on <peer> (channel or user) as read, this also will',
- 'reset the number of mentions to you on <peer>.'
+ 'reset the number of mentions to you on <peer>.',
)
return reply
diff -rN -u old-irgramd/telegram.py new-irgramd/telegram.py
--- old-irgramd/telegram.py 2024-11-22 16:29:58.123958660 +0100
+++ new-irgramd/telegram.py 2024-11-22 16:29:58.127958653 +0100
@@ -2,34 +2,35 @@
# telegram.py: Interface to Telethon Telegram library
#
# Copyright (c) 2019 Peter Bui <pbui@bx612.space>
-# Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+# Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
#
# Use of this source code is governed by a MIT style license that
# can be found in the LICENSE file included in this project.
import logging
import os
-import datetime
import re
import aioconsole
import asyncio
import collections
import telethon
from telethon import types as tgty, utils as tgutils
-from telethon.tl.functions.messages import GetMessagesReactionsRequest
+from telethon.tl.functions.messages import GetMessagesReactionsRequest, GetFullChatRequest
+from telethon.tl.functions.channels import GetFullChannelRequest
# Local modules
-from include import CHAN_MAX_LENGHT, NICK_MAX_LENGTH
+from include import CHAN_MAX_LENGTH, NICK_MAX_LENGTH
from irc import IRCUser
-from utils import sanitize_filename, is_url_equiv, extract_url, get_human_size, get_human_duration, get_highlighted, fix_braces, format_timestamp
+from utils import sanitize_filename, add_filename, is_url_equiv, extract_url, get_human_size, get_human_duration
+from utils import get_highlighted, fix_braces, format_timestamp, pretty, current_date
import emoji2emoticon as e
# Test IP table
TEST_IPS = { 1: '149.154.175.10',
2: '149.154.167.40',
- 3: '149.154.175.117'
+ 3: '149.154.175.117',
}
# Telegram
@@ -38,10 +39,12 @@
def __init__(self, irc, settings):
self.logger = logging.getLogger()
self.config_dir = settings['config_dir']
+ self.cache_dir = settings['cache_dir']
self.download = settings['download_media']
self.notice_size = settings['download_notice'] * 1048576
self.media_dir = settings['media_dir']
self.media_url = settings['media_url']
+ self.upload_dir = settings['upload_dir']
self.api_id = settings['api_id']
self.api_hash = settings['api_hash']
self.phone = settings['phone']
@@ -66,16 +69,24 @@
self.webpending = {}
self.refwd_me = False
self.cache = collections.OrderedDict()
+ self.volatile_cache = collections.OrderedDict()
+ self.prev_id = {}
self.sorted_len_usernames = []
+ self.last_reaction = None
# Set event to be waited by irc.check_telegram_auth()
self.auth_checked = asyncio.Event()
async def initialize_telegram(self):
# Setup media folder
- self.telegram_media_dir = self.media_dir or os.path.join(self.config_dir, 'media')
+ self.telegram_media_dir = os.path.expanduser(self.media_dir or os.path.join(self.cache_dir, 'media'))
if not os.path.exists(self.telegram_media_dir):
os.makedirs(self.telegram_media_dir)
+ # Setup upload folder
+ self.telegram_upload_dir = os.path.expanduser(self.upload_dir or os.path.join(self.cache_dir, 'upload'))
+ if not os.path.exists(self.telegram_upload_dir):
+ os.makedirs(self.telegram_upload_dir)
+
# Setup session folder
self.telegram_session_dir = os.path.join(self.config_dir, 'session')
if not os.path.exists(self.telegram_session_dir):
@@ -95,10 +106,10 @@
# Register Telegram callbacks
callbacks = (
(self.handle_telegram_message , telethon.events.NewMessage),
- (self.handle_raw, telethon.events.Raw),
+ (self.handle_raw , telethon.events.Raw),
(self.handle_telegram_chat_action, telethon.events.ChatAction),
(self.handle_telegram_deleted , telethon.events.MessageDeleted),
- (self.handle_telegram_edited, telethon.events.MessageEdited),
+ (self.handle_telegram_edited , telethon.events.MessageEdited),
)
for handler, event in callbacks:
self.telegram_client.add_event_handler(handler, event)
@@ -171,10 +182,12 @@
if not user.is_self:
self.irc.irc_channels[chan].add(user_nick)
# Add admin users as ops in irc
- if isinstance(user.participant, tgty.ChatParticipantAdmin):
+ if isinstance(user.participant, tgty.ChatParticipantAdmin) or \
+ isinstance(user.participant, tgty.ChannelParticipantAdmin):
self.irc.irc_channels_ops[chan].add(user_nick)
# Add creator users as founders in irc
- elif isinstance(user.participant, tgty.ChatParticipantCreator):
+ elif isinstance(user.participant, tgty.ChatParticipantCreator) or \
+ isinstance(user.participant, tgty.ChannelParticipantCreator):
self.irc.irc_channels_founder[chan].add(user_nick)
def get_telegram_nick(self, user):
@@ -279,7 +292,7 @@
idle = 0
elif isinstance(user.status,tgty.UserStatusOffline):
last = user.status.was_online
- current = datetime.datetime.now(datetime.timezone.utc)
+ current = current_date()
idle = int((current - last).total_seconds())
elif isinstance(user.status,tgty.UserStatusLastWeek):
idle = 604800
@@ -297,8 +310,16 @@
else:
entity = await self.telegram_client.get_entity(tid)
entity_cache[0] = entity
+ if isinstance(entity, tgty.Channel):
+ full = await self.telegram_client(GetFullChannelRequest(channel=entity))
+ elif isinstance(entity, tgty.Chat):
+ full = await self.telegram_client(GetFullChatRequest(chat_id=tid))
+ else:
+ return ''
entity_type = self.get_entity_type(entity, format='long')
- return 'Telegram ' + entity_type + ' ' + str(tid) + ': ' + entity.title
+ topic = full.full_chat.about
+ sep = ': ' if topic else ''
+ return entity_type + sep + topic
async def get_channel_creation(self, channel, entity_cache):
tid = self.get_tid(channel)
@@ -380,7 +401,9 @@
)
async def get_reactions(m):
react = await self.telegram_client(GetMessagesReactionsRequest(m.peer_id, id=[m.id]))
- return react.updates[0].reactions.recent_reactions
+ updates = react.updates
+ r = next((x for x in updates if type(x) is tgty.UpdateMessageReactions), None)
+ return r.reactions.recent_reactions if r else None
react = None
if msg.reactions is None:
@@ -390,7 +413,7 @@
case = 'edition'
else:
case = 'react-del'
- elif react := next((x for x in reactions if x.date == msg.edit_date), None):
+ elif react := max(reactions, key=lambda y: y.date):
case = 'react-add'
else:
if msg_edited(msg):
@@ -401,17 +424,36 @@
return case, react
def to_cache(self, id, mid, message, proc_message, user, chan, media):
- if len(self.cache) >= 10000:
- self.cache.popitem(last=False)
+ self.limit_cache(self.cache)
self.cache[id] = {
'mid': mid,
'text': message,
'rendered_text': proc_message,
'user': user,
'channel': chan,
- 'media': media
+ 'media': media,
}
+ def to_volatile_cache(self, prev_id, id, ev, user, chan, date):
+ if chan in prev_id:
+ prid = prev_id[chan] if chan else prev_id[user]
+ self.limit_cache(self.volatile_cache)
+ elem = {
+ 'id': id,
+ 'rendered_event': ev,
+ 'user': user,
+ 'channel': chan,
+ 'date': date,
+ }
+ if prid not in self.volatile_cache:
+ self.volatile_cache[prid] = [elem]
+ else:
+ self.volatile_cache[prid].append(elem)
+
+ def limit_cache(self, cache):
+ if len(cache) >= 10000:
+ cache.popitem(last=False)
+
def replace_mentions(self, text, me_nick='', received=True):
# For received replace @mention to ~mention~
# For sent replace mention: to @mention
@@ -473,8 +515,27 @@
self.sorted_len_usernames.append(username)
self.sorted_len_usernames.sort(key=lambda k: len(k), reverse=True)
+ def format_reaction(self, msg, message_rendered, edition_case, reaction):
+ react_quote_len = self.quote_len * 2
+ if len(message_rendered) > react_quote_len:
+ text_old = '{}...'.format(message_rendered[:react_quote_len])
+ text_old = fix_braces(text_old)
+ else:
+ text_old = message_rendered
+
+ if edition_case == 'react-add':
+ user = self.get_irc_user_from_telegram(reaction.peer_id.user_id)
+ emoji = reaction.reaction.emoticon
+ react_action = '+'
+ react_icon = e.emo[emoji] if emoji in e.emo else emoji
+ elif edition_case == 'react-del':
+ user = self.get_irc_user_from_telegram(msg.sender_id)
+ react_action = '-'
+ react_icon = ''
+ return text_old, '{}{}'.format(react_action, react_icon), user
+
async def handle_telegram_edited(self, event):
- self.logger.debug('Handling Telegram Message Edited: %s', event)
+ self.logger.debug('Handling Telegram Message Edited: %s', pretty(event))
id = event.message.id
mid = self.mid.num_to_id_offset(event.message.peer_id, id)
@@ -506,32 +567,45 @@
# Reactions
else:
+ if reaction:
+ if self.last_reaction == reaction.date:
+ return
+ self.last_reaction = reaction.date
action = 'React'
- if len(message_rendered) > self.quote_len:
- text_old = '{}...'.format(message_rendered[:self.quote_len])
- text_old = fix_braces(text_old)
- else:
- text_old = message_rendered
-
- if edition_case == 'react-add':
- user = self.get_irc_user_from_telegram(reaction.peer_id.user_id)
- emoji = reaction.reaction.emoticon
- react_action = '+'
- react_icon = e.emo[emoji] if emoji in e.emo else emoji
- elif edition_case == 'react-del':
- user = self.get_irc_user_from_telegram(event.sender_id)
- react_action = '-'
- react_icon = ''
- edition_react = '{}{}'.format(react_action, react_icon)
+ text_old, edition_react, user = self.format_reaction(event.message, message_rendered, edition_case, reaction)
text = '|{} {}| {}'.format(action, text_old, edition_react)
chan = await self.relay_telegram_message(event, user, text)
self.to_cache(id, mid, message, message_rendered, user, chan, event.message.media)
+ self.to_volatile_cache(self.prev_id, id, text, user, chan, current_date())
+
+ async def handle_next_reaction(self, event):
+ self.logger.debug('Handling Telegram Next Reaction (2nd, 3rd, ...): %s', pretty(event))
+
+ reactions = event.reactions.recent_reactions
+ react = max(reactions, key=lambda y: y.date) if reactions else None
+
+ if react and self.last_reaction != react.date:
+ self.last_reaction = react.date
+ id = event.msg_id
+ msg = await self.telegram_client.get_messages(entity=event.peer, ids=id)
+ mid = self.mid.num_to_id_offset(msg.peer_id, id)
+ message = self.filters(msg.message)
+ message_rendered = await self.render_text(msg, mid, upd_to_webpend=None)
+
+ text_old, edition_react, user = self.format_reaction(msg, message_rendered, edition_case='react-add', reaction=react)
+
+ text = '|React {}| {}'.format(text_old, edition_react)
+
+ chan = await self.relay_telegram_message(msg, user, text)
+
+ self.to_cache(id, mid, message, message_rendered, user, chan, msg.media)
+ self.to_volatile_cache(self.prev_id, id, text, user, chan, current_date())
async def handle_telegram_deleted(self, event):
- self.logger.debug('Handling Telegram Message Deleted: %s', event)
+ self.logger.debug('Handling Telegram Message Deleted: %s', pretty(event))
for deleted_id in event.original_update.messages:
if deleted_id in self.cache:
@@ -540,20 +614,24 @@
user = self.cache[deleted_id]['user']
chan = self.cache[deleted_id]['channel']
await self.relay_telegram_message(message=None, user=user, text=text, channel=chan)
+ self.to_volatile_cache(self.prev_id, deleted_id, text, user, chan, current_date())
else:
text = 'Message id {} deleted not in cache'.format(deleted_id)
await self.relay_telegram_private_message(self.irc.service_user, text)
async def handle_raw(self, update):
- self.logger.debug('Handling Telegram Raw Event: %s', update)
+ self.logger.debug('Handling Telegram Raw Event: %s', pretty(update))
if isinstance(update, tgty.UpdateWebPage) and isinstance(update.webpage, tgty.WebPage):
message = self.webpending.pop(update.webpage.id, None)
if message:
await self.handle_telegram_message(event=None, message=message, upd_to_webpend=update.webpage)
+ elif isinstance(update, tgty.UpdateMessageReactions):
+ await self.handle_next_reaction(update)
+
async def handle_telegram_message(self, event, message=None, upd_to_webpend=None, history=False):
- self.logger.debug('Handling Telegram Message: %s', event or message)
+ self.logger.debug('Handling Telegram Message: %s', pretty(event or message))
msg = event.message if event else message
@@ -562,21 +640,24 @@
text = await self.render_text(msg, mid, upd_to_webpend, user)
text_send = self.set_history_timestamp(text, history, msg.date, msg.action)
chan = await self.relay_telegram_message(msg, user, text_send)
+ await self.history_search_volatile(history, msg.id)
self.to_cache(msg.id, mid, msg.message, text, user, chan, msg.media)
+ peer = chan if chan else user
+ self.prev_id[peer] = msg.id
self.refwd_me = False
async def render_text(self, message, mid, upd_to_webpend, user=None):
if upd_to_webpend:
- text = await self.handle_webpage(upd_to_webpend, message)
+ text = await self.handle_webpage(upd_to_webpend, message, mid)
elif message.media:
text = await self.handle_telegram_media(message, user, mid)
else:
text = message.message
if message.action:
- final_text = await self.handle_telegram_action(message)
+ final_text = await self.handle_telegram_action(message, mid)
return final_text
elif message.is_reply:
refwd_text = await self.handle_telegram_reply(message)
@@ -602,6 +683,17 @@
res = text
return res
+ async def history_search_volatile(self, history, id):
+ if history:
+ if id in self.volatile_cache:
+ for item in self.volatile_cache[id]:
+ user = item['user']
+ text = item['rendered_event']
+ chan = item['channel']
+ date = item['date']
+ text_send = self.set_history_timestamp(text, history=True, date=date, action=False)
+ await self.relay_telegram_message(None, user, text_send, chan)
+
async def relay_telegram_message(self, message, user, text, channel=None):
private = (message and message.is_private) or (not message and not channel)
action = (message and message.action)
@@ -613,7 +705,7 @@
return chan
async def relay_telegram_private_message(self, user, message, action=None):
- self.logger.debug('Handling Telegram Private Message: %s, %s', user, message)
+ self.logger.debug('Relaying Telegram Private Message: %s, %s', user, message)
if action:
await self.irc.send_action(user, None, message)
@@ -621,14 +713,14 @@
await self.irc.send_msg(user, None, message)
async def relay_telegram_channel_message(self, message, user, text, channel, action):
- self.logger.debug('Handling Telegram Channel Message: %s', message or text)
-
if message:
entity = await message.get_chat()
chan = await self.get_irc_channel_from_telegram_id(message.chat_id, entity)
else:
chan = channel
+ self.logger.debug('Relaying Telegram Channel Message: %s, %s', chan, text)
+
if action:
await self.irc.send_action(user, chan, text)
else:
@@ -637,7 +729,7 @@
return chan
async def handle_telegram_chat_action(self, event):
- self.logger.debug('Handling Telegram Chat Action: %s', event)
+ self.logger.debug('Handling Telegram Chat Action: %s', pretty(event))
try:
tid = event.action_message.to_id.channel_id
@@ -669,14 +761,14 @@
self.irc.iid_to_tid[channel] = chat.id
await self.irc.join_irc_channel(self.irc.irc_nick, channel, full_join=True)
- async def handle_telegram_action(self, message):
+ async def handle_telegram_action(self, message, mid):
if isinstance(message.action, tgty.MessageActionPinMessage):
replied = await message.get_reply_message()
cid = self.mid.num_to_id_offset(replied.peer_id, replied.id)
action_text = 'has pinned message [{}]'.format(cid)
elif isinstance(message.action, tgty.MessageActionChatEditPhoto):
_, media_type = self.scan_photo_attributes(message.action.photo)
- photo_url = await self.download_telegram_media(message)
+ photo_url = await self.download_telegram_media(message, mid)
action_text = 'has changed chat [{}] {}'.format(media_type, photo_url)
else:
action_text = ''
@@ -686,18 +778,33 @@
space = ' '
trunc = ''
replied = await message.get_reply_message()
- replied_msg = replied.message
- cid = self.mid.num_to_id_offset(replied.peer_id, replied.id)
+ if replied:
+ replied_msg = replied.message
+ cid = self.mid.num_to_id_offset(replied.peer_id, replied.id)
+ replied_user = self.get_irc_user_from_telegram(replied.sender_id)
+ else:
+ replied_id = message.reply_to.reply_to_msg_id
+ cid = self.mid.num_to_id_offset(message.peer_id, replied_id)
+ if replied_id in self.cache:
+ text = self.cache[replied_id]['text']
+ replied_user = self.cache[replied_id]['user']
+ sp = ' '
+ else:
+ text = ''
+ replied_user = ''
+ sp = ''
+ replied_msg = '|Deleted|{}{}'.format(sp, text)
if not replied_msg:
replied_msg = ''
space = ''
elif len(replied_msg) > self.quote_len:
replied_msg = replied_msg[:self.quote_len]
trunc = '...'
- replied_user = self.get_irc_user_from_telegram(replied.sender_id)
if replied_user is None:
replied_nick = '{}'
self.refwd_me = True
+ elif replied_user == '':
+ replied_nick = ''
else:
replied_nick = replied_user.irc_nick
@@ -712,8 +819,8 @@
secondary_name = saved_peer_name
else:
# if it's from me I want to know who was the destination of a message (user)
- if self.refwd_me:
- secondary_name = self.get_irc_user_from_telegram(message.fwd_from.saved_from_peer.user_id).irc_nick
+ if self.refwd_me and (saved_from_peer := message.fwd_from.saved_from_peer) is not None:
+ secondary_name = self.get_irc_user_from_telegram(saved_from_peer.user_id).irc_nick
else:
secondary_name = ''
space2 = ''
@@ -728,23 +835,23 @@
filename = None
def scan_doc_attributes(document):
- attrib_file = attrib_video = filename = None
+ attrib_file = attrib_av = filename = None
size = document.size
h_size = get_human_size(size)
for x in document.attributes:
- if isinstance(x, tgty.DocumentAttributeVideo):
- attrib_video = x
+ if isinstance(x, tgty.DocumentAttributeVideo) or isinstance(x, tgty.DocumentAttributeAudio):
+ attrib_av = x
if isinstance(x, tgty.DocumentAttributeFilename):
attrib_file = x
filename = attrib_file.file_name if attrib_file else None
- return size, h_size, attrib_video, filename
+ return size, h_size, attrib_av, filename
if isinstance(message.media, tgty.MessageMediaWebPage):
to_download = False
if isinstance(message.media.webpage, tgty.WebPage):
# web
- return await self.handle_webpage(message.media.webpage, message)
+ return await self.handle_webpage(message.media.webpage, message, mid)
elif isinstance(message.media.webpage, tgty.WebPagePending):
media_type = 'webpending'
media_url_or_data = message.message
@@ -756,8 +863,17 @@
caption = ''
elif message.photo:
size, media_type = self.scan_photo_attributes(message.media.photo)
- elif message.audio: media_type = 'audio'
- elif message.voice: media_type = 'rec'
+ elif message.audio:
+ size, h_size, attrib_audio, filename = scan_doc_attributes(message.media.document)
+ dur = get_human_duration(attrib_audio.duration) if attrib_audio else ''
+ per = attrib_audio.performer or ''
+ tit = attrib_audio.title or ''
+ theme = ',{}/{}'.format(per, tit) if per or tit else ''
+ media_type = 'audio:{},{}{}'.format(h_size, dur, theme)
+ elif message.voice:
+ size, _, attrib_audio, filename = scan_doc_attributes(message.media.document)
+ dur = get_human_duration(attrib_audio.duration) if attrib_audio else ''
+ media_type = 'rec:{}'.format(dur)
elif message.video:
size, h_size, attrib_video, filename = scan_doc_attributes(message.media.document)
dur = get_human_duration(attrib_video.duration) if attrib_video else ''
@@ -822,7 +938,7 @@
if to_download:
relay_attr = (message, user, mid, media_type)
- media_url_or_data = await self.download_telegram_media(message, filename, size, relay_attr)
+ media_url_or_data = await self.download_telegram_media(message, mid, filename, size, relay_attr)
return self.format_media(media_type, media_url_or_data, caption)
@@ -845,9 +961,9 @@
target_mine = ''
return target_mine
- async def handle_webpage(self, webpage, message):
+ async def handle_webpage(self, webpage, message, mid):
media_type = 'web'
- logo = await self.download_telegram_media(message)
+ logo = await self.download_telegram_media(message, mid)
if is_url_equiv(webpage.url, webpage.display_url):
url_data = webpage.url
else:
@@ -905,11 +1021,12 @@
return size, media_type
- async def download_telegram_media(self, message, filename=None, size=0, relay_attr=None):
+ async def download_telegram_media(self, message, mid, filename=None, size=0, relay_attr=None):
if not self.download:
return ''
if filename:
- new_file = sanitize_filename(filename)
+ idd_file = add_filename(filename, mid)
+ new_file = sanitize_filename(idd_file)
new_path = os.path.join(self.telegram_media_dir, new_file)
if os.path.exists(new_path):
local_path = new_path
@@ -922,7 +1039,9 @@
local_path = await message.download_media(self.telegram_media_dir)
if not local_path: return ''
filetype = os.path.splitext(local_path)[1]
- new_file = str(self.media_cn) + filetype
+ gen_file = str(self.media_cn) + filetype
+ idd_file = add_filename(gen_file, mid)
+ new_file = sanitize_filename(idd_file)
self.media_cn += 1
new_path = os.path.join(self.telegram_media_dir, new_file)
diff -rN -u old-irgramd/utils.py new-irgramd/utils.py
--- old-irgramd/utils.py 2024-11-22 16:29:58.123958660 +0100
+++ new-irgramd/utils.py 2024-11-22 16:29:58.131958647 +0100
@@ -2,7 +2,7 @@
# utils.py: Helper functions
#
# Copyright (c) 2019 Peter Bui <pbui@bx612.space>
-# Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+# Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
#
# Use of this source code is governed by a MIT style license that
# can be found in the LICENSE file included in this project.
@@ -13,12 +13,15 @@
import datetime
import zoneinfo
import difflib
+import logging
# Constants
-FILENAME_INVALID_CHARS = re.compile('[/{}<>()"\'\\|&]')
+FILENAME_INVALID_CHARS = re.compile('[/{}<>()"\'\\|&#%?]')
SIMPLE_URL = re.compile('http(|s)://[^ ]+')
+from include import MAX_LINE
+
# Utilities
class command:
@@ -42,6 +45,9 @@
desc = 1
brief = 2
+class LOGL:
+ debug = False
+
def chunks(iterable, n, fillvalue=None):
''' Return iterable consisting of a sequence of n-length chunks '''
args = [iter(iterable)] * n
@@ -57,9 +63,8 @@
return (x + mark if n != length else x for n, x in enumerate(items, start=1))
def split_lines(message):
- MAX = 400
messages_limited = []
- wr = textwrap.TextWrapper(width=MAX)
+ wr = textwrap.TextWrapper(width=MAX_LINE)
# Split when Telegram original message has breaks
messages = message.splitlines()
@@ -81,7 +86,24 @@
return messages_limited
def sanitize_filename(fn):
- return FILENAME_INVALID_CHARS.sub('', fn).strip('-').replace(' ','_')
+ cn = str(sanitize_filename.cn)
+ new_fn, ns = FILENAME_INVALID_CHARS.subn(cn, fn)
+ if ns:
+ sanitize_filename.cn += 1
+ return new_fn.strip('-').replace(' ','_')
+sanitize_filename.cn = 0
+
+def add_filename(filename, add):
+ if add:
+ aux = filename.rsplit('.', 1)
+ name = aux[0]
+ try:
+ ext = aux[1]
+ except:
+ ext = ''
+ return '{}-{}.{}'.format(name, add, ext)
+ else:
+ return filename
def remove_slash(url):
return url[:-1] if url[-1:] == '/' else url
@@ -130,11 +152,11 @@
if h > 0: res = str(h) + 'h'
if m > 0: res += str(m) + 'm'
- if s > 0: res += str(s) + 's'
+ if s > 0 or duration < 60: res += str(s) + 's'
return res
def compact_date(date, tz):
- delta = datetime.datetime.now(datetime.timezone.utc) - date
+ delta = current_date() - date
date_local = date.astimezone(zoneinfo.ZoneInfo(tz))
if delta.days < 1:
@@ -146,6 +168,9 @@
return compact_date
+def current_date():
+ return datetime.datetime.now(datetime.timezone.utc)
+
def get_highlighted(a, b):
awl = len(a.split())
bwl = len(b.split())
@@ -200,3 +225,18 @@
def format_timestamp(format, tz, date):
date_local = date.astimezone(zoneinfo.ZoneInfo(tz))
return date_local.strftime(format)
+
+def parse_loglevel(level):
+ levelu = level.upper()
+ if levelu == 'DEBUG':
+ LOGL.debug = True
+ if levelu == 'NONE':
+ l = None
+ elif levelu in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'):
+ l = getattr(logging, levelu)
+ else:
+ l = False
+ return l
+
+def pretty(object):
+ return object.stringify() if LOGL.debug and object else object