mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-02-07 00:26:09 +01:00
Draupnir news system (#965)
Some checks failed
Docker Hub - Develop / docker-latest (push) Has been cancelled
Tests / Build & Lint (push) Has been cancelled
Tests / Unit tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Application Service Integration tests (push) Has been cancelled
Some checks failed
Docker Hub - Develop / docker-latest (push) Has been cancelled
Tests / Build & Lint (push) Has been cancelled
Tests / Unit tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Application Service Integration tests (push) Has been cancelled
* Add infrastructure for testing Draupnir news. https://github.com/the-draupnir-project/planning/issues/56. * Update protections for new PermalinkSchema. https://github.com/the-draupnir-project/planning/issues/57 * Add a way to announce Draupnir longhouse assemblies. This is kind of stupid though we should have just made a generic news system that deals with actual events pulled from the static blog and just sent into the room... https://github.com/the-draupnir-project/planning/issues/56. * Update news to just use a blob in the repository. https://github.com/the-draupnir-project/planning/issues/56. * Simplify seen news mechanism. https://github.com/the-draupnir-project/planning/issues/56. * Cut some dependencies out of DraupnirNews for unit testing. https://github.com/the-draupnir-project/planning/issues/58 * Rename the longhouse assembly thing to be a generic news reader. It was already changed to be generic we just forogt the name. https://github.com/the-draupnir-project/planning/issues/58. * Improve code quality of DraupnirNews. No way is this being tested without being a lot neater. The problem is that any test was going to be too coupled to implementation due to the shared responsibilities of the old class. https://github.com/the-draupnir-project/planning/issues/58. * Add DraupnirNews unit test. https://github.com/the-draupnir-project/planning/issues/58. * Allow filesystem news to show when remote news fails to fetch. Discovery from https://github.com/the-draupnir-project/planning/issues/58. * Add a comment about how news gets cleaned up.
This commit is contained in:
@@ -139,6 +139,8 @@ protections:
|
||||
roomStateBackingStore:
|
||||
enabled: false
|
||||
|
||||
draupnirNewsURL: http://127.0.0.1:8081/draupnir_news.json
|
||||
|
||||
# Options for monitoring the health of the bot
|
||||
health:
|
||||
# healthz options. These options are best for use in container environments
|
||||
|
||||
@@ -17,6 +17,8 @@ up:
|
||||
# Launch the reverse proxy, listening for connections *only* on the local host.
|
||||
- docker run --rm --network host --name mjolnir-test-reverse-proxy -p
|
||||
127.0.0.1:8081:80 -v $MX_TEST_CWD/test/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
-v
|
||||
$MX_TEST_CWD/test/draupnir_news.json:/var/www/test/draupnir_news.json:ro
|
||||
-d nginx
|
||||
- corepack yarn install
|
||||
- corepack yarn ts-node src/appservice/cli.ts -r -u
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"@sentry/node": "^7.17.2",
|
||||
"@sinclair/typebox": "0.34.13",
|
||||
"@the-draupnir-project/interface-manager": "4.2.5",
|
||||
"@the-draupnir-project/matrix-basic-types": "1.4.0",
|
||||
"@the-draupnir-project/matrix-basic-types": "1.4.1",
|
||||
"@the-draupnir-project/mps-interface-adaptor": "0.5.1",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
@@ -63,7 +63,7 @@
|
||||
"jsdom": "^24.0.0",
|
||||
"matrix-appservice-bridge": "^10.3.1",
|
||||
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.1-element.6",
|
||||
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@3.13.0",
|
||||
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@4.0.0",
|
||||
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@3.12.0",
|
||||
"pg": "^8.8.0",
|
||||
"yaml": "^2.3.2"
|
||||
|
||||
@@ -169,6 +169,9 @@ export interface IConfig {
|
||||
// This can not be used with Pantalaimon.
|
||||
experimentalRustCrypto: boolean;
|
||||
|
||||
// Where to fetch news from
|
||||
draupnirNewsURL: string;
|
||||
|
||||
configMeta:
|
||||
| {
|
||||
/**
|
||||
@@ -262,6 +265,8 @@ const defaultConfig: IConfig = {
|
||||
enabled: true,
|
||||
},
|
||||
experimentalRustCrypto: false,
|
||||
draupnirNewsURL:
|
||||
"https://raw.githubusercontent.com/the-draupnir-project/Draupnir/refs/heads/main/src/protections/DraupnirNews/news.json",
|
||||
configMeta: undefined,
|
||||
};
|
||||
|
||||
|
||||
295
src/protections/DraupnirNews/DraupnirNews.tsx
Normal file
295
src/protections/DraupnirNews/DraupnirNews.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { readFileSync } from "fs";
|
||||
import { isOk, Ok, Result } from "@gnuxie/typescript-result";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import {
|
||||
AbstractProtection,
|
||||
ActionException,
|
||||
ActionExceptionKind,
|
||||
ConstantPeriodBatch,
|
||||
describeProtection,
|
||||
EDStatic,
|
||||
isError,
|
||||
Logger,
|
||||
MessageContent,
|
||||
ProtectedRoomsSet,
|
||||
ProtectionDescription,
|
||||
StandardTimedGate,
|
||||
Value,
|
||||
} from "matrix-protection-suite";
|
||||
import { Draupnir } from "../../Draupnir";
|
||||
import { DraupnirProtection } from "../Protection";
|
||||
import path from "path";
|
||||
|
||||
const log = new Logger("DraupnirNews");
|
||||
|
||||
// TODO:
|
||||
// We should probably allow tagging these e.g. to assist making an automated system
|
||||
// for adding release news.
|
||||
export type DraupnirNewsItem = EDStatic<typeof DraupnirNewsItem>;
|
||||
export const DraupnirNewsItem = Type.Object({
|
||||
news_id: Type.String({
|
||||
description: "An identifier that can be persisted for an item of news.",
|
||||
}),
|
||||
matrix_event_content: Type.Union([MessageContent], {
|
||||
description: "Matrix event content for the news item that can be sent",
|
||||
}),
|
||||
});
|
||||
|
||||
export type DraupnirNewsBlob = EDStatic<typeof DraupnirNewsBlob>;
|
||||
export const DraupnirNewsBlob = Type.Object({
|
||||
news: Type.Array(DraupnirNewsItem),
|
||||
});
|
||||
|
||||
export const DraupnirNewsHelper = Object.freeze({
|
||||
mergeSources(...blobs: DraupnirNewsBlob[]): DraupnirNewsItem[] {
|
||||
return [
|
||||
...new Map(
|
||||
blobs
|
||||
.reduce<DraupnirNewsItem[]>((acc, blob) => [...acc, ...blob.news], [])
|
||||
.map((item) => [item.news_id, item])
|
||||
).values(),
|
||||
];
|
||||
},
|
||||
removeSeenNews(news: DraupnirNewsItem[], seenNewsIDs: Set<string>) {
|
||||
return news.filter((item) => !seenNewsIDs.has(item.news_id));
|
||||
},
|
||||
removeUnseenNews(news: DraupnirNewsItem[], seenNewsIDs: Set<string>) {
|
||||
return news.filter((item) => seenNewsIDs.has(item.news_id));
|
||||
},
|
||||
});
|
||||
|
||||
export type StoreSeenNews = (
|
||||
seenNews: DraupnirNewsItem[]
|
||||
) => Promise<Result<void>>;
|
||||
export type FetchRemoteNews = () => Promise<Result<DraupnirNewsBlob>>;
|
||||
export type NotifyNewsItem = (item: DraupnirNewsItem) => Promise<Result<void>>;
|
||||
|
||||
/**
|
||||
* This class manages requesting news from upstream, notifying, and storing
|
||||
* the newly seen news. Once seen news is updated, the instance should be
|
||||
* disposed.
|
||||
*/
|
||||
export class DraupnirNewsLifecycle {
|
||||
public constructor(
|
||||
private readonly seenNewsIDs: Set<string>,
|
||||
private readonly localNews: DraupnirNewsBlob,
|
||||
private readonly storeNews: StoreSeenNews,
|
||||
private readonly fetchRemoteNews: FetchRemoteNews,
|
||||
private readonly notifyNewsItem: NotifyNewsItem
|
||||
) {
|
||||
// nothing to do.
|
||||
}
|
||||
|
||||
public async checkForNews(): Promise<void> {
|
||||
const remoteNews = await this.fetchRemoteNews();
|
||||
if (isError(remoteNews)) {
|
||||
log.error("Unable to fetch news blob", remoteNews.error);
|
||||
// fall through, we still want to be able to show filesystem news.
|
||||
}
|
||||
const allNews = DraupnirNewsHelper.mergeSources(
|
||||
this.localNews,
|
||||
isOk(remoteNews) ? remoteNews.ok : { news: [] }
|
||||
);
|
||||
const unseenNews = DraupnirNewsHelper.removeSeenNews(
|
||||
allNews,
|
||||
this.seenNewsIDs
|
||||
);
|
||||
const notifiedNews = DraupnirNewsHelper.removeUnseenNews(
|
||||
allNews,
|
||||
this.seenNewsIDs
|
||||
);
|
||||
for (const item of unseenNews) {
|
||||
const sendResult = await this.notifyNewsItem(item);
|
||||
if (isError(sendResult)) {
|
||||
log.error("Unable to notify of news item");
|
||||
} else {
|
||||
notifiedNews.push(item);
|
||||
}
|
||||
}
|
||||
const updateResult = await this.storeNews(notifiedNews);
|
||||
if (isError(updateResult)) {
|
||||
log.error("Unable to update stored news", updateResult.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FSNews = (() => {
|
||||
const content = JSON.parse(
|
||||
readFileSync(path.join(__dirname, "./news.json"), "utf8")
|
||||
);
|
||||
return Value.Decode(DraupnirNewsBlob, content).expect(
|
||||
"File system news should match the schema"
|
||||
);
|
||||
})();
|
||||
|
||||
async function fetchNews(newsURL: string): Promise<Result<DraupnirNewsBlob>> {
|
||||
return await fetch(newsURL, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(
|
||||
(json) => Value.Decode(DraupnirNewsBlob, json),
|
||||
(error) =>
|
||||
ActionException.Result("unable to fetch news", {
|
||||
exception: error,
|
||||
exceptionKind: ActionExceptionKind.Unknown,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class schedules when to request news from the upstream repository.
|
||||
*
|
||||
* Lifecycle:
|
||||
* - unregisterListeners MUST be called when the parent protection is disposed.
|
||||
*/
|
||||
export class DraupnirNewsReader {
|
||||
private readonly newsGate = new StandardTimedGate(
|
||||
this.requestNews.bind(this),
|
||||
this.requestIntervalMS
|
||||
);
|
||||
private requestLoop: ConstantPeriodBatch;
|
||||
public constructor(
|
||||
private readonly lifecycle: DraupnirNewsLifecycle,
|
||||
private readonly requestIntervalMS: number
|
||||
) {
|
||||
this.newsGate.enqueueOpen();
|
||||
this.requestLoop = this.createRequestLoop();
|
||||
}
|
||||
|
||||
private createRequestLoop(): ConstantPeriodBatch {
|
||||
return new ConstantPeriodBatch(() => {
|
||||
this.newsGate.enqueueOpen();
|
||||
this.requestLoop = this.createRequestLoop();
|
||||
}, this.requestIntervalMS);
|
||||
}
|
||||
|
||||
private async requestNews(): Promise<void> {
|
||||
await this.lifecycle.checkForNews();
|
||||
}
|
||||
|
||||
public unregisterListeners(): void {
|
||||
this.newsGate.destroy();
|
||||
this.requestLoop.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// Seen news gets cleaned up by storing the merged file system and remote
|
||||
// news items which have been notified.
|
||||
export const DraupnirNewsProtectionSettings = Type.Object(
|
||||
{
|
||||
seenNews: Type.Array(Type.String(), {
|
||||
default: [],
|
||||
uniqueItems: true,
|
||||
description: "Any news items that have been seen by the protection.",
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: "DraupnirNewsProtectionSettings",
|
||||
}
|
||||
);
|
||||
|
||||
export type DraupnirNewsProtectionCapabilities = Record<string, never>;
|
||||
export type DraupnirNewsProtectionSettings = EDStatic<
|
||||
typeof DraupnirNewsProtectionSettings
|
||||
>;
|
||||
|
||||
export type DraupnirNewsDescription = ProtectionDescription<
|
||||
Draupnir,
|
||||
typeof DraupnirNewsProtectionSettings,
|
||||
DraupnirNewsProtectionCapabilities
|
||||
>;
|
||||
|
||||
export class DraupnirNews
|
||||
extends AbstractProtection<DraupnirNewsDescription>
|
||||
implements DraupnirProtection<DraupnirNewsDescription>
|
||||
{
|
||||
private readonly newsReader = new DraupnirNewsReader(
|
||||
new DraupnirNewsLifecycle(
|
||||
new Set(this.settings.seenNews),
|
||||
FSNews,
|
||||
this.updateNews.bind(this),
|
||||
() => fetchNews(this.draupnir.config.draupnirNewsURL),
|
||||
(item) =>
|
||||
this.draupnir.clientPlatform
|
||||
.toRoomMessageSender()
|
||||
.sendMessage(
|
||||
this.draupnir.managementRoomID,
|
||||
item.matrix_event_content
|
||||
) as Promise<Result<void>>
|
||||
),
|
||||
4.32e7 // 12 hours
|
||||
);
|
||||
public constructor(
|
||||
description: DraupnirNewsDescription,
|
||||
capabilities: DraupnirNewsProtectionCapabilities,
|
||||
protectedRoomsSet: ProtectedRoomsSet,
|
||||
private readonly settings: DraupnirNewsProtectionSettings,
|
||||
private readonly draupnir: Draupnir
|
||||
) {
|
||||
super(description, capabilities, protectedRoomsSet, {});
|
||||
}
|
||||
|
||||
private async updateNews(allNews: DraupnirNewsItem[]): Promise<Result<void>> {
|
||||
const newSettings = this.description.protectionSettings.toMirror().setValue(
|
||||
this.settings,
|
||||
"seenNews",
|
||||
allNews.map((item) => item.news_id)
|
||||
);
|
||||
if (isError(newSettings)) {
|
||||
return newSettings.elaborate("Unable to set protection settings");
|
||||
}
|
||||
const result =
|
||||
await this.protectedRoomsSet.protections.changeProtectionSettings(
|
||||
this.description as unknown as ProtectionDescription,
|
||||
this.protectedRoomsSet,
|
||||
this.draupnir,
|
||||
newSettings.ok
|
||||
);
|
||||
if (isError(result)) {
|
||||
return result.elaborate("Unable to change protection settings");
|
||||
}
|
||||
return Ok(undefined);
|
||||
}
|
||||
|
||||
handleProtectionDisable(): void {
|
||||
this.newsReader.unregisterListeners();
|
||||
}
|
||||
}
|
||||
|
||||
describeProtection<
|
||||
DraupnirNewsProtectionCapabilities,
|
||||
Draupnir,
|
||||
typeof DraupnirNewsProtectionSettings
|
||||
>({
|
||||
name: DraupnirNews.name,
|
||||
description: "Provides news about the Draupnir project.",
|
||||
capabilityInterfaces: {},
|
||||
defaultCapabilities: {},
|
||||
configSchema: DraupnirNewsProtectionSettings,
|
||||
async factory(
|
||||
description,
|
||||
protectedRoomsSet,
|
||||
draupnir,
|
||||
capabilities,
|
||||
settings
|
||||
) {
|
||||
return Ok(
|
||||
new DraupnirNews(
|
||||
description,
|
||||
capabilities,
|
||||
protectedRoomsSet,
|
||||
settings,
|
||||
draupnir
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
3
src/protections/DraupnirNews/news.json
Normal file
3
src/protections/DraupnirNews/news.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"news": []
|
||||
}
|
||||
3
src/protections/DraupnirNews/news.json.license
Normal file
3
src/protections/DraupnirNews/news.json.license
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
@@ -14,6 +14,7 @@ import "../capabilities/capabilityIndex";
|
||||
// keep alphabetical please.
|
||||
import "./BanPropagation";
|
||||
import "./BasicFlooding";
|
||||
import "./DraupnirNews/DraupnirNews";
|
||||
import "./FirstMessageIsImage";
|
||||
import "./HomeserverUserPolicyApplication/HomeserverUserPolicyProtection";
|
||||
import "./InvalidEventProtection";
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Task,
|
||||
Value,
|
||||
isError,
|
||||
PermalinkSchema,
|
||||
RoomIDPermalinkSchema,
|
||||
} from "matrix-protection-suite";
|
||||
import {
|
||||
renderActionResultToEvent,
|
||||
@@ -40,7 +40,7 @@ const PROTECT_ROOMS_ON_INVITE_PROMPT_LISTENER =
|
||||
|
||||
// would be nice to be able to use presentation types here idk.
|
||||
const ProtectRoomsOnInvitePromptContext = Type.Object({
|
||||
invited_room: PermalinkSchema,
|
||||
invited_room: RoomIDPermalinkSchema,
|
||||
});
|
||||
// this rule is stupid.
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
MJOLNIR_SHORTCODE_EVENT_TYPE,
|
||||
MembershipEvent,
|
||||
Ok,
|
||||
PermalinkSchema,
|
||||
ProtectedRoomsSet,
|
||||
RoomEvent,
|
||||
RoomIDPermalinkSchema,
|
||||
RoomStateRevision,
|
||||
Task,
|
||||
Value,
|
||||
@@ -43,7 +43,7 @@ const WATCH_LISTS_ON_INVITE_PROMPT_LISTENER =
|
||||
|
||||
// would be nice to be able to use presentation types here idk.
|
||||
const WatchRoomsOnInvitePromptContext = Type.Object({
|
||||
invited_room: PermalinkSchema,
|
||||
invited_room: RoomIDPermalinkSchema,
|
||||
});
|
||||
// this rule is stupid.
|
||||
|
||||
|
||||
11
test/draupnir_news.json
Normal file
11
test/draupnir_news.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"news": [
|
||||
{
|
||||
"news_id": "59e0dd6e-87da-4459-98ae-627c0f2a7d8b",
|
||||
"matrix_event_content": {
|
||||
"body": "Announcing the Draupnir Longhouse Assembly! https://matrix.to/#/!DtwZFWORUIApKsOVWi:matrix.org/%24GdBN1XqoOnAfc5tJgxhoXNoAdW2YUbS1Mtsb8LbzIJ4?via=matrix.org&via=feline.support&via=asgard.chat",
|
||||
"msgtype": "m.notice"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
3
test/draupnir_news.json.license
Normal file
3
test/draupnir_news.json.license
Normal file
@@ -0,0 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
@@ -37,5 +37,10 @@ http {
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:9999$request_uri;
|
||||
}
|
||||
# draupnir news blob
|
||||
location /draupnir_news.json {
|
||||
root /var/www/test;
|
||||
default_type application/json;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
101
test/unit/protections/DraupnirNewsTest.ts
Normal file
101
test/unit/protections/DraupnirNewsTest.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: AFL-3.0
|
||||
|
||||
import { Ok, ResultError } from "@gnuxie/typescript-result";
|
||||
import {
|
||||
DraupnirNewsBlob,
|
||||
DraupnirNewsLifecycle,
|
||||
} from "../../../src/protections/DraupnirNews/DraupnirNews";
|
||||
import expect from "expect";
|
||||
|
||||
describe("DraupnirNewsTest", function () {
|
||||
it("Filesystem news items get sent if the protection hasn't seen them before", async function () {
|
||||
const fileSystemNews = {
|
||||
news: [
|
||||
{
|
||||
news_id: "1",
|
||||
matrix_event_content: {
|
||||
body: "Announcing release v3.0.0!! wohoo",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
},
|
||||
{
|
||||
news_id: "2",
|
||||
matrix_event_content: {
|
||||
body: "Draupnir needs your support!",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies DraupnirNewsBlob;
|
||||
const remoteNews = {
|
||||
news: [
|
||||
{
|
||||
news_id: "3",
|
||||
matrix_event_content: {
|
||||
body: "Announcing draupnir news",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies DraupnirNewsBlob;
|
||||
const seenNews = new Set<string>();
|
||||
const notifiedNews: string[] = [];
|
||||
const newsLifecycle = new DraupnirNewsLifecycle(
|
||||
seenNews,
|
||||
fileSystemNews,
|
||||
async (allNews) => {
|
||||
allNews.forEach((item) => seenNews.add(item.news_id));
|
||||
return Ok(undefined);
|
||||
},
|
||||
async () => Ok(remoteNews),
|
||||
async (item) => {
|
||||
notifiedNews.push(item.news_id);
|
||||
return Ok(undefined);
|
||||
}
|
||||
);
|
||||
expect(seenNews.size).toBe(0);
|
||||
expect(notifiedNews.length).toBe(0);
|
||||
await newsLifecycle.checkForNews();
|
||||
expect(seenNews.size).toBe(3);
|
||||
expect(notifiedNews.length).toBe(3);
|
||||
await newsLifecycle.checkForNews();
|
||||
expect(notifiedNews.length).toBe(3);
|
||||
});
|
||||
it("Still works if remote news is inaccessible", async function () {
|
||||
const fileSystemNews = {
|
||||
news: [
|
||||
{
|
||||
news_id: "1",
|
||||
matrix_event_content: {
|
||||
body: "Announcing release v3.0.0!! wohoo",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies DraupnirNewsBlob;
|
||||
const seenNews = new Set<string>();
|
||||
const notifiedNews: string[] = [];
|
||||
const newsLifecycle = new DraupnirNewsLifecycle(
|
||||
seenNews,
|
||||
fileSystemNews,
|
||||
async (allNews) => {
|
||||
allNews.forEach((item) => seenNews.add(item.news_id));
|
||||
return Ok(undefined);
|
||||
},
|
||||
async () => ResultError.Result("Can't fetch remote news :("),
|
||||
async (item) => {
|
||||
notifiedNews.push(item.news_id);
|
||||
return Ok(undefined);
|
||||
}
|
||||
);
|
||||
expect(seenNews.size).toBe(0);
|
||||
expect(notifiedNews.length).toBe(0);
|
||||
await newsLifecycle.checkForNews();
|
||||
expect(seenNews.size).toBe(1);
|
||||
expect(notifiedNews.length).toBe(1);
|
||||
await newsLifecycle.checkForNews();
|
||||
expect(notifiedNews.length).toBe(1);
|
||||
});
|
||||
});
|
||||
16
yarn.lock
16
yarn.lock
@@ -319,10 +319,10 @@
|
||||
"@gnuxie/super-cool-stream" "^0.2.1"
|
||||
"@gnuxie/typescript-result" "^1.0.0"
|
||||
|
||||
"@the-draupnir-project/matrix-basic-types@1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@the-draupnir-project/matrix-basic-types/-/matrix-basic-types-1.4.0.tgz#18fcfc7561ad495f4868ef4298131a3e20e7d946"
|
||||
integrity sha512-nKK9vmAXh87VwaANvlNlUaq/rIu50VcdRXfoPJB99RqY4dt6iXRu/1b8mQJ5rDCK4yun/4IyGexw6FVQAqT58Q==
|
||||
"@the-draupnir-project/matrix-basic-types@1.4.1":
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@the-draupnir-project/matrix-basic-types/-/matrix-basic-types-1.4.1.tgz#2e8fdbadb0781fba29383c5cdf530357d25b8718"
|
||||
integrity sha512-NzA2EWiTQK774J6hZZ4W5xJWY0G9J1gadrTWJfVrAo5GupwfY3nmRtVgZogpqwWbMV0t2zKcqXYuYqyP+1ju5w==
|
||||
dependencies:
|
||||
"@gnuxie/typescript-result" "^1.0.0"
|
||||
glob-to-regexp "^0.4.1"
|
||||
@@ -2606,10 +2606,10 @@ matrix-appservice@^2.0.0:
|
||||
"@gnuxie/typescript-result" "^1.0.0"
|
||||
await-lock "^2.2.2"
|
||||
|
||||
"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@3.13.0":
|
||||
version "3.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-3.13.0.tgz#cff398d88a1e4230074b4e29671cd088efa85943"
|
||||
integrity sha512-sgBDvHwR1J/1//Z5grWz2+J46Q2VZpzF4QjlaE2VzBQxLjlQS7d8csM2/QxRpaiLcMUdU+d+5QTqxQT131Bd9g==
|
||||
"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-4.0.0.tgz#86d9522397f79672dd65077c1c4d344bcc63094e"
|
||||
integrity sha512-NmkqQMgPr3mDyg8KYfSx9Prb/T1RgGG/O2ASXmJzGfEBTfWf9NeXiDeG4VqgIqJkuoxvp8vbk1/nRLcYnMDDuQ==
|
||||
dependencies:
|
||||
"@gnuxie/typescript-result" "^1.0.0"
|
||||
await-lock "^2.2.2"
|
||||
|
||||
Reference in New Issue
Block a user