Welcome to mirror list, hosted at ThFree Co, Russian Federation.

groupchats.py « extensions - github.com/mrDoctorWho/vk4xmpp.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: f183891b1a87ec8c7e8d42b497a9713f1b18bcd3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# coding: utf-8
# This file is a part of VK4XMPP transport
# © simpleApps, 2013 — 2015.
# File contains parts of code from 
# BlackSmith mark.1 XMPP Bot, © simpleApps 2011 — 2014.

if not require("attachments") or not require("forwarded_messages"):
	raise AssertionError("'groupchats' requires 'forwarded_messages'")

try:
	import mod_xhtml
except ImportError:
	mod_xhtml = None


def sendIQ(chat, attr, data, afrls, role, jidFrom, cb=None, args={}):
	stanza = xmpp.Iq("set", to=chat, frm=jidFrom)
	query = xmpp.Node("query", {"xmlns": xmpp.NS_MUC_ADMIN})
	arole = query.addChild("item", {attr: data, afrls: role})
	stanza.addChild(node = query)
	sender(Component, stanza, cb, args)


def makeMember(chat, jid, jidFrom, cb=None, args={}):
	sendIQ(chat, "jid", jid, "affiliation", "member", jidFrom, cb, args)


def inviteUser(chat, jidTo, jidFrom, name):
	invite = xmpp.Message(to=chat, frm=jidFrom)
	x = xmpp.Node("x", {"xmlns": xmpp.NS_MUC_USER})
	inv = x.addChild("invite", {"to": jidTo})
	inv.setTagData("reason", _("You're invited by user «%s»") % name)
	invite.addChild(node=x)
	sender(Component, invite)


def joinChat(chat, name, jidFrom):
	prs = xmpp.Presence("%s/%s" % (chat, name), frm=jidFrom)
	prs.setTag("c", {"node": "http://simpleapps.ru/caps/vk4xmpp", "ver": Revision}, xmpp.NS_CAPS)
	sender(Component, prs)


def leaveChat(chat, jidFrom):
	prs = xmpp.Presence(chat, "unavailable", frm=jidFrom)
	sender(Component, prs)


def chatMessage(chat, text, jidFrom, subj=None, timestamp=0):
	message = xmpp.Message(chat, typ="groupchat")
	if timestamp:
		timestamp = time.gmtime(timestamp)
		message.setTimestamp(time.strftime("%Y%m%dT%H:%M:%S", timestamp))
	if not subj:
		message.setBody(text)
	else:
		message.setSubject(text)
	message.setFrom(jidFrom)
	sender(Component, message)


def outgoingChatMessageHandler(self, vkChat):
	"""
	Handles outging messages (VK) and sends them to XMPP
	"""
	if not self.settings.groupchats:
		return None
	if vkChat.has_key("chat_id"):
		owner = vkChat.get("admin_id", "1")
		fromID = vkChat["uid"]
		chatID = vkChat["chat_id"]
		chatJID = "%s_chat#%s@%s" % (self.vk.userID, chatID, ConferenceServer)

		if not hasattr(self, "chats"):
			self.chats = {}

		if not self.vk.userID:
			logger.warning("groupchats: we didn't receive user id, trying again after 10 seconds (jid: %s)" % self.source)
			self.vk.getUserID()
			runThread(outgoingChatMessageHandler, (self, vkChat), delay=10)
			return None

		if chatJID not in self.chats:
			chat = self.chats[chatJID] = Chat(owner, chatID, chatJID, vkChat["title"], vkChat["date"], vkChat["chat_active"].split(","))
			chat.create(self)
		else:
			chat = self.chats[chatJID]

		## joining new people and make the old ones leave
		chat.update(self, vkChat)

		body = escape("", uHTML(vkChat["body"]))
		body += parseAttachments(self, vkChat)
		body += parseForwardedMessages(self, vkChat)
		if body:
			chatMessage(chatJID, body, vk2xmpp(fromID), None, vkChat["date"])
		return None
	return ""


class Chat(object):
	"""
	Class used for Chat handling
	"""
	def __init__(self, owner, id, jid, topic, date, users=[]):
		self.id = id
		self.jid = jid
		self.owner = owner
		self.users = {} ## jabber users
		self.raw_users = users ## vk ids (users)
		self.created = False
		self.invited = False
		self.topic = topic
		self.creation_date = date

	def create(self, user):	
		"""
		Creates a chat, join it and set the config
		"""
		logger.debug("groupchats: creating %s. Users: %s; owner: %s (jid: %s)" % (self.jid, self.raw_users, self.owner, user.source))
		name = user.vk.getUserData(self.owner)["name"]
		self.users[TransportID] = {"name": name, "jid": TransportID}
		joinChat(self.jid, name, TransportID) ## we're joining to chat with the room owner's name to set the topic. That's why we have so many lines of code right here
		self.setConfig(self.jid, TransportID, False, self.onConfigSet, {"user": user}) ## executehandler?

	def initialize(self, user, chat):
		"""
		Initializes chat object: 
			1) requests users list if required
			2) makes them members
			3) invites the user 
			4) sets the chat topic
		Parameters:
			chat: chat's jid
		"""
		if not self.users:
			vkChat = self.getVKChat(user, self.id)
			if vkChat:
				vkChat = vkChat[0]
			elif not self.invited:
				logger.error("groupchats: damn vk didn't answer to chat list request, starting timer to try again (jid: %s)" % user.source)
				runThread(self.initialize, (user, chat), delay=10)
				return False
			self.raw_users = vkChat.get("users")

		name = "@%s" % TransportID
		makeMember(chat, user.source, TransportID)
		if not self.invited:
			inviteUser(chat, user.source, TransportID, user.vk.getUserData(self.owner)["name"])
			self.invited = True
		logger.debug("groupchats: user has been invited to chat %s (jid: %s)" % (chat, user.source))
		chatMessage(chat, self.topic, TransportID, True, self.creation_date)
		joinChat(chat, name, TransportID) ## let's rename ourself
		self.users[TransportID] = {"name": name, "jid": TransportID}

	def update(self, userObject, vkChat):
		"""
		Updates chat users and sends messages
		Uses two users list to prevent losing anyone
		"""
		users = vkChat["chat_active"].split(",") or []
		all_users = users
		if userObject.settings.show_all_chat_users:
			users = self.getVKChat(userObject, self.id)
			if users:
				all_users = users.get("users", [])

		for user in all_users:
			## checking if there new users we didn't join yet
			if not user in self.raw_users:
				logger.debug("groupchats: user %s has joined the chat %s (jid: %s)" % (user, self.jid, userObject.source))
				jid = vk2xmpp(user)
				name = userObject.vk.getUserData(user)["name"]
				self.users[user] = {"name": name, "jid": jid}
				makeMember(self.jid, jid, TransportID)
				joinChat(self.jid, name, jid) 

		for user in self.users.keys():
			## checking if there old users we have to remove
			if not user in all_users and user != TransportID:
				logger.debug("groupchats: user %s has left the chat %s (jid: %s)" % (user, self.jid, userObject.source))
				del self.users[user]
				leaveChat(self.jid, vk2xmpp(user))
				if user == userObject.vk.userID:
					self.setConfig(self.jid, TransportID, exterminate=True) ## exterminate the chats when user leave conference or just go offline?

		topic = vkChat["title"]
		if topic != self.topic:
			chatMessage(self.jid, topic, TransportID, True)
			self.topic = topic
		self.raw_users = all_users

	@classmethod
	def setConfig(cls, chat, jidFrom, exterminate=False, cb=None, args={}):
		"""
		Sets the chat config
		"""
		iq = xmpp.Iq("set", to=chat, frm=jidFrom)
		query = iq.addChild("query", namespace=xmpp.NS_MUC_OWNER)
		if exterminate:
			query.addChild("destroy")
		else:
			form = utils.buildDataForm(fields = [{"var": "FORM_TYPE", "type": "hidden", "value": xmpp.NS_MUC_ROOMCONFIG},
											 {"var": "muc#roomconfig_membersonly", "type": "boolean", "value": "1"},
											 {"var": "muc#roomconfig_publicroom", "type": "boolean", "value": "0"},
											 {"var": "muc#roomconfig_whois", "value": "anyone"}], type="submit")
			query.addChild(node=form)
		sender(Component, iq, cb, args)

	def onConfigSet(self, cl, stanza, user):
		"""
		Called when the chat config has been set
		"""
		chat = stanza.getFrom().getStripped()
		if xmpp.isResultNode(stanza):
			logger.debug("groupchats: stanza \"result\" received from %s, continuing initialization (jid: %s)" % (chat, user.source))
			execute(self.initialize, (user, chat)) ## i don't trust VK so it's better to execute it
			self.created = True
		else:
			logger.error("groupchats: couldn't set room %s config (jid: %s)" % (chat, user.source))

	@classmethod
	def getVKChat(cls, user, id):
		"""
		Get vk chat by id
		"""
		return user.vk.method("messages.getChat", {"chat_id": id}) or []

	@classmethod
	def getParts(cls, source):
		"""
		Split the source and returns required parts
		"""
		node, domain = source.split("@")
		creator, id = node.split("_chat#")
		return (int(creator), int(id), domain)

	@classmethod
	def getUserObject(cls, source):
		"""
		Gets user object by chat jid
		"""
		user = None
		creator, id, domain = cls.getParts(source)
		if domain == ConferenceServer and creator: ## we will ignore zero-id
			jid = jidToID[creator]
			if jid in Transport:
				user = Transport[jid]
		return user


def incomingChatMessageHandler(msg):
	"""
	Handles incoming (xmpp) messages and sends them to VK
	"""
	if msg.getType() == "groupchat":
		body = msg.getBody()
		destination = msg.getTo().getStripped()
		source = msg.getFrom().getStripped()
		if mod_xhtml:
			html = msg.getTag("html")
		else:
			html = None

		x = msg.getTag("x", {"xmlns": "http://jabber.org/protocol/muc#user"})
		if x and x.getTagAttr("status", "code") == "100":
			raise xmpp.NodeProcessed()

		if not msg.getTimestamp() and body and destination == TransportID:
			user = Chat.getUserObject(source)
			creator, id, domain = Chat.getParts(source)
			if user:
				if html and html.getTag("body"): ## XHTML-IM!
					logger.debug("groupchats: fetched xhtml image (jid: %s)" % source)
					try:
						xhtml = mod_xhtml.parseXHTML(user, html, source, source, "chat_id")
					except Exception:
						xhtml = False
					if xhtml:
						raise xmpp.NodeProcessed()
				user.vk.sendMessage(body, id, "chat_id")


def handleChatErrors(source, prs):
	"""
	Handles error presences
	"""
	## todo: leave on 401, 403, 405
	## and rejoin timer on 404, 503
	## is it safe by the way?
	destination = prs.getTo().getStripped()
	if prs.getType() == "error":
		error = prs.getErrorCode()
		status = prs.getStatusCode()
		nick = prs.getFrom().getResource()
		if source.split("@")[1] == ConferenceServer:
			user = Chat.getUserObject(source)
			if user and source in getattr(user, "chats", {}):
				chat = user.chats[source]
				if error == "409":
					id = vk2xmpp(destination)
					if id in chat.users:
						nick += "."
						if not chat.created and id == TransportID:
							chat.users[id]["name"] = nick
							chat.create(user)
						else:
							joinChat(source, nick, destination)
		logger.debug("groupchats: presence error (error #%s, status #%s) from source %s" % (error, status, source))
	# TODO:
	## Make user leave if he left when transport's user wasn't online
	## This could be done using jids or/and nicks lists. Completely unreliably as well as the groupchats realization itself
#	if prs.getStatusCode() == "110":
#		print prs.getJid()
#		print prs.getType()


def exterminateChat(user):
	"""
	Calls a Dalek for exterminate the chat
	"""
	chats = user.vk.method("execute.getChats")
	for chat in chats:
		Chat.setConfig("%s_chat#%s@%s" % (user.vk.userID, chat["chat_id"], ConferenceServer), TransportID, True)


if ConferenceServer:
	GLOBAL_USER_SETTINGS["show_all_chat_users"] = {"label": "Show all chat users (only active ones by default)", "value": 0}
	logger.info("extension groupchats is loaded")
	TransportFeatures.append(xmpp.NS_GROUPCHAT)
	registerHandler("msg01", outgoingChatMessageHandler)
	registerHandler("msg02", incomingChatMessageHandler)
	registerHandler("prs01", handleChatErrors)
	registerHandler("evt03", exterminateChat)

else:
	del sendIQ, makeMember, inviteUser, joinChat, leaveChat, \
		outgoingChatMessageHandler, chatMessage, Chat, \
		incomingChatMessageHandler, handleChatErrors