Compare commits

..

45 Commits

Author SHA1 Message Date
c04a1b8926
Updated to support mastodon-cpp 0.106.0. 2019-09-19 22:56:17 +02:00
7baaae1abc
Bugfix: Use the right mastodon-cpp object.
All checks were successful
the build was successful
2019-04-21 06:13:51 +02:00
64f78e0300
Ported to mastodon-cpp 0.105.0.
All checks were successful
the build was successful
2019-04-20 01:47:23 +02:00
a4988d72f2
Got rid of tables in manpage.
All checks were successful
the build was successful
2019-04-12 19:49:49 +02:00
087ae68136
Fixed build script: would never compile manpage
All checks were successful
the build was successful
2019-01-31 18:49:57 +01:00
217a4bb7f0
Added cmake options
All checks were successful
the build was successful
2019-01-31 18:34:37 +01:00
1ba5d4e444
Added manpage
All checks were successful
the build was successful
2019-01-31 18:03:39 +01:00
32fbdf7375
CI: migrated to Debian stretch containers.
All checks were successful
the build was successful
2019-01-27 05:09:43 +01:00
d938959598
curlpp is not in buster anymore, changed to sid
Some checks failed
the build failed
2019-01-27 05:00:20 +01:00
0f471e860f
Updated to mastodon-cpp 0.30.0
Some checks failed
the build failed
2019-01-27 04:51:03 +01:00
cade0f8a4a
Bugfix: would hang if link is of class mention
All checks were successful
the build was successful
2018-08-19 22:19:48 +02:00
3808a89a13
Set timeout to 30 seconds
All checks were successful
the build was successful
2018-08-19 21:42:08 +02:00
3a44361bf7
Added more default replacements (AMP) 2018-08-19 21:24:57 +02:00
73ddaf2776
Updated dependencies 2018-08-14 17:01:24 +02:00
b744fb0903
Enhanced CI
All checks were successful
the build was successful
2018-08-14 16:46:17 +02:00
f42bff3918
bugfix: Treated Easy::Results as Easy::Status, resulting in always-false validity.
All checks were successful
the build was successful
2018-08-14 15:46:31 +02:00
52604d536a
Made URL-detecting regular expression better
All checks were successful
the build was successful
fixes #1
2018-06-28 13:59:39 +02:00
23ad1917f9
drone: changed downloaded mastodon-cpp to 0.17.1
All checks were successful
the build was successful
2018-06-21 18:01:37 +02:00
07629de436
Updated dependencies 2018-06-21 14:10:43 +02:00
8c91751fbe
downgraded drone from buster to stretch
Some checks failed
the build failed
2018-06-21 14:08:25 +02:00
a25672c039
drone: always pull new images 2018-06-19 09:34:57 +02:00
c4b4522dc7
Rewrote send_reply(), upped mastodon-cpp dependency to version 0.17.0
Some checks failed
the build failed
2018-06-16 03:53:36 +02:00
0b218fd472
updated dependencies
All checks were successful
the build was successful
2018-06-13 05:28:42 +02:00
46f7ddc575
drone: Added email notification
All checks were successful
the build was successful
2018-06-13 05:13:55 +02:00
c602278dc5
changed download method in drone script
All checks were successful
the build was successful
2018-06-12 20:40:06 +02:00
ef7067a169
added drone config
All checks were successful
the build was successful
2018-06-12 08:34:30 +02:00
764e84cefd
fixed connectionbrokennessdetection 2018-06-11 17:15:35 +02:00
209743fd55
Check for new messages every 2 seconds (was: 5), made the connection-brokenness-detection easier to understand 2018-06-11 05:47:34 +02:00
f70b5f1a82
strip: replaced wtmc and wt_zmc with wt_? 2018-06-09 20:47:00 +02:00
cfec84d455
Changed URL of screenshot, syntax highlighting clarifications 2018-06-08 00:14:41 +02:00
f5a8f04d73
make surer config file is written on shutdown 2018-06-06 21:17:10 +02:00
42d1898ce3
git.schlomp.space -> schlomp.space 2018-06-04 21:04:38 +02:00
bf5fffca3a
Changed website URL when registering on the server 2018-06-04 20:20:01 +02:00
a6f37098f2
changed spaces to tabs 2018-06-04 14:02:33 +02:00
6463f8ef1f
readme highlighting 2018-06-04 00:59:28 +02:00
5d34c938e1
Changed URLs to point to git.schlomp.space 2018-06-04 00:49:01 +02:00
b147b42b79
Increased delay after error from 60 to 120 seconds 2018-06-02 14:43:18 +02:00
bf608f5145
removed redundancy in error message 2018-05-29 19:15:53 +02:00
e528cf6baa
updated documentation 2018-05-29 15:15:23 +02:00
46aa2b89c3
Bugfix: Made sure that the first parameter has a leading '?' 2018-05-29 15:03:04 +02:00
f3db10dc0a
Added support for configurable regular expressions 2018-05-29 14:56:39 +02:00
a1ef65b71c
set _ptr to nullptr after reset() 2018-05-28 15:52:57 +02:00
50d3ab6bba
Fixed reconnect issues, hopefully
Make the main thread sleep when the stream thread sleeps due to a connection error.
This should solve the problem of having multiple stream threads running doing too many connection attempts
2018-05-28 13:23:16 +02:00
2c955a2a18
updated ebuild 2018-05-27 16:13:04 +02:00
7938cfb30b
Rewrote config file management 2018-05-27 16:08:49 +02:00
12 changed files with 613 additions and 269 deletions

87
.drone.yml Normal file
View File

@ -0,0 +1,87 @@
pipeline:
download:
image: plugins/download
pull: true
source: https://schlomp.space/tastytea/mastodon-cpp/releases/download/0.106.0/libmastodon-cpp_0.106.0-0_amd64.deb
destination: mastodon-cpp.deb
gcc6:
image: debian:stretch-slim
pull: true
environment:
- LANG=C.utf8
- CXX=g++-6
- CXXFLAGS=-pipe -O2
commands:
- echo "APT::Default-Release \"stretch\";" >> /etc/apt/apt.conf.d/00default_release
- echo "deb http://deb.debian.org/debian sid main" >> /etc/apt/sources.list.d/sid.list
- apt-get update -q
- apt-get install -qy build-essential cmake pkg-config
- apt-get install -qy libjsoncpp-dev libcurl4-openssl-dev libxdg-basedir-dev asciidoc
- apt-get install -qy -t sid libcurlpp-dev
- dpkg -i mastodon-cpp.deb
- rm -rf build && mkdir -p build && cd build
- cmake ..
- make VERBOSE=1
- make install DESTDIR=install
gcc7:
image: debian:stretch-slim
pull: true
environment:
- LANG=C.utf8
- CXX=g++-7
- CXXFLAGS=-pipe -O2
commands:
- echo "APT::Default-Release \"stretch\";" >> /etc/apt/apt.conf.d/00default_release
- echo "deb http://deb.debian.org/debian sid main" >> /etc/apt/sources.list.d/sid.list
- echo "deb http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu xenial main" >> /etc/apt/sources.list.d/ubuntu-toolchain-r.list
- apt-get update -q
- apt-get install -qy gnupg
- gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 0x60c317803a41ba51845e371a1e9377a2ba9ef27f
- gpg --armor --export 0x60c317803a41ba51845e371a1e9377a2ba9ef27f | apt-key add -
- apt-get update -q
- apt-get install -qy build-essential cmake pkg-config
- apt-get install -qy -t xenial g++-7
- apt-get install -qy libjsoncpp-dev libcurl4-openssl-dev libxdg-basedir-dev asciidoc
- apt-get install -qy -t sid libcurlpp-dev
- dpkg -i mastodon-cpp.deb
- rm -rf build && mkdir -p build && cd build
- cmake ..
- make VERBOSE=1
- make install DESTDIR=install
gcc8:
image: debian:stretch-slim
pull: true
environment:
- LANG=C.utf8
- CXX=g++-8
- CXXFLAGS=-pipe -O2
commands:
- echo "APT::Default-Release \"stretch\";" >> /etc/apt/apt.conf.d/00default_release
- echo "deb http://deb.debian.org/debian sid main" >> /etc/apt/sources.list.d/sid.list
- echo "deb http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu xenial main" >> /etc/apt/sources.list.d/ubuntu-toolchain-r.list
- apt-get update -q
- apt-get install -qy gnupg
- gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 0x60c317803a41ba51845e371a1e9377a2ba9ef27f
- gpg --armor --export 0x60c317803a41ba51845e371a1e9377a2ba9ef27f | apt-key add -
- apt-get update -q
- apt-get install -qy build-essential cmake pkg-config
- apt-get install -qy -t xenial g++-8
- apt-get install -qy libjsoncpp-dev libcurl4-openssl-dev libxdg-basedir-dev asciidoc
- apt-get install -qy -t sid libcurlpp-dev
- dpkg -i mastodon-cpp.deb
- rm -rf build && mkdir -p build && cd build
- cmake ..
- make VERBOSE=1
- make install DESTDIR=install
notify:
image: drillster/drone-email
pull: true
host: cryptoparty-celle.de
secrets: [ email_username, email_password ]
from: drone@tzend.de
when:
status: [ changed, failure ]

View File

@ -1,46 +1,58 @@
cmake_minimum_required (VERSION 3.7)
project (expandurl-mastodon
VERSION 0.7.4
LANGUAGES CXX
)
VERSION 0.9.14
LANGUAGES CXX
)
include(GNUInstallDirs)
find_package(CURL REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(CURLPP REQUIRED curlpp)
pkg_check_modules(JSONCPP REQUIRED jsoncpp)
pkg_check_modules(LIBXDG_BASEDIR REQUIRED libxdg-basedir)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall")
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
# uint_fast16_t can be bigger than 16 bit, but that doesn't matter because
# everything but the last 16 bit is padded with zeroes.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format")
endif()
set(CMAKE_CXX_FLAGS_DEBUG
"${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wextra -Wpedantic -ftrapv \
-fsanitize=undefined -g -Og -fno-omit-frame-pointer")
include_directories(${PROJECT_SOURCE_DIR}/src)
include_directories(${PROJECT_BINARY_DIR})
include_directories(${CURL_INCLUDE_DIRS})
include_directories(${CURLPP_INCLUDE_DIRS})
include_directories(${JSONCPP_INCLUDE_DIRS})
include_directories(${LIBXDG_BASEDIR_INCLUDE_DIRS})
link_directories(${CURL_LIBRARY_DIRS})
link_directories(${CURLPP_LIBRARY_DIRS})
link_directories(${JSONCPP_LIBRARY_DIRS})
link_directories(${LIBXDG_BASEDIR_LIBRARY_DIRS})
# Write version in header
configure_file (
"${PROJECT_SOURCE_DIR}/src/version.hpp.in"
"${PROJECT_BINARY_DIR}/version.hpp"
)
"${PROJECT_SOURCE_DIR}/src/version.hpp.in"
"${PROJECT_BINARY_DIR}/version.hpp"
)
file(GLOB sources src/*.cpp)
add_executable(expandurl-mastodon ${sources})
target_link_libraries(expandurl-mastodon
${CURLPP_LIBRARIES} ${JSONCPP_LIBRARIES}
mastodon-cpp pthread)
${CURLPP_LIBRARIES} ${JSONCPP_LIBRARIES} ${LIBXDG_BASEDIR_LIBRARIES}
mastodon-cpp pthread stdc++fs)
install(TARGETS expandurl-mastodon DESTINATION ${CMAKE_INSTALL_BINDIR})
set(WITH_MAN "YES" CACHE STRING "WITH_MAN defaults to \"YES\"")
if (WITH_MAN)
add_custom_command(OUTPUT "${PROJECT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.1"
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
DEPENDS "${CMAKE_SOURCE_DIR}/${CMAKE_PROJECT_NAME}.1.adoc"
COMMAND ${CMAKE_SOURCE_DIR}/build_manpage.sh
ARGS ${PROJECT_VERSION})
add_custom_target(run ALL
DEPENDS "${PROJECT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.1")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_PROJECT_NAME}.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
endif()

View File

@ -3,7 +3,7 @@
If you want the bot to expand an URL, reply to the post with the URL in it and
mention the bot account (`@expandurl@botsin.space` for example).
![Example screenshot](https://user-images.githubusercontent.com/3681516/39963736-908e3eea-5663-11e8-9a9c-55ca74279235.jpg)
![Example screenshot](https://doc.schlomp.space/expandurl-mastodon/expandurl_screenshot.jpg)
This bot uses the same visibility as you, but posts unlisted instead of public.
It retains the sensitive flag and spoiler warnings.
@ -14,67 +14,59 @@ to rewrite [AMP](https://en.wikipedia.org/wiki/Accelerated_Mobile_Pages) URLs to
point at the real webpages.
Please report any bugs via the
[issue tracker on GitHub](https://github.com/tastytea/expandurl-mastodon/issues)
[issue tracker on schlomp.space](https://schlomp.space/tastytea/expandurl-mastodon/issues)
or to [@tastytea@soc.ialis.me](https://soc.ialis.me/@tastytea).
# Install
## Dependencies
* Tested OS: Linux
* C++ compiler (tested: gcc 6.4)
* [cmake](https://cmake.org/) (tested: 3.9.6)
* [curlpp](http://www.curlpp.org/) (tested: 0.8.1)
* [mastodon-cpp](https://github.com/tastytea/mastodon-cpp) (at least: 0.15.1)
* [jsoncpp](https://github.com/open-source-parsers/jsoncpp) (tested: 1.8.4)
* Tested OS: Linux
* C++ compiler (tested: gcc 6/7/8)
* [cmake](https://cmake.org/) (tested: 3.9 / 3.11)
* [curlpp](http://www.curlpp.org/) (tested: 0.8)
* [mastodon-cpp](https://schlomp.space/tastytea/mastodon-cpp) (at least: 0.106)
* [jsoncpp](https://github.com/open-source-parsers/jsoncpp) (tested: 1.8 / 1.7)
* [libxdg-basedir](http://repo.or.cz/w/libxdg-basedir.git) (tested: 1.2)
* Optional:
* Manpage: [asciidoc](http://asciidoc.org/) (tested: 8.6)
## Get sourcecode
### Latest release
https://github.com/tastytea/expandurl-mastodon/releases/latest
https://schlomp.space/tastytea/expandurl-mastodon/releases
### Development version
git clone https://github.com/tastytea/expandurl-mastodon.git
```SH
git clone https://schlomp.space/tastytea/expandurl-mastodon.git
```
## Compile
mkdir build
cd build/
cmake ..
make
```SH
mkdir build
cd build/
cmake ..
make
```
cmake options:
* `-DCMAKE_BUILD_TYPE=Debug` for a debug build
* `-DWITH_MAN=NO` to not compile the manpage
Install with `make install`.
# Usage
**The config file has changed from cfg to JSON in 0.4.0.**
Start expandurl-mastodon without parameters.
If no config file is found, you will be asked to provide your account address
and an access token is generated. The config file can be found in
`${HOME}/.config/expandurl-mastodon.json` and looks like this:
{
"account": "expandurl@example.social",
"access_token": "abc123",
"proxy":
{
"url": "socks5h://[::1]:1080/",
"user": "user23",
"password": "supersecure"
}
}
If you want to use a proxy, you have to edit the configuration file manually.
After the configuration file is generated, you can start expandurl-mastodon as
daemon.
Have a look at the [manpage](https://schlomp.space/tastytea/expandurl-mastodon/src/branch/master/expandurl-mastodon.1.adoc).
# Copyright
Copyright © 2018 tastytea <tastytea@tastytea.de>.
License GPLv3: GNU GPL version 3 <https://www.gnu.org/licenses/gpl-3.0.html>.
This program comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
```PLAIN
Copyright © 2018, 2019 tastytea <tastytea@tastytea.de>.
License GPLv3: GNU GPL version 3 <https://www.gnu.org/licenses/gpl-3.0.html>.
This program comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
```

13
build_manpage.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
name="expandurl-mastodon"
if [ -n "${1}" ]; then
dir="$(dirname ${0})"
version="${1}"
cp -vf "${dir}/${name}.1.adoc" .
sed -Ei "s/(Revision: +)[0-9]+\.[0-9]\.[0-9]/\1${version}/" "${name}.1.adoc"
a2x --doctype manpage --format manpage --no-xmllint "${name}.1.adoc"
else
echo "usage: ${0} VERSION" >&2
fi

75
expandurl-mastodon.1.adoc Normal file
View File

@ -0,0 +1,75 @@
= expandurl-mastodon(1)
:Author: tastytea
:Email: tastytea@tastytea.de
:Date: 2019-04-12
:Revision: 0.0.0
:man source: expandurl-mastodon
:man version: {revision}
:man manual: General Commands Manual
== NAME
expandurl-mastodon - Mastodon bot that expands shortened URLs.
== SYNOPSIS
*expandurl-mastodon*
== DESCRIPTION
If you want the bot to expand an URL, reply to the post with the URL in it and
mention the bot account.
This bot uses the same visibility as you, but posts unlisted instead of public.
It retains the sensitive flag and spoiler warnings.
Some tracking parameters, like those beginning with
https://en.wikipedia.org/wiki/UTM_parameters[utm_] are stripped. It also tries
to rewrite https://en.wikipedia.org/wiki/Accelerated_Mobile_Pages[AMP] URLs to
point at the real webpages.
== CONFIGURATION
If no config file is found, you will be asked to provide your account address
and an access token is generated. The config file can be found in
`${XDG_CONFIG_HOME}/expandurl-mastodon.json` and looks like this:
[source,json]
----
{
"account": "expandurl@example.social",
"access_token": "abc123",
"proxy":
{
"url": "socks5h://[::1]:1080/",
"user": "user23",
"password": "supersecure"
},
"replace" :
{
"//amp\\." : "//",
"[\\?&]__twitter_impression=[^&]+" : "",
"[\\?&]utm_[^&]+" : "",
"[\\?&]wt_zmc=[^&]+" : "",
"[\\?&]wtmc=[^&]+" : ""
}
}
----
If you want to use a proxy or define your own replacements, you have to edit the
configuration file manually. After the configuration file is generated, you can
start expandurl-mastodon as daemon.
== FILES
- *Configuration file*: `${XDG_CONFIG_HOME}/expandurl-mastodon.json`
`${XDG_CONFIG_HOME}` is usually `~/.config`.
== REPORTING BUGS
Bugtracker: https://schlomp.space/tastytea/expandurl-mastodon/issues
E-mail: tastytea@tastytea.de
Fediverse: https://soc.ialis.me/@tastytea

View File

@ -6,14 +6,15 @@ EAPI=6
inherit git-r3 cmake-utils
DESCRIPTION="Mastodon bot that expands shortened URLs."
HOMEPAGE="https://github.com/tastytea/expandurl-mastodon"
EGIT_REPO_URI="https://github.com/tastytea/expandurl-mastodon.git"
HOMEPAGE="https://git.schlomp.space/tastytea/expandurl-mastodon"
EGIT_REPO_URI="https://git.schlomp.space/tastytea/expandurl-mastodon.git"
LICENSE="GPL-3"
SLOT="0"
KEYWORDS=""
IUSE=""
RDEPEND=">=dev-cpp/mastodon-cpp-9999
>=dev-cpp/curlpp-0.8.1"
>=dev-cpp/curlpp-0.8.1
>=dev-libs/libxdg-basedir-1.2.0-r1"
DEPEND=">=dev-util/cmake-3.9.6
${RDEPEND}"

88
src/configjson.cpp Normal file
View File

@ -0,0 +1,88 @@
/* This file is part of expandurl-mastodon.
* Copyright © 2018, 2019 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <fstream>
#include <experimental/filesystem>
#include <basedir.h>
#include <sstream>
#include "configjson.hpp"
using std::string;
namespace fs = std::experimental::filesystem;
ConfigJSON::ConfigJSON(const string &filename, const string &subdir)
: _json()
{
xdgHandle xdg;
xdgInitHandle(&xdg);
_filepath = xdgConfigHome(&xdg);
xdgWipeHandle(&xdg);
if (!subdir.empty())
{
_filepath += '/' + subdir;
if (!fs::exists(_filepath))
{
fs::create_directory(_filepath);
}
}
_filepath += '/' + filename;
}
bool ConfigJSON::read()
{
std::ifstream file(_filepath);
if (file.is_open())
{
std::stringstream config;
config << file.rdbuf();
file.close();
config >> _json;
return true;
}
else
{
return false;
}
}
bool ConfigJSON::write()
{
std::ofstream file(_filepath);
if (file.is_open())
{
const string config = _json.toStyledString();
file.write(config.c_str(), config.length());
file.close();
return true;
}
else
{
return false;
}
}
Json::Value &ConfigJSON::get_json()
{
return _json;
}
const string ConfigJSON::get_filepath() const
{
return _filepath;
}

82
src/configjson.hpp Normal file
View File

@ -0,0 +1,82 @@
/* This file is part of expandurl-mastodon.
* Copyright © 2018, 2019 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef CONFIGJSON_HPP
#define CONFIGJSON_HPP
#include <string>
#include <jsoncpp/json/json.h>
using std::string;
class ConfigJSON
{
public:
/*!
* @brief Checks if subdir is present, creates it if necessary
*
* Example:
* @code
* ConfigJSON config("test.json", "subdirectory");
* @endcode
*
* @param filename The name of the file, including extension
* @param subdir The subdir (optional)
*/
explicit ConfigJSON(const string &filename, const string &subdir = "");
/*!
* @brief Read the file
*
* @return `true` on success
*/
bool read();
/*!
* @brief Write the file
*
* @return `true` on success
*/
bool write();
/*!
* @brief Returns a reference to the config as Json::Value
*
* Example:
* @code
* Json::Value &json = config.get_json();
* @endcode
*/
Json::Value &get_json();
/*!
* @brief Gets the complete filepath
*/
const string get_filepath() const;
private:
/*!
* Holds the contents of the JSON file
*/
Json::Value _json;
/*!
* Complete filepath
*/
string _filepath;
};
#endif // CONFIGJSON_HPP

View File

@ -1,5 +1,5 @@
/* This file is part of expandurl-mastodon.
* Copyright © 2018 tastytea <tastytea@tastytea.de>
* Copyright © 2018, 2019 tastytea <tastytea@tastytea.de>
*
* 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
@ -25,14 +25,17 @@
#include <mastodon-cpp/mastodon-cpp.hpp>
#include <mastodon-cpp/easy/all.hpp>
#include <jsoncpp/json/json.h>
#include "configjson.hpp"
using namespace Mastodon;
using std::string;
using Mastodon::API;
using Mastodon::Easy;
extern ConfigJSON configfile;
void signal_handler(int signum);
/*!
* @brief Extract URLs from HTML
*
@ -60,6 +63,15 @@ const string expand(const string &url);
*/
const string strip(const string &url);
/*!
* @brief Initialize replacements for URLs
*
* If no replacements are found in the config file, a default list is
* inserted.
*
*/
void init_replacements();
class Listener
{
@ -70,38 +82,37 @@ public:
/*!
* @brief Starts listening on Mastodon
*/
const void start();
void start();
/*!
* @brief Stops listening on Mastodon
*/
const void stop();
void stop();
const std::vector<Easy::Notification> get_new_messages();
const std::vector<Easy::Notification> catchup();
Easy::Status get_status(const std::uint_fast64_t &id);
const bool send_reply(const Easy::Status &to_status, const string &message);
const std::uint_fast64_t get_parent_id(const Easy::Notification &notif);
Easy::Status get_status(const string &id);
bool send_reply(const Easy::Status &to_status, const string &message);
const string get_parent_id(const Easy::Notification &notif);
const bool stillrunning() const;
bool stillrunning() const;
private:
string _instance;
string _access_token;
std::unique_ptr<Easy> _masto;
std::unique_ptr<Easy::API> _masto;
string _stream;
std::unique_ptr<API::http> _ptr;
std::thread _thread;
bool _running;
Json::Value _config;
const string _configfilepath;
string _proxy;
string _proxy_user;
string _proxy_password;
Json::Value &_config;
const bool read_config();
const bool write_config();
const bool register_app();
const void set_proxy(Easy &masto);
void read_config();
bool write_config();
bool register_app();
void set_proxy(Easy::API &masto);
};
#endif // EXPANDURL_MASTODON_HPP

View File

@ -1,5 +1,5 @@
/* This file is part of expandurl-mastodon.
* Copyright © 2018 tastytea <tastytea@tastytea.de>
* Copyright © 2018, 2019 tastytea <tastytea@tastytea.de>
*
* 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
@ -18,15 +18,19 @@
#include <chrono>
#include <csignal>
#include <regex>
#include <numeric>
#include <syslog.h>
#include <unistd.h> // getuid()
#include <curlpp/cURLpp.hpp>
#include "configjson.hpp"
#include "expandurl-mastodon.hpp"
using namespace Mastodon;
using std::string;
using Mastodon::Easy;
bool running = true;
ConfigJSON configfile("expandurl-mastodon.json");
void signal_handler(int signum)
{
@ -48,10 +52,18 @@ void signal_handler(int signum)
}
}
int main(int argc, char *argv[])
int main()
{
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
if (!configfile.read())
{
syslog(LOG_WARNING, "Could not open %s.",
configfile.get_filepath().c_str());
}
init_replacements();
curlpp::initialize();
openlog("expandurl-mastodon", LOG_CONS | LOG_NDELAY | LOG_PID, LOG_LOCAL1);
syslog(LOG_NOTICE, "Program started by user %d", getuid());
@ -62,13 +74,12 @@ int main(int argc, char *argv[])
while (running)
{
std::this_thread::sleep_for(std::chrono::seconds(5));
std::this_thread::sleep_for(std::chrono::seconds(2));
if (!listener.stillrunning())
{
listener.stop();
syslog(LOG_DEBUG, "Reestablishing connection...");
listener.start();
syslog(LOG_NOTICE, "Reestablished connection.");
new_messages = listener.catchup();
}
@ -81,25 +92,26 @@ int main(int argc, char *argv[])
for (Easy::Notification &notif : new_messages)
{
syslog(LOG_DEBUG, "new message");
const std::uint_fast64_t id = listener.get_parent_id(notif);
syslog(LOG_DEBUG, "in_reply_to_id: %lu", id);
const string id = listener.get_parent_id(notif);
syslog(LOG_DEBUG, "in_reply_to_id: %s", id.c_str());
Easy::Status status;
if (id > 0)
if (!id.empty())
{
status = listener.get_status(id);
if (status.valid())
{
string message = "";
for (const string &url : get_urls(status.content()))
{
message += url + " \n";
}
const std::vector<string> vec = get_urls(status.content());
const string message =
std::accumulate(vec.begin(), vec.end(), string(),
[](const string &s1, const string s2)
{ return s1 + s2 + " \n"; });
if (!message.empty())
{
if (!listener.send_reply(notif.status(), message))
{
syslog(LOG_ERR, "could not send reply to %lu", id);
syslog(LOG_ERR, "could not send reply to %s",
id.c_str());
}
}
else

View File

@ -1,5 +1,5 @@
/* This file is part of expandurl-mastodon.
* Copyright © 2018 tastytea <tastytea@tastytea.de>
* Copyright © 2018, 2019 tastytea <tastytea@tastytea.de>
*
* 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
@ -20,11 +20,13 @@
#include <mutex>
#include <sstream>
#include <syslog.h>
#include <chrono>
#include "version.hpp"
#include "expandurl-mastodon.hpp"
using std::cout;
using std::string;
using std::uint8_t;
Listener::Listener()
: _instance("")
@ -32,30 +34,22 @@ Listener::Listener()
, _stream("")
, _ptr(nullptr)
, _running(false)
, _configfilepath(static_cast<const string>(getenv("HOME")) +
"/.config/expandurl-mastodon.json")
, _proxy("")
, _proxy_user("")
, _proxy_password("")
, _config(configfile.get_json())
{
if (read_config())
read_config();
if (_config["access_token"].isNull())
{
_masto = std::make_unique<Easy>(_instance, _access_token);
_masto->set_useragent(static_cast<const string>("expandurl-mastodon/") +
global::version);
set_proxy(*_masto);
}
else
{
syslog(LOG_WARNING, "Could not open %s.", _configfilepath.c_str());
syslog(LOG_INFO, "Attempting to register application and write config file.");
if (register_app())
{
syslog(LOG_INFO, "Registration successful.");
if (!write_config())
if (!configfile.write())
{
syslog(LOG_ERR, "Could not write %s.", _configfilepath.c_str());
syslog(LOG_ERR, "Could not write %s.",
configfile.get_filepath().c_str());
std::exit(1);
}
}
@ -65,92 +59,49 @@ Listener::Listener()
std::exit(2);
}
}
_masto = std::make_unique<Easy::API>(_instance, _access_token);
_masto->set_useragent(static_cast<const string>("expandurl-mastodon/") +
global::version);
set_proxy(*_masto);
}
Listener::~Listener()
{
if (!write_config())
{
syslog(LOG_ERR, "Could not write %s.", _configfilepath.c_str());
}
}
const bool Listener::read_config()
void Listener::read_config()
{
std::ifstream file(_configfilepath);
if (file.is_open())
{
std::stringstream json;
json << file.rdbuf();
file.close();
json >> _config;
_instance = _config["account"].asString();
_instance = _instance.substr(_instance.find('@') + 1);
_access_token = _config["access_token"].asString();
_proxy = _config["proxy"]["url"].asString();
_proxy_user = _config["proxy"]["user"].asString();
_proxy_password = _config["proxy"]["password"].asString();
return true;
}
return false;
_instance = _config["account"].asString();
_instance = _instance.substr(_instance.find('@') + 1);
_access_token = _config["access_token"].asString();
_proxy = _config["proxy"]["url"].asString();
_proxy_user = _config["proxy"]["user"].asString();
_proxy_password = _config["proxy"]["password"].asString();
}
const bool Listener::write_config()
void Listener::start()
{
std::ofstream outfile(_configfilepath);
if (outfile.is_open())
{
outfile.write(_config.toStyledString().c_str(),
_config.toStyledString().length());
outfile.close();
_running = true;
_masto->get_stream(API::v1::streaming_user, _ptr, _stream);
return true;
}
return false;
syslog(LOG_NOTICE, "Connecting to %s ...", _instance.c_str());
}
const void Listener::start()
void Listener::stop()
{
static std::uint_fast16_t ret = 0;
_thread = std::thread([=]
if (!configfile.write())
{
_running = true;
Easy masto(_instance, _access_token);
masto.set_useragent(static_cast<const string>("expandurl-mastodon/") +
global::version);
set_proxy(masto);
ret = masto.get_stream(Mastodon::API::v1::streaming_user, _stream, _ptr);
syslog(LOG_DEBUG, "Connection lost.");
if (ret != 0 && ret != 14) // 14 means canceled by user
{
syslog(LOG_ERR, "Connection terminated: Error %u", ret);
syslog(LOG_INFO, "Waiting for 60 seconds");
std::this_thread::sleep_for(std::chrono::seconds(60));
}
_running = false;
});
while (_ptr == nullptr)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
syslog(LOG_ERR, "Could not write %s.",
configfile.get_filepath().c_str());
}
if (ret == 0)
{
syslog(LOG_NOTICE, "Connected to %s", _instance.c_str());
}
}
const void Listener::stop()
{
if (_ptr)
{
_ptr->cancel_stream();
_thread.join();
_ptr.reset();
_ptr = nullptr;
_stream = "";
}
else
@ -161,35 +112,45 @@ const void Listener::stop()
const std::vector<Easy::Notification> Listener::get_new_messages()
{
using namespace std::chrono;
std::vector<Easy::Notification> v;
static std::uint_fast8_t count_empty = 0;
static system_clock::time_point lastping = system_clock::now();
std::lock_guard<std::mutex> lock(_ptr->get_mutex());
if (!_stream.empty())
{
const string buffer = _stream;
_stream.clear();
for (const Easy::stream_event &event : Easy::parse_stream(buffer))
for (const Easy::stream_event_type &event : Easy::parse_stream(_stream))
{
if (event.first == Easy::event_type::Notification)
if (event.type == Easy::event_type::Notification)
{
Easy::Notification notif(event.second);
Easy::Notification notif(event.data);
if (notif.type() == Easy::notification_type::Mention)
{
v.push_back(notif);
}
}
else if (event.type == Easy::event_type::Error)
{
constexpr uint8_t delay_after_error = 120;
syslog(LOG_DEBUG, "Connection lost.");
const Json::Value err;
syslog(LOG_ERR, "Connection terminated: Error %u",
err["error_code"].asUInt());
syslog(LOG_INFO, "Waiting for %u seconds", delay_after_error);
std::this_thread::sleep_for(std::chrono::seconds(delay_after_error));
_running = false;
}
}
count_empty = 0;
_stream.clear();
lastping = system_clock::now();
}
else
{
// If we got an empty message 5 times, set state to not running
++count_empty;
if (count_empty > 5)
// If the last keep-alive packet was received 25 seconds or more ago
if (duration_cast<seconds>(system_clock::now() - lastping).count() >= 25)
{
count_empty = 0;
lastping = system_clock::now();
syslog(LOG_NOTICE, "Detected broken connection.");
_running = false;
}
@ -205,139 +166,118 @@ const std::vector<Easy::Notification> Listener::catchup()
if (last_id != "")
{
syslog(LOG_DEBUG, "Catching up...");
API::parametermap parameter =
parameters parameter =
{
{ "since_id", { last_id } },
{ "exclude_types", { "follow", "favourite", "reblog" } }
};
string answer;
std::uint_fast16_t ret;
return_call ret;;
ret = _masto->get(API::v1::notifications, parameter, answer);
ret = _masto->get(API::v1::notifications, parameter);
if (ret == 0)
if (ret)
{
for (const string str : Easy::json_array_to_vector(answer))
for (const string str : Easy::json_array_to_vector(ret.answer))
{
v.push_back(Easy::Notification(str));
}
}
else
{
syslog(LOG_ERR, "Could not catch up: Error %u", ret.error_code);
}
}
return v;
}
Mastodon::Easy::Status Listener::get_status(const std::uint_fast64_t &id)
Mastodon::Easy::Status Listener::get_status(const string &id)
{
std::uint_fast16_t ret;
string answer;
return_call ret;
ret = _masto->get(API::v1::statuses_id, {{ "id", { std::to_string(id) }}},
answer);
if (ret == 0)
ret = _masto->get(API::v1::statuses_id, {{ "id", { id }}});
if (ret)
{
return Easy::Status(answer);
return Easy::Status(ret.answer);
}
else
{
syslog(LOG_ERR, "Error %u in %s.", ret, __FUNCTION__);
syslog(LOG_ERR, "Error %u in %s.", ret.error_code, __FUNCTION__);
return Easy::Status();
}
}
const bool Listener::send_reply(const Easy::Status &to_status,
const string &message)
bool Listener::send_reply(const Easy::Status &to_status,
const string &message)
{
std::uint_fast16_t ret;
string answer;
const string id = std::to_string(to_status.id());
string strvisibility;
Easy::return_entity<Easy::Status> ret;
switch (to_status.visibility())
Easy::Status new_status;
if (to_status.visibility() == Easy::visibility_type::Public)
{
case Easy::visibility_type::Private:
strvisibility = "private";
break;
case Easy::visibility_type::Direct:
strvisibility = "direct";
break;
default:
strvisibility = "unlisted";
break;
new_status.visibility(Easy::visibility_type::Unlisted);
}
Easy::parametermap parameters =
else
{
{ "in_reply_to_id", { id } },
{ "visibility", { strvisibility } },
{ "status", { '@' + to_status.account().acct() + ' ' + message } }
};
if (to_status.sensitive())
{
parameters.insert({ "sensitive", { "true" } });
new_status.visibility(to_status.visibility());
}
new_status.in_reply_to_id(to_status.id());
new_status.content('@' + to_status.account().acct() + ' ' + message);
new_status.sensitive(to_status.sensitive());
new_status.spoiler_text(to_status.spoiler_text());
if (!to_status.spoiler_text().empty())
{
parameters.insert({ "spoiler_text", { to_status.spoiler_text() } });
}
ret = _masto->send_post(new_status);
ret = _masto->post(API::v1::statuses, parameters, answer);
if (ret == 0)
if (ret)
{
syslog(LOG_DEBUG, "Sent reply");
return true;
}
else
{
syslog(LOG_ERR, "Error %u in %s.", ret, __FUNCTION__);
syslog(LOG_ERR, "Error %u in %s.", ret.error_code, __FUNCTION__);
return false;
}
}
const std::uint_fast64_t Listener::get_parent_id(const Easy::Notification &notif)
const string Listener::get_parent_id(const Easy::Notification &notif)
{
string answer;
std::uint_fast16_t ret;
return_call ret;
// Retry up to 2 times
for (std::uint_fast8_t retries = 1; retries <= 2; ++retries)
{
// Fetch full status
ret = _masto->get(API::v1::search, {{ "q", { notif.status().url() }}},
answer);
if (ret > 0 || !Easy::Status(answer).valid())
ret = _masto->get(API::v1::search, {{ "q", { notif.status().url() }}});
if (!ret)
{
syslog(LOG_ERR, "Error %u: Could not fetch status (in %s).",
ret, __FUNCTION__);
ret.error_code, __FUNCTION__);
return 0;
}
ret = _masto->get(API::v1::statuses_id,
{{ "id", { std::to_string(notif.status().id()) }}},
answer);
{{ "id", { notif.status().id() }}});
if (ret > 0)
if (!ret)
{
syslog(LOG_ERR, "Error %u: Could not get status (in %s).",
ret, __FUNCTION__);
ret.error_code, __FUNCTION__);
return 0;
}
else
{
_config["last_id"] = std::to_string(notif.id());
const Easy::Status s(answer);
_config["last_id"] = notif.id();
const Easy::Status s(ret.answer);
// If parent is found, return ID; else retry
if (s.in_reply_to_id() != 0)
if (!s.in_reply_to_id().empty())
{
return s.in_reply_to_id();
}
else
{
syslog(LOG_WARNING, "Could not get ID of replied-to parent post");
syslog(LOG_WARNING, "Could not get ID of replied-to post");
std::this_thread::sleep_for(std::chrono::seconds(2));
}
}
@ -346,32 +286,32 @@ const std::uint_fast64_t Listener::get_parent_id(const Easy::Notification &notif
return 0;
}
const bool Listener::stillrunning() const
bool Listener::stillrunning() const
{
return _running;
}
const bool Listener::register_app()
bool Listener::register_app()
{
cout << "Account (username@instance): ";
std::cin >> _instance;
_config["account"] = _instance;
_instance = _instance.substr(_instance.find('@') + 1);
_masto = std::make_unique<Easy>(_instance, "");
_masto = std::make_unique<Easy::API>(_instance, "");
_masto->set_useragent(static_cast<const string>("expandurl-mastodon/") +
global::version);
std::uint_fast16_t ret;
return_call ret;
string client_id, client_secret, url;
ret = _masto->register_app1("expandurl-mastodon",
"urn:ietf:wg:oauth:2.0:oob",
"read write",
"https://github.com/tastytea/expandurl-mastodon",
"https://schlomp.space/tastytea/expandurl-mastodon",
client_id,
client_secret,
url);
if (ret == 0)
if (ret)
{
string code;
cout << "Visit " << url << " to authorize this application.\n";
@ -382,25 +322,25 @@ const bool Listener::register_app()
"urn:ietf:wg:oauth:2.0:oob",
code,
_access_token);
if (ret == 0)
if (ret)
{
_config["access_token"] = _access_token;
return true;
}
else
{
syslog(LOG_ERR, "register_app2(): %u", ret);
syslog(LOG_ERR, "register_app2(): %u", ret.error_code);
}
}
else
{
syslog(LOG_ERR, "register_app1(): %u", ret);
syslog(LOG_ERR, "register_app1(): %u", ret.error_code);
}
return false;
}
const void Listener::set_proxy(Mastodon::Easy &masto)
void Listener::set_proxy(Easy::API &masto)
{
if (!_proxy.empty())
{

View File

@ -1,5 +1,5 @@
/* This file is part of expandurl-mastodon.
* Copyright © 2018 tastytea <tastytea@tastytea.de>
* Copyright © 2018, 2019 tastytea <tastytea@tastytea.de>
*
* 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
@ -31,15 +31,19 @@ namespace curlopts = curlpp::options;
const std::vector<string> get_urls(const string &html)
{
const std::regex re_url("href=\"([^\"]+)\" rel");
const std::regex re_url("href=\\\\?\"([^\"\\\\]+)\\\\?\"([^>]+)");
std::smatch match;
string buffer = html;
std::vector<string> v;
while (std::regex_search(buffer, match, re_url))
{
string url = Easy::unescape_html(match[1].str());
v.push_back(strip(expand(url)));
// Add URL to vector if it is not a mention.#
if (match[2].str().find("mention") == std::string::npos)
{
string url = unescape_html(match[1].str());
v.push_back(strip(expand(url)));
}
buffer = match.suffix().str();
}
@ -61,6 +65,7 @@ const string expand(const string &url)
"Connection: close",
});
request.setOpt<curlopts::FollowLocation>(true);
request.setOpt(curlopts::Timeout(30));
try
{
@ -69,6 +74,7 @@ const string expand(const string &url)
catch (const std::exception &e)
{
syslog(LOG_ERR, "%s", e.what());
// TODO: Do something when: "Couldn't resolve host …"
syslog(LOG_NOTICE, "The previous error is ignored.");
}
@ -77,23 +83,48 @@ const string expand(const string &url)
const string strip(const string &url)
{
using replace_pair = std::pair<const std::regex, const std::string>;
using namespace std::regex_constants;
Json::Value &config = configfile.get_json();
string newurl = url;
const std::array<const replace_pair, 5> replace_array =
{{
{ std::regex("[\\?&]utm_[^&]+", icase), "" }, // Google
{ std::regex("[\\?&]wtmc=[^&]+", icase), "" }, // Twitter?
{ std::regex("[\\?&]__twitter_impression=[^&]+", icase), "" }, // Twitter?
{ std::regex("[\\?&]wt_zmc=[^&]+", icase), "" }, // Twitter?
{ std::regex("//amp\\.", icase), "//" } // AMP
}};
for (const replace_pair &pair : replace_array)
for (auto it = config["replace"].begin(); it != config["replace"].end();
++it)
{
newurl = std::regex_replace(newurl, pair.first, pair.second);
newurl = std::regex_replace(newurl,
std::regex(it.name(), icase),
(*it).asString());
}
// If '&' is found in the new URL, but no '?'
if (newurl.find('&') != std::string::npos &&
newurl.find('?') == std::string::npos)
{
size_t pos = newurl.find('&');
newurl.replace(pos, 1, "?");
}
return newurl;
}
void init_replacements()
{
using replace_pair = std::pair<const std::string, const std::string>;
Json::Value &config = configfile.get_json();
if (config["replace"].isNull())
{
const std::array<const replace_pair, 6> replace_array =
{{
{ "[\\?&]utm_[^&]+", "" }, // Google
{ "[\\?&]wt_?[^&]+", "" }, // Twitter?
{ "[\\?&]__twitter_impression=[^&]+", "" }, // Twitter?
{ "//amp\\.", "//" }, // AMP
{ "/amp/", "" }, // AMP
{ "[\\?&]service=amp", "" } // AMP
}};
for (const replace_pair &pair : replace_array)
{
config["replace"][pair.first] = pair.second;
}
}
}