1# Copyright (c) Twisted Matrix Laboratories.
2# See LICENSE for details.
3
4"""
5IRC support for Instance Messenger.
6"""
7
8from zope.interface import implementer
9
10from twisted.internet import defer, protocol, reactor
11from twisted.internet.defer import succeed
12from twisted.words.im import basesupport, interfaces, locals
13from twisted.words.im.locals import ONLINE
14from twisted.words.protocols import irc
15
16
17class IRCPerson(basesupport.AbstractPerson):
18    def imperson_whois(self):
19        if self.account.client is None:
20            raise locals.OfflineError
21        self.account.client.sendLine("WHOIS %s" % self.name)
22
23    ### interface impl
24    def isOnline(self):
25        return ONLINE
26
27    def getStatus(self):
28        return ONLINE
29
30    def setStatus(self, status):
31        self.status = status
32        self.chat.getContactsList().setContactStatus(self)
33
34    def sendMessage(self, text, meta=None):
35        if self.account.client is None:
36            raise locals.OfflineError
37        for line in text.split("\n"):
38            if meta and meta.get("style", None) == "emote":
39                self.account.client.ctcpMakeQuery(self.name, [("ACTION", line)])
40            else:
41                self.account.client.msg(self.name, line)
42        return succeed(text)
43
44
45@implementer(interfaces.IGroup)
46class IRCGroup(basesupport.AbstractGroup):
47    def imgroup_testAction(self):
48        pass
49
50    def imtarget_kick(self, target):
51        if self.account.client is None:
52            raise locals.OfflineError
53        reason = "for great justice!"
54        self.account.client.sendLine(f"KICK #{self.name} {target.name} :{reason}")
55
56    ### Interface Implementation
57    def setTopic(self, topic):
58        if self.account.client is None:
59            raise locals.OfflineError
60        self.account.client.topic(self.name, topic)
61
62    def sendGroupMessage(self, text, meta={}):
63        if self.account.client is None:
64            raise locals.OfflineError
65        if meta and meta.get("style", None) == "emote":
66            self.account.client.ctcpMakeQuery(self.name, [("ACTION", text)])
67            return succeed(text)
68        # standard shmandard, clients don't support plain escaped newlines!
69        for line in text.split("\n"):
70            self.account.client.say(self.name, line)
71        return succeed(text)
72
73    def leave(self):
74        if self.account.client is None:
75            raise locals.OfflineError
76        self.account.client.leave(self.name)
77        self.account.client.getGroupConversation(self.name, 1)
78
79
80class IRCProto(basesupport.AbstractClientMixin, irc.IRCClient):
81    def __init__(self, account, chatui, logonDeferred=None):
82        basesupport.AbstractClientMixin.__init__(self, account, chatui, logonDeferred)
83        self._namreplies = {}
84        self._ingroups = {}
85        self._groups = {}
86        self._topics = {}
87
88    def getGroupConversation(self, name, hide=0):
89        name = name.lower()
90        return self.chat.getGroupConversation(
91            self.chat.getGroup(name, self), stayHidden=hide
92        )
93
94    def getPerson(self, name):
95        return self.chat.getPerson(name, self)
96
97    def connectionMade(self):
98        # XXX: Why do I duplicate code in IRCClient.register?
99        try:
100            self.performLogin = True
101            self.nickname = self.account.username
102            self.password = self.account.password
103            self.realname = "Twisted-IM user"
104
105            irc.IRCClient.connectionMade(self)
106
107            for channel in self.account.channels:
108                self.joinGroup(channel)
109            self.account._isOnline = 1
110            if self._logonDeferred is not None:
111                self._logonDeferred.callback(self)
112            self.chat.getContactsList()
113        except BaseException:
114            import traceback
115
116            traceback.print_exc()
117
118    def setNick(self, nick):
119        self.name = nick
120        self.accountName = "%s (IRC)" % nick
121        irc.IRCClient.setNick(self, nick)
122
123    def kickedFrom(self, channel, kicker, message):
124        """
125        Called when I am kicked from a channel.
126        """
127        return self.chat.getGroupConversation(self.chat.getGroup(channel[1:], self), 1)
128
129    def userKicked(self, kickee, channel, kicker, message):
130        pass
131
132    def noticed(self, username, channel, message):
133        self.privmsg(username, channel, message, {"dontAutoRespond": 1})
134
135    def privmsg(self, username, channel, message, metadata=None):
136        if metadata is None:
137            metadata = {}
138        username = username.split("!", 1)[0]
139        if username == self.name:
140            return
141        if channel[0] == "#":
142            group = channel[1:]
143            self.getGroupConversation(group).showGroupMessage(
144                username, message, metadata
145            )
146            return
147        self.chat.getConversation(self.getPerson(username)).showMessage(
148            message, metadata
149        )
150
151    def action(self, username, channel, emote):
152        username = username.split("!", 1)[0]
153        if username == self.name:
154            return
155        meta = {"style": "emote"}
156        if channel[0] == "#":
157            group = channel[1:]
158            self.getGroupConversation(group).showGroupMessage(username, emote, meta)
159            return
160        self.chat.getConversation(self.getPerson(username)).showMessage(emote, meta)
161
162    def irc_RPL_NAMREPLY(self, prefix, params):
163        """
164        RPL_NAMREPLY
165        >> NAMES #bnl
166        << :Arlington.VA.US.Undernet.Org 353 z3p = #bnl :pSwede Dan-- SkOyg AG
167        """
168        group = params[2][1:].lower()
169        users = params[3].split()
170        for ui in range(len(users)):
171            while users[ui][0] in ["@", "+"]:  # channel modes
172                users[ui] = users[ui][1:]
173        if group not in self._namreplies:
174            self._namreplies[group] = []
175        self._namreplies[group].extend(users)
176        for nickname in users:
177            try:
178                self._ingroups[nickname].append(group)
179            except BaseException:
180                self._ingroups[nickname] = [group]
181
182    def irc_RPL_ENDOFNAMES(self, prefix, params):
183        group = params[1][1:]
184        self.getGroupConversation(group).setGroupMembers(
185            self._namreplies[group.lower()]
186        )
187        del self._namreplies[group.lower()]
188
189    def irc_RPL_TOPIC(self, prefix, params):
190        self._topics[params[1][1:]] = params[2]
191
192    def irc_333(self, prefix, params):
193        group = params[1][1:]
194        self.getGroupConversation(group).setTopic(self._topics[group], params[2])
195        del self._topics[group]
196
197    def irc_TOPIC(self, prefix, params):
198        nickname = prefix.split("!")[0]
199        group = params[0][1:]
200        topic = params[1]
201        self.getGroupConversation(group).setTopic(topic, nickname)
202
203    def irc_JOIN(self, prefix, params):
204        nickname = prefix.split("!")[0]
205        group = params[0][1:].lower()
206        if nickname != self.nickname:
207            try:
208                self._ingroups[nickname].append(group)
209            except BaseException:
210                self._ingroups[nickname] = [group]
211            self.getGroupConversation(group).memberJoined(nickname)
212
213    def irc_PART(self, prefix, params):
214        nickname = prefix.split("!")[0]
215        group = params[0][1:].lower()
216        if nickname != self.nickname:
217            if group in self._ingroups[nickname]:
218                self._ingroups[nickname].remove(group)
219                self.getGroupConversation(group).memberLeft(nickname)
220
221    def irc_QUIT(self, prefix, params):
222        nickname = prefix.split("!")[0]
223        if nickname in self._ingroups:
224            for group in self._ingroups[nickname]:
225                self.getGroupConversation(group).memberLeft(nickname)
226            self._ingroups[nickname] = []
227
228    def irc_NICK(self, prefix, params):
229        fromNick = prefix.split("!")[0]
230        toNick = params[0]
231        if fromNick not in self._ingroups:
232            return
233        for group in self._ingroups[fromNick]:
234            self.getGroupConversation(group).memberChangedNick(fromNick, toNick)
235        self._ingroups[toNick] = self._ingroups[fromNick]
236        del self._ingroups[fromNick]
237
238    def irc_unknown(self, prefix, command, params):
239        pass
240
241    # GTKIM calls
242    def joinGroup(self, name):
243        self.join(name)
244        self.getGroupConversation(name)
245
246
247@implementer(interfaces.IAccount)
248class IRCAccount(basesupport.AbstractAccount):
249    gatewayType = "IRC"
250
251    _groupFactory = IRCGroup
252    _personFactory = IRCPerson
253
254    def __init__(
255        self, accountName, autoLogin, username, password, host, port, channels=""
256    ):
257        basesupport.AbstractAccount.__init__(
258            self, accountName, autoLogin, username, password, host, port
259        )
260        self.channels = [channel.strip() for channel in channels.split(",")]
261        if self.channels == [""]:
262            self.channels = []
263
264    def _startLogOn(self, chatui):
265        logonDeferred = defer.Deferred()
266        cc = protocol.ClientCreator(reactor, IRCProto, self, chatui, logonDeferred)
267        d = cc.connectTCP(self.host, self.port)
268        d.addErrback(logonDeferred.errback)
269        return logonDeferred
270
271    def logOff(self):
272        # IAccount.logOff
273        pass
274