├── .gitignore ├── LICENSE ├── README.md ├── README.zhs.md ├── config ├── example_services ├── telegramircd └── telegramircd.service ├── requirements.txt └── telegramircd.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [简体中文](README.zhs.md) 2 | 3 | # telegramircd [![IRC](https://img.shields.io/badge/IRC-freenode-yellow.svg)](https://webchat.freenode.net/?channels=wechatircd) [![Telegram](https://img.shields.io/badge/chat-Telegram-blue.svg)](https://t.me/wechatircd) [![Gitter](https://img.shields.io/badge/chat-Gitter-753a88.svg)](https://gitter.im/wechatircd/wechatircd) 4 | 5 | telegramircd is an IRC server that enables IRC clients to send and receive messages from Telegram. 6 | 7 | telegramircd uses [telethon-sync](https://github.com/LonamiWebs/Telethon) to communicate with Telegram servers. 8 | 9 | ## Installation 10 | 11 | - `git clone https://github.com/MaskRay/telegramircd && cd telegramircd` 12 | - python >= 3.5 13 | - libmagic 14 | - `pip3 install -r requirements.txt` 15 | 16 | Create a Telegram App. 17 | 18 | - Visit , create an App, and get `app_id, app_hash`. 19 | - Update `config`: change `tg-api-id, tg-api-hash, tg-phone`; change `tg-session-dir` to the directory where you want to store `telegramircd.session` (defaults to `.` for current working directory, it will be created after the initial login) 20 | - `./telegramircd.py -c config` 21 | 22 | ### Arch Linux 23 | 24 | The `git clone` and `pip3 install -r requirements.txt` steps can be replaced with: 25 | 26 | - Install `aur/telegramircd-git` (which depends on `aur/python-telethon`. You may also use `archlinuxcn/python-telethon`). 27 | - The server is installed at `/usr/bin/telegramircd`. 28 | 29 | A systemd service template is install at `/lib/systemd/system/telegramircd.service`. You may create `/etc/systemd/system/telegramircd.service` from the template. Change the `User=` and `Group=` fields to whom `telethon` is installed with. Run `systemctl start telegramircd`. 30 | 31 | ## Running telegramircd 32 | 33 | `telegramircd.py` (the server) will listen on 127.0.0.1:6669 (IRC, `irc-listen, irc-port`) and 127.0.0.1:9003 (HTTPS + WebSocket over TLS, `http-url`). 34 | 35 | Connect to the IRC server with you favorite IRC client. You will join the channel `+telegram` automatically. For the first login, you need to type `/oper a $login_code` where `$login_code` is sent to your phone as a short message. If two-step verification is enabled, you will need to type `/oper a $password`. A file named `$tg_session.session` is saved in `$tg_session_dir`, and login code is not required for future logins. 36 | 37 | Session files can also be created by executing `TelegramClient(session_name, api_id, api_hash)` (see ). 38 | 39 | If you run the server on another machine, it is recommended to set up IRC over TLS and an IRC connection password with a few more options: `--irc-cert /path/to/irc.key --irc-key /path/to/irc.cert --irc-password yourpassword`. As an alternative to the IRC connection password, you may specify `--sasl-password yourpassword` and authenticate with SASL PLAIN. You can reuse the HTTPS certificate+key. If you use WeeChat and find it difficult to set up a valid certificate (gnutls checks the hostname), type the following lines in WeeChat: 40 | ``` 41 | /set irc.server.telegram.ssl on 42 | /set irc.server.telegram.ssl_verify off 43 | /set irc.server.telegram.password yourpassword 44 | ``` 45 | 46 | ### Serve file links via HTTPS 47 | 48 | A few more options: `--http-key /etc/telegramircd/key.pem --http-cert /etc/telegramircd/cert.pem --http-url https://127.1:9003`. File links will be shown as `https://127.1:9003/document/$id`. 49 | 50 | You may create a CA certificate/key pair and use that to sign another certificate/key pair. 51 | 52 | ```zsh 53 | openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key.pem -out ca.cert.pem -days 9999 -subj '/CN=127.0.0.1' 54 | openssl req -new -newkey rsa:2048 -nodes -keyout key.pem -subj '/CN=127.0.0.1' | 55 | openssl x509 -req -out cert.pem -CAkey ca.key.pem -CA ca.cert.pem -set_serial 2 -days 9999 -extfile <( 56 | printf "subjectAltName = IP:127.0.0.1, DNS:localhost") 57 | ``` 58 | 59 | Chrome/Chromium 60 | 61 | - Visit `chrome://settings/certificates`, import `ca.cert.pem`, click the `Authorities` tab, select the `127.0.0.1` certificate, Edit->Trust this certificate for identifying websites. 62 | 63 | The IP address or the domain name should match the `subjectAlternativeName` fields. Chrome has removed support for `commonName` matching in certificates since version 58. See for detail. 64 | 65 | Firefox 66 | 67 | - Install extension Redirector, redirects `app.js` as above, click ` Applies to: Main window (address bar), Scripts`. 68 | - Visit one file link, Firefox will show "Your connection is not secure", Advanced->Add Exception->Confirm Security Exception. 69 | 70 | ## Usage 71 | 72 | - Run `telegramircd.py`. 73 | - Connect to 127.0.0.1:6669 in your IRC client 74 | 75 | You will join `+telegram` channel automatically and find your contact list there. Some commands are available: 76 | 77 | - `help` 78 | - `status`, mutual contact list、group/supergroup list 79 | - `eval $expr`: eval the Python expression `$expr`. Examples: 80 | ``` 81 | eval client.peer_id2special_room 82 | eval client.peer_id2special_user 83 | ``` 84 | 85 | The server will be bound to one account, however, you may have more than one IRC clients connected to the server. 86 | 87 | ## IRC features 88 | 89 | - Surnames come first when displaying Chinese names for users without `username`. `SpecialUser#name` 90 | - Standard IRC channels have names beginning with `#`. 91 | - Telegram channels/chats have names beginning with `&`. The channel name is generated from the group title. `SpecialChannel#update` 92 | - Contacts have modes `+v` (voice, usually displayed with a prefix `+`). `SpecialChannel#update_detail` 93 | - Multi-line messages: `!m line0\nline1` 94 | - Multi-line messages: `!html line0
line1` 95 | - `nick0: nick1: test` will be converted to `@GroupAlias0 @GroupAlias1 test`, where `GroupAlias0` is the name set by that user, not your `Set Remark and Tag`. It corresponds to `On-screen names` in the mobile application. 96 | - Reply to the message at 12:34:SS: `@1234 !m multi\nline\nreply`, which will be sent as `「Re GroupAlias: text」text` 97 | - Reply to the message at 12:34:56: `!m @123456 multi\nline\nreply` 98 | - Reply to the penultimate message (your own messages are not counted) in this channel/chat: `@2 reply` 99 | - Paste detection. PRIVMSG lines will be hold for up to 0.1 seconds, lines in this interval will be packed to a multiline message 100 | 101 | `!m `, `@3 `, `nick: ` can be arranged in any order 102 | 103 | For WeeChat, its anti-flood mechanism will prevent two user messages sent to IRC server in the same time. Disable anti-flood to enable paste detection. 104 | ``` 105 | /set irc.server.wechat.anti_flood_prio_high 0 106 | ``` 107 | 108 | `server-time` extension from IRC version 3.1, 3.2. `telegramircd.py` includes the timestamp (obtained from JavaScript) in messages to tell IRC clients that the message happened at the given time. See . See for Client support of IRCv3. 109 | 110 | Configuration for WeeChat: 111 | ``` 112 | /set irc.server_default.capabilities "account-notify,away-notify,cap-notify,multi-prefix,server-time,znc.in/server-time-iso,znc.in/self-message" 113 | ``` 114 | 115 | Supported IRC commands: 116 | 117 | - `/cap`, supported capabilities. 118 | - `/dcc send $nick/$channel $filename`, send image or file。This feature borrows the command `/dcc send` which is well supported in IRC clients. See . 119 | - `/invite $nick [$channel]`, invite a contact to the channel. 120 | - `/kick $nick`, delete a group member. You must be the group leader to do this. Due to the defect of the Web client, you may not receive notifcations about the change of members. 121 | - `/kill $nick [$reason]`, cause the connection of that client to be closed 122 | - `/list`, list groups. 123 | - `/mode +m`, no rejoin in `--join new` mode. `/mode -m` to revert. 124 | - `/names`, update nicks in the channel. 125 | - `/part [$channel]`, no longer receive messages from the channel. It just borrows the command `/part` and it will not leave the group. 126 | - `/query $nick`, open a chat window with `$nick`. 127 | - `/topic topic`, change the topic of a group. Because IRC does not support renaming of a channel, you will leave the channel with the old name and join a channel with the new name. 128 | - `/who $channel`, see the member list. 129 | 130 | ## Server options 131 | 132 | - `--config`, short option `-c`, config file path, see [config](config) 133 | - HTTP/WebSocket related options 134 | + `--http-cert cert.pem`, TLS certificate for HTTPS. You may concatenate certificate+key, specify a single PEM file and omit `--http-key`. Use HTTP if neither --http-cert nor --http-key is specified. 135 | + `--http-url http://localhost`, Show file links as http://localhost/document/$id . 136 | + `--http-key key.pem`, TLS key for HTTPS. 137 | + `--http-listen 127.1 ::1`, change HTTPS listen address to `127.1` and `::1`, overriding `--listen`. 138 | + `--http-port 9003`, change HTTPS listen port to 9003. 139 | - Groups that should not join automatically. This feature supplements join mode. 140 | + `--ignore '&fo[o]' '&bar'`, do not auto join channels whose names(generated from topics) partially match regex `&fo[o]` or `&bar` 141 | + `--ignore-topic 'fo[o]' bar`, short option `-I`, do not auto join channels whose topics match regex `fo[o]` or `bar` 142 | - `--ignore-bot`, ignore private messages with bots 143 | - IRC related options 144 | + `--irc-cert cert.pem`, TLS certificate for IRC over TLS. You may concatenate certificate+key, specify a single PEM file and omit `--irc-key`. Use plain IRC if neither --irc-cert nor --irc-key is specified. 145 | + `--irc-key key.pem`, TLS key for IRC over TLS. 146 | + `--irc-listen 127.1 ::1`, change IRC listen address to `127.1` and `::1`, overriding `--listen`. 147 | + `--irc-nicks ray ray1`, reverved nicks for clients. `SpecialUser` will not have these nicks. 148 | + `--irc-password pass`, set the connection password to `pass`. 149 | + `--irc-port 6669`, IRC server listen port. 150 | - Join mode, short option `-j` 151 | + `--join auto`, default: join the channel upon receiving the first message, no rejoin after issuing `/part` and receiving messages later 152 | + `--join all`: join all the channels 153 | + `--join manual`: no automatic join 154 | + `--join new`: like `auto`, but rejoin when new messages arrive even if after `/part` 155 | - `--listen 127.0.0.1`, short option `-l`, change IRC/HTTP/WebSocket listen address to `127.0.0.1`. 156 | - Server side log 157 | + `--logger-ignore '&test0' '&test1'`, list of ignored regex, do not log contacts/groups whose names match 158 | + `--logger-mask '/tmp/telegram/$channel/%Y-%m-%d.log'`, format of log filenames 159 | + `--logger-time-format %H:%M`, time format of server side log 160 | - `--mark-read`, when to `mark_read` private messages from users 161 | + `always`, `mark_read` all messages 162 | + `reply`, default: `mark_read` when sending messages to the peer 163 | + `never`, never 164 | - `--paste-wait`, PRIVMSG lines will be hold for up to `$paste_wait` seconds, lines in this interval will be packed to a multiline message 165 | - `--sasl-password pass`, set the SASL password to `pass`. 166 | - `--special-channel-prefix`, choices: `&`, `!`, `#`, `##`, prefix for SpecialChannel. [Quassel](quassel-irc.org) does not seem to support channels with prefixes `&`, `--special-channel-prefix '##'` to make Quassel happy 167 | - Telegram related options 168 | + `--tg-phone`, phone number 169 | + `--tg-api-id` 170 | + `--tg-api-hash` 171 | + `--tg-session telegramircd`, session filename. 172 | + `--tg-session-dir .`, where to save session file 173 | 174 | See [telegramircd.service](example_services/telegramircd.service) for a template of `/etc/systemd/system/telegramircd.service`. Change `User=` and `Group=`. Change the `User=` and `Group=` fields. 175 | 176 | ## Demo 177 | 178 | ![](https://maskray.me/static/2016-05-07-telegramircd/telegramircd.jpg) 179 | 180 | ## Known issues 181 | 182 | - Sometimes `struct.error: required argument is not an integer` when calling `self.channel_get_participants(channel)` 183 | -------------------------------------------------------------------------------- /README.zhs.md: -------------------------------------------------------------------------------- 1 | # telegramircd [![IRC](https://img.shields.io/badge/IRC-freenode-yellow.svg)](https://webchat.freenode.net/?channels=wechatircd) [![Telegram](https://img.shields.io/badge/chat-Telegram-blue.svg)](https://t.me/wechatircd) [![Gitter](https://img.shields.io/badge/chat-Gitter-753a88.svg)](https://gitter.im/wechatircd/wechatircd) 2 | 3 | telegramircd类似于bitlbee,可以用IRC客户端收发Telegram消息。 4 | 5 | telegramircd使用[Telethon](https://github.com/LonamiWebs/Telethon)和Telegram服务器通信。 6 | 7 | ## 安装 8 | 9 | - `git clone https://github.com/MaskRay/telegramircd && cd telegramircd` 10 | - python >= 3.5 11 | - libmagic 12 | - `pip3 install -r requirements.txt` 13 | 14 | 创建一个Telegram App。 15 | 16 | - 访问,创建App,获取`app_id, app_hash`。 17 | - 更新`config`:修改`tg-api-id, tg-api-hash, tg-phone`,把`tg-session-dir`改成存储`telegramircd.session`的目录(默认为当前目录,session文件首次登录时创建) 18 | - `./telegramircd.py -c config` 19 | 20 | ### Arch Linux 21 | 22 | `git clone`和`pip3 install -r requirements.txt`两步可以换成如下命令: 23 | 24 | - 安装`aur/telegramircd-git`(依賴`aur/python-telethon`。您也可以安裝`archlinuxcn/python-telethon`) 25 | - 服务器可执行文件为`/usr/bin/telegramircd` 26 | 27 | systemd service模板安装在`/lib/systemd/system/telegramircd.service`,可以据此创建`/etc/systemd/system/telegramircd.service`。注意修改`User=` `Group=`为安装`telethon`包的用户。运行`systemctl start telegramircd`。 28 | 29 | ## 运行telegramircd 30 | 31 | `telegramircd.py`默认监听127.0.0.1:6669 (IRC, `irc-listen, irc-port`)和127.0.0.1:9000 (HTTPS + WebSocket over TLS, `http-url`)。 32 | 33 | 用IRC客户端连接,会自动加入`+telegram`频道。首次登录需要输入`/oper a $login_code`,其中`$login_code`会以短信息形式发送到手机。如果启用了两步认证,需要输入`/oper a $password`。登录后,`$tg_session.session`会保存在`$tg_session_dir`,以后登录就不需要login code了。 34 | 35 | 如果你在非本机运行,建议配置IRC over TLS,设置IRC connection password,添加这些选项:`--irc-cert /path/to/irc.key --irc-key /path/to/irc.cert --irc-password yourpassword`。 36 | 37 | 可以把HTTPS私钥证书用作IRC over TLS私钥证书。使用WeeChat的话,如果觉得让WeeChat信任证书比较麻烦(gnutls会检查hostname),可以用: 38 | ``` 39 | /set irc.server.telegram.ssl on 40 | /set irc.server.telegram.ssl_verify off 41 | /set irc.server.telegram.password yourpassword 42 | ``` 43 | 44 | ### 用HTTPS伺服文件链接 45 | 46 | 加上额外一些选项`--http-key /etc/telegramircd/key.pem --http-cert /etc/telegramircd/cert.pem --http-url https://127.1:9003`。文件链接就会显示为`https://127.1:9003/document/$id`. 47 | 48 | 你需要把创建CA certificate/key,并用它签署另一个certificate/key。 49 | 50 | ```zsh 51 | openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key.pem -out ca.cert.pem -days 9999 -subj '/CN=127.0.0.1' 52 | openssl req -new -newkey rsa:2048 -nodes -keyout key.pem -subj '/CN=127.0.0.1' | 53 | openssl x509 -req -out cert.pem -CAkey ca.key.pem -CA ca.cert.pem -set_serial 2 -days 9999 -extfile <( 54 | printf "subjectAltName = IP:127.0.0.1, DNS:localhost") 55 | ``` 56 | 57 | Chrome/Chromium 58 | 59 | - 访问`chrome://settings/certificates`,导入`ca.cert.pem`,在Authorities标签页选择该证书,Edit->Trust this certificate for identifying websites. 60 | - 安装Switcheroo Redirector扩展,把重定向至。 61 | 62 | IP或域名必须匹配`subjectAlternativeName`。Chrome从版本58起不再支持用证书中的`commonName`匹配IP/域名,参见。 63 | 64 | Firefox 65 | 66 | - 安装Redirector扩展,重定向js,设置`Applies to: Main window (address bar), Scripts`。 67 | - 访问重定向后的js URL,报告Your connection is not secure,Advanced->Add Exception->Confirm Security Exception 68 | 69 | ## 使用 70 | 71 | - 访问,会自动发起WebSocket连接。若打开多个,只有第一个生效 72 | - IRC客户端连接127.1:6669,会发现自动加入了`+telegram` channel 73 | 74 | 在`+telegram`发信并不会群发,只是为了方便查看有哪些朋友。在`+telegram` channel可以执行一些命令: 75 | 76 | - `help`,帮助 77 | - `status`,已获取的mutual friend、群列表 78 | - `eval $expr`。例如: 79 | ``` 80 | eval client.peer_id2special_room 81 | eval client.peer_id2special_user 82 | ``` 83 | 84 | 服务器只能和一个帐号绑定,但支持多个IRC客户端。 85 | 86 | ## IRC功能 87 | 88 | - 对于没有`username`的用户,显示中文姓名时姓在前 89 | - 标准IRC channel名以`#`开头 90 | - Telegram chat/channel名以`&`开头,根据title生成。`SpecialChannel#update` 91 | - 联系人mode为`+v`(voice,通常用`+`前缀标识)。`SpecialChannel#update_detail` 92 | - 管理员mode为`+o`(op,通常用`@`前缀标识) 93 | - 多行消息:`!m line0\nline1\nline2` 94 | - 回复12:34:SS的消息:`@1234 !m multi\nline\nreply` 95 | - 回复12:34:56的消息:`!m @123456 multi\nline\nreply` 96 | - 回复Telegram channel/chat倒数第二条消息(自己的消息不计数):`@2 reply` 97 | - 粘贴检测。待发送消息延迟0.1秒发送,期间收到的所有行合并为一个多行消息发送 98 | 99 | `!m `, `@3 `, `nick: `可以任意安排顺序。 100 | 101 | 对于, 默认的anti-flood机制会让发出去的两条消息间隔至少2秒。禁用该机制使粘贴检测生效: 102 | ``` 103 | /set irc.server.telegram.anti_flood_prio_high 0 104 | ``` 105 | 106 | 若客户端启用IRC 3.1 3.2的`server-time`扩展,`wechatircd.py`会在发送的消息中包含 网页版获取的时间戳。客户端显示消息时时间就会和服务器收到的消息的时刻一致。参见。参见查看IRCv3的客户端支持情况。 107 | 108 | WeeChat配置如下: 109 | ``` 110 | /set irc.server_default.capabilities "account-notify,away-notify,cap-notify,multi-prefix,server-time,znc.in/server-time-iso,znc.in/self-message" 111 | ``` 112 | 113 | 支持的IRC命令: 114 | 115 | - `/cap`,列出支持的capabilities 116 | - `/dcc send $nick/$channel $filename`, 发送图片或文件。借用了IRC客户端的`/dcc send`命令,但含义不同,参见 117 | - `/invite $nick [$channel]`,邀请用户加入群 118 | - `/kick $nick`,删除群成员,群主才有效。由于网页版限制,可能收不到群成员变更的消息 119 | - `/kill $nick [$reason]`,断开指定客户端的连接 120 | - `/list`,列出所有群 121 | - `/mode +m`, `--join new`模式下防止自动重新join。用`/mode -m`撤销 122 | - `/names`, 更新当前群成员列表 123 | - `/part [$channel]`的IRC原义为离开channel,这里表示当前IRC会话中不再接收该群的消息。不用担心,telegramircd并没有主动退出群的功能 124 | - `/query $nick`,打开和`$nick`聊天的窗口 125 | - `/topic topic`修改群标题。因为IRC不支持channel改名,实现为离开原channel并加入新channel 126 | - `/who $channel`,查看群的成员列表 127 | 128 | ## 服务器选项 129 | 130 | - `--config`, short option `-c`,配置文件路径,参见[config](config) 131 | - HTTP/WebSocket相关选项 132 | + `--http-cert cert.pem`,HTTPS/WebSocketTLS的证书。你可以把证书和私钥合并为一个文件,省略`--http-key`选项。如果`--http-cert`和`--http-key`均未指定,使用不加密的HTTP 133 | + `--http-key key.pem`,HTTPS/WebSocket的私钥 134 | + `--http-listen 127.1 ::1`,HTTPS/WebSocket监听地址设置为`127.1`和`::1`,overriding `--listen` 135 | + `--http-port 9000`,HTTPS/WebSocket监听端口设置为9000 136 | + `--http-root .`, 存放`injector.js`的根目录 137 | - 指定不自动加入的群名,用于补充join mode 138 | + `--ignore 'fo[o]' bar`,channel名部分匹配正则表达式`fo[o]`或`bar` 139 | - `--ignore-bot`, 忽略与bot的私聊消息 140 | - IRC相关选项 141 | + `--irc-cert cert.pem`,IRC over TLS的证书。你可以把证书和私钥合并为一个文件,省略`--irc-key`选项。如果`--irc-cert`和`--irc-key`均未指定,使用不加密的IRC 142 | + `--irc-key key.pem`,IRC over TLS的私钥 143 | + `--irc-listen 127.1 ::1`,IRC over TLS监听地址设置为`127.1`和`::1`,overriding `--listen` 144 | + `--irc-nicks ray ray1`,给客户端保留的nick。`SpecialUser`不会占用这些名字 145 | + `--irc-password pass`,IRC connection password设置为`pass` 146 | + `--irc-port 6667`,IRC监听端口 147 | - Join mode,短选项`-j` 148 | + `--join auto`,默认:收到某个群第一条消息后自动加入,如果执行过`/part`命令了,则之后收到消息不会重新加入 149 | + `--join all`:加入所有channel 150 | + `--join manual`:不自动加入 151 | + `--join new`:类似于`auto`,但执行`/part`命令后,之后收到消息仍自动加入 152 | - `--listen 127.0.0.1`,`-l`,IRC/HTTP/WebSocket监听地址设置为`127.0.0.1` 153 | - 服务端日志 154 | + `--logger-ignore '&test0' '&test1'`,不记录部分匹配指定正则表达式的朋友/群日志 155 | + `--logger-mask '/tmp/wechat/$channel/%Y-%m-%d.log'`,日志文件名格式 156 | + `--logger-time-format %H:%M`,日志单条消息的时间格式 157 | - `--mark-read`, 自动`mark_read`私聊消息 158 | - `--paste-wait`,待发送消息延迟0.1秒发送,期间收到的所有行合并为一个多行消息发送 159 | - `--special-channel-prefix`,选项:`&`, `!`, `#`, `##`,SpecialChannel的前缀。[Quassel](quassel-irc.org)似乎不支持channel前缀`&`,指定`--special-channel-prefix '##'`让Quassel高兴 160 | - Telegram相关选项 161 | + `--tg-phone`, phone number 162 | + `--tg-api-id` 163 | + `--tg-api-hash` 164 | + `--tg-session telegramircd`,session文件名 165 | + `--tg-session-dir .`,保存session文件的目录 166 | 167 | [telegramircd.service](example_services/telegramircd.service)是`/etc/systemd/system/telegramircd.service`的模板,修改其中的`User=` and `Group=`。 168 | 169 | ## Demo 170 | 171 | ![](https://maskray.me/static/2016-05-07-telegramircd/telegramircd.jpg) 172 | 173 | ## 已知问题 174 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | # See https://pypi.python.org/pypi/ConfigArgParse for syntax 2 | 3 | # Phone number. No leading + 4 | tg-phone: 10123456789 5 | # App api_id from https://my.telegram.org/apps 6 | tg-api-id: 00000 7 | # App api_hash. No single quotation marks 8 | tg-api-hash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 9 | # Session name (account credential, login code is not needed for future logins) 10 | tg-session: telegramircd 11 | # Where to store $tg_session.session . Make sure this directory is writable (especially when you use a systemd service) 12 | tg-session-dir: . 13 | 14 | # HTTP/WebSocket related options 15 | # Use HTTPS to server links if at least one of http-cert or http-key is specified 16 | #http-cert: /etc/telegramircd/cert.pem 17 | #http-key: /etc/telegramircd/key.pem 18 | http-listen: [127.0.0.1, ::1] 19 | http-port: 9003 20 | # Display document links as http://127.1:9003/document/$id 21 | http-url: http://127.1:9003 22 | 23 | #ignore: [channel_name_regex0, channel_name_regex1] 24 | # uncomment the next line if you do not want to receive private messages with bots 25 | #ignore-bot 26 | #ignore-topic: [group_name_regex0] 27 | 28 | # IRC related options 29 | # Use IRC over TLS if at least one of irc-cert or irc-key is specified 30 | #irc-cert: /etc/telegramircd/irc-cert.pem 31 | #irc-key: /etc/telegramircd/irc-key.pem 32 | irc-listen: [127.0.0.1, ::1] 33 | # reserved nicks for clients 34 | #irc-nicks: [ray] 35 | # IRC connection password 36 | #irc-password: 37 | irc-port: 6669 38 | 39 | # join mode 40 | #join: new 41 | 42 | # logger 43 | #logger-ignore: 44 | #logger-mask: /tmp/telegramircd/$channel/%Y-%m-%d.log 45 | #logger-time-format: %H:%M 46 | 47 | # when to mark_read private messages from users, choices: always, reply, never 48 | #mark-read: reply 49 | 50 | # SASL PLAIN password (as an alternative to --irc-password) 51 | #sasl-password: 52 | -------------------------------------------------------------------------------- /example_services/telegramircd: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | USER=nobody 4 | 5 | description="IRC server capable of controlling Telegram" 6 | 7 | depend(){ 8 | need net logger 9 | } 10 | 11 | start() { 12 | ebegin "Starting telegramircd" 13 | start-stop-daemon --wait 1000 -b --start \ 14 | --exec /usr/bin/python3 -u "$USER" \ 15 | --make-pidfile --pidfile /run/telegramircd.pid \ 16 | -- /usr/bin/telegramircd -c /etc/telegramircd/config 17 | eend $? 18 | } 19 | 20 | stop() { 21 | ebegin "Stopping telegramircd" 22 | start-stop-daemon --stop --exec /usr/bin/telegramircd \ 23 | --pidfile /run/telegramircd.pid 24 | eend $? 25 | } 26 | -------------------------------------------------------------------------------- /example_services/telegramircd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=IRC server capable of controlling Telegram 3 | Documentation=https://github.com/MaskRay/telegramircd 4 | After=network.target 5 | 6 | [Service] 7 | User=nobody 8 | Group=nobody 9 | ExecStart=/usr/bin/telegramircd -c /etc/telegramircd/config 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | python-magic 3 | ipdb 4 | ipython 5 | configargparse 6 | https://github.com/LonamiWebs/Telethon/archive/sync-stale.zip 7 | -------------------------------------------------------------------------------- /telegramircd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from configargparse import ArgParser, Namespace 3 | #from ipdb import set_trace as bp 4 | from collections import deque 5 | from datetime import datetime, timezone 6 | from itertools import chain 7 | import subprocess 8 | 9 | from telethon import TelegramClient 10 | from telethon.errors import SessionPasswordNeededError 11 | import telethon.tl as tl 12 | #from telethon.tl.functions.contacts import GetContactsRequest 13 | import telethon.tl.types as tg_types 14 | import telethon.tl.functions.contacts 15 | import telethon.tl.functions.messages 16 | 17 | import aiohttp.web, asyncio, base64, inspect, json, logging.handlers, magic, os, pprint, random, re, \ 18 | shlex, signal, socket, ssl, string, sys, tempfile, time, traceback, uuid, weakref 19 | 20 | logger = logging.getLogger('telegramircd') 21 | im_name = 'Telegram' 22 | capabilities = set(['away-notify', 'draft/message-tags', 'echo-message', 'multi-prefix', 'sasl', 'server-time']) # http://ircv3.net/irc/ 23 | options = None 24 | server = None 25 | web = None 26 | 27 | 28 | def debug(msg, *args): 29 | logger.debug(msg, *args) 30 | 31 | 32 | def info(msg, *args): 33 | logger.info(msg, *args) 34 | 35 | 36 | def warning(msg, *args): 37 | logger.warning(msg, *args) 38 | 39 | 40 | def error(msg, *args): 41 | logger.error(msg, *args) 42 | 43 | 44 | class ExceptionHook(object): 45 | instance = None 46 | 47 | def __call__(self, *args, **kwargs): 48 | if self.instance is None: 49 | from IPython.core import ultratb 50 | self.instance = ultratb.VerboseTB(call_pdb=True) 51 | return self.instance(*args, **kwargs) 52 | 53 | 54 | class TelegramCliFail(Exception): 55 | def __init__(self, *msg): 56 | super().__init__(*msg) 57 | 58 | ### HTTP server 59 | 60 | class Web(object): 61 | def __init__(self, tls): 62 | global web 63 | web = self 64 | self.tls = tls 65 | self.id2media = {} 66 | self.id2message = {} 67 | self.webpage_id2sender_to = {} 68 | self.recent_messages = deque() 69 | self.proc = None 70 | self.authorized = False 71 | self.two_step = False 72 | 73 | async def handle_media(self, typ, request): 74 | id = re.sub(r'\..*', '', request.match_info.get('id')) 75 | if id not in self.id2media: 76 | return aiohttp.web.Response(status=404, text='Not Found') 77 | try: 78 | media, filename = self.id2media[id] 79 | if not filename: 80 | try: 81 | with tempfile.NamedTemporaryFile(dir=options.tg_media_dir, suffix='.blob') as temp: 82 | filename = temp.name 83 | self.proc.download_media(media, filename) 84 | self.id2media[id] = (media, filename) 85 | except asyncio.TimeoutError: 86 | return aiohttp.web.Response(status=504, text='I used to live in 504A') 87 | except TelegramCliFail as ex: 88 | return aiohttp.web.Response(status=404, text=ex.args[0]) 89 | with open(filename, 'rb') as f: 90 | mime = None 91 | if sys.platform == 'linux': 92 | try: 93 | mime = subprocess.check_output(['xdg-mime', 'query', 'filetype', filename], 94 | stdin=subprocess.DEVNULL).decode().strip() 95 | except: 96 | pass 97 | if mime is None: 98 | mime = magic.from_file(filename, mime=True) 99 | return aiohttp.web.Response(body=f.read(), headers={'Content-Type': mime}) 100 | except Exception as ex: 101 | return aiohttp.web.Response(status=500, text=str(ex)) 102 | 103 | async def handle_document(self, request): 104 | return await self.handle_media('document', request) 105 | 106 | def run_telethon(self): 107 | if self.proc: 108 | self.proc.disconnect() 109 | self.proc = TelegramClient(options.tg_session, options.tg_api_id, options.tg_api_hash) 110 | try: 111 | self.proc.connect() 112 | except: 113 | error('Failed to connect to Telegram server') 114 | sys.exit(2) 115 | self.authorized = self.proc.is_user_authorized() 116 | if not self.authorized and not options.tg_phone: 117 | error('Not authorized. Please set --tg-phone') 118 | sys.exit(2) 119 | self.proc.add_event_handler(server.on_telegram_update) 120 | 121 | async def restart_telegram_cli(self): 122 | traceback.print_stack() 123 | if self.proc: 124 | try: 125 | self.proc.disconnect() 126 | time.sleep(1) 127 | except: 128 | pass 129 | os.execl(sys.executable, sys.executable, *sys.argv) 130 | 131 | def start(self, listens, port, loop): 132 | self.loop = loop 133 | self.app = aiohttp.web.Application() 134 | self.app.router.add_route('GET', '/document/{id}', self.handle_document) 135 | self.handler = self.app.make_handler() 136 | self.srv = [] 137 | for i in listens: 138 | self.srv.append(loop.run_until_complete( 139 | loop.create_server(self.handler, i, port, ssl=self.tls))) 140 | self.run_telethon() 141 | if self.authorized: 142 | self.init() 143 | 144 | #async def poll(): 145 | # while 1: 146 | # await asyncio.sleep(options.tg_poll_interval) 147 | # for peer_id in options.tg_poll_channels: 148 | # self.proc.stdin.write('history channel#{} {}\n'.format(peer_id, options.tg_poll_limit).encode()) 149 | 150 | #if options.telegram_cli_poll_channels: 151 | # self.poll = loop.create_task(poll()) 152 | 153 | def stop(self): 154 | self.proc.disconnect() 155 | for i in self.srv: 156 | i.close() 157 | self.loop.run_until_complete(i.wait_closed()) 158 | self.loop.run_until_complete(self.app.shutdown()) 159 | self.loop.run_until_complete(self.handler.shutdown(3)) 160 | self.loop.run_until_complete(self.app.cleanup()) 161 | for _, filename in self.id2media.values(): 162 | if filename: 163 | try: 164 | os.unlink(filename) 165 | except: 166 | pass 167 | 168 | def append_history(self, record): 169 | if len(self.recent_messages) >= 10000: 170 | msg = self.recent_messages.popleft() 171 | del self.id2message[msg['id']] 172 | self.recent_messages.append(record) 173 | self.id2message[record['id']] = record 174 | 175 | # TODO admin channel.update_admins(members) 176 | def channel_get_participants(self, channel): 177 | tg_users = [] 178 | offset = 0 179 | while True: 180 | participants = self.proc(tl.functions.channels.GetParticipantsRequest( 181 | channel.tg_room, 182 | tl.types.ChannelParticipantsSearch(''), 183 | offset, 184 | 100, 185 | 0, # hash 186 | )) 187 | if not participants.users: break 188 | tg_users.extend(participants.users) 189 | offset += len(participants.users) 190 | channel.update_members(tg_users) 191 | 192 | def channel_invite(self, client, channel, user): 193 | try: 194 | if channel.is_type(tl.types.PeerChannel): 195 | self.proc(tl.functions.channels.InviteToChannelRequest( 196 | channel.peer.channel_id, 197 | [self.proc.get_input_entity(user.user_id)] 198 | )) 199 | elif channel.is_type(tl.types.PeerChat): 200 | self.proc(tl.functions.messages.AddChatUserRequest( 201 | channel.peer.chat_id, 202 | self.proc.get_input_entity(user.user_id), 203 | 0, 204 | )) 205 | except telethon.errors.rpcerrorlist.UserAlreadyParticipantError: 206 | client.err_useronchannel(user.nick, channel.name) 207 | except telethon.errors.rpcbaseerrors.RPCError: 208 | pass 209 | 210 | async def channel_set_admin(self, client, channel, user, ty): 211 | if channel.is_type(tl.types.PeerChannel): 212 | try: 213 | await self.send_command('channel_set_admin {} {} {}'.format(channel.peer, user.peer, ty)) 214 | except (asyncio.TimeoutError, TelegramCliFail): 215 | client.err_chanoprivsneeded(channel.name) 216 | else: 217 | if ty == 0: 218 | self.unset_cmode(user, 'h') 219 | self.unset_cmode(user, 'o') 220 | elif ty == 1: 221 | self.set_cmode(user, 'h') 222 | channel.halfop_event(user) 223 | elif ty == 2: 224 | self.set_cmode(user, 'o') 225 | channel.op_event(user) 226 | else: 227 | client.err_chanoprivsneeded(channel.name) 228 | 229 | def channel_kick(self, client, channel, user): 230 | try: 231 | if channel.is_type(tl.types.PeerChannel): 232 | pass 233 | elif channel.is_type(tl.types.PeerChat): 234 | self.proc(tl.functions.messages.DeleteChatUserRequest( 235 | channel.peer.chat_id, 236 | self.proc.get_input_entity(user.user_id), 237 | )) 238 | except telethon.errors.rpcerrorlist.UserNotParticipantError: 239 | client.err_usernotinchannel(user.nick, channel.name) 240 | except telethon.errors.rpcbaseerrors.RPCError: 241 | pass 242 | 243 | def chat_get_full(self, channel): 244 | chatfull = self.proc(tl.functions.messages.GetFullChatRequest( 245 | channel.tg_room.id 246 | )) 247 | channel.update_members(chatfull.users) 248 | 249 | def contact_list(self): 250 | contacts = self.proc(tl.functions.contacts.GetContactsRequest(0)) 251 | for tg_user in contacts.users: 252 | server.ensure_special_user(tg_user.id, tg_user) 253 | 254 | def channel_list(self): 255 | # https://github.com/LonamiWebs/Telethon/wiki/Retrieving-all-dialogs 256 | last_date = None 257 | chunk_size = 20 258 | while True: 259 | debug('channel_list %r', last_date) 260 | r = self.proc(tl.functions.messages.GetDialogsRequest( 261 | offset_date=last_date, 262 | offset_id=0, 263 | offset_peer=tl.types.InputPeerEmpty(), 264 | limit=chunk_size, 265 | hash=0 266 | )) 267 | for tg_user in r.users: 268 | server.ensure_special_user(tg_user.id, tg_user) 269 | # Channel & Chat & ChatForbidden & ... 270 | for tg_room in r.chats: 271 | if isinstance(tg_room, (tl.types.Channel, tl.types.Chat)): 272 | server.ensure_special_room(tg_room.id, tg_room) 273 | if not r.messages: break 274 | date = min(msg.date for msg in r.messages) 275 | if date == last_date: break 276 | last_date = date 277 | time.sleep(0.7) 278 | 279 | def channel_message_get(self, channel, id): 280 | messages = self.proc(tl.functions.channels.GetMessagesRequest( 281 | self.proc.get_input_entity(channel.peer), [id])).messages 282 | return messages[0] if messages else None 283 | 284 | def message_get(self, id): 285 | messages = self.proc(tl.functions.messages.GetMessagesRequest([id])).messages 286 | return messages[0] if messages else None 287 | 288 | def get_self(self): 289 | data = self.proc.get_me() 290 | server.user_id = data.id 291 | 292 | def init(self): 293 | try: 294 | web.get_self() 295 | web.channel_list() 296 | web.contact_list() 297 | except Exception as ex: 298 | traceback.print_exc() 299 | 300 | def mark_read(self, peer, max_id): 301 | self.proc.send_read_acknowledge(peer, max_id=max_id) 302 | 303 | def channel_members(self, channel): 304 | try: 305 | if channel.is_type(tl.types.PeerChannel): 306 | self.channel_get_participants(channel) 307 | elif channel.is_type(tl.types.PeerChat): 308 | self.chat_get_full(channel) 309 | except Exception as ex: 310 | error('channel_members %r', channel) 311 | traceback.print_exc() 312 | 313 | def send_file(self, client, peer, filename, body): 314 | with tempfile.TemporaryDirectory() as directory: 315 | filename = os.path.join(directory, filename) 316 | try: 317 | with open(filename, 'wb') as f: 318 | f.write(body) 319 | f.flush() 320 | self.proc.send_file(peer, f.name) 321 | except telethon.errors.rpcbaseerrors.RPCError: 322 | client.err_cannotsendtochan(peer.nick, 'Cannot send the file') 323 | os.unlink(filename) 324 | 325 | def msg(self, client, to, text, reply_to=None): 326 | try: 327 | msg = self.proc.send_message(to.peer, text, reply_to=reply_to) 328 | irc_log(to, to, msg.date, client, msg.message) 329 | except telethon.errors.rpcbaseerrors.RPCError: 330 | traceback.print_exc() 331 | if isinstance(to.peer, SpecialChannel): 332 | client.err_nosuchchannel(to.name) 333 | elif isinstance(to.peer, SpecialUser): 334 | client.err_nosuchnick(to.nick) 335 | 336 | ### IRC utilities 337 | 338 | def irc_lower(s): 339 | irc_trans = str.maketrans(string.ascii_uppercase + '[]\\^', 340 | string.ascii_lowercase + '{}|~') 341 | return s.translate(irc_trans) 342 | 343 | 344 | # loose 345 | def irc_escape(s): 346 | s = re.sub(r',', '.', s) # `,` is used as seprator in IRC messages 347 | s = re.sub(r'&?', '', s) # chatroom name may include `&` 348 | s = re.sub(r'<[^>]*>', '', s) # remove emoji 349 | return re.sub(r'[^-\w$%^*()=./]', '', s) 350 | 351 | 352 | def irc_escape_nick(s): 353 | return re.sub('^[&#!+:]*', '', irc_escape(s)) 354 | 355 | 356 | def process_text(to, text): 357 | # !m 358 | # @(\d\d)(\d\d)(\d\d)? 359 | reply = None 360 | multiline = False 361 | while 1: 362 | cont = False 363 | in_channel = isinstance(to, SpecialChannel) 364 | match = re.match(r'@(\d\d)(\d\d)(\d\d)? ', text) 365 | if match: 366 | cont = True 367 | text = text[match.end():] 368 | HH, MM, SS = int(match.group(1)), int(match.group(2)), match.group(3) 369 | if SS is not None: 370 | SS = int(SS) 371 | for msg in reversed(web.recent_messages): 372 | if (msg['to'] is to and msg['from'] is not server) if in_channel \ 373 | else (msg['from'] is to and msg['to'] is server): 374 | # UTC -> local 375 | dt = datetime.fromtimestamp(msg['date'].timestamp()) 376 | if dt.hour == HH and dt.minute == MM and (SS is None or dt.second == SS): 377 | reply = msg['id'] 378 | break 379 | match = re.match(r'@(\d{1,2}) ', text) 380 | if match: 381 | cont = True 382 | text = text[match.end():] 383 | which = int(match.group(1)) 384 | if which > 0: 385 | for msg in reversed(web.recent_messages): 386 | if not msg['inferred']: 387 | if (msg['to'] is to and msg['from'] is not server) if in_channel \ 388 | else (msg['from'] is to and msg['to'] is server): 389 | which -= 1 if 'media' in msg else len(msg['message'].splitlines()) 390 | if which <= 0: 391 | reply = msg['id'] 392 | break 393 | if text.startswith('!m '): 394 | cont = True 395 | text = text[3:] 396 | multiline = True 397 | if not cont: break 398 | if multiline: 399 | text = text.replace('\\n', '\n') 400 | 401 | # nick: -> @username 402 | at = '' 403 | i = 0 404 | while i < len(text) and text[i] != ' ': 405 | j = text.find(': ', i) 406 | if j == -1: break 407 | nick = text[i:j] 408 | if not server.has_special_user(nick): break 409 | at += '@'+server.get_special_user(nick).preferred_nick()+' ' 410 | i = j+2 411 | return reply, at + text[i:] 412 | 413 | 414 | def irc_log(where, peer, local_time, sender, line): 415 | if options.logger_mask is None: 416 | return 417 | for regex in options.logger_ignore or []: 418 | if re.search(regex, peer.name): 419 | return 420 | filename = local_time.strftime(options.logger_mask.replace('$channel', peer.nick)) 421 | time_str = local_time.strftime(options.logger_time_format.replace('$channel', peer.nick)) 422 | if where.log_file is None or where.log_file.name != filename: 423 | if where.log_file is not None: 424 | where.log_file.close() 425 | os.makedirs(os.path.dirname(filename), exist_ok=True) 426 | where.log_file = open(filename, 'a') 427 | where.log_file.write('{}\t{}\t{}\n'.format( 428 | time_str, sender.nick, 429 | re.sub(r'\x03\d+(,\d+)?|[\x02\x0f\x1d\x1f\x16]', '', line))) 430 | where.log_file.flush() 431 | 432 | 433 | def irc_privmsg(client, command, to, text): 434 | if command == 'PRIVMSG' and client.ctcp(to.peer, text): 435 | return 436 | 437 | def send(): 438 | web.msg(client, to, to.privmsg_text, to.privmsg_reply) 439 | to.last_text_by_client[client] = to.privmsg_text 440 | to.privmsg_reply = None 441 | to.privmsg_text = '' 442 | 443 | async def wait(seq): 444 | await asyncio.sleep(options.paste_wait) 445 | if to.privmsg_seq == seq: 446 | send() 447 | 448 | reply, text = process_text(to, text) 449 | if reply and len(to.privmsg_text): 450 | send() 451 | to.privmsg_reply = reply 452 | to.privmsg_seq = to.privmsg_seq+1 453 | if len(to.privmsg_text): 454 | to.privmsg_text += '\n' 455 | to.privmsg_text += text 456 | server.loop.create_task(wait(to.privmsg_seq)) 457 | 458 | ### Commands 459 | 460 | cmd_use_case = {} 461 | 462 | 463 | def registered(v): 464 | def wrapped(fn): 465 | cmd_use_case[fn.__name__] = v 466 | return fn 467 | return wrapped 468 | 469 | 470 | class Command: 471 | @staticmethod 472 | @registered(7) 473 | def authenticate(client, arg): 474 | if arg.upper() == 'PLAIN': 475 | client.write('AUTHENTICATE +') 476 | return 477 | if not (client.nick and client.user): 478 | return 479 | try: 480 | if base64.b64decode(arg).split(b'\0')[2].decode() == options.sasl_password: 481 | client.authenticated = True 482 | client.reply('900 {} {} {} :You are now logged in as {}', client.nick, client.user, client.nick, client.nick) 483 | client.reply('903 {} :SASL authentication successful', client.nick) 484 | client.register() 485 | else: 486 | client.reply('904 {} :SASL authentication failed', client.nick) 487 | except: 488 | client.reply('904 {} :SASL authentication failed', client.nick) 489 | 490 | @staticmethod 491 | def away(client): 492 | pass 493 | 494 | @staticmethod 495 | @registered(7) 496 | def cap(client, *args): 497 | if not args: return 498 | comm = args[0].lower() 499 | if comm == 'list': 500 | client.reply('CAP * LIST :{}', ' '.join(client.capabilities)) 501 | elif comm == 'ls': 502 | client.reply('CAP * LS :{}', ' '.join(capabilities)) 503 | elif comm == 'req': 504 | enabled, disabled = set(), set() 505 | for name in args[1].split(): 506 | if name.startswith('-'): 507 | disabled.add(name[1:]) 508 | else: 509 | enabled.add(name) 510 | client.capabilities = (capabilities & enabled) - disabled 511 | client.reply('CAP * ACK :{}', ' '.join(client.capabilities)) 512 | 513 | @staticmethod 514 | def info(client): 515 | client.rpl_info('{} users', len(server.nicks)) 516 | client.rpl_info('{} {} users', im_name, len(client.nick2special_user)) 517 | client.rpl_info('{} {} rooms', im_name, len(client.name2special_room)) 518 | 519 | @staticmethod 520 | def invite(client, nick, channelname): 521 | if client.is_in_channel(channelname): 522 | server.get_channel(channelname).on_invite(client, nick) 523 | else: 524 | client.err_notonchannel(channelname) 525 | 526 | @staticmethod 527 | def ison(client, *nicks): 528 | client.reply('303 {} :{}', client.nick, 529 | ' '.join(nick for nick in nicks 530 | if server.has_nick(nick))) 531 | 532 | @staticmethod 533 | def join(client, *args): 534 | if not args: 535 | self.err_needmoreparams('JOIN') 536 | else: 537 | arg = args[0] 538 | if arg == '0': 539 | channels = list(client.channels.values()) 540 | for channel in channels: 541 | channel.on_part(client, channel.name) 542 | else: 543 | JOINCHAT = 'https://t.me/joinchat/' 544 | for channelname in arg.split(','): 545 | # Join via joinchat link 546 | if channelname.startswith(JOINCHAT): 547 | web.proc(tl.functions.messages.ImportChatInviteRequest(channelname[len(JOINCHAT):])) 548 | else: 549 | if server.has_special_room(channelname): 550 | server.get_special_room(channelname).on_join(client) 551 | else: 552 | try: 553 | server.ensure_channel(channelname).on_join(client) 554 | except ValueError: 555 | client.err_nosuchchannel(channelname) 556 | 557 | @staticmethod 558 | def kick(client, channelname, nick, reason=None): 559 | if client.is_in_channel(channelname): 560 | server.get_channel(channelname).on_kick(client, nick, reason) 561 | else: 562 | client.err_notonchannel(channelname) 563 | 564 | @staticmethod 565 | def kill(client, nick, reason=None): 566 | if not server.has_nick(nick): 567 | client.err_nosuchnick(nick) 568 | return 569 | user = server.get_nick(nick) 570 | if not isinstance(user, Client) or user == client: 571 | client.err_nosuchnick(nick) 572 | return 573 | user.disconnect(reason) 574 | 575 | @staticmethod 576 | def list(client, arg=None): 577 | if arg: 578 | channels = [] 579 | for channelname in arg.split(','): 580 | if server.has_channel(channelname): 581 | channels.append(server.get_channel(channelname)) 582 | else: 583 | web.init() 584 | channels = set(server.channels.values()) 585 | channels.update(server.name2special_room.values()) 586 | channels = list(channels) 587 | channels.sort(key=lambda ch: ch.name) 588 | for channel in channels: 589 | n = channel.n_members(client) 590 | if n == 0 and channel.is_type(tl.types.PeerChat): 591 | n = channel.tg_room.participants_count 592 | client.reply('322 {} {} {} :{}', client.nick, channel.name, n, channel.topic) 593 | client.reply('323 {} :End of LIST', client.nick) 594 | 595 | @staticmethod 596 | def lusers(client): 597 | client.reply('251 :There are {} users and {} {} users on 1 server', 598 | len(server.nicks), 599 | len(server.nick2special_user), 600 | im_name 601 | ) 602 | 603 | @staticmethod 604 | def mode(client, target, *args): 605 | if server.has_nick(target): 606 | if args: 607 | client.err_umodeunknownflag() 608 | else: 609 | client.rpl_umodeis(server.get_nick(target).mode) 610 | elif server.has_channel(target): 611 | server.get_channel(target).on_mode(client, *args) 612 | else: 613 | client.err_nosuchchannel(target) 614 | 615 | @staticmethod 616 | def motd(client): 617 | async def do(): 618 | try: 619 | async with aiohttp.ClientSession() as session: 620 | async with session.get('https://api.github.com/repos/MaskRay/telegramircd/commits') as resp: 621 | client.reply('375 {} :- {} Message of the Day -', client.nick, server.name) 622 | data = await resp.json() 623 | for x in data[:5]: 624 | client.reply('372 {} :- {} {} {}'.format(client.nick, x['sha'][:7], x['commit']['committer']['date'][:10], x['commit']['message'].replace('\n', '\\n'))) 625 | client.reply('376 {} :End of /MOTD command.', client.nick) 626 | except: 627 | pass 628 | server.loop.create_task(do()) 629 | 630 | @staticmethod 631 | def names(client, target): 632 | if not client.is_in_channel(target): 633 | client.err_notonchannel(target) 634 | return 635 | channel = server.get_channel(target) 636 | web.channel_members(channel) 637 | channel.on_names(client) 638 | 639 | @staticmethod 640 | @registered(7) 641 | def nick(client, *args): 642 | if len(options.irc_password) and not client.authenticated: 643 | client.err_passwdmismatch('NICK') 644 | return 645 | if not args: 646 | client.err_nonicknamegiven() 647 | return 648 | server.change_nick(client, args[0]) 649 | 650 | @staticmethod 651 | @registered(7) 652 | def oper(client, _, password): 653 | ok = False 654 | StatusChannel.instance.respond(client, 'Signing in...') 655 | if web.two_step: 656 | web.two_step = False 657 | ok = web.proc.sign_in(password=password) 658 | if not ok: 659 | StatusChannel.instance.respond(client, 'Wrong password. Please type /oper a $login_code; /oper a $password') 660 | else: 661 | try: 662 | ok = web.proc.sign_in(options.tg_phone, password) 663 | if not ok: 664 | StatusChannel.instance.respond(client, 'Wrong login code. Please type /oper a $login_code') 665 | except SessionPasswordNeededError: 666 | web.two_step = True 667 | StatusChannel.instance.respond(client, 'Two step verification enabled. Please type /oper a $password') 668 | if ok: 669 | StatusChannel.instance.respond(client, 'Authorized. Initializing...') 670 | web.init() 671 | 672 | @staticmethod 673 | def notice(client, *args): 674 | Command.notice_or_privmsg(client, 'NOTICE', *args) 675 | 676 | @staticmethod 677 | def part(client, arg, *args): 678 | partmsg = args[0] if args else None 679 | for channelname in arg.split(','): 680 | if client.is_in_channel(channelname): 681 | server.get_channel(channelname).on_part(client, partmsg) 682 | else: 683 | client.err_notonchannel(channelname) 684 | 685 | @staticmethod 686 | @registered(6) 687 | def pass_(client, password): 688 | if len(options.irc_password) and password == options.irc_password: 689 | client.authenticated = True 690 | client.register() 691 | 692 | @staticmethod 693 | @registered(7) 694 | def ping(client, *args): 695 | if not args: 696 | client.err_noorigin() 697 | return 698 | client.reply('PONG {} :{}', server.name, args[0]) 699 | 700 | @staticmethod 701 | @registered(7) 702 | def pong(client, *args): 703 | pass 704 | 705 | @staticmethod 706 | def privmsg(client, *args): 707 | Command.notice_or_privmsg(client, 'PRIVMSG', *args) 708 | 709 | @staticmethod 710 | @registered(7) 711 | def quit(client, *args): 712 | client.disconnect(args[0] if args else client.prefix) 713 | 714 | @staticmethod 715 | def squit(client, *args): 716 | client.err_unknowncommand('SQUIT') 717 | 718 | @staticmethod 719 | def stats(client, query): 720 | if len(query) == 1: 721 | if query == 'u': 722 | td = datetime.now() - server._boot 723 | client.reply('242 {} :Server Up {} days {}:{:02}:{:02}', 724 | client.nick, td.days, td.seconds // 3600, 725 | td.seconds // 60 % 60, td.seconds % 60) 726 | client.reply('219 {} {} :End of STATS report', client.nick, query) 727 | 728 | @staticmethod 729 | def summon(client, nick, msg): 730 | client.err_nologin(nick) 731 | 732 | @staticmethod 733 | def time(client): 734 | client.reply('391 {} {} :{}Z', client.nick, server.name, 735 | datetime.utcnow().isoformat()) 736 | 737 | @staticmethod 738 | def topic(client, channelname, new=None): 739 | if not client.is_in_channel(channelname): 740 | client.err_notonchannel(channelname) 741 | return 742 | server.get_channel(channelname).on_topic(client, new) 743 | 744 | @staticmethod 745 | def who(client, target): 746 | if server.has_channel(target): 747 | server.get_channel(target).on_who(client) 748 | elif server.has_nick(target): 749 | server.get_nick(target).on_who_member(client, server) 750 | client.reply('315 {} {} :End of WHO list', client.nick, target) 751 | 752 | @staticmethod 753 | def whois(client, *args): 754 | if not args: 755 | client.err_nonicknamegiven() 756 | return 757 | elif len(args) == 1: 758 | target = args[0] 759 | else: 760 | target = args[1] 761 | if server.has_nick(target): 762 | server.get_nick(target).on_whois(client) 763 | else: 764 | client.err_nosuchnick(target) 765 | return 766 | client.reply('318 {} {} :End of WHOIS list', client.nick, target) 767 | 768 | @classmethod 769 | def notice_or_privmsg(cls, client, command, *args): 770 | if not args: 771 | client.err_norecipient(command) 772 | return 773 | if len(args) == 1: 774 | client.err_notexttosend() 775 | return 776 | target = args[0] 777 | msg = args[1] 778 | # on name conflict, prefer to resolve user first 779 | if server.has_nick(target): 780 | user = server.get_nick(target) 781 | if isinstance(user, Client): 782 | user.write(':{} PRIVMSG {} :{}'.format(client.prefix, user.nick, msg)) 783 | else: 784 | user.on_notice_or_privmsg(client, command, msg) 785 | # IRC channel or special chatroom 786 | elif client.is_in_channel(target): 787 | server.get_channel(target).on_notice_or_privmsg( 788 | client, command, msg) 789 | elif command == 'PRIVMSG': 790 | client.err_nosuchnick(target) 791 | 792 | @staticmethod 793 | @registered(6) 794 | def user(client, user, mode, _, realname): 795 | if len(options.irc_password) and not client.authenticated: 796 | client.err_passwdmismatch('USER') 797 | return 798 | client.user = user 799 | client.realname = realname 800 | client.register() 801 | 802 | ### Channels: StandardChannel, StatusChannel, SpecialChannel 803 | 804 | class Channel: 805 | def __init__(self, name): 806 | self.name = name 807 | self.peer_type = '' 808 | self.topic = '' 809 | self.mode = 'n' 810 | self.members = {} 811 | 812 | def __repr__(self): 813 | return repr({k: v for k, v in self.__dict__.items() 814 | if k in ('name', 'topic')}) 815 | 816 | @property 817 | def prefix(self): 818 | return self.name 819 | 820 | def log(self, source, fmt, *args): 821 | info('%s %s '+fmt, self.name, source.nick, *args) 822 | 823 | def multicast_group(self, source): 824 | return self.members.keys() 825 | 826 | def n_members(self, client): 827 | return len(self.members) 828 | 829 | def event(self, source, command, fmt, *args, include_source=True): 830 | line = fmt.format(*args) if args else fmt 831 | for client in self.multicast_group(source): 832 | if client != source or include_source: 833 | client.write(':{} {} {}'.format(source.prefix, command, line)) 834 | 835 | def dehalfop_event(self, user): 836 | self.event(self, 'MODE', '{} -h {}', self.name, user.nick) 837 | 838 | def deop_event(self, user): 839 | self.event(self, 'MODE', '{} -o {}', self.name, user.nick) 840 | 841 | def devoice_event(self, user): 842 | self.event(self, 'MODE', '{} -v {}', self.name, user.nick) 843 | 844 | def halfop_event(self, user): 845 | self.event(self, 'MODE', '{} +h {}', self.name, user.nick) 846 | 847 | def nick_event(self, user, new): 848 | self.event(user, 'NICK', new) 849 | 850 | def join_event(self, user): 851 | self.event(user, 'JOIN', self.name) 852 | 853 | def kick_event(self, kicker, channel, kicked, reason=None): 854 | if reason: 855 | self.event(kicker, 'KICK', '{} {}: {}', channel.name, kicked.nick, reason) 856 | else: 857 | self.event(kicker, 'KICK', '{} {}', channel.name, kicked.nick) 858 | self.log(kicker, 'kicked %s', kicked.prefix) 859 | 860 | def op_event(self, user): 861 | self.event(self, 'MODE', '{} +o {}', self.name, user.nick) 862 | 863 | def part_event(self, user, partmsg): 864 | if partmsg: 865 | self.event(user, 'PART', '{} :{}', self.name, partmsg) 866 | else: 867 | self.event(user, 'PART', self.name) 868 | 869 | def voice_event(self, user): 870 | self.event(user, 'MODE', '{} +v {}', self.name, user.nick) 871 | 872 | def on_invite(self, client, nick): 873 | # TODO 874 | client.err_chanoprivsneeded(self.name) 875 | 876 | # subclasses should return True if succeeded to join 877 | def on_join(self, client): 878 | client.enter(self) 879 | self.join_event(client) 880 | self.on_topic(client) 881 | self.on_names(client) 882 | 883 | def on_kick(self, client, nick, reason): 884 | client.err_chanoprivsneeded(self.name) 885 | 886 | def on_mode(self, client): 887 | client.rpl_channelmodeis(self.name, self.mode) 888 | 889 | def on_names(self, client): 890 | self.on_names_impl(client, self.members.items()) 891 | 892 | def on_names_impl(self, client, items): 893 | names = [] 894 | for u, mode in items: 895 | nick = u.nick 896 | prefix = '' 897 | while 1: 898 | if 'o' in mode: 899 | prefix += '@' 900 | if 'multi-prefix' not in client.capabilities: 901 | break 902 | if 'h' in mode: 903 | prefix += '%' 904 | if 'multi-prefix' not in client.capabilities: 905 | break 906 | if 'v' in mode: 907 | prefix += '+' 908 | if 'multi-prefix' not in client.capabilities: 909 | break 910 | break 911 | names.append(prefix+nick) 912 | buf = '' 913 | bytelen = 0 914 | maxlen = 510-1-len(server.name)-5-len(client.nick.encode())-3-len(self.name.encode())-2 915 | for name in names: 916 | if bytelen+1+len(name.encode()) > maxlen: 917 | client.reply('353 {} = {} :{}', client.nick, self.name, buf) 918 | buf = '' 919 | bytelen = 0 920 | if buf: 921 | buf += ' ' 922 | bytelen += 1 923 | buf += name 924 | bytelen += len(name.encode()) 925 | if buf: 926 | client.reply('353 {} = {} :{}', client.nick, self.name, buf) 927 | client.reply('366 {} {} :End of NAMES list', client.nick, self.name) 928 | 929 | def on_topic(self, client, new=None): 930 | if new: 931 | client.err_nochanmodes(self.name) 932 | else: 933 | if self.topic: 934 | client.reply('332 {} {} :{}', client.nick, self.name, self.topic) 935 | else: 936 | client.reply('331 {} {} :No topic is set', client.nick, self.name) 937 | 938 | 939 | class StandardChannel(Channel): 940 | def __init__(self, name): 941 | super().__init__(name) 942 | 943 | def on_notice_or_privmsg(self, client, command, msg): 944 | self.event(client, command, '{} :{}', self.name, msg, include_source=False) 945 | 946 | def on_join(self, client): 947 | if client in self.members: 948 | return False 949 | # first user becomes op 950 | self.members[client] = 'o' if not self.members else '' 951 | super().on_join(client) 952 | return True 953 | 954 | def on_kick(self, client, nick, reason): 955 | if 'o' not in self.members[client]: 956 | client.err_chanoprivsneeded(self.name) 957 | elif not server.has_nick(nick): 958 | client.err_usernotinchannel(nick, self.name) 959 | else: 960 | user = server.get_nick(nick) 961 | if user not in self.members: 962 | client.err_usernotinchannel(nick, self.name) 963 | elif client != user: 964 | self.kick_event(client, self, user, reason) 965 | self.on_part(user, None) 966 | 967 | def on_part(self, client, msg=None): 968 | if client not in self.members: 969 | client.err_notonchannel(self.name) 970 | return False 971 | if msg: # explicit PART, not disconnection 972 | self.part_event(client, msg) 973 | if len(self.members) == 1: 974 | server.remove_channel(self.name) 975 | elif 'o' in self.members.pop(client): 976 | user = next(iter(self.members)) 977 | self.members[user] += 'o' 978 | self.op_event(user) 979 | client.leave(self) 980 | return True 981 | 982 | def on_topic(self, client, new=None): 983 | if new: 984 | self.log(client, 'set topic %r', new) 985 | self.topic = new 986 | self.event(client, 'TOPIC', '{} :{}', self.name, new) 987 | else: 988 | super().on_topic(client, new) 989 | 990 | def on_who(self, client): 991 | for member in self.members: 992 | member.on_who_member(client, self) 993 | 994 | 995 | # A special channel where each client can only see himself 996 | class StatusChannel(Channel): 997 | instance = None 998 | 999 | def __init__(self): 1000 | super().__init__('+telegram') 1001 | self.topic = "Your friends are listed here. Messages wont't be broadcasted to them. Type 'help' to see available commands" 1002 | assert not StatusChannel.instance 1003 | StatusChannel.instance = self 1004 | 1005 | def respond(self, client, fmt, *args): 1006 | if args: 1007 | client.write((':{} PRIVMSG {} :'+fmt).format(self.name, self.name, *args)) 1008 | else: 1009 | client.write((':{} PRIVMSG {} :').format(self.name, self.name)+fmt) 1010 | 1011 | def multicast_group(self, source): 1012 | return (x for x in self.members if isinstance(x, Client)) 1013 | 1014 | def on_notice_or_privmsg(self, client, command, msg): 1015 | if client not in self.members: 1016 | client.err_notonchannel(self.name) 1017 | return 1018 | if msg == 'help': 1019 | self.respond(client, 'help') 1020 | self.respond(client, ' display this help') 1021 | self.respond(client, 'eval expression') 1022 | self.respond(client, ' eval a Python expression') 1023 | self.respond(client, 'status [pattern]') 1024 | self.respond(client, ' show contacts/chats/channels') 1025 | elif msg.startswith('status'): 1026 | pattern = None 1027 | ary = msg.split(' ', 1) 1028 | if len(ary) > 1: 1029 | pattern = ary[1] 1030 | self.respond(client, 'IRC channels:') 1031 | for name, room in server.channels.items(): 1032 | if pattern is not None and pattern not in name: continue 1033 | if isinstance(room, StandardChannel): 1034 | self.respond(client, ' ' + name) 1035 | self.respond(client, '{} contacts:', im_name) 1036 | for peer_id, user in server.user_id2special_user.items(): 1037 | if user.is_contact: 1038 | if pattern is not None and not (pattern in user.username or pattern in user.printname): continue 1039 | self.respond(client, ' ' + repr(user)) 1040 | self.respond(client, '{} chats/channels:', im_name) 1041 | for peer_id, room in server.peer_id2special_room.items(): 1042 | if pattern is not None and pattern not in room.name: continue 1043 | if isinstance(room, SpecialChannel): 1044 | self.respond(client, ' ' + room.name) 1045 | else: 1046 | m = re.match(r'eval (.+)$', msg.strip()) 1047 | if m: 1048 | try: 1049 | r = pprint.pformat(eval(m.group(1))) 1050 | except: 1051 | r = traceback.format_exc() 1052 | for line in r.splitlines(): 1053 | self.respond(client, line) 1054 | else: 1055 | self.respond(client, 'Unknown command {}', msg) 1056 | 1057 | def on_join(self, member): 1058 | if isinstance(member, Client): 1059 | if member in self.members: 1060 | return False 1061 | self.members[member] = 'o' 1062 | super().on_join(member) 1063 | else: 1064 | if member in self.members: 1065 | return False 1066 | member.enter(self) 1067 | self.join_event(member) 1068 | if member.tg_user.mutual_contact: 1069 | self.voice_event(member) 1070 | self.members[member] = 'v' 1071 | else: 1072 | self.members[member] = '' 1073 | return True 1074 | 1075 | def on_part(self, member, msg=None): 1076 | if isinstance(member, Client): 1077 | if member not in self.members: 1078 | member.err_notonchannel(self.name) 1079 | return False 1080 | self.part_event(member, msg) 1081 | del self.members[member] 1082 | else: 1083 | if member not in self.members: 1084 | return False 1085 | self.part_event(member, msg) 1086 | del self.members[member] 1087 | member.leave(self) 1088 | return True 1089 | 1090 | def on_who(self, client): 1091 | if client in self.members: 1092 | client.on_who_member(client, self) 1093 | 1094 | 1095 | class SpecialChannel(Channel): 1096 | def __init__(self, tg_room): 1097 | super().__init__(None) 1098 | if isinstance(tg_room, tl.types.Channel): 1099 | self.peer = tl.types.PeerChannel(tg_room.id) 1100 | elif isinstance(tg_room, tl.types.Chat): 1101 | self.peer = tl.types.PeerChat(tg_room.id) 1102 | else: 1103 | error(tg_room) 1104 | assert False 1105 | self.joined = {} # `client` has not joined 1106 | self.explicit_parted = set() 1107 | self.update(tg_room) 1108 | self.log_file = None 1109 | self.last_text_by_client = weakref.WeakKeyDictionary() 1110 | self.max_id = 0 1111 | self.privmsg_reply = None 1112 | self.privmsg_seq = 0 1113 | self.privmsg_text = '' 1114 | 1115 | def __repr__(self): 1116 | return repr({k: v for k, v in self.__dict__.items() 1117 | if k in ('flags', 'name', 'peer')}) 1118 | 1119 | @property 1120 | def nick(self): 1121 | return self.name 1122 | 1123 | def is_type(self, type): 1124 | return isinstance(self.peer, type) 1125 | 1126 | def update(self, tg_room): 1127 | self.tg_room = tg_room 1128 | old_name = getattr(self, 'name', None) 1129 | base = options.special_channel_prefix + irc_escape(tg_room.title) 1130 | #if base == options.special_channel_prefix: 1131 | # base += '.'.join(member.nick for member in self.members)[:20] 1132 | suffix = '' 1133 | while 1: 1134 | name = base+suffix 1135 | if name == old_name or not server.has_channel(name): 1136 | break 1137 | suffix = str(int(suffix or 0)+1) 1138 | if name != old_name: 1139 | # PART -> rename -> JOIN to notify the IRC client 1140 | joined = [client for client in server.auth_clients() if client in self.joined] 1141 | for client in joined: 1142 | self.on_part(client, 'Changing name') 1143 | self.name = name 1144 | for client in joined: 1145 | self.on_join(client) 1146 | if self.is_type(tl.types.PeerChannel): 1147 | topic = '{} {}'.format(self.peer.channel_id, tg_room.title.replace('\n', '\\n')) 1148 | elif self.is_type(tl.types.PeerChat): 1149 | topic = 'chat#{} {}'.format(self.peer.chat_id, tg_room.title.replace('\n', '\\n')) 1150 | if self.topic != topic: 1151 | self.topic = topic 1152 | for client in server.auth_clients(): 1153 | client.reply('332 {} {} :{}', client.nick, self.name, self.topic) 1154 | 1155 | def update_admins(self, admins): 1156 | seen_me = False 1157 | seen = set() 1158 | for admin in admins: 1159 | user = server.ensure_special_user(admin) 1160 | if user == server: 1161 | seen_me = True 1162 | elif user in self.members: 1163 | seen.add(user) 1164 | for client, mode in self.joined.items(): 1165 | if 'o' in mode and not seen_me: 1166 | self.unset_cmode(user, 'o') 1167 | self.deop_event(client) 1168 | if 'o' not in mode and seen_me: 1169 | self.set_cmode(user, 'o') 1170 | self.op_event(client) 1171 | for user, mode in self.members.items(): 1172 | if 'o' in mode and user not in seen: 1173 | self.unset_cmode(user, 'o') 1174 | self.deop_event(user) 1175 | if 'o' not in mode and user in seen: 1176 | self.set_cmode(user, 'o') 1177 | self.op_event(user) 1178 | 1179 | def update_members(self, tg_users): 1180 | seen = {} 1181 | for tg_user in tg_users: 1182 | user = server.ensure_special_user(tg_user.id, tg_user) 1183 | if user != server: 1184 | seen[user] = 'v' if user.is_contact else '' 1185 | for user in self.members.keys() - seen.keys(): 1186 | self.on_part(user, self.name) 1187 | for user in seen.keys() - self.members.keys(): 1188 | self.on_join(user) 1189 | for user, mode in seen.items(): 1190 | old = self.members.get(user, '') 1191 | if 'h' in old and 'h' not in mode: 1192 | self.unset_cmode(user, 'h') 1193 | self.dehalfop_event(user) 1194 | if 'h' not in old and 'h' in mode: 1195 | self.set_cmode(user, 'h') 1196 | self.halfop_event(user) 1197 | if 'v' in old and 'v' not in mode: 1198 | self.unset_cmode(user, 'v') 1199 | self.devoice_event(user) 1200 | if 'v' not in old and 'v' in mode: 1201 | self.set_cmode(user, 'v') 1202 | self.voice_event(user) 1203 | self.members = seen 1204 | 1205 | def multicast_group(self, source): 1206 | ret = [] 1207 | for client in server.auth_clients(): 1208 | if client in self.joined: 1209 | ret.append(client) 1210 | return ret 1211 | 1212 | def set_cmode(self, user, m): 1213 | if user in self.joined: 1214 | self.joined[user] = m+self.joined[user].replace(m, '') 1215 | elif user in self.members: 1216 | self.members[user] = m+self.members[user].replace(m, '') 1217 | 1218 | def unset_cmode(self, user, m): 1219 | if user in self.joined: 1220 | self.joined[user] = self.joined[user].replace(m, '') 1221 | elif user in self.members: 1222 | self.members[user] = self.members[user].replace(m, '') 1223 | 1224 | def on_mode(self, client, *args): 1225 | if len(args): 1226 | if args[0] == '+m': 1227 | self.mode = 'm'+self.mode.replace('m', '') 1228 | self.event(client, 'MODE', '{} {}', self.name, args[0]) 1229 | elif args[0] == '-m': 1230 | self.mode = self.mode.replace('m', '') 1231 | self.event(client, 'MODE', '{} {}', self.name, args[0]) 1232 | elif args[0] in ('+o', '-o', '+h', '-h') and len(args) == 2: 1233 | nick = args[1] 1234 | if not server.has_special_user(nick): 1235 | client.err_nosuchnick(nick) 1236 | else: 1237 | user = server.get_special_user(nick) 1238 | if self not in user.channels: 1239 | client.err_usernotinchannel(nick, self.name) 1240 | else: 1241 | server.loop.create_task(web.channel_set_admin(client, self, user, {'+h':1,'-h':0,'+o':2,'-o':0}[args[0]])) 1242 | elif re.match('[-+]', args[0]): 1243 | client.err_unknownmode(args[0][1] if len(args[0]) > 1 else '') 1244 | else: 1245 | client.err_unknownmode(args[0][0] if len(args[0]) else '') 1246 | else: 1247 | client.rpl_channelmodeis(self.name, self.mode) 1248 | 1249 | def on_names(self, client): 1250 | self.on_names_impl(client, chain(self.joined.items(), self.members.items())) 1251 | 1252 | def on_notice_or_privmsg(self, client, command, text): 1253 | irc_privmsg(client, command, self, text) 1254 | 1255 | def on_invite(self, client, nick): 1256 | if server.has_special_user(nick): 1257 | user = server.get_special_user(nick) 1258 | if user in self.members: 1259 | client.err_useronchannel(nick, self.name) 1260 | else: 1261 | web.channel_invite(client, self, user) 1262 | else: 1263 | client.err_nosuchnick(nick) 1264 | 1265 | def on_join(self, member): 1266 | if isinstance(member, Client): 1267 | if member in self.joined: 1268 | return False 1269 | self.joined[member] = '' 1270 | self.explicit_parted.discard(member) 1271 | web.channel_members(self) 1272 | super().on_join(member) 1273 | else: 1274 | if member in self.members: 1275 | return False 1276 | self.members[member] = '' 1277 | member.enter(self) 1278 | self.join_event(member) 1279 | return True 1280 | 1281 | def on_kick(self, client, nick, reason): 1282 | if server.has_special_user(nick): 1283 | user = server.get_special_user(nick) 1284 | web.channel_kick(client, self, user) 1285 | else: 1286 | client.err_usernotinchannel(nick, self.name) 1287 | 1288 | def on_part(self, member, msg=None): 1289 | if isinstance(member, Client): 1290 | if member not in self.joined: 1291 | member.err_notonchannel(self.name) 1292 | return False 1293 | if msg: # not msg implies being disconnected/kicked/... 1294 | self.part_event(member, msg) 1295 | del self.joined[member] 1296 | self.explicit_parted.add(member) 1297 | else: 1298 | if member not in self.members: 1299 | return False 1300 | self.part_event(member, msg) 1301 | del self.members[member] 1302 | member.leave(self) 1303 | return True 1304 | 1305 | def on_topic(self, client, new=None): 1306 | if new: 1307 | client.err_nochanmodes(self.name) 1308 | else: 1309 | super().on_topic(client, new) 1310 | 1311 | def on_who(self, client): 1312 | members = tuple(self.members)+(client,) 1313 | for member in members: 1314 | member.on_who_member(client, self) 1315 | 1316 | 1317 | class Client: 1318 | def __init__(self, reader, writer): 1319 | self.reader = reader 1320 | self.writer = writer 1321 | peer = writer.get_extra_info('socket').getpeername() 1322 | self.host = peer[0] 1323 | if self.host[0] == ':': 1324 | self.host = '[{}]'.format(self.host) 1325 | self.user = None 1326 | self.nick = None 1327 | self.registered = False 1328 | self.mode = '' 1329 | self.channels = {} # joined, name -> channel 1330 | self.capabilities = set() 1331 | self.authenticated = False 1332 | 1333 | def enter(self, channel): 1334 | self.channels[irc_lower(channel.name)] = channel 1335 | 1336 | def leave(self, channel): 1337 | del self.channels[irc_lower(channel.name)] 1338 | 1339 | def auto_join(self, room): 1340 | for regex in options.ignore or []: 1341 | if re.search(regex, room.name): 1342 | return 1343 | for regex in options.ignore_topic or []: 1344 | if re.search(regex, room.topic): 1345 | return 1346 | room.on_join(self) 1347 | 1348 | def is_in_channel(self, name): 1349 | return irc_lower(name) in self.channels 1350 | 1351 | def disconnect(self, quitmsg): 1352 | if quitmsg: 1353 | self.write('ERROR :{}'.format(quitmsg)) 1354 | self.message_related(False, ':{} QUIT :{}', self.prefix, quitmsg) 1355 | if self.nick is not None: 1356 | info('Disconnected from %s', self.prefix) 1357 | try: 1358 | self.writer.write_eof() 1359 | self.writer.close() 1360 | except: 1361 | pass 1362 | if self.nick is None: 1363 | return 1364 | channels = list(self.channels.values()) 1365 | for channel in channels: 1366 | channel.on_part(self, None) 1367 | server.remove_nick(self.nick) 1368 | self.nick = None 1369 | server.clients.discard(self) 1370 | 1371 | def reply(self, msg, *args): 1372 | '''Respond to the client's request''' 1373 | self.write((':{} '+msg).format(server.name, *args)) 1374 | 1375 | def write(self, msg): 1376 | try: 1377 | self.writer.write(msg.encode()+b'\r\n') 1378 | except: 1379 | pass 1380 | 1381 | def status(self, msg): 1382 | '''A status message from the server''' 1383 | self.write(':{} NOTICE {} :{}'.format(server.name, server.name, msg)) 1384 | 1385 | @property 1386 | def prefix(self): 1387 | return '{}!{}@{}'.format(self.nick or '', self.user or '', self.host or '') 1388 | 1389 | def rpl_umodeis(self, mode): 1390 | self.reply('221 {} +{}', self.nick, mode) 1391 | 1392 | def rpl_channelmodeis(self, channelname, mode): 1393 | self.reply('324 {} {} +{}', self.nick, channelname, mode) 1394 | 1395 | def rpl_endofnames(self, channelname): 1396 | self.reply('366 {} {} :End of NAMES list', self.nick, channelname) 1397 | 1398 | def rpl_info(self, fmt, *args): 1399 | line = fmt.format(*args) if args else fmt 1400 | self.reply('371 {} :{}', self.nick, line) 1401 | 1402 | def rpl_endofinfo(self, msg): 1403 | self.reply('374 {} :End of INFO list', self.nick) 1404 | 1405 | def err_nosuchnick(self, name): 1406 | self.reply('401 {} {} :No such nick/channel', self.nick, name) 1407 | 1408 | def err_nosuchserver(self, name): 1409 | self.reply('402 {} {} :No such server', self.nick, name) 1410 | 1411 | def err_nosuchchannel(self, channelname): 1412 | self.reply('403 {} {} :No such channel', self.nick, channelname) 1413 | 1414 | def err_cannotsendtochan(self, channelname, text): 1415 | self.reply('404 {} {} :{}', self.nick, channelname, text or 'Cannot send to channel') 1416 | 1417 | def err_noorigin(self): 1418 | self.reply('409 {} :No origin specified', self.nick) 1419 | 1420 | def err_norecipient(self, command): 1421 | self.reply('411 {} :No recipient given ({})', self.nick, command) 1422 | 1423 | def err_notexttosend(self): 1424 | self.reply('412 {} :No text to send', self.nick) 1425 | 1426 | def err_unknowncommand(self, command): 1427 | self.reply('421 {} {} :Unknown command', self.nick, command) 1428 | 1429 | def err_nonicknamegiven(self): 1430 | self.reply('431 {} :No nickname given', self.nick) 1431 | 1432 | def err_errorneusnickname(self, nick): 1433 | self.reply('432 * {} :Erroneous nickname', nick) 1434 | 1435 | def err_nicknameinuse(self, nick): 1436 | self.reply('433 * {} :Nickname is already in use', nick) 1437 | 1438 | def err_usernotinchannel(self, nick, channelname): 1439 | self.reply("441 {} {} {} :They are't on that channel", self.nick, nick, channelname) 1440 | 1441 | def err_notonchannel(self, channelname): 1442 | self.reply("442 {} {} :You're not on that channel", self.nick, channelname) 1443 | 1444 | def err_useronchannel(self, nick, channelname): 1445 | self.reply('443 {} {} {} :is already on channel', self.nick, nick, channelname) 1446 | 1447 | def err_nologin(self, nick): 1448 | self.reply('444 {} {} :User not logged in', self.nick, nick) 1449 | 1450 | def err_needmoreparams(self, command): 1451 | self.reply('461 {} {} :Not enough parameters', self.nick, command) 1452 | 1453 | def err_passwdmismatch(self, command): 1454 | self.reply('464 * {} :Password incorrect', command) 1455 | 1456 | def err_unknownmode(self, mode): 1457 | self.reply("472 {} {} :is unknown mode char to me", self.nick, mode) 1458 | 1459 | def err_nochanmodes(self, channelname): 1460 | self.reply("477 {} {} :Channel doesn't support modes", self.nick, channelname) 1461 | 1462 | def err_chanoprivsneeded(self, channelname): 1463 | self.reply("482 {} {} :You're not channel operator", self.nick, channelname) 1464 | 1465 | def err_umodeunknownflag(self): 1466 | self.reply('501 {} :Unknown MODE flag', self.nick) 1467 | 1468 | def message_related(self, include_self, fmt, *args): 1469 | '''Send a message to related clients which source is self''' 1470 | line = fmt.format(*args) 1471 | clients = [c for c in server.clients if c != self] 1472 | if include_self: 1473 | clients.append(self) 1474 | for client in clients: 1475 | client.write(line) 1476 | 1477 | def register(self): 1478 | if self.registered: 1479 | return 1480 | if self.user and self.nick and (not (options.irc_password or options.sasl_password) or self.authenticated): 1481 | self.registered = True 1482 | info('%s registered', self.prefix) 1483 | self.reply('001 {} :Hi, welcome to IRC', self.nick) 1484 | self.reply('002 {} :Your host is {}', self.nick, server.name) 1485 | self.reply('005 {} PREFIX=(ohv)@%+ CHANTYPES=!#&+ CHANMODES=,,,m SAFELIST :are supported by this server', self.nick) 1486 | Command.lusers(self) 1487 | Command.motd(self) 1488 | 1489 | Command.join(self, StatusChannel.instance.name) 1490 | StatusChannel.instance.respond(self, 'Your contacts are listed in this channel') 1491 | if not web.authorized: 1492 | StatusChannel.instance.respond(self, 'This session is unauthorized. Requesting login code. Please type /oper a $login_code') 1493 | web.proc.send_code_request(options.tg_phone) 1494 | else: 1495 | StatusChannel.instance.respond(self, 'Reuse {}.session . Initializing...', options.tg_session) 1496 | 1497 | def handle_command(self, command, args): 1498 | cmd = irc_lower(command) 1499 | if cmd == 'pass': 1500 | cmd = cmd+'_' 1501 | if type(Command.__dict__.get(cmd)) != staticmethod: 1502 | self.err_unknowncommand(command) 1503 | return 1504 | fn = getattr(Command, cmd) 1505 | if not web.authorized: 1506 | code = 4 1507 | elif not self.registered: 1508 | code = 2 1509 | else: 1510 | code = 1 1511 | 1512 | if not (cmd_use_case.get(cmd, 1) & code): 1513 | self.err_unknowncommand(command) 1514 | return 1515 | try: 1516 | ba = inspect.signature(fn).bind(self, *args) 1517 | except TypeError: 1518 | self.err_needmoreparams(command) 1519 | return 1520 | fn(*ba.args) 1521 | 1522 | async def handle_irc(self): 1523 | sent_ping = False 1524 | while 1: 1525 | try: 1526 | line = await asyncio.wait_for( 1527 | self.reader.readline(), loop=server.loop, 1528 | timeout=options.heartbeat) 1529 | except asyncio.TimeoutError: 1530 | if sent_ping: 1531 | self.disconnect('ping timeout') 1532 | return 1533 | else: 1534 | sent_ping = True 1535 | self.write('PING :'+server.name) 1536 | continue 1537 | except ConnectionResetError: 1538 | self.disconnect('ConnectionResetError') 1539 | break 1540 | if not line: 1541 | return 1542 | line = line.rstrip(b'\r\n').decode('utf-8', 'ignore') 1543 | sent_ping = False 1544 | if not line: 1545 | continue 1546 | # http://ircv3.net/specs/core/message-tags-3.2.html 1547 | if line.startswith('@'): 1548 | x = line.split(' ', 1) 1549 | if len(x) == 1: continue 1550 | line = x[1] 1551 | 1552 | x = line.split(' ', 1) 1553 | command = x[0] 1554 | if len(x) == 1: 1555 | args = [] 1556 | elif len(x[1]) > 0 and x[1][0] == ':': 1557 | args = [x[1][1:]] 1558 | else: 1559 | y = x[1].split(' :', 1) 1560 | args = y[0].split(' ') 1561 | if len(y) == 2: 1562 | args.append(y[1]) 1563 | try: 1564 | self.handle_command(command, args) 1565 | except: 1566 | traceback.print_exc() 1567 | self.disconnect('client error') 1568 | break 1569 | 1570 | def ctcp(self, peer, msg): 1571 | async def download(): 1572 | reader, writer = await asyncio.open_connection(ip, port) 1573 | body = b'' 1574 | while 1: 1575 | # TODO timeout 1576 | buf = await reader.read(size-len(body)) 1577 | if not buf: 1578 | break 1579 | body += buf 1580 | if len(body) >= size: 1581 | break 1582 | web.send_file(self, peer, filename, body) 1583 | 1584 | async def download_wrap(): 1585 | try: 1586 | await asyncio.wait_for(download(), options.dcc_send_download_timeout) 1587 | except asyncio.TimeoutError: 1588 | self.status('Downloading of DCC SEND timeout') 1589 | 1590 | if len(msg) > 2 and msg[0] == '\1' and msg[-1] == '\1': 1591 | # VULNERABILITY used as proxy 1592 | try: 1593 | dcc_, send_, filename, ip, port, size = msg[1:-1].split(' ') 1594 | ip = socket.gethostbyname(str(int(ip))) 1595 | size = int(size) 1596 | assert dcc_ == 'DCC' and send_ == 'SEND' 1597 | if 0 < size <= options.dcc_send: 1598 | server.loop.create_task(download()) 1599 | else: 1600 | self.status('DCC SEND: invalid size of {}, (0,{}] is acceptable'.format( 1601 | filename, options.dcc_send)) 1602 | except: 1603 | pass 1604 | return True 1605 | return False 1606 | 1607 | def on_who_member(self, client, channel): 1608 | client.reply('352 {} {} {} {} {} {} H :0 {}', client.nick, channel.name, 1609 | self.user, self.host, server.name, 1610 | self.nick, self.realname) 1611 | 1612 | def on_whois(self, client): 1613 | client.reply('311 {} {} {} {} * :{}', client.nick, self.nick, 1614 | self.user, self.host, self.realname) 1615 | client.reply('319 {} {} :{}', client.nick, self.nick, 1616 | ' '.join(name for name in 1617 | client.channels.keys() & self.channels.keys())) 1618 | 1619 | 1620 | class TelegramUpdate: 1621 | @staticmethod 1622 | def UpdateChannelPinnedMessage(server, update): 1623 | info('UpdateChannelPinnedMessage %r', update.to_dict()) 1624 | 1625 | @staticmethod 1626 | def UpdateChannelWebPage(server, update): 1627 | info('UpdateChannelWebpage %r', update.to_dict()) 1628 | 1629 | @staticmethod 1630 | def UpdateChatParticipantAdd(server, update): 1631 | user = server.ensure_special_user(update.user_id, None) 1632 | if user is not server: 1633 | room = server.ensure_special_room(update.chat_id, None) 1634 | room.on_join(user) 1635 | if user.is_contact: 1636 | room.voice_event(user) 1637 | room.set_cmode(user, 'v') 1638 | 1639 | @staticmethod 1640 | def UpdateChatParticipantDelete(server, update): 1641 | room = server.ensure_special_room(update.chat_id, None) 1642 | user = server.ensure_special_user(update.user_id, None) 1643 | if user is server: 1644 | joined = [client for client in server.auth_clients() if client in channel.joined] 1645 | for client in joined: 1646 | channel.on_part(client) 1647 | else: 1648 | room.on_part(user) 1649 | 1650 | @staticmethod 1651 | def UpdateChatUserTyping(server, update): 1652 | pass 1653 | 1654 | @staticmethod 1655 | def UpdateContactLink(server, update): 1656 | pass 1657 | 1658 | @staticmethod 1659 | def UpdateContactRegistered(server, update): 1660 | pass 1661 | 1662 | @staticmethod 1663 | def UpdateDeleteChannelMessages(server, update): 1664 | pass 1665 | 1666 | @staticmethod 1667 | def UpdateDeleteMessages(server, update): 1668 | pass 1669 | 1670 | @staticmethod 1671 | def UpdateEditChannelMessage(server, update): 1672 | server.on_telegram_update_message(update, update.message) 1673 | 1674 | @staticmethod 1675 | def UpdateEditMessage(server, update): 1676 | server.on_telegram_update_message(update, update.message) 1677 | 1678 | @staticmethod 1679 | def UpdateNewChannelMessage(server, update): 1680 | server.on_telegram_update_message(update, update.message) 1681 | 1682 | @staticmethod 1683 | def UpdateNewMessage(server, update): 1684 | server.on_telegram_update_message(update, update.message) 1685 | 1686 | @staticmethod 1687 | def UpdateReadChannelInbox(server, update): 1688 | pass 1689 | 1690 | @staticmethod 1691 | def UpdateReadChannelOutbox(server, update): 1692 | pass 1693 | 1694 | @staticmethod 1695 | def UpdateReadHistoryInbox(server, update): 1696 | pass 1697 | 1698 | @staticmethod 1699 | def UpdateReadHistoryOutbox(server, update): 1700 | pass 1701 | 1702 | @staticmethod 1703 | def UpdateShortChatMessage(server, update): 1704 | from_ = server.ensure_special_user(update.from_id, None) 1705 | to = server.ensure_special_room(update.chat_id, None) 1706 | server.on_telegram_update_message(update, update, from_, to) 1707 | 1708 | @staticmethod 1709 | def UpdateShortMessage(server, update): 1710 | from_ = server.ensure_special_user(update.user_id, None) 1711 | to = server 1712 | if update.out: 1713 | from_, to = to, from_ 1714 | server.on_telegram_update_message(update, update, from_, to) 1715 | 1716 | @staticmethod 1717 | def UpdateUserName(server, update): 1718 | pass 1719 | 1720 | @staticmethod 1721 | def UpdateUserStatus(server, update): 1722 | try: 1723 | user = server.ensure_special_user(update.user_id, None) 1724 | except: 1725 | return 1726 | if user is not server: 1727 | if isinstance(update.status, tl.types.UserStatusOffline): 1728 | user.set_umode('a') 1729 | user.event('AWAY', 'offline') 1730 | elif isinstance(update.status, tl.types.UserStatusOnline): 1731 | user.unset_umode('a') 1732 | user.event('AWAY') 1733 | 1734 | @staticmethod 1735 | def UpdateUserTyping(server, update): 1736 | pass 1737 | 1738 | @staticmethod 1739 | def UpdateWebPage(server, update): 1740 | webpage = update.webpage 1741 | info('UpdateWebpage %r %r', type(webpage), webpage.to_dict()) 1742 | if isinstance(webpage, tl.types.WebPage): 1743 | value = web.webpage_id2sender_to.pop(webpage.id, None) 1744 | if value is None: 1745 | return 1746 | sender, to = value 1747 | server.deliver_message(None, sender, to, datetime.utcnow(), 1748 | '[WebPage] {} {}'.format(webpage.url.replace('\n', '\\n'), 1749 | (webpage.title or webpage.display_url).replace('\n', '\\n'))) 1750 | 1751 | 1752 | class SpecialUser: 1753 | def __init__(self, tg_user): 1754 | self.user_id = tg_user.id 1755 | self.peer = tl.types.PeerUser(tg_user.id) 1756 | self.username = None 1757 | self.channels = set() 1758 | self.is_contact = False 1759 | self.mode = '' 1760 | self.update(tg_user) 1761 | self.log_file = None 1762 | self.last_text_by_client = weakref.WeakKeyDictionary() 1763 | self.max_id = 0 1764 | self.privmsg_reply = None 1765 | self.privmsg_seq = 0 1766 | self.privmsg_text = '' 1767 | 1768 | def __repr__(self): 1769 | return repr({k: v for k, v in self.__dict__.items() 1770 | if k in ('flags', 'name', 'peer_id', 'print_name', 'username')}) 1771 | 1772 | @property 1773 | def prefix(self): 1774 | return '{}!{}@{}'.format(self.nick, self.user_id, im_name) 1775 | 1776 | def preferred_nick(self): 1777 | if self.username: 1778 | return self.username 1779 | # fix order of Chinese names 1780 | han = r'[\u3400-\u4dbf\u4e00-\u9fff\U00020000-\U0002ceaf]' 1781 | m = re.match('({}+)_({}+)$'.format(han, han), self.print_name) 1782 | if m: 1783 | return m.group(2)+m.group(1) 1784 | return self.print_name 1785 | 1786 | def event(self, command, fmt=None, *args): 1787 | if fmt: 1788 | line = fmt.format(*args) if args else fmt 1789 | for client in server.auth_clients(): 1790 | if command == 'AWAY' and 'away-notify' not in client.capabilities: continue 1791 | if fmt: 1792 | client.write(':{} {} {}'.format(self.prefix, command, line)) 1793 | else: 1794 | client.write(':{} {}'.format(self.prefix, command)) 1795 | 1796 | def set_umode(self, m): 1797 | if m not in self.mode: 1798 | self.mode += m 1799 | 1800 | def unset_umode(self, m): 1801 | if m in self.mode: 1802 | self.mode = self.mode.replace(m, '') 1803 | 1804 | def update(self, tg_user): 1805 | self.tg_user = tg_user 1806 | self.username = tg_user.username 1807 | self.print_name = (tg_user.first_name or '') + '_' + (tg_user.last_name or '') 1808 | old_nick = getattr(self, 'nick', None) 1809 | base = irc_escape_nick(self.preferred_nick()) or 'Guest' 1810 | suffix = '' 1811 | while 1: 1812 | nick = base+suffix 1813 | lower = irc_lower(nick) 1814 | if nick and (nick == old_nick or 1815 | not (server.has_nick(nick) or lower in server.services or lower in options.irc_nicks)): 1816 | break 1817 | suffix = str(int(suffix or 0)+1) 1818 | if nick != old_nick: 1819 | for channel in self.channels: 1820 | channel.nick_event(self, nick) 1821 | self.nick = nick 1822 | if tg_user.contact and not tg_user.restricted: 1823 | if not self.is_contact: 1824 | self.is_contact = True 1825 | StatusChannel.instance.on_join(self) 1826 | for channel in self.channels: 1827 | if isinstance(channel, SpecialChannel): 1828 | channel.set_cmode(self, 'v') 1829 | channel.voice_event(self) 1830 | else: 1831 | if self.is_contact: 1832 | self.is_contact = False 1833 | StatusChannel.instance.on_part(self) 1834 | for channel in self.channels: 1835 | if isinstance(channel, SpecialChannel): 1836 | channel.unset_cmode(self, 'v') 1837 | channel.devoice_event(self) 1838 | 1839 | def enter(self, channel): 1840 | self.channels.add(channel) 1841 | 1842 | def leave(self, channel): 1843 | self.channels.remove(channel) 1844 | 1845 | def on_notice_or_privmsg(self, client, command, text): 1846 | irc_privmsg(client, command, self, text) 1847 | if 'a' in self.mode: 1848 | client.write(':{} AWAY away'.format(self.prefix)) 1849 | if options.mark_read == 'reply': 1850 | web.mark_read(self.peer, self.max_id) 1851 | 1852 | def on_who_member(self, client, channel): 1853 | client.reply('352 {} {} {} {} {} {} H :0 {}', client.nick, channel.name, 1854 | self.user_id, im_name, server.name, 1855 | self.nick, self.print_name) 1856 | 1857 | def on_whois(self, client): 1858 | client.reply('311 {} {} {} {} * :{}', client.nick, self.nick, 1859 | self.user_id, im_name, self.print_name) 1860 | if 'a' in self.mode: 1861 | client.reply('301 {} {} away', client.nick, self.nick) 1862 | 1863 | 1864 | class Server: 1865 | valid_nickname = re.compile(r"^[][\`_^{|}A-Za-z][][\`_^{|}A-Za-z0-9-]{0,50}$") 1866 | # initial character `+` is reserved for StatusChannel 1867 | # initial character `&` is reserved for SpecialChannel 1868 | valid_channelname = re.compile(r"^[#!][^\x00\x07\x0a\x0d ,:]{0,50}$") 1869 | 1870 | def __init__(self): 1871 | global server 1872 | server = self 1873 | status = StatusChannel() 1874 | self.channels = {status.name: status} 1875 | self.name = 'telegramircd.maskray.me' 1876 | self.nicks = {} 1877 | self.clients = weakref.WeakSet() 1878 | self.log_file = None 1879 | self._boot = datetime.now() 1880 | self.services = ('ChanServ',) 1881 | 1882 | self.last_text_by_client = weakref.WeakKeyDictionary() 1883 | self.max_id = 0 1884 | self.user_id = 0 1885 | self.name2special_room = {} # name -> Telegram chatroom 1886 | self.peer_id2special_room = {} # peer_id -> SpecialChannel 1887 | self.user_id2special_user = {} # peer_id -> SpecialUser 1888 | self.nick2special_user = {} # nick -> IRC user or Telegram user (friend or room contact) 1889 | 1890 | def _accept(self, reader, writer): 1891 | try: 1892 | client = Client(reader, writer) 1893 | self.clients.add(client) 1894 | task = self.loop.create_task(client.handle_irc()) 1895 | def done(task): 1896 | client.disconnect(None) 1897 | 1898 | task.add_done_callback(done) 1899 | except Exception as e: 1900 | traceback.print_exc() 1901 | 1902 | def auth_clients(self): 1903 | return (client for client in self.clients if client.registered) 1904 | 1905 | def preferred_client(self): 1906 | n = len(self.clients) 1907 | opt, optv = None, n+2 1908 | for c in self.clients: 1909 | if c.nick: 1910 | try: 1911 | v = options.irc_nicks.index(c.nick) 1912 | except ValueError: 1913 | v = n+1 if c.nick.endswith('bot') else n 1914 | if v < optv: 1915 | opt, optv = c, v 1916 | return opt 1917 | 1918 | def has_channel(self, name): 1919 | x = irc_lower(name) 1920 | return x in self.channels or x in self.name2special_room 1921 | 1922 | def has_nick(self, nick): 1923 | x = irc_lower(nick) 1924 | return x in self.nicks or x in self.nick2special_user 1925 | 1926 | def has_special_room(self, name): 1927 | return irc_lower(name) in self.name2special_room 1928 | 1929 | def has_special_user(self, nick): 1930 | return irc_lower(nick) in self.nick2special_user 1931 | 1932 | def get_channel(self, name): 1933 | x = irc_lower(name) 1934 | return self.channels[x] if x in self.channels else self.name2special_room[x] 1935 | 1936 | def get_nick(self, nick): 1937 | x = irc_lower(nick) 1938 | return self.nicks[x] if x in self.nicks else self.nick2special_user[x] 1939 | 1940 | def get_special_user(self, nick): 1941 | return self.nick2special_user[irc_lower(nick)] 1942 | 1943 | def get_special_room(self, name): 1944 | return self.name2special_room[irc_lower(name)] 1945 | 1946 | def remove_special_user(self, nick): 1947 | del self.nick2special_user[irc_lower(nick)] 1948 | 1949 | # IRC channel or special chatroom 1950 | def ensure_channel(self, channelname): 1951 | if self.has_channel(channelname): 1952 | return self.channels[irc_lower(channelname)] 1953 | if not Server.valid_channelname.match(channelname): 1954 | raise ValueError 1955 | channel = StandardChannel(channelname) 1956 | self.channels[irc_lower(channelname)] = channel 1957 | return channel 1958 | 1959 | def ensure_special_room(self, peer_id, tg_room): 1960 | debug('ensure_special_room %r %r', peer_id, tg_room) 1961 | if peer_id in self.peer_id2special_room: 1962 | room = self.peer_id2special_room[peer_id] 1963 | del self.name2special_room[irc_lower(room.name)] 1964 | #room.update(record) 1965 | else: 1966 | if tg_room is None: 1967 | tg_room = web.proc.get_entity(peer_id) 1968 | room = SpecialChannel(tg_room) 1969 | self.peer_id2special_room[peer_id] = room 1970 | if options.join == 'all': 1971 | for client in self.auth_clients(): 1972 | client.auto_join(room) 1973 | self.name2special_room[irc_lower(room.name)] = room 1974 | return room 1975 | 1976 | def ensure_special_user(self, user_id, tg_user): 1977 | debug('ensure_special_user %r %r', user_id, tg_user) 1978 | if user_id == self.user_id: 1979 | return self 1980 | if user_id in self.user_id2special_user: 1981 | user = self.user_id2special_user[user_id] 1982 | self.remove_special_user(user.nick) 1983 | #user.update(tg_user) 1984 | else: 1985 | if tg_user is None: 1986 | tg_user = web.proc.get_entity(user_id) 1987 | user = SpecialUser(tg_user) 1988 | self.user_id2special_user[user.user_id] = user 1989 | self.nick2special_user[irc_lower(user.nick)] = user 1990 | return user 1991 | 1992 | def remove_channel(self, channelname): 1993 | del self.channels[irc_lower(channelname)] 1994 | 1995 | def change_nick(self, client, new): 1996 | lower = irc_lower(new) 1997 | if self.has_nick(new) or lower in self.services: 1998 | client.err_nicknameinuse(new) 1999 | elif not Server.valid_nickname.match(new): 2000 | client.err_errorneusnickname(new) 2001 | else: 2002 | if client.nick: 2003 | info('%s changed nick to %s', client.prefix, new) 2004 | self.remove_nick(client.nick) 2005 | client.message_related(True, ':{} NICK {}', client.prefix, new) 2006 | self.nicks[lower] = client 2007 | client.nick = new 2008 | 2009 | def remove_nick(self, nick): 2010 | del self.nicks[irc_lower(nick)] 2011 | 2012 | def start(self, loop, tls): 2013 | self.loop = loop 2014 | self.servers = [] 2015 | for i in options.irc_listen if options.irc_listen else options.listen: 2016 | self.servers.append(loop.run_until_complete( 2017 | asyncio.streams.start_server(self._accept, i, options.irc_port, ssl=tls))) 2018 | 2019 | def stop(self): 2020 | for i in self.servers: 2021 | i.close() 2022 | self.loop.run_until_complete(i.wait_closed()) 2023 | 2024 | def resolve_from_to(self, msg): 2025 | if isinstance(msg.to_id, tl.types.PeerUser): 2026 | to = server.ensure_special_user(msg.to_id.user_id, None) 2027 | elif isinstance(msg.to_id, tl.types.PeerChannel): 2028 | to = server.ensure_special_room(msg.to_id.channel_id, None) 2029 | elif isinstance(msg.to_id, tl.types.PeerChat): 2030 | to = server.ensure_special_room(msg.to_id.chat_id, None) 2031 | else: 2032 | assert False 2033 | try: 2034 | from_ = server.ensure_special_user(msg.from_id, None) 2035 | except: 2036 | # Haven't seen the peer before. Retry. 2037 | if isinstance(msg.to_id, (tl.types.PeerChannel, tl.types.PeerChat)): 2038 | web.channel_members(to) 2039 | from_ = server.ensure_special_user(msg.from_id, None) 2040 | 2041 | return from_, to 2042 | 2043 | def is_type(self, _type): 2044 | return False 2045 | 2046 | def on_telegram_update(self, update): 2047 | name = type(update).__name__ 2048 | if type(TelegramUpdate.__dict__.get(name)) is staticmethod: 2049 | getattr(TelegramUpdate, name)(self, update) 2050 | else: 2051 | info('on_telegram_update %r %r', type(update).__name__, update.to_dict()) 2052 | 2053 | def on_telegram_update_message(self, update, msg, sender=None, to=None): 2054 | if sender is None: 2055 | sender, to = self.resolve_from_to(msg) 2056 | if options.ignore_bot and isinstance(sender, SpecialUser) and sender.bot: 2057 | return 2058 | 2059 | if isinstance(msg, tl.types.MessageService): 2060 | info('on_telegram_update_message %r', msg.to_dict()) 2061 | return 2062 | 2063 | date = msg.date.replace(tzinfo=timezone.utc) 2064 | sender.max_id = msg.id 2065 | record = {'id': msg.id, 'date': date, 'from': sender, 'to': to, 'message': msg.message, 'inferred': False} 2066 | web.append_history(record) 2067 | # UpdateShort{,Chat}Message do not have update.media 2068 | # UpdateNewChannelMessage may have {media: None} 2069 | if getattr(msg, 'media', None): 2070 | text = None 2071 | if isinstance(msg.media, tl.types.MessageMediaContact): 2072 | typ = 'contact' 2073 | elif isinstance(msg.media, tl.types.MessageMediaDocument): 2074 | typ = 'document' 2075 | elif isinstance(msg.media, tl.types.MessageMediaEmpty): 2076 | typ = 'empty' 2077 | elif isinstance(msg.media, tl.types.MessageMediaGeo): 2078 | typ = 'geo' 2079 | text = '[{}] latitude:{} longitude:{}'.format(typ, msg.media.geo.long, msg.media.geo.lat) 2080 | elif isinstance(msg.media, tl.types.MessageMediaPhoto): 2081 | typ = 'photo' 2082 | elif isinstance(msg.media, tl.types.MessageMediaWebPage): 2083 | typ = 'webpage' 2084 | webpage = msg.media.webpage 2085 | if isinstance(webpage, tl.types.WebPage): 2086 | text = '[{}] {} {}'.format(typ, webpage.url.replace('\n', '\\n'), webpage.title) 2087 | elif isinstance(webpage, tl.types.WebPagePending): 2088 | text = '[WebPagePending] {}'.format(webpage.id) 2089 | web.webpage_id2sender_to[webpage.id] = (sender, to) 2090 | else: 2091 | typ = 'unknown' 2092 | if typ in ('document', 'photo'): 2093 | media_id = str(len(web.id2media)) 2094 | text = '[{}] {}/document/{}{}'.format(typ, options.http_url, media_id, {'photo': '.jpg'}.get(typ, '')) 2095 | if type == 'photo' and isinstance(msg.media.photo, tl.types.Photo): 2096 | for size in msg.media.photo.sizes: 2097 | if isinstance(size, tl.types.PhotoCachedSize): 2098 | text += ' {}x{}'.format(size.w, size.h) 2099 | elif isinstance(size, tl.types.PhotoSize): 2100 | text += ' {}x{},{}B'.format(size.w, size.h, size.size) 2101 | web.id2media[media_id] = (msg.media, None) 2102 | elif text is None: 2103 | text = '[{}] {}'.format(type(msg.media).__name__, msg.media.to_dict()) 2104 | if getattr(msg.media, 'caption', None): 2105 | text += ' | ' + msg.media.caption.replace('\n', '\\n') 2106 | if msg.message: 2107 | text += ' | ' + msg.message.replace('\n', '\\n') 2108 | else: 2109 | text = msg.message 2110 | 2111 | self.deliver_message(msg.id, sender, to, msg.date, text, fwd_from=msg.fwd_from, reply_to_msg_id=msg.reply_to_msg_id) 2112 | 2113 | def deliver_message(self, msg_id, sender, to, date, text, fwd_from=None, reply_to_msg_id=None): 2114 | for line in text.splitlines(): 2115 | if fwd_from is not None: 2116 | try: 2117 | from1 = server.ensure_special_user(fwd_from.from_id, None) 2118 | for client in server.auth_clients(): 2119 | line = '\x0315「Fwd {}」\x0f{}'.format( 2120 | client.nick if from1 == server else from1.nick, line) 2121 | break 2122 | except Exception as ex: 2123 | error('resolve fwd_from %r', ex) 2124 | elif reply_to_msg_id is not None: 2125 | if reply_to_msg_id in web.id2message: 2126 | refer = web.id2message[reply_to_msg_id] 2127 | else: 2128 | if to.is_type(tl.types.PeerChannel): 2129 | message = web.channel_message_get(to, reply_to_msg_id) 2130 | else: 2131 | message = web.message_get(reply_to_msg_id) 2132 | if isinstance(message, tl.types.MessageEmpty): 2133 | refer = None 2134 | else: 2135 | from1, to1 = self.resolve_from_to(message) 2136 | refer = {'id': message.id, 'date': message.date, 'from': from1, 'to': to1, 'message': message.message, 'inferred': True} 2137 | web.append_history(refer) 2138 | if refer is not None: 2139 | refer_text = refer['message'].replace('\n', '\\n') 2140 | if len(refer_text) > 8: 2141 | refer_text = refer_text[:8]+'...' 2142 | user = refer['from'] 2143 | for client in server.auth_clients(): 2144 | line = '\x0315「Re {}: {}」\x0f{}'.format( 2145 | client.nick if user == server else user.nick, refer_text, line) 2146 | break 2147 | 2148 | client = server.preferred_client() 2149 | if client: 2150 | where = sender if to == server else to 2151 | irc_log(where, client if where == server else where, date, 2152 | client if sender == server else sender, line) 2153 | 2154 | if isinstance(to, SpecialChannel): 2155 | for c in server.auth_clients(): 2156 | if c not in to.joined and 'm' not in to.mode: 2157 | if options.join in ('all', 'auto') and c not in to.explicit_parted or options.join == 'new': 2158 | c.auto_join(to) 2159 | for client in server.auth_clients(): 2160 | #if isinstance(to, Channel) and client not in to.joined or ( 2161 | # 'echo-message' not in client.capabilities and 2162 | # sender == server and 'media' not in data and 2163 | # data['text'] == to.last_text_by_client.get(client)): 2164 | # continue 2165 | sender_prefix = client.prefix if sender == server else sender.prefix 2166 | to_nick = client.nick if to == server else to.nick 2167 | line = ':{} PRIVMSG {} :{}'.format(sender_prefix, to_nick, line) 2168 | tags = [] 2169 | if msg_id is not None: 2170 | if 'draft/message-tags' in client.capabilities: 2171 | tags.append('draft/msgid={}'.format(msg_id)) 2172 | if 'server-time' in client.capabilities: 2173 | tags.append('time={}Z'.format(date.strftime('%FT%T.%f')[:23])) 2174 | if tags: 2175 | line = '@{} {}'.format(';'.join(tags), line) 2176 | client.write(line) 2177 | if msg_id is not None and options.mark_read == 'always' and isinstance(sender, SpecialUser): 2178 | if to is server: 2179 | if sender is not server: 2180 | web.mark_read(sender.peer, msg_id) 2181 | else: 2182 | web.mark_read(to.peer, msg_id) 2183 | 2184 | def on_disconnect(self, peername): 2185 | # PART all special channels, these chatrooms will be garbage collected 2186 | for room in self.peer_id2special_room.values(): 2187 | if self in room.joined: 2188 | room.on_part(self, 'client disconnection') 2189 | self.peer_id2special_room.clear() 2190 | 2191 | # instead of flooding +telegram with massive PART messages, 2192 | # take the shortcut by rejoining the client 2193 | self.user_id2special_user.clear() 2194 | status = StatusChannel.instance 2195 | clients = [x for x in status.members if isinstance(x, Client)] 2196 | status.members.clear() 2197 | for client in clients: 2198 | status.on_part(self, 'client disconnected from {}'.format(peername)) 2199 | status.on_join(self) 2200 | 2201 | 2202 | def main(): 2203 | ap = ArgParser(description='telegramircd brings Telegram to IRC clients') 2204 | ap.add('-c', '--config', is_config_file=True, help='config file path') 2205 | ap.add_argument('-d', '--debug', action='store_true', help='run ipdb on uncaught exception') 2206 | ap.add_argument('--dcc-send', type=int, default=10*1024*1024, help='size limit receiving from DCC SEND. 0: disable DCC SEND') 2207 | ap.add_argument('--heartbeat', type=int, default=30, help='time to wait for IRC commands. The server will send PING and close the connection after another timeout of equal duration if no commands is received.') 2208 | ap.add_argument('--http-cert', help='TLS certificate for HTTPS over TLS. You may concatenate certificate+key, specify a single PEM file and omit `--http-key`. Use HTTP if neither --http-cert nor --http-key is specified') 2209 | ap.add_argument('--http-url', default='http://localhost', 2210 | help='Show document links as http://localhost/document/$id') 2211 | ap.add_argument('--http-key', help='TLS key for HTTPS over TLS') 2212 | ap.add_argument('--http-listen', nargs='*', 2213 | help='HTTP listen addresses (overriding --listen)') 2214 | ap.add_argument('--http-port', type=int, default=9003, help='HTTP listen port, default: 9003') 2215 | ap.add_argument('-i', '--ignore', nargs='*', 2216 | help='list of ignored regex, do not auto join to a '+im_name+' chatroom whose channel name(generated from DisplayName) matches') 2217 | ap.add_argument('--ignore-bot', action='store_true', help='ignore private messages with bots') 2218 | ap.add_argument('-I', '--ignore-topic', nargs='*', 2219 | help='list of ignored regex, do not auto join to a '+im_name+' chatroom whose topic matches') 2220 | ap.add_argument('--irc-cert', help='TLS certificate for IRC over TLS. You may concatenate certificate+key, specify a single PEM file and omit `--irc-key`. Use plain IRC if neither --irc-cert nor --irc-key is specified') 2221 | ap.add_argument('--irc-key', help='TLS key for IRC over TLS') 2222 | ap.add_argument('--irc-listen', nargs='*', 2223 | help='IRC listen addresses (overriding --listen)') 2224 | ap.add_argument('--irc-nicks', nargs='*', default=[], 2225 | help='reserved nicks for clients') 2226 | ap.add_argument('--irc-password', default='', help='Set the IRC connection password') 2227 | ap.add_argument('--irc-port', type=int, default=6669, 2228 | help='IRC server listen port. default: 6669') 2229 | ap.add_argument('-j', '--join', choices=['all', 'auto', 'manual', 'new'], default='new', 2230 | help='join mode for '+im_name+' chatrooms. all: join all after connected; auto: join after the first message arrives; manual: no automatic join; new: join whenever messages arrive (even if after /part); default: auto') 2231 | ap.add_argument('-l', '--listen', nargs='*', default=['127.0.0.1'], 2232 | help='IRC/HTTP listen addresses, default: 127.0.0.1') 2233 | ap.add_argument('--logger-ignore', nargs='*', help='list of ignored regex, do not log contacts/chatrooms whose names match') 2234 | ap.add_argument('--logger-mask', help='WeeChat logger.mask.irc') 2235 | ap.add_argument('--logger-time-format', default='%H:%M', help='WeeChat logger.file.time_format') 2236 | ap.add_argument('--mark-read', choices=('always', 'reply', 'never'), default='reply', help='when to mark_read private messages from users. always: mark_read all messages; reply: mark_read when sending messages to the peer; never: never mark_read. default: reply'), 2237 | ap.add_argument('--paste-wait', type=float, default=0.1, help='PRIVMSG lines will be hold for up to $paste_wait seconds, lines in this interval will be packed to a multiline message') 2238 | ap.add_argument('-q', '--quiet', action='store_const', const=logging.WARN, dest='loglevel') 2239 | ap.add_argument('--sasl-password', default='', help='Set the SASL password') 2240 | ap.add_argument('--special-channel-prefix', choices=('&', '!', '#', '##'), default='&', help='prefix for SpecialChannel') 2241 | ap.add_argument('--tg-api-id', type=int, help='App api_id on https://my.telegram.org/apps') 2242 | ap.add_argument('--tg-api-hash', help='App api_hash on https://my.telegram.org/apps') 2243 | ap.add_argument('--tg-media-dir', default='/tmp/telegramircd', help='directory of media files') 2244 | ap.add_argument('--tg-session', default='telegramircd', help='Telethon session name') 2245 | ap.add_argument('--tg-session-dir', default='.', help='directory of Telethon session file') 2246 | ap.add_argument('--tg-phone', type=int, help='phone number') 2247 | ap.add_argument('-v', '--verbose', action='store_const', const=logging.DEBUG, dest='loglevel') 2248 | global options 2249 | options = ap.parse_args() 2250 | options.irc_nicks = [irc_lower(x) for x in options.irc_nicks] 2251 | 2252 | os.chdir(options.tg_session_dir) 2253 | os.makedirs(options.tg_media_dir, exist_ok=True) 2254 | 2255 | if sys.platform == 'linux': 2256 | # send to syslog if run as a daemon (no controlling terminal) 2257 | try: 2258 | with open('/dev/tty'): 2259 | pass 2260 | logging.basicConfig(format='%(levelname)s: %(message)s') 2261 | except OSError: 2262 | logging.root.addHandler(logging.handlers.SysLogHandler('/dev/log')) 2263 | else: 2264 | logging.basicConfig(format='%(levelname)s: %(message)s') 2265 | logging.root.setLevel(options.loglevel or logging.INFO) 2266 | 2267 | if options.http_cert or options.http_key: 2268 | http_tls = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 2269 | http_tls.load_cert_chain(options.http_cert or options.http_key, 2270 | options.http_key or options.http_cert) 2271 | else: 2272 | http_tls = None 2273 | if options.irc_cert or options.irc_key: 2274 | irc_tls = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 2275 | irc_tls.load_cert_chain(options.irc_cert or options.irc_key, 2276 | options.irc_key or options.irc_cert) 2277 | else: 2278 | irc_tls = None 2279 | 2280 | loop = asyncio.get_event_loop() 2281 | if options.debug: 2282 | sys.excepthook = ExceptionHook() 2283 | server = Server() 2284 | web = Web(http_tls) 2285 | 2286 | # FIXME 2287 | def exception_handler(loop, context): 2288 | server.loop.create_task(web.restart_telegram_cli()) 2289 | #loop.set_exception_handler(exception_handler) 2290 | 2291 | server.start(loop, irc_tls) 2292 | web.start(options.http_listen if options.http_listen else options.listen, 2293 | options.http_port, loop) 2294 | 2295 | for signame in ('SIGINT', 'SIGTERM'): 2296 | loop.add_signal_handler(getattr(signal, signame), loop.stop) 2297 | try: 2298 | loop.run_forever() 2299 | finally: 2300 | server.stop() 2301 | web.stop() 2302 | loop.close() 2303 | 2304 | 2305 | if __name__ == '__main__': 2306 | sys.exit(main()) 2307 | --------------------------------------------------------------------------------