Chat-O-Matic/protocols/xmpp/JabberHandler.cpp
Jaidyn Ann 7c9d1d9eaa Rework communication of room metadata, add room flags
In the API, a firm distinction between initially fetching a room's
metadata and receiving further changes has been made:
IM_ROOM_METADATA will be sent to Caya on request (IM_GET_ROOM_METADATA)
with the room's name, subject, etc. The other metadata-related messages
should only be sent to Caya after the room is initialized, not as a
means of initially setting its metadata.

Basic room flags were added, though they aren't yet used― they should
allow the protocol/add-on/user to configure some room features. Logging,
auto-joining, etc.
2021-06-13 01:16:30 -05:00

1763 lines
45 KiB
C++

/*
* Copyright 2021, Jaidyn Levesque. All rights reserved.
* Copyright 2010, Pier Luigi Fiorini. All rights reserved.
* Distributed under the terms of the GPL v2 License.
*
* Authors:
* Pier Luigi Fiorini, pierluigi.fiorini@gmail.com
* Jaidyn Levesque, jadedctrl@teknik.io
*/
#include <Directory.h>
#include <Entry.h>
#include <File.h>
#include <FindDirectory.h>
#include <List.h>
#include <StringList.h>
#include <libsupport/SHA1.h>
#include <CayaProtocolMessages.h>
#include <Role.h>
#include <RoomFlags.h>
#include <gloox/chatstatefilter.h>
#include <gloox/messageeventfilter.h>
#include <gloox/mucroom.h>
#include "JabberHandler.h"
static status_t
connect_thread(void* data)
{
JabberHandler* handler = (JabberHandler*)data;
if (!handler)
return B_BAD_VALUE;
gloox::Client* client = handler->Client();
if (!client)
return B_BAD_VALUE;
gloox::ConnectionError e;
while ((e = client->recv(10000000)) == gloox::ConnNoError);
if (e != gloox::ConnUserDisconnected)
handler->HandleConnectionError(e);
return B_OK;
}
JabberHandler::JabberHandler()
:
fClient(NULL),
fVCardManager(NULL),
fSession(NULL)
{
fAvatars = new BList();
}
JabberHandler::~JabberHandler()
{
Shutdown();
BString* item = NULL;
for (int32 i = 0; (item = (BString*)fAvatars->ItemAt(i)); i++)
delete item;
delete fAvatars;
}
status_t
JabberHandler::Init(CayaProtocolMessengerInterface* messenger)
{
fServerMessenger = messenger;
return B_OK;
}
status_t
JabberHandler::Process(BMessage* msg)
{
if (msg->what != IM_MESSAGE)
return B_ERROR;
int32 im_what = 0;
msg->FindInt32("im_what", &im_what);
switch (im_what) {
case IM_SET_OWN_STATUS: {
int32 status = msg->FindInt32("status");
BString status_msg = msg->FindString("message");
switch (status) {
case CAYA_ONLINE:
// Log in if we still need to
resume_thread(fRecvThread);
break;
case CAYA_OFFLINE:
kill_thread(fRecvThread);
break;
default:
break;
}
break;
}
case IM_SEND_MESSAGE: {
const char* id = msg->FindString("chat_id");
const char* subject = msg->FindString("subject");
const char* body = msg->FindString("body");
gloox::MUCRoom* room = fRooms.ValueFor(id);
if (!id || !body)
return B_ERROR;
// Send JabberHandler message
gloox::Message jm(gloox::Message::Chat, gloox::JID(id),
body, (subject ? subject : gloox::EmptyString));
if (room != NULL) {
room->send(body);
break;
}
else
fClient->send(jm);
// If non-MUC, tell Caya we actually sent the message
// (An MUC should echo the message back to us later, see
// handleMUCMessage)
_MessageSent(id, subject, body);
break;
}
case IM_CREATE_CHAT: {
const char* invite_id = msg->FindString("user_id");
// TODO: Contact validation, make sure permssion is granted
if (!invite_id)
return B_ERROR;
_ChatCreatedMsg(invite_id);
break;
}
case IM_JOIN_ROOM: {
BString chat_id;
if (msg->FindString("chat_id", &chat_id) == B_OK)
_JoinRoom(chat_id.String());
break;
}
case IM_LEAVE_ROOM: {
BString chat_id = msg->FindString("chat_id");
gloox::MUCRoom* room = fRooms.ValueFor(chat_id);
// MUCs are special, one-on-ones we can just drop
if (room != NULL)
room->leave();
// We've gotta let Caya know!
BMessage left(IM_MESSAGE);
left.AddInt32("im_what", IM_ROOM_LEFT);
left.AddString("chat_id", chat_id);
_SendMessage(&left);
break;
}
case IM_ROOM_INVITE_ACCEPT: {
BString chat_id;
if (msg->FindString("chat_id", &chat_id) != B_OK)
break;
BStringList splitAtPassword;
chat_id.Split("#", false, splitAtPassword);
chat_id = splitAtPassword.StringAt(0);
_JoinRoom(chat_id.String());
break;
}
case IM_ROOM_SEND_INVITE: {
BString chat_id = msg->FindString("chat_id");
gloox::MUCRoom* room = fRooms.ValueFor(chat_id);
BString user_id;
if (room == NULL || msg->FindString("user_id", &user_id) != B_OK)
break;
room->invite(gloox::JID(user_id.String()), "");
break;
}
case IM_GET_ROOM_PARTICIPANTS: {
BString chat_id = msg->FindString("chat_id");
gloox::MUCRoom* room = fRooms.ValueFor(chat_id);
if (room != NULL)
room->getRoomItems();
else if (fUserChats.HasString(chat_id) == true) {
BMessage users(IM_MESSAGE);
users.AddInt32("im_what", IM_ROOM_PARTICIPANTS);
users.AddString("user_id", chat_id);
}
break;
}
case IM_GET_ROOM_METADATA: {
BString chat_id = msg->FindString("chat_id");
gloox::MUCRoom* room = fRooms.ValueFor(chat_id);
if (room != NULL)
room->getRoomInfo();
else if (fUserChats.HasString(chat_id) == true)
{
BMessage metadata(IM_MESSAGE);
metadata.AddInt32("im_what", IM_ROOM_METADATA);
metadata.AddString("chat_id", chat_id);
metadata.AddInt32("room_default_flags",
0 | ROOM_AUTOCREATE | ROOM_LOG_LOCALLY | ROOM_POPULATE_LOGS);
metadata.AddInt32("room_disallowed_flags", 0 | ROOM_AUTOJOIN);
}
break;
}
case IM_ROOM_KICK_PARTICIPANT:
case IM_ROOM_BAN_PARTICIPANT:
case IM_ROOM_UNBAN_PARTICIPANT:
case IM_ROOM_MUTE_PARTICIPANT:
case IM_ROOM_UNMUTE_PARTICIPANT:
_MUCModeration(msg);
break;
default:
return B_ERROR;
}
return B_OK;
}
status_t
JabberHandler::Shutdown()
{
if (fVCardManager)
fVCardManager->cancelVCardOperations(this);
if (fClient)
fClient->disposeMessageSession(fSession);
kill_thread(fRecvThread);
return B_OK;
}
void
JabberHandler::SetAddOnPath(BPath path)
{
fPath = path;
}
BPath
JabberHandler::AddOnPath()
{
return fPath;
}
void
JabberHandler::SetName(const char* name)
{
fName = name;
}
const char*
JabberHandler::GetName()
{
return fName.String();
}
status_t
JabberHandler::UpdateSettings(BMessage* msg)
{
const char* username = msg->FindString("username");
const char* password = msg->FindString("password");
const char* server = msg->FindString("server");
const char* resource = msg->FindString("resource");
// Sanity check
if (!username || !password || !resource)
return B_ERROR;
// Store settings
fUsername = username;
fNick = username;
fPassword = password;
fServer = server;
fResource = resource;
fPort = 5222;
// Give the add-on a change to override settings
OverrideSettings();
// Compose JID
BString jid = ComposeJID();
fJid.setJID(jid.String());
// Register our client
fClient = new gloox::Client(fJid, fPassword.String());
fClient->setServer(fServer.String());
if (fPort > 0)
fClient->setPort(fPort);
fClient->registerConnectionListener(this);
fClient->registerMessageSessionHandler(this);
fClient->registerMUCInvitationHandler(new InviteHandler(fClient, this));
fClient->rosterManager()->registerRosterListener(this);
fClient->disco()->setVersion("Caya", VERSION);
fClient->disco()->setIdentity("client", "caya");
fClient->logInstance().registerLogHandler(gloox::LogLevelDebug,
gloox::LogAreaAll, this);
// Register vCard manager
fVCardManager = new gloox::VCardManager(fClient);
// Connect to the server
fClient->connect(false);
// Read data from another thread
fRecvThread = spawn_thread(connect_thread, "connect_thread",
B_NORMAL_PRIORITY, (void*)this);
if (fRecvThread < B_OK)
return B_ERROR;
return B_OK;
}
uint32
JabberHandler::GetEncoding()
{
return 0xffff;
}
CayaProtocolMessengerInterface*
JabberHandler::MessengerInterface() const
{
return fServerMessenger;
}
gloox::Client*
JabberHandler::Client() const
{
return fClient;
}
void
JabberHandler::HandleConnectionError(gloox::ConnectionError& e)
{
// Handle error
BMessage errMsg(IM_ERROR);
switch (e) {
case gloox::ConnStreamError:
{
gloox::StreamError streamError = fClient->streamError();
errMsg.AddString("error", "Bad or malformed XML stream.");
switch (streamError) {
case gloox::StreamErrorBadFormat:
errMsg.AddString("detail", "The entity has sent XML that "
"cannot be processed");
break;
case gloox::StreamErrorBadNamespacePrefix:
errMsg.AddString("detail", "The entity has sent a namespace "
"prefix which is not supported, or has sent no namespace "
"prefix on an element that requires such prefix.");
break;
case gloox::StreamErrorConflict:
errMsg.AddString("detail", "The server is closing the active "
"stream for this entity because a new stream has been "
"initiated that conflicts with the existing stream.");
break;
case gloox::StreamErrorConnectionTimeout:
errMsg.AddString("detail", "The entity has not generated any "
"traffic over the stream for some period of time.");
break;
case gloox::StreamErrorHostGone:
errMsg.AddString("detail", "The host initially used corresponds "
"to a hostname that is no longer hosted by the server.");
break;
case gloox::StreamErrorHostUnknown:
errMsg.AddString("detail", "The host initially used does not "
"correspond to a hostname that is hosted by the server.");
break;
case gloox::StreamErrorImproperAddressing:
errMsg.AddString("detail", "A stanza sent between two servers "
"lacks a 'to' or 'from' attribute (or the attribute has no "
"value.");
break;
case gloox::StreamErrorInternalServerError:
errMsg.AddString("detail", "The Server has experienced a "
"misconfiguration or an otherwise-undefined internal error "
"that prevents it from servicing the stream.");
break;
case gloox::StreamErrorInvalidFrom:
errMsg.AddString("detail", "The JID or hostname provided in a "
"'from' address does not match an authorized JID or validated "
"domain negotiation between servers via SASL or dialback, or "
"between a client and a server via authentication and resource "
"binding.");
break;
case gloox::StreamErrorInvalidId:
errMsg.AddString("detail", "The stream ID or dialback ID is invalid "
"or does not match and ID previously provdided.");
break;
case gloox::StreamErrorInvalidNamespace:
errMsg.AddString("detail", "The streams namespace name is something "
"other than \"http://etherx.jabber.org/streams\" or the dialback "
"namespace name is something other than \"jabber:server:dialback\".");
break;
case gloox::StreamErrorInvalidXml:
errMsg.AddString("detail", "The entity has sent invalid XML over the "
"stream to a server that performs validation.");
break;
case gloox::StreamErrorNotAuthorized:
errMsg.AddString("detail", "The entity has attempted to send data before "
"the stream has been authenticated, or otherwise is not authorized to "
"perform an action related to stream negotiation; the receiving entity "
"must not process the offending stanza before sending the stream error.");
break;
case gloox::StreamErrorPolicyViolation:
errMsg.AddString("detail", "The entity has violated some local service "
"policy; the server may choose to specify the policy in the <text/> "
"element or an application-specific condition element.");
break;
case gloox::StreamErrorRemoteConnectionFailed:
errMsg.AddString("detail", "The server is unable to properly connect to "
"a remote entit that is required for authentication.");
break;
case gloox::StreamErrorResourceConstraint:
errMsg.AddString("detail", "The server lacks the system resources necessary "
"to service the stream.");
break;
case gloox::StreamErrorRestrictedXml:
errMsg.AddString("detail", "The entity has attempted to send restricted XML "
"features such as a comment, processing instruction, DTD, entity reference "
"or unescaped character.");
break;
case gloox::StreamErrorSeeOtherHost:
errMsg.AddString("detail", "The server will not provide service to the initiating "
"entity but is redirecting traffic to another host; the server should specify "
"the alternate hostname or IP address (which MUST be a valid domain identifier) "
"as the XML characted data of the <see-other-host/> element.");
break;
case gloox::StreamErrorSystemShutdown:
errMsg.AddString("detail", "The server is being shut down and all active streams "
"are being closed.");
break;
case gloox::StreamErrorUndefinedCondition:
errMsg.AddString("detail", "The error condition is not one of those defined by the "
"other condition in this list; this error condition should be used only in "
"conjunction with an application-specific condition.");
break;
case gloox::StreamErrorUnsupportedEncoding:
errMsg.AddString("detail", "The initiating entity has encoded the stream in an "
"encoding that is not supported by the server.");
break;
case gloox::StreamErrorUnsupportedStanzaType:
errMsg.AddString("detail", "The initiating entity has sent a first-level child "
"of the stream that is not supported by the server.");
break;
case gloox::StreamErrorUnsupportedVersion:
errMsg.AddString("detail", "The value of the 'version' attribute provided by the "
"initiating entity in the stream header specifies a version of XMPP that is not "
"supported by the server; the server may specify the version(s) it supports in "
"the <text/> element.");
break;
case gloox::StreamErrorXmlNotWellFormed:
errMsg.AddString("detail", "The initiating entity has sent XML that is not "
"well-formed as defined by XML.");
break;
default:
break;
}
break;
}
case gloox::ConnStreamVersionError:
errMsg.AddString("detail", "The incoming stream's version is not "
"supported.");
break;
case gloox::ConnStreamClosed:
errMsg.AddString("detail", "The stream has been closed by the server.");
break;
case gloox::ConnProxyAuthRequired:
errMsg.AddString("detail", "The HTTP/SOCKS5 proxy requires authentication.");
break;
case gloox::ConnProxyAuthFailed:
errMsg.AddString("detail", "HTTP/SOCKS5 proxy authentication failed.");
break;
case gloox::ConnProxyNoSupportedAuth:
errMsg.AddString("detail", "The HTTP/SOCKS5 proxy requires an unsupported "
"authentication mechanism.");
break;
case gloox::ConnIoError:
errMsg.AddString("detail", "Input/output error.");
break;
case gloox::ConnParseError:
errMsg.AddString("detail", "A XML parse error occurred.");
break;
case gloox::ConnConnectionRefused:
errMsg.AddString("detail", "The connection was refused by the server "
"on the socket level.");
break;
case gloox::ConnDnsError:
errMsg.AddString("detail", "Server's hostname resolution failed.");
break;
case gloox::ConnOutOfMemory:
errMsg.AddString("detail", "Out of memory.");
break;
case gloox::ConnTlsFailed:
errMsg.AddString("detail", "The server's certificate could not be verified or "
"the TLS handshake did not complete successfully.");
break;
case gloox::ConnTlsNotAvailable:
errMsg.AddString("detail", "The server didn't offer TLS while it was set to be "
"required, or TLS was not compiled in.");
break;
case gloox::ConnCompressionFailed:
errMsg.AddString("detail", "Negotiating or initializing compression failed.");
break;
case gloox::ConnAuthenticationFailed:
{
gloox::AuthenticationError authError = fClient->authError();
errMsg.AddString("error", "Authentication failed. Username or password wrong "
"or account does not exist.");
switch (authError) {
case gloox::SaslAborted:
errMsg.AddString("detail", "The receiving entity acknowledges an <abort/> "
"element sent by initiating entity; sent in reply to the <abort/> "
"element.");
break;
case gloox::SaslIncorrectEncoding:
errMsg.AddString("detail", "The data provided by the initiating entity "
"could not be processed because the base64 encoding is incorrect.");
break;
case gloox::SaslInvalidAuthzid:
errMsg.AddString("detail", "The authid provided by the initiating entity "
"is invalid, either because it is incorrectly formatted or because "
"the initiating entity does not have permissions to authorize that ID; "
"sent in reply to a <response/> element or an <auth/> element with "
"initial response data.");
break;
case gloox::SaslInvalidMechanism:
errMsg.AddString("detail", "The initiating element did not provide a "
"mechanism or requested a mechanism that is not supported by the "
"receiving entity; sent in reply to an <auth/> element.");
break;
case gloox::SaslMalformedRequest:
errMsg.AddString("detail", "The request is malformed (e.g., the <auth/> "
"element includes an initial response but the mechanism does not "
"allow that); sent in reply to an <abort/>, <auth/>, <challenge/>, or "
"<response/> element.");
break;
case gloox::SaslMechanismTooWeak:
errMsg.AddString("detail", "The mechanism requested by the initiating entity "
"is weaker than server policy permits for that initiating entity; sent in "
"reply to a <response/> element or an <auth/> element with initial "
"response data.");
break;
case gloox::SaslNotAuthorized:
errMsg.AddString("detail", "The authentication failed because the initiating "
"entity did not provide valid credentials (this includes but is not "
"limited to the case of an unknown username); sent in reply to a "
"<response/> element or an <auth/> element with initial response data.");
break;
case gloox::SaslTemporaryAuthFailure:
errMsg.AddString("detail", "The authentication failed because of a temporary "
"error condition within the receiving entity; sent in reply to an "
"<auth/> element or <response/> element.");
break;
case gloox::NonSaslConflict:
errMsg.AddString("detail", "Resource conflict, see XEP-0078.");
break;
case gloox::NonSaslNotAcceptable:
errMsg.AddString("detail", "Required information not provided, "
"see XEP-0078.");
break;
case gloox::NonSaslNotAuthorized:
errMsg.AddString("detail", "Incorrect credentials.");
break;
default:
break;
}
break;
}
case gloox::ConnNotConnected:
errMsg.AddString("error", "There is no active connection.");
break;
default:
break;
}
_SendMessage(&errMsg);
}
void
JabberHandler::HandleStanzaError(gloox::StanzaError error)
{
BMessage errMsg(IM_ERROR);
errMsg.AddString("error", "Stanza-related error");
BString detail;
switch (error)
{
case gloox::StanzaErrorBadRequest:
detail = "The sender has sent XML that is malformed or that cannot "
"be processed.";
break;
case gloox::StanzaErrorConflict:
detail = "Access cannot be granted because an existing resource or "
"session exists with the same name or address.";
break;
case gloox::StanzaErrorFeatureNotImplemented:
detail = "This feature hasn't been implemented by the recipient or "
"by the server.";
break;
case gloox::StanzaErrorForbidden:
detail = "You don't have permssion to do this.";
break;
case gloox::StanzaErrorGone:
detail = "The recipient or server can no longer be contacted at "
"this address. Try again later, or with a different address.";
break;
case gloox::StanzaErrorInternalServerError:
detail = "The server could not process the stanza because of a "
"misconfiguration or an otherwise-undefined internal server error.";
break;
case gloox::StanzaErrorItemNotFound:
detail = "The addressed JID or item requested cannot be found.";
break;
case gloox::StanzaErrorJidMalformed:
detail = "An invalid XMPP address or identifier was given. If you "
"can, please try a different one.";
break;
case gloox::StanzaErrorNotAcceptable:
detail = "The server or user refuses to accept this, because some "
"criteria hasn't been met (e.g., a local policy regarding "
"acceptable words in messages).";
break;
case gloox::StanzaErrorNotAllowed:
detail = "You aren't allowed to do this by the server or recepient.";
break;
case gloox::StanzaErrorNotAuthorized:
detail = "You need to be properily authenticated before doing this.";
case gloox::StanzaErrorNotModified:
detail = "The item requested has not changed since it was last "
"requested.";
case gloox::StanzaErrorPaymentRequired:
detail = "The server refuses to offer service, because payment is "
"required.";
break;
case gloox::StanzaErrorRecipientUnavailable:
detail = "The recipient is temporarily unavailable.";
break;
case gloox::StanzaErrorRedirect:
detail = "The recipient or server is redirecting requests for this "
"information to another entity, usually temporarily.";
break;
case gloox::StanzaErrorRegistrationRequired:
detail = "You can't do this before registration! Be sure your "
"finished registering for your account.";
break;
case gloox::StanzaErrorRemoteServerNotFound:
detail = "That user's server doesn't exist.";
break;
case gloox::StanzaErrorRemoteServerTimeout:
detail = "Connection to that user's server has timed out.";
break;
case gloox::StanzaErrorResourceConstraint:
detail = "The server or recipient are too busy right now; try "
"again later.";
break;
case gloox::StanzaErrorServiceUnavailable:
detail = "The server or recipient don't provide this service.";
break;
case gloox::StanzaErrorSubscribtionRequired:
detail = "You can't access this unless you are subscribed.";
break;
}
errMsg.AddString("detail", detail);
_SendMessage(&errMsg);
}
void
JabberHandler::_SendMessage(BMessage* msg)
{
// Skip invalid messages
if (!msg)
return;
msg->AddString("protocol", Signature());
fServerMessenger->SendMessage(msg);
}
void
JabberHandler::_Notify(notification_type type, const char* title, const char* message)
{
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_NOTIFICATION);
msg.AddInt32("type", (int32)type);
msg.AddString("title", title);
msg.AddString("message", message);
_SendMessage(&msg);
}
void
JabberHandler::_NotifyProgress(const char* title, const char* message, float progress)
{
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_PROGRESS);
msg.AddString("title", title);
msg.AddString("message", message);
msg.AddFloat("progress", progress);
_SendMessage(&msg);
}
void
JabberHandler::_MessageSent(const char* id, const char* subject,
const char* body)
{
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_MESSAGE_SENT);
msg.AddString("user_id", fJid.bare().c_str());
msg.AddString("chat_id", id);
msg.AddString("subject", subject);
msg.AddString("body", body);
_SendMessage(&msg);
}
void
JabberHandler::_ChatCreatedMsg(const char* id)
{
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_CHAT_CREATED);
msg.AddString("chat_id", id);
msg.AddString("user_id", id);
_SendMessage(&msg);
}
void
JabberHandler::_RoleChangedMsg(BString chat_id, BString user_id,
gloox::MUCRoomRole role, gloox::MUCRoomAffiliation aff)
{
BMessage roleMsg(IM_MESSAGE);
roleMsg.AddInt32("im_what", IM_ROOM_ROLECHANGED);
roleMsg.AddString("user_id", user_id);
roleMsg.AddString("chat_id", chat_id);
roleMsg.AddString("role_title", _RoleTitle(role, aff));
roleMsg.AddInt32("role_perms", _RolePerms(role, aff));
roleMsg.AddInt32("role_priority", _RolePriority(role, aff));
_SendMessage(&roleMsg);
}
void
JabberHandler::_UserLeftMsg(BString chat_id, gloox::MUCRoomParticipant participant)
{
BString user_id;
const char* nick = participant.nick->resource().c_str();
bool isSelf = _MUCUserId(chat_id, nick, &user_id);
if (user_id.IsEmpty() == true)
return;
int32 im_what = IM_ROOM_PARTICIPANT_LEFT;
int flags = participant.flags;
if (flags & gloox::UserBanned)
im_what = IM_ROOM_PARTICIPANT_BANNED;
else if (flags & gloox::UserKicked)
im_what = IM_ROOM_PARTICIPANT_KICKED;
BMessage leftMsg(IM_MESSAGE);
leftMsg.AddInt32("im_what", im_what);
leftMsg.AddString("chat_id", chat_id);
leftMsg.AddString("user_id", user_id);
leftMsg.AddString("user_name", nick);
if (participant.reason.empty() == false)
leftMsg.AddString("body", participant.reason.c_str());
_SendMessage(&leftMsg);
}
void
JabberHandler::_StatusSetMsg(const char* user_id, gloox::Presence::PresenceType type,
const char* message, const char* resource)
{
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_STATUS_SET);
msg.AddString("user_id", user_id);
msg.AddInt32("status", _GlooxStatusToCaya(type));
if (BString(resource).IsEmpty() == false)
msg.AddString("resource", resource);
if (BString(message).IsEmpty() == false)
msg.AddString("message", message);
_SendMessage(&msg);
}
void
JabberHandler::_EnsureUserChat(const char* chat_id)
{
if (fUserChats.HasString(BString(chat_id)) == false)
fUserChats.Add(BString(chat_id));
}
status_t
JabberHandler::_SetupAvatarCache()
{
if (fAvatarCachePath.InitCheck() == B_OK)
return B_OK;
BPath path;
if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
return B_ERROR;
path.Append("Caya");
path.Append("Cache");
path.Append(GetName());
if (create_directory(path.Path(), 0755) != B_OK)
return B_ERROR;
fCachePath = path;
path.Append("avatar-cache");
BFile file(path.Path(), B_READ_ONLY);
fAvatarCache.Unflatten(&file);
fAvatarCachePath = path;
return B_OK;
}
status_t
JabberHandler::_SaveAvatarCache()
{
if (fAvatarCachePath.InitCheck() != B_OK)
return B_ERROR;
BFile file(fAvatarCachePath.Path(), B_CREATE_FILE | B_WRITE_ONLY | B_ERASE_FILE);
return fAvatarCache.Flatten(&file);
}
void
JabberHandler::_CacheAvatar(const char* id, const char* binimage, size_t length)
{
if (!id || !binimage || length <= 0)
return;
// Calculate avatar hash
CSHA1 s1;
char hash[256];
s1.Reset();
s1.Update((uchar*)binimage, length);
s1.Final();
s1.ReportHash(hash, CSHA1::REPORT_HEX);
BString sha1;
sha1.SetTo(hash, 256);
BString oldSha1;
if (fAvatarCache.FindString(id, &oldSha1) != B_OK || oldSha1 == "" || sha1 != oldSha1) {
// Replace old hash and save cache
fAvatarCache.RemoveName(id);
fAvatarCache.AddString(id, sha1);
_SaveAvatarCache();
if (oldSha1 != "") {
BPath path(fCachePath);
path.Append(oldSha1);
// Remove old image file
BEntry entry(path.Path());
entry.Remove();
// Remove old hash from the list
BString* item = NULL;
for (int32 i = 0; (item = (BString*)fAvatars->ItemAt(i)); i++) {
if (item->Compare(oldSha1) == 0) {
fAvatars->RemoveItem(item);
break;
}
}
}
}
// Determine file path
BPath path(fCachePath);
path.Append(sha1);
BEntry entry(path.Path());
if (!entry.Exists()) {
// Save to file
BFile file(path.Path(), B_CREATE_FILE | B_WRITE_ONLY | B_ERASE_FILE);
file.Write(binimage, length);
}
// Do we need to notify Caya?
bool found = false;
BString* item = NULL;
for (int32 i = 0; (item = (BString*)fAvatars->ItemAt(i)); i++) {
if (item->Compare(sha1) == 0) {
found = true;
break;
}
}
if (!found) {
// Add new hash to the list if needed
fAvatars->AddItem(new BString(sha1));
// Notify Caya that the avatar has changed
_AvatarChanged(id, path.Path());
}
}
void
JabberHandler::_AvatarChanged(const char* id, const char* filename)
{
entry_ref ref;
if (get_ref_for_path(filename, &ref) != B_OK)
return;
BMessage msg(IM_MESSAGE);
if (fJid.bare() == id)
msg.AddInt32("im_what", IM_OWN_AVATAR_SET);
else {
msg.AddInt32("im_what", IM_AVATAR_SET);
msg.AddString("user_id", id);
}
msg.AddRef("ref", &ref);
_SendMessage(&msg);
}
CayaStatus
JabberHandler::_GlooxStatusToCaya(gloox::Presence::PresenceType type)
{
switch (type) {
case gloox::Presence::Available:
case gloox::Presence::Chat:
return CAYA_ONLINE;
case gloox::Presence::Away:
return CAYA_AWAY;
case gloox::Presence::XA:
return CAYA_CUSTOM_STATUS;
case gloox::Presence::DND:
return CAYA_DO_NOT_DISTURB;
case gloox::Presence::Unavailable:
return CAYA_OFFLINE;
default:
break;
}
return CAYA_OFFLINE;
}
BString
JabberHandler::_MUCChatId(gloox::MUCRoom* room)
{
BString chat_id(room->name().c_str());
chat_id << "@" << room->service().c_str();
BStringList parts;
chat_id.Split("/", false, parts);
return parts.StringAt(0);
}
bool
JabberHandler::_MUCUserId(BString chat_id, const char* nick, BString* id)
{
BString chat(chat_id);
chat << "/" << nick;
// If sent from own user, use normal ID
if (nick == fNick) {
*id = fJid.bare().c_str();
return true;
}
*id = chat;
return false;
}
void
JabberHandler::_JoinRoom(const char* chat_id)
{
BString join_id(chat_id);
join_id << "/" << fNick;
gloox::MUCRoom* room = fRooms.ValueFor(chat_id);
if (room == NULL)
room = new gloox::MUCRoom(fClient, gloox::JID(join_id.String()), this, this);
room->join();
fRooms.AddItem(BString(chat_id), room);
}
void
JabberHandler::_MUCModeration(BMessage* msg)
{
BString chat_id = msg->FindString("chat_id");
BString user_id;
BString body = msg->FindString("body");
gloox::MUCRoom* room = fRooms.ValueFor(chat_id);
if (room == NULL || msg->FindString("user_id", &user_id) != B_OK)
return;
std::string nick = gloox::JID(user_id.String()).resource();
switch (msg->FindInt32("im_what"))
{
case IM_ROOM_KICK_PARTICIPANT:
room->kick(nick, body.String());
break;
case IM_ROOM_BAN_PARTICIPANT:
room->ban(nick, body.String());
break;
case IM_ROOM_MUTE_PARTICIPANT:
room->revokeVoice(nick, body.String());
break;
case IM_ROOM_UNMUTE_PARTICIPANT:
room->grantVoice(nick, body.String());
break;
}
}
const char*
JabberHandler::_RoleTitle(gloox::MUCRoomRole role, gloox::MUCRoomAffiliation aff)
{
switch (role)
{
case gloox::RoleVisitor:
return "Visitor";
case gloox::RoleParticipant:
return "Member";
case gloox::RoleModerator:
if (aff == gloox::AffiliationOwner)
return "Owner";
return "Moderator";
}
return "Invalid";
}
int32
JabberHandler::_RolePerms(gloox::MUCRoomRole role, gloox::MUCRoomAffiliation aff)
{
switch (role)
{
case gloox::RoleVisitor:
return 0 | PERM_READ | PERM_NICK;
case gloox::RoleParticipant:
return 0 | PERM_READ | PERM_WRITE | PERM_ROOM_SUBJECT;
case gloox::RoleModerator: {
int32 perm = 0 | PERM_READ | PERM_WRITE | PERM_ROOM_SUBJECT
| PERM_KICK | PERM_MUTE;
if (aff == gloox::AffiliationOwner)
perm = perm | PERM_ROLECHANGE | PERM_BAN;
return perm;
}
}
return 0;
}
int32
JabberHandler::_RolePriority(gloox::MUCRoomRole role, gloox::MUCRoomAffiliation aff)
{
switch (role)
{
case gloox::RoleParticipant:
return 1;
case gloox::RoleModerator:
if (aff == gloox::AffiliationOwner)
return 3;
return 2;
}
return 0;
}
BMessage
JabberHandler::_SettingsTemplate(const char* username, bool serverOption)
{
BMessage stemplate('IMst');
BMessage usernameText;
usernameText.AddString("name", "username");
usernameText.AddString("description", username);
usernameText.AddInt32("type", 'CSTR');
stemplate.AddMessage("setting", &usernameText);
BMessage passwordText;
passwordText.AddString("name", "password");
passwordText.AddString("description", "Password");
passwordText.AddInt32("type", 'CSTR');
passwordText.AddBool("is_secret", true);
stemplate.AddMessage("setting", &passwordText);
BMessage serverText;
serverText.AddString("name", "server");
serverText.AddString("description", "Server");
serverText.AddInt32("type", 'CSTR');
if (serverOption == true)
stemplate.AddMessage("setting", &serverText);
BMessage resourceText;
resourceText.AddString("name", "resource");
resourceText.AddString("description", "Resource");
resourceText.AddInt32("type", 'CSTR');
resourceText.AddString("default", "Caya");
stemplate.AddMessage("setting", &resourceText);
return stemplate;
}
/***********************************************************************
* gloox callbacks
**********************************************************************/
void
JabberHandler::onConnect()
{
// We are online
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_OWN_STATUS_SET);
msg.AddInt32("status", CAYA_ONLINE);
_SendMessage(&msg);
fVCardManager->fetchVCard(fJid, this);
}
void
JabberHandler::onDisconnect(gloox::ConnectionError e)
{
// We are offline
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_OWN_STATUS_SET);
msg.AddInt32("status", CAYA_OFFLINE);
_SendMessage(&msg);
if (e == gloox::ConnNoError) {
// Notify our offline status
BString content(fUsername);
content << " has logged out!";
_Notify(B_INFORMATION_NOTIFICATION, "Disconnected",
content.String());
return;
}
// Handle error
HandleConnectionError(e);
}
bool
JabberHandler::onTLSConnect(const gloox::CertInfo& info)
{
return true;
}
void
JabberHandler::onResourceBindError(const gloox::Error* error)
{
}
void
JabberHandler::handleRoster(const gloox::Roster& roster)
{
std::list<BMessage> msgs;
BMessage contactListMsg(IM_MESSAGE);
contactListMsg.AddInt32("im_what", IM_CONTACT_LIST);
gloox::Roster::const_iterator it = roster.begin();
for (; it != roster.end(); ++it) {
const char* jid = (*it).second->jidJID().full().c_str();
const char* name = (*it).second->name().c_str();
int32 subscription = (*it).second->subscription();
// Add jid to the server based contact list message
contactListMsg.AddString("user_id", jid);
// Contact information message
BMessage infoMsg(IM_MESSAGE);
infoMsg.AddInt32("im_what", IM_CONTACT_INFO);
infoMsg.AddString("user_id", jid);
infoMsg.AddString("user_name", name);
infoMsg.AddInt32("status", CAYA_OFFLINE);
// Groups
gloox::StringList g = (*it).second->groups();
gloox::StringList::const_iterator it_g = g.begin();
for (; it_g != g.end(); ++it_g)
infoMsg.AddString("group", (*it_g).c_str());
// Resources
gloox::RosterItem::ResourceMap::const_iterator rit
= (*it).second->resources().begin();
for (; rit != (*it).second->resources().end(); ++rit)
infoMsg.AddString("resource", (*rit).first.c_str());
// Store contact info message to be sent later
msgs.push_back(infoMsg);
}
// Send server based contact list
_SendMessage(&contactListMsg);
// Contact list and vCard request
std::list<BMessage>::iterator msgsIt;
for (msgsIt = msgs.begin(); msgsIt != msgs.end(); ++msgsIt) {
BMessage msg = (*msgsIt);
const char* jid = msg.FindString("user_id");
_SendMessage(&msg);
fVCardManager->fetchVCard(gloox::JID(jid), this);
}
// This is a safe spot to say "READY", since this is called immediately
// after logging in, whether the user has friends in the roster or not―
// especially since loading all the users from the roster can take a while.
BMessage readyMsg(IM_MESSAGE);
readyMsg.AddInt32("im_what", IM_PROTOCOL_READY);
_SendMessage(&readyMsg);
}
void
JabberHandler::handleMessageSession(gloox::MessageSession* session)
{
// Delete previous session
if (fSession)
fClient->disposeMessageSession(fSession);
fSession = session;
// Register message handler
fSession->registerMessageHandler(this);
// Message event filter
gloox::MessageEventFilter* messageFilter
= new gloox::MessageEventFilter(fSession);
messageFilter->registerMessageEventHandler(this);
// Chat state filter
gloox::ChatStateFilter* chatFilter
= new gloox::ChatStateFilter(fSession);
chatFilter->registerChatStateHandler(this);
}
void
JabberHandler::handleMessage(const gloox::Message& m, gloox::MessageSession*)
{
// Only chat messages are handled now
if (m.subtype() != gloox::Message::Chat)
return;
// We need a body
if (m.body() == "")
return;
_EnsureUserChat(m.from().bare().c_str());
// Notify that a chat message was received
BMessage msg(IM_MESSAGE);
msg.AddString("user_id", m.from().bare().c_str());
msg.AddString("chat_id", m.from().bare().c_str());
msg.AddInt32("im_what", IM_MESSAGE_RECEIVED);
if (m.subject() != "")
msg.AddString("subject", m.subject().c_str());
msg.AddString("body", m.body().c_str());
_SendMessage(&msg);
}
void
JabberHandler::handleMessageEvent(const gloox::JID& from, gloox::MessageEventType event)
{
}
void
JabberHandler::handleChatState(const gloox::JID& from, gloox::ChatStateType state)
{
printf("------ %d\n", state);
// We're interested only in some states
if (state == gloox::ChatStateActive || state == gloox::ChatStateInvalid)
return;
_EnsureUserChat(from.bare().c_str());
BMessage msg(IM_MESSAGE);
msg.AddString("user_id", from.bare().c_str());
msg.AddString("chat_id", from.bare().c_str());
switch (state) {
case gloox::ChatStateComposing:
msg.AddInt32("im_what", IM_USER_STARTED_TYPING);
break;
case gloox::ChatStatePaused:
msg.AddInt32("im_what", IM_USER_STOPPED_TYPING);
break;
case gloox::ChatStateGone:
// TODO
break;
default:
break;
}
_SendMessage(&msg);
}
void
JabberHandler::handleMUCParticipantPresence(gloox::MUCRoom *room,
const gloox::MUCRoomParticipant participant,
const gloox::Presence &presence)
{
// participant.flags, particpiant.role
// 0-0 (left), 0-* (joined/rolechange)
gloox::MUCRoomRole role = participant.role;
gloox::MUCRoomAffiliation aff = participant.affiliation;
const char* nick = participant.nick->resource().c_str();
BString user_id;
BString chat_id = _MUCChatId(room);
bool isSelf = _MUCUserId(chat_id, nick, &user_id);
if (chat_id.IsEmpty() == true || user_id.IsEmpty() == true)
return;
if (isSelf == true) {
int im_what = IM_ROOM_JOINED;
if (presence.presence() == 5)
im_what = IM_ROOM_LEFT;
BMessage joinedMsg(IM_MESSAGE);
joinedMsg.AddInt32("im_what", im_what);
joinedMsg.AddString("chat_id", chat_id);
_SendMessage(&joinedMsg);
_RoleChangedMsg(chat_id, user_id, role, aff);
return;
}
_StatusSetMsg(user_id.String(), presence.presence(), presence.status().c_str(), "");
// If unavailable (disconnected/left chat)
if (presence.presence() == 5) {
_UserLeftMsg(chat_id, participant);
return;
}
BMessage joinMsg(IM_MESSAGE);
joinMsg.AddInt32("im_what", IM_ROOM_PARTICIPANT_JOINED);
joinMsg.AddString("user_id", user_id);
joinMsg.AddString("user_name", nick);
joinMsg.AddString("chat_id", chat_id);
_SendMessage(&joinMsg);
_RoleChangedMsg(chat_id, user_id, role, aff);
}
void
JabberHandler::handleMUCMessage(gloox::MUCRoom *room, const gloox::Message &m,
bool priv)
{
BString user_id;
BString chat_id = _MUCChatId(room);
bool isSelf = _MUCUserId(chat_id, m.from().resource().c_str(), &user_id);
if (chat_id.IsEmpty() == true || user_id.IsEmpty() == true)
return;
int32 im_what = IM_MESSAGE_RECEIVED;
// We need a body
if (m.body() == "")
return;
// If sent from own user, then IM_MESSAGE_SENT
if (isSelf == true)
im_what = IM_MESSAGE_SENT;
// when() is only nonzero when sending backdated messages (logs)
if (m.when() != 0)
im_what = IM_LOGS_RECEIVED;
// Notify that a chat message was received
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", im_what);
msg.AddString("user_id", user_id);
msg.AddString("chat_id", chat_id);
if (m.subject() != "")
msg.AddString("subject", m.subject().c_str());
msg.AddString("body", m.body().c_str());
_SendMessage(&msg);
}
bool
JabberHandler::handleMUCRoomCreation(gloox::MUCRoom *room)
{
return true;
}
void
JabberHandler::handleMUCSubject(gloox::MUCRoom *room, const std::string &nick,
const std::string &subject)
{
BString user_id;
BString chat_id = _MUCChatId(room);
bool isSelf = _MUCUserId(chat_id, nick.c_str(), &user_id);
if (chat_id.IsEmpty() == true)
return;
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_ROOM_SUBJECT_SET);
msg.AddString("subject", subject.c_str());
msg.AddString("chat_id", chat_id);
if (user_id.IsEmpty() == false)
msg.AddString("user_id", user_id);
_SendMessage(&msg);
}
void
JabberHandler::handleMUCInviteDecline(gloox::MUCRoom *room, const gloox::JID &invitee,
const std::string &reason)
{
}
void
JabberHandler::handleMUCError(gloox::MUCRoom *room, gloox::StanzaError error)
{
HandleStanzaError(error);
}
void
JabberHandler::handleMUCInfo(gloox::MUCRoom *room, int features,
const std::string &name, const gloox::DataForm *infoForm)
{
BString chat_id = _MUCChatId(room);
BMessage metadata(IM_MESSAGE);
metadata.AddInt32("im_what", IM_ROOM_METADATA);
metadata.AddString("chat_id", chat_id);
metadata.AddString("chat_name", name.c_str());
metadata.AddInt32("room_default_flags",
0 | ROOM_AUTOJOIN | ROOM_LOG_LOCALLY | ROOM_POPULATE_LOGS);
metadata.AddInt32("room_disallowed_flags", 0 | ROOM_AUTOCREATE);
_SendMessage(&metadata);
}
void
JabberHandler::handleMUCItems(gloox::MUCRoom *room, const gloox::Disco::ItemList &items)
{
BString chat_id = _MUCChatId(room);
BStringList nicks;
BStringList ids;
if (chat_id.IsEmpty() == true)
return;
for (auto item: items) {
BString nick = item->jid().resource().c_str();
nicks.Add(nick);
// Unless it's the user, derive ID from room resource
if (fNick != nick) {
BString user_id(chat_id);
user_id << "/" << nick;
ids.Add(user_id);
}
else
ids.Add(BString(fJid.bare().c_str()));
}
BMessage msg(IM_MESSAGE);
msg.AddStrings("user_id", ids);
msg.AddStrings("user_name", nicks);
msg.AddString("chat_id", chat_id);
msg.AddInt32("im_what", IM_ROOM_PARTICIPANTS);
_SendMessage(&msg);
}
void
JabberHandler::handleMUCConfigList(gloox::MUCRoom* room, const gloox::MUCListItemList &items,
gloox::MUCOperation operation)
{
}
void
JabberHandler::handleMUCConfigForm(gloox::MUCRoom* room, const gloox::DataForm &form)
{
}
void
JabberHandler::handleMUCConfigResult(gloox::MUCRoom* room, bool success,
gloox::MUCOperation operation)
{
}
void
JabberHandler::handleMUCRequest(gloox::MUCRoom* room, const gloox::DataForm &form)
{
}
void
JabberHandler::handleItemAdded(const gloox::JID&)
{
}
void
JabberHandler::handleItemSubscribed(const gloox::JID&)
{
}
void
JabberHandler::handleItemUnsubscribed(const gloox::JID&)
{
}
void
JabberHandler::handleItemRemoved(const gloox::JID&)
{
}
void
JabberHandler::handleItemUpdated(const gloox::JID&)
{
}
void
JabberHandler::handleRosterPresence(const gloox::RosterItem& item,
const std::string& resource,
gloox::Presence::PresenceType type,
const std::string& presenceMsg)
{
_StatusSetMsg(item.jidJID().full().c_str(), type, presenceMsg.c_str(),
resource.c_str());
}
void
JabberHandler::handleSelfPresence(const gloox::RosterItem& item, const std::string&,
gloox::Presence::PresenceType type,
const std::string& presenceMsg)
{
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_OWN_CONTACT_INFO);
msg.AddString("protocol", Signature());
msg.AddString("user_id", item.jidJID().full().c_str());
msg.AddString("user_name", item.name().c_str());
msg.AddInt32("subscription", item.subscription());
msg.AddInt32("status", _GlooxStatusToCaya(type));
msg.AddString("message", presenceMsg.c_str());
// Groups
gloox::StringList g = item.groups();
gloox::StringList::const_iterator it_g = g.begin();
for (; it_g != g.end(); ++it_g)
msg.AddString("group", (*it_g).c_str());
// Resources
gloox::RosterItem::ResourceMap::const_iterator rit
= item.resources().begin();
for (; rit != item.resources().end(); ++rit)
msg.AddString("resource", (*rit).first.c_str());
_SendMessage(&msg);
}
bool
JabberHandler::handleSubscriptionRequest(const gloox::JID&, const std::string&)
{
return true;
}
bool
JabberHandler::handleUnsubscriptionRequest(const gloox::JID&, const std::string&)
{
return true;
}
void
JabberHandler::handleNonrosterPresence(const gloox::Presence&)
{
}
void
JabberHandler::handleRosterError(const gloox::IQ&)
{
}
void
JabberHandler::handleLog(gloox::LogLevel level, gloox::LogArea,
const std::string& msg)
{
if (level >= gloox::LogLevelWarning)
printf("%s\n", msg.c_str());
}
void
JabberHandler::handleVCard(const gloox::JID& jid, const gloox::VCard* card)
{
if (!card)
return;
gloox::VCard::Name name = card->name();
gloox::VCard::Photo photo = card->photo();
std::string fullName = name.family + " " + name.given;
BMessage msg(IM_MESSAGE);
msg.AddInt32("im_what", IM_EXTENDED_CONTACT_INFO);
msg.AddString("user_id", jid.bare().c_str());
msg.AddString("user_name", card->nickname().c_str());
msg.AddString("family_name", name.family.c_str());
msg.AddString("given_name", name.given.c_str());
msg.AddString("middle_name", name.middle.c_str());
msg.AddString("prefix", name.prefix.c_str());
msg.AddString("suffix", name.suffix.c_str());
msg.AddString("full_name", fullName.c_str());
_SendMessage(&msg);
// Return if there's no avatar icon
if (!photo.binval.c_str())
return;
if (_SetupAvatarCache() == B_OK)
// Cache avatar icon and notify the change
_CacheAvatar(jid.bare().c_str(), photo.binval.c_str(),
photo.binval.length());
}
void
JabberHandler::handleVCardResult(gloox::VCardHandler::VCardContext context,
const gloox::JID& jid,
gloox::StanzaError)
{
//if (context == gloox::VCardHandler::FetchVCard)
//else
}
InviteHandler::InviteHandler(gloox::ClientBase* client, JabberHandler* handler)
:
gloox::MUCInvitationHandler(client),
fHandler(handler)
{
}
void
InviteHandler::handleMUCInvitation(const gloox::JID& room, const gloox::JID& from,
const std::string& reason, const std::string& body,
const std::string& password, bool cont,
const std::string& thread)
{
std::string chat_name = room.resource().c_str();
BString chat_id = room.full().c_str();
if (chat_name.empty() == true)
chat_name = chat_id.String();
if (password.empty() == false)
chat_id << "#" << password.c_str();
BMessage invite(IM_MESSAGE);
invite.AddInt32("im_what", IM_ROOM_INVITE_RECEIVED);
invite.AddString("chat_id", chat_id);
invite.AddString("chat_name", chat_name.c_str());
invite.AddString("user_id", from.bare().c_str());
if (reason.empty() == false)
invite.AddString("body", reason.c_str());
invite.AddString("protocol", fHandler->Signature());
fHandler->MessengerInterface()->SendMessage(new BMessage(invite));
}