├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── bot ├── bot.lua └── utils.lua ├── botapi.lua ├── data └── .gitkeep ├── libs ├── JSON.lua ├── hookio.js ├── mimetype.lua └── redis.lua ├── merbot ├── patches └── merbot.patch └── plugins ├── 9gag.lua ├── administration.lua ├── apod.lua ├── bing.lua ├── btc.lua ├── calculator.lua ├── cats.lua ├── currency.lua ├── dilbert.lua ├── dogify.lua ├── forecast.lua ├── gmaps.lua ├── gsmarena.lua ├── hackernews.lua ├── help.lua ├── id.lua ├── imdb.lua ├── isup.lua ├── kaskus.lua ├── kbbi.lua ├── logger.lua ├── patterns.lua ├── plugins.lua ├── quran.lua ├── reddit.lua ├── rss.lua ├── salat.lua ├── stats.lua ├── sudo.lua ├── time.lua ├── translate.lua ├── urbandictionary.lua ├── webshot.lua ├── whois.lua ├── xkcd.lua └── yify.lua /.gitignore: -------------------------------------------------------------------------------- 1 | log 2 | tg/ 3 | data/ 4 | .luarocks/ 5 | .telegram-cli/ 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tg"] 2 | path = tg 3 | url = https://github.com/vysheng/tg 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

merbot 2 | 3 |

**A Telegram Group Administration Bot** 4 | 5 | 6 |

Merbot illustration 7 | 8 | **Merbot** is a dedicated Telegram group administration bot based on [telegram-bot](https://github.com/yagop/telegram-bot). 9 | 10 | Consult [Merbot's wiki](https://github.com/rizaumami/merbot/wiki) for documentation. 11 | 12 | Please open a [github issue](https://github.com/rizaumami/merbot/issues) if you found an issue with `merbot`. 13 | 14 | *** 15 | 16 | _Thanks to [rizkymsyahputra](https://github.com/rizkymsyahputra) for the awesome merbot artwork_. 17 | -------------------------------------------------------------------------------- /bot/bot.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path .. ';.luarocks/share/lua/5.2/?.lua' 2 | .. ';.luarocks/share/lua/5.2/?/init.lua' 3 | package.cpath = package.cpath .. ';.luarocks/lib/lua/5.2/?.so' 4 | 5 | require('./bot/utils') 6 | 7 | local f = assert(io.popen('/usr/bin/git describe --tags', 'r')) 8 | VERSION = assert(f:read('*a')) 9 | f:close() 10 | 11 | -- This function is called when tg receive a msg 12 | function on_msg_receive (msg) 13 | if not started then 14 | return 15 | end 16 | 17 | vardump(msg) 18 | msg = pre_process_service_msg(msg) 19 | if msg_valid(msg) then 20 | msg = pre_process_msg(msg) 21 | if msg then 22 | match_plugins(msg) 23 | mark_read(get_receiver(msg), ok_cb, false) 24 | end 25 | end 26 | end 27 | 28 | function ok_cb(extra, success, result) 29 | end 30 | 31 | function on_binlog_replay_end() 32 | started = true 33 | postpone (cron_plugins, false, 60*5.0) 34 | -- See plugins/isup.lua as an example for cron 35 | 36 | _config = load_config() 37 | 38 | -- load plugins 39 | plugins = {} 40 | load_plugins() 41 | end 42 | 43 | function msg_valid(msg) 44 | -- Don't process outgoing messages 45 | -- if msg.out then 46 | -- print('\27[36mNot valid: msg from us\27[39m') 47 | -- return false 48 | -- end 49 | 50 | -- Before bot was started 51 | if msg.date < now then 52 | print('\27[36mNot valid: old msg\27[39m') 53 | return false 54 | end 55 | 56 | if msg.unread == 0 then 57 | print('\27[36mNot valid: readed\27[39m') 58 | return false 59 | end 60 | 61 | if not msg.to.peer_id then 62 | print('\27[36mNot valid: To id not provided\27[39m') 63 | return false 64 | end 65 | 66 | if not msg.from.peer_id then 67 | print('\27[36mNot valid: From id not provided\27[39m') 68 | return false 69 | end 70 | 71 | if msg.from.peer_id == tonumber(_config.bot_api.uid) then 72 | print('\27[36mNot valid: Msg from our companion bot\27[39m') 73 | return false 74 | end 75 | 76 | -- if msg.from.peer_id == our_id then 77 | -- print('\27[36mNot valid: Msg from our id\27[39m') 78 | -- return false 79 | -- end 80 | 81 | if msg.to.peer_type == 'encr_chat' then 82 | print('\27[36mNot valid: Encrypted chat\27[39m') 83 | return false 84 | end 85 | 86 | if msg.from.peer_id == 777000 then 87 | print('\27[36mNot valid: Telegram message\27[39m') 88 | return false 89 | end 90 | 91 | return true 92 | end 93 | 94 | local function process_api_msg(msg) 95 | if not is_chat_msg(msg) and msg.from.peer_id == _config.bot_api.uid then 96 | local loadapimsg = loadstring(msg.text) 97 | local apimsg = loadapimsg().message 98 | local target = tostring(apimsg.chat.id):gsub('-', '') 99 | 100 | if apimsg.chat.type == 'supergroup' or apimsg.chat.type == 'channel' then 101 | target = tostring(apimsg.chat.id):gsub('-100', '') 102 | end 103 | 104 | if not _config.administration[tonumber(target)] or apimsg.chat.type == 'supergroup' then 105 | msg.from.api = true 106 | msg.from.first_name = apimsg.from.first_name 107 | msg.from.peer_id = apimsg.from.id 108 | msg.from.username = apimsg.from.username 109 | msg.to.peer_id = apimsg.chat.id 110 | msg.to.peer_type = apimsg.chat.type 111 | msg.id = apimsg.message_id 112 | msg.text = apimsg.text 113 | 114 | if apimsg.chat.type == 'group' or apimsg.chat.type == 'supergroup' or apimsg.chat.type == 'channel' then 115 | msg.to.title = apimsg.chat.title 116 | msg.to.username = apimsg.chat.username 117 | end 118 | 119 | if apimsg.chat.type == 'private' then 120 | msg.to.first_name = apimsg.chat.first_name 121 | msg.to.username = apimsg.chat.username 122 | end 123 | 124 | if apimsg.reply_to_message then 125 | msg.reply_to_message = apimsg.reply_to_message 126 | end 127 | 128 | if apimsg.new_chat_title then 129 | msg.action = { title = apimsg.new_chat_title, type = 'chat_rename' } 130 | end 131 | 132 | if apimsg.new_chat_participant then 133 | msg.action.type = 'chat_add_user' 134 | msg.action.user.first_name = apimsg.new_chat_participant.first_name 135 | msg.action.user.peer_id = apimsg.new_chat_participant.id 136 | msg.action.user.username = apimsg.new_chat_participant.username 137 | end 138 | 139 | if apimsg.left_chat_participant then 140 | msg.action.type = 'chat_del_user' 141 | msg.action.user.first_name = apimsg.new_chat_participant.first_name 142 | msg.action.user.peer_id = apimsg.new_chat_participant.id 143 | msg.action.user.username = apimsg.new_chat_participant.username 144 | end 145 | 146 | if apimsg.new_chat_photo then 147 | msg.action.type = 'chat_change_photo' 148 | end 149 | 150 | if apimsg.delete_chat_photo then 151 | msg.action.type = 'chat_delete_photo' 152 | end 153 | 154 | -- if apimsg.group_chat_created then 155 | -- msg.action = { title = apimsg.group_chat_created, type = 'chat_created' } 156 | -- end 157 | -- if apimsg.supergroup_chat_created then 158 | -- msg.action = { title = apimsg.supergroup_chat_created , type = '' } 159 | -- end 160 | -- if apimsg.channel_chat_created then 161 | -- msg.action = { title = apimsg.channel_chat_created, type = '' } 162 | -- end 163 | -- if apimsg.migrate_to_chat_id then 164 | -- msg.action = { title = apimsg.migrate_to_chat_id, type = '' } 165 | -- end 166 | -- if apimsg.migrate_from_chat_id then 167 | -- msg.action = { title = apimsg.migrate_from_chat_id, type = 'migrated_from' } 168 | -- end 169 | end 170 | end 171 | return msg 172 | end 173 | 174 | function pre_process_service_msg(msg) 175 | if msg.service then 176 | local action = msg.action or {type=''} 177 | -- Double ! to discriminate of normal actions 178 | msg.text = '!!tgservice ' .. action.type 179 | 180 | -- wipe the data to allow the bot to read service messages 181 | if msg.out then 182 | msg.out = false 183 | end 184 | if msg.from.peer_id == our_id then 185 | msg.from.peer_id = 0 186 | end 187 | end 188 | 189 | -- if is_chat_msg(msg) then 190 | -- msg.is_processed_by_tgcli = true 191 | -- end 192 | 193 | -- if not msg.is_processed_by_tgcli then 194 | -- msg = process_api_msg(msg) 195 | -- end 196 | 197 | local msg = process_api_msg(msg) 198 | 199 | return msg 200 | end 201 | 202 | -- Apply plugin.pre_process function 203 | function pre_process_msg(msg) 204 | for name,plugin in pairs(plugins) do 205 | if plugin.pre_process and msg then 206 | print('Preprocess', name) 207 | msg = plugin.pre_process(msg) 208 | end 209 | end 210 | return msg 211 | end 212 | 213 | -- Go over enabled plugins patterns. 214 | function match_plugins(msg) 215 | for name, plugin in pairs(plugins) do 216 | match_plugin(plugin, name, msg) 217 | end 218 | end 219 | 220 | -- Check if plugin is on _config.disabled_plugin_on_chat table 221 | local function is_plugin_disabled_on_chat(plugin_name, receiver) 222 | local disabled_chats = _config.disabled_plugin_on_chat 223 | -- Table exists and chat has disabled plugins 224 | if disabled_chats and disabled_chats[receiver] then 225 | -- Checks if plugin is disabled on this chat 226 | for disabled_plugin,disabled in pairs(disabled_chats[receiver]) do 227 | if disabled_plugin == plugin_name and disabled then 228 | local warning = 'Plugin ' .. disabled_plugin .. ' is disabled on this chat' 229 | print(warning) 230 | send_msg(receiver, warning, ok_cb, false) 231 | return true 232 | end 233 | end 234 | end 235 | return false 236 | end 237 | 238 | function match_plugin(plugin, plugin_name, msg) 239 | -- Go over patterns. If one matches it's enough. 240 | for k, pattern in pairs(plugin.patterns) do 241 | local matches = match_pattern(pattern, msg.text) 242 | if matches then 243 | print('msg matches: ', pattern) 244 | 245 | if is_plugin_disabled_on_chat(plugin_name, get_receiver(msg)) then 246 | return nil 247 | end 248 | -- Function exists 249 | if plugin.run then 250 | -- If plugin is for privileged users only 251 | if not warns_user_not_allowed(plugin, msg) then 252 | local result = plugin.run(msg, matches) 253 | if result then 254 | send_large_msg(get_receiver(msg), result) 255 | end 256 | end 257 | end 258 | -- One patterns matches 259 | return 260 | end 261 | end 262 | end 263 | 264 | -- DEPRECATED, use send_large_msg(destination, text) 265 | function _send_msg(destination, text) 266 | send_large_msg(destination, text) 267 | end 268 | 269 | -- Create a basic config.lua file and saves it. 270 | function create_config() 271 | print('\n\27[1;33m Some functions and plugins using bot API as sender.\n' 272 | .. ' Please provide bots API token to ensure it\'s works as intended.\n' 273 | .. ' You can ENTER to skip and then fill the required info into data/config.lua.\27[0;39;49m\n') 274 | 275 | io.write('\27[1m Input your bot API key (token) here: \27[0;39;49m') 276 | 277 | local bot_api_key = io.read() 278 | local response = {} 279 | 280 | local botid = api_getme(bot_api_key) 281 | 282 | -- A simple config with basic plugins and ourselves as privileged user 283 | _config = { 284 | administration = {}, 285 | administrators = {}, 286 | api_key = { 287 | bing_url = 'https://datamarket.azure.com/dataset/bing/search', 288 | bing = '', 289 | forecast_url = 'https://developer.forecast.io/', 290 | forecast = '', 291 | globalquran_url = 'http://globalquran.com/contribute/signup.php', 292 | globalquran = '', 293 | muslimsalat_url = 'http://muslimsalat.com/panel/signup.php', 294 | muslimsalat = '', 295 | nasa_api_url = 'http://api.nasa.gov', 296 | nasa_api = '', 297 | thecatapi_url = 'http://thecatapi.com/docs.html', 298 | thecatapi = '', 299 | yandex_url = 'http://tech.yandex.com/keys/get', 300 | yandex = '', 301 | }, 302 | autoleave = false, 303 | bot_api = { 304 | key = bot_api_key, 305 | master = our_id, 306 | uid = botid.id, 307 | uname = botid.username 308 | }, 309 | disabled_channels = {}, 310 | disabled_plugin_on_chat = {}, 311 | enabled_plugins = { 312 | '9gag', 313 | 'administration', 314 | 'bing', 315 | 'calculator', 316 | 'cats', 317 | 'currency', 318 | 'dilbert', 319 | 'dogify', 320 | 'forecast', 321 | 'gmaps', 322 | 'hackernews', 323 | 'help', 324 | 'id', 325 | 'imdb', 326 | 'isup', 327 | 'patterns', 328 | 'plugins', 329 | 'reddit', 330 | 'rss', 331 | 'salat', 332 | 'stats', 333 | 'sudo', 334 | 'time', 335 | 'urbandictionary', 336 | 'webshot', 337 | 'whois', 338 | 'xkcd', 339 | }, 340 | globally_banned = {}, 341 | mkgroup = {founded = '', founder = '', title = '', gtype = '', uid = ''}, 342 | realm = {}, 343 | sudo_users = {[our_id] = our_id} 344 | } 345 | save_config() 346 | end 347 | 348 | -- Save the content of _config to config.lua 349 | function save_config() 350 | serialize_to_file(_config, './data/config.lua') 351 | print ('Saved config into ./data/config.lua') 352 | end 353 | 354 | -- Returns the config from config.lua file. 355 | -- If file doesn't exist, create it. 356 | function load_config() 357 | local exist = os.execute('test -s .telegram-cli/auth') 358 | if not exist then 359 | print('\n\27[1;33m You are not logged in.\n' 360 | .. ' Please log your bot in first, then restart merbot.\27[0;39;49m\n') 361 | return 362 | end 363 | local f = io.open('./data/config.lua', 'r') 364 | -- If config.lua doesn't exist 365 | if not f then 366 | print ('Created new config file: data/config.lua') 367 | create_config() 368 | print('\27[1;33m \n' 369 | .. ' Required datas has been saved to ./data/config.lua.\n' 370 | .. ' Please run bot-api.lua in another tmux/multiplexer window.\27[0;39;49m\n') 371 | else 372 | f:close() 373 | end 374 | local config = loadfile('./data/config.lua')() 375 | for v,user in pairs(config.sudo_users) do 376 | print('Allowed user: ' .. user) 377 | end 378 | return config 379 | end 380 | 381 | function on_our_id (id) 382 | our_id = id 383 | end 384 | 385 | function on_user_update (user, what) 386 | --vardump (user) 387 | end 388 | 389 | function on_chat_update (chat, what) 390 | --vardump(chat) 391 | end 392 | 393 | function on_secret_chat_update (schat, what) 394 | --vardump(schat) 395 | end 396 | 397 | function on_get_difference_end () 398 | end 399 | 400 | -- Enable plugins in config.json 401 | function load_plugins() 402 | if _config then 403 | for k, v in pairs(_config.enabled_plugins) do 404 | print('Loading plugin', v) 405 | 406 | local ok, err = pcall(function() 407 | plug = loadfile('plugins/' .. v .. '.lua')() 408 | plugins[v] = plug 409 | end) 410 | 411 | if not ok then 412 | print('\27[31mError loading plugin ' .. v .. '\27[39m') 413 | print('\27[31m' .. err .. '\27[39m') 414 | else 415 | if plug.is_need_api_key then 416 | local keyname = _config.api_key[plug.is_need_api_key[1]] 417 | if not keyname or keyname == '' then 418 | table.remove(_config.enabled_plugins, k) 419 | save_config() 420 | print('\27[33mMissing ' .. v .. ' api key\27[39m') 421 | print('\27[33m' .. v .. '.lua will not be enabled.\27[39m') 422 | end 423 | end 424 | end 425 | end 426 | end 427 | end 428 | 429 | function load_data(filename) 430 | if not filename then 431 | _groups_data = {} 432 | else 433 | _groups_data = loadfile(filename)() 434 | end 435 | return _groups_data 436 | end 437 | 438 | function save_data(data, file) 439 | file = io.open(file, 'w+') 440 | local serialized = serpent.block(data, {comment = false, name = '_'}) 441 | file:write(serialized) 442 | file:close() 443 | end 444 | 445 | -- Call and postpone execution for cron plugins 446 | function cron_plugins() 447 | for name, plugin in pairs(plugins) do 448 | -- Only plugins with cron function 449 | if plugin.cron ~= nil then 450 | plugin.cron() 451 | end 452 | end 453 | -- Called again in 5 mins 454 | postpone (cron_plugins, false, 5*60.0) 455 | end 456 | 457 | -- Start and load values 458 | our_id = 0 459 | now = os.time() 460 | math.randomseed(now) 461 | started = false 462 | -------------------------------------------------------------------------------- /bot/utils.lua: -------------------------------------------------------------------------------- 1 | URL = require "socket.url" 2 | http = require "socket.http" 3 | https = require "ssl.https" 4 | ltn12 = require "ltn12" 5 | serpent = require "serpent" 6 | feedparser = require "feedparser" 7 | multipart = require 'multipart-post' 8 | 9 | json = (loadfile "./libs/JSON.lua")() 10 | mimetype = (loadfile "./libs/mimetype.lua")() 11 | redis = (loadfile "./libs/redis.lua")() 12 | 13 | http.TIMEOUT = 10 14 | tgclie = './tg/bin/telegram-cli -k ./tg/tg-server-pub -c ./data/tg-cli.config -p default -De %q' 15 | 16 | function get_receiver(msg) 17 | if msg.to.peer_type == 'user' then 18 | return 'user#id' .. msg.from.peer_id 19 | end 20 | if msg.to.peer_type == 'chat' then 21 | return 'chat#id' .. msg.to.peer_id 22 | end 23 | if msg.to.peer_type == 'encr_chat' then 24 | return msg.to.print_name 25 | end 26 | if msg.to.peer_type == 'channel' then 27 | return 'channel#id' .. msg.to.peer_id 28 | end 29 | end 30 | 31 | function get_receiver_api(msg) 32 | if msg.to.peer_type == 'user' or msg.to.peer_type == 'private' then 33 | return msg.from.peer_id 34 | end 35 | if msg.to.peer_type == 'chat' or msg.to.peer_type == 'group' then 36 | if not msg.from.api then 37 | return '-' .. msg.to.peer_id 38 | else 39 | return msg.to.peer_id 40 | end 41 | end 42 | --TODO testing needed 43 | -- if msg.to.peer_type == 'encr_chat' then 44 | -- return msg.to.print_name 45 | -- end 46 | if msg.to.peer_type == 'channel' or msg.to.peer_type == 'supergroup' then 47 | if not msg.from.api then 48 | return '-100' .. msg.to.peer_id 49 | else 50 | return msg.to.peer_id 51 | end 52 | end 53 | end 54 | 55 | function is_chat_msg(msg) 56 | if msg.to.peer_type == 'private' or msg.to.peer_type == 'user' then 57 | return false 58 | else 59 | return true 60 | end 61 | end 62 | 63 | function is_realm(msg) 64 | if msg.to.peer_id == _config.realm.gid then 65 | return true 66 | else 67 | return false 68 | end 69 | end 70 | 71 | function string.random(length) 72 | local str = ''; 73 | for i = 1, length do 74 | math.random(97, 122) 75 | str = str .. string.char(math.random(97, 122)); 76 | end 77 | return str; 78 | end 79 | 80 | function string:split(sep) 81 | local sep, fields = sep or ':', {} 82 | local pattern = string.format("([^%s]+)", sep) 83 | self:gsub(pattern, function(c) fields[#fields+1] = c end) 84 | return fields 85 | end 86 | 87 | -- Removes spaces 88 | function string:trim() 89 | return self:gsub("^%s*(.-)%s*$", "%1") 90 | end 91 | 92 | function get_http_file_name(url, headers) 93 | -- Eg: foo.var 94 | local file_name = url:match("[^%w]+([%.%w]+)$") 95 | -- Any delimited alphanumeric on the url 96 | file_name = file_name or url:match("[^%w]+(%w+)[^%w]+$") 97 | -- Random name, hope content-type works 98 | file_name = file_name or str:random(5) 99 | 100 | local content_type = headers['content-type'] 101 | 102 | local extension = nil 103 | if content_type then 104 | extension = mimetype.get_mime_extension(content_type) 105 | end 106 | if extension then 107 | file_name = file_name .. '.' .. extension 108 | end 109 | 110 | local disposition = headers['content-disposition'] 111 | if disposition then 112 | -- attachment; filename=CodeCogsEqn.png 113 | file_name = disposition:match('filename=([^;]+)') or file_name 114 | end 115 | 116 | return file_name 117 | end 118 | 119 | -- Saves file to /tmp/. 120 | -- If file_name isn't provided, will get the text after the last "/" for 121 | -- filename and content-type for extension 122 | function download_to_file(url, file_name) 123 | print('url to download: ' .. url) 124 | 125 | local respbody = {} 126 | local options = { 127 | url = url, 128 | sink = ltn12.sink.table(respbody), 129 | redirect = true 130 | } 131 | 132 | -- nil, code, headers, status 133 | local response = nil 134 | 135 | if url:starts('https') then 136 | options.redirect = false 137 | response = {https.request(options)} 138 | else 139 | response = {http.request(options)} 140 | end 141 | 142 | local code = response[2] 143 | local headers = response[3] 144 | local status = response[4] 145 | 146 | if code ~= 200 then return nil end 147 | 148 | file_name = file_name or get_http_file_name(url, headers) 149 | 150 | local file_path = '/tmp/' .. file_name 151 | print('Saved to: ' .. file_path) 152 | 153 | file = io.open(file_path, "w+") 154 | file:write(table.concat(respbody)) 155 | file:close() 156 | 157 | return file_path 158 | end 159 | 160 | function vardump(value) 161 | print(serpent.block(value, {comment=false})) 162 | end 163 | 164 | -- http://stackoverflow.com/a/11130774/3163199 165 | function scandir(directory) 166 | local i, t, popen = 0, {}, io.popen 167 | for filename in popen('ls -a "' .. directory .. '"'):lines() do 168 | i = i + 1 169 | t[i] = filename 170 | end 171 | return t 172 | end 173 | 174 | -- http://www.lua.org/manual/5.2/manual.html#pdf-io.popen 175 | function run_command(str) 176 | local cmd = io.popen(str) 177 | local result = cmd:read('*all') 178 | cmd:close() 179 | return result 180 | end 181 | 182 | function is_administrate(msg, gid) 183 | local var = true 184 | if not _config.administration[gid] then 185 | var = false 186 | send_message(msg, 'I do not administrate this group', 'html') 187 | end 188 | return var 189 | end 190 | 191 | -- User has privileges 192 | function is_sudo(user_id) 193 | local var = false 194 | if _config.sudo_users[user_id] then 195 | var = true 196 | end 197 | return var 198 | end 199 | 200 | -- User is a global administrator 201 | function is_admin(user_id) 202 | local var = false 203 | if _config.administrators[user_id] then 204 | var = true 205 | end 206 | if _config.sudo_users[user_id] then 207 | var = true 208 | end 209 | return var 210 | end 211 | 212 | -- User is a group owner 213 | function is_owner(msg, chat_id, user_id) 214 | local var = false 215 | local data = load_data(_config.administration[chat_id]) 216 | if data.owners == nil then 217 | var = false 218 | elseif data.owners[user_id] then 219 | var = true 220 | end 221 | if _config.administrators[user_id] then 222 | var = true 223 | end 224 | if _config.sudo_users[user_id] then 225 | var = true 226 | end 227 | return var 228 | end 229 | 230 | -- User is a group moderator 231 | function is_mod(msg, chat_id, user_id) 232 | local var = false 233 | local data = load_data(_config.administration[chat_id]) 234 | if data.moderators == nil then 235 | var = false 236 | elseif data.moderators[user_id] then 237 | var = true 238 | end 239 | if data.owners == nil then 240 | var = false 241 | elseif data.owners[user_id] then 242 | var = true 243 | end 244 | if _config.administrators[user_id] then 245 | var = true 246 | end 247 | if _config.sudo_users[user_id] then 248 | var = true 249 | end 250 | return var 251 | end 252 | 253 | -- Returns bot api properties (as getMe method) 254 | function api_getme(bot_api_key) 255 | local response = {} 256 | local getme = https.request{ 257 | url = 'https://api.telegram.org/bot' .. bot_api_key .. '/getMe', 258 | method = "POST", 259 | sink = ltn12.sink.table(response), 260 | } 261 | local body = table.concat(response or {"no response"}) 262 | local jbody = json:decode(body) 263 | 264 | if jbody.ok then 265 | botid = jbody.result 266 | else 267 | print('Error: ' .. jbody.error_code .. ', ' .. jbody.description) 268 | botid = {id = '', username = ''} 269 | end 270 | 271 | return botid 272 | end 273 | 274 | -- Returns the name of the sender 275 | function get_name(msg) 276 | local name = msg.from.first_name 277 | if name == nil then 278 | name = msg.from.peer_id 279 | end 280 | return name 281 | end 282 | 283 | -- Returns at table of lua files inside plugins 284 | function plugins_names() 285 | local files = {} 286 | for k, v in pairs(scandir('plugins')) do 287 | -- Ends with .lua 288 | if (v:match(".lua$")) then 289 | table.insert(files, v) 290 | end 291 | end 292 | return files 293 | end 294 | 295 | -- Function name explains what it does. 296 | function file_exists(name) 297 | local f = io.open(name,'r') 298 | if f ~= nil then 299 | io.close(f) 300 | return true 301 | else 302 | return false 303 | end 304 | end 305 | 306 | -- Save into file the data serialized for lua. 307 | -- Set uglify true to minify the file. 308 | function serialize_to_file(data, file, uglify) 309 | file = io.open(file, 'w+') 310 | local serialized 311 | if not uglify then 312 | serialized = serpent.block(data, { 313 | comment = false, 314 | name = '_' 315 | }) 316 | else 317 | serialized = serpent.dump(data) 318 | end 319 | file:write(serialized) 320 | file:close() 321 | end 322 | 323 | -- Returns true if the string is empty 324 | function string:isempty() 325 | return self == nil or self == '' 326 | end 327 | 328 | -- Returns true if the string is blank 329 | function string:isblank() 330 | self = self:trim() 331 | return self:isempty() 332 | end 333 | 334 | -- Returns true if String starts with Start 335 | function string:starts(text) 336 | return text == self:sub(1, string.len(text)) 337 | end 338 | 339 | -- which bot messages sent by 340 | function send_message(msg, text, markdown) 341 | if msg.from.api then 342 | if msg.reply_to_message then 343 | msg.id = msg.reply_to_message.message_id 344 | end 345 | bot_sendMessage(get_receiver_api(msg), text, true, msg.id, markdown) 346 | else 347 | if msg.reply_id then 348 | msg.id = msg.reply_id 349 | end 350 | -- this will strip all html tags 351 | local text = text:gsub('<.->', '') 352 | reply_msg(msg.id, text, ok_cb, true) 353 | end 354 | end 355 | 356 | -- Send image to user and delete it when finished. 357 | -- cb_function and cb_extra are optionals callback 358 | function _send_photo(receiver, file_path, cb_function, cb_extra) 359 | local cb_extra = { 360 | file_path = file_path, 361 | cb_function = cb_function, 362 | cb_extra = cb_extra 363 | } 364 | -- Call to remove with optional callback 365 | send_photo(receiver, file_path, cb_function, cb_extra) 366 | end 367 | 368 | -- Download the image and send to receiver, it will be deleted. 369 | -- cb_function and cb_extra are optionals callback 370 | function send_photo_from_url(receiver, url, cb_function, cb_extra) 371 | -- If callback not provided 372 | cb_function = cb_function or ok_cb 373 | cb_extra = cb_extra or false 374 | 375 | local file_path = download_to_file(url, false) 376 | if not file_path then -- Error 377 | local text = 'Error downloading the image' 378 | send_msg(receiver, text, cb_function, cb_extra) 379 | else 380 | print('File path: ' .. file_path) 381 | _send_photo(receiver, file_path, cb_function, cb_extra) 382 | end 383 | end 384 | 385 | -- Same as send_photo_from_url but as callback function 386 | function send_photo_from_url_callback(cb_extra, success, result) 387 | local receiver = cb_extra.receiver 388 | local url = cb_extra.url 389 | 390 | local file_path = download_to_file(url, false) 391 | if not file_path then -- Error 392 | local text = 'Error downloading the image' 393 | send_msg(receiver, text, ok_cb, false) 394 | else 395 | print('File path: ' .. file_path) 396 | _send_photo(receiver, file_path, ok_cb, false) 397 | end 398 | end 399 | 400 | -- Send multiple images asynchronous. 401 | -- param urls must be a table. 402 | function send_photos_from_url(receiver, urls) 403 | local cb_extra = { 404 | receiver = receiver, 405 | urls = urls, 406 | remove_path = nil 407 | } 408 | send_photos_from_url_callback(cb_extra) 409 | end 410 | 411 | -- Use send_photos_from_url. 412 | -- This function might be difficult to understand. 413 | function send_photos_from_url_callback(cb_extra, success, result) 414 | -- cb_extra is a table containing receiver, urls and remove_path 415 | local receiver = cb_extra.receiver 416 | local urls = cb_extra.urls 417 | local remove_path = cb_extra.remove_path 418 | 419 | -- The previously image to remove 420 | if remove_path ~= nil then 421 | os.remove(remove_path) 422 | print('Deleted: ' .. remove_path) 423 | end 424 | 425 | -- Nil or empty, exit case (no more urls) 426 | if urls == nil or #urls == 0 then 427 | return false 428 | end 429 | 430 | -- Take the head and remove from urls table 431 | local head = table.remove(urls, 1) 432 | 433 | local file_path = download_to_file(head, false) 434 | local cb_extra = { 435 | receiver = receiver, 436 | urls = urls, 437 | remove_path = file_path 438 | } 439 | 440 | -- Send first and postpone the others as callback 441 | send_photo(receiver, file_path, send_photos_from_url_callback, cb_extra) 442 | end 443 | 444 | -- Callback to remove a file 445 | function rmtmp_cb(cb_extra, success, result) 446 | local file_path = cb_extra.file_path 447 | local cb_function = cb_extra.cb_function or ok_cb 448 | local cb_extra = cb_extra.cb_extra 449 | 450 | if file_path ~= nil then 451 | os.remove(file_path) 452 | print('Deleted: ' .. file_path) 453 | end 454 | -- Finally call the callback 455 | cb_function(cb_extra, success, result) 456 | end 457 | 458 | -- Send document to user and delete it when finished. 459 | -- cb_function and cb_extra are optionals callback 460 | function _send_document(receiver, file_path, cb_function, cb_extra) 461 | local cb_extra = { 462 | file_path = file_path, 463 | cb_function = cb_function or ok_cb, 464 | cb_extra = cb_extra or false 465 | } 466 | -- Call to remove with optional callback 467 | send_document(receiver, file_path, rmtmp_cb, cb_extra) 468 | end 469 | 470 | -- Download the image and send to receiver, it will be deleted. 471 | -- cb_function and cb_extra are optionals callback 472 | function send_document_from_url(receiver, url, cb_function, cb_extra) 473 | local file_path = download_to_file(url, false) 474 | print('File path: ' .. file_path) 475 | _send_document(receiver, file_path, cb_function, cb_extra) 476 | end 477 | 478 | -- Parameters in ?a=1&b=2 style 479 | function format_http_params(params, is_get) 480 | local str = '' 481 | -- If is get add ? to the beginning 482 | if is_get then str = '?' end 483 | local first = true -- Frist param 484 | for k,v in pairs (params) do 485 | if v then -- nil value 486 | if first then 487 | first = false 488 | str = str .. k .. '=' .. v 489 | else 490 | str = str .. '&' .. k .. '=' .. v 491 | end 492 | end 493 | end 494 | return str 495 | end 496 | 497 | -- Check if user can use the plugin and warns user 498 | -- Returns true if user was warned and false if not warned (is allowed) 499 | function warns_user_not_allowed(plugin, msg) 500 | if not user_allowed(plugin, msg) then 501 | local text = 'This plugin requires privileged user' 502 | reply_msg(msg.id, text, ok_cb, true) 503 | return true 504 | else 505 | return false 506 | end 507 | end 508 | 509 | -- Check if user can use the plugin 510 | function user_allowed(plugin, msg) 511 | if plugin.moderated and not is_mod(msg, msg.to.peer_id, msg.from.peer_id) then 512 | if plugin.moderated and not is_owner(msg, msg.to.peer_id, msg.from.peer_id) then 513 | if plugin.moderated and not is_admin(msg.from.peer_id) then 514 | if plugin.moderated and not is_sudo(msg.from.peer_id) then 515 | return false 516 | end 517 | end 518 | end 519 | end 520 | -- If plugins privileged = true 521 | if plugin.privileged and not is_sudo(msg.from.peer_id) then 522 | return false 523 | end 524 | return true 525 | end 526 | 527 | function send_order_msg(destination, msgs) 528 | local cb_extra = { 529 | destination = destination, 530 | msgs = msgs 531 | } 532 | send_order_msg_callback(cb_extra, true) 533 | end 534 | 535 | function send_order_msg_callback(cb_extra, success, result) 536 | local destination = cb_extra.destination 537 | local msgs = cb_extra.msgs 538 | local file_path = cb_extra.file_path 539 | if file_path ~= nil then 540 | os.remove(file_path) 541 | print('Deleted: ' .. file_path) 542 | end 543 | if type(msgs) == 'string' then 544 | send_large_msg(destination, msgs) 545 | elseif type(msgs) ~= 'table' then 546 | return 547 | end 548 | if #msgs < 1 then 549 | return 550 | end 551 | local msg = table.remove(msgs, 1) 552 | local new_cb_extra = { 553 | destination = destination, 554 | msgs = msgs 555 | } 556 | if type(msg) == 'string' then 557 | send_msg(destination, msg, send_order_msg_callback, new_cb_extra) 558 | elseif type(msg) == 'table' then 559 | local typ = msg[1] 560 | local nmsg = msg[2] 561 | new_cb_extra.file_path = nmsg 562 | if typ == 'document' then 563 | send_document(destination, nmsg, send_order_msg_callback, new_cb_extra) 564 | elseif typ == 'image' or typ == 'photo' then 565 | send_photo(destination, nmsg, send_order_msg_callback, new_cb_extra) 566 | elseif typ == 'audio' then 567 | send_audio(destination, nmsg, send_order_msg_callback, new_cb_extra) 568 | elseif typ == 'video' then 569 | send_video(destination, nmsg, send_order_msg_callback, new_cb_extra) 570 | else 571 | send_file(destination, nmsg, send_order_msg_callback, new_cb_extra) 572 | end 573 | end 574 | end 575 | 576 | -- Same as send_large_msg_callback but friendly params 577 | function send_large_msg(destination, text) 578 | local cb_extra = { 579 | destination = destination, 580 | text = text 581 | } 582 | send_large_msg_callback(cb_extra, true) 583 | end 584 | 585 | -- If text is longer than 4096 chars, send multiple msg. 586 | -- https://core.telegram.org/method/messages.sendMessage 587 | function send_large_msg_callback(cb_extra, success, result) 588 | local text_max = 4096 589 | 590 | local destination = cb_extra.destination 591 | local text = cb_extra.text 592 | local text_len = string.len(text) 593 | local num_msg = math.ceil(text_len / text_max) 594 | 595 | if num_msg <= 1 then 596 | send_msg(destination, text, ok_cb, false) 597 | else 598 | 599 | local my_text = text:sub(1, 4096) 600 | local rest = text:sub(4096, text_len) 601 | 602 | local cb_extra = { 603 | destination = destination, 604 | text = rest 605 | } 606 | 607 | send_msg(destination, my_text, send_large_msg_callback, cb_extra) 608 | end 609 | end 610 | 611 | -- Returns a table with matches or nil 612 | function match_pattern(pattern, text, lower_case) 613 | if text then 614 | local matches = {} 615 | if lower_case then 616 | matches = { string.match(text:lower(), pattern) } 617 | else 618 | matches = { string.match(text, pattern) } 619 | end 620 | if next(matches) then 621 | return matches 622 | end 623 | end 624 | -- nil 625 | end 626 | 627 | -- Function to read data from files 628 | function load_from_file(file, default_data) 629 | local f = io.open(file, 'r+') 630 | -- If file doesn't exists 631 | if f == nil then 632 | -- Create a new empty table 633 | default_data = default_data or {} 634 | serialize_to_file(default_data, file) 635 | print ('Created file', file) 636 | else 637 | print ('Data loaded from file', file) 638 | f:close() 639 | end 640 | return loadfile (file)() 641 | end 642 | 643 | -- See http://stackoverflow.com/a/14899740 644 | function unescape_html(str) 645 | local map = { 646 | ["lt"] = "<", 647 | ["gt"] = ">", 648 | ["amp"] = "&", 649 | ["quot"] = '"', 650 | ["apos"] = "'" 651 | } 652 | new = str:gsub('(&(#?x?)([%d%a]+);)', function(orig, n, s) 653 | var = map[s] or n == "#" and string.char(s) 654 | var = var or n == "#x" and string.char(tonumber(s,16)) 655 | var = var or orig 656 | return var 657 | end) 658 | return new 659 | end 660 | 661 | function markdown_escape(text) 662 | text = text:gsub('_', '\\_') 663 | text = text:gsub('%[', '\\[') 664 | text = text:gsub('%]', '\\]') 665 | text = text:gsub('%*', '\\*') 666 | text = text:gsub('`', '\\`') 667 | return text 668 | end 669 | 670 | function group_into_three(number) 671 | while true do 672 | number, k = string.gsub(number, "^(-?%d+)(%d%d%d)", '%1.%2') 673 | 674 | if (k==0) then 675 | break 676 | end 677 | end 678 | return number 679 | end 680 | 681 | function pairsByKeys(t, f) 682 | local a = {} 683 | for n in pairs(t) do 684 | a[#a+1] = n 685 | end 686 | table.sort(a, f) 687 | local i = 0 -- iterator variable 688 | local iter = function () -- iterator function 689 | i = i + 1 690 | if a[i] == nil then 691 | return nil 692 | else 693 | return a[i], t[a[i]] 694 | end 695 | end 696 | return iter 697 | end 698 | 699 | -- Gets coordinates for a location. 700 | function get_coords(msg, input) 701 | local url = 'https://maps.googleapis.com/maps/api/geocode/json?address=' .. URL.escape(input) 702 | 703 | local jstr, res = http.request(url) 704 | if res ~= 200 then 705 | reply_msg(msg.id, 'Connection error.', ok_cb, true) 706 | return 707 | end 708 | 709 | local jdat = json:decode(jstr) 710 | if jdat.status == 'ZERO_RESULTS' then 711 | reply_msg(msg.id, 'ZERO_RESULTS', ok_cb, true) 712 | return 713 | end 714 | 715 | return { 716 | lat = jdat.results[1].geometry.location.lat, 717 | lon = jdat.results[1].geometry.location.lng, 718 | formatted_address = jdat.results[1].formatted_address 719 | } 720 | end 721 | 722 | -- Text formatting is server side. And (until now) only for API bots. 723 | -- So, here is a simple workaround; send message through Telegram official API. 724 | -- You need to provide your API bots TOKEN in config.lua. 725 | local function bot(method, parameters, file) 726 | local parameters = parameters or {} 727 | for k,v in pairs(parameters) do 728 | parameters[k] = tostring(v) 729 | end 730 | if file and next(file) ~= nil then 731 | local file_type, file_name = next(file) 732 | local file_file = io.open(file_name, 'r') 733 | local file_data = { 734 | filename = file_name, 735 | data = file_file:read('*a') 736 | } 737 | file_file:close() 738 | parameters[file_type] = file_data 739 | end 740 | if next(parameters) == nil then 741 | parameters = {''} 742 | end 743 | 744 | if parameters.reply_to_message_id and #parameters.reply_to_message_id > 30 then 745 | parameters.reply_to_message_id = nil 746 | end 747 | 748 | local response = {} 749 | local body, boundary = multipart.encode(parameters) 750 | local success = https.request{ 751 | url = 'https://api.telegram.org/bot' .. _config.bot_api.key .. '/' .. method, 752 | method = 'POST', 753 | headers = { 754 | ["Content-Type"] = "multipart/form-data; boundary=" .. boundary, 755 | ["Content-Length"] = #body, 756 | }, 757 | source = ltn12.source.string(body), 758 | sink = ltn12.sink.table(response) 759 | } 760 | local data = table.concat(response) 761 | local jdata = json:decode(data) 762 | if not jdata.ok then 763 | vardump(jdata) 764 | end 765 | end 766 | 767 | function bot_sendMessage(chat_id, text, disable_web_page_preview, reply_to_message_id, parse_mode) 768 | return bot('sendMessage', { 769 | chat_id = chat_id, 770 | text = text, 771 | disable_web_page_preview = disable_web_page_preview, 772 | reply_to_message_id = reply_to_message_id, 773 | parse_mode = parse_mode or nil 774 | } ) 775 | end 776 | 777 | function bot_sendPhoto(chat_id, photo, caption, disable_notification, reply_to_message_id) 778 | return bot('sendPhoto', { 779 | chat_id = chat_id, 780 | caption = caption, 781 | disable_notification = disable_notification, 782 | reply_to_message_id = reply_to_message_id, 783 | }, {photo = photo} ) 784 | end 785 | 786 | function bot_sendLocation(chat_id, latitude, longitude, disable_notification, reply_to_message_id) 787 | return bot('sendLocation', { 788 | chat_id = chat_id, 789 | latitude = latitude, 790 | longitude = longitude, 791 | disable_notification = disable_notification, 792 | reply_to_message_id = reply_to_message_id, 793 | }) 794 | end 795 | 796 | function bot_sendDocument(chat_id, document, caption, disable_notification, reply_to_message_id) 797 | return bot('sendDocument', { 798 | chat_id = chat_id, 799 | document = document, 800 | caption = caption, 801 | disable_notification = disable_notification, 802 | reply_to_message_id = reply_to_message_id, 803 | }, {document = document}) 804 | end 805 | -------------------------------------------------------------------------------- /botapi.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | As for now, telegram-cli have unresolved bug that it's unable to read messages 3 | in supergroup with 200+ members. 4 | 5 | I cannot C, hence this shitty workaround. 6 | This is an api bot script that relaying all command messages to telegram-cli 7 | bot account. 8 | 9 | Run this script in separate tmux/multiplexer window. 10 | --]] 11 | 12 | package.path = package.path .. ';.luarocks/share/lua/5.2/?.lua' 13 | .. ';.luarocks/share/lua/5.2/?/init.lua' 14 | package.cpath = package.cpath .. ';.luarocks/lib/lua/5.2/?.so' 15 | 16 | local https = require "ssl.https" 17 | local serpent = require "serpent" 18 | local json = (loadfile "./libs/JSON.lua")() 19 | 20 | local config = (loadfile './data/config.lua')() 21 | local url = 'https://api.telegram.org/bot' .. config.bot_api.key 22 | local offset = 0 23 | 24 | local function getUpdates() 25 | local response = {} 26 | local success, code, headers, status = https.request{ 27 | url = url .. '/getUpdates?timeout=20&limit=1&offset=' .. offset, 28 | method = "POST", 29 | sink = ltn12.sink.table(response), 30 | } 31 | 32 | local body = table.concat(response or {"no response"}) 33 | if (success == 1) then 34 | return json:decode(body) 35 | else 36 | return nil, "Request Error" 37 | end 38 | end 39 | 40 | function vardump(value) 41 | print(serpent.block(value, {comment=false})) 42 | end 43 | 44 | local function run() 45 | while true do 46 | local updates = getUpdates() 47 | vardump(updates) 48 | if(updates) then 49 | if (updates.result) then 50 | for i=1, #updates.result do 51 | local msg = updates.result[i] 52 | offset = msg.update_id + 1 53 | print '==================================================' 54 | vardump(msg) 55 | if msg.message and msg.message.date > (os.time() - 5) then 56 | if msg.message.text and msg.message.text:match('^[!/]') then 57 | https.request{ 58 | url = url .. '/sendMessage?chat_id=' .. config.bot_api.master .. '&text=' .. serpent.dump(msg), 59 | method = "POST", 60 | } 61 | end 62 | end 63 | end 64 | end 65 | end 66 | end 67 | end 68 | 69 | return run() -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizaumami/merbot/5f9b7ffaf57d7680accc2ae795bb388705a44364/data/.gitkeep -------------------------------------------------------------------------------- /libs/hookio.js: -------------------------------------------------------------------------------- 1 | // https://unnikked.ga/build-telegram-bot-hook-io/ 2 | 3 | module['exports'] = function relayBot (hook) { 4 | var request = require('request'); 5 | var msg = hook.params.message; 6 | var TOKEN = hook.env.merbot_key; // change env to reflect your bot 7 | var MASTER = hook.env.mimin_teh_bot; // change env to your tg-cli bots id 8 | var closetag = '}};return _;end'; 9 | var textmessage = msg.text; 10 | 11 | if (textmessage.match(/^!.*/)) { 12 | var fmessage = 'do local _={message={chat={title="' + msg.chat.title + 13 | '",id=' + msg.chat.id + 14 | ',type="' + msg.chat.type + 15 | '",username="' + msg.chat.username + 16 | '",first_name="' + msg.chat.first_name + 17 | '"},from={first_name="' + msg.from.first_name + 18 | '",id=' + msg.from.id + 19 | ',username="' + msg.from.username + 20 | '"},date=' + msg.date + 21 | ',text="' + msg.text + 22 | '",message_id=' + msg.message_id; 23 | 24 | if (msg.new_chat_title) { 25 | var fmessage = fmessage + ',new_chat_title="' + msg.new_chat_title + '"' 26 | } 27 | 28 | if (msg.new_chat_participant) { 29 | var fmessage = fmessage + ',new_chat_participant={first_name="' + 30 | msg.new_chat_participant.first_name + 31 | '",id=' + msg.new_chat_participant.id + 32 | ',username="' + msg.new_chat_participant.username + '"}'; 33 | } 34 | 35 | if (msg.left_chat_participant) { 36 | var fmessage = fmessage + ',left_chat_participan={first_name="' + 37 | msg.left_chat_participant.first_name + 38 | '",id=' + msg.left_chat_participant.id + 39 | ', username="' + msg.left_chat_participant.username + '"}'; 40 | } 41 | 42 | if (msg.new_chat_photo) { 43 | var fmessage = fmessage + ',new_chat_photo={}' 44 | } 45 | 46 | if (msg.delete_chat_photo) { 47 | var fmessage = fmessage + ',delete_chat_photo = true' 48 | } 49 | 50 | if (msg.reply_to_message) { 51 | var reply = msg.reply_to_message 52 | var fmessage = fmessage + 53 | ',reply_to_message={chat={id=' + reply.chat.id + 54 | ',title="' + reply.chat.title + 55 | '",type="' + reply.chat.type + 56 | '",username="' + reply.chat.username + 57 | '"},date=' + reply.date + 58 | ',from={first_name="' + reply.from.first_name + 59 | '",id=' + reply.from.id + 60 | ',type=' + reply.from.type + 61 | ',username="' + reply.from.username + 62 | '"},message_id=' + reply.message_id + 63 | ',text="' + reply.text + '"},'; 64 | } 65 | 66 | request 67 | .post('https://api.telegram.org/bot' + TOKEN + '/sendMessage') 68 | .form({ 69 | "chat_id": MASTER, 70 | "text": fmessage + closetag, 71 | }); 72 | }; 73 | }; -------------------------------------------------------------------------------- /libs/mimetype.lua: -------------------------------------------------------------------------------- 1 | -- Thanks to https://github.com/catwell/lua-toolbox/blob/master/mime.types 2 | do 3 | 4 | local mimetype = {} 5 | 6 | -- TODO: Add more? 7 | local types = { 8 | ["text/html"] = "html", 9 | ["text/css"] = "css", 10 | ["text/xml"] = "xml", 11 | ["image/gif"] = "gif", 12 | ["image/jpeg"] = "jpg", 13 | ["application/x-javascript"] = "js", 14 | ["application/atom+xml"] = "atom", 15 | ["application/rss+xml"] = "rss", 16 | ["text/mathml"] = "mml", 17 | ["text/plain"] = "txt", 18 | ["text/vnd.sun.j2me.app-descriptor"] = "jad", 19 | ["text/vnd.wap.wml"] = "wml", 20 | ["text/x-component"] = "htc", 21 | ["image/png"] = "png", 22 | ["image/tiff"] = "tiff", 23 | ["image/vnd.wap.wbmp"] = "wbmp", 24 | ["image/x-icon"] = "ico", 25 | ["image/x-jng"] = "jng", 26 | ["image/x-ms-bmp"] = "bmp", 27 | ["image/svg+xml"] = "svg", 28 | ["image/webp"] = "webp", 29 | ["application/java-archive"] = "jar", 30 | ["application/mac-binhex40"] = "hqx", 31 | ["application/msword"] = "doc", 32 | ["application/pdf"] = "pdf", 33 | ["application/postscript"] = "ps", 34 | ["application/rtf"] = "rtf", 35 | ["application/vnd.ms-excel"] = "xls", 36 | ["application/vnd.ms-powerpoint"] = "ppt", 37 | ["application/vnd.wap.wmlc"] = "wmlc", 38 | ["application/vnd.google-earth.kml+xml"] = "kml", 39 | ["application/vnd.google-earth.kmz"] = "kmz", 40 | ["application/x-7z-compressed"] = "7z", 41 | ["application/x-cocoa"] = "cco", 42 | ["application/x-java-archive-diff"] = "jardiff", 43 | ["application/x-java-jnlp-file"] = "jnlp", 44 | ["application/x-makeself"] = "run", 45 | ["application/x-perl"] = "pl", 46 | ["application/x-pilot"] = "prc", 47 | ["application/x-rar-compressed"] = "rar", 48 | ["application/x-redhat-package-manager"] = "rpm", 49 | ["application/x-sea"] = "sea", 50 | ["application/x-shockwave-flash"] = "swf", 51 | ["application/x-stuffit"] = "sit", 52 | ["application/x-tcl"] = "tcl", 53 | ["application/x-x509-ca-cert"] = "crt", 54 | ["application/x-xpinstall"] = "xpi", 55 | ["application/xhtml+xml"] = "xhtml", 56 | ["application/zip"] = "zip", 57 | ["application/octet-stream"] = "bin", 58 | ["audio/midi"] = "mid", 59 | ["audio/mpeg"] = "mp3", 60 | ["audio/ogg"] = "ogg", 61 | ["audio/x-m4a"] = "m4a", 62 | ["audio/x-realaudio"] = "ra", 63 | ["video/3gpp"] = "3gpp", 64 | ["video/mp4"] = "mp4", 65 | ["video/mpeg"] = "mpeg", 66 | ["video/quicktime"] = "mov", 67 | ["video/webm"] = "webm", 68 | ["video/x-flv"] = "flv", 69 | ["video/x-m4v"] = "m4v", 70 | ["video/x-mng"] = "mng", 71 | ["video/x-ms-asf"] = "asf", 72 | ["video/x-ms-wmv"] = "wmv", 73 | ["video/x-msvideo"] = "avi" 74 | } 75 | 76 | -- Returns the common file extension from a content-type 77 | function mimetype.get_mime_extension(content_type) 78 | return types[content_type] 79 | end 80 | 81 | -- Returns the mimetype and subtype 82 | function mimetype.get_content_type(extension) 83 | for k,v in pairs(types) do 84 | if v == extension then 85 | return k 86 | end 87 | end 88 | end 89 | 90 | -- Returns the mimetype without the subtype 91 | function mimetype.get_content_type_no_sub(extension) 92 | for k,v in pairs(types) do 93 | if v == extension then 94 | -- Before / 95 | return k:match('([%w-]+)/') 96 | end 97 | end 98 | end 99 | 100 | return mimetype 101 | end -------------------------------------------------------------------------------- /libs/redis.lua: -------------------------------------------------------------------------------- 1 | local Redis = require 'redis' 2 | local FakeRedis = require 'fakeredis' 3 | 4 | local params = { 5 | host = os.getenv('REDIS_HOST') or '127.0.0.1', 6 | port = tonumber(os.getenv('REDIS_PORT') or 6379) 7 | } 8 | 9 | local database = os.getenv('REDIS_DB') 10 | local password = os.getenv('REDIS_PASSWORD') 11 | 12 | -- Overwrite HGETALL 13 | Redis.commands.hgetall = Redis.command('hgetall', { 14 | response = function(reply, command, ...) 15 | local new_reply = { } 16 | for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end 17 | return new_reply 18 | end 19 | }) 20 | 21 | local redis = nil 22 | 23 | -- Won't launch an error if fails 24 | local ok = pcall(function() 25 | redis = Redis.connect(params) 26 | end) 27 | 28 | if not ok then 29 | 30 | local fake_func = function() 31 | print('\27[31mCan\'t connect with Redis, install/configure it!\27[39m') 32 | end 33 | fake_func() 34 | fake = FakeRedis.new() 35 | 36 | print('\27[31mRedis addr: '..params.host..'\27[39m') 37 | print('\27[31mRedis port: '..params.port..'\27[39m') 38 | 39 | redis = setmetatable({fakeredis=true}, { 40 | __index = function(a, b) 41 | if b ~= 'data' and fake[b] then 42 | fake_func(b) 43 | end 44 | return fake[b] or fake_func 45 | end }) 46 | 47 | else 48 | if password then 49 | redis:auth(password) 50 | end 51 | if database then 52 | redis:select(database) 53 | end 54 | end 55 | 56 | 57 | return redis 58 | -------------------------------------------------------------------------------- /merbot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #set -euo 4 | 5 | THIS_DIR=$(cd "$(dirname "$0")"; pwd) 6 | cd "$THIS_DIR" 7 | 8 | update() { 9 | git pull 10 | git submodule update --init --recursive 11 | install_rocks 12 | } 13 | 14 | # Will install luarocks on THIS_DIR/.luarocks 15 | install_luarocks() { 16 | git clone https://github.com/keplerproject/luarocks.git 17 | cd luarocks 18 | git checkout tags/v2.4.0 # Current stable 19 | 20 | PREFIX="$THIS_DIR/.luarocks" 21 | 22 | ./configure --prefix="$PREFIX" --sysconfdir="$PREFIX"/luarocks --force-config 23 | 24 | RET=$? 25 | if [ $RET -ne 0 ]; then 26 | printf '\e[1;31m%s\n\e[0;39;49m' 'Error. Exiting.' 27 | exit $RET 28 | fi 29 | 30 | make build && make install 31 | RET=$? 32 | if [ $RET -ne 0 ]; then 33 | printf '\e[1;31m%s\n\e[0;39;49m' 'Error. Exiting.' 34 | exit $RET 35 | fi 36 | 37 | cd .. 38 | rm -rf luarocks 39 | } 40 | 41 | install_rocks() { 42 | ./.luarocks/bin/luarocks install lbase64 20120807-3 43 | RET=$? 44 | if [ $RET -ne 0 ]; then 45 | printf '\e[1;31m%s\n\e[0;39;49m' 'Error. Exiting.' 46 | exit $RET 47 | fi 48 | for i in luasec luasocket oauth redis-lua lua-cjson fakeredis xml feedparser serpent multipart-post; do 49 | ./.luarocks/bin/luarocks install "$i" 50 | RET=$? 51 | if [ $RET -ne 0 ]; then 52 | printf '\e[1;31m%s\n\e[0;39;49m' 'Error. Exiting.' 53 | exit $RET 54 | fi 55 | done 56 | } 57 | 58 | merbot_upstart() { 59 | printf '%s\n' " 60 | description 'Merbots upstart script.' 61 | 62 | respawn 63 | respawn limit 15 5 64 | 65 | start on runlevel [2345] 66 | stop on shutdown 67 | 68 | setuid $(whoami) 69 | exec /bin/sh $(pwd)/merbot 70 | " | sudo tee /etc/init/merbot.conf > /dev/null 71 | 72 | [[ -f /etc/init/merbot.conf ]] && printf '%s\n' ' 73 | 74 | Upstart script installed to /etc/init/merbot.conf. 75 | Now you can: 76 | Start merbot with : sudo start merbot 77 | See merbot status with: sudo status merbot 78 | Stop merbot with : sudo stop merbot 79 | 80 | ' 81 | } 82 | 83 | merbotapi_upstart() { 84 | printf '%s\n' " 85 | description 'Merbot APIs upstart script.' 86 | 87 | respawn 88 | respawn limit 15 5 89 | 90 | start on runlevel [2345] 91 | stop on shutdown 92 | 93 | setuid $(whoami) 94 | exec /usr/bin/lua $(pwd)/botapi.lua 95 | " | sudo tee /etc/init/merbotapi.conf > /dev/null 96 | 97 | [[ -f /etc/init/merbotapi.conf ]] && printf '%s\n' ' 98 | 99 | Upstart script installed to /etc/init/merbotapi.conf. 100 | Now you can: 101 | Start merbot api with : sudo start merbotapi 102 | See merbot api status with: sudo status merbotapi 103 | Stop merbot api with : sudo stop merbotapi 104 | 105 | ' 106 | } 107 | 108 | tgcli_config() { 109 | mkdir -p "$THIS_DIR"/.telegram-cli 110 | printf '%s\n' " 111 | default_profile = \"default\"; 112 | 113 | default = { 114 | config_directory = \"$THIS_DIR/.telegram-cli\"; 115 | auth_file = \"$THIS_DIR/.telegram-cli/auth\"; 116 | test = false; 117 | msg_num = true; 118 | log_level = 2; 119 | }; 120 | " > "$THIS_DIR"/data/tg-cli.config 121 | } 122 | 123 | install() { 124 | git pull 125 | git submodule update --init --recursive 126 | patch -i 'patches/merbot.patch' -p 0 --batch --forward 127 | RET=$?; 128 | 129 | cd tg 130 | if [ $RET -ne 0 ]; then 131 | autoconf -i 132 | fi 133 | ./configure && make 134 | 135 | RET=$? 136 | if [ $RET -ne 0 ]; then 137 | printf '\e[1;31m%s\n\e[0;39;49m' 'Error. Exiting.' 138 | exit $RET 139 | fi 140 | cd .. 141 | install_luarocks 142 | install_rocks 143 | tgcli_config 144 | } 145 | 146 | if [[ "$1" = "install" ]]; then 147 | install 148 | elif [[ "$1" = "update" ]]; then 149 | update 150 | elif [[ "$1" = "upstart" ]]; then 151 | merbot_upstart 152 | merbotapi_upstart 153 | else 154 | if [[ ! -f ./tg/telegram.h ]]; then 155 | printf '\e[1;31m%s\n\e[0;39;49m' ' tg not found' " Run $0 install" 156 | exit 1 157 | fi 158 | 159 | if [[ ! -f ./tg/bin/telegram-cli ]]; then 160 | printf '\e[1;31m%s\n\e[0;39;49m' ' tg binary not found' " Run $0 install" 161 | exit 1 162 | fi 163 | tgcli_config 164 | rm ./telegram-cli/state 165 | ./tg/bin/telegram-cli -k ./tg/tg-server-pub --disable-link-preview -s ./bot/bot.lua -l 1 -E -c ./data/tg-cli.config -p default "$@" 166 | fi 167 | -------------------------------------------------------------------------------- /patches/merbot.patch: -------------------------------------------------------------------------------- 1 | --- tg/lua-tg.c 2016-04-21 23:15:20.657750668 +0700 2 | +++ /home/iza/lua-tg.c 2016-04-21 23:15:02.649847315 +0700 3 | @@ -161,6 +161,7 @@ 4 | my_lua_checkstack (luaState, 4); 5 | lua_add_string_field ("title", P->channel.title); 6 | lua_add_string_field ("about", P->channel.about); 7 | + lua_add_string_field ("username", P->channel.username); 8 | lua_add_num_field ("participants_count", P->channel.participants_count); 9 | lua_add_num_field ("admins_count", P->channel.admins_count); 10 | lua_add_num_field ("kicked_count", P->channel.kicked_count); 11 | @@ -288,11 +289,24 @@ 12 | lua_add_string_field ("caption", M->caption); 13 | break; 14 | case tgl_message_media_document: 15 | + lua_newtable (luaState); 16 | + lua_add_string_field ("type", "document"); 17 | + lua_add_string_field ("caption", M->document->caption); 18 | + break; 19 | case tgl_message_media_audio: 20 | + lua_newtable (luaState); 21 | + lua_add_string_field ("type", "audio"); 22 | + lua_add_string_field ("caption", M->caption); 23 | + break; 24 | case tgl_message_media_video: 25 | + lua_newtable (luaState); 26 | + lua_add_string_field ("type", "video"); 27 | + lua_add_string_field ("caption", M->caption); 28 | + break; 29 | case tgl_message_media_document_encr: 30 | lua_newtable (luaState); 31 | - lua_add_string_field ("type", "document"); 32 | + lua_add_string_field ("type", "encr_document"); 33 | + lua_add_string_field ("caption", M->document->caption); 34 | break; 35 | case tgl_message_media_unsupported: 36 | lua_newtable (luaState); 37 | @@ -680,6 +694,12 @@ 38 | lq_send_video, 39 | lq_send_text, 40 | lq_reply, 41 | + lq_reply_audio, 42 | + lq_reply_document, 43 | + lq_reply_file, 44 | + lq_reply_location, 45 | + lq_reply_photo, 46 | + lq_reply_video, 47 | lq_fwd, 48 | lq_fwd_media, 49 | lq_load_photo, 50 | @@ -712,13 +732,37 @@ 51 | lq_status_online, 52 | lq_status_offline, 53 | lq_send_location, 54 | + lq_post_location, 55 | lq_extf, 56 | lq_import_chat_link, 57 | lq_export_chat_link, 58 | lq_channel_invite_user, 59 | lq_channel_kick_user, 60 | lq_channel_get_admins, 61 | - lq_channel_get_users 62 | + lq_channel_get_users, 63 | + lq_block_user, 64 | + lq_unblock_user, 65 | + lq_channel_set_username, 66 | + lq_rename_channel, 67 | + lq_export_channel_link, 68 | + lq_channel_set_about, 69 | + lq_channel_set_admin, 70 | + lq_channel_set_mod, 71 | + lq_channel_del_admin, 72 | + lq_channel_set_photo, 73 | + lq_chat_upgrade, 74 | + lq_contact_search, 75 | + lq_create_channel, 76 | + lq_get_message, 77 | + lq_channel_join, 78 | + lq_channel_leave, 79 | + lq_channel_list, 80 | + lq_post_audio, 81 | + lq_post_document, 82 | + lq_post_file, 83 | + lq_post_photo, 84 | + lq_post_text, 85 | + lq_post_video, 86 | }; 87 | 88 | struct lua_query_extra { 89 | @@ -1138,6 +1182,38 @@ 90 | free (cb); 91 | } 92 | 93 | +void lua_contact_search_cb (struct tgl_state *TLSR, void *cb_extra, int success, tgl_peer_t *C) { 94 | + assert (TLSR == TLS); 95 | + struct lua_query_extra *cb = cb_extra; 96 | + lua_settop (luaState, 0); 97 | + //lua_checkstack (luaState, 20); 98 | + my_lua_checkstack (luaState, 20); 99 | + 100 | + lua_rawgeti (luaState, LUA_REGISTRYINDEX, cb->func); 101 | + lua_rawgeti (luaState, LUA_REGISTRYINDEX, cb->param); 102 | + 103 | + lua_pushnumber (luaState, success); 104 | + 105 | + if (success) { 106 | + push_peer (C->id, (void *)C); 107 | + } else { 108 | + lua_pushboolean (luaState, 0); 109 | + } 110 | + 111 | + assert (lua_gettop (luaState) == 4); 112 | + 113 | + int r = ps_lua_pcall (luaState, 3, 0, 0); 114 | + 115 | + luaL_unref (luaState, LUA_REGISTRYINDEX, cb->func); 116 | + luaL_unref (luaState, LUA_REGISTRYINDEX, cb->param); 117 | + 118 | + if (r) { 119 | + logprintf ("lua: %s\n", lua_tostring (luaState, -1)); 120 | + } 121 | + 122 | + free (cb); 123 | +} 124 | + 125 | #define LUA_STR_ARG(n) lua_ptr[n].str, strlen (lua_ptr[n].str) 126 | 127 | void lua_do_all (void) { 128 | @@ -1199,6 +1275,30 @@ 129 | case lq_send_text: 130 | tgl_do_send_text (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].str, 0, lua_msg_cb, lua_ptr[p].ptr); 131 | p += 3; 132 | + break; 133 | + case lq_post_audio: 134 | + tgl_do_send_document (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].str, NULL, 0, TGL_SEND_MSG_FLAG_DOCUMENT_AUDIO | TGLMF_POST_AS_CHANNEL, lua_msg_cb, lua_ptr[p].ptr); 135 | + p += 3; 136 | + break; 137 | + case lq_post_document: 138 | + tgl_do_send_document (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].str, NULL, 0, TGLMF_POST_AS_CHANNEL, lua_msg_cb, lua_ptr[p].ptr); 139 | + p += 3; 140 | + break; 141 | + case lq_post_file: 142 | + tgl_do_send_document (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].str, NULL, 0, TGL_SEND_MSG_FLAG_DOCUMENT_AUTO | TGLMF_POST_AS_CHANNEL, lua_msg_cb, lua_ptr[p].ptr); 143 | + p += 3; 144 | + break; 145 | + case lq_post_photo: 146 | + tgl_do_send_document (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].str, NULL, 0, TGL_SEND_MSG_FLAG_DOCUMENT_PHOTO | TGLMF_POST_AS_CHANNEL, lua_msg_cb, lua_ptr[p].ptr); 147 | + p += 3; 148 | + break; 149 | + case lq_post_text: 150 | + tgl_do_send_text (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].str, 256, lua_msg_cb, lua_ptr[p].ptr); 151 | + p += 3; 152 | + break; 153 | + case lq_post_video: 154 | + tgl_do_send_document (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].str, NULL, 0, TGL_SEND_MSG_FLAG_DOCUMENT_VIDEO | TGLMF_POST_AS_CHANNEL, lua_msg_cb, lua_ptr[p].ptr); 155 | + p += 3; 156 | break; 157 | case lq_chat_set_photo: 158 | tgl_do_set_chat_photo (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].str, lua_empty_cb, lua_ptr[p].ptr); 159 | @@ -1238,6 +1338,30 @@ 160 | tgl_do_reply_message (TLS, &lua_ptr[p + 1].msg_id, LUA_STR_ARG (p + 2), 0, lua_msg_cb, lua_ptr[p].ptr); 161 | p += 3; 162 | break; 163 | + case lq_reply_audio: 164 | + tgl_do_reply_document (TLS, &lua_ptr[p + 1].msg_id, lua_ptr[p + 2].str, NULL, 0, TGL_SEND_MSG_FLAG_DOCUMENT_AUDIO, lua_msg_cb, lua_ptr[p].ptr); 165 | + p += 3; 166 | + break; 167 | + case lq_reply_document: 168 | + tgl_do_reply_document (TLS, &lua_ptr[p + 1].msg_id, lua_ptr[p + 2].str, NULL, 0, 0, lua_msg_cb, lua_ptr[p].ptr); 169 | + p += 3; 170 | + break; 171 | + case lq_reply_file: 172 | + tgl_do_reply_document (TLS, &lua_ptr[p + 1].msg_id, lua_ptr[p + 2].str, NULL, 0, TGL_SEND_MSG_FLAG_DOCUMENT_AUTO, lua_msg_cb, lua_ptr[p].ptr); 173 | + p += 3; 174 | + break; 175 | + case lq_reply_location: // TODO - I DON'T UNDERSTAND WHY IT'S NOT WORKING 176 | + tgl_do_reply_location (TLS, &lua_ptr[p + 1].msg_id, lua_ptr[p + 2].dnum, lua_ptr[p + 3].dnum, 0, lua_msg_cb, lua_ptr[p].ptr); 177 | + p += 4; 178 | + break; 179 | + case lq_reply_photo: 180 | + tgl_do_reply_document (TLS, &lua_ptr[p + 1].msg_id, lua_ptr[p + 2].str, NULL, 0, TGL_SEND_MSG_FLAG_DOCUMENT_PHOTO, lua_msg_cb, lua_ptr[p].ptr); 181 | + p += 3; 182 | + break; 183 | + case lq_reply_video: 184 | + tgl_do_reply_document (TLS, &lua_ptr[p + 1].msg_id, lua_ptr[p + 2].str, NULL, 0, TGL_SEND_MSG_FLAG_DOCUMENT_VIDEO, lua_msg_cb, lua_ptr[p].ptr); 185 | + p += 3; 186 | + break; 187 | case lq_fwd: 188 | tmp_msg_id = &lua_ptr[p + 2].msg_id; 189 | tgl_do_forward_messages (TLS, lua_ptr[p + 1].peer_id, 1, (void *)&tmp_msg_id, 0, lua_one_msg_cb, lua_ptr[p].ptr); 190 | @@ -1347,6 +1471,10 @@ 191 | tgl_do_send_location (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].dnum, lua_ptr[p + 3].dnum, 0, lua_msg_cb, lua_ptr[p].ptr); 192 | p += 4; 193 | break; 194 | + case lq_post_location: 195 | + tgl_do_send_location (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].dnum, lua_ptr[p + 3].dnum, 256, lua_msg_cb, lua_ptr[p].ptr); 196 | + p += 4; 197 | + break; 198 | case lq_channel_invite_user: 199 | tgl_do_channel_invite_user (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].peer_id, lua_empty_cb, lua_ptr[p].ptr); 200 | p += 3; 201 | @@ -1363,6 +1491,73 @@ 202 | tgl_do_channel_get_members (TLS, lua_ptr[p + 1].peer_id, 100, 0, 0, lua_contact_list_cb, lua_ptr[p].ptr); 203 | p += 2; 204 | break; 205 | + case lq_block_user: 206 | + tgl_do_block_user (TLS, lua_ptr[p + 1].peer_id, lua_empty_cb, lua_ptr[p].ptr); 207 | + p += 2; 208 | + break; 209 | + case lq_unblock_user: 210 | + tgl_do_unblock_user (TLS, lua_ptr[p + 1].peer_id, lua_empty_cb, lua_ptr[p].ptr); 211 | + p += 2; 212 | + break; 213 | + case lq_channel_set_username: 214 | + tgl_do_channel_set_username (TLS, lua_ptr[p + 1].peer_id, LUA_STR_ARG (p + 2), lua_empty_cb, lua_ptr[p].ptr); 215 | + p += 3; 216 | + break; 217 | + case lq_rename_channel: 218 | + tgl_do_rename_channel (TLS, lua_ptr[p + 1].peer_id, LUA_STR_ARG (p + 2), lua_empty_cb, lua_ptr[p].ptr); 219 | + p += 3; 220 | + break; 221 | + case lq_contact_search: 222 | + tgl_do_contact_search (TLS, LUA_STR_ARG (p + 1), lua_contact_search_cb, lua_ptr[p].ptr); 223 | + p += 2; 224 | + break; 225 | + case lq_create_channel: 226 | + tgl_do_create_channel (TLS, 1, &lua_ptr[p + 1].peer_id, LUA_STR_ARG (p + 2), LUA_STR_ARG (p + 3), 1,lua_empty_cb, lua_ptr[p].ptr); 227 | + p += 4; 228 | + break; 229 | + case lq_get_message: 230 | + tgl_do_get_message (TLS, &lua_ptr[p + 1].msg_id, lua_msg_cb, lua_ptr[p].ptr); 231 | + p += 2; 232 | + break; 233 | + case lq_channel_set_about: 234 | + tgl_do_channel_set_about (TLS, lua_ptr[p + 1].peer_id, LUA_STR_ARG (p + 2), lua_empty_cb, lua_ptr[p].ptr); 235 | + p += 3; 236 | + break; 237 | + case lq_channel_set_admin: 238 | + tgl_do_channel_set_admin (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].peer_id, 2, lua_empty_cb, lua_ptr[p].ptr); 239 | + p += 3; 240 | + break; 241 | + case lq_channel_set_mod: 242 | + tgl_do_channel_set_admin (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].peer_id, 1, lua_empty_cb, lua_ptr[p].ptr); 243 | + p += 3; 244 | + break; 245 | + case lq_channel_del_admin: 246 | + tgl_do_channel_set_admin (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].peer_id, 0, lua_empty_cb, lua_ptr[p].ptr); 247 | + p += 3; 248 | + break; 249 | + case lq_channel_set_photo: 250 | + tgl_do_set_channel_photo (TLS, lua_ptr[p + 1].peer_id, lua_ptr[p + 2].str, lua_empty_cb, lua_ptr[p].ptr); 251 | + p += 3; 252 | + break; 253 | + case lq_chat_upgrade: 254 | + tgl_do_upgrade_group (TLS, lua_ptr[p + 1].peer_id, lua_empty_cb, lua_ptr[p].ptr); 255 | + p += 2; 256 | + break; 257 | + case lq_export_channel_link: 258 | + tgl_do_export_channel_link (TLS, lua_ptr[p + 1].peer_id, lua_str_cb, lua_ptr[p].ptr); 259 | + p += 2; 260 | + break; 261 | + case lq_channel_list: 262 | + tgl_do_get_channels_dialog_list (TLS, 100, 0, lua_dialog_list_cb, lua_ptr[p ++].ptr); 263 | + break; 264 | + case lq_channel_join: 265 | + tgl_do_join_channel (TLS, lua_ptr[p + 1].peer_id, lua_empty_cb, lua_ptr[p].ptr); 266 | + p += 2; 267 | + break; 268 | + case lq_channel_leave: 269 | + tgl_do_leave_channel (TLS, lua_ptr[p + 1].peer_id, lua_empty_cb, lua_ptr[p].ptr); 270 | + p += 2; 271 | + break; 272 | /* 273 | lq_delete_msg, 274 | lq_restore_msg, 275 | @@ -1436,6 +1631,12 @@ 276 | {"load_document", lq_load_document, { lfp_msg, lfp_none }}, 277 | {"load_document_thumb", lq_load_document_thumb, { lfp_msg, lfp_none }}, 278 | {"reply_msg", lq_reply, { lfp_msg, lfp_string, lfp_none }}, 279 | + {"reply_file", lq_reply_file, {lfp_msg, lfp_string, lfp_none}}, 280 | + {"reply_audio", lq_send_audio, {lfp_msg, lfp_string, lfp_none}}, 281 | + {"reply_location", lq_reply_location, { lfp_msg, lfp_double, lfp_double, lfp_none }}, 282 | + {"reply_document", lq_reply_document, {lfp_msg, lfp_string, lfp_none}}, 283 | + {"reply_photo", lq_reply_photo, {lfp_msg, lfp_string, lfp_none}}, 284 | + {"reply_video", lq_reply_video, {lfp_msg, lfp_string, lfp_none}}, 285 | {"fwd_msg", lq_fwd, { lfp_peer, lfp_msg, lfp_none }}, 286 | {"fwd_media", lq_fwd_media, { lfp_peer, lfp_msg, lfp_none }}, 287 | {"chat_info", lq_chat_info, { lfp_chat, lfp_none }}, 288 | @@ -1460,7 +1661,8 @@ 289 | {"send_contact", lq_send_contact, { lfp_peer, lfp_string, lfp_string, lfp_string, lfp_none }}, 290 | {"status_online", lq_status_online, { lfp_none }}, 291 | {"status_offline", lq_status_offline, { lfp_none }}, 292 | - {"send_location", lq_send_location, { lfp_peer, lfp_double, lfp_double, lfp_none }}, 293 | + {"send_location", lq_send_location, { lfp_peer, lfp_double, lfp_double, lfp_none }}, 294 | + {"post_location", lq_post_location, { lfp_peer, lfp_double, lfp_double, lfp_none }}, 295 | {"ext_function", lq_extf, { lfp_string, lfp_none }}, 296 | {"import_chat_link", lq_import_chat_link, { lfp_string, lfp_none }}, 297 | {"export_chat_link", lq_export_chat_link, { lfp_chat, lfp_none }}, 298 | @@ -1468,6 +1670,31 @@ 299 | {"channel_kick_user", lq_channel_kick_user, { lfp_channel, lfp_user, lfp_none }}, 300 | {"channel_get_admins", lq_channel_get_admins, { lfp_channel, lfp_none }}, 301 | {"channel_get_users", lq_channel_get_users, { lfp_channel, lfp_none }}, 302 | + {"block_user", lq_block_user, { lfp_user, lfp_none }}, 303 | + {"unblock_user", lq_unblock_user, { lfp_user, lfp_none }}, 304 | + {"import_channel_link", lq_import_chat_link, { lfp_string, lfp_none }}, 305 | + {"channel_set_username", lq_channel_set_username, { lfp_channel, lfp_string, lfp_none }}, 306 | + {"rename_channel", lq_rename_channel, { lfp_channel, lfp_string, lfp_none }}, 307 | + {"resolve_username", lq_contact_search, { lfp_string, lfp_none }}, 308 | + {"create_channel", lq_create_channel, { lfp_peer, lfp_string, lfp_string, lfp_none }}, 309 | + {"get_message", lq_get_message, { lfp_msg, lfp_none }}, 310 | + {"export_channel_link", lq_export_channel_link, { lfp_channel, lfp_none }}, 311 | + {"channel_set_admin", lq_channel_set_admin, { lfp_channel, lfp_user,lfp_none }}, 312 | + {"channel_set_mod", lq_channel_set_mod, { lfp_channel, lfp_peer, lfp_none }}, 313 | + {"channel_del_admin", lq_channel_del_admin, { lfp_channel, lfp_user,lfp_none }}, 314 | + {"channel_del_mod", lq_channel_del_admin, { lfp_channel, lfp_user,lfp_none }}, 315 | + {"channel_set_about", lq_channel_set_about, { lfp_channel, lfp_string, lfp_none }}, 316 | + {"channel_set_photo", lq_channel_set_photo, { lfp_channel, lfp_string, lfp_none }}, 317 | + {"chat_upgrade", lq_chat_upgrade, { lfp_peer, lfp_none }}, 318 | + {"channel_leave", lq_channel_leave, { lfp_channel, lfp_none }}, 319 | + {"channel_join", lq_channel_join, { lfp_channel, lfp_none }}, 320 | + {"get_channel_list", lq_channel_list, { lfp_none }}, 321 | + {"post_audio", lq_post_audio, { lfp_peer, lfp_string, lfp_none }}, 322 | + {"post_document", lq_post_document, { lfp_peer, lfp_string, lfp_none }}, 323 | + {"post_file", lq_post_file, { lfp_peer, lfp_string, lfp_none }}, 324 | + {"post_photo", lq_post_photo, { lfp_peer, lfp_string, lfp_none }}, 325 | + {"post_text", lq_post_text, { lfp_peer, lfp_string, lfp_none }}, 326 | + {"post_video", lq_post_video, { lfp_peer, lfp_string, lfp_none }}, 327 | { 0, 0, { lfp_none}} 328 | }; 329 | 330 | -------------------------------------------------------------------------------- /plugins/9gag.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function get_9GAG() 4 | local url = 'http://api-9gag.herokuapp.com/' 5 | local b,c = http.request(url) 6 | 7 | if c ~= 200 then 8 | return nil 9 | end 10 | 11 | local gag = json:decode(b) 12 | -- random max json table size 13 | local i = math.random(#gag) 14 | local link_image = gag[i].src 15 | local title = gag[i].title 16 | 17 | if link_image:sub(0,2) == '//' then 18 | link_image = msg.text:sub(3,-1) 19 | end 20 | 21 | return link_image, title 22 | end 23 | 24 | local function run(msg, matches) 25 | local url, title = get_9GAG() 26 | local gag_file = '/tmp/gag.jpg' 27 | local g_file = ltn12.sink.file(io.open(gag_file, 'w')) 28 | http.request { 29 | url = url, 30 | sink = g_file, 31 | } 32 | 33 | if msg.from.api then 34 | bot_sendPhoto(get_receiver_api(msg), gag_file, nil, true, msg.id) 35 | else 36 | reply_photo(msg.id, gag_file, ok_cb, true) 37 | end 38 | end 39 | 40 | return { 41 | description = '9GAG for Telegram', 42 | usage = { 43 | '!9gag', 44 | 'Send random image from 9gag', 45 | }, 46 | patterns = { 47 | '^!9gag$' 48 | }, 49 | run = run 50 | } 51 | 52 | end 53 | -------------------------------------------------------------------------------- /plugins/apod.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function run(msg, matches) 4 | check_api_key(msg, 'nasa_api', 'http://api.nasa.gov') 5 | 6 | if matches[1] == 'setapikey nasa_api' and is_sudo(msg.from.peer_id) then 7 | _config.api_key.nasa_api = matches[2] 8 | save_config() 9 | send_message(msg, 'NASA api key has been saved.', 'html') 10 | return 11 | end 12 | 13 | local apodate = '' .. os.date("%F") .. '\n\n' 14 | local url = 'https://api.nasa.gov/planetary/apod?api_key=' .. _config.api_key.nasa_api 15 | 16 | if matches[2] then 17 | if matches[2]:match('%d%d%d%d%-%d%d%-%d%d$') then 18 | url = url .. '&date=' .. URL.escape(matches[2]) 19 | apodate = '' .. matches[2] .. '\n\n' 20 | else 21 | send_message(msg, 'Request must be in following format:\n!' .. matches[1] .. ' YYYY-MM-DD', 'html') 22 | return 23 | end 24 | end 25 | 26 | local str, res = https.request(url) 27 | 28 | if res ~= 200 then 29 | send_message(msg, 'Connection error', 'html') 30 | return 31 | end 32 | 33 | local jstr = json:decode(str) 34 | 35 | if jstr.error then 36 | send_message(msg, 'No results found', 'html') 37 | return 38 | end 39 | 40 | local img_url = jstr.hdurl or jstr.url 41 | local apod = apodate .. '' .. jstr.title .. '' 42 | 43 | if matches[1] == 'apodtext' then 44 | apod = apod .. '\n\n' .. jstr.explanation 45 | end 46 | 47 | if jstr.copyright then 48 | apod = apod .. '\n\nCopyright: ' .. jstr.copyright .. '' 49 | end 50 | 51 | bot_sendMessage(get_receiver_api(msg), apod, false, msg.id, 'html') 52 | end 53 | 54 | return { 55 | description = "Returns the NASA's Astronomy Picture of the Day.", 56 | usage = { 57 | sudo = { 58 | '!setapikey nasa_api [api_key]', 59 | 'Set NASA APOD API key.' 60 | }, 61 | user = { 62 | '!apod', 63 | 'Returns the Astronomy Picture of the Day (APOD).', 64 | '', 65 | '!apod YYYY-MM-DD', 66 | 'Returns the YYYY-MM-DD APOD.', 67 | 'Example: !apod 2016-08-17', 68 | '', 69 | '!apodtext', 70 | 'Returns the explanation of the APOD.', 71 | '', 72 | '!apodtext YYYY-MM-DD', 73 | 'Returns the explanation of YYYY-MM-DD APOD.', 74 | 'Example: !apodtext 2016-08-17', 75 | '', 76 | }, 77 | }, 78 | patterns = { 79 | '^!(apod)$', 80 | '^!(apodtext)$', 81 | '^!(apod) (%g+)$', 82 | '^!(apodtext) (%g+)$', 83 | '^!(setapikey nasa_api) (.*)$' 84 | }, 85 | run = run 86 | } 87 | 88 | end 89 | -------------------------------------------------------------------------------- /plugins/bing.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Get Bing search API from from https://datamarket.azure.com/dataset/bing/search 3 | Set the key by: !setapi bing [bing_api_key] or manually inserted into config.lua 4 | --]] 5 | 6 | do 7 | 8 | local mime = require('mime') 9 | 10 | local function bingo(msg, burl, terms) 11 | local burl = burl:format(URL.escape("'" .. terms .. "'")) 12 | local limit = 5 13 | 14 | if not is_chat_msg(msg) then 15 | limit = 8 16 | end 17 | 18 | local resbody = {} 19 | local bang, bing, bung = https.request{ 20 | url = burl .. '&$top=' .. limit, 21 | headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. _config.api_key.bing) }, 22 | sink = ltn12.sink.table(resbody), 23 | } 24 | local dat = json:decode(table.concat(resbody)) 25 | local jresult = dat.d.results 26 | 27 | if next(jresult) == nil then 28 | send_message(msg, 'No Bing results for: ' .. terms, 'html') 29 | else 30 | local reslist = {} 31 | for i = 1, #jresult do 32 | local result = jresult[i] 33 | reslist[i] = '' .. i .. '. ' 34 | .. '' .. result.Title .. '' 35 | end 36 | 37 | local reslist = table.concat(reslist, '\n') 38 | local header = 'Bing results for ' .. terms .. ' :\n' 39 | 40 | bot_sendMessage(get_receiver_api(msg), header .. reslist, true, msg.id, 'html') 41 | end 42 | end 43 | 44 | local function bing_by_reply(extra, success, result) 45 | local terms = result.text 46 | 47 | bingo(extra.msg, extra.burl, terms) 48 | end 49 | 50 | local function run(msg, matches) 51 | check_api_key(msg, 'bing', 'https://datamarket.azure.com/dataset/bing/search') 52 | 53 | if matches[1] == 'setapikey bing' and is_sudo(msg.from.peer_id) then 54 | _config.api_key.bing = matches[2] 55 | save_config() 56 | send_message(msg, 'Bing api key has been saved.', 'html') 57 | return 58 | end 59 | 60 | local burl = "https://api.datamarket.azure.com/Data.ashx/Bing/Search/Web?Query=%s&$format=json" 61 | 62 | if matches[1]:match('nsfw') then 63 | burl = burl .. '&Adult=%%27Off%%27' 64 | else 65 | burl = burl .. '&Adult=%%27Strict%%27' 66 | end 67 | 68 | if msg.reply_id then 69 | get_message(msg.reply_id, bing_by_reply, {msg=msg, burl=burl}) 70 | else 71 | if msg.reply_to_message then 72 | bingo(msg, burl, msg.reply_to_message.text) 73 | else 74 | bingo(msg, burl, matches[2]) 75 | end 76 | end 77 | end 78 | 79 | -------------------------------------------------------------------------------- 80 | 81 | return { 82 | description = 'Returns 5 (if group) or 8 (if private message) Bing results.\n' 83 | .. 'Safe search is enabled by default, use !bnsfw or !bingnsfw to disable it.', 84 | usage = { 85 | sudo = { 86 | '!setapikey bing [api_key]', 87 | 'Set Bing API key.' 88 | }, 89 | user = { 90 | '!bing [terms]', 91 | '!b [terms]', 92 | 'Safe searches Bing', 93 | '', 94 | '!bing', 95 | '!b', 96 | 'Safe searches Bing by reply. The search terms is the replied message text.', 97 | '', 98 | '!bingnsfw [terms]', 99 | '!bnsfw [terms]', 100 | 'Searches Bing (include NSFW)', 101 | '', 102 | '!bingnsfw', 103 | '!bnsfw', 104 | 'Searches Bing (include NSFW). The search terms is the replied message text.' 105 | }, 106 | }, 107 | patterns = { 108 | '^!(b)$', '^!(bing)$', 109 | '^!b(nsfw)$', '^!bing(nsfw)$', 110 | '^!(b) (.*)$', '^!(bing) (.*)$', 111 | '^!b(nsfw) (.*)$', '^!bing(nsfw) (.*)$', 112 | '^!(setapikey bing) (.*)$', 113 | }, 114 | run = run 115 | } 116 | 117 | end 118 | -------------------------------------------------------------------------------- /plugins/btc.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | -- See https://bitcoinaverage.com/api 4 | local function run(msg, matches) 5 | local base_url = 'https://api.bitcoinaverage.com/ticker/global/' 6 | local currency = 'USD' 7 | 8 | if matches[2] then 9 | currency = matches[2]:upper() 10 | end 11 | 12 | -- Do request on bitcoinaverage, the final / is critical! 13 | local res, code = https.request(base_url .. currency .. '/') 14 | 15 | if code ~= 200 then return nil end 16 | 17 | local data = json:decode(res) 18 | local ask = string.gsub(data.ask, '%.', ',') 19 | local bid = string.gsub(data.bid, '%.', ',') 20 | local index = 'BTC in ' .. currency .. ':\n' 21 | .. '• Buy: ' .. group_into_three(ask) .. '\n' 22 | .. '• Sell: ' .. group_into_three(bid) 23 | 24 | bot_sendMessage(get_receiver_api(msg), index, true, msg.id, 'html') 25 | end 26 | 27 | -------------------------------------------------------------------------------- 28 | 29 | return { 30 | description = 'Displays the current Bitcoin price.', 31 | usage = { 32 | '!btc', 33 | 'Displays Bitcoin price in USD', 34 | '', 35 | '!btc [currency]', 36 | 'Displays Bitcoin price in [currency]', 37 | '[currency] is in ISO 4217 format.', 38 | '', 39 | }, 40 | patterns = { 41 | '^!(btc)$', 42 | '^!(btc) (%a%a%a)$', 43 | }, 44 | run = run 45 | } 46 | 47 | end -------------------------------------------------------------------------------- /plugins/calculator.lua: -------------------------------------------------------------------------------- 1 | -- Function reference: http://mathjs.org/docs/reference/functions/categorical.html 2 | 3 | do 4 | 5 | local function mathjs(msg, exp) 6 | local result = http.request('http://api.mathjs.org/v1/?expr=' .. URL.escape(exp)) 7 | 8 | if not result then 9 | result = 'Unexpected error\nIs api.mathjs.org up?' 10 | end 11 | 12 | send_message(msg, '' .. result .. '', 'html') 13 | end 14 | 15 | local function run(msg, matches) 16 | mathjs(msg, matches[1]) 17 | end 18 | 19 | return { 20 | description = "Calculate math expressions with mathjs.org API.", 21 | usage = { 22 | '!calc [expression]', 23 | '!calculator [expression]', 24 | 'Evaluates the expression and sends the result.', 25 | }, 26 | patterns = { 27 | "^!calc (.*)$", 28 | "^!calculator (.*)" 29 | }, 30 | run = run 31 | } 32 | 33 | end 34 | -------------------------------------------------------------------------------- /plugins/cats.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | function run(msg, matches) 4 | local filetype = '&type=jpg' 5 | 6 | if matches[1] == 'gif' then 7 | filetype = '&type=gif' 8 | end 9 | 10 | local url = 'http://thecatapi.com/api/images/get?format=html' .. filetype .. '&api_key=' .. _config.api_key.thecatapi 11 | local str, res = http.request(url) 12 | 13 | if res ~= 200 then 14 | send_message(msg, 'Connection error', 'html') 15 | return 16 | end 17 | 18 | local str = str:match('') 19 | 20 | bot_sendMessage(get_receiver_api(msg), 'Cat!', false, msg.id, 'html') 21 | end 22 | 23 | return { 24 | description = 'Returns a cat!', 25 | usage = { 26 | '!cat', 27 | '!cats', 28 | 'Returns a picture of cat!', 29 | '', 30 | '!cat gif', 31 | '!cats gif', 32 | 'Returns an animated picture of cat!', 33 | }, 34 | patterns = { 35 | '^!cats?$', 36 | '^!cats? (gif)$', 37 | }, 38 | run = run, 39 | is_need_api_key = {'thecatapi', 'http://thecatapi.com/docs.html'} 40 | } 41 | 42 | end -------------------------------------------------------------------------------- /plugins/currency.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function get_word(s, i) 4 | s = s or '' 5 | i = i or 1 6 | local t = {} 7 | 8 | for w in s:gmatch('%g+') do 9 | table.insert(t, w) 10 | end 11 | 12 | return t[i] or false 13 | end 14 | 15 | local function run(msg, matches) 16 | local input = msg.text:upper() 17 | 18 | if not input:match('%a%a%a TO %a%a%a') then 19 | send_message(msg, 'Example: !cash 5 USD to IDR', 'html') 20 | return 21 | end 22 | 23 | local from = input:match('(%a%a%a) TO') 24 | local to = input:match('TO (%a%a%a)') 25 | local amount = get_word(input, 2) 26 | local amount = tonumber(amount) or 1 27 | local result = 1 28 | local url = 'https://www.google.com/finance/converter' 29 | 30 | if from ~= to then 31 | local url = url .. '?from=' .. from .. '&to=' .. to .. '&a=' .. amount 32 | local str, res = https.request(url) 33 | 34 | if res ~= 200 then 35 | send_message(msg, 'Connection error', 'html') 36 | return 37 | end 38 | 39 | str = str:match('(.*) %u+') 40 | 41 | if not str then 42 | send_message(msg, 'Connection error', 'html') 43 | return 44 | end 45 | 46 | result = string.format('%.2f', str):gsub('%.', ',') 47 | end 48 | 49 | local headerapi = '' .. amount .. ' ' .. from .. ' = ' .. group_into_three(result) .. ' ' .. to .. '\n\n' 50 | local source = 'Source: Google Finance\n' .. os.date('%F %T %Z') .. '' 51 | 52 | send_message(msg, headerapi .. source, 'html') 53 | end 54 | 55 | -------------------------------------------------------------------------------- 56 | 57 | return { 58 | description = 'Returns (Google Finance) exchange rates for various currencies.', 59 | usage = { 60 | '!cash [amount] [from] to [to]', 61 | 'Example:', 62 | ' * !cash 5 USD to EUR', 63 | ' * !currency 1 usd to idr', 64 | }, 65 | patterns = { 66 | '^!cash (.*)$', 67 | '^!currency (.*)$', 68 | }, 69 | run = run 70 | } 71 | 72 | end 73 | -------------------------------------------------------------------------------- /plugins/dilbert.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function run(msg, matches) 4 | local input = os.date('%F') 5 | 6 | if matches[2] then 7 | if matches[2]:match('^%d%d%d%d%-%d%d%-%d%d$') then 8 | input = matches[2] 9 | else 10 | send_message(msg, 'Request must be in following format:\n' 11 | .. '!' .. matches[1] .. ' YYYY-MM-DD', 'html') 12 | return 13 | end 14 | end 15 | 16 | local url = 'http://dilbert.com/strip/' .. URL.escape(input) 17 | local str, res = http.request(url) 18 | 19 | if res ~= 200 then 20 | send_message(msg, 'Connection error', 'html') 21 | return 22 | end 23 | 24 | local strip_filename = '/tmp/' .. input .. '.gif' 25 | local strip_file = io.open(strip_filename) 26 | 27 | if strip_file then 28 | strip_file:close() 29 | strip_file = strip_filename 30 | else 31 | local strip_url = str:match('') 32 | strip_file = download_to_file(strip_url, input .. '.gif') 33 | end 34 | 35 | local strip_title = str:match('') 36 | local strip_date = str:match('') 37 | 38 | if msg.from.api then 39 | bot_sendPhoto(get_receiver_api(msg), strip_file, strip_date .. '. ' .. strip_title, true, msg.id) 40 | else 41 | local cmd = 'send_photo %s %s %s' 42 | local command = cmd:format(get_receiver(msg), strip_file, strip_date .. '. ' .. strip_title) 43 | os.execute(tgclie:format(command)) 44 | end 45 | end 46 | 47 | return { 48 | description = 'Returns the latest Dilbert strip or that of the provided date.\n' 49 | .. 'Dates before the first strip will return the first strip.\n' 50 | .. 'Dates after the last trip will return the last strip.\n' 51 | .. 'Source: dilbert.com', 52 | usage = { 53 | '!dilbert', 54 | 'Returns todays Dilbert comic', 55 | '', 56 | '!dilbert YYYY-MM-DD', 57 | 'Returns Dilbert comic published on YYYY-MM-DD', 58 | 'Example: !dilbert 2016-08-17', 59 | '', 60 | }, 61 | patterns = { 62 | '^!(dilbert)$', 63 | '^!(dilbert) (%g+)$' 64 | }, 65 | run = run 66 | } 67 | 68 | end -------------------------------------------------------------------------------- /plugins/dogify.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function run(msg, matches) 4 | local base = 'http://dogr.io/' 5 | local dogetext = URL.escape(matches[1]) 6 | local dogetext = string.gsub(dogetext, '%%2f', '/') 7 | local url = base .. dogetext .. '.png?split=false&.png' 8 | local urlm = 'https?://[%%%w-_%.%?%.:/%+=&]+' 9 | 10 | if string.match(url, urlm) == url then 11 | bot_sendMessage(get_receiver_api(msg), '[doge](' .. url .. ')', false, msg.id, 'markdown') 12 | else 13 | print("Can't build a good URL with parameter " .. matches[1]) 14 | end 15 | end 16 | 17 | return { 18 | description = 'Create a doge image with you words.', 19 | usage = { 20 | '!dogify (your/words/with/slashes)', 21 | '!doge (your/words/with/slashes)', 22 | 'Create a doge with the image and words.', 23 | 'Example: !doge wow/merbot/soo/cool', 24 | }, 25 | patterns = { 26 | '^!dogify (.+)$', 27 | '^!doge (.+)$', 28 | }, 29 | run = run 30 | } 31 | 32 | end -------------------------------------------------------------------------------- /plugins/forecast.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function round(val, decimal) 4 | local exp = decimal and 10^decimal or 1 5 | return math.ceil(val * exp - 0.5) / exp 6 | end 7 | 8 | local function wemoji(weather_data) 9 | if weather_data.icon == 'clear-day' then 10 | return '☀️' 11 | elseif weather_data.icon == 'clear-night' then 12 | return '🌙' 13 | elseif weather_data.icon == 'rain' then 14 | return '☔️' 15 | elseif weather_data.icon == 'snow' then 16 | return '❄️' 17 | elseif weather_data.icon == 'sleet' then 18 | return '🌨' 19 | elseif weather_data.icon == 'wind' then 20 | return '💨' 21 | elseif weather_data.icon == 'fog' then 22 | return '🌫' 23 | elseif weather_data.icon == 'cloudy' then 24 | return '☁️☁️' 25 | elseif weather_data.icon == 'partly-cloudy-day' then 26 | return '🌤' 27 | elseif weather_data.icon == 'partly-cloudy-night' then 28 | return '🌙☁️' 29 | else 30 | return '' 31 | end 32 | end 33 | 34 | -- Use timezone api to get the time in the lat 35 | local function getforecast(msg, area) 36 | local coords, code = get_coords(msg, area) 37 | local lat = coords.lat 38 | local long = coords.lon 39 | local address = coords.formatted_address 40 | local url = 'https://api.darksky.net/forecast/' 41 | local units = '?units=si' 42 | local url = url .. _config.api_key.forecast .. '/' .. URL.escape(lat) .. ',' .. URL.escape(long) .. units 43 | local res, code = https.request(url) 44 | 45 | if code ~= 200 then 46 | return nil 47 | end 48 | 49 | local jcast = json:decode(res) 50 | local todate = os.date('%A, %F', jcast.currently.time) 51 | 52 | local forecast = 'Weather for: ' .. address .. '\n' .. todate .. '\n\n' 53 | .. 'Right now ' .. wemoji(jcast.currently) .. '\n' 54 | .. jcast.currently.summary .. ' - Feels like ' .. round(jcast.currently.apparentTemperature) .. '°C\n\n' 55 | .. 'Next 24 hours ' .. wemoji(jcast.hourly) .. '\n' .. jcast.hourly.summary .. '\n\n' 56 | .. 'Next 7 days ' .. wemoji(jcast.daily) .. '\n' .. jcast.daily.summary .. '\n\n' 57 | .. 'Powered by Dark Sky' 58 | 59 | bot_sendMessage(get_receiver_api(msg), forecast, true, msg.id, 'html') 60 | end 61 | 62 | local function run(msg, matches) 63 | if matches[1] == 'setapikey forecast' and is_sudo(msg.from.peer_id) then 64 | _config.api_key.forecast = matches[2] 65 | save_config() 66 | send_message(msg, 'Dark Sky API key has been saved.', 'html') 67 | return 68 | else 69 | return getforecast(msg, matches[1]) 70 | end 71 | end 72 | 73 | return { 74 | description = 'Returns forecast from forecast.io.', 75 | usage = { 76 | '!cast [area]', 77 | '!forecast [area]', 78 | '!weather [area]', 79 | 'Forecast for that [area].', 80 | 'Example: !weather dago parung panjang', 81 | }, 82 | patterns = { 83 | '^!cast (.*)$', 84 | '^!forecast (.*)$', 85 | '^!weather (.*)$', 86 | '^!(setapikey forecast) (.*)$' 87 | }, 88 | run = run, 89 | is_need_api_key = {'forecast', 'https://darksky.net/dev/'} 90 | } 91 | 92 | end 93 | 94 | 95 | -------------------------------------------------------------------------------- /plugins/gmaps.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function run(msg, matches) 4 | local coords = get_coords(msg, matches[1]) 5 | 6 | if coords then 7 | if msg.from.api then 8 | bot_sendLocation(get_receiver_api(msg), coords.lat, coords.lon, true, msg.id) 9 | else 10 | send_location(get_receiver(msg), coords.lat, coords.lon, ok_cb, true) 11 | end 12 | end 13 | end 14 | 15 | return { 16 | description = 'Returns a location from Google Maps.', 17 | usage = { 18 | '!loc [query]', 19 | '!location [query]', 20 | '!gmaps [query]', 21 | 'Returns Google Maps of [query].', 22 | 'Example: !loc raja ampat', 23 | }, 24 | patterns = { 25 | '^!gmaps (.*)$', 26 | '^!location (.*)$', 27 | '^!loc (.*)$', 28 | }, 29 | run = run 30 | } 31 | 32 | end 33 | -------------------------------------------------------------------------------- /plugins/gsmarena.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Get Bing search API from from https://datamarket.azure.com/dataset/bing/search 3 | Set the key by: !setapi bing [bing_api_key] or manually inserted into config.lua 4 | --]] 5 | 6 | do 7 | 8 | local mime = require('mime') 9 | local function get_galink(msg, query) 10 | local burl = "https://api.datamarket.azure.com/Data.ashx/Bing/Search/Web?Query=%s&$format=json&$top=1" 11 | local burl = burl:format(URL.escape("'site:gsmarena.com intitle:" .. query .. "'")) 12 | local resbody = {} 13 | local bang, bing, bung = https.request{ 14 | url = burl, 15 | headers = { ["Authorization"] = "Basic " .. mime.b64(":" .. _config.api_key.bing) }, 16 | sink = ltn12.sink.table(resbody), 17 | } 18 | local dat = json:decode(table.concat(resbody)) 19 | local jresult = dat.d.results 20 | 21 | if next(jresult) ~= nil then 22 | return jresult[1].Url 23 | end 24 | end 25 | 26 | local function run(msg, matches) 27 | local phone = get_galink(msg, matches[2]) 28 | local slug = phone:gsub('^.+/', '') 29 | local slug = slug:gsub('.php', '') 30 | local ibacor = 'http://ibacor.com/api/gsm-arena?view=product&slug=' 31 | local res, code = http.request(ibacor .. slug) 32 | local gsm = json:decode(res) 33 | local phdata = {} 34 | 35 | if gsm == nil or gsm.status == 'error' or next(gsm.data) == nil then 36 | send_message(msg, 'No phones found!\n' 37 | .. 'Request must be in the following format:\n' 38 | .. '!gsm brand type', 'html') 39 | return 40 | end 41 | if not gsm.data.platform then 42 | gsm.data.platform = {} 43 | end 44 | if gsm.data.launch.status == 'Discontinued' then 45 | launch = gsm.data.launch.status .. '. Was announced in ' .. gsm.data.launch.announced 46 | else 47 | launch = gsm.data.launch.status 48 | end 49 | if gsm.data.platform.os then 50 | phdata[1] = 'OS: ' .. gsm.data.platform.os 51 | end 52 | if gsm.data.platform.chipset then 53 | phdata[2] = 'Chipset: ' .. gsm.data.platform.chipset 54 | end 55 | if gsm.data.platform.cpu then 56 | phdata[3] = 'CPU: ' .. gsm.data.platform.cpu 57 | end 58 | if gsm.data.platform.gpu then 59 | phdata[4] = 'GPU: ' .. gsm.data.platform.gpu 60 | end 61 | if gsm.data.camera.primary then 62 | local phcam = 'Camera: ' .. gsm.data.camera.primary:gsub(',.*$', '') .. ', ' .. (gsm.data.camera.video or '') 63 | phdata[5] = phcam:gsub(', check quality', '') 64 | end 65 | if gsm.data.memory.internal then 66 | phdata[6] = 'RAM: ' .. gsm.data.memory.internal 67 | end 68 | 69 | local gadata = table.concat(phdata, '\n') 70 | local title = '' .. gsm.title .. '\n\n' 71 | local dimensions = gsm.data.body.dimensions:gsub('%(.-%)', '') 72 | local display = gsm.data.display.size:gsub(' .*$', '"') .. ', ' 73 | .. gsm.data.display.resolution:gsub('%(.-%)', '') 74 | local output = title .. 'Status: ' .. launch .. '\n' 75 | .. 'Dimensions: ' .. dimensions .. '\n' 76 | .. 'Weight: ' .. gsm.data.body.weight:gsub('%(.-%)', '') .. '\n' 77 | .. 'SIM: ' .. gsm.data.body.sim .. '\n' 78 | .. 'Display: ' .. display .. '\n' 79 | .. gadata 80 | .. '\nMC: ' .. gsm.data.memory.card_slot .. '\n' 81 | .. 'Battery: ' .. gsm.data.battery._empty_:gsub('battery', '') .. '\n' 82 | .. 'More on gsmarena.com ...' 83 | 84 | bot_sendMessage(get_receiver_api(msg), output:gsub('
', ''), false, msg.id, 'html') 85 | end 86 | 87 | -------------------------------------------------------------------------------- 88 | 89 | return { 90 | description = 'Returns mobile phone specification.', 91 | usage = { 92 | '!phone [phone]', 93 | '!gsm [phone]', 94 | 'Returns phone specification.', 95 | 'Example: !gsm xiaomi mi4c', 96 | }, 97 | patterns = { 98 | '^!(phone) (.*)$', 99 | '^!(gsmarena) (.*)$', 100 | '^!(gsm) (.*)$' 101 | }, 102 | run = run 103 | } 104 | 105 | end 106 | -------------------------------------------------------------------------------- /plugins/hackernews.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function run(msg, matches) 4 | local jstr, res = https.request('https://hacker-news.firebaseio.com/v0/topstories.json') 5 | 6 | if res ~= 200 then 7 | bot_sendMessage(get_receiver_api(msg), 'Connection error.', true, msg.id, 'html') 8 | return 9 | end 10 | 11 | local jdat = json:decode(jstr) 12 | local res_count = 8 13 | local header = 'Hacker News\n\n' 14 | local hackernew = {} 15 | 16 | for i = 1, res_count do 17 | local res_url = 'https://hacker-news.firebaseio.com/v0/item/' .. jdat[i] .. '.json' 18 | local jstr, res = https.request(res_url) 19 | 20 | if res ~= 200 then 21 | send_message(msg, 'Connection error', 'html') 22 | return 23 | end 24 | 25 | local res_jdat = json:decode(jstr) 26 | local title = res_jdat.title:gsub('%[.+%]', ''):gsub('%(.+%)', ''):gsub('&', '&') 27 | 28 | if title:len() > 48 then 29 | title = title:sub(1, 45) .. '...' 30 | end 31 | 32 | local url = res_jdat.url 33 | 34 | if not url then 35 | send_message(msg, 'Connection error', 'html') 36 | return 37 | end 38 | 39 | local title = unescape_html(title) 40 | 41 | hackernew[i] = '' .. i .. '. ' .. title .. '\n' 42 | end 43 | 44 | local hackernews = table.concat(hackernew) 45 | 46 | bot_sendMessage(get_receiver_api(msg), header .. hackernews, true, msg.id, 'html') 47 | end 48 | 49 | return { 50 | description = 'Returns top stories from Hacker News.', 51 | usage = { 52 | '!hackernews', 53 | '!hn', 54 | 'Returns top stories from Hacker News.', 55 | }, 56 | patterns = { 57 | '^!(hackernews)$', 58 | '^!(hn)$', 59 | }, 60 | run = run 61 | } 62 | 63 | end 64 | -------------------------------------------------------------------------------- /plugins/help.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | -- Returns true if is not empty 4 | local function has_usage_data(dict) 5 | if (dict.usage == nil or dict.usage == '') then 6 | return false 7 | end 8 | return true 9 | end 10 | 11 | -- Get commands for that plugin 12 | local function plugin_help(name, number, requester) 13 | local plugin = '' 14 | 15 | if number then 16 | local i = 0 17 | 18 | for name in pairsByKeys(plugins) do 19 | if plugins[name].hidden then 20 | name = nil 21 | else 22 | i = i + 1 23 | if i == tonumber(number) then 24 | plugin = plugins[name] 25 | end 26 | end 27 | end 28 | else 29 | plugin = plugins[name] 30 | if not plugin then return nil end 31 | end 32 | 33 | local text = '' 34 | 35 | if (type(plugin.usage) == 'table') then 36 | for ku,usage in pairs(plugin.usage) do 37 | if ku == 'user' then -- usage for user 38 | if (type(plugin.usage.user) == 'table') then 39 | for k,v in pairs(plugin.usage.user) do 40 | text = text .. v .. '\n' 41 | end 42 | elseif has_usage_data(plugin) then -- Is not empty 43 | text = text .. plugin.usage.user .. '\n' 44 | end 45 | elseif ku == 'moderator' then -- usage for moderator 46 | if requester == 'moderator' or requester == 'owner' or requester == 'admin' or requester == 'sudo' then 47 | if (type(plugin.usage.moderator) == 'table') then 48 | for k,v in pairs(plugin.usage.moderator) do 49 | text = text .. v .. '\n' 50 | end 51 | elseif has_usage_data(plugin) then -- Is not empty 52 | text = text .. plugin.usage.moderator .. '\n' 53 | end 54 | end 55 | elseif ku == 'owner' then -- usage for owner 56 | if requester == 'owner' or requester == 'admin' or requester == 'sudo' then 57 | if (type(plugin.usage.owner) == 'table') then 58 | for k,v in pairs(plugin.usage.owner) do 59 | text = text .. v .. '\n' 60 | end 61 | elseif has_usage_data(plugin) then -- Is not empty 62 | text = text .. plugin.usage.owner .. '\n' 63 | end 64 | end 65 | elseif ku == 'admin' then -- usage for admin 66 | if requester == 'admin' or requester == 'sudo' then 67 | if (type(plugin.usage.admin) == 'table') then 68 | for k,v in pairs(plugin.usage.admin) do 69 | text = text .. v .. '\n' 70 | end 71 | elseif has_usage_data(plugin) then -- Is not empty 72 | text = text .. plugin.usage.admin .. '\n' 73 | end 74 | end 75 | elseif ku == 'sudo' then -- usage for sudo 76 | if requester == 'sudo' then 77 | if (type(plugin.usage.sudo) == 'table') then 78 | for k,v in pairs(plugin.usage.sudo) do 79 | text = text .. v .. '\n' 80 | end 81 | elseif has_usage_data(plugin) then -- Is not empty 82 | text = text .. plugin.usage.sudo .. '\n' 83 | end 84 | end 85 | else 86 | text = text .. usage .. '\n' 87 | end 88 | end 89 | elseif has_usage_data(plugin) then -- Is not empty 90 | text = text .. plugin.usage 91 | end 92 | return text 93 | end 94 | 95 | 96 | -- !help command 97 | local function telegram_help(msg) 98 | local i = 0 99 | local text = 'Plugins\n\n' 100 | -- Plugins names 101 | for name in pairsByKeys(plugins) do 102 | if plugins[name].hidden then 103 | name = nil 104 | else 105 | i = i + 1 106 | text = text .. '' .. i .. '. ' .. name .. '\n' 107 | end 108 | end 109 | text = text .. '\n' .. 'There are ' .. i .. ' plugins help available.\n' 110 | .. '- !help [plugin name] for more info.\n' 111 | .. '- !help [plugin number] for more info.\n' 112 | .. '- !help all to show all info.' 113 | 114 | bot_sendMessage(get_receiver_api(msg), text, true, msg.id, 'html') 115 | end 116 | 117 | -- -- !help all command 118 | -- local function help_all(requester) 119 | -- local ret = '' 120 | -- for name in pairsByKeys(plugins) do 121 | -- if plugins[name].hidden then 122 | -- name = nil 123 | -- else 124 | -- ret = ret .. plugin_help(name, nil, requester) 125 | -- end 126 | -- end 127 | -- return ret 128 | -- end 129 | 130 | -------------------------------------------------------------------------------- 131 | 132 | local function run(msg, matches) 133 | local uid = msg.from.peer_id 134 | local gid = msg.to.peer_id 135 | 136 | if is_sudo(uid) then 137 | requester = 'sudo' 138 | elseif is_admin(uid) then 139 | requester = 'admin' 140 | elseif is_owner(msg, gid, uid) then 141 | requester = 'owner' 142 | elseif is_mod(msg, gid, uid) then 143 | requester = 'moderator' 144 | else 145 | requester = 'user' 146 | end 147 | 148 | if msg.text == '!help' then 149 | return telegram_help(msg) 150 | elseif matches[1] == 'all' then 151 | send_message(msg, 'Please read @thefinemanual', 'html') 152 | --return help_all(requester) 153 | else 154 | local text = '' 155 | 156 | if tonumber(matches[1]) then 157 | text = plugin_help(nil, matches[1], requester) 158 | else 159 | text = plugin_help(matches[1], nil, requester) 160 | end 161 | if not text then 162 | send_message(msg, 'No help entry for "' .. matches[1] .. '".\n' 163 | .. 'Please visit @thefinemanual for the complete list.', 'html') 164 | return 165 | end 166 | if text == 'text' then 167 | send_message(msg, 'The plugins is not for your privilege.', 'html') 168 | return 169 | end 170 | 171 | bot_sendMessage(get_receiver_api(msg), text, true, msg.id, 'html') 172 | end 173 | end 174 | 175 | -------------------------------------------------------------------------------- 176 | 177 | return { 178 | description = 'Help plugin. Get info from other plugins.', 179 | usage = { 180 | '!help', 181 | 'Show list of plugins.', 182 | '', 183 | '!help all', 184 | 'Show all commands for every plugin.', 185 | '', 186 | '!help [plugin_name]', 187 | 'Commands for that plugin.', 188 | '', 189 | '!help [number]', 190 | 'Commands for that plugin. Type !help to get the plugin number.' 191 | }, 192 | patterns = { 193 | '^!help$', 194 | '^!help (%g+)$', 195 | }, 196 | run = run 197 | } 198 | 199 | end 200 | -------------------------------------------------------------------------------- /plugins/id.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Print user identification/informations by replying their post or by providing 4 | their username or print_name. 5 | 6 | !id is the least reliable because it will scan trough all of members 7 | and print all member with in their print_name. 8 | 9 | chat_info can be displayed on group, send it into PM, or save as file then send 10 | it into group or PM. 11 | 12 | --]] 13 | 14 | do 15 | 16 | local function send_group_members(extra, list) 17 | local msg = extra.msg 18 | local cmd = extra.cmd 19 | 20 | if cmd == 'pm' then 21 | bot_sendMessage(msg.from.peer_id, list, true, nil, 'html') 22 | elseif msg.text == '!id chat' then 23 | bot_sendMessage(get_receiver_api(msg), list, true, msg.id, 'html') 24 | end 25 | end 26 | 27 | local function resolve_user(extra, success, result) 28 | if success == 1 then 29 | if result.username then 30 | user_name = ' @' .. result.username 31 | else 32 | user_name = '' 33 | end 34 | 35 | local msg = extra.msg 36 | local text = 'Name :' .. (result.first_name or '') .. ' ' .. (result.last_name or '') .. '\n' 37 | .. 'First name:' .. (result.first_name or '') .. '\n' 38 | .. 'Last name :' .. (result.last_name or '') .. '\n' 39 | .. 'User name :' .. user_name .. '\n' 40 | .. 'ID :' .. result.peer_id .. '' 41 | 42 | bot_sendMessage(get_receiver_api(msg), text, true, msg.id, 'html') 43 | else 44 | bot_sendMessage(get_receiver_api(msg), 'Failed to resolve ' 45 | .. extra.usr .. ' IDs.\nCheck if ' .. extra.usr .. ' is correct.', true, msg.id, 'html') 46 | end 47 | end 48 | 49 | local function scan_name(extra, success, result) 50 | local msg = extra.msg 51 | local uname = extra.uname 52 | 53 | if msg.to.peer_type == 'channel' then 54 | member_list = result 55 | else 56 | member_list = result.members 57 | end 58 | 59 | local founds = {} 60 | 61 | for k,member in pairs(member_list) do 62 | local fields = {'first_name', 'last_name', 'print_name'} 63 | 64 | for k,field in pairs(fields) do 65 | if member[field] and type(member[field]) == 'string' then 66 | local gp_member = member[field]:lower() 67 | if gp_member:match(uname:lower()) then 68 | founds[tostring(member.id)] = member 69 | end 70 | end 71 | end 72 | end 73 | if next(founds) == nil then -- Empty table 74 | bot_sendMessage(get_receiver_api(msg), uname .. ' not found on this chat', true, msg.id, 'html') 75 | else 76 | local text = '' 77 | 78 | for k,user in pairs(founds) do 79 | if user.username then 80 | user_name = ' @' .. user.username 81 | else 82 | user_name = '' 83 | end 84 | text = 'Name :' .. (user.first_name or '') .. ' ' .. (user.last_name or '') .. '\n' 85 | .. 'First name:' .. (user.first_name or '') .. '\n' 86 | .. 'Last name :' .. (user.last_name or '') .. '\n' 87 | .. 'User name :' .. user_name .. '\n' 88 | .. 'ID :' .. user.peer_id .. '\n\n' 89 | end 90 | 91 | bot_sendMessage(get_receiver_api(msg), text, true, msg.id, 'html') 92 | end 93 | end 94 | 95 | local function action_by_reply(extra, success, result) 96 | if result.from.username then 97 | user_name = ' @' .. result.from.username 98 | else 99 | user_name = '' 100 | end 101 | 102 | local text = 'Name :' .. (result.from.first_name or '') .. ' ' .. (result.from.last_name or '') .. '\n' 103 | .. 'First name:' .. (result.from.first_name or '') .. '\n' 104 | .. 'Last name :' .. (result.from.last_name or '') .. '\n' 105 | .. 'User name :' .. user_name .. '\n' 106 | .. 'ID :' .. result.from.peer_id .. '' 107 | 108 | bot_sendMessage(get_receiver_api(extra), text, true, extra.id, 'html') 109 | end 110 | 111 | local function returnids(extra, success, result) 112 | local msg = extra.msg 113 | local cmd = extra.cmd 114 | 115 | if msg.to.peer_type == 'channel' then 116 | chat_id = msg.to.peer_id 117 | chat_title = msg.to.title 118 | member_list = result 119 | else 120 | chat_id = result.peer_id 121 | chat_title = result.title 122 | member_list = result.members 123 | end 124 | 125 | local list = '' .. chat_title .. ' - ' .. chat_id .. '\n\n' 126 | local text = chat_title .. ' - ' .. chat_id .. '\n\n' 127 | 128 | i = 0 129 | 130 | for k,v in pairsByKeys(member_list) do 131 | i = i+1 132 | 133 | if v.username then 134 | user_name = ' @' .. v.username 135 | else 136 | user_name = '' 137 | end 138 | 139 | list = list .. '' .. i .. '. ' .. v.peer_id .. ' -' .. user_name .. ' ' .. (v.first_name or '') .. (v.last_name or '') .. '\n' 140 | 141 | if #list > 2048 then 142 | send_group_members(extra, list) 143 | list = '' 144 | end 145 | text = text .. i .. '. ' .. v.peer_id .. ' -' .. user_name .. ' ' .. (v.first_name or '') .. (v.last_name or '') .. '\n' 146 | end 147 | 148 | if cmd == 'txt' or cmd == 'pmtxt' then 149 | local textfile = '/tmp/chat_info_' .. msg.to.peer_id .. '_' .. os.date("%y%m%d.%H%M%S") .. '.txt' 150 | local file = io.open(textfile, 'w') 151 | file:write(text) 152 | file:flush() 153 | file:close() 154 | 155 | if cmd == 'txt' then 156 | send_document(get_receiver(msg), textfile, rmtmp_cb, {file_path=textfile}) 157 | elseif cmd == 'pmtxt' then 158 | send_document('user#id' .. msg.from.peer_id, textfile, rmtmp_cb, {file_path=textfile}) 159 | end 160 | end 161 | 162 | send_group_members(extra, list) 163 | end 164 | 165 | -------------------------------------------------------------------------------- 166 | 167 | local function run(msg, matches) 168 | local gid = msg.to.peer_id 169 | local uid = msg.from.peer_id 170 | 171 | if is_mod(msg, gid, uid) then 172 | if msg.reply_id and msg.text == '!id' then 173 | get_message(msg.reply_id, action_by_reply, msg) 174 | elseif matches[1] == 'chat' then 175 | if msg.to.peer_type == 'channel' then 176 | channel_get_users('channel#id' .. gid, returnids, {msg=msg, cmd=matches[2]}) 177 | end 178 | if msg.to.peer_type == 'chat' then 179 | chat_info('chat#id' .. gid, returnids, {msg=msg, cmd=matches[2]}) 180 | end 181 | elseif matches[1] == '@' then 182 | resolve_username(matches[2], resolve_user, {msg=msg, usr=matches[2]}) 183 | elseif matches[1]:match('%d+$') then 184 | user_info('user#id' .. matches[1], resolve_user, {msg=msg, usr=matches[1]}) 185 | elseif matches[1] == 'name' then 186 | if msg.to.peer_type == 'channel' then 187 | channel_get_users('channel#id' .. gid, scan_name, {msg=msg, uname=matches[2]}) 188 | end 189 | if msg.to.peer_type == 'chat' then 190 | chat_info('chat#id' .. gid, scan_name, {msg=msg, uname=matches[2]}) 191 | end 192 | end 193 | end 194 | 195 | if not msg.reply_id and msg.text == '!id' then 196 | if msg.from.username then 197 | user_name = '@' .. msg.from.username 198 | else 199 | user_name = '' 200 | end 201 | 202 | local text = 'Name :' .. (msg.from.first_name or '') .. ' ' .. (msg.from.last_name or '') .. '\n' 203 | .. 'First name:' .. (msg.from.first_name or '') .. '\n' 204 | .. 'Last name :' .. (msg.from.last_name or '') .. '\n' 205 | .. 'User name :' .. user_name .. '\n' 206 | .. 'ID :' .. uid .. '' 207 | 208 | if not is_chat_msg(msg) then 209 | bot_sendMessage(get_receiver_api(msg), text, true, msg.id, 'html') 210 | else 211 | bot_sendMessage(get_receiver_api(msg), text .. '\n\nYou are in group ' .. msg.to.title .. ' [' .. tostring(gid):gsub('-', '') .. ']', true, msg.id, 'html') 212 | end 213 | end 214 | 215 | end 216 | 217 | -------------------------------------------------------------------------------- 218 | 219 | return { 220 | description = 'Know your id or the id of a chat members.', 221 | usage = { 222 | moderator = { 223 | '!id', 224 | 'Return ID of replied user if used by reply.', 225 | '', 226 | '!id chat', 227 | 'Return the IDs of the current chat members.', 228 | '', 229 | '!id chat txt', 230 | 'Return the IDs of the current chat members and send it as text file.', 231 | '', 232 | '!id chat pm', 233 | 'Return the IDs of the current chat members and send it to PM.', 234 | '', 235 | '!id chat pmtxt', 236 | 'Return the IDs of the current chat members, save it as text file and then send it to PM.', 237 | '', 238 | '!id [user_id]', 239 | 'Return the IDs of the user_id.', 240 | '', 241 | '!id @[user_name]', 242 | 'Return the member username ID from the current chat.', 243 | '', 244 | '!id [name]', 245 | 'Search for users with name on first_name, last_name, or print_name on current chat.' 246 | }, 247 | user = { 248 | '!id', 249 | 'Return your ID and the chat id if you are in one.' 250 | }, 251 | }, 252 | patterns = { 253 | '^!(id)$', 254 | '^!id (chat)$', 255 | '^!id (chat) (.+)$', 256 | '^!id (name) (.*)$', 257 | '^!id (@)(.+)$', 258 | '^!id (%d+)$', 259 | }, 260 | run = run 261 | } 262 | 263 | end 264 | -------------------------------------------------------------------------------- /plugins/imdb.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function run(msg, matches) 4 | local omdbapi = 'http://www.omdbapi.com/?plot=full&r=json' 5 | local movietitle = matches[1] 6 | 7 | if matches[1]:match(' %d%d%d%d$') then 8 | local movieyear = matches[1]:match('%d%d%d%d$') 9 | movietitle = matches[1]:match('^.+ ') 10 | omdbapi = omdbapi .. '&y=' .. movieyear 11 | end 12 | 13 | local success, code = http.request(omdbapi .. '&t=' .. URL.escape(movietitle)) 14 | 15 | if success then 16 | jomdb = json:decode(success) 17 | end 18 | 19 | if jomdb.Response == 'False' then 20 | send_message(msg, '' .. jomdb.Error .. '', 'html') 21 | return 22 | end 23 | 24 | if not jomdb then 25 | send_message(msg, '' .. json:decode(code) .. '', 'html') 26 | return 27 | end 28 | 29 | local omdb = '' .. jomdb.Title .. '\n\n' 30 | .. 'Year: ' .. jomdb.Year .. '\n' 31 | .. 'Rated: ' .. jomdb.Rated .. '\n' 32 | .. 'Runtime: ' .. jomdb.Runtime .. '\n' 33 | .. 'Genre: ' .. jomdb.Genre .. '\n' 34 | .. 'Director: ' .. jomdb.Director .. '\n' 35 | .. 'Writer: ' .. jomdb.Writer .. '\n' 36 | .. 'Actors: ' .. jomdb.Actors .. '\n' 37 | .. 'Country: ' .. jomdb.Country .. '\n' 38 | .. 'Awards: ' .. jomdb.Awards .. '\n' 39 | .. 'Plot: ' .. jomdb.Plot .. '\n\n' 40 | .. 'IMDB:\n' 41 | .. 'Metascore: ' .. jomdb.Metascore .. '\n' 42 | .. 'Rating: ' .. jomdb.imdbRating .. '\n' 43 | .. 'Votes: ' .. jomdb.imdbVotes .. '\n\n' 44 | 45 | bot_sendMessage(get_receiver_api(msg), omdb, false, msg.id, 'html') 46 | end 47 | 48 | return { 49 | description = 'The Open Movie Database plugin for Telegram.', 50 | usage = { 51 | '!imdb [movie]', 52 | '!omdb [movie]', 53 | 'Returns IMDb entry for [movie]', 54 | 'Example: !imdb the matrix', 55 | '', 56 | '!imdb [movie] [year]', 57 | '!omdb [movie] [year]', 58 | 'Returns IMDb entry for [movie] that was released in [year]', 59 | 'Example: !imdb the matrix 2003', 60 | }, 61 | patterns = { 62 | '^![io]mdb (.+)$', 63 | }, 64 | run = run 65 | } 66 | 67 | end 68 | -------------------------------------------------------------------------------- /plugins/isup.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local socket = require('socket') 4 | local cronned = load_from_file('data/isup.lua') 5 | 6 | local function save_cron(msg, url, delete) 7 | local origin = get_receiver(msg) 8 | 9 | if not cronned[origin] then 10 | cronned[origin] = {} 11 | end 12 | if not delete then 13 | table.insert(cronned[origin], url) 14 | else 15 | for k,v in pairs(cronned[origin]) do 16 | if v == url then 17 | table.remove(cronned[origin], k) 18 | end 19 | end 20 | end 21 | 22 | serialize_to_file(cronned, 'data/isup.lua') 23 | send_message(msg, 'Saved!', 'html') 24 | end 25 | 26 | local function is_up_socket(ip, port) 27 | print('Connect to', ip, port) 28 | local c = socket.try(socket.tcp()) 29 | c:settimeout(3) 30 | local conn = c:connect(ip, port) 31 | 32 | if not conn then 33 | return false 34 | else 35 | c:close() 36 | return true 37 | end 38 | end 39 | 40 | local function is_up_http(url) 41 | -- Parse URL from input, default to http 42 | local parsed_url = URL.parse(url, { scheme = 'http', authority = '' }) 43 | -- Fix URLs without subdomain not parsed properly 44 | if not parsed_url.host and parsed_url.path then 45 | parsed_url.host = parsed_url.path 46 | parsed_url.path = "" 47 | end 48 | -- Re-build URL 49 | local url = URL.build(parsed_url) 50 | local protocols = { 51 | ['https'] = https, 52 | ['http'] = http 53 | } 54 | local options = { 55 | url = url, 56 | redirect = false, 57 | method = 'GET' 58 | } 59 | local response = { protocols[parsed_url.scheme].request(options) } 60 | local code = tonumber(response[2]) 61 | 62 | if code == nil or code >= 400 then 63 | return false 64 | end 65 | return true 66 | end 67 | 68 | local function isup(url) 69 | local pattern = '^(%d%d?%d?%.%d%d?%d?%.%d%d?%d?%.%d%d?%d?):?(%d?%d?%d?%d?%d?)$' 70 | local ip,port = string.match(url, pattern) 71 | local result = nil 72 | 73 | -- !isup 8.8.8.8:53 74 | if ip then 75 | port = port or '80' 76 | result = is_up_socket(ip, port) 77 | else 78 | result = is_up_http(url) 79 | end 80 | 81 | return result 82 | end 83 | 84 | local function cron() 85 | for chan, urls in pairs(cronned) do 86 | for k,url in pairs(urls) do 87 | print('Checking', url) 88 | if not isup(url) then 89 | local text = url .. ' looks DOWN from here. 😱' 90 | send_msg(chan, text, ok_cb, false) 91 | end 92 | end 93 | end 94 | end 95 | 96 | local function run(msg, matches) 97 | if matches[1] == 'cron delete' then 98 | if not is_sudo(msg) then 99 | send_message(msg, 'This command requires privileged user', 'html') 100 | end 101 | return save_cron(msg, matches[2], true) 102 | elseif matches[1] == 'cron' then 103 | if not is_sudo(msg) then 104 | send_message(msg, 'This command requires privileged user', 'html') 105 | end 106 | return save_cron(msg, matches[2]) 107 | elseif isup(matches[1]) then 108 | send_message(msg, matches[1] .. ' looks UP from here. 😃', 'html') 109 | else 110 | send_message(msg, matches[1] .. ' looks DOWN from here. 😱', 'html') 111 | end 112 | end 113 | 114 | return { 115 | description = 'Check if a website or server is up.', 116 | usage = { 117 | '!isup [host]', 118 | 'Performs a HTTP request or Socket (ip:port) connection', 119 | '', 120 | '!isup cron [host]', 121 | 'Every 5mins check if host is up. (Requires privileged user)', 122 | '', 123 | '!isup cron delete [host]', 124 | 'Disable checking that host.' 125 | }, 126 | patterns = { 127 | '^!isup (cron delete) (.*)$', 128 | '^!isup (cron) (.*)$', 129 | '^!isup (.*)$', 130 | '^!ping (.*)$', 131 | '^!ping (cron delete) (.*)$', 132 | '^!ping (cron) (.*)$' 133 | }, 134 | run = run, 135 | cron = cron 136 | } 137 | 138 | end 139 | -------------------------------------------------------------------------------- /plugins/kaskus.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local kaskus_forums = { 4 | [8] = "Lounge Video", 5 | [9] = "Jokes & Cartoon", 6 | [10] = "Berita dan Politik", 7 | [11] = "Movies", 8 | [13] = "Website, Webmaster, Webdeveloper", 9 | [14] = "CCPB - Shareware & Freeware", 10 | [15] = "Disturbing Picture", 11 | [16] = "Heart to Heart", 12 | [18] = "Can You Solve This Game?", 13 | [19] = "Computer Stuff", 14 | [21] = "The Lounge", 15 | [22] = "Buat Latihan Posting", 16 | [23] = "Supranatural", 17 | [24] = "Lifestyle", 18 | [26] = "Anime & Manga Haven", 19 | [28] = "Otomotif", 20 | [29] = "Cooking & Resto Guide", 21 | [30] = "Bisnis", 22 | [31] = "Kritik, Saran, Pertanyaan Seputar KASKUS", 23 | [32] = "Hewan Peliharaan", 24 | [33] = "Music", 25 | [34] = "All About Design", 26 | [35] = "Sports", 27 | [37] = "Ragnarok Online", 28 | [38] = "Web-based Games", 29 | [39] = "Girls & Boys's Corner", 30 | [44] = "Games", 31 | [49] = "B-Log Collections", 32 | [50] = "Poetry", 33 | [51] = "Stories from the Heart", 34 | [54] = "Photography", 35 | [58] = "KaskusRadio.com", 36 | [59] = "Gosip Nyok!", 37 | [60] = "Selera Nusantara (Indonesian Food)", 38 | [61] = "The Rest of the World (International Food)", 39 | [62] = "Oriental Exotic (Asian food)", 40 | [63] = "The KASKUS Bar", 41 | [65] = "Linux dan OS Selain Microsoft & Mac", 42 | [66] = "Buku", 43 | [67] = "Education", 44 | [70] = "Model Kit & R", 45 | [73] = "Indonesia", 46 | [74] = "Australia", 47 | [76] = "USA", 48 | [77] = "Singapore", 49 | [78] = "DKI Jakarta", 50 | [79] = "Melbourne", 51 | [80] = "Sydney", 52 | [81] = "Palembang", 53 | [82] = "Germany", 54 | [83] = "Canada", 55 | [84] = "Yogyakarta", 56 | [85] = "Netherlands", 57 | [87] = "Music Review", 58 | [88] = "Help, Tips & Tutorial", 59 | [89] = "Bandung", 60 | [90] = "Malaysia", 61 | [91] = "Kalimantan Timur - Kalimantan Utara", 62 | [92] = "Surabaya", 63 | [93] = "Medan", 64 | [94] = "Health", 65 | [96] = "East USA", 66 | [97] = "Bangka - Belitung", 67 | [98] = "Outdoor Adventure & Nature Clubs", 68 | [100] = "Online Games", 69 | [103] = "Welcome to KASKUS", 70 | [104] = "Soccer & Futsal Room", 71 | [105] = "Ask da Girls", 72 | [106] = "Perth", 73 | [107] = "Bogor", 74 | [108] = "Japan", 75 | [109] = "Asia", 76 | [111] = "Semarang", 77 | [112] = "Kendaraan Roda 2", 78 | [113] = "English", 79 | [114] = "Ask da Boys", 80 | [115] = "China", 81 | [116] = "AMHelpdesk", 82 | [117] = "Riau Raya", 83 | [119] = "Console & Handheld Games", 84 | [122] = "Western Comic", 85 | [123] = "Mamalia", 86 | [124] = "Burung", 87 | [125] = "Reptil", 88 | [126] = "Saltwater Fish", 89 | [127] = "Freshwater Fish", 90 | [129] = "United Kingdom", 91 | [133] = "Malang", 92 | [136] = "Feedback & Testimonial", 93 | [137] = "Music Corner", 94 | [140] = "Militer", 95 | [144] = "Martial Arts", 96 | [145] = "Lampung", 97 | [146] = "Kalimantan Selatan", 98 | [151] = "Gratisan", 99 | [156] = "Minangkabau", 100 | [157] = "Europe", 101 | [158] = "Regional Lainnya", 102 | [160] = "Solo", 103 | [161] = "Aceh", 104 | [162] = "Kalimantan Barat", 105 | [164] = "Banten", 106 | [166] = "Palu", 107 | [167] = "Bali", 108 | [170] = "Makassar", 109 | [171] = "TV", 110 | [173] = "Spiritual", 111 | [176] = "Programmer Forum", 112 | [177] = "KASKUS Plus Lounge", 113 | [179] = "Manado", 114 | [181] = "Banyumas", 115 | [183] = "Internet Service & Networking", 116 | [186] = "Lounge Pictures", 117 | [187] = "Airsoft Indonesia", 118 | [188] = "Surat Pembaca", 119 | [191] = "Debate Club", 120 | [192] = "Tanaman", 121 | [193] = "Wedding & Family", 122 | [194] = "Timur Tengah", 123 | [195] = "Elektronik", 124 | [196] = "Fashion & Mode", 125 | [197] = "Flora & Fauna", 126 | [198] = "Koleksi, Hobi & Mainan", 127 | [199] = "Properti", 128 | [200] = "CD & DVD", 129 | [201] = "Makanan & Minuman", 130 | [202] = "Jasa", 131 | [204] = "Jual Beli Feedback & Testimonials", 132 | [205] = "Otomotif", 133 | [210] = "Handphone & Gadget", 134 | [220] = "Alat-Alat Olahraga", 135 | [221] = "Alat-Alat Musik", 136 | [227] = "Buku", 137 | [229] = "Obat-obatan", 138 | [234] = "Arsitektur", 139 | [235] = "Travellers", 140 | [236] = "Muscle Building", 141 | [239] = "KASKUS Corner", 142 | [240] = "KASKUS Peduli", 143 | [243] = "Hardware Computer", 144 | [244] = "Latest Release", 145 | [246] = "Sejarah & Xenology", 146 | [247] = "Civitas Academica", 147 | [248] = "Restaurant Review", 148 | [249] = "Inspirasi", 149 | [250] = "Berita Luar Negeri", 150 | [251] = "Plamo", 151 | [252] = "Figures", 152 | [253] = "Gallery & Tutorial", 153 | [263] = "KASKUS Celeb", 154 | [270] = "Blacklist Jual Beli", 155 | [271] = "Official Testimonials Jual Beli", 156 | [272] = "Tips & Tutorial Jual Beli", 157 | [273] = "Product Review", 158 | [274] = "Fat-loss,Gain-Mass,Nutrisi Diet & Suplementasi Fitness", 159 | [275] = "Nightlife & Events", 160 | [276] = "Grappling", 161 | [277] = "Entrepreneur Corner", 162 | [278] = "Lowongan Kerja", 163 | [279] = "Forex, Option, Saham, & Derivatifnya", 164 | [280] = "HYIP", 165 | [281] = "Electronics", 166 | [282] = "Audio & Video", 167 | [283] = "Antik", 168 | [284] = "Karya Seni & Desain", 169 | [286] = "Industri & Supplier", 170 | [293] = "Kamera & Aksesoris", 171 | [295] = "Perawatan Tubuh & Wajah", 172 | [296] = "Furniture", 173 | [297] = "Video Games", 174 | [298] = "Perkakas", 175 | [299] = "Kerajinan Tangan", 176 | [300] = "Perlengkapan Kantor", 177 | [303] = "Perlengkapan Rumah Tangga", 178 | [304] = "Tur & Paket Perjalanan", 179 | [305] = "Perlengkapan Anak & Bayi", 180 | [306] = "Fashion", 181 | [316] = "Tiket", 182 | [317] = "Komputer", 183 | [331] = "Pictures", 184 | [332] = "Racing", 185 | [382] = "KASKUS Theater", 186 | [383] = "Macintosh", 187 | [384] = "Brisbane", 188 | [385] = "Kalimantan Tengah", 189 | [386] = "Music Event", 190 | [387] = "Ilmu Marketing", 191 | [388] = "Ilmu Marketing & Research", 192 | [389] = "Budaya", 193 | [390] = "TokuSenKa", 194 | [391] = "REQUEST @ CCPB", 195 | [392] = "Hobby & Community", 196 | [393] = "Pisau", 197 | [394] = "Sepeda", 198 | [395] = "Mancing", 199 | [397] = "ISP", 200 | [398] = "Kepulauan Riau", 201 | [402] = "Tegal", 202 | [403] = "Karesidenan Besuki", 203 | [405] = "Bekasi", 204 | [407] = "Depok", 205 | [411] = "Karesidenan Madiun", 206 | [412] = "Cirebon", 207 | [419] = "Taman Bacaan CCPB", 208 | [421] = "Visit USA", 209 | [423] = "West USA", 210 | [425] = "Central USA", 211 | [427] = "Papua", 212 | [429] = "Kids & Parenting", 213 | [431] = "Anime", 214 | [433] = "Manga, Manhua, & Manhwa", 215 | [435] = "KASKUS Promo", 216 | [437] = "Domestik", 217 | [439] = "Mancanegara", 218 | [440] = "Basketball", 219 | [442] = "Templates & Scripts Stuff", 220 | [443] = "Hosting Stuff", 221 | [452] = "Karesidenan Kediri", 222 | [457] = "Jawa Tengah & Yogyakarta", 223 | [458] = "Jawa Barat, Jakarta & Banten", 224 | [459] = "Jawa Timur & Bali", 225 | [460] = "Sumatera", 226 | [461] = "Kalimantan", 227 | [462] = "Sulawesi", 228 | [463] = "Indonesia Timur", 229 | [464] = "English Education & Literature", 230 | [465] = "Fun with English", 231 | [466] = "Penawaran Kerjasama & Investasi", 232 | [467] = "Forex", 233 | [468] = "Options", 234 | [469] = "Saham", 235 | [470] = "Forex Broker", 236 | [471] = "The Online Business", 237 | [472] = "MLM, Member Get Member, & Sejenisnya", 238 | [473] = "Gathering, Event Report & Bakti Sosial", 239 | [474] = "Event from Kaskuser", 240 | [475] = "Others", 241 | [476] = "Others", 242 | [477] = "Others", 243 | [478] = "Others", 244 | [479] = "Cinta Indonesiaku", 245 | [480] = "Arsitektur", 246 | [481] = "Kuliner", 247 | [482] = "Pakaian", 248 | [483] = "Lagu, Tarian & Alat Musik", 249 | [484] = "Kerajinan & Ukiran", 250 | [485] = "Kekayaan Alam, Flora & Fauna", 251 | [486] = "Kesusastraan, Bahasa & Dongeng", 252 | [487] = "Tata Cara Adat, Upacara & Ritual", 253 | [488] = "Permainan Rakyat", 254 | [489] = "Seni Peran", 255 | [490] = "Properti Sejarah Nasional", 256 | [491] = "Mobile Broadband", 257 | [528] = "PC Games", 258 | [537] = "Sport Games", 259 | [538] = "Racket", 260 | [539] = "Badminton", 261 | [540] = "Korea Selatan", 262 | [542] = "Pahlawan & Tokoh Nasional", 263 | [543] = "Batam", 264 | [544] = "Young on Top KASKUS Community (YOTKC)", 265 | [545] = "Pro Wrestling", 266 | [546] = "Dunia Kerja & Profesi", 267 | [548] = "Jambi", 268 | [552] = "Fanstuff", 269 | [555] = "Jember", 270 | [557] = "Hardware Review Lab", 271 | [558] = "Fitness & Healthy Body", 272 | [559] = "Quit Smoking, Alcohol & Drugs", 273 | [561] = "Kendari", 274 | [564] = "Karesidenan Pati", 275 | [565] = "FutSal", 276 | [566] = "Surat Terbuka Jual Beli", 277 | [567] = "Gresik", 278 | [568] = "Mac OSX Info & Discussion", 279 | [569] = "Mac Applications & Games", 280 | [570] = "Kendaraan Roda 4", 281 | [571] = "The Exclusive Business Club (ExBC)", 282 | [572] = "Penawaran Kerjasama, BO, Distribusi, Reseller, & Agen", 283 | [575] = "Militer dan Kepolisian", 284 | [576] = "Kepolisian", 285 | [578] = "Teknik", 286 | [579] = "Sipil", 287 | [580] = "Radio Komunikasi", 288 | [581] = "Lampu Senter", 289 | [583] = "Mojokerto", 290 | [584] = "Gorontalo", 291 | [585] = "Sukabumi", 292 | [586] = "Bengkulu", 293 | [587] = "Bromo", 294 | [588] = "Online Gaming", 295 | [591] = "Jam", 296 | [594] = "Melek Hukum", 297 | [595] = "UKM", 298 | [596] = "Catatan Perjalanan OANC", 299 | [597] = "Sains & Teknologi", 300 | [598] = "Klaten", 301 | [599] = "Tasikmalaya", 302 | [614] = "Serba Serbi", 303 | [615] = "Bisnis", 304 | [617] = "America", 305 | [619] = "Indie Filmmaker", 306 | [620] = "Perencanaan Keuangan", 307 | [621] = "Film Indonesia", 308 | [626] = "Karawang", 309 | [627] = "Karesidenan Kedu", 310 | [628] = "Sidoarjo", 311 | [629] = "KASKUS Playground", 312 | [629] = "Official Forum", 313 | [630] = "Green Lifestyle", 314 | [633] = "Pemilu & Pilkada", 315 | [651] = "Karesidenan Bojonegoro", 316 | [652] = "Madura", 317 | [653] = "Cilacap", 318 | [654] = "Garut", 319 | [655] = "Komik & Ilustrasi", 320 | [662] = "Jual Beli Latihan Posting", 321 | [663] = "Jual Beli Zone", 322 | [668] = "Suara KASKUSers", 323 | [669] = "Cerita Pejalan Domestik", 324 | [670] = "Cerita Pejalan Mancanegara", 325 | [671] = "Animasi", 326 | [673] = "Home Appliance", 327 | [674] = "Vaporizer", 328 | [675] = "Ngampus di KASKUS", 329 | [677] = "KASKUS Online Bazaar", 330 | [678] = "Gemstone", 331 | [679] = "Gemstone", 332 | [683] = "Koleksi Idola", 333 | [684] = "MotoGP", 334 | [685] = "F1", 335 | [689] = "Stand Up Comedy", 336 | [708] = "Indonesia Pusaka", 337 | [709] = "Private Servers", 338 | [710] = "Moba", 339 | [711] = "Dota 2", 340 | [712] = "Pilkada", 341 | [713] = "Deals", 342 | [715] = "Sista", 343 | [716] = "Fashionista", 344 | [717] = "Beauty", 345 | [718] = "Women’s Health", 346 | [723] = "Liga Mahasiswa ( Lima )", 347 | [724] = "Health Consultation", 348 | [725] = "Healthy Lifestyle", 349 | [726] = "Liga Mahasiswa ( Lima )", 350 | [727] = "Deals", 351 | [729] = "Official Merchandise", 352 | [730] = "Metrotvnews.com", 353 | [731] = "Berita Olahraga", 354 | [732] = "Berita Dunia Hiburan", 355 | [733] = "Citizen Journalism", 356 | [734] = "B-Log Personal", 357 | [735] = "B-Log Community", 358 | [736] = "Series & Film Asia", 359 | [737] = "Reksa Dana", 360 | } 361 | 362 | local url = 'http://m.kaskus.co.id/' 363 | 364 | local function escape_html(text) 365 | local text = text:gsub('<', '<') 366 | local text = text:gsub('>', '>') 367 | local text = text:gsub('"', '"') 368 | local text = text:gsub("'", '&apos') 369 | return text 370 | end 371 | 372 | local function get_hot_thread(msg) 373 | local hot, code = http.request(url .. 'forum/hotthread/index/?ref=forumlanding&med=hot_thread') 374 | local hotblock = hot:match('

Hot Threads

.-Kembali ke Atas') 375 | local hotthread = {} 376 | 377 | i = 0 378 | for hotgrab in hotblock:gmatch('' .. hotgrab:gsub('<.->', '') .. '' 381 | end 382 | 383 | local hotthread = table.concat(hotthread, '\n') 384 | local header = 'Kaskus Hot Thread\n\n' 385 | 386 | bot_sendMessage(get_receiver_api(msg), header .. hotthread, true, msg.id, 'html') 387 | end 388 | 389 | local function get_kaskus_thread(msg, forum_id) 390 | local res, kaskus = http.request(url .. 'forum/' .. forum_id) 391 | local kasthread = {} 392 | 393 | i = 0 394 | for grabbedlink in res:gmatch(' thread_.- ', '') 399 | kasthread[i] = '' .. i .. '. ' .. lema .. '" di kbbi.web.id' , 'html') 40 | return 41 | end 42 | 43 | if #matches == 2 then 44 | kbbi_desc = res:match('%-%- ' .. matches[2] .. '.-') 45 | title = '' .. matches[1] .. '\n\n' 46 | else 47 | local grabbedlema = res:match('{"x":1,"w":.-}') 48 | local jlema = json:decode(grabbedlema) 49 | title = '' .. jlema.w .. '\n\n' 50 | 51 | if jlema.d:match('
') then 52 | kbbi_desc = jlema.d:match('^.-
') 53 | else 54 | kbbi_desc = jlema.d 55 | end 56 | end 57 | print(cleanup_tag(title .. kbbi_desc)) 58 | bot_sendMessage(get_receiver_api(msg), cleanup_tag(title .. kbbi_desc), true, msg.id, 'html') 59 | end 60 | 61 | -------------------------------------------------------------------------------- 62 | 63 | return { 64 | description = 'Kamus Besar Bahasa Indonesia dari http://kbbi.web.id.', 65 | usage = { 66 | '!kbbi [lema]', 67 | 'Menampilkan arti dari [lema]' 68 | }, 69 | patterns = { 70 | '^!kbbi (%w+)$', 71 | '^!kbbi (%w+) (%w+)$' 72 | }, 73 | run = run 74 | } 75 | 76 | end 77 | -------------------------------------------------------------------------------- /plugins/logger.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Save message as json format to a log file. 4 | Remove phone number, in case if there is phone contact in bots account. 5 | Basically, it's just dumping the message into a file. So, don't be surprised 6 | if the log file size is big. 7 | 8 | --]] 9 | 10 | do 11 | 12 | local function pre_process(msg) 13 | local gid = tonumber(msg.to.peer_id) 14 | 15 | if _config.administration[gid] and is_chat_msg(msg) then 16 | local message = serpent.dump(msg, {comment=false}) 17 | local message = message:match('do local _=(.*);return _;end') 18 | local message = message:gsub('phone="%d+"', '') 19 | local logfile = io.open('data/' .. gid .. '/' .. gid .. '.log', 'a') 20 | logfile:write(message .. '\n') 21 | logfile:close() 22 | end 23 | 24 | return msg 25 | end 26 | 27 | local function run(msg, matches) 28 | local uid = msg.from.peer_id 29 | local loggid = msg.to.peer_id 30 | local receiver = get_receiver(msg) 31 | 32 | if matches[2] and matches[2]:match('^%d+$') then 33 | loggid = matches[2] 34 | end 35 | 36 | if is_owner(msg, loggid, uid) then 37 | if matches[1] == 'get' then 38 | send_document(receiver, './data/' .. loggid .. '/' .. loggid .. '.log', ok_cb, false) 39 | elseif matches[1] == 'pm' then 40 | send_document('user#id' .. uid, './data/' .. loggid .. '/' .. loggid .. '.log', ok_cb, false) 41 | end 42 | else 43 | reply_msg(msg.id, 'You have no privilege to get ' .. loggid .. ' log.', ok_cb, true) 44 | end 45 | end 46 | 47 | -------------------------------------------------------------------------------- 48 | 49 | return { 50 | description = 'Logging group messages.', 51 | usage = { 52 | sudo = { 53 | '!log get [chat_id]', 54 | 'Send chat_id chat log.', 55 | '', 56 | '!log pm [chat_id]', 57 | 'Send chat_id chat log to private message' 58 | }, 59 | owner = { 60 | '!log get', 61 | 'Send chat log to its chat group', 62 | '', 63 | '!log pm', 64 | 'Send chat log to private message' 65 | }, 66 | }, 67 | patterns = { 68 | '^!log (%a+)$', 69 | '^!log (%a+) (%d+)$' 70 | }, 71 | run = run, 72 | pre_process = pre_process, 73 | } 74 | 75 | end 76 | -------------------------------------------------------------------------------- /plugins/patterns.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function regex(msg, text) 4 | local patterns = msg.text:match('s/.*') 5 | local m1, m2 = patterns:match('^s/(.-)/(.-)/?$') 6 | 7 | if not m2 or m2:match('\n') then 8 | return 9 | end 10 | 11 | local substring = text:gsub(m1, m2) 12 | 13 | send_message(msg, 'Did you mean:\n"' .. substring:sub(1, 4000) .. '"', 'html') 14 | end 15 | 16 | local function patterns_by_reply(extra, success, result) 17 | local text = result.text or '' 18 | regex(extra, text) 19 | end 20 | 21 | local function run(msg, matches) 22 | if msg.reply_id then 23 | get_message(msg.reply_id, patterns_by_reply, msg) 24 | end 25 | if msg.from.api and msg.reply_to_message then 26 | regex(msg, msg.reply_to_message.text) 27 | end 28 | end 29 | 30 | return { 31 | description = 'Replace patterns in a message.', 32 | usage = { 33 | '/s/from/to/', 34 | '/s/from/to', 35 | 's/from/to', 36 | '!s/from/to/', 37 | '!s/from/to', 38 | 'Replace from with to' 39 | }, 40 | patterns = { 41 | '^/?s/.-/.-/?$', 42 | '^!s/.-/.-/?$' 43 | }, 44 | run = run 45 | } 46 | 47 | end 48 | -------------------------------------------------------------------------------- /plugins/plugins.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | -- Returns the key (index) in the config.enabled_plugins table 4 | local function plugin_enabled(name) 5 | for k,v in pairs(_config.enabled_plugins) do 6 | if name == v then 7 | return k 8 | end 9 | end 10 | -- If not found 11 | return false 12 | end 13 | 14 | -- Returns true if file exists in plugins folder 15 | local function plugin_exists(name) 16 | for k,v in pairs(plugins_names()) do 17 | if name .. '.lua' == v then 18 | return true 19 | end 20 | end 21 | return false 22 | end 23 | 24 | local function list_plugins(only_enabled, msg) 25 | local text = '' 26 | local psum = 0 27 | for k, v in pairs(plugins_names()) do 28 | -- ✅ enabled, ❌ disabled 29 | local status = '❌' 30 | psum = psum+1 31 | pact = 0 32 | -- Check if is enabled 33 | for k2, v2 in pairs(_config.enabled_plugins) do 34 | if v == v2 .. '.lua' then 35 | status = '✅' 36 | end 37 | pact = pact+1 38 | end 39 | if not only_enabled or status == '✅' then 40 | -- get the name 41 | v = v:match('(.*)%.lua') 42 | text = text .. status .. ' ' .. v .. '\n' 43 | end 44 | end 45 | local text = text .. '\n' .. psum .. ' plugins installed.\n' 46 | .. '✅ ' .. pact .. ' enabled.\n❌ ' .. psum-pact .. ' disabled.' 47 | reply_msg(msg.id, text, ok_cb, true) 48 | end 49 | 50 | local function reload_plugins(only_enabled, msg) 51 | plugins = {} 52 | load_plugins() 53 | return list_plugins(true, msg) 54 | end 55 | 56 | -------------------------------------------------------------------------------- 57 | 58 | local function run(msg, matches) 59 | local plugin = matches[2] 60 | local receiver = get_receiver(msg) 61 | 62 | if is_sudo(msg.from.peer_id) then 63 | 64 | -- Enable a plugin 65 | if not matches[3] then 66 | if matches[1] == 'enable' then 67 | print("enable: " .. plugin) 68 | print('checking if ' .. plugin .. ' exists') 69 | 70 | -- Check if plugin is enabled 71 | if plugin_enabled(plugin) then 72 | reply_msg(msg.id, 'Plugin ' .. plugin .. ' is enabled', ok_cb, true) 73 | end 74 | 75 | -- Checks if plugin exists 76 | if plugin_exists(plugin) then 77 | -- Add to the config table 78 | table.insert(_config.enabled_plugins, plugin) 79 | print(plugin .. ' added to _config table') 80 | save_config() 81 | -- Reload the plugins 82 | return reload_plugins(false, msg) 83 | else 84 | reply_msg(msg.id, 'Plugin ' .. plugin .. ' does not exists', ok_cb, true) 85 | end 86 | end 87 | 88 | -- Disable a plugin 89 | if matches[1] == 'disable' then 90 | print("disable: " .. plugin) 91 | 92 | -- Check if plugins exists 93 | if not plugin_exists(plugin) then 94 | reply_msg(msg.id, 'Plugin ' .. plugin .. ' does not exists', ok_cb, true) 95 | end 96 | 97 | local k = plugin_enabled(plugin) 98 | -- Check if plugin is enabled 99 | if not k then 100 | reply_msg(msg.id, 'Plugin ' .. plugin .. ' not enabled', ok_cb, true) 101 | end 102 | 103 | -- Disable and reload 104 | table.remove(_config.enabled_plugins, k) 105 | save_config( ) 106 | return reload_plugins(true, msg) 107 | end 108 | end 109 | 110 | -- Reload all the plugins! 111 | if matches[1] == 'reload' then 112 | return reload_plugins(false, msg) 113 | end 114 | end 115 | 116 | if is_mod(msg, msg.to.peer_id, msg.from.peer_id) then 117 | -- Show the available plugins 118 | if matches[1] == '!plugins' then 119 | return list_plugins(false, msg) 120 | end 121 | 122 | -- Re-enable a plugin for this chat 123 | if matches[3] == 'chat' then 124 | if matches[1] == 'enable' then 125 | print('enable ' .. plugin .. ' on this chat') 126 | if not _config.disabled_plugin_on_chat then 127 | reply_msg(msg.id, "There aren't any disabled plugins", ok_cb, true) 128 | end 129 | 130 | if not _config.disabled_plugin_on_chat[receiver] then 131 | reply_msg(msg.id, "There aren't any disabled plugins for this chat", ok_cb, true) 132 | end 133 | 134 | if not _config.disabled_plugin_on_chat[receiver][plugin] then 135 | reply_msg(msg.id, 'This plugin is not disabled', ok_cb, true) 136 | end 137 | 138 | _config.disabled_plugin_on_chat[receiver][plugin] = false 139 | save_config() 140 | reply_msg(msg.id, 'Plugin ' .. plugin .. ' is enabled again', ok_cb, true) 141 | end 142 | 143 | -- Disable a plugin on a chat 144 | if matches[1] == 'disable' then 145 | print('disable ' .. plugin .. ' on this chat') 146 | if not plugin_exists(plugin) then 147 | reply_msg(msg.id, "Plugin doesn't exists", ok_cb, true) 148 | end 149 | 150 | if not _config.disabled_plugin_on_chat then 151 | _config.disabled_plugin_on_chat = {} 152 | end 153 | 154 | if not _config.disabled_plugin_on_chat[receiver] then 155 | _config.disabled_plugin_on_chat[receiver] = {} 156 | end 157 | 158 | _config.disabled_plugin_on_chat[receiver][plugin] = true 159 | save_config() 160 | reply_msg(msg.id, 'Plugin ' .. plugin .. ' disabled on this chat', ok_cb, true) 161 | end 162 | end 163 | end 164 | end 165 | 166 | -------------------------------------------------------------------------------- 167 | 168 | return { 169 | description = 'Plugin to manage other plugins. Enable, disable or reload.', 170 | usage = { 171 | sudo = { 172 | '!plugins enable [plugin]', 173 | 'Enable plugin.', 174 | '', 175 | '!plugins disable [plugin]', 176 | 'Disable plugin.', 177 | '', 178 | '!plugins reload', 179 | 'Reloads all plugins.' 180 | }, 181 | moderator = { 182 | '!plugins', 183 | 'List all plugins.', 184 | '', 185 | '!plugins enable [plugin] chat', 186 | 'Re-enable plugin only this chat.', 187 | '', 188 | '!plugins disable [plugin] chat', 189 | 'Disable plugin only this chat.' 190 | }, 191 | }, 192 | patterns = { 193 | '^!plugins$', 194 | '^!plugins? (enable) ([%w_%.%-]+)$', 195 | '^!plugins? (disable) ([%w_%.%-]+)$', 196 | '^!plugins? (enable) ([%w_%.%-]+) (chat)$', 197 | '^!plugins? (disable) ([%w_%.%-]+) (chat)$', 198 | '^!plugins? (reload)$' 199 | }, 200 | run = run 201 | } 202 | 203 | end 204 | -------------------------------------------------------------------------------- /plugins/quran.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local surah_name = { 4 | [1] = "Al-Fatihah", 5 | [2] = "Al-Baqarah", 6 | [3] = "Ali-Imran", 7 | [4] = "An-Nisaa'", 8 | [5] = "Al-Maaidah", 9 | [6] = "Al-An'aam", 10 | [7] = "Al-A'raaf", 11 | [8] = "Al-Anfaal", 12 | [9] = "At-Taubah", 13 | [10] = "Yunus", 14 | [11] = "Huud", 15 | [12] = "Yusuf", 16 | [13] = "Ar-Ra'd", 17 | [14] = "Ibrahim", 18 | [15] = "Al-Hijr", 19 | [16] = "An-Nahl", 20 | [17] = "Al-Israa'", 21 | [18] = "Al-Kahfi", 22 | [19] = "Maryam", 23 | [20] = "Thaahaa", 24 | [21] = "Al-Anbiyaa'", 25 | [22] = "Al-Hajj", 26 | [23] = "Al-Mukminuun", 27 | [24] = "An-Nuur", 28 | [25] = "Al-Furqaan", 29 | [26] = "Ash-Shu'araa", 30 | [27] = "An-Naml", 31 | [28] = "Al-Qashash", 32 | [29] = "Al-Ankabuut", 33 | [30] = "Ar-Ruum", 34 | [31] = "Luqman", 35 | [32] = "As-Sajdah", 36 | [33] = "Al-Ahzaab", 37 | [34] = "Saba'", 38 | [35] = "Faathir", 39 | [36] = "Yasiin", 40 | [37] = "As-Shaaffaat", 41 | [38] = "Shaad", 42 | [39] = "Az-Zumar", 43 | [40] = "Al-Ghaafir", 44 | [41] = "Fushshilat", 45 | [42] = "Asy-Syuura", 46 | [43] = "Az-Zukhruf", 47 | [44] = "Ad-Dukhaan", 48 | [45] = "Al-Jaatsiyah", 49 | [46] = "Al-Ahqaaf", 50 | [47] = "Muhammad", 51 | [48] = "Al-Fath", 52 | [49] = "Al-Hujuraat", 53 | [50] = "Qaaf", 54 | [51] = "Adz-Dzaariyat", 55 | [52] = "Ath-Thur", 56 | [53] = "An-Najm", 57 | [54] = "Al-Qamar", 58 | [55] = "Ar-Rahmaan", 59 | [56] = "Al-Waaqi'ah", 60 | [57] = "Al-Hadiid", 61 | [58] = "Al-Mujaadilah", 62 | [59] = "Al-Hasyr", 63 | [60] = "Al-Mumtahanah", 64 | [61] = "Ash-Shaff", 65 | [62] = "Al-Jumu'ah", 66 | [63] = "Al-Munaafiquun", 67 | [64] = "At-Taghaabuun", 68 | [65] = "Ath-Thaalaq", 69 | [66] = "At-Tahrim", 70 | [67] = "Al-Mulk", 71 | [68] = "Al-Qalam", 72 | [69] = "Al-Haaqqah", 73 | [70] = "Al-Ma'aarij", 74 | [71] = "Nuuh", 75 | [72] = "Al-Jin", 76 | [73] = "Al-Muzzammil", 77 | [74] = "Al-Muddatstsir", 78 | [75] = "Al-Qiyaamah", 79 | [76] = "Al-Insaan", 80 | [77] = "Al-Mursalaat", 81 | [78] = "An-Naba'", 82 | [79] = "An-Naazi'aat", 83 | [80] = "'Abasa", 84 | [81] = "At-Takwir", 85 | [82] = "Al-Infithaar", 86 | [83] = "Al-Mutaffifin", 87 | [84] = "Al-Insyiqaaq", 88 | [85] = "Al-Buruuj", 89 | [86] = "Ath-Thaariq", 90 | [87] = "Al-A'laa", 91 | [88] = "Al-Ghaashiyah", 92 | [89] = "Al-Fajr", 93 | [90] = "Al-Balad", 94 | [91] = "Asy-Syams", 95 | [92] = "Al-Lail", 96 | [93] = "Ad-Dhuhaa", 97 | [94] = "Alam Nasyrah", 98 | [95] = "At-Tiin", 99 | [96] = "Al-'Alaq", 100 | [97] = "Al-Qadr", 101 | [98] = "Al-Bayyinah", 102 | [99] = "Al-Zalzalah", 103 | [100] = "Al-'Aadiyaat", 104 | [101] = "Al-Qaari'ah", 105 | [102] = "At-Takaatsur", 106 | [103] = "Al-'Ashr", 107 | [104] = "Al-Humazah", 108 | [105] = "Al-Fiil", 109 | [106] = "Quraisy", 110 | [107] = "Al-Maa'uun", 111 | [108] = "Al-Kautsar", 112 | [109] = "Al-Kaafiruun", 113 | [110] = "An-Nashr", 114 | [111] = "Al-Lahab", 115 | [112] = "Al-Ikhlaas", 116 | [113] = "Al-Falaq", 117 | [114] = "An-Naas" 118 | } 119 | 120 | local language = { 121 | ['ar'] = "ar.muyassar", 122 | ['az'] = "az.musayev", 123 | ['bg'] = "bg.theophanov", 124 | ['bn'] = "bn.bengali", 125 | ['bs'] = "bs.mlivo", 126 | ['cs'] = "cs.hrbek", 127 | ['de'] = "de.aburida", 128 | ['dv'] = "dv.divehi", 129 | ['en'] = "en.yusufali", 130 | ['es'] = "es.cortes", 131 | ['fa'] = "fa.makarem", 132 | ['fr'] = "fr.hamidullah", 133 | ['ha'] = "ha.gumi", 134 | ['hi'] = "hi.hindi", 135 | ['id'] = "id.indonesian", 136 | ['it'] = "it.piccardo", 137 | ['ja'] = "ja.japanese", 138 | ['ko'] = "ko.korean", 139 | ['ku'] = "ku.asan", 140 | ['ml'] = "ml.abdulhameed", 141 | ['ms'] = "ms.basmeih", 142 | ['nl'] = "nl.keyzer", 143 | ['no'] = "no.berg", 144 | ['pl'] = "pl.bielawskiego", 145 | ['pt'] = "pt.elhayek", 146 | ['ro'] = "ro.grigore", 147 | ['ru'] = "ru.kuliev", 148 | ['sd'] = "sd.amroti", 149 | ['so'] = "so.abduh", 150 | ['sq'] = "sq.ahmeti", 151 | ['sv'] = "sv.bernstrom", 152 | ['sw'] = "sw.barwani", 153 | ['ta'] = "ta.tamil", 154 | ['tg'] = "tg.ayati", 155 | ['th'] = "th.thai", 156 | ['tr'] = "tr.ozturk", 157 | ['tt'] = "tt.nugman", 158 | ['ug'] = "ug.saleh", 159 | ['ur'] = "ur.ahmedali", 160 | ['uz'] = "uz.sodik", 161 | ['zh'] = "zh.majian" 162 | } 163 | 164 | local function get_verse_num(verse) 165 | for i=1, 6666 do 166 | if verse.quran['quran-simple'][tostring(i)] then 167 | return tostring(i) 168 | end 169 | end 170 | end 171 | 172 | local function get_ayah(msg, surah, ayah, verse, lang) 173 | local gq = 'http://api.globalquran.com/ayah/' 174 | local gq_lang = nil 175 | 176 | if lang then 177 | if language[tostring(lang)] then 178 | translation = language[tostring(lang)] 179 | else 180 | translation = lang 181 | end 182 | end 183 | 184 | if verse then 185 | gq_ayah = gq .. verse .. '/quran-simple?key=' .. _config.api_key.globalquran 186 | if lang then 187 | gq_lang = gq .. verse .. '/' .. translation .. '?key=' .. _config.api_key.globalquran 188 | end 189 | end 190 | 191 | if surah and ayah then 192 | gq_ayah = gq .. surah .. ':' .. ayah .. '/quran-simple?key=' .. _config.api_key.globalquran 193 | if lang then 194 | gq_lang = gq .. surah .. ':' .. ayah .. '/' .. translation .. '?key=' .. _config.api_key.globalquran 195 | end 196 | end 197 | 198 | local verse_trans = '' 199 | local res_ayah, code_ayah = http.request(gq_ayah) 200 | local jayah = json:decode(res_ayah) 201 | local verse_num = get_verse_num(jayah) 202 | 203 | 204 | if gq_lang then 205 | local res_lang, code_lang = http.request(gq_lang) 206 | local jlang = json:decode(res_lang) 207 | if next(jlang.quran) == nil then 208 | send_message(msg, 'Unknown language.\nPlease consult http://api.globalquran.com/quran for full list of the supported languages.', 'html') 209 | return 210 | else 211 | verse_trans = jlang.quran[translation][verse_num].verse 212 | end 213 | end 214 | 215 | local surah_num = jayah.quran['quran-simple'][verse_num].surah 216 | local ayah_num = jayah.quran['quran-simple'][verse_num].ayah 217 | local gq_output = jayah.quran['quran-simple'][verse_num].verse .. '\n\n' 218 | .. verse_trans .. ' (' .. surah_name[surah_num] .. ':' .. ayah_num .. ')' 219 | 220 | send_message(msg, gq_output, 'html') 221 | end 222 | 223 | function run(msg, matches) 224 | check_api_key(msg, 'globalquran', 'http://globalquran.com/contribute/signup.php') 225 | 226 | if matches[1] == 'setapikey globalquran' and is_sudo(msg.from.peer_id) then 227 | _config.api_key.globalquran = matches[2] 228 | save_config() 229 | send_message(msg, 'Global Quran api key has been saved.', 'html') 230 | return 231 | end 232 | 233 | if #matches == 1 then 234 | print('method #1') 235 | get_ayah(msg, nil, nil, matches[1], nil) 236 | end 237 | 238 | if #matches == 2 then 239 | print('method #2') 240 | get_ayah(msg, nil, nil, matches[1], matches[2]) 241 | end 242 | 243 | if #matches == 3 then 244 | print('method #3') 245 | get_ayah(msg, matches[1], matches[3], nil, nil) 246 | end 247 | 248 | if #matches == 4 then 249 | print('method #4') 250 | get_ayah(msg, matches[1], matches[3], nil, matches[4]) 251 | end 252 | end 253 | 254 | return { 255 | description = "Returns Al Qur'an verse.", 256 | usage = { 257 | sudo = { 258 | '!setapikey globalquran [api_key]', 259 | 'Set Global Quran API key.' 260 | }, 261 | user = { 262 | '!quran [verse number]', 263 | "Returns Qur'an verse by its number.", 264 | 'Example: !quran 17', 265 | '', 266 | '!quran [verse number] [lang]', 267 | "Returns Qur'an verse and its translation.", 268 | 'Example: !quran 17 id', 269 | '', 270 | '!quran [surah]:[ayah]', 271 | "Returns Qur'an verse by surah and ayah number.", 272 | 'Example: !quran 17:8', 273 | '', 274 | '!quran [surah]:[ayah] [lang]', 275 | "Returns Qur'an verse and its translation by surah and ayah number.", 276 | 'Example: !quran 17:8 id', 277 | '', 278 | 'lang is ISO 639-1 language code.' 279 | }, 280 | }, 281 | patterns = { 282 | '^!quran ([%d]+)$', 283 | '^!quran ([%d]+) (%g.*)$', 284 | '^!quran ([%d]+)(:)([%d]+)$', 285 | '^!quran ([%d]+)(:)([%d]+) (%g.*)$', 286 | '^!(setapikey globalquran) (.*)$' 287 | }, 288 | run = run 289 | } 290 | 291 | end -------------------------------------------------------------------------------- /plugins/reddit.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function run(msg, matches) 4 | local thread_limit = 5 5 | local is_nsfw = false 6 | 7 | if not is_chat_msg(msg) then 8 | thread_limit = 8 9 | end 10 | 11 | if matches[1] == 'nsfw' then 12 | is_nsfw = true 13 | end 14 | 15 | if matches[2] then 16 | if matches[2]:match('^r/') then 17 | url = 'https://www.reddit.com/' .. URL.escape(matches[2]) .. '/.json?limit=' .. thread_limit 18 | else 19 | url = 'https://www.reddit.com/search.json?q=' .. URL.escape(matches[2]) .. '&limit=' .. thread_limit 20 | end 21 | elseif msg.text == '!reddit' then 22 | url = 'https://www.reddit.com/.json?limit=' .. thread_limit 23 | end 24 | 25 | -- Do the request 26 | local res, code = https.request(url) 27 | 28 | if code ~= 200 then 29 | send_message(msg, "There doesn't seem to be anything...", 'html') 30 | end 31 | 32 | local jdat = json:decode(res) 33 | local jdata_child = jdat.data.children 34 | 35 | if #jdata_child == 0 then 36 | return nil 37 | end 38 | 39 | local threadit = {} 40 | local long_url = '' 41 | 42 | for k=1, #jdata_child do 43 | local redd = jdata_child[k].data 44 | 45 | if not redd.is_self then 46 | local link = URL.parse(redd.url) 47 | long_url = '\nLink: ' .. link.scheme .. '://' .. link.host .. '' 48 | end 49 | 50 | local title = unescape_html(redd.title) 51 | 52 | if redd.over_18 and not is_nsfw then 53 | threadit[k] = '' 54 | elseif redd.over_18 and is_nsfw then 55 | threadit[k] = '' .. k .. '. NSFW ' .. '' .. title .. '' .. long_url 56 | else 57 | threadit[k] = '' .. k .. '. ' .. '' .. title .. '' .. long_url 58 | end 59 | end 60 | 61 | local threadit = table.concat(threadit, '\n') 62 | local subreddit = '' .. (matches[2] or 'redd.it') .. '\n\n' 63 | local subreddit = subreddit .. threadit 64 | 65 | if not threadit:match('%w+') then 66 | send_message(msg, 'You must be 18+ to view this community.', 'html') 67 | else 68 | bot_sendMessage(get_receiver_api(msg), subreddit, true, msg.id, 'html') 69 | end 70 | end 71 | 72 | -------------------------------------------------------------------------------- 73 | 74 | return { 75 | description = 'Returns the five (if group) or eight (if private message) top posts for the given subreddit or query, or from the frontpage.', 76 | usage = { 77 | '!reddit', 78 | 'Reddit frontpage.', 79 | '', 80 | '!reddit r/[query]', 81 | '!r r/[query]', 82 | 'Subreddit', 83 | 'Example: !r r/linux', 84 | '', 85 | '!redditnsfw [query]', 86 | '!rnsfw [query]', 87 | 'Subreddit (include NSFW).', 88 | 'Example: !rnsfw r/juicyasians', 89 | '', 90 | '!reddit [query]', 91 | '!r [query]', 92 | 'Search subreddit.', 93 | 'Example: !r telegram bot', 94 | '', 95 | '!redditnsfw [query]', 96 | '!rnsfw [query]', 97 | 'Search subreddit (include NSFW).', 98 | 'Example: !rnsfw maria ozawa', 99 | }, 100 | patterns = { 101 | '^!reddit$', 102 | '^!(r) (.*)$', 103 | '^!(reddit) (.*)$', 104 | '^!r(nsfw) (.*)$', 105 | '^!reddit(nsfw) (.*)$' 106 | }, 107 | run = run 108 | } 109 | 110 | end 111 | -------------------------------------------------------------------------------- /plugins/rss.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function get_base_redis(id, option, extra) 4 | local ex = '' 5 | 6 | if option ~= nil then 7 | ex = ex .. ':' .. option 8 | if extra ~= nil then 9 | ex = ex .. ':' .. extra 10 | end 11 | end 12 | return 'rss:' .. id .. ex 13 | end 14 | 15 | local function prot_url(url) 16 | local url, h = url:gsub('http://', '') 17 | local url, hs = url:gsub('https://', '') 18 | local protocol = 'http' 19 | 20 | if hs == 1 then 21 | protocol = 'https' 22 | end 23 | return url, protocol 24 | end 25 | 26 | local function get_rss(url, prot) 27 | local res, code = nil, 0 28 | 29 | if prot == 'http' then 30 | res, code = http.request(url) 31 | elseif prot == 'https' then 32 | res, code = https.request(url) 33 | end 34 | if code ~= 200 then 35 | return nil, 'Error while doing the petition to ' .. url 36 | end 37 | 38 | local parsed = feedparser.parse(res) 39 | 40 | if parsed == nil then 41 | return nil, 'Error decoding the RSS.\nAre you sure that ' .. url .. ' is an RSS?' 42 | end 43 | return parsed, nil 44 | end 45 | 46 | local function get_new_entries(last, nentries) 47 | local entries = {} 48 | 49 | for k,v in pairs(nentries) do 50 | if v.id == last then 51 | return entries 52 | else 53 | table.insert(entries, v) 54 | end 55 | end 56 | return entries 57 | end 58 | 59 | local function print_subs(msg, id) 60 | local subscriber = msg.to.title 61 | 62 | if id:match('user') then 63 | subscriber = 'You' 64 | end 65 | 66 | local uhash = get_base_redis(id) 67 | local subs = redis:smembers(uhash) 68 | local text = subscriber .. ' are subscribed to:\n---------\n' 69 | 70 | for k,v in pairs(subs) do 71 | text = text .. k .. ') ' .. v .. '\n' 72 | end 73 | 74 | reply_msg(msg.id, text, ok_cb, true) 75 | end 76 | 77 | local function subscribe(msg, id, url) 78 | local baseurl, protocol = prot_url(url) 79 | local prothash = get_base_redis(baseurl, 'protocol') 80 | local lasthash = get_base_redis(baseurl, 'last_entry') 81 | local lhash = get_base_redis(baseurl, 'subs') 82 | local uhash = get_base_redis(id) 83 | 84 | if redis:sismember(uhash, baseurl) then 85 | reply_msg(msg.id, 'You are already subscribed to ' .. url, ok_cb, true) 86 | end 87 | 88 | local parsed, err = get_rss(url, protocol) 89 | 90 | if err ~= nil then 91 | return err 92 | end 93 | 94 | local last_entry = '' 95 | 96 | if #parsed.entries > 0 then 97 | last_entry = parsed.entries[1].id 98 | end 99 | 100 | local name = parsed.feed.title 101 | 102 | redis:set(prothash, protocol) 103 | redis:set(lasthash, last_entry) 104 | redis:sadd(lhash, id) 105 | redis:sadd(uhash, baseurl) 106 | 107 | reply_msg(msg.id, 'You had been subscribed to ' .. name, ok_cb, true) 108 | end 109 | 110 | local function unsubscribe(msg, id, n) 111 | if #n > 3 then 112 | reply_msg(msg.id, "I don't think that you have that many subscriptions.", ok_cb, true) 113 | end 114 | 115 | n = tonumber(n) 116 | local uhash = get_base_redis(id) 117 | local subs = redis:smembers(uhash) 118 | 119 | if n < 1 or n > #subs then 120 | reply_msg(msg.id, 'Subscription id out of range!', ok_cb, true) 121 | end 122 | 123 | local sub = subs[n] 124 | local lhash = get_base_redis(sub, 'subs') 125 | 126 | redis:srem(uhash, sub) 127 | redis:srem(lhash, id) 128 | 129 | local left = redis:smembers(lhash) 130 | 131 | if #left < 1 then -- no one subscribed, remove it 132 | local prothash = get_base_redis(sub, 'protocol') 133 | local lasthash = get_base_redis(sub, 'last_entry') 134 | redis:del(prothash) 135 | redis:del(lasthash) 136 | end 137 | 138 | reply_msg(msg.id, 'You had been unsubscribed from ' .. sub, ok_cb, true) 139 | end 140 | 141 | local function cron() 142 | -- sync every 15 mins? 143 | local keys = redis:keys(get_base_redis('*', 'subs')) 144 | 145 | for k,v in pairs(keys) do 146 | local base = v:match('rss:(.+):subs') -- Get the URL base 147 | local prot = redis:get(get_base_redis(base, 'protocol')) 148 | local last = redis:get(get_base_redis(base, 'last_entry')) 149 | local url = prot .. '://' .. base 150 | local parsed, err = get_rss(url, prot) 151 | 152 | if err ~= nil then 153 | return 154 | end 155 | 156 | local newentr = get_new_entries(last, parsed.entries) 157 | local subscribers = {} 158 | local text = '' -- Send only one message with all updates 159 | 160 | for k2, v2 in pairs(newentr) do 161 | local title = v2.title or 'No title' 162 | local link = v2.link or v2.id or 'No Link' 163 | text = text .. k2 .. '. ' .. title .. '\n' .. link .. '\n' 164 | end 165 | if text ~= '' then 166 | local newlast = newentr[1].id 167 | redis:set(get_base_redis(base, 'last_entry'), newlast) 168 | for k2, receiver in pairs(redis:smembers(v)) do 169 | send_msg(receiver, text, ok_cb, false) 170 | end 171 | end 172 | end 173 | end 174 | 175 | -------------------------------------------------------------------------------- 176 | 177 | local function run(msg, matches) 178 | 179 | local uid = msg.from.peer_id 180 | 181 | -- comment this line if you want this plugin works for all members. 182 | if not is_owner(msg, msg.to.peer_id , uid) then return nil end 183 | 184 | local id = get_receiver(msg) 185 | 186 | if matches[1] == '!rss'then 187 | print_subs(msg, id) 188 | end 189 | if matches[1] == 'sync' then 190 | if not is_sudo(uid) then 191 | reply_msg(msg.id, 'Only sudo users can sync the RSS.', ok_cb, true) 192 | else 193 | cron() 194 | end 195 | end 196 | if matches[1] == 'subscribe' or matches[1] == 'sub' then 197 | subscribe(msg, id, matches[2]) 198 | end 199 | if matches[1] == 'unsubscribe' or matches[1] == 'uns' then 200 | unsubscribe(msg, id, matches[2]) 201 | end 202 | end 203 | 204 | -------------------------------------------------------------------------------- 205 | 206 | 207 | return { 208 | description = 'Manage User/Chat RSS subscriptions. If you are in a chat group, the RSS subscriptions will be of that chat. If you are in an one-to-one talk with the bot, the RSS subscriptions will be yours.', 209 | usage = { 210 | admin = { 211 | '!rss', 212 | 'Get your rss (or chat rss) subscriptions', 213 | '', 214 | '!rss subscribe [url]', 215 | 'Subscribe to that url', 216 | '', 217 | '!rss unsubscribe [id]', 218 | 'Unsubscribe of that id', 219 | '', 220 | '!rss sync', 221 | 'Download now the updates and send it. Only sudo users can use this option.' 222 | }, 223 | }, 224 | patterns = { 225 | '^!rss$', 226 | '^!rss (subscribe) (https?://[%w-_%.%?%.:/%+=&]+)$', 227 | '^!rss (sub) (https?://[%w-_%.%?%.:/%+=&]+)$', 228 | '^!rss (unsubscribe) (%d+)$', 229 | '^!rss (uns) (%d+)$', 230 | '^!rss (sync)$' 231 | }, 232 | run = run, 233 | cron = cron 234 | } 235 | 236 | end 237 | -------------------------------------------------------------------------------- /plugins/salat.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local base_api = 'http://muslimsalat.com' 4 | local calculation = { 5 | [1] = 'Egyptian General Authority of Survey', 6 | [2] = 'University Of Islamic Sciences, Karachi (Shafi)', 7 | [3] = 'University Of Islamic Sciences, Karachi (Hanafi)', 8 | [4] = 'Islamic Circle of North America', 9 | [5] = 'Muslim World League', 10 | [6] = 'Umm Al-Qura', 11 | [7] = 'Fixed Isha' 12 | } 13 | 14 | local function get_time(lat, lng) 15 | local api = 'https://maps.googleapis.com/maps/api/timezone/json?' 16 | local timestamp = os.time(os.date('!*t')) 17 | local parameters = 'location=' .. URL.escape(lat) .. ',' .. URL.escape(lng) 18 | .. '×tamp=' .. URL.escape(timestamp) 19 | local res,code = https.request(api .. parameters) 20 | 21 | if code ~= 200 then 22 | return nil 23 | end 24 | 25 | local data = json:decode(res) 26 | 27 | if (data.status == 'ZERO_RESULTS') then 28 | return nil 29 | end 30 | if (data.status == 'OK') then 31 | return timestamp + data.rawOffset + data.dstOffset 32 | end 33 | end 34 | 35 | function totwentyfour(twelvehour) 36 | local hour, minute, meridiem = string.match(twelvehour, '^(.-):(.-) (.-)$') 37 | local hour = tonumber(hour) 38 | if (meridiem == 'am') and (hour == 12) then 39 | hour = 0 40 | elseif (meridiem == 'pm') and (hour < 12) then 41 | hour = hour + 12 42 | end 43 | 44 | if hour < 10 then 45 | hour = '0' .. hour 46 | end 47 | 48 | return (hour .. ':' .. minute) 49 | end 50 | 51 | function run(msg, matches) 52 | check_api_key(msg, 'muslimsalat', 'http://muslimsalat.com/panel/signup.php') 53 | 54 | if matches[1] == 'setapikey muslimsalat' and is_sudo(msg.from.peer_id) then 55 | _config.api_key.muslimsalat = matches[2] 56 | save_config() 57 | send_message(msg, 'Muslim salat api key has been saved.', 'html') 58 | return 59 | end 60 | 61 | local area = matches[1] 62 | local method = 5 63 | local notif = '' 64 | local url = base_api .. '/' .. URL.escape(area) .. '.json' 65 | 66 | if matches[2] and matches[1]:match('%d') then 67 | local c_method = tonumber(matches[1]) 68 | 69 | if c_method == 0 or c_method > 7 then 70 | local text = 'Calculation method is out of range\n' 71 | .. 'Consult !help salat' 72 | send_message(msg, text, 'html') 73 | return 74 | else 75 | method = c_method 76 | url = base_api .. '/' .. URL.escape(matches[2]) .. '.json' 77 | notif = '\n\nMethod: ' .. calculation[method] 78 | area = matches[2] 79 | end 80 | end 81 | 82 | local res, code = http.request(url .. '/' .. method .. '?key=' .. _config.api_key.muslimsalat) 83 | 84 | if code ~= 200 then 85 | send_message(msg, 'Error: ' .. code .. '', 'html') 86 | return 87 | end 88 | 89 | local salat = json:decode(res) 90 | local localTime = get_time(salat.latitude, salat.longitude) 91 | 92 | if salat.title == '' then 93 | salat_area = area .. ', ' .. salat.country 94 | else 95 | salat_area = salat.title 96 | end 97 | 98 | local is_salat_time = 'Salat time\n\n' 99 | .. '' .. salat_area .. '\n\n' 100 | .. 'Time : ' .. os.date('%T', localTime) .. '\n' 101 | .. 'Qibla : ' .. salat.qibla_direction .. '°\n\n' 102 | .. 'Fajr : ' .. totwentyfour(salat.items[1].fajr) .. '\n' 103 | .. 'Sunrise : ' .. totwentyfour(salat.items[1].shurooq) .. '\n' 104 | .. 'Dhuhr : ' .. totwentyfour(salat.items[1].dhuhr) .. '\n' 105 | .. 'Asr : ' .. totwentyfour(salat.items[1].asr) .. '\n' 106 | .. 'Maghrib : ' .. totwentyfour(salat.items[1].maghrib) .. '\n' 107 | .. 'Isha : ' .. totwentyfour(salat.items[1].isha) .. '' .. notif 108 | 109 | bot_sendMessage(get_receiver_api(msg), is_salat_time, true, msg.id, 'html') 110 | end 111 | 112 | return { 113 | description = 'Returns todays prayer times.', 114 | usage = { 115 | sudo = { 116 | '!setapikey muslimsalat [api_key]', 117 | 'Set Muslim Salat API key.' 118 | }, 119 | user = { 120 | '!salat [area]', 121 | 'Returns todays prayer times for that area', 122 | 'Example: !salat bandung', 123 | '', 124 | '!salat [method] [area]', 125 | 'Returns todays prayer times for that area calculated by [method]:', 126 | '1 = Egyptian General Authority of Survey', 127 | '2 = University Of Islamic Sciences, Karachi (Shafi)', 128 | '3 = University Of Islamic Sciences, Karachi (Hanafi)', 129 | '4 = Islamic Circle of North America', 130 | '5 = Muslim World League', 131 | '6 = Umm Al-Qura', 132 | '7 = Fixed Isha', 133 | 'Example: !salat 2 denpasar', 134 | }, 135 | }, 136 | patterns = { 137 | '^!salat (%a.*)$', 138 | '^!salat (%d) (%a.*)$', 139 | '^!(setapikey muslimsalat) (.*)$' 140 | }, 141 | run = run 142 | } 143 | 144 | end 145 | 146 | -------------------------------------------------------------------------------- /plugins/stats.lua: -------------------------------------------------------------------------------- 1 | -- Saves the number of messages from a user 2 | -- Can check the number of messages with !stats 3 | 4 | do 5 | 6 | local NUM_MSG_MAX = 5 7 | local TIME_CHECK = 4 -- seconds 8 | 9 | local function user_print_name(user) 10 | local text = '' 11 | 12 | if user.first_name then 13 | text = user.first_name .. ' ' 14 | end 15 | if user.last_name then 16 | text = text .. user.last_name 17 | end 18 | return text 19 | end 20 | 21 | -- Returns a table with `name` and `msgs` 22 | local function get_msgs_user_chat(user_id, chat_id) 23 | local user_info = {} 24 | local uhash = 'user:' .. user_id 25 | local user = redis:hgetall(uhash) 26 | local um_hash = 'msgs:' .. user_id .. ':' .. chat_id 27 | user_info.msgs = tonumber(redis:get(um_hash) or 0) 28 | user_info.id = user_id 29 | user_info.name = user_print_name(user) 30 | return user_info 31 | end 32 | 33 | local function chat_stats(msg, chat_id) 34 | -- Users on chat 35 | local hash = 'chat:' .. chat_id .. ':users' 36 | local users = redis:smembers(hash) 37 | local users_info = {} 38 | 39 | -- Get user info 40 | for i = 1, #users do 41 | local user_id = users[i] 42 | local user_info = get_msgs_user_chat(user_id, chat_id) 43 | table.insert(users_info, user_info) 44 | end 45 | 46 | -- Sort users by msgs number 47 | table.sort(users_info, function(a, b) 48 | if a.msgs and b.msgs then 49 | return a.msgs > b.msgs 50 | end 51 | end) 52 | 53 | local text = '' 54 | 55 | for k,user in pairs(users_info) do 56 | text = text .. '*' .. k .. '*. `' .. user.id .. '` - ' .. markdown_escape(user.name) .. ' = *' .. user.msgs .. '*\n' 57 | end 58 | bot_sendMessage(get_receiver_api(msg), text, true, msg.id, 'markdown') 59 | end 60 | 61 | -------------------------------------------------------------------------------- 62 | 63 | -- Save stats, ban user 64 | local function pre_process(msg) 65 | -- Ignore service msg 66 | if msg.service then 67 | print('Service message') 68 | return msg 69 | end 70 | 71 | -- Save user on Redis 72 | if msg.from.peer_type == 'user' then 73 | local hash = 'user:' .. msg.from.peer_id 74 | print('Saving user', hash) 75 | if msg.from.print_name then 76 | redis:hset(hash, 'print_name', msg.from.print_name) 77 | end 78 | if msg.from.first_name then 79 | redis:hset(hash, 'first_name', msg.from.first_name) 80 | end 81 | if msg.from.last_name then 82 | redis:hset(hash, 'last_name', msg.from.last_name) 83 | end 84 | end 85 | 86 | -- Save stats on Redis 87 | if msg.to.peer_type == 'chat' or msg.to.peer_type == 'channel' then 88 | -- User is on chat 89 | local hash = 'chat:' .. msg.to.peer_id .. ':users' 90 | redis:sadd(hash, msg.from.peer_id) 91 | end 92 | 93 | -- Total user msgs 94 | local hash = 'msgs:' .. msg.from.peer_id .. ':' .. msg.to.peer_id 95 | redis:incr(hash) 96 | 97 | -- Check flood 98 | if msg.from.peer_type == 'user' then 99 | local hash = 'user:' .. msg.from.peer_id .. ':msgs' 100 | local msgs = tonumber(redis:get(hash) or 0) 101 | 102 | if msgs > NUM_MSG_MAX then 103 | print('User ' .. msg.from.peer_id .. ' is flooding ' .. msgs) 104 | msg = nil 105 | end 106 | redis:setex(hash, TIME_CHECK, msgs+1) 107 | end 108 | 109 | return msg 110 | end 111 | 112 | local function bot_stats() 113 | 114 | local redis_scan = [[ 115 | local cursor = '0' 116 | local count = 0 117 | 118 | repeat 119 | local r = redis.call('SCAN', cursor, 'MATCH', KEYS[1]) 120 | cursor = r[1] 121 | count = count + #r[2] 122 | until cursor == '0' 123 | return count]] 124 | 125 | -- Users 126 | local hash = 'msgs:*:' .. our_id 127 | local r = redis:eval(redis_scan, 1, hash) 128 | local text = 'Users: ' .. r 129 | 130 | hash = 'chat:*:users' 131 | r = redis:eval(redis_scan, 1, hash) 132 | text = text .. '\nChats: ' .. r 133 | 134 | return text 135 | 136 | end 137 | 138 | -------------------------------------------------------------------------------- 139 | 140 | local function run(msg, matches) 141 | if is_administrate(msg, msg.to.peer_id) then 142 | if matches[1]:lower() == 'stats' then 143 | if not matches[2] then 144 | if is_chat_msg(msg) then 145 | return chat_stats(msg, msg.to.peer_id) 146 | else 147 | return 'Stats works only on chats' 148 | end 149 | end 150 | 151 | if matches[2] == 'bot' then 152 | if not is_sudo(msg.from.peer_id) then 153 | return 'Bot stats requires privileged user' 154 | else 155 | return bot_stats() 156 | end 157 | end 158 | 159 | if matches[2] == 'chat' then 160 | if not is_sudo(msg.from.peer_id) then 161 | return 'This command requires privileged user' 162 | else 163 | return chat_stats(msg, matches[3]) 164 | end 165 | end 166 | end 167 | end 168 | end 169 | 170 | -------------------------------------------------------------------------------- 171 | 172 | return { 173 | description = 'Plugin to update user stats.', 174 | usage = { 175 | '!stats', 176 | 'Returns a list of Username [telegram_id] : msg_num', 177 | '', 178 | '!stats chat [chat_id]', 179 | 'Show stats for chat_id', 180 | '', 181 | '!stats bot', 182 | 'Shows bot stats (sudo users)' 183 | }, 184 | patterns = { 185 | '^!([Ss]tats)$', 186 | '^!([Ss]tats) (chat) (%d+)', 187 | '^!([Ss]tats) (bot)' 188 | }, 189 | run = run, 190 | pre_process = pre_process 191 | } 192 | 193 | end 194 | -------------------------------------------------------------------------------- /plugins/sudo.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function cb_getdialog(extra, success, result) 4 | vardump(extra) 5 | vardump(result) 6 | end 7 | 8 | local function parsed_url(link) 9 | local parsed_link = URL.parse(link) 10 | local parsed_path = URL.parse_path(parsed_link.path) 11 | 12 | for k,segment in pairs(parsed_path) do 13 | if segment == 'joinchat' then 14 | invite_link = parsed_path[k+1]:gsub('[ %c].+$', '') 15 | break 16 | end 17 | end 18 | return invite_link 19 | end 20 | 21 | local function action_by_reply(extra, success, result) 22 | local hash = parsed_url(result.text) 23 | join = import_chat_link(hash, ok_cb, false) 24 | end 25 | 26 | -------------------------------------------------------------------------------- 27 | 28 | function run(msg, matches) 29 | 30 | if not is_sudo(msg.from.peer_id) then 31 | return 32 | end 33 | 34 | if matches[1] == 'bin' then 35 | local input = matches[2]:gsub('—', '--') 36 | local header = '$ ' .. input .. '\n' 37 | local stdout = io.popen(input):read('*all') 38 | 39 | bot_sendMessage(get_receiver_api(msg), header .. '' .. stdout .. '', true, msg.id, 'html') 40 | end 41 | 42 | if matches[1] == 'bot' then 43 | if matches[2] == 'token' then 44 | if not _config.bot_api then 45 | _config.bot_api = {key = '', uid = '', uname = '', master = ''} 46 | end 47 | local botid = api_getme(matches[3]) 48 | _config.bot_api.key = matches[3] 49 | _config.bot_api.uid = botid.id 50 | _config.bot_api.uname = botid.username 51 | save_config() 52 | send_message(msg, 'Bot API key has been saved', 'html') 53 | end 54 | end 55 | if matches[1] == "block" then 56 | block_user("user#id" .. matches[2], ok_cb, false) 57 | 58 | if is_mod(matches[2], msg.to.peer_id) then 59 | return "You can't block moderators." 60 | end 61 | if is_admin(matches[2]) then 62 | return "You can't block administrators." 63 | end 64 | block_user("user#id" .. matches[2], ok_cb, false) 65 | return "User blocked" 66 | end 67 | 68 | if matches[1] == "unblock" then 69 | unblock_user("user#id" .. matches[2], ok_cb, false) 70 | return "User unblocked" 71 | end 72 | 73 | if matches[1] == "join" then 74 | if msg.reply_id then 75 | get_message(msg.reply_id, action_by_reply, msg) 76 | elseif matches[2] then 77 | local hash = parsed_url(matches[2]) 78 | join = import_channel_link(hash, ok_cb, false) 79 | end 80 | end 81 | 82 | if matches[1] == 'setlang' then 83 | _config.lang = matches[2] 84 | save_config() 85 | 86 | send_message(msg, 'Set bot language to ' .. matches[2], 'html') 87 | end 88 | end 89 | 90 | -------------------------------------------------------------------------------- 91 | 92 | return { 93 | description = 'Various sudo commands.', 94 | usage = { 95 | sudo = { 96 | '!bin [command]', 97 | 'Run a system command.', 98 | '', 99 | '!block [user_id]', 100 | 'Block user_id to PM.', 101 | '', 102 | '!unblock [user_id]', 103 | 'Allowed user_id to PM.', 104 | '', 105 | '!bot restart', 106 | 'Restart bot.', 107 | '', 108 | '!bot status', 109 | 'Print bot status.', 110 | '', 111 | '!bot token [bot_api_key]', 112 | 'Input bot API key.', 113 | '', 114 | '!join', 115 | 'Join a group by replying a message containing invite link.', 116 | '', 117 | '!join [invite_link]', 118 | 'Join into a group by providing their [invite_link].', 119 | '', 120 | '!version', 121 | 'Shows bot version', 122 | }, 123 | }, 124 | patterns = { 125 | '^!(bin) (.*)$', 126 | '^!(block) (.*)$', 127 | '^!(unblock) (.*)$', 128 | '^!(block) (%d+)$', 129 | '^!(unblock) (%d+)$', 130 | '^!(bot) (%g+) (.*)$', 131 | '^!(join)$', 132 | '^!(join) (.*)$', 133 | '^!(setlang) (%g+)$' 134 | }, 135 | run = run 136 | } 137 | 138 | end -------------------------------------------------------------------------------- /plugins/time.lua: -------------------------------------------------------------------------------- 1 | -- Implement a command !time [area] which uses 2 | -- 2 Google APIs to get the desired result: 3 | -- 1. Geocoding to get from area to a lat/long pair 4 | -- 2. Timezone to get the local time in that lat/long location 5 | 6 | do 7 | 8 | -- Globals 9 | -- If you have a google api key for the geocoding/timezone api 10 | local api_key = nil 11 | local dateFormat = '%A, %F %T' 12 | 13 | -- Need the utc time for the google api 14 | local function utctime() 15 | return os.time(os.date('!*t')) 16 | end 17 | 18 | -- Use timezone api to get the time in the lat, 19 | -- Note: this needs an API key 20 | local function get_time(lat, lng) 21 | local api = 'https://maps.googleapis.com/maps/api/timezone/json?' 22 | 23 | -- Get a timestamp (server time is relevant here) 24 | local timestamp = utctime() 25 | local parameters = 'location=' .. URL.escape(lat) .. ',' .. URL.escape(lng) 26 | .. '×tamp=' .. URL.escape(timestamp) 27 | 28 | if api_key ~=nil then 29 | parameters = parameters .. '&key=' .. api_key 30 | end 31 | 32 | local res,code = https.request(api .. parameters) 33 | 34 | if code ~= 200 then 35 | return nil 36 | end 37 | 38 | local data = json:decode(res) 39 | 40 | if (data.status == 'ZERO_RESULTS') then 41 | return nil 42 | end 43 | if (data.status == 'OK') then 44 | -- Construct what we want 45 | -- The local time in the location is: timestamp + rawOffset + dstOffset 46 | local localTime = timestamp + data.rawOffset + data.dstOffset 47 | return localTime, data.timeZoneId 48 | end 49 | return localTime 50 | end 51 | 52 | local function getformattedLocalTime(msg, area) 53 | if area == nil then 54 | send_message(msg, 'The time in nowhere is never.', 'html') 55 | end 56 | 57 | local coordinats, code = get_coords(msg, area) 58 | 59 | if not coordinats then 60 | send_message(msg, 'It seems that in "' .. area .. '" they do not have a concept of time.', 'html') 61 | return 62 | end 63 | 64 | local lat = coordinats.lat 65 | local long = coordinats.lon 66 | local localTime, timeZoneId = get_time(lat, long) 67 | 68 | send_message(msg, 'The local time in ' .. area .. ' (' .. timeZoneId .. ') is:\n' 69 | .. '' .. os.date(dateFormat,localTime) .. '', 'html') 70 | end 71 | 72 | local function run(msg, matches) 73 | return getformattedLocalTime(msg, matches[1]) 74 | end 75 | 76 | return { 77 | description = 'Displays the local time in an area', 78 | usage = { 79 | '!time [area]', 80 | 'Displays the local time in that [area]', 81 | 'Example: !time yogyakarta', 82 | }, 83 | patterns = { 84 | '^!time (.*)$' 85 | }, 86 | run = run 87 | } 88 | 89 | end 90 | 91 | -------------------------------------------------------------------------------- /plugins/translate.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function yandex_translate(msg, source_lang, target_lang, text) 4 | if source_lang and target_lang then 5 | lang = source_lang .. '-' .. target_lang 6 | elseif target_lang then 7 | lang = target_lang 8 | elseif not source_lang and not target_lang then 9 | lang = _config.lang or 'en' 10 | end 11 | 12 | local url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=' .. _config.api_key.yandex .. '&lang=' .. lang .. '&text=' .. URL.escape(text) 13 | local str, res= https.request(url) 14 | local jstr = json:decode(str) 15 | 16 | if jstr.code == 200 then 17 | send_message(msg, jstr.text[1], 'html') 18 | else 19 | send_message(msg, jstr.message, 'html') 20 | end 21 | end 22 | 23 | local function trans_by_reply(extra, success, result) 24 | yandex_translate(extra.msg, extra.srclang, extra.tolang, result.text) 25 | end 26 | 27 | local function run(msg, matches) 28 | check_api_key(msg, 'yandex', 'http://tech.yandex.com/keys/get') 29 | 30 | if matches[1] == 'setapikey yandex' and is_sudo(msg.from.peer_id) then 31 | _config.api_key.yandex = matches[2] 32 | save_config() 33 | send_message(msg, 'Muslim salat api key has been saved.', 'html') 34 | return 35 | end 36 | 37 | -- comment this line if you want this plugin to works in private message. 38 | --if not is_chat_msg(msg) and not is_admin(msg.from.peer_id) then return nil end 39 | 40 | if msg.reply_id then 41 | if matches[1] == 'translate' then 42 | -- Third pattern 43 | if #matches == 1 then 44 | print("First") 45 | get_message(msg.reply_id, trans_by_reply, {msg=msg, srclang=nil, tolang=nil}) 46 | end 47 | 48 | -- Second pattern 49 | if #matches == 2 then 50 | print("Second") 51 | get_message(msg.reply_id, trans_by_reply, {msg=msg, srclang=nil, tolang=matches[2]}) 52 | end 53 | 54 | -- First pattern 55 | if #matches == 3 then 56 | print("Third") 57 | get_message(msg.reply_id, trans_by_reply, {msg=msg, srclang=matches[2], tolang=matches[3]}) 58 | end 59 | end 60 | else 61 | if msg.reply_to_message then 62 | local text = msg.reply_to_message.text 63 | if matches[1] == 'translate' then 64 | if #matches == 1 then 65 | print("First") 66 | yandex_translate(msg, nil, nil, text) 67 | end 68 | 69 | if #matches == 2 then 70 | print("Second") 71 | yandex_translate(msg, nil, matches[2], text) 72 | end 73 | 74 | if #matches == 3 then 75 | print("Third") 76 | yandex_translate(msg, matches[2], matches[3], text) 77 | end 78 | end 79 | else 80 | if matches[1] == 'trans' then 81 | -- Third pattern 82 | if #matches == 2 then 83 | print("Fourth") 84 | local text = matches[2] 85 | yandex_translate(msg, nil, nil, text) 86 | end 87 | 88 | -- Second pattern 89 | if #matches == 3 then 90 | print("Fifth") 91 | local target = matches[2] 92 | local text = matches[3] 93 | yandex_translate(msg, nil, target, text) 94 | end 95 | 96 | -- First pattern 97 | if #matches == 4 then 98 | print("Sixth") 99 | local source = matches[2] 100 | local target = matches[3] 101 | local text = matches[4] 102 | yandex_translate(msg, source, target, text) 103 | end 104 | end 105 | end 106 | end 107 | end 108 | 109 | return { 110 | description = "Translate some text", 111 | usage = { 112 | sudo = { 113 | '!setapikey yandex [api_key]', 114 | 'Set Yandex Translate API key.' 115 | }, 116 | user = { 117 | '!trans text', 118 | 'Translate the text into the default language (or english).', 119 | 'Example: !trans terjemah', 120 | '', 121 | '!trans target_lang text', 122 | 'Translate the text to target_lang.', 123 | 'Example: !trans en terjemah', 124 | '', 125 | '!trans source,target text', 126 | 'Translate the source to target.', 127 | 'Example: !trans id,en terjemah', 128 | '', 129 | 'Use !translate when reply!', 130 | '', 131 | '!translate', 132 | 'By reply. Translate the replied text into the default language (or english).', 133 | '', 134 | '!translate target_lang', 135 | 'By reply. Translate the replied text into target_lang.', 136 | '', 137 | '!translate source,target', 138 | 'By reply. Translate the replied text source to target.', 139 | '', 140 | 'Languages are two letter ISO 639-1 language code', 141 | }, 142 | }, 143 | patterns = { 144 | "^!(trans) ([%w]+),([%a]+) (.+)", 145 | "^!(trans) ([%w]+) (.+)", 146 | "^!(trans) (.+)", 147 | "^!(translate) ([%w]+),([%a]+)", 148 | "^!(translate) ([%w]+)", 149 | "^!(translate)", 150 | '^!(setapikey yandex) (.*)$' 151 | }, 152 | run = run 153 | } 154 | 155 | end 156 | -------------------------------------------------------------------------------- /plugins/urbandictionary.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function get_udescription(msg, matches) 4 | local url = 'http://api.urbandictionary.com/v0/define?term=' .. URL.escape(matches) 5 | 6 | local jstr, res = http.request(url) 7 | if res ~= 200 then 8 | send_message(msg, 'Connection error', 'html') 9 | return 10 | end 11 | 12 | local jdat = json:decode(jstr) 13 | if jdat.result_type == 'no_results' then 14 | send_message(msg, "There aren't any definitions for " .. matches .. " yet.", 'html') 15 | return 16 | end 17 | 18 | local output = jdat.list[1].definition:trim() 19 | if string.len(jdat.list[1].example) > 0 then 20 | output = output .. '\n\n' .. jdat.list[1].example:trim() 21 | end 22 | 23 | send_message(msg, output, nil) 24 | end 25 | 26 | local function ud_by_reply(extra, success, result) 27 | if extra.to.peer_id == result.to.peer_id then 28 | get_udescription(result, result.text) 29 | else 30 | reply_msg(extra.id, "Sorry, I can't resolve a username from an old message", ok_cb, true) 31 | end 32 | end 33 | 34 | local function run(msg, matches) 35 | if msg.reply_id then 36 | if matches[1] == 'urbandictionary' or matches[1] == 'ud' or matches[1] == 'urban' then 37 | get_message(msg.reply_id, ud_by_reply, msg) 38 | end 39 | else 40 | if msg.reply_to_message then 41 | get_udescription(msg, msg.reply_to_message.text) 42 | else 43 | get_udescription(msg, matches[1]) 44 | end 45 | end 46 | end 47 | 48 | return { 49 | description = 'Returns a definition from Urban Dictionary.', 50 | usage = { 51 | '!ud [query]', 52 | '!urban [query]', 53 | '!urbandictionary [query]', 54 | 'Returns a [query] definition from urbandictionary.com', 55 | 'Example: !ud fam', 56 | '', 57 | '!ud', 58 | '!urban', 59 | '!urbandictionary', 60 | 'By reply. Returns a [query] definition from urbandictionary.com', 61 | 'The [query] is the replied message text.' 62 | }, 63 | patterns = { 64 | '^!(urbandictionary)$', 65 | '^!(ud)$', 66 | '^!(urban)$', 67 | '^!urbandictionary (.+)$', 68 | '^!ud (.+)$', 69 | '^!urban (.+)$' 70 | }, 71 | run = run 72 | } 73 | 74 | end 75 | -------------------------------------------------------------------------------- /plugins/webshot.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local helpers = require 'OAuth.helpers' 4 | local base = 'https://screenshotmachine.com/' 5 | local url = base .. 'processor.php' 6 | 7 | local function get_webshot_url(param) 8 | local response_body = {} 9 | local request_constructor = { 10 | url = url, 11 | method = 'GET', 12 | sink = ltn12.sink.table(response_body), 13 | headers = { 14 | referer = base, 15 | dnt = '1', 16 | origin = base, 17 | ['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0' 18 | }, 19 | redirect = false 20 | } 21 | local arguments = { 22 | urlparam = param, 23 | size = 'FULL' 24 | } 25 | 26 | request_constructor.url = url .. '?' .. helpers.url_encode_arguments(arguments) 27 | 28 | local ok, response_code, response_headers, response_status_line = https.request(request_constructor) 29 | 30 | if not ok or response_code ~= 200 then 31 | return nil 32 | end 33 | 34 | local response = table.concat(response_body) 35 | 36 | return string.match(response, "href='(.-)'") 37 | end 38 | 39 | local function run(msg, matches) 40 | local find = get_webshot_url(matches[1]) 41 | 42 | if find then 43 | local imgurl = base .. find 44 | local receiver = get_receiver(msg) 45 | --send_photo_from_url(receiver, imgurl) 46 | local webshotimg = download_to_file(imgurl, nil) 47 | 48 | if msg.from.api then 49 | bot_sendPhoto(get_receiver_api(msg), webshotimg, nil, true, msg.id) 50 | else 51 | reply_photo(msg.id, webshotimg, ok_cb, true) 52 | end 53 | end 54 | end 55 | 56 | return { 57 | description = 'Send an screenshot of a website.', 58 | usage = { 59 | '!webshot [url]', 60 | 'Take an screenshot of the [url] and send it back to you.' 61 | }, 62 | patterns = { 63 | '^!webshot (https?://[%w-_%.%?%.:/%+=&]+)$', 64 | }, 65 | run = run 66 | } 67 | 68 | end 69 | -------------------------------------------------------------------------------- /plugins/whois.lua: -------------------------------------------------------------------------------- 1 | -- dependency: whois 2 | -- install on your system, i.e sudo aptitude install whois 3 | 4 | do 5 | 6 | local whofile = '/tmp/whois.txt' 7 | 8 | local function whoinfo() 9 | local file = io.open(whofile, 'r') 10 | local content = file:read "*a" 11 | file:close() 12 | return content:sub(1, 4000) 13 | end 14 | 15 | local function run(msg, matches) 16 | local result = os.execute('whois ' .. matches[1] .. ' > ' .. whofile) 17 | 18 | if not result then 19 | if whoinfo():match('no match') then 20 | send_message(msg, 'No match for ' .. matches[1] .. '\n' 21 | .. '* type the URL correctly\n' 22 | .. '* exclude http(s) and www from the URL.', 'html') 23 | elseif not os.execute('which whois') then 24 | send_message(msg, 'sh: 1: whois: not found.\n' 25 | .. 'Please install whois package on your system.', 'html') 26 | end 27 | return 28 | end 29 | 30 | if matches[2] then 31 | if matches[2] == 'txt' then 32 | if msg.from.api then 33 | bot_sendDocument(get_receiver_api(msg), whofile, nil, true, msg.id) 34 | else 35 | reply_file(msg.id, whofile, ok_cb, true) 36 | end 37 | end 38 | if matches[2] == 'pm' and is_chat_msg(msg) then 39 | bot_sendMessage(msg.from.peer_id, whoinfo(), true, nil, nil) 40 | end 41 | if matches[2] == 'pmtxt' and is_chat_msg(msg) then 42 | bot_sendDocument(msg.from.peer_id, whofile, nil, true, nil) 43 | end 44 | else 45 | send_message(msg, whoinfo(), nil) 46 | end 47 | end 48 | 49 | return { 50 | description = 'Whois lookup.', 51 | usage = { 52 | '!whois [url]', 53 | 'Returns whois lookup for [url]', 54 | '', 55 | '!whois [url] txt', 56 | 'Returns whois lookup for [url] and then send as text file.', 57 | '', 58 | '!whois [url] pm', 59 | 'Returns whois lookup for [url] into requester PM.', 60 | '', 61 | '!whois [url] pmtxt', 62 | 'Returns whois lookup file for [url] and then send into requester PM.', 63 | }, 64 | patterns = { 65 | '^!whois (%g+)$', 66 | '^!whois (%g+) (txt)$', 67 | '^!whois (%g+) (pm)$', 68 | '^!whois (%g+) (pmtxt)$' 69 | }, 70 | run = run 71 | } 72 | 73 | end 74 | -------------------------------------------------------------------------------- /plugins/xkcd.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | function get_last_id(msg) 4 | local res, code = https.request('http://xkcd.com/info.0.json') 5 | 6 | if code ~= 200 then 7 | send_message(msg, 'HTTP ERROR', 'html') 8 | end 9 | 10 | local data = json:decode(res) 11 | 12 | return data.num 13 | end 14 | 15 | function get_xkcd(msg, id) 16 | local res,code = http.request('http://xkcd.com/' .. id .. '/info.0.json') 17 | 18 | if code ~= 200 then 19 | send_message(msg, 'HTTP ERROR', 'html') 20 | end 21 | 22 | local data = json:decode(res) 23 | local link_image = data.img 24 | 25 | if link_image:sub(0,2) == '//' then 26 | link_image = msg.text:sub(3,-1) 27 | end 28 | 29 | return link_image, data.num, data.title, data.alt 30 | end 31 | 32 | function get_xkcd_random(msg) 33 | local last = get_last_id(msg) 34 | local i = math.random(1, last) 35 | return get_xkcd(msg, i) 36 | end 37 | 38 | function run(msg, matches) 39 | if matches[1] == 'xkcd' then 40 | url, num, title, alt = get_xkcd_random(msg) 41 | else 42 | url, num, title, alt = get_xkcd(msg, matches[1]) 43 | end 44 | 45 | local relevantxkcd = '' .. title .. '\n\n' .. alt .. '\n\n' 46 | 47 | bot_sendMessage(get_receiver_api(msg), relevantxkcd, false, msg.id, 'html') 48 | end 49 | 50 | return { 51 | description = 'Send comic images from xkcd', 52 | usage = { 53 | '!xkcd', 54 | 'Send random xkcd image and title.', 55 | '', 56 | '!xkcd (id)', 57 | 'Send an xkcd image and title.', 58 | 'Example: !xkcd 149', 59 | }, 60 | patterns = { 61 | '^!(xkcd)$', 62 | '^!xkcd (%d+)', 63 | }, 64 | run = run 65 | } 66 | 67 | end 68 | -------------------------------------------------------------------------------- /plugins/yify.lua: -------------------------------------------------------------------------------- 1 | do 2 | 3 | local function search_yify(msg, query) 4 | local url = 'https://yts.ag/api/v2/list_movies.json?limit=1&query_term=' .. URL.escape(query) 5 | local resp = {} 6 | local b,c = https.request { 7 | url = url, 8 | protocol = 'tlsv1', 9 | sink = ltn12.sink.table(resp) 10 | } 11 | local resp = table.concat(resp) 12 | local jresult = json:decode(resp) 13 | 14 | if not jresult.data.movies then 15 | send_message(msg, 'No torrent results for: ' .. query, 'html') 16 | else 17 | local yify = jresult.data.movies[1] 18 | local yts = yify.torrents 19 | local yifylist = {} 20 | 21 | for i=1, #yts do 22 | yifylist[i] = '' .. yts[i].quality .. ': .torrent\n' 23 | .. 'Seeds: ' .. yts[i].seeds .. ' | ' .. 'Peers: ' .. yts[i].peers .. ' | ' .. 'Size: ' .. yts[i].size .. '' 24 | end 25 | 26 | local torrlist = table.concat(yifylist, '\n\n') 27 | local title = '' .. yify.title_long .. '' 28 | local output = title .. '\n\n' 29 | .. '' .. yify.year .. ' | ' .. yify.rating .. '/10 | ' .. yify.runtime .. ' min\n\n' 30 | .. torrlist .. '\n\n' .. yify.synopsis:sub(1, 2000) .. ' More on yts.ag ...' 31 | 32 | bot_sendMessage(get_receiver_api(msg), output, false, msg.id, 'html') 33 | end 34 | end 35 | 36 | local function run(msg, matches) 37 | return search_yify(msg, matches[1]) 38 | end 39 | 40 | return { 41 | description = 'Search YTS YIFY movies.', 42 | usage = { 43 | '!yify [search term]', 44 | '!yts [search term]', 45 | 'Search YTS YIFY movie torrents from yts.ag', 46 | 'Example: !yts ex machina', 47 | }, 48 | patterns = { 49 | '^!yify (.+)$', 50 | '^!yts (.+)$' 51 | }, 52 | run = run, 53 | } 54 | 55 | end --------------------------------------------------------------------------------