/* * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-desktop * (see https://www.linphone.org). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "config.h" #include "app/App.hpp" #include "components/calls/CallsListModel.hpp" #include "components/core/CoreHandlers.hpp" #include "components/core/CoreManager.hpp" #include "components/settings/SettingsModel.hpp" #include "utils/Utils.hpp" #include "Cli.hpp" // ============================================================================= using namespace std; // ============================================================================= // API. // ============================================================================= static void cliShow (QHash &args) { App *app = App::getInstance(); if( args.size() > 0){ app->processArguments(args); app->initContentApp(); } app->smartShowWindow(app->getMainWindow()); } static void cliCall (QHash &args) { QString addressToCall = args["sip-address"]; if(args.size() > 1){// Call with options App *app = App::getInstance(); args["call"] = args["sip-address"];// Swap cli def to parser args.remove("sip-address"); app->processArguments(args); app->initContentApp(); }else CoreManager::getInstance()->getCallsListModel()->launchAudioCall(args["sip-address"], ""); } static void cliAccept (QHash &args) { auto currentCall = CoreManager::getInstance()->getCore()->getCurrentCall(); App *app = App::getInstance(); if( args.size() > 0){ app->processArguments(args); app->initContentApp(); } if(currentCall){ currentCall->accept(); } } static void cliDecline (QHash &args) { auto currentCall = CoreManager::getInstance()->getCore()->getCurrentCall(); App *app = App::getInstance(); if( args.size() > 0){ app->processArguments(args); app->initContentApp(); } if(currentCall){ currentCall->decline(linphone::Reason::Declined); } } static void cliBye (QHash &args) { auto currentCall = CoreManager::getInstance()->getCore()->getCurrentCall(); if(args.size() > 0) { if( args["sip-address"] == "*")// Call with options CoreManager::getInstance()->getCallsListModel()->terminateAllCalls(); else if( args["sip-address"] == ""){ if(currentCall) currentCall->terminate(); }else CoreManager::getInstance()->getCallsListModel()->terminateCall(args["sip-address"]); }else if(currentCall){ currentCall->terminate(); } } static void cliJoinConference (QHash &args) { const QString sipAddress = args.take("sip-address"); CoreManager *coreManager = CoreManager::getInstance(); const shared_ptr core = coreManager->getCore(); { shared_ptr address = core->createPrimaryContactParsed(); address->setDisplayName(Utils::appStringToCoreString(args.take("display-name"))); core->setPrimaryContact(address->asString()); } args["method"] = QStringLiteral("join-conference"); coreManager->getCallsListModel()->launchAudioCall(sipAddress, "", args); } static void cliJoinConferenceAs (QHash &args) { const QString fromSipAddress = args.take("guest-sip-address"); const QString toSipAddress = args.take("sip-address"); CoreManager *coreManager = CoreManager::getInstance(); /*shared_ptr proxyConfig = coreManager->getCore()->getDefaultProxyConfig(); if (!proxyConfig) { qWarning() << QStringLiteral("You have no proxy config."); return; } const shared_ptr currentSipAddress = proxyConfig->getIdentityAddress(); */ shared_ptr account = coreManager->getCore()->getDefaultAccount(); if (!account) { qWarning() << QStringLiteral("You have no account."); return; } const shared_ptr currentSipAddress = account->getParams()->getIdentityAddress(); const shared_ptr askedSipAddress = linphone::Factory::get()->createAddress( Utils::appStringToCoreString(fromSipAddress) ); if (!currentSipAddress->weakEqual(askedSipAddress)) { qWarning() << QStringLiteral("Guest sip address `%1` doesn't match with default account.") .arg(fromSipAddress); return; } args["method"] = QStringLiteral("join-conference"); coreManager->getCallsListModel()->launchAudioCall(toSipAddress, "", args); } static void cliInitiateConference (QHash &args) { shared_ptr core = CoreManager::getInstance()->getCore(); // Check identity. { shared_ptr address = core->interpretUrl(Utils::appStringToCoreString(args["sip-address"])); if (!address || address->getUsername().empty()) { qWarning() << QStringLiteral("Unable to parse invalid sip address."); return; } address->clean(); shared_ptr account = core->getDefaultAccount(); if (!account) { qWarning() << QStringLiteral("Not connected to an account"); return; } if (!account->getParams()->getIdentityAddress()->weakEqual(address)) { qWarning() << QStringLiteral("Received different sip address from identity : `%1 != %2`.") .arg(Utils::coreStringToAppString(account->getParams()->getIdentityAddress()->asString())) .arg(Utils::coreStringToAppString(address->asString())); return; } } shared_ptr conference = core->getConference(); const QString id = args["conference-id"]; auto updateCallsWindow = []() { QQuickWindow *callsWindow = App::getInstance()->getCallsWindow(); if (!callsWindow) return; // TODO: Set the view to the "waiting call view". if (CoreManager::getInstance()->getSettingsModel()->getKeepCallsWindowInBackground()) { if (!callsWindow->isVisible()) callsWindow->showMinimized(); } else App::smartShowWindow(callsWindow); }; if (conference) { if (conference->getId() == Utils::appStringToCoreString(id)) { qInfo() << QStringLiteral("Conference `%1` already exists.").arg(id); updateCallsWindow(); return; } qInfo() << QStringLiteral("Remove existing conference with id: `%1`.") .arg(Utils::coreStringToAppString(conference->getId())); core->terminateConference(); } qInfo() << QStringLiteral("Create conference with id: `%1`.").arg(id); auto confParameters = core->createConferenceParams(conference); confParameters->enableVideo(false);// Video is not yet fully supported by the application in conference conference = core->createConferenceWithParams(confParameters); conference->setId(Utils::appStringToCoreString(id)); if (core->enterConference() == -1) { qWarning() << QStringLiteral("Unable to join created conference: `%1`.").arg(id); return; } updateCallsWindow(); } // ============================================================================= // Helpers. // ============================================================================= static QString splitWord (QString word, int &curPos, const int lineLength, const QString &padding) { QString out; out += word.mid(0, lineLength - curPos) + "\n" + padding; curPos = padding.length(); word = word.mid(lineLength - curPos); while (word.length() > lineLength - curPos) { out += word.mid(0, lineLength - curPos); word = word.mid(lineLength - curPos); out += "\n" + padding; } out += word; curPos = word.length() + padding.length(); return out; } static QString indentedWord (QString word, int &curPos, const int lineLength, const QString &padding) { QString out; if (curPos + word.length() > lineLength) { if (padding.length() + word.length() > lineLength) { out += splitWord(word, curPos, lineLength, padding); } else { out += QStringLiteral("\n"); out += padding + word; curPos = padding.length(); } } else { out += word; curPos += word.length(); } return out; } static string multilineIndent (const QString &str, int indentationNumber = 0) { constexpr int lineLength(80); static const QRegExp spaceRegexp("(\\s)"); const QString padding(indentationNumber * 2, ' '); QString out = padding; int indentedTextCurPos = padding.length(); int spacePos = 0; int wordPos = spacePos; QString word; while ((spacePos = spaceRegexp.indexIn(str, spacePos)) != -1) { word = str.mid(wordPos, spacePos - wordPos); out += indentedWord(word, indentedTextCurPos, lineLength, padding); switch (str[spacePos].unicode()) { case '\n': out += "\n" + padding; indentedTextCurPos = padding.length(); break; case '\t': // TAB as space. case ' ': if (indentedTextCurPos == lineLength) { out += "\n" + padding; indentedTextCurPos = padding.length(); } else { out += " "; indentedTextCurPos += 1; } break; default: break; } spacePos += 1; wordPos = spacePos; } word = str.mid(wordPos); out += indentedWord(word, indentedTextCurPos, lineLength, padding); out += "\n"; return Utils::appStringToCoreString(out); } // ============================================================================= Cli::Command::Command ( const QString &functionName, const char *functionDescription, Cli::Function function, const QHash &argsScheme, const bool &genericArguments ) : mFunctionName(functionName), mFunctionDescription(functionDescription), mFunction(function), mArgsScheme(argsScheme), mGenericArguments(genericArguments) {} void Cli::Command::execute (QHash &args) const { if(!mGenericArguments){// Check arguments validity. for (const auto &argName : args.keys()) { if (!mArgsScheme.contains(argName)) { qWarning() << QStringLiteral("Command with invalid argument: `%1 (%2)`.") .arg(mFunctionName).arg(argName); return; } } } // Check missing arguments. for (const auto &argName : mArgsScheme.keys()) { if (!mArgsScheme[argName].isOptional && (!args.contains(argName) || args[argName].isEmpty())) { qWarning() << QStringLiteral("Missing argument for command: `%1 (%2)`.") .arg(mFunctionName).arg(argName); return; } } // Execute! App *app = App::getInstance(); if (app->isOpened()) { qInfo() << QStringLiteral("Execute command:") << args; (*mFunction)(args); } else { Function f = mFunction; QObject * context = new QObject(); QObject::connect(app, &App::opened, [f, args, context]()mutable { if(context){ delete context; context = nullptr; qInfo() << QStringLiteral("Execute deferred command:") << args; QHash fuckConst = args; (*f)(fuckConst); } } ); } } void Cli::Command::executeUri (const shared_ptr &address) const { QHash args; QString qAddress = Utils::coreStringToAppString(address->asString()); if(address->getDomain() == "" && qAddress.back() == '@') qAddress.remove(qAddress.size()-1, 1); QUrl url(qAddress); QString query = url.query(); QStringList parameters = query.split('&'); for(int i = 0 ; i < parameters.size() ; ++i){ QStringList parameter = parameters[i].split('='); if( parameter[0]!="" && parameter[0] != "method"){ if(parameter.size() > 1) args[parameter[0]] = QByteArray::fromBase64(parameter[1].toUtf8() ); else args[parameter[0]] = ""; } } // TODO: check if there is too much headers. for (const auto &argName : mArgsScheme.keys()) { const string header = address->getHeader(Utils::appStringToCoreString(argName)); args[argName] = QByteArray::fromBase64(QByteArray(header.c_str(), int(header.length()))); } args["sip-address"] = qAddress; execute(args); } // pUrl can be `anytoken?p1=x&p2=y` or `p1=x&p2=y`. It will only use p1 and p2 void Cli::Command::executeUrl (const QString &pUrl) const { QHash args; QStringList urlParts = pUrl.split('?'); QString query = (urlParts.size()>1?urlParts[1]:urlParts[0]); QString authority = (urlParts.size()>1 && urlParts[0].contains(':')?urlParts[0].split(':')[1]:""); QStringList parameters = query.split('&'); for(int i = 0 ; i < parameters.size() ; ++i){ QStringList parameter = parameters[i].split('='); if( parameter[0] != "method"){ if(parameter.size() > 1) args[parameter[0]] = QByteArray::fromBase64(parameter[1].toUtf8() ); else args[parameter[0]] = ""; } } if(!authority.isEmpty()) args["sip-address"] = authority; execute(args); } QString Cli::Command::getFunctionSyntax () const { QString functionSyntax; functionSyntax += QStringLiteral("\""); functionSyntax += mFunctionName; for (auto &argName : mArgsScheme.keys()){ functionSyntax += QStringLiteral(" "); functionSyntax += mArgsScheme[argName].isOptional ? QStringLiteral("[") : QStringLiteral(""); functionSyntax += argName; functionSyntax += QStringLiteral("=<"); switch (mArgsScheme[argName].type) { case String: functionSyntax += QStringLiteral("str"); break; default: functionSyntax += QStringLiteral("value"); break; } functionSyntax += QString(">"); functionSyntax += mArgsScheme[argName].isOptional ? QStringLiteral("]") : QStringLiteral(""); } functionSyntax += QStringLiteral("\""); return functionSyntax; } // ============================================================================= // FIXME: Do not accept args without value like: cmd toto. // In the future `toto` could be a boolean argument. QRegExp Cli::mRegExpArgs("(?:(?:([\\w-]+)\\s*)=\\s*(?:\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"|([^\\s]+)\\s*))"); QRegExp Cli::mRegExpFunctionName("^\\s*([a-z-]+)\\s*"); QMap Cli::mCommands = { createCommand("show", QT_TR_NOOP("showFunctionDescription"), cliShow, QHash(), true), createCommand("call", QT_TR_NOOP("callFunctionDescription"), cliCall, { { "sip-address", {} } }, true), createCommand("initiate-conference", QT_TR_NOOP("initiateConferenceFunctionDescription"), cliInitiateConference, { { "sip-address", {} }, { "conference-id", {} } }), createCommand("join-conference", QT_TR_NOOP("joinConferenceFunctionDescription"), cliJoinConference, { { "sip-address", {} }, { "conference-id", {} }, { "display-name", {} } }), createCommand("join-conference-as", QT_TR_NOOP("joinConferenceAsFunctionDescription"), cliJoinConferenceAs, { { "sip-address", {} }, { "conference-id", {} }, { "guest-sip-address", {} } }), createCommand("bye", QT_TR_NOOP("byeFunctionDescription"), cliBye, QHash(), true), createCommand("accept", QT_TR_NOOP("acceptFunctionDescription"), cliAccept, QHash(), true), createCommand("decline", QT_TR_NOOP("declineFunctionDescription"), cliDecline, QHash(), true), }; // ----------------------------------------------------------------------------- /* string Cli::getScheme(const QString& address){ QStringList tempSipAddress = address->split(':'); if( tempSipAddress.size() > 0) return tempSipAddress[0].toStdString(); else return ""; } bool Cli::changeScheme(QString * address){ QStringList tempSipAddress = address->split(':'); string scheme; bool ok = false; if(tempSipAddress.size() > 1) { scheme = tempSipAddress[0].toStdString(); for (const string &validScheme : { string("sip"), "sip-"+string(EXECUTABLE_NAME), string("sips"), "sips-"+string(EXECUTABLE_NAME), string("tel"), string("callto"), string(EXECUTABLE_NAME)+ "-config" }) if (scheme == validScheme) ok = true; if( !ok){ qWarning() << QStringLiteral("Not a valid uri: `%1` Unsupported scheme: `%2`.").arg(*address).arg(Utils::coreStringToAppString(scheme)); }else{ tempSipAddress[0] = "sip";// In order to pass bellesip parsing. *address = tempSipAddress.join(':'); } } return ok; } */ void Cli::executeCommand (const QString &command, CommandFormat *format) { // Detect if command is a CLI by testing commands const QString &functionName = parseFunctionName(command); if(!functionName.isEmpty()){// It is a CLI qInfo() << QStringLiteral("Detecting cli command: `%1`...").arg(command); QHash args = parseArgs(command); mCommands[functionName].execute(args); if (format) *format = CliFormat; return; }else{// It is a URI QStringList tempSipAddress = command.split(':'); string scheme="sip"; QString transformedCommand; // In order to pass bellesip parsing, set scheme to 'sip:'. if( tempSipAddress.size() == 1){ transformedCommand = "sip:"+command; }else{ scheme = tempSipAddress[0].toStdString(); bool ok = false; for (const string &validScheme : { string("sip"), "sip-"+string(EXECUTABLE_NAME), string("sips"), "sips-"+string(EXECUTABLE_NAME), string("tel"), string("callto"), string(EXECUTABLE_NAME)+ "-config" }) if (scheme == validScheme) ok = true; if( !ok){ qWarning() << QStringLiteral("Not a valid URI: `%1` Unsupported scheme: `%2`.").arg(command).arg(Utils::coreStringToAppString(scheme)); return; } tempSipAddress[0] = "sip"; transformedCommand = tempSipAddress.join(':'); } if( scheme == string(EXECUTABLE_NAME)+"-config" ){ QHash args = parseArgs(command); if(args.contains("fetch-config")) args["fetch-config"] = QByteArray::fromBase64(args["fetch-config"].toUtf8() ); else { QUrl url(command); url.setScheme("https"); args["fetch-config"] = url.toString(); } if (format) *format = CliFormat; mCommands["show"].execute(args); }else{ shared_ptr address; if(Utils::isUsername(transformedCommand)){ address = linphone::Factory::get()->createAddress(Utils::appStringToCoreString(transformedCommand+"@to.remove")); if(address) address->setDomain(""); }else address = linphone::Factory::get()->createAddress(Utils::appStringToCoreString(transformedCommand));// Test if command is an address if (format) *format = UriFormat; qInfo() << QStringLiteral("Detecting URI command: `%1`...").arg(command); QString functionName; if( address) { functionName = Utils::coreStringToAppString(address->getHeader("method")).isEmpty() ? QStringLiteral("call") : Utils::coreStringToAppString(address->getHeader("method")); }else{ QStringList fields = command.split('?'); if(fields.size() >1){ fields = fields[1].split('&'); for(int i = 0 ; i < fields.size() && functionName.isEmpty(); ++i){ QStringList data = fields[i].split('='); if( data[0] == "method" && data.size() >1) functionName = data[1]; } if(functionName.isEmpty()) functionName = "call"; } } functionName = functionName.toLower(); if( functionName.isEmpty()){ qWarning() << QStringLiteral("There is no method set in `%1`.").arg(command); return; }else if( !mCommands.contains(functionName)) { qWarning() << QStringLiteral("This command doesn't exist: `%1`.").arg(functionName); return; } if(address) mCommands[functionName].executeUri(address); else mCommands[functionName].executeUrl(command); } } } void Cli::showHelp () { cout << multilineIndent(tr("appCliDescription").arg(APPLICATION_NAME), 0) << endl << "Usage: " << endl << multilineIndent(tr("uriCommandLineSyntax").arg(EXECUTABLE_NAME), 0) << multilineIndent(tr("cliCommandLineSyntax").arg(EXECUTABLE_NAME), 0) << endl << multilineIndent(tr("commandsName")) << endl; for (const auto &method : mCommands.keys()) cout << multilineIndent(mCommands[method].getFunctionSyntax(), 1) << multilineIndent(tr(mCommands[method].getFunctionDescription()), 2) << endl; } // ----------------------------------------------------------------------------- pair Cli::createCommand ( const QString &functionName, const char *functionDescription, Function function, const QHash &argsScheme, const bool &genericArguments ) { return { functionName.toLower(), Cli::Command(functionName.toLower(), functionDescription, function, argsScheme, genericArguments) }; } // ----------------------------------------------------------------------------- QString Cli::parseFunctionName (const QString &command) { mRegExpFunctionName.indexIn(command.toLower()); if (mRegExpFunctionName.pos(1) == -1) { qWarning() << QStringLiteral("Unable to parse function name of command: `%1`.").arg(command); return QString(""); } const QStringList texts = mRegExpFunctionName.capturedTexts(); const QString functionName = texts[1]; if (!mCommands.contains(functionName)) { qWarning() << QStringLiteral("This command doesn't exist: `%1`.").arg(functionName); return QString(""); } return functionName; } QHash Cli::parseArgs (const QString &command) { QHash args; int pos = 0; while ((pos = mRegExpArgs.indexIn(command.toLower(), pos)) != -1) { pos += mRegExpArgs.matchedLength(); args[mRegExpArgs.cap(1)] = (mRegExpArgs.cap(2).isEmpty() ? mRegExpArgs.cap(3) : mRegExpArgs.cap(2)); } return args; }