telegram: Refactor forward handle and related functions --> to head
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
patch 17b3e7fb0ee1d0153c40740e2934a2030bd9b0ba
Author: E. Bosch <presidev@AT@gmail.com>
Date: Thu Dec 7 21:16:12 CET 2023
* README update
patch ec320fd2c408d1239ba7e980e42318e1ab50bb21
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Dec 3 00:12:34 CET 2023
* Correct OpenStreetMap URL in irgramdrc.sample
patch 3976770a9424b2085ba1260328b3adb5ce6b1ebe
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Dec 2 20:41:44 CET 2023
* telegram: Add target nick in private messages sent from the self user
from another Telegram client
patch d9d472cb672a8172ee1d54241f20899754fb0640
Author: E. Bosch <presidev@AT@gmail.com>
Date: Tue Nov 28 23:53:52 CET 2023
* README update
patch 9c73d6fcff003a7d9b4f5f90df4f6dc71d84ae9d
Author: E. Bosch <presidev@AT@gmail.com>
Date: Tue Nov 28 22:38:01 CET 2023
* service: Add absolute ID as argument to get command
patch c36c361baf23612e31780ba510278ceef611b7ec
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Nov 26 23:13:52 CET 2023
* telegram: Add support for the change of channel photo action
including download of the new photo
mapped to CTCP action on IRC
patch 9c00648eaf7c048826dc4fe6bdbd509eabe57f47
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Nov 26 20:07:44 CET 2023
* telegram: Add support for pin message action
irc: Add support for sending CTCP actions (me) to map actions from Telegram
patch 583b2fd653aa6786aba38cb9a00c6e990f3a1d11
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Nov 19 00:54:47 CET 2023
* telegram: Change add symbol in editions from "_" to "+"
patch 8167f7dfd2712a9a103504fe7763e2a497adf723
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Nov 19 00:43:04 CET 2023
* telegram: Refactor download media code.
If named media files already downloaded, don't try to download again
patch 24a50b886f4cef2e1da003b74921982f5da0941c
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Oct 16 00:24:53 CEST 2023
* telegram: Add option "download_notice" to show the start of a big download
patch 1fb88a2962a05a4a46ccec54dc3ae3b65f73ab78
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Oct 15 22:06:49 CEST 2023
* telegram: Add option "download_media" to control if media files must be downloaded
patch a62e1a9973921a97198519dd5a74e1a4472b9364
Author: E. Bosch <presidev@AT@gmail.com>
Date: Wed Oct 11 00:35:28 CEST 2023
* telegram: Support for showing question and options of polls
patch 400207de8aa3ca0b2dfc8ff1eeaafeddc8221070
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Sep 17 20:57:11 CEST 2023
* README update
patch 2ab08fbae8ec48e123e86a498c287e7afcd173e1
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Jul 22 20:32:19 CEST 2023
* telegram: Add compact ID to all replies
patch 505cd255e7a1cea83385a48467c6d1d099b3bc5d
Author: E. Bosch <presidev@AT@gmail.com>
Date: Fri Jul 21 01:12:18 CEST 2023
* telegram: Add option for geo custom URL
patch f83be7c1c04d7dcb5ebd0119895eafbc6d784ecb
Author: E. Bosch <presidev@AT@gmail.com>
Date: Thu Jul 20 04:46:06 CEST 2023
* README update
patch ee2b02319b5759d67df9fcc08fdafbd57a8fcc80
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Jul 17 02:37:21 CEST 2023
* exclam: Add delete (!del) command to delete messages
patch 1de628ed1063db8129e0c142d5fe08907b27930a
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Jul 16 20:09:53 CEST 2023
* README update
patch cbe1c8b9a949b8eb5b466496eb7eab3b2f3874bd
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Jul 16 20:02:03 CEST 2023
* README update
patch fe0c125aa83d95b5472c0bc599e883c9b47213f6
Author: E. Bosch <presidev@AT@gmail.com>
Date: Tue Jul 11 23:54:41 CEST 2023
* exclam: Fix indentation
patch 74e097435eeb72086c6ed3f43a98ce3f5463d1c6
Author: E. Bosch <presidev@AT@gmail.com>
Date: Tue Jul 11 01:48:40 CEST 2023
* exclam: Add edit (!ed) command to modify already sent messages
patch a8d4d79f7ef62a0938f42c2895c99e5d9341d50c
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Jul 10 00:56:15 CEST 2023
* README update
patch 93ee900b41a7ba1e912d3897d242f714e866de09
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Jul 10 00:52:02 CEST 2023
* Add configuration file sample (irgramdrc.sample)
patch 274ec21ecc7d6c159237df2648668f54b65d5332
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Jul 8 01:58:26 CEST 2023
* telegram, irc: Refactor and improve routine for conversion of mentions
patch 153aba3773e6ca3c7c505212ac1aeac19f15e68d
Author: E. Bosch <presidev@AT@gmail.com>
Date: Tue Jun 27 03:00:57 CEST 2023
* Fix typos in README
patch 7225855530de2f42820f61b5a8f1083269aab749
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Jun 26 22:55:35 CEST 2023
* README update
patch 1b816235b1b9baeae5f394f01c379bbb2e0136ce
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Jun 26 22:23:59 CEST 2023
* README update
patch ae837b8af1788904bfa4ed7430d331028475e8e3
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Jun 26 00:29:50 CEST 2023
* Remove trailing spaces
patch 5b2c938f7967a3345435fe21b2127999d0975f50
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Jun 26 00:17:22 CEST 2023
* Add exclam module to handle channel/chat commands begining with exclamation mark (!)
Implement reply (!re) as first exclam command
patch cbec6bc5a68bcadaaea6113355f12056d10cf577
Author: E. Bosch <presidev@AT@gmail.com>
Date: Fri Jun 23 23:49:58 CEST 2023
* telegram: Fix: in forwards when the original user is unknown use the ID
patch dd182f24b1eb76b357ab4ab84363ca55669f7b97
Author: E. Bosch <presidev@AT@gmail.com>
Date: Thu Jun 22 21:57:16 CEST 2023
* Move parse_command() to a new class, the code will be reused for the future
exclam module
patch 3ec451218b80bdd3c409b4aec91ae94ef4fa5c38
Author: E. Bosch <presidev@AT@gmail.com>
Date: Thu Jun 15 20:08:03 CEST 2023
* telegram, irc: Add conversion of "mention:" (IRC style) to "@mention" in sent messages
patch eb5b15eba474680cab8764d0242229617d960e38
Author: E. Bosch <presidev@AT@gmail.com>
Date: Thu Jun 15 01:15:43 CEST 2023
* irc: Add log for user registered (authorized in IRC)
patch 5da8c71bd90c689771ee2afddbb6f16a663c7c35
Author: E. Bosch <presidev@AT@gmail.com>
Date: Wed Jun 14 00:43:05 CEST 2023
* emoji2emoticon: Add emoji "thinking face" to convert to ASCII
patch 38a5f5ab57d1d819c40ed65f00e86c2cf6a9042f
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Jun 11 22:27:57 CEST 2023
* emoji2emoticon: Add emoji "shushing face" to convert to ASCII
patch 746b8dec8a7b53ef2750501c320aaa627d5ae7fe
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun Jun 11 00:44:55 CEST 2023
* irc: Separate character encoding options as input and output
patch 7300fb2b6fde17386971ea497343afae8526fb4c
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Jun 10 22:31:22 CEST 2023
* telegram, irc: Add conversion of mentions for self @username as well as
other mentions in self messages [saved messages]
patch da6b06a9a974df34a72286bd8b93194e712490d5
Author: E. Bosch <presidev@AT@gmail.com>
Date: Thu Jun 8 00:26:21 CEST 2023
* telegram: Refactor forward handle and related functions
Fix the use of saved_from_peer attribute and other improvements
diff -rN -u old-irgramd/LICENSE new-irgramd/LICENSE
--- old-irgramd/LICENSE 2024-10-23 04:26:42.335938425 +0200
+++ new-irgramd/LICENSE 2024-10-23 04:26:42.339938418 +0200
@@ -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-10-23 04:26:42.335938425 +0200
+++ new-irgramd/README.md 2024-10-23 04:26:42.339938418 +0200
@@ -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
@@ -32,20 +32,29 @@
Telegram account, this allows connecting to the same Telegram account from
different IRC clients on different locations or devices, so one irgramd
instance only connects to one Telegram account, if you want to connect to
-several Telegram accounts you will need to run several irgramd instances.
+several Telegram accounts you will need to run several irgramd instances. If
+all IRC clients are disconnected, irgramd will remain connected to Telegram.
+
+irgramd can also be seen as a kind of bouncer ([BNC]), with the difference
+that instead of talking IRC protocol on the client side, it talks Telegram
+protocol (MTProto), and can hide the IP and location of the IRC client (if
+executed in a different host).
## Features
- Channels, groups and private chats
- Users and channels mapped in IRC
- Messages (receive, send)
-- Media in messages (receive, download)
-- Replies (receive)
-- Forwards (receive)
-- Deletions (receive)
-- Editions (receive)
-- Reactions (receive)
+- Media in messages (receive, download, upload)
+- Replies (receive, send)
+- Forwards (receive, send)
+- Deletions (receive, do)
+- Editions (receive, do)
+- Reactions (receive, send, remove)
+- Polls (receive, show)
+- Actions [pin message, channel photo] (receive)
- Dialogs management
+- History
- Authentication and TLS for IRC
- Multiple connections from IRC
@@ -55,11 +64,68 @@
- [telethon] (tested with v1.28.5)
- [tornado] (tested with v6.1.0)
- [aioconsole] (tested with v0.6.1)
+- [pyPAM] (optional, tested with v0.4.2-13.4 from deb, [legacy web](https://web.archive.org/web/20110316070059/http://www.pangalactic.org/PyPAM/))
+
+## Instalation
+
+### From darcs
+
+ darcs clone https://src.presi.org/repos/darcs/irgramd
+ chmod +x irgramd/irgramd
+
+### From git
+
+ git clone https://github.com/prsai/irgramd.git
+ chmod +x irgramd/irgramd
+
+## Configuration
+
+From irgramd directory `./irgramd --help` will show all configuration
+options available, these options can be used directy in the command line or
+in a file.
+
+When used in command line the separator is `-` (dash) with two leading
+dashes, example: `--api-hash`.
+
+When used in a file the separator is `_` (underscore) without two leading
+dashes nor underscores, example: `api_hash`. The syntax of this file is just
+Python so strings are surrounded by quotes (`'`) and lists by brackets (`[]`).
+
+A sample of the configuration file is provided, copy it to the default
+configuration location:
+
+ mkdir -p ~/.config/irgramd
+ cp irgramd/irgramdrc.sample ~/.config/irgramd/irgramdrc
+
+And modified it with your API IDs and preferences.
+
+## Usage
+
+From irgramd directory, in foreground:
+
+ ./irgramd
+
+In background (with logs):
+
+ ./irgramd --log-file=irgramd.log &
+
+## Notes
+
+PAM authentication: it allows to authenticate IRC users from the system in
+Unix/Linux. The user that executes irgramd must have permissions to use PAM
+(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.
@@ -75,3 +141,8 @@
[python]: https://www.python.org
[tornado]: https://www.tornadoweb.org
[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-10-23 04:26:42.335938425 +0200
+++ new-irgramd/emoji2emoticon.py 2024-10-23 04:26:42.339938418 +0200
@@ -84,9 +84,15 @@
'\U0001f642': '":)"',
'\U0001f643': '"(:"',
'\U0001f644': '"o o,"',
- '\U0001f970': '":)e>"'
+ '\U0001f914': '":-L"',
+ '\U0001f92b': '":-o-m"',
+ '\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 1970-01-01 01:00:00.000000000 +0100
+++ new-irgramd/exclam.py 2024-10-23 04:26:42.339938418 +0200
@@ -0,0 +1,224 @@
+# irgramd: IRC-Telegram gateway
+# exclam.py: IRC exclamation command handlers
+#
+# 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.
+
+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),
+ '!fwd': (self.handle_command_fwd, 2, 2, -1),
+ '!upl': (self.handle_command_upl, 1, 2, 2),
+ '!reupl': (self.handle_command_reupl, 2, 3, 3),
+ '!react': (self.handle_command_react, 2, 2, -1),
+ }
+ self.tg = telegram
+ self.irc = telegram.irc
+ self.tmp_ircnick = None
+ self.tmp_telegram_id = None
+ self.tmp_tg_msg = None
+
+ async def command(self, message, telegram_id, user):
+ self.tmp_telegram_id = telegram_id
+ res = await self.parse_command(message, nick=None)
+ if isinstance(res, tuple):
+ await self.irc.send_msg(self.irc.service_user, None, res[0], user)
+ res = False
+ return res, self.tmp_tg_msg
+
+ async def check_msg(self, cid):
+ id = self.tg.mid.id_to_num_offset(self.tmp_telegram_id, cid)
+ 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)
+ return id, chk_msg
+
+ async def handle_command_re(self, cid=None, msg=None, help=None):
+ if not help:
+ id, chk_msg = await self.check_msg(cid)
+ if chk_msg is not None:
+ self.tmp_tg_msg = await self.tg.telegram_client.send_message(self.tmp_telegram_id, msg, reply_to=id)
+ reply = True
+ else:
+ 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
+ reply += \
+ (
+ ' !re <compact_id> <message>',
+ 'Reply with <message> to a message with <compact_id> on current',
+ 'channel/chat.',
+ )
+ return reply
+
+ async def handle_command_ed(self, cid=None, new_msg=None, help=None):
+ if not help:
+ id, ed_msg = await self.check_msg(cid)
+ if ed_msg is not None:
+ try:
+ self.tmp_tg_msg = await self.tg.telegram_client.edit_message(ed_msg, new_msg)
+ except MessageNotModifiedError:
+ self.tmp_tg_msg = ed_msg
+ reply = True
+ except MessageAuthorRequiredError:
+ reply = ('!ed: Not the author of the message to edit',)
+ else:
+ reply = True
+ else:
+ reply = ('Unknown message to edit',)
+ else: # HELP.brief or HELP.desc (first line)
+ reply = (' !ed Edit a message',)
+ if help == HELP.desc: # rest of HELP.desc
+ reply += \
+ (
+ ' !ed <compact_id> <new_message>',
+ 'Edit a message with <compact_id> on current channel/chat,',
+ '<new_message> replaces the current message.',
+ )
+ return reply
+
+ async def handle_command_del(self, cid=None, help=None):
+ if not help:
+ id, del_msg = await self.check_msg(cid)
+ 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 = ('!del: Not possible to delete',)
+ else:
+ self.tmp_tg_msg = None
+ reply = None
+ else:
+ reply = ('Unknown message to delete',)
+ else: # HELP.brief or HELP.desc (first line)
+ reply = (' !del Delete a message',)
+ if help == HELP.desc: # rest of HELP.desc
+ reply += \
+ (
+ ' !del <compact_id>',
+ '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-10-23 04:26:42.335938425 +0200
+++ new-irgramd/include.py 2024-10-23 04:26:42.343938411 +0200
@@ -8,6 +8,6 @@
# Constants
-VERSION = '0.1'
+VERSION = '0.2'
NICK_MAX_LENGTH = 20
-CHAN_MAX_LENGHT = 50
+CHAN_MAX_LENGTH = 50
diff -rN -u old-irgramd/irc.py new-irgramd/irc.py
--- old-irgramd/irc.py 2024-10-23 04:26:42.335938425 +0200
+++ new-irgramd/irc.py 2024-10-23 04:26:42.343938411 +0200
@@ -18,10 +18,11 @@
# Local modules
-from include import VERSION, CHAN_MAX_LENGHT, NICK_MAX_LENGTH
+from include import VERSION, CHAN_MAX_LENGTH, NICK_MAX_LENGTH
from irc_replies import irc_codes
from utils import chunks, set_replace, split_lines
from service import service
+from exclam import exclam
# Constants
@@ -82,7 +83,7 @@
user.del_from_channels(self)
del user
break
- message = message.decode(self.conf['char_encoding'], errors='replace').replace('\r','\n')
+ message = message.decode(self.conf['char_in_encoding'], errors='replace').replace('\r','\n')
self.logger.debug(message)
for pattern, handler, register_required, num_params_required in self.irc_handlers:
@@ -109,6 +110,7 @@
def set_telegram(self, tg):
self.tg = tg
self.service = service(self.conf, self.tg)
+ self.exclam = exclam(self.tg)
# IRC
@@ -140,14 +142,14 @@
self.irc_channels_founder = collections.defaultdict(set)
self.start_time = time.strftime('%a %d %b %Y %H:%M:%S %z')
- self.service_user = IRCUser(None, ('Services',), self.conf['service_user'],
+ self.service_user = IRCUser(None, ('Services',''), self.conf['service_user'],
'Control', 'Telegram Service', is_service=True)
self.users[self.conf['service_user'].lower()] = self.service_user
async def send_irc_command(self, user, command):
self.logger.debug('Send IRC Command: %s', command)
command = command + '\r\n'
- user.stream.write(command.encode(self.conf['char_encoding'], errors='replace'))
+ user.stream.write(command.encode(self.conf['char_out_encoding'], errors='replace'))
# IRC handlers
@@ -406,15 +408,20 @@
tgt = tgl
if tgt in self.iid_to_tid:
+ message = self.tg.replace_mentions(message, me_nick='', received=False)
telegram_id = self.iid_to_tid[tgt]
- tg_msg = await self.tg.telegram_client.send_message(telegram_id, message)
-
- mid = self.tg.mid.num_to_id_offset(telegram_id, tg_msg.id)
- text = '[{}] {}'.format(mid, message)
- self.tg.to_cache(tg_msg.id, mid, text, message, user, chan, media=None)
+ if message[0] == '!':
+ cont, tg_msg = await self.exclam.command(message, telegram_id, user)
+ else:
+ tg_msg = await self.tg.telegram_client.send_message(telegram_id, message)
+ cont = True
+ if cont:
+ mid = self.tg.mid.num_to_id_offset(telegram_id, tg_msg.id)
+ text = '[{}] {}'.format(mid, message)
+ self.tg.to_cache(tg_msg.id, mid, text, message, user, chan, media=None)
- if defered_send:
- await defered_send(user, defered_target, text)
+ if defered_send:
+ await defered_send(user, defered_target, text)
else:
await self.reply_code(user, 'ERR_NOSUCHNICK', (target,))
@@ -427,6 +434,8 @@
# IRC functions
async def register(self, user):
+ self.logger.info('Registered IRC user "%s" from %s:%s', user.irc_nick, user.address, user.port)
+
user.registered = True
await self.send_greeting(user)
await self.send_help(user)
@@ -461,6 +470,10 @@
for irc_user in irc_users:
await self.send_privmsg(irc_user, source_mask, target, message)
+ async def send_action(self, source, target, message):
+ action_message = '\x01ACTION {}\x01'.format(message)
+ await self.send_msg(source, target, action_message)
+
async def send_privmsg(self, user, source_mask, target, msg):
# reference [1]
src_mask = source_mask if source_mask else user.get_irc_mask()
@@ -468,6 +481,9 @@
tgt = target if target else user.irc_nick
if self.tg.refwd_me:
msg = msg.format(user.irc_nick)
+ # replace self @username and other mentions for self messages sent by this instance of irgramd
+ msg = self.tg.replace_mentions(msg, user.irc_nick)
+
await self.send_irc_command(user, ':{} PRIVMSG {} :{}'.format(src_mask, tgt, msg))
async def reply_command(self, user, prfx, comm, params):
@@ -513,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):
@@ -637,6 +649,7 @@
def __init__(self, stream, address, irc_nick=None, username='', realname=None, is_service=False):
self.stream = stream
self.address = address[0]
+ self.port = str(address[1])
self.from_localhost = True if address[0].split('.')[0] == '127' else False
self.irc_nick = irc_nick
self.irc_username = str(username)
diff -rN -u old-irgramd/irgramd new-irgramd/irgramd
--- old-irgramd/irgramd 2024-10-23 04:26:42.335938425 +0200
+++ new-irgramd/irgramd 2024-10-23 04:26:42.343938411 +0200
@@ -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,20 +66,36 @@
# 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('char_encoding', default='utf-8', metavar='ENCODING', help='Character encoding for 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)')
tornado.options.define('config_dir', default='~/.config/irgramd', metavar='PATH', help='Configuration directory where telegram session info is saved')
+ tornado.options.define('download_media', default=True, help='Enable download of any media (photos, documents, etc.), if not set only a message of media will be shown')
+ tornado.options.define('download_notice', default=10, metavar='SIZE (MiB)', help='Enable a notice when a download starts if its size is greater than SIZE, this is useful when a download takes some time to be completed')
tornado.options.define('emoji_ascii', default=False, help='Replace emoji with ASCII emoticons')
+ tornado.options.define('geo_url', type=str, default=None, metavar='TEMPLATE_URL', help='Use custom URL for showing geo latitude/longitude location, eg. OpenStreetMap')
tornado.options.define('hist_timestamp_format', metavar='DATETIME_FORMAT', help='Format string for timestamps in history, see https://www.strfti.me')
tornado.options.define('irc_address', default='127.0.0.1', metavar='ADDRESS', help='Address to listen on for IRC')
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')
@@ -93,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/irgramdrc.sample new-irgramd/irgramdrc.sample
--- old-irgramd/irgramdrc.sample 1970-01-01 01:00:00.000000000 +0100
+++ new-irgramd/irgramdrc.sample 2024-10-23 04:26:42.343938411 +0200
@@ -0,0 +1,21 @@
+api_id=XXXXXX
+api_hash='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
+phone='XXXXXXXXXXX'
+#ask_code=True
+#irc_address='0.0.0.0'
+#tls=True
+#tls_cert='/path/to/certificate'
+#tls_key='/path/to/key'
+irc_nicks=['XXXXX', 'XXXXXX']
+media_url='https://server/token/'
+#pam=True
+#pam_group='XXXXX'
+#char_in_encoding='iso-8859-1'
+#char_out_encoding='iso-8859-1'
+#emoji_ascii=True
+hist_timestamp_format='[%m-%d %H:%M]'
+#timezone='Europe/Madrid'
+#geo url OpenStreetMap
+#geo_url='https://osm.org/?mlat={lat}&mlon={long}&zoom=15'
+#geo url Google Maps
+#geo_url='https://maps.google.com/?q={lat},{long}'
diff -rN -u old-irgramd/service.py new-irgramd/service.py
--- old-irgramd/service.py 2024-10-23 04:26:42.339938418 +0200
+++ new-irgramd/service.py 2024-10-23 04:26:42.343938411 +0200
@@ -6,41 +6,32 @@
# 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 utils import compact_date
+from utils import compact_date, command, HELP
from telethon import utils as tgutils
-class service:
+class service(command):
def __init__(self, settings, telegram):
self.commands = \
- { # Command Handler Arguments Min Max
- 'code': (self.handle_command_code, 1, 1),
- 'dialog': (self.handle_command_dialog, 1, 2),
- 'get': (self.handle_command_get, 2, 2),
- 'help': (self.handle_command_help, 0, 1),
- 'history': (self.handle_command_history, 1, 3),
- 'mark_read': (self.handle_command_mark_read, 1, 1),
+ { # Command Handler Arguments Min Max Maxsplit
+ 'code': (self.handle_command_code, 1, 1, -1),
+ 'dialog': (self.handle_command_dialog, 1, 2, -1),
+ 'get': (self.handle_command_get, 2, 2, -1),
+ 'help': (self.handle_command_help, 0, 1, -1),
+ 'history': (self.handle_command_history, 1, 3, -1),
+ 'mark_read': (self.handle_command_mark_read, 1, 1, -1),
}
self.ask_code = settings['ask_code']
self.tg = telegram
self.irc = telegram.irc
self.tmp_ircnick = None
- async def parse_command(self, line, nick):
-
- words = line.split()
- command = words.pop(0).lower()
- self.tmp_ircnick = nick
- if command in self.commands.keys():
- handler, min_args, max_args = self.commands[command]
- num_words = len(words)
- if num_words < min_args or num_words > max_args:
- reply = ('Wrong number of arguments',)
- else:
- reply = await handler(*words)
- else:
- reply = ('Unknown command',)
-
- return reply
+ 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:
@@ -94,10 +85,8 @@
if id == self.tg.id:
name_in_irc = self.tmp_ircnick
else:
- if id in self.tg.tid_to_iid.keys():
- name_in_irc = self.tg.tid_to_iid[id]
- else:
- name_in_irc = '<Unknown>'
+ name_in_irc = self.tg.get_irc_name_from_telegram_id(id)
+
reply += (' {:<11d} {:<9d} {:<9d} {:5} {:<3} {:<4} {:<6} {}'.format(
id, unr, men, ty, pin, arch, last, name_in_irc),
)
@@ -110,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
@@ -123,13 +112,18 @@
if reply: return reply
else: reply = ()
- id = self.tg.mid.id_to_num_offset(peer_id, mid)
+ # If the ID starts with '=' is absolute ID, not compact ID
+ # character '=' is not used by compact IDs
+ if mid[0] == '=':
+ id = int(mid[1:])
+ else:
+ id = self.tg.mid.id_to_num_offset(peer_id, mid)
if id is not None:
msg = await self.tg.telegram_client.get_messages(entity=peer_id, ids=id)
if msg is not None:
await self.tg.handle_telegram_message(event=None, message=msg, history=True)
else:
- reply = ('Message not found',)
+ reply = ('Message not found',)
return reply
else: # HELP.brief or HELP.desc (first line)
@@ -137,8 +131,8 @@
if help == HELP.desc: # rest of HELP.desc
reply += \
(
- ' get <peer> <compact_id>',
- 'Get one message from peer with the compact ID',
+ ' get <peer> <compact_id|=ID>',
+ 'Get one message from peer with the compact or absolute ID',
)
return reply
@@ -161,12 +155,20 @@
help_text += await handler(help=HELP.brief)
help_text += \
(
+ 'The commands begining with ! (exclamation) must be used directly',
+ 'in channels or chats. The following ! commands are available:',
+ )
+ for command in self.irc.exclam.commands.values():
+ handler = command[0]
+ help_text += await handler(help=HELP.brief)
+ help_text += \
+ (
'If you need more information about a specific command you can use',
'help <command>',
)
help_text += end_help
- elif help_command in self.commands.keys():
- handler = self.commands[help_command][0]
+ elif help_command in (all_commands := dict(**self.commands, **self.irc.exclam.commands)).keys():
+ handler = all_commands[help_command][0]
help_text = start_help
help_text += await handler(help=HELP.desc)
help_text += end_help
@@ -255,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
@@ -267,7 +269,3 @@
peer_id = None
reply = ('Unknown user or channel',)
return peer_id, reply
-
-class HELP:
- desc = 1
- brief = 2
diff -rN -u old-irgramd/telegram.py new-irgramd/telegram.py
--- old-irgramd/telegram.py 2024-10-23 04:26:42.339938418 +0200
+++ new-irgramd/telegram.py 2024-10-23 04:26:42.343938411 +0200
@@ -2,14 +2,13 @@
# 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
@@ -20,16 +19,17 @@
# 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,8 +38,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']
@@ -51,6 +55,7 @@
self.quote_len = settings['quote_length']
self.hist_fmt = settings['hist_timestamp_format']
self.timezone = settings['timezone']
+ self.geo_url = settings['geo_url']
if not settings['emoji_ascii']:
e.emo = {}
self.media_cn = 0
@@ -63,15 +68,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):
@@ -91,10 +105,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)
@@ -132,6 +146,7 @@
tg_user = await self.telegram_client.get_me()
self.id = tg_user.id
self.tg_username = self.get_telegram_nick(tg_user)
+ self.add_sorted_len_usernames(self.tg_username)
self.set_ircuser_from_telegram(tg_user)
async for dialog in self.telegram_client.iter_dialogs():
chat = dialog.entity
@@ -145,8 +160,9 @@
tg_nick = self.get_telegram_nick(user)
tg_ni = tg_nick.lower()
if not user.is_self:
- irc_user = IRCUser(None, ('Telegram',), tg_nick, user.id, self.get_telegram_display_name(user))
+ irc_user = IRCUser(None, ('Telegram',''), tg_nick, user.id, self.get_telegram_display_name(user))
self.irc.users[tg_ni] = irc_user
+ self.add_sorted_len_usernames(tg_ni)
self.tid_to_iid[user.id] = tg_nick
self.irc.iid_to_tid[tg_ni] = user.id
else:
@@ -200,18 +216,38 @@
if nick == self.tg_username: return None
return self.irc.users[nick.lower()]
- def get_irc_nick_from_telegram_forward(self, fwd):
- if fwd.from_id is None:
+ def get_irc_name_from_telegram_id(self, tid):
+ if tid in self.tid_to_iid.keys():
+ name_in_irc = self.tid_to_iid[tid]
+ else:
+ name_in_irc = '<Unknown>'
+ return name_in_irc
+
+ async def get_irc_name_from_telegram_forward(self, fwd, saved):
+ from_id = fwd.saved_from_peer if saved else fwd.from_id
+ if from_id is None:
# telegram user has privacy options to show only the name
- nick = fwd.from_name
+ # or was a broadcast from a channel (no user)
+ name = fwd.from_name
else:
- user = self.get_irc_user_from_telegram(fwd.from_id.user_id)
- if user is None:
- nick = '{}'
- self.refwd_me = True
+ peer_id, type = self.get_peer_id_and_type(from_id)
+ if type == 'user':
+ try:
+ user = self.get_irc_user_from_telegram(peer_id)
+ except:
+ name = str(peer_id)
+ else:
+ if user is None:
+ name = '{}'
+ self.refwd_me = True
+ else:
+ name = user.irc_nick
else:
- nick = user.irc_nick
- return nick
+ try:
+ name = await self.get_irc_channel_from_telegram_id(peer_id)
+ except:
+ name = ''
+ return name
async def get_irc_nick_from_telegram_id(self, tid, entity=None):
if tid not in self.tid_to_iid:
@@ -253,7 +289,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
@@ -318,6 +354,21 @@
return short if format == 'short' else long
+ def get_peer_id_and_type(self, peer):
+ if isinstance(peer, tgty.PeerChannel):
+ id = peer.channel_id
+ type = 'chan'
+ elif isinstance(peer, tgty.PeerChat):
+ id = peer.chat_id
+ type = 'chan'
+ elif isinstance(peer, tgty.PeerUser):
+ id = peer.user_id
+ type = 'user'
+ else:
+ id = peer
+ type = ''
+ return id, type
+
async def is_bot(self, irc_nick, tid=None):
user = self.irc.users[irc_nick]
if user.stream or user.is_service:
@@ -339,7 +390,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:
@@ -349,7 +402,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):
@@ -360,29 +413,84 @@
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 replace_mentions(self, text):
- def repl_mentioned(text):
- if text and text[0] == '@':
- part = text[1:].lower()
- if part in self.irc.users:
- return '{}{}{}'.format('~', self.irc.users[part].irc_nick, '~')
- return text
-
- if text.find('@') != -1:
- words = text.split(' ')
- words_replaced = [repl_mentioned(elem) for elem in words]
- text_replaced = ' '.join(words_replaced)
+ 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
+ rargs = {}
+ def repl_mentioned(text, me_nick, received, mark, repl_pref, repl_suff):
+ new_text = text
+
+ for user in self.sorted_len_usernames:
+ if user == self.tg_username:
+ if me_nick:
+ username = me_nick
+ else:
+ continue
+ else:
+ username = self.irc.users[user].irc_nick
+
+ if received:
+ mention = mark + user
+ mention_case = mark + username
+ else: # sent
+ mention = user + mark
+ mention_case = username + mark
+ replcmnt = repl_pref + username + repl_suff
+
+ # Start of the text
+ for ment in (mention, mention_case):
+ if new_text.startswith(ment):
+ new_text = new_text.replace(ment, replcmnt, 1)
+
+ # Next words (with space as separator)
+ mention = ' ' + mention
+ mention_case = ' ' + mention_case
+ replcmnt = ' ' + replcmnt
+ new_text = new_text.replace(mention, replcmnt).replace(mention_case, replcmnt)
+
+ return new_text
+
+ if received:
+ mark = '@'
+ rargs['repl_pref'] = '~'
+ rargs['repl_suff'] = '~'
+ else: # sent
+ mark = ':'
+ rargs['repl_pref'] = '@'
+ rargs['repl_suff'] = ''
+
+ if text.find(mark) != -1:
+ text_replaced = repl_mentioned(text, me_nick, received, mark, **rargs)
else:
text_replaced = text
return text_replaced
@@ -392,8 +500,31 @@
filtered = self.replace_mentions(filtered)
return filtered
+ def add_sorted_len_usernames(self, username):
+ 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)
@@ -425,32 +556,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 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:
@@ -459,87 +603,122 @@
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
user = self.get_irc_user_from_telegram(msg.sender_id)
mid = self.mid.num_to_id_offset(msg.peer_id, msg.id)
- text = await self.render_text(msg, mid, upd_to_webpend)
- text_send = self.set_history_timestamp(text, history, msg.date)
+ 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, history=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)
+ text = await self.handle_telegram_media(message, user, mid)
else:
text = message.message
- if message.is_reply:
+ if message.action:
+ final_text = await self.handle_telegram_action(message, mid)
+ return final_text
+ elif message.is_reply:
refwd_text = await self.handle_telegram_reply(message)
elif message.forward:
refwd_text = await self.handle_telegram_forward(message)
else:
refwd_text = ''
- final_text = '[{}] {}{}'.format(mid, refwd_text, text)
+ target_mine = self.handle_target_mine(message.peer_id, user)
+
+ final_text = '[{}] {}{}{}'.format(mid, target_mine, refwd_text, text)
final_text = self.filters(final_text)
return final_text
- def set_history_timestamp(self, text, history, date):
+ def set_history_timestamp(self, text, history, date, action):
if history and self.hist_fmt:
timestamp = format_timestamp(self.hist_fmt, self.timezone, date)
- res = '{} {}'.format(timestamp, text)
+ if action:
+ res = '{} {}'.format(text, timestamp)
+ else:
+ res = '{} {}'.format(timestamp, text)
else:
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)
if private:
- await self.relay_telegram_private_message(user, text)
+ await self.relay_telegram_private_message(user, text, action)
chan = None
else:
- chan = await self.relay_telegram_channel_message(message, user, text, channel)
+ chan = await self.relay_telegram_channel_message(message, user, text, channel, action)
return chan
- async def relay_telegram_private_message(self, user, message):
- self.logger.debug('Handling Telegram Private Message: %s, %s', user, message)
-
- await self.irc.send_msg(user, None, message)
+ async def relay_telegram_private_message(self, user, message, action=None):
+ self.logger.debug('Relaying Telegram Private Message: %s, %s', user, message)
- async def relay_telegram_channel_message(self, message, user, text, channel=None):
- self.logger.debug('Handling Telegram Channel Message: %s', message or text)
+ if action:
+ await self.irc.send_action(user, None, message)
+ else:
+ await self.irc.send_msg(user, None, message)
+ async def relay_telegram_channel_message(self, message, user, text, channel, action):
if message:
entity = await message.get_chat()
chan = await self.get_irc_channel_from_telegram_id(message.chat_id, entity)
else:
chan = channel
- await self.irc.send_msg(user, chan, text)
+
+ self.logger.debug('Relaying Telegram Channel Message: %s, %s', chan, text)
+
+ if action:
+ await self.irc.send_action(user, chan, text)
+ else:
+ await self.irc.send_msg(user, chan, text)
+
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
@@ -571,50 +750,97 @@
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, 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, mid)
+ action_text = 'has changed chat [{}] {}'.format(media_type, photo_url)
+ else:
+ action_text = ''
+ return action_text
+
async def handle_telegram_reply(self, message):
+ space = ' '
trunc = ''
replied = await message.get_reply_message()
- replied_msg = replied.message
+ 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 = '[{}]'.format(self.mid.num_to_id_offset(replied.peer_id, replied.id))
+ 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
- return '|Re {}: {}{}| '.format(replied_nick, replied_msg, trunc)
+ return '|Re {}: [{}]{}{}{}| '.format(replied_nick, cid, space, replied_msg, trunc)
async def handle_telegram_forward(self, message):
- forwarded_nick = self.get_irc_nick_from_telegram_forward(message.fwd_from)
- forwarded_peer = message.fwd_from.saved_from_peer
- if isinstance(forwarded_peer, tgty.PeerChannel):
- dest = ' ' + await self.get_irc_channel_from_telegram_id(forwarded_peer.channel_id)
- elif isinstance(forwarded_peer, tgty.PeerChat):
- dest = ' ' + await self.get_irc_channel_from_telegram_id(forwarded_peer.chat_id)
+ space = space2 = ' '
+ if not (forwarded_peer_name := await self.get_irc_name_from_telegram_forward(message.fwd_from, saved=False)):
+ space = ''
+ saved_peer_name = await self.get_irc_name_from_telegram_forward(message.fwd_from, saved=True)
+ if saved_peer_name and saved_peer_name != forwarded_peer_name:
+ 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:
- dest = ' ' + self.get_irc_user_from_telegram(forwarded_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:
- dest = ''
+ secondary_name = ''
+ space2 = ''
- return '|Fwd {}{}| '.format(forwarded_nick, dest)
+ return '|Fwd{}{}{}{}| '.format(space, forwarded_peer_name, space2, secondary_name)
- async def handle_telegram_media(self, message):
+ async def handle_telegram_media(self, message, user, mid):
caption = ' | {}'.format(message.message) if message.message else ''
to_download = True
media_url_or_data = ''
+ size = 0
+ filename = None
+
+ def scan_doc_attributes(document):
+ 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) 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_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
@@ -625,24 +851,28 @@
media_url_or_data = message.message
caption = ''
elif message.photo:
- size = message.media.photo.sizes[-1]
- if hasattr(size, 'w') and hasattr(size, 'h'):
- media_type = 'photo:{}x{}'.format(size.w, size.h)
- else:
- media_type = 'photo'
- elif message.audio: media_type = 'audio'
- elif message.voice: media_type = 'rec'
+ size, media_type = self.scan_photo_attributes(message.media.photo)
+ 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 = get_human_size(message.media.document.size)
- attrib = next(x for x in message.media.document.attributes if isinstance(x, tgty.DocumentAttributeVideo))
- dur = get_human_duration(attrib.duration)
- media_type = 'video:{},{}'.format(size, dur)
+ size, h_size, attrib_video, filename = scan_doc_attributes(message.media.document)
+ dur = get_human_duration(attrib_video.duration) if attrib_video else ''
+ media_type = 'video:{},{}'.format(h_size, dur)
elif message.video_note: media_type = 'videorec'
elif message.gif: media_type = 'anim'
elif message.sticker: media_type = 'sticker'
elif message.document:
- size = get_human_size(message.media.document.size)
- media_type = 'file:{}'.format(size)
+ size, h_size, _, filename = scan_doc_attributes(message.media.document)
+ media_type = 'file:{}'.format(h_size)
elif message.contact:
media_type = 'contact'
caption = ''
@@ -665,7 +895,12 @@
media_type = 'geo'
caption = ''
to_download = False
- media_url_or_data = 'lat: {}, long: {}'.format(message.media.geo.lat, message.media.geo.long)
+ if self.geo_url:
+ geo_url = ' | ' + self.geo_url
+ else:
+ geo_url = ''
+ lat_long_template = 'lat: {lat}, long: {long}' + geo_url
+ media_url_or_data = lat_long_template.format(lat=message.media.geo.lat, long=message.media.geo.long)
elif message.invoice:
media_type = 'invoice'
@@ -677,7 +912,7 @@
media_type = 'poll'
caption = ''
to_download = False
- media_url_or_data = ''
+ media_url_or_data = self.handle_poll(message.media.poll)
elif message.venue:
media_type = 'venue'
@@ -691,13 +926,33 @@
media_url_or_data = message.message
if to_download:
- media_url_or_data = await self.download_telegram_media(message)
+ relay_attr = (message, user, mid, media_type)
+ 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)
- async def handle_webpage(self, webpage, message):
+ def handle_poll(self, poll):
+ text = poll.question
+ for ans in poll.answers:
+ text += '\n* ' + ans.text
+ return text
+
+ def handle_target_mine(self, target, user):
+ # Add the target of messages sent by self user (me)
+ # received in other clients
+ target_id, target_type = self.get_peer_id_and_type(target)
+ if user is None and target_type == 'user' and target_id != self.id:
+ # self user^
+ # as sender
+ irc_id = self.get_irc_name_from_telegram_id(target_id)
+ target_mine = '[T: {}] '.format(irc_id)
+ else:
+ target_mine = ''
+ return target_mine
+
+ 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:
@@ -736,24 +991,60 @@
def format_media(self, media_type, media_url_or_data, caption):
return '[{}] {}{}'.format(media_type, media_url_or_data, caption)
- async def download_telegram_media(self, message):
- local_path = await message.download_media(self.telegram_media_dir)
- if not local_path: return ''
-
- if message.document:
- new_file = sanitize_filename(os.path.basename(local_path))
- else:
+ def scan_photo_attributes(self, photo):
+ size = 0
+ sizes = photo.sizes
+ ph_size = sizes[-1]
+ if isinstance(ph_size, tgty.PhotoSizeProgressive):
+ size = ph_size.sizes[-1]
+ else:
+ for x in sizes:
+ if isinstance(x, tgty.PhotoSize):
+ if x.size > size:
+ size = x.size
+ ph_size = x
+ if hasattr(ph_size, 'w') and hasattr(ph_size, 'h'):
+ media_type = 'photo:{}x{}'.format(ph_size.w, ph_size.h)
+ else:
+ media_type = 'photo'
+
+ return size, media_type
+
+ async def download_telegram_media(self, message, mid, filename=None, size=0, relay_attr=None):
+ if not self.download:
+ return ''
+ if 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
+ else:
+ await self.notice_downloading(size, relay_attr)
+ local_path = await message.download_media(new_path)
+ if not local_path: return ''
+ else:
+ await self.notice_downloading(size, relay_attr)
+ 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)
- new_path = os.path.join(self.telegram_media_dir, new_file)
if local_path != new_path:
os.replace(local_path, new_path)
if self.media_url[-1:] != '/':
self.media_url += '/'
return self.media_url + new_file
+ async def notice_downloading(self, size, relay_attr):
+ if relay_attr and size > self.notice_size:
+ message, user, mid, media_type = relay_attr
+ await self.relay_telegram_message(message, user, '[{}] [{}] [Downloading]'.format(mid, media_type))
+
class mesg_id:
def __init__(self, alpha):
self.alpha = alpha
diff -rN -u old-irgramd/utils.py new-irgramd/utils.py
--- old-irgramd/utils.py 2024-10-23 04:26:42.339938418 +0200
+++ new-irgramd/utils.py 2024-10-23 04:26:42.347938405 +0200
@@ -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,14 +13,39 @@
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)://[^ ]+')
# Utilities
+class command:
+ async def parse_command(self, line, nick):
+ command = line.partition(' ')[0].lower()
+ self.tmp_ircnick = nick
+ if command in self.commands.keys():
+ handler, min_args, max_args, maxsplit = self.commands[command]
+ words = line.split(maxsplit=maxsplit)[1:]
+ num_words = len(words)
+ if num_words < min_args or num_words > max_args:
+ reply = ('Wrong number of arguments',)
+ else:
+ reply = await handler(*words)
+ else:
+ reply = ('Unknown command',)
+
+ return reply
+
+class HELP:
+ 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
@@ -60,7 +85,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
@@ -109,11 +151,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:
@@ -125,6 +167,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())
@@ -156,7 +201,7 @@
res += '-{}- '.format(i[2:])
# addition of words
elif i[0] == '+':
- res += '_{}_ '.format(i[2:])
+ res += '+{}+ '.format(i[2:])
else:
res += '{} '.format(i[2:])
eq += 1
@@ -179,3 +224,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