public release

This commit is contained in:
infactum 2018-10-04 12:07:36 +05:00
parent f9315603b6
commit d95aa2840c
26 changed files with 6375 additions and 0 deletions

59
CMakeLists.txt Executable file
View File

@ -0,0 +1,59 @@
cmake_minimum_required(VERSION 3.9)
project(tg2sip)
set(CMAKE_CXX_STANDARD 17)
add_subdirectory(libtgvoip)
find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED)
find_package(Td 1.2.0 REQUIRED)
find_package(spdlog 0.17 REQUIRED)
pkg_check_modules(PJSIP libpjproject>=2.8 REQUIRED)
pkg_check_modules(OPUS opus REQUIRED)
add_executable(tg2sip
tg2sip/main.cpp
tg2sip/tg.cpp
tg2sip/tg.h
tg2sip/sip.cpp
tg2sip/sip.h
tg2sip/settings.cpp
tg2sip/settings.h
tg2sip/logging.cpp
tg2sip/logging.h
tg2sip/utils.cpp
tg2sip/utils.h
tg2sip/queue.h
tg2sip/gateway.cpp
tg2sip/gateway.h
)
add_custom_command(
TARGET tg2sip PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/settings.ini
$<TARGET_FILE_DIR:tg2sip>)
target_include_directories(tg2sip PRIVATE
${PJSIP_INCLUDE_DIRS}
${PROJECT_SOURCE_DIR}
${OPUS_INCLUDE_DIRS}
include)
target_link_libraries(tg2sip PRIVATE
${PJSIP_LIBRARIES}
libtgvoip
Threads::Threads
Td::TdStatic)
add_executable(gen_db
tg2sip/gen_db.cpp
tg2sip/settings.cpp
tg2sip/settings.h)
target_include_directories(gen_db PRIVATE
include)
target_link_libraries(gen_db PRIVATE
Td::TdStatic)

339
COPYING Normal file
View File

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 2 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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

69
Dockerfile Executable file
View File

@ -0,0 +1,69 @@
FROM ubuntu:bionic as builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential git \
wget ca-certificates \
pkg-config libopus-dev libssl-dev \
zlib1g-dev gperf ccache \
&& rm -rf /var/lib/apt/lists/*
RUN wget https://cmake.org/files/v3.9/cmake-3.9.6-Linux-x86_64.sh \
&& sh cmake-3.9.6-Linux-x86_64.sh --prefix=/usr --exclude-subdir
COPY tdlib_header.patch /
RUN git clone https://github.com/tdlib/td.git \
&& cd td \
&& git checkout cfe4d9bdcee9305632eb228a46a95407d05b5c7a -b build \
&& git apply /tdlib_header.patch \
&& mkdir build \
&& cd build \
&& cmake -DCMAKE_BUILD_TYPE=Release .. \
&& cmake --build . --target install
COPY config_site.h /
COPY pj_threadname.patch /
COPY pj_cpp1z.patch /
RUN git clone https://github.com/pjsip/pjproject.git \
&& cd pjproject \
&& git reset --hard 2.8 \
&& git apply /pj_threadname.patch \
&& git apply /pj_cpp1z.patch \
&& cp /config_site.h pjlib/include/pj \
&& ./configure --disable-sound CFLAGS="-O3 -DNDEBUG" \
&& make dep \
&& make -j $(shell nproc) \
&& make install
RUN git clone -n https://github.com/gabime/spdlog.git \
&& cd spdlog \
&& git checkout tags/v0.17.0 \
&& mkdir build \
&& cd build \
&& cmake -DCMAKE_BUILD_TYPE=Release -DSPDLOG_BUILD_EXAMPLES=OFF -DSPDLOG_BUILD_TESTING=OFF .. \
&& cmake --build . --target install
COPY . /src
RUN cd src \
&& mkdir build \
&& cd build \
&& cmake -DCMAKE_BUILD_TYPE=Release .. \
&& cmake --build .
FROM ubuntu:bionic
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libopus0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /gateway
COPY --from=builder /src/build/tg2sip .
COPY --from=builder /src/build/gen_db .
COPY --from=builder /src/build/settings.ini .
CMD ./tg2sip

View File

@ -0,0 +1,27 @@
# TG2SIP
TG2SIP is a Telegram<->SIP voice gateway. It can be used to forward incoming telegram calls to your SIP PBX or make SIP->Telegram calls.
## Requirements
Your SIP PBX should be comaptible with `L16@48000` or `OPUS@48000` voice codec.
## Usage
1. Download prebuild version from CI or compile it yourself.
2. Obtain `api_id` and `api_hash` tokens from [this](https://my.telegram.org) page and put them in `settings.ini` file.
3. Login into telegram with `gen_db` app
4. Set SIP server settings in `settings.ini`
5. Run `tg2sip`
## Donate
[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=755FZWPRC9YGL&lc=US&item_name=TG2SIP&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted)
[Yandex.Money](https://yasobe.ru/na/tg2sip)
**BTC** 39wNzvtcyRrTKmq5DjcUfGTixnGVSf8qLg
**BCH** qqgwg0g96sayht4lzxc89ky7mkdxfyj7jcl5m8qfps
**ETH** 0x72B8cb476b2c85b1170Ae2cdFB243B17680290b4
**ETC** 0x9C7d6CD9F9E0584e65f8aD20e1d2Ced947a55207
**LTC** MFyBRJTnHqXharzH7D3FYeEhAJuywMRfMd

47
buildenv/Dockerfile Normal file
View File

@ -0,0 +1,47 @@
FROM ubuntu:bionic
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential git \
wget ca-certificates \
pkg-config libopus-dev libssl-dev \
zlib1g-dev gperf ccache \
&& rm -rf /var/lib/apt/lists/*
RUN wget https://cmake.org/files/v3.9/cmake-3.9.6-Linux-x86_64.sh \
&& sh cmake-3.9.6-Linux-x86_64.sh --prefix=/usr --exclude-subdir
COPY tdlib_header.patch /
COPY tdlib_threadname.patch /
RUN git clone https://github.com/tdlib/td.git \
&& cd td \
&& git reset --hard v1.2.0 \
&& git apply /tdlib_header.patch \
&& git apply /tdlib_threadname.patch \
&& mkdir build \
&& cd build \
&& cmake -DCMAKE_BUILD_TYPE=Release .. \
&& cmake --build . --target install \
&& cd / \
&& rm -rf td
COPY config_site.h /
RUN git clone https://github.com/Infactum/pjproject.git \
&& cd pjproject \
&& cp /config_site.h pjlib/include/pj \
&& ./configure --disable-sound CFLAGS="-O3 -DNDEBUG" \
&& make dep && make && make install \
&& cd / \
&& rm -rf pjproject
RUN git clone -n https://github.com/gabime/spdlog.git \
&& cd spdlog \
&& git checkout tags/v0.17.0 \
&& mkdir build \
&& cd build \
&& cmake -DCMAKE_BUILD_TYPE=Release -DSPDLOG_BUILD_EXAMPLES=OFF -DSPDLOG_BUILD_TESTING=OFF .. \
&& cmake --build . --target install \
&& cd / \
&& rm -rf spdlog

8
buildenv/config_site.h Executable file
View File

@ -0,0 +1,8 @@
#define PJMEDIA_CONF_USE_SWITCH_BOARD 1
#define PJMEDIA_CONF_SWITCH_BOARD_BUF_SIZE 2000
#define PJSUA_MAX_CALLS 128
#define PJSUA_MAX_PLAYERS 256
#define PJ_IOQUEUE_MAX_HANDLES 256
#define PJMEDIA_CODEC_L16_HAS_48KHZ_MONO 1

View File

@ -0,0 +1,19 @@
diff --git a/td/telegram/Td.cpp b/td/telegram/Td.cpp
index a150e6c..cd66d91 100644
--- a/td/telegram/Td.cpp
+++ b/td/telegram/Td.cpp
@@ -4827,10 +4827,10 @@ Status Td::set_parameters(td_api::object_ptr<td_api::tdlibParameters> parameters
if (options.application_version.empty()) {
return Status::Error(400, "Application version must be non-empty");
}
- if (options.api_id != 21724) {
- options.application_version += ", TDLib ";
- options.application_version += TDLIB_VERSION;
- }
+ // if (options.api_id != 21724) {
+ // options.application_version += ", TDLib ";
+ // options.application_version += TDLIB_VERSION;
+ // }
G()->set_mtproto_header(std::make_unique<MtprotoHeader>(options));
state_ = State::Decrypt;

View File

@ -0,0 +1,12 @@
diff --git a/tdutils/td/utils/port/detail/ThreadStl.h b/tdutils/td/utils/port/detail/ThreadStl.h
index 64bf321..68d3bf6 100644
--- a/tdutils/td/utils/port/detail/ThreadStl.h
+++ b/tdutils/td/utils/port/detail/ThreadStl.h
@@ -37,6 +37,7 @@ class ThreadStl {
invoke_tuple(std::move(args));
clear_thread_locals();
});
+ pthread_setname_np(thread_.native_handle(), "tdlib");
}
void join() {

441
include/INIReader.h Executable file
View File

@ -0,0 +1,441 @@
// Read an INI file into easy-to-access name/value pairs.
// inih and INIReader are released under the New BSD license (see LICENSE.txt).
// Go to the project home page for more info:
//
// https://github.com/benhoyt/inih
/* inih -- simple .INI file parser
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#ifndef __INI_H__
#define __INI_H__
/* Make this header file easier to include in C++ code */
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
/* Typedef for prototype of handler function. */
typedef int (*ini_handler)(void* user, const char* section,
const char* name, const char* value);
/* Typedef for prototype of fgets-style reader function. */
typedef char* (*ini_reader)(char* str, int num, void* stream);
/* Parse given INI-style file. May have [section]s, name=value pairs
(whitespace stripped), and comments starting with ';' (semicolon). Section
is "" if name=value pair parsed before any section heading. name:value
pairs are also supported as a concession to Python's configparser.
For each name=value pair parsed, call handler function with given user
pointer as well as section, name, and value (data only valid for duration
of handler call). Handler should return nonzero on success, zero on error.
Returns 0 on success, line number of first error on parse error (doesn't
stop on first error), -1 on file open error, or -2 on memory allocation
error (only when INI_USE_STACK is zero).
*/
int ini_parse(const char* filename, ini_handler handler, void* user);
/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
close the file when it's finished -- the caller must do that. */
int ini_parse_file(FILE* file, ini_handler handler, void* user);
/* Same as ini_parse(), but takes an ini_reader function pointer instead of
filename. Used for implementing custom or string-based I/O. */
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user);
/* Nonzero to allow multi-line value parsing, in the style of Python's
configparser. If allowed, ini_parse() will call the handler with the same
name for each subsequent line parsed. */
#ifndef INI_ALLOW_MULTILINE
#define INI_ALLOW_MULTILINE 1
#endif
/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
the file. See http://code.google.com/p/inih/issues/detail?id=21 */
#ifndef INI_ALLOW_BOM
#define INI_ALLOW_BOM 1
#endif
/* Nonzero to allow inline comments (with valid inline comment characters
specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
Python 3.2+ configparser behaviour. */
#ifndef INI_ALLOW_INLINE_COMMENTS
#define INI_ALLOW_INLINE_COMMENTS 1
#endif
#ifndef INI_INLINE_COMMENT_PREFIXES
#define INI_INLINE_COMMENT_PREFIXES ";"
#endif
/* Nonzero to use stack, zero to use heap (malloc/free). */
#ifndef INI_USE_STACK
#define INI_USE_STACK 1
#endif
/* Stop parsing on first error (default is to keep parsing). */
#ifndef INI_STOP_ON_FIRST_ERROR
#define INI_STOP_ON_FIRST_ERROR 0
#endif
/* Maximum line length for any line in INI file. */
#ifndef INI_MAX_LINE
#define INI_MAX_LINE 200
#endif
#ifdef __cplusplus
}
#endif
/* inih -- simple .INI file parser
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#if !INI_USE_STACK
#include <stdlib.h>
#endif
#define MAX_SECTION 50
#define MAX_NAME 50
/* Strip whitespace chars off end of given string, in place. Return s. */
inline static char* rstrip(char* s)
{
char* p = s + strlen(s);
while (p > s && isspace((unsigned char)(*--p)))
*p = '\0';
return s;
}
/* Return pointer to first non-whitespace char in given string. */
inline static char* lskip(const char* s)
{
while (*s && isspace((unsigned char)(*s)))
s++;
return (char*)s;
}
/* Return pointer to first char (of chars) or inline comment in given string,
or pointer to null at end of string if neither found. Inline comment must
be prefixed by a whitespace character to register as a comment. */
inline static char* find_chars_or_comment(const char* s, const char* chars)
{
#if INI_ALLOW_INLINE_COMMENTS
int was_space = 0;
while (*s && (!chars || !strchr(chars, *s)) &&
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
was_space = isspace((unsigned char)(*s));
s++;
}
#else
while (*s && (!chars || !strchr(chars, *s))) {
s++;
}
#endif
return (char*)s;
}
/* Version of strncpy that ensures dest (size bytes) is null-terminated. */
inline static char* strncpy0(char* dest, const char* src, size_t size)
{
strncpy(dest, src, size);
dest[size - 1] = '\0';
return dest;
}
/* See documentation in header file. */
inline int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user)
{
/* Uses a fair bit of stack (use heap instead if you need to) */
#if INI_USE_STACK
char line[INI_MAX_LINE];
#else
char* line;
#endif
char section[MAX_SECTION] = "";
char prev_name[MAX_NAME] = "";
char* start;
char* end;
char* name;
char* value;
int lineno = 0;
int error = 0;
#if !INI_USE_STACK
line = (char*)malloc(INI_MAX_LINE);
if (!line) {
return -2;
}
#endif
/* Scan through stream line by line */
while (reader(line, INI_MAX_LINE, stream) != NULL) {
lineno++;
start = line;
#if INI_ALLOW_BOM
if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
(unsigned char)start[1] == 0xBB &&
(unsigned char)start[2] == 0xBF) {
start += 3;
}
#endif
start = lskip(rstrip(start));
if (*start == ';' || *start == '#') {
/* Per Python configparser, allow both ; and # comments at the
start of a line */
}
#if INI_ALLOW_MULTILINE
else if (*prev_name && *start && start > line) {
#if INI_ALLOW_INLINE_COMMENTS
end = find_chars_or_comment(start, NULL);
if (*end)
*end = '\0';
rstrip(start);
#endif
/* Non-blank line with leading whitespace, treat as continuation
of previous name's value (as per Python configparser). */
if (!handler(user, section, prev_name, start) && !error)
error = lineno;
}
#endif
else if (*start == '[') {
/* A "[section]" line */
end = find_chars_or_comment(start + 1, "]");
if (*end == ']') {
*end = '\0';
strncpy0(section, start + 1, sizeof(section));
*prev_name = '\0';
}
else if (!error) {
/* No ']' found on section line */
error = lineno;
}
}
else if (*start) {
/* Not a comment, must be a name[=:]value pair */
end = find_chars_or_comment(start, "=:");
if (*end == '=' || *end == ':') {
*end = '\0';
name = rstrip(start);
value = lskip(end + 1);
#if INI_ALLOW_INLINE_COMMENTS
end = find_chars_or_comment(value, NULL);
if (*end)
*end = '\0';
#endif
rstrip(value);
/* Valid name[=:]value pair found, call handler */
strncpy0(prev_name, name, sizeof(prev_name));
if (!handler(user, section, name, value) && !error)
error = lineno;
}
else if (!error) {
/* No '=' or ':' found on name[=:]value line */
error = lineno;
}
}
#if INI_STOP_ON_FIRST_ERROR
if (error)
break;
#endif
}
#if !INI_USE_STACK
free(line);
#endif
return error;
}
/* See documentation in header file. */
inline int ini_parse_file(FILE* file, ini_handler handler, void* user)
{
return ini_parse_stream((ini_reader)fgets, file, handler, user);
}
/* See documentation in header file. */
inline int ini_parse(const char* filename, ini_handler handler, void* user)
{
FILE* file;
int error;
file = fopen(filename, "r");
if (!file)
return -1;
error = ini_parse_file(file, handler, user);
fclose(file);
return error;
}
#endif /* __INI_H__ */
#ifndef __INIREADER_H__
#define __INIREADER_H__
#include <map>
#include <set>
#include <string>
// Read an INI file into easy-to-access name/value pairs. (Note that I've gone
// for simplicity here rather than speed, but it should be pretty decent.)
class INIReader
{
public:
// Empty Constructor
INIReader() {};
// Construct INIReader and parse given filename. See ini.h for more info
// about the parsing.
INIReader(std::string filename);
// Return the result of ini_parse(), i.e., 0 on success, line number of
// first error on parse error, or -1 on file open error.
int ParseError() const;
// Return the list of sections found in ini file
std::set<std::string> Sections();
// Get a string value from INI file, returning default_value if not found.
std::string Get(std::string section, std::string name,
std::string default_value);
// Get an integer (long) value from INI file, returning default_value if
// not found or not a valid integer (decimal "1234", "-1234", or hex "0x4d2").
long GetInteger(std::string section, std::string name, long default_value);
// Get a real (floating point double) value from INI file, returning
// default_value if not found or not a valid floating point value
// according to strtod().
double GetReal(std::string section, std::string name, double default_value);
// Get a boolean value from INI file, returning default_value if not found or if
// not a valid true/false value. Valid true values are "true", "yes", "on", "1",
// and valid false values are "false", "no", "off", "0" (not case sensitive).
bool GetBoolean(std::string section, std::string name, bool default_value);
private:
int _error;
std::map<std::string, std::string> _values;
std::set<std::string> _sections;
static std::string MakeKey(std::string section, std::string name);
static int ValueHandler(void* user, const char* section, const char* name,
const char* value);
};
#endif // __INIREADER_H__
#ifndef __INIREADER__
#define __INIREADER__
#include <algorithm>
#include <cctype>
#include <cstdlib>
using std::string;
inline INIReader::INIReader(string filename)
{
_error = ini_parse(filename.c_str(), ValueHandler, this);
}
inline int INIReader::ParseError() const
{
return _error;
}
inline std::set<string> INIReader::Sections()
{
return _sections;
}
inline string INIReader::Get(string section, string name, string default_value)
{
string key = MakeKey(section, name);
return _values.count(key) ? _values[key] : default_value;
}
inline long INIReader::GetInteger(string section, string name, long default_value)
{
string valstr = Get(section, name, "");
const char* value = valstr.c_str();
char* end;
// This parses "1234" (decimal) and also "0x4D2" (hex)
long n = strtol(value, &end, 0);
return end > value ? n : default_value;
}
inline double INIReader::GetReal(string section, string name, double default_value)
{
string valstr = Get(section, name, "");
const char* value = valstr.c_str();
char* end;
double n = strtod(value, &end);
return end > value ? n : default_value;
}
inline bool INIReader::GetBoolean(string section, string name, bool default_value)
{
string valstr = Get(section, name, "");
// Convert to lower case to make string comparisons case-insensitive
std::transform(valstr.begin(), valstr.end(), valstr.begin(), ::tolower);
if (valstr == "true" || valstr == "yes" || valstr == "on" || valstr == "1")
return true;
else if (valstr == "false" || valstr == "no" || valstr == "off" || valstr == "0")
return false;
else
return default_value;
}
inline string INIReader::MakeKey(string section, string name)
{
string key = section + "=" + name;
// Convert to lower case to make section/name lookups case-insensitive
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
return key;
}
inline int INIReader::ValueHandler(void* user, const char* section, const char* name,
const char* value)
{
INIReader* reader = (INIReader*)user;
string key = MakeKey(section, name);
if (reader->_values[key].size() > 0)
reader->_values[key] += "\n";
reader->_values[key] += value;
reader->_sections.insert(section);
return 1;
}
#endif // __INIREADER__

2535
include/boost/sml.hpp Normal file

File diff suppressed because it is too large Load Diff

82
settings.ini Executable file
View File

@ -0,0 +1,82 @@
; sample settings file
; commented settings are optional
[logging]
;core=2 ; 0-trace 2-info 4-err 6-off
; 1-debug 3-warn 5-crit
;tgvoip=5 ; same as core
;pjsip=2 ; same as core
;sip_messages=true ; log sip messages if pjsip debug is enabled
;console_min_level=0 ; minimal log level that will be written into console
;file_min_level=0 ; same but into file
;tdlib=3 ; TDLib is written to file only and has its own log level values
; not affected by other log settings
; 0-fatal 2-warnings 4-debug
; 1-errors 3-info 5-verbose debug
[sip]
;public_address= ; Address to advertise as the address UDP transport.
;stun_server=
;port=5060
;port_range=0 ; Specify the port range for socket binding, relative to the start
; port number specified in port.
;id_uri=sip:localhost ; The Address of Record or AOR, that is full SIP URL that identifies the account.
; The value can take name address or URL format, and will look something
; like "sip:account@serviceprovider".
;callback_uri= ; SIP URI for TG->SIP incoming calls processing
;raw_pcm=true ; use L16@48k codec if true or OPUS@48k otherwise
; keep true for lower CPU consumption
;thread_count=1 ; Specify the number of worker threads to handle incoming RTP
; packets. A value of one is recommended for most applications.
[telegram]
api_id= ; Application identifier for Telegram API access
api_hash= ; Application identifier hash for Telegram API access
; which can be obtained at https://my.telegram.org.
;system_language_code=en-US ; IETF language tag of the user's operating system language
;device_model=PC ; Model of the device the application is being run on
;system_version=Linux ; Version of the operating system the application is being run on
;application_version=1.0
;database_folder= ; The path to the directory for the persistent TDLib database;
; if empty, the current working directory will be used.
;udp_p2p=false ; True, if UDP peer-to-peer connections are supported
;udp_reflector=true ; True, if connection through UDP reflectors is supported.
;enable_aec=false ; acoustic echo cancellation
;enable_ns=false ; noise suppression
;enable_agc=false ; automatic gain control
;use_proxy=false ; use SOCKS5 proxy for MTProto requests
;proxy_address=
;proxy_port=0
;proxy_username=
;proxy_password=
;use_voip_proxy=false ; use SOCKS5 proxy for VoIP
;voip_proxy_address=
;voip_proxy_port=0
;voip_proxy_username=
;voip_proxy_password=
[other]
;extra_wait_time=30 ; If gateway gets temporary blocked with "Too Many Requests" reason,
; then block all outgoing telegram requests for X more seconds than was
; requested by server
;peer_flood_time=86400 ; Seconds to wait on PEER_FLOOD

883
tg2sip/gateway.cpp Executable file
View File

@ -0,0 +1,883 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#include <regex>
#include "gateway.h"
namespace sml = boost::sml;
namespace td_api = td::td_api;
#define CALL_PROTO_MIN_LAYER 65
#define CALL_PROTO_MAX_LAYER 74
namespace state_machine::guards {
bool IsIncoming::operator()(const td::td_api::object_ptr<td::td_api::updateCall> &event) const {
return !event->call_->is_outgoing_;
}
bool IsInState::operator()(const td::td_api::object_ptr<td::td_api::updateCall> &event) const {
return event->call_->state_->get_id() == id_;
}
bool CallbackUriIsSet::operator()(const Settings &settings) const {
return !settings.callback_uri().empty();
}
bool IsMediaReady::operator()(const sip::events::CallMediaStateUpdate &event) const {
return event.has_media;
}
bool IsSipInState::operator()(const sip::events::CallStateUpdate &event) const {
return event.state == state_;
}
bool IsTextContent::operator()(const td::td_api::object_ptr<td::td_api::updateNewMessage> &event) const {
return event->message_->content_->get_id() == td::td_api::messageText::ID;
}
bool IsDtmfString::operator()(const td::td_api::object_ptr<td::td_api::updateNewMessage> &event) const {
// String must contain only characters as described on RFC 2833 section 3.10.
// if PJMEDIA_HAS_DTMF_FLASH is enabled, character 'R' is used to represent
// the event type 16 (flash) as stated in RFC 4730.
// PJSUA maximum number of characters are 32.
auto content = td_api::move_object_as<td_api::messageText>(event->message_->content_);
const std::regex dtmf_regex("^[0-9A-D*#]{1,32}$");
auto result = regex_match(content->text_->text_, dtmf_regex);
// Don't forget to put data back!
event->message_->content_ = td_api::move_object_as<td_api::MessageContent>(content);
return result;
}
}
namespace state_machine::actions {
void StoreSipId::operator()(Context &ctx, const sip::events::IncomingCall &event,
std::shared_ptr<spdlog::logger> logger) const {
ctx.sip_call_id = event.id;
DEBUG(logger, "[{}] associated with SIP#{}", ctx.id(), ctx.sip_call_id);
}
void StoreTgId::operator()(Context &ctx, const td::td_api::object_ptr<td::td_api::updateCall> &event,
std::shared_ptr<spdlog::logger> logger) const {
ctx.tg_call_id = event->call_->id_;
DEBUG(logger, "[{}] associated with TG#{}", ctx.id(), ctx.tg_call_id);
}
void StoreTgUserId::operator()(Context &ctx, const td::td_api::object_ptr<td::td_api::updateCall> &event,
std::shared_ptr<spdlog::logger> logger) const {
ctx.user_id = event->call_->user_id_;
DEBUG(logger, "[{}] stored user id {}", ctx.id(), ctx.user_id);
}
void CleanTgId::operator()(Context &ctx) const {
ctx.tg_call_id = 0;
}
void CleanSipId::operator()(Context &ctx) const {
ctx.sip_call_id = PJSUA_INVALID_ID;
}
void CleanUp::operator()(Context &ctx, sip::Client &sip_client, tg::Client &tg_client,
std::shared_ptr<spdlog::logger> logger) const {
TRACE(logger, "[{}] cleanup start", ctx.id());
if (ctx.controller) {
ctx.controller->Stop();
}
if (ctx.tg_call_id != 0) {
DEBUG(logger, "[{}] hangup TG #{}", ctx.id(), ctx.tg_call_id);
auto result = tg_client.send_query_async(td_api::make_object<td_api::discardCall>(
ctx.tg_call_id, /* call_id_ */
false, /* is_disconnected_ */
0, /* duration_ */
ctx.tg_call_id /*connection_id */
)).get();
if (result->get_id() == td_api::error::ID) {
logger->error("[{}] TG call discard failure:\n{}", ctx.id(), to_string(result));
}
ctx.tg_call_id = 0;
}
if (ctx.sip_call_id != PJSUA_INVALID_ID) {
try {
sip_client.Hangup(ctx.sip_call_id, ctx.hangup_prm);
} catch (const pj::Error &error) {
logger->error(error.reason);
}
ctx.sip_call_id = PJSUA_INVALID_ID;
}
TRACE(logger, "[{}] cleanup end");
}
void DialSip::operator()(Context &ctx, tg::Client &tg_client, sip::Client &sip_client,
const td_api::object_ptr<td_api::updateCall> &event,
OptionalQueue<state_machine::events::Event> &internal_events,
const Settings &settings, std::shared_ptr<spdlog::logger> logger) const {
auto tg_user_id = event->call_->user_id_;
auto response = tg_client.send_query_async(td_api::make_object<td_api::getUser>(tg_user_id)).get();
if (response->get_id() == td_api::error::ID) {
logger->error("[{}] get user info of id {} failed\n{}", ctx.id(), tg_user_id, to_string(response));
auto err = td_api::move_object_as<td_api::error>(response);
throw std::runtime_error{err->message_};
}
auto user = td::move_tl_object_as<td_api::user>(response);
auto prm = pj::CallOpParam(true);
auto &headers = prm.txOption.headers;
auto header = pj::SipHeader();
{
// debug purpose header
header.hName = "X-GW-Context";
header.hValue = ctx.id();
headers.push_back(header);
}
{
header.hName = "X-TG-ID";
header.hValue = std::to_string(tg_user_id);
headers.push_back(header);
}
if (!user->first_name_.empty()) {
header.hName = "X-TG-FirstName";
header.hValue = user->first_name_;
headers.push_back(header);
}
if (!user->last_name_.empty()) {
header.hName = "X-TG-LastName";
header.hValue = user->last_name_;
headers.push_back(header);
}
if (!user->username_.empty()) {
header.hName = "X-TG-Username";
header.hValue = user->username_;
headers.push_back(header);
}
if (!user->phone_number_.empty()) {
header.hName = "X-TG-Phone";
header.hValue = user->phone_number_;
headers.push_back(header);
}
try {
ctx.sip_call_id = sip_client.Dial(settings.callback_uri(), prm);
} catch (const pj::Error &error) {
pj::CallOpParam hangup_prm;
hangup_prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
hangup_prm.reason = error.reason;
internal_events.emplace(state_machine::events::InternalError{ctx.id(), hangup_prm});
return;
}
DEBUG(logger, "[{}] associated with SIP#{}", ctx.id(), ctx.sip_call_id);
}
void AnswerTg::operator()(Context &ctx, tg::Client &tg_client, const Settings &settings,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger) const {
auto response = tg_client.send_query_async(td_api::make_object<td_api::acceptCall>(
ctx.tg_call_id,
td_api::make_object<td_api::callProtocol>(settings.udp_p2p(),
settings.udp_reflector(),
CALL_PROTO_MIN_LAYER, CALL_PROTO_MAX_LAYER)
)).get();
if (response->get_id() == td_api::error::ID) {
logger->error("[{}] TG #{} accept failure\n{}", ctx.id(), ctx.tg_call_id, to_string(response));
auto error = td::move_tl_object_as<td_api::error>(response);
pj::CallOpParam hangup_prm;
hangup_prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
hangup_prm.reason = std::to_string(error->code_) + "; " + error->message_;
internal_events.emplace(state_machine::events::InternalError{ctx.id(), hangup_prm});
}
}
void AcceptIncomingSip::operator()(Context &ctx, sip::Client &sip_client, const Settings &settings,
const sip::events::IncomingCall &event,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger) const {
auto ext = event.extension;
bool ext_valid{true};
if (ext.length() > 3 && ext.substr(0, 3) == "tg#") {
ctx.ext_username = ext.substr(3, std::string::npos);
} else if (ext.length() > 1 && ext[0] == '+' && is_digits(ext.substr(1, std::string::npos))) {
ctx.ext_phone = ext.substr(1, std::string::npos);
} else if (is_digits(ext)) {
try {
ctx.user_id = std::stoi(ext);
} catch (const std::invalid_argument &e) {
ext_valid = false;
} catch (const std::out_of_range &e) {
ext_valid = false;
}
} else {
ext_valid = false;
}
if (ext_valid) {
auto a_prm = pj::CallOpParam(true);
a_prm.statusCode = PJSIP_SC_RINGING;
DEBUG(logger, "[{}] setting SIP #{} in ringing mode", ctx.id(), ctx.sip_call_id);
try {
sip_client.Answer(ctx.sip_call_id, a_prm);
} catch (const pj::Error &error) {
pj::CallOpParam hangup_prm;
hangup_prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
hangup_prm.reason = error.reason;
internal_events.emplace(state_machine::events::InternalError{ctx.id(), hangup_prm});
} catch (const std::runtime_error &error) {
pj::CallOpParam hangup_prm;
hangup_prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
hangup_prm.reason = error.what();
internal_events.emplace(state_machine::events::InternalError{ctx.id(), hangup_prm});
}
} else {
pj::CallOpParam prm;
prm.statusCode = PJSIP_SC_BAD_EXTENSION;
logger->warn("[{}] called invalid extension {}", ctx.id(), ext);
internal_events.emplace(state_machine::events::InternalError{ctx.id(), prm});
}
}
void AnswerSip::operator()(Context &ctx, sip::Client &sip_client, const Settings &settings,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger) const {
auto prm = pj::CallOpParam(true);
prm.statusCode = PJSIP_SC_OK;
DEBUG(logger, "[{}] answering SIP #{}", ctx.id(), ctx.sip_call_id);
try {
sip_client.Answer(ctx.sip_call_id, prm);
} catch (const pj::Error &error) {
pj::CallOpParam hangup_prm;
hangup_prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
hangup_prm.reason = error.reason;
internal_events.emplace(state_machine::events::InternalError{ctx.id(), hangup_prm});
} catch (const std::runtime_error &error) {
pj::CallOpParam hangup_prm;
hangup_prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
hangup_prm.reason = error.what();
internal_events.emplace(state_machine::events::InternalError{ctx.id(), hangup_prm});
}
}
void state_machine::actions::CreateTgVoip::operator()(Context &ctx, const Settings &settings,
const td::td_api::object_ptr<td::td_api::updateCall> &event,
std::shared_ptr<spdlog::logger> logger) const {
DEBUG(logger, "[{}] creating voip for TG #{}", ctx.id(), ctx.tg_call_id);
using namespace tgvoip;
auto state = td_api::move_object_as<td_api::callStateReady>(event->call_->state_);
auto voip_controller = std::make_shared<VoIPController>();
static auto config = VoIPController::Config(
3000, /*init_timeout*/
3000, /*recv_timeout*/
DATA_SAVING_NEVER, /*data_saving*/
settings.aec_enabled(), /*enableAEC*/
settings.ns_enabled(), /*enableNS*/
settings.agc_enabled(), /*enableAGC*/
false /*enableCallUpgrade*/
);
voip_controller->SetConfig(config);
if (settings.voip_proxy_enabled()) {
voip_controller->SetProxy(
PROXY_SOCKS5,
settings.voip_proxy_address(),
settings.voip_proxy_port(),
settings.voip_proxy_username(),
settings.voip_proxy_password()
);
}
char encryption_key[256];
memcpy(encryption_key, state->encryption_key_.c_str(), 256);
voip_controller->SetEncryptionKey(encryption_key, event->call_->is_outgoing_);
vector<Endpoint> endpoints;
for (auto &connection : state->connections_) {
unsigned char peer_tag[16];
memcpy(peer_tag, connection->peer_tag_.c_str(), 16);
auto ipv4 = IPv4Address(connection->ip_);
auto ipv6 = IPv6Address(connection->ipv6_);
endpoints.emplace_back(Endpoint(connection->id_,
static_cast<uint16_t>(connection->port_),
ipv4,
ipv6,
Endpoint::TYPE_UDP_RELAY,
peer_tag));
}
voip_controller->SetRemoteEndpoints(endpoints, settings.udp_p2p(), CALL_PROTO_MAX_LAYER);
voip_controller->Start();
voip_controller->Connect();
ctx.controller = std::move(voip_controller);
}
void state_machine::actions::BridgeAudio::operator()(Context &ctx, sip::Client &sip_client,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger) const {
DEBUG(logger, "[{}] bridging tgvoip audio with SIP#{}", ctx.id(), ctx.sip_call_id);
try {
sip_client.BridgeAudio(ctx.sip_call_id,
ctx.controller->AudioMediaInput(),
ctx.controller->AudioMediaOutput());
} catch (const pj::Error &error) {
pj::CallOpParam hangup_prm;
hangup_prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
hangup_prm.reason = error.reason;
internal_events.emplace(state_machine::events::InternalError{ctx.id(), hangup_prm});
} catch (const std::runtime_error &error) {
pj::CallOpParam hangup_prm;
hangup_prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
hangup_prm.reason = error.what();
internal_events.emplace(state_machine::events::InternalError{ctx.id(), hangup_prm});
}
}
void SetHangupPrm::operator()(Context &ctx, const state_machine::events::InternalError &event) const {
ctx.hangup_prm = event.prm;
}
void DialTg::operator()(Context &ctx, tg::Client &tg_client, const Settings &settings, Cache &cache,
std::chrono::steady_clock::time_point &block_until,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger) {
DEBUG(logger, "[{}] dialing tg", ctx.id());
ctx_ = &ctx;
tg_client_ = &tg_client;
settings_ = &settings;
cache_ = &cache;
block_until_ = &block_until;
internal_events_ = &internal_events;
logger_ = logger;
if (block_until > std::chrono::steady_clock::now()) {
auto seconds_left = std::chrono::duration_cast<std::chrono::seconds>(
block_until - std::chrono::steady_clock::now()).count();
DEBUG(logger, "[{}] dropping call cause of temp TG block for {} seconds", ctx.id(), seconds_left);
pj::CallOpParam prm;
prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
prm.reason = "FLOOD_WAIT " + std::to_string(seconds_left);
internal_events.emplace(state_machine::events::InternalError{ctx.id(), prm});
return;
}
if (!ctx.ext_username.empty()) {
dial_by_username();
} else if (!ctx.ext_phone.empty()) {
dial_by_phone();
} else {
dial_by_id(ctx.user_id);
}
}
void DialTg::dial_by_id(int32_t id) {
auto response = tg_client_->send_query_async(td_api::make_object<td_api::createCall>(
id /* id */,
td_api::make_object<td_api::callProtocol>(settings_->udp_p2p(), settings_->udp_reflector(),
CALL_PROTO_MIN_LAYER, CALL_PROTO_MAX_LAYER))
).get();
if (response->get_id() == td_api::error::ID) {
logger_->error("[{}] failed to create telegram call\n{}", ctx_->id(), to_string(response));
auto error = td::move_tl_object_as<td_api::error>(response);
auto prm = pj::CallOpParam(true);
prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
prm.reason = std::to_string(error->code_) + "; " + error->message_;
internal_events_->emplace(state_machine::events::InternalError{ctx_->id(), prm});
parse_error(std::move(error));
return;
}
auto call_id_ = td::move_tl_object_as<td_api::callId>(response);
DEBUG(logger_, "[{}] associated with TG#{}", ctx_->id(), call_id_->id_);
ctx_->tg_call_id = call_id_->id_;
}
void DialTg::dial_by_phone() {
auto it = cache_->phone_cache.find(ctx_->ext_phone);
if (it != cache_->phone_cache.end()) {
DEBUG(logger_, "[{}] found id {} for {} in phone cache", ctx_->id(), it->second, ctx_->ext_phone);
dial_by_id(it->second);
return;
}
auto contact = td_api::make_object<td_api::contact>();
contact->phone_number_ = ctx_->ext_phone;
auto contacts = std::vector<td_api::object_ptr<td_api::contact>>();
contacts.emplace_back(std::move(contact));
auto response = tg_client_->send_query_async(
td_api::make_object<td_api::importContacts>(std::move(contacts))).get();
if (response->get_id() == td_api::error::ID) {
logger_->error("[{}] contacts import failure\n{}", ctx_->id(), to_string(response));
auto error = td::move_tl_object_as<td_api::error>(response);
auto prm = pj::CallOpParam(true);
prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
prm.reason = std::to_string(error->code_) + "; " + error->message_;
internal_events_->emplace(state_machine::events::InternalError{ctx_->id(), prm});
parse_error(std::move(error));
return;
}
auto imported_contacts = td::move_tl_object_as<td_api::importedContacts>(response);
auto user_id_ = imported_contacts->user_ids_[0];
if (user_id_ == 0) {
logger_->error("[{}] {} is not telegram user yet", ctx_->id(), ctx_->ext_phone);
auto prm = pj::CallOpParam(true);
prm.statusCode = PJSIP_SC_NOT_FOUND;
prm.reason = "not registered in telegram";
internal_events_->emplace(state_machine::events::InternalError{ctx_->id(), prm});
return;
}
DEBUG(logger_, "[{}] adding id {} for {} to phone cache", ctx_->id(), user_id_, ctx_->ext_phone);
cache_->phone_cache.emplace(ctx_->ext_phone, user_id_);
dial_by_id(user_id_);
}
void DialTg::dial_by_username() {
auto it = cache_->username_cache.find(ctx_->ext_username);
if (it != cache_->username_cache.end()) {
DEBUG(logger_, "[{}] found id {} for {} in username cache", ctx_->id(), it->second, ctx_->ext_username);
dial_by_id(it->second);
return;
}
auto response = tg_client_->send_query_async(
td_api::make_object<td_api::searchPublicChat>(ctx_->ext_username)).get();
if (response->get_id() == td_api::error::ID) {
logger_->error("[{}] chat request failure\n{}", ctx_->id(), to_string(response));
auto error = td::move_tl_object_as<td_api::error>(response);
auto prm = pj::CallOpParam(true);
prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
prm.reason = std::to_string(error->code_) + "; " + error->message_;
internal_events_->emplace(state_machine::events::InternalError{ctx_->id(), prm});
parse_error(std::move(error));
return;
}
auto chat = td::move_tl_object_as<td_api::chat>(response);
if (chat->type_->get_id() != td_api::chatTypePrivate::ID) {
auto prm = pj::CallOpParam(true);
prm.statusCode = PJSIP_SC_INTERNAL_SERVER_ERROR;
prm.reason = "not a user";
internal_events_->emplace(state_machine::events::InternalError{ctx_->id(), prm});
return;
}
auto id = static_cast<int32_t>(chat->id_);
DEBUG(logger_, "[{}] adding id {} for {} to username cache", ctx_->id(), id, ctx_->ext_username);
cache_->username_cache.emplace(ctx_->ext_username, id);
dial_by_id(id);
}
void DialTg::parse_error(td_api::object_ptr<td::td_api::error> error) {
std::smatch match;
const std::regex delay_regex("Too Many Requests: retry after (\\d+)");
if (std::regex_search(error->message_, match, delay_regex)) {
auto seconds = std::stoi(match[1]);
*block_until_ = std::chrono::steady_clock::now() +
std::chrono::seconds(seconds + settings_->extra_wait_time());
return;
}
const std::regex flood_regex("PEER_FLOOD");
if (std::regex_search(error->message_, match, flood_regex)) {
*block_until_ = std::chrono::steady_clock::now() + std::chrono::seconds(settings_->peer_flood_time());
return;
}
}
void DialDtmf::operator()(Context &ctx, sip::Client &sip_client,
const td_api::object_ptr<td::td_api::updateNewMessage> &event,
std::shared_ptr<spdlog::logger> logger) const {
auto content = td_api::move_object_as<td_api::messageText>(event->message_->content_);
DEBUG(logger, "[{}] sending DTMF {}", ctx.id(), content->text_->text_);
try {
sip_client.DialDtmf(ctx.sip_call_id, content->text_->text_);
} catch (const pj::Error &error) {
logger->error(error.reason);
}
}
}
namespace state_machine {
Logger::Logger(std::string context_id, shared_ptr<spdlog::logger> logger) : logger_(std::move(logger)),
context_id_(std::move(context_id)) {
TRACE(logger_, "[{}] logger created", context_id_);
}
Logger::~Logger() {
TRACE(logger_, "[{}] ~logger", context_id_);
};
template<class SM>
void Logger::log_process_event(const td::td_api::object_ptr<td::td_api::updateCall> &event) {
TRACE(logger_, "[{}] [process_event]\n{}", context_id_, td::td_api::to_string(event));
};
template<class SM>
void Logger::log_process_event(const sip::events::Event &event) {
TRACE(logger_, "[{}] [process_event] sip", context_id_);
};
template<class SM, class TEvent>
void Logger::log_process_event(const TEvent &) {
TRACE(logger_, "[{}] [process_event] {}", context_id_, sml::aux::get_type_name<TEvent>());
}
template<class SM, class TGuard, class TEvent>
void Logger::log_guard(const TGuard &, const TEvent &event, bool result) {
TRACE(logger_, "[{}] [guard] {} {} {}", context_id_, sml::aux::get_type_name<TGuard>(),
"event", (result ? "[OK]" : "[Reject]"));
};
template<class SM, class TAction, class TEvent>
void Logger::log_action(const TAction &, const TEvent &event) {
TRACE(logger_, "[{}] [action] {} {}", context_id_, sml::aux::get_type_name<TAction>(), "event");
};
template<class SmLogger, class TSrcState, class TDstState>
void Logger::log_state_change(const TSrcState &src, const TDstState &dst) {
TRACE(logger_, "[{}] [transition] {} -> {}", context_id_, src.c_str(), dst.c_str());
}
struct from_tg {
auto operator()() const {
using namespace boost::sml;
using namespace td::td_api;
using namespace guards;
using namespace actions;
return make_transition_table(
*"sip_wait_media"_s + event<sip::events::CallMediaStateUpdate>[IsMediaReady{}]
/ AnswerTg{} = "wait_tg"_s,
"wait_tg"_s + event<object_ptr<updateCall>>[IsInState{callStateReady::ID}]
/ (CreateTgVoip{}, BridgeAudio{}) = "wait_dtmf"_s,
"wait_dtmf"_s + event<object_ptr<updateNewMessage>>[IsTextContent{} && IsDtmfString{}]
/ DialDtmf{} = "wait_dtmf"_s
);
}
};
struct from_sip {
auto operator()() const {
using namespace boost::sml;
using namespace td::td_api;
using namespace guards;
using namespace actions;
return make_transition_table(
*"sip_wait_confirm"_s + event<sip::events::CallStateUpdate>
[IsSipInState{PJSIP_INV_STATE_EARLY}] / DialTg{} = "wait_tg"_s,
"wait_tg"_s + event<object_ptr<updateCall>>[IsInState{callStateReady::ID}]
/ (StoreTgUserId{}, CreateTgVoip{}, AnswerSip{}) = "sip_wait_media"_s,
"sip_wait_media"_s + event<sip::events::CallMediaStateUpdate>[IsMediaReady{}]
/ BridgeAudio{} = "wait_dtmf"_s,
"wait_dtmf"_s + event<object_ptr<updateNewMessage>>[IsTextContent{} && IsDtmfString{}]
/ DialDtmf{} = "wait_dtmf"_s
);
}
};
struct StateMachine {
auto operator()() const {
using namespace boost::sml;
using namespace td::td_api;
using namespace guards;
using namespace actions;
using namespace events;
return make_transition_table(
*"init"_s + event<object_ptr<updateCall>>
[IsIncoming{} && IsInState{callStatePending::ID} && CallbackUriIsSet{}]
/ (StoreTgId{}, StoreTgUserId{}, DialSip{}) = state<from_tg>,
"init"_s + event<object_ptr<updateCall>>
[IsIncoming{} && IsInState{callStatePending::ID} && !CallbackUriIsSet{}]
/ StoreTgId{} = X,
"init"_s + event<object_ptr<updateCall>> = X,
"init"_s + event<sip::events::IncomingCall> / (StoreSipId{}, AcceptIncomingSip{}) = state<from_sip>,
"init"_s + event<sip::events::CallStateUpdate> = X,
"init"_s + event<sip::events::CallMediaStateUpdate> = X,
state<from_tg> + event<object_ptr<updateCall>>
[IsInState{callStateDiscarded::ID} || IsInState{callStateError::ID}] /
CleanTgId{} = X,
state<from_tg> + event<sip::events::CallStateUpdate>
[IsSipInState{PJSIP_INV_STATE_DISCONNECTED}] / CleanSipId{} = X,
state<from_tg> + event<InternalError> / SetHangupPrm{} = X,
state<from_sip> + event<object_ptr<updateCall>>
[IsInState{callStateDiscarded::ID} || IsInState{callStateError::ID}] /
CleanTgId{} = X,
state<from_sip> + event<InternalError> / SetHangupPrm{} = X,
state<from_sip> + event<sip::events::CallStateUpdate>
[IsSipInState{PJSIP_INV_STATE_DISCONNECTED}] / CleanSipId{} = X,
X + on_entry<_> / CleanUp{}
);
}
};
}
Context::Context() : id_(next_ctx_id()) {}
const std::string Context::id() const { return id_; };
std::string Context::next_ctx_id() {
static int64_t ctx_counter{0};
static std::string id_prefix = std::to_string(getpid()) + "-";
return id_prefix + std::to_string(++ctx_counter);
}
Gateway::Gateway(sip::Client &sip_client_, tg::Client &tg_client_,
OptionalQueue<sip::events::Event> &sip_events_,
OptionalQueue<tg::Client::Object> &tg_events_,
std::shared_ptr<spdlog::logger> logger_,
Settings &settings)
: sip_client_(sip_client_), tg_client_(tg_client_), logger_(std::move(logger_)),
sip_events_(sip_events_), tg_events_(tg_events_), settings_(settings) {}
void Gateway::start(volatile sig_atomic_t e_flag) {
load_cache();
while (!e_flag) {
auto tick_start = std::chrono::steady_clock::now();
if (auto event = internal_events_.pop(); event) {
std::visit([this](auto &&casted_event) {
process_event(casted_event);
}, event.value());
}
if (auto event = tg_events_.pop(); event) {
using namespace td::td_api;
auto &&object = event.value();
switch (object->get_id()) {
case updateCall::ID:
process_event(move_object_as<updateCall>(object));
break;
case updateNewMessage::ID:
process_event(move_object_as<updateNewMessage>(object));
break;
default:
break;
}
}
if (auto event = sip_events_.pop(); event) {
std::visit([this](auto &&casted_event) {
process_event(casted_event);
}, event.value());
}
auto tick_end = std::chrono::steady_clock::now();
auto duration = tick_end - tick_start;
auto sleep_time = std::chrono::milliseconds(10) - duration;
if (sleep_time.count() > 0) {
std::this_thread::sleep_for(sleep_time);
}
}
}
void Gateway::load_cache() {
logger_->info("Loading contacts cache");
auto search_response = tg_client_.send_query_async(
td::td_api::make_object<td::td_api::searchContacts>("", INT32_MAX)).get();
if (search_response->get_id() == td::td_api::error::ID) {
logger_->error("offline contacts search failed during cache fill\n{}", to_string(search_response));
return;
}
auto users = td::td_api::move_object_as<td::td_api::users>(search_response);
std::vector<std::future<tg::Client::Object>> responses;
for (auto user_id : users->user_ids_) {
responses.emplace_back(tg_client_.send_query_async(td::td_api::make_object<td::td_api::getUser>(user_id)));
}
for (auto &future : responses) {
auto response = future.get();
if (response->get_id() == td::td_api::error::ID) {
logger_->error("failed to get user info from DB\n{}", to_string(response));
continue;
}
auto user = td::td_api::move_object_as<td::td_api::user>(response);
if (!user->have_access_) {
return;
}
if (!user->username_.empty()) {
cache_.username_cache.emplace(user->username_, user->id_);
}
if (!user->phone_number_.empty()) {
cache_.phone_cache.emplace(user->phone_number_, user->id_);
}
}
logger_->info("Loaded {} usernames and {} phones into contacts cache",
cache_.username_cache.size(),
cache_.phone_cache.size());
}
std::vector<Bridge *>::iterator Gateway::search_call(const std::function<bool(const Bridge *)> &predicate) {
auto iter = std::find_if(bridges.begin(), bridges.end(), predicate);
if (iter == bridges.end()) {
auto ctx = std::make_unique<Context>();
auto sm_logger = std::make_unique<state_machine::Logger>(ctx->id(), logger_);
auto sm = std::make_unique<state_machine::sm_t>(*sm_logger, sip_client_, tg_client_, settings_, logger_,
*ctx, cache_, internal_events_, block_until);
auto bridge = new Bridge;
bridge->ctx = std::move(ctx);
bridge->sm = std::move(sm);
bridge->logger = std::move(sm_logger);
bridges.emplace_back(bridge);
iter = bridges.end() - 1;
}
return iter;
}
void Gateway::process_event(td::td_api::object_ptr<td::td_api::updateCall> update_call) {
auto iter = search_call([id = update_call->call_->id_](const Bridge *bridge) {
return bridge->ctx->tg_call_id == id;
});
(*iter)->sm->process_event(update_call);
if ((*iter)->sm->is(sml::X)) {
delete *iter;
bridges.erase(iter);
}
}
void Gateway::process_event(td::td_api::object_ptr<td::td_api::updateNewMessage> update_message) {
std::vector<Bridge *> matches;
for (auto bridge : bridges) {
if (bridge->ctx->user_id == update_message->message_->sender_user_id_) {
matches.emplace_back(bridge);
}
}
if (matches.size() > 1) {
logger_->error("ambiguous message from {}", update_message->message_->sender_user_id_);
return;
} else if (matches.size() == 1) {
TRACE(logger_, "routing message to ctx {}", matches[0]->ctx->id());
matches[0]->sm->process_event(update_message);
}
}
void Gateway::process_event(state_machine::events::InternalError &event) {
auto iter = std::find_if(bridges.begin(), bridges.end(), [id = event.ctx_id](const Bridge *bridge) {
return bridge->ctx->id() == id;
});
if (iter == bridges.end()) {
return;
}
(*iter)->sm->process_event(event);
if ((*iter)->sm->is(sml::X)) {
delete *iter;
bridges.erase(iter);
}
}
template<typename TSipEvent>
void Gateway::process_event(const TSipEvent &event) {
auto iter = search_call([id = event.id](const Bridge *bridge) {
return bridge->ctx->sip_call_id == id;
});
(*iter)->sm->process_event(event);
if ((*iter)->sm->is(sml::X)) {
delete *iter;
bridges.erase(iter);
}
}

306
tg2sip/gateway.h Executable file
View File

@ -0,0 +1,306 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#ifndef TG2SIP_GATEWAY_H
#define TG2SIP_GATEWAY_H
#include <stdexcept>
#include <regex>
#include <libtgvoip/VoIPController.h>
#include <boost/sml.hpp>
#include <csignal>
#include "sip.h"
#include "tg.h"
#include "utils.h"
#include "queue.h"
namespace sml = boost::sml;
class Context;
struct Cache;
namespace state_machine::events {
struct InternalError {
std::string ctx_id;
pj::CallOpParam prm;
};
typedef std::variant<InternalError> Event;
}
namespace state_machine::guards {
struct IsIncoming {
bool operator()(const td::td_api::object_ptr<td::td_api::updateCall> &event) const;
};
class IsInState {
public:
explicit IsInState(int32_t id) : id_(id) {}
bool operator()(const td::td_api::object_ptr<td::td_api::updateCall> &event) const;
private:
const int32_t id_;
};
class IsSipInState {
public:
explicit IsSipInState(pjsip_inv_state state) : state_(state) {}
bool operator()(const sip::events::CallStateUpdate &event) const;
private:
const pjsip_inv_state state_;
};
struct CallbackUriIsSet {
bool operator()(const Settings &settings) const;
};
struct IsMediaReady {
bool operator()(const sip::events::CallMediaStateUpdate &event) const;
};
struct IsTextContent {
bool operator()(const td::td_api::object_ptr<td::td_api::updateNewMessage> &event) const;
};
struct IsDtmfString {
bool operator()(const td::td_api::object_ptr<td::td_api::updateNewMessage> &event) const;
};
}
namespace state_machine::actions {
struct StoreTgId {
void operator()(Context &ctx, const td::td_api::object_ptr<td::td_api::updateCall> &event,
std::shared_ptr<spdlog::logger> logger) const;
};
struct StoreTgUserId {
void operator()(Context &ctx, const td::td_api::object_ptr<td::td_api::updateCall> &event,
std::shared_ptr<spdlog::logger> logger) const;
};
struct CleanTgId {
void operator()(Context &ctx) const;
};
struct CleanSipId {
void operator()(Context &ctx) const;
};
struct StoreSipId {
void operator()(Context &ctx, const sip::events::IncomingCall &event,
std::shared_ptr<spdlog::logger> logger) const;
};
struct DialSip {
void operator()(Context &ctx, tg::Client &tg_client, sip::Client &sip_client,
const td::td_api::object_ptr<td::td_api::updateCall> &event,
OptionalQueue<state_machine::events::Event> &internal_events,
const Settings &settings, std::shared_ptr<spdlog::logger> logger) const;
};
struct AnswerTg {
void operator()(Context &ctx, tg::Client &tg_client, const Settings &settings,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger) const;
};
struct AcceptIncomingSip {
void operator()(Context &ctx, sip::Client &sip_client, const Settings &settings,
const sip::events::IncomingCall &event,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger) const;
};
struct AnswerSip {
void operator()(Context &ctx, sip::Client &sip_client, const Settings &settings,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger) const;
};
struct BridgeAudio {
void operator()(Context &ctx, sip::Client &sip_client,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger) const;
};
struct CreateTgVoip {
void operator()(Context &ctx, const Settings &settings,
const td::td_api::object_ptr<td::td_api::updateCall> &event,
std::shared_ptr<spdlog::logger> logger) const;
};
struct CleanUp {
void operator()(Context &ctx, sip::Client &sip_client, tg::Client &tg_client,
std::shared_ptr<spdlog::logger> logger) const;
};
struct SetHangupPrm {
void operator()(Context &ctx, const state_machine::events::InternalError &event) const;
};
class DialTg {
private:
Context *ctx_;
tg::Client *tg_client_;
Settings const *settings_;
Cache *cache_;
std::chrono::steady_clock::time_point *block_until_;
OptionalQueue<state_machine::events::Event> *internal_events_;
std::shared_ptr<spdlog::logger> logger_;
void parse_error(td::td_api::object_ptr<td::td_api::error> error);
void dial_by_id(int32_t id);
void dial_by_phone();
void dial_by_username();
public:
void operator()(Context &ctx, tg::Client &tg_client, const Settings &settings, Cache &cache,
std::chrono::steady_clock::time_point &block_until,
OptionalQueue<state_machine::events::Event> &internal_events,
std::shared_ptr<spdlog::logger> logger);
};
struct DialDtmf {
void operator()(Context &ctx, sip::Client &sip_client,
const td::td_api::object_ptr<td::td_api::updateNewMessage> &event,
std::shared_ptr<spdlog::logger> logger) const;
};
}
namespace state_machine {
class Logger {
public:
Logger(std::string context_id, shared_ptr<spdlog::logger> logger);
virtual ~Logger();
template<class SM>
void log_process_event(const td::td_api::object_ptr<td::td_api::updateCall> &event);
template<class SM>
void log_process_event(const sip::events::Event &event);
template<class SM, class TEvent>
void log_process_event(const TEvent &);
template<class SM, class TGuard, class TEvent>
void log_guard(const TGuard &, const TEvent &event, bool result);
template<class SM, class TAction, class TEvent>
void log_action(const TAction &, const TEvent &event);
template<class SmLogger, class TSrcState, class TDstState>
void log_state_change(const TSrcState &src, const TDstState &dst);
private:
const std::string context_id_;
std::shared_ptr<spdlog::logger> logger_;
};
struct StateMachine;
typedef sml::sm<StateMachine, sml::logger<Logger>, sml::thread_safe<std::recursive_mutex>> sm_t;
}
struct Cache {
std::map<std::string, int32_t> username_cache;
std::map<std::string, int32_t> phone_cache;
};
class Context {
public:
Context();
const std::string id() const;
pjsua_call_id sip_call_id{PJSUA_INVALID_ID};
int32_t tg_call_id{0};
std::shared_ptr<tgvoip::VoIPController> controller{nullptr};
std::string ext_phone;
std::string ext_username;
int32_t user_id{0};
pj::CallOpParam hangup_prm;
private:
const std::string id_;
std::string next_ctx_id();
};
struct Bridge {
std::unique_ptr<state_machine::sm_t> sm;
std::unique_ptr<Context> ctx;
std::unique_ptr<state_machine::Logger> logger;
};
class Gateway {
public:
Gateway(sip::Client &sip_client_, tg::Client &tg_client_,
OptionalQueue<sip::events::Event> &sip_events_,
OptionalQueue<tg::Client::Object> &tg_events_,
std::shared_ptr<spdlog::logger> logger_,
Settings &settings);
Gateway(const Gateway &) = delete;
Gateway &operator=(const Gateway &) = delete;
void start(volatile sig_atomic_t e_flag);
private:
std::shared_ptr<spdlog::logger> logger_;
sip::Client &sip_client_;
tg::Client &tg_client_;
const Settings &settings_;
OptionalQueue<sip::events::Event> &sip_events_;
OptionalQueue<tg::Client::Object> &tg_events_;
OptionalQueue<state_machine::events::Event> internal_events_;
std::chrono::steady_clock::time_point block_until{std::chrono::steady_clock::now()};
Cache cache_;
// Would be better to use smart pointer here,
// but it is not allowed by sm_t forward declaration
std::vector<Bridge *> bridges;
std::vector<Bridge *>::iterator search_call(const std::function<bool(const Bridge *)> &predicate);
void process_event(td::td_api::object_ptr<td::td_api::updateCall> update_call);
void process_event(td::td_api::object_ptr<td::td_api::updateNewMessage> update_message);
void process_event(state_machine::events::InternalError &event);
template<typename TSipEvent>
void process_event(const TSipEvent &event);
void load_cache();
};
#endif //TG2SIP_GATEWAY_H

243
tg2sip/gen_db.cpp Normal file
View File

@ -0,0 +1,243 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <INIReader.h>
#include <td/telegram/Client.h>
#include <td/telegram/Log.h>
#include <functional>
#include <map>
#include "settings.h"
// overloaded
namespace detail {
template<class... Fs>
struct overload;
template<class F>
struct overload<F> : public F {
explicit overload(F f) : F(f) {
}
};
template<class F, class... Fs>
struct overload<F, Fs...>
: public overload<F>, overload<Fs...> {
overload(F f, Fs... fs) : overload<F>(f), overload<Fs...>(fs...) {
}
using overload<F>::operator();
using overload<Fs...>::operator();
};
} // namespace detail
template<class... F>
auto overloaded(F... f) {
return detail::overload<F...>(f...);
}
namespace td_api = td::td_api;
class TDClient {
public:
explicit TDClient(Settings &settings_) : settings(settings_) {
td::Log::set_verbosity_level(1);
client = std::make_unique<td::Client>();
}
void auth() {
while (!sequence_done) {
process_response(client->receive(10));
}
}
private:
using Object = td_api::object_ptr<td_api::Object>;
Settings &settings;
bool sequence_done{false};
std::unique_ptr<td::Client> client;
td_api::object_ptr<td_api::AuthorizationState> authorization_state;
std::uint64_t current_query_id{0};
std::uint64_t authentication_query_id{0};
std::map<std::uint64_t, std::function<void(Object)>> handlers;
std::uint64_t next_query_id() {
return ++current_query_id;
}
void send_query(td_api::object_ptr<td_api::Function> f, std::function<void(Object)> handler) {
auto query_id = next_query_id();
if (handler) {
handlers.emplace(query_id, std::move(handler));
}
client->send({query_id, std::move(f)});
}
void process_response(td::Client::Response response) {
if (!response.object) {
return;
}
if (response.id == 0) {
return process_update(std::move(response.object));
}
auto it = handlers.find(response.id);
if (it != handlers.end()) {
it->second(std::move(response.object));
handlers.erase(it->first);
}
}
void process_update(td_api::object_ptr<td_api::Object> update) {
td_api::downcast_call(
*update, overloaded(
[this](td_api::updateAuthorizationState &update_authorization_state) {
authorization_state = std::move(update_authorization_state.authorization_state_);
on_authorization_state_update();
},
[](auto &update) {}));
}
auto create_authentication_query_handler() {
return [this, id = authentication_query_id](Object object) {
if (id == authentication_query_id) {
check_authentication_error(std::move(object));
}
};
}
void check_authentication_error(Object object) {
if (object->get_id() == td_api::error::ID) {
auto error = td::move_tl_object_as<td_api::error>(object);
std::cerr << "Error: " << to_string(error);
on_authorization_state_update();
}
}
void on_authorization_state_update() {
authentication_query_id++;
td_api::downcast_call(
*authorization_state,
overloaded(
[this](td_api::authorizationStateReady &) {
sequence_done = true;
std::cerr << "Authorization OK" << std::endl;
},
[this](td_api::authorizationStateLoggingOut &) {
sequence_done = true;
std::cerr << "Logging out" << std::endl;
},
[this](td_api::authorizationStateClosing &) {
std::cerr << "Closing" << std::endl;
},
[this](td_api::authorizationStateClosed &) {
sequence_done = true;
std::cerr << "Terminated" << std::endl;
},
[this](td_api::authorizationStateWaitCode &wait_code) {
std::string first_name;
std::string last_name;
if (!wait_code.is_registered_) {
std::cerr << "Enter your first name: ";
std::cin >> first_name;
std::cerr << "Enter your last name: ";
std::cin >> last_name;
}
std::cerr << "Enter authentication code: ";
std::string code;
std::cin >> code;
send_query(
td_api::make_object<td_api::checkAuthenticationCode>(code, first_name, last_name),
create_authentication_query_handler());
},
[this](td_api::authorizationStateWaitPassword &) {
std::cerr << "Enter authentication password: ";
std::string password;
std::cin >> password;
send_query(td_api::make_object<td_api::checkAuthenticationPassword>(password),
create_authentication_query_handler());
},
[this](td_api::authorizationStateWaitPhoneNumber &) {
std::cerr << "Enter phone number: ";
std::string phone_number;
std::cin >> phone_number;
send_query(td_api::make_object<td_api::setAuthenticationPhoneNumber>(
phone_number, false /*allow_flash_calls*/, false /*is_current_phone_number*/),
create_authentication_query_handler());
},
[this](td_api::authorizationStateWaitEncryptionKey &) {
send_query(td_api::make_object<td_api::checkDatabaseEncryptionKey>(""),
create_authentication_query_handler());
td_api::object_ptr<td_api::Proxy> proxy;
if (settings.proxy_enabled()) {
auto socks_proxy = td_api::make_object<td_api::proxySocks5>(
settings.proxy_address(),
settings.proxy_port(),
settings.proxy_username(),
settings.proxy_password()
);
proxy = td_api::move_object_as<td_api::Proxy>(socks_proxy);
} else {
auto empty_proxy = td_api::make_object<td_api::proxyEmpty>();
proxy = td_api::move_object_as<td_api::Proxy>(empty_proxy);
}
send_query(td_api::make_object<td_api::setProxy>(
td_api::move_object_as<td_api::Proxy>(proxy)),
[](Object) {});
},
[this](td_api::authorizationStateWaitTdlibParameters &) {
auto lib_parameters = td_api::make_object<td_api::tdlibParameters>();
lib_parameters = td_api::make_object<td_api::tdlibParameters>();
lib_parameters->api_id_ = settings.api_id();
lib_parameters->api_hash_ = settings.api_hash();
lib_parameters->database_directory_ = settings.db_folder();
lib_parameters->system_language_code_ = settings.sys_lang_code();
lib_parameters->device_model_ = settings.device_model();
lib_parameters->system_version_ = settings.system_version();
lib_parameters->application_version_ = settings.app_version();
lib_parameters->use_file_database_ = false;
lib_parameters->use_chat_info_database_ = true;
lib_parameters->use_message_database_ = false;
lib_parameters->use_secret_chats_ = false;
lib_parameters->enable_storage_optimizer_ = true;
send_query(td_api::make_object<td_api::setTdlibParameters>(std::move(lib_parameters)),
create_authentication_query_handler());
}));
}
};
int main() {
auto reader = INIReader("settings.ini");
Settings settings(reader);
if (!settings.is_loaded()) {
return 1;
}
TDClient client(settings);
client.auth();
}

65
tg2sip/logging.cpp Executable file
View File

@ -0,0 +1,65 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <td/telegram/Log.h>
#include "logging.h"
void init_logging(Settings &settings) {
try {
std::vector<spdlog::sink_ptr> sinks;
auto console_sink = std::make_shared<spdlog::sinks::ansicolor_stdout_sink_mt>();
console_sink->set_level(static_cast<spdlog::level::level_enum>(settings.console_min_level()));
sinks.push_back(std::move(console_sink));
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>("tg2sip.log", 100 * 1024 * 1024, 1);
file_sink->set_level(static_cast<spdlog::level::level_enum>(settings.file_min_level()));
sinks.push_back(std::move(file_sink));
std::vector<string> loggers;
loggers.emplace_back("core");
loggers.emplace_back("pjsip");
loggers.emplace_back("tgvoip");
for (auto log_name : loggers) {
auto logger = std::make_shared<spdlog::logger>(log_name, begin(sinks), end(sinks));
spdlog::register_logger(logger);
}
#ifndef DISABLE_DEBUG_
spdlog::set_pattern("%^[%T.%e][t:%t][p:%P][%n][%l] %v%$");
#endif
spdlog::apply_all([&](std::shared_ptr<spdlog::logger> l) {
l->set_level(spdlog::level::info);
});
}
catch (const spdlog::spdlog_ex &ex) {
std::cerr << ex.what() << std::endl;
}
td::Log::set_file_path("tdlib.log");
td::Log::set_verbosity_level(settings.tdlib_log_level());
spdlog::get("core")->set_level(static_cast<spdlog::level::level_enum>(settings.log_level()));
spdlog::get("pjsip")->set_level(static_cast<spdlog::level::level_enum>(settings.pjsip_log_level()));
spdlog::get("tgvoip")->set_level(static_cast<spdlog::level::level_enum>(settings.tgvoip_log_level()));
}

37
tg2sip/logging.h Executable file
View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#ifndef TG2SIP_LOGGING_H
#define TG2SIP_LOGGING_H
#include <spdlog/spdlog.h>
#include <spdlog/fmt/ostr.h>
#include "settings.h"
#ifndef DISABLE_DEBUG_
#define STRINGIZE_(x) #x
#define STRINGIZE(x) STRINGIZE_(x)
#define TRACE(logger, ...) logger->trace("" __FILE__ ":" STRINGIZE(__LINE__)" " __VA_ARGS__)
#define DEBUG(logger, ...) logger->debug(__VA_ARGS__)
#else
#define TRACE(logger, ...) (void)0
#define DEBUG(logger, ...) (void)0
#endif
void init_logging(Settings &settings);
#endif //TG2SIP_LOGGING_H

92
tg2sip/main.cpp Executable file
View File

@ -0,0 +1,92 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#include <thread>
#include <iostream>
#include <csignal>
#include <variant>
#include "logging.h"
#include "queue.h"
#include "utils.h"
#include "tg.h"
#include "sip.h"
#include "gateway.h"
volatile sig_atomic_t e_flag = 0;
int main() {
pthread_setname_np(pthread_self(), "main");
auto reader = INIReader("settings.ini");
Settings settings(reader);
if (!settings.is_loaded()) {
return 1;
}
init_logging(settings);
std::set_terminate([]() {
auto exc = std::current_exception();
try {
rethrow_exception(exc);
} catch (std::exception &e) {
spdlog::get("core")->critical("Unhandled exception: {}", e.what());
}
spdlog::drop_all();
std::abort();
});
auto logger = spdlog::get("core");
OptionalQueue<sip::events::Event> sip_events;
/* will be removed on pj endpoint destroy */
auto sip_log_writer = new sip::LogWriter(spdlog::get("pjsip"));
auto sip_account = std::make_unique<sip::Account>(logger);
auto sip_account_cfg = std::make_unique<sip::AccountConfig>(settings);
auto sip_client = std::make_unique<sip::Client>(
std::move(sip_account),
std::move(sip_account_cfg),
sip_events,
settings,
logger,
sip_log_writer
);
sip_client->start();
OptionalQueue<tg::Client::Object> tg_events;
auto tg_client = std::make_unique<tg::Client>(settings, logger, tg_events);
tg_client->start();
auto tg_is_ready_future = tg_client->is_ready();
auto tg_status = tg_is_ready_future.wait_for(std::chrono::seconds(5));
if (tg_status != std::future_status::ready || !tg_is_ready_future.get()) {
logger->critical("failed to start TG client");
return 1;
}
auto gateway = std::make_unique<Gateway>(*sip_client, *tg_client, sip_events, tg_events, logger, settings);
signal(SIGINT, [](int) { e_flag = 1; });
signal(SIGTERM, [](int) { e_flag = 1; });
gateway->start(e_flag);
logger->info("performing a graceful shutdown...");
spdlog::drop_all();
return 0;
}

61
tg2sip/queue.h Executable file
View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#ifndef TG2SIP_QUEUE_H
#define TG2SIP_QUEUE_H
#include <mutex>
#include <queue>
#include <condition_variable>
template<typename T>
class OptionalQueue {
public:
OptionalQueue() = default;
OptionalQueue(const OptionalQueue &) = delete;
OptionalQueue &operator=(const OptionalQueue &) = delete;
virtual ~OptionalQueue() = default;
void emplace(std::optional<T> &&value) {
{
std::unique_lock<std::mutex> lock(this->mutex);
q.emplace(std::move(value));
}
};
std::optional<T> pop() {
std::unique_lock<std::mutex> lock(this->mutex);
if (q.empty()) {
return std::nullopt;
}
std::optional<T> value = std::move(this->q.front());
this->q.pop();
return value;
};
private:
std::queue<std::optional<T>> q;
std::mutex mutex;
};
#endif //TG2SIP_QUEUE_H

85
tg2sip/settings.cpp Executable file
View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <thread>
#include "settings.h"
Settings::Settings(INIReader &reader) {
if (reader.ParseError() < 0) {
std::cerr << "Can't load settings file!\n";
return;
}
// logging
console_min_level_ = std::clamp(static_cast<int>(reader.GetInteger("logging", "console_min_level", 0)), 0, 6);
file_min_level_ = std::clamp(static_cast<int>(reader.GetInteger("logging", "file_min_level", 0)), 0, 6);
log_level_ = std::clamp(static_cast<int>(reader.GetInteger("logging", "core", 2)), 0, 6);
tdlib_log_level_ = std::clamp(static_cast<int>(reader.GetInteger("logging", "tdlib", 3)), 0, 6);
tgvoip_log_level_ = std::clamp(static_cast<int>(reader.GetInteger("logging", "tgvoip", 5)), 0, 6);
pjsip_log_level_ = std::clamp(static_cast<int>(reader.GetInteger("logging", "pjsip", 2)), 0, 6);
pjsip_log_sip_messages_ = reader.GetBoolean("logging", "sip_messages", true);
// sip
sip_port_ = static_cast<unsigned int>(reader.GetInteger("sip", "port", 0));
id_uri_ = reader.Get("sip", "id_uri", "sip:localhost");
callback_uri_ = reader.Get("sip", "callback_uri", "");
public_address_ = reader.Get("sip", "public_address", "");
stun_server_ = reader.Get("sip", "stun_server", "");
raw_pcm_ = reader.GetBoolean("sip", "raw_pcm", true);
sip_thread_count_ = static_cast<unsigned int>(reader.GetInteger("sip", "thread_count", 1));
sip_port_range_ = static_cast<unsigned int>(reader.GetInteger("sip", "port_range", 0));
//telegram
api_id_ = static_cast<int>(reader.GetInteger("telegram", "api_id", 0));
api_hash_ = reader.Get("telegram", "api_hash", "");
db_folder_ = reader.Get("telegram", "database_folder", "");
system_language_code_ = reader.Get("telegram", "database_folder", "en-US");
device_model_ = reader.Get("telegram", "device_model", "PC");
system_version_ = reader.Get("telegram", "system_version", "Linux");
application_version_ = reader.Get("telegram", "application_version", "1.0");
udp_p2p_ = reader.GetBoolean("telegram", "udp_p2p", false);
udp_reflector_ = reader.GetBoolean("telegram", "udp_reflector", true);
aec_enabled_ = reader.GetBoolean("telegram", "enable_aec", false);
ns_enabled_ = reader.GetBoolean("telegram", "enable_ns", false);
agc_enabled_ = reader.GetBoolean("telegram", "enable_agc", false);
proxy_enabled_ = reader.GetBoolean("telegram", "use_proxy", false);
proxy_address_ = reader.Get("telegram", "proxy_address", "");
proxy_port_ = static_cast<int32_t>(reader.GetInteger("telegram", "proxy_port", 0));
proxy_username_ = reader.Get("telegram", "proxy_username", "");
proxy_password_ = reader.Get("telegram", "proxy_password", "");
voip_proxy_enabled_ = reader.GetBoolean("telegram", "use_voip_proxy", false);
voip_proxy_address_ = reader.Get("telegram", "voip_proxy_address", "");
voip_proxy_port_ = static_cast<uint16_t>(reader.GetInteger("telegram", "voip_proxy_port", 0));
voip_proxy_username_ = reader.Get("telegram", "voip_proxy_username", "");
voip_proxy_password_ = reader.Get("telegram", "voip_proxy_password", "");
extra_wait_time_ = static_cast<unsigned int>(reader.GetInteger("other", "extra_wait_time", 30));
peer_flood_time_ = static_cast<unsigned int>(reader.GetInteger("other", "peer_flood_time", 86400));
if (api_id_ == 0 || api_hash_.empty()) {
std::cerr << "TDLib api settings must be set!\n";
return;
}
is_loaded_ = true;
}

162
tg2sip/settings.h Executable file
View File

@ -0,0 +1,162 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#ifndef TG2SIP_SETTINGS_H
#define TG2SIP_SETTINGS_H
#include <INIReader.h>
class Settings {
private:
bool is_loaded_{false};
int console_min_level_;
int file_min_level_;
int log_level_;
int pjsip_log_level_;
bool pjsip_log_sip_messages_;
int tgvoip_log_level_;
int tdlib_log_level_;
unsigned int sip_port_;
std::string id_uri_;
std::string callback_uri_;
std::string public_address_;
std::string stun_server_;
bool raw_pcm_;
unsigned int sip_thread_count_;
unsigned int sip_port_range_;
int api_id_;
std::string api_hash_;
std::string db_folder_;
std::string system_language_code_;
std::string device_model_;
std::string system_version_;
std::string application_version_;
bool udp_p2p_;
bool udp_reflector_;
bool aec_enabled_;
bool ns_enabled_;
bool agc_enabled_;
bool proxy_enabled_;
std::string proxy_address_;
std::int32_t proxy_port_;
std::string proxy_username_;
std::string proxy_password_;
bool voip_proxy_enabled_;
std::string voip_proxy_address_;
uint16_t voip_proxy_port_;
std::string voip_proxy_username_;
std::string voip_proxy_password_;
unsigned int extra_wait_time_;
unsigned int peer_flood_time_;
public:
explicit Settings(INIReader &reader);
Settings(const Settings &) = delete;
Settings &operator=(const Settings &) = delete;
bool is_loaded() const { return is_loaded_; };
int console_min_level() const { return console_min_level_; };
int file_min_level() const { return file_min_level_; };
int log_level() const { return log_level_; };
int pjsip_log_level() const { return pjsip_log_level_; };
bool pjsip_log_sip_messages() const { return pjsip_log_sip_messages_; }
int tgvoip_log_level() const { return tgvoip_log_level_; };
int tdlib_log_level() const { return tdlib_log_level_; };
unsigned int sip_port() const { return sip_port_; };
string id_uri() const { return id_uri_; };
string callback_uri() const { return callback_uri_; };
string public_address() const { return public_address_; };
string stun_server() const { return stun_server_; };
bool raw_pcm() const { return raw_pcm_; };
unsigned int sip_thread_count() const { return sip_thread_count_; };
unsigned int sip_port_range() const { return sip_port_range_; };
int api_id() const { return api_id_; };
std::string api_hash() const { return api_hash_; };
std::string db_folder() const { return db_folder_; };
std::string sys_lang_code() const { return system_language_code_; };
std::string device_model() const { return device_model_; };
std::string system_version() const { return system_version_; };
std::string app_version() const { return application_version_; };
bool udp_p2p() const { return udp_p2p_; };
bool udp_reflector() const { return udp_reflector_; };
bool aec_enabled() const { return aec_enabled_; };
bool ns_enabled() const { return ns_enabled_; };
bool agc_enabled() const { return agc_enabled_; };
bool proxy_enabled() const { return proxy_enabled_; };
std::string proxy_address() const { return proxy_address_; };
int32_t proxy_port() const { return proxy_port_; };
std::string proxy_username() const { return proxy_username_; };
std::string proxy_password() const { return proxy_password_; };
bool voip_proxy_enabled() const { return voip_proxy_enabled_; };
std::string voip_proxy_address() const { return voip_proxy_address_; };
uint16_t voip_proxy_port() const { return voip_proxy_port_; };
std::string voip_proxy_username() const { return voip_proxy_username_; };
std::string voip_proxy_password() const { return voip_proxy_password_; };
unsigned int extra_wait_time() const { return extra_wait_time_; };
unsigned int peer_flood_time() const { return peer_flood_time_; };
};
#endif //TG2SIP_SETTINGS_H

312
tg2sip/sip.cpp Executable file
View File

@ -0,0 +1,312 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#include "sip.h"
using namespace sip;
void LogWriter::write(const pj::LogEntry &entry) {
auto msg = entry.msg;
// 6 = DTRACE, 5 = TRACE, ... , 0 = FATAL
int spd_log_level = entry.level > 5 ? 0 : 5 - entry.level;
logger->log(static_cast<spdlog::level::level_enum>(spd_log_level), msg);
}
AccountConfig::AccountConfig(Settings &settings) {
idUri = settings.id_uri();
// in case SIP timer RFC4028 is used
// required when registration is used
callConfig.timerMinSESec = 120;
callConfig.timerSessExpiresSec = 1800;
}
void Account::addHandler(std::function<void(pj::OnIncomingCallParam &)> &&handler) {
onIncomingCallHandler = std::move(handler);
}
void Account::onIncomingCall(pj::OnIncomingCallParam &prm) {
if (onIncomingCallHandler) {
onIncomingCallHandler(prm);
}
}
Call::Call(pj::Account &acc, std::shared_ptr<spdlog::logger> logger, int call_id = PJSUA_INVALID_ID)
: pj::Call(acc, call_id),
logger(std::move(logger)) {}
Call::~Call() {
TRACE(logger, "~Call {}", getId());
}
void Call::addHandler(std::function<void(pj::OnCallStateParam &)> &&handler) {
onCallStateHandler = std::move(handler);
}
void Call::addHandler(std::function<void(pj::OnCallMediaStateParam &)> &&handler) {
onCallMediaStateHandler = std::move(handler);
}
pj::AudioMedia *Call::audio_media() {
pj::AudioMedia *aud_med{nullptr};
auto ci = getInfo();
for (unsigned i = 0; i < ci.media.size(); i++) {
if (ci.media[i].type == PJMEDIA_TYPE_AUDIO && getMedia(i)) {
aud_med = dynamic_cast<pj::AudioMedia *>(getMedia(i));
break;
}
}
return aud_med;
}
void Call::onCallState(pj::OnCallStateParam &prm) {
if (onCallStateHandler) {
onCallStateHandler(prm);
}
}
void Call::onCallMediaState(pj::OnCallMediaStateParam &prm) {
if (onCallMediaStateHandler) {
onCallMediaStateHandler(prm);
}
}
const string Call::localUser() {
return local_user_;
}
Client::Client(std::unique_ptr<Account> account_, std::unique_ptr<AccountConfig> account_cfg_,
OptionalQueue<events::Event> &events_, Settings &settings,
std::shared_ptr<spdlog::logger> logger_, LogWriter *sip_log_writer)
: account(std::move(account_)),
account_cfg(std::move(account_cfg_)),
events(events_),
logger(std::move(logger_)) {
// magic statics yay!
static auto ep = std::make_unique<pj::Endpoint>();
if (ep->libGetState() == PJSUA_STATE_NULL) {
init_pj_endpoint(settings, sip_log_writer);
}
account->addHandler([this](pj::OnIncomingCallParam &prm) {
auto call = std::make_shared<Call>(*account, logger, prm.callId);
auto ci = call->getInfo();
call->local_user_ = user_from_uri(ci.localUri);
calls.emplace(call->getId(), call);
set_default_handlers(call);
DEBUG(logger, "incoming SIP call #{} from {} to {} with call-id {}", ci.id, ci.remoteUri, ci.localUri,
ci.callIdString);
events.emplace(events::IncomingCall{ci.id, call->localUser()});
});
}
Client::~Client() {
TRACE(logger, "~Client");
}
void Client::init_pj_endpoint(Settings &settings, LogWriter *sip_log_writer) {
using namespace pj;
auto log_writer = sip_log_writer;
auto &ep = pj::Endpoint::instance();
// Hide logs before pjsua2 init
pj_log_set_level(0);
ep.libCreate();
EpConfig ep_cfg;
if (!settings.pjsip_log_sip_messages()) {
// don't log SIP messages
ep_cfg.logConfig.msgLogging = 0;
}
ep_cfg.logConfig.consoleLevel = 6;
// TODO: partially remove decor. Leave filename. Could require pjsip recompile.
ep_cfg.logConfig.decor = 0;
ep_cfg.logConfig.writer = log_writer;
// https://trac.pjsip.org/repos/wiki/FAQ#high-perf
// disable echo canceller
ep_cfg.medConfig.ecTailLen = 0;
// set SIP threads number
ep_cfg.medConfig.threadCnt = settings.sip_thread_count();
// 10ms ptime required to keep L16 RTP packet below MTU
ep_cfg.medConfig.audioFramePtime = 10;
ep_cfg.medConfig.ptime = 10;
// must be the same as used in media ports
ep_cfg.medConfig.clockRate = 48000;
// in case of trouble check:
// PJSUA_MAX_CALLS
// PJMEDIA_MAX_MTU
// PJ_IOQUEUE_MAX_HANDLES = calls x2
// PJSUA_MAX_PLAYERS = max calls
ep_cfg.uaConfig.maxCalls = PJSUA_MAX_CALLS;
auto stun_server = settings.stun_server();
if (!stun_server.empty()) {
ep_cfg.uaConfig.stunServer.emplace_back(stun_server);
}
ep.libInit(ep_cfg);
ep.audDevManager().setNullDev();
// pjSIP with switch board require matching of SIP audio
// and TG audio port clock rate so we MUST force
// using 48kHz codecs for all SIP calls
std::string codecId = settings.raw_pcm() ? "L16/48000/1" : "opus/48000/2";
CodecInfoVector codecVector = ep.codecEnum();
for (auto const &value : codecVector) {
ep.codecSetPriority(value->codecId, (pj_uint8_t) (value->codecId == codecId ? 255 : 0));
}
TransportConfig t_cfg;
// defaults to any open port
t_cfg.port = settings.sip_port();
t_cfg.portRange = settings.sip_port_range();
auto public_address = settings.public_address();
if (!public_address.empty()) {
t_cfg.publicAddress = public_address;
}
ep.transportCreate(PJSIP_TRANSPORT_UDP, t_cfg);
ep.libStart();
}
void Client::start() {
account->create(*account_cfg);
}
std::string Client::user_from_uri(const std::string &uri) {
std::string user{};
pj_pool_t *pj_pool = pjsua_pool_create("temp%p", 2048, 1024);
if (pj_pool != nullptr) {
std::vector<char> buffer(uri.begin(), uri.end());
buffer.push_back('\0');
pjsip_uri *pj_uri = pjsip_parse_uri(pj_pool, &buffer[0], uri.size(), PJSIP_PARSE_URI_IN_FROM_TO_HDR);
if (pj_uri != nullptr && PJSIP_URI_SCHEME_IS_SIP(pj_uri)) {
auto pj_sip_uri = static_cast<pjsip_sip_uri *>(pjsip_uri_get_uri(pj_uri));
user = std::string(pj_sip_uri->user.ptr,
static_cast<unsigned long>(pj_sip_uri->user.slen));
}
pj_pool_release(pj_pool);
}
return user;
}
pjsua_call_id Client::Dial(const std::string &uri, const pj::CallOpParam &prm) {
auto call = std::make_shared<Call>(*account, logger);
set_default_handlers(call);
// sip ID is known only after making call
call->makeCall(uri, prm);
auto sip_id = call->getId();
calls.emplace(sip_id, call);
return sip_id;
}
void Client::set_default_handlers(const std::shared_ptr<Call> &call) {
call->addHandler([this, call_wpt = std::weak_ptr<Call>(call)](pj::OnCallStateParam &prm) {
if (auto call_spt = call_wpt.lock()) {
events.emplace(events::CallStateUpdate{call_spt->getId(), call_spt->getInfo().state});
}
});
call->addHandler([this, call_wpt = std::weak_ptr<Call>(call)](pj::OnCallMediaStateParam &prm) {
if (auto call_spt = call_wpt.lock()) {
events.emplace(events::CallMediaStateUpdate{call_spt->getId(), call_spt->hasMedia()});
}
});
}
void Client::Hangup(const pjsua_call_id call_id, const pj::CallOpParam &prm) {
auto it = calls.find(call_id);
if (it != calls.end()) {
auto call = it->second;
call->hangup(prm);
calls.erase(it);
}
}
void Client::BridgeAudio(const pjsua_call_id call_id, pj::AudioMedia *input, pj::AudioMedia *output) {
auto it = calls.find(call_id);
if (it == calls.end()) {
throw std::runtime_error{"CALL_NOT_FOUND"};
}
auto sip_audio = it->second->audio_media();
if (sip_audio == nullptr) {
throw std::runtime_error{"SIP_MEDIA_NOT_READY"};
}
sip_audio->startTransmit(*input);
output->startTransmit(*sip_audio);
}
void Client::Answer(pjsua_call_id call_id, const pj::CallOpParam &prm) {
auto it = calls.find(call_id);
if (it == calls.end()) {
throw std::runtime_error{"CALL_NOT_FOUND"};
}
auto call = it->second;
call->answer(prm);
}
void Client::DialDtmf(pjsua_call_id call_id, const string &dtmf_digits) {
auto it = calls.find(call_id);
if (it == calls.end()) {
throw std::runtime_error{"CALL_NOT_FOUND"};
}
auto call = it->second;
call->dialDtmf(dtmf_digits);
}

147
tg2sip/sip.h Executable file
View File

@ -0,0 +1,147 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#ifndef TG2SIP_SIP_H
#define TG2SIP_SIP_H
#include <variant>
#include <pjsua2.hpp>
#include "logging.h"
#include "settings.h"
#include "queue.h"
#include "utils.h"
namespace sip {
namespace events {
struct IncomingCall {
pjsua_call_id id;
std::string extension;
};
struct CallStateUpdate {
pjsua_call_id id;
pjsip_inv_state state;
};
struct CallMediaStateUpdate {
pjsua_call_id id;
bool has_media;
};
typedef std::variant<IncomingCall, CallStateUpdate, CallMediaStateUpdate> Event;
}
class LogWriter : public pj::LogWriter {
public:
explicit LogWriter(std::shared_ptr<spdlog::logger> logger) : logger(std::move(logger)) {};
~LogWriter() override = default;
void write(const pj::LogEntry &entry) override;
private:
std::shared_ptr<spdlog::logger> logger;
};
struct AccountConfig : public pj::AccountConfig {
explicit AccountConfig(Settings &settings);
};
class Call : public pj::Call {
public:
Call(pj::Account &acc, std::shared_ptr<spdlog::logger> logger, int call_id);
~Call() override;
void addHandler(std::function<void(pj::OnCallStateParam &)> &&handler);
void addHandler(std::function<void(pj::OnCallMediaStateParam &)> &&handler);
const std::string localUser();
pj::AudioMedia *audio_media();
private:
friend class Client;
void onCallState(pj::OnCallStateParam &prm) override;
void onCallMediaState(pj::OnCallMediaStateParam &prm) override;
std::function<void(pj::OnCallStateParam &)> onCallStateHandler;
std::function<void(pj::OnCallMediaStateParam &)> onCallMediaStateHandler;
std::string local_user_;
std::shared_ptr<spdlog::logger> logger;
};
class Account : public pj::Account {
public:
explicit Account(std::shared_ptr<spdlog::logger> logger) : logger(std::move(logger)) {};
void addHandler(std::function<void(pj::OnIncomingCallParam &)> &&handler);
private:
void onIncomingCall(pj::OnIncomingCallParam &prm) override;
std::function<void(pj::OnIncomingCallParam &)> onIncomingCallHandler;
std::shared_ptr<spdlog::logger> logger;
};
class Client {
public:
Client(std::unique_ptr<Account> account_, std::unique_ptr<AccountConfig> account_cfg_,
OptionalQueue<events::Event> &events, Settings &settings,
std::shared_ptr<spdlog::logger> logger_, LogWriter *sip_log_writer);
Client(const Client &) = delete;
Client &operator=(const Client &) = delete;
virtual ~Client();
void start();
pjsua_call_id Dial(const std::string &uri, const pj::CallOpParam &prm);
void Answer(pjsua_call_id call_id, const pj::CallOpParam &prm);
void Hangup(pjsua_call_id call_id, const pj::CallOpParam &prm);
void DialDtmf(pjsua_call_id call_id, const string &dtmf_digits);
void BridgeAudio(pjsua_call_id call_id, pj::AudioMedia *input, pj::AudioMedia *output);
private:
std::shared_ptr<spdlog::logger> logger;
std::unique_ptr<Account> account;
std::unique_ptr<AccountConfig> account_cfg;
OptionalQueue<events::Event> &events;
std::map<int, std::shared_ptr<Call>> calls;
static void init_pj_endpoint(Settings &settings, LogWriter *sip_log_writer);
static std::string user_from_uri(const std::string &uri);
void set_default_handlers(const std::shared_ptr<Call> &call);
};
}
#endif //TG2SIP_SIP_H

202
tg2sip/tg.cpp Executable file
View File

@ -0,0 +1,202 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#include "tg.h"
#include "queue.h"
using namespace tg;
Client::Client(Settings &settings, std::shared_ptr<spdlog::logger> logger_,
OptionalQueue<Object> &events_)
: logger(std::move(logger_)), events(events_) {
client = std::make_unique<td::Client>();
init_lib_parameters(settings);
init_proxy(settings);
}
Client::~Client() {
TRACE(logger, "~Client");
is_closed = true;
if (thread_.joinable()) {
thread_.join();
}
client.release();
TRACE(logger, "~Client done");
}
void Client::init_lib_parameters(Settings &settings) {
lib_parameters = td_api::make_object<td_api::tdlibParameters>();
lib_parameters->api_id_ = settings.api_id();
lib_parameters->api_hash_ = settings.api_hash();
lib_parameters->database_directory_ = settings.db_folder();
lib_parameters->system_language_code_ = settings.sys_lang_code();
lib_parameters->device_model_ = settings.device_model();
lib_parameters->system_version_ = settings.system_version();
lib_parameters->application_version_ = settings.app_version();
lib_parameters->use_file_database_ = false;
lib_parameters->use_chat_info_database_ = true;
lib_parameters->use_message_database_ = false;
lib_parameters->use_secret_chats_ = false;
lib_parameters->enable_storage_optimizer_ = true;
}
void Client::init_proxy(Settings &settings) {
if (settings.proxy_enabled()) {
auto socks_proxy = td_api::make_object<td_api::proxySocks5>(
settings.proxy_address(),
settings.proxy_port(),
settings.proxy_username(),
settings.proxy_password()
);
proxy = td_api::move_object_as<td_api::Proxy>(socks_proxy);
} else {
auto empty_proxy = td_api::make_object<td_api::proxyEmpty>();
proxy = td_api::move_object_as<td_api::Proxy>(empty_proxy);
}
}
void Client::loop() {
TRACE(logger, "TG client thread started");
while (!is_closed) {
auto response = client->receive(WAIT_TIMEOUT);
process_response(std::move(response));
}
TRACE(logger, "TG client thread ended");
}
void Client::start() {
thread_ = std::thread(&Client::loop, this);
pthread_setname_np(thread_.native_handle(), "tg_client");
}
void Client::process_response(td::Client::Response response) {
if (!response.object) {
return;
}
TRACE(logger, "TG client got response with ID {}\n{}", response.id, to_string(response.object));
if (response.id == 0) {
return process_update(std::move(response.object));
}
auto it = handlers.find(response.id);
if (it != handlers.end()) {
it->second(std::move(response.object));
handlers.erase(it->first);
}
}
void Client::process_update(Object update) {
switch (update->get_id()) {
case td_api::updateAuthorizationState::ID: {
auto update_authorization_state = td_api::move_object_as<td_api::updateAuthorizationState>(update);
on_authorization_state_update(std::move(update_authorization_state->authorization_state_));
break;
}
case td_api::updateConnectionState::ID: {
auto update_connection_state = td_api::move_object_as<td_api::updateConnectionState>(update);
if (update_connection_state->state_->get_id() == td_api::connectionStateReady::ID) {
logger->info("TG client connected");
}
break;
}
case td_api::updateCall::ID:
case td_api::updateNewMessage::ID: {
events.emplace(std::move(update));
break;
}
default:
break;
}
}
void Client::send_query(td_api::object_ptr<td_api::Function> f, std::function<void(Object)> handler) {
auto query_id = next_query_id();
if (handler) {
handlers.emplace(query_id, std::move(handler));
}
client->send({query_id, std::move(f)});
}
std::future<Client::Object> Client::send_query_async(td_api::object_ptr<td_api::Function> f) {
if (std::this_thread::get_id() == thread_.get_id()) {
logger->critical("Call of send_query_async from TG thread will cause deadlock");
throw;
}
auto promise = std::make_shared<std::promise<Object>>();
auto future = promise->get_future();
send_query(std::move(f), [this, promise](Object object) {
try {
promise->set_value(std::move(object));
} catch (std::future_error &error) {
logger->critical("failed to set send_query_async promise value {}", error.code());
}
});
return future;
}
std::uint64_t Client::next_query_id() {
return ++current_query_id;
}
auto Client::create_authentication_query_handler() {
return [this](Object object) {
check_authentication_error(std::move(object));
};
}
void Client::check_authentication_error(Object object) {
if (object->get_id() == td_api::error::ID) {
auto error = td_api::move_object_as<td_api::error>(object);
logger->error("TG client authorization error\n{}", to_string(error));
}
}
void Client::on_authorization_state_update(td_api::object_ptr<td_api::AuthorizationState> authorization_state) {
switch (authorization_state->get_id()) {
case td_api::authorizationStateReady::ID:
is_ready_.set_value(true);
logger->info("TG client authorization ready");
break;
case td_api::authorizationStateWaitEncryptionKey::ID:
send_query(td_api::make_object<td_api::checkDatabaseEncryptionKey>(),
create_authentication_query_handler());
send_query(td_api::make_object<td_api::setProxy>(td_api::move_object_as<td_api::Proxy>(proxy)),
create_authentication_query_handler());
break;
case td_api::authorizationStateWaitTdlibParameters::ID: {
send_query(td_api::make_object<td_api::setTdlibParameters>(
std::move(lib_parameters)), create_authentication_query_handler());
break;
}
default:
is_ready_.set_value(false);
logger->error("TG client auto sign-on failed");
break;
}
}

91
tg2sip/tg.h Executable file
View File

@ -0,0 +1,91 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#ifndef TG2SIP_TG_H
#define TG2SIP_TG_H
#include <thread>
#include <functional>
#include <future>
#include <map>
#include <td/telegram/Client.h>
#include "utils.h"
#include "logging.h"
#include "settings.h"
#include "queue.h"
namespace tg {
namespace td_api = td::td_api;
class Client {
public:
using Object = td_api::object_ptr<td_api::Object>;
virtual ~Client();
explicit Client(Settings &settings, std::shared_ptr<spdlog::logger> logger_,
OptionalQueue<Object> &events_);
void start();
void send_query(td_api::object_ptr<td_api::Function> f, std::function<void(Object)> handler = nullptr);
std::future<Object> send_query_async(td_api::object_ptr<td_api::Function> f);
std::future<bool> is_ready() { return is_ready_.get_future(); };
private:
std::shared_ptr<spdlog::logger> logger;
OptionalQueue<Object> &events;
const double WAIT_TIMEOUT = 10;
std::unique_ptr<td::Client> client;
std::unique_ptr<td_api::tdlibParameters> lib_parameters;
td_api::object_ptr<td_api::Proxy> proxy;
std::thread thread_;
std::promise<bool> is_ready_;
bool is_closed{false};
std::uint64_t current_query_id{0};
std::map<std::uint64_t, std::function<void(Object)>> handlers;
void init_lib_parameters(Settings &settings);
void init_proxy(Settings &settings);
void loop();
void process_response(td::Client::Response response);
void process_update(Object update);
std::uint64_t next_query_id();
auto create_authentication_query_handler();
void check_authentication_error(Object object);
void on_authorization_state_update(td_api::object_ptr<td_api::AuthorizationState> authorization_state);
};
}
#endif //TG2SIP_TG_H

21
tg2sip/utils.cpp Normal file
View File

@ -0,0 +1,21 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#include <algorithm>
#include "utils.h"
bool is_digits(const std::string &str) { return std::all_of(str.begin(), str.end(), ::isdigit); };

30
tg2sip/utils.h Executable file
View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2017-2018 infactum (infactum@gmail.com)
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/
#ifndef TG2SIP_UTILS_H
#define TG2SIP_UTILS_H
#include <memory>
bool is_digits(const std::string &str);
template<class ToT, class FromT>
std::unique_ptr<ToT> move_unique_ptr_as(std::unique_ptr<FromT> &from) {
return std::unique_ptr<ToT>(static_cast<ToT *>(from.release()));
}
#endif //TG2SIP_UTILS_H