Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor FCM #72

Merged
merged 12 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
MAILGUN_API_KEY=
MAILGUN_DOMAIN=
SENDGRID_API_KEY=
FCM_SERVER_KEY=
FCM_SERVICE_ACCOUNT_JSON=
FCM_TO=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_TO=
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
MAILGUN_API_KEY: ${{ secrets.MAILGUN_API_KEY }}
MAILGUN_DOMAIN: ${{ secrets.MAILGUN_DOMAIN }}
SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
FCM_SERVER_KEY: ${{ secrets.FCM_SERVER_KEY }}
FCM_SERVER_TO: ${{ secrets.FCM_SERVER_TO }}
FCM_SERVICE_ACCOUNT_JSON: ${{ secrets.FCM_SERVICE_ACCOUNT_JSON }}
FCM_TO: ${{ secrets.FCM_TO }}
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
TWILIO_TO: ${{ secrets.TWILIO_TO }}
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ $message = new Push(
content: 'Hello World'
);

$messaging = new FCM('YOUR_SERVER_KEY');
$messaging = new FCM('YOUR_SERVICE_ACCOUNT_JSON');
$messaging->send($message);
```

Expand Down Expand Up @@ -108,7 +108,7 @@ $messaging->send($message);

### Push
- [x] [FCM](https://firebase.google.com/docs/cloud-messaging)
- [ ] [APNS](https://developer.apple.com/documentation/usernotifications)
- [x] [APNS](https://developer.apple.com/documentation/usernotifications)
- [ ] [OneSignal](https://onesignal.com/)
- [ ] [Pusher](https://pusher.com/)
- [ ] [WebPush](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
},
"require": {
"php": ">=8.0.0",
"ext-curl": "*"
"ext-curl": "*",
"ext-openssl": "*"
},
"require-dev": {
"ext-openssl": "*",
"phpunit/phpunit": "9.6.10",
"phpmailer/phpmailer": "6.8.*",
"laravel/pint": "1.13.*",
Expand Down
58 changes: 29 additions & 29 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,41 @@ version: '3.9'

services:
tests:
environment:
- MAILGUN_API_KEY
- MAILGUN_DOMAIN
- SENDGRID_API_KEY
- FCM_SERVER_KEY
- FCM_SERVER_TO
- TWILIO_ACCOUNT_SID
- TWILIO_AUTH_TOKEN
- TWILIO_TO
- TWILIO_FROM
- TELNYX_API_KEY
- TELNYX_PUBLIC_KEY
- APNS_AUTHKEY_8KVVCLA3HL
- APNS_AUTH_ID
- APNS_TEAM_ID
- APNS_BUNDLE_ID
- APNS_TO
- MSG_91_SENDER_ID
- MSG_91_AUTH_KEY
- MSG_91_TO
- MSG_91_FROM
- TEST_EMAIL
- TEST_FROM_EMAIL
- VONAGE_API_KEY
- VONAGE_API_SECRET
- VONAGE_TO
- VONAGE_FROM
- DISCORD_WEBHOOK_ID
- DISCORD_WEBHOOK_TOKEN
build:
context: .
volumes:
- ./src:/usr/local/src/src
- ./tests:/usr/local/src/tests
- ./phpunit.xml:/usr/local/src/phpunit.xml
environment:
- MAILGUN_API_KEY
- MAILGUN_DOMAIN
- SENDGRID_API_KEY
- FCM_SERVICE_ACCOUNT_JSON
- FCM_TO
- TWILIO_ACCOUNT_SID
- TWILIO_AUTH_TOKEN
- TWILIO_TO
- TWILIO_FROM
- TELNYX_API_KEY
- TELNYX_PUBLIC_KEY
- APNS_AUTHKEY_8KVVCLA3HL
- APNS_AUTH_ID
- APNS_TEAM_ID
- APNS_BUNDLE_ID
- APNS_TO
- MSG_91_SENDER_ID
- MSG_91_AUTH_KEY
- MSG_91_TO
- MSG_91_FROM
- TEST_EMAIL
- TEST_FROM_EMAIL
- VONAGE_API_KEY
- VONAGE_API_SECRET
- VONAGE_TO
- VONAGE_FROM
- DISCORD_WEBHOOK_ID
- DISCORD_WEBHOOK_TOKEN

maildev:
image: appwrite/mailcatcher:1.0.0
Expand Down
19 changes: 16 additions & 3 deletions docs/add-new-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,25 @@ public function process(Email $message): string
}
```

The base `Adapter` class includes a helper method called `request()` that can be used to send HTTP requests to the messaging provider. It accepts the following parameters, and returns the response as a string:
The base `Adapter` class includes a two helper functions called `request()` and `requestMulti()` that can be used to send HTTP requests to the messaging provider.

The `request()` function will send a single request and accepts the following parameters:

- `method` - The HTTP method to use. For example, `POST`, `GET`, `PUT`, `PATCH` or `DELETE`.
- `url` - The URL to send the request to.
- `headers` - An array of headers to send with the request.
- `body` - The body of the request. It can be either a string or an array.
- `body` - The body of the request as a string, or null if no body is required.
- `timeout` - The timeout in seconds for the request.

The `requestMulti()` function will send multiple concurrent requests via HTTP/2 multiplexing, and accepts the following parameters:

- `method` - The HTTP method to use. For example, `POST`, `GET`, `PUT`, `PATCH` or `DELETE`.
- `urls` - An array of URLs to send the requests to.
- `headers` - An array of headers to send with the requests.
- `bodies` - An array of bodies of the requests as strings, or an empty array if no body is required.
- `timeout` - The timeout in seconds for the requests.

`urls` and `bodies` must either be the same length, or one of them must contain only a single element. If `urls` contains only a single element, it will be used for all requests. If `bodies` contains only a single element, it will be used for all requests.

The default content type of the request is `x-www-form-urlencoded`, but you can change it by adding a `Content-Type` header. No encoding is applied to the body, so you need to make sure it is encoded properly before sending the request.

Expand Down Expand Up @@ -144,7 +157,7 @@ If everything goes well, raise a pull request and be ready to respond to any fee

## 4. Raise a pull request

First of all, commit the changes with the message `Added YYY Storage adapter` and push it. This will publish a new branch to your forked version of `utopia-php/messaging`. If you visit it at `github.com/YOUR_USERNAME/messaging`, you will see a new alert saying you are ready to submit a pull request. Follow the steps GitHub provides, and at the end, you will have your pull request submitted.
First of all, commit the changes with the message `Added YYY adapter` and push it. This will publish a new branch to your forked version of `utopia-php/messaging`. If you visit it at `github.com/YOUR_USERNAME/messaging`, you will see a new alert saying you are ready to submit a pull request. Follow the steps GitHub provides, and at the end, you will have your pull request submitted.

## 🤕 Stuck ?
If you need any help with the contribution, feel free to head over to [our discord channel](https://appwrite.io/discord) and we'll be happy to help you out.
168 changes: 146 additions & 22 deletions src/Utopia/Messaging/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,19 @@ abstract public function getMaxMessagesPerRequest(): int;
/**
* Send a message.
*
* @param Message $message The message to send.
* @return string The response body.
* @return array{
* deliveredTo: int,
* type: string,
* results: array<array<string, mixed>>
* } | array<string, array{
* deliveredTo: int,
* type: string,
* results: array<array<string, mixed>>
* }>
*
* @throws \Exception If the message fails.
* @throws \Exception
*/
public function send(Message $message): string
public function send(Message $message): array
{
if (! \is_a($message, $this->getMessageType())) {
throw new \Exception('Invalid message type.');
Expand All @@ -48,13 +55,19 @@ public function send(Message $message): string
}

/**
* Send an HTTP request.
* Send a single HTTP request.
*
* @param string $method The HTTP method to use.
* @param string $url The URL to send the request to.
* @param array<string> $headers An array of headers to send with the request.
* @param string|null $body The body of the request.
* @return array<string, mixed> The response body.
* @param int $timeout The timeout in seconds.
* @return array{
* url: string,
* statusCode: int,
* response: array<string, mixed>|null,
* error: string|null
* }
*
* @throws \Exception If the request fails.
*/
Expand All @@ -63,6 +76,7 @@ protected function request(
string $url,
array $headers = [],
string $body = null,
int $timeout = 30
): array {
$ch = \curl_init();

Expand All @@ -71,31 +85,141 @@ protected function request(
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}

\curl_setopt($ch, CURLOPT_URL, $url);
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
\curl_setopt($ch, CURLOPT_USERAGENT, "Appwrite {$this->getName()} Message Sender");
\curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => "Appwrite {$this->getName()} Message Sender",
CURLOPT_TIMEOUT => $timeout,
]);

$response = \curl_exec($ch);

\curl_close($ch);

if (\curl_errno($ch)) {
throw new \Exception('Error: '.\curl_error($ch));
try {
$response = \json_decode($response, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException) {
// Ignore
}

return [
'url' => $url,
abnegate marked this conversation as resolved.
Show resolved Hide resolved
'statusCode' => \curl_getinfo($ch, CURLINFO_RESPONSE_CODE),
'response' => $response,
'error' => \curl_error($ch),
];
}

/**
* Send multiple concurrent HTTP requests using HTTP/2 multiplexing.
*
* @param array<string> $urls
* @param array<string> $headers
* @param array<string> $bodies
* @return array<array{
* url: string,
* statusCode: int,
* response: array<string, mixed>|null,
* error: string|null
* }>
*
* @throws \Exception
*/
protected function requestMulti(
abnegate marked this conversation as resolved.
Show resolved Hide resolved
string $method,
array $urls,
array $headers = [],
array $bodies = [],
int $timeout = 30
): array {
if (empty($urls)) {
throw new \Exception('No URLs provided. Must provide at least one URL.');
}

$sh = \curl_share_init();
$mh = \curl_multi_init();
$ch = \curl_init();

\curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
\curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);

\curl_setopt_array($ch, [
CURLOPT_SHARE => $sh,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FORBID_REUSE => false,
CURLOPT_FRESH_CONNECT => false,
CURLOPT_TIMEOUT => $timeout,
]);

$urlCount = \count($urls);
$bodyCount = \count($bodies);

if (
$urlCount != $bodyCount &&
($urlCount == 1 && $bodyCount != 1 || $urlCount != 1 && $bodyCount == 1)
) {
throw new \Exception('URL and body counts must be equal or 1.');
}

if ($urlCount > $bodyCount) {
$bodies = \array_pad($bodies, $urlCount, $bodies[0]);
} elseif ($urlCount < $bodyCount) {
$urls = \array_pad($urls, $bodyCount, $urls[0]);
}

$jsonResponse = \json_decode($response, true);
foreach (\array_combine($urls, $bodies) as $url => $body) {
if (! empty($body)) {
$headers[] = 'Content-Length: '.\strlen($body);
}

\curl_setopt($ch, CURLOPT_URL, $url);
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
\curl_multi_add_handle($mh, \curl_copy_handle($ch));
}

$active = true;
do {
$status = \curl_multi_exec($mh, $active);

if ($active) {
\curl_multi_select($mh);
}
} while ($active && $status == CURLM_OK);

if (\json_last_error() == JSON_ERROR_NONE) {
return [
'response' => $jsonResponse,
'statusCode' => \curl_getinfo($ch, CURLINFO_HTTP_CODE),
$responses = [];

// Check each handle's result
while ($info = \curl_multi_info_read($mh)) {
$ch = $info['handle'];

$response = \curl_multi_getcontent($ch);

try {
$response = \json_decode($response, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException) {
// Ignore
}

$responses[] = [
'url' => \curl_getinfo($ch, CURLINFO_EFFECTIVE_URL),
'statusCode' => \curl_getinfo($ch, CURLINFO_RESPONSE_CODE),
'response' => $response,
'error' => \curl_error($ch),
];

\curl_multi_remove_handle($mh, $ch);
\curl_close($ch);
}

return [
'response' => $response,
'statusCode' => \curl_getinfo($ch, CURLINFO_HTTP_CODE),
];
\curl_multi_close($mh);
\curl_share_close($sh);

return $responses;
}
}
9 changes: 7 additions & 2 deletions src/Utopia/Messaging/Adapter/Chat/Discord.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ public function getMaxMessagesPerRequest(): int
return 1;
}

protected function process(DiscordMessage $message): string
/**
* @return array{deliveredTo: int, type: string, results: array<array<string, mixed>>}
*
* @throws \Exception
*/
protected function process(DiscordMessage $message): array
{
$query = [];

Expand Down Expand Up @@ -89,6 +94,6 @@ protected function process(DiscordMessage $message): string
$response->addResultForRecipient($this->webhookId, 'Unknown Error.');
}

return \json_encode($response->toArray());
return $response->toArray();
}
}
Loading
Loading