You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
RnQ/RnQ/ICQ/ICQSession.pas

6030 lines
182 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
This file is part of R&Q.
Under same license
}
unit ICQSession;
{$I RnQConfig.inc}
interface
uses
Winapi.Windows, System.SysUtils, System.Classes, System.Types, System.StrUtils, System.Math, System.JSON,
Net.HttpClient, Net.URLClient, Generics.Defaults, Generics.Collections,
RnQGlobal, RnQNet, RDGlobal, RQUtil, RnQPrefsLib, ICQCommon, ICQContacts, ICQConsts, Stickers,
mormot.crypt.core, mormot.crypt.ecc256r1;
{$I NoRTTI.inc}
type
TICQEvent = (
IE_error = Byte(ICQConsts.IE_error),
IE_online,
IE_offline,
IE_incoming,
IE_outgoing,
IE_msg,
IE_buzz,
IE_userinfo = Byte(High(ICQConsts.TProtoEvent)) + 20,
IE_userinfoCP,
IE_contacts,
IE_numOfContactsChanged,
IE_wpEnd,
IE_userSimpleInfo,
IE_statusChanged,
IE_xstatusChanged,
IE_authReq,
IE_auth,
IE_authDenied,
IE_wpResult,
IE_addedYou,
IE_visibilityChanged,
IE_toofast,
IE_connecting,
IE_connected,
IE_loggin,
IE_redirecting,
IE_redirected,
IE_almostOnline,
IE_serverSent,
IE_serverGot,
IE_MyInfoAck,
IE_pause,
IE_resume,
IE_ack,
IE_serverAck,
IE_msgError,
IE_Missed_MSG,
IE_sendingXStatus,
IE_ackXStatus,
IE_contactupdate,
IE_contactSelfDeleted,
IE_contactupdateInPlace,
IE_typing,
IE_avatar_changed,
IE_srvSomeInfo,
IE_MultiChat,
IE_serverHistoryReady,
IE_stickersupdate,
IE_stickersearchupdate,
IE_MsgPatchUpdate,
IE_MsgPatchDelete,
IE_MsgDecryptFailed,
IE_SmartReply,
IE_UpdateEvent
);
TICQPhase = (
null_, // offline
connecting_, // trying to reach the login server
login_, // performing login on login server
reconnecting_, // trying to reach the service server
relogin_, // performing login on service server
settingup_, // setting up things
online_
);
TICQSession = class;
TProtoNotify = procedure (Sender: TICQSession; event: Integer);
TICQNotify = procedure(Sender: TICQSession; event: TicqEvent) of object;
TErrorProc = reference to procedure(Resp: TPair);
THandlerProc = reference to procedure(const RespStr: String);
TReturnData = (RT_None, RT_JSON);
TICQSession = class
protected
StartAccount: TUID;
MyAccount: TUID;
// event managing
Listener: TProtoNotify;
procedure NotifyListeners(ev: TICQEvent);
procedure InitListenerVars(Contact: TICQContact = nil; Time: TDateTime = 0);
public
progLogon: Double;
aProxy: Tproxy;
SupportTypingNotif,
IsSendTypingNotif: Boolean;
class var ContactsDB: TRnQCList;
function ValidICQ(const UID: TUID): Boolean; inline;
function ValidUID(const UID: TUID): Boolean; inline;
function ValidPhone(const Phone: TUID): Boolean; inline;
function ValidMail(const Mail: TUID): Boolean;
function ContactExists(const UID: TUID): Boolean;
function GetShowStr: String;
property MyAccNum: TUID read MyAccount;
property StartAccountNum: TUID read StartAccount;
private
Phase: TICQPhase;
// WasUINwp: Boolean; // trigger a last result at first result
// PreviousInvisible : Boolean;
// P_WebAware: Boolean;
// P_AuthNeeded: Boolean;
// P_ShowInfo: Byte;
CurStatus: TICQStatus;
fVisibility: TVisibility;
SNACref: TMsgID;
Cookie: RawByteString;
Refs: array [1..MaxRefs] of record
Kind: TRefKind;
UID: TUID;
end;
SavingMyInfo: record
Running: Boolean;
ACKcount: Integer;
c: TICQContact;
end;
fRoster: TRnQCList;
SpamList: TRnQCList;
fPwd: String;
fSessionSecret: String;
fSessionKey: RawByteString;
fAuthToken: String;
fAuthTokenTime: Integer;
fAuthTokenExpIn: Integer;
fHostOffset: Integer;
fAimSid: String;
fDevId: String;
fFetchBaseURL: String;
// fRESTToken: String;
// fRESTTokenTime: TDateTime;
// fRESTClientId: String;
LastFetchBaseURL: String;
FatalErrorCount: Integer;
BuzzedLastTime: TDateTime;
public
fECCKeys: record
Generated: Boolean;
PubEccKey: TECCPublicKey;
PrivKey: TECCPrivateKey;
end;
// listener: TicqNotify;
// MyInfo0: TICQcontact;
BirthdayFlag: Boolean;
CurXStatusVal: Byte;
CurXStatusStr: TXStatStr;
// used to pass valors to listeners
eventError : TicqError;
eventOldStatus: TICQstatus;
eventOldInvisible: Boolean;
eventUrgent: Boolean;
eventContact: TICQContact;
eventContacts: TRnQCList;
eventMsgA: RawByteString;
eventAddress: String;
eventNameA: AnsiString;
eventData: String;
// eventBinData: TBytes;
// eventFilename: String;
eventInt: Integer; // multi-purpose
eventFlags: Dword;
// eventFileSize: LongWord;
eventTime: TDateTime; // in local time
eventMsgID: TMsgID;
eventInBackground: Boolean;
eventStream: TMemoryStream;
eventWID: RawByteString;
eventEncoding: TEncoding;
eventArray: TArray;
// acceptKey: String;
// ConnectSSL: Boolean;
ShowClientID,
UseCryptMsg,
UseEccCryptMsg,
SaveToken,
AvatarsSupport,
AvatarsAutoGet,
AutoReqXStatus: Boolean;
MyAvatarHash: String;
HttpPoll: THttpAsync;
PollingTask, ReconnectTask: TAnonTask;
LastSearchPacks: TStickerPacks;
SrvHist: TSrvHist;
class function _GetProtoName: string;
class function _IsProtoUid(const UID: TUID): Boolean;
class function _IsValidUid(const UIN: TUID): Boolean;
class function _IsValidPhone(const Phone: TUID): Boolean;
class function _IsValidMail(const Mail: TUID): Boolean;
class function _CreateProto(const UID: TUID): TICQSession;
class function _RegisterUser: Boolean;
class function _MaxPWDLen: Integer;
function GetICQContact(const uid: TUID): TICQContact; overload;
function GetICQContact(const uin: Integer): TICQContact; overload;
function GetContact(const UID: TUID): TICQContact; overload;
function GetContact(const UIN: Integer): TICQContact; overload;
function PwdEqual(const Pass: String): Boolean;
// constructor Create; override;
// destructor Destroy; override;
class constructor InitICQProto;
class destructor UnInitICQProto;
constructor Create(const id: TUID);
destructor Destroy; override; final;
procedure ResetPrefs;
procedure GetPrefs(var pp: TRnQPref);
procedure SetPrefs(pp: TRnQPref);
procedure Clear;
function RequestPasswordIfNeeded(DoConnect: Boolean = True): Boolean;
procedure Connect;
procedure Disconnect;
function SetStatusAndVis(st, vi: Byte; IsAuto: Boolean = False): Byte;
function SetStatus(st: Byte; IsAuto: Boolean = False): Byte;
procedure SetVisibility(vi: Byte);
procedure DoSetStatus(st: Byte; vi: Byte);
function GetPwd: String;
procedure SetPwd(const Value: String);
procedure SetCurXStatus(XStatus: Byte);
function MakeParams(const Params: TDictionary; DoublePercent: Boolean = False): String;
// procedure OpenICQURL(const URL: String);
// procedure RESTGenToken;
// procedure RESTCheckToken;
// procedure RESTAddClient;
// function RESTCheckErrors(var JSON: TJSONObject): Boolean;
// function RESTShowError(var JSON: TJSONObject; ErrText: String = ''): Boolean;
// function RESTAvailable: Boolean;
function ClientLogin: Boolean;
function StartSession: Boolean;
function PingSession: Boolean;
procedure AfterSessionStarted;
procedure ResetSession;
procedure EndSession(EndToken: Boolean = False);
function GetSessionsList: TArray;
function GetAccountUID(const Buddy: TJSONObject): String;
procedure CloseSession(const Hash: String; Current: Boolean);
procedure CloseAllSessions;
procedure PollError(const ExtraError: String = ''; Silent: Boolean = False);
procedure StartPolling;
procedure RestartPolling(Delay: Integer = 100);
procedure AbortPolling;
procedure PollURL(URL: String);
procedure PollRequestDone(Sender: TObject; const Response: IHTTPResponse; const Error: String = '');
procedure ProcessContactList(const CL: TJSONArray; Batch: Boolean = False);
function ProcessContact(const Buddy: TJSONObject; GroupToAddTo: Integer = -1; Batch: Boolean = False): TICQContact;
procedure ProcessNewStatus(var Cnt: TICQcontact; NewStatus: TICQstatus; XStatusStrChanged: Boolean = False; NoNotify: Boolean = False);
procedure ProcessUsersAndGroups(const JSON: TJSONObject);
function ProcessMsg(const UID: TUID; Msg: TJSONObject; Patch: Boolean = False): TMsgID;
procedure ProcessDialogState(const Dlg: TJSONObject);
procedure ProcessIMState(const Data: TJSONObject);
procedure ProcessTyping(const Data: TJSONObject);
procedure ProcessAddedYou(const Data: TJSONObject);
procedure ProcessPermitDeny(const Data: TJSONObject);
procedure ProcessDiff(const Data: TJSONArray);
procedure ProcessXStatus(const Data: TJSONObject);
procedure ProcessSmartReply(const Data: TJSONObject);
procedure ProcessReactions(const Data: TJSONObject);
procedure ParseReactions(MsgID: TMsgID; const ChatID: String; NotifyMsgID: TMsgID; Data: TJSONArray; MyReaction: String = ''); overload;
procedure InitWebRTC;
function RequiresLogin: Boolean;
function GetStatus: Byte;
function GetVisibility: Byte;
function IsOnline: Boolean;
function IsOffline: Boolean;
function IsReady: Boolean;
function IsConnecting: Boolean;
function IsSSCL: Boolean;
function IsMobileAccount: Boolean;
function IsVisible: Boolean;
function IsInvisible: Boolean;
function GetStatusName: String;
function GetStatusImg: TPicName;
function GetXStatus: Byte;
// manage contact lists
function ReadList(l: TLIST_TYPES): TRnQCList;
procedure AddToList(l: TLIST_TYPES; cl: TRnQCList); overload;
// procedure RemFromList(l: TLIST_TYPES; cl: TRnQCList); overload;
// manage contacts
procedure AddToList(l: TLIST_TYPES; cnt: TICQContact); overload;
procedure RemFromList(l: TLIST_TYPES; cnt: TICQContact); overload;
function IsInList(l: TLIST_TYPES; cnt: TICQContact): Boolean;
function AddContact(c: TICQContact; IsLocal: Boolean = false): Boolean;
function RemoveContact(c: TICQContact): Boolean;
procedure RemoveContactFromServer(c: TICQContact);
function SendUpdateGroup(const Name: String; ga: TGroupAction; const Old: String = ''): Boolean;
procedure InputChangedFor(c: TICQContact; InpIsEmpty: Boolean; TimeOut: Boolean = False);
function UpdateGroupOf(c: TICQContact; grp: Integer): Boolean;
procedure GetClientPicAndDesc4(cnt: TICQContact; var pPic: TPicName; var CliDesc: String);
function MaxCharsFor(const c: TICQContact): Integer;
function CompareStatusFor(Cnt1, Cnt2: TICQContact): SmallInt;
function CanAddCntOutOfGroup: Boolean;
function CreateNewGUID: String;
procedure AddMsg(Flags: Byte; Urgent: Boolean; const Msg: RawByteString);
// ICQ Only
// property WebAware: Boolean read P_WebAware write SetWebAware;
// property AuthNeeded: Boolean read P_AuthNeeded write SetAuthNeeded;
// property ShowInfo: Byte read P_ShowInfo write P_ShowInfo;
property Pwd: String read GetPwd write SetPwd;
property AimSid: String read fAimSid;
property Visibility: TVisibility read fVisibility write fVisibility;
property CurXStatus: Byte read CurXStatusVal write SetCurXStatus;
function GetHistoryChunk(const UID: TUID; PatchVer: String; const From: String; Count: Integer; const Till: String = ''): TJSONObject;
procedure GetServerHistory(const UID: TUID; FromMsgId: TMsgID; const PatchVer: String);
procedure GetServerPatches(const UID: TUID; const PatchVer: String);
procedure GetReactions(const UID: TUID; MsgID: TMsgID);
procedure AddReaction(const UID: TUID; MsgID: TMsgID; Reaction: Integer);
procedure RemoveReaction(const UID: TUID; MsgID: TMsgID);
procedure ListReactions(const UID: TUID; MsgID: TMsgID);
procedure MarkRead(const UID: TUID; MsgID: TMsgID);
procedure DeleteMessages(const UID: TUID; var IDs: TArray; ForAll: Boolean);
procedure EventSubscribe(Contact: TICQContact = nil);
procedure EventResubscribe(Contact: TICQContact = nil);
procedure CheckEventSubscribe(Interval: Integer = 3);
function SetNick: Boolean;
function FilesInit(const FileName: String; Size: Int64): String;
private
//procedure sendAddTempContact(const buinlist: RawByteString); overload; // 030F
public // ICQ Only
procedure SendAddContact(c: TICQContact);
procedure SendRemoveContact(c: TICQContact);
procedure AddContactToCL(var c: TICQContact);
procedure AddContactsToCL(cl: TRnQCList);
procedure SendTyping(c: TICQContact; NotifType: Word);
procedure SendSaveMyInfo(Cnt: TICQContact);
function GetMyCaps: String;
procedure GetProfile(const UID: TUID);
procedure GetUserInfo(const UID: TUID);
function GetLastSeen(const UIDs: TUIDS): Boolean;
procedure GetContactInfo(const UID: TUID; const IncludeField: String);
procedure GetContactAttrs(const UID: TUID);
procedure SendContactAttrs(c: TICQContact);
procedure GetCL;
function SearchContact(const Keyword: String; out Anketa: TAnketa): Boolean;
procedure ValidateSid;
procedure GetExpressions;
function GetSpeechToText(const FileId: String): String;
procedure GetAllCaps;
procedure GetAllLastSeen;
procedure Test;
function SendSessionRequest(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String;
const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean; overload;
function SendSessionRequest(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String; Ret: TReturnData;
out JSON: TJSONObject; const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean; overload;
function SendRAPIRequest(JSONReq: Boolean; const Method: String; Params: Pointer; const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean; overload;
function SendRAPIRequest(JSONReq: Boolean; const Method: String; Params: Pointer; Ret: TReturnData; out JSON: TJSONObject; const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean; overload;
function SendRequest(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String;
const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean; overload;
function SendRequest(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String; Ret: TReturnData;
out JSON: TJSONObject; const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean; overload;
procedure SendRequestAsync(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String; const Header: AnsiString = ''; HandlerProc: THandlerProc = nil);
function SendPresenceState(ForceOnline: Boolean = False): Boolean;
procedure SendStatus(XStatus: Integer = -1; const StText: String = '');
private
procedure GetPermitDeny;
procedure SetPermitDenyMode(const Mode: String);
procedure AddToBlock(const c: String);
procedure RemFromBlock(const c: String);
function Add2Ignore(c: TICQContact): Boolean; //overload;
function RemFromIgnore(c: TICQContact): Boolean;
procedure GoneOffline; // called going offline
public // All
function CreateDataPayload(Caps: TArray; const Data: TBytes = nil; Compressed: Integer = -1; CRC: Cardinal = 0; Len: Integer = 0): String;
procedure SendMsg(Cnt: TICQContact; Kind: Integer; Flags: DWord; const HistMsg: String; const Msg: String; Update: TMsgID = 0);
function SendBuzz(Cnt: TICQContact): Boolean;
function SendDecryptError(Cnt: TICQContact; MsgID: TMsgID): Boolean;
procedure SetListener(l: TProtoNotify);
procedure SetMuted(c: TICQcontact; Mute: Boolean);
procedure Authorize(c: TICQContact; Grant: Boolean = True); deprecated;
procedure AuthRequest(c: TICQContact; Reason: String); deprecated;
function AddRef(k: TRefKind; const uin: TUID): Integer;
function IsMyAcc(c: TICQContact): Boolean;
function GetMyInfo: TICQContact;
function GetStartInfo: TICQContact;
function GetStatuses: TStatusArray;
function GetVisibilities: TStatusArray;
function GetStatusMenu: TStatusMenu;
function GetVisMenu: TStatusMenu;
function GetStatusDisable: TOnStatusDisable;
procedure ApplyBalloon;
property Statuses: TStatusArray read GetStatuses;
property MyInfo: TICQContact read getMyInfo;
procedure GetStoreStickerPacks;
procedure SearchStoreStickerPacks(const Query: String);
procedure SearchStoreStickerPack(const Id: String; IdType: TStickerIDType);
function GetStoreStickerPackInfo(const Id: String; IdType: TStickerIDType = SIDT_ID; WithMeta: Boolean = True): TStickerPack;
procedure BuyStickerPack(const PackId: String);
procedure RemoveStickerPack(const PackId: String);
end;
TICQProtoClass = class of TICQSession;
const
SupportedPatches: array [0..3] of String = ('update', 'modify', 'delete', 'setReactions');
var
// sendInterests,
ShowInvisSts,
AvatarsNotDnlddInform: Boolean;
ExtClientCaps: String;
AddExtCliCaps: Boolean;
SendBalloonOn: Integer;
SendBalloonOnDate: TDateTime;
ICQStatuses, ICQVis: TStatusArray;
StatMenu, ICQVisMenu: TStatusMenu;
ReqId: Integer = 1;
AttachedLoginPhone: String;
implementation
uses
System.DateUtils,
SciterLib, GlobalLib, GroupsLib, UtilLib, OutboxLib, RoasterLib,
{$IFDEF UNICODE}
AnsiStrings,
{$ENDIF UNICODE}
RnQZip, RnQLangs, RnQSysUtils, RDUtils, RnQCrypt, Base64,
RnQStrings, RQCodes, Protocol_ICQ, ICQClients, SQLiteDB, events;
const
AESBLKSIZE = SizeOf(TAESBlock);
function CheckResponseData(var JSON: TJSONObject; out SReqID: String): TPair; overload;
var
Tmp: TJSONValue;
begin
Result.Key := 0;
Result.Value := '';
if Assigned(JSON) then
begin
Tmp := JSON.GetValue('response');
if Assigned(Tmp) and (Tmp is TJSONObject) then
begin
Tmp.Owned := False;
JSON.Free;
JSON := Tmp as TJSONObject;
JSON.GetValueSafe('requestId', SReqID);
JSON.GetValueSafe('statusCode', Result.Key);
JSON.GetValueSafe('statusText', Result.Value);
if Result.Key = Ord(EAC_OK) then
begin
Tmp := JSON.GetValue('data');
if Assigned(Tmp) and (Tmp is TJSONObject) then
begin
Tmp.Owned := False;
JSON.Free;
JSON := Tmp as TJSONObject;
end;
end;
end;
end;
end;
function CheckResponseData(var JSON: TJSONObject): TPair; overload;
var
SReqID: String;
begin
Result := CheckResponseData(JSON, SReqID);
end;
function CheckRAPIData(var JSON: TJSONObject): TPair; overload;
var
Tmp: TJSONValue;
begin
Result.Key := 0;
Result.Value := '';
if Assigned(JSON) then
begin
// if TJSONObject(JSON.GetValue('status')).GetValue('code').TryGetValue(Code) then
// if not (Code = 20000) then
// begin
// // if Code = 40201 then
// // RESTGenToken
// // else
// ODS('Error code: ' + IntToStr(Code));
// Result := False;
// end;
// JSON.GetValueSafe('reqId', SReqID);
Tmp := JSON.GetValue('status');
if Assigned(Tmp) and (Tmp is TJSONObject) then
begin
Tmp.GetValueSafe('code', Result.Key);
Tmp.GetValueSafe('reason', Result.Value);
end;
Tmp := JSON.GetValue('results');
if not Assigned(Tmp) or not (Tmp is TJSONObject) then
Tmp := JSON.GetValue('result');
if Assigned(Tmp) and (Tmp is TJSONObject) then
begin
Tmp.Owned := False;
JSON.Free;
JSON := Tmp as TJSONObject;
end else JSON := nil;
end;
end;
function CheckSimpleData(var JSON: TJSONObject; StatusOnly: Boolean = False): Boolean;
var
Tmp: TJSONValue;
begin
Result := True;
Tmp := JSON.GetValue('status');
if not Assigned(Tmp) or not (Tmp is TJSONNumber) or not (TJSONNumber(Tmp).AsInt = Ord(EAC_OK)) then
Exit(False);
if not StatusOnly then
begin
Tmp := JSON.GetValue('data');
if not Assigned(Tmp) or not (Tmp is TJSONObject) then
Exit(False);
JSON := TJSONObject(Tmp);
end;
end;
function TICQSession.ValidUID(const UID: TUID): Boolean;
begin
Result := (Length(UID) > 0) and Self._IsValidUid(UID);
end;
function TICQSession.ValidPhone(const Phone: TUID): Boolean;
begin
Result := (Length(Phone) > 0) and Self._IsValidPhone(Phone);
end;
function TICQSession.ValidMail(const Mail: TUID): Boolean;
begin
Result := (Length(Mail) > 0) and Self._IsValidMail(Mail);
end;
function TICQSession.ValidICQ(const UID: TUID): Boolean;
begin
Result := (Length(UID) > 0) and (ValidUID(UID) or ValidPhone(UID) or ValidMail(UID));
end;
function TICQSession.ContactExists(const UID: TUID): Boolean;
begin
Result := contactsDB.exists(UID);
end;
function TICQSession.GetShowStr: String;
var
mi: TICQContact;
begin
mi := GetMyInfo;
if Assigned(mi) then
Result := GetMyInfo.Displayed
else
Result := MyAccNum;
Result := '(' + _GetProtoName + ') ' + Result;
end;
class function TICQSession._RegisterUser: Boolean;
begin
Result := False;
OpenURL('https://icq.com/join/');
end;
class function TICQSession._CreateProto(const UID: TUID): TICQSession;
begin
Result := TICQSession.Create(uid);
end;
constructor TICQSession.Create(const id: TUID);
begin
ContactsDB := TRnQCList.Create;
Phase := null_;
Listener := nil;
PollingTask := nil;
ReconnectTask := nil;
StartAccount := TICQContact.TrimUID(id);
MyAccount := StartAccount;
// if (MyAccount <> '') and (Pos(AnsiChar('@'), MyAccount) > 1) then
// AttachedLoginEmail := MyAccount
// else
// AttachedLoginEmail := '';
AttachedLoginPhone := '';
fPwd := '';
fHostOffset := 0;
fAuthTokenExpIn := 0; // Never
fDevId := ICQ_DEV_ID;
SNACref := 1;
CurStatus := SC_OFFLINE;
CurXStatus := 0;
Visibility := VI_normal;
Cookie := '';
// ShowInfo := 2;
// WebAware := True;
fRoster := TRnQCList.Create;
SpamList := TRnQCList.Create;
SavingMyInfo.Running := False;
fECCKeys.Generated := Ecc256r1MakeKey(fECCKeys.PubEccKey, fECCKeys.PrivKey);
HttpPoll := THttpAsync.Create(58);
HttpPoll.Callback := PollRequestDone;
end;
procedure TICQSession.ResetPrefs;
var
i : Integer;
begin
pwd := '';
SupportTypingNotif := True;
IsSendTypingNotif := True;
CurXStatus := 0;
// AuthNeeded := True;
ShowClientID := True;
EnableRecentlyOffline := False;
RecentlyOfflineDelay := 15;
AddExtCliCaps := False;
ExtClientCaps := '';
TypingInterval := 5;
UseCryptMsg := True;
UseEccCryptMsg := True;
AutoReqXStatus := True;
AvatarsSupport := True;
AvatarsAutoGet := True;
AvatarsNotDnlddInform := False;
MyAvatarHash := '';
SaveToken := True;
ShowInvisSts := True;
SendBalloonOn := BALLOON_NEVER;
OnStatusDisable[byte(SC_OCCUPIED)].Blinking := True;
OnStatusDisable[byte(SC_OCCUPIED)].Sounds := True;
for i := Low(XStatusArray) to High(XStatusArray) do
begin
ExtStsStrings[i].Cap := GetTranslation(XStatusArray[i].Text);
ExtStsStrings[i].Desc := '';
end;
end;
procedure TICQSession.GetPrefs(var pp: TRnQPref);
var
s: String;
begin
if (MyAccount <> '') and (Pos(AnsiChar('@'), MyAccount) <= 0) then
pp.addPrefStr('oscar-uid', MyAccount);
pp.addPrefBool('add-client-caps', AddExtCliCaps);
pp.addPrefStr('add-client-caps-str', ExtClientCaps);
pp.addPrefInt('send-balloon-on', SendBalloonOn);
pp.addPrefDate('send-balloon-on-date', SendBalloonOnDate);
pp.addPrefBool('save-token', SaveToken);
pp.addPrefInt('typing-notify-interval', TypingInterval);
pp.addPrefBool('use-crypt-msg', UseCryptMsg);
pp.addPrefBool('use-ecc-crypt-msg', UseEccCryptMsg);
pp.addPrefBool('xstatus-auto-request', AutoReqXStatus);
pp.addPrefBool('avatars-flag', AvatarsSupport);
pp.addPrefBool('avatars-auto-load-flag', AvatarsAutoGet);
pp.addPrefBool('avatars-not-downloaded-inform-flag', AvatarsNotDnlddInform);
pp.addPrefStr('avatar-my', MyAvatarHash);
pp.addPrefBool('recently-offline-enable', EnableRecentlyOffline);
pp.addPrefInt('recently-offline-delay', RecentlyOfflineDelay);
pp.addPrefBool('show-invis-status', ShowInvisSts);
for var i in Self.GetStatusMenu do
if i <> Byte(SC_OFFLINE) then
begin
s := Status2Img[i] + '-disable-';
pp.addPrefBool(s + 'blinking', OnStatusDisable[i].Blinking);
pp.addPrefBool(s + 'tips', OnStatusDisable[i].Tips);
pp.addPrefBool(s + 'sounds', OnStatusDisable[i].Sounds);
pp.addPrefBool(s + 'openchat', OnStatusDisable[i].OpenChat);
end;
// pp.addPrefBool('auth-needed', Self.AuthNeeded);
// pp.addPrefBool('webaware', Self.WebAware);
pp.addPrefBool('show-client-id', ShowClientID);
pp.addPrefInt('xstatus', Self.CurXStatus);
// pp.addPrefInt('icq-showinfo', Self.ShowInfo);
if not (RnQstartingStatus in [Low(Status2Img)..High(Status2Img)]) then
pp.addPrefStr('starting-status', 'last_used')
else
pp.addPrefStr('starting-status', Status2Img[RnQstartingStatus]);
pp.addPrefStr('starting-visibility', Visib2Str[TVisibility(RnQStartingVisibility)]);
pp.addPrefStr('last-set-status', Status2Img[LastStatusUserSet]);
pp.addPrefBool('typing-notify-flag', SupportTypingNotif);
pp.addPrefBool('show-typing', isSendTypingNotif);
if not dontSavePwd //and not locked
then
pp.addPrefBlob64('crypted-password64', PassCrypt(UTF(pwd)))
else
pp.DeletePref('crypted-password64');
pp.DeletePref('crypted-password');
if SaveToken then
begin
pp.addPrefBlob64('crypted-session-key64', PassCrypt(fSessionKey));
pp.addPrefBlob64('crypted-auth-token64', PassCrypt(fAuthToken));
pp.addPrefInt('auth-token-time', fAuthTokenTime);
pp.addPrefInt('auth-token-expiresin', fAuthTokenExpIn);
end;
pp.addPrefInt('session-last-host-offset', fHostOffset);
pp.addPrefStr('contact-list', ReadList(LT_ROSTER).Text);
pp.addPrefStr('not-in-list', NotInList.Text);
pp.addPrefStr('ignore-list', IgnoreList.Text);
pp.addPrefStr('quiet-list', QuietList.Text);
pp.addPrefStr('retrieveq-list', RetrieveQ.Text);
if Assigned(UI) and Assigned(UI.Chat) then
pp.addPrefStr('reopen-list', UI.Chat.Pages2String);
end;
procedure TICQSession.SetPrefs(pp: TRnQPref);
var
I: Integer;
L, sU, sU2: String;
st: Byte;
begin
if pp.prefExists('crypted-password64') then
pwd := UnUTF(PassDecrypt(pp.getPrefBlob64Def('crypted-password64')))
else
pwd := PassDecrypt(pp.getPrefBlobDef('crypted-password'));
pp.getPrefBool('typing-notify-flag', SupportTypingNotif);
pp.getPrefBool('show-typing', isSendTypingNotif);
pp.getPrefStr('oscar-uid', sU);
if sU > '' then
MyAccount := sU;
pp.getPrefBool('add-client-caps', AddExtCliCaps);
ExtClientCaps := pp.getPrefStrDef('add-client-caps-str');
// if not (Length(ExtClientCaps) = 32) then
// ExtClientCaps := '00000000000000000000000000000000;
// authneeded := pp.getPrefBoolDef('auth-needed', AuthNeeded);
// webaware := pp.getPrefBoolDef('webaware', WebAware);
// showInfo := pp.getPrefIntDef('icq-showinfo', ShowInfo);
i := pp.getPrefIntDef('xstatus');
if i >= 0 then
if (i in [Low(XStatusArray)..High(XStatusArray)]) then
CurXStatus := i
else
CurXStatus := 0;
pp.getPrefInt('send-balloon-on', SendBalloonOn);
pp.getPrefDate('send-balloon-on-date', SendBalloonOnDate);
for st := Byte(Low(tICQstatus)) to Byte(High(tICQstatus)) do
with OnStatusDisable[Byte(st)] do
begin
sU2 := status2Img[st] + '-disable-';
sU := sU2 + 'blinking';
pp.getPrefBool(sU, Blinking);
sU := sU2 + 'tips';
pp.getPrefBool(sU, Tips);
sU := sU2 + 'sounds';
pp.getPrefBool(sU, Sounds);
sU := sU2 +'openchat';
pp.getPrefBool(sU, OpenChat);
end;
pp.getPrefBool('recently-offline-enable', EnableRecentlyOffline);
pp.getPrefInt('recently-offline-delay', RecentlyOfflineDelay);
pp.getPrefBool('save-token', SaveToken);
if SaveToken then
begin
fSessionKey := passDecrypt(pp.getPrefBlob64Def('crypted-session-key64'));
fAuthToken := passDecrypt(pp.getPrefBlob64Def('crypted-auth-token64'));
pp.getPrefInt('auth-token-time', fAuthTokenTime);
pp.getPrefInt('auth-token-expiresin', fAuthTokenExpIn);
end;
pp.getPrefInt('session-last-host-offset', fHostOffset);
pp.getPrefInt('typing-notify-interval', TypingInterval);
pp.getPrefBool('use-crypt-msg', UseCryptMsg);
pp.getPrefBool('use-ecc-crypt-msg', UseEccCryptMsg);
pp.getPrefBool('xstatus-auto-request', AutoReqXStatus);
pp.getPrefBool('avatars-flag', AvatarsSupport);
pp.getPrefBool('avatars-auto-load-flag', AvatarsAutoGet);
pp.getPrefBool('avatars-not-downloaded-inform-flag', AvatarsNotDnlddInform);
pp.getPrefBool('show-invis-status', ShowInvisSts);
pp.getPrefBool('show-client-id', ShowClientID);
L := pp.getPrefStrDef('starting-status');
if l='last_used' then
RnQstartingStatus := -1
else
RnQstartingStatus := Str2Status(l);
L := pp.getPrefStrDef('starting-visibility');
RnQStartingVisibility := Byte(Str2Visibility(l));
L := pp.getPrefStrDef('last-set-status');
LastStatusUserSet := Str2Status(l);
pp.getPrefStr('contact-list', sU);
fRoster.Free;
fRoster := TRnQCList.FromArray(sU.Split([#10]));
pp.getPrefStr('not-in-list', sU);
NotInList.Free;
NotInList := TRnQCList.FromArray(sU.Split([#10]));
pp.getPrefStr('ignore-list', sU);
IgnoreList.Free;
IgnoreList := TRnQCList.FromArray(sU.Split([#10]));
pp.getPrefStr('quiet-list', sU);
QuietList.Free;
QuietList := TRnQCList.FromArray(sU.Split([#10]));
pp.getPrefStr('retrieveq-list', sU);
RetrieveQ.Free;
RetrieveQ := TRnQCList.FromArray(sU.Split([#10]));
Visibility := TVisibility(RnQStartingVisibility);
MyAvatarHash := pp.getPrefStrDef('avatar-my');
if ContactsDB.IdxOf(MyAccount) >= 0 then
GetMyInfo.IconID := MyAvatarHash;
ApplyBalloon;
end;
procedure TICQSession.Clear;
begin
ReadList(LT_ROSTER).Clear;
ReadList(LT_SPAM).Clear;
FreeAndNil(eventContacts);
eventContact := nil;
end;
destructor TICQSession.Destroy;
begin
fRoster.Free;
SpamList.Free;
FreeAndNil(PollingTask);
FreeAndNil(ReconnectTask);
FreeAndNil(HttpPoll);
FreeAndNil(ContactsDB);
inherited;
end;
function TICQSession.GetMyInfo: TICQContact;
begin
Result := ContactsDB.Add(MyAccount);
end;
function TICQSession.GetStartInfo: TICQContact;
begin
Result := ContactsDB.Add(StartAccount);
end;
function TICQSession.IsMyAcc(c: TICQContact): Boolean;
begin
Result := Assigned(c) and c.equals(MyAccount)
end;
function TICQSession.CanAddCntOutOfGroup : Boolean;
begin
Result := False;
end;
function TICQSession.PwdEqual(const Pass: String) : Boolean;
begin
Result := (not (Pass = '') and (Pass = fPwd));
end;
function TICQSession.GetPwd: String;
begin
Result := fPwd;
end;
function TICQSession.RequiresLogin: Boolean;
begin
Result := (fSessionKey = '') or (fAuthToken = '') or (fAuthTokenTime = 0) or
(not (fAuthTokenExpIn = 0) and (fAuthTokenTime + fAuthTokenExpIn < DateTimeToUnix(Now, False)));
end;
//function TICQSession.RESTAvailable: Boolean;
//begin
// Result := not (fRESTToken = '') and not (fRESTClientId = '');
//end;
procedure TICQSession.SetPwd(const Value: String);
begin
if (Length(Value) <= MaxPwdLength) then
if not (Value = fPwd) then
fPwd := Value;
end;
procedure TICQSession.SetCurXStatus(XStatus: Byte);
begin
CurXStatusVal := XStatus;
UI.CL.UpdateAdditionalImage;
end;
procedure TICQSession.InitListenerVars(Contact: TICQContact = nil; Time: TDateTime = 0);
begin
eventContact := Contact;
eventTime := Time;
eventFlags := 0;
eventMsgID := 0;
eventWID := '';
eventMsgA := '';
end;
procedure TICQSession.NotifyListeners(ev: TICQEvent);
begin
if Assigned(Listener) then
Listener(Self, Integer(ev));
end;
function TICQSession.IsOffline: Boolean;
begin
Result := Phase = null_
end;
function TICQSession.IsOnline: Boolean;
begin
Result := Phase = online_
end;
function TICQSession.IsConnecting: Boolean;
begin
Result := (Phase <> online_) and (Phase <> null_)
end;
procedure TICQSession.GoneOffline;
begin
if Phase = null_ then
Exit;
Phase := null_;
CurStatus := SC_OFFLINE;
fRoster.ForEach(procedure(cnt: TICQContact)
begin
cnt.OfflineClear;
cnt.Status := SC_UNK;
end);
NotifyListeners(IE_offline);
end;
procedure TICQSession.Disconnect;
begin
CleanDisconnect := True;
if Phase = online_ then
EndSession(not SaveToken)
else
Phase := null_;
end;
function TICQSession.IsReady: Boolean;
begin
Result := Phase in [settingup_, online_]
end;
function TICQSession.IsSSCL: Boolean;
begin
Result := True;
end;
function TICQSession.IsMobileAccount: Boolean;
begin
Result := String(StartAccount).StartsWith('+');
end;
function TICQSession.SendSessionRequest(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String; const Header: AnsiString = '';
const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean;
var
JSON: TJSONObject;
begin
Result := SendSessionRequest(IsPOST, BaseURL, Query, RT_None, JSON, Header, ErrMsg, ErrProc);
end;
function TICQSession.SendSessionRequest(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String; Ret: TReturnData;
out JSON: TJSONObject; const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean;
begin
if fAimSid = '' then
Exit(False);
Result := SendRequest(IsPOST, BaseURL, 'f=json&aimsid=' + fAimSid + '&r=' + CreateNewGUID + Query, Ret, JSON, Header, ErrMsg, ErrProc);
end;
function TICQSession.SendRAPIRequest(JSONReq: Boolean; const Method: String; Params: Pointer;
const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean;
var
JSON: TJSONObject;
begin
Result := SendRAPIRequest(JSONReq, Method, Params, RT_None, JSON, Header, ErrMsg, ErrProc);
end;
function TICQSession.SendRAPIRequest(JSONReq: Boolean; const Method: String; Params: Pointer; Ret: TReturnData;
out JSON: TJSONObject; const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean;
var
JSONQ: TJSONObject;
ParamsDic: TDictionary;
Query: UTF8String;
BaseURL: String;
begin
JSON := nil;
if fAimSid = '' then
Exit(False);
BaseURL := RAPI_HOST + Method;
if JSONReq then
begin
JSONQ := TJSONObject.Create;
try
// JSONQ.AddPair('f', 'json');
// JSONQ.AddPair('k', fDevId);
// JSONQ.AddPair('client', 'icq');
// JSONQ.AddPair('lang', IfThen(IsRuLang, 'ru-ru', 'en-us'));
JSONQ.AddPair('aimsid', fAimSid);
JSONQ.AddPair('reqId', CreateNewGUID);
if Assigned(Params) then
begin
JSONQ.AddPair('params', TJSONObject(Params^));
TJSONObject(Params^).Owned := False;
end;
Query := JSONQ.ToString;
finally
FreeAndNil(JSONQ);
end;
end
else
begin
if Assigned(Params) then
ParamsDic := TDictionary(Params^)
else
ParamsDic := TDictionary.Create;
ParamsDic.Add('f', 'json');
ParamsDic.Add('k', fDevId);
ParamsDic.Add('client', 'icq');
ParamsDic.Add('lang', IfThen(IsRuLang, 'ru-ru', 'en-us'));
ParamsDic.Add('aimsid', fAimSid);
ParamsDic.Add('reqId', CreateNewGUID);
Query := MakeParams(ParamsDic);
if not Assigned(Params) then
FreeAndNil(ParamsDic);
end;
Result := SendRequest(True, BaseURL, Query, Ret, JSON, Header, ErrMsg, ErrProc);
end;
function TICQSession.SendRequest(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String; const Header: AnsiString = '';
const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean;
var
JSON: TJSONObject;
begin
Result := SendRequest(IsPOST, BaseURL, Query, RT_None, JSON, Header, ErrMsg, ErrProc);
end;
function TICQSession.SendRequest(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String; Ret: TReturnData; out JSON: TJSONObject;
const Header: AnsiString = ''; const ErrMsg: String = ''; const ErrProc: TErrorProc = nil): Boolean;
var
Method, RespStr: String;
Resp: TPair;
begin
JSON := nil;
Result := False;
if not Running then
Exit;
Method := IfThen(IsPOST, 'POST', 'GET');
eventNameA := Method;
eventMsgA := Header;
eventData := BaseURL + IfThen(IsPOST, #10, '?') + Query;
NotifyListeners(IE_serverGot);
if IsPOST then
LoadFromURLAsString(BaseURL, RespStr, Query)
else
LoadFromURLAsString(BaseURL + '?' + Query, RespStr);
if not (Trim(RespStr) = '') then
begin
eventNameA := Method;
eventMsgA := Header;
eventData := RespStr;
NotifyListeners(IE_serverSent);
end;
if not ParseJSON(RespStr, JSON) then
Exit;
try
if BaseURL.StartsWith(RAPI_HOST) then
begin
Resp := CheckRAPIData(JSON);
if Resp.Key = Ord(ERAPI_OK) then
Result := True
end
else
begin
Resp := CheckResponseData(JSON);
if Resp.Key = Ord(EAC_OK) then
Result := True
end;
if not Result then
if Assigned(ErrProc) then
ErrProc(Resp)
else if not (ErrMsg = '') then
MsgDlg(Format(GetTranslation(ErrMsg) + #13#10 + GetTranslation('Server returned error:') + #13#10 + '%s', [Resp.Value]), False, mtError)
else
begin
// Too many useless errors :)
//eventInt := Resp.Key;
//eventMsgA := Resp.Value;
//eventError := EC_other;
//NotifyListeners(IE_error);
end;
finally
if Ret = RT_None then
FreeAndNil(JSON);
end;
end;
procedure TICQSession.SendRequestAsync(IsPOST: Boolean; const BaseURL: String; const Query: UTF8String; const Header: AnsiString = ''; HandlerProc: THandlerProc = nil);
var
Method: String;
begin
Method := IfThen(IsPOST, 'POST', 'GET');
eventNameA := Method;
eventMsgA := Header;
eventData := BaseURL + '?' + Query;
NotifyListeners(IE_serverGot);
TThread.CreateAnonymousThread(procedure
var
RespStr: String;
begin
if IsPOST then
LoadFromURLAsString(BaseURL, RespStr, Query)
else
LoadFromURLAsString(BaseURL + '?' + Query, RespStr);
TThread.Queue(nil, procedure
begin
if not Running then
Exit;
if Assigned(HandlerProc) then
HandlerProc(RespStr);
eventNameA := Method;
eventMsgA := Header;
eventData := RespStr;
NotifyListeners(IE_serverSent);
end);
end).Start;
end;
function TICQSession.SendPresenceState(ForceOnline: Boolean = False): Boolean;
var
Query: UTF8String;
BaseURL, Status: String;
begin
Result := False;
BaseURL := WIM_HOST + 'presence/setState';
Status := IfThen(not ForceOnline and IsInvisible, 'offline', Status2Srv[Byte(curStatus)]);
Query := '&view=' + Status +
'&invisible=' + IfThen(not ForceOnline and IsInvisible, '1', '0');
//IfThen(curStatus = SC_AWAY, '&away=Seeya', ''); // Not really useful, only you receive your awayMsg :)
if not (Status = 'offline') then
Query := Query + '&assertCaps=' + ParamEncode(GetMyCaps);
if SendSessionRequest(False, BaseURL, Query, 'Set status and visibility', 'Failed to set status') then
begin
// Not needed, same info as in myInfo in fetched event
//ProcessContaсt(json.GetValue('myInfo') as TJSONObject)
Result := True;
end;
end;
procedure TICQSession.SendStatus(XStatus: Integer = -1; const StText: String = '');
var
TmpStr: String;
Params: TJSONObject;
begin
eventContact := nil;
if not (XStatus in [Low(XStatusArray)..High(XStatusArray)]) then
XStatus := CurXStatus
else
CurXStatus := XStatus;
if StText <> ExtStsStrings[XStatus].Desc then
begin
ExtStsStrings[XStatus].Desc := StText;
ActionManager.Execute(AK_SAVEXSTATUSES, SaveDelay);
end;
eventInt := XStatus;
curXStatusStr.Cap := ExtStsStrings[XStatus].Cap;
curXStatusStr.Desc := ExtStsStrings[XStatus].Desc;
eventNameA := UTF(ExtStsStrings[XStatus].Cap);
eventMsgA := UTF(ExtStsStrings[XStatus].Desc);
NotifyListeners(IE_sendingXStatus);
TmpStr := UnUTF(eventNameA);
if curXStatusStr.Cap <> TmpStr then
curXStatusStr.Cap := TmpStr;
TmpStr := UnUTF(eventMsgA);
if curXStatusStr.Desc <> TmpStr then
curXStatusStr.Desc := TmpStr;
ActionManager.Execute(AK_SAVECONFIG, SaveDelay);
// Do not change msg if invisible, it generates "offline" presence event
if not IsReady or IsInvisible then
Exit;
Params := TJSONObject.Create;
if XStatusArray[XStatus].Status = '' then
Params.AddPair('type', 'empty')
else
begin
Params.AddPair('media', XStatusArray[XStatus].Status);
Params.AddPair('text', CurXStatusStr.Desc);
Params.AddPair('type', 'emoji');
//Params.AddPair('duration', TJSONNumber.Create(3600));
end;
if SendRAPIRequest(True, 'status/set', @Params, 'Set xstatus', 'Failed to set xstatus') then
begin
GetMyInfo.StatusStr := CurXStatusStr.Desc;
// Not needed, same info as in myInfo in fetched event
//ProcessContaсt(json.GetValue('myInfo') as TJSONObject)
end;
Params.Free;
// BaseURL := WIM_HOST + 'presence/setStatus';
// Query := '&statusMsg=' + ParamEncode(CurXStatusStr.Desc); // &title=moodTitle
// if SendSessionRequest(True, BaseURL, Query, 'Set status string', 'Failed to set status message') then
// begin
// GetMyInfo.StatusStr := CurXStatusStr.Desc;
// // Not needed, same info as in myInfo in fetched event
// //ProcessContaсt(json.GetValue('myInfo') as TJSONObject)
// end;
end;
//procedure TICQSession.SendXStatus;
//var
// Params: TJSONObject;
//begin
// Params := TJSONObject.Create;
// Params.AddPair('status', ...);
//
// SendRAPIRequest(True, 'setStatus', @Params, 'Set xstatus', 'Failed to set xstatus');
//
// Params.Free;
//end;
procedure CalcKey(IsEcc: Boolean; const EccKey, u1, u2: RawByteString; l1, l2: Int64; var Key: TSHA256Digest);
var
sr: RawByteString;
begin
if isEcc then
Pbkdf2HmacSha256(EccKey, not2Translate[2] + AIM_MD5_STRING + IntToHex(l1, 2) + u1 + IntToHex(l2, 2) + u2, 3, Key)
else
begin
sr := MD5Pass(IntToHex(l1, 2) + not2Translate[2] + u1 + AIM_MD5_STRING);
CopyMemory(@Key[0], @sr[1], SizeOf(TMD5Digest));
sr := MD5Pass(IntToHex(l2, 2) + not2Translate[2] + u2 + AIM_MD5_STRING);
CopyMemory(@Key[16], @sr[1], SizeOf(TMD5Digest));
end;
end;
procedure TICQSession.SendMsg(Cnt: TICQContact; Kind: Integer; Flags: DWord; const HistMsg: String; const Msg: String; Update: TMsgID = 0);
var
Msg2, Msg2Enc, CrptMsg: TBytes;
ReadyMsg: String;
Key: TSHA256Digest;
Ctx: TAESECB;
i, Len, Len2, Compressed, Encrypted: Integer;
CRC: Cardinal;
ShouldEncrypt, IsBin, IsSticker: Boolean;
Params: TDictionary;
BaseURL: String;
Handler: THandlerProc;
OEv: TOEvent;
begin
if not IsReady then
Exit;
ReqID := AddRef(REF_msg, Cnt.UID);
OEv := Account.acks.Add(Kind, Cnt, Flags, 'MSG');
OEv.sID := CreateNewGUID;
OEv.ID := ReqID;
IsBin := (Pos(RnQImageTag, Msg) > 0) or ((Pos(RnQImageExTag, Msg) > 0)) or (Flags and IF_Bin > 0);
if IsBin then
Flags := Flags or IF_Bin;
// if not ImVisible then
// if AddTempVisMsg then
// AddTemporaryVisible(c); // TODO: New proto implementation
IsSticker := Flags and IF_sticker > 0;
if Cnt.SendTransl and not IsBin and not IsSticker then
ReadyMsg := Translit(Msg)
else
ReadyMsg := Msg;
// Encrypted := 0;
ShouldEncrypt := (UseCryptMsg and (Cnt.Crypt.SupportCryptMsg or (fECCKeys.Generated and UseEccCryptMsg and Cnt.Crypt.SupportEcc))) and not IsBin;
if ShouldEncrypt and not IsSticker then
begin
Msg2 := TEncoding.UTF8.GetBytes(ReadyMsg);
Len := Length(Msg2);
CRC := ZipCrc32($FFFFFFFF, @Msg2[0], Len) XOR $FFFFFFFF;
Compressed := 0;
Msg2Enc := ZCompressBytes(Msg2);
if Assigned(Msg2Enc) then
if Length(Msg2Enc) < Len then
begin
Msg2 := Msg2Enc;
Compressed := 1;
end;
CalcKey(fECCKeys.Generated and UseEccCryptMsg and Cnt.Crypt.SupportEcc, Cnt.Crypt.EccMsgKey, MyAccount, Cnt.UID, 0, Len, Key);
i := Len mod AESBLKSIZE;
if (i > 0) then
begin
Len2 := Len + AESBLKSIZE - i;
SetLength(Msg2, Len2);
FillChar(Msg2[Len], AESBLKSIZE - i, 0);
end else
Len2 := Len;
SetLength(CrptMsg, Len2);
Ctx := TAESECB.Create(Key[0], 256);
Ctx.Encrypt(@Msg2[0], @CrptMsg[0], Len2);
Ctx.Free;
SetLength(Msg2, 0);
Base64EncodeBytes(CrptMsg, Msg2);
flags := flags or IF_Encrypt;
if fECCKeys.Generated and UseEccCryptMsg and Cnt.Crypt.SupportEcc then
begin
Encrypted := 2;
flags := flags or IF_Encrypt_ECC;
end else
Encrypted := 1;
if Encrypted = 2 then
ReadyMsg := CreateDataPayload([
String2Hex('RDEC0' + Copy(Cnt.Crypt.EccPubKey, 1, 11)),
String2Hex('RDEC1' + Copy(Cnt.Crypt.EccPubKey, 12, 11)),
String2Hex('RDEC2' + Copy(Cnt.Crypt.EccPubKey, 23, 11))
], Msg2, Compressed, CRC, Len)
else if Encrypted = 1 then
ReadyMsg := CreateDataPayload([String2Hex(BigCapability[CAPS_big_CryptMsg].v)], Msg2, Compressed, CRC, Len);
end;
// else
// if UseCryptMsg and (CAPS_big_QIP_Secure in Cnt.capabilitiesBig) and (Cnt.Crypt.qippwd > 0) and not IsBin then
// begin // QIP crypt message
// // Still relevant?
//
// Msg2Send := qip_msg_crypt(msg2send, c.Crypt.qippwd);
//// sutf := Length_DLE(GUIDToString(msgQIPpass));
// sutf := Length_DLE(msgQIPpassStr);
// flags := flags or IF_Encrypt;
// end;
if Update = 0 then
if IsBin then
AddOutgoingMessage(Cnt, '', HistMsg, Now, Flags, ReqID)
else
AddOutgoingMessage(Cnt, HistMsg, '', Now, Flags, ReqID);
if ReadyMsg = '' then
Exit;
Handler := procedure(const RespStr: String)
var
MsgID: TMsgID;
State, sTmp: String;
iTmp: Integer;
JSON: TJSONObject;
Resp: TPair;
begin
if ParseJSON(RespStr, JSON) then
try
Resp := CheckResponseData(JSON, eventData);
if not (Resp.Key = Ord(EAC_OK)) then
begin
if Update > 0 then
MsgDlg(GetTranslation('Failed to update message on server') + #10 + '[' + IntToStr(Resp.Key) + '] ' + Resp.Value, False, mtError);
Exit;
end;
JSON.GetValueSafe('state', State);
if State = '' then
Exit;
eventFlags := 0;
eventMsgA := State;
if JSON.GetValueSafe('ts', iTmp) then
eventTime := UnixToDateTime(iTmp, False);
if JSON.GetValueSafe('msgId', sTmp) then
eventWID := sTmp;
if JSON.GetValueSafe('histMsgId', MsgID) then
eventMsgID := MsgID;
NotifyListeners(IE_serverAck);
finally
FreeAndNil(JSON);
end;
end;
Params := TDictionary.Create;
Params.Add('f', 'json');
Params.Add('aimsid', fAimSid);
Params.Add('t', Cnt.UID);
Params.Add('r', OEv.sID);
// Access to im/sendDataIM is forbidden :(
// if Encrypted > 0 then
// begin
// if Encrypted = 2 then
// Params.Add('cap', RDEC_CAPS)
// else
// Params.Add('cap', String2Hex(BigCapability[CAPS_big_CryptMsg].v));
//
// Params.Add('type', 'data');
// Params.Add('data', Msg2);
// Params.Add('dataIsBase64', '1');
// Params.Add('hostCheck', '1');
// BaseURL := WIM_HOST + 'im/sendDataIM';
// SendRequestAsync(BaseURL, MakeParams('POST', BaseURL, Params));
// end
// parts[quotes], mentions
Params.Add(IfThen(IsSticker, 'stickerId', 'message'), ReadyMsg);
// (is_sms)
// 'displaySMSSegmentData': 'true'
// else
Params.Add('offlineIM', '1');
Params.Add('notifyDelivery', 'true');
if Update > 0 then
Params.Add('updateMsgId', UIntToStr(Update));
BaseURL := WIM_HOST + IfThen(IsSticker, 'im/sendSticker', 'im/sendIM');
SendRequestAsync(True, BaseURL, MakeParams(Params), 'Send ' + IfThen(IsSticker, 'sticker', 'message'), Handler);
Params.Free;
if IsInvisible then
begin
SendPresenceState(True);
ActionManager.Execute(AK_SENDSTATUS, 1000);
end;
end;
function TICQSession.CreateDataPayload(Caps: TArray; const Data: TBytes = nil; Compressed: Integer = -1; CRC: Cardinal = 0; Len: Integer = 0): String;
var
JSON: TJSONObject;
CapsArr: TJSONArray;
begin
Result := TEncoding.UTF8.GetString(Data);
JSON := TJSONObject.Create;
try
CapsArr := TJSONArray.Create;
for var Cap in Caps do
CapsArr.Add(Cap);
JSON.AddPair('type', 'RnQDataIM');
JSON.AddPair('caps', CapsArr);
if Assigned(Data) then
JSON.AddPair('data', TEncoding.ANSI.GetString(Data));
if not (Compressed = -1) then
JSON.AddPair('compressed', TJSONNumber.Create(Compressed));
if not (CRC = 0) then
JSON.AddPair('crc', TJSONNumber.Create(CRC));
if not (Len = 0) then
JSON.AddPair('length', TJSONNumber.Create(Len));
Result := JSON.ToString;
finally
JSON.Free;
end;
end;
function TICQSession.SendBuzz(Cnt: TICQContact): Boolean;
var
Params: TDictionary;
BaseURL: String;
begin
Result := False;
if not IsReady or (SecondsBetween(Now, buzzedLastTime) < 15) then
Exit;
BuzzedLastTime := Now;
Params := TDictionary.Create;
try
Params.Add('f', 'json');
Params.Add('aimsid', fAimSid);
Params.Add('t', cnt.UID);
Params.Add('r', CreateNewGUID);
Params.Add('message', CreateDataPayload([String2Hex(BigCapability[CAPS_big_Buzz].v)]));
Params.Add('offlineIM', '1');
Params.Add('notifyDelivery', 'true');
BaseURL := WIM_HOST + 'im/sendIM';
SendRequestAsync(True, BaseURL, MakeParams(Params), 'Send buzz');
Result := True;
finally
Params.Free;
end;
end;
function TICQSession.SendDecryptError(Cnt: TICQContact; MsgID: TMsgID): Boolean;
var
Params: TDictionary;
BaseURL: String;
begin
Result := False;
if not IsReady then
Exit;
Params := TDictionary.Create;
try
Params.Add('f', 'json');
Params.Add('aimsid', fAimSid);
Params.Add('t', cnt.UID);
Params.Add('r', CreateNewGUID);
Params.Add('message', CreateDataPayload([String2Hex(CapsInvalidPublicKey)], TEncoding.ASCII.GetBytes(UIntToStr(MsgID))));
Params.Add('offlineIM', '1');
Params.Add('notifyDelivery', 'true');
BaseURL := WIM_HOST + 'im/sendIM';
SendRequestAsync(True, BaseURL, MakeParams(Params), 'Send decryption error notification');
Result := True;
finally
Params.Free;
end;
end;
procedure TICQSession.GetProfile(const UID: TUID);
var
JSON: TJSONObject;
BaseURL: String;
Params: TDictionary;
begin
if not IsReady or (UID = '') then
Exit;
GetContactAttrs(UID);
BaseURL := WIM_HOST_FALLBACK + 'presence/get';
Params := TDictionary.Create();
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('r', CreateNewGUID);
Params.Add('mdir', '1');
Params.Add('t', UID);
try
if SendRequest(False, BaseURL, MakeParams(Params) + AllFieldsAsQuery, RT_JSON, JSON, 'Get contact [' + String(UID) + '] info') then
ProcessUsersAndGroups(JSON);
finally
JSON.Free;
end;
Params.Free;
end;
procedure TICQSession.GetUserInfo(const UID: TUID);
var
Params, Results: TJSONObject;
begin
if not IsReady or (UID = '') then
Exit;
GetContactAttrs(UID);
Params := TJSONObject.Create;
Params.AddPair('sn', UID);
if SendRAPIRequest(True, 'getUserInfo', @Params, RT_JSON, Results, 'Contact info') then
if Assigned(Results) then
try
Results.AddPair('aimId', UID);
ProcessContact(Results);
finally
FreeAndNil(Results);
end;
Params.Free;
end;
function TICQSession.GetLastSeen(const UIDs: TUIDS): Boolean;
var
Params, Results, TmpObj: TJSONObject;
IDs, Entries: TJSONArray;
Entry: TJSONValue;
Contact: TICQContact;
UnixTime: Integer;
ErrHandler: TErrorProc;
begin
Result := True;
if not IsReady or (Length(UIDs) = 0) then
Exit;
IDs := TJSONArray.Create;
for var UID in UIDs do
if not (UID = '') then
IDs.Add(UID);
if IDs.Count = 0 then
begin
IDs.Free;
Exit;
end;
Result := False;
Params := TJSONObject.Create;
Params.AddPair('ids', IDs);
ErrHandler := procedure(Resp: TPair)
begin
//if Resp.Key = Ord(ERAPI_Rate_Limit) then
end;
if SendRAPIRequest(True, 'getUserLastseen', @Params, RT_JSON, Results, 'Get contacts last seen', '', ErrHandler) then
if Assigned(Results) then
try
Result := True;
Entries := Results.GetValue('entries') as TJSONArray;
if not Assigned(Entries) then
Exit;
for Entry in Entries do
begin
TmpObj := Entry as TJSONObject;
if not (TmpObj.GetValue('userState') = nil) then
if (TmpObj.GetValue('userState') as TJSONObject).GetValueSafe('lastseen', UnixTime) then
begin
Contact := GetICQContact(TmpObj.GetValue('sn').Value);
if Assigned(Contact) then
begin
if not (UnixTime = 0) then
Contact.LastTimeSeenOnline := UnixToDateTime(UnixTime, False);
if not Contact.Official then
ProcessNewStatus(Contact, THelpers.IfThen(UnixTime = 0, SC_ONLINE, SC_OFFLINE));
end;
end;
end;
finally
FreeAndNil(Results);
end;
Params.Free;
end;
procedure TICQSession.GetContactAttrs(const UID: TUID);
var
Contact: TICQContact;
JSON: TJSONObject;
Query: UTF8String;
BaseURL: String;
begin
BaseURL := WIM_HOST + 'buddylist/getBuddyAttribute';
Query := '&buddy=' + ParamEncode(String(UID));
try
if SendSessionRequest(False, BaseURL, Query, RT_JSON, JSON, 'Get contact [' + String(UID) + '] attributes') then
begin
Contact := GetICQContact(UID);
if Assigned(Contact) then
with JSON do
begin
GetValueSafe('note', Contact.ssImportant);
GetValueSafe('smsNumber', Contact.ssCell1);
GetValueSafe('workNumber', Contact.ssCell2);
GetValueSafe('phoneNumber', Contact.ssCell3);
GetValueSafe('otherNumber', Contact.ssCell4);
GetValueSafe('friendly', Contact.ssNickname)
end;
end;
finally
JSON.Free;
end;
end;
procedure TICQSession.SendContactAttrs(c: TICQContact);
var
BaseURL: String;
Params: TDictionary;
begin
Params := TDictionary.Create();
BaseURL := WIM_HOST + 'buddylist/setBuddyAttribute';
Params.Clear;
Params.Add('buddy', String(c.UID));
if not IsMyAcc(c) then // Returns error for own contact
Params.Add('friendly', c.ssNickname);
Params.Add('note', c.ssImportant); // Not working, value stays unchanged on server
Params.Add('smsNumber', c.ssCell1);
Params.Add('workNumber', c.ssCell2);
Params.Add('phoneNumber', c.ssCell3);
Params.Add('otherNumber', c.ssCell4);
if SendSessionRequest(True, BaseURL, '&' + MakeParams(Params), 'Save my contact attributes', 'Failed to save your contact attributes') then
NotifyListeners(IE_MyInfoAck);
Params.Free;
end;
procedure TICQSession.GetContactInfo(const UID: TUID; const IncludeField: String);
var
JSON: TJSONObject;
BaseURL: String;
Params: TDictionary;
begin
if not IsReady or (IncludeField = '') then
Exit;
BaseURL := WIM_HOST_FALLBACK + 'presence/get';
Params := TDictionary.Create();
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('r', CreateNewGUID);
Params.Add('mdir', '0');
Params.Add('t', UID);
Params.Add(IncludeField, '1'); // No profile, but still some other fields are there
try
if SendRequest(False, BaseURL, MakeParams(Params), RT_JSON, JSON, 'Get contact [' + String(UID) + '] info [' + IncludeField + ']') then
ProcessUsersAndGroups(JSON);
finally
JSON.Free;
end;
Params.Free;
end;
procedure TICQSession.GetCL;
var
JSON: TJSONObject;
BaseURL: String;
Params: TDictionary;
begin
if not IsReady then
Exit;
// BaseURL := WIM_HOST + 'buddylist/get';
// Query := '&includeBuddies=0'; // groups+users or groups only
BaseURL := WIM_HOST_FALLBACK + 'presence/get';
Params := TDictionary.Create();
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('r', CreateNewGUID);
Params.Add('mdir', '1');
Params.Add('bl', '1');
try
if SendRequest(False, BaseURL, MakeParams(Params) + AllFieldsAsQuery, RT_JSON, JSON, 'Get CL', 'Failed to get CL') then
begin
ProcessContactList(JSON.GetValue('groups') as TJSONArray, True);
UI.CL.FinishBuild;
end;
finally
JSON.Free;
end;
Params.Free
end;
function TICQSession.SearchContact(const Keyword: String; out Anketa: TAnketa): Boolean;
var
Params: TDictionary;
Results: TJSONObject;
People: TJSONArray;
Contact, ContactAnketa: TJSONObject;
begin
Result := False;
if fAimSid = '' then
Exit;
Params := TDictionary.Create();
Params.Add('keyword', Keyword);
//Params.Add('phonenum', '');
if SendRAPIRequest(False, 'search', @Params, RT_JSON, Results, 'Search contact') then
if Assigned(Results) then
try
People := TJSONArray(Results.GetValue('data'));
if People.Count > 0 then
begin
Anketa.UID := '';
Contact := TJSONObject(People.Items[0]);
if not Contact.TryGetValue('sn', Anketa.UID) or (Anketa.UID = '') then
Exit;
if not Contact.TryGetValue('bot', Anketa.Bot) then
Anketa.Bot := False;
ContactAnketa := TJSONObject(Contact.GetValue('anketa'));
ContactAnketa.TryGetValue('firstName', Anketa.First);
ContactAnketa.TryGetValue('nickname', Anketa.Nick);
ContactAnketa.TryGetValue('friendly', Anketa.Friendly);
Anketa.AvatarURL := BIN_FILES_HOST + 'avatar/get?targetSn=' + Anketa.UID + '&size=64';
Result := True;
end;
finally
FreeAndNil(Results);
end;
Params.Free;
end;
procedure TICQSession.ValidateSid;
var
BaseURL: String;
begin
if not IsReady then
Exit;
BaseURL := WIM_HOST + 'aim/validateSid';
SendSessionRequest(False, BaseURL, '', 'Validate AimSid');
end;
procedure TICQSession.GetExpressions; // Avatars only?
var
Query: UTF8String;
BaseURL: String;
begin
BaseURL := WIM_HOST + 'expressions/get2'; // expressions/get
Query := 'f=json' +
'&t=' + ParamEncode(String('230490'));
SendRequest(False, BaseURL, Query, 'Get expressions');
end;
function TICQSession.GetSpeechToText(const FileId: String): String;
var
JSON: TJSONObject;
Query: UTF8String;
BaseURL: String;
Status: Integer;
begin
Result := '';
if not IsReady or (FileId = '') then
Exit;
BaseURL := FILES_HOST + 'speechtotext/' + FileId;
Query := 'locale=ru' +
'&type=ptt' +
'&k=' + fDevId;
SendSessionRequest(False, BaseURL, Query, RT_JSON, JSON, 'Get audio file transcription');
if Assigned(JSON) then
try
JSON.GetValueSafe('status', Status);
if Status = 200 then
JSON.GetValueSafe('text', Result);
finally
FreeAndNil(JSON);
end;
end;
procedure TICQSession.GetAllCaps;
var
JSON: TJSONObject;
Query, SubQuery: UTF8String;
BaseURL: String;
Params: TDictionary;
Contact: TICQContact;
I, Cnt: Integer;
begin
if not IsReady or (fRoster.Count = 0) then
Exit;
BaseURL := WIM_HOST_FALLBACK + 'presence/get';
Params := TDictionary.Create();
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('r', CreateNewGUID);
Params.Add('capabilities', '1');
Query := MakeParams(Params);
Params.Free;
Cnt := 0;
SubQuery := Query;
for I := 0 to fRoster.Count - 1 do
begin
Contact := fRoster.getAt(I);
if not (Contact.Status in [SC_OFFLINE, SC_UNK]) then
begin
SubQuery := SubQuery + '&t=' + String(Contact.UID);
Inc(Cnt);
end;
if (Cnt >= 100) or ((Cnt > 0) and (I = fRoster.Count - 1)) then
begin
try
if SendRequest(True, BaseURL, SubQuery, RT_JSON, JSON, 'Get caps for all online contacts') then
ProcessUsersAndGroups(JSON);
finally
JSON.Free;
end;
Cnt := 0;
SubQuery := Query;
end;
end;
end;
procedure TICQSession.GetAllLastSeen;
var
Contact: TICQContact;
I, Cnt: Integer;
begin
if not IsReady or (fRoster.Count = 0) then
Exit;
Cnt := 0;
for Contact in fRoster do
if ((Contact.UserType = CT_ICQ) or (Contact.UserType = CT_OLDICQ)) and not (Contact.Status in [SC_OFFLINE, SC_UNK]) then
begin
ActionManager.AddContact(Contact.UID);
Inc(Cnt);
if Cnt >= 100 then
Break;
end;
if Cnt > 0 then
ActionManager.Execute(AK_GETLASTSEEN, 5000);
end;
procedure TICQSession.Test;
var
Query{, Params}: UTF8String;
BaseURL: String;
// Params: TDictionary;
begin
// BaseURL := WIM_HOST + 'aim/getSMSInfo'; // Server error
// Query := '&phone=911';
// BaseURL := WIM_HOST + 'memberDir/get';
// Query := '&locale=en-us&t=230490&infoLevel=full';
SendSessionRequest(False, BaseURL, Query, 'Test');
// BaseURL := STORE_HOST + 'store/showcase';
// Params := TDictionary.Create();
// Params.Add('a', fAuthToken);
// Params.Add('f', 'json');
// Params.Add('k', fDevId);
// Params.Add('ts', IntToStr(DateTimeToUnix(Now, False) - fHostOffset));
// Params.Add('r', CreateNewGUID);
// Params.Add('client', 'icq');
// Params.Add('lang', IfThen(IsRuLang, 'ru-ru', 'en-us'));
// Params.Add('platform', 'windows');
// SendRequest(False, BaseURL, MakeParams('GET', BaseURL, Params), 'Test');
// Params.Free;
// PatchVersion := '6652078904624284395';
// Params := '{"sn": "' + '230490' + '", "fromMsgId": ' + '6652082520986747860' + ', "count": ' + IntToStr(999) + ', "aimSid": "' + fAimSid + '", "patchVersion": "' + PatchVersion + '"}';
// Query := '{"method": "getHistory", "reqId": "' + IntToStr(ReqId) + '-' + IntToStr(DateTimeToUnix(Now, False) - fHostOffset) + '", "authToken": "' + fRESTToken + '", "clientId": ' + fRESTClientId + ', "params": ' + params + ' }';
// SendRequest(True, REST_HOST, Query, 'Test');
// Inc(ReqId);
end;
procedure TICQSession.GetStoreStickerPacks;
var
BaseURL: String;
Params: TDictionary;
Handler: THandlerProc;
begin
if RequiresLogin then
Exit;
BaseURL := STORE_HOST + 'openstore/contentlist';
Params := TDictionary.Create();
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('ts', IntToStr(DateTimeToUnix(Now, False) - fHostOffset));
Params.Add('r', CreateNewGUID);
// Params.Add('platform', 'windows');
Params.Add('client', 'icq');
Params.Add('lang', IfThen(IsRuLang, 'ru-ru', 'en-us'));
Handler := procedure(const RespStr: String)
var
Fn: String;
Tmp: TJSONValue;
JSON: TJSONObject;
Stickers: TJSONArray;
SRecord, SRecordTmp: TStickerPack;
begin
if ParseJSON(RespStr, JSON) then
try
if not CheckSimpleData(JSON, True) then
Exit;
Tmp := JSON.GetValue('stickers');
if not Assigned(Tmp) or not (Tmp is TJSONObject) then
Exit;
Tmp := TJSONObject(Tmp).GetValue('set');
if not Assigned(Tmp) or not (Tmp is TJSONArray) then
Exit;
SQLDB.ClearStickerPacks;
Stickers := TJSONArray(Tmp);
for var Sticker in Stickers do
if Assigned(Sticker) then
begin
SRecord := TStickerPack.FromJSON(TJSONObject(Sticker));
// Skip disabled and duplicates
if DupStickerPacks.Contains(SRecord.Id) or not SRecord.IsEnabled then
Continue;
if SRecord.Purchased then
begin
SRecordTmp := GetStoreStickerPackInfo(IntToStr(SRecord.Id));
if not SRecordTmp._IsDefault then
SRecord := SRecordTmp;
end;
SQLDB.AddStickerPack(SRecord);
if not (SRecord.ContentType = 'animated') and not (SRecord.ListIconLink = '') then
begin
Fn := StickerPath + IntToStr(SRecord.Id) + '_listicon_small.webp';
if not FileExists(Fn) then
begin
if not DirectoryExists(StickerPath) then
ForceDirectories(StickerPath);
LoadFromURLAsFile(SRecord.ListIconLink, Fn);
end;
end;
end;
finally
FreeAndNil(JSON);
end;
NotifyListeners(IE_stickersupdate);
end;
SendRequestAsync(False, BaseURL, MakeParams(Params), 'Get store sticker packs', Handler);
Params.Free;
end;
procedure TICQSession.SearchStoreStickerPack(const Id: String; IdType: TStickerIDType);
var
SRecord: TStickerPack;
begin
SetLength(LastSearchPacks, 0);
SRecord := GetStoreStickerPackInfo(Id, IdType, False);
if not SRecord._IsDefault then
begin
SetLength(LastSearchPacks, 1);
LastSearchPacks[0] := SRecord;
end;
NotifyListeners(IE_stickersearchupdate);
end;
procedure TICQSession.SearchStoreStickerPacks(const Query: String);
var
BaseURL: String;
Params: TDictionary;
Handler: THandlerProc;
begin
if RequiresLogin then
Exit;
BaseURL := STORE_HOST_FALLBACK + 'store/showcase';
Params := TDictionary.Create();
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('ts', IntToStr(DateTimeToUnix(Now, False) - fHostOffset));
Params.Add('r', CreateNewGUID);
Params.Add('platform', 'windows');
Params.Add('client', 'icq');
Params.Add('lang', IfThen(IsRuLang, 'ru-ru', 'en-us'));
Params.Add('search', Trim(Query));
Handler := procedure(const RespStr: String)
var
Tmp: TJSONValue;
JSON: TJSONObject;
Ress: TJSONArray;
// Resp: TPair;
begin
SetLength(LastSearchPacks, 0);
if ParseJSON(RespStr, JSON) then
try
// Resp := CheckRAPIData(JSON);
// if not (Resp.Key = Ord(ERAPI_OK)) then
// Exit;
if not CheckSimpleData(JSON) then
Exit;
Tmp := JSON.GetValue('top');
if not Assigned(Tmp) or not (Tmp is TJSONArray) then
Exit;
Ress := TJSONArray(Tmp);
for var Res in Ress do
if Assigned(Res) then
LastSearchPacks := LastSearchPacks + [TStickerPack.FromJSON(TJSONObject(Res))];
finally
FreeAndNil(JSON);
end;
NotifyListeners(IE_stickersearchupdate);
end;
SendRequestAsync(False, BaseURL, MakeParams(Params), 'Search store sticker packs', Handler);
Params.Free;
end;
function TICQSession.GetStoreStickerPackInfo(const Id: String; IdType: TStickerIDType = SIDT_ID; WithMeta: Boolean = True): TStickerPack;
var
JSON: TJSONObject;
BaseURL: String;
Params: TDictionary;
begin
Result := Default(TStickerPack);
Result._IsDefault := True;
if RequiresLogin or (Id = '') then
Exit;
BaseURL := STORE_HOST + 'openstore/' + IfThen(WithMeta, 'filespackinfowithmeta', 'filespackinfo'); // packinfo
Params := TDictionary.Create();
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('ts', IntToStr(DateTimeToUnix(Now, False) - fHostOffset));
Params.Add('r', CreateNewGUID);
Params.Add('platform', 'windows');
Params.Add('client', 'icq');
Params.Add('lang', IfThen(IsRuLang, 'ru-ru', 'en-us'));
if IdType = SIDT_STOREID then
Params.Add('store_id', Id)
else if IdType = SIDT_FILEID then
Params.Add('file_id', Id)
else
Params.Add('id', Id);
SendRequest(False, BaseURL, MakeParams(Params), RT_JSON, JSON, 'Get sticker pack store info');
if Assigned(JSON) then
try
if CheckSimpleData(JSON) then
Result := TStickerPack.FromJSON(TJSONObject(JSON))
else
OutputDebugString(PChar('Error: ' + Id));
finally
FreeAndNil(JSON);
end;
Params.Free;
end;
procedure TICQSession.BuyStickerPack(const PackId: String);
var
SRecord: TStickerPack;
BaseURL: String;
Params: TDictionary;
Handler: THandlerProc;
begin
SRecord := GetStoreStickerPackInfo(PackId);
if SRecord._IsDefault or (SRecord.StoreId = '') then
MsgDlg(GetTranslation(ICQError2Str[EC_StoreProblem], [GetTranslation('Unable to get sticker pack store id')]), False, mtError);
if RequiresLogin or (PackId = '') or (SRecord.StoreId = '') then
Exit;
BaseURL := STORE_HOST + 'store/buy/free';
Params := TDictionary.Create();
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('ts', IntToStr(DateTimeToUnix(Now, False) - fHostOffset));
Params.Add('r', CreateNewGUID);
Params.Add('platform', 'windows');
Params.Add('client', 'icq');
Params.Add('lang', IfThen(IsRuLang, 'ru-ru', 'en-us'));
Params.Add('product', SRecord.StoreId);
Handler := procedure(const RespStr: String)
var
JSON: TJSONObject;
Verified: Boolean;
begin
if ParseJSON(RespStr, JSON) then
try
if not CheckSimpleData(JSON) then
Exit;
JSON.GetValueSafe('is_verified', Verified);
if Verified then
begin
SQLDB.AddStickerPack(SRecord);
SQLDB.ChangeStickerPackStatus(PackId, True);
NotifyListeners(IE_stickersupdate);
end
else
begin
eventError := EC_StoreProblem;
eventMsgA := GetTranslation('Purchase data failed the verification');
NotifyListeners(IE_error);
end;
finally
FreeAndNil(JSON);
end;
end;
SendRequestAsync(True, BaseURL + '?' + MakeParams(Params), 'product=' + SRecord.StoreId, 'Buy free sticker pack', Handler);
Params.Free;
end;
procedure TICQSession.RemoveStickerPack(const PackId: String);
var
BaseURL: String;
Params: TDictionary;
Handler: THandlerProc;
begin
if RequiresLogin or (PackId = '') then
Exit;
BaseURL := STORE_HOST + 'store/deletepurchase';
Params := TDictionary.Create();
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('ts', IntToStr(DateTimeToUnix(Now, False) - fHostOffset));
Params.Add('r', CreateNewGUID);
Params.Add('platform', 'windows');
Params.Add('client', 'icq');
Params.Add('lang', IfThen(IsRuLang, 'ru-ru', 'en-us'));
Params.Add('product_id', PackId);
Handler := procedure(const RespStr: String)
var
JSON: TJSONObject;
Status: String;
begin
if ParseJSON(RespStr, JSON) then
try
if not CheckSimpleData(JSON, True) then
Exit;
JSON.GetValueSafe('description', Status);
if Status = 'OK' then
begin
SQLDB.ChangeStickerPackStatus(PackId, False);
RemoveStickerPackCache(PackId);
NotifyListeners(IE_stickersupdate);
end
else
begin
eventError := EC_StoreProblem;
eventMsgA := GetTranslation('Cannot remove sticker pack: %s', [Status]);
NotifyListeners(IE_error);
end;
finally
FreeAndNil(JSON);
end;
end;
SendRequestAsync(False, BaseURL + '?' + MakeParams(Params), '', 'Remove sticker pack', Handler);
Params.Free;
end;
procedure TICQSession.SendSaveMyInfo(Cnt: TICQContact);
var
BaseURL: String;
Params: TDictionary;
begin
if Cnt.birth > 0 then
Cnt.age := YearsBetween(Now, Cnt.birth);
SavingMyInfo.ACKcount := 3;
BaseURL := WIM_HOST + 'memberDir/update';
Params := TDictionary.Create();
Params.Add('set=firstName', Cnt.First);
Params.Add('set=lastName', Cnt.Last);
// Params.Add('set=nick', Cnt.Nick);
Params.Add('set=friendlyName', Cnt.Display);
Params.Add('set=relationshipStatus', SrvMarStsByID(Cnt.MarStatus));
Params.Add('set=birthDate', IntToStr(DateTimeToUnix(Cnt.Birth)));
Params.Add('set=gender', IfThen(Cnt.Gender = 2, 'male', IfThen(Cnt.Gender = 1, 'female', 'unknown')));
Params.Add('set=aboutMe', Cnt.About);
// Params.Add('set=lang1', Cnt.Lang[1]);
// Params.Add('set=lang2', Cnt.Lang[2]);
// Params.Add('set=lang3', Cnt.Lang[3]);
// Params.Add('set=tz', '99'{Result.GMThalfs});
// Params.Add('set=originAddress', '{city=' + ParamEncode(Cnt.BirthCity) + ',state=' + ParamEncode(Cnt.BirthState) + ',' +
// 'country=' + ParamEncode(Cnt.BirthCountry) + '}');
// Params.Add('set=homeAddress', '{street=' + ParamEncode(Cnt.Address) + ',city=' + ParamEncode(Cnt.City) + ',' +
// 'state=' + ParamEncode(Cnt.State) + ',zip=' + ParamEncode(Cnt.ZIP) + ',' +
// 'country=' + ParamEncode(Cnt.Country) + '}');
// Params.Add('set=homeAddress', '{city=' + ParamEncode(Cnt.City) + ',state=' + ParamEncode(Cnt.State) + ',' +
// 'country=' + ParamEncode(Cnt.Country) + '}');
// Params.Add('set=phones', '[{type=home,phone=666h} {type=work,phone=666w} {type=mobile,phone=666m} {type=homeFax,phone=666hf} {type=workFax,phone=666wf} {type=other,phone=666o}]');
// Params.Add('set=jobs', '[{title=1,company=2,website=3,department=4,industry=arts,subIndustry=music,startDate=444,endDate=666,street=5,city=6,state=7,zip=8,country=' + ParamEncode(Cnt.Country) + '}]');
// Params.Add('set=interests', '[{code=art,text=test}]');
if SendSessionRequest(True, BaseURL, '&' + MakeParams(Params, True), 'Save my info', 'Failed to save your information') then
if (Cnt.Nick = Cnt.TmpNick) or SetNick then
NotifyListeners(IE_MyInfoAck);
Params.Free;
end;
procedure TICQSession.AddMsg(Flags: Byte; Urgent: Boolean; const Msg: RawByteString);
begin
if Flags and $80 > 0 then Inc(eventFlags, IF_multiple);
if Flags and $40 > 0 then Inc(eventFlags, IF_no_matter);
if Urgent then Inc(eventFlags, IF_urgent);
eventMsgA := msg;
eventInBackground := False;
NotifyListeners(IE_msg);
end;
function TypeStringToTypeId(const s: AnsiString): Integer;
var
nTypeID : Integer;
begin
nTypeID := 0;
if (s = Str_message) then
nTypeID := MTYPE_PLAIN
else if s = 'StatusMsgExt' then
nTypeID := MTYPE_AUTOAWAY
else if (s = 'Web Page Address (URL)') or
(s = 'Send Web Page Address (URL)') or
(s = 'Send URL') then
nTypeID := MTYPE_URL
else if (s = 'Contacts') or
(s = 'Send Contacts') then
nTypeID := MTYPE_CONTACTS
else if (s = 'ICQ Chat') then
nTypeID := MTYPE_CHAT
else if (s = 'Send / Start ICQ Chat') then
nTypeID := MTYPE_CHAT
else if (s ='File') or
(s = 'File Transfer')or
(s = 'Файл') then
nTypeID := MTYPE_FILEREQ
else if (s = 'Request For Contacts') then
nTypeID := MTYPE_PLUGIN
else if s=PLUGIN_SCRIPT then
nTypeID := MTYPE_XSTATUS
else if (s = 'Greeting Card') or
(s = 'Send Greeting Card')or
(s = 'Отправить открытку') then
nTypeID := MTYPE_GCARD
else if (s = 'T-Zer Message')or(s = 'Send Tzer') then
nTypeID := MTYPE_PLAIN
else if s = 'StatusMsgExt' then
else if pos(AnsiString('Сообщ'), s) > 0 then
nTypeID := MTYPE_PLAIN;
result := nTypeID;
end;
function parseTzerTag(const sA: RawByteString): RawByteString;
var
p : Integer;
imgStr: String;
ext: RawByteString;
begin
p := PosEx('name="', sA);
Result := GetTranslation('tZer') + ': ' + copy(sA, p + 6, PosEx('"', sA, p + 7) - p - 6) + #13#10;
p := PosEx('url="', sA);
Result := Result + copy(sA, p + 5, PosEx('"', sA, p + 6) - p - 5) + #13#10;
p := PosEx('thumb="', sA);
ext := copy(sA, p + 7, PosEx('"', sA, p + 8) - p - 7);
try
imgStr := '';
LoadFromURLAsString(ext, imgStr);
if Trim(imgStr) = '' then
imgStr := ext
else
imgStr := RnQImageExTag + Base64EncodeString(imgStr) + RnQImageExUnTag;
except
imgStr := ext;
end;
Result := Result + imgStr + #13#10;
end;
function TICQSession.GetMyCaps: String;
var
s: RawByteString;
begin
// Result := String2Hex(CAPS_sm2big(CAPS_sm_UniqueID));
Result := String2Hex(CAPS_sm2big(CAPS_sm_Emoji));
Result := Result + ',' + String2Hex(CAPS_sm2big(CAPS_sm_MailNotify));
// Result := Result + ',' + String2Hex(CAPS_sm2big(CAPS_sm_IntroDlgStates)); // intro/tail messages
Result := Result + ',' + String2Hex(CAPS_sm2big(CAPS_sm_UTF8));
Result := Result + ',' + String2Hex(BigCapability[CAPS_big_Buzz].v);
Result := Result + ',' + String2Hex(BigCapability[CAPS_big_Reactions].v);
Result := Result + ',' + CAPS_CustomStatuses;
if ShowClientID then
Result := Result + ',' + String2Hex(CapsCustomBuild) + IntToHex(RnQBuild, 4) + IntToHex(RnQBuildCustom, 4) + String2Hex(#00#00#00);
// Result := Result + ',' + String2Hex(BigCapability[CAPS_big_Build].v);
if SupportTypingNotif then
Result := Result + ',' + String2Hex(BigCapability[CAPS_big_MTN].v);
if AvatarsSupport then
Result := Result + ',' + String2Hex(CAPS_sm2big(CAPS_sm_Avatar));
// What are thoooooose?!
//Result := Result + ',' + '094613584C7F11D18222444553540000';
if UseCryptMsg then
begin
if fECCKeys.Generated then
begin
SetLength(s, 11);
CopyMemory(@s[1], @fECCKeys.pubEccKey[0], Length(s));
Result := Result + ',' + String2Hex('RDEC0' + s);
CopyMemory(@s[1], @fECCKeys.pubEccKey[11], Length(s));
Result := Result + ',' + String2Hex('RDEC1' + s);
CopyMemory(@s[1], @fECCKeys.pubEccKey[22], Length(s));
Result := Result + ',' + String2Hex('RDEC2' + s);
end;
Result := Result + ',' + String2Hex(BigCapability[CAPS_big_CryptMsg].v);
// Result := Result + ',' + String2Hex(BigCapability[CAPS_big_QIP_Secure].v); // QIP protect message
end;
// if (curXStatus > 0) and not (XStatusArray[curXStatus].pidOld = '') then
// Result := Result + ',' + String2Hex(XStatusArray[curXStatus].pidOld);
if AddExtCliCaps and (Length(ExtClientCaps) = 32) then
Result := Result + ',' + ExtClientCaps;
end;
function TICQSession.RemoveContact(c: TICQContact): Boolean;
var
IsLocal: Boolean;
begin
IsLocal := c.CntIsLocal or (c.Group = 0);
Result := NotInList.remove(c);
Result := fRoster.remove(c) or Result;
if Result then
begin
if not IsLocal and IsReady then
SendRemoveContact(c);
c.status := SC_UNK;
eventInt := TList(fRoster).Count;
NotifyListeners(IE_numOfContactsChanged);
end
end;
procedure TICQSession.RemoveContactFromServer(c: TICQContact);
begin
if IsReady then
SendRemoveContact(c);
eventContact := c;
NotifyListeners(IE_contactupdate);
end;
function TICQSession.SetStatusAndVis(st, vi: Byte; IsAuto: Boolean = False): Byte;
begin
if not IsAuto then
autoaway.triggered := TR_NONE;
Result := Byte(CurStatus);
if not (st in [Byte(SC_away), Byte(SC_na)]) then
ImAwaySince := 0
else if not (Byte(LastStatus) in [Byte(SC_away), Byte(SC_na)]) then
ImAwaySince := Now;
LastStatus := st;
if IsOffline and (st <> Byte(SC_OFFLINE)) then
DoConnect
else
DoSetStatus(st, vi);
if st = Byte(SC_OFFLINE) then
begin
StayConnected := False;
SetProgBar(Self, 0);
end;
UI.CL.UpdateStatusGlyphs;
end;
function TICQSession.SetStatus(st: Byte; IsAuto: Boolean = False): Byte;
begin
Result := SetStatusAndVis(st, GetVisibility, IsAuto);
end;
procedure TICQSession.SetVisibility(vi: Byte);
begin
if IsReady then
SetStatusAndVis(GetStatus, vi)
else
begin
if vi > Byte(High(TVisibility)) then
vi := 0;
Visibility := TVisibility(vi);
UI.CL.UpdateStatusGlyphs;
UI.CL.UpdateVisibilityImage;
end;
end;
procedure TICQSession.DoSetStatus(st: Byte; vi: Byte);
begin
if st = Byte(SC_OFFLINE) then
begin
Disconnect;
Exit;
end;
if vi > Byte(High(TVisibility)) then
vi := 0;
if (st = Byte(CurStatus)) and (vi = Byte(Visibility)) then
Exit;
if not (st = Byte(CurStatus)) then
eventOldStatus := CurStatus;
if not (vi = Byte(Visibility)) then
eventOldInvisible := IsInvisible;
if IsReady then
begin
CurStatus := TICQStatus(st);
Visibility := TVisibility(vi);
if SendPresenceState then
begin
InitListenerVars(nil, Now);
if not (eventOldStatus = CurStatus) then
NotifyListeners(IE_statusChanged);
if not (eventOldInvisible = IsInvisible) then
NotifyListeners(IE_visibilityChanged);
end; // else restore status and vis?
SendStatus(CurXStatus, ExtStsStrings[CurXStatus].Desc);
end else
Connect;
end;
function TICQSession.GetStatus: Byte;
begin
Result:= Byte(CurStatus)
end;
function TICQSession.GetXStatus: Byte;
begin
Result := CurXStatus;
end;
function TICQSession.GetStatusName: String;
begin
// if (ForTray and XStatusInTray and (CurStatus = SC_ONLINE)) and (CurXStatus > 0) then
// begin
// if CurXStatusStr.Desc > '' then
// Result := CurXStatusStr.Desc
// else
// Result := GetTranslation(Status2ShowStr[CurStatus])
// end else
Result := GetTranslation(Status2ShowStr[CurStatus])
end;
function TICQSession.GetStatusImg: TPicName;
begin
Result := Status2ImgName(Byte(CurStatus), IsInvisible);
end;
function TICQSession.GetVisibility: Byte;
begin
Result := Byte(fVisibility)
end;
class function TICQSession._GetProtoName: string;
begin
Result := 'ICQ';
end;
function TICQSession.GetICQContact(const uid: TUID): TICQContact;
begin
Result := ContactsDB.Add(uid);
end;
function TICQSession.GetICQContact(const uin: Integer): TICQContact;
begin
Result := ContactsDB.Add(IntToStr(uin));
end;
class function TICQSession._IsProtoUid(const UID: TUID): Boolean; //Static;
begin
Result := _IsValidUid(UID) or _IsValidPhone(UID) or _IsValidMail(UID);
end;
class function TICQSession._IsValidUid(const UIN: TUID): Boolean;
var
k: Integer;
fUIN: Int64;
Temp: TUID;
begin
Result := False;
Temp := TICQContact.TrimUID(uin);
if Length(Temp) = 0 then
Exit;
Val(Temp, fUIN, k);
if k = 0 then
Result := True
end;
class function TICQSession._IsValidPhone(const Phone: TUID): Boolean;
var
k: Integer;
Temp: TUID;
begin
Result := True;
Temp := String(Phone).Trim();
if Length(Temp) = 0 then
Exit(False);
if not (Phone[1] = '+') then
Exit(False);
for k := 1 to Length(Phone) do
if not (Phone[k] in ['0'..'9','+']) then
Result := False;
end;
class function TICQSession._IsValidMail(const Mail: TUID): Boolean;
var
k: Integer;
Temp: TUID;
begin
Result := False;
Temp := String(Mail).Trim();
if Length(Temp) = 0 then
Exit;
for k := 1 to Length(Mail) do
if (Mail[k] in ['@']) then // Super basic :)
Result := True;
end;
procedure TICQSession.AddContactToCL(var c: TICQContact);
begin
if not Assigned(c) then
Exit;
if c.IsInRoster then
Exit;
// if IsReady then
// c.status := SC_OFFLINE
// else
// c.status := SC_UNK;
fRoster.add(c);
eventInt := TList(fRoster).count;
NotifyListeners(IE_numOfContactsChanged);
end;
procedure TICQSession.AddContactsToCL(cl: TRnQCList);
begin
if cl = nil then
Exit;
if TList(cl).count = 0 then
Exit;
cl := cl.clone.remove(fRoster);
if IsReady then
ICQCL_SetStatus(cl, SC_OFFLINE)
else
ICQCL_SetStatus(cl, SC_UNK);
fRoster.add(cl);
eventInt := TList(fRoster).count;
NotifyListeners(IE_numOfContactsChanged);
cl.Free;
end;
function TICQSession.AddContact(c: TICQContact; IsLocal: Boolean = False):boolean;
begin
Result := False;
if (c = nil) or (c.UID = '') then
Exit;
Result := fRoster.add(c);
Result := Result or (not IsLocal and c.CntIsLocal);
if Result then
begin
if IsReady then
begin
if c.Status = SC_UNK then
c.Status := SC_OFFLINE;
if not IsLocal then
begin
if c.IsInRoster then
SendAddContact(c)
else
c.CntIsLocal := True;
end;
end;
eventInt := TList(fRoster).Count;
NotifyListeners(IE_numOfContactsChanged);
end;
end;
function TICQSession.ReadList(l: TLIST_TYPES): TRnQCList;
begin
case l of
LT_ROSTER: Result := fRoster;
LT_SPAM: Result := SpamList;
else
Result := nil;
end;
end;
procedure TICQSession.AddToList(l: TLIST_TYPES; cl: TRnQCList);
begin
case l of
LT_ROSTER: AddContactsToCL(cl);
end;
end;
{
procedure TICQSession.RemFromList(l: TLIST_TYPES; cl: TRnQCList);
begin
case l of
end;
end;
}
procedure TICQSession.AddToList(l: TLIST_TYPES; cnt: TICQContact);
begin
case l of
LT_ROSTER: AddContact(cnt);
LT_SPAM: Add2Ignore(cnt);
end;
end;
procedure TICQSession.RemFromList(l: TLIST_TYPES; cnt: TICQContact);
begin
case l of
LT_ROSTER: RemoveContact(cnt);
LT_SPAM: RemFromIgnore(cnt);
end;
end;
function TICQSession.IsInList(l: TLIST_TYPES; cnt: TICQContact): Boolean;
begin
case l of
LT_ROSTER: Result := fRoster.exists(cnt);
LT_SPAM: Result := SpamList.exists(cnt);
else
Result := False;
end;
end;
function TICQSession.Add2Ignore(c: TICQcontact): Boolean;
var
Query: UTF8String;
BaseURL: String;
begin
Result := False;
if IsReady then
begin
BaseURL := WIM_HOST + 'preference/setPermitDeny';
Query := '&pdIgnore=' + ParamEncode(String(c.UID));
Result := SendSessionRequest(False, BaseURL, Query, 'Add to ignore list');
end;
end;
function TICQSession.RemFromIgnore(c: TICQcontact): Boolean;
var
Query: UTF8String;
BaseURL: String;
begin
Result := False;
if IsReady then
begin
BaseURL := WIM_HOST + 'preference/setPermitDeny';
Query := '&pdIgnoreRemove=' + ParamEncode(String(c.UID));
Result := SendSessionRequest(False, BaseURL, Query, 'Remove from ignore list');
end;
end;
procedure TICQSession.GetPermitDeny; // Unused
var
JSON: TJSONObject;
Query: UTF8String;
BaseURL: String;
begin
if not IsReady then
Exit;
BaseURL := WIM_HOST + 'preference/getPermitDeny'; // &friendly=1
Query := '';
try
if SendSessionRequest(False, BaseURL, Query, RT_JSON, JSON, 'Get permit/deny lists') then
ProcessPermitDeny(JSON);
finally
JSON.Free;
end;
end;
procedure TICQSession.SetPermitDenyMode(const Mode: String);
var
JSON: TJSONObject;
Query: UTF8String;
BaseURL: String;
begin
if not IsReady then
Exit;
BaseURL := WIM_HOST + 'preference/setPermitDeny';
Query := '&pdMode=' + Mode;
try
if SendSessionRequest(False, BaseURL, Query, RT_JSON, JSON, 'Set permit/deny mode') then
//ProcessPermitDeny(JSON);
finally
JSON.Free;
end;
end;
procedure TICQSession.AddToBlock(const c: String); // Unused
var
Query: UTF8String;
BaseURL: String;
begin
if not IsReady then
Exit;
BaseURL := WIM_HOST + 'preference/setPermitDeny';
Query := '&pdBlock=' + ParamEncode(c);
SendSessionRequest(False, BaseURL, Query, 'Add contact to block list');
end;
procedure TICQSession.RemFromBlock(const c: String); // Unused
var
Query: UTF8String;
BaseURL: String;
begin
if IsReady then
begin
BaseURL := WIM_HOST + 'preference/setPermitDeny';
Query := '&pdBlockRemove=' + ParamEncode(c);
SendSessionRequest(False, BaseURL, Query, 'Remove contact from block list');
end;
end;
procedure TICQSession.SetMuted(c: TICQcontact; Mute: Boolean);
var
Query: UTF8String;
BaseURL: String;
begin
if IsReady then
begin
BaseURL := WIM_HOST + 'buddylist/Mute';
Query := '&buddy=' + c.UID +
'&eternal=' + IfThen(Mute, '1', '0');
SendSessionRequest(False, BaseURL, Query, '(Un)mute contact');
end;
end;
function TICQSession.MaxCharsFor(const c: TICQContact): Integer;
begin
Result := 10000;
with c do
begin
// if not IsBin then
// Result := Result div 2;
if UseCryptMsg and (Crypt.SupportCryptMsg or (UseEccCryptMsg and fECCKeys.Generated and Crypt.SupportEcc)) then
Result := Result * 3 div 4;
end;
end;
function TICQSession.IsVisible: Boolean;
begin
Result := Visibility = VI_normal;
end;
function TICQSession.IsInvisible: Boolean;
begin
Result := Visibility = VI_invisible;
end;
function TICQSession.CreateNewGUID: String;
var
UID: TGUID;
begin
CreateGuid(UID);
Result := GUIDtoString(UID).Trim(['{', '}']).ToLower;
end;
function TICQSession.AddRef(k: TRefKind; const uin: TUID): Integer;
begin
Result := SNACref;
Refs[SNACref].Kind := k;
Refs[SNACref].UID := uin;
Inc(SNACref);
if SNACref > maxRefs then
SNACref := 1;
end;
function TICQSession.RequestPasswordIfNeeded(DoConnect: Boolean = True): Boolean;
begin
Result := False;
if not IsMobileAccount and RequiresLogin and ((fPwd = '') or (StartAccount = '')) then
begin
eventData := IfThen(DoConnect, '', 'pwdonly');
eventError := EC_missingLogin;
NotifyListeners(IE_error);
Result := True;
end;
end;
procedure TICQSession.Connect;
begin
if not IsOffline then
Exit;
if RequestPasswordIfNeeded then
Exit;
Phase := connecting_;
eventAddress := WIM_HOST + 'aim/startSession';
NotifyListeners(IE_connecting);
SNACref := 1;
if StartSession then
AfterSessionStarted
else
GoneOffline;
end;
procedure TICQSession.AfterSessionStarted;
begin
StartPolling;
if LastStatus = Byte(SC_OFFLINE) then
DoSetStatus(Byte(SC_ONLINE), Byte(VI_normal))
else if not ExitFromAutoaway then
DoSetStatus(Byte(LastStatus), Byte(Visibility));
end;
function TICQSession.MakeParams(const Params: TDictionary; DoublePercent: Boolean = False): String;
var
// Hash: String;
EncParams: TStringList;
begin
EncParams := TStringList.Create;
EncParams.Sorted := True;
EncParams.StrictDelimiter := True;
EncParams.Delimiter := '&';
EncParams.QuoteChar := #0;
with Params.GetEnumerator do
begin
while MoveNext do
EncParams.Add(Current.Key + '=' + IfThen(Current.Key = 'stickerId', Current.Value, ParamEncode(Current.Value, DoublePercent)));
Free;
end;
Result := EncParams.DelimitedText;
EncParams.Free;
// if Sign then
// begin
// hash := method + '&' + ParamEncode(BaseURL) + '&' + ParamEncode(Result);
// Result := Result + '&sig_sha256=' + ParamEncode(HashString(fSessionKey, UTF(hash)));
// end;
end;
//procedure TICQSession.OpenICQURL(const URL: String);
//var
// BaseURL: String;
// Params: TDictionary;
//begin
// BaseURL := 'https://www.icq.com/karma_api/karma_client2web_login.php';
//
// Params := TDictionary.Create();
// Params.Add('ts', IntToStr(DateTimeToUnix(Now, False) - fHostOffset));
// Params.Add('owner', MyAccNum);
// Params.Add('a', fAuthToken);
// Params.Add('k', fDevId);
// Params.Add('d', URL);
// OpenURL(BaseURL + '?' + MakeParams('GET', BaseURL, Params));
// Params.Free;
//end;
function TICQSession.ClientLogin: Boolean;
var
JSON: TJSONObject;
Query: UTF8String;
BaseURL, TransId, SMSCode: String;
ErrHandler: TErrorProc;
begin
Result := False;
if (StartAccountNum = '') or (not IsMobileAccount and (fPwd = '')) then
Exit;
Phase := login_;
NotifyListeners(IE_loggin);
if IsMobileAccount then
begin
fSessionKey := '';
BaseURL := SMS_REG + 'requestPhoneValidation.php';
Query := 'locale=ru' +
'&msisdn=' + ParamEncode(StartAccountNum) +
'&smsFormatType=human' +
'&k=' + fDevId +
'&r=' + CreateNewGUID;
ErrHandler := procedure(Resp: TPair)
begin
ResetSession;
eventInt := Resp.Key;
eventMsgA := GetTranslation(Resp.Value);
if Resp.Key = Ord(EAC_Unknown) then
eventError := EC_Login_Seq_Failed
else
eventError := EC_other;
NotifyListeners(IE_error);
end;
try
if SendRequest(False, BaseURL, Query, RT_JSON, JSON, 'Request SMS code', '', ErrHandler) then
begin
JSON.GetValueSafe('trans_id', TransId);
SMSCode := UI.InputQuery(GetTranslation('Phone login'), GetTranslation('Enter SMS code'), 'sms');
if (Trim(SMSCode) = '') or not IsOnlyDigits(SMSCode) then
begin
ResetSession;
Exit;
end;
BaseURL := SMS_REG + 'loginWithPhoneNumber.php';
Query := 'f=json' +
'&locale=ru' +
'&msisdn=' + ParamEncode(StartAccountNum) +
'&trans_id=' + ParamEncode(TransId) +
'&sms_code=' + Trim(SMSCode) +
'&create_account=1' +
'&k=' + fDevId +
'&r=' + CreateNewGUID;
FreeAndNil(JSON);
if SendRequest(False, BaseURL, Query, RT_JSON, JSON, 'Login using phone and create auth data', '', ErrHandler) then
begin
Result := True;
JSON.GetValueSafe('sessionKey', fSessionKey);
fAuthTokenTime := DateTimeToUnix(Now, False);
if not (JSON.GetValue('hostTime') = nil) then
fHostOffset := DateTimeToUnix(Now, False) - StrToInt(JSON.GetValue('hostTime').Value)
else
fHostOffset := 0;
if not (JSON.GetValue('token') = nil) then
with JSON.GetValue('token') do
begin
GetValueSafe('a', fAuthToken);
GetValueSafe('expiresIn', fAuthTokenExpIn);
end;
end;
end;
finally
FreeAndNil(JSON);
end;
end
else
begin
BaseURL := WIM_HOST + 'auth/clientLogin';
Query := 'f=json' +
'&clientName=' + ParamEncode(IfThen(ShowClientID, 'R&Q', 'Mail.ru Windows ICQ')) +
'&clientVersion=' + IfThen(ShowClientID, '0.11.9999.' + IntToStr(RnQBuild) + 'v' + IntToStr(RnQBuildCustom), ICQ_FAKE_VERSION + '.' + ICQ_FAKE_BUILD) +
'&devId=' + fDevId +
'&service=icq' +
'&tokenType=longterm' +
'&s=' + ParamEncode(String(StartAccountNum)) +
'&pwd=' + ParamEncode(fPwd) +
'&r=' + CreateNewGUID;
ErrHandler := procedure(Resp: TPair)
begin
ResetSession;
eventInt := Resp.Key;
eventMsgA := GetTranslation(Resp.Value);
if Resp.Key = Ord(EAC_Unknown) then
eventError := EC_Login_Seq_Failed
else if Resp.Key = Ord(EAC_Wrong_Login) then
eventError := EC_badPwd
else if (Resp.Key = Ord(EAC_Send_Rate_Limit)) or (Resp.Key = Ord(EAC_Rate_Limit)) then
eventError := EC_rateExceeded
else
eventError := EC_other;
NotifyListeners(IE_error);
end;
try
if SendRequest(True, BaseURL, Query, RT_JSON, JSON, 'Login using UIN and create auth data', '', ErrHandler) then
begin
Result := True;
JSON.GetValueSafe('sessionSecret', fSessionSecret);
fAuthTokenTime := DateTimeToUnix(Now, False);
// loginId - UIN
// "settings":{"needFillProfile":false}
if not (JSON.GetValue('hostTime') = nil) then
fHostOffset := DateTimeToUnix(Now, False) - StrToInt(JSON.GetValue('hostTime').Value)
else
fHostOffset := 0;
if not (JSON.GetValue('token') = nil) then
with JSON.GetValue('token') do
begin
GetValueSafe('a', fAuthToken);
GetValueSafe('expiresIn', fAuthTokenExpIn);
end;
end;
finally
FreeAndNil(JSON);
end;
end;
{
BaseURL := 'https://icq.com/siteim/icqbar/php/proxy_jsonp_connect.php';
query := 'username=' + String(StartAccountNum) + '&password=' + fPwd + '&time=' + IntToStr(DateTimeToUnix(Now, False)) + '&remember=1';
LoggaICQPkt('[POST] Login and create session', WL_sent_text, BaseURL + '?' + query);
// LoggaICQPkt('[POST] Login and create session', WL_sent_text, 'https://icq.com/siteim/icqbar/php/proxy_jsonp_connect.php?[...]');
fs := TMemoryStream.Create;
LoadFromUrl(BaseURL, fs, 0, False, True, query);
SetLength(session, fs.Size);
fs.ReadBuffer(session[1], fs.Size);
fs.Clear;
LoggaICQPkt('[POST] Login and create session', WL_rcvd_text, session);
try
json := TJSONObject.ParseJSONValue(TEncoding.UTF8.GetBytes(session)) as TJSONObject;
if Assigned(json) then
if json.GetValue('statusCode').Value = '200' then
begin
fDevId := json.GetValue('k').Value;
fSessionKey := json.GetValue('sessionKey').Value;
fSessionToken := json.GetValue('a').Value;
fSessionTokenTime := StrToInt(json.GetValue('ts').Value);
if not (json.GetValue('tsDelta') = nil) then
fHostOffset := StrToInt(json.GetValue('tsDelta').Value)
else
fHostOffset := 0;
end;
FreeAndNil(json);
}
if IsMobileAccount then
begin
if fSessionKey = '' then
begin
ODS('Not enough data to login!');
Exit;
end;
end
else
begin
if (getPwd = '') or (fSessionSecret = '') or (fAuthToken = '') then
begin
ODS('Not enough data to login!');
Exit;
end;
fSessionKey := HashString(UTF(getPwd), UTF(fSessionSecret));
end;
end;
//procedure TICQSession.RESTGenToken;
//var
// BaseURL, UnixTime: String;
// Params: TDictionary;
// JSON: TJSONObject;
//begin
// BaseURL := REST_HOST + 'genToken';
// UnixTime := IntToStr(DateTimeToUnix(Now, False) - fHostOffset);
//
// Params := TDictionary.Create();
// Params.Add('a', fAuthToken);
// Params.Add('k', fDevId);
// Params.Add('ts', UnixTime);
// //Params.Add('nonce', UnixTime + '-' + IntToStr(Random(10)));
//
// SendRequest(True, BaseURL, MakeParams('POST', BaseURL, Params), RT_JSON, JSON, 'Generate REST auth token');
// if Assigned(JSON) then
// try
// if (JSON.GetValue('results') = nil) or (TJSONObject(JSON.GetValue('results')).Count = 0) then
// begin
// fRESTToken := '';
// MsgDlg('Failed to get REST auth token', True, mtError);
// //TJSONObject(JSON.GetValue('status')).GetValue('reason').Value
// end else try
// fRESTToken := TJSONObject(JSON.GetValue('results')).GetValue('authToken').Value;
// fRESTTokenTime := Now;
// except
// fRESTToken := '';
// end;
// finally
// FreeAndNil(JSON);
// end;
//
// Params.Free;
//end;
//
//procedure TICQSession.RESTCheckToken;
//begin
// if MinutesBetween(Now, fRESTTokenTime) > 15 then
// RESTGenToken;
//end;
//
//procedure TICQSession.RESTAddClient;
//var
// BaseURL, UnixTime: String;
// Params: TDictionary;
// Code: Integer;
// JSON: TJSONObject;
//begin
// if fRESTToken = '' then
// Exit;
//
// BaseURL := REST_HOST + 'addClient';
// UnixTime := IntToStr(DateTimeToUnix(Now, False) - fHostOffset);
//
// Params := TDictionary.Create();
// Params.Add('a', fAuthToken);
// Params.Add('f', 'json');
// Params.Add('k', fDevId);
// Params.Add('ts', UnixTime);
// Params.Add('client', 'icq');
// Params.Add('lang', IfThen(IsRuLang, 'ru-ru', 'en-us'));
// Params.Add('reqId', CreateNewGUID);
// Params.Add('authToken', fRESTToken);
//
// SendRequest(True, BaseURL, MakeParams('POST', BaseURL, Params), RT_JSON, JSON, 'Get REST client id');
// if Assigned(JSON) then
// try
// TJSONObject(JSON.GetValue('status')).GetValue('code').TryGetValue(Code);
// if not (Code = 20000) or (JSON.GetValue('results') = nil) then
// begin
// fRESTClientId := '';
// MsgDlg('[' + IntToStr(Code) + '] ' + GetTranslation('Failed to get REST client id'), False, mtError);
// //TJSONObject(JSON.GetValue('status')).GetValue('reason').Value
// end else
// fRESTClientId := TJSONObject(JSON.GetValue('results')).GetValue('clientId').Value;
// //"appStamp" : "jySC-tr4EKsnOb6gUIl-f6z4d6QxmGwntxpEJg6Aj_k=",
// finally
// FreeAndNil(JSON);
// end;
//
// Params.Free;
//end;
//
//function TICQSession.RESTCheckErrors(var JSON: TJSONObject): Boolean;
//var
// Code: Integer;
//begin
// Result := True;
// if TJSONObject(JSON.GetValue('status')).GetValue('code').TryGetValue(Code) then
// if not (Code = 20000) then
// begin
//// if Code = 40201 then
//// RESTGenToken
//// else
// ODS('Error code: ' + IntToStr(Code));
// Result := False;
// end;
//end;
//
//function TICQSession.RESTShowError(var JSON: TJSONObject; ErrText: String = ''): Boolean;
//var
// Code: Integer;
//begin
// Result := True;
// if TJSONObject(JSON.GetValue('status')).GetValue('code').TryGetValue(Code) then
// if not (Code = 20000) then
// begin
// MsgDlg('[' + IntToStr(Code) + '] ' + GetTranslation(ErrText), False, mtError);
// Result := False;
// end;
//end;
function TICQSession.StartSession: Boolean;
var
Query: UTF8String;
ts: Integer;
BaseURL, UnixTime, AutoCaps: String;
Params: TDictionary;
JSON: TJSONObject;
UsingSaved, Relogin, SeqFailed, ProcResult: Boolean;
ErrHandler: TErrorProc;
begin
Result := False;
ProcResult := False;
UsingSaved := True;
SeqFailed := False;
Relogin := False;
if RequiresLogin then
begin
UsingSaved := False;
if not ClientLogin then
Exit;
end;
if RequiresLogin and not UsingSaved then
begin
eventInt := Ord(EAC_Not_Enough_Data);
eventMsgA := '';
eventError := EC_other;
NotifyListeners(IE_error);
Exit;
end;
// Start session
BaseURL := WIM_HOST + 'aim/startSession';
UnixTime := IntToStr(DateTimeToUnix(Now, False) - fHostOffset);
//AutoCaps := '8eec67ce70d041009409a7c1602a5c84';
AutoCaps := '';
Params := TDictionary.Create();
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('a', fAuthToken);
Params.Add('clientName', IfThen(ShowClientID, 'R&Q', 'Mail.ru Windows ICQ'));
Params.Add('clientVersion', IfThen(ShowClientID, IntToStr(RnQBuild) + 'v' + IntToStr(RnQBuildCustom), '5000'));
Params.Add('majorVersion', IfThen(ShowClientID, '0', ICQ_FAKE_VERSION_MAJOR));
Params.Add('minorVersion', IfThen(ShowClientID, '11', ICQ_FAKE_VERSION_MINOR));
Params.Add('buildNumber', IfThen(ShowClientID, '9999', ICQ_FAKE_BUILD));
Params.Add('pointVersion', IfThen(ShowClientID, IntToStr(RnQBuild), ICQ_FAKE_VERSION_POINT));
Params.Add('assertCaps', GetMyCaps);
Params.Add('interestCaps', AutoCaps);
Params.Add('ts', UnixTime);
Params.Add('imf', 'plain');
Params.Add('inactiveView', 'offline');
// Full invisibility is not working, "offline" presence event is still being sent to others when starting/ending session
Params.Add('invisible', IfThen(IsInvisible, 'true', 'false'));
Params.Add('view', IfThen(IsInvisible, 'offline', 'online'));
Params.Add('activeTimeout', '180');
Params.Add('mobile', '0');
Params.Add('rawMsg', '0');
Params.Add('language', IfThen(IsRuLang, 'ru-ru', 'en-us'));
Params.Add('deviceId', CreateNewGUID);
Params.Add('sessionTimeout', '7776000'); // 90 days
Params.Add('events', 'myInfo,presence,buddylist,typing,dataIM,userAddedToBuddyList,service,webrtcMsg,mchat,hist,hiddenChat,diff,permitDeny,imState,notification,apps,offlineIM,sentIM,alert');
Params.Add('includePresenceFields', AllFieldsAsParam);
if AutoReqXStatus then
Params.Add('subscriptions', '[{"type":"status"}]');
// Params.Add('minimizeResponse', '1');
Params.Add('nonce', UnixTime + '-1');
ErrHandler := procedure(Resp: TPair)
begin
if ((Resp.Key = Ord(EAC_Auth_Required)) or (Resp.Key = Ord(EAC_Wrong_DevKey))) and UsingSaved then
begin
Relogin := True;
ResetSession;
RequestPasswordIfNeeded(False);
ProcResult := StartSession;
end else
begin
SeqFailed := True;
eventInt := Resp.Key;
eventMsgA := GetTranslation(Resp.Value);
if Resp.Key = Ord(EAC_Unknown) then
eventError := EC_Login_Seq_Failed
else
eventError := EC_other;
NotifyListeners(IE_error);
end;
end;
try
if SendRequest(True, BaseURL, MakeParams(Params), RT_JSON, JSON, 'Start session', '', ErrHandler) then
begin
JSON.GetValueSafe('aimsid', fAimSid);
JSON.GetValueSafe('fetchBaseURL', fFetchBaseURL);
JSON.GetValueSafe('ts', ts);
if not (JSON.GetValue('myInfo') = nil) then
GetAccountUID(JSON.GetValue('myInfo') as TJSONObject);
LastFetchBaseURL := fFetchBaseURL;
fHostOffset := DateTimeToUnix(Now, False) - ts;
ProcResult := True;
end;
finally
FreeAndNil(JSON);
end;
Result := ProcResult;
Params.Free;
if Relogin then
Exit;
if SeqFailed then
begin
ResetSession;
Exit;
end;
if not Result then
Exit;
Phase := settingup_;
eventAddress := BaseURL;
NotifyListeners(IE_connected);
BaseURL := WIM_HOST + 'timezone/set';
Query := '&TimeZoneOffset=' + IntToStr(DateTimeToUnix(Now, True) - (DateTimeToUnix(Now, False) + fHostOffset));
SendSessionRequest(False, BaseURL, Query, 'Set timezone');
NotifyListeners(IE_almostOnline);
end;
function TICQSession.PingSession: Boolean;
var
JSON: TJSONObject;
Query: UTF8String;
BaseURL: String;
ts: Integer;
begin
BaseURL := WIM_HOST + 'aim/pingSession';
Query := '&k=' + fDevId;
Result := SendSessionRequest(False, BaseURL, Query, RT_JSON, JSON, 'Restore session');
try
if Result then
begin
JSON.GetValueSafe('aimsid', fAimSid);
JSON.GetValueSafe('fetchBaseURL', fFetchBaseURL);
JSON.GetValueSafe('ts', ts);
LastFetchBaseURL := fFetchBaseURL;
fHostOffset := DateTimeToUnix(Now, False) - ts;
AfterSessionStarted;
end;
finally
JSON.Free;
end;
end;
procedure TICQSession.ResetSession;
begin
fSessionSecret := '';
fSessionKey := '';
fAuthToken := '';
fAuthTokenTime := 0;
fAuthTokenExpIn := 0;
fHostOffset := 0;
end;
procedure TICQSession.EndSession(EndToken: Boolean = False);
var
Query: UTF8String;
BaseURL: String;
begin
BaseURL := WIM_HOST + 'aim/endSession';
Query := IfThen(EndToken, '&invalidateToken=1');
SendSessionRequest(False, BaseURL, Query, 'End current session');
GoneOffline;
if EndToken then
ResetSession;
end;
function TICQSession.GetSessionsList: TArray;
var
Results, SessionObj: TJSONObject;
Sessions: TJSONArray;
Cnt: Integer;
Format: TFormatSettings;
UnixTime: Int64;
begin
SetLength(Result, 0);
if fAimSid = '' then
Exit;
if SendRAPIRequest(True, 'session/list', nil, RT_JSON, Results, 'Get a list of opened sessions', 'Failed to retrieve sessions list') then
if Assigned(Results) then
try
Format := TFormatSettings.Create(IfThen(IsRuLang, System.SysUtils.TLanguages.GetLocaleIDFromLocaleName('ru-RU'), LOCALE_USER_DEFAULT));
Sessions := TJSONArray(Results.GetValue('sessions'));
SetLength(Result, Sessions.Count);
Cnt := 0;
if Sessions.Count > 0 then
for var Session in Sessions do
if Assigned(Session) and (Session is TJSONObject) then
begin
Result[Cnt].current := False;
SessionObj := TJSONObject(Session);
SessionObj.TryGetValue('hash', Result[Cnt].hash);
SessionObj.TryGetValue('client', Result[Cnt].client);
SessionObj.TryGetValue('current', Result[Cnt].current);
SessionObj.TryGetValue('ip', Result[Cnt].ip);
SessionObj.TryGetValue('location', Result[Cnt].location);
SessionObj.TryGetValue('os', Result[Cnt].os);
SessionObj.TryGetValue('startedTime', UnixTime);
Result[Cnt].startedTime := Capitalize(FormatDateTime('dddd, mmmm d yyyy, hh:nn:ss', UnixToDateTime(UnixTime, False), Format));
Inc(Cnt);
end;
finally
FreeAndNil(Results);
end;
end;
procedure TICQSession.CloseSession(const Hash: String; Current: Boolean);
var
Params: TJSONObject;
begin
Params := TJSONObject.Create;
Params.AddPair('hash', Hash);
if SendRAPIRequest(True, 'session/reset', @Params, 'Close single opened session', 'Failed to close session') then
if Running and Assigned(UI) then
UI.UpdateSessions(Current);
Params.Free;
end;
procedure TICQSession.CloseAllSessions;
begin
if SendRAPIRequest(True, 'session/resetAll', nil, 'Close all opened sessions', 'Failed to close all sessions') then
if Running and Assigned(UI) then
UI.UpdateSessions;
end;
procedure TICQSession.PollError(const ExtraError: String = ''; Silent: Boolean = False);
begin
if CleanDisconnect then
Exit;
Inc(FatalErrorCount);
if (FatalErrorCount > 5) then
begin
FatalErrorCount := 0;
MsgDlg(GetTranslation('Failed to start listening for events, disconnecting...') +
IfThen(ExtraError = '', '', #13#10 + ExtraError), False, mtError);
LogICQPacket(WL_disconnected, '', 'Encountered unrecoverable error, disconnecting...', ExtraError);
EndSession;
Exit;
end else if (not Silent) then
MsgDlg(GetTranslation('Failed to start listening for events, waiting %d sec before retry...', [Round(ICQErrorReconnectDelay / 1000)]) +
IfThen(ExtraError = '', '', #13#10 + ExtraError), False, mtError);
FreeAndNil(ReconnectTask);
ReconnectTask := TAnonTask.Create(procedure
begin
Sleep(ICQErrorReconnectDelay);
if not TThread.Current.CheckTerminated then
TThread.Queue(nil, procedure
begin
// Try to use existing session, get new initial fetch url and start polling again. Go offline if all fails.
if Running then
if not PingSession then
EndSession;
end);
end);
ReconnectTask.Start;
end;
procedure TICQSession.StartPolling;
begin
Inc(ReqId);
LogICQPacket(WL_sent_text, 'GET', 'Event fetch loop started', fFetchBaseURL);
PollURL(fFetchBaseURL);
Phase := online_;
end;
procedure TICQSession.PollURL(URL: String);
begin
if not Running then
Exit;
if not Assigned(HttpPoll) or (URL = '') then
begin
PollError('[ERR_UNASSIGNED]');
Exit;
end;
URL := URL.TrimRight(['/']);
if (pos('?', URL) = 0) then
URL := URL + '?'
else
URL := URL + '&';
URL := URL + 'f=json&r=' + CreateNewGUID + '&peek=0&timeout=' + IfThen(URL.Contains('first=1'), '500', '60000');
URL := URL + '&supportedSuggestTypes=text-smartreply,sticker-smartreply';
If IsInvisible then
URL := URL + '&bg=1&hidden=1';
HttpPoll.Setup(URL);
HttpPoll.SetLongPoll;
HttpPoll.GetAsync;
HttpPoll.StartTimeout;
end;
procedure TICQSession.PollRequestDone(Sender: TObject; const Response: IHTTPResponse; const Error: String = '');
var
t, ts, code: Integer;
RespStr, ErrText: String;
Resp: TPair;
JSON: TJSONObject;
event, etype, edata: TJSONValue;
events: TJSONArray;
// function UnixTimeInMs: Int64;
// var
// ST: SystemTime;
// DT: TDateTime;
// begin
// Windows.GetSystemTime(ST);
// DT := SysUtils.EncodeDate(ST.wYear, ST.wMonth, ST.wDay) +
// SysUtils.EncodeTime(ST.wHour, ST.wMinute, ST.wSecond, ST.wMilliseconds);
// Result := DateUtils.MilliSecondsBetween(DT, UnixDateDelta);
// end;
begin
// Abort and request fetch URL again every <60 sec to stay online
if not Assigned(Response) or (Response.StatusCode = 0) then
begin
if Error = '' then
RestartPolling
else
PollError(Error, not ShowTempConnectErrors);
Exit;
end;
RespStr := Response.ContentAsString(TEncoding.UTF8);
// 5 sec delay after HTTP error
if not (Response.StatusCode = 200) then
begin
if ShowTempConnectErrors {and not (Response.StatusCode = 0)} then
begin
ErrText := Error;
if not (RespStr = '') then
ErrText := ErrText + #13#10 + 'Response: ' + RespStr;
MsgDlg(GetTranslation('Server error during event fetch') + ': ' + IntToStr(Response.StatusCode) + ErrText, False, mtWarning);
end;
if (Response.StatusCode >= 500) and (Response.StatusCode < 600) then
RestartPolling(ICQErrorReconnectDelay)
else
PollError('[ERR_HTTPCODE]', not ShowTempConnectErrors and (Response.StatusCode = 0));
Exit;
end;
// if (RespStr = '') and (HttpPoll.ContentLength > 0) then
// begin
// if HttpPoll.State = httpWaitingBody then
// Exit;
// end;
if Trim(RespStr) = '' then
begin
// PollError('[ERR_EMPTYRESP]: ' + HttpPoll.URL);
RestartPolling(1000);
Exit;
end;
ts := 0;
JSON := nil;
try
eventNameA := 'POST';
eventMsgA := 'Fetched new events';
eventData := RespStr;
NotifyListeners(IE_serverSent);
LastFetchBaseURL := '';
if not ParseJSON(RespStr, JSON) then
begin
PollError('[ERR_NOTAJSON]');
Exit;
end;
FatalErrorCount := 0;
Resp := CheckResponseData(JSON);
if Resp.Key = Ord(EAC_OK) then
if not (JSON.GetValue('fetchBaseURL') = nil) then
begin
JSON.GetValueSafe('fetchBaseURL', LastFetchBaseURL);
JSON.GetValueSafe('fetchTimeout', t);
HttpPoll.SetTimeout(Max(60, t) - (2 + Random(3)));
JSON.GetValueSafe('timeToNextFetch', t);
JSON.GetValueSafe('ts', ts);
fHostOffset := DateTimeToUnix(Now, False) - ts;
events := JSON.GetValue('events') as TJSONArray;
for event in events do
if Assigned(event) and (event is TJSONObject) then
begin
etype := TJSONObject(event).GetValue('type');
edata := TJSONObject(event).GetValue('eventData');
if Assigned(etype) and Assigned(edata) then
if etype.Value = 'buddylist' then
begin
ProcessContactList(TJSONObject(edata).GetValue('groups') as TJSONArray, True);
// Get caps of users currently online
GetAllCaps;
if AutoReqXStatus then
EventSubscribe;
NotifyListeners(IE_online);
// Get own profile's settings
GetProfile(MyAccNum);
end else if (etype.Value = 'presence') or (etype.Value = 'myInfo') then
ProcessContact(TJSONObject(edata))
else if etype.Value = 'histDlgState' then
ProcessDialogState(TJSONObject(edata))
else if etype.Value = 'imState' then
ProcessIMState(TJSONObject(edata))
else if etype.Value = 'typing' then
ProcessTyping(TJSONObject(edata))
else if etype.Value = 'userAddedToBuddyList' then
ProcessAddedYou(TJSONObject(edata))
else if etype.Value = 'permitDeny' then
ProcessPermitDeny(TJSONObject(edata))
else if etype.Value = 'diff' then
ProcessDiff(TJSONArray(edata))
else if etype.Value = 'status' then
ProcessXStatus(TJSONObject(edata))
else if etype.Value = 'suggest' then
ProcessSmartReply(TJSONObject(edata))
else if etype.Value = 'reactions' then
ProcessReactions(TJSONObject(edata))
else
ODS('Unhandled event type: ' + etype.Value);
end;
end
else // Events that do not continue events fetching
begin
LastFetchBaseURL := '';
events := json.GetValue('events') as TJSONArray;
for event in events do
if Assigned(event) and (event is TJSONObject) then
begin
etype := TJSONObject(event).GetValue('type');
edata := TJSONObject(event).GetValue('eventData');
if Assigned(etype) then
if etype.Value = 'sessionEnded' then
begin
if Assigned(edata) and (edata is TJSONObject) then
if edata.GetValueSafe('endCode', code) then
if (code = 142) or // "offReason" : "Killed Sessions"
(code = 143) or // "offReason" : "Fired employee", session kill too
(code = 26) then // "offReason" : "User Initiated Bump"
begin
CleanDisconnect := True;
StayConnected := False;
end;
LogICQPacket(WL_disconnected, '', '', 'Session end code: ' + IntToStr(code));
GoneOffline;
end;
end;
end;
finally
JSON.Free;
end;
RestartPolling(t);
end;
procedure TICQSession.RestartPolling(Delay: Integer = 100);
begin
if (LastFetchBaseURL = '') then
PollError('[ERR_UNCLEAN]')
else
begin
FreeAndNil(PollingTask);
PollingTask := TAnonTask.Create(procedure
begin
Sleep(Max(100, Delay)); // Min 100ms between fetches, just in case :)
if not TThread.Current.CheckTerminated then
TThread.Queue(nil, procedure
begin
if Running then
PollURL(LastFetchBaseURL);
end);
end);
PollingTask.Start;
end;
end;
procedure TICQSession.AbortPolling;
begin
HttpPoll.StopTimeout;
HttpPoll.Abort;
end;
procedure TICQSession.ProcessContactList(const CL: TJSONArray; Batch: Boolean = False);
var
buddies: TJSONArray;
id: Integer;
name: String;
begin
if not Assigned(CL) then
Exit;
if Batch then
building := True;
try
fRoster.ForEach(procedure(cnt: TICQContact)
begin
cnt.CntIsLocal := True;
cnt.LastStatusSubscribe := 0;
end);
groups.MakeAllLocal;
for var group in CL do
if Assigned(Group) and (Group is TJSONObject) then
begin
group.GetValueSafe('name', name);
group.GetValueSafe('id', id);
if groups.Exists(id) then
groups.RenameLocal(id, name)
else
groups.AddWithValues(id, name);
groups.SetLocal(id, False);
buddies := TJSONObject(group).GetValue('buddies') as TJSONArray;
for var buddy in buddies do
if Assigned(buddy) then
ProcessContact(TJSONObject(buddy), id, True);
end;
except end;
if Batch then
building := False;
end;
function TICQSession.GetAccountUID(const Buddy: TJSONObject): String;
var
Tmp: String;
begin
Result := Buddy.GetValue('aimId').Value;
if Buddy.GetValueSafe('attachedPhoneNumber', Tmp) then
if not (Tmp = '') then
begin
AttachedLoginPhone := Tmp;
if '+' + AttachedLoginPhone = StartAccount then
MyAccount := Result;
end;
end;
function TICQSession.ProcessContact(const Buddy: TJSONObject; GroupToAddTo: Integer = -1; Batch: Boolean = False): TICQContact;
var
I, Mute, ECC: Integer;
B, FoundCap: Boolean;
Tmp, PhoneType, OldStatusStr: String;
TheCap, TheCap2: RawByteString;
UnixTime: Integer;
NewStatus: TICQStatus;
Profile, TmpObj: TJSONObject;
Ph, TmpArr: TJSONValue;
Caps: TJSONArray;
begin
Result := nil;
if not Assigned(Buddy) then
Exit;
Result := GetICQContact(GetAccountUID(Buddy));
if not Assigned(Result) then
Exit;
// if Buddy.GetValueSafe('abContactName', Name) then
// if not (Name = '') then
// Result.Nick := Name
if Buddy.GetValueSafe('nick', Tmp) then
if not (Tmp = '') then
Result.Nick := Tmp;
// if (Result.Nick = '') then
// begin
// if Buddy.GetValueSafe('displayId', Tmp) then
// if not (Tmp = '') then
// Result.Nick := Tmp;
// if Buddy.GetValueSafe('friendly', Tmp) then
// if not (Tmp = '') then
// Result.Nick := Tmp;
// end;
if Buddy.GetValueSafe('emailId', Tmp) then
if not TryStrToInt(Tmp, I) then
Result.Email := Tmp;
TmpArr := Buddy.GetValue('abPhones');
if Assigned(TmpArr) and (TmpArr is TJSONArray) and (TJSONArray(TmpArr).Count > 0) then
for Ph in TJSONArray(TmpArr) do
if Assigned(Ph) then
begin
Ph.GetValueSafe('type', PhoneType);
if not Ph.GetValueSafe('rawNumber', Tmp) then
Ph.GetValueSafe('number', Tmp);
if not (Tmp = '') then
if PhoneType = 'home' then
Result.Regular := Tmp
else if PhoneType = 'mobile' then
Result.Cellular := Tmp
else if PhoneType = 'work' then
Result.WorkPhone := Tmp
else if PhoneType = 'other' then
Result.OtherPhone := Tmp
end;
// Useless, just a copy of server notes
// if Buddy.GetValueSafe('cellNumber', Tmp) then
// Result.Cellular := Tmp;
// if Buddy.GetValueSafe('phoneNumber', Tmp) then
// Result.Regular := Tmp;
// if Buddy.GetValueSafe('workNumber', Tmp) then
// Result.Workphone := Tmp;
// if Buddy.GetValueSafe('otherNumber', Tmp) then
// Result.OtherPhone := Tmp;
// if Buddy.GetValueSafe('smsNumber', Tmp) then
// Result.SMSMobile := Tmp;
// Result.SMSable := not (Result.SMSMobile = '');
if Buddy.GetValueSafe('official', I) then
Result.Official := I = 1;
if Buddy.GetValueSafe('bot', B) then
Result.Bot := B;
if Buddy.GetValueSafe('deleted', I) then
Result.Deleted := I = 1
else
Result.Deleted := False;
Buddy.GetValueSafe('userType', Tmp);
Result.UserType := CT_UNK;
if Tmp = 'sms' then
begin
Result.UserType := CT_SMS;
Result.SMSable := True;
end else if Tmp = 'phone' then
Result.UserType := CT_PHONE
else if Tmp = 'icq' then
Result.UserType := CT_ICQ
else if Tmp = 'chat' then
Result.UserType := CT_CHAT
else if Tmp = 'interop' then // MRA?
Result.UserType := CT_INTEROP
else if Tmp = 'oldIcq' then
Result.UserType := CT_OLDICQ;
//Other possible types:
//aim - AIM or AOL
//interop - Gatewayed from another network
//imserv - IMServ group target
if not (Buddy.GetValue('capabilities') = nil) then
begin
Result.LastCapsUpdate := Now;
Result.CapabilitiesSm := [];
Result.CapabilitiesBig := [];
Result.ExtraCapabilities := '';
ECC := 0;
Caps := Buddy.GetValue('capabilities') as TJSONArray;
if Assigned(Caps) then
for var Cap in Caps do
if Assigned(Cap) then
begin
TheCap := Hex2String(Cap.Value);
FoundCap := False;
for I := 1 to Length(BigCapability) do
if TheCap = BigCapability[i].v then
begin
Include(Result.CapabilitiesBig, I);
FoundCap := True;
Break;
end;
if Copy(TheCap, 1, 2) = CapsMakeBig1 then
if Copy(TheCap, 5, 12) = CapsMakeBig2 then
begin
TheCap2 := Copy(TheCap, 3, 2);
for I := 1 to Length(CapsSmall) do
if TheCap2 = CapsSmall[i].v then
begin
Include(Result.CapabilitiesSm, I);
FoundCap := True;
Break;
end;
end;
var TheCapPrefix := Copy(TheCap, 1, 5);
if TheCapPrefix = 'RDEC0' then Inc(ECC)
else if TheCapPrefix = 'RDEC1' then Inc(ECC, 2)
else if TheCapPrefix = 'RDEC2' then Inc(ECC, 4);
if not FoundCap then
Result.ExtraCapabilities := Result.ExtraCapabilities + TheCap;
end;
Result.Crypt.SupportCryptMsg := CAPS_big_CryptMsg in Result.CapabilitiesBig;
Result.Crypt.SupportEcc := False;
I := Pos('RDEC0', Result.ExtraCapabilities);
if ECC = 7 then
if I > 0 then
begin
Result.Crypt.EccPubKey := Copy(Result.ExtraCapabilities, I + 5, 11);
I := Pos('RDEC1', Result.ExtraCapabilities);
if I > 0 then
begin
Result.Crypt.EccPubKey := Result.Crypt.EccPubKey + Copy(Result.ExtraCapabilities, I + 5, 11);
I := Pos('RDEC2', Result.ExtraCapabilities);
if I > 0 then
begin
Result.Crypt.EccPubKey := Result.Crypt.EccPubKey + Copy(Result.ExtraCapabilities, I + 5, 11);
Result.Crypt.SupportEcc := Length(Result.Crypt.EccPubKey) = 33;
if Result.Crypt.SupportEcc and fECCKeys.generated then
begin
SetLength(Result.Crypt.EccMsgKey, SizeOf(TECCSecretKey));
if not Ecc256r1SharedSecret(PECCPublicKey(Result.Crypt.EccPubKey)^, fECCKeys.PrivKey, PECCSecretKey(Result.Crypt.EccMsgKey)^) then
begin
Result.Crypt.EccMsgKey := '';
Result.Crypt.SupportEcc := False;
end;
end;
end;
end;
end;
Result.Typing.bSupport := CAPS_big_MTN in Result.CapabilitiesBig;
end;
NewStatus := SC_OFFLINE;
if Buddy.GetValueSafe('state', Tmp) then
if Tmp = 'online' then
NewStatus := SC_ONLINE
else
NewStatus := SC_OFFLINE;
TmpObj := Buddy;
if not (Buddy.GetValue('userState') = nil) then
TmpObj := Buddy.GetValue('userState') as TJSONObject;
if TmpObj.GetValueSafe('lastseen', UnixTime) then
if UnixTime = 0 then
begin
if Buddy.GetValueSafe('onlineTime', UnixTime) then
begin
NewStatus := SC_ONLINE;
if not Batch and (Result.Status = SC_ONLINE) and (UnixTime = 0) then
begin
ActionManager.AddContact(Result.UID);
ActionManager.Execute(AK_GETLASTSEEN, 5000);
end;
end else
NewStatus := SC_OFFLINE
end else
Result.LastTimeSeenOnline := UnixToDateTime(UnixTime, False);
Result.NoClient := False;
if Buddy.GetValueSafe('onlineTime', UnixTime) then
begin
Result.OnlineTime := UnixTime;
if (NewStatus = SC_OFFLINE) and (UnixTime > 0) then
begin
Result.NoClient := True;
Result.ClientClosed := Now - UnixTime * DTseconds;
end;
end;
if Result.UserType = CT_CHAT then
NewStatus := SC_ONLINE;
if Buddy.GetValueSafe('idleTime', UnixTime) then
Result.IdleTime := UnixTime;
if Buddy.GetValueSafe('statusTime', UnixTime) then
Result.LastStatusUpdate := UnixToDateTime(UnixTime, False);
// if Buddy.GetValueSafe('awayTime', UnixTime) then
// Result.AwayTime := UnixTime;
if Buddy.GetValueSafe('memberSince', UnixTime) then
begin
Result.MemberSince := UnixToDateTime(UnixTime);
if NewStatus = SC_OFFLINE then
Result.NoClient := True;
end;
if Buddy.GetValueSafe('mute', Mute) then
Result.Muted := Mute > 0
else
Result.Muted := False;
// awayMsg, profileMsg - ?
try
OldStatusStr := Result.StatusStr;
if Buddy.GetValueSafe('statusMsg', Tmp) then
Result.StatusStr := HTMLEntitiesDecode(Tmp);
if (Result.StatusStr = '') and Buddy.GetValueSafe('moodTitle', Tmp) then
Result.StatusStr := HTMLEntitiesDecode(Tmp);
//XStatusArray[curXStatus].pid6
except
// Cannot decode HTML for some reason
end;
// Owner only
if Result.UID = MyAccNum then
begin
NewStatus := CurStatus;
Result.NoClient := not IsReady;
end;
Profile := TJSONObject(Buddy.GetValue('profile'));
if Assigned(Profile) then
with Profile do
begin
GetValueSafe('firstName', Result.first);
GetValueSafe('lastName', Result.last);
if GetValueSafe('nick', Tmp) then
if not (Tmp = '') then
Result.Nick := Tmp;
if GetValueSafe('gender', Tmp) then
Result.Gender := SrvStrToGenderI(Tmp)
else
Result.Gender := 0;
// Not there in new proto
if GetValueSafe('relationshipStatus', Tmp) then
Result.MarStatus := SrvStrToMarStI(Tmp)
else
Result.MarStatus := $0000;
if GetValueSafe('birthDate', Tmp) then
if TryStrToInt(Tmp, UnixTime) then
if UnixTime < 0 then
Result.Birth := 0
else
Result.Birth := UnixToDateTime(UnixTime, True);
TmpArr := Profile.GetValue('homeAddress');
if Assigned(TmpArr) and (TmpArr is TJSONArray) and (TJSONArray(TmpArr).Count > 0) then
begin
TmpObj := TJSONObject(TJSONArray(TmpArr).Items[0]);
TmpObj.GetValueSafe('country', Result.Country);
TmpObj.GetValueSafe('state', Result.State);
TmpObj.GetValueSafe('city', Result.City);
end;
// Not there in new proto
GetValueSafe('lang1', Result.Lang[1]);
GetValueSafe('lang2', Result.Lang[2]);
GetValueSafe('lang3', Result.Lang[3]);
// Not there in new proto?
TmpArr := Profile.GetValue('phones');
if Assigned(TmpArr) and (TmpArr is TJSONArray) and (TJSONArray(TmpArr).Count > 0) then
for Ph in TJSONArray(TmpArr) do
if Assigned(Ph) then
begin
Ph.GetValueSafe('type', PhoneType);
Ph.GetValueSafe('phone', Tmp);
if not (Tmp = '') then
begin
if PhoneType = 'home' then
Result.Regular := Tmp
else if PhoneType = 'mobile' then
Result.Cellular := Tmp
else if PhoneType = 'work' then
Result.WorkPhone := Tmp
else if PhoneType = 'other' then
Result.OtherPhone := Tmp
end;
end;
GetValueSafe('aboutMe', Result.About);
GetValueSafe('statusLine', Result.LifeStatus);
// Possible fields: jobs[], validatedEmail, pendingEmail, emails[], studies[], interests[], groups[], pasts[]
// anniversary, children, smoking, height, lastupdated, hideFlag, validatedCellular
{
"birthDate" : -2147472000,
"education" : "unknown",
"religion" : "unknown",
"hairColor" : "unknown",
"sexualOrientation" : "unknown",
"photo" : "false",
"betaFlag" : 0,
"autoSms" : "false", // autoforward IM to SMS
}
Result.Authorized := True; // Assume this is the default :)
if GetValueSafe('authRequired', I) then
Result.Authorized := I = 0;
// Owner only
if Result.UID = MyAccNum then
begin
Result.Authorized := True;
{
if GetValueSafe('webAware', I) then
webAware := I = 1;
if GetValueSafe('authRequired', I) then
authNeeded := I = 1;
if GetValueSafe('hideLevel', Tmp) then
if Tmp = 'none' then
showInfo := 0
else if Tmp = 'emailsAndCellular' then
showInfo := 1
else if Tmp = 'allExceptFln' then
showInfo := 2
else if Tmp = 'all' then
showInfo := 3
else
showInfo := 0;
}
// "privateKey" : "945b74c51c594c4c987f0b194fdabbd6",
// "allowEmail" : "false",
end;
end;
//location - Information that the user has provided about their location
//pending - ICQWEB: For buddylist events, any pending authorization buddies will have this flag
//recent - For buddylist events, any buddies in the Recent Buddies group will have this set
//bot - For buddylist events, any buddies that are BOTs will have this set
//shared - For buddylist events, any buddies in the a shared buddies group will have this set
// Add to roster if it's CL response and group is defined
if GroupToAddTo >= 0 then
begin
Result.CntIsLocal := False;
Result.Group := GroupToAddTo;
if not Result.IsInRoster then
fRoster.add(Result);
end;
// Handle status change
ProcessNewStatus(Result, NewStatus, not (OldStatusStr = Result.StatusStr), Batch);
GetClientPicAndDesc4(Result, Result.ClientPic, Result.ClientDesc);
if Buddy.GetValueSafe('iconId', Tmp) then
if not (Tmp = Result.IconID) then
begin
Result.IconID := Tmp;
eventContact := Result;
NotifyListeners(IE_avatar_changed);
if IsMyAcc(Result) then
MyAvatarHash := Result.IconID;
end;
Result.InfoUpdatedTo := Now;
eventTime := Now;
eventContact := Result;
NotifyListeners(IE_userinfo);
end;
procedure TICQSession.ProcessNewStatus(var Cnt: TICQcontact; NewStatus: TICQstatus; XStatusStrChanged: Boolean = False; NoNotify: Boolean = False);
var
StatusChanged: Boolean;
begin
InitListenerVars(Cnt, Now);
Cnt.PrevStatus := Cnt.Status;
eventOldStatus := Cnt.Status;
Cnt.BirthFlag := BirthdayFlag and (not (Cnt.birth = 0) or not (Cnt.birthL = 0));
StatusChanged := not (NewStatus = eventOldStatus);
//ODS(String(Cnt.uid) + ': ' + inttostr(integer(eventOldStatus)) + ' -> ' + inttostr(integer(newstatus)));
if StatusChanged then
Cnt.Status := NewStatus;
// Very slow with large CL otherwise
if NoNotify then
Exit;
if StatusChanged then
begin
if Cnt.PrevStatus = SC_UNK then
NotifyListeners(IE_statuschanged)
else if Cnt.PrevStatus = SC_OFFLINE then
NotifyListeners(IE_incoming)
else
begin
Cnt.LastTimeSeenOnline := eventTime;
Cnt.ClientClosed := eventTime;
NotifyListeners(IE_outgoing);
end;
end else if XStatusStrChanged then
begin
NotifyListeners(IE_statuschanged);
EventResubscribe(Cnt);
end else
NotifyListeners(IE_contactupdate);
end;
procedure TICQSession.ProcessUsersAndGroups(const JSON: TJSONObject);
var
groups: TJSONValue;
users: TJSONArray;
begin
if not IsReady then
Exit;
groups := JSON.GetValue('groups');
if Assigned(groups) and (groups is TJSONArray) then
ProcessContactList(TJSONArray(groups));
try
users := JSON.GetValue('users') as TJSONArray;
if Assigned(users) then
for var user in users do
ProcessContact(TJSONObject(user), -1, True);
except end;
end;
function TICQSession.ProcessMsg(const UID: TUID; Msg: TJSONObject; Patch: Boolean = False): TMsgID;
var
Contact: TICQContact;
Outgoing: Boolean;
MsgID: TMsgID;
procedure DecryptMessage(Encrypted: Integer; Payload: TJSONObject; var Msg: String);
var
RQCompressed, RQLen: Integer;
RQCRC: Cardinal;
Ctx: TAESECB;
Key: TSHA256Digest;
Msg2, CrptMsg: TBytes;
i: Integer;
begin
Payload.GetValueSafe('compressed', RQCompressed);
Payload.GetValueSafe('crc', RQCRC);
Payload.GetValueSafe('length', RQLen);
if RQLen = 0 then
begin
eventError := EC_FailedDecrypt;
eventMsgA := GetTranslation('Zero length message');
NotifyListeners(IE_error);
Exit;
end else if RQCRC = 0 then
begin
eventError := EC_FailedDecrypt;
eventMsgA := GetTranslation('Incorrect CRC');
NotifyListeners(IE_error);
Exit;
end else if not (RQCompressed in [0,1]) then
begin
eventError := EC_FailedDecrypt;
eventMsgA := GetTranslation('Unknown type of compression [%d]', [RQCompressed]);
NotifyListeners(IE_error);
Exit;
end else if Contact.Crypt.EccMsgKey = '' then
begin
eventError := EC_FailedDecrypt;
eventMsgA := GetTranslation('Unable to create encryption key');
NotifyListeners(IE_error);
Exit;
end;
eventFlags := eventFlags or IF_Encrypt;
if Encrypted = 2 then
eventFlags := eventFlags or IF_Encrypt_ECC;
Msg2 := TEncoding.ANSI.GetBytes(Msg); // Should be Base64
Base64DecodeBytes(Msg2, CrptMsg);
SetLength(Msg2, 0);
CalcKey(
Encrypted = 2,
IfThen(Encrypted = 2, Contact.Crypt.EccMsgKey, ''),
IfThen(Outgoing, MyAccount, Contact.UID),
IfThen(Outgoing, Contact.UID, MyAccount),
0, RQLen, Key
);
i := Length(CrptMsg);
SetLength(Msg2, i + AESBLKSIZE);
ctx := TAESECB.Create(Key[0], 256);
ctx.Decrypt(@CrptMsg[0], @Msg2[0], i);
ctx.Free;
//ODS('Compressed: ' + TEncoding.ANSI.GetString(Msg2));
if RQCompressed = 1 then
Msg2 := ZDecompressBytes(Msg2);
//ODS('Decompressed: ' + TEncoding.ANSI.GetString(Msg2));
SetLength(Msg2, RQLen);
//ODS('Length applied: ' + TEncoding.ANSI.GetString(Msg2));
//ODS('My CRC: ' + inttostr(ZipCrc32($FFFFFFFF, @Msg2[0], RQLen) XOR $FFFFFFFF));
//ODS('His CRC: ' + IntToStr(RQCRC));
if Length(Msg2) > 0 then
if not ((ZipCrc32($FFFFFFFF, @Msg2[0], RQLen) XOR $FFFFFFFF) = RQCRC) then
begin
eventError := EC_FailedDecrypt;
eventMsgA := GetTranslation('Bad CRC');
NotifyListeners(IE_error);
//eventFlags := eventFlags and not IF_Bin and not IF_CODEPAGE_MASK;
end else
Msg := TEncoding.UTF8.GetString(Msg2)
else
begin
eventError := EC_FailedDecrypt;
eventMsgA := GetTranslation('Zero length message');
NotifyListeners(IE_error);
end;
{
eventContact := c;
notificationForMsg(msgtype, msgflags, priority=2, msg);
case getStatus of
byte(SC_away): sendACK(thisCnt, ACK_AWAY, '');
byte(SC_na): sendACK(thisCnt, ACK_NA, '');
byte(SC_dnd), byte(SC_occupied):
if priority = 2 then
sendACK(thisCnt, ACK_OK, '', msgDwnCnt)
else
sendACK(thisCnt, ACK_NOBLINK,'')
else sendACK(thisCnt, ACK_OK, '', msgDwnCnt)
end;
}
end;
function CheckDataPayload(var Msg: String): Boolean;
var
Payload: TJSONObject;
RQCaps: TJSONValue;
RQType: String;
Caps: TArray;
Pub: array [0..2] of Integer;
Encryped, ECC, i: Integer;
RcvPubKey: AnsiString;
begin
Result := False;
Encryped := 0;
if ParseJSON(Msg, Payload) then
try
Payload.GetValueSafe('type', RQType);
if not (RQType = 'RnQDataIM') then
Exit;
RQCaps := Payload.GetValue('caps');
if not Assigned(RQCaps) or not (RQCaps is TJSONArray) then
Exit;
Payload.GetValueSafe('data', Msg);
// No caps? Get data as is
if TJSONArray(RQCaps).Count = 0 then
Exit;
for var RQCap in TJSONArray(RQCaps) do
if Assigned(RQCap) and (RQCap is TJSONString) then
Insert(TJSONString(RQCap).Value, Caps, High(Caps));
// Buzz
if MatchText(String2Hex(BigCapability[CAPS_big_Buzz].v), Caps) and not Outgoing then
begin
eventContact := Contact;
NotifyListeners(IE_buzz);
Result := True;
Exit;
end;
// Contact failed to decrypt your message
if MatchText(String2Hex(CapsInvalidPublicKey), Caps) then
begin
if not Outgoing then
begin
eventContact := Contact;
eventError := EC_FailedDecryptNotif;
NotifyListeners(IE_error);
eventContact := Contact;
eventMsgID := StrToUInt64(Msg);
NotifyListeners(IE_MsgDecryptFailed);
end;
Result := True;
Exit;
end;
// Regular crypt
if MatchText(String2Hex(BigCapability[CAPS_big_CryptMsg].v), Caps) then
Encryped := 1;
// ECC crypt
ECC := 0;
for i := Low(Caps) to High(Caps) do
begin
if Caps[i].StartsWith(String2Hex('RDEC0')) then begin Pub[0] := i; Inc(ECC); end
else if Caps[i].StartsWith(String2Hex('RDEC1')) then begin Pub[1] := i; Inc(ECC, 2); end
else if Caps[i].StartsWith(String2Hex('RDEC2')) then begin Pub[2] := i; Inc(ECC, 4); end;
end;
if ECC = 7 then
begin
RcvPubKey := THelpers.IfThen(Outgoing, Contact.Crypt.EccPubKey, AnsiString(TEncoding.ANSI.GetString(fECCKeys.PubEccKey)));
if RcvPubKey = Copy(Hex2String(Caps[Pub[0]]), 6, 11) + Copy(Hex2String(Caps[Pub[1]]), 6, 11) + Copy(Hex2String(Caps[Pub[2]]), 6, 11) then
Encryped := 2
else
begin
eventError := EC_FailedDecrypt;
eventMsgA := GetTranslation('Message from %s was encrypted using outdated public key, sender has been notified about this', [Contact.Displayed]);
NotifyListeners(IE_error);
SendDecryptError(Contact, MsgID);
Result := True;
Exit;
end;
end;
if Encryped > 0 then
DecryptMessage(Encryped, Payload, Msg);
finally
FreeAndNil(Payload);
end;
end;
var
StickerStr, mType, sTmp: String;
Ack, iTmp: Integer;
evtmp: Thevent;
FetchReactions, UINAdded, bTmp: Boolean;
Parts: TJSONArray;
Sticker, VoIP, Event, RNotify: TJSONValue;
ExtSticker: TStringDynArray;
begin
Result := 0;
if not Assigned(Msg) then
Exit;
Contact := Account.AccProto.GetContact(UID);
Msg.GetValueSafe('text', sTmp);
if not (sTmp = '') and ContainsStr(sTmp, 'RnQDataIM') and (Contact.Crypt.EccMsgKey = '') then
GetContactInfo(Contact.UID, 'capabilities');
InitListenerVars;
eventData := '';
eventEncoding := TEncoding.Default;
if Msg.GetValueSafe('msgId', MsgID) then
eventMsgID := MsgID;
if Msg.GetValueSafe('wid', sTmp) then
eventWID := sTmp;
if Msg.GetValueSafe('time', iTmp) then
eventTime := UnixToDateTime(iTmp, False)
else
eventTime := Now;
if not (Msg.GetValue('updatePatchVersion') = nil) then
eventFlags := eventFlags or IF_Patched;
Outgoing := False;
if Msg.GetValueSafe('outgoing', Outgoing) then
if Outgoing and not Patch then
begin
Msg.GetValueSafe('reqId', sTmp);
Ack := Account.acks.FindID(sTmp);
if Ack >= 0 then
begin
eventData := sTmp;
eventMsgA := 'delivered';
NotifyListeners(IE_serverAck);
Exit;
end;
end;
if not Patch and not (eventMsgID = 0) then
begin
evtmp := SQLDB.GetByMsgID(Contact.UID, eventMsgID, False);
if Assigned(evtmp) then
begin
ODS('Msg is already in history (MsgID ' + IntToStr(eventMsgID) + ')');
FreeAndNil(evtmp);
Exit;
end;
end;
eventInBackground := eventMsgID <= SrvHist.LastRead;
// offlineIM/dataIM:
// "imf": "plain"
// "autoresponse" : 0
mType := 'text'; // text or sticker
if Msg.GetValueSafe('mediaType', sTmp) then
if not (sTmp = '') then
mType := sTmp;
if (mType = 'text') and not (Msg.GetValue('sticker') = nil) then
mType := 'sticker';
if mType = 'sticker' then
begin
Sticker := Msg.GetValue('sticker');
if Sticker.GetValueSafe('id', sTmp) then
StickerStr := sTmp;
ExtSticker := SplitString(StickerStr, ':');
if (Length(ExtSticker) >= 4) then
begin
eventData := StickerStr;
eventFlags := eventFlags or IF_sticker;
end;
end else if mType = 'voip' then
begin
VoIP := Msg.GetValue('voip');
bTmp := False;
if Assigned(VoIP) then
VoIP.GetValueSafe('video', bTmp);
eventData := GetTranslation('Contact was trying to initiate %s call', [AnsiLowerCase(GetTranslation(IfThen(bTmp, 'Video', 'Audio')))]);
eventFlags := eventFlags or IF_Not_Delivered;
end else if Msg.GetValueSafe('class', sTmp) and (sTmp = 'event') then
begin
if Msg.GetValueSafe('text', sTmp) then
begin
eventData := GetTranslation(sTmp);
Msg.GetValueSafe('eventTypeId', sTmp);
if sTmp = '27:51000' then // Message was removed
if Patch then
begin
eventContact := Contact;
NotifyListeners(IE_MsgPatchDelete);
Exit;
end;
// Stranger danger!
Event := Msg.GetValue('event');
if Assigned(Event) then
if Event.GetValueSafe('type', sTmp) then
if (sTmp = 'warnAboutStranger') or (sTmp = 'noLongerStranger') then
Exit;
end else // ignore all textless events, e.g. chat invites, etc
ODS('Unhandled event type: ' + IfThen(Msg.GetValue('chat') = nil, 'unknown', 'chat'));
Exit;
end else if Msg.GetValueSafe('text', sTmp) then
begin
eventData := sTmp;
// Process special R&Q messages
if not (eventData = '') and ContainsStr(eventData, 'RnQDataIM') then
if CheckDataPayload(eventData) then
Exit;
end;
// Reactions
FetchReactions := False;
if not (Msg.GetValue('reactions') = nil) then
Msg.GetValue('reactions').GetValueSafe('exist', FetchReactions);
// Notifications in a special chat about all reactions to your messages
Parts := Msg.GetValue('parts') as TJSONArray;
if Assigned(Parts) then
for var Part in Parts do
begin
UINAdded := False;
if (eventData = '') then
if Part.GetValueSafe('mediaType', sTmp) then
if sTmp = 'text' then
if Part.GetValueSafe('text', sTmp) then
begin
eventData := sTmp;
UINAdded := True;
end;
RNotify := TJSONObject(Part).GetValue('reactionsNotify');
if Assigned(RNotify) and RNotify.GetValueSafe('msgId', MsgID) then
begin
sTmp := '';
RNotify.GetValueSafe('fromDialog', sTmp);
if not UINAdded and not (sTmp = '') then
eventData := eventData + ' @ ' + sTmp;
ParseReactions(MsgID, sTmp, eventMsgID, TJSONArray(TJSONObject(RNotify).GetValue('reactions')), '-');
end;
end;
eventContact := Contact;
if Outgoing then
begin
if Length(eventData) > 0 then
AddOutgoingMessage(Contact, '', eventData, eventTime, eventFlags, eventMsgID, eventWID, Patch);
end
else
begin
Result := eventMsgID;
if Patch then
NotifyListeners(IE_MsgPatchUpdate)
else
NotifyListeners(IE_msg);
end;
if FetchReactions then
GetReactions(UID, eventMsgID);
end;
procedure TICQSession.ProcessDialogState(const Dlg: TJSONObject);
var
c: TICQContact;
Starting: Boolean;
sn, sTmp, PatchVersion: String;
iTmp: Integer;
LastMsgId, LastRead, MaxProcessedMsgId: TMsgID;
Yours, Theirs, Msg, MsgPos: TJSONValue;
Msgs, Persons: TJSONArray;
begin
if not Assigned(Dlg) or (Dlg.Count = 0) then
Exit;
MaxProcessedMsgId := 0;
Dlg.GetValueSafe('sn', sn);
if sn = '' then
begin
Persons := TJSONArray(Dlg.GetValue('persons'));
if Assigned(Persons) and (Persons.Count > 0) then
for var Person in Persons do
if Person is TJSONObject then
if (Persons.Count = 1) or ((Persons.Count > 1) and not (TJSONObject(Person).GetValue('sn').Value = MyAccNum)) then
Person.GetValueSafe('sn', sn);
end;
c := nil;
if not (sn = '') then
c := GetICQContact(sn);
if not Assigned(c) then
begin
eventError := EC_MalformedMsg;
eventMsgA := Dlg.ToString;
NotifyListeners(IE_error);
Exit;
end;
with Dlg do
begin
PatchVersion := '';
if GetValueSafe('patchVersion', sTmp) then
PatchVersion := sTmp;
GetValueSafe('lastMsgId', LastMsgId);
SrvHist := SQLDB.GetHistDlg(c.UID);
// Delivery/read status
LastRead := 0;
Yours := GetValue('yours');
if Assigned(Yours) then
begin
if Yours.GetValueSafe('lastRead', LastRead) then
if SrvHist.LastRead = 0 then
begin
SQLDB.UpdateLastRead(c.UID, LastRead);
SrvHist.LastRead := LastRead;
end;
end;
Theirs := GetValue('theirs');
if Assigned(Theirs) then
begin
eventContact := c;
if Theirs.GetValueSafe('lastDelivered', eventMsgID) then
NotifyListeners(IE_ack);
// if Theirs.GetValueSafe('lastRead', eventMsgID) then
// NotifyListeners(IE_readAck);
end;
GetValueSafe('unreadCnt', iTmp);
GetValueSafe('unreadMentionMeCount', iTmp);
if not (PatchVersion = SrvHist.PatchVersion) then
GetServerPatches(c.UID, SrvHist.PatchVersion);
if GetValueSafe('starting', Starting) then
if Starting then
begin
SQLDB.UpdateLastMsg(c.UID, LastMsgId, PatchVersion);
if (LastMsgId > 0) and (LastMsgId > SrvHist.LastMsgId) then
GetServerHistory(c.UID, SrvHist.LastMsgId, SrvHist.PatchVersion);
Exit;
end;
if (LastMsgId > 0) and not (PatchVersion = '') then
SQLDB.UpdateLastMsg(c.UID, LastMsgId, PatchVersion);
Msgs := GetValue('messages') as TJSONArray;
if Assigned(Msgs) then
for Msg in Msgs do
if Msg is TJSONObject then
MaxProcessedMsgId := Max(MaxProcessedMsgId, ProcessMsg(sn, TJSONObject(Msg)));
MsgPos := GetValue('intro');
if Assigned(MsgPos) and (MsgPos is TJSONObject) then
begin
Msgs := TJSONObject(MsgPos).GetValue('messages') as TJSONArray;
if Assigned(Msgs) then
for Msg in Msgs do
if Msg is TJSONObject then
MaxProcessedMsgId := Max(MaxProcessedMsgId, ProcessMsg(sn, TJSONObject(Msg)));
end;
MsgPos := GetValue('tail');
if Assigned(MsgPos) and (MsgPos is TJSONObject) then
begin
Msgs := TJSONObject(MsgPos).GetValue('messages') as TJSONArray;
if Assigned(Msgs) then
for Msg in Msgs do
if Msg is TJSONObject then
MaxProcessedMsgId := Max(MaxProcessedMsgId, ProcessMsg(sn, TJSONObject(Msg)));
end;
if (MaxProcessedMsgId > 0) and not (SrvHist.LastRead = MaxProcessedMsgId) then
begin
SQLDB.UpdateLastRead(c.UID, MaxProcessedMsgId);
MarkRead(c.UID, MaxProcessedMsgId);
end;
end;
end;
procedure TICQSession.ProcessIMState(const Data: TJSONObject);
var
IMStates: TJSONArray;
WID, State: String;
UnixTime: Integer;
MsgID: TMsgID;
begin
if not Assigned(Data) then
Exit;
IMStates := Data.GetValue('imStates') as TJSONArray;
if Assigned(IMStates) then
for var IMState in IMStates do
with IMState do
begin
GetValueSafe('state', State);
if State = '' then
Continue;
InitListenerVars;
eventMsgA := State;
if GetValueSafe('ts', UnixTime) then
eventTime := UnixToDateTime(UnixTime, False);
if GetValueSafe('msgId', WID) then
eventWID := WID;
if GetValueSafe('histMsgId', MsgID) then
eventMsgID := MsgID;
if GetValueSafe('sendReqId', eventData) then
NotifyListeners(IE_serverAck);
end;
end;
procedure TICQSession.ProcessTyping(const Data: TJSONObject);
var
Contact: TICQContact;
TypingStatus: String;
begin
if not Assigned(Data) then
Exit;
Contact := GetICQContact(Data.GetValue('aimId').Value);
if not Assigned(Contact) then
Exit;
InitListenerVars(Contact, Now);
Data.GetValueSafe('typingStatus', TypingStatus);
if TypingStatus = 'typing' then
eventInt := MTN_BEGUN
else if TypingStatus = 'typed' then
eventInt := MTN_TYPED
else
eventInt := MTN_FINISHED;
case eventInt of
MTN_FINISHED, MTN_TYPED, MTN_CLOSED: eventContact.typing.bIsTyping := False;
MTN_BEGUN: eventContact.typing.bIsTyping := True;
end;
NotifyListeners(IE_typing);
end;
procedure TICQSession.ProcessAddedYou(const Data: TJSONObject);
var
Contact: TICQContact;
NeedAuth: Integer;
Msg: String;
begin
if not Assigned(Data) then
Exit;
Contact := GetICQContact(Data.GetValue('requester').Value);
if not Assigned(Contact) then
Exit;
//Data.GetValueSafe('displayAIMid', Name);
Data.GetValueSafe('authRequested', NeedAuth);
Data.GetValueSafe('msg', Msg);
InitListenerVars(Contact, Now);
eventMsgA := Msg;
NotifyListeners(IE_addedYou);
if NeedAuth = 1 then
begin
InitListenerVars(Contact, Now);
eventMsgA := Msg;
NotifyListeners(IE_authReq);
end;
end;
procedure TICQSession.ProcessPermitDeny(const Data: TJSONObject);
var
c: TICQContact;
Mode: String;
Items: TJSONValue;
begin
if not Assigned(Data) then
Exit;
SpamList.Clear;
Items := Data.GetValue('ignores');
if Assigned(Items) and (Items is TJSONArray) then
for var Item in TJSONArray(Items) do
begin
c := nil;
if not (TJSONString(Item).Value = '') then
c := GetICQContact(TJSONString(Item).Value);
if not Assigned(c) then
Continue;
SpamList.Add(c);
AddToIgnoreList(c, True);
eventContact := c;
NotifyListeners(IE_contactupdate);
end;
(* Clear unsupported block list?
Items := Data.GetValue('blocks');
if Assigned(Items) and (Items is TJSONArray) then
for var Item in TJSONArray(Items) do
RemFromBlock(TJSONString(Item).Value);
*)
if Data.GetValueSafe('pdMode', Mode) then
if not (Mode = '') then
if (SpamList.Count > 0) and not (Mode = 'denySome') then
SetPermitDenyMode('denySome');
UpdatePrefsFrm;
end;
procedure TICQSession.ProcessDiff(const Data: TJSONArray);
var
DiffType, GroupName: String;
Contact: TICQContact;
Groups, Buddies: TJSONArray;
GroupID: Integer;
begin
if not Assigned(Data) then
Exit;
for var Diff in Data do
if Diff is TJSONObject then
if TJSONObject(Diff).GetValueSafe('type', DiffType) then
if DiffType = 'created' then
begin
Groups := TJSONArray(TJSONObject(Diff).GetValue('data'));
if Assigned(Groups) then
for var Group in Groups do
if Group is TJSONObject then
begin
if not TryStrToInt(TJSONObject(Group).GetValue('id').Value, GroupID) then
Continue;
GroupName := '';
TJSONObject(Group).TryGetValue('name', GroupName);
Buddies := TJSONObject(Group).GetValue('buddies') as TJSONArray;
if Assigned(Buddies) then
for var Buddy in Buddies do
if Assigned(Buddy) and (Buddy is TJSONObject) then
begin
Contact := GetICQContact(TJSONObject(Buddy).GetValue('aimId').Value);
if Assigned(Contact) and (not Contact.IsInRoster or not (Contact.Group = GroupID)) then
MoveToGroup(Contact, GroupID, GroupName);
end;
end;
end;
end;
procedure TICQSession.ProcessXStatus(const Data: TJSONObject);
var
Contact: TICQContact;
Tmp: String;
begin
if not Assigned(Data) then
Exit;
Contact := GetICQContact(Data.GetValue('sn').Value);
if not Assigned(Contact) then
Exit;
eventData := '';
if not Data.GetValueSafe('media', eventData) then
Data.GetValueSafe('status', eventData);
Contact.XStatus := eventData;
if Data.GetValueSafe('text', Tmp) then
Contact.StatusStr := HTMLEntitiesDecode(Tmp);
InitListenerVars(Contact, Now);
NotifyListeners(IE_xstatusChanged);
end;
procedure TICQSession.ProcessSmartReply(const Data: TJSONObject);
var
Contact: TICQContact;
SmartReplies: TJSONArray;
begin
if not Assigned(Data) or not (Data.GetValue('type').Value = 'text-smartreply') then // sticker-smartreply
Exit;
Contact := GetICQContact(Data.GetValue('sn').Value);
if not Assigned(Contact) then
Exit;
InitListenerVars(Contact, Now);
SmartReplies := TJSONArray(Data.GetValue('text'));
if SmartReplies.Count = 0 then
Exit;
for var SmartReply in SmartReplies do
if Assigned(SmartReply) then
eventArray := eventArray + [SmartReply.Value];
NotifyListeners(IE_SmartReply);
end;
procedure TICQSession.ProcessReactions(const Data: TJSONObject);
var
MsgID: TMsgID;
My, ChatId: String;
begin
if not Assigned(Data) then
Exit;
My := '';
Data.GetValueSafe('myReaction', My);
Data.GetValueSafe('msgId', MsgID);
Data.GetValueSafe('chatId', ChatId);
ParseReactions(MsgID, ChatId, 0, TJSONArray(Data.GetValue('reactions')), My);
end;
procedure TICQSession.ParseReactions(MsgID: TMsgID; const ChatID: String; NotifyMsgID: TMsgID; Data: TJSONArray; MyReaction: String);
var
I, Cnt: Integer;
HasReactions: Boolean;
Ev: Thevent;
Reactions: TReactions;
begin
if MyReaction = '-' then
if MsgsReactions.TryGetValue(MsgID, Reactions) then
MyReaction := Reactions.My
else
MyReaction := '';
if NotifyMsgID = 0 then
if MsgsReactions.TryGetValue(MsgID, Reactions) then
NotifyMsgID := Reactions.NotifyMsgID;
HasReactions := not (MyReaction = '');
for I := 0 to Data.Count - 1 do
begin
Cnt := 0;
if Data[I].GetValueSafe('counter', Cnt) then
if Cnt > 0 then
HasReactions := True;
end;
if HasReactions then
MsgsReactions.AddOrSetValue(MsgID, TReactions.Create(MsgID, ChatID, NotifyMsgID, Data.Clone as TJSONArray, MyReaction))
else
MsgsReactions.Remove(MsgID);
ActionManager.Execute(AK_SAVEREACTIONS, SaveDelay);
Ev := SQLDB.GetByMsgID(ChatID, MsgID, False);
if Assigned(Ev) then
begin
UI.Chat.UpdateEvent(ChatID, Ev);
Ev.Free;
end;
end;
// Not working, for VoIP?
procedure TICQSession.InitWebRTC;
var
BaseURL, UnixTime: String;
Params: TDictionary;
begin
UnixTime := IntToStr(DateTimeToUnix(Now, False) - fHostOffset);
Params := TDictionary.Create;
Params.Add('a', fAuthToken);
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('r', CreateNewGUID);
Params.Add('ts', UnixTime);
BaseURL := WIM_HOST + 'webrtc/alloc';
SendRequest(True, BaseURL, MakeParams(Params), 'Init WebRTC');
Params.Free;
end;
function TICQSession.GetHistoryChunk(const UID: TUID; PatchVer: String; const From: String; Count: Integer; const Till: String = ''): TJSONObject;
var
Params: TDictionary;
begin
Result := nil;
if fAimSid = '' then
Exit;
if PatchVer = '' then
PatchVer := '1';
Params := TDictionary.Create;
Params.Add('sn', UID);
Params.Add('fromMsgId', From);
if not (Till = '') then
Params.Add('tillMsgId', Till);
Params.Add('count', IntToStr(Count));
Params.Add('patchVersion', PatchVer);
SendRAPIRequest(False, 'getHistory', @Params, RT_JSON, Result, 'Get a chunk of server history [' + From + ':' + IntToStr(Count) + ']');
Params.Free;
end;
procedure TICQSession.GetServerHistory(const UID: TUID; FromMsgId: TMsgID; const PatchVer: String);
function SameTextMsgExists(Event: Thevent; const Text: String; Kind: Integer): Boolean;
begin
Result := not (Event = nil) and (Event.GetBodyText = text) and (Event.Kind = kind);
end;
function SameBinMsgExists(Event: Thevent; Bin: TBytes; Kind: Integer): Boolean;
begin
Result := not (Event = nil) and (Event.GetBodyBin = bin) and (Event.Kind = kind);
end;
var
JSON: TJSONObject;
evtmp, evtmp2: Thevent;
procedure FreeBeforeContinue;
begin
if Assigned(evtmp) then
FreeAndNil(evtmp);
if Assigned(evtmp2) then
FreeAndNil(evtmp2);
end;
begin
if not logpref.writehistory then
Exit;
evtmp := nil;
evtmp2 := nil;
// cht := GetContact(UID);
if FromMsgId = 0 then
if SQLDB.GetEventCount(UID) > 0 then
Exit;
JSON := GetHistoryChunk(UID, PatchVer, UIntToStr(FromMsgId), MAXINT - 1);
if Assigned(JSON) then
try
ProcessDialogState(JSON);
finally
FreeAndNil(JSON);
end;
// hist := Thistory.Create(AnsiLowerCase(UID));
//
// for Msg in Messages do
// if Assigned(Msg) and (Msg is TJSONObject) then
// begin
// Msg.GetValueSafe('time', UnixTime);
// Time := UnixToDateTime(UnixTime, False);
//
// evtmp := hist.getByTime(Time);
//
// Tmp := TJSONObject(Msg).GetValue('outgoing');
// Outgoing := Assigned(Tmp) and (Tmp.Value = 'true');
//
// if Outgoing then
// cnt := GetMyInfo
// else
// cnt := cht;
//
// WID := '';
// Tmp := TJSONObject(Msg).GetValue('wid');
// if Assigned(Tmp) then
// begin
// Tmp.TryGetValue(WID);
// if not (WID = '') then
// begin
// evtmp2 := hist.getByWID(WID);
// if Assigned(evtmp2) then
// begin
// ODS('Msg is already in history (WID ' + wid + ')');
// FreeBeforeContinue;
// Continue;
// end;
// end;
// end;
//
// Text := TJSONObject(Msg).GetValue('text');
// StickerObj := TJSONObject(msg).GetValue('sticker') as TJSONObject;
// if Assigned(StickerObj) then
// begin
// Text := StickerObj.GetValue('id');
// ExtSticker := SplitString(Text.Value, ':');
// if EnableStickers and (Length(ExtSticker) >= 4) then
// begin
// Kind := EK_msg;
// StickerBin := GetSticker(ExtSticker[1], ExtSticker[3]);
// evtmp2 := hist.getByTime(Time);
// if SameBinMsgExists(evtmp2, StickerBin, Kind) then
// begin
// ODS('EK_msg with the same sticker is already in history (WID ' + wid + ')');
// FreeBeforeContinue;
// Continue;
// end;
// if Assigned(evtmp2) then
// FreeAndNil(evtmp2);
// ev := Thevent.new(Kind, cht, cnt, Time, '', [], 0, 0, WID);
// ev.outgoing := Outgoing;
// ev.setImgBin(StickerBin);
// history.WriteToHistory(ev);
// FreeBeforeContinue;
// Continue;
// end;
// end;
//
// Tmp := TJSONObject(Msg).GetValue('eventTypeId');
// if Assigned(Tmp) then
// begin
// if Tmp.Value = '27:51000' then
// begin
// Kind := EK_msg;
// if SameTextMsgExists(evtmp, Text.Value, Kind) then
// begin
// ODS('EK_msg with the same time is already in history');
// FreeBeforeContinue;
// Continue;
// end;
// ev := Thevent.new(Kind, cht, cnt, Time, '[' + GetTranslation('Message deleted') + ']', [], IF_not_delivered, 0, WID);
// ev.outgoing := Outgoing;
// history.WriteToHistory(ev);
// FreeBeforeContinue;
// Continue;
// end else if Tmp.Value = '27:33000' then
// begin
// Kind := EK_AddedYou;
// if SameTextMsgExists(evtmp, Text.Value, Kind) then
// begin
// ODS('EK_AddedYou with the same time is already in history');
// FreeBeforeContinue;
// Continue;
// end;
// ev := Thevent.new(Kind, cht, cht, Time, '', [], 0);
// ev.outgoing := False;
// history.WriteToHistory(ev);
// FreeBeforeContinue;
// Continue;
// end else if Tmp.Value = '27:33000' then
// begin
// // Bday event is never saved on disk, ignore
// Kind := EK_BirthDay;
// FreeBeforeContinue;
// Continue;
// end;
// end;
//
// if Assigned(Text) then
// try
// Kind := EK_msg;
// evtmp2 := hist.getByTime(Time);
// if SameTextMsgExists(evtmp2, Text.Value, Kind) then
// begin
// ODS('EK_msg with the same time/text is already in history (WID ' + wid + ')');
// FreeBeforeContinue;
// Continue;
// end;
// if Assigned(evtmp2) then
// FreeAndNil(evtmp2);
// ev := Thevent.new(Kind, cht, cnt, Time, Text.Value, [], 0, 0, WID);
// ev.outgoing := Outgoing;
// history.WriteToHistory(ev);
// except
// ODS('Not a json');
// end else
// ODS('Empty msg');
// if Assigned(evtmp) then
// FreeAndNil(evtmp);
// end;
// hist.Free;
end;
procedure TICQSession.GetServerPatches(const UID: TUID; const PatchVer: String);
var
PatchType, PatchVersion: String;
Params: TDictionary;
Results: TJSONObject;
Patches, Msgs: TJSONArray;
PatchMsgId: TMsgID;
begin
if fAimSid = '' then
Exit;
PatchVersion := PatchVer;
if PatchVersion = '' then
PatchVersion := '1';
Params := TDictionary.Create();
Params.Add('sn', UID);
Params.Add('fromMsgId', '0');
Params.Add('count', '0');
Params.Add('patchVersion', PatchVersion);
SendRAPIRequest(False, 'getHistory', @Params, RT_JSON, Results, 'Get server history patches [' + PatchVersion +']');
Params.Free;
if Assigned(Results) then
try
if Results.Count = 0 then
begin
ODS('No results');
Exit;
end;
Results.GetValueSafe('patchVersion', PatchVersion);
Patches := TJSONArray(Results.GetValue('patch'));
if Patches = nil then
begin
ODS('No patches');
Exit;
end;
for var Patch in Patches do
if Assigned(Patch) and (Patch is TJSONObject) then
begin
PatchMsgId := StrToInt64(TJSONString(TJSONObject(Patch).GetValue('msgId')).Value);
PatchType := TJSONString(TJSONObject(Patch).GetValue('type')).Value;
if not MatchText(PatchType, SupportedPatches) then
Continue;
SQLDB.WritePatch(UID, PatchMsgId, PatchType);
if PatchType = 'delete' then
begin
eventContact := Account.AccProto.GetContact(UID);
eventMsgID := PatchMsgId;
NotifyListeners(IE_MsgPatchDelete);
Continue;
end;
Results := GetHistoryChunk(UID, PatchVersion, IntToStr(PatchMsgId - 1), 1);
if not Assigned(Results) then
Continue
else if Results.Count = 0 then
begin
FreeAndNil(Results);
Continue;
end;
Msgs := Results.GetValue('messages') as TJSONArray;
if Assigned(Msgs) then
for var Msg in Msgs do
if Msg is TJSONObject then
ProcessMsg(UID, TJSONObject(Msg), True);
FreeAndNil(Results);
end;
finally
FreeAndNil(Results);
end;
end;
procedure TICQSession.GetReactions(const UID: TUID; MsgID: TMsgID);
var
Params, Results: TJSONObject;
MsgIDs, Reactions: TJSONArray;
My: String;
begin
Params := TJSONObject.Create;
Params.AddPair('chatId', UID);
MsgIDs := TJSONArray.Create;
MsgIDs.AddElement(TJSONNumber.Create(IntToStr(MsgID)));
Params.AddPair('msgIds', MsgIDs);
if SendRAPIRequest(True, 'reaction/get', @Params, RT_JSON, Results, 'Get message reactions', 'Failed to retrieve message reactions') then
if Assigned(Results) then
try
Reactions := Results.GetValue('reactions') as TJSONArray;
for var MsgReactions in Reactions do
begin
My := '';
MsgReactions.GetValueSafe('myReaction', My);
ParseReactions(MsgID, UID, 0, TJSONArray(TJSONObject(MsgReactions).GetValue('reactions')), My);
end;
finally
FreeAndNil(Results);
end;
Params.Free;
end;
procedure TICQSession.AddReaction(const UID: TUID; MsgID: TMsgID; Reaction: Integer);
var
Params, Results: TJSONObject;
Reactions, MsgReactions: TJSONArray;
My: String;
I: Integer;
begin
Params := TJSONObject.Create;
Params.AddPair('chatId', UID);
Params.AddPair('msgId', TJSONNumber.Create(IntToStr(MsgID)));
Params.AddPair('reaction', ReactionOptions[Reaction]);
Reactions := TJSONArray.Create;
for I := Low(ReactionOptions) to High(ReactionOptions) do
Reactions.Add(ReactionOptions[I]);
Params.AddPair('reactions', Reactions);
if SendRAPIRequest(True, 'reaction/add', @Params, RT_JSON, Results, 'Add message reaction', 'Failed to set reaction to message') then
if Assigned(Results) then
try
MsgReactions := Results.GetValue('reactions') as TJSONArray;
My := '';
Results.GetValueSafe('myReaction', My);
ParseReactions(MsgID, UID, 0, MsgReactions, My);
finally
FreeAndNil(Results);
end;
Params.Free;
end;
procedure TICQSession.RemoveReaction(const UID: TUID; MsgID: TMsgID);
var
Params: TJSONObject;
begin
Params := TJSONObject.Create;
Params.AddPair('chatId', UID);
Params.AddPair('msgId', TJSONNumber.Create(IntToStr(MsgID)));
if SendRAPIRequest(True, 'reaction/remove', @Params, 'Remove message reaction', 'Failed to remove reaction to message') then
GetReactions(UID, MsgID);
Params.Free;
end;
procedure TICQSession.ListReactions(const UID: TUID; MsgID: TMsgID);
var
Params, Results: TJSONObject;
begin
Params := TJSONObject.Create;
Params.AddPair('chatId', UID);
Params.AddPair('msgId', TJSONNumber.Create(IntToStr(MsgID)));
Params.AddPair('reaction', '🤣');
Params.AddPair('olderThan', '0');
Params.AddPair('limit', '13');
if SendRAPIRequest(True, 'reaction/list', @Params, RT_JSON, Results, 'List message reaction', 'Failed to retrieve reaction list') then
if Assigned(Results) then
try
// Reactions := Results.GetValue('reactions') as TJSONArray;
// for var MsgReactions in Reactions do
// ParseReactions(MsgID, UID, 0, TJSONArray(TJSONObject(MsgReactions).GetValue('reactions')));
finally
FreeAndNil(Results);
end;
Params.Free;
end;
procedure TICQSession.MarkRead(const UID: TUID; MsgID: TMsgID);
var
Params: TDictionary;
begin
if IsInvisible then
Exit;
Params := TDictionary.Create();
Params.Add('sn', UID);
Params.Add('lastRead', UIntToStr(MsgID));
// lastDelivered
// "stranger": true
// "suspicious": true
SendRAPIRequest(False, 'setDlgState', @Params, 'Mark message as last read');
Params.Free;
end;
procedure TICQSession.DeleteMessages(const UID: TUID; var IDs: TArray; ForAll: Boolean);
var
Params, Results: TJSONObject;
MsgIDs: TJSONArray;
Index: Integer;
begin
if fAimSid = '' then
Exit;
Params := TJSONObject.Create;
Params.AddPair('sn', UID);
Params.AddPair('shared', TJSONBool.Create(ForAll));
MsgIDs := TJSONArray.Create;
for var ID in IDs do
MsgIDs.AddElement(TJSONNumber.Create(ID));
Params.AddPair('msgIds', MsgIDs);
if SendRAPIRequest(True, 'delMsgBatch', @Params, RT_JSON, Results, 'Delete ' + IntToStr(Length(IDs)) + ' messages from server') then
if Assigned(Results) then
try
MsgIDs := TJSONArray(Results.GetValue('failedMsgIds'));
if Assigned(MsgIDs) then
begin
for var MsgID in MsgIDs do
if MsgID is TJSONNumber then
if TArray.BinarySearch(IDs, TJSONNumber(MsgID).Value, Index, TStringComparer.Ordinal) then
Delete(IDs, Index, 1);
MsgDlg(GetTranslation('Operation failed for these messages') + ':'#10 + MsgIDs.ToString, False, mtError);
end;
finally
FreeAndNil(Results);
end;
end;
procedure TICQSession.EventSubscribe(Contact: TICQContact = nil);
var
Params: TJSONObject;
Subscriptions: TJSONArray;
DataType, Contacts: TJSONObject;
Subs: TJSONArray;
SubCount: Integer;
begin
SubCount := 0;
Subs := TJSONArray.Create;
if not Assigned(Contact) then
begin
LastEventSubscribe := Now;
if fRoster.Count > 0 then
for var Cnt in fRoster do
if not (Cnt.Status in [SC_OFFLINE, SC_UNK]) then
if SecondsBetween(Now, Cnt.LastStatusSubscribe) >= 60 then
begin
Subs.AddElement(TJSONString.Create(Cnt.UID));
Cnt.LastStatusSubscribe := Now;
Inc(SubCount);
end;
end else if SecondsBetween(Now, Contact.LastStatusSubscribe) >= 60 then
begin
Subs.AddElement(TJSONString.Create(Contact.UID));
Contact.LastStatusSubscribe := Now;
Inc(SubCount);
end;
if SubCount = 0 then
begin
Subs.Free;
Exit;
end;
Contacts := TJSONObject.Create;
Contacts.AddPair('contacts', Subs);
DataType := TJSONObject.Create;
DataType.AddPair('data', Contacts);
DataType.AddPair('type', 'status');
Subscriptions := TJSONArray.Create;
Subscriptions.AddElement(DataType);
Params := TJSONObject.Create;
Params.AddPair('subscriptions', Subscriptions);
SendRAPIRequest(True, 'eventSubscribe', @Params, 'Subscribe to events');
Params.Free;
end;
procedure TICQSession.EventResubscribe(Contact: TICQContact = nil);
begin
if Assigned(Contact) and AutoReqXStatus then
if (Contact.LastStatusSubscribe = 0) or (SecondsBetween(Now, Contact.LastStatusSubscribe) >= 60) then
EventSubscribe(Contact);
end;
procedure TICQSession.CheckEventSubscribe(Interval: Integer = 3);
var
Minutes: Integer;
begin
if not Running or not AutoReqXStatus or not IsOnline then
Exit;
// Should be every minute actually
Minutes := MinutesBetween(Now, LastEventSubscribe);
if Minutes >= Interval then
EventSubscribe;
end;
function TICQSession.SetNick: Boolean;
var
Params: TDictionary;
MyInfo: TICQContact;
ErrHandler: TErrorProc;
begin
Result := False;
if fAimSid = '' then
Exit;
MyInfo := GetMyInfo;
Params := TDictionary.Create();
Params.Add('nick', MyInfo.TmpNick);
ErrHandler := procedure(Resp: TPair)
begin
if Resp.Key = 40600 then
MsgDlg('This nick is already taken', True, mtWarning)
else if Resp.Key = 40100 then
MsgDlg('This nick is not allowed', True, mtWarning)
else if Resp.Key = 40000 then
MsgDlg(GetTranslation('Incorrect nick. Allowed characters (min. 5 max. 30):') + ' a-zA-Z0-9._', False, mtError);
end;
Result := SendRAPIRequest(False, 'setNick', @Params, 'Save nick on server', '', ErrHandler);
Params.Free;
if Result then
MyInfo.Nick := MyInfo.TmpNick
else
begin
eventContact := MyInfo;
NotifyListeners(IE_contactupdate);
end;
end;
function TICQSession.FilesInit(const FileName: String; Size: Int64): String;
var
BaseURL: String;
Params: TDictionary;
JSON, Res: TJSONObject;
Code: Integer;
begin
Result := '';
if fAimSid = '' then
Exit;
BaseURL := FILES_HOST + 'init';
Params := TDictionary.Create();
Params.Add('f', 'json');
Params.Add('k', fDevId);
Params.Add('aimsid', fAimSid);
Params.Add('client', IfThen(ShowClientID, 'R&Q', 'Mail.ru Windows ICQ'));
Params.Add('language', IfThen(IsRuLang, 'ru-ru', 'en-us'));
Params.Add('r', CreateNewGUID);
Params.Add('filename', FileName);
Params.Add('size', IntToStr(Size));
SendRequest(False, BaseURL, MakeParams(Params), RT_JSON, JSON, 'Init file upload');
if Assigned(JSON) then
try
TJSONObject(JSON.GetValue('status')).GetValue('code').TryGetValue(Code);
if not (Code = 200) or (JSON.GetValue('result') = nil) then
begin
MsgDlg(InitFailed, True, mtError);
Exit;
end;
Res := TJSONObject(JSON.GetValue('result'));
Result := 'https://' + Res.GetValue('host').Value + Res.GetValue('url').Value;
Result := Result + '?f=json&k=' + fDevId + '&aimsid=' + fAimSid + '&r=' + CreateNewGUID;
finally
FreeAndNil(JSON);
end;
Params.Free;
end;
procedure TICQSession.SetListener(l : TProtoNotify);
begin
listener := l;
end;
procedure TICQSession.SendTyping(c: TICQContact; NotifType: Word);
var
TypingStatus: String;
Query: UTF8String;
BaseURL: String;
begin
if not Assigned(c) or not IsOnline or IsInvisible then
Exit;
TypingStatus := 'none';
if NotifType = MTN_BEGUN then
TypingStatus := 'typing'
else if NotifType = MTN_TYPED then
TypingStatus := 'typed';
BaseURL := WIM_HOST + 'im/setTyping';
Query := '&t=' + ParamEncode(String(c.UID)) +
'&typingStatus=' + TypingStatus;
SendSessionRequest(False, BaseURL, Query, 'Send typing');
end;
procedure TICQSession.SendAddContact(c: TICQcontact);
var
JSON: TJSONObject;
Query: UTF8String;
BaseURL, ResCode: String;
Results: TJSONArray;
Code: Integer;
begin
BaseURL := WIM_HOST + 'buddylist/addBuddy';
Query := '&buddy=' + ParamEncode(String(c.UID)) +
'&group=' + ParamEncode(groups.id2name(c.Group)) +
'&authorizationMsg=' + ParamEncode(GetTranslation(Str_AuthRequest)) +
'&preAuthorized=1';
try
if SendSessionRequest(False, BaseURL, Query, RT_JSON, JSON, 'Add contact', 'Failed to add contact') then
begin
Results := JSON.GetValue('results') as TJSONArray;
if Assigned(Results) and (Results.Count > 0) then
if not (Results.Items[0] = nil) then
if Results.Items[0].GetValueSafe('resultCode', ResCode) then
if TryStrToInt(ResCode, Code) then
begin
if Code = 0 then // Success! Remove local state and get profile of a newly added contact
begin
c.CntIsLocal := False;
GetProfile(c.UID);
end
else
begin
eventError := EC_AddContact_Error;
eventInt := Code;
NotifyListeners(IE_error);
// Already in CL? Request it again
if Code = 3 then
GetCL;
end;
end;
end;
finally
FreeAndNil(JSON);
end;
end;
procedure TICQSession.SendRemoveContact(c: TICQcontact);
var
Query: UTF8String;
BaseURL: String;
begin
BaseURL := WIM_HOST + 'buddylist/removeBuddy';
Query := '&buddy=' + ParamEncode(String(c.UID)) +
'&allGroups=1';
if SendSessionRequest(False, BaseURL, Query, 'Remove contact', 'Failed to remove contact') then
c.CntIsLocal := c.IsInRoster
end;
function TICQSession.UpdateGroupOf(c: TICQContact; grp: Integer): Boolean;
var
Query: UTF8String;
BaseURL: String;
begin
Result := False;
if c.CntIsLocal then
Exit;
BaseURL := WIM_HOST + 'buddylist/moveBuddy';
Query := '&buddy=' + ParamEncode(String(c.UID)) +
'&group=' + ParamEncode(groups.id2name(c.Group)) +
'&newGroup=' + ParamEncode(groups.id2name(grp));
if SendSessionRequest(False, BaseURL, Query, 'Move contact', 'Failed to move contact') then
begin
Result := True;
eventContact := c;
NotifyListeners(IE_contactupdate);
end;
end;
function TICQSession.SendUpdateGroup(const Name: String; ga: TGroupAction; const Old: String = ''): Boolean;
var
Query: UTF8String;
BaseURL: String;
begin
Result := False;
if ga = GA_Add then
begin
BaseURL := WIM_HOST + 'buddylist/addGroup';
Query := '&group=' + ParamEncode(Name);
Result := SendSessionRequest(False, BaseURL, Query, 'Add group', 'Failed to add group');
end else if (ga = GA_Rename) and not (Old = '') then
begin
BaseURL := WIM_HOST + 'buddylist/renameGroup';
Query := '&oldGroup=' + ParamEncode(Old) +
'&newGroup=' + ParamEncode(Name);
Result := SendSessionRequest(False, BaseURL, Query, 'Rename group', 'Failed to rename group');
end else if ga = GA_Remove then
begin
BaseURL := WIM_HOST + 'buddylist/removeGroup';
Query := '&group=' + ParamEncode(Name);
Result := SendSessionRequest(False, BaseURL, Query, 'Remove group', 'Failed to remove group');
end else
Exit;
end;
procedure TICQSession.Authorize(c: TICQContact; Grant: Boolean = True);
var
Query: UTF8String;
BaseURL: String;
begin
BaseURL := WIM_HOST + 'buddylist/authorizeUser';
Query := '&t=' + ParamEncode(String(c.UID)) +
'&authorized=' + IfThen(Grant, '1', '0');
SendSessionRequest(False, BaseURL, Query, IfThen(Grant, 'Grant', 'Deny') + ' auth', 'Failed to ' + IfThen(Grant, 'grant', 'deny') + ' authorization');
end;
procedure TICQSession.AuthRequest(c: TICQContact; Reason: String);
var
Query: UTF8String;
BaseURL: String;
begin
if Reason = '' then
Reason := GetTranslation(Str_AuthRequest);
BaseURL := WIM_HOST + 'buddylist/requestAuthorization';
Query := '&t=' + ParamEncode(String(c.UID)) +
'&authorizationMsg=' + ParamEncode(Reason);
SendSessionRequest(False, BaseURL, Query, 'Request auth', 'Failed to request authorization')
end;
class function TICQSession._MaxPWDLen: Integer;
begin
Result := MaxPwdLength;
end;
function TICQSession.GetContact(const UID: TUID): TICQContact;
begin
Result := GetICQContact(uid);
end;
function TICQSession.GetContact(const UIN: Integer): TICQContact;
begin
Result := GetICQContact(uin);
end;
function TICQSession.GetStatuses: TStatusArray;
begin
Result := ICQStatuses;
end;
function TICQSession.GetVisibilities: TStatusArray;
begin
Result := ICQVis;
end;
function TICQSession.GetStatusMenu: TStatusMenu;
begin
Result := StatMenu;
end;
function TICQSession.GetVisMenu: TStatusMenu;
begin
Result := ICQVisMenu;
end;
function TICQSession.GetStatusDisable: TOnStatusDisable;
begin
Result := OnStatusDisable[Byte(curStatus)];
end;
procedure TICQSession.InputChangedFor(c: TICQContact; InpIsEmpty: Boolean; TimeOut: Boolean = False);
begin
if (not SupportTypingNotif) or (not IsSendTypingNotif) or not Assigned(c) then
Exit;
with c do
if not (c.Status in [SC_OFFLINE, SC_UNK]) {and (Typing.bSupport)} then
begin
if not InpIsEmpty then
begin
if TimeOut then
begin
Typing.bIamTyping := False;
SendTyping(c, MTN_TYPED);
end
else
begin
Typing.TypingTime := Now;
if not Typing.bIamTyping then
begin
Typing.bIamTyping := True;
SendTyping(c, MTN_BEGUN);
end;
end;
end else if Typing.bIamTyping then
begin
SendTyping(c, MTN_FINISHED);
Typing.bIamTyping := False;
end
end
end;
function TICQSession.CompareStatusFor(Cnt1, Cnt2: TICQContact): SmallInt;
begin
if StatusPriority[Cnt1.Status] < StatusPriority[Cnt2.Status] then
Result := -1
else if StatusPriority[Cnt1.Status] > StatusPriority[Cnt2.Status] then
Result := +1
else
Result := 0;
end;
procedure TICQSession.GetClientPicAndDesc4(cnt: TICQContact; var pPic: TPicName; var CliDesc: String);
begin
if IsOffline or (cnt = nil){ or cnt.IsOffline} then
Exit;
GetICQClientPicAndDesc(cnt, pPic, CliDesc);
end;
procedure TICQSession.ApplyBalloon;
function SameMonthDay(d1, d2: TDateTime): Boolean;
begin
Result := (MonthOf(d1) = MonthOf(d2)) and (DayOf(d1) = DayOf(d2))
end;
begin
if GetMyInfo = nil then
raise Exception.create('ApplyBalloon: ICQ.MyInfo is nil');
Self.BirthdayFlag := (SendBalloonOn = BALLOON_BDAY) and SameMonthDay(self.GetMyInfo.birth, Now)
or (SendBalloonOn = BALLOON_DATE) and SameMonthDay(SendBalloonOnDate, Now)
or (SendBalloonOn = BALLOON_ALWAYS);
end;
class constructor TICQSession.InitICQProto;
var
b, b2: Byte;
begin
SetLength(ICQStatuses, Byte(High(TICQStatus)) + 1);
for b := Byte(Low(TICQStatus)) to Byte(High(TICQStatus)) do
with ICQStatuses[b] do
begin
idx := b;
ShortName := Status2Img[b];
Cptn := Status2ShowStr[TICQStatus(b)];
// ImageName := 'status.' + status2str[st1];
ImageName := 'status.' + ShortName;
end;
SetLength(StatMenu, 9);
b2 := 0;
StatMenu[b2] := Byte(SC_ONLINE); Inc(b2);
StatMenu[b2] := Byte(SC_F4C); Inc(b2);
StatMenu[b2] := Byte(SC_OCCUPIED); Inc(b2);
StatMenu[b2] := Byte(SC_DND); Inc(b2);
StatMenu[b2] := Byte(SC_AWAY); Inc(b2);
StatMenu[b2] := Byte(SC_NA); Inc(b2);
StatMenu[b2] := Byte(SC_EVIL); Inc(b2);
StatMenu[b2] := Byte(SC_DEPRESSION); Inc(b2);
StatMenu[b2] := Byte(SC_OFFLINE);
SetLength(ICQVis, Byte(High(TVisibility)) + 1);
for b := Byte(Low(TVisibility)) to Byte(High(TVisibility)) do
with ICQvis[B] do
begin
idx := b;
ShortName := Visib2Str[TVisibility(b)];
Cptn := Visibility2ShowStr[TVisibility(b)];
// ImageName := 'status.' + Status2Str[st1];
ImageName := Visibility2ImgName[TVisibility(b)];
end;
SetLength(ICQVisMenu, 2);
ICQVisMenu[0] := Byte(VI_normal);
ICQVisMenu[1] := Byte(VI_invisible);
end;
class destructor TICQSession.UnInitICQProto;
var
b: Byte;
begin
if Length(ICQStatuses) > 0 then
for b := Byte(Low(TICQStatus)) to Byte(High(TICQStatus)) do
with ICQStatuses[b] do
begin
SetLength(ShortName, 0);
SetLength(Cptn, 0);
SetLength(ImageName, 0);
end;
SetLength(ICQStatuses, 0);
SetLength(StatMenu, 0);
if Length(ICQVis) > 0 then
for b := Byte(Low(TVisibility)) to Byte(High(TVisibility)) do
with ICQVis[B] do
begin
SetLength(ShortName, 0);
SetLength(Cptn, 0);
SetLength(ImageName, 0);
end;
SetLength(ICQVis, 0);
SetLength(ICQVisMenu, 0);
end;
end.