@rimori/client 2.4.0-next.4 → 2.4.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,192 @@
1
+ import { EventBus, EventBusMessage, EventHandler, EventPayload, EventListener } from '../../fromRimori/EventBus';
2
+ import { MainPanelAction } from '../../fromRimori/PluginTypes';
3
+ import { AccomplishmentController, AccomplishmentPayload } from '../../controller/AccomplishmentController';
4
+
5
+ /**
6
+ * Event module for plugin event bus operations.
7
+ * Provides methods for emitting, listening to, and responding to events.
8
+ */
9
+ export class EventModule {
10
+ private pluginId: string;
11
+ private accomplishmentController: AccomplishmentController;
12
+
13
+ constructor(pluginId: string) {
14
+ this.pluginId = pluginId;
15
+ this.accomplishmentController = new AccomplishmentController(pluginId);
16
+ }
17
+
18
+ public getGlobalEventTopic(preliminaryTopic: string): string {
19
+ if (preliminaryTopic.startsWith('global.')) {
20
+ return preliminaryTopic;
21
+ }
22
+ if (preliminaryTopic.startsWith('self.')) {
23
+ return preliminaryTopic;
24
+ }
25
+ const topicParts = preliminaryTopic.split('.');
26
+ if (topicParts.length === 3) {
27
+ if (!topicParts[0].startsWith('pl') && topicParts[0] !== 'global') {
28
+ throw new Error("The event topic must start with the plugin id or 'global'.");
29
+ }
30
+ return preliminaryTopic;
31
+ } else if (topicParts.length > 3) {
32
+ throw new Error(
33
+ `The event topic must consist of 3 parts. <pluginId>.<topic area>.<action>. Received: ${preliminaryTopic}`,
34
+ );
35
+ }
36
+
37
+ const topicRoot = this.pluginId ?? 'global';
38
+ return `${topicRoot}.${preliminaryTopic}`;
39
+ }
40
+
41
+ /**
42
+ * Emit an event to Rimori or a plugin.
43
+ * The topic schema is:
44
+ * {pluginId}.{eventId}
45
+ * Check out the event bus documentation for more information.
46
+ * For triggering events from Rimori like context menu actions use the "global" keyword.
47
+ * @param topic The topic to emit the event on.
48
+ * @param data The data to emit.
49
+ * @param eventId The event id.
50
+ */
51
+ emit(topic: string, data?: any, eventId?: number): void {
52
+ const globalTopic = this.getGlobalEventTopic(topic);
53
+ EventBus.emit(this.pluginId, globalTopic, data, eventId);
54
+ }
55
+
56
+ /**
57
+ * Request an event.
58
+ * @param topic The topic to request the event on.
59
+ * @param data The data to request.
60
+ * @returns The response from the event.
61
+ */
62
+ request<T>(topic: string, data?: any): Promise<EventBusMessage<T>> {
63
+ const globalTopic = this.getGlobalEventTopic(topic);
64
+ return EventBus.request<T>(this.pluginId, globalTopic, data);
65
+ }
66
+
67
+ /**
68
+ * Subscribe to an event.
69
+ * @param topic The topic to subscribe to.
70
+ * @param callback The callback to call when the event is emitted.
71
+ * @returns An EventListener object containing an off() method to unsubscribe the listeners.
72
+ */
73
+ on<T = EventPayload>(topic: string | string[], callback: EventHandler<T>): EventListener {
74
+ const topics = Array.isArray(topic) ? topic : [topic];
75
+ return EventBus.on<T>(
76
+ topics.map((t) => this.getGlobalEventTopic(t)),
77
+ callback,
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Subscribe to an event once.
83
+ * @param topic The topic to subscribe to.
84
+ * @param callback The callback to call when the event is emitted.
85
+ */
86
+ once<T = EventPayload>(topic: string, callback: EventHandler<T>): void {
87
+ EventBus.once<T>(this.getGlobalEventTopic(topic), callback);
88
+ }
89
+
90
+ /**
91
+ * Respond to an event.
92
+ * @param topic The topic to respond to.
93
+ * @param data The data to respond with.
94
+ */
95
+ respond<T = EventPayload>(
96
+ topic: string | string[],
97
+ data: EventPayload | ((data: EventBusMessage<T>) => EventPayload | Promise<EventPayload>),
98
+ ): void {
99
+ const topics = Array.isArray(topic) ? topic : [topic];
100
+ EventBus.respond(
101
+ this.pluginId,
102
+ topics.map((t) => this.getGlobalEventTopic(t)),
103
+ data,
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Emit an accomplishment.
109
+ * @param payload The payload to emit.
110
+ */
111
+ emitAccomplishment(payload: AccomplishmentPayload): void {
112
+ this.accomplishmentController.emitAccomplishment(payload);
113
+ }
114
+
115
+ /**
116
+ * Subscribe to an accomplishment.
117
+ * @param accomplishmentTopic The topic to subscribe to.
118
+ * @param callback The callback to call when the accomplishment is emitted.
119
+ */
120
+ onAccomplishment(
121
+ accomplishmentTopic: string,
122
+ callback: (payload: EventBusMessage<AccomplishmentPayload>) => void,
123
+ ): void {
124
+ this.accomplishmentController.subscribe(accomplishmentTopic, callback);
125
+ }
126
+
127
+ /**
128
+ * Trigger an action that opens the sidebar and triggers an action in the designated plugin.
129
+ * @param pluginId The id of the plugin to trigger the action for.
130
+ * @param actionKey The key of the action to trigger.
131
+ * @param text Optional text to be used for the action like for example text that the translator would look up.
132
+ */
133
+ emitSidebarAction(pluginId: string, actionKey: string, text?: string): void {
134
+ this.emit('global.sidebar.triggerAction', { plugin_id: pluginId, action_key: actionKey, text });
135
+ }
136
+
137
+ /**
138
+ * Subscribe to main panel actions triggered by the user from the dashboard.
139
+ * @param callback Handler function that receives the action data when a matching action is triggered.
140
+ * @param actionsToListen Optional filter to listen only to specific action keys. If empty or not provided, all actions will trigger the callback.
141
+ * @returns An EventListener object with an `off()` method for cleanup.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const listener = client.event.onMainPanelAction((data) => {
146
+ * console.log('Action received:', data.action_key);
147
+ * }, ['startSession', 'pauseSession']);
148
+ *
149
+ * // Clean up when component unmounts to prevent events from firing
150
+ * // when navigating away or returning to the page
151
+ * useEffect(() => {
152
+ * return () => listener.off();
153
+ * }, []);
154
+ * ```
155
+ *
156
+ * **Important:** Always call `listener.off()` when your component unmounts or when you no longer need to listen.
157
+ * This prevents the event handler from firing when navigating away from or returning to the page, which could
158
+ * cause unexpected behavior or duplicate event handling.
159
+ */
160
+ onMainPanelAction(callback: (data: MainPanelAction) => void, actionsToListen: string | string[] = []): EventListener {
161
+ const listeningActions = Array.isArray(actionsToListen) ? actionsToListen : [actionsToListen];
162
+ // this needs to be a emit and on because the main panel action is triggered by the user and not by the plugin
163
+ this.emit('action.requestMain');
164
+ return this.on<MainPanelAction>('action.requestMain', ({ data }) => {
165
+ // console.log('Received action for main panel ' + data.action_key);
166
+ // console.log('Listening to actions', listeningActions);
167
+ if (listeningActions.length === 0 || listeningActions.includes(data.action_key)) {
168
+ callback(data);
169
+ }
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Subscribe to side panel actions triggered by the user from the dashboard.
175
+ * @param callback Handler function that receives the action data when a matching action is triggered.
176
+ * @param actionsToListen Optional filter to listen only to specific action keys. If empty or not provided, all actions will trigger the callback.
177
+ * @returns An EventListener object with an `off()` method for cleanup.
178
+ */
179
+ onSidePanelAction(callback: (data: MainPanelAction) => void, actionsToListen: string | string[] = []): EventListener {
180
+ const listeningActions = Array.isArray(actionsToListen) ? actionsToListen : [actionsToListen];
181
+ // this needs to be a emit and on because the main panel action is triggered by the user and not by the plugin
182
+ this.emit('action.requestSidebar');
183
+ return this.on<MainPanelAction>('action.requestSidebar', ({ data }) => {
184
+ // console.log("eventHandler .onSidePanelAction", data);
185
+ // console.log('Received action for sidebar ' + data.action);
186
+ // console.log('Listening to actions', listeningActions);
187
+ if (listeningActions.length === 0 || listeningActions.includes(data.action)) {
188
+ callback(data);
189
+ }
190
+ });
191
+ }
192
+ }
@@ -1,5 +1,6 @@
1
1
  import { SupabaseClient } from '@supabase/supabase-js';
2
- import { RimoriClient } from '../plugin/RimoriClient';
2
+ import { RimoriCommunicationHandler, RimoriInfo } from '../CommunicationHandler';
3
+ import { EventModule } from './EventModule';
3
4
 
4
5
  export type TriggerAction = { action_key: string } & Record<string, string | number | boolean>;
5
6
 
@@ -20,7 +21,6 @@ export interface Exercise {
20
21
  start_date: string;
21
22
  end_date: string;
22
23
  trigger_action: TriggerAction;
23
- // topic that identifies the accomplishment that the exercise is related to
24
24
  achievement_topic: string;
25
25
  name: string;
26
26
  description: string;
@@ -29,13 +29,32 @@ export interface Exercise {
29
29
  updated_at?: string;
30
30
  }
31
31
 
32
- export class ExerciseController {
32
+ /**
33
+ * Controller for exercise-related operations.
34
+ * Provides access to weekly exercises and exercise management.
35
+ */
36
+ export class ExerciseModule {
33
37
  private supabase: SupabaseClient;
34
- private rimoriClient: RimoriClient;
35
-
36
- constructor(supabase: SupabaseClient, rimoriClient: RimoriClient) {
38
+ private communicationHandler: RimoriCommunicationHandler;
39
+ private eventModule: EventModule;
40
+ private backendUrl: string;
41
+ private token: string;
42
+
43
+ constructor(
44
+ supabase: SupabaseClient,
45
+ communicationHandler: RimoriCommunicationHandler,
46
+ info: RimoriInfo,
47
+ eventModule: EventModule,
48
+ ) {
37
49
  this.supabase = supabase;
38
- this.rimoriClient = rimoriClient;
50
+ this.communicationHandler = communicationHandler;
51
+ this.eventModule = eventModule;
52
+ this.token = info.token;
53
+ this.backendUrl = info.backendUrl;
54
+
55
+ this.communicationHandler.onUpdate((updatedInfo) => {
56
+ this.token = updatedInfo.token;
57
+ });
39
58
  }
40
59
 
41
60
  /**
@@ -43,7 +62,7 @@ export class ExerciseController {
43
62
  * Shows exercises for the current week that haven't expired.
44
63
  * @returns Array of exercise objects.
45
64
  */
46
- public async viewWeeklyExercises(): Promise<Exercise[]> {
65
+ async view(): Promise<Exercise[]> {
47
66
  const { data, error } = await this.supabase.from('weekly_exercises').select('*');
48
67
 
49
68
  if (error) {
@@ -54,21 +73,21 @@ export class ExerciseController {
54
73
  }
55
74
 
56
75
  /**
57
- * Creates multiple exercises via the backend API.
58
- * All requests are made in parallel but only one event is emitted.
59
- * @param token The token to use for authentication.
60
- * @param backendUrl The URL of the backend API.
61
- * @param exercises Exercise creation parameters.
76
+ * Creates a new exercise or multiple exercises via the backend API.
77
+ * When creating multiple exercises, all requests are made in parallel but only one event is emitted.
78
+ * @param params Exercise creation parameters (single or array).
62
79
  * @returns Created exercise objects.
63
80
  */
64
- public async addExercise(token: string, backendUrl: string, exercises: CreateExerciseParams[]): Promise<Exercise[]> {
81
+ async add(params: CreateExerciseParams | CreateExerciseParams[]): Promise<Exercise[]> {
82
+ const exercises = Array.isArray(params) ? params : [params];
83
+
65
84
  const responses = await Promise.all(
66
85
  exercises.map(async (exercise) => {
67
- const response = await fetch(`${backendUrl}/exercises`, {
86
+ const response = await fetch(`${this.backendUrl}/exercises`, {
68
87
  method: 'POST',
69
88
  headers: {
70
89
  'Content-Type': 'application/json',
71
- Authorization: `Bearer ${token}`,
90
+ Authorization: `Bearer ${this.token}`,
72
91
  },
73
92
  body: JSON.stringify(exercise),
74
93
  });
@@ -82,27 +101,21 @@ export class ExerciseController {
82
101
  }),
83
102
  );
84
103
 
85
- this.rimoriClient.event.emit('global.exercises.triggerChange');
104
+ this.eventModule.emit('global.exercises.triggerChange');
86
105
 
87
106
  return responses;
88
107
  }
89
108
 
90
109
  /**
91
110
  * Deletes an exercise via the backend API.
92
- * @param token The token to use for authentication.
93
- * @param backendUrl The URL of the backend API.
94
- * @param exerciseId The exercise ID to delete.
111
+ * @param id The exercise ID to delete.
95
112
  * @returns Success status.
96
113
  */
97
- public async deleteExercise(
98
- token: string,
99
- backendUrl: string,
100
- id: string,
101
- ): Promise<{ success: boolean; message: string }> {
102
- const response = await fetch(`${backendUrl}/exercises/${id}`, {
114
+ async delete(id: string): Promise<{ success: boolean; message: string }> {
115
+ const response = await fetch(`${this.backendUrl}/exercises/${id}`, {
103
116
  method: 'DELETE',
104
117
  headers: {
105
- Authorization: `Bearer ${token}`,
118
+ Authorization: `Bearer ${this.token}`,
106
119
  },
107
120
  });
108
121
 
@@ -110,7 +123,8 @@ export class ExerciseController {
110
123
  const errorText = await response.text();
111
124
  throw new Error(`Failed to delete exercise: ${errorText}`);
112
125
  }
113
- this.rimoriClient.event.emit('global.exercises.triggerChange');
126
+
127
+ this.eventModule.emit('global.exercises.triggerChange');
114
128
 
115
129
  return await response.json();
116
130
  }
@@ -0,0 +1,114 @@
1
+ import { SettingsController, UserInfo } from '../../controller/SettingsController';
2
+ import { RimoriCommunicationHandler, RimoriInfo } from '../CommunicationHandler';
3
+ import { Translator } from '../../controller/TranslationController';
4
+ import { ActivePlugin, Plugin } from '../../fromRimori/PluginTypes';
5
+ import { SupabaseClient } from '@supabase/supabase-js';
6
+
7
+ type Theme = 'light' | 'dark';
8
+ type ApplicationMode = 'main' | 'sidebar' | 'settings';
9
+ /**
10
+ * Controller for plugin-related operations.
11
+ * Provides access to plugin settings, user info, and plugin information.
12
+ */
13
+ export class PluginModule {
14
+ private settingsController: SettingsController;
15
+ private communicationHandler: RimoriCommunicationHandler;
16
+ private translator: Translator;
17
+ private rimoriInfo: RimoriInfo;
18
+ public pluginId: string;
19
+ /**
20
+ * The release channel of this plugin installation.
21
+ * Determines which database schema is used for plugin tables.
22
+ */
23
+ public releaseChannel: string;
24
+ public applicationMode: ApplicationMode;
25
+ public theme: Theme;
26
+
27
+ constructor(supabase: SupabaseClient, communicationHandler: RimoriCommunicationHandler, info: RimoriInfo) {
28
+ this.rimoriInfo = info;
29
+ this.communicationHandler = communicationHandler;
30
+ this.pluginId = info.pluginId;
31
+ this.releaseChannel = info.releaseChannel;
32
+
33
+ this.settingsController = new SettingsController(supabase, info.pluginId, info.guild);
34
+
35
+ const currentPlugin = info.installedPlugins.find((plugin) => plugin.id === info.pluginId);
36
+ this.translator = new Translator(info.interfaceLanguage, currentPlugin?.endpoint || '');
37
+
38
+ this.communicationHandler.onUpdate((updatedInfo) => (this.rimoriInfo = updatedInfo));
39
+ this.applicationMode = this.communicationHandler.getQueryParam('applicationMode') as ApplicationMode;
40
+ this.theme = (this.communicationHandler.getQueryParam('rm_theme') as Theme) || 'light';
41
+ }
42
+
43
+ /**
44
+ * Set the settings for the plugin.
45
+ * @param settings The settings to set.
46
+ */
47
+ async setSettings(settings: any): Promise<void> {
48
+ await this.settingsController.setSettings(settings);
49
+ }
50
+
51
+ /**
52
+ * Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
53
+ * @param defaultSettings The default settings to use if no settings are found.
54
+ * @returns The settings for the plugin.
55
+ */
56
+ async getSettings<T extends object>(defaultSettings: T): Promise<T> {
57
+ return await this.settingsController.getSettings<T>(defaultSettings);
58
+ }
59
+
60
+ /**
61
+ * Get the current user info.
62
+ * Note: For reactive updates in React components, use the userInfo from useRimori() hook instead.
63
+ * @returns The user info.
64
+ */
65
+ getUserInfo(): UserInfo {
66
+ return this.rimoriInfo.profile;
67
+ }
68
+
69
+ /**
70
+ * Register a callback to be notified when RimoriInfo is updated.
71
+ * This is useful for reacting to changes in user info, tokens, or other rimori data.
72
+ * @param callback - Function to call with the new RimoriInfo
73
+ * @returns Cleanup function to unregister the callback
74
+ */
75
+ onRimoriInfoUpdate(callback: (info: RimoriInfo) => void): () => void {
76
+ return this.communicationHandler.onUpdate(callback);
77
+ }
78
+
79
+ /**
80
+ * Retrieves information about plugins, including:
81
+ * - All installed plugins
82
+ * - The currently active plugin in the main panel
83
+ * - The currently active plugin in the side panel
84
+ */
85
+ getPluginInfo(): {
86
+ /**
87
+ * All installed plugins.
88
+ */
89
+ installedPlugins: Plugin[];
90
+ /**
91
+ * The plugin that is loaded in the main panel.
92
+ */
93
+ mainPanelPlugin?: ActivePlugin;
94
+ /**
95
+ * The plugin that is loaded in the side panel.
96
+ */
97
+ sidePanelPlugin?: ActivePlugin;
98
+ } {
99
+ return {
100
+ installedPlugins: this.rimoriInfo.installedPlugins,
101
+ mainPanelPlugin: this.rimoriInfo.mainPanelPlugin,
102
+ sidePanelPlugin: this.rimoriInfo.sidePanelPlugin,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Get the translator for the plugin.
108
+ * @returns The translator for the plugin.
109
+ */
110
+ async getTranslator(): Promise<Translator> {
111
+ await this.translator.initialize();
112
+ return this.translator;
113
+ }
114
+ }