Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: organize code in modules, absolute imports, better exports #47

Closed
wants to merge 5 commits into from

Conversation

Lukapetro
Copy link

@Lukapetro Lukapetro commented Jan 8, 2025

Refactor: Reorganize Project Structure with Modular Design

Reorganizes codebase with a modular, feature-based architecture:

src/
  core/                   # Core functionality
    auth/
    api/
    types/
    utils/
  features/              # Feature modules
    tweets/
    profiles/
    search/
    relationships/
    messages/
    spaces/
      core/
      plugins/
    trends/
  timeline/              # Timeline related functionality
    components/
    types/
  platform/              # Platform specific code
  cli/                   # Command line interface
  scraper/               # Main scraper functionality
  index.ts              # Main library entry point

Changes:

  • Moves files into feature-based modules
  • Each feature has its own types, tests, and utils
  • Core functionality separated from features
  • Path aliases enable clean imports (@/core/auth vs ../../core/auth)

Example changes:

  • Before: import { TwitterAuth } from '../../core/auth/auth'
  • After: import { TwitterAuth } from '@/core/auth'

⚠️ NOTE: This refactor has high impact! Thorough testing needed to catch any broken imports or functionality.

Testing Strategy:

  1. Do manual testing of core features
  2. Test import functionality in various modules
  3. Verify build process works
  4. Test published package imports

Summary by CodeRabbit

Release Notes

  • Project Restructuring

    • Significant reorganization of project directory structure.
    • Introduced modular import system with @/ alias.
    • Consolidated exports through centralized index.ts files.
  • Code Organization

    • Moved modules into more logical directories.
    • Updated import paths across multiple files.
    • Enhanced type and utility imports.
  • Build Configuration

    • Updated TypeScript configuration to support new module resolution.
    • Added base URL and path mapping for improved imports.
  • Maintenance

    • Improved code readability and maintainability.
    • Standardized module and file structure.
    • Removed unused modules and exports.
    • No significant changes to core functionality.

@Lukapetro
Copy link
Author

@wtfsayo after this is merged i can take a look at:

@Lukapetro
Copy link
Author

⚠️ NOTE: This refactor has high impact! Thorough testing needed to catch any broken imports or functionality. DO NOT MERGE BLINDLY, it may break features around

@HashWarlock
Copy link

I see some imports use @ then others use ../../src is there a reason for this that I'm missing or can they all move to use @ with this change now?

@Lukapetro
Copy link
Author

we can use absolute import everywhere @HashWarlock, where you see the imports with ../../ is because there is the old import left that I missed

@wtfsayo
Copy link
Member

wtfsayo commented Jan 9, 2025

why inconsistent with @/... path and ../

@Lukapetro
Copy link
Author

why inconsistent with @/... path and ../

aight let me fix those, also another thing I've seen is having a convention for naming files. Example:

  • ChatClient.ts -> PascalCase
  • testParticipant.ts -> camelCase
  • platform-interface.ts -> kebab-case

For convenience I would say define a standard and adapt the code. Maybe just a separate doc for the whole elizaOS organization, which applies to all repos @wtfsayo @lalalune

@wtfsayo
Copy link
Member

wtfsayo commented Jan 10, 2025

hey so I think the code organisation is good but its hard to truly test unless this is followed up #44;

if you take on #44 you can base it off this branch and can merge together

@wtfsayo
Copy link
Member

wtfsayo commented Jan 12, 2025

@coderabbitai review

Copy link

coderabbitai bot commented Jan 12, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

coderabbitai bot commented Jan 12, 2025

Walkthrough

This pull request involves a comprehensive restructuring of the project's module organization, focusing on improving code structure, import paths, and export mechanisms. The changes primarily involve moving files to new directories, updating import statements to use absolute paths with the @/ alias, and consolidating exports through index.ts files. The modifications aim to enhance code maintainability and provide a more modular approach to the project's architecture.

Changes

File/Directory Change Summary
rollup.config.mjs Updated input source from src/_module.ts to src/index.ts
src/_module.ts Deleted file with consolidated exports
src/cli/command.ts Reorganized imports and added global variables for platform detection
src/cli/index.ts Added re-export for all entities from ./command
src/core/api/api.ts Updated import paths for clarity
src/core/auth/auth-user.ts Updated import paths to reflect new structure
src/core/auth/index.ts Added re-exports for auth and auth-user modules
src/core/types/index.ts Added re-export for spaces module
src/features/index.ts Added re-exports for multiple feature modules
src/features/messages/index.ts Added re-export for all entities from messages module
src/features/profiles/index.ts Added re-export for all entities from profile module
src/features/relationships/index.ts Added re-export for all entities from relationships module
src/features/search/index.ts Added re-export for all entities from search module
src/features/tweets/index.ts Added re-export for all entities from tweets module
src/timeline/index.ts Added re-exports for components and types
tsconfig.json Added baseUrl, paths for module resolution, and updated excluded file path

Sequence Diagram

sequenceDiagram
    participant Dev as Developer
    participant Config as Rollup/TypeScript Config
    participant Modules as Project Modules
    
    Dev->>Config: Update import paths
    Config->>Modules: Resolve using @/ alias
    Modules-->>Config: Return correct module
    Config-->>Dev: Successful module resolution
Loading

Poem

🐰 A Rabbit's Refactoring Tale 🔧
With paths rewritten, imports so neat,
Modules dancing to a modular beat
Aliases spring, directories align
Code structure now looks simply divine!
Hop, hop, hooray for clean design! 🚀

Finishing Touches

  • 📝 Generate Docstrings (Beta)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🔭 Outside diff range comments (2)
src/features/spaces/plugins/MonitorAudioPlugin.ts (1)

Line range hint 24-54: Add error handling for missing ffplay dependency.

The constructor spawns ffplay without first verifying its availability. This could lead to runtime errors if ffplay is not installed on the system.

Consider adding a check before spawning:

  constructor(private readonly sampleRate = 48000, debug = false) {
    this.logger = new Logger(debug);
+   
+   // Verify ffplay is available
+   try {
+     spawn('ffplay', ['-version']).kill();
+   } catch (error) {
+     throw new Error('ffplay is not installed. Please install ffmpeg/ffplay to use MonitorAudioPlugin.');
+   }

    // Spawn ffplay to read raw PCM (s16le) on stdin

Also, consider adding input validation:

  constructor(private readonly sampleRate = 48000, debug = false) {
+   if (sampleRate <= 0 || !Number.isInteger(sampleRate)) {
+     throw new Error('Sample rate must be a positive integer');
+   }
    this.logger = new Logger(debug);
src/features/relationships/relationships.ts (1)

Line range hint 82-124: Reduce code duplication in timeline functions

The getFollowingTimeline and getFollowersTimeline functions share significant code. Consider extracting the common logic into a base function.

+type TimelineType = 'following' | 'followers';
+
+interface TimelineConfig {
+  endpoint: string;
+  errorMessage: string;
+}
+
+const TIMELINE_CONFIGS: Record<TimelineType, TimelineConfig> = {
+  following: {
+    endpoint: 'iSicc7LrzWGBgDPL0tM_TQ/Following',
+    errorMessage: 'Scraper is not logged-in for profile following.',
+  },
+  followers: {
+    endpoint: 'rRXFSG5vR6drKr5M37YOTw/Followers',
+    errorMessage: 'Scraper is not logged-in for profile followers.',
+  },
+};
+
+async function getRelationshipTimeline(
+  type: TimelineType,
+  userId: string,
+  maxItems: number,
+  auth: TwitterAuth,
+  cursor?: string,
+): Promise<RelationshipTimeline> {
+  if (!auth.isLoggedIn()) {
+    throw new Error(TIMELINE_CONFIGS[type].errorMessage);
+  }
+
+  maxItems = validateMaxItems(maxItems);
+
+  const variables: Record<string, any> = {
+    userId,
+    count: maxItems,
+    includePromotedContent: false,
+  };
+
+  if (cursor != null && cursor != '') {
+    variables['cursor'] = cursor;
+  }
+
+  const features = addApiFeatures(TIMELINE_API_FEATURES);
+  const params = new URLSearchParams();
+  params.set('features', stringify(features) ?? '');
+  params.set('variables', stringify(variables) ?? '');
+
+  const res = await requestApi<RelationshipTimeline>(
+    `https://twitter.com/i/api/graphql/${TIMELINE_CONFIGS[type].endpoint}?${params.toString()}`,
+    auth,
+  );
+
+  if (!res.success) {
+    throw res.err;
+  }
+
+  return res.value;
+}
+
+async function getFollowingTimeline(
+  userId: string,
+  maxItems: number,
+  auth: TwitterAuth,
+  cursor?: string,
+): Promise<RelationshipTimeline> {
+  return getRelationshipTimeline('following', userId, maxItems, auth, cursor);
+}
+
+async function getFollowersTimeline(
+  userId: string,
+  maxItems: number,
+  auth: TwitterAuth,
+  cursor?: string,
+): Promise<RelationshipTimeline> {
+  return getRelationshipTimeline('followers', userId, maxItems, auth, cursor);
+}

Also applies to: 139-181

🧰 Tools
🪛 eslint

[error] 10-12: Delete ⏎⏎

(prettier/prettier)

🧹 Nitpick comments (19)
src/features/spaces/plugins/MonitorAudioPlugin.ts (1)

Line range hint 1-15: Enhance documentation with dependency requirements.

The documentation is clear, but consider adding information about external dependencies (ffplay) and system requirements. Also, the comment escape in the usage example could be improved.

  * Usage:
- *   const plugin = new MonitorAudioPlugin(48000, /* debug= *\/ true);
+ *   const plugin = new MonitorAudioPlugin(48000, /* debug= */ true);
  *   space.use(plugin);
src/features/relationships/relationships.ts (3)

Line range hint 89-90: Extract maxItems validation to a shared utility

The maxItems validation is duplicated in both timeline functions. Consider extracting it to a shared utility function to maintain DRY principles.

+const MAX_TIMELINE_ITEMS = 50;
+
+function validateMaxItems(items: number): number {
+  return Math.min(items, MAX_TIMELINE_ITEMS);
+}
+
 async function getFollowingTimeline(
   userId: string,
   maxItems: number,
   auth: TwitterAuth,
   cursor?: string,
 ): Promise<RelationshipTimeline> {
   if (!auth.isLoggedIn()) {
     throw new Error('Scraper is not logged-in for profile following.');
   }
-  if (maxItems > 50) {
-    maxItems = 50;
-  }
+  maxItems = validateMaxItems(maxItems);

Also applies to: 146-147

🧰 Tools
🪛 eslint

[error] 10-12: Delete ⏎⏎

(prettier/prettier)


Line range hint 93-101: Extract common API features configuration

The API features configuration is duplicated across timeline functions. Consider extracting it to a shared constant.

+const TIMELINE_API_FEATURES = {
+  responsive_web_twitter_article_tweet_consumption_enabled: false,
+  tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
+  longform_notetweets_inline_media_enabled: true,
+  responsive_web_media_download_video_enabled: false,
+};
+
 async function getFollowingTimeline(
   // ... parameters ...
 ): Promise<RelationshipTimeline> {
   // ... auth check and maxItems validation ...
 
-  const features = addApiFeatures({
-    responsive_web_twitter_article_tweet_consumption_enabled: false,
-    tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
-    longform_notetweets_inline_media_enabled: true,
-    responsive_web_media_download_video_enabled: false,
-  });
+  const features = addApiFeatures(TIMELINE_API_FEATURES);

Also applies to: 150-158

🧰 Tools
🪛 eslint

[error] 10-12: Delete ⏎⏎

(prettier/prettier)


Line range hint 183-248: Enhance error handling in followUser function

The error handling in the followUser function could be improved:

  1. Consider adding specific error types for different failure scenarios
  2. Add retry logic for transient failures
  3. Add rate limiting handling
+interface TwitterError {
+  errors?: Array<{ code: number; message: string }>;
+}
+
+class FollowError extends Error {
+  constructor(
+    message: string,
+    public readonly code?: number,
+    public readonly response?: Response
+  ) {
+    super(message);
+  }
+}
+
 export async function followUser(
   username: string,
   auth: TwitterAuth,
+  retries = 3
 ): Promise<Response> {
   // Check if the user is logged in
   if (!(await auth.isLoggedIn())) {
-    throw new Error('Must be logged in to follow users');
+    throw new FollowError('Authentication required to follow users', 401);
   }
 
   // Get user ID from username
   const userIdResult = await getUserIdByScreenName(username, auth);
   if (!userIdResult.success) {
-    throw new Error(`Failed to get user ID: ${userIdResult.err.message}`);
+    throw new FollowError(
+      `Failed to resolve user ID for ${username}`,
+      404,
+      userIdResult.err
+    );
   }
 
+  let lastError: Error | null = null;
+  for (let attempt = 1; attempt <= retries; attempt++) {
+    try {
+      return await attemptFollow(username, userIdResult.value, auth);
+    } catch (error) {
+      lastError = error;
+      if (error instanceof FollowError && error.code === 429) {
+        // Rate limited - wait before retry
+        await new Promise(resolve => setTimeout(resolve, attempt * 1000));
+        continue;
+      }
+      throw error;
+    }
+  }
+  throw lastError || new FollowError('Max retries exceeded');
+}
+
+async function attemptFollow(
+  username: string,
+  userId: string,
+  auth: TwitterAuth
+): Promise<Response> {
   // ... rest of the implementation ...
 }
🧰 Tools
🪛 eslint

[error] 10-12: Delete ⏎⏎

(prettier/prettier)

src/scraper/scraper.ts (2)

105-108: Update import path for createGrokConversation and grokChat

The functions createGrokConversation and grokChat are imported from '../grok', which is a relative path. To maintain consistency with the use of absolute imports, consider updating this import statement to use the @/ alias if applicable.

Apply this diff to update the import path:

-import {
-  createGrokConversation,
-  grokChat,
-  GrokChatOptions,
-  GrokChatResponse,
-} from '../grok';
+import {
+  createGrokConversation,
+  grokChat,
+  GrokChatOptions,
+  GrokChatResponse,
+} from '@/grok';

Line range hint 362-367: Deprecation warnings for withCookie and withXCsrfToken methods

The methods withCookie and withXCsrfToken are marked as deprecated and include console warnings. Consider removing these methods in a future release to clean up the codebase and prevent their use.

Apply this diff to deprecate the methods properly:

 /**
  * Sets the optional cookie to be used in requests.
  * @param _cookie The cookie to be used in requests.
- * @deprecated This function no longer represents any part of Twitter's auth flow.
+ * @deprecated Use Scraper#setCookies instead.
  * @returns This scraper instance.
  */

And consider adding the @deprecated annotation in TypeScript to provide better tooling support.

src/features/spaces/core/ChatClient.ts (3)

2-2: Maintain consistent import ordering

The import statements have been reordered. To improve code readability, group similar imports together and maintain a consistent ordering, such as:

  • Node.js built-in modules
  • Third-party modules
  • Local modules

Apply this diff to reorder the imports:

 import { EventEmitter } from 'events';
 import { Logger } from '../logger';
 import type { OccupancyUpdate, SpeakerRequest } from '../types';
+import WebSocket from 'ws';

Alternatively, if WebSocket is a third-party module, it should be grouped with other third-party imports.


81-99: Optional chaining unnecessary when WebSocket instance is guaranteed

In the setupHandlers method, using optional chaining ?. on this.ws might be unnecessary since a check is performed earlier to ensure this.ws is available.

Consider replacing this.ws?.on with this.ws.on to reflect that this.ws is indeed defined at this point.

Apply this diff:

-          this.ws?.on('open', () => {
+          this.ws.on('open', () => {

Repeat this change for all subsequent event handler registrations within the method.


Line range hint 185-187: Improve error handling in safeJson function

Currently, the safeJson function silently fails and returns null on invalid JSON. Consider logging the error or providing more context to aid in debugging.

Apply this diff to log the error:

 function safeJson(text: string): any {
   try {
     return JSON.parse(text);
   } catch (error) {
+    console.error('[ChatClient] Failed to parse JSON:', error);
     return null;
   }
 }

Alternatively, use the Logger instance for consistent logging:

 function safeJson(text: string): any {
   try {
     return JSON.parse(text);
   } catch (error) {
+    this.logger.error('[ChatClient] Failed to parse JSON:', error);
     return null;
   }
 }

Note: Ensure safeJson has access to the logger instance if using it.

src/timeline/types/timeline-v2.ts (1)

1-5: Fix quote style to maintain consistency.

The import statements use double quotes, but the project style guide (as indicated by ESLint) prefers single quotes.

Apply this diff to fix the quote style:

-import { isFieldDefined } from "@/core/utils";
-import { LegacyUserRaw } from "@/features/profiles";
-import { Tweet } from "@/features/tweets";
-import { parseMediaGroups, reconstructTweetHtml } from "../components";
-import { LegacyTweetRaw, ParseTweetResult, QueryTweetsResponse, SearchResultRaw, TimelineResultRaw } from "./timeline-v1";
+import { isFieldDefined } from '@/core/utils';
+import { LegacyUserRaw } from '@/features/profiles';
+import { Tweet } from '@/features/tweets';
+import { parseMediaGroups, reconstructTweetHtml } from '../components';
+import {
+  LegacyTweetRaw,
+  ParseTweetResult,
+  QueryTweetsResponse,
+  SearchResultRaw,
+  TimelineResultRaw,
+} from './timeline-v1';
🧰 Tools
🪛 eslint

[error] 1-1: Replace "@/core/utils" with '@/core/utils'

(prettier/prettier)


[error] 2-2: Replace "@/features/profiles" with '@/features/profiles'

(prettier/prettier)


[error] 3-3: Replace "@/features/tweets" with '@/features/tweets'

(prettier/prettier)


[error] 4-4: Replace "../components" with '../components'

(prettier/prettier)

src/cli/command.ts (1)

Line range hint 1-16: Consider using a dedicated platform detection module.

The platform detection logic could be moved to a separate module to improve maintainability and reusability.

Consider creating a new file src/core/platform.ts:

// src/core/platform.ts
export const PLATFORM_NODE = typeof process !== 'undefined' && (
  // Node.js check
  (process.versions?.node != null) ||
  // Bun check
  (process.versions?.bun != null)
);

export const PLATFORM_NODE_JEST = false;

Then update the imports:

-// Declare the types for our custom global properties
-declare global {
-  var PLATFORM_NODE: boolean;
-  var PLATFORM_NODE_JEST: boolean;
-}
-
-// Define platform constants before imports
-globalThis.PLATFORM_NODE = typeof process !== 'undefined' && (
-  // Node.js check
-  (process.versions?.node != null) ||
-  // Bun check
-  (process.versions?.bun != null)
-);
-globalThis.PLATFORM_NODE_JEST = false;
+import { PLATFORM_NODE, PLATFORM_NODE_JEST } from '@/core/platform';
tsconfig.json (1)

Line range hint 3-24: LGTM! Path alias configuration is well structured.

The configuration properly sets up absolute imports with the @/* alias, which will help standardize import paths across the codebase. The baseUrl and paths settings are correctly configured for TypeScript path resolution.

Consider adding a trailing comma after the paths object for consistency with the rest of the file:

    "paths": {
      "@/*": ["src/*"]
-    }
+    },
src/timeline/components/timeline-tweet-util.ts (1)

1-3: Update quotes to use single quotes for consistency.

The import statements are correctly using the new path aliases, which improves code organization. However, the quote style should be standardized.

-import { isFieldDefined, NonNullableField } from "@/core/utils";
-import { Photo, Video } from "@/features/tweets";
-import { LegacyTweetRaw, TimelineMediaExtendedRaw } from "../types";
+import { isFieldDefined, NonNullableField } from '@/core/utils';
+import { Photo, Video } from '@/features/tweets';
+import { LegacyTweetRaw, TimelineMediaExtendedRaw } from '../types';
🧰 Tools
🪛 eslint

[error] 1-1: Replace "@/core/utils" with '@/core/utils'

(prettier/prettier)


[error] 2-2: Replace "@/features/tweets" with '@/features/tweets'

(prettier/prettier)

src/features/tweets/tweets.test.ts (2)

Line range hint 1-11: Consider enhancing security for API credentials in tests.

While the test properly checks for environment variables before running API tests, it's recommended to:

  1. Use a mock API service for tests to avoid requiring real credentials
  2. Document the required environment variables in the README
  3. Provide example values for the environment variables
🧰 Tools
🪛 eslint

[error] 7-8: Delete

(prettier/prettier)


Line range hint 32-38: Add error handling for API rate limits.

The test for getting tweets from a list should handle potential API rate limits.

 const res: QueryTweetsResponse = await scraper.fetchListTweets(
   '1736495155002106192',
   maxTweets,
   cursor,
 );
+  if (res.tweets.length === 0) {
+    console.warn('No tweets returned. Possible rate limit.');
+  }
🧰 Tools
🪛 eslint

[error] 7-8: Delete

(prettier/prettier)

src/features/tweets/tweets.ts (1)

481-481: Consider adding rate limiting mechanism.

The API interactions could benefit from a rate limiting mechanism to prevent hitting Twitter's API limits.

Consider implementing a rate limiter utility:

// src/core/utils/rate-limiter.ts
export class RateLimiter {
  private timestamps: number[] = [];
  constructor(private windowMs: number, private maxRequests: number) {}

  async throttle(): Promise<void> {
    const now = Date.now();
    this.timestamps = this.timestamps.filter(t => now - t < this.windowMs);
    if (this.timestamps.length >= this.maxRequests) {
      const oldestTimestamp = this.timestamps[0];
      const waitTime = this.windowMs - (now - oldestTimestamp);
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }
    this.timestamps.push(now);
  }
}
src/timeline/components/timeline-search.ts (1)

1-5: Import paths have been properly reorganized.

The changes align well with the modular architecture, separating core utilities and feature-specific imports. However, consider using consistent import quote styles (single vs double quotes) across the codebase.

-import { isFieldDefined } from "@/core/utils";
-import { LegacyUserRaw, parseProfile, Profile } from "@/features/profiles";
-import { PlaceRaw, Tweet } from "@/features/tweets";
-import { parseMediaGroups, reconstructTweetHtml } from "../components";
+import { isFieldDefined } from '@/core/utils';
+import { LegacyUserRaw, parseProfile, Profile } from '@/features/profiles';
+import { PlaceRaw, Tweet } from '@/features/tweets';
+import { parseMediaGroups, reconstructTweetHtml } from '../components';
🧰 Tools
🪛 eslint

[error] 1-2: Delete

(prettier/prettier)


[error] 4-4: Replace ·QueryProfilesResponse,·QueryTweetsResponse· with ⏎··QueryProfilesResponse,⏎··QueryTweetsResponse,⏎

(prettier/prettier)

src/grok.ts (1)

1-2: Core functionality properly organized into dedicated modules.

The changes correctly move core functionality into a dedicated core directory. However, consider using consistent import quote styles (single vs double quotes) across the codebase.

-import { requestApi } from "./core/api";
-import { TwitterAuth } from "./core/auth";
+import { requestApi } from './core/api';
+import { TwitterAuth } from './core/auth';
🧰 Tools
🪛 eslint

[error] 1-1: Replace "./core/api" with './core/api'

(prettier/prettier)

src/timeline/types/timeline-v1.ts (1)

1-5: Import paths properly organized into feature modules.

The changes successfully separate core utilities and feature-specific types into their respective modules. However, consider using consistent import quote styles (single vs double quotes) across the codebase.

-import { isFieldDefined } from "@/core/utils";
-import { LegacyUserRaw, parseProfile, Profile } from "@/features/profiles";
-import { PlaceRaw, Tweet } from "@/features/tweets";
-import { parseMediaGroups, reconstructTweetHtml } from "../components";
+import { isFieldDefined } from '@/core/utils';
+import { LegacyUserRaw, parseProfile, Profile } from '@/features/profiles';
+import { PlaceRaw, Tweet } from '@/features/tweets';
+import { parseMediaGroups, reconstructTweetHtml } from '../components';
🧰 Tools
🪛 eslint

[error] 1-1: Replace "@/core/utils" with '@/core/utils'

(prettier/prettier)


[error] 2-2: Replace "@/features/profiles" with '@/features/profiles'

(prettier/prettier)


[error] 3-3: Replace "@/features/tweets" with '@/features/tweets'

(prettier/prettier)


[error] 4-5: Replace "../components";⏎ with '../components';

(prettier/prettier)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6678581 and 3deb681.

📒 Files selected for processing (67)
  • rollup.config.mjs (3 hunks)
  • src/_module.ts (0 hunks)
  • src/cli/command.ts (1 hunks)
  • src/cli/index.ts (1 hunks)
  • src/core/api/api.ts (1 hunks)
  • src/core/api/index.ts (1 hunks)
  • src/core/auth/auth-user.ts (1 hunks)
  • src/core/auth/auth.test.ts (1 hunks)
  • src/core/auth/auth.ts (1 hunks)
  • src/core/auth/index.ts (1 hunks)
  • src/core/types/index.ts (1 hunks)
  • src/core/utils/index.ts (1 hunks)
  • src/core/utils/test-utils.ts (1 hunks)
  • src/features/index.ts (1 hunks)
  • src/features/messages/index.ts (1 hunks)
  • src/features/messages/messages.test.ts (1 hunks)
  • src/features/messages/messages.ts (1 hunks)
  • src/features/profiles/index.ts (1 hunks)
  • src/features/profiles/profile.test.ts (1 hunks)
  • src/features/profiles/profile.ts (1 hunks)
  • src/features/relationships/index.ts (1 hunks)
  • src/features/relationships/relationships.test.ts (1 hunks)
  • src/features/relationships/relationships.ts (1 hunks)
  • src/features/search/index.ts (1 hunks)
  • src/features/search/search.test.ts (1 hunks)
  • src/features/search/search.ts (1 hunks)
  • src/features/spaces/core/ChatClient.ts (2 hunks)
  • src/features/spaces/core/JanusAudio.ts (1 hunks)
  • src/features/spaces/core/JanusClient.ts (3 hunks)
  • src/features/spaces/core/Space.ts (1 hunks)
  • src/features/spaces/core/SpaceParticipant.ts (1 hunks)
  • src/features/spaces/core/index.ts (1 hunks)
  • src/features/spaces/index.ts (1 hunks)
  • src/features/spaces/logger.ts (0 hunks)
  • src/features/spaces/plugins/HlsRecordPlugin.ts (9 hunks)
  • src/features/spaces/plugins/IdleMonitorPlugin.ts (3 hunks)
  • src/features/spaces/plugins/MonitorAudioPlugin.ts (4 hunks)
  • src/features/spaces/plugins/index.ts (1 hunks)
  • src/features/spaces/test.ts (1 hunks)
  • src/features/spaces/testParticipant.ts (1 hunks)
  • src/features/spaces/types.ts (0 hunks)
  • src/features/spaces/utils.ts (0 hunks)
  • src/features/trends/index.ts (1 hunks)
  • src/features/trends/trends.test.ts (1 hunks)
  • src/features/trends/trends.ts (1 hunks)
  • src/features/tweets/index.ts (1 hunks)
  • src/features/tweets/tweets.test.ts (1 hunks)
  • src/features/tweets/tweets.ts (3 hunks)
  • src/grok.ts (1 hunks)
  • src/index.ts (1 hunks)
  • src/scraper/index.ts (1 hunks)
  • src/scraper/scraper.test.ts (1 hunks)
  • src/scraper/scraper.ts (2 hunks)
  • src/spaces.ts (2 hunks)
  • src/timeline/components/index.ts (1 hunks)
  • src/timeline/components/timeline-async.ts (1 hunks)
  • src/timeline/components/timeline-following.ts (1 hunks)
  • src/timeline/components/timeline-home.ts (1 hunks)
  • src/timeline/components/timeline-list.ts (1 hunks)
  • src/timeline/components/timeline-relationship.ts (1 hunks)
  • src/timeline/components/timeline-search.ts (1 hunks)
  • src/timeline/components/timeline-tweet-util.ts (1 hunks)
  • src/timeline/index.ts (1 hunks)
  • src/timeline/types/index.ts (1 hunks)
  • src/timeline/types/timeline-v1.ts (1 hunks)
  • src/timeline/types/timeline-v2.ts (1 hunks)
  • tsconfig.json (2 hunks)
💤 Files with no reviewable changes (4)
  • src/features/spaces/logger.ts
  • src/features/spaces/types.ts
  • src/features/spaces/utils.ts
  • src/_module.ts
✅ Files skipped from review due to trivial changes (47)
  • src/features/messages/index.ts
  • src/core/types/index.ts
  • src/features/profiles/index.ts
  • src/features/relationships/index.ts
  • src/features/tweets/index.ts
  • src/features/search/index.ts
  • src/cli/index.ts
  • src/core/auth/index.ts
  • src/core/utils/index.ts
  • src/features/spaces/plugins/IdleMonitorPlugin.ts
  • src/features/spaces/core/index.ts
  • src/features/spaces/core/JanusAudio.ts
  • src/features/spaces/plugins/HlsRecordPlugin.ts
  • src/features/spaces/index.ts
  • src/features/trends/trends.test.ts
  • src/features/spaces/core/SpaceParticipant.ts
  • src/features/messages/messages.ts
  • src/features/spaces/plugins/index.ts
  • src/features/relationships/relationships.test.ts
  • src/scraper/scraper.test.ts
  • src/features/spaces/core/Space.ts
  • src/core/api/index.ts
  • src/features/spaces/testParticipant.ts
  • src/core/auth/auth.test.ts
  • src/features/profiles/profile.test.ts
  • src/features/search/search.test.ts
  • rollup.config.mjs
  • src/features/messages/messages.test.ts
  • src/features/index.ts
  • src/features/trends/index.ts
  • src/scraper/index.ts
  • src/features/trends/trends.ts
  • src/timeline/components/timeline-list.ts
  • src/core/utils/test-utils.ts
  • src/spaces.ts
  • src/core/auth/auth.ts
  • src/timeline/types/index.ts
  • src/timeline/index.ts
  • src/timeline/components/timeline-following.ts
  • src/core/api/api.ts
  • src/timeline/components/timeline-home.ts
  • src/timeline/components/timeline-relationship.ts
  • src/timeline/components/timeline-async.ts
  • src/features/profiles/profile.ts
  • src/features/search/search.ts
  • src/features/spaces/core/JanusClient.ts
  • src/timeline/components/index.ts
🧰 Additional context used
🪛 eslint
src/timeline/types/timeline-v2.ts

[error] 1-1: Replace "@/core/utils" with '@/core/utils'

(prettier/prettier)


[error] 2-2: Replace "@/features/profiles" with '@/features/profiles'

(prettier/prettier)


[error] 3-3: Replace "@/features/tweets" with '@/features/tweets'

(prettier/prettier)


[error] 4-4: Replace "../components" with '../components'

(prettier/prettier)


[error] 5-6: Replace ·LegacyTweetRaw,·ParseTweetResult,·QueryTweetsResponse,·SearchResultRaw,·TimelineResultRaw·}·from·"./timeline-v1";⏎ with ⏎··LegacyTweetRaw,⏎··ParseTweetResult,⏎··QueryTweetsResponse,⏎··SearchResultRaw,⏎··TimelineResultRaw,⏎}·from·'./timeline-v1';

(prettier/prettier)

src/grok.ts

[error] 1-1: Replace "./core/api" with './core/api'

(prettier/prettier)


[error] 2-3: Replace "./core/auth";⏎ with './core/auth';

(prettier/prettier)

src/timeline/components/timeline-search.ts

[error] 1-2: Delete

(prettier/prettier)


[error] 4-4: Replace ·QueryProfilesResponse,·QueryTweetsResponse· with ⏎··QueryProfilesResponse,⏎··QueryTweetsResponse,⏎

(prettier/prettier)

src/timeline/components/timeline-tweet-util.ts

[error] 1-1: Replace "@/core/utils" with '@/core/utils'

(prettier/prettier)


[error] 2-2: Replace "@/features/tweets" with '@/features/tweets'

(prettier/prettier)


[error] 3-4: Replace "../types";⏎ with '../types';

(prettier/prettier)

src/timeline/types/timeline-v1.ts

[error] 1-1: Replace "@/core/utils" with '@/core/utils'

(prettier/prettier)


[error] 2-2: Replace "@/features/profiles" with '@/features/profiles'

(prettier/prettier)


[error] 3-3: Replace "@/features/tweets" with '@/features/tweets'

(prettier/prettier)


[error] 4-5: Replace "../components";⏎ with '../components';

(prettier/prettier)

🔇 Additional comments (9)
src/scraper/scraper.ts (4)

6-12: Consistent usage of absolute import paths throughout the codebase

The transition from relative imports to absolute imports using the @/ alias enhances code readability and maintainability. This change aligns with the project's objective to improve the modular structure of the codebase.

Also applies to: 14-19, 21-25, 29-31, 33-38, 45-46, 48-72


74-81: Ensure that the @/spaces module exports required functions

The imports from '@/spaces' for functions like fetchAudioSpaceById, fetchAuthenticatePeriscope, etc., indicate that these functions have been moved to the spaces module. Verify that the @/spaces module correctly exports these functions to prevent any runtime import errors.


81-90: Check for compatibility with timeline-related imports

The imports related to timelines, such as fetchFollowingTimeline, fetchHomeTimeline, and others, have been updated to use absolute paths from '@/timeline'. Ensure that these functions are properly defined and exported in the timeline module, and that any dependencies are correctly resolved.


Line range hint 143-146: Verify the hardcoded URLs and constants

The URLs such as twUrl and UserTweetsUrl are hardcoded. Ensure that these endpoints are still valid and have not changed, especially given Twitter's frequent API updates.

Run the following script to check the validity of the URLs:

✅ Verification successful

The hardcoded URLs and GraphQL endpoints are valid and current

The URLs follow Twitter's current API structure and are consistently used across multiple files in the codebase. The 403 responses are expected as these endpoints require authentication, not because they're invalid. The GraphQL operation ID is actively used in the production code, indicating it's a current endpoint.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the hardcoded URLs are still valid.

# Check the main Twitter URL
curl -I https://twitter.com -o /dev/null -w '%{http_code}\n' -s

# Check the UserTweets API endpoint
curl -I 'https://twitter.com/i/api/graphql/E3opETHurmVJflFsUBVuUQ/UserTweets' -o /dev/null -w '%{http_code}\n' -s

Length of output: 187


Script:

#!/bin/bash
# Search for the GraphQL operation ID and URL patterns
echo "=== Searching for GraphQL operation ID ==="
rg "E3opETHurmVJflFsUBVuUQ" -l

echo -e "\n=== Searching for Twitter API URL patterns ==="
rg "twitter\.com/i/api/graphql" -l

echo -e "\n=== Looking for URL configurations ==="
fd -e json -e ts -e js -e yaml -e yml . | xargs rg "twUrl|UserTweetsUrl|baseUrl"

echo -e "\n=== Checking recent changes to scraper.ts ==="
git log -n 5 --oneline src/scraper/scraper.ts

Length of output: 1337

src/features/spaces/test.ts (1)

1-1: LGTM! Import paths updated to match new project structure.

The changes align with the PR objective of using absolute imports for better clarity and maintainability.

Also applies to: 4-5

src/core/auth/auth-user.ts (1)

1-1: LGTM! Import paths updated to maintain clear module boundaries.

The changes appropriately use:

  • Absolute paths (@/) for feature imports
  • Relative paths for core functionality imports

Also applies to: 7-9

src/cli/command.ts (1)

17-22: LGTM! Import paths updated to match new project structure.

The changes correctly use relative paths for importing from the new feature-based modules.

src/features/tweets/tweets.ts (1)

1-17: LGTM! Import organization is clean and follows the new structure.

The imports are properly organized using the new path aliases, improving code maintainability.

src/timeline/types/timeline-v1.ts (1)

1-5: Verify consistent usage of path aliases across the codebase.

Let's verify that all imports consistently use the new path aliases and identify any remaining relative imports that should be updated.

✅ Verification successful

Import paths follow consistent conventions

The codebase follows a clear pattern where '@/' aliases are used for cross-feature imports while relative paths are used for imports within the same feature. The imports in the reviewed file adhere to these conventions.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for inconsistent import paths

# Find relative imports that could be using path aliases
rg -g '*.ts' -g '*.tsx' "from '\.\./|from \"\.\." --no-heading

# Find inconsistent usage of path aliases
rg -g '*.ts' -g '*.tsx' "from ['\"]((@/[^'\"]+)|([^./@][^'\"]+))" --no-heading

Length of output: 13711

🧰 Tools
🪛 eslint

[error] 1-1: Replace "@/core/utils" with '@/core/utils'

(prettier/prettier)


[error] 2-2: Replace "@/features/profiles" with '@/features/profiles'

(prettier/prettier)


[error] 3-3: Replace "@/features/tweets" with '@/features/tweets'

(prettier/prettier)


[error] 4-5: Replace "../components";⏎ with '../components';

(prettier/prettier)

Comment on lines 60 to 64
onAudioData(data: AudioDataWithUser): void {
// Log debug info
this.logger.debug(
`[MonitorAudioPlugin] onAudioData => userId=${data.userId}, samples=${data.samples.length}, sampleRate=${data.sampleRate}`,
`[MonitorAudioPlugin] onAudioData => userId=${data.userId}, samples=${data.samples.length}, sampleRate=${data.sampleRate}`,
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add sample rate validation and error handling.

The onAudioData method makes assumptions about sample rates and lacks error handling for buffer operations.

Consider adding these safety checks:

  onAudioData(data: AudioDataWithUser): void {
+   if (data.sampleRate !== this.sampleRate) {
+     this.logger.error(
+       `[MonitorAudioPlugin] Sample rate mismatch: expected ${this.sampleRate}, got ${data.sampleRate}`
+     );
+     return;
+   }

    this.logger.debug(
      `[MonitorAudioPlugin] onAudioData => userId=${data.userId}, samples=${data.samples.length}, sampleRate=${data.sampleRate}`,
    );

    if (!this.ffplay?.stdin.writable) {
      return;
    }

    const pcmBuffer = Buffer.from(data.samples.buffer);
-   this.ffplay.stdin.write(pcmBuffer);
+   try {
+     const success = this.ffplay.stdin.write(pcmBuffer);
+     if (!success) {
+       this.logger.warn('[MonitorAudioPlugin] Buffer full, may need to implement backpressure handling');
+     }
+   } catch (error) {
+     this.logger.error('[MonitorAudioPlugin] Error writing to ffplay:', error);
+   }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1 to +8
import { addApiFeatures, bearerToken, requestApi } from '@/core/api';
import { TwitterAuth } from '@/core/auth';
import { parseRelationshipTimeline, RelationshipTimeline } from '@/timeline';
import { getUserTimeline } from '@/timeline/components/timeline-async';
import { QueryProfilesResponse } from '@/timeline/types/timeline-v1';
import { Headers } from 'headers-polyfill';
import { TwitterAuth } from './auth';
import { Profile, getUserIdByScreenName } from './profile';
import { QueryProfilesResponse } from './timeline-v1';
import { getUserTimeline } from './timeline-async';
import {
RelationshipTimeline,
parseRelationshipTimeline,
} from './timeline-relationship';
import stringify from 'json-stable-stringify';
import { getUserIdByScreenName, Profile } from '../profiles';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Convert remaining relative import to absolute path

Per the PR objectives of standardizing imports to use absolute paths with the @/ prefix, the relative import from ../profiles should be updated to maintain consistency.

-import { getUserIdByScreenName, Profile } from '../profiles';
+import { getUserIdByScreenName, Profile } from '@/features/profiles';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { addApiFeatures, bearerToken, requestApi } from '@/core/api';
import { TwitterAuth } from '@/core/auth';
import { parseRelationshipTimeline, RelationshipTimeline } from '@/timeline';
import { getUserTimeline } from '@/timeline/components/timeline-async';
import { QueryProfilesResponse } from '@/timeline/types/timeline-v1';
import { Headers } from 'headers-polyfill';
import { TwitterAuth } from './auth';
import { Profile, getUserIdByScreenName } from './profile';
import { QueryProfilesResponse } from './timeline-v1';
import { getUserTimeline } from './timeline-async';
import {
RelationshipTimeline,
parseRelationshipTimeline,
} from './timeline-relationship';
import stringify from 'json-stable-stringify';
import { getUserIdByScreenName, Profile } from '../profiles';
import { addApiFeatures, bearerToken, requestApi } from '@/core/api';
import { TwitterAuth } from '@/core/auth';
import { parseRelationshipTimeline, RelationshipTimeline } from '@/timeline';
import { getUserTimeline } from '@/timeline/components/timeline-async';
import { QueryProfilesResponse } from '@/timeline/types/timeline-v1';
import { Headers } from 'headers-polyfill';
import stringify from 'json-stable-stringify';
import { getUserIdByScreenName, Profile } from '@/features/profiles';

src/index.ts Outdated
Comment on lines 1 to 9
export { Scraper } from './scraper';

export * from '@/cli';
// Not sure if this is needed
// export * from '@/core';
export * from '@/features';
export * from '@/scraper';
export * from '@/spaces';
export * from '@/timeline';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Uncomment and keep the core module export

The @/core module contains essential utilities, API handling, authentication, and types that are heavily used throughout the codebase. The export should be uncommented to maintain a clean public API.

 export { Scraper } from './scraper';

 export * from '@/cli';
-// Not sure if this is needed
-// export * from '@/core';
+export * from '@/core';
 export * from '@/features';
 export * from '@/scraper';
 export * from '@/spaces';
 export * from '@/timeline';
🔗 Analysis chain

Consolidate exports and remove unnecessary comments

The consolidation of exports in src/index.ts enhances the module's accessibility. However, the commented-out export of @/core indicates uncertainty.

  • Confirm if re-exporting from @/core is necessary. If not, consider removing the comment to clean up the code.

Apply this diff to remove the unnecessary comment:

 export { Scraper } from './scraper';

 export * from '@/cli';
-// Not sure if this is needed
-// export * from '@/core';
 export * from '@/features';
 export * from '@/scraper';
 export * from '@/spaces';
 export * from '@/timeline';

Ensure that all necessary exports are included and that the application functions as expected without the core exports.

🏁 Scripts executed

The following scripts were executed for the analysis:

Error: No shell scripts found


Script:

#!/bin/bash
# Check if core module exists and show its contents
echo "=== Checking core module ==="
fd -t f "index.ts" -p "core/"

echo -e "\n=== Looking for @/core imports ==="
rg "@/core" --type ts

echo -e "\n=== Looking for package imports ==="
# This will help us understand if anyone is importing from the root package
rg "from '[^@].*'" --type ts

Length of output: 16242

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/index.ts (1)

1-1: Consider standardizing to absolute import path.

For consistency with other imports and the PR's objective of using absolute paths, consider updating to @/scraper. However, since this is the main entry point, the current relative import is also acceptable.

-export { Scraper } from './scraper';
+export { Scraper } from '@/scraper';
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3deb681 and e33e625.

📒 Files selected for processing (1)
  • src/index.ts (1 hunks)
🔇 Additional comments (2)
src/index.ts (2)

1-9: Well-organized module structure!

The export structure successfully implements a clean, modular API that aligns with the PR's objectives. The feature-based organization and consistent use of absolute imports (with one minor exception) will improve code maintainability.


3-8: Verify potential circular dependencies and build impact.

While the module organization looks good, there are two concerns to address:

  1. Potential circular dependency between the individual Scraper export and @/scraper module export
  2. Impact of these exports on the build process

Let's verify the import patterns and potential issues:

✅ Verification successful

Export structure is clean and well-organized

The verification shows:

  • No circular dependencies between Scraper exports
  • Consistent import patterns across the codebase
  • Proper build configuration supporting the module structure
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for potential circular dependencies and import patterns

echo "=== Checking for circular dependencies ==="
# Look for imports of Scraper in scraper module
rg "import.*Scraper.*from.*['\"]@/scraper['\"]" --type ts

echo -e "\n=== Analyzing import patterns ==="
# Check if any files are importing both individual Scraper and from @/scraper
rg "import.*Scraper.*from" --type ts

echo -e "\n=== Verifying build configuration ==="
# Check if rollup config handles these exports correctly
fd -g "rollup.config.*" -x cat {}

Length of output: 3105

@wtfsayo
Copy link
Member

wtfsayo commented Jan 13, 2025

okay impossible to review! please send progressively in smaller PRs

@wtfsayo wtfsayo closed this Jan 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants