Skip to content

Commit

Permalink
feat(instagram): implement instagram watcher (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
async3619 authored Nov 1, 2024
1 parent 9a8a474 commit a679f23
Show file tree
Hide file tree
Showing 7 changed files with 564 additions and 14 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ services:
| Twitter | ✅ |
| GitHub | ✅ |
| Mastodon | ✅ |
| Instagram | |
| Instagram | |
| TikTok | ❌ |
| YouTube | ❌ |
| Twitch | ❌ |
Expand Down Expand Up @@ -149,6 +149,22 @@ watcher configuration for GitHub service.
}
```

#### instagram

watcher configuration for Instagram service.

```json5
{
"type": "instagram", // required
"username": "your instagram username", // string, required
"password": "your instagram password", // string, required
"targetUserName": "target instagram username you want to crawl followers", // string, required
"requestDelay": 1000 // number, optional, default: 1000
// requestDelay is the delay time between each request to instagram server.
// this is to prevent getting banned from instagram server.
}
```

### notifiers: `Record<string, NotifierOptions>` (required)

#### discord
Expand Down
28 changes: 28 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,34 @@
"type"
],
"additionalProperties": false
},
"instagram": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "instagram"
},
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"targetUserName": {
"type": "string"
},
"requestDelay": {
"type": "number"
}
},
"required": [
"password",
"targetUserName",
"type",
"username"
],
"additionalProperties": false
}
},
"additionalProperties": false
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"fs-extra": "^11.1.0",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"instagram-private-api": "^1.46.1",
"lodash": "^4.17.21",
"masto": "^5.11.3",
"node-cron": "^3.0.2",
Expand Down
2 changes: 2 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Logger } from "@utils/logger";

export type Resolve<T> = T extends Promise<infer U> ? U : T;

export type SelectOnly<Record, Type extends Record[keyof Record]> = {
[Key in keyof Required<Record> as Required<Record>[Key] extends Type ? Key : never]: Record[Key];
};
Expand Down
7 changes: 5 additions & 2 deletions src/watchers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { MastodonWatcher, MastodonWatcherOptions } from "@watchers/mastodon";
import { BlueSkyWatcher, BlueSkyWatcherOptions } from "@watchers/bluesky";

import { TypeMap } from "@utils/types";
import { InstagramWatcher, InstagramWatcherOptions } from "@watchers/instagram";

export type WatcherClasses = TwitterWatcher | GitHubWatcher | MastodonWatcher | BlueSkyWatcher;
export type WatcherClasses = TwitterWatcher | GitHubWatcher | MastodonWatcher | BlueSkyWatcher | InstagramWatcher;
export type WatcherTypes = Lowercase<WatcherClasses["name"]>;

export type WatcherOptions =
| TwitterWatcherOptions
| GitHubWatcherOptions
| MastodonWatcherOptions
| BlueSkyWatcherOptions;
| BlueSkyWatcherOptions
| InstagramWatcherOptions;

export type WatcherOptionMap = TypeMap<WatcherOptions>;

Expand All @@ -32,6 +34,7 @@ const AVAILABLE_WATCHERS: Readonly<WatcherFactoryMap> = {
github: options => new GitHubWatcher(options),
mastodon: options => new MastodonWatcher(options),
bluesky: options => new BlueSkyWatcher(options),
instagram: options => new InstagramWatcher(options),
};

export const createWatcher = (options: BaseWatcherOptions): BaseWatcher<string> => {
Expand Down
69 changes: 69 additions & 0 deletions src/watchers/instagram/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { BaseWatcher, BaseWatcherOptions, PartialUser } from "@watchers/base";
import { IgApiClient } from "instagram-private-api";
import { Resolve, sleep } from "@utils";

export interface InstagramWatcherOptions extends BaseWatcherOptions<InstagramWatcher> {
username: string;
password: string;
targetUserName: string;
requestDelay?: number;
}

export class InstagramWatcher extends BaseWatcher<"Instagram"> {
private readonly client = new IgApiClient();

private readonly username: string;
private readonly password: string;
private readonly targetUserName: string;
private readonly requestDelay: number;

private loggedInUser: Resolve<ReturnType<typeof this.client.account.login>> | null = null;

public constructor({ username, password, targetUserName, requestDelay = 1000 }: InstagramWatcherOptions) {
super("Instagram");

this.username = username;
this.password = password;
this.targetUserName = targetUserName;
this.requestDelay = requestDelay;
}

public async initialize() {
this.client.state.generateDevice(this.username);
await this.client.simulate.preLoginFlow();

this.loggedInUser = await this.client.account.login(this.username, this.password);

this.logger.verbose("Successfully initialized with user name {}", [this.loggedInUser.username]);
}

protected async getFollowers() {
// get followers
const id = await this.client.user.getIdByUsername(this.targetUserName);
if (!id) {
throw new Error("Failed to get user id");
}

const followersFeed = this.client.feed.accountFollowers(id);
const followers: PartialUser[] = [];
while (true) {
const items = await followersFeed.items();
followers.push(
...items.map(user => ({
uniqueId: user.pk.toString(),
displayName: user.full_name,
userId: user.username,
profileUrl: `https://instagram.com/${user.username}`,
})),
);

if (!followersFeed.isMoreAvailable()) {
break;
}

await sleep(this.requestDelay);
}

return followers;
}
}
Loading

0 comments on commit a679f23

Please sign in to comment.