Verified Commit d1ffc619 authored by FabioWidmer's avatar FabioWidmer
Browse files

Initial commit

parents
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
* text=auto
*.md diff=markdown
*.php diff=php
/tests export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.gitlab-ci.yml export-ignore
phpunit.xml export-ignore
/.idea
/.phpunit.cache
/vendor
stages:
- test
- build
phpunit:
image: composer:latest
stage: test
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- vendor/
artifacts:
when: always
paths:
- report.xml
- coverage.xml
reports:
junit: report.xml
cobertura: coverage.xml
expire_in: 1 week
script:
- composer install
- ./vendor/phpunit/phpunit/phpunit --configuration phpunit.xml --coverage-text --colors=never --log-junit report.xml --coverage-cobertura coverage.xml
publish:
image: curlimages/curl:latest
stage: build
variables:
URL: "$CI_SERVER_PROTOCOL://$CI_SERVER_HOST:$CI_SERVER_PORT/api/v4/projects/$CI_PROJECT_ID/packages/composer?job_token=$CI_JOB_TOKEN"
script:
- version=$([[ -z "$CI_COMMIT_TAG" ]] && echo "branch=$CI_COMMIT_REF_NAME" || echo "tag=$CI_COMMIT_TAG")
- insecure=$([ "$CI_SERVER_PROTOCOL" = "http" ] && echo "--insecure" || echo "")
- response=$(curl -s -w "\n%{http_code}" $insecure --data $version $URL)
- code=$(echo "$response" | tail -n 1)
- body=$(echo "$response" | head -n 1)
- if [ $code -eq 201 ]; then
echo "Package created - Code $code - $body";
else
echo "Could not create package - Code $code - $body";
exit 1;
fi
# Changelog
## v1.0.0 - 2022-03-05
### Added
- Support Laravel v9
- Support basic and end-to-end IDs
- Support text messages
- Support automatic ID lookup via email and phone
MIT License
Copyright (c) 2022 Ablota
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# Laravel Threema Notification Channel
Laravel package for sending notifications with Threema.
## Prerequisites
1. Add the following repository to your `composer.json`:
```json
{
"repositories": [
{
"type": "package",
"package": {
"name": "threema/msgapi-sdk",
"version": "1.5.6",
"dist": {
"url": "https://gateway.threema.ch/sdk/threema-msgapi-sdk-php-1.5.6.zip",
"type": "zip"
}
}
}
]
}
```
2. Install the package with Composer:
```shell
composer require ablota/laravel-threema-notification-channel
```
3. Request a [Threema Gateway ID](https://gateway.threema.ch/en/products). Register to get a basic ID for testing immediately. If you are interested in an end-to-end ID, contact [Threema](https://gateway.threema.ch/en/contact) and they will usually provide you with an ID for testing.
## Configuration
The package includes a [configuration file](config/threema.php). However, you are not required to export this configuration file to your own
application. You can simply use the environment variables below:
```shell
// Required
THREEMA_GATEWAY_ID=
THREEMA_GATEWAY_SECRET=
// Optional
THREEMA_GATEWAY_PRIVATE_KEY=
THREEMA_MSGAPI_HOST=
THREEMA_TLS_FORCE_HTTPS=true|false
THREEMA_TLS_CIPHER=...
THREEMA_TLS_VERSION=1.0|1.1|1.2
```
The required variables are shown on the [Threema Gateway](https://gateway.threema.ch/en/id) website. The `THREEMA_GATEWAY_PRIVATE_KEY` is required in
hex if you're using the end-to-end mode.
## Formatting Notifications
If a notification supports being sent as a Threema message, you should define a `toThreema` method on the notification class. This method will receive
a `$notifiable` entity and should return an `Illuminate\Notifications\Messages\ThreemaMessage` instance. Let's take a look at a basic `toThreema`
example:
```php
use Illuminate\Notifications\Messages\ThreemaMessage;
use Illuminate\Notifications\Messages\ThreemaTextMessage;
public function toThreema(mixed $notifiable): ThreemaMessage
{
return new ThreemaTextMessage('Hello World!');
}
```
_Right now only text messages are supported. More message types are coming soon._
## Routing Notifications
To route Threema notifications to the proper Threema ID, define a `routeNotificationForThreema` method on your notifiable entity:
```php
use Threema\MsgApi\Receiver;
public function routeNotificationForThreema(mixed $notification): Receiver
{
return new Receiver($this->threema_id, Receiver::TYPE_ID);
}
```
By using `Receiver::TYPE_EMAIL` or `Receiver::TYPE_PHONE` you can make use of an automatic ID lookup.
{
"name": "ablota/laravel-threema-notification-channel",
"description": "Laravel package for sending notifications with Threema.",
"keywords": ["ablota", "laravel", "threema", "notification", "channel"],
"license": "MIT",
"require": {
"php": "^8.0",
"illuminate/notifications": "^9.0",
"illuminate/support": "^9.0",
"threema/msgapi-sdk": "^1.5"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"autoload": {
"psr-4": {
"Illuminate\\Notifications\\": "src/",
"Threema\\MsgApi\\": "vendor/threema/msgapi-sdk/src/MsgApi/"
},
"files": [
"vendor/threema/msgapi-sdk/src/Salt/Curve25519/Curve25519.php",
"vendor/threema/msgapi-sdk/src/Salt/FieldElement.php",
"vendor/threema/msgapi-sdk/src/Salt/Salt.php",
"vendor/threema/msgapi-sdk/src/Salt/SaltException.php",
"vendor/threema/msgapi-sdk/src/Salt/Poly1305/Poly1305.php",
"vendor/threema/msgapi-sdk/src/Salt/Salsa20/Salsa20.php"
]
},
"autoload-dev": {
"psr-4": {
"Illuminate\\Notifications\\Tests\\": "tests/"
}
},
"config": {
"sort-packages": true
},
"repositories": [
{
"type": "package",
"package": {
"name": "threema/msgapi-sdk",
"version": "1.5.6",
"dist": {
"url": "https://gateway.threema.ch/sdk/threema-msgapi-sdk-php-1.5.6.zip",
"type": "zip"
}
}
}
],
"extra": {
"laravel": {
"providers": [
"Illuminate\\Notifications\\ThreemaChannelServiceProvider"
],
"aliases": {
"Threema": "IIlluminate\\Notifications\\Facades\\ThreemaChannelServiceProvider"
}
}
},
"prefer-stable": true
}
This diff is collapsed.
<?php
use Threema\MsgApi\ConnectionSettings;
return [
/*
|--------------------------------------------------------------------------
| Gateway ID
|--------------------------------------------------------------------------
|
| The custom Gateway ID you requested on Threema Gateway.
|
*/
'gateway_id' => env('THREEMA_GATEWAY_ID'),
/*
|--------------------------------------------------------------------------
| Gateway Secret
|--------------------------------------------------------------------------
|
| The secret of the custom Gateway ID you requested on Threema Gateway.
|
*/
'gateway_secret' => env('THREEMA_GATEWAY_SECRET'),
/*
|--------------------------------------------------------------------------
| Gateway Private Key
|--------------------------------------------------------------------------
|
| The private key of the custom Gateway ID you requested on Threema Gateway.
| (End-to-End)
|
*/
'gateway_private_key' => env('THREEMA_GATEWAY_PRIVATE_KEY'),
/*
|--------------------------------------------------------------------------
| MsgApi Host
|--------------------------------------------------------------------------
|
| The host URL of the MsgApi server you want to send messages over.
|
*/
'msgapi_host' => env('THREEMA_MSGAPI_HOST'),
/*
|--------------------------------------------------------------------------
| TLS Options
|--------------------------------------------------------------------------
|
| When connecting to the MsgApi server you can define the following TLS options.
|
*/
'tls_options' => [
ConnectionSettings::tlsOptionForceHttps => env('THREEMA_TLS_FORCE_HTTPS'),
ConnectionSettings::tlsOptionCipher => env('THREEMA_TLS_CIPHER'),
ConnectionSettings::tlsOptionVersion => env('THREEMA_TLS_VERSION'),
],
];
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>
<?php
namespace Illuminate\Notifications\Channels;
use Illuminate\Notifications\Exceptions\ThreemaChannelException;
use Illuminate\Notifications\Messages\ThreemaTextMessage;
use Illuminate\Notifications\Notification;
use Threema\MsgApi\Commands\Results\Result;
use Threema\MsgApi\Connection;
use Threema\MsgApi\Core\Exception;
use Threema\MsgApi\Helpers\E2EHelper;
use Threema\MsgApi\Receiver;
class ThreemaChannel
{
protected Connection $connection;
protected ?string $privateKey;
/**
* @throws ThreemaChannelException
*/
public function __construct(Connection $connection, ?string $privateKey = null)
{
if ($privateKey !== null) {
if (@hex2bin($privateKey) === false) {
throw new ThreemaChannelException(
'The provided private key for sending E2E messages is invalid.'
);
}
$privateKey = hex2bin($privateKey);
}
$this->connection = $connection;
$this->privateKey = $privateKey;
}
/**
* @throws ThreemaChannelException
*/
public function send(mixed $notifiable, Notification $notification): ?Result
{
$message = $notification->toThreema($notifiable);
$connection = $message->channel ? $message->channel->connection : $this->connection;
$privateKey = $message->channel ? $message->channel->privateKey : $this->privateKey;
if (!$receiver = $notifiable->routeNotificationFor('threema', $notification)) {
throw new ThreemaChannelException('Notifiable is missing "routeNotificationForThreema" function.');
}
if ($privateKey === null && $message instanceof ThreemaTextMessage) {
$result = $connection->sendSimple($receiver, $message->text);
} else {
$e2eHelper = new E2EHelper($privateKey, $connection);
$receiver = $receiver->getParams();
if (array_key_exists(Receiver::TYPE_ID, $receiver)) {
$threemaId = $receiver[Receiver::TYPE_ID];
} else {
if (array_key_exists(Receiver::TYPE_EMAIL, $receiver)) {
$lookup = $connection->keyLookupByEmail($receiver[Receiver::TYPE_EMAIL]);
} else if (array_key_exists(Receiver::TYPE_PHONE, $receiver)) {
$lookup = $connection->keyLookupByPhoneNumber($receiver[Receiver::TYPE_PHONE]);
}
if (isset($lookup)) {
if ($lookup->isSuccess()) {
$threemaId = $lookup->getId();
} else {
return $lookup;
}
} else {
throw new ThreemaChannelException('This lookup type is not supported by Laravel Threema.');
}
}
try {
if ($message instanceof ThreemaTextMessage) {
$result = $e2eHelper->sendTextMessage($threemaId, $message->text);
} else {
throw new ThreemaChannelException('This message type is not supported by Laravel Threema.');
}
} catch (Exception $exception) {
throw new ThreemaChannelException('The underlying Threema MsgApi has thrown an exception.', 0, $exception);
}
}
return $result;
}
}
<?php
namespace Illuminate\Notifications\Exceptions;
use Exception;
class ThreemaChannelException extends Exception
{
}
<?php
namespace Illuminate\Notifications\Facades;
use Illuminate\Notifications\Channels\ThreemaChannel;
use Illuminate\Support\Facades\Facade;
class Threema extends Facade
{
protected static function getFacadeAccessor(): string
{
return ThreemaChannel::class;
}
}
<?php
namespace Illuminate\Notifications\Messages;
use Illuminate\Notifications\Channels\ThreemaChannel;
abstract class ThreemaMessage
{
public ?ThreemaChannel $channel;
public function __construct(?ThreemaChannel $channel = null)
{
$this->channel = $channel;
}
}
<?php
namespace Illuminate\Notifications\Messages;
use Illuminate\Notifications\Channels\ThreemaChannel;
class ThreemaTextMessage extends ThreemaMessage
{
public string $text;
public function __construct(string $text, ?ThreemaChannel $channel = null)
{
parent::__construct($channel);
$this->text = $text;
}
}
<?php
namespace Illuminate\Notifications;
use Illuminate\Notifications\Channels\ThreemaChannel;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider;
use Threema\MsgApi\Connection;
use Threema\MsgApi\ConnectionSettings;
use Threema\MsgApi\PublicKeyStores\File;
class ThreemaChannelServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(__DIR__ . '/../config/threema.php', 'threema');
$this->app->singleton(ThreemaChannel::class, function ($app) {
Storage::disk('local')->put('threema.pks', '');
$config = $app['config']['threema'];
$settings = new ConnectionSettings(
$config['gateway_id'],
$config['gateway_secret'],
$config['msgapi_host'],
$config['tls_options']
);
$publicKeyStore = new File(Storage::disk('local')->path('threema.pks'));
$connection = new Connection($settings, $publicKeyStore);
return new ThreemaChannel($connection, $config['gateway_private_key']);
});
Notification::resolved(function (ChannelManager $service) {
$service->extend('threema', function ($app) {
return $app->make(ThreemaChannel::class);
});
});
}
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__ . '/../config/threema.php' => $this->app->configPath('threema.php'),
], 'threema');
}
}
}
<?php
namespace Illuminate\Notifications\Tests\Unit\Channels;
use Illuminate\Notifications\Channels\ThreemaChannel;
use Illuminate\Notifications\Exceptions\ThreemaChannelException;
use Illuminate\Notifications\Messages\ThreemaMessage;
use Illuminate\Notifications\Messages\ThreemaTextMessage;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;
use PHPUnit\Framework\TestCase;
use Threema\MsgApi\Commands\Results\FetchPublicKeyResult;
use Threema\MsgApi\Commands\Results\LookupIdResult;
use Threema\MsgApi\Commands\Results\SendE2EResult;
use Threema\MsgApi\Commands\Results\SendSimpleResult;
use Threema\MsgApi\Connection;
use Threema\MsgApi\Core\Exception;
use Threema\MsgApi\Receiver;
class ThreemaChannelTest extends TestCase
{
public const PUBLIC_KEY = '51a50031b2e203368b636f58a8a3aa36373d88888a6cbe6218f4d87614c23067';
public const PRIVATE_KEY = 'ebb1ac251ae06b8ea15d9e91c346b0b90484002a44dd1331f92ab1331498a2a0';
private Connection $connectionMock;
/**
* @throws ThreemaChannelException
*/
public function testValidPrivateKey()
{
new ThreemaChannel($this->connectionMock, null);
new ThreemaChannel($this->connectionMock, self::PRIVATE_KEY);
$this->expectNotToPerformAssertions();
}
public function testInvalidPrivateKey()
{
$this->expectException(ThreemaChannelException::class);
new ThreemaChannel($this->connectionMock, '0Z');
}
/**
* @throws ThreemaChannelException
*/
public function testTextMessageSimpleId()
{
$notification = new ThreemaChannelTextMessageNotificationTest();
$notifiable = new ThreemaChannelIdNotifiableTest();
$this->connectionMock
->expects($this->once())
->method('sendSimple')
->willReturn(new SendSimpleResult(200, null));
$channel = new ThreemaChannel($this->connectionMock);
$this->assertTrue($channel->send($notifiable, $notification)->isSuccess());
}
/**
* @throws ThreemaChannelException
*/
public function testTextMessageSimpleEmail()
{
$notification = new ThreemaChannelTextMessageNotificationTest();
$notifiable = new ThreemaChannelEmailNotifiableTest();
$this->connectionMock
->expects($this->once())
->method('sendSimple')
->willReturn(new SendSimpleResult(200, null));
$channel = new ThreemaChannel($this->connectionMock);
$this->assertTrue($channel->send($notifiable, $notification)->isSuccess());
}
/**
* @throws ThreemaChannelException
*/
public function testTextMessageSimplePhone()
{