This commit is contained in:
Daniel Klabbers 2021-10-12 23:15:47 +02:00
commit 2bf4ec01d0
19 changed files with 11548 additions and 0 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4
[*.md]
indent_size = 2
trim_trailing_whitespace = false

13
.gitattributes vendored Normal file
View File

@ -0,0 +1,13 @@
.gitattributes export-ignore
.gitignore export-ignore
.gitmodules export-ignore
.travis.yml export-ignore
changelog.md export-ignore
tests export-ignore
phpunit.xml export-ignore
js/*/src export-ignore
js/*/Gulpfile.js
js/*/package.json
js/*/yarn.lock
js/*/dist/*.js -diff

17
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Build JavaScript assets
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: flarum/action-build@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
js/node_modules
js/dist
vendor/
composer.lock

47
composer.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "blomstra/search",
"description": "Replaces Flarum search with a fulltext search server.",
"keywords": [
"extension",
"flarum", "blomstra",
"search", "fulltext"
],
"support": {
"email": "helpdesk@blomstra.net",
"forum": "https://blomstra.community/t/ext-search"
},
"type": "flarum-extension",
"license": "MIT",
"authors": [
{
"name": "Daniël Klabbers",
"email": "daniel@klabbers.email",
"homepage": "http://luceos.com"
}
],
"require": {
"php": ">= 8.0",
"flarum/core": "^1.0.0"
},
"suggest": {
"meilisearch/meilisearch-php": "Required to use the MeiliSearch engine (^0.19)."
},
"extra": {
"flarum-extension": {
"title": "Search",
"category": "feature",
"icon": {
"image": "resources/assets/logo.svg",
"backgroundColor": "#E3E7F1",
"backgroundSize": "100%",
"backgroundRepeat": "no-repeat",
"backgroundPosition": "center"
}
}
},
"autoload": {
"psr-4": {
"Blomstra\\Search\\": "src/"
}
}
}

15
extend.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace Blomstra\Search;
use Flarum\Extend as Flarum;
return [
(new Flarum\ServiceProvider)->register(Provider::class),
(new Flarum\Frontend('forum'))
->js(__DIR__ . '/js/dist/forum.js'),
(new Flarum\Routes('api'))
->get('blomstra/search', 'blomstra.search', Api\Controllers\SearchController::class)
];

1
js/forum.js Normal file
View File

@ -0,0 +1 @@
export * from './src/forum';

11147
js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
js/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"private": true,
"name": "@blomstra/realtime",
"dependencies": {
"flarum-webpack-config": "^1.0.0",
"lodash-es": "^4.17.14",
"pusher-js": "^7.0.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
},
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"lint": "prettier --single-quote --trailing-comma es5 --print-width 150 --tab-width 4 --write src"
},
"devDependencies": {
"prettier": "^2.3.1"
}
}

58
js/src/forum/Source.tsx Normal file
View File

@ -0,0 +1,58 @@
import app from 'flarum/app';
import highlight from 'flarum/common/helpers/highlight';
import LinkButton from 'flarum/common/components/LinkButton';
import Link from 'flarum/common/components/Link';
import { SearchSource } from 'flarum/forum/components/Search';
import type Mithril from 'mithril';
/**
* The `DiscussionsSearchSource` finds and displays discussion search results in
* the search dropdown.
*/
export default class Source implements SearchSource {
protected results = new Map<string, unknown[]>();
search(query: string) {
query = query.toLowerCase();
this.results.set(query, []);
const params = {
filter: { q: query },
page: { limit: 3 },
include: 'mostRelevantPost',
};
return app.request(
app.forum.attribute('apiUrl') + '/blomstra/search',
params
).then((results) => this.results.set(query, results));
}
view(query: string): Array<Mithril.Vnode> {
query = query.toLowerCase();
const results = (this.results.get(query) || []).map((discussion: unknown) => {
const mostRelevantPost = discussion.mostRelevantPost();
return (
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
<Link href={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())}>
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div> : ''}
</Link>
</li>
);
}) as Array<Mithril.Vnode>;
return [
<li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>,
<li>
<LinkButton icon="fas fa-search" href={app.route('index', { q: query })}>
{app.translator.trans('core.forum.search.all_discussions_button', { query })}
</LinkButton>
</li>,
...results,
];
}
}

15
js/src/forum/index.js Normal file
View File

@ -0,0 +1,15 @@
import app from 'flarum/app';
import { extend } from 'flarum/common/extend';
import Search from 'flarum/forum/components/Search';
import Source from './Source';
app.initializers.add('blomstra-search', () => {
extend(Search.prototype, 'sourceItems', function (items) {
items.remove('users');
items.remove('discussions');
items.add('discussions', new Source());
console.log(items)
});
});

3
js/webpack.config.js Normal file
View File

@ -0,0 +1,3 @@
const config = require('flarum-webpack-config');
module.exports = config();

View File

@ -0,0 +1,15 @@
<?php
namespace Blomstra\Search\Api\Controllers;
use Flarum\Api\Controller\AbstractListController;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class SearchController extends AbstractListController
{
protected function data(ServerRequestInterface $request, Document $document)
{
// TODO: Implement data() method.
}
}

9
src/Mapping.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace Blomstra\Search;
use Illuminate\Support\Collection;
class Mapping extends Collection
{
}

34
src/Observe/Job.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace Blomstra\Search\Observe;
use Blomstra\Search\Mapping;
use Blomstra\Search\Searchables\Searchable;
use Flarum\Queue\AbstractJob;
use Illuminate\Database\Eloquent\Model;
use MeiliSearch\Client;
class Job extends AbstractJob
{
public function __construct(protected Model $model)
{}
public function handle(Client $meili, Mapping $mapping)
{
$map = $mapping->get(get_class($this->model));
/** @var Searchable $searchable */
$searchable = new $map['searchable']($this->model);
$body = array_merge([
$searchable->fulltext()
], [
$this->model->getKeyName() => $this->model->getKey()
]);
$meili->index($map['index'])->addDocuments(
[$body],
$this->model->getKeyName()
);
}
}

24
src/Observe/Observer.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace Blomstra\Search\Observe;
use Illuminate\Contracts\Queue\Queue;
class Observer
{
public function saved($model)
{
$this->queue()->push(new Job($model));
}
public function deleted($model)
{
}
protected function queue(): Queue
{
return resolve(Queue::class);
}
}

54
src/Provider.php Normal file
View File

@ -0,0 +1,54 @@
<?php
namespace Blomstra\Search;
use Blomstra\Search\Observe\Observer;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Post\CommentPost;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr;
use MeiliSearch\Client;
class Provider extends AbstractServiceProvider
{
public function register()
{
$this->container->singleton(Mapping::class, function () {
return new Mapping([
\Flarum\Discussion\Discussion::class => [
'index' => 'discussions',
'searchable' => \Blomstra\Search\Searchables\Discussion::class
],
CommentPost::class => [
'index' => 'posts',
'searchable' => null,
]
]);
});
$this->container->singleton(Client::class, function (Container $container) {
$config = $container->make('flarum.config') ?? [];
$meili = Arr::get($config, 'meili');
if (! $meili) {
return null;
}
return new Client($meili);
});
}
public function boot()
{
/** @var Mapping $searchables */
$searchables = $this->container->get(Mapping::class);
$searchables->each(function ($mapped, $model) {
forward_static_call(
[$model, 'observe'],
Observer::class
);
});
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Blomstra\Search\Searchables;
use Flarum\Discussion\Discussion as Model;
use Flarum\Tags\Tag;
class Discussion extends Searchable
{
public function __construct(
protected Model $discussion
) {}
public function filters(): ?array
{
if ($this->extensionEnabled('flarum-tags')) {
return $this->discussion->tags->map(function (Tag $tag) {
return "tag:$tag->id";
})->toArray();
}
return [];
}
public function fulltext(): ?array
{
return [
'title' => $this->discussion->title,
'content' => $this->discussion->firstPost->content
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Blomstra\Search\Searchables;
use Flarum\Extension\ExtensionManager;
abstract class Searchable
{
/**
* Returns an array of filterable attributes.
*
* @return array|null
*/
abstract public function filters(): ?array;
/**
* An array of strings with which fulltext matching can be executed.
*
* @return array|null
*/
abstract public function fulltext(): ?array;
protected function extensionEnabled(string $extension): bool
{
/** @var ExtensionManager $manager */
$manager = resolve(ExtensionManager::class);
return $manager->isEnabled($extension);
}
}