Compare commits
680 commits
WIP/module
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 27bd0c50c1 | |||
| 9d7c278d1a | |||
| 310f933091 | |||
| 26282cb81d | |||
| de2c37a54a | |||
| 622159f6b5 | |||
| 32ebe42f41 | |||
| ea0ec42a4b | |||
| 23f043673f | |||
| ac432fabcc | |||
| 38b5b1eabd | |||
| 84fef789b5 | |||
| 45a27b477d | |||
| a8472ecc29 | |||
| 861ca0afdd | |||
| 9f83e5b178 | |||
| 68c61f40d3 | |||
| 60a9ec92b7 | |||
| 8dd6b9d471 | |||
| 13c643fc19 | |||
| 5ec2f2997b | |||
| faa5759645 | |||
| 37a230e70e | |||
| 904ad72316 | |||
| f17f8b9dfa | |||
| d56c2396c0 | |||
| b369683914 | |||
| aee2da4122 | |||
| 1f2d297ea1 | |||
|
|
b72871a8c2 | ||
| 644a641b13 | |||
| 4499677d55 | |||
| 87b5ce842d | |||
| 144551a232 | |||
| 7854e8628f | |||
| 85c418bd06 | |||
| 20c19a72bc | |||
| 9417e2ba93 | |||
| 517bf21d25 | |||
| fa0f2e93ef | |||
| b349d22370 | |||
| 8605932702 | |||
| 10b8ce8940 | |||
| 445a66ea90 | |||
| 46541cb35e | |||
| 8224d11207 | |||
| 15064204d8 | |||
| 67dee382a6 | |||
| f1da640a5b | |||
| 2fd20d9002 | |||
| 31abcc97cf | |||
| 53fe00ed58 | |||
| 4a636b2b11 | |||
| 72bc8d3839 | |||
| 5578e8b86e | |||
| 3b99099b52 | |||
| cd6750154c | |||
| b8741bb1f7 | |||
| 125ae6ad0b | |||
| 015fb47d90 | |||
| 8a25ebb45b | |||
| 1887e481d2 | |||
| 342bb9acdc | |||
| 2af56e606a | |||
| 4275009dea | |||
| f520c67c89 | |||
| c3c7484792 | |||
| 4cd099e087 | |||
| d528746cb5 | |||
|
|
5cbad96492 | ||
| 226ee4e34e | |||
|
|
e0d7ef1314 | ||
|
|
99384ad6f7 | ||
|
|
ef4c119f1f | ||
|
|
a7f4ccc959 | ||
|
|
c23dc22ce2 | ||
| bb0e958118 | |||
| b15d18b3a5 | |||
| 5646850df1 | |||
| 30ec912162 | |||
| 28d4e507eb | |||
| 62cd92e1cb | |||
| 12ddf40ef4 | |||
| 7a4b27510c | |||
| 05d20ed6ee | |||
| f60de818f2 | |||
| 45fe5b2156 | |||
| e49312e63e | |||
| a11ccb2e39 | |||
| fde459c3ff | |||
| 694c54a6bc | |||
| 1dae3c713a | |||
| 89772ebce0 | |||
| aa81aa4e96 | |||
| 3c7ed176c0 | |||
| 6dda142188 | |||
| dcb44ca3f2 | |||
| 0a576410c7 | |||
| 128afb5914 | |||
| 39b7b1ae2f | |||
| 09462d0d90 | |||
| b8f4560780 | |||
| 281d81acc4 | |||
| 29817ba1c1 | |||
| b517cac4cf | |||
| f81349bbfd | |||
| ce012b7017 | |||
|
|
e3b6c3b85e | ||
| 39056cf358 | |||
| f16dedb320 | |||
| 171297b581 | |||
| 3267c3e2e1 | |||
| aad777058e | |||
| f633a3effe | |||
| bbfecdfced | |||
| 94ff951b2e | |||
| a5479d7b0d | |||
| 0a3744577d | |||
| 67cb3caa95 | |||
| 2265e1a096 | |||
| b6945cf81c | |||
| e8809b77d2 | |||
| 9d446cbd14 | |||
| cde4ee05f7 | |||
| ac0cf729f1 | |||
| 35e0890563 | |||
| 58c349eb2c | |||
| bcd57e61ea | |||
| 0be6ebcd4b | |||
| b4218478bd | |||
| 6ac9fc4857 | |||
| 8a96f7bee9 | |||
| 7791f24423 | |||
| b809451be2 | |||
| dbcc7c664f | |||
| 8de31d784b | |||
| f4216af7c7 | |||
| cf8e1cffc5 | |||
| 97a1385903 | |||
| 764e6f070b | |||
| 6d8dca211d | |||
| 1c21231f31 | |||
| 2a3cd07c63 | |||
| b5d5a67b2d | |||
| 4c11c5e215 | |||
| a0c3f6d2b3 | |||
| 7cf73fb84a | |||
| 6cd299ab60 | |||
| fc14c76b6d | |||
| 24eb9a6911 | |||
| 38fd9e5091 | |||
| a7d7013639 | |||
| 9dc385a32a | |||
| 150d069dfb | |||
| f160411f71 | |||
| b0678ceb84 | |||
| 57275f5735 | |||
| ec512fc540 | |||
| 7bc37617b0 | |||
| 1858a045cc | |||
| 1b108428c2 | |||
| 08a7701238 | |||
| be9492c151 | |||
| 679f50b730 | |||
| 2334bc502a | |||
| c3f2c89c7c | |||
| 920506c702 | |||
| aefc0bb534 | |||
| 52b3bfa945 | |||
| c8c9112b0f | |||
| 7a48dc2cef | |||
| 0efee0cb83 | |||
| 3301fb87c2 | |||
| 3f2b18cae8 | |||
| e9cea5d010 | |||
|
|
f2c44a1108 | ||
|
|
f15ebd7c02 | ||
| 91b550754f | |||
| abf810209e | |||
| 358499e6d5 | |||
| 5fae67255b | |||
| 2c3d61495f | |||
| a05821620d | |||
| 26668c81b1 | |||
| 663e5e7207 | |||
| 6ad979a5eb | |||
| 09e3b082c1 | |||
| ff6460b92e | |||
| d028afd09e | |||
| bd2eff83b7 | |||
| 645c18c981 | |||
| 1d13d56dce | |||
| 277d55d521 | |||
| d705d351c0 | |||
| 9ff8a3a02b | |||
| 1d18305870 | |||
| 009ab08821 | |||
| 313c693d48 | |||
| c9801ee2f7 | |||
| a089efff1a | |||
| d4b6283e23 | |||
| a3236cd67a | |||
| 274836e39a | |||
| 707131023a | |||
| 57c460fc9c | |||
| cd0dbc4cc2 | |||
| d59f629dd9 | |||
| 1e29061bc9 | |||
| e03d803ae0 | |||
| f47aa8c478 | |||
| 6fc6561186 | |||
| ea8656ce0d | |||
| 0ba763f8b1 | |||
| 43c42e1397 | |||
| 926648517f | |||
| 31d93734a6 | |||
| e83c4091bf | |||
| 7ae7e381c3 | |||
| f27347f028 | |||
| 38412c1c16 | |||
| 2ebd86b80f | |||
| 0f4a904a77 | |||
| 36cfdd8861 | |||
| 11bdf8d0a1 | |||
| 00fa139e54 | |||
| 1ef54426bc | |||
| 6aef54910e | |||
| a4e6e4ce84 | |||
| c4a7df7a6f | |||
| 3a1ce6c9e8 | |||
| c06fb69c8b | |||
| f39a0eac56 | |||
| 49d7e4ced6 | |||
| de2e1d6216 | |||
| ea9829b341 | |||
| 70b52d5567 | |||
| 8ff0b626a2 | |||
| 979f1d0c55 | |||
| 9790954dfc | |||
| c6aa38147b | |||
| 8b4f08c5bd | |||
| ac33ceb579 | |||
| 9935e038fc | |||
| f496c31d1c | |||
|
|
c6e1e9acb2 | ||
|
|
1e36846265 | ||
|
|
04d5be04fa | ||
|
|
3cb9a54cee | ||
|
|
497263eaf7 | ||
| e4d67ec345 | |||
| 2fdef0afe4 | |||
| c560e13f24 | |||
| 92530ef1b2 | |||
| 59ea2e971b | |||
| 2b96c32063 | |||
|
|
aca073faff | ||
|
|
7ce9b2bb4c | ||
| 5141a0dc17 | |||
| 56c43179f3 | |||
| 089823d884 | |||
| 7400957ac2 | |||
| 60f7c6eea7 | |||
| 4af108265b | |||
| a4fd04c310 | |||
| f9f54989fe | |||
| f9ee107403 | |||
| 6c244cffa0 | |||
| c94d9743dd | |||
| 5e95f50fb6 | |||
| e925c47961 | |||
| aee8545e65 | |||
| 39b7ecdaa4 | |||
| a1e7a7cff8 | |||
| 981f6cc66c | |||
| dbca402fe7 | |||
| 3004c01db4 | |||
| c0ce0ca263 | |||
| 2f1f573af2 | |||
| ffc8fe40c3 | |||
| fd99deed1d | |||
| 55e6550cb1 | |||
| 76ec0d26b4 | |||
| 68e357d037 | |||
| aa4050f6cd | |||
| 2cd8f70cdc | |||
| eb95480c8f | |||
| 20105e7d98 | |||
| 7102e08000 | |||
| 04dcf07fb2 | |||
| fd8567c60c | |||
| db70974504 | |||
|
|
ece42ffe47 | ||
|
|
c55e66dd70 | ||
| 684806baaf | |||
| ff2911dbd3 | |||
| a6a10b78d1 | |||
|
|
e1310516fa | ||
|
|
461c62f596 | ||
| 57ba0d5db9 | |||
| c812fd8c16 | |||
| 7970fca93a | |||
| 080ab9a626 | |||
| ff605756ff | |||
| 59aff52ce1 | |||
| 283b0d006e | |||
| b66d7d30ed | |||
| f66ed07496 | |||
| ae7526dd96 | |||
| dda78df9d2 | |||
| 3cfbfd96b0 | |||
| ee1910806c | |||
| a4f4bb799c | |||
| 471feca8fb | |||
| bbf5acafbb | |||
| 7a1ad6430c | |||
| 4cb8b0f1a6 | |||
| 4f7d89a3a1 | |||
| be776405e3 | |||
| 3c51b1f03b | |||
| ecd9457691 | |||
| 35ba5c03c9 | |||
|
|
8018369800 | ||
| d74a9067c0 | |||
| d5f07ec338 | |||
|
|
e915c4930c | ||
| 760da8ef61 | |||
| 9b2bc27374 | |||
| 8988dd0d41 | |||
| 9fa8902f1a | |||
| beeb5573e1 | |||
| ac3ed0d492 | |||
| 9686f36522 | |||
| ec9481e65b | |||
| e837f9c8e5 | |||
| 969210a723 | |||
| 2b0593a51e | |||
| 6147eef19b | |||
| 67cd66b922 | |||
|
|
710896f711 | ||
|
|
5b039edb62 | ||
| d1c28fc4a3 | |||
| c27540eb87 | |||
| 88a8e0fe59 | |||
| 0208a5d552 | |||
| a00c354287 | |||
| d269468287 | |||
| a1ac7d480d | |||
| 3c15d35fca | |||
| 4d51bc1fda | |||
| 4bc8bc3c12 | |||
|
|
b5e4acdb70 | ||
|
|
6e226f0fb3 | ||
| b75c54419f | |||
| 787a5fd3da | |||
| 000c67e45e | |||
| ae4a303554 | |||
| 6415e4a697 | |||
| 3b8195b81b | |||
|
|
dc681fdc35 | ||
|
|
3cad591086 | ||
|
|
ea05b3014c | ||
| 0c960e984a | |||
| 487cb13e14 | |||
| d90c44de49 | |||
| b44464d255 | |||
| 26515677b8 | |||
| 92895a7b1d | |||
|
|
0e76a6ed2a | ||
| c7706bfc97 | |||
| ab2eb405ca | |||
|
|
8a7ca25d6f | ||
| 5f29fe8167 | |||
| 889b129da3 | |||
| ed6da06271 | |||
| cacf216e36 | |||
|
|
ef73516ceb | ||
|
|
73082e4109 | ||
| 3ac151f888 | |||
| 9cf4b9becb | |||
| c0e6b26b0c | |||
| 859b32abb7 | |||
| 381cf13432 | |||
|
|
f786dd1d43 | ||
| f4a80e0fda | |||
| c86031ea32 | |||
| d95de8c195 | |||
| 2e55ba5671 | |||
| c1858fff3a | |||
| 91688875d4 | |||
| 19ad2d7a32 | |||
| 04023e945e | |||
| 8d91ad31fb | |||
| 889e376254 | |||
| 63a6654331 | |||
|
|
b3274c0dc7 | ||
| fc500bc853 | |||
| 4be9f78104 | |||
| 500e3a6e01 | |||
| fc1bc135df | |||
|
|
481b1974c3 | ||
|
|
9120cf56c2 | ||
|
|
7d051f7b35 | ||
| 9c78e9df1d | |||
| 65b5f6b056 | |||
| 40ff3d6eda | |||
| 0fb58f0ff2 | |||
| 0f2f14ddda | |||
| f19dd81a0d | |||
| 2644d1bc02 | |||
| 8bcceb641f | |||
| 48ebc1b1f5 | |||
| 002f2463a3 | |||
| c8d495d508 | |||
| 57bbca4e7a | |||
| 1b9395ca37 | |||
| 418ff4f519 | |||
| 4d7d1ccab2 | |||
| 3d1a8ff2ba | |||
| c984493c79 | |||
| 06bc0a7693 | |||
| 806ff1d4a0 | |||
| e588c30044 | |||
| 2e7a4ad132 | |||
| dacb618069 | |||
| 46268cb2cf | |||
| 28005e5654 | |||
| 1f5364c387 | |||
| bafc14bd79 | |||
| f66d724d40 | |||
| e7fd7c5ec4 | |||
| 55b1eb5075 | |||
| e7d37991b3 | |||
| e225f3b8d7 | |||
| 2b9c810e88 | |||
| 38aea5dd37 | |||
| 06c85289e0 | |||
| 7c7b63634b | |||
| 5a6230d844 | |||
| 41f7dc2456 | |||
| 8aebeb6346 | |||
| fd6d9288f7 | |||
| 0d21b1fa2c | |||
| 1a04a107ac | |||
| 116c81f5b2 | |||
| 0d4130b391 | |||
| 17bbb000ad | |||
| 466ec31be7 | |||
| 192a26b5ea | |||
| 7805f27458 | |||
| 463faed697 | |||
| c691450111 | |||
| bf266dd21f | |||
| d14fec4cec | |||
| 66ec7cb7ca | |||
| 6b6ff0cb56 | |||
| 99106d26a9 | |||
| a7b166498c | |||
| 0b06261d18 | |||
| dd285b67d1 | |||
| a1c086a807 | |||
| 86fdaa4dd9 | |||
| 02acad5968 | |||
| 22a2ba76c4 | |||
| f575674d47 | |||
| 3265006adb | |||
| 65aa371fdc | |||
| d6ea5736a5 | |||
| 5dcf0d6961 | |||
| c75d85b88b | |||
| c7baf6ecbe | |||
| 52cf7b5ad7 | |||
| 48149fadc1 | |||
| f181d644b4 | |||
| bbd928c6fa | |||
| a418ca860a | |||
|
|
0dd6036808 | ||
|
|
58d330c333 | ||
| 23b60814b7 | |||
| 001ff35758 | |||
| fd5fbf6c6c | |||
| 63cc770800 | |||
| 093581f646 | |||
| 2dfe1f0e9a | |||
| e1aff6c4cf | |||
| 4f27232fd4 | |||
| b6c5bf4f10 | |||
| e17996d858 | |||
| 95deafe7af | |||
| 8dfd0f07cc | |||
| 745d2b0487 | |||
| 77b897a1e3 | |||
| 7c12f31d2c | |||
| fafa261811 | |||
| 0731803550 | |||
| 5e097b5415 | |||
| f8884a53ec | |||
| f927d5ab0a | |||
| 5e87843dda | |||
| 67f6d49fb8 | |||
| fe709e630f | |||
| bd92f64449 | |||
| 6c89f80bcf | |||
| 41c33354c3 | |||
| 4776fbe931 | |||
| 4dd837cf4b | |||
| f9ee1fe898 | |||
| dfde4c5f49 | |||
| 981025610e | |||
| 020759fdab | |||
| 49bfcdcae5 | |||
| 5d5030efe1 | |||
| ce9dec7ed4 | |||
| ada19a221c | |||
| 302add96ca | |||
| 1c1139df9f | |||
| 23c660ab57 | |||
| c5a69f1bd0 | |||
| 32cb79344b | |||
| 74bb0caa1b | |||
| 99a5e8e5ad | |||
| 41da1c0780 | |||
| 8f620b9756 | |||
| 5be1e97411 | |||
| cdaad47b13 | |||
| acded35e1a | |||
| 04eccbe250 | |||
| 0ab51d79ae | |||
| 314d410789 | |||
| a7830f709d | |||
| 7a5c2d9786 | |||
| b184b27d4f | |||
| 8b819f097d | |||
| 880b2950d3 | |||
| ae3c46e693 | |||
| dee6ec28fe | |||
| 4d187f61e3 | |||
| 772d68a34d | |||
| 5e202063d4 | |||
| a0a1ef8989 | |||
| 1beed6751b | |||
| edbfac2943 | |||
| 73acc00762 | |||
| fa81fa5814 | |||
| 7dc3b55c34 | |||
| db22436e5d | |||
| 8c52f75b6a | |||
| 877041bb12 | |||
| d83b0d1b81 | |||
| 4cbf73c45a | |||
| 4f19f08c9f | |||
| 88219a773a | |||
| 5f86b35cf0 | |||
| 1c847d11d6 | |||
| 28c86d461a | |||
| d32a0cdc15 | |||
| f51ad21e91 | |||
| 95db63bf47 | |||
| 9b9c02fe29 | |||
| eba4a07ed1 | |||
| 3ac40ac703 | |||
| cccee20cdf | |||
| ee14682c4f | |||
| 04bde60482 | |||
| c13173e62c | |||
| 76399a110f | |||
| 8f17c0a977 | |||
| 8efc92edde | |||
| b63170244a | |||
| 028b7fd88d | |||
| 85ec2dcd01 | |||
| c32f1579ee | |||
| b0e457ffc9 | |||
| 7387fabee1 | |||
| 3bc53bb4ef | |||
| 29819c7874 | |||
| 038590c659 | |||
| dcce36eb7c | |||
| 81593a493b | |||
| 28c1ad088b | |||
| 0a16321259 | |||
| 0e26450d8f | |||
| b4800643e1 | |||
| 8d1919a36b | |||
| a8fe4c5159 | |||
| 4b9a6305d4 | |||
| da32ee6490 | |||
| 039c578987 | |||
| 97143a0182 | |||
| fdd4847f71 | |||
| eae0adbb43 | |||
| cd7843e16e | |||
| e5ec487d29 | |||
| 84d3ee262c | |||
| 0e5562fa01 | |||
| 2100afed66 | |||
| 82156543aa | |||
| 5559ae20d9 | |||
|
|
17c29e386c | ||
| 0a96627d6a | |||
| 46c8048b53 | |||
| d0b1336d07 | |||
| 3839455f42 | |||
| 85981b4d99 | |||
| 94a9a9a30b | |||
| ef50f6a1c9 | |||
| fe0f120038 | |||
| 23bc61cce0 | |||
| 1464f92c87 | |||
| 582da6746f | |||
| ccff1c8b1e | |||
| 3ae01da380 | |||
| 7aaa65d4a6 | |||
| 22b6392cc8 | |||
| 1e139b3afa | |||
| 99c6a5c271 | |||
| c15127feb8 | |||
| d985b71373 | |||
| 7a55840aeb | |||
| b34a73cea6 | |||
| eccf4ddf7a | |||
| 5bec50744c | |||
| 495c1f0efa | |||
| 1a3912cc4f | |||
| d16f57f8d5 | |||
| 9b010544b5 | |||
| d575c0d6d3 | |||
| 6976846e23 | |||
|
|
be80e84323 | ||
| 411d8cdc41 | |||
| 0036dcc285 | |||
|
|
9eb3357148 | ||
|
|
cd16bf8de9 | ||
|
|
fee11aca42 | ||
| fabdbc7c47 | |||
| 82198160fd | |||
| ba1b5774bb | |||
| a8ce37a372 | |||
| e5741ce1cb | |||
|
|
fde217dfde | ||
| 5555a71ecc | |||
| 0f01d28528 | |||
|
|
f6d4600df3 | ||
| 509e85f55a | |||
| 85edb9489d | |||
| de85344b84 | |||
| 75def22e57 | |||
| 4e1fc0bca1 | |||
| 95ceeeaec9 | |||
| fa77a3b323 | |||
| 63f24c7b59 | |||
| 971e3c882d | |||
| bead2e31b2 | |||
| 3e014d4ebe | |||
| c5ee6084c0 | |||
| 05a148fef4 | |||
| 7575c41645 | |||
| 31fe146c6a | |||
| f4edaa3c38 | |||
| 6755b88229 | |||
| f97bd593af | |||
| f8dbb7d2e1 | |||
| be1beec980 | |||
| 8a3037d288 | |||
| 0cceb8226d | |||
| 02d0d91df9 | |||
| 34188c71a5 | |||
| d549d91aca | |||
| 14e2c59064 | |||
| da84a493ed | |||
| f8999d1e7f | |||
| aa285b1393 | |||
| a0fca91d06 | |||
| 818d8b754c | |||
| 471e40aed1 | |||
| d56f873fd4 | |||
| b925cee08a | |||
| 26502abe35 | |||
| d38ebd372c | |||
| 08f3a31e88 | |||
| eefcf96516 |
192 changed files with 13174 additions and 7465 deletions
26
.drone.yml
Normal file
26
.drone.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default-arm64
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: python:3.11-alpine
|
||||||
|
commands:
|
||||||
|
- pip install --no-cache-dir -r requirements.txt
|
||||||
|
- pip install .
|
||||||
|
|
||||||
|
- name: docker
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
repo: nemunaire/nemubot
|
||||||
|
auto_tag: true
|
||||||
|
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
*#
|
*#
|
||||||
*~
|
*~
|
||||||
|
*.log
|
||||||
TAGS
|
TAGS
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
[submodule "modules/nextstop/external"]
|
|
||||||
path = modules/nextstop/external
|
|
||||||
url = git://github.com/nbr23/NextStop.git
|
|
||||||
12
.travis.yml
Normal file
12
.travis.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- 3.4
|
||||||
|
- 3.5
|
||||||
|
- 3.6
|
||||||
|
- 3.7
|
||||||
|
- nightly
|
||||||
|
install:
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- pip install .
|
||||||
|
script: nosetests -w nemubot
|
||||||
|
sudo: false
|
||||||
241
DCC.py
241
DCC.py
|
|
@ -1,241 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import imp
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import message
|
|
||||||
import server
|
|
||||||
|
|
||||||
#Store all used ports
|
|
||||||
PORTS = list()
|
|
||||||
|
|
||||||
class DCC(server.Server):
|
|
||||||
def __init__(self, srv, dest, socket=None):
|
|
||||||
server.Server.__init__(self)
|
|
||||||
|
|
||||||
self.error = False # An error has occur, closing the connection?
|
|
||||||
self.messages = list() # Message queued before connexion
|
|
||||||
|
|
||||||
# Informations about the sender
|
|
||||||
self.sender = dest
|
|
||||||
if self.sender is not None:
|
|
||||||
self.nick = (self.sender.split('!'))[0]
|
|
||||||
if self.nick != self.sender:
|
|
||||||
self.realname = (self.sender.split('!'))[1]
|
|
||||||
else:
|
|
||||||
self.realname = self.nick
|
|
||||||
|
|
||||||
# Keep the server
|
|
||||||
self.srv = srv
|
|
||||||
self.treatement = self.treat_msg
|
|
||||||
|
|
||||||
# Found a port for the connection
|
|
||||||
self.port = self.foundPort()
|
|
||||||
|
|
||||||
if self.port is None:
|
|
||||||
print ("No more available slot for DCC connection")
|
|
||||||
self.setError("Il n'y a plus de place disponible sur le serveur"
|
|
||||||
" pour initialiser une session DCC.")
|
|
||||||
|
|
||||||
def foundPort(self):
|
|
||||||
"""Found a free port for the connection"""
|
|
||||||
for p in range(65432, 65535):
|
|
||||||
if p not in PORTS:
|
|
||||||
PORTS.append(p)
|
|
||||||
return p
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self):
|
|
||||||
"""Gives the server identifiant"""
|
|
||||||
return self.srv.id + "/" + self.sender
|
|
||||||
|
|
||||||
def setError(self, msg):
|
|
||||||
self.error = True
|
|
||||||
self.srv.send_msg_usr(self.sender, msg)
|
|
||||||
|
|
||||||
def accept_user(self, host, port):
|
|
||||||
"""Accept a DCC connection"""
|
|
||||||
self.s = socket.socket()
|
|
||||||
try:
|
|
||||||
self.s.connect((host, port))
|
|
||||||
print ('Accepted user from', host, port, "for", self.sender)
|
|
||||||
self.connected = True
|
|
||||||
self.stop = False
|
|
||||||
except:
|
|
||||||
self.connected = False
|
|
||||||
self.error = True
|
|
||||||
return False
|
|
||||||
self.start()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def request_user(self, type="CHAT", filename="CHAT", size=""):
|
|
||||||
"""Create a DCC connection"""
|
|
||||||
#Open the port
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
try:
|
|
||||||
s.bind(('', self.port))
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
self.port = self.foundPort()
|
|
||||||
s.bind(('', self.port))
|
|
||||||
except:
|
|
||||||
self.setError("Une erreur s'est produite durant la tentative"
|
|
||||||
" d'ouverture d'une session DCC.")
|
|
||||||
return False
|
|
||||||
print ('Listen on', self.port, "for", self.sender)
|
|
||||||
|
|
||||||
#Send CTCP request for DCC
|
|
||||||
self.srv.send_ctcp(self.sender,
|
|
||||||
"DCC %s %s %d %d %s" % (type, filename, self.srv.ip,
|
|
||||||
self.port, size),
|
|
||||||
"PRIVMSG")
|
|
||||||
|
|
||||||
s.listen(1)
|
|
||||||
#Waiting for the client
|
|
||||||
(self.s, addr) = s.accept()
|
|
||||||
print ('Connected by', addr)
|
|
||||||
self.connected = True
|
|
||||||
return True
|
|
||||||
|
|
||||||
def send_dcc_raw(self, line):
|
|
||||||
self.s.sendall(line + b'\n')
|
|
||||||
|
|
||||||
def send_dcc(self, msg, to = None):
|
|
||||||
"""If we talk to this user, send a message through this connection
|
|
||||||
else, send the message to the server class"""
|
|
||||||
if to is None or to == self.sender or to == self.nick:
|
|
||||||
if self.error:
|
|
||||||
self.srv.send_msg_final(self.nick, msg)
|
|
||||||
elif not self.connected or self.s is None:
|
|
||||||
try:
|
|
||||||
self.start()
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
self.messages.append(msg)
|
|
||||||
else:
|
|
||||||
for line in msg.split("\n"):
|
|
||||||
self.send_dcc_raw(line.encode())
|
|
||||||
else:
|
|
||||||
self.srv.send_dcc(msg, to)
|
|
||||||
|
|
||||||
def send_file(self, filename):
|
|
||||||
"""Send a file over DCC"""
|
|
||||||
if os.path.isfile(filename):
|
|
||||||
self.messages = filename
|
|
||||||
try:
|
|
||||||
self.start()
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
print("File not found `%s'" % filename)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.stopping.clear()
|
|
||||||
|
|
||||||
# Send file connection
|
|
||||||
if not isinstance(self.messages, list):
|
|
||||||
self.request_user("SEND",
|
|
||||||
os.path.basename(self.messages),
|
|
||||||
os.path.getsize(self.messages))
|
|
||||||
if self.connected:
|
|
||||||
with open(self.messages, 'rb') as f:
|
|
||||||
d = f.read(268435456) #Packets size: 256Mo
|
|
||||||
while d:
|
|
||||||
self.s.sendall(d)
|
|
||||||
self.s.recv(4) #The client send a confirmation after each packet
|
|
||||||
d = f.read(268435456) #Packets size: 256Mo
|
|
||||||
|
|
||||||
# Messages connection
|
|
||||||
else:
|
|
||||||
if not self.connected:
|
|
||||||
if not self.request_user():
|
|
||||||
#TODO: do something here
|
|
||||||
return False
|
|
||||||
|
|
||||||
#Start by sending all queued messages
|
|
||||||
for mess in self.messages:
|
|
||||||
self.send_dcc(mess)
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
readbuffer = b''
|
|
||||||
self.nicksize = len(self.srv.nick)
|
|
||||||
self.Bnick = self.srv.nick.encode()
|
|
||||||
while not self.stop:
|
|
||||||
raw = self.s.recv(1024) #recieve server messages
|
|
||||||
if not raw:
|
|
||||||
break
|
|
||||||
readbuffer = readbuffer + raw
|
|
||||||
temp = readbuffer.split(b'\n')
|
|
||||||
readbuffer = temp.pop()
|
|
||||||
|
|
||||||
for line in temp:
|
|
||||||
self.treatement(line)
|
|
||||||
|
|
||||||
if self.connected:
|
|
||||||
self.s.close()
|
|
||||||
self.connected = False
|
|
||||||
|
|
||||||
#Remove from DCC connections server list
|
|
||||||
if self.realname in self.srv.dcc_clients:
|
|
||||||
del self.srv.dcc_clients[self.realname]
|
|
||||||
|
|
||||||
print ("Closing connection with", self.nick)
|
|
||||||
self.stopping.set()
|
|
||||||
if self.closing_event is not None:
|
|
||||||
self.closing_event()
|
|
||||||
#Rearm Thread
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
|
|
||||||
def treat_msg(self, line):
|
|
||||||
"""Treat a receive message, *can be overwritten*"""
|
|
||||||
if line == b'NEMUBOT###':
|
|
||||||
bot = self.srv.add_networkbot(self.srv, self.sender, self)
|
|
||||||
self.treatement = bot.treat_msg
|
|
||||||
self.send_dcc("NEMUBOT###")
|
|
||||||
elif (line[:self.nicksize] == self.Bnick and
|
|
||||||
line[self.nicksize+1:].strip()[:10] == b'my name is'):
|
|
||||||
name = line[self.nicksize+1:].strip()[11:].decode('utf-8',
|
|
||||||
'replace')
|
|
||||||
if re.match("^[a-zA-Z0-9_-]+$", name):
|
|
||||||
if name not in self.srv.dcc_clients:
|
|
||||||
del self.srv.dcc_clients[self.sender]
|
|
||||||
self.nick = name
|
|
||||||
self.sender = self.nick + "!" + self.realname
|
|
||||||
self.srv.dcc_clients[self.realname] = self
|
|
||||||
self.send_dcc("Hi " + self.nick)
|
|
||||||
else:
|
|
||||||
self.send_dcc("This nickname is already in use"
|
|
||||||
", please choose another one.")
|
|
||||||
else:
|
|
||||||
self.send_dcc("The name you entered contain"
|
|
||||||
" invalid char.")
|
|
||||||
else:
|
|
||||||
self.srv.treat_msg(
|
|
||||||
(":%s PRIVMSG %s :" % (
|
|
||||||
self.sender,self.srv.nick)).encode() + line,
|
|
||||||
True)
|
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY requirements.txt /usr/src/app/
|
||||||
|
RUN apk add --no-cache bash build-base capstone-dev mandoc-doc man-db w3m youtube-dl aspell aspell-fr py3-matrix-nio && \
|
||||||
|
pip install --no-cache-dir --ignore-installed -r requirements.txt && \
|
||||||
|
pip install bs4 capstone dnspython openai && \
|
||||||
|
apk del build-base capstone-dev && \
|
||||||
|
ln -s /var/lib/nemubot/home /home/nemubot
|
||||||
|
|
||||||
|
VOLUME /var/lib/nemubot
|
||||||
|
|
||||||
|
COPY . /usr/src/app/
|
||||||
|
|
||||||
|
RUN ./setup.py install
|
||||||
|
|
||||||
|
WORKDIR /var/lib/nemubot
|
||||||
|
USER guest
|
||||||
|
ENTRYPOINT [ "python", "-m", "nemubot", "-d", "-P", "", "-M", "/usr/src/app/modules" ]
|
||||||
|
CMD [ "-D", "/var/lib/nemubot" ]
|
||||||
290
IRCServer.py
290
IRCServer.py
|
|
@ -1,290 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from channel import Channel
|
|
||||||
from DCC import DCC
|
|
||||||
from hooks import Hook
|
|
||||||
import message
|
|
||||||
import server
|
|
||||||
import xmlparser
|
|
||||||
|
|
||||||
class IRCServer(server.Server):
|
|
||||||
"""Class to interact with an IRC server"""
|
|
||||||
|
|
||||||
def __init__(self, node, nick, owner, realname):
|
|
||||||
"""Initialize an IRC server
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
node -- server node from XML configuration
|
|
||||||
nick -- nick used by the bot on this server
|
|
||||||
owner -- nick used by the bot owner on this server
|
|
||||||
realname -- string used as realname on this server
|
|
||||||
|
|
||||||
"""
|
|
||||||
server.Server.__init__(self)
|
|
||||||
|
|
||||||
self.node = node
|
|
||||||
|
|
||||||
self.nick = nick
|
|
||||||
self.owner = owner
|
|
||||||
self.realname = realname
|
|
||||||
|
|
||||||
# Listen private messages?
|
|
||||||
self.listen_nick = True
|
|
||||||
|
|
||||||
self.dcc_clients = dict()
|
|
||||||
|
|
||||||
self.channels = dict()
|
|
||||||
for chn in self.node.getNodes("channel"):
|
|
||||||
chan = Channel(chn["name"], chn["password"])
|
|
||||||
self.channels[chan.name] = chan
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def host(self):
|
|
||||||
"""Return the server hostname"""
|
|
||||||
if self.node is not None and self.node.hasAttribute("server"):
|
|
||||||
return self.node["server"]
|
|
||||||
else:
|
|
||||||
return "localhost"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def port(self):
|
|
||||||
"""Return the connection port used on this server"""
|
|
||||||
if self.node is not None and self.node.hasAttribute("port"):
|
|
||||||
return self.node.getInt("port")
|
|
||||||
else:
|
|
||||||
return "6667"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def password(self):
|
|
||||||
"""Return the password used to connect to this server"""
|
|
||||||
if self.node is not None and self.node.hasAttribute("password"):
|
|
||||||
return self.node["password"]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def allow_all(self):
|
|
||||||
"""If True, treat message from all channels, not only listed one"""
|
|
||||||
return (self.node is not None and self.node.hasAttribute("allowall")
|
|
||||||
and self.node["allowall"] == "true")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def autoconnect(self):
|
|
||||||
"""Autoconnect the server when added"""
|
|
||||||
if self.node is not None and self.node.hasAttribute("autoconnect"):
|
|
||||||
value = self.node["autoconnect"].lower()
|
|
||||||
return value != "no" and value != "off" and value != "false"
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self):
|
|
||||||
"""Gives the server identifiant"""
|
|
||||||
return self.host + ":" + str(self.port)
|
|
||||||
|
|
||||||
def register_hooks(self):
|
|
||||||
self.add_hook(Hook(self.evt_channel, "JOIN"))
|
|
||||||
self.add_hook(Hook(self.evt_channel, "PART"))
|
|
||||||
self.add_hook(Hook(self.evt_server, "NICK"))
|
|
||||||
self.add_hook(Hook(self.evt_server, "QUIT"))
|
|
||||||
self.add_hook(Hook(self.evt_channel, "332"))
|
|
||||||
self.add_hook(Hook(self.evt_channel, "353"))
|
|
||||||
|
|
||||||
def evt_server(self, msg, srv):
|
|
||||||
for chan in self.channels:
|
|
||||||
self.channels[chan].treat(msg.cmd, msg)
|
|
||||||
|
|
||||||
def evt_channel(self, msg, srv):
|
|
||||||
if msg.channel is not None:
|
|
||||||
if msg.channel in self.channels:
|
|
||||||
self.channels[msg.channel].treat(msg.cmd, msg)
|
|
||||||
|
|
||||||
def accepted_channel(self, chan, sender=None):
|
|
||||||
"""Return True if the channel (or the user) is authorized"""
|
|
||||||
if self.allow_all:
|
|
||||||
return True
|
|
||||||
elif self.listen_nick:
|
|
||||||
return (chan in self.channels and (sender is None or sender in
|
|
||||||
self.channels[chan].people)
|
|
||||||
) or chan == self.nick
|
|
||||||
else:
|
|
||||||
return chan in self.channels and (sender is None or sender
|
|
||||||
in self.channels[chan].people)
|
|
||||||
|
|
||||||
def join(self, chan, password=None, force=False):
|
|
||||||
"""Join a channel"""
|
|
||||||
if force or (chan is not None and
|
|
||||||
self.connected and chan not in self.channels):
|
|
||||||
self.channels[chan] = Channel(chan, password)
|
|
||||||
if password is not None:
|
|
||||||
self.s.send(("JOIN %s %s\r\n" % (chan, password)).encode())
|
|
||||||
else:
|
|
||||||
self.s.send(("JOIN %s\r\n" % chan).encode())
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def leave(self, chan):
|
|
||||||
"""Leave a channel"""
|
|
||||||
if chan is not None and self.connected and chan in self.channels:
|
|
||||||
if isinstance(chan, list):
|
|
||||||
for c in chan:
|
|
||||||
self.leave(c)
|
|
||||||
else:
|
|
||||||
self.s.send(("PART %s\r\n" % self.channels[chan].name).encode())
|
|
||||||
del self.channels[chan]
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Main loop
|
|
||||||
def run(self):
|
|
||||||
if not self.connected:
|
|
||||||
self.s = socket.socket() #Create the socket
|
|
||||||
try:
|
|
||||||
self.s.connect((self.host, self.port)) #Connect to server
|
|
||||||
except socket.error as e:
|
|
||||||
self.s = None
|
|
||||||
print ("\033[1;31mError:\033[0m Unable to connect to %s:%d: %s"
|
|
||||||
% (self.host, self.port, os.strerror(e.errno)))
|
|
||||||
return
|
|
||||||
self.stopping.clear()
|
|
||||||
|
|
||||||
if self.password != None:
|
|
||||||
self.s.send(b"PASS " + self.password.encode () + b"\r\n")
|
|
||||||
self.s.send(("NICK %s\r\n" % self.nick).encode ())
|
|
||||||
self.s.send(("USER %s %s bla :%s\r\n" % (self.nick, self.host,
|
|
||||||
self.realname)).encode())
|
|
||||||
raw = self.s.recv(1024)
|
|
||||||
if not raw:
|
|
||||||
print ("Unable to connect to %s:%d" % (self.host, self.port))
|
|
||||||
return
|
|
||||||
self.connected = True
|
|
||||||
print ("Connection to %s:%d completed" % (self.host, self.port))
|
|
||||||
|
|
||||||
if len(self.channels) > 0:
|
|
||||||
for chn in self.channels.keys():
|
|
||||||
self.join(self.channels[chn].name,
|
|
||||||
self.channels[chn].password, force=True)
|
|
||||||
|
|
||||||
|
|
||||||
readbuffer = b'' #Here we store all the messages from server
|
|
||||||
try:
|
|
||||||
while not self.stop:
|
|
||||||
readbuffer = readbuffer + raw
|
|
||||||
temp = readbuffer.split(b'\n')
|
|
||||||
readbuffer = temp.pop()
|
|
||||||
|
|
||||||
for line in temp:
|
|
||||||
self.treat_msg(line)
|
|
||||||
raw = self.s.recv(1024) #recieve server messages
|
|
||||||
except socket.error:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if self.connected:
|
|
||||||
self.s.close()
|
|
||||||
self.connected = False
|
|
||||||
if self.closing_event is not None:
|
|
||||||
self.closing_event()
|
|
||||||
print ("Server `%s' successfully stopped." % self.id)
|
|
||||||
self.stopping.set()
|
|
||||||
# Rearm Thread
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
|
|
||||||
|
|
||||||
# Overwritted methods
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
"""Close the socket with the server and all DCC client connections"""
|
|
||||||
#Close all DCC connection
|
|
||||||
clts = [c for c in self.dcc_clients]
|
|
||||||
for clt in clts:
|
|
||||||
self.dcc_clients[clt].disconnect()
|
|
||||||
return server.Server.disconnect(self)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Abstract methods
|
|
||||||
|
|
||||||
def send_pong(self, cnt):
|
|
||||||
"""Send a PONG command to the server with argument cnt"""
|
|
||||||
self.s.send(("PONG %s\r\n" % cnt).encode())
|
|
||||||
|
|
||||||
def msg_treated(self, origin):
|
|
||||||
"""Do nothing; here for implement abstract class"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def send_dcc(self, msg, to):
|
|
||||||
"""Send a message through DCC connection"""
|
|
||||||
if msg is not None and to is not None:
|
|
||||||
realname = to.split("!")[1]
|
|
||||||
if realname not in self.dcc_clients.keys():
|
|
||||||
d = DCC(self, to)
|
|
||||||
self.dcc_clients[realname] = d
|
|
||||||
self.dcc_clients[realname].send_dcc(msg)
|
|
||||||
|
|
||||||
def send_msg_final(self, channel, line, cmd="PRIVMSG", endl="\r\n"):
|
|
||||||
"""Send a message without checks or format"""
|
|
||||||
#TODO: add something for post message treatment here
|
|
||||||
if channel == self.nick:
|
|
||||||
print ("\033[1;35mWarning:\033[0m Nemubot talks to himself: %s" % msg)
|
|
||||||
traceback.print_stack()
|
|
||||||
if line is not None and channel is not None:
|
|
||||||
if self.s is None:
|
|
||||||
print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, line))
|
|
||||||
traceback.print_stack()
|
|
||||||
elif len(line) < 442:
|
|
||||||
self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ())
|
|
||||||
else:
|
|
||||||
print ("\033[1;35mWarning:\033[0m Message truncated due to size (%d ; max : 442) : %s" % (len(line), line))
|
|
||||||
traceback.print_stack()
|
|
||||||
self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"...", endl)).encode ())
|
|
||||||
|
|
||||||
def send_msg_usr(self, user, msg):
|
|
||||||
"""Send a message to a user instead of a channel"""
|
|
||||||
if user is not None and user[0] != "#":
|
|
||||||
realname = user.split("!")[1]
|
|
||||||
if realname in self.dcc_clients or user in self.dcc_clients:
|
|
||||||
self.send_dcc(msg, user)
|
|
||||||
else:
|
|
||||||
for line in msg.split("\n"):
|
|
||||||
if line != "":
|
|
||||||
self.send_msg_final(user.split('!')[0], msg)
|
|
||||||
|
|
||||||
def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
|
|
||||||
"""Send a message to a channel"""
|
|
||||||
if self.accepted_channel(channel):
|
|
||||||
server.Server.send_msg(self, channel, msg, cmd, endl)
|
|
||||||
|
|
||||||
def send_msg_verified(self, sender, channel, msg, cmd = "PRIVMSG", endl = "\r\n"):
|
|
||||||
"""Send a message to a channel, only if the source user is on this channel too"""
|
|
||||||
if self.accepted_channel(channel, sender):
|
|
||||||
self.send_msg_final(channel, msg, cmd, endl)
|
|
||||||
|
|
||||||
def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"):
|
|
||||||
"""Send a message to all channels on this server"""
|
|
||||||
for channel in self.channels.keys():
|
|
||||||
self.send_msg(channel, msg, cmd, endl)
|
|
||||||
49
README.md
49
README.md
|
|
@ -1,7 +1,50 @@
|
||||||
# *nemubot*
|
nemubot
|
||||||
|
=======
|
||||||
|
|
||||||
An extremely modulable IRC bot, built around XML configuration files!
|
An extremely modulable IRC bot, built around XML configuration files!
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
*nemubot* requires at least Python 3.3 to work.
|
||||||
|
|
||||||
|
Some modules (like `cve`, `nextstop` or `laposte`) require the
|
||||||
|
[BeautifulSoup module](https://www.crummy.com/software/BeautifulSoup/),
|
||||||
|
but the core and framework has no dependency.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Use the `setup.py` file: `python setup.py install`.
|
||||||
|
|
||||||
|
### VirtualEnv setup
|
||||||
|
|
||||||
|
The easiest way to do this is through a virtualenv:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
virtualenv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
python setup.py install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a new configuration file
|
||||||
|
|
||||||
|
There is a sample configuration file, called `bot_sample.xml`. You can
|
||||||
|
create your own configuration file from it.
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Don't forget to activate your virtualenv in further terminals, if you
|
||||||
|
use it.
|
||||||
|
|
||||||
|
To launch the bot, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nemubot bot.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `bot.xml` is your configuration file.
|
||||||
|
|
|
||||||
24
bin/nemubot
Executable file
24
bin/nemubot
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Nemubot is a smart and modulable IM bot.
|
||||||
|
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from nemubot.__main__ import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
641
bot.py
641
bot.py
|
|
@ -1,641 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from datetime import timedelta
|
|
||||||
from queue import Queue
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
import consumer
|
|
||||||
import event
|
|
||||||
import hooks
|
|
||||||
from networkbot import NetworkBot
|
|
||||||
from IRCServer import IRCServer
|
|
||||||
from DCC import DCC
|
|
||||||
import response
|
|
||||||
|
|
||||||
ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
|
|
||||||
class Bot:
|
|
||||||
def __init__(self, ip, realname, mp=list()):
|
|
||||||
# Bot general informations
|
|
||||||
self.version = 3.3
|
|
||||||
self.version_txt = "3.3-dev"
|
|
||||||
|
|
||||||
# Save various informations
|
|
||||||
self.ip = ip
|
|
||||||
self.realname = realname
|
|
||||||
self.ctcp_capabilities = dict()
|
|
||||||
self.init_ctcp_capabilities()
|
|
||||||
|
|
||||||
# Keep global context: servers and modules
|
|
||||||
self.servers = dict()
|
|
||||||
self.modules = dict()
|
|
||||||
|
|
||||||
# Context paths
|
|
||||||
self.modules_path = mp
|
|
||||||
self.datas_path = './datas/'
|
|
||||||
|
|
||||||
# Events
|
|
||||||
self.events = list()
|
|
||||||
self.event_timer = None
|
|
||||||
|
|
||||||
# Own hooks
|
|
||||||
self.hooks = hooks.MessagesHook(self, self)
|
|
||||||
|
|
||||||
# Other known bots, making a bots network
|
|
||||||
self.network = dict()
|
|
||||||
self.hooks_cache = dict()
|
|
||||||
|
|
||||||
# Messages to be treated
|
|
||||||
self.cnsr_queue = Queue()
|
|
||||||
self.cnsr_thrd = list()
|
|
||||||
self.cnsr_thrd_size = -1
|
|
||||||
|
|
||||||
self.hooks.add_hook("irc_hook",
|
|
||||||
hooks.Hook(self.treat_prvmsg, "PRIVMSG"),
|
|
||||||
self)
|
|
||||||
|
|
||||||
|
|
||||||
def init_ctcp_capabilities(self):
|
|
||||||
"""Reset existing CTCP capabilities to default one"""
|
|
||||||
self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive")
|
|
||||||
self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo
|
|
||||||
self.ctcp_capabilities["DCC"] = self._ctcp_dcc
|
|
||||||
self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response(
|
|
||||||
msg.sender, "NEMUBOT %f" % self.version)
|
|
||||||
self.ctcp_capabilities["TIME"] = lambda srv, msg: _ctcp_response(
|
|
||||||
msg.sender, "TIME %s" % (datetime.now()))
|
|
||||||
self.ctcp_capabilities["USERINFO"] = lambda srv, msg: _ctcp_response(
|
|
||||||
msg.sender, "USERINFO %s" % self.realname)
|
|
||||||
self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response(
|
|
||||||
msg.sender, "VERSION nemubot v%s" % self.version_txt)
|
|
||||||
|
|
||||||
def _ctcp_clientinfo(self, srv, msg):
|
|
||||||
"""Response to CLIENTINFO CTCP message"""
|
|
||||||
return _ctcp_response(msg.sndr,
|
|
||||||
" ".join(self.ctcp_capabilities.keys()))
|
|
||||||
|
|
||||||
def _ctcp_dcc(self, srv, msg):
|
|
||||||
"""Response to DCC CTCP message"""
|
|
||||||
ip = srv.toIP(int(msg.cmds[3]))
|
|
||||||
conn = DCC(srv, msg.sender)
|
|
||||||
if conn.accept_user(ip, int(msg.cmds[4])):
|
|
||||||
srv.dcc_clients[conn.sender] = conn
|
|
||||||
conn.send_dcc("Hello %s!" % conn.nick)
|
|
||||||
else:
|
|
||||||
print ("DCC: unable to connect to %s:%s" % (ip, msg.cmds[4]))
|
|
||||||
|
|
||||||
|
|
||||||
def add_event(self, evt, eid=None, module_src=None):
|
|
||||||
"""Register an event and return its identifiant for futur update"""
|
|
||||||
if eid is None:
|
|
||||||
# Find an ID
|
|
||||||
now = datetime.now()
|
|
||||||
evt.id = "%d%c%d%d%c%d%d%c%d" % (now.year, ID_letters[now.microsecond % 52],
|
|
||||||
now.month, now.day, ID_letters[now.microsecond % 42],
|
|
||||||
now.hour, now.minute, ID_letters[now.microsecond % 32],
|
|
||||||
now.second)
|
|
||||||
else:
|
|
||||||
evt.id = eid
|
|
||||||
|
|
||||||
# Add the event in place
|
|
||||||
t = evt.current
|
|
||||||
i = -1
|
|
||||||
for i in range(0, len(self.events)):
|
|
||||||
if self.events[i].current > t:
|
|
||||||
i -= 1
|
|
||||||
break
|
|
||||||
self.events.insert(i + 1, evt)
|
|
||||||
if i == -1:
|
|
||||||
self.update_timer()
|
|
||||||
if len(self.events) <= 0 or self.events[i+1] != evt:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if module_src is not None:
|
|
||||||
module_src.REGISTERED_EVENTS.append(evt.id)
|
|
||||||
|
|
||||||
return evt.id
|
|
||||||
|
|
||||||
def del_event(self, id, module_src=None):
|
|
||||||
"""Find and remove an event from list"""
|
|
||||||
if len(self.events) > 0 and id == self.events[0].id:
|
|
||||||
self.events.remove(self.events[0])
|
|
||||||
self.update_timer()
|
|
||||||
if module_src is not None:
|
|
||||||
module_src.REGISTERED_EVENTS.remove(evt.id)
|
|
||||||
return True
|
|
||||||
|
|
||||||
for evt in self.events:
|
|
||||||
if evt.id == id:
|
|
||||||
self.events.remove(evt)
|
|
||||||
|
|
||||||
if module_src is not None:
|
|
||||||
module_src.REGISTERED_EVENTS.remove(evt.id)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def update_timer(self):
|
|
||||||
"""Relaunch the timer to end with the closest event"""
|
|
||||||
# Reset the timer if this is the first item
|
|
||||||
if self.event_timer is not None:
|
|
||||||
self.event_timer.cancel()
|
|
||||||
if len(self.events) > 0:
|
|
||||||
#print ("Update timer, next in", self.events[0].time_left.seconds,
|
|
||||||
# "seconds")
|
|
||||||
if datetime.now() + timedelta(seconds=5) >= self.events[0].current:
|
|
||||||
while datetime.now() < self.events[0].current:
|
|
||||||
time.sleep(0.6)
|
|
||||||
self.end_timer()
|
|
||||||
else:
|
|
||||||
self.event_timer = threading.Timer(
|
|
||||||
self.events[0].time_left.seconds + 1, self.end_timer)
|
|
||||||
self.event_timer.start()
|
|
||||||
#else:
|
|
||||||
# print ("Update timer: no timer left")
|
|
||||||
|
|
||||||
def end_timer(self):
|
|
||||||
"""Function called at the end of the timer"""
|
|
||||||
#print ("end timer")
|
|
||||||
while len(self.events)>0 and datetime.now() >= self.events[0].current:
|
|
||||||
#print ("end timer: while")
|
|
||||||
evt = self.events.pop(0)
|
|
||||||
self.cnsr_queue.put_nowait(consumer.EventConsumer(evt))
|
|
||||||
self.update_consumers()
|
|
||||||
|
|
||||||
self.update_timer()
|
|
||||||
|
|
||||||
|
|
||||||
def addServer(self, node, nick, owner, realname):
|
|
||||||
"""Add a new server to the context"""
|
|
||||||
srv = IRCServer(node, nick, owner, realname)
|
|
||||||
srv.add_hook = lambda h: self.hooks.add_hook("irc_hook", h, self)
|
|
||||||
srv.add_networkbot = self.add_networkbot
|
|
||||||
srv.send_bot = lambda d: self.send_networkbot(srv, d)
|
|
||||||
srv.register_hooks()
|
|
||||||
if srv.id not in self.servers:
|
|
||||||
self.servers[srv.id] = srv
|
|
||||||
if srv.autoconnect:
|
|
||||||
srv.launch(self.receive_message)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def add_module(self, module):
|
|
||||||
"""Add a module to the context, if already exists, unload the
|
|
||||||
old one before"""
|
|
||||||
# Check if the module already exists
|
|
||||||
for mod in self.modules.keys():
|
|
||||||
if self.modules[mod].name == module.name:
|
|
||||||
self.unload_module(self.modules[mod].name)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.modules[module.name] = module
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def add_modules_path(self, path):
|
|
||||||
"""Add a path to the modules_path array, used by module loader"""
|
|
||||||
# The path must end by / char
|
|
||||||
if path[len(path)-1] != "/":
|
|
||||||
path = path + "/"
|
|
||||||
|
|
||||||
if path not in self.modules_path:
|
|
||||||
self.modules_path.append(path)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def unload_module(self, name, verb=False):
|
|
||||||
"""Unload a module"""
|
|
||||||
if name in self.modules:
|
|
||||||
print (name)
|
|
||||||
self.modules[name].save()
|
|
||||||
if hasattr(self.modules[name], "unload"):
|
|
||||||
self.modules[name].unload(self)
|
|
||||||
# Remove registered hooks
|
|
||||||
for (s, h) in self.modules[name].REGISTERED_HOOKS:
|
|
||||||
self.hooks.del_hook(s, h)
|
|
||||||
# Remove registered events
|
|
||||||
for e in self.modules[name].REGISTERED_EVENTS:
|
|
||||||
self.del_event(e)
|
|
||||||
# Remove from the dict
|
|
||||||
del self.modules[name]
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def update_consumers(self):
|
|
||||||
"""Launch new consumer thread if necessary"""
|
|
||||||
if self.cnsr_queue.qsize() > self.cnsr_thrd_size:
|
|
||||||
c = consumer.Consumer(self)
|
|
||||||
self.cnsr_thrd.append(c)
|
|
||||||
c.start()
|
|
||||||
self.cnsr_thrd_size += 2
|
|
||||||
|
|
||||||
|
|
||||||
def receive_message(self, srv, raw_msg, private=False, data=None):
|
|
||||||
"""Queued the message for treatment"""
|
|
||||||
#print (raw_msg)
|
|
||||||
self.cnsr_queue.put_nowait(consumer.MessageConsumer(srv, raw_msg, datetime.now(), private, data))
|
|
||||||
|
|
||||||
# Launch a new thread if necessary
|
|
||||||
self.update_consumers()
|
|
||||||
|
|
||||||
|
|
||||||
def add_networkbot(self, srv, dest, dcc=None):
|
|
||||||
"""Append a new bot into the network"""
|
|
||||||
id = srv.id + "/" + dest
|
|
||||||
if id not in self.network:
|
|
||||||
self.network[id] = NetworkBot(self, srv, dest, dcc)
|
|
||||||
return self.network[id]
|
|
||||||
|
|
||||||
def send_networkbot(self, srv, cmd, data=None):
|
|
||||||
for bot in self.network:
|
|
||||||
if self.network[bot].srv == srv:
|
|
||||||
self.network[bot].send_cmd(cmd, data)
|
|
||||||
|
|
||||||
def quit(self, verb=False):
|
|
||||||
"""Save and unload modules and disconnect servers"""
|
|
||||||
if self.event_timer is not None:
|
|
||||||
if verb: print ("Stop the event timer...")
|
|
||||||
self.event_timer.cancel()
|
|
||||||
|
|
||||||
if verb: print ("Save and unload all modules...")
|
|
||||||
k = list(self.modules.keys())
|
|
||||||
for mod in k:
|
|
||||||
self.unload_module(mod, verb)
|
|
||||||
|
|
||||||
if verb: print ("Close all servers connection...")
|
|
||||||
k = list(self.servers.keys())
|
|
||||||
for srv in k:
|
|
||||||
self.servers[srv].disconnect()
|
|
||||||
|
|
||||||
# Hooks cache
|
|
||||||
|
|
||||||
def create_cache(self, name):
|
|
||||||
if name not in self.hooks_cache:
|
|
||||||
if isinstance(self.hooks.__dict__[name], list):
|
|
||||||
self.hooks_cache[name] = list()
|
|
||||||
|
|
||||||
# Start by adding locals hooks
|
|
||||||
for h in self.hooks.__dict__[name]:
|
|
||||||
tpl = (h, 0, self.hooks.__dict__[name], self.hooks.bot)
|
|
||||||
self.hooks_cache[name].append(tpl)
|
|
||||||
|
|
||||||
# Now, add extermal hooks
|
|
||||||
level = 0
|
|
||||||
while level == 0 or lvl_exist:
|
|
||||||
lvl_exist = False
|
|
||||||
for ext in self.network:
|
|
||||||
if len(self.network[ext].hooks) > level:
|
|
||||||
lvl_exist = True
|
|
||||||
for h in self.network[ext].hooks[level].__dict__[name]:
|
|
||||||
if h not in self.hooks_cache[name]:
|
|
||||||
self.hooks_cache[name].append((h, level + 1,
|
|
||||||
self.network[ext].hooks[level].__dict__[name], self.network[ext].hooks[level].bot))
|
|
||||||
level += 1
|
|
||||||
|
|
||||||
elif isinstance(self.hooks.__dict__[name], dict):
|
|
||||||
self.hooks_cache[name] = dict()
|
|
||||||
|
|
||||||
# Start by adding locals hooks
|
|
||||||
for h in self.hooks.__dict__[name]:
|
|
||||||
self.hooks_cache[name][h] = (self.hooks.__dict__[name][h], 0,
|
|
||||||
self.hooks.__dict__[name],
|
|
||||||
self.hooks.bot)
|
|
||||||
|
|
||||||
# Now, add extermal hooks
|
|
||||||
level = 0
|
|
||||||
while level == 0 or lvl_exist:
|
|
||||||
lvl_exist = False
|
|
||||||
for ext in self.network:
|
|
||||||
if len(self.network[ext].hooks) > level:
|
|
||||||
lvl_exist = True
|
|
||||||
for h in self.network[ext].hooks[level].__dict__[name]:
|
|
||||||
if h not in self.hooks_cache[name]:
|
|
||||||
self.hooks_cache[name][h] = (self.network[ext].hooks[level].__dict__[name][h], level + 1, self.network[ext].hooks[level].__dict__[name], self.network[ext].hooks[level].bot)
|
|
||||||
level += 1
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise Exception(name + " hook type unrecognized")
|
|
||||||
|
|
||||||
return self.hooks_cache[name]
|
|
||||||
|
|
||||||
# Treatment
|
|
||||||
|
|
||||||
def check_rest_times(self, store, hook):
|
|
||||||
"""Remove from store the hook if it has been executed given time"""
|
|
||||||
if hook.times == 0:
|
|
||||||
if isinstance(store, dict):
|
|
||||||
store[hook.name].remove(hook)
|
|
||||||
if len(store) == 0:
|
|
||||||
del store[hook.name]
|
|
||||||
elif isinstance(store, list):
|
|
||||||
store.remove(hook)
|
|
||||||
|
|
||||||
def treat_pre(self, msg, srv):
|
|
||||||
"""Treat a message before all other treatment"""
|
|
||||||
for h, lvl, store, bot in self.create_cache("all_pre"):
|
|
||||||
if h.is_matching(None, server=srv):
|
|
||||||
h.run(msg, self.create_cache)
|
|
||||||
self.check_rest_times(store, h)
|
|
||||||
|
|
||||||
|
|
||||||
def treat_post(self, res):
|
|
||||||
"""Treat a message before send"""
|
|
||||||
for h, lvl, store, bot in self.create_cache("all_post"):
|
|
||||||
if h.is_matching(None, channel=res.channel, server=res.server):
|
|
||||||
c = h.run(res)
|
|
||||||
self.check_rest_times(store, h)
|
|
||||||
if not c:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def treat_irc(self, msg, srv):
|
|
||||||
"""Treat all incoming IRC commands"""
|
|
||||||
treated = list()
|
|
||||||
|
|
||||||
irc_hooks = self.create_cache("irc_hook")
|
|
||||||
if msg.cmd in irc_hooks:
|
|
||||||
(hks, lvl, store, bot) = irc_hooks[msg.cmd]
|
|
||||||
for h in hks:
|
|
||||||
if h.is_matching(msg.cmd, server=srv):
|
|
||||||
res = h.run(msg, srv, msg.cmd)
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, h)
|
|
||||||
|
|
||||||
return treated
|
|
||||||
|
|
||||||
|
|
||||||
def treat_prvmsg_ask(self, msg, srv):
|
|
||||||
# Treat ping
|
|
||||||
if re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)",
|
|
||||||
msg.content, re.I) is not None:
|
|
||||||
return response.Response(msg.sender, message="pong",
|
|
||||||
channel=msg.channel, nick=msg.nick)
|
|
||||||
|
|
||||||
# Ask hooks
|
|
||||||
else:
|
|
||||||
return self.treat_ask(msg, srv)
|
|
||||||
|
|
||||||
def treat_prvmsg(self, msg, srv):
|
|
||||||
# First, treat CTCP
|
|
||||||
if msg.ctcp:
|
|
||||||
if msg.cmds[0] in self.ctcp_capabilities:
|
|
||||||
return self.ctcp_capabilities[msg.cmds[0]](srv, msg)
|
|
||||||
else:
|
|
||||||
return _ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request")
|
|
||||||
|
|
||||||
# Treat all messages starting with 'nemubot:' as distinct commands
|
|
||||||
elif msg.content.find("%s:"%srv.nick) == 0:
|
|
||||||
# Remove the bot name
|
|
||||||
msg.content = msg.content[len(srv.nick)+1:].strip()
|
|
||||||
|
|
||||||
return self.treat_prvmsg_ask(msg, srv)
|
|
||||||
|
|
||||||
# Owner commands
|
|
||||||
elif msg.content[0] == '`' and msg.nick == srv.owner:
|
|
||||||
#TODO: owner commands
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif msg.content[0] == '!' and len(msg.content) > 1:
|
|
||||||
# Remove the !
|
|
||||||
msg.cmds[0] = msg.cmds[0][1:]
|
|
||||||
|
|
||||||
if msg.cmds[0] == "help":
|
|
||||||
return _help_msg(msg.sender, self.modules, msg.cmds)
|
|
||||||
|
|
||||||
elif msg.cmds[0] == "more":
|
|
||||||
if msg.channel == srv.nick:
|
|
||||||
if msg.sender in srv.moremessages:
|
|
||||||
return srv.moremessages[msg.sender]
|
|
||||||
else:
|
|
||||||
if msg.channel in srv.moremessages:
|
|
||||||
return srv.moremessages[msg.channel]
|
|
||||||
|
|
||||||
elif msg.cmds[0] == "dcc":
|
|
||||||
print("dcctest for", msg.sender)
|
|
||||||
srv.send_dcc("Hello %s!" % msg.nick, msg.sender)
|
|
||||||
elif msg.cmds[0] == "pvdcctest":
|
|
||||||
print("dcctest")
|
|
||||||
return Response(msg.sender, message="Test DCC")
|
|
||||||
elif msg.cmds[0] == "dccsendtest":
|
|
||||||
print("dccsendtest")
|
|
||||||
conn = DCC(srv, msg.sender)
|
|
||||||
conn.send_file("bot_sample.xml")
|
|
||||||
|
|
||||||
else:
|
|
||||||
return self.treat_cmd(msg, srv)
|
|
||||||
|
|
||||||
else:
|
|
||||||
res = self.treat_answer(msg, srv)
|
|
||||||
# Assume the message starts with nemubot:
|
|
||||||
if (res is None or len(res) <= 0) and msg.private:
|
|
||||||
return self.treat_prvmsg_ask(msg, srv)
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def treat_cmd(self, msg, srv):
|
|
||||||
"""Treat a command message"""
|
|
||||||
treated = list()
|
|
||||||
|
|
||||||
# First, treat simple hook
|
|
||||||
cmd_hook = self.create_cache("cmd_hook")
|
|
||||||
if msg.cmds[0] in cmd_hook:
|
|
||||||
(hks, lvl, store, bot) = cmd_hook[msg.cmds[0]]
|
|
||||||
for h in hks:
|
|
||||||
if h.is_matching(msg.cmds[0], channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
|
|
||||||
res = h.run(msg, strcmp=msg.cmds[0])
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, h)
|
|
||||||
|
|
||||||
# Then, treat regexp based hook
|
|
||||||
cmd_rgxp = self.create_cache("cmd_rgxp")
|
|
||||||
for hook, lvl, store, bot in cmd_rgxp:
|
|
||||||
if hook.is_matching(msg.cmds[0], msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
|
|
||||||
res = hook.run(msg)
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, hook)
|
|
||||||
|
|
||||||
# Finally, treat default hooks if not catched before
|
|
||||||
cmd_default = self.create_cache("cmd_default")
|
|
||||||
for hook, lvl, store, bot in cmd_default:
|
|
||||||
if treated:
|
|
||||||
break
|
|
||||||
res = hook.run(msg)
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, hook)
|
|
||||||
|
|
||||||
return treated
|
|
||||||
|
|
||||||
def treat_ask(self, msg, srv):
|
|
||||||
"""Treat an ask message"""
|
|
||||||
treated = list()
|
|
||||||
|
|
||||||
# First, treat simple hook
|
|
||||||
ask_hook = self.create_cache("ask_hook")
|
|
||||||
if msg.content in ask_hook:
|
|
||||||
hks, lvl, store, bot = ask_hook[msg.content]
|
|
||||||
for h in hks:
|
|
||||||
if h.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
|
|
||||||
res = h.run(msg, strcmp=msg.content)
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, h)
|
|
||||||
|
|
||||||
# Then, treat regexp based hook
|
|
||||||
ask_rgxp = self.create_cache("ask_rgxp")
|
|
||||||
for hook, lvl, store, bot in ask_rgxp:
|
|
||||||
if hook.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
|
|
||||||
res = hook.run(msg, strcmp=msg.content)
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, hook)
|
|
||||||
|
|
||||||
# Finally, treat default hooks if not catched before
|
|
||||||
ask_default = self.create_cache("ask_default")
|
|
||||||
for hook, lvl, store, bot in ask_default:
|
|
||||||
if treated:
|
|
||||||
break
|
|
||||||
res = hook.run(msg)
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, hook)
|
|
||||||
|
|
||||||
return treated
|
|
||||||
|
|
||||||
def treat_answer(self, msg, srv):
|
|
||||||
"""Treat a normal message"""
|
|
||||||
treated = list()
|
|
||||||
|
|
||||||
# First, treat simple hook
|
|
||||||
msg_hook = self.create_cache("msg_hook")
|
|
||||||
if msg.content in msg_hook:
|
|
||||||
hks, lvl, store, bot = msg_hook[msg.content]
|
|
||||||
for h in hks:
|
|
||||||
if h.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
|
|
||||||
res = h.run(msg, strcmp=msg.content)
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, h)
|
|
||||||
|
|
||||||
# Then, treat regexp based hook
|
|
||||||
msg_rgxp = self.create_cache("msg_rgxp")
|
|
||||||
for hook, lvl, store, bot in msg_rgxp:
|
|
||||||
if hook.is_matching(msg.content, channel=msg.channel, server=srv) and (msg.private or lvl == 0 or bot.nick not in srv.channels[msg.channel].people):
|
|
||||||
res = hook.run(msg, strcmp=msg.content)
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, hook)
|
|
||||||
|
|
||||||
# Finally, treat default hooks if not catched before
|
|
||||||
msg_default = self.create_cache("msg_default")
|
|
||||||
for hook, lvl, store, bot in msg_default:
|
|
||||||
if len(treated) > 0:
|
|
||||||
break
|
|
||||||
res = hook.run(msg)
|
|
||||||
if res is not None and res != False:
|
|
||||||
treated.append(res)
|
|
||||||
self.check_rest_times(store, hook)
|
|
||||||
|
|
||||||
return treated
|
|
||||||
|
|
||||||
def _ctcp_response(sndr, msg):
|
|
||||||
return response.Response(sndr, msg, ctcp=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _help_msg(sndr, modules, cmd):
|
|
||||||
"""Parse and response to help messages"""
|
|
||||||
res = response.Response(sndr)
|
|
||||||
if len(cmd) > 1:
|
|
||||||
if cmd[1] in modules:
|
|
||||||
if len(cmd) > 2:
|
|
||||||
if hasattr(modules[cmd[1]], "HELP_cmd"):
|
|
||||||
res.append_message(modules[cmd[1]].HELP_cmd(cmd[2]))
|
|
||||||
else:
|
|
||||||
res.append_message("No help for command %s in module %s" % (cmd[2], cmd[1]))
|
|
||||||
elif hasattr(modules[cmd[1]], "help_full"):
|
|
||||||
res.append_message(modules[cmd[1]].help_full())
|
|
||||||
else:
|
|
||||||
res.append_message("No help for module %s" % cmd[1])
|
|
||||||
else:
|
|
||||||
res.append_message("No module named %s" % cmd[1])
|
|
||||||
else:
|
|
||||||
res.append_message("Pour me demander quelque chose, commencez "
|
|
||||||
"votre message par mon nom ; je réagis "
|
|
||||||
"également à certaine commandes commençant par"
|
|
||||||
" !. Pour plus d'informations, envoyez le "
|
|
||||||
"message \"!more\".")
|
|
||||||
res.append_message("Mon code source est libre, publié sous "
|
|
||||||
"licence AGPL (http://www.gnu.org/licenses/). "
|
|
||||||
"Vous pouvez le consulter, le dupliquer, "
|
|
||||||
"envoyer des rapports de bogues ou bien "
|
|
||||||
"contribuer au projet sur GitHub : "
|
|
||||||
"http://github.com/nemunaire/nemubot/")
|
|
||||||
res.append_message(title="Pour plus de détails sur un module, "
|
|
||||||
"envoyez \"!help nomdumodule\". Voici la liste"
|
|
||||||
" de tous les modules disponibles localement",
|
|
||||||
message=["\x03\x02%s\x03\x02 (%s)" % (im, modules[im].help_tiny ()) for im in modules if hasattr(modules[im], "help_tiny")])
|
|
||||||
return res
|
|
||||||
|
|
||||||
def hotswap(bak):
|
|
||||||
return Bot(bak.servers, bak.modules, bak.modules_path)
|
|
||||||
|
|
||||||
def reload():
|
|
||||||
import imp
|
|
||||||
|
|
||||||
import channel
|
|
||||||
imp.reload(channel)
|
|
||||||
|
|
||||||
import consumer
|
|
||||||
imp.reload(consumer)
|
|
||||||
|
|
||||||
import DCC
|
|
||||||
imp.reload(DCC)
|
|
||||||
|
|
||||||
import event
|
|
||||||
imp.reload(event)
|
|
||||||
|
|
||||||
import hooks
|
|
||||||
imp.reload(hooks)
|
|
||||||
|
|
||||||
import importer
|
|
||||||
imp.reload(importer)
|
|
||||||
|
|
||||||
import message
|
|
||||||
imp.reload(message)
|
|
||||||
|
|
||||||
import prompt.builtins
|
|
||||||
imp.reload(prompt.builtins)
|
|
||||||
|
|
||||||
import server
|
|
||||||
imp.reload(server)
|
|
||||||
|
|
||||||
import xmlparser
|
|
||||||
imp.reload(xmlparser)
|
|
||||||
import xmlparser.node
|
|
||||||
imp.reload(xmlparser.node)
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
<nemubotconfig nick="nemubot" realname="nemubot speaker" owner="someone">
|
<nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone">
|
||||||
<server server="irc.freenode.org" port="6667" password="secret" autoconnect="true">
|
|
||||||
|
<server uri="irc://irc.rezosup.org:6667" autoconnect="true" caps="znc.in/server-time-iso">
|
||||||
<channel name="#nemutest" />
|
<channel name="#nemutest" />
|
||||||
</server>
|
</server>
|
||||||
<load path="modules/birthday.xml" />
|
|
||||||
<load path="modules/ycc.xml" />
|
<!--
|
||||||
<load path="modules/qcm.xml" />
|
<server host="ircs://my_host.local:6667" password="secret" autoconnect="true">
|
||||||
<load path="modules/soutenance.xml" />
|
<channel name="#nemutest" />
|
||||||
<load path="modules/velib.xml" />
|
</server>
|
||||||
<load path="modules/whereis.xml" />
|
-->
|
||||||
<load path="modules/watchWebsite.xml" />
|
|
||||||
<load path="modules/events.xml" />
|
<!--
|
||||||
|
<module name="wolframalpha" apikey="YOUR-APIKEY" />
|
||||||
|
-->
|
||||||
|
|
||||||
|
<module name="cmd_server" />
|
||||||
|
|
||||||
|
<module name="alias" />
|
||||||
|
<module name="ycc" />
|
||||||
|
<module name="events" />
|
||||||
|
|
||||||
</nemubotconfig>
|
</nemubotconfig>
|
||||||
|
|
|
||||||
102
channel.py
102
channel.py
|
|
@ -1,102 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
class Channel:
|
|
||||||
def __init__(self, name, password=None):
|
|
||||||
self.name = name
|
|
||||||
self.password = password
|
|
||||||
self.people = dict()
|
|
||||||
self.topic = ""
|
|
||||||
|
|
||||||
def treat(self, cmd, msg):
|
|
||||||
if cmd == "353":
|
|
||||||
self.parse353(msg)
|
|
||||||
elif cmd == "332":
|
|
||||||
self.parse332(msg)
|
|
||||||
elif cmd == "MODE":
|
|
||||||
self.mode(msg)
|
|
||||||
elif cmd == "JOIN":
|
|
||||||
self.join(msg.nick)
|
|
||||||
elif cmd == "NICK":
|
|
||||||
self.nick(msg.nick, msg.content)
|
|
||||||
elif cmd == "PART" or cmd == "QUIT":
|
|
||||||
self.part(msg.nick)
|
|
||||||
elif cmd == "TOPIC":
|
|
||||||
self.topic = self.content
|
|
||||||
|
|
||||||
def join(self, nick, level = 0):
|
|
||||||
"""Someone join the channel"""
|
|
||||||
#print ("%s arrive sur %s" % (nick, self.name))
|
|
||||||
self.people[nick] = level
|
|
||||||
|
|
||||||
def chtopic(self, newtopic):
|
|
||||||
"""Send command to change the topic"""
|
|
||||||
self.srv.send_msg(self.name, newtopic, "TOPIC")
|
|
||||||
self.topic = newtopic
|
|
||||||
|
|
||||||
def nick(self, oldnick, newnick):
|
|
||||||
"""Someone change his nick"""
|
|
||||||
if oldnick in self.people:
|
|
||||||
#print ("%s change de nom pour %s sur %s" % (oldnick, newnick, self.name))
|
|
||||||
lvl = self.people[oldnick]
|
|
||||||
del self.people[oldnick]
|
|
||||||
self.people[newnick] = lvl
|
|
||||||
|
|
||||||
def part(self, nick):
|
|
||||||
"""Someone leave the channel"""
|
|
||||||
if nick in self.people:
|
|
||||||
#print ("%s vient de quitter %s" % (nick, self.name))
|
|
||||||
del self.people[nick]
|
|
||||||
|
|
||||||
def mode(self, msg):
|
|
||||||
if msg.content[0] == "-k":
|
|
||||||
self.password = ""
|
|
||||||
elif msg.content[0] == "+k":
|
|
||||||
if len(msg.content) > 1:
|
|
||||||
self.password = ' '.join(msg.content[1:])[1:]
|
|
||||||
else:
|
|
||||||
self.password = msg.content[1]
|
|
||||||
elif msg.content[0] == "+o":
|
|
||||||
self.people[msg.nick] |= 4
|
|
||||||
elif msg.content[0] == "-o":
|
|
||||||
self.people[msg.nick] &= ~4
|
|
||||||
elif msg.content[0] == "+h":
|
|
||||||
self.people[msg.nick] |= 2
|
|
||||||
elif msg.content[0] == "-h":
|
|
||||||
self.people[msg.nick] &= ~2
|
|
||||||
elif msg.content[0] == "+v":
|
|
||||||
self.people[msg.nick] |= 1
|
|
||||||
elif msg.content[0] == "-v":
|
|
||||||
self.people[msg.nick] &= ~1
|
|
||||||
|
|
||||||
def parse332(self, msg):
|
|
||||||
self.topic = msg.content
|
|
||||||
|
|
||||||
def parse353(self, msg):
|
|
||||||
for p in msg.content:
|
|
||||||
p = p.decode()
|
|
||||||
if p[0] == "@":
|
|
||||||
level = 4
|
|
||||||
elif p[0] == "%":
|
|
||||||
level = 2
|
|
||||||
elif p[0] == "+":
|
|
||||||
level = 1
|
|
||||||
else:
|
|
||||||
self.join(p, 0)
|
|
||||||
continue
|
|
||||||
self.join(p[1:], level)
|
|
||||||
143
consumer.py
143
consumer.py
|
|
@ -1,143 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import queue
|
|
||||||
import re
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import bot
|
|
||||||
from DCC import DCC
|
|
||||||
from message import Message
|
|
||||||
import response
|
|
||||||
import server
|
|
||||||
|
|
||||||
class MessageConsumer:
|
|
||||||
"""Store a message before treating"""
|
|
||||||
def __init__(self, srv, raw, time, prvt, data):
|
|
||||||
self.srv = srv
|
|
||||||
self.raw = raw
|
|
||||||
self.time = time
|
|
||||||
self.prvt = prvt
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
|
|
||||||
def treat_in(self, context, msg):
|
|
||||||
"""Treat the input message"""
|
|
||||||
if msg.cmd == "PING":
|
|
||||||
self.srv.send_pong(msg.content)
|
|
||||||
else:
|
|
||||||
# TODO: Manage credits
|
|
||||||
if msg.channel is None or self.srv.accepted_channel(msg.channel):
|
|
||||||
# All messages
|
|
||||||
context.treat_pre(msg, self.srv)
|
|
||||||
|
|
||||||
return context.treat_irc(msg, self.srv)
|
|
||||||
|
|
||||||
def treat_out(self, context, res):
|
|
||||||
"""Treat the output message"""
|
|
||||||
if isinstance(res, list):
|
|
||||||
for r in res:
|
|
||||||
if r is not None: self.treat_out(context, r)
|
|
||||||
|
|
||||||
elif isinstance(res, response.Response):
|
|
||||||
# Define the destination server
|
|
||||||
if (res.server is not None and
|
|
||||||
isinstance(res.server, str) and res.server in context.servers):
|
|
||||||
res.server = context.servers[res.server]
|
|
||||||
if (res.server is not None and
|
|
||||||
not isinstance(res.server, server.Server)):
|
|
||||||
print ("\033[1;35mWarning:\033[0m the server defined in this "
|
|
||||||
"response doesn't exist: %s" % (res.server))
|
|
||||||
res.server = None
|
|
||||||
if res.server is None:
|
|
||||||
res.server = self.srv
|
|
||||||
|
|
||||||
# Sent the message only if treat_post authorize it
|
|
||||||
if context.treat_post(res):
|
|
||||||
res.server.send_response(res, self.data)
|
|
||||||
|
|
||||||
elif isinstance(res, response.Hook):
|
|
||||||
context.hooks.add_hook(res.type, res.hook, res.src)
|
|
||||||
|
|
||||||
elif res is not None:
|
|
||||||
print ("\033[1;35mWarning:\033[0m unrecognized response type "
|
|
||||||
": %s" % res)
|
|
||||||
|
|
||||||
def run(self, context):
|
|
||||||
"""Create, parse and treat the message"""
|
|
||||||
try:
|
|
||||||
msg = Message(self.raw, self.time, self.prvt)
|
|
||||||
msg.server = self.srv.id
|
|
||||||
if msg.cmd == "PRIVMSG":
|
|
||||||
msg.is_owner = (msg.nick == self.srv.owner)
|
|
||||||
res = self.treat_in(context, msg)
|
|
||||||
except:
|
|
||||||
print ("\033[1;31mERROR:\033[0m occurred during the "
|
|
||||||
"processing of the message: %s" % self.raw)
|
|
||||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
||||||
traceback.print_exception(exc_type, exc_value,
|
|
||||||
exc_traceback)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Send message
|
|
||||||
self.treat_out(context, res)
|
|
||||||
|
|
||||||
# Inform that the message has been treated
|
|
||||||
self.srv.msg_treated(self.data)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class EventConsumer:
|
|
||||||
"""Store a event before treating"""
|
|
||||||
def __init__(self, evt, timeout=20):
|
|
||||||
self.evt = evt
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
|
|
||||||
def run(self, context):
|
|
||||||
try:
|
|
||||||
self.evt.launch_check()
|
|
||||||
except:
|
|
||||||
print ("\033[1;31mError:\033[0m during event end")
|
|
||||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
||||||
traceback.print_exception(exc_type, exc_value,
|
|
||||||
exc_traceback)
|
|
||||||
if self.evt.next is not None:
|
|
||||||
context.add_event(self.evt, self.evt.id)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Consumer(threading.Thread):
|
|
||||||
"""Dequeue and exec requested action"""
|
|
||||||
def __init__(self, context):
|
|
||||||
self.context = context
|
|
||||||
self.stop = False
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
while not self.stop:
|
|
||||||
stm = self.context.cnsr_queue.get(True, 20)
|
|
||||||
stm.run(self.context)
|
|
||||||
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
self.context.cnsr_thrd_size -= 2
|
|
||||||
43
credits.py
43
credits.py
|
|
@ -1,43 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from datetime import timedelta
|
|
||||||
import random
|
|
||||||
|
|
||||||
BANLIST = []
|
|
||||||
|
|
||||||
class Credits:
|
|
||||||
def __init__ (self, name):
|
|
||||||
self.name = name
|
|
||||||
self.credits = 5
|
|
||||||
self.randsec = timedelta(seconds=random.randint(0, 55))
|
|
||||||
self.lastmessage = datetime.now() + self.randsec
|
|
||||||
self.iask = True
|
|
||||||
|
|
||||||
def ask(self):
|
|
||||||
if self.name in BANLIST:
|
|
||||||
return False
|
|
||||||
|
|
||||||
now = datetime.now() + self.randsec
|
|
||||||
if self.lastmessage.minute == now.minute and (self.lastmessage.second == now.second or self.lastmessage.second == now.second - 1):
|
|
||||||
print("\033[1;36mAUTOBAN\033[0m %s: too low time between messages" % self.name)
|
|
||||||
#BANLIST.append(self.name)
|
|
||||||
self.credits -= self.credits / 2 #Une alternative
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.iask = True
|
|
||||||
return self.credits > 0 or self.lastmessage.minute != now.minute
|
|
||||||
|
|
||||||
def speak(self):
|
|
||||||
if self.iask:
|
|
||||||
self.iask = False
|
|
||||||
now = datetime.now() + self.randsec
|
|
||||||
if self.lastmessage.minute != now.minute:
|
|
||||||
self.credits = min (15, self.credits + 5)
|
|
||||||
self.lastmessage = now
|
|
||||||
|
|
||||||
self.credits -= 1
|
|
||||||
return self.credits > -3
|
|
||||||
|
|
||||||
def to_string(self):
|
|
||||||
print ("%s: %d ; reset: %d" % (self.name, self.credits, self.randsec.seconds))
|
|
||||||
118
event.py
118
event.py
|
|
@ -1,118 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
class ModuleEvent:
|
|
||||||
def __init__(self, func=None, func_data=None, check=None, cmp_data=None,
|
|
||||||
intervalle=60, offset=0, call=None, call_data=None, times=1):
|
|
||||||
# What have we to check?
|
|
||||||
self.func = func
|
|
||||||
self.func_data = func_data
|
|
||||||
|
|
||||||
# How detect a change?
|
|
||||||
self.check = check
|
|
||||||
if cmp_data is not None:
|
|
||||||
self.cmp_data = cmp_data
|
|
||||||
elif self.func is not None:
|
|
||||||
if self.func_data is None:
|
|
||||||
self.cmp_data = self.func()
|
|
||||||
elif isinstance(self.func_data, dict):
|
|
||||||
self.cmp_data = self.func(**self.func_data)
|
|
||||||
else:
|
|
||||||
self.cmp_data = self.func(self.func_data)
|
|
||||||
else:
|
|
||||||
self.cmp_data = None
|
|
||||||
|
|
||||||
self.offset = timedelta(seconds=offset) # Time to wait before the first check
|
|
||||||
self.intervalle = timedelta(seconds=intervalle)
|
|
||||||
self.end = None
|
|
||||||
|
|
||||||
# What should we call when
|
|
||||||
self.call = call
|
|
||||||
if call_data is not None:
|
|
||||||
self.call_data = call_data
|
|
||||||
else:
|
|
||||||
self.call_data = func_data
|
|
||||||
|
|
||||||
# How many times do this event?
|
|
||||||
self.times = times
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current(self):
|
|
||||||
"""Return the date of the near check"""
|
|
||||||
if self.times != 0:
|
|
||||||
if self.end is None:
|
|
||||||
self.end = datetime.now() + self.offset + self.intervalle
|
|
||||||
return self.end
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def next(self):
|
|
||||||
"""Return the date of the next check"""
|
|
||||||
if self.times != 0:
|
|
||||||
if self.end is None:
|
|
||||||
return self.current
|
|
||||||
elif self.end < datetime.now():
|
|
||||||
self.end += self.intervalle
|
|
||||||
return self.end
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_left(self):
|
|
||||||
"""Return the time left before/after the near check"""
|
|
||||||
if self.current is not None:
|
|
||||||
return self.current - datetime.now()
|
|
||||||
return 99999
|
|
||||||
|
|
||||||
def launch_check(self):
|
|
||||||
if self.func is None:
|
|
||||||
d = self.func_data
|
|
||||||
elif self.func_data is None:
|
|
||||||
d = self.func()
|
|
||||||
elif isinstance(self.func_data, dict):
|
|
||||||
d = self.func(**self.func_data)
|
|
||||||
else:
|
|
||||||
d = self.func(self.func_data)
|
|
||||||
#print ("do test with", d, self.cmp_data)
|
|
||||||
|
|
||||||
if self.check is None:
|
|
||||||
if self.cmp_data is None:
|
|
||||||
r = True
|
|
||||||
else:
|
|
||||||
r = d != self.cmp_data
|
|
||||||
elif self.cmp_data is None:
|
|
||||||
r = self.check(d)
|
|
||||||
elif isinstance(self.cmp_data, dict):
|
|
||||||
r = self.check(d, **self.cmp_data)
|
|
||||||
else:
|
|
||||||
r = self.check(d, self.cmp_data)
|
|
||||||
|
|
||||||
if r:
|
|
||||||
self.times -= 1
|
|
||||||
if self.call_data is None:
|
|
||||||
if d is None:
|
|
||||||
self.call()
|
|
||||||
else:
|
|
||||||
self.call(d)
|
|
||||||
elif isinstance(self.call_data, dict):
|
|
||||||
self.call(d, **self.call_data)
|
|
||||||
else:
|
|
||||||
self.call(d, self.call_data)
|
|
||||||
220
hooks.py
220
hooks.py
|
|
@ -1,220 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from response import Response
|
|
||||||
|
|
||||||
class MessagesHook:
|
|
||||||
def __init__(self, context, bot):
|
|
||||||
self.context = context
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
# Store specials hooks
|
|
||||||
self.all_pre = list() # Treated before any parse
|
|
||||||
self.all_post = list() # Treated before send message to user
|
|
||||||
|
|
||||||
# Store IRC commands hooks
|
|
||||||
self.irc_hook = dict()
|
|
||||||
|
|
||||||
# Store direct hooks
|
|
||||||
self.cmd_hook = dict()
|
|
||||||
self.ask_hook = dict()
|
|
||||||
self.msg_hook = dict()
|
|
||||||
|
|
||||||
# Store regexp hooks
|
|
||||||
self.cmd_rgxp = list()
|
|
||||||
self.ask_rgxp = list()
|
|
||||||
self.msg_rgxp = list()
|
|
||||||
|
|
||||||
# Store default hooks (after other hooks if no match)
|
|
||||||
self.cmd_default = list()
|
|
||||||
self.ask_default = list()
|
|
||||||
self.msg_default = list()
|
|
||||||
|
|
||||||
|
|
||||||
def add_hook(self, store, hook, module_src=None):
|
|
||||||
"""Insert in the right place a hook into the given store"""
|
|
||||||
if module_src is None:
|
|
||||||
print ("\033[1;35mWarning:\033[0m No source module was passed to "
|
|
||||||
"add_hook function, please fix it in order to be "
|
|
||||||
"compatible with unload feature")
|
|
||||||
|
|
||||||
if store in self.context.hooks_cache:
|
|
||||||
del self.context.hooks_cache[store]
|
|
||||||
|
|
||||||
if not hasattr(self, store):
|
|
||||||
print ("\033[1;35mWarning:\033[0m unrecognized hook store")
|
|
||||||
return
|
|
||||||
attr = getattr(self, store)
|
|
||||||
|
|
||||||
if isinstance(attr, dict) and hook.name is not None:
|
|
||||||
if hook.name not in attr:
|
|
||||||
attr[hook.name] = list()
|
|
||||||
attr[hook.name].append(hook)
|
|
||||||
if hook.end is not None:
|
|
||||||
if hook.end not in attr:
|
|
||||||
attr[hook.end] = list()
|
|
||||||
attr[hook.end].append(hook)
|
|
||||||
elif isinstance(attr, list):
|
|
||||||
attr.append(hook)
|
|
||||||
else:
|
|
||||||
print ("\033[1;32mWarning:\033[0m unrecognized hook store type")
|
|
||||||
return
|
|
||||||
if module_src is not None and hasattr(module_src, "REGISTERED_HOOKS"):
|
|
||||||
module_src.REGISTERED_HOOKS.append((store, hook))
|
|
||||||
|
|
||||||
def register_hook_attributes(self, store, module, node):
|
|
||||||
if node.hasAttribute("data"):
|
|
||||||
data = node["data"]
|
|
||||||
else:
|
|
||||||
data = None
|
|
||||||
if node.hasAttribute("name"):
|
|
||||||
self.add_hook(store + "_hook", Hook(getattr(module, node["call"]),
|
|
||||||
node["name"], data=data),
|
|
||||||
module)
|
|
||||||
elif node.hasAttribute("regexp"):
|
|
||||||
self.add_hook(store + "_rgxp", Hook(getattr(module, node["call"]),
|
|
||||||
regexp=node["regexp"], data=data),
|
|
||||||
module)
|
|
||||||
|
|
||||||
def register_hook(self, module, node):
|
|
||||||
"""Create a hook from configuration node"""
|
|
||||||
if node.name == "message" and node.hasAttribute("type"):
|
|
||||||
if node["type"] == "cmd" or node["type"] == "all":
|
|
||||||
self.register_hook_attributes("cmd", module, node)
|
|
||||||
|
|
||||||
if node["type"] == "ask" or node["type"] == "all":
|
|
||||||
self.register_hook_attributes("ask", module, node)
|
|
||||||
|
|
||||||
if (node["type"] == "msg" or node["type"] == "answer" or
|
|
||||||
node["type"] == "all"):
|
|
||||||
self.register_hook_attributes("answer", module, node)
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
for h in self.all_pre:
|
|
||||||
self.del_hook("all_pre", h)
|
|
||||||
for h in self.all_post:
|
|
||||||
self.del_hook("all_post", h)
|
|
||||||
|
|
||||||
for l in self.irc_hook:
|
|
||||||
for h in self.irc_hook[l]:
|
|
||||||
self.del_hook("irc_hook", h)
|
|
||||||
|
|
||||||
for l in self.cmd_hook:
|
|
||||||
for h in self.cmd_hook[l]:
|
|
||||||
self.del_hook("cmd_hook", h)
|
|
||||||
for l in self.ask_hook:
|
|
||||||
for h in self.ask_hook[l]:
|
|
||||||
self.del_hook("ask_hook", h)
|
|
||||||
for l in self.msg_hook:
|
|
||||||
for h in self.msg_hook[l]:
|
|
||||||
self.del_hook("msg_hook", h)
|
|
||||||
|
|
||||||
for h in self.cmd_rgxp:
|
|
||||||
self.del_hook("cmd_rgxp", h)
|
|
||||||
for h in self.ask_rgxp:
|
|
||||||
self.del_hook("ask_rgxp", h)
|
|
||||||
for h in self.msg_rgxp:
|
|
||||||
self.del_hook("msg_rgxp", h)
|
|
||||||
|
|
||||||
for h in self.cmd_default:
|
|
||||||
self.del_hook("cmd_default", h)
|
|
||||||
for h in self.ask_default:
|
|
||||||
self.del_hook("ask_default", h)
|
|
||||||
for h in self.msg_default:
|
|
||||||
self.del_hook("msg_default", h)
|
|
||||||
|
|
||||||
def del_hook(self, store, hook, module_src=None):
|
|
||||||
"""Remove a registered hook from a given store"""
|
|
||||||
if store in self.context.hooks_cache:
|
|
||||||
del self.context.hooks_cache[store]
|
|
||||||
|
|
||||||
if not hasattr(self, store):
|
|
||||||
print ("Warning: unrecognized hook store type")
|
|
||||||
return
|
|
||||||
attr = getattr(self, store)
|
|
||||||
|
|
||||||
if isinstance(attr, dict) and hook.name is not None:
|
|
||||||
if hook.name in attr:
|
|
||||||
attr[hook.name].remove(hook)
|
|
||||||
if hook.end is not None and hook.end in attr:
|
|
||||||
attr[hook.end].remove(hook)
|
|
||||||
else:
|
|
||||||
attr.remove(hook)
|
|
||||||
|
|
||||||
if module_src is not None:
|
|
||||||
module_src.REGISTERED_HOOKS.remove((store, hook))
|
|
||||||
|
|
||||||
|
|
||||||
class Hook:
|
|
||||||
"""Class storing hook informations"""
|
|
||||||
def __init__(self, call, name=None, data=None, regexp=None, channels=list(), server=None, end=None, call_end=None):
|
|
||||||
self.name = name
|
|
||||||
self.end = end
|
|
||||||
self.call = call
|
|
||||||
if call_end is None:
|
|
||||||
self.call_end = self.call
|
|
||||||
else:
|
|
||||||
self.call_end = call_end
|
|
||||||
self.regexp = regexp
|
|
||||||
self.data = data
|
|
||||||
self.times = -1
|
|
||||||
self.server = server
|
|
||||||
self.channels = channels
|
|
||||||
|
|
||||||
def is_matching(self, strcmp, channel=None, server=None):
|
|
||||||
"""Test if the current hook correspond to the message"""
|
|
||||||
return (channel is None or len(self.channels) <= 0 or
|
|
||||||
channel in self.channels) and (server is None or
|
|
||||||
self.server is None or self.server == server) and (
|
|
||||||
(self.name is None or strcmp == self.name) and (
|
|
||||||
self.end is None or strcmp == self.end) and (
|
|
||||||
self.regexp is None or re.match(self.regexp, strcmp)))
|
|
||||||
|
|
||||||
def run(self, msg, data2=None, strcmp=None):
|
|
||||||
"""Run the hook"""
|
|
||||||
if self.times != 0:
|
|
||||||
self.times -= 1
|
|
||||||
|
|
||||||
if (self.end is not None and strcmp is not None and
|
|
||||||
self.call_end is not None and strcmp == self.end):
|
|
||||||
call = self.call_end
|
|
||||||
self.times = 0
|
|
||||||
else:
|
|
||||||
call = self.call
|
|
||||||
|
|
||||||
if self.data is None:
|
|
||||||
if data2 is None:
|
|
||||||
return call(msg)
|
|
||||||
elif isinstance(data2, dict):
|
|
||||||
return call(msg, **data2)
|
|
||||||
else:
|
|
||||||
return call(msg, data2)
|
|
||||||
elif isinstance(self.data, dict):
|
|
||||||
if data2 is None:
|
|
||||||
return call(msg, **self.data)
|
|
||||||
else:
|
|
||||||
return call(msg, data2, **self.data)
|
|
||||||
else:
|
|
||||||
if data2 is None:
|
|
||||||
return call(msg, self.data)
|
|
||||||
elif isinstance(data2, dict):
|
|
||||||
return call(msg, self.data, **data2)
|
|
||||||
else:
|
|
||||||
return call(msg, self.data, data2)
|
|
||||||
264
importer.py
264
importer.py
|
|
@ -1,264 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from importlib.abc import Finder
|
|
||||||
from importlib.abc import SourceLoader
|
|
||||||
import imp
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import event
|
|
||||||
from hooks import Hook
|
|
||||||
import response
|
|
||||||
import xmlparser
|
|
||||||
|
|
||||||
class ModuleFinder(Finder):
|
|
||||||
def __init__(self, context, prompt):
|
|
||||||
self.context = context
|
|
||||||
self.prompt = prompt
|
|
||||||
|
|
||||||
def find_module(self, fullname, path=None):
|
|
||||||
#print ("looking for", fullname, "in", path)
|
|
||||||
# Search only for new nemubot modules (packages init)
|
|
||||||
if path is None:
|
|
||||||
for mpath in self.context.modules_path:
|
|
||||||
#print ("looking for", fullname, "in", mpath)
|
|
||||||
if os.path.isfile(mpath + fullname + ".xml"):
|
|
||||||
return ModuleLoader(self.context, self.prompt, fullname,
|
|
||||||
mpath, mpath + fullname + ".xml")
|
|
||||||
elif (os.path.isfile(mpath + fullname + ".py") or
|
|
||||||
os.path.isfile(mpath + fullname + "/__init__.py")):
|
|
||||||
return ModuleLoader(self.context, self.prompt,
|
|
||||||
fullname, mpath, None)
|
|
||||||
#print ("not found")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleLoader(SourceLoader):
|
|
||||||
def __init__(self, context, prompt, fullname, path, config_path):
|
|
||||||
self.context = context
|
|
||||||
self.prompt = prompt
|
|
||||||
self.name = fullname
|
|
||||||
self.config_path = config_path
|
|
||||||
|
|
||||||
if config_path is not None:
|
|
||||||
self.config = xmlparser.parse_file(config_path)
|
|
||||||
if self.config.hasAttribute("name"):
|
|
||||||
self.name = self.config["name"]
|
|
||||||
else:
|
|
||||||
self.config = None
|
|
||||||
|
|
||||||
if os.path.isfile(path + fullname + ".py"):
|
|
||||||
self.source_path = path + self.name + ".py"
|
|
||||||
self.package = False
|
|
||||||
self.mpath = path
|
|
||||||
elif os.path.isfile(path + fullname + "/__init__.py"):
|
|
||||||
self.source_path = path + self.name + "/__init__.py"
|
|
||||||
self.package = True
|
|
||||||
self.mpath = path + self.name + "/"
|
|
||||||
else:
|
|
||||||
raise ImportError
|
|
||||||
|
|
||||||
def get_filename(self, fullname):
|
|
||||||
"""Return the path to the source file as found by the finder."""
|
|
||||||
return self.source_path
|
|
||||||
|
|
||||||
def get_data(self, path):
|
|
||||||
"""Return the data from path as raw bytes."""
|
|
||||||
with open(path, 'rb') as file:
|
|
||||||
return file.read()
|
|
||||||
|
|
||||||
def path_mtime(self, path):
|
|
||||||
st = os.stat(path)
|
|
||||||
return int(st.st_mtime)
|
|
||||||
|
|
||||||
def set_data(self, path, data):
|
|
||||||
"""Write bytes data to a file."""
|
|
||||||
parent, filename = os.path.split(path)
|
|
||||||
path_parts = []
|
|
||||||
# Figure out what directories are missing.
|
|
||||||
while parent and not os.path.isdir(parent):
|
|
||||||
parent, part = os.path.split(parent)
|
|
||||||
path_parts.append(part)
|
|
||||||
# Create needed directories.
|
|
||||||
for part in reversed(path_parts):
|
|
||||||
parent = os.path.join(parent, part)
|
|
||||||
try:
|
|
||||||
os.mkdir(parent)
|
|
||||||
except FileExistsError:
|
|
||||||
# Probably another Python process already created the dir.
|
|
||||||
continue
|
|
||||||
except PermissionError:
|
|
||||||
# If can't get proper access, then just forget about writing
|
|
||||||
# the data.
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
with open(path, 'wb') as file:
|
|
||||||
file.write(data)
|
|
||||||
except (PermissionError, FileExistsError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_code(self, fullname):
|
|
||||||
return SourceLoader.get_code(self, fullname)
|
|
||||||
|
|
||||||
def get_source(self, fullname):
|
|
||||||
return SourceLoader.get_source(self, fullname)
|
|
||||||
|
|
||||||
def is_package(self, fullname):
|
|
||||||
return self.package
|
|
||||||
|
|
||||||
def load_module(self, fullname):
|
|
||||||
module = self._load_module(fullname, sourceless=True)
|
|
||||||
|
|
||||||
# Remove the module from sys list
|
|
||||||
del sys.modules[fullname]
|
|
||||||
|
|
||||||
# If the module was already loaded, then reload it
|
|
||||||
if hasattr(module, '__LOADED__'):
|
|
||||||
reload(module)
|
|
||||||
|
|
||||||
# Check that is a valid nemubot module
|
|
||||||
if not hasattr(module, "nemubotversion"):
|
|
||||||
raise ImportError("Module `%s' is not a nemubot module."%self.name)
|
|
||||||
# Check module version
|
|
||||||
if module.nemubotversion != self.context.version:
|
|
||||||
raise ImportError("Module `%s' is not compatible with this "
|
|
||||||
"version." % self.name)
|
|
||||||
|
|
||||||
# Set module common functions and datas
|
|
||||||
module.__LOADED__ = True
|
|
||||||
|
|
||||||
# Set module common functions and datas
|
|
||||||
module.REGISTERED_HOOKS = list()
|
|
||||||
module.REGISTERED_EVENTS = list()
|
|
||||||
module.DEBUG = False
|
|
||||||
module.DIR = self.mpath
|
|
||||||
module.name = fullname
|
|
||||||
module.print = lambda msg: print("[%s] %s"%(module.name, msg))
|
|
||||||
module.print_debug = lambda msg: mod_print_dbg(module, msg)
|
|
||||||
module.send_response = lambda srv, res: mod_send_response(self.context, srv, res)
|
|
||||||
module.add_hook = lambda store, hook: self.context.hooks.add_hook(store, hook, module)
|
|
||||||
module.del_hook = lambda store, hook: self.context.hooks.del_hook(store, hook)
|
|
||||||
module.add_event = lambda evt: self.context.add_event(evt, module_src=module)
|
|
||||||
module.add_event_eid = lambda evt, eid: self.context.add_event(evt, eid, module_src=module)
|
|
||||||
module.del_event = lambda evt: self.context.del_event(evt, module_src=module)
|
|
||||||
|
|
||||||
if not hasattr(module, "NODATA"):
|
|
||||||
module.DATAS = xmlparser.parse_file(self.context.datas_path
|
|
||||||
+ module.name + ".xml")
|
|
||||||
module.save = lambda: mod_save(module, self.context.datas_path)
|
|
||||||
else:
|
|
||||||
module.DATAS = None
|
|
||||||
module.save = lambda: False
|
|
||||||
module.CONF = self.config
|
|
||||||
module.has_access = lambda msg: mod_has_access(module,
|
|
||||||
module.CONF, msg)
|
|
||||||
|
|
||||||
module.ModuleEvent = event.ModuleEvent
|
|
||||||
module.ModuleState = xmlparser.module_state.ModuleState
|
|
||||||
module.Response = response.Response
|
|
||||||
|
|
||||||
# Load dependancies
|
|
||||||
if module.CONF is not None and module.CONF.hasNode("dependson"):
|
|
||||||
module.MODS = dict()
|
|
||||||
for depend in module.CONF.getNodes("dependson"):
|
|
||||||
for md in MODS:
|
|
||||||
if md.name == depend["name"]:
|
|
||||||
mod.MODS[md.name] = md
|
|
||||||
break
|
|
||||||
if depend["name"] not in module.MODS:
|
|
||||||
print ("\033[1;31mERROR:\033[0m in module `%s', module "
|
|
||||||
"`%s' require by this module but is not loaded."
|
|
||||||
% (module.name, depend["name"]))
|
|
||||||
return
|
|
||||||
|
|
||||||
# Add the module to the global modules list
|
|
||||||
if self.context.add_module(module):
|
|
||||||
|
|
||||||
# Launch the module
|
|
||||||
if hasattr(module, "load"):
|
|
||||||
module.load(self.context)
|
|
||||||
|
|
||||||
# Register hooks
|
|
||||||
register_hooks(module, self.context, self.prompt)
|
|
||||||
|
|
||||||
print (" Module `%s' successfully loaded." % module.name)
|
|
||||||
else:
|
|
||||||
raise ImportError("An error occurs while importing `%s'."
|
|
||||||
% module.name)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
def add_cap_hook(prompt, module, cmd):
|
|
||||||
if hasattr(module, cmd["call"]):
|
|
||||||
prompt.add_cap_hook(cmd["name"], getattr(module, cmd["call"]))
|
|
||||||
else:
|
|
||||||
print ("Warning: In module `%s', no function `%s' defined for `%s' "
|
|
||||||
"command hook." % (module.name, cmd["call"], cmd["name"]))
|
|
||||||
|
|
||||||
def register_hooks(module, context, prompt):
|
|
||||||
"""Register all available hooks"""
|
|
||||||
if module.CONF is not None:
|
|
||||||
# Register command hooks
|
|
||||||
if module.CONF.hasNode("command"):
|
|
||||||
for cmd in module.CONF.getNodes("command"):
|
|
||||||
if cmd.hasAttribute("name") and cmd.hasAttribute("call"):
|
|
||||||
add_cap_hook(prompt, module, cmd)
|
|
||||||
|
|
||||||
# Register message hooks
|
|
||||||
if module.CONF.hasNode("message"):
|
|
||||||
for msg in module.CONF.getNodes("message"):
|
|
||||||
context.hooks.register_hook(module, msg)
|
|
||||||
|
|
||||||
# Register legacy hooks
|
|
||||||
if hasattr(module, "parseanswer"):
|
|
||||||
context.hooks.add_hook("cmd_default", Hook(module.parseanswer), module)
|
|
||||||
if hasattr(module, "parseask"):
|
|
||||||
context.hooks.add_hook("ask_default", Hook(module.parseask), module)
|
|
||||||
if hasattr(module, "parselisten"):
|
|
||||||
context.hooks.add_hook("msg_default", Hook(module.parselisten), module)
|
|
||||||
|
|
||||||
##########################
|
|
||||||
# #
|
|
||||||
# Module functions #
|
|
||||||
# #
|
|
||||||
##########################
|
|
||||||
|
|
||||||
def mod_print_dbg(mod, msg):
|
|
||||||
if mod.DEBUG:
|
|
||||||
print("{%s} %s"%(mod.name, msg))
|
|
||||||
|
|
||||||
def mod_save(mod, datas_path):
|
|
||||||
mod.DATAS.save(datas_path + "/" + mod.name + ".xml")
|
|
||||||
mod.print_debug("Saving!")
|
|
||||||
|
|
||||||
def mod_has_access(mod, config, msg):
|
|
||||||
if config is not None and config.hasNode("channel"):
|
|
||||||
for chan in config.getNodes("channel"):
|
|
||||||
if (chan["server"] is None or chan["server"] == msg.srv.id) and (
|
|
||||||
chan["channel"] is None or chan["channel"] == msg.channel):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def mod_send_response(context, server, res):
|
|
||||||
if server in context.servers:
|
|
||||||
context.servers[server].send_response(res, None)
|
|
||||||
else:
|
|
||||||
print("\033[1;35mWarning:\033[0m Try to send a message to the unknown server: %s" % server)
|
|
||||||
294
message.py
294
message.py
|
|
@ -1,294 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import time
|
|
||||||
|
|
||||||
import credits
|
|
||||||
from credits import Credits
|
|
||||||
from response import Response
|
|
||||||
import xmlparser
|
|
||||||
|
|
||||||
CREDITS = {}
|
|
||||||
filename = ""
|
|
||||||
|
|
||||||
def load(config_file):
|
|
||||||
global CREDITS, filename
|
|
||||||
CREDITS = dict ()
|
|
||||||
filename = config_file
|
|
||||||
credits.BANLIST = xmlparser.parse_file(filename)
|
|
||||||
|
|
||||||
def save():
|
|
||||||
global filename
|
|
||||||
credits.BANLIST.save(filename)
|
|
||||||
|
|
||||||
|
|
||||||
class Message:
|
|
||||||
def __init__ (self, line, timestamp, private = False):
|
|
||||||
self.raw = line
|
|
||||||
self.time = timestamp
|
|
||||||
self.channel = None
|
|
||||||
self.content = b''
|
|
||||||
self.ctcp = False
|
|
||||||
line = line.rstrip() #remove trailing 'rn'
|
|
||||||
|
|
||||||
words = line.split(b' ')
|
|
||||||
if words[0][0] == 58: #58 is : in ASCII table
|
|
||||||
self.sender = words[0][1:].decode()
|
|
||||||
self.cmd = words[1].decode()
|
|
||||||
else:
|
|
||||||
self.cmd = words[0].decode()
|
|
||||||
self.sender = None
|
|
||||||
|
|
||||||
if self.cmd == 'PING':
|
|
||||||
self.content = words[1]
|
|
||||||
elif self.sender is not None:
|
|
||||||
self.nick = (self.sender.split('!'))[0]
|
|
||||||
if self.nick != self.sender:
|
|
||||||
self.realname = (self.sender.split('!'))[1]
|
|
||||||
else:
|
|
||||||
self.realname = self.nick
|
|
||||||
self.sender = self.nick + "!" + self.realname
|
|
||||||
|
|
||||||
if len(words) > 2:
|
|
||||||
self.channel = self.pickWords(words[2:]).decode()
|
|
||||||
|
|
||||||
if self.cmd == 'PRIVMSG':
|
|
||||||
# Check for CTCP request
|
|
||||||
self.ctcp = len(words[3]) > 1 and (words[3][0] == 0x01 or words[3][1] == 0x01)
|
|
||||||
self.content = self.pickWords(words[3:])
|
|
||||||
elif self.cmd == '353' and len(words) > 3:
|
|
||||||
for i in range(2, len(words)):
|
|
||||||
if words[i][0] == 58:
|
|
||||||
self.content = words[i:]
|
|
||||||
#Remove the first :
|
|
||||||
self.content[0] = self.content[0][1:]
|
|
||||||
self.channel = words[i-1].decode()
|
|
||||||
break
|
|
||||||
elif self.cmd == 'NICK':
|
|
||||||
self.content = self.pickWords(words[2:])
|
|
||||||
elif self.cmd == 'MODE':
|
|
||||||
self.content = words[3:]
|
|
||||||
elif self.cmd == '332':
|
|
||||||
self.channel = words[3]
|
|
||||||
self.content = self.pickWords(words[4:])
|
|
||||||
else:
|
|
||||||
#print (line)
|
|
||||||
self.content = self.pickWords(words[3:])
|
|
||||||
else:
|
|
||||||
print (line)
|
|
||||||
if self.cmd == 'PRIVMSG':
|
|
||||||
self.channel = words[2].decode()
|
|
||||||
self.content = b' '.join(words[3:])
|
|
||||||
self.decode()
|
|
||||||
if self.cmd == 'PRIVMSG':
|
|
||||||
self.parse_content()
|
|
||||||
self.private = private
|
|
||||||
|
|
||||||
def parse_content(self):
|
|
||||||
"""Parse or reparse the message content"""
|
|
||||||
# If CTCP, remove 0x01
|
|
||||||
if self.ctcp:
|
|
||||||
self.content = self.content[1:len(self.content)-1]
|
|
||||||
|
|
||||||
# Split content by words
|
|
||||||
try:
|
|
||||||
self.cmds = shlex.split(self.content)
|
|
||||||
except ValueError:
|
|
||||||
self.cmds = self.content.split(' ')
|
|
||||||
|
|
||||||
def pickWords(self, words):
|
|
||||||
"""Parse last argument of a line: can be a single word or a sentence starting with :"""
|
|
||||||
if len(words) > 0 and len(words[0]) > 0:
|
|
||||||
if words[0][0] == 58:
|
|
||||||
return b' '.join(words[0:])[1:]
|
|
||||||
else:
|
|
||||||
return words[0]
|
|
||||||
else:
|
|
||||||
return b''
|
|
||||||
|
|
||||||
def decode(self):
|
|
||||||
"""Decode the content string usign a specific encoding"""
|
|
||||||
if isinstance(self.content, bytes):
|
|
||||||
try:
|
|
||||||
self.content = self.content.decode()
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
#TODO: use encoding from config file
|
|
||||||
self.content = self.content.decode('utf-8', 'replace')
|
|
||||||
|
|
||||||
def authorize_DEPRECATED(self):
|
|
||||||
"""Is nemubot listening for the sender on this channel?"""
|
|
||||||
# TODO: deprecated
|
|
||||||
if self.srv.isDCC(self.sender):
|
|
||||||
return True
|
|
||||||
elif self.realname not in CREDITS:
|
|
||||||
CREDITS[self.realname] = Credits(self.realname)
|
|
||||||
elif self.content[0] == '`':
|
|
||||||
return True
|
|
||||||
elif not CREDITS[self.realname].ask():
|
|
||||||
return False
|
|
||||||
return self.srv.accepted_channel(self.channel)
|
|
||||||
|
|
||||||
##############################
|
|
||||||
# #
|
|
||||||
# Extraction/Format text #
|
|
||||||
# #
|
|
||||||
##############################
|
|
||||||
|
|
||||||
def just_countdown (self, delta, resolution = 5):
|
|
||||||
sec = delta.seconds
|
|
||||||
hours, remainder = divmod(sec, 3600)
|
|
||||||
minutes, seconds = divmod(remainder, 60)
|
|
||||||
an = int(delta.days / 365.25)
|
|
||||||
days = delta.days % 365.25
|
|
||||||
|
|
||||||
sentence = ""
|
|
||||||
force = False
|
|
||||||
|
|
||||||
if resolution > 0 and (force or an > 0):
|
|
||||||
force = True
|
|
||||||
sentence += " %i an"%(an)
|
|
||||||
|
|
||||||
if an > 1:
|
|
||||||
sentence += "s"
|
|
||||||
if resolution > 2:
|
|
||||||
sentence += ","
|
|
||||||
elif resolution > 1:
|
|
||||||
sentence += " et"
|
|
||||||
|
|
||||||
if resolution > 1 and (force or days > 0):
|
|
||||||
force = True
|
|
||||||
sentence += " %i jour"%(days)
|
|
||||||
|
|
||||||
if days > 1:
|
|
||||||
sentence += "s"
|
|
||||||
if resolution > 3:
|
|
||||||
sentence += ","
|
|
||||||
elif resolution > 2:
|
|
||||||
sentence += " et"
|
|
||||||
|
|
||||||
if resolution > 2 and (force or hours > 0):
|
|
||||||
force = True
|
|
||||||
sentence += " %i heure"%(hours)
|
|
||||||
if hours > 1:
|
|
||||||
sentence += "s"
|
|
||||||
if resolution > 4:
|
|
||||||
sentence += ","
|
|
||||||
elif resolution > 3:
|
|
||||||
sentence += " et"
|
|
||||||
|
|
||||||
if resolution > 3 and (force or minutes > 0):
|
|
||||||
force = True
|
|
||||||
sentence += " %i minute"%(minutes)
|
|
||||||
if minutes > 1:
|
|
||||||
sentence += "s"
|
|
||||||
if resolution > 4:
|
|
||||||
sentence += " et"
|
|
||||||
|
|
||||||
if resolution > 4 and (force or seconds > 0):
|
|
||||||
force = True
|
|
||||||
sentence += " %i seconde"%(seconds)
|
|
||||||
if seconds > 1:
|
|
||||||
sentence += "s"
|
|
||||||
return sentence[1:]
|
|
||||||
|
|
||||||
|
|
||||||
def countdown_format (self, date, msg_before, msg_after, timezone = None):
|
|
||||||
"""Replace in a text %s by a sentence incidated the remaining time before/after an event"""
|
|
||||||
if timezone != None:
|
|
||||||
os.environ['TZ'] = timezone
|
|
||||||
time.tzset()
|
|
||||||
|
|
||||||
#Calculate time before the date
|
|
||||||
if datetime.now() > date:
|
|
||||||
sentence_c = msg_after
|
|
||||||
delta = datetime.now() - date
|
|
||||||
else:
|
|
||||||
sentence_c = msg_before
|
|
||||||
delta = date - datetime.now()
|
|
||||||
|
|
||||||
if timezone != None:
|
|
||||||
os.environ['TZ'] = "Europe/Paris"
|
|
||||||
|
|
||||||
return sentence_c % self.just_countdown(delta)
|
|
||||||
|
|
||||||
|
|
||||||
def extractDate (self):
|
|
||||||
"""Parse a message to extract a time and date"""
|
|
||||||
msgl = self.content.lower ()
|
|
||||||
result = re.match("^[^0-9]+(([0-9]{1,4})[^0-9]+([0-9]{1,2}|janvier|january|fevrier|février|february|mars|march|avril|april|mai|maï|may|juin|juni|juillet|july|jully|august|aout|août|septembre|september|october|octobre|oktober|novembre|november|decembre|décembre|december)([^0-9]+([0-9]{1,4}))?)[^0-9]+(([0-9]{1,2})[^0-9]*[h':]([^0-9]*([0-9]{1,2})([^0-9]*[m\":][^0-9]*([0-9]{1,2}))?)?)?.*$", msgl + " TXT")
|
|
||||||
if result is not None:
|
|
||||||
day = result.group(2)
|
|
||||||
if len(day) == 4:
|
|
||||||
year = day
|
|
||||||
day = 0
|
|
||||||
month = result.group(3)
|
|
||||||
if month == "janvier" or month == "january" or month == "januar":
|
|
||||||
month = 1
|
|
||||||
elif month == "fevrier" or month == "février" or month == "february":
|
|
||||||
month = 2
|
|
||||||
elif month == "mars" or month == "march":
|
|
||||||
month = 3
|
|
||||||
elif month == "avril" or month == "april":
|
|
||||||
month = 4
|
|
||||||
elif month == "mai" or month == "may" or month == "maï":
|
|
||||||
month = 5
|
|
||||||
elif month == "juin" or month == "juni" or month == "junni":
|
|
||||||
month = 6
|
|
||||||
elif month == "juillet" or month == "jully" or month == "july":
|
|
||||||
month = 7
|
|
||||||
elif month == "aout" or month == "août" or month == "august":
|
|
||||||
month = 8
|
|
||||||
elif month == "september" or month == "septembre":
|
|
||||||
month = 9
|
|
||||||
elif month == "october" or month == "october" or month == "oktober":
|
|
||||||
month = 10
|
|
||||||
elif month == "november" or month == "novembre":
|
|
||||||
month = 11
|
|
||||||
elif month == "december" or month == "decembre" or month == "décembre":
|
|
||||||
month = 12
|
|
||||||
|
|
||||||
if day == 0:
|
|
||||||
day = result.group(5)
|
|
||||||
else:
|
|
||||||
year = result.group(5)
|
|
||||||
|
|
||||||
hour = result.group(7)
|
|
||||||
minute = result.group(9)
|
|
||||||
second = result.group(11)
|
|
||||||
|
|
||||||
print ("Chaîne reconnue : %s/%s/%s %s:%s:%s"%(day, month, year, hour, minute, second))
|
|
||||||
if year == None:
|
|
||||||
year = date.today().year
|
|
||||||
if hour == None:
|
|
||||||
hour = 0
|
|
||||||
if minute == None:
|
|
||||||
minute = 0
|
|
||||||
if second == None:
|
|
||||||
second = 1
|
|
||||||
else:
|
|
||||||
second = int (second) + 1
|
|
||||||
if second > 59:
|
|
||||||
minute = int (minute) + 1
|
|
||||||
second = 0
|
|
||||||
|
|
||||||
return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second))
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
277
modules/alias.py
Normal file
277
modules/alias.py
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
"""Create alias of commands"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from nemubot import context
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.message import Command
|
||||||
|
from nemubot.tools.human import guess
|
||||||
|
from nemubot.tools.xmlparser.node import ModuleState
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# LOADING #############################################################
|
||||||
|
|
||||||
|
def load(context):
|
||||||
|
"""Load this module"""
|
||||||
|
if not context.data.hasNode("aliases"):
|
||||||
|
context.data.addChild(ModuleState("aliases"))
|
||||||
|
context.data.getNode("aliases").setIndex("alias")
|
||||||
|
if not context.data.hasNode("variables"):
|
||||||
|
context.data.addChild(ModuleState("variables"))
|
||||||
|
context.data.getNode("variables").setIndex("name")
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
## Alias management
|
||||||
|
|
||||||
|
def list_alias(channel=None):
|
||||||
|
"""List known aliases.
|
||||||
|
|
||||||
|
Argument:
|
||||||
|
channel -- optional, if defined, return a list of aliases only defined on this channel, else alias widly defined
|
||||||
|
"""
|
||||||
|
|
||||||
|
for alias in context.data.getNode("aliases").index.values():
|
||||||
|
if (channel is None and "channel" not in alias) or (channel is not None and "channel" in alias and alias["channel"] == channel):
|
||||||
|
yield alias
|
||||||
|
|
||||||
|
def create_alias(alias, origin, channel=None, creator=None):
|
||||||
|
"""Create or erase an existing alias
|
||||||
|
"""
|
||||||
|
|
||||||
|
anode = ModuleState("alias")
|
||||||
|
anode["alias"] = alias
|
||||||
|
anode["origin"] = origin
|
||||||
|
if channel is not None:
|
||||||
|
anode["creator"] = channel
|
||||||
|
if creator is not None:
|
||||||
|
anode["creator"] = creator
|
||||||
|
context.data.getNode("aliases").addChild(anode)
|
||||||
|
context.save()
|
||||||
|
|
||||||
|
|
||||||
|
## Variables management
|
||||||
|
|
||||||
|
def get_variable(name, msg=None):
|
||||||
|
"""Get the value for the given variable
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name -- The variable identifier
|
||||||
|
msg -- optional, original message where some variable can be picked
|
||||||
|
"""
|
||||||
|
|
||||||
|
if msg is not None and (name == "sender" or name == "from" or name == "nick"):
|
||||||
|
return msg.frm
|
||||||
|
elif msg is not None and (name == "chan" or name == "channel"):
|
||||||
|
return msg.channel
|
||||||
|
elif name == "date":
|
||||||
|
return datetime.now(timezone.utc).strftime("%c")
|
||||||
|
elif name in context.data.getNode("variables").index:
|
||||||
|
return context.data.getNode("variables").index[name]["value"]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_variables(user=None):
|
||||||
|
"""List known variables.
|
||||||
|
|
||||||
|
Argument:
|
||||||
|
user -- optional, if defined, display only variable created by the given user
|
||||||
|
"""
|
||||||
|
if user is not None:
|
||||||
|
return [x for x in context.data.getNode("variables").index.values() if x["creator"] == user]
|
||||||
|
else:
|
||||||
|
return context.data.getNode("variables").index.values()
|
||||||
|
|
||||||
|
|
||||||
|
def set_variable(name, value, creator):
|
||||||
|
"""Define or erase a variable.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name -- The variable identifier
|
||||||
|
value -- Variable value
|
||||||
|
creator -- User who has created this variable
|
||||||
|
"""
|
||||||
|
|
||||||
|
var = ModuleState("variable")
|
||||||
|
var["name"] = name
|
||||||
|
var["value"] = value
|
||||||
|
var["creator"] = creator
|
||||||
|
context.data.getNode("variables").addChild(var)
|
||||||
|
context.save()
|
||||||
|
|
||||||
|
|
||||||
|
def replace_variables(cnts, msg):
|
||||||
|
"""Replace variables contained in the content
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
cnt -- content where search variables
|
||||||
|
msg -- Message where pick some variables
|
||||||
|
"""
|
||||||
|
|
||||||
|
unsetCnt = list()
|
||||||
|
if not isinstance(cnts, list):
|
||||||
|
cnts = list(cnts)
|
||||||
|
resultCnt = list()
|
||||||
|
|
||||||
|
for cnt in cnts:
|
||||||
|
for res, name, default in re.findall("\\$\{(([a-zA-Z0-9:]+)(?:-([^}]+))?)\}", cnt):
|
||||||
|
rv = re.match("([0-9]+)(:([0-9]*))?", name)
|
||||||
|
if rv is not None:
|
||||||
|
varI = int(rv.group(1)) - 1
|
||||||
|
if varI >= len(msg.args):
|
||||||
|
cnt = cnt.replace("${%s}" % res, default, 1)
|
||||||
|
elif rv.group(2) is not None:
|
||||||
|
if rv.group(3) is not None and len(rv.group(3)):
|
||||||
|
varJ = int(rv.group(3)) - 1
|
||||||
|
cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:varJ]), 1)
|
||||||
|
for v in range(varI, varJ):
|
||||||
|
unsetCnt.append(v)
|
||||||
|
else:
|
||||||
|
cnt = cnt.replace("${%s}" % res, " ".join(msg.args[varI:]), 1)
|
||||||
|
for v in range(varI, len(msg.args)):
|
||||||
|
unsetCnt.append(v)
|
||||||
|
else:
|
||||||
|
cnt = cnt.replace("${%s}" % res, msg.args[varI], 1)
|
||||||
|
unsetCnt.append(varI)
|
||||||
|
else:
|
||||||
|
cnt = cnt.replace("${%s}" % res, get_variable(name) or default, 1)
|
||||||
|
resultCnt.append(cnt)
|
||||||
|
|
||||||
|
# Remove used content
|
||||||
|
for u in sorted(set(unsetCnt), reverse=True):
|
||||||
|
msg.args.pop(u)
|
||||||
|
|
||||||
|
return resultCnt
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
## Variables management
|
||||||
|
|
||||||
|
@hook.command("listvars",
|
||||||
|
help="list defined variables for substitution in input commands",
|
||||||
|
help_usage={
|
||||||
|
None: "List all known variables",
|
||||||
|
"USER": "List variables created by USER"})
|
||||||
|
def cmd_listvars(msg):
|
||||||
|
if len(msg.args):
|
||||||
|
res = list()
|
||||||
|
for user in msg.args:
|
||||||
|
als = [v["name"] for v in list_variables(user)]
|
||||||
|
if len(als) > 0:
|
||||||
|
res.append("%s's variables: %s" % (user, ", ".join(als)))
|
||||||
|
else:
|
||||||
|
res.append("%s didn't create variable yet." % user)
|
||||||
|
return Response(" ; ".join(res), channel=msg.channel)
|
||||||
|
elif len(context.data.getNode("variables").index):
|
||||||
|
return Response(list_variables(),
|
||||||
|
channel=msg.channel,
|
||||||
|
title="Known variables")
|
||||||
|
else:
|
||||||
|
return Response("There is currently no variable stored.", channel=msg.channel)
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("set",
|
||||||
|
help="Create or set variables for substitution in input commands",
|
||||||
|
help_usage={"KEY VALUE": "Define the variable named KEY and fill it with VALUE as content"})
|
||||||
|
def cmd_set(msg):
|
||||||
|
if len(msg.args) < 2:
|
||||||
|
raise IMException("!set take two args: the key and the value.")
|
||||||
|
set_variable(msg.args[0], " ".join(msg.args[1:]), msg.frm)
|
||||||
|
return Response("Variable $%s successfully defined." % msg.args[0],
|
||||||
|
channel=msg.channel)
|
||||||
|
|
||||||
|
|
||||||
|
## Alias management
|
||||||
|
|
||||||
|
@hook.command("listalias",
|
||||||
|
help="List registered aliases",
|
||||||
|
help_usage={
|
||||||
|
None: "List all registered aliases",
|
||||||
|
"USER": "List all aliases created by USER"})
|
||||||
|
def cmd_listalias(msg):
|
||||||
|
aliases = [a for a in list_alias(None)] + [a for a in list_alias(msg.channel)]
|
||||||
|
if len(aliases):
|
||||||
|
return Response([a["alias"] for a in aliases],
|
||||||
|
channel=msg.channel,
|
||||||
|
title="Known aliases")
|
||||||
|
return Response("There is no alias currently.", channel=msg.channel)
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("alias",
|
||||||
|
help="Display or define the replacement command for a given alias",
|
||||||
|
help_usage={
|
||||||
|
"ALIAS": "Extends the given alias",
|
||||||
|
"ALIAS COMMAND [ARGS ...]": "Create a new alias named ALIAS as replacement to the given COMMAND and ARGS",
|
||||||
|
})
|
||||||
|
def cmd_alias(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("!alias takes as argument an alias to extend.")
|
||||||
|
|
||||||
|
alias = context.subparse(msg, msg.args[0])
|
||||||
|
if alias is None or not isinstance(alias, Command):
|
||||||
|
raise IMException("%s is not a valid alias" % msg.args[0])
|
||||||
|
|
||||||
|
if alias.cmd in context.data.getNode("aliases").index:
|
||||||
|
return Response("%s corresponds to %s" % (alias.cmd, context.data.getNode("aliases").index[alias.cmd]["origin"]),
|
||||||
|
channel=msg.channel, nick=msg.frm)
|
||||||
|
|
||||||
|
elif len(msg.args) > 1:
|
||||||
|
create_alias(alias.cmd,
|
||||||
|
" ".join(msg.args[1:]),
|
||||||
|
channel=msg.channel,
|
||||||
|
creator=msg.frm)
|
||||||
|
return Response("New alias %s successfully registered." % alias.cmd,
|
||||||
|
channel=msg.channel)
|
||||||
|
|
||||||
|
else:
|
||||||
|
wym = [m for m in guess(alias.cmd, context.data.getNode("aliases").index)]
|
||||||
|
raise IMException(msg.args[0] + " is not an alias." + (" Would you mean: %s?" % ", ".join(wym) if len(wym) else ""))
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("unalias",
|
||||||
|
help="Remove a previously created alias")
|
||||||
|
def cmd_unalias(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("Which alias would you want to remove?")
|
||||||
|
res = list()
|
||||||
|
for alias in msg.args:
|
||||||
|
if alias[0] == "!" and len(alias) > 1:
|
||||||
|
alias = alias[1:]
|
||||||
|
if alias in context.data.getNode("aliases").index:
|
||||||
|
context.data.getNode("aliases").delChild(context.data.getNode("aliases").index[alias])
|
||||||
|
res.append(Response("%s doesn't exist anymore." % alias,
|
||||||
|
channel=msg.channel))
|
||||||
|
else:
|
||||||
|
res.append(Response("%s is not an alias" % alias,
|
||||||
|
channel=msg.channel))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
## Alias replacement
|
||||||
|
|
||||||
|
@hook.add(["pre","Command"])
|
||||||
|
def treat_alias(msg):
|
||||||
|
if context.data.getNode("aliases") is not None and msg.cmd in context.data.getNode("aliases").index:
|
||||||
|
origin = context.data.getNode("aliases").index[msg.cmd]["origin"]
|
||||||
|
rpl_msg = context.subparse(msg, origin)
|
||||||
|
if isinstance(rpl_msg, Command):
|
||||||
|
rpl_msg.args = replace_variables(rpl_msg.args, msg)
|
||||||
|
rpl_msg.args += msg.args
|
||||||
|
rpl_msg.kwargs.update(msg.kwargs)
|
||||||
|
elif len(msg.args) or len(msg.kwargs):
|
||||||
|
raise IMException("This kind of alias doesn't take any argument (haven't you forgotten the '!'?).")
|
||||||
|
|
||||||
|
# Avoid infinite recursion
|
||||||
|
if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd:
|
||||||
|
return rpl_msg
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
nemubotversion = 3.3
|
|
||||||
|
|
||||||
def load(context):
|
|
||||||
"""Load this module"""
|
|
||||||
from hooks import Hook
|
|
||||||
add_hook("cmd_hook", Hook(cmd_unalias, "unalias"))
|
|
||||||
add_hook("cmd_hook", Hook(cmd_alias, "alias"))
|
|
||||||
add_hook("cmd_hook", Hook(cmd_set, "set"))
|
|
||||||
add_hook("all_pre", Hook(treat_alias))
|
|
||||||
add_hook("all_post", Hook(treat_variables))
|
|
||||||
|
|
||||||
global DATAS
|
|
||||||
if not DATAS.hasNode("aliases"):
|
|
||||||
DATAS.addChild(ModuleState("aliases"))
|
|
||||||
DATAS.getNode("aliases").setIndex("alias")
|
|
||||||
if not DATAS.hasNode("variables"):
|
|
||||||
DATAS.addChild(ModuleState("variables"))
|
|
||||||
DATAS.getNode("variables").setIndex("name")
|
|
||||||
|
|
||||||
|
|
||||||
def help_tiny ():
|
|
||||||
"""Line inserted in the response to the command !help"""
|
|
||||||
return "alias module"
|
|
||||||
|
|
||||||
def help_full ():
|
|
||||||
return "TODO"
|
|
||||||
|
|
||||||
def set_variable(name, value):
|
|
||||||
var = ModuleState("variable")
|
|
||||||
var["name"] = name
|
|
||||||
var["value"] = value
|
|
||||||
DATAS.getNode("variables").addChild(var)
|
|
||||||
|
|
||||||
def get_variable(name, msg=None):
|
|
||||||
if name == "sender":
|
|
||||||
return msg.sender
|
|
||||||
elif name == "nick":
|
|
||||||
return msg.nick
|
|
||||||
elif name == "chan" or name == "channel":
|
|
||||||
return msg.channel
|
|
||||||
elif name == "date":
|
|
||||||
now = datetime.now()
|
|
||||||
return ("%d/%d/%d %d:%d:%d"%(now.day, now.month, now.year, now.hour,
|
|
||||||
now.minute, now.second))
|
|
||||||
elif name in DATAS.getNode("variables").index:
|
|
||||||
return DATAS.getNode("variables").index[name]["value"]
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def cmd_set(msg):
|
|
||||||
if len (msg.cmds) > 2:
|
|
||||||
set_variable(msg.cmds[1], " ".join(msg.cmds[2:]))
|
|
||||||
res = Response(msg.sender, "Variable \$%s définie." % msg.cmds[1])
|
|
||||||
save()
|
|
||||||
return res
|
|
||||||
return Response(msg.sender, "!set prend au minimum deux arguments : le nom de la variable et sa valeur.")
|
|
||||||
|
|
||||||
def cmd_alias(msg):
|
|
||||||
if len (msg.cmds) > 1:
|
|
||||||
res = list()
|
|
||||||
for alias in msg.cmds[1:]:
|
|
||||||
if alias[0] == "!":
|
|
||||||
alias = alias[1:]
|
|
||||||
if alias in DATAS.getNode("aliases").index:
|
|
||||||
res.append(Response(msg.sender, "!%s correspond à %s" % (alias,
|
|
||||||
DATAS.getNode("aliases").index[alias]["origin"]),
|
|
||||||
channel=msg.channel))
|
|
||||||
else:
|
|
||||||
res.append(Response(msg.sender, "!%s n'est pas un alias" % alias,
|
|
||||||
channel=msg.channel))
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
return Response(msg.sender, "!alias prend en argument l'alias à étendre.",
|
|
||||||
channel=msg.channel)
|
|
||||||
|
|
||||||
def cmd_unalias(msg):
|
|
||||||
if len (msg.cmds) > 1:
|
|
||||||
res = list()
|
|
||||||
for alias in msg.cmds[1:]:
|
|
||||||
if alias[0] == "!" and len(alias) > 1:
|
|
||||||
alias = alias[1:]
|
|
||||||
if alias in DATAS.getNode("aliases").index:
|
|
||||||
if DATAS.getNode("aliases").index[alias]["creator"] == msg.nick or msg.is_owner:
|
|
||||||
DATAS.getNode("aliases").delChild(DATAS.getNode("aliases").index[alias])
|
|
||||||
res.append(Response(msg.sender, "%s a bien été supprimé" % alias, channel=msg.channel))
|
|
||||||
else:
|
|
||||||
res.append(Response(msg.sender, "Vous n'êtes pas le createur de l'alias %s." % alias, channel=msg.channel))
|
|
||||||
else:
|
|
||||||
res.append(Response(msg.sender, "%s n'est pas un alias" % alias, channel=msg.channel))
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
return Response(msg.sender, "!unalias prend en argument l'alias à supprimer.", channel=msg.channel)
|
|
||||||
|
|
||||||
def replace_variables(cnt, msg=None):
|
|
||||||
cnt = cnt.split(' ')
|
|
||||||
unsetCnt = list()
|
|
||||||
for i in range(0, len(cnt)):
|
|
||||||
if i not in unsetCnt:
|
|
||||||
res = re.match("^([^$]*)(\\\\)?\\$([a-zA-Z0-9]+)(.*)$", cnt[i])
|
|
||||||
if res is not None:
|
|
||||||
try:
|
|
||||||
varI = int(res.group(3))
|
|
||||||
unsetCnt.append(varI)
|
|
||||||
cnt[i] = res.group(1) + msg.cmds[varI] + res.group(4)
|
|
||||||
except:
|
|
||||||
if res.group(2) != "":
|
|
||||||
cnt[i] = res.group(1) + "$" + res.group(3) + res.group(4)
|
|
||||||
else:
|
|
||||||
cnt[i] = res.group(1) + get_variable(res.group(3), msg) + res.group(4)
|
|
||||||
return " ".join(cnt)
|
|
||||||
|
|
||||||
|
|
||||||
def treat_variables(res):
|
|
||||||
for i in range(0, len(res.messages)):
|
|
||||||
if isinstance(res.messages[i], list):
|
|
||||||
res.messages[i] = replace_variables(", ".join(res.messages[i]), res)
|
|
||||||
else:
|
|
||||||
res.messages[i] = replace_variables(res.messages[i], res)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def treat_alias(msg, hooks_cache):
|
|
||||||
if msg.cmd == "PRIVMSG" and (len(msg.cmds[0]) > 0 and msg.cmds[0][0] == "!"
|
|
||||||
and msg.cmds[0][1:] in DATAS.getNode("aliases").index
|
|
||||||
and msg.cmds[0][1:] not in hooks_cache("cmd_hook")):
|
|
||||||
msg.content = msg.content.replace(msg.cmds[0],
|
|
||||||
DATAS.getNode("aliases").index[msg.cmds[0][1:]]["origin"], 1)
|
|
||||||
|
|
||||||
msg.content = replace_variables(msg.content, msg)
|
|
||||||
|
|
||||||
msg.parse_content()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def parseask(msg):
|
|
||||||
global ALIAS
|
|
||||||
if re.match(".*(set|cr[ée]{2}|nouvel(le)?) alias.*", msg.content) is not None:
|
|
||||||
result = re.match(".*alias !?([^ ]+) (pour|=|:) (.+)$", msg.content)
|
|
||||||
if result.group(1) in DATAS.getNode("aliases").index or result.group(3).find("alias") >= 0:
|
|
||||||
return Response(msg.sender, "Cet alias est déjà défini.")
|
|
||||||
else:
|
|
||||||
alias = ModuleState("alias")
|
|
||||||
alias["alias"] = result.group(1)
|
|
||||||
alias["origin"] = result.group(3)
|
|
||||||
alias["creator"] = msg.nick
|
|
||||||
DATAS.getNode("aliases").addChild(alias)
|
|
||||||
res = Response(msg.sender, "Nouvel alias %s défini avec succès." % result.group(1))
|
|
||||||
save()
|
|
||||||
return res
|
|
||||||
return False
|
|
||||||
|
|
@ -1,111 +1,134 @@
|
||||||
# coding=utf-8
|
"""People birthdays and ages"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
from xmlparser.node import ModuleState
|
from nemubot import context
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools.countdown import countdown_format
|
||||||
|
from nemubot.tools.date import extractDate
|
||||||
|
from nemubot.tools.xmlparser.node import ModuleState
|
||||||
|
|
||||||
nemubotversion = 3.3
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# LOADING #############################################################
|
||||||
|
|
||||||
def load(context):
|
def load(context):
|
||||||
global DATAS
|
context.data.setIndex("name", "birthday")
|
||||||
DATAS.setIndex("name", "birthday")
|
|
||||||
|
|
||||||
|
|
||||||
def help_tiny ():
|
# MODULE CORE #########################################################
|
||||||
"""Line inserted in the response to the command !help"""
|
|
||||||
return "People birthdays and ages"
|
|
||||||
|
|
||||||
|
|
||||||
def help_full ():
|
|
||||||
return "!anniv /who/: gives the remaining time before the anniversary of /who/\n!age /who/: gives the age of /who/\nIf /who/ is not given, gives the remaining time before your anniversary.\n\n To set yout birthday, say it to nemubot :)"
|
|
||||||
|
|
||||||
|
|
||||||
def findName(msg):
|
def findName(msg):
|
||||||
if len(msg.cmds) < 2 or msg.cmds[1].lower() == "moi" or msg.cmds[1].lower() == "me":
|
if (not len(msg.args) or msg.args[0].lower() == "moi" or
|
||||||
name = msg.nick.lower()
|
msg.args[0].lower() == "me"):
|
||||||
|
name = msg.frm.lower()
|
||||||
else:
|
else:
|
||||||
name = msg.cmds[1].lower()
|
name = msg.args[0].lower()
|
||||||
|
|
||||||
matches = []
|
matches = []
|
||||||
|
|
||||||
if name in DATAS.index:
|
if name in context.data.index:
|
||||||
matches.append(name)
|
matches.append(name)
|
||||||
else:
|
else:
|
||||||
for k in DATAS.index.keys ():
|
for k in context.data.index.keys():
|
||||||
if k.find (name) == 0:
|
if k.find(name) == 0:
|
||||||
matches.append (k)
|
matches.append(k)
|
||||||
return (matches, name)
|
return (matches, name)
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
@hook.command("anniv",
|
||||||
|
help="gives the remaining time before the anniversary of known people",
|
||||||
|
help_usage={
|
||||||
|
None: "Calculate the time remaining before your birthday",
|
||||||
|
"WHO": "Calculate the time remaining before WHO's birthday",
|
||||||
|
})
|
||||||
def cmd_anniv(msg):
|
def cmd_anniv(msg):
|
||||||
(matches, name) = findName(msg)
|
(matches, name) = findName(msg)
|
||||||
if len(matches) == 1:
|
if len(matches) == 1:
|
||||||
name = matches[0]
|
name = matches[0]
|
||||||
tyd = DATAS.index[name].getDate("born")
|
tyd = context.data.index[name].getDate("born")
|
||||||
tyd = datetime(date.today().year, tyd.month, tyd.day)
|
tyd = datetime(date.today().year, tyd.month, tyd.day)
|
||||||
|
|
||||||
if (tyd.day == datetime.today().day and
|
if (tyd.day == datetime.today().day and
|
||||||
tyd.month == datetime.today().month):
|
tyd.month == datetime.today().month):
|
||||||
return Response(msg.sender, msg.countdown_format(
|
return Response(countdown_format(
|
||||||
DATAS.index[name].getDate("born"), "",
|
context.data.index[name].getDate("born"), "",
|
||||||
"C'est aujourd'hui l'anniversaire de %s !"
|
"C'est aujourd'hui l'anniversaire de %s !"
|
||||||
" Il a %s. Joyeux anniversaire :)" % (name, "%s")),
|
" Il a %s. Joyeux anniversaire :)" % (name, "%s")),
|
||||||
msg.channel)
|
msg.channel)
|
||||||
else:
|
else:
|
||||||
if tyd < datetime.today():
|
if tyd < datetime.today():
|
||||||
tyd = datetime(date.today().year + 1, tyd.month, tyd.day)
|
tyd = datetime(date.today().year + 1, tyd.month, tyd.day)
|
||||||
|
|
||||||
return Response(msg.sender, msg.countdown_format(tyd,
|
return Response(countdown_format(tyd,
|
||||||
"Il reste %s avant l'anniversaire de %s !" % ("%s",
|
"Il reste %s avant l'anniversaire de %s !" % ("%s",
|
||||||
name), ""),
|
name), ""),
|
||||||
msg.channel)
|
msg.channel)
|
||||||
else:
|
else:
|
||||||
return Response(msg.sender, "désolé, je ne connais pas la date d'anniversaire"
|
return Response("désolé, je ne connais pas la date d'anniversaire"
|
||||||
" de %s. Quand est-il né ?" % name,
|
" de %s. Quand est-il né ?" % name,
|
||||||
msg.channel, msg.nick)
|
msg.channel, msg.frm)
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("age",
|
||||||
|
help="Calculate age of known people",
|
||||||
|
help_usage={
|
||||||
|
None: "Calculate your age",
|
||||||
|
"WHO": "Calculate the age of WHO"
|
||||||
|
})
|
||||||
def cmd_age(msg):
|
def cmd_age(msg):
|
||||||
(matches, name) = findName(msg)
|
(matches, name) = findName(msg)
|
||||||
if len(matches) == 1:
|
if len(matches) == 1:
|
||||||
name = matches[0]
|
name = matches[0]
|
||||||
d = DATAS.index[name].getDate("born")
|
d = context.data.index[name].getDate("born")
|
||||||
|
|
||||||
return Response(msg.sender, msg.countdown_format(d,
|
return Response(countdown_format(d,
|
||||||
"%s va naître dans %s." % (name, "%s"),
|
"%s va naître dans %s." % (name, "%s"),
|
||||||
"%s a %s." % (name, "%s")),
|
"%s a %s." % (name, "%s")),
|
||||||
msg.channel)
|
msg.channel)
|
||||||
else:
|
else:
|
||||||
return Response(msg.sender, "désolé, je ne connais pas l'âge de %s."
|
return Response("désolé, je ne connais pas l'âge de %s."
|
||||||
" Quand est-il né ?" % name, msg.channel, msg.nick)
|
" Quand est-il né ?" % name, msg.channel, msg.frm)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
## Input parsing
|
||||||
|
|
||||||
|
@hook.ask()
|
||||||
def parseask(msg):
|
def parseask(msg):
|
||||||
msgl = msg.content.lower ()
|
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.message, re.I)
|
||||||
if re.match("^.*(date de naissance|birthday|geburtstag|née? |nee? le|born on).*$", msgl) is not None:
|
if res is not None:
|
||||||
try:
|
try:
|
||||||
extDate = msg.extractDate()
|
extDate = extractDate(msg.message)
|
||||||
if extDate is None or extDate.year > datetime.now().year:
|
if extDate is None or extDate.year > datetime.now().year:
|
||||||
return Response(msg.sender,
|
return Response("la date de naissance ne paraît pas valide...",
|
||||||
"ta date de naissance ne paraît pas valide...",
|
|
||||||
msg.channel,
|
msg.channel,
|
||||||
msg.nick)
|
msg.frm)
|
||||||
else:
|
else:
|
||||||
if msg.nick.lower() in DATAS.index:
|
nick = res.group(1)
|
||||||
DATAS.index[msg.nick.lower()]["born"] = extDate
|
if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma":
|
||||||
|
nick = msg.frm
|
||||||
|
if nick.lower() in context.data.index:
|
||||||
|
context.data.index[nick.lower()]["born"] = extDate
|
||||||
else:
|
else:
|
||||||
ms = ModuleState("birthday")
|
ms = ModuleState("birthday")
|
||||||
ms.setAttribute("name", msg.nick.lower())
|
ms.setAttribute("name", nick.lower())
|
||||||
ms.setAttribute("born", extDate)
|
ms.setAttribute("born", extDate)
|
||||||
DATAS.addChild(ms)
|
context.data.addChild(ms)
|
||||||
save()
|
context.save()
|
||||||
return Response(msg.sender,
|
return Response("ok, c'est noté, %s est né le %s"
|
||||||
"ok, c'est noté, ta date de naissance est le %s"
|
% (nick, extDate.strftime("%A %d %B %Y à %H:%M")),
|
||||||
% extDate.strftime("%A %d %B %Y à %H:%M"),
|
|
||||||
msg.channel,
|
msg.channel,
|
||||||
msg.nick)
|
msg.frm)
|
||||||
except:
|
except:
|
||||||
return Response(msg.sender, "ta date de naissance ne paraît pas valide...",
|
raise IMException("la date de naissance ne paraît pas valide.")
|
||||||
msg.channel, msg.nick)
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" ?>
|
|
||||||
<nemubotmodule name="birthday">
|
|
||||||
<message type="cmd" name="anniv" call="cmd_anniv" />
|
|
||||||
<message type="cmd" name="age" call="cmd_age" />
|
|
||||||
</nemubotmodule>
|
|
||||||
|
|
@ -1,51 +1,74 @@
|
||||||
# coding=utf-8
|
"""Wishes Happy New Year when the time comes"""
|
||||||
|
|
||||||
from datetime import datetime
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
nemubotversion = 3.3
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from nemubot.event import ModuleEvent
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools.countdown import countdown_format
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# GLOBALS #############################################################
|
||||||
|
|
||||||
|
yr = datetime.now(timezone.utc).year
|
||||||
|
yrn = datetime.now(timezone.utc).year + 1
|
||||||
|
|
||||||
|
|
||||||
|
# LOADING #############################################################
|
||||||
|
|
||||||
def load(context):
|
def load(context):
|
||||||
yr = datetime.today().year
|
if not context.config or not context.config.hasNode("sayon"):
|
||||||
yrn = datetime.today().year + 1
|
print("You can append in your configuration some balise to "
|
||||||
|
"automaticaly wish an happy new year on some channels like:\n"
|
||||||
|
"<sayon hostid=\"nemubot@irc.freenode.net:6667\" "
|
||||||
|
"channel=\"#nemutest\" />")
|
||||||
|
|
||||||
d = datetime(yrn, 1, 1, 0, 0, 0) - datetime.now()
|
def bonneannee():
|
||||||
# d = datetime(yr, 12, 31, 19, 34, 0) - datetime.now()
|
txt = "Bonne année %d !" % yrn
|
||||||
add_event(ModuleEvent(intervalle=0, offset=d.total_seconds(), call=bonneannee))
|
print(txt)
|
||||||
|
if context.config and context.config.hasNode("sayon"):
|
||||||
|
for sayon in context.config.getNodes("sayon"):
|
||||||
|
if "hostid" not in sayon or "channel" not in sayon:
|
||||||
|
print("Error: missing hostif or channel")
|
||||||
|
continue
|
||||||
|
srv = sayon["hostid"]
|
||||||
|
chan = sayon["channel"]
|
||||||
|
context.send_response(srv, Response(txt, chan))
|
||||||
|
|
||||||
from hooks import Hook
|
d = datetime(yrn, 1, 1, 0, 0, 0, 0,
|
||||||
add_hook("cmd_rgxp", Hook(cmd_timetoyear, data=yrn, regexp="^[0-9]{4}$"))
|
timezone.utc) - datetime.now(timezone.utc)
|
||||||
add_hook("cmd_hook", Hook(cmd_newyear, str(yrn), yrn))
|
context.add_event(ModuleEvent(interval=0, offset=d.total_seconds(),
|
||||||
add_hook("cmd_hook", Hook(cmd_newyear, "ny", yrn))
|
call=bonneannee))
|
||||||
add_hook("cmd_hook", Hook(cmd_newyear, "newyear", yrn))
|
|
||||||
add_hook("cmd_hook", Hook(cmd_newyear, "new-year", yrn))
|
|
||||||
add_hook("cmd_hook", Hook(cmd_newyear, "new year", yrn))
|
|
||||||
|
|
||||||
def bonneannee():
|
|
||||||
txt = "Bonne année %d !" % datetime.today().year
|
|
||||||
print (txt)
|
|
||||||
send_response("localhost:2771", Response(None, txt, "#epitagueule"))
|
|
||||||
send_response("localhost:2771", Response(None, txt, "#yaka"))
|
|
||||||
send_response("localhost:2771", Response(None, txt, "#epita2014"))
|
|
||||||
send_response("localhost:2771", Response(None, txt, "#ykar"))
|
|
||||||
send_response("localhost:2771", Response(None, txt, "#ordissimo"))
|
|
||||||
send_response("localhost:2771", Response(None, txt, "#42sh"))
|
|
||||||
send_response("localhost:2771", Response(None, txt, "#nemubot"))
|
|
||||||
|
|
||||||
def cmd_newyear(msg, yr):
|
# MODULE INTERFACE ####################################################
|
||||||
return Response(msg.sender,
|
|
||||||
msg.countdown_format(datetime(yr, 1, 1, 0, 0, 1),
|
@hook.command("newyear",
|
||||||
"Il reste %s avant la nouvelle année.",
|
help="Display the remaining time before the next new year")
|
||||||
"Nous faisons déjà la fête depuis %s !"),
|
@hook.command(str(yrn),
|
||||||
|
help="Display the remaining time before %d" % yrn)
|
||||||
|
def cmd_newyear(msg):
|
||||||
|
return Response(countdown_format(datetime(yrn, 1, 1, 0, 0, 1, 0,
|
||||||
|
timezone.utc),
|
||||||
|
"Il reste %s avant la nouvelle année.",
|
||||||
|
"Nous faisons déjà la fête depuis %s !"),
|
||||||
channel=msg.channel)
|
channel=msg.channel)
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command(data=yrn, regexp="^[0-9]{4}$",
|
||||||
|
help="Calculate time remaining/passed before/since the requested year")
|
||||||
def cmd_timetoyear(msg, cur):
|
def cmd_timetoyear(msg, cur):
|
||||||
yr = int(msg.cmds[0])
|
yr = int(msg.cmd)
|
||||||
|
|
||||||
if yr == cur:
|
if yr == cur:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return Response(msg.sender,
|
return Response(countdown_format(datetime(yr, 1, 1, 0, 0, 1, 0,
|
||||||
msg.countdown_format(datetime(yr, 1, 1, 0, 0, 1),
|
timezone.utc),
|
||||||
"Il reste %s avant %d." % ("%s", yr),
|
"Il reste %s avant %d." % ("%s", yr),
|
||||||
"Le premier janvier %d est passé depuis %s !" % (yr, "%s")),
|
"Le premier janvier %d est passé "
|
||||||
|
"depuis %s !" % (yr, "%s")),
|
||||||
channel=msg.channel)
|
channel=msg.channel)
|
||||||
|
|
|
||||||
115
modules/books.py
Normal file
115
modules/books.py
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
"""Looking for books"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
from nemubot import context
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools import web
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# LOADING #############################################################
|
||||||
|
|
||||||
|
def load(context):
|
||||||
|
if not context.config or "goodreadskey" not in context.config:
|
||||||
|
raise ImportError("You need a Goodreads API key in order to use this "
|
||||||
|
"module. Add it to the module configuration file:\n"
|
||||||
|
"<module name=\"books\" goodreadskey=\"XXXXXX\" />\n"
|
||||||
|
"Get one at https://www.goodreads.com/api/keys")
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
def get_book(title):
|
||||||
|
"""Retrieve a book from its title"""
|
||||||
|
response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" %
|
||||||
|
(context.config["goodreadskey"], urllib.parse.quote(title)))
|
||||||
|
if response is not None and len(response.getElementsByTagName("book")):
|
||||||
|
return response.getElementsByTagName("book")[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def search_books(title):
|
||||||
|
"""Get a list of book matching given title"""
|
||||||
|
response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" %
|
||||||
|
(context.config["goodreadskey"], urllib.parse.quote(title)))
|
||||||
|
if response is not None and len(response.getElementsByTagName("search")):
|
||||||
|
return response.getElementsByTagName("search")[0].getElementsByTagName("results")[0].getElementsByTagName("work")
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def search_author(name):
|
||||||
|
"""Looking for an author"""
|
||||||
|
response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" %
|
||||||
|
(urllib.parse.quote(name), context.config["goodreadskey"]))
|
||||||
|
if response is not None and len(response.getElementsByTagName("author")) and response.getElementsByTagName("author")[0].hasAttribute("id"):
|
||||||
|
response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" %
|
||||||
|
(urllib.parse.quote(response.getElementsByTagName("author")[0].getAttribute("id")), context.config["goodreadskey"]))
|
||||||
|
if response is not None and len(response.getElementsByTagName("author")):
|
||||||
|
return response.getElementsByTagName("author")[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("book",
|
||||||
|
help="Get information about a book from its title",
|
||||||
|
help_usage={
|
||||||
|
"TITLE": "Get information about a book titled TITLE"
|
||||||
|
})
|
||||||
|
def cmd_book(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("please give me a title to search")
|
||||||
|
|
||||||
|
book = get_book(" ".join(msg.args))
|
||||||
|
if book is None:
|
||||||
|
raise IMException("unable to find book named like this")
|
||||||
|
res = Response(channel=msg.channel)
|
||||||
|
res.append_message("%s, written by %s: %s" % (book.getElementsByTagName("title")[0].firstChild.nodeValue,
|
||||||
|
book.getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue,
|
||||||
|
web.striphtml(book.getElementsByTagName("description")[0].firstChild.nodeValue if book.getElementsByTagName("description")[0].firstChild else "")))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("search_books",
|
||||||
|
help="Search book's title",
|
||||||
|
help_usage={
|
||||||
|
"APPROX_TITLE": "Search for a book approximately titled APPROX_TITLE"
|
||||||
|
})
|
||||||
|
def cmd_books(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("please give me a title to search")
|
||||||
|
|
||||||
|
title = " ".join(msg.args)
|
||||||
|
res = Response(channel=msg.channel,
|
||||||
|
title="%s" % (title),
|
||||||
|
count=" (%d more books)")
|
||||||
|
|
||||||
|
for book in search_books(title):
|
||||||
|
res.append_message("%s, writed by %s" % (book.getElementsByTagName("best_book")[0].getElementsByTagName("title")[0].firstChild.nodeValue,
|
||||||
|
book.getElementsByTagName("best_book")[0].getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("author_books",
|
||||||
|
help="Looking for books writen by a given author",
|
||||||
|
help_usage={
|
||||||
|
"AUTHOR": "Looking for books writen by AUTHOR"
|
||||||
|
})
|
||||||
|
def cmd_author(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("please give me an author to search")
|
||||||
|
|
||||||
|
name = " ".join(msg.args)
|
||||||
|
ath = search_author(name)
|
||||||
|
if ath is None:
|
||||||
|
raise IMException("%s does not appear to be a published author." % name)
|
||||||
|
return Response([b.getElementsByTagName("title")[0].firstChild.nodeValue for b in ath.getElementsByTagName("book")],
|
||||||
|
channel=msg.channel,
|
||||||
|
title=ath.getElementsByTagName("name")[0].firstChild.nodeValue)
|
||||||
55
modules/cat.py
Normal file
55
modules/cat.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
"""Concatenate commands"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
from nemubot import context
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.message import Command, DirectAsk, Text
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
def cat(msg, *terms):
|
||||||
|
res = Response(channel=msg.to_response, server=msg.server)
|
||||||
|
for term in terms:
|
||||||
|
m = context.subparse(msg, term)
|
||||||
|
if isinstance(m, Command) or isinstance(m, DirectAsk):
|
||||||
|
for r in context.subtreat(m):
|
||||||
|
if isinstance(r, Response):
|
||||||
|
for t in range(len(r.messages)):
|
||||||
|
res.append_message(r.messages[t],
|
||||||
|
title=r.rawtitle if not isinstance(r.rawtitle, list) else r.rawtitle[t])
|
||||||
|
|
||||||
|
elif isinstance(r, Text):
|
||||||
|
res.append_message(r.message)
|
||||||
|
|
||||||
|
elif isinstance(r, str):
|
||||||
|
res.append_message(r)
|
||||||
|
|
||||||
|
else:
|
||||||
|
res.append_message(term)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("cat",
|
||||||
|
help="Concatenate responses of commands given as argument",
|
||||||
|
help_usage={"!SUBCMD [!SUBCMD [...]]": "Concatenate response of subcommands"},
|
||||||
|
keywords={
|
||||||
|
"merge": "Merge messages into the same",
|
||||||
|
})
|
||||||
|
def cmd_cat(msg):
|
||||||
|
if len(msg.args) < 1:
|
||||||
|
raise IMException("No subcommand to concatenate")
|
||||||
|
|
||||||
|
r = cat(msg, *msg.args)
|
||||||
|
|
||||||
|
if "merge" in msg.kwargs and len(r.messages) > 1:
|
||||||
|
r.messages = [ r.messages ]
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from datetime import timedelta
|
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
from tools import web
|
|
||||||
|
|
||||||
nemubotversion = 3.3
|
|
||||||
|
|
||||||
def help_tiny ():
|
|
||||||
"""Line inserted in the response to the command !help"""
|
|
||||||
return "Gets informations about current and next Épita courses"
|
|
||||||
|
|
||||||
def help_full ():
|
|
||||||
return "!chronos [spé] : gives current and next courses."
|
|
||||||
|
|
||||||
|
|
||||||
def get_courses(classe=None, room=None, teacher=None, date=None):
|
|
||||||
url = CONF.getNode("server")["url"]
|
|
||||||
if classe is not None:
|
|
||||||
url += "&class=" + quote(classe)
|
|
||||||
if room is not None:
|
|
||||||
url += "&room=" + quote(room)
|
|
||||||
if teacher is not None:
|
|
||||||
url += "&teacher=" + quote(teacher)
|
|
||||||
#TODO: date, not implemented at 23.tf
|
|
||||||
|
|
||||||
print_debug(url)
|
|
||||||
response = web.getXML(url)
|
|
||||||
if response is not None:
|
|
||||||
print_debug(response)
|
|
||||||
return response.getNodes("course")
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_next_courses(classe=None, room=None, teacher=None, date=None):
|
|
||||||
courses = get_courses(classe, room, teacher, date)
|
|
||||||
now = datetime.now()
|
|
||||||
for c in courses:
|
|
||||||
start = c.getFirstNode("start").getDate()
|
|
||||||
|
|
||||||
if now > start:
|
|
||||||
return c
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_near_courses(classe=None, room=None, teacher=None, date=None):
|
|
||||||
courses = get_courses(classe, room, teacher, date)
|
|
||||||
return courses[0]
|
|
||||||
|
|
||||||
def cmd_chronos(msg):
|
|
||||||
if len(msg.cmds) > 1:
|
|
||||||
classe = msg.cmds[1]
|
|
||||||
else:
|
|
||||||
classe = ""
|
|
||||||
|
|
||||||
res = Response(msg.sender, channel=msg.channel, nomore="Je n'ai pas d'autre cours à afficher")
|
|
||||||
|
|
||||||
courses = get_courses(classe)
|
|
||||||
print_debug(courses)
|
|
||||||
if courses is not None:
|
|
||||||
now = datetime.now()
|
|
||||||
tomorrow = now + timedelta(days=1)
|
|
||||||
for c in courses:
|
|
||||||
idc = c.getFirstNode("id").getContent()
|
|
||||||
crs = c.getFirstNode("title").getContent()
|
|
||||||
start = c.getFirstNode("start").getDate()
|
|
||||||
end = c.getFirstNode("end").getDate()
|
|
||||||
where = c.getFirstNode("where").getContent()
|
|
||||||
teacher = c.getFirstNode("teacher").getContent()
|
|
||||||
students = c.getFirstNode("students").getContent()
|
|
||||||
|
|
||||||
if now > start:
|
|
||||||
title = "Actuellement "
|
|
||||||
msg = "\x03\x02" + crs + "\x03\x02 jusqu'"
|
|
||||||
if end < tomorrow:
|
|
||||||
msg += "à \x03\x02" + end.strftime("%H:%M")
|
|
||||||
else:
|
|
||||||
msg += "au \x03\x02" + end.strftime("%a %d à %H:%M")
|
|
||||||
msg += "\x03\x02 en \x03\x02" + where + "\x03\x02"
|
|
||||||
else:
|
|
||||||
title = "Prochainement "
|
|
||||||
duration = (end - start).total_seconds() / 60
|
|
||||||
|
|
||||||
msg = "\x03\x02" + crs + "\x03\x02 le \x03\x02" + start.strftime("%a %d à %H:%M") + "\x03\x02 pour " + "%dh%02d" % (int(duration / 60), duration % 60) + " en \x03\x02" + where + "\x03\x02"
|
|
||||||
|
|
||||||
if teacher != "":
|
|
||||||
msg += " avec " + teacher
|
|
||||||
if students != "":
|
|
||||||
msg += " pour les " + students
|
|
||||||
|
|
||||||
res.append_message(msg, title)
|
|
||||||
else:
|
|
||||||
res.append_message("Aucun cours n'a été trouvé")
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" ?>
|
|
||||||
<nemubotmodule name="chronos">
|
|
||||||
<server url="http://chronos.23.tf/index.php?xml" />
|
|
||||||
<message type="cmd" name="chronos" call="cmd_chronos" />
|
|
||||||
<message type="cmd" name="Χρονος" call="cmd_chronos" />
|
|
||||||
</nemubotmodule>
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
||||||
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from networkbot import NetworkBot
|
|
||||||
|
|
||||||
nemubotversion = 3.3
|
|
||||||
NODATA = True
|
|
||||||
|
|
||||||
def getserver(toks, context, prompt):
|
|
||||||
"""Choose the server in toks or prompt"""
|
|
||||||
if len(toks) > 1 and toks[0] in context.servers:
|
|
||||||
return (context.servers[toks[0]], toks[1:])
|
|
||||||
elif prompt.selectedServer is not None:
|
|
||||||
return (prompt.selectedServer, toks)
|
|
||||||
else:
|
|
||||||
return (None, toks)
|
|
||||||
|
|
||||||
def close(data, toks, context, prompt):
|
|
||||||
"""Disconnect and forget (remove from the servers list) the server"""
|
|
||||||
if len(toks) > 1:
|
|
||||||
for s in toks[1:]:
|
|
||||||
if s in servers:
|
|
||||||
context.servers[s].disconnect()
|
|
||||||
del context.servers[s]
|
|
||||||
else:
|
|
||||||
print ("close: server `%s' not found." % s)
|
|
||||||
elif prompt.selectedServer is not None:
|
|
||||||
prompt.selectedServer.disconnect()
|
|
||||||
del prompt.servers[selectedServer.id]
|
|
||||||
prompt.selectedServer = None
|
|
||||||
return
|
|
||||||
|
|
||||||
def connect(data, toks, context, prompt):
|
|
||||||
"""Make the connexion to a server"""
|
|
||||||
if len(toks) > 1:
|
|
||||||
for s in toks[1:]:
|
|
||||||
if s in context.servers:
|
|
||||||
context.servers[s].launch(context.receive_message)
|
|
||||||
else:
|
|
||||||
print ("connect: server `%s' not found." % s)
|
|
||||||
|
|
||||||
elif prompt.selectedServer is not None:
|
|
||||||
prompt.selectedServer.launch(context.receive_message)
|
|
||||||
else:
|
|
||||||
print (" Please SELECT a server or give its name in argument.")
|
|
||||||
|
|
||||||
def disconnect(data, toks, context, prompt):
|
|
||||||
"""Close the connection to a server"""
|
|
||||||
if len(toks) > 1:
|
|
||||||
for s in toks[1:]:
|
|
||||||
if s in context.servers:
|
|
||||||
if not context.servers[s].disconnect():
|
|
||||||
print ("disconnect: server `%s' already disconnected." % s)
|
|
||||||
else:
|
|
||||||
print ("disconnect: server `%s' not found." % s)
|
|
||||||
elif prompt.selectedServer is not None:
|
|
||||||
if not prompt.selectedServer.disconnect():
|
|
||||||
print ("disconnect: server `%s' already disconnected."
|
|
||||||
% prompt.selectedServer.id)
|
|
||||||
else:
|
|
||||||
print (" Please SELECT a server or give its name in argument.")
|
|
||||||
|
|
||||||
def discover(data, toks, context, prompt):
|
|
||||||
"""Discover a new bot on a server"""
|
|
||||||
(srv, toks) = getserver(toks, context, prompt)
|
|
||||||
if srv is not None:
|
|
||||||
for name in toks[1:]:
|
|
||||||
if "!" in name:
|
|
||||||
bot = context.add_networkbot(srv, name)
|
|
||||||
bot.connect()
|
|
||||||
else:
|
|
||||||
print (" %s is not a valid fullname, for example: nemubot!nemubotV3@bot.nemunai.re")
|
|
||||||
else:
|
|
||||||
print (" Please SELECT a server or give its name in first argument.")
|
|
||||||
|
|
||||||
def hotswap(data, toks, context, prompt):
|
|
||||||
"""Reload a server class"""
|
|
||||||
if len(toks) > 1:
|
|
||||||
print ("hotswap: apply only on selected server")
|
|
||||||
elif prompt.selectedServer is not None:
|
|
||||||
del context.servers[prompt.selectedServer.id]
|
|
||||||
srv = server.Server(selectedServer.node, selectedServer.nick,
|
|
||||||
selectedServer.owner, selectedServer.realname,
|
|
||||||
selectedServer.s)
|
|
||||||
context.servers[srv.id] = srv
|
|
||||||
prompt.selectedServer.kill()
|
|
||||||
prompt.selectedServer = srv
|
|
||||||
prompt.selectedServer.start()
|
|
||||||
else:
|
|
||||||
print (" Please SELECT a server or give its name in argument.")
|
|
||||||
|
|
||||||
def join(data, toks, context, prompt):
|
|
||||||
"""Join or leave a channel"""
|
|
||||||
rd = 1
|
|
||||||
if len(toks) <= rd:
|
|
||||||
print ("%s: not enough arguments." % toks[0])
|
|
||||||
return
|
|
||||||
|
|
||||||
if toks[rd] in context.servers:
|
|
||||||
srv = context.servers[toks[rd]]
|
|
||||||
rd += 1
|
|
||||||
elif prompt.selectedServer is not None:
|
|
||||||
srv = prompt.selectedServer
|
|
||||||
else:
|
|
||||||
print (" Please SELECT a server or give its name in argument.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(toks) <= rd:
|
|
||||||
print ("%s: not enough arguments." % toks[0])
|
|
||||||
return
|
|
||||||
|
|
||||||
if toks[0] == "join":
|
|
||||||
if len(toks) > rd + 1:
|
|
||||||
srv.join(toks[rd], toks[rd + 1])
|
|
||||||
else:
|
|
||||||
srv.join(toks[rd])
|
|
||||||
elif toks[0] == "leave" or toks[0] == "part":
|
|
||||||
srv.leave(toks[rd])
|
|
||||||
return
|
|
||||||
|
|
||||||
def save_mod(data, toks, context, prompt):
|
|
||||||
"""Force save module data"""
|
|
||||||
if len(toks) < 2:
|
|
||||||
print ("save: not enough arguments.")
|
|
||||||
return
|
|
||||||
|
|
||||||
for mod in toks[1:]:
|
|
||||||
if mod in context.modules:
|
|
||||||
context.modules[mod].save()
|
|
||||||
print ("save: module `%s´ saved successfully" % mod)
|
|
||||||
else:
|
|
||||||
print ("save: no module named `%s´" % mod)
|
|
||||||
return
|
|
||||||
|
|
||||||
def send(data, toks, context, prompt):
|
|
||||||
"""Send a message on a channel"""
|
|
||||||
rd = 1
|
|
||||||
if len(toks) <= rd:
|
|
||||||
print ("send: not enough arguments.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if toks[rd] in context.servers:
|
|
||||||
srv = context.servers[toks[rd]]
|
|
||||||
rd += 1
|
|
||||||
elif prompt.selectedServer is not None:
|
|
||||||
srv = prompt.selectedServer
|
|
||||||
else:
|
|
||||||
print (" Please SELECT a server or give its name in argument.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(toks) <= rd:
|
|
||||||
print ("send: not enough arguments.")
|
|
||||||
return
|
|
||||||
|
|
||||||
#Check the server is connected
|
|
||||||
if not srv.connected:
|
|
||||||
print ("send: server `%s' not connected." % srv.id)
|
|
||||||
return
|
|
||||||
|
|
||||||
if toks[rd] in srv.channels:
|
|
||||||
chan = toks[rd]
|
|
||||||
rd += 1
|
|
||||||
else:
|
|
||||||
print ("send: channel `%s' not authorized in server `%s'."
|
|
||||||
% (toks[rd], srv.id))
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(toks) <= rd:
|
|
||||||
print ("send: not enough arguments.")
|
|
||||||
return
|
|
||||||
|
|
||||||
srv.send_msg_final(chan, toks[rd])
|
|
||||||
return "done"
|
|
||||||
|
|
||||||
def zap(data, toks, context, prompt):
|
|
||||||
"""Hard change connexion state"""
|
|
||||||
if len(toks) > 1:
|
|
||||||
for s in toks[1:]:
|
|
||||||
if s in context.servers:
|
|
||||||
context.servers[s].connected = not context.servers[s].connected
|
|
||||||
else:
|
|
||||||
print ("zap: server `%s' not found." % s)
|
|
||||||
elif prompt.selectedServer is not None:
|
|
||||||
prompt.selectedServer.connected = not prompt.selectedServer.connected
|
|
||||||
else:
|
|
||||||
print (" Please SELECT a server or give its name in argument.")
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" ?>
|
|
||||||
<nemubotmodule name="cmd_server">
|
|
||||||
<command name="close" call="close" />
|
|
||||||
<command name="connect" call="connect" />
|
|
||||||
<command name="discover" call="discover" />
|
|
||||||
<command name="disconnect" call="disconnect" />
|
|
||||||
<command name="hotswap" call="hotswap" />
|
|
||||||
<command name="join" call="join" />
|
|
||||||
<command name="leave" call="join" />
|
|
||||||
<command name="part" call="join" />
|
|
||||||
<command name="save" call="save_mod" />
|
|
||||||
<command name="send" call="send" />
|
|
||||||
<command name="zap" call="zap" />
|
|
||||||
</nemubotmodule>
|
|
||||||
94
modules/conjugaison.py
Normal file
94
modules/conjugaison.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
"""Find french conjugaison"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
import re
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools import web
|
||||||
|
from nemubot.tools.web import striphtml
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# GLOBALS #############################################################
|
||||||
|
|
||||||
|
s = [('present', '0'), ('présent', '0'), ('pr', '0'),
|
||||||
|
('passé simple', '12'), ('passe simple', '12'), ('ps', '12'),
|
||||||
|
('passé antérieur', '112'), ('passe anterieur', '112'), ('pa', '112'),
|
||||||
|
('passé composé', '100'), ('passe compose', '100'), ('pc', '100'),
|
||||||
|
('futur', '18'), ('f', '18'),
|
||||||
|
('futur antérieur', '118'), ('futur anterieur', '118'), ('fa', '118'),
|
||||||
|
('subjonctif présent', '24'), ('subjonctif present', '24'), ('spr', '24'),
|
||||||
|
('subjonctif passé', '124'), ('subjonctif passe', '124'), ('spa', '124'),
|
||||||
|
('plus que parfait', '106'), ('pqp', '106'),
|
||||||
|
('imparfait', '6'), ('ii', '6')]
|
||||||
|
|
||||||
|
d = defaultdict(list)
|
||||||
|
|
||||||
|
for k, v in s:
|
||||||
|
d[k].append(v)
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
def get_conjug(verb, stringTens):
|
||||||
|
url = ("https://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" %
|
||||||
|
quote(verb.encode("ISO-8859-1")))
|
||||||
|
page = web.getURLContent(url)
|
||||||
|
|
||||||
|
if page is not None:
|
||||||
|
for line in page.split("\n"):
|
||||||
|
if re.search('<div class="modeBloc">', line) is not None:
|
||||||
|
return compute_line(line, stringTens)
|
||||||
|
return list()
|
||||||
|
|
||||||
|
|
||||||
|
def compute_line(line, stringTens):
|
||||||
|
try:
|
||||||
|
idTemps = d[stringTens]
|
||||||
|
except:
|
||||||
|
raise IMException("le temps demandé n'existe pas")
|
||||||
|
|
||||||
|
if len(idTemps) == 0:
|
||||||
|
raise IMException("le temps demandé n'existe pas")
|
||||||
|
|
||||||
|
index = line.index('<div id="temps' + idTemps[0] + '\"')
|
||||||
|
endIndex = line[index:].index('<div class=\"conjugBloc\"')
|
||||||
|
|
||||||
|
endIndex += index
|
||||||
|
newLine = line[index:endIndex]
|
||||||
|
|
||||||
|
res = list()
|
||||||
|
for elt in re.finditer("[p|/]>([^/]*/b>)", newLine):
|
||||||
|
res.append(striphtml(elt.group(1)
|
||||||
|
.replace("<b>", "\x02")
|
||||||
|
.replace("</b>", "\x0F")))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("conjugaison",
|
||||||
|
help_usage={
|
||||||
|
"TENS VERB": "give the conjugaison for VERB in TENS."
|
||||||
|
})
|
||||||
|
def cmd_conjug(msg):
|
||||||
|
if len(msg.args) < 2:
|
||||||
|
raise IMException("donne moi un temps et un verbe, et je te donnerai "
|
||||||
|
"sa conjugaison!")
|
||||||
|
|
||||||
|
tens = ' '.join(msg.args[:-1])
|
||||||
|
|
||||||
|
verb = msg.args[-1]
|
||||||
|
|
||||||
|
conjug = get_conjug(verb, tens)
|
||||||
|
|
||||||
|
if len(conjug) > 0:
|
||||||
|
return Response(conjug, channel=msg.channel,
|
||||||
|
title="Conjugaison de %s" % verb)
|
||||||
|
else:
|
||||||
|
raise IMException("aucune conjugaison de '%s' n'a été trouvé" % verb)
|
||||||
32
modules/ctfs.py
Normal file
32
modules/ctfs.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
"""List upcoming CTFs"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools.web import getURLContent, striphtml
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# GLOBALS #############################################################
|
||||||
|
|
||||||
|
URL = 'https://ctftime.org/event/list/upcoming'
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("ctfs",
|
||||||
|
help="Display the upcoming CTFs")
|
||||||
|
def get_info_yt(msg):
|
||||||
|
soup = BeautifulSoup(getURLContent(URL))
|
||||||
|
|
||||||
|
res = Response(channel=msg.channel, nomore="No more upcoming CTF")
|
||||||
|
|
||||||
|
for line in soup.body.find_all('tr'):
|
||||||
|
n = line.find_all('td')
|
||||||
|
if len(n) == 7:
|
||||||
|
res.append_message("\x02%s:\x0F from %s type %s at %s. Weight: %s. %s%s" %
|
||||||
|
tuple([striphtml(x.text).strip() for x in n]))
|
||||||
|
|
||||||
|
return res
|
||||||
99
modules/cve.py
Normal file
99
modules/cve.py
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
"""Read CVE in your IM client"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools.web import getURLContent, striphtml
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/'
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
VULN_DATAS = {
|
||||||
|
"alert-title": "vuln-warning-status-name",
|
||||||
|
"alert-content": "vuln-warning-banner-content",
|
||||||
|
|
||||||
|
"description": "vuln-description",
|
||||||
|
"published": "vuln-published-on",
|
||||||
|
"last_modified": "vuln-last-modified-on",
|
||||||
|
|
||||||
|
"base_score": "vuln-cvssv3-base-score-link",
|
||||||
|
"severity": "vuln-cvssv3-base-score-severity",
|
||||||
|
"impact_score": "vuln-cvssv3-impact-score",
|
||||||
|
"exploitability_score": "vuln-cvssv3-exploitability-score",
|
||||||
|
|
||||||
|
"av": "vuln-cvssv3-av",
|
||||||
|
"ac": "vuln-cvssv3-ac",
|
||||||
|
"pr": "vuln-cvssv3-pr",
|
||||||
|
"ui": "vuln-cvssv3-ui",
|
||||||
|
"s": "vuln-cvssv3-s",
|
||||||
|
"c": "vuln-cvssv3-c",
|
||||||
|
"i": "vuln-cvssv3-i",
|
||||||
|
"a": "vuln-cvssv3-a",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_cve(cve_id):
|
||||||
|
search_url = BASEURL_NIST + quote(cve_id.upper())
|
||||||
|
|
||||||
|
soup = BeautifulSoup(getURLContent(search_url))
|
||||||
|
|
||||||
|
vuln = {}
|
||||||
|
|
||||||
|
for vd in VULN_DATAS:
|
||||||
|
r = soup.body.find(attrs={"data-testid": VULN_DATAS[vd]})
|
||||||
|
if r:
|
||||||
|
vuln[vd] = r.text.strip()
|
||||||
|
|
||||||
|
return vuln
|
||||||
|
|
||||||
|
|
||||||
|
def display_metrics(av, ac, pr, ui, s, c, i, a, **kwargs):
|
||||||
|
ret = []
|
||||||
|
if av != "None": ret.append("Attack Vector: \x02%s\x0F" % av)
|
||||||
|
if ac != "None": ret.append("Attack Complexity: \x02%s\x0F" % ac)
|
||||||
|
if pr != "None": ret.append("Privileges Required: \x02%s\x0F" % pr)
|
||||||
|
if ui != "None": ret.append("User Interaction: \x02%s\x0F" % ui)
|
||||||
|
if s != "Unchanged": ret.append("Scope: \x02%s\x0F" % s)
|
||||||
|
if c != "None": ret.append("Confidentiality: \x02%s\x0F" % c)
|
||||||
|
if i != "None": ret.append("Integrity: \x02%s\x0F" % i)
|
||||||
|
if a != "None": ret.append("Availability: \x02%s\x0F" % a)
|
||||||
|
return ', '.join(ret)
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("cve",
|
||||||
|
help="Display given CVE",
|
||||||
|
help_usage={"CVE_ID": "Display the description of the given CVE"})
|
||||||
|
def get_cve_desc(msg):
|
||||||
|
res = Response(channel=msg.channel)
|
||||||
|
|
||||||
|
for cve_id in msg.args:
|
||||||
|
if cve_id[:3].lower() != 'cve':
|
||||||
|
cve_id = 'cve-' + cve_id
|
||||||
|
|
||||||
|
cve = get_cve(cve_id)
|
||||||
|
if not cve:
|
||||||
|
raise IMException("CVE %s doesn't exists." % cve_id)
|
||||||
|
|
||||||
|
if "alert-title" in cve or "alert-content" in cve:
|
||||||
|
alert = "\x02%s:\x0F %s " % (cve["alert-title"] if "alert-title" in cve else "",
|
||||||
|
cve["alert-content"] if "alert-content" in cve else "")
|
||||||
|
else:
|
||||||
|
alert = ""
|
||||||
|
|
||||||
|
if "base_score" not in cve and "description" in cve:
|
||||||
|
res.append_message("{alert}Last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, **cve), title=cve_id)
|
||||||
|
else:
|
||||||
|
metrics = display_metrics(**cve)
|
||||||
|
res.append_message("{alert}Base score: \x02{base_score} {severity}\x0F (impact: \x02{impact_score}\x0F, exploitability: \x02{exploitability_score}\x0F; {metrics}), last modified on \x02{last_modified}\x0F. {description}".format(alert=alert, metrics=metrics, **cve), title=cve_id)
|
||||||
|
|
||||||
|
return res
|
||||||
138
modules/ddg.py
Normal file
138
modules/ddg.py
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
"""Search around DuckDuckGo search engine"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools import web
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
def do_search(terms):
|
||||||
|
if "!safeoff" in terms:
|
||||||
|
terms.remove("!safeoff")
|
||||||
|
safeoff = True
|
||||||
|
else:
|
||||||
|
safeoff = False
|
||||||
|
|
||||||
|
sterm = " ".join(terms)
|
||||||
|
return DDGResult(sterm, web.getJSON(
|
||||||
|
"https://api.duckduckgo.com/?q=%s&format=json&no_redirect=1%s" %
|
||||||
|
(quote(sterm), "&kp=-1" if safeoff else "")))
|
||||||
|
|
||||||
|
|
||||||
|
class DDGResult:
|
||||||
|
|
||||||
|
def __init__(self, terms, res):
|
||||||
|
if res is None:
|
||||||
|
raise IMException("An error occurs during search")
|
||||||
|
|
||||||
|
self.terms = terms
|
||||||
|
self.ddgres = res
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
if not self.ddgres or "Type" not in self.ddgres:
|
||||||
|
return ""
|
||||||
|
return self.ddgres["Type"]
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def definition(self):
|
||||||
|
if "Definition" not in self.ddgres or not self.ddgres["Definition"]:
|
||||||
|
return None
|
||||||
|
return self.ddgres["Definition"] + " <" + self.ddgres["DefinitionURL"] + "> from " + self.ddgres["DefinitionSource"]
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def relatedTopics(self):
|
||||||
|
if "RelatedTopics" in self.ddgres:
|
||||||
|
for rt in self.ddgres["RelatedTopics"]:
|
||||||
|
if "Text" in rt:
|
||||||
|
yield rt["Text"] + " <" + rt["FirstURL"] + ">"
|
||||||
|
elif "Topics" in rt:
|
||||||
|
yield rt["Name"] + ": " + "; ".join([srt["Text"] + " <" + srt["FirstURL"] + ">" for srt in rt["Topics"]])
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def redirect(self):
|
||||||
|
if "Redirect" not in self.ddgres or not self.ddgres["Redirect"]:
|
||||||
|
return None
|
||||||
|
return self.ddgres["Redirect"]
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity(self):
|
||||||
|
if "Entity" not in self.ddgres or not self.ddgres["Entity"]:
|
||||||
|
return None
|
||||||
|
return self.ddgres["Entity"]
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def heading(self):
|
||||||
|
if "Heading" not in self.ddgres or not self.ddgres["Heading"]:
|
||||||
|
return " ".join(self.terms)
|
||||||
|
return self.ddgres["Heading"]
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def result(self):
|
||||||
|
if "Results" in self.ddgres:
|
||||||
|
for res in self.ddgres["Results"]:
|
||||||
|
yield res["Text"] + " <" + res["FirstURL"] + ">"
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def answer(self):
|
||||||
|
if "Answer" not in self.ddgres or not self.ddgres["Answer"]:
|
||||||
|
return None
|
||||||
|
return web.striphtml(self.ddgres["Answer"])
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def abstract(self):
|
||||||
|
if "Abstract" not in self.ddgres or not self.ddgres["Abstract"]:
|
||||||
|
return None
|
||||||
|
return self.ddgres["AbstractText"] + " <" + self.ddgres["AbstractURL"] + "> from " + self.ddgres["AbstractSource"]
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("define")
|
||||||
|
def define(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("Indicate a term to define")
|
||||||
|
|
||||||
|
s = do_search(msg.args)
|
||||||
|
|
||||||
|
if not s.definition:
|
||||||
|
raise IMException("no definition found for '%s'." % " ".join(msg.args))
|
||||||
|
|
||||||
|
return Response(s.definition, channel=msg.channel)
|
||||||
|
|
||||||
|
@hook.command("search")
|
||||||
|
def search(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("Indicate a term to search")
|
||||||
|
|
||||||
|
s = do_search(msg.args)
|
||||||
|
|
||||||
|
res = Response(channel=msg.channel, nomore="No more results",
|
||||||
|
count=" (%d more results)")
|
||||||
|
|
||||||
|
res.append_message(s.redirect)
|
||||||
|
res.append_message(s.answer)
|
||||||
|
res.append_message(s.abstract)
|
||||||
|
res.append_message([r for r in s.result])
|
||||||
|
|
||||||
|
for rt in s.relatedTopics:
|
||||||
|
res.append_message(rt)
|
||||||
|
|
||||||
|
res.append_message(s.definition)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
from urllib.parse import quote
|
|
||||||
from urllib.request import urlopen
|
|
||||||
|
|
||||||
import xmlparser
|
|
||||||
from tools import web
|
|
||||||
|
|
||||||
class DDGSearch:
|
|
||||||
def __init__(self, terms):
|
|
||||||
self.terms = terms
|
|
||||||
|
|
||||||
raw = urlopen("https://api.duckduckgo.com/?q=%s&format=xml" % quote(terms), timeout=10)
|
|
||||||
self.ddgres = xmlparser.parse_string(raw.read())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self):
|
|
||||||
if self.ddgres and self.ddgres.hasNode("Type"):
|
|
||||||
return self.ddgres.getFirstNode("Type").getContent()
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def definition(self):
|
|
||||||
if self.ddgres.hasNode("Definition"):
|
|
||||||
return self.ddgres.getFirstNode("Definition").getContent()
|
|
||||||
else:
|
|
||||||
return "Sorry, no definition found for %s" % self.terms
|
|
||||||
|
|
||||||
@property
|
|
||||||
def relatedTopics(self):
|
|
||||||
try:
|
|
||||||
for rt in self.ddgres.getFirstNode("RelatedTopics").getNodes("RelatedTopic"):
|
|
||||||
yield rt.getFirstNode("Text").getContent()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def redirect(self):
|
|
||||||
try:
|
|
||||||
return self.ddgres.getFirstNode("Redirect").getContent()
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def result(self):
|
|
||||||
try:
|
|
||||||
node = self.ddgres.getFirstNode("Results").getFirstNode("Result")
|
|
||||||
return node.getFirstNode("Text").getContent() + ": " + node.getFirstNode("FirstURL").getContent()
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def answer(self):
|
|
||||||
try:
|
|
||||||
return web.striphtml(self.ddgres.getFirstNode("Answer").getContent())
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def abstract(self):
|
|
||||||
try:
|
|
||||||
if self.ddgres.getNode("Abstract").getContent() != "":
|
|
||||||
return self.ddgres.getNode("Abstract").getContent() + " <" + self.ddgres.getNode("AbstractURL").getContent() + ">"
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
from urllib.parse import quote
|
|
||||||
from urllib.request import urlopen
|
|
||||||
|
|
||||||
import xmlparser
|
|
||||||
|
|
||||||
class WFASearch:
|
|
||||||
def __init__(self, terms):
|
|
||||||
self.terms = terms
|
|
||||||
try:
|
|
||||||
raw = urlopen("http://api.wolframalpha.com/v2/query?"
|
|
||||||
"input=%s&appid=%s"
|
|
||||||
% (quote(terms),
|
|
||||||
CONF.getNode("wfaapi")["key"]), timeout=15)
|
|
||||||
self.wfares = xmlparser.parse_string(raw.read())
|
|
||||||
except (TypeError, KeyError):
|
|
||||||
print ("You need a Wolfram|Alpha API key in order to use this "
|
|
||||||
"module. Add it to the module configuration file:\n<wfaapi"
|
|
||||||
" key=\"XXXXXX-XXXXXXXXXX\" />\nRegister at "
|
|
||||||
"http://products.wolframalpha.com/api/")
|
|
||||||
self.wfares = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success(self):
|
|
||||||
try:
|
|
||||||
return self.wfares["success"] == "true"
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def error(self):
|
|
||||||
if self.wfares is None:
|
|
||||||
return "An error occurs during computation."
|
|
||||||
elif self.wfares["error"] == "true":
|
|
||||||
return "An error occurs during computation: " + self.wfares.getNode("error").getNode("msg").getContent()
|
|
||||||
elif self.wfares.hasNode("didyoumeans"):
|
|
||||||
start = "Did you mean: "
|
|
||||||
tag = "didyoumean"
|
|
||||||
end = "?"
|
|
||||||
elif self.wfares.hasNode("tips"):
|
|
||||||
start = "Tips: "
|
|
||||||
tag = "tip"
|
|
||||||
end = ""
|
|
||||||
elif self.wfares.hasNode("relatedexamples"):
|
|
||||||
start = "Related examples: "
|
|
||||||
tag = "relatedexample"
|
|
||||||
end = ""
|
|
||||||
elif self.wfares.hasNode("futuretopic"):
|
|
||||||
return self.wfares.getNode("futuretopic")["msg"]
|
|
||||||
else:
|
|
||||||
return "An error occurs during computation"
|
|
||||||
proposal = list()
|
|
||||||
for dym in self.wfares.getNode(tag + "s").getNodes(tag):
|
|
||||||
if tag == "tip":
|
|
||||||
proposal.append(dym["text"])
|
|
||||||
elif tag == "relatedexample":
|
|
||||||
proposal.append(dym["desc"])
|
|
||||||
else:
|
|
||||||
proposal.append(dym.getContent())
|
|
||||||
return start + ', '.join(proposal) + end
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nextRes(self):
|
|
||||||
try:
|
|
||||||
for node in self.wfares.getNodes("pod"):
|
|
||||||
for subnode in node.getNodes("subpod"):
|
|
||||||
if subnode.getFirstNode("plaintext").getContent() != "":
|
|
||||||
yield node["title"] + " " + subnode["title"] + ": " + subnode.getFirstNode("plaintext").getContent()
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
import re
|
|
||||||
from urllib.parse import quote
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
import xmlparser
|
|
||||||
|
|
||||||
class Wikipedia:
|
|
||||||
def __init__(self, terms, lang="fr", site="wikipedia.org", section=0):
|
|
||||||
self.terms = terms
|
|
||||||
self.lang = lang
|
|
||||||
self.curRT = 0
|
|
||||||
|
|
||||||
raw = urllib.request.urlopen(urllib.request.Request("http://" + self.lang + "." + site + "/w/api.php?format=xml&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % (quote(terms)), headers={"User-agent": "Nemubot v3"}))
|
|
||||||
self.wres = xmlparser.parse_string(raw.read())
|
|
||||||
if self.wres is None or not (self.wres.hasNode("query") and self.wres.getFirstNode("query").hasNode("pages") and self.wres.getFirstNode("query").getFirstNode("pages").hasNode("page") and self.wres.getFirstNode("query").getFirstNode("pages").getFirstNode("page").hasNode("revisions")):
|
|
||||||
self.wres = None
|
|
||||||
else:
|
|
||||||
self.wres = self.wres.getFirstNode("query").getFirstNode("pages").getFirstNode("page").getFirstNode("revisions").getFirstNode("rev").getContent()
|
|
||||||
self.wres = striplink(self.wres)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nextRes(self):
|
|
||||||
if self.wres is not None:
|
|
||||||
for cnt in self.wres.split("\n"):
|
|
||||||
if self.curRT > 0:
|
|
||||||
self.curRT -= 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
(c, u) = RGXP_s.subn(' ', cnt)
|
|
||||||
c = c.strip()
|
|
||||||
if c != "":
|
|
||||||
yield c
|
|
||||||
|
|
||||||
RGXP_p = re.compile(r"(<!--.*-->|<ref[^>]*/>|<ref[^>]*>[^>]*</ref>|<dfn[^>]*>[^>]*</dfn>|\{\{[^{}]*\}\}|\[\[([^\[\]]*\[\[[^\]\[]*\]\])+[^\[\]]*\]\]|\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}|\{\{([^{}]*\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}[^{}]*)+\}\}|\[\[[^\]|]+(\|[^\]\|]+)*\]\])|#\* ''" + "\n", re.I)
|
|
||||||
RGXP_l = re.compile(r'\{\{(nobr|lang\|[^|}]+)\|([^}]+)\}\}', re.I)
|
|
||||||
RGXP_m = re.compile(r'\{\{pron\|([^|}]+)\|[^}]+\}\}', re.I)
|
|
||||||
RGXP_t = re.compile("==+ *([^=]+) *=+=\n+([^\n])", re.I)
|
|
||||||
RGXP_q = re.compile(r'\[\[([^\[\]|]+)\|([^\]|]+)]]', re.I)
|
|
||||||
RGXP_r = re.compile(r'\[\[([^\[\]|]+)\]\]', re.I)
|
|
||||||
RGXP_s = re.compile(r'\s+')
|
|
||||||
|
|
||||||
def striplink(s):
|
|
||||||
s.replace("{{m}}", "masculin").replace("{{f}}", "feminin").replace("{{n}}", "neutre")
|
|
||||||
(s, n) = RGXP_m.subn(r"[\1]", s)
|
|
||||||
(s, n) = RGXP_l.subn(r"\2", s)
|
|
||||||
|
|
||||||
(s, n) = RGXP_q.subn(r"\1", s)
|
|
||||||
(s, n) = RGXP_r.subn(r"\1", s)
|
|
||||||
|
|
||||||
(s, n) = RGXP_p.subn('', s)
|
|
||||||
if s == "": return s
|
|
||||||
|
|
||||||
(s, n) = RGXP_t.subn("\x03\x16" + r"\1" + " :\x03\x16 " + r"\2", s)
|
|
||||||
return s.replace("'''", "\x03\x02").replace("''", "\x03\x1f")
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
import imp
|
|
||||||
|
|
||||||
nemubotversion = 3.3
|
|
||||||
|
|
||||||
from . import DDGSearch
|
|
||||||
from . import WFASearch
|
|
||||||
from . import Wikipedia
|
|
||||||
|
|
||||||
def load(context):
|
|
||||||
global CONF
|
|
||||||
WFASearch.CONF = CONF
|
|
||||||
|
|
||||||
from hooks import Hook
|
|
||||||
add_hook("cmd_hook", Hook(define, "define"))
|
|
||||||
add_hook("cmd_hook", Hook(search, "search"))
|
|
||||||
add_hook("cmd_hook", Hook(search, "ddg"))
|
|
||||||
add_hook("cmd_hook", Hook(search, "g"))
|
|
||||||
add_hook("cmd_hook", Hook(calculate, "wa"))
|
|
||||||
add_hook("cmd_hook", Hook(calculate, "calc"))
|
|
||||||
add_hook("cmd_hook", Hook(wiki, "dico"))
|
|
||||||
add_hook("cmd_hook", Hook(wiki, "wiki"))
|
|
||||||
|
|
||||||
def reload():
|
|
||||||
imp.reload(DDGSearch)
|
|
||||||
imp.reload(WFASearch)
|
|
||||||
imp.reload(Wikipedia)
|
|
||||||
|
|
||||||
|
|
||||||
def define(msg):
|
|
||||||
if len(msg.cmds) <= 1:
|
|
||||||
return Response(msg.sender,
|
|
||||||
"Indicate a term to define",
|
|
||||||
msg.channel, nick=msg.nick)
|
|
||||||
|
|
||||||
s = DDGSearch.DDGSearch(' '.join(msg.cmds[1:]))
|
|
||||||
|
|
||||||
res = Response(msg.sender, channel=msg.channel)
|
|
||||||
|
|
||||||
res.append_message(s.definition)
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def search(msg):
|
|
||||||
if len(msg.cmds) <= 1:
|
|
||||||
return Response(msg.sender,
|
|
||||||
"Indicate a term to search",
|
|
||||||
msg.channel, nick=msg.nick)
|
|
||||||
|
|
||||||
s = DDGSearch.DDGSearch(' '.join(msg.cmds[1:]))
|
|
||||||
|
|
||||||
res = Response(msg.sender, channel=msg.channel, nomore="No more results",
|
|
||||||
count=" (%d more results)")
|
|
||||||
|
|
||||||
res.append_message(s.redirect)
|
|
||||||
res.append_message(s.abstract)
|
|
||||||
res.append_message(s.result)
|
|
||||||
res.append_message(s.answer)
|
|
||||||
|
|
||||||
for rt in s.relatedTopics:
|
|
||||||
res.append_message(rt)
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def calculate(msg):
|
|
||||||
if len(msg.cmds) <= 1:
|
|
||||||
return Response(msg.sender,
|
|
||||||
"Indicate a calcul to compute",
|
|
||||||
msg.channel, nick=msg.nick)
|
|
||||||
|
|
||||||
s = WFASearch.WFASearch(' '.join(msg.cmds[1:]))
|
|
||||||
|
|
||||||
if s.success:
|
|
||||||
res = Response(msg.sender, channel=msg.channel, nomore="No more results")
|
|
||||||
for result in s.nextRes:
|
|
||||||
res.append_message(result)
|
|
||||||
if (len(res.messages) > 0):
|
|
||||||
res.messages.pop(0)
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
return Response(msg.sender, s.error, msg.channel)
|
|
||||||
|
|
||||||
|
|
||||||
def wiki(msg):
|
|
||||||
if len(msg.cmds) <= 1:
|
|
||||||
return Response(msg.sender,
|
|
||||||
"Indicate a term to search",
|
|
||||||
msg.channel, nick=msg.nick)
|
|
||||||
if len(msg.cmds) > 2 and len(msg.cmds[1]) < 4:
|
|
||||||
lang = msg.cmds[1]
|
|
||||||
extract = 2
|
|
||||||
else:
|
|
||||||
lang = "fr"
|
|
||||||
extract = 1
|
|
||||||
if msg.cmds[0] == "dico":
|
|
||||||
site = "wiktionary.org"
|
|
||||||
section = 1
|
|
||||||
else:
|
|
||||||
site = "wikipedia.org"
|
|
||||||
section = 0
|
|
||||||
|
|
||||||
s = Wikipedia.Wikipedia(' '.join(msg.cmds[extract:]), lang, site, section)
|
|
||||||
|
|
||||||
res = Response(msg.sender, channel=msg.channel, nomore="No more results")
|
|
||||||
if site == "wiktionary.org":
|
|
||||||
tout = [result for result in s.nextRes if result.find("\x03\x16 :\x03\x16 ") != 0]
|
|
||||||
if len(tout) > 0:
|
|
||||||
tout.remove(tout[0])
|
|
||||||
defI=1
|
|
||||||
for t in tout:
|
|
||||||
if t.find("# ") == 0:
|
|
||||||
t = t.replace("# ", "%d. " % defI)
|
|
||||||
defI += 1
|
|
||||||
elif t.find("#* ") == 0:
|
|
||||||
t = t.replace("#* ", " * ")
|
|
||||||
res.append_message(t)
|
|
||||||
else:
|
|
||||||
for result in s.nextRes:
|
|
||||||
res.append_message(result)
|
|
||||||
|
|
||||||
if len(res.messages) > 0:
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
return Response(msg.sender,
|
|
||||||
"No information about " + " ".join(msg.cmds[extract:]),
|
|
||||||
msg.channel)
|
|
||||||
94
modules/dig.py
Normal file
94
modules/dig.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
"""DNS resolver"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import dns.exception
|
||||||
|
import dns.name
|
||||||
|
import dns.rdataclass
|
||||||
|
import dns.rdatatype
|
||||||
|
import dns.resolver
|
||||||
|
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("dig",
|
||||||
|
help="Resolve domain name with a basic syntax similar to dig(1)")
|
||||||
|
def dig(msg):
|
||||||
|
lclass = "IN"
|
||||||
|
ltype = "A"
|
||||||
|
ledns = None
|
||||||
|
ltimeout = 6.0
|
||||||
|
ldomain = None
|
||||||
|
lnameservers = []
|
||||||
|
lsearchlist = []
|
||||||
|
loptions = []
|
||||||
|
for a in msg.args:
|
||||||
|
if a in dns.rdatatype._by_text:
|
||||||
|
ltype = a
|
||||||
|
elif a in dns.rdataclass._by_text:
|
||||||
|
lclass = a
|
||||||
|
elif a[0] == "@":
|
||||||
|
try:
|
||||||
|
lnameservers.append(str(ipaddress.ip_address(a[1:])))
|
||||||
|
except ValueError:
|
||||||
|
for r in socket.getaddrinfo(a[1:], 53, proto=socket.IPPROTO_UDP):
|
||||||
|
lnameservers.append(r[4][0])
|
||||||
|
|
||||||
|
elif a[0:8] == "+domain=":
|
||||||
|
lsearchlist.append(dns.name.from_unicode(a[8:]))
|
||||||
|
elif a[0:6] == "+edns=":
|
||||||
|
ledns = int(a[6:])
|
||||||
|
elif a[0:6] == "+time=":
|
||||||
|
ltimeout = float(a[6:])
|
||||||
|
elif a[0] == "+":
|
||||||
|
loptions.append(a[1:])
|
||||||
|
else:
|
||||||
|
ldomain = a
|
||||||
|
|
||||||
|
if not ldomain:
|
||||||
|
raise IMException("indicate a domain to resolve")
|
||||||
|
|
||||||
|
resolv = dns.resolver.Resolver()
|
||||||
|
if ledns:
|
||||||
|
resolv.edns = ledns
|
||||||
|
resolv.lifetime = ltimeout
|
||||||
|
resolv.timeout = ltimeout
|
||||||
|
resolv.flags = (
|
||||||
|
dns.flags.QR | dns.flags.RA |
|
||||||
|
dns.flags.AA if "aaonly" in loptions or "aaflag" in loptions else 0 |
|
||||||
|
dns.flags.AD if "adflag" in loptions else 0 |
|
||||||
|
dns.flags.CD if "cdflag" in loptions else 0 |
|
||||||
|
dns.flags.RD if "norecurse" not in loptions else 0
|
||||||
|
)
|
||||||
|
if lsearchlist:
|
||||||
|
resolv.search = lsearchlist
|
||||||
|
else:
|
||||||
|
resolv.search = [dns.name.from_text(".")]
|
||||||
|
|
||||||
|
if lnameservers:
|
||||||
|
resolv.nameservers = lnameservers
|
||||||
|
|
||||||
|
try:
|
||||||
|
answers = resolv.query(ldomain, ltype, lclass, tcp="tcp" in loptions)
|
||||||
|
except dns.exception.DNSException as e:
|
||||||
|
raise IMException(str(e))
|
||||||
|
|
||||||
|
res = Response(channel=msg.channel, count=" (%s others entries)")
|
||||||
|
for rdata in answers:
|
||||||
|
res.append_message("%s %s %s %s %s" % (
|
||||||
|
answers.qname.to_text(),
|
||||||
|
answers.ttl if not "nottlid" in loptions else "",
|
||||||
|
dns.rdataclass.to_text(answers.rdclass) if not "nocl" in loptions else "",
|
||||||
|
dns.rdatatype.to_text(answers.rdtype),
|
||||||
|
rdata.to_text())
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
89
modules/disas.py
Normal file
89
modules/disas.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""The Ultimate Disassembler Module"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
import capstone
|
||||||
|
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
ARCHITECTURES = {
|
||||||
|
"arm": capstone.CS_ARCH_ARM,
|
||||||
|
"arm64": capstone.CS_ARCH_ARM64,
|
||||||
|
"mips": capstone.CS_ARCH_MIPS,
|
||||||
|
"ppc": capstone.CS_ARCH_PPC,
|
||||||
|
"sparc": capstone.CS_ARCH_SPARC,
|
||||||
|
"sysz": capstone.CS_ARCH_SYSZ,
|
||||||
|
"x86": capstone.CS_ARCH_X86,
|
||||||
|
"xcore": capstone.CS_ARCH_XCORE,
|
||||||
|
}
|
||||||
|
|
||||||
|
MODES = {
|
||||||
|
"arm": capstone.CS_MODE_ARM,
|
||||||
|
"thumb": capstone.CS_MODE_THUMB,
|
||||||
|
"mips32": capstone.CS_MODE_MIPS32,
|
||||||
|
"mips64": capstone.CS_MODE_MIPS64,
|
||||||
|
"mips32r6": capstone.CS_MODE_MIPS32R6,
|
||||||
|
"16": capstone.CS_MODE_16,
|
||||||
|
"32": capstone.CS_MODE_32,
|
||||||
|
"64": capstone.CS_MODE_64,
|
||||||
|
"le": capstone.CS_MODE_LITTLE_ENDIAN,
|
||||||
|
"be": capstone.CS_MODE_BIG_ENDIAN,
|
||||||
|
"micro": capstone.CS_MODE_MICRO,
|
||||||
|
"mclass": capstone.CS_MODE_MCLASS,
|
||||||
|
"v8": capstone.CS_MODE_V8,
|
||||||
|
"v9": capstone.CS_MODE_V9,
|
||||||
|
}
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("disas",
|
||||||
|
help="Display assembly code",
|
||||||
|
help_usage={"CODE": "Display assembly code corresponding to the given CODE"},
|
||||||
|
keywords={
|
||||||
|
"arch=ARCH": "Specify the architecture of the code to disassemble (default: x86, choose between: %s)" % ', '.join(ARCHITECTURES.keys()),
|
||||||
|
"modes=MODE[,MODE]": "Specify hardware mode of the code to disassemble (default: 32, between: %s)" % ', '.join(MODES.keys()),
|
||||||
|
})
|
||||||
|
def cmd_disas(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("please give me some code")
|
||||||
|
|
||||||
|
# Determine the architecture
|
||||||
|
if "arch" in msg.kwargs:
|
||||||
|
if msg.kwargs["arch"] not in ARCHITECTURES:
|
||||||
|
raise IMException("unknown architectures '%s'" % msg.kwargs["arch"])
|
||||||
|
architecture = ARCHITECTURES[msg.kwargs["arch"]]
|
||||||
|
else:
|
||||||
|
architecture = capstone.CS_ARCH_X86
|
||||||
|
|
||||||
|
# Determine hardware modes
|
||||||
|
modes = 0
|
||||||
|
if "modes" in msg.kwargs:
|
||||||
|
for mode in msg.kwargs["modes"].split(','):
|
||||||
|
if mode not in MODES:
|
||||||
|
raise IMException("unknown mode '%s'" % mode)
|
||||||
|
modes += MODES[mode]
|
||||||
|
elif architecture == capstone.CS_ARCH_X86 or architecture == capstone.CS_ARCH_PPC:
|
||||||
|
modes = capstone.CS_MODE_32
|
||||||
|
elif architecture == capstone.CS_ARCH_ARM or architecture == capstone.CS_ARCH_ARM64:
|
||||||
|
modes = capstone.CS_MODE_ARM
|
||||||
|
elif architecture == capstone.CS_ARCH_MIPS:
|
||||||
|
modes = capstone.CS_MODE_MIPS32
|
||||||
|
|
||||||
|
# Get the code
|
||||||
|
code = bytearray.fromhex(''.join([a.replace("0x", "") for a in msg.args]))
|
||||||
|
|
||||||
|
# Setup capstone
|
||||||
|
md = capstone.Cs(architecture, modes)
|
||||||
|
|
||||||
|
res = Response(channel=msg.channel, nomore="No more instruction")
|
||||||
|
|
||||||
|
for isn in md.disasm(code, 0x1000):
|
||||||
|
res.append_message("%s %s" %(isn.mnemonic, isn.op_str), title="0x%x" % isn.address)
|
||||||
|
|
||||||
|
return res
|
||||||
296
modules/events.py
Normal file
296
modules/events.py
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
"""Create countdowns and reminders"""
|
||||||
|
|
||||||
|
import calendar
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from functools import partial
|
||||||
|
import re
|
||||||
|
|
||||||
|
from nemubot import context
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.event import ModuleEvent
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.message import Command
|
||||||
|
from nemubot.tools.countdown import countdown_format, countdown
|
||||||
|
from nemubot.tools.date import extractDate
|
||||||
|
from nemubot.tools.xmlparser.basic import DictNode
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
class Event:
|
||||||
|
|
||||||
|
def __init__(self, server, channel, creator, start_time, end_time=None):
|
||||||
|
self._server = server
|
||||||
|
self._channel = channel
|
||||||
|
self._creator = creator
|
||||||
|
self._start = datetime.utcfromtimestamp(float(start_time)).replace(tzinfo=timezone.utc) if not isinstance(start_time, datetime) else start_time
|
||||||
|
self._end = datetime.utcfromtimestamp(float(end_time)).replace(tzinfo=timezone.utc) if end_time else None
|
||||||
|
self._evt = None
|
||||||
|
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self._evt is not None:
|
||||||
|
context.del_event(self._evt)
|
||||||
|
self._evt = None
|
||||||
|
|
||||||
|
|
||||||
|
def saveElement(self, store, tag="event"):
|
||||||
|
attrs = {
|
||||||
|
"server": str(self._server),
|
||||||
|
"channel": str(self._channel),
|
||||||
|
"creator": str(self._creator),
|
||||||
|
"start_time": str(calendar.timegm(self._start.timetuple())),
|
||||||
|
}
|
||||||
|
if self._end:
|
||||||
|
attrs["end_time"] = str(calendar.timegm(self._end.timetuple()))
|
||||||
|
store.startElement(tag, attrs)
|
||||||
|
store.endElement(tag)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def creator(self):
|
||||||
|
return self._creator
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start(self):
|
||||||
|
return self._start
|
||||||
|
|
||||||
|
@property
|
||||||
|
def end(self):
|
||||||
|
return self._end
|
||||||
|
|
||||||
|
@end.setter
|
||||||
|
def end(self, c):
|
||||||
|
self._end = c
|
||||||
|
|
||||||
|
@end.deleter
|
||||||
|
def end(self):
|
||||||
|
self._end = None
|
||||||
|
|
||||||
|
|
||||||
|
def help_full ():
|
||||||
|
return "This module store a lot of events: ny, we, " + (", ".join(context.datas.keys()) if hasattr(context, "datas") else "") + "\n!eventslist: gets list of timer\n!start /something/: launch a timer"
|
||||||
|
|
||||||
|
|
||||||
|
def load(context):
|
||||||
|
context.set_knodes({
|
||||||
|
"dict": DictNode,
|
||||||
|
"event": Event,
|
||||||
|
})
|
||||||
|
|
||||||
|
if context.data is None:
|
||||||
|
context.set_default(DictNode())
|
||||||
|
|
||||||
|
# Relaunch all timers
|
||||||
|
for kevt in context.data:
|
||||||
|
if context.data[kevt].end:
|
||||||
|
context.data[kevt]._evt = context.add_event(ModuleEvent(partial(fini, kevt, context.data[kevt]), offset=context.data[kevt].end - datetime.now(timezone.utc), interval=0))
|
||||||
|
|
||||||
|
|
||||||
|
def fini(name, evt):
|
||||||
|
context.send_response(evt._server, Response("%s arrivé à échéance." % name, channel=evt._channel, nick=evt.creator))
|
||||||
|
evt._evt = None
|
||||||
|
del context.data[name]
|
||||||
|
context.save()
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("goûter")
|
||||||
|
def cmd_gouter(msg):
|
||||||
|
ndate = datetime.now(timezone.utc)
|
||||||
|
ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42, 0, 0, timezone.utc)
|
||||||
|
return Response(countdown_format(ndate,
|
||||||
|
"Le goûter aura lieu dans %s, préparez vos biscuits !",
|
||||||
|
"Nous avons %s de retard pour le goûter :("),
|
||||||
|
channel=msg.channel)
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("week-end")
|
||||||
|
def cmd_we(msg):
|
||||||
|
ndate = datetime.now(timezone.utc) + timedelta(5 - datetime.today().weekday())
|
||||||
|
ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1, 0, timezone.utc)
|
||||||
|
return Response(countdown_format(ndate,
|
||||||
|
"Il reste %s avant le week-end, courage ;)",
|
||||||
|
"Youhou, on est en week-end depuis %s."),
|
||||||
|
channel=msg.channel)
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("start")
|
||||||
|
def start_countdown(msg):
|
||||||
|
"""!start /something/: launch a timer"""
|
||||||
|
if len(msg.args) < 1:
|
||||||
|
raise IMException("indique le nom d'un événement à chronométrer")
|
||||||
|
if msg.args[0] in context.data:
|
||||||
|
raise IMException("%s existe déjà." % msg.args[0])
|
||||||
|
|
||||||
|
evt = Event(server=msg.server, channel=msg.channel, creator=msg.frm, start_time=msg.date)
|
||||||
|
|
||||||
|
if len(msg.args) > 1:
|
||||||
|
result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.args[1])
|
||||||
|
result2 = re.match("(.*[^0-9])?([0-3]?[0-9])/([0-1]?[0-9])/((19|20)?[01239][0-9])", msg.args[1])
|
||||||
|
result3 = re.match("(.*[^0-9])?([0-2]?[0-9]):([0-5]?[0-9])(:([0-5]?[0-9]))?", msg.args[1])
|
||||||
|
if result2 is not None or result3 is not None:
|
||||||
|
try:
|
||||||
|
now = msg.date
|
||||||
|
if result3 is None or result3.group(5) is None: sec = 0
|
||||||
|
else: sec = int(result3.group(5))
|
||||||
|
if result3 is None or result3.group(3) is None: minu = 0
|
||||||
|
else: minu = int(result3.group(3))
|
||||||
|
if result3 is None or result3.group(2) is None: hou = 0
|
||||||
|
else: hou = int(result3.group(2))
|
||||||
|
if result2 is None or result2.group(4) is None: yea = now.year
|
||||||
|
else: yea = int(result2.group(4))
|
||||||
|
if result2 is not None and result3 is not None:
|
||||||
|
evt.end = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec, timezone.utc)
|
||||||
|
elif result2 is not None:
|
||||||
|
evt.end = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2)), 0, 0, 0, timezone.utc)
|
||||||
|
elif result3 is not None:
|
||||||
|
if hou * 3600 + minu * 60 + sec > now.hour * 3600 + now.minute * 60 + now.second:
|
||||||
|
evt.end = datetime(now.year, now.month, now.day, hou, minu, sec, timezone.utc)
|
||||||
|
else:
|
||||||
|
evt.end = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc)
|
||||||
|
except:
|
||||||
|
raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0])
|
||||||
|
|
||||||
|
elif result1 is not None and len(result1) > 0:
|
||||||
|
evt.end = msg.date
|
||||||
|
for (t, g) in result1:
|
||||||
|
if g is None or g == "" or g == "m" or g == "M":
|
||||||
|
evt.end += timedelta(minutes=int(t))
|
||||||
|
elif g == "h" or g == "H":
|
||||||
|
evt.end += timedelta(hours=int(t))
|
||||||
|
elif g == "d" or g == "D" or g == "j" or g == "J":
|
||||||
|
evt.end += timedelta(days=int(t))
|
||||||
|
elif g == "w" or g == "W":
|
||||||
|
evt.end += timedelta(days=int(t)*7)
|
||||||
|
elif g == "y" or g == "Y" or g == "a" or g == "A":
|
||||||
|
evt.end += timedelta(days=int(t)*365)
|
||||||
|
else:
|
||||||
|
evt.end += timedelta(seconds=int(t))
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0])
|
||||||
|
|
||||||
|
context.data[msg.args[0]] = evt
|
||||||
|
context.save()
|
||||||
|
|
||||||
|
if evt.end is not None:
|
||||||
|
context.add_event(ModuleEvent(partial(fini, msg.args[0], evt),
|
||||||
|
offset=evt.end - datetime.now(timezone.utc),
|
||||||
|
interval=0))
|
||||||
|
return Response("%s commencé le %s et se terminera le %s." %
|
||||||
|
(msg.args[0], msg.date.strftime("%A %d %B %Y à %H:%M:%S"),
|
||||||
|
evt.end.strftime("%A %d %B %Y à %H:%M:%S")),
|
||||||
|
channel=msg.channel)
|
||||||
|
else:
|
||||||
|
return Response("%s commencé le %s"% (msg.args[0],
|
||||||
|
msg.date.strftime("%A %d %B %Y à %H:%M:%S")),
|
||||||
|
channel=msg.channel)
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("end")
|
||||||
|
@hook.command("forceend")
|
||||||
|
def end_countdown(msg):
|
||||||
|
if len(msg.args) < 1:
|
||||||
|
raise IMException("quel événement terminer ?")
|
||||||
|
|
||||||
|
if msg.args[0] in context.data:
|
||||||
|
if context.data[msg.args[0]].creator == msg.frm or (msg.cmd == "forceend" and msg.frm_owner):
|
||||||
|
duration = countdown(msg.date - context.data[msg.args[0]].start)
|
||||||
|
del context.data[msg.args[0]]
|
||||||
|
context.save()
|
||||||
|
return Response("%s a duré %s." % (msg.args[0], duration),
|
||||||
|
channel=msg.channel, nick=msg.frm)
|
||||||
|
else:
|
||||||
|
raise IMException("Vous ne pouvez pas terminer le compteur %s, créé par %s." % (msg.args[0], context.data[msg.args[0]].creator))
|
||||||
|
else:
|
||||||
|
return Response("%s n'est pas un compteur connu."% (msg.args[0]), channel=msg.channel, nick=msg.frm)
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("eventslist")
|
||||||
|
def liste(msg):
|
||||||
|
"""!eventslist: gets list of timer"""
|
||||||
|
if len(msg.args):
|
||||||
|
res = Response(channel=msg.channel)
|
||||||
|
for user in msg.args:
|
||||||
|
cmptr = [k for k in context.data if context.data[k].creator == user]
|
||||||
|
if len(cmptr) > 0:
|
||||||
|
res.append_message(cmptr, title="Events created by %s" % user)
|
||||||
|
else:
|
||||||
|
res.append_message("%s doesn't have any counting events" % user)
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
return Response(list(context.data.keys()), channel=msg.channel, title="Known events")
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command(match=lambda msg: isinstance(msg, Command) and msg.cmd in context.data)
|
||||||
|
def parseanswer(msg):
|
||||||
|
res = Response(channel=msg.channel)
|
||||||
|
|
||||||
|
# Avoid message starting by ! which can be interpreted as command by other bots
|
||||||
|
if msg.cmd[0] == "!":
|
||||||
|
res.nick = msg.frm
|
||||||
|
|
||||||
|
if msg.cmd in context.data:
|
||||||
|
if context.data[msg.cmd].end:
|
||||||
|
res.append_message("%s commencé il y a %s et se terminera dans %s." % (msg.cmd, countdown(msg.date - context.data[msg.cmd].start), countdown(context.data[msg.cmd].end - msg.date)))
|
||||||
|
else:
|
||||||
|
res.append_message("%s commencé il y a %s." % (msg.cmd, countdown(msg.date - context.data[msg.cmd].start)))
|
||||||
|
else:
|
||||||
|
res.append_message(countdown_format(context.data[msg.cmd].start, context.data[msg.cmd]["msg_before"], context.data[msg.cmd]["msg_after"]))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
RGXP_ask = re.compile(r"^.*((create|new)\s+(a|an|a\s*new|an\s*other)?\s*(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3})\s+(un)?\s*([eé]v[ée]nements?|commande?)).*$", re.I)
|
||||||
|
|
||||||
|
@hook.ask(match=lambda msg: RGXP_ask.match(msg.message))
|
||||||
|
def parseask(msg):
|
||||||
|
name = re.match("^.*!([^ \"'@!]+).*$", msg.message)
|
||||||
|
if name is None:
|
||||||
|
raise IMException("il faut que tu attribues une commande à l'événement.")
|
||||||
|
if name.group(1) in context.data:
|
||||||
|
raise IMException("un événement portant ce nom existe déjà.")
|
||||||
|
|
||||||
|
texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.message, re.I)
|
||||||
|
if texts is not None and texts.group(3) is not None:
|
||||||
|
extDate = extractDate(msg.message)
|
||||||
|
if extDate is None or extDate == "":
|
||||||
|
raise IMException("la date de l'événement est invalide !")
|
||||||
|
|
||||||
|
if texts.group(1) is not None and (texts.group(1) == "après" or texts.group(1) == "apres" or texts.group(1) == "after"):
|
||||||
|
msg_after = texts.group(2)
|
||||||
|
msg_before = texts.group(5)
|
||||||
|
if (texts.group(4) is not None and (texts.group(4) == "après" or texts.group(4) == "apres" or texts.group(4) == "after")) or texts.group(1) is None:
|
||||||
|
msg_before = texts.group(2)
|
||||||
|
msg_after = texts.group(5)
|
||||||
|
|
||||||
|
if msg_before.find("%s") == -1 or msg_after.find("%s") == -1:
|
||||||
|
raise IMException("Pour que l'événement soit valide, ajouter %s à"
|
||||||
|
" l'endroit où vous voulez que soit ajouté le"
|
||||||
|
" compte à rebours.")
|
||||||
|
|
||||||
|
evt = ModuleState("event")
|
||||||
|
evt["server"] = msg.server
|
||||||
|
evt["channel"] = msg.channel
|
||||||
|
evt["proprio"] = msg.frm
|
||||||
|
evt["name"] = name.group(1)
|
||||||
|
evt["start"] = extDate
|
||||||
|
evt["msg_after"] = msg_after
|
||||||
|
evt["msg_before"] = msg_before
|
||||||
|
context.data.addChild(evt)
|
||||||
|
context.save()
|
||||||
|
return Response("Nouvel événement !%s ajouté avec succès." % name.group(1),
|
||||||
|
channel=msg.channel)
|
||||||
|
|
||||||
|
elif texts is not None and texts.group(2) is not None:
|
||||||
|
evt = ModuleState("event")
|
||||||
|
evt["server"] = msg.server
|
||||||
|
evt["channel"] = msg.channel
|
||||||
|
evt["proprio"] = msg.frm
|
||||||
|
evt["name"] = name.group(1)
|
||||||
|
evt["msg_before"] = texts.group (2)
|
||||||
|
context.data.addChild(evt)
|
||||||
|
context.save()
|
||||||
|
return Response("Nouvelle commande !%s ajoutée avec succès." % name.group(1),
|
||||||
|
channel=msg.channel)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise IMException("Veuillez indiquez les messages d'attente et d'après événement entre guillemets.")
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" ?>
|
|
||||||
<nemubotmodule name="events">
|
|
||||||
<message type="cmd" name="start" call="start_countdown" />
|
|
||||||
<message type="cmd" name="end" call="end_countdown" />
|
|
||||||
<message type="cmd" name="forceend" call="end_countdown" />
|
|
||||||
<message type="cmd" name="eventlist" call="liste" />
|
|
||||||
<message type="cmd" name="eventslist" call="liste" />
|
|
||||||
<message type="cmd" name="eventliste" call="liste" />
|
|
||||||
<message type="cmd" name="eventsliste" call="liste" />
|
|
||||||
<message type="cmd" name="gouter" call="cmd_gouter" />
|
|
||||||
<message type="cmd" name="goûter" call="cmd_gouter" />
|
|
||||||
<message type="cmd" name="week-end" call="cmd_we" />
|
|
||||||
<message type="cmd" name="weekend" call="cmd_we" />
|
|
||||||
</nemubotmodule>
|
|
||||||
|
|
@ -1,238 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
import imp
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from datetime import timedelta
|
|
||||||
from datetime import datetime
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
nemubotversion = 3.3
|
|
||||||
|
|
||||||
from event import ModuleEvent
|
|
||||||
from hooks import Hook
|
|
||||||
|
|
||||||
def help_tiny ():
|
|
||||||
"""Line inserted in the response to the command !help"""
|
|
||||||
return "events manager"
|
|
||||||
|
|
||||||
def help_full ():
|
|
||||||
return "This module store a lot of events: ny, we, vacs, " + (", ".join(DATAS.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer"
|
|
||||||
|
|
||||||
CONTEXT = None
|
|
||||||
|
|
||||||
def load(context):
|
|
||||||
global DATAS, CONTEXT
|
|
||||||
CONTEXT = context
|
|
||||||
#Define the index
|
|
||||||
DATAS.setIndex("name")
|
|
||||||
|
|
||||||
for evt in DATAS.index.keys():
|
|
||||||
if DATAS.index[evt].hasAttribute("end"):
|
|
||||||
event = ModuleEvent(call=fini, call_data=dict(strend=DATAS.index[evt]))
|
|
||||||
event.end = DATAS.index[evt].getDate("end")
|
|
||||||
idt = context.add_event(event)
|
|
||||||
if idt is not None:
|
|
||||||
DATAS.index[evt]["id"] = idt
|
|
||||||
|
|
||||||
|
|
||||||
def fini(d, strend):
|
|
||||||
for server in CONTEXT.servers.keys():
|
|
||||||
if not strend.hasAttribute("server") or server == strend["server"]:
|
|
||||||
if strend["channel"] == CONTEXT.servers[server].nick:
|
|
||||||
CONTEXT.servers[server].send_msg_usr(strend["sender"], "%s: %s arrivé à échéance." % (strend["proprio"], strend["name"]))
|
|
||||||
else:
|
|
||||||
CONTEXT.servers[server].send_msg(strend["channel"], "%s: %s arrivé à échéance." % (strend["proprio"], strend["name"]))
|
|
||||||
DATAS.delChild(DATAS.index[strend["name"]])
|
|
||||||
save()
|
|
||||||
|
|
||||||
def cmd_gouter(msg):
|
|
||||||
ndate = datetime.today()
|
|
||||||
ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42)
|
|
||||||
return Response(msg.sender,
|
|
||||||
msg.countdown_format(ndate,
|
|
||||||
"Le goûter aura lieu dans %s, préparez vos biscuits !",
|
|
||||||
"Nous avons %s de retard pour le goûter :("),
|
|
||||||
channel=msg.channel)
|
|
||||||
|
|
||||||
def cmd_we(msg):
|
|
||||||
ndate = datetime.today() + timedelta(5 - datetime.today().weekday())
|
|
||||||
ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1)
|
|
||||||
return Response(msg.sender,
|
|
||||||
msg.countdown_format(ndate,
|
|
||||||
"Il reste %s avant le week-end, courage ;)",
|
|
||||||
"Youhou, on est en week-end depuis %s."),
|
|
||||||
channel=msg.channel)
|
|
||||||
|
|
||||||
def cmd_vacances(msg):
|
|
||||||
return Response(msg.sender,
|
|
||||||
msg.countdown_format(datetime(2013, 7, 30, 18, 0, 1),
|
|
||||||
"Il reste %s avant les vacances :)",
|
|
||||||
"Profitons, c'est les vacances depuis %s."),
|
|
||||||
channel=msg.channel)
|
|
||||||
|
|
||||||
def start_countdown(msg):
|
|
||||||
if msg.cmds[1] not in DATAS.index:
|
|
||||||
|
|
||||||
strnd = ModuleState("strend")
|
|
||||||
strnd["server"] = msg.server
|
|
||||||
strnd["channel"] = msg.channel
|
|
||||||
strnd["proprio"] = msg.nick
|
|
||||||
strnd["sender"] = msg.sender
|
|
||||||
strnd["start"] = datetime.now()
|
|
||||||
strnd["name"] = msg.cmds[1]
|
|
||||||
DATAS.addChild(strnd)
|
|
||||||
|
|
||||||
evt = ModuleEvent(call=fini, call_data=dict(strend=strnd))
|
|
||||||
|
|
||||||
if len(msg.cmds) > 2:
|
|
||||||
result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.cmds[2])
|
|
||||||
result2 = re.match("(.*[^0-9])?([0-3]?[0-9])/([0-1]?[0-9])/((19|20)?[01239][0-9])", msg.cmds[2])
|
|
||||||
result3 = re.match("(.*[^0-9])?([0-2]?[0-9]):([0-5]?[0-9])(:([0-5]?[0-9]))?", msg.cmds[2])
|
|
||||||
if result2 is not None or result3 is not None:
|
|
||||||
try:
|
|
||||||
now = datetime.now()
|
|
||||||
if result3 is None or result3.group(5) is None: sec = 0
|
|
||||||
else: sec = int(result3.group(5))
|
|
||||||
if result3 is None or result3.group(3) is None: minu = 0
|
|
||||||
else: minu = int(result3.group(3))
|
|
||||||
if result3 is None or result3.group(2) is None: hou = 0
|
|
||||||
else: hou = int(result3.group(2))
|
|
||||||
|
|
||||||
if result2 is None or result2.group(4) is None: yea = now.year
|
|
||||||
else: yea = int(result2.group(4))
|
|
||||||
|
|
||||||
if result2 is not None and result3 is not None:
|
|
||||||
strnd["end"] = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec)
|
|
||||||
elif result2 is not None:
|
|
||||||
strnd["end"] = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2)))
|
|
||||||
elif result3 is not None:
|
|
||||||
if hou * 3600 + minu * 60 + sec > now.hour * 3600 + now.minute * 60 + now.second:
|
|
||||||
strnd["end"] = datetime(now.year, now.month, now.day, hou, minu, sec)
|
|
||||||
else:
|
|
||||||
strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec)
|
|
||||||
|
|
||||||
evt.end = strnd.getDate("end")
|
|
||||||
strnd["id"] = CONTEXT.add_event(evt)
|
|
||||||
save()
|
|
||||||
return Response(msg.sender, "%s commencé le %s et se terminera le %s." %
|
|
||||||
(msg.cmds[1], datetime.now().strftime("%A %d %B %Y a %H:%M:%S"),
|
|
||||||
strnd.getDate("end").strftime("%A %d %B %Y a %H:%M:%S")))
|
|
||||||
except:
|
|
||||||
DATAS.delChild(strnd)
|
|
||||||
return Response(msg.sender,
|
|
||||||
"Mauvais format de date pour l'evenement %s. Il n'a pas ete cree." % msg.cmds[1])
|
|
||||||
elif result1 is not None and len(result1) > 0:
|
|
||||||
strnd["end"] = datetime.now()
|
|
||||||
for (t, g) in result1:
|
|
||||||
if g is None or g == "" or g == "m" or g == "M":
|
|
||||||
strnd["end"] += timedelta(minutes=int(t))
|
|
||||||
elif g == "h" or g == "H":
|
|
||||||
strnd["end"] += timedelta(hours=int(t))
|
|
||||||
elif g == "d" or g == "D" or g == "j" or g == "J":
|
|
||||||
strnd["end"] += timedelta(days=int(t))
|
|
||||||
elif g == "w" or g == "W":
|
|
||||||
strnd["end"] += timedelta(days=int(t)*7)
|
|
||||||
elif g == "y" or g == "Y" or g == "a" or g == "A":
|
|
||||||
strnd["end"] += timedelta(days=int(t)*365)
|
|
||||||
else:
|
|
||||||
strnd["end"] += timedelta(seconds=int(t))
|
|
||||||
evt.end = strnd.getDate("end")
|
|
||||||
strnd["id"] = CONTEXT.add_event(evt)
|
|
||||||
save()
|
|
||||||
return Response(msg.sender, "%s commencé le %s et se terminera le %s." %
|
|
||||||
(msg.cmds[1], datetime.now().strftime("%A %d %B %Y a %H:%M:%S"),
|
|
||||||
strnd.getDate("end").strftime("%A %d %B %Y a %H:%M:%S")))
|
|
||||||
save()
|
|
||||||
return Response(msg.sender, "%s commencé le %s"% (msg.cmds[1],
|
|
||||||
datetime.now().strftime("%A %d %B %Y a %H:%M:%S")))
|
|
||||||
else:
|
|
||||||
return Response(msg.sender, "%s existe déjà."% (msg.cmds[1]))
|
|
||||||
|
|
||||||
def end_countdown(msg):
|
|
||||||
if msg.cmds[1] in DATAS.index:
|
|
||||||
res = Response(msg.sender,
|
|
||||||
"%s a duré %s." % (msg.cmds[1],
|
|
||||||
msg.just_countdown(datetime.now () - DATAS.index[msg.cmds[1]].getDate("start"))),
|
|
||||||
channel=msg.channel)
|
|
||||||
if DATAS.index[msg.cmds[1]]["proprio"] == msg.nick or (msg.cmds[0] == "forceend" and msg.is_owner):
|
|
||||||
CONTEXT.del_event(DATAS.index[msg.cmds[1]]["id"])
|
|
||||||
DATAS.delChild(DATAS.index[msg.cmds[1]])
|
|
||||||
save()
|
|
||||||
else:
|
|
||||||
res.append_message("Vous ne pouvez pas terminer le compteur %s, créé par %s."% (msg.cmds[1], DATAS.index[msg.cmds[1]]["proprio"]))
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
return Response(msg.sender, "%s n'est pas un compteur connu."% (msg.cmds[1]))
|
|
||||||
|
|
||||||
def liste(msg):
|
|
||||||
msg.send_snd ("Compteurs connus : %s." % ", ".join(DATAS.index.keys()))
|
|
||||||
|
|
||||||
def parseanswer(msg):
|
|
||||||
if msg.cmds[0] in DATAS.index:
|
|
||||||
if DATAS.index[msg.cmds[0]].name == "strend":
|
|
||||||
if DATAS.index[msg.cmds[0]].hasAttribute("end"):
|
|
||||||
return Response(msg.sender, "%s commencé il y a %s et se terminera dans %s." % (msg.cmds[0], msg.just_countdown(datetime.now() - DATAS.index[msg.cmds[0]].getDate("start")), msg.just_countdown(DATAS.index[msg.cmds[0]].getDate("end") - datetime.now())), channel=msg.channel)
|
|
||||||
else:
|
|
||||||
return Response(msg.sender, "%s commencé il y a %s." % (msg.cmds[0], msg.just_countdown(datetime.now () - DATAS.index[msg.cmds[0]].getDate("start"))), channel=msg.channel)
|
|
||||||
else:
|
|
||||||
save()
|
|
||||||
return Response(msg.sender, msg.countdown_format (DATAS.index[msg.cmds[0]].getDate("start"), DATAS.index[msg.cmds[0]]["msg_before"], DATAS.index[msg.cmds[0]]["msg_after"]), channel=msg.channel)
|
|
||||||
|
|
||||||
def parseask(msg):
|
|
||||||
msgl = msg.content.lower()
|
|
||||||
if re.match("^.*((create|new) +(a|an|a +new|an *other)? *(events?|commande?)|(nouvel(le)?|ajoute|cr[ée]{1,3}) +(un)? *([eé]v[ée]nements?|commande?)).*$", msgl) is not None:
|
|
||||||
name = re.match("^.*!([^ \"'@!]+).*$", msg.content)
|
|
||||||
if name is not None and name.group (1) not in DATAS.index:
|
|
||||||
texts = re.match("^[^\"]*(avant|après|apres|before|after)?[^\"]*\"([^\"]+)\"[^\"]*((avant|après|apres|before|after)?.*\"([^\"]+)\".*)?$", msg.content)
|
|
||||||
if texts is not None and texts.group (3) is not None:
|
|
||||||
extDate = msg.extractDate ()
|
|
||||||
if extDate is None or extDate == "":
|
|
||||||
return Response(msg.sender, "La date de l'événement est invalide...", channel=msg.channel)
|
|
||||||
else:
|
|
||||||
if texts.group (1) is not None and (texts.group (1) == "après" or texts.group (1) == "apres" or texts.group (1) == "after"):
|
|
||||||
msg_after = texts.group (2)
|
|
||||||
msg_before = texts.group (5)
|
|
||||||
if (texts.group (4) is not None and (texts.group (4) == "après" or texts.group (4) == "apres" or texts.group (4) == "after")) or texts.group (1) is None:
|
|
||||||
msg_before = texts.group (2)
|
|
||||||
msg_after = texts.group (5)
|
|
||||||
|
|
||||||
if msg_before.find ("%s") != -1 and msg_after.find ("%s") != -1:
|
|
||||||
evt = ModuleState("event")
|
|
||||||
evt["server"] = msg.server
|
|
||||||
evt["channel"] = msg.channel
|
|
||||||
evt["proprio"] = msg.nick
|
|
||||||
evt["sender"] = msg.sender
|
|
||||||
evt["name"] = name.group(1)
|
|
||||||
evt["start"] = extDate
|
|
||||||
evt["msg_after"] = msg_after
|
|
||||||
evt["msg_before"] = msg_before
|
|
||||||
DATAS.addChild(evt)
|
|
||||||
save()
|
|
||||||
return Response(msg.sender,
|
|
||||||
"Nouvel événement !%s ajouté avec succès." % name.group(1),
|
|
||||||
msg.channel)
|
|
||||||
else:
|
|
||||||
return Response(msg.sender,
|
|
||||||
"Pour que l'événement soit valide, ajouter %s à"
|
|
||||||
" l'endroit où vous voulez que soit ajouté le"
|
|
||||||
" compte à rebours.")
|
|
||||||
elif texts is not None and texts.group (2) is not None:
|
|
||||||
evt = ModuleState("event")
|
|
||||||
evt["server"] = msg.server
|
|
||||||
evt["channel"] = msg.channel
|
|
||||||
evt["proprio"] = msg.nick
|
|
||||||
evt["sender"] = msg.sender
|
|
||||||
evt["name"] = name.group(1)
|
|
||||||
evt["msg_before"] = texts.group (2)
|
|
||||||
DATAS.addChild(evt)
|
|
||||||
save()
|
|
||||||
return Response(msg.sender, "Nouvelle commande !%s ajoutée avec succès." % name.group(1))
|
|
||||||
else:
|
|
||||||
return Response(msg.sender, "Veuillez indiquez les messages d'attente et d'après événement entre guillemets.")
|
|
||||||
elif name is None:
|
|
||||||
return Response(msg.sender, "Veuillez attribuer une commande à l'événement.")
|
|
||||||
else:
|
|
||||||
return Response(msg.sender, "Un événement portant ce nom existe déjà.")
|
|
||||||
64
modules/freetarifs.py
Normal file
64
modules/freetarifs.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
"""Inform about Free Mobile tarifs"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
import urllib.parse
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools import web
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
ACT = {
|
||||||
|
"ff_toFixe": "Appel vers les fixes",
|
||||||
|
"ff_toMobile": "Appel vers les mobiles",
|
||||||
|
"ff_smsSendedToCountry": "SMS vers le pays",
|
||||||
|
"ff_mmsSendedToCountry": "MMS vers le pays",
|
||||||
|
"fc_callToFrance": "Appel vers la France",
|
||||||
|
"fc_smsToFrance": "SMS vers la france",
|
||||||
|
"fc_mmsSended": "MMS vers la france",
|
||||||
|
"fc_callToSameCountry": "Réception des appels",
|
||||||
|
"fc_callReceived": "Appel dans le pays",
|
||||||
|
"fc_smsReceived": "SMS (Réception)",
|
||||||
|
"fc_mmsReceived": "MMS (Réception)",
|
||||||
|
"fc_moDataFromCountry": "Data",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_land_tarif(country, forfait="pkgFREE"):
|
||||||
|
url = "http://mobile.international.free.fr/?" + urllib.parse.urlencode({'pays': country})
|
||||||
|
page = web.getURLContent(url)
|
||||||
|
soup = BeautifulSoup(page)
|
||||||
|
|
||||||
|
fact = soup.find(class_=forfait)
|
||||||
|
|
||||||
|
if fact is None:
|
||||||
|
raise IMException("Country or forfait not found.")
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
for s in ACT.keys():
|
||||||
|
try:
|
||||||
|
res[s] = fact.find(attrs={"data-bind": "text: " + s}).text + " " + fact.find(attrs={"data-bind": "html: " + s + "Unit"}).text
|
||||||
|
except AttributeError:
|
||||||
|
res[s] = "inclus"
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
@hook.command("freetarifs",
|
||||||
|
help="Show Free Mobile tarifs for given contries",
|
||||||
|
help_usage={"COUNTRY": "Show Free Mobile tarifs for given CONTRY"},
|
||||||
|
keywords={
|
||||||
|
"forfait=FORFAIT": "Related forfait between Free (default) and 2euro"
|
||||||
|
})
|
||||||
|
def get_freetarif(msg):
|
||||||
|
res = Response(channel=msg.channel)
|
||||||
|
|
||||||
|
for country in msg.args:
|
||||||
|
t = get_land_tarif(country.lower().capitalize(), "pkg" + (msg.kwargs["forfait"] if "forfait" in msg.kwargs else "FREE").upper())
|
||||||
|
res.append_message(["\x02%s\x0F : %s" % (ACT[k], t[k]) for k in sorted(ACT.keys(), reverse=True)], title=country)
|
||||||
|
|
||||||
|
return res
|
||||||
231
modules/github.py
Normal file
231
modules/github.py
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
"""Repositories, users or issues on GitHub"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
import re
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools import web
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
def info_repos(repo):
|
||||||
|
return web.getJSON("https://api.github.com/search/repositories?q=%s" %
|
||||||
|
quote(repo))
|
||||||
|
|
||||||
|
|
||||||
|
def info_user(username):
|
||||||
|
user = web.getJSON("https://api.github.com/users/%s" % quote(username))
|
||||||
|
|
||||||
|
user["repos"] = web.getJSON("https://api.github.com/users/%s/"
|
||||||
|
"repos?sort=updated" % quote(username))
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def user_keys(username):
|
||||||
|
keys = web.getURLContent("https://github.com/%s.keys" % quote(username))
|
||||||
|
return keys.split('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def info_issue(repo, issue=None):
|
||||||
|
rp = info_repos(repo)
|
||||||
|
if rp["items"]:
|
||||||
|
fullname = rp["items"][0]["full_name"]
|
||||||
|
else:
|
||||||
|
fullname = repo
|
||||||
|
|
||||||
|
if issue is not None:
|
||||||
|
return [web.getJSON("https://api.github.com/repos/%s/issues/%s" %
|
||||||
|
(quote(fullname), quote(issue)))]
|
||||||
|
else:
|
||||||
|
return web.getJSON("https://api.github.com/repos/%s/issues?"
|
||||||
|
"sort=updated" % quote(fullname))
|
||||||
|
|
||||||
|
|
||||||
|
def info_commit(repo, commit=None):
|
||||||
|
rp = info_repos(repo)
|
||||||
|
if rp["items"]:
|
||||||
|
fullname = rp["items"][0]["full_name"]
|
||||||
|
else:
|
||||||
|
fullname = repo
|
||||||
|
|
||||||
|
if commit is not None:
|
||||||
|
return [web.getJSON("https://api.github.com/repos/%s/commits/%s" %
|
||||||
|
(quote(fullname), quote(commit)))]
|
||||||
|
else:
|
||||||
|
return web.getJSON("https://api.github.com/repos/%s/commits" %
|
||||||
|
quote(fullname))
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("github",
|
||||||
|
help="Display information about some repositories",
|
||||||
|
help_usage={
|
||||||
|
"REPO": "Display information about the repository REPO",
|
||||||
|
})
|
||||||
|
def cmd_github(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("indicate a repository name to search")
|
||||||
|
|
||||||
|
repos = info_repos(" ".join(msg.args))
|
||||||
|
|
||||||
|
res = Response(channel=msg.channel,
|
||||||
|
nomore="No more repository",
|
||||||
|
count=" (%d more repo)")
|
||||||
|
|
||||||
|
for repo in repos["items"]:
|
||||||
|
homepage = ""
|
||||||
|
if repo["homepage"] is not None:
|
||||||
|
homepage = repo["homepage"] + " - "
|
||||||
|
res.append_message("Repository %s: %s%s Main language: %s; %d forks; %d stars; %d watchers; %d opened_issues; view it at %s" %
|
||||||
|
(repo["full_name"],
|
||||||
|
homepage,
|
||||||
|
repo["description"],
|
||||||
|
repo["language"], repo["forks"],
|
||||||
|
repo["stargazers_count"],
|
||||||
|
repo["watchers_count"],
|
||||||
|
repo["open_issues_count"],
|
||||||
|
repo["html_url"]))
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("github_user",
|
||||||
|
help="Display information about users",
|
||||||
|
help_usage={
|
||||||
|
"USERNAME": "Display information about the user USERNAME",
|
||||||
|
})
|
||||||
|
def cmd_github_user(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("indicate a user name to search")
|
||||||
|
|
||||||
|
res = Response(channel=msg.channel, nomore="No more user")
|
||||||
|
|
||||||
|
user = info_user(" ".join(msg.args))
|
||||||
|
|
||||||
|
if "login" in user:
|
||||||
|
if user["repos"]:
|
||||||
|
kf = (" Known for: " +
|
||||||
|
", ".join([repo["name"] for repo in user["repos"]]))
|
||||||
|
else:
|
||||||
|
kf = ""
|
||||||
|
if "name" in user:
|
||||||
|
name = user["name"]
|
||||||
|
else:
|
||||||
|
name = user["login"]
|
||||||
|
res.append_message("User %s: %d public repositories; %d public gists; %d followers; %d following; view it at %s.%s" %
|
||||||
|
(name,
|
||||||
|
user["public_repos"],
|
||||||
|
user["public_gists"],
|
||||||
|
user["followers"],
|
||||||
|
user["following"],
|
||||||
|
user["html_url"],
|
||||||
|
kf))
|
||||||
|
else:
|
||||||
|
raise IMException("User not found")
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("github_user_keys",
|
||||||
|
help="Display user SSH keys",
|
||||||
|
help_usage={
|
||||||
|
"USERNAME": "Show USERNAME's SSH keys",
|
||||||
|
})
|
||||||
|
def cmd_github_user_keys(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("indicate a user name to search")
|
||||||
|
|
||||||
|
res = Response(channel=msg.channel, nomore="No more keys")
|
||||||
|
|
||||||
|
for k in user_keys(" ".join(msg.args)):
|
||||||
|
res.append_message(k)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("github_issue",
|
||||||
|
help="Display repository's issues",
|
||||||
|
help_usage={
|
||||||
|
"REPO": "Display latest issues created on REPO",
|
||||||
|
"REPO #ISSUE": "Display the issue number #ISSUE for REPO",
|
||||||
|
})
|
||||||
|
def cmd_github_issue(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("indicate a repository to view its issues")
|
||||||
|
|
||||||
|
issue = None
|
||||||
|
|
||||||
|
li = re.match("^#?([0-9]+)$", msg.args[0])
|
||||||
|
ri = re.match("^#?([0-9]+)$", msg.args[-1])
|
||||||
|
if li is not None:
|
||||||
|
issue = li.group(1)
|
||||||
|
del msg.args[0]
|
||||||
|
elif ri is not None:
|
||||||
|
issue = ri.group(1)
|
||||||
|
del msg.args[-1]
|
||||||
|
|
||||||
|
repo = " ".join(msg.args)
|
||||||
|
|
||||||
|
count = " (%d more issues)" if issue is None else None
|
||||||
|
res = Response(channel=msg.channel, nomore="No more issue", count=count)
|
||||||
|
|
||||||
|
issues = info_issue(repo, issue)
|
||||||
|
|
||||||
|
if issues is None:
|
||||||
|
raise IMException("Repository not found")
|
||||||
|
|
||||||
|
for issue in issues:
|
||||||
|
res.append_message("%s%s issue #%d: \x03\x02%s\x03\x02 opened by %s on %s: %s" %
|
||||||
|
(issue["state"][0].upper(),
|
||||||
|
issue["state"][1:],
|
||||||
|
issue["number"],
|
||||||
|
issue["title"],
|
||||||
|
issue["user"]["login"],
|
||||||
|
issue["created_at"],
|
||||||
|
issue["body"].replace("\n", " ")))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("github_commit",
|
||||||
|
help="Display repository's commits",
|
||||||
|
help_usage={
|
||||||
|
"REPO": "Display latest commits on REPO",
|
||||||
|
"REPO COMMIT": "Display details for the COMMIT on REPO",
|
||||||
|
})
|
||||||
|
def cmd_github_commit(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("indicate a repository to view its commits")
|
||||||
|
|
||||||
|
commit = None
|
||||||
|
if re.match("^[a-fA-F0-9]+$", msg.args[0]):
|
||||||
|
commit = msg.args[0]
|
||||||
|
del msg.args[0]
|
||||||
|
elif re.match("^[a-fA-F0-9]+$", msg.args[-1]):
|
||||||
|
commit = msg.args[-1]
|
||||||
|
del msg.args[-1]
|
||||||
|
|
||||||
|
repo = " ".join(msg.args)
|
||||||
|
|
||||||
|
count = " (%d more commits)" if commit is None else None
|
||||||
|
res = Response(channel=msg.channel, nomore="No more commit", count=count)
|
||||||
|
|
||||||
|
commits = info_commit(repo, commit)
|
||||||
|
|
||||||
|
if commits is None:
|
||||||
|
raise IMException("Repository or commit not found")
|
||||||
|
|
||||||
|
for commit in commits:
|
||||||
|
res.append_message("Commit %s by %s on %s: %s" %
|
||||||
|
(commit["sha"][:10],
|
||||||
|
commit["commit"]["author"]["name"],
|
||||||
|
commit["commit"]["author"]["date"],
|
||||||
|
commit["commit"]["message"].replace("\n", " ")))
|
||||||
|
return res
|
||||||
85
modules/grep.py
Normal file
85
modules/grep.py
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
"""Filter messages, displaying lines matching a pattern"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from nemubot import context
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.message import Command, Text
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
def grep(fltr, cmd, msg, icase=False, only=False):
|
||||||
|
"""Perform a grep like on known nemubot structures
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
fltr -- The filter regexp
|
||||||
|
cmd -- The subcommand to execute
|
||||||
|
msg -- The original message
|
||||||
|
icase -- like the --ignore-case parameter of grep
|
||||||
|
only -- like the --only-matching parameter of grep
|
||||||
|
"""
|
||||||
|
|
||||||
|
fltr = re.compile(fltr, re.I if icase else 0)
|
||||||
|
|
||||||
|
for r in context.subtreat(context.subparse(msg, cmd)):
|
||||||
|
if isinstance(r, Response):
|
||||||
|
for i in range(len(r.messages) - 1, -1, -1):
|
||||||
|
if isinstance(r.messages[i], list):
|
||||||
|
for j in range(len(r.messages[i]) - 1, -1, -1):
|
||||||
|
res = fltr.match(r.messages[i][j])
|
||||||
|
if not res:
|
||||||
|
r.messages[i].pop(j)
|
||||||
|
elif only:
|
||||||
|
r.messages[i][j] = res.group(1) if fltr.groups else res.group(0)
|
||||||
|
if len(r.messages[i]) <= 0:
|
||||||
|
r.messages.pop(i)
|
||||||
|
elif isinstance(r.messages[i], str):
|
||||||
|
res = fltr.match(r.messages[i])
|
||||||
|
if not res:
|
||||||
|
r.messages.pop(i)
|
||||||
|
elif only:
|
||||||
|
r.messages[i] = res.group(1) if fltr.groups else res.group(0)
|
||||||
|
yield r
|
||||||
|
|
||||||
|
elif isinstance(r, Text):
|
||||||
|
res = fltr.match(r.message)
|
||||||
|
if res:
|
||||||
|
if only:
|
||||||
|
r.message = res.group(1) if fltr.groups else res.group(0)
|
||||||
|
yield r
|
||||||
|
|
||||||
|
else:
|
||||||
|
yield r
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("grep",
|
||||||
|
help="Display only lines from a subcommand matching the given pattern",
|
||||||
|
help_usage={"PTRN !SUBCMD": "Filter SUBCMD command using the pattern PTRN"},
|
||||||
|
keywords={
|
||||||
|
"nocase": "Perform case-insensitive matching",
|
||||||
|
"only": "Print only the matched parts of a matching line",
|
||||||
|
})
|
||||||
|
def cmd_grep(msg):
|
||||||
|
if len(msg.args) < 2:
|
||||||
|
raise IMException("Please provide a filter and a command")
|
||||||
|
|
||||||
|
only = "only" in msg.kwargs
|
||||||
|
|
||||||
|
l = [m for m in grep(msg.args[0] if len(msg.args[0]) and msg.args[0][0] == "^" else ".*?(" + msg.args[0] + ").*?",
|
||||||
|
" ".join(msg.args[1:]),
|
||||||
|
msg,
|
||||||
|
icase="nocase" in msg.kwargs,
|
||||||
|
only=only) if m is not None]
|
||||||
|
|
||||||
|
if len(l) <= 0:
|
||||||
|
raise IMException("Pattern not found in output")
|
||||||
|
|
||||||
|
return l
|
||||||
115
modules/imdb.py
Normal file
115
modules/imdb.py
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
"""Show many information about a movie or serie"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
|
import re
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.tools import web
|
||||||
|
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE CORE #########################################################
|
||||||
|
|
||||||
|
def get_movie_by_id(imdbid):
|
||||||
|
"""Returns the information about the matching movie"""
|
||||||
|
|
||||||
|
url = "http://www.imdb.com/title/" + urllib.parse.quote(imdbid)
|
||||||
|
soup = BeautifulSoup(web.getURLContent(url))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"imdbID": imdbid,
|
||||||
|
"Title": soup.body.find('h1').contents[0].strip(),
|
||||||
|
"Year": soup.body.find(id="titleYear").find("a").text.strip() if soup.body.find(id="titleYear") else ", ".join([y.text.strip() for y in soup.body.find(attrs={"class": "seasons-and-year-nav"}).find_all("a")[1:]]),
|
||||||
|
"Duration": soup.body.find(attrs={"class": "title_wrapper"}).find("time").text.strip() if soup.body.find(attrs={"class": "title_wrapper"}).find("time") else None,
|
||||||
|
"imdbRating": soup.body.find(attrs={"class": "ratingValue"}).find("strong").text.strip() if soup.body.find(attrs={"class": "ratingValue"}) else None,
|
||||||
|
"imdbVotes": soup.body.find(attrs={"class": "imdbRating"}).find("a").text.strip() if soup.body.find(attrs={"class": "imdbRating"}) else None,
|
||||||
|
"Plot": re.sub(r"\s+", " ", soup.body.find(attrs={"class": "summary_text"}).text).strip(),
|
||||||
|
|
||||||
|
"Type": "TV Series" if soup.find(id="title-episode-widget") else "Movie",
|
||||||
|
"Genre": ", ".join([x.text.strip() for x in soup.body.find(id="titleStoryLine").find_all("a") if x.get("href") is not None and x.get("href")[:21] == "/search/title?genres="]),
|
||||||
|
"Country": ", ".join([x.text.strip() for x in soup.body.find(id="titleDetails").find_all("a") if x.get("href") is not None and x.get("href")[:32] == "/search/title?country_of_origin="]),
|
||||||
|
"Credits": " ; ".join([x.find("h4").text.strip() + " " + (", ".join([y.text.strip() for y in x.find_all("a") if y.get("href") is not None and y.get("href")[:6] == "/name/"])) for x in soup.body.find_all(attrs={"class": "credit_summary_item"})]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def find_movies(title, year=None):
|
||||||
|
"""Find existing movies matching a approximate title"""
|
||||||
|
|
||||||
|
title = title.lower()
|
||||||
|
|
||||||
|
# Built URL
|
||||||
|
url = "https://v2.sg.media-imdb.com/suggests/%s/%s.json" % (urllib.parse.quote(title[0]), urllib.parse.quote(title.replace(" ", "_")))
|
||||||
|
|
||||||
|
# Make the request
|
||||||
|
data = web.getJSON(url, remove_callback=True)
|
||||||
|
|
||||||
|
if "d" not in data:
|
||||||
|
return None
|
||||||
|
elif year is None:
|
||||||
|
return data["d"]
|
||||||
|
else:
|
||||||
|
return [d for d in data["d"] if "y" in d and str(d["y"]) == year]
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("imdb",
|
||||||
|
help="View movie/serie details, using OMDB",
|
||||||
|
help_usage={
|
||||||
|
"TITLE": "Look for a movie titled TITLE",
|
||||||
|
"IMDB_ID": "Look for the movie with the given IMDB_ID",
|
||||||
|
})
|
||||||
|
def cmd_imdb(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("precise a movie/serie title!")
|
||||||
|
|
||||||
|
title = ' '.join(msg.args)
|
||||||
|
|
||||||
|
if re.match("^tt[0-9]{7}$", title) is not None:
|
||||||
|
data = get_movie_by_id(imdbid=title)
|
||||||
|
else:
|
||||||
|
rm = re.match(r"^(.+)\s\(([0-9]{4})\)$", title)
|
||||||
|
if rm is not None:
|
||||||
|
data = find_movies(rm.group(1), year=rm.group(2))
|
||||||
|
else:
|
||||||
|
data = find_movies(title)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
raise IMException("Movie/series not found")
|
||||||
|
|
||||||
|
data = get_movie_by_id(data[0]["id"])
|
||||||
|
|
||||||
|
res = Response(channel=msg.channel,
|
||||||
|
title="%s (%s)" % (data['Title'], data['Year']),
|
||||||
|
nomore="No more information, more at http://www.imdb.com/title/%s" % data['imdbID'])
|
||||||
|
|
||||||
|
res.append_message("%s \x02genre:\x0F %s; \x02rating\x0F: %s (%s votes); \x02plot\x0F: %s" %
|
||||||
|
(data['Type'], data['Genre'], data['imdbRating'], data['imdbVotes'], data['Plot']))
|
||||||
|
res.append_message("%s \x02from\x0F %s; %s"
|
||||||
|
% (data['Type'], data['Country'], data['Credits']))
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@hook.command("imdbs",
|
||||||
|
help="Search a movie/serie by title",
|
||||||
|
help_usage={
|
||||||
|
"TITLE": "Search a movie/serie by TITLE",
|
||||||
|
})
|
||||||
|
def cmd_search(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("precise a movie/serie title!")
|
||||||
|
|
||||||
|
data = find_movies(' '.join(msg.args))
|
||||||
|
|
||||||
|
movies = list()
|
||||||
|
for m in data:
|
||||||
|
movies.append("\x02%s\x0F%s with %s" % (m['l'], (" (" + str(m['y']) + ")") if "y" in m else "", m['s']))
|
||||||
|
|
||||||
|
return Response(movies, title="Titles found", channel=msg.channel)
|
||||||
58
modules/jsonbot.py
Normal file
58
modules/jsonbot.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
from nemubot.hooks import hook
|
||||||
|
from nemubot.exception import IMException
|
||||||
|
from nemubot.tools import web
|
||||||
|
from nemubot.module.more import Response
|
||||||
|
import json
|
||||||
|
|
||||||
|
nemubotversion = 3.4
|
||||||
|
|
||||||
|
def help_full():
|
||||||
|
return "Retrieves data from json"
|
||||||
|
|
||||||
|
def getRequestedTags(tags, data):
|
||||||
|
response = ""
|
||||||
|
if isinstance(data, list):
|
||||||
|
for element in data:
|
||||||
|
repdata = getRequestedTags(tags, element)
|
||||||
|
if response:
|
||||||
|
response = response + "\n" + repdata
|
||||||
|
else:
|
||||||
|
response = repdata
|
||||||
|
else:
|
||||||
|
for tag in tags:
|
||||||
|
if tag in data.keys():
|
||||||
|
if response:
|
||||||
|
response += ", " + tag + ": " + str(data[tag])
|
||||||
|
else:
|
||||||
|
response = tag + ": " + str(data[tag])
|
||||||
|
return response
|
||||||
|
|
||||||
|
def getJsonKeys(data):
|
||||||
|
if isinstance(data, list):
|
||||||
|
pkeys = []
|
||||||
|
for element in data:
|
||||||
|
keys = getJsonKeys(element)
|
||||||
|
for key in keys:
|
||||||
|
if not key in pkeys:
|
||||||
|
pkeys.append(key)
|
||||||
|
return pkeys
|
||||||
|
else:
|
||||||
|
return data.keys()
|
||||||
|
|
||||||
|
@hook.command("json")
|
||||||
|
def get_json_info(msg):
|
||||||
|
if not len(msg.args):
|
||||||
|
raise IMException("Please specify a url and a list of JSON keys.")
|
||||||
|
|
||||||
|
request_data = web.getURLContent(msg.args[0].replace(' ', "%20"))
|
||||||
|
if not request_data:
|
||||||
|
raise IMException("Please specify a valid url.")
|
||||||
|
json_data = json.loads(request_data)
|
||||||
|
|
||||||
|
if len(msg.args) == 1:
|
||||||
|
raise IMException("Please specify the keys to return (%s)" % ", ".join(getJsonKeys(json_data)))
|
||||||
|
|
||||||
|
tags = ','.join(msg.args[1:]).split(',')
|
||||||
|
response = getRequestedTags(tags, json_data)
|
||||||
|
|
||||||
|
return Response(response, channel=msg.channel, nomore="No more content", count=" (%d more lines)")
|
||||||
|
|
@ -1,66 +1,78 @@
|
||||||
# coding=utf-8
|
"""Read manual pages on IRC"""
|
||||||
|
|
||||||
|
# PYTHON STUFFS #######################################################
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
|
||||||
nemubotversion = 3.3
|
from nemubot.hooks import hook
|
||||||
|
|
||||||
def load(context):
|
from nemubot.module.more import Response
|
||||||
from hooks import Hook
|
|
||||||
add_hook("cmd_hook", Hook(cmd_man, "MAN"))
|
|
||||||
add_hook("cmd_hook", Hook(cmd_whatis, "man"))
|
|
||||||
|
|
||||||
def help_tiny ():
|
|
||||||
"""Line inserted in the response to the command !help"""
|
|
||||||
return "Read man on IRC"
|
|
||||||
|
|
||||||
def help_full ():
|
# GLOBALS #############################################################
|
||||||
return "!man [0-9] /what/: gives informations about /what/."
|
|
||||||
|
|
||||||
RGXP_s = re.compile(b'\x1b\\[[0-9]+m')
|
RGXP_s = re.compile(b'\x1b\\[[0-9]+m')
|
||||||
|
|
||||||
|
|
||||||
|
# MODULE INTERFACE ####################################################
|
||||||
|
|
||||||
|
@hook.command("MAN",
|
||||||
|
help="Show man pages",
|
||||||
|
help_usage={
|
||||||
|
"SUBJECT": "Display the default man page for SUBJECT",
|
||||||
|
"SECTION SUBJECT": "Display the man page in SECTION for SUBJECT"
|
||||||
|
})
|
||||||
def cmd_man(msg):
|
def cmd_man(msg):
|
||||||
args = ["man"]
|
args = ["man"]
|
||||||
num = None
|
num = None
|
||||||
if len(msg.cmds) == 2:
|
if len(msg.args) == 1:
|
||||||
args.append(msg.cmds[1])
|
args.append(msg.args[0])
|
||||||
elif len(msg.cmds) >= 3:
|
elif len(msg.args) >= 2:
|
||||||
try:
|
try:
|
||||||
num = int(msg.cmds[1])
|
num = int(msg.args[0])
|
||||||
args.append("%d" % num)
|
args.append("%d" % num)
|
||||||
args.append(msg.cmds[2])
|
args.append(msg.args[1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
args.append(msg.cmds[1])
|
args.append(msg.args[0])
|
||||||
|
|
||||||
os.unsetenv("LANG")
|
os.unsetenv("LANG")
|
||||||
res = Response(msg.sender, channel=msg.channel)
|
res = Response(channel=msg.channel)
|
||||||
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
|
with subprocess.Popen(args,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE) as proc:
|
||||||
for line in proc.stdout.read().split(b"\n"):
|
for line in proc.stdout.read().split(b"\n"):
|
||||||
(line, n) = RGXP_s.subn(b'', line)
|
(line, n) = RGXP_s.subn(b'', line)
|
||||||
res.append_message(line.decode())
|
res.append_message(line.decode())
|
||||||
|
|
||||||
if len(res.messages) <= 0:
|
if len(res.messages) <= 0:
|
||||||
if num is not None:
|
if num is not None:
|
||||||
res.append_message("Il n'y a pas d'entrée %s dans la section %d du manuel." % (msg.cmds[1], num))
|
res.append_message("There is no entry %s in section %d." %
|
||||||
|
(msg.args[0], num))
|
||||||
else:
|
else:
|
||||||
res.append_message("Il n'y a pas de page de manuel pour %s." % msg.cmds[1])
|
res.append_message("There is no man page for %s." % msg.args[0])
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def cmd_whatis(msg):
|
|
||||||
args = ["whatis", " ".join(msg.cmds[1:])]
|
|
||||||
|
|
||||||
res = Response(msg.sender, channel=msg.channel)
|
@hook.command("man",
|
||||||
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
|
help="Show man pages synopsis (in one line)",
|
||||||
|
help_usage={
|
||||||
|
"SUBJECT": "Display man page synopsis for SUBJECT",
|
||||||
|
})
|
||||||
|
def cmd_whatis(msg):
|
||||||
|
args = ["whatis", " ".join(msg.args)]
|
||||||
|
|
||||||
|
res = Response(channel=msg.channel)
|
||||||
|
with subprocess.Popen(args,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE) as proc:
|
||||||
for line in proc.stdout.read().split(b"\n"):
|
for line in proc.stdout.read().split(b"\n"):
|
||||||
(line, n) = RGXP_s.subn(b'', line)
|
(line, n) = RGXP_s.subn(b'', line)
|
||||||
res.append_message(" ".join(line.decode().split()))
|
res.append_message(" ".join(line.decode().split()))
|
||||||
|
|
||||||
if len(res.messages) <= 0:
|
if len(res.messages) <= 0:
|
||||||
if num is not None:
|
res.append_message("There is no man page for %s." % msg.args[0])
|
||||||
res.append_message("Il n'y a pas d'entrée %s dans la section %d du manuel." % (msg.cmds[1], num))
|
|
||||||
else:
|
|
||||||
res.append_message("Il n'y a pas de page de manuel pour %s." % msg.cmds[1])
|
|
||||||
|
|||||||