mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-20 21:59:53 -05:00
feat(sync): notify on enable, change defaults to daily and designer profiles
This commit is contained in:
1 parent
acfa844ebe
commit
9334438a52
11 files changed
+60
-15
No files matched your search
@@ -285,7 +285,7 @@ out as the backend's user, so `PUID`/`PGID` need write access to the folder. The
|
||||
## Synced Collections
|
||||
|
||||
Link a MakerWorld, Printables or Thingiverse collection to Thingport once, with one tick in Thingport Grab, and every
|
||||
model added to it later is imported into your library on its own. Thingport checks it every hour, 6 hours or 24 hours,
|
||||
model added to it later is imported into your library on its own. Thingport checks it once a day, or every 6 hours or every hour,
|
||||
imports new models one at a time and sends a notification saying what came in. Sync only ever adds: nothing is deleted
|
||||
on either side. See the **[synced collections guide](docs/guides/synced-collections.md)**.
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- New syncs are checked daily. Existing syncs keep their interval.
|
||||
ALTER TABLE "CollectionSync" ALTER COLUMN "intervalHours" SET DEFAULT 24;
|
||||
@@ -208,9 +208,10 @@ model CollectionSync {
|
||||
externalId String
|
||||
sourceUrl String
|
||||
// MakerWorld only: which print profiles a new model brings ("url" is the model's default one).
|
||||
// New MakerWorld syncs start on "designer" (see createSync).
|
||||
profileScope String @default("url")
|
||||
// How often the provider's collection is checked: 1, 6 or 24.
|
||||
intervalHours Int @default(1)
|
||||
intervalHours Int @default(24)
|
||||
// Every model id the provider's collection has listed, so a sync only imports what's new since.
|
||||
knownIds String[] @default([])
|
||||
// Checks in a row that found the provider's collection gone. One bad answer doesn't unsync it.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { COLLECTION_SYNC_ENABLED } from "../config";
|
||||
|
||||
/** The intervals a collection can pick, in hours. The shortest is also the default. */
|
||||
/** The intervals a collection can pick, in hours. New syncs get 24 (see CollectionSync.intervalHours). */
|
||||
export const SYNC_INTERVAL_HOURS = [1, 6, 24] as const;
|
||||
export type SyncIntervalHours = (typeof SYNC_INTERVAL_HOURS)[number];
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { parseMakerworldCollectionUrl } from "./makerworldCollections";
|
||||
import { parsePrintablesCollectionUrl } from "./printablesApi";
|
||||
import { parseThingiverseCollectionUrl, parseThingiverseLikesUrl } from "./thingiverseApi";
|
||||
import { createLog } from "./auditLog";
|
||||
import { createNotification } from "./notificationService";
|
||||
import type { SyncIntervalHours } from "./collectionSyncSchedule";
|
||||
import {
|
||||
LIKES_ID_PREFIX,
|
||||
@@ -206,9 +207,38 @@ export async function enableSync(userId: string, input: EnableSyncInput): Promis
|
||||
targetId: created.collectionId,
|
||||
details: { name: created.collection.name, provider: source.provider, url: source.url },
|
||||
});
|
||||
// The sync exists by now, so a failed notification mustn't fail the request: a retry finds the
|
||||
// sync and wouldn't send it either.
|
||||
await notifySyncEnabled(created).catch((err) => console.error("[sync] enable notification failed", err));
|
||||
return toSyncLinkOut(created);
|
||||
}
|
||||
|
||||
const INTERVAL_PHRASES: Record<number, string> = { 1: "every hour", 6: "every 6 hours", 24: "once a day" };
|
||||
const PROFILE_SCOPE_PHRASES: Record<string, string> = {
|
||||
url: "its default print profile",
|
||||
designer: "all of the designer's print profiles",
|
||||
all: "all of its print profiles, community ones included",
|
||||
};
|
||||
|
||||
/** Confirms in the app what was switched on from Thingport Grab, with the defaults it got. */
|
||||
async function notifySyncEnabled(sync: CollectionSync & { collection: Collection }): Promise<void> {
|
||||
const name = sync.collection.name;
|
||||
const interval = INTERVAL_PHRASES[sync.intervalHours] ?? `every ${sync.intervalHours} hours`;
|
||||
const profiles =
|
||||
sync.provider === "makerworld" && PROFILE_SCOPE_PHRASES[sync.profileScope]
|
||||
? `, each with ${PROFILE_SCOPE_PHRASES[sync.profileScope]}`
|
||||
: "";
|
||||
await createNotification(sync.userId, {
|
||||
type: "sync",
|
||||
title: `"${name}" is now synced`,
|
||||
body:
|
||||
`New models in ${sourceDescription(sync)} are imported here ${interval}${profiles}. ` +
|
||||
"Change this in the collection's sync configuration.",
|
||||
externalUrl: sync.sourceUrl,
|
||||
internalPath: `/models/collections/${sync.collectionId}`,
|
||||
});
|
||||
}
|
||||
|
||||
// How many "Things (MakerWorld 2)"-style names to try before giving up.
|
||||
const MAX_NAME_SUFFIX = 20;
|
||||
|
||||
@@ -222,6 +252,9 @@ function* collectionNamesFor(title: string, provider: string): Generator<string>
|
||||
for (let n = 2; n <= MAX_NAME_SUFFIX; n++) yield `${title} (${label} ${n})`;
|
||||
}
|
||||
|
||||
// A synced collection follows what the designer publishes, so new models bring all of their profiles.
|
||||
const NEW_MAKERWORLD_SYNC_PROFILE_SCOPE: SyncProfileScope = "designer";
|
||||
|
||||
/** Links `collection` to `source`. `takenBy` names the provider when the collection already follows
|
||||
* another source; `link` is the existing one when another request just made this same link. */
|
||||
async function createSync(
|
||||
@@ -241,6 +274,7 @@ async function createSync(
|
||||
externalId: source.externalId,
|
||||
sourceUrl: source.url,
|
||||
knownIds,
|
||||
...(source.provider === "makerworld" ? { profileScope: NEW_MAKERWORLD_SYNC_PROFILE_SCOPE } : {}),
|
||||
},
|
||||
include: { collection: true },
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ async function importDirectly(designId: string) {
|
||||
}
|
||||
|
||||
async function latestNotification() {
|
||||
return prisma.notification.findFirstOrThrow({ where: { userId }, orderBy: { createdAt: "desc" } });
|
||||
return prisma.notification.findFirstOrThrow({ where: { userId }, orderBy: [{ createdAt: "desc" }, { id: "desc" }] });
|
||||
}
|
||||
|
||||
describe("synced collections", () => {
|
||||
@@ -164,7 +164,12 @@ describe("synced collections", () => {
|
||||
|
||||
const list = await request(app).get("/api/collections").set(auth());
|
||||
const listed = list.body.find((c: { id: string }) => c.id === res.body.collection_id);
|
||||
expect(listed.sync).toMatchObject({ provider: "makerworld", profile_scope: "url" });
|
||||
expect(listed.sync).toMatchObject({ provider: "makerworld", profile_scope: "designer", interval_hours: 24 });
|
||||
|
||||
const notification = await latestNotification();
|
||||
expect(notification.title).toBe(`"Phoenix flock" is now synced`);
|
||||
expect(notification.body).toContain("once a day, each with all of the designer's print profiles");
|
||||
expect(notification.internalPath).toBe(`/models/collections/${res.body.collection_id}`);
|
||||
});
|
||||
|
||||
it("brings back a deleted collection of already-imported models, in the provider's order", async () => {
|
||||
@@ -556,13 +561,13 @@ describe("synced collections", () => {
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("checks every collection on its own interval, 1 hour unless it picks 6 or 24", async () => {
|
||||
it("checks every collection on its own interval, 24 hours unless it picks 1 or 6", async () => {
|
||||
const enabled = await enable({
|
||||
url: `https://www.thingiverse.com/collections/${newId()}`,
|
||||
title: `Thingiverse ${newId()}`,
|
||||
known_ids: [],
|
||||
});
|
||||
expect(enabled.body.interval_hours).toBe(1);
|
||||
expect(enabled.body.interval_hours).toBe(24);
|
||||
const patch = (body: Record<string, unknown>) =>
|
||||
request(app).patch(`/api/collection/${enabled.body.collection_id}/sync`).set(auth()).send(body);
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ out: sync only picks up models added **after** you turn it on.
|
||||
Already imported the collection before? Open its page again, open the panel and tick the box. The panel says
|
||||
everything is already in your library, and **Save** turns sync on.
|
||||
|
||||
Thingport sends a notification once sync is on, naming the collection it follows and its settings.
|
||||
|
||||
## Find it in Thingport
|
||||
|
||||
A synced collection's tile on the **Collections** page shows the provider's logo with a green tick.
|
||||
@@ -57,12 +59,13 @@ linked:
|
||||
- **Overview** shows the chain from the provider's collection to yours, when it was last checked and when the next
|
||||
check is due, and a link to the collection on the provider's site. **Sync now** checks it right away. **Stop
|
||||
syncing** unlinks it.
|
||||
- **Settings** sets how often it's checked: every hour, 6 hours or 24 hours. For MakerWorld it also sets which print
|
||||
profiles each new model brings: the default one, the designer's, or all of them, community ones included.
|
||||
- **Settings** sets how often it's checked: every hour, 6 hours or 24 hours (the default). For MakerWorld it also sets
|
||||
which print profiles each new model brings: the default one, the designer's (the default), or all of them, community
|
||||
ones included.
|
||||
|
||||
## How it works
|
||||
|
||||
- **On a schedule.** Each synced collection is checked on its own interval, every hour unless you change it. A check
|
||||
- **On a schedule.** Each synced collection is checked on its own interval, once a day unless you change it. A check
|
||||
compares the provider's collection with the models Thingport has already seen there.
|
||||
- **New models only.** A model that's already in your library is just added to the collection. The rest are queued and
|
||||
imported one at a time, a minute apart, so the provider isn't flooded with requests. You get a notification when
|
||||
|
||||
+2
-2
@@ -186,7 +186,7 @@ On a MakerWorld, Printables or Thingiverse collection page, the panel's **Keep t
|
||||
sync** box (off by default) links the collection to its Thingport counterpart. A Thingiverse user's
|
||||
Likes page works the same way (**Keep these likes in sync**), filing into "Thingiverse likes
|
||||
(@username)" and picking up models they like later. Thingport then checks
|
||||
it every hour (or every 6 or 24 hours, picked per collection) and imports models added to it later, pausing a minute between models, and sends a
|
||||
it once a day (or every hour or 6 hours, picked per collection) and imports models added to it later, pausing a minute between models, and sends a
|
||||
notification saying what came in. Models already on the page when you turn sync on, including any
|
||||
you leave unticked, aren't imported by the sync.
|
||||
|
||||
@@ -209,7 +209,7 @@ extension can turn sync on, since it has just seen which models the collection h
|
||||
rest wait, paused, in the import queue until it's started again from Administration > Import queue.
|
||||
- If the provider's collection is deleted, the Thingport collection is unsynced (its models stay)
|
||||
and you get a notification. Deleting the Thingport collection removes the sync with it.
|
||||
- **Sync now** checks the collection right away instead of waiting for the hour, and a notification
|
||||
- **Sync now** checks the collection right away instead of waiting for the next check, and a notification
|
||||
says how it went, "up to date" included. It's available again 5 minutes after the last check.
|
||||
- To turn sync off, untick the box on the collection page, or use **Stop syncing** in the
|
||||
collection's sync configuration.
|
||||
|
||||
@@ -35,7 +35,7 @@ export function syncToggleHtml(state: SyncLookup | null): string {
|
||||
const added = isLikes() ? "Models liked later" : `Models added to it on ${provider} later`;
|
||||
const hint = state.sync
|
||||
? `Synced with "${escapeHtml(state.sync.collection_name)}" in Thingport. Untick to stop.`
|
||||
: `${added} are imported into Thingport automatically, every hour unless you change it there. Models you leave out now stay out.`;
|
||||
: `${added} are imported into Thingport automatically, on a schedule you can change there. Models you leave out now stay out.`;
|
||||
return `
|
||||
<label class="tg-toggle">
|
||||
<input id="tg-sync" type="checkbox" ${state.sync ? "checked" : ""} />
|
||||
|
||||
@@ -34,7 +34,7 @@ collection before? Tick the box and click **Save**.
|
||||
|
||||
## What happens next
|
||||
|
||||
Thingport checks the collection every hour (or every 6 or 24 hours, if you'd rather) and imports what's new, one model
|
||||
Thingport checks the collection once a day (or every 6 hours or every hour, if you'd rather) and imports what's new, one model
|
||||
at a time, a minute apart, so the provider's limits aren't hit. A notification tells you what came in.
|
||||
|
||||
Synced collections show the provider's logo on the Collections page. Open one and click the logo to see its sync
|
||||
|
||||
@@ -45,7 +45,7 @@ const SECTIONS = [
|
||||
points: [
|
||||
"Keep saving models to collections on the sites you browse, even from your phone. They turn up in your library on their own.",
|
||||
"Follow someone else's public collection, or a Thingiverse user's likes, and get each model they add.",
|
||||
"Checked every hour, 6 hours or 24 hours, with a notification saying what came in, and Sync now for when you can't wait.",
|
||||
"Checked once a day, or every 6 hours or every hour, with a notification saying what came in, and Sync now for when you can't wait.",
|
||||
"New models are imported one at a time, a minute apart, so the provider's limits aren't hit.",
|
||||
"Sync only adds: removing a model on the provider never deletes it from Thingport, and a model you deleted isn't brought back.",
|
||||
"Turn it on with one tick in Thingport Grab, on the collection's own page.",
|
||||
|
||||
Reference in new issue
Block a user