', 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('
([^/]*/b>)", newLine):
+ res.append(striphtml(elt.group(1)
+ .replace("
", "\x02")
+ .replace("", "\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)
diff --git a/modules/cristal.py b/modules/cristal.py
deleted file mode 100644
index fb674ea..0000000
--- a/modules/cristal.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# coding=utf-8
-
-from tools import web
-
-nemubotversion = 3.3
-
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "Gets information about Cristal missions"
-
-def help_full ():
- return "!cristal [id|name] : gives information about id Cristal mission."
-
-
-def get_all_missions():
- print (web.getContent(CONF.getNode("server")["url"]))
- response = web.getXML(CONF.getNode("server")["url"])
- print (CONF.getNode("server")["url"])
- if response is not None:
- return response.getNodes("mission")
- else:
- return None
-
-def get_mission(id=None, name=None, people=None):
- missions = get_all_missions()
- if missions is not None:
- for m in missions.childs:
- if id is not None and m.getFirstNode("id").getContent() == id:
- return m
- elif (name is not None or name in m.getFirstNode("title").getContent()) and (people is not None or people in m.getFirstNode("contact").getContent()):
- return m
- return None
-
-def cmd_cristal(msg):
- if len(msg.cmds) > 1:
- srch = msg.cmds[1]
- else:
- srch = ""
-
- res = Response(msg.sender, channel=msg.channel, nomore="Je n'ai pas d'autre mission à afficher")
-
- try:
- id=int(srch)
- name=""
- except:
- id=None
- name=srch
-
- missions = get_all_missions()
- if missions is not None:
- print (missions)
- for m in missions:
- print (m)
- idm = m.getFirstNode("id").getContent()
- crs = m.getFirstNode("title").getContent()
- contact = m.getFirstNode("contact").getDate()
- updated = m.getFirstNode("updated").getDate()
- content = m.getFirstNode("content").getContent()
-
- res.append_message(msg, crs + " ; contacter : " + contact + " : " + content)
- else:
- res.append_message("Aucune mission n'a été trouvée")
-
- return res
diff --git a/modules/cristal.xml b/modules/cristal.xml
deleted file mode 100644
index 3e83d90..0000000
--- a/modules/cristal.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/modules/ctfs.py b/modules/ctfs.py
new file mode 100644
index 0000000..ac27c4a
--- /dev/null
+++ b/modules/ctfs.py
@@ -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
diff --git a/modules/cve.py b/modules/cve.py
new file mode 100644
index 0000000..18d9898
--- /dev/null
+++ b/modules/cve.py
@@ -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
diff --git a/modules/ddg.py b/modules/ddg.py
new file mode 100644
index 0000000..089409b
--- /dev/null
+++ b/modules/ddg.py
@@ -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
diff --git a/modules/ddg/DDGSearch.py b/modules/ddg/DDGSearch.py
deleted file mode 100644
index 77aee50..0000000
--- a/modules/ddg/DDGSearch.py
+++ /dev/null
@@ -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
diff --git a/modules/ddg/WFASearch.py b/modules/ddg/WFASearch.py
deleted file mode 100644
index b91fa2c..0000000
--- a/modules/ddg/WFASearch.py
+++ /dev/null
@@ -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
\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
diff --git a/modules/ddg/Wikipedia.py b/modules/ddg/Wikipedia.py
deleted file mode 100644
index 314af38..0000000
--- a/modules/ddg/Wikipedia.py
+++ /dev/null
@@ -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"(|
[]*/>|][]*>[^>]*]|]*>[^>]*|\{\{[^{}]*\}\}|\[\[([^\[\]]*\[\[[^\]\[]*\]\])+[^\[\]]*\]\]|\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}|\{\{([^{}]*\{\{([^{}]*\{\{[^{}]*\}\}[^{}]*)+\}\}[^{}]*)+\}\}|\[\[[^\]|]+(\|[^\]\|]+)*\]\])|#\* ''" + "\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")
diff --git a/modules/ddg/__init__.py b/modules/ddg/__init__.py
deleted file mode 100644
index ff50274..0000000
--- a/modules/ddg/__init__.py
+++ /dev/null
@@ -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)
diff --git a/modules/dig.py b/modules/dig.py
new file mode 100644
index 0000000..bec0a87
--- /dev/null
+++ b/modules/dig.py
@@ -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
diff --git a/modules/disas.py b/modules/disas.py
new file mode 100644
index 0000000..cb80ef3
--- /dev/null
+++ b/modules/disas.py
@@ -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
diff --git a/modules/events.py b/modules/events.py
new file mode 100644
index 0000000..acac196
--- /dev/null
+++ b/modules/events.py
@@ -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.")
diff --git a/modules/events.xml b/modules/events.xml
deleted file mode 100644
index a96794d..0000000
--- a/modules/events.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/modules/events/__init__.py b/modules/events/__init__.py
deleted file mode 100644
index c331157..0000000
--- a/modules/events/__init__.py
+++ /dev/null
@@ -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à.")
diff --git a/modules/freetarifs.py b/modules/freetarifs.py
new file mode 100644
index 0000000..49ad8a6
--- /dev/null
+++ b/modules/freetarifs.py
@@ -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
diff --git a/modules/github.py b/modules/github.py
new file mode 100644
index 0000000..5f9a7d9
--- /dev/null
+++ b/modules/github.py
@@ -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
diff --git a/modules/grep.py b/modules/grep.py
new file mode 100644
index 0000000..fde8ecb
--- /dev/null
+++ b/modules/grep.py
@@ -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
diff --git a/modules/imdb.py b/modules/imdb.py
new file mode 100644
index 0000000..7a42935
--- /dev/null
+++ b/modules/imdb.py
@@ -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)
diff --git a/modules/jsonbot.py b/modules/jsonbot.py
new file mode 100644
index 0000000..3126dc1
--- /dev/null
+++ b/modules/jsonbot.py
@@ -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)")
diff --git a/modules/man.py b/modules/man.py
index 00edc8e..f60e0cf 100644
--- a/modules/man.py
+++ b/modules/man.py
@@ -1,66 +1,78 @@
-# coding=utf-8
+"""Read manual pages on IRC"""
+
+# PYTHON STUFFS #######################################################
import subprocess
import re
import os
-nemubotversion = 3.3
+from nemubot.hooks import hook
-def load(context):
- from hooks import Hook
- add_hook("cmd_hook", Hook(cmd_man, "MAN"))
- add_hook("cmd_hook", Hook(cmd_whatis, "man"))
+from nemubot.module.more import Response
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "Read man on IRC"
-def help_full ():
- return "!man [0-9] /what/: gives informations about /what/."
+# GLOBALS #############################################################
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):
args = ["man"]
num = None
- if len(msg.cmds) == 2:
- args.append(msg.cmds[1])
- elif len(msg.cmds) >= 3:
+ if len(msg.args) == 1:
+ args.append(msg.args[0])
+ elif len(msg.args) >= 2:
try:
- num = int(msg.cmds[1])
+ num = int(msg.args[0])
args.append("%d" % num)
- args.append(msg.cmds[2])
+ args.append(msg.args[1])
except ValueError:
- args.append(msg.cmds[1])
+ args.append(msg.args[0])
os.unsetenv("LANG")
- res = Response(msg.sender, channel=msg.channel)
- with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
+ 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"):
(line, n) = RGXP_s.subn(b'', line)
res.append_message(line.decode())
if len(res.messages) <= 0:
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:
- 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
-def cmd_whatis(msg):
- args = ["whatis", " ".join(msg.cmds[1:])]
- res = Response(msg.sender, channel=msg.channel)
- with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
+@hook.command("man",
+ 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"):
(line, n) = RGXP_s.subn(b'', line)
res.append_message(" ".join(line.decode().split()))
if len(res.messages) <= 0:
- 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))
- 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
diff --git a/modules/mapquest.py b/modules/mapquest.py
new file mode 100644
index 0000000..f328e1d
--- /dev/null
+++ b/modules/mapquest.py
@@ -0,0 +1,68 @@
+"""Transform name location to GPS coordinates"""
+
+# 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
+
+# GLOBALS #############################################################
+
+URL_API = "https://open.mapquestapi.com/geocoding/v1/address?key=%s&location=%%s"
+
+
+# LOADING #############################################################
+
+def load(context):
+ if not context.config or "apikey" not in context.config:
+ raise ImportError("You need a MapQuest API key in order to use this "
+ "module. Add it to the module configuration file:\n"
+ "\nRegister at https://developer.mapquest.com/")
+ global URL_API
+ URL_API = URL_API % context.config["apikey"].replace("%", "%%")
+
+
+# MODULE CORE #########################################################
+
+def geocode(location):
+ obj = web.getJSON(URL_API % quote(location))
+
+ if "results" in obj and "locations" in obj["results"][0]:
+ for loc in obj["results"][0]["locations"]:
+ yield loc
+
+
+def where(loc):
+ return re.sub(" +", " ",
+ "{street} {adminArea5} {adminArea4} {adminArea3} "
+ "{adminArea1}".format(**loc)).strip()
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("geocode",
+ help="Get GPS coordinates of a place",
+ help_usage={
+ "PLACE": "Get GPS coordinates of PLACE"
+ })
+def cmd_geocode(msg):
+ if not len(msg.args):
+ raise IMException("indicate a name")
+
+ res = Response(channel=msg.channel, nick=msg.frm,
+ nomore="No more geocode", count=" (%s more geocode)")
+
+ for loc in geocode(' '.join(msg.args)):
+ res.append_message("%s is at %s,%s (%s precision)" %
+ (where(loc),
+ loc["latLng"]["lat"],
+ loc["latLng"]["lng"],
+ loc["geocodeQuality"].lower()))
+
+ return res
diff --git a/modules/mediawiki.py b/modules/mediawiki.py
new file mode 100644
index 0000000..be608ca
--- /dev/null
+++ b/modules/mediawiki.py
@@ -0,0 +1,249 @@
+# coding=utf-8
+
+"""Use MediaWiki API to get pages"""
+
+import re
+import urllib.parse
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
+
+nemubotversion = 3.4
+
+from nemubot.module.more import Response
+
+
+# MEDIAWIKI REQUESTS ##################################################
+
+def get_namespaces(site, ssl=False, path="/w/api.php"):
+ # Built URL
+ url = "http%s://%s%s?format=json&action=query&meta=siteinfo&siprop=namespaces" % (
+ "s" if ssl else "", site, path)
+
+ # Make the request
+ data = web.getJSON(url)
+
+ namespaces = dict()
+ for ns in data["query"]["namespaces"]:
+ namespaces[data["query"]["namespaces"][ns]["*"]] = data["query"]["namespaces"][ns]
+ return namespaces
+
+
+def get_raw_page(site, term, ssl=False, path="/w/api.php"):
+ # Built URL
+ url = "http%s://%s%s?format=json&redirects&action=query&prop=revisions&rvprop=content&titles=%s" % (
+ "s" if ssl else "", site, path, urllib.parse.quote(term))
+
+ # Make the request
+ data = web.getJSON(url)
+
+ for k in data["query"]["pages"]:
+ try:
+ return data["query"]["pages"][k]["revisions"][0]["*"]
+ except:
+ raise IMException("article not found")
+
+
+def get_unwikitextified(site, wikitext, ssl=False, path="/w/api.php"):
+ # Built URL
+ url = "http%s://%s%s?format=json&action=expandtemplates&text=%s" % (
+ "s" if ssl else "", site, path, urllib.parse.quote(wikitext))
+
+ # Make the request
+ data = web.getJSON(url)
+
+ return data["expandtemplates"]["*"]
+
+
+## Search
+
+def opensearch(site, term, ssl=False, path="/w/api.php"):
+ # Built URL
+ url = "http%s://%s%s?format=json&action=opensearch&search=%s" % (
+ "s" if ssl else "", site, path, urllib.parse.quote(term))
+
+ # Make the request
+ response = web.getJSON(url)
+
+ if response is not None and len(response) >= 4:
+ for k in range(len(response[1])):
+ yield (response[1][k],
+ response[2][k],
+ response[3][k])
+
+
+def search(site, term, ssl=False, path="/w/api.php"):
+ # Built URL
+ url = "http%s://%s%s?format=json&action=query&list=search&srsearch=%s&srprop=titlesnippet|snippet" % (
+ "s" if ssl else "", site, path, urllib.parse.quote(term))
+
+ # Make the request
+ data = web.getJSON(url)
+
+ if data is not None and "query" in data and "search" in data["query"]:
+ for itm in data["query"]["search"]:
+ yield (web.striphtml(itm["titlesnippet"].replace("", "\x03\x02").replace("", "\x03\x02")),
+ web.striphtml(itm["snippet"].replace("", "\x03\x02").replace("", "\x03\x02")))
+
+
+# PARSING FUNCTIONS ###################################################
+
+def get_model(cnt, model="Infobox"):
+ for full in re.findall(r"(\{\{" + model + " .*?(?:\{\{.*?}}.*?)*}})", cnt, flags=re.DOTALL):
+ return full[3 + len(model):-2].replace("\n", " ").strip()
+
+
+def strip_model(cnt):
+ # Strip models at begin: mostly useless
+ cnt = re.sub(r"^(({{([^{]|\s|({{([^{]|\s|{{.*?}})*?}})*?)*?}}|\[\[([^[]|\s|\[\[.*?\]\])*?\]\])\s*)+", "", cnt, flags=re.DOTALL)
+
+ # Remove new line from models
+ for full in re.findall(r"{{.*?}}", cnt, flags=re.DOTALL):
+ cnt = cnt.replace(full, full.replace("\n", " "), 1)
+
+ # Remove new line after titles
+ cnt, _ = re.subn(r"((?P==+)\s*(.*?)\s*(?P=title))\n+", r"\1", cnt)
+
+ # Strip HTML comments
+ cnt = re.sub(r"", "", cnt, flags=re.DOTALL)
+
+ # Strip ref
+ cnt = re.sub(r"", "", cnt, flags=re.DOTALL)
+ return cnt
+
+
+def parse_wikitext(site, cnt, namespaces=dict(), **kwargs):
+ for i, _, _, _ in re.findall(r"({{([^{]|\s|({{(.|\s|{{.*?}})*?}})*?)*?}})", cnt):
+ cnt = cnt.replace(i, get_unwikitextified(site, i, **kwargs), 1)
+
+ # Strip [[...]]
+ for full, args, lnk in re.findall(r"(\[\[(.*?|)?([^|]*?)\]\])", cnt):
+ ns = lnk.find(":")
+ if lnk == "":
+ cnt = cnt.replace(full, args[:-1], 1)
+ elif ns > 0:
+ namespace = lnk[:ns]
+ if namespace in namespaces and namespaces[namespace]["canonical"] == "Category":
+ cnt = cnt.replace(full, "", 1)
+ continue
+ cnt = cnt.replace(full, lnk, 1)
+ else:
+ cnt = cnt.replace(full, lnk, 1)
+
+ # Strip HTML tags
+ cnt = web.striphtml(cnt)
+
+ return cnt
+
+
+# FORMATING FUNCTIONS #################################################
+
+def irc_format(cnt):
+ cnt, _ = re.subn(r"(?P==+)\s*(.*?)\s*(?P=title)", "\x03\x16" + r"\2" + " :\x03\x16 ", cnt)
+ return cnt.replace("'''", "\x03\x02").replace("''", "\x03\x1f")
+
+
+def parse_infobox(cnt):
+ for v in cnt.split("|"):
+ try:
+ yield re.sub(r"^\s*([^=]*[^=\s])\s*=\s*(.+)\s*$", "\x03\x02" + r"\1" + ":\x03\x02 " + r"\2", v).replace("
", ", ").replace("
", ", ").strip()
+ except:
+ yield re.sub(r"^\s+(.+)\s+$", "\x03\x02" + r"\1" + "\x03\x02", v).replace("
", ", ").replace("
", ", ").strip()
+
+
+def get_page(site, term, subpart=None, **kwargs):
+ raw = get_raw_page(site, term, **kwargs)
+
+ if subpart is not None:
+ subpart = subpart.replace("_", " ")
+ raw = re.sub(r"^.*(?P==+)\s*(" + subpart + r")\s*(?P=title)", r"\1 \2 \1", raw, flags=re.DOTALL)
+
+ return raw
+
+
+# NEMUBOT #############################################################
+
+def mediawiki_response(site, term, to, **kwargs):
+ ns = get_namespaces(site, **kwargs)
+
+ terms = term.split("#", 1)
+
+ try:
+ # Print the article if it exists
+ return Response(strip_model(get_page(site, terms[0], subpart=terms[1] if len(terms) > 1 else None, **kwargs)),
+ line_treat=lambda line: irc_format(parse_wikitext(site, line, ns, **kwargs)),
+ channel=to)
+ except:
+ pass
+
+ # Try looking at opensearch
+ os = [x for x, _, _ in opensearch(site, terms[0], **kwargs)]
+ print(os)
+ # Fallback to global search
+ if not len(os):
+ os = [x for x, _ in search(site, terms[0], **kwargs) if x is not None and x != ""]
+ return Response(os,
+ channel=to,
+ title="Article not found, would you mean")
+
+
+@hook.command("mediawiki",
+ help="Read an article on a MediaWiki",
+ keywords={
+ "ssl": "query over https instead of http",
+ "path=PATH": "absolute path to the API",
+ })
+def cmd_mediawiki(msg):
+ if len(msg.args) < 2:
+ raise IMException("indicate a domain and a term to search")
+
+ return mediawiki_response(msg.args[0],
+ " ".join(msg.args[1:]),
+ msg.to_response,
+ **msg.kwargs)
+
+
+@hook.command("mediawiki_search",
+ help="Search an article on a MediaWiki",
+ keywords={
+ "ssl": "query over https instead of http",
+ "path=PATH": "absolute path to the API",
+ })
+def cmd_srchmediawiki(msg):
+ if len(msg.args) < 2:
+ raise IMException("indicate a domain and a term to search")
+
+ res = Response(channel=msg.to_response, nomore="No more results", count=" (%d more results)")
+
+ for r in search(msg.args[0], " ".join(msg.args[1:]), **msg.kwargs):
+ res.append_message("%s: %s" % r)
+
+ return res
+
+
+@hook.command("mediawiki_infobox",
+ help="Highlight information from an article on a MediaWiki",
+ keywords={
+ "ssl": "query over https instead of http",
+ "path=PATH": "absolute path to the API",
+ })
+def cmd_infobox(msg):
+ if len(msg.args) < 2:
+ raise IMException("indicate a domain and a term to search")
+
+ ns = get_namespaces(msg.args[0], **msg.kwargs)
+
+ return Response(", ".join([x for x in parse_infobox(get_model(get_page(msg.args[0], " ".join(msg.args[1:]), **msg.kwargs), "Infobox"))]),
+ line_treat=lambda line: irc_format(parse_wikitext(msg.args[0], line, ns, **msg.kwargs)),
+ channel=msg.to_response)
+
+
+@hook.command("wikipedia")
+def cmd_wikipedia(msg):
+ if len(msg.args) < 2:
+ raise IMException("indicate a lang and a term to search")
+
+ return mediawiki_response(msg.args[0] + ".wikipedia.org",
+ " ".join(msg.args[1:]),
+ msg.to_response)
diff --git a/modules/networking.py b/modules/networking.py
deleted file mode 100644
index d6431e0..0000000
--- a/modules/networking.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# coding=utf-8
-
-import http.client
-import json
-import socket
-from urllib.parse import quote
-from urllib.parse import urlparse
-from urllib.request import urlopen
-
-from tools import web
-
-nemubotversion = 3.3
-
-def load(context):
- from hooks import Hook
- add_hook("cmd_hook", Hook(cmd_traceurl, "traceurl"))
- add_hook("cmd_hook", Hook(cmd_isup, "isup"))
- add_hook("cmd_hook", Hook(cmd_curl, "curl"))
-
-
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "The networking module"
-
-def help_full ():
- return "!traceurl /url/: Follow redirections from /url/."
-
-def cmd_curl(msg):
- if len(msg.cmds) > 1:
- try:
- req = web.getURLContent(" ".join(msg.cmds[1:]))
- if req is not None:
- res = Response(msg.sender, channel=msg.channel)
- for m in req.decode().split("\n"):
- res.append_message(m)
- return res
- else:
- return Response(msg.sender, "Une erreur est survenue lors de l'accès à cette URL", channel=msg.channel)
- except socket.error as e:
- return Response(msg.sender, e.strerror, channel=msg.channel)
- else:
- return Response(msg.sender, "Veuillez indiquer une URL à visiter.",
- channel=msg.channel)
-
-def cmd_traceurl(msg):
- if 1 < len(msg.cmds) < 6:
- res = list()
- for url in msg.cmds[1:]:
- trace = traceURL(url)
- res.append(Response(msg.sender, trace, channel=msg.channel, title="TraceURL"))
- return res
- else:
- return Response(msg.sender, "Indiquer une URL a tracer !", channel=msg.channel)
-
-def cmd_isup(msg):
- if 1 < len(msg.cmds) < 6:
- res = list()
- for url in msg.cmds[1:]:
- o = urlparse(url, "http")
- if o.netloc == "":
- o = urlparse("http://" + url)
- if o.netloc != "":
- raw = urlopen("http://isitup.org/" + o.netloc + ".json", timeout=10)
- isup = json.loads(raw.read().decode())
- if "status_code" in isup and isup["status_code"] == 1:
- res.append(Response(msg.sender, "%s est accessible (temps de reponse : %ss)" % (isup["domain"], isup["response_time"]), channel=msg.channel))
- else:
- res.append(Response(msg.sender, "%s n'est pas accessible :(" % (isup["domain"]), channel=msg.channel))
- else:
- res.append(Response(msg.sender, "%s n'est pas une URL valide" % url, channel=msg.channel))
- return res
- else:
- return Response(msg.sender, "Indiquer une URL à vérifier !", channel=msg.channel)
-
-def traceURL(url, timeout=5, stack=None):
- """Follow redirections and return the redirections stack"""
- if stack is None:
- stack = list()
- stack.append(url)
-
- if len(stack) > 15:
- stack.append('stack overflow :(')
- return stack
-
- o = urlparse(url, "http")
- if o.netloc == "":
- return stack
- if o.scheme == "http":
- conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=timeout)
- else:
- conn = http.client.HTTPSConnection(o.netloc, port=o.port, timeout=timeout)
- try:
- conn.request("HEAD", o.path, None, {"User-agent": "Nemubot v3"})
- except socket.timeout:
- stack.append("Timeout")
- return stack
- except socket.gaierror:
- print (" Unable to receive page %s from %s on %d."
- % (o.path, o.netloc, o.port))
- return stack
-
- try:
- res = conn.getresponse()
- except http.client.BadStatusLine:
- return stack
- finally:
- conn.close()
-
- if res.status == http.client.OK:
- return stack
- elif res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY or res.status == http.client.SEE_OTHER:
- url = res.getheader("Location")
- if url in stack:
- stack.append("loop on " + url)
- return stack
- else:
- return traceURL(url, timeout, stack)
- else:
- return stack
diff --git a/modules/networking/__init__.py b/modules/networking/__init__.py
new file mode 100644
index 0000000..3b939ab
--- /dev/null
+++ b/modules/networking/__init__.py
@@ -0,0 +1,184 @@
+"""Various network tools (w3m, w3c validator, curl, traceurl, ...)"""
+
+# PYTHON STUFFS #######################################################
+
+import logging
+import re
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+
+from nemubot.module.more import Response
+
+from . import isup
+from . import page
+from . import w3c
+from . import watchWebsite
+from . import whois
+
+logger = logging.getLogger("nemubot.module.networking")
+
+
+# LOADING #############################################################
+
+def load(context):
+ for mod in [isup, page, w3c, watchWebsite, whois]:
+ mod.add_event = context.add_event
+ mod.del_event = context.del_event
+ mod.save = context.save
+ mod.print = print
+ mod.send_response = context.send_response
+ page.load(context.config, context.add_hook)
+ watchWebsite.load(context.data)
+ try:
+ whois.load(context.config, context.add_hook)
+ except ImportError:
+ logger.exception("Unable to load netwhois module")
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("title",
+ help="Retrieve webpage's title",
+ help_usage={"URL": "Display the title of the given URL"})
+def cmd_title(msg):
+ if not len(msg.args):
+ raise IMException("Indicate the URL to visit.")
+
+ url = " ".join(msg.args)
+ res = re.search("(.*?)", page.fetch(" ".join(msg.args)), re.DOTALL)
+
+ if res is None:
+ raise IMException("The page %s has no title" % url)
+ else:
+ return Response("%s: %s" % (url, res.group(1).replace("\n", " ")), channel=msg.channel)
+
+
+@hook.command("curly",
+ help="Retrieve webpage's headers",
+ help_usage={"URL": "Display HTTP headers of the given URL"})
+def cmd_curly(msg):
+ if not len(msg.args):
+ raise IMException("Indicate the URL to visit.")
+
+ url = " ".join(msg.args)
+ version, status, reason, headers = page.headers(url)
+
+ return Response("Entêtes de la page %s : HTTP/%s, statut : %d %s ; headers : %s" % (url, version, status, reason, ", ".join(["\x03\x02" + h + "\x03\x02: " + v for h, v in headers])), channel=msg.channel)
+
+
+@hook.command("curl",
+ help="Retrieve webpage's body",
+ help_usage={"URL": "Display raw HTTP body of the given URL"})
+def cmd_curl(msg):
+ if not len(msg.args):
+ raise IMException("Indicate the URL to visit.")
+
+ res = Response(channel=msg.channel)
+ for m in page.fetch(" ".join(msg.args)).split("\n"):
+ res.append_message(m)
+ return res
+
+
+@hook.command("w3m",
+ help="Retrieve and format webpage's content",
+ help_usage={"URL": "Display and format HTTP content of the given URL"})
+def cmd_w3m(msg):
+ if not len(msg.args):
+ raise IMException("Indicate the URL to visit.")
+ res = Response(channel=msg.channel)
+ for line in page.render(" ".join(msg.args)).split("\n"):
+ res.append_message(line)
+ return res
+
+
+@hook.command("traceurl",
+ help="Follow redirections of a given URL and display each step",
+ help_usage={"URL": "Display redirections steps for the given URL"})
+def cmd_traceurl(msg):
+ if not len(msg.args):
+ raise IMException("Indicate an URL to trace!")
+
+ res = list()
+ for url in msg.args[:4]:
+ try:
+ trace = page.traceURL(url)
+ res.append(Response(trace, channel=msg.channel, title="TraceURL"))
+ except:
+ pass
+ return res
+
+
+@hook.command("isup",
+ help="Check if a website is up",
+ help_usage={"DOMAIN": "Check if a DOMAIN is up"})
+def cmd_isup(msg):
+ if not len(msg.args):
+ raise IMException("Indicate an domain name to check!")
+
+ res = list()
+ for url in msg.args[:4]:
+ rep = isup.isup(url)
+ if rep:
+ res.append(Response("%s is up (response time: %ss)" % (url, rep), channel=msg.channel))
+ else:
+ res.append(Response("%s is down" % (url), channel=msg.channel))
+ return res
+
+
+@hook.command("w3c",
+ help="Perform a w3c HTML validator check",
+ help_usage={"URL": "Do W3C HTML validation on the given URL"})
+def cmd_w3c(msg):
+ if not len(msg.args):
+ raise IMException("Indicate an URL to validate!")
+
+ headers, validator = w3c.validator(msg.args[0])
+
+ res = Response(channel=msg.channel, nomore="No more error")
+
+ res.append_message("%s: status: %s, %s warning(s), %s error(s)" % (validator["url"], headers["X-W3C-Validator-Status"], headers["X-W3C-Validator-Warnings"], headers["X-W3C-Validator-Errors"]))
+
+ for m in validator["messages"]:
+ if "lastLine" not in m:
+ res.append_message("%s%s: %s" % (m["type"][0].upper(), m["type"][1:], m["message"]))
+ else:
+ res.append_message("%s%s on line %s, col %s: %s" % (m["type"][0].upper(), m["type"][1:], m["lastLine"], m["lastColumn"], m["message"]))
+
+ return res
+
+
+
+@hook.command("watch", data="diff",
+ help="Alert on webpage change",
+ help_usage={"URL": "Watch the given URL and alert when it changes"})
+@hook.command("updown", data="updown",
+ help="Alert on server availability change",
+ help_usage={"URL": "Watch the given domain and alert when it availability status changes"})
+def cmd_watch(msg, diffType="diff"):
+ if not len(msg.args):
+ raise IMException("indicate an URL to watch!")
+
+ return watchWebsite.add_site(msg.args[0], msg.frm, msg.channel, msg.server, diffType)
+
+
+@hook.command("listwatch",
+ help="List URL watched for the channel",
+ help_usage={None: "List URL watched for the channel"})
+def cmd_listwatch(msg):
+ wl = watchWebsite.watchedon(msg.channel)
+ if len(wl):
+ return Response(wl, channel=msg.channel, title="URL watched on this channel")
+ else:
+ return Response("No URL are currently watched. Use !watch URL to watch one.", channel=msg.channel)
+
+
+@hook.command("unwatch",
+ help="Unwatch a previously watched URL",
+ help_usage={"URL": "Unwatch the given URL"})
+def cmd_unwatch(msg):
+ if not len(msg.args):
+ raise IMException("which URL should I stop watching?")
+
+ for arg in msg.args:
+ return watchWebsite.del_site(arg, msg.frm, msg.channel, msg.frm_owner)
diff --git a/modules/networking/isup.py b/modules/networking/isup.py
new file mode 100644
index 0000000..99e2664
--- /dev/null
+++ b/modules/networking/isup.py
@@ -0,0 +1,18 @@
+import urllib
+
+from nemubot.tools.web import getNormalizedURL, getJSON
+
+def isup(url):
+ """Determine if the given URL is up or not
+
+ Argument:
+ url -- the URL to check
+ """
+
+ o = urllib.parse.urlparse(getNormalizedURL(url), "http")
+ if o.netloc != "":
+ isup = getJSON("https://isitup.org/%s.json" % o.netloc)
+ if isup is not None and "status_code" in isup and isup["status_code"] == 1:
+ return isup["response_time"]
+
+ return None
diff --git a/modules/networking/page.py b/modules/networking/page.py
new file mode 100644
index 0000000..689944b
--- /dev/null
+++ b/modules/networking/page.py
@@ -0,0 +1,131 @@
+import http.client
+import socket
+import subprocess
+import tempfile
+import urllib
+
+from nemubot import __version__
+from nemubot.exception import IMException
+from nemubot.tools import web
+
+
+def load(CONF, add_hook):
+ # TODO: check w3m exists
+ pass
+
+
+def headers(url):
+ """Retrieve HTTP header for the given URL
+
+ Argument:
+ url -- the page URL to get header
+ """
+
+ o = urllib.parse.urlparse(web.getNormalizedURL(url), "http")
+ if o.netloc == "":
+ raise IMException("invalid URL")
+ if o.scheme == "http":
+ conn = http.client.HTTPConnection(o.hostname, port=o.port, timeout=5)
+ else:
+ conn = http.client.HTTPSConnection(o.hostname, port=o.port, timeout=5)
+ try:
+ conn.request("HEAD", o.path, None, {"User-agent":
+ "Nemubot v%s" % __version__})
+ except ConnectionError as e:
+ raise IMException(e.strerror)
+ except socket.timeout:
+ raise IMException("request timeout")
+ except socket.gaierror:
+ print (" Unable to receive page %s from %s on %d."
+ % (o.path, o.hostname, o.port if o.port is not None else 0))
+ raise IMException("an unexpected error occurs")
+
+ try:
+ res = conn.getresponse()
+ except http.client.BadStatusLine:
+ raise IMException("An error occurs")
+ finally:
+ conn.close()
+
+ return (res.version, res.status, res.reason, res.getheaders())
+
+
+def _onNoneDefault():
+ raise IMException("An error occurs when trying to access the page")
+
+
+def fetch(url, onNone=_onNoneDefault):
+ """Retrieve the content of the given URL
+
+ Argument:
+ url -- the URL to fetch
+ """
+
+ try:
+ req = web.getURLContent(url)
+ if req is not None:
+ return req
+ else:
+ if callable(onNone):
+ return onNone()
+ else:
+ return None
+ except ConnectionError as e:
+ raise IMException(e.strerror)
+ except socket.timeout:
+ raise IMException("The request timeout when trying to access the page")
+ except socket.error as e:
+ raise IMException(e.strerror)
+
+
+def _render(cnt):
+ """Render the page contained in cnt as HTML page"""
+ if cnt is None:
+ return None
+
+ with tempfile.NamedTemporaryFile() as fp:
+ fp.write(cnt.encode())
+
+ args = ["w3m", "-T", "text/html", "-dump"]
+ args.append(fp.name)
+ with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
+ return proc.stdout.read().decode()
+
+
+def render(url, onNone=_onNoneDefault):
+ """Use w3m to render the given url
+
+ Argument:
+ url -- the URL to render
+ """
+
+ return _render(fetch(url, onNone))
+
+
+def traceURL(url, stack=None):
+ """Follow redirections and return the redirections stack
+
+ Argument:
+ url -- the URL to trace
+ """
+
+ if stack is None:
+ stack = list()
+ stack.append(url)
+
+ if len(stack) > 15:
+ stack.append('stack overflow :(')
+ return stack
+
+ _, status, _, heads = headers(url)
+
+ if status == http.client.FOUND or status == http.client.MOVED_PERMANENTLY or status == http.client.SEE_OTHER:
+ for h, c in heads:
+ if h == "Location":
+ url = c
+ if url in stack:
+ stack.append("loop on " + url)
+ return stack
+ else:
+ return traceURL(url, stack)
+ return stack
diff --git a/modules/networking/w3c.py b/modules/networking/w3c.py
new file mode 100644
index 0000000..3c8084f
--- /dev/null
+++ b/modules/networking/w3c.py
@@ -0,0 +1,32 @@
+import json
+import urllib
+
+from nemubot import __version__
+from nemubot.exception import IMException
+from nemubot.tools.web import getNormalizedURL
+
+def validator(url):
+ """Run the w3c validator on the given URL
+
+ Argument:
+ url -- the URL to validate
+ """
+
+ o = urllib.parse.urlparse(getNormalizedURL(url), "http")
+ if o.netloc == "":
+ raise IMException("Indicate a valid URL!")
+
+ try:
+ req = urllib.request.Request("https://validator.w3.org/check?uri=%s&output=json" % (urllib.parse.quote(o.geturl())), headers={ 'User-Agent' : "Nemubot v%s" % __version__})
+ raw = urllib.request.urlopen(req, timeout=10)
+ except urllib.error.HTTPError as e:
+ raise IMException("HTTP error occurs: %s %s" % (e.code, e.reason))
+
+ headers = dict()
+ for Hname, Hval in raw.getheaders():
+ headers[Hname] = Hval
+
+ if "X-W3C-Validator-Status" not in headers or (headers["X-W3C-Validator-Status"] != "Valid" and headers["X-W3C-Validator-Status"] != "Invalid"):
+ raise IMException("Unexpected error on W3C servers" + (" (" + headers["X-W3C-Validator-Status"] + ")" if "X-W3C-Validator-Status" in headers else ""))
+
+ return headers, json.loads(raw.read().decode())
diff --git a/modules/networking/watchWebsite.py b/modules/networking/watchWebsite.py
new file mode 100644
index 0000000..d6b806f
--- /dev/null
+++ b/modules/networking/watchWebsite.py
@@ -0,0 +1,223 @@
+"""Alert on changes on websites"""
+
+from functools import partial
+import logging
+from random import randint
+import urllib.parse
+from urllib.parse import urlparse
+
+from nemubot.event import ModuleEvent
+from nemubot.exception import IMException
+from nemubot.tools.web import getNormalizedURL
+from nemubot.tools.xmlparser.node import ModuleState
+
+logger = logging.getLogger("nemubot.module.networking.watchWebsite")
+
+from nemubot.module.more import Response
+
+from . import page
+
+DATAS = None
+
+
+def load(datas):
+ """Register events on watched website"""
+
+ global DATAS
+ DATAS = datas
+
+ DATAS.setIndex("url", "watch")
+ for site in DATAS.getNodes("watch"):
+ if site.hasNode("alert"):
+ start_watching(site, randint(-30, 30))
+ else:
+ print("No alert defined for this site: " + site["url"])
+ #DATAS.delChild(site)
+
+
+def watchedon(channel):
+ """Get a list of currently watched URL on the given channel.
+ """
+
+ res = list()
+ for site in DATAS.getNodes("watch"):
+ if site.hasNode("alert"):
+ for a in site.getNodes("alert"):
+ if a["channel"] == channel:
+ res.append("%s (%s)" % (site["url"], site["type"]))
+ break
+ return res
+
+
+def del_site(url, nick, channel, frm_owner):
+ """Remove a site from watching list
+
+ Argument:
+ url -- URL to unwatch
+ """
+
+ o = urlparse(getNormalizedURL(url), "http")
+ if o.scheme != "" and url in DATAS.index:
+ site = DATAS.index[url]
+ for a in site.getNodes("alert"):
+ if a["channel"] == channel:
+# if not (nick == a["nick"] or frm_owner):
+# raise IMException("you cannot unwatch this URL.")
+ site.delChild(a)
+ if not site.hasNode("alert"):
+ del_event(site["_evt_id"])
+ DATAS.delChild(site)
+ save()
+ return Response("I don't watch this URL anymore.",
+ channel=channel, nick=nick)
+ raise IMException("I didn't watch this URL!")
+
+
+def add_site(url, nick, channel, server, diffType="diff"):
+ """Add a site to watching list
+
+ Argument:
+ url -- URL to watch
+ """
+
+ o = urlparse(getNormalizedURL(url), "http")
+ if o.netloc == "":
+ raise IMException("sorry, I can't watch this URL :(")
+
+ alert = ModuleState("alert")
+ alert["nick"] = nick
+ alert["server"] = server
+ alert["channel"] = channel
+ alert["message"] = "{url} just changed!"
+
+ if url not in DATAS.index:
+ watch = ModuleState("watch")
+ watch["type"] = diffType
+ watch["url"] = url
+ watch["time"] = 123
+ DATAS.addChild(watch)
+ watch.addChild(alert)
+ start_watching(watch)
+ else:
+ DATAS.index[url].addChild(alert)
+
+ save()
+ return Response(channel=channel, nick=nick,
+ message="this site is now under my supervision.")
+
+
+def format_response(site, link='%s', title='%s', categ='%s', content='%s'):
+ """Format and send response for given site
+
+ Argument:
+ site -- DATAS structure representing a site to watch
+
+ Keyword arguments:
+ link -- link to the content
+ title -- for ATOM feed: title of the new article
+ categ -- for ATOM feed: category of the new article
+ content -- content of the page/new article
+ """
+
+ for a in site.getNodes("alert"):
+ send_response(a["server"],
+ Response(a["message"].format(url=site["url"],
+ link=link,
+ title=title,
+ categ=categ,
+ content=content),
+ channel=a["channel"],
+ server=a["server"]))
+
+
+def alert_change(content, site):
+ """Function called when a change is detected on a given site
+
+ Arguments:
+ content -- The new content
+ site -- DATAS structure representing a site to watch
+ """
+
+ if site["type"] == "updown":
+ if site["lastcontent"] is None:
+ site["lastcontent"] = content is not None
+
+ if (content is not None) != site.getBool("lastcontent"):
+ format_response(site, link=site["url"])
+ site["lastcontent"] = content is not None
+ start_watching(site)
+ return
+
+ if content is None:
+ start_watching(site)
+ return
+
+ if site["type"] == "atom":
+ from nemubot.tools.feed import Feed
+ if site["_lastpage"] is None:
+ if site["lastcontent"] is None or site["lastcontent"] == "":
+ site["lastcontent"] = content
+ site["_lastpage"] = Feed(site["lastcontent"])
+ try:
+ page = Feed(content)
+ except:
+ print("An error occurs during Atom parsing. Restart event...")
+ start_watching(site)
+ return
+ diff = site["_lastpage"] & page
+ if len(diff) > 0:
+ site["_lastpage"] = page
+ diff.reverse()
+ for d in diff:
+ site.setIndex("term", "category")
+ categories = site.index
+
+ if len(categories) > 0:
+ if d.category is None or d.category not in categories:
+ format_response(site, link=d.link, categ=categories[""]["part"], title=d.title)
+ else:
+ format_response(site, link=d.link, categ=categories[d.category]["part"], title=d.title)
+ else:
+ format_response(site, link=d.link, title=urllib.parse.unquote(d.title))
+ else:
+ start_watching(site)
+ return # Stop here, no changes, so don't save
+
+ else: # Just looking for any changes
+ format_response(site, link=site["url"], content=content)
+ site["lastcontent"] = content
+ start_watching(site)
+ save()
+
+
+def fwatch(url):
+ cnt = page.fetch(url, None)
+ if cnt is not None:
+ render = page._render(cnt)
+ if render is None or render == "":
+ return cnt
+ return render
+ return None
+
+
+def start_watching(site, offset=0):
+ """Launch the event watching given site
+
+ Argument:
+ site -- DATAS structure representing a site to watch
+
+ Keyword argument:
+ offset -- offset time to delay the launch of the first check
+ """
+
+ #o = urlparse(getNormalizedURL(site["url"]), "http")
+ #print("Add %s event for site: %s" % (site["type"], o.netloc))
+
+ try:
+ evt = ModuleEvent(func=partial(fwatch, url=site["url"]),
+ cmp=site["lastcontent"],
+ offset=offset, interval=site.getInt("time"),
+ call=partial(alert_change, site=site))
+ site["_evt_id"] = add_event(evt)
+ except IMException:
+ logger.exception("Unable to watch %s", site["url"])
diff --git a/modules/networking/whois.py b/modules/networking/whois.py
new file mode 100644
index 0000000..999dc01
--- /dev/null
+++ b/modules/networking/whois.py
@@ -0,0 +1,136 @@
+# PYTHON STUFFS #######################################################
+
+import datetime
+import urllib
+
+from nemubot.exception import IMException
+from nemubot.tools.web import getJSON
+
+from nemubot.module.more import Response
+
+URL_AVAIL = "https://www.whoisxmlapi.com/whoisserver/WhoisService?cmd=GET_DN_AVAILABILITY&domainName=%%s&outputFormat=json&username=%s&password=%s"
+URL_WHOIS = "https://www.whoisxmlapi.com/whoisserver/WhoisService?da=2&domainName=%%s&outputFormat=json&userName=%s&password=%s"
+
+
+# LOADING #############################################################
+
+def load(CONF, add_hook):
+ global URL_AVAIL, URL_WHOIS
+
+ if not CONF or not CONF.hasNode("whoisxmlapi") or "username" not in CONF.getNode("whoisxmlapi") or "password" not in CONF.getNode("whoisxmlapi"):
+ raise ImportError("You need a WhoisXML API account in order to use "
+ "the !netwhois feature. Add it to the module "
+ "configuration file:\n\nRegister at "
+ "https://www.whoisxmlapi.com/newaccount.php")
+
+ URL_AVAIL = URL_AVAIL % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"]))
+ URL_WHOIS = URL_WHOIS % (urllib.parse.quote(CONF.getNode("whoisxmlapi")["username"]), urllib.parse.quote(CONF.getNode("whoisxmlapi")["password"]))
+
+ import nemubot.hooks
+ add_hook(nemubot.hooks.Command(cmd_whois, "netwhois",
+ help="Get whois information about given domains",
+ help_usage={"DOMAIN": "Return whois information on the given DOMAIN"}),
+ "in","Command")
+ add_hook(nemubot.hooks.Command(cmd_avail, "domain_available",
+ help="Domain availability check using whoisxmlapi.com",
+ help_usage={"DOMAIN": "Check if the given DOMAIN is available or not"}),
+ "in","Command")
+
+
+# MODULE CORE #########################################################
+
+def whois_entityformat(entity):
+ ret = ""
+ if "organization" in entity:
+ ret += entity["organization"]
+ if "organization" in entity and "name" in entity:
+ ret += " "
+ if "name" in entity:
+ ret += entity["name"]
+
+ if "country" in entity or "city" in entity or "telephone" in entity or "email" in entity:
+ ret += " (from "
+ if "street1" in entity:
+ ret += entity["street1"] + " "
+ if "city" in entity:
+ ret += entity["city"] + " "
+ if "state" in entity:
+ ret += entity["state"] + " "
+ if "country" in entity:
+ ret += entity["country"] + " "
+ if "telephone" in entity:
+ ret += entity["telephone"] + " "
+ if "email" in entity:
+ ret += entity["email"] + " "
+ ret = ret.rstrip() + ")"
+
+ return ret.lstrip()
+
+def available(dom):
+ js = getJSON(URL_AVAIL % urllib.parse.quote(dom))
+
+ if "ErrorMessage" in js:
+ raise IMException(js["ErrorMessage"]["msg"])
+
+ return js["DomainInfo"]["domainAvailability"] == "AVAILABLE"
+
+
+# MODULE INTERFACE ####################################################
+
+def cmd_avail(msg):
+ if not len(msg.args):
+ raise IMException("Indicate a domain name for having its availability status!")
+
+ return Response(["%s: %s" % (dom, "available" if available(dom) else "unavailable") for dom in msg.args],
+ channel=msg.channel)
+
+
+def cmd_whois(msg):
+ if not len(msg.args):
+ raise IMException("Indiquer un domaine ou une IP à whois !")
+
+ dom = msg.args[0]
+
+ js = getJSON(URL_WHOIS % urllib.parse.quote(dom))
+
+ if "ErrorMessage" in js:
+ raise IMException(js["ErrorMessage"]["msg"])
+
+ whois = js["WhoisRecord"]
+
+ res = []
+
+ if "registrarName" in whois:
+ res.append("\x03\x02registered by\x03\x02 " + whois["registrarName"])
+
+ if "domainAvailability" in whois:
+ res.append(whois["domainAvailability"])
+
+ if "contactEmail" in whois:
+ res.append("\x03\x02contact email\x03\x02 " + whois["contactEmail"])
+
+ if "audit" in whois:
+ if "createdDate" in whois["audit"] and "$" in whois["audit"]["createdDate"]:
+ res.append("\x03\x02created on\x03\x02 " + whois["audit"]["createdDate"]["$"])
+ if "updatedDate" in whois["audit"] and "$" in whois["audit"]["updatedDate"]:
+ res.append("\x03\x02updated on\x03\x02 " + whois["audit"]["updatedDate"]["$"])
+
+ if "registryData" in whois:
+ if "expiresDateNormalized" in whois["registryData"]:
+ res.append("\x03\x02expire on\x03\x02 " + whois["registryData"]["expiresDateNormalized"])
+ if "registrant" in whois["registryData"]:
+ res.append("\x03\x02registrant:\x03\x02 " + whois_entityformat(whois["registryData"]["registrant"]))
+ if "zoneContact" in whois["registryData"]:
+ res.append("\x03\x02zone contact:\x03\x02 " + whois_entityformat(whois["registryData"]["zoneContact"]))
+ if "technicalContact" in whois["registryData"]:
+ res.append("\x03\x02technical contact:\x03\x02 " + whois_entityformat(whois["registryData"]["technicalContact"]))
+ if "administrativeContact" in whois["registryData"]:
+ res.append("\x03\x02administrative contact:\x03\x02 " + whois_entityformat(whois["registryData"]["administrativeContact"]))
+ if "billingContact" in whois["registryData"]:
+ res.append("\x03\x02billing contact:\x03\x02 " + whois_entityformat(whois["registryData"]["billingContact"]))
+
+ return Response(res,
+ title=whois["domainName"],
+ channel=msg.channel,
+ nomore="No more whois information")
diff --git a/modules/news.py b/modules/news.py
new file mode 100644
index 0000000..c4c967a
--- /dev/null
+++ b/modules/news.py
@@ -0,0 +1,61 @@
+"""Display latests news from a website"""
+
+# PYTHON STUFFS #######################################################
+
+import datetime
+import re
+from urllib.parse import urljoin
+
+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
+from nemubot.module.urlreducer import reduce_inline
+from nemubot.tools.feed import Feed, AtomEntry
+
+
+# HELP ################################################################
+
+def help_full():
+ return "Display the latests news from a given URL: !news URL"
+
+
+# MODULE CORE #########################################################
+
+def find_rss_links(url):
+ url = web.getNormalizedURL(url)
+ soup = BeautifulSoup(web.getURLContent(url))
+ for rss in soup.find_all('link', attrs={"type": re.compile("^application/(atom|rss)")}):
+ yield urljoin(url, rss["href"])
+
+def get_last_news(url):
+ from xml.parsers.expat import ExpatError
+ try:
+ feed = Feed(web.getURLContent(url))
+ return feed.entries
+ except ExpatError:
+ return []
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("news")
+def cmd_news(msg):
+ if not len(msg.args):
+ raise IMException("Indicate the URL to visit.")
+
+ url = " ".join(msg.args)
+ links = [x for x in find_rss_links(url)]
+ if len(links) == 0: links = [ url ]
+
+ res = Response(channel=msg.channel, nomore="No more news from %s" % url, line_treat=reduce_inline)
+ for n in get_last_news(links[0]):
+ res.append_message("%s published %s: %s %s" % (("\x02" + web.striphtml(n.title) + "\x0F") if n.title else "An article without title",
+ (n.updated.strftime("on %A %d. %B %Y at %H:%M") if n.updated else "someday") if isinstance(n, AtomEntry) else n.pubDate,
+ web.striphtml(n.summary) if n.summary else "",
+ n.link if n.link else ""))
+
+ return res
diff --git a/modules/nextstop.xml b/modules/nextstop.xml
deleted file mode 100644
index d34e8ae..0000000
--- a/modules/nextstop.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py
deleted file mode 100644
index 71816a8..0000000
--- a/modules/nextstop/__init__.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# coding=utf-8
-
-import http.client
-import re
-from xml.dom.minidom import parseString
-
-from .external.src import ratp
-
-nemubotversion = 3.3
-
-def load(context):
- global DATAS
- DATAS.setIndex("name", "station")
-
-
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "Informe les usagers des prochains passages des transports en communs de la RATP"
-
-def help_full ():
- return "!ratp transport line [station]: Donne des informations sur les prochains passages du transport en commun séléctionné à l'arrêt désiré. Si aucune station n'est précisée, les liste toutes."
-
-
-def extractInformation(msg, transport, line, station=None):
- if station is not None and station != "":
- times = ratp.getNextStopsAtStation(transport, line, station)
- if len(times) > 0:
- (time, direction, stationname) = times[0]
- return Response(msg.sender, message=["\x03\x02"+time+"\x03\x02 direction "+direction for time, direction, stationname in times], title="Prochains passages du %s ligne %s à l'arrêt %s" %
- (transport, line, stationname), channel=msg.channel)
- else:
- return Response(msg.sender, "La station `%s' ne semble pas exister sur le %s ligne %s."
- % (station, transport, line), msg.channel)
- else:
- stations = ratp.getAllStations(transport, line)
- if len(stations) > 0:
- return Response(msg.sender, [s for s in stations], title="Stations", channel=msg.channel)
- else:
- return Response(msg.sender, "Aucune station trouvée.", msg.channel)
-
-def ask_ratp(msg):
- """Hook entry from !ratp"""
- global DATAS
- if len(msg.cmds) == 4:
- return extractInformation(msg, msg.cmds[1], msg.cmds[2], msg.cmds[3])
- elif len(msg.cmds) == 3:
- return extractInformation(msg, msg.cmds[1], msg.cmds[2])
- else:
- return Response(msg.sender, "Mauvais usage, merci de spécifier un type de transport et une ligne, ou de consulter l'aide du module.", msg.channel, msg.nick)
- return False
diff --git a/modules/nextstop/external b/modules/nextstop/external
deleted file mode 160000
index e5675c6..0000000
--- a/modules/nextstop/external
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit e5675c631665dfbdaba55a0be66708a07d157408
diff --git a/modules/nntp.py b/modules/nntp.py
new file mode 100644
index 0000000..7fdceb4
--- /dev/null
+++ b/modules/nntp.py
@@ -0,0 +1,229 @@
+"""The NNTP module"""
+
+# PYTHON STUFFS #######################################################
+
+import email
+import email.policy
+from email.utils import mktime_tz, parseaddr, parsedate_tz
+from functools import partial
+from nntplib import NNTP, decode_header
+import re
+import time
+from datetime import datetime
+from zlib import adler32
+
+from nemubot import context
+from nemubot.event import ModuleEvent
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools.xmlparser.node import ModuleState
+
+from nemubot.module.more import Response
+
+
+# LOADING #############################################################
+
+def load(context):
+ for wn in context.data.getNodes("watched_newsgroup"):
+ watch(**wn.attributes)
+
+
+# MODULE CORE #########################################################
+
+def list_groups(group_pattern="*", **server):
+ with NNTP(**server) as srv:
+ response, l = srv.list(group_pattern)
+ for i in l:
+ yield i.group, srv.description(i.group), i.flag
+
+def read_group(group, **server):
+ with NNTP(**server) as srv:
+ response, count, first, last, name = srv.group(group)
+ resp, overviews = srv.over((first, last))
+ for art_num, over in reversed(overviews):
+ yield over
+
+def read_article(msg_id, **server):
+ with NNTP(**server) as srv:
+ response, info = srv.article(msg_id)
+ return email.message_from_bytes(b"\r\n".join(info.lines), policy=email.policy.SMTPUTF8)
+
+
+servers_lastcheck = dict()
+servers_lastseen = dict()
+
+def whatsnew(group="*", **server):
+ fill = dict()
+ if "user" in server: fill["user"] = server["user"]
+ if "password" in server: fill["password"] = server["password"]
+ if "host" in server: fill["host"] = server["host"]
+ if "port" in server: fill["port"] = server["port"]
+
+ idx = _indexServer(**server)
+ if idx in servers_lastcheck and servers_lastcheck[idx] is not None:
+ date_last_check = servers_lastcheck[idx]
+ else:
+ date_last_check = datetime.now()
+
+ if idx not in servers_lastseen:
+ servers_lastseen[idx] = []
+
+ with NNTP(**fill) as srv:
+ response, servers_lastcheck[idx] = srv.date()
+
+ response, groups = srv.newgroups(date_last_check)
+ for g in groups:
+ yield g
+
+ response, articles = srv.newnews(group, date_last_check)
+ for msg_id in articles:
+ if msg_id not in servers_lastseen[idx]:
+ servers_lastseen[idx].append(msg_id)
+ response, info = srv.article(msg_id)
+ yield email.message_from_bytes(b"\r\n".join(info.lines))
+
+ # Clean huge lists
+ if len(servers_lastseen[idx]) > 42:
+ servers_lastseen[idx] = servers_lastseen[idx][23:]
+
+
+def format_article(art, **response_args):
+ art["X-FromName"], art["X-FromEmail"] = parseaddr(art["From"] if "From" in art else "")
+ if art["X-FromName"] == '': art["X-FromName"] = art["X-FromEmail"]
+
+ date = mktime_tz(parsedate_tz(art["Date"]))
+ if date < time.time() - 120:
+ title = "\x0314In \x0F\x03{0:02d}{Newsgroups}\x0F\x0314: on \x0F{Date}\x0314 by \x0F\x03{0:02d}{X-FromName}\x0F \x02{Subject}\x0F"
+ else:
+ title = "\x0314In \x0F\x03{0:02d}{Newsgroups}\x0F\x0314: by \x0F\x03{0:02d}{X-FromName}\x0F \x02{Subject}\x0F"
+
+ return Response(art.get_payload().replace('\n', ' '),
+ title=title.format(adler32(art["Newsgroups"].encode()) & 0xf, adler32(art["X-FromEmail"].encode()) & 0xf, **{h: decode_header(i) for h,i in art.items()}),
+ **response_args)
+
+
+watches = dict()
+
+def _indexServer(**kwargs):
+ if "user" not in kwargs: kwargs["user"] = ""
+ if "password" not in kwargs: kwargs["password"] = ""
+ if "host" not in kwargs: kwargs["host"] = ""
+ if "port" not in kwargs: kwargs["port"] = 119
+ return "{user}:{password}@{host}:{port}".format(**kwargs)
+
+def _newevt(**args):
+ context.add_event(ModuleEvent(call=partial(_ticker, **args), interval=42))
+
+def _ticker(to_server, to_channel, group, server):
+ _newevt(to_server=to_server, to_channel=to_channel, group=group, server=server)
+ n = 0
+ for art in whatsnew(group, **server):
+ n += 1
+ if n > 10:
+ continue
+ context.send_response(to_server, format_article(art, channel=to_channel))
+ if n > 10:
+ context.send_response(to_server, Response("... and %s others news" % (n - 10), channel=to_channel))
+
+def watch(to_server, to_channel, group="*", **server):
+ _newevt(to_server=to_server, to_channel=to_channel, group=group, server=server)
+
+
+# MODULE INTERFACE ####################################################
+
+keywords_server = {
+ "host=HOST": "hostname or IP of the NNTP server",
+ "port=PORT": "port of the NNTP server",
+ "user=USERNAME": "username to use to connect to the server",
+ "password=PASSWORD": "password to use to connect to the server",
+}
+
+@hook.command("nntp_groups",
+ help="Show list of existing groups",
+ help_usage={
+ None: "Display all groups",
+ "PATTERN": "Filter on group matching the PATTERN"
+ },
+ keywords=keywords_server)
+def cmd_groups(msg):
+ if "host" not in msg.kwargs:
+ raise IMException("please give a hostname in keywords")
+
+ return Response(["\x02\x03{0:02d}{1}\x0F: {2}".format(adler32(g[0].encode()) & 0xf, *g) for g in list_groups(msg.args[0] if len(msg.args) > 0 else "*", **msg.kwargs)],
+ channel=msg.channel,
+ title="Matching groups on %s" % msg.kwargs["host"])
+
+
+@hook.command("nntp_overview",
+ help="Show an overview of articles in given group(s)",
+ help_usage={
+ "GROUP": "Filter on group matching the PATTERN"
+ },
+ keywords=keywords_server)
+def cmd_overview(msg):
+ if "host" not in msg.kwargs:
+ raise IMException("please give a hostname in keywords")
+
+ if not len(msg.args):
+ raise IMException("which group would you overview?")
+
+ for g in msg.args:
+ arts = []
+ for grp in read_group(g, **msg.kwargs):
+ grp["X-FromName"], grp["X-FromEmail"] = parseaddr(grp["from"] if "from" in grp else "")
+ if grp["X-FromName"] == '': grp["X-FromName"] = grp["X-FromEmail"]
+
+ arts.append("On {date}, from \x03{0:02d}{X-FromName}\x0F \x02{subject}\x0F: \x0314{message-id}\x0F".format(adler32(grp["X-FromEmail"].encode()) & 0xf, **{h: decode_header(i) for h,i in grp.items()}))
+
+ if len(arts):
+ yield Response(arts,
+ channel=msg.channel,
+ title="In \x03{0:02d}{1}\x0F".format(adler32(g[0].encode()) & 0xf, g))
+
+
+@hook.command("nntp_read",
+ help="Read an article from a server",
+ help_usage={
+ "MSG_ID": "Read the given message"
+ },
+ keywords=keywords_server)
+def cmd_read(msg):
+ if "host" not in msg.kwargs:
+ raise IMException("please give a hostname in keywords")
+
+ for msgid in msg.args:
+ if not re.match("<.*>", msgid):
+ msgid = "<" + msgid + ">"
+ art = read_article(msgid, **msg.kwargs)
+ yield format_article(art, channel=msg.channel)
+
+
+@hook.command("nntp_watch",
+ help="Launch an event looking for new groups and articles on a server",
+ help_usage={
+ None: "Watch all groups",
+ "PATTERN": "Limit the watch on group matching this PATTERN"
+ },
+ keywords=keywords_server)
+def cmd_watch(msg):
+ if "host" not in msg.kwargs:
+ raise IMException("please give a hostname in keywords")
+
+ if not msg.frm_owner:
+ raise IMException("sorry, this command is currently limited to the owner")
+
+ wnnode = ModuleState("watched_newsgroup")
+ wnnode["id"] = _indexServer(**msg.kwargs)
+ wnnode["to_server"] = msg.server
+ wnnode["to_channel"] = msg.channel
+ wnnode["group"] = msg.args[0] if len(msg.args) > 0 else "*"
+
+ wnnode["user"] = msg.kwargs["user"] if "user" in msg.kwargs else ""
+ wnnode["password"] = msg.kwargs["password"] if "password" in msg.kwargs else ""
+ wnnode["host"] = msg.kwargs["host"] if "host" in msg.kwargs else ""
+ wnnode["port"] = msg.kwargs["port"] if "port" in msg.kwargs else 119
+
+ context.data.addChild(wnnode)
+ watch(**wnnode.attributes)
+
+ return Response("Ok ok, I watch this newsgroup!", channel=msg.channel)
diff --git a/modules/openai.py b/modules/openai.py
new file mode 100644
index 0000000..b9b6e21
--- /dev/null
+++ b/modules/openai.py
@@ -0,0 +1,87 @@
+"""Perform requests to openai"""
+
+# PYTHON STUFFS #######################################################
+
+from openai import OpenAI
+
+from nemubot import context
+from nemubot.hooks import hook
+from nemubot.tools import web
+
+from nemubot.module.more import Response
+
+
+# LOADING #############################################################
+
+CLIENT = None
+MODEL = "gpt-4"
+ENDPOINT = None
+
+def load(context):
+ global CLIENT, ENDPOINT, MODEL
+ if not context.config or ("apikey" not in context.config and "endpoint" not in context.config):
+ raise ImportError ("You need a OpenAI API key in order to use "
+ "this module. Add it to the module configuration: "
+ "\n")
+ kwargs = {
+ "api_key": context.config["apikey"] or "",
+ }
+
+ if "endpoint" in context.config:
+ ENDPOINT = context.config["endpoint"]
+ kwargs["base_url"] = ENDPOINT
+
+ CLIENT = OpenAI(**kwargs)
+
+ if "model" in context.config:
+ MODEL = context.config["model"]
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("list_models",
+ help="list available LLM")
+def cmd_listllm(msg):
+ llms = web.getJSON(ENDPOINT + "/models", timeout=6)
+ return Response(message=[m for m in map(lambda i: i["id"], llms["data"])], title="Here is the available models", channel=msg.channel)
+
+
+@hook.command("set_model",
+ help="Set the model to use when talking to nemubot")
+def cmd_setllm(msg):
+ if len(msg.args) != 1:
+ raise IMException("Indicate 1 model to use")
+
+ wanted_model = msg.args[0]
+
+ llms = web.getJSON(ENDPOINT + "/models", timeout=6)
+ for model in llms["data"]:
+ if wanted_model == model["id"]:
+ break
+ else:
+ raise IMException("Unable to set such model: unknown")
+
+ MODEL = wanted_model
+ return Response("New model in use: " + wanted_model, channel=msg.channel)
+
+
+@hook.ask()
+def parseask(msg):
+ chat_completion = CLIENT.chat.completions.create(
+ messages=[
+ {
+ "role": "system",
+ "content": "You are a kind multilingual assistant. Respond to the user request in 255 characters maximum. Be conscise, go directly to the point. Never add useless terms.",
+ },
+ {
+ "role": "user",
+ "content": msg.message,
+ }
+ ],
+ model=MODEL,
+ )
+
+ return Response(chat_completion.choices[0].message.content,
+ msg.channel,
+ msg.frm)
diff --git a/modules/openroute.py b/modules/openroute.py
new file mode 100644
index 0000000..c280dec
--- /dev/null
+++ b/modules/openroute.py
@@ -0,0 +1,158 @@
+"""Lost? use our commands to find your way!"""
+
+# PYTHON STUFFS #######################################################
+
+import re
+import urllib.parse
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
+
+from nemubot.module.more import Response
+
+# GLOBALS #############################################################
+
+URL_DIRECTIONS_API = "https://api.openrouteservice.org/directions?api_key=%s&"
+URL_GEOCODE_API = "https://api.openrouteservice.org/geocoding?api_key=%s&"
+
+waytype = [
+ "unknown",
+ "state road",
+ "road",
+ "street",
+ "path",
+ "track",
+ "cycleway",
+ "footway",
+ "steps",
+ "ferry",
+ "construction",
+]
+
+
+# LOADING #############################################################
+
+def load(context):
+ if not context.config or "apikey" not in context.config:
+ raise ImportError("You need an OpenRouteService API key in order to use this "
+ "module. Add it to the module configuration file:\n"
+ "\nRegister at https://developers.openrouteservice.org")
+ global URL_DIRECTIONS_API
+ URL_DIRECTIONS_API = URL_DIRECTIONS_API % context.config["apikey"]
+ global URL_GEOCODE_API
+ URL_GEOCODE_API = URL_GEOCODE_API % context.config["apikey"]
+
+
+# MODULE CORE #########################################################
+
+def approx_distance(lng):
+ if lng > 1111:
+ return "%f km" % (lng / 1000)
+ else:
+ return "%f m" % lng
+
+
+def approx_duration(sec):
+ days = int(sec / 86400)
+ if days > 0:
+ return "%d days %f hours" % (days, (sec % 86400) / 3600)
+ hours = int((sec % 86400) / 3600)
+ if hours > 0:
+ return "%d hours %f minutes" % (hours, (sec % 3600) / 60)
+ minutes = (sec % 3600) / 60
+ if minutes > 0:
+ return "%d minutes" % minutes
+ else:
+ return "%d seconds" % sec
+
+
+def geocode(query, limit=7):
+ obj = web.getJSON(URL_GEOCODE_API + urllib.parse.urlencode({
+ 'query': query,
+ 'limit': limit,
+ }))
+
+ for f in obj["features"]:
+ yield f["geometry"]["coordinates"], f["properties"]
+
+
+def firstgeocode(query):
+ for g in geocode(query, limit=1):
+ return g
+
+
+def where(loc):
+ return "{name} {city} {state} {county} {country}".format(**loc)
+
+
+def directions(coordinates, **kwargs):
+ kwargs['coordinates'] = '|'.join(coordinates)
+
+ print(URL_DIRECTIONS_API + urllib.parse.urlencode(kwargs))
+ return web.getJSON(URL_DIRECTIONS_API + urllib.parse.urlencode(kwargs), decode_error=True)
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("geocode",
+ help="Get GPS coordinates of a place",
+ help_usage={
+ "PLACE": "Get GPS coordinates of PLACE"
+ })
+def cmd_geocode(msg):
+ res = Response(channel=msg.channel, nick=msg.frm,
+ nomore="No more geocode", count=" (%s more geocode)")
+
+ for loc in geocode(' '.join(msg.args)):
+ res.append_message("%s is at %s,%s" % (
+ where(loc[1]),
+ loc[0][1], loc[0][0],
+ ))
+
+ return res
+
+
+@hook.command("directions",
+ help="Get routing instructions",
+ help_usage={
+ "POINT1 POINT2 ...": "Get routing instructions to go from POINT1 to the last POINTX via intermediates POINTX"
+ },
+ keywords={
+ "profile=PROF": "One of driving-car, driving-hgv, cycling-regular, cycling-road, cycling-safe, cycling-mountain, cycling-tour, cycling-electric, foot-walking, foot-hiking, wheelchair. Default: foot-walking",
+ "preference=PREF": "One of fastest, shortest, recommended. Default: recommended",
+ "lang=LANG": "default: en",
+ })
+def cmd_directions(msg):
+ drcts = directions(["{0},{1}".format(*firstgeocode(g)[0]) for g in msg.args],
+ profile=msg.kwargs["profile"] if "profile" in msg.kwargs else "foot-walking",
+ preference=msg.kwargs["preference"] if "preference" in msg.kwargs else "recommended",
+ units="m",
+ language=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
+ geometry=False,
+ instructions=True,
+ instruction_format="text")
+ if "error" in drcts and "message" in drcts["error"] and drcts["error"]["message"]:
+ raise IMException(drcts["error"]["message"])
+
+ if "routes" not in drcts or not drcts["routes"]:
+ raise IMException("No route available for this trip")
+
+ myway = drcts["routes"][0]
+ myway["summary"]["strduration"] = approx_duration(myway["summary"]["duration"])
+ myway["summary"]["strdistance"] = approx_distance(myway["summary"]["distance"])
+ res = Response("Trip summary: {strdistance} in approximate {strduration}; elevation +{ascent} m -{descent} m".format(**myway["summary"]), channel=msg.channel, count=" (%d more steps)", nomore="You have arrived!")
+
+ def formatSegments(segments):
+ for segment in segments:
+ for step in segment["steps"]:
+ step["strtype"] = waytype[step["type"]]
+ step["strduration"] = approx_duration(step["duration"])
+ step["strdistance"] = approx_distance(step["distance"])
+ yield "{instruction} for {strdistance} on {strtype} (approximate time: {strduration})".format(**step)
+
+ if "segments" in myway:
+ res.append_message([m for m in formatSegments(myway["segments"])])
+
+ return res
diff --git a/modules/pkgs.py b/modules/pkgs.py
new file mode 100644
index 0000000..386946f
--- /dev/null
+++ b/modules/pkgs.py
@@ -0,0 +1,68 @@
+"""Get information about common software"""
+
+# PYTHON STUFFS #######################################################
+
+import portage
+
+from nemubot import context
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+
+from nemubot.module.more import Response
+
+DB = None
+
+# MODULE CORE #########################################################
+
+def get_db():
+ global DB
+ if DB is None:
+ DB = portage.db[portage.root]["porttree"].dbapi
+ return DB
+
+
+def package_info(pkgname):
+ pv = get_db().xmatch("match-all", pkgname)
+ if not pv:
+ raise IMException("No package named '%s' found" % pkgname)
+
+ bv = get_db().xmatch("bestmatch-visible", pkgname)
+ pvsplit = portage.catpkgsplit(bv if bv else pv[-1])
+ info = get_db().aux_get(bv if bv else pv[-1], ["DESCRIPTION", "HOMEPAGE", "LICENSE", "IUSE", "KEYWORDS"])
+
+ return {
+ "pkgname": '/'.join(pvsplit[:2]),
+ "category": pvsplit[0],
+ "shortname": pvsplit[1],
+ "lastvers": '-'.join(pvsplit[2:]) if pvsplit[3] != "r0" else pvsplit[2],
+ "othersvers": ['-'.join(portage.catpkgsplit(p)[2:]) for p in pv if p != bv],
+ "description": info[0],
+ "homepage": info[1],
+ "license": info[2],
+ "uses": info[3],
+ "keywords": info[4],
+ }
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("eix",
+ help="Get information about a package",
+ help_usage={
+ "NAME": "Get information about a software NAME"
+ })
+def cmd_eix(msg):
+ if not len(msg.args):
+ raise IMException("please give me a package to search")
+
+ def srch(term):
+ try:
+ yield package_info(term)
+ except portage.exception.AmbiguousPackageName as e:
+ for i in e.args[0]:
+ yield package_info(i)
+
+ res = Response(channel=msg.channel, count=" (%d more packages)", nomore="No more package '%s'" % msg.args[0])
+ for pi in srch(msg.args[0]):
+ res.append_message("\x03\x02{pkgname}:\x03\x02 {description} - {homepage} - {license} - last revisions: \x03\x02{lastvers}\x03\x02{ov}".format(ov=(", " + ', '.join(pi["othersvers"])) if pi["othersvers"] else "", **pi))
+ return res
diff --git a/modules/qcm.xml b/modules/qcm.xml
deleted file mode 100644
index 05a7076..0000000
--- a/modules/qcm.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/modules/qcm/Course.py b/modules/qcm/Course.py
deleted file mode 100644
index 9cddf1a..0000000
--- a/modules/qcm/Course.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# coding=utf-8
-
-COURSES = None
-
-class Course:
- def __init__(self, iden):
- global COURSES
- if iden in COURSES.index:
- self.node = COURSES.index[iden]
- else:
- self.node = { "code":"N/A", "name":"N/A", "branch":"N/A" }
-
- @property
- def id(self):
- return self.node["xml:id"]
-
- @property
- def code(self):
- return self.node["code"]
-
- @property
- def name(self):
- return self.node["name"]
-
- @property
- def branch(self):
- return self.node["branch"]
-
- @property
- def validated(self):
- return int(self.node["validated"]) > 0
diff --git a/modules/qcm/Question.py b/modules/qcm/Question.py
deleted file mode 100644
index 6895680..0000000
--- a/modules/qcm/Question.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# coding=utf-8
-
-from datetime import datetime
-import hashlib
-import http.client
-import socket
-from urllib.parse import quote
-
-from .Course import Course
-from .User import User
-
-QUESTIONS = None
-
-class Question:
- def __init__(self, node):
- self.node = node
-
- @property
- def ident(self):
- return self.node["xml:id"]
-
- @property
- def id(self):
- return self.node["xml:id"]
-
- @property
- def question(self):
- return self.node["question"]
-
- @property
- def course(self):
- return Course(self.node["course"])
-
- @property
- def answers(self):
- return self.node.getNodes("answer")
-
- @property
- def validator(self):
- return User(self.node["validator"])
-
- @property
- def writer(self):
- return User(self.node["writer"])
-
- @property
- def validated(self):
- return self.node["validated"]
-
- @property
- def addedtime(self):
- return datetime.fromtimestamp(float(self.node["addedtime"]))
-
- @property
- def author(self):
- return User(self.node["writer"])
-
- def report(self, raison="Sans raison"):
- conn = http.client.HTTPConnection(CONF.getNode("server")["url"], timeout=10)
- try:
- conn.request("GET", "report.php?id=" + hashlib.md5(self.id.encode()).hexdigest() + "&raison=" + quote(raison))
- except socket.gaierror:
- print ("[%s] impossible de récupérer la page %s."%(s, p))
- return False
- res = conn.getresponse()
- conn.close()
- return (res.status == http.client.OK)
-
- @property
- def tupleInfo(self):
- return (self.author.username, self.validator.username, self.addedtime)
-
- @property
- def bestAnswer(self):
- best = self.answers[0]
- for answer in self.answers:
- if best.getInt("score") < answer.getInt("score"):
- best = answer
- return best["answer"]
-
- def isCorrect(self, msg):
- msg = msg.lower().replace(" ", "")
- for answer in self.answers:
- if msg == answer["answer"].lower().replace(" ", ""):
- return True
- return False
-
- def getScore(self, msg):
- msg = msg.lower().replace(" ", "")
- for answer in self.answers:
- if msg == answer["answer"].lower().replace(" ", ""):
- return answer.getInt("score")
- return 0
diff --git a/modules/qcm/QuestionFile.py b/modules/qcm/QuestionFile.py
deleted file mode 100644
index 48ed23f..0000000
--- a/modules/qcm/QuestionFile.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# coding=utf-8
-
-import module_states_file as xmlparser
-
-from .Question import Question
-
-class QuestionFile:
- def __init__(self, filename):
- self.questions = xmlparser.parse_file(filename)
- self.questions.setIndex("xml:id")
-
- def getQuestion(self, ident):
- if ident in self.questions.index:
- return Question(self.questions.index[ident])
- else:
- return None
diff --git a/modules/qcm/Session.py b/modules/qcm/Session.py
deleted file mode 100644
index 11ab46b..0000000
--- a/modules/qcm/Session.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# coding=utf-8
-
-import threading
-
-SESSIONS = dict()
-
-from . import Question
-
-from response import Response
-
-class Session:
- def __init__(self, srv, chan, sender):
- self.questions = list()
- self.current = -1
- self.score = 0
- self.good = 0
- self.bad = 0
- self.trys = 0
- self.timer = None
- self.server = srv
- self.channel = chan
- self.sender = sender
-
- def addQuestion(self, ident):
- if ident not in self.questions:
- self.questions.append(ident)
- return True
- return False
-
- def next_question(self):
- self.trys = 0
- self.current += 1
- return self.question
-
- @property
- def question(self):
- if self.current >= 0 and self.current < len(self.questions):
- return Question.Question(Question.QUESTIONS.index[self.questions[self.current]])
- else:
- return None
-
- def askNext(self, bfr = ""):
- global SESSIONS
- self.timer = None
- nextQ = self.next_question()
- if nextQ is not None:
- if self.sender.split("!")[0] != self.channel:
- self.server.send_response(Response(self.sender, "%s%s" % (bfr, nextQ.question), self.channel, nick=self.sender.split("!")[0]))
- else:
- self.server.send_response(Response(self.sender, "%s%s" % (bfr, nextQ.question), self.channel))
- else:
- if self.good > 1:
- goodS = "s"
- else:
- goodS = ""
-
- if self.sender.split("!")[0] != self.channel:
- self.server.send_response(Response(self.sender, "%sFini, tu as donné %d bonne%s réponse%s sur %d questions." % (self.sender, bfr, self.good, goodS, goodS, len(self.questions)), self.channel, nick=self.sender.split("!")[0]))
- else:
- self.server.send_response(Response(self.sender, "%sFini, tu as donné %d bonne%s réponse%s sur %d questions." % (self.sender, bfr, self.good, goodS, goodS, len(self.questions)), self.channel))
- del SESSIONS[self.sender]
-
- def prepareNext(self, lag = 3):
- if self.timer is None:
- self.timer = threading.Timer(lag, self.askNext)
- self.timer.start()
-
diff --git a/modules/qcm/User.py b/modules/qcm/User.py
deleted file mode 100644
index 5f18831..0000000
--- a/modules/qcm/User.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# coding=utf-8
-
-USERS = None
-
-class User:
- def __init__(self, iden):
- global USERS
- if iden in USERS.index:
- self.node = USERS.index[iden]
- else:
- self.node = { "username":"N/A", "email":"N/A" }
-
- @property
- def id(self):
- return self.node["xml:id"]
-
- @property
- def username(self):
- return self.node["username"]
-
- @property
- def email(self):
- return self.node["email"]
-
- @property
- def validated(self):
- return int(self.node["validated"]) > 0
diff --git a/modules/qcm/__init__.py b/modules/qcm/__init__.py
deleted file mode 100644
index b8b01df..0000000
--- a/modules/qcm/__init__.py
+++ /dev/null
@@ -1,197 +0,0 @@
-# coding=utf-8
-
-from datetime import datetime
-import http.client
-import re
-import random
-import sys
-import time
-
-import xmlparser
-
-nemubotversion = 3.2
-
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "MCQ module, working with http://bot.nemunai.re/"
-
-def help_full ():
- return "!qcm [] []"
-
-from . import Question
-from . import Course
-from . import Session
-
-def load(context):
- CONF.setIndex("name", "file")
-
-def buildSession(msg, categ = None, nbQuest = 10, channel = False):
- if Question.QUESTIONS is None:
- Question.QUESTIONS = xmlparser.parse_file(CONF.index["main"]["url"])
- Question.QUESTIONS.setIndex("xml:id")
- Course.COURSES = xmlparser.parse_file(CONF.index["courses"]["url"])
- Course.COURSES.setIndex("xml:id")
- User.USERS = xmlparser.parse_file(CONF.index["users"]["url"])
- User.USERS.setIndex("xml:id")
- #Remove no validated questions
- keys = list()
- for k in Question.QUESTIONS.index.keys():
- keys.append(k)
- for ques in keys:
- if Question.QUESTIONS.index[ques]["validated"] != "1" or Question.QUESTIONS.index[ques]["reported"] == "1":
- del Question.QUESTIONS.index[ques]
-
- #Apply filter
- QS = list()
- if categ is not None and len(categ) > 0:
- #Find course id corresponding to categ
- courses = list()
- for c in Course.COURSES.childs:
- if c["code"] in categ:
- courses.append(c["xml:id"])
-
- #Keep only questions matching course or branch
- for q in Question.QUESTIONS.index.keys():
- if (Question.QUESTIONS.index[q]["branch"] is not None and Question.QUESTIONS.index[q]["branch"].find(categ)) or Question.QUESTIONS.index[q]["course"] in courses:
- QS.append(q)
- else:
- for q in Question.QUESTIONS.index.keys():
- QS.append(q)
-
- nbQuest = min(nbQuest, len(QS))
-
- if channel:
- sess = Session.Session(msg.srv, msg.channel, msg.channel)
- else:
- sess = Session.Session(msg.srv, msg.channel, msg.sender)
- maxQuest = len(QS) - 1
- for i in range(0, nbQuest):
- while True:
- q = QS[random.randint(0, maxQuest)]
- if sess.addQuestion(q):
- break
- if channel:
- Session.SESSIONS[msg.channel] = sess
- else:
- Session.SESSIONS[msg.realname] = sess
-
-
-def askQuestion(msg, bfr = ""):
- return Session.SESSIONS[msg.realname].askNext(bfr)
-
-def parseanswer(msg):
- global DATAS
- if msg.cmd[0] == "qcm" or msg.cmd[0] == "qcmchan" or msg.cmd[0] == "simulateqcm":
- if msg.realname in Session.SESSIONS:
- if len(msg.cmd) > 1:
- if msg.cmd[1] == "stop" or msg.cmd[1] == "end":
- sess = Session.SESSIONS[msg.realname]
- if sess.good > 1: goodS = "s"
- else: goodS = ""
- del Session.SESSIONS[msg.realname]
- return Response(msg.sender,
- "Fini, tu as donné %d bonne%s réponse%s sur %d questions." % (sess.good, goodS, goodS, sess.current),
- msg.channel, nick=msg.nick)
- elif msg.cmd[1] == "next" or msg.cmd[1] == "suivant" or msg.cmd[1] == "suivante":
- return askQuestion(msg)
- return Response(msg.sender, "tu as déjà une session de QCM en cours, finis-la avant d'en commencer une nouvelle.", msg.channel, msg.nick)
- elif msg.channel in Session.SESSIONS:
- if len(msg.cmd) > 1:
- if msg.cmd[1] == "stop" or msg.cmd[1] == "end":
- sess = Session.SESSIONS[msg.channel]
- if sess.good > 1: goodS = "s"
- else: goodS = ""
- del Session.SESSIONS[msg.channel]
- return Response(msg.sender, "Fini, vous avez donné %d bonne%s réponse%s sur %d questions." % (sess.good, goodS, goodS, sess.current), msg.channel)
- elif msg.cmd[1] == "next" or msg.cmd[1] == "suivant" or msg.cmd[1] == "suivante":
- Session.SESSIONS[msg.channel].prepareNext(1)
- return True
- else:
- nbQuest = 10
- filtre = list()
- if len(msg.cmd) > 1:
- for cmd in msg.cmd[1:]:
- try:
- tmp = int(cmd)
- nbQuest = tmp
- except ValueError:
- filtre.append(cmd.upper())
- if len(filtre) == 0:
- filtre = None
- if msg.channel in Session.SESSIONS:
- return Response(msg.sender, "Il y a deja une session de QCM sur ce chan.")
- else:
- buildSession(msg, filtre, nbQuest, msg.cmd[0] == "qcmchan")
- if msg.cmd[0] == "qcm":
- return askQuestion(msg)
- elif msg.cmd[0] == "qcmchan":
- return Session.SESSIONS[msg.channel].askNext()
- else:
- del Session.SESSIONS[msg.realname]
- return Response(msg.sender, "QCM de %d questions" % len(Session.SESSIONS[msg.realname].questions), msg.channel)
- return True
- elif msg.realname in Session.SESSIONS:
- if msg.cmd[0] == "info" or msg.cmd[0] == "infoquestion":
- return Response(msg.sender, "Cette question a été écrite par %s et validée par %s, le %s" % Session.SESSIONS[msg.realname].question.tupleInfo, msg.channel)
- elif msg.cmd[0] == "report" or msg.cmd[0] == "reportquestion":
- if len(msg.cmd) == 1:
- return Response(msg.sender, "Veuillez indiquer une raison de report", msg.channel)
- elif Session.SESSIONS[msg.realname].question.report(' '.join(msg.cmd[1:])):
- return Response(msg.sender, "Cette question vient d'être signalée.", msg.channel)
- Session.SESSIONS[msg.realname].askNext()
- else:
- return Response(msg.sender, "Une erreur s'est produite lors du signalement de la question, veuillez recommencer plus tard.", msg.channel)
- elif msg.channel in Session.SESSIONS:
- if msg.cmd[0] == "info" or msg.cmd[0] == "infoquestion":
- return Response(msg.sender, "Cette question a été écrite par %s et validée par %s, le %s" % Session.SESSIONS[msg.channel].question.tupleInfo, msg.channel)
- elif msg.cmd[0] == "report" or msg.cmd[0] == "reportquestion":
- if len(msg.cmd) == 1:
- return Response(msg.sender, "Veuillez indiquer une raison de report", msg.channel)
- elif Session.SESSIONS[msg.channel].question.report(' '.join(msg.cmd[1:])):
- Session.SESSIONS[msg.channel].prepareNext()
- return Response(msg.sender, "Cette question vient d'être signalée.", msg.channel)
- else:
- return Response(msg.sender, "Une erreur s'est produite lors du signalement de la question, veuillez recommencer plus tard.", msg.channel)
- else:
- if msg.cmd[0] == "listecours":
- if Course.COURSES is None:
- return Response(msg.sender, "La liste de cours n'est pas encore construite, lancez un QCM pour la construire.", msg.channel)
- else:
- res = Response(msg.sender, channel=msg.channel, title="Liste des cours existants : ")
- res.append_message([cours["code"] + " (" + cours["name"] + ")" for cours in Course.COURSES.getNodes("course")])
- return res
- elif msg.cmd[0] == "refreshqcm":
- Question.QUESTIONS = None
- Course.COURSES = None
- User.USERS = None
- return True
- return False
-
-def parseask(msg):
- if msg.realname in Session.SESSIONS:
- dest = msg.realname
-
- if Session.SESSIONS[dest].question.isCorrect(msg.content):
- Session.SESSIONS[dest].good += 1
- Session.SESSIONS[dest].score += Session.SESSIONS[dest].question.getScore(msg.content)
- return askQuestion(msg, "correct ; ")
- else:
- Session.SESSIONS[dest].bad += 1
- if Session.SESSIONS[dest].trys == 0:
- Session.SESSIONS[dest].trys = 1
- return Response(msg.sender, "non, essaie encore :p", msg.channel, msg.nick)
- else:
- return askQuestion(msg, "non, la bonne reponse était : %s ; " % Session.SESSIONS[dest].question.bestAnswer)
-
- elif msg.channel in Session.SESSIONS:
- dest = msg.channel
-
- if Session.SESSIONS[dest].question.isCorrect(msg.content):
- Session.SESSIONS[dest].good += 1
- Session.SESSIONS[dest].score += Session.SESSIONS[dest].question.getScore(msg.content)
- Session.SESSIONS[dest].prepareNext()
- return Response(msg.sender, "correct :)", msg.channel, nick=msg.nick)
- else:
- Session.SESSIONS[dest].bad += 1
- return Response(msg.sender, "non, essaie encore :p", msg.channel, nick=msg.nick)
- return False
diff --git a/modules/qd/DelayedTuple.py b/modules/qd/DelayedTuple.py
deleted file mode 100644
index a81ac5d..0000000
--- a/modules/qd/DelayedTuple.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# coding=utf-8
-
-import re
-import threading
-
-class DelayedTuple:
- def __init__(self, regexp, great):
- self.delayEvnt = threading.Event()
- self.msg = None
- self.regexp = regexp
- self.great = great
-
- def triche(self, res):
- if res is not None:
- return re.match(".*" + self.regexp + ".*", res.lower() + " ") is None
- else:
- return True
-
- def perfect(self, res):
- if res is not None:
- return re.match(".*" + self.great + ".*", res.lower() + " ") is not None
- else:
- return False
-
- def good(self, res):
- if res is not None:
- return re.match(".*" + self.regexp + ".*", res.lower() + " ") is not None
- else:
- return False
-
- def wait(self, timeout):
- self.delayEvnt.wait(timeout)
diff --git a/modules/qd/GameUpdater.py b/modules/qd/GameUpdater.py
deleted file mode 100644
index 7449489..0000000
--- a/modules/qd/GameUpdater.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# coding=utf-8
-
-from datetime import datetime
-import random
-import threading
-from .DelayedTuple import DelayedTuple
-
-DELAYED = dict()
-
-LASTQUESTION = 99999
-
-class GameUpdater(threading.Thread):
- def __init__(self, msg, bfrseen):
- self.msg = msg
- self.bfrseen = bfrseen
- threading.Thread.__init__(self)
-
- def run(self):
- global DELAYED, LASTQUESTION
-
- if self.bfrseen is not None:
- seen = datetime.now() - self.bfrseen
- rnd = random.randint(0, int(seen.seconds/90))
- else:
- rnd = 1
-
- if rnd != 0:
- QUESTIONS = CONF.getNodes("question")
-
- if self.msg.channel == "#nemutest":
- quest = 9
- else:
- if LASTQUESTION >= len(QUESTIONS):
- print (QUESTIONS)
- random.shuffle(QUESTIONS)
- LASTQUESTION = 0
- quest = LASTQUESTION
- LASTQUESTION += 1
-
- question = QUESTIONS[quest]["question"]
- regexp = QUESTIONS[quest]["regexp"]
- great = QUESTIONS[quest]["great"]
- self.msg.send_chn("%s: %s" % (self.msg.nick, question))
-
- DELAYED[self.msg.nick] = DelayedTuple(regexp, great)
-
- DELAYED[self.msg.nick].wait(20)
-
- if DELAYED[self.msg.nick].triche(DELAYED[self.msg.nick].msg):
- getUser(self.msg.nick).playTriche()
- self.msg.send_chn("%s: Tricheur !" % self.msg.nick)
- elif DELAYED[self.msg.nick].perfect(DELAYED[self.msg.nick].msg):
- if random.randint(0, 10) == 1:
- getUser(self.msg.nick).bonusQuestion()
- self.msg.send_chn("%s: Correct !" % self.msg.nick)
- else:
- self.msg.send_chn("%s: J'accepte" % self.msg.nick)
- del DELAYED[self.msg.nick]
- SCORES.save(self.msg.nick)
- save()
diff --git a/modules/qd/QDWrapper.py b/modules/qd/QDWrapper.py
deleted file mode 100644
index 41b2eff..0000000
--- a/modules/qd/QDWrapper.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# coding=utf-8
-
-from tools.wrapper import Wrapper
-from .Score import Score
-
-class QDWrapper(Wrapper):
- def __init__(self, datas):
- Wrapper.__init__(self)
- self.DATAS = datas
- self.stateName = "player"
- self.attName = "name"
-
- def __getitem__(self, i):
- if i in self.cache:
- return self.cache[i]
- else:
- sc = Score()
- sc.parse(Wrapper.__getitem__(self, i))
- self.cache[i] = sc
- return sc
diff --git a/modules/qd/Score.py b/modules/qd/Score.py
deleted file mode 100644
index 52c5692..0000000
--- a/modules/qd/Score.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# coding=utf-8
-
-from datetime import datetime
-
-class Score:
- """Manage the user's scores"""
- def __init__(self):
- #FourtyTwo
- self.ftt = 0
- #TwentyThree
- self.twt = 0
- self.pi = 0
- self.notfound = 0
- self.tententen = 0
- self.leet = 0
- self.great = 0
- self.bad = 0
- self.triche = 0
- self.last = None
- self.changed = False
-
- def parse(self, item):
- self.ftt = item.getInt("fourtytwo")
- self.twt = item.getInt("twentythree")
- self.pi = item.getInt("pi")
- self.notfound = item.getInt("notfound")
- self.tententen = item.getInt("tententen")
- self.leet = item.getInt("leet")
- self.great = item.getInt("great")
- self.bad = item.getInt("bad")
- self.triche = item.getInt("triche")
-
- def save(self, state):
- state.setAttribute("fourtytwo", self.ftt)
- state.setAttribute("twentythree", self.twt)
- state.setAttribute("pi", self.pi)
- state.setAttribute("notfound", self.notfound)
- state.setAttribute("tententen", self.tententen)
- state.setAttribute("leet", self.leet)
- state.setAttribute("great", self.great)
- state.setAttribute("bad", self.bad)
- state.setAttribute("triche", self.triche)
-
- def merge(self, other):
- self.ftt += other.ftt
- self.twt += other.twt
- self.pi += other.pi
- self.notfound += other.notfound
- self.tententen += other.tententen
- self.leet += other.leet
- self.great += other.great
- self.bad += other.bad
- self.triche += other.triche
-
- def newWinner(self):
- self.ftt = 0
- self.twt = 0
- self.pi = 1
- self.notfound = 1
- self.tententen = 0
- self.leet = 1
- self.great = -1
- self.bad = -4
- self.triche = 0
-
- def isWinner(self):
- return self.great >= 42
-
- def playFtt(self):
- if self.canPlay():
- self.ftt += 1
- def playTwt(self):
- if self.canPlay():
- self.twt += 1
- def playSuite(self):
- self.canPlay()
- self.twt += 1
- self.great += 1
- def playPi(self):
- if self.canPlay():
- self.pi += 1
- def playNotfound(self):
- if self.canPlay():
- self.notfound += 1
- def playTen(self):
- if self.canPlay():
- self.tententen += 1
- def playLeet(self):
- if self.canPlay():
- self.leet += 1
- def playGreat(self):
- if self.canPlay():
- self.great += 1
- def playBad(self):
- if self.canPlay():
- self.bad += 1
- self.great += 1
- def playTriche(self):
- self.triche += 1
- def oupsTriche(self):
- self.triche -= 1
- def bonusQuestion(self):
- return
-
- def toTuple(self):
- return (self.ftt, self.twt, self.pi, self.notfound, self.tententen, self.leet, self.great, self.bad, self.triche)
-
- def canPlay(self):
- now = datetime.now()
- ret = self.last == None or self.last.minute != now.minute or self.last.hour != now.hour or self.last.day != now.day
- self.changed = self.changed or ret
- return ret
-
- def hasChanged(self):
- if self.changed:
- self.changed = False
- self.last = datetime.now()
- return True
- else:
- return False
-
- def score(self):
- return (self.ftt * 2 + self.great * 5 + self.leet * 13.37 + (self.pi + 1) * 3.1415 * (self.notfound + 1) + self.tententen * 10 + self.twt - (self.bad + 1) * 10 * (self.triche * 5 + 1) + 7)
-
- def details(self):
- return "42: %d, 23: %d, leet: %d, pi: %d, 404: %d, 10: %d, great: %d, bad: %d, triche: %d = %d."%(self.ftt, self.twt, self.leet, self.pi, self.notfound, self.tententen, self.great, self.bad, self.triche, self.score())
diff --git a/modules/qd/__init__.py b/modules/qd/__init__.py
deleted file mode 100644
index 871512b..0000000
--- a/modules/qd/__init__.py
+++ /dev/null
@@ -1,224 +0,0 @@
-# coding=utf-8
-
-import re
-import imp
-from datetime import datetime
-
-nemubotversion = 3.0
-
-from . import GameUpdater
-from . import QDWrapper
-from . import Score
-
-channels = "#nemutest #42sh #ykar #epitagueule"
-LASTSEEN = dict ()
-temps = dict ()
-
-SCORES = None
-
-def load(context):
- global DATAS, SCORES, CONF
- DATAS.setIndex("name", "player")
- SCORES = QDWrapper.QDWrapper(DATAS)
- GameUpdater.SCORES = SCORES
- GameUpdater.CONF = CONF
- GameUpdater.save = save
- GameUpdater.getUser = getUser
-
-def reload():
- imp.reload(GameUpdater)
- imp.reload(QDWrapper)
- imp.reload(Score)
-
-
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "42 game!"
-
-def help_full ():
- return "!42: display scores\n!42 help: display the performed calculate\n!42 manche: display information about current round\n!42 /who/: show the /who/'s scores"
-
-
-def parseanswer (msg):
- if msg.cmd[0] == "42" or msg.cmd[0] == "score" or msg.cmd[0] == "scores":
- global SCORES
- if len(msg.cmd) > 2 and msg.is_owner and ((msg.cmd[1] == "merge" and len(msg.cmd) > 3) or msg.cmd[1] == "oupstriche"):
- if msg.cmd[2] in SCORES and (len(msg.cmd) <= 3 or msg.cmd[3] in SCORES):
- if msg.cmd[1] == "merge":
- SCORES[msg.cmd[2]].merge (SCORES[msg.cmd[3]])
- del SCORES[msg.cmd[3]]
- msg.send_chn ("%s a été correctement fusionné avec %s."%(msg.cmd[3], msg.cmd[2]))
- elif msg.cmd[1] == "oupstriche":
- SCORES[msg.cmd[2]].oupsTriche()
- else:
- if msg.cmd[2] not in SCORES:
- msg.send_chn ("%s n'est pas un joueur connu."%msg.cmd[2])
- elif msg.cmd[3] not in SCORES:
- msg.send_chn ("%s n'est pas un joueur connu."%msg.cmd[3])
- elif len(msg.cmd) > 1 and (msg.cmd[1] == "help" or msg.cmd[1] == "aide"):
- msg.send_chn ("Formule : \"42\" * 2 + great * 5 + leet * 13.37 + (pi + 1) * 3.1415 * (not_found + 1) + tententen * 10 + \"23\" - (bad + 1) * 10 * (triche * 5 + 1) + 7")
- elif len(msg.cmd) > 1 and (msg.cmd[1] == "manche" or msg.cmd[1] == "round"):
- manche = DATAS.getNode("manche")
- msg.send_chn ("Nous sommes dans la %de manche, gagnée par %s avec %d points et commencée par %s le %s." % (manche.getInt("number"), manche["winner"], manche.getInt("winner_score"), manche["who"], manche.getDate("date")))
- #elif msg.channel == "#nemutest":
- else:
- phrase = ""
-
- if len(msg.cmd) > 1:
- if msg.cmd[1] in SCORES:
- phrase += " " + msg.cmd[1] + ": " + SCORES[msg.cmd[1]].details()
- else:
- phrase = " %s n'a encore jamais joué,"%(msg.cmd[1])
- else:
- for nom, scr in sorted(SCORES.items(), key=rev, reverse=True):
- score = scr.score()
- if score != 0:
- if phrase == "":
- phrase = " *%s.%s: %d*,"%(nom[0:1], nom[1:len(nom)], score)
- else:
- phrase += " %s.%s: %d,"%(nom[0:1], nom[1:len(nom)], score)
-
- msg.send_chn ("Scores :%s" % (phrase[0:len(phrase)-1]))
- return True
- else:
- return False
-
-
-def win(msg):
- global SCORES
- who = msg.nick
-
- manche = DATAS.getNode("manche")
-
- maxi_scor = 0
- maxi_name = None
-
- for player in DATAS.index.keys():
- scr = SCORES[player].score()
- if scr > maxi_scor:
- maxi_scor = scr
- maxi_name = player
-
- for player in DATAS.index.keys():
- scr = SCORES[player].score()
- if scr > maxi_scor / 3:
- del SCORES[player]
- else:
- DATAS.index[player]["great"] = 0
- SCORES.flush()
-
- if who != maxi_name:
- msg.send_chn ("Félicitations %s, tu remportes cette manche terminée par %s, avec un score de %d !"%(maxi_name, who, maxi_scor))
- else:
- msg.send_chn ("Félicitations %s, tu remportes cette manche avec %d points !"%(maxi_name, maxi_scor))
-
- manche.setAttribute("number", manche.getInt("number") + 1)
- manche.setAttribute("winner", maxi_name)
- manche.setAttribute("winner_score", maxi_scor)
- manche.setAttribute("who", who)
- manche.setAttribute("date", datetime.now())
-
- print ("Nouvelle manche !")
- save()
-
-
-def parseask (msg):
- if len(GameUpdater.DELAYED) > 0:
- if msg.nick in GameUpdater.DELAYED:
- GameUpdater.DELAYED[msg.nick].msg = msg.content
- GameUpdater.DELAYED[msg.nick].delayEvnt.set()
- return True
- return False
-
-
-
-def rev (tupl):
- (k, v) = tupl
- return (v.score(), k)
-
-
-def getUser(name):
- global SCORES
- if name not in SCORES:
- SCORES[name] = Score.Score()
- return SCORES[name]
-
-
-def parselisten (msg):
- if len(GameUpdater.DELAYED) > 0 and msg.nick in GameUpdater.DELAYED and GameUpdater.DELAYED[msg.nick].good(msg.content):
- msg.send_chn("%s: n'oublie pas le nemubot: devant ta réponse pour qu'elle soit prise en compte !" % msg.nick)
-
- bfrseen = None
- if msg.realname in LASTSEEN:
- bfrseen = LASTSEEN[msg.realname]
- LASTSEEN[msg.realname] = datetime.now()
-
-# if msg.channel == "#nemutest" and msg.nick not in GameUpdater.DELAYED:
- if msg.channel != "#nemutest" and msg.nick not in GameUpdater.DELAYED:
-
- if re.match("^(42|quarante[- ]?deux).{,2}$", msg.content.strip().lower()):
- if msg.time.minute == 10 and msg.time.second == 10 and msg.time.hour == 10:
- getUser(msg.nick).playTen()
- getUser(msg.nick).playGreat()
- elif msg.time.minute == 42:
- if msg.time.second == 0:
- getUser(msg.nick).playGreat()
- getUser(msg.nick).playFtt()
- else:
- getUser(msg.nick).playBad()
-
- if re.match("^(23|vingt[ -]?trois).{,2}$", msg.content.strip().lower()):
- if msg.time.minute == 23:
- if msg.time.second == 0:
- getUser(msg.nick).playGreat()
- getUser(msg.nick).playTwt()
- else:
- getUser(msg.nick).playBad()
-
- if re.match("^(10){3}.{,2}$", msg.content.strip().lower()):
- if msg.time.minute == 10 and msg.time.hour == 10:
- if msg.time.second == 10:
- getUser(msg.nick).playGreat()
- getUser(msg.nick).playTen()
- else:
- getUser(msg.nick).playBad()
-
- if re.match("^0?12345.{,2}$", msg.content.strip().lower()):
- if msg.time.hour == 1 and msg.time.minute == 23 and (msg.time.second == 45 or (msg.time.second == 46 and msg.time.microsecond < 330000)):
- getUser(msg.nick).playSuite()
- else:
- getUser(msg.nick).playBad()
-
- if re.match("^[1l][e3]{2}[t7] ?t?ime.{,2}$", msg.content.strip().lower()):
- if msg.time.hour == 13 and msg.time.minute == 37:
- if msg.time.second == 0:
- getUser(msg.nick).playGreat()
- getUser(msg.nick).playLeet()
- else:
- getUser(msg.nick).playBad()
-
- if re.match("^(pi|3.14) ?time.{,2}$", msg.content.strip().lower()):
- if msg.time.hour == 3 and msg.time.minute == 14:
- if msg.time.second == 15 or msg.time.second == 16:
- getUser(msg.nick).playGreat()
- getUser(msg.nick).playPi()
- else:
- getUser(msg.nick).playBad()
-
- if re.match("^(404( ?time)?|time ?not ?found).{,2}$", msg.content.strip().lower()):
- if msg.time.hour == 4 and msg.time.minute == 4:
- if msg.time.second == 0 or msg.time.second == 4:
- getUser(msg.nick).playGreat()
- getUser(msg.nick).playNotfound()
- else:
- getUser(msg.nick).playBad()
-
- if getUser(msg.nick).isWinner():
- print ("Nous avons un vainqueur ! Nouvelle manche :p")
- win(msg)
- return True
- elif getUser(msg.nick).hasChanged():
- gu = GameUpdater.GameUpdater(msg, bfrseen)
- gu.start()
- return True
- return False
diff --git a/modules/ratp.py b/modules/ratp.py
new file mode 100644
index 0000000..06f5f1d
--- /dev/null
+++ b/modules/ratp.py
@@ -0,0 +1,74 @@
+"""Informe les usagers des prochains passages des transports en communs de la RATP"""
+
+# PYTHON STUFFS #######################################################
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.module.more import Response
+
+from nextstop import ratp
+
+@hook.command("ratp",
+ help="Affiche les prochains horaires de passage",
+ help_usage={
+ "TRANSPORT": "Affiche les lignes du moyen de transport donné",
+ "TRANSPORT LINE": "Affiche les stations sur la ligne de transport donnée",
+ "TRANSPORT LINE STATION": "Affiche les prochains horaires de passage à l'arrêt donné",
+ "TRANSPORT LINE STATION DESTINATION": "Affiche les prochains horaires de passage dans la direction donnée",
+ })
+def ask_ratp(msg):
+ l = len(msg.args)
+
+ transport = msg.args[0] if l > 0 else None
+ line = msg.args[1] if l > 1 else None
+ station = msg.args[2] if l > 2 else None
+ direction = msg.args[3] if l > 3 else None
+
+ if station is not None:
+ times = sorted(ratp.getNextStopsAtStation(transport, line, station, direction), key=lambda i: i[0])
+
+ if len(times) == 0:
+ raise IMException("la station %s n'existe pas sur le %s ligne %s." % (station, transport, line))
+
+ (time, direction, stationname) = times[0]
+ return Response(message=["\x03\x02%s\x03\x02 direction %s" % (time, direction) for time, direction, stationname in times],
+ title="Prochains passages du %s ligne %s à l'arrêt %s" % (transport, line, stationname),
+ channel=msg.channel)
+
+ elif line is not None:
+ stations = ratp.getAllStations(transport, line)
+
+ if len(stations) == 0:
+ raise IMException("aucune station trouvée.")
+ return Response(stations, title="Stations", channel=msg.channel)
+
+ elif transport is not None:
+ lines = ratp.getTransportLines(transport)
+ if len(lines) == 0:
+ raise IMException("aucune ligne trouvée.")
+ return Response(lines, title="Lignes", channel=msg.channel)
+
+ else:
+ raise IMException("précise au moins un moyen de transport.")
+
+
+@hook.command("ratp_alert",
+ help="Affiche les perturbations en cours sur le réseau")
+def ratp_alert(msg):
+ if len(msg.args) == 0:
+ raise IMException("précise au moins un moyen de transport.")
+
+ l = len(msg.args)
+ transport = msg.args[0] if l > 0 else None
+ line = msg.args[1] if l > 1 else None
+
+ if line is not None:
+ d = ratp.getDisturbanceFromLine(transport, line)
+ if "date" in d and d["date"] is not None:
+ incidents = "Au {date[date]}, {title}: {message}".format(**d)
+ else:
+ incidents = "{title}: {message}".format(**d)
+ else:
+ incidents = ratp.getDisturbance(None, transport)
+
+ return Response(incidents, channel=msg.channel, nomore="No more incidents", count=" (%d more incidents)")
diff --git a/modules/reddit.py b/modules/reddit.py
new file mode 100644
index 0000000..d4def85
--- /dev/null
+++ b/modules/reddit.py
@@ -0,0 +1,97 @@
+# coding=utf-8
+
+"""Get information about subreddit"""
+
+import re
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
+
+nemubotversion = 3.4
+
+from nemubot.module.more import Response
+
+
+def help_full():
+ return "!subreddit /subreddit/: Display information on the subreddit."
+
+LAST_SUBS = dict()
+
+
+@hook.command("subreddit")
+def cmd_subreddit(msg):
+ global LAST_SUBS
+ if not len(msg.args):
+ if msg.channel in LAST_SUBS and len(LAST_SUBS[msg.channel]) > 0:
+ subs = [LAST_SUBS[msg.channel].pop()]
+ else:
+ raise IMException("Which subreddit? Need inspiration? "
+ "type !horny or !bored")
+ else:
+ subs = msg.args
+
+ all_res = list()
+ for osub in subs:
+ sub = re.match(r"^/?(?:(\w)/)?(\w+)/?$", osub)
+ if sub is not None:
+ if sub.group(1) is not None and sub.group(1) != "":
+ where = sub.group(1)
+ else:
+ where = "r"
+
+ sbr = web.getJSON("https://www.reddit.com/%s/%s/about.json" %
+ (where, sub.group(2)))
+
+ if sbr is None:
+ raise IMException("subreddit not found")
+
+ if "title" in sbr["data"]:
+ res = Response(channel=msg.channel,
+ nomore="No more information")
+ res.append_message(
+ ("[NSFW] " if sbr["data"]["over18"] else "") +
+ sbr["data"]["url"] + " " + sbr["data"]["title"] + ": " +
+ sbr["data"]["public_description" if sbr["data"]["public_description"] != "" else "description"].replace("\n", " ") +
+ " %s subscriber(s)" % sbr["data"]["subscribers"])
+ if sbr["data"]["public_description"] != "":
+ res.append_message(
+ sbr["data"]["description"].replace("\n", " "))
+ all_res.append(res)
+ else:
+ all_res.append(Response("/%s/%s doesn't exist" %
+ (where, sub.group(2)),
+ channel=msg.channel))
+ else:
+ all_res.append(Response("%s is not a valid subreddit" % osub,
+ channel=msg.channel, nick=msg.frm))
+
+ return all_res
+
+
+@hook.message()
+def parselisten(msg):
+ global LAST_SUBS
+
+ if hasattr(msg, "message") and msg.message and type(msg.message) == str:
+ urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.message)
+ for url in urls:
+ for recv in msg.to:
+ if recv not in LAST_SUBS:
+ LAST_SUBS[recv] = list()
+ LAST_SUBS[recv].append(url)
+
+
+@hook.post()
+def parseresponse(msg):
+ global LAST_SUBS
+
+ if hasattr(msg, "text") and msg.text and type(msg.text) == str:
+ urls = re.findall("www.reddit.com(/\w/\w+/?)", msg.text)
+ for url in urls:
+ for recv in msg.to:
+ if recv not in LAST_SUBS:
+ LAST_SUBS[recv] = list()
+ LAST_SUBS[recv].append(url)
+
+ return msg
diff --git a/modules/repology.py b/modules/repology.py
new file mode 100644
index 0000000..8dbc6da
--- /dev/null
+++ b/modules/repology.py
@@ -0,0 +1,94 @@
+# coding=utf-8
+
+"""Repology.org module: the packaging hub"""
+
+import datetime
+import re
+
+from nemubot import context
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
+from nemubot.tools.xmlparser.node import ModuleState
+
+nemubotversion = 4.0
+
+from nemubot.module.more import Response
+
+URL_REPOAPI = "https://repology.org/api/v1/project/%s"
+
+def get_json_project(project):
+ prj = web.getJSON(URL_REPOAPI % (project))
+
+ return prj
+
+
+@hook.command("repology",
+ help="Display version information about a package",
+ help_usage={
+ "PACKAGE_NAME": "Retrieve informations about PACKAGE_NAME",
+ },
+ keywords={
+ "distro=DISTRO": "filter by disto",
+ "status=STATUS[,STATUS...]": "filter by status",
+ })
+def cmd_repology(msg):
+ if len(msg.args) == 0:
+ raise IMException("Please provide at least a package name")
+
+ res = Response(channel=msg.channel, nomore="No more information on package")
+
+ for project in msg.args:
+ prj = get_json_project(project)
+ if len(prj) == 0:
+ raise IMException("Unable to find package " + project)
+
+ pkg_versions = {}
+ pkg_maintainers = {}
+ pkg_licenses = {}
+ summary = None
+
+ for repo in prj:
+ # Apply filters
+ if "distro" in msg.kwargs and repo["repo"].find(msg.kwargs["distro"]) < 0:
+ continue
+ if "status" in msg.kwargs and repo["status"] not in msg.kwargs["status"].split(","):
+ continue
+
+ name = repo["visiblename"] if "visiblename" in repo else repo["name"]
+ status = repo["status"] if "status" in repo else "unknown"
+ if name not in pkg_versions:
+ pkg_versions[name] = {}
+ if status not in pkg_versions[name]:
+ pkg_versions[name][status] = []
+ if repo["version"] not in pkg_versions[name][status]:
+ pkg_versions[name][status].append(repo["version"])
+
+ if "maintainers" in repo:
+ if name not in pkg_maintainers:
+ pkg_maintainers[name] = []
+ for maintainer in repo["maintainers"]:
+ if maintainer not in pkg_maintainers[name]:
+ pkg_maintainers[name].append(maintainer)
+
+ if "licenses" in repo:
+ if name not in pkg_licenses:
+ pkg_licenses[name] = []
+ for lic in repo["licenses"]:
+ if lic not in pkg_licenses[name]:
+ pkg_licenses[name].append(lic)
+
+ if "summary" in repo and summary is None:
+ summary = repo["summary"]
+
+ for pkgname in sorted(pkg_versions.keys()):
+ m = "Package " + pkgname + " (" + summary + ")"
+ if pkgname in pkg_licenses:
+ m += " under " + ", ".join(pkg_licenses[pkgname])
+ m += ": " + " - ".join([status + ": " + ", ".join(pkg_versions[pkgname][status]) for status in ["newest", "devel", "unique", "outdated", "legacy", "rolling", "noscheme", "untrusted", "ignored"] if status in pkg_versions[pkgname]])
+ if "distro" in msg.kwargs and pkgname in pkg_maintainers:
+ m += " - Maintained by " + ", ".join(pkg_maintainers[pkgname])
+
+ res.append_message(m)
+
+ return res
diff --git a/modules/rnd.py b/modules/rnd.py
index 198983c..d1c6fe7 100644
--- a/modules/rnd.py
+++ b/modules/rnd.py
@@ -1,12 +1,54 @@
-# coding=utf-8
+"""Help to make choice"""
+
+# PYTHON STUFFS #######################################################
import random
+import shlex
-nemubotversion = 3.3
+from nemubot import context
+from nemubot.exception import IMException
+from nemubot.hooks import hook
-def load(context):
- from hooks import Hook
- add_hook("cmd_hook", Hook(cmd_choice, "choice"))
+from nemubot.module.more import Response
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("choice")
def cmd_choice(msg):
- return Response(msg.sender, random.choice(msg.cmds[1:]), channel=msg.channel)
+ if not len(msg.args):
+ raise IMException("indicate some terms to pick!")
+
+ return Response(random.choice(msg.args),
+ channel=msg.channel,
+ nick=msg.frm)
+
+
+@hook.command("choicecmd")
+def cmd_choicecmd(msg):
+ if not len(msg.args):
+ raise IMException("indicate some command to pick!")
+
+ choice = shlex.split(random.choice(msg.args))
+
+ return [x for x in context.subtreat(context.subparse(msg, choice))]
+
+
+@hook.command("choiceres")
+def cmd_choiceres(msg):
+ if not len(msg.args):
+ raise IMException("indicate some command to pick a message from!")
+
+ rl = [x for x in context.subtreat(context.subparse(msg, " ".join(msg.args)))]
+ if len(rl) <= 0:
+ return rl
+
+ r = random.choice(rl)
+
+ if isinstance(r, Response):
+ for i in range(len(r.messages) - 1, -1, -1):
+ if isinstance(r.messages[i], list):
+ r.messages = [ random.choice(random.choice(r.messages)) ]
+ elif isinstance(r.messages[i], str):
+ r.messages = [ random.choice(r.messages) ]
+ return r
diff --git a/modules/sap.py b/modules/sap.py
new file mode 100644
index 0000000..0b9017f
--- /dev/null
+++ b/modules/sap.py
@@ -0,0 +1,43 @@
+# coding=utf-8
+
+"""Find information about an SAP transaction codes"""
+
+import urllib.parse
+import urllib.request
+from bs4 import BeautifulSoup
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
+
+nemubotversion = 4.0
+
+from nemubot.module.more import Response
+
+
+def help_full():
+ return "Retrieve SAP transaction codes and details using tcodes or keywords: !tcode "
+
+
+@hook.command("tcode")
+def cmd_tcode(msg):
+ if not len(msg.args):
+ raise IMException("indicate a transaction code or "
+ "a keyword to search!")
+
+ url = ("https://www.tcodesearch.com/tcodes/search?q=%s" %
+ urllib.parse.quote(msg.args[0]))
+
+ page = web.getURLContent(url)
+ soup = BeautifulSoup(page)
+
+ res = Response(channel=msg.channel,
+ nomore="No more transaction code",
+ count=" (%d more tcodes)")
+
+
+ search_res = soup.find("", {'id':'searchresults'})
+ for item in search_res.find_all('dd'):
+ res.append_message(item.get_text().split('\n')[1].strip())
+
+ return res
diff --git a/modules/shodan.py b/modules/shodan.py
new file mode 100644
index 0000000..9c158c6
--- /dev/null
+++ b/modules/shodan.py
@@ -0,0 +1,104 @@
+"""Search engine for IoT"""
+
+# PYTHON STUFFS #######################################################
+
+from datetime import datetime
+import ipaddress
+import urllib.parse
+
+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
+
+
+# GLOBALS #############################################################
+
+BASEURL = "https://api.shodan.io/shodan/"
+
+
+# LOADING #############################################################
+
+def load(context):
+ if not context.config or "apikey" not in context.config:
+ raise ImportError("You need a Shodan API key in order to use this "
+ "module. Add it to the module configuration file:\n"
+ "\nRegister at https://account.shodan.io/register")
+
+
+# MODULE CORE #########################################################
+
+def host_lookup(ip):
+ url = BASEURL + "host/" + urllib.parse.quote(ip) + "?" + urllib.parse.urlencode({'key': context.config["apikey"]})
+ return web.getJSON(url)
+
+
+def search_hosts(query):
+ url = BASEURL + "host/search?" + urllib.parse.urlencode({'query': query, 'key': context.config["apikey"]})
+ return web.getJSON(url, max_size=4194304)
+
+
+def print_ssl(ssl):
+ return (
+ "SSL: " +
+ " ".join([v for v in ssl["versions"] if v[0] != "-"]) +
+ "; cipher used: " + ssl["cipher"]["name"] +
+ ("; certificate: " + ssl["cert"]["sig_alg"] +
+ " issued by: " + ssl["cert"]["issuer"]["CN"] +
+ " expires on: " + str(datetime.strptime(ssl["cert"]["expires"], "%Y%m%d%H%M%SZ")) if "cert" in ssl else "")
+ )
+
+def print_service(svc):
+ ip = ipaddress.ip_address(svc["ip_str"])
+ return ((svc["ip_str"] if ip.version == 4 else "[%s]" % svc["ip_str"]) +
+ ":{port}/{transport} ({module}):" +
+ (" {os}" if svc["os"] else "") +
+ (" {product}" if "product" in svc else "") +
+ (" {version}" if "version" in svc else "") +
+ (" {info}" if "info" in svc else "") +
+ (" Vulns: " + ", ".join(svc["opts"]["vulns"]) if "opts" in svc and "vulns" in svc["opts"] else "") +
+ (" " + print_ssl(svc["ssl"]) if "ssl" in svc else "") +
+ (" \x03\x1D" + svc["data"].replace("\r\n", "\n").split("\n")[0] + "\x03\x1D" if "data" in svc else "") +
+ (" " + svc["title"] if "title" in svc else "")
+ ).format(module=svc["_shodan"]["module"], **svc)
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("shodan",
+ help="Use shodan.io to get information on machines connected to Internet",
+ help_usage={
+ "IP": "retrieve information about the given IP (can be v4 or v6)",
+ "TERM": "retrieve all hosts matching TERM somewhere in their exposed stuff"
+ })
+def shodan(msg):
+ if not msg.args:
+ raise IMException("indicate an IP or a term to search!")
+
+ terms = " ".join(msg.args)
+
+ try:
+ ip = ipaddress.ip_address(terms)
+ except ValueError:
+ ip = None
+
+ if ip:
+ h = host_lookup(terms)
+ res = Response(channel=msg.channel,
+ title="%s" % ((h["ip_str"] if ip.version == 4 else "[%s]" % h["ip_str"]) + (" (" + ", ".join(h["hostnames"]) + ")") if h["hostnames"] else ""))
+ res.append_message("{isp} ({asn}) -> {city} ({country_code}), running {os}. Vulns: {vulns_str}. Open ports: {open_ports}. Last update: {last_update}".format(
+ open_ports=", ".join(map(lambda a: str(a), h["ports"])), vulns_str=", ".join(h["vulns"]) if "vulns" in h else None, **h).strip())
+ for d in h["data"]:
+ res.append_message(print_service(d))
+
+ else:
+ q = search_hosts(terms)
+ res = Response(channel=msg.channel,
+ count=" (%%s/%s results)" % q["total"])
+ for r in q["matches"]:
+ res.append_message(print_service(r))
+
+ return res
diff --git a/modules/sleepytime.py b/modules/sleepytime.py
index b53a2e5..f7fb626 100644
--- a/modules/sleepytime.py
+++ b/modules/sleepytime.py
@@ -1,52 +1,50 @@
# coding=utf-8
+"""as http://sleepyti.me/, give you the best time to go to bed"""
+
import re
import imp
-from datetime import datetime
-from datetime import timedelta
+from datetime import datetime, timedelta, timezone
-nemubotversion = 3.3
+from nemubot.hooks import hook
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "as http://sleepyti.me/, give you the best time to go to bed"
+nemubotversion = 3.4
-def help_full ():
- return "If you would like to sleep soon, use !sleepytime to know the best time to wake up; use !sleepytime hh:mm if you want to wake up at hh:mm"
-
-def load(context):
- from hooks import Hook
- add_hook("cmd_hook", Hook(cmd_sleep, "sleeptime"))
- add_hook("cmd_hook", Hook(cmd_sleep, "sleepytime"))
+from nemubot.module.more import Response
+def help_full():
+ return ("If you would like to sleep soon, use !sleepytime to know the best"
+ " time to wake up; use !sleepytime hh:mm if you want to wake up at"
+ " hh:mm")
+
+
+@hook.command("sleepytime")
def cmd_sleep(msg):
- if len (msg.cmds) > 1 and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?",
- msg.cmds[1]) is not None:
+ if len(msg.args) and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?",
+ msg.args[0]) is not None:
# First, parse the hour
- p = re.match("([0-9]{1,2})[h':.,-]([0-9]{1,2})?[m':.,-]?", msg.cmds[1])
- f = [datetime(datetime.today().year,
- datetime.today().month,
- datetime.today().day,
+ p = re.match("([0-9]{1,2})[h':.,-]([0-9]{1,2})?[m':.,-]?", msg.args[0])
+ f = [datetime(datetime.now(timezone.utc).year,
+ datetime.now(timezone.utc).month,
+ datetime.now(timezone.utc).day,
hour=int(p.group(1)))]
if p.group(2) is not None:
- f[0] += timedelta(minutes=int(p.group(2)))
+ f[0] += timedelta(minutes=int(p.group(2)))
g = list()
- for i in range(0,6):
- f.append(f[i] - timedelta(hours=1,minutes=30))
+ for i in range(6):
+ f.append(f[i] - timedelta(hours=1, minutes=30))
g.append(f[i+1].strftime("%H:%M"))
- return Response(msg.sender,
- "You should try to fall asleep at one of the following"
- " times: %s" % ', '.join(g), msg.channel)
+ return Response("You should try to fall asleep at one of the following"
+ " times: %s" % ', '.join(g), channel=msg.channel)
# Just get awake times
else:
- f = [datetime.now() + timedelta(minutes=15)]
+ f = [datetime.now(timezone.utc) + timedelta(minutes=15)]
g = list()
- for i in range(0,6):
- f.append(f[i] + timedelta(hours=1,minutes=30))
+ for i in range(6):
+ f.append(f[i] + timedelta(hours=1, minutes=30))
g.append(f[i+1].strftime("%H:%M"))
- return Response(msg.sender,
- "If you head to bed right now, you should try to wake"
+ return Response("If you head to bed right now, you should try to wake"
" up at one of the following times: %s" %
- ', '.join(g), msg.channel)
+ ', '.join(g), channel=msg.channel)
diff --git a/modules/smmry.py b/modules/smmry.py
new file mode 100644
index 0000000..b1fe72c
--- /dev/null
+++ b/modules/smmry.py
@@ -0,0 +1,116 @@
+"""Summarize texts"""
+
+# PYTHON STUFFS #######################################################
+
+from urllib.parse import quote
+
+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
+from nemubot.module.urlreducer import LAST_URLS
+
+
+# GLOBALS #############################################################
+
+URL_API = "https://api.smmry.com/?SM_API_KEY=%s"
+
+
+# LOADING #############################################################
+
+def load(context):
+ if not context.config or "apikey" not in context.config:
+ raise ImportError("You need a Smmry API key in order to use this "
+ "module. Add it to the module configuration file:\n"
+ "\nRegister at https://smmry.com/partner")
+ global URL_API
+ URL_API = URL_API % context.config["apikey"]
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("smmry",
+ help="Summarize the following words/command return",
+ help_usage={
+ "WORDS/CMD": ""
+ },
+ keywords={
+ "keywords?=X": "Returns keywords instead of summary (count optional)",
+ "length=7": "The number of sentences returned, default 7",
+ "break": "inserts the string [BREAK] between sentences",
+ "ignore_length": "returns summary regardless of quality or length",
+ "quote_avoid": "sentences with quotations will be excluded",
+ "question_avoid": "sentences with question will be excluded",
+ "exclamation_avoid": "sentences with exclamation marks will be excluded",
+ })
+def cmd_smmry(msg):
+ if not len(msg.args):
+ global LAST_URLS
+ if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
+ msg.args.append(LAST_URLS[msg.channel].pop())
+ else:
+ raise IMException("I have no more URL to sum up.")
+
+ URL = URL_API
+ if "length" in msg.kwargs:
+ if int(msg.kwargs["length"]) > 0 :
+ URL += "&SM_LENGTH=" + msg.kwargs["length"]
+ else:
+ msg.kwargs["ignore_length"] = True
+ if "break" in msg.kwargs: URL += "&SM_WITH_BREAK"
+ if "ignore_length" in msg.kwargs: URL += "&SM_IGNORE_LENGTH"
+ if "quote_avoid" in msg.kwargs: URL += "&SM_QUOTE_AVOID"
+ if "question_avoid" in msg.kwargs: URL += "&SM_QUESTION_AVOID"
+ if "exclamation_avoid" in msg.kwargs: URL += "&SM_EXCLAMATION_AVOID"
+ if "keywords" in msg.kwargs and msg.kwargs["keywords"] is not None and int(msg.kwargs["keywords"]) > 0: URL += "&SM_KEYWORD_COUNT=" + msg.kwargs["keywords"]
+
+ res = Response(channel=msg.channel)
+
+ if web.isURL(" ".join(msg.args)):
+ smmry = web.getJSON(URL + "&SM_URL=" + quote(" ".join(msg.args)), timeout=23)
+ else:
+ cnt = ""
+ for r in context.subtreat(context.subparse(msg, " ".join(msg.args))):
+ 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):
+ cnt += r.messages[i][j] + "\n"
+ elif isinstance(r.messages[i], str):
+ cnt += r.messages[i] + "\n"
+ else:
+ cnt += str(r.messages) + "\n"
+
+ elif isinstance(r, Text):
+ cnt += r.message + "\n"
+
+ else:
+ cnt += str(r) + "\n"
+
+ smmry = web.getJSON(URL, body="sm_api_input=" + quote(cnt), timeout=23)
+
+ if "sm_api_error" in smmry:
+ if smmry["sm_api_error"] == 0:
+ title = "Internal server problem (not your fault)"
+ elif smmry["sm_api_error"] == 1:
+ title = "Incorrect submission variables"
+ elif smmry["sm_api_error"] == 2:
+ title = "Intentional restriction (low credits?)"
+ elif smmry["sm_api_error"] == 3:
+ title = "Summarization error"
+ else:
+ title = "Unknown error"
+ raise IMException(title + ": " + smmry['sm_api_message'].lower())
+
+ if "keywords" in msg.kwargs:
+ smmry["sm_api_content"] = ", ".join(smmry["sm_api_keyword_array"])
+
+ if "sm_api_title" in smmry and smmry["sm_api_title"] != "":
+ res.append_message(smmry["sm_api_content"], title=smmry["sm_api_title"])
+ else:
+ res.append_message(smmry["sm_api_content"])
+
+ return res
diff --git a/modules/sms.py b/modules/sms.py
new file mode 100644
index 0000000..57ab3ae
--- /dev/null
+++ b/modules/sms.py
@@ -0,0 +1,153 @@
+# coding=utf-8
+
+"""Send SMS using SMS API (currently only Free Mobile)"""
+
+import re
+import socket
+import time
+import urllib.error
+import urllib.request
+import urllib.parse
+
+from nemubot import context
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools.xmlparser.node import ModuleState
+
+nemubotversion = 3.4
+
+from nemubot.module.more import Response
+
+def load(context):
+ context.data.setIndex("name", "phone")
+
+def help_full():
+ return "!sms /who/[,/who/[,...]] message: send a SMS to /who/."
+
+def send_sms(frm, api_usr, api_key, content):
+ content = "<%s> %s" % (frm, content)
+
+ try:
+ req = urllib.request.Request("https://smsapi.free-mobile.fr/sendmsg?user=%s&pass=%s&msg=%s" % (api_usr, api_key, urllib.parse.quote(content)))
+ res = urllib.request.urlopen(req, timeout=5)
+ except socket.timeout:
+ return "timeout"
+ except urllib.error.HTTPError as e:
+ if e.code == 400:
+ return "paramètre manquant"
+ elif e.code == 402:
+ return "paiement requis"
+ elif e.code == 403 or e.code == 404:
+ return "clef incorrecte"
+ elif e.code != 200:
+ return "erreur inconnue (%d)" % status
+ except:
+ return "unknown error"
+
+ return None
+
+def check_sms_dests(dests, cur_epoch):
+ """Raise exception if one of the dest is not known or has already receive a SMS recently
+ """
+ for u in dests:
+ if u not in context.data.index:
+ raise IMException("Désolé, je sais pas comment envoyer de SMS à %s." % u)
+ elif cur_epoch - float(context.data.index[u]["lastuse"]) < 42:
+ raise IMException("Un peu de calme, %s a déjà reçu un SMS il n'y a pas si longtemps." % u)
+ return True
+
+
+def send_sms_to_list(msg, frm, dests, content, cur_epoch):
+ fails = list()
+ for u in dests:
+ context.data.index[u]["lastuse"] = cur_epoch
+ test = send_sms(frm, context.data.index[u]["user"], context.data.index[u]["key"], content)
+ if test is not None:
+ fails.append( "%s: %s" % (u, test) )
+
+ if len(fails) > 0:
+ return Response("quelque chose ne s'est pas bien passé durant l'envoi du SMS : " + ", ".join(fails), msg.channel, msg.frm)
+ else:
+ return Response("le SMS a bien été envoyé", msg.channel, msg.frm)
+
+
+@hook.command("sms")
+def cmd_sms(msg):
+ if not len(msg.args):
+ raise IMException("À qui veux-tu envoyer ce SMS ?")
+
+ cur_epoch = time.mktime(time.localtime())
+ dests = msg.args[0].split(",")
+ frm = msg.frm if msg.to_response[0] == msg.frm else msg.frm + "@" + msg.to[0]
+ content = " ".join(msg.args[1:])
+
+ check_sms_dests(dests, cur_epoch)
+ return send_sms_to_list(msg, frm, dests, content, cur_epoch)
+
+
+@hook.command("smscmd")
+def cmd_smscmd(msg):
+ if not len(msg.args):
+ raise IMException("À qui veux-tu envoyer ce SMS ?")
+
+ cur_epoch = time.mktime(time.localtime())
+ dests = msg.args[0].split(",")
+ frm = msg.frm if msg.to_response[0] == msg.frm else msg.frm + "@" + msg.to[0]
+ cmd = " ".join(msg.args[1:])
+
+ content = None
+ for r in context.subtreat(context.subparse(msg, cmd)):
+ if isinstance(r, Response):
+ for m in r.messages:
+ if isinstance(m, list):
+ for n in m:
+ content = n
+ break
+ if content is not None:
+ break
+ elif isinstance(m, str):
+ content = m
+ break
+
+ elif isinstance(r, Text):
+ content = r.message
+
+ if content is None:
+ raise IMException("Aucun SMS envoyé : le résultat de la commande n'a pas retourné de contenu.")
+
+ check_sms_dests(dests, cur_epoch)
+ return send_sms_to_list(msg, frm, dests, content, cur_epoch)
+
+
+apiuser_ask = re.compile(r"(utilisateur|user|numéro|numero|compte|abonne|abone|abonné|account)\s+(est|is)\s+(?P[0-9]{7,})", re.IGNORECASE)
+apikey_ask = re.compile(r"(clef|key|password|mot de passe?)\s+(?:est|is)?\s+(?P[a-zA-Z0-9]{10,})", re.IGNORECASE)
+
+@hook.ask()
+def parseask(msg):
+ if msg.message.find("Free") >= 0 and (
+ msg.message.find("API") >= 0 or msg.message.find("api") >= 0) and (
+ msg.message.find("SMS") >= 0 or msg.message.find("sms") >= 0):
+ resuser = apiuser_ask.search(msg.message)
+ reskey = apikey_ask.search(msg.message)
+ if resuser is not None and reskey is not None:
+ apiuser = resuser.group("user")
+ apikey = reskey.group("key")
+
+ test = send_sms("nemubot", apiuser, apikey,
+ "Vous avez enregistré vos codes d'authentification dans nemubot, félicitation !")
+ if test is not None:
+ return Response("je n'ai pas pu enregistrer tes identifiants : %s" % test, msg.channel, msg.frm)
+
+ if msg.frm in context.data.index:
+ context.data.index[msg.frm]["user"] = apiuser
+ context.data.index[msg.frm]["key"] = apikey
+ else:
+ ms = ModuleState("phone")
+ ms.setAttribute("name", msg.frm)
+ ms.setAttribute("user", apiuser)
+ ms.setAttribute("key", apikey)
+ ms.setAttribute("lastuse", 0)
+ context.data.addChild(ms)
+ context.save()
+ return Response("ok, c'est noté. Je t'ai envoyé un SMS pour tester ;)",
+ msg.channel, msg.frm)
diff --git a/modules/soutenance.xml b/modules/soutenance.xml
deleted file mode 100644
index 957423b..0000000
--- a/modules/soutenance.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/modules/soutenance/Delayed.py b/modules/soutenance/Delayed.py
deleted file mode 100644
index 8cf47c5..0000000
--- a/modules/soutenance/Delayed.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# coding=utf-8
-
-import threading
-
-class Delayed:
- def __init__(self, name):
- self.name = name
- self.res = None
- self.evt = threading.Event()
-
- def wait(self, timeout):
- self.evt.clear()
- self.evt.wait(timeout)
diff --git a/modules/soutenance/SiteSoutenances.py b/modules/soutenance/SiteSoutenances.py
deleted file mode 100644
index 63833b7..0000000
--- a/modules/soutenance/SiteSoutenances.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# coding=utf-8
-
-from datetime import datetime
-from datetime import timedelta
-import http.client
-import re
-import threading
-import time
-
-from response import Response
-
-from .Soutenance import Soutenance
-
-class SiteSoutenances(threading.Thread):
- def __init__(self, datas):
- self.souts = list()
- self.updated = datetime.now()
- self.datas = datas
- threading.Thread.__init__(self)
-
- def getPage(self):
- conn = http.client.HTTPSConnection(CONF.getNode("server")["ip"], timeout=10)
- try:
- conn.request("GET", CONF.getNode("server")["url"])
-
- res = conn.getresponse()
- page = res.read()
- except:
- print ("[%s] impossible de récupérer la page %s."%(s, p))
- return ""
- conn.close()
- return page
-
- def parsePage(self, page):
- save = False
- for line in page.split("\n"):
- if re.match("", line) is not None:
- save = False
- elif re.match("", line) is not None:
- save = True
- last = Soutenance()
- self.souts.append(last)
- elif save:
- result = re.match("]+>(.*) | ", line)
- if last.hour is None:
- try:
- last.hour = datetime.fromtimestamp(time.mktime(time.strptime(result.group(1), "%Y-%m-%d %H:%M")))
- except ValueError:
- continue
- elif last.rank == 0:
- last.rank = int (result.group(1))
- elif last.login == None:
- last.login = result.group(1)
- elif last.state == None:
- last.state = result.group(1)
- elif last.assistant == None:
- last.assistant = result.group(1)
- elif last.start == None:
- try:
- last.start = datetime.fromtimestamp(time.mktime(time.strptime(result.group(1), "%Y-%m-%d %H:%M")))
- except ValueError:
- last.start = None
- elif last.end == None:
- try:
- last.end = datetime.fromtimestamp(time.mktime(time.strptime(result.group(1), "%Y-%m-%d %H:%M")))
- except ValueError:
- last.end = None
-
- def gen_response(self, req, msg):
- """Generate a text response on right server and channel"""
- return Response(req["sender"], msg, req["channel"], server=req["server"])
-
- def res_next(self, req):
- soutenance = self.findLast()
- if soutenance is None:
- return self.gen_response(req, "Il ne semble pas y avoir de soutenance pour le moment.")
- else:
- if soutenance.start > soutenance.hour:
- avre = "%s de *retard*"%msg.just_countdown(soutenance.start - soutenance.hour, 4)
- else:
- avre = "%s *d'avance*"%msg.just_countdown(soutenance.hour - soutenance.start, 4)
- self.gen_response(req, "Actuellement à la soutenance numéro %d, commencée il y a %s avec %s."%(soutenance.rank, msg.just_countdown(datetime.now () - soutenance.start, 4), avre))
-
- def res_assistants(self, req):
- assistants = self.findAssistants()
- if len(assistants) > 0:
- return self.gen_response(req, "Les %d assistants faisant passer les soutenances sont : %s." % (len(assistants), ', '.join(assistants.keys())))
- else:
- return self.gen_response(req, "Il ne semble pas y avoir de soutenance pour le moment.")
-
- def res_soutenance(self, req):
- name = req["user"]
-
- if name == "acu" or name == "yaka" or name == "acus" or name == "yakas" or name == "assistant" or name == "assistants":
- return self.res_assistants(req)
- elif name == "next":
- return self.res_next(req)
-
- soutenance = self.findClose(name)
- if soutenance is None:
- return self.gen_response(req, "Pas d'horaire de soutenance pour %s."%name)
- else:
- if soutenance.state == "En cours":
- return self.gen_response(req, "%s est actuellement en soutenance avec %s. Elle était prévue à %s, position %d."%(name, soutenance.assistant, soutenance.hour, soutenance.rank))
- elif soutenance.state == "Effectue":
- return self.gen_response(req, "%s a passé sa soutenance avec %s. Elle a duré %s."%(name, soutenance.assistant, msg.just_countdown(soutenance.end - soutenance.start, 4)))
- elif soutenance.state == "Retard":
- return self.gen_response(req, "%s était en retard à sa soutenance de %s."%(name, soutenance.hour))
- else:
- last = self.findLast()
- if last is not None:
- if soutenance.hour + (last.start - last.hour) > datetime.now ():
- return self.gen_response(req, "Soutenance de %s : %s, position %d ; estimation du passage : dans %s."%(name, soutenance.hour, soutenance.rank, msg.just_countdown((soutenance.hour - datetime.now ()) + (last.start - last.hour))))
- else:
- return self.gen_response(req, "Soutenance de %s : %s, position %d ; passage imminent."%(name, soutenance.hour, soutenance.rank))
- else:
- return self.gen_response(req, "Soutenance de %s : %s, position %d."%(name, soutenance.hour, soutenance.rank))
-
- def res_list(self, req):
- name = req["user"]
-
- souts = self.findAll(name)
- if souts is None:
- self.gen_response(req, "Pas de soutenance prévues pour %s."%name)
- else:
- first = True
- for s in souts:
- if first:
- self.gen_response(req, "Soutenance(s) de %s : - %s (position %d) ;"%(name, s.hour, s.rank))
- first = False
- else:
- self.gen_response(req, " %s - %s (position %d) ;"%(len(name)*' ', s.hour, s.rank))
-
- def run(self):
- self.parsePage(self.getPage().decode())
- res = list()
- for u in self.datas.getNodes("request"):
- res.append(self.res_soutenance(u))
- return res
-
- def needUpdate(self):
- if self.findLast() is not None and datetime.now () - self.updated > timedelta(minutes=2):
- return True
- elif datetime.now () - self.updated < timedelta(hours=1):
- return False
- else:
- return True
-
- def findAssistants(self):
- h = dict()
- for s in self.souts:
- if s.assistant is not None and s.assistant != "":
- h[s.assistant] = (s.start, s.end)
- return h
-
- def findLast(self):
- close = None
- for s in self.souts:
- if (s.state != "En attente" and s.start is not None and (close is None or close.rank < s.rank or close.hour.day > s.hour.day)) and (close is None or s.hour - close.hour < timedelta(seconds=2499)):
- close = s
- return close
-
- def findAll(self, login):
- ss = list()
- for s in self.souts:
- if s.login == login:
- ss.append(s)
- return ss
-
- def findClose(self, login):
- ss = self.findAll(login)
- close = None
- for s in ss:
- if close is not None:
- print (close.hour)
- print (s.hour)
- if close is None or (close.hour < s.hour and close.hour.day >= datetime.datetime().day):
- close = s
- return close
diff --git a/modules/soutenance/Soutenance.py b/modules/soutenance/Soutenance.py
deleted file mode 100644
index e2a0882..0000000
--- a/modules/soutenance/Soutenance.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# coding=utf-8
-
-class Soutenance:
- def __init__(self):
- self.hour = None
- self.rank = 0
- self.login = None
- self.state = None
- self.assistant = None
- self.start = None
- self.end = None
diff --git a/modules/soutenance/__init__.py b/modules/soutenance/__init__.py
deleted file mode 100644
index 61b3aa6..0000000
--- a/modules/soutenance/__init__.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# coding=utf-8
-
-import time
-import re
-import threading
-from datetime import date
-from datetime import datetime
-
-from . import SiteSoutenances
-
-nemubotversion = 3.3
-
-def help_tiny():
- """Line inserted in the response to the command !help"""
- return "EPITA ING1 defenses module"
-
-def help_full():
- return "!soutenance: gives information about current defenses state\n!soutenance : gives the date of the next defense of /who/.\n!soutenances : gives all defense dates of /who/"
-
-def load(context):
- global CONF
- SiteSoutenances.CONF = CONF
-
-def ask_soutenance(msg):
- req = ModuleState("request")
- if len(msg.cmds) > 1:
- req.setAttribute("user", msg.cmds[1])
- else:
- req.setAttribute("user", "next")
- req.setAttribute("server", msg.server)
- req.setAttribute("channel", msg.channel)
- req.setAttribute("sender", msg.sender)
-
- #An instance of this module is already running?
- if not DATAS.hasAttribute("_running") or DATAS["_running"].needUpdate():
- DATAS.addChild(req)
- site = SiteSoutenances.SiteSoutenances(DATAS)
- DATAS.setAttribute("_running", site)
-
- res = site.run()
-
- for n in DATAS.getNodes("request"):
- DATAS.delChild(n)
-
- return res
- else:
- site = DATAS["_running"]
- return site.res_soutenance(req)
diff --git a/modules/speak.py b/modules/speak.py
new file mode 100644
index 0000000..c08b2bd
--- /dev/null
+++ b/modules/speak.py
@@ -0,0 +1,133 @@
+# coding=utf-8
+
+from datetime import timedelta
+from queue import Queue
+import re
+import subprocess
+from threading import Thread
+
+from nemubot.hooks import hook
+from nemubot.message import Text
+from nemubot.message.visitor import AbstractVisitor
+
+nemubotversion = 3.4
+
+queue = Queue()
+spk_th = None
+last = None
+
+SMILEY = list()
+CORRECTIONS = list()
+
+def load(context):
+ for smiley in context.config.getNodes("smiley"):
+ if smiley.hasAttribute("txt") and smiley.hasAttribute("mood"):
+ SMILEY.append((smiley.getAttribute("txt"), smiley.getAttribute("mood")))
+ print ("%d smileys loaded" % len(SMILEY))
+
+ for correct in context.config.getNodes("correction"):
+ if correct.hasAttribute("bad") and correct.hasAttribute("good"):
+ CORRECTIONS.append((" " + (correct.getAttribute("bad") + " "), (" " + correct.getAttribute("good") + " ")))
+ print ("%d corrections loaded" % len(CORRECTIONS))
+
+
+class Speaker(Thread):
+
+ def run(self):
+ global queue, spk_th
+ while not queue.empty():
+ sentence = queue.get_nowait()
+ lang = "fr"
+ subprocess.call(["espeak", "-v", lang, "--", sentence])
+ queue.task_done()
+
+ spk_th = None
+
+
+class SpeakerVisitor(AbstractVisitor):
+
+ def __init__(self, last):
+ self.pp = ""
+ self.last = last
+
+
+ def visit_Text(self, msg):
+ force = (self.last is None)
+
+ if force or msg.date - self.last.date > timedelta(0, 500):
+ self.pp += "A %d heure %d : " % (msg.date.hour, msg.date.minute)
+ force = True
+
+ if force or msg.channel != self.last.channel:
+ if msg.to_response == msg.to:
+ self.pp += "sur %s. " % (", ".join(msg.to))
+ else:
+ self.pp += "en message priver. "
+
+ action = False
+ if msg.message.find("ACTION ") == 0:
+ self.pp += "%s " % msg.frm
+ msg.message = msg.message.replace("ACTION ", "")
+ action = True
+ for (txt, mood) in SMILEY:
+ if msg.message.find(txt) >= 0:
+ self.pp += "%s %s : " % (msg.frm, mood)
+ msg.message = msg.message.replace(txt, "")
+ action = True
+ break
+
+ if not action and (force or msg.frm != self.last.frm):
+ self.pp += "%s dit : " % msg.frm
+
+ if re.match(".*https?://.*", msg.message) is not None:
+ msg.message = re.sub(r'https?://([^/]+)[^ ]*', " U.R.L \\1", msg.message)
+
+ self.pp += msg.message
+
+
+ def visit_DirectAsk(self, msg):
+ res = Text("%s: %s" % (msg.designated, msg.message),
+ server=msg.server, date=msg.date,
+ to=msg.to, frm=msg.frm)
+ res.accept(self)
+
+
+ def visit_Command(self, msg):
+ res = Text("Bang %s%s%s" % (msg.cmd,
+ " " if len(msg.args) else "",
+ " ".join(msg.args)),
+ server=msg.server, date=msg.date,
+ to=msg.to, frm=msg.frm)
+ res.accept(self)
+
+
+ def visit_OwnerCommand(self, msg):
+ res = Text("Owner Bang %s%s%s" % (msg.cmd,
+ " " if len(msg.args) else "",
+ " ".join(msg.args)),
+ server=msg.server, date=msg.date,
+ to=msg.to, frm=msg.frm)
+ res.accept(self)
+
+
+@hook("in")
+def treat_for_speak(msg):
+ if not msg.frm_owner:
+ append_message(msg)
+
+def append_message(msg):
+ global last, spk_th
+
+ if hasattr(msg, "message") and msg.message.find("TYPING ") == 0:
+ return
+ if last is not None and last.message == msg.message:
+ return
+
+ vprnt = SpeakerVisitor(last)
+ msg.accept(vprnt)
+ queue.put_nowait(vprnt.pp)
+ last = msg
+
+ if spk_th is None:
+ spk_th = Speaker()
+ spk_th.start()
diff --git a/modules/spell/__init__.py b/modules/spell/__init__.py
index 918831b..da16a80 100644
--- a/modules/spell/__init__.py
+++ b/modules/spell/__init__.py
@@ -1,89 +1,97 @@
-# coding=utf-8
+"""Check words spelling"""
-import re
-from urllib.parse import quote
+# PYTHON STUFFS #######################################################
+
+from nemubot import context
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools.xmlparser.node import ModuleState
from .pyaspell import Aspell
from .pyaspell import AspellError
-nemubotversion = 3.3
+from nemubot.module.more import Response
-def help_tiny ():
- return "Check words spelling"
-def help_full ():
- return "!spell [] : give the correct spelling of in ."
+# LOADING #############################################################
def load(context):
- global DATAS
- DATAS.setIndex("name", "score")
-
- from hooks import Hook
- add_hook("cmd_hook", Hook(cmd_spell, "spell"))
- add_hook("cmd_hook", Hook(cmd_spell, "orthographe"))
- add_hook("cmd_hook", Hook(cmd_score, "spellscore"))
+ context.data.setIndex("name", "score")
-def cmd_spell(msg):
- if len(msg.cmds) < 2:
- return Response(msg.sender, "Indiquer une orthographe approximative du mot dont vous voulez vérifier l'orthographe.", msg.channel)
-
- lang = "fr"
- strRes = list()
- for word in msg.cmds[1:]:
- if len(word) <= 2 and len(msg.cmds) > 2:
- lang = word
- else:
- try:
- r = check_spell(word, lang)
- except AspellError:
- return Response(msg.sender, "Je n'ai pas le dictionnaire `%s' :(" % lang, msg.channel)
- if r == True:
- add_score(msg.nick, "correct")
- strRes.append("l'orthographe de `%s' est correcte" % word)
- elif len(r) > 0:
- add_score(msg.nick, "bad")
- strRes.append("suggestions pour `%s' : %s" % (word, ", ".join(r)))
- else:
- add_score(msg.nick, "bad")
- strRes.append("aucune suggestion pour `%s'" % word)
- return Response(msg.sender, strRes, channel=msg.channel)
+# MODULE CORE #########################################################
def add_score(nick, t):
- global DATAS
- if nick not in DATAS.index:
+ if nick not in context.data.index:
st = ModuleState("score")
st["name"] = nick
- DATAS.addChild(st)
+ context.data.addChild(st)
- if DATAS.index[nick].hasAttribute(t):
- DATAS.index[nick][t] = DATAS.index[nick].getInt(t) + 1
+ if context.data.index[nick].hasAttribute(t):
+ context.data.index[nick][t] = context.data.index[nick].getInt(t) + 1
else:
- DATAS.index[nick][t] = 1
- save()
+ context.data.index[nick][t] = 1
+ context.save()
-def cmd_score(msg):
- global DATAS
- res = list()
- unknown = list()
- if len(msg.cmds) > 1:
- for cmd in msg.cmds[1:]:
- if cmd in DATAS.index:
- res.append(Response(msg.sender, "%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, DATAS.index[cmd].getInt(a)) for a in DATAS.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel))
- else:
- unknown.append(cmd)
+
+def check_spell(word, lang='fr'):
+ a = Aspell([("lang", lang)])
+ if a.check(word.encode("utf-8")):
+ ret = True
else:
- return Response(msg.sender, "De qui veux-tu voir les scores ?", channel=msg.channel, nick=msg.nick)
- if len(unknown) > 0:
- res.append(Response(msg.sender, "%s inconnus" % ", ".join(unknown), channel=msg.channel))
+ ret = a.suggest(word.encode("utf-8"))
+ a.close()
+ return ret
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("spell",
+ help="give the correct spelling of given words",
+ help_usage={"WORD": "give the correct spelling of the WORD."},
+ keywords={"lang=": "change the language use for checking, default fr"})
+def cmd_spell(msg):
+ if not len(msg.args):
+ raise IMException("indique une orthographe approximative du mot dont tu veux vérifier l'orthographe.")
+
+ lang = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr"
+
+ res = Response(channel=msg.channel)
+ for word in msg.args:
+ try:
+ r = check_spell(word, lang)
+ except AspellError:
+ raise IMException("Je n'ai pas le dictionnaire `%s' :(" % lang)
+
+ if r == True:
+ add_score(msg.frm, "correct")
+ res.append_message("l'orthographe de `%s' est correcte" % word)
+
+ elif len(r) > 0:
+ add_score(msg.frm, "bad")
+ res.append_message(r, title="suggestions pour `%s'" % word)
+
+ else:
+ add_score(msg.frm, "bad")
+ res.append_message("aucune suggestion pour `%s'" % word)
return res
-def check_spell(word, lang='fr'):
- a = Aspell([("lang", lang), ("lang", "fr")])
- if a.check(word.encode("iso-8859-15")):
- ret = True
- else:
- ret = a.suggest(word.encode("iso-8859-15"))
- a.close()
- return ret
+
+@hook.command("spellscore",
+ help="Show spell score (tests, mistakes, ...) for someone",
+ help_usage={"USER": "Display score of USER"})
+def cmd_score(msg):
+ res = list()
+ unknown = list()
+ if not len(msg.args):
+ raise IMException("De qui veux-tu voir les scores ?")
+ for cmd in msg.args:
+ if cmd in context.data.index:
+ res.append(Response("%s: %s" % (cmd, " ; ".join(["%s: %d" % (a, context.data.index[cmd].getInt(a)) for a in context.data.index[cmd].attributes.keys() if a != "name"])), channel=msg.channel))
+ else:
+ unknown.append(cmd)
+ if len(unknown) > 0:
+ res.append(Response("%s inconnus" % ", ".join(unknown), channel=msg.channel))
+
+ return res
diff --git a/modules/suivi.py b/modules/suivi.py
new file mode 100644
index 0000000..a54b722
--- /dev/null
+++ b/modules/suivi.py
@@ -0,0 +1,332 @@
+"""Postal tracking module"""
+
+# PYTHON STUFF ############################################
+
+import json
+import urllib.parse
+from bs4 import BeautifulSoup
+import re
+
+from nemubot.hooks import hook
+from nemubot.exception import IMException
+from nemubot.tools.web import getURLContent, getURLHeaders, getJSON
+from nemubot.module.more import Response
+
+
+# POSTAGE SERVICE PARSERS ############################################
+
+def get_tnt_info(track_id):
+ values = []
+ data = getURLContent('https://www.tnt.fr/public/suivi_colis/recherche/visubontransport.do?bonTransport=%s' % track_id)
+ soup = BeautifulSoup(data)
+ status_list = soup.find('div', class_='result__content')
+ if not status_list:
+ return None
+ last_status = status_list.find('div', class_='roster')
+ if last_status:
+ for info in last_status.find_all('div', class_='roster__item'):
+ values.append(info.get_text().strip())
+ if len(values) == 3:
+ return (values[0], values[1], values[2])
+
+
+def get_colissimo_info(colissimo_id):
+ colissimo_data = getURLContent("https://www.laposte.fr/particulier/outils/suivre-vos-envois?code=%s" % colissimo_id)
+ soup = BeautifulSoup(colissimo_data)
+
+ dataArray = soup.find(class_='results-suivi')
+ if dataArray and dataArray.table and dataArray.table.tbody and dataArray.table.tbody.tr:
+ td = dataArray.table.tbody.tr.find_all('td')
+ if len(td) > 2:
+ date = td[0].get_text()
+ libelle = re.sub(r'[\n\t\r]', '', td[1].get_text())
+ site = td[2].get_text().strip()
+ return (date, libelle, site.strip())
+
+
+def get_chronopost_info(track_id):
+ data = urllib.parse.urlencode({'listeNumeros': track_id})
+ track_baseurl = "https://www.chronopost.fr/expedier/inputLTNumbersNoJahia.do?lang=fr_FR"
+ track_data = getURLContent(track_baseurl, data.encode('utf-8'))
+ soup = BeautifulSoup(track_data)
+
+ infoClass = soup.find(class_='numeroColi2')
+ if infoClass and infoClass.get_text():
+ info = infoClass.get_text().split("\n")
+ if len(info) >= 1:
+ info = info[1].strip().split("\"")
+ if len(info) >= 2:
+ date = info[2]
+ libelle = info[1]
+ return (date, libelle)
+
+
+def get_colisprive_info(track_id):
+ data = urllib.parse.urlencode({'numColis': track_id})
+ track_baseurl = "https://www.colisprive.com/moncolis/pages/detailColis.aspx"
+ track_data = getURLContent(track_baseurl, data.encode('utf-8'))
+ soup = BeautifulSoup(track_data)
+
+ dataArray = soup.find(class_='BandeauInfoColis')
+ if (dataArray and dataArray.find(class_='divStatut')
+ and dataArray.find(class_='divStatut').find(class_='tdText')):
+ status = dataArray.find(class_='divStatut') \
+ .find(class_='tdText').get_text()
+ return status
+
+
+def get_ups_info(track_id):
+ data = json.dumps({'Locale': "en_US", 'TrackingNumber': [track_id]})
+ track_baseurl = "https://www.ups.com/track/api/Track/GetStatus?loc=en_US"
+ track_data = getJSON(track_baseurl, data.encode('utf-8'), header={"Content-Type": "application/json"})
+ return (track_data["trackDetails"][0]["trackingNumber"],
+ track_data["trackDetails"][0]["packageStatus"],
+ track_data["trackDetails"][0]["shipmentProgressActivities"][0]["date"] + " " + track_data["trackDetails"][0]["shipmentProgressActivities"][0]["time"],
+ track_data["trackDetails"][0]["shipmentProgressActivities"][0]["location"],
+ track_data["trackDetails"][0]["shipmentProgressActivities"][0]["activityScan"])
+
+
+def get_laposte_info(laposte_id):
+ status, laposte_headers = getURLHeaders("https://www.laposte.fr/outils/suivre-vos-envois?" + urllib.parse.urlencode({'code': laposte_id}))
+
+ laposte_cookie = None
+ for k,v in laposte_headers:
+ if k.lower() == "set-cookie" and v.find("access_token") >= 0:
+ laposte_cookie = v.split(";")[0]
+
+ laposte_data = getJSON("https://api.laposte.fr/ssu/v1/suivi-unifie/idship/%s?lang=fr_FR" % urllib.parse.quote(laposte_id), header={"Accept": "application/json", "Cookie": laposte_cookie})
+
+ shipment = laposte_data["shipment"]
+ return (shipment["product"], shipment["idShip"], shipment["event"][0]["label"], shipment["event"][0]["date"])
+
+
+def get_postnl_info(postnl_id):
+ data = urllib.parse.urlencode({'barcodes': postnl_id})
+ postnl_baseurl = "http://www.postnl.post/details/"
+
+ postnl_data = getURLContent(postnl_baseurl, data.encode('utf-8'))
+ soup = BeautifulSoup(postnl_data)
+ if (soup.find(id='datatables')
+ and soup.find(id='datatables').tbody
+ and soup.find(id='datatables').tbody.tr):
+ search_res = soup.find(id='datatables').tbody.tr
+ if len(search_res.find_all('td')) >= 3:
+ field = field.find_next('td')
+ post_date = field.get_text()
+
+ field = field.find_next('td')
+ post_status = field.get_text()
+
+ field = field.find_next('td')
+ post_destination = field.get_text()
+
+ return (post_status.lower(), post_destination, post_date)
+
+
+def get_usps_info(usps_id):
+ usps_parcelurl = "https://tools.usps.com/go/TrackConfirmAction_input?" + urllib.parse.urlencode({'qtc_tLabels1': usps_id})
+
+ usps_data = getURLContent(usps_parcelurl)
+ soup = BeautifulSoup(usps_data)
+ if (soup.find(id="trackingHistory_1")
+ and soup.find(class_="tracking_history").find(class_="row_notification")
+ and soup.find(class_="tracking_history").find(class_="row_top").find_all("td")):
+ notification = soup.find(class_="tracking_history").find(class_="row_notification").text.strip()
+ date = re.sub(r"\s+", " ", soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[0].text.strip())
+ status = soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[1].text.strip()
+ last_location = soup.find(class_="tracking_history").find(class_="row_top").find_all("td")[2].text.strip()
+
+ print(notification)
+
+ return (notification, date, status, last_location)
+
+
+def get_fedex_info(fedex_id, lang="en_US"):
+ data = urllib.parse.urlencode({
+ 'data': json.dumps({
+ "TrackPackagesRequest": {
+ "appType": "WTRK",
+ "appDeviceType": "DESKTOP",
+ "uniqueKey": "",
+ "processingParameters": {},
+ "trackingInfoList": [
+ {
+ "trackNumberInfo": {
+ "trackingNumber": str(fedex_id),
+ "trackingQualifier": "",
+ "trackingCarrier": ""
+ }
+ }
+ ]
+ }
+ }),
+ 'action': "trackpackages",
+ 'locale': lang,
+ 'version': 1,
+ 'format': "json"
+ })
+ fedex_baseurl = "https://www.fedex.com/trackingCal/track"
+
+ fedex_data = getJSON(fedex_baseurl, data.encode('utf-8'))
+
+ if ("TrackPackagesResponse" in fedex_data and
+ "packageList" in fedex_data["TrackPackagesResponse"] and
+ len(fedex_data["TrackPackagesResponse"]["packageList"]) and
+ (not fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] or
+ fedex_data["TrackPackagesResponse"]["errorList"][0]["code"] == '0') and
+ not fedex_data["TrackPackagesResponse"]["packageList"][0]["errorList"][0]["code"]
+ ):
+ return fedex_data["TrackPackagesResponse"]["packageList"][0]
+
+
+def get_dhl_info(dhl_id, lang="en"):
+ dhl_parcelurl = "http://www.dhl.com/shipmentTracking?" + urllib.parse.urlencode({'AWB': dhl_id})
+
+ dhl_data = getJSON(dhl_parcelurl)
+
+ if "results" in dhl_data and dhl_data["results"]:
+ return dhl_data["results"][0]
+
+
+# TRACKING HANDLERS ###################################################
+
+def handle_tnt(tracknum):
+ info = get_tnt_info(tracknum)
+ if info:
+ status, date, place = info
+ placestr = ''
+ if place:
+ placestr = ' à \x02{place}\x0f'
+ return ('Le colis \x02{trackid}\x0f a actuellement le status: '
+ '\x02{status}\x0F mis à jour le \x02{date}\x0f{place}.'
+ .format(trackid=tracknum, status=status,
+ date=re.sub(r'\s+', ' ', date), place=placestr))
+
+
+def handle_laposte(tracknum):
+ info = get_laposte_info(tracknum)
+ if info:
+ poste_type, poste_id, poste_status, poste_date = info
+ return ("\x02%s\x0F : \x02%s\x0F est actuellement "
+ "\x02%s\x0F (Mis à jour le \x02%s\x0F"
+ ")." % (poste_type, poste_id, poste_status, poste_date))
+
+
+def handle_postnl(tracknum):
+ info = get_postnl_info(tracknum)
+ if info:
+ post_status, post_destination, post_date = info
+ return ("PostNL \x02%s\x0F est actuellement "
+ "\x02%s\x0F vers le pays \x02%s\x0F (Mis à jour le \x02%s\x0F"
+ ")." % (tracknum, post_status, post_destination, post_date))
+
+
+def handle_usps(tracknum):
+ info = get_usps_info(tracknum)
+ if info:
+ notif, last_date, last_status, last_location = info
+ return ("USPS \x02{tracknum}\x0F: {last_status} in \x02{last_location}\x0F as of {last_date}: {notif}".format(tracknum=tracknum, notif=notif, last_date=last_date, last_status=last_status.lower(), last_location=last_location))
+
+
+def handle_ups(tracknum):
+ info = get_ups_info(tracknum)
+ if info:
+ tracknum, status, last_date, last_location, last_status = info
+ return ("UPS \x02{tracknum}\x0F: {status}: in \x02{last_location}\x0F as of {last_date}: {last_status}".format(tracknum=tracknum, status=status, last_date=last_date, last_status=last_status.lower(), last_location=last_location))
+
+
+def handle_colissimo(tracknum):
+ info = get_colissimo_info(tracknum)
+ if info:
+ date, libelle, site = info
+ return ("Colissimo: \x02%s\x0F : \x02%s\x0F Dernière mise à jour le "
+ "\x02%s\x0F au site \x02%s\x0F."
+ % (tracknum, libelle, date, site))
+
+
+def handle_chronopost(tracknum):
+ info = get_chronopost_info(tracknum)
+ if info:
+ date, libelle = info
+ return ("Colis Chronopost: \x02%s\x0F : \x02%s\x0F. Dernière mise à "
+ "jour \x02%s\x0F." % (tracknum, libelle, date))
+
+
+def handle_coliprive(tracknum):
+ info = get_colisprive_info(tracknum)
+ if info:
+ return ("Colis Privé: \x02%s\x0F : \x02%s\x0F." % (tracknum, info))
+
+
+def handle_fedex(tracknum):
+ info = get_fedex_info(tracknum)
+ if info:
+ if info["displayActDeliveryDateTime"] != "":
+ return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, delivered on: {displayActDeliveryDateTime}.".format(**info))
+ elif info["statusLocationCity"] != "":
+ return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: estimated delivery: {displayEstDeliveryDateTime}.".format(**info))
+ else:
+ return ("{trackingCarrierDesc}: \x02{statusWithDetails}\x0F: in \x02{statusLocationCity}, {statusLocationCntryCD}\x0F, estimated delivery: {displayEstDeliveryDateTime}.".format(**info))
+
+
+def handle_dhl(tracknum):
+ info = get_dhl_info(tracknum)
+ if info:
+ return "DHL {label} {id}: \x02{description}\x0F".format(**info)
+
+
+TRACKING_HANDLERS = {
+ 'laposte': handle_laposte,
+ 'postnl': handle_postnl,
+ 'colissimo': handle_colissimo,
+ 'chronopost': handle_chronopost,
+ 'coliprive': handle_coliprive,
+ 'tnt': handle_tnt,
+ 'fedex': handle_fedex,
+ 'dhl': handle_dhl,
+ 'usps': handle_usps,
+ 'ups': handle_ups,
+}
+
+
+# HOOKS ##############################################################
+
+@hook.command("track",
+ help="Track postage delivery",
+ help_usage={
+ "TRACKING_ID [...]": "Track the specified postage IDs on various tracking services."
+ },
+ keywords={
+ "tracker=TRK": "Precise the tracker (default: all) among: " + ', '.join(TRACKING_HANDLERS)
+ })
+def get_tracking_info(msg):
+ if not len(msg.args):
+ raise IMException("Renseignez un identifiant d'envoi.")
+
+ res = Response(channel=msg.channel, count=" (%d suivis supplémentaires)")
+
+ if 'tracker' in msg.kwargs:
+ if msg.kwargs['tracker'] in TRACKING_HANDLERS:
+ trackers = {
+ msg.kwargs['tracker']: TRACKING_HANDLERS[msg.kwargs['tracker']]
+ }
+ else:
+ raise IMException("No tracker named \x02{tracker}\x0F, please use"
+ " one of the following: \x02{trackers}\x0F"
+ .format(tracker=msg.kwargs['tracker'],
+ trackers=', '
+ .join(TRACKING_HANDLERS.keys())))
+ else:
+ trackers = TRACKING_HANDLERS
+
+ for tracknum in msg.args:
+ for name, tracker in trackers.items():
+ ret = tracker(tracknum)
+ if ret:
+ res.append_message(ret)
+ break
+ if not ret:
+ res.append_message("L'identifiant \x02{id}\x0F semble incorrect,"
+ " merci de vérifier son exactitude."
+ .format(id=tracknum))
+ return res
diff --git a/modules/syno.py b/modules/syno.py
index 047fe03..78f0b7d 100644
--- a/modules/syno.py
+++ b/modules/syno.py
@@ -1,61 +1,117 @@
-# coding=utf-8
+"""Find synonyms"""
+
+# PYTHON STUFFS #######################################################
import re
-import traceback
-import sys
from urllib.parse import quote
-from tools import web
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
-nemubotversion = 3.3
+from nemubot.module.more import Response
-def help_tiny ():
- return "Find french synonyms"
-def help_full ():
- return "!syno : give a list of synonyms for ."
+# LOADING #############################################################
def load(context):
- from hooks import Hook
- add_hook("cmd_hook", Hook(cmd_syno, "syno"))
- add_hook("cmd_hook", Hook(cmd_syno, "synonyme"))
+ global lang_binding
-
-def cmd_syno(msg):
- if 1 < len(msg.cmds) < 6:
- for word in msg.cmds[1:]:
- try:
- synos = get_synos(word)
- except:
- synos = None
- exc_type, exc_value, exc_traceback = sys.exc_info()
- traceback.print_exception(exc_type, exc_value,
- exc_traceback)
-
- if synos is None:
- return Response(msg.sender,
- "Une erreur s'est produite durant la recherche"
- " d'un synonyme de %s" % word, msg.channel)
- elif len(synos) > 0:
- return Response(msg.sender, synos, msg.channel,
- title="Synonymes de %s" % word)
- else:
- return Response(msg.sender,
- "Aucun synonymes de %s n'a été trouvé" % word,
- msg.channel)
- return False
-
-
-def get_synos(word):
- url = "http://www.crisco.unicaen.fr/des/synonymes/" + quote(word.encode("ISO-8859-1"))
- print_debug (url)
- page = web.getURLContent(url)
- if page is not None:
- synos = list()
- for line in page.decode().split("\n"):
- if re.match("[ \t]*]*>.*
[ \t]*.*", line) is not None:
- for elt in re.finditer(">&[^;]+;([^&]*)&[^;]+;<", line):
- synos.append(elt.group(1))
- return synos
+ if not context.config or not "bighugelabskey" in context.config:
+ logger.error("You need a NigHugeLabs API key in order to have english "
+ "theasorus. Add it to the module configuration file:\n"
+ "\nRegister at https://words.bighugelabs.com/getkey.php")
else:
- return None
+ lang_binding["en"] = lambda word: get_english_synos(context.config["bighugelabskey"], word)
+
+
+# MODULE CORE #########################################################
+
+def get_french_synos(word):
+ url = "https://crisco.unicaen.fr/des/synonymes/" + quote(word)
+ page = web.getURLContent(url)
+
+ best = list(); synos = list(); anton = list()
+
+ if page is not None:
+ for line in page.split("\n"):
+
+ if line.find("!-- Fin liste des antonymes --") > 0:
+ for elt in re.finditer(">([^<>]+)", line):
+ anton.append(elt.group(1))
+
+ elif line.find("!--Fin liste des synonymes--") > 0:
+ for elt in re.finditer(">([^<>]+)", line):
+ synos.append(elt.group(1))
+
+ elif re.match("[ \t]*]*>.*
[ \t]*.*", line) is not None:
+ for elt in re.finditer(">&[^;]+;([^&]*)&[^;]+;<", line):
+ best.append(elt.group(1))
+
+ return (best, synos, anton)
+
+
+def get_english_synos(key, word):
+ cnt = web.getJSON("https://words.bighugelabs.com/api/2/%s/%s/json" %
+ (quote(key), quote(word.encode("ISO-8859-1"))))
+
+ best = list(); synos = list(); anton = list()
+
+ if cnt is not None:
+ for k, c in cnt.items():
+ if "syn" in c: best += c["syn"]
+ if "rel" in c: synos += c["rel"]
+ if "ant" in c: anton += c["ant"]
+
+ return (best, synos, anton)
+
+
+lang_binding = { 'fr': get_french_synos }
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("synonymes", data="synonymes",
+ help="give a list of synonyms",
+ help_usage={"WORD": "give synonyms of the given WORD"},
+ keywords={
+ "lang=LANG": "change the dictionnary language: default fr, available: " + ", ".join(lang_binding)
+ })
+@hook.command("antonymes", data="antonymes",
+ help="give a list of antonyms",
+ help_usage={"WORD": "give antonyms of the given WORD"},
+ keywords={
+ "lang=LANG": "change the dictionnary language: default fr, available: " + ", ".join(lang_binding)
+ })
+def go(msg, what):
+ if not len(msg.args):
+ raise IMException("de quel mot veux-tu connaître la liste des synonymes ?")
+
+ lang = msg.kwargs["lang"] if "lang" in msg.kwargs else "fr"
+ word = ' '.join(msg.args)
+
+ try:
+ best, synos, anton = lang_binding[lang](word)
+ except:
+ best, synos, anton = (list(), list(), list())
+
+ if what == "synonymes":
+ if len(synos) > 0 or len(best) > 0:
+ res = Response(channel=msg.channel, title="Synonymes de %s" % word)
+ if len(best) > 0: res.append_message(best)
+ if len(synos) > 0: res.append_message(synos)
+ return res
+ else:
+ raise IMException("Aucun synonyme de %s n'a été trouvé" % word)
+
+ elif what == "antonymes":
+ if len(anton) > 0:
+ res = Response(anton, channel=msg.channel,
+ title="Antonymes de %s" % word)
+ return res
+ else:
+ raise IMException("Aucun antonyme de %s n'a été trouvé" % word)
+
+ else:
+ raise IMException("WHAT?!")
diff --git a/modules/tpb.py b/modules/tpb.py
new file mode 100644
index 0000000..a752324
--- /dev/null
+++ b/modules/tpb.py
@@ -0,0 +1,40 @@
+from datetime import datetime
+import urllib
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import human
+from nemubot.tools.web import getJSON
+
+nemubotversion = 4.0
+
+from nemubot.module.more import Response
+
+URL_TPBAPI = None
+
+def load(context):
+ if not context.config or "url" not in context.config:
+ raise ImportError("You need a TPB API in order to use the !tpb feature"
+ ". Add it to the module configuration file:\n\nSample "
+ "API: "
+ "https://gist.github.com/colona/07a925f183cfb47d5f20")
+ global URL_TPBAPI
+ URL_TPBAPI = context.config["url"]
+
+@hook.command("tpb")
+def cmd_tpb(msg):
+ if not len(msg.args):
+ raise IMException("indicate an item to search!")
+
+ torrents = getJSON(URL_TPBAPI + urllib.parse.quote(" ".join(msg.args)))
+
+ res = Response(channel=msg.channel, nomore="No more torrents", count=" (%d more torrents)")
+
+ if torrents:
+ for t in torrents:
+ t["sizeH"] = human.size(t["size"])
+ t["dateH"] = datetime.fromtimestamp(t["date"]).strftime('%Y-%m-%d %H:%M:%S')
+ res.append_message("\x03\x02{title}\x03\x02 in {category}, {sizeH}; added at {dateH}; id: {id}; magnet:?xt=urn:btih:{magnet}&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80&tr=udp%3A%2F%2Ftracker.istole.it%3A6969&tr=udp%3A%2F%2Fopen.demonii.com%3A1337".format(**t))
+
+ return res
diff --git a/modules/translate.py b/modules/translate.py
index 60838f0..906ba93 100644
--- a/modules/translate.py
+++ b/modules/translate.py
@@ -1,97 +1,111 @@
-# coding=utf-8
+"""Translation module"""
+
+# PYTHON STUFFS #######################################################
-import http.client
-import re
-import socket
-import json
from urllib.parse import quote
-nemubotversion = 3.3
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
-import xmlparser
+from nemubot.module.more import Response
+
+
+# GLOBALS #############################################################
LANG = ["ar", "zh", "cz", "en", "fr", "gr", "it",
"ja", "ko", "pl", "pt", "ro", "es", "tr"]
+URL = "http://api.wordreference.com/0.8/%s/json/%%s%%s/%%s"
+
+
+# LOADING #############################################################
def load(context):
- from hooks import Hook
- add_hook("cmd_hook", Hook(cmd_translate, "translate"))
- add_hook("cmd_hook", Hook(cmd_translate, "traduction"))
- add_hook("cmd_hook", Hook(cmd_translate, "traduit"))
- add_hook("cmd_hook", Hook(cmd_translate, "traduire"))
+ if not context.config or "wrapikey" not in context.config:
+ raise ImportError("You need a WordReference API key in order to use "
+ "this module. Add it to the module configuration "
+ "file:\n\nRegister at http://"
+ "www.wordreference.com/docs/APIregistration.aspx")
+ global URL
+ URL = URL % context.config["wrapikey"]
+# MODULE CORE #########################################################
+
+def meaning(entry):
+ ret = list()
+ if "sense" in entry and len(entry["sense"]) > 0:
+ ret.append('« %s »' % entry["sense"])
+ if "usage" in entry and len(entry["usage"]) > 0:
+ ret.append(entry["usage"])
+ if len(ret) > 0:
+ return " as " + "/".join(ret)
+ else:
+ return ""
+
+
+def extract_traslation(entry):
+ ret = list()
+ for i in [ "FirstTranslation", "SecondTranslation", "ThirdTranslation", "FourthTranslation" ]:
+ if i in entry:
+ ret.append("\x03\x02%s\x03\x02%s" % (entry[i]["term"], meaning(entry[i])))
+ if "Note" in entry and entry["Note"]:
+ ret.append("note: %s" % entry["Note"])
+ return ", ".join(ret)
+
+
+def translate(term, langFrom="en", langTo="fr"):
+ wres = web.getJSON(URL % (langFrom, langTo, quote(term)))
+
+ if "Error" in wres:
+ raise IMException(wres["Note"])
+
+ else:
+ for k in sorted(wres.keys()):
+ t = wres[k]
+ if len(k) > 4 and k[:4] == "term":
+ if "Entries" in t:
+ ent = t["Entries"]
+ else:
+ ent = t["PrincipalTranslations"]
+
+ for i in sorted(ent.keys()):
+ yield "Translation of %s%s: %s" % (
+ ent[i]["OriginalTerm"]["term"],
+ meaning(ent[i]["OriginalTerm"]),
+ extract_traslation(ent[i]))
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("translate",
+ help="Word translation using WordReference.com",
+ help_usage={
+ "TERM": "Found translation of TERM from/to english to/from ."
+ },
+ keywords={
+ "from=LANG": "language of the term you asked for translation between: en, " + ", ".join(LANG),
+ "to=LANG": "language of the translated terms between: en, " + ", ".join(LANG),
+ })
def cmd_translate(msg):
- global LANG
- startWord = 1
- if msg.cmds[startWord] in LANG:
- langTo = msg.cmds[startWord]
- startWord += 1
+ if not len(msg.args):
+ raise IMException("which word would you translate?")
+
+ langFrom = msg.kwargs["from"] if "from" in msg.kwargs else "en"
+ if "to" in msg.kwargs:
+ langTo = msg.kwargs["to"]
else:
- langTo = "fr"
- if msg.cmds[startWord] in LANG:
- langFrom = langTo
- langTo = msg.cmds[startWord]
- startWord += 1
- else:
- if langTo == "en":
- langFrom = "fr"
- else:
- langFrom = "en"
+ langTo = "fr" if langFrom == "en" else "en"
- (res, page) = getPage(' '.join(msg.cmds[startWord:]), langFrom, langTo)
- if res == http.client.OK:
- wres = json.loads(page.decode())
- if "Error" in wres:
- return Response(msg.sender, wres["Note"], msg.channel)
- else:
- start = "Traduction de %s : "%' '.join(msg.cmds[startWord:])
- if "Entries" in wres["term0"]:
- if "SecondTranslation" in wres["term0"]["Entries"]["0"]:
- return Response(msg.sender, start +
- wres["term0"]["Entries"]["0"]["FirstTranslation"]["term"] +
- " ; " +
- wres["term0"]["Entries"]["0"]["SecondTranslation"]["term"],
- msg.channel)
- else:
- return Response(msg.sender, start +
- wres["term0"]["Entries"]["0"]["FirstTranslation"]["term"],
- msg.channel)
- elif "PrincipalTranslations" in wres["term0"]:
- if "1" in wres["term0"]["PrincipalTranslations"]:
- return Response(msg.sender, start +
- wres["term0"]["PrincipalTranslations"]["0"]["FirstTranslation"]["term"] +
- " ; " +
- wres["term0"]["PrincipalTranslations"]["1"]["FirstTranslation"]["term"],
- msg.channel)
- else:
- return Response(msg.sender, start +
- wres["term0"]["PrincipalTranslations"]["0"]["FirstTranslation"]["term"],
- msg.channel)
- else:
- return Response(msg.sender, "Une erreur s'est produite durant la recherche"
- " d'une traduction de %s"
- % ' '.join(msg.cmds[startWord:]),
- msg.channel)
+ if langFrom not in LANG or langTo not in LANG:
+ raise IMException("sorry, I can only translate to or from: " + ", ".join(LANG))
+ if langFrom != "en" and langTo != "en":
+ raise IMException("sorry, I can only translate to or from english")
-
-def getPage(terms, langfrom="fr", langto="en"):
- conn = http.client.HTTPConnection("api.wordreference.com", timeout=5)
- try:
- conn.request("GET", "/0.8/%s/json/%s%s/%s" % (
- CONF.getNode("wrapi")["key"], langfrom, langto, quote(terms)))
- except socket.gaierror:
- print ("impossible de récupérer la page WordReference.")
- return (http.client.INTERNAL_SERVER_ERROR, None)
- except (TypeError, KeyError):
- print ("You need a WordReference API key in order to use this module."
- " Add it to the module configuration file:\n\nRegister at "
- "http://www.wordreference.com/docs/APIregistration.aspx")
- return (http.client.INTERNAL_SERVER_ERROR, None)
-
- res = conn.getresponse()
- data = res.read()
-
- conn.close()
- return (res.status, data)
+ res = Response(channel=msg.channel,
+ count=" (%d more meanings)",
+ nomore="No more translation")
+ for t in translate(" ".join(msg.args), langFrom=langFrom, langTo=langTo):
+ res.append_message(t)
+ return res
diff --git a/modules/urbandict.py b/modules/urbandict.py
new file mode 100644
index 0000000..b561e89
--- /dev/null
+++ b/modules/urbandict.py
@@ -0,0 +1,37 @@
+"""Search definition from urbandictionnary"""
+
+# 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 search(terms):
+ return web.getJSON(
+ "https://api.urbandictionary.com/v0/define?term=%s"
+ % quote(' '.join(terms)))
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("urbandictionnary")
+def udsearch(msg):
+ if not len(msg.args):
+ raise IMException("Indicate a term to search")
+
+ s = search(msg.args)
+
+ res = Response(channel=msg.channel, nomore="No more results",
+ count=" (%d more definitions)")
+
+ for i in s["list"]:
+ res.append_message(i["definition"].replace("\n", " "),
+ title=i["word"])
+
+ return res
diff --git a/modules/urlreducer.py b/modules/urlreducer.py
new file mode 100644
index 0000000..86f4d42
--- /dev/null
+++ b/modules/urlreducer.py
@@ -0,0 +1,173 @@
+"""URL reducer module"""
+
+# PYTHON STUFFS #######################################################
+
+import re
+import json
+from urllib.parse import urlparse
+from urllib.parse import quote
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.message import Text
+from nemubot.tools import web
+
+
+# MODULE FUNCTIONS ####################################################
+
+def default_reducer(url, data):
+ snd_url = url + quote(data, "/:%@&=?")
+ return web.getURLContent(snd_url)
+
+
+def ycc_reducer(url, data):
+ return "https://ycc.fr/%s" % default_reducer(url, data)
+
+def lstu_reducer(url, data):
+ json_data = json.loads(web.getURLContent(url, "lsturl=" + quote(data),
+ header={"Content-Type": "application/x-www-form-urlencoded"}))
+ if 'short' in json_data:
+ return json_data['short']
+ elif 'msg' in json_data:
+ raise IMException("Error: %s" % json_data['msg'])
+ else:
+ IMException("An error occured while shortening %s." % data)
+
+# MODULE VARIABLES ####################################################
+
+PROVIDERS = {
+ "tinyurl": (default_reducer, "https://tinyurl.com/api-create.php?url="),
+ "ycc": (ycc_reducer, "https://ycc.fr/redirection/create/"),
+ "framalink": (lstu_reducer, "https://frama.link/a?format=json"),
+ "huitre": (lstu_reducer, "https://huit.re/a?format=json"),
+ "lstu": (lstu_reducer, "https://lstu.fr/a?format=json"),
+}
+DEFAULT_PROVIDER = "framalink"
+
+PROVIDERS_NETLOC = [urlparse(web.getNormalizedURL(url), "http").netloc for f, url in PROVIDERS.values()]
+
+# LOADING #############################################################
+
+
+def load(context):
+ global DEFAULT_PROVIDER
+
+ if "provider" in context.config:
+ if context.config["provider"] == "custom":
+ PROVIDERS["custom"] = context.config["provider_url"]
+ DEFAULT_PROVIDER = context.config["provider"]
+
+
+# MODULE CORE #########################################################
+
+def reduce_inline(txt, provider=None):
+ for url in re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)", txt):
+ txt = txt.replace(url, reduce(url, provider))
+ return txt
+
+
+def reduce(url, provider=None):
+ """Ask the url shortner website to reduce given URL
+
+ Argument:
+ url -- the URL to reduce
+ """
+ if provider is None:
+ provider = DEFAULT_PROVIDER
+ return PROVIDERS[provider][0](PROVIDERS[provider][1], url)
+
+
+def gen_response(res, msg, srv):
+ if res is None:
+ raise IMException("bad URL : %s" % srv)
+ else:
+ return Text("URL for %s: %s" % (srv, res), server=None,
+ to=msg.to_response)
+
+
+## URL stack
+
+LAST_URLS = dict()
+
+
+@hook.message()
+def parselisten(msg):
+ global LAST_URLS
+ if hasattr(msg, "message") and isinstance(msg.message, str):
+ urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)",
+ msg.message)
+ for url in urls:
+ o = urlparse(web._getNormalizedURL(url), "http")
+
+ # Skip short URLs
+ if (o.netloc == "" or o.netloc in PROVIDERS or
+ len(o.netloc) + len(o.path) < 17):
+ continue
+
+ for recv in msg.to:
+ if recv not in LAST_URLS:
+ LAST_URLS[recv] = list()
+ LAST_URLS[recv].append(url)
+
+
+@hook.post()
+def parseresponse(msg):
+ global LAST_URLS
+ if hasattr(msg, "text") and isinstance(msg.text, str):
+ urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?(?:[^ :/]+:[0-9]+)?[^ :]+)",
+ msg.text)
+ for url in urls:
+ o = urlparse(web._getNormalizedURL(url), "http")
+
+ # Skip short URLs
+ if (o.netloc == "" or o.netloc in PROVIDERS or
+ len(o.netloc) + len(o.path) < 17):
+ continue
+
+ for recv in msg.to:
+ if recv not in LAST_URLS:
+ LAST_URLS[recv] = list()
+ LAST_URLS[recv].append(url)
+ return msg
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("framalink",
+ help="Reduce any long URL",
+ help_usage={
+ None: "Reduce the last URL said on the channel",
+ "URL [URL ...]": "Reduce the given URL(s)"
+ },
+ keywords={
+ "provider=SMTH": "Change the service provider used (by default: %s) among %s" % (DEFAULT_PROVIDER, ", ".join(PROVIDERS.keys()))
+ })
+def cmd_reduceurl(msg):
+ minify = list()
+
+ if not len(msg.args):
+ global LAST_URLS
+ if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
+ minify.append(LAST_URLS[msg.channel].pop())
+ else:
+ raise IMException("I have no more URL to reduce.")
+
+ if len(msg.args) > 4:
+ raise IMException("I cannot reduce that many URLs at once.")
+ else:
+ minify += msg.args
+
+ if 'provider' in msg.kwargs and msg.kwargs['provider'] in PROVIDERS:
+ provider = msg.kwargs['provider']
+ else:
+ provider = DEFAULT_PROVIDER
+
+ res = list()
+ for url in minify:
+ o = urlparse(web.getNormalizedURL(url), "http")
+ minief_url = reduce(url, provider)
+ if o.netloc == "":
+ res.append(gen_response(minief_url, msg, o.scheme))
+ else:
+ res.append(gen_response(minief_url, msg, o.netloc))
+ return res
diff --git a/modules/velib.py b/modules/velib.py
index 8385476..71c472c 100644
--- a/modules/velib.py
+++ b/modules/velib.py
@@ -1,51 +1,53 @@
-# coding=utf-8
+"""Gets information about velib stations"""
+
+# PYTHON STUFFS #######################################################
import re
-from tools import web
+from nemubot import context
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
-nemubotversion = 3.3
+from nemubot.module.more import Response
+
+
+# LOADING #############################################################
+
+URL_API = None # http://www.velib.paris.fr/service/stationdetails/paris/%s
def load(context):
- global DATAS
- DATAS.setIndex("name", "station")
+ global URL_API
+ if not context.config or "url" not in context.config:
+ raise ImportError("Please provide url attribute in the module configuration")
+ URL_API = context.config["url"]
+ context.data.setIndex("name", "station")
# evt = ModuleEvent(station_available, "42706",
# (lambda a, b: a != b), None, 60,
# station_status)
# context.add_event(evt)
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "Gets information about velib stations"
-
-def help_full ():
- return "!velib /number/ ...: gives available bikes and slots at the station /number/."
+# MODULE CORE #########################################################
def station_status(station):
"""Gets available and free status of a given station"""
- response = web.getXML(CONF.getNode("server")["url"] + station)
+ response = web.getXML(URL_API % station)
if response is not None:
- available = response.getNode("available").getContent()
- if available is not None and len(available) > 0:
- available = int(available)
- else:
- available = 0
- free = response.getNode("free").getContent()
- if free is not None and len(free) > 0:
- free = int(free)
- else:
- free = 0
+ available = int(response.getElementsByTagName("available")[0].firstChild.nodeValue)
+ free = int(response.getElementsByTagName("free")[0].firstChild.nodeValue)
return (available, free)
else:
return (None, None)
+
def station_available(station):
"""Gets available velib at a given velib station"""
(a, f) = station_status(station)
return a
+
def station_free(station):
"""Gets free slots at a given velib station"""
(a, f) = station_status(station)
@@ -56,33 +58,30 @@ def print_station_status(msg, station):
"""Send message with information about the given station"""
(available, free) = station_status(station)
if available is not None and free is not None:
- return Response(msg.sender,
- "%s: à la station %s : %d vélib et %d points d'attache"
- " disponibles." % (msg.nick, station, available, free),
- msg.channel)
- else:
- return Response(msg.sender,
- "%s: station %s inconnue." % (msg.nick, station),
- msg.channel)
+ return Response("À la station %s : %d vélib et %d points d'attache"
+ " disponibles." % (station, available, free),
+ channel=msg.channel)
+ raise IMException("station %s inconnue." % station)
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("velib",
+ help="gives available bikes and slots at the given station",
+ help_usage={
+ "STATION_ID": "gives available bikes and slots at the station STATION_ID"
+ })
def ask_stations(msg):
- """Hook entry from !velib"""
- global DATAS
- if len(msg.cmds) > 5:
- return Response(msg.sender,
- "Demande-moi moins de stations à la fois.",
- msg.channel, nick=msg.nick)
- elif len(msg.cmds) > 1:
- for station in msg.cmds[1:]:
- if re.match("^[0-9]{4,5}$", station):
- return print_station_status(msg, station)
- elif station in DATAS.index:
- return print_station_status(msg, DATAS.index[station]["number"])
- else:
- return Response(msg.sender,
- "numéro de station invalide.",
- msg.channel, nick=msg.nick)
- else:
- return Response(msg.sender,
- "Pour quelle station ?",
- msg.channel, nick=msg.nick)
+ if len(msg.args) > 4:
+ raise IMException("demande-moi moins de stations à la fois.")
+ elif not len(msg.args):
+ raise IMException("pour quelle station ?")
+
+ for station in msg.args:
+ if re.match("^[0-9]{4,5}$", station):
+ return print_station_status(msg, station)
+ elif station in context.data.index:
+ return print_station_status(msg,
+ context.data.index[station]["number"])
+ else:
+ raise IMException("numéro de station invalide.")
diff --git a/modules/virtualradar.py b/modules/virtualradar.py
new file mode 100644
index 0000000..2c87e79
--- /dev/null
+++ b/modules/virtualradar.py
@@ -0,0 +1,100 @@
+"""Retrieve flight information from VirtualRadar APIs"""
+
+# PYTHON STUFFS #######################################################
+
+import re
+from urllib.parse import quote
+import time
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
+
+from nemubot.module.more import Response
+from nemubot.module import mapquest
+
+# GLOBALS #############################################################
+
+URL_API = "https://public-api.adsbexchange.com/VirtualRadar/AircraftList.json?fcallC=%s"
+
+SPEED_TYPES = {
+ 0: 'Ground speed',
+ 1: 'Ground speed reversing',
+ 2: 'Indicated air speed',
+ 3: 'True air speed'}
+
+WTC_CAT = {
+ 0: 'None',
+ 1: 'Light',
+ 2: 'Medium',
+ 3: 'Heavy'
+ }
+
+SPECIES = {
+ 1: 'Land plane',
+ 2: 'Sea plane',
+ 3: 'Amphibian',
+ 4: 'Helicopter',
+ 5: 'Gyrocopter',
+ 6: 'Tiltwing',
+ 7: 'Ground vehicle',
+ 8: 'Tower'}
+
+HANDLER_TABLE = {
+ 'From': lambda x: 'From: \x02%s\x0F' % x,
+ 'To': lambda x: 'To: \x02%s\x0F' % x,
+ 'Op': lambda x: 'Airline: \x02%s\x0F' % x,
+ 'Mdl': lambda x: 'Model: \x02%s\x0F' % x,
+ 'Call': lambda x: 'Flight: \x02%s\x0F' % x,
+ 'PosTime': lambda x: 'Last update: \x02%s\x0F' % (time.ctime(int(x)/1000)),
+ 'Alt': lambda x: 'Altitude: \x02%s\x0F ft' % x,
+ 'Spd': lambda x: 'Speed: \x02%s\x0F kn' % x,
+ 'SpdTyp': lambda x: 'Speed type: \x02%s\x0F' % SPEED_TYPES[x] if x in SPEED_TYPES else None,
+ 'Engines': lambda x: 'Engines: \x02%s\x0F' % x,
+ 'Gnd': lambda x: 'On the ground' if x else None,
+ 'Mil': lambda x: 'Military aicraft' if x else None,
+ 'Species': lambda x: 'Aircraft species: \x02%s\x0F' % SPECIES[x] if x in SPECIES else None,
+ 'WTC': lambda x: 'Turbulence level: \x02%s\x0F' % WTC_CAT[x] if x in WTC_CAT else None,
+ }
+
+# MODULE CORE #########################################################
+
+def virtual_radar(flight_call):
+ obj = web.getJSON(URL_API % quote(flight_call))
+
+ if "acList" in obj:
+ for flight in obj["acList"]:
+ yield flight
+
+def flight_info(flight):
+ for prop in HANDLER_TABLE:
+ if prop in flight:
+ yield HANDLER_TABLE[prop](flight[prop])
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("flight",
+ help="Get flight information",
+ help_usage={ "FLIGHT": "Get information on FLIGHT" })
+def cmd_flight(msg):
+ if not len(msg.args):
+ raise IMException("please indicate a flight")
+
+ res = Response(channel=msg.channel, nick=msg.frm,
+ nomore="No more flights", count=" (%s more flights)")
+
+ for param in msg.args:
+ for flight in virtual_radar(param):
+ if 'Lat' in flight and 'Long' in flight:
+ loc = None
+ for location in mapquest.geocode('{Lat},{Long}'.format(**flight)):
+ loc = location
+ break
+ if loc:
+ res.append_message('\x02{0}\x0F: Position: \x02{1}\x0F, {2}'.format(flight['Call'], \
+ mapquest.where(loc), \
+ ', '.join(filter(None, flight_info(flight)))))
+ continue
+ res.append_message('\x02{0}\x0F: {1}'.format(flight['Call'], \
+ ', '.join(filter(None, flight_info(flight)))))
+ return res
diff --git a/modules/watchWebsite.xml b/modules/watchWebsite.xml
deleted file mode 100644
index 7a116e9..0000000
--- a/modules/watchWebsite.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py
deleted file mode 100644
index 1f69158..0000000
--- a/modules/watchWebsite/__init__.py
+++ /dev/null
@@ -1,181 +0,0 @@
-# coding=utf-8
-
-from datetime import datetime
-from datetime import timedelta
-import http.client
-import hashlib
-import re
-import socket
-import sys
-import urllib.parse
-from urllib.parse import urlparse
-from urllib.request import urlopen
-
-from .atom import Atom
-
-nemubotversion = 3.3
-
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "Alert on changes on websites"
-
-def help_full ():
- return "This module is autonomous you can't interract with it."
-
-def load(context):
- """Register watched website"""
- DATAS.setIndex("url", "watch")
- for site in DATAS.getNodes("watch"):
- if site.hasNode("alert"):
- start_watching(site)
- else:
- print("No alert defined for this site: " + site["url"])
- #DATAS.delChild(site)
-
-def unload(context):
- """Unregister watched website"""
- # Useless in 3.3?
-# for site in DATAS.getNodes("watch"):
-# context.del_event(site["evt_id"])
- pass
-
-def getPageContent(url):
- """Returns the content of the given url"""
- print_debug("Get page %s" % url)
- try:
- raw = urlopen(url, timeout=15)
- return raw.read().decode()
- except:
- return None
-
-def start_watching(site):
- o = urlparse(site["url"], "http")
- print_debug("Add event for site: %s" % o.netloc)
- evt = ModuleEvent(func=getPageContent, cmp_data=site["lastcontent"],
- func_data=site["url"],
- intervalle=site.getInt("time"),
- call=alert_change, call_data=site)
- site["_evt_id"] = add_event(evt)
-
-
-def del_site(msg):
- if len(msg.cmds) <= 1:
- return Response(msg.sender, "quel site dois-je arrêter de surveiller ?",
- msg.channel, msg.nick)
-
- url = msg.cmds[1]
-
- o = urlparse(url, "http")
- if o.scheme != "" and url in DATAS.index:
- site = DATAS.index[url]
- for a in site.getNodes("alert"):
- if a["channel"] == msg.channel:
- if (msg.sender == a["sender"] or msg.is_owner):
- site.delChild(a)
- if not site.hasNode("alert"):
- del_event(site["_evt_id"])
- DATAS.delChild(site)
- save()
- return Response(msg.sender,
- "je ne surveille désormais plus cette URL.",
- channel=msg.channel, nick=msg.nick)
- else:
- return Response(msg.sender,
- "Vous ne pouvez pas supprimer cette URL.",
- channel=msg.channel, nick=msg.nick)
- return Response(msg.sender,
- "je ne surveillais pas cette URL, impossible de la supprimer.",
- channel=msg.channel, nick=msg.nick)
- return Response(msg.sender, "je ne surveillais pas cette URL pour vous.",
- channel=msg.channel, nick=msg.nick)
-
-def add_site(msg, diffType="diff"):
- print (diffType)
- if len(msg.cmds) <= 1:
- return Response(msg.sender, "quel site dois-je surveiller ?",
- msg.channel, msg.nick)
-
- url = msg.cmds[1]
-
- o = urlparse(url, "http")
- if o.netloc != "":
- alert = ModuleState("alert")
- alert["sender"] = msg.sender
- alert["server"] = msg.server
- alert["channel"] = msg.channel
- alert["message"] = "%s a changé !" % url
-
- if url not in DATAS.index:
- watch = ModuleState("watch")
- watch["type"] = diffType
- watch["url"] = url
- watch["time"] = 123
- DATAS.addChild(watch)
- watch.addChild(alert)
- start_watching(watch)
- else:
- DATAS.index[url].addChild(alert)
- else:
- return Response(msg.sender, "je ne peux pas surveiller cette URL",
- channel=msg.channel, nick=msg.nick)
-
- save()
- return Response(msg.sender, channel=msg.channel, nick=msg.nick,
- message="ce site est maintenant sous ma surveillance.")
-
-def format_response(site, link='%s', title='%s', categ='%s'):
- for a in site.getNodes("alert"):
- send_response(a["server"], Response(a["sender"], a["message"].format(url=site["url"], link=link, title=title, categ=categ),
- channel=a["channel"], server=a["server"]))
-
-def alert_change(content, site):
- """Alert when a change is detected"""
- if site["type"] == "updown":
- if site["lastcontent"] is None:
- site["lastcontent"] = content is not None
-
- if (content is not None) != site.getBool("lastcontent"):
- format_response(site, link=site["url"])
- site["lastcontent"] = content is not None
- start_watching(site)
- return
-
- if content is None:
- start_watching(site)
- return
-
- if site["type"] == "atom":
- if site["_lastpage"] is None:
- if site["lastcontent"] is None or site["lastcontent"] == "":
- site["lastcontent"] = content
- site["_lastpage"] = Atom(site["lastcontent"])
- try:
- page = Atom(content)
- except:
- print ("An error occurs during Atom parsing. Restart event...")
- start_watching(site)
- return
- diff = site["_lastpage"].diff(page)
- if len(diff) > 0:
- site["_lastpage"] = page
- diff.reverse()
- for d in diff:
- site.setIndex("term", "category")
- categories = site.index
-
- if len(categories) > 0:
- if d.category is None or d.category not in categories:
- format_response(site, link=d.link, categ=categories[""]["part"], title=d.title)
- else:
- format_response(site, link=d.link, categ=categories[d.category]["part"], title=d.title)
- else:
- format_response(site, link=d.link, title=urllib.parse.unquote(d.title))
- else:
- start_watching(site)
- return #Stop here, no changes, so don't save
-
- else: # Just looking for any changes
- format_response(site, link=site["url"])
- site["lastcontent"] = content
- start_watching(site)
- save()
diff --git a/modules/watchWebsite/atom.py b/modules/watchWebsite/atom.py
deleted file mode 100755
index 30272e0..0000000
--- a/modules/watchWebsite/atom.py
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/usr/bin/python3
-# coding=utf-8
-
-import time
-from xml.dom.minidom import parse
-from xml.dom.minidom import parseString
-from xml.dom.minidom import getDOMImplementation
-
-class AtomEntry:
- def __init__ (self, node):
- self.id = node.getElementsByTagName("id")[0].firstChild.nodeValue
- if node.getElementsByTagName("title")[0].firstChild is not None:
- self.title = node.getElementsByTagName("title")[0].firstChild.nodeValue
- else:
- self.title = ""
- try:
- self.updated = time.strptime(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:19], "%Y-%m-%dT%H:%M:%S")
- except:
- try:
- self.updated = time.strptime(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:10], "%Y-%m-%d")
- except:
- print (node.getElementsByTagName("updated")[0].firstChild.nodeValue[:10])
- self.updated = time.localtime ()
- if len(node.getElementsByTagName("summary")) > 0 and node.getElementsByTagName("summary")[0].firstChild is not None:
- self.summary = node.getElementsByTagName("summary")[0].firstChild.nodeValue
- else:
- self.summary = None
- if len(node.getElementsByTagName("link")) > 0:
- self.link = node.getElementsByTagName("link")[0].getAttribute ("href")
- else:
- self.link = None
- if len (node.getElementsByTagName("category")) >= 1:
- self.category = node.getElementsByTagName("category")[0].getAttribute ("term")
- else:
- self.category = None
- if len (node.getElementsByTagName("link")) > 1:
- self.link2 = node.getElementsByTagName("link")[1].getAttribute ("href")
- else:
- self.link2 = None
-
-class Atom:
- def __init__ (self, string):
- self.raw = string
- self.feed = parseString (string).documentElement
- self.id = self.feed.getElementsByTagName("id")[0].firstChild.nodeValue
- self.title = self.feed.getElementsByTagName("title")[0].firstChild.nodeValue
-
- self.updated = None
- self.entries = dict ()
- for item in self.feed.getElementsByTagName("entry"):
- entry = AtomEntry (item)
- self.entries[entry.id] = entry
- if self.updated is None or self.updated < entry.updated:
- self.updated = entry.updated
-
- def __str__(self):
- return self.raw
-
- def diff (self, other):
- differ = list ()
- for k in other.entries.keys ():
- if self.updated is None and k not in self.entries:
- self.updated = other.entries[k].updated
- if k not in self.entries and other.entries[k].updated >= self.updated:
- differ.append (other.entries[k])
- return differ
-
-
-if __name__ == "__main__":
- content1 = ""
- with open("rss.php.1", "r") as f:
- for line in f:
- content1 += line
- content2 = ""
- with open("rss.php", "r") as f:
- for line in f:
- content2 += line
- a = Atom (content1)
- print (a.updated)
- b = Atom (content2)
- print (b.updated)
-
- diff = a.diff (b)
- print (diff)
diff --git a/modules/weather.py b/modules/weather.py
new file mode 100644
index 0000000..9b36470
--- /dev/null
+++ b/modules/weather.py
@@ -0,0 +1,261 @@
+# coding=utf-8
+
+"""The weather module. Powered by Dark Sky """
+
+import datetime
+import re
+
+from nemubot import context
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools import web
+from nemubot.tools.xmlparser.node import ModuleState
+
+from nemubot.module import mapquest
+
+nemubotversion = 4.0
+
+from nemubot.module.more import Response
+
+URL_DSAPI = "https://api.darksky.net/forecast/%s/%%s,%%s?lang=%%s&units=%%s"
+
+UNITS = {
+ "ca": {
+ "temperature": "°C",
+ "distance": "km",
+ "precipIntensity": "mm/h",
+ "precip": "cm",
+ "speed": "km/h",
+ "pressure": "hPa",
+ },
+ "uk2": {
+ "temperature": "°C",
+ "distance": "mi",
+ "precipIntensity": "mm/h",
+ "precip": "cm",
+ "speed": "mi/h",
+ "pressure": "hPa",
+ },
+ "us": {
+ "temperature": "°F",
+ "distance": "mi",
+ "precipIntensity": "in/h",
+ "precip": "in",
+ "speed": "mi/h",
+ "pressure": "mbar",
+ },
+ "si": {
+ "temperature": "°C",
+ "distance": "km",
+ "precipIntensity": "mm/h",
+ "precip": "cm",
+ "speed": "m/s",
+ "pressure": "hPa",
+ },
+}
+
+def load(context):
+ if not context.config or "darkskyapikey" not in context.config:
+ raise ImportError("You need a Dark-Sky API key in order to use this "
+ "module. Add it to the module configuration file:\n"
+ "\n"
+ "Register at https://developer.forecast.io/")
+ context.data.setIndex("name", "city")
+ global URL_DSAPI
+ URL_DSAPI = URL_DSAPI % context.config["darkskyapikey"]
+
+
+def format_wth(wth, flags):
+ units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"]
+ return ("{temperature} {units[temperature]} {summary}; precipitation ({precipProbability:.0%} chance) intensity: {precipIntensity} {units[precipIntensity]}; relative humidity: {humidity:.0%}; wind speed: {windSpeed} {units[speed]} {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} {units[pressure]}; ozone: {ozone} DU"
+ .format(units=units, **wth)
+ )
+
+
+def format_forecast_daily(wth, flags):
+ units = UNITS[flags["units"] if "units" in flags and flags["units"] in UNITS else "si"]
+ print(units)
+ return ("{summary}; between {temperatureMin}-{temperatureMax} {units[temperature]}; precipitation ({precipProbability:.0%} chance) intensity: maximum {precipIntensity} {units[precipIntensity]}; relative humidity: {humidity:.0%}; wind speed: {windSpeed} {units[speed]} {windBearing}°; cloud coverage: {cloudCover:.0%}; pressure: {pressure} {units[pressure]}; ozone: {ozone} DU".format(units=units, **wth))
+
+
+def format_timestamp(timestamp, tzname, tzoffset, format="%c"):
+ tz = datetime.timezone(datetime.timedelta(hours=tzoffset), tzname)
+ time = datetime.datetime.fromtimestamp(timestamp, tz=tz)
+ return time.strftime(format)
+
+
+def treat_coord(msg):
+ if len(msg.args) > 0:
+
+ # catch dans X[jh]$
+ if len(msg.args) > 2 and (msg.args[-2] == "dans" or msg.args[-2] == "in" or msg.args[-2] == "next"):
+ specific = msg.args[-1]
+ city = " ".join(msg.args[:-2]).lower()
+ else:
+ specific = None
+ city = " ".join(msg.args).lower()
+
+ if len(msg.args) == 2:
+ coords = msg.args
+ else:
+ coords = msg.args[0].split(",")
+
+ try:
+ if len(coords) == 2 and str(float(coords[0])) == coords[0] and str(float(coords[1])) == coords[1]:
+ return coords, specific
+ except ValueError:
+ pass
+
+ if city in context.data.index:
+ coords = list()
+ coords.append(context.data.index[city]["lat"])
+ coords.append(context.data.index[city]["long"])
+ return city, coords, specific
+
+ else:
+ geocode = [x for x in mapquest.geocode(city)]
+ if len(geocode):
+ coords = list()
+ coords.append(geocode[0]["latLng"]["lat"])
+ coords.append(geocode[0]["latLng"]["lng"])
+ return mapquest.where(geocode[0]), coords, specific
+
+ raise IMException("Je ne sais pas où se trouve %s." % city)
+
+ else:
+ raise IMException("indique-moi un nom de ville ou des coordonnées.")
+
+
+def get_json_weather(coords, lang="en", units="ca"):
+ wth = web.getJSON(URL_DSAPI % (float(coords[0]), float(coords[1]), lang, units))
+
+ # First read flags
+ if wth is None or "darksky-unavailable" in wth["flags"]:
+ raise IMException("The given location is supported but a temporary error (such as a radar station being down for maintenace) made data unavailable.")
+
+ return wth
+
+
+@hook.command("coordinates")
+def cmd_coordinates(msg):
+ if len(msg.args) < 1:
+ raise IMException("indique-moi un nom de ville.")
+
+ j = msg.args[0].lower()
+ if j not in context.data.index:
+ raise IMException("%s n'est pas une ville connue" % msg.args[0])
+
+ coords = context.data.index[j]
+ return Response("Les coordonnées de %s sont %s,%s" % (msg.args[0], coords["lat"], coords["long"]), channel=msg.channel)
+
+
+@hook.command("alert",
+ keywords={
+ "lang=LANG": "change the output language of weather sumarry; default: en",
+ "units=UNITS": "return weather conditions in the requested units; default: ca",
+ })
+def cmd_alert(msg):
+ loc, coords, specific = treat_coord(msg)
+ wth = get_json_weather(coords,
+ lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
+ units=msg.kwargs["units"] if "units" in msg.kwargs else "ca")
+
+ res = Response(channel=msg.channel, nomore="No more weather alert", count=" (%d more alerts)")
+
+ if "alerts" in wth:
+ for alert in wth["alerts"]:
+ if "expires" in alert:
+ res.append_message("\x03\x02%s\x03\x02 (see %s expire on %s): %s" % (alert["title"], alert["uri"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"]), alert["description"].replace("\n", " ")))
+ else:
+ res.append_message("\x03\x02%s\x03\x02 (see %s): %s" % (alert["title"], alert["uri"], alert["description"].replace("\n", " ")))
+
+ return res
+
+
+@hook.command("météo",
+ help="Display current weather and previsions",
+ help_usage={
+ "CITY": "Display the current weather and previsions in CITY",
+ },
+ keywords={
+ "lang=LANG": "change the output language of weather sumarry; default: en",
+ "units=UNITS": "return weather conditions in the requested units; default: ca",
+ })
+def cmd_weather(msg):
+ loc, coords, specific = treat_coord(msg)
+ wth = get_json_weather(coords,
+ lang=msg.kwargs["lang"] if "lang" in msg.kwargs else "en",
+ units=msg.kwargs["units"] if "units" in msg.kwargs else "ca")
+
+ res = Response(channel=msg.channel, nomore="No more weather information")
+
+ if "alerts" in wth:
+ alert_msgs = list()
+ for alert in wth["alerts"]:
+ if "expires" in alert:
+ alert_msgs.append("\x03\x02%s\x03\x02 expire on %s" % (alert["title"], format_timestamp(int(alert["expires"]), wth["timezone"], wth["offset"])))
+ else:
+ alert_msgs.append("\x03\x02%s\x03\x02" % (alert["title"]))
+ res.append_message("\x03\x16\x03\x02/!\\\x03\x02 Alert%s:\x03\x16 " % ("s" if len(alert_msgs) > 1 else "") + ", ".join(alert_msgs))
+
+ if specific is not None:
+ gr = re.match(r"^([0-9]*)\s*([a-zA-Z])", specific)
+ if gr is None or gr.group(1) == "":
+ gr1 = 1
+ else:
+ gr1 = int(gr.group(1))
+
+ if gr.group(2).lower() == "h" and gr1 < len(wth["hourly"]["data"]):
+ hour = wth["hourly"]["data"][gr1]
+ res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'), format_wth(hour, wth["flags"])))
+
+ elif gr.group(2).lower() == "d" and gr1 < len(wth["daily"]["data"]):
+ day = wth["daily"]["data"][gr1]
+ res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'), format_forecast_daily(day, wth["flags"])))
+
+ else:
+ res.append_message("I don't understand %s or information is not available" % specific)
+
+ else:
+ res.append_message("\x03\x02Currently:\x03\x02 " + format_wth(wth["currently"], wth["flags"]))
+
+ nextres = "\x03\x02Today:\x03\x02 %s " % wth["daily"]["data"][0]["summary"]
+ if "minutely" in wth:
+ nextres += "\x03\x02Next hour:\x03\x02 %s " % wth["minutely"]["summary"]
+ nextres += "\x03\x02Next 24 hours:\x03\x02 %s \x03\x02Next week:\x03\x02 %s" % (wth["hourly"]["summary"], wth["daily"]["summary"])
+ res.append_message(nextres)
+
+ for hour in wth["hourly"]["data"][1:4]:
+ res.append_message("\x03\x02At %sh:\x03\x02 %s" % (format_timestamp(int(hour["time"]), wth["timezone"], wth["offset"], '%H'),
+ format_wth(hour, wth["flags"])))
+
+ for day in wth["daily"]["data"][1:]:
+ res.append_message("\x03\x02On %s:\x03\x02 %s" % (format_timestamp(int(day["time"]), wth["timezone"], wth["offset"], '%A'),
+ format_forecast_daily(day, wth["flags"])))
+
+ return res
+
+
+gps_ask = re.compile(r"^\s*(?P.*\w)\s*(?:(?:se|est)\s+(?:trouve|situ[ée]*)\s+[aà])\s*(?P-?[0-9]+(?:[,.][0-9]+))[^0-9.](?P-?[0-9]+(?:[,.][0-9]+))\s*$", re.IGNORECASE)
+
+
+@hook.ask()
+def parseask(msg):
+ res = gps_ask.match(msg.message)
+ if res is not None:
+ city_name = res.group("city").lower()
+ gps_lat = res.group("lat").replace(",", ".")
+ gps_long = res.group("long").replace(",", ".")
+
+ if city_name in context.data.index:
+ context.data.index[city_name]["lat"] = gps_lat
+ context.data.index[city_name]["long"] = gps_long
+ else:
+ ms = ModuleState("city")
+ ms.setAttribute("name", city_name)
+ ms.setAttribute("lat", gps_lat)
+ ms.setAttribute("long", gps_long)
+ context.data.addChild(ms)
+ context.save()
+ return Response("ok, j'ai bien noté les coordonnées de %s" % res.group("city"),
+ msg.channel, msg.frm)
diff --git a/modules/whereis.xml b/modules/whereis.xml
deleted file mode 100644
index 90b2c2f..0000000
--- a/modules/whereis.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/modules/whereis/Delayed.py b/modules/whereis/Delayed.py
deleted file mode 100644
index 45826f4..0000000
--- a/modules/whereis/Delayed.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# coding=utf-8
-
-class Delayed:
- def __init__(self):
- self.names = dict()
diff --git a/modules/whereis/UpdatedStorage.py b/modules/whereis/UpdatedStorage.py
deleted file mode 100644
index de09848..0000000
--- a/modules/whereis/UpdatedStorage.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# coding=utf-8
-
-import socket
-from datetime import datetime
-from datetime import timedelta
-
-from .User import User
-
-class UpdatedStorage:
- def __init__(self, url, port):
- sock = connect_to_ns(url, port)
- self.users = dict()
- if sock != None:
- users = list_users(sock)
- if users is not None:
- for l in users:
- u = User(l)
- if u.login not in self.users:
- self.users[u.login] = list()
- self.users[u.login].append(u)
- self.lastUpdate = datetime.now ()
- else:
- self.users = None
- sock.close()
- else:
- self.users = None
-
- def update(self):
- if datetime.now () - self.lastUpdate < timedelta(minutes=10):
- return self
- else:
- return None
-
-
-def connect_to_ns(server, port):
- try:
- s = socket.socket()
- s.settimeout(3)
- s.connect((server, port))
- except socket.error:
- return None
- s.recv(8192)
- return s
-
-
-def list_users(sock):
- try:
- sock.send('list_users\n'.encode())
- buf = ''
- while True:
- tmp = sock.recv(8192).decode()
- buf += tmp
- if '\nrep 002' in tmp or tmp == '':
- break
- return buf.split('\n')[:-2]
- except socket.error:
- return None
diff --git a/modules/whereis/User.py b/modules/whereis/User.py
deleted file mode 100644
index d4b48b4..0000000
--- a/modules/whereis/User.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# coding=utf-8
-
-class User(object):
- def __init__(self, line):
- fields = line.split()
- self.login = fields[1]
- self.ip = fields[2]
- self.location = fields[8]
- self.promo = fields[9]
-
- @property
- def sm(self):
- for sm in CONF.getNodes("sm"):
- if self.ip.startswith(sm["ip"]):
- return sm["name"]
- return None
-
- @property
- def poste(self):
- if self.sm is None:
- if self.ip.startswith('10.'):
- return 'quelque part sur le PIE (%s)'%self.ip
- else:
- return "chez lui"
- else:
- if self.ip.startswith('10.247') or self.ip.startswith('10.248') or self.ip.startswith('10.249') or self.ip.startswith('10.250'):
- return "en " + self.sm + " rangée " + self.ip.split('.')[2] + " poste " + self.ip.split('.')[3]
- else:
- return "en " + self.sm
-
- def __cmp__(self, other):
- return cmp(self.login, other.login)
-
- def __hash__(self):
- return hash(self.login)
diff --git a/modules/whereis/__init__.py b/modules/whereis/__init__.py
deleted file mode 100644
index 57ebb73..0000000
--- a/modules/whereis/__init__.py
+++ /dev/null
@@ -1,206 +0,0 @@
-# coding=utf-8
-
-import re
-import sys
-import socket
-import time
-import _thread
-import threading
-from datetime import datetime
-from datetime import date
-from datetime import timedelta
-from urllib.parse import unquote
-
-from module_state import ModuleState
-
-from . import User
-from .UpdatedStorage import UpdatedStorage
-from .Delayed import Delayed
-
-nemubotversion = 3.0
-
-THREAD = None
-search = list()
-
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "Find a user on the PIE"
-
-def help_full ():
- return "!whereis : gives the position of /who/.\n!whereare [ ...]: gives the position of these .\n!peoplein : gives the number of people in this /sm/.\n!ip : gets the IP adress of /who/.\n!whoison : gives the name or the number (if > 15) of people at this /location/.\n!whoisin : gives the name or the number of people in this /sm/"
-
-def load():
- global CONF
- User.CONF = CONF
-
-datas = None
-
-def startWhereis(msg):
- global datas, THREAD, search
- if datas is not None:
- datas = datas.update ()
- if datas is None:
- datas = UpdatedStorage(CONF.getNode("server")["url"], CONF.getNode("server").getInt("port"))
- if datas is None or datas.users is None:
- msg.send_chn("Hmm c'est embarassant, serait-ce la fin du monde ou juste netsoul qui est mort ?")
- return
-
- if msg.cmd[0] == "peoplein":
- peoplein(msg)
- elif msg.cmd[0] == "whoison" or msg.cmd[0] == "whoisin":
- whoison(msg)
- else:
- whereis_msg(msg)
-
- THREAD = None
- if len(search) > 0:
- startWhereis(search.pop())
-
-def peoplein(msg):
- if len(msg.cmd) > 1:
- for sm in msg.cmd:
- sm = sm.lower()
- if sm == "peoplein":
- continue
- else:
- count = 0
- for userC in datas.users:
- for user in datas.users[userC]:
- usersm = user.sm
- if usersm is not None and usersm.lower() == sm:
- count += 1
- if count > 1:
- sOrNot = "s"
- else:
- sOrNot = ""
- msg.send_chn ("Il y a %d personne%s en %s." % (count, sOrNot, sm))
-
-def whoison(msg):
- if len(msg.cmd) > 1:
- for pb in msg.cmd:
- pc = pb.lower()
- if pc == "whoison" or pc == "whoisin":
- continue
- else:
- found = list()
- for userC in datas.users:
- for user in datas.users[userC]:
- if (msg.cmd[0] == "whoison" and (user.ip[:len(pc)] == pc or user.location.lower() == pc)) or (msg.cmd[0] == "whoisin" and user.sm == pc):
- found.append(user.login)
- if len(found) > 0:
- if len(found) <= 15:
- if pc == "whoisin":
- msg.send_chn ("En %s, il y a %s" % (pb, ", ".join(found)))
- else:
- msg.send_chn ("%s correspond à %s" % (pb, ", ".join(found)))
- else:
- msg.send_chn ("%s: %d personnes" % (pb, len(found)))
- else:
- msg.send_chn ("%s: personne ne match ta demande :(" % (msg.nick))
-
-DELAYED = dict()
-delayEvnt = threading.Event()
-
-def whereis_msg(msg):
- names = list()
- for name in msg.cmd:
- if name == "whereis" or name == "whereare" or name == "ouest" or name == "ousont" or name == "ip":
- if len(msg.cmd) >= 2:
- continue
- else:
- name = msg.nick
- else:
- names.append(name)
- pasla = whereis(msg, names)
- if len(pasla) > 0:
- global DELAYED
- DELAYED[msg] = Delayed()
- for name in pasla:
- DELAYED[msg].names[name] = None
- #msg.srv.send_msg_prtn ("~whois %s" % name)
- msg.srv.send_msg_prtn ("~whois %s" % " ".join(pasla))
- startTime = datetime.now()
- names = list()
- while len(DELAYED[msg].names) > 0 and startTime + timedelta(seconds=4) > datetime.now():
- delayEvnt.clear()
- delayEvnt.wait(2)
- rem = list()
- for name in DELAYED[msg].names.keys():
- if DELAYED[msg].names[name] is not None:
- pasla = whereis(msg, (DELAYED[msg].names[name],))
- if len(pasla) != 0:
- names.append(pasla[0])
- rem.append(name)
- for r in rem:
- del DELAYED[msg].names[r]
- for name in DELAYED[msg].names.keys():
- if DELAYED[msg].names[name] is None:
- names.append(name)
- else:
- names.append(DELAYED[msg].names[name])
- if len(names) > 1:
- msg.send_chn ("%s ne sont pas connectés sur le PIE." % (", ".join(names)))
- else:
- for name in names:
- msg.send_chn ("%s n'est pas connecté sur le PIE." % name)
-
-
-def whereis(msg, names):
- pasla = list()
-
- for name in names:
- if name in datas.users:
- if msg.cmd[0] == "ip":
- if len(datas.users[name]) == 1:
- msg.send_chn ("L'ip de %s est %s." %(name, datas.users[name][0].ip))
- else:
- out = ""
- for local in datas.users[name]:
- out += ", " + local.ip
- msg.send_chn ("%s est connecté à plusieurs endroits : %s." %(name, out[2:]))
- else:
- if len(datas.users[name]) == 1:
- msg.send_chn ("%s est %s (%s)." %(name, datas.users[name][0].poste, unquote(datas.users[name][0].location)))
- else:
- out = ""
- for local in datas.users[name]:
- out += ", " + local.poste + " (" + unquote(local.location) + ")"
- msg.send_chn ("%s est %s." %(name, out[2:]))
- else:
- pasla.append(name)
-
- return pasla
-
-
-def parseanswer (msg):
- global datas, THREAD, search
- if msg.cmd[0] == "whereis" or msg.cmd[0] == "whereare" or msg.cmd[0] == "ouest" or msg.cmd[0] == "ousont" or msg.cmd[0] == "ip" or msg.cmd[0] == "peoplein" or msg.cmd[0] == "whoison" or msg.cmd[0] == "whoisin":
- if len(msg.cmd) > 10:
- msg.send_snd ("Demande moi moins de personnes à la fois dans ton !%s" % msg.cmd[0])
- return True
-
- if THREAD is None:
- THREAD = _thread.start_new_thread (startWhereis, (msg,))
- else:
- search.append(msg)
- return True
- return False
-
-def parseask (msg):
- if len(DELAYED) > 0 and msg.nick == msg.srv.partner:
- treat = False
- for part in msg.content.split(';'):
- if part is None:
- continue
- for d in DELAYED.keys():
- nKeys = list()
- for n in DELAYED[d].names.keys():
- nKeys.append(n)
- for n in nKeys:
- if DELAYED[d].names[n] is None and part.find(n) >= 0:
- result = re.match(".* est (.*[^.])\.?", part)
- if result is not None:
- DELAYED[d].names[n] = result.group(1)
- delayEvnt.set()
- return treat
- return False
diff --git a/modules/whois.py b/modules/whois.py
new file mode 100644
index 0000000..1a5f598
--- /dev/null
+++ b/modules/whois.py
@@ -0,0 +1,167 @@
+# coding=utf-8
+
+import json
+import re
+
+from nemubot import context
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools.xmlparser.node import ModuleState
+
+nemubotversion = 3.4
+
+from nemubot.module.more import Response
+from nemubot.module.networking.page import headers
+
+PASSWD_FILE = None
+# You can get one with: curl -b "sessionid=YOURSESSIONID" 'https://accounts.cri.epita.net/api/users/?limit=10000' > users.json
+APIEXTRACT_FILE = None
+
+def load(context):
+ global PASSWD_FILE
+ if not context.config or "passwd" not in context.config:
+ print("No passwd file given")
+ else:
+ PASSWD_FILE = context.config["passwd"]
+ print("passwd file loaded:", PASSWD_FILE)
+
+ global APIEXTRACT_FILE
+ if not context.config or "apiextract" not in context.config:
+ print("No passwd file given")
+ else:
+ APIEXTRACT_FILE = context.config["apiextract"]
+ print("JSON users file loaded:", APIEXTRACT_FILE)
+
+ if PASSWD_FILE is None and APIEXTRACT_FILE is None:
+ return None
+
+ if not context.data.hasNode("aliases"):
+ context.data.addChild(ModuleState("aliases"))
+ context.data.getNode("aliases").setIndex("from", "alias")
+
+ import nemubot.hooks
+ context.add_hook(nemubot.hooks.Command(cmd_whois, "whois", keywords={"lookup": "Perform a lookup of the begining of the login instead of an exact search."}),
+ "in","Command")
+
+class Login:
+
+ def __init__(self, line=None, login=None, uidNumber=None, firstname=None, lastname=None, promo=None, **kwargs):
+ if line is not None:
+ s = line.split(":")
+ self.login = s[0]
+ self.uid = s[2]
+ self.gid = s[3]
+ self.cn = s[4]
+ self.home = s[5]
+ else:
+ self.login = login
+ self.uid = uidNumber
+ self.promo = promo
+ self.cn = firstname + " " + lastname
+ try:
+ self.gid = "epita" + str(int(promo))
+ except:
+ self.gid = promo
+
+ def get_promo(self):
+ if hasattr(self, "promo"):
+ return self.promo
+ if hasattr(self, "home"):
+ try:
+ return self.home.split("/")[2].replace("_", " ")
+ except:
+ return self.gid
+
+ def get_photo(self):
+ for url in [ "https://photos.cri.epita.fr/%s", "https://intra-bocal.epitech.eu/trombi/%s.jpg" ]:
+ url = url % self.login
+ try:
+ _, status, _, _ = headers(url)
+ if status == 200:
+ return url
+ except:
+ logger.exception("On URL %s", url)
+ return None
+
+
+def login_lookup(login, search=False):
+ if login in context.data.getNode("aliases").index:
+ login = context.data.getNode("aliases").index[login]["to"]
+
+ if APIEXTRACT_FILE:
+ with open(APIEXTRACT_FILE, encoding="utf-8") as f:
+ api = json.load(f)
+ for l in api["results"]:
+ if (not search and l["login"] == login) or (search and (("login" in l and l["login"].find(login) != -1) or ("cn" in l and l["cn"].find(login) != -1) or ("uid" in l and str(l["uid"]) == login))):
+ yield Login(**l)
+
+ login_ = login + (":" if not search else "")
+ lsize = len(login_)
+
+ if PASSWD_FILE:
+ with open(PASSWD_FILE, encoding="iso-8859-15") as f:
+ for l in f.readlines():
+ if l[:lsize] == login_:
+ yield Login(l.strip())
+
+def cmd_whois(msg):
+ if len(msg.args) < 1:
+ raise IMException("Provide a name")
+
+ def format_response(t):
+ srch, l = t
+ if type(l) is Login:
+ pic = l.get_photo()
+ return "%s is %s (%s %s): %s%s" % (srch, l.cn.capitalize(), l.login, l.uid, l.get_promo(), " and looks like %s" % pic if pic is not None else "")
+ else:
+ return l % srch
+
+ res = Response(channel=msg.channel, count=" (%d more logins)", line_treat=format_response)
+ for srch in msg.args:
+ found = False
+ for l in login_lookup(srch, "lookup" in msg.kwargs):
+ found = True
+ res.append_message((srch, l))
+ if not found:
+ res.append_message((srch, "Unknown %s :("))
+ return res
+
+@hook.command("nicks")
+def cmd_nicks(msg):
+ if len(msg.args) < 1:
+ raise IMException("Provide a login")
+ nick = login_lookup(msg.args[0])
+ if nick is None:
+ nick = msg.args[0]
+ else:
+ nick = nick.login
+
+ nicks = []
+ for alias in context.data.getNode("aliases").getChilds():
+ if alias["to"] == nick:
+ nicks.append(alias["from"])
+ if len(nicks) >= 1:
+ return Response("%s is also known as %s." % (nick, ", ".join(nicks)), channel=msg.channel)
+ else:
+ return Response("%s has no known alias." % nick, channel=msg.channel)
+
+@hook.ask()
+def parseask(msg):
+ res = re.match(r"^(\S+)\s*('s|suis|est|is|was|were)\s+([a-zA-Z0-9_-]{3,8})$", msg.message, re.I)
+ if res is not None:
+ nick = res.group(1)
+ login = res.group(3)
+ if nick == "my" or nick == "I" or nick == "i" or nick == "je" or nick == "mon" or nick == "ma":
+ nick = msg.frm
+ if nick in context.data.getNode("aliases").index:
+ context.data.getNode("aliases").index[nick]["to"] = login
+ else:
+ ms = ModuleState("alias")
+ ms.setAttribute("from", nick)
+ ms.setAttribute("to", login)
+ context.data.getNode("aliases").addChild(ms)
+ context.save()
+ return Response("ok, c'est noté, %s est %s"
+ % (nick, login),
+ channel=msg.channel,
+ nick=msg.frm)
diff --git a/modules/wolframalpha.py b/modules/wolframalpha.py
new file mode 100644
index 0000000..fc83815
--- /dev/null
+++ b/modules/wolframalpha.py
@@ -0,0 +1,118 @@
+"""Performing search and calculation"""
+
+# PYTHON STUFFS #######################################################
+
+from urllib.parse import quote
+import re
+
+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 #############################################################
+
+URL_API = "https://api.wolframalpha.com/v2/query?input=%%s&format=plaintext&appid=%s"
+
+def load(context):
+ global URL_API
+ if not context.config or "apikey" not in context.config:
+ raise ImportError ("You need a Wolfram|Alpha API key in order to use "
+ "this module. Add it to the module configuration: "
+ "\n\n"
+ "Register at https://products.wolframalpha.com/api/")
+ URL_API = URL_API % quote(context.config["apikey"]).replace("%", "%%")
+
+
+# MODULE CORE #########################################################
+
+class WFAResults:
+
+ def __init__(self, terms):
+ self.wfares = web.getXML(URL_API % quote(terms),
+ timeout=12)
+
+
+ @property
+ def success(self):
+ try:
+ return self.wfares.documentElement.hasAttribute("success") and self.wfares.documentElement.getAttribute("success") == "true"
+ except:
+ return False
+
+
+ @property
+ def error(self):
+ if self.wfares is None:
+ return "An error occurs during computation."
+ elif self.wfares.documentElement.hasAttribute("error") and self.wfares.documentElement.getAttribute("error") == "true":
+ return ("An error occurs during computation: " +
+ self.wfares.getElementsByTagName("error")[0].getElementsByTagName("msg")[0].firstChild.nodeValue)
+ elif len(self.wfares.getElementsByTagName("didyoumeans")):
+ start = "Did you mean: "
+ tag = "didyoumean"
+ end = "?"
+ elif len(self.wfares.getElementsByTagName("tips")):
+ start = "Tips: "
+ tag = "tip"
+ end = ""
+ elif len(self.wfares.getElementsByTagName("relatedexamples")):
+ start = "Related examples: "
+ tag = "relatedexample"
+ end = ""
+ elif len(self.wfares.getElementsByTagName("futuretopic")):
+ return self.wfares.getElementsByTagName("futuretopic")[0].getAttribute("msg")
+ else:
+ return "An error occurs during computation"
+
+ proposal = list()
+ for dym in self.wfares.getElementsByTagName(tag):
+ if tag == "tip":
+ proposal.append(dym.getAttribute("text"))
+ elif tag == "relatedexample":
+ proposal.append(dym.getAttribute("desc"))
+ else:
+ proposal.append(dym.firstChild.nodeValue)
+
+ return start + ', '.join(proposal) + end
+
+
+ @property
+ def results(self):
+ for node in self.wfares.getElementsByTagName("pod"):
+ for subnode in node.getElementsByTagName("subpod"):
+ if subnode.getElementsByTagName("plaintext")[0].firstChild:
+ yield (node.getAttribute("title") +
+ ((" / " + subnode.getAttribute("title")) if subnode.getAttribute("title") else "") + ": " +
+ "; ".join(subnode.getElementsByTagName("plaintext")[0].firstChild.nodeValue.split("\n")))
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.command("calculate",
+ help="Perform search and calculation using WolframAlpha",
+ help_usage={
+ "TERM": "Look at the given term on WolframAlpha",
+ "CALCUL": "Perform the computation over WolframAlpha service",
+ })
+def calculate(msg):
+ if not len(msg.args):
+ raise IMException("Indicate a calcul to compute")
+
+ s = WFAResults(' '.join(msg.args))
+
+ if not s.success:
+ raise IMException(s.error)
+
+ res = Response(channel=msg.channel, nomore="No more results")
+
+ for result in s.results:
+ res.append_message(re.sub(r' +', ' ', result))
+ if len(res.messages):
+ res.messages.pop(0)
+
+ return res
diff --git a/modules/worldcup.py b/modules/worldcup.py
new file mode 100644
index 0000000..e72f1ac
--- /dev/null
+++ b/modules/worldcup.py
@@ -0,0 +1,216 @@
+# coding=utf-8
+
+"""The 2014,2018 football worldcup module"""
+
+from datetime import datetime, timezone
+from functools import partial
+import json
+import re
+from urllib.parse import quote
+from urllib.request import urlopen
+
+from nemubot import context
+from nemubot.event import ModuleEvent
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools.xmlparser.node import ModuleState
+
+nemubotversion = 3.4
+
+from nemubot.module.more import Response
+
+API_URL="http://worldcup.sfg.io/%s"
+
+def load(context):
+ context.add_event(ModuleEvent(func=partial(lambda url: urlopen(url, timeout=10).read().decode(), API_URL % "matches/current?by_date=DESC"), call=current_match_new_action, interval=30))
+
+
+def help_full ():
+ return "!worldcup: do something."
+
+
+def start_watch(msg):
+ w = ModuleState("watch")
+ w["server"] = msg.server
+ w["channel"] = msg.channel
+ w["proprio"] = msg.frm
+ w["start"] = datetime.now(timezone.utc)
+ context.data.addChild(w)
+ context.save()
+ raise IMException("This channel is now watching world cup events!")
+
+@hook.command("watch_worldcup")
+def cmd_watch(msg):
+
+ # Get current state
+ node = None
+ for n in context.data.getChilds():
+ if n["server"] == msg.server and n["channel"] == msg.channel:
+ node = n
+ break
+
+ if len(msg.args):
+ if msg.args[0] == "stop" and node is not None:
+ context.data.delChild(node)
+ context.save()
+ raise IMException("This channel will not anymore receives world cup events.")
+ elif msg.args[0] == "start" and node is None:
+ start_watch(msg)
+ else:
+ raise IMException("Use only start or stop as first argument")
+ else:
+ if node is None:
+ start_watch(msg)
+ else:
+ context.data.delChild(node)
+ context.save()
+ raise IMException("This channel will not anymore receives world cup events.")
+
+def current_match_new_action(matches):
+ def cmp(om, nm):
+ return len(nm) and (len(om) == 0 or len(nm[0]["home_team_events"]) != len(om[0]["home_team_events"]) or len(nm[0]["away_team_events"]) != len(om[0]["away_team_events"]))
+ context.add_event(ModuleEvent(func=partial(lambda url: json.loads(urlopen(url).read().decode()), API_URL % "matches/current?by_date=DESC"), cmp=partial(cmp, matches), call=current_match_new_action, interval=30))
+
+ for match in matches:
+ if is_valid(match):
+ events = sort_events(match["home_team"], match["away_team"], match["home_team_events"], match["away_team_events"])
+ msg = "Match %s vs. %s ; score %s - %s" % (match["home_team"]["country"], match["away_team"]["country"], match["home_team"]["goals"], match["away_team"]["goals"])
+
+ if len(events) > 0:
+ msg += " ; à la " + txt_event(events[0])
+
+ for n in context.data.getChilds():
+ context.send_response(n["server"], Response(msg, channel=n["channel"]))
+
+def is_int(s):
+ try:
+ int(s)
+ return True
+ except ValueError:
+ return False
+
+def sort_events(teamA, teamB, eventA, eventB):
+ res = []
+
+ for e in eventA:
+ e["team"] = teamA
+ res.append(e)
+ for e in eventB:
+ e["team"] = teamB
+ res.append(e)
+
+ return sorted(res, key=lambda evt: int(evt["time"][0:2]), reverse=True)
+
+def detail_event(evt):
+ if evt == "yellow-card":
+ return "carton jaune pour"
+ elif evt == "yellow-card-second":
+ return "second carton jaune pour"
+ elif evt == "red-card":
+ return "carton rouge pour"
+ elif evt == "substitution-in" or evt == "substitution-in halftime":
+ return "joueur entrant :"
+ elif evt == "substitution-out" or evt == "substitution-out halftime":
+ return "joueur sortant :"
+ elif evt == "goal":
+ return "but de"
+ elif evt == "goal-own":
+ return "but contre son camp de"
+ elif evt == "goal-penalty":
+ return "but (pénalty) de"
+ return evt + " par"
+
+def txt_event(e):
+ return "%s minute : %s %s (%s)" % (e["time"], detail_event(e["type_of_event"]), e["player"], e["team"]["code"])
+
+def prettify(match):
+ matchdate = datetime.strptime(match["datetime"].replace(':', ''), "%Y-%m-%dT%H%M%SZ").replace(tzinfo=timezone.utc)
+ if match["status"] == "future":
+ return ["Match à venir (%s) le %s : %s vs. %s" % (match["fifa_id"], matchdate.strftime("%A %d à %H:%M"), match["home_team"]["country"], match["away_team"]["country"])]
+ else:
+ msgs = list()
+ msg = ""
+ if match["status"] == "completed":
+ msg += "Match (%s) du %s terminé : " % (match["fifa_id"], matchdate.strftime("%A %d à %H:%M"))
+ else:
+ msg += "Match en cours (%s) depuis %d minutes : " % (match["fifa_id"], (datetime.now(tz=timezone.utc) - matchdate).total_seconds() / 60)
+
+ msg += "%s %d - %d %s" % (match["home_team"]["country"], match["home_team"]["goals"], match["away_team"]["goals"], match["away_team"]["country"])
+
+ events = sort_events(match["home_team"], match["away_team"], match["home_team_events"], match["away_team_events"])
+
+ if len(events) > 0:
+ msg += " ; dernière action, à la " + txt_event(events[0])
+ msgs.append(msg)
+
+ for e in events[1:]:
+ msgs.append("À la " + txt_event(e))
+ else:
+ msgs.append(msg)
+
+ return msgs
+
+
+def is_valid(match):
+ return isinstance(match, dict) and (
+ isinstance(match.get('home_team'), dict) and
+ 'goals' in match.get('home_team')
+ ) and (
+ isinstance(match.get('away_team'), dict) and
+ 'goals' in match.get('away_team')
+ ) or isinstance(match.get('group_id'), int)
+
+def get_match(url, matchid):
+ allm = get_matches(url)
+ for m in allm:
+ if int(m["fifa_id"]) == matchid:
+ return [ m ]
+
+def get_matches(url):
+ try:
+ raw = urlopen(url)
+ except:
+ raise IMException("requête invalide")
+ matches = json.loads(raw.read().decode())
+
+ for match in matches:
+ if is_valid(match):
+ yield match
+
+@hook.command("worldcup")
+def cmd_worldcup(msg):
+ res = Response(channel=msg.channel, nomore="No more match to display", count=" (%d more matches)")
+
+ url = None
+ if len(msg.args) == 1:
+ if msg.args[0] == "today" or msg.args[0] == "aujourd'hui":
+ url = "matches/today?by_date=ASC"
+ elif msg.args[0] == "tomorrow" or msg.args[0] == "demain":
+ url = "matches/tomorrow?by_date=ASC"
+ elif msg.args[0] == "all" or msg.args[0] == "tout" or msg.args[0] == "tous":
+ url = "matches/"
+ elif len(msg.args[0]) == 3:
+ url = "matches/country?fifa_code=%s&by_date=DESC" % msg.args[0]
+ elif is_int(msg.args[0]):
+ url = int(msg.args[0])
+ else:
+ raise IMException("unrecognized request; choose between 'today', 'tomorrow', a FIFA country code or a match identifier")
+
+ if url is None:
+ url = "matches/current?by_date=ASC"
+ res.nomore = "There is no match currently"
+
+ if isinstance(url, int):
+ matches = get_match(API_URL % "matches/", url)
+ else:
+ matches = [m for m in get_matches(API_URL % url)]
+
+ for match in matches:
+ if len(matches) == 1:
+ res.count = " (%d more actions)"
+ for m in prettify(match):
+ res.append_message(m)
+ else:
+ res.append_message(prettify(match)[0])
+
+ return res
diff --git a/modules/ycc.py b/modules/ycc.py
deleted file mode 100644
index 7180ba2..0000000
--- a/modules/ycc.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# coding=utf-8
-
-import re
-from urllib.parse import urlparse
-from urllib.parse import quote
-from urllib.request import urlopen
-
-nemubotversion = 3.3
-
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "Gets YCC urls"
-
-def help_full ():
- return "!ycc []: with an argument, reduce the given thanks to ycc.fr; without argument, reduce the last URL said on the current channel."
-
-def load(context):
- from hooks import Hook
- add_hook("cmd_hook", Hook(cmd_ycc, "ycc"))
- add_hook("all_post", Hook(parseresponse))
-
-LAST_URLS = dict()
-
-def gen_response(res, msg, srv):
- if res is None:
- return Response(msg.sender, "La situation est embarassante, il semblerait que YCC soit down :(", msg.channel)
- elif isinstance(res, str):
- return Response(msg.sender, "URL pour %s : %s" % (srv, res), msg.channel)
- else:
- return Response(msg.sender, "Mauvaise URL : %s" % srv, msg.channel)
-
-def cmd_ycc(msg):
- if len(msg.cmds) == 1:
- global LAST_URLS
- if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
- msg.cmds.append(LAST_URLS[msg.channel].pop())
- else:
- return Response(msg.sender, "Je n'ai pas d'autre URL à réduire.", msg.channel)
-
- if len(msg.cmds) < 6:
- res = list()
- for url in msg.cmds[1:]:
- o = urlparse(url, "http")
- if o.scheme != "":
- snd_url = "http://ycc.fr/redirection/create/" + quote(url, "/:%#@&=?")
- print_debug(snd_url)
- raw = urlopen(snd_url, timeout=10)
- if o.netloc == "":
- res.append(gen_response(raw.read().decode(), msg, o.scheme))
- else:
- res.append(gen_response(raw.read().decode(), msg, o.netloc))
- else:
- res.append(gen_response(False, msg, url))
- return res
- else:
- return Response(msg.sender, "je ne peux pas réduire autant d'URL "
- "d'un seul coup.", msg.channel, nick=msg.nick)
-
-def parselisten(msg):
- global LAST_URLS
- urls = re.findall("([a-zA-Z0-9+.-]+:(//)?[^ ]+)", msg.content)
- for (url, osef) in urls:
- o = urlparse(url)
- if o.scheme != "":
- if o.netloc == "ycc.fr" or (o.netloc == "" and len(o.path) < 10):
- continue
- if msg.channel not in LAST_URLS:
- LAST_URLS[msg.channel] = list()
- LAST_URLS[msg.channel].append(o.geturl())
- return False
-
-def parseresponse(res):
- parselisten(res)
- return True
diff --git a/modules/youtube-title.py b/modules/youtube-title.py
new file mode 100644
index 0000000..41b613a
--- /dev/null
+++ b/modules/youtube-title.py
@@ -0,0 +1,96 @@
+from urllib.parse import urlparse
+import re, json, subprocess
+
+from nemubot.exception import IMException
+from nemubot.hooks import hook
+from nemubot.tools.web import _getNormalizedURL, getURLContent
+from nemubot.module.more import Response
+
+"""Get information of youtube videos"""
+
+nemubotversion = 3.4
+
+def help_full():
+ return "!yt []: with an argument, get information about the given link; without arguments, use the latest link seen on the current channel."
+
+def _get_ytdl(links):
+ cmd = 'youtube-dl -j --'.split()
+ cmd.extend(links)
+ res = []
+ with subprocess.Popen(cmd, stdout=subprocess.PIPE) as p:
+ if p.wait() > 0:
+ raise IMException("Error while retrieving video information.")
+ for line in p.stdout.read().split(b"\n"):
+ localres = ''
+ if not line:
+ continue
+ info = json.loads(line.decode('utf-8'))
+ if info.get('fulltitle'):
+ localres += info['fulltitle']
+ elif info.get('title'):
+ localres += info['title']
+ else:
+ continue
+ if info.get('duration'):
+ d = info['duration']
+ localres += ' [{0}:{1:06.3f}]'.format(int(d/60), d%60)
+ if info.get('age_limit'):
+ localres += ' [-{}]'.format(info['age_limit'])
+ if info.get('uploader'):
+ localres += ' by {}'.format(info['uploader'])
+ if info.get('upload_date'):
+ localres += ' on {}'.format(info['upload_date'])
+ if info.get('description'):
+ localres += ': ' + info['description']
+ if info.get('webpage_url'):
+ localres += ' | ' + info['webpage_url']
+ res.append(localres)
+ if not res:
+ raise IMException("No video information to retrieve about this. Sorry!")
+ return res
+
+LAST_URLS = dict()
+
+
+@hook.command("yt")
+def get_info_yt(msg):
+ links = list()
+
+ if len(msg.args) <= 0:
+ global LAST_URLS
+ if msg.channel in LAST_URLS and len(LAST_URLS[msg.channel]) > 0:
+ links.append(LAST_URLS[msg.channel].pop())
+ else:
+ raise IMException("I don't have any youtube URL for now, please provide me one to get information!")
+ else:
+ for url in msg.args:
+ links.append(url)
+
+ data = _get_ytdl(links)
+ res = Response(channel=msg.channel)
+ for msg in data:
+ res.append_message(msg)
+ return res
+
+
+@hook.message()
+def parselisten(msg):
+ parseresponse(msg)
+ return None
+
+
+@hook.post()
+def parseresponse(msg):
+ global LAST_URLS
+ if hasattr(msg, "text") and msg.text and type(msg.text) == str:
+ urls = re.findall("([a-zA-Z0-9+.-]+:(?://)?[^ :]+)", msg.text)
+ for url in urls:
+ o = urlparse(_getNormalizedURL(url))
+ if o.scheme != "":
+ if o.netloc == "" and len(o.path) < 10:
+ continue
+ for recv in msg.to:
+ if recv not in LAST_URLS:
+ LAST_URLS[recv] = list()
+ LAST_URLS[recv].append(url)
+ return msg
diff --git a/modules/youtube.py b/modules/youtube.py
deleted file mode 100644
index f28ef77..0000000
--- a/modules/youtube.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# coding=utf-8
-
-import re
-import http.client
-
-idAtom = "http://musik.p0m.fr/atom.php?nemubot"
-URLS = dict ()
-
-def load_module(datas_path):
- """Load this module"""
- global URLS
- URLS = dict ()
-
-def save_module():
- """Save the dates"""
- return
-
-def help_tiny ():
- """Line inserted in the response to the command !help"""
- return "music extractor"
-
-def help_full ():
- return "To launch a convertion task, juste paste a youtube link (or compatible service) and wait for nemubot answer!"
-
-def parseanswer(msg):
- return False
-
-
-def parseask(msg):
- return False
-
-def parselisten (msg):
- global URLS
- matches = [".*(http://(www\.)?youtube.com/watch\?v=([a-zA-Z0-9_-]{11})).*",
- ".*(http://(www\.)?youtu.be/([a-zA-Z0-9_-]{11})).*"]
- for m in matches:
- res = re.match (m, msg.content)
- if res is not None:
- #print ("seen : %s"%res.group(1))
- URLS[res.group(1)] = msg
- conn = http.client.HTTPConnection("musik.p0m.fr", timeout=10)
- conn.request("GET", "/?nemubot&a=add&url=%s"%(res.group (1)))
- conn.getresponse()
- conn.close()
- return True
- return False
-
-def send_global (origin, msg):
- if origin in URLS:
- URLS[origin].send_chn (msg)
- del URLS[origin]
diff --git a/nemubot.py b/nemubot.py
deleted file mode 100755
index 5948304..0000000
--- a/nemubot.py
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/python3
-# -*- 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 .
-
-import sys
-import os
-import imp
-import traceback
-
-import bot
-import prompt
-from prompt.builtins import load_file
-import importer
-
-if __name__ == "__main__":
- # Create bot context
- context = bot.Bot(0, "FIXME")
-
- # Load the prompt
- prmpt = prompt.Prompt()
-
- # Register the hook for futur import
- import sys
- sys.meta_path.append(importer.ModuleFinder(context, prmpt))
-
- #Add modules dir path
- if os.path.isdir("./modules/"):
- context.add_modules_path(
- os.path.realpath(os.path.abspath("./modules/")))
-
- # Parse command line arguments
- if len(sys.argv) >= 2:
- for arg in sys.argv[1:]:
- if os.path.isdir(arg):
- context.add_modules_path(arg)
- else:
- load_file(arg, context)
-
- print ("Nemubot v%s ready, my PID is %i!" % (context.version_txt,
- os.getpid()))
- while prmpt.run(context):
- try:
- # Reload context
- imp.reload(bot)
- context = bot.hotswap(context)
- # Reload prompt
- imp.reload(prompt)
- prmpt = prompt.hotswap(prmpt)
- # Reload all other modules
- bot.reload()
- print ("\033[1;32mContext reloaded\033[0m, now in Nemubot %s" %
- context.version_txt)
- except:
- print ("\033[1;31mUnable to reload the prompt due to errors.\033[0"
- "m Fix them before trying to reload the prompt.")
- exc_type, exc_value, exc_traceback = sys.exc_info()
- sys.stderr.write (traceback.format_exception_only(exc_type,
- exc_value)[0])
-
- print ("\nWaiting for other threads shuts down...")
- sys.exit(0)
diff --git a/nemubot/__init__.py b/nemubot/__init__.py
new file mode 100644
index 0000000..62807c6
--- /dev/null
+++ b/nemubot/__init__.py
@@ -0,0 +1,148 @@
+# 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 .
+
+__version__ = '4.0.dev3'
+__author__ = 'nemunaire'
+
+from nemubot.modulecontext import _ModuleContext
+
+context = _ModuleContext()
+
+
+def requires_version(min=None, max=None):
+ """Raise ImportError if the current version is not in the given range
+
+ Keyword arguments:
+ min -- minimal compatible version
+ max -- last compatible version
+ """
+
+ from distutils.version import LooseVersion
+ if min is not None and LooseVersion(__version__) < LooseVersion(str(min)):
+ raise ImportError("Requires version above %s, "
+ "but this is nemubot v%s." % (str(min), __version__))
+ if max is not None and LooseVersion(__version__) > LooseVersion(str(max)):
+ raise ImportError("Requires version under %s, "
+ "but this is nemubot v%s." % (str(max), __version__))
+
+
+def attach(pidfile, socketfile):
+ import socket
+ import sys
+
+ # Read PID from pidfile
+ with open(pidfile, "r") as f:
+ pid = int(f.readline())
+
+ print("nemubot is launched with PID %d. Attaching to Unix socket at: %s" % (pid, socketfile))
+
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try:
+ sock.connect(socketfile)
+ except socket.error as e:
+ sys.stderr.write(str(e))
+ sys.stderr.write("\n")
+ return 1
+
+ import select
+ mypoll = select.poll()
+
+ mypoll.register(sys.stdin.fileno(), select.POLLIN | select.POLLPRI)
+ mypoll.register(sock.fileno(), select.POLLIN | select.POLLPRI)
+ try:
+ while True:
+ for fd, flag in mypoll.poll():
+ if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
+ sock.close()
+ print("Connection closed.")
+ return 1
+
+ if fd == sys.stdin.fileno():
+ line = sys.stdin.readline().strip()
+ if line == "exit" or line == "quit":
+ return 0
+ elif line == "reload":
+ import os, signal
+ os.kill(pid, signal.SIGHUP)
+ print("Reload signal sent. Please wait...")
+
+ elif line == "shutdown":
+ import os, signal
+ os.kill(pid, signal.SIGTERM)
+ print("Shutdown signal sent. Please wait...")
+
+ elif line == "kill":
+ import os, signal
+ os.kill(pid, signal.SIGKILL)
+ print("Signal sent...")
+ return 0
+
+ elif line == "stack" or line == "stacks":
+ import os, signal
+ os.kill(pid, signal.SIGUSR1)
+ print("Debug signal sent. Consult logs.")
+
+ else:
+ sock.send(line.encode() + b'\r\n')
+
+ if fd == sock.fileno():
+ sys.stdout.write(sock.recv(2048).decode())
+
+ except KeyboardInterrupt:
+ pass
+ except:
+ return 1
+ finally:
+ sock.close()
+ return 0
+
+
+def daemonize(socketfile=None):
+ """Detach the running process to run as a daemon
+ """
+
+ import os
+ import sys
+
+ try:
+ pid = os.fork()
+ if pid > 0:
+ sys.exit(0)
+ except OSError as err:
+ sys.stderr.write("Unable to fork: %s\n" % err)
+ sys.exit(1)
+
+ os.setsid()
+ os.umask(0)
+ os.chdir('/')
+
+ try:
+ pid = os.fork()
+ if pid > 0:
+ sys.exit(0)
+ except OSError as err:
+ sys.stderr.write("Unable to fork: %s\n" % err)
+ sys.exit(1)
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+ si = open(os.devnull, 'r')
+ so = open(os.devnull, 'a+')
+ se = open(os.devnull, 'a+')
+
+ os.dup2(si.fileno(), sys.stdin.fileno())
+ os.dup2(so.fileno(), sys.stdout.fileno())
+ os.dup2(se.fileno(), sys.stderr.fileno())
diff --git a/nemubot/__main__.py b/nemubot/__main__.py
new file mode 100644
index 0000000..7070639
--- /dev/null
+++ b/nemubot/__main__.py
@@ -0,0 +1,279 @@
+# Nemubot is a smart and modulable IM bot.
+# Copyright (C) 2012-2017 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 .
+
+def main():
+ import os
+ import signal
+ import sys
+
+ # Parse command line arguments
+ import argparse
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument("-a", "--no-connect", action="store_true",
+ help="disable auto-connect to servers at startup")
+
+ parser.add_argument("-v", "--verbose", action="count",
+ default=0,
+ help="verbosity level")
+
+ parser.add_argument("-V", "--version", action="store_true",
+ help="display nemubot version and exit")
+
+ parser.add_argument("-M", "--modules-path", nargs='*',
+ default=["./modules/"],
+ help="directory to use as modules store")
+
+ parser.add_argument("-A", "--no-attach", action="store_true",
+ help="don't attach after fork")
+
+ parser.add_argument("-d", "--debug", action="store_true",
+ help="don't deamonize, keep in foreground")
+
+ parser.add_argument("-P", "--pidfile", default="./nemubot.pid",
+ help="Path to the file where store PID")
+
+ parser.add_argument("-S", "--socketfile", default="./nemubot.sock",
+ help="path where open the socket for internal communication")
+
+ parser.add_argument("-l", "--logfile", default="./nemubot.log",
+ help="Path to store logs")
+
+ parser.add_argument("-m", "--module", nargs='*',
+ help="load given modules")
+
+ parser.add_argument("-D", "--data-path", default="./datas/",
+ help="path to use to save bot data")
+
+ parser.add_argument('files', metavar='FILE', nargs='*',
+ help="configuration files to load")
+
+ args = parser.parse_args()
+
+ import nemubot
+
+ if args.version:
+ print(nemubot.__version__)
+ sys.exit(0)
+
+ # Resolve relatives paths
+ args.data_path = os.path.abspath(os.path.expanduser(args.data_path))
+ args.pidfile = os.path.abspath(os.path.expanduser(args.pidfile)) if args.pidfile is not None and args.pidfile != "" else None
+ args.socketfile = os.path.abspath(os.path.expanduser(args.socketfile)) if args.socketfile is not None and args.socketfile != "" else None
+ args.logfile = os.path.abspath(os.path.expanduser(args.logfile))
+ args.files = [x for x in map(os.path.abspath, args.files)]
+ args.modules_path = [x for x in map(os.path.abspath, args.modules_path)]
+
+ # Prepare the attached client, before setting other stuff
+ if not args.debug and not args.no_attach and args.socketfile is not None and args.pidfile is not None:
+ try:
+ pid = os.fork()
+ if pid > 0:
+ import time
+ os.waitpid(pid, 0)
+ time.sleep(1)
+ from nemubot import attach
+ sys.exit(attach(args.pidfile, args.socketfile))
+ except OSError as err:
+ sys.stderr.write("Unable to fork: %s\n" % err)
+ sys.exit(1)
+
+ # Setup logging interface
+ import logging
+ logger = logging.getLogger("nemubot")
+ logger.setLevel(logging.DEBUG)
+
+ formatter = logging.Formatter(
+ '%(asctime)s %(name)s %(levelname)s %(message)s')
+
+ if args.debug:
+ ch = logging.StreamHandler()
+ ch.setFormatter(formatter)
+ if args.verbose < 2:
+ ch.setLevel(logging.INFO)
+ logger.addHandler(ch)
+
+ fh = logging.FileHandler(args.logfile)
+ fh.setFormatter(formatter)
+ logger.addHandler(fh)
+
+ # Check if an instance is already launched
+ if args.pidfile is not None and os.path.isfile(args.pidfile):
+ with open(args.pidfile, "r") as f:
+ pid = int(f.readline())
+ try:
+ os.kill(pid, 0)
+ except OSError:
+ pass
+ else:
+ from nemubot import attach
+ sys.exit(attach(args.pidfile, args.socketfile))
+
+ # Add modules dir paths
+ modules_paths = list()
+ for path in args.modules_path:
+ if os.path.isdir(path):
+ modules_paths.append(path)
+ else:
+ logger.error("%s is not a directory", path)
+
+ # Create bot context
+ from nemubot import datastore
+ from nemubot.bot import Bot
+ context = Bot(modules_paths=modules_paths,
+ data_store=datastore.XML(args.data_path),
+ debug=args.verbose > 0)
+
+ if args.no_connect:
+ context.noautoconnect = True
+
+ # Register the hook for futur import
+ from nemubot.importer import ModuleFinder
+ module_finder = ModuleFinder(context.modules_paths, context.add_module)
+ sys.meta_path.append(module_finder)
+
+ # Load requested configuration files
+ for path in args.files:
+ if not os.path.isfile(path):
+ logger.error("%s is not a readable file", path)
+ continue
+
+ config = load_config(path)
+
+ # Preset each server in this file
+ for server in config.servers:
+ # Add the server in the context
+ for i in [0,1,2,3]:
+ srv = server.server(config, trynb=i)
+ try:
+ if context.add_server(srv):
+ logger.info("Server '%s' successfully added.", srv.name)
+ else:
+ logger.error("Can't add server '%s'.", srv.name)
+ except Exception as e:
+ logger.error("Unable to connect to '%s': %s", srv.name, e)
+ continue
+ break
+
+ # Load module and their configuration
+ for mod in config.modules:
+ context.modules_configuration[mod.name] = mod
+ if mod.autoload:
+ try:
+ __import__("nemubot.module." + mod.name)
+ except:
+ logger.exception("Exception occurs when loading module"
+ " '%s'", mod.name)
+
+ # Load files asked by the configuration file
+ args.files += config.includes
+
+
+ if args.module:
+ for module in args.module:
+ __import__("nemubot.module." + module)
+
+ if args.socketfile:
+ from nemubot.server.socket import UnixSocketListener
+ context.add_server(UnixSocketListener(new_server_cb=context.add_server,
+ location=args.socketfile,
+ name="master_socket"))
+
+ # Daemonize
+ if not args.debug:
+ from nemubot import daemonize
+ daemonize(args.socketfile)
+
+ # Signals handling
+ def sigtermhandler(signum, frame):
+ """On SIGTERM and SIGINT, quit nicely"""
+ context.quit()
+ signal.signal(signal.SIGINT, sigtermhandler)
+ signal.signal(signal.SIGTERM, sigtermhandler)
+
+ def sighuphandler(signum, frame):
+ """On SIGHUP, perform a deep reload"""
+ nonlocal context
+
+ logger.debug("SIGHUP receive, iniate reload procedure...")
+
+ # Reload configuration file
+ for path in args.files:
+ if os.path.isfile(path):
+ sync_act("loadconf", path)
+ signal.signal(signal.SIGHUP, sighuphandler)
+
+ def sigusr1handler(signum, frame):
+ """On SIGHUSR1, display stacktraces"""
+ import threading, traceback
+ for threadId, stack in sys._current_frames().items():
+ thName = "#%d" % threadId
+ for th in threading.enumerate():
+ if th.ident == threadId:
+ thName = th.name
+ break
+ logger.debug("########### Thread %s:\n%s",
+ thName,
+ "".join(traceback.format_stack(stack)))
+ signal.signal(signal.SIGUSR1, sigusr1handler)
+
+ # Store PID to pidfile
+ if args.pidfile is not None:
+ with open(args.pidfile, "w+") as f:
+ f.write(str(os.getpid()))
+
+ # context can change when performing an hotswap, always join the latest context
+ oldcontext = None
+ while oldcontext != context:
+ oldcontext = context
+ context.start()
+ context.join()
+
+ # Wait for consumers
+ logger.info("Waiting for other threads shuts down...")
+ if args.debug:
+ sigusr1handler(0, None)
+ sys.exit(0)
+
+
+def load_config(filename):
+ """Load a configuration file
+
+ Arguments:
+ filename -- the path to the file to load
+ """
+
+ from nemubot.channel import Channel
+ from nemubot import config
+ from nemubot.tools.xmlparser import XMLParser
+
+ try:
+ p = XMLParser({
+ "nemubotconfig": config.Nemubot,
+ "server": config.Server,
+ "channel": Channel,
+ "module": config.Module,
+ "include": config.Include,
+ })
+ return p.parse_file(filename)
+ except:
+ logger.exception("Can't load `%s'; this is not a valid nemubot "
+ "configuration file.", filename)
+ return None
+
+
+if __name__ == "__main__":
+ main()
diff --git a/nemubot/bot.py b/nemubot/bot.py
new file mode 100644
index 0000000..2b6e15c
--- /dev/null
+++ b/nemubot/bot.py
@@ -0,0 +1,548 @@
+# 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 .
+
+from datetime import datetime, timezone
+import logging
+from multiprocessing import JoinableQueue
+import threading
+import select
+import sys
+import weakref
+
+from nemubot import __version__
+from nemubot.consumer import Consumer, EventConsumer, MessageConsumer
+from nemubot import datastore
+import nemubot.hooks
+
+logger = logging.getLogger("nemubot")
+
+sync_queue = JoinableQueue()
+
+def sync_act(*args):
+ sync_queue.put(list(args))
+
+
+class Bot(threading.Thread):
+
+ """Class containing the bot context and ensuring key goals"""
+
+ def __init__(self, ip="127.0.0.1", modules_paths=list(),
+ data_store=datastore.Abstract(), debug=False):
+ """Initialize the bot context
+
+ Keyword arguments:
+ ip -- The external IP of the bot (default: 127.0.0.1)
+ modules_paths -- Paths to all directories where looking for modules
+ data_store -- An instance of the nemubot datastore for bot's modules
+ debug -- enable debug
+ """
+
+ super().__init__(name="Nemubot main")
+
+ logger.info("Initiate nemubot v%s (running on Python %s.%s.%s)",
+ __version__,
+ sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
+
+ self.debug = debug
+ self.stop = True
+
+ # External IP for accessing this bot
+ import ipaddress
+ self.ip = ipaddress.ip_address(ip)
+
+ # Context paths
+ self.modules_paths = modules_paths
+ self.datastore = data_store
+ self.datastore.open()
+
+ # Keep global context: servers and modules
+ self._poll = select.poll()
+ self.servers = dict()
+ self.modules = dict()
+ self.modules_configuration = dict()
+
+ # Events
+ self.events = list()
+ self.event_timer = None
+
+ # Own hooks
+ from nemubot.treatment import MessageTreater
+ self.treater = MessageTreater()
+
+ import re
+ def in_ping(msg):
+ return msg.respond("pong")
+ self.treater.hm.add_hook(nemubot.hooks.Message(in_ping,
+ match=lambda msg: re.match("^ *(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)",
+ msg.message, re.I)),
+ "in", "DirectAsk")
+
+ def in_echo(msg):
+ from nemubot.message import Text
+ return Text(msg.frm + ": " + " ".join(msg.args), to=msg.to_response)
+ self.treater.hm.add_hook(nemubot.hooks.Command(in_echo, "echo"), "in", "Command")
+
+ def _help_msg(msg):
+ """Parse and response to help messages"""
+ from nemubot.module.more import Response
+ res = Response(channel=msg.to_response)
+ if len(msg.args) >= 1:
+ if "nemubot.module." + msg.args[0] in self.modules and self.modules["nemubot.module." + msg.args[0]]() is not None:
+ mname = "nemubot.module." + msg.args[0]
+ if hasattr(self.modules[mname](), "help_full"):
+ hlp = self.modules[mname]().help_full()
+ if isinstance(hlp, Response):
+ return hlp
+ else:
+ res.append_message(hlp)
+ else:
+ res.append_message([str(h) for s,h in self.modules[mname]().__nemubot_context__.hooks], title="Available commands for module " + msg.args[0])
+ elif msg.args[0][0] == "!":
+ from nemubot.message.command import Command
+ for h in self.treater._in_hooks(Command(msg.args[0][1:])):
+ if h.help_usage:
+ lp = ["\x03\x02%s%s\x03\x02: %s" % (msg.args[0], (" " + k if k is not None else ""), h.help_usage[k]) for k in h.help_usage]
+ jp = h.keywords.help()
+ return res.append_message(lp + ([". Moreover, you can provides some optional parameters: "] + jp if len(jp) else []), title="Usage for command %s" % msg.args[0])
+ elif h.help:
+ return res.append_message("Command %s: %s" % (msg.args[0], h.help))
+ else:
+ return res.append_message("Sorry, there is currently no help for the command %s. Feel free to make a pull request at https://github.com/nemunaire/nemubot/compare" % msg.args[0])
+ res.append_message("Sorry, there is no command %s" % msg.args[0])
+ else:
+ res.append_message("Sorry, there is no module named %s" % msg.args[0])
+ 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 : "
+ "https://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, self.modules[im]().__doc__) for im in self.modules if self.modules[im]() is not None and self.modules[im]().__doc__])
+ return res
+ self.treater.hm.add_hook(nemubot.hooks.Command(_help_msg, "help"), "in", "Command")
+
+ import os
+ from queue import Queue
+ # Messages to be treated — shared across all server connections.
+ # cnsr_active tracks consumers currently inside stm.run() (not idle),
+ # which lets us spawn a new thread the moment all existing ones are busy.
+ self.cnsr_queue = Queue()
+ self.cnsr_thrd = list()
+ self.cnsr_lock = threading.Lock()
+ self.cnsr_active = 0 # consumers currently executing a task
+ self.cnsr_max = os.cpu_count() or 4 # upper bound on concurrent consumer threads
+
+
+ def __del__(self):
+ self.datastore.close()
+
+
+ def run(self):
+ global sync_queue
+
+ # Rewrite the sync_queue, as the daemonization process tend to disturb it
+ old_sync_queue, sync_queue = sync_queue, JoinableQueue()
+ while not old_sync_queue.empty():
+ sync_queue.put_nowait(old_sync_queue.get())
+
+ self._poll.register(sync_queue._reader, select.POLLIN | select.POLLPRI)
+
+
+ self.stop = False
+
+ # Relaunch events
+ self._update_event_timer()
+
+ logger.info("Starting main loop")
+ while not self.stop:
+ for fd, flag in self._poll.poll():
+ # Handle internal socket passing orders
+ if fd != sync_queue._reader.fileno() and fd in self.servers:
+ srv = self.servers[fd]
+
+ if flag & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
+ try:
+ srv.exception(flag)
+ except:
+ logger.exception("Uncatched exception on server exception")
+
+ if srv.fileno() > 0:
+ if flag & (select.POLLOUT):
+ try:
+ srv.async_write()
+ except:
+ logger.exception("Uncatched exception on server write")
+
+ if flag & (select.POLLIN | select.POLLPRI):
+ try:
+ for i in srv.async_read():
+ self.receive_message(srv, i)
+ except:
+ logger.exception("Uncatched exception on server read")
+
+ else:
+ del self.servers[fd]
+
+
+ # Always check the sync queue
+ while not sync_queue.empty():
+ args = sync_queue.get()
+ action = args.pop(0)
+
+ logger.debug("Executing sync_queue action %s%s", action, args)
+
+ if action == "sckt" and len(args) >= 2:
+ try:
+ if args[0] == "write":
+ self._poll.modify(int(args[1]), select.POLLOUT | select.POLLIN | select.POLLPRI)
+ elif args[0] == "unwrite":
+ self._poll.modify(int(args[1]), select.POLLIN | select.POLLPRI)
+
+ elif args[0] == "register":
+ self._poll.register(int(args[1]), select.POLLIN | select.POLLPRI)
+ elif args[0] == "unregister":
+ try:
+ self._poll.unregister(int(args[1]))
+ except KeyError:
+ pass
+ except:
+ logger.exception("Unhandled excpetion during action:")
+
+ elif action == "exit":
+ self.quit()
+
+ elif action == "launch_consumer":
+ pass # This is treated after the loop
+
+ sync_queue.task_done()
+
+
+ # Spawn a new consumer whenever the queue has work and every
+ # existing consumer is already busy executing a task.
+ with self.cnsr_lock:
+ while (not self.cnsr_queue.empty()
+ and self.cnsr_active >= len(self.cnsr_thrd)
+ and len(self.cnsr_thrd) < self.cnsr_max):
+ c = Consumer(self)
+ self.cnsr_thrd.append(c)
+ c.start()
+ sync_queue = None
+ logger.info("Ending main loop")
+
+
+
+ # Events methods
+
+ def add_event(self, evt, eid=None, module_src=None):
+ """Register an event and return its identifiant for futur update
+
+ Return:
+ None if the event is not in the queue (eg. if it has been executed during the call) or
+ returns the event ID.
+
+ Argument:
+ evt -- The event object to add
+
+ Keyword arguments:
+ eid -- The desired event ID (object or string UUID)
+ module_src -- The module to which the event is attached to
+ """
+
+ import uuid
+
+ # Generate the event id if no given
+ if eid is None:
+ eid = uuid.uuid1()
+
+ # Fill the id field of the event
+ if type(eid) is uuid.UUID:
+ evt.id = str(eid)
+ else:
+ # Ok, this is quiet useless...
+ try:
+ evt.id = str(uuid.UUID(eid))
+ except ValueError:
+ evt.id = eid
+
+ # TODO: mutex here plz
+
+ # Add the event in its place
+ t = evt.current
+ i = 0 # sentinel
+ for i in range(0, len(self.events)):
+ if self.events[i].current > t:
+ break
+ self.events.insert(i, evt)
+
+ if i == 0 and not self.stop:
+ # First event changed, reset timer
+ self._update_event_timer()
+ if len(self.events) <= 0 or self.events[i] != evt:
+ # Our event has been executed and removed from queue
+ return None
+
+ # Register the event in the source module
+ if module_src is not None:
+ module_src.__nemubot_context__.events.append((evt, evt.id))
+ evt.module_src = module_src
+
+ logger.info("New event registered in %d position: %s", i, t)
+ return evt.id
+
+
+ def del_event(self, evt, module_src=None):
+ """Find and remove an event from list
+
+ Return:
+ True if the event has been found and removed, False else
+
+ Argument:
+ evt -- The ModuleEvent object to remove or just the event identifier
+
+ Keyword arguments:
+ module_src -- The module to which the event is attached to (ignored if evt is a ModuleEvent)
+ """
+
+ logger.info("Removing event: %s from %s", evt, module_src)
+
+ from nemubot.event import ModuleEvent
+ if type(evt) is ModuleEvent:
+ id = evt.id
+ module_src = evt.module_src
+ else:
+ id = evt
+
+ if len(self.events) > 0 and id == self.events[0].id:
+ if module_src is not None:
+ module_src.__nemubot_context__.events.remove((self.events[0], id))
+ self.events.remove(self.events[0])
+ self._update_event_timer()
+ return True
+
+ for evt in self.events:
+ if evt.id == id:
+ self.events.remove(evt)
+
+ if module_src is not None:
+ module_src.__nemubot_context__.events.remove((evt, evt.id))
+ return True
+ return False
+
+
+ def _update_event_timer(self):
+ """(Re)launch 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):
+ try:
+ remaining = self.events[0].time_left.total_seconds()
+ except:
+ logger.exception("An error occurs during event time calculation:")
+ self.events.pop(0)
+ return self._update_event_timer()
+
+ logger.debug("Update timer: next event in %d seconds", remaining)
+ self.event_timer = threading.Timer(remaining if remaining > 0 else 0, self._end_event_timer)
+ self.event_timer.start()
+
+ else:
+ logger.debug("Update timer: no timer left")
+
+
+ def _end_event_timer(self):
+ """Function called at the end of the event timer"""
+
+ while len(self.events) > 0 and datetime.now(timezone.utc) >= self.events[0].current:
+ evt = self.events.pop(0)
+ self.cnsr_queue.put_nowait(EventConsumer(evt))
+ sync_act("launch_consumer")
+
+ self._update_event_timer()
+
+
+ # Consumers methods
+
+ def add_server(self, srv, autoconnect=True):
+ """Add a new server to the context
+
+ Arguments:
+ srv -- a concrete AbstractServer instance
+ autoconnect -- connect after add?
+ """
+
+ fileno = srv.fileno()
+ if fileno not in self.servers:
+ self.servers[fileno] = srv
+ self.servers[srv.name] = srv
+ if autoconnect and not hasattr(self, "noautoconnect"):
+ srv.connect()
+ return True
+
+ else:
+ return False
+
+
+ # Modules methods
+
+ def import_module(self, name):
+ """Load a module
+
+ Argument:
+ name -- name of the module to load
+ """
+
+ if name in self.modules:
+ self.unload_module(name)
+
+ __import__(name)
+
+
+ def add_module(self, module):
+ """Add a module to the context, if already exists, unload the
+ old one before"""
+ module_name = module.__spec__.name if hasattr(module, "__spec__") else module.__name__
+
+ # Check if the module already exists
+ if module_name in self.modules:
+ self.unload_module(module_name)
+
+ # Overwrite print built-in
+ def prnt(*args):
+ if hasattr(module, "logger"):
+ module.logger.info(" ".join([str(s) for s in args]))
+ else:
+ logger.info("[%s] %s", module_name, " ".join([str(s) for s in args]))
+ module.print = prnt
+
+ # Create module context
+ from nemubot.modulecontext import _ModuleContext, ModuleContext
+ module.__nemubot_context__ = ModuleContext(self, module)
+
+ if not hasattr(module, "logger"):
+ module.logger = logging.getLogger("nemubot.module." + module_name)
+
+ # Replace imported context by real one
+ for attr in module.__dict__:
+ if attr != "__nemubot_context__" and type(module.__dict__[attr]) == _ModuleContext:
+ module.__dict__[attr] = module.__nemubot_context__
+
+ # Register decorated functions
+ import nemubot.hooks
+ for s, h in nemubot.hooks.hook.last_registered:
+ module.__nemubot_context__.add_hook(h, *s if isinstance(s, list) else s)
+ nemubot.hooks.hook.last_registered = []
+
+ # Launch the module
+ if hasattr(module, "load"):
+ try:
+ module.load(module.__nemubot_context__)
+ except:
+ module.__nemubot_context__.unload()
+ raise
+
+ # Save a reference to the module
+ self.modules[module_name] = weakref.ref(module)
+ logger.info("Module '%s' successfully loaded.", module_name)
+
+
+ def unload_module(self, name):
+ """Unload a module"""
+ if name in self.modules and self.modules[name]() is not None:
+ module = self.modules[name]()
+ module.print("Unloading module %s" % name)
+
+ # Call the user defined unload method
+ if hasattr(module, "unload"):
+ module.unload(self)
+ module.__nemubot_context__.unload()
+
+ # Remove from the nemubot dict
+ del self.modules[name]
+
+ # Remove from the Python dict
+ del sys.modules[name]
+ for mod in [i for i in sys.modules]:
+ if mod[:len(name) + 1] == name + ".":
+ logger.debug("Module '%s' also removed from system modules list.", mod)
+ del sys.modules[mod]
+
+ logger.info("Module `%s' successfully unloaded.", name)
+
+ return True
+ return False
+
+
+ def receive_message(self, srv, msg):
+ """Queued the message for treatment
+
+ Arguments:
+ srv -- The server where the message comes from
+ msg -- The message not parsed, as simple as possible
+ """
+
+ self.cnsr_queue.put_nowait(MessageConsumer(srv, msg))
+
+
+ def quit(self):
+ """Save and unload modules and disconnect servers"""
+
+ if self.event_timer is not None:
+ logger.info("Stop the event timer...")
+ self.event_timer.cancel()
+
+ logger.info("Save and unload all modules...")
+ for mod in [m for m in self.modules.keys()]:
+ self.unload_module(mod)
+
+ logger.info("Close all servers connection...")
+ for srv in [self.servers[k] for k in self.servers]:
+ srv.close()
+
+ logger.info("Stop consumers")
+ with self.cnsr_lock:
+ k = list(self.cnsr_thrd)
+ for cnsr in k:
+ cnsr.stop = True
+
+ if self.stop is False or sync_queue is not None:
+ self.stop = True
+ sync_act("end")
+ sync_queue.join()
+
+
+ # 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)
diff --git a/nemubot/channel.py b/nemubot/channel.py
new file mode 100644
index 0000000..835c22f
--- /dev/null
+++ b/nemubot/channel.py
@@ -0,0 +1,162 @@
+# 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 .
+
+import logging
+
+
+class Channel:
+
+ """A chat room"""
+
+ def __init__(self, name, password=None, encoding=None):
+ """Initialize the channel
+
+ Arguments:
+ name -- the channel name
+ password -- the optional password use to join it
+ encoding -- the optional encoding of the channel
+ """
+
+ self.name = name
+ self.password = password
+ self.encoding = encoding
+ self.people = dict()
+ self.topic = ""
+ self.logger = logging.getLogger("nemubot.channel." + name)
+
+ def treat(self, cmd, msg):
+ """Treat a incoming IRC command
+
+ Arguments:
+ cmd -- the command
+ msg -- the whole message
+ """
+
+ if cmd == "353":
+ self.parse353(msg)
+ elif cmd == "332":
+ self.parse332(msg)
+ elif cmd == "MODE":
+ self.mode(msg)
+ elif cmd == "JOIN":
+ self.join(msg.frm)
+ elif cmd == "NICK":
+ self.nick(msg.frm, msg.text)
+ elif cmd == "PART" or cmd == "QUIT":
+ self.part(msg.frm)
+ elif cmd == "TOPIC":
+ self.topic = self.text
+
+ def join(self, nick, level=0):
+ """Someone join the channel
+
+ Argument:
+ nick -- nickname of the user joining the channel
+ level -- authorization level of the user
+ """
+
+ self.logger.debug("%s join", nick)
+ self.people[nick] = level
+
+ def chtopic(self, newtopic):
+ """Send command to change the topic
+
+ Arguments:
+ newtopic -- the new topic of the channel
+ """
+
+ self.srv.send_msg(self.name, newtopic, "TOPIC")
+ self.topic = newtopic
+
+ def nick(self, oldnick, newnick):
+ """Someone change his nick
+
+ Arguments:
+ oldnick -- the previous nick of the user
+ newnick -- the new nick of the user
+ """
+
+ if oldnick in self.people:
+ self.logger.debug("%s switch nick to %s on", oldnick, newnick)
+ lvl = self.people[oldnick]
+ del self.people[oldnick]
+ self.people[newnick] = lvl
+
+ def part(self, nick):
+ """Someone leave the channel
+
+ Argument:
+ nick -- name of the user that leave
+ """
+
+ if nick in self.people:
+ self.logger.debug("%s has left", nick)
+ del self.people[nick]
+
+ def mode(self, msg):
+ """Channel or user mode change
+
+ Argument:
+ msg -- the whole message
+ """
+ if msg.text[0] == "-k":
+ self.password = ""
+ elif msg.text[0] == "+k":
+ if len(msg.text) > 1:
+ self.password = ' '.join(msg.text[1:])[1:]
+ else:
+ self.password = msg.text[1]
+ elif msg.text[0] == "+o":
+ self.people[msg.frm] |= 4
+ elif msg.text[0] == "-o":
+ self.people[msg.frm] &= ~4
+ elif msg.text[0] == "+h":
+ self.people[msg.frm] |= 2
+ elif msg.text[0] == "-h":
+ self.people[msg.frm] &= ~2
+ elif msg.text[0] == "+v":
+ self.people[msg.frm] |= 1
+ elif msg.text[0] == "-v":
+ self.people[msg.frm] &= ~1
+
+ def parse332(self, msg):
+ """Parse RPL_TOPIC message
+
+ Argument:
+ msg -- the whole message
+ """
+
+ self.topic = msg.text
+
+ def parse353(self, msg):
+ """Parse RPL_ENDOFWHO message
+
+ Argument:
+ msg -- the whole message
+ """
+
+ for p in msg.text:
+ 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)
diff --git a/nemubot/config/__init__.py b/nemubot/config/__init__.py
new file mode 100644
index 0000000..6bbc1b2
--- /dev/null
+++ b/nemubot/config/__init__.py
@@ -0,0 +1,26 @@
+# 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 .
+
+def get_boolean(s):
+ if isinstance(s, bool):
+ return s
+ else:
+ return (s and s != "0" and s.lower() != "false" and s.lower() != "off")
+
+from nemubot.config.include import Include
+from nemubot.config.module import Module
+from nemubot.config.nemubot import Nemubot
+from nemubot.config.server import Server
diff --git a/nemubot/config/include.py b/nemubot/config/include.py
new file mode 100644
index 0000000..408c09a
--- /dev/null
+++ b/nemubot/config/include.py
@@ -0,0 +1,20 @@
+# 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 .
+
+class Include:
+
+ def __init__(self, path):
+ self.path = path
diff --git a/nemubot/config/module.py b/nemubot/config/module.py
new file mode 100644
index 0000000..ab51971
--- /dev/null
+++ b/nemubot/config/module.py
@@ -0,0 +1,26 @@
+# 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 .
+
+from nemubot.config import get_boolean
+from nemubot.tools.xmlparser.genericnode import GenericNode
+
+
+class Module(GenericNode):
+
+ def __init__(self, name, autoload=True, **kwargs):
+ super().__init__(None, **kwargs)
+ self.name = name
+ self.autoload = get_boolean(autoload)
diff --git a/nemubot/config/nemubot.py b/nemubot/config/nemubot.py
new file mode 100644
index 0000000..992cd8e
--- /dev/null
+++ b/nemubot/config/nemubot.py
@@ -0,0 +1,46 @@
+# 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 .
+
+from nemubot.config.include import Include
+from nemubot.config.module import Module
+from nemubot.config.server import Server
+
+
+class Nemubot:
+
+ def __init__(self, nick="nemubot", realname="nemubot", owner=None,
+ ip=None, ssl=False, caps=None, encoding="utf-8"):
+ self.nick = nick
+ self.realname = realname
+ self.owner = owner
+ self.ip = ip
+ self.caps = caps.split(" ") if caps is not None else []
+ self.encoding = encoding
+ self.servers = []
+ self.modules = []
+ self.includes = []
+
+
+ def addChild(self, name, child):
+ if name == "module" and isinstance(child, Module):
+ self.modules.append(child)
+ return True
+ elif name == "server" and isinstance(child, Server):
+ self.servers.append(child)
+ return True
+ elif name == "include" and isinstance(child, Include):
+ self.includes.append(child)
+ return True
diff --git a/nemubot/config/server.py b/nemubot/config/server.py
new file mode 100644
index 0000000..17bfaee
--- /dev/null
+++ b/nemubot/config/server.py
@@ -0,0 +1,45 @@
+# 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 .
+
+from nemubot.channel import Channel
+
+
+class Server:
+
+ def __init__(self, uri="irc://nemubot@localhost/", autoconnect=True, caps=None, **kwargs):
+ self.uri = uri
+ self.autoconnect = autoconnect
+ self.caps = caps.split(" ") if caps is not None else []
+ self.args = kwargs
+ self.channels = []
+
+
+ def addChild(self, name, child):
+ if name == "channel" and isinstance(child, Channel):
+ self.channels.append(child)
+ return True
+
+
+ def server(self, parent, trynb=0):
+ from nemubot.server import factory
+
+ for a in ["nick", "owner", "realname", "encoding"]:
+ if a not in self.args:
+ self.args[a] = getattr(parent, a)
+
+ self.caps += parent.caps
+
+ return factory(self.uri, caps=self.caps, channels=self.channels, trynb=trynb, **self.args)
diff --git a/nemubot/consumer.py b/nemubot/consumer.py
new file mode 100644
index 0000000..a9a4146
--- /dev/null
+++ b/nemubot/consumer.py
@@ -0,0 +1,129 @@
+# 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 .
+
+import logging
+import queue
+import threading
+
+logger = logging.getLogger("nemubot.consumer")
+
+
+class MessageConsumer:
+
+ """Store a message before treating"""
+
+ def __init__(self, srv, msg):
+ self.srv = srv
+ self.orig = msg
+
+
+ def run(self, context):
+ """Create, parse and treat the message"""
+
+ from nemubot.bot import Bot
+ assert isinstance(context, Bot)
+
+ msgs = []
+
+ # Parse message
+ try:
+ for msg in self.srv.parse(self.orig):
+ msgs.append(msg)
+ except:
+ logger.exception("Error occurred during the processing of the %s: "
+ "%s", type(self.orig).__name__, self.orig)
+
+ # Treat message
+ for msg in msgs:
+ for res in context.treater.treat_msg(msg):
+ # Identify destination
+ to_server = None
+ if isinstance(res, str):
+ to_server = self.srv
+ elif not hasattr(res, "server"):
+ logger.error("No server defined for response of type %s: %s", type(res).__name__, res)
+ continue
+ elif res.server is None:
+ to_server = self.srv
+ res.server = self.srv.fileno()
+ elif res.server in context.servers:
+ to_server = context.servers[res.server]
+ else:
+ to_server = res.server
+
+ if to_server is None or not hasattr(to_server, "send_response") or not callable(to_server.send_response):
+ logger.error("The server defined in this response doesn't exist: %s", res.server)
+ continue
+
+ # Sent message
+ to_server.send_response(res)
+
+
+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.check()
+ except:
+ logger.exception("Error during event end")
+
+ # Reappend the event in the queue if it has next iteration
+ if self.evt.next is not None:
+ context.add_event(self.evt, eid=self.evt.id)
+
+ # Or remove reference of this event
+ elif (hasattr(self.evt, "module_src") and
+ self.evt.module_src is not None):
+ self.evt.module_src.__nemubot_context__.events.remove((self.evt, self.evt.id))
+
+
+
+class Consumer(threading.Thread):
+
+ """Dequeue and exec requested action"""
+
+ def __init__(self, context):
+ self.context = context
+ self.stop = False
+ super().__init__(name="Nemubot consumer", daemon=True)
+
+
+ def run(self):
+ try:
+ while not self.stop:
+ try:
+ stm = self.context.cnsr_queue.get(True, 1)
+ except queue.Empty:
+ break
+
+ with self.context.cnsr_lock:
+ self.context.cnsr_active += 1
+ try:
+ stm.run(self.context)
+ finally:
+ self.context.cnsr_queue.task_done()
+ with self.context.cnsr_lock:
+ self.context.cnsr_active -= 1
+ finally:
+ with self.context.cnsr_lock:
+ self.context.cnsr_thrd.remove(self)
diff --git a/nemubot/datastore/__init__.py b/nemubot/datastore/__init__.py
new file mode 100644
index 0000000..3e38ad2
--- /dev/null
+++ b/nemubot/datastore/__init__.py
@@ -0,0 +1,18 @@
+# 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 .
+
+from nemubot.datastore.abstract import Abstract
+from nemubot.datastore.xml import XML
diff --git a/nemubot/datastore/abstract.py b/nemubot/datastore/abstract.py
new file mode 100644
index 0000000..aeaecc6
--- /dev/null
+++ b/nemubot/datastore/abstract.py
@@ -0,0 +1,69 @@
+# 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 .
+
+class Abstract:
+
+ """Abstract implementation of a module data store, that always return an
+ empty set"""
+
+ def new(self):
+ """Initialize a new empty storage tree
+ """
+
+ from nemubot.tools.xmlparser import module_state
+ return module_state.ModuleState("nemubotstate")
+
+ def open(self):
+ return
+
+ def close(self):
+ return
+
+ def load(self, module, knodes):
+ """Load data for the given module
+
+ Argument:
+ module -- the module name of data to load
+ knodes -- the schema to use to load the datas
+
+ Return:
+ The loaded data
+ """
+
+ if knodes is not None:
+ return None
+
+ return self.new()
+
+ def save(self, module, data):
+ """Load data for the given module
+
+ Argument:
+ module -- the module name of data to load
+ data -- the new data to save
+
+ Return:
+ Saving status
+ """
+
+ return True
+
+ def __enter__(self):
+ self.open()
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.close()
diff --git a/nemubot/datastore/xml.py b/nemubot/datastore/xml.py
new file mode 100644
index 0000000..aa6cbd0
--- /dev/null
+++ b/nemubot/datastore/xml.py
@@ -0,0 +1,171 @@
+# 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 .
+
+import fcntl
+import logging
+import os
+import xml.parsers.expat
+
+from nemubot.datastore.abstract import Abstract
+
+logger = logging.getLogger("nemubot.datastore.xml")
+
+
+class XML(Abstract):
+
+ """A concrete implementation of a data store that relies on XML files"""
+
+ def __init__(self, basedir, rotate=True):
+ """Initialize the datastore
+
+ Arguments:
+ basedir -- path to directory containing XML files
+ rotate -- auto-backup files?
+ """
+
+ self.basedir = basedir
+ self.rotate = rotate
+ self.nb_save = 0
+
+ def open(self):
+ """Lock the directory"""
+
+ if not os.path.isdir(self.basedir):
+ os.mkdir(self.basedir)
+
+ lock_path = os.path.join(self.basedir, ".used_by_nemubot")
+
+ self.lock_file = open(lock_path, 'a+')
+ ok = True
+ try:
+ fcntl.lockf(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except OSError:
+ ok = False
+
+ if not ok:
+ with open(lock_path, 'r') as lf:
+ pid = lf.readline()
+ raise Exception("Data dir already locked, by PID %s" % pid)
+
+ self.lock_file.truncate()
+ self.lock_file.write(str(os.getpid()))
+ self.lock_file.flush()
+
+ return True
+
+ def close(self):
+ """Release a locked path"""
+
+ if hasattr(self, "lock_file"):
+ self.lock_file.close()
+ lock_path = os.path.join(self.basedir, ".used_by_nemubot")
+ if os.path.isdir(self.basedir) and os.path.exists(lock_path):
+ os.unlink(lock_path)
+ del self.lock_file
+ return True
+ return False
+
+ def _get_data_file_path(self, module):
+ """Get the path to the module data file"""
+
+ return os.path.join(self.basedir, module + ".xml")
+
+ def load(self, module, knodes):
+ """Load data for the given module
+
+ Argument:
+ module -- the module name of data to load
+ knodes -- the schema to use to load the datas
+ """
+
+ data_file = self._get_data_file_path(module)
+
+ if knodes is None:
+ from nemubot.tools.xmlparser import parse_file
+ def _true_load(path):
+ return parse_file(path)
+
+ else:
+ from nemubot.tools.xmlparser import XMLParser
+ p = XMLParser(knodes)
+ def _true_load(path):
+ return p.parse_file(path)
+
+ # Try to load original file
+ if os.path.isfile(data_file):
+ try:
+ return _true_load(data_file)
+ except xml.parsers.expat.ExpatError:
+ # Try to load from backup
+ for i in range(10):
+ path = data_file + "." + str(i)
+ if os.path.isfile(path):
+ try:
+ cnt = _true_load(path)
+
+ logger.warn("Restoring from backup: %s", path)
+
+ return cnt
+ except xml.parsers.expat.ExpatError:
+ continue
+
+ # Default case: initialize a new empty datastore
+ return super().load(module, knodes)
+
+ def _rotate(self, path):
+ """Backup given path
+
+ Argument:
+ path -- location of the file to backup
+ """
+
+ self.nb_save += 1
+
+ for i in range(10):
+ if self.nb_save % (1 << i) == 0:
+ src = path + "." + str(i-1) if i != 0 else path
+ dst = path + "." + str(i)
+ if os.path.isfile(src):
+ os.rename(src, dst)
+
+ def save(self, module, data):
+ """Load data for the given module
+
+ Argument:
+ module -- the module name of data to load
+ data -- the new data to save
+ """
+
+ path = self._get_data_file_path(module)
+
+ if self.rotate:
+ self._rotate(path)
+
+ if data is None:
+ return
+
+ import tempfile
+ _, tmpath = tempfile.mkstemp()
+ with open(tmpath, "w") as f:
+ import xml.sax.saxutils
+ gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
+ gen.startDocument()
+ data.saveElement(gen)
+ gen.endDocument()
+
+ # Atomic save
+ import shutil
+ shutil.move(tmpath, path)
diff --git a/nemubot/event/__init__.py b/nemubot/event/__init__.py
new file mode 100644
index 0000000..49c6902
--- /dev/null
+++ b/nemubot/event/__init__.py
@@ -0,0 +1,104 @@
+# 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 .
+
+from datetime import datetime, timedelta, timezone
+
+
+class ModuleEvent:
+
+ """Representation of a event initiated by a bot module"""
+
+ def __init__(self, call=None, func=None, cmp=None, interval=60, offset=0, times=1):
+
+ """Initialize the event
+
+ Keyword arguments:
+ call -- Function to call when the event is realized
+ func -- Function called to check
+ cmp -- Boolean function called to check changes or value to compare with
+ interval -- Time in seconds between each check (default: 60)
+ offset -- Time in seconds added to interval before the first check (default: 0)
+ times -- Number of times the event has to be realized before being removed; -1 for no limit (default: 1)
+ """
+
+ # What have we to check?
+ self.func = func
+
+ # How detect a change?
+ self.cmp = cmp
+
+ # What should we call when?
+ self.call = call
+
+ # Store times
+ if isinstance(offset, timedelta):
+ self.offset = offset # Time to wait before the first check
+ else:
+ self.offset = timedelta(seconds=offset) # Time to wait before the first check
+ if isinstance(interval, timedelta):
+ self.interval = interval
+ else:
+ self.interval = timedelta(seconds=interval)
+ self._end = None # Cache
+
+ # 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(timezone.utc) + self.offset + self.interval
+ 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(timezone.utc):
+ self._end += self.interval
+ 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(timezone.utc)
+ return timedelta.max
+
+ def check(self):
+ """Run a check and realized the event if this is time"""
+
+ # Get new data
+ if self.func is not None:
+ d_new = self.func()
+ else:
+ d_new = None
+
+ # then compare with current data
+ if self.cmp is None or (callable(self.cmp) and self.cmp(d_new)) or (not callable(self.cmp) and d_new != self.cmp):
+ self.times -= 1
+
+ # Call attended function
+ if self.func is not None:
+ self.call(d_new)
+ else:
+ self.call()
diff --git a/nemubot/exception/__init__.py b/nemubot/exception/__init__.py
new file mode 100644
index 0000000..84464a0
--- /dev/null
+++ b/nemubot/exception/__init__.py
@@ -0,0 +1,34 @@
+# 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 .
+
+class IMException(Exception):
+
+
+ def __init__(self, message, personnal=True):
+ super(IMException, self).__init__(message)
+ self.personnal = personnal
+
+
+ def fill_response(self, msg):
+ if self.personnal:
+ from nemubot.message import DirectAsk
+ return DirectAsk(msg.frm, *self.args,
+ server=msg.server, to=msg.to_response)
+
+ else:
+ from nemubot.message import Text
+ return Text(*self.args,
+ server=msg.server, to=msg.to_response)
diff --git a/nemubot/exception/keyword.py b/nemubot/exception/keyword.py
new file mode 100644
index 0000000..6e3c07f
--- /dev/null
+++ b/nemubot/exception/keyword.py
@@ -0,0 +1,23 @@
+# 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 .
+
+from nemubot.exception import IMException
+
+
+class KeywordException(IMException):
+
+ def __init__(self, message):
+ super(KeywordException, self).__init__(message)
diff --git a/nemubot/hooks/__init__.py b/nemubot/hooks/__init__.py
new file mode 100644
index 0000000..9024494
--- /dev/null
+++ b/nemubot/hooks/__init__.py
@@ -0,0 +1,51 @@
+# 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 .
+
+from nemubot.hooks.abstract import Abstract
+from nemubot.hooks.command import Command
+from nemubot.hooks.message import Message
+
+
+class hook:
+
+ last_registered = []
+
+
+ def _add(store, h, *args, **kwargs):
+ """Function used as a decorator for module loading"""
+ def sec(call):
+ hook.last_registered.append((store, h(call, *args, **kwargs)))
+ return call
+ return sec
+
+
+ def add(store, *args, **kwargs):
+ return hook._add(store, Abstract, *args, **kwargs)
+
+ def ask(*args, store=["in","DirectAsk"], **kwargs):
+ return hook._add(store, Message, *args, **kwargs)
+
+ def command(*args, store=["in","Command"], **kwargs):
+ return hook._add(store, Command, *args, **kwargs)
+
+ def message(*args, store=["in","Text"], **kwargs):
+ return hook._add(store, Message, *args, **kwargs)
+
+ def post(*args, store=["post"], **kwargs):
+ return hook._add(store, Abstract, *args, **kwargs)
+
+ def pre(*args, store=["pre"], **kwargs):
+ return hook._add(store, Abstract, *args, **kwargs)
diff --git a/nemubot/hooks/abstract.py b/nemubot/hooks/abstract.py
new file mode 100644
index 0000000..ffe79fb
--- /dev/null
+++ b/nemubot/hooks/abstract.py
@@ -0,0 +1,138 @@
+# 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 .
+
+import types
+
+def call_game(call, *args, **kargs):
+ """With given args, try to determine the right call to make
+
+ Arguments:
+ call -- the function to call
+ *args -- unamed arguments to pass, dictionnaries contains are placed into kargs
+ **kargs -- named arguments
+ """
+
+ assert callable(call)
+
+ l = list()
+ d = kargs
+
+ for a in args:
+ if a is not None:
+ if isinstance(a, dict):
+ d.update(a)
+ else:
+ l.append(a)
+
+ return call(*l, **d)
+
+
+class Abstract:
+
+ """Abstract class for Hook implementation"""
+
+ def __init__(self, call, data=None, channels=None, servers=None, mtimes=-1,
+ end_call=None, check=None, match=None):
+ """Create basis of the hook
+
+ Arguments:
+ call -- function to call to perform the hook
+
+ Keyword arguments:
+ data -- optional datas passed to call
+ """
+
+ if channels is None: channels = list()
+ if servers is None: servers = list()
+
+ assert callable(call), call
+ assert end_call is None or callable(end_call), end_call
+ assert check is None or callable(check), check
+ assert match is None or callable(match), match
+ assert isinstance(channels, list), channels
+ assert isinstance(servers, list), servers
+ assert type(mtimes) is int, mtimes
+
+ self.call = call
+ self.data = data
+
+ self.mod_check = check
+ self.mod_match = match
+
+ # TODO: find a way to have only one list: a limit is server + channel, not only server or channel
+ self.channels = channels
+ self.servers = servers
+
+ self.times = mtimes
+ self.end_call = end_call
+
+
+ def can_read(self, receivers=list(), server=None):
+ assert isinstance(receivers, list), receivers
+
+ if server is None or len(self.servers) == 0 or server in self.servers:
+ if len(self.channels) == 0:
+ return True
+
+ for receiver in receivers:
+ if receiver in self.channels:
+ return True
+
+ return False
+
+
+ def __str__(self):
+ return ""
+
+
+ def can_write(self, receivers=list(), server=None):
+ return True
+
+
+ def check(self, data1):
+ return self.mod_check(data1) if self.mod_check is not None else True
+
+
+ def match(self, data1):
+ return self.mod_match(data1) if self.mod_match is not None else True
+
+
+ def run(self, data1, *args):
+ """Run the hook"""
+
+ from nemubot.exception import IMException
+ self.times -= 1
+
+ ret = None
+
+ try:
+ if self.check(data1):
+ ret = call_game(self.call, data1, self.data, *args)
+ if isinstance(ret, types.GeneratorType):
+ for r in ret:
+ yield r
+ ret = None
+ except IMException as e:
+ ret = e.fill_response(data1)
+ finally:
+ if self.times == 0:
+ self.call_end(ret)
+
+ if isinstance(ret, list):
+ for r in ret:
+ yield ret
+ elif ret is not None:
+ yield ret
diff --git a/nemubot/hooks/command.py b/nemubot/hooks/command.py
new file mode 100644
index 0000000..863d672
--- /dev/null
+++ b/nemubot/hooks/command.py
@@ -0,0 +1,67 @@
+# 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 .
+
+import re
+
+from nemubot.hooks.message import Message
+from nemubot.hooks.abstract import Abstract
+from nemubot.hooks.keywords import NoKeyword
+from nemubot.hooks.keywords.abstract import Abstract as AbstractKeywords
+from nemubot.hooks.keywords.dict import Dict as DictKeywords
+import nemubot.message
+
+
+class Command(Message):
+
+ """Class storing hook information, specialized for Command messages"""
+
+ def __init__(self, call, name=None, help_usage=dict(), keywords=NoKeyword(),
+ **kargs):
+
+ super().__init__(call=call, **kargs)
+
+ if isinstance(keywords, dict):
+ keywords = DictKeywords(keywords)
+
+ assert type(help_usage) is dict, help_usage
+ assert isinstance(keywords, AbstractKeywords), keywords
+
+ self.name = str(name) if name is not None else None
+ self.help_usage = help_usage
+ self.keywords = keywords
+
+
+ def __str__(self):
+ return "\x03\x02%s\x03\x02%s%s" % (
+ self.name if self.name is not None else "\x03\x1f" + self.regexp + "\x03\x1f" if self.regexp is not None else "",
+ " (restricted to %:%s)" % ((",".join(self.servers) if self.server else "*") + (",".join(self.channels) if self.channels else "*")) if len(self.channels) or len(self.servers) else "",
+ ": %s" % self.help if self.help is not None else ""
+ )
+
+
+ def check(self, msg):
+ return self.keywords.check(msg.kwargs) and super().check(msg)
+
+
+ def match(self, msg):
+ if not isinstance(msg, nemubot.message.command.Command):
+ return False
+ else:
+ return (
+ (self.name is None or msg.cmd == self.name) and
+ (self.regexp is None or re.match(self.regexp, msg.cmd)) and
+ Abstract.match(self, msg)
+ )
diff --git a/nemubot/hooks/keywords/__init__.py b/nemubot/hooks/keywords/__init__.py
new file mode 100644
index 0000000..598b04f
--- /dev/null
+++ b/nemubot/hooks/keywords/__init__.py
@@ -0,0 +1,47 @@
+# 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 .
+
+from nemubot.exception.keyword import KeywordException
+from nemubot.hooks.keywords.abstract import Abstract
+
+
+class NoKeyword(Abstract):
+
+ def check(self, mkw):
+ if len(mkw):
+ raise KeywordException("This command doesn't take any keyword arguments.")
+ return super().check(mkw)
+
+
+class AnyKeyword(Abstract):
+
+ def __init__(self, h):
+ """Class that accepts any passed keywords
+
+ Arguments:
+ h -- Help string
+ """
+
+ super().__init__()
+ self.h = h
+
+
+ def check(self, mkw):
+ return super().check(mkw)
+
+
+ def help(self):
+ return self.h
diff --git a/nemubot/hooks/keywords/abstract.py b/nemubot/hooks/keywords/abstract.py
new file mode 100644
index 0000000..a990cf3
--- /dev/null
+++ b/nemubot/hooks/keywords/abstract.py
@@ -0,0 +1,35 @@
+# 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 .
+
+class Abstract:
+
+ def __init__(self):
+ pass
+
+ def check(self, mkw):
+ """Check that all given message keywords are valid
+
+ Argument:
+ mkw -- dictionnary of keywords present in the message
+ """
+
+ assert type(mkw) is dict, mkw
+
+ return True
+
+
+ def help(self):
+ return ""
diff --git a/nemubot/hooks/keywords/dict.py b/nemubot/hooks/keywords/dict.py
new file mode 100644
index 0000000..c2d3f2e
--- /dev/null
+++ b/nemubot/hooks/keywords/dict.py
@@ -0,0 +1,59 @@
+# 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 .
+
+from nemubot.exception.keyword import KeywordException
+from nemubot.hooks.keywords.abstract import Abstract
+from nemubot.tools.human import guess
+
+
+class Dict(Abstract):
+
+
+ def __init__(self, d):
+ super().__init__()
+ self.d = d
+
+
+ @property
+ def chk_noarg(self):
+ if not hasattr(self, "_cache_chk_noarg"):
+ self._cache_chk_noarg = [k for k in self.d if "=" not in k]
+ return self._cache_chk_noarg
+
+
+ @property
+ def chk_args(self):
+ if not hasattr(self, "_cache_chk_args"):
+ self._cache_chk_args = [k.split("=", 1)[0] for k in self.d if "=" in k]
+ return self._cache_chk_args
+
+
+ def check(self, mkw):
+ for k in mkw:
+ if ((k + "?") not in self.chk_args) and ((mkw[k] and k not in self.chk_args) or (not mkw[k] and k not in self.chk_noarg)):
+ if mkw[k] and k in self.chk_noarg:
+ raise KeywordException("Keyword %s doesn't take value." % k)
+ elif not mkw[k] and k in self.chk_args:
+ raise KeywordException("Keyword %s requires a value." % k)
+ else:
+ ch = [c for c in guess(k, self.d)]
+ raise KeywordException("Unknown keyword %s." % k + (" Did you mean: " + ", ".join(ch) + "?" if len(ch) else ""))
+
+ return super().check(mkw)
+
+
+ def help(self):
+ return ["\x03\x02@%s\x03\x02: %s" % (k, self.d[k]) for k in self.d]
diff --git a/nemubot/hooks/manager.py b/nemubot/hooks/manager.py
new file mode 100644
index 0000000..6a57d2a
--- /dev/null
+++ b/nemubot/hooks/manager.py
@@ -0,0 +1,134 @@
+# 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 .
+
+import logging
+
+
+class HooksManager:
+
+ """Class to manage hooks"""
+
+ def __init__(self, name="core"):
+ """Initialize the manager"""
+
+ self.hooks = dict()
+ self.logger = logging.getLogger("nemubot.hooks.manager." + name)
+
+
+ def _access(self, *triggers):
+ """Access to the given triggers chain"""
+
+ h = self.hooks
+ for t in triggers:
+ if t not in h:
+ h[t] = dict()
+ h = h[t]
+
+ if "__end__" not in h:
+ h["__end__"] = list()
+
+ return h
+
+
+ def _search(self, hook, *where, start=None):
+ """Search all occurence of the given hook"""
+
+ if start is None:
+ start = self.hooks
+
+ for k in start:
+ if k == "__end__":
+ if hook in start[k]:
+ yield where
+ else:
+ yield from self._search(hook, *where + (k,), start=start[k])
+
+
+ def add_hook(self, hook, *triggers):
+ """Add a hook to the manager
+
+ Argument:
+ hook -- a Hook instance
+ triggers -- string that trigger the hook
+ """
+
+ assert hook is not None, hook
+
+ h = self._access(*triggers)
+
+ h["__end__"].append(hook)
+
+ self.logger.debug("New hook successfully added in %s: %s",
+ "/".join(triggers), hook)
+
+
+ def del_hooks(self, *triggers, hook=None):
+ """Remove the given hook from the manager
+
+ Argument:
+ triggers -- trigger string to remove
+
+ Keyword argument:
+ hook -- a Hook instance to remove from the trigger string
+ """
+
+ assert hook is not None or len(triggers)
+
+ self.logger.debug("Trying to delete hook in %s: %s",
+ "/".join(triggers), hook)
+
+ if hook is not None:
+ for h in self._search(hook, *triggers, start=self._access(*triggers)):
+ self._access(*h)["__end__"].remove(hook)
+
+ else:
+ if len(triggers):
+ del self._access(*triggers[:-1])[triggers[-1]]
+ else:
+ self.hooks = dict()
+
+
+ def get_hooks(self, *triggers):
+ """Returns list of trigger hooks that match the given trigger string
+
+ Argument:
+ triggers -- the trigger string
+ """
+
+ for n in range(len(triggers) + 1):
+ i = self._access(*triggers[:n])
+ for h in i["__end__"]:
+ yield h
+
+
+ def get_reverse_hooks(self, *triggers, exclude_first=False):
+ """Returns list of triggered hooks that are bellow or at the same level
+
+ Argument:
+ triggers -- the trigger string
+
+ Keyword arguments:
+ exclude_first -- start reporting hook at the next level
+ """
+
+ h = self._access(*triggers)
+ for k in h:
+ if k == "__end__":
+ if not exclude_first:
+ for hk in h[k]:
+ yield hk
+ else:
+ yield from self.get_reverse_hooks(*triggers + (k,))
diff --git a/nemubot/hooks/manager_test.py b/nemubot/hooks/manager_test.py
new file mode 100755
index 0000000..a0f38d7
--- /dev/null
+++ b/nemubot/hooks/manager_test.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+
+import unittest
+
+from nemubot.hooks.manager import HooksManager
+
+class TestHookManager(unittest.TestCase):
+
+
+ def test_access(self):
+ hm = HooksManager()
+
+ h1 = "HOOK1"
+ h2 = "HOOK2"
+ h3 = "HOOK3"
+
+ hm.add_hook(h1)
+ hm.add_hook(h2, "pre")
+ hm.add_hook(h3, "pre", "Text")
+ hm.add_hook(h2, "post", "Text")
+
+ self.assertIn("__end__", hm._access())
+ self.assertIn("__end__", hm._access("pre"))
+ self.assertIn("__end__", hm._access("pre", "Text"))
+ self.assertIn("__end__", hm._access("post", "Text"))
+
+ self.assertFalse(hm._access("inexistant")["__end__"])
+ self.assertTrue(hm._access()["__end__"])
+ self.assertTrue(hm._access("pre")["__end__"])
+ self.assertTrue(hm._access("pre", "Text")["__end__"])
+ self.assertTrue(hm._access("post", "Text")["__end__"])
+
+
+ def test_search(self):
+ hm = HooksManager()
+
+ h1 = "HOOK1"
+ h2 = "HOOK2"
+ h3 = "HOOK3"
+ h4 = "HOOK4"
+
+ hm.add_hook(h1)
+ hm.add_hook(h2, "pre")
+ hm.add_hook(h3, "pre", "Text")
+ hm.add_hook(h2, "post", "Text")
+
+ self.assertTrue([h for h in hm._search(h1)])
+ self.assertFalse([h for h in hm._search(h4)])
+ self.assertEqual(2, len([h for h in hm._search(h2)]))
+ self.assertEqual([("pre", "Text")], [h for h in hm._search(h3)])
+
+
+ def test_delete(self):
+ hm = HooksManager()
+
+ h1 = "HOOK1"
+ h2 = "HOOK2"
+ h3 = "HOOK3"
+ h4 = "HOOK4"
+
+ hm.add_hook(h1)
+ hm.add_hook(h2, "pre")
+ hm.add_hook(h3, "pre", "Text")
+ hm.add_hook(h2, "post", "Text")
+
+ hm.del_hooks(hook=h4)
+
+ self.assertTrue(hm._access("pre")["__end__"])
+ self.assertTrue(hm._access("pre", "Text")["__end__"])
+ hm.del_hooks("pre")
+ self.assertFalse(hm._access("pre")["__end__"])
+
+ self.assertTrue(hm._access("post", "Text")["__end__"])
+ hm.del_hooks("post", "Text", hook=h2)
+ self.assertFalse(hm._access("post", "Text")["__end__"])
+
+ self.assertTrue(hm._access()["__end__"])
+ hm.del_hooks(hook=h1)
+ self.assertFalse(hm._access()["__end__"])
+
+
+ def test_get(self):
+ hm = HooksManager()
+
+ h1 = "HOOK1"
+ h2 = "HOOK2"
+ h3 = "HOOK3"
+
+ hm.add_hook(h1)
+ hm.add_hook(h2, "pre")
+ hm.add_hook(h3, "pre", "Text")
+ hm.add_hook(h2, "post", "Text")
+
+ self.assertEqual([h1, h2], [h for h in hm.get_hooks("pre")])
+ self.assertEqual([h1, h2, h3], [h for h in hm.get_hooks("pre", "Text")])
+
+
+ def test_get_rev(self):
+ hm = HooksManager()
+
+ h1 = "HOOK1"
+ h2 = "HOOK2"
+ h3 = "HOOK3"
+
+ hm.add_hook(h1)
+ hm.add_hook(h2, "pre")
+ hm.add_hook(h3, "pre", "Text")
+ hm.add_hook(h2, "post", "Text")
+
+ self.assertEqual([h2, h3], [h for h in hm.get_reverse_hooks("pre")])
+ self.assertEqual([h3], [h for h in hm.get_reverse_hooks("pre", exclude_first=True)])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/nemubot/hooks/message.py b/nemubot/hooks/message.py
new file mode 100644
index 0000000..ee07600
--- /dev/null
+++ b/nemubot/hooks/message.py
@@ -0,0 +1,49 @@
+# 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 .
+
+import re
+
+from nemubot.hooks.abstract import Abstract
+import nemubot.message
+
+
+class Message(Abstract):
+
+ """Class storing hook information, specialized for a generic Message"""
+
+ def __init__(self, call, regexp=None, help=None, **kwargs):
+ super().__init__(call=call, **kwargs)
+
+ assert regexp is None or type(regexp) is str, regexp
+
+ self.regexp = regexp
+ self.help = help
+
+
+ def __str__(self):
+ # TODO: find a way to name the feature (like command: help)
+ return self.help if self.help is not None else super().__str__()
+
+
+ def check(self, msg):
+ return super().check(msg)
+
+
+ def match(self, msg):
+ if not isinstance(msg, nemubot.message.text.Text):
+ return False
+ else:
+ return (self.regexp is None or re.match(self.regexp, msg.message)) and super().match(msg)
diff --git a/nemubot/importer.py b/nemubot/importer.py
new file mode 100644
index 0000000..674ab40
--- /dev/null
+++ b/nemubot/importer.py
@@ -0,0 +1,69 @@
+# 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 .
+
+from importlib.abc import Finder
+from importlib.machinery import SourceFileLoader
+import logging
+import os
+
+logger = logging.getLogger("nemubot.importer")
+
+
+class ModuleFinder(Finder):
+
+ def __init__(self, modules_paths, add_module):
+ self.modules_paths = modules_paths
+ self.add_module = add_module
+
+ def find_module(self, fullname, path=None):
+ if path is not None and fullname.startswith("nemubot.module."):
+ module_name = fullname.split(".", 2)[2]
+ for mpath in self.modules_paths:
+ if os.path.isfile(os.path.join(mpath, module_name + ".py")):
+ return ModuleLoader(self.add_module, fullname,
+ os.path.join(mpath, module_name + ".py"))
+ elif os.path.isfile(os.path.join(os.path.join(mpath, module_name), "__init__.py")):
+ return ModuleLoader(self.add_module, fullname,
+ os.path.join(
+ os.path.join(mpath, module_name),
+ "__init__.py"))
+ return None
+
+
+class ModuleLoader(SourceFileLoader):
+
+ def __init__(self, add_module, fullname, path):
+ self.add_module = add_module
+ SourceFileLoader.__init__(self, fullname, path)
+
+
+ def _load(self, module, name):
+ # Add the module to the global modules list
+ self.add_module(module)
+ logger.info("Module '%s' successfully imported from %s.", name.split(".", 2)[2], self.path)
+ return module
+
+
+ # Python 3.4
+ def exec_module(self, module):
+ super().exec_module(module)
+ self._load(module, module.__spec__.name)
+
+
+ # Python 3.3
+ def load_module(self, fullname):
+ module = super().load_module(fullname)
+ return self._load(module, module.__name__)
diff --git a/nemubot/message/__init__.py b/nemubot/message/__init__.py
new file mode 100644
index 0000000..4d69dbb
--- /dev/null
+++ b/nemubot/message/__init__.py
@@ -0,0 +1,21 @@
+# 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 .
+
+from nemubot.message.abstract import Abstract
+from nemubot.message.text import Text
+from nemubot.message.directask import DirectAsk
+from nemubot.message.command import Command
+from nemubot.message.command import OwnerCommand
diff --git a/nemubot/message/abstract.py b/nemubot/message/abstract.py
new file mode 100644
index 0000000..3af0511
--- /dev/null
+++ b/nemubot/message/abstract.py
@@ -0,0 +1,83 @@
+# 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 .
+
+from datetime import datetime, timezone
+
+
+class Abstract:
+
+ """This class represents an abstract message"""
+
+ def __init__(self, server=None, date=None, to=None, to_response=None, frm=None, frm_owner=False):
+ """Initialize an abstract message
+
+ Arguments:
+ server -- the servir identifier
+ date -- time of the message reception, default: now
+ to -- list of recipients
+ to_response -- if channel(s) where send the response differ
+ frm -- the sender
+ """
+
+ self.server = server
+ self.date = datetime.now(timezone.utc) if date is None else date
+ self.to = to if to is not None else list()
+ self._to_response = (to_response if (to_response is None or
+ isinstance(to_response, list))
+ else [ to_response ])
+ self.frm = frm # None allowed when it designate this bot
+
+ self.frm_owner = frm_owner
+
+
+ @property
+ def to_response(self):
+ if self._to_response is not None:
+ return self._to_response
+ else:
+ return self.to
+
+
+ @property
+ def channel(self):
+ # TODO: this is for legacy modules
+ if self.to_response is not None and len(self.to_response) > 0:
+ return self.to_response[0]
+ else:
+ return None
+
+ def accept(self, visitor):
+ visitor.visit(self)
+
+
+ def export_args(self, without=list()):
+ if not isinstance(without, list):
+ without = [ without ]
+
+ ret = {
+ "server": self.server,
+ "date": self.date,
+ "to": self.to,
+ "to_response": self._to_response,
+ "frm": self.frm,
+ "frm_owner": self.frm_owner,
+ }
+
+ for w in without:
+ if w in ret:
+ del ret[w]
+
+ return ret
diff --git a/nemubot/message/command.py b/nemubot/message/command.py
new file mode 100644
index 0000000..ca87e4c
--- /dev/null
+++ b/nemubot/message/command.py
@@ -0,0 +1,39 @@
+# 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 .
+
+from nemubot.message.abstract import Abstract
+
+
+class Command(Abstract):
+
+ """This class represents a specialized TextMessage"""
+
+ def __init__(self, cmd, args=None, kwargs=None, *nargs, **kargs):
+ super().__init__(*nargs, **kargs)
+
+ self.cmd = cmd
+ self.args = args if args is not None else list()
+ self.kwargs = kwargs if kwargs is not None else dict()
+
+ def __str__(self):
+ return self.cmd + " @" + ",@".join(self.args)
+
+
+class OwnerCommand(Command):
+
+ """This class represents a special command incomming from the owner"""
+
+ pass
diff --git a/nemubot/message/directask.py b/nemubot/message/directask.py
new file mode 100644
index 0000000..3b1fabb
--- /dev/null
+++ b/nemubot/message/directask.py
@@ -0,0 +1,39 @@
+# 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 .
+
+from nemubot.message.text import Text
+
+
+class DirectAsk(Text):
+
+ """This class represents a message to this bot"""
+
+ def __init__(self, designated, *args, **kargs):
+ """Initialize a message to a specific person
+
+ Argument:
+ designated -- the user designated by the message
+ """
+
+ super().__init__(*args, **kargs)
+
+ self.designated = designated
+
+ def respond(self, message):
+ return DirectAsk(self.frm,
+ message,
+ server=self.server,
+ to=self.to_response)
diff --git a/nemubot/message/printer/IRCLib.py b/nemubot/message/printer/IRCLib.py
new file mode 100644
index 0000000..abd1f2f
--- /dev/null
+++ b/nemubot/message/printer/IRCLib.py
@@ -0,0 +1,67 @@
+# Nemubot is a smart and modulable IM bot.
+# Copyright (C) 2012-2026 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 .
+
+from nemubot.message.visitor import AbstractVisitor
+
+
+class IRCLib(AbstractVisitor):
+
+ """Visitor that sends bot responses via an irc.client.ServerConnection.
+
+ Unlike the socket-based IRC printer (which builds a raw PRIVMSG string),
+ this calls connection.privmsg() directly so the library handles encoding,
+ line-length capping, and any internal locking.
+ """
+
+ def __init__(self, connection):
+ self._conn = connection
+
+ def _send(self, target, text):
+ try:
+ self._conn.privmsg(target, text)
+ except Exception:
+ pass # drop silently during reconnection
+
+ # Visitor methods
+
+ def visit_Text(self, msg):
+ if isinstance(msg.message, str):
+ for target in msg.to:
+ self._send(target, msg.message)
+ else:
+ msg.message.accept(self)
+
+ def visit_DirectAsk(self, msg):
+ text = msg.message if isinstance(msg.message, str) else str(msg.message)
+ # Mirrors socket.py logic:
+ # rooms that are NOT the designated nick get a "nick: " prefix
+ others = [to for to in msg.to if to != msg.designated]
+ if len(others) == 0 or len(others) != len(msg.to):
+ for target in msg.to:
+ self._send(target, text)
+ if others:
+ for target in others:
+ self._send(target, "%s: %s" % (msg.designated, text))
+
+ def visit_Command(self, msg):
+ parts = ["!" + msg.cmd] + list(msg.args)
+ for target in msg.to:
+ self._send(target, " ".join(parts))
+
+ def visit_OwnerCommand(self, msg):
+ parts = ["`" + msg.cmd] + list(msg.args)
+ for target in msg.to:
+ self._send(target, " ".join(parts))
diff --git a/nemubot/message/printer/Matrix.py b/nemubot/message/printer/Matrix.py
new file mode 100644
index 0000000..ad1b99e
--- /dev/null
+++ b/nemubot/message/printer/Matrix.py
@@ -0,0 +1,69 @@
+# Nemubot is a smart and modulable IM bot.
+# Copyright (C) 2012-2026 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 .
+
+from nemubot.message.visitor import AbstractVisitor
+
+
+class Matrix(AbstractVisitor):
+
+ """Visitor that sends bot responses as Matrix room messages.
+
+ Instead of accumulating text like the IRC printer does, each visit_*
+ method calls send_func(room_id, text) directly for every destination room.
+ """
+
+ def __init__(self, send_func):
+ """
+ Argument:
+ send_func -- callable(room_id: str, text: str) that sends a plain-text
+ message to the given Matrix room
+ """
+ self._send = send_func
+
+ def visit_Text(self, msg):
+ if isinstance(msg.message, str):
+ for room in msg.to:
+ self._send(room, msg.message)
+ else:
+ # Nested message object — let it visit itself
+ msg.message.accept(self)
+
+ def visit_DirectAsk(self, msg):
+ text = msg.message if isinstance(msg.message, str) else str(msg.message)
+ # Rooms that are NOT the designated nick → prefix with "nick: "
+ others = [to for to in msg.to if to != msg.designated]
+ if len(others) == 0 or len(others) != len(msg.to):
+ # At least one room IS the designated target → send plain
+ for room in msg.to:
+ self._send(room, text)
+ if len(others):
+ # Other rooms → prefix with nick
+ for room in others:
+ self._send(room, "%s: %s" % (msg.designated, text))
+
+ def visit_Command(self, msg):
+ parts = ["!" + msg.cmd]
+ if msg.args:
+ parts.extend(msg.args)
+ for room in msg.to:
+ self._send(room, " ".join(parts))
+
+ def visit_OwnerCommand(self, msg):
+ parts = ["`" + msg.cmd]
+ if msg.args:
+ parts.extend(msg.args)
+ for room in msg.to:
+ self._send(room, " ".join(parts))
diff --git a/nemubot/message/printer/__init__.py b/nemubot/message/printer/__init__.py
new file mode 100644
index 0000000..e0fbeef
--- /dev/null
+++ b/nemubot/message/printer/__init__.py
@@ -0,0 +1,15 @@
+# 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 .
diff --git a/nemubot/message/printer/socket.py b/nemubot/message/printer/socket.py
new file mode 100644
index 0000000..6884c88
--- /dev/null
+++ b/nemubot/message/printer/socket.py
@@ -0,0 +1,68 @@
+# 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 .
+
+from nemubot.message import Text
+from nemubot.message.visitor import AbstractVisitor
+
+
+class Socket(AbstractVisitor):
+
+ def __init__(self):
+ self.pp = ""
+
+
+ def visit_Text(self, msg):
+ if isinstance(msg.message, str):
+ self.pp += msg.message
+ else:
+ msg.message.accept(self)
+
+
+ def visit_DirectAsk(self, msg):
+ others = [to for to in msg.to if to != msg.designated]
+
+ # Avoid nick starting message when discussing on user channel
+ if len(others) == 0 or len(others) != len(msg.to):
+ res = Text(msg.message,
+ server=msg.server, date=msg.date,
+ to=msg.to, frm=msg.frm)
+ res.accept(self)
+
+ if len(others):
+ res = Text("%s: %s" % (msg.designated, msg.message),
+ server=msg.server, date=msg.date,
+ to=others, frm=msg.frm)
+ res.accept(self)
+
+
+ def visit_Command(self, msg):
+ res = Text("!%s%s%s%s%s" % (msg.cmd,
+ " " if len(msg.kwargs) else "",
+ " ".join(["@%s=%s" % (k, msg.kwargs[k]) if msg.kwargs[k] is not None else "@%s" % k for k in msg.kwargs]),
+ " " if len(msg.args) else "",
+ " ".join(msg.args)),
+ server=msg.server, date=msg.date,
+ to=msg.to, frm=msg.frm)
+ res.accept(self)
+
+
+ def visit_OwnerCommand(self, msg):
+ res = Text("`%s%s%s" % (msg.cmd,
+ " " if len(msg.args) else "",
+ " ".join(msg.args)),
+ server=msg.server, date=msg.date,
+ to=msg.to, frm=msg.frm)
+ res.accept(self)
diff --git a/nemubot/message/printer/test_socket.py b/nemubot/message/printer/test_socket.py
new file mode 100644
index 0000000..41f74b0
--- /dev/null
+++ b/nemubot/message/printer/test_socket.py
@@ -0,0 +1,112 @@
+# 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 .
+
+import unittest
+
+from nemubot.message import Command, DirectAsk, Text
+from nemubot.message.printer.socket import Socket as SocketVisitor
+
+class TestSocketPrinter(unittest.TestCase):
+
+
+ def setUp(self):
+ self.msgs = [
+ # Texts
+ (
+ Text(message="TEXT",
+ ),
+ "TEXT"
+ ),
+ (
+ Text(message="TEXT TEXT2",
+ ),
+ "TEXT TEXT2"
+ ),
+ (
+ Text(message="TEXT @ARG=1 TEXT2",
+ ),
+ "TEXT @ARG=1 TEXT2"
+ ),
+
+
+ # DirectAsk
+ (
+ DirectAsk(message="TEXT",
+ designated="someone",
+ to=["#somechannel"]
+ ),
+ "someone: TEXT"
+ ),
+ (
+ # Private message to someone
+ DirectAsk(message="TEXT",
+ designated="someone",
+ to=["someone"]
+ ),
+ "TEXT"
+ ),
+
+
+ # Commands
+ (
+ Command(cmd="COMMAND",
+ ),
+ "!COMMAND"
+ ),
+ (
+ Command(cmd="COMMAND",
+ args=["TEXT"],
+ ),
+ "!COMMAND TEXT"
+ ),
+ (
+ Command(cmd="COMMAND",
+ kwargs={"KEY1": "VALUE"},
+ ),
+ "!COMMAND @KEY1=VALUE"
+ ),
+ (
+ Command(cmd="COMMAND",
+ args=["TEXT"],
+ kwargs={"KEY1": "VALUE"},
+ ),
+ "!COMMAND @KEY1=VALUE TEXT"
+ ),
+ (
+ Command(cmd="COMMAND",
+ kwargs={"KEY2": None},
+ ),
+ "!COMMAND @KEY2"
+ ),
+ (
+ Command(cmd="COMMAND",
+ args=["TEXT"],
+ kwargs={"KEY2": None},
+ ),
+ "!COMMAND @KEY2 TEXT"
+ ),
+ ]
+
+
+ def test_printer(self):
+ for msg, pp in self.msgs:
+ sv = SocketVisitor()
+ msg.accept(sv)
+ self.assertEqual(sv.pp, pp)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/nemubot/message/response.py b/nemubot/message/response.py
new file mode 100644
index 0000000..f9353ad
--- /dev/null
+++ b/nemubot/message/response.py
@@ -0,0 +1,29 @@
+# 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 .
+
+from nemubot.message.abstract import Abstract
+
+
+class Response(Abstract):
+
+ def __init__(self, cmd, args=None, *nargs, **kargs):
+ super().__init__(*nargs, **kargs)
+
+ self.cmd = cmd
+ self.args = args if args is not None else list()
+
+ def __str__(self):
+ return self.cmd + " @" + ",@".join(self.args)
diff --git a/nemubot/message/text.py b/nemubot/message/text.py
new file mode 100644
index 0000000..f691a04
--- /dev/null
+++ b/nemubot/message/text.py
@@ -0,0 +1,41 @@
+# 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 .
+
+from nemubot.message.abstract import Abstract
+
+
+class Text(Abstract):
+
+ """This class represent a simple message send to someone"""
+
+ def __init__(self, message, *args, **kargs):
+ """Initialize a message with no particular specificity
+
+ Argument:
+ message -- the parsed message
+ """
+
+ super().__init__(*args, **kargs)
+
+ self.message = message
+
+ def __str__(self):
+ return self.message
+
+ @property
+ def text(self):
+ # TODO: this is for legacy modules
+ return self.message
diff --git a/nemubot/message/visitor.py b/nemubot/message/visitor.py
new file mode 100644
index 0000000..454633a
--- /dev/null
+++ b/nemubot/message/visitor.py
@@ -0,0 +1,24 @@
+# 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 .
+
+
+class AbstractVisitor:
+
+ def visit(self, obj):
+ """Visit a node"""
+ method_name = "visit_%s" % obj.__class__.__name__
+ method = getattr(self, method_name)
+ return method(obj)
diff --git a/nemubot/module/__init__.py b/nemubot/module/__init__.py
new file mode 100644
index 0000000..33f0e41
--- /dev/null
+++ b/nemubot/module/__init__.py
@@ -0,0 +1,7 @@
+#
+# This directory aims to store nemubot core modules.
+#
+# Custom modules should be placed into a separate directory.
+# By default, this is the directory modules in your current directory.
+# Use the --modules-path argument to define a custom directory for your modules.
+#
diff --git a/nemubot/module/more.py b/nemubot/module/more.py
new file mode 100644
index 0000000..206d97a
--- /dev/null
+++ b/nemubot/module/more.py
@@ -0,0 +1,299 @@
+# 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 .
+
+"""Progressive display of very long messages"""
+
+# PYTHON STUFFS #######################################################
+
+import logging
+
+from nemubot.message import Text, DirectAsk
+from nemubot.hooks import hook
+
+logger = logging.getLogger("nemubot.response")
+
+
+# MODULE CORE #########################################################
+
+class Response:
+
+ def __init__(self, message=None, channel=None, nick=None, server=None,
+ nomore="No more message", title=None, more="(suite) ",
+ count=None, shown_first_count=-1, line_treat=None):
+ self.nomore = nomore
+ self.more = more
+ self.line_treat = line_treat
+ self.rawtitle = title
+ self.server = server
+ self.messages = list()
+ self.alone = True
+ if message is not None:
+ self.append_message(message, shown_first_count=shown_first_count)
+ self.elt = 0 # Next element to display
+
+ self.channel = channel
+ self.nick = nick
+ self.count = count
+
+
+ @property
+ def to(self):
+ if self.channel is None:
+ if self.nick is not None:
+ return [self.nick]
+ return list()
+ elif isinstance(self.channel, list):
+ return self.channel
+ else:
+ return [self.channel]
+
+
+ def append_message(self, message, title=None, shown_first_count=-1):
+ if type(message) is str:
+ message = message.split('\n')
+ if len(message) > 1:
+ for m in message:
+ self.append_message(m)
+ return
+ else:
+ message = message[0]
+ if message is not None and len(message) > 0:
+ if shown_first_count >= 0:
+ self.messages.append(message[:shown_first_count])
+ message = message[shown_first_count:]
+ self.messages.append(message)
+ self.alone = self.alone and len(self.messages) <= 1
+ if isinstance(self.rawtitle, list):
+ self.rawtitle.append(title)
+ elif title is not None:
+ rawtitle = self.rawtitle
+ self.rawtitle = list()
+ for osef in self.messages:
+ self.rawtitle.append(rawtitle)
+ self.rawtitle.pop()
+ self.rawtitle.append(title)
+ return self
+
+
+ def append_content(self, message):
+ if message is not None and len(message) > 0:
+ if self.messages is None or len(self.messages) == 0:
+ self.messages = [message]
+ self.alone = True
+ else:
+ self.messages[len(self.messages)-1] += message
+ self.alone = self.alone and len(self.messages) <= 1
+ return self
+
+
+ @property
+ def empty(self):
+ return len(self.messages) <= 0
+
+
+ @property
+ def title(self):
+ if isinstance(self.rawtitle, list):
+ return self.rawtitle[0]
+ else:
+ return self.rawtitle
+
+
+ @property
+ def text(self):
+ if len(self.messages) < 1:
+ return self.nomore
+ else:
+ for msg in self.messages:
+ if isinstance(msg, list):
+ return ", ".join(msg)
+ else:
+ return msg
+
+
+ def pop(self):
+ self.messages.pop(0)
+ self.elt = 0
+ if isinstance(self.rawtitle, list):
+ self.rawtitle.pop(0)
+ if len(self.rawtitle) <= 0:
+ self.rawtitle = None
+
+
+ def accept(self, visitor):
+ visitor.visit(self.next_response())
+
+
+ def next_response(self, maxlen=440):
+ if self.nick:
+ return DirectAsk(self.nick,
+ self.get_message(maxlen - len(self.nick) - 2),
+ server=None, to=self.to)
+ else:
+ return Text(self.get_message(maxlen),
+ server=None, to=self.to)
+
+
+ def __str__(self):
+ ret = []
+ if len(self.messages):
+ for msg in self.messages:
+ if isinstance(msg, list):
+ ret.append(", ".join(msg))
+ else:
+ ret.append(msg)
+ ret.append(self.nomore)
+ return "\n".join(ret)
+
+ def get_message(self, maxlen):
+ if self.alone and len(self.messages) > 1:
+ self.alone = False
+
+ if self.empty:
+ if hasattr(self.nomore, '__call__'):
+ res = self.nomore(self)
+ if res is None:
+ return "No more message"
+ elif isinstance(res, Response):
+ self.__dict__ = res.__dict__
+ elif isinstance(res, list):
+ self.messages = res
+ elif isinstance(res, str):
+ self.messages.append(res)
+ else:
+ raise Exception("Type returned by nomore (%s) is not "
+ "handled here." % type(res))
+ return self.get_message()
+ else:
+ return self.nomore
+
+ if self.line_treat is not None and self.elt == 0:
+ try:
+ if isinstance(self.messages[0], list):
+ for x in self.messages[0]:
+ print(x, self.line_treat(x))
+ self.messages[0] = [self.line_treat(x).replace("\n", " ").strip() for x in self.messages[0]]
+ else:
+ self.messages[0] = (self.line_treat(self.messages[0])
+ .replace("\n", " ").strip())
+ except Exception as e:
+ logger.exception(e)
+
+ msg = ""
+ if self.title is not None:
+ if self.elt > 0:
+ msg += self.title + " " + self.more + ": "
+ else:
+ msg += self.title + ": "
+
+ elif self.elt > 0:
+ msg += "[…]"
+ if self.messages[0][self.elt - 1] == ' ':
+ msg += " "
+
+ elts = self.messages[0][self.elt:]
+ if isinstance(elts, list):
+ for e in elts:
+ if len(msg) + len(e) > maxlen - 3:
+ msg += "[…]"
+ self.alone = False
+ return msg
+ else:
+ msg += e + ", "
+ self.elt += 1
+ self.pop()
+ return msg[:len(msg)-2]
+
+ else:
+ if len(elts.encode()) <= maxlen:
+ self.pop()
+ if self.count is not None and not self.alone:
+ return msg + elts + (self.count % len(self.messages))
+ else:
+ return msg + elts
+
+ else:
+ words = elts.split(' ')
+
+ if len(words[0].encode()) > maxlen - len(msg.encode()):
+ self.elt += maxlen - len(msg.encode())
+ return msg + elts[:self.elt] + "[…]"
+
+ for w in words:
+ if len(msg.encode()) + len(w.encode()) >= maxlen:
+ msg += "[…]"
+ self.alone = False
+ return msg
+ else:
+ msg += w + " "
+ self.elt += len(w) + 1
+ self.pop()
+ return msg
+
+
+SERVERS = dict()
+
+
+# MODULE INTERFACE ####################################################
+
+@hook.post()
+def parseresponse(res):
+ # TODO: handle inter-bot communication NOMORE
+ # TODO: check that the response is not the one already saved
+ if isinstance(res, Response):
+ if res.server not in SERVERS:
+ SERVERS[res.server] = dict()
+ for receiver in res.to:
+ if receiver in SERVERS[res.server]:
+ nw, bk = SERVERS[res.server][receiver]
+ else:
+ nw, bk = None, None
+ if nw != res:
+ SERVERS[res.server][receiver] = (res, bk)
+ return res
+
+
+@hook.command("more")
+def cmd_more(msg):
+ """Display next chunck of the message"""
+ res = list()
+ if msg.server in SERVERS:
+ for receiver in msg.to_response:
+ if receiver in SERVERS[msg.server]:
+ nw, bk = SERVERS[msg.server][receiver]
+ if nw is not None and not nw.alone:
+ bk = nw
+ SERVERS[msg.server][receiver] = None, bk
+ if bk is not None:
+ res.append(bk)
+ return res
+
+
+@hook.command("next")
+def cmd_next(msg):
+ """Display the next information include in the message"""
+ res = list()
+ if msg.server in SERVERS:
+ for receiver in msg.to_response:
+ if receiver in SERVERS[msg.server]:
+ nw, bk = SERVERS[msg.server][receiver]
+ if nw is not None and not nw.alone:
+ bk = nw
+ SERVERS[msg.server][receiver] = None, bk
+ bk.pop()
+ if bk is not None:
+ res.append(bk)
+ return res
diff --git a/nemubot/modulecontext.py b/nemubot/modulecontext.py
new file mode 100644
index 0000000..4af3731
--- /dev/null
+++ b/nemubot/modulecontext.py
@@ -0,0 +1,155 @@
+# Nemubot is a smart and modulable IM bot.
+# Copyright (C) 2012-2017 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 .
+
+class _ModuleContext:
+
+ def __init__(self, module=None, knodes=None):
+ self.module = module
+
+ if module is not None:
+ self.module_name = (module.__spec__.name if hasattr(module, "__spec__") else module.__name__).replace("nemubot.module.", "")
+ else:
+ self.module_name = ""
+
+ self.hooks = list()
+ self.events = list()
+ self.debug = False
+
+ from nemubot.config.module import Module
+ self.config = Module(self.module_name)
+ self._knodes = knodes
+
+
+ def load_data(self):
+ from nemubot.tools.xmlparser import module_state
+ return module_state.ModuleState("nemubotstate")
+
+ def set_knodes(self, knodes):
+ self._knodes = knodes
+
+ def set_default(self, default):
+ # Access to data will trigger the load of data
+ if self.data is None:
+ self._data = default
+
+ def add_hook(self, hook, *triggers):
+ from nemubot.hooks import Abstract as AbstractHook
+ assert isinstance(hook, AbstractHook), hook
+ self.hooks.append((triggers, hook))
+
+ def del_hook(self, hook, *triggers):
+ from nemubot.hooks import Abstract as AbstractHook
+ assert isinstance(hook, AbstractHook), hook
+ self.hooks.remove((triggers, hook))
+
+ def subtreat(self, msg):
+ return None
+
+ def add_event(self, evt, eid=None):
+ return self.events.append((evt, eid))
+
+ def del_event(self, evt):
+ for i in self.events:
+ e, eid = i
+ if e == evt:
+ self.events.remove(i)
+ return True
+ return False
+
+ def send_response(self, server, res):
+ self.module.logger.info("Send response: %s", res)
+
+ def save(self):
+ self.context.datastore.save(self.module_name, self.data)
+
+ def subparse(self, orig, cnt):
+ if orig.server in self.context.servers:
+ return self.context.servers[orig.server].subparse(orig, cnt)
+
+ @property
+ def data(self):
+ if not hasattr(self, "_data"):
+ self._data = self.load_data()
+ return self._data
+
+
+ def unload(self):
+ """Perform actions for unloading the module"""
+
+ # Remove registered hooks
+ for (s, h) in self.hooks:
+ self.del_hook(h, *s)
+
+ # Remove registered events
+ for evt, eid in self.events:
+ self.del_event(evt)
+
+ self.save()
+
+
+class ModuleContext(_ModuleContext):
+
+ def __init__(self, context, *args, **kwargs):
+ """Initialize the module context
+
+ arguments:
+ context -- the bot context
+ module -- the module
+ """
+
+ super().__init__(*args, **kwargs)
+
+ # Load module configuration if exists
+ if self.module_name in context.modules_configuration:
+ self.config = context.modules_configuration[self.module_name]
+
+ self.context = context
+ self.debug = context.debug
+
+
+ def load_data(self):
+ return self.context.datastore.load(self.module_name, self._knodes)
+
+ def add_hook(self, hook, *triggers):
+ from nemubot.hooks import Abstract as AbstractHook
+ assert isinstance(hook, AbstractHook), hook
+ self.hooks.append((triggers, hook))
+ return self.context.treater.hm.add_hook(hook, *triggers)
+
+ def del_hook(self, hook, *triggers):
+ from nemubot.hooks import Abstract as AbstractHook
+ assert isinstance(hook, AbstractHook), hook
+ self.hooks.remove((triggers, hook))
+ return self.context.treater.hm.del_hooks(*triggers, hook=hook)
+
+ def subtreat(self, msg):
+ yield from self.context.treater.treat_msg(msg)
+
+ def add_event(self, evt, eid=None):
+ return self.context.add_event(evt, eid, module_src=self.module)
+
+ def del_event(self, evt):
+ return self.context.del_event(evt, module_src=self.module)
+
+ def send_response(self, server, res):
+ if server in self.context.servers:
+ if res.server is not None:
+ return self.context.servers[res.server].send_response(res)
+ else:
+ return self.context.servers[server].send_response(res)
+ else:
+ self.module.logger.error("Try to send a message to the unknown server: %s", server)
+ return False
diff --git a/nemubot/server/IRCLib.py b/nemubot/server/IRCLib.py
new file mode 100644
index 0000000..eb7c16f
--- /dev/null
+++ b/nemubot/server/IRCLib.py
@@ -0,0 +1,375 @@
+# Nemubot is a smart and modulable IM bot.
+# Copyright (C) 2012-2026 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 .
+
+from datetime import datetime
+import shlex
+import threading
+
+import irc.bot
+import irc.client
+import irc.connection
+
+import nemubot.message as message
+from nemubot.server.threaded import ThreadedServer
+
+
+class _IRCBotAdapter(irc.bot.SingleServerIRCBot):
+
+ """Internal adapter that bridges the irc library event model to nemubot.
+
+ Subclasses SingleServerIRCBot to get automatic reconnection, PING/PONG,
+ and nick-collision handling for free.
+ """
+
+ def __init__(self, server_name, push_fn, channels, on_connect_cmds,
+ nick, server_list, owner=None, realname="Nemubot",
+ encoding="utf-8", **connect_params):
+ super().__init__(server_list, nick, realname, **connect_params)
+ self._nemubot_name = server_name
+ self._push = push_fn
+ self._channels_to_join = channels
+ self._on_connect_cmds = on_connect_cmds or []
+ self.owner = owner
+ self.encoding = encoding
+ self._stop_event = threading.Event()
+
+
+ # Event loop control
+
+ def start(self):
+ """Run the reactor loop until stop() is called."""
+ self._connect()
+ while not self._stop_event.is_set():
+ self.reactor.process_once(timeout=0.2)
+
+ def stop(self):
+ """Signal the loop to exit and disconnect cleanly."""
+ self._stop_event.set()
+ try:
+ self.connection.disconnect("Goodbye")
+ except Exception:
+ pass
+
+ def on_disconnect(self, connection, event):
+ """Reconnect automatically unless we are shutting down."""
+ if not self._stop_event.is_set():
+ self.jump_server()
+
+
+ # Connection lifecycle
+
+ def on_welcome(self, connection, event):
+ """001 — run on_connect commands then join channels."""
+ for cmd in self._on_connect_cmds:
+ if callable(cmd):
+ for c in (cmd() or []):
+ connection.send_raw(c)
+ else:
+ connection.send_raw(cmd)
+
+ for ch in self._channels_to_join:
+ if isinstance(ch, tuple):
+ connection.join(ch[0], ch[1] if len(ch) > 1 else "")
+ elif hasattr(ch, 'name'):
+ connection.join(ch.name, getattr(ch, 'password', "") or "")
+ else:
+ connection.join(str(ch))
+
+ def on_invite(self, connection, event):
+ """Auto-join on INVITE."""
+ if event.arguments:
+ connection.join(event.arguments[0])
+
+
+ # CTCP
+
+ def on_ctcp(self, connection, event):
+ """Handle CTCP requests (irc library >= 19 dispatches all to on_ctcp)."""
+ nick = irc.client.NickMask(event.source).nick
+ ctcp_type = event.arguments[0].upper() if event.arguments else ""
+ ctcp_arg = event.arguments[1] if len(event.arguments) > 1 else ""
+ self._reply_ctcp(connection, nick, ctcp_type, ctcp_arg)
+
+ # Fallbacks for older irc library versions that dispatch per-type
+ def on_ctcpversion(self, connection, event):
+ import nemubot
+ nick = irc.client.NickMask(event.source).nick
+ connection.ctcp_reply(nick, "VERSION nemubot v%s" % nemubot.__version__)
+
+ def on_ctcpping(self, connection, event):
+ nick = irc.client.NickMask(event.source).nick
+ arg = event.arguments[0] if event.arguments else ""
+ connection.ctcp_reply(nick, "PING %s" % arg)
+
+ def _reply_ctcp(self, connection, nick, ctcp_type, ctcp_arg):
+ import nemubot
+ responses = {
+ "ACTION": None, # handled as on_action
+ "CLIENTINFO": "CLIENTINFO ACTION CLIENTINFO FINGER PING SOURCE TIME USERINFO VERSION",
+ "FINGER": "FINGER nemubot v%s" % nemubot.__version__,
+ "PING": "PING %s" % ctcp_arg,
+ "SOURCE": "SOURCE https://github.com/nemunaire/nemubot",
+ "TIME": "TIME %s" % datetime.now(),
+ "USERINFO": "USERINFO Nemubot",
+ "VERSION": "VERSION nemubot v%s" % nemubot.__version__,
+ }
+ if ctcp_type in responses and responses[ctcp_type] is not None:
+ connection.ctcp_reply(nick, responses[ctcp_type])
+
+
+ # Incoming messages
+
+ def _decode(self, text):
+ if isinstance(text, bytes):
+ try:
+ return text.decode("utf-8")
+ except UnicodeDecodeError:
+ return text.decode(self.encoding, "replace")
+ return text
+
+ def _make_message(self, connection, source, target, text):
+ """Convert raw IRC event data into a nemubot bot message."""
+ nick = irc.client.NickMask(source).nick
+ text = self._decode(text)
+ bot_nick = connection.get_nickname()
+ is_channel = irc.client.is_channel(target)
+ to = [target] if is_channel else [nick]
+ to_response = [target] if is_channel else [nick]
+
+ common = dict(
+ server=self._nemubot_name,
+ to=to,
+ to_response=to_response,
+ frm=nick,
+ frm_owner=(nick == self.owner),
+ )
+
+ # "botname: text" or "botname, text"
+ if (text.startswith(bot_nick + ":") or
+ text.startswith(bot_nick + ",")):
+ inner = text[len(bot_nick) + 1:].strip()
+ return message.DirectAsk(designated=bot_nick, message=inner,
+ **common)
+
+ # "!command [args]"
+ if len(text) > 1 and text[0] == '!':
+ inner = text[1:].strip()
+ try:
+ args = shlex.split(inner)
+ except ValueError:
+ args = inner.split()
+ if args:
+ # Extract @key=value named arguments (same logic as IRC.py)
+ kwargs = {}
+ while len(args) > 1:
+ arg = args[1]
+ if len(arg) > 2 and arg[0:2] == '\\@':
+ args[1] = arg[1:]
+ elif len(arg) > 1 and arg[0] == '@':
+ arsp = arg[1:].split("=", 1)
+ kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None
+ args.pop(1)
+ continue
+ break
+ return message.Command(cmd=args[0], args=args[1:],
+ kwargs=kwargs, **common)
+
+ return message.Text(message=text, **common)
+
+ def on_pubmsg(self, connection, event):
+ msg = self._make_message(
+ connection, event.source, event.target,
+ event.arguments[0] if event.arguments else "",
+ )
+ if msg:
+ self._push(msg)
+
+ def on_privmsg(self, connection, event):
+ nick = irc.client.NickMask(event.source).nick
+ msg = self._make_message(
+ connection, event.source, nick,
+ event.arguments[0] if event.arguments else "",
+ )
+ if msg:
+ self._push(msg)
+
+ def on_action(self, connection, event):
+ """CTCP ACTION (/me) — delivered as a plain Text message."""
+ nick = irc.client.NickMask(event.source).nick
+ text = "/me %s" % (event.arguments[0] if event.arguments else "")
+ is_channel = irc.client.is_channel(event.target)
+ to = [event.target] if is_channel else [nick]
+ self._push(message.Text(
+ message=text,
+ server=self._nemubot_name,
+ to=to, to_response=to,
+ frm=nick, frm_owner=(nick == self.owner),
+ ))
+
+
+class IRCLib(ThreadedServer):
+
+ """IRC server using the irc Python library (jaraco).
+
+ Compared to the hand-rolled IRC.py implementation, this gets:
+ - Automatic exponential-backoff reconnection
+ - PING/PONG handled transparently
+ - Nick-collision suffix logic built-in
+ """
+
+ def __init__(self, host="localhost", port=6667, nick="nemubot",
+ username=None, password=None, realname="Nemubot",
+ encoding="utf-8", owner=None, channels=None,
+ on_connect=None, ssl=False, **kwargs):
+ """Prepare a connection to an IRC server.
+
+ Keyword arguments:
+ host -- IRC server hostname
+ port -- IRC server port (default 6667)
+ nick -- bot's nickname
+ username -- username for USER command (defaults to nick)
+ password -- server password (sent as PASS)
+ realname -- bot's real name
+ encoding -- fallback encoding for non-UTF-8 servers
+ owner -- nick of the bot's owner (sets frm_owner on messages)
+ channels -- list of channel names / (name, key) tuples to join
+ on_connect -- list of raw IRC commands (or a callable returning one)
+ to send after receiving 001
+ ssl -- wrap the connection in TLS
+ """
+ name = (username or nick) + "@" + host + ":" + str(port)
+ super().__init__(name=name)
+
+ self._host = host
+ self._port = int(port)
+ self._nick = nick
+ self._username = username or nick
+ self._password = password
+ self._realname = realname
+ self._encoding = encoding
+ self.owner = owner
+ self._channels = channels or []
+ self._on_connect_cmds = on_connect
+ self._ssl = ssl
+
+ self._bot = None
+ self._thread = None
+
+
+ # ThreadedServer hooks
+
+ def _start(self):
+ server_list = [irc.bot.ServerSpec(self._host, self._port,
+ self._password)]
+
+ connect_params = {"username": self._username}
+
+ if self._ssl:
+ import ssl as ssl_mod
+ ctx = ssl_mod.create_default_context()
+ host = self._host # capture for closure
+ connect_params["connect_factory"] = irc.connection.Factory(
+ wrapper=lambda sock: ctx.wrap_socket(sock,
+ server_hostname=host)
+ )
+
+ self._bot = _IRCBotAdapter(
+ server_name=self.name,
+ push_fn=self._push_message,
+ channels=self._channels,
+ on_connect_cmds=self._on_connect_cmds,
+ nick=self._nick,
+ server_list=server_list,
+ owner=self.owner,
+ realname=self._realname,
+ encoding=self._encoding,
+ **connect_params,
+ )
+ self._thread = threading.Thread(
+ target=self._bot.start,
+ daemon=True,
+ name="nemubot.IRC/" + self.name,
+ )
+ self._thread.start()
+
+ def _stop(self):
+ if self._bot:
+ self._bot.stop()
+ if self._thread:
+ self._thread.join(timeout=5)
+
+
+ # Outgoing messages
+
+ def send_response(self, response):
+ if response is None:
+ return
+ if isinstance(response, list):
+ for r in response:
+ self.send_response(r)
+ return
+ if not self._bot:
+ return
+
+ from nemubot.message.printer.IRCLib import IRCLib as IRCLibPrinter
+ printer = IRCLibPrinter(self._bot.connection)
+ response.accept(printer)
+
+
+ # subparse: re-parse a plain string in the context of an existing message
+ # (used by alias, rnd, grep, cat, smmry, sms modules)
+
+ def subparse(self, orig, cnt):
+ bot_nick = (self._bot.connection.get_nickname()
+ if self._bot else self._nick)
+ common = dict(
+ server=self.name,
+ to=orig.to,
+ to_response=orig.to_response,
+ frm=orig.frm,
+ frm_owner=orig.frm_owner,
+ date=orig.date,
+ )
+ text = cnt
+
+ if (text.startswith(bot_nick + ":") or
+ text.startswith(bot_nick + ",")):
+ inner = text[len(bot_nick) + 1:].strip()
+ return message.DirectAsk(designated=bot_nick, message=inner,
+ **common)
+
+ if len(text) > 1 and text[0] == '!':
+ inner = text[1:].strip()
+ try:
+ args = shlex.split(inner)
+ except ValueError:
+ args = inner.split()
+ if args:
+ kwargs = {}
+ while len(args) > 1:
+ arg = args[1]
+ if len(arg) > 2 and arg[0:2] == '\\@':
+ args[1] = arg[1:]
+ elif len(arg) > 1 and arg[0] == '@':
+ arsp = arg[1:].split("=", 1)
+ kwargs[arsp[0]] = arsp[1] if len(arsp) == 2 else None
+ args.pop(1)
+ continue
+ break
+ return message.Command(cmd=args[0], args=args[1:],
+ kwargs=kwargs, **common)
+
+ return message.Text(message=text, **common)
diff --git a/nemubot/server/Matrix.py b/nemubot/server/Matrix.py
new file mode 100644
index 0000000..ed4b746
--- /dev/null
+++ b/nemubot/server/Matrix.py
@@ -0,0 +1,200 @@
+# Nemubot is a smart and modulable IM bot.
+# Copyright (C) 2012-2026 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 .
+
+import asyncio
+import shlex
+import threading
+
+import nemubot.message as message
+from nemubot.server.threaded import ThreadedServer
+
+
+class Matrix(ThreadedServer):
+
+ """Matrix server implementation using matrix-nio's AsyncClient.
+
+ Runs an asyncio event loop in a daemon thread. Incoming room messages are
+ converted to nemubot bot messages and pushed through the pipe; outgoing
+ responses are sent via the async client from the same event loop.
+ """
+
+ def __init__(self, homeserver, user_id, password=None, access_token=None,
+ owner=None, nick=None, channels=None, **kwargs):
+ """Prepare a connection to a Matrix homeserver.
+
+ Keyword arguments:
+ homeserver -- base URL of the homeserver, e.g. "https://matrix.org"
+ user_id -- full MXID (@user:server) or bare localpart
+ password -- login password (required if no access_token)
+ access_token -- pre-obtained access token (alternative to password)
+ owner -- MXID of the bot owner (marks frm_owner on messages)
+ nick -- display name / prefix for DirectAsk detection
+ channels -- list of room IDs / aliases to join on connect
+ """
+
+ # Ensure fully-qualified MXID
+ if not user_id.startswith("@"):
+ host = homeserver.split("//")[-1].rstrip("/")
+ user_id = "@%s:%s" % (user_id, host)
+
+ super().__init__(name=user_id)
+
+ self.homeserver = homeserver
+ self.user_id = user_id
+ self.password = password
+ self.access_token = access_token
+ self.owner = owner
+ self.nick = nick or user_id
+
+ self._initial_rooms = channels or []
+ self._client = None
+ self._loop = None
+ self._thread = None
+
+
+ # Open/close
+
+ def _start(self):
+ self._thread = threading.Thread(
+ target=self._run_loop,
+ daemon=True,
+ name="nemubot.Matrix/" + self._name,
+ )
+ self._thread.start()
+
+ def _stop(self):
+ if self._client and self._loop and not self._loop.is_closed():
+ try:
+ asyncio.run_coroutine_threadsafe(
+ self._client.close(), self._loop
+ ).result(timeout=5)
+ except Exception:
+ self._logger.exception("Error while closing Matrix client")
+ if self._thread:
+ self._thread.join(timeout=5)
+
+
+ # Asyncio thread
+
+ def _run_loop(self):
+ self._loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self._loop)
+ try:
+ self._loop.run_until_complete(self._async_main())
+ except Exception:
+ self._logger.exception("Unhandled exception in Matrix event loop")
+ finally:
+ self._loop.close()
+
+ async def _async_main(self):
+ from nio import AsyncClient, LoginError, RoomMessageText
+
+ self._client = AsyncClient(self.homeserver, self.user_id)
+
+ if self.access_token:
+ self._client.access_token = self.access_token
+ self._logger.info("Using provided access token for %s", self.user_id)
+ elif self.password:
+ resp = await self._client.login(self.password)
+ if isinstance(resp, LoginError):
+ self._logger.error("Matrix login failed: %s", resp.message)
+ return
+ self._logger.info("Logged in to Matrix as %s", self.user_id)
+ else:
+ self._logger.error("Need either password or access_token to connect")
+ return
+
+ self._client.add_event_callback(self._on_room_message, RoomMessageText)
+
+ for room in self._initial_rooms:
+ await self._client.join(room)
+ self._logger.info("Joined room %s", room)
+
+ await self._client.sync_forever(timeout=30000, full_state=True)
+
+
+ # Incoming messages
+
+ async def _on_room_message(self, room, event):
+ """Callback invoked by matrix-nio for each m.room.message event."""
+
+ if event.sender == self.user_id:
+ return # ignore own messages
+
+ text = event.body
+ room_id = room.room_id
+ frm = event.sender
+
+ common_args = {
+ "server": self.name,
+ "to": [room_id],
+ "to_response": [room_id],
+ "frm": frm,
+ "frm_owner": frm == self.owner,
+ }
+
+ if len(text) > 1 and text[0] == '!':
+ text = text[1:].strip()
+ try:
+ args = shlex.split(text)
+ except ValueError:
+ args = text.split(' ')
+ msg = message.Command(cmd=args[0], args=args[1:], **common_args)
+
+ elif (text.lower().startswith(self.nick.lower() + ":")
+ or text.lower().startswith(self.nick.lower() + ",")):
+ text = text[len(self.nick) + 1:].strip()
+ msg = message.DirectAsk(designated=self.nick, message=text,
+ **common_args)
+
+ else:
+ msg = message.Text(message=text, **common_args)
+
+ self._push_message(msg)
+
+
+ # Outgoing messages
+
+ def send_response(self, response):
+ if response is None:
+ return
+ if isinstance(response, list):
+ for r in response:
+ self.send_response(r)
+ return
+
+ from nemubot.message.printer.Matrix import Matrix as MatrixPrinter
+ printer = MatrixPrinter(self._send_text)
+ response.accept(printer)
+
+ def _send_text(self, room_id, text):
+ """Thread-safe: schedule a Matrix room_send on the asyncio loop."""
+ if not self._client or not self._loop or self._loop.is_closed():
+ self._logger.warning("Cannot send: Matrix client not ready")
+ return
+ future = asyncio.run_coroutine_threadsafe(
+ self._client.room_send(
+ room_id=room_id,
+ message_type="m.room.message",
+ content={"msgtype": "m.text", "body": text},
+ ignore_unverified_devices=True,
+ ),
+ self._loop,
+ )
+ future.add_done_callback(
+ lambda f: self._logger.warning("Matrix send error: %s", f.exception())
+ if not f.cancelled() and f.exception() else None
+ )
diff --git a/nemubot/server/__init__.py b/nemubot/server/__init__.py
new file mode 100644
index 0000000..db9ad87
--- /dev/null
+++ b/nemubot/server/__init__.py
@@ -0,0 +1,98 @@
+# 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 .
+
+
+def factory(uri, ssl=False, **init_args):
+ from urllib.parse import urlparse, unquote, parse_qs
+ o = urlparse(uri)
+
+ srv = None
+
+ if o.scheme == "irc" or o.scheme == "ircs":
+ # https://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt
+ # https://www-archive.mozilla.org/projects/rt-messaging/chatzilla/irc-urls.html
+ args = dict(init_args)
+
+ if o.scheme == "ircs": ssl = True
+ if o.hostname is not None: args["host"] = o.hostname
+ if o.port is not None: args["port"] = o.port
+ if o.username is not None: args["username"] = o.username
+ if o.password is not None: args["password"] = unquote(o.password)
+
+ modifiers = o.path.split(",")
+ target = unquote(modifiers.pop(0)[1:])
+
+ # Read query string
+ params = parse_qs(o.query)
+
+ if "msg" in params:
+ if "on_connect" not in args:
+ args["on_connect"] = []
+ args["on_connect"].append("PRIVMSG %s :%s" % (target, params["msg"][0]))
+
+ if "key" in params:
+ if "channels" not in args:
+ args["channels"] = []
+ args["channels"].append((target, params["key"][0]))
+
+ if "pass" in params:
+ args["password"] = params["pass"][0]
+
+ if "charset" in params:
+ args["encoding"] = params["charset"][0]
+
+ if "channels" not in args and "isnick" not in modifiers:
+ args["channels"] = [target]
+
+ args["ssl"] = ssl
+
+ from nemubot.server.IRCLib import IRCLib as IRCServer
+ srv = IRCServer(**args)
+
+ elif o.scheme == "matrix":
+ # matrix://localpart:password@homeserver.tld/!room:homeserver.tld
+ # matrix://localpart:password@homeserver.tld/%23alias:homeserver.tld
+ # Use matrixs:// for https (default) vs http
+ args = dict(init_args)
+
+ homeserver = "https://" + o.hostname
+ if o.port is not None:
+ homeserver += ":%d" % o.port
+ args["homeserver"] = homeserver
+
+ if o.username is not None:
+ args["user_id"] = o.username
+ if o.password is not None:
+ args["password"] = unquote(o.password)
+
+ # Parse rooms from path (comma-separated, URL-encoded)
+ if o.path and o.path != "/":
+ rooms = [unquote(r) for r in o.path.lstrip("/").split(",") if r]
+ if rooms:
+ args.setdefault("channels", []).extend(rooms)
+
+ params = parse_qs(o.query)
+ if "token" in params:
+ args["access_token"] = params["token"][0]
+ if "nick" in params:
+ args["nick"] = params["nick"][0]
+ if "owner" in params:
+ args["owner"] = params["owner"][0]
+
+ from nemubot.server.Matrix import Matrix as MatrixServer
+ srv = MatrixServer(**args)
+
+ return srv
diff --git a/nemubot/server/abstract.py b/nemubot/server/abstract.py
new file mode 100644
index 0000000..8fbb923
--- /dev/null
+++ b/nemubot/server/abstract.py
@@ -0,0 +1,167 @@
+# 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 .
+
+import logging
+import queue
+import traceback
+
+from nemubot.bot import sync_act
+
+
+class AbstractServer:
+
+ """An abstract server: handle communication with an IM server"""
+
+ def __init__(self, name, fdClass, **kwargs):
+ """Initialize an abstract server
+
+ Keyword argument:
+ name -- Identifier of the socket, for convinience
+ fdClass -- Class to instantiate as support file
+ """
+
+ self._name = name
+ self._fd = fdClass(**kwargs)
+
+ self._logger = logging.getLogger("nemubot.server." + str(self.name))
+ self._readbuffer = b''
+ self._sending_queue = queue.Queue()
+
+
+ @property
+ def name(self):
+ if self._name is not None:
+ return self._name
+ else:
+ return self._fd.fileno()
+
+
+ # Open/close
+
+ def connect(self, *args, **kwargs):
+ """Register the server in _poll"""
+
+ self._logger.info("Opening connection")
+
+ self._fd.connect(*args, **kwargs)
+
+ self._on_connect()
+
+ def _on_connect(self):
+ sync_act("sckt", "register", self._fd.fileno())
+
+
+ def close(self, *args, **kwargs):
+ """Unregister the server from _poll"""
+
+ self._logger.info("Closing connection")
+
+ if self._fd.fileno() > 0:
+ sync_act("sckt", "unregister", self._fd.fileno())
+
+ self._fd.close(*args, **kwargs)
+
+
+ # Writes
+
+ def write(self, message):
+ """Asynchronymously send a message to the server using send_callback
+
+ Argument:
+ message -- message to send
+ """
+
+ self._sending_queue.put(self.format(message))
+ self._logger.debug("Message '%s' appended to write queue coming from %s:%d in %s", message, *traceback.extract_stack(limit=3)[0][:3])
+ sync_act("sckt", "write", self._fd.fileno())
+
+
+ def async_write(self):
+ """Internal function used when the file descriptor is writable"""
+
+ try:
+ sync_act("sckt", "unwrite", self._fd.fileno())
+ while not self._sending_queue.empty():
+ self._write(self._sending_queue.get_nowait())
+ self._sending_queue.task_done()
+
+ except queue.Empty:
+ pass
+
+
+ def send_response(self, response):
+ """Send a formated Message class
+
+ Argument:
+ response -- message to send
+ """
+
+ if response is None:
+ return
+
+ elif isinstance(response, list):
+ for r in response:
+ self.send_response(r)
+
+ else:
+ vprnt = self.printer()
+ response.accept(vprnt)
+ self.write(vprnt.pp)
+
+
+ # Read
+
+ def async_read(self):
+ """Internal function used when the file descriptor is readable
+
+ Returns:
+ A list of fully received messages
+ """
+
+ ret, self._readbuffer = self.lex(self._readbuffer + self.read())
+
+ for r in ret:
+ yield r
+
+
+ def lex(self, buf):
+ """Assume lexing in default case is per line
+
+ Argument:
+ buf -- buffer to lex
+ """
+
+ msgs = buf.split(b'\r\n')
+ partial = msgs.pop()
+
+ return msgs, partial
+
+
+ def parse(self, msg):
+ raise NotImplemented
+
+
+ # Exceptions
+
+ def exception(self, flags):
+ """Exception occurs on fd"""
+
+ self._fd.close()
+
+ # Proxy
+
+ def fileno(self):
+ return self._fd.fileno()
diff --git a/nemubot/server/socket.py b/nemubot/server/socket.py
new file mode 100644
index 0000000..bf55bf5
--- /dev/null
+++ b/nemubot/server/socket.py
@@ -0,0 +1,172 @@
+# 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 .
+
+import os
+import socket
+
+import nemubot.message as message
+from nemubot.message.printer.socket import Socket as SocketPrinter
+from nemubot.server.abstract import AbstractServer
+
+
+class _Socket(AbstractServer):
+
+ """Concrete implementation of a socket connection"""
+
+ def __init__(self, printer=SocketPrinter, **kwargs):
+ """Create a server socket
+ """
+
+ super().__init__(**kwargs)
+
+ self.readbuffer = b''
+ self.printer = printer
+
+
+ # Write
+
+ def _write(self, cnt):
+ self._fd.sendall(cnt)
+
+
+ def format(self, txt):
+ if isinstance(txt, bytes):
+ return txt + b'\r\n'
+ else:
+ return txt.encode() + b'\r\n'
+
+
+ # Read
+
+ def read(self, bufsize=1024, *args, **kwargs):
+ return self._fd.recv(bufsize, *args, **kwargs)
+
+
+ def parse(self, line):
+ """Implement a default behaviour for socket"""
+ import shlex
+
+ line = line.strip().decode()
+ try:
+ args = shlex.split(line)
+ except ValueError:
+ args = line.split(' ')
+
+ if len(args):
+ yield message.Command(cmd=args[0], args=args[1:], server=self._fd.fileno(), to=["you"], frm="you")
+
+
+ def subparse(self, orig, cnt):
+ for m in self.parse(cnt):
+ m.to = orig.to
+ m.frm = orig.frm
+ m.date = orig.date
+ yield m
+
+
+class SocketServer(_Socket):
+
+ def __init__(self, host, port, bind=None, trynb=0, **kwargs):
+ destlist = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)
+ (family, type, proto, canonname, self._sockaddr) = destlist[trynb%len(destlist)]
+
+ super().__init__(fdClass=socket.socket, family=family, type=type, proto=proto, **kwargs)
+
+ self._bind = bind
+
+
+ def connect(self):
+ self._logger.info("Connecting to %s:%d", *self._sockaddr[:2])
+ super().connect(self._sockaddr)
+ self._logger.info("Connected to %s:%d", *self._sockaddr[:2])
+
+ if self._bind:
+ self._fd.bind(self._bind)
+
+
+class UnixSocket:
+
+ def __init__(self, location, **kwargs):
+ super().__init__(fdClass=socket.socket, family=socket.AF_UNIX, **kwargs)
+
+ self._socket_path = location
+
+
+ def connect(self):
+ self._logger.info("Connection to unix://%s", self._socket_path)
+ self.connect(self._socket_path)
+
+
+class SocketClient(_Socket):
+
+ def __init__(self, **kwargs):
+ super().__init__(fdClass=socket.socket, **kwargs)
+
+
+class _Listener:
+
+ def __init__(self, new_server_cb, instanciate=SocketClient, **kwargs):
+ super().__init__(**kwargs)
+
+ self._instanciate = instanciate
+ self._new_server_cb = new_server_cb
+
+
+ def read(self):
+ conn, addr = self._fd.accept()
+ fileno = conn.fileno()
+ self._logger.info("Accept new connection from %s (fd=%d)", addr, fileno)
+
+ ss = self._instanciate(name=self.name + "#" + str(fileno), fileno=conn.detach())
+ ss.connect = ss._on_connect
+ self._new_server_cb(ss, autoconnect=True)
+
+ return b''
+
+
+class UnixSocketListener(_Listener, UnixSocket, _Socket):
+
+ def connect(self):
+ self._logger.info("Creating Unix socket at unix://%s", self._socket_path)
+
+ try:
+ os.remove(self._socket_path)
+ except FileNotFoundError:
+ pass
+
+ self._fd.bind(self._socket_path)
+ self._fd.listen(5)
+ self._logger.info("Socket ready for accepting new connections")
+
+ self._on_connect()
+
+
+ def close(self):
+ import os
+ import socket
+
+ try:
+ self._fd.shutdown(socket.SHUT_RDWR)
+ except socket.error:
+ pass
+
+ super().close()
+
+ try:
+ if self._socket_path is not None:
+ os.remove(self._socket_path)
+ except:
+ pass
diff --git a/nemubot/server/threaded.py b/nemubot/server/threaded.py
new file mode 100644
index 0000000..eb1ae19
--- /dev/null
+++ b/nemubot/server/threaded.py
@@ -0,0 +1,132 @@
+# Nemubot is a smart and modulable IM bot.
+# Copyright (C) 2012-2026 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 .
+
+import logging
+import os
+import queue
+
+from nemubot.bot import sync_act
+
+
+class ThreadedServer:
+
+ """A server backed by a library running in its own thread.
+
+ Uses an os.pipe() as a fake file descriptor to integrate with the bot's
+ select.poll() main loop without requiring direct socket access.
+
+ When the library thread has a message ready, it calls _push_message(),
+ which writes a wakeup byte to the pipe's write end. The bot's poll loop
+ sees the read end become readable, calls async_read(), which drains the
+ message queue and yields already-parsed bot-level messages.
+
+ This abstraction lets any IM library (IRC via python-irc, Matrix via
+ matrix-nio, …) plug into nemubot without touching bot.py.
+ """
+
+ def __init__(self, name):
+ self._name = name
+ self._logger = logging.getLogger("nemubot.server." + name)
+ self._queue = queue.Queue()
+ self._pipe_r, self._pipe_w = os.pipe()
+
+
+ @property
+ def name(self):
+ return self._name
+
+ def fileno(self):
+ return self._pipe_r
+
+
+ # Open/close
+
+ def connect(self):
+ """Start the library and register the pipe read-end with the poll loop."""
+ self._logger.info("Starting connection")
+ self._start()
+ sync_act("sckt", "register", self._pipe_r)
+
+ def _start(self):
+ """Override: start the library's connection (e.g. launch a thread)."""
+ raise NotImplementedError
+
+ def close(self):
+ """Unregister from poll, stop the library, and close the pipe."""
+ self._logger.info("Closing connection")
+ sync_act("sckt", "unregister", self._pipe_r)
+ self._stop()
+ for fd in (self._pipe_w, self._pipe_r):
+ try:
+ os.close(fd)
+ except OSError:
+ pass
+
+ def _stop(self):
+ """Override: stop the library thread gracefully."""
+ pass
+
+
+ # Writes
+
+ def send_response(self, response):
+ """Override: send a response via the underlying library."""
+ raise NotImplementedError
+
+ def async_write(self):
+ """No-op: writes go directly through the library, not via poll."""
+ pass
+
+
+ # Read
+
+ def _push_message(self, msg):
+ """Called from the library thread to enqueue a bot-level message.
+
+ Writes a wakeup byte to the pipe so the main loop wakes up and
+ calls async_read().
+ """
+ self._queue.put(msg)
+ try:
+ os.write(self._pipe_w, b'\x00')
+ except OSError:
+ pass # pipe closed during shutdown
+
+ def async_read(self):
+ """Called by the bot when the pipe is readable.
+
+ Drains the wakeup bytes and yields all queued bot messages.
+ """
+ try:
+ os.read(self._pipe_r, 256)
+ except OSError:
+ return
+ while not self._queue.empty():
+ try:
+ yield self._queue.get_nowait()
+ except queue.Empty:
+ break
+
+ def parse(self, msg):
+ """Messages pushed via _push_message are already bot-level — pass through."""
+ yield msg
+
+
+ # Exceptions
+
+ def exception(self, flags):
+ """Called by the bot on POLLERR/POLLHUP/POLLNVAL."""
+ self._logger.warning("Exception on server %s: flags=0x%x", self._name, flags)
diff --git a/nemubot/tools/__init__.py b/nemubot/tools/__init__.py
new file mode 100644
index 0000000..57f3468
--- /dev/null
+++ b/nemubot/tools/__init__.py
@@ -0,0 +1,15 @@
+# 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 .
diff --git a/nemubot/tools/countdown.py b/nemubot/tools/countdown.py
new file mode 100644
index 0000000..afd585f
--- /dev/null
+++ b/nemubot/tools/countdown.py
@@ -0,0 +1,108 @@
+# 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 .
+
+def countdown(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(date, msg_before, msg_after, tz=None):
+ """Replace in a text %s by a sentence incidated the remaining time
+ before/after an event"""
+ if tz is not None:
+ import os
+ oldtz = os.environ['TZ']
+ os.environ['TZ'] = tz
+
+ import time
+ time.tzset()
+
+ from datetime import datetime, timezone
+
+ # Calculate time before the date
+ try:
+ if datetime.now(timezone.utc) > date:
+ sentence_c = msg_after
+ delta = datetime.now(timezone.utc) - date
+ else:
+ sentence_c = msg_before
+ delta = date - datetime.now(timezone.utc)
+ except TypeError:
+ if datetime.now() > date:
+ sentence_c = msg_after
+ delta = datetime.now() - date
+ else:
+ sentence_c = msg_before
+ delta = date - datetime.now()
+
+ if tz is not None:
+ import os
+ os.environ['TZ'] = oldtz
+
+ return sentence_c % countdown(delta)
diff --git a/nemubot/tools/date.py b/nemubot/tools/date.py
new file mode 100644
index 0000000..9e9bbad
--- /dev/null
+++ b/nemubot/tools/date.py
@@ -0,0 +1,83 @@
+# 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 .
+
+# Extraction/Format text
+
+import re
+
+month_binding = {
+ "janvier": 1, "january": 1, "januar": 1,
+ "fevrier": 2, "février": 2, "february": 2,
+ "march": 3, "mars": 3,
+ "avril": 4, "april": 4,
+ "mai": 5, "may": 5, "maï": 5,
+ "juin": 6, "juni": 6, "junni": 6,
+ "juillet": 7, "jully": 7, "july": 7,
+ "aout": 8, "août": 8, "august": 8,
+ "septembre": 9, "september": 9,
+ "october": 10, "oktober": 10, "octobre": 10,
+ "november": 11, "novembre": 11,
+ "decembre": 12, "décembre": 12, "december": 12,
+}
+
+xtrdt = re.compile(r'''^.*? (?P[0-9]{1,4}) .+?
+ (?P[0-9]{1,2}|"''' + "|".join(month_binding) + '''")
+ (?:.+?(?P[0-9]{1,4}))? (?:[^0-9]+
+ (?:(?P[0-9]{1,2})[^0-9]*[h':]
+ (?:[^0-9]*(?P[0-9]{1,2})
+ (?:[^0-9]*[m\":][^0-9]*(?P[0-9]{1,2}))?)?)?.*?)?
+ $''', re.X)
+
+
+def extractDate(msg):
+ """Parse a message to extract a time and date"""
+ result = xtrdt.match(msg.lower())
+ if result is not None:
+ day = result.group("day")
+ month = result.group("month")
+
+ if month in month_binding:
+ month = month_binding[month]
+
+ year = result.group("year")
+
+ if len(day) == 4:
+ day, year = year, day
+
+ hour = result.group("hour")
+ minute = result.group("minute")
+ second = result.group("second")
+
+ if year is None:
+ from datetime import date
+ year = date.today().year
+ if hour is None:
+ hour = 0
+ if minute is None:
+ minute = 0
+ if second is None:
+ second = 1
+ else:
+ second = int(second) + 1
+ if second > 59:
+ minute = int(minute) + 1
+ second = 0
+
+ from datetime import datetime
+ return datetime(int(year), int(month), int(day),
+ int(hour), int(minute), int(second))
+ else:
+ return None
diff --git a/nemubot/tools/feed.py b/nemubot/tools/feed.py
new file mode 100644
index 0000000..6f8930d
--- /dev/null
+++ b/nemubot/tools/feed.py
@@ -0,0 +1,157 @@
+#!/usr/bin/python3
+
+import datetime
+import time
+from xml.dom.minidom import parse
+from xml.dom.minidom import parseString
+from xml.dom.minidom import getDOMImplementation
+
+
+class AtomEntry:
+
+ def __init__(self, node):
+ if len(node.getElementsByTagName("id")) > 0 and node.getElementsByTagName("id")[0].firstChild is not None:
+ self.id = node.getElementsByTagName("id")[0].firstChild.nodeValue
+ else:
+ self.id = None
+
+ if len(node.getElementsByTagName("title")) > 0 and node.getElementsByTagName("title")[0].firstChild is not None:
+ self.title = node.getElementsByTagName("title")[0].firstChild.nodeValue
+ else:
+ self.title = ""
+
+ try:
+ self.updated = time.strptime(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:19], "%Y-%m-%dT%H:%M:%S")
+ except:
+ try:
+ self.updated = time.strptime(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:10], "%Y-%m-%d")
+ except:
+ print(node.getElementsByTagName("updated")[0].firstChild.nodeValue[:10])
+ self.updated = time.localtime()
+ self.updated = datetime.datetime(*self.updated[:6])
+
+ if len(node.getElementsByTagName("summary")) > 0 and node.getElementsByTagName("summary")[0].firstChild is not None:
+ self.summary = node.getElementsByTagName("summary")[0].firstChild.nodeValue
+ else:
+ self.summary = None
+
+ if len(node.getElementsByTagName("link")) > 0 and node.getElementsByTagName("link")[0].hasAttribute("href"):
+ self.link = node.getElementsByTagName("link")[0].getAttribute("href")
+ else:
+ self.link = None
+
+ if len(node.getElementsByTagName("category")) >= 1 and node.getElementsByTagName("category")[0].hasAttribute("term"):
+ self.category = node.getElementsByTagName("category")[0].getAttribute("term")
+ else:
+ self.category = None
+
+ if len(node.getElementsByTagName("link")) > 1 and node.getElementsByTagName("link")[1].hasAttribute("href"):
+ self.link2 = node.getElementsByTagName("link")[1].getAttribute("href")
+ else:
+ self.link2 = None
+
+
+ def __repr__(self):
+ return "" % (self.title, self.updated)
+
+
+ def __cmp__(self, other):
+ return not (self.id == other.id)
+
+
+class RSSEntry:
+
+ def __init__(self, node):
+ if len(node.getElementsByTagName("guid")) > 0 and node.getElementsByTagName("guid")[0].firstChild is not None:
+ self.id = node.getElementsByTagName("guid")[0].firstChild.nodeValue
+ else:
+ self.id = None
+
+ if len(node.getElementsByTagName("title")) > 0 and node.getElementsByTagName("title")[0].firstChild is not None:
+ self.title = node.getElementsByTagName("title")[0].firstChild.nodeValue
+ else:
+ self.title = ""
+
+ if len(node.getElementsByTagName("pubDate")) > 0 and node.getElementsByTagName("pubDate")[0].firstChild is not None:
+ self.pubDate = node.getElementsByTagName("pubDate")[0].firstChild.nodeValue
+ else:
+ self.pubDate = ""
+
+ if len(node.getElementsByTagName("description")) > 0 and node.getElementsByTagName("description")[0].firstChild is not None:
+ self.summary = node.getElementsByTagName("description")[0].firstChild.nodeValue
+ else:
+ self.summary = None
+
+ if len(node.getElementsByTagName("link")) > 0:
+ self.link = node.getElementsByTagName("link")[0].firstChild.nodeValue
+ else:
+ self.link = None
+
+ if len(node.getElementsByTagName("enclosure")) > 0 and node.getElementsByTagName("enclosure")[0].hasAttribute("url"):
+ self.enclosure = node.getElementsByTagName("enclosure")[0].getAttribute("url")
+ else:
+ self.enclosure = None
+
+
+ def __repr__(self):
+ return "" % (self.title, self.pubDate)
+
+
+ def __cmp__(self, other):
+ return not (self.id == other.id)
+
+
+class Feed:
+
+ def __init__(self, string):
+ self.feed = parseString(string).documentElement
+ self.id = None
+ self.title = None
+ self.updated = None
+ self.entries = list()
+
+ if self.feed.tagName == "rdf:RDF" or self.feed.tagName == "rss":
+ self._parse_rss_feed()
+ elif self.feed.tagName == "feed":
+ self._parse_atom_feed()
+ else:
+ from nemubot.exception import IMException
+ raise IMException("This is not a valid Atom or RSS feed.")
+
+
+ def _parse_atom_feed(self):
+ self.id = self.feed.getElementsByTagName("id")[0].firstChild.nodeValue
+ self.title = self.feed.getElementsByTagName("title")[0].firstChild.nodeValue
+
+ for item in self.feed.getElementsByTagName("entry"):
+ self._add_entry(AtomEntry(item))
+
+
+ def _parse_rss_feed(self):
+ self.title = self.feed.getElementsByTagName("title")[0].firstChild.nodeValue
+
+ for item in self.feed.getElementsByTagName("item"):
+ self._add_entry(RSSEntry(item))
+
+
+ def _add_entry(self, entry):
+ if entry is not None:
+ self.entries.append(entry)
+ if hasattr(entry, "updated") and (self.updated is None or self.updated < entry.updated):
+ self.updated = entry.updated
+
+
+ def __and__(self, b):
+ ret = []
+
+ for e in self.entries:
+ if e not in b.entries:
+ ret.append(e)
+
+ for e in b.entries:
+ if e not in self.entries:
+ ret.append(e)
+
+ # TODO: Sort by date
+
+ return ret
diff --git a/nemubot/tools/human.py b/nemubot/tools/human.py
new file mode 100644
index 0000000..a18cde2
--- /dev/null
+++ b/nemubot/tools/human.py
@@ -0,0 +1,67 @@
+# 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 .
+
+import math
+
+def size(size, unit=True):
+ """Convert a given byte size to an more human readable way
+
+ Argument:
+ size -- the size to convert
+ unit -- append the unit at the end of the string
+ """
+
+ if size <= 0:
+ return "0 B" if unit else "0"
+
+ units = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']
+ p = math.floor(math.log(size, 2) / 10)
+
+ s = size / math.pow(1024, p)
+ r = size % math.pow(1024, p)
+ return (("%.3f" if r else "%.0f") % s) + ((" " + units[int(p)]) if unit else "")
+
+
+def word_distance(str1, str2):
+ """Perform a Damerau-Levenshtein distance on the two given strings"""
+
+ d = [[i + j for j in range(len(str2) + 1)] for i in range(len(str1) + 1)]
+
+ for i in range(0, len(str1)):
+ for j in range(0, len(str2)):
+ cost = 0 if str1[i-1] == str2[j-1] else 1
+ d[i+1][j+1] = min(
+ d[i][j+1] + 1, # deletion
+ d[i+1][j] + 1, # insertion
+ d[i][j] + cost, # substitution
+ )
+ if i >= 1 and j >= 1 and str1[i] == str2[j-1] and str1[i-1] == str2[j]:
+ d[i+1][j+1] = min(
+ d[i+1][j+1],
+ d[i-1][j-1] + cost, # transposition
+ )
+
+ return d[len(str1)][len(str2)]
+
+
+def guess(pattern, expect):
+ if len(expect):
+ se = sorted([(e, word_distance(pattern, e)) for e in expect], key=lambda x: x[1])
+ _, m = se[0]
+ for e, wd in se:
+ if wd > m or wd > 1 + len(pattern) / 4:
+ break
+ yield e
diff --git a/nemubot/tools/test_human.py b/nemubot/tools/test_human.py
new file mode 100644
index 0000000..8ebdd49
--- /dev/null
+++ b/nemubot/tools/test_human.py
@@ -0,0 +1,40 @@
+import unittest
+
+from nemubot.tools.human import guess, size, word_distance
+
+class TestHuman(unittest.TestCase):
+
+ def test_size(self):
+ self.assertEqual(size(42), "42 B")
+ self.assertEqual(size(42, False), "42")
+ self.assertEqual(size(1023), "1023 B")
+ self.assertEqual(size(1024), "1 KiB")
+ self.assertEqual(size(1024, False), "1")
+ self.assertEqual(size(1025), "1.001 KiB")
+ self.assertEqual(size(1025, False), "1.001")
+ self.assertEqual(size(1024000), "1000 KiB")
+ self.assertEqual(size(1024000, False), "1000")
+ self.assertEqual(size(1024 * 1024), "1 MiB")
+ self.assertEqual(size(1024 * 1024, False), "1")
+ self.assertEqual(size(1024 * 1024 * 1024), "1 GiB")
+ self.assertEqual(size(1024 * 1024 * 1024, False), "1")
+ self.assertEqual(size(1024 * 1024 * 1024 * 1024), "1 TiB")
+ self.assertEqual(size(1024 * 1024 * 1024 * 1024, False), "1")
+
+ def test_Levenshtein(self):
+ self.assertEqual(word_distance("", "a"), 1)
+ self.assertEqual(word_distance("a", ""), 1)
+ self.assertEqual(word_distance("a", "a"), 0)
+ self.assertEqual(word_distance("a", "b"), 1)
+ self.assertEqual(word_distance("aa", "ba"), 1)
+ self.assertEqual(word_distance("ba", "ab"), 1)
+ self.assertEqual(word_distance("long", "short"), 4)
+ self.assertEqual(word_distance("long", "short"), word_distance("short", "long"))
+
+ def test_guess(self):
+ self.assertListEqual([g for g in guess("drunk", ["eat", "drink"])], ["drink"])
+ self.assertListEqual([g for g in guess("drunk", ["long", "short"])], [])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/nemubot/tools/test_xmlparser.py b/nemubot/tools/test_xmlparser.py
new file mode 100644
index 0000000..0feda73
--- /dev/null
+++ b/nemubot/tools/test_xmlparser.py
@@ -0,0 +1,113 @@
+import unittest
+
+import io
+import xml.parsers.expat
+
+from nemubot.tools.xmlparser import XMLParser
+
+
+class StringNode():
+ def __init__(self):
+ self.string = ""
+
+ def characters(self, content):
+ self.string += content
+
+ def saveElement(self, store, tag="string"):
+ store.startElement(tag, {})
+ store.characters(self.string)
+ store.endElement(tag)
+
+
+class TestNode():
+ def __init__(self, option=None):
+ self.option = option
+ self.mystr = None
+
+ def addChild(self, name, child):
+ self.mystr = child.string
+ return True
+
+ def saveElement(self, store, tag="test"):
+ store.startElement(tag, {"option": self.option})
+
+ strNode = StringNode()
+ strNode.string = self.mystr
+ strNode.saveElement(store)
+
+ store.endElement(tag)
+
+
+class Test2Node():
+ def __init__(self, option=None):
+ self.option = option
+ self.mystrs = list()
+
+ def startElement(self, name, attrs):
+ if name == "string":
+ self.mystrs.append(attrs["value"])
+ return True
+
+ def saveElement(self, store, tag="test"):
+ store.startElement(tag, {"option": self.option} if self.option is not None else {})
+
+ for mystr in self.mystrs:
+ store.startElement("string", {"value": mystr})
+ store.endElement("string")
+
+ store.endElement(tag)
+
+
+class TestXMLParser(unittest.TestCase):
+
+ def test_parser1(self):
+ p = xml.parsers.expat.ParserCreate()
+ mod = XMLParser({"string": StringNode})
+
+ p.StartElementHandler = mod.startElement
+ p.CharacterDataHandler = mod.characters
+ p.EndElementHandler = mod.endElement
+
+ inputstr = "toto"
+ p.Parse(inputstr, 1)
+
+ self.assertEqual(mod.root.string, "toto")
+ self.assertEqual(mod.saveDocument(header=False).getvalue(), inputstr)
+
+
+ def test_parser2(self):
+ p = xml.parsers.expat.ParserCreate()
+ mod = XMLParser({"string": StringNode, "test": TestNode})
+
+ p.StartElementHandler = mod.startElement
+ p.CharacterDataHandler = mod.characters
+ p.EndElementHandler = mod.endElement
+
+ inputstr = 'toto'
+ p.Parse(inputstr, 1)
+
+ self.assertEqual(mod.root.option, "123")
+ self.assertEqual(mod.root.mystr, "toto")
+ self.assertEqual(mod.saveDocument(header=False).getvalue(), inputstr)
+
+
+ def test_parser3(self):
+ p = xml.parsers.expat.ParserCreate()
+ mod = XMLParser({"string": StringNode, "test": Test2Node})
+
+ p.StartElementHandler = mod.startElement
+ p.CharacterDataHandler = mod.characters
+ p.EndElementHandler = mod.endElement
+
+ inputstr = ''
+ p.Parse(inputstr, 1)
+
+ self.assertEqual(mod.root.option, None)
+ self.assertEqual(len(mod.root.mystrs), 2)
+ self.assertEqual(mod.root.mystrs[0], "toto")
+ self.assertEqual(mod.root.mystrs[1], "toto2")
+ self.assertEqual(mod.saveDocument(header=False, short_empty_elements=True).getvalue(), inputstr)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/nemubot/tools/web.py b/nemubot/tools/web.py
new file mode 100644
index 0000000..a545b19
--- /dev/null
+++ b/nemubot/tools/web.py
@@ -0,0 +1,274 @@
+# 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 .
+
+from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit
+import socket
+
+from nemubot.exception import IMException
+
+
+def isURL(url):
+ """Return True if the URL can be parsed"""
+ o = urlparse(_getNormalizedURL(url))
+ return o.netloc != "" and o.path != ""
+
+
+def _getNormalizedURL(url):
+ """Return a light normalized form for the given URL"""
+ return url if "//" in url or ":" in url else "//" + url
+
+def getNormalizedURL(url):
+ """Return a normalized form for the given URL"""
+ return urlunsplit(urlsplit(_getNormalizedURL(url), "http"))
+
+
+def getScheme(url):
+ """Return the protocol of a given URL"""
+ o = urlparse(url, "http")
+ return o.scheme
+
+
+def getHost(url):
+ """Return the domain of a given URL"""
+ return urlparse(_getNormalizedURL(url), "http").hostname
+
+
+def getPort(url):
+ """Return the port of a given URL"""
+ return urlparse(_getNormalizedURL(url), "http").port
+
+
+def getPath(url):
+ """Return the page request of a given URL"""
+ return urlparse(_getNormalizedURL(url), "http").path
+
+
+def getUser(url):
+ """Return the page request of a given URL"""
+ return urlparse(_getNormalizedURL(url), "http").username
+
+
+def getPassword(url):
+ """Return the page request of a given URL"""
+ return urlparse(_getNormalizedURL(url), "http").password
+
+
+# Get real pages
+
+def _URLConn(cb, url, body=None, timeout=7, header=None, follow_redir=True):
+ o = urlparse(_getNormalizedURL(url), "http")
+
+ import http.client
+
+ kwargs = {
+ 'host': o.hostname,
+ 'port': o.port,
+ 'timeout': timeout
+ }
+
+ if o.scheme == "http":
+ conn = http.client.HTTPConnection(**kwargs)
+ elif o.scheme == "https":
+ # For Python>3.4, restore the Python 3.3 behavior
+ import ssl
+ if hasattr(ssl, "create_default_context"):
+ kwargs["context"] = ssl.create_default_context()
+ kwargs["context"].check_hostname = False
+ kwargs["context"].verify_mode = ssl.CERT_NONE
+
+ conn = http.client.HTTPSConnection(**kwargs)
+ elif o.scheme is None or o.scheme == "":
+ conn = http.client.HTTPConnection(**kwargs)
+ else:
+ raise IMException("Invalid URL")
+
+ from nemubot import __version__
+ if header is None:
+ header = {"User-agent": "Nemubot v%s" % __version__}
+ elif "User-agent" not in header:
+ header["User-agent"] = "Nemubot v%s" % __version__
+
+ if body is not None and "Content-Type" not in header:
+ header["Content-Type"] = "application/x-www-form-urlencoded"
+
+ import socket
+ try:
+ if o.query != '':
+ conn.request("GET" if body is None else "POST",
+ o.path + "?" + o.query,
+ body,
+ header)
+ else:
+ conn.request("GET" if body is None else "POST",
+ o.path,
+ body,
+ header)
+ except socket.timeout as e:
+ raise IMException(e)
+ except OSError as e:
+ raise IMException(e.strerror)
+
+ try:
+ res = conn.getresponse()
+ if follow_redir and ((res.status == http.client.FOUND or
+ res.status == http.client.MOVED_PERMANENTLY) and
+ res.getheader("Location") != url):
+ return _URLConn(cb,
+ url=urljoin(url, res.getheader("Location")),
+ body=body,
+ timeout=timeout,
+ header=header,
+ follow_redir=follow_redir)
+ return cb(res)
+ except http.client.BadStatusLine:
+ raise IMException("Invalid HTTP response")
+ finally:
+ conn.close()
+
+
+def getURLHeaders(url, body=None, timeout=7, header=None, follow_redir=True):
+ """Return page headers corresponding to URL or None if any error occurs
+
+ Arguments:
+ url -- the URL to get
+ body -- Data to send as POST content
+ timeout -- maximum number of seconds to wait before returning an exception
+ """
+
+ def next(res):
+ return res.status, res.getheaders()
+ return _URLConn(next, url=url, body=body, timeout=timeout, header=header, follow_redir=follow_redir)
+
+
+def getURLContent(url, body=None, timeout=7, header=None, decode_error=False,
+ max_size=524288):
+ """Return page content corresponding to URL or None if any error occurs
+
+ Arguments:
+ url -- the URL to get
+ body -- Data to send as POST content
+ timeout -- maximum number of seconds to wait before returning an exception
+ decode_error -- raise exception on non-200 pages or ignore it
+ max_size -- maximal size allow for the content
+ """
+
+ def _nextURLContent(res):
+ size = int(res.getheader("Content-Length", 524288))
+ cntype = res.getheader("Content-Type")
+
+ if max_size >= 0 and (size > max_size or (cntype is not None and cntype[:4] != "text" and cntype[:4] != "appl")):
+ raise IMException("Content too large to be retrieved")
+
+ data = res.read(size)
+
+ # Decode content
+ charset = "utf-8"
+ if cntype is not None:
+ lcharset = res.getheader("Content-Type").split(";")
+ if len(lcharset) > 1:
+ for c in lcharset:
+ ch = c.split("=")
+ if ch[0].strip().lower() == "charset" and len(ch) > 1:
+ cha = ch[1].split(".")
+ if len(cha) > 1:
+ charset = cha[1]
+ else:
+ charset = cha[0]
+
+ import http.client
+
+ if res.status == http.client.OK or res.status == http.client.SEE_OTHER:
+ return data.decode(charset, errors='ignore').strip()
+ elif decode_error:
+ return data.decode(charset, errors='ignore').strip()
+ else:
+ raise IMException("A HTTP error occurs: %d - %s" %
+ (res.status, http.client.responses[res.status]))
+
+ return _URLConn(_nextURLContent, url=url, body=body, timeout=timeout, header=header)
+
+
+def getXML(*args, **kwargs):
+ """Get content page and return XML parsed content
+
+ Arguments: same as getURLContent
+ """
+
+ cnt = getURLContent(*args, **kwargs)
+ if cnt is None:
+ return None
+ else:
+ from xml.dom.minidom import parseString
+ return parseString(cnt)
+
+
+def getJSON(*args, remove_callback=False, **kwargs):
+ """Get content page and return JSON content
+
+ Arguments: same as getURLContent
+ """
+
+ cnt = getURLContent(*args, **kwargs)
+ if cnt is None:
+ return None
+ else:
+ import json
+ if remove_callback:
+ import re
+ cnt = re.sub(r"^[^(]+\((.*)\)$", r"\1", cnt)
+ return json.loads(cnt)
+
+
+# Other utils
+
+def striphtml(data):
+ """Remove HTML tags from text
+
+ Argument:
+ data -- the string to strip
+ """
+
+ if not isinstance(data, str) and not isinstance(data, bytes):
+ return data
+
+ try:
+ from html import unescape
+ except ImportError:
+ def _replace_charref(s):
+ s = s.group(1)
+
+ if s[0] == '#':
+ if s[1] in 'xX':
+ return chr(int(s[2:], 16))
+ else:
+ return chr(int(s[2:]))
+ else:
+ from html.entities import name2codepoint
+ return chr(name2codepoint[s])
+
+ # unescape exists from Python 3.4
+ def unescape(s):
+ if '&' not in s:
+ return s
+
+ import re
+
+ return re.sub('&([^;]+);', _replace_charref, s)
+
+
+ import re
+ return re.sub(r' +', ' ',
+ unescape(re.sub(r'<.*?>', '', data)).replace('\n', ' '))
diff --git a/nemubot/tools/xmlparser/__init__.py b/nemubot/tools/xmlparser/__init__.py
new file mode 100644
index 0000000..1bf60a8
--- /dev/null
+++ b/nemubot/tools/xmlparser/__init__.py
@@ -0,0 +1,174 @@
+# 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 .
+
+import xml.parsers.expat
+
+from nemubot.tools.xmlparser import node as module_state
+
+
+class ModuleStatesFile:
+
+ def __init__(self):
+ self.root = None
+ self.stack = list()
+
+ def startElement(self, name, attrs):
+ cur = module_state.ModuleState(name)
+
+ for name in attrs.keys():
+ cur.setAttribute(name, attrs[name])
+
+ self.stack.append(cur)
+
+ def characters(self, content):
+ self.stack[len(self.stack)-1].content += content
+
+ def endElement(self, name):
+ child = self.stack.pop()
+ size = len(self.stack)
+ if size > 0:
+ self.stack[size - 1].content = self.stack[size - 1].content.strip()
+ self.stack[size - 1].addChild(child)
+ else:
+ self.root = child
+
+
+class XMLParser:
+
+ def __init__(self, knodes):
+ self.knodes = knodes
+
+ self.stack = list()
+ self.child = 0
+
+
+ def parse_file(self, path):
+ p = xml.parsers.expat.ParserCreate()
+
+ p.StartElementHandler = self.startElement
+ p.CharacterDataHandler = self.characters
+ p.EndElementHandler = self.endElement
+
+ with open(path, "rb") as f:
+ p.ParseFile(f)
+
+ return self.root
+
+
+ def parse_string(self, s):
+ p = xml.parsers.expat.ParserCreate()
+
+ p.StartElementHandler = self.startElement
+ p.CharacterDataHandler = self.characters
+ p.EndElementHandler = self.endElement
+
+ p.Parse(s, 1)
+
+ return self.root
+
+
+ @property
+ def root(self):
+ if len(self.stack):
+ return self.stack[0][0]
+ else:
+ return None
+
+
+ @property
+ def current(self):
+ if len(self.stack):
+ return self.stack[-1][0]
+ else:
+ return None
+
+
+ def display_stack(self):
+ return " in ".join([str(type(s).__name__) for s,c in reversed(self.stack)])
+
+
+ def startElement(self, name, attrs):
+ if not self.current or not hasattr(self.current, "startElement") or not self.current.startElement(name, attrs):
+ if name not in self.knodes:
+ raise TypeError(name + " is not a known type to decode")
+ else:
+ self.stack.append((self.knodes[name](**attrs), self.child))
+ self.child = 0
+ else:
+ self.child += 1
+
+
+ def characters(self, content):
+ if self.current and hasattr(self.current, "characters"):
+ self.current.characters(content)
+
+
+ def endElement(self, name):
+ if hasattr(self.current, "endElement"):
+ self.current.endElement(None)
+
+ if self.child:
+ self.child -= 1
+
+ # Don't remove root
+ elif len(self.stack) > 1:
+ last, self.child = self.stack.pop()
+ if hasattr(self.current, "addChild"):
+ if self.current.addChild(name, last):
+ return
+ raise TypeError(name + " tag not expected in " + self.display_stack())
+
+ def saveDocument(self, f=None, header=True, short_empty_elements=False):
+ if f is None:
+ import io
+ f = io.StringIO()
+
+ import xml.sax.saxutils
+ gen = xml.sax.saxutils.XMLGenerator(f, "utf-8", short_empty_elements=short_empty_elements)
+ if header:
+ gen.startDocument()
+ self.root.saveElement(gen)
+ if header:
+ gen.endDocument()
+
+ return f
+
+
+def parse_file(filename):
+ p = xml.parsers.expat.ParserCreate()
+ mod = ModuleStatesFile()
+
+ p.StartElementHandler = mod.startElement
+ p.EndElementHandler = mod.endElement
+ p.CharacterDataHandler = mod.characters
+
+ with open(filename, "rb") as f:
+ p.ParseFile(f)
+
+ return mod.root
+
+
+def parse_string(string):
+ p = xml.parsers.expat.ParserCreate()
+ mod = ModuleStatesFile()
+
+ p.StartElementHandler = mod.startElement
+ p.EndElementHandler = mod.endElement
+ p.CharacterDataHandler = mod.characters
+
+ p.Parse(string, 1)
+
+ return mod.root
diff --git a/nemubot/tools/xmlparser/basic.py b/nemubot/tools/xmlparser/basic.py
new file mode 100644
index 0000000..dadff23
--- /dev/null
+++ b/nemubot/tools/xmlparser/basic.py
@@ -0,0 +1,153 @@
+# 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 .
+
+class ListNode:
+
+ """XML node representing a Python dictionnnary
+ """
+
+ def __init__(self, **kwargs):
+ self.items = list()
+
+
+ def addChild(self, name, child):
+ self.items.append(child)
+ return True
+
+
+ def __len__(self):
+ return len(self.items)
+
+ def __getitem__(self, item):
+ return self.items[item]
+
+ def __setitem__(self, item, v):
+ self.items[item] = v
+
+ def __contains__(self, item):
+ return item in self.items
+
+ def __repr__(self):
+ return self.items.__repr__()
+
+
+ def saveElement(self, store, tag="list"):
+ store.startElement(tag, {})
+ for i in self.items:
+ i.saveElement(store)
+ store.endElement(tag)
+
+
+class DictNode:
+
+ """XML node representing a Python dictionnnary
+ """
+
+ def __init__(self, **kwargs):
+ self.items = dict()
+ self._cur = None
+
+
+ def startElement(self, name, attrs):
+ if self._cur is None and "key" in attrs:
+ self._cur = (attrs["key"], "")
+ return True
+ return False
+
+
+ def characters(self, content):
+ if self._cur is not None:
+ key, cnt = self._cur
+ if isinstance(cnt, str):
+ cnt += content
+ self._cur = key, cnt
+
+
+ def endElement(self, name):
+ if name is not None or self._cur is None:
+ return
+
+ key, cnt = self._cur
+ if isinstance(cnt, list) and len(cnt) == 1:
+ self.items[key] = cnt[0]
+ else:
+ self.items[key] = cnt
+
+ self._cur = None
+ return True
+
+
+ def addChild(self, name, child):
+ if self._cur is None:
+ return False
+
+ key, cnt = self._cur
+ if not isinstance(cnt, list):
+ cnt = []
+ cnt.append(child)
+ self._cur = key, cnt
+ return True
+
+
+ def __getitem__(self, item):
+ return self.items[item]
+
+ def __setitem__(self, item, v):
+ self.items[item] = v
+
+ def __contains__(self, item):
+ return item in self.items
+
+ def __repr__(self):
+ return self.items.__repr__()
+
+
+ def saveElement(self, store, tag="dict"):
+ store.startElement(tag, {})
+ for k, v in self.items.items():
+ store.startElement("item", {"key": k})
+ if isinstance(v, str):
+ store.characters(v)
+ else:
+ if hasattr(v, "__iter__"):
+ for i in v:
+ i.saveElement(store)
+ else:
+ v.saveElement(store)
+ store.endElement("item")
+ store.endElement(tag)
+
+
+ def __contain__(self, i):
+ return i in self.items
+
+ def __getitem__(self, i):
+ return self.items[i]
+
+ def __setitem__(self, i, c):
+ self.items[i] = c
+
+ def __delitem__(self, k):
+ del self.items[k]
+
+ def __iter__(self):
+ return self.items.__iter__()
+
+ def keys(self):
+ return self.items.keys()
+
+ def items(self):
+ return self.items.items()
diff --git a/nemubot/tools/xmlparser/genericnode.py b/nemubot/tools/xmlparser/genericnode.py
new file mode 100644
index 0000000..425934c
--- /dev/null
+++ b/nemubot/tools/xmlparser/genericnode.py
@@ -0,0 +1,102 @@
+# 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 .
+
+class ParsingNode:
+
+ """Allow any kind of subtags, just keep parsed ones
+ """
+
+ def __init__(self, tag=None, **kwargs):
+ self.tag = tag
+ self.attrs = kwargs
+ self.content = ""
+ self.children = []
+
+
+ def characters(self, content):
+ self.content += content
+
+
+ def addChild(self, name, child):
+ self.children.append(child)
+ return True
+
+
+ def hasNode(self, nodename):
+ return self.getNode(nodename) is not None
+
+
+ def getNode(self, nodename):
+ for c in self.children:
+ if c is not None and c.tag == nodename:
+ return c
+ return None
+
+
+ def __getitem__(self, item):
+ return self.attrs[item]
+
+ def __contains__(self, item):
+ return item in self.attrs
+
+
+ def saveElement(self, store, tag=None):
+ store.startElement(tag if tag is not None else self.tag, self.attrs)
+ for child in self.children:
+ child.saveElement(store)
+ store.characters(self.content)
+ store.endElement(tag if tag is not None else self.tag)
+
+
+class GenericNode(ParsingNode):
+
+ """Consider all subtags as dictionnary
+ """
+
+ def __init__(self, tag, **kwargs):
+ super().__init__(tag, **kwargs)
+ self._cur = None
+ self._deep_cur = 0
+
+
+ def startElement(self, name, attrs):
+ if self._cur is None:
+ self._cur = GenericNode(name, **attrs)
+ self._deep_cur = 0
+ else:
+ self._deep_cur += 1
+ self._cur.startElement(name, attrs)
+ return True
+
+
+ def characters(self, content):
+ if self._cur is None:
+ super().characters(content)
+ else:
+ self._cur.characters(content)
+
+
+ def endElement(self, name):
+ if name is None:
+ return
+
+ if self._deep_cur:
+ self._deep_cur -= 1
+ self._cur.endElement(name)
+ else:
+ self.children.append(self._cur)
+ self._cur = None
+ return True
diff --git a/nemubot/tools/xmlparser/node.py b/nemubot/tools/xmlparser/node.py
new file mode 100644
index 0000000..7df255e
--- /dev/null
+++ b/nemubot/tools/xmlparser/node.py
@@ -0,0 +1,223 @@
+# 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 .
+
+import logging
+
+logger = logging.getLogger("nemubot.tools.xmlparser.node")
+
+
+class ModuleState:
+ """Tiny tree representation of an XML file"""
+
+ def __init__(self, name):
+ self.name = name
+ self.content = ""
+ self.attributes = dict()
+ self.childs = list()
+ self.index = dict()
+ self.index_fieldname = None
+ self.index_tagname = None
+
+ def getName(self):
+ """Get the name of the current node"""
+ return self.name
+
+ def display(self, level=0):
+ ret = ""
+ out = list()
+ for k in self.attributes:
+ out.append("%s : %s" % (k, self.attributes[k]))
+ ret += "%s%s { %s } = '%s'\n" % (' ' * level, self.name,
+ ' ; '.join(out), self.content)
+ for c in self.childs:
+ ret += c.display(level + 2)
+ return ret
+
+ def __str__(self):
+ return self.display()
+
+ def __repr__(self):
+ return self.display()
+
+ def __getitem__(self, i):
+ """Return the attribute asked"""
+ return self.getAttribute(i)
+
+ def __setitem__(self, i, c):
+ """Set the attribute"""
+ return self.setAttribute(i, c)
+
+ def getAttribute(self, name):
+ """Get the asked argument or return None if doesn't exist"""
+ if name in self.attributes:
+ return self.attributes[name]
+ else:
+ return None
+
+ def getDate(self, name=None):
+ """Get the asked argument and return it as a date"""
+ if name is None:
+ source = self.content
+ elif name in self.attributes.keys():
+ source = self.attributes[name]
+ else:
+ return None
+
+ from datetime import datetime
+ if isinstance(source, datetime):
+ return source
+ else:
+ from datetime import timezone
+ try:
+ return datetime.utcfromtimestamp(float(source)).replace(tzinfo=timezone.utc)
+ except ValueError:
+ while True:
+ try:
+ import calendar, time
+ return datetime.utcfromtimestamp(calendar.timegm(time.strptime(source[:19], "%Y-%m-%d %H:%M:%S"))).replace(tzinfo=timezone.utc)
+ except ImportError:
+ pass
+
+ def getInt(self, name=None):
+ """Get the asked argument and return it as an integer"""
+ if name is None:
+ source = self.content
+ elif name in self.attributes.keys():
+ source = self.attributes[name]
+ else:
+ return None
+
+ return int(float(source))
+
+ def getBool(self, name=None):
+ """Get the asked argument and return it as an integer"""
+ if name is None:
+ source = self.content
+ elif name in self.attributes.keys():
+ source = self.attributes[name]
+ else:
+ return False
+
+ return (isinstance(source, bool) and source) or source == "True"
+
+ def tmpIndex(self, fieldname="name", tagname=None):
+ index = dict()
+ for child in self.childs:
+ if ((tagname is None or tagname == child.name) and
+ child.hasAttribute(fieldname)):
+ index[child[fieldname]] = child
+ return index
+
+ def setIndex(self, fieldname="name", tagname=None):
+ """Defines an hash table to accelerate childs search.
+ You have just to define a common attribute"""
+ self.index = self.tmpIndex(fieldname, tagname)
+ self.index_fieldname = fieldname
+ self.index_tagname = tagname
+
+ def __contains__(self, i):
+ """Return true if i is found in the index"""
+ if self.index:
+ return i in self.index
+ else:
+ return self.hasAttribute(i)
+
+ def hasAttribute(self, name):
+ """DOM like method"""
+ return (name in self.attributes)
+
+ def setAttribute(self, name, value):
+ """DOM like method"""
+ from datetime import datetime
+ if (isinstance(value, datetime) or isinstance(value, str) or
+ isinstance(value, int) or isinstance(value, float)):
+ self.attributes[name] = value
+ else:
+ raise TypeError("attributes must be primary type "
+ "or datetime (here %s)" % type(value))
+
+ def getContent(self):
+ return self.content
+
+ def getChilds(self):
+ """Return a full list of direct child of this node"""
+ return self.childs
+
+ def getNode(self, tagname):
+ """Get a unique node (or the last one) with the given tagname"""
+ ret = None
+ for child in self.childs:
+ if tagname is None or tagname == child.name:
+ ret = child
+ return ret
+
+ def getFirstNode(self, tagname):
+ """Get a unique node (or the last one) with the given tagname"""
+ for child in self.childs:
+ if tagname is None or tagname == child.name:
+ return child
+ return None
+
+ def getNodes(self, tagname):
+ """Get all direct childs that have the given tagname"""
+ for child in self.childs:
+ if tagname is None or tagname == child.name:
+ yield child
+
+ def hasNode(self, tagname):
+ """Return True if at least one node with the given tagname exists"""
+ for child in self.childs:
+ if tagname is None or tagname == child.name:
+ return True
+ return False
+
+ def addChild(self, child):
+ """Add a child to this node"""
+ self.childs.append(child)
+ if self.index_fieldname is not None:
+ self.setIndex(self.index_fieldname, self.index_tagname)
+
+ def delChild(self, child):
+ """Remove the given child from this node"""
+ self.childs.remove(child)
+ if self.index_fieldname is not None:
+ self.setIndex(self.index_fieldname, self.index_tagname)
+
+ def saveElement(self, gen):
+ """Serialize this node as a XML node"""
+ from datetime import datetime
+ attribs = {}
+ for att in self.attributes.keys():
+ if att[0] != "_": # Don't save attribute starting by _
+ if isinstance(self.attributes[att], datetime):
+ import calendar
+ attribs[att] = str(calendar.timegm(
+ self.attributes[att].timetuple()))
+ else:
+ attribs[att] = str(self.attributes[att])
+ import xml.sax
+ attrs = xml.sax.xmlreader.AttributesImpl(attribs)
+
+ try:
+ gen.startElement(self.name, attrs)
+
+ for child in self.childs:
+ child.saveElement(gen)
+
+ gen.endElement(self.name)
+ except:
+ logger.exception("Error occured when saving the following "
+ "XML node: %s with %s", self.name, attrs)
diff --git a/nemubot/treatment.py b/nemubot/treatment.py
new file mode 100644
index 0000000..ed7cacb
--- /dev/null
+++ b/nemubot/treatment.py
@@ -0,0 +1,161 @@
+# 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 .
+
+import logging
+
+logger = logging.getLogger("nemubot.treatment")
+
+
+class MessageTreater:
+
+ """Treat a message"""
+
+ def __init__(self):
+ from nemubot.hooks.manager import HooksManager
+ self.hm = HooksManager()
+
+
+ def treat_msg(self, msg):
+ """Treat a given message
+
+ Arguments:
+ msg -- the message to treat
+ """
+
+ try:
+ handled = False
+
+ # Run pre-treatment: from Message to [ Message ]
+ msg_gen = self._pre_treat(msg)
+ m = next(msg_gen, None)
+
+ # Run in-treatment: from Message to [ Response ]
+ while m is not None:
+
+ hook_gen = self._in_hooks(m)
+ hook = next(hook_gen, None)
+ if hook is not None:
+ handled = True
+
+ for response in self._in_treat(m, hook, hook_gen):
+ # Run post-treatment: from Response to [ Response ]
+ yield from self._post_treat(response)
+
+ m = next(msg_gen, None)
+
+ if not handled:
+ for m in self._in_miss(msg):
+ yield from self._post_treat(m)
+ except BaseException as e:
+ logger.exception("Error occurred during the processing of the %s: "
+ "%s", type(msg).__name__, msg)
+
+ from nemubot.message import Text
+ yield from self._post_treat(Text("Sorry, an error occured (%s). Feel free to open a new issue at https://github.com/nemunaire/nemubot/issues/new" % type(e).__name__,
+ to=msg.to_response))
+
+
+
+ def _pre_treat(self, msg):
+ """Modify input Messages
+
+ Arguments:
+ msg -- message to treat
+ """
+
+ for h in self.hm.get_hooks("pre", type(msg).__name__):
+ if h.can_read(msg.to, msg.server) and h.match(msg):
+ for res in flatify(h.run(msg)):
+ if res is not None and res != msg:
+ yield from self._pre_treat(res)
+
+ elif res is None or res is False:
+ break
+ else:
+ yield msg
+
+
+ def _in_hooks(self, msg):
+ for h in self.hm.get_hooks("in", type(msg).__name__):
+ if h.can_read(msg.to, msg.server) and h.match(msg):
+ yield h
+
+
+ def _in_treat(self, msg, hook, hook_gen):
+ """Treats Messages and returns Responses
+
+ Arguments:
+ msg -- message to treat
+ """
+
+ if hasattr(msg, "frm_owner"):
+ msg.frm_owner = (not hasattr(msg.server, "owner") or msg.server.owner == msg.frm)
+
+ while hook is not None:
+ for res in flatify(hook.run(msg)):
+ if not hasattr(res, "server") or res.server is None:
+ res.server = msg.server
+ yield res
+
+ hook = next(hook_gen, None)
+
+
+ def _in_miss(self, msg):
+ from nemubot.message.command import Command as CommandMessage
+ from nemubot.message.directask import DirectAsk as DirectAskMessage
+
+ if isinstance(msg, CommandMessage):
+ from nemubot.hooks import Command as CommandHook
+ from nemubot.tools.human import guess
+ hooks = self.hm.get_reverse_hooks("in", type(msg).__name__)
+ suggest = [s for s in guess(msg.cmd, [h.name for h in hooks if isinstance(h, CommandHook) and h.name is not None])]
+ if len(suggest) >= 1:
+ yield DirectAskMessage(msg.frm,
+ "Unknown command %s. Would you mean: %s?" % (msg.cmd, ", ".join(suggest)),
+ to=msg.to_response)
+
+ elif isinstance(msg, DirectAskMessage):
+ yield DirectAskMessage(msg.frm,
+ "Sorry, I'm just a bot and your sentence is too complex for me :( But feel free to teach me some tricks at https://github.com/nemunaire/nemubot/!",
+ to=msg.to_response)
+
+
+ def _post_treat(self, msg):
+ """Modify output Messages
+
+ Arguments:
+ msg -- response to treat
+ """
+
+ for h in self.hm.get_hooks("post", type(msg).__name__):
+ if h.can_write(msg.to, msg.server) and h.match(msg):
+ for res in flatify(h.run(msg)):
+ if res is not None and res != msg:
+ yield from self._post_treat(res)
+
+ elif res is None or res is False:
+ break
+
+ else:
+ yield msg
+
+
+def flatify(g):
+ if hasattr(g, "__iter__"):
+ for i in g:
+ yield from flatify(i)
+ else:
+ yield g
diff --git a/nemuspeak.py b/nemuspeak.py
deleted file mode 100755
index 9501e17..0000000
--- a/nemuspeak.py
+++ /dev/null
@@ -1,188 +0,0 @@
-#!/usr/bin/python3
-# coding=utf-8
-
-import sys
-import socket
-import signal
-import os
-import re
-import subprocess
-import shlex
-import traceback
-from datetime import datetime
-from datetime import timedelta
-import _thread
-
-if len(sys.argv) <= 1:
- print ("This script takes exactly 1 arg: a XML config file")
- sys.exit(1)
-
-def onSignal(signum, frame):
- print ("\nSIGINT receive, saving states and close")
- sys.exit (0)
-signal.signal(signal.SIGINT, onSignal)
-
-if len(sys.argv) == 3:
- basedir = sys.argv[2]
-else:
- basedir = "./"
-
-import xmlparser as msf
-import message
-import IRCServer
-
-SMILEY = list()
-CORRECTIONS = list()
-g_queue = list()
-talkEC = 0
-stopSpk = 0
-lastmsg = None
-
-def speak(endstate):
- global lastmsg, g_queue, talkEC, stopSpk
- talkEC = 1
- stopSpk = 0
-
- if lastmsg is None:
- lastmsg = message.Message(b":Quelqun!someone@p0m.fr PRIVMSG channel nothing", datetime.now())
-
- while not stopSpk and len(g_queue) > 0:
- srv, msg = g_queue.pop(0)
- lang = "fr"
- sentence = ""
- force = 0
-
- #Skip identic body
- if msg.content == lastmsg.content:
- continue
-
- if force or msg.time - lastmsg.time > timedelta(0, 500):
- sentence += "A {0} heure {1} : ".format(msg.time.hour, msg.time.minute)
- force = 1
-
- if force or msg.channel != lastmsg.channel:
- if msg.channel == srv.owner:
- sentence += "En message priver. " #Just to avoid é :p
- else:
- sentence += "Sur " + msg.channel + ". "
- force = 1
-
- action = 0
- if msg.content.find("ACTION ") == 1:
- sentence += msg.nick + " "
- msg.content = msg.content.replace("ACTION ", "")
- action = 1
- for (txt, mood) in SMILEY:
- if msg.content.find(txt) >= 0:
- sentence += msg.nick + (" %s : "%mood)
- msg.content = msg.content.replace(txt, "")
- action = 1
- break
-
- for (bad, good) in CORRECTIONS:
- if msg.content.find(bad) >= 0:
- msg.content = (" " + msg.content + " ").replace(bad, good)
-
- if action == 0 and (force or msg.sender != lastmsg.sender):
- sentence += msg.nick + " dit : "
-
- if re.match(".*(https?://)?(www\\.)?ycc.fr/[a-z0-9A-Z]+.*", msg.content) is not None:
- msg.content = re.sub("(https?://)?(www\\.)?ycc.fr/[a-z0-9A-Z]+", " U.R.L Y.C.C ", msg.content)
-
- if re.match(".*https?://.*", msg.content) is not None:
- msg.content = re.sub(r'https?://[^ ]+', " U.R.L ", msg.content)
-
- if re.match("^ *[^a-zA-Z0-9 ][a-zA-Z]{2}[^a-zA-Z0-9 ]", msg.content) is not None:
- if sentence != "":
- intro = subprocess.call(["espeak", "-v", "fr", "--", sentence])
- #intro.wait()
-
- lang = msg.content[1:3].lower()
- sentence = msg.content[4:]
- else:
- sentence += msg.content
-
- spk = subprocess.call(["espeak", "-v", lang, "--", sentence])
- #spk.wait()
-
- lastmsg = msg
-
- if not stopSpk:
- talkEC = endstate
- else:
- talkEC = 1
-
-
-class Server(IRCServer.IRCServer):
- def treat_msg(self, line, private = False):
- global stopSpk, talkEC, g_queue
- try:
- msg = message.Message (line, datetime.now(), private)
- if msg.cmd == 'PING':
- msg.treat (self.mods)
- elif msg.cmd == 'PRIVMSG' and self.accepted_channel(msg.channel):
- if msg.nick != self.owner:
- g_queue.append((self, msg))
- if talkEC == 0:
- _thread.start_new_thread(speak, (0,))
- elif msg.content[0] == "`" and len(msg.content) > 1:
- msg.cmds = msg.cmds[1:]
- if msg.cmds[0] == "speak":
- _thread.start_new_thread(speak, (0,))
- elif msg.cmds[0] == "reset":
- while len(g_queue) > 0:
- g_queue.pop()
- elif msg.cmds[0] == "save":
- if talkEC == 0:
- talkEC = 1
- stopSpk = 1
- elif msg.cmds[0] == "add":
- self.channels.append(msg.cmds[1])
- print (cmd[1] + " added to listened channels")
- elif msg.cmds[0] == "del":
- if self.channels.count(msg.cmds[1]) > 0:
- self.channels.remove(msg.cmds[1])
- print (msg.cmds[1] + " removed from listened channels")
- else:
- print (cmd[1] + " not in listened channels")
- except:
- print ("\033[1;31mERROR:\033[0m occurred during the processing of the message: %s" % line)
- exc_type, exc_value, exc_traceback = sys.exc_info()
- traceback.print_exception(exc_type, exc_value, exc_traceback)
-
-
-config = msf.parse_file(sys.argv[1])
-
-for smiley in config.getNodes("smiley"):
- if smiley.hasAttribute("txt") and smiley.hasAttribute("mood"):
- SMILEY.append((smiley.getAttribute("txt"), smiley.getAttribute("mood")))
-print ("%d smileys loaded"%len(SMILEY))
-
-for correct in config.getNodes("correction"):
- if correct.hasAttribute("bad") and correct.hasAttribute("good"):
- CORRECTIONS.append((" " + (correct.getAttribute("bad") + " "), (" " + correct.getAttribute("good") + " ")))
-print ("%d corrections loaded"%len(CORRECTIONS))
-
-for serveur in config.getNodes("server"):
- srv = Server(serveur, config["nick"], config["owner"], config["realname"])
- srv.launch(None)
-
-def sighup_h(signum, frame):
- global talkEC, stopSpk
- sys.stdout.write ("Signal reçu ... ")
- if os.path.exists("/tmp/isPresent"):
- _thread.start_new_thread(speak, (0,))
- print ("Morning!")
- else:
- print ("Sleeping!")
- if talkEC == 0:
- talkEC = 1
- stopSpk = 1
-signal.signal(signal.SIGHUP, sighup_h)
-
-print ("Nemuspeak ready, waiting for new messages...")
-prompt=""
-while prompt != "quit":
- prompt=sys.stdin.readlines ()
-
-sys.exit(0)
diff --git a/networkbot.py b/networkbot.py
deleted file mode 100644
index 756ab3c..0000000
--- a/networkbot.py
+++ /dev/null
@@ -1,240 +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 .
-
-import json
-import random
-import shlex
-import urllib.parse
-import zlib
-
-from DCC import DCC
-import hooks
-from response import Response
-
-class NetworkBot:
- def __init__(self, context, srv, dest, dcc=None):
- # General informations
- self.context = context
- self.srv = srv
- self.dest = dest
-
- self.dcc = dcc # DCC connection to the other bot
- if self.dcc is not None:
- self.dcc.closing_event = self.closing_event
-
- self.hooks = list()
- self.REGISTERED_HOOKS = list()
-
- # Tags monitor
- self.my_tag = random.randint(0,255)
- self.inc_tag = 0
- self.tags = dict()
-
- @property
- def id(self):
- return self.dcc.id
- @property
- def sender(self):
- if self.dcc is not None:
- return self.dcc.sender
- return None
- @property
- def nick(self):
- if self.dcc is not None:
- return self.dcc.nick
- return None
- @property
- def realname(self):
- if self.dcc is not None:
- return self.dcc.realname
- return None
- @property
- def owner(self):
- return self.srv.owner
-
- def isDCC(self, someone):
- """Abstract implementation"""
- return True
-
- def accepted_channel(self, chan, sender=None):
- return True
-
- def send_cmd(self, cmd, data=None):
- """Create a tag and send the command"""
- # First, define a tag
- self.inc_tag = (self.inc_tag + 1) % 256
- while self.inc_tag in self.tags:
- self.inc_tag = (self.inc_tag + 1) % 256
- tag = ("%c%c" % (self.my_tag, self.inc_tag)).encode()
-
- self.tags[tag] = (cmd, data)
-
- # Send the command with the tag
- self.send_response_final(tag, cmd)
-
- def send_response(self, res, tag):
- self.send_response_final(tag, [res.sender, res.channel, res.nick, res.nomore, res.title, res.more, res.count, json.dumps(res.messages)])
-
- def msg_treated(self, tag):
- self.send_ack(tag)
-
- def send_response_final(self, tag, msg):
- """Send a response with a tag"""
- if isinstance(msg, list):
- cnt = b''
- for i in msg:
- if i is None:
- cnt += b' ""'
- elif isinstance(i, int):
- cnt += (' %d' % i).encode()
- elif isinstance(i, float):
- cnt += (' %f' % i).encode()
- else:
- cnt += b' "' + urllib.parse.quote(i).encode() + b'"'
- if False and len(cnt) > 10:
- cnt = b' Z ' + zlib.compress(cnt)
- print (cnt)
- self.dcc.send_dcc_raw(tag + cnt)
- else:
- for line in msg.split("\n"):
- self.dcc.send_dcc_raw(tag + b' ' + line.encode())
-
- def send_ack(self, tag):
- """Acknowledge a command"""
- if tag in self.tags:
- del self.tags[tag]
- self.send_response_final(tag, "ACK")
-
- def connect(self):
- """Making the connexion with dest through srv"""
- if self.dcc is None or not self.dcc.connected:
- self.dcc = DCC(self.srv, self.dest)
- self.dcc.closing_event = self.closing_event
- self.dcc.treatement = self.hello
- self.dcc.send_dcc("NEMUBOT###")
- else:
- self.send_cmd("FETCH")
-
- def disconnect(self, reason=""):
- """Close the connection and remove the bot from network list"""
- del self.context.network[self.dcc.id]
- self.dcc.send_dcc("DISCONNECT :%s" % reason)
- self.dcc.disconnect()
-
- def hello(self, line):
- if line == b'NEMUBOT###':
- self.dcc.treatement = self.treat_msg
- self.send_cmd("MYTAG %c" % self.my_tag)
- self.send_cmd("FETCH")
- elif line != b'Hello ' + self.srv.nick.encode() + b'!':
- self.disconnect("Sorry, I think you were a bot")
-
- def treat_msg(self, line, cmd=None):
- words = line.split(b' ')
-
- # Ignore invalid commands
- if len(words) >= 2:
- tag = words[0]
-
- # Is it a response?
- if tag in self.tags:
- # Is it compressed content?
- if words[1] == b'Z':
- #print (line)
- line = zlib.decompress(line[len(tag) + 3:])
- self.response(line, tag, [urllib.parse.unquote(arg) for arg in shlex.split(line[len(tag) + 1:].decode())], self.tags[tag])
- else:
- cmd = words[1]
- if len(words) > 2:
- args = shlex.split(line[len(tag) + len(cmd) + 2:].decode())
- args = [urllib.parse.unquote(arg) for arg in args]
- else:
- args = list()
- #print ("request:", line)
- self.request(tag, cmd, args)
-
- def closing_event(self):
- for lvl in self.hooks:
- lvl.clear()
-
- def response(self, line, tag, args, t):
- (cmds, data) = t
- #print ("response for", cmds, ":", args)
-
- if isinstance(cmds, list):
- cmd = cmds[0]
- else:
- cmd = cmds
- cmds = list(cmd)
-
- if args[0] == 'ACK': # Acknowledge a command
- del self.tags[tag]
-
- elif cmd == "FETCH" and len(args) >= 5:
- level = int(args[1])
- while len(self.hooks) <= level:
- self.hooks.append(hooks.MessagesHook(self.context, self))
-
- if args[2] == "": args[2] = None
- if args[3] == "": args[3] = None
- if args[4] == "": args[4] = list()
- else: args[4] = args[4].split(',')
-
- self.hooks[level].add_hook(args[0], hooks.Hook(self.exec_hook, args[2], None, args[3], args[4]), self)
-
- elif cmd == "HOOK" and len(args) >= 8:
- # Rebuild the response
- if args[1] == '': args[1] = None
- if args[2] == '': args[2] = None
- if args[3] == '': args[3] = None
- if args[4] == '': args[4] = None
- if args[5] == '': args[5] = None
- if args[6] == '': args[6] = None
- res = Response(args[0], channel=args[1], nick=args[2], nomore=args[3], title=args[4], more=args[5], count=args[6])
- for msg in json.loads(args[7]):
- res.append_message(msg)
- if len(res.messages) <= 1:
- res.alone = True
- self.srv.send_response(res, None)
-
-
- def request(self, tag, cmd, args):
- # Parse
- if cmd == b'MYTAG' and len(args) > 0: # Inform about choosen tag
- while args[0] == self.my_tag:
- self.my_tag = random.randint(0,255)
- self.send_ack(tag)
-
- elif cmd == b'FETCH': # Get known commands
- for name in ["cmd_hook", "ask_hook", "msg_hook"]:
- elts = self.context.create_cache(name)
- for elt in elts:
- (hooks, lvl, store, bot) = elts[elt]
- for h in hooks:
- self.send_response_final(tag, [name, lvl, elt, h.regexp, ','.join(h.channels)])
- self.send_ack(tag)
-
- elif (cmd == b'HOOK' or cmd == b'"HOOK"') and len(args) > 0: # Action requested
- self.context.receive_message(self, args[0].encode(), True, tag)
-
- elif (cmd == b'NOMORE' or cmd == b'"NOMORE"') and len(args) > 0: # Reset !more feature
- if args[0] in self.srv.moremessages:
- del self.srv.moremessages[args[0]]
-
- def exec_hook(self, msg):
- self.send_cmd(["HOOK", msg.raw])
diff --git a/prompt/__init__.py b/prompt/__init__.py
deleted file mode 100644
index 62c8dc3..0000000
--- a/prompt/__init__.py
+++ /dev/null
@@ -1,105 +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 .
-
-import imp
-import os
-import shlex
-import sys
-import traceback
-
-from . import builtins
-
-class Prompt:
- def __init__(self, hc=dict(), hl=dict()):
- self.selectedServer = None
-
- self.HOOKS_CAPS = hc
- self.HOOKS_LIST = hl
-
- def add_cap_hook(self, name, call, data=None):
- self.HOOKS_CAPS[name] = (lambda d, t, c, p: call(d, t, c, p), data)
-
-
- def lex_cmd(self, line):
- """Return an array of tokens"""
- ret = list()
- try:
- cmds = shlex.split(line)
- bgn = 0
- for i in range(0, len(cmds)):
- if cmds[i] == ';':
- if i != bgn:
- cmds[bgn] = cmds[bgn].lower()
- ret.append(cmds[bgn:i])
- bgn = i + 1
-
- if bgn != len(cmds):
- cmds[bgn] = cmds[bgn].lower()
- ret.append(cmds[bgn:len(cmds)])
-
- return ret
- except:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- sys.stderr.write (traceback.format_exception_only(
- exc_type, exc_value)[0])
- return ret
-
- def exec_cmd(self, toks, context):
- """Execute the command"""
- if toks[0] in builtins.CAPS:
- return builtins.CAPS[toks[0]](toks, context, self)
- elif toks[0] in self.HOOKS_CAPS:
- (f,d) = self.HOOKS_CAPS[toks[0]]
- return f(d, toks, context, self)
- else:
- print ("Unknown command: `%s'" % toks[0])
- return ""
-
- def getPS1(self):
- """Get the PS1 associated to the selected server"""
- if self.selectedServer is None:
- return "nemubot"
- else:
- return self.selectedServer.id
-
- def run(self, context):
- """Launch the prompt"""
- ret = ""
- while ret != "quit" and ret != "reset" and ret != "refresh":
- sys.stdout.write("\033[0;33m%s§\033[0m " % self.getPS1())
- sys.stdout.flush()
-
- try:
- line = sys.stdin.readline()
- if len(line) <= 0:
- line = "quit"
- print ("quit")
- cmds = self.lex_cmd(line.strip())
- for toks in cmds:
- try:
- ret = self.exec_cmd(toks, context)
- except:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- traceback.print_exception(exc_type, exc_value, exc_traceback)
- except KeyboardInterrupt:
- print ("")
- return ret != "quit"
-
-
-def hotswap(prompt):
- return Prompt(prompt.HOOKS_CAPS, prompt.HOOKS_LIST)
diff --git a/prompt/builtins.py b/prompt/builtins.py
deleted file mode 100644
index 512549d..0000000
--- a/prompt/builtins.py
+++ /dev/null
@@ -1,157 +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 .
-
-import os
-import xmlparser
-
-def end(toks, context, prompt):
- """Quit the prompt for reload or exit"""
- if toks[0] == "refresh":
- return "refresh"
- elif toks[0] == "reset":
- return "reset"
- else:
- context.quit()
- return "quit"
-
-
-def liste(toks, context, prompt):
- """Show some lists"""
- if len(toks) > 1:
- for l in toks[1:]:
- l = l.lower()
- if l == "server" or l == "servers":
- for srv in context.servers.keys():
- print (" - %s ;" % srv)
- else:
- print (" > No server loaded")
- elif l == "mod" or l == "mods" or l == "module" or l == "modules":
- for mod in context.modules.keys():
- print (" - %s ;" % mod)
- else:
- print (" > No module loaded")
- elif l in prompt.HOOKS_LIST:
- (f,d) = prompt.HOOKS_LIST[l]
- f(d, context, prompt)
- else:
- print (" Unknown list `%s'" % l)
- else:
- print (" Please give a list to show: servers, ...")
-
-
-def load_file(filename, context):
- if os.path.isfile(filename):
- config = xmlparser.parse_file(filename)
-
- # This is a true nemubot configuration file, load it!
- if (config.getName() == "nemubotconfig"
- or config.getName() == "config"):
- # Preset each server in this file
- for server in config.getNodes("server"):
- if context.addServer(server, config["nick"],
- config["owner"], config["realname"]):
- print (" Server `%s:%s' successfully added."
- % (server["server"], server["port"]))
- else:
- print (" Server `%s:%s' already added, skiped."
- % (server["server"], server["port"]))
-
- # Load files asked by the configuration file
- for load in config.getNodes("load"):
- load_file(load["path"], context)
-
- # This is a nemubot module configuration file, load the module
- elif config.getName() == "nemubotmodule":
- __import__(config["name"])
-
- # Other formats
- else:
- print (" Can't load `%s'; this is not a valid nemubot "
- "configuration file." % filename)
-
- # Unexisting file, assume a name was passed, import the module!
- else:
- __import__(filename)
-
-
-def load(toks, context, prompt):
- """Load an XML configuration file"""
- if len(toks) > 1:
- for filename in toks[1:]:
- load_file(filename, context)
- else:
- print ("Not enough arguments. `load' takes a filename.")
- return
-
-
-def select(toks, context, prompt):
- """Select the current server"""
- if (len(toks) == 2 and toks[1] != "None"
- and toks[1] != "nemubot" and toks[1] != "none"):
- if toks[1] in context.servers:
- prompt.selectedServer = context.servers[toks[1]]
- else:
- print ("select: server `%s' not found." % toks[1])
- else:
- prompt.selectedServer = None
- return
-
-
-def unload(toks, context, prompt):
- """Unload a module"""
- if len(toks) == 2 and toks[1] == "all":
- for name in context.modules.keys():
- context.unload_module(name)
- elif len(toks) > 1:
- for name in toks[1:]:
- if context.unload_module(name):
- print (" Module `%s' successfully unloaded." % name)
- else:
- print (" No module `%s' loaded, can't unload!" % name)
- else:
- print ("Not enough arguments. `unload' takes a module name.")
-
-
-def debug(toks, context, prompt):
- """Enable/Disable debug mode on a module"""
- if len(toks) > 1:
- for name in toks[1:]:
- if name in context.modules:
- context.modules[name].DEBUG = not context.modules[name].DEBUG
- if context.modules[name].DEBUG:
- print (" Module `%s' now in DEBUG mode." % name)
- else:
- print (" Debug for module module `%s' disabled." % name)
- else:
- print (" No module `%s' loaded, can't debug!" % name)
- else:
- print ("Not enough arguments. `debug' takes a module name.")
-
-
-#Register build-ins
-CAPS = {
- 'quit': end, #Disconnect all server and quit
- 'exit': end, #Alias for quit
- 'reset': end, #Reload the prompt
- 'refresh': end, #Reload the prompt but save modules
- 'load': load, #Load a servers or module configuration file
- 'unload': unload, #Unload a module and remove it from the list
- 'select': select, #Select a server
- 'list': liste, #Show lists
- 'debug': debug, #Pass a module in debug mode
-}
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e037895
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+irc
+matrix-nio
diff --git a/response.py b/response.py
deleted file mode 100644
index 9fda7f8..0000000
--- a/response.py
+++ /dev/null
@@ -1,176 +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 .
-
-import traceback
-import sys
-
-class Response:
- def __init__(self, sender, message=None, channel=None, nick=None, server=None,
- nomore="No more message", title=None, more="(suite) ", count=None,
- ctcp=False, shown_first_count=-1):
- self.nomore = nomore
- self.more = more
- self.rawtitle = title
- self.server = server
- self.messages = list()
- self.alone = True
- self.ctcp = ctcp
- if message is not None:
- self.append_message(message, shown_first_count=shown_first_count)
- self.elt = 0 # Next element to display
-
- self.channel = channel
- self.nick = nick
- self.set_sender(sender)
- self.count = count
-
- @property
- def content(self):
- #FIXME: error when messages in self.messages are list!
- try:
- if self.title is not None:
- return self.title + ", ".join(self.messages)
- else:
- return ", ".join(self.messages)
- except:
- return ""
-
- def set_sender(self, sender):
- if sender is None or sender.find("!") < 0:
- if sender is not None:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- traceback.print_exception(exc_type, "\033[1;35mWarning:\033[0m bad sender provided in Response, it will be ignored.", exc_traceback)
- self.sender = None
- else:
- self.sender = sender
-
- def append_message(self, message, title=None, shown_first_count=-1):
- if message is not None and len(message) > 0:
- if shown_first_count >= 0:
- self.messages.append(message[:shown_first_count])
- message = message[shown_first_count:]
- self.messages.append(message)
- self.alone = self.alone and len(self.messages) <= 1
- if isinstance(self.rawtitle, list):
- self.rawtitle.append(title)
- elif title is not None:
- rawtitle = self.rawtitle
- self.rawtitle = list()
- for osef in self.messages:
- self.rawtitle.append(rawtitle)
- self.rawtitle.pop()
- self.rawtitle.append(title)
-
- def append_content(self, message):
- if message is not None and len(message) > 0:
- if self.messages is None or len(self.messages) == 0:
- self.messages = list(message)
- self.alone = True
- else:
- self.messages[len(self.messages)-1] += message
- self.alone = self.alone and len(self.messages) <= 1
-
- @property
- def empty(self):
- return len(self.messages) <= 0
-
- @property
- def title(self):
- if isinstance(self.rawtitle, list):
- return self.rawtitle[0]
- else:
- return self.rawtitle
-
- def pop(self):
- self.messages.pop(0)
- if isinstance(self.rawtitle, list):
- self.rawtitle.pop(0)
- if len(self.rawtitle) <= 0:
- self.rawtitle = None
-
- def get_message(self):
- if self.alone and len(self.messages) > 1:
- self.alone = False
-
- if self.empty:
- return self.nomore
-
- msg = ""
- if self.channel is not None and self.nick is not None:
- msg += self.nick + ": "
-
- if self.title is not None:
- if self.elt > 0:
- msg += self.title + " " + self.more + ": "
- else:
- msg += self.title + ": "
-
- if self.elt > 0:
- msg += "[…] "
-
- elts = self.messages[0][self.elt:]
- if isinstance(elts, list):
- for e in elts:
- if len(msg) + len(e) > 430:
- msg += "[…]"
- self.alone = False
- return msg
- else:
- msg += e + ", "
- self.elt += 1
- self.pop()
- self.elt = 0
- return msg[:len(msg)-2]
-
- else:
- if len(elts) <= 432:
- self.pop()
- self.elt = 0
- if self.count is not None:
- return msg + elts + (self.count % len(self.messages))
- else:
- return msg + elts
-
- else:
- words = elts.split(' ')
-
- if len(words[0]) > 432 - len(msg):
- self.elt += 432 - len(msg)
- return msg + elts[:self.elt] + "[…]"
-
- for w in words:
- if len(msg) + len(w) > 431:
- msg += "[…]"
- self.alone = False
- return msg
- else:
- msg += w + " "
- self.elt += len(w) + 1
- self.pop()
- self.elt = 0
- return msg
-
-import hooks
-class Hook:
- def __init__(self, TYPE, call, name=None, data=None, regexp=None,
- channels=list(), server=None, end=None, call_end=None,
- SRC=None):
- self.hook = hooks.Hook(call, name, data, regexp, channels,
- server, end, call_end)
- self.type = TYPE
- self.src = SRC
diff --git a/server.py b/server.py
deleted file mode 100644
index e16bd57..0000000
--- a/server.py
+++ /dev/null
@@ -1,169 +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 .
-
-import socket
-import threading
-
-class Server(threading.Thread):
- def __init__(self, socket = None):
- self.stop = False
- self.stopping = threading.Event()
- self.s = socket
- self.connected = self.s is not None
- self.closing_event = None
-
- self.moremessages = dict()
-
- threading.Thread.__init__(self)
-
- def isDCC(self, to=None):
- return to is not None and to in self.dcc_clients
-
- @property
- def ip(self):
- """Convert common IP representation to little-endian integer representation"""
- sum = 0
- if self.node.hasAttribute("ip"):
- ip = self.node["ip"]
- else:
- #TODO: find the external IP
- ip = "0.0.0.0"
- for b in ip.split("."):
- sum = 256 * sum + int(b)
- return sum
-
- def toIP(self, input):
- """Convert little-endian int to IPv4 adress"""
- ip = ""
- for i in range(0,4):
- mod = input % 256
- ip = "%d.%s" % (mod, ip)
- input = (input - mod) / 256
- return ip[:len(ip) - 1]
-
- @property
- def id(self):
- """Gives the server identifiant"""
- raise NotImplemented()
-
- def accepted_channel(self, msg, sender=None):
- return True
-
- def msg_treated(self, origin):
- """Action done on server when a message was treated"""
- raise NotImplemented()
-
- def send_response(self, res, origin):
- """Analyse a Response and send it"""
- # TODO: how to send a CTCP message to a different person
- if res.ctcp:
- self.send_ctcp(res.sender, res.get_message())
-
- elif res.channel is not None and res.channel != self.nick:
- self.send_msg(res.channel, res.get_message())
-
- if not res.alone:
- if hasattr(self, "send_bot"):
- self.send_bot("NOMORE %s" % res.channel)
- self.moremessages[res.channel] = res
- elif res.sender is not None:
- self.send_msg_usr(res.sender, res.get_message())
-
- if not res.alone:
- self.moremessages[res.sender] = res
-
- def send_ctcp(self, to, msg, cmd="NOTICE", endl="\r\n"):
- """Send a message as CTCP response"""
- if msg is not None and to is not None:
- for line in msg.split("\n"):
- if line != "":
- self.send_msg_final(to.split("!")[0], "\x01" + line + "\x01", cmd, endl)
-
- def send_dcc(self, msg, to):
- """Send a message through DCC connection"""
- raise NotImplemented()
-
- def send_msg_final(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
- """Send a message without checks or format"""
- raise NotImplemented()
-
- def send_msg_usr(self, user, msg):
- """Send a message to a user instead of a channel"""
- raise NotImplemented()
-
- def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
- """Send a message to a channel"""
- if msg is not None:
- for line in msg.split("\n"):
- if line != "":
- self.send_msg_final(channel, line, cmd, endl)
-
- def send_msg_verified(self, sender, channel, msg, cmd="PRIVMSG", endl="\r\n"):
- """A more secure way to send messages"""
- raise NotImplemented()
-
- def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"):
- """Send a message to all channels on this server"""
- raise NotImplemented()
-
- def disconnect(self):
- """Close the socket with the server"""
- if self.connected:
- self.stop = True
- try:
- self.s.shutdown(socket.SHUT_RDWR)
- except socket.error:
- pass
-
- self.stopping.wait()
- return True
- else:
- return False
-
- def kill(self):
- """Just stop the main loop, don't close the socket directly"""
- if self.connected:
- self.stop = True
- self.connected = False
- #Send a message in order to close the socket
- try:
- self.s.send(("Bye!\r\n" % self.nick).encode ())
- except:
- pass
- self.stopping.wait()
- return True
- else:
- return False
-
- def launch(self, receive_action, verb=True):
- """Connect to the server if it is no yet connected"""
- self._receive_action = receive_action
- if not self.connected:
- self.stop = False
- try:
- self.start()
- except RuntimeError:
- pass
- elif verb:
- print (" Already connected.")
-
- def treat_msg(self, line, private=False):
- self._receive_action(self, line, private)
-
- def run(self):
- raise NotImplemented()
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..7b5bdcd
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+
+import os
+import re
+from glob import glob
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+with open(os.path.join(os.path.dirname(__file__),
+ 'nemubot',
+ '__init__.py')) as f:
+ version = re.search("__version__ = '([^']+)'", f.read()).group(1)
+
+with open('requirements.txt', 'r') as f:
+ requires = [x.strip() for x in f if x.strip()]
+
+#with open('test-requirements.txt', 'r') as f:
+# test_requires = [x.strip() for x in f if x.strip()]
+
+dirs = os.listdir("./modules/")
+data_files = []
+for i in dirs:
+ data_files.append(("nemubot/modules", glob('./modules/' + i + '/*')))
+
+setup(
+ name = "nemubot",
+ version = version,
+ description = "An extremely modulable IRC bot, built around XML configuration files!",
+ long_description = open('README.md').read(),
+
+ author = 'nemunaire',
+ author_email = 'nemunaire@nemunai.re',
+
+ url = 'https://github.com/nemunaire/nemubot',
+ license = 'AGPLv3',
+
+ classifiers = [
+ 'Development Status :: 2 - Pre-Alpha',
+
+ 'Environment :: Console',
+
+ 'Topic :: Communications :: Chat :: Internet Relay Chat',
+ 'Intended Audience :: Information Technology',
+
+ 'License :: OSI Approved :: GNU Affero General Public License v3',
+
+ 'Operating System :: POSIX',
+
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ ],
+
+ keywords = 'bot irc',
+
+ provides = ['nemubot'],
+
+ install_requires = requires,
+
+ packages=[
+ 'nemubot',
+ 'nemubot.config',
+ 'nemubot.datastore',
+ 'nemubot.event',
+ 'nemubot.exception',
+ 'nemubot.hooks',
+ 'nemubot.hooks.keywords',
+ 'nemubot.message',
+ 'nemubot.message.printer',
+ 'nemubot.module',
+ 'nemubot.server',
+ 'nemubot.tools',
+ 'nemubot.tools.xmlparser',
+ ],
+
+ scripts=[
+ 'bin/nemubot',
+# 'bin/module_tester',
+ ],
+
+# data_files=data_files,
+)
diff --git a/speak_sample.xml b/speak_sample.xml
index c1c6f61..ee403ac 100644
--- a/speak_sample.xml
+++ b/speak_sample.xml
@@ -1,27 +1,35 @@
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/__init__.py b/tools/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tools/web.py b/tools/web.py
deleted file mode 100644
index b0bf2e3..0000000
--- a/tools/web.py
+++ /dev/null
@@ -1,119 +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 .
-
-import http.client
-import json
-import re
-import socket
-from urllib.parse import quote
-from urllib.parse import urlparse
-from urllib.request import urlopen
-
-import xmlparser
-
-def isURL(url):
- """Return True if the URL can be parsed"""
- o = urlparse(url)
- return o.scheme == "" and o.netloc == "" and o.path == ""
-
-def getScheme(url):
- """Return the protocol of a given URL"""
- o = urlparse(url)
- return o.scheme
-
-def getHost(url):
- """Return the domain of a given URL"""
- return urlparse(url).netloc
-
-def getPort(url):
- """Return the port of a given URL"""
- return urlparse(url).port
-
-def getPath(url):
- """Return the page request of a given URL"""
- return urlparse(url).path
-
-def getUser(url):
- """Return the page request of a given URL"""
- return urlparse(url).username
-def getPassword(url):
- """Return the page request of a given URL"""
- return urlparse(url).password
-
-
-# Get real pages
-
-def getURLContent(url, timeout=15):
- """Return page content corresponding to URL or None if any error occurs"""
- o = urlparse(url)
- conn = http.client.HTTPConnection(o.netloc, port=o.port, timeout=timeout)
- try:
- if o.query != '':
- conn.request("GET", o.path + "?" + o.query, None, {"User-agent": "Nemubot v3"})
- else:
- conn.request("GET", o.path, None, {"User-agent": "Nemubot v3"})
- except socket.timeout:
- return None
- except socket.gaierror:
- print (" Unable to receive page %s from %s on %d."
- % (o.path, o.netloc, o.port))
- return None
-
- try:
- res = conn.getresponse()
- size = int(res.getheader("Content-Length", 200000))
- cntype = res.getheader("Content-Type")
-
- if size > 200000 or (cntype[:4] != "text" and cntype[:4] != "appl"):
- return None
-
- data = res.read(size)
- except http.client.BadStatusLine:
- return None
- finally:
- conn.close()
-
- if res.status == http.client.OK or res.status == http.client.SEE_OTHER:
- return data
- elif res.status == http.client.FOUND or res.status == http.client.MOVED_PERMANENTLY:
- return getURLContent(res.getheader("Location"), timeout)
- else:
- return None
-
-def getXML(url, timeout=15):
- """Get content page and return XML parsed content"""
- cnt = getURLContent(url, timeout)
- if cnt is None:
- return None
- else:
- return xmlparser.parse_string(cnt)
-
-def getJSON(url, timeout=15):
- """Get content page and return JSON content"""
- cnt = getURLContent(url, timeout)
- if cnt is None:
- return None
- else:
- return json.loads(cnt.decode())
-
-# Other utils
-
-def striphtml(data):
- """Remove HTML tags from text"""
- p = re.compile(r'<.*?>')
- return p.sub('', data).replace("(", "/(").replace(")", ")/").replace(""", "\"")
diff --git a/tools/wrapper.py b/tools/wrapper.py
deleted file mode 100644
index 3f4f5e6..0000000
--- a/tools/wrapper.py
+++ /dev/null
@@ -1,66 +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 .
-
-from xmlparser.node import ModuleState
-
-class Wrapper:
- """Simulate a hash table
-
- """
-
- def __init__(self):
- self.stateName = "state"
- self.attName = "name"
- self.cache = dict()
-
- def items(self):
- ret = list()
- for k in self.DATAS.index.keys():
- ret.append((k, self[k]))
- return ret
-
- def __contains__(self, i):
- return i in self.DATAS.index
-
- def __getitem__(self, i):
- return self.DATAS.index[i]
-
- def __setitem__(self, i, j):
- ms = ModuleState(self.stateName)
- ms.setAttribute(self.attName, i)
- j.save(ms)
- self.DATAS.addChild(ms)
- self.DATAS.setIndex(self.attName, self.stateName)
-
- def __delitem__(self, i):
- self.DATAS.delChild(self.DATAS.index[i])
-
- def save(self, i):
- if i in self.cache:
- self.cache[i].save(self.DATAS.index[i])
- del self.cache[i]
-
- def flush(self):
- """Remove all cached datas"""
- self.cache = dict()
-
- def reset(self):
- """Erase the list and flush the cache"""
- for child in self.DATAS.getNodes(self.stateName):
- self.DATAS.delChild(child)
- self.flush()
diff --git a/xmlparser/__init__.py b/xmlparser/__init__.py
deleted file mode 100644
index adfb85b..0000000
--- a/xmlparser/__init__.py
+++ /dev/null
@@ -1,74 +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 .
-
-import os
-import imp
-import xml.sax
-
-from . import node as module_state
-
-class ModuleStatesFile(xml.sax.ContentHandler):
- def startDocument(self):
- self.root = None
- self.stack = list()
-
- def startElement(self, name, attrs):
- cur = module_state.ModuleState(name)
-
- for name in attrs.keys():
- cur.setAttribute(name, attrs.getValue(name))
-
- self.stack.append(cur)
-
- def characters(self, content):
- self.stack[len(self.stack)-1].content += content
-
- def endElement(self, name):
- child = self.stack.pop()
- size = len(self.stack)
- if size > 0:
- self.stack[size - 1].content = self.stack[size - 1].content.strip()
- self.stack[size - 1].addChild(child)
- else:
- self.root = child
-
-def parse_file(filename):
- parser = xml.sax.make_parser()
- mod = ModuleStatesFile()
- parser.setContentHandler(mod)
- try:
- parser.parse(open(filename, "r"))
- return mod.root
- except IOError:
- return module_state.ModuleState("nemubotstate")
- except:
- if mod.root is None:
- return module_state.ModuleState("nemubotstate")
- else:
- return mod.root
-
-def parse_string(string):
- mod = ModuleStatesFile()
- try:
- xml.sax.parseString(string, mod)
- return mod.root
- except:
- if mod.root is None:
- return module_state.ModuleState("nemubotstate")
- else:
- return mod.root
diff --git a/xmlparser/node.py b/xmlparser/node.py
deleted file mode 100644
index 4aa5d2f..0000000
--- a/xmlparser/node.py
+++ /dev/null
@@ -1,191 +0,0 @@
-# coding=utf-8
-
-import xml.sax
-from datetime import datetime
-from datetime import date
-import time
-
-class ModuleState:
- """Tiny tree representation of an XML file"""
-
- def __init__(self, name):
- self.name = name
- self.content = ""
- self.attributes = dict()
- self.childs = list()
- self.index = dict()
- self.index_fieldname = None
- self.index_tagname = None
-
- def getName(self):
- """Get the name of the current node"""
- return self.name
-
- def display(self, level = 0):
- ret = ""
- out = list()
- for k in self.attributes:
- out.append("%s : %s" % (k, self.attributes[k]))
- ret += "%s%s { %s } = '%s'\n" % (' ' * level, self.name, ' ; '.join(out), self.content)
- for c in self.childs:
- ret += c.display(level + 2)
- return ret
-
- def __str__(self):
- return self.display()
-
- def __getitem__(self, i):
- """Return the attribute asked"""
- return self.getAttribute(i)
-
- def __setitem__(self, i, c):
- """Set the attribute"""
- return self.setAttribute(i, c)
-
- def getAttribute(self, name):
- """Get the asked argument or return None if doesn't exist"""
- if name in self.attributes:
- return self.attributes[name]
- else:
- return None
-
- def getDate(self, name=None):
- """Get the asked argument and return it as a date"""
- if name is None:
- source = self.content
- elif name in self.attributes.keys():
- source = self.attributes[name]
- else:
- return None
-
- if isinstance(source, datetime):
- return source
- else:
- try:
- return datetime.fromtimestamp(float(source))
- except ValueError:
- while True:
- try:
- return datetime.fromtimestamp(time.mktime(
- time.strptime(source[:19], "%Y-%m-%d %H:%M:%S")))
- except ImportError:
- pass
-
- def getInt(self, name=None):
- """Get the asked argument and return it as an integer"""
- if name is None:
- source = self.content
- elif name in self.attributes.keys():
- source = self.attributes[name]
- else:
- return None
-
- return int(float(source))
-
- def getBool(self, name=None):
- """Get the asked argument and return it as an integer"""
- if name is None:
- source = self.content
- elif name in self.attributes.keys():
- source = self.attributes[name]
- else:
- return False
-
- return (isinstance(source, bool) and source) or source == "True"
-
- def setIndex(self, fieldname = "name", tagname = None):
- """Defines an hash table to accelerate childs search. You have just to define a common attribute"""
- self.index = dict()
- self.index_fieldname = fieldname
- self.index_tagname = tagname
- for child in self.childs:
- if (tagname is None or tagname == child.name) and child.hasAttribute(fieldname):
- self.index[child[fieldname]] = child
-
- def __contains__(self, i):
- """Return true if i is found in the index"""
- return i in self.index
-
- def hasAttribute(self, name):
- """DOM like method"""
- return (name in self.attributes)
-
- def setAttribute(self, name, value):
- """DOM like method"""
- self.attributes[name] = value
-
- def getContent(self):
- return self.content
-
- def getChilds(self):
- """Return a full list of direct child of this node"""
- return self.childs
-
- def getNode(self, tagname):
- """Get a unique node (or the last one) with the given tagname"""
- ret = None
- for child in self.childs:
- if tagname is None or tagname == child.name:
- ret = child
- return ret
-
- def getFirstNode(self, tagname):
- """Get a unique node (or the last one) with the given tagname"""
- for child in self.childs:
- if tagname is None or tagname == child.name:
- return child
- return None
-
- def getNodes(self, tagname):
- """Get all direct childs that have the given tagname"""
- ret = list()
- for child in self.childs:
- if tagname is None or tagname == child.name:
- ret.append(child)
- return ret
-
- def hasNode(self, tagname):
- """Return True if at least one node with the given tagname exists"""
- ret = list()
- for child in self.childs:
- if tagname is None or tagname == child.name:
- return True
- return False
-
- def addChild(self, child):
- """Add a child to this node"""
- self.childs.append(child)
- if self.index_fieldname is not None:
- self.setIndex(self.index_fieldname, self.index_tagname)
-
- def delChild(self, child):
- """Remove the given child from this node"""
- self.childs.remove(child)
- if self.index_fieldname is not None:
- self.setIndex(self.index_fieldname, self.index_tagname)
-
- def save_node(self, gen):
- """Serialize this node as a XML node"""
- attribs = {}
- for att in self.attributes.keys():
- if att[0] != "_": # Don't save attribute starting by _
- if isinstance(self.attributes[att], datetime):
- attribs[att] = str(time.mktime(self.attributes[att].timetuple()))
- else:
- attribs[att] = str(self.attributes[att])
- attrs = xml.sax.xmlreader.AttributesImpl(attribs)
-
- gen.startElement(self.name, attrs)
-
- for child in self.childs:
- child.save_node(gen)
-
- gen.endElement(self.name)
-
- def save(self, filename):
- """Save the current node as root node in a XML file"""
- with open(filename,"w") as f:
- gen = xml.sax.saxutils.XMLGenerator(f, "utf-8")
- gen.startDocument()
- self.save_node(gen)
- gen.endDocument()