', 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
new file mode 100644
index 0000000..fb674ea
--- /dev/null
+++ b/modules/cristal.py
@@ -0,0 +1,64 @@
+# 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
new file mode 100644
index 0000000..3e83d90
--- /dev/null
+++ b/modules/cristal.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/modules/ctfs.py b/modules/ctfs.py
deleted file mode 100644
index ac27c4a..0000000
--- a/modules/ctfs.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""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
deleted file mode 100644
index 18d9898..0000000
--- a/modules/cve.py
+++ /dev/null
@@ -1,99 +0,0 @@
-"""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
deleted file mode 100644
index 089409b..0000000
--- a/modules/ddg.py
+++ /dev/null
@@ -1,138 +0,0 @@
-"""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
new file mode 100644
index 0000000..77aee50
--- /dev/null
+++ b/modules/ddg/DDGSearch.py
@@ -0,0 +1,68 @@
+# 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
new file mode 100644
index 0000000..b91fa2c
--- /dev/null
+++ b/modules/ddg/WFASearch.py
@@ -0,0 +1,71 @@
+# 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
new file mode 100644
index 0000000..314af38
--- /dev/null
+++ b/modules/ddg/Wikipedia.py
@@ -0,0 +1,56 @@
+# 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
new file mode 100644
index 0000000..ff50274
--- /dev/null
+++ b/modules/ddg/__init__.py
@@ -0,0 +1,129 @@
+# 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
deleted file mode 100644
index bec0a87..0000000
--- a/modules/dig.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""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
deleted file mode 100644
index cb80ef3..0000000
--- a/modules/disas.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""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
deleted file mode 100644
index acac196..0000000
--- a/modules/events.py
+++ /dev/null
@@ -1,296 +0,0 @@
-"""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
new file mode 100644
index 0000000..a96794d
--- /dev/null
+++ b/modules/events.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/events/__init__.py b/modules/events/__init__.py
new file mode 100644
index 0000000..c331157
--- /dev/null
+++ b/modules/events/__init__.py
@@ -0,0 +1,238 @@
+# 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
deleted file mode 100644
index 49ad8a6..0000000
--- a/modules/freetarifs.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""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
deleted file mode 100644
index 5f9a7d9..0000000
--- a/modules/github.py
+++ /dev/null
@@ -1,231 +0,0 @@
-"""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
deleted file mode 100644
index fde8ecb..0000000
--- a/modules/grep.py
+++ /dev/null
@@ -1,85 +0,0 @@
-"""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
deleted file mode 100644
index 7a42935..0000000
--- a/modules/imdb.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""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
deleted file mode 100644
index 3126dc1..0000000
--- a/modules/jsonbot.py
+++ /dev/null
@@ -1,58 +0,0 @@
-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 f60e0cf..00edc8e 100644
--- a/modules/man.py
+++ b/modules/man.py
@@ -1,78 +1,66 @@
-"""Read manual pages on IRC"""
-
-# PYTHON STUFFS #######################################################
+# coding=utf-8
import subprocess
import re
import os
-from nemubot.hooks import hook
+nemubotversion = 3.3
-from nemubot.module.more import Response
+def load(context):
+ from hooks import Hook
+ add_hook("cmd_hook", Hook(cmd_man, "MAN"))
+ add_hook("cmd_hook", Hook(cmd_whatis, "man"))
+def help_tiny ():
+ """Line inserted in the response to the command !help"""
+ return "Read man on IRC"
-# GLOBALS #############################################################
+def help_full ():
+ return "!man [0-9] /what/: gives informations about /what/."
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.args) == 1:
- args.append(msg.args[0])
- elif len(msg.args) >= 2:
+ if len(msg.cmds) == 2:
+ args.append(msg.cmds[1])
+ elif len(msg.cmds) >= 3:
try:
- num = int(msg.args[0])
+ num = int(msg.cmds[1])
args.append("%d" % num)
- args.append(msg.args[1])
+ args.append(msg.cmds[2])
except ValueError:
- args.append(msg.args[0])
+ args.append(msg.cmds[1])
os.unsetenv("LANG")
- res = Response(channel=msg.channel)
- with subprocess.Popen(args,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE) as proc:
+ res = Response(msg.sender, 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("There is no entry %s in section %d." %
- (msg.args[0], num))
+ 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("There is no man page for %s." % msg.args[0])
+ res.append_message("Il n'y a pas de page de manuel pour %s." % msg.cmds[1])
return res
-
-@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)]
+ args = ["whatis", " ".join(msg.cmds[1:])]
- res = Response(channel=msg.channel)
- with subprocess.Popen(args,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE) as proc:
+ res = Response(msg.sender, 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:
- res.append_message("There is no man page for %s." % msg.args[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])
return res
diff --git a/modules/mapquest.py b/modules/mapquest.py
deleted file mode 100644
index f328e1d..0000000
--- a/modules/mapquest.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""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
deleted file mode 100644
index be608ca..0000000
--- a/modules/mediawiki.py
+++ /dev/null
@@ -1,249 +0,0 @@
-# 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
new file mode 100644
index 0000000..d6431e0
--- /dev/null
+++ b/modules/networking.py
@@ -0,0 +1,119 @@
+# 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
deleted file mode 100644
index 3b939ab..0000000
--- a/modules/networking/__init__.py
+++ /dev/null
@@ -1,184 +0,0 @@
-"""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
deleted file mode 100644
index 99e2664..0000000
--- a/modules/networking/isup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-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
deleted file mode 100644
index 689944b..0000000
--- a/modules/networking/page.py
+++ /dev/null
@@ -1,131 +0,0 @@
-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
deleted file mode 100644
index 3c8084f..0000000
--- a/modules/networking/w3c.py
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644
index d6b806f..0000000
--- a/modules/networking/watchWebsite.py
+++ /dev/null
@@ -1,223 +0,0 @@
-"""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
deleted file mode 100644
index 999dc01..0000000
--- a/modules/networking/whois.py
+++ /dev/null
@@ -1,136 +0,0 @@
-# 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
deleted file mode 100644
index c4c967a..0000000
--- a/modules/news.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""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
new file mode 100644
index 0000000..d34e8ae
--- /dev/null
+++ b/modules/nextstop.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/modules/nextstop/__init__.py b/modules/nextstop/__init__.py
new file mode 100644
index 0000000..71816a8
--- /dev/null
+++ b/modules/nextstop/__init__.py
@@ -0,0 +1,50 @@
+# 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
new file mode 160000
index 0000000..e5675c6
--- /dev/null
+++ b/modules/nextstop/external
@@ -0,0 +1 @@
+Subproject commit e5675c631665dfbdaba55a0be66708a07d157408
diff --git a/modules/nntp.py b/modules/nntp.py
deleted file mode 100644
index 7fdceb4..0000000
--- a/modules/nntp.py
+++ /dev/null
@@ -1,229 +0,0 @@
-"""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
deleted file mode 100644
index b9b6e21..0000000
--- a/modules/openai.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""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
deleted file mode 100644
index c280dec..0000000
--- a/modules/openroute.py
+++ /dev/null
@@ -1,158 +0,0 @@
-"""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
deleted file mode 100644
index 386946f..0000000
--- a/modules/pkgs.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""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
new file mode 100644
index 0000000..05a7076
--- /dev/null
+++ b/modules/qcm.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/qcm/Course.py b/modules/qcm/Course.py
new file mode 100644
index 0000000..9cddf1a
--- /dev/null
+++ b/modules/qcm/Course.py
@@ -0,0 +1,31 @@
+# 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
new file mode 100644
index 0000000..6895680
--- /dev/null
+++ b/modules/qcm/Question.py
@@ -0,0 +1,93 @@
+# 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
new file mode 100644
index 0000000..48ed23f
--- /dev/null
+++ b/modules/qcm/QuestionFile.py
@@ -0,0 +1,16 @@
+# 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
new file mode 100644
index 0000000..11ab46b
--- /dev/null
+++ b/modules/qcm/Session.py
@@ -0,0 +1,67 @@
+# 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
new file mode 100644
index 0000000..5f18831
--- /dev/null
+++ b/modules/qcm/User.py
@@ -0,0 +1,27 @@
+# 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
new file mode 100644
index 0000000..b8b01df
--- /dev/null
+++ b/modules/qcm/__init__.py
@@ -0,0 +1,197 @@
+# 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
new file mode 100644
index 0000000..a81ac5d
--- /dev/null
+++ b/modules/qd/DelayedTuple.py
@@ -0,0 +1,32 @@
+# 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
new file mode 100644
index 0000000..7449489
--- /dev/null
+++ b/modules/qd/GameUpdater.py
@@ -0,0 +1,60 @@
+# 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
new file mode 100644
index 0000000..41b2eff
--- /dev/null
+++ b/modules/qd/QDWrapper.py
@@ -0,0 +1,20 @@
+# 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
new file mode 100644
index 0000000..52c5692
--- /dev/null
+++ b/modules/qd/Score.py
@@ -0,0 +1,126 @@
+# 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
new file mode 100644
index 0000000..871512b
--- /dev/null
+++ b/modules/qd/__init__.py
@@ -0,0 +1,224 @@
+# 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
deleted file mode 100644
index 06f5f1d..0000000
--- a/modules/ratp.py
+++ /dev/null
@@ -1,74 +0,0 @@
-"""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
deleted file mode 100644
index d4def85..0000000
--- a/modules/reddit.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# 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
deleted file mode 100644
index 8dbc6da..0000000
--- a/modules/repology.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# 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 d1c6fe7..198983c 100644
--- a/modules/rnd.py
+++ b/modules/rnd.py
@@ -1,54 +1,12 @@
-"""Help to make choice"""
-
-# PYTHON STUFFS #######################################################
+# coding=utf-8
import random
-import shlex
-from nemubot import context
-from nemubot.exception import IMException
-from nemubot.hooks import hook
+nemubotversion = 3.3
-from nemubot.module.more import Response
+def load(context):
+ from hooks import Hook
+ add_hook("cmd_hook", Hook(cmd_choice, "choice"))
-
-# MODULE INTERFACE ####################################################
-
-@hook.command("choice")
def cmd_choice(msg):
- 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
+ return Response(msg.sender, random.choice(msg.cmds[1:]), channel=msg.channel)
diff --git a/modules/sap.py b/modules/sap.py
deleted file mode 100644
index 0b9017f..0000000
--- a/modules/sap.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# 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
deleted file mode 100644
index 9c158c6..0000000
--- a/modules/shodan.py
+++ /dev/null
@@ -1,104 +0,0 @@
-"""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 f7fb626..b53a2e5 100644
--- a/modules/sleepytime.py
+++ b/modules/sleepytime.py
@@ -1,50 +1,52 @@
# coding=utf-8
-"""as http://sleepyti.me/, give you the best time to go to bed"""
-
import re
import imp
-from datetime import datetime, timedelta, timezone
+from datetime import datetime
+from datetime import timedelta
-from nemubot.hooks import hook
+nemubotversion = 3.3
-nemubotversion = 3.4
+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"
-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"
+
+def load(context):
+ from hooks import Hook
+ add_hook("cmd_hook", Hook(cmd_sleep, "sleeptime"))
+ add_hook("cmd_hook", Hook(cmd_sleep, "sleepytime"))
-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.args) and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?",
- msg.args[0]) is not None:
+ if len (msg.cmds) > 1 and re.match("[0-9]{1,2}[h':.,-]([0-9]{1,2})?[m'\":.,-]?",
+ msg.cmds[1]) is not None:
# First, parse the hour
- 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,
+ 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,
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(6):
- f.append(f[i] - timedelta(hours=1, minutes=30))
+ for i in range(0,6):
+ f.append(f[i] - timedelta(hours=1,minutes=30))
g.append(f[i+1].strftime("%H:%M"))
- return Response("You should try to fall asleep at one of the following"
- " times: %s" % ', '.join(g), channel=msg.channel)
+ return Response(msg.sender,
+ "You should try to fall asleep at one of the following"
+ " times: %s" % ', '.join(g), msg.channel)
# Just get awake times
else:
- f = [datetime.now(timezone.utc) + timedelta(minutes=15)]
+ f = [datetime.now() + timedelta(minutes=15)]
g = list()
- for i in range(6):
- f.append(f[i] + timedelta(hours=1, minutes=30))
+ for i in range(0,6):
+ f.append(f[i] + timedelta(hours=1,minutes=30))
g.append(f[i+1].strftime("%H:%M"))
- return Response("If you head to bed right now, you should try to wake"
+ return Response(msg.sender,
+ "If you head to bed right now, you should try to wake"
" up at one of the following times: %s" %
- ', '.join(g), channel=msg.channel)
+ ', '.join(g), msg.channel)
diff --git a/modules/smmry.py b/modules/smmry.py
deleted file mode 100644
index b1fe72c..0000000
--- a/modules/smmry.py
+++ /dev/null
@@ -1,116 +0,0 @@
-"""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
deleted file mode 100644
index 57ab3ae..0000000
--- a/modules/sms.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# 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
new file mode 100644
index 0000000..957423b
--- /dev/null
+++ b/modules/soutenance.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/modules/soutenance/Delayed.py b/modules/soutenance/Delayed.py
new file mode 100644
index 0000000..8cf47c5
--- /dev/null
+++ b/modules/soutenance/Delayed.py
@@ -0,0 +1,13 @@
+# 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
new file mode 100644
index 0000000..63833b7
--- /dev/null
+++ b/modules/soutenance/SiteSoutenances.py
@@ -0,0 +1,179 @@
+# 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
new file mode 100644
index 0000000..e2a0882
--- /dev/null
+++ b/modules/soutenance/Soutenance.py
@@ -0,0 +1,11 @@
+# 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
new file mode 100644
index 0000000..61b3aa6
--- /dev/null
+++ b/modules/soutenance/__init__.py
@@ -0,0 +1,48 @@
+# 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
deleted file mode 100644
index c08b2bd..0000000
--- a/modules/speak.py
+++ /dev/null
@@ -1,133 +0,0 @@
-# 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 da16a80..918831b 100644
--- a/modules/spell/__init__.py
+++ b/modules/spell/__init__.py
@@ -1,97 +1,89 @@
-"""Check words spelling"""
+# coding=utf-8
-# PYTHON STUFFS #######################################################
-
-from nemubot import context
-from nemubot.exception import IMException
-from nemubot.hooks import hook
-from nemubot.tools.xmlparser.node import ModuleState
+import re
+from urllib.parse import quote
from .pyaspell import Aspell
from .pyaspell import AspellError
-from nemubot.module.more import Response
+nemubotversion = 3.3
+def help_tiny ():
+ return "Check words spelling"
-# LOADING #############################################################
+def help_full ():
+ return "!spell [] : give the correct spelling of in ."
def load(context):
- context.data.setIndex("name", "score")
+ 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"))
-# MODULE CORE #########################################################
+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)
def add_score(nick, t):
- if nick not in context.data.index:
+ global DATAS
+ if nick not in DATAS.index:
st = ModuleState("score")
st["name"] = nick
- context.data.addChild(st)
+ DATAS.addChild(st)
- if context.data.index[nick].hasAttribute(t):
- context.data.index[nick][t] = context.data.index[nick].getInt(t) + 1
+ if DATAS.index[nick].hasAttribute(t):
+ DATAS.index[nick][t] = DATAS.index[nick].getInt(t) + 1
else:
- context.data.index[nick][t] = 1
- context.save()
+ DATAS.index[nick][t] = 1
+ save()
-
-def check_spell(word, lang='fr'):
- a = Aspell([("lang", lang)])
- if a.check(word.encode("utf-8")):
- ret = True
- else:
- 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
-
-
-@hook.command("spellscore",
- help="Show spell score (tests, mistakes, ...) for someone",
- help_usage={"USER": "Display score of USER"})
def cmd_score(msg):
+ global DATAS
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(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)
+ 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("%s inconnus" % ", ".join(unknown), channel=msg.channel))
+ res.append(Response(msg.sender, "%s inconnus" % ", ".join(unknown), channel=msg.channel))
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
diff --git a/modules/suivi.py b/modules/suivi.py
deleted file mode 100644
index a54b722..0000000
--- a/modules/suivi.py
+++ /dev/null
@@ -1,332 +0,0 @@
-"""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 78f0b7d..047fe03 100644
--- a/modules/syno.py
+++ b/modules/syno.py
@@ -1,117 +1,61 @@
-"""Find synonyms"""
-
-# PYTHON STUFFS #######################################################
+# coding=utf-8
import re
+import traceback
+import sys
from urllib.parse import quote
-from nemubot.exception import IMException
-from nemubot.hooks import hook
-from nemubot.tools import web
+from tools import web
-from nemubot.module.more import Response
+nemubotversion = 3.3
+def help_tiny ():
+ return "Find french synonyms"
-# LOADING #############################################################
+def help_full ():
+ return "!syno : give a list of synonyms for ."
def load(context):
- global lang_binding
-
- 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:
- lang_binding["en"] = lambda word: get_english_synos(context.config["bighugelabskey"], word)
+ from hooks import Hook
+ add_hook("cmd_hook", Hook(cmd_syno, "syno"))
+ add_hook("cmd_hook", Hook(cmd_syno, "synonyme"))
-# MODULE CORE #########################################################
+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)
-def get_french_synos(word):
- url = "https://crisco.unicaen.fr/des/synonymes/" + quote(word)
+ 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)
-
- 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:
+ synos = list()
+ for line in page.decode().split("\n"):
+ if 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)
-
+ synos.append(elt.group(1))
+ return synos
else:
- raise IMException("WHAT?!")
+ return None
diff --git a/modules/tpb.py b/modules/tpb.py
deleted file mode 100644
index a752324..0000000
--- a/modules/tpb.py
+++ /dev/null
@@ -1,40 +0,0 @@
-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 906ba93..60838f0 100644
--- a/modules/translate.py
+++ b/modules/translate.py
@@ -1,111 +1,97 @@
-"""Translation module"""
-
-# PYTHON STUFFS #######################################################
+# coding=utf-8
+import http.client
+import re
+import socket
+import json
from urllib.parse import quote
-from nemubot.exception import IMException
-from nemubot.hooks import hook
-from nemubot.tools import web
+nemubotversion = 3.3
-from nemubot.module.more import Response
-
-
-# GLOBALS #############################################################
+import xmlparser
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):
- 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"]
+ 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"))
-# 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):
- 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"]
+ global LANG
+ startWord = 1
+ if msg.cmds[startWord] in LANG:
+ langTo = msg.cmds[startWord]
+ startWord += 1
else:
- langTo = "fr" if langFrom == "en" else "en"
+ 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"
- 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")
+ (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)
- 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
+
+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)
diff --git a/modules/urbandict.py b/modules/urbandict.py
deleted file mode 100644
index b561e89..0000000
--- a/modules/urbandict.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""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
deleted file mode 100644
index 86f4d42..0000000
--- a/modules/urlreducer.py
+++ /dev/null
@@ -1,173 +0,0 @@
-"""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 71c472c..8385476 100644
--- a/modules/velib.py
+++ b/modules/velib.py
@@ -1,53 +1,51 @@
-"""Gets information about velib stations"""
-
-# PYTHON STUFFS #######################################################
+# coding=utf-8
import re
-from nemubot import context
-from nemubot.exception import IMException
-from nemubot.hooks import hook
-from nemubot.tools import web
+from tools import web
-from nemubot.module.more import Response
-
-
-# LOADING #############################################################
-
-URL_API = None # http://www.velib.paris.fr/service/stationdetails/paris/%s
+nemubotversion = 3.3
def load(context):
- 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")
+ global DATAS
+ DATAS.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(URL_API % station)
+ response = web.getXML(CONF.getNode("server")["url"] + station)
if response is not None:
- available = int(response.getElementsByTagName("available")[0].firstChild.nodeValue)
- free = int(response.getElementsByTagName("free")[0].firstChild.nodeValue)
+ 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
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)
@@ -58,30 +56,33 @@ 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("À la station %s : %d vélib et %d points d'attache"
- " disponibles." % (station, available, free),
- channel=msg.channel)
- raise IMException("station %s inconnue." % station)
+ 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)
-
-# 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):
- 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.")
+ """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)
diff --git a/modules/virtualradar.py b/modules/virtualradar.py
deleted file mode 100644
index 2c87e79..0000000
--- a/modules/virtualradar.py
+++ /dev/null
@@ -1,100 +0,0 @@
-"""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
new file mode 100644
index 0000000..7a116e9
--- /dev/null
+++ b/modules/watchWebsite.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/modules/watchWebsite/__init__.py b/modules/watchWebsite/__init__.py
new file mode 100644
index 0000000..1f69158
--- /dev/null
+++ b/modules/watchWebsite/__init__.py
@@ -0,0 +1,181 @@
+# 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
new file mode 100755
index 0000000..30272e0
--- /dev/null
+++ b/modules/watchWebsite/atom.py
@@ -0,0 +1,84 @@
+#!/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
deleted file mode 100644
index 9b36470..0000000
--- a/modules/weather.py
+++ /dev/null
@@ -1,261 +0,0 @@
-# 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
new file mode 100644
index 0000000..90b2c2f
--- /dev/null
+++ b/modules/whereis.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/whereis/Delayed.py b/modules/whereis/Delayed.py
new file mode 100644
index 0000000..45826f4
--- /dev/null
+++ b/modules/whereis/Delayed.py
@@ -0,0 +1,5 @@
+# coding=utf-8
+
+class Delayed:
+ def __init__(self):
+ self.names = dict()
diff --git a/modules/whereis/UpdatedStorage.py b/modules/whereis/UpdatedStorage.py
new file mode 100644
index 0000000..de09848
--- /dev/null
+++ b/modules/whereis/UpdatedStorage.py
@@ -0,0 +1,57 @@
+# 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
new file mode 100644
index 0000000..d4b48b4
--- /dev/null
+++ b/modules/whereis/User.py
@@ -0,0 +1,35 @@
+# 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
new file mode 100644
index 0000000..57ebb73
--- /dev/null
+++ b/modules/whereis/__init__.py
@@ -0,0 +1,206 @@
+# 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
deleted file mode 100644
index 1a5f598..0000000
--- a/modules/whois.py
+++ /dev/null
@@ -1,167 +0,0 @@
-# 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
deleted file mode 100644
index fc83815..0000000
--- a/modules/wolframalpha.py
+++ /dev/null
@@ -1,118 +0,0 @@
-"""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
deleted file mode 100644
index e72f1ac..0000000
--- a/modules/worldcup.py
+++ /dev/null
@@ -1,216 +0,0 @@
-# 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
new file mode 100644
index 0000000..7180ba2
--- /dev/null
+++ b/modules/ycc.py
@@ -0,0 +1,74 @@
+# 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
deleted file mode 100644
index 41b613a..0000000
--- a/modules/youtube-title.py
+++ /dev/null
@@ -1,96 +0,0 @@
-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
new file mode 100644
index 0000000..f28ef77
--- /dev/null
+++ b/modules/youtube.py
@@ -0,0 +1,51 @@
+# 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
new file mode 100755
index 0000000..5948304
--- /dev/null
+++ b/nemubot.py
@@ -0,0 +1,76 @@
+#!/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
deleted file mode 100644
index 62807c6..0000000
--- a/nemubot/__init__.py
+++ /dev/null
@@ -1,148 +0,0 @@
-# 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
deleted file mode 100644
index 7070639..0000000
--- a/nemubot/__main__.py
+++ /dev/null
@@ -1,279 +0,0 @@
-# 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
deleted file mode 100644
index 2b6e15c..0000000
--- a/nemubot/bot.py
+++ /dev/null
@@ -1,548 +0,0 @@
-# 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
deleted file mode 100644
index 835c22f..0000000
--- a/nemubot/channel.py
+++ /dev/null
@@ -1,162 +0,0 @@
-# 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
deleted file mode 100644
index 6bbc1b2..0000000
--- a/nemubot/config/__init__.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# 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
deleted file mode 100644
index 408c09a..0000000
--- a/nemubot/config/include.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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
deleted file mode 100644
index ab51971..0000000
--- a/nemubot/config/module.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# 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
deleted file mode 100644
index 992cd8e..0000000
--- a/nemubot/config/nemubot.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# 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
deleted file mode 100644
index 17bfaee..0000000
--- a/nemubot/config/server.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# 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
deleted file mode 100644
index a9a4146..0000000
--- a/nemubot/consumer.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# 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
deleted file mode 100644
index 3e38ad2..0000000
--- a/nemubot/datastore/__init__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# 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
deleted file mode 100644
index aeaecc6..0000000
--- a/nemubot/datastore/abstract.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# 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
deleted file mode 100644
index aa6cbd0..0000000
--- a/nemubot/datastore/xml.py
+++ /dev/null
@@ -1,171 +0,0 @@
-# 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
deleted file mode 100644
index 49c6902..0000000
--- a/nemubot/event/__init__.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# 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
deleted file mode 100644
index 84464a0..0000000
--- a/nemubot/exception/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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
deleted file mode 100644
index 6e3c07f..0000000
--- a/nemubot/exception/keyword.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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
deleted file mode 100644
index 9024494..0000000
--- a/nemubot/hooks/__init__.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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
deleted file mode 100644
index ffe79fb..0000000
--- a/nemubot/hooks/abstract.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# 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
deleted file mode 100644
index 863d672..0000000
--- a/nemubot/hooks/command.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# 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
deleted file mode 100644
index 598b04f..0000000
--- a/nemubot/hooks/keywords/__init__.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# 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
deleted file mode 100644
index a990cf3..0000000
--- a/nemubot/hooks/keywords/abstract.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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
deleted file mode 100644
index c2d3f2e..0000000
--- a/nemubot/hooks/keywords/dict.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# 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
deleted file mode 100644
index 6a57d2a..0000000
--- a/nemubot/hooks/manager.py
+++ /dev/null
@@ -1,134 +0,0 @@
-# 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
deleted file mode 100755
index a0f38d7..0000000
--- a/nemubot/hooks/manager_test.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/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
deleted file mode 100644
index ee07600..0000000
--- a/nemubot/hooks/message.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# 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
deleted file mode 100644
index 674ab40..0000000
--- a/nemubot/importer.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# 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
deleted file mode 100644
index 4d69dbb..0000000
--- a/nemubot/message/__init__.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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
deleted file mode 100644
index 3af0511..0000000
--- a/nemubot/message/abstract.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# 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
deleted file mode 100644
index ca87e4c..0000000
--- a/nemubot/message/command.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# 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
deleted file mode 100644
index 3b1fabb..0000000
--- a/nemubot/message/directask.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# 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
deleted file mode 100644
index abd1f2f..0000000
--- a/nemubot/message/printer/IRCLib.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# 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
deleted file mode 100644
index ad1b99e..0000000
--- a/nemubot/message/printer/Matrix.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# 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
deleted file mode 100644
index e0fbeef..0000000
--- a/nemubot/message/printer/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# 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
deleted file mode 100644
index 6884c88..0000000
--- a/nemubot/message/printer/socket.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# 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
deleted file mode 100644
index 41f74b0..0000000
--- a/nemubot/message/printer/test_socket.py
+++ /dev/null
@@ -1,112 +0,0 @@
-# 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
deleted file mode 100644
index f9353ad..0000000
--- a/nemubot/message/response.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# 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
deleted file mode 100644
index f691a04..0000000
--- a/nemubot/message/text.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# 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
deleted file mode 100644
index 454633a..0000000
--- a/nemubot/message/visitor.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# 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
deleted file mode 100644
index 33f0e41..0000000
--- a/nemubot/module/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-#
-# 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
deleted file mode 100644
index 206d97a..0000000
--- a/nemubot/module/more.py
+++ /dev/null
@@ -1,299 +0,0 @@
-# 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
deleted file mode 100644
index 4af3731..0000000
--- a/nemubot/modulecontext.py
+++ /dev/null
@@ -1,155 +0,0 @@
-# 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
deleted file mode 100644
index cdd13cf..0000000
--- a/nemubot/server/IRCLib.py
+++ /dev/null
@@ -1,375 +0,0 @@
-# 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():
- super().on_disconnect(connection, event)
-
-
- # 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
deleted file mode 100644
index ed4b746..0000000
--- a/nemubot/server/Matrix.py
+++ /dev/null
@@ -1,200 +0,0 @@
-# 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
deleted file mode 100644
index db9ad87..0000000
--- a/nemubot/server/__init__.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# 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
deleted file mode 100644
index 8fbb923..0000000
--- a/nemubot/server/abstract.py
+++ /dev/null
@@ -1,167 +0,0 @@
-# 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
deleted file mode 100644
index bf55bf5..0000000
--- a/nemubot/server/socket.py
+++ /dev/null
@@ -1,172 +0,0 @@
-# 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
deleted file mode 100644
index eb1ae19..0000000
--- a/nemubot/server/threaded.py
+++ /dev/null
@@ -1,132 +0,0 @@
-# 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
deleted file mode 100644
index 57f3468..0000000
--- a/nemubot/tools/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# 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
deleted file mode 100644
index afd585f..0000000
--- a/nemubot/tools/countdown.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# 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
deleted file mode 100644
index 9e9bbad..0000000
--- a/nemubot/tools/date.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# 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
deleted file mode 100644
index 6f8930d..0000000
--- a/nemubot/tools/feed.py
+++ /dev/null
@@ -1,157 +0,0 @@
-#!/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
deleted file mode 100644
index a18cde2..0000000
--- a/nemubot/tools/human.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# 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
deleted file mode 100644
index 8ebdd49..0000000
--- a/nemubot/tools/test_human.py
+++ /dev/null
@@ -1,40 +0,0 @@
-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
deleted file mode 100644
index 0feda73..0000000
--- a/nemubot/tools/test_xmlparser.py
+++ /dev/null
@@ -1,113 +0,0 @@
-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
deleted file mode 100644
index a545b19..0000000
--- a/nemubot/tools/web.py
+++ /dev/null
@@ -1,274 +0,0 @@
-# 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
deleted file mode 100644
index 1bf60a8..0000000
--- a/nemubot/tools/xmlparser/__init__.py
+++ /dev/null
@@ -1,174 +0,0 @@
-# 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
deleted file mode 100644
index dadff23..0000000
--- a/nemubot/tools/xmlparser/basic.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# 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
deleted file mode 100644
index 425934c..0000000
--- a/nemubot/tools/xmlparser/genericnode.py
+++ /dev/null
@@ -1,102 +0,0 @@
-# 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
deleted file mode 100644
index 7df255e..0000000
--- a/nemubot/tools/xmlparser/node.py
+++ /dev/null
@@ -1,223 +0,0 @@
-# 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
deleted file mode 100644
index ed7cacb..0000000
--- a/nemubot/treatment.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# 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
new file mode 100755
index 0000000..9501e17
--- /dev/null
+++ b/nemuspeak.py
@@ -0,0 +1,188 @@
+#!/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
new file mode 100644
index 0000000..756ab3c
--- /dev/null
+++ b/networkbot.py
@@ -0,0 +1,240 @@
+# -*- 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
new file mode 100644
index 0000000..62c8dc3
--- /dev/null
+++ b/prompt/__init__.py
@@ -0,0 +1,105 @@
+# -*- 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
new file mode 100644
index 0000000..512549d
--- /dev/null
+++ b/prompt/builtins.py
@@ -0,0 +1,157 @@
+# -*- 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
deleted file mode 100644
index e037895..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-irc
-matrix-nio
diff --git a/response.py b/response.py
new file mode 100644
index 0000000..9fda7f8
--- /dev/null
+++ b/response.py
@@ -0,0 +1,176 @@
+# -*- 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
new file mode 100644
index 0000000..e16bd57
--- /dev/null
+++ b/server.py
@@ -0,0 +1,169 @@
+# -*- 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
deleted file mode 100755
index 7b5bdcd..0000000
--- a/setup.py
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/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 ee403ac..c1c6f61 100644
--- a/speak_sample.xml
+++ b/speak_sample.xml
@@ -1,35 +1,27 @@
-
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/__init__.py b/tools/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tools/web.py b/tools/web.py
new file mode 100644
index 0000000..b0bf2e3
--- /dev/null
+++ b/tools/web.py
@@ -0,0 +1,119 @@
+# 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
new file mode 100644
index 0000000..3f4f5e6
--- /dev/null
+++ b/tools/wrapper.py
@@ -0,0 +1,66 @@
+# 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
new file mode 100644
index 0000000..adfb85b
--- /dev/null
+++ b/xmlparser/__init__.py
@@ -0,0 +1,74 @@
+# -*- 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
new file mode 100644
index 0000000..4aa5d2f
--- /dev/null
+++ b/xmlparser/node.py
@@ -0,0 +1,191 @@
+# 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()