From a46ce7ff9ea84df9a3d8f6cd9e1f409fd9eafc73 Mon Sep 17 00:00:00 2001 From: Deon George Date: Tue, 11 Jun 2024 14:17:03 +1000 Subject: [PATCH] Posting messages to matrix --- app/Events/Echomail.php | 21 +++ app/Events/Matrix/Base.php | 8 +- app/Events/Matrix/Factory.php | 11 +- app/Events/Matrix/Message.php | 3 - app/Http/Controllers/MatrixController.php | 10 +- app/Listeners/EchomailListener.php | 43 ++++++ app/Models/Echomail.php | 3 + app/Notifications/Channels/MatrixChannel.php | 47 +++++++ app/Notifications/Echomails/MatrixMessage.php | 5 +- app/Notifications/Matrix.php | 48 +++++++ app/Notifications/Matrix/Echomail.php | 128 ++++++++++++++++++ app/Providers/AppServiceProvider.php | 21 ++- app/Providers/EventServiceProvider.php | 40 ------ config/app.php | 1 - 14 files changed, 329 insertions(+), 60 deletions(-) create mode 100644 app/Events/Echomail.php create mode 100644 app/Listeners/EchomailListener.php create mode 100644 app/Notifications/Channels/MatrixChannel.php create mode 100644 app/Notifications/Matrix.php create mode 100644 app/Notifications/Matrix/Echomail.php delete mode 100644 app/Providers/EventServiceProvider.php diff --git a/app/Events/Echomail.php b/app/Events/Echomail.php new file mode 100644 index 0000000..143a407 --- /dev/null +++ b/app/Events/Echomail.php @@ -0,0 +1,21 @@ +get(sprintf('%s/_matrix/client/v3/rooms/%s/state/m.room.canonical_alias',config('matrix.server'),$this->room_id)); + ->get(sprintf('https://%s/_matrix/client/v3/rooms/%s/state/m.room.canonical_alias',config('matrix.server'),$this->room_id)); return $room_alias->json('alias',$this->room_id); case 'topic': $subject = Http::withToken(config('matrix.as_token')) - ->get(sprintf('%s/_matrix/client/v3/rooms/%s/state/m.room.topic',config('matrix.server'),$this->room_id)); + ->get(sprintf('https://%s/_matrix/client/v3/rooms/%s/state/m.room.topic',config('matrix.server'),$this->room_id)); return $subject->json('topic','Message from Matrix'); + case 'ts': + return object_get($this->_data,'origin_server_ts'); + + case 'event_id': case 'room_id': default: return object_get($this->_data,$key); diff --git a/app/Events/Matrix/Factory.php b/app/Events/Matrix/Factory.php index e080173..de393e1 100644 --- a/app/Events/Matrix/Factory.php +++ b/app/Events/Matrix/Factory.php @@ -36,15 +36,6 @@ class Factory { public static function make(array $request): Base { - // During the life of the event, this method is called twice - once during Middleware processing, and finally by the Controller. - static $o = NULL; - static $or = NULL; - - if (! $o OR ($or != $request)) { - $or = $request; - $o = self::create(Arr::get($request,'type','unknown'),$request); - } - - return $o; + return self::create(Arr::get($request,'type','unknown'),$request); } } \ No newline at end of file diff --git a/app/Events/Matrix/Message.php b/app/Events/Matrix/Message.php index 2398b41..929561d 100644 --- a/app/Events/Matrix/Message.php +++ b/app/Events/Matrix/Message.php @@ -42,9 +42,6 @@ class Message extends Base case 'sender': return object_get($this->_data,$key); - case 'ts': - return object_get($this->_data,'origin_server_ts'); - default: return parent::__get($key); } diff --git a/app/Http/Controllers/MatrixController.php b/app/Http/Controllers/MatrixController.php index 233b3f8..77c3cfb 100644 --- a/app/Http/Controllers/MatrixController.php +++ b/app/Http/Controllers/MatrixController.php @@ -7,14 +7,22 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Log; use App\Events\Matrix\Factory as MatrixEventFactory; +use App\Events\Matrix\Message; final class MatrixController extends Controller { private const LOGKEY = 'CMC'; - public function webhook(Request $request) + public function webhook(Request $request): mixed { $event = MatrixEventFactory::make(Arr::get($request->events,0,[])); + + // Catch our messages that we've posted + if (($event instanceof Message) && preg_match('#^.*\^[0-9]+_[0-9]+/[0-9]+(\.[0-9]+)?:#',$event->sender)) { + Log::info(sprintf('%s:- Ignoring Matrix Message event, probably from us [%s]',static::LOGKEY,$event->sender)); + return response(['result'=>'OK']); + } + Log::info(sprintf('%s:- Dispatching Matrix Event [%s]',static::LOGKEY,get_class($event))); event($event); diff --git a/app/Listeners/EchomailListener.php b/app/Listeners/EchomailListener.php new file mode 100644 index 0000000..138f9ed --- /dev/null +++ b/app/Listeners/EchomailListener.php @@ -0,0 +1,43 @@ +eo->echoarea; + + // Catch our messages that we've posted, so they dont go back + if (str_ends_with($event->eo->from,':'.config('matrix.server'))) + return; + + if ($ea && collect(config('matrix.rooms'))->contains($ea->name)) { + Log::debug(sprintf('%s:- Sending echomail to matrix for [%s]',self::LOGKEY,$event->eo->msgid)); + Notification::route('matrix',collect(config('matrix.rooms'))->search($ea->name))->notify(new Echomail($event->eo->withoutRelations())); + + } else { + Log::debug(sprintf('%s:- Not sending echomail to matrix for [%s]',self::LOGKEY,$event->eo->msgid)); + } + } +} \ No newline at end of file diff --git a/app/Models/Echomail.php b/app/Models/Echomail.php index 7764d45..fecc9a1 100644 --- a/app/Models/Echomail.php +++ b/app/Models/Echomail.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Log; use App\Casts\{CollectionOrNull,CompressedStringOrNull,UTF8StringOrNull}; use App\Classes\FTN\Message; +use App\Events\Echomail as EchomailEvent; use App\Interfaces\Packet; use App\Traits\{MessageAttributes,MsgID,ParseAddresses,QueryCacheableConfig}; @@ -270,6 +271,8 @@ final class Echomail extends Model implements Packet $model->seenby()->syncWithPivotValues($exportto,['export_at'=>Carbon::now()],FALSE); } } + + event(new EchomailEvent($model->withoutRelations())); }); } diff --git a/app/Notifications/Channels/MatrixChannel.php b/app/Notifications/Channels/MatrixChannel.php new file mode 100644 index 0000000..57f3793 --- /dev/null +++ b/app/Notifications/Channels/MatrixChannel.php @@ -0,0 +1,47 @@ +routeNotificationFor('matrix',$notification)) + return; + + try { + $o = $notification->toMatrix($notifiable); + + // @todo Check this exception works as expected (putting the job back on the queue when the user doesnt exist), else it might need to go in Matrix/Echomail + } catch (\Exception $e) { + Log::info(sprintf('%s:= Exception [%s] posting to Matrix, putting job back on queue for [%s]',self::LOGKEY,$e->getMessage(),$room)); + $this->release(); + return; + } + + // @todo Be nice to get the message ID for debugging + Log::info(sprintf('%s:= Posted echomail to Matrix [%s]',self::LOGKEY,$room),['o'=>$o]); + } +} \ No newline at end of file diff --git a/app/Notifications/Echomails/MatrixMessage.php b/app/Notifications/Echomails/MatrixMessage.php index bfccf4e..7b9325c 100644 --- a/app/Notifications/Echomails/MatrixMessage.php +++ b/app/Notifications/Echomails/MatrixMessage.php @@ -38,7 +38,7 @@ class MatrixMessage extends Echomails $echoarea = $notifiable->routeNotificationFor(static::via); $o = $this->setupEchomail($echoarea); - Log::info(sprintf('%s:+ Sending Matrix Message to [%s]',self::LOGKEY,$echoarea->name)); + Log::info(sprintf('%s:+ Sending Matrix Message to [%s]',self::LOGKEY,$echoarea->name),['mo'=>$this->mo]); $our = our_address($echoarea->domain)->last(); $o->to = 'All'; @@ -47,9 +47,10 @@ class MatrixMessage extends Echomails $o->subject = $this->mo->topic; $o->fftn_id = $our->id; $o->kludges->put('CHRS:','UTF8 2'); + $o->kludges->put('EVENT:',$this->mo->event_id); // Message - $o->msg = $this->mo->message; + $o->msg = str_replace("\n","\r",$this->mo->message); $o->set_origin = sprintf('Matrix %s (%s)',$this->mo->room,$our->ftn4d); $o->save(); diff --git a/app/Notifications/Matrix.php b/app/Notifications/Matrix.php new file mode 100644 index 0000000..e083aad --- /dev/null +++ b/app/Notifications/Matrix.php @@ -0,0 +1,48 @@ +queue = 'matrix'; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return [ self::via ]; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return Echomail + * @throws \Exception + */ + abstract public function toMatrix(object $notifiable): mixed; +} \ No newline at end of file diff --git a/app/Notifications/Matrix/Echomail.php b/app/Notifications/Matrix/Echomail.php new file mode 100644 index 0000000..70a4eb9 --- /dev/null +++ b/app/Notifications/Matrix/Echomail.php @@ -0,0 +1,128 @@ +routeNotificationFor(static::via); + + Log::info(sprintf('%s:+ Sending Echo Message to Matrix [%s]',self::LOGKEY,$room)); + + $username = sprintf('%s^%d_%d/%d.%d', + $this->o->from, + $this->o->fftn->zone->zone_id, + $this->o->fftn->host_id, + $this->o->fftn->node_id, + $this->o->fftn->point_id, + ); + + $user = sprintf('@%s:%s',$username,config('matrix.server')); + + // Set topic if it is different: + $subject = Http::withToken(config('matrix.as_token')) + ->get(sprintf('https://%s/_matrix/client/v3/rooms/%s/state/m.room.topic',config('matrix.server'),$room)); + + if (($x=preg_replace('/^RE:\s*/i','',$this->o->subject)) !== $subject->json('topic','Message from Matrix')) { + $topic = Http::withToken(config('matrix.as_token')) + ->put(sprintf('https://%s/_matrix/client/v3/rooms/%s/state/m.room.topic',config('matrix.server'),$room),[ + 'topic'=>$x, + ]); + + if ($topic->status() !== 200) + Log::error(sprintf('%s:! Failed to set matrix room topic to [%s] in room [%s]',self::LOGKEY,$x,$room),['msg'=>$topic->body()]); + } + + $msg = Http::withToken(config('matrix.as_token')) + ->withQueryParameters(['user_id'=>$user]) + ->post(sprintf('https://%s/_matrix/client/v3/rooms/%s/send/m.room.message',config('matrix.server'),$room),[ + 'msgtype'=>'m.text', + 'body'=>mb_convert_encoding(str_replace("\r","\n",$this->o->msg),'UTF-8','IBM850'), + ]); + + switch ($msg->status()) { + case 200: + break; + + case 403: + Log::alert(sprintf('%s:! Got 403 with errcode [%s] reason [%s]',self::LOGKEY,$msg->json('errcode'),$msg->json('error'))); + + // @todo Test that the user doesnt exist + // If the user doesnt exist in matrix yet + if (str_starts_with($msg->json('error'),'Application service has not registered this user')) { + // Register user + $msg = Http::withToken(config('matrix.as_token')) + ->post(sprintf('https://%s/_matrix/client/v3/register',config('matrix.server')),[ + 'type'=>'m.login.application_service', + 'username'=>$username, + ]); + + if ($msg->status() !== 200) { + Log::error(sprintf('%s:! Failed to register user [%s] to matrix',self::LOGKEY,$username),['msg'=>$msg->body()]); + throw new \Exception(sprintf('Failed to invite user [%s] to matrix',$username)); + } + + // @todo Test that the user has been invited + // Invite user + $msg = Http::withToken(config('matrix.as_token')) + //->withQueryParameters(['user_id'=>$user]) + ->post(sprintf('https://%s/_matrix/client/v3/rooms/%s/invite',config('matrix.server'),$room),[ + 'user_id'=>$user, + ]); + + if ($msg->status() !== 200) { + Log::error(sprintf('%s:! Failed to invite user [%s] to matrix room [%s]',self::LOGKEY,$user,$room),['msg'=>$msg->body()]); + throw new \Exception(sprintf('Failed to invite user [%s] to matrix room [%s]',$user,$room)); + } + + // Join as user + $msg = Http::withToken(config('matrix.as_token')) + ->withQueryParameters(['user_id'=>$user]) + ->post(sprintf('https://%s/_matrix/client/v3/rooms/%s/join',config('matrix.server'),$room),[ + 'user_id'=>$user, + ]); + + if ($msg->status() !== 200) { + Log::error(sprintf('%s:! Failed to join user [%s] to matrix room [%s]',self::LOGKEY,$user,$room),['msg'=>$msg->body()]); + throw new \Exception(sprintf('Failed to join user [%s] to matrix room [%s]',$user,$room)); + } + + // retry this message + throw new \Exception('Need to create user on matrix first'); + } + + break; + + default: + Log::error(sprintf('%s:! Unknown status [%d] with errcode [%s] reason [%s] when posting message [%d] to matrix',self::LOGKEY,$msg->status(),$msg->json('errcode'),$msg->json('error'),$this->o->id),['msg'=>$msg->body()]); + } + + return $msg->body(); + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 932d10e..b96c71a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,10 +5,14 @@ namespace App\Providers; use Illuminate\Http\Request; use Illuminate\Notifications\ChannelManager; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Notification; use Illuminate\Support\ServiceProvider; -use App\Notifications\Channels\{EchomailChannel,NetmailChannel}; +use App\Events\Matrix\Message; +use App\Listeners\EchomailListener; +use App\Listeners\Matrix\MessageListener; +use App\Notifications\Channels\{EchomailChannel,MatrixChannel,NetmailChannel}; use App\Models\{Echomail,Netmail}; use App\Traits\SingleOrFail; @@ -31,6 +35,10 @@ class AppServiceProvider extends ServiceProvider $service->extend('netmail', function ($app) { return new NetmailChannel($app->make(Netmail::class)); }); + + $service->extend('matrix', function ($app) { + return new MatrixChannel($app->make(Echomail::class)); + }); }); } @@ -46,5 +54,16 @@ class AppServiceProvider extends ServiceProvider Auth::viaRequest('matrix-token',function (Request $request) { return (config('matrix.hs_token') && ($request->bearerToken() === config('matrix.hs_token'))) ? TRUE : NULL; }); + + Event::listen( + Message::class, + MessageListener::class, + ); + + // @todo This should be detected automatically? + Event::listen( + \App\Events\Echomail::class, + EchomailListener::class, + ); } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php deleted file mode 100644 index 9074bf2..0000000 --- a/app/Providers/EventServiceProvider.php +++ /dev/null @@ -1,40 +0,0 @@ - [ - SendEmailVerificationNotification::class, - ], - ]; - - /** - * Register any events for your application. - * - * @return void - */ - public function boot() - { - parent::boot(); - - Event::listen( - Message::class, - MessageListener::class, - ); - } -} diff --git a/config/app.php b/config/app.php index 78236b7..64d9edc 100644 --- a/config/app.php +++ b/config/app.php @@ -179,7 +179,6 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, // App\Providers\BroadcastServiceProvider::class, - App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\CustomBladeServiceProvider::class,