Compare commits

...

259 commits

Author SHA1 Message Date
27bd0c50c1 server: Fix on_disconnect AttributeError when irc library lacks the method
All checks were successful
continuous-integration/drone/push Build is passing
Replace super().on_disconnect() call (absent in some irc library versions)
with self.jump_server() which is the actual reconnect method provided by
SingleServerIRCBot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 15:55:24 +07:00
9d7c278d1a bot: Fix sequential message processing with proper consumer pool
All checks were successful
continuous-integration/drone/push Build is passing
Replace the flawed cnsr_thrd_size threshold with cnsr_active, which
tracks the number of consumers currently executing a task. A new
consumer thread is now spawned the moment the queue is non-empty and
all existing consumers are busy, enabling true parallel execution of
slow and fast commands. The pool is capped at os.cpu_count() threads.

- bot.py: replace cnsr_thrd_size with cnsr_active + cnsr_lock + cnsr_max
- consumer.py: increment/decrement cnsr_active around stm.run(), remove
  itself from cnsr_thrd under the lock, mark thread as daemon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:08:27 +07:00
310f933091 bot: fix duplicate unregister KeyError and improve connection error logging
All checks were successful
continuous-integration/drone/push Build is passing
Silently ignore KeyError when unregistering an already-removed FD from
the poll loop (servers can queue multiple close events). Also include
the exception message when a server connection fails at startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 22:34:14 +07:00
26282cb81d server: Replace hand-rolled IRC with irc (jaraco) library
Switch the IRC server implementation from the custom socket-based parser
to the irc Python library (SingleServerIRCBot), gaining automatic
exponential-backoff reconnection, built-in PING/PONG handling, and
nick-collision recovery for free.

- Add IRCLib server (server/IRCLib.py) extending ThreadedServer:
  _IRCBotAdapter wraps SingleServerIRCBot with a threading.Event stop
  flag so shutdown is clean and on_disconnect skips reconnect when
  stopping. subparse() is implemented directly for alias/grep/rnd/cat.
- Add IRCLib printer (message/printer/IRCLib.py) calling
  connection.privmsg() directly instead of building raw PRIVMSG strings.
- Update factory to use IRCLib for irc:// and ircs://; SSL is now
  passed as a connect_factory kwarg rather than post-hoc socket wrapping.
- Add irc to requirements.txt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 22:34:14 +07:00
de2c37a54a matrix: Add Matrix server support via matrix-nio
Implement Matrix protocol support with MatrixServer (ThreadedServer subclass),
a Matrix message printer, factory URI parsing for matrix:// schemes, and
matrix-nio[e2e] dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 21:53:15 +07:00
622159f6b5 server: Add threaded server implementation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 15:47:41 +07:00
32ebe42f41 Don't compile for arm (requires rust ?????)
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-07 23:38:49 +01:00
ea0ec42a4b openai: Add commands list_models and set_model 2025-02-07 23:38:23 +01:00
23f043673f Add openai module 2025-02-07 21:41:02 +01:00
ac432fabcc Keep in 3.11
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-10-15 01:19:30 +02:00
38b5b1eabd Also build for arm64
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-10-14 23:01:11 +00:00
84fef789b5 Ignore decoding error when charset is erroneous
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-23 09:20:21 +02:00
45a27b477d syno: Fix new service URL
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-08 19:07:12 +02:00
a8472ecc29 CI: Fix build on arm
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-18 15:27:38 +01:00
861ca0afdd Try to connect multiple times (with different servers if any)
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-01-17 21:55:25 +01:00
9f83e5b178 Dockerfile: Point home into volume
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-06 14:10:30 +01:00
68c61f40d3 Rework Dockerfile and run as user
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-06 12:52:09 +01:00
60a9ec92b7 Rework Dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-11 10:46:53 +02:00
8dd6b9d471 Fix a strange problem with saved PID file between runs
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-01 00:47:14 +01:00
13c643fc19 Add missing deps in container
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-08 14:04:42 +01:00
5ec2f2997b Add drone CI
Some checks reported errors
continuous-integration/drone/push Build was killed
2020-10-18 18:06:16 +02:00
faa5759645 suivi: add UPS tracking infos 2019-12-03 15:54:50 +01:00
37a230e70e suivi: kuse new laposte API to get infos 2019-12-03 15:54:50 +01:00
904ad72316 suivi: fix usps 2019-12-03 15:54:49 +01:00
f17f8b9dfa nntp: keep in memory latests news seen to avoid loop 2019-12-03 15:54:49 +01:00
d56c2396c0 repology: new module 2019-12-02 17:49:08 +01:00
b369683914 nntp: use timestamp from servers to handle desynchronized clocks 2019-11-09 14:46:32 +01:00
aee2da4122 Don't silent Exception in line_treat. Skip the treatment, but log. 2019-09-21 02:01:29 +02:00
1f2d297ea1 Update travis python versions 2019-09-21 01:16:52 +02:00
Maxence
b72871a8c2 Remove rms.py module
It should be replaced by a more generic:
!news https://www.fsf.org/static/fsforg/rss/events.xml which can be
aliased.
2019-09-21 01:15:13 +02:00
644a641b13 nntp: fix bad behaviour with UTF-8 encoded headers
Read-Also: https://tools.ietf.org/html/rfc3977
2019-09-10 15:50:17 +02:00
4499677d55 whois: don't use custom picture anymore 2019-08-22 15:51:36 +02:00
87b5ce842d cve: fix module with new cve website 2019-08-22 15:51:10 +02:00
144551a232 imdb: fix unrated content 2019-06-14 19:33:51 +02:00
7854e8628f news: reduce link URL by default 2019-02-03 12:59:17 +01:00
85c418bd06 feed: fix RSS link handling 2019-02-03 12:59:17 +01:00
20c19a72bc urlreducer: new function to be used in responses' treat_line 2019-02-03 12:59:17 +01:00
9417e2ba93 urlreducer: define DEFAULT_PROVIDER later 2019-02-03 12:59:17 +01:00
517bf21d25 imdb: fix series changed attributes 2019-02-03 12:59:17 +01:00
fa0f2e93ef whois: update module 2018-12-30 10:59:10 +01:00
b349d22370 events: ModuleEvent don't store function argument anymore 2018-12-30 00:42:21 +01:00
8605932702 imdb: follow imdb.com evolutions 2018-12-03 23:55:25 +01:00
10b8ce8940 smmry: add keywords options 2018-09-25 21:25:57 +02:00
445a66ea90 hooks: keywords can have optional values: place a question mark before =
{
	    "keyword?=X": "help about keyword (precise the default value if needed)"
	}
2018-09-25 21:25:02 +02:00
46541cb35e smmry: handle some more options 2018-09-25 21:23:46 +02:00
8224d11207 smmry: use URLStack from urlreducer module 2018-09-25 20:50:53 +02:00
15064204d8 smmry: don't ask to URLencode returned texts 2018-09-25 20:46:43 +02:00
67dee382a6 New module smmry, using https://smmry.com/ API 2018-09-25 20:43:21 +02:00
f1da640a5b tools/web: fix isURL function 2018-09-25 20:42:51 +02:00
2fd20d9002 nntp: Here it is! 2018-09-09 19:33:42 +02:00
31abcc97cf event: extract dict before call without init data 2018-09-09 19:17:56 +02:00
53fe00ed58 tools/web: new function to retrieve only headers 2018-08-30 12:59:55 +02:00
4a636b2b11 tools/web: follow redirection in URLConn 2018-08-30 12:59:28 +02:00
72bc8d3839 feed: accept RSS that begins with <rss> tag 2018-08-30 12:54:06 +02:00
5578e8b86e tools/web: split getURLContent function 2018-08-30 11:32:59 +02:00
3b99099b52 imdb: fix compatibility with new IMDB version 2018-08-30 11:20:36 +02:00
cd6750154c worldcup: update module to 2018 worldcup 2018-06-21 17:28:28 +02:00
b8741bb1f7 imdb: fix exception when no movie found 2018-06-20 17:46:52 +02:00
125ae6ad0b feed: fix RSS parsing 2018-06-06 14:50:45 +02:00
015fb47d90 events: alert on malformed start command 2018-06-06 14:50:45 +02:00
8a25ebb45b xmlparser: fix parsing of subchild 2018-06-06 14:50:45 +02:00
1887e481d2 sms: send result of command by SMS 2018-06-06 14:50:45 +02:00
342bb9acdc Refactor in treatment analysis 2018-06-06 14:50:44 +02:00
2af56e606a events: Use the new data parser, knodes based 2018-01-14 19:19:02 +01:00
4275009dea events: now support timedelta instead of int/float 2018-01-14 19:19:02 +01:00
f520c67c89 context: new function to define default data, instead of None 2018-01-14 19:19:02 +01:00
c3c7484792 In debug mode, display the last stack element to be able to trace 2018-01-14 19:19:02 +01:00
4cd099e087 xmlparser: make DictNode more usable 2018-01-14 19:19:02 +01:00
d528746cb5 datastore: support custom knodes instead of nemubotstate 2018-01-14 19:19:01 +01:00
Max
5cbad96492 [module] RMS upcoming locations 2018-01-14 19:19:01 +01:00
226ee4e34e ctfs: update module reflecting site changes 2018-01-14 19:19:01 +01:00
Max
e0d7ef1314 Fix https links when available, everywhere 2018-01-14 19:19:01 +01:00
Max
99384ad6f7 [suivi] Fix awkward USPS message 2018-01-14 19:19:01 +01:00
Max
ef4c119f1f [suivi] Add https when supported by service 2018-01-14 19:19:01 +01:00
Max
a7f4ccc959 [suivi] Fix Fedex tracking 2018-01-14 19:19:00 +01:00
Max
c23dc22ce2 [suivi] Fix colissimo tracking 2018-01-14 19:19:00 +01:00
bb0e958118 grep: allow the pattern to be empty 2018-01-14 19:19:00 +01:00
b15d18b3a5 events: fix event removal 2018-01-14 19:19:00 +01:00
5646850df1 Don't launch timer thread before bot launch 2018-01-14 19:19:00 +01:00
30ec912162 daemonize: fork client before loading context 2018-01-14 19:19:00 +01:00
28d4e507eb servers: call recv late 2018-01-14 19:18:59 +01:00
62cd92e1cb server: Rework factory tests 2018-01-14 19:18:59 +01:00
12ddf40ef4 servers: use proxy design pattern instead of inheritance, because Python ssl patch has benn refused 2018-01-14 19:18:59 +01:00
7a4b27510c Replace logger by _logger in servers 2018-01-14 19:18:59 +01:00
05d20ed6ee weather: handle units 2018-01-14 19:18:59 +01:00
f60de818f2 Virtualy move all nemubot modules into nemubot.module.* hierarchy, to avoid conflict with system/vendor modules 2018-01-14 19:18:58 +01:00
45fe5b2156 Refactor configuration loading 2017-09-19 07:25:34 +02:00
e49312e63e Remove legacy msg.text 2017-09-19 07:25:34 +02:00
a11ccb2e39 Remove legacy msg.cmds 2017-09-19 07:25:33 +02:00
fde459c3ff Remove legacy msg.nick 2017-09-19 07:25:33 +02:00
694c54a6bc imdb: switch to ugly IMDB HTML parsing 2017-09-19 07:25:33 +02:00
1dae3c713a tools/web: new option to remove callback from JSON files 2017-09-19 07:25:33 +02:00
89772ebce0 whois: now able to use a CRI API dump 2017-09-19 07:25:33 +02:00
aa81aa4e96 dig: better parse dig syntax @ and some + 2017-09-19 07:25:33 +02:00
3c7ed176c0 dig: new module 2017-09-19 07:25:33 +02:00
6dda142188 shodan: introducing new module to search on shodan 2017-09-19 07:25:33 +02:00
dcb44ca3f2 tools/web: new parameter to choose max content size to retrieve 2017-09-19 07:25:33 +02:00
0a576410c7 cve: improve read of partial and inexistant CVE 2017-09-19 07:25:32 +02:00
128afb5914 disas: new module, aim to disassemble binary code. Closing #67 2017-09-19 07:25:32 +02:00
39b7b1ae2f freetarifs: new module 2017-09-19 07:25:32 +02:00
09462d0d90 suivi: support USPS 2017-09-19 07:25:32 +02:00
b8f4560780 suivi: support DHL 2017-09-19 07:25:32 +02:00
281d81acc4 suivi: fix error handling of fedex parcel 2017-09-19 07:25:32 +02:00
29817ba1c1 pkgs: new module to display quick information about common softwares 2017-09-19 07:25:32 +02:00
b517cac4cf Fix module unloading 2017-09-19 07:25:32 +02:00
f81349bbfd Store module into weakref 2017-09-19 07:25:32 +02:00
ce012b7017 datastore/xml: handle entire file save and be closer with new nemubot XML API 2017-09-19 07:25:32 +02:00
Max
e3b6c3b85e Set urlreducer to use https 2017-09-19 07:25:31 +02:00
39056cf358 tools/xmlparser: implement writer 2017-08-23 21:13:39 +02:00
f16dedb320 openroute: new module providing geocode and direction instructions
Closing issue #46
2017-08-23 21:13:39 +02:00
171297b581 tools/web: new option decode_error to decode non-200 page content (useful on REST API) 2017-08-23 21:13:39 +02:00
3267c3e2e1 tools/web: display socket timeout 2017-08-23 21:13:39 +02:00
aad777058e cve: update and clean module, following NIST website changes 2017-08-23 21:13:38 +02:00
f633a3effe socket: limit getaddrinfo to TCP connections 2017-07-19 10:36:28 +02:00
bbfecdfced events: fix help when no event is defined 2017-07-19 10:36:28 +02:00
94ff951b2e run: recreate the sync_queue on run, it seems to have strange behaviour when created before the fork 2017-07-19 10:36:28 +02:00
a5479d7b0d event: ensure that enough consumers are launched at the end of an event 2017-07-19 10:36:28 +02:00
0a3744577d rename module nextstop: ratp to avoid import loop with the inderlying Python module 2017-07-19 10:36:27 +02:00
67cb3caa95 main: new option -A to run as daemon 2017-07-19 10:36:27 +02:00
2265e1a096 Use getaddrinfo to create the right socket 2017-07-19 10:36:27 +02:00
b6945cf81c Try to restaure frm_owner flag 2017-07-19 10:36:27 +02:00
e8809b77d2 When launched in daemon mode, attach to the socket 2017-07-19 10:36:27 +02:00
9d446cbd14 Deamonize later 2017-07-19 10:36:27 +02:00
cde4ee05f7 Local client now detects when server close the connection 2017-07-19 10:36:27 +02:00
ac0cf729f1 Fix communication over unix socket 2017-07-19 10:36:27 +02:00
35e0890563 Handle multiple SIGTERM 2017-07-19 10:36:27 +02:00
58c349eb2c suivi: add fedex 2017-07-19 10:36:26 +02:00
bcd57e61ea suivi: use getURLContent instead of call to urllib 2017-07-19 10:36:26 +02:00
0be6ebcd4b tools/web: fill a default Content-Type in case of POST 2017-07-19 10:36:26 +02:00
b4218478bd tools/web: improve redirection reliability 2017-07-19 10:36:26 +02:00
6ac9fc4857 tools/web: forward all arguments passed to getJSON and getXML to getURLContent 2017-07-19 10:36:26 +02:00
8a96f7bee9 Update weather module: refleting forcastAPI changes 2017-07-19 10:36:26 +02:00
7791f24423 modulecontext: use inheritance instead of conditional init 2017-07-16 21:17:48 +02:00
b809451be2 Avoid stack-trace and DOS if event is not well formed 2017-07-16 21:17:48 +02:00
dbcc7c664f [nextstop] Use as system wide module 2017-07-16 21:17:48 +02:00
8de31d784b Allow module function to be generators 2017-07-16 21:17:48 +02:00
f4216af7c7 Parse server urls using parse_qs 2017-07-16 21:17:48 +02:00
cf8e1cffc5 Format and typo 2017-07-16 21:17:48 +02:00
97a1385903 Implement socket server subparse 2017-07-16 21:17:48 +02:00
764e6f070b Refactor file/socket management (use poll instead of select) 2017-07-16 21:17:48 +02:00
6d8dca211d Use fileno instead of name to index existing servers 2017-07-16 21:17:48 +02:00
1c21231f31 Use super() instead of parent class name 2017-07-16 21:17:48 +02:00
2a3cd07c63 Documentation 2017-07-16 21:17:48 +02:00
b5d5a67b2d In debug mode, display running thread at exit 2017-07-16 21:17:48 +02:00
4c11c5e215 Handle case where frm and to have not been filled 2017-07-16 21:17:48 +02:00
a0c3f6d2b3 Review consumer errors 2017-07-16 21:17:48 +02:00
7cf73fb84a Remove reload feature
As reload shoudl be done in a particular order, to keep valid types, and because maintaining such system is too complex (currently, it doesn't work for a while), now, a reload is just reload configuration file (and possibly modules)
2017-07-16 21:17:48 +02:00
6cd299ab60 New keywords class that accepts any keywords 2017-07-16 21:17:48 +02:00
fc14c76b6d [rnd] Add new function choiceres which pick a random response returned by a given subcommand 2017-07-16 21:17:48 +02:00
24eb9a6911 Can attach to the main process 2017-07-16 21:17:48 +02:00
38fd9e5091 Remove legacy prompt 2017-07-14 12:35:55 +02:00
a7d7013639 Fix and improve reload process 2017-07-14 12:35:55 +02:00
9dc385a32a New argument: --socketfile that create a socket for internal communication 2017-07-14 12:35:55 +02:00
150d069dfb New CLI argument: --pidfile, path to store the daemon PID 2017-07-14 12:35:55 +02:00
f160411f71 Catch SIGUSR1: log threads stack traces 2017-07-14 12:35:55 +02:00
b0678ceb84 Extract deamonize to a dedicated function that can be called from anywhere 2017-07-14 12:35:55 +02:00
57275f5735 Catch SIGHUP: deep reload 2017-07-14 12:35:55 +02:00
ec512fc540 Do a proper close on SIGINT and SIGTERM 2017-07-14 12:35:55 +02:00
7bc37617b0 Remove prompt at launch 2017-07-14 12:35:55 +02:00
1858a045cc Introducing daemon mode 2017-07-14 12:35:55 +02:00
1b108428c2 Fix issue with some non-text messages 2017-07-14 12:35:55 +02:00
08a7701238 whois: add @lookup keyword to perform research in the list 2017-07-14 12:35:55 +02:00
be9492c151 weather: don't show expire date if not provided 2017-07-14 12:35:30 +02:00
679f50b730 alias: fix lookup replacement when empty list 2017-07-14 12:30:21 +02:00
2334bc502a alias: add syntax to handle default variable replacement 2017-07-14 12:30:21 +02:00
c3f2c89c7c alias: only perform alias expansion on Command 2017-07-14 12:30:15 +02:00
920506c702 wolframalpha: avoid content that is not plaintext 2017-07-14 09:33:34 +02:00
aefc0bb534 event: don't forward d_init if None 2017-07-14 09:32:41 +02:00
52b3bfa945 suivi: add postnl tracking 2017-07-14 09:23:57 +02:00
c8c9112b0f syno: CRISCO now speaks utf-8 2017-07-14 08:56:37 +02:00
7a48dc2cef whois: new URL to pick picts 2016-09-19 01:01:44 +02:00
0efee0cb83 [mediawiki] parse Infobox 2016-07-30 21:11:38 +02:00
3301fb87c2 [more] line_treat over an array 2016-07-30 21:11:25 +02:00
3f2b18cae8 [mediawiki] Permit control of ssl and absolute path through keywords 2016-07-30 21:09:18 +02:00
e9cea5d010 Fix events expiration 2016-07-29 03:13:37 +02:00
Max
f2c44a1108 Add virtual radar flight tracking module 2016-07-23 00:17:23 +02:00
Max
f15ebd7c02 [suivi] Fix TNT tracking 2016-07-23 00:17:23 +02:00
91b550754f [cve] Reflects site changes 2016-07-08 22:27:45 +02:00
abf810209e [alias] Fix empty error message 2016-04-18 17:21:51 +02:00
358499e6d5 Expect IM keyword argument in command to be at the begining of the args list 2016-04-18 17:21:51 +02:00
5fae67255b Log Python version 2016-04-08 12:55:24 +02:00
2c3d61495f Welcome in 2016... Happy new year! 2016-04-08 12:55:24 +02:00
a05821620d [alias] Allow arguments only on Command 2016-04-08 12:55:23 +02:00
26668c81b1 Update README 2016-03-01 16:56:19 +01:00
663e5e7207 Don't force python3.3 2016-03-01 16:56:18 +01:00
6ad979a5eb Fix event/timer issue if very close to 0 2016-01-29 16:52:52 +01:00
09e3b082c1 [alias] Give near alias in case of error 2016-01-24 18:44:28 +01:00
ff6460b92e Fix IRC message parameter escape 2016-01-19 18:45:14 +01:00
d028afd09e [alias] use subparse method 2016-01-19 14:50:47 +01:00
bd2eff83b7 [alias] Use alias command to display and define new aliases 2016-01-15 18:56:56 +01:00
645c18c981 [grep] use subparse feature 2016-01-12 23:54:26 +01:00
1d13d56dce [cat] New module performing cat like action 2016-01-12 23:53:43 +01:00
277d55d521 Add subparse method in context, that use server parser 2016-01-12 18:09:22 +01:00
d705d351c0 [grep] Add @nocase option, --ignore-case like 2016-01-12 16:50:49 +01:00
9ff8a3a02b [grep] raise an IMException if pattern not found 2016-01-12 16:49:51 +01:00
1d18305870 [grep] Add -o option 2016-01-12 16:39:01 +01:00
009ab08821 [man] Dusting + fill help 2015-12-28 19:20:31 +01:00
313c693d48 [imdb] Dusting + fill help 2015-12-28 19:15:32 +01:00
c9801ee2f7 [mapquest] Dusting + fill help 2015-12-28 19:10:00 +01:00
a089efff1a [more] Don't append space after a cut not ended by space 2015-12-28 19:09:41 +01:00
d4b6283e23 [github] new command to retrieve SSH keys 2015-12-28 19:09:20 +01:00
a3236cd67a [github] Dusting + fill help 2015-12-28 19:08:33 +01:00
274836e39a [github] Use default HTTP request timeout 2015-12-28 19:08:07 +01:00
707131023a [urlreducer] add some checks 2015-12-23 12:51:16 +01:00
57c460fc9c Simplify date extraction 2015-12-23 12:50:25 +01:00
cd0dbc4cc2 Xmlparser: parser for lists and dicts 2015-12-21 14:50:48 +01:00
d59f629dd9 Xmlparser: new class that just store one node, futher nodes will be parsed 2015-12-21 14:50:48 +01:00
1e29061bc9 [urlreducer] Framalink is in fact LSTU 2015-12-21 14:50:48 +01:00
e03d803ae0 [wolframalpha] Servers take a long times to respond theses days :( 2015-12-21 14:50:47 +01:00
f47aa8c478 Load module data on first access 2015-12-21 14:50:47 +01:00
6fc6561186 [alias] Fix parsing error when creating a (not allowed) spaced alias 2015-12-21 14:50:47 +01:00
ea8656ce0d Refactor command help: use hookmanager to get command help instead of custom search 2015-11-18 20:21:06 +01:00
0ba763f8b1 Display miss string only if no hook match on a full message treatment 2015-11-18 20:21:05 +01:00
43c42e1397 Rework hook managment and add some tests 2015-11-18 20:21:05 +01:00
926648517f Add config to package setup 2015-11-18 19:59:00 +01:00
31d93734a6 Fixed empty module configuration 2015-11-18 19:58:46 +01:00
e83c4091bf Avoid catchall DirectAsk 2015-11-18 19:58:26 +01:00
7ae7e381c3 [alias] Forward command keywords 2015-11-18 19:58:03 +01:00
f27347f028 [grep] Introducing new module that perform grep like action on subcommand 2015-11-13 17:32:58 +01:00
38412c1c16 Suggest command(s) on typo 2015-11-13 17:06:28 +01:00
2ebd86b80f [events] Avoid catchall hook 2015-11-13 17:06:10 +01:00
0f4a904a77 Log configuration loading 2015-11-13 17:05:52 +01:00
36cfdd8861 Added check and match module defined functions to hooks 2015-11-13 17:05:27 +01:00
11bdf8d0a1 [cve] Dusting module 2015-11-08 01:12:21 +01:00
00fa139e54 [syno] Dusting module 2015-11-08 01:12:21 +01:00
1ef54426bc [networking/whois] improve netwhois response by using normalized API fields 2015-11-08 01:12:20 +01:00
6aef54910e [networking/whois] New function to get domain availability status 2015-11-08 01:12:20 +01:00
a4e6e4ce84 [more] Fix append_content behaviour: initialize a list, don't convert string to char list 2015-11-08 01:12:20 +01:00
c4a7df7a6f [spell] Dusting module 2015-11-07 10:06:06 +01:00
3a1ce6c9e8 [ddg] Don't include empty definition in global results 2015-11-05 14:32:34 +01:00
c06fb69c8b Extract tools.config as config module 2015-11-03 16:53:49 +01:00
f39a0eac56 Refactors hooks registration 2015-11-03 16:53:49 +01:00
49d7e4ced6 Hooks: add global methods to restrict read/write on channels 2015-11-03 16:35:19 +01:00
de2e1d6216 Remove Message.receivers, long time deprecated 2015-11-03 07:26:01 +01:00
ea9829b341 Check command keywords using keyword help (passed in @hook) 2015-11-03 07:23:21 +01:00
70b52d5567 [translate] Refactor module, use keywords 2015-11-03 07:22:55 +01:00
8ff0b626a2 Update help of module using keywords 2015-11-03 07:22:26 +01:00
979f1d0c55 [more] Don't display the count string if the message is alone 2015-11-03 07:22:16 +01:00
9790954dfc Hooks can now contain help on optional keywords 2015-11-03 07:22:01 +01:00
c6aa38147b Include some forgotten module in reload process 2015-11-03 07:21:49 +01:00
8b4f08c5bd Replace IRCException by IMException, as nemubot is not only built for IRC 2015-11-03 07:21:06 +01:00
ac33ceb579 Remove dead or useless code 2015-11-03 07:20:52 +01:00
9935e038fc [man] num variable wasn't used here 2015-11-03 07:20:28 +01:00
f496c31d1c Help: don't append space character before ':' when the usage key is None 2015-10-31 17:45:50 +01:00
Max
c6e1e9acb2 [framalink] Update regex, clean up code 2015-10-29 16:34:58 +01:00
Max
1e36846265 [framalink] Fix ycc shortner 2015-10-29 16:34:58 +01:00
Max
04d5be04fa [suivi] Add TNT support 2015-10-29 16:34:58 +01:00
Max
3cb9a54cee [suivi] Code cleanup 2015-10-29 16:34:58 +01:00
Max
497263eaf7 [suivi] improve the suivi module
* Add multiple arguments/tracking numbers support
* Make it easier to add new tracking services
* Simplified to just one hook to which we can specify trackers using the
names variables

(correct typo in framalink comments)
2015-10-29 16:34:58 +01:00
e4d67ec345 Use Channel class when creating Server 2015-10-29 15:25:54 +01:00
2fdef0afe4 addChild should return a boolean 2015-10-28 10:55:03 +01:00
c560e13f24 Rework XML parser: part 1
This is the first step of the parser refactoring: here we change
	the configuration, next step will change data saving.
2015-10-28 10:55:02 +01:00
92530ef1b2 Server factory takes initializer dict 2015-10-28 10:55:02 +01:00
59ea2e971b Refactor modules that used nemubot XML parser due to previous commit 2015-10-28 10:55:02 +01:00
2b96c32063 [ddg] Split the module in two: ddg for search and urbandict for urbandictionnary 2015-10-27 23:05:49 +01:00
Max
aca073faff [framalink] Fix framalink quoting; add @provider
!framalink now allows the provider to be specified using the @provider
parameter.
2015-10-27 23:05:49 +01:00
Max
7ce9b2bb4c [framalink] Add error handling (invalid URLs) 2015-10-27 23:05:49 +01:00
5141a0dc17 tools/web: simplify regexp and typo 2015-10-27 23:05:38 +01:00
56c43179f3 tools/web: use core xml minidom instead of nemubot xml parser 2015-10-27 23:04:54 +01:00
138 changed files with 6680 additions and 4053 deletions

26
.drone.yml Normal file
View 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

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "modules/nextstop/external"]
path = modules/nextstop/external
url = git://github.com/nbr23/NextStop.git

View file

@ -1,8 +1,9 @@
language: python language: python
python: python:
- 3.3
- 3.4 - 3.4
- 3.5 - 3.5
- 3.6
- 3.7
- nightly - nightly
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt

21
Dockerfile Normal file
View 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" ]

View file

@ -1,17 +1,50 @@
# *nemubot* nemubot
=======
An extremely modulable IRC bot, built around XML configuration files! An extremely modulable IRC bot, built around XML configuration files!
## Requirements Requirements
------------
*nemubot* requires at least Python 3.3 to work. *nemubot* requires at least Python 3.3 to work.
Some modules (like `cve`, `nextstop` or `laposte`) require the Some modules (like `cve`, `nextstop` or `laposte`) require the
[BeautifulSoup module](http://www.crummy.com/software/BeautifulSoup/), [BeautifulSoup module](https://www.crummy.com/software/BeautifulSoup/),
but the core and framework has no dependency. but the core and framework has no dependency.
## Documentation Installation
------------
Have a look to the wiki at https://github.com/nemunaire/nemubot/wiki 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.

View file

@ -1,8 +1,7 @@
#!/usr/bin/env python3.3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot. # Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier # Copyright (C) 2012-2016 Mercier Pierre-Olivier
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU Affero General Public License as published by

View file

@ -1,11 +1,11 @@
<nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone"> <nemubotconfig nick="nemubot" realname="nemubot clone" owner="someone">
<server host="irc.rezosup.org" port="6667" autoconnect="true" caps="znc.in/server-time-iso"> <server uri="irc://irc.rezosup.org:6667" autoconnect="true" caps="znc.in/server-time-iso">
<channel name="#nemutest" /> <channel name="#nemutest" />
</server> </server>
<!-- <!--
<server host="my_host.local" port="6667" password="secret" autoconnect="true" ip="10.69.42.23" ssl="on"> <server host="ircs://my_host.local:6667" password="secret" autoconnect="true">
<channel name="#nemutest" /> <channel name="#nemutest" />
</server> </server>
--> -->

View file

@ -3,17 +3,16 @@
# PYTHON STUFFS ####################################################### # PYTHON STUFFS #######################################################
import re import re
import sys
from datetime import datetime, timezone from datetime import datetime, timezone
import shlex
from nemubot import context from nemubot import context
from nemubot.exception import IRCException from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.message import Command from nemubot.message import Command
from nemubot.tools.human import guess
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
from more import Response from nemubot.module.more import Response
# LOADING ############################################################# # LOADING #############################################################
@ -77,7 +76,7 @@ def get_variable(name, msg=None):
elif name in context.data.getNode("variables").index: elif name in context.data.getNode("variables").index:
return context.data.getNode("variables").index[name]["value"] return context.data.getNode("variables").index[name]["value"]
else: else:
return "" return None
def list_variables(user=None): def list_variables(user=None):
@ -109,12 +108,12 @@ def set_variable(name, value, creator):
context.save() context.save()
def replace_variables(cnts, msg=None): def replace_variables(cnts, msg):
"""Replace variables contained in the content """Replace variables contained in the content
Arguments: Arguments:
cnt -- content where search variables cnt -- content where search variables
msg -- optional message where pick some variables msg -- Message where pick some variables
""" """
unsetCnt = list() unsetCnt = list()
@ -123,12 +122,12 @@ def replace_variables(cnts, msg=None):
resultCnt = list() resultCnt = list()
for cnt in cnts: for cnt in cnts:
for res in re.findall("\\$\{(?P<name>[a-zA-Z0-9:]+)\}", cnt): for res, name, default in re.findall("\\$\{(([a-zA-Z0-9:]+)(?:-([^}]+))?)\}", cnt):
rv = re.match("([0-9]+)(:([0-9]*))?", res) rv = re.match("([0-9]+)(:([0-9]*))?", name)
if rv is not None: if rv is not None:
varI = int(rv.group(1)) - 1 varI = int(rv.group(1)) - 1
if varI > len(msg.args): if varI >= len(msg.args):
cnt = cnt.replace("${%s}" % res, "", 1) cnt = cnt.replace("${%s}" % res, default, 1)
elif rv.group(2) is not None: elif rv.group(2) is not None:
if rv.group(3) is not None and len(rv.group(3)): if rv.group(3) is not None and len(rv.group(3)):
varJ = int(rv.group(3)) - 1 varJ = int(rv.group(3)) - 1
@ -143,11 +142,12 @@ def replace_variables(cnts, msg=None):
cnt = cnt.replace("${%s}" % res, msg.args[varI], 1) cnt = cnt.replace("${%s}" % res, msg.args[varI], 1)
unsetCnt.append(varI) unsetCnt.append(varI)
else: else:
cnt = cnt.replace("${%s}" % res, get_variable(res), 1) cnt = cnt.replace("${%s}" % res, get_variable(name) or default, 1)
resultCnt.append(cnt) resultCnt.append(cnt)
# Remove used content
for u in sorted(set(unsetCnt), reverse=True): for u in sorted(set(unsetCnt), reverse=True):
k = msg.args.pop(u) msg.args.pop(u)
return resultCnt return resultCnt
@ -156,7 +156,7 @@ def replace_variables(cnts, msg=None):
## Variables management ## Variables management
@hook("cmd_hook", "listvars", @hook.command("listvars",
help="list defined variables for substitution in input commands", help="list defined variables for substitution in input commands",
help_usage={ help_usage={
None: "List all known variables", None: "List all known variables",
@ -179,20 +179,20 @@ def cmd_listvars(msg):
return Response("There is currently no variable stored.", channel=msg.channel) return Response("There is currently no variable stored.", channel=msg.channel)
@hook("cmd_hook", "set", @hook.command("set",
help="Create or set variables for substitution in input commands", 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"}) help_usage={"KEY VALUE": "Define the variable named KEY and fill it with VALUE as content"})
def cmd_set(msg): def cmd_set(msg):
if len(msg.args) < 2: if len(msg.args) < 2:
raise IRCException("!set take two args: the key and the value.") raise IMException("!set take two args: the key and the value.")
set_variable(msg.args[0], " ".join(msg.args[1:]), msg.nick) set_variable(msg.args[0], " ".join(msg.args[1:]), msg.frm)
return Response("Variable $%s successfully defined." % msg.args[0], return Response("Variable $%s successfully defined." % msg.args[0],
channel=msg.channel) channel=msg.channel)
## Alias management ## Alias management
@hook("cmd_hook", "listalias", @hook.command("listalias",
help="List registered aliases", help="List registered aliases",
help_usage={ help_usage={
None: "List all registered aliases", None: "List all registered aliases",
@ -206,27 +206,42 @@ def cmd_listalias(msg):
return Response("There is no alias currently.", channel=msg.channel) return Response("There is no alias currently.", channel=msg.channel)
@hook("cmd_hook", "alias", @hook.command("alias",
help="Display the replacement command for a given 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): def cmd_alias(msg):
if not len(msg.args): if not len(msg.args):
raise IRCException("!alias takes as argument an alias to extend.") raise IMException("!alias takes as argument an alias to extend.")
res = list()
for alias in msg.args: alias = context.subparse(msg, msg.args[0])
if alias[0] == "!": if alias is None or not isinstance(alias, Command):
alias = alias[1:] raise IMException("%s is not a valid alias" % msg.args[0])
if alias in context.data.getNode("aliases").index:
res.append("!%s correspond to %s" % (alias, context.data.getNode("aliases").index[alias]["origin"])) 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: else:
res.append("!%s is not an alias" % alias) wym = [m for m in guess(alias.cmd, context.data.getNode("aliases").index)]
return Response(res, channel=msg.channel, nick=msg.nick) raise IMException(msg.args[0] + " is not an alias." + (" Would you mean: %s?" % ", ".join(wym) if len(wym) else ""))
@hook("cmd_hook", "unalias", @hook.command("unalias",
help="Remove a previously created alias") help="Remove a previously created alias")
def cmd_unalias(msg): def cmd_unalias(msg):
if not len(msg.args): if not len(msg.args):
raise IRCException("Which alias would you want to remove?") raise IMException("Which alias would you want to remove?")
res = list() res = list()
for alias in msg.args: for alias in msg.args:
if alias[0] == "!" and len(alias) > 1: if alias[0] == "!" and len(alias) > 1:
@ -243,39 +258,20 @@ def cmd_unalias(msg):
## Alias replacement ## Alias replacement
@hook("pre_Command") @hook.add(["pre","Command"])
def treat_alias(msg): def treat_alias(msg):
if msg.cmd in context.data.getNode("aliases").index: if context.data.getNode("aliases") is not None and msg.cmd in context.data.getNode("aliases").index:
txt = context.data.getNode("aliases").index[msg.cmd]["origin"] origin = context.data.getNode("aliases").index[msg.cmd]["origin"]
# TODO: for legacy compatibility rpl_msg = context.subparse(msg, origin)
if txt[0] == "!": if isinstance(rpl_msg, Command):
txt = txt[1:] rpl_msg.args = replace_variables(rpl_msg.args, msg)
try: rpl_msg.args += msg.args
args = shlex.split(txt) rpl_msg.kwargs.update(msg.kwargs)
except ValueError: elif len(msg.args) or len(msg.kwargs):
args = txt.split(' ') raise IMException("This kind of alias doesn't take any argument (haven't you forgotten the '!'?).")
nmsg = Command(args[0], replace_variables(args[1:], msg) + msg.args, **msg.export_args())
# Avoid infinite recursion # Avoid infinite recursion
if msg.cmd != nmsg.cmd: if not isinstance(rpl_msg, Command) or msg.cmd != rpl_msg.cmd:
# Also return origin message, if it can be treated as well return rpl_msg
return [msg, nmsg]
return msg return msg
@hook("ask_default")
def parseask(msg):
if re.match(".*(register|set|cr[ée]{2}|new|nouvel(le)?) alias.*", msg.text) is not None:
result = re.match(".*alias !?([^ ]+) ?(pour|for|=|:) ?(.+)$", msg.text)
if result.group(1) in context.data.getNode("aliases").index:
raise IRCException("this alias is already defined.")
else:
create_alias(result.group(1),
result.group(3),
channel=msg.channel,
creator=msg.nick)
res = Response("New alias %s successfully registered." %
result.group(1), channel=msg.channel)
return res
return None

View file

@ -7,13 +7,13 @@ import sys
from datetime import date, datetime from datetime import date, datetime
from nemubot import context from nemubot import context
from nemubot.exception import IRCException from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.countdown import countdown_format from nemubot.tools.countdown import countdown_format
from nemubot.tools.date import extractDate from nemubot.tools.date import extractDate
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.node import ModuleState
from more import Response from nemubot.module.more import Response
# LOADING ############################################################# # LOADING #############################################################
@ -27,7 +27,7 @@ def load(context):
def findName(msg): def findName(msg):
if (not len(msg.args) or msg.args[0].lower() == "moi" or if (not len(msg.args) or msg.args[0].lower() == "moi" or
msg.args[0].lower() == "me"): msg.args[0].lower() == "me"):
name = msg.nick.lower() name = msg.frm.lower()
else: else:
name = msg.args[0].lower() name = msg.args[0].lower()
@ -46,7 +46,7 @@ def findName(msg):
## Commands ## Commands
@hook("cmd_hook", "anniv", @hook.command("anniv",
help="gives the remaining time before the anniversary of known people", help="gives the remaining time before the anniversary of known people",
help_usage={ help_usage={
None: "Calculate the time remaining before your birthday", None: "Calculate the time remaining before your birthday",
@ -77,10 +77,10 @@ def cmd_anniv(msg):
else: else:
return Response("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("cmd_hook", "age", @hook.command("age",
help="Calculate age of known people", help="Calculate age of known people",
help_usage={ help_usage={
None: "Calculate your age", None: "Calculate your age",
@ -98,26 +98,26 @@ def cmd_age(msg):
msg.channel) msg.channel)
else: else:
return Response("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 ## Input parsing
@hook("ask_default") @hook.ask()
def parseask(msg): def parseask(msg):
res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)?\s+(birthday|geburtstag|née? |nee? le|born on).*$", msg.text, re.I) 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 res is not None: if res is not None:
try: try:
extDate = extractDate(msg.text) 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("la date de naissance ne paraît pas valide...", return Response("la date de naissance ne paraît pas valide...",
msg.channel, msg.channel,
msg.nick) msg.frm)
else: else:
nick = res.group(1) nick = res.group(1)
if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma": if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma":
nick = msg.nick nick = msg.frm
if nick.lower() in context.data.index: if nick.lower() in context.data.index:
context.data.index[nick.lower()]["born"] = extDate context.data.index[nick.lower()]["born"] = extDate
else: else:
@ -129,6 +129,6 @@ def parseask(msg):
return Response("ok, c'est noté, %s est né le %s" return Response("ok, c'est noté, %s est né le %s"
% (nick, extDate.strftime("%A %d %B %Y à %H:%M")), % (nick, extDate.strftime("%A %d %B %Y à %H:%M")),
msg.channel, msg.channel,
msg.nick) msg.frm)
except: except:
raise IRCException("la date de naissance ne paraît pas valide.") raise IMException("la date de naissance ne paraît pas valide.")

View file

@ -4,12 +4,11 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from nemubot import context
from nemubot.event import ModuleEvent from nemubot.event import ModuleEvent
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.countdown import countdown_format from nemubot.tools.countdown import countdown_format
from more import Response from nemubot.module.more import Response
# GLOBALS ############################################################# # GLOBALS #############################################################
@ -47,9 +46,9 @@ def load(context):
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook("cmd_hook", "newyear", @hook.command("newyear",
help="Display the remaining time before the next new year") help="Display the remaining time before the next new year")
@hook("cmd_hook", str(yrn), @hook.command(str(yrn),
help="Display the remaining time before %d" % yrn) help="Display the remaining time before %d" % yrn)
def cmd_newyear(msg): def cmd_newyear(msg):
return Response(countdown_format(datetime(yrn, 1, 1, 0, 0, 1, 0, return Response(countdown_format(datetime(yrn, 1, 1, 0, 0, 1, 0,
@ -59,7 +58,7 @@ def cmd_newyear(msg):
channel=msg.channel) channel=msg.channel)
@hook("cmd_rgxp", data=yrn, regexp="^[0-9]{4}$", @hook.command(data=yrn, regexp="^[0-9]{4}$",
help="Calculate time remaining/passed before/since the requested year") help="Calculate time remaining/passed before/since the requested year")
def cmd_timetoyear(msg, cur): def cmd_timetoyear(msg, cur):
yr = int(msg.cmd) yr = int(msg.cmd)

View file

@ -5,17 +5,17 @@
import urllib import urllib
from nemubot import context from nemubot import context
from nemubot.exception import IRCException from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from more import Response from nemubot.module.more import Response
# LOADING ############################################################# # LOADING #############################################################
def load(context): def load(context):
if not context.config or not context.config.getAttribute("goodreadskey"): if not context.config or "goodreadskey" not in context.config:
raise ImportError("You need a Goodreads API key in order to use this " raise ImportError("You need a Goodreads API key in order to use this "
"module. Add it to the module configuration file:\n" "module. Add it to the module configuration file:\n"
"<module name=\"books\" goodreadskey=\"XXXXXX\" />\n" "<module name=\"books\" goodreadskey=\"XXXXXX\" />\n"
@ -28,8 +28,8 @@ def get_book(title):
"""Retrieve a book from its title""" """Retrieve a book from its title"""
response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" % response = web.getXML("https://www.goodreads.com/book/title.xml?key=%s&title=%s" %
(context.config["goodreadskey"], urllib.parse.quote(title))) (context.config["goodreadskey"], urllib.parse.quote(title)))
if response is not None and response.hasNode("book"): if response is not None and len(response.getElementsByTagName("book")):
return response.getNode("book") return response.getElementsByTagName("book")[0]
else: else:
return None return None
@ -38,8 +38,8 @@ def search_books(title):
"""Get a list of book matching given title""" """Get a list of book matching given title"""
response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" % response = web.getXML("https://www.goodreads.com/search.xml?key=%s&q=%s" %
(context.config["goodreadskey"], urllib.parse.quote(title))) (context.config["goodreadskey"], urllib.parse.quote(title)))
if response is not None and response.hasNode("search"): if response is not None and len(response.getElementsByTagName("search")):
return response.getNode("search").getNode("results").getNodes("work") return response.getElementsByTagName("search")[0].getElementsByTagName("results")[0].getElementsByTagName("work")
else: else:
return [] return []
@ -48,43 +48,43 @@ def search_author(name):
"""Looking for an author""" """Looking for an author"""
response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" % response = web.getXML("https://www.goodreads.com/api/author_url/%s?key=%s" %
(urllib.parse.quote(name), context.config["goodreadskey"])) (urllib.parse.quote(name), context.config["goodreadskey"]))
if response is not None and response.hasNode("author") and response.getNode("author").hasAttribute("id"): 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" % response = web.getXML("https://www.goodreads.com/author/show/%s.xml?key=%s" %
(urllib.parse.quote(response.getNode("author")["id"]), context.config["goodreadskey"])) (urllib.parse.quote(response.getElementsByTagName("author")[0].getAttribute("id")), context.config["goodreadskey"]))
if response is not None and response.hasNode("author"): if response is not None and len(response.getElementsByTagName("author")):
return response.getNode("author") return response.getElementsByTagName("author")[0]
return None return None
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook("cmd_hook", "book", @hook.command("book",
help="Get information about a book from its title", help="Get information about a book from its title",
help_usage={ help_usage={
"TITLE": "Get information about a book titled TITLE" "TITLE": "Get information about a book titled TITLE"
}) })
def cmd_book(msg): def cmd_book(msg):
if not len(msg.args): if not len(msg.args):
raise IRCException("please give me a title to search") raise IMException("please give me a title to search")
book = get_book(" ".join(msg.args)) book = get_book(" ".join(msg.args))
if book is None: if book is None:
raise IRCException("unable to find book named like this") raise IMException("unable to find book named like this")
res = Response(channel=msg.channel) res = Response(channel=msg.channel)
res.append_message("%s, writed by %s: %s" % (book.getNode("title").getContent(), res.append_message("%s, written by %s: %s" % (book.getElementsByTagName("title")[0].firstChild.nodeValue,
book.getNode("authors").getNode("author").getNode("name").getContent(), book.getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue,
web.striphtml(book.getNode("description").getContent()))) web.striphtml(book.getElementsByTagName("description")[0].firstChild.nodeValue if book.getElementsByTagName("description")[0].firstChild else "")))
return res return res
@hook("cmd_hook", "search_books", @hook.command("search_books",
help="Search book's title", help="Search book's title",
help_usage={ help_usage={
"APPROX_TITLE": "Search for a book approximately titled APPROX_TITLE" "APPROX_TITLE": "Search for a book approximately titled APPROX_TITLE"
}) })
def cmd_books(msg): def cmd_books(msg):
if not len(msg.args): if not len(msg.args):
raise IRCException("please give me a title to search") raise IMException("please give me a title to search")
title = " ".join(msg.args) title = " ".join(msg.args)
res = Response(channel=msg.channel, res = Response(channel=msg.channel,
@ -92,21 +92,24 @@ def cmd_books(msg):
count=" (%d more books)") count=" (%d more books)")
for book in search_books(title): for book in search_books(title):
res.append_message("%s, writed by %s" % (book.getNode("best_book").getNode("title").getContent(), res.append_message("%s, writed by %s" % (book.getElementsByTagName("best_book")[0].getElementsByTagName("title")[0].firstChild.nodeValue,
book.getNode("best_book").getNode("author").getNode("name").getContent())) book.getElementsByTagName("best_book")[0].getElementsByTagName("author")[0].getElementsByTagName("name")[0].firstChild.nodeValue))
return res return res
@hook("cmd_hook", "author_books", @hook.command("author_books",
help="Looking for books writen by a given author", help="Looking for books writen by a given author",
help_usage={ help_usage={
"AUTHOR": "Looking for books writen by AUTHOR" "AUTHOR": "Looking for books writen by AUTHOR"
}) })
def cmd_author(msg): def cmd_author(msg):
if not len(msg.args): if not len(msg.args):
raise IRCException("please give me an author to search") raise IMException("please give me an author to search")
ath = search_author(" ".join(msg.args)) name = " ".join(msg.args)
return Response([b.getNode("title").getContent() for b in ath.getNode("books").getNodes("book")], 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, channel=msg.channel,
title=ath.getNode("name").getContent()) title=ath.getElementsByTagName("name")[0].firstChild.nodeValue)

55
modules/cat.py Normal file
View 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

View file

@ -1,204 +0,0 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 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 traceback
import sys
from nemubot.hooks import hook
nemubotversion = 3.4
NODATA = True
def getserver(toks, context, prompt, mandatory=False, **kwargs):
"""Choose the server in toks or prompt.
This function modify the tokens list passed as argument"""
if len(toks) > 1 and toks[1] in context.servers:
return context.servers[toks.pop(1)]
elif not mandatory or prompt.selectedServer:
return prompt.selectedServer
else:
from nemubot.prompt.error import PromptError
raise PromptError("Please SELECT a server or give its name in argument.")
@hook("prompt_cmd", "close")
def close(toks, context, **kwargs):
"""Disconnect and forget (remove from the servers list) the server"""
srv = getserver(toks, context=context, mandatory=True, **kwargs)
if srv.close():
del context.servers[srv.id]
return 0
return 1
@hook("prompt_cmd", "connect")
def connect(toks, **kwargs):
"""Make the connexion to a server"""
srv = getserver(toks, mandatory=True, **kwargs)
return not srv.open()
@hook("prompt_cmd", "disconnect")
def disconnect(toks, **kwargs):
"""Close the connection to a server"""
srv = getserver(toks, mandatory=True, **kwargs)
return not srv.close()
@hook("prompt_cmd", "discover")
def discover(toks, context, **kwargs):
"""Discover a new bot on a server"""
srv = getserver(toks, context=context, mandatory=True, **kwargs)
if len(toks) > 1 and "!" in toks[1]:
bot = context.add_networkbot(srv, name)
return not bot.connect()
else:
print(" %s is not a valid fullname, for example: "
"nemubot!nemubotV3@bot.nemunai.re" % ''.join(toks[1:1]))
return 1
@hook("prompt_cmd", "join")
@hook("prompt_cmd", "leave")
@hook("prompt_cmd", "part")
def join(toks, **kwargs):
"""Join or leave a channel"""
srv = getserver(toks, mandatory=True, **kwargs)
if len(toks) <= 2:
print("%s: not enough arguments." % toks[0])
return 1
if toks[0] == "join":
if len(toks) > 2:
srv.write("JOIN %s %s" % (toks[1], toks[2]))
else:
srv.write("JOIN %s" % toks[1])
elif toks[0] == "leave" or toks[0] == "part":
if len(toks) > 2:
srv.write("PART %s :%s" % (toks[1], " ".join(toks[2:])))
else:
srv.write("PART %s" % toks[1])
return 0
@hook("prompt_cmd", "save")
def save_mod(toks, context, **kwargs):
"""Force save module data"""
if len(toks) < 2:
print("save: not enough arguments.")
return 1
wrn = 0
for mod in toks[1:]:
if mod in context.modules:
context.modules[mod].save()
print("save: module `%s´ saved successfully" % mod)
else:
wrn += 1
print("save: no module named `%s´" % mod)
return wrn
@hook("prompt_cmd", "send")
def send(toks, **kwargs):
"""Send a message on a channel"""
srv = getserver(toks, mandatory=True, **kwargs)
# Check the server is connected
if not srv.connected:
print ("send: server `%s' not connected." % srv.id)
return 2
if len(toks) <= 3:
print ("send: not enough arguments.")
return 1
if toks[1] not in srv.channels:
print ("send: channel `%s' not authorized in server `%s'."
% (toks[1], srv.id))
return 3
from nemubot.message import Text
srv.send_response(Text(" ".join(toks[2:]), server=None,
to=[toks[1]]))
return 0
@hook("prompt_cmd", "zap")
def zap(toks, **kwargs):
"""Hard change connexion state"""
srv = getserver(toks, mandatory=True, **kwargs)
srv.connected = not srv.connected
@hook("prompt_cmd", "top")
def top(toks, context, **kwargs):
"""Display consumers load information"""
print("Queue size: %d, %d thread(s) running (counter: %d)" %
(context.cnsr_queue.qsize(),
len(context.cnsr_thrd),
context.cnsr_thrd_size))
if len(context.events) > 0:
print("Events registered: %d, next in %d seconds" %
(len(context.events),
context.events[0].time_left.seconds))
else:
print("No events registered")
for th in context.cnsr_thrd:
if th.is_alive():
print(("#" * 15 + " Stack trace for thread %u " + "#" * 15) %
th.ident)
traceback.print_stack(sys._current_frames()[th.ident])
@hook("prompt_cmd", "netstat")
def netstat(toks, context, **kwargs):
"""Display sockets in use and many other things"""
if len(context.network) > 0:
print("Distant bots connected: %d:" % len(context.network))
for name, bot in context.network.items():
print("# %s:" % name)
print(" * Declared hooks:")
lvl = 0
for hlvl in bot.hooks:
lvl += 1
for hook in (hlvl.all_pre + hlvl.all_post + hlvl.cmd_rgxp +
hlvl.cmd_default + hlvl.ask_rgxp +
hlvl.ask_default + hlvl.msg_rgxp +
hlvl.msg_default):
print(" %s- %s" % (' ' * lvl * 2, hook))
for kind in ["irc_hook", "cmd_hook", "ask_hook", "msg_hook"]:
print(" %s- <%s> %s" % (' ' * lvl * 2, kind,
", ".join(hlvl.__dict__[kind].keys())))
print(" * My tag: %d" % bot.my_tag)
print(" * Tags in use (%d):" % bot.inc_tag)
for tag, (cmd, data) in bot.tags.items():
print(" - %11s: %s « %s »" % (tag, cmd, data))
else:
print("No distant bot connected")

View file

@ -6,12 +6,12 @@ from collections import defaultdict
import re import re
from urllib.parse import quote from urllib.parse import quote
from nemubot.exception import IRCException from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools import web from nemubot.tools import web
from nemubot.tools.web import striphtml from nemubot.tools.web import striphtml
from more import Response from nemubot.module.more import Response
# GLOBALS ############################################################# # GLOBALS #############################################################
@ -36,7 +36,7 @@ for k, v in s:
# MODULE CORE ######################################################### # MODULE CORE #########################################################
def get_conjug(verb, stringTens): def get_conjug(verb, stringTens):
url = ("http://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" % url = ("https://leconjugueur.lefigaro.fr/conjugaison/verbe/%s.html" %
quote(verb.encode("ISO-8859-1"))) quote(verb.encode("ISO-8859-1")))
page = web.getURLContent(url) page = web.getURLContent(url)
@ -51,10 +51,10 @@ def compute_line(line, stringTens):
try: try:
idTemps = d[stringTens] idTemps = d[stringTens]
except: except:
raise IRCException("le temps demandé n'existe pas") raise IMException("le temps demandé n'existe pas")
if len(idTemps) == 0: if len(idTemps) == 0:
raise IRCException("le temps demandé n'existe pas") raise IMException("le temps demandé n'existe pas")
index = line.index('<div id="temps' + idTemps[0] + '\"') index = line.index('<div id="temps' + idTemps[0] + '\"')
endIndex = line[index:].index('<div class=\"conjugBloc\"') endIndex = line[index:].index('<div class=\"conjugBloc\"')
@ -72,13 +72,13 @@ def compute_line(line, stringTens):
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook("cmd_hook", "conjugaison", @hook.command("conjugaison",
help_usage={ help_usage={
"TENS VERB": "give the conjugaison for VERB in TENS." "TENS VERB": "give the conjugaison for VERB in TENS."
}) })
def cmd_conjug(msg): def cmd_conjug(msg):
if len(msg.args) < 2: if len(msg.args) < 2:
raise IRCException("donne moi un temps et un verbe, et je te donnerai " raise IMException("donne moi un temps et un verbe, et je te donnerai "
"sa conjugaison!") "sa conjugaison!")
tens = ' '.join(msg.args[:-1]) tens = ' '.join(msg.args[:-1])
@ -91,4 +91,4 @@ def cmd_conjug(msg):
return Response(conjug, channel=msg.channel, return Response(conjug, channel=msg.channel,
title="Conjugaison de %s" % verb) title="Conjugaison de %s" % verb)
else: else:
raise IRCException("aucune conjugaison de '%s' n'a été trouvé" % verb) raise IMException("aucune conjugaison de '%s' n'a été trouvé" % verb)

View file

@ -6,7 +6,7 @@ from bs4 import BeautifulSoup
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.web import getURLContent, striphtml from nemubot.tools.web import getURLContent, striphtml
from more import Response from nemubot.module.more import Response
# GLOBALS ############################################################# # GLOBALS #############################################################
@ -16,7 +16,7 @@ URL = 'https://ctftime.org/event/list/upcoming'
# MODULE INTERFACE #################################################### # MODULE INTERFACE ####################################################
@hook("cmd_hook", "ctfs", @hook.command("ctfs",
help="Display the upcoming CTFs") help="Display the upcoming CTFs")
def get_info_yt(msg): def get_info_yt(msg):
soup = BeautifulSoup(getURLContent(URL)) soup = BeautifulSoup(getURLContent(URL))
@ -25,10 +25,8 @@ def get_info_yt(msg):
for line in soup.body.find_all('tr'): for line in soup.body.find_all('tr'):
n = line.find_all('td') n = line.find_all('td')
if len(n) == 5: if len(n) == 7:
try: res.append_message("\x02%s:\x0F from %s type %s at %s. Weight: %s. %s%s" %
res.append_message("\x02%s:\x0F from %s type %s at %s. %s" % tuple([striphtml(x.text).strip() for x in n]))
tuple([striphtml(x.text) for x in n]))
except:
pass
return res return res

View file

@ -1,26 +1,78 @@
"""Read CVE in your IM client"""
# PYTHON STUFFS #######################################################
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from urllib.parse import quote from urllib.parse import quote
from nemubot.exception import IMException
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.tools.web import getURLContent from nemubot.tools.web import getURLContent, striphtml
from more import Response
"""CVE description""" from nemubot.module.more import Response
nemubotversion = 4.0 BASEURL_NIST = 'https://nvd.nist.gov/vuln/detail/'
BASEURL_MITRE = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name='
# 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): def get_cve(cve_id):
search_url = BASEURL_MITRE + quote(cve_id.upper()) search_url = BASEURL_NIST + quote(cve_id.upper())
soup = BeautifulSoup(getURLContent(search_url)) soup = BeautifulSoup(getURLContent(search_url))
desc = soup.body.findAll('td')
return desc[17].text.replace("\n", " ") + " Moar at " + search_url vuln = {}
@hook("cmd_hook", "cve") 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): def get_cve_desc(msg):
res = Response(channel=msg.channel) res = Response(channel=msg.channel)
@ -28,6 +80,20 @@ def get_cve_desc(msg):
if cve_id[:3].lower() != 'cve': if cve_id[:3].lower() != 'cve':
cve_id = 'cve-' + cve_id cve_id = 'cve-' + cve_id
res.append_message(get_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 return res

138
modules/ddg.py Normal file
View 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

View file

@ -1,71 +0,0 @@
# coding=utf-8
from urllib.parse import quote
from nemubot.tools import web
from nemubot.tools.xmlparser import parse_string
class DDGSearch:
def __init__(self, terms, safeoff=False):
self.terms = terms
self.ddgres = web.getXML(
"https://api.duckduckgo.com/?q=%s&format=xml&no_redirect=1%s" %
(quote(terms), "&kp=-1" if safeoff else ""),
timeout=10)
@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

View file

@ -1,30 +0,0 @@
# coding=utf-8
from urllib.parse import quote
from nemubot.tools import web
class UrbanDictionnary:
def __init__(self, terms):
self.terms = terms
self.udres = web.getJSON(
"http://api.urbandictionary.com/v0/define?term=%s" % quote(terms),
timeout=10)
@property
def result_type(self):
if self.udres and "result_type" in self.udres:
return self.udres["result_type"]
else:
return ""
@property
def definitions(self):
if self.udres and "list" in self.udres:
for d in self.udres["list"]:
yield d["definition"] + "\n" + d["example"]
else:
yield "Sorry, no definition found for %s" % self.terms

View file

@ -1,70 +0,0 @@
# coding=utf-8
"""Search around various search engine or knowledges database"""
import imp
from nemubot import context
from nemubot.exception import IRCException
from nemubot.hooks import hook
nemubotversion = 3.4
from more import Response
from . import DDGSearch
from . import UrbanDictionnary
@hook("cmd_hook", "define")
def define(msg):
if not len(msg.args):
raise IRCException("Indicate a term to define")
s = DDGSearch.DDGSearch(' '.join(msg.args))
return Response(s.definition, channel=msg.channel)
@hook("cmd_hook", "search")
def search(msg):
if not len(msg.args):
raise IRCException("Indicate a term to search")
if "!safeoff" in msg.args:
msg.args.remove("!safeoff")
safeoff = True
else:
safeoff = False
s = DDGSearch.DDGSearch(' '.join(msg.args), safeoff)
res = Response(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)
res.append_message(s.definition)
return res
@hook("cmd_hook", "urbandictionnary")
def udsearch(msg):
if not len(msg.args):
raise IRCException("Indicate a term to search")
s = UrbanDictionnary.UrbanDictionnary(' '.join(msg.args))
res = Response(channel=msg.channel, nomore="No more results",
count=" (%d more definitions)")
for d in s.definitions:
res.append_message(d.replace("\n", " "))
return res

94
modules/dig.py Normal file
View 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
View 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

View file

@ -1,49 +1,99 @@
# coding=utf-8
"""Create countdowns and reminders""" """Create countdowns and reminders"""
import imp import calendar
import re
import sys
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import time from functools import partial
import threading import re
import traceback
from nemubot import context from nemubot import context
from nemubot.exception import IRCException from nemubot.exception import IMException
from nemubot.event import ModuleEvent from nemubot.event import ModuleEvent
from nemubot.hooks import hook from nemubot.hooks import hook
from nemubot.message import Command
from nemubot.tools.countdown import countdown_format, countdown from nemubot.tools.countdown import countdown_format, countdown
from nemubot.tools.date import extractDate from nemubot.tools.date import extractDate
from nemubot.tools.xmlparser.node import ModuleState from nemubot.tools.xmlparser.basic import DictNode
nemubotversion = 3.4 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
from more import Response
def help_full (): def help_full ():
return "This module store a lot of events: ny, we, " + (", ".join(context.datas.index.keys())) + "\n!eventslist: gets list of timer\n!start /something/: launch a timer" 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): def load(context):
#Define the index context.set_knodes({
context.data.setIndex("name") "dict": DictNode,
"event": Event,
})
for evt in context.data.index.keys(): if context.data is None:
if context.data.index[evt].hasAttribute("end"): context.set_default(DictNode())
event = ModuleEvent(call=fini, call_data=dict(strend=context.data.index[evt]))
event._end = context.data.index[evt].getDate("end") # Relaunch all timers
idt = context.add_event(event) for kevt in context.data:
if idt is not None: if context.data[kevt].end:
context.data.index[evt]["_id"] = idt 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(d, strend): def fini(name, evt):
context.send_response(strend["server"], Response("%s arrivé à échéance." % strend["name"], channel=strend["channel"], nick=strend["proprio"])) context.send_response(evt._server, Response("%s arrivé à échéance." % name, channel=evt._channel, nick=evt.creator))
context.data.delChild(context.data.index[strend["name"]]) evt._evt = None
del context.data[name]
context.save() context.save()
@hook("cmd_hook", "goûter")
@hook.command("goûter")
def cmd_gouter(msg): def cmd_gouter(msg):
ndate = datetime.now(timezone.utc) ndate = datetime.now(timezone.utc)
ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42, 0, 0, timezone.utc) ndate = datetime(ndate.year, ndate.month, ndate.day, 16, 42, 0, 0, timezone.utc)
@ -52,7 +102,8 @@ def cmd_gouter(msg):
"Nous avons %s de retard pour le goûter :("), "Nous avons %s de retard pour le goûter :("),
channel=msg.channel) channel=msg.channel)
@hook("cmd_hook", "week-end")
@hook.command("week-end")
def cmd_we(msg): def cmd_we(msg):
ndate = datetime.now(timezone.utc) + timedelta(5 - datetime.today().weekday()) ndate = datetime.now(timezone.utc) + timedelta(5 - datetime.today().weekday())
ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1, 0, timezone.utc) ndate = datetime(ndate.year, ndate.month, ndate.day, 0, 0, 1, 0, timezone.utc)
@ -61,23 +112,16 @@ def cmd_we(msg):
"Youhou, on est en week-end depuis %s."), "Youhou, on est en week-end depuis %s."),
channel=msg.channel) channel=msg.channel)
@hook("cmd_hook", "start")
@hook.command("start")
def start_countdown(msg): def start_countdown(msg):
"""!start /something/: launch a timer""" """!start /something/: launch a timer"""
if len(msg.args) < 1: if len(msg.args) < 1:
raise IRCException("indique le nom d'un événement à chronométrer") raise IMException("indique le nom d'un événement à chronométrer")
if msg.args[0] in context.data.index: if msg.args[0] in context.data:
raise IRCException("%s existe déjà." % msg.args[0]) raise IMException("%s existe déjà." % msg.args[0])
strnd = ModuleState("strend") evt = Event(server=msg.server, channel=msg.channel, creator=msg.frm, start_time=msg.date)
strnd["server"] = msg.server
strnd["channel"] = msg.channel
strnd["proprio"] = msg.nick
strnd["start"] = msg.date
strnd["name"] = msg.args[0]
context.data.addChild(strnd)
evt = ModuleEvent(call=fini, call_data=dict(strend=strnd))
if len(msg.args) > 1: if len(msg.args) > 1:
result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.args[1]) result1 = re.findall("([0-9]+)([smhdjwyaSMHDJWYA])?", msg.args[1])
@ -95,136 +139,138 @@ def start_countdown(msg):
if result2 is None or result2.group(4) is None: yea = now.year if result2 is None or result2.group(4) is None: yea = now.year
else: yea = int(result2.group(4)) else: yea = int(result2.group(4))
if result2 is not None and result3 is not None: 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, timezone.utc) evt.end = datetime(yea, int(result2.group(3)), int(result2.group(2)), hou, minu, sec, timezone.utc)
elif result2 is not None: elif result2 is not None:
strnd["end"] = datetime(int(result2.group(4)), int(result2.group(3)), int(result2.group(2)), 0, 0, 0, timezone.utc) 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: elif result3 is not None:
if hou * 3600 + minu * 60 + sec > now.hour * 3600 + now.minute * 60 + now.second: 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, timezone.utc) evt.end = datetime(now.year, now.month, now.day, hou, minu, sec, timezone.utc)
else: else:
strnd["end"] = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc) evt.end = datetime(now.year, now.month, now.day + 1, hou, minu, sec, timezone.utc)
evt._end = strnd.getDate("end")
strnd["_id"] = context.add_event(evt)
except: except:
context.data.delChild(strnd) raise IMException("Mauvais format de date pour l'événement %s. Il n'a pas été créé." % msg.args[0])
raise IRCException("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: elif result1 is not None and len(result1) > 0:
strnd["end"] = msg.date evt.end = msg.date
for (t, g) in result1: for (t, g) in result1:
if g is None or g == "" or g == "m" or g == "M": if g is None or g == "" or g == "m" or g == "M":
strnd["end"] += timedelta(minutes=int(t)) evt.end += timedelta(minutes=int(t))
elif g == "h" or g == "H": elif g == "h" or g == "H":
strnd["end"] += timedelta(hours=int(t)) evt.end += timedelta(hours=int(t))
elif g == "d" or g == "D" or g == "j" or g == "J": elif g == "d" or g == "D" or g == "j" or g == "J":
strnd["end"] += timedelta(days=int(t)) evt.end += timedelta(days=int(t))
elif g == "w" or g == "W": elif g == "w" or g == "W":
strnd["end"] += timedelta(days=int(t)*7) evt.end += timedelta(days=int(t)*7)
elif g == "y" or g == "Y" or g == "a" or g == "A": elif g == "y" or g == "Y" or g == "a" or g == "A":
strnd["end"] += timedelta(days=int(t)*365) evt.end += timedelta(days=int(t)*365)
else: else:
strnd["end"] += timedelta(seconds=int(t)) evt.end += timedelta(seconds=int(t))
evt._end = strnd.getDate("end")
eid = context.add_event(evt)
if eid is not None:
strnd["_id"] = eid
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() context.save()
if "end" in strnd:
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." % return Response("%s commencé le %s et se terminera le %s." %
(msg.args[0], msg.date.strftime("%A %d %B %Y à %H:%M:%S"), (msg.args[0], msg.date.strftime("%A %d %B %Y à %H:%M:%S"),
strnd.getDate("end").strftime("%A %d %B %Y à %H:%M:%S")), evt.end.strftime("%A %d %B %Y à %H:%M:%S")),
nick=msg.frm) channel=msg.channel)
else: else:
return Response("%s commencé le %s"% (msg.args[0], return Response(