|
1
|
|
|
# coding=utf-8 |
|
2
|
|
|
"""Sopel IRC Trigger Lines""" |
|
3
|
|
|
from __future__ import unicode_literals, absolute_import, print_function, division |
|
4
|
|
|
|
|
5
|
|
|
import re |
|
6
|
|
|
import sys |
|
7
|
|
|
import datetime |
|
8
|
|
|
|
|
9
|
|
|
from sopel import tools |
|
10
|
|
|
|
|
11
|
|
|
|
|
12
|
|
|
__all__ = [ |
|
13
|
|
|
'PreTrigger', |
|
14
|
|
|
'Trigger', |
|
15
|
|
|
] |
|
16
|
|
|
|
|
17
|
|
|
if sys.version_info.major >= 3: |
|
18
|
|
|
unicode = str |
|
19
|
|
|
basestring = str |
|
20
|
|
|
|
|
21
|
|
|
|
|
22
|
|
|
class PreTrigger(object): |
|
23
|
|
|
"""A parsed message from the server, which has not been matched against |
|
24
|
|
|
any rules.""" |
|
25
|
|
|
component_regex = re.compile(r'([^!]*)!?([^@]*)@?(.*)') |
|
26
|
|
|
intent_regex = re.compile('\x01(\\S+) ?(.*)\x01') |
|
27
|
|
|
|
|
28
|
|
|
def __init__(self, own_nick, line): |
|
29
|
|
|
"""own_nick is the bot's nick, needed to correctly parse sender. |
|
30
|
|
|
line is the full line from the server.""" |
|
31
|
|
|
line = line.strip('\r') |
|
32
|
|
|
self.line = line |
|
33
|
|
|
|
|
34
|
|
|
# Break off IRCv3 message tags, if present |
|
35
|
|
|
self.tags = {} |
|
36
|
|
|
if line.startswith('@'): |
|
37
|
|
|
tagstring, line = line.split(' ', 1) |
|
38
|
|
|
for tag in tagstring[1:].split(';'): |
|
39
|
|
|
tag = tag.split('=', 1) |
|
40
|
|
|
if len(tag) > 1: |
|
41
|
|
|
self.tags[tag[0]] = tag[1] |
|
42
|
|
|
else: |
|
43
|
|
|
self.tags[tag[0]] = None |
|
44
|
|
|
|
|
45
|
|
|
self.time = datetime.datetime.utcnow() |
|
46
|
|
|
if 'time' in self.tags: |
|
47
|
|
|
try: |
|
48
|
|
|
self.time = datetime.datetime.strptime(self.tags['time'], '%Y-%m-%dT%H:%M:%S.%fZ') |
|
49
|
|
|
except ValueError: |
|
50
|
|
|
pass # Server isn't conforming to spec, ignore the server-time |
|
51
|
|
|
|
|
52
|
|
|
# Grabs hostmask from line. |
|
53
|
|
|
# Example: line = ':Sopel!foo@bar PRIVMSG #sopel :foobar!' |
|
54
|
|
|
# print(hostmask) # Sopel!foo@bar |
|
55
|
|
|
# All lines start with ":" except PING. |
|
56
|
|
|
if line.startswith(':'): |
|
57
|
|
|
self.hostmask, line = line[1:].split(' ', 1) |
|
58
|
|
|
else: |
|
59
|
|
|
self.hostmask = None |
|
60
|
|
|
|
|
61
|
|
|
# Parses the line into a list of arguments. |
|
62
|
|
|
# Some events like MODE don't have a secondary string argument, i.e. no ' :' inside the line. |
|
63
|
|
|
# Example 1: line = ':nick!ident@domain PRIVMSG #sopel :foo bar!' |
|
64
|
|
|
# print(text) # 'foo bar!' |
|
65
|
|
|
# print(argstr) # ':nick!ident@domain PRIVMSG #sopel' |
|
66
|
|
|
# print(args) # [':nick!ident@domain', 'PRIVMSG', '#sopel', 'foo bar!'] |
|
67
|
|
|
# Example 2: line = 'irc.freenode.net MODE Sopel +i' |
|
68
|
|
|
# print(text) # '+i' |
|
69
|
|
|
# print(args) # ['irc.freenode.net', 'MODE', 'Sopel', '+i'] |
|
70
|
|
|
if ' :' in line: |
|
71
|
|
|
argstr, text = line.split(' :', 1) |
|
72
|
|
|
self.args = argstr.split(' ') |
|
73
|
|
|
self.args.append(text) |
|
74
|
|
|
else: |
|
75
|
|
|
self.args = line.split(' ') |
|
76
|
|
|
self.text = self.args[-1] |
|
77
|
|
|
|
|
78
|
|
|
self.event = self.args[0] |
|
79
|
|
|
self.args = self.args[1:] |
|
80
|
|
|
components = PreTrigger.component_regex.match(self.hostmask or '').groups() |
|
81
|
|
|
self.nick, self.user, self.host = components |
|
82
|
|
|
self.nick = tools.Identifier(self.nick) |
|
83
|
|
|
|
|
84
|
|
|
# If we have arguments, the first one is the sender |
|
85
|
|
|
# Unless it's a QUIT event |
|
86
|
|
|
if self.args and self.event != 'QUIT': |
|
87
|
|
|
target = tools.Identifier(self.args[0]) |
|
88
|
|
|
else: |
|
89
|
|
|
target = None |
|
90
|
|
|
|
|
91
|
|
|
# Unless we're messaging the bot directly, in which case that second |
|
92
|
|
|
# arg will be our bot's name. |
|
93
|
|
|
if target and target.lower() == own_nick.lower(): |
|
94
|
|
|
target = self.nick |
|
95
|
|
|
self.sender = target |
|
96
|
|
|
|
|
97
|
|
|
# Parse CTCP into a form consistent with IRCv3 intents |
|
98
|
|
|
if self.event == 'PRIVMSG' or self.event == 'NOTICE': |
|
99
|
|
|
intent_match = PreTrigger.intent_regex.match(self.args[-1]) |
|
100
|
|
|
if intent_match: |
|
101
|
|
|
intent, message = intent_match.groups() |
|
102
|
|
|
self.tags['intent'] = intent |
|
103
|
|
|
self.args[-1] = message or '' |
|
104
|
|
|
|
|
105
|
|
|
# Populate account from extended-join messages |
|
106
|
|
|
if self.event == 'JOIN' and len(self.args) == 3: |
|
107
|
|
|
# Account is the second arg `...JOIN #Sopel account :realname` |
|
108
|
|
|
self.tags['account'] = self.args[1] |
|
109
|
|
|
|
|
110
|
|
|
|
|
111
|
|
|
class Trigger(unicode): |
|
|
|
|
|
|
112
|
|
|
"""A line from the server, which has matched a callable's rules. |
|
113
|
|
|
|
|
114
|
|
|
Note that CTCP messages (`PRIVMSG`es and `NOTICE`es which start and end |
|
115
|
|
|
with `'\\x01'`) will have the `'\\x01'` bytes stripped, and the command |
|
116
|
|
|
(e.g. `ACTION`) placed mapped to the `'intent'` key in `Trigger.tags`. |
|
117
|
|
|
""" |
|
118
|
|
|
sender = property(lambda self: self._pretrigger.sender) |
|
119
|
|
|
"""The channel from which the message was sent. |
|
120
|
|
|
|
|
121
|
|
|
In a private message, this is the nick that sent the message.""" |
|
122
|
|
|
time = property(lambda self: self._pretrigger.time) |
|
123
|
|
|
"""A datetime object at which the message was received by the IRC server. |
|
124
|
|
|
|
|
125
|
|
|
If the server does not support server-time, then `time` will be the time |
|
126
|
|
|
that the message was received by Sopel""" |
|
127
|
|
|
raw = property(lambda self: self._pretrigger.line) |
|
128
|
|
|
"""The entire message, as sent from the server. This includes the CTCP |
|
129
|
|
|
\\x01 bytes and command, if they were included.""" |
|
130
|
|
|
is_privmsg = property(lambda self: self._is_privmsg) |
|
131
|
|
|
"""True if the trigger is from a user, False if it's from a channel.""" |
|
132
|
|
|
hostmask = property(lambda self: self._pretrigger.hostmask) |
|
133
|
|
|
"""Hostmask of the person who sent the message as <nick>!<user>@<host>""" |
|
134
|
|
|
user = property(lambda self: self._pretrigger.user) |
|
135
|
|
|
"""Local username of the person who sent the message""" |
|
136
|
|
|
nick = property(lambda self: self._pretrigger.nick) |
|
137
|
|
|
"""The :class:`sopel.tools.Identifier` of the person who sent the message. |
|
138
|
|
|
""" |
|
139
|
|
|
host = property(lambda self: self._pretrigger.host) |
|
140
|
|
|
"""The hostname of the person who sent the message""" |
|
141
|
|
|
event = property(lambda self: self._pretrigger.event) |
|
142
|
|
|
"""The IRC event (e.g. ``PRIVMSG`` or ``MODE``) which triggered the |
|
143
|
|
|
message.""" |
|
144
|
|
|
match = property(lambda self: self._match) |
|
145
|
|
|
"""The regular expression :class:`re.MatchObject` for the triggering line. |
|
146
|
|
|
""" |
|
147
|
|
|
group = property(lambda self: self._match.group) |
|
148
|
|
|
"""The ``group`` function of the ``match`` attribute. |
|
149
|
|
|
|
|
150
|
|
|
See Python :mod:`re` documentation for details.""" |
|
151
|
|
|
groups = property(lambda self: self._match.groups) |
|
152
|
|
|
"""The ``groups`` function of the ``match`` attribute. |
|
153
|
|
|
|
|
154
|
|
|
See Python :mod:`re` documentation for details.""" |
|
155
|
|
|
groupdict = property(lambda self: self._match.groupdict) |
|
156
|
|
|
"""The ``groupdict`` function of the ``match`` attribute. |
|
157
|
|
|
|
|
158
|
|
|
See Python :mod:`re` documentation for details.""" |
|
159
|
|
|
args = property(lambda self: self._pretrigger.args) |
|
160
|
|
|
""" |
|
161
|
|
|
A tuple containing each of the arguments to an event. These are the |
|
162
|
|
|
strings passed between the event name and the colon. For example, |
|
163
|
|
|
setting ``mode -m`` on the channel ``#example``, args would be |
|
164
|
|
|
``('#example', '-m')`` |
|
165
|
|
|
""" |
|
166
|
|
|
tags = property(lambda self: self._pretrigger.tags) |
|
167
|
|
|
"""A map of the IRCv3 message tags on the message.""" |
|
168
|
|
|
admin = property(lambda self: self._admin) |
|
169
|
|
|
"""True if the nick which triggered the command is one of the bot's admins. |
|
170
|
|
|
""" |
|
171
|
|
|
owner = property(lambda self: self._owner) |
|
172
|
|
|
"""True if the nick which triggered the command is the bot's owner.""" |
|
173
|
|
|
account = property(lambda self: self.tags.get('account') or self._account) |
|
174
|
|
|
"""The account name of the user sending the message. |
|
175
|
|
|
|
|
176
|
|
|
This is only available if either the account-tag or the account-notify and |
|
177
|
|
|
extended-join capabilites are available. If this isn't the case, or the user |
|
178
|
|
|
sending the message isn't logged in, this will be None. |
|
179
|
|
|
""" |
|
180
|
|
|
|
|
181
|
|
|
def __new__(cls, config, message, match, account=None): |
|
182
|
|
|
self = unicode.__new__(cls, message.args[-1] if message.args else '') |
|
|
|
|
|
|
183
|
|
|
self._account = account |
|
184
|
|
|
self._pretrigger = message |
|
185
|
|
|
self._match = match |
|
186
|
|
|
self._is_privmsg = message.sender and message.sender.is_nick() |
|
187
|
|
|
|
|
188
|
|
|
def match_host_or_nick(pattern): |
|
189
|
|
|
pattern = tools.get_hostmask_regex(pattern) |
|
190
|
|
|
return bool( |
|
191
|
|
|
pattern.match(self.nick) or |
|
192
|
|
|
pattern.match('@'.join((self.nick, self.host))) |
|
193
|
|
|
) |
|
194
|
|
|
|
|
195
|
|
|
if config.core.owner_account: |
|
196
|
|
|
self._owner = config.core.owner_account == self.account |
|
197
|
|
|
else: |
|
198
|
|
|
self._owner = match_host_or_nick(config.core.owner) |
|
199
|
|
|
self._admin = ( |
|
200
|
|
|
self._owner or |
|
201
|
|
|
self.account in config.core.admin_accounts or |
|
202
|
|
|
any(match_host_or_nick(item) for item in config.core.admins) |
|
203
|
|
|
) |
|
204
|
|
|
|
|
205
|
|
|
return self |
|
206
|
|
|
|