/*
Copyright (c) 2025 Miranda NG team (https://miranda-ng.org)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation version 2
of the License.
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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include "stdafx.h"
CTeamsProto::CTeamsProto(const char *protoName, const wchar_t *userName) :
PROTO(protoName, userName),
m_impl(*this),
m_requests(10),
m_PopupClasses(1),
m_OutMessages(3, PtrKeySortT),
m_bAutoHistorySync(this, "AutoSync", true),
m_bUseHostnameAsPlace(this, "UseHostName", true),
m_bUseBBCodes(this, "UseBBCodes", true),
m_wstrCListGroup(this, DBKEY_GROUP, L"Teams"),
m_wstrPlace(this, "Place", L""),
m_iMood(this, "Mood", 0),
m_wstrMoodEmoji(this, "MoodEmoji", L""),
m_wstrMoodMessage(this, "XStatusMsg", L"")
{
// create endpoint
m_szEndpoint = getMStringA("Endpoint");
if (m_szEndpoint.IsEmpty()) {
m_szEndpoint = Utils_GenerateUUID();
setString("Endpoint", m_szEndpoint);
}
// network
NETLIBUSER nlu = {};
nlu.flags = NUF_OUTGOING | NUF_INCOMING | NUF_HTTPCONNS | NUF_UNICODE;
nlu.szDescriptiveName.w = m_tszUserName;
nlu.szSettingsModule = m_szModuleName;
m_hNetlibUser = Netlib_RegisterUser(&nlu);
CMStringA module(FORMAT, "%s.TRouter", m_szModuleName);
CMStringW descr(FORMAT, TranslateT("%s websocket connection"), m_tszUserName);
nlu.szSettingsModule = module.GetBuffer();
nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_UNICODE;
nlu.szDescriptiveName.w = descr.GetBuffer();
m_hTrouterNetlibUser = Netlib_RegisterUser(&nlu);
CreateProtoService(PS_GETAVATARINFO, &CTeamsProto::SvcGetAvatarInfo);
CreateProtoService(PS_GETAVATARCAPS, &CTeamsProto::SvcGetAvatarCaps);
CreateProtoService(PS_GETMYAVATAR, &CTeamsProto::SvcGetMyAvatar);
CreateProtoService(PS_SETMYAVATAR, &CTeamsProto::SvcSetMyAvatar);
CreateProtoService(PS_OFFLINEFILE, &CTeamsProto::SvcOfflineFile);
CreateProtoService(PS_MENU_REQAUTH, &CTeamsProto::OnRequestAuth);
CreateProtoService(PS_MENU_GRANTAUTH, &CTeamsProto::OnGrantAuth);
CreateProtoService(PS_MENU_LOADHISTORY, &CTeamsProto::SvcLoadHistory);
CreateProtoService(PS_EMPTY_SRV_HISTORY, &CTeamsProto::SvcEmptyHistory);
HookProtoEvent(ME_OPT_INITIALISE, &CTeamsProto::OnOptionsInit);
CreateDirectoryTreeW(GetAvatarPath());
// sounds
g_plugin.addSound("skype_inc_call", L"SkypeWeb", LPGENW("Incoming call"));
g_plugin.addSound("skype_call_canceled", L"SkypeWeb", LPGENW("Incoming call canceled"));
InitGroupChatModule();
}
CTeamsProto::~CTeamsProto()
{
UninitPopups();
}
void CTeamsProto::OnEventDeleted(MCONTACT hContact, MEVENT hDbEvent, int flags)
{
if (!hContact || !(flags & CDF_DEL_HISTORY))
return;
DB::EventInfo dbei(hDbEvent, false);
if (dbei.szId) {
auto *pReq = new AsyncHttpRequest(REQUEST_DELETE, HOST_CHATS, "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/messages/" + dbei.szId);
pReq->AddAuthentication(this);
pReq->AddHeader("Origin", "https://web.skype.com");
pReq->AddHeader("Referer", "https://web.skype.com/");
PushRequest(pReq);
}
}
void CTeamsProto::OnEventEdited(MCONTACT hContact, MEVENT, const DBEVENTINFO &dbei)
{
if (dbei.szId)
SendServerMsg(hContact, dbei.pBlob, _atoi64(dbei.szId));
}
void CTeamsProto::OnModulesLoaded()
{
setAllContactStatuses(ID_STATUS_OFFLINE, false);
HookProtoEvent(ME_MSG_PRECREATEEVENT, &CTeamsProto::OnPreCreateMessage);
InitPopups();
}
void CTeamsProto::OnShutdown()
{
StopQueue();
StopTrouter();
}
INT_PTR CTeamsProto::GetCaps(int type, MCONTACT)
{
switch (type) {
case PFLAGNUM_1:
return PF1_IM | PF1_AUTHREQ | PF1_CHAT | PF1_BASICSEARCH | PF1_MODEMSG | PF1_FILE | PF1_SERVERCLIST;
case PFLAGNUM_2:
case PFLAGNUM_3:
return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND;
case PFLAGNUM_4:
return PF4_NOAUTHDENYREASON | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_IMSENDOFFLINE | PF4_OFFLINEFILES | PF4_SERVERMSGID | PF4_SERVERFORMATTING;
case PFLAG_UNIQUEIDTEXT:
return (INT_PTR)TranslateT("Teams ID");
}
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
MCONTACT CTeamsProto::AddToList(int, PROTOSEARCHRESULT *psr)
{
debugLogA(__FUNCTION__);
if (psr->id.a == nullptr)
return NULL;
MCONTACT hContact;
if (psr->flags & PSR_UNICODE)
hContact = AddContact(T2Utf(psr->id.w), T2Utf(psr->nick.w));
else
hContact = AddContact(psr->id.a, psr->nick.a);
return hContact;
}
MCONTACT CTeamsProto::AddToListByEvent(int, int, MEVENT hDbEvent)
{
debugLogA(__FUNCTION__);
DB::EventInfo dbei(hDbEvent);
if (!dbei)
return NULL;
if (mir_strcmp(dbei.szModule, m_szModuleName))
return NULL;
if (dbei.eventType != EVENTTYPE_AUTHREQUEST)
return NULL;
DB::AUTH_BLOB blob(dbei.pBlob);
return AddContact(blob.get_email(), blob.get_nick());
}
int CTeamsProto::Authorize(MEVENT hDbEvent)
{
MCONTACT hContact = GetContactFromAuthEvent(hDbEvent);
if (hContact == INVALID_CONTACT_ID)
return 1;
PushRequest(new AsyncHttpRequest(REQUEST_POST, HOST_CONTACTS, "/users/SELF/invites/" + mir_urlEncode(getId(hContact)) + "/accept"));
return 0;
}
int CTeamsProto::AuthDeny(MEVENT hDbEvent, const wchar_t *)
{
MCONTACT hContact = GetContactFromAuthEvent(hDbEvent);
if (hContact == INVALID_CONTACT_ID)
return 1;
PushRequest(new AsyncHttpRequest(REQUEST_POST, HOST_CONTACTS, "/users/SELF/invites/" + mir_urlEncode(getId(hContact)) + "/decline"));
return 0;
}
int CTeamsProto::AuthRecv(MCONTACT, DB::EventInfo &dbei)
{
return Proto_AuthRecv(m_szModuleName, dbei);
}
int CTeamsProto::AuthRequest(MCONTACT hContact, const wchar_t *szMessage)
{
if (hContact == INVALID_CONTACT_ID)
return 1;
auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_CONTACTS, "/users/SELF/contacts");
JSONNode node;
node << CHAR_PARAM("mri", getId(hContact));
if (mir_wstrlen(szMessage))
node << WCHAR_PARAM("greeting", szMessage);
pReq->m_szParam = node.write().c_str();
PushRequest(pReq);
return 0;
}
int CTeamsProto::GetInfo(MCONTACT hContact, int)
{
if (isChatRoom(hContact))
return 1;
GetProfileInfo(hContact);
return 0;
}
int CTeamsProto::SendMsg(MCONTACT hContact, MEVENT, const char *szMessage)
{
return SendServerMsg(hContact, szMessage);
}
int CTeamsProto::SetStatus(int iNewStatus)
{
if (iNewStatus == m_iDesiredStatus)
return 0;
debugLogA(__FUNCTION__ ": changing status from %i to %i", m_iStatus, iNewStatus);
int old_status = m_iStatus;
m_iDesiredStatus = iNewStatus;
if (iNewStatus == ID_STATUS_OFFLINE) {
m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE;
StopQueue();
StopTrouter();
ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, ID_STATUS_OFFLINE);
if (!Miranda_IsTerminated())
setAllContactStatuses(ID_STATUS_OFFLINE, false);
return 0;
}
if (m_iStatus == ID_STATUS_OFFLINE)
Login();
else
SetServerStatus(m_iDesiredStatus);
return 0;
}
int CTeamsProto::UserIsTyping(MCONTACT hContact, int iState)
{
JSONNode node;
node << INT64_PARAM("clientmessageid", getRandomId()) << CHAR_PARAM("contenttype", "Application/Message") << CHAR_PARAM("content", "")
<< CHAR_PARAM("messagetype", (iState == PROTOTYPE_SELFTYPING_ON) ? "Control/Typing" : "Control/ClearTyping");
auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_CHATS, "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/messages");
pReq->m_szParam = node.write().c_str();
PushRequest(pReq);
return 0;
}
int CTeamsProto::RecvContacts(MCONTACT hContact, DB::EventInfo &dbei)
{
PROTOSEARCHRESULT **isrList = (PROTOSEARCHRESULT **)dbei.pBlob;
int nCount = dbei.cbBlob;
uint32_t cbBlob = 0;
for (int i = 0; i < nCount; i++)
cbBlob += int(/*mir_wstrlen(isrList[i]->nick.w)*/0 + 2 + mir_wstrlen(isrList[i]->id.w));
char *pBlob = (char *)mir_calloc(cbBlob);
char *pCurBlob = pBlob;
for (int i = 0; i < nCount; i++) {
pCurBlob += mir_strlen(pCurBlob) + 1;
mir_strcpy(pCurBlob, _T2A(isrList[i]->id.w));
pCurBlob += mir_strlen(pCurBlob) + 1;
}
dbei.szModule = m_szModuleName;
dbei.eventType = EVENTTYPE_CONTACTS;
dbei.cbBlob = cbBlob;
dbei.pBlob = pBlob;
db_event_add(hContact, &dbei);
mir_free(pBlob);
return 0;
}